diff --git a/BT-Panel b/BT-Panel index 2188b694..c75ae114 100644 --- a/BT-Panel +++ b/BT-Panel @@ -1,26 +1,36 @@ #!/www/server/panel/pyenv/bin/python #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- from gevent import monkey + + monkey.patch_all() -import os,sys,ssl,time,logging + + +import os +import sys +import ssl +import time +import logging +import psutil + _PATH = '/www/server/panel' os.chdir(_PATH) -os.system("nohup ./pyenv/bin/python3 class/jobs.py &>/dev/null &") -upgrade_file = 'script/upgrade_flask.sh' -if os.path.exists(upgrade_file): - os.system("nohup bash {} &>/dev/null &".format(upgrade_file)) -upgrade_file = 'script/upgrade_gevent.sh' -if os.path.exists(upgrade_file): - os.system("nohup bash {} &>/dev/null &".format(upgrade_file)) +# upgrade_file = 'script/upgrade_flask.sh' +# if os.path.exists(upgrade_file): +# os.system("nohup bash {} &>/dev/null &".format(upgrade_file)) +# +# upgrade_file = 'script/upgrade_gevent.sh' +# if os.path.exists(upgrade_file): +# os.system("nohup bash {} &>/dev/null &".format(upgrade_file)) upgrade_file = 'script/upgrade_telegram.sh' if os.path.exists(upgrade_file): @@ -44,13 +54,13 @@ def check_plugin_loader(): machine = os.uname().machine except: pass - plugin_loader_src_file = "class/PluginLoader.{}.Python3.7.so".format(machine) + plugin_loader_src_file = "class/PluginLoader.{}.Python3.12.so".format(machine) if machine == 'x86_64': glibc_version = public.get_glibc_version() if glibc_version in ['2.14','2.13','2.12','2.11','2.10']: - plugin_loader_src_file = "class/PluginLoader.{}.glibc214.Python3.7.so".format(machine) + plugin_loader_src_file = "class/PluginLoader.{}.glibc214.Python3.12.so".format(machine) if os.path.exists(plugin_loader_src_file): - os.system("\cp -f {} {}".format(plugin_loader_src_file, plugin_loader_file)) + os.system(r"\cp -f {} {}".format(plugin_loader_src_file, plugin_loader_file)) check_plugin_loader() @@ -67,7 +77,8 @@ if is_debug: re.compile('{}/(tmp|temp)/.+'.format(_PATH)), re.compile('{}/pyenv/.+'.format(_PATH)), re.compile('{}/class/projectModel/.+'.format(_PATH)), - re.compile('{}/class/databaseModel/.+'.format(_PATH)) + re.compile('{}/class/databaseModel/.+'.format(_PATH)), + re.compile('{}/panel/data/mail/in_bulk/content/.+'.format(_PATH)) ] _lsat_time = 0 @@ -155,6 +166,29 @@ def daemon_task(): run_task() continue +def get_process_count(): + ''' + @name 获取进程数量 + @return int + ''' + + # 如果存在用户配置,则直接返回用户配置的进程数量 + process_count_file = "{}/data/process_count.pl".format(_PATH) + if os.path.exists(process_count_file): + str_count = public.readFile(process_count_file).strip() + try: + if str_count: return int(str_count) + except: pass + + # 否则根据内存和CPU核心数来决定启动进程数量 + memory = psutil.virtual_memory().total / 1024 / 1024 + cpu_count = psutil.cpu_count() + if memory < 4000 or cpu_count < 4: return 1 # 内存小于4G或CPU核心小于4核,则只启动1个进程 + if memory < 8000 and cpu_count > 3: return 2 # 内存大于4G且小于8G,且CPU核心大于3核,则启动2个进程 + if memory > 14000 and cpu_count > 7: return 3 # 内存大于8G且14G,且CPU核心大于7核,则启动3个进程 + if memory > 30000 and cpu_count > 15: return 4 # 内存大于30G且CPU核心大于15核,则启动4个进程 + return 1 + if __name__ == '__main__': pid_file = "{}/logs/panel.pid".format(_PATH) if os.path.exists(pid_file): @@ -172,6 +206,8 @@ if __name__ == '__main__': sys.stdout.flush() sys.stderr.flush() + # 面板启动任务初始化 + os.system("nohup ./pyenv/bin/python3 class/jobs.py &>/dev/null &") try: f = open('data/port.pl') @@ -199,8 +235,7 @@ if __name__ == '__main__': print(ex) import threading - task_thread = threading.Thread(target=daemon_task) - task_thread.setDaemon(True) + task_thread = threading.Thread(target=daemon_task, daemon=True) task_thread.start() if is_ssl: @@ -232,18 +267,76 @@ if __name__ == '__main__': app.logger = logger from gevent.pywsgi import WSGIServer - try: - import flask_sock + import webserver + + class BtWSGIServer(WSGIServer): + def wrap_socket_and_handle(self, client_socket, address): + try: + return super(BtWSGIServer, self).wrap_socket_and_handle(client_socket, address) + except OSError as e: + pass + # public.print_exc_stack(e) + + def do_read(self): + try: + return super(BtWSGIServer, self).do_read() + except OSError as e: + pass + # public.print_exc_stack(e) + + webserver_obj = webserver.webserver() + is_webserver = webserver_obj.run_webserver() + # is_webserver = False + + if is_webserver: + from gevent import socket + listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + unix_socket = '/tmp/panel.sock' + if os.path.exists(unix_socket): + os.remove(unix_socket) + listener.bind(unix_socket) + listener.listen(500) + os.chmod(unix_socket, 0o777) + try: + import flask_sock + http_server = BtWSGIServer(listener, app,log=app.logger) + except: + from geventwebsocket.handler import WebSocketHandler + http_server = BtWSGIServer(listener, app,handler_class=WebSocketHandler,log=app.logger) + else: if is_ssl: - http_server = WSGIServer((HOST, PORT), app,ssl_context = ssl_context,log=app.logger) - else: - http_server = WSGIServer((HOST, PORT), app,log=app.logger) - except: - from geventwebsocket.handler import WebSocketHandler - if is_ssl: - http_server = WSGIServer((HOST, PORT), app,ssl_context = ssl_context,handler_class=WebSocketHandler,log=app.logger) - else: - http_server = WSGIServer((HOST, PORT), app,handler_class=WebSocketHandler,log=app.logger) + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) + if hasattr(ssl_context, "minimum_version"): + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + else: + ssl_context.options = (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1) + + ssl_context.set_ciphers("ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE") + is_ssl_verify = os.path.exists('/www/server/panel/data/ssl_verify_data.pl') + if is_ssl_verify: + crlfile = '/www/server/panel/ssl/crl.pem' + rootcafile = '/www/server/panel/ssl/ca.pem' + #注销列表 + # ssl_context.load_verify_locations(crlfile) + # ssl_context.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN + #加载证书 + ssl_context.load_verify_locations(rootcafile) + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.set_default_verify_paths() + + try: + import flask_sock + if is_ssl: + http_server = BtWSGIServer((HOST, PORT), app,ssl_context = ssl_context,log=app.logger) + else: + http_server = BtWSGIServer((HOST, PORT), app,log=app.logger) + except: + from geventwebsocket.handler import WebSocketHandler + if is_ssl: + http_server = BtWSGIServer((HOST, PORT), app,ssl_context = ssl_context,handler_class=WebSocketHandler,log=app.logger) + else: + http_server = BtWSGIServer((HOST, PORT), app,handler_class=WebSocketHandler,log=app.logger) if is_debug: @@ -258,20 +351,58 @@ if __name__ == '__main__': try: http_server.serve_forever() except: - pass + from traceback import format_exc + public.print_log(format_exc()) app.run(host=HOST, port=PORT, threaded=True) else: http_server.start() from multiprocessing import Process + + def serve_forever(): http_server.start_accepting() http_server._stop_event.wait() - process_count = 2 - for i in range(process_count): - p = Process(target=serve_forever) - p.daemon = True - p.start() + # 获取最大进程数量,最小为2个 + process_count = get_process_count() + if process_count < 2: process_count = 2 + + # 启动主进程 + main_p = Process(target=serve_forever) + main_p.daemon = True + main_p.start() + main_psutil = psutil.Process(main_p.pid) + + # 动态按需调整子进程数量 + process_dict = {} while 1: - time.sleep(1000) \ No newline at end of file + t = time.time() + # 当主进程CPU占用率超过90%时,尝试启动新的子进程协同处理 + cpu_percent = main_psutil.cpu_percent(interval=1) + if cpu_percent > 90: + is_alive = 0 + process_num = 0 + + # 检查是否存在空闲的子进程 + for i in process_dict.keys(): + process_num += 1 + if process_dict[i][2].cpu_percent(interval=1) > 0: + is_alive += 1 + + # 如果没有空闲的子进程,且当前子进程数量小于最大进程数量,则启动新的子进程 + if process_num == is_alive and process_num < process_count: + p = Process(target=serve_forever) + p.daemon = True + p.start() + process_dict[p.pid] = [p, t, psutil.Process(p.pid)] + + # 结束创建时间超过60秒,且连续空闲5秒钟以上的子进程 + keys = list(process_dict.keys()) + for i in keys: + if t - process_dict[i][1] < 60: continue + if process_dict[i][2].cpu_percent(interval=5) > 0: continue + process_dict[i][0].kill() + process_dict.pop(i) + + time.sleep(1) diff --git a/BT-Task b/BT-Task index c9b0ca46..65683ed0 100644 --- a/BT-Task +++ b/BT-Task @@ -1,11 +1,11 @@ #!/www/server/panel/pyenv/bin/python #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import os,sys diff --git a/BTPanel/__init__.py b/BTPanel/__init__.py index 527deba4..092fd1e4 100755 --- a/BTPanel/__init__.py +++ b/BTPanel/__init__.py @@ -1,11 +1,17 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -# +------------------------------------------------------------------- -# | Author: hwliang +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- +# | Author: hwliang +# +--- + +# from .app import * +# from .routes.flask_hook import * +# from .routes.v1 import * +# from .routes.v2 import * + import logging import sys import json @@ -21,7 +27,8 @@ if not os.name in ['nt']: os.chdir(panel_path) if not 'class/' in sys.path: sys.path.insert(0, 'class/') - +if not 'class_v2/' in sys.path: + sys.path.insert(0, 'class_v2/') from flask import Flask, session, render_template, send_file, request, redirect, g, make_response, \ render_template_string, abort, stream_with_context, Response as Resp from cachelib import SimpleCache, SimpleCacheSession @@ -33,12 +40,15 @@ cache = SimpleCache(5000) import public # 初始化Flask应用 -app = Flask(__name__, template_folder="templates/{}".format(public.GetConfigValue('template'))) +app = Flask(__name__, + template_folder="templates/{}".format( + public.GetConfigValue('template'))) Compress(app) try: from flask_sock import Sock except: from flask_sockets import Sockets as Sock + sockets = Sock(app) # 注册HOOK hooks = {} @@ -62,7 +72,9 @@ if os.path.exists(basic_auth_conf): pass # 初始化SESSION服务 -app.secret_key = public.md5(str(os.uname()) + str(psutil.boot_time())) # uuid.UUID(int=uuid.getnode()).hex[-12:] +app.secret_key = public.md5( + str(os.uname()) + + str(psutil.boot_time())) # uuid.UUID(int=uuid.getnode()).hex[-12:] local_ip = None my_terms = {} app.config['SESSION_MEMCACHED'] = SimpleCacheSession(1000, 86400) @@ -92,7 +104,8 @@ text_header = {'Content-Type': 'text/plain; charset=utf-8'} cache.set('p_token', 'bmac_' + public.Md5(public.get_mac_address())) admin_path_file = 'data/admin_path.pl' admin_path = '/' -if os.path.exists(admin_path_file): admin_path = public.readFile(admin_path_file).strip() +if os.path.exists(admin_path_file): + admin_path = public.readFile(admin_path_file).strip() admin_path_checks = [ '/', '/san', @@ -139,39 +152,55 @@ admin_path_checks = [ '/tips', '/message', '/warning', - '/userRegister', - + '/userRegister', # 面板内注册 + '/docker', + '/btdocker', ] if admin_path in admin_path_checks: admin_path = '/bt' if admin_path[-1] == '/': admin_path = admin_path[:-1] uri_match = re.compile( - r"(^/static/[\w_\./\-]+\.(js|css|png|jpg|gif|ico|svg|woff|woff2|ttf|otf|eot|map)$|^/[\w_\./\-]*$)") + r"(^/static/[\w_\./\-]+\.(js|css|png|jpg|gif|ico|svg|woff|woff2|ttf|otf|eot|map)$|^/[\w_\./\-]*$)" +) session_id_match = re.compile(r"^[\w\.\-]+$") +route_v2 = '/v2' # v2版本路由前缀 + +# load translations +from public.translations import load_translations +load_translations() # ===================================Flask HOOK========================# + # Flask请求勾子 @app.before_request def request_check(): if request.method not in ['GET', 'POST']: return abort(404) + + # 获取客户端真实IP + x_real_ip = request.headers.get('X-Real-Ip') + if x_real_ip: + request.remote_addr = x_real_ip + request.environ.setdefault('REMOTE_PORT', public.get_remote_port()) + g.request_time = time.time() + g.return_message = False # 路由和URI长度过滤 if len(request.path) > 256: return abort(403) if len(request.url) > 1024: return abort(403) # URI过滤 if not uri_match.match(request.path): return abort(403) # POST参数过滤 - if request.path in ['/login', - '/safe', - '/hook', - '/public', - '/down', - '/get_app_bind_status', - '/check_bind', - '/userRegister', - - ]: + if request.path in [ + '/login', + '/safe', + '/hook', + '/public', + '/down', + '/get_app_bind_status', + '/check_bind', + '/userRegister', + ]: pdata = request.form.to_dict() for k in pdata.keys(): if len(k) > 48: return abort(403) @@ -188,8 +217,11 @@ def request_check(): g.get_csrf_html_token_key = public.get_csrf_html_token_key() if app.config['BASIC_AUTH_OPEN']: - if request.path in ['/public', '/download', '/mail_sys', '/hook', '/down', '/check_bind', - '/get_app_bind_status']: return + if request.path in [ + '/public', '/download', '/mail_sys', '/hook', '/down', + '/check_bind', '/get_app_bind_status' + ]: + return auth = request.authorization if not comm.get_sk(): return if not auth: return send_authenticated() @@ -198,7 +230,13 @@ def request_check(): or public.md5(auth.password.strip() + tips) != app.config['BASIC_AUTH_PASSWORD']: return send_authenticated() - if not request.path in ['/safe', '/hook', '/public', '/mail_sys', '/down']: + if not request.path in [ + '/safe', + '/hook', + '/public', + '/mail_sys', + '/down' + ]: ip_check = public.check_ip_panel() if ip_check: return ip_check @@ -210,27 +248,126 @@ def request_check(): if public.is_local(): not_networks = ['uninstall_plugin', 'install_plugin', 'UpdatePanel'] if request.args.get('action') in not_networks: - return public.returnJson(False, 'This feature cannot be used in offline mode!'), json_header - - if request.path in ['/site', '/ftp', '/database', '/soft', '/control', '/firewall', '/files', '/xterm', '/crontab', - '/config']: + return public.returnJson( + False, + 'This feature cannot be used in offline mode!'), json_header + # 适配docker---- '/docker', + if request.path in [ + '/site', '/ftp', '/database', '/soft', '/control', '/firewall', + '/files', '/xterm', '/crontab', '/config', '/docker', '/btdocker', + ]: if public.is_error_path(): return redirect('/error', 302) if not request.path in ['/config']: if session.get('password_expire', False): return redirect('/modify_password', 302) + # 新增 适配docker时增加 未测试 + # 处理登录页面相对路径的静态文件 + if request.path.find('/static/') > 0: + new_auth_path = _auth_path = public.get_admin_path() + + # 2024/1/3 下午 8:35 检测_auth_path是否有包含2个以上/符号,如果有则取最后一个/符号前的字符串然后替换成_auth_path + if _auth_path.count('/') > 1: + new_auth_path = _auth_path[:_auth_path.rfind('/')] + + if not public.path_safe_check(request.path): return abort(404) # 路径安全检查 + + _new_route = request.path[0:request.path.find('/static/')] + if request.path.find(_auth_path) == 0: + static_file = public.get_panel_path() + '/BTPanel' + request.path.replace(_auth_path, '').replace('//', '/') + if not os.path.exists(static_file): return abort(404) + return send_file(static_file, conditional=True, etag=True) + elif request.path.find(new_auth_path) == 0: + static_file = public.get_panel_path() + '/BTPanel' + request.path.replace(new_auth_path, '').replace('//', + '/') + if not os.path.exists(static_file): return abort(404) + return send_file(static_file, conditional=True, etag=True) + elif _new_route in admin_path_checks: + + static_file = public.get_panel_path() + '/BTPanel' + request.path[len(_new_route):].replace('//', '/') + # if not os.path.exists(static_file): return abort(404) + # 检测是否是插件静态文件 + plugin_static_file = public.get_panel_path() + '/plugin/' + request.path + is_plugin_static = os.path.exists(plugin_static_file) + + # 既不是面板静态文件也不是插件静态文件 + if not os.path.exists(static_file) and not is_plugin_static: return abort(404) + + # 如果是插件静态文件 + if is_plugin_static: + return send_file(plugin_static_file, conditional=True, etag=True) + + # 如果是面板静态文件 + return send_file(static_file, conditional=True, etag=True) + + if request.path.find('/static/img/soft_ico/ico') >= 0: + static_file = "{}/BTPanel/{}".format(panel_path, request.path) + if not os.path.exists(static_file): + static_file = "{}/BTPanel/static/img/soft_ico/icon_plug.svg".format(panel_path) + return send_file(static_file, conditional=True, etag=True) + + # 处理登录成功状态,更新节点 + if 'login' in session and session['login'] == True: + if not cache.get('bt_home_node'): + public.run_thread(public.ExecShell, ('btpython /www/server/panel/script/reload_check.py hour',)) + cache.set('bt_home_node', True, 3600) + + # Flask 请求结束勾子 @app.teardown_request def request_end(reques=None): if request.method not in ['GET', 'POST']: return - if not request.path.startswith('/static/'): + if not request.path.startswith('/static/') or not request.path.startswith('/v2/static/'): + # import public public.write_request_log(reques) + + # 当路由为v2版才检测,且不检测/plugin时 + if request.path.startswith('/v2'): + now_time = time.time() + session_timeout = session.get('session_timeout', 0) + if (session_timeout > now_time or session_timeout == 0) and not request.path.startswith('/v2/plugin'): + # 首页涉及的请求模块,暂不强制 + prefixes = ["/v2/site", "/v2/ftp", "/v2/database", "/v2/docker", "/v2/safe/security/set_security", + "/v2/safe/security/get_repair_bar"] + for prefix in prefixes: + if request.path.startswith(prefix): + if 'return_message' in g: + if not g.return_message: + # public.print_log("当前路由且未使用统一响应函数public.return_message") + # return abort(404) + # return public.returnJson( + # False, 'Request failed!Request not using unified response!' + # ), json_header + pass + else: + # return abort(405) + g.return_message = False + # public.print_log("当前路由已使用统一响应函数public.return_message") + break if 'api_request' in g: if g.api_request: session.clear() +# Flask 405页面勾子 +@app.errorhandler(405) +def error_404(e): + if request.method not in ['GET', 'POST']: return + if not session.get('login', None): + g.auth_error = True + return public.error_not_login() + errorStr = ''' +405 Not Found + +

请求接口请使用统一响应函数

+
nginx
+ +''' + headers = {"Content-Type": "text/html"} + return Response(errorStr, status=404, headers=headers) + + # Flask 404页面勾子 @app.errorhandler(404) def error_404(e): @@ -245,9 +382,7 @@ def error_404(e):
nginx
''' - headers = { - "Content-Type": "text/html" - } + headers = {"Content-Type": "text/html"} return Response(errorStr, status=404, headers=headers) @@ -265,30 +400,49 @@ def error_403(e):
nginx
''' - headers = { - "Content-Type": "text/html" - } + headers = {"Content-Type": "text/html"} return Response(errorStr, status=403, headers=headers) -# Flask 500页面勾子 -@app.errorhandler(500) +# 错误收集 +@app.errorhandler(Exception) def error_500(e): - if request.method not in ['GET', 'POST']: return + # handle the hint exception. + if isinstance(e, public.HintException): + return public.fail_v2(str(e)) + + # Print error traceback. + from traceback import format_exc + public.print_log(format_exc()) + + if request.method not in ['GET', 'POST']: return Response(status=500) + if not session.get('login', None): g.auth_error = True return public.error_not_login() + ss = '''404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. During handling of the above exception, another exception occurred:''' error_info = public.get_error_info().strip().split(ss)[-1].strip() + + nn = 'During handling of the above exception, another exception occurred:' + if error_info.find(nn) != -1 and error_info.find('public.error_conn_cloud') != -1: + error_info = error_info.split(nn)[0].strip() + _form = request.form.to_dict() if 'username' in _form: _form['username'] = '******' if 'password' in _form: _form['password'] = '******' if 'phone' in _form: _form['phone'] = '******' + if 'pem' in _form: _form['pem'] = '******' + if 'pwd' in _form: _form['pwd'] = '******' + if 'key' in _form: _form['key'] = '******' + if 'csr' in _form: _form['csr'] = '******' + if 'db_user' in _form: _form['db_user'] = '******' + if 'db_password' in _form: _form['db_pwd'] = '******' + request_info = '''REQUEST_DATE: {request_date} - PAN_VERSION: {panel_version} - OS_VERSION: {os_version} + VERSION: {os_version} - {panel_version} REMOTE_ADDR: {remote_addr} REQUEST_URI: {method} {full_path} REQUEST_FORM: {request_form} @@ -300,162 +454,182 @@ REQUEST_FORM: {request_form} request_form=public.xsssec(str(_form)), user_agent=public.xsssec(request.headers.get('User-Agent')), panel_version=public.version(), - os_version=public.get_os_version() - ) + os_version=public.get_os_version()) + error_title = error_info.split("\n")[-1].replace('public.PanelError: ', + '').strip() + if error_info.find('Failed to connect to the cloud server') != -1: + error_title = "Failed to connect to the cloud server!" + + result = public.readFile( + public.get_panel_path() + + '/BTPanel/templates/default/panel_error.html').format( + error_title=error_title, + request_info=request_info, + error_msg=error_info) + + # 用户信息 + # if not public.cache_get("infos"): + # user_info = json.loads(public.ReadFile("{}/data/userInfo.json".format(public.get_panel_path()))) + # public.cache_set("infos", user_info, 1800) + # else: + # user_info = public.cache_get("infos") + try: + if "import panelSSL" in error_info: + result = public.ExecShell("btpip list|grep pyOpenSSL")[0] + error_info = "{}\n版本信息:{}".format(error_info, result.strip()) + except: + error_info = "{}\n版本信息: 获取失败".format(error_info) + + # 错误信息 + error_infos = { + # "UID": user_info['uid'], # 用户ID + # 'ACCESS_KEY': user_info['access_key'], # 用户密钥 + # 'SERVER_ID': user_info['serverid'], # 服务器ID + "REQUEST_DATE": public.getDate(), # 请求时间 + "PANEL_VERSION": public.version(), # 面板版本 + "OS_VERSION": public.get_os_version(), # 操作系统版本 + "REMOTE_ADDR": public.GetClientIp(), # 请求IP + "REQUEST_URI": request.method + request.full_path, # 请求URI + "REQUEST_FORM": public.xsssec(str(_form)), # 请求表单 + "USER_AGENT": public.xsssec(request.headers.get('User-Agent')), # 客户端连接信息 + "ERROR_INFO": error_info, # 错误信息 + "PACK_TIME": public.readFile("/www/server/panel/config/update_time.pl") if os.path.exists( + "/www/server/panel/config/update_time.pl") else public.getDate(), # 打包时间 + "TYPE": 100, + "ERROR_ID": str(e) + } + pkey = public.Md5(error_infos["ERROR_ID"]) + + # 提交异常报告 + if not public.cache_get(pkey): + try: + public.run_thread(public.httpPost, ("https://geterror.aapanel.com/bt_error/index.php", error_infos)) + public.cache_set(pkey, 1, 1800) + except Exception as e: + pass - result = public.readFile(public.get_panel_path() + '/BTPanel/templates/default/panel_error.html').format( - error_title=error_info.split("\n")[-1], request_info=request_info, error_msg=error_info) return Resp(result, 500) -# # 错误收集适配 -# @app.errorhandler(Exception) -# def error_500(e): -# if request.method not in ['GET', 'POST']: return Response(status=500) -# if not session.get('login', None): -# g.auth_error = True -# return public.error_not_login() -# ss = '''404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. -# -# During handling of the above exception, another exception occurred:''' -# error_info = public.get_error_info().strip().split(ss)[-1].strip() -# -# nn = 'During handling of the above exception, another exception occurred:' -# if error_info.find(nn) != -1 and error_info.find('public.error_conn_cloud') != -1: -# error_info = error_info.split(nn)[0].strip() -# -# _form = request.form.to_dict() -# if 'username' in _form: _form['username'] = '******' -# if 'password' in _form: _form['password'] = '******' -# if 'phone' in _form: _form['phone'] = '******' -# if 'pem' in _form: _form['pem'] = '******' -# if 'pwd' in _form: _form['pwd'] = '******' -# if 'key' in _form: _form['key'] = '******' -# if 'csr' in _form: _form['csr'] = '******' -# if 'db_user' in _form: _form['db_user'] = '******' -# if 'db_password' in _form: _form['db_pwd'] = '******' -# -# request_info = '''REQUEST_DATE: {request_date} -# VERSION: {os_version} - {panel_version} -# REMOTE_ADDR: {remote_addr} -# REQUEST_URI: {method} {full_path} -# REQUEST_FORM: {request_form} -# USER_AGENT: {user_agent}'''.format( -# request_date=public.getDate(), -# remote_addr=public.GetClientIp(), -# method=request.method, -# full_path=public.xsssec(request.full_path), -# request_form=public.xsssec(str(_form)), -# user_agent=public.xsssec(request.headers.get('User-Agent')), -# panel_version=public.version(), -# os_version=public.get_os_version()) -# error_title = error_info.split("\n")[-1].replace('public.PanelError: ', -# '').strip() -# if error_info.find('连接云端服务器失败') != -1: -# error_title = "连接云端服务器失败!" -# -# result = public.readFile( -# public.get_panel_path() + -# '/BTPanel/templates/default/panel_error.html').format( -# error_title=error_title, -# request_info=request_info, -# error_msg=error_info) -# -# # 用户信息 -# # if not public.cache_get("infos"): -# # user_info = json.loads(public.ReadFile("{}/data/userInfo.json".format(public.get_panel_path()))) -# # public.cache_set("infos", user_info, 1800) -# # else: -# # user_info = public.cache_get("infos") -# try: -# if "import panelSSL" in error_info: -# result = public.ExecShell("btpip list|grep pyOpenSSL")[0] -# error_info = "{}\n版本信息:{}".format(error_info, result.strip()) -# except: -# error_info = "{}\n版本信息: 获取失败".format(error_info) -# -# -# # 错误信息 -# error_infos = { -# # "UID": user_info['uid'], # 用户ID -# # 'ACCESS_KEY': user_info['access_key'], # 用户密钥 -# # 'SERVER_ID': user_info['serverid'], # 服务器ID -# "REQUEST_DATE": public.getDate(), # 请求时间 -# "PANEL_VERSION": public.version(), # 面板版本 -# "OS_VERSION": public.get_os_version(), # 操作系统版本 -# "REMOTE_ADDR": public.GetClientIp(), # 请求IP -# "REQUEST_URI": request.method + request.full_path, # 请求URI -# "REQUEST_FORM": public.xsssec(str(_form)), # 请求表单 -# "USER_AGENT": public.xsssec(request.headers.get('User-Agent')), # 客户端连接信息 -# "ERROR_INFO": error_info, # 错误信息 -# "PACK_TIME": public.readFile("/www/server/panel/config/update_time.pl") if os.path.exists("/www/server/panel/config/update_time.pl") else public.getDate(), # 打包时间 -# "TYPE": 0, -# "ERROR_ID": str(e) -# } -# pkey = public.Md5(error_infos["ERROR_ID"]) -# -# # 提交异常报告 -# if not public.cache_get(pkey): -# try: -# public.run_thread(public.httpPost, ("https://api.bt.cn/bt_error/index.php", error_infos)) -# public.cache_set(pkey, 1, 1800) -# except Exception as e: -# pass -# -# return Resp(result, 500) - - # ===================================Flask HOOK========================# # ===================================普通路由区========================# -@app.route('/', methods=method_all) -def home(): +# @app.route('/', methods=method_all) +# def home(): +# # 面板首页 +# comReturn = comm.local() +# if comReturn: return comReturn +# data = {} +# data[public.to_string([112, +# 100])], data['pro_end'], data['ltd_end'] = get_pd() +# data['siteCount'] = public.M('sites').count() +# data['ftpCount'] = public.M('ftps').count() +# data['databaseCount'] = public.M('databases').count() +# data['lan'] = public.GetLan('index') +# data['js_random'] = get_js_random() +# return render_template('index.html', data=data) + + +@app.route('/', methods=method_get) +@app.route('/', methods=method_get) +def index_new(sub_path: str = ''): # 面板首页 comReturn = comm.local() if comReturn: return comReturn data = {} - data[public.to_string([112, 100])], data['pro_end'], data['ltd_end'] = get_pd() - data['siteCount'] = public.M('sites').count() - data['ftpCount'] = public.M('ftps').count() - data['databaseCount'] = public.M('databases').count() - data['lan'] = public.GetLan('index') - data['js_random'] = get_js_random() - return render_template('index.html', data=data) -@app.route('/xterm', methods=method_all) -def xterm(): - # 宝塔终端管理 - comReturn = comm.local() - if comReturn: return comReturn - if request.method == method_get[0]: + if sub_path == '': + data[public.to_string([112, + 100])], data['pro_end'], data['ltd_end'] = get_pd() + data['siteCount'] = public.M('sites').count() + data['ftpCount'] = public.M('ftps').count() + data['databaseCount'] = public.M('databases').count() + data['lan'] = public.GetLan('index') + data['js_random'] = get_js_random() + elif sub_path.startswith('config'): + import system, wxapp, config + c_obj = config.config() + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('config') + try: + data['wx'] = wxapp.wxapp().get_user_info(None)['msg'] + except: + data['wx'] = 'INIT_WX_NOT_BIND' + data['api'] = '' + data['ipv6'] = '' + sess_out_path = 'data/session_timeout.pl' + if not os.path.exists(sess_out_path): + public.writeFile(sess_out_path, '86400') + s_time_tmp = public.readFile(sess_out_path) + if not s_time_tmp: s_time_tmp = '0' + data['session_timeout'] = int(s_time_tmp) + if c_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' + if c_obj.get_token(None)['open']: data['api'] = 'checked' + data['basic_auth'] = c_obj.get_basic_auth_stat(None) + data['status_code'] = c_obj.get_not_auth_status() + data['basic_auth']['value'] = public.getMsg('CLOSED') + if data['basic_auth']['open']: + data['basic_auth']['value'] = public.getMsg('OPENED') + data['debug'] = '' + data['js_random'] = get_js_random() + if app.config['DEBUG']: data['debug'] = 'checked' + data['is_local'] = '' + if public.is_local(): data['is_local'] = 'checked' + data['public_key'] = public.get_rsa_public_key().replace("\n", "") + elif sub_path.startswith('soft'): import system data = system.system().GetConcifInfo() - return render_template('xterm.html', data=data) - import ssh_terminal - ssh_host_admin = ssh_terminal.ssh_host_admin() - defs = ( - 'get_host_list', 'get_host_find', 'modify_host', 'create_host', 'remove_host', 'set_sort', 'get_command_list', - 'create_command', 'get_command_find', 'modify_command', 'remove_command') - return publicObject(ssh_host_admin, defs, None) - -@app.route('/modify_password', methods=method_get) -def modify_password(): - comReturn = comm.local() - if comReturn: return comReturn - # if not session.get('password_expire',False): return redirect('/',302) - data = {} - g.title = public.get_msg_gettext('The password has expired, please change it!') - return render_template('modify_password.html', data=data) - - -@app.route('/site', methods=method_all) -def site(pdata=None): - # 网站管理 - comReturn = comm.local() - if comReturn: return comReturn - if request.method == method_get[0] and not pdata: - # data = {} + data['lan'] = public.GetLan('soft') + data['js_random'] = get_js_random() + elif sub_path.startswith('crontab'): + import system + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('crontab') + data['js_random'] = get_js_random() + elif sub_path.startswith('docker'): + import system + data = system.system().GetConcifInfo() + data['js_random'] = get_js_random() + data['lan'] = public.GetLan('files') + elif sub_path.startswith('control'): + import system + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('control') + data['js_random'] = get_js_random() + elif sub_path.startswith('logs'): + data = {} + data['lan'] = public.GetLan('soft') + data['show_workorder'] = not os.path.exists('data/not_workorder.pl') + elif sub_path.startswith('database'): + import ajax + from panelPlugin import panelPlugin + session['phpmyadminDir'] = False + if panelPlugin().get_phpmyadmin_stat(): + pmd = get_phpmyadmin_dir() + if pmd: + session['phpmyadminDir'] = 'http://' + public.GetHost( + ) + ':' + pmd[1] + '/' + pmd[0] + ajax.ajax().set_phpmyadmin_session() + import system + data = system.system().GetConcifInfo() + data['isSetup'] = os.path.exists( + public.GetConfigValue('setup_path') + '/mysql/bin') + data['mysql_root'] = public.M('config').where( + 'id=?', (1,)).getField('mysql_root') + data['lan'] = public.GetLan('database') + data['js_random'] = get_js_random() + elif sub_path.startswith('ftp'): + FtpPort() + import system + data = system.system().GetConcifInfo() + data['isSetup'] = True + data['js_random'] = get_js_random() + if os.path.exists(public.GetConfigValue('setup_path') + + '/pure-ftpd') == False: + data['isSetup'] = False + data['lan'] = public.GetLan('ftp') + elif sub_path.startswith('site'): import system data = system.system().GetConcifInfo() data['isSetup'] = True @@ -465,99 +639,217 @@ def site(pdata=None): and os.path.exists(public.GetConfigValue('setup_path') + '/apache') == False \ and os.path.exists('/usr/local/lsws/bin/lswsctrl') == False: data['isSetup'] = False - return render_template('site.html', data=data) + elif sub_path.startswith('xterm'): + import system + data = system.system().GetConcifInfo() + elif sub_path.startswith('firewall'): + import system + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + elif sub_path.startswith('files'): + import system + data = system.system().GetConcifInfo() + data['recycle_bin'] = os.path.exists('data/recycle_bin.pl') + data['lan'] = public.GetLan('files') + data['js_random'] = get_js_random() + elif sub_path.startswith('ssh_security'): + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + + data['isSetup'] = True + if os.path.exists(public.GetConfigValue('setup_path') + '/nginx') == False \ + and os.path.exists(public.GetConfigValue('setup_path') + '/apache') == False \ + and os.path.exists('/usr/local/lsws/bin/lswsctrl') == False: + data['isSetup'] = False + + import base64 + + # load translations + data['translations'] = base64.b64encode(json.dumps(load_translations()).encode()).decode() + + return render_template('index_new.html', data=data) + + + +@app.route('/xterm', methods=method_post) +def xterm(): + # 宝塔终端管理 + comReturn = comm.local() + if comReturn: return comReturn + import ssh_terminal + ssh_host_admin = ssh_terminal.ssh_host_admin() + defs = ('get_host_list', 'get_host_find', 'modify_host', 'create_host', + 'remove_host', 'set_sort', 'get_command_list', 'create_command', + 'get_command_find', 'modify_command', 'remove_command') + return publicObject(ssh_host_admin, defs, None) + + +@app.route('/modify_password', methods=method_get) +def modify_password(): + comReturn = comm.local() + if comReturn: return comReturn + # if not session.get('password_expire',False): return redirect ('/',302) + data = {} + g.title = public.get_msg_gettext( + 'The password has expired, please change it!') + return render_template('modify_password.html', data=data) + + +@app.route('/site', methods=method_post) +def site(pdata=None): + # 网站管理 + comReturn = comm.local() + if comReturn: return comReturn import panelSite siteObject = panelSite.panelSite() - defs = ('get_auto_restart_rph', 'remove_auto_restart_rph', 'auto_restart_rph', 'check_del_data', 'upload_csv', - 'create_website_multiple', 'del_redirect_multiple', 'del_proxy_multiple', 'delete_dir_auth_multiple', - 'delete_dir_bind_multiple', 'delete_domain_multiple', 'set_site_etime_multiple', - 'set_site_php_version_multiple', - 'delete_website_multiple', 'set_site_status_multiple', 'get_site_err_log', 'get_site_domains', - 'GetRedirectFile', - 'SaveRedirectFile', 'DeleteRedirect', 'GetRedirectList', 'CreateRedirect', 'ModifyRedirect', - "set_error_redirect", - 'set_dir_auth', 'delete_dir_auth', 'get_dir_auth', 'modify_dir_auth_pass', 'reset_wp_db', 'export_domains', - 'import_domains', - 'GetSiteLogs', 'GetSiteDomains', 'GetSecurity', 'SetSecurity', 'ProxyCache', 'CloseToHttps', 'HttpToHttps', - 'SetEdate', - 'SetRewriteTel', 'GetCheckSafe', 'CheckSafe', 'GetDefaultSite', 'SetDefaultSite', 'CloseTomcat', - 'SetTomcat', 'apacheAddPort', - 'AddSite', 'GetPHPVersion', 'SetPHPVersion', 'DeleteSite', 'AddDomain', 'DelDomain', 'GetDirBinding', - 'AddDirBinding', 'GetDirRewrite', - 'DelDirBinding', 'get_site_types', 'add_site_type', 'remove_site_type', 'modify_site_type_name', - 'set_site_type', 'UpdateRulelist', - 'SetSiteRunPath', 'GetSiteRunPath', 'SetPath', 'SetIndex', 'GetIndex', 'GetDirUserINI', 'SetDirUserINI', - 'GetRewriteList', 'SetSSL', - 'SetSSLConf', 'CreateLet', 'CloseSSLConf', 'GetSSL', 'SiteStart', 'SiteStop', 'Set301Status', - 'Get301Status', 'CloseLimitNet', 'SetLimitNet', - 'GetLimitNet', 'RemoveProxy', 'GetProxyList', 'GetProxyDetals', 'CreateProxy', 'ModifyProxy', - 'GetProxyFile', 'SaveProxyFile', 'ToBackup', - 'DelBackup', 'GetSitePHPVersion', 'logsOpen', 'GetLogsStatus', 'CloseHasPwd', 'SetHasPwd', 'GetHasPwd', - 'GetDnsApi', 'SetDnsApi', - 'reset_wp_password', 'is_update', 'purge_all_cache', 'set_fastcgi_cache', 'update_wp', 'get_wp_username', - 'get_language', 'deploy_wp', - # 网站管理新增 - 'test_domains_api', 'site_rname', - - ) + defs = ( + 'get_auto_restart_rph', + 'remove_auto_restart_rph', + 'auto_restart_rph', + 'check_del_data', + 'upload_csv', + 'create_website_multiple', + 'del_redirect_multiple', + 'del_proxy_multiple', + 'delete_dir_auth_multiple', + 'delete_dir_bind_multiple', + 'delete_domain_multiple', + 'set_site_etime_multiple', + 'set_site_php_version_multiple', + 'delete_website_multiple', + 'set_site_status_multiple', + 'get_site_err_log', + 'get_site_domains', + 'GetRedirectFile', + 'SaveRedirectFile', + 'DeleteRedirect', + 'GetRedirectList', + 'CreateRedirect', + 'ModifyRedirect', + "set_error_redirect", + 'set_dir_auth', + 'delete_dir_auth', + 'get_dir_auth', + 'modify_dir_auth_pass', + 'reset_wp_db', + 'export_domains', + 'import_domains', + 'GetSiteLogs', + 'GetSiteDomains', + 'GetSecurity', + 'SetSecurity', + 'ProxyCache', + 'CloseToHttps', + 'HttpToHttps', + 'SetEdate', + 'SetRewriteTel', + 'GetCheckSafe', + 'CheckSafe', + 'GetDefaultSite', + 'SetDefaultSite', + 'CloseTomcat', + 'SetTomcat', + 'apacheAddPort', + 'AddSite', + 'GetPHPVersion', + 'SetPHPVersion', + 'DeleteSite', + 'AddDomain', + 'DelDomain', + 'GetDirBinding', + 'AddDirBinding', + 'GetDirRewrite', + 'DelDirBinding', + 'get_site_types', + 'add_site_type', + 'remove_site_type', + 'modify_site_type_name', + 'set_site_type', + 'UpdateRulelist', + 'SetSiteRunPath', + 'GetSiteRunPath', + 'SetPath', + 'SetIndex', + 'GetIndex', + 'GetDirUserINI', + 'SetDirUserINI', + 'GetRewriteList', + 'SetSSL', + 'SetSSLConf', + 'CreateLet', + 'CloseSSLConf', + 'GetSSL', + 'SiteStart', + 'SiteStop', + 'Set301Status', + 'Get301Status', + 'CloseLimitNet', + 'SetLimitNet', + 'GetLimitNet', + 'RemoveProxy', + 'GetProxyList', + 'GetProxyDetals', + 'CreateProxy', + 'ModifyProxy', + 'GetProxyFile', + 'SaveProxyFile', + 'ToBackup', + 'DelBackup', + 'GetSitePHPVersion', + 'logsOpen', + 'GetLogsStatus', + 'CloseHasPwd', + 'SetHasPwd', + 'GetHasPwd', + 'GetDnsApi', + 'SetDnsApi', + 'reset_wp_password', + 'is_update', + 'purge_all_cache', + 'set_fastcgi_cache', + 'update_wp', + 'get_wp_username', + 'get_language', + 'deploy_wp', + # 网站管理新增 + 'test_domains_api', + 'site_rname', + ) return publicObject(siteObject, defs, None, pdata) -@app.route('/ftp', methods=method_all) +@app.route('/ftp', methods=method_post) def ftp(pdata=None): # FTP管理 comReturn = comm.local() if comReturn: return comReturn - if request.method == method_get[0] and not pdata: - FtpPort() - import system - data = system.system().GetConcifInfo() - data['isSetup'] = True - data['js_random'] = get_js_random() - if os.path.exists(public.GetConfigValue('setup_path') + '/pure-ftpd') == False: data['isSetup'] = False - data['lan'] = public.GetLan('ftp') - return render_template('ftp.html', data=data) import ftp ftpObject = ftp.ftp() - defs = ('AddUser', 'DeleteUser', 'SetUserPassword', 'SetStatus', 'setPort', 'set_user_home', 'get_login_logs', - 'get_action_logs', 'set_ftp_logs') + defs = ('AddUser', 'DeleteUser', 'SetUserPassword', 'SetStatus', 'setPort', + 'set_user_home', 'get_login_logs', 'get_action_logs', + 'set_ftp_logs') return publicObject(ftpObject, defs, None, pdata) -@app.route('/database', methods=method_all) +@app.route('/database', methods=method_post) def database(pdata=None): # 数据库管理 comReturn = comm.local() if comReturn: return comReturn - if request.method == method_get[0] and not pdata: - import ajax - from panelPlugin import panelPlugin - session['phpmyadminDir'] = False - if panelPlugin().get_phpmyadmin_stat(): - pmd = get_phpmyadmin_dir() - if pmd: - session['phpmyadminDir'] = 'http://' + public.GetHost() + ':' + pmd[1] + '/' + pmd[0] - ajax.ajax().set_phpmyadmin_session() - import system - data = system.system().GetConcifInfo() - data['isSetup'] = os.path.exists(public.GetConfigValue('setup_path') + '/mysql/bin') - data['mysql_root'] = public.M('config').where('id=?', (1,)).getField('mysql_root') - data['lan'] = public.GetLan('database') - data['js_random'] = get_js_random() - return render_template('database.html', data=data) import database databaseObject = database.database() - defs = ( - 'GetdataInfo', 'check_del_data', 'get_database_size', 'GetInfo', 'ReTable', 'OpTable', 'AlTable', 'GetSlowLogs', - 'GetRunStatus', - 'SetDbConf', 'GetDbStatus', 'BinLog', 'GetErrorLog', 'GetMySQLInfo', 'SetDataDir', 'SetMySQLPort', - 'AddCloudDatabase', - 'AddDatabase', 'DeleteDatabase', 'SetupPassword', 'ResDatabasePassword', 'ToBackup', 'DelBackup', - 'AddCloudServer', - 'GetCloudServer', 'RemoveCloudServer', 'ModifyCloudServer', - 'InputSql', 'SyncToDatabases', 'SyncGetDatabases', 'GetDatabaseAccess', 'SetDatabaseAccess', - 'get_mysql_user', 'check_mysql_ssl_status', 'write_ssl_to_mysql', 'GetdataInfo') + defs = ('GetdataInfo', 'check_del_data', 'get_database_size', 'GetInfo', + 'ReTable', 'OpTable', 'AlTable', 'GetSlowLogs', 'GetRunStatus', + 'SetDbConf', 'GetDbStatus', 'BinLog', 'GetErrorLog', + 'GetMySQLInfo', 'SetDataDir', 'SetMySQLPort', 'AddCloudDatabase', + 'AddDatabase', 'DeleteDatabase', 'SetupPassword', + 'ResDatabasePassword', 'ToBackup', 'DelBackup', 'AddCloudServer', + 'GetCloudServer', 'RemoveCloudServer', 'ModifyCloudServer', + 'InputSql', 'SyncToDatabases', 'SyncGetDatabases', + 'GetDatabaseAccess', 'SetDatabaseAccess', 'get_mysql_user', + 'check_mysql_ssl_status', 'write_ssl_to_mysql', 'GetdataInfo') return publicObject(databaseObject, defs, None, pdata) @@ -568,9 +860,10 @@ def acme(pdata=None): if comReturn: return comReturn import acme_v2 acme_v2_object = acme_v2.acme_v2() - defs = ('get_orders', 'remove_order', 'get_order_find', 'revoke_order', 'create_order', 'get_account_info', - 'set_account_info', 'update_zip', 'get_cert_init_api', - 'get_auths', 'auth_domain', 'check_auth_status', 'download_cert', 'apply_cert', 'renew_cert', + defs = ('get_orders', 'remove_order', 'get_order_find', 'revoke_order', + 'create_order', 'get_account_info', 'set_account_info', + 'update_zip', 'get_cert_init_api', 'get_auths', 'auth_domain', + 'check_auth_status', 'download_cert', 'apply_cert', 'renew_cert', 'apply_cert_api', 'apply_dns_auth') return publicObject(acme_v2_object, defs, None, pdata) @@ -596,49 +889,22 @@ def api(pdata=None): if comReturn: return comReturn import panelApi api_object = panelApi.panelApi() - defs = ('get_token', 'check_bind', 'get_bind_status', 'get_apps', 'add_bind_app', 'remove_bind_app', 'set_token', - 'get_tmp_token', 'get_app_bind_status', 'login_for_app') + defs = ('get_token', 'check_bind', 'get_bind_status', 'get_apps', + 'add_bind_app', 'remove_bind_app', 'set_token', 'get_tmp_token', + 'get_app_bind_status', 'login_for_app') return publicObject(api_object, defs, None, pdata) -@app.route('/control', methods=method_all) -def control(pdata=None): - # 监控页面 - comReturn = comm.local() - if comReturn: return comReturn - import system - data = system.system().GetConcifInfo() - data['lan'] = public.GetLan('control') - data['js_random'] = get_js_random() - return render_template('control.html', data=data) - - -@app.route('/logs', methods=method_all) -def logs(pdata=None): - comReturn = comm.local() - if comReturn: return comReturn - if request.method == method_get[0] and not pdata: - data = {} - data['lan'] = public.GetLan('soft') - data['show_workorder'] = not os.path.exists('data/not_workorder.pl') - return render_template('logs.html', data=data) - - -@app.route('/firewall', methods=method_all) +@app.route('/firewall', methods=method_post) def firewall(pdata=None): # 安全页面 comReturn = comm.local() if comReturn: return comReturn - if request.method == method_get[0] and not pdata: - import system - data = system.system().GetConcifInfo() - data['lan'] = public.GetLan('firewall') - data['js_random'] = get_js_random() - return render_template('firewall.html', data=data) import firewalls firewallObject = firewalls.firewalls() - defs = ('GetList', 'AddDropAddress', 'DelDropAddress', 'FirewallReload', 'SetFirewallStatus', - 'AddAcceptPort', 'DelAcceptPort', 'SetSshStatus', 'SetPing', 'SetSshPort', 'GetSshInfo', + defs = ('GetList', 'AddDropAddress', 'DelDropAddress', 'FirewallReload', + 'SetFirewallStatus', 'AddAcceptPort', 'DelAcceptPort', + 'SetSshStatus', 'SetPing', 'SetSshPort', 'GetSshInfo', 'SetFirewallStatus') return publicObject(firewallObject, defs, None, pdata) @@ -648,20 +914,20 @@ def ssh_security(pdata=None): # SSH安全 comReturn = comm.local() if comReturn: return comReturn - if request.method == method_get[0] and not pdata and not request.args.get('action', '') in ['download_key']: - data = {} - data['lan'] = public.GetLan('firewall') - data['js_random'] = get_js_random() - return render_template('firewall.html', data=data) + if request.method == method_get[0] and not pdata and not request.args.get( + 'action', '') in ['download_key']: + return index_new('ssh_security') import ssh_security firewallObject = ssh_security.ssh_security() is_csrf = True if request.args.get('action', '') in ['download_key']: is_csrf = False - defs = ('san_ssh_security', 'set_password', 'set_sshkey', 'stop_key', 'get_config', 'download_key', - 'stop_password', 'get_key', 'return_ip', 'add_return_ip', 'del_return_ip', 'start_jian', 'stop_jian', - 'get_jian', 'get_logs', 'set_root', 'stop_root', 'start_auth_method', 'stop_auth_method', 'get_auth_method', - 'check_so_file', 'get_so_file', 'get_pin', 'set_login_send', 'get_login_send', 'get_msg_push_list', - 'clear_login_send') + defs = ('san_ssh_security', 'set_password', 'set_sshkey', 'stop_key', + 'get_config', 'download_key', 'stop_password', 'get_key', + 'return_ip', 'add_return_ip', 'del_return_ip', 'start_jian', + 'stop_jian', 'get_jian', 'get_logs', 'set_root', 'stop_root', + 'start_auth_method', 'stop_auth_method', 'get_auth_method', + 'check_so_file', 'get_so_file', 'get_pin', 'set_login_send', + 'get_login_send', 'get_msg_push_list', 'clear_login_send') return publicObject(firewallObject, defs, None, pdata, is_csrf) @@ -672,7 +938,8 @@ def panel_monitor(pdata=None): if comReturn: return comReturn import monitor dataObject = monitor.Monitor() - defs = ('get_spider', 'get_exception', 'get_request_count_qps', 'load_and_up_flow', 'get_request_count_by_hour') + defs = ('get_spider', 'get_exception', 'get_request_count_qps', + 'load_and_up_flow', 'get_request_count_by_hour') return publicObject(dataObject, defs, None, pdata) @@ -683,7 +950,8 @@ def san_baseline(pdata=None): if comReturn: return comReturn import san_baseline dataObject = san_baseline.san_baseline() - defs = ('start', 'get_api_log', 'get_resut', 'get_ssh_errorlogin', 'repair', 'repair_all') + defs = ('start', 'get_api_log', 'get_resut', 'get_ssh_errorlogin', + 'repair', 'repair_all') return publicObject(dataObject, defs, None, pdata) @@ -694,10 +962,10 @@ def panel_password(pdata=None): if comReturn: return comReturn import password dataObject = password.password() - defs = ('set_root_password', 'get_mysql_root', 'set_mysql_password', 'set_panel_password', - 'SetPassword', 'SetSshKey', 'StopKey', 'GetConfig', 'StopPassword', 'GetKey', - 'get_databses', 'rem_mysql_pass', 'set_mysql_access', "get_panel_username" - ) + defs = ('set_root_password', 'get_mysql_root', 'set_mysql_password', + 'set_panel_password', 'SetPassword', 'SetSshKey', 'StopKey', + 'GetConfig', 'StopPassword', 'GetKey', 'get_databses', + 'rem_mysql_pass', 'set_mysql_access', "get_panel_username") return publicObject(dataObject, defs, None, pdata) @@ -707,7 +975,8 @@ def panel_warning(pdata=None): comReturn = comm.local() if comReturn: return comReturn if public.get_csrf_html_token_key() in session and 'login' in session: - if not check_csrf(): return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header get = get_input() ikey = 'warning_list' import panelWarning @@ -725,7 +994,8 @@ def panel_warning(pdata=None): pass return result - defs = ('get_list', 'set_ignore', 'check_find', 'check_cve', 'set_vuln_ignore', 'get_scan_bar', 'get_tmp_result', + defs = ('get_list', 'set_ignore', 'check_find', 'check_cve', + 'set_vuln_ignore', 'get_scan_bar', 'get_tmp_result', 'kill_get_list') if get.action in ['set_ignore', 'check_find', 'set_vuln_ignore']: @@ -740,11 +1010,11 @@ def backup_bak(pdata=None): if comReturn: return comReturn import backup_bak dataObject = backup_bak.backup_bak() - defs = ('get_sites', 'get_databases', 'backup_database', 'backup_site', 'backup_path', 'get_database_progress', - 'get_site_progress', 'down', 'get_down_progress', 'download_path', 'backup_site_all', - 'get_all_site_progress', - 'backup_date_all', 'get_all_date_progress' - ) + defs = ('get_sites', 'get_databases', 'backup_database', 'backup_site', + 'backup_path', 'get_database_progress', 'get_site_progress', + 'down', 'get_down_progress', 'download_path', 'backup_site_all', + 'get_all_site_progress', 'backup_date_all', + 'get_all_date_progress') return publicObject(dataObject, defs, None, pdata) @@ -755,9 +1025,9 @@ def abnormal(pdata=None): if comReturn: return comReturn import abnormal dataObject = abnormal.abnormal() - defs = ('mysql_server', 'mysql_cpu', 'mysql_count', 'php_server', 'php_conn_max', - 'php_cpu', 'CPU', 'Memory', 'disk', 'not_root_user', 'start' - ) + defs = ('mysql_server', 'mysql_cpu', 'mysql_count', 'php_server', + 'php_conn_max', 'php_cpu', 'CPU', 'Memory', 'disk', + 'not_root_user', 'start') return publicObject(dataObject, defs, None, pdata) @@ -792,16 +1062,32 @@ def msgcontroller(mod_name, def_name): return publicObject(project_obj, defs, None, get) -@app.route('/docker', methods=method_all) -def docker(pdata=None): - comReturn = comm.local() - if comReturn: return comReturn - if request.method == method_get[0]: - import system - data = system.system().GetConcifInfo() - data['js_random'] = get_js_random() - data['lan'] = public.GetLan('files') - return render_template('docker.html', data=data) +# @app.route('/docker', methods=method_all) +# def docker(pdata=None): +# comReturn = comm.local() +# if comReturn: return comReturn +# if request.method == method_get[0]: +# import system +# data = system.system().GetConcifInfo() +# data['js_random'] = get_js_random() +# data['lan'] = public.GetLan('files') +# return render_template('docker.html', data=data) + + +# @app.route('/docker', methods=method_all) +# @app.route('/docker/', methods=method_all) +# @app.route('/docker_ifame', methods=method_all) +# def docker(action=None, pdata=None): +# if not public.is_bind(): +# return redirect('/bind', 302) +# comReturn = comm.local() +# if comReturn: return comReturn +# if request.method == method_get[0]: +# import system +# data = system.system().GetConcifInfo() +# data['js_random'] = get_js_random() +# data['lan'] = public.GetLan('files') +# return render_template('index1.html', data=data) @app.route('/dbmodel//', methods=method_all) @@ -825,150 +1111,238 @@ def files(pdata=None): comReturn = comm.local() if comReturn: return comReturn if request.method == method_get[0] and not request.args.get('path') and not pdata: - import system - data = system.system().GetConcifInfo() - data['recycle_bin'] = os.path.exists('data/recycle_bin.pl') - data['lan'] = public.GetLan('files') - data['js_random'] = get_js_random() - return render_template('files.html', data=data) + return index_new('files') import files filesObject = files.files() - defs = ('files_search', 'files_replace', 'get_replace_logs', 'get_images_resize', 'add_files_rsync', - 'get_file_attribute', 'get_file_hash', 'CreateLink', 'get_progress', 'restore_website', 'fix_permissions', - 'get_all_back', - 'restore_path_permissions', 'del_path_premissions', 'get_path_premissions', 'back_path_permissions', - 'upload_file_exists', - 'CheckExistsFiles', 'GetExecLog', 'GetSearch', 'ExecShell', 'GetExecShellMsg', 'exec_git', 'exec_composer', - 'create_download_url', - 'UploadFile', 'GetDir', 'CreateFile', 'CreateDir', 'DeleteDir', 'DeleteFile', 'get_download_url_list', - 'remove_download_url', 'modify_download_url', - 'CopyFile', 'CopyDir', 'MvFile', 'GetFileBody', 'SaveFileBody', 'Zip', 'UnZip', 'get_download_url_find', - 'set_file_ps', - 'SearchFiles', 'upload', 'read_history', 're_history', 'auto_save_temp', 'get_auto_save_body', 'get_videos', - 'GetFileAccess', 'SetFileAccess', 'GetDirSize', 'SetBatchData', 'BatchPaste', 'install_rar', - 'get_path_size', - 'DownloadFile', 'GetTaskSpeed', 'CloseLogs', 'InstallSoft', 'UninstallSoft', 'SaveTmpFile', + defs = ('files_search', 'files_replace', 'get_replace_logs', + 'get_images_resize', 'add_files_rsync', 'get_file_attribute', + 'get_file_hash', 'CreateLink', 'get_progress', 'restore_website', + 'fix_permissions', 'get_all_back', 'restore_path_permissions', + 'del_path_premissions', 'get_path_premissions', + 'back_path_permissions', 'upload_file_exists', 'CheckExistsFiles', + 'GetExecLog', 'GetSearch', 'ExecShell', 'GetExecShellMsg', + 'exec_git', 'exec_composer', 'create_download_url', 'UploadFile', + 'GetDir', 'CreateFile', 'CreateDir', 'DeleteDir', 'DeleteFile', + 'get_download_url_list', 'remove_download_url', + 'modify_download_url', 'CopyFile', 'CopyDir', 'MvFile', + 'GetFileBody', 'SaveFileBody', 'Zip', 'UnZip', + 'get_download_url_find', 'set_file_ps', 'SearchFiles', 'upload', + 'read_history', 're_history', 'auto_save_temp', + 'get_auto_save_body', 'get_videos', 'GetFileAccess', + 'SetFileAccess', 'GetDirSize', 'SetBatchData', 'BatchPaste', + 'install_rar', 'get_path_size', 'DownloadFile', 'GetTaskSpeed', + 'CloseLogs', 'InstallSoft', 'UninstallSoft', 'SaveTmpFile', 'get_composer_version', 'exec_composer', 'update_composer', - 'GetTmpFile', 'del_files_store', 'add_files_store', 'get_files_store', 'del_files_store_types', - 'add_files_store_types', 'exec_git', - 'RemoveTask', 'ActionTask', 'Re_Recycle_bin', 'Get_Recycle_bin', 'Del_Recycle_bin', 'Close_Recycle_bin', - 'Recycle_bin', 'file_webshell_check', 'dir_webshell_check', - 'files_search', 'files_replace', 'get_replace_logs' - ) + 'GetTmpFile', 'del_files_store', 'add_files_store', + 'get_files_store', 'del_files_store_types', + 'add_files_store_types', 'exec_git', 'RemoveTask', 'ActionTask', + 'Re_Recycle_bin', 'Get_Recycle_bin', 'Del_Recycle_bin', + 'Close_Recycle_bin', 'Recycle_bin', 'file_webshell_check', + 'dir_webshell_check', 'files_search', 'files_replace', + 'get_replace_logs') return publicObject(filesObject, defs, None, pdata) -@app.route('/crontab', methods=method_all) +@app.route('/crontab', methods=method_post) def crontab(pdata=None): # 计划任务 comReturn = comm.local() if comReturn: return comReturn - if request.method == method_get[0] and not pdata: - import system - data = system.system().GetConcifInfo() - data['lan'] = public.GetLan('crontab') - data['js_random'] = get_js_random() - return render_template('crontab.html', data=data) import crontab crontabObject = crontab.crontab() - defs = ('GetCrontab', 'AddCrontab', 'GetDataList', 'GetLogs', 'DelLogs', 'DelCrontab', - 'StartTask', 'set_cron_status', 'get_crond_find', 'modify_crond', 'get_backup_list' - ) + defs = ('GetCrontab', 'AddCrontab', 'GetDataList', 'GetLogs', 'DelLogs', + 'DelCrontab', 'StartTask', 'set_cron_status', 'get_crond_find', + 'modify_crond', 'get_backup_list') return publicObject(crontabObject, defs, None, pdata) -@app.route('/soft', methods=method_all) -def soft(pdata=None): - # 软件商店页面 - comReturn = comm.local() - if comReturn: return comReturn - import system - data = system.system().GetConcifInfo() - data['lan'] = public.GetLan('soft') - data['js_random'] = get_js_random() - return render_template('soft.html', data=data) - - -@app.route('/config', methods=method_all) +@app.route('/config', methods=method_post) def config(pdata=None): # 面板设置页面 comReturn = comm.local() if comReturn: return comReturn - if request.method == method_get[0] and not pdata: - import system, wxapp, config - c_obj = config.config() - data = system.system().GetConcifInfo() - data['lan'] = public.GetLan('config') - try: - data['wx'] = wxapp.wxapp().get_user_info(None)['msg'] - except: - data['wx'] = 'INIT_WX_NOT_BIND' - data['api'] = '' - data['ipv6'] = '' - sess_out_path = 'data/session_timeout.pl' - if not os.path.exists(sess_out_path): public.writeFile(sess_out_path, '86400') - s_time_tmp = public.readFile(sess_out_path) - if not s_time_tmp: s_time_tmp = '0' - data['session_timeout'] = int(s_time_tmp) - if c_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' - if c_obj.get_token(None)['open']: data['api'] = 'checked' - data['basic_auth'] = c_obj.get_basic_auth_stat(None) - data['status_code'] = c_obj.get_not_auth_status() - data['basic_auth']['value'] = public.getMsg('CLOSED') - if data['basic_auth']['open']: data['basic_auth']['value'] = public.getMsg('OPENED') - data['debug'] = '' - data['js_random'] = get_js_random() - if app.config['DEBUG']: data['debug'] = 'checked' - data['is_local'] = '' - if public.is_local(): data['is_local'] = 'checked' - data['public_key'] = public.get_rsa_public_key().replace("\n", "") - return render_template('config.html', data=data) import config defs = ( - 'send_by_telegram', 'set_empty', 'set_backup_notification', 'get_panel_ssl_status', 'set_file_deny', - 'del_file_deny', 'get_file_deny', 'set_improvement', - 'get_httpd_access_log_format_parameter', 'set_httpd_format_log_to_website', 'get_httpd_access_log_format', - 'del_httpd_access_log_format', 'add_httpd_access_log_format', 'get_nginx_access_log_format_parameter', - 'set_format_log_to_website', 'get_nginx_access_log_format', 'del_nginx_access_log_format', 'set_click_logs', + 'send_by_telegram', + 'set_empty', + 'set_backup_notification', + 'get_panel_ssl_status', + 'set_file_deny', + 'del_file_deny', + 'get_file_deny', + 'set_improvement', + 'get_httpd_access_log_format_parameter', + 'set_httpd_format_log_to_website', + 'get_httpd_access_log_format', + 'del_httpd_access_log_format', + 'add_httpd_access_log_format', + 'get_nginx_access_log_format_parameter', + 'set_format_log_to_website', + 'get_nginx_access_log_format', + 'del_nginx_access_log_format', + 'set_click_logs', 'get_node_config', - 'add_nginx_access_log_format', 'get_ols_private_cache_status', 'get_ols_value', 'set_ols_value', + 'add_nginx_access_log_format', + 'get_ols_private_cache_status', + 'get_ols_value', + 'set_ols_value', 'set_node_config', - 'get_ols_private_cache', 'get_ols_static_cache', 'set_ols_static_cache', 'switch_ols_private_cache', + 'get_ols_private_cache', + 'get_ols_static_cache', + 'set_ols_static_cache', + 'switch_ols_private_cache', 'set_ols_private_cache', - 'set_coll_open', 'get_qrcode_data', 'check_two_step', 'set_two_step_auth', 'create_user', 'remove_user', + 'set_coll_open', + 'get_qrcode_data', + 'check_two_step', + 'set_two_step_auth', + 'create_user', + 'remove_user', 'modify_user', - 'get_key', 'get_php_session_path', 'set_php_session_path', 'get_cert_source', 'get_users', 'set_request_iptype', - 'set_local', 'set_debug', 'get_panel_error_logs', 'clean_panel_error_logs', 'get_menu_list', + 'get_key', + 'get_php_session_path', + 'set_php_session_path', + 'get_cert_source', + 'get_users', + 'set_request_iptype', + 'set_local', + 'set_debug', + 'get_panel_error_logs', + 'clean_panel_error_logs', + 'get_menu_list', 'set_hide_menu_list', - 'get_basic_auth_stat', 'set_basic_auth', 'get_cli_php_version', 'get_tmp_token', 'get_temp_login', - 'set_temp_login', 'remove_temp_login', 'clear_temp_login', 'get_temp_login_logs', - 'set_cli_php_version', 'DelOldSession', 'GetSessionCount', 'SetSessionConf', 'set_not_auth_status', - 'GetSessionConf', 'get_ipv6_listen', 'set_ipv6_status', 'GetApacheValue', 'SetApacheValue', + 'get_basic_auth_stat', + 'set_basic_auth', + 'get_cli_php_version', + 'get_tmp_token', + 'get_temp_login', + 'set_temp_login', + 'remove_temp_login', + 'clear_temp_login', + 'get_temp_login_logs', + 'set_cli_php_version', + 'DelOldSession', + 'GetSessionCount', + 'SetSessionConf', + 'set_not_auth_status', + 'GetSessionConf', + 'get_ipv6_listen', + 'set_ipv6_status', + 'GetApacheValue', + 'SetApacheValue', 'install_msg_module', - 'GetNginxValue', 'SetNginxValue', 'get_token', 'set_token', 'set_admin_path', 'is_pro', 'set_msg_config', - 'get_php_config', 'get_config', 'SavePanelSSL', 'GetPanelSSL', 'GetPHPConf', 'SetPHPConf', + 'GetNginxValue', + 'SetNginxValue', + 'get_token', + 'set_token', + 'set_admin_path', + 'is_pro', + 'set_msg_config', + 'get_php_config', + 'get_config', + 'SavePanelSSL', + 'GetPanelSSL', + 'GetPHPConf', + 'SetPHPConf', 'uninstall_msg_module', - 'GetPanelList', 'AddPanelInfo', 'SetPanelInfo', 'DelPanelInfo', 'ClickPanelInfo', 'SetPanelSSL', + 'GetPanelList', + 'AddPanelInfo', + 'SetPanelInfo', + 'DelPanelInfo', + 'ClickPanelInfo', + 'SetPanelSSL', 'get_msg_configs', - 'SetTemplates', 'Set502', 'setPassword', 'setUsername', 'setPanel', 'setPathInfo', 'setPHPMaxSize', + 'SetTemplates', + 'Set502', + 'setPassword', + 'setUsername', + 'setPanel', + 'setPathInfo', + 'setPHPMaxSize', 'get_msg_fun', - 'getFpmConfig', 'setFpmConfig', 'setPHPMaxTime', 'syncDate', 'setPHPDisable', 'SetControl', 'get_settings2', - 'del_tg_info', 'set_tg_bot', - 'ClosePanel', 'AutoUpdatePanel', 'SetPanelLock', 'return_mail_list', 'del_mail_list', 'add_mail_address', - 'user_mail_send', 'get_user_mail', 'set_dingding', 'get_dingding', - 'get_settings', 'user_stmp_mail_send', 'user_dingding_send', 'get_login_send', 'set_login_send', - 'clear_login_send', 'get_login_log', 'login_ipwhite', - 'set_ssl_verify', 'get_ssl_verify', 'get_password_config', 'set_password_expire', 'set_password_safe', + 'getFpmConfig', + 'setFpmConfig', + 'setPHPMaxTime', + 'syncDate', + 'setPHPDisable', + 'SetControl', + 'get_settings2', + 'del_tg_info', + 'set_tg_bot', + 'ClosePanel', + 'AutoUpdatePanel', + 'SetPanelLock', + 'return_mail_list', + 'del_mail_list', + 'add_mail_address', + 'user_mail_send', + 'get_user_mail', + 'set_dingding', + 'get_dingding', + 'get_settings', + 'user_stmp_mail_send', + 'user_dingding_send', + 'get_login_send', + 'set_login_send', + 'clear_login_send', + 'get_login_log', + 'login_ipwhite', + 'set_ssl_verify', + 'get_ssl_verify', + 'get_password_config', + 'set_password_expire', + 'set_password_safe', 'get_module_template', # 新增nps评分 - 'write_nps_new', 'get_nps_new', "check_nps", + 'write_nps_new', + 'get_nps_new', + "check_nps", # 提交报错信息 # 错误收集 'err_collection' + ) return publicObject(config.config(), defs, None, pdata) +@app.route('/config', methods=method_get) +def config_old(pdata=None): + # 面板设置页面 + comReturn = comm.local() + if comReturn: return comReturn + + import system, wxapp, config + c_obj = config.config() + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('config') + try: + data['wx'] = wxapp.wxapp().get_user_info(None)['msg'] + except: + data['wx'] = 'INIT_WX_NOT_BIND' + data['api'] = '' + data['ipv6'] = '' + sess_out_path = 'data/session_timeout.pl' + if not os.path.exists(sess_out_path): + public.writeFile(sess_out_path, '86400') + s_time_tmp = public.readFile(sess_out_path) + if not s_time_tmp: s_time_tmp = '0' + data['session_timeout'] = int(s_time_tmp) + if c_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' + if c_obj.get_token(None)['open']: data['api'] = 'checked' + data['basic_auth'] = c_obj.get_basic_auth_stat(None) + data['status_code'] = c_obj.get_not_auth_status() + data['basic_auth']['value'] = public.getMsg('CLOSED') + if data['basic_auth']['open']: + data['basic_auth']['value'] = public.getMsg('OPENED') + data['debug'] = '' + data['js_random'] = get_js_random() + if app.config['DEBUG']: data['debug'] = 'checked' + data['is_local'] = '' + if public.is_local(): data['is_local'] = 'checked' + data['public_key'] = public.get_rsa_public_key().replace("\n", "") + return render_template('config.html', data=data) + + @app.route('/ajax', methods=method_all) def ajax(pdata=None): # 面板系统服务状态接口 @@ -976,18 +1350,22 @@ def ajax(pdata=None): if comReturn: return comReturn import ajax ajaxObject = ajax.ajax() - defs = ('get_lines', 'php_info', 'change_phpmyadmin_ssl_port', 'set_phpmyadmin_ssl', 'get_phpmyadmin_ssl', 'get_pd', - 'check_user_auth', 'to_not_beta', 'get_beta_logs', 'apple_beta', 'GetApacheStatus', 'GetCloudHtml', - 'get_pay_type', - 'get_load_average', 'GetOpeLogs', 'GetFpmLogs', 'GetFpmSlowLogs', 'SetMemcachedCache', 'GetMemcachedStatus', - 'GetRedisStatus', 'GetWarning', 'SetWarning', 'CheckLogin', 'GetSpeed', 'GetAd', 'phpSort', 'ToPunycode', - 'GetBetaStatus', 'SetBeta', 'setPHPMyAdmin', 'delClose', 'KillProcess', 'GetPHPInfo', 'GetQiniuFileList', - 'get_process_tops', 'get_process_cpu_high', - 'UninstallLib', 'InstallLib', 'SetQiniuAS', 'GetQiniuAS', 'GetLibList', 'GetProcessList', 'GetNetWorkList', - 'GetNginxStatus', 'GetPHPStatus', 'GetTaskCount', 'GetSoftList', 'GetNetWorkIo', 'GetDiskIo', 'GetCpuIo', - 'CheckInstalled', 'UpdatePanel', 'GetInstalled', 'GetPHPConfig', 'SetPHPConfig', 'log_analysis', - 'speed_log', - 'get_result', 'get_detailed', 'ignore_version') + defs = ('get_lines', 'php_info', 'change_phpmyadmin_ssl_port', + 'set_phpmyadmin_ssl', 'get_phpmyadmin_ssl', 'get_pd', + 'check_user_auth', 'to_not_beta', 'get_beta_logs', 'apple_beta', + 'GetApacheStatus', 'GetCloudHtml', 'get_pay_type', + 'get_load_average', 'GetOpeLogs', 'GetFpmLogs', 'GetFpmSlowLogs', + 'SetMemcachedCache', 'GetMemcachedStatus', 'GetRedisStatus', + 'GetWarning', 'SetWarning', 'CheckLogin', 'GetSpeed', 'GetAd', + 'phpSort', 'ToPunycode', 'GetBetaStatus', 'SetBeta', + 'setPHPMyAdmin', 'delClose', 'KillProcess', 'GetPHPInfo', + 'GetQiniuFileList', 'get_process_tops', 'get_process_cpu_high', + 'UninstallLib', 'InstallLib', 'SetQiniuAS', 'GetQiniuAS', + 'GetLibList', 'GetProcessList', 'GetNetWorkList', 'GetNginxStatus', + 'GetPHPStatus', 'GetTaskCount', 'GetSoftList', 'GetNetWorkIo', + 'GetDiskIo', 'GetCpuIo', 'CheckInstalled', 'UpdatePanel', + 'GetInstalled', 'GetPHPConfig', 'SetPHPConfig', 'log_analysis', + 'speed_log', 'get_result', 'get_detailed', 'ignore_version') return publicObject(ajaxObject, defs, None, pdata) @@ -999,10 +1377,11 @@ def system(pdata=None): if comReturn: return comReturn import system sysObject = system.system() - defs = ('get_io_info', 'UpdatePro', 'GetAllInfo', 'GetNetWorkApi', 'GetLoadAverage', 'ClearSystem', - 'GetNetWorkOld', 'GetNetWork', 'GetDiskInfo', 'GetCpuInfo', 'GetBootTime', 'GetSystemVersion', - 'GetMemInfo', 'GetSystemTotal', 'GetConcifInfo', 'ServiceAdmin', 'ReWeb', 'RestartServer', 'ReMemory', - 'RepPanel') + defs = ('get_io_info', 'UpdatePro', 'GetAllInfo', 'GetNetWorkApi', + 'GetLoadAverage', 'ClearSystem', 'GetNetWorkOld', 'GetNetWork', + 'GetDiskInfo', 'GetCpuInfo', 'GetBootTime', 'GetSystemVersion', + 'GetMemInfo', 'GetSystemTotal', 'GetConcifInfo', 'ServiceAdmin', + 'ReWeb', 'RestartServer', 'ReMemory', 'RepPanel') return publicObject(sysObject, defs, None, pdata) @@ -1013,7 +1392,8 @@ def deployment(pdata=None): if comReturn: return comReturn import plugin_deployment sysObject = plugin_deployment.plugin_deployment() - defs = ('GetList', 'AddPackage', 'DelPackage', 'SetupPackage', 'GetSpeed', 'GetPackageOther') + defs = ('GetList', 'AddPackage', 'DelPackage', 'SetupPackage', 'GetSpeed', + 'GetPackageOther') return publicObject(sysObject, defs, None, pdata) @@ -1036,30 +1416,30 @@ def ssl(pdata=None): if comReturn: return comReturn import panelSSL toObject = panelSSL.panelSSL() - defs = ('check_url_txt', 'RemoveCert', 'renew_lets_ssl', 'SetCertToSite', 'GetCertList', 'SaveCert', 'GetCert', - 'GetCertName', 'again_verify', - 'DelToken', 'GetToken', 'GetUserInfo', 'GetOrderList', 'GetDVSSL', 'Completed', 'SyncOrder', - 'download_cert', 'set_cert', 'cancel_cert_order', - 'get_order_list', 'get_order_find', 'apply_order_pay', 'get_pay_status', 'apply_order', 'get_verify_info', - 'get_verify_result', 'get_product_list', 'set_verify_info', - 'GetSSLInfo', 'downloadCRT', 'GetSSLProduct', 'Renew_SSL', 'Get_Renew_SSL', - # 新增 购买证书对接接口 - 'get_product_list_v2', 'apply_cert_order_pay', 'get_cert_admin', 'apply_order_ca', 'apply_cert_install_pay', - - # 'pay_test' - - ) + defs = ( + 'check_url_txt', 'RemoveCert', 'renew_lets_ssl', 'SetCertToSite', 'GetCertList', + 'SaveCert', 'GetCert', 'GetCertName', 'again_verify', 'DelToken', 'GetToken', + 'GetUserInfo', 'GetOrderList', 'GetDVSSL', 'Completed', 'SyncOrder', 'download_cert', + 'set_cert', 'cancel_cert_order', 'get_order_list', 'get_order_find', 'apply_order_pay', + 'get_pay_status', 'apply_order', 'get_verify_info', 'get_verify_result', 'get_product_list', + 'set_verify_info', 'GetSSLInfo', 'downloadCRT', 'GetSSLProduct', 'Renew_SSL', 'Get_Renew_SSL', + # 新增 购买证书对接接口 + 'get_product_list_v2', 'apply_cert_order_pay', 'get_cert_admin', 'apply_order_ca', + 'apply_cert_install_pay', + # 'pay_test' + ) get = get_input() if get.action == 'download_cert': from io import BytesIO import base64 result = toObject.download_cert(get) - # public.print_log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@1111111111111111 result: {}".format(result)) - # {'success': False, 'res': '[code: 0] no data [file: /www/wwwroot/192.168.1.139/app/Api/Cert/controllers/Cert.php] [line: 955]', 'nonce': 1706498844} fp = BytesIO(base64.b64decode(result['res']['data'])) - return send_file(fp, download_name=result['res']['filename'], as_attachment=True, mimetype='application/zip') + return send_file(fp, + download_name=result['res']['filename'], + as_attachment=True, + mimetype='application/zip') result = publicObject(toObject, defs, get.action, get) return result @@ -1071,7 +1451,8 @@ def task(pdata=None): if comReturn: return comReturn import panelTask toObject = panelTask.bt_task() - defs = ('get_task_lists', 'remove_task', 'get_task_find', "get_task_log_by_id") + defs = ('get_task_lists', 'remove_task', 'get_task_find', + "get_task_log_by_id") result = publicObject(toObject, defs, None, pdata) return result @@ -1083,14 +1464,15 @@ def plugin(pdata=None): if comReturn: return comReturn import panelPlugin pluginObject = panelPlugin.panelPlugin() - defs = ( - 'get_usually_plugin', 'check_install_limit', 'set_score', 'get_score', 'update_zip', 'input_zip', 'export_zip', - 'add_index', 'remove_index', 'sort_index', - 'install_plugin', 'uninstall_plugin', 'get_soft_find', 'get_index_list', 'get_soft_list', 'get_cloud_list', - 'check_deps', 'flush_cache', 'GetCloudWarning', 'install', 'unInstall', 'getPluginList', 'getPluginInfo', - 'get_make_args', 'add_make_args', - 'getPluginStatus', 'setPluginStatus', 'a', 'getCloudPlugin', 'getConfigHtml', 'savePluginSort', 'del_make_args', - 'set_make_args') + defs = ('get_usually_plugin', 'check_install_limit', 'set_score', + 'get_score', 'update_zip', 'input_zip', 'export_zip', 'add_index', + 'remove_index', 'sort_index', 'install_plugin', 'uninstall_plugin', + 'get_soft_find', 'get_index_list', 'get_soft_list', + 'get_cloud_list', 'check_deps', 'flush_cache', 'GetCloudWarning', + 'install', 'unInstall', 'getPluginList', 'getPluginInfo', + 'get_make_args', 'add_make_args', 'getPluginStatus', + 'setPluginStatus', 'a', 'getCloudPlugin', 'getConfigHtml', + 'savePluginSort', 'del_make_args', 'set_make_args') return publicObject(pluginObject, defs, None, pdata) @@ -1102,7 +1484,8 @@ def panel_wxapp(pdata=None): if comReturn: return comReturn import wxapp toObject = wxapp.wxapp() - defs = ('blind', 'get_safe_log', 'blind_result', 'get_user_info', 'blind_del', 'blind_qrcode') + defs = ('blind', 'get_safe_log', 'blind_result', 'get_user_info', + 'blind_del', 'blind_qrcode') result = publicObject(toObject, defs, None, pdata) return result @@ -1114,16 +1497,19 @@ def auth(pdata=None): if comReturn: return comReturn import panelAuth toObject = panelAuth.panelAuth() - defs = ('free_trial', 'renew_product_auth', 'auth_activate', 'get_product_auth', 'get_stripe_session_id', - 'get_re_order_status_plugin', 'create_plugin_other_order', 'get_order_stat', - 'get_voucher_plugin', 'create_order_voucher_plugin', 'get_product_discount_by', - 'get_re_order_status', 'create_order_voucher', 'create_order', 'get_order_status', - 'get_voucher', 'flush_pay_status', 'create_serverid', 'check_serverid', - 'get_plugin_list', 'check_plugin', 'get_buy_code', 'check_pay_status', + defs = ('free_trial', 'renew_product_auth', 'auth_activate', + 'get_product_auth', 'get_stripe_session_id', + 'get_re_order_status_plugin', 'create_plugin_other_order', + 'get_order_stat', 'get_voucher_plugin', + 'create_order_voucher_plugin', 'get_product_discount_by', + 'get_re_order_status', 'create_order_voucher', 'create_order', + 'get_order_status', 'get_voucher', 'flush_pay_status', + 'create_serverid', 'check_serverid', 'get_plugin_list', + 'check_plugin', 'get_buy_code', 'check_pay_status', 'get_renew_code', 'check_renew_code', 'get_business_plugin', - 'get_ad_list', 'check_plugin_end', 'get_plugin_price', 'get_plugin_remarks', - 'get_paypal_session_id', 'check_paypal_status', - ) + 'get_ad_list', 'check_plugin_end', 'get_plugin_price', + 'get_plugin_remarks', 'get_paypal_session_id', + 'check_paypal_status') result = publicObject(toObject, defs, None, pdata) return result @@ -1136,20 +1522,26 @@ def download(): filename = request.args.get('filename') if filename.find('|') != -1: filename = filename.split('|')[0] # 改为获取本地备份 - if not filename: return public.ReturnJson(False, "INIT_ARGS_ERR"), json_header + if not filename: + return public.ReturnJson(False, "INIT_ARGS_ERR"), json_header # if filename in ['alioss','qiniu','upyun','txcos','ftp','msonedrive','gcloud_storage', 'gdrive', 'aws_s3']: return panel_cloud() - if not os.path.exists(filename): return public.ReturnJson(False, "FILE_NOT_EXISTS"), json_header + if not os.path.exists(filename): + return public.ReturnJson(False, "FILE_NOT_EXISTS"), json_header if request.args.get('play') == 'true': import panelVideo start, end = panelVideo.get_range(request) + g.return_message = True return panelVideo.partial_response(filename, start, end) else: mimetype = "application/octet-stream" extName = filename.split('.')[-1] if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None - public.WriteLog("TYPE_FILE", 'FILE_DOWNLOAD', (filename, public.GetClientIp())) - return send_file(filename, mimetype=mimetype, + public.WriteLog("TYPE_FILE", 'FILE_DOWNLOAD', + (filename, public.GetClientIp())) + g.return_message = True + return send_file(filename, + mimetype=mimetype, as_attachment=True, etag=True, conditional=True, @@ -1163,7 +1555,8 @@ def panel_cloud(is_csrf=True): comReturn = comm.local() if comReturn: return comReturn if is_csrf: - if not check_csrf(): return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header get = get_input() _filename = get.filename plugin_name = "" @@ -1172,19 +1565,25 @@ def panel_cloud(is_csrf=True): else: plugin_name = get.filename - if not os.path.exists('plugin/' + plugin_name + '/' + plugin_name + '_main.py'): - return public.returnJson(False, 'The specified plugin does not exist!'), json_header + if not os.path.exists('plugin/' + plugin_name + '/' + plugin_name + + '_main.py'): + return public.returnJson( + False, 'The specified plugin does not exist!'), json_header public.package_path_append('plugin/' + plugin_name) plugin_main = __import__(plugin_name + '_main') public.mod_reload(plugin_main) tmp = eval("plugin_main.%s_main()" % plugin_name) - if not hasattr(tmp, 'download_file'): return public.returnJson(False, - 'Specified plugin has no file download function!'), json_header + if not hasattr(tmp, 'download_file'): + return public.returnJson( + False, + 'Specified plugin has no file download function!'), json_header download_url = tmp.download_file(get.name) if plugin_name == 'ftp': - if download_url.find("ftp") != 0: download_url = "ftp://" + download_url + if download_url.find("ftp") != 0: + download_url = "ftp://" + download_url else: - if download_url.find('http') != 0: download_url = 'http://' + download_url + if download_url.find('http') != 0: + download_url = 'http://' + download_url if "toserver" in get and get.toserver == "true": download_dir = "/tmp/" @@ -1200,13 +1599,15 @@ def panel_cloud(is_csrf=True): if os.path.isfile(local_file): return { "status": True, - "msg": "The file already exists and will be restored locally.", + "msg": + "The file already exists and will be restored locally.", "task_id": -1, "local_file": local_file } from panelTask import bt_task task_obj = bt_task() - task_id = task_obj.create_task('Download file', 1, download_url, local_file) + task_id = task_obj.create_task('Download file', 1, download_url, + local_file) return { "status": True, "msg": "The download task was created successfully", @@ -1252,17 +1653,26 @@ def proxy_rspamd_requests(path): for h in request.headers.keys(): headers[h] = request.headers[h] if request.method == "GET": - if re.search("\.(js|css)$", path): - return send_file('/usr/share/rspamd/www/rspamd/' + path, conditional=True, etag=True) + if re.search(r"\.(js|css)$", path): + return send_file('/usr/share/rspamd/www/rspamd/' + path, + conditional=True, + etag=True) if path == "/": - return send_file('/usr/share/rspamd/www/rspamd/', conditional=True, etag=True) + return send_file('/usr/share/rspamd/www/rspamd/', + conditional=True, + etag=True) url = "http://127.0.0.1:11334/rspamd/" + path + "?" + param - for i in ['stat', 'auth', 'neighbours', 'list_extractors', 'list_transforms', 'graph', 'maps', 'actions', - 'symbols', 'history', 'errors', 'check_selector', 'saveactions', 'savesymbols', 'getmap']: + for i in [ + 'stat', 'auth', 'neighbours', 'list_extractors', + 'list_transforms', 'graph', 'maps', 'actions', 'symbols', + 'history', 'errors', 'check_selector', 'saveactions', + 'savesymbols', 'getmap' + ]: if i in path: url = "http://127.0.0.1:11334/" + path + "?" + param req = requests.get(url, headers=headers, stream=True) - return Resp(stream_with_context(req.iter_content()), content_type=req.headers['content-type']) + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) else: url = "http://127.0.0.1:11334/" + path for i in request.form.keys(): @@ -1270,7 +1680,8 @@ def proxy_rspamd_requests(path): # public.writeFile('/tmp/2',data+"\n","a+") req = requests.post(url, data=data, headers=headers, stream=True) - return Resp(stream_with_context(req.iter_content()), content_type=req.headers['content-type']) + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) @app.route('/tips', methods=method_get) @@ -1279,16 +1690,14 @@ def tips(): comReturn = comm.local() if comReturn: return abort(404) get = get_input() - if len(get.__dict__.keys()) > 1: return abort(404) + if len(get.get_items().keys()) > 1: return abort(404) return render_template('tips.html') # ======================普通路由区============================# - # ======================严格排查区域============================# - route_path = os.path.join(admin_path, '') if not route_path: route_path = '/' if route_path[-1] == '/': route_path = route_path[:-1] @@ -1303,7 +1712,8 @@ def login(): if os.path.exists('install.pl'): return redirect('/install') global admin_check_auth, admin_path, route_path is_auth_path = False - if admin_path != '/bt' and os.path.exists(admin_path_file) and not 'admin_auth' in session: + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: is_auth_path = True # 登录输入验证 if request.method == method_post[0]: @@ -1315,9 +1725,12 @@ def login(): if v in ['username', 'password']: continue pv = request.form.get(v, '').strip() if v == 'cdn_url': - if len(pv) > 32: return public.return_msg_gettext(False, 'Wrong parameter length!'), json_header - if not re.match(r"^[\w\.-]+$", pv): public.return_msg_gettext(False, - 'Wrong parameter format!'), json_header + if len(pv) > 32: + return public.return_msg_gettext( + False, 'Wrong parameter length!'), json_header + if not re.match(r"^[\w\.-]+$", pv): + public.return_msg_gettext( + False, 'Wrong parameter format!'), json_header continue if not pv: continue @@ -1325,20 +1738,28 @@ def login(): if v == 'code': p_len = 4 if v == 'vcode': p_len = 6 if len(pv) != p_len: - if v == 'code': return public.returnJson(False, 'Verification code length error!'), json_header - return public.returnJson(False, 'Wrong parameter length!'), json_header + if v == 'code': + return public.returnJson( + False, 'Verification code length error!'), json_header + return public.returnJson( + False, 'Wrong parameter length!'), json_header if not re.match(r"^\w+$", pv): - return public.returnJson(False, 'Wrong parameter format!'), json_header - + return public.returnJson( + False, 'Wrong parameter format!'), json_header for n in request.form.keys(): + if not n in v_list: - return public.returnJson(False, 'There can be no extra parameters in the login parameters'), json_header + return public.returnJson( + False, + 'There can be no extra parameters in the login parameters' + ), json_header get = get_input() import userlogin if hasattr(get, 'tmp_token'): result = userlogin.userlogin().request_tmp(get) return is_login(result) + # 过滤爬虫 if public.is_spider(): return abort(404) if hasattr(get, 'dologin'): @@ -1348,8 +1769,10 @@ def login(): if session['login'] != False: session['login'] = False cache.set('dologin', True) - public.write_log_gettext('Logout', 'Client: {}, has manually exited the panel', - (public.GetClientIp() + ":" + str(request.environ.get('REMOTE_PORT')),)) + public.write_log_gettext( + 'Logout', 'Client: {}, has manually exited the panel', + (public.GetClientIp() + ":" + + str(request.environ.get('REMOTE_PORT')),)) if 'tmp_login_expire' in session: s_file = 'data/session/{}'.format(session['tmp_login_id']) if os.path.exists(s_file): @@ -1383,7 +1806,8 @@ def login(): session['admin_auth'] = True comReturn = common.panelSetup().init() - if comReturn: return comReturn + if comReturn: + return comReturn if request.method == method_post[0]: result = userlogin.userlogin().request_post(get) @@ -1404,7 +1828,10 @@ def login(): else: data['hosts'] = json.dumps(data['hosts']) data['app_login'] = os.path.exists('data/app_login.pl') - public.cache_set(public.Md5(uuid.UUID(int=uuid.getnode()).hex[-12:] + public.GetClientIp()), 'check', 360) + public.cache_set( + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp()), 'check', 360) # 生成登录token last_key = 'last_login_token' @@ -1432,19 +1859,14 @@ def login(): # return render_template('login.html', data=data) +# 新增面板内注册 @app.route('/userRegister', methods=method_all) def userRegister(): comReturn = comm.local() if comReturn: return comReturn - # from panelSafeController import SafeController - # project_obj = SafeController() import userRegister reg = userRegister.userRegister() - defs = ('toRegister', ) - # get = get_input() - # get.action = 'model' - # get.mod_name = mod_name - # get.def_name = def_name + defs = ('toRegister',) return publicObject(reg, defs, None, None) @@ -1463,11 +1885,12 @@ def get_app_bind_status(pdata=None): # APP绑定状态查询 if not public.check_app('app_bind'): return abort(404) get = get_input() - if len(get.__dict__.keys()) > 2: return 'There are meaningless parameters!' + if len(get.get_items().keys()) > 2: return 'There are meaningless parameters!' v_list = ['bind_token', 'data'] - for n in get.__dict__.keys(): + for n in get.get_items().keys(): if not n in v_list: - return public.returnJson(False, 'There can be no redundant parameters'), json_header + return public.returnJson( + False, 'There can be no redundant parameters'), json_header import panelApi api_object = panelApi.panelApi() return json.dumps(api_object.get_app_bind_status(get_input())), json_header @@ -1478,11 +1901,12 @@ def check_bind(pdata=None): # APP绑定查询 if not public.check_app('app_bind'): return abort(404) get = get_input() - if len(get.__dict__.keys()) > 4: return 'There are meaningless parameters!' + if len(get.get_items().keys()) > 4: return 'There are meaningless parameters!' v_list = ['bind_token', 'client_brand', 'client_model', 'data'] - for n in get.__dict__.keys(): + for n in get.get_items().keys(): if not n in v_list: - return public.returnJson(False, 'There can be no redundant parameters'), json_header + return public.returnJson( + False, 'There can be no redundant parameters'), json_header import panelApi api_object = panelApi.panelApi() return json.dumps(api_object.check_bind(get_input())), json_header @@ -1527,16 +1951,19 @@ def down(token=None, fname=None): if fname: fname = fname.strip('/') if not token: return abort(404) if len(token) > 48: return abort(404) - char_list = ['\\', '/', ':', '*', '?', '"', '<', '>', '|', ';', '&', '`'] + char_list = [ + '\\', '/', ':', '*', '?', '"', '<', '>', '|', ';', '&', '`' + ] for char in char_list: if char in token: return abort(404) if not request.args.get('play') in ['true', None, '']: return abort(404) args = get_input() v_list = ['fname', 'play', 'file_password', 'data'] - for n in args.__dict__.keys(): + for n in args.get_items().keys(): if not n in v_list: - return public.returnJson(False, 'There can be no redundant parameters'), json_header + return public.returnJson( + False, 'There can be no redundant parameters'), json_header if not re.match(r"^[\w\.]+$", token): return abort(404) find = public.M('download_token').where('token=?', (token,)).find() @@ -1547,12 +1974,14 @@ def down(token=None, fname=None): if find['password'] and not token in session: if 'file_password' in args: if not re.match(r"^\w+$", args.file_password): - return public.ReturnJson(False, 'Wrong password!'), json_header + return public.ReturnJson(False, + 'Wrong password!'), json_header if re.match(r"^\d+$", args.file_password): args.file_password = str(int(args.file_password)) args.file_password += ".0" if args.file_password != str(find['password']): - return public.ReturnJson(False, 'Wrong password!'), json_header + return public.ReturnJson(False, + 'Wrong password!'), json_header session[token] = 1 session['down'] = True else: @@ -1595,7 +2024,8 @@ def down(token=None, fname=None): extName = filename.split('.')[-1] if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None b_name = os.path.basename(filename) - return send_file(filename, mimetype=mimetype, + return send_file(filename, + mimetype=mimetype, as_attachment=True, download_name=b_name, max_age=0) @@ -1603,8 +2033,15 @@ def down(token=None, fname=None): return abort(404) -@app.route('/database//', methods=method_all) -def databaseModel(mod_name, def_name): +@app.route('/database/mongodb/', methods=method_all) +@app.route('/database/pgsql/', methods=method_all) +@app.route('/database/redis/', methods=method_all) +@app.route('/database/sqlite/', methods=method_all) +@app.route('/database/sqlserver/', methods=method_all) +def databaseModel(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 4: return comReturn = comm.local() if comReturn: return comReturn from panelDatabaseController import DatabaseController @@ -1612,15 +2049,23 @@ def databaseModel(mod_name, def_name): defs = ('model',) get = get_input() get.action = 'model' - get.mod_name = mod_name + get.mod_name = path_split[2] get.def_name = def_name return publicObject(project_obj, defs, None, get) # 系统安全模型页面 -@app.route('/safe//', methods=method_all) -def safeModel(mod_name, def_name): +@app.route('/safe/firewall/', methods=method_all) +@app.route('/safe/freeip/', methods=method_all) +@app.route('/safe/ips/', methods=method_all) +@app.route('/safe/security/', methods=method_all) +@app.route('/safe/ssh/', methods=method_all) +@app.route('/safe/syslog/', methods=method_all) +def safeModel(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 4: return comReturn = comm.local() if comReturn: return comReturn from panelSafeController import SafeController @@ -1628,7 +2073,7 @@ def safeModel(mod_name, def_name): defs = ('model',) get = get_input() get.action = 'model' - get.mod_name = mod_name + get.mod_name = path_split[2] get.def_name = def_name return publicObject(project_obj, defs, None, get) @@ -1651,13 +2096,14 @@ def allModule(index, mod_name, def_name): get.action = 'model' get.mod_name = mod_name get.def_name = def_name + return publicObject(controller_obj, defs, None, get) @app.route('/public', methods=method_all) def panel_public(): get = get_input() - if len("{}".format(get.__dict__)) > 1024 * 32: + if len("{}".format(get.get_items())) > 1024 * 32: return 'ERROR' # 获取ping测试 @@ -1675,28 +2121,37 @@ def panel_public(): return abort(404) if public.cache_get( - public.Md5(uuid.UUID(int=uuid.getnode()).hex[-12:] + public.GetClientIp())) != 'check': return abort(404) + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp())) != 'check': + return abort(404) global admin_check_auth, admin_path, route_path, admin_path_file - if admin_path != '/bt' and os.path.exists(admin_path_file) and not 'admin_auth' in session: + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: return abort(404) v_list = ['fun', 'name', 'filename', 'data', 'secret_key'] - for n in get.__dict__.keys(): + for n in get.get_items().keys(): if not n in v_list: return abort(404) get.client_ip = public.GetClientIp() num_key = get.client_ip + '_wxapp' if not public.get_error_num(num_key, 10): - return public.return_msg_gettext(False, '10 consecutive authentication failures are prohibited for 1 hour') + return public.return_msg_gettext( + False, + '10 consecutive authentication failures are prohibited for 1 hour') if not hasattr(get, 'name'): get.name = '' if not hasattr(get, 'fun'): return abort(404) - if not public.path_safe_check("%s/%s" % (get.name, get.fun)): return abort(404) + if not public.path_safe_check("%s/%s" % (get.name, get.fun)): + return abort(404) if get.fun in ['login_qrcode', 'is_scan_ok', 'set_login']: # 检查是否验证过安全入口 - if admin_path != '/bt' and os.path.exists(admin_path_file) and not 'admin_auth' in session: + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: return abort(404) # 验证是否绑定了设备 - if not public.check_app('app'): return public.return_msg_gettext(False, 'Unbound user') + if not public.check_app('app'): + return public.return_msg_gettext(False, 'Unbound user') import wxapp pluwx = wxapp.wxapp() checks = pluwx._check(get) @@ -1712,6 +2167,9 @@ def panel_public(): @app.route('//', methods=method_all) @app.route('///', methods=method_all) def panel_other(name=None, fun=None, stype=None): + if name in ('site', 'database', 'docker', 'wp', 'mail'): + return index_new('{}/{}'.format(name, fun)) + # 插件接口 if public.is_error_path(): return redirect('/error', 302) @@ -1724,7 +2182,7 @@ def panel_other(name=None, fun=None, stype=None): if not stype: tmp = fun.split('.') fun = tmp[0] - if len(tmp) == 1: tmp.append('') + if len(tmp) == 1: tmp.append('') stype = tmp[1] if fun: if name == 'btwaf' and fun == 'index': @@ -1736,16 +2194,23 @@ def panel_other(name=None, fun=None, stype=None): elif stype == 'html': pass else: - if public.get_csrf_cookie_token_key() in session and 'login' in session: - if not check_csrf(): return public.ReturnJson(False, - 'CSRF calibration failed, please login again'), json_header + if public.get_csrf_cookie_token_key( + ) in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson( + False, + 'CSRF calibration failed, please login again' + ), json_header args = None else: p_path = public.get_plugin_path() + '/' + name if not os.path.exists(p_path): return abort(404) args = get_input() - args_list = ['mail_from', 'password', 'mail_to', 'subject', 'content', 'subtype', 'data'] - for k in args.__dict__: + args_list = [ + 'mail_from', 'password', 'mail_to', 'subject', 'content', + 'subtype', 'data' + ] + for k in args.get_items(): if not k in args_list: return abort(404) is_accept = False @@ -1753,13 +2218,17 @@ def panel_other(name=None, fun=None, stype=None): if not stype: tmp = fun.split('.') fun = tmp[0] - if len(tmp) == 1: tmp.append('') + if len(tmp) == 1: tmp.append('') stype = tmp[1] if not name: name = 'coll' - if not public.path_safe_check("%s/%s/%s" % (name, fun, stype)): return abort(404) - if name.find('./') != -1 or not re.match(r"^[\w-]+$", name): return abort(404) - if not name: return public.returnJson(False, 'Please pass in the plug-in name!'), json_header + if not public.path_safe_check("%s/%s/%s" % (name, fun, stype)): + return abort(404) + if name.find('./') != -1 or not re.match(r"^[\w-]+$", name): + return abort(404) + if not name: + return public.returnJson( + False, 'Please pass in the plug-in name!'), json_header p_path = public.get_plugin_path() + '/' + name if not os.path.exists(p_path): if name == 'btwaf' and fun == 'index': @@ -1778,7 +2247,8 @@ def panel_other(name=None, fun=None, stype=None): # 是否响插件应静态文件 if fun == 'static': - if stype.find('./') != -1 or not os.path.exists(p_path + '/static'): return abort(404) + if stype.find('./') != -1 or not os.path.exists(p_path + '/static'): + return abort(404) s_file = p_path + '/static/' + stype if s_file.find('..') != -1: return abort(404) if not re.match(r"^[\w\./-]+$", s_file): return abort(404) @@ -1793,66 +2263,20 @@ def panel_other(name=None, fun=None, stype=None): # 初始化插件对象 try: - is_php = os.path.exists(p_path + '/index.php') - if not is_php: - import panelPlugin - plu_panel = panelPlugin.panelPlugin() - plugin_list = plu_panel.get_cloud_list() - waf = 0 - if not 'pro' in plugin_list: plugin_list['pro'] = -1 - for p in plugin_list['list']: - if p['name'] in ['btwaf']: - if p['endtime'] != 0 and p['endtime'] < time.time(): - waf = -1 - try: - public.package_path_append(p_path) - plugin_main = __import__(name + '_main') - if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list['pro'] == -1: - return render_template('error3.html', data={}) - except: - if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list['pro'] == -1: - return render_template('error3.html', data={}) - if os.path.exists("{}/btwaf".format(public.get_plugin_path())): - return render_template('error3.html', data={}) - try: - if sys.version_info[0] == 2: - reload(plugin_main) - else: - from imp import reload - reload(plugin_main) - except: - pass - # public.print_log("plugin_main22222: {}".format(plugin_main)) - plu = eval('plugin_main.' + name + '_main()') - - # methods = dir(plu) - # # 遍历列表并打印每个方法的名称 - # for method in methods: - # # 排除以双下划线开头和结尾的特殊属性和方法 - # if not method.startswith("__") and not method.endswith("__"): - # public.print_log(method) - - if not hasattr(plu, fun): - # public.print_log("333333: {}".format(plu)) - # public.print_log("444444: {}".format(fun)) - return public.returnJson(False, 'Plugin does not exist'), json_header - # public.print_log("aaaa plu: {}".format(plu)) - # public.print_log("bbbbb fun: {}".format(fun)) - # 执行插件方法 - if not is_php: - if is_accept: - checks = plu._check(args) - if type(checks) != bool or not checks: - return public.getJson(checks), json_header - data = eval('plu.' + fun + '(args)') - else: - comReturn = comm.local() - if comReturn: return comReturn - import panelPHP + import PluginLoader + try: args.s = fun - args.name = name - data = panelPHP.panelPHP(name).exec_php_script(args) + data = PluginLoader.plugin_run(name, fun, args) + if isinstance(data, dict): + if 'status' in data and data['status'] == False and 'msg' in data: + if isinstance(data['msg'], str): + if data['msg'].find('加载失败') != -1 or data['msg'].find('Traceback ') == 0: + raise public.PanelError(data['msg']) + except Exception as ex: + if name == 'btwaf' and fun == 'index' and str(ex).find('未购买') != -1: + return render_template('error3.html', data={}) + return public.get_error_object(None, plugin_name=name) r_type = type(data) if r_type in [Response, Resp]: @@ -1865,7 +2289,9 @@ def panel_other(name=None, fun=None, stype=None): t_path_root = p_path + '/templates/' t_path = t_path_root + fun + '.html' if not os.path.exists(t_path): - return public.returnJson(False, 'The specified template does not exist!'), json_header + return public.returnJson( + False, + 'The specified template does not exist!'), json_header t_body = public.readFile(t_path) # 处理模板包含 @@ -1883,8 +2309,12 @@ def panel_other(name=None, fun=None, stype=None): r_type = type(data) if r_type == dict: if name == 'btwaf' and 'msg' in data: - return render_template('error3.html', data={"error_msg": data['msg']}) - return public.returnJson(False, public.getMsg('Bad return type [{}]').format(r_type)), json_header + return render_template('error3.html', + data={"error_msg": data['msg']}) + return public.returnJson( + False, + public.getMsg('Bad return type [{}]').format(r_type)), json_header + # public.getMsg('PUBLIC_ERR_RETURN')), json_header return data except: return public.get_error_info() @@ -1925,17 +2355,22 @@ def install(): elif request.method == method_post[0]: if not os.path.exists('install.pl'): return redirect(ret_login) get = get_input() - if not hasattr(get, 'bt_username'): return public.get_msg_gettext('The user name cannot be empty!') - if not get.bt_username: return public.get_msg_gettext('The user name cannot be empty!') - if not hasattr(get, 'bt_password1'): return public.get_msg_gettext('Password can not be blank!') - if not get.bt_password1: return public.get_msg_gettext('Password can not be blank!') - if get.bt_password1 != get.bt_password2: return public.get_msg_gettext( - 'The passwords entered twice do not match, please re-enter!') - public.M('users').where("id=?", (1,)).save('username,password', - (get.bt_username, - public.password_salt(public.md5(get.bt_password1.strip()), uid=1) - ) - ) + if not hasattr(get, 'bt_username'): + return public.get_msg_gettext('The user name cannot be empty!') + if not get.bt_username: + return public.get_msg_gettext('The user name cannot be empty!') + if not hasattr(get, 'bt_password1'): + return public.get_msg_gettext('Password can not be blank!') + if not get.bt_password1: + return public.get_msg_gettext('Password can not be blank!') + if get.bt_password1 != get.bt_password2: + return public.get_msg_gettext( + 'The passwords entered twice do not match, please re-enter!') + public.M('users').where("id=?", (1,)).save( + 'username,password', + (get.bt_username, + public.password_salt(public.md5(get.bt_password1.strip()), + uid=1))) os.remove('install.pl') public.M('config').where("id=?", ('1',)).setField('status', 1) data = {} @@ -1946,7 +2381,6 @@ def install(): # ==================================================# - # ======================公共方法区域START============================# @@ -1971,7 +2405,8 @@ def get_dir_down(filename, token, find): pdata['expire'] = public.format_date(times=find['expire']) else: pdata['expire'] = public.get_msg_gettext('Never Expires') - pdata['filename'] = (find['filename'].split('/')[-1] + '/' + to_path).strip('/') + pdata['filename'] = (find['filename'].split('/')[-1] + '/' + + to_path).strip('/') return render_template('down.html', data=pdata, to_size=public.to_size) @@ -1983,21 +2418,24 @@ def get_phpmyadmin_dir(): try: import re if session['webserver'] == 'nginx': - filename = public.GetConfigValue('setup_path') + '/nginx/conf/nginx.conf' + filename = public.GetConfigValue( + 'setup_path') + '/nginx/conf/nginx.conf' conf = public.readFile(filename) rep = r"listen\s+([0-9]+)\s*;" rtmp = re.search(rep, conf) if rtmp: phpport = rtmp.groups()[0] if session['webserver'] == 'apache': - filename = public.GetConfigValue('setup_path') + '/apache/conf/extra/httpd-vhosts.conf' + filename = public.GetConfigValue( + 'setup_path') + '/apache/conf/extra/httpd-vhosts.conf' conf = public.readFile(filename) rep = r"Listen\s+([0-9]+)\s*\n" rtmp = re.search(rep, conf) if rtmp: phpport = rtmp.groups()[0] if session['webserver'] == 'openlitespeed': - filename = public.GetConfigValue('setup_path') + '/panel/vhost/openlitespeed/listen/888.conf' + filename = public.GetConfigValue( + 'setup_path') + '/panel/vhost/openlitespeed/listen/888.conf' public.writeFile('/tmp/2', filename) conf = public.readFile(filename) rep = r"address\s*\*\:\s*(\d+)" @@ -2019,12 +2457,11 @@ class run_exec: # 模块访问对像 def run(self, toObject, defs, get): result = None - # public.print_log("OOOOOOOOOOOO))))))))))))))))))000000000000000000000000000000000run_exec: ") - # public.print_log("defs----: {}".format(defs)) - # public.print_log("get ----: {}".format(get)) if not get.action in defs: - return public.ReturnJson(False, 'Specific parameters are invalid!'), json_header + return public.ReturnJson( + False, 'Specific parameters are invalid!'), json_header + result = getattr(toObject, get.action)(get) if not hasattr(get, 'html') and not hasattr(get, 's_module'): r_type = type(result) @@ -2033,6 +2470,7 @@ class run_exec: if g.is_aes: result = public.aes_encrypt(result[0], g.aes_key), json_header + return result @@ -2048,15 +2486,19 @@ def check_csrf(): def publicObject(toObject, defs, action=None, get=None, is_csrf=True): try: # 模块访问前置检查 - if is_csrf and public.get_csrf_sess_html_token_value() and session.get('login', None): - if not check_csrf(): return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + if is_csrf and public.get_csrf_sess_html_token_value() and session.get( + 'login', None): + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header - if not get: get = get_input() + if not get: + get = get_input() if action: get.action = action if hasattr(get, 'path'): get.path = get.path.replace('//', '/').replace('\\', '/') - if get.path.find('./') != -1: return public.ReturnJson(False, 'Unsafe path'), json_header + if get.path.find('./') != -1: + return public.ReturnJson(False, 'Unsafe path'), json_header if get.path.find('->') != -1: get.path = get.path.split('->')[0].strip() get.path = public.xssdecode(get.path) @@ -2071,11 +2513,12 @@ def publicObject(toObject, defs, action=None, get=None, is_csrf=True): get.dfile = public.xssdecode(get.dfile) if hasattr(toObject, 'site_path_check'): - if not toObject.site_path_check(get): return public.ReturnJson(False, - "Overstepping one authority!"), json_header + if not toObject.site_path_check(get): + return public.ReturnJson( + False, "Overstepping one authority!"), json_header return run_exec().run(toObject, defs, get) - except: - return error_500(None) + except Exception as e: + return error_500(e) def check_login(http_token=None): @@ -2084,121 +2527,23 @@ def check_login(http_token=None): if 'login' in session: loginStatus = session['login'] if loginStatus and http_token: - if public.get_csrf_sess_html_token_value() != http_token: return False + if public.get_csrf_sess_html_token_value() != http_token: + return False return loginStatus return False def get_pd(): # 获取授权信息 - tmp = -1 - # try: - # import panelPlugin - # get = public.dict_obj() - # # get.init = 1 - # tmp1 = panelPlugin.panelPlugin().get_cloud_list(get) - # except: - tmp1 = None - if tmp1: - tmp = tmp1[public.to_string([112, 114, 111])] - ltd = tmp1.get('ltd', -1) - else: - ltd = -1 - tmp4 = cache.get(public.to_string([112, 95, 116, 111, 107, 101, 110])) - if tmp4: - tmp_f = public.to_string([47, 116, 109, 112, 47]) + tmp4 - if not os.path.exists(tmp_f): public.writeFile(tmp_f, '-1') - tmp = public.readFile(tmp_f) - if tmp: tmp = int(tmp) - - if ltd < 1: - if ltd == -2: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 116, 100, - 45, 103, 114, 97, 121, 34, 62, 60, 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, - 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, - 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, - 114, 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, - 36807, - 26399, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, - 98, 116, - 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, - 115, 111, - 102, 116, 46, 117, 112, 100, 97, 116, 97, 95, 108, 116, 100, 40, 41, 34, 62, 82, - 69, 78, 69, 87, 60, 47, 97, - 62, 60, 47, 115, 112, 97, 110, 62]) - elif tmp == -1: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, - 116, 112, 114, 111, 45, 102, 114, 101, 101, 34, 32, 111, 110, 99, 108, 105, 99, - 107, - 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, 114, - 111, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, 107, 32, - 116, 111, 32, - 103, 101, 116, 32, 80, 82, 79, 34, 62, 20813, 36153, 29256, 60, 47, 115, 112, 97, - 110, 62]) - elif tmp == -2: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, - 112, 114, 111, 45, 103, 114, 97, 121, 34, 62, 60, 115, 112, 97, 110, 32, - 115, 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, - 102, 99, 54, 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, - 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, - 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, 36807, 26399, - 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, - 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, - 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, 114, - 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, 112, 97, 110, - 62]) - if tmp >= 0 and ltd in [-1, -2]: - if tmp == 0: - tmp2 = public.to_string([76, 105, 102, 101, 116, 105, 109, 101]) - - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 112, 114, - 111, 34, 62, 69, 120, 112, 105, 114, 101, 58, 60, 115, 112, 97, 110, 32, 115, - 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, - 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, - 98, 111, 108, 100, 59, 34, 62, 123, 48, 125, 60, 47, 115, 112, 97, 110, 62, 60, - 47, 115, 112, 97, 110, 62]).format(tmp2) - - else: - tmp2 = time.strftime(public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), time.localtime(tmp)) - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, - 112, 114, 111, 34, 62, 69, 120, 112, 105, 114, 101, 58, 32, 60, 115, 112, - 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, - 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, - 101, 105, 103, 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, - 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 123, - 48, 125, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, - 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, - 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, - 112, 114, 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, - 47, 115, 112, 97, 110, 62]).format(tmp2) - else: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 112, - 114, 111, 45, 103, 114, 97, 121, 34, 32, 111, 110, 99, 108, 105, 99, 107, - 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, - 114, 111, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, 107, - 32, 116, - 111, 32, 103, 101, 116, 32, 80, 82, 79, 34, 62, 70, 82, - 69, 69, 60, 47, 115, 112, 97, 110, 62]) - else: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 116, - 100, 34, 62, 69, 120, 112, 105, 114, 101, 58, 32, 60, 115, 112, 97, 110, 32, 115, 116, - 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, - 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, - 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, - 112, 120, 34, 62, 123, 125, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, - 97, 115, 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, - 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, - 112, 114, 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, - 112, 97, 110, 62]).format( - time.strftime(public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), time.localtime(ltd))) - - return tmp3, tmp, ltd + return public.get_pd() def send_authenticated(): # 发送http认证信息 request_host = public.GetHost() - result = Response('', 401, {'WWW-Authenticate': 'Basic realm="%s"' % request_host.strip()}) + result = Response( + '', 401, + {'WWW-Authenticate': 'Basic realm="%s"' % request_host.strip()}) if not 'login' in session and not 'admin_auth' in session: session.clear() return result @@ -2209,7 +2554,8 @@ def FtpPort(): if session.get('port'): return import re try: - file = public.GetConfigValue('setup_path') + '/pure-ftpd/etc/pure-ftpd.conf' + file = public.GetConfigValue( + 'setup_path') + '/pure-ftpd/etc/pure-ftpd.conf' conf = public.readFile(file) rep = r"\n#?\s*Bind\s+[0-9]+\.[0-9]+\.[0-9]+\.+[0-9]+,([0-9]+)" port = re.search(rep, conf).groups()[0] @@ -2222,20 +2568,6 @@ def is_login(result): # 判断是否登录2 if 'login' in session: if session['login'] == True: - # result = make_response(result) - # request_token = public.GetRandomString(48) - # request_token_key = public.get_csrf_cookie_token_key() - # session[request_token_key] = request_token - # samesite = app.config['SESSION_COOKIE_SAMESITE'] - # secure = app.config['SESSION_COOKIE_SECURE'] - # if app.config['SSL'] and request.full_path.find('/login?tmp_token=') == 0: - # samesite = 'None' - # secure = True - # result.set_cookie(request_token_key, request_token, - # max_age=86400 * 30, - # samesite= samesite, - # secure=secure - # ) pass return result @@ -2257,9 +2589,12 @@ def get_input(): data.set(key, str(request.args.get(key, ''))) try: for key in request.form.keys(): - if key in exludes: continue + if key in exludes: + continue data.set(key, str(request.form.get(key, ''))) - except: + + except Exception as ex: + try: post = request.form.to_dict() for key in post.keys(): @@ -2272,7 +2607,8 @@ def get_input(): for k in g.form_data.keys(): data.set(k, str(g.form_data[k])) - if not hasattr(data, 'data'): data.data = [] + if not hasattr(data, 'data'): + data.data = [] return data @@ -2357,53 +2693,65 @@ def ws_panel_thread(get): ''' if not hasattr(get, 'ws_callback'): - get._ws.send(public.getJson(public.return_status_code(1001, 'ws_callback'))) + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) return if not hasattr(get, 'mod_name'): - get._ws.send(public.getJson(public.return_status_code(1001, 'mod_name'))) + get._ws.send( + public.getJson(public.return_status_code(1001, 'mod_name'))) return if not hasattr(get, 'def_name'): - get._ws.send(public.getJson(public.return_status_code(1001, 'def_name'))) + get._ws.send( + public.getJson(public.return_status_code(1001, 'def_name'))) return get.mod_name = get.mod_name.strip() get.def_name = get.def_name.strip() check_str = '{}{}'.format(get.mod_name, get.def_name) - if not re.match("^\w+$", check_str) or get.mod_name in ['public', 'common', 'db', 'db_mysql', 'downloadFile', - 'jobs']: - get._ws.send(public.getJson(public.return_status_code(1000, 'Unsafe mod_name, def_name parameter content'))) + if not re.match(r"^\w+$", check_str) or get.mod_name in [ + 'public', 'common', 'db', 'db_mysql', 'downloadFile', 'jobs' + ]: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Unsafe mod_name, def_name parameter content'))) return - # if not hasattr(get, 'args'): - # get._ws.send(public.getJson(public.return_status_code(1001, 'args'))) - # return mod_file = '{}/{}.py'.format(public.get_class_path(), get.mod_name) + if not os.path.exists(mod_file): get._ws.send( - public.getJson(public.return_status_code(1000, 'Specified module {} does not exist'.format(get.mod_name)))) + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) return _obj = public.get_script_object(mod_file) if not _obj: get._ws.send( - public.getJson(public.return_status_code(1000, 'Specified module {} does not exist'.format(get.mod_name)))) + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) return _cls = getattr(_obj, get.mod_name) if not _cls: get._ws.send( - public.getJson(public.return_status_code(1000, - 'The {} object was not found in the {} module'.format(get.mod_name, - get.mod_name)))) + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.mod_name)))) return _def = getattr(_cls(), get.def_name) if not _def: get._ws.send( - public.getJson(public.return_status_code(1000, - 'The {} object was not found in the {} module'.format(get.mod_name, - get.def_name)))) + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.def_name)))) return - result = { - 'callback': get.ws_callback, - 'result': _def(get) - } + result = {'callback': get.ws_callback, 'result': _def(get)} get._ws.send(public.getJson(result)) @@ -2432,6 +2780,59 @@ def ws_project(ws): p.start() +# docker模块内用到的ws +@sockets.route('/ws_model') +def ws_model(ws): + ''' + @name 模型控制器ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + + comReturn = comm.local() + if comReturn: return comReturn + get = ws.receive() + get = json.loads(get) + + if not check_csrf_websocket(ws, get): return + + from panelController import Controller + model_obj = Controller() + while True: + pdata = ws.receive() + if pdata in ['{}', {}, None, '']: + ws.send(json.dumps(public.return_status_code(1000, '请求参数不能为空'))) + break + try: + get = public.to_dict_obj(json.loads(pdata)) + except: + request.form = { + "error": pdata + } + raise Exception('json load error !') + get._ws = ws + get.model_index = get.model_index.strip() + p = threading.Thread(target=ws_model_thread, args=(model_obj, get)) + p.start() + + +def ws_model_thread(_obj, get): + ''' + @name 模型控制器ws线程 + @author hwliang<2021-07-24> + @param _obj 控制器对像 + @param get 请求参数 + @return void + ''' + if not hasattr(get, 'ws_callback'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) + return + result = {'callback': get.ws_callback, 'result': _obj.model(get)} + get._ws.send(public.getJson(result)) + + def ws_project_thread(_obj, get): ''' @name 项目管理ws线程 @@ -2441,12 +2842,10 @@ def ws_project_thread(_obj, get): @return void ''' if not hasattr(get, 'ws_callback'): - get._ws.send(public.getJson(public.return_status_code(1001, 'ws_callback'))) + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) return - result = { - 'callback': get.ws_callback, - 'result': _obj.model(get) - } + result = {'callback': get.ws_callback, 'result': _obj.model(get)} get._ws.send(public.getJson(result)) @@ -2524,15 +2923,23 @@ def kill_closed(): def sock_recv(cmdstring, ws): global sock_pids try: - p = subprocess.Popen(cmdstring + " 2>&1", close_fds=True, shell=True, bufsize=4096, stdout=subprocess.PIPE, + p = subprocess.Popen(cmdstring + " 2>&1", + close_fds=True, + shell=True, + bufsize=4096, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) sock_pids[p.pid] = ws kill_closed() while p.poll() == None: send_line = p.stdout.readline().decode() if not send_line or send_line.find('tail: ') != -1: continue - ws.send(send_line) - ws.send(p.stdout.read().decode()) + # ws.send(send_line) + # ws.send(p.stdout.read().decode()) + if ws.connected: + ws.send(send_line) + if ws.connected: + ws.send(p.stdout.read().decode()) except: kill_closed() @@ -2550,16 +2957,20 @@ def close_sock_shell(): comReturn = comm.local() if comReturn: return comReturn args = get_input() - if not check_csrf(): return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header cmdstring = args.cmdstring.strip() skey = public.md5(cmdstring) pid = cache.get(skey) if not pid: return json.dumps( - public.return_data(False, [], error_msg='The specified sock has been terminated!')), json_header + public.return_data( + False, [], error_msg='The specified sock has been terminated!') + ), json_header os.kill(pid, 9) cache.delete(skey) - return json.dumps(public.return_data(True, 'Successful operation!')), json_header + return json.dumps(public.return_data(True, + 'Successful operation!')), json_header def check_csrf_websocket(ws, args): @@ -2615,7 +3026,8 @@ def webssh(ws): if 'pkey' in get: ssh_info['pkey'] = get['pkey'].strip() - if get['host'] in ['127.0.0.1', 'localhost'] and 'port' not in ssh_info: + if get['host'] in ['127.0.0.1', 'localhost' + ] and 'port' not in ssh_info: ssh_info = sp.get_ssh_info('127.0.0.1') if not ssh_info: ssh_info = sp.get_ssh_info('localhost') if not ssh_info: ssh_info = {"host": "127.0.0.1"} @@ -2630,7 +3042,9 @@ def webssh(ws): if not 'username' in ssh_info: ssh_info = sp.get_ssh_info(ssh_info['host']) if not ssh_info: - ws.send('The specified host information is not found, please add it again!') + ws.send( + 'The specified host information is not found, please add it again!' + ) return p = ssh_terminal.ssh_terminal() p.run(ws, ssh_info) @@ -2642,6 +3056,7 @@ def webssh(ws): # --------------------- websocket END -------------------------- # + @app.route("/daily", methods=method_all) def daily(): """面板日报数据""" @@ -2670,7 +3085,8 @@ def pma_proxy(path_full=None): pmd = cache.get(cache_key) if not pmd: pmd = get_phpmyadmin_dir() - if not pmd: return 'phpMyAdmin is not installed, please go to the [App Store] page to install it!' + if not pmd: + return 'phpMyAdmin is not installed, please go to the [App Store] page to install it!' pmd = list(pmd) cache.set(cache_key, pmd, 10) panel_pool = 'http://' @@ -2683,7 +3099,9 @@ def pma_proxy(path_full=None): else: panel_pool = 'http://' - proxy_url = '{}127.0.0.1:{}/{}/'.format(panel_pool, pmd[1], pmd[0]) + request.full_path.replace('/phpmyadmin/', '') + proxy_url = '{}127.0.0.1:{}/{}/'.format( + panel_pool, pmd[1], pmd[0]) + request.full_path.replace( + '/phpmyadmin/', '') from panelHttpProxy import HttpProxy px = HttpProxy() return px.proxy(proxy_url) @@ -2701,7 +3119,8 @@ def proxy_port(port, full_path=None): comReturn = comm.local() if comReturn: return comReturn - full_path = request.full_path.replace('/p/{}/'.format(port), '').replace('/p/{}'.format(port), '') + full_path = request.full_path.replace('/p/{}/'.format(port), + '').replace('/p/{}'.format(port), '') uri = '{}/{}'.format(port, full_path) uri = uri.replace('//', '/') proxy_url = 'http://127.0.0.1:{}'.format(uri) @@ -2716,8 +3135,2621 @@ def push(pdata=None): if comReturn: return comReturn import panelPush toObject = panelPush.panelPush() - defs = ('set_push_status', 'get_push_msg_list', 'get_modules_list', 'install_module', 'uninstall_module', - 'get_module_template', 'set_push_config', 'get_push_config', 'del_push_config', 'get_module_logs', - 'get_module_config', 'get_push_list', 'get_push_logs') + defs = ('set_push_status', 'get_push_msg_list', 'get_modules_list', + 'install_module', 'uninstall_module', 'get_module_template', + 'set_push_config', 'get_push_config', 'del_push_config', + 'get_module_logs', 'get_module_config', 'get_push_list', + 'get_push_logs') result = publicObject(toObject, defs, None, pdata) return result + + +# ===========================================================v2路由区start===========================================================# +# docker模块内用到的ws +@sockets.route(route_v2 + '/ws_model') +def ws_model_v2(ws): + ''' + @name 模型控制器ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + # 开发时暂时注释---------------- + comReturn = comm.local() + if comReturn: return comReturn + get = ws.receive() + get = json.loads(get) + + # 开发时暂时注释---------------- + if not check_csrf_websocket(ws, get): return + + from panelControllerV2 import Controller + model_obj = Controller() + while True: + pdata = ws.receive() + if pdata in ['{}', {}, None, '']: + ws.send(json.dumps(public.return_status_code(1000, 'The request parameter cannot be null'))) + break + try: + get = public.to_dict_obj(json.loads(pdata)) + except: + request.form = { + "error": pdata + } + raise Exception('json load error !') + get._ws = ws + get.model_index = get.model_index.strip() + p = threading.Thread(target=ws_model_thread, args=(model_obj, get)) + p.start() + + +# ======================普通路由区start============================# + + +@app.route(route_v2 + '/', methods=method_all) +def home_v2(): + # 面板首页 + comReturn = comm.local() + if comReturn: return comReturn + data = {} + data[public.to_string([112, + 100])], data['pro_end'], data['ltd_end'] = get_pd() + data['siteCount'] = public.M('sites').count() + data['ftpCount'] = public.M('ftps').count() + data['databaseCount'] = public.M('databases').count() + data['lan'] = public.GetLan('index') + data['js_random'] = get_js_random() + return render_template('index.html', data=data) + + +@app.route(route_v2 + '/xterm', methods=method_all) +def xterm_v2(): + # 宝塔终端管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0]: + import system_v2 + data = system_v2.system().GetConcifInfo() + return render_template('xterm.html', data=data) + import ssh_terminal_v2 + ssh_host_admin = ssh_terminal_v2.ssh_host_admin() + defs = ('get_host_list', 'get_host_find', 'modify_host', 'create_host', + 'remove_host', 'set_sort', 'get_command_list', 'create_command', + 'get_command_find', 'modify_command', 'remove_command') + return publicObject(ssh_host_admin, defs, None) + + +@app.route(route_v2 + '/modify_password', methods=method_get) +def modify_password_v2(): + comReturn = comm.local() + if comReturn: return comReturn + # if not session.get('password_expire',False): return redirect('/',302) + data = {} + g.title = public.get_msg_gettext( + 'The password has expired, please change it!') + return render_template('modify_password.html', data=data) + + +@app.route(route_v2 + '/site', methods=method_all) +def site_v2(pdata=None): + # 网站管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + # data = {} + import system_v2 + data = system_v2.system().GetConcifInfo() + data['isSetup'] = True + data['lan'] = public.getLan('site') + data['js_random'] = get_js_random() + if os.path.exists(public.GetConfigValue('setup_path') + '/nginx') == False \ + and os.path.exists(public.GetConfigValue('setup_path') + '/apache') == False \ + and os.path.exists('/usr/local/lsws/bin/lswsctrl') == False: + data['isSetup'] = False + return render_template('site.html', data=data) + + import panel_site_v2 + siteObject = panel_site_v2.panelSite() + defs = ( + 'get_auto_restart_rph', + 'remove_auto_restart_rph', + 'auto_restart_rph', + 'check_del_data', + 'upload_csv', + 'create_website_multiple', + 'del_redirect_multiple', + 'del_proxy_multiple', + 'delete_dir_auth_multiple', + 'delete_dir_bind_multiple', + 'delete_domain_multiple', + 'set_site_etime_multiple', + 'set_site_php_version_multiple', + 'delete_website_multiple', + 'set_site_status_multiple', + 'get_site_err_log', + 'get_site_domains', + 'GetRedirectFile', + 'SaveRedirectFile', + 'DeleteRedirect', + 'GetRedirectList', + 'CreateRedirect', + 'ModifyRedirect', + "set_error_redirect", + 'set_dir_auth', + 'delete_dir_auth', + 'get_dir_auth', + 'modify_dir_auth_pass', + 'reset_wp_db', + 'export_domains', + 'import_domains', + 'GetSiteLogs', + 'GetSiteDomains', + 'GetSecurity', + 'SetSecurity', + 'ProxyCache', + 'CloseToHttps', + 'HttpToHttps', + 'SetEdate', + 'SetRewriteTel', + 'GetCheckSafe', + 'CheckSafe', + 'GetDefaultSite', + 'SetDefaultSite', + 'CloseTomcat', + 'SetTomcat', + 'apacheAddPort', + 'AddSite', + 'GetPHPVersion', + 'SetPHPVersion', + 'DeleteSite', + 'AddDomain', + 'DelDomain', + 'GetDirBinding', + 'AddDirBinding', + 'GetDirRewrite', + 'DelDirBinding', + 'get_site_types', + 'add_site_type', + 'remove_site_type', + 'modify_site_type_name', + 'set_site_type', + 'UpdateRulelist', + 'SetSiteRunPath', + 'GetSiteRunPath', + 'SetPath', + 'SetIndex', + 'GetIndex', + 'GetDirUserINI', + 'SetDirUserINI', + 'GetRewriteList', + 'SetSSL', + 'SetSSLConf', + 'CreateLet', + 'CloseSSLConf', + 'GetSSL', + 'SiteStart', + 'SiteStop', + 'Set301Status', + 'Get301Status', + 'CloseLimitNet', + 'SetLimitNet', + 'GetLimitNet', + 'RemoveProxy', + 'GetProxyList', + 'GetProxyDetals', + 'CreateProxy', + 'ModifyProxy', + 'GetProxyFile', + 'SaveProxyFile', + 'ToBackup', + 'DelBackup', + 'GetSitePHPVersion', + 'logsOpen', + 'GetLogsStatus', + 'CloseHasPwd', + 'SetHasPwd', + 'GetHasPwd', + 'GetDnsApi', + 'SetDnsApi', + 'reset_wp_password', + 'is_update', + 'purge_all_cache', + 'set_fastcgi_cache', + 'update_wp', + 'get_wp_username', + 'get_language', + 'deploy_wp', + # 网站管理新增 + 'test_domains_api', + 'site_rname', + 'get_wp_versions', + 'AddWPSite', + 'get_wp_configurations', + 'save_wp_configurations', + 'wp_backup_list', + 'wp_backup', + 'wp_restore', + 'wp_remove_backup', + 'get_wp_security_info', + 'open_wp_file_protection', + 'close_wp_file_protection', + 'get_wp_file_info', + 'open_wp_firewall_protection', + 'close_wp_firewall_protection', + 'get_wp_firewall_info', + 'wp_migrate_from_website_to_wptoolkit', + 'wp_can_migrate_from_website_to_wptoolkit', + 'wp_create_with_aap_bak', + 'wp_create_with_plesk_or_cpanel_bak', + 'wp_clone', + 'wp_integrity_check', + 'wp_reinstall_files', + 'wp_plugin_list', + 'wp_install_plugin', + 'wp_installed_plugins', + 'wp_update_plugin', + 'wp_set_plugin_auto_update', + 'wp_set_plugin_status', + 'wp_uninstall_plugin', + 'wp_theme_list', + 'wp_install_theme', + 'wp_installed_themes', + 'wp_update_theme', + 'wp_set_theme_auto_update', + 'wp_switch_theme', + 'wp_uninstall_theme', + ) + return publicObject(siteObject, defs, None, pdata) + + +@app.route(route_v2 + '/ftp', methods=method_all) +def ftp_v2(pdata=None): + # FTP管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + FtpPort() + import system_v2 + data = system_v2.system().GetConcifInfo() + data['isSetup'] = True + data['js_random'] = get_js_random() + if os.path.exists(public.GetConfigValue('setup_path') + + '/pure-ftpd') == False: + data['isSetup'] = False + data['lan'] = public.GetLan('ftp') + return render_template('ftp.html', data=data) + import ftp_v2 + ftpObject = ftp_v2.ftp() + defs = ('AddUser', 'DeleteUser', 'SetUserPassword', 'SetStatus', 'setPort', + 'set_user_home', 'get_login_logs', 'get_action_logs', + 'set_ftp_logs') + return publicObject(ftpObject, defs, None, pdata) + + +@app.route(route_v2 + '/database', methods=method_all) +def database_v2(pdata=None): + # 数据库管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import ajax_v2 + from panelPlugin import panelPlugin + session['phpmyadminDir'] = False + if panelPlugin().get_phpmyadmin_stat(): + pmd = get_phpmyadmin_dir() + if pmd: + session['phpmyadminDir'] = 'http://' + public.GetHost( + ) + ':' + pmd[1] + '/' + pmd[0] + ajax_v2.ajax().set_phpmyadmin_session() + import system_v2 + data = system_v2.system().GetConcifInfo() + data['isSetup'] = os.path.exists( + public.GetConfigValue('setup_path') + '/mysql/bin') + data['mysql_root'] = public.M('config').where( + 'id=?', (1,)).getField('mysql_root') + data['lan'] = public.GetLan('database') + data['js_random'] = get_js_random() + return render_template('database.html', data=data) + import database_v2 + databaseObject = database_v2.database() + defs = ( + 'GetdataInfo', + 'check_del_data', + 'get_database_size', + 'GetInfo', + 'ReTable', + 'OpTable', + 'AlTable', + 'GetSlowLogs', + 'GetRunStatus', + 'SetDbConf', + 'GetDbStatus', + 'BinLog', + 'GetErrorLog', + 'GetMySQLInfo', + 'SetDataDir', + 'SetMySQLPort', + 'AddCloudDatabase', + 'AddDatabase', + 'DeleteDatabase', + 'SetupPassword', + 'ResDatabasePassword', + 'ToBackup', + 'DelBackup', + 'AddCloudServer', + 'GetCloudServer', + 'RemoveCloudServer', + 'ModifyCloudServer', + 'InputSql', + 'SyncToDatabases', + 'SyncGetDatabases', + 'GetDatabaseAccess', + 'SetDatabaseAccess', + 'get_mysql_user', + 'check_mysql_ssl_status', + 'write_ssl_to_mysql', + 'GetdataInfo', + 'GetBackup', + ) + return publicObject(databaseObject, defs, None, pdata) + + +@app.route(route_v2 + '/acme', methods=method_all) +def acme_v2(pdata=None): + # Let's 证书管理 + comReturn = comm.local() + if comReturn: return comReturn + import acme_v3 + acme_v2_object = acme_v3.acme_v2() + defs = ('get_orders', 'remove_order', 'get_order_find', 'revoke_order', + 'create_order', 'get_account_info', 'set_account_info', + 'update_zip', 'get_cert_init_api', 'get_auths', 'auth_domain', + 'check_auth_status', 'download_cert', 'apply_cert', 'renew_cert', + 'apply_cert_api', 'apply_dns_auth') + return publicObject(acme_v2_object, defs, None, pdata) + + +@app.route(route_v2 + '/api', methods=method_all) +def api_v2(pdata=None): + # APP使用的API接口管理 + comReturn = comm.local() + if comReturn: return comReturn + import panel_api_v2 + api_object = panel_api_v2.panelApi() + defs = ('get_token', 'check_bind', 'get_bind_status', 'get_apps', + 'add_bind_app', 'remove_bind_app', 'set_token', 'get_tmp_token', + 'get_app_bind_status', 'login_for_app') + return publicObject(api_object, defs, None, pdata) + + +@app.route(route_v2 + '/control', methods=method_all) +def control_v2(pdata=None): + # 监控页面 + comReturn = comm.local() + if comReturn: return comReturn + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('control') + data['js_random'] = get_js_random() + return render_template('control.html', data=data) + + +@app.route(route_v2 + '/logs', methods=method_all) +def logs_v2(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + data = {} + data['lan'] = public.GetLan('soft') + data['show_workorder'] = not os.path.exists('data/not_workorder.pl') + return render_template('logs.html', data=data) + + +@app.route(route_v2 + '/firewall', methods=method_all) +def firewall_v2(pdata=None): + # 安全页面 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + return render_template('firewall.html', data=data) + import firewalls_v2 + firewallObject = firewalls_v2.firewalls() + defs = ('GetList', 'AddDropAddress', 'DelDropAddress', 'FirewallReload', + 'SetFirewallStatus', 'AddAcceptPort', 'DelAcceptPort', + 'SetSshStatus', 'SetPing', 'SetSshPort', 'GetSshInfo', + 'SetFirewallStatus') + return publicObject(firewallObject, defs, None, pdata) + + +@app.route(route_v2 + '/ssh_security', methods=method_all) +def ssh_security_v2(pdata=None): + # SSH安全 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata and not request.args.get( + 'action', '') in ['download_key']: + data = {} + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + return render_template('firewall.html', data=data) + import ssh_security_v2 + firewallObject = ssh_security_v2.ssh_security() + is_csrf = True + if request.args.get('action', '') in ['download_key']: is_csrf = False + defs = ('san_ssh_security', 'set_password', 'set_sshkey', 'stop_key', + 'get_config', 'download_key', 'stop_password', 'get_key', + 'return_ip', 'add_return_ip', 'del_return_ip', 'start_jian', + 'stop_jian', 'get_jian', 'get_logs', 'set_root', 'stop_root', + 'start_auth_method', 'stop_auth_method', 'get_auth_method', + 'check_so_file', 'get_so_file', 'get_pin', 'set_login_send', + 'get_login_send', 'get_msg_push_list', 'clear_login_send') + return publicObject(firewallObject, defs, None, pdata, is_csrf) + + +@app.route(route_v2 + '/monitor', methods=method_all) +def panel_monitor_v2(pdata=None): + # 云控统计信息 + comReturn = comm.local() + if comReturn: return comReturn + import monitor_v2 + dataObject = monitor_v2.Monitor() + defs = ('get_spider', 'get_exception', 'get_request_count_qps', + 'load_and_up_flow', 'get_request_count_by_hour') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/san', methods=method_all) +def san_baseline_v2(pdata=None): + # 云控安全扫描 + comReturn = comm.local() + if comReturn: return comReturn + import san_baseline_v2 + dataObject = san_baseline_v2.san_baseline() + defs = ('start', 'get_api_log', 'get_resut', 'get_ssh_errorlogin', + 'repair', 'repair_all') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/password', methods=method_all) +def panel_password_v2(pdata=None): + # 云控密码管理 + comReturn = comm.local() + if comReturn: return comReturn + import password_v2 + dataObject = password_v2.password() + defs = ('set_root_password', 'get_mysql_root', 'set_mysql_password', + 'set_panel_password', 'SetPassword', 'SetSshKey', 'StopKey', + 'GetConfig', 'StopPassword', 'GetKey', 'get_databses', + 'rem_mysql_pass', 'set_mysql_access', "get_panel_username") + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/warning', methods=method_all) +def panel_warning_v2(pdata=None): + # 首页安全警告 + comReturn = comm.local() + if comReturn: return comReturn + if public.get_csrf_html_token_key() in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + get = get_input() + ikey = 'warning_list' + import panel_warning_v2 + dataObject = panel_warning_v2.panelWarning() + if get.action == 'get_list': + result = cache.get(ikey) + if not result or 'force' in get: + result = json.loads('{"ignore":[],"risk":[],"security":[]}') + try: + defs = ("get_list",) + result = publicObject(dataObject, defs, None, pdata) + cache.set(ikey, result, 3600) + return result + except: + pass + return result + + defs = ('get_list', 'set_ignore', 'check_find', 'check_cve', + 'set_vuln_ignore', 'get_scan_bar', 'get_tmp_result', + 'kill_get_list') + + if get.action in ['set_ignore', 'check_find', 'set_vuln_ignore']: + cache.delete(ikey) + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/bak', methods=method_all) +def backup_bak_v2(pdata=None): + # 云控备份服务 + comReturn = comm.local() + if comReturn: return comReturn + import backup_bak_v2 + dataObject = backup_bak_v2.backup_bak() + defs = ('get_sites', 'get_databases', 'backup_database', 'backup_site', + 'backup_path', 'get_database_progress', 'get_site_progress', + 'down', 'get_down_progress', 'download_path', 'backup_site_all', + 'get_all_site_progress', 'backup_date_all', + 'get_all_date_progress') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/abnormal', methods=method_all) +def abnormal_v2(pdata=None): + # 云控系统统计 + comReturn = comm.local() + if comReturn: return comReturn + import abnormal_v2 + dataObject = abnormal_v2.abnormal() + defs = ('mysql_server', 'mysql_cpu', 'mysql_count', 'php_server', + 'php_conn_max', 'php_cpu', 'CPU', 'Memory', 'disk', + 'not_root_user', 'start') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/project/nodejs/', methods=method_all) +@app.route(route_v2 + '/project/nodejs//html', methods=method_all) +@app.route(route_v2 + '/project/docker/', methods=method_all) +@app.route(route_v2 + '/project/docker//html', methods=method_all) +@app.route(route_v2 + '/project/quota/', methods=method_all) +@app.route(route_v2 + '/project/quota//html', methods=method_all) +@app.route(route_v2 + '/project/proxy/', methods=method_all) +@app.route(route_v2 + '/project/proxy//html', methods=method_all) +def project_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + comReturn = comm.local() + if comReturn: return comReturn + from panelProjectControllerV2 import ProjectController + project_obj = ProjectController() + defs = ('model',) + get = get_input() + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + if request.path.endswith('/html'): + return project_obj.model(get) + return publicObject(project_obj, defs, None, get) + + +@app.route(route_v2 + '/msg//', methods=method_all) +def msgcontroller_v2(mod_name, def_name): + comReturn = comm.local() + if comReturn: return comReturn + from MsgControllerV2 import MsgController + project_obj = MsgController() + defs = ('model',) + get = get_input() + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + return publicObject(project_obj, defs, None, get) + + +# @app.route(route_v2 + '/docker', methods=method_all) +# def docker_v2(pdata=None): +# comReturn = comm.local() +# if comReturn: return comReturn +# if request.method == method_get[0]: +# import system_v2 +# data = system_v2.system().GetConcifInfo() +# data['js_random'] = get_js_random() +# data['lan'] = public.GetLan('files') +# return render_template('docker.html', data=data) + +# @app.route(route_v2 + '/docker', methods=method_all) +@app.route(route_v2 + '/btdocker/app/', methods=method_all) +@app.route(route_v2 + '/btdocker/backup/', methods=method_all) +@app.route(route_v2 + '/btdocker/container/', methods=method_all) +@app.route(route_v2 + '/btdocker/compose/', methods=method_all) +@app.route(route_v2 + '/btdocker/dkgroup/', methods=method_all) +@app.route(route_v2 + '/btdocker/image/', methods=method_all) +@app.route(route_v2 + '/btdocker/network/', methods=method_all) +@app.route(route_v2 + '/btdocker/proxy/', methods=method_all) +@app.route(route_v2 + '/btdocker/project/', methods=method_all) +@app.route(route_v2 + '/btdocker/registry/', methods=method_all) +@app.route(route_v2 + '/btdocker/setup/', methods=method_all) +@app.route(route_v2 + '/btdocker/site/', methods=method_all) +@app.route(route_v2 + '/btdocker/status/', methods=method_all) +@app.route(route_v2 + '/btdocker/volume/', methods=method_all) +def docker_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + get = get_input() + get.action = 'model' + get.model_index = 'btDocker' + get.mod_name = path_split[3] + get.def_name = def_name + + comReturn = comm.local() + if comReturn: return comReturn + # p_path = public.get_plugin_path() + '/' + path_split[2] + # if os.path.exists(p_path): + # return panel_other(get.model_index, get.mod_name, def_name) + from panelDockerControllerV2 import DockerController + controller_obj = DockerController() + defs = ('model',) + return publicObject(controller_obj, defs, None, get) + + +@app.route(route_v2 + '/dbmodel//', methods=method_all) +def dbmodel_v2(mod_name, def_name): + comReturn = comm.local() + if comReturn: return comReturn + from panelDatabaseControllerV2 import DatabaseController + database_obj = DatabaseController() + defs = ('model',) + get = get_input() + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + + return publicObject(database_obj, defs, None, get) + + +@app.route(route_v2 + '/files', methods=method_all) +def files_v2(pdata=None): + # 文件管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not request.args.get( + 'path') and not pdata: + import system_v2 + data = system_v2.system().GetConcifInfo() + data['recycle_bin'] = os.path.exists('data/recycle_bin.pl') + data['lan'] = public.GetLan('files') + data['js_random'] = get_js_random() + return render_template('files.html', data=data) + import files_v2 + filesObject = files_v2.files() + defs = ('files_search', 'files_replace', 'get_replace_logs', + 'get_images_resize', 'add_files_rsync', 'get_file_attribute', + 'get_file_hash', 'CreateLink', 'get_progress', 'restore_website', + 'fix_permissions', 'get_all_back', 'restore_path_permissions', + 'del_path_premissions', 'get_path_premissions', + 'back_path_permissions', 'upload_file_exists', 'CheckExistsFiles', + 'GetExecLog', 'GetSearch', 'ExecShell', 'GetExecShellMsg', + 'exec_git', 'exec_composer', 'create_download_url', 'UploadFile', + 'GetDir', 'CreateFile', 'CreateDir', 'DeleteDir', 'DeleteFile', + 'get_download_url_list', 'remove_download_url', + 'modify_download_url', 'CopyFile', 'CopyDir', 'MvFile', + 'GetFileBody', 'SaveFileBody', 'Zip', 'UnZip', + 'get_download_url_find', 'set_file_ps', 'SearchFiles', 'upload', + 'read_history', 're_history', 'auto_save_temp', + 'get_auto_save_body', 'get_videos', 'GetFileAccess', + 'SetFileAccess', 'GetDirSize', 'SetBatchData', 'BatchPaste', + 'install_rar', 'get_path_size', 'DownloadFile', 'GetTaskSpeed', + 'CloseLogs', 'InstallSoft', 'UninstallSoft', 'SaveTmpFile', + 'get_composer_version', 'exec_composer', 'update_composer', + 'GetTmpFile', 'del_files_store', 'add_files_store', + 'get_files_store', 'del_files_store_types', + 'add_files_store_types', 'exec_git', 'RemoveTask', 'ActionTask', + 'Re_Recycle_bin', 'Get_Recycle_bin', 'Del_Recycle_bin', + 'Close_Recycle_bin', 'Recycle_bin', 'file_webshell_check', + 'dir_webshell_check', 'files_search', 'files_replace', + 'get_replace_logs') + return publicObject(filesObject, defs, None, pdata) + + +@app.route(route_v2 + '/crontab', methods=method_all) +def crontab_v2(pdata=None): + # 计划任务 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('crontab') + data['js_random'] = get_js_random() + return render_template('crontab.html', data=data) + import crontab_v2 + crontabObject = crontab_v2.crontab() + defs = ('GetCrontab', 'AddCrontab', 'GetDataList', 'GetLogs', 'DelLogs', + 'DelCrontab', 'StartTask', 'set_cron_status', 'get_crond_find', + 'modify_crond', 'get_backup_list') + return publicObject(crontabObject, defs, None, pdata) + + +@app.route(route_v2 + '/soft', methods=method_all) +def soft_v2(pdata=None): + # 软件商店页面 + comReturn = comm.local() + if comReturn: return comReturn + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('soft') + data['js_random'] = get_js_random() + return render_template('soft.html', data=data) + + +@app.route(route_v2 + '/config', methods=method_all) +def config_v2(pdata=None): + # 面板设置页面 + comReturn = comm.local() + if comReturn: return comReturn + + if request.method == method_get[0] and not pdata: + import system_v2, wxapp_v2, config_v2 + c_obj = config_v2.config() + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('config') + try: + data['wx'] = wxapp_v2.wxapp().get_user_info(None)['msg'] + except: + data['wx'] = 'INIT_WX_NOT_BIND' + data['api'] = '' + data['ipv6'] = '' + sess_out_path = 'data/session_timeout.pl' + if not os.path.exists(sess_out_path): + public.writeFile(sess_out_path, '86400') + s_time_tmp = public.readFile(sess_out_path) + if not s_time_tmp: s_time_tmp = '0' + data['session_timeout'] = int(s_time_tmp) + if c_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' + if c_obj.get_token(None)['open']: data['api'] = 'checked' + data['basic_auth'] = c_obj.get_basic_auth_stat(None) + data['status_code'] = c_obj.get_not_auth_status() + data['basic_auth']['value'] = public.getMsg('CLOSED') + if data['basic_auth']['open']: + data['basic_auth']['value'] = public.getMsg('OPENED') + data['debug'] = '' + data['js_random'] = get_js_random() + if app.config['DEBUG']: data['debug'] = 'checked' + data['is_local'] = '' + if public.is_local(): data['is_local'] = 'checked' + data['public_key'] = public.get_rsa_public_key().replace("\n", "") + return render_template('config.html', data=data) + import config_v2 + defs = ( + 'send_by_telegram', + 'set_empty', + 'set_backup_notification', + 'get_panel_ssl_status', + 'set_file_deny', + 'del_file_deny', + 'get_file_deny', + 'set_improvement', + 'get_httpd_access_log_format_parameter', + 'set_httpd_format_log_to_website', + 'get_httpd_access_log_format', + 'del_httpd_access_log_format', + 'add_httpd_access_log_format', + 'get_nginx_access_log_format_parameter', + 'set_format_log_to_website', + 'get_nginx_access_log_format', + 'del_nginx_access_log_format', + 'set_click_logs', + 'get_node_config', + 'add_nginx_access_log_format', + 'get_ols_private_cache_status', + 'get_ols_value', + 'set_ols_value', + 'set_node_config', + 'get_ols_private_cache', + 'get_ols_static_cache', + 'set_ols_static_cache', + 'switch_ols_private_cache', + 'set_ols_private_cache', + 'set_coll_open', + 'get_qrcode_data', + 'check_two_step', + 'set_two_step_auth', + 'create_user', + 'remove_user', + 'modify_user', + 'get_key', + 'get_php_session_path', + 'set_php_session_path', + 'get_cert_source', + 'get_users', + 'set_request_iptype', + 'set_local', + 'set_debug', + 'get_panel_error_logs', + 'clean_panel_error_logs', + 'get_menu_list', + 'set_hide_menu_list', + 'get_basic_auth_stat', + 'set_basic_auth', + 'get_cli_php_version', + 'get_tmp_token', + 'get_temp_login', + 'set_temp_login', + 'remove_temp_login', + 'clear_temp_login', + 'get_temp_login_logs', + 'set_cli_php_version', + 'DelOldSession', + 'GetSessionCount', + 'SetSessionConf', + 'set_not_auth_status', + 'GetSessionConf', + 'get_ipv6_listen', + 'set_ipv6_status', + 'GetApacheValue', + 'SetApacheValue', + 'install_msg_module', + 'GetNginxValue', + 'SetNginxValue', + 'get_token', + 'set_token', + 'set_admin_path', + 'is_pro', + 'set_msg_config', + 'get_php_config', + 'get_config', + 'SavePanelSSL', + 'GetPanelSSL', + 'GetPHPConf', + 'SetPHPConf', + 'uninstall_msg_module', + 'GetPanelList', + 'AddPanelInfo', + 'SetPanelInfo', + 'DelPanelInfo', + 'ClickPanelInfo', + 'SetPanelSSL', + 'get_msg_configs', + 'SetTemplates', + 'Set502', + 'setPassword', + 'setUsername', + 'setPanel', + 'setPathInfo', + 'setPHPMaxSize', + 'get_msg_fun', + 'getFpmConfig', + 'setFpmConfig', + 'setPHPMaxTime', + 'syncDate', + 'setPHPDisable', + 'SetControl', + 'get_settings2', + 'del_tg_info', + 'set_tg_bot', + 'ClosePanel', + 'AutoUpdatePanel', + 'SetPanelLock', + 'return_mail_list', + 'del_mail_list', + 'add_mail_address', + 'user_mail_send', + 'get_user_mail', + 'set_dingding', + 'get_dingding', + 'get_settings', + 'user_stmp_mail_send', + 'user_dingding_send', + 'get_login_send', + 'set_login_send', + 'clear_login_send', + 'get_login_log', + 'login_ipwhite', + 'set_ssl_verify', + 'get_ssl_verify', + 'get_password_config', + 'set_password_expire', + 'set_password_safe', + 'get_module_template', + # 新增nps评分 + 'write_nps_new', + 'get_nps_new', + "check_nps", + 'get_translations', + ) + return publicObject(config_v2.config(), defs, None, pdata) + + +@app.route(route_v2 + '/ajax', methods=method_all) +def ajax_v2(pdata=None): + # 面板系统服务状态接口 + comReturn = comm.local() + if comReturn: return comReturn + import ajax_v2 + ajaxObject = ajax_v2.ajax() + defs = ('get_lines', 'php_info', 'change_phpmyadmin_ssl_port', + 'set_phpmyadmin_ssl', 'get_phpmyadmin_ssl', 'get_pd', + 'check_user_auth', 'to_not_beta', 'get_beta_logs', 'apple_beta', + 'GetApacheStatus', 'GetCloudHtml', 'get_pay_type', + 'get_load_average', 'GetOpeLogs', 'GetFpmLogs', 'GetFpmSlowLogs', + 'SetMemcachedCache', 'GetMemcachedStatus', 'GetRedisStatus', + 'GetWarning', 'SetWarning', 'CheckLogin', 'GetSpeed', 'GetAd', + 'phpSort', 'ToPunycode', 'GetBetaStatus', 'SetBeta', + 'setPHPMyAdmin', 'delClose', 'KillProcess', 'GetPHPInfo', + 'GetQiniuFileList', 'get_process_tops', 'get_process_cpu_high', + 'UninstallLib', 'InstallLib', 'SetQiniuAS', 'GetQiniuAS', + 'GetLibList', 'GetProcessList', 'GetNetWorkList', 'GetNginxStatus', + 'GetPHPStatus', 'GetTaskCount', 'GetSoftList', 'GetNetWorkIo', + 'GetDiskIo', 'GetCpuIo', 'CheckInstalled', 'UpdatePanel', + 'GetInstalled', 'GetPHPConfig', 'SetPHPConfig', 'log_analysis', + 'speed_log', 'get_result', 'get_detailed', 'ignore_version') + + return publicObject(ajaxObject, defs, None, pdata) + + +@app.route(route_v2 + '/system', methods=method_all) +def system_v2(pdata=None): + # 面板系统状态接口 + comReturn = comm.local() + if comReturn: return comReturn + import system_v2 + sysObject = system_v2.system() + defs = ('get_io_info', 'UpdatePro', 'GetAllInfo', 'GetNetWorkApi', + 'GetLoadAverage', 'ClearSystem', 'GetNetWorkOld', 'GetNetWork', + 'GetDiskInfo', 'GetCpuInfo', 'GetBootTime', 'GetSystemVersion', + 'GetMemInfo', 'GetSystemTotal', 'GetConcifInfo', 'ServiceAdmin', + 'ReWeb', 'RestartServer', 'ReMemory', 'RepPanel') + return publicObject(sysObject, defs, None, pdata) + + +@app.route(route_v2 + '/deployment', methods=method_all) +def deployment_v2(pdata=None): + # 一键部署接口 + comReturn = comm.local() + if comReturn: return comReturn + import plugin_deployment_v2 + sysObject = plugin_deployment_v2.plugin_deployment() + defs = ('GetList', 'AddPackage', 'DelPackage', 'SetupPackage', 'GetSpeed', + 'GetPackageOther') + return publicObject(sysObject, defs, None, pdata) + + +@app.route(route_v2 + '/data', methods=method_all) +@app.route(route_v2 + '/panel_data', methods=method_all) +def panel_data_v2(pdata=None): + # 从数据库获取数据接口 + comReturn = comm.local() + if comReturn: return comReturn + import data_v2 + dataObject = data_v2.data() + defs = ('setPs', 'getData', 'getFind', 'getKey') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/ssl', methods=method_all) +def ssl_v2(pdata=None): + # 商业SSL证书申请接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_ssl_v2 + toObject = panel_ssl_v2.panelSSL() + defs = ( + 'check_url_txt', + 'RemoveCert', + 'renew_lets_ssl', + 'SetCertToSite', + 'GetCertList', + 'SaveCert', + 'GetCert', + 'GetCertName', + 'again_verify', + 'DelToken', + 'GetToken', + 'GetUserInfo', + 'GetOrderList', + 'GetDVSSL', + 'Completed', + 'SyncOrder', + 'download_cert', + 'set_cert', + 'cancel_cert_order', + 'get_order_list', + 'get_order_find', + 'apply_order_pay', + 'get_pay_status', + 'apply_order', + 'get_verify_info', + 'get_verify_result', + 'get_product_list', + 'set_verify_info', + 'GetSSLInfo', + 'downloadCRT', + 'GetSSLProduct', + 'Renew_SSL', + 'Get_Renew_SSL', + # 新增 购买证书对接接口 + 'get_product_list_v2', + 'apply_cert_order_pay', + 'get_cert_admin', + 'apply_order_ca', + 'apply_cert_install_pay', + + # 'pay_test' + ) + get = get_input() + + if get.action == 'download_cert': + from io import BytesIO + import base64 + result = toObject.download_cert(get) + # public.print_log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@1111111111111111 result: {}".format(result)) + # {'success': False, 'res': '[code: 0] no data [file: /www/wwwroot/192.168.1.139/app/Api/Cert/controllers/Cert.php] [line: 955]', 'nonce': 1706498844} + + fp = BytesIO(base64.b64decode(result['res']['data'])) + return send_file(fp, + download_name=result['res']['filename'], + as_attachment=True, + mimetype='application/zip') + result = publicObject(toObject, defs, get.action, get) + return result + + +@app.route(route_v2 + '/task', methods=method_all) +def task_v2(pdata=None): + # 后台任务接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_task_v2 + toObject = panel_task_v2.bt_task() + defs = ('get_task_lists', 'remove_task', 'get_task_find', + "get_task_log_by_id") + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route(route_v2 + '/plugin', methods=method_all) +def plugin_v2(pdata=None): + # 插件系统接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_plugin_v2 + pluginObject = panel_plugin_v2.panelPlugin() + defs = ('get_usually_plugin', 'check_install_limit', 'set_score', + 'get_score', 'update_zip', 'input_zip', 'export_zip', 'add_index', + 'remove_index', 'sort_index', 'install_plugin', 'uninstall_plugin', + 'get_soft_find', 'get_index_list', 'get_soft_list', + 'get_cloud_list', 'check_deps', 'flush_cache', 'GetCloudWarning', + 'install', 'unInstall', 'getPluginList', 'getPluginInfo', + 'get_make_args', 'add_make_args', 'getPluginStatus', + 'setPluginStatus', 'a', 'getCloudPlugin', 'getConfigHtml', + 'savePluginSort', 'del_make_args', 'set_make_args') + return publicObject(pluginObject, defs, None, pdata) + + +@app.route(route_v2 + '/wxapp', methods=method_all) +@app.route(route_v2 + '/panel_wxapp', methods=method_all) +def panel_wxapp_v2(pdata=None): + # 微信小程序绑定接口 + comReturn = comm.local() + if comReturn: return comReturn + import wxapp_v2 + toObject = wxapp_v2.wxapp() + defs = ('blind', 'get_safe_log', 'blind_result', 'get_user_info', + 'blind_del', 'blind_qrcode') + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route(route_v2 + '/auth', methods=method_all) +def auth_v2(pdata=None): + # 面板认证接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_auth_v2 + toObject = panel_auth_v2.panelAuth() + defs = ('free_trial', 'renew_product_auth', 'auth_activate', + 'get_product_auth', 'get_stripe_session_id', + 'get_re_order_status_plugin', 'create_plugin_other_order', + 'get_order_stat', 'get_voucher_plugin', + 'create_order_voucher_plugin', 'get_product_discount_by', + 'get_re_order_status', 'create_order_voucher', 'create_order', + 'get_order_status', 'get_voucher', 'flush_pay_status', + 'create_serverid', 'check_serverid', 'get_plugin_list', + 'check_plugin', 'get_buy_code', 'check_pay_status', + 'get_renew_code', 'check_renew_code', 'get_business_plugin', + 'get_ad_list', 'check_plugin_end', 'get_plugin_price', + 'get_plugin_remarks', 'get_paypal_session_id', + 'check_paypal_status', 'get_wx_order_status', 'get_apply_copon', + 'get_coupon_list', 'ignore_coupon_time', 'set_user_adviser', 'rest_unbind_count', + 'unbind_authorization', 'get_all_voucher_plugin', 'get_pay_unbind_count', + 'get_coupons', 'get_credits', 'create_with_credit_by_panel', 'get_last_paid_time', + 'get_all_coupons') + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route(route_v2 + '/download', methods=method_get) +def download_v2(): + # 文件下载接口 + comReturn = comm.local() + if comReturn: return comReturn + filename = request.args.get('filename') + if filename.find('|') != -1: + filename = filename.split('|')[0] # 改为获取本地备份 + if not filename: + return public.ReturnJson(False, "INIT_ARGS_ERR"), json_header + # if filename in ['alioss','qiniu','upyun','txcos','ftp','msonedrive','gcloud_storage', 'gdrive', 'aws_s3']: return panel_cloud() + if not os.path.exists(filename): + return public.ReturnJson(False, "FILE_NOT_EXISTS"), json_header + + if request.args.get('play') == 'true': + import panelVideo + start, end = panelVideo.get_range(request) + g.return_message = True + return panelVideo.partial_response(filename, start, end) + else: + mimetype = "application/octet-stream" + extName = filename.split('.')[-1] + if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None + public.WriteLog("TYPE_FILE", 'FILE_DOWNLOAD', + (filename, public.GetClientIp())) + g.return_message = True + return send_file(filename, + mimetype=mimetype, + as_attachment=True, + etag=True, + conditional=True, + download_name=os.path.basename(filename), + max_age=0) + + +@app.route(route_v2 + '/cloud', methods=method_all) +def panel_cloud_v2(is_csrf=True): + # 从对像存储下载备份文件接口 + comReturn = comm.local() + if comReturn: return comReturn + if is_csrf: + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + get = get_input() + _filename = get.filename + plugin_name = "" + if _filename.find('|') != -1: + plugin_name = get.filename.split('|')[1] + else: + plugin_name = get.filename + + if not os.path.exists('plugin/' + plugin_name + '/' + plugin_name + + '_main.py'): + return public.returnJson( + False, 'The specified plugin does not exist!'), json_header + public.package_path_append('plugin/' + plugin_name) + plugin_main = __import__(plugin_name + '_main') + public.mod_reload(plugin_main) + tmp = eval("plugin_main.%s_main()" % plugin_name) + if not hasattr(tmp, 'download_file'): + return public.returnJson( + False, + 'Specified plugin has no file download function!'), json_header + download_url = tmp.download_file(get.name) + if plugin_name == 'ftp': + if download_url.find("ftp") != 0: + download_url = "ftp://" + download_url + else: + if download_url.find('http') != 0: + download_url = 'http://' + download_url + + if "toserver" in get and get.toserver == "true": + download_dir = "/tmp/" + if "download_dir" in get: + download_dir = get.download_dir + local_file = os.path.join(download_dir, get.name) + + input_from_local = False + if "input_from_local" in get: + input_from_local = True if get.input_from_local == "true" else False + + if input_from_local: + if os.path.isfile(local_file): + return { + "status": True, + "msg": + "The file already exists and will be restored locally.", + "task_id": -1, + "local_file": local_file + } + from panel_task_v2 import bt_task + task_obj = bt_task() + task_id = task_obj.create_task('Download file', 1, download_url, + local_file) + return { + "status": True, + "msg": "The download task was created successfully", + "local_file": local_file, + "task_id": task_id + } + + return redirect(download_url) + + +@app.route(route_v2 + '/btwaf_error', methods=method_get) +def btwaf_error_v2(): + # 图标 + comReturn = comm.local() + if comReturn: return comReturn + get = get_input() + p_path = os.path.join('/www/server/panel/plugin/', "btwaf") + if not os.path.exists(p_path): + if get.name == 'btwaf' and get.fun == 'index': + return render_template('error3.html', data={}) + return render_template('error3.html', data={}) + + +@app.route(route_v2 + '/favicon.ico', methods=method_get) +def send_favicon_v2(): + # 图标 + comReturn = comm.local() + if comReturn: return abort(404) + s_file = '/www/server/panel/BTPanel/static/favicon.ico' + if not os.path.exists(s_file): return abort(404) + return send_file(s_file, conditional=True, etag=True) + + +@app.route(route_v2 + '/rspamd', defaults={'path': ''}, methods=method_all) +@app.route(route_v2 + '/rspamd/', methods=method_all) +def proxy_rspamd_requests_v2(path): + comReturn = comm.local() + if comReturn: return comReturn + param = str(request.url).split('?') + param = "" if len(param) < 2 else param[-1] + import requests + headers = {} + for h in request.headers.keys(): + headers[h] = request.headers[h] + if request.method == "GET": + if re.search(r"\.(js|css)$", path): + return send_file('/usr/share/rspamd/www/rspamd/' + path, + conditional=True, + etag=True) + if path == "/": + return send_file('/usr/share/rspamd/www/rspamd/', + conditional=True, + etag=True) + url = "http://127.0.0.1:11334/rspamd/" + path + "?" + param + for i in [ + 'stat', 'auth', 'neighbours', 'list_extractors', + 'list_transforms', 'graph', 'maps', 'actions', 'symbols', + 'history', 'errors', 'check_selector', 'saveactions', + 'savesymbols', 'getmap' + ]: + if i in path: + url = "http://127.0.0.1:11334/" + path + "?" + param + req = requests.get(url, headers=headers, stream=True) + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) + else: + url = "http://127.0.0.1:11334/" + path + for i in request.form.keys(): + data = '{}='.format(i) + # public.writeFile('/tmp/2',data+"\n","a+") + req = requests.post(url, data=data, headers=headers, stream=True) + + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) + + +@app.route(route_v2 + '/tips', methods=method_get) +def tips_v2(): + # 提示页面 + comReturn = comm.local() + if comReturn: return abort(404) + get = get_input() + if len(get.get_items().keys()) > 1: return abort(404) + return render_template('tips.html') + + +# ======================普通路由区end============================# + +# ======================严格排查区域start============================# + + +@app.route(route_v2 + '/login', methods=method_all) +@app.route(route_v2 + route_path, methods=method_all) +@app.route(route_v2 + route_path + '/', methods=method_all) +def login_v2(): + # 面板登录接口 + if os.path.exists('install.pl'): return redirect('/install') + global admin_check_auth, admin_path, route_path + is_auth_path = False + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + is_auth_path = True + # 登录输入验证 + if request.method == method_post[0]: + if is_auth_path: + g.auth_error = True + return public.error_not_login(None) + v_list = ['username', 'password', 'code', 'vcode', 'cdn_url'] + for v in v_list: + if v in ['username', 'password']: continue + pv = request.form.get(v, '').strip() + if v == 'cdn_url': + if len(pv) > 32: + return public.return_msg_gettext( + False, 'Wrong parameter length!'), json_header + if not re.match(r"^[\w\.-]+$", pv): + public.return_msg_gettext( + False, 'Wrong parameter format!'), json_header + continue + + if not pv: continue + p_len = 32 + if v == 'code': p_len = 4 + if v == 'vcode': p_len = 6 + if len(pv) != p_len: + if v == 'code': + return public.returnJson( + False, 'Verification code length error!'), json_header + return public.returnJson( + False, 'Wrong parameter length!'), json_header + if not re.match(r"^\w+$", pv): + return public.returnJson( + False, 'Wrong parameter format!'), json_header + + for n in request.form.keys(): + if not n in v_list: + return public.returnJson( + False, + 'There can be no extra parameters in the login parameters' + ), json_header + + get = get_input() + import user_login_v2 + if hasattr(get, 'tmp_token'): + result = user_login_v2.userlogin().request_tmp(get) + return is_login(result) + # 过滤爬虫 + if public.is_spider(): return abort(404) + if hasattr(get, 'dologin'): + login_path = '/login' + if not 'login' in session: return redirect(login_path) + if os.path.exists(admin_path_file): login_path = route_path + if session['login'] != False: + session['login'] = False + cache.set('dologin', True) + public.write_log_gettext( + 'Logout', 'Client: {}, has manually exited the panel', + (public.GetClientIp() + ":" + + str(request.environ.get('REMOTE_PORT')),)) + if 'tmp_login_expire' in session: + s_file = 'data/session/{}'.format(session['tmp_login_id']) + if os.path.exists(s_file): + os.remove(s_file) + token_key = public.get_csrf_html_token_key() + if token_key in session: + del (session[token_key]) + session.clear() + sess_file = 'data/sess_files/' + public.get_sess_key() + if os.path.exists(sess_file): + try: + os.remove(sess_file) + except: + pass + sess_tmp_file = public.get_full_session_file() + if os.path.exists(sess_tmp_file): os.remove(sess_tmp_file) + g.dologin = True + return redirect(public.get_admin_path()) + + if is_auth_path: + if route_path != request.path and route_path + '/' != request.path: + referer = request.headers.get('Referer', 'err') + referer_tmp = referer.split('/') + referer_path = referer_tmp[-1] + if referer_path == '': + referer_path = referer_tmp[-2] + if route_path != '/' + referer_path: + g.auth_error = True + # return render_template('autherr.html') + return public.error_not_login(None) + + session['admin_auth'] = True + comReturn = common.panelSetup().init() + if comReturn: return comReturn + + if request.method == method_post[0]: + result = userlogin.userlogin().request_post(get) + return is_login(result) + + if request.method == method_get[0]: + result = userlogin.userlogin().request_get(get) + if result: + return result + data = {} + data['lan'] = public.GetLan('login') + data['hosts'] = '[]' + hosts_file = 'plugin/static_cdn/hosts.json' + if os.path.exists(hosts_file): + data['hosts'] = public.get_cdn_hosts() + if type(data['hosts']) == dict: + data['hosts'] = '[]' + else: + data['hosts'] = json.dumps(data['hosts']) + data['app_login'] = os.path.exists('data/app_login.pl') + public.cache_set( + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp()), 'check', 360) + + # 生成登录token + last_key = 'last_login_token' + # ----------- + last_time_key = 'last_login_token_time' + s_time = int(time.time()) + if last_key in session and last_time_key in session: + # 10秒内不重复生成token + if s_time - session[last_time_key] > 10: + session[last_key] = public.GetRandomString(32) + session[last_time_key] = s_time + else: + session[last_key] = public.GetRandomString(32) + session[last_time_key] = s_time + + data[last_key] = session[last_key] + data['public_key'] = public.get_rsa_public_key() + return render_template('login.html', data=data) + + +@app.route(route_v2 + '/close', methods=method_get) +def close_v2(): + # 面板已关闭页面 + if not os.path.exists('data/close.pl'): return redirect('/') + data = {} + data['lan'] = public.getLan('close') + return render_template('close.html', data=data) + + +@app.route(route_v2 + '/get_app_bind_status', methods=method_all) +def get_app_bind_status_v2(pdata=None): + # APP绑定状态查询 + if not public.check_app('app_bind'): return abort(404) + get = get_input() + if len(get.get_items().keys()) > 2: return 'There are meaningless parameters!' + v_list = ['bind_token', 'data'] + for n in get.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + import panel_api_v2 + api_object = panel_api_v2.panelApi() + return json.dumps(api_object.get_app_bind_status(get_input())), json_header + + +@app.route(route_v2 + '/check_bind', methods=method_all) +def check_bind_v2(pdata=None): + # APP绑定查询 + if not public.check_app('app_bind'): return abort(404) + get = get_input() + if len(get.get_items().keys()) > 4: return 'There are meaningless parameters!' + v_list = ['bind_token', 'client_brand', 'client_model', 'data'] + for n in get.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + import panel_api_v2 + api_object = panel_api_v2.panelApi() + return json.dumps(api_object.check_bind(get_input())), json_header + + +@app.route(route_v2 + '/code', methods=method_get) +def code_v2(): + if not 'code' in session: return '' + if not session['code']: return '' + # 获取图片验证码 + try: + import vilidate_v2 + except: + public.ExecShell("btpip install Pillow -I") + return "Pillow not install!" + vie = vilidate_v2.vieCode() + codeImage = vie.GetCodeImage(80, 4) + if sys.version_info[0] == 2: + try: + from cStringIO import StringIO + except: + from StringIO import StringIO + out = StringIO() + else: + from io import BytesIO + out = BytesIO() + codeImage[0].save(out, "png") + cache.set("codeStr", public.md5("".join(codeImage[1]).lower()), 180) + cache.set("codeOut", 1, 0.1) + out.seek(0) + return send_file(out, mimetype='image/png', max_age=0) + + +@app.route(route_v2 + '/down/', methods=method_all) +def down_v2(token=None, fname=None): + # 文件分享对外接口 + try: + if public.M('download_token').count() == 0: return abort(404) + fname = request.args.get('fname') + if fname: + if (len(fname) > 256): return abort(404) + if fname: fname = fname.strip('/') + if not token: return abort(404) + if len(token) > 48: return abort(404) + char_list = [ + '\\', '/', ':', '*', '?', '"', '<', '>', '|', ';', '&', '`' + ] + for char in char_list: + if char in token: return abort(404) + if not request.args.get('play') in ['true', None, '']: + return abort(404) + args = get_input() + v_list = ['fname', 'play', 'file_password', 'data'] + for n in args.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + if not re.match(r"^[\w\.]+$", token): return abort(404) + find = public.M('download_token').where('token=?', (token,)).find() + + if not find: return abort(404) + if time.time() > int(find['expire']): return abort(404) + + if not os.path.exists(find['filename']): return abort(404) + if find['password'] and not token in session: + if 'file_password' in args: + if not re.match(r"^\w+$", args.file_password): + return public.ReturnJson(False, + 'Wrong password!'), json_header + if re.match(r"^\d+$", args.file_password): + args.file_password = str(int(args.file_password)) + args.file_password += ".0" + if args.file_password != str(find['password']): + return public.ReturnJson(False, + 'Wrong password!'), json_header + session[token] = 1 + session['down'] = True + else: + pdata = { + "to_path": "", + "src_path": find['filename'], + "password": True, + "filename": find['filename'].split('/')[-1], + "ps": find['ps'], + "total": find['total'], + "token": find['token'], + "expire": public.format_date(times=find['expire']) + } + session['down'] = True + return render_template('down.html', data=pdata) + + if not find['password']: + session['down'] = True + session[token] = 1 + + if session[token] != 1: + return abort(404) + + filename = find['filename'] + if fname: + filename = os.path.join(filename, fname) + if not public.path_safe_check(fname, False): return abort(404) + if os.path.isdir(filename): + return get_dir_down(filename, token, find) + else: + if os.path.isdir(filename): + return get_dir_down(filename, token, find) + + if request.args.get('play') == 'true': + import panel_video_v2 + start, end = panel_video_v2.get_range(request) + return panel_video_v2.partial_response(filename, start, end) + else: + mimetype = "application/octet-stream" + extName = filename.split('.')[-1] + if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None + b_name = os.path.basename(filename) + return send_file(filename, + mimetype=mimetype, + as_attachment=True, + download_name=b_name, + max_age=0) + except: + return abort(404) + + +@app.route(route_v2 + '/database/mongodb/', methods=method_all) +@app.route(route_v2 + '/database/pgsql/', methods=method_all) +@app.route(route_v2 + '/database/redis/', methods=method_all) +@app.route(route_v2 + '/database/sqlite/', methods=method_all) +@app.route(route_v2 + '/database/sqlserver/', methods=method_all) +def databaseModel_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + comReturn = comm.local() + if comReturn: return comReturn + from panelDatabaseControllerV2 import DatabaseController + project_obj = DatabaseController() + defs = ('model',) + get = get_input() + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + return publicObject(project_obj, defs, None, get) + + +# 系统安全模型页面 +# @app.route(route_v2+'/safe//', methods=method_all) +@app.route(route_v2 + '/safe/firewall/', methods=method_all) +@app.route(route_v2 + '/safe/freeip/', methods=method_all) +@app.route(route_v2 + '/safe/ips/', methods=method_all) +@app.route(route_v2 + '/safe/security/', methods=method_all) +@app.route(route_v2 + '/safe/ssh/', methods=method_all) +@app.route(route_v2 + '/safe/syslog/', methods=method_all) +def safeModel_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + comReturn = comm.local() + if comReturn: return comReturn + from panelSafeControllerV2 import SafeController + project_obj = SafeController() + defs = ('model',) + get = get_input() + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + + return publicObject(project_obj, defs, None, get) + + +# 通用模型路由 +@app.route(route_v2 + '/panel/binlog/', methods=method_all) +@app.route(route_v2 + '/panel/bt_check/', methods=method_all) +@app.route(route_v2 + '/panel/clear/', methods=method_all) +@app.route(route_v2 + '/panel/content/', methods=method_all) +@app.route(route_v2 + '/panel/docker/', methods=method_all) +@app.route(route_v2 + '/panel/go/', methods=method_all) +@app.route(route_v2 + '/panel/java/', methods=method_all) +@app.route(route_v2 + '/panel/nodejs/', methods=method_all) +@app.route(route_v2 + '/panel/other/', methods=method_all) +@app.route(route_v2 + '/panel/php/', methods=method_all) +@app.route(route_v2 + '/panel/python/', methods=method_all) +@app.route(route_v2 + '/panel/quota/', methods=method_all) +@app.route(route_v2 + '/panel/quota/', methods=method_all) +@app.route(route_v2 + '/panel/safe_detect/', methods=method_all) +@app.route(route_v2 + '/panel/scanning/', methods=method_all) +@app.route(route_v2 + '/panel/start_content/', methods=method_all) +@app.route(route_v2 + '/panel/totle_db/', methods=method_all) +@app.route(route_v2 + '/panel/webscanning/', methods=method_all) +@app.route(route_v2 + '/panel/public/', methods=method_all) +@app.route(route_v2 + '/monitor/process_management/', + methods=method_all) +@app.route(route_v2 + '/monitor/soft/', methods=method_all) +@app.route(route_v2 + '/files/down/', methods=method_all) +@app.route(route_v2 + '/files/gz/', methods=method_all) +@app.route(route_v2 + '/files/logs/', methods=method_all) +@app.route(route_v2 + '/files/rar/', methods=method_all) +@app.route(route_v2 + '/files/search/', methods=method_all) +@app.route(route_v2 + '/files/size/', methods=method_all) +@app.route(route_v2 + '/files/upload/', methods=method_all) +@app.route(route_v2 + '/files/zip/', methods=method_all) +@app.route(route_v2 + '/logs/ftp/', methods=method_all) +@app.route(route_v2 + '/logs/panel/', methods=method_all) +@app.route(route_v2 + '/logs/site/', methods=method_all) +def allModule_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 4: return + comReturn = comm.local() + if comReturn: return comReturn + p_path = public.get_plugin_path() + '/' + path_split[2] + + defs = ('model',) + get = get_input() + get.model_index = path_split[2] + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + if not request.path.startswith(route_v2 + '/monitor/') and os.path.exists(p_path): + return panel_other(get.model_index, get.mod_name, def_name) + + from panelControllerV2 import Controller + controller_obj = Controller() + return publicObject(controller_obj, defs, None, get) + + +@app.route(route_v2 + '/public', methods=method_all) +def panel_public_v2(): + get = get_input() + if len("{}".format(get.get_items())) > 1024 * 32: + return 'ERROR' + + # 获取ping测试 + if 'get_ping' in get: + try: + import panel_ping_v2 + p = panel_ping_v2.Test() + get = p.check(get) + if not get: return 'ERROR' + result = getattr(p, get['act'])(get) + result_type = type(result) + if str(result_type).find('Response') != -1: return result + return public.getJson(result), json_header + except: + return abort(404) + + if public.cache_get( + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp())) != 'check': + return abort(404) + global admin_check_auth, admin_path, route_path, admin_path_file + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + return abort(404) + v_list = ['fun', 'name', 'filename', 'data', 'secret_key'] + for n in get.get_items().keys(): + if not n in v_list: + return abort(404) + + get.client_ip = public.GetClientIp() + num_key = get.client_ip + '_wxapp' + if not public.get_error_num(num_key, 10): + return public.return_msg_gettext( + False, + '10 consecutive authentication failures are prohibited for 1 hour') + if not hasattr(get, 'name'): get.name = '' + if not hasattr(get, 'fun'): return abort(404) + if not public.path_safe_check("%s/%s" % (get.name, get.fun)): + return abort(404) + if get.fun in ['login_qrcode', 'is_scan_ok', 'set_login']: + # 检查是否验证过安全入口 + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + return abort(404) + # 验证是否绑定了设备 + if not public.check_app('app'): + return public.return_msg_gettext(False, 'Unbound user') + import wxapp_v2 + pluwx = wxapp_v2.wxapp() + checks = pluwx._check(get) + if type(checks) != bool or not checks: + public.set_error_num(num_key) + return public.getJson(checks), json_header + data = public.getJson(eval('pluwx.' + get.fun + '(get)')) + return data, json_header + else: + return abort(404) + + +@app.route(route_v2 + '//', methods=method_all) +@app.route(route_v2 + '///', methods=method_all) +def panel_other_v2(name=None, fun=None, stype=None): + # 插件接口 + if public.is_error_path(): + return redirect('/error', 302) + if not name: return abort(404) + if not re.match(r"^[\w\-]+$", name): return abort(404) + if fun and not re.match(r"^[\w\-\.]+$", fun): return abort(404) + if name != "mail_sys" or fun != "send_mail_http.json": + comReturn = comm.local() + if comReturn: return comReturn + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + if fun: + if name == 'btwaf' and fun == 'index': + pass + elif name == 'firewall' and fun == 'get_file': + pass + elif fun == 'static': + pass + elif stype == 'html': + pass + else: + if public.get_csrf_cookie_token_key( + ) in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson( + False, + 'CSRF calibration failed, please login again' + ), json_header + args = None + else: + p_path = public.get_plugin_path() + '/' + name + if not os.path.exists(p_path): return abort(404) + args = get_input() + args_list = [ + 'mail_from', 'password', 'mail_to', 'subject', 'content', + 'subtype', 'data' + ] + for k in args.get_items(): + if not k in args_list: return abort(404) + + is_accept = False + if not fun: fun = 'index.html' + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + + if not name: name = 'coll' + if not public.path_safe_check("%s/%s/%s" % (name, fun, stype)): + return abort(404) + if name.find('./') != -1 or not re.match(r"^[\w-]+$", name): + return abort(404) + if not name: + return public.returnJson( + False, 'Please pass in the plug-in name!'), json_header + p_path = public.get_plugin_path() + '/' + name + if not os.path.exists(p_path): + if name == 'btwaf' and fun == 'index': + pdata = {} + import panel_plugin_v2 + plu_panel = panel_plugin_v2.panelPlugin() + plugin_list = plu_panel.get_cloud_list() + if not 'pro' in plugin_list: plugin_list['pro'] = -1 + for p in plugin_list['list']: + if p['name'] in ['btwaf']: + if p['endtime'] != 0 and p['endtime'] < time.time(): + pdata['error_msg'] = 1 + break + return render_template('error3.html', data=pdata) + return abort(404) + + # 是否响插件应静态文件 + if fun == 'static': + if stype.find('./') != -1 or not os.path.exists(p_path + '/static'): + return abort(404) + s_file = p_path + '/static/' + stype + if s_file.find('..') != -1: return abort(404) + if not re.match(r"^[\w\./-]+$", s_file): return abort(404) + if not public.path_safe_check(s_file): return abort(404) + if not os.path.exists(s_file): return abort(404) + return send_file(s_file, conditional=True, etag=True) + + # 准备参数 + if not args: args = get_input() + args.client_ip = public.GetClientIp() + args.fun = fun + + # 初始化插件对象 + try: + is_php = os.path.exists(p_path + '/index.php') + if not is_php: + import panel_plugin_v2 + plu_panel = panel_plugin_v2.panelPlugin() + plugin_list = plu_panel.get_cloud_list() + waf = 0 + if not 'pro' in plugin_list: plugin_list['pro'] = -1 + for p in plugin_list['list']: + if p['name'] in ['btwaf']: + if p['endtime'] != 0 and p['endtime'] < time.time(): + waf = -1 + try: + public.package_path_append(p_path) + plugin_main = __import__(name + '_main') + if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list[ + 'pro'] == -1: + return render_template('error3.html', data={}) + except: + if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list[ + 'pro'] == -1: + return render_template('error3.html', data={}) + if os.path.exists("{}/btwaf".format(public.get_plugin_path())): + return render_template('error3.html', data={}) + try: + if sys.version_info[0] == 2: + reload(plugin_main) + else: + from imp import reload + reload(plugin_main) + except: + pass + + plu = eval('plugin_main.' + name + '_main()') + + if not hasattr(plu, fun): + return public.returnJson(False, + 'Plugin does not exist'), json_header + # 执行插件方法 + if not is_php: + if is_accept: + checks = plu._check(args) + if type(checks) != bool or not checks: + return public.getJson(checks), json_header + data = eval('plu.' + fun + '(args)') + else: + comReturn = comm.local() + if comReturn: return comReturn + import panel_php_v2 + args.s = fun + args.name = name + data = panel_php_v2.panelPHP(name).exec_php_script(args) + + r_type = type(data) + if r_type in [Response, Resp]: + return data + + # 处理响应 + if stype == 'json': # 响应JSON + return public.getJson(data), json_header + elif stype == 'html': # 使用模板 + t_path_root = p_path + '/templates/' + t_path = t_path_root + fun + '.html' + if not os.path.exists(t_path): + return public.returnJson( + False, + 'The specified template does not exist!'), json_header + t_body = public.readFile(t_path) + + # 处理模板包含 + rep = r'{%\s?include\s"(.+)"\s?%}' + includes = re.findall(rep, t_body) + for i_file in includes: + filename = p_path + '/templates/' + i_file + i_body = 'ERROR: File ' + filename + ' does not exists.' + if os.path.exists(filename): + i_body = public.readFile(filename) + t_body = re.sub(rep.replace('(.+)', i_file), i_body, t_body) + + return render_template_string(t_body, data=data) + else: # 直接响应插件返回值,可以是任意flask支持的响应类型 + r_type = type(data) + if r_type == dict: + if name == 'btwaf' and 'msg' in data: + return render_template('error3.html', + data={"error_msg": data['msg']}) + return public.returnJson( + False, + public.getMsg('Bad return type [{}]').format( + r_type)), json_header + return data + except: + return public.get_error_info() + return public.get_error_object(None, plugin_name=name) + + +@app.route(route_v2 + '/hook', methods=method_all) +def panel_hook_v2(): + # webhook接口 + get = get_input() + if not os.path.exists('plugin/webhook'): + return abort(404) + public.package_path_append('plugin/webhook') + import webhook_main + return public.getJson(webhook_main.webhook_main().RunHook(get)) + + +@app.route(route_v2 + '/install', methods=method_all) +def install_v2(): + # 初始化面板接口 + if public.is_spider(): return abort(404) + if not os.path.exists('install.pl'): return redirect('/login') + if public.M('config').where("id=?", ('1',)).getField('status') == 1: + if os.path.exists('install.pl'): os.remove('install.pl') + session.clear() + return redirect('/login') + ret_login = os.path.join('/', admin_path) + if admin_path == '/' or admin_path == '/bt': ret_login = '/login' + session['admin_path'] = False + session['login'] = False + if request.method == method_get[0]: + if not os.path.exists('install.pl'): return redirect(ret_login) + data = {} + data['status'] = os.path.exists('install.pl') + data['username'] = public.GetRandomString(8).lower() + return render_template('install.html', data=data) + + elif request.method == method_post[0]: + if not os.path.exists('install.pl'): return redirect(ret_login) + get = get_input() + if not hasattr(get, 'bt_username'): + return public.get_msg_gettext('The user name cannot be empty!') + if not get.bt_username: + return public.get_msg_gettext('The user name cannot be empty!') + if not hasattr(get, 'bt_password1'): + return public.get_msg_gettext('Password can not be blank!') + if not get.bt_password1: + return public.get_msg_gettext('Password can not be blank!') + if get.bt_password1 != get.bt_password2: + return public.get_msg_gettext( + 'The passwords entered twice do not match, please re-enter!') + public.M('users').where("id=?", (1,)).save( + 'username,password', + (get.bt_username, + public.password_salt(public.md5(get.bt_password1.strip()), + uid=1))) + os.remove('install.pl') + public.M('config').where("id=?", ('1',)).setField('status', 1) + data = {} + data['status'] = os.path.exists('install.pl') + data['username'] = get.bt_username + return render_template('install.html', data=data) + + +# --------------------- websocket START -------------------------- # + + +@sockets.route(route_v2 + '/workorder_client') +def workorder_client_v2(ws): + comReturn = comm.local() + if comReturn: return comReturn + + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): + return + + import panelWorkorder + toObject = panelWorkorder.panelWorkorder() + get = get_input() + toObject.client(ws, get) + + +@sockets.route(route_v2 + '/ws_panel') +def ws_panel_v2(ws): + ''' + @name 面板接口ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + comReturn = comm.local() + if comReturn: return comReturn + + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + while True: + pdata = ws.receive() + if pdata == '{}': break + data = json.loads(pdata) + get = public.to_dict_obj(data) + get._ws = ws + p = threading.Thread(target=ws_panel_thread_v2, args=(get,)) + p.start() + + +def ws_panel_thread_v2(get): + ''' + @name 面板管理ws线程 + @author hwliang<2021-07-24> + @param get 请求参数 + @return void + ''' + + if not hasattr(get, 'ws_callback'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) + return + if not hasattr(get, 'mod_name'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'mod_name'))) + return + if not hasattr(get, 'def_name'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'def_name'))) + return + get.mod_name = get.mod_name.strip() + get.def_name = get.def_name.strip() + check_str = '{}{}'.format(get.mod_name, get.def_name) + if not re.match(r"^\w+$", check_str) or get.mod_name in [ + 'public', 'common', 'db', 'db_mysql', 'downloadFile', 'jobs' + ]: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Unsafe mod_name, def_name parameter content'))) + return + + mod_file = '{}/{}.py'.format(public.get_class_path(), get.mod_name) + if not os.path.exists(mod_file): + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) + return + _obj = public.get_script_object(mod_file) + if not _obj: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) + return + _cls = getattr(_obj, get.mod_name) + if not _cls: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.mod_name)))) + return + _def = getattr(_cls(), get.def_name) + if not _def: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.def_name)))) + return + result = {'callback': get.ws_callback, 'result': _def(get)} + get._ws.send(public.getJson(result)) + + +@sockets.route(route_v2 + '/ws_project') +def ws_project_v2(ws): + ''' + @name 项目管理ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + comReturn = comm.local() + if comReturn: return comReturn + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + from panelProjectControllerV2 import ProjectController + project_obj = ProjectController() + while True: + pdata = ws.receive() + if pdata in '{}': break + get = public.to_dict_obj(json.loads(pdata)) + get._ws = ws + p = threading.Thread(target=ws_project_thread_v2, + args=(project_obj, get)) + p.start() + + +def ws_project_thread_v2(_obj, get): + ''' + @name 项目管理ws线程 + @author hwliang<2021-07-24> + @param _obj 项目管理控制器对像 + @param get 请求参数 + @return void + ''' + if not hasattr(get, 'ws_callback'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) + return + result = {'callback': get.ws_callback, 'result': _obj.model(get)} + get._ws.send(public.getJson(result)) + + +sock_pids = {} + + +@sockets.route(route_v2 + '/sock_shell') +def sock_shell_v2(ws): + ''' + @name 执行指定命令,实时输出命令执行结果 + @author hwliang<2021-07-19> + @return void + + 示例: + p = new WebSocket('ws://192.168.1.247:8888/sock_shell') + p.send('ping www.bt.cn -c 100') + ''' + comReturn = comm.local() + if comReturn: + ws.send(str(comReturn)) + return + kill_closed_v2() + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + t = None + try: + while True: + cmdstring = ws.receive() + if cmdstring in ['stop', 'error'] or not cmdstring: + break + t = threading.Thread(target=sock_recv, args=(cmdstring, ws)) + t.start() + kill_closed_v2() + except: + kill_closed_v2() + + +def kill_closed_v2(): + ''' + @name 关闭已关闭的连接 + @author hwliang<2021-07-24> + @return void + ''' + global sock_pids + import psutil + pids = psutil.pids() + keys = sock_pids.copy().keys() + for pid in keys: + if hasattr(sock_pids[pid], 'closed'): + is_closed = sock_pids[pid].closed + else: + is_closed = not sock_pids[pid].connected + + logging.debug("PID: {} , sock_stat: {}".format(pid, is_closed)) + if not is_closed: continue + + if pid in pids: + try: + p = psutil.Process(pid) + for cp in p.children(): + cp.kill() + p.kill() + logging.debug("killed: {}".format(pid)) + sock_pids.pop(pid) + except: + pass + else: + sock_pids.pop(pid) + + +@app.route(route_v2 + '/close_sock_shell', methods=method_all) +def close_sock_shell_v2(): + ''' + @name 关闭指定命令 + @author hwliang<2021-07-19> + @param cmdstring 完整命令行 + @return dict + 示例: + $.post('/close_sock_shell',{cmdstring:'ping www.bt.cn -c 100'}) + ''' + comReturn = comm.local() + if comReturn: return comReturn + args = get_input() + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + cmdstring = args.cmdstring.strip() + skey = public.md5(cmdstring) + pid = cache.get(skey) + if not pid: + return json.dumps( + public.return_data( + False, [], error_msg='The specified sock has been terminated!') + ), json_header + os.kill(pid, 9) + cache.delete(skey) + return json.dumps(public.return_data(True, + 'Successful operation!')), json_header + + +@sockets.route(route_v2 + '/webssh') +def webssh_v2(ws): + # 宝塔终端连接 + comReturn = comm.local() + if comReturn: + ws.send(str(comReturn)) + return + if not ws: return 'False' + get = ws.receive() + if not get: return + get = json.loads(get) + if not check_csrf_websocket(ws, get): + return + + import ssh_terminal_v2 + sp = ssh_terminal_v2.ssh_host_admin() + if 'host' in get: + ssh_info = {} + ssh_info['host'] = get['host'].strip() + if 'port' in get: + ssh_info['port'] = int(get['port']) + if 'username' in get: + ssh_info['username'] = get['username'].strip() + if 'password' in get: + ssh_info['password'] = get['password'].strip() + if 'pkey' in get: + ssh_info['pkey'] = get['pkey'].strip() + + if get['host'] in ['127.0.0.1', 'localhost' + ] and 'port' not in ssh_info: + ssh_info = sp.get_ssh_info('127.0.0.1') + if not ssh_info: ssh_info = sp.get_ssh_info('localhost') + if not ssh_info: ssh_info = {"host": "127.0.0.1"} + ssh_info['port'] = public.get_ssh_port() + else: + ssh_info = sp.get_ssh_info('127.0.0.1') + if not ssh_info: ssh_info = sp.get_ssh_info('localhost') + if not ssh_info: ssh_info = {"host": "127.0.0.1"} + ssh_info['port'] = public.get_ssh_port() + + if not ssh_info['host'] in ['127.0.0.1', 'localhost']: + if not 'username' in ssh_info: + ssh_info = sp.get_ssh_info(ssh_info['host']) + if not ssh_info: + ws.send( + 'The specified host information is not found, please add it again!' + ) + return + p = ssh_terminal_v2.ssh_terminal() + p.run(ws, ssh_info) + del (p) + if ws.connected: + ws.close() + return 'False' + + +# --------------------- websocket END -------------------------- # + + +@app.route(route_v2 + "/daily", methods=method_all) +def daily_v2(): + """面板日报数据""" + + comReturn = comm.local() + if comReturn: return comReturn + + import panelDaily + toObject = panelDaily.panelDaily() + + defs = ("get_app_usage", "get_daily_data", "get_daily_list") + result = publicObject(toObject, defs) + return result + + +@app.route(route_v2 + '/phpmyadmin/', methods=method_all) +def pma_proxy_v2(path_full=None): + ''' + @name phpMyAdmin代理 + @author hwliang<2022-01-19> + @return Response + ''' + comReturn = comm.local() + if comReturn: return comReturn + cache_key = 'pmd_port_path' + pmd = cache.get(cache_key) + if not pmd: + pmd = get_phpmyadmin_dir() + if not pmd: + return 'phpMyAdmin is not installed, please go to the [App Store] page to install it!' + pmd = list(pmd) + cache.set(cache_key, pmd, 10) + panel_pool = 'http://' + if request.url_root[:5] == 'https': + panel_pool = 'https://' + import ajax + ssl_info = ajax.ajax().get_phpmyadmin_ssl(None) + if ssl_info['status']: + pmd[1] = ssl_info['port'] + else: + panel_pool = 'http://' + + proxy_url = '{}127.0.0.1:{}/{}/'.format( + panel_pool, pmd[1], pmd[0]) + request.full_path.replace( + '/phpmyadmin/', '') + from panel_http_proxy_v2 import HttpProxy + px = HttpProxy() + return px.proxy(proxy_url) + + +@app.route(route_v2 + '/p/', methods=method_all) +@app.route(route_v2 + '/p//', methods=method_all) +@app.route(route_v2 + '/p//', methods=method_all) +def proxy_port_v2(port, full_path=None): + ''' + @name 代理指定端口 + @author hwliang<2022-01-19> + @return Response + ''' + + comReturn = comm.local() + if comReturn: return comReturn + full_path = request.full_path.replace('/p/{}/'.format(port), + '').replace('/p/{}'.format(port), '') + uri = '{}/{}'.format(port, full_path) + uri = uri.replace('//', '/') + proxy_url = 'http://127.0.0.1:{}'.format(uri) + from panel_http_proxy_v2 import HttpProxy + px = HttpProxy() + return px.proxy(proxy_url) + + +@app.route(route_v2 + '/push', methods=method_all) +def push_v2(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + import panel_push_v2 + toObject = panel_push_v2.panelPush() + defs = ('set_push_status', 'get_push_msg_list', 'get_modules_list', + 'install_module', 'uninstall_module', 'get_module_template', + 'set_push_config', 'get_push_config', 'del_push_config', + 'get_module_logs', 'get_module_config', 'get_push_list', + 'get_push_logs') + result = publicObject(toObject, defs, None, pdata) + return result + + +# 2024/1/24 上午 11:42 新场景模型的路由 +# @app.route(route_v2 + '/mod///', methods=method_all) +# @app.route(route_v2 + '/mod////', methods=method_all) +# def panel_mod_v2(name=None, sub_name=None, fun=None, stype=None): +# @app.route('/mod/proxy/com/', methods=method_all) +# @app.route('/mod/proxy/com//', methods=method_all) +@app.route(route_v2 + '/mod/proxy/com/', methods=method_all) +@app.route(route_v2 + '/mod/proxy/com//', methods=method_all) +# @app.route(route_v2 + '/project/proxy/', methods=method_all) +# @app.route(route_v2 + '/project/proxy//', methods=method_all) +def panel_mod_v2(fun=None, stype=None): + ''' + @name 新场景模型的路由 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + name = path_split[3] + sub_name = path_split[4] + # if not public.is_bind(): + # return redirect('/bind', 302) + if public.is_error_path(): + return redirect('/error', 302) + if not name: return abort(404) + if not sub_name: return abort(404) + if not re.match(r"^[\w\-]+$", name): return abort(404) + if not re.match(r"^[\w\-]+$", sub_name): return abort(404) + if fun and not re.match(r"^[\w\-\.]+$", fun): return abort(404) + + comReturn = comm.local() + if comReturn: return comReturn + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + if fun: + if public.get_csrf_cookie_token_key() in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + + args = get_mod_input() + + if not fun: fun = 'index.html' + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + + if not name: name = 'coll' + if not public.path_safe_check("%s/%s/%s/%s" % (name, sub_name, fun, stype)): + return abort(404) + if name.find('./') != -1 or not re.match(r"^[\w-]+$", name): + return abort(404) + if sub_name.find('./') != -1 or not re.match(r"^[\w-]+$", sub_name): + return abort(404) + if not name: + return public.returnJson(False, 'PLUGIN_INPUT_ERR'), json_header + + args.client_ip = public.GetClientIp() + + # 初始化新场景模型对象 + try: + from mod.modController import Controller + controller_obj = Controller() + defs = ('model',) + args.model_index = "mod" + args.action = 'model' + args.mod_name = name + args.sub_mod_name = sub_name + args.def_name = fun + data = publicObject(controller_obj, defs, None, args) + r_type = type(data) + if r_type in [Response, Resp]: + return data + + p_path = public.get_mod_path() + '/' + name + # 处理响应 + if stype == 'json': # 响应JSON + return public.getJson(data), json_header + elif stype == 'html': # 使用模板 + t_path_root = p_path + '/templates/' + t_path = t_path_root + fun + '.html' + if not os.path.exists(t_path): + return public.returnJson(False, + 'PLUGIN_NOT_TEMPLATE'), json_header + t_body = public.readFile(t_path) + # 处理模板包含 + rep = r'{%\s?include\s"(.+)"\s?%}' + includes = re.findall(rep, t_body) + for i_file in includes: + filename = p_path + '/templates/' + i_file + i_body = 'ERROR: File ' + filename + ' does not exists.' + if os.path.exists(filename): + i_body = public.readFile(filename) + t_body = re.sub(rep.replace('(.+)', i_file), i_body, t_body) + return render_template_string(t_body, data=data) + else: # 直接响应插件返回值,可以是任意flask支持的响应类型 + r_type = type(data) + if r_type == dict: + if name == 'btwaf' and 'msg' in data: + return render_template('error3.html', + data={"error_msg": data['msg']}) + return public.returnJson( + False, + public.getMsg('PUBLIC_ERR_RETURN').format( + r_type)), json_header + return data + except: + if not 'login' in session: return abort(404) + return public.get_error_object(None, plugin_name=name) + +@app.route(route_v2 + '/check_auth', methods=method_all) +def check_auth_v2(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + if os.path.exists('data/.is_pro.pl'): + return public.return_message(0,0,'true') + return public.return_message(-1,0,'false') + +@app.route('/bind', methods=method_get) +def bind(): + comReturn = comm.local() + if comReturn: return comReturn + if public.is_bind(): return redirect('/', 302) + data = {} + data['lan'] = public.GetLan('index_new') + # g.title = '请先绑定宝塔帐号' + return render_template('index_new.html', data=data) + + + + +# ===========================================================v2路由区end===========================================================# + +@app.route('/v2/install_finish', methods=method_post) +def install_finish(): + with open('{}/data/install_finished.mark'.format(public.get_panel_path()), 'w') as fp: + fp.write('True') + return public.return_message(0, 0, 'Successfully') + + +@app.route('/v2/wp/login/', methods=method_get) +def wp_login(site_id: int): + if site_id < 1: + return public.gettext_msg('Invalid site_id') + + from wp_toolkit import wpmgr + + return wpmgr(site_id).auto_login() + + +# 获取新场景模型的传参数据 +def get_mod_input(): + ''' + @name # 获取新场景模型的传参数据 + @author wzz <2024/1/24 上午 11:42> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + data = public.dict_obj() + exludes = ['blob'] + for key in request.args.keys(): + data.set(key, str(request.args.get(key, ''))) + + if request.is_json: + for key in request.get_json().keys(): + data.set(key, str(request.get_json()[key])) + else: + try: + for key in request.form.keys(): + if key in exludes: continue + data.set(key, str(request.form.get(key, ''))) + except: + try: + post = request.form.to_dict() + for key in post.keys(): + if key in exludes: continue + data.set(key, str(post[key])) + except: + pass + + if 'form_data' in g: + for k in g.form_data.keys(): + data.set(k, str(g.form_data[k])) + + if not hasattr(data, 'data'): data.data = [] + return data diff --git a/BTPanel/app.py b/BTPanel/app.py new file mode 100644 index 00000000..6b62c077 --- /dev/null +++ b/BTPanel/app.py @@ -0,0 +1,671 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +import logging +import sys +import json +import os +import threading +import time +import re +import uuid +import psutil + +panel_path = '/www/server/panel' +if not os.name in ['nt']: + os.chdir(panel_path) +if not 'class/' in sys.path: + sys.path.insert(0, 'class/') +if not 'class_v2/' in sys.path: + sys.path.insert(0, 'class_v2/') +from flask import Flask, session, render_template, send_file, request, redirect, g, make_response, \ + render_template_string, abort, stream_with_context, Response as Resp +from cachelib import SimpleCache, SimpleCacheSession +from werkzeug.wrappers import Response +from werkzeug.routing import BaseConverter +from flask_session import Session +from flask_compress import Compress + +cache = SimpleCache(5000) +import public + +# class RestrictedImportHook: +# def __init__(self, allowed_modules): +# self.allowed_modules = allowed_modules +# +# def find_spec(self, fullname, path, target=None): +# # 判断当前导入是否是功能模块,如果是则禁止导入 +# if fullname in self.allowed_modules: +# return None +# return self +# +# def loader(self, fullname): +# # 不禁止public模块的导入 +# if fullname == 'public': +# pass +# +# # 其他功能模块 不能互相导入 +# # elif fullname in ['aaa', 'bbb', 'ccc']: +# # return APIModuleLoader(self.allowed_module) +# 设置导入钩子 +# sys.meta_path.insert(0, RestrictedImportHook(['panelPlugin', 'pay', 'ols', 'wxapp'])) +# sys.meta_path.insert(0, RestrictedImportHook(['wxapp'])) +# class APIModuleLoader: +# def __init__(self, allowed_module): +# self.allowed_module = allowed_module +# +# def exec_module(self, module): +# if self.allowed_module != 'public': +# raise ImportError(f"API module '{self.allowed_module}' cannot import other API modules") +# +# if module.__name__ != 'public': +# module.__dict__['public'] = sys.modules['public'] + +# # 设置导入钩子 +# sys.meta_path.insert(0, RestrictedImportHook('public')) + +# 初始化Flask应用 +app = Flask(__name__, + template_folder="templates/{}".format( + public.GetConfigValue('template'))) +Compress(app) +try: + from flask_sock import Sock +except: + from flask_sockets import Sockets as Sock + +sockets = Sock(app) +# 注册HOOK +hooks = {} +if not hooks: + public.check_hooks() +# import db +dns_client = None +app.config['DEBUG'] = os.path.exists('data/debug.pl') +app.config['SSL'] = os.path.exists('data/ssl.pl') + +# 设置BasicAuth +basic_auth_conf = 'config/basic_auth.json' +app.config['BASIC_AUTH_OPEN'] = False +if os.path.exists(basic_auth_conf): + try: + ba_conf = json.loads(public.readFile(basic_auth_conf)) + app.config['BASIC_AUTH_USERNAME'] = ba_conf['basic_user'] + app.config['BASIC_AUTH_PASSWORD'] = ba_conf['basic_pwd'] + app.config['BASIC_AUTH_OPEN'] = ba_conf['open'] + except: + pass + +# 初始化SESSION服务 +app.secret_key = public.md5( + str(os.uname()) + + str(psutil.boot_time())) # uuid.UUID(int=uuid.getnode()).hex[-12:] +local_ip = None +my_terms = {} +app.config['SESSION_MEMCACHED'] = SimpleCacheSession(1000, 86400) +app.config['SESSION_TYPE'] = 'memcached' +app.config['SESSION_PERMANENT'] = True +app.config['SESSION_USE_SIGNER'] = True +app.config['SESSION_KEY_PREFIX'] = 'BT_:' +app.config['SESSION_COOKIE_NAME'] = public.md5(app.secret_key) +app.config['PERMANENT_SESSION_LIFETIME'] = 86400 * 30 +if app.config['SSL']: + app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' + app.config['SESSION_COOKIE_SECURE'] = True +else: + app.config['SESSION_COOKIE_SAMESITE'] = None + +Session(app) + +import common + +# 初始化路由 +comm = common.panelAdmin() +method_all = ['GET', 'POST'] +method_get = ['GET'] +method_post = ['POST'] +json_header = {'Content-Type': 'application/json; charset=utf-8'} +text_header = {'Content-Type': 'text/plain; charset=utf-8'} +cache.set('p_token', 'bmac_' + public.Md5(public.get_mac_address())) +admin_path_file = 'data/admin_path.pl' +admin_path = '/' +if os.path.exists(admin_path_file): + admin_path = public.readFile(admin_path_file).strip() +admin_path_checks = [ + '/', + '/san', + '/bak', + '/monitor', + '/abnormal', + '/close', + '/task', + '/login', + '/config', + '/site', + '/sites', + '/ftp', + '/public', + '/database', + '/data', + '/download_file', + '/control', + '/crontab', + '/firewall', + '/files', + '/soft', + '/ajax', + '/system', + '/panel_data', + '/code', + '/ssl', + '/plugin', + '/wxapp', + '/hook', + '/safe', + '/yield', + '/downloadApi', + '/pluginApi', + '/auth', + '/download', + '/cloud', + '/webssh', + '/connect_event', + '/panel', + '/acme', + '/down', + '/api', + '/tips', + '/message', + '/warning', + '/userRegister', # 面板内注册 + '/docker', + '/btdocker', +] +if admin_path in admin_path_checks: admin_path = '/bt' +if admin_path[-1] == '/': admin_path = admin_path[:-1] +uri_match = re.compile( + r"(^/static/[\w_\./\-]+\.(js|css|png|jpg|gif|ico|svg|woff|woff2|ttf|otf|eot|map)$|^/[\w_\./\-]*$)" +) +session_id_match = re.compile(r"^[\w\.\-]+$") + +route_path = os.path.join(admin_path, '') +if not route_path: route_path = '/' +if route_path[-1] == '/': route_path = route_path[:-1] +if route_path[0] != '/': route_path = '/' + route_path + +route_v2 = '/v2' #v2版本路由前缀 + +# ======================公共方法区域START============================# + +def error_500(e: Exception): + if request.method not in ['GET', 'POST']: return + if not session.get('login', None): + g.auth_error = True + return public.error_not_login() + ss = '''404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +During handling of the above exception, another exception occurred:''' + error_info = public.get_error_info().strip().split(ss)[-1].strip() + _form = request.form.to_dict() + if 'username' in _form: _form['username'] = '******' + if 'password' in _form: _form['password'] = '******' + if 'phone' in _form: _form['phone'] = '******' + request_info = '''REQUEST_DATE: {request_date} + PAN_VERSION: {panel_version} + OS_VERSION: {os_version} + REMOTE_ADDR: {remote_addr} + REQUEST_URI: {method} {full_path} +REQUEST_FORM: {request_form} + USER_AGENT: {user_agent}'''.format( + request_date=public.getDate(), + remote_addr=public.GetClientIp(), + method=request.method, + full_path=public.xsssec(request.full_path), + request_form=public.xsssec(str(_form)), + user_agent=public.xsssec(request.headers.get('User-Agent')), + panel_version=public.version(), + os_version=public.get_os_version()) + + result = public.readFile( + public.get_panel_path() + + '/BTPanel/templates/default/panel_error.html').format( + error_title=error_info.split("\n")[-1], + request_info=request_info, + error_msg=error_info) + return Resp(result, 500) + +def get_dir_down(filename, token, find): + # 获取分享目录信息 + import files + args = public.dict_obj() + args.path = filename + args.share = True + to_path = filename.replace(find['filename'], '').strip('/') + + if request.args.get('play') == 'true': + pdata = files.files().get_videos(args) + return public.GetJson(pdata), json_header + else: + pdata = files.files().GetDir(args) + pdata['token'] = token + pdata['ps'] = find['ps'] + pdata['src_path'] = find['filename'] + pdata['to_path'] = to_path + if find['expire'] < (time.time() + (86400 * 365 * 10)): + pdata['expire'] = public.format_date(times=find['expire']) + else: + pdata['expire'] = public.get_msg_gettext('Never Expires') + pdata['filename'] = (find['filename'].split('/')[-1] + '/' + + to_path).strip('/') + return render_template('down.html', data=pdata, to_size=public.to_size) + + +def get_phpmyadmin_dir(): + # 获取phpmyadmin目录 + path = public.GetConfigValue('setup_path') + '/phpmyadmin' + if not os.path.exists(path): return None + phpport = '888' + try: + import re + if session['webserver'] == 'nginx': + filename = public.GetConfigValue( + 'setup_path') + '/nginx/conf/nginx.conf' + conf = public.readFile(filename) + rep = r"listen\s+([0-9]+)\s*;" + rtmp = re.search(rep, conf) + if rtmp: + phpport = rtmp.groups()[0] + if session['webserver'] == 'apache': + filename = public.GetConfigValue( + 'setup_path') + '/apache/conf/extra/httpd-vhosts.conf' + conf = public.readFile(filename) + rep = r"Listen\s+([0-9]+)\s*\n" + rtmp = re.search(rep, conf) + if rtmp: + phpport = rtmp.groups()[0] + if session['webserver'] == 'openlitespeed': + filename = public.GetConfigValue( + 'setup_path') + '/panel/vhost/openlitespeed/listen/888.conf' + public.writeFile('/tmp/2', filename) + conf = public.readFile(filename) + rep = r"address\s*\*\:\s*(\d+)" + rtmp = re.search(rep, conf) + if rtmp: + phpport = rtmp.groups()[0] + except: + pass + + for filename in os.listdir(path): + filepath = path + '/' + filename + if os.path.isdir(filepath): + if filename[0:10] == 'phpmyadmin': + return str(filename), phpport + return None + + +class run_exec: + # 模块访问对像 + def run(self, toObject, defs, get): + result = None + + if not get.action in defs: + return public.ReturnJson( + False, 'Specific parameters are invalid!'), json_header + + result = getattr(toObject, get.action)(get) + if not hasattr(get, 'html') and not hasattr(get, 's_module'): + r_type = type(result) + if r_type in [Response, Resp]: return result + result = public.GetJson(result), json_header + + if g.is_aes: + result = public.aes_encrypt(result[0], g.aes_key), json_header + + return result + + +def check_csrf(): + # CSRF校验 + if app.config['DEBUG']: return True + http_token = request.headers.get('x-http-token') + if not http_token: return False + if http_token != public.get_csrf_sess_html_token_value(): return False + return True + + +def publicObject(toObject, defs, action=None, get=None, is_csrf=True): + try: + # 模块访问前置检查 + if is_csrf and public.get_csrf_sess_html_token_value() and session.get( + 'login', None): + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + + if not get: get = get_input() + if action: get.action = action + + if hasattr(get, 'path'): + get.path = get.path.replace('//', '/').replace('\\', '/') + if get.path.find('./') != -1: + return public.ReturnJson(False, 'Unsafe path'), json_header + if get.path.find('->') != -1: + get.path = get.path.split('->')[0].strip() + get.path = public.xssdecode(get.path) + if hasattr(get, 'filename'): + get.filename = public.xssdecode(get.filename) + + if hasattr(get, 'sfile'): + get.sfile = get.sfile.replace('//', '/').replace('\\', '/') + get.sfile = public.xssdecode(get.sfile) + if hasattr(get, 'dfile'): + get.dfile = get.dfile.replace('//', '/').replace('\\', '/') + get.dfile = public.xssdecode(get.dfile) + + if hasattr(toObject, 'site_path_check'): + if not toObject.site_path_check(get): + return public.ReturnJson( + False, "Overstepping one authority!"), json_header + return run_exec().run(toObject, defs, get) + except: + return error_500(None) + + +def check_login(http_token=None): + # 检查是否登录面板 + if cache.get('dologin'): return False + if 'login' in session: + loginStatus = session['login'] + if loginStatus and http_token: + if public.get_csrf_sess_html_token_value() != http_token: + return False + return loginStatus + return False + + +def get_pd(): + # 获取授权信息 + tmp = -1 + # try: + # import panelPlugin + # get = public.dict_obj() + # # get.init = 1 + # tmp1 = panelPlugin.panelPlugin().get_cloud_list(get) + # except: + tmp1 = None + if tmp1: + tmp = tmp1[public.to_string([112, 114, 111])] + ltd = tmp1.get('ltd', -1) + else: + ltd = -1 + tmp4 = cache.get(public.to_string([112, 95, 116, 111, 107, 101, 110])) + if tmp4: + tmp_f = public.to_string([47, 116, 109, 112, 47]) + tmp4 + if not os.path.exists(tmp_f): public.writeFile(tmp_f, '-1') + tmp = public.readFile(tmp_f) + if tmp: tmp = int(tmp) + + if ltd < 1: + if ltd == -2: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, + 116, 108, 116, 100, 45, 103, 114, 97, 121, 34, 62, 60, 115, + 112, 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, 111, + 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, + 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, + 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, 105, + 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, 36807, 26399, + 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, + 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, + 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, + 117, 112, 100, 97, 116, 97, 95, 108, 116, 100, 40, 41, 34, 62, + 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, 112, 97, 110, + 62 + ]) + elif tmp == -1: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, + 116, 112, 114, 111, 45, 102, 114, 101, 101, 34, 32, 111, 110, + 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, + 46, 114, 101, 110, 101, 119, 95, 112, 114, 111, 40, 41, 34, 32, + 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, 107, 32, + 116, 111, 32, 103, 101, 116, 32, 80, 82, 79, 34, 62, 20813, + 36153, 29256, 60, 47, 115, 112, 97, 110, 62 + ]) + elif tmp == -2: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, + 116, 112, 114, 111, 45, 103, 114, 97, 121, 34, 62, 60, 115, + 112, 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, 111, + 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, + 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, + 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, 105, + 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, 36807, 26399, + 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, + 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, + 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, + 114, 101, 110, 101, 119, 95, 112, 114, 111, 40, 41, 34, 62, 82, + 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, 112, 97, 110, 62 + ]) + if tmp >= 0 and ltd in [-1, -2]: + if tmp == 0: + tmp2 = public.to_string([27704, 20037, 25480, 26435]) + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, + 98, 116, 112, 114, 111, 34, 62, 123, 48, 125, 60, 115, 112, + 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, 111, 108, + 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, + 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, + 98, 111, 108, 100, 59, 34, 62, 123, 49, 125, 60, 47, 115, + 112, 97, 110, 62, 60, 47, 115, 112, 97, 110, 62 + ]).format( + public.to_string([21040, 26399, 26102, 38388, 65306]), + tmp2) + else: + tmp2 = time.strftime( + public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), + time.localtime(tmp)) + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, + 98, 116, 112, 114, 111, 34, 62, 69, 120, 112, 105, 114, + 101, 58, 32, 60, 115, 112, 97, 110, 32, 115, 116, 121, 108, + 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, + 54, 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, + 103, 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, + 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, 112, + 120, 34, 62, 123, 48, 125, 60, 47, 115, 112, 97, 110, 62, + 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, + 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, + 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, + 101, 119, 95, 112, 114, 111, 40, 41, 34, 62, 82, 69, 78, + 69, 87, 60, 47, 97, 62, 60, 47, 115, 112, 97, 110, 62 + ]).format(tmp2) + else: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, + 116, 112, 114, 111, 45, 103, 114, 97, 121, 34, 32, 111, 110, + 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, + 46, 114, 101, 110, 101, 119, 95, 112, 114, 111, 40, 41, 34, 32, + 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, 107, 32, + 116, 111, 32, 103, 101, 116, 32, 80, 82, 79, 34, 62, 70, 82, + 69, 69, 60, 47, 115, 112, 97, 110, 62 + ]) + else: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, + 108, 116, 100, 34, 62, 69, 120, 112, 105, 114, 101, 58, 32, 60, + 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, 111, + 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, + 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, 108, + 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, 105, 103, 104, 116, + 58, 53, 112, 120, 34, 62, 123, 125, 60, 47, 115, 112, 97, 110, 62, + 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 105, 110, + 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, + 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, 114, 111, + 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, + 112, 97, 110, 62 + ]).format( + time.strftime(public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), + time.localtime(ltd))) + + return tmp3, tmp, ltd + + +def send_authenticated(): + # 发送http认证信息 + request_host = public.GetHost() + result = Response( + '', 401, + {'WWW-Authenticate': 'Basic realm="%s"' % request_host.strip()}) + if not 'login' in session and not 'admin_auth' in session: session.clear() + return result + + +# 取端口 +def FtpPort(): + # 获取FTP端口 + if session.get('port'): return + import re + try: + file = public.GetConfigValue( + 'setup_path') + '/pure-ftpd/etc/pure-ftpd.conf' + conf = public.readFile(file) + rep = r"\n#?\s*Bind\s+[0-9]+\.[0-9]+\.[0-9]+\.+[0-9]+,([0-9]+)" + port = re.search(rep, conf).groups()[0] + except: + port = '21' + session['port'] = port + + +def is_login(result): + # 判断是否登录2 + if 'login' in session: + if session['login'] == True: + # result = make_response(result) + # request_token = public.GetRandomString(48) + # request_token_key = public.get_csrf_cookie_token_key() + # session[request_token_key] = request_token + # samesite = app.config['SESSION_COOKIE_SAMESITE'] + # secure = app.config['SESSION_COOKIE_SECURE'] + # if app.config['SSL'] and request.full_path.find('/login?tmp_token=') == 0: + # samesite = 'None' + # secure = True + # result.set_cookie(request_token_key, request_token, + # max_age=86400 * 30, + # samesite= samesite, + # secure=secure + # ) + pass + return result + + +# js随机数模板使用,用于不更新版本号时更新前端文件不需要用户强制刷新浏览器 +def get_js_random(): + js_random = public.readFile('data/js_random.pl') + if not js_random or js_random == '1': + js_random = public.GetRandomString(16) + public.writeFile('data/js_random.pl', js_random) + return js_random + + +# 获取输入数据 +def get_input(): + data = public.dict_obj() + exludes = ['blob'] + for key in request.args.keys(): + data.set(key, str(request.args.get(key, ''))) + try: + for key in request.form.keys(): + if key in exludes: + continue + data.set(key, str(request.form.get(key, ''))) + + except Exception as ex: + # public.print_log("error1 {}".format(ex)) + + try: + post = request.form.to_dict() + for key in post.keys(): + if key in exludes: continue + data.set(key, str(post[key])) + except: + pass + + if 'form_data' in g: + for k in g.form_data.keys(): + data.set(k, str(g.form_data[k])) + + if not hasattr(data, 'data'): + data.data = [] + return data + + +# 取数据对象 +def get_input_data(data): + pdata = public.dict_obj() + for key in data.keys(): + pdata[key] = str(data[key]) + return pdata + + +# 检查Token +def check_token(data): + # 已作废 + pluginPath = 'plugin/safelogin/token.pl' + if not os.path.exists(pluginPath): return False + from urllib import unquote + from binascii import unhexlify + from json import loads + + result = unquote(unhexlify(data)) + token = public.readFile(pluginPath).strip() + + result = loads(result) + if not result: return False + if result['token'] != token: return False + return result + + +# ======================公共方法区域END============================# + + +# ======================自定义路由URL匹配规则转换器Begin===================== # + +class RegexConverter(BaseConverter): + """ + 自定义URL匹配正则表达式 + """ + + def __init__(self, map, regex): + super(RegexConverter, self).__init__(map) + self.regex = regex + + def to_python(self, value): + """ + 路由匹配时,匹配成功后传递给视图函数中参数的值 + :param value: + :return: + """ + return value + + def to_url(self, value): + """ + 使用url_for反向生成URL时,传递的参数经过该方法处理,返回的值用于生成URL中的参数 + :param value: + :return: + """ + val = super(RegexConverter, self).to_url(value) + return val + + +# 添加到flask中 +app.url_map.converters.update({ + 'regex': RegexConverter +}) + +# ======================自定义路由URL匹配规则转换器End===================== # diff --git a/BTPanel/routes/flask_hook.py b/BTPanel/routes/flask_hook.py new file mode 100644 index 00000000..221c98c5 --- /dev/null +++ b/BTPanel/routes/flask_hook.py @@ -0,0 +1,194 @@ +# ===================================Flask HOOK========================# +from BTPanel.app import * + +# Flask请求勾子 +@app.before_request +def request_check(): + if request.method not in ['GET', 'POST']: return abort(404) + + # 获取客户端真实IP + x_real_ip = request.headers.get('X-Real-Ip') + if x_real_ip: + request.remote_addr = x_real_ip + request.environ.setdefault('REMOTE_PORT', public.get_remote_port()) + + g.request_time = time.time() + g.return_message = False + # 路由和URI长度过滤 + if len(request.path) > 256: return abort(403) + if len(request.url) > 1024: return abort(403) + # URI过滤 + if not uri_match.match(request.path): return abort(403) + # POST参数过滤 + if request.path in [ + '/login', + '/safe', + # '/v2_safe', + '/hook', + '/public', + '/down', + '/get_app_bind_status', + '/check_bind', + '/userRegister', + ]: + pdata = request.form.to_dict() + for k in pdata.keys(): + if len(k) > 48: return abort(403) + if len(pdata[k]) > 256: return abort(403) + # SESSIONID过滤 + session_id = request.cookies.get(app.config['SESSION_COOKIE_NAME'], '') + if session_id and not session_id_match.match(session_id): return abort(403) + + # 请求头过滤 + # if not public.filter_headers(): + # return abort(403) + + if session.get('debug') == 1: return + g.get_csrf_html_token_key = public.get_csrf_html_token_key() + + if app.config['BASIC_AUTH_OPEN']: + if request.path in [ + '/public', '/download', '/mail_sys', '/hook', '/down', + '/check_bind', '/get_app_bind_status' + ]: + return + auth = request.authorization + if not comm.get_sk(): return + if not auth: return send_authenticated() + tips = '_bt.cn' + if public.md5(auth.username.strip() + tips) != app.config['BASIC_AUTH_USERNAME'] \ + or public.md5(auth.password.strip() + tips) != app.config['BASIC_AUTH_PASSWORD']: + return send_authenticated() + + if not request.path in [ + '/safe', + # '/v2_safe', + '/hook', + '/public', + '/mail_sys', + '/down' + ]: + ip_check = public.check_ip_panel() + if ip_check: return ip_check + + if request.path.startswith('/static/') or request.path == '/code': + if not 'login' in session and not 'admin_auth' in session and not 'down' in session: + return abort(401) + domain_check = public.check_domain_panel() + if domain_check: return domain_check + if public.is_local(): + not_networks = ['uninstall_plugin', 'install_plugin', 'UpdatePanel'] + if request.args.get('action') in not_networks: + return public.returnJson( + False, + 'This feature cannot be used in offline mode!'), json_header + + if request.path in [ + '/site', '/ftp', '/database', '/soft', '/control', '/firewall', + '/files', '/xterm', '/crontab', '/config' + ]: + if public.is_error_path(): + return redirect('/error', 302) + if not request.path in ['/config']: + if session.get('password_expire', False): + return redirect('/modify_password', 302) + + +# Flask 请求结束勾子 +@app.teardown_request +def request_end(reques=None): + if request.method not in ['GET', 'POST']: return + if not request.path.startswith('/static/'): + public.write_request_log(reques) + #当路由为/plugin时,不检测g.return_message + # if not request.path.startswith('/plugin'): + if request.path.startswith('/sitetest'): + if 'return_message' in g: + if not g.return_message: + public.print_log("当前为网站路由,且未使用统一响应函数public.return_message") + return abort(403) + # return public.returnJson( + # False, 'Request failed!Request not using unified response!' + # ), json_header + else: + g.return_message = False + public.print_log("当前为网站路由,且已使用统一响应函数public.return_message") + if 'api_request' in g: + if g.api_request: + session.clear() + + +# Flask 404页面勾子 +@app.errorhandler(404) +def error_404(e): + if request.method not in ['GET', 'POST']: return + if not session.get('login', None): + g.auth_error = True + return public.error_not_login() + errorStr = ''' +404 Not Found + +

404 Not Found

+
nginx
+ +''' + headers = {"Content-Type": "text/html"} + return Response(errorStr, status=404, headers=headers) + + +# Flask 403页面勾子 +@app.errorhandler(403) +def error_403(e): + if request.method not in ['GET', 'POST']: return + if not session.get('login', None): + g.auth_error = True + return public.error_not_login() + errorStr = ''' +403 Forbidden + +

403 Forbidden

+
nginx
+ +''' + headers = {"Content-Type": "text/html"} + return Response(errorStr, status=403, headers=headers) + + +# Flask 500页面勾子 +@app.errorhandler(500) +def error_500(e): + if request.method not in ['GET', 'POST']: return + if not session.get('login', None): + g.auth_error = True + return public.error_not_login() + ss = '''404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +During handling of the above exception, another exception occurred:''' + error_info = public.get_error_info().strip().split(ss)[-1].strip() + _form = request.form.to_dict() + if 'username' in _form: _form['username'] = '******' + if 'password' in _form: _form['password'] = '******' + if 'phone' in _form: _form['phone'] = '******' + request_info = '''REQUEST_DATE: {request_date} + PAN_VERSION: {panel_version} + OS_VERSION: {os_version} + REMOTE_ADDR: {remote_addr} + REQUEST_URI: {method} {full_path} +REQUEST_FORM: {request_form} + USER_AGENT: {user_agent}'''.format( + request_date=public.getDate(), + remote_addr=public.GetClientIp(), + method=request.method, + full_path=public.xsssec(request.full_path), + request_form=public.xsssec(str(_form)), + user_agent=public.xsssec(request.headers.get('User-Agent')), + panel_version=public.version(), + os_version=public.get_os_version()) + + result = public.readFile( + public.get_panel_path() + + '/BTPanel/templates/default/panel_error.html').format( + error_title=error_info.split("\n")[-1], + request_info=request_info, + error_msg=error_info) + return Resp(result, 500) diff --git a/BTPanel/routes/v1.py b/BTPanel/routes/v1.py new file mode 100644 index 00000000..fb7eafc5 --- /dev/null +++ b/BTPanel/routes/v1.py @@ -0,0 +1,2311 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 V1路由 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +from BTPanel.app import * + +@app.route('/', methods=method_all) +def home(): + # 面板首页 + comReturn = comm.local() + if comReturn: return comReturn + data = {} + data[public.to_string([112, + 100])], data['pro_end'], data['ltd_end'] = get_pd() + data['siteCount'] = public.M('sites').count() + data['ftpCount'] = public.M('ftps').count() + data['databaseCount'] = public.M('databases').count() + data['lan'] = public.GetLan('index') + data['js_random'] = get_js_random() + return render_template('index.html', data=data) + + +@app.route('/xterm', methods=method_all) +def xterm(): + # 宝塔终端管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0]: + import system + data = system.system().GetConcifInfo() + return render_template('xterm.html', data=data) + import ssh_terminal + ssh_host_admin = ssh_terminal.ssh_host_admin() + defs = ('get_host_list', 'get_host_find', 'modify_host', 'create_host', + 'remove_host', 'set_sort', 'get_command_list', 'create_command', + 'get_command_find', 'modify_command', 'remove_command') + return publicObject(ssh_host_admin, defs, None) + + +@app.route('/modify_password', methods=method_get) +def modify_password(): + comReturn = comm.local() + if comReturn: return comReturn + # if not session.get('password_expire',False): return redirect ('/',302) + data = {} + g.title = public.get_msg_gettext( + 'The password has expired, please change it!') + return render_template('modify_password.html', data=data) + + +@app.route('/site', methods=method_all) +def site(pdata=None): + # 网站管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + # data = {} + import system + data = system.system().GetConcifInfo() + data['isSetup'] = True + data['lan'] = public.getLan('site') + data['js_random'] = get_js_random() + if os.path.exists(public.GetConfigValue('setup_path') + '/nginx') == False \ + and os.path.exists(public.GetConfigValue('setup_path') + '/apache') == False \ + and os.path.exists('/usr/local/lsws/bin/lswsctrl') == False: + data['isSetup'] = False + return render_template('site.html', data=data) + import panelSite + siteObject = panelSite.panelSite() + + defs = ( + 'get_auto_restart_rph', + 'remove_auto_restart_rph', + 'auto_restart_rph', + 'check_del_data', + 'upload_csv', + 'create_website_multiple', + 'del_redirect_multiple', + 'del_proxy_multiple', + 'delete_dir_auth_multiple', + 'delete_dir_bind_multiple', + 'delete_domain_multiple', + 'set_site_etime_multiple', + 'set_site_php_version_multiple', + 'delete_website_multiple', + 'set_site_status_multiple', + 'get_site_err_log', + 'get_site_domains', + 'GetRedirectFile', + 'SaveRedirectFile', + 'DeleteRedirect', + 'GetRedirectList', + 'CreateRedirect', + 'ModifyRedirect', + "set_error_redirect", + 'set_dir_auth', + 'delete_dir_auth', + 'get_dir_auth', + 'modify_dir_auth_pass', + 'reset_wp_db', + 'export_domains', + 'import_domains', + 'GetSiteLogs', + 'GetSiteDomains', + 'GetSecurity', + 'SetSecurity', + 'ProxyCache', + 'CloseToHttps', + 'HttpToHttps', + 'SetEdate', + 'SetRewriteTel', + 'GetCheckSafe', + 'CheckSafe', + 'GetDefaultSite', + 'SetDefaultSite', + 'CloseTomcat', + 'SetTomcat', + 'apacheAddPort', + 'AddSite', + 'GetPHPVersion', + 'SetPHPVersion', + 'DeleteSite', + 'AddDomain', + 'DelDomain', + 'GetDirBinding', + 'AddDirBinding', + 'GetDirRewrite', + 'DelDirBinding', + 'get_site_types', + 'add_site_type', + 'remove_site_type', + 'modify_site_type_name', + 'set_site_type', + 'UpdateRulelist', + 'SetSiteRunPath', + 'GetSiteRunPath', + 'SetPath', + 'SetIndex', + 'GetIndex', + 'GetDirUserINI', + 'SetDirUserINI', + 'GetRewriteList', + 'SetSSL', + 'SetSSLConf', + 'CreateLet', + 'CloseSSLConf', + 'GetSSL', + 'SiteStart', + 'SiteStop', + 'Set301Status', + 'Get301Status', + 'CloseLimitNet', + 'SetLimitNet', + 'GetLimitNet', + 'RemoveProxy', + 'GetProxyList', + 'GetProxyDetals', + 'CreateProxy', + 'ModifyProxy', + 'GetProxyFile', + 'SaveProxyFile', + 'ToBackup', + 'DelBackup', + 'GetSitePHPVersion', + 'logsOpen', + 'GetLogsStatus', + 'CloseHasPwd', + 'SetHasPwd', + 'GetHasPwd', + 'GetDnsApi', + 'SetDnsApi', + 'reset_wp_password', + 'is_update', + 'purge_all_cache', + 'set_fastcgi_cache', + 'update_wp', + 'get_wp_username', + 'get_language', + 'deploy_wp', + # 网站管理新增 + 'test_domains_api', + 'site_rname', + ) + return publicObject(siteObject, defs, None, pdata) + + +@app.route('/ftp', methods=method_all) +def ftp(pdata=None): + # FTP管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + FtpPort() + import system + data = system.system().GetConcifInfo() + data['isSetup'] = True + data['js_random'] = get_js_random() + if os.path.exists(public.GetConfigValue('setup_path') + + '/pure-ftpd') == False: + data['isSetup'] = False + data['lan'] = public.GetLan('ftp') + return render_template('ftp.html', data=data) + import ftp + ftpObject = ftp.ftp() + defs = ('AddUser', 'DeleteUser', 'SetUserPassword', 'SetStatus', 'setPort', + 'set_user_home', 'get_login_logs', 'get_action_logs', + 'set_ftp_logs') + return publicObject(ftpObject, defs, None, pdata) + + +@app.route('/database', methods=method_all) +def database(pdata=None): + # 数据库管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import ajax + from panelPlugin import panelPlugin + session['phpmyadminDir'] = False + if panelPlugin().get_phpmyadmin_stat(): + pmd = get_phpmyadmin_dir() + if pmd: + session['phpmyadminDir'] = 'http://' + public.GetHost( + ) + ':' + pmd[1] + '/' + pmd[0] + ajax.ajax().set_phpmyadmin_session() + import system + data = system.system().GetConcifInfo() + data['isSetup'] = os.path.exists( + public.GetConfigValue('setup_path') + '/mysql/bin') + data['mysql_root'] = public.M('config').where( + 'id=?', (1, )).getField('mysql_root') + data['lan'] = public.GetLan('database') + data['js_random'] = get_js_random() + return render_template('database.html', data=data) + import database + databaseObject = database.database() + defs = ('GetdataInfo', 'check_del_data', 'get_database_size', 'GetInfo', + 'ReTable', 'OpTable', 'AlTable', 'GetSlowLogs', 'GetRunStatus', + 'SetDbConf', 'GetDbStatus', 'BinLog', 'GetErrorLog', + 'GetMySQLInfo', 'SetDataDir', 'SetMySQLPort', 'AddCloudDatabase', + 'AddDatabase', 'DeleteDatabase', 'SetupPassword', + 'ResDatabasePassword', 'ToBackup', 'DelBackup', 'AddCloudServer', + 'GetCloudServer', 'RemoveCloudServer', 'ModifyCloudServer', + 'InputSql', 'SyncToDatabases', 'SyncGetDatabases', + 'GetDatabaseAccess', 'SetDatabaseAccess', 'get_mysql_user', + 'check_mysql_ssl_status', 'write_ssl_to_mysql', 'GetdataInfo') + return publicObject(databaseObject, defs, None, pdata) + + +@app.route('/acme', methods=method_all) +def acme(pdata=None): + # Let's 证书管理 + comReturn = comm.local() + if comReturn: return comReturn + import acme_v2 + acme_v2_object = acme_v2.acme_v2() + defs = ('get_orders', 'remove_order', 'get_order_find', 'revoke_order', + 'create_order', 'get_account_info', 'set_account_info', + 'update_zip', 'get_cert_init_api', 'get_auths', 'auth_domain', + 'check_auth_status', 'download_cert', 'apply_cert', 'renew_cert', + 'apply_cert_api', 'apply_dns_auth') + return publicObject(acme_v2_object, defs, None, pdata) + + +# import panelMessage +# message_object = panelMessage.panelMessage() +# @app.route('/message/', methods=method_all) +# def message(action=None): +# # 提示消息管理 +# comReturn = comm.local() +# if comReturn: return comReturn +# import panelMessage +# message_object = panelMessage.panelMessage() +# defs = ( +# 'get_messages', 'get_message_find', 'create_message', 'status_message', 'remove_message', 'get_messages_all') +# return publicObject(message_object, defs, action, None) + + +@app.route('/api', methods=method_all) +def api(pdata=None): + # APP使用的API接口管理 + comReturn = comm.local() + if comReturn: return comReturn + import panelApi + api_object = panelApi.panelApi() + defs = ('get_token', 'check_bind', 'get_bind_status', 'get_apps', + 'add_bind_app', 'remove_bind_app', 'set_token', 'get_tmp_token', + 'get_app_bind_status', 'login_for_app') + return publicObject(api_object, defs, None, pdata) + + +@app.route('/control', methods=method_all) +def control(pdata=None): + # 监控页面 + comReturn = comm.local() + if comReturn: return comReturn + import system + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('control') + data['js_random'] = get_js_random() + return render_template('control.html', data=data) + + +@app.route('/logs', methods=method_all) +def logs(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + data = {} + data['lan'] = public.GetLan('soft') + data['show_workorder'] = not os.path.exists('data/not_workorder.pl') + return render_template('logs.html', data=data) + + +@app.route('/firewall', methods=method_all) +def firewall(pdata=None): + # 安全页面 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import system + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + return render_template('firewall.html', data=data) + import firewalls + firewallObject = firewalls.firewalls() + defs = ('GetList', 'AddDropAddress', 'DelDropAddress', 'FirewallReload', + 'SetFirewallStatus', 'AddAcceptPort', 'DelAcceptPort', + 'SetSshStatus', 'SetPing', 'SetSshPort', 'GetSshInfo', + 'SetFirewallStatus') + return publicObject(firewallObject, defs, None, pdata) + + +@app.route('/ssh_security', methods=method_all) +def ssh_security(pdata=None): + # SSH安全 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata and not request.args.get( + 'action', '') in ['download_key']: + data = {} + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + return render_template('firewall.html', data=data) + import ssh_security + firewallObject = ssh_security.ssh_security() + is_csrf = True + if request.args.get('action', '') in ['download_key']: is_csrf = False + defs = ('san_ssh_security', 'set_password', 'set_sshkey', 'stop_key', + 'get_config', 'download_key', 'stop_password', 'get_key', + 'return_ip', 'add_return_ip', 'del_return_ip', 'start_jian', + 'stop_jian', 'get_jian', 'get_logs', 'set_root', 'stop_root', + 'start_auth_method', 'stop_auth_method', 'get_auth_method', + 'check_so_file', 'get_so_file', 'get_pin', 'set_login_send', + 'get_login_send', 'get_msg_push_list', 'clear_login_send') + return publicObject(firewallObject, defs, None, pdata, is_csrf) + + +@app.route('/monitor', methods=method_all) +def panel_monitor(pdata=None): + # 云控统计信息 + comReturn = comm.local() + if comReturn: return comReturn + import monitor + dataObject = monitor.Monitor() + defs = ('get_spider', 'get_exception', 'get_request_count_qps', + 'load_and_up_flow', 'get_request_count_by_hour') + return publicObject(dataObject, defs, None, pdata) + + +@app.route('/san', methods=method_all) +def san_baseline(pdata=None): + # 云控安全扫描 + comReturn = comm.local() + if comReturn: return comReturn + import san_baseline + dataObject = san_baseline.san_baseline() + defs = ('start', 'get_api_log', 'get_resut', 'get_ssh_errorlogin', + 'repair', 'repair_all') + return publicObject(dataObject, defs, None, pdata) + + +@app.route('/password', methods=method_all) +def panel_password(pdata=None): + # 云控密码管理 + comReturn = comm.local() + if comReturn: return comReturn + import password + dataObject = password.password() + defs = ('set_root_password', 'get_mysql_root', 'set_mysql_password', + 'set_panel_password', 'SetPassword', 'SetSshKey', 'StopKey', + 'GetConfig', 'StopPassword', 'GetKey', 'get_databses', + 'rem_mysql_pass', 'set_mysql_access', "get_panel_username") + return publicObject(dataObject, defs, None, pdata) + + +@app.route('/warning', methods=method_all) +def panel_warning(pdata=None): + # 首页安全警告 + comReturn = comm.local() + if comReturn: return comReturn + if public.get_csrf_html_token_key() in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + get = get_input() + ikey = 'warning_list' + import panelWarning + dataObject = panelWarning.panelWarning() + if get.action == 'get_list': + result = cache.get(ikey) + if not result or 'force' in get: + result = json.loads('{"ignore":[],"risk":[],"security":[]}') + try: + defs = ("get_list", ) + result = publicObject(dataObject, defs, None, pdata) + cache.set(ikey, result, 3600) + return result + except: + pass + return result + + defs = ('get_list', 'set_ignore', 'check_find', 'check_cve', + 'set_vuln_ignore', 'get_scan_bar', 'get_tmp_result', + 'kill_get_list') + + if get.action in ['set_ignore', 'check_find', 'set_vuln_ignore']: + cache.delete(ikey) + return publicObject(dataObject, defs, None, pdata) + + +@app.route('/bak', methods=method_all) +def backup_bak(pdata=None): + # 云控备份服务 + comReturn = comm.local() + if comReturn: return comReturn + import backup_bak + dataObject = backup_bak.backup_bak() + defs = ('get_sites', 'get_databases', 'backup_database', 'backup_site', + 'backup_path', 'get_database_progress', 'get_site_progress', + 'down', 'get_down_progress', 'download_path', 'backup_site_all', + 'get_all_site_progress', 'backup_date_all', + 'get_all_date_progress') + return publicObject(dataObject, defs, None, pdata) + + +@app.route('/abnormal', methods=method_all) +def abnormal(pdata=None): + # 云控系统统计 + comReturn = comm.local() + if comReturn: return comReturn + import abnormal + dataObject = abnormal.abnormal() + defs = ('mysql_server', 'mysql_cpu', 'mysql_count', 'php_server', + 'php_conn_max', 'php_cpu', 'CPU', 'Memory', 'disk', + 'not_root_user', 'start') + return publicObject(dataObject, defs, None, pdata) + + +@app.route('/project///', methods=method_all) +def project(mod_name, def_name, stype=None): + comReturn = comm.local() + if comReturn: return comReturn + from panelProjectController import ProjectController + project_obj = ProjectController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + get.stype = stype + if stype == "html": + return project_obj.model(get) + return publicObject(project_obj, defs, None, get) + + +@app.route('/msg//', methods=method_all) +def msgcontroller(mod_name, def_name): + comReturn = comm.local() + if comReturn: return comReturn + from MsgController import MsgController + project_obj = MsgController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + return publicObject(project_obj, defs, None, get) + + +@app.route('/docker', methods=method_all) +def docker(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0]: + import system + data = system.system().GetConcifInfo() + data['js_random'] = get_js_random() + data['lan'] = public.GetLan('files') + return render_template('docker.html', data=data) + + +@app.route('/dbmodel//', methods=method_all) +def dbmodel(mod_name, def_name): + comReturn = comm.local() + if comReturn: return comReturn + from panelDatabaseController import DatabaseController + database_obj = DatabaseController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + + return publicObject(database_obj, defs, None, get) + + +@app.route('/files', methods=method_all) +def files(pdata=None): + # 文件管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not request.args.get( + 'path') and not pdata: + import system + data = system.system().GetConcifInfo() + data['recycle_bin'] = os.path.exists('data/recycle_bin.pl') + data['lan'] = public.GetLan('files') + data['js_random'] = get_js_random() + return render_template('files.html', data=data) + import files + filesObject = files.files() + defs = ('files_search', 'files_replace', 'get_replace_logs', + 'get_images_resize', 'add_files_rsync', 'get_file_attribute', + 'get_file_hash', 'CreateLink', 'get_progress', 'restore_website', + 'fix_permissions', 'get_all_back', 'restore_path_permissions', + 'del_path_premissions', 'get_path_premissions', + 'back_path_permissions', 'upload_file_exists', 'CheckExistsFiles', + 'GetExecLog', 'GetSearch', 'ExecShell', 'GetExecShellMsg', + 'exec_git', 'exec_composer', 'create_download_url', 'UploadFile', + 'GetDir', 'CreateFile', 'CreateDir', 'DeleteDir', 'DeleteFile', + 'get_download_url_list', 'remove_download_url', + 'modify_download_url', 'CopyFile', 'CopyDir', 'MvFile', + 'GetFileBody', 'SaveFileBody', 'Zip', 'UnZip', + 'get_download_url_find', 'set_file_ps', 'SearchFiles', 'upload', + 'read_history', 're_history', 'auto_save_temp', + 'get_auto_save_body', 'get_videos', 'GetFileAccess', + 'SetFileAccess', 'GetDirSize', 'SetBatchData', 'BatchPaste', + 'install_rar', 'get_path_size', 'DownloadFile', 'GetTaskSpeed', + 'CloseLogs', 'InstallSoft', 'UninstallSoft', 'SaveTmpFile', + 'get_composer_version', 'exec_composer', 'update_composer', + 'GetTmpFile', 'del_files_store', 'add_files_store', + 'get_files_store', 'del_files_store_types', + 'add_files_store_types', 'exec_git', 'RemoveTask', 'ActionTask', + 'Re_Recycle_bin', 'Get_Recycle_bin', 'Del_Recycle_bin', + 'Close_Recycle_bin', 'Recycle_bin', 'file_webshell_check', + 'dir_webshell_check', 'files_search', 'files_replace', + 'get_replace_logs') + return publicObject(filesObject, defs, None, pdata) + + +@app.route('/crontab', methods=method_all) +def crontab(pdata=None): + # 计划任务 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import system + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('crontab') + data['js_random'] = get_js_random() + return render_template('crontab.html', data=data) + import crontab + crontabObject = crontab.crontab() + defs = ('GetCrontab', 'AddCrontab', 'GetDataList', 'GetLogs', 'DelLogs', + 'DelCrontab', 'StartTask', 'set_cron_status', 'get_crond_find', + 'modify_crond', 'get_backup_list') + return publicObject(crontabObject, defs, None, pdata) + + +@app.route('/soft', methods=method_all) +def soft(pdata=None): + # 软件商店页面 + comReturn = comm.local() + if comReturn: return comReturn + import system + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('soft') + data['js_random'] = get_js_random() + return render_template('soft.html', data=data) + + +@app.route('/config', methods=method_all) +def config(pdata=None): + # 面板设置页面 + comReturn = comm.local() + if comReturn: return comReturn + + if request.method == method_get[0] and not pdata: + import system, wxapp, config + c_obj = config.config() + data = system.system().GetConcifInfo() + data['lan'] = public.GetLan('config') + try: + data['wx'] = wxapp.wxapp().get_user_info(None)['msg'] + except: + data['wx'] = 'INIT_WX_NOT_BIND' + data['api'] = '' + data['ipv6'] = '' + sess_out_path = 'data/session_timeout.pl' + if not os.path.exists(sess_out_path): + public.writeFile(sess_out_path, '86400') + s_time_tmp = public.readFile(sess_out_path) + if not s_time_tmp: s_time_tmp = '0' + data['session_timeout'] = int(s_time_tmp) + if c_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' + if c_obj.get_token(None)['open']: data['api'] = 'checked' + data['basic_auth'] = c_obj.get_basic_auth_stat(None) + data['status_code'] = c_obj.get_not_auth_status() + data['basic_auth']['value'] = public.getMsg('CLOSED') + if data['basic_auth']['open']: + data['basic_auth']['value'] = public.getMsg('OPENED') + data['debug'] = '' + data['js_random'] = get_js_random() + if app.config['DEBUG']: data['debug'] = 'checked' + data['is_local'] = '' + if public.is_local(): data['is_local'] = 'checked' + data['public_key'] = public.get_rsa_public_key().replace("\n", "") + return render_template('config.html', data=data) + import config + defs = ( + 'send_by_telegram', + 'set_empty', + 'set_backup_notification', + 'get_panel_ssl_status', + 'set_file_deny', + 'del_file_deny', + 'get_file_deny', + 'set_improvement', + 'get_httpd_access_log_format_parameter', + 'set_httpd_format_log_to_website', + 'get_httpd_access_log_format', + 'del_httpd_access_log_format', + 'add_httpd_access_log_format', + 'get_nginx_access_log_format_parameter', + 'set_format_log_to_website', + 'get_nginx_access_log_format', + 'del_nginx_access_log_format', + 'set_click_logs', + 'get_node_config', + 'add_nginx_access_log_format', + 'get_ols_private_cache_status', + 'get_ols_value', + 'set_ols_value', + 'set_node_config', + 'get_ols_private_cache', + 'get_ols_static_cache', + 'set_ols_static_cache', + 'switch_ols_private_cache', + 'set_ols_private_cache', + 'set_coll_open', + 'get_qrcode_data', + 'check_two_step', + 'set_two_step_auth', + 'create_user', + 'remove_user', + 'modify_user', + 'get_key', + 'get_php_session_path', + 'set_php_session_path', + 'get_cert_source', + 'get_users', + 'set_request_iptype', + 'set_local', + 'set_debug', + 'get_panel_error_logs', + 'clean_panel_error_logs', + 'get_menu_list', + 'set_hide_menu_list', + 'get_basic_auth_stat', + 'set_basic_auth', + 'get_cli_php_version', + 'get_tmp_token', + 'get_temp_login', + 'set_temp_login', + 'remove_temp_login', + 'clear_temp_login', + 'get_temp_login_logs', + 'set_cli_php_version', + 'DelOldSession', + 'GetSessionCount', + 'SetSessionConf', + 'set_not_auth_status', + 'GetSessionConf', + 'get_ipv6_listen', + 'set_ipv6_status', + 'GetApacheValue', + 'SetApacheValue', + 'install_msg_module', + 'GetNginxValue', + 'SetNginxValue', + 'get_token', + 'set_token', + 'set_admin_path', + 'is_pro', + 'set_msg_config', + 'get_php_config', + 'get_config', + 'SavePanelSSL', + 'GetPanelSSL', + 'GetPHPConf', + 'SetPHPConf', + 'uninstall_msg_module', + 'GetPanelList', + 'AddPanelInfo', + 'SetPanelInfo', + 'DelPanelInfo', + 'ClickPanelInfo', + 'SetPanelSSL', + 'get_msg_configs', + 'SetTemplates', + 'Set502', + 'setPassword', + 'setUsername', + 'setPanel', + 'setPathInfo', + 'setPHPMaxSize', + 'get_msg_fun', + 'getFpmConfig', + 'setFpmConfig', + 'setPHPMaxTime', + 'syncDate', + 'setPHPDisable', + 'SetControl', + 'get_settings2', + 'del_tg_info', + 'set_tg_bot', + 'ClosePanel', + 'AutoUpdatePanel', + 'SetPanelLock', + 'return_mail_list', + 'del_mail_list', + 'add_mail_address', + 'user_mail_send', + 'get_user_mail', + 'set_dingding', + 'get_dingding', + 'get_settings', + 'user_stmp_mail_send', + 'user_dingding_send', + 'get_login_send', + 'set_login_send', + 'clear_login_send', + 'get_login_log', + 'login_ipwhite', + 'set_ssl_verify', + 'get_ssl_verify', + 'get_password_config', + 'set_password_expire', + 'set_password_safe', + 'get_module_template', + # 新增nps评分 + 'write_nps_new', + 'get_nps_new', + "check_nps") + return publicObject(config.config(), defs, None, pdata) + + +@app.route('/ajax', methods=method_all) +def ajax(pdata=None): + # 面板系统服务状态接口 + comReturn = comm.local() + if comReturn: return comReturn + import ajax + ajaxObject = ajax.ajax() + defs = ('get_lines', 'php_info', 'change_phpmyadmin_ssl_port', + 'set_phpmyadmin_ssl', 'get_phpmyadmin_ssl', 'get_pd', + 'check_user_auth', 'to_not_beta', 'get_beta_logs', 'apple_beta', + 'GetApacheStatus', 'GetCloudHtml', 'get_pay_type', + 'get_load_average', 'GetOpeLogs', 'GetFpmLogs', 'GetFpmSlowLogs', + 'SetMemcachedCache', 'GetMemcachedStatus', 'GetRedisStatus', + 'GetWarning', 'SetWarning', 'CheckLogin', 'GetSpeed', 'GetAd', + 'phpSort', 'ToPunycode', 'GetBetaStatus', 'SetBeta', + 'setPHPMyAdmin', 'delClose', 'KillProcess', 'GetPHPInfo', + 'GetQiniuFileList', 'get_process_tops', 'get_process_cpu_high', + 'UninstallLib', 'InstallLib', 'SetQiniuAS', 'GetQiniuAS', + 'GetLibList', 'GetProcessList', 'GetNetWorkList', 'GetNginxStatus', + 'GetPHPStatus', 'GetTaskCount', 'GetSoftList', 'GetNetWorkIo', + 'GetDiskIo', 'GetCpuIo', 'CheckInstalled', 'UpdatePanel', + 'GetInstalled', 'GetPHPConfig', 'SetPHPConfig', 'log_analysis', + 'speed_log', 'get_result', 'get_detailed', 'ignore_version') + + return publicObject(ajaxObject, defs, None, pdata) + + +@app.route('/system', methods=method_all) +def system(pdata=None): + # 面板系统状态接口 + comReturn = comm.local() + if comReturn: return comReturn + import system + sysObject = system.system() + defs = ('get_io_info', 'UpdatePro', 'GetAllInfo', 'GetNetWorkApi', + 'GetLoadAverage', 'ClearSystem', 'GetNetWorkOld', 'GetNetWork', + 'GetDiskInfo', 'GetCpuInfo', 'GetBootTime', 'GetSystemVersion', + 'GetMemInfo', 'GetSystemTotal', 'GetConcifInfo', 'ServiceAdmin', + 'ReWeb', 'RestartServer', 'ReMemory', 'RepPanel') + return publicObject(sysObject, defs, None, pdata) + + +@app.route('/deployment', methods=method_all) +def deployment(pdata=None): + # 一键部署接口 + comReturn = comm.local() + if comReturn: return comReturn + import plugin_deployment + sysObject = plugin_deployment.plugin_deployment() + defs = ('GetList', 'AddPackage', 'DelPackage', 'SetupPackage', 'GetSpeed', + 'GetPackageOther') + return publicObject(sysObject, defs, None, pdata) + + +@app.route('/data', methods=method_all) +@app.route('/panel_data', methods=method_all) +def panel_data(pdata=None): + # 从数据库获取数据接口 + comReturn = comm.local() + if comReturn: return comReturn + import data + dataObject = data.data() + defs = ('setPs', 'getData', 'getFind', 'getKey') + return publicObject(dataObject, defs, None, pdata) + + +@app.route('/ssl', methods=method_all) +def ssl(pdata=None): + # 商业SSL证书申请接口 + comReturn = comm.local() + if comReturn: return comReturn + import panelSSL + toObject = panelSSL.panelSSL() + defs = ( + 'check_url_txt', + 'RemoveCert', + 'renew_lets_ssl', + 'SetCertToSite', + 'GetCertList', + 'SaveCert', + 'GetCert', + 'GetCertName', + 'again_verify', + 'DelToken', + 'GetToken', + 'GetUserInfo', + 'GetOrderList', + 'GetDVSSL', + 'Completed', + 'SyncOrder', + 'download_cert', + 'set_cert', + 'cancel_cert_order', + 'get_order_list', + 'get_order_find', + 'apply_order_pay', + 'get_pay_status', + 'apply_order', + 'get_verify_info', + 'get_verify_result', + 'get_product_list', + 'set_verify_info', + 'GetSSLInfo', + 'downloadCRT', + 'GetSSLProduct', + 'Renew_SSL', + 'Get_Renew_SSL', + # 新增 购买证书对接接口 + 'get_product_list_v2', + 'apply_cert_order_pay', + 'get_cert_admin', + 'apply_order_ca', + 'apply_cert_install_pay', + + # 'pay_test' + ) + get = get_input() + + if get.action == 'download_cert': + from io import BytesIO + import base64 + result = toObject.download_cert(get) + # public.print_log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@1111111111111111 result: {}".format(result)) + # {'success': False, 'res': '[code: 0] no data [file: /www/wwwroot/192.168.1.139/app/Api/Cert/controllers/Cert.php] [line: 955]', 'nonce': 1706498844} + + fp = BytesIO(base64.b64decode(result['res']['data'])) + return send_file(fp, + download_name=result['res']['filename'], + as_attachment=True, + mimetype='application/zip') + result = publicObject(toObject, defs, get.action, get) + return result + + +@app.route('/task', methods=method_all) +def task(pdata=None): + # 后台任务接口 + comReturn = comm.local() + if comReturn: return comReturn + import panelTask + toObject = panelTask.bt_task() + defs = ('get_task_lists', 'remove_task', 'get_task_find', + "get_task_log_by_id") + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route('/plugin', methods=method_all) +def plugin(pdata=None): + # 插件系统接口 + comReturn = comm.local() + if comReturn: return comReturn + import panelPlugin + pluginObject = panelPlugin.panelPlugin() + defs = ('get_usually_plugin', 'check_install_limit', 'set_score', + 'get_score', 'update_zip', 'input_zip', 'export_zip', 'add_index', + 'remove_index', 'sort_index', 'install_plugin', 'uninstall_plugin', + 'get_soft_find', 'get_index_list', 'get_soft_list', + 'get_cloud_list', 'check_deps', 'flush_cache', 'GetCloudWarning', + 'install', 'unInstall', 'getPluginList', 'getPluginInfo', + 'get_make_args', 'add_make_args', 'getPluginStatus', + 'setPluginStatus', 'a', 'getCloudPlugin', 'getConfigHtml', + 'savePluginSort', 'del_make_args', 'set_make_args') + return publicObject(pluginObject, defs, None, pdata) + + +@app.route('/wxapp', methods=method_all) +@app.route('/panel_wxapp', methods=method_all) +def panel_wxapp(pdata=None): + # 微信小程序绑定接口 + comReturn = comm.local() + if comReturn: return comReturn + import wxapp + toObject = wxapp.wxapp() + defs = ('blind', 'get_safe_log', 'blind_result', 'get_user_info', + 'blind_del', 'blind_qrcode') + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route('/auth', methods=method_all) +def auth(pdata=None): + # 面板认证接口 + comReturn = comm.local() + if comReturn: return comReturn + import panelAuth + toObject = panelAuth.panelAuth() + defs = ('free_trial', 'renew_product_auth', 'auth_activate', + 'get_product_auth', 'get_stripe_session_id', + 'get_re_order_status_plugin', 'create_plugin_other_order', + 'get_order_stat', 'get_voucher_plugin', + 'create_order_voucher_plugin', 'get_product_discount_by', + 'get_re_order_status', 'create_order_voucher', 'create_order', + 'get_order_status', 'get_voucher', 'flush_pay_status', + 'create_serverid', 'check_serverid', 'get_plugin_list', + 'check_plugin', 'get_buy_code', 'check_pay_status', + 'get_renew_code', 'check_renew_code', 'get_business_plugin', + 'get_ad_list', 'check_plugin_end', 'get_plugin_price', + 'get_plugin_remarks', 'get_paypal_session_id', + 'check_paypal_status') + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route('/download', methods=method_get) +def download(): + # 文件下载接口 + comReturn = comm.local() + if comReturn: return comReturn + filename = request.args.get('filename') + if filename.find('|') != -1: + filename = filename.split('|')[0] # 改为获取本地备份 + if not filename: + return public.ReturnJson(False, "INIT_ARGS_ERR"), json_header + # if filename in ['alioss','qiniu','upyun','txcos','ftp','msonedrive','gcloud_storage', 'gdrive', 'aws_s3']: return panel_cloud() + if not os.path.exists(filename): + return public.ReturnJson(False, "FILE_NOT_EXISTS"), json_header + + if request.args.get('play') == 'true': + import panelVideo + start, end = panelVideo.get_range(request) + g.return_message = True + return panelVideo.partial_response(filename, start, end) + else: + mimetype = "application/octet-stream" + extName = filename.split('.')[-1] + if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None + public.WriteLog("TYPE_FILE", 'FILE_DOWNLOAD', + (filename, public.GetClientIp())) + g.return_message = True + return send_file(filename, + mimetype=mimetype, + as_attachment=True, + etag=True, + conditional=True, + download_name=os.path.basename(filename), + max_age=0) + + +@app.route('/cloud', methods=method_all) +def panel_cloud(is_csrf=True): + # 从对像存储下载备份文件接口 + comReturn = comm.local() + if comReturn: return comReturn + if is_csrf: + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + get = get_input() + _filename = get.filename + plugin_name = "" + if _filename.find('|') != -1: + plugin_name = get.filename.split('|')[1] + else: + plugin_name = get.filename + + if not os.path.exists('plugin/' + plugin_name + '/' + plugin_name + + '_main.py'): + return public.returnJson( + False, 'The specified plugin does not exist!'), json_header + public.package_path_append('plugin/' + plugin_name) + plugin_main = __import__(plugin_name + '_main') + public.mod_reload(plugin_main) + tmp = eval("plugin_main.%s_main()" % plugin_name) + if not hasattr(tmp, 'download_file'): + return public.returnJson( + False, + 'Specified plugin has no file download function!'), json_header + download_url = tmp.download_file(get.name) + if plugin_name == 'ftp': + if download_url.find("ftp") != 0: + download_url = "ftp://" + download_url + else: + if download_url.find('http') != 0: + download_url = 'http://' + download_url + + if "toserver" in get and get.toserver == "true": + download_dir = "/tmp/" + if "download_dir" in get: + download_dir = get.download_dir + local_file = os.path.join(download_dir, get.name) + + input_from_local = False + if "input_from_local" in get: + input_from_local = True if get.input_from_local == "true" else False + + if input_from_local: + if os.path.isfile(local_file): + return { + "status": True, + "msg": + "The file already exists and will be restored locally.", + "task_id": -1, + "local_file": local_file + } + from panelTask import bt_task + task_obj = bt_task() + task_id = task_obj.create_task('Download file', 1, download_url, + local_file) + return { + "status": True, + "msg": "The download task was created successfully", + "local_file": local_file, + "task_id": task_id + } + + return redirect(download_url) + + +@app.route('/btwaf_error', methods=method_get) +def btwaf_error(): + # 图标 + comReturn = comm.local() + if comReturn: return comReturn + get = get_input() + p_path = os.path.join('/www/server/panel/plugin/', "btwaf") + if not os.path.exists(p_path): + if get.name == 'btwaf' and get.fun == 'index': + return render_template('error3.html', data={}) + return render_template('error3.html', data={}) + + +@app.route('/favicon.ico', methods=method_get) +def send_favicon(): + # 图标 + comReturn = comm.local() + if comReturn: return abort(404) + s_file = '/www/server/panel/BTPanel/static/favicon.ico' + if not os.path.exists(s_file): return abort(404) + return send_file(s_file, conditional=True, etag=True) + + +@app.route('/rspamd', defaults={'path': ''}, methods=method_all) +@app.route('/rspamd/', methods=method_all) +def proxy_rspamd_requests(path): + comReturn = comm.local() + if comReturn: return comReturn + param = str(request.url).split('?') + param = "" if len(param) < 2 else param[-1] + import requests + headers = {} + for h in request.headers.keys(): + headers[h] = request.headers[h] + if request.method == "GET": + if re.search(r"\.(js|css)$", path): + return send_file('/usr/share/rspamd/www/rspamd/' + path, + conditional=True, + etag=True) + if path == "/": + return send_file('/usr/share/rspamd/www/rspamd/', + conditional=True, + etag=True) + url = "http://127.0.0.1:11334/rspamd/" + path + "?" + param + for i in [ + 'stat', 'auth', 'neighbours', 'list_extractors', + 'list_transforms', 'graph', 'maps', 'actions', 'symbols', + 'history', 'errors', 'check_selector', 'saveactions', + 'savesymbols', 'getmap' + ]: + if i in path: + url = "http://127.0.0.1:11334/" + path + "?" + param + req = requests.get(url, headers=headers, stream=True) + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) + else: + url = "http://127.0.0.1:11334/" + path + for i in request.form.keys(): + data = '{}='.format(i) + # public.writeFile('/tmp/2',data+"\n","a+") + req = requests.post(url, data=data, headers=headers, stream=True) + + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) + + +@app.route('/tips', methods=method_get) +def tips(): + # 提示页面 + comReturn = comm.local() + if comReturn: return abort(404) + get = get_input() + if len(get.get_items().keys()) > 1: return abort(404) + return render_template('tips.html') + +@app.route('/login', methods=method_all) +@app.route(route_path, methods=method_all) +@app.route(route_path + '/', methods=method_all) +def login(): + # 面板登录接口 + if os.path.exists('install.pl'): return redirect('/install') + global admin_check_auth, admin_path, route_path + is_auth_path = False + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + is_auth_path = True + # 登录输入验证 + if request.method == method_post[0]: + if is_auth_path: + g.auth_error = True + return public.error_not_login(None) + v_list = ['username', 'password', 'code', 'vcode', 'cdn_url'] + for v in v_list: + if v in ['username', 'password']: continue + pv = request.form.get(v, '').strip() + if v == 'cdn_url': + if len(pv) > 32: + return public.return_msg_gettext( + False, 'Wrong parameter length!'), json_header + if not re.match(r"^[\w\.-]+$", pv): + public.return_msg_gettext( + False, 'Wrong parameter format!'), json_header + continue + + if not pv: continue + p_len = 32 + if v == 'code': p_len = 4 + if v == 'vcode': p_len = 6 + if len(pv) != p_len: + if v == 'code': + return public.returnJson( + False, 'Verification code length error!'), json_header + return public.returnJson( + False, 'Wrong parameter length!'), json_header + if not re.match(r"^\w+$", pv): + return public.returnJson( + False, 'Wrong parameter format!'), json_header + + for n in request.form.keys(): + if not n in v_list: + return public.returnJson( + False, + 'There can be no extra parameters in the login parameters' + ), json_header + + get = get_input() + import userlogin + if hasattr(get, 'tmp_token'): + result = userlogin.userlogin().request_tmp(get) + return is_login(result) + # 过滤爬虫 + if public.is_spider(): return abort(404) + if hasattr(get, 'dologin'): + login_path = '/login' + if not 'login' in session: return redirect(login_path) + if os.path.exists(admin_path_file): login_path = route_path + if session['login'] != False: + session['login'] = False + cache.set('dologin', True) + public.write_log_gettext( + 'Logout', 'Client: {}, has manually exited the panel', + (public.GetClientIp() + ":" + + str(request.environ.get('REMOTE_PORT')), )) + if 'tmp_login_expire' in session: + s_file = 'data/session/{}'.format(session['tmp_login_id']) + if os.path.exists(s_file): + os.remove(s_file) + token_key = public.get_csrf_html_token_key() + if token_key in session: + del (session[token_key]) + session.clear() + sess_file = 'data/sess_files/' + public.get_sess_key() + if os.path.exists(sess_file): + try: + os.remove(sess_file) + except: + pass + sess_tmp_file = public.get_full_session_file() + if os.path.exists(sess_tmp_file): os.remove(sess_tmp_file) + g.dologin = True + return redirect(public.get_admin_path()) + + if is_auth_path: + if route_path != request.path and route_path + '/' != request.path: + referer = request.headers.get('Referer', 'err') + referer_tmp = referer.split('/') + referer_path = referer_tmp[-1] + if referer_path == '': + referer_path = referer_tmp[-2] + if route_path != '/' + referer_path: + g.auth_error = True + # return render_template('autherr.html') + return public.error_not_login(None) + + session['admin_auth'] = True + comReturn = common.panelSetup().init() + if comReturn: return comReturn + + if request.method == method_post[0]: + result = userlogin.userlogin().request_post(get) + return is_login(result) + + if request.method == method_get[0]: + result = userlogin.userlogin().request_get(get) + if result: + return result + data = {} + data['lan'] = public.GetLan('login') + data['hosts'] = '[]' + hosts_file = 'plugin/static_cdn/hosts.json' + if os.path.exists(hosts_file): + data['hosts'] = public.get_cdn_hosts() + if type(data['hosts']) == dict: + data['hosts'] = '[]' + else: + data['hosts'] = json.dumps(data['hosts']) + data['app_login'] = os.path.exists('data/app_login.pl') + public.cache_set( + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp()), 'check', 360) + + # 生成登录token + last_key = 'last_login_token' + # ----------- + last_time_key = 'last_login_token_time' + s_time = int(time.time()) + if last_key in session and last_time_key in session: + # 10秒内不重复生成token + if s_time - session[last_time_key] > 10: + session[last_key] = public.GetRandomString(32) + session[last_time_key] = s_time + else: + session[last_key] = public.GetRandomString(32) + session[last_time_key] = s_time + + data[last_key] = session[last_key] + data['public_key'] = public.get_rsa_public_key() + return render_template('login.html', data=data) + # ----------- + + # rsa_key = 'public_key' + # session[last_key] = public.GetRandomString(32) + # data[last_key] = session[last_key] + # data[rsa_key] = public.get_rsa_public_key().replace("\n", "") + # return render_template('login.html', data=data) + + +@app.route('/close', methods=method_get) +def close(): + # 面板已关闭页面 + if not os.path.exists('data/close.pl'): return redirect('/') + data = {} + data['lan'] = public.getLan('close') + return render_template('close.html', data=data) + + +@app.route('/get_app_bind_status', methods=method_all) +def get_app_bind_status(pdata=None): + # APP绑定状态查询 + if not public.check_app('app_bind'): return abort(404) + get = get_input() + if len(get.get_items().keys()) > 2: return 'There are meaningless parameters!' + v_list = ['bind_token', 'data'] + for n in get.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + import panelApi + api_object = panelApi.panelApi() + return json.dumps(api_object.get_app_bind_status(get_input())), json_header + + +@app.route('/check_bind', methods=method_all) +def check_bind(pdata=None): + # APP绑定查询 + if not public.check_app('app_bind'): return abort(404) + get = get_input() + if len(get.get_items().keys()) > 4: return 'There are meaningless parameters!' + v_list = ['bind_token', 'client_brand', 'client_model', 'data'] + for n in get.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + import panelApi + api_object = panelApi.panelApi() + return json.dumps(api_object.check_bind(get_input())), json_header + + +@app.route('/code', methods=method_get) +def code(): + if not 'code' in session: return '' + if not session['code']: return '' + # 获取图片验证码 + try: + import vilidate + except: + public.ExecShell("btpip install Pillow -I") + return "Pillow not install!" + vie = vilidate.vieCode() + codeImage = vie.GetCodeImage(80, 4) + if sys.version_info[0] == 2: + try: + from cStringIO import StringIO + except: + from StringIO import StringIO + out = StringIO() + else: + from io import BytesIO + out = BytesIO() + codeImage[0].save(out, "png") + cache.set("codeStr", public.md5("".join(codeImage[1]).lower()), 180) + cache.set("codeOut", 1, 0.1) + out.seek(0) + return send_file(out, mimetype='image/png', max_age=0) + + +@app.route('/down/', methods=method_all) +def down(token=None, fname=None): + # 文件分享对外接口 + try: + if public.M('download_token').count() == 0: return abort(404) + fname = request.args.get('fname') + if fname: + if (len(fname) > 256): return abort(404) + if fname: fname = fname.strip('/') + if not token: return abort(404) + if len(token) > 48: return abort(404) + char_list = [ + '\\', '/', ':', '*', '?', '"', '<', '>', '|', ';', '&', '`' + ] + for char in char_list: + if char in token: return abort(404) + if not request.args.get('play') in ['true', None, '']: + return abort(404) + args = get_input() + v_list = ['fname', 'play', 'file_password', 'data'] + for n in args.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + if not re.match(r"^[\w\.]+$", token): return abort(404) + find = public.M('download_token').where('token=?', (token, )).find() + + if not find: return abort(404) + if time.time() > int(find['expire']): return abort(404) + + if not os.path.exists(find['filename']): return abort(404) + if find['password'] and not token in session: + if 'file_password' in args: + if not re.match(r"^\w+$", args.file_password): + return public.ReturnJson(False, + 'Wrong password!'), json_header + if re.match(r"^\d+$", args.file_password): + args.file_password = str(int(args.file_password)) + args.file_password += ".0" + if args.file_password != str(find['password']): + return public.ReturnJson(False, + 'Wrong password!'), json_header + session[token] = 1 + session['down'] = True + else: + pdata = { + "to_path": "", + "src_path": find['filename'], + "password": True, + "filename": find['filename'].split('/')[-1], + "ps": find['ps'], + "total": find['total'], + "token": find['token'], + "expire": public.format_date(times=find['expire']) + } + session['down'] = True + return render_template('down.html', data=pdata) + + if not find['password']: + session['down'] = True + session[token] = 1 + + if session[token] != 1: + return abort(404) + + filename = find['filename'] + if fname: + filename = os.path.join(filename, fname) + if not public.path_safe_check(fname, False): return abort(404) + if os.path.isdir(filename): + return get_dir_down(filename, token, find) + else: + if os.path.isdir(filename): + return get_dir_down(filename, token, find) + + if request.args.get('play') == 'true': + import panelVideo + start, end = panelVideo.get_range(request) + return panelVideo.partial_response(filename, start, end) + else: + mimetype = "application/octet-stream" + extName = filename.split('.')[-1] + if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None + b_name = os.path.basename(filename) + return send_file(filename, + mimetype=mimetype, + as_attachment=True, + download_name=b_name, + max_age=0) + except: + return abort(404) + + +@app.route('/database/mongodb/', methods=method_all) +@app.route('/database/pgsql/', methods=method_all) +@app.route('/database/redis/', methods=method_all) +@app.route('/database/sqlite/', methods=method_all) +@app.route('/database/sqlserver/', methods=method_all) +def databaseModel(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 4: return + comReturn = comm.local() + if comReturn: return comReturn + from panelDatabaseController import DatabaseController + project_obj = DatabaseController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = path_split[2] + get.def_name = def_name + + return publicObject(project_obj, defs, None, get) + + +# 系统安全模型页面 +@app.route('/safe/firewall/', methods=method_all) +@app.route('/safe/freeip/', methods=method_all) +@app.route('/safe/ips/', methods=method_all) +@app.route('/safe/security/', methods=method_all) +@app.route('/safe/ssh/', methods=method_all) +@app.route('/safe/syslog/', methods=method_all) +def safeModel(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 4: return + comReturn = comm.local() + if comReturn: return comReturn + from panelSafeController import SafeController + project_obj = SafeController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = path_split[2] + get.def_name = def_name + # if get.mod_name.startswith("v2_"): + # get.mod_name = 'v2_' + mod_name + # return publicObject(project_obj, defs, None, get) + + return publicObject(project_obj, defs, None, get) + + +# 通用模型路由 +@app.route('///', methods=method_all) +def allModule(index, mod_name, def_name): + comReturn = comm.local() + if comReturn: return comReturn + p_path = public.get_plugin_path() + '/' + index + if os.path.exists(p_path): + return panel_other(index, mod_name, def_name) + + from panelController import Controller + controller_obj = Controller() + defs = ('model', ) + get = get_input() + get.model_index = index + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + return publicObject(controller_obj, defs, None, get) + + +@app.route('/public', methods=method_all) +def panel_public(): + get = get_input() + if len("{}".format(get.get_items())) > 1024 * 32: + return 'ERROR' + + # 获取ping测试 + if 'get_ping' in get: + try: + import panelPing + p = panelPing.Test() + get = p.check(get) + if not get: return 'ERROR' + result = getattr(p, get['act'])(get) + result_type = type(result) + if str(result_type).find('Response') != -1: return result + return public.getJson(result), json_header + except: + return abort(404) + + if public.cache_get( + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp())) != 'check': + return abort(404) + global admin_check_auth, admin_path, route_path, admin_path_file + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + return abort(404) + v_list = ['fun', 'name', 'filename', 'data', 'secret_key'] + for n in get.get_items().keys(): + if not n in v_list: + return abort(404) + + get.client_ip = public.GetClientIp() + num_key = get.client_ip + '_wxapp' + if not public.get_error_num(num_key, 10): + return public.return_msg_gettext( + False, + '10 consecutive authentication failures are prohibited for 1 hour') + if not hasattr(get, 'name'): get.name = '' + if not hasattr(get, 'fun'): return abort(404) + if not public.path_safe_check("%s/%s" % (get.name, get.fun)): + return abort(404) + if get.fun in ['login_qrcode', 'is_scan_ok', 'set_login']: + # 检查是否验证过安全入口 + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + return abort(404) + # 验证是否绑定了设备 + if not public.check_app('app'): + return public.return_msg_gettext(False, 'Unbound user') + import wxapp + pluwx = wxapp.wxapp() + checks = pluwx._check(get) + if type(checks) != bool or not checks: + public.set_error_num(num_key) + return public.getJson(checks), json_header + data = public.getJson(eval('pluwx.' + get.fun + '(get)')) + return data, json_header + else: + return abort(404) + + +@app.route('//', methods=method_all) +@app.route('///', methods=method_all) +def panel_other(name=None, fun=None, stype=None): + # 插件接口 + if public.is_error_path(): + return redirect('/error', 302) + if not name: return abort(404) + if not re.match(r"^[\w\-]+$", name): return abort(404) + if fun and not re.match(r"^[\w\-\.]+$", fun): return abort(404) + if name != "mail_sys" or fun != "send_mail_http.json": + comReturn = comm.local() + if comReturn: return comReturn + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + if fun: + if name == 'btwaf' and fun == 'index': + pass + elif name == 'firewall' and fun == 'get_file': + pass + elif fun == 'static': + pass + elif stype == 'html': + pass + else: + if public.get_csrf_cookie_token_key( + ) in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson( + False, + 'CSRF calibration failed, please login again' + ), json_header + args = None + else: + p_path = public.get_plugin_path() + '/' + name + if not os.path.exists(p_path): return abort(404) + args = get_input() + args_list = [ + 'mail_from', 'password', 'mail_to', 'subject', 'content', + 'subtype', 'data' + ] + for k in args.get_items(): + if not k in args_list: return abort(404) + + is_accept = False + if not fun: fun = 'index.html' + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + + if not name: name = 'coll' + if not public.path_safe_check("%s/%s/%s" % (name, fun, stype)): + return abort(404) + if name.find('./') != -1 or not re.match(r"^[\w-]+$", name): + return abort(404) + if not name: + return public.returnJson( + False, 'Please pass in the plug-in name!'), json_header + p_path = public.get_plugin_path() + '/' + name + if not os.path.exists(p_path): + if name == 'btwaf' and fun == 'index': + pdata = {} + import panelPlugin + plu_panel = panelPlugin.panelPlugin() + plugin_list = plu_panel.get_cloud_list() + if not 'pro' in plugin_list: plugin_list['pro'] = -1 + for p in plugin_list['list']: + if p['name'] in ['btwaf']: + if p['endtime'] != 0 and p['endtime'] < time.time(): + pdata['error_msg'] = 1 + break + return render_template('error3.html', data=pdata) + return abort(404) + + # 是否响插件应静态文件 + if fun == 'static': + if stype.find('./') != -1 or not os.path.exists(p_path + '/static'): + return abort(404) + s_file = p_path + '/static/' + stype + if s_file.find('..') != -1: return abort(404) + if not re.match(r"^[\w\./-]+$", s_file): return abort(404) + if not public.path_safe_check(s_file): return abort(404) + if not os.path.exists(s_file): return abort(404) + return send_file(s_file, conditional=True, etag=True) + + # 准备参数 + if not args: args = get_input() + args.client_ip = public.GetClientIp() + args.fun = fun + + # 初始化插件对象 + try: + is_php = os.path.exists(p_path + '/index.php') + if not is_php: + import panelPlugin + plu_panel = panelPlugin.panelPlugin() + plugin_list = plu_panel.get_cloud_list() + waf = 0 + if not 'pro' in plugin_list: plugin_list['pro'] = -1 + for p in plugin_list['list']: + if p['name'] in ['btwaf']: + if p['endtime'] != 0 and p['endtime'] < time.time(): + waf = -1 + try: + public.package_path_append(p_path) + plugin_main = __import__(name + '_main') + if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list[ + 'pro'] == -1: + return render_template('error3.html', data={}) + except: + if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list[ + 'pro'] == -1: + return render_template('error3.html', data={}) + if os.path.exists("{}/btwaf".format(public.get_plugin_path())): + return render_template('error3.html', data={}) + try: + if sys.version_info[0] == 2: + reload(plugin_main) + else: + from imp import reload + reload(plugin_main) + except: + pass + + + plu = eval('plugin_main.' + name + '_main()') + + # methods = dir(plu) + # # 遍历列表并打印每个方法的名称 + # for method in methods: + # # 排除以双下划线开头和结尾的特殊属性和方法 + # if not method.startswith("__") and not method.endswith("__"): + # public.print_log(method) + + if not hasattr(plu, fun): + + return public.returnJson(False, + 'Plugin does not exist'), json_header + + # 执行插件方法 + if not is_php: + if is_accept: + checks = plu._check(args) + if type(checks) != bool or not checks: + return public.getJson(checks), json_header + data = eval('plu.' + fun + '(args)') + else: + comReturn = comm.local() + if comReturn: return comReturn + import panelPHP + args.s = fun + args.name = name + data = panelPHP.panelPHP(name).exec_php_script(args) + + r_type = type(data) + if r_type in [Response, Resp]: + return data + + # 处理响应 + if stype == 'json': # 响应JSON + return public.getJson(data), json_header + elif stype == 'html': # 使用模板 + t_path_root = p_path + '/templates/' + t_path = t_path_root + fun + '.html' + if not os.path.exists(t_path): + return public.returnJson( + False, + 'The specified template does not exist!'), json_header + t_body = public.readFile(t_path) + + # 处理模板包含 + rep = r'{%\s?include\s"(.+)"\s?%}' + includes = re.findall(rep, t_body) + for i_file in includes: + filename = p_path + '/templates/' + i_file + i_body = 'ERROR: File ' + filename + ' does not exists.' + if os.path.exists(filename): + i_body = public.readFile(filename) + t_body = re.sub(rep.replace('(.+)', i_file), i_body, t_body) + + return render_template_string(t_body, data=data) + else: # 直接响应插件返回值,可以是任意flask支持的响应类型 + r_type = type(data) + if r_type == dict: + if name == 'btwaf' and 'msg' in data: + return render_template('error3.html', + data={"error_msg": data['msg']}) + return public.returnJson( + False, + public.getMsg('Bad return type [{}]').format( + r_type)), json_header + return data + except: + return public.get_error_info() + return public.get_error_object(None, plugin_name=name) + + +@app.route('/hook', methods=method_all) +def panel_hook(): + # webhook接口 + get = get_input() + if not os.path.exists('plugin/webhook'): + return abort(404) + public.package_path_append('plugin/webhook') + import webhook_main + return public.getJson(webhook_main.webhook_main().RunHook(get)) + + +@app.route('/install', methods=method_all) +def install(): + # 初始化面板接口 + if public.is_spider(): return abort(404) + if not os.path.exists('install.pl'): return redirect('/login') + if public.M('config').where("id=?", ('1', )).getField('status') == 1: + if os.path.exists('install.pl'): os.remove('install.pl') + session.clear() + return redirect('/login') + ret_login = os.path.join('/', admin_path) + if admin_path == '/' or admin_path == '/bt': ret_login = '/login' + session['admin_path'] = False + session['login'] = False + if request.method == method_get[0]: + if not os.path.exists('install.pl'): return redirect(ret_login) + data = {} + data['status'] = os.path.exists('install.pl') + data['username'] = public.GetRandomString(8).lower() + return render_template('install.html', data=data) + + elif request.method == method_post[0]: + if not os.path.exists('install.pl'): return redirect(ret_login) + get = get_input() + if not hasattr(get, 'bt_username'): + return public.get_msg_gettext('The user name cannot be empty!') + if not get.bt_username: + return public.get_msg_gettext('The user name cannot be empty!') + if not hasattr(get, 'bt_password1'): + return public.get_msg_gettext('Password can not be blank!') + if not get.bt_password1: + return public.get_msg_gettext('Password can not be blank!') + if get.bt_password1 != get.bt_password2: + return public.get_msg_gettext( + 'The passwords entered twice do not match, please re-enter!') + public.M('users').where("id=?", (1, )).save( + 'username,password', + (get.bt_username, + public.password_salt(public.md5(get.bt_password1.strip()), + uid=1))) + os.remove('install.pl') + public.M('config').where("id=?", ('1', )).setField('status', 1) + data = {} + data['status'] = os.path.exists('install.pl') + data['username'] = get.bt_username + return render_template('install.html', data=data) + + +# --------------------- websocket START -------------------------- # + + +@sockets.route('/workorder_client') +def workorder_client(ws): + comReturn = comm.local() + if comReturn: return comReturn + + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): + return + + import panelWorkorder + toObject = panelWorkorder.panelWorkorder() + get = get_input() + toObject.client(ws, get) + + +@sockets.route('/ws_panel') +def ws_panel(ws): + ''' + @name 面板接口ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + comReturn = comm.local() + if comReturn: return comReturn + + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + while True: + pdata = ws.receive() + if pdata == '{}': break + data = json.loads(pdata) + get = public.to_dict_obj(data) + get._ws = ws + p = threading.Thread(target=ws_panel_thread, args=(get, )) + p.start() + + +def ws_panel_thread(get): + ''' + @name 面板管理ws线程 + @author hwliang<2021-07-24> + @param get 请求参数 + @return void + ''' + + public.print_log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@") + + if not hasattr(get, 'ws_callback'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) + return + if not hasattr(get, 'mod_name'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'mod_name'))) + return + if not hasattr(get, 'def_name'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'def_name'))) + return + get.mod_name = get.mod_name.strip() + get.def_name = get.def_name.strip() + check_str = '{}{}'.format(get.mod_name, get.def_name) + if not re.match(r"^\w+$", check_str) or get.mod_name in [ + 'public', 'common', 'db', 'db_mysql', 'downloadFile', 'jobs' + ]: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Unsafe mod_name, def_name parameter content'))) + return + # if not hasattr(get, 'args'): + # get._ws.send(public.getJson(public.return_status_code(1001, 'args'))) + # return + + mod_file = '{}/{}.py'.format(public.get_class_path(), get.mod_name) + + if not os.path.exists(mod_file): + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) + return + _obj = public.get_script_object(mod_file) + if not _obj: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) + return + _cls = getattr(_obj, get.mod_name) + if not _cls: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.mod_name)))) + return + _def = getattr(_cls(), get.def_name) + if not _def: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.def_name)))) + return + result = {'callback': get.ws_callback, 'result': _def(get)} + get._ws.send(public.getJson(result)) + + +@sockets.route('/ws_project') +def ws_project(ws): + ''' + @name 项目管理ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + comReturn = comm.local() + if comReturn: return comReturn + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + from panelProjectController import ProjectController + project_obj = ProjectController() + while True: + pdata = ws.receive() + if pdata in '{}': break + get = public.to_dict_obj(json.loads(pdata)) + get._ws = ws + p = threading.Thread(target=ws_project_thread, args=(project_obj, get)) + p.start() + + +def ws_project_thread(_obj, get): + ''' + @name 项目管理ws线程 + @author hwliang<2021-07-24> + @param _obj 项目管理控制器对像 + @param get 请求参数 + @return void + ''' + if not hasattr(get, 'ws_callback'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) + return + result = {'callback': get.ws_callback, 'result': _obj.model(get)} + get._ws.send(public.getJson(result)) + + +import subprocess + +sock_pids = {} + + +@sockets.route('/sock_shell') +def sock_shell(ws): + ''' + @name 执行指定命令,实时输出命令执行结果 + @author hwliang<2021-07-19> + @return void + + 示例: + p = new WebSocket('ws://192.168.1.247:8888/sock_shell') + p.send('ping www.bt.cn -c 100') + ''' + comReturn = comm.local() + if comReturn: + ws.send(str(comReturn)) + return + kill_closed() + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + t = None + try: + while True: + cmdstring = ws.receive() + if cmdstring in ['stop', 'error'] or not cmdstring: + break + t = threading.Thread(target=sock_recv, args=(cmdstring, ws)) + t.start() + kill_closed() + except: + kill_closed() + + +def kill_closed(): + ''' + @name 关闭已关闭的连接 + @author hwliang<2021-07-24> + @return void + ''' + global sock_pids + import psutil + pids = psutil.pids() + keys = sock_pids.copy().keys() + for pid in keys: + if hasattr(sock_pids[pid], 'closed'): + is_closed = sock_pids[pid].closed + else: + is_closed = not sock_pids[pid].connected + + logging.debug("PID: {} , sock_stat: {}".format(pid, is_closed)) + if not is_closed: continue + + if pid in pids: + try: + p = psutil.Process(pid) + for cp in p.children(): + cp.kill() + p.kill() + logging.debug("killed: {}".format(pid)) + sock_pids.pop(pid) + except: + pass + else: + sock_pids.pop(pid) + + +def sock_recv(cmdstring, ws): + global sock_pids + try: + p = subprocess.Popen(cmdstring + " 2>&1", + close_fds=True, + shell=True, + bufsize=4096, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + sock_pids[p.pid] = ws + kill_closed() + while p.poll() == None: + send_line = p.stdout.readline().decode() + if not send_line or send_line.find('tail: ') != -1: continue + ws.send(send_line) + ws.send(p.stdout.read().decode()) + except: + kill_closed() + + +@app.route('/close_sock_shell', methods=method_all) +def close_sock_shell(): + ''' + @name 关闭指定命令 + @author hwliang<2021-07-19> + @param cmdstring 完整命令行 + @return dict + 示例: + $.post('/close_sock_shell',{cmdstring:'ping www.bt.cn -c 100'}) + ''' + comReturn = comm.local() + if comReturn: return comReturn + args = get_input() + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + cmdstring = args.cmdstring.strip() + skey = public.md5(cmdstring) + pid = cache.get(skey) + if not pid: + return json.dumps( + public.return_data( + False, [], error_msg='The specified sock has been terminated!') + ), json_header + os.kill(pid, 9) + cache.delete(skey) + return json.dumps(public.return_data(True, + 'Successful operation!')), json_header + + +def check_csrf_websocket(ws, args): + ''' + @name 检查websocket是否被csrf攻击 + @author hwliang<2021-07-24> + @param ws websocket对像 + @return void + ''' + if g.is_aes: return True + if g.api_request: return True + if public.is_debug(): return True + is_success = True + if not 'x-http-token' in args: + is_success = False + + if is_success: + if public.get_csrf_sess_html_token_value() != args['x-http-token']: + is_success = False + + if not is_success: + ws.send('token error') + return False + + return True + + +@sockets.route('/webssh') +def webssh(ws): + # 宝塔终端连接 + comReturn = comm.local() + if comReturn: + ws.send(str(comReturn)) + return + if not ws: return 'False' + get = ws.receive() + if not get: return + get = json.loads(get) + if not check_csrf_websocket(ws, get): + return + + import ssh_terminal + sp = ssh_terminal.ssh_host_admin() + if 'host' in get: + ssh_info = {} + ssh_info['host'] = get['host'].strip() + if 'port' in get: + ssh_info['port'] = int(get['port']) + if 'username' in get: + ssh_info['username'] = get['username'].strip() + if 'password' in get: + ssh_info['password'] = get['password'].strip() + if 'pkey' in get: + ssh_info['pkey'] = get['pkey'].strip() + + if get['host'] in ['127.0.0.1', 'localhost' + ] and 'port' not in ssh_info: + ssh_info = sp.get_ssh_info('127.0.0.1') + if not ssh_info: ssh_info = sp.get_ssh_info('localhost') + if not ssh_info: ssh_info = {"host": "127.0.0.1"} + ssh_info['port'] = public.get_ssh_port() + else: + ssh_info = sp.get_ssh_info('127.0.0.1') + if not ssh_info: ssh_info = sp.get_ssh_info('localhost') + if not ssh_info: ssh_info = {"host": "127.0.0.1"} + ssh_info['port'] = public.get_ssh_port() + + if not ssh_info['host'] in ['127.0.0.1', 'localhost']: + if not 'username' in ssh_info: + ssh_info = sp.get_ssh_info(ssh_info['host']) + if not ssh_info: + ws.send( + 'The specified host information is not found, please add it again!' + ) + return + p = ssh_terminal.ssh_terminal() + p.run(ws, ssh_info) + del (p) + if ws.connected: + ws.close() + return 'False' + + +# --------------------- websocket END -------------------------- # + +@app.route("/daily", methods=method_all) +def daily(): + """面板日报数据""" + + comReturn = comm.local() + if comReturn: return comReturn + + import panelDaily + toObject = panelDaily.panelDaily() + + defs = ("get_app_usage", "get_daily_data", "get_daily_list") + result = publicObject(toObject, defs) + return result + + +@app.route('/phpmyadmin/', methods=method_all) +def pma_proxy(path_full=None): + ''' + @name phpMyAdmin代理 + @author hwliang<2022-01-19> + @return Response + ''' + comReturn = comm.local() + if comReturn: return comReturn + cache_key = 'pmd_port_path' + pmd = cache.get(cache_key) + if not pmd: + pmd = get_phpmyadmin_dir() + if not pmd: + return 'phpMyAdmin is not installed, please go to the [App Store] page to install it!' + pmd = list(pmd) + cache.set(cache_key, pmd, 10) + panel_pool = 'http://' + if request.url_root[:5] == 'https': + panel_pool = 'https://' + import ajax + ssl_info = ajax.ajax().get_phpmyadmin_ssl(None) + if ssl_info['status']: + pmd[1] = ssl_info['port'] + else: + panel_pool = 'http://' + + proxy_url = '{}127.0.0.1:{}/{}/'.format( + panel_pool, pmd[1], pmd[0]) + request.full_path.replace( + '/phpmyadmin/', '') + from panelHttpProxy import HttpProxy + px = HttpProxy() + return px.proxy(proxy_url) + + +@app.route('/p/', methods=method_all) +@app.route('/p//', methods=method_all) +@app.route('/p//', methods=method_all) +def proxy_port(port, full_path=None): + ''' + @name 代理指定端口 + @author hwliang<2022-01-19> + @return Response + ''' + + comReturn = comm.local() + if comReturn: return comReturn + full_path = request.full_path.replace('/p/{}/'.format(port), + '').replace('/p/{}'.format(port), '') + uri = '{}/{}'.format(port, full_path) + uri = uri.replace('//', '/') + proxy_url = 'http://127.0.0.1:{}'.format(uri) + from panelHttpProxy import HttpProxy + px = HttpProxy() + return px.proxy(proxy_url) + + +@app.route('/push', methods=method_all) +def push(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + import panelPush + toObject = panelPush.panelPush() + defs = ('set_push_status', 'get_push_msg_list', 'get_modules_list', + 'install_module', 'uninstall_module', 'get_module_template', + 'set_push_config', 'get_push_config', 'del_push_config', + 'get_module_logs', 'get_module_config', 'get_push_list', + 'get_push_logs') + result = publicObject(toObject, defs, None, pdata) + return result diff --git a/BTPanel/routes/v2.py b/BTPanel/routes/v2.py new file mode 100644 index 00000000..00e8ec55 --- /dev/null +++ b/BTPanel/routes/v2.py @@ -0,0 +1,2397 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 V2路由 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +from BTPanel.app import * + +@app.route('/new', methods=method_get) +@app.route('/new/', methods=method_get) +def index_new(sub_path: str = ''): + # 面板首页 + comReturn = comm.local() + if comReturn: return comReturn + data = {} + data[public.to_string([112, + 100])], data['pro_end'], data['ltd_end'] = get_pd() + data['siteCount'] = public.M('sites').count() + data['ftpCount'] = public.M('ftps').count() + data['databaseCount'] = public.M('databases').count() + data['lan'] = public.GetLan('index') + data['js_random'] = get_js_random() + return render_template('index_new.html', data=data) + +@app.route(route_v2 + '/', methods=method_all) +def home_v2(): + # 面板首页 + comReturn = comm.local() + if comReturn: return comReturn + data = {} + data[public.to_string([112, + 100])], data['pro_end'], data['ltd_end'] = get_pd() + data['siteCount'] = public.M('sites').count() + data['ftpCount'] = public.M('ftps').count() + data['databaseCount'] = public.M('databases').count() + data['lan'] = public.GetLan('index') + data['js_random'] = get_js_random() + return render_template('index.html', data=data) + + +@app.route(route_v2 + '/xterm', methods=method_all) +def xterm_v2(): + # 宝塔终端管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0]: + import system_v2 + data = system_v2.system().GetConcifInfo() + return render_template('xterm.html', data=data) + import ssh_terminal_v2 + ssh_host_admin = ssh_terminal_v2.ssh_host_admin() + defs = ('get_host_list', 'get_host_find', 'modify_host', 'create_host', + 'remove_host', 'set_sort', 'get_command_list', 'create_command', + 'get_command_find', 'modify_command', 'remove_command') + return publicObject(ssh_host_admin, defs, None) + + +@app.route(route_v2 + '/modify_password', methods=method_get) +def modify_password_v2(): + comReturn = comm.local() + if comReturn: return comReturn + # if not session.get('password_expire',False): return redirect('/',302) + data = {} + g.title = public.get_msg_gettext( + 'The password has expired, please change it!') + return render_template('modify_password.html', data=data) + + +@app.route(route_v2 + '/site', methods=method_all) +def site_v2(pdata=None): + # public.print_log("----/v2/site_v2----1") + # 网站管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + # data = {} + import system_v2 + data = system_v2.system().GetConcifInfo() + data['isSetup'] = True + data['lan'] = public.getLan('site') + data['js_random'] = get_js_random() + if os.path.exists(public.GetConfigValue('setup_path') + '/nginx') == False \ + and os.path.exists(public.GetConfigValue('setup_path') + '/apache') == False \ + and os.path.exists('/usr/local/lsws/bin/lswsctrl') == False: + data['isSetup'] = False + return render_template('site.html', data=data) + import panel_site_v2 + siteObject = panel_site_v2.panelSite() + defs = ( + 'get_auto_restart_rph', + 'remove_auto_restart_rph', + 'auto_restart_rph', + 'check_del_data', + 'upload_csv', + 'create_website_multiple', + 'del_redirect_multiple', + 'del_proxy_multiple', + 'delete_dir_auth_multiple', + 'delete_dir_bind_multiple', + 'delete_domain_multiple', + 'set_site_etime_multiple', + 'set_site_php_version_multiple', + 'delete_website_multiple', + 'set_site_status_multiple', + 'get_site_err_log', + 'get_site_domains', + 'GetRedirectFile', + 'SaveRedirectFile', + 'DeleteRedirect', + 'GetRedirectList', + 'CreateRedirect', + 'ModifyRedirect', + "set_error_redirect", + 'set_dir_auth', + 'delete_dir_auth', + 'get_dir_auth', + 'modify_dir_auth_pass', + 'reset_wp_db', + 'export_domains', + 'import_domains', + 'GetSiteLogs', + 'GetSiteDomains', + 'GetSecurity', + 'SetSecurity', + 'ProxyCache', + 'CloseToHttps', + 'HttpToHttps', + 'SetEdate', + 'SetRewriteTel', + 'GetCheckSafe', + 'CheckSafe', + 'GetDefaultSite', + 'SetDefaultSite', + 'CloseTomcat', + 'SetTomcat', + 'apacheAddPort', + 'AddSite', + 'GetPHPVersion', + 'SetPHPVersion', + 'DeleteSite', + 'AddDomain', + 'DelDomain', + 'GetDirBinding', + 'AddDirBinding', + 'GetDirRewrite', + 'DelDirBinding', + 'get_site_types', + 'add_site_type', + 'remove_site_type', + 'modify_site_type_name', + 'set_site_type', + 'UpdateRulelist', + 'SetSiteRunPath', + 'GetSiteRunPath', + 'SetPath', + 'SetIndex', + 'GetIndex', + 'GetDirUserINI', + 'SetDirUserINI', + 'GetRewriteList', + 'SetSSL', + 'SetSSLConf', + 'CreateLet', + 'CloseSSLConf', + 'GetSSL', + 'SiteStart', + 'SiteStop', + 'Set301Status', + 'Get301Status', + 'CloseLimitNet', + 'SetLimitNet', + 'GetLimitNet', + 'RemoveProxy', + 'GetProxyList', + 'GetProxyDetals', + 'CreateProxy', + 'ModifyProxy', + 'GetProxyFile', + 'SaveProxyFile', + 'ToBackup', + 'DelBackup', + 'GetSitePHPVersion', + 'logsOpen', + 'GetLogsStatus', + 'CloseHasPwd', + 'SetHasPwd', + 'GetHasPwd', + 'GetDnsApi', + 'SetDnsApi', + 'reset_wp_password', + 'is_update', + 'purge_all_cache', + 'set_fastcgi_cache', + 'update_wp', + 'get_wp_username', + 'get_language', + 'deploy_wp', + # 网站管理新增 + 'test_domains_api', + 'site_rname', + ) + return publicObject(siteObject, defs, None, pdata) + + +@app.route(route_v2 + '/ftp', methods=method_all) +def ftp_v2(pdata=None): + # FTP管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + FtpPort() + import system_v2 + data = system_v2.system().GetConcifInfo() + data['isSetup'] = True + data['js_random'] = get_js_random() + if os.path.exists(public.GetConfigValue('setup_path') + + '/pure-ftpd') == False: + data['isSetup'] = False + data['lan'] = public.GetLan('ftp') + return render_template('ftp.html', data=data) + import ftp_v2 + ftpObject = ftp_v2.ftp() + defs = ('AddUser', 'DeleteUser', 'SetUserPassword', 'SetStatus', 'setPort', + 'set_user_home', 'get_login_logs', 'get_action_logs', + 'set_ftp_logs') + return publicObject(ftpObject, defs, None, pdata) + + +@app.route(route_v2 + '/database', methods=method_all) +def database_v2(pdata=None): + # 数据库管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import ajax_v2 + from panelPlugin import panelPlugin + session['phpmyadminDir'] = False + if panelPlugin().get_phpmyadmin_stat(): + pmd = get_phpmyadmin_dir() + if pmd: + session['phpmyadminDir'] = 'http://' + public.GetHost( + ) + ':' + pmd[1] + '/' + pmd[0] + ajax_v2.ajax().set_phpmyadmin_session() + import system_v2 + data = system_v2.system().GetConcifInfo() + data['isSetup'] = os.path.exists( + public.GetConfigValue('setup_path') + '/mysql/bin') + data['mysql_root'] = public.M('config').where( + 'id=?', (1, )).getField('mysql_root') + data['lan'] = public.GetLan('database') + data['js_random'] = get_js_random() + return render_template('database.html', data=data) + import database_v2 + databaseObject = database_v2.database() + defs = ( + 'GetdataInfo', + 'check_del_data', + 'get_database_size', + 'GetInfo', + 'ReTable', + 'OpTable', + 'AlTable', + 'GetSlowLogs', + 'GetRunStatus', + 'SetDbConf', + 'GetDbStatus', + 'BinLog', + 'GetErrorLog', + 'GetMySQLInfo', + 'SetDataDir', + 'SetMySQLPort', + 'AddCloudDatabase', + 'AddDatabase', + 'DeleteDatabase', + 'SetupPassword', + 'ResDatabasePassword', + 'ToBackup', + 'DelBackup', + 'AddCloudServer', + 'GetCloudServer', + 'RemoveCloudServer', + 'ModifyCloudServer', + 'InputSql', + 'SyncToDatabases', + 'SyncGetDatabases', + 'GetDatabaseAccess', + 'SetDatabaseAccess', + 'get_mysql_user', + 'check_mysql_ssl_status', + 'write_ssl_to_mysql', + 'GetdataInfo', + 'GetBackup', + ) + return publicObject(databaseObject, defs, None, pdata) + + +@app.route(route_v2 + '/acme', methods=method_all) +def acme_v2(pdata=None): + # Let's 证书管理 + comReturn = comm.local() + if comReturn: return comReturn + import acme_v3 + acme_v2_object = acme_v3.acme_v2() + defs = ('get_orders', 'remove_order', 'get_order_find', 'revoke_order', + 'create_order', 'get_account_info', 'set_account_info', + 'update_zip', 'get_cert_init_api', 'get_auths', 'auth_domain', + 'check_auth_status', 'download_cert', 'apply_cert', 'renew_cert', + 'apply_cert_api', 'apply_dns_auth') + return publicObject(acme_v2_object, defs, None, pdata) + + +@app.route(route_v2 + '/api', methods=method_all) +def api_v2(pdata=None): + # APP使用的API接口管理 + comReturn = comm.local() + if comReturn: return comReturn + import panel_api_v2 + api_object = panel_api_v2.panelApi() + defs = ('get_token', 'check_bind', 'get_bind_status', 'get_apps', + 'add_bind_app', 'remove_bind_app', 'set_token', 'get_tmp_token', + 'get_app_bind_status', 'login_for_app') + return publicObject(api_object, defs, None, pdata) + + +@app.route(route_v2 + '/control', methods=method_all) +def control_v2(pdata=None): + # 监控页面 + comReturn = comm.local() + if comReturn: return comReturn + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('control') + data['js_random'] = get_js_random() + return render_template('control.html', data=data) + + +@app.route(route_v2 + '/logs', methods=method_all) +def logs_v2(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + data = {} + data['lan'] = public.GetLan('soft') + data['show_workorder'] = not os.path.exists('data/not_workorder.pl') + return render_template('logs.html', data=data) + + +@app.route(route_v2 + '/firewall', methods=method_all) +def firewall_v2(pdata=None): + # 安全页面 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + return render_template('firewall.html', data=data) + import firewalls_v2 + firewallObject = firewalls_v2.firewalls() + defs = ('GetList', 'AddDropAddress', 'DelDropAddress', 'FirewallReload', + 'SetFirewallStatus', 'AddAcceptPort', 'DelAcceptPort', + 'SetSshStatus', 'SetPing', 'SetSshPort', 'GetSshInfo', + 'SetFirewallStatus') + return publicObject(firewallObject, defs, None, pdata) + + +@app.route(route_v2 + '/ssh_security', methods=method_all) +def ssh_security_v2(pdata=None): + # SSH安全 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata and not request.args.get( + 'action', '') in ['download_key']: + data = {} + data['lan'] = public.GetLan('firewall') + data['js_random'] = get_js_random() + return render_template('firewall.html', data=data) + import ssh_security_v2 + firewallObject = ssh_security_v2.ssh_security() + is_csrf = True + if request.args.get('action', '') in ['download_key']: is_csrf = False + defs = ('san_ssh_security', 'set_password', 'set_sshkey', 'stop_key', + 'get_config', 'download_key', 'stop_password', 'get_key', + 'return_ip', 'add_return_ip', 'del_return_ip', 'start_jian', + 'stop_jian', 'get_jian', 'get_logs', 'set_root', 'stop_root', + 'start_auth_method', 'stop_auth_method', 'get_auth_method', + 'check_so_file', 'get_so_file', 'get_pin', 'set_login_send', + 'get_login_send', 'get_msg_push_list', 'clear_login_send') + return publicObject(firewallObject, defs, None, pdata, is_csrf) + + +@app.route(route_v2 + '/monitor', methods=method_all) +def panel_monitor_v2(pdata=None): + # 云控统计信息 + comReturn = comm.local() + if comReturn: return comReturn + import monitor_v2 + dataObject = monitor_v2.Monitor() + defs = ('get_spider', 'get_exception', 'get_request_count_qps', + 'load_and_up_flow', 'get_request_count_by_hour') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/san', methods=method_all) +def san_baseline_v2(pdata=None): + # 云控安全扫描 + comReturn = comm.local() + if comReturn: return comReturn + import san_baseline_v2 + dataObject = san_baseline_v2.san_baseline() + defs = ('start', 'get_api_log', 'get_resut', 'get_ssh_errorlogin', + 'repair', 'repair_all') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/password', methods=method_all) +def panel_password_v2(pdata=None): + # 云控密码管理 + comReturn = comm.local() + if comReturn: return comReturn + import password_v2 + dataObject = password_v2.password() + defs = ('set_root_password', 'get_mysql_root', 'set_mysql_password', + 'set_panel_password', 'SetPassword', 'SetSshKey', 'StopKey', + 'GetConfig', 'StopPassword', 'GetKey', 'get_databses', + 'rem_mysql_pass', 'set_mysql_access', "get_panel_username") + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/warning', methods=method_all) +def panel_warning_v2(pdata=None): + # 首页安全警告 + comReturn = comm.local() + if comReturn: return comReturn + if public.get_csrf_html_token_key() in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + get = get_input() + ikey = 'warning_list' + import panel_warning_v2 + dataObject = panel_warning_v2.panelWarning() + if get.action == 'get_list': + result = cache.get(ikey) + if not result or 'force' in get: + result = json.loads('{"ignore":[],"risk":[],"security":[]}') + try: + defs = ("get_list", ) + result = publicObject(dataObject, defs, None, pdata) + cache.set(ikey, result, 3600) + return result + except: + pass + return result + + defs = ('get_list', 'set_ignore', 'check_find', 'check_cve', + 'set_vuln_ignore', 'get_scan_bar', 'get_tmp_result', + 'kill_get_list') + + if get.action in ['set_ignore', 'check_find', 'set_vuln_ignore']: + cache.delete(ikey) + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/bak', methods=method_all) +def backup_bak_v2(pdata=None): + # 云控备份服务 + comReturn = comm.local() + if comReturn: return comReturn + import backup_bak_v2 + dataObject = backup_bak_v2.backup_bak() + defs = ('get_sites', 'get_databases', 'backup_database', 'backup_site', + 'backup_path', 'get_database_progress', 'get_site_progress', + 'down', 'get_down_progress', 'download_path', 'backup_site_all', + 'get_all_site_progress', 'backup_date_all', + 'get_all_date_progress') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/abnormal', methods=method_all) +def abnormal_v2(pdata=None): + # 云控系统统计 + comReturn = comm.local() + if comReturn: return comReturn + import abnormal_v2 + dataObject = abnormal_v2.abnormal() + defs = ('mysql_server', 'mysql_cpu', 'mysql_count', 'php_server', + 'php_conn_max', 'php_cpu', 'CPU', 'Memory', 'disk', + 'not_root_user', 'start') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/project/nodejs/', methods=method_all) +@app.route(route_v2 + '/project/nodejs//html', methods=method_all) +@app.route(route_v2 + '/project/docker/', methods=method_all) +@app.route(route_v2 + '/project/docker//html', methods=method_all) +@app.route(route_v2 + '/project/quota/', methods=method_all) +@app.route(route_v2 + '/project/quota//html', methods=method_all) +def project_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + comReturn = comm.local() + if comReturn: return comReturn + from panelProjectControllerV2 import ProjectController + project_obj = ProjectController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + if request.path.endswith('/html'): + return project_obj.model(get) + return publicObject(project_obj, defs, None, get) + + +@app.route(route_v2 + '/msg//', methods=method_all) +def msgcontroller_v2(mod_name, def_name): + comReturn = comm.local() + if comReturn: return comReturn + from MsgControllerV2 import MsgController + project_obj = MsgController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + return publicObject(project_obj, defs, None, get) + + +@app.route(route_v2 + '/docker', methods=method_all) +def docker_v2(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0]: + import system_v2 + data = system_v2.system().GetConcifInfo() + data['js_random'] = get_js_random() + data['lan'] = public.GetLan('files') + return render_template('docker.html', data=data) + + +@app.route(route_v2 + '/dbmodel//', methods=method_all) +def dbmodel_v2(mod_name, def_name): + comReturn = comm.local() + if comReturn: return comReturn + from panelDatabaseControllerV2 import DatabaseController + database_obj = DatabaseController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = mod_name + get.def_name = def_name + + return publicObject(database_obj, defs, None, get) + + +@app.route(route_v2 + '/files', methods=method_all) +def files_v2(pdata=None): + # 文件管理 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not request.args.get( + 'path') and not pdata: + import system_v2 + data = system_v2.system().GetConcifInfo() + data['recycle_bin'] = os.path.exists('data/recycle_bin.pl') + data['lan'] = public.GetLan('files') + data['js_random'] = get_js_random() + return render_template('files.html', data=data) + import files_v2 + filesObject = files_v2.files() + defs = ('files_search', 'files_replace', 'get_replace_logs', + 'get_images_resize', 'add_files_rsync', 'get_file_attribute', + 'get_file_hash', 'CreateLink', 'get_progress', 'restore_website', + 'fix_permissions', 'get_all_back', 'restore_path_permissions', + 'del_path_premissions', 'get_path_premissions', + 'back_path_permissions', 'upload_file_exists', 'CheckExistsFiles', + 'GetExecLog', 'GetSearch', 'ExecShell', 'GetExecShellMsg', + 'exec_git', 'exec_composer', 'create_download_url', 'UploadFile', + 'GetDir', 'CreateFile', 'CreateDir', 'DeleteDir', 'DeleteFile', + 'get_download_url_list', 'remove_download_url', + 'modify_download_url', 'CopyFile', 'CopyDir', 'MvFile', + 'GetFileBody', 'SaveFileBody', 'Zip', 'UnZip', + 'get_download_url_find', 'set_file_ps', 'SearchFiles', 'upload', + 'read_history', 're_history', 'auto_save_temp', + 'get_auto_save_body', 'get_videos', 'GetFileAccess', + 'SetFileAccess', 'GetDirSize', 'SetBatchData', 'BatchPaste', + 'install_rar', 'get_path_size', 'DownloadFile', 'GetTaskSpeed', + 'CloseLogs', 'InstallSoft', 'UninstallSoft', 'SaveTmpFile', + 'get_composer_version', 'exec_composer', 'update_composer', + 'GetTmpFile', 'del_files_store', 'add_files_store', + 'get_files_store', 'del_files_store_types', + 'add_files_store_types', 'exec_git', 'RemoveTask', 'ActionTask', + 'Re_Recycle_bin', 'Get_Recycle_bin', 'Del_Recycle_bin', + 'Close_Recycle_bin', 'Recycle_bin', 'file_webshell_check', + 'dir_webshell_check', 'files_search', 'files_replace', + 'get_replace_logs') + return publicObject(filesObject, defs, None, pdata) + + +@app.route(route_v2 + '/crontab', methods=method_all) +def crontab_v2(pdata=None): + # 计划任务 + comReturn = comm.local() + if comReturn: return comReturn + if request.method == method_get[0] and not pdata: + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('crontab') + data['js_random'] = get_js_random() + return render_template('crontab.html', data=data) + import crontab_v2 + crontabObject = crontab_v2.crontab() + defs = ('GetCrontab', 'AddCrontab', 'GetDataList', 'GetLogs', 'DelLogs', + 'DelCrontab', 'StartTask', 'set_cron_status', 'get_crond_find', + 'modify_crond', 'get_backup_list') + return publicObject(crontabObject, defs, None, pdata) + + +@app.route(route_v2 + '/soft', methods=method_all) +def soft_v2(pdata=None): + # 软件商店页面 + comReturn = comm.local() + if comReturn: return comReturn + import system_v2 + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('soft') + data['js_random'] = get_js_random() + return render_template('soft.html', data=data) + + +@app.route(route_v2 + '/config', methods=method_all) +def config_v2(pdata=None): + # 面板设置页面 + comReturn = comm.local() + if comReturn: return comReturn + + if request.method == method_get[0] and not pdata: + import system_v2, wxapp_v2, config_v2 + c_obj = config_v2.config() + data = system_v2.system().GetConcifInfo() + data['lan'] = public.GetLan('config') + try: + data['wx'] = wxapp_v2.wxapp().get_user_info(None)['msg'] + except: + data['wx'] = 'INIT_WX_NOT_BIND' + data['api'] = '' + data['ipv6'] = '' + sess_out_path = 'data/session_timeout.pl' + if not os.path.exists(sess_out_path): + public.writeFile(sess_out_path, '86400') + s_time_tmp = public.readFile(sess_out_path) + if not s_time_tmp: s_time_tmp = '0' + data['session_timeout'] = int(s_time_tmp) + if c_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' + if c_obj.get_token(None)['open']: data['api'] = 'checked' + data['basic_auth'] = c_obj.get_basic_auth_stat(None) + data['status_code'] = c_obj.get_not_auth_status() + data['basic_auth']['value'] = public.getMsg('CLOSED') + if data['basic_auth']['open']: + data['basic_auth']['value'] = public.getMsg('OPENED') + data['debug'] = '' + data['js_random'] = get_js_random() + if app.config['DEBUG']: data['debug'] = 'checked' + data['is_local'] = '' + if public.is_local(): data['is_local'] = 'checked' + data['public_key'] = public.get_rsa_public_key().replace("\n", "") + return render_template('config.html', data=data) + import config_v2 + defs = ( + 'send_by_telegram', + 'set_empty', + 'set_backup_notification', + 'get_panel_ssl_status', + 'set_file_deny', + 'del_file_deny', + 'get_file_deny', + 'set_improvement', + 'get_httpd_access_log_format_parameter', + 'set_httpd_format_log_to_website', + 'get_httpd_access_log_format', + 'del_httpd_access_log_format', + 'add_httpd_access_log_format', + 'get_nginx_access_log_format_parameter', + 'set_format_log_to_website', + 'get_nginx_access_log_format', + 'del_nginx_access_log_format', + 'set_click_logs', + 'get_node_config', + 'add_nginx_access_log_format', + 'get_ols_private_cache_status', + 'get_ols_value', + 'set_ols_value', + 'set_node_config', + 'get_ols_private_cache', + 'get_ols_static_cache', + 'set_ols_static_cache', + 'switch_ols_private_cache', + 'set_ols_private_cache', + 'set_coll_open', + 'get_qrcode_data', + 'check_two_step', + 'set_two_step_auth', + 'create_user', + 'remove_user', + 'modify_user', + 'get_key', + 'get_php_session_path', + 'set_php_session_path', + 'get_cert_source', + 'get_users', + 'set_request_iptype', + 'set_local', + 'set_debug', + 'get_panel_error_logs', + 'clean_panel_error_logs', + 'get_menu_list', + 'set_hide_menu_list', + 'get_basic_auth_stat', + 'set_basic_auth', + 'get_cli_php_version', + 'get_tmp_token', + 'get_temp_login', + 'set_temp_login', + 'remove_temp_login', + 'clear_temp_login', + 'get_temp_login_logs', + 'set_cli_php_version', + 'DelOldSession', + 'GetSessionCount', + 'SetSessionConf', + 'set_not_auth_status', + 'GetSessionConf', + 'get_ipv6_listen', + 'set_ipv6_status', + 'GetApacheValue', + 'SetApacheValue', + 'install_msg_module', + 'GetNginxValue', + 'SetNginxValue', + 'get_token', + 'set_token', + 'set_admin_path', + 'is_pro', + 'set_msg_config', + 'get_php_config', + 'get_config', + 'SavePanelSSL', + 'GetPanelSSL', + 'GetPHPConf', + 'SetPHPConf', + 'uninstall_msg_module', + 'GetPanelList', + 'AddPanelInfo', + 'SetPanelInfo', + 'DelPanelInfo', + 'ClickPanelInfo', + 'SetPanelSSL', + 'get_msg_configs', + 'SetTemplates', + 'Set502', + 'setPassword', + 'setUsername', + 'setPanel', + 'setPathInfo', + 'setPHPMaxSize', + 'get_msg_fun', + 'getFpmConfig', + 'setFpmConfig', + 'setPHPMaxTime', + 'syncDate', + 'setPHPDisable', + 'SetControl', + 'get_settings2', + 'del_tg_info', + 'set_tg_bot', + 'ClosePanel', + 'AutoUpdatePanel', + 'SetPanelLock', + 'return_mail_list', + 'del_mail_list', + 'add_mail_address', + 'user_mail_send', + 'get_user_mail', + 'set_dingding', + 'get_dingding', + 'get_settings', + 'user_stmp_mail_send', + 'user_dingding_send', + 'get_login_send', + 'set_login_send', + 'clear_login_send', + 'get_login_log', + 'login_ipwhite', + 'set_ssl_verify', + 'get_ssl_verify', + 'get_password_config', + 'set_password_expire', + 'set_password_safe', + 'get_module_template', + # 新增nps评分 + 'write_nps_new', + 'get_nps_new', + "check_nps") + return publicObject(config_v2.config(), defs, None, pdata) + + +@app.route(route_v2 + '/ajax', methods=method_all) +def ajax_v2(pdata=None): + # 面板系统服务状态接口 + comReturn = comm.local() + if comReturn: return comReturn + import ajax_v2 + ajaxObject = ajax_v2.ajax() + defs = ('get_lines', 'php_info', 'change_phpmyadmin_ssl_port', + 'set_phpmyadmin_ssl', 'get_phpmyadmin_ssl', 'get_pd', + 'check_user_auth', 'to_not_beta', 'get_beta_logs', 'apple_beta', + 'GetApacheStatus', 'GetCloudHtml', 'get_pay_type', + 'get_load_average', 'GetOpeLogs', 'GetFpmLogs', 'GetFpmSlowLogs', + 'SetMemcachedCache', 'GetMemcachedStatus', 'GetRedisStatus', + 'GetWarning', 'SetWarning', 'CheckLogin', 'GetSpeed', 'GetAd', + 'phpSort', 'ToPunycode', 'GetBetaStatus', 'SetBeta', + 'setPHPMyAdmin', 'delClose', 'KillProcess', 'GetPHPInfo', + 'GetQiniuFileList', 'get_process_tops', 'get_process_cpu_high', + 'UninstallLib', 'InstallLib', 'SetQiniuAS', 'GetQiniuAS', + 'GetLibList', 'GetProcessList', 'GetNetWorkList', 'GetNginxStatus', + 'GetPHPStatus', 'GetTaskCount', 'GetSoftList', 'GetNetWorkIo', + 'GetDiskIo', 'GetCpuIo', 'CheckInstalled', 'UpdatePanel', + 'GetInstalled', 'GetPHPConfig', 'SetPHPConfig', 'log_analysis', + 'speed_log', 'get_result', 'get_detailed', 'ignore_version') + + return publicObject(ajaxObject, defs, None, pdata) + + +@app.route(route_v2 + '/system', methods=method_all) +def system_v2(pdata=None): + # 面板系统状态接口 + comReturn = comm.local() + if comReturn: return comReturn + import system_v2 + sysObject = system_v2.system() + defs = ('get_io_info', 'UpdatePro', 'GetAllInfo', 'GetNetWorkApi', + 'GetLoadAverage', 'ClearSystem', 'GetNetWorkOld', 'GetNetWork', + 'GetDiskInfo', 'GetCpuInfo', 'GetBootTime', 'GetSystemVersion', + 'GetMemInfo', 'GetSystemTotal', 'GetConcifInfo', 'ServiceAdmin', + 'ReWeb', 'RestartServer', 'ReMemory', 'RepPanel') + return publicObject(sysObject, defs, None, pdata) + + +@app.route(route_v2 + '/deployment', methods=method_all) +def deployment_v2(pdata=None): + # 一键部署接口 + comReturn = comm.local() + if comReturn: return comReturn + import plugin_deployment_v2 + sysObject = plugin_deployment_v2.plugin_deployment() + defs = ('GetList', 'AddPackage', 'DelPackage', 'SetupPackage', 'GetSpeed', + 'GetPackageOther') + return publicObject(sysObject, defs, None, pdata) + + +@app.route(route_v2 + '/data', methods=method_all) +@app.route(route_v2 + '/panel_data', methods=method_all) +def panel_data_v2(pdata=None): + # 从数据库获取数据接口 + comReturn = comm.local() + if comReturn: return comReturn + import data_v2 + dataObject = data_v2.data() + defs = ('setPs', 'getData', 'getFind', 'getKey') + return publicObject(dataObject, defs, None, pdata) + + +@app.route(route_v2 + '/ssl', methods=method_all) +def ssl_v2(pdata=None): + # 商业SSL证书申请接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_ssl_v2 + toObject = panel_ssl_v2.panelSSL() + defs = ( + 'check_url_txt', + 'RemoveCert', + 'renew_lets_ssl', + 'SetCertToSite', + 'GetCertList', + 'SaveCert', + 'GetCert', + 'GetCertName', + 'again_verify', + 'DelToken', + 'GetToken', + 'GetUserInfo', + 'GetOrderList', + 'GetDVSSL', + 'Completed', + 'SyncOrder', + 'download_cert', + 'set_cert', + 'cancel_cert_order', + 'get_order_list', + 'get_order_find', + 'apply_order_pay', + 'get_pay_status', + 'apply_order', + 'get_verify_info', + 'get_verify_result', + 'get_product_list', + 'set_verify_info', + 'GetSSLInfo', + 'downloadCRT', + 'GetSSLProduct', + 'Renew_SSL', + 'Get_Renew_SSL', + # 新增 购买证书对接接口 + 'get_product_list_v2', + 'apply_cert_order_pay', + 'get_cert_admin', + 'apply_order_ca', + 'apply_cert_install_pay', + + # 'pay_test' + ) + get = get_input() + + if get.action == 'download_cert': + from io import BytesIO + import base64 + result = toObject.download_cert(get) + # public.print_log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@1111111111111111 result: {}".format(result)) + # {'success': False, 'res': '[code: 0] no data [file: /www/wwwroot/192.168.1.139/app/Api/Cert/controllers/Cert.php] [line: 955]', 'nonce': 1706498844} + + fp = BytesIO(base64.b64decode(result['res']['data'])) + return send_file(fp, + download_name=result['res']['filename'], + as_attachment=True, + mimetype='application/zip') + result = publicObject(toObject, defs, get.action, get) + return result + + +@app.route(route_v2 + '/task', methods=method_all) +def task_v2(pdata=None): + # 后台任务接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_task_v2 + toObject = panel_task_v2.bt_task() + defs = ('get_task_lists', 'remove_task', 'get_task_find', + "get_task_log_by_id") + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route(route_v2 + '/plugin', methods=method_all) +def plugin_v2(pdata=None): + # 插件系统接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_plugin_v2 + pluginObject = panel_plugin_v2.panelPlugin() + defs = ('get_usually_plugin', 'check_install_limit', 'set_score', + 'get_score', 'update_zip', 'input_zip', 'export_zip', 'add_index', + 'remove_index', 'sort_index', 'install_plugin', 'uninstall_plugin', + 'get_soft_find', 'get_index_list', 'get_soft_list', + 'get_cloud_list', 'check_deps', 'flush_cache', 'GetCloudWarning', + 'install', 'unInstall', 'getPluginList', 'getPluginInfo', + 'get_make_args', 'add_make_args', 'getPluginStatus', + 'setPluginStatus', 'a', 'getCloudPlugin', 'getConfigHtml', + 'savePluginSort', 'del_make_args', 'set_make_args') + return publicObject(pluginObject, defs, None, pdata) + + +@app.route(route_v2 + '/wxapp', methods=method_all) +@app.route(route_v2 + '/panel_wxapp', methods=method_all) +def panel_wxapp_v2(pdata=None): + # 微信小程序绑定接口 + comReturn = comm.local() + if comReturn: return comReturn + import wxapp_v2 + toObject = wxapp_v2.wxapp() + defs = ('blind', 'get_safe_log', 'blind_result', 'get_user_info', + 'blind_del', 'blind_qrcode') + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route(route_v2 + '/auth', methods=method_all) +def auth_v2(pdata=None): + # 面板认证接口 + comReturn = comm.local() + if comReturn: return comReturn + import panel_auth_v2 + toObject = panel_auth_v2.panelAuth() + defs = ('free_trial', 'renew_product_auth', 'auth_activate', + 'get_product_auth', 'get_stripe_session_id', + 'get_re_order_status_plugin', 'create_plugin_other_order', + 'get_order_stat', 'get_voucher_plugin', + 'create_order_voucher_plugin', 'get_product_discount_by', + 'get_re_order_status', 'create_order_voucher', 'create_order', + 'get_order_status', 'get_voucher', 'flush_pay_status', + 'create_serverid', 'check_serverid', 'get_plugin_list', + 'check_plugin', 'get_buy_code', 'check_pay_status', + 'get_renew_code', 'check_renew_code', 'get_business_plugin', + 'get_ad_list', 'check_plugin_end', 'get_plugin_price', + 'get_plugin_remarks', 'get_paypal_session_id', + 'check_paypal_status') + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route(route_v2 + '/download', methods=method_get) +def download_v2(): + # 文件下载接口 + comReturn = comm.local() + if comReturn: return comReturn + filename = request.args.get('filename') + if filename.find('|') != -1: + filename = filename.split('|')[0] # 改为获取本地备份 + if not filename: + return public.ReturnJson(False, "INIT_ARGS_ERR"), json_header + # if filename in ['alioss','qiniu','upyun','txcos','ftp','msonedrive','gcloud_storage', 'gdrive', 'aws_s3']: return panel_cloud() + if not os.path.exists(filename): + return public.ReturnJson(False, "FILE_NOT_EXISTS"), json_header + + if request.args.get('play') == 'true': + import panelVideo + start, end = panelVideo.get_range(request) + g.return_message = True + return panelVideo.partial_response(filename, start, end) + else: + mimetype = "application/octet-stream" + extName = filename.split('.')[-1] + if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None + public.WriteLog("TYPE_FILE", 'FILE_DOWNLOAD', + (filename, public.GetClientIp())) + g.return_message = True + return send_file(filename, + mimetype=mimetype, + as_attachment=True, + etag=True, + conditional=True, + download_name=os.path.basename(filename), + max_age=0) + + +@app.route(route_v2 + '/cloud', methods=method_all) +def panel_cloud_v2(is_csrf=True): + # 从对像存储下载备份文件接口 + comReturn = comm.local() + if comReturn: return comReturn + if is_csrf: + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + get = get_input() + _filename = get.filename + plugin_name = "" + if _filename.find('|') != -1: + plugin_name = get.filename.split('|')[1] + else: + plugin_name = get.filename + + if not os.path.exists('plugin/' + plugin_name + '/' + plugin_name + + '_main.py'): + return public.returnJson( + False, 'The specified plugin does not exist!'), json_header + public.package_path_append('plugin/' + plugin_name) + plugin_main = __import__(plugin_name + '_main') + public.mod_reload(plugin_main) + tmp = eval("plugin_main.%s_main()" % plugin_name) + if not hasattr(tmp, 'download_file'): + return public.returnJson( + False, + 'Specified plugin has no file download function!'), json_header + download_url = tmp.download_file(get.name) + if plugin_name == 'ftp': + if download_url.find("ftp") != 0: + download_url = "ftp://" + download_url + else: + if download_url.find('http') != 0: + download_url = 'http://' + download_url + + if "toserver" in get and get.toserver == "true": + download_dir = "/tmp/" + if "download_dir" in get: + download_dir = get.download_dir + local_file = os.path.join(download_dir, get.name) + + input_from_local = False + if "input_from_local" in get: + input_from_local = True if get.input_from_local == "true" else False + + if input_from_local: + if os.path.isfile(local_file): + return { + "status": True, + "msg": + "The file already exists and will be restored locally.", + "task_id": -1, + "local_file": local_file + } + from panel_task_v2 import bt_task + task_obj = bt_task() + task_id = task_obj.create_task('Download file', 1, download_url, + local_file) + return { + "status": True, + "msg": "The download task was created successfully", + "local_file": local_file, + "task_id": task_id + } + + return redirect(download_url) + + +@app.route(route_v2 + '/btwaf_error', methods=method_get) +def btwaf_error_v2(): + # 图标 + comReturn = comm.local() + if comReturn: return comReturn + get = get_input() + p_path = os.path.join('/www/server/panel/plugin/', "btwaf") + if not os.path.exists(p_path): + if get.name == 'btwaf' and get.fun == 'index': + return render_template('error3.html', data={}) + return render_template('error3.html', data={}) + + +@app.route(route_v2 + '/favicon.ico', methods=method_get) +def send_favicon_v2(): + # 图标 + comReturn = comm.local() + if comReturn: return abort(404) + s_file = '/www/server/panel/BTPanel/static/favicon.ico' + if not os.path.exists(s_file): return abort(404) + return send_file(s_file, conditional=True, etag=True) + + +@app.route(route_v2 + '/rspamd', defaults={'path': ''}, methods=method_all) +@app.route(route_v2 + '/rspamd/', methods=method_all) +def proxy_rspamd_requests_v2(path): + comReturn = comm.local() + if comReturn: return comReturn + param = str(request.url).split('?') + param = "" if len(param) < 2 else param[-1] + import requests + headers = {} + for h in request.headers.keys(): + headers[h] = request.headers[h] + if request.method == "GET": + if re.search(r"\.(js|css)$", path): + return send_file('/usr/share/rspamd/www/rspamd/' + path, + conditional=True, + etag=True) + if path == "/": + return send_file('/usr/share/rspamd/www/rspamd/', + conditional=True, + etag=True) + url = "http://127.0.0.1:11334/rspamd/" + path + "?" + param + for i in [ + 'stat', 'auth', 'neighbours', 'list_extractors', + 'list_transforms', 'graph', 'maps', 'actions', 'symbols', + 'history', 'errors', 'check_selector', 'saveactions', + 'savesymbols', 'getmap' + ]: + if i in path: + url = "http://127.0.0.1:11334/" + path + "?" + param + req = requests.get(url, headers=headers, stream=True) + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) + else: + url = "http://127.0.0.1:11334/" + path + for i in request.form.keys(): + data = '{}='.format(i) + # public.writeFile('/tmp/2',data+"\n","a+") + req = requests.post(url, data=data, headers=headers, stream=True) + + return Resp(stream_with_context(req.iter_content()), + content_type=req.headers['content-type']) + + +@app.route(route_v2 + '/tips', methods=method_get) +def tips_v2(): + # 提示页面 + comReturn = comm.local() + if comReturn: return abort(404) + get = get_input() + if len(get.get_items().keys()) > 1: return abort(404) + return render_template('tips.html') + + +# ======================普通路由区end============================# + +# ======================严格排查区域start============================# + + +@app.route(route_v2 + '/login', methods=method_all) +@app.route(route_v2 + route_path, methods=method_all) +@app.route(route_v2 + route_path + '/', methods=method_all) +def login_v2(): + # 面板登录接口 + if os.path.exists('install.pl'): return redirect('/install') + global admin_check_auth, admin_path, route_path + is_auth_path = False + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + is_auth_path = True + # 登录输入验证 + if request.method == method_post[0]: + if is_auth_path: + g.auth_error = True + return public.error_not_login(None) + v_list = ['username', 'password', 'code', 'vcode', 'cdn_url'] + for v in v_list: + if v in ['username', 'password']: continue + pv = request.form.get(v, '').strip() + if v == 'cdn_url': + if len(pv) > 32: + return public.return_msg_gettext( + False, 'Wrong parameter length!'), json_header + if not re.match(r"^[\w\.-]+$", pv): + public.return_msg_gettext( + False, 'Wrong parameter format!'), json_header + continue + + if not pv: continue + p_len = 32 + if v == 'code': p_len = 4 + if v == 'vcode': p_len = 6 + if len(pv) != p_len: + if v == 'code': + return public.returnJson( + False, 'Verification code length error!'), json_header + return public.returnJson( + False, 'Wrong parameter length!'), json_header + if not re.match(r"^\w+$", pv): + return public.returnJson( + False, 'Wrong parameter format!'), json_header + + for n in request.form.keys(): + if not n in v_list: + return public.returnJson( + False, + 'There can be no extra parameters in the login parameters' + ), json_header + + get = get_input() + import user_login_v2 + if hasattr(get, 'tmp_token'): + result = user_login_v2.userlogin().request_tmp(get) + return is_login(result) + # 过滤爬虫 + if public.is_spider(): return abort(404) + if hasattr(get, 'dologin'): + login_path = '/login' + if not 'login' in session: return redirect(login_path) + if os.path.exists(admin_path_file): login_path = route_path + if session['login'] != False: + session['login'] = False + cache.set('dologin', True) + public.write_log_gettext( + 'Logout', 'Client: {}, has manually exited the panel', + (public.GetClientIp() + ":" + + str(request.environ.get('REMOTE_PORT')), )) + if 'tmp_login_expire' in session: + s_file = 'data/session/{}'.format(session['tmp_login_id']) + if os.path.exists(s_file): + os.remove(s_file) + token_key = public.get_csrf_html_token_key() + if token_key in session: + del (session[token_key]) + session.clear() + sess_file = 'data/sess_files/' + public.get_sess_key() + if os.path.exists(sess_file): + try: + os.remove(sess_file) + except: + pass + sess_tmp_file = public.get_full_session_file() + if os.path.exists(sess_tmp_file): os.remove(sess_tmp_file) + g.dologin = True + return redirect(public.get_admin_path()) + + if is_auth_path: + if route_path != request.path and route_path + '/' != request.path: + referer = request.headers.get('Referer', 'err') + referer_tmp = referer.split('/') + referer_path = referer_tmp[-1] + if referer_path == '': + referer_path = referer_tmp[-2] + if route_path != '/' + referer_path: + g.auth_error = True + # return render_template('autherr.html') + return public.error_not_login(None) + + session['admin_auth'] = True + comReturn = common.panelSetup().init() + if comReturn: return comReturn + + if request.method == method_post[0]: + result = userlogin.userlogin().request_post(get) + return is_login(result) + + if request.method == method_get[0]: + result = userlogin.userlogin().request_get(get) + if result: + return result + data = {} + data['lan'] = public.GetLan('login') + data['hosts'] = '[]' + hosts_file = 'plugin/static_cdn/hosts.json' + if os.path.exists(hosts_file): + data['hosts'] = public.get_cdn_hosts() + if type(data['hosts']) == dict: + data['hosts'] = '[]' + else: + data['hosts'] = json.dumps(data['hosts']) + data['app_login'] = os.path.exists('data/app_login.pl') + public.cache_set( + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp()), 'check', 360) + + # 生成登录token + last_key = 'last_login_token' + # ----------- + last_time_key = 'last_login_token_time' + s_time = int(time.time()) + if last_key in session and last_time_key in session: + # 10秒内不重复生成token + if s_time - session[last_time_key] > 10: + session[last_key] = public.GetRandomString(32) + session[last_time_key] = s_time + else: + session[last_key] = public.GetRandomString(32) + session[last_time_key] = s_time + + data[last_key] = session[last_key] + data['public_key'] = public.get_rsa_public_key() + return render_template('login.html', data=data) + # ----------- + + # rsa_key = 'public_key' + # session[last_key] = public.GetRandomString(32) + # data[last_key] = session[last_key] + # data[rsa_key] = public.get_rsa_public_key().replace("\n", "") + # return render_template('login.html', data=data) + + +@app.route(route_v2 + '/close', methods=method_get) +def close_v2(): + # 面板已关闭页面 + if not os.path.exists('data/close.pl'): return redirect('/') + data = {} + data['lan'] = public.getLan('close') + return render_template('close.html', data=data) + + +@app.route(route_v2 + '/get_app_bind_status', methods=method_all) +def get_app_bind_status_v2(pdata=None): + # APP绑定状态查询 + if not public.check_app('app_bind'): return abort(404) + get = get_input() + if len(get.get_items().keys()) > 2: return 'There are meaningless parameters!' + v_list = ['bind_token', 'data'] + for n in get.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + import panel_api_v2 + api_object = panel_api_v2.panelApi() + return json.dumps(api_object.get_app_bind_status(get_input())), json_header + + +@app.route(route_v2 + '/check_bind', methods=method_all) +def check_bind_v2(pdata=None): + # APP绑定查询 + if not public.check_app('app_bind'): return abort(404) + get = get_input() + if len(get.get_items().keys()) > 4: return 'There are meaningless parameters!' + v_list = ['bind_token', 'client_brand', 'client_model', 'data'] + for n in get.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + import panel_api_v2 + api_object = panel_api_v2.panelApi() + return json.dumps(api_object.check_bind(get_input())), json_header + + +@app.route(route_v2 + '/code', methods=method_get) +def code_v2(): + if not 'code' in session: return '' + if not session['code']: return '' + # 获取图片验证码 + try: + import vilidate_v2 + except: + public.ExecShell("btpip install Pillow -I") + return "Pillow not install!" + vie = vilidate_v2.vieCode() + codeImage = vie.GetCodeImage(80, 4) + if sys.version_info[0] == 2: + try: + from cStringIO import StringIO + except: + from StringIO import StringIO + out = StringIO() + else: + from io import BytesIO + out = BytesIO() + codeImage[0].save(out, "png") + cache.set("codeStr", public.md5("".join(codeImage[1]).lower()), 180) + cache.set("codeOut", 1, 0.1) + out.seek(0) + return send_file(out, mimetype='image/png', max_age=0) + + +@app.route(route_v2 + '/down/', methods=method_all) +def down_v2(token=None, fname=None): + # 文件分享对外接口 + try: + if public.M('download_token').count() == 0: return abort(404) + fname = request.args.get('fname') + if fname: + if (len(fname) > 256): return abort(404) + if fname: fname = fname.strip('/') + if not token: return abort(404) + if len(token) > 48: return abort(404) + char_list = [ + '\\', '/', ':', '*', '?', '"', '<', '>', '|', ';', '&', '`' + ] + for char in char_list: + if char in token: return abort(404) + if not request.args.get('play') in ['true', None, '']: + return abort(404) + args = get_input() + v_list = ['fname', 'play', 'file_password', 'data'] + for n in args.get_items().keys(): + if not n in v_list: + return public.returnJson( + False, 'There can be no redundant parameters'), json_header + if not re.match(r"^[\w\.]+$", token): return abort(404) + find = public.M('download_token').where('token=?', (token, )).find() + + if not find: return abort(404) + if time.time() > int(find['expire']): return abort(404) + + if not os.path.exists(find['filename']): return abort(404) + if find['password'] and not token in session: + if 'file_password' in args: + if not re.match(r"^\w+$", args.file_password): + return public.ReturnJson(False, + 'Wrong password!'), json_header + if re.match(r"^\d+$", args.file_password): + args.file_password = str(int(args.file_password)) + args.file_password += ".0" + if args.file_password != str(find['password']): + return public.ReturnJson(False, + 'Wrong password!'), json_header + session[token] = 1 + session['down'] = True + else: + pdata = { + "to_path": "", + "src_path": find['filename'], + "password": True, + "filename": find['filename'].split('/')[-1], + "ps": find['ps'], + "total": find['total'], + "token": find['token'], + "expire": public.format_date(times=find['expire']) + } + session['down'] = True + return render_template('down.html', data=pdata) + + if not find['password']: + session['down'] = True + session[token] = 1 + + if session[token] != 1: + return abort(404) + + filename = find['filename'] + if fname: + filename = os.path.join(filename, fname) + if not public.path_safe_check(fname, False): return abort(404) + if os.path.isdir(filename): + return get_dir_down(filename, token, find) + else: + if os.path.isdir(filename): + return get_dir_down(filename, token, find) + + if request.args.get('play') == 'true': + import panel_video_v2 + start, end = panel_video_v2.get_range(request) + return panel_video_v2.partial_response(filename, start, end) + else: + mimetype = "application/octet-stream" + extName = filename.split('.')[-1] + if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None + b_name = os.path.basename(filename) + return send_file(filename, + mimetype=mimetype, + as_attachment=True, + download_name=b_name, + max_age=0) + except: + return abort(404) + + +@app.route(route_v2 + '/database/mongodb/', methods=method_all) +@app.route(route_v2 + '/database/pgsql/', methods=method_all) +@app.route(route_v2 + '/database/redis/', methods=method_all) +@app.route(route_v2 + '/database/sqlite/', methods=method_all) +@app.route(route_v2 + '/database/sqlserver/', methods=method_all) +def databaseModel_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + comReturn = comm.local() + if comReturn: return comReturn + from panelDatabaseControllerV2 import DatabaseController + project_obj = DatabaseController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + return publicObject(project_obj, defs, None, get) + + +# 系统安全模型页面 +# @app.route(route_v2+'/safe//', methods=method_all) +@app.route(route_v2 + '/safe/firewall/', methods=method_all) +@app.route(route_v2 + '/safe/freeip/', methods=method_all) +@app.route(route_v2 + '/safe/ips/', methods=method_all) +@app.route(route_v2 + '/safe/security/', methods=method_all) +@app.route(route_v2 + '/safe/ssh/', methods=method_all) +@app.route(route_v2 + '/safe/syslog/', methods=method_all) +def safeModel_v2(mod_name, def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 5: return + comReturn = comm.local() + if comReturn: return comReturn + from panelSafeControllerV2 import SafeController + project_obj = SafeController() + defs = ('model', ) + get = get_input() + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + + return publicObject(project_obj, defs, None, get) + + +# 通用模型路由 +@app.route(route_v2 + '/panel/binlog/', methods=method_all) +@app.route(route_v2 + '/panel/bt_check/', methods=method_all) +@app.route(route_v2 + '/panel/clear/', methods=method_all) +@app.route(route_v2 + '/panel/content/', methods=method_all) +@app.route(route_v2 + '/panel/docker/', methods=method_all) +@app.route(route_v2 + '/panel/go/', methods=method_all) +@app.route(route_v2 + '/panel/java/', methods=method_all) +@app.route(route_v2 + '/panel/nodejs/', methods=method_all) +@app.route(route_v2 + '/panel/other/', methods=method_all) +@app.route(route_v2 + '/panel/php/', methods=method_all) +@app.route(route_v2 + '/panel/python/', methods=method_all) +@app.route(route_v2 + '/panel/quota/', methods=method_all) +@app.route(route_v2 + '/panel/quota/', methods=method_all) +@app.route(route_v2 + '/panel/safe_detect/', methods=method_all) +@app.route(route_v2 + '/panel/scanning/', methods=method_all) +@app.route(route_v2 + '/panel/start_content/', methods=method_all) +@app.route(route_v2 + '/panel/totle_db/', methods=method_all) +@app.route(route_v2 + '/panel/webscanning/', methods=method_all) +@app.route(route_v2 + '/panel/public/', methods=method_all) +@app.route(route_v2 + '/monitor/process_management/', + methods=method_all) +@app.route(route_v2 + '/monitor/soft/', methods=method_all) +@app.route(route_v2 + '/files/down/', methods=method_all) +@app.route(route_v2 + '/files/gz/', methods=method_all) +@app.route(route_v2 + '/files/logs/', methods=method_all) +@app.route(route_v2 + '/files/rar/', methods=method_all) +@app.route(route_v2 + '/files/search/', methods=method_all) +@app.route(route_v2 + '/files/size/', methods=method_all) +@app.route(route_v2 + '/files/upload/', methods=method_all) +@app.route(route_v2 + '/files/zip/', methods=method_all) +@app.route(route_v2 + '/logs/ftp/', methods=method_all) +@app.route(route_v2 + '/logs/panel/', methods=method_all) +@app.route(route_v2 + '/logs/site/', methods=method_all) +def allModule_v2(def_name): + if request.method not in ['GET', 'POST']: return + path_split = request.path.split("/") + if len(path_split) < 4: return + comReturn = comm.local() + if comReturn: return comReturn + p_path = public.get_plugin_path() + '/' + path_split[2] + if os.path.exists(p_path): + return panel_other(index, mod_name, def_name) + + from panelControllerV2 import Controller + controller_obj = Controller() + defs = ('model', ) + get = get_input() + get.model_index = path_split[2] + get.action = 'model' + get.mod_name = path_split[3] + get.def_name = def_name + return publicObject(controller_obj, defs, None, get) + + +@app.route(route_v2 + '/public', methods=method_all) +def panel_public_v2(): + get = get_input() + if len("{}".format(get.get_items())) > 1024 * 32: + return 'ERROR' + + # 获取ping测试 + if 'get_ping' in get: + try: + import panel_ping_v2 + p = panel_ping_v2.Test() + get = p.check(get) + if not get: return 'ERROR' + result = getattr(p, get['act'])(get) + result_type = type(result) + if str(result_type).find('Response') != -1: return result + return public.getJson(result), json_header + except: + return abort(404) + + if public.cache_get( + public.Md5( + uuid.UUID(int=uuid.getnode()).hex[-12:] + + public.GetClientIp())) != 'check': + return abort(404) + global admin_check_auth, admin_path, route_path, admin_path_file + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + return abort(404) + v_list = ['fun', 'name', 'filename', 'data', 'secret_key'] + for n in get.get_items().keys(): + if not n in v_list: + return abort(404) + + get.client_ip = public.GetClientIp() + num_key = get.client_ip + '_wxapp' + if not public.get_error_num(num_key, 10): + return public.return_msg_gettext( + False, + '10 consecutive authentication failures are prohibited for 1 hour') + if not hasattr(get, 'name'): get.name = '' + if not hasattr(get, 'fun'): return abort(404) + if not public.path_safe_check("%s/%s" % (get.name, get.fun)): + return abort(404) + if get.fun in ['login_qrcode', 'is_scan_ok', 'set_login']: + # 检查是否验证过安全入口 + if admin_path != '/bt' and os.path.exists( + admin_path_file) and not 'admin_auth' in session: + return abort(404) + # 验证是否绑定了设备 + if not public.check_app('app'): + return public.return_msg_gettext(False, 'Unbound user') + import wxapp_v2 + pluwx = wxapp_v2.wxapp() + checks = pluwx._check(get) + if type(checks) != bool or not checks: + public.set_error_num(num_key) + return public.getJson(checks), json_header + data = public.getJson(eval('pluwx.' + get.fun + '(get)')) + return data, json_header + else: + return abort(404) + + +@app.route(route_v2 + '//', methods=method_all) +@app.route(route_v2 + '///', methods=method_all) +def panel_other_v2(name=None, fun=None, stype=None): + # 插件接口 + if public.is_error_path(): + return redirect('/error', 302) + if not name: return abort(404) + if not re.match(r"^[\w\-]+$", name): return abort(404) + if fun and not re.match(r"^[\w\-\.]+$", fun): return abort(404) + if name != "mail_sys" or fun != "send_mail_http.json": + comReturn = comm.local() + if comReturn: return comReturn + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + if fun: + if name == 'btwaf' and fun == 'index': + pass + elif name == 'firewall' and fun == 'get_file': + pass + elif fun == 'static': + pass + elif stype == 'html': + pass + else: + if public.get_csrf_cookie_token_key( + ) in session and 'login' in session: + if not check_csrf(): + return public.ReturnJson( + False, + 'CSRF calibration failed, please login again' + ), json_header + args = None + else: + p_path = public.get_plugin_path() + '/' + name + if not os.path.exists(p_path): return abort(404) + args = get_input() + args_list = [ + 'mail_from', 'password', 'mail_to', 'subject', 'content', + 'subtype', 'data' + ] + for k in args.get_items(): + if not k in args_list: return abort(404) + + is_accept = False + if not fun: fun = 'index.html' + if not stype: + tmp = fun.split('.') + fun = tmp[0] + if len(tmp) == 1: tmp.append('') + stype = tmp[1] + + if not name: name = 'coll' + if not public.path_safe_check("%s/%s/%s" % (name, fun, stype)): + return abort(404) + if name.find('./') != -1 or not re.match(r"^[\w-]+$", name): + return abort(404) + if not name: + return public.returnJson( + False, 'Please pass in the plug-in name!'), json_header + p_path = public.get_plugin_path() + '/' + name + if not os.path.exists(p_path): + if name == 'btwaf' and fun == 'index': + pdata = {} + import panel_plugin_v2 + plu_panel = panel_plugin_v2.panelPlugin() + plugin_list = plu_panel.get_cloud_list() + if not 'pro' in plugin_list: plugin_list['pro'] = -1 + for p in plugin_list['list']: + if p['name'] in ['btwaf']: + if p['endtime'] != 0 and p['endtime'] < time.time(): + pdata['error_msg'] = 1 + break + return render_template('error3.html', data=pdata) + return abort(404) + + # 是否响插件应静态文件 + if fun == 'static': + if stype.find('./') != -1 or not os.path.exists(p_path + '/static'): + return abort(404) + s_file = p_path + '/static/' + stype + if s_file.find('..') != -1: return abort(404) + if not re.match(r"^[\w\./-]+$", s_file): return abort(404) + if not public.path_safe_check(s_file): return abort(404) + if not os.path.exists(s_file): return abort(404) + return send_file(s_file, conditional=True, etag=True) + + # 准备参数 + if not args: args = get_input() + args.client_ip = public.GetClientIp() + args.fun = fun + + # 初始化插件对象 + try: + is_php = os.path.exists(p_path + '/index.php') + if not is_php: + import panel_plugin_v2 + plu_panel = panel_plugin_v2.panelPlugin() + plugin_list = plu_panel.get_cloud_list() + waf = 0 + if not 'pro' in plugin_list: plugin_list['pro'] = -1 + for p in plugin_list['list']: + if p['name'] in ['btwaf']: + if p['endtime'] != 0 and p['endtime'] < time.time(): + waf = -1 + try: + public.package_path_append(p_path) + plugin_main = __import__(name + '_main') + if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list[ + 'pro'] == -1: + return render_template('error3.html', data={}) + except: + if name == 'btwaf' and fun == 'index' and waf == -1 and plugin_list[ + 'pro'] == -1: + return render_template('error3.html', data={}) + if os.path.exists("{}/btwaf".format(public.get_plugin_path())): + return render_template('error3.html', data={}) + try: + if sys.version_info[0] == 2: + reload(plugin_main) + else: + from imp import reload + reload(plugin_main) + except: + pass + # public.print_log("plugin_main22222: {}".format(plugin_main)) + + plu = eval('plugin_main.' + name + '_main()') + + # methods = dir(plu) + # # 遍历列表并打印每个方法的名称 + # for method in methods: + # # 排除以双下划线开头和结尾的特殊属性和方法 + # if not method.startswith("__") and not method.endswith("__"): + # public.print_log(method) + + if not hasattr(plu, fun): + + return public.returnJson(False, + 'Plugin does not exist'), json_header + + # 执行插件方法 + if not is_php: + if is_accept: + checks = plu._check(args) + if type(checks) != bool or not checks: + return public.getJson(checks), json_header + data = eval('plu.' + fun + '(args)') + else: + comReturn = comm.local() + if comReturn: return comReturn + import panel_php_v2 + args.s = fun + args.name = name + data = panel_php_v2.panelPHP(name).exec_php_script(args) + + r_type = type(data) + if r_type in [Response, Resp]: + return data + + # 处理响应 + if stype == 'json': # 响应JSON + return public.getJson(data), json_header + elif stype == 'html': # 使用模板 + t_path_root = p_path + '/templates/' + t_path = t_path_root + fun + '.html' + if not os.path.exists(t_path): + return public.returnJson( + False, + 'The specified template does not exist!'), json_header + t_body = public.readFile(t_path) + + # 处理模板包含 + rep = r'{%\s?include\s"(.+)"\s?%}' + includes = re.findall(rep, t_body) + for i_file in includes: + filename = p_path + '/templates/' + i_file + i_body = 'ERROR: File ' + filename + ' does not exists.' + if os.path.exists(filename): + i_body = public.readFile(filename) + t_body = re.sub(rep.replace('(.+)', i_file), i_body, t_body) + + return render_template_string(t_body, data=data) + else: # 直接响应插件返回值,可以是任意flask支持的响应类型 + r_type = type(data) + if r_type == dict: + if name == 'btwaf' and 'msg' in data: + return render_template('error3.html', + data={"error_msg": data['msg']}) + return public.returnJson( + False, + public.getMsg('Bad return type [{}]').format( + r_type)), json_header + return data + except: + return public.get_error_info() + return public.get_error_object(None, plugin_name=name) + + +@app.route(route_v2 + '/hook', methods=method_all) +def panel_hook_v2(): + # webhook接口 + get = get_input() + if not os.path.exists('plugin/webhook'): + return abort(404) + public.package_path_append('plugin/webhook') + import webhook_main + return public.getJson(webhook_main.webhook_main().RunHook(get)) + + +@app.route(route_v2 + '/install', methods=method_all) +def install_v2(): + # 初始化面板接口 + if public.is_spider(): return abort(404) + if not os.path.exists('install.pl'): return redirect('/login') + if public.M('config').where("id=?", ('1', )).getField('status') == 1: + if os.path.exists('install.pl'): os.remove('install.pl') + session.clear() + return redirect('/login') + ret_login = os.path.join('/', admin_path) + if admin_path == '/' or admin_path == '/bt': ret_login = '/login' + session['admin_path'] = False + session['login'] = False + if request.method == method_get[0]: + if not os.path.exists('install.pl'): return redirect(ret_login) + data = {} + data['status'] = os.path.exists('install.pl') + data['username'] = public.GetRandomString(8).lower() + return render_template('install.html', data=data) + + elif request.method == method_post[0]: + if not os.path.exists('install.pl'): return redirect(ret_login) + get = get_input() + if not hasattr(get, 'bt_username'): + return public.get_msg_gettext('The user name cannot be empty!') + if not get.bt_username: + return public.get_msg_gettext('The user name cannot be empty!') + if not hasattr(get, 'bt_password1'): + return public.get_msg_gettext('Password can not be blank!') + if not get.bt_password1: + return public.get_msg_gettext('Password can not be blank!') + if get.bt_password1 != get.bt_password2: + return public.get_msg_gettext( + 'The passwords entered twice do not match, please re-enter!') + public.M('users').where("id=?", (1, )).save( + 'username,password', + (get.bt_username, + public.password_salt(public.md5(get.bt_password1.strip()), + uid=1))) + os.remove('install.pl') + public.M('config').where("id=?", ('1', )).setField('status', 1) + data = {} + data['status'] = os.path.exists('install.pl') + data['username'] = get.bt_username + return render_template('install.html', data=data) + +# --------------------- websocket START -------------------------- # + + +@sockets.route(route_v2 + '/workorder_client') +def workorder_client_v2(ws): + comReturn = comm.local() + if comReturn: return comReturn + + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): + return + + import panelWorkorder + toObject = panelWorkorder.panelWorkorder() + get = get_input() + toObject.client(ws, get) + + +@sockets.route(route_v2 + '/ws_panel') +def ws_panel_v2(ws): + ''' + @name 面板接口ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + comReturn = comm.local() + if comReturn: return comReturn + + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + while True: + pdata = ws.receive() + if pdata == '{}': break + data = json.loads(pdata) + get = public.to_dict_obj(data) + get._ws = ws + p = threading.Thread(target=ws_panel_thread_v2, args=(get, )) + p.start() + + +def ws_panel_thread_v2(get): + ''' + @name 面板管理ws线程 + @author hwliang<2021-07-24> + @param get 请求参数 + @return void + ''' + + if not hasattr(get, 'ws_callback'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) + return + if not hasattr(get, 'mod_name'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'mod_name'))) + return + if not hasattr(get, 'def_name'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'def_name'))) + return + get.mod_name = get.mod_name.strip() + get.def_name = get.def_name.strip() + check_str = '{}{}'.format(get.mod_name, get.def_name) + if not re.match(r"^\w+$", check_str) or get.mod_name in [ + 'public', 'common', 'db', 'db_mysql', 'downloadFile', 'jobs' + ]: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Unsafe mod_name, def_name parameter content'))) + return + # if not hasattr(get, 'args'): + # get._ws.send(public.getJson(public.return_status_code(1001, 'args'))) + # return + + mod_file = '{}/{}.py'.format(public.get_class_path(), get.mod_name) + if not os.path.exists(mod_file): + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) + return + _obj = public.get_script_object(mod_file) + if not _obj: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, 'Specified module {} does not exist'.format( + get.mod_name)))) + return + _cls = getattr(_obj, get.mod_name) + if not _cls: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.mod_name)))) + return + _def = getattr(_cls(), get.def_name) + if not _def: + get._ws.send( + public.getJson( + public.return_status_code( + 1000, + 'The {} object was not found in the {} module'.format( + get.mod_name, get.def_name)))) + return + result = {'callback': get.ws_callback, 'result': _def(get)} + get._ws.send(public.getJson(result)) + + +@sockets.route(route_v2 + '/ws_project') +def ws_project_v2(ws): + ''' + @name 项目管理ws入口 + @author hwliang<2021-07-24> + @param ws websocket会话对像 + @return void + ''' + comReturn = comm.local() + if comReturn: return comReturn + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + from panelProjectControllerV2 import ProjectController + project_obj = ProjectController() + while True: + pdata = ws.receive() + if pdata in '{}': break + get = public.to_dict_obj(json.loads(pdata)) + get._ws = ws + p = threading.Thread(target=ws_project_thread_v2, + args=(project_obj, get)) + p.start() + + +def ws_project_thread_v2(_obj, get): + ''' + @name 项目管理ws线程 + @author hwliang<2021-07-24> + @param _obj 项目管理控制器对像 + @param get 请求参数 + @return void + ''' + if not hasattr(get, 'ws_callback'): + get._ws.send( + public.getJson(public.return_status_code(1001, 'ws_callback'))) + return + result = {'callback': get.ws_callback, 'result': _obj.model(get)} + get._ws.send(public.getJson(result)) + + +sock_pids = {} + + +@sockets.route(route_v2 + '/sock_shell') +def sock_shell_v2(ws): + ''' + @name 执行指定命令,实时输出命令执行结果 + @author hwliang<2021-07-19> + @return void + + 示例: + p = new WebSocket('ws://192.168.1.247:8888/sock_shell') + p.send('ping www.bt.cn -c 100') + ''' + comReturn = comm.local() + if comReturn: + ws.send(str(comReturn)) + return + kill_closed_v2() + get = ws.receive() + get = json.loads(get) + if not check_csrf_websocket(ws, get): return + + t = None + try: + while True: + cmdstring = ws.receive() + if cmdstring in ['stop', 'error'] or not cmdstring: + break + t = threading.Thread(target=sock_recv, args=(cmdstring, ws)) + t.start() + kill_closed_v2() + except: + kill_closed_v2() + + +def kill_closed_v2(): + ''' + @name 关闭已关闭的连接 + @author hwliang<2021-07-24> + @return void + ''' + global sock_pids + import psutil + pids = psutil.pids() + keys = sock_pids.copy().keys() + for pid in keys: + if hasattr(sock_pids[pid], 'closed'): + is_closed = sock_pids[pid].closed + else: + is_closed = not sock_pids[pid].connected + + logging.debug("PID: {} , sock_stat: {}".format(pid, is_closed)) + if not is_closed: continue + + if pid in pids: + try: + p = psutil.Process(pid) + for cp in p.children(): + cp.kill() + p.kill() + logging.debug("killed: {}".format(pid)) + sock_pids.pop(pid) + except: + pass + else: + sock_pids.pop(pid) + + +@app.route(route_v2 + '/close_sock_shell', methods=method_all) +def close_sock_shell_v2(): + ''' + @name 关闭指定命令 + @author hwliang<2021-07-19> + @param cmdstring 完整命令行 + @return dict + 示例: + $.post('/close_sock_shell',{cmdstring:'ping www.bt.cn -c 100'}) + ''' + comReturn = comm.local() + if comReturn: return comReturn + args = get_input() + if not check_csrf(): + return public.ReturnJson(False, 'INIT_CSRF_ERR'), json_header + cmdstring = args.cmdstring.strip() + skey = public.md5(cmdstring) + pid = cache.get(skey) + if not pid: + return json.dumps( + public.return_data( + False, [], error_msg='The specified sock has been terminated!') + ), json_header + os.kill(pid, 9) + cache.delete(skey) + return json.dumps(public.return_data(True, + 'Successful operation!')), json_header + + +@sockets.route(route_v2 + '/webssh') +def webssh_v2(ws): + # 宝塔终端连接 + comReturn = comm.local() + if comReturn: + ws.send(str(comReturn)) + return + if not ws: return 'False' + get = ws.receive() + if not get: return + get = json.loads(get) + if not check_csrf_websocket(ws, get): + return + + import ssh_terminal_v2 + sp = ssh_terminal_v2.ssh_host_admin() + if 'host' in get: + ssh_info = {} + ssh_info['host'] = get['host'].strip() + if 'port' in get: + ssh_info['port'] = int(get['port']) + if 'username' in get: + ssh_info['username'] = get['username'].strip() + if 'password' in get: + ssh_info['password'] = get['password'].strip() + if 'pkey' in get: + ssh_info['pkey'] = get['pkey'].strip() + + if get['host'] in ['127.0.0.1', 'localhost' + ] and 'port' not in ssh_info: + ssh_info = sp.get_ssh_info('127.0.0.1') + if not ssh_info: ssh_info = sp.get_ssh_info('localhost') + if not ssh_info: ssh_info = {"host": "127.0.0.1"} + ssh_info['port'] = public.get_ssh_port() + else: + ssh_info = sp.get_ssh_info('127.0.0.1') + if not ssh_info: ssh_info = sp.get_ssh_info('localhost') + if not ssh_info: ssh_info = {"host": "127.0.0.1"} + ssh_info['port'] = public.get_ssh_port() + + if not ssh_info['host'] in ['127.0.0.1', 'localhost']: + if not 'username' in ssh_info: + ssh_info = sp.get_ssh_info(ssh_info['host']) + if not ssh_info: + ws.send( + 'The specified host information is not found, please add it again!' + ) + return + p = ssh_terminal_v2.ssh_terminal() + p.run(ws, ssh_info) + del (p) + if ws.connected: + ws.close() + return 'False' + + +# --------------------- websocket END -------------------------- # + + +@app.route(route_v2 + "/daily", methods=method_all) +def daily_v2(): + """面板日报数据""" + + comReturn = comm.local() + if comReturn: return comReturn + + import panelDaily + toObject = panelDaily.panelDaily() + + defs = ("get_app_usage", "get_daily_data", "get_daily_list") + result = publicObject(toObject, defs) + return result + + +@app.route(route_v2 + '/phpmyadmin/', methods=method_all) +def pma_proxy_v2(path_full=None): + ''' + @name phpMyAdmin代理 + @author hwliang<2022-01-19> + @return Response + ''' + comReturn = comm.local() + if comReturn: return comReturn + cache_key = 'pmd_port_path' + pmd = cache.get(cache_key) + if not pmd: + pmd = get_phpmyadmin_dir() + if not pmd: + return 'phpMyAdmin is not installed, please go to the [App Store] page to install it!' + pmd = list(pmd) + cache.set(cache_key, pmd, 10) + panel_pool = 'http://' + if request.url_root[:5] == 'https': + panel_pool = 'https://' + import ajax + ssl_info = ajax.ajax().get_phpmyadmin_ssl(None) + if ssl_info['status']: + pmd[1] = ssl_info['port'] + else: + panel_pool = 'http://' + + proxy_url = '{}127.0.0.1:{}/{}/'.format( + panel_pool, pmd[1], pmd[0]) + request.full_path.replace( + '/phpmyadmin/', '') + from panel_http_proxy_v2 import HttpProxy + px = HttpProxy() + return px.proxy(proxy_url) + + +@app.route(route_v2 + '/p/', methods=method_all) +@app.route(route_v2 + '/p//', methods=method_all) +@app.route(route_v2 + '/p//', methods=method_all) +def proxy_port_v2(port, full_path=None): + ''' + @name 代理指定端口 + @author hwliang<2022-01-19> + @return Response + ''' + + comReturn = comm.local() + if comReturn: return comReturn + full_path = request.full_path.replace('/p/{}/'.format(port), + '').replace('/p/{}'.format(port), '') + uri = '{}/{}'.format(port, full_path) + uri = uri.replace('//', '/') + proxy_url = 'http://127.0.0.1:{}'.format(uri) + from panel_http_proxy_v2 import HttpProxy + px = HttpProxy() + return px.proxy(proxy_url) + + +@app.route(route_v2 + '/push', methods=method_all) +def push_v2(pdata=None): + comReturn = comm.local() + if comReturn: return comReturn + import panel_push_v2 + toObject = panel_push_v2.panelPush() + defs = ('set_push_status', 'get_push_msg_list', 'get_modules_list', + 'install_module', 'uninstall_module', 'get_module_template', + 'set_push_config', 'get_push_config', 'del_push_config', + 'get_module_logs', 'get_module_config', 'get_push_list', + 'get_push_logs') + result = publicObject(toObject, defs, None, pdata) + return result + + +@app.route('/hello/') +def test_regex_route(username: str): + return 'hi, '+username + + +@app.route('/v2/install_finish', methods=method_post) +def install_finish(): + with open('{}/data/install_finished.mark'.format(public.get_panel_path()), 'w') as fp: + fp.write('True') + return public.return_message(0, 0, 'Successfully') + + +@app.route('/v2/login_wp', methods=method_get) +def login_wp(): + admin_php = '/www/wwwroot/wp-study.aap/wp-admin/includes/admin.php' + + with open(admin_php, 'r') as fp: + admin_php_content = fp.read() + + if admin_php_content.find('''/** Automatic login administrator account */ +require_once ABSPATH . 'wp-admin/includes/auto-login.php';''') < 0: + with open(admin_php, 'a') as fp: + fp.write(''' +/** Automatic login administrator account */ +require_once ABSPATH . 'wp-admin/includes/auto-login.php'; +''') + + mark_file_name = public.GetRandomString(16) + mark_file = '/www/wwwroot/wp-study.aap/wp-admin/includes/{}.mark'.format(mark_file_name) + + with open(mark_file, 'w') as fp: + fp.write('True') + + user_id = 1 + token_key = public.GetRandomString(16) + token = public.GetRandomString(32) + + auto_login_wp = ''' 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; } op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + }; +var __spreadArray = + (this && this.__spreadArray) || + function (to, from, pack) { + if (pack || arguments.length === 2) + for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; + } } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -define(["require", "exports", "./snabbdom", "./public/public", "./panelConfig", "./safeConfig", "./noticeConfig"], function (require, exports, snabbdom_1, public_1, panelConfig_1, safeConfig_1, noticeConfig_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); + return to.concat(ar || Array.prototype.slice.call(from)); + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +define(['require', 'exports', './snabbdom', './public/public', './panelConfig', './safeConfig', './noticeConfig'], function ( + require, + exports, + snabbdom_1, + public_1, + panelConfig_1, + safeConfig_1, + noticeConfig_1 +) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); exports.Config = void 0; public_1 = __importDefault(public_1); panelConfig_1 = __importDefault(panelConfig_1); @@ -73,1099 +176,1164 @@ define(["require", "exports", "./snabbdom", "./public/public", "./panelConfig", var safeConfig = new safeConfig_1.default(); var noticeConfig = new noticeConfig_1.default(); var Config = (function (_super) { - __extends(Config, _super); - function Config() { - var _this = _super.call(this) || this; - _this.Info = {}; - _this.configInfo = {}; - _this.formInfo = {}; - _this.alertListModule = {}; - _this.panelSiteList = []; - _this.taskTypeList = [ - { title: 'Website certificate (SSL) expires', value: 'ssl', model: 'site_push' }, - { title: 'Website expiration', value: 'site_endtime', model: 'site_push' }, - { title: 'Panel password validity period', value: 'panel_pwd_endtime', model: 'site_push' }, - { title: 'Panel login alarm', value: 'panel_login', model: 'site_push' }, - { title: 'SSH login alarm', value: 'ssh_login', model: 'site_push' }, - { title: 'SSH login failure alarm', value: 'ssh_login_error', model: 'site_push' }, - { title: 'Panel security alarm', value: 'panel_safe_push', model: 'site_push' }, - // { title: 'Panel update reminder', value: 'panel_update', model: 'site_push' }, - ]; - _this.disabledOption = ['site_endtime', 'ssh_login', 'ssh_login_error', 'panel_login', 'panel_pwd_endtime', 'panel_safe_push', 'panel_update']; - _this.apiInfo = { - getConfig: ['config/get_config', lan.public.the], - getCheckTwoStep: ['config/check_two_step', lan.public.the], - getPasswordConfig: ['config/get_password_config', 'Getting the password complexity verification status, please wait...'], - getMenuList: ['config/get_menu_list', 'Getting panel menu bar, please wait...'], - getMessageChannel: ['config/get_msg_configs', 'Getting profile, please wait...'], - getLoginAlarm: ['config/get_login_send', 'Getting login information, please wait...'], - setMsgConfigmail: ['config/set_msg_config&name=mail', 'Setting recipient email'], - setPanelConfig: ['config/setPanel', lan.config.config_save], - }; - _this.alertConfigForm = [ - { - label: 'Task type', - group: { - type: 'select', - name: 'type', - width: '250px', - value: 'ssl', - class: 'projectBox', - list: _this.taskTypeList, - disabled: false, - change: function (formData, element, that) { - var config = _this.switchPushType(that.config.form, formData); - that.$again_render_form(config); - }, - }, + __extends(Config, _super); + function Config() { + var _this = _super.call(this) || this; + _this.Info = {}; + _this.configInfo = {}; + _this.formInfo = {}; + _this.alertListModule = {}; + _this.panelSiteList = []; + _this.taskTypeList = [ + { title: 'Website certificate (SSL) expires', value: 'ssl', model: 'site_push' }, + { title: 'Website expiration', value: 'site_endtime', model: 'site_push' }, + { title: 'Panel password validity period', value: 'panel_pwd_endtime', model: 'site_push' }, + { title: 'Panel login alarm', value: 'panel_login', model: 'site_push' }, + { title: 'SSH login alarm', value: 'ssh_login', model: 'site_push' }, + { title: 'SSH login failure alarm', value: 'ssh_login_error', model: 'site_push' }, + { title: 'Panel security alarm', value: 'panel_safe_push', model: 'site_push' }, + // { title: 'Panel update reminder', value: 'panel_update', model: 'site_push' }, + ]; + _this.disabledOption = ['site_endtime', 'ssh_login', 'ssh_login_error', 'panel_login', 'panel_pwd_endtime', 'panel_safe_push', 'panel_update']; + _this.apiInfo = { + getConfig: ['config/get_config', lan.public.the], + getCheckTwoStep: ['config/check_two_step', lan.public.the], + getPasswordConfig: ['config/get_password_config', 'Getting the password complexity verification status, please wait...'], + getMenuList: ['config/get_menu_list', 'Getting panel menu bar, please wait...'], + getMessageChannel: ['config/get_msg_configs', 'Getting profile, please wait...'], + getLoginAlarm: ['config/get_login_send', 'Getting login information, please wait...'], + setMsgConfigmail: ['config/set_msg_config&name=mail', 'Setting recipient email'], + setPanelConfig: ['config/setPanel', lan.config.config_save], + }; + _this.alertConfigForm = [ + { + label: 'Task type', + group: { + type: 'select', + name: 'type', + width: '250px', + value: 'ssl', + class: 'projectBox', + list: _this.taskTypeList, + disabled: false, + change: function (formData, element, that) { + var config = _this.switchPushType(that.config.form, formData); + that.$again_render_form(config); + }, + }, + }, + { + label: 'Website', + group: { + type: 'select', + name: 'site', + width: '250px', + value: '', + list: [], + }, + }, + { + label: 'Remaining days', + group: { + type: 'number', + name: 'cycle', + width: '70px', + unit: 'Day(s)', + value: 1, + }, + }, + { + label: 'Cycle', + hide: true, + group: [ + { + type: 'number', + name: 'where1', + width: '70px', + value: 30, + unit: 'Minute(s)
Frequency
', + input: function (data, b, c, d, e) { + var $input = $(e.currentTarget); + var num = $input.val(); + if (num < 0) { + $input.val(0); + num = 0; + } + var text = ''.concat(num, ' minute').concat(num > 1 ? 's' : ''); + $('.condition_tips').find('.minute').text(text); }, - { - label: 'Website', - group: { - type: 'select', - name: 'site', - width: '250px', - value: '', - list: [], - }, + }, + { + type: 'number', + name: 'count', + width: '50px', + style: { 'vertical-align': 'initial', 'margin-left': '10px' }, + value: 3, + unit: 'Time(s) ', + input: function (data, b, c, d, e) { + var $input = $(e.currentTarget); + var num = $input.val(); + if (num < 0) { + $input.val(0); + num = 0; + } + var text = ''.concat(num, ' time').concat(num > 1 ? 's' : ''); + $('.condition_tips').find('.time').text(text); }, - { - label: 'Remaining days', - group: { - type: 'number', - name: 'cycle', - width: '70px', - unit: 'Day(s)', - value: 1, - }, - }, - { - label: 'Cycle', - hide: true, - group: [ - { - type: 'number', - name: 'where1', - width: '70px', - value: 30, - unit: 'Minute(s)
Frequency
', - input: function (data, b, c, d, e) { - var $input = $(e.currentTarget); - var num = $input.val(); - if (num < 0) { - $input.val(0); - num = 0; - } - var text = "".concat(num, " minute").concat(num > 1 ? 's' : ''); - $('.condition_tips').find('.minute').text(text); - }, - }, - { - type: 'number', - name: 'count', - width: '50px', - style: { 'vertical-align': 'initial', 'margin-left': '10px' }, - value: 3, - unit: 'Time(s) ', - input: function (data, b, c, d, e) { - var $input = $(e.currentTarget); - var num = $input.val(); - if (num < 0) { - $input.val(0); - num = 0; - } - var text = "".concat(num, " time").concat(num > 1 ? 's' : ''); - $('.condition_tips').find('.time').text(text); - }, - }, - { - type: 'div', - dispaly: 'block', - class: 'condition_tips', - style: { 'margin-top': '10px', color: '#666' }, - content: 'Login failed 3 times within 30 minutes', - }, - ], - }, - { - label: 'Interval', - group: { - type: 'number', - name: 'interval', - width: '70px', - value: 600, - unit: 'second(s)
Monitor the trigger condition again after an interval of 600 seconds
', - input: function (data, b, c, d, e) { - var $input = $(e.currentTarget); - var num = $input.val(); - if (num < 0) { - $input.val(0); - num = 0; - } - var text = "".concat(num, " second").concat(num > 1 ? 's' : ''); - $input.next().find('.count').text(text); - }, - }, - }, - { - label: 'Send times', - group: { - type: 'number', - name: 'push_count', - width: '70px', - value: 1, - unit: 'Time(s)
After sending 1 time, no more alarm messages will be sent,
if you want to send multiple times, please fill in more than 2 times.
', - input: function (data, b, c, d, e) { - var $input = $(e.currentTarget); - var num = $input.val(); - if (num < 0) { - $input.val(0); - num = 0; - } - var text = "".concat(num, " time").concat(num > 1 ? 's' : ''); - $input.next().find('.count').text(text); - }, - }, - }, - { - label: 'Alarm mode', - group: [], - }, - { - label: 'Alarm content', - hide: true, - group: { - type: 'help', - style: { 'margin-top': '6px' }, - list: ['panel user change, panel log delete, panel open developer, panel open API'], - }, - }, - { - label: '', - group: { - type: 'button', - name: 'submitForm', - title: 'Add task', - event: function (formData, element, that) { - that.submit(formData); - }, - }, - }, - ]; - _this.init(); - return _this; + }, + { + type: 'div', + dispaly: 'block', + class: 'condition_tips', + style: { 'margin-top': '10px', color: '#666' }, + content: 'Login failed 3 times within 30 minutes', + }, + ], + }, + { + label: 'Interval', + group: { + type: 'number', + name: 'interval', + width: '70px', + value: 600, + unit: 'second(s)
Monitor the trigger condition again after an interval of 600 seconds
', + input: function (data, b, c, d, e) { + var $input = $(e.currentTarget); + var num = $input.val(); + if (num < 0) { + $input.val(0); + num = 0; + } + var text = ''.concat(num, ' second').concat(num > 1 ? 's' : ''); + $input.next().find('.count').text(text); + }, + }, + }, + { + label: 'Send times', + group: { + type: 'number', + name: 'push_count', + width: '70px', + value: 1, + unit: 'Time(s)
After sending 1 time, no more alarm messages will be sent,
if you want to send multiple times, please fill in more than 2 times.
', + input: function (data, b, c, d, e) { + var $input = $(e.currentTarget); + var num = $input.val(); + if (num < 0) { + $input.val(0); + num = 0; + } + var text = ''.concat(num, ' time').concat(num > 1 ? 's' : ''); + $input.next().find('.count').text(text); + }, + }, + }, + { + label: 'Alarm mode', + group: [], + }, + { + label: 'Alarm content', + hide: true, + group: { + type: 'help', + style: { 'margin-top': '6px' }, + list: ['panel user change, panel log delete, panel open developer, panel open API'], + }, + }, + { + label: '', + group: { + type: 'button', + name: 'submitForm', + title: 'Add task', + event: function (formData, element, that) { + that.submit(formData); + }, + }, + }, + ]; + _this.init(); + return _this; + } + Config.prototype.init = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + this.$apiInit(this.apiInfo); + this.render(); + this.event(); + return [2]; + }); + }); + }; + Config.prototype.render = function () { + var _this = this; + var loadT = this.$load(lan.public.the); + Promise.all([ + this.$request('getConfig', false), + this.$request('getCheckTwoStep', { loading: false, msg: false }), + this.$request('getPasswordConfig', { loading: false, msg: false }), + this.$request('getUserInfo', { loading: false, msg: false }), + this.$request('getMessageChannel', { loading: false, msg: false }), + this.$request('getLoginAlarm', { loading: false, msg: false }), + this.$request('getMenuList', { loading: false, msg: false }), + ]) + .then(function (resArr) { + var configInfo = resArr[0], + twoStep = resArr[1], + pawComplexity = resArr[2], + bindUserInfo = resArr[3], + messageChannelInfo = resArr[4], + loginAlarmInfo = resArr[5], + menuList = resArr[6]; + panelConfig.init({ configInfo: configInfo, menuList: menuList, bindUserInfo: bindUserInfo }); + safeConfig.init({ configInfo: configInfo, twoStep: twoStep, pawComplexity: pawComplexity }); + noticeConfig.init({ messageChannelInfo: messageChannelInfo, loginAlarmInfo: loginAlarmInfo }); + }) + .catch(function (err) { + console.log(err); + _this.$error(err.msg || 'Server Error'); + }) + .finally(function () { + loadT.close(); + }); + }; + Config.prototype.event = function () { + var _this = this; + $('#configTab').on('click', '.tabs-item', function (ev) { + var el = $(ev.currentTarget); + var type = el.attr('data-type'); + el.addClass('active').siblings().removeClass('active'); + $('.configure-box .panel-config').addClass('hide'); + if (type === 'allConfig') { + $('.configure-box .panel-config:not(.alert-view-box)').removeClass('hide'); + } else { + if (type === 'alertConfig') _this.renderAlertView(); + $('.configure-box .panel-config[data-type="' + type + '"]').removeClass('hide'); + } + _this.$setCookie('config-tab', type); + }); + this.cateClick(); + $('input[type="text"]').on('input', function (ev) { + return __awaiter(_this, void 0, void 0, function () { + var el, value, oldValue; + return __generator(this, function (_a) { + el = $(ev.target); + value = el.val(); + oldValue = el.attr('value'); + value != oldValue ? el.parent().next().removeAttr('disabled') : el.parent().next().attr('disabled', 'disabled'); + return [2]; + }); + }); + }); + $('.savePanelConfig').click(function () { + return __awaiter(_this, void 0, void 0, function () { + var data, res, href; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + data = this.getInputData(); + return [4, this.$request('setPanelConfig', data)]; + case 1: + res = _a.sent(); + href = ''; + if (data.domain) { + href = window.location.protocol + '//' + data.domain + ':' + window.location.port + window.location.pathname; + } else { + href = window.location.protocol + '//' + data.address + ':' + window.location.port + window.location.pathname; + } + res.status && this.$refreshBrowser(); + return [2]; + } + }); + }); + }); + $('.setPanelPort').click(function () { + return _this.setPanelPortView(); + }); + $('#addAlertTask').on('click', '.alertInstall', function (ev) { + var _type = $(ev.currentTarget).parent('span').siblings('input').attr('name'); + _this.setAlertConfigType(_type); + }); + + setTimeout(function () { + $.fn.serializeObject = function () { + var hasOwnProperty = Object.prototype.hasOwnProperty; + return this.serializeArray().reduce(function (data, pair) { + if (!hasOwnProperty.call(data, pair.name)) { + data[pair.name] = pair.value; + } + return data; + }, {}); + }; + }, 300); + + panelConfig.event(); + safeConfig.event(); + noticeConfig.event(); + }; + Config.prototype.cateClick = function () { + var configTab = this.$getCookie('config-tab') || 'allConfig'; + if (!isNaN(Number(configTab))) { + configTab = 'allConfig'; } - Config.prototype.init = function () { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - this.$apiInit(this.apiInfo); - this.render(); - this.event(); - return [2]; - }); - }); - }; - Config.prototype.render = function () { - var _this = this; - var loadT = this.$load(lan.public.the); - Promise.all([ - this.$request('getConfig', false), - this.$request('getCheckTwoStep', { loading: false, msg: false }), - this.$request('getPasswordConfig', { loading: false, msg: false }), - this.$request('getUserInfo', { loading: false, msg: false }), - this.$request('getMessageChannel', { loading: false, msg: false }), - this.$request('getLoginAlarm', { loading: false, msg: false }), - this.$request('getMenuList', { loading: false, msg: false }), - ]) - .then(function (resArr) { - var configInfo = resArr[0], twoStep = resArr[1], pawComplexity = resArr[2], bindUserInfo = resArr[3], messageChannelInfo = resArr[4], loginAlarmInfo = resArr[5], menuList = resArr[6]; - panelConfig.init({ configInfo: configInfo, menuList: menuList, bindUserInfo: bindUserInfo }); - safeConfig.init({ configInfo: configInfo, twoStep: twoStep, pawComplexity: pawComplexity }); - noticeConfig.init({ messageChannelInfo: messageChannelInfo, loginAlarmInfo: loginAlarmInfo }); - }) - .catch(function (err) { - console.log(err); - _this.$error(err.msg || 'Server Error'); - }) - .finally(function () { - loadT.close(); - }); - }; - Config.prototype.event = function () { - var _this = this; - $('#configTab').on('click', '.tabs-item', function (ev) { - var el = $(ev.currentTarget); - var type = el.attr('data-type'); - el.addClass('active').siblings().removeClass('active'); - $('.configure-box .panel-config').addClass('hide'); - if (type === 'allConfig') { - $('.configure-box .panel-config:not(.alert-view-box)').removeClass('hide'); - } - else { - if (type === 'alertConfig') - _this.renderAlertView(); - $('.configure-box .panel-config[data-type="' + type + '"]').removeClass('hide'); - } - _this.$setCookie('config-tab', type); - }); - this.cateClick(); - $('input[type="text"]').on('input', function (ev) { return __awaiter(_this, void 0, void 0, function () { - var el, value, oldValue; - return __generator(this, function (_a) { - el = $(ev.target); - value = el.val(); - oldValue = el.attr('value'); - value != oldValue ? el.parent().next().removeAttr('disabled') : el.parent().next().attr('disabled', 'disabled'); - return [2]; - }); - }); }); - $('.savePanelConfig').click(function () { return __awaiter(_this, void 0, void 0, function () { - var data, res, href; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - data = this.getInputData(); - return [4, this.$request('setPanelConfig', data)]; - case 1: - res = _a.sent(); - href = ''; - if (data.domain) { - href = window.location.protocol + '//' + data.domain + ':' + window.location.port + window.location.pathname; - } - else { - href = window.location.protocol + '//' + data.address + ':' + window.location.port + window.location.pathname; - } - res.status && this.$refreshBrowser(href); - return [2]; + $('#configTab .tabs-item[data-type="' + configTab + '"]').trigger('click'); + }; + Config.prototype.setPanelPortView = function () { + var _this = this; + var $input = $('input[name="port"]'); + var port = $input.val(); + this.$open({ + title: 'Change Panel Port', + area: ['380px', '380px'], + btn: ['Confirm', 'Cancel'], + content: { + data: { port: port, agreement: false }, + template: function () { + return (0, snabbdom_1.jsx)( + 'div', + { class: this.$class('pd20 bt-form') }, + this.$ul({ className: 'explainDescribeList', style: 'margin-top:0;' }, [ + ['1. Have a security group server, please release the new port in the security group in advance.', 'red'], + ['2. If the panel is inaccessible after modifying the port, change the original port to the SSH command line by using the bt command.', 'red'], + ]), + this.$line({ title: 'Port', width: '60px' }, this.$input({ model: 'port', width: '210px' })), + this.$learnMore({ + title: (0, snabbdom_1.jsx)( + 'span', + null, + 'I already understand, ', + this.$link({ title: 'How to release the port?', href: 'https://forum.aapanel.com/d/599-how-to-release-the-aapanel-port' }) + ), + model: 'agreement', + id: 'checkPanelPort', + }) + ); + }, + }, + yes: function (content) { + return __awaiter(_this, void 0, void 0, function () { + var close, vm, port, data, rdata; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + (close = content.close), (vm = content.vm), (port = parseInt(vm.port)); + if (!vm.agreement) return [2, this.$tips({ el: '#checkPanelPort', msg: 'Please tick the one I already know' })]; + return [4, this.$verifySubmit(!this.$checkPort(port), 'Please enter correct panel port!')]; + case 1: + _a.sent(); + data = this.getInputData(); + data.port = port; + return [4, this.$request('setPanelConfig', data)]; + case 2: + rdata = _a.sent(); + if (rdata.status) { + close(); + this.$refreshBrowser(''.concat(location.protocol, '//').concat(location.hostname, ':').concat(port).concat(location.pathname)); } - }); - }); }); - $('.setPanelPort').click(function () { return _this.setPanelPortView(); }); - $('#addAlertTask').on('click', '.alertInstall', function (ev) { - var _type = $(ev.currentTarget).parent('span').siblings('input').attr('name'); - _this.setAlertConfigType(_type); + return [2]; + } + }); }); - - setTimeout(function () { - $.fn.serializeObject = function () { - var hasOwnProperty = Object.prototype.hasOwnProperty; - return this.serializeArray().reduce(function (data, pair) { - if (!hasOwnProperty.call(data, pair.name)) { - data[pair.name] = pair.value; - } - return data; - }, {}); - }; - }, 300); - - panelConfig.event(); - safeConfig.event(); - noticeConfig.event(); - }; - Config.prototype.cateClick = function () { - var configTab = this.$getCookie('config-tab') || 'allConfig'; - if (!isNaN(Number(configTab))) { - configTab = 'allConfig'; + }, + }).catch(function (err) {}); + }; + Config.prototype.getInputData = function () { + var data = {}; + $('.savePanelConfig').each(function (index, item) { + var $input = $(item).parents('.line').find('input[type="text"]'); + var key = $input.attr('name'); + var value = $input.val(); + data[key] = value; + }); + return data; + }; + Config.renderFormColumn = function (configInfo) { + for (var key in configInfo) { + if (Object.prototype.hasOwnProperty.call(configInfo, key)) { + var value = configInfo[key].value; + var el = $('input[name="' + key + '"]'); + var type = el.attr('type'); + if (type === 'checkbox') { + el.prop('checked', value); + } else { + el.val(value); } - $('#configTab .tabs-item[data-type="' + configTab + '"]').trigger('click'); - }; - Config.prototype.setPanelPortView = function () { - var _this = this; - var $input = $('input[name="port"]'); - var port = $input.val(); - this.$open({ - title: 'Change Panel Port', - area: ['380px', '380px'], - btn: ['Confirm', 'Cancel'], - content: { - data: { port: port, agreement: false }, - template: function () { - return ((0, snabbdom_1.jsx)("div", { class: this.$class('pd20 bt-form') }, - this.$ul({ className: 'explainDescribeList', style: 'margin-top:0;' }, [ - ['1. Have a security group server, please release the new port in the security group in advance.', 'red'], - ['2. If the panel is inaccessible after modifying the port, change the original port to the SSH command line by using the bt command.', 'red'], - ]), - this.$line({ title: 'Port', width: '60px' }, this.$input({ model: 'port', width: '210px' })), - this.$learnMore({ - title: (0, snabbdom_1.jsx)("span", null, - "I already understand, ", - this.$link({ title: 'How to release the port?', href: 'https://forum.aapanel.com/d/599-how-to-release-the-aapanel-port' })), - model: 'agreement', - id: 'checkPanelPort', - }))); - }, + } + } + }; + Config.prototype.renderAlertView = function () { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + this.alertTaskList(); + $('.alert-view-box') + .unbind('click') + .on('click', '.tab-nav-border span', function (ev) { + var el = $(ev.currentTarget), + index = $(el).index(); + $(el).addClass('on').siblings().removeClass('on'); + $(el).parent().next().find('.tab-block').eq(index).addClass('on').siblings().removeClass('on'); + switch (index) { + case 0: + _this.alertTaskList(); + break; + case 1: + _this.alertConfigTable(); + break; + case 2: + _this.alertLogsTable(); + break; + } + }); + return [2]; + }); + }); + }; + Config.prototype.alertTaskList = function () { + return __awaiter(this, void 0, void 0, function () { + var ChannelMessage, resetChannelMessage, prevArray, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; + case 1: + ChannelMessage = _b.sent(); + resetChannelMessage = []; + prevArray = []; + Object.getOwnPropertyNames(ChannelMessage).forEach(function (key) { + var mod = ChannelMessage[key]; + key == 'wx_account' ? prevArray.push(mod) : resetChannelMessage.push(mod); + }); + this.alertListModule = __spreadArray(__spreadArray([], prevArray, true), resetChannelMessage, true); + _a = this; + return [4, this.$request(['crontab/GetDataList'], { type: 'sites' })]; + case 2: + _a.panelSiteList = _b.sent(); + return [4, this.addAlertTask()]; + case 3: + _b.sent(); + this.renderAlarmList(); + return [2]; + } + }); + }); + }; + Config.prototype.addAlertTask = function () { + return __awaiter(this, void 0, void 0, function () { + var _config; + var _this = this; + return __generator(this, function (_a) { + _config = this.switchPushType(__spreadArray([], this.alertConfigForm, true)); + bt_tools.form({ + el: '#addAlertTask', + form: _config, + submit: function (formData) { + _this.setAlertConfigTask(formData); + }, + }); + return [2]; + }); + }); + }; + Config.prototype.renderAlarmList = function () { + var _this = this; + $('#alertList').empty(); + var alertListTabel = bt_tools.table({ + el: '#alertList', + url: '/push?action=get_push_list', + default: 'The alarm list is empty', + autoHeight: true, + height: 320, + dataFilter: function (res) { + $.each(res.site_push, function (index, item) { + item['id'] = index; + }); + var data = Object.values(res.site_push || []); + return { data: data }; + }, + column: [ + { + type: 'checkbox', + width: 20, + }, + { + fid: 'title', + title: 'Title', + type: 'text', + template: function (row) { + var _title = ''; + switch (row.type) { + case 'ssl': + _title = '['.concat(row.project == 'all' ? 'All' : row.project, ']').concat(row.title); + break; + default: + _title = row.title; + break; + } + return ''.concat(_title, ''); + }, + }, + { + fid: 'status', + title: 'Status', + config: { + icon: true, + list: [ + [true, 'Normal', 'bt_success', 'glyphicon-play'], + [false, 'Suspend', 'bt_danger', 'glyphicon-pause'], + ], + }, + type: 'status', + event: function (row) { + return __awaiter(_this, void 0, void 0, function () { + var param, eData, rdata; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (row.type == 'ssh_login') return [2, layer.msg('Do not support suspend SSH login alarm, if you want to stop it, please delete it directly.', { icon: 0 })]; + (param = {}), (eData = $.extend(true, row, { status: row.status ? false : true })); + param['name'] = row.module_type; + param['id'] = row.id; + param['data'] = JSON.stringify(eData); + return [4, this.$request(['push/set_push_config', 'Setting alarm tasks'], param)]; + case 1: + rdata = _a.sent(); + if (!rdata.status) return [3, 3]; + return [4, this.alertTaskList()]; + case 2: + _a.sent(); + _a.label = 3; + case 3: + return [2]; + } + }); + }); + }, + }, + { + title: 'Alarm mode', + type: 'text', + width: 265, + template: function (row) { + var alertMode = row.module.split(','), + _mode = ''; + _this.alertListModule.forEach(function (mod) { + if ($.inArray(mod.name, alertMode) >= 0) _mode += mod.title + ','; + }); + _mode = _mode.substring(0, _mode.length - 1); + return '' + _mode + ''; + }, + }, + { + fid: 'cycle', + title: 'Alarm condition', + template: function (row) { + switch (row.type) { + case 'ssl': + case 'site_endtime': + case 'panel_pwd_endtime': + return 'Less than ' + .concat(row.cycle, ' days remaining ') + .concat(typeof row.push_count != 'undefined' ? '(If not processed, it will be resent 1 time the next day for ' + row.push_count + ' days)' : '', ''); + case 'ssh_login_error': + return 'Triggered by ' + .concat(row.count, ' consecutive failed login attempts within ') + .concat(row.cycle, ' minutes, to be detected again after every ') + .concat(row.interval, ' seconds'); + case 'panel_update': + return 'Send a notification when a new version is detected'; + default: + return '--'; + } + }, + }, + { + title: lan.public.operate, + type: 'group', + width: 150, + align: 'right', + group: [ + { + title: lan.public.edit, + event: function (row) { + _this.setAlertTaskConfig(row); + }, }, - yes: function (content) { return __awaiter(_this, void 0, void 0, function () { - var close, vm, port, data, rdata; - return __generator(this, function (_a) { + { + title: lan.public.del, + event: function (row) { + return __awaiter(_this, void 0, void 0, function () { + var rdata; + return __generator(this, function (_a) { switch (_a.label) { - case 0: - close = content.close, vm = content.vm, port = parseInt(vm.port); - if (!vm.agreement) - return [2, this.$tips({ el: '#checkPanelPort', msg: 'Please tick the one I already know' })]; - return [4, this.$verifySubmit(!this.$checkPort(port), 'Please enter correct panel port!')]; - case 1: - _a.sent(); - data = this.getInputData(); - data.port = port; - return [4, this.$request('setPanelConfig', data)]; - case 2: - rdata = _a.sent(); - if (rdata.status) { - close(); - this.$refreshBrowser("".concat(location.protocol, "//").concat(location.hostname, ":").concat(port).concat(location.pathname)); - } - return [2]; - } - }); - }); }, - }).catch(function (err) { }); - }; - Config.prototype.getInputData = function () { - var data = {}; - $('.savePanelConfig').each(function (index, item) { - var $input = $(item).parents('.line').find('input[type="text"]'); - var key = $input.attr('name'); - var value = $input.val(); - data[key] = value; - }); - return data; - }; - Config.renderFormColumn = function (configInfo) { - for (var key in configInfo) { - if (Object.prototype.hasOwnProperty.call(configInfo, key)) { - var value = configInfo[key].value; - var el = $('input[name="' + key + '"]'); - var type = el.attr('type'); - if (type === 'checkbox') { - el.prop('checked', value); - } - else { - el.val(value); - } - } - } - }; - Config.prototype.renderAlertView = function () { - return __awaiter(this, void 0, void 0, function () { - var _this = this; - return __generator(this, function (_a) { - this.alertTaskList(); - $('.alert-view-box') - .unbind('click') - .on('click', '.tab-nav-border span', function (ev) { - var el = $(ev.currentTarget), index = $(el).index(); - $(el).addClass('on').siblings().removeClass('on'); - $(el).parent().next().find('.tab-block').eq(index).addClass('on').siblings().removeClass('on'); - switch (index) { - case 0: - _this.alertTaskList(); - break; - case 1: - _this.alertConfigTable(); - break; - case 2: - _this.alertLogsTable(); - break; - } - }); - return [2]; - }); - }); - }; - Config.prototype.alertTaskList = function () { - return __awaiter(this, void 0, void 0, function () { - var ChannelMessage, resetChannelMessage, prevArray, _a; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; - case 1: - ChannelMessage = _b.sent(); - resetChannelMessage = []; - prevArray = []; - Object.getOwnPropertyNames(ChannelMessage).forEach(function (key) { - var mod = ChannelMessage[key]; - key == 'wx_account' ? prevArray.push(mod) : resetChannelMessage.push(mod); - }); - this.alertListModule = __spreadArray(__spreadArray([], prevArray, true), resetChannelMessage, true); - _a = this; - return [4, this.$request(['crontab/GetDataList'], { type: 'sites' })]; - case 2: - _a.panelSiteList = _b.sent(); - return [4, this.addAlertTask()]; - case 3: - _b.sent(); - this.renderAlarmList(); - return [2]; - } - }); - }); - }; - Config.prototype.addAlertTask = function () { - return __awaiter(this, void 0, void 0, function () { - var _config; - var _this = this; - return __generator(this, function (_a) { - _config = this.switchPushType(__spreadArray([], this.alertConfigForm, true)); - bt_tools.form({ - el: '#addAlertTask', - form: _config, - submit: function (formData) { - _this.setAlertConfigTask(formData); - }, - }); - return [2]; - }); - }); - }; - Config.prototype.renderAlarmList = function () { - var _this = this; - $('#alertList').empty(); - var alertListTabel = bt_tools.table({ - el: '#alertList', - url: '/push?action=get_push_list', - default: 'The alarm list is empty', - autoHeight: true, - height: 320, - dataFilter: function (res) { - $.each(res.site_push, function (index, item) { - item['id'] = index; - }); - var data = Object.values(res.site_push || []); - return { data: data }; - }, - column: [ - { - type: 'checkbox', - width: 20, - }, - { - fid: 'title', - title: 'Title', - type: 'text', - template: function (row) { - var _title = ''; - switch (row.type) { - case 'ssl': - _title = "[".concat(row.project == 'all' ? 'All' : row.project, "]").concat(row.title); - break; - default: - _title = row.title; - break; - } - return "".concat(_title, ""); - }, - }, - { - fid: 'status', - title: 'Status', - config: { - icon: true, - list: [ - [true, 'Normal', 'bt_success', 'glyphicon-play'], - [false, 'Suspend', 'bt_danger', 'glyphicon-pause'], - ], - }, - type: 'status', - event: function (row) { return __awaiter(_this, void 0, void 0, function () { - var param, eData, rdata; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - if (row.type == 'ssh_login') - return [2, layer.msg('Do not support suspend SSH login alarm, if you want to stop it, please delete it directly.', { icon: 0 })]; - param = {}, eData = $.extend(true, row, { status: row.status ? false : true }); - param['name'] = row.module_type; - param['id'] = row.id; - param['data'] = JSON.stringify(eData); - return [4, this.$request(['push/set_push_config', 'Setting alarm tasks'], param)]; - case 1: - rdata = _a.sent(); - if (!rdata.status) return [3, 3]; - return [4, this.alertTaskList()]; - case 2: - _a.sent(); - _a.label = 3; - case 3: return [2]; - } - }); - }); }, - }, - { - title: 'Alarm mode', - type: 'text', - width: 265, - template: function (row) { - var alertMode = row.module.split(','), _mode = ''; - _this.alertListModule.forEach(function (mod) { - if ($.inArray(mod.name, alertMode) >= 0) - _mode += mod.title + ','; - }); - _mode = _mode.substring(0, _mode.length - 1); - return '' + _mode + ''; - }, - }, - { - fid: 'cycle', - title: 'Alarm condition', - template: function (row) { - switch (row.type) { - case 'ssl': - case 'site_endtime': - case 'panel_pwd_endtime': - return "Less than ".concat(row.cycle, " days remaining ").concat(typeof row.push_count != 'undefined' ? '(If not processed, it will be resent 1 time the next day for ' + row.push_count + ' days)' : '', ""); - case 'ssh_login_error': - return "Triggered by ".concat(row.count, " consecutive failed login attempts within ").concat(row.cycle, " minutes, to be detected again after every ").concat(row.interval, " seconds"); - case 'panel_update': - return "Send a notification when a new version is detected"; - default: - return "--"; - } - }, - }, - { - title: lan.public.operate, - type: 'group', - width: 150, - align: 'right', - group: [ - { - title: lan.public.edit, - event: function (row) { - _this.setAlertTaskConfig(row); - }, - }, - { - title: lan.public.del, - event: function (row) { return __awaiter(_this, void 0, void 0, function () { - var rdata; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4, this.$confirm({ - title: 'Delete Alarm Tasks', - msg: 'Delete will no longer alert this task, do you want to continue?', - })]; - case 1: - _a.sent(); - return [4, this.$request(['push/del_push_config', 'Deleting the alarm task'], { name: row.module_type, id: row.id })]; - case 2: - rdata = _a.sent(); - if (!rdata.status) return [3, 4]; - return [4, this.alertTaskList()]; - case 3: - _a.sent(); - _a.label = 4; - case 4: return [2]; - } - }); - }); }, - }, - ], - }, - ], - tootls: [ - { - type: 'batch', - positon: ['left', 'bottom'], - config: { - title: ' Delete', - url: 'push?action=del_push_config', - load: true, - param: function (row) { - return { name: row.module_type, id: row.id }; - }, - callback: function (that) { - bt.confirm({ title: 'Batch Delete Tasks', msg: 'The batch deletion will not be recovered, does it continue?', icon: 0 }, function (index) { - layer.close(index); - that.start_batch({}, function (list) { - var html = ''; - for (var i = 0; i < list.length; i++) { - var item = list[i]; - html += - '' + - (typeof item.project == 'undefined' ? item.title : (item.project == 'all' ? 'All' : item.project) + item.title) + - '
' + - item.request.msg + - '
'; - } - alertListTabel.$batch_success_table({ title: 'Batch Delete Tasks', th: 'Task title', html: html }); - alertListTabel.$refresh_table_list(true); - }); - }); - }, - }, - }, - ], - }); - }; - Config.prototype.setAlertTaskConfig = function (row) { - return __awaiter(this, void 0, void 0, function () { - var _config; - var _this = this; - return __generator(this, function (_a) { - _config = this.switchPushType($.extend(true, {}, this.alertConfigForm), row); - _config[0].group.disabled = true; - _config[0].group.unit = ''; - _config[8].hide = true; - if (row.type == 'ssh_login_error') { - row.where1 = row.cycle; - } - bt_tools.open({ - type: 1, - title: 'Edit Alert Tasks', - area: '540px', - skin: 'panel_alert_task_view', - btn: [lan.public.save, lan.public.cancel], - content: { - class: 'pd15', - data: row, - form: _config, - }, - success: function (layers) { - $(layers) - .find('.layui-layer-content') - .css('overflow', window.innerHeight > $(layers).height() ? 'inherit' : 'auto'); - $('.alertInstall').click(function (ev) { - var _type = $(ev.currentTarget).parent('span').siblings('input').attr('name'); - _this.setAlertConfigType(_type); - }); - }, - yes: function (formData, index) { - _this.setAlertConfigTask($.extend(true, {}, row, formData), index); - }, - }); - return [2]; - }); - }); - }; - Config.prototype.setAlertConfigTask = function (row, close) { - if (close === void 0) { close = null; } - return __awaiter(this, void 0, void 0, function () { - var _configD, eData, pushType, otherType, isCheck, rdata; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _configD = {}, eData = {}, pushType = this.taskTypeList.find(function (el) { return el.value == row.type; }), otherType = this.disabledOption; - _configD['name'] = typeof row.module_type !== 'undefined' ? row.module_type : pushType.model; - _configD['id'] = row.id ? row.id : $.inArray(row.type, otherType) >= 0 ? row.type : new Date().getTime(); - eData['interval'] = 600; - switch (row.type) { - case 'ssl': - case 'site_endtime': - case 'panel_pwd_endtime': - if (row.type == 'ssl') - eData['project'] = row.site || 'all'; - if (row.cycle == '' || row.cycle < 0) - return [2, this.$msg({ icon: 2, msg: 'Remaining days cannot be less than 1', time: 0, closeBtn: 2 })]; - eData['cycle'] = Number(row.cycle); - eData['push_count'] = Number(row.push_count); - break; - case 'ssh_login_error': - if (row.where1 == '' || row.where1 <= 0) - return [2, this.$msg({ icon: 2, msg: 'Trigger time cannot be less than 1', time: 0, closeBtn: 2 })]; - if (row.count == '' || row.count <= 0) - return [2, this.$msg({ icon: 2, msg: 'Trigger times cannot be less than 1', time: 0, closeBtn: 2 })]; - if (row.interval == '' || row.interval <= 0) - return [2, this.$msg({ icon: 2, msg: 'Interval cannot be less than 1', time: 0, closeBtn: 2 })]; - eData['cycle'] = Number(row.where1); - eData['count'] = Number(row.count); - eData['interval'] = Number(row.interval); - break; - } - isCheck = []; - $((row.id ? '.panel_alert_task_view ' : '#addAlertTask ') + '.module-check') - .not('.check_disabled') - .each(function () { - if ($(this).find('input').prop('checked')) { - isCheck.push($(this).find('input').prop('name')); - } - }); - eData['type'] = row.type; - eData['module'] = isCheck.join(); - if (typeof eData['push_count'] != 'undefined' && (eData['push_count'] <= 0 || eData['push_count'] == '')) { - this.$msg({ icon: 2, msg: 'The number of sending cannot be less than 1', time: 0, closeBtn: 2 }); - return [2, false]; - } - if (!eData['module']) { - this.$msg({ icon: 2, msg: 'Please select an alarm mode', time: 0, closeBtn: 2 }); - return [2, false]; - } - eData['status'] = typeof row.status !== 'undefined' ? row.status : true; - eData['title'] = $((row.id ? '.panel_alert_task_view ' : '#addAlertTask ') + '.projectBox .bt_select_content').html(); - _configD['data'] = JSON.stringify(eData); - return [4, this.$request(['push/set_push_config', 'Setting alarm task, Please wait...'], _configD)]; - case 1: + case 0: + return [ + 4, + this.$confirm({ + title: 'Delete Alarm Tasks', + msg: 'Delete will no longer alert this task, do you want to continue?', + }), + ]; + case 1: + _a.sent(); + return [4, this.$request(['push/del_push_config', 'Deleting the alarm task'], { name: row.module_type, id: row.id })]; + case 2: rdata = _a.sent(); - if (!rdata.status) return [3, 3]; - layer.close(close); + if (!rdata.status) return [3, 4]; return [4, this.alertTaskList()]; - case 2: + case 3: _a.sent(); - _a.label = 3; - case 3: return [2]; - } - }); - }); - }; - Config.prototype.switchPushType = function (config, formData) { - if (formData === void 0) { formData = {}; } - var _checklist = [], isCheckType = [], siteList = [{ title: 'All Website', value: 'all' }], accountConfigStatus = false; - if (!formData.type) { - formData.type = 'ssl'; - config[1].group.value = 'all'; - } - this.panelSiteList['data'].forEach(function (key) { - siteList.push({ title: key.name, value: key.name }); - }); - this.alertListModule.forEach(function (mod, i) { - if (formData.type != 'ssl' && mod.name == 'sms') - return; - if (formData.module) { - isCheckType = formData.module.split(','); - } - if (mod.name === 'wx_account') { - if (!$.isEmptyObject(mod.data) && mod.data.res.is_subscribe && mod.data.res.is_bound) { - accountConfigStatus = true; - } - } - _checklist.push({ - type: 'checkbox', - name: mod.name, - class: 'module-check ' + (!mod.setup || $.isEmptyObject(mod.data) ? 'check_disabled' : mod.name == 'wx_account' && !accountConfigStatus ? 'check_disabled' : '') + '', - style: { 'margin-right': '10px' }, - disabled: !mod.setup || $.isEmptyObject(mod.data) ? true : mod.name == 'wx_account' && !accountConfigStatus ? true : false, - value: $.inArray(mod.name, isCheckType) >= 0 ? 1 : 0, - title: (mod.name == 'wx_account' ? ' [Recommend]' : '') + - mod.title + - (!mod.setup || $.isEmptyObject(mod.data) - ? ' [Install]' - : mod.name == 'wx_account' && !accountConfigStatus - ? ' [Not set]' - : ''), - event: function (formData, element, thatE) { - thatE.config.form[6].group[i].value = !formData[mod.name] ? 0 : 1; - }, - }); - }); - if (!formData.id) { - var checkActive = _checklist.findIndex(function (ev) { return !ev.disabled; }); - if (checkActive >= 0) - _checklist[checkActive].value = 1; - } - else { - if (formData.type == 'ssl') - config[1].group.value = formData.project; - } - config[1].hide = true; - config[3].hide = true; - config[4].hide = true; - config[5].hide = false; - delete config[0].group.unit; - switch (formData.type) { - case 'ssl': - config[1].hide = false; - config[2].hide = false; - config[2].group.value = 15; - break; - case 'site_endtime': - config[2].hide = false; - config[2].group.value = 7; - break; - case 'panel_pwd_endtime': - config[2].hide = false; - config[2].group.value = 15; - break; - case 'panel_login': - case 'ssh_login': - case 'panel_safe_push': - case 'panel_update': - config[2].hide = true; - config[5].hide = true; - if (formData.type == 'panel_update') { - config[0].group.unit = '* Send a notification when a new version is detected'; - } - break; - case 'ssh_login_error': - config[2].hide = true; - config[3].hide = false; - config[4].hide = false; - config[5].hide = true; - break; - } - config[7].hide = formData.type === 'panel_safe_push' ? false : true; - config[0].group.value = formData.type; - config[1].group.list = siteList; - config[6].group = _checklist; - return config; - }; - Config.prototype.setAlertConfigType = function (type) { - return __awaiter(this, void 0, void 0, function () { - var _configData; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; - case 1: - _configData = _a.sent(); - switch (type) { - case 'mail': - renderMailConfigView(_configData[type]); - break; - case 'dingding': - case 'feishu': - case 'weixin': - renderAlertUrlTypeChannelView(_configData[type]); - break; - case 'tg': - renderTelegramConfigView(_configData[type]); - break; - } + _a.label = 4; + case 4: return [2]; - } - }); - }); - }; - Config.prototype.alertConfigTable = function () { - return __awaiter(this, void 0, void 0, function () { - var ChannelInfo, html, tbody, prevHTML; - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; - case 1: - ChannelInfo = _a.sent(); - html = '', tbody = '', prevHTML = ''; - $('#alertConfig').empty(); - Object.getOwnPropertyNames(ChannelInfo).forEach(function (key) { - var item = ChannelInfo[key], btnGroup = ''; - if (item.setup) { - if (item.name != 'sms') { - if (!$.isEmptyObject(item.data)) { - if (item.name == 'mail') - btnGroup += 'Recipient | '; - btnGroup += - '' + lan.public.edit + ' | Test | ' + lan.public.del + ''; - if (item.name == 'wx_account') - btnGroup = 'Bind | Test | ' + lan.public.del + ''; - } - else { - btnGroup = '' + lan.public.set + ''; - } - } - else { - btnGroup = '' + lan.public.del + ''; - } - } - else { - btnGroup = '' + lan.public.set + ''; - } - var renderHTML = "\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t").concat(item.title, "\n\t\t\t\t\t\t

").concat(item.ps, ">>").concat(lan.public.help, "

\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t\t").concat(_this.renderAlertModelConfigInfo(item), "\n\t\t\t\t").concat(item.version, "\n\t\t\t\t").concat(btnGroup, "\n\t\t\t"); - item.name === 'wx_account' ? (prevHTML = renderHTML) : (tbody += renderHTML); - }); - html = "
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t").concat(prevHTML + tbody, "\n\t\t\t\t\t\t\t
Alarm moduleConfigurationVersion".concat(lan.public.operate, "
\n\t\t\t\t\t\t
"); - $('#alertConfig').html(html); - this.alertEventBind(ChannelInfo); - return [2]; - } - }); - }); - }; - Config.prototype.renderAlertModelConfigInfo = function (mode) { - var _info = '', noConfig = 'Unconfigured', _data = mode.data, isEmpty = $.isEmptyObject(_data); - if (mode.setup) { - if (mode.name != 'sms' && mode.name != 'wx_account') { - if (!$.isEmptyObject(_data)) { - switch (mode.name) { - case 'mail': - if (_data.receive[0] == '') { - _info = "No incoming email set"; - } - else { - _info = "".concat(_data.receive.length, " incoming email has been set up, Click to view"); - } - break; - case 'dingding': - case 'feishu': - case 'weixin': - _info = "Receiver: [".concat(isEmpty ? '' : _data.list.default.title, "]"); - break; - case 'tg': - _info = "Receiver: [".concat(isEmpty ? '' : _data.my_id, "]"); - break; } - } - else { - _info = noConfig; - } - } - else if (mode.name == 'sms') { - _info = "\u5269\u4F59\u53D1\u9001\u544A\u8B66".concat(_data.count, "\u6B21"); - } - else if (mode.name == 'wx_account') { - var boundCheck = '', res = $.isEmptyObject(_data) ? { is_subscribe: 0, is_bound: 0 } : _data.res; - if (!res.is_subscribe || !res.is_bound) - boundCheck = '未订阅公众号或绑定微信'; - if (res.is_subscribe && res.is_bound) - boundCheck = "\u5FAE\u4FE1\u8D26\u53F7\u3010".concat(res.nickname, "\u3011,\u4ECA\u65E5\u5269\u4F59\u53D1\u9001\u6B21\u6570:").concat(res.remaining); - _info = boundCheck; - } - } - else { - _info = noConfig; - } - return _info; - }; - Config.prototype.alertEventBind = function (info) { - var _this = this; - $('.receiveMail').click(function () { return __awaiter(_this, void 0, void 0, function () { - var currentItem; - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; - case 1: - currentItem = _a.sent(); - this.$open({ - title: 'Recipient Email', - area: ['335px', '280px'], - btn: [lan.public.save, lan.public.cancel], - skin: 'alert-receive-view', - content: "
\n\t\t\t\t
Fill in one mailbox per line, e:
xxx@163.com
xxx@qq.com
", - success: function () { - var _tips = $('textarea[name=recipient_textarea]'); - var msg = ''; - if (!$.isEmptyObject(currentItem['mail']['data']['receive'])) { - msg = currentItem['mail']['data']['receive'] ? currentItem['mail']['data']['receive'].join('\n') : ''; - } - _tips.html(msg); - if (_tips.val() == '') - $('.reci_tips.placeholder').show(); - $('.placeholder').click(function () { - $(this).hide().siblings('textarea').focus(); - }); - _tips.focus(function () { - $('.reci_tips.placeholder').hide(); - }); - _tips.blur(function () { - _tips.val() == '' ? $('.reci_tips.placeholder').show() : $('.reci_tips.placeholder').hide(); - }); - }, - yes: function (config) { return __awaiter(_this, void 0, void 0, function () { - var close, reci_, rdata; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - close = config.close; - reci_ = $('textarea[name=recipient_textarea]').val(); - return [4, this.$request('setMsgConfigmail', { mails: reci_ })]; - case 1: - rdata = _a.sent(); - rdata.status && close(); - return [2]; - } - }); - }); }, - }); - return [2]; - } - }); - }); }); - $('.configEdit').click(function (ev) { return __awaiter(_this, void 0, void 0, function () { - var _type; - return __generator(this, function (_a) { - _type = $(ev.currentTarget).parents('tr').data('name'); - this.setAlertConfigType(_type); - return [2]; - }); - }); }); - $('.alertTest').click(function (ev) { return __awaiter(_this, void 0, void 0, function () { - var _type; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _type = $(ev.currentTarget).parents('tr').data('name'); - return [4, this.$request(['config/get_msg_fun', 'Testing Send, Please wait...'], { fun_name: 'push_data', module_name: _type, msg: 'Testing Send' })]; - case 1: - _a.sent(); - return [2]; - } - }); - }); }); - $('.replaceWx').click(function () { - _this.setAlertConfigType('wx_account'); - }); - $('.uninstall_alert').click(function (ev) { return __awaiter(_this, void 0, void 0, function () { - var _type, rdata, _a; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - _type = $(ev.currentTarget).parents('tr').data('name'); - return [4, this.$confirm({ - title: 'Delete ' + info[_type].title + ' module', - msg: 'After deleting the ' + info[_type].title + ' module, it will not be able to send panel alert messages, should I continue?', - })]; - case 1: - _b.sent(); - return [4, this.$request(['config/uninstall_msg_module&name=' + _type, 'Delete ' + info[_type].title + ' alert module'])]; - case 2: - rdata = _b.sent(); - _a = rdata.status; - if (!_a) return [3, 4]; - return [4, this.alertConfigTable()]; - case 3: - _a = (_b.sent()); - _b.label = 4; - case 4: - _a; - return [2]; - } - }); - }); }); - }; - Config.prototype.alertLogsTable = function () { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - $('#alertLog').empty(); - bt_tools.table({ - el: '#alertLog', - load: 'Getting the alarm log list', - url: '/push?action=get_push_logs', - default: 'The alarm log is empty', - dataFilter: function (res) { - return { data: res.data }; - }, - column: [ - { - fid: 'log', - title: 'Title', - type: 'text', - }, - { - fid: 'addtime', - title: 'Time', - type: 'text', - }, - ], - tootls: [ - { - type: 'page', - positon: ['right', 'bottom'], - pageParam: 'p', - page: 1, - numberParam: 'limit', - number: 20, - numberList: [10, 20, 50, 100, 200], - numberStatus: true, - jump: true, - }, - ], + }); }); - return [2]; + }, + }, + ], + }, + ], + tootls: [ + { + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' Delete', + url: 'push?action=del_push_config', + load: true, + param: function (row) { + return { name: row.module_type, id: row.id }; + }, + callback: function (that) { + bt.confirm({ title: 'Batch Delete Tasks', msg: 'The batch deletion will not be recovered, does it continue?', icon: 0 }, function (index) { + layer.close(index); + that.start_batch({}, function (list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += + '' + + (typeof item.project == 'undefined' ? item.title : (item.project == 'all' ? 'All' : item.project) + item.title) + + '
' + + item.request.msg + + '
'; + } + alertListTabel.$batch_success_table({ title: 'Batch Delete Tasks', th: 'Task title', html: html }); + alertListTabel.$refresh_table_list(true); + }); + }); + }, + }, + }, + ], + }); + }; + Config.prototype.setAlertTaskConfig = function (row) { + return __awaiter(this, void 0, void 0, function () { + var _config; + var _this = this; + return __generator(this, function (_a) { + _config = this.switchPushType($.extend(true, {}, this.alertConfigForm), row); + _config[0].group.disabled = true; + _config[0].group.unit = ''; + _config[8].hide = true; + if (row.type == 'ssh_login_error') { + row.where1 = row.cycle; + } + bt_tools.open({ + type: 1, + title: 'Edit Alert Tasks', + area: '540px', + skin: 'panel_alert_task_view', + btn: [lan.public.save, lan.public.cancel], + content: { + class: 'pd15', + data: row, + form: _config, + }, + success: function (layers) { + $(layers) + .find('.layui-layer-content') + .css('overflow', window.innerHeight > $(layers).height() ? 'inherit' : 'auto'); + $('.alertInstall').click(function (ev) { + var _type = $(ev.currentTarget).parent('span').siblings('input').attr('name'); + _this.setAlertConfigType(_type); }); + }, + yes: function (formData, index) { + _this.setAlertConfigTask($.extend(true, {}, row, formData), index); + }, }); - }; - return Config; - }(public_1.default)); + return [2]; + }); + }); + }; + Config.prototype.setAlertConfigTask = function (row, close) { + if (close === void 0) { + close = null; + } + return __awaiter(this, void 0, void 0, function () { + var _configD, eData, pushType, otherType, isCheck, rdata; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + (_configD = {}), + (eData = {}), + (pushType = this.taskTypeList.find(function (el) { + return el.value == row.type; + })), + (otherType = this.disabledOption); + _configD['name'] = typeof row.module_type !== 'undefined' ? row.module_type : pushType.model; + _configD['id'] = row.id ? row.id : $.inArray(row.type, otherType) >= 0 ? row.type : new Date().getTime(); + eData['interval'] = 600; + switch (row.type) { + case 'ssl': + case 'site_endtime': + case 'panel_pwd_endtime': + if (row.type == 'ssl') eData['project'] = row.site || 'all'; + if (row.cycle == '' || row.cycle < 0) return [2, this.$msg({ icon: 2, msg: 'Remaining days cannot be less than 1', time: 0, closeBtn: 2 })]; + eData['cycle'] = Number(row.cycle); + eData['push_count'] = Number(row.push_count); + break; + case 'ssh_login_error': + if (row.where1 == '' || row.where1 <= 0) return [2, this.$msg({ icon: 2, msg: 'Trigger time cannot be less than 1', time: 0, closeBtn: 2 })]; + if (row.count == '' || row.count <= 0) return [2, this.$msg({ icon: 2, msg: 'Trigger times cannot be less than 1', time: 0, closeBtn: 2 })]; + if (row.interval == '' || row.interval <= 0) return [2, this.$msg({ icon: 2, msg: 'Interval cannot be less than 1', time: 0, closeBtn: 2 })]; + eData['cycle'] = Number(row.where1); + eData['count'] = Number(row.count); + eData['interval'] = Number(row.interval); + break; + } + isCheck = []; + $((row.id ? '.panel_alert_task_view ' : '#addAlertTask ') + '.module-check') + .not('.check_disabled') + .each(function () { + if ($(this).find('input').prop('checked')) { + isCheck.push($(this).find('input').prop('name')); + } + }); + eData['type'] = row.type; + eData['module'] = isCheck.join(); + if (typeof eData['push_count'] != 'undefined' && (eData['push_count'] <= 0 || eData['push_count'] == '')) { + this.$msg({ icon: 2, msg: 'The number of sending cannot be less than 1', time: 0, closeBtn: 2 }); + return [2, false]; + } + if (!eData['module']) { + this.$msg({ icon: 2, msg: 'Please select an alarm mode', time: 0, closeBtn: 2 }); + return [2, false]; + } + eData['status'] = typeof row.status !== 'undefined' ? row.status : true; + eData['title'] = $((row.id ? '.panel_alert_task_view ' : '#addAlertTask ') + '.projectBox .bt_select_content').html(); + _configD['data'] = JSON.stringify(eData); + return [4, this.$request(['push/set_push_config', 'Setting alarm task, Please wait...'], _configD)]; + case 1: + rdata = _a.sent(); + if (!rdata.status) return [3, 3]; + layer.close(close); + return [4, this.alertTaskList()]; + case 2: + _a.sent(); + _a.label = 3; + case 3: + return [2]; + } + }); + }); + }; + Config.prototype.switchPushType = function (config, formData) { + if (formData === void 0) { + formData = {}; + } + var _checklist = [], + isCheckType = [], + siteList = [{ title: 'All Website', value: 'all' }], + accountConfigStatus = false; + if (!formData.type) { + formData.type = 'ssl'; + config[1].group.value = 'all'; + } + this.panelSiteList['data'].forEach(function (key) { + siteList.push({ title: key.name, value: key.name }); + }); + this.alertListModule.forEach(function (mod, i) { + if (formData.type != 'ssl' && mod.name == 'sms') return; + if (formData.module) { + isCheckType = formData.module.split(','); + } + if (mod.name === 'wx_account') { + if (!$.isEmptyObject(mod.data) && mod.data.res.is_subscribe && mod.data.res.is_bound) { + accountConfigStatus = true; + } + } + _checklist.push({ + type: 'checkbox', + name: mod.name, + class: 'module-check ' + (!mod.setup || $.isEmptyObject(mod.data) ? 'check_disabled' : mod.name == 'wx_account' && !accountConfigStatus ? 'check_disabled' : '') + '', + style: { 'margin-right': '10px' }, + disabled: !mod.setup || $.isEmptyObject(mod.data) ? true : mod.name == 'wx_account' && !accountConfigStatus ? true : false, + value: $.inArray(mod.name, isCheckType) >= 0 ? 1 : 0, + title: + (mod.name == 'wx_account' ? ' [Recommend]' : '') + + mod.title + + (!mod.setup || $.isEmptyObject(mod.data) + ? ' [Install]' + : mod.name == 'wx_account' && !accountConfigStatus + ? ' [Not set]' + : ''), + event: function (formData, element, thatE) { + thatE.config.form[6].group[i].value = !formData[mod.name] ? 0 : 1; + }, + }); + }); + if (!formData.id) { + var checkActive = _checklist.findIndex(function (ev) { + return !ev.disabled; + }); + if (checkActive >= 0) _checklist[checkActive].value = 1; + } else { + if (formData.type == 'ssl') config[1].group.value = formData.project; + } + config[1].hide = true; + config[3].hide = true; + config[4].hide = true; + config[5].hide = false; + delete config[0].group.unit; + switch (formData.type) { + case 'ssl': + config[1].hide = false; + config[2].hide = false; + config[2].group.value = 15; + break; + case 'site_endtime': + config[2].hide = false; + config[2].group.value = 7; + break; + case 'panel_pwd_endtime': + config[2].hide = false; + config[2].group.value = 15; + break; + case 'panel_login': + case 'ssh_login': + case 'panel_safe_push': + case 'panel_update': + config[2].hide = true; + config[5].hide = true; + if (formData.type == 'panel_update') { + config[0].group.unit = '* Send a notification when a new version is detected'; + } + break; + case 'ssh_login_error': + config[2].hide = true; + config[3].hide = false; + config[4].hide = false; + config[5].hide = true; + break; + } + config[7].hide = formData.type === 'panel_safe_push' ? false : true; + config[0].group.value = formData.type; + config[1].group.list = siteList; + config[6].group = _checklist; + return config; + }; + Config.prototype.setAlertConfigType = function (type) { + return __awaiter(this, void 0, void 0, function () { + var _configData; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; + case 1: + _configData = _a.sent(); + switch (type) { + case 'mail': + renderMailConfigView(_configData[type]); + break; + case 'dingding': + case 'feishu': + case 'weixin': + renderAlertUrlTypeChannelView(_configData[type]); + break; + case 'tg': + renderTelegramConfigView(_configData[type]); + break; + } + return [2]; + } + }); + }); + }; + Config.prototype.alertConfigTable = function () { + return __awaiter(this, void 0, void 0, function () { + var ChannelInfo, html, tbody, prevHTML; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; + case 1: + ChannelInfo = _a.sent(); + (html = ''), (tbody = ''), (prevHTML = ''); + $('#alertConfig').empty(); + Object.getOwnPropertyNames(ChannelInfo).forEach(function (key) { + var item = ChannelInfo[key], + btnGroup = ''; + if (item.setup) { + if (item.name != 'sms') { + if (!$.isEmptyObject(item.data)) { + if (item.name == 'mail') btnGroup += 'Recipient | '; + btnGroup += + '' + + lan.public.edit + + ' | Test | ' + + lan.public.del + + ''; + if (item.name == 'wx_account') + btnGroup = 'Bind | Test | ' + lan.public.del + ''; + } else { + btnGroup = '' + lan.public.set + ''; + } + } else { + btnGroup = '' + lan.public.del + ''; + } + } else { + btnGroup = '' + lan.public.set + ''; + } + var renderHTML = '\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t') + .concat(item.title, '\n\t\t\t\t\t\t

') + .concat(item.ps, '>>') + .concat(lan.public.help, '

\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t\t') + .concat(_this.renderAlertModelConfigInfo(item), '\n\t\t\t\t') + .concat(item.version, '\n\t\t\t\t') + .concat(btnGroup, '\n\t\t\t'); + item.name === 'wx_account' ? (prevHTML = renderHTML) : (tbody += renderHTML); + }); + html = + '
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t') + .concat(prevHTML + tbody, '\n\t\t\t\t\t\t\t
Alarm moduleConfigurationVersion' + .concat(lan.public.operate, '
\n\t\t\t\t\t\t
'); + $('#alertConfig').html(html); + this.alertEventBind(ChannelInfo); + return [2]; + } + }); + }); + }; + Config.prototype.renderAlertModelConfigInfo = function (mode) { + var _info = '', + noConfig = 'Unconfigured', + _data = mode.data, + isEmpty = $.isEmptyObject(_data); + if (mode.setup) { + if (mode.name != 'sms' && mode.name != 'wx_account') { + if (!$.isEmptyObject(_data)) { + switch (mode.name) { + case 'mail': + if (_data.receive[0] == '') { + _info = 'No incoming email set'; + } else { + _info = ''.concat(_data.receive.length, ' incoming email has been set up, Click to view'); + } + break; + case 'dingding': + case 'feishu': + case 'weixin': + _info = 'Receiver: ['.concat(isEmpty ? '' : _data.list.default.title, ']'); + break; + case 'tg': + _info = 'Receiver: ['.concat(isEmpty ? '' : _data.my_id, ']'); + break; + } + } else { + _info = noConfig; + } + } else if (mode.name == 'sms') { + _info = '\u5269\u4F59\u53D1\u9001\u544A\u8B66'.concat(_data.count, '\u6B21'); + } else if (mode.name == 'wx_account') { + var boundCheck = '', + res = $.isEmptyObject(_data) ? { is_subscribe: 0, is_bound: 0 } : _data.res; + if (!res.is_subscribe || !res.is_bound) boundCheck = '未订阅公众号或绑定微信'; + if (res.is_subscribe && res.is_bound) boundCheck = '\u5FAE\u4FE1\u8D26\u53F7\u3010'.concat(res.nickname, '\u3011,\u4ECA\u65E5\u5269\u4F59\u53D1\u9001\u6B21\u6570:').concat(res.remaining); + _info = boundCheck; + } + } else { + _info = noConfig; + } + return _info; + }; + Config.prototype.alertEventBind = function (info) { + var _this = this; + $('.receiveMail').click(function () { + return __awaiter(_this, void 0, void 0, function () { + var currentItem; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + return [4, this.$request('getMessageChannel', { loading: false, msg: false })]; + case 1: + currentItem = _a.sent(); + this.$open({ + title: 'Recipient Email', + area: ['335px', '280px'], + btn: [lan.public.save, lan.public.cancel], + skin: 'alert-receive-view', + content: + '
\n\t\t\t\t
', + success: function () { + var _tips = $('textarea[name=recipient_textarea]'); + var msg = ''; + if (!$.isEmptyObject(currentItem['mail']['data']['receive'])) { + msg = currentItem['mail']['data']['receive'] ? currentItem['mail']['data']['receive'].join('\n') : ''; + } + _tips.html(msg); + if (_tips.val() == '') $('.reci_tips.placeholder').show(); + $('.placeholder').click(function () { + $(this).hide().siblings('textarea').focus(); + }); + _tips.focus(function () { + $('.reci_tips.placeholder').hide(); + }); + _tips.blur(function () { + _tips.val() == '' ? $('.reci_tips.placeholder').show() : $('.reci_tips.placeholder').hide(); + }); + }, + yes: function (config) { + return __awaiter(_this, void 0, void 0, function () { + var close, reci_, rdata; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + close = config.close; + reci_ = $('textarea[name=recipient_textarea]').val(); + return [4, this.$request('setMsgConfigmail', { mails: reci_ })]; + case 1: + rdata = _a.sent(); + rdata.status && close(); + return [2]; + } + }); + }); + }, + }); + return [2]; + } + }); + }); + }); + $('.configEdit').click(function (ev) { + return __awaiter(_this, void 0, void 0, function () { + var _type; + return __generator(this, function (_a) { + _type = $(ev.currentTarget).parents('tr').data('name'); + this.setAlertConfigType(_type); + return [2]; + }); + }); + }); + $('.alertTest').click(function (ev) { + return __awaiter(_this, void 0, void 0, function () { + var _type; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _type = $(ev.currentTarget).parents('tr').data('name'); + return [4, this.$request(['config/get_msg_fun', 'Testing Send, Please wait...'], { fun_name: 'push_data', module_name: _type, msg: 'Testing Send' })]; + case 1: + _a.sent(); + return [2]; + } + }); + }); + }); + $('.replaceWx').click(function () { + _this.setAlertConfigType('wx_account'); + }); + $('.uninstall_alert').click(function (ev) { + return __awaiter(_this, void 0, void 0, function () { + var _type, rdata, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _type = $(ev.currentTarget).parents('tr').data('name'); + return [ + 4, + this.$confirm({ + title: 'Delete ' + info[_type].title + ' module', + msg: 'After deleting the ' + info[_type].title + ' module, it will not be able to send panel alert messages, should I continue?', + }), + ]; + case 1: + _b.sent(); + return [4, this.$request(['config/uninstall_msg_module&name=' + _type, 'Delete ' + info[_type].title + ' alert module'])]; + case 2: + rdata = _b.sent(); + _a = rdata.status; + if (!_a) return [3, 4]; + return [4, this.alertConfigTable()]; + case 3: + _a = _b.sent(); + _b.label = 4; + case 4: + _a; + return [2]; + } + }); + }); + }); + }; + Config.prototype.alertLogsTable = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + $('#alertLog').empty(); + bt_tools.table({ + el: '#alertLog', + load: 'Getting the alarm log list', + url: '/push?action=get_push_logs', + default: 'The alarm log is empty', + dataFilter: function (res) { + return { data: res.data }; + }, + column: [ + { + fid: 'log', + title: 'Title', + type: 'text', + }, + { + fid: 'addtime', + title: 'Time', + type: 'text', + }, + ], + tootls: [ + { + type: 'page', + positon: ['right', 'bottom'], + pageParam: 'p', + page: 1, + numberParam: 'limit', + number: 20, + numberList: [10, 20, 50, 100, 200], + numberStatus: true, + jump: true, + }, + ], + }); + return [2]; + }); + }); + }; + return Config; + })(public_1.default); exports.Config = Config; }); diff --git a/BTPanel/static/amd/utils.min.js b/BTPanel/static/amd/utils.min.js index 0ef85d4e..e2b7d128 100644 --- a/BTPanel/static/amd/utils.min.js +++ b/BTPanel/static/amd/utils.min.js @@ -1,3227 +1,3053 @@ var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, 'default', {enumerable: true, value: v}); - } - : function (o, v) { - o['default'] = v; - }); + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; var __exportStar = - (this && this.__exportStar) || - function (m, exports) { - for (var p in m) if (p !== 'default' && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); - }; + (this && this.__exportStar) || + function (m, exports) { + for (var p in m) if (p !== 'default' && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); + }; var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - - function rejected(value) { - try { - step(generator['throw'](value)); - } catch (e) { - reject(e); - } - } - - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator['throw'](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; var __generator = - (this && this.__generator) || - function (thisArg, body) { - var _ = { - label: 0, - sent: function () { - if (t[0] & 1) throw t[1]; - return t[1]; - }, - trys: [], - ops: [], - }, - f, - y, - t, - g; - return ( - (g = {next: verb(0), throw: verb(1), return: verb(2)}), - typeof Symbol === 'function' && - (g[Symbol.iterator] = function () { - return this; - }), - g - ); - - function verb(n) { - return function (v) { - return step([n, v]); - }; - } - - function step(op) { - if (f) throw new TypeError('Generator is already executing.'); - while (_) - try { - if (((f = 1), y && (t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done)) return t; - if (((y = 0), t)) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: - case 1: - t = op; - break; - case 4: - _.label++; - return {value: op[1], done: false}; - case 5: - _.label++; - y = op[1]; - op = [0]; - continue; - case 7: - op = _.ops.pop(); - _.trys.pop(); - continue; - default: - if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) { - _ = 0; - continue; - } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { - _.label = op[1]; - break; - } - if (op[0] === 6 && _.label < t[1]) { - _.label = t[1]; - t = op; - break; - } - if (t && _.label < t[2]) { - _.label = t[2]; - _.ops.push(op); - break; - } - if (t[2]) _.ops.pop(); - _.trys.pop(); - continue; - } - op = body.call(thisArg, _); - } catch (e) { - op = [6, e]; - y = 0; - } finally { - f = t = 0; - } - if (op[0] & 5) throw op[1]; - return {value: op[0] ? op[1] : void 0, done: true}; - } - }; + (this && this.__generator) || + function (thisArg, body) { + var _ = { + label: 0, + sent: function () { + if (t[0] & 1) throw t[1]; + return t[1]; + }, + trys: [], + ops: [], + }, + f, + y, + t, + g; + return ( + (g = { next: verb(0), throw: verb(1), return: verb(2) }), + typeof Symbol === 'function' && + (g[Symbol.iterator] = function () { + return this; + }), + g + ); + function verb(n) { + return function (v) { + return step([n, v]); + }; + } + function step(op) { + if (f) throw new TypeError('Generator is already executing.'); + while (_) + try { + if (((f = 1), y && (t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done)) return t; + if (((y = 0), t)) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; + } + op = body.call(thisArg, _); + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + }; var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : {default: mod}; - }; + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; var __extends = - (this && this.__extends) || - (function () { - var extendStatics = function (d, b) { - extendStatics = - Object.setPrototypeOf || - ({__proto__: []} instanceof Array && - function (d, b) { - d.__proto__ = b; - }) || - function (d, b) { - for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; - }; - return extendStatics(d, b); - }; - return function (d, b) { - if (typeof b !== 'function' && b !== null) throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); - extendStatics(d, b); - - function __() { - this.constructor = d; - } - - d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __()); - }; - })(); + (this && this.__extends) || + (function () { + var extendStatics = function (d, b) { + extendStatics = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function (d, b) { + d.__proto__ = b; + }) || + function (d, b) { + for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; + }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== 'function' && b !== null) throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); + extendStatics(d, b); + function __() { + this.constructor = d; + } + d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __()); + }; + })(); define('h', ['require', 'exports', './vnode', './is'], function (require, exports, vnode_1, is) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.h = void 0; - is = __importStar(is); - - function addNS(data, children, sel) { - data.ns = 'http://www.w3.org/2000/svg'; - if (sel !== 'foreignObject' && children !== undefined) { - for (var i = 0; i < children.length; ++i) { - var childData = children[i].data; - if (childData !== undefined) { - addNS(childData, children[i].children, children[i].sel); - } - } - } - } - - function h(sel, b, c) { - var data = {}; - var children; - var text; - var i; - if (c !== undefined) { - if (b !== null) { - data = b; - } - if (is.array(c)) { - children = c; - } else if (is.primitive(c)) { - text = c.toString(); - } else if (c && c.sel) { - children = [c]; - } - } else if (b !== undefined && b !== null) { - if (is.array(b)) { - children = b; - } else if (is.primitive(b)) { - text = b.toString(); - } else if (b && b.sel) { - children = [b]; - } else { - data = b; - } - } - if (children !== undefined) { - for (i = 0; i < children.length; ++i) { - if (is.primitive(children[i])) children[i] = (0, vnode_1.vnode)(undefined, undefined, undefined, children[i], undefined); - } - } - if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' && (sel.length === 3 || sel[3] === '.' || sel[3] === '#')) { - addNS(data, children, sel); - } - return (0, vnode_1.vnode)(sel, data, children, text, undefined); - } - - exports.h = h; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.h = void 0; + is = __importStar(is); + function addNS(data, children, sel) { + data.ns = 'http://www.w3.org/2000/svg'; + if (sel !== 'foreignObject' && children !== undefined) { + for (var i = 0; i < children.length; ++i) { + var childData = children[i].data; + if (childData !== undefined) { + addNS(childData, children[i].children, children[i].sel); + } + } + } + } + function h(sel, b, c) { + var data = {}; + var children; + var text; + var i; + if (c !== undefined) { + if (b !== null) { + data = b; + } + if (is.array(c)) { + children = c; + } else if (is.primitive(c)) { + text = c.toString(); + } else if (c && c.sel) { + children = [c]; + } + } else if (b !== undefined && b !== null) { + if (is.array(b)) { + children = b; + } else if (is.primitive(b)) { + text = b.toString(); + } else if (b && b.sel) { + children = [b]; + } else { + data = b; + } + } + if (children !== undefined) { + for (i = 0; i < children.length; ++i) { + if (is.primitive(children[i])) children[i] = (0, vnode_1.vnode)(undefined, undefined, undefined, children[i], undefined); + } + } + if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' && (sel.length === 3 || sel[3] === '.' || sel[3] === '#')) { + addNS(data, children, sel); + } + return (0, vnode_1.vnode)(sel, data, children, text, undefined); + } + exports.h = h; }); define('hooks', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); }); define('htmldomapi', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.htmlDomApi = void 0; - - function createElement(tagName, options) { - return document.createElement(tagName, options); - } - - function createElementNS(namespaceURI, qualifiedName, options) { - return document.createElementNS(namespaceURI, qualifiedName, options); - } - - function createTextNode(text) { - return document.createTextNode(text); - } - - function createComment(text) { - return document.createComment(text); - } - - function insertBefore(parentNode, newNode, referenceNode) { - parentNode.insertBefore(newNode, referenceNode); - } - - function removeChild(node, child) { - node.removeChild(child); - } - - function appendChild(node, child) { - node.appendChild(child); - } - - function parentNode(node) { - return node.parentNode; - } - - function nextSibling(node) { - return node.nextSibling; - } - - function tagName(elm) { - return elm.tagName; - } - - function setTextContent(node, text) { - node.textContent = text; - } - - function getTextContent(node) { - return node.textContent; - } - - function isElement(node) { - return node.nodeType === 1; - } - - function isText(node) { - return node.nodeType === 3; - } - - function isComment(node) { - return node.nodeType === 8; - } - - exports.htmlDomApi = { - createElement: createElement, - createElementNS: createElementNS, - createTextNode: createTextNode, - createComment: createComment, - insertBefore: insertBefore, - removeChild: removeChild, - appendChild: appendChild, - parentNode: parentNode, - nextSibling: nextSibling, - tagName: tagName, - setTextContent: setTextContent, - getTextContent: getTextContent, - isElement: isElement, - isText: isText, - isComment: isComment, - }; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.htmlDomApi = void 0; + function createElement(tagName, options) { + return document.createElement(tagName, options); + } + function createElementNS(namespaceURI, qualifiedName, options) { + return document.createElementNS(namespaceURI, qualifiedName, options); + } + function createTextNode(text) { + return document.createTextNode(text); + } + function createComment(text) { + return document.createComment(text); + } + function insertBefore(parentNode, newNode, referenceNode) { + parentNode.insertBefore(newNode, referenceNode); + } + function removeChild(node, child) { + node.removeChild(child); + } + function appendChild(node, child) { + node.appendChild(child); + } + function parentNode(node) { + return node.parentNode; + } + function nextSibling(node) { + return node.nextSibling; + } + function tagName(elm) { + return elm.tagName; + } + function setTextContent(node, text) { + node.textContent = text; + } + function getTextContent(node) { + return node.textContent; + } + function isElement(node) { + return node.nodeType === 1; + } + function isText(node) { + return node.nodeType === 3; + } + function isComment(node) { + return node.nodeType === 8; + } + exports.htmlDomApi = { + createElement: createElement, + createElementNS: createElementNS, + createTextNode: createTextNode, + createComment: createComment, + insertBefore: insertBefore, + removeChild: removeChild, + appendChild: appendChild, + parentNode: parentNode, + nextSibling: nextSibling, + tagName: tagName, + setTextContent: setTextContent, + getTextContent: getTextContent, + isElement: isElement, + isText: isText, + isComment: isComment, + }; }); define('init', ['require', 'exports', './vnode', './is', './htmldomapi'], function (require, exports, vnode_2, is, htmldomapi_1) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.init = void 0; - is = __importStar(is); - - function isUndef(s) { - return s === undefined; - } - - function isDef(s) { - return s !== undefined; - } - - var emptyNode = (0, vnode_2.vnode)('', {}, [], undefined, undefined); - - function sameVnode(vnode1, vnode2) { - var _a, _b; - var isSameKey = vnode1.key === vnode2.key; - var isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is); - var isSameSel = vnode1.sel === vnode2.sel; - return isSameSel && isSameKey && isSameIs; - } - - function isVnode(vnode) { - return vnode.sel !== undefined; - } - - function createKeyToOldIdx(children, beginIdx, endIdx) { - var _a; - var map = {}; - for (var i = beginIdx; i <= endIdx; ++i) { - var key = (_a = children[i]) === null || _a === void 0 ? void 0 : _a.key; - if (key !== undefined) { - map[key] = i; - } - } - return map; - } - - var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; - - function init(modules, domApi) { - var cbs = { - create: [], - update: [], - remove: [], - destroy: [], - pre: [], - post: [], - }; - var api = domApi !== undefined ? domApi : htmldomapi_1.htmlDomApi; - for (var _i = 0, hooks_1 = hooks; _i < hooks_1.length; _i++) { - var hook = hooks_1[_i]; - for (var _f = 0, modules_1 = modules; _f < modules_1.length; _f++) { - var module_1 = modules_1[_f]; - var currentHook = module_1[hook]; - if (currentHook !== undefined) { - cbs[hook].push(currentHook); - } - } - } - - function emptyNodeAt(elm) { - var id = elm.id ? '#' + elm.id : ''; - var classes = elm.getAttribute('class'); - var c = classes ? '.' + classes.split(' ').join('.') : ''; - return (0, vnode_2.vnode)(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); - } - - function createRmCb(childElm, listeners) { - return function rmCb() { - if (--listeners === 0) { - var parent_1 = api.parentNode(childElm); - api.removeChild(parent_1, childElm); - } - }; - } - - function createElm(vnode, insertedVnodeQueue) { - var _a, _b; - var i; - var data = vnode.data; - if (data !== undefined) { - var init_1 = (_a = data.hook) === null || _a === void 0 ? void 0 : _a.init; - if (isDef(init_1)) { - init_1(vnode); - data = vnode.data; - } - } - var children = vnode.children; - var sel = vnode.sel; - if (sel === '!') { - if (isUndef(vnode.text)) { - vnode.text = ''; - } - vnode.elm = api.createComment(vnode.text); - } else if (sel !== undefined) { - var hashIdx = sel.indexOf('#'); - var dotIdx = sel.indexOf('.', hashIdx); - var hash = hashIdx > 0 ? hashIdx : sel.length; - var dot = dotIdx > 0 ? dotIdx : sel.length; - var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; - var elm = (vnode.elm = isDef(data) && isDef((i = data.ns)) ? api.createElementNS(i, tag, data) : api.createElement(tag, data)); - if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)); - if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')); - for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); - if (is.array(children)) { - for (i = 0; i < children.length; ++i) { - var ch = children[i]; - if (ch != null) { - api.appendChild(elm, createElm(ch, insertedVnodeQueue)); - } - } - } else if (is.primitive(vnode.text)) { - api.appendChild(elm, api.createTextNode(vnode.text)); - } - var hook = vnode.data.hook; - if (isDef(hook)) { - (_b = hook.create) === null || _b === void 0 ? void 0 : _b.call(hook, emptyNode, vnode); - if (hook.insert) { - insertedVnodeQueue.push(vnode); - } - } - } else { - vnode.elm = api.createTextNode(vnode.text); - } - return vnode.elm; - } - - function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - var ch = vnodes[startIdx]; - if (ch != null) { - api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); - } - } - } - - function invokeDestroyHook(vnode) { - var _a, _b; - var data = vnode.data; - if (data !== undefined) { - (_b = (_a = data === null || data === void 0 ? void 0 : data.hook) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a, vnode); - for (var i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); - if (vnode.children !== undefined) { - for (var j = 0; j < vnode.children.length; ++j) { - var child = vnode.children[j]; - if (child != null && typeof child !== 'string') { - invokeDestroyHook(child); - } - } - } - } - } - - function removeVnodes(parentElm, vnodes, startIdx, endIdx) { - var _a, _b; - for (; startIdx <= endIdx; ++startIdx) { - var listeners = void 0; - var rm = void 0; - var ch = vnodes[startIdx]; - if (ch != null) { - if (isDef(ch.sel)) { - invokeDestroyHook(ch); - listeners = cbs.remove.length + 1; - rm = createRmCb(ch.elm, listeners); - for (var i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); - var removeHook = (_b = (_a = ch === null || ch === void 0 ? void 0 : ch.data) === null || _a === void 0 ? void 0 : _a.hook) === null || _b === void 0 ? void 0 : _b.remove; - if (isDef(removeHook)) { - removeHook(ch, rm); - } else { - rm(); - } - } else { - api.removeChild(parentElm, ch.elm); - } - } - } - } - - function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { - var oldStartIdx = 0; - var newStartIdx = 0; - var oldEndIdx = oldCh.length - 1; - var oldStartVnode = oldCh[0]; - var oldEndVnode = oldCh[oldEndIdx]; - var newEndIdx = newCh.length - 1; - var newStartVnode = newCh[0]; - var newEndVnode = newCh[newEndIdx]; - var oldKeyToIdx; - var idxInOld; - var elmToMove; - var before; - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldStartVnode == null) { - oldStartVnode = oldCh[++oldStartIdx]; - } else if (oldEndVnode == null) { - oldEndVnode = oldCh[--oldEndIdx]; - } else if (newStartVnode == null) { - newStartVnode = newCh[++newStartIdx]; - } else if (newEndVnode == null) { - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldStartVnode, newEndVnode)) { - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldEndVnode, newStartVnode)) { - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } else { - if (oldKeyToIdx === undefined) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - } - idxInOld = oldKeyToIdx[newStartVnode.key]; - if (isUndef(idxInOld)) { - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - } else { - elmToMove = oldCh[idxInOld]; - if (elmToMove.sel !== newStartVnode.sel) { - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - } else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); - } - } - newStartVnode = newCh[++newStartIdx]; - } - } - if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { - if (oldStartIdx > oldEndIdx) { - before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } else { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } - } - - function patchVnode(oldVnode, vnode, insertedVnodeQueue) { - var _a, _b, _c, _d, _e; - var hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook; - (_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode); - var elm = (vnode.elm = oldVnode.elm); - var oldCh = oldVnode.children; - var ch = vnode.children; - if (oldVnode === vnode) return; - if (vnode.data !== undefined) { - for (var i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); - (_d = (_c = vnode.data.hook) === null || _c === void 0 ? void 0 : _c.update) === null || _d === void 0 ? void 0 : _d.call(_c, oldVnode, vnode); - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); - } else if (isDef(ch)) { - if (isDef(oldVnode.text)) api.setTextContent(elm, ''); - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } else if (isDef(oldVnode.text)) { - api.setTextContent(elm, ''); - } - } else if (oldVnode.text !== vnode.text) { - if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - api.setTextContent(elm, vnode.text); - } - (_e = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _e === void 0 ? void 0 : _e.call(hook, oldVnode, vnode); - } - - return function patch(oldVnode, vnode) { - var i, elm, parent; - var insertedVnodeQueue = []; - for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); - if (!isVnode(oldVnode)) { - oldVnode = emptyNodeAt(oldVnode); - } - if (sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue); - } else { - elm = oldVnode.elm; - parent = api.parentNode(elm); - createElm(vnode, insertedVnodeQueue); - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } - } - for (i = 0; i < insertedVnodeQueue.length; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); - } - for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); - return vnode; - }; - } - - exports.init = init; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.init = void 0; + is = __importStar(is); + function isUndef(s) { + return s === undefined; + } + function isDef(s) { + return s !== undefined; + } + var emptyNode = (0, vnode_2.vnode)('', {}, [], undefined, undefined); + function sameVnode(vnode1, vnode2) { + var _a, _b; + var isSameKey = vnode1.key === vnode2.key; + var isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is); + var isSameSel = vnode1.sel === vnode2.sel; + return isSameSel && isSameKey && isSameIs; + } + function isVnode(vnode) { + return vnode.sel !== undefined; + } + function createKeyToOldIdx(children, beginIdx, endIdx) { + var _a; + var map = {}; + for (var i = beginIdx; i <= endIdx; ++i) { + var key = (_a = children[i]) === null || _a === void 0 ? void 0 : _a.key; + if (key !== undefined) { + map[key] = i; + } + } + return map; + } + var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; + function init(modules, domApi) { + var cbs = { + create: [], + update: [], + remove: [], + destroy: [], + pre: [], + post: [], + }; + var api = domApi !== undefined ? domApi : htmldomapi_1.htmlDomApi; + for (var _i = 0, hooks_1 = hooks; _i < hooks_1.length; _i++) { + var hook = hooks_1[_i]; + for (var _f = 0, modules_1 = modules; _f < modules_1.length; _f++) { + var module_1 = modules_1[_f]; + var currentHook = module_1[hook]; + if (currentHook !== undefined) { + cbs[hook].push(currentHook); + } + } + } + function emptyNodeAt(elm) { + var id = elm.id ? '#' + elm.id : ''; + var classes = elm.getAttribute('class'); + var c = classes ? '.' + classes.split(' ').join('.') : ''; + return (0, vnode_2.vnode)(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); + } + function createRmCb(childElm, listeners) { + return function rmCb() { + if (--listeners === 0) { + var parent_1 = api.parentNode(childElm); + api.removeChild(parent_1, childElm); + } + }; + } + function createElm(vnode, insertedVnodeQueue) { + var _a, _b; + var i; + var data = vnode.data; + if (data !== undefined) { + var init_1 = (_a = data.hook) === null || _a === void 0 ? void 0 : _a.init; + if (isDef(init_1)) { + init_1(vnode); + data = vnode.data; + } + } + var children = vnode.children; + var sel = vnode.sel; + if (sel === '!') { + if (isUndef(vnode.text)) { + vnode.text = ''; + } + vnode.elm = api.createComment(vnode.text); + } else if (sel !== undefined) { + var hashIdx = sel.indexOf('#'); + var dotIdx = sel.indexOf('.', hashIdx); + var hash = hashIdx > 0 ? hashIdx : sel.length; + var dot = dotIdx > 0 ? dotIdx : sel.length; + var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; + var elm = (vnode.elm = isDef(data) && isDef((i = data.ns)) ? api.createElementNS(i, tag, data) : api.createElement(tag, data)); + if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)); + if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')); + for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); + if (is.array(children)) { + for (i = 0; i < children.length; ++i) { + var ch = children[i]; + if (ch != null) { + api.appendChild(elm, createElm(ch, insertedVnodeQueue)); + } + } + } else if (is.primitive(vnode.text)) { + api.appendChild(elm, api.createTextNode(vnode.text)); + } + var hook = vnode.data.hook; + if (isDef(hook)) { + (_b = hook.create) === null || _b === void 0 ? void 0 : _b.call(hook, emptyNode, vnode); + if (hook.insert) { + insertedVnodeQueue.push(vnode); + } + } + } else { + vnode.elm = api.createTextNode(vnode.text); + } + return vnode.elm; + } + function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { + for (; startIdx <= endIdx; ++startIdx) { + var ch = vnodes[startIdx]; + if (ch != null) { + api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); + } + } + } + function invokeDestroyHook(vnode) { + var _a, _b; + var data = vnode.data; + if (data !== undefined) { + (_b = (_a = data === null || data === void 0 ? void 0 : data.hook) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a, vnode); + for (var i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); + if (vnode.children !== undefined) { + for (var j = 0; j < vnode.children.length; ++j) { + var child = vnode.children[j]; + if (child != null && typeof child !== 'string') { + invokeDestroyHook(child); + } + } + } + } + } + function removeVnodes(parentElm, vnodes, startIdx, endIdx) { + var _a, _b; + for (; startIdx <= endIdx; ++startIdx) { + var listeners = void 0; + var rm = void 0; + var ch = vnodes[startIdx]; + if (ch != null) { + if (isDef(ch.sel)) { + invokeDestroyHook(ch); + listeners = cbs.remove.length + 1; + rm = createRmCb(ch.elm, listeners); + for (var i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); + var removeHook = (_b = (_a = ch === null || ch === void 0 ? void 0 : ch.data) === null || _a === void 0 ? void 0 : _a.hook) === null || _b === void 0 ? void 0 : _b.remove; + if (isDef(removeHook)) { + removeHook(ch, rm); + } else { + rm(); + } + } else { + api.removeChild(parentElm, ch.elm); + } + } + } + } + function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { + var oldStartIdx = 0; + var newStartIdx = 0; + var oldEndIdx = oldCh.length - 1; + var oldStartVnode = oldCh[0]; + var oldEndVnode = oldCh[oldEndIdx]; + var newEndIdx = newCh.length - 1; + var newStartVnode = newCh[0]; + var newEndVnode = newCh[newEndIdx]; + var oldKeyToIdx; + var idxInOld; + var elmToMove; + var before; + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (oldStartVnode == null) { + oldStartVnode = oldCh[++oldStartIdx]; + } else if (oldEndVnode == null) { + oldEndVnode = oldCh[--oldEndIdx]; + } else if (newStartVnode == null) { + newStartVnode = newCh[++newStartIdx]; + } else if (newEndVnode == null) { + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + } else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newEndVnode)) { + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } else { + if (oldKeyToIdx === undefined) { + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + idxInOld = oldKeyToIdx[newStartVnode.key]; + if (isUndef(idxInOld)) { + api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); + } else { + elmToMove = oldCh[idxInOld]; + if (elmToMove.sel !== newStartVnode.sel) { + api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); + } else { + patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); + oldCh[idxInOld] = undefined; + api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { + if (oldStartIdx > oldEndIdx) { + before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; + addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } else { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } + } + function patchVnode(oldVnode, vnode, insertedVnodeQueue) { + var _a, _b, _c, _d, _e; + var hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook; + (_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode); + var elm = (vnode.elm = oldVnode.elm); + var oldCh = oldVnode.children; + var ch = vnode.children; + if (oldVnode === vnode) return; + if (vnode.data !== undefined) { + for (var i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); + (_d = (_c = vnode.data.hook) === null || _c === void 0 ? void 0 : _c.update) === null || _d === void 0 ? void 0 : _d.call(_c, oldVnode, vnode); + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); + } else if (isDef(ch)) { + if (isDef(oldVnode.text)) api.setTextContent(elm, ''); + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); + } else if (isDef(oldCh)) { + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } else if (isDef(oldVnode.text)) { + api.setTextContent(elm, ''); + } + } else if (oldVnode.text !== vnode.text) { + if (isDef(oldCh)) { + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } + api.setTextContent(elm, vnode.text); + } + (_e = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _e === void 0 ? void 0 : _e.call(hook, oldVnode, vnode); + } + return function patch(oldVnode, vnode) { + var i, elm, parent; + var insertedVnodeQueue = []; + for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); + if (!isVnode(oldVnode)) { + oldVnode = emptyNodeAt(oldVnode); + } + if (sameVnode(oldVnode, vnode)) { + patchVnode(oldVnode, vnode, insertedVnodeQueue); + } else { + elm = oldVnode.elm; + parent = api.parentNode(elm); + createElm(vnode, insertedVnodeQueue); + if (parent !== null) { + api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); + removeVnodes(parent, [oldVnode], 0, 0); + } + } + for (i = 0; i < insertedVnodeQueue.length; ++i) { + insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); + } + for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); + return vnode; + }; + } + exports.init = init; }); define('is', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.primitive = exports.array = void 0; - exports.array = Array.isArray; - - function primitive(s) { - return typeof s === 'string' || typeof s === 'number' || s instanceof String || s instanceof Number; - } - - exports.primitive = primitive; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.primitive = exports.array = void 0; + exports.array = Array.isArray; + function primitive(s) { + return typeof s === 'string' || typeof s === 'number' || s instanceof String || s instanceof Number; + } + exports.primitive = primitive; }); define('jsx', ['require', 'exports', './vnode', './h'], function (require, exports, vnode_3, h_1) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.jsx = void 0; - - function flattenAndFilter(children, flattened) { - for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { - var child = children_1[_i]; - if (child !== undefined && child !== null && child !== false && child !== '') { - if (Array.isArray(child)) { - flattenAndFilter(child, flattened); - } else if (typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean') { - flattened.push((0, vnode_3.vnode)(undefined, undefined, undefined, String(child), undefined)); - } else { - flattened.push(child); - } - } - } - return flattened; - } - - function jsx(tag, data) { - var children = []; - for (var _i = 2; _i < arguments.length; _i++) { - children[_i - 2] = arguments[_i]; - } - var flatChildren = flattenAndFilter(children, []); - if (typeof tag === 'function') { - return tag(data, flatChildren); - } else { - if (flatChildren.length === 1 && !flatChildren[0].sel && flatChildren[0].text) { - return (0, h_1.h)(tag, data, flatChildren[0].text); - } else { - return (0, h_1.h)(tag, data, flatChildren); - } - } - } - - exports.jsx = jsx; - (function (jsx) { - })(jsx || (exports.jsx = jsx = {})); + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.jsx = void 0; + function flattenAndFilter(children, flattened) { + for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { + var child = children_1[_i]; + if (child !== undefined && child !== null && child !== false && child !== '') { + if (Array.isArray(child)) { + flattenAndFilter(child, flattened); + } else if (typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean') { + flattened.push((0, vnode_3.vnode)(undefined, undefined, undefined, String(child), undefined)); + } else { + flattened.push(child); + } + } + } + return flattened; + } + function jsx(tag, data) { + var children = []; + for (var _i = 2; _i < arguments.length; _i++) { + children[_i - 2] = arguments[_i]; + } + var flatChildren = flattenAndFilter(children, []); + if (typeof tag === 'function') { + return tag(data, flatChildren); + } else { + if (flatChildren.length === 1 && !flatChildren[0].sel && flatChildren[0].text) { + return (0, h_1.h)(tag, data, flatChildren[0].text); + } else { + return (0, h_1.h)(tag, data, flatChildren); + } + } + } + exports.jsx = jsx; + (function (jsx) {})(jsx || (exports.jsx = jsx = {})); }); define('snabbdom', [ - 'require', - 'exports', - './htmldomapi', - './init', - './thunk', - './vnode', - './helpers/attachto', - './is', - './tovnode', - './h', - './hooks', - './modules/attributes', - './modules/class', - './modules/dataset', - './modules/eventlisteners', - './modules/props', - './modules/style', - './jsx', + 'require', + 'exports', + './htmldomapi', + './init', + './thunk', + './vnode', + './helpers/attachto', + './is', + './tovnode', + './h', + './hooks', + './modules/attributes', + './modules/class', + './modules/dataset', + './modules/eventlisteners', + './modules/props', + './modules/style', + './jsx', ], function (require, exports, htmldomapi_2, init_2, thunk_1, vnode_4, attachto_1, is_1, tovnode_1, h_2, hooks_2, attributes_1, class_1, dataset_1, eventlisteners_1, props_1, style_1, jsx_1) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.jsx = - exports.styleModule = - exports.propsModule = - exports.eventListenersModule = - exports.datasetModule = - exports.classModule = - exports.attributesModule = - exports.h = - exports.toVNode = - exports.primitive = - exports.array = - exports.attachTo = - exports.vnode = - exports.thunk = - exports.init = - exports.htmlDomApi = - void 0; - Object.defineProperty(exports, 'htmlDomApi', { - enumerable: true, - get: function () { - return htmldomapi_2.htmlDomApi; - }, - }); - Object.defineProperty(exports, 'init', { - enumerable: true, - get: function () { - return init_2.init; - }, - }); - Object.defineProperty(exports, 'thunk', { - enumerable: true, - get: function () { - return thunk_1.thunk; - }, - }); - Object.defineProperty(exports, 'vnode', { - enumerable: true, - get: function () { - return vnode_4.vnode; - }, - }); - Object.defineProperty(exports, 'attachTo', { - enumerable: true, - get: function () { - return attachto_1.attachTo; - }, - }); - Object.defineProperty(exports, 'array', { - enumerable: true, - get: function () { - return is_1.array; - }, - }); - Object.defineProperty(exports, 'primitive', { - enumerable: true, - get: function () { - return is_1.primitive; - }, - }); - Object.defineProperty(exports, 'toVNode', { - enumerable: true, - get: function () { - return tovnode_1.toVNode; - }, - }); - Object.defineProperty(exports, 'h', { - enumerable: true, - get: function () { - return h_2.h; - }, - }); - __exportStar(hooks_2, exports); - Object.defineProperty(exports, 'attributesModule', { - enumerable: true, - get: function () { - return attributes_1.attributesModule; - }, - }); - Object.defineProperty(exports, 'classModule', { - enumerable: true, - get: function () { - return class_1.classModule; - }, - }); - Object.defineProperty(exports, 'datasetModule', { - enumerable: true, - get: function () { - return dataset_1.datasetModule; - }, - }); - Object.defineProperty(exports, 'eventListenersModule', { - enumerable: true, - get: function () { - return eventlisteners_1.eventListenersModule; - }, - }); - Object.defineProperty(exports, 'propsModule', { - enumerable: true, - get: function () { - return props_1.propsModule; - }, - }); - Object.defineProperty(exports, 'styleModule', { - enumerable: true, - get: function () { - return style_1.styleModule; - }, - }); - Object.defineProperty(exports, 'jsx', { - enumerable: true, - get: function () { - return jsx_1.jsx; - }, - }); + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.jsx = + exports.styleModule = + exports.propsModule = + exports.eventListenersModule = + exports.datasetModule = + exports.classModule = + exports.attributesModule = + exports.h = + exports.toVNode = + exports.primitive = + exports.array = + exports.attachTo = + exports.vnode = + exports.thunk = + exports.init = + exports.htmlDomApi = + void 0; + Object.defineProperty(exports, 'htmlDomApi', { + enumerable: true, + get: function () { + return htmldomapi_2.htmlDomApi; + }, + }); + Object.defineProperty(exports, 'init', { + enumerable: true, + get: function () { + return init_2.init; + }, + }); + Object.defineProperty(exports, 'thunk', { + enumerable: true, + get: function () { + return thunk_1.thunk; + }, + }); + Object.defineProperty(exports, 'vnode', { + enumerable: true, + get: function () { + return vnode_4.vnode; + }, + }); + Object.defineProperty(exports, 'attachTo', { + enumerable: true, + get: function () { + return attachto_1.attachTo; + }, + }); + Object.defineProperty(exports, 'array', { + enumerable: true, + get: function () { + return is_1.array; + }, + }); + Object.defineProperty(exports, 'primitive', { + enumerable: true, + get: function () { + return is_1.primitive; + }, + }); + Object.defineProperty(exports, 'toVNode', { + enumerable: true, + get: function () { + return tovnode_1.toVNode; + }, + }); + Object.defineProperty(exports, 'h', { + enumerable: true, + get: function () { + return h_2.h; + }, + }); + __exportStar(hooks_2, exports); + Object.defineProperty(exports, 'attributesModule', { + enumerable: true, + get: function () { + return attributes_1.attributesModule; + }, + }); + Object.defineProperty(exports, 'classModule', { + enumerable: true, + get: function () { + return class_1.classModule; + }, + }); + Object.defineProperty(exports, 'datasetModule', { + enumerable: true, + get: function () { + return dataset_1.datasetModule; + }, + }); + Object.defineProperty(exports, 'eventListenersModule', { + enumerable: true, + get: function () { + return eventlisteners_1.eventListenersModule; + }, + }); + Object.defineProperty(exports, 'propsModule', { + enumerable: true, + get: function () { + return props_1.propsModule; + }, + }); + Object.defineProperty(exports, 'styleModule', { + enumerable: true, + get: function () { + return style_1.styleModule; + }, + }); + Object.defineProperty(exports, 'jsx', { + enumerable: true, + get: function () { + return jsx_1.jsx; + }, + }); }); define('thunk', ['require', 'exports', './h'], function (require, exports, h_3) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.thunk = void 0; - - function copyToThunk(vnode, thunk) { - vnode.data.fn = thunk.data.fn; - vnode.data.args = thunk.data.args; - thunk.data = vnode.data; - thunk.children = vnode.children; - thunk.text = vnode.text; - thunk.elm = vnode.elm; - } - - function init(thunk) { - var cur = thunk.data; - var vnode = cur.fn.apply(cur, cur.args); - copyToThunk(vnode, thunk); - } - - function prepatch(oldVnode, thunk) { - var i; - var old = oldVnode.data; - var cur = thunk.data; - var oldArgs = old.args; - var args = cur.args; - if (old.fn !== cur.fn || oldArgs.length !== args.length) { - copyToThunk(cur.fn.apply(cur, args), thunk); - return; - } - for (i = 0; i < args.length; ++i) { - if (oldArgs[i] !== args[i]) { - copyToThunk(cur.fn.apply(cur, args), thunk); - return; - } - } - copyToThunk(oldVnode, thunk); - } - - var thunk = function thunk(sel, key, fn, args) { - if (args === undefined) { - args = fn; - fn = key; - key = undefined; - } - return (0, h_3.h)(sel, { - key: key, - hook: {init: init, prepatch: prepatch}, - fn: fn, - args: args, - }); - }; - exports.thunk = thunk; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.thunk = void 0; + function copyToThunk(vnode, thunk) { + vnode.data.fn = thunk.data.fn; + vnode.data.args = thunk.data.args; + thunk.data = vnode.data; + thunk.children = vnode.children; + thunk.text = vnode.text; + thunk.elm = vnode.elm; + } + function init(thunk) { + var cur = thunk.data; + var vnode = cur.fn.apply(cur, cur.args); + copyToThunk(vnode, thunk); + } + function prepatch(oldVnode, thunk) { + var i; + var old = oldVnode.data; + var cur = thunk.data; + var oldArgs = old.args; + var args = cur.args; + if (old.fn !== cur.fn || oldArgs.length !== args.length) { + copyToThunk(cur.fn.apply(cur, args), thunk); + return; + } + for (i = 0; i < args.length; ++i) { + if (oldArgs[i] !== args[i]) { + copyToThunk(cur.fn.apply(cur, args), thunk); + return; + } + } + copyToThunk(oldVnode, thunk); + } + var thunk = function thunk(sel, key, fn, args) { + if (args === undefined) { + args = fn; + fn = key; + key = undefined; + } + return (0, h_3.h)(sel, { + key: key, + hook: { init: init, prepatch: prepatch }, + fn: fn, + args: args, + }); + }; + exports.thunk = thunk; }); define('tovnode', ['require', 'exports', './vnode', './htmldomapi'], function (require, exports, vnode_5, htmldomapi_3) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.toVNode = void 0; - - function toVNode(node, domApi) { - var api = domApi !== undefined ? domApi : htmldomapi_3.htmlDomApi; - var text; - if (api.isElement(node)) { - var id = node.id ? '#' + node.id : ''; - var cn = node.getAttribute('class'); - var c = cn ? '.' + cn.split(' ').join('.') : ''; - var sel = api.tagName(node).toLowerCase() + id + c; - var attrs = {}; - var children = []; - var name_1; - var i = void 0, - n = void 0; - var elmAttrs = node.attributes; - var elmChildren = node.childNodes; - for (i = 0, n = elmAttrs.length; i < n; i++) { - name_1 = elmAttrs[i].nodeName; - if (name_1 !== 'id' && name_1 !== 'class') { - attrs[name_1] = elmAttrs[i].nodeValue; - } - } - for (i = 0, n = elmChildren.length; i < n; i++) { - children.push(toVNode(elmChildren[i], domApi)); - } - return (0, vnode_5.vnode)(sel, {attrs: attrs}, children, undefined, node); - } else if (api.isText(node)) { - text = api.getTextContent(node); - return (0, vnode_5.vnode)(undefined, undefined, undefined, text, node); - } else if (api.isComment(node)) { - text = api.getTextContent(node); - return (0, vnode_5.vnode)('!', {}, [], text, node); - } else { - return (0, vnode_5.vnode)('', {}, [], undefined, node); - } - } - - exports.toVNode = toVNode; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.toVNode = void 0; + function toVNode(node, domApi) { + var api = domApi !== undefined ? domApi : htmldomapi_3.htmlDomApi; + var text; + if (api.isElement(node)) { + var id = node.id ? '#' + node.id : ''; + var cn = node.getAttribute('class'); + var c = cn ? '.' + cn.split(' ').join('.') : ''; + var sel = api.tagName(node).toLowerCase() + id + c; + var attrs = {}; + var children = []; + var name_1; + var i = void 0, + n = void 0; + var elmAttrs = node.attributes; + var elmChildren = node.childNodes; + for (i = 0, n = elmAttrs.length; i < n; i++) { + name_1 = elmAttrs[i].nodeName; + if (name_1 !== 'id' && name_1 !== 'class') { + attrs[name_1] = elmAttrs[i].nodeValue; + } + } + for (i = 0, n = elmChildren.length; i < n; i++) { + children.push(toVNode(elmChildren[i], domApi)); + } + return (0, vnode_5.vnode)(sel, { attrs: attrs }, children, undefined, node); + } else if (api.isText(node)) { + text = api.getTextContent(node); + return (0, vnode_5.vnode)(undefined, undefined, undefined, text, node); + } else if (api.isComment(node)) { + text = api.getTextContent(node); + return (0, vnode_5.vnode)('!', {}, [], text, node); + } else { + return (0, vnode_5.vnode)('', {}, [], undefined, node); + } + } + exports.toVNode = toVNode; }); define('vnode', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.vnode = void 0; - - function vnode(sel, data, children, text, elm) { - var key = data === undefined ? undefined : data.key; - return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; - } - - exports.vnode = vnode; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.vnode = void 0; + function vnode(sel, data, children, text, elm) { + var key = data === undefined ? undefined : data.key; + return { sel: sel, data: data, children: children, text: text, elm: elm, key: key }; + } + exports.vnode = vnode; }); define('helpers/attachto', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.attachTo = void 0; - - function pre(vnode, newVnode) { - var attachData = vnode.data.attachData; - newVnode.data.attachData.placeholder = attachData.placeholder; - newVnode.data.attachData.real = attachData.real; - vnode.elm = vnode.data.attachData.real; - } - - function post(_, vnode) { - vnode.elm = vnode.data.attachData.placeholder; - } - - function destroy(vnode) { - if (vnode.elm !== undefined) { - vnode.elm.parentNode.removeChild(vnode.elm); - } - vnode.elm = vnode.data.attachData.real; - } - - function create(_, vnode) { - var real = vnode.elm; - var attachData = vnode.data.attachData; - var placeholder = document.createElement('span'); - vnode.elm = placeholder; - attachData.target.appendChild(real); - attachData.real = real; - attachData.placeholder = placeholder; - } - - function attachTo(target, vnode) { - if (vnode.data === undefined) vnode.data = {}; - if (vnode.data.hook === undefined) vnode.data.hook = {}; - var data = vnode.data; - var hook = vnode.data.hook; - data.attachData = {target: target, placeholder: undefined, real: undefined}; - hook.create = create; - hook.prepatch = pre; - hook.postpatch = post; - hook.destroy = destroy; - return vnode; - } - - exports.attachTo = attachTo; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.attachTo = void 0; + function pre(vnode, newVnode) { + var attachData = vnode.data.attachData; + newVnode.data.attachData.placeholder = attachData.placeholder; + newVnode.data.attachData.real = attachData.real; + vnode.elm = vnode.data.attachData.real; + } + function post(_, vnode) { + vnode.elm = vnode.data.attachData.placeholder; + } + function destroy(vnode) { + if (vnode.elm !== undefined) { + vnode.elm.parentNode.removeChild(vnode.elm); + } + vnode.elm = vnode.data.attachData.real; + } + function create(_, vnode) { + var real = vnode.elm; + var attachData = vnode.data.attachData; + var placeholder = document.createElement('span'); + vnode.elm = placeholder; + attachData.target.appendChild(real); + attachData.real = real; + attachData.placeholder = placeholder; + } + function attachTo(target, vnode) { + if (vnode.data === undefined) vnode.data = {}; + if (vnode.data.hook === undefined) vnode.data.hook = {}; + var data = vnode.data; + var hook = vnode.data.hook; + data.attachData = { target: target, placeholder: undefined, real: undefined }; + hook.create = create; + hook.prepatch = pre; + hook.postpatch = post; + hook.destroy = destroy; + return vnode; + } + exports.attachTo = attachTo; }); define('modules/attributes', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.attributesModule = void 0; - var xlinkNS = 'http://www.w3.org/1999/xlink'; - var xmlNS = 'http://www.w3.org/XML/1998/namespace'; - var colonChar = 58; - var xChar = 120; - - function updateAttrs(oldVnode, vnode) { - var key; - var elm = vnode.elm; - var oldAttrs = oldVnode.data.attrs; - var attrs = vnode.data.attrs; - if (!oldAttrs && !attrs) return; - if (oldAttrs === attrs) return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - for (key in attrs) { - var cur = attrs[key]; - var old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ''); - } else if (cur === false) { - elm.removeAttribute(key); - } else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } else if (key.charCodeAt(3) === colonChar) { - elm.setAttributeNS(xmlNS, key, cur); - } else if (key.charCodeAt(5) === colonChar) { - elm.setAttributeNS(xlinkNS, key, cur); - } else { - elm.setAttribute(key, cur); - } - } - } - } - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); - } - } - } - - exports.attributesModule = { - create: updateAttrs, - update: updateAttrs, - }; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.attributesModule = void 0; + var xlinkNS = 'http://www.w3.org/1999/xlink'; + var xmlNS = 'http://www.w3.org/XML/1998/namespace'; + var colonChar = 58; + var xChar = 120; + function updateAttrs(oldVnode, vnode) { + var key; + var elm = vnode.elm; + var oldAttrs = oldVnode.data.attrs; + var attrs = vnode.data.attrs; + if (!oldAttrs && !attrs) return; + if (oldAttrs === attrs) return; + oldAttrs = oldAttrs || {}; + attrs = attrs || {}; + for (key in attrs) { + var cur = attrs[key]; + var old = oldAttrs[key]; + if (old !== cur) { + if (cur === true) { + elm.setAttribute(key, ''); + } else if (cur === false) { + elm.removeAttribute(key); + } else { + if (key.charCodeAt(0) !== xChar) { + elm.setAttribute(key, cur); + } else if (key.charCodeAt(3) === colonChar) { + elm.setAttributeNS(xmlNS, key, cur); + } else if (key.charCodeAt(5) === colonChar) { + elm.setAttributeNS(xlinkNS, key, cur); + } else { + elm.setAttribute(key, cur); + } + } + } + } + for (key in oldAttrs) { + if (!(key in attrs)) { + elm.removeAttribute(key); + } + } + } + exports.attributesModule = { + create: updateAttrs, + update: updateAttrs, + }; }); define('modules/class', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.classModule = void 0; - - function updateClass(oldVnode, vnode) { - var cur; - var name; - var elm = vnode.elm; - var oldClass = oldVnode.data.class; - var klass = vnode.data.class; - if (!oldClass && !klass) return; - if (oldClass === klass) return; - oldClass = oldClass || {}; - klass = klass || {}; - for (name in oldClass) { - if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { - elm.classList.remove(name); - } - } - for (name in klass) { - cur = klass[name]; - if (cur !== oldClass[name]) { - elm.classList[cur ? 'add' : 'remove'](name); - } - } - } - - exports.classModule = {create: updateClass, update: updateClass}; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.classModule = void 0; + function updateClass(oldVnode, vnode) { + var cur; + var name; + var elm = vnode.elm; + var oldClass = oldVnode.data.class; + var klass = vnode.data.class; + if (!oldClass && !klass) return; + if (oldClass === klass) return; + oldClass = oldClass || {}; + klass = klass || {}; + for (name in oldClass) { + if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { + elm.classList.remove(name); + } + } + for (name in klass) { + cur = klass[name]; + if (cur !== oldClass[name]) { + elm.classList[cur ? 'add' : 'remove'](name); + } + } + } + exports.classModule = { create: updateClass, update: updateClass }; }); define('modules/dataset', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.datasetModule = void 0; - var CAPS_REGEX = /[A-Z]/g; - - function updateDataset(oldVnode, vnode) { - var elm = vnode.elm; - var oldDataset = oldVnode.data.dataset; - var dataset = vnode.data.dataset; - var key; - if (!oldDataset && !dataset) return; - if (oldDataset === dataset) return; - oldDataset = oldDataset || {}; - dataset = dataset || {}; - var d = elm.dataset; - for (key in oldDataset) { - if (!dataset[key]) { - if (d) { - if (key in d) { - delete d[key]; - } - } else { - elm.removeAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase()); - } - } - } - for (key in dataset) { - if (oldDataset[key] !== dataset[key]) { - if (d) { - d[key] = dataset[key]; - } else { - elm.setAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase(), dataset[key]); - } - } - } - } - - exports.datasetModule = { - create: updateDataset, - update: updateDataset, - }; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.datasetModule = void 0; + var CAPS_REGEX = /[A-Z]/g; + function updateDataset(oldVnode, vnode) { + var elm = vnode.elm; + var oldDataset = oldVnode.data.dataset; + var dataset = vnode.data.dataset; + var key; + if (!oldDataset && !dataset) return; + if (oldDataset === dataset) return; + oldDataset = oldDataset || {}; + dataset = dataset || {}; + var d = elm.dataset; + for (key in oldDataset) { + if (!dataset[key]) { + if (d) { + if (key in d) { + delete d[key]; + } + } else { + elm.removeAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase()); + } + } + } + for (key in dataset) { + if (oldDataset[key] !== dataset[key]) { + if (d) { + d[key] = dataset[key]; + } else { + elm.setAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase(), dataset[key]); + } + } + } + } + exports.datasetModule = { + create: updateDataset, + update: updateDataset, + }; }); define('modules/eventlisteners', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.eventListenersModule = void 0; - - function invokeHandler(handler, vnode, event) { - if (typeof handler === 'function') { - handler.call(vnode, event, vnode); - } else if (typeof handler === 'object') { - for (var i = 0; i < handler.length; i++) { - invokeHandler(handler[i], vnode, event); - } - } - } - - function handleEvent(event, vnode) { - var name = event.type; - var on = vnode.data.on; - if (on && on[name]) { - invokeHandler(on[name], vnode, event); - } - } - - function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); - }; - } - - function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on; - var oldListener = oldVnode.listener; - var oldElm = oldVnode.elm; - var on = vnode && vnode.data.on; - var elm = vnode && vnode.elm; - var name; - if (oldOn === on) { - return; - } - if (oldOn && oldListener) { - if (!on) { - for (name in oldOn) { - oldElm.removeEventListener(name, oldListener, false); - } - } else { - for (name in oldOn) { - if (!on[name]) { - oldElm.removeEventListener(name, oldListener, false); - } - } - } - } - if (on) { - var listener = (vnode.listener = oldVnode.listener || createListener()); - listener.vnode = vnode; - if (!oldOn) { - for (name in on) { - elm.addEventListener(name, listener, false); - } - } else { - for (name in on) { - if (!oldOn[name]) { - elm.addEventListener(name, listener, false); - } - } - } - } - } - - exports.eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners, - }; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.eventListenersModule = void 0; + function invokeHandler(handler, vnode, event) { + if (typeof handler === 'function') { + handler.call(vnode, event, vnode); + } else if (typeof handler === 'object') { + for (var i = 0; i < handler.length; i++) { + invokeHandler(handler[i], vnode, event); + } + } + } + function handleEvent(event, vnode) { + var name = event.type; + var on = vnode.data.on; + if (on && on[name]) { + invokeHandler(on[name], vnode, event); + } + } + function createListener() { + return function handler(event) { + handleEvent(event, handler.vnode); + }; + } + function updateEventListeners(oldVnode, vnode) { + var oldOn = oldVnode.data.on; + var oldListener = oldVnode.listener; + var oldElm = oldVnode.elm; + var on = vnode && vnode.data.on; + var elm = vnode && vnode.elm; + var name; + if (oldOn === on) { + return; + } + if (oldOn && oldListener) { + if (!on) { + for (name in oldOn) { + oldElm.removeEventListener(name, oldListener, false); + } + } else { + for (name in oldOn) { + if (!on[name]) { + oldElm.removeEventListener(name, oldListener, false); + } + } + } + } + if (on) { + var listener = (vnode.listener = oldVnode.listener || createListener()); + listener.vnode = vnode; + if (!oldOn) { + for (name in on) { + elm.addEventListener(name, listener, false); + } + } else { + for (name in on) { + if (!oldOn[name]) { + elm.addEventListener(name, listener, false); + } + } + } + } + } + exports.eventListenersModule = { + create: updateEventListeners, + update: updateEventListeners, + destroy: updateEventListeners, + }; }); define('modules/module', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); }); define('modules/props', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.propsModule = void 0; - - function updateProps(oldVnode, vnode) { - var key; - var cur; - var old; - var elm = vnode.elm; - var oldProps = oldVnode.data.props; - var props = vnode.data.props; - if (!oldProps && !props) return; - if (oldProps === props) return; - oldProps = oldProps || {}; - props = props || {}; - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== 'value' || elm[key] !== cur)) { - elm[key] = cur; - } - } - } - - exports.propsModule = {create: updateProps, update: updateProps}; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.propsModule = void 0; + function updateProps(oldVnode, vnode) { + var key; + var cur; + var old; + var elm = vnode.elm; + var oldProps = oldVnode.data.props; + var props = vnode.data.props; + if (!oldProps && !props) return; + if (oldProps === props) return; + oldProps = oldProps || {}; + props = props || {}; + for (key in props) { + cur = props[key]; + old = oldProps[key]; + if (old !== cur && (key !== 'value' || elm[key] !== cur)) { + elm[key] = cur; + } + } + } + exports.propsModule = { create: updateProps, update: updateProps }; }); define('modules/style', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - exports.styleModule = void 0; - var raf = (typeof window !== 'undefined' && window.requestAnimationFrame.bind(window)) || setTimeout; - var nextFrame = function (fn) { - raf(function () { - raf(fn); - }); - }; - var reflowForced = false; - - function setNextFrame(obj, prop, val) { - nextFrame(function () { - obj[prop] = val; - }); - } - - function updateStyle(oldVnode, vnode) { - var cur; - var name; - var elm = vnode.elm; - var oldStyle = oldVnode.data.style; - var style = vnode.data.style; - if (!oldStyle && !style) return; - if (oldStyle === style) return; - oldStyle = oldStyle || {}; - style = style || {}; - var oldHasDel = 'delayed' in oldStyle; - for (name in oldStyle) { - if (!style[name]) { - if (name[0] === '-' && name[1] === '-') { - elm.style.removeProperty(name); - } else { - elm.style[name] = ''; - } - } - } - for (name in style) { - cur = style[name]; - if (name === 'delayed' && style.delayed) { - for (var name2 in style.delayed) { - cur = style.delayed[name2]; - if (!oldHasDel || cur !== oldStyle.delayed[name2]) { - setNextFrame(elm.style, name2, cur); - } - } - } else if (name !== 'remove' && cur !== oldStyle[name]) { - if (name[0] === '-' && name[1] === '-') { - elm.style.setProperty(name, cur); - } else { - elm.style[name] = cur; - } - } - } - } - - function applyDestroyStyle(vnode) { - var style; - var name; - var elm = vnode.elm; - var s = vnode.data.style; - if (!s || !(style = s.destroy)) return; - for (name in style) { - elm.style[name] = style[name]; - } - } - - function applyRemoveStyle(vnode, rm) { - var s = vnode.data.style; - if (!s || !s.remove) { - rm(); - return; - } - if (!reflowForced) { - vnode.elm.offsetLeft; - reflowForced = true; - } - var name; - var elm = vnode.elm; - var i = 0; - var style = s.remove; - var amount = 0; - var applied = []; - for (name in style) { - applied.push(name); - elm.style[name] = style[name]; - } - var compStyle = getComputedStyle(elm); - var props = compStyle['transition-property'].split(', '); - for (; i < props.length; ++i) { - if (applied.indexOf(props[i]) !== -1) amount++; - } - elm.addEventListener('transitionend', function (ev) { - if (ev.target === elm) --amount; - if (amount === 0) rm(); - }); - } - - function forceReflow() { - reflowForced = false; - } - - exports.styleModule = { - pre: forceReflow, - create: updateStyle, - update: updateStyle, - destroy: applyDestroyStyle, - remove: applyRemoveStyle, - }; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.styleModule = void 0; + var raf = (typeof window !== 'undefined' && window.requestAnimationFrame.bind(window)) || setTimeout; + var nextFrame = function (fn) { + raf(function () { + raf(fn); + }); + }; + var reflowForced = false; + function setNextFrame(obj, prop, val) { + nextFrame(function () { + obj[prop] = val; + }); + } + function updateStyle(oldVnode, vnode) { + var cur; + var name; + var elm = vnode.elm; + var oldStyle = oldVnode.data.style; + var style = vnode.data.style; + if (!oldStyle && !style) return; + if (oldStyle === style) return; + oldStyle = oldStyle || {}; + style = style || {}; + var oldHasDel = 'delayed' in oldStyle; + for (name in oldStyle) { + if (!style[name]) { + if (name[0] === '-' && name[1] === '-') { + elm.style.removeProperty(name); + } else { + elm.style[name] = ''; + } + } + } + for (name in style) { + cur = style[name]; + if (name === 'delayed' && style.delayed) { + for (var name2 in style.delayed) { + cur = style.delayed[name2]; + if (!oldHasDel || cur !== oldStyle.delayed[name2]) { + setNextFrame(elm.style, name2, cur); + } + } + } else if (name !== 'remove' && cur !== oldStyle[name]) { + if (name[0] === '-' && name[1] === '-') { + elm.style.setProperty(name, cur); + } else { + elm.style[name] = cur; + } + } + } + } + function applyDestroyStyle(vnode) { + var style; + var name; + var elm = vnode.elm; + var s = vnode.data.style; + if (!s || !(style = s.destroy)) return; + for (name in style) { + elm.style[name] = style[name]; + } + } + function applyRemoveStyle(vnode, rm) { + var s = vnode.data.style; + if (!s || !s.remove) { + rm(); + return; + } + if (!reflowForced) { + vnode.elm.offsetLeft; + reflowForced = true; + } + var name; + var elm = vnode.elm; + var i = 0; + var style = s.remove; + var amount = 0; + var applied = []; + for (name in style) { + applied.push(name); + elm.style[name] = style[name]; + } + var compStyle = getComputedStyle(elm); + var props = compStyle['transition-property'].split(', '); + for (; i < props.length; ++i) { + if (applied.indexOf(props[i]) !== -1) amount++; + } + elm.addEventListener('transitionend', function (ev) { + if (ev.target === elm) --amount; + if (amount === 0) rm(); + }); + } + function forceReflow() { + reflowForced = false; + } + exports.styleModule = { + pre: forceReflow, + create: updateStyle, + update: updateStyle, + destroy: applyDestroyStyle, + remove: applyRemoveStyle, + }; }); define('public/describe', ['require', 'exports'], function (require, exports) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); }); define('public/vdom', ['require', 'exports', 'snabbdom'], function (require, exports, snabbdom_1) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - var patch = (0, snabbdom_1.init)([snabbdom_1.classModule, snabbdom_1.propsModule, snabbdom_1.styleModule, snabbdom_1.eventListenersModule]); - var Vm = (function () { - function Vm(config) { - var _this = this; - var el = config.el, - template = config.template, - methods = config.methods, - mounted = config.mounted; - this.$element = document.querySelector(''.concat(el)); - this.$el = el; - this.$methods = methods; - this.$template = template; - this.$mounted = mounted; - this.$keys = {}; - this.$watcher({ - data: config.data, - watch: function (key, oldVal, newVal) { - _this.$reanderElement(_this.$el); - }, - }); - this.$eventHandle(); - this.$reanderElement(el); - } - - Vm.prototype.$watcher = function (opts) { - this._data = this.$getBaseType(opts.data) === 'Object' ? opts.data : {}; - this.$watch = opts.watch; - for (var key in opts.data) { - this.$setData(key); - } - }; - Vm.prototype.$getBaseType = function (target) { - var typeStr = Object.prototype.toString.apply(target); - return typeStr.slice(8, -1); - }; - Vm.prototype.$reanderElement = function (el) { - var _this = this; - var jsxTemplate = (0, snabbdom_1.jsx)( - 'div', - { - props: {id: el.replace('#', '')}, - hook: { - insert: function (vnode) { - if (typeof _this.$VirtualDOM === 'undefined') _this.$mounted && _this.$mounted(); - }, - }, - }, - ' ', - this.$template(this) - ); - this.$VirtualDOM = patch(typeof this.$VirtualDOM !== 'undefined' ? this.$VirtualDOM : this.$element, jsxTemplate); - }; - Vm.prototype.$setData = function (_key) { - Object.defineProperty(this, _key, { - get: function () { - return this._data[_key]; - }, - set: function (val) { - var oldVal = this._data[_key]; - if (oldVal === val) return val; - this._data[_key] = val; - this.$watch.call(this, _key, oldVal, val); - return val; - }, - }); - }; - Vm.prototype.$eventHandle = function () { - Object.assign(this, this.$methods); - for (var key in this.$methods) { - if (Object.prototype.hasOwnProperty.call(this.$methods, key)) { - this.$methods[key].bind(this); - } - } - }; - Vm.prototype.$class = function (array) { - if (typeof array === 'string') array = array.trim().split(' '); - var classList = {}; - for (var i = 0; i < array.length; i++) classList[array[i]] = true; - return classList; - }; - Vm.prototype.$style = function (str) { - if (!str) return {}; - if (typeof str === 'object') return str; - var style = {}, - styleList = str.split(';'); - for (var i = 0; i < styleList.length; i++) { - if (styleList[i] === '') continue; - var styleItem = styleList[i].split(':'); - style[styleItem[0].trim()] = styleItem[1]; - } - return style; - }; - Vm.prototype.$ul = function (config, arry) { - if (Array.isArray(config)) { - arry = config; - config = {style: {}, className: ''}; - } - var style = config.style, - className = config.className; - var liList = arry.map(function (item, index) { - if (typeof item === 'string') item = [item]; - return (0, snabbdom_1.jsx)('li', {key: index, style: {color: item[1] || ''}}, item[0] || item); - }); - return (0, snabbdom_1.jsx)('ul', { - class: this.$class('help-info-text c7 '.concat(className || '')), - style: this.$style(style) - }, liList); - }; - Vm.prototype.$line = function (config, content) { - if (typeof config != 'object') config = {title: config}; - var title = config.title, - width = config.width, - style = config.style, - hide = config.hide; - return (0, snabbdom_1.jsx)( - 'div', - {class: {line: true, hide: hide}}, - (0, snabbdom_1.jsx)('span', { - class: {tname: true}, - style: this.$style(''.concat(width ? 'width:' + width + ';' : '').concat(style)) - }, title), - (0, snabbdom_1.jsx)('div', {class: {'info-r': true}, style: {marginLeft: width}}, content) - ); - }; - Vm.prototype.$box = function (element1, element2) { - return (0, snabbdom_1.jsx)('div', {class: this.$class('group-box')}, ' ', element1, ' ', element2); - }; - Vm.prototype.$switch = function (config) { - var checked = config.checked, - change = config.change, - model = config.model, - name = config.name; - if (model) (checked = this[model]), (name = model); - return (0, snabbdom_1.jsx)( - 'div', - {class: {'info-block': true}}, - (0, snabbdom_1.jsx)('input', { - class: this.$class('btswitch btswitch-ios'), - props: {id: model + '_vm', type: 'checkbox', name: name, checked: checked}, - on: {input: this.$inputEvent.bind(this), change: change}, - }), - (0, snabbdom_1.jsx)('label', { - style: {position: 'relative', top: '5px'}, - class: {'btswitch-btn': true}, - props: {htmlFor: model + '_vm'} - }) - ); - }; - Vm.prototype.$input = function (config) { - var name = config.name, - readonly = config.readonly, - disabled = config.disabled, - style = config.style, - value = config.value, - type = config.type, - change = config.change, - className = config.className, - model = config.model, - width = config.width, - id = config.id, - placeholder = config.placeholder, - keyup = config.keyup; - if (model) (value = this[model]), (name = model); - if (width) style = 'width:'.concat(width, ';').concat(style); - return (0, snabbdom_1.jsx)('input', { - class: this.$class('bt-input-text mr5 '.concat(className || '')), - props: { - readonly: readonly, - name: name, - type: type || 'text', - disabled: disabled, - value: value, - id: id, - placeholder: placeholder - }, - style: this.$style(style), - on: {input: this.$inputEvent.bind(this), change: change, keyup: keyup}, - }); - }; - Vm.prototype.$select = function (config) { - var style = config.style, - className = config.className, - options = config.options, - model = config.model, - name = config.name, - value = config.value, - width = config.width, - change = config.change; - if (model) (value = this[model]), (name = model); - if (width) style = 'width:'.concat(width, ';').concat(style); - var optionList = options.map(function (item, index) { - if (typeof item === 'string') item = {value: index, label: item}; - return (0, snabbdom_1.jsx)('option', { - key: index, - props: {value: item.value, selected: item.value === value} - }, item.label); - }); - return (0, snabbdom_1.jsx)( - 'select', - { - class: this.$class('bt-input-text mr5 '.concat(className || '')), - props: {name: name}, - on: {input: this.$inputEvent.bind(this), change: change}, - style: this.$style(style) - }, - optionList - ); - }; - Vm.prototype.$textarea = function (config) { - var style = config.style, - className = config.className, - value = config.value, - model = config.model, - name = config.name, - width = config.width, - height = config.height, - id = config.id; - if (model) (value = this[model]), (name = model); - if (width) style = 'width:'.concat(width, ';').concat(style); - if (height) style = 'height:'.concat(height, ';').concat(style); - return (0, snabbdom_1.jsx)( - 'textarea', - { - class: this.$class('bt-input-text '.concat(className || '')), - style: this.$style(style), - props: {name: name, id: id}, - on: {input: this.$inputEvent.bind(this)} - }, - value - ); - }; - Vm.prototype.$button = function (config) { - var type = config.type, - size = config.size, - click = config.click, - style = config.style, - className = config.className, - title = config.title, - width = config.width, - height = config.height; - if (width) style = 'width:'.concat(width, ';').concat(style); - if (height) style = 'height:'.concat(height, ';').concat(style); - return (0, snabbdom_1.jsx)( - 'button', - { - class: this.$class( - 'btn btn-' - .concat(type || 'success', ' btn-') - .concat(size || 'sm', ' ') - .concat(className || '') - ), - style: this.$style(style), - on: {click: click}, - }, - title - ); - }; - Vm.prototype.$link = function (config) { - var click = config.click, - style = config.style, - className = config.className, - title = config.title, - href = config.href, - target = config.target; - return (0, snabbdom_1.jsx)( - 'a', - { - class: this.$class('btlink ' + className), - props: {href: href || 'javascript:;', target: target || '_blank'}, - style: this.$style(style), - on: {click: click} - }, - title - ); - }; - Vm.prototype.$icon = function (config) { - var click = config.click, - style = config.style, - type = config.type; - return (0, snabbdom_1.jsx)('span', { - class: this.$class('glyphicon glyphicon-'.concat(type, ' cursor')), - style: this.$style(style), - on: {click: click} - }); - }; - Vm.prototype.$warningTitle = function (tips) { - return (0, snabbdom_1.jsx)( - 'div', - {class: {mb15: true, 'layer-info-head': true}}, - (0, snabbdom_1.jsx)('i', {class: this.$class('layui-layer-ico layui-layer-ico3 layer-info-ico')}), - (0, snabbdom_1.jsx)('h3', {class: {'layer-info-title': true}}, tips) - ); - }; - Vm.prototype.$learnMore = function (config) { - var title = config.title, - model = config.model, - className = config.className, - style = config.style, - id = config.id, - link = config.link, - relation = model + '_more'; - return (0, snabbdom_1.jsx)( - 'div', - { - class: this.$class('mt10 agreementBox '.concat(className || '')), - props: {id: id}, - style: this.$style(style) - }, - (0, snabbdom_1.jsx)( - 'div', - {class: this.$class('agreementCont')}, - this.$input({type: 'checkbox', model: model, id: relation}), - (0, snabbdom_1.jsx)('label', {props: {htmlFor: relation}}, title) - ), - link - ); - }; - Vm.prototype.$table = function (config) { - return (0, snabbdom_1.jsx)('div', {class: {divtable: true}}, (0, snabbdom_1.jsx)('table', {class: {'table table-bordered table-hover': true}})); - }; - Vm.prototype.$tab = function (config) { - var title = config.title, - content = config.content, - className = config.className, - style = config.style, - id = config.id; - return (0, snabbdom_1.jsx)( - 'div', - {class: {'bt-w-main': true}}, - (0, snabbdom_1.jsx)('div', {class: {'bt-w-menu': true}}), - (0, snabbdom_1.jsx)('div', {class: {'bt-w-con': true, pd15: true}}, {content: content}) - ); - }; - Vm.prototype.$tabItem = function (config) { - var title = config.title, - content = config.content, - hide = config.hide; - return (0, snabbdom_1.jsx)('div', {class: {'bt-w-item': true, hide: hide}, props: {title: title}}, content); - }; - Vm.prototype.$inputEvent = function (ev, fn) { - var targets = ev.target; - var targetValue = targets.value; - var targetType = targets.getAttribute('type'); - var name = targets.getAttribute('name'); - if (ev.type === 'input' && targetType === 'checkbox') { - this[name] = targets.checked; - } else { - this[name] = targetValue; - } - if (typeof fn === 'function') fn(ev); - }; - return Vm; - })(); - exports.default = Vm; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + var patch = (0, snabbdom_1.init)([snabbdom_1.classModule, snabbdom_1.propsModule, snabbdom_1.styleModule, snabbdom_1.eventListenersModule]); + var Vm = (function () { + function Vm(config) { + var _this = this; + var el = config.el, + template = config.template, + methods = config.methods, + mounted = config.mounted; + this.$element = document.querySelector(''.concat(el)); + this.$el = el; + this.$methods = methods; + this.$template = template; + this.$mounted = mounted; + this.$keys = {}; + this.$watcher({ + data: config.data, + watch: function (key, oldVal, newVal) { + _this.$reanderElement(_this.$el); + }, + }); + this.$eventHandle(); + this.$reanderElement(el); + } + Vm.prototype.$watcher = function (opts) { + this._data = this.$getBaseType(opts.data) === 'Object' ? opts.data : {}; + this.$watch = opts.watch; + for (var key in opts.data) { + this.$setData(key); + } + }; + Vm.prototype.$getBaseType = function (target) { + var typeStr = Object.prototype.toString.apply(target); + return typeStr.slice(8, -1); + }; + Vm.prototype.$reanderElement = function (el) { + var _this = this; + var jsxTemplate = (0, snabbdom_1.jsx)( + 'div', + { + props: { id: el.replace('#', '') }, + hook: { + insert: function (vnode) { + if (typeof _this.$VirtualDOM === 'undefined') _this.$mounted && _this.$mounted(); + }, + }, + }, + ' ', + this.$template(this) + ); + this.$VirtualDOM = patch(typeof this.$VirtualDOM !== 'undefined' ? this.$VirtualDOM : this.$element, jsxTemplate); + }; + Vm.prototype.$setData = function (_key) { + Object.defineProperty(this, _key, { + get: function () { + return this._data[_key]; + }, + set: function (val) { + var oldVal = this._data[_key]; + if (oldVal === val) return val; + this._data[_key] = val; + this.$watch.call(this, _key, oldVal, val); + return val; + }, + }); + }; + Vm.prototype.$eventHandle = function () { + Object.assign(this, this.$methods); + for (var key in this.$methods) { + if (Object.prototype.hasOwnProperty.call(this.$methods, key)) { + this.$methods[key].bind(this); + } + } + }; + Vm.prototype.$class = function (array) { + if (typeof array === 'string') array = array.trim().split(' '); + var classList = {}; + for (var i = 0; i < array.length; i++) classList[array[i]] = true; + return classList; + }; + Vm.prototype.$style = function (str) { + if (!str) return {}; + if (typeof str === 'object') return str; + var style = {}, + styleList = str.split(';'); + for (var i = 0; i < styleList.length; i++) { + if (styleList[i] === '') continue; + var styleItem = styleList[i].split(':'); + style[styleItem[0].trim()] = styleItem[1]; + } + return style; + }; + Vm.prototype.$ul = function (config, arry) { + if (Array.isArray(config)) { + arry = config; + config = { style: {}, className: '' }; + } + var style = config.style, + className = config.className; + var liList = arry.map(function (item, index) { + if (typeof item === 'string') item = [item]; + return (0, snabbdom_1.jsx)('li', { key: index, style: { color: item[1] || '' } }, item[0] || item); + }); + return (0, snabbdom_1.jsx)('ul', { class: this.$class('help-info-text c7 '.concat(className || '')), style: this.$style(style) }, liList); + }; + Vm.prototype.$line = function (config, content) { + if (typeof config != 'object') config = { title: config }; + var title = config.title, + width = config.width, + style = config.style, + hide = config.hide; + return (0, snabbdom_1.jsx)( + 'div', + { class: { line: true, hide: hide } }, + (0, snabbdom_1.jsx)('span', { class: { tname: true }, style: this.$style(''.concat(width ? 'width:' + width + ';' : '').concat(style)) }, title), + (0, snabbdom_1.jsx)('div', { class: { 'info-r': true }, style: { marginLeft: width } }, content) + ); + }; + Vm.prototype.$box = function (element1, element2) { + return (0, snabbdom_1.jsx)('div', { class: this.$class('group-box') }, ' ', element1, ' ', element2); + }; + Vm.prototype.$switch = function (config) { + var checked = config.checked, + change = config.change, + model = config.model, + name = config.name; + if (model) (checked = this[model]), (name = model); + return (0, snabbdom_1.jsx)( + 'div', + { class: { 'info-block': true } }, + (0, snabbdom_1.jsx)('input', { + class: this.$class('btswitch btswitch-ios'), + props: { id: model + '_vm', type: 'checkbox', name: name, checked: checked }, + on: { input: this.$inputEvent.bind(this), change: change }, + }), + (0, snabbdom_1.jsx)('label', { style: { position: 'relative', top: '5px' }, class: { 'btswitch-btn': true }, props: { htmlFor: model + '_vm' } }) + ); + }; + Vm.prototype.$input = function (config) { + var name = config.name, + readonly = config.readonly, + disabled = config.disabled, + style = config.style, + value = config.value, + type = config.type, + change = config.change, + className = config.className, + model = config.model, + width = config.width, + id = config.id, + placeholder = config.placeholder, + keyup = config.keyup; + if (model) (value = this[model]), (name = model); + if (width) style = 'width:'.concat(width, ';').concat(style); + return (0, snabbdom_1.jsx)('input', { + class: this.$class('bt-input-text mr5 '.concat(className || '')), + props: { readonly: readonly, name: name, type: type || 'text', disabled: disabled, value: value, id: id, placeholder: placeholder }, + style: this.$style(style), + on: { input: this.$inputEvent.bind(this), change: change, keyup: keyup }, + }); + }; + Vm.prototype.$select = function (config) { + var style = config.style, + className = config.className, + options = config.options, + model = config.model, + name = config.name, + value = config.value, + width = config.width, + change = config.change; + if (model) (value = this[model]), (name = model); + if (width) style = 'width:'.concat(width, ';').concat(style); + var optionList = options.map(function (item, index) { + if (typeof item === 'string') item = { value: index, label: item }; + return (0, snabbdom_1.jsx)('option', { key: index, props: { value: item.value, selected: item.value === value } }, item.label); + }); + return (0, snabbdom_1.jsx)( + 'select', + { class: this.$class('bt-input-text mr5 '.concat(className || '')), props: { name: name }, on: { input: this.$inputEvent.bind(this), change: change }, style: this.$style(style) }, + optionList + ); + }; + Vm.prototype.$textarea = function (config) { + var style = config.style, + className = config.className, + value = config.value, + model = config.model, + name = config.name, + width = config.width, + height = config.height, + id = config.id; + if (model) (value = this[model]), (name = model); + if (width) style = 'width:'.concat(width, ';').concat(style); + if (height) style = 'height:'.concat(height, ';').concat(style); + return (0, snabbdom_1.jsx)( + 'textarea', + { class: this.$class('bt-input-text '.concat(className || '')), style: this.$style(style), props: { name: name, id: id }, on: { input: this.$inputEvent.bind(this) } }, + value + ); + }; + Vm.prototype.$button = function (config) { + var type = config.type, + size = config.size, + click = config.click, + style = config.style, + className = config.className, + title = config.title, + width = config.width, + height = config.height; + if (width) style = 'width:'.concat(width, ';').concat(style); + if (height) style = 'height:'.concat(height, ';').concat(style); + return (0, snabbdom_1.jsx)( + 'button', + { + class: this.$class( + 'btn btn-' + .concat(type || 'success', ' btn-') + .concat(size || 'sm', ' ') + .concat(className || '') + ), + style: this.$style(style), + on: { click: click }, + }, + title + ); + }; + Vm.prototype.$link = function (config) { + var click = config.click, + style = config.style, + className = config.className, + title = config.title, + href = config.href, + target = config.target; + return (0, snabbdom_1.jsx)( + 'a', + { class: this.$class('btlink ' + className), props: { href: href || 'javascript:;', target: target || '_blank' }, style: this.$style(style), on: { click: click } }, + title + ); + }; + Vm.prototype.$icon = function (config) { + var click = config.click, + style = config.style, + type = config.type; + return (0, snabbdom_1.jsx)('span', { class: this.$class('glyphicon glyphicon-'.concat(type, ' cursor')), style: this.$style(style), on: { click: click } }); + }; + Vm.prototype.$warningTitle = function (tips) { + return (0, snabbdom_1.jsx)( + 'div', + { class: { mb15: true, 'layer-info-head': true } }, + (0, snabbdom_1.jsx)('i', { class: this.$class('layui-layer-ico layui-layer-ico3 layer-info-ico') }), + (0, snabbdom_1.jsx)('h3', { class: { 'layer-info-title': true } }, tips) + ); + }; + Vm.prototype.$learnMore = function (config) { + var title = config.title, + model = config.model, + className = config.className, + style = config.style, + id = config.id, + link = config.link, + relation = model + '_more'; + return (0, snabbdom_1.jsx)( + 'div', + { class: this.$class('mt10 agreementBox '.concat(className || '')), props: { id: id }, style: this.$style(style) }, + (0, snabbdom_1.jsx)( + 'div', + { class: this.$class('agreementCont') }, + this.$input({ type: 'checkbox', model: model, id: relation }), + (0, snabbdom_1.jsx)('label', { props: { htmlFor: relation } }, title) + ), + link + ); + }; + Vm.prototype.$table = function (config) { + return (0, snabbdom_1.jsx)('div', { class: { divtable: true } }, (0, snabbdom_1.jsx)('table', { class: { 'table table-bordered table-hover': true } })); + }; + Vm.prototype.$tab = function (config) { + var title = config.title, + content = config.content, + className = config.className, + style = config.style, + id = config.id; + return (0, snabbdom_1.jsx)( + 'div', + { class: { 'bt-w-main': true } }, + (0, snabbdom_1.jsx)('div', { class: { 'bt-w-menu': true } }), + (0, snabbdom_1.jsx)('div', { class: { 'bt-w-con': true, pd15: true } }, { content: content }) + ); + }; + Vm.prototype.$tabItem = function (config) { + var title = config.title, + content = config.content, + hide = config.hide; + return (0, snabbdom_1.jsx)('div', { class: { 'bt-w-item': true, hide: hide }, props: { title: title } }, content); + }; + Vm.prototype.$inputEvent = function (ev, fn) { + var targets = ev.target; + var targetValue = targets.value; + var targetType = targets.getAttribute('type'); + var name = targets.getAttribute('name'); + if (ev.type === 'input' && targetType === 'checkbox') { + this[name] = targets.checked; + } else { + this[name] = targetValue; + } + if (typeof fn === 'function') fn(ev); + }; + return Vm; + })(); + exports.default = Vm; }); define('public/utils', ['require', 'exports', 'public/vdom'], function (require, exports, vdom_1) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - vdom_1 = __importDefault(vdom_1); - var Utils = (function () { - function Utils() { - this.System = 'linux'; - this.Language = 'zh-CN'; - this.API = {}; - this.vDdomList = {}; - this.layerIndex = 0; - this.$ajaxSetup(); - this.$requestInit(); - } - - Utils.prototype.$ajaxSetup = function () { - var my_headers = {}; - var request_token_ele = document.getElementById('request_token_head'); - if (request_token_ele) { - var request_token = request_token_ele.getAttribute('token'); - if (request_token) { - my_headers['x-http-token'] = request_token; - } - } - var request_token_cookie = this.$getCookie('request_token'); - if (request_token_cookie) { - my_headers['x-cookie-token'] = request_token_cookie; - } - if (my_headers) { - $.ajaxSetup({ - headers: my_headers, - error: function (jqXHR, textStatus, errorThrown) { - if (!jqXHR.responseText) return; - if (typeof String.prototype.trim === 'undefined') { - String.prototype.trim = function () { - return String(this).replace(/^\s+|\s+$/g, ''); - }; - } - var error_key = 'We need to make sure this has a favicon so that the debugger does'; - var error_find = jqXHR.responseText.indexOf(error_key); - if (jqXHR.status == 500 && (jqXHR.responseText.indexOf('An error occurred while the panel was running') != -1 || error_find != -1)) { - if (error_find != -1) { - var error_body = jqXHR.responseText.split('', ''); - var tmp = error_body.split('During handling of the above exception, another exception occurred:'); - error_body = tmp[tmp.length - 1]; - var error_msg = - '
\ + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + vdom_1 = __importDefault(vdom_1); + var Utils = (function () { + function Utils() { + this.System = 'linux'; + this.Language = 'zh-CN'; + this.API = {}; + this.vDdomList = {}; + this.layerIndex = 0; + this.$ajaxSetup(); + this.$requestInit(); + } + Utils.prototype.$ajaxSetup = function () { + var my_headers = {}; + var request_token_ele = document.getElementById('request_token_head'); + if (request_token_ele) { + var request_token = request_token_ele.getAttribute('token'); + if (request_token) { + my_headers['x-http-token'] = request_token; + } + } + var request_token_cookie = this.$getCookie('request_token'); + if (request_token_cookie) { + my_headers['x-cookie-token'] = request_token_cookie; + } + if (my_headers) { + $.ajaxSetup({ + headers: my_headers, + error: function (jqXHR, textStatus, errorThrown) { + if (!jqXHR.responseText) return; + if (typeof String.prototype.trim === 'undefined') { + String.prototype.trim = function () { + return String(this).replace(/^\s+|\s+$/g, ''); + }; + } + var error_key = 'We need to make sure this has a favicon so that the debugger does'; + var error_find = jqXHR.responseText.indexOf(error_key); + if (jqXHR.status == 500 && (jqXHR.responseText.indexOf('An error occurred while the panel was running') != -1 || error_find != -1)) { + if (error_find != -1) { + var error_body = jqXHR.responseText.split('', ''); + var tmp = error_body.split('During handling of the above exception, another exception occurred:'); + error_body = tmp[tmp.length - 1]; + var error_msg = + '
\

An error occurred while the panel was running!

\
' +
-                                    error_body.trim() +
-                                    '
\ + error_body.trim() + + '\
    \
  • Sorry, please try to resolve this error in the following order:
  • \
  • 1. Click the repair panel in the upper right corner of the [Homepage], log out of the panel and log in again.
  • \
  • 2. If the above attempts fail to resolve this error, please screenshot this window and post it to the Pagoda Forum for help, forum address:https://forum.aapanel.com
  • \
\
'; - } else { - error_msg = jqXHR.responseText; - } - $('.layui-layer-padding').parents('.layer-anim').remove(); - $('.layui-layer-shade').remove(); - setTimeout(function () { - layer.open({ - title: false, - content: error_msg, - closeBtn: 2, - area: ['1000px', '800px'], - btn: false, - shadeClose: false, - shade: 0.3, - success: function () { - $('pre').scrollTop(100000000000); - }, - }); - }, 100); - } - }, - }); - } - }; - Utils.prototype.$checkIp = function (ip) { - var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; - return reg.test(ip); - }; - Utils.prototype.$checkIps = function (ips) { - var reg = /^\d{1, 3}\.\d{1, 3}\.\d{1, 3}\.\d{1, 3}(\/\d{1, 2})?$/; - return reg.test(ips); - }; - Utils.prototype.$checkDomainList = function (domainInfo) { - if (typeof domainInfo === 'string') domainInfo = domainInfo.split(','); - var reg = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0, 61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2, 6}$/; - for (var _i = 0, domainInfo_1 = domainInfo; _i < domainInfo_1.length; _i++) { - var item = domainInfo_1[_i]; - if (!reg.test(item)) return false; - } - return true; - }; - Utils.prototype.$checkPawComplexity = function (paw) { - var regList = { - length: /^.{8,}$/, - number: /\d+/, - lowercase: /[a-z]+/, - capital: /[A-Z]+/, - special: /[^A-Za-z0-9]+/, - }; - return false; - }; - Utils.prototype.$checkWeakCipher = function (paw) { - var checks = ['admin888', '123123123', '12345678', '45678910', '87654321', 'asdfghjkl', 'password', 'qwerqwer'], - pchecks = 'abcdefghijklmnopqrstuvwxyz1234567890', - lower = paw.toLowerCase(), - isError = ''; - for (var i = 0; i < pchecks.length; i++) { - var item = pchecks[i]; - checks.push(item + item + item + item + item + item + item + item); - } - for (var i = 0; i < checks.length; i++) { - var item = checks[i]; - if (lower === item) isError += '['.concat(item, ']'); - break; - } - return { - status: !isError, - msg: isError, - }; - }; - Utils.prototype.$checkUrl = function (url) { - var reg = /^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+/; - return reg.test(url); - }; - Utils.prototype.$checkPort = function (port) { - var reg = /^([1-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/; - return reg.test(port.toString()); - }; - Utils.prototype.$checkChinese = function (chinese) { - var reg = /^[\u4e00-\u9fa5]+$/; - return reg.test(chinese); - }; - Utils.prototype.$checkDomain = function (domain) { - var reg = /^([\w\u4e00-\u9fa5\-\*]{1, 100}\.){1, 10}([\w\u4e00-\u9fa5\-]{1, 24}|[\w\u4e00-\u9fa5\-]{1, 24}\.[\w\u4e00-\u9fa5\-]{1, 24})$/; - return reg.test(domain); - }; - Utils.prototype.$checkEmail = function (email) { - var reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/; - return reg.test(email); - }; - Utils.prototype.$checkPhone = function (phone) { - var reg = /^1[3456789]\d{9}$/; - return reg.test(phone.toString()); - }; - Utils.prototype.$containsStr = function (str, subStr) { - if (typeof str !== 'string' && typeof subStr !== 'string') return false; - return str.indexOf(subStr) > -1; - }; - Utils.prototype.$replaceTrim = function (str) { - return str.replace(/\s+/g, ''); - }; - Utils.prototype.$ltrim = function (str, l) { - var reg = new RegExp('/(^\\' + l + '+)/g'); - return str.replace(reg, ''); - }; - Utils.prototype.$rtrim = function (str, r) { - var reg = new RegExp('/(\\' + r + '+$)/g'); - return str.replace(reg, ''); - }; - Utils.prototype.$formatTime = function (time, format) { - if (format === void 0) { - format = 'yyyy/MM/dd hh:mm:ss'; - } - var timestamp = ''; - if (typeof time === 'object') timestamp = time.getTime().toString(); - if (typeof time === 'string') timestamp = new Date(time).getTime().toString(); - if (typeof time === 'number') timestamp = time.toString(); - if (timestamp.length > 10) timestamp = timestamp.substring(0, 10); - var date = new Date(parseInt(timestamp) * 1000); - var o = { - 'M+': date.getMonth() + 1, - 'd+': date.getDate(), - 'h+': date.getHours(), - 'm+': date.getMinutes(), - 's+': date.getSeconds(), - 'q+': Math.floor((date.getMonth() + 3) / 3), - S: date.getMilliseconds(), - }; - if (/(y+)/.test(format)) format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)); - for (var k in o) { - if (new RegExp('(' + k + ')').test(format)) { - format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)); - } - } - return format; - }; - Utils.prototype.$formatSize = function (bytes, isUnit, fixed, endUnit) { - if (bytes === void 0) { - bytes = 0; - } - if (isUnit === void 0) { - isUnit = true; - } - if (fixed === void 0) { - fixed = 2; - } - if (endUnit === void 0) { - endUnit = ''; - } - if (typeof bytes === 'string') bytes = parseInt(bytes); - var unit = [' B', ' KB', ' MB', ' GB', 'TB'], - c = 1024; - for (var i = 0; i < unit.length; i++) { - var cUnit = unit[i]; - var val = bytes; - if (fixed !== 0 && i === 0) val = bytes.toFixed(fixed); - if (endUnit) { - if (cUnit.trim() == endUnit.trim()) { - if (endUnit) { - return val + cUnit; - } else { - return val; - } - } - } else { - if (bytes < c) { - if (isUnit) { - return val + cUnit; - } else { - return val; - } - } - } - bytes /= c; - } - }; - Utils.prototype.$formatPath = function (path) { - var reg = /(\\)/g; - path = path.replace(reg, '/'); - return path; - }; - Utils.prototype.$getFilePath = function (filename) { - if (filename === '/') return '/'; - filename = (filename + '/').replace(/\/\//g, '/'); - var arr = filename.split('/'), - last = arr[arr.length - 1]; - return filename.replace('/' + arr[arr.length - (last === '' ? 2 : 1)], ''); - }; - Utils.prototype.$getRandom = function (len) { - if (len === void 0) { - len = 32; - } - var $chars = 'AaBbCcDdEeFfGHhiJjKkLMmNnPpRSrTsWtXwYxZyz2345678', - maxPos = $chars.length; - var password = ''; - for (var i = 0; i < len; i++) { - password += $chars.charAt(Math.floor(Math.random() * maxPos)); - } - return password; - }; - Utils.prototype.$getRandomNum = function (min, max) { - if (min === void 0) { - min = 0; - } - if (max === void 0) { - max = 9; - } - return Math.floor(Math.random() * (max - min + 1) + min); - }; - Utils.prototype.$getStorage = function (key) { - return window.localStorage.getItem(key); - }; - Utils.prototype.$setStorage = function (key, value) { - window.localStorage.setItem(key, value); - }; - Utils.prototype.$removeStorage = function (key) { - window.localStorage.removeItem(key); - }; - Utils.prototype.$getCookie = function (name) { - var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); - var itExist = document.cookie.match(reg); - if (itExist) { - var val = unescape(itExist[2]); - return val == 'undefined' ? '' : val; - } else { - return null; - } - }; - Utils.prototype.$setCookie = function (name, value, time) { - if (time === void 0) { - time = 2592000000; - } - var date = '', - expires = new Date(); - expires.setTime(expires.getTime() + time); - date = expires.toGMTString(); - var isHttps = window.location.protocol === 'https:'; - var sameSite = ';Secure; Path=/; SameSite=None'; - document.cookie = name + '=' + escape(value) + ';expires=' + time + (isHttps ? sameSite : ''); - }; - Utils.prototype.$removeCookie = function (name) { - this.$setCookie(name, '', 0); - }; - Utils.prototype.$requestInit = function () { - var _this_1 = this; - var requestTokenHead = document.getElementById('request_token_head'); - var headers = {'x-http-token': '', 'x-cookie-token': ''}; - var httpToken = (requestTokenHead && requestTokenHead.getAttribute('token')) || ''; - var cookieToken = this.$getCookie('request_token') || ''; - httpToken && (headers['x-http-token'] = httpToken); - cookieToken && (headers['x-cookie-token'] = cookieToken); - if (httpToken) { - $.ajaxSetup({ - headers: headers, - error: function (XHR) { - return __awaiter(_this_1, void 0, void 0, function () { - var resText, resStatus, monitorStr, content, errorHead, isErrorHead, errorBody, tmp; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - (resText = XHR.responseText), (resStatus = XHR.status), (monitorStr = ['/static/favicon.ico', '/static/img/qrCode.png', '']), (content = ''); - if (resText) return [2, false]; - if ( - typeof resText == 'string' && - monitorStr.some(function (item) { - return resText.indexOf(item) > -1; - }) - ) - return [2, this.$refreshBrowser('/login')]; - (errorHead = 'We need to make sure this has a favicon so that the debugger does'), (isErrorHead = resText.indexOf(errorHead) > -1); - if (resStatus === 500 && resText.indexOf('运行时发生错误') > -1 && isErrorHead) { - if (resText.indexOf('请先绑定宝塔帐号!') > -1) { - this.$refreshBrowser('/bind?redirect='.concat(encodeURIComponent(window.location.href))); - return [2, false]; - } - if (isErrorHead) { - errorBody = resText.split('', ''); - tmp = errorBody.split('During handling of the above exception, another exception occurred:'); - errorBody = tmp[tmp.length - 1]; - content = - '
\n

Something went wrong, an error occurred while the panel was running!

\n
'.concat(
-                                                        errorBody.trim(),
-                                                        '
\n
    \n
  • Sorry, an unexpected error occurred while panel was running, please try to resolve this error in the following order:
  • \n
  • 1. Click Fix Panel in the upper right corner of [Home] and log out of the panel to log in again.
  • \n
  • 2. if the above attempts fail to lift this error, please screenshot this window to the aaPanel Forum to post for help, forum address: https://forum.aapanel.com
\n
' - ); - } else { - content = resText; - } - } - return [ - 4, - this.$open({ - title: false, - content: content, - area: ['1200px', '810px'], - btn: false, - }), - ]; - case 1: - _a.sent(); - return [2]; - } - }); - }); - }, - }); - } - }; - Utils.prototype.$send = function (param, param1, param2) { - var _this_1 = this; - if (param2 === void 0) { - param2 = ''; - } - return new Promise(function (resolve, reject) { - var config = { - url: '', - method: 'POST', - msg: false, - data: {}, - }, - loading = '', - loadT; - if (typeof param === 'string') { - var urls = param.split('/'); - config.url = '/'.concat(urls[0], '?action=').concat(urls[1]); - if (typeof param1 === 'string') { - loading = param1; - } else if (typeof param1 === 'object') { - config.data = param1; - loading = param2; - } - } else if (typeof param === 'object') { - config.url = param.url; - config.method = param.method || 'POST'; - config.msg = param.msg || false; - config.data = param.data || {}; - loading = param.loading || ''; - } - if (loading) loadT = _this_1.$load(''.concat(loading)); - $.ajax({ - url: config.url, - method: config.method, - data: config.data, - success: function (rdata) { - return __awaiter(_this_1, void 0, void 0, function () { - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - if (loadT) loadT.close(); - if (!(typeof rdata === 'object')) return [3, 3]; - if (!(typeof rdata.msg === 'string' && rdata.msg.indexOf('CSRF校验失败,请重新登录面板') > -1)) return [3, 2]; - return [4, this.$confirm({ - title: 'Tips', - msg: 'Panel login has expired, please login again!' - })]; - case 1: - _a.sent(); - layer.closeAll(); - this.$load('Logging out, please wait...'); - this.$refreshBrowser('/login?dologin=True'); - return [2, false]; - case 2: - if (typeof rdata.msg === 'string' && rdata.msg === '没有权限' && !rdata.status) { - layer.closeAll(); - this.$error(rdata.msg); - return [2, false]; - } - if (typeof rdata.status === 'boolean' && !rdata.status && rdata.msg && config.msg) this.$error(rdata.msg); - _a.label = 3; - case 3: - resolve(rdata); - return [2]; - } - }); - }); - }, - error: function (err) { - reject(err); - }, - }); - }); - }; - Utils.prototype.$apiInit = function (apiInfo) { - this.API = Object.assign(this.API, apiInfo); - }; - Utils.prototype.$request = function (info, param, config) { - var _this_1 = this; - if (param === void 0) { - param = {}; - } - return new Promise(function (resolve, reject) { - return __awaiter(_this_1, void 0, void 0, function () { - var url, loading, rdata, error_1; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - if (typeof info === 'string') info = this.API[info]; - if (typeof param === 'boolean') (config = param), (param = {}); - if (typeof param === 'object' && param.hasOwnProperty('loading') && param.hasOwnProperty('msg')) (config = param), (param = {}); - if (typeof config === 'boolean') config = {msg: config}; - if (typeof config === 'undefined') config = {msg: true, loading: true}; - (url = info[0]), (loading = info[1] || ''); - if (!config.loading) loading = ''; - _a.label = 1; - case 1: - _a.trys.push([1, 3, , 4]); - return [4, this.$send(url, param, loading)]; - case 2: - rdata = _a.sent(); - config.msg && typeof rdata.msg === 'string' && this.$msg(rdata); - resolve(rdata); - return [3, 4]; - case 3: - error_1 = _a.sent(); - reject(error_1); - return [3, 4]; - case 4: - return [2]; - } - }); - }); - }); - }; - Utils.prototype.$open = function (param) { - var _this_1 = this; - var _this = this; - return new Promise(function (resolve, reject) { - var id = 'virtual-'.concat(_this_1.$getRandom(5)); - var content = param.content, - success = param.success, - yes = param.yes, - btn2 = param.btn2, - cancel = param.cancel, - title = param.title, - shadeClose = param.shadeClose, - closeBtn = param.closeBtn, - skin = param.skin, - type = param.type; - var config = typeof param.content === 'string' ? '' : param.content; - var vm; - switch (typeof param.content) { - case 'string': - content = param.content; - break; - case 'object': - content = '
'); - break; - } - var layerConfig = { - title: title, - type: type || 1, - shadeClose: shadeClose, - content: content, - skin: skin, - closeBtn: closeBtn || 2, - success: function (layers, indexs) { - if (typeof config === 'object') { - var vm_1 = new vdom_1.default(Object.assign(config, {el: '#'.concat(id)})); - vm_1['$closeLayer'] = function () { - layer.close(indexs); - return indexs; - }; - _this.vDdomList[indexs] = vm_1; - } - success && success(layers, indexs, vm); - }, - yes: function (indexs) { - _this.layerIndex = indexs; - var config = { - close: function () { - layer.close(indexs); - return indexs; - }, - vm: _this.vDdomList[indexs], - }; - yes && yes(config); - resolve(config); - }, - cancel: function (indexs) { - _this.layerIndex = indexs; - if (cancel) { - cancel && cancel(); - } else { - reject({cancel: true, type: 'cancel'}); - } - delete _this.vDdomList[indexs]; - }, - btn2: function (indexs) { - _this.layerIndex = indexs; - if (btn2) { - btn2 && btn2(); - } else { - reject({cancel: true, type: 'btn2'}); - } - delete _this.vDdomList[indexs]; - }, - end: function () { - delete _this.vDdomList[_this.layerIndex]; - }, - }; - layer.open(Object.assign(param, layerConfig)); - }); - }; - Utils.prototype.$confirm = function (param) { - return new Promise(function (resolve, reject) { - var msg = ''; - if (param.hasOwnProperty('msg')) { - msg = param.msg; - delete param.msg; - } - layer.confirm( - msg, - Object.assign( - { - title: 'Tips', - icon: 3, - btn: [lan.public.confirm, lan.public.cancel], - shadeClose: false, - closeBtn: 2, - cancel: function () { - return reject({cancel: true}); - }, - }, - param - ), - function (indexs) { - return resolve(indexs); - }, - function () { - return reject({cancel: true}); - } - ); - }); - }; - Utils.prototype.$tips = function (param) { - var msg = '', - el = ''; - if (param.msg) { - msg = param.msg; - delete param.msg; - } - if (param.el) { - el = param.el; - delete param.el; - } - param.success = function (layero) { - var oldLeft = layero.css('left'); - oldLeft = oldLeft.substring(0, oldLeft.indexOf('px')); - layero.css('left', ''.concat(oldLeft - 10, 'px')); - }; - return { - layer: layer.tips(msg, el, Object.assign({tips: [1, 'red']}, param)), - close: function () { - layer.close(this.layer); - }, - }; - }; - Utils.prototype.$close = function (index) { - layer.close(index); - }; - Utils.prototype.$msg = function (param, param1) { - var config = { - time: 1500, - shade: 0.3, - shadeClose: true, - closeBtn: 0, - icon: 1, - }, - msg = ''; - if (typeof param === 'object') { - for (var key in config) { - if (Object.prototype.hasOwnProperty.call(config, key)) { - if (typeof param[key] !== 'undefined') config[key] = param[key]; - } - } - msg = param.msg + (param.msg_error || '') + (param.msg_solve || ''); - if (typeof msg == 'string' && typeof param.status === 'boolean') { - config.icon = typeof param.status === 'boolean' ? (param.status ? 1 : 2) : 1; - } - } else if (typeof param === 'string') { - msg = param; - if (typeof param1 === 'boolean' || typeof param1 === 'number') { - config.icon = typeof param1 === 'boolean' ? (param1 ? 1 : 2) : param1; - } - } - return { - layer: layer.msg(msg, config), - close: function () { - layer.close(this.layer); - }, - }; - }; - Utils.prototype.$error = function (msg) { - return this.$msg({msg: msg, icon: 2}); - }; - Utils.prototype.$warning = function (msg) { - return this.$msg({msg: msg, icon: 0}); - }; - Utils.prototype.$load = function (tips) { - if (tips === void 0) { - tips = 'Processing, please wait...'; - } - return this.$msg({msg: tips, icon: 16, time: 0, shade: [0.3, '#000']}); - }; - Utils.prototype.$verifySubmit = function (param, msg) { - var _this_1 = this; - return new Promise(function (resolve) { - var status = false; - status = typeof param === 'function' ? param() : param; - if (status) { - _this_1.$error(msg); - } else { - resolve(status); - } - }); - }; - Utils.prototype.$verifySubmitList = function (list) { - var _this_1 = this; - return new Promise(function (resolve) { - return __awaiter(_this_1, void 0, void 0, function () { - var status, i, element; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - status = true; - i = 0; - _a.label = 1; - case 1: - if (!(i < list.length)) return [3, 4]; - element = list[i]; - return [4, this.$verifySubmit(element[0], element[1])]; - case 2: - status = _a.sent(); - if (status) return [3, 4]; - _a.label = 3; - case 3: - i++; - return [3, 1]; - case 4: - if (!status) resolve(true); - return [2]; - } - }); - }); - }); - }; - Utils.prototype.$refreshBrowser = function (href, time) { - if (href === void 0) { - href = 1500; - } - if (time === void 0) { - time = 1500; - } - typeof href === 'number' && (time = href); - setTimeout(function () { - switch (typeof href) { - case 'string': - location.href = href; - break; - case 'number': - location.reload(); - break; - } - }, time); - }; - Utils.prototype.$require = function (moduleName) { - return new Promise(function (resolve, reject) { - try { - if (!Array.isArray(moduleName)) moduleName = [moduleName]; - require(moduleName, function () { - var param = {}; - for (var i = 0; i < arguments.length; i++) { - param[moduleName[i]] = arguments[i]; - } - resolve(param); - }); - } catch (error) { - reject(error); - } - }); - }; - Utils.prototype.$delay = function (time) { - if (time === void 0) { - time = 1000; - } - return new Promise(function (resolve) { - setTimeout(function () { - resolve(time); - }, time); - }); - }; - return Utils; - })(); - exports.default = Utils; + } else { + error_msg = jqXHR.responseText; + } + $('.layui-layer-padding').parents('.layer-anim').remove(); + $('.layui-layer-shade').remove(); + setTimeout(function () { + layer.open({ + title: false, + content: error_msg, + closeBtn: 2, + area: ['1000px', '800px'], + btn: false, + shadeClose: false, + shade: 0.3, + success: function () { + $('pre').scrollTop(100000000000); + }, + }); + }, 100); + } + }, + }); + } + }; + Utils.prototype.$checkIp = function (ip) { + var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; + return reg.test(ip); + }; + Utils.prototype.$checkIps = function (ips) { + var reg = /^\d{1, 3}\.\d{1, 3}\.\d{1, 3}\.\d{1, 3}(\/\d{1, 2})?$/; + return reg.test(ips); + }; + Utils.prototype.$checkDomainList = function (domainInfo) { + if (typeof domainInfo === 'string') domainInfo = domainInfo.split(','); + var reg = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0, 61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2, 6}$/; + for (var _i = 0, domainInfo_1 = domainInfo; _i < domainInfo_1.length; _i++) { + var item = domainInfo_1[_i]; + if (!reg.test(item)) return false; + } + return true; + }; + Utils.prototype.$checkPawComplexity = function (paw) { + var regList = { + length: /^.{8,}$/, + number: /\d+/, + lowercase: /[a-z]+/, + capital: /[A-Z]+/, + special: /[^A-Za-z0-9]+/, + }; + return false; + }; + Utils.prototype.$checkWeakCipher = function (paw) { + var checks = ['admin888', '123123123', '12345678', '45678910', '87654321', 'asdfghjkl', 'password', 'qwerqwer'], + pchecks = 'abcdefghijklmnopqrstuvwxyz1234567890', + lower = paw.toLowerCase(), + isError = ''; + for (var i = 0; i < pchecks.length; i++) { + var item = pchecks[i]; + checks.push(item + item + item + item + item + item + item + item); + } + for (var i = 0; i < checks.length; i++) { + var item = checks[i]; + if (lower === item) isError += '['.concat(item, ']'); + break; + } + return { + status: !isError, + msg: isError, + }; + }; + Utils.prototype.$checkUrl = function (url) { + var reg = /^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+/; + return reg.test(url); + }; + Utils.prototype.$checkPort = function (port) { + var reg = /^([1-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/; + return reg.test(port.toString()); + }; + Utils.prototype.$checkChinese = function (chinese) { + var reg = /^[\u4e00-\u9fa5]+$/; + return reg.test(chinese); + }; + Utils.prototype.$checkDomain = function (domain) { + var reg = /^([\w\u4e00-\u9fa5\-\*]{1, 100}\.){1, 10}([\w\u4e00-\u9fa5\-]{1, 24}|[\w\u4e00-\u9fa5\-]{1, 24}\.[\w\u4e00-\u9fa5\-]{1, 24})$/; + return reg.test(domain); + }; + Utils.prototype.$checkEmail = function (email) { + var reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/; + return reg.test(email); + }; + Utils.prototype.$checkPhone = function (phone) { + var reg = /^1[3456789]\d{9}$/; + return reg.test(phone.toString()); + }; + Utils.prototype.$containsStr = function (str, subStr) { + if (typeof str !== 'string' && typeof subStr !== 'string') return false; + return str.indexOf(subStr) > -1; + }; + Utils.prototype.$replaceTrim = function (str) { + return str.replace(/\s+/g, ''); + }; + Utils.prototype.$ltrim = function (str, l) { + var reg = new RegExp('/(^\\' + l + '+)/g'); + return str.replace(reg, ''); + }; + Utils.prototype.$rtrim = function (str, r) { + var reg = new RegExp('/(\\' + r + '+$)/g'); + return str.replace(reg, ''); + }; + Utils.prototype.$formatTime = function (time, format) { + if (format === void 0) { + format = 'yyyy/MM/dd hh:mm:ss'; + } + var timestamp = ''; + if (typeof time === 'object') timestamp = time.getTime().toString(); + if (typeof time === 'string') timestamp = new Date(time).getTime().toString(); + if (typeof time === 'number') timestamp = time.toString(); + if (timestamp.length > 10) timestamp = timestamp.substring(0, 10); + var date = new Date(parseInt(timestamp) * 1000); + var o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'h+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + 'q+': Math.floor((date.getMonth() + 3) / 3), + S: date.getMilliseconds(), + }; + if (/(y+)/.test(format)) format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)); + for (var k in o) { + if (new RegExp('(' + k + ')').test(format)) { + format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)); + } + } + return format; + }; + Utils.prototype.$formatSize = function (bytes, isUnit, fixed, endUnit) { + if (bytes === void 0) { + bytes = 0; + } + if (isUnit === void 0) { + isUnit = true; + } + if (fixed === void 0) { + fixed = 2; + } + if (endUnit === void 0) { + endUnit = ''; + } + if (typeof bytes === 'string') bytes = parseInt(bytes); + var unit = [' B', ' KB', ' MB', ' GB', 'TB'], + c = 1024; + for (var i = 0; i < unit.length; i++) { + var cUnit = unit[i]; + var val = bytes; + if (fixed !== 0 && i === 0) val = bytes.toFixed(fixed); + if (endUnit) { + if (cUnit.trim() == endUnit.trim()) { + if (endUnit) { + return val + cUnit; + } else { + return val; + } + } + } else { + if (bytes < c) { + if (isUnit) { + return val + cUnit; + } else { + return val; + } + } + } + bytes /= c; + } + }; + Utils.prototype.$formatPath = function (path) { + var reg = /(\\)/g; + path = path.replace(reg, '/'); + return path; + }; + Utils.prototype.$getFilePath = function (filename) { + if (filename === '/') return '/'; + filename = (filename + '/').replace(/\/\//g, '/'); + var arr = filename.split('/'), + last = arr[arr.length - 1]; + return filename.replace('/' + arr[arr.length - (last === '' ? 2 : 1)], ''); + }; + Utils.prototype.$getRandom = function (len) { + if (len === void 0) { + len = 32; + } + var $chars = 'AaBbCcDdEeFfGHhiJjKkLMmNnPpRSrTsWtXwYxZyz2345678', + maxPos = $chars.length; + var password = ''; + for (var i = 0; i < len; i++) { + password += $chars.charAt(Math.floor(Math.random() * maxPos)); + } + return password; + }; + Utils.prototype.$getRandomNum = function (min, max) { + if (min === void 0) { + min = 0; + } + if (max === void 0) { + max = 9; + } + return Math.floor(Math.random() * (max - min + 1) + min); + }; + Utils.prototype.$getStorage = function (key) { + return window.localStorage.getItem(key); + }; + Utils.prototype.$setStorage = function (key, value) { + window.localStorage.setItem(key, value); + }; + Utils.prototype.$removeStorage = function (key) { + window.localStorage.removeItem(key); + }; + Utils.prototype.$getCookie = function (name) { + var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); + var itExist = document.cookie.match(reg); + if (itExist) { + var val = unescape(itExist[2]); + return val == 'undefined' ? '' : val; + } else { + return null; + } + }; + Utils.prototype.$setCookie = function (name, value, time) { + if (time === void 0) { + time = 2592000000; + } + var date = '', + expires = new Date(); + expires.setTime(expires.getTime() + time); + date = expires.toGMTString(); + var isHttps = window.location.protocol === 'https:'; + var sameSite = ';Secure; Path=/; SameSite=None'; + document.cookie = name + '=' + escape(value) + ';expires=' + time + (isHttps ? sameSite : ''); + }; + Utils.prototype.$removeCookie = function (name) { + this.$setCookie(name, '', 0); + }; + Utils.prototype.$requestInit = function () { + var _this_1 = this; + var requestTokenHead = document.getElementById('request_token_head'); + var headers = { 'x-http-token': '', 'x-cookie-token': '' }; + var httpToken = (requestTokenHead && requestTokenHead.getAttribute('token')) || ''; + var cookieToken = this.$getCookie('request_token') || ''; + httpToken && (headers['x-http-token'] = httpToken); + cookieToken && (headers['x-cookie-token'] = cookieToken); + if (httpToken) { + $.ajaxSetup({ + headers: headers, + error: function (XHR) { + return __awaiter(_this_1, void 0, void 0, function () { + var resText, resStatus, monitorStr, content, errorHead, isErrorHead, errorBody, tmp; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + (resText = XHR.responseText), (resStatus = XHR.status), (monitorStr = ['/static/favicon.ico', '/static/img/qrCode.png', '']), (content = ''); + if (resText) return [2, false]; + if ( + typeof resText == 'string' && + monitorStr.some(function (item) { + return resText.indexOf(item) > -1; + }) + ) + return [2, this.$refreshBrowser('/login')]; + (errorHead = 'We need to make sure this has a favicon so that the debugger does'), (isErrorHead = resText.indexOf(errorHead) > -1); + if (resStatus === 500 && resText.indexOf('运行时发生错误') > -1 && isErrorHead) { + if (resText.indexOf('请先绑定宝塔帐号!') > -1) { + this.$refreshBrowser('/bind?redirect='.concat(encodeURIComponent(window.location.href))); + return [2, false]; + } + if (isErrorHead) { + errorBody = resText.split('', ''); + tmp = errorBody.split('During handling of the above exception, another exception occurred:'); + errorBody = tmp[tmp.length - 1]; + content = + '
\n

Something went wrong, an error occurred while the panel was running!

\n
'.concat(
+														errorBody.trim(),
+														'
\n
    \n
  • Sorry, an unexpected error occurred while panel was running, please try to resolve this error in the following order:
  • \n
  • 1. Click Fix Panel in the upper right corner of [Home] and log out of the panel to log in again.
  • \n
  • 2. if the above attempts fail to lift this error, please screenshot this window to the aaPanel Forum to post for help, forum address: https://forum.aapanel.com
\n
' + ); + } else { + content = resText; + } + } + return [ + 4, + this.$open({ + title: false, + content: content, + area: ['1200px', '810px'], + btn: false, + }), + ]; + case 1: + _a.sent(); + return [2]; + } + }); + }); + }, + }); + } + }; + Utils.prototype.$send = function (param, param1, param2) { + var _this_1 = this; + if (param2 === void 0) { + param2 = ''; + } + return new Promise(function (resolve, reject) { + var config = { + url: '', + method: 'POST', + msg: false, + data: {}, + }, + loading = '', + loadT; + if (typeof param === 'string') { + var urls = param.split('/'); + config.url = '/'.concat(urls[0], '?action=').concat(urls[1]); + if (typeof param1 === 'string') { + loading = param1; + } else if (typeof param1 === 'object') { + config.data = param1; + loading = param2; + } + } else if (typeof param === 'object') { + config.url = param.url; + config.method = param.method || 'POST'; + config.msg = param.msg || false; + config.data = param.data || {}; + loading = param.loading || ''; + } + if (loading) loadT = _this_1.$load(''.concat(loading)); + $.ajax({ + url: config.url, + method: config.method, + data: config.data, + success: function (rdata) { + return __awaiter(_this_1, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (loadT) loadT.close(); + if (!(typeof rdata === 'object')) return [3, 3]; + if (!(typeof rdata.msg === 'string' && rdata.msg.indexOf('CSRF校验失败,请重新登录面板') > -1)) return [3, 2]; + return [4, this.$confirm({ title: 'Tips', msg: 'Panel login has expired, please login again!' })]; + case 1: + _a.sent(); + layer.closeAll(); + this.$load('Logging out, please wait...'); + this.$refreshBrowser('/login?dologin=True'); + return [2, false]; + case 2: + if (typeof rdata.msg === 'string' && rdata.msg === '没有权限' && !rdata.status) { + layer.closeAll(); + this.$error(rdata.msg); + return [2, false]; + } + if (typeof rdata.status === 'boolean' && !rdata.status && rdata.msg && config.msg) this.$error(rdata.msg); + _a.label = 3; + case 3: + resolve(rdata); + return [2]; + } + }); + }); + }, + error: function (err) { + reject(err); + }, + }); + }); + }; + Utils.prototype.$apiInit = function (apiInfo) { + this.API = Object.assign(this.API, apiInfo); + }; + Utils.prototype.$request = function (info, param, config) { + var _this_1 = this; + if (param === void 0) { + param = {}; + } + return new Promise(function (resolve, reject) { + return __awaiter(_this_1, void 0, void 0, function () { + var url, loading, rdata, error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (typeof info === 'string') info = this.API[info]; + if (typeof param === 'boolean') (config = param), (param = {}); + if (typeof param === 'object' && param.hasOwnProperty('loading') && param.hasOwnProperty('msg')) (config = param), (param = {}); + if (typeof config === 'boolean') config = { msg: config }; + if (typeof config === 'undefined') config = { msg: true, loading: true }; + (url = info[0]), (loading = info[1] || ''); + if (!config.loading) loading = ''; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4, this.$send(url, param, loading)]; + case 2: + rdata = _a.sent(); + config.msg && typeof rdata.msg === 'string' && this.$msg(rdata); + resolve(rdata); + return [3, 4]; + case 3: + error_1 = _a.sent(); + reject(error_1); + return [3, 4]; + case 4: + return [2]; + } + }); + }); + }); + }; + Utils.prototype.$open = function (param) { + var _this_1 = this; + var _this = this; + return new Promise(function (resolve, reject) { + var id = 'virtual-'.concat(_this_1.$getRandom(5)); + var content = param.content, + success = param.success, + yes = param.yes, + btn2 = param.btn2, + cancel = param.cancel, + title = param.title, + shadeClose = param.shadeClose, + closeBtn = param.closeBtn, + skin = param.skin, + type = param.type; + var config = typeof param.content === 'string' ? '' : param.content; + var vm; + switch (typeof param.content) { + case 'string': + content = param.content; + break; + case 'object': + content = '
'); + break; + } + var layerConfig = { + title: title, + type: type || 1, + shadeClose: shadeClose, + content: content, + skin: skin, + closeBtn: closeBtn || 2, + success: function (layers, indexs) { + if (typeof config === 'object') { + var vm_1 = new vdom_1.default(Object.assign(config, { el: '#'.concat(id) })); + vm_1['$closeLayer'] = function () { + layer.close(indexs); + return indexs; + }; + _this.vDdomList[indexs] = vm_1; + } + success && success(layers, indexs, vm); + }, + yes: function (indexs) { + _this.layerIndex = indexs; + var config = { + close: function () { + layer.close(indexs); + return indexs; + }, + vm: _this.vDdomList[indexs], + }; + yes && yes(config); + resolve(config); + }, + cancel: function (indexs) { + _this.layerIndex = indexs; + if (cancel) { + cancel && cancel(); + } else { + reject({ cancel: true, type: 'cancel' }); + } + delete _this.vDdomList[indexs]; + }, + btn2: function (indexs) { + _this.layerIndex = indexs; + if (btn2) { + btn2 && btn2(); + } else { + reject({ cancel: true, type: 'btn2' }); + } + delete _this.vDdomList[indexs]; + }, + end: function () { + delete _this.vDdomList[_this.layerIndex]; + }, + }; + layer.open(Object.assign(param, layerConfig)); + }); + }; + Utils.prototype.$confirm = function (param) { + return new Promise(function (resolve, reject) { + var msg = ''; + if (param.hasOwnProperty('msg')) { + msg = param.msg; + delete param.msg; + } + layer.confirm( + msg, + Object.assign( + { + title: 'Tips', + icon: 3, + btn: [lan.public.confirm, lan.public.cancel], + shadeClose: false, + closeBtn: 2, + cancel: function () { + return reject({ cancel: true }); + }, + }, + param + ), + function (indexs) { + return resolve(indexs); + }, + function () { + return reject({ cancel: true }); + } + ); + }); + }; + Utils.prototype.$tips = function (param) { + var msg = '', + el = ''; + if (param.msg) { + msg = param.msg; + delete param.msg; + } + if (param.el) { + el = param.el; + delete param.el; + } + param.success = function (layero) { + var oldLeft = layero.css('left'); + oldLeft = oldLeft.substring(0, oldLeft.indexOf('px')); + layero.css('left', ''.concat(oldLeft - 10, 'px')); + }; + return { + layer: layer.tips(msg, el, Object.assign({ tips: [1, 'red'] }, param)), + close: function () { + layer.close(this.layer); + }, + }; + }; + Utils.prototype.$close = function (index) { + layer.close(index); + }; + Utils.prototype.$msg = function (param, param1) { + var config = { + time: 1500, + shade: 0.3, + shadeClose: true, + closeBtn: 0, + icon: 1, + }, + msg = ''; + if (typeof param === 'object') { + for (var key in config) { + if (Object.prototype.hasOwnProperty.call(config, key)) { + if (typeof param[key] !== 'undefined') config[key] = param[key]; + } + } + msg = param.msg + (param.msg_error || '') + (param.msg_solve || ''); + if (typeof msg == 'string' && typeof param.status === 'boolean') { + config.icon = typeof param.status === 'boolean' ? (param.status ? 1 : 2) : 1; + } + } else if (typeof param === 'string') { + msg = param; + if (typeof param1 === 'boolean' || typeof param1 === 'number') { + config.icon = typeof param1 === 'boolean' ? (param1 ? 1 : 2) : param1; + } + } + return { + layer: layer.msg(msg, config), + close: function () { + layer.close(this.layer); + }, + }; + }; + Utils.prototype.$error = function (msg) { + return this.$msg({ msg: msg, icon: 2 }); + }; + Utils.prototype.$warning = function (msg) { + return this.$msg({ msg: msg, icon: 0 }); + }; + Utils.prototype.$load = function (tips) { + if (tips === void 0) { + tips = 'Processing, please wait...'; + } + return this.$msg({ msg: tips, icon: 16, time: 0, shade: [0.3, '#000'] }); + }; + Utils.prototype.$verifySubmit = function (param, msg) { + var _this_1 = this; + return new Promise(function (resolve) { + var status = false; + status = typeof param === 'function' ? param() : param; + if (status) { + _this_1.$error(msg); + } else { + resolve(status); + } + }); + }; + Utils.prototype.$verifySubmitList = function (list) { + var _this_1 = this; + return new Promise(function (resolve) { + return __awaiter(_this_1, void 0, void 0, function () { + var status, i, element; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + status = true; + i = 0; + _a.label = 1; + case 1: + if (!(i < list.length)) return [3, 4]; + element = list[i]; + return [4, this.$verifySubmit(element[0], element[1])]; + case 2: + status = _a.sent(); + if (status) return [3, 4]; + _a.label = 3; + case 3: + i++; + return [3, 1]; + case 4: + if (!status) resolve(true); + return [2]; + } + }); + }); + }); + }; + Utils.prototype.$refreshBrowser = function (href, time) { + if (href === void 0) { + href = 1500; + } + if (time === void 0) { + time = 1500; + } + typeof href === 'number' && (time = href); + setTimeout(function () { + switch (typeof href) { + case 'string': + window.parent.location.href = href; + break; + case 'number': + window.parent.location.reload(); + break; + } + }, time); + }; + Utils.prototype.$require = function (moduleName) { + return new Promise(function (resolve, reject) { + try { + if (!Array.isArray(moduleName)) moduleName = [moduleName]; + require(moduleName, function () { + var param = {}; + for (var i = 0; i < arguments.length; i++) { + param[moduleName[i]] = arguments[i]; + } + resolve(param); + }); + } catch (error) { + reject(error); + } + }); + }; + Utils.prototype.$delay = function (time) { + if (time === void 0) { + time = 1000; + } + return new Promise(function (resolve) { + setTimeout(function () { + resolve(time); + }, time); + }); + }; + return Utils; + })(); + exports.default = Utils; }); define('public/public', ['require', 'exports', 'snabbdom', 'public/utils'], function (require, exports, snabbdom_2, utils_1) { - 'use strict'; - Object.defineProperty(exports, '__esModule', {value: true}); - utils_1 = __importDefault(utils_1); - var Public = (function (_super) { - __extends(Public, _super); - - function Public() { - var _this_1 = _super.call(this) || this; - _this_1.apiInfo = { - getUserInfo: ['ssl/GetUserInfo', 'Getting bind account information, please wait...'], - unbindUserInfo: ['ssl/DelToken', 'Unbinding the pagoda account, please wait...'], - restartPanel: ['system/ReWeb', lan.public.the], - GetToken: ['ssl/GetToken', lan.config.token_get], - getFileDir: ['files/GetDir', lan.public.the], - getMsgConfig: ['config/get_settings2', 'Getting profile, please wait...'], - setTelegramConfig: ['config/set_tg_bot', 'The notification is being generated, please wait...'], - setDingDingConfig: ['config/set_msg_config&name=dingding', 'The notification is being generated, please wait...'], - clearTelegramConfig: ['config/del_tg_info', 'Deleting notification, please wait...'], - addMailAddress: ['config/add_mail_address', 'Please wait while creating recipient list...'], - delMailAddress: ['config/del_mail_list', 'Deleting email, please wait...'], - getMailList: ['config/get_settings2', lan.public.the], - setMailConfig: ['config/user_mail_send', 'The notification is being generated, please wait...'], - }; - _this_1.$apiInit(_this_1.apiInfo); - return _this_1; - } - - Public.prototype.fixedTableHead = function (el, height, isBorder) { - if (isBorder === void 0) { - isBorder = true; - } - $(el).css({border: 'none'}); - var data = {'overflow-y': 'auto', 'max-height': height}; - if (isBorder) data.border = '1px solid #ddd'; - $(el).parent().css(data); - $(el) - .parent() - .bind('scroll', function () { - var scrollTop = this.scrollTop; - $(this) - .find('thead') - .css({ - transform: 'translateY(' + scrollTop + 'px)', - position: 'relative', - 'z-index': '1', - }); - }); - }; - Public.prototype.bindBtAccount = function (isEdit) { - if (isEdit === void 0) { - isEdit = false; - } - var that = this; - var title = !isEdit ? lan.config.config_user_binding : lan.config.config_user_edit; - var bindBtn = !isEdit ? lan.config.binding : lan.public.edit; - this.$open({ - title: title, - area: ['420px', '360px'], - content: { - data: { - username: '', - password: '', - }, - template: function () { - return (0, snabbdom_2.jsx)( - 'div', - {class: this.$class('pd20 bt-form libLogin')}, - (0, snabbdom_2.jsx)('h4', {class: this.$class('c2 f18 text-center mtb20')}, lan.public_backup.bind_bt_account), - (0, snabbdom_2.jsx)('div', {class: {line: true}}, this.$input({ - model: 'username', - placeholder: lan.public.user - })), - (0, snabbdom_2.jsx)('div', {class: {line: true}}, this.$input({ - model: 'password', - type: 'password', - placeholder: lan.public.pass, - keyup: this.bindUserInfo.bind(this) - })), - (0, snabbdom_2.jsx)('div', {class: {line: true}}, this.$button({ - className: 'login-button', - title: bindBtn, - click: this.bindUserInfo.bind(this), - width: '360px', - height: '40px' - })), - (0, snabbdom_2.jsx)('p', {class: {'text-right': true}}, ' ', this.$link({ - title: lan.public_backup.no_account, - href: 'https://www.aapanel.com/user_admin/register' - })) - ); - }, - methods: { - bindUserInfo: function (ev) { - return __awaiter(this, void 0, void 0, function () { - var _a, username, password, param, rdata; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - if (ev.type === 'keyup' && ev.keyCode !== 13) return [2, false]; - (_a = this), (username = _a.username), (password = _a.password); - return [ - 4, - that.$verifySubmitList([ - [!username, 'Please input account'], - [!password, 'Please input password'], - ]), - ]; - case 1: - _b.sent(); - param = {username: username, password: password}; - return [4, that.$request('GetToken', param)]; - case 2: - rdata = _b.sent(); - that.$msg(rdata); - rdata.status && that.$refreshBrowser(); - return [2]; - } - }); - }); - }, - }, - }, - }).catch(function (err) { - }); - }; - Public.prototype.selectFileDir = function (id, type, success, default_path) { - var _this_1 = this; - if (type === void 0) { - type = 'all'; - } - this.$setCookie('SetName', ''); - if (typeof type !== 'string') (success = type), (type = 'dir'); - this.$open({ - area: '680px', - title: type === 'all' ? 'Select directories or files' : type === 'file' ? lan.bt.file : lan.bt.dir, - closeBtn: 2, - content: - "\n
\n
\n \n
") - .concat( - lan.bt.path, - "
\n
\n
\n
\n
\n
\n
\n
\n
\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    " - ) - .concat(lan.bt.filename, "") - .concat(lan.bt.etime, "") - .concat(lan.bt.access, "") - .concat( - lan.bt.own, - "
    \n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n '), - success: function (layers, indexs) { - var el = null; - if (id.indexOf('.') === 0) el = $('.' + id); - if (id.indexOf('#') === 0) el = $('#' + id); - if (id.indexOf('.') !== 0 && id.indexOf('#') !== 0) el = $(id); - _this_1.fixedTableHead('.file-list .table', '100%', false); - $('#btn_back').on('click', function () { - var path = $('#PathPlace').find('span').text(); - path = _this_1.$rtrim(_this_1.$formatPath(path), '/'); - _this_1.getFileList(_this_1.$getFilePath(path), type); - }); - $('#bt_select').on('click', function () { - var path = _this_1.$formatPath($('#PathPlace').find('span').text()); - if (type === 'file' && !$('#tbody tr.active').length) { - layer.msg('Select the file first!', {icon: 0}); - return false; - } - if ($('#tbody tr').hasClass('active')) path = $('#tbody tr.active .bt_open_dir').attr('path'); - path = _this_1.$rtrim(path, '/'); - el.val(path).trigger('input'); - success && success(path); - layer.close(indexs); - }); - $('.closeLayer').on('click', function () { - layer.close(indexs); - }); - _this_1.getFileList(el.val() || '/www/wwwroot', type); - }, - }).catch(function (err) { - }); - }; - Public.prototype.getFileList = function (path, type) { - if (type === void 0) { - type = 'dir'; - } - return __awaiter(this, void 0, void 0, function () { - var _this, diskHtml, fileHtml, dirHtml, rdata, fileList, dirList, diskList, i_1, item, i_2, item, - unfoldList, dirName, i, item, unfoldList, fileName; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - (_this = this), (diskHtml = ''), (fileHtml = ''), (dirHtml = ''); - return [4, this.$request('getFileDir', {path: path, disk: true})]; - case 1: - rdata = _a.sent(); - (fileList = rdata.FILES), (dirList = rdata.DIR), (diskList = rdata.DISK); - for (i_1 = 0; i_1 < diskList.length; i_1++) { - item = diskList[i_1]; - diskHtml += '
     " + item.path + '
    '; - } - for (i_2 = 0; i_2 < dirList.length; i_2++) { - (item = dirList[i_2]), (unfoldList = item.split(';')); - dirName = unfoldList[0]; - if (dirName.length > 20) dirName = dirName.substring(0, 20) + '...'; - if (this.$checkChinese(dirName) && dirName.length > 10) dirName = dirName.substring(0, 10) + '...'; - dirHtml += - '' + - (type === 'all' || type === 'dir' ? '' : '') + - '" + - dirName + - '' + - this.$formatTime(parseInt(unfoldList[2])) + - '' + - unfoldList[3] + - '' + - unfoldList[4] + - ''; - } - for (i = 0; i < fileList.length; i++) { - (item = fileList[i]), (unfoldList = item.split(';')); - fileName = unfoldList[0]; - if (fileName.length > 20) fileName = fileName.substring(0, 20) + '...'; - if (this.$checkChinese(fileName) && fileName.length > 10) fileName = fileName.substring(0, 10) + '...'; - fileHtml += - '' + - (type === 'all' || type === 'file' ? '' : '') + - '" + - fileName + - '' + - this.$formatTime(parseInt(unfoldList[2])) + - '' + - unfoldList[3] + - '' + - unfoldList[4] + - ''; - } - $('#changecomlist').html(diskHtml); - $('.default').hide(); - $('.file-list').show(); - $('#tbody').html(dirHtml + fileHtml); - if (rdata.PATH.substr(rdata.PATH.length - 1, 1) != '/') { - rdata.PATH += '/'; - } - $('#PathPlace').find('span').html(rdata.PATH); - $('#tbody tr').click(function () { - if ($(this).find('td:eq(0) input').length > 0) { - if ($(this).hasClass('active')) { - $(this).removeClass('active'); - $(this).find('td:eq(0) input').prop('checked', false); - } else { - $(this).find('td:eq(0) input').prop('checked', true); - $(this).siblings().find('td:eq(0) input').prop('checked', false); - $(this).addClass('active').siblings().removeClass('active'); - } - } - }); - $('#changecomlist dd').click(function () { - _this.getFileList($(this).attr('path'), type); - }); - $('.bt_open_dir span').click(function () { - if ($(this).parent().data('type') == 'dir') _this.getFileList($(this).parent().attr('path'), type); - }); - return [2]; - } - }); - }); - }; - Public.prototype.setMessageChannelView = function (type, info) { - if (type === void 0) { - type = 0; - } - return __awaiter(this, void 0, void 0, function () { - var rdata, _a, setup, id, token, error_2; - var _this_1 = this; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - _b.trys.push([0, 3, , 4]); - return [4, this.$request('getMsgConfig')]; - case 1: - rdata = _b.sent(); - (_a = rdata.telegram), (setup = _a.setup), (id = _a.my_id), (token = _a.bot_token); - console.log(rdata); - console.log(rdata); - return [ - 4, - this.$open({ - area: '600px', - title: 'Setting up notification', - skin: 'layer-channel-auth', - content: '
    \n
    \n
    \n

    Email

    \n

    DingDing

    \n

    Feishu

    \n

    Telegram

    \n
    \n
    \n
    \n
    \n
    \n
    \n \n \n
    \n
    \n
    \n \n \n \n \n \n \n \n \n \n \n
    EmailOperating
    No Data
    \n
    \n
    \n
    \n
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t
    \n Name\n
    \n \n
    \n
    \n\t\t\t\t\t\t\t\t\t\t
    \n Url\n
    \n \n
    \n
    \n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t' - ) - .concat( - setup ? '' : '', - '\n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t
    \n Name\n
    \n \n
    \n
    \n\t\t\t\t\t\t\t\t\t\t
    \n Url\n
    \n \n
    \n
    \n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t' - ) - .concat( - setup ? '' : '', - '\n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n
    \n
    \n
    \n ID\n
    \n \n
    \n
    \n
    \n TOKEN\n
    \n \n
    \n
    \n \n \n ' - ) - .concat( - setup ? '' : '', - '\n
    \n
    \n
    \n
      \n
    • ID: Your telegram user ID
    • \n
    • Token: Your telegram bot token
    • \n
    • e.g: [ 12345677:AAAAAAAAA_a0VUo2jjr__CCCCDDD ] Help
    • \n
    \n
    \n
    \n
    \n
    \n
    ' - ), - success: function ($layer) { - $('.bt-w-menu p').click(function () { - var index = $(this).index(); - $(this).addClass('bgw').siblings().removeClass('bgw'); - $('.conter_box').eq(index).removeClass('hide').siblings().addClass('hide'); - }); - $('.addRecipient').on('click', function () { - return _this_1.addMessageMail(); - }); - $('.setMailMessageView').on('click', function () { - return _this_1.setMailMessageView(info); - }); - $('#receive_table').on('click', '.del_email', function (ev) { - return __awaiter(_this_1, void 0, void 0, function () { - var email, res, err_1; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - email = $(ev.target).data('mail'); - _a.label = 1; - case 1: - _a.trys.push([1, 6, , 7]); - return [4, this.$confirm({ - title: 'Delete Email ['.concat(email, ']'), - msg: 'Are your sure to delete the Email?' - })]; - case 2: - _a.sent(); - return [4, this.$request('delMailAddress', {email: email})]; - case 3: - res = _a.sent(); - if (!res.status) return [2]; - return [4, this.$delay()]; - case 4: - _a.sent(); - return [4, this.renderMailMessageList()]; - case 5: - _a.sent(); - return [3, 7]; - case 6: - err_1 = _a.sent(); - return [3, 7]; - case 7: - return [2]; - } - }); - }); - }); - $('.addTelegram').click(function () { - return __awaiter(_this_1, void 0, void 0, function () { - var id_1, token_1, res, err_2; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 3, , 4]); - id_1 = $('input[name="telegram_id"]').val(); - token_1 = $('input[name="telegram_token"]').val(); - return [4, this.$verifySubmit(id_1 == '' || token_1 == '', 'input box cannot be empty!')]; - case 1: - _a.sent(); - return [4, this.$request('setTelegramConfig', { - my_id: id_1, - bot_token: token_1 - })]; - case 2: - res = _a.sent(); - if (!res.status) return [2]; - $('.addTelegram').after(''); - this.setLinkText('.setMessageChannelTelegram', 'Telegram is set', 'btlink'); - return [3, 4]; - case 3: - err_2 = _a.sent(); - return [3, 4]; - case 4: - return [2]; - } - }); - }); - }); - $layer.on('click', '.delTelegram', function () { - return __awaiter(_this_1, void 0, void 0, function () { - var res, err_3; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 3, , 4]); - return [4, this.$confirm({ - title: 'Clear set', - msg: 'Delete Clear Settings?' - })]; - case 1: - _a.sent(); - return [4, this.$request('clearTelegramConfig')]; - case 2: - res = _a.sent(); - if (!res.status) return [2]; - $('.delTelegram').remove(); - $('input[name="telegram_id"]').val(''); - $('input[name="telegram_token"]').val(''); - this.setLinkText('.setMessageChannelTelegram', 'Telegram is not set', 'bt_warning'); - return [3, 4]; - case 3: - err_3 = _a.sent(); - return [3, 4]; - case 4: - return [2]; - } - }); - }); - }); - $('.addDingDing').click(function () { - return __awaiter(_this_1, void 0, void 0, function () { - var name_2, url, res, err_4; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 3, , 4]); - name_2 = $('input[name=chatName]').val(); - url = $('[name=dingding_url]').val(); - return [4, this.$verifySubmit(name_2 == '' || url == '', 'input box cannot be empty!')]; - case 1: - _a.sent(); - return [4, this.$request('setDingDingConfig', { - title: name_2, - url: url, - atall: 'True' - })]; - case 2: - res = _a.sent(); - if (!res.status) return [2]; - return [3, 4]; - case 3: - err_4 = _a.sent(); - return [3, 4]; - case 4: - return [2]; - } - }); - }); - }); - _this_1.fixedTableHead('#receive_table', '356px'); - _this_1.renderMailMessageList(); - }, - }), - ]; - case 2: - _b.sent(); - return [3, 4]; - case 3: - error_2 = _b.sent(); - return [3, 4]; - case 4: - return [2]; - } - }); - }); - }; - Public.prototype.setLinkText = function (el, text, type) { - var $el = $(el); - switch (type) { - case 'btlink': - $el.text(text).addClass('btlink').removeClass('bt_warning'); - break; - case 'bt_warning': - $el.text(text).addClass('bt_warning').removeClass('btlink'); - break; - } - }; - Public.prototype.addMessageMail = function () { - return __awaiter(this, void 0, void 0, function () { - var error_3; - var _this_1 = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 2, , 3]); - return [ - 4, - this.$open({ - area: '400px', - title: 'Add recipient email', - btn: ['Create', 'Close'], - content: - '
    \n
    \n Recipient mailbox\n
    \n \n
    \n
    \n
    ', - yes: function (config) { - return __awaiter(_this_1, void 0, void 0, function () { - var email, rdata; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - email = $('input[name=creater_email_value]').val(); - return [ - 4, - this.$verifySubmitList([ - [email === '', 'Recipient mailbox cannot be empty!'], - [!this.$checkEmail(email), 'Recipient mailbox format is incorrect!'], - ]), - ]; - case 1: - _a.sent(); - return [4, this.$request('addMailAddress', {email: email})]; - case 2: - rdata = _a.sent(); - if (!rdata.status) return [3, 5]; - config.close(); - return [4, this.$delay()]; - case 3: - _a.sent(); - return [4, this.renderMailMessageList()]; - case 4: - _a.sent(); - _a.label = 5; - case 5: - return [2]; - } - }); - }); - }, - }), - ]; - case 1: - _a.sent(); - return [3, 3]; - case 2: - error_3 = _a.sent(); - return [3, 3]; - case 3: - return [2]; - } - }); - }); - }; - Public.prototype.setMailMessageView = function (info) { - return __awaiter(this, void 0, void 0, function () { - var rdata, _a, qq_mail, qq_stmp_pwd, hosts, port, verifyPortList, err_5; - var _this_1 = this; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - _b.trys.push([0, 3, , 4]); - return [4, this.$request('getMsgConfig')]; - case 1: - rdata = _b.sent(); - (_a = rdata.user_mail.info.msg), (qq_mail = _a.qq_mail), (qq_stmp_pwd = _a.qq_stmp_pwd), (hosts = _a.hosts), (port = _a.port); - qq_mail = qq_mail || ''; - qq_stmp_pwd = qq_stmp_pwd || ''; - hosts = hosts || ''; - port = port || ''; - verifyPortList = ['25', '465', '587', '']; - return [ - 4, - this.$open({ - title: 'Set sender email information', - area: '466px', - content: - '
    \n
    \n Sender email\n
    \n
    \n
    \n SMTP password\n
    \n
    \n
    \n SMTP server\n
    \n
    \n
    \n SMTP port\n
    \n \n \n
    \n
    \n
      \n
    • 465 port is recommended, the protocol is SSL/TLS
    • \n
    • Port 25 is SMTP protocol, port 587 is STARTTLS protocol
    • \n
    \n
    \n ' - ) - .concat( - qq_mail ? '' : '', - '\n \n \n
    \n
    ' - ), - success: function (layers, indexs) { - return __awaiter(_this_1, void 0, void 0, function () { - var portSelect, mailPort; - var _this_1 = this; - return __generator(this, function (_a) { - portSelect = $('#port_select'); - mailPort = $('input[name=channel_email_port]'); - portSelect.change(function (ev) { - var that = $(ev.target), - mailPort = $('input[name=channel_email_port]'); - that.css('width', ev.target.value === 'other' ? '100px' : '300px'); - mailPort.css('display', ev.target.value === 'other' ? 'inline-block' : 'none'); - }); - $('.SetChannelEmail').click(function () { - return __awaiter(_this_1, void 0, void 0, function () { - var email, stmp_pwd, hosts, port, portSelectVal, rdata, text, - type; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - email = $('input[name="channel_email_value"]').val(); - stmp_pwd = $('input[name="channel_email_password"]').val(); - hosts = $('input[name="channel_email_server"]').val(); - (port = ''), (portSelectVal = portSelect.val()); - port = portSelectVal === 'other' ? mailPort.val() : portSelectVal; - return [ - 4, - this.$verifySubmitList([ - [!email, 'Email address cannot be empty!'], - [!stmp_pwd, 'STMP password cannot be empty!'], - [!hosts, 'STMP server address cannot be empty!'], - [!port, 'STMP server port cannot be empty!'], - [!this.$checkPort(port), 'STMP server port format is incorrect'], - ]), - ]; - case 1: - _a.sent(); - return [4, this.$request('setMailConfig', { - email: email, - stmp_pwd: stmp_pwd, - hosts: hosts, - port: port - })]; - case 2: - rdata = _a.sent(); - if (!rdata.status) return [2]; - layer.close(indexs); - info.isSetEmail = true; - text = info.mail ? 'Already set' : 'Not set'; - type = info.mail ? 'btlink' : 'bt_warning'; - this.setLinkText('.setMessageChannelMail', 'Email is set', 'btlink'); - this.setLinkText('.setAlarmMail', text, type); - return [4, this.$delay()]; - case 3: - _a.sent(); - return [4, this.renderMailMessageList()]; - case 4: - _a.sent(); - return [2]; - } - }); - }); - }); - $('.smtp_closeBtn').click(function () { - layer.close(indexs); - }); - $('.set_empty').click(function () { - return __awaiter(_this_1, void 0, void 0, function () { - var rdata; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - return [4, this.$request(['config/set_empty', 'notification, please wait...'], {type: 'mail'})]; - case 1: - rdata = _a.sent(); - if (!rdata.status) return [2]; - layer.close(indexs); - info.isSetEmail = false; - this.setLinkText('.setMessageChannelMail', 'Email is not set', 'bt_warning'); - this.setLinkText('.setAlarmMail', 'Email is not set', 'bt_warning'); - return [2]; - } - }); - }); - }); - return [2]; - }); - }); - }, - }), - ]; - case 2: - _b.sent(); - return [3, 4]; - case 3: - err_5 = _b.sent(); - return [3, 4]; - case 4: - return [2]; - } - }); - }); - }; - Public.prototype.renderMailMessageList = function () { - return __awaiter(this, void 0, void 0, function () { - var rdata, _html, _list, i, item; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - return [4, this.$request('getMailList', {loading: true, msg: false})]; - case 1: - rdata = _a.sent(); - (_html = ''), (_list = rdata.user_mail.mail_list); - if (_list.length > 0) { - for (i = 0; i < _list.length; i++) { - item = _list[i]; - _html += '\n ' - .concat(item, '.\n \n Del\n \n '); - } - } else { - _html = 'No Data'; - } - $('#receive_table tbody').html(_html); - return [2]; - } - }); - }); - }; - return Public; - })(utils_1.default); - exports.default = Public; + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + utils_1 = __importDefault(utils_1); + var Public = (function (_super) { + __extends(Public, _super); + function Public() { + var _this_1 = _super.call(this) || this; + _this_1.apiInfo = { + getUserInfo: ['ssl/GetUserInfo', 'Getting bind account information, please wait...'], + unbindUserInfo: ['ssl/DelToken', 'Unbinding the pagoda account, please wait...'], + restartPanel: ['system/ReWeb', lan.public.the], + GetToken: ['ssl/GetToken', lan.config.token_get], + getFileDir: ['files/GetDir', lan.public.the], + getMsgConfig: ['config/get_settings2', 'Getting profile, please wait...'], + setTelegramConfig: ['config/set_tg_bot', 'The notification is being generated, please wait...'], + setDingDingConfig: ['config/set_msg_config&name=dingding', 'The notification is being generated, please wait...'], + clearTelegramConfig: ['config/del_tg_info', 'Deleting notification, please wait...'], + addMailAddress: ['config/add_mail_address', 'Please wait while creating recipient list...'], + delMailAddress: ['config/del_mail_list', 'Deleting email, please wait...'], + getMailList: ['config/get_settings2', lan.public.the], + setMailConfig: ['config/user_mail_send', 'The notification is being generated, please wait...'], + }; + _this_1.$apiInit(_this_1.apiInfo); + return _this_1; + } + Public.prototype.fixedTableHead = function (el, height, isBorder) { + if (isBorder === void 0) { + isBorder = true; + } + $(el).css({ border: 'none' }); + var data = { 'overflow-y': 'auto', 'max-height': height }; + if (isBorder) data.border = '1px solid #ddd'; + $(el).parent().css(data); + $(el) + .parent() + .bind('scroll', function () { + var scrollTop = this.scrollTop; + $(this) + .find('thead') + .css({ + transform: 'translateY(' + scrollTop + 'px)', + position: 'relative', + 'z-index': '1', + }); + }); + }; + Public.prototype.bindBtAccount = function (isEdit) { + if (isEdit === void 0) { + isEdit = false; + } + var that = this; + var title = !isEdit ? lan.config.config_user_binding : lan.config.config_user_edit; + var bindBtn = !isEdit ? lan.config.binding : lan.public.edit; + this.$open({ + title: title, + area: ['420px', '360px'], + content: { + data: { + username: '', + password: '', + }, + template: function () { + return (0, snabbdom_2.jsx)( + 'div', + { class: this.$class('pd20 bt-form libLogin') }, + (0, snabbdom_2.jsx)('h4', { class: this.$class('c2 f18 text-center mtb20') }, lan.public_backup.bind_bt_account), + (0, snabbdom_2.jsx)('div', { class: { line: true } }, this.$input({ model: 'username', placeholder: lan.public.user })), + (0, snabbdom_2.jsx)('div', { class: { line: true } }, this.$input({ model: 'password', type: 'password', placeholder: lan.public.pass, keyup: this.bindUserInfo.bind(this) })), + (0, snabbdom_2.jsx)('div', { class: { line: true } }, this.$button({ className: 'login-button', title: bindBtn, click: this.bindUserInfo.bind(this), width: '360px', height: '40px' })), + (0, snabbdom_2.jsx)('p', { class: { 'text-right': true } }, ' ', this.$link({ title: lan.public_backup.no_account, href: 'https://brandnew.aapanel.com/user_admin/register' })) + ); + }, + methods: { + bindUserInfo: function (ev) { + return __awaiter(this, void 0, void 0, function () { + var _a, username, password, param, rdata; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (ev.type === 'keyup' && ev.keyCode !== 13) return [2, false]; + (_a = this), (username = _a.username), (password = _a.password); + return [ + 4, + that.$verifySubmitList([ + [!username, 'Please input account'], + [!password, 'Please input password'], + ]), + ]; + case 1: + _b.sent(); + param = { username: username, password: password }; + return [4, that.$request('GetToken', param)]; + case 2: + rdata = _b.sent(); + that.$msg(rdata); + rdata.status && that.$refreshBrowser(); + return [2]; + } + }); + }); + }, + }, + }, + }).catch(function (err) {}); + }; + Public.prototype.selectFileDir = function (id, type, success, default_path) { + var _this_1 = this; + if (type === void 0) { + type = 'all'; + } + this.$setCookie('SetName', ''); + if (typeof type !== 'string') (success = type), (type = 'dir'); + this.$open({ + area: '680px', + title: type === 'all' ? 'Select directories or files' : type === 'file' ? lan.bt.file : lan.bt.dir, + closeBtn: 2, + content: + "\n
    \n
    \n \n
    ") + .concat( + lan.bt.path, + "
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
      \n
      \n \n \n \n \n \n \n \n \n \n \n \n
      " + ) + .concat(lan.bt.filename, "") + .concat(lan.bt.etime, "") + .concat(lan.bt.access, "") + .concat( + lan.bt.own, + "
      \n
      \n
      \n
      \n
      \n
      \n \n \n \n
      \n '), + success: function (layers, indexs) { + var el = null; + if (id.indexOf('.') === 0) el = $('.' + id); + if (id.indexOf('#') === 0) el = $('#' + id); + if (id.indexOf('.') !== 0 && id.indexOf('#') !== 0) el = $(id); + _this_1.fixedTableHead('.file-list .table', '100%', false); + $('#btn_back').on('click', function () { + var path = $('#PathPlace').find('span').text(); + path = _this_1.$rtrim(_this_1.$formatPath(path), '/'); + _this_1.getFileList(_this_1.$getFilePath(path), type); + }); + $('#bt_select').on('click', function () { + var path = _this_1.$formatPath($('#PathPlace').find('span').text()); + if (type === 'file' && !$('#tbody tr.active').length) { + layer.msg('Select the file first!', { icon: 0 }); + return false; + } + if ($('#tbody tr').hasClass('active')) path = $('#tbody tr.active .bt_open_dir').attr('path'); + path = _this_1.$rtrim(path, '/'); + el.val(path).trigger('input'); + success && success(path); + layer.close(indexs); + }); + $('.closeLayer').on('click', function () { + layer.close(indexs); + }); + _this_1.getFileList(el.val() || '/www/wwwroot', type); + }, + }).catch(function (err) {}); + }; + Public.prototype.getFileList = function (path, type) { + if (type === void 0) { + type = 'dir'; + } + return __awaiter(this, void 0, void 0, function () { + var _this, diskHtml, fileHtml, dirHtml, rdata, fileList, dirList, diskList, i_1, item, i_2, item, unfoldList, dirName, i, item, unfoldList, fileName; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + (_this = this), (diskHtml = ''), (fileHtml = ''), (dirHtml = ''); + return [4, this.$request('getFileDir', { path: path, disk: true })]; + case 1: + rdata = _a.sent(); + (fileList = rdata.FILES), (dirList = rdata.DIR), (diskList = rdata.DISK); + for (i_1 = 0; i_1 < diskList.length; i_1++) { + item = diskList[i_1]; + diskHtml += '
       " + item.path + '
      '; + } + for (i_2 = 0; i_2 < dirList.length; i_2++) { + (item = dirList[i_2]), (unfoldList = item.split(';')); + dirName = unfoldList[0]; + if (dirName.length > 20) dirName = dirName.substring(0, 20) + '...'; + if (this.$checkChinese(dirName) && dirName.length > 10) dirName = dirName.substring(0, 10) + '...'; + dirHtml += + '' + + (type === 'all' || type === 'dir' ? '' : '') + + '" + + dirName + + '' + + this.$formatTime(parseInt(unfoldList[2])) + + '' + + unfoldList[3] + + '' + + unfoldList[4] + + ''; + } + for (i = 0; i < fileList.length; i++) { + (item = fileList[i]), (unfoldList = item.split(';')); + fileName = unfoldList[0]; + if (fileName.length > 20) fileName = fileName.substring(0, 20) + '...'; + if (this.$checkChinese(fileName) && fileName.length > 10) fileName = fileName.substring(0, 10) + '...'; + fileHtml += + '' + + (type === 'all' || type === 'file' ? '' : '') + + '" + + fileName + + '' + + this.$formatTime(parseInt(unfoldList[2])) + + '' + + unfoldList[3] + + '' + + unfoldList[4] + + ''; + } + $('#changecomlist').html(diskHtml); + $('.default').hide(); + $('.file-list').show(); + $('#tbody').html(dirHtml + fileHtml); + if (rdata.PATH.substr(rdata.PATH.length - 1, 1) != '/') { + rdata.PATH += '/'; + } + $('#PathPlace').find('span').html(rdata.PATH); + $('#tbody tr').click(function () { + if ($(this).find('td:eq(0) input').length > 0) { + if ($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).find('td:eq(0) input').prop('checked', false); + } else { + $(this).find('td:eq(0) input').prop('checked', true); + $(this).siblings().find('td:eq(0) input').prop('checked', false); + $(this).addClass('active').siblings().removeClass('active'); + } + } + }); + $('#changecomlist dd').click(function () { + _this.getFileList($(this).attr('path'), type); + }); + $('.bt_open_dir span').click(function () { + if ($(this).parent().data('type') == 'dir') _this.getFileList($(this).parent().attr('path'), type); + }); + return [2]; + } + }); + }); + }; + Public.prototype.setMessageChannelView = function (type, info) { + if (type === void 0) { + type = 0; + } + return __awaiter(this, void 0, void 0, function () { + var rdata, _a, setup, id, token, error_2; + var _this_1 = this; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 3, , 4]); + return [4, this.$request('getMsgConfig')]; + case 1: + rdata = _b.sent(); + (_a = rdata.telegram), (setup = _a.setup), (id = _a.my_id), (token = _a.bot_token); + console.log(rdata); + console.log(rdata); + return [ + 4, + this.$open({ + area: '600px', + title: 'Setting up notification', + skin: 'layer-channel-auth', + content: '
      \n
      \n
      \n

      Email

      \n

      DingDing

      \n

      Feishu

      \n

      Telegram

      \n
      \n
      \n
      \n
      \n
      \n
      \n \n \n
      \n
      \n
      \n \n \n \n \n \n \n \n \n \n \n
      EmailOperating
      No Data
      \n
      \n
      \n
      \n
      \n\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t\t
      \n Name\n
      \n \n
      \n
      \n\t\t\t\t\t\t\t\t\t\t
      \n Url\n
      \n \n
      \n
      \n\t\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t' + ) + .concat( + setup ? '' : '', + '\n\t\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t\t
      \n Name\n
      \n \n
      \n
      \n\t\t\t\t\t\t\t\t\t\t
      \n Url\n
      \n \n
      \n
      \n\t\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t' + ) + .concat( + setup ? '' : '', + '\n\t\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t
      \n
      \n
      \n
      \n ID\n
      \n \n
      \n
      \n
      \n TOKEN\n
      \n \n
      \n
      \n \n \n ' + ) + .concat( + setup ? '' : '', + '\n
      \n
      \n
      \n
        \n
      • ID: Your telegram user ID
      • \n
      • Token: Your telegram bot token
      • \n
      • e.g: [ 12345677:AAAAAAAAA_a0VUo2jjr__CCCCDDD ] Help
      • \n
      \n
      \n
      \n
      \n
      \n
      ' + ), + success: function ($layer) { + $('.bt-w-menu p').click(function () { + var index = $(this).index(); + $(this).addClass('bgw').siblings().removeClass('bgw'); + $('.conter_box').eq(index).removeClass('hide').siblings().addClass('hide'); + }); + $('.addRecipient').on('click', function () { + return _this_1.addMessageMail(); + }); + $('.setMailMessageView').on('click', function () { + return _this_1.setMailMessageView(info); + }); + $('#receive_table').on('click', '.del_email', function (ev) { + return __awaiter(_this_1, void 0, void 0, function () { + var email, res, err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + email = $(ev.target).data('mail'); + _a.label = 1; + case 1: + _a.trys.push([1, 6, , 7]); + return [4, this.$confirm({ title: 'Delete Email ['.concat(email, ']'), msg: 'Are your sure to delete the Email?' })]; + case 2: + _a.sent(); + return [4, this.$request('delMailAddress', { email: email })]; + case 3: + res = _a.sent(); + if (!res.status) return [2]; + return [4, this.$delay()]; + case 4: + _a.sent(); + return [4, this.renderMailMessageList()]; + case 5: + _a.sent(); + return [3, 7]; + case 6: + err_1 = _a.sent(); + return [3, 7]; + case 7: + return [2]; + } + }); + }); + }); + $('.addTelegram').click(function () { + return __awaiter(_this_1, void 0, void 0, function () { + var id_1, token_1, res, err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 3, , 4]); + id_1 = $('input[name="telegram_id"]').val(); + token_1 = $('input[name="telegram_token"]').val(); + return [4, this.$verifySubmit(id_1 == '' || token_1 == '', 'input box cannot be empty!')]; + case 1: + _a.sent(); + return [4, this.$request('setTelegramConfig', { my_id: id_1, bot_token: token_1 })]; + case 2: + res = _a.sent(); + if (!res.status) return [2]; + $('.addTelegram').after(''); + this.setLinkText('.setMessageChannelTelegram', 'Telegram is set', 'btlink'); + return [3, 4]; + case 3: + err_2 = _a.sent(); + return [3, 4]; + case 4: + return [2]; + } + }); + }); + }); + $layer.on('click', '.delTelegram', function () { + return __awaiter(_this_1, void 0, void 0, function () { + var res, err_3; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 3, , 4]); + return [4, this.$confirm({ title: 'Clear set', msg: 'Delete Clear Settings?' })]; + case 1: + _a.sent(); + return [4, this.$request('clearTelegramConfig')]; + case 2: + res = _a.sent(); + if (!res.status) return [2]; + $('.delTelegram').remove(); + $('input[name="telegram_id"]').val(''); + $('input[name="telegram_token"]').val(''); + this.setLinkText('.setMessageChannelTelegram', 'Telegram is not set', 'bt_warning'); + return [3, 4]; + case 3: + err_3 = _a.sent(); + return [3, 4]; + case 4: + return [2]; + } + }); + }); + }); + $('.addDingDing').click(function () { + return __awaiter(_this_1, void 0, void 0, function () { + var name_2, url, res, err_4; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 3, , 4]); + name_2 = $('input[name=chatName]').val(); + url = $('[name=dingding_url]').val(); + return [4, this.$verifySubmit(name_2 == '' || url == '', 'input box cannot be empty!')]; + case 1: + _a.sent(); + return [4, this.$request('setDingDingConfig', { title: name_2, url: url, atall: 'True' })]; + case 2: + res = _a.sent(); + if (!res.status) return [2]; + return [3, 4]; + case 3: + err_4 = _a.sent(); + return [3, 4]; + case 4: + return [2]; + } + }); + }); + }); + _this_1.fixedTableHead('#receive_table', '356px'); + _this_1.renderMailMessageList(); + }, + }), + ]; + case 2: + _b.sent(); + return [3, 4]; + case 3: + error_2 = _b.sent(); + return [3, 4]; + case 4: + return [2]; + } + }); + }); + }; + Public.prototype.setLinkText = function (el, text, type) { + var $el = $(el); + switch (type) { + case 'btlink': + $el.text(text).addClass('btlink').removeClass('bt_warning'); + break; + case 'bt_warning': + $el.text(text).addClass('bt_warning').removeClass('btlink'); + break; + } + }; + Public.prototype.addMessageMail = function () { + return __awaiter(this, void 0, void 0, function () { + var error_3; + var _this_1 = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [ + 4, + this.$open({ + area: '400px', + title: 'Add recipient email', + btn: ['Create', 'Close'], + content: + '
      \n
      \n Recipient mailbox\n
      \n \n
      \n
      \n
      ', + yes: function (config) { + return __awaiter(_this_1, void 0, void 0, function () { + var email, rdata; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + email = $('input[name=creater_email_value]').val(); + return [ + 4, + this.$verifySubmitList([ + [email === '', 'Recipient mailbox cannot be empty!'], + [!this.$checkEmail(email), 'Recipient mailbox format is incorrect!'], + ]), + ]; + case 1: + _a.sent(); + return [4, this.$request('addMailAddress', { email: email })]; + case 2: + rdata = _a.sent(); + if (!rdata.status) return [3, 5]; + config.close(); + return [4, this.$delay()]; + case 3: + _a.sent(); + return [4, this.renderMailMessageList()]; + case 4: + _a.sent(); + _a.label = 5; + case 5: + return [2]; + } + }); + }); + }, + }), + ]; + case 1: + _a.sent(); + return [3, 3]; + case 2: + error_3 = _a.sent(); + return [3, 3]; + case 3: + return [2]; + } + }); + }); + }; + Public.prototype.setMailMessageView = function (info) { + return __awaiter(this, void 0, void 0, function () { + var rdata, _a, qq_mail, qq_stmp_pwd, hosts, port, verifyPortList, err_5; + var _this_1 = this; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 3, , 4]); + return [4, this.$request('getMsgConfig')]; + case 1: + rdata = _b.sent(); + (_a = rdata.user_mail.info.msg), (qq_mail = _a.qq_mail), (qq_stmp_pwd = _a.qq_stmp_pwd), (hosts = _a.hosts), (port = _a.port); + qq_mail = qq_mail || ''; + qq_stmp_pwd = qq_stmp_pwd || ''; + hosts = hosts || ''; + port = port || ''; + verifyPortList = ['25', '465', '587', '']; + return [ + 4, + this.$open({ + title: 'Set sender email information', + area: '466px', + content: + '
      \n
      \n Sender email\n
      \n
      \n
      \n SMTP password\n
      \n
      \n
      \n SMTP server\n
      \n
      \n
      \n SMTP port\n
      \n \n \n
      \n
      \n
        \n
      • 465 port is recommended, the protocol is SSL/TLS
      • \n
      • Port 25 is SMTP protocol, port 587 is STARTTLS protocol
      • \n
      \n
      \n ' + ) + .concat( + qq_mail ? '' : '', + '\n \n \n
      \n
      ' + ), + success: function (layers, indexs) { + return __awaiter(_this_1, void 0, void 0, function () { + var portSelect, mailPort; + var _this_1 = this; + return __generator(this, function (_a) { + portSelect = $('#port_select'); + mailPort = $('input[name=channel_email_port]'); + portSelect.change(function (ev) { + var that = $(ev.target), + mailPort = $('input[name=channel_email_port]'); + that.css('width', ev.target.value === 'other' ? '100px' : '300px'); + mailPort.css('display', ev.target.value === 'other' ? 'inline-block' : 'none'); + }); + $('.SetChannelEmail').click(function () { + return __awaiter(_this_1, void 0, void 0, function () { + var email, stmp_pwd, hosts, port, portSelectVal, rdata, text, type; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + email = $('input[name="channel_email_value"]').val(); + stmp_pwd = $('input[name="channel_email_password"]').val(); + hosts = $('input[name="channel_email_server"]').val(); + (port = ''), (portSelectVal = portSelect.val()); + port = portSelectVal === 'other' ? mailPort.val() : portSelectVal; + return [ + 4, + this.$verifySubmitList([ + [!email, 'Email address cannot be empty!'], + [!stmp_pwd, 'STMP password cannot be empty!'], + [!hosts, 'STMP server address cannot be empty!'], + [!port, 'STMP server port cannot be empty!'], + [!this.$checkPort(port), 'STMP server port format is incorrect'], + ]), + ]; + case 1: + _a.sent(); + return [4, this.$request('setMailConfig', { email: email, stmp_pwd: stmp_pwd, hosts: hosts, port: port })]; + case 2: + rdata = _a.sent(); + if (!rdata.status) return [2]; + layer.close(indexs); + info.isSetEmail = true; + text = info.mail ? 'Already set' : 'Not set'; + type = info.mail ? 'btlink' : 'bt_warning'; + this.setLinkText('.setMessageChannelMail', 'Email is set', 'btlink'); + this.setLinkText('.setAlarmMail', text, type); + return [4, this.$delay()]; + case 3: + _a.sent(); + return [4, this.renderMailMessageList()]; + case 4: + _a.sent(); + return [2]; + } + }); + }); + }); + $('.smtp_closeBtn').click(function () { + layer.close(indexs); + }); + $('.set_empty').click(function () { + return __awaiter(_this_1, void 0, void 0, function () { + var rdata; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + return [4, this.$request(['config/set_empty', 'notification, please wait...'], { type: 'mail' })]; + case 1: + rdata = _a.sent(); + if (!rdata.status) return [2]; + layer.close(indexs); + info.isSetEmail = false; + this.setLinkText('.setMessageChannelMail', 'Email is not set', 'bt_warning'); + this.setLinkText('.setAlarmMail', 'Email is not set', 'bt_warning'); + return [2]; + } + }); + }); + }); + return [2]; + }); + }); + }, + }), + ]; + case 2: + _b.sent(); + return [3, 4]; + case 3: + err_5 = _b.sent(); + return [3, 4]; + case 4: + return [2]; + } + }); + }); + }; + Public.prototype.renderMailMessageList = function () { + return __awaiter(this, void 0, void 0, function () { + var rdata, _html, _list, i, item; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + return [4, this.$request('getMailList', { loading: true, msg: false })]; + case 1: + rdata = _a.sent(); + (_html = ''), (_list = rdata.user_mail.mail_list); + if (_list.length > 0) { + for (i = 0; i < _list.length; i++) { + item = _list[i]; + _html += '\n ' + .concat(item, '.\n \n Del\n \n '); + } + } else { + _html = 'No Data'; + } + $('#receive_table tbody').html(_html); + return [2]; + } + }); + }); + }; + return Public; + })(utils_1.default); + exports.default = Public; }); diff --git a/BTPanel/static/bootstrap-3.3.5/css/bootstrap.min.css b/BTPanel/static/bootstrap-3.3.5/css/bootstrap.min.css index da4797c2..f3605bee 100644 --- a/BTPanel/static/bootstrap-3.3.5/css/bootstrap.min.css +++ b/BTPanel/static/bootstrap-3.3.5/css/bootstrap.min.css @@ -2,4 +2,4 @@ * Bootstrap v3.3.5 (http://getbootstrap.com) * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-vip:before{content:"\e600"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before,.glyphicon-True:before{content:"\e072"}.glyphicon-pause:before,.glyphicon-False:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:none}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:none}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.33px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:none}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#555;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#fff;background-color:#10952a;border-color:#398439}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#fff;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#20a53a;border-color:#20a53a}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#10952a;border-color:#255625}.btn-success:hover{color:#fff;background-color:#10952a;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#10952a;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#20a53a;border-color:#20a53a}.btn-success .badge{color:#20a53a;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:3;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#20a53a}.label-success[href]:focus,.label-success[href]:hover{background-color:#10952a}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#20a53a}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}.bootstrap-select>select.bs-select-hidden,select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\0}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;z-index:1;text-align:right;white-space:nowrap}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover{color:rgba(255,255,255,.5)}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2}.bootstrap-select.is-invalid .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle,.was-validated .bootstrap-select .selectpicker:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select .selectpicker:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}:not(.input-group)>.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{z-index:auto}.bootstrap-select.form-control.input-group-btn:not(:first-child):not(:last-child)>.btn{border-radius:0}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*=col-].dropdown-menu-right,.row .bootstrap-select[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select,.form-horizontal .bootstrap-select,.form-inline .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-lg .dropdown-toggle,.bootstrap-select.form-control-sm .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:0!important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0!important;padding:0!important}.bootstrap-select.bs-container .dropdown-menu{z-index:1060}.bootstrap-select .dropdown-toggle:before{content:'';display:inline-block}.bootstrap-select .dropdown-toggle .filter-option{position:absolute;top:0;left:0;padding-top:inherit;padding-right:inherit;padding-bottom:inherit;padding-left:inherit;height:100%;width:100%;text-align:left}.bootstrap-select .dropdown-toggle .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:0!important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,.5)!important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:.5em;height:1em;border-style:solid;border-width:0 .26em .26em 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} \ No newline at end of file + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-vip:before{content:"\e600"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before,.glyphicon-True:before{content:"\e072"}.glyphicon-pause:before,.glyphicon-False:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{text-decoration:none}a:focus{outline:none}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:none}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.33px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:none}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#555;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#fff;background-color:#10952a;border-color:#398439}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#fff;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#20a53a;border-color:#20a53a}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#10952a;border-color:#255625}.btn-success:hover{color:#fff;background-color:#10952a;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#10952a;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#20a53a;border-color:#20a53a}.btn-success .badge{color:#20a53a;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:3;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#20a53a}.label-success[href]:focus,.label-success[href]:hover{background-color:#10952a}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#20a53a}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}.bootstrap-select>select.bs-select-hidden,select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\0}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;z-index:1;text-align:right;white-space:nowrap}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover{color:rgba(255,255,255,.5)}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2}.bootstrap-select.is-invalid .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle,.was-validated .bootstrap-select .selectpicker:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select .selectpicker:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}:not(.input-group)>.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{z-index:auto}.bootstrap-select.form-control.input-group-btn:not(:first-child):not(:last-child)>.btn{border-radius:0}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*=col-].dropdown-menu-right,.row .bootstrap-select[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select,.form-horizontal .bootstrap-select,.form-inline .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-lg .dropdown-toggle,.bootstrap-select.form-control-sm .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:0!important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0!important;padding:0!important}.bootstrap-select.bs-container .dropdown-menu{z-index:1060}.bootstrap-select .dropdown-toggle:before{content:'';display:inline-block}.bootstrap-select .dropdown-toggle .filter-option{position:absolute;top:0;left:0;padding-top:inherit;padding-right:inherit;padding-bottom:inherit;padding-left:inherit;height:100%;width:100%;text-align:left}.bootstrap-select .dropdown-toggle .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:0!important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,.5)!important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:.5em;height:1em;border-style:solid;border-width:0 .26em .26em 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} \ No newline at end of file diff --git a/BTPanel/static/ckeditor/CHANGES.md b/BTPanel/static/ckeditor/CHANGES.md new file mode 100644 index 00000000..acb979c1 --- /dev/null +++ b/BTPanel/static/ckeditor/CHANGES.md @@ -0,0 +1,1703 @@ +CKEditor 4 Changelog +==================== + +## CKEditor 4.11.4 + +Fixed Issues: + +* [#589](https://github.com/ckeditor/ckeditor-dev/issues/589): Fixed: The editor causes memory leaks in create and destroy cycles. +* [#1397](https://github.com/ckeditor/ckeditor-dev/issues/1397): Fixed: Using the dialog to remove headers from a [table](https://ckeditor.com/cke4/addon/table) with one header row only throws an error. +* [#1479](https://github.com/ckeditor/ckeditor-dev/issues/1479): Fixed: [Justification](https://ckeditor.com/cke4/addon/justify) for styled content in BR mode is disabled. +* [#2816](https://github.com/ckeditor/ckeditor-dev/issues/2816): Fixed: [Enhanced Image](https://ckeditor.com/cke4/addon/image2) resize handler is visible in [read-only mode](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_readonly.html). +* [#2874](https://github.com/ckeditor/ckeditor-dev/issues/2874): Fixed: [Enhanced Image](https://ckeditor.com/cke4/addon/image2) resize handler is not created when the editor is initialized in [read-only mode](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_readonly.html). +* [#2775](https://github.com/ckeditor/ckeditor-dev/issues/2775): Fixed: [Clipboard](https://ckeditor.com/cke4/addon/clipboard) paste buttons have wrong state when [read-only](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_readonly.html) mode is set by the mouse event listener with the [Div Editing Area](https://ckeditor.com/cke4/addon/divarea) plugin. +* [#1901](https://github.com/ckeditor/ckeditor-dev/issues/1901): Fixed: Cannot open the context menu over a [Widget](https://ckeditor.com/cke4/addon/widget) with the Shift+F10 keyboard shortcut. + +Other Changes: + +* Updated [WebSpellChecker](https://ckeditor.com/cke4/addon/wsc) (WSC) and [SpellCheckAsYouType](https://ckeditor.com/cke4/addon/scayt) (SCAYT) plugins: + * Language dictionary update: German language was extended with over 600k new words. + * Language dictionary update: Swedish language was extended with over 300k new words. + * Grammar support added for Australian and New Zealand English, Polish, Slovak, Slovenian and Austrian languages. + * Changed wavy red and green lines that underline spelling and grammar errors to straight ones. + * [#55](https://github.com/WebSpellChecker/ckeditor-plugin-wsc/issues/55): Fixed: WSC does not use [`CKEDITOR.getUrl()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#method-getUrl) when referencing style sheets. + * [#166](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/166): Fixed: SCAYT does not use [`CKEDITOR.getUrl()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#method-getUrl) when referencing style sheets. + * [#56](https://github.com/WebSpellChecker/ckeditor-plugin-wsc/issues/56): [Chrome] Fixed: SCAYT/WSC throws errors when running inside a Chrome extension. + * Fixed: After removing a dictionary, the words are not underlined and considered as incorrect. + * Fixed: The Slovenian (`sl_SL`) language does not work. + * Fixed: Quotes with code `U+2019` (Right single quotation mark) are considered separators. + * Fixed: Wrong error message formatting when the service ID is invalid. + * Fixed: Absent languages in the Languages tab when using SCAYT with the [Shared Spaces](https://ckeditor.com/cke4/addon/sharedspace) plugin. + +## CKEditor 4.11.3 + +Fixed Issues: + +* [#2721](https://github.com/ckeditor/ckeditor-dev/issues/2721), [#487](https://github.com/ckeditor/ckeditor-dev/issues/487): Fixed: The order of sublist items is reversed when a higher level list item is removed. +* [#2527](https://github.com/ckeditor/ckeditor-dev/issues/2527): Fixed: [Emoji](https://ckeditor.com/cke4/addon/emoji) autocomplete order does not prioritize emojis with the name starting from the used string. +* [#2572](https://github.com/ckeditor/ckeditor-dev/issues/2572): Fixed: Icons in the [Emoji](https://ckeditor.com/cke4/addon/emoji) dropdown navigation groups are not centered. +* [#1191](https://github.com/ckeditor/ckeditor-dev/issues/1191): Fixed: Items in the [elements path](https://ckeditor.com/cke4/addon/elementspath) are draggable. +* [#2292](https://github.com/ckeditor/ckeditor-dev/issues/2292): Fixed: Dropping a list with a link on the editor's margin causes a console error and removes the dragged text from editor. +* [#2756](https://github.com/ckeditor/ckeditor-dev/issues/2756): Fixed: The [Auto Link](https://ckeditor.com/cke4/addon/autolink) plugin causes an error when typing in the [source editing mode](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_sourcearea.html). +* [#1986](https://github.com/ckeditor/ckeditor-dev/issues/1986): Fixed: The Cell Properties dialog from the [Table Tools](https://ckeditor.com/cke4/addon/tabletools) plugin shows styles that are not allowed through [`config.allowedContent`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-allowedContent). +* [#2565](https://github.com/ckeditor/ckeditor-dev/issues/2565): [IE, Edge] Fixed: Buttons in the [editor toolbar](https://ckeditor.com/cke4/addon/toolbar) are activated by clicking them with the right mouse button. +* [#2792](https://github.com/ckeditor/ckeditor-dev/pull/2792): Fixed: A bug in the [Copy Formatting](https://ckeditor.com/cke4/addon/copyformatting) plugin that caused the following issues: + * [#2780](https://github.com/ckeditor/ckeditor-dev/issues/2780): Fixed: Undo steps disappear after multiple changes of selection. + * [#2470](https://github.com/ckeditor/ckeditor-dev/issues/2470): [Firefox] Fixed: Widget's nested editable gets blurred upon focus. + * [#2655](https://github.com/ckeditor/ckeditor-dev/issues/2655): [Chrome, Safari] Fixed: Widget's nested editable cannot be focused under certain circumstances. + +## CKEditor 4.11.2 + +Fixed Issues: + +* [#2403](https://github.com/ckeditor/ckeditor-dev/issues/2403): Fixed: Styling inline editor initialized inside a table with the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin is causing style leaks. +* [#2514](https://github.com/ckeditor/ckeditor-dev/issues/2403): Fixed: Pasting table data into inline editor initialized inside a table with the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin inserts pasted content into the wrapping table. +* [#2451](https://github.com/ckeditor/ckeditor-dev/issues/2451): Fixed: The [Remove Format](https://ckeditor.com/cke4/addon/removeformat) plugin changes selection. +* [#2546](https://github.com/ckeditor/ckeditor-dev/issues/2546): Fixed: The separator in the toolbar moves when buttons are focused. +* [#2506](https://github.com/ckeditor/ckeditor-dev/issues/2506): Fixed: [Enhanced Image](https://ckeditor.com/cke4/addon/image2) throws a type error when an empty `
      ` tag with an `image` class is upcasted. +* [#2650](https://github.com/ckeditor/ckeditor-dev/issues/2650): Fixed: [Table](https://ckeditor.com/cke4/addon/table) dialog validator fails when the `getValue()` function is defined in the global scope. +* [#2690](https://github.com/ckeditor/ckeditor-dev/issues/2690): Fixed: Decimal characters are removed from the inside of numbered lists when pasting content using the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#2205](https://github.com/ckeditor/ckeditor-dev/issues/2205): Fixed: It is not possible to add new list items under an item containing a block element. +* [#2411](https://github.com/ckeditor/ckeditor-dev/issues/2411), [#2438](https://github.com/ckeditor/ckeditor-dev/issues/2438) Fixed: Apply numbered list option throws a console error for a specific markup. +* [#2430](https://github.com/ckeditor/ckeditor-dev/issues/2430) Fixed: [Color Button](https://ckeditor.com/cke4/addon/colorbutton) and [List Block](https://ckeditor.com/cke4/addon/listblock) items are draggable. + +Other Changes: + +* Updated the [WebSpellChecker](https://ckeditor.com/cke4/addon/wsc) (WSC) plugin: + * [#52](https://github.com/WebSpellChecker/ckeditor-plugin-wsc/issues/52) Fixed: Clicking "Finish Checking" without a prior action would hang the Spell Checking dialog. +* [#2603](https://github.com/ckeditor/ckeditor-dev/issues/2603): Corrected the GPL license entry in the `package.json` file. + +## CKEditor 4.11.1 + +Fixed Issues: + +* [#2571](https://github.com/ckeditor/ckeditor-dev/issues/2571): Fixed: Clicking the categories in the [Emoji](https://ckeditor.com/cke4/addon/emoji) dropdown panel scrolls the entire page. + +## CKEditor 4.11 + +**Security Updates:** + +* Fixed XSS vulnerability in the HTML parser reported by [maxarr](https://hackerone.com/maxarr). + + Issue summary: It was possible to execute XSS inside CKEditor after persuading the victim to: (i) switch CKEditor to source mode, then (ii) paste a specially crafted HTML code, prepared by the attacker, into the opened CKEditor source area, and (iii) switch back to WYSIWYG mode. + +**An upgrade is highly recommended!** + +New Features: + +* [#2062](https://github.com/ckeditor/ckeditor-dev/pull/2062): Added the emoji dropdown that allows the user to choose the emoji from the toolbar and search for them using keywords. +* [#2154](https://github.com/ckeditor/ckeditor-dev/issues/2154): The [Link](https://ckeditor.com/cke4/addon/link) plugin now supports phone number links. +* [#1815](https://github.com/ckeditor/ckeditor-dev/issues/1815): The [Auto Link](https://ckeditor.com/cke4/addon/autolink) plugin supports typing link completion. +* [#2478](https://github.com/ckeditor/ckeditor-dev/issues/2478): [Link](https://ckeditor.com/cke4/addon/link) can be inserted using the Ctrl/Cmd + K keystroke. +* [#651](https://github.com/ckeditor/ckeditor-dev/issues/651): Text pasted using the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin preserves indentation in paragraphs. +* [#2248](https://github.com/ckeditor/ckeditor-dev/issues/2248): Added support for justification in the [BBCode](https://ckeditor.com/cke4/addon/bbcode) plugin. Thanks to [Matěj Kmínek](https://github.com/KminekMatej)! +* [#706](https://github.com/ckeditor/ckeditor-dev/issues/706): Added a different cursor style when selecting cells for the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin. +* [#2072](https://github.com/ckeditor/ckeditor-dev/issues/2072): The [UI Button](https://ckeditor.com/cke4/addon/button) plugin supports custom `aria-haspopup` property values. The [Menu Button](https://ckeditor.com/cke4/addon/menubutton) `aria-haspopup` value is now `menu`, the [Panel Button](https://ckeditor.com/cke4/addon/panelbutton) and [Rich Combo](https://ckeditor.com/cke4/addon/richcombo) `aria-haspopup` value is now `listbox`. +* [#1176](https://github.com/ckeditor/ckeditor-dev/pull/1176): The [Balloon Panel](https://ckeditor.com/cke4/addon/balloonpanel) can now be attached to a selection instead of an element. +* [#2202](https://github.com/ckeditor/ckeditor-dev/issues/2202): Added the `contextmenu_contentsCss` configuration option to allow adding custom CSS to the [Context Menu](https://ckeditor.com/cke4/addon/contextmenu). + +Fixed Issues: + +* [#1477](https://github.com/ckeditor/ckeditor-dev/issues/1477): Fixed: On destroy, [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) does not destroy its content. +* [#2394](https://github.com/ckeditor/ckeditor-dev/issues/2394): Fixed: [Emoji](https://ckeditor.com/cke4/addon/emoji) dropdown does not show up with repeated symbols in a single line. +* [#1181](https://github.com/ckeditor/ckeditor-dev/issues/1181): [Chrome] Fixed: Opening the context menu in a read-only editor results in an error. +* [#2276](https://github.com/ckeditor/ckeditor-dev/issues/2276): [iOS] Fixed: [Button](https://ckeditor.com/cke4/addon/button) state does not refresh properly. +* [#1489](https://github.com/ckeditor/ckeditor-dev/issues/1489): Fixed: Table contents can be removed in read-only mode when the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin is used. +* [#1264](https://github.com/ckeditor/ckeditor-dev/issues/1264) Fixed: Right-click does not clear the selection created with the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin. +* [#586](https://github.com/ckeditor/ckeditor-dev/issues/586) Fixed: The `required` attribute is not correctly recognized by the [Form Elements](https://ckeditor.com/cke4/addon/forms) plugin dialog. Thanks to [Roli Züger](https://github.com/rzueger)! +* [#2380](https://github.com/ckeditor/ckeditor-dev/issues/2380) Fixed: Styling HTML comments in a top-level element results in extra paragraphs. +* [#2294](https://github.com/ckeditor/ckeditor-dev/issues/2294) Fixed: Pasting content from Microsoft Outlook and then bolding it results in an error. +* [#2035](https://github.com/ckeditor/ckeditor-dev/issues/2035) [Edge] Fixed: `Permission denied` is thrown when opening a [Panel](https://ckeditor.com/cke4/addon/panel) instance. +* [#965](https://github.com/ckeditor/ckeditor-dev/issues/965) Fixed: The [`config.forceSimpleAmpersand`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-forceSimpleAmpersand) option does not work. Thanks to [Alex Maris](https://github.com/alexmaris)! +* [#2448](https://github.com/ckeditor/ckeditor-dev/issues/2448): Fixed: The [`Escape HTML Entities`] plugin with custom [additional entities](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-entities_additional) configuration breaks HTML escaping. +* [#898](https://github.com/ckeditor/ckeditor-dev/issues/898): Fixed: [Enhanced Image](https://ckeditor.com/cke4/addon/image2) long alternative text protrudes into the editor when the image is selected. +* [#1113](https://github.com/ckeditor/ckeditor-dev/issues/1113): [Firefox] Fixed: Nested contenteditable elements path is not updated on focus with the [Div Editing Area](https://ckeditor.com/cke4/addon/divarea) plugin. +* [#1682](https://github.com/ckeditor/ckeditor-dev/issues/1682) Fixed: Hovering the [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) panel changes its size, causing flickering. +* [#421](https://github.com/ckeditor/ckeditor-dev/issues/421) Fixed: Expandable [Button](https://ckeditor.com/cke4/addon/button) puts the `(Selected)` text at the end of the label when clicked. +* [#1454](https://github.com/ckeditor/ckeditor-dev/issues/1454): Fixed: The [`onAbort`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_fileTools_uploadWidgetDefinition.html#property-onAbort) method of the [Upload Widget](https://ckeditor.com/cke4/addon/uploadwidget) is not called when the loader is aborted. +* [#1451](https://github.com/ckeditor/ckeditor-dev/issues/1451): Fixed: The context menu is incorrectly positioned when opened with Shift+F10. +* [#1722](https://github.com/ckeditor/ckeditor-dev/issues/1722): [`CKEDITOR.filter.instances`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_filter.html#static-property-instances) is causing memory leaks. +* [#2491](https://github.com/ckeditor/ckeditor-dev/issues/2491): Fixed: The [Mentions](https://ckeditor.com/cke4/addon/mentions) plugin is not matching diacritic characters. +* [#2519](https://github.com/ckeditor/ckeditor-dev/issues/2519): Fixed: The [Accessibility Help](https://ckeditor.com/cke4/addon/a11yhelp) dialog should display all available keystrokes for a single command. + +API Changes: + +* [#2453](https://github.com/ckeditor/ckeditor-dev/issues/2453): The [`CKEDITOR.ui.panel.block.getItems`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_ui_panel_block.html#method-getItems) method now also returns `input` elements in addition to links. +* [#2224](https://github.com/ckeditor/ckeditor-dev/issues/2224): The [`CKEDITOR.tools.convertToPx`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-convertToPx) function now converts negative values. +* [#2253](https://github.com/ckeditor/ckeditor-dev/issues/2253): The widget definition [`insert`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_plugins_widget_definition.html#property-insert) method now passes `editor` and `commandData`. Thanks to [marcparmet](https://github.com/marcparmet)! +* [#2045](https://github.com/ckeditor/ckeditor-dev/issues/2045): Extracted [`tools.eventsBuffer`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-eventsBuffer) and [`tools.throttle`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-throttle) functions logic into a separate namespace. + * [`tools.eventsBuffer`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-eventsBuffer) was extracted into [`tools.buffers.event`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools_buffers_event.html), + * [`tools.throttle`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-throttle) was extracted into [`tools.buffers.throttle`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools_buffers_throttle.html). +* [#2466](https://github.com/ckeditor/ckeditor-dev/issues/2466): The [`CKEDITOR.filter`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-constructor) constructor accepts an additional `rules` parameter allowing to bind the editor and filter together. +* [#2493](https://github.com/ckeditor/ckeditor-dev/issues/2493): The [`editor.getCommandKeystroke`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-getCommandKeystroke) method accepts an additional `all` parameter allowing to retrieve an array of all command keystrokes. +* [#2483](https://github.com/ckeditor/ckeditor-dev/issues/2483): Button's DOM element created with the [`hasArrow`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_ui.html#method-addButton) definition option can by identified by the `.cke_button_expandable` CSS class. + +Other Changes: + +* [#1713](https://github.com/ckeditor/ckeditor-dev/issues/1713): Removed the redundant `lang.title` entry from the [Clipboard](https://ckeditor.com/cke4/addon/clipboard) plugin. + +## CKEditor 4.10.1 + +Fixed Issues: + +* [#2114](https://github.com/ckeditor/ckeditor-dev/issues/2114): Fixed: [Autocomplete](https://ckeditor.com/cke4/addon/autocomplete) cannot be initialized before [`instanceReady`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-instanceReady). +* [#2107](https://github.com/ckeditor/ckeditor-dev/issues/2107): Fixed: Holding and releasing the mouse button is not inserting an [autocomplete](https://ckeditor.com/cke4/addon/autocomplete) suggestion. +* [#2167](https://github.com/ckeditor/ckeditor-dev/issues/2167): Fixed: Matching in [Emoji](https://ckeditor.com/cke4/addon/emoji) plugin is not case insensitive. +* [#2195](https://github.com/ckeditor/ckeditor-dev/issues/2195): Fixed: [Emoji](https://ckeditor.com/cke4/addon/emoji) shows the suggestion box when the colon is preceded with other characters than white space. +* [#2169](https://github.com/ckeditor/ckeditor-dev/issues/2169): [Edge] Fixed: Error thrown when pasting into the editor. +* [#1084](https://github.com/ckeditor/ckeditor-dev/issues/1084) Fixed: Using the "Automatic" option with [Color Button](https://ckeditor.com/cke4/addon/colorbutton) on a text with the color already defined sets an invalid color value. +* [#2271](https://github.com/ckeditor/ckeditor-dev/issues/2271): Fixed: Custom color name not used as a label in the [Color Button](https://ckeditor.com/cke4/addon/image2) plugin. Thanks to [Eric Geloen](https://github.com/egeloen)! +* [#2296](https://github.com/ckeditor/ckeditor-dev/issues/2296): Fixed: The [Color Button](https://ckeditor.com/cke4/addon/colorbutton) plugin throws an error when activated on content containing HTML comments. +* [#966](https://github.com/ckeditor/ckeditor-dev/issues/966): Fixed: Executing [`editor.destroy()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-destroy) during the [file upload](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_fileTools_uploadWidgetDefinition.html#property-onUploading) throws an error. Thanks to [Maksim Makarevich](https://github.com/MaksimMakarevich)! +* [#1719](https://github.com/ckeditor/ckeditor-dev/issues/1719): Fixed: Ctrl/Cmd + A inadvertently focuses inline editor if it is starting and ending with a list. Thanks to [theNailz](https://github.com/theNailz)! +* [#1046](https://github.com/ckeditor/ckeditor-dev/issues/1046): Fixed: Subsequent new links do not include the `id` attribute. Thanks to [Nathan Samson](https://github.com/nathansamson)! +* [#1348](https://github.com/ckeditor/ckeditor-dev/issues/1348): Fixed: [Enhanced Image](https://ckeditor.com/cke4/addon/image2) plugin aspect ratio locking uses an old width and height on image URL change. +* [#1791](https://github.com/ckeditor/ckeditor-dev/issues/1791): Fixed: [Image](https://ckeditor.com/cke4/addon/image) and [Enhanced Image](https://ckeditor.com/cke4/addon/image2) plugins can be enabled when [Easy Image](https://ckeditor.com/cke4/addon/easyimage) is present. +* [#2254](https://github.com/ckeditor/ckeditor-dev/issues/2254): Fixed: [Image](https://ckeditor.com/cke4/addon/image) ratio locking is too precise for resized images. Thanks to [Jonathan Gilbert](https://github.com/logiclrd)! +* [#1184](https://github.com/ckeditor/ckeditor-dev/issues/1184): [IE8-11] Fixed: Copying and pasting data in [read-only mode](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#property-readOnly) throws an error. +* [#1916](https://github.com/ckeditor/ckeditor-dev/issues/1916): [IE9-11] Fixed: Pressing the Delete key in [read-only mode](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#property-readOnly) throws an error. +* [#2003](https://github.com/ckeditor/ckeditor-dev/issues/2003): [Firefox] Fixed: Right-clicking multiple selected table cells containing empty paragraphs removes the selection. +* [#1816](https://github.com/ckeditor/ckeditor-dev/issues/1816): Fixed: Table breaks when Enter is pressed over the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin. +* [#1115](https://github.com/ckeditor/ckeditor-dev/issues/1115): Fixed: The `` tag is not preserved when proper configuration is provided and a style is applied by the [Font](https://ckeditor.com/cke4/addon/font) plugin. +* [#727](https://github.com/ckeditor/ckeditor-dev/issues/727): Fixed: Custom styles may be invisible in the [Styles Combo](https://ckeditor.com/cke4/addon/stylescombo) plugin. +* [#988](https://github.com/ckeditor/ckeditor-dev/issues/988): Fixed: ACF-enabled custom elements prefixed with `object`, `embed`, `param` are removed from the editor content. + +API Changes: + +* [#2249](https://github.com/ckeditor/ckeditor-dev/issues/1791): Added the [`editor.plugins.detectConflict()`](https://ckeditor.com/docs/ckeditor4/latest/CKEDITOR_editor_plugins.html#method-detectConflict) method finding conflicts between provided plugins. + +## CKEditor 4.10 + +New Features: + +* [#1751](https://github.com/ckeditor/ckeditor-dev/issues/1751): Introduced the **Autocomplete** feature that consists of the following plugins: + * [Autocomplete](https://ckeditor.com/cke4/addon/autocomplete) – Provides contextual completion feature for custom text matches based on user input. + * [Text Watcher](https://ckeditor.com/cke4/addon/textWatcher) – Checks whether an editor's text change matches the chosen criteria. + * [Text Match](https://ckeditor.com/cke4/addon/textMatch) – Allows to search [`CKEDITOR.dom.range`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_range.html) for matching text. +* [#1703](https://github.com/ckeditor/ckeditor-dev/issues/1703): Introduced the [Mentions](https://ckeditor.com/cke4/addon/mentions) plugin providing smart completion feature for custom text matches based on user input starting with a chosen marker character. +* [#1746](https://github.com/ckeditor/ckeditor-dev/issues/1703): Introduced the [Emoji](https://ckeditor.com/cke4/addon/emoji) plugin providing completion feature for emoji ideograms. +* [#1761](https://github.com/ckeditor/ckeditor-dev/issues/1761): The [Auto Link](https://ckeditor.com/cke4/addon/autolink) plugin now supports email links. + +Fixed Issues: + +* [#1458](https://github.com/ckeditor/ckeditor-dev/issues/1458): [Edge] Fixed: After blurring the editor it takes 2 clicks to focus a widget. +* [#1034](https://github.com/ckeditor/ckeditor-dev/issues/1034): Fixed: JAWS leaves forms mode after pressing the Enter key in an inline editor instance. +* [#1748](https://github.com/ckeditor/ckeditor-dev/pull/1748): Fixed: Missing [`CKEDITOR.dialog.definition.onHide`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dialog_definition.html#property-onHide) API documentation. Thanks to [sunnyone](https://github.com/sunnyone)! +* [#1321](https://github.com/ckeditor/ckeditor-dev/issues/1321): Fixed: Ideographic space character (`\u3000`) is lost when pasting text. +* [#1776](https://github.com/ckeditor/ckeditor-dev/issues/1776): Fixed: Empty caption placeholder of the [Image Base](https://ckeditor.com/cke4/addon/imagebase) plugin is not hidden when blurred. +* [#1592](https://github.com/ckeditor/ckeditor-dev/issues/1592): Fixed: The [Image Base](https://ckeditor.com/cke4/addon/imagebase) plugin caption is not visible after paste. +* [#620](https://github.com/ckeditor/ckeditor-dev/issues/620): Fixed: The [`config.forcePasteAsPlainText`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-forcePasteAsPlainText) option is not respected in internal and cross-editor pasting. +* [#1467](https://github.com/ckeditor/ckeditor-dev/issues/1467): Fixed: The resizing cursor of the [Table Resize](https://ckeditor.com/cke4/addon/tableresize) plugin appearing in the middle of a merged cell. + +API Changes: + +* [#850](https://github.com/ckeditor/ckeditor-dev/issues/850): Backward incompatibility: Replaced the `replace` dialog from the [Find / Replace](https://ckeditor.com/cke4/addon/find) plugin with a `tabId` option in the `find` command. +* [#1582](https://github.com/ckeditor/ckeditor-dev/issues/1582): The [`CKEDITOR.editor.addCommand()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-addCommand) method can now accept a [`CKEDITOR.command`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_command.html) instance as a parameter. +* [#1712](https://github.com/ckeditor/ckeditor-dev/issues/1712): The [`extraPlugins`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-extraPlugins), [`removePlugins`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-removePlugins) and [`plugins`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-plugins) configuration options allow whitespace. +* [#1802](https://github.com/ckeditor/ckeditor-dev/issues/1802): The [`extraPlugins`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-extraPlugins), [`removePlugins`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-removePlugins) and [`plugins`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-plugins) configuration options allow passing plugin names as an array. +* [#1724](https://github.com/ckeditor/ckeditor-dev/issues/1724): Added an option to the [`getClientRect()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_element.html#method-getClientRect) function allowing to retrieve an absolute bounding rectangle of the element, i.e. a position relative to the upper-left corner of the topmost viewport. +* [#1498](https://github.com/ckeditor/ckeditor-dev/issues/1498) : Added a new [`getClientRects()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_range.html#method-getClientRects) method to `CKEDITOR.dom.range`. It returns a list of rectangles for each selected element. +* [#1993](https://github.com/ckeditor/ckeditor-dev/issues/1993): Added the [`CKEDITOR.tools.throttle()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-throttle) function. + +Other Changes: + +* Updated [SCAYT](https://ckeditor.com/cke4/addon/scayt) (Spell Check As You Type) and [WebSpellChecker](https://ckeditor.com/cke4/addon/wsc) (WSC) plugins: + * Language dictionary update: Added support for the Uzbek Latin language. + * Languages no longer supported as additional languages: Manx - Isle of Man (`gv_GB`) and Interlingua (`ia_XR`). + * Extended and improved language dictionaries: Georgian and Swedish. Also added the missing word _"Ensure"_ to the American, British and Canada English language. + * [#141](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/141) Fixed: SCAYT throws "Uncaught Error: Error in RangyWrappedRange module: createRange(): Parameter must be a Window object or DOM node". + * [#153](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/153) [Chrome] Fixed: Correcting a word in the widget in SCAYT moves focus to another editable. + * [#155](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/155) [IE8] Fixed: SCAYT throws an error and does not work. + * [#156](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/156) [IE10] Fixed: SCAYT does not seem to work. + * Fixed: After some text is dragged and dropped, the markup is not refreshed for grammar problems in SCAYT. + * Fixed: Request to FastCGI fails when the user tries to replace a word with non-English characters with a proper suggestion in WSC. + * [Firefox] Fixed: Ctrl+Z removes focus in SCAYT. + * Grammar support for default languages was improved. + * New application source URL was added in SCAYT. + * Removed green marks and legend related to grammar-supported languages in the Languages tab of SCAYT. Grammar is now supported for almost all the anguages in the list for an additional fee. + * Fixed: JavaScript error in the console: "Cannot read property 'split' of undefined" in SCAYT and WSC. + * [IE10] Fixed: Markup is not set for a specific case in SCAYT. + * Fixed: Accessibility issue: No `alt` attribute for the logo image in the About tab of SCAYT. + +## CKEditor 4.9.2 + +**Security Updates:** + +* Fixed XSS vulnerability in the [Enhanced Image](https://ckeditor.com/cke4/addon/image2) (`image2`) plugin reported by [Kyaw Min Thein](https://twitter.com/kyawminthein99). + + Issue summary: It was possible to execute XSS inside CKEditor using the `` tag and specially crafted HTML. Please note that the default presets (Basic/Standard/Full) do not include this plugin, so you are only at risk if you made a custom build and enabled this plugin. + +We would like to thank the [Drupal security team](https://www.drupal.org/drupal-security-team) for bringing this matter to our attention and coordinating the fix and release process! + +## CKEditor 4.9.1 + +Fixed Issues: + +* [#1835](https://github.com/ckeditor/ckeditor-dev/issues/1835): Fixed: Integration between [CKFinder](https://ckeditor.com/ckeditor-4/ckfinder/) and the [File Browser](https://ckeditor.com/cke4/addon/filebrowser) plugin does not work. + +## CKEditor 4.9 + +New Features: + +* [#932](https://github.com/ckeditor/ckeditor-dev/issues/932): Introduced Easy Image feature for inserting images that are automatically rescaled, optimized, responsive and delivered through a blazing-fast CDN. Three new plugins were added to support it: + * [Easy Image](https://ckeditor.com/cke4/addon/easyimage), + * [Cloud Services](https://ckeditor.com/cke4/addon/cloudservices) + * [Image Base](https://ckeditor.com/cke4/addon/imagebase) +* [#1338](https://github.com/ckeditor/ckeditor-dev/issues/1338): Keystroke labels are displayed for function keys (like F7, F8). +* [#643](https://github.com/ckeditor/ckeditor-dev/issues/643): The [File Browser](https://ckeditor.com/cke4/addon/filebrowser) plugin can now upload files using XHR requests. This allows for setting custom HTTP headers using the [`config.fileTools_requestHeaders`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-fileTools_requestHeaders) configuration option. +* [#1365](https://github.com/ckeditor/ckeditor-dev/issues/1365): The [File Browser](https://ckeditor.com/cke4/addon/filebrowser) plugin uses XHR requests by default. +* [#1399](https://github.com/ckeditor/ckeditor-dev/issues/1399): Added the possibility to set [`CKEDITOR.config.startupFocus`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-startupFocus) as `start` or `end` to specify where the editor focus should be after the initialization. +* [#1441](https://github.com/ckeditor/ckeditor-dev/issues/1441): The [Magic Line](https://ckeditor.com/cke4/addon/magicline) plugin line element can now be identified by the `data-cke-magic-line="1"` attribute. + +Fixed Issues: + +* [#595](https://github.com/ckeditor/ckeditor-dev/issues/595): Fixed: Pasting does not work on mobile devices. +* [#869](https://github.com/ckeditor/ckeditor-dev/issues/869): Fixed: Empty selection clears cached clipboard data in the editor. +* [#1419](https://github.com/ckeditor/ckeditor-dev/issues/1419): Fixed: The [Widget Selection](https://ckeditor.com/cke4/addon/widgetselection) plugin selects the editor content with the Alt+A key combination on Windows. +* [#1274](https://github.com/ckeditor/ckeditor-dev/issues/1274): Fixed: [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) does not match a single selected image using the [`contextDefinition.cssSelector`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_plugins_balloontoolbar_contextDefinition.html#property-cssSelector) matcher. +* [#1232](https://github.com/ckeditor/ckeditor-dev/issues/1232): Fixed: [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) buttons should be registered as focusable elements. +* [#1342](https://github.com/ckeditor/ckeditor-dev/issues/1342): Fixed: [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) should be re-positioned after the [`change`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change) event. +* [#1426](https://github.com/ckeditor/ckeditor-dev/issues/1426): [IE8-9] Fixed: Missing [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) background in the [Kama](https://ckeditor.com/cke4/addon/kama) skin. Thanks to [Christian Elmer](https://github.com/keinkurt)! +* [#1470](https://github.com/ckeditor/ckeditor-dev/issues/1470): Fixed: [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) is not visible after drag and drop of a widget it is attached to. +* [#1048](https://github.com/ckeditor/ckeditor-dev/issues/1048): Fixed: [Balloon Panel](https://ckeditor.com/cke4/addon/balloonpanel) is not positioned properly when a margin is added to its non-static parent. +* [#889](https://github.com/ckeditor/ckeditor-dev/issues/889): Fixed: Unclear error message for width and height fields in the [Image](https://ckeditor.com/cke4/addon/image) and [Enhanced Image](https://ckeditor.com/cke4/addon/image2) plugins. +* [#859](https://github.com/ckeditor/ckeditor-dev/issues/859): Fixed: Cannot edit a link after a double-click on the text in the link. +* [#1013](https://github.com/ckeditor/ckeditor-dev/issues/1013): Fixed: [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) does not work correctly with the [`config.forcePasteAsPlainText`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-forcePasteAsPlainText) option. +* [#1356](https://github.com/ckeditor/ckeditor-dev/issues/1356): Fixed: [Border parse function](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools_style_parse.html#method-border) does not allow spaces in the color value. +* [#1010](https://github.com/ckeditor/ckeditor-dev/issues/1010): Fixed: The CSS `border` shorthand property was incorrectly expanded ignoring the `border-color` style. +* [#1535](https://github.com/ckeditor/ckeditor-dev/issues/1535): Fixed: [Widget](https://ckeditor.com/cke4/addon/widget) mouseover border contrast is insufficient. +* [#1516](https://github.com/ckeditor/ckeditor-dev/issues/1516): Fixed: Fake selection allows removing content in read-only mode using the Backspace and Delete keys. +* [#1570](https://github.com/ckeditor/ckeditor-dev/issues/1570): Fixed: Fake selection allows cutting content in read-only mode using the Ctrl/Cmd + X keys. +* [#1363](https://github.com/ckeditor/ckeditor-dev/issues/1363): Fixed: Paste notification is unclear and it might confuse users. + +API Changes: + +* [#1346](https://github.com/ckeditor/ckeditor-dev/issues/1346): [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) [context manager API](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.plugins.balloontoolbar.contextManager.html) is now available in the [`pluginDefinition.init()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_pluginDefinition.html#method-init) method of the [requiring](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_pluginDefinition.html#property-requires) plugin. +* [#1530](https://github.com/ckeditor/ckeditor-dev/issues/1530): Added the possibility to use custom icons for [buttons](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_ui_button.html.html). + +Other Changes: + +* Updated [SCAYT](https://ckeditor.com/cke4/addon/scayt) (Spell Check As You Type) and [WebSpellChecker](https://ckeditor.com/cke4/addon/wsc) (WSC) plugins: + * SCAYT [`scayt_minWordLength`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#scayt_minWordLength) configuration option now defaults to 3 instead of 4. + * SCAYT default number of suggested words in the context menu changed to 3. + * [#90](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/90): Fixed: Selection is lost on link creation if SCAYT highlights the word. + * Fixed: SCAYT crashes when the browser `localStorage` is disabled. + * [IE11] Fixed: `Unable to get property type of undefined or null reference` error in the browser console when SCAYT is disabled/enabled. + * [#46](https://github.com/WebSpellChecker/ckeditor-plugin-wsc/issues/46): Fixed: Editing is blocked when remote spell checker server is offline. + * Fixed: User Dictionary cannot be created in WSC due to `You already have the dictionary` error. + * Fixed: Words with apostrophe `'` on the replacement make the WSC dialog inaccessible. + * Fixed: SCAYT/WSC causes the `Uncaught TypeError` error in the browser console. +* [#1337](https://github.com/ckeditor/ckeditor-dev/issues/1337): Updated the samples layout with the new CKEditor 4 logo and color scheme. +* [#1591](https://github.com/ckeditor/ckeditor-dev/issues/1591): CKBuilder and language tools are now downloaded over HTTPS. Thanks to [August Detlefsen](https://github.com/augustd)! + +## CKEditor 4.8 + +**Important Notes:** + +* [#1249](https://github.com/ckeditor/ckeditor-dev/issues/1249): Enabled the [Upload Image](https://ckeditor.com/cke4/addon/uploadimage) plugin by default in standard and full presets. Also, it will no longer log an error in case of missing [`config.imageUploadUrl`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-imageUploadUrl) property. + +New Features: + +* [#933](https://github.com/ckeditor/ckeditor-dev/issues/933): Introduced [Balloon Toolbar](https://ckeditor.com/cke4/addon/balloontoolbar) plugin. +* [#662](https://github.com/ckeditor/ckeditor-dev/issues/662): Introduced image inlining for the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#468](https://github.com/ckeditor/ckeditor-dev/issues/468): [Edge] Introduced support for the Clipboard API. +* [#607](https://github.com/ckeditor/ckeditor-dev/issues/607): Manually inserted Hex color is prefixed with a hash character (`#`) if needed. It ensures a valid Hex color value is used when setting the table cell border or background color with the [Color Dialog](https://ckeditor.com/cke4/addon/colordialog) window. +* [#584](https://github.com/ckeditor/ckeditor-dev/issues/584): [Font size and Family](https://ckeditor.com/cke4/addon/font) and [Format](https://ckeditor.com/cke4/addon/format) drop-downs are not toggleable anymore. Default option to reset styles added. +* [#856](https://github.com/ckeditor/ckeditor-dev/issues/856): Introduced the [`CKEDITOR.tools.keystrokeToArray()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools.html#method-keystrokeToArray) method. It converts a keystroke into its string representation, returning every key name as a separate array element. +* [#1053](https://github.com/ckeditor/ckeditor-dev/issues/1053): Introduced the [`CKEDITOR.tools.object.merge()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools_object.html#method-merge) method. It allows to merge two objects, returning the new object with all properties from both objects deeply cloned. +* [#1073](https://github.com/ckeditor/ckeditor-dev/issues/1073): Introduced the [`CKEDITOR.tools.array.every()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_tools_array.html#method-every) method. It invokes a given test function on every array element and returns `true` if all elements pass the test. + +Fixed Issues: + +* [#796](https://github.com/ckeditor/ckeditor-dev/issues/796): Fixed: A list is pasted from OneNote in the reversed order. +* [#834](https://github.com/ckeditor/ckeditor-dev/issues/834): [IE9-11] Fixed: The editor does not save the selected state of radio buttons inserted by the [Form Elements](https://ckeditor.com/cke4/addon/forms) plugin. +* [#704](https://github.com/ckeditor/ckeditor-dev/issues/704): [Edge] Fixed: Using Ctrl/Cmd + Z breaks widget structure. +* [#591](https://github.com/ckeditor/ckeditor-dev/issues/591): Fixed: A column is inserted in a wrong order inside the table if any cell has a vertical split. +* [#787](https://github.com/ckeditor/ckeditor-dev/issues/787): Fixed: Using Cut inside a nested table does not cut the selected content. +* [#842](https://github.com/ckeditor/ckeditor-dev/issues/842): Fixed: List style not restored when toggling list indent level in the [Indent List](https://ckeditor.com/cke4/addon/indentlist) plugin. +* [#711](https://github.com/ckeditor/ckeditor-dev/issues/711): Fixed: Dragging widgets should only work with the left mouse button. +* [#862](https://github.com/ckeditor/ckeditor-dev/issues/862): Fixed: The "Object Styles" group in the [Styles Combo](https://ckeditor.com/cke4/addon/stylescombo) plugin is visible only if the whole element is selected. +* [#994](https://github.com/ckeditor/ckeditor-dev/pull/994): Fixed: Typo in the [`CKEDITOR.focusManager.focus()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_focusManager.html#method-focus) API documentation. Thanks to [benjy](https://github.com/benjy)! +* [#1014](https://github.com/ckeditor/ckeditor-dev/issues/1014): Fixed: The [Table Tools](https://ckeditor.com/cke4/addon/tabletools) Cell Properties dialog is now [Advanced Content Filter](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_acf.html) aware — it is not possible to change the cell width or height if corresponding styles are disabled. +* [#877](https://github.com/ckeditor/ckeditor-dev/issues/877): Fixed: A list with custom bullets with exotic characters crashes the editor when [pasted from Word](https://ckeditor.com/cke4/addon/pastefromword). +* [#605](https://github.com/ckeditor/ckeditor-dev/issues/605): Fixed: Inline widgets do not preserve trailing spaces. +* [#1008](https://github.com/ckeditor/ckeditor-dev/issues/1008): Fixed: Shorthand Hex colors from the [`config.colorButton_colors`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-colorButton_colors) option are not correctly highlighted in the [Color Button](https://ckeditor.com/cke4/addon/colorbutton) Text Color or Background Color panel. +* [#1094](https://github.com/ckeditor/ckeditor-dev/issues/1094): Fixed: Widget definition [`upcast`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_plugins_widget_definition.html#property-upcasts) methods are called for every element. +* [#1057](https://github.com/ckeditor/ckeditor-dev/issues/1057): Fixed: The [Notification](https://ckeditor.com/addon/notification) plugin overwrites Web Notifications API due to leakage to the global scope. +* [#1068](https://github.com/ckeditor/ckeditor-dev/issues/1068): Fixed: Upload widget paste listener ignores changes to the [`uploadWidgetDefinition`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.fileTools.uploadWidgetDefinition.html). +* [#921](https://github.com/ckeditor/ckeditor-dev/issues/921): Fixed: [Edge] CKEditor erroneously perceives internal copy and paste as type "external". +* [#1213](https://github.com/ckeditor/ckeditor-dev/issues/1213): Fixed: Multiple images uploaded using [Upload Image](https://ckeditor.com/cke4/addon/uploadimage) plugin are randomly duplicated or mangled. +* [#532](https://github.com/ckeditor/ckeditor-dev/issues/532): Fixed: Removed an outdated user guide link from the [About](https://ckeditor.com/cke4/addon/about) dialog. +* [#1221](https://github.com/ckeditor/ckeditor-dev/issues/1221): Fixed: Invalid CSS loaded by [Balloon Panel](https://ckeditor.com/cke4/addon/balloonpanel) plugin when [`config.skin`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-skin) is loaded using a custom path. +* [#522](https://github.com/ckeditor/ckeditor-dev/issues/522): Fixed: Widget selection is not removed when widget is inside table cell with [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin enabled. +* [#1027](https://github.com/ckeditor/ckeditor-dev/issues/1027): Fixed: Cannot add multiple images to the table with [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin in certain situations. +* [#1069](https://github.com/ckeditor/ckeditor-dev/issues/1069): Fixed: Wrong shape processing by [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#995](https://github.com/ckeditor/ckeditor-dev/issues/995): Fixed: Hyperlinked image gets inserted twice by [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#1287](https://github.com/ckeditor/ckeditor-dev/issues/1287): Fixed: [Widget](https://ckeditor.com/cke4/addon/widget) plugin throws exception if included in editor build but not loaded into editor's instance. + +API Changes: + +* [#1097](https://github.com/ckeditor/ckeditor-dev/issues/1097): Widget [`upcast`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_plugins_widget_definition.html#property-upcast) methods are now called in the [widget definition's](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_plugins_widget.html#property-definition) context. +* [#1118](https://github.com/ckeditor/ckeditor-dev/issues/1118): Added the `show` option in the [`balloonPanel.attach()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_ui_balloonPanel.html#method-attach) method, allowing to attach a hidden [Balloon Panel](https://ckeditor.com/cke4/addon/balloonpanel) instance. +* [#1145](https://github.com/ckeditor/ckeditor-dev/issues/1145): Added the [`skipNotifications`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_fileTools_uploadWidgetDefinition.html#property-skipNotifications) option to the [`CKEDITOR.fileTools.uploadWidgetDefinition`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.fileTools.uploadWidgetDefinition.html), allowing to switch off default notifications displayed by upload widgets. + +Other Changes: + +* [#815](https://github.com/ckeditor/ckeditor-dev/issues/815): Removed Node.js dependency from the CKEditor build script. +* [#1041](https://github.com/ckeditor/ckeditor-dev/pull/1041), [#1131](https://github.com/ckeditor/ckeditor-dev/issues/1131): Updated URLs pointing to [CKSource](https://cksource.com/) and [CKEditor](https://ckeditor.com/) resources after the launch of new websites. + +## CKEditor 4.7.3 + +New Features: + +* [#568](https://github.com/ckeditor/ckeditor-dev/issues/568): Added possibility to adjust nested editables' filters using the [`CKEDITOR.filter.disallowedContent`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_filter.html#property-disallowedContent) property. + +Fixed Issues: + +* [#554](https://github.com/ckeditor/ckeditor-dev/issues/554): Fixed: [`change`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change) event not fired when typing the first character after pasting into the editor. Thanks to [Daniel Miller](https://github.com/millerdev)! +* [#566](https://github.com/ckeditor/ckeditor-dev/issues/566): Fixed: The CSS `border` shorthand property with zero width (`border: 0px solid #000;`) causes the table to have the border attribute set to 1. +* [#779](https://github.com/ckeditor/ckeditor-dev/issues/779): Fixed: The [Remove Format](https://ckeditor.com/cke4/addon/removeformat) plugin removes elements with language definition inserted by the [Language](https://ckeditor.com/cke4/addon/language) plugin. +* [#423](https://github.com/ckeditor/ckeditor-dev/issues/423): Fixed: The [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin pastes paragraphs into the editor even if [`CKEDITOR.config.enterMode`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-enterMode) is set to `CKEDITOR.ENTER_BR`. +* [#719](https://github.com/ckeditor/ckeditor-dev/issues/719): Fixed: Image inserted using the [Enhanced Image](https://ckeditor.com/cke4/addon/image2) plugin can be resized when the editor is in [read-only mode](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_readonly.html). +* [#577](https://github.com/ckeditor/ckeditor-dev/issues/577): Fixed: The "Delete Columns" command provided by the [Table Tools](https://ckeditor.com/cke4/addon/tabletools) plugin throws an error when trying to delete columns. +* [#867](https://github.com/ckeditor/ckeditor-dev/issues/867): Fixed: Typing into a selected table throws an error. +* [#817](https://github.com/ckeditor/ckeditor-dev/issues/817): Fixed: The [Save](https://ckeditor.com/cke4/addon/save) plugin does not work in [Source Mode](https://ckeditor.com/cke4/addon/sourcearea). + +Other Changes: + +* Updated the [WebSpellChecker](https://ckeditor.com/cke4/addon/wsc) plugin: + * [#40](https://github.com/WebSpellChecker/ckeditor-plugin-wsc/issues/40): Fixed: IE10 throws an error when spell checking is started. +* [#800](https://github.com/ckeditor/ckeditor-dev/issues/800): Added the [`CKEDITOR.dom.selection.isCollapsed()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_selection.html#method-isCollapsed) method which is a simpler way to check if the selection is collapsed. +* [#830](https://github.com/ckeditor/ckeditor-dev/issues/830): Added an option to define which dialog tab should be shown by default when creating [`CKEDITOR.dialogCommand`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.dialogCommand.html). + +## CKEditor 4.7.2 + +New Features: + +* [#455](https://github.com/ckeditor/ckeditor-dev/issues/455): Added [Advanced Content Filter](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_acf.html) integration with the [Justify](https://ckeditor.com/cke4/addon/justify) plugin. + +Fixed Issues: + +* [#663](https://github.com/ckeditor/ckeditor-dev/issues/663): [Chrome] Fixed: Clicking the scrollbar throws an `Uncaught TypeError: element.is is not a function` error. +* [#694](https://github.com/ckeditor/ckeditor-dev/pull/694): Refactoring in the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin: + * [#520](https://github.com/ckeditor/ckeditor-dev/issues/520): Fixed: Widgets cannot be properly pasted into a table cell. + * [#460](https://github.com/ckeditor/ckeditor-dev/issues/460): Fixed: Editor gone after pasting into an editor within a table. +* [#579](https://github.com/ckeditor/ckeditor-dev/issues/579): Fixed: Internal `cke_table-faked-selection-table` class is visible in the Stylesheet Classes field of the [Table Properties](https://ckeditor.com/cke4/addon/table) dialog. +* [#545](https://github.com/ckeditor/ckeditor-dev/issues/545): [Edge] Fixed: Error thrown when pressing the [Select All](https://ckeditor.com/cke4/addon/selectall) button in [Source Mode](https://ckeditor.com/cke4/addon/sourcearea). +* [#582](https://github.com/ckeditor/ckeditor-dev/issues/582): Fixed: Double slash in the path to stylesheet needed by the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin. Thanks to [Marius Dumitru Florea](https://github.com/mflorea)! +* [#491](https://github.com/ckeditor/ckeditor-dev/issues/491): Fixed: Unnecessary dependency on the [Editor Toolbar](https://ckeditor.com/cke4/addon/toolbar) plugin inside the [Notification](https://ckeditor.com/cke4/addon/notification) plugin. +* [#646](https://github.com/ckeditor/ckeditor-dev/issues/646): Fixed: Error thrown into the browser console after opening the [Styles Combo](https://ckeditor.com/cke4/addon/stylescombo) plugin menu in the editor without any selection. +* [#501](https://github.com/ckeditor/ckeditor-dev/issues/501): Fixed: Double click does not open the dialog for modifying anchors inserted via the [Link](https://ckeditor.com/cke4/addon/link) plugin. +* [#9780](https://dev.ckeditor.com/ticket/9780): [IE8-9] Fixed: Clicking inside an empty [read-only](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#property-readOnly) editor throws an error. +* [#16820](https://dev.ckeditor.com/ticket/16820): [IE10] Fixed: Clicking below a single horizontal rule throws an error. +* [#426](https://github.com/ckeditor/ckeditor-dev/issues/426): Fixed: The [`range.cloneContents()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_range.html#method-cloneContents) method selects the whole element when the selection starts at the beginning of that element. +* [#644](https://github.com/ckeditor/ckeditor-dev/issues/644): Fixed: The [`range.extractContents()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_range.html#method-extractContents) method returns an incorrect result when multiple nodes are selected. +* [#684](https://github.com/ckeditor/ckeditor-dev/issues/684): Fixed: The [`elementPath.contains()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_elementPath.html#method-contains) method incorrectly excludes the last element instead of root when the `fromTop` parameter is set to `true`. + +Other Changes: + +* Updated the [SCAYT](https://ckeditor.com/cke4/addon/scayt) (Spell Check As You Type) plugin: + * [#148](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/148): Fixed: SCAYT leaves underlined word after the CKEditor Replace dialog corrects it. +* [#751](https://github.com/ckeditor/ckeditor-dev/issues/751): Added the [`CKEDITOR.dom.nodeList.toArray()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_nodeList.html#method-toArray) method which returns an array representation of a [node list](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.dom.nodeList.html). + +## CKEditor 4.7.1 + +New Features: + +* Added a new Mexican Spanish localization. Thanks to [David Alexandro Rodriguez](https://www.transifex.com/user/profile/darsco16/)! +* [#413](https://github.com/ckeditor/ckeditor-dev/issues/413): Added Paste as Plain Text keyboard shortcut to the [Accessibility Help](https://ckeditor.com/cke4/addon/a11yhelp) instructions. + +Fixed Issues: + +* [#515](https://github.com/ckeditor/ckeditor-dev/issues/515): [Chrome] Fixed: Mouse actions on CKEditor scrollbar throw an exception when the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin is loaded. +* [#493](https://github.com/ckeditor/ckeditor-dev/issues/493): Fixed: Selection started from a nested table causes an error in the browser while scrolling down. +* [#415](https://github.com/ckeditor/ckeditor-dev/issues/415): [Firefox] Fixed: Enter key breaks the table structure when pressed in a table selection. +* [#457](https://github.com/ckeditor/ckeditor-dev/issues/457): Fixed: Error thrown when deleting content from the editor with no selection. +* [#478](https://github.com/ckeditor/ckeditor-dev/issues/478): [Chrome] Fixed: Error thrown by the [Enter Key](https://ckeditor.com/cke4/addon/enterkey) plugin when pressing Enter with no selection. +* [#424](https://github.com/ckeditor/ckeditor-dev/issues/424): Fixed: Error thrown by [Tab Key Handling](https://ckeditor.com/cke4/addon/tab) and [Indent List](https://ckeditor.com/cke4/addon/indentlist) plugins when pressing Tab with no selection in inline editor. +* [#476](https://github.com/ckeditor/ckeditor-dev/issues/476): Fixed: Anchors inserted with the [Link](https://ckeditor.com/cke4/addon/link) plugin on collapsed selection cannot be edited. +* [#417](https://github.com/ckeditor/ckeditor-dev/issues/417): Fixed: The [Table Resize](https://ckeditor.com/cke4/addon/tableresize) plugin throws an error when used with a table with only header or footer rows. +* [#523](https://github.com/ckeditor/ckeditor-dev/issues/523): Fixed: The [`editor.getCommandKeystroke()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-getCommandKeystroke) method does not obtain the correct keystroke. +* [#534](https://github.com/ckeditor/ckeditor-dev/issues/534): [IE] Fixed: [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) does not work in Quirks Mode. +* [#450](https://github.com/ckeditor/ckeditor-dev/issues/450): Fixed: [`CKEDITOR.filter`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.filter.html) incorrectly transforms the `margin` CSS property. + +## CKEditor 4.7 + +**Important Notes:** + +* [#13793](https://dev.ckeditor.com/ticket/13793): The [`embed_provider`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-embed_provider) configuration option for the [Media Embed](https://ckeditor.com/cke4/addon/embed) and [Semantic Media Embed](https://ckeditor.com/cke4/addon/embedsemantic) plugins is no longer preset by default. +* The [UI Color](https://ckeditor.com/cke4/addon/uicolor) plugin now uses a custom color picker instead of the `YUI 2.7.0` library which has some known vulnerabilities (it's a security precaution, there was no security issue in CKEditor due to the way it was used). + +New Features: + +* [#16755](https://dev.ckeditor.com/ticket/16755): Added the [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin that lets you select and manipulate an arbitrary rectangular table fragment (a few cells, a row or a column). +* [#16961](https://dev.ckeditor.com/ticket/16961): Added support for pasting from Microsoft Excel. +* [#13381](https://dev.ckeditor.com/ticket/13381): Dynamic code evaluation call in [`CKEDITOR.template`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.template.html) removed. CKEditor can now be used without the `unsafe-eval` Content Security Policy. Thanks to [Caridy Patiño](http://caridy.name)! +* [#16971](https://dev.ckeditor.com/ticket/16971): Added support for color in the `background` property containing also other styles for table cells in the [Table Tools](https://ckeditor.com/cke4/addon/tabletools) plugin. +* [#16847](https://dev.ckeditor.com/ticket/16847): Added support for parsing and inlining any formatting created using the Microsoft Word style system to the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#16818](https://dev.ckeditor.com/ticket/16818): Added table cell height parsing in the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#16850](https://dev.ckeditor.com/ticket/16850): Added a new [`config.enableContextMenu`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-enableContextMenu) configuration option for enabling and disabling the [context menu](https://ckeditor.com/cke4/addon/contextmenu). +* [#16937](https://dev.ckeditor.com/ticket/16937): The `command` parameter in [`CKEDITOR.editor.getCommandKeystroke()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-getCommandKeystroke) now also accepts a command name as an argument. +* [#17010](https://dev.ckeditor.com/ticket/17010): The [`CKEDITOR.dom.range.shrink()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_range.html#method-shrink) method now allows for skipping bogus `
      ` elements. + +Fixed Issues: + +* [#16935](https://dev.ckeditor.com/ticket/16935): [Chrome] Fixed: Blurring the editor in [Source Mode](https://ckeditor.com/cke4/addon/sourcearea) throws an error. +* [#16825](https://dev.ckeditor.com/ticket/16825): [Chrome] Fixed: Error thrown when destroying a focused inline editor. +* [#16857](https://dev.ckeditor.com/ticket/16857): Fixed: Ctrl+Shift+V blocked by [Copy Formatting](https://ckeditor.com/cke4/addon/copyformatting). +* [#16845](https://dev.ckeditor.com/ticket/16845): [IE] Fixed: Cursor jumps to the top of the scrolled editor after focusing it when the [Copy Formatting](https://ckeditor.com/cke4/addon/copyformatting) plugin is enabled. +* [#16786](https://dev.ckeditor.com/ticket/16786): Fixed: Added missing translations for the [Copy Formatting](https://ckeditor.com/cke4/addon/copyformatting) plugin. +* [#14714](https://dev.ckeditor.com/ticket/14714): [WebKit/Blink] Fixed: Exception thrown on refocusing a blurred inline editor. +* [#16913](https://dev.ckeditor.com/ticket/16913): [Firefox, IE] Fixed: [Paste as Plain Text](https://ckeditor.com/cke4/addon/pastetext) keystroke does not work. +* [#16968](https://dev.ckeditor.com/ticket/16968): Fixed: [Safari] [Paste as Plain Text](https://ckeditor.com/cke4/addon/pastetext) is not handled by the editor. +* [#16912](https://dev.ckeditor.com/ticket/16912): Fixed: Exception thrown when a single image is pasted using [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword). +* [#16821](https://dev.ckeditor.com/ticket/16821): Fixed: Extraneous `` elements with `height` style stacked when [pasting from Word](https://ckeditor.com/cke4/addon/pastefromword). +* [#16866](https://dev.ckeditor.com/ticket/16866): [IE, Edge] Fixed: Whitespaces not preserved when [pasting from Word](https://ckeditor.com/cke4/addon/pastefromword). +* [#16860](https://dev.ckeditor.com/ticket/16860): Fixed: Paragraphs which only look like lists incorrectly transformed into them when [pasting from Word](https://ckeditor.com/cke4/addon/pastefromword). +* [#16817](https://dev.ckeditor.com/ticket/16817): Fixed: When [pasting from Word](https://ckeditor.com/cke4/addon/pastefromword), paragraphs are transformed into lists with some corrupted data. +* [#16833](https://dev.ckeditor.com/ticket/16833): [IE11] Fixed: Malformed list with headers [pasted from Word](https://ckeditor.com/cke4/addon/pastefromword). +* [#16826](https://dev.ckeditor.com/ticket/16826): [IE] Fixed: Superfluous paragraphs within lists [pasted from Word](https://ckeditor.com/cke4/addon/pastefromword). +* [#12465](https://dev.ckeditor.com/ticket/12465): Fixed: Cannot change the state of checkboxes or radio buttons if the properties dialog was invoked with a double-click. +* [#13062](https://dev.ckeditor.com/ticket/13062): Fixed: Impossible to unlink when the caret is at the edge of the link. +* [#13585](https://dev.ckeditor.com/ticket/13585): Fixed: Error when wrapping two adjacent `
      ` elements with a `
      `. +* [#16811](https://dev.ckeditor.com/ticket/16811): Fixed: Table alignment is not preserved by the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#16810](https://dev.ckeditor.com/ticket/16810): Fixed: Vertical align in tables is not supported by the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin. +* [#11956](https://dev.ckeditor.com/ticket/11956): [Blink, IE] Fixed: [Link](https://ckeditor.com/cke4/addon/link) dialog does not open on a double click on the second word of the link with a background color or other styles. +* [#10472](https://dev.ckeditor.com/ticket/10472): Fixed: Unable to use [Table Resize](https://ckeditor.com/cke4/addon/tableresize) on table header and footer. +* [#14762](https://dev.ckeditor.com/ticket/14762): Fixed: Hovering over an empty table (without rows or cells) throws an error when the [Table Resize](https://ckeditor.com/cke4/addon/tableresize) plugin is active. +* [#16777](https://dev.ckeditor.com/ticket/16777): [Edge] Fixed: The [Clipboard](https://ckeditor.com/cke4/addon/clipboard) plugin does not allow to drop widgets into the editor. +* [#14894](https://dev.ckeditor.com/ticket/14894): [Chrome] Fixed: The editor scrolls to the top after focusing or when a dialog is opened. +* [#14769](https://dev.ckeditor.com/ticket/14769): Fixed: URLs with '-' in host are not detected by the [Auto Link](https://ckeditor.com/cke4/addon/autolink) plugin. +* [#16804](https://dev.ckeditor.com/ticket/16804): Fixed: Focus is not on the first menu item when the user opens a context menu or a drop-down list from the editor toolbar. +* [#14407](https://dev.ckeditor.com/ticket/14407): [IE] Fixed: Non-editable widgets can be edited. +* [#16927](https://dev.ckeditor.com/ticket/16927): Fixed: An error thrown if a bundle containing the [Color Button](https://ckeditor.com/cke4/addon/colorbutton) plugin is run in ES5 strict mode. Thanks to [Igor Rubinovich](https://github.com/IgorRubinovich)! +* [#16920](https://dev.ckeditor.com/ticket/16920): Fixed: Several plugins not using the [Dialog](https://ckeditor.com/cke4/addon/dialog) plugin as a direct dependency. +* [PR#336](https://github.com/ckeditor/ckeditor-dev/pull/336): Fixed: Typo in [`CKEDITOR.getCss()`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#method-getCss) API documentation. Thanks to [knusperpixel](https://github.com/knusperpixel)! +* [#17027](https://dev.ckeditor.com/ticket/17027): Fixed: Command event data should be initialized as an empty object. +* Fixed the behavior of HTML parser when parsing `src`/`srcdoc` attributes of the `\n\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t
      \n\t\t\t\t\t
      \n\t\t\t\t\t
      \n\t\t\t\t\t\t
      \n\t\t\t\t\t\t\tApp Sort\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
      \n\t\t\t\t\t
      \n\t\t\t\t\n\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t
      Upgrade to Pro edition, all plugins, free to use!
      \n\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t
      Recently visited plugin:
      \n\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t
      \n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t'),window.usePay=dC,$("#soft-main").append("\n\t\t\t'); + if (aceEditor.editor !== null) { + if (aceEditor.isAceView == false) { + aceEditor.isAceView = true; + $('.aceEditors .layui-layer-max').click(); + } + aceEditor.openEditorView(path); + return false; + } + var r = layer.open({ + type: 1, + maxmin: true, + shade: false, + area: ['80%', '80%'], + title: lan.public.online_text_editor, + skin: 'aceEditors', + zIndex: 19999, + content: _aceTmplate, + success: function (layero, index) { + function set_edit_file() { + // aceEditor.layer_view = index; + aceEditor.ace_active = ''; + aceEditor.eventEditor(); + $('#ace_conter').addClass(aceEditor.editorTheme); + ace.require('/ace/ext/language_tools'); + ace.config.set('modePath', '/static/ace'); + ace.config.set('workerPath', '/static/ace'); + ace.config.set('themePath', '/static/ace'); + aceEditor.openEditorView(path); + var _left = parseInt($(layero).css('left')), + _top = parseInt($(layero).css('top')); + _left < 0 ? $(layero).css('left', Math.abs(_left)) : $(layero).css('left', _left); + _top < 0 ? $(layero).css('top', Math.abs(_top)) : $(layero).css('top', _top); + // $('.aceEditors .layui-layer-min').click(function(e) { + // aceEditor.isAceView = false; + // setTimeout(function() { + // var _id = $('.ace_conter_menu .active').attr('data-id'); + // aceEditor.editor['ace_editor_' + _id].ace.resize(); + // }, 105); + // }); + // $('.aceEditors .layui-layer-max').click(function(e) { + // setTimeout(function() { + // aceEditor.setEditorView(); + // var _id = $('.ace_conter_menu .active').attr('data-id'); + // aceEditor.editor['ace_editor_' + _id].ace.resize(); + // }, 105); + // }); + $('.aceEditors .layui-layer-min').click(function (e) { + aceEditor.setEditorView(); + }); + $('.aceEditors .layui-layer-max').click(function (e) { + aceEditor.setEditorView(); + }); + } + var aceConfig = aceEditor.getStorage('aceConfig'); + if (aceConfig == null) { + // 获取编辑器配置 + aceEditor.getAceConfig(function (res) { + aceEditor.aceConfig = res; // 赋值配置参数 + set_edit_file(); + }); + } else { + aceEditor.aceConfig = JSON.parse(aceConfig); + typeof aceEditor.aceConfig == 'string' ? (aceEditor.aceConfig = JSON.parse(aceEditor.aceConfig)) : ''; + set_edit_file(); + } + }, + cancel: function () { + for (var item in aceEditor.editor) { + if (aceEditor.editor[item].fileType == 1) { + layer.open({ + type: 1, + area: ['400px', '180px'], + title: lan.public.save_tips, + content: + '\ +
      \ +
      \ +
      ' + + lan.public.save_tips1 + + '
      \ +
      ' + + lan.public.save_tips2 + + '
      \ +
      \ + \ + \ + \ +
      \ +
      ', + success: function (layers, indexs) { + $('.ace-clear-btn button').click(function () { + var _type = $(this).attr('data-type'); + switch (_type) { + case '2': + aceEditor.editor = null; + layer.closeAll(); + break; + case '1': + layer.close(indexs); + break; + case '0': + var _arry = [], + editor = aceEditor['editor']; + for (var item in editor) { + _arry.push({ + path: editor[item]['path'], + data: editor[item]['ace'].getValue(), + encoding: editor[item]['encoding'], + }); + } + aceEditor.saveAllFileBody(_arry, function () { + $('.ace_conter_menu>.item').each(function (el, indexx) { + var _id = $(this).attr('data-id'); + $(this).find('i').removeClass('glyphicon-exclamation-sign').addClass('glyphicon-remove').attr('data-file-state', '0'); + aceEditor.editor['ace_editor_' + _id].fileType = 0; + }); + aceEditor.editor = null; + aceEditor.pathAarry = []; + layer.closeAll(); + }); + break; + } + }); + }, + }); + return false; + } + } + }, + full: function (layero, index) { + //最大化 + aceEditor.editorStatus = 1; + }, + min: function (layero, index) { + //最小化 + aceEditor.editorStatus = -1; + }, + restore: function (layero, index) { + //还原 + aceEditor.editorStatus = 0; + }, + end: function () { + aceEditor.ace_active = ''; + aceEditor.editor = null; + aceEditor.pathAarry = []; + aceEditor.menu_path = ''; + }, + }); +} + +/** + * AES加密 + * @param {string} s_text 等待加密的字符串 + * @param {string} s_key 16位密钥 + * @param {array} ctx 可选,默认为 { mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.ZeroPadding } + * @return {string} + */ +function aes_encrypt(s_text, s_key, ctx) { + if (ctx == undefined) ctx = { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.ZeroPadding }; + var key = CryptoJS.enc.Utf8.parse(s_key); + var encrypt_data = CryptoJS.AES.encrypt(s_text, key, ctx); + return encrypt_data.toString(); +} + +/** + * AES解密 + * @param {string} s_text 等待解密的密文 + * @param {string} s_key 16位密钥 + * @param {array} ctx 可选,默认为 { mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.ZeroPadding } + * @return {string} + */ +function aes_decrypt(s_text, s_key, ctx) { + if (ctx == undefined) ctx = { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.ZeroPadding }; + var key = CryptoJS.enc.Utf8.parse(s_key); + var decrypt_data = CryptoJS.AES.decrypt(s_text, key, ctx); + return decrypt_data.toString(CryptoJS.enc.Utf8); +} + +/** + * ajax内容解密 + * @param {string} data 加密的响应数据 + * @param {string} stype ajax中定义的数据类型 + * @return {string} 解密后的响应数据 + */ +function ajax_decrypt(data, stype) { + if (!data) return data; + if (data.substring(0, 6) == 'BT-CRT') { + var token = $('#request_token_head').attr('token'); + var pwd = token.substring(0, 8) + token.substring(40, 48); + data = aes_decrypt(data.substring(6), pwd); + if (stype == undefined) { + stype = ''; + } + if (stype.toLowerCase() != 'json') { + data = JSON.parse(data); + } + } + return data; +} +/** + * 格式化form_data数据,并加密 + * @param {string} form_data 加密前的form_data数据 + * @return {string} 加密后的form_data数据 + */ +function format_form_data(form_data) { + var data_tmp = form_data.split('&'); + var form_info = {}; + var token = $('#request_token_head').attr('token'); + if (!token) return form_data; + var pwd = token.substring(0, 8) + token.substring(40, 48); + for (var i = 0; i < data_tmp.length; i++) { + var tmp = data_tmp[i].split('='); + if (tmp.length < 2) continue; + // if(!tmp[1]) continue; + var val = decodeURIComponent(tmp[1].replace(/\+/g, '%20')); + if (val.length > 3) { + form_info[tmp[0]] = 'BT-CRT' + aes_encrypt(val, pwd); + } else { + form_info[tmp[0]] = val; + } + } + return $.param(form_info); +} + +function ajax_encrypt(request) { + if (!this.type || !this.data || !this.contentType) return; + if ($('#panel_debug').attr('data') == 'True') return; + if ($('#panel_debug').attr('data-pyversion') == '2') return; + if (this.type == 'POST' && this.data.length > 1) { + this.data = format_form_data(this.data); + } +} + +// function ajaxSetup() { +// var my_headers = {}; +// var request_token_ele = document.getElementById("request_token_head"); +// if (request_token_ele) { +// var request_token = request_token_ele.getAttribute('token'); +// if (request_token) { +// my_headers['x-http-token'] = request_token +// } +// } +// request_token_cookie = getCookie('request_token'); +// if (request_token_cookie) { +// my_headers['x-cookie-token'] = request_token_cookie +// } +// +// if (my_headers) { +// $.ajaxSetup({ +// headers: my_headers, +// // dataFilter: ajax_decrypt, +// // beforeSend: ajax_encrypt +// }); +// } +// } +function ajaxSetup() { + var my_headers = {}; + var request_token_ele = document.getElementById('request_token_head'); + if (request_token_ele) { + var request_token = request_token_ele.getAttribute('token'); + if (request_token) { + my_headers['x-http-token'] = request_token; + } + } + request_token_cookie = getCookie('request_token'); + if (request_token_cookie) { + my_headers['x-cookie-token'] = request_token_cookie; + } + + if (my_headers) { + $.ajaxSetup({ + headers: my_headers, + error: function (jqXHR, textStatus, errorThrown) { + if (!jqXHR.responseText) return; + if (typeof String.prototype.trim === 'undefined') { + String.prototype.trim = function () { + return String(this).replace(/^\s+|\s+$/g, ''); + }; + } + + error_key = 'We need to make sure this has a favicon so that the debugger does'; + error_find = jqXHR.responseText.indexOf(error_key); + if (jqXHR.status == 500 && (jqXHR.responseText.indexOf('An error occurred while the panel was running') != -1 || error_find != -1)) { + // if(jqXHR.responseText.indexOf('请先绑定宝塔帐号!') != -1){ + // bt.pub.bind_btname(function(){ + // window.location.reload(); + // }); + // return; + // } + if (error_find != -1) { + var error_body = jqXHR.responseText.split('', ''); + var tmp = error_body.split('During handling of the above exception, another exception occurred:'); + error_body = tmp[tmp.length - 1]; + var error_msg = + '
      \ +

      出错了,面板运行时发生错误!

      \ +
      ' +
      +							error_body.trim() +
      +							'
      \ +
        \ +
      • 很抱歉,面板运行时意外发生错误,请尝试按以下顺序尝试解除此错误:
      • \ +
      • 1、在[首页]右上角点击修复面板,并退出面板重新登录。
      • \ +
      • 2、如上述尝试未能解除此错误,请截图此窗口到宝塔论坛发贴寻求帮助, 论坛地址:https://www.bt.cn/bbs
      • \ +
      \ +
      '; + } else { + var error_msg = jqXHR.responseText; + } + $('.layui-layer-padding').parents('.layer-anim').remove(); + $('.layui-layer-shade').remove(); + setTimeout(function () { + layer.open({ + title: false, + content: error_msg, + closeBtn: 2, + area: ['1000px', '800px'], + btn: false, + shadeClose: false, + shade: 0.3, + success: function () { + $('pre').scrollTop(100000000000); + }, + }); + }, 100); + } + }, + // dataFilter: ajax_decrypt, + // beforeSend: ajax_encrypt + }); + } +} +ajaxSetup(); + +function RandomStrPwd(b) { + b = b || 32; + var c = 'AaBbCcDdEeFfGHhiJjKkLMmNnPpRSrTsWtXwYxZyz2345678'; + var a = c.length; + var d = ''; + for (i = 0; i < b; i++) { + d += c.charAt(Math.floor(Math.random() * a)); + } + return d; +} + +function repeatPwd(a) { + $('#MyPassword').val(RandomStrPwd(a)); +} + +function refresh() { + window.location.reload(); +} + +function GetBakPost(b) { + $('.baktext').hide().prev().show(); + var c = $('.baktext').attr('data-id'); + var a = $('.baktext').val(); + if (a == '') { + a = lan.bt.empty; + } + setWebPs(b, c, a); + $("a[data-id='" + c + "']").html(a); + $('.baktext').remove(); +} + +function setWebPs(b, e, a) { + var d = layer.load({ + shade: true, + shadeClose: false, + }); + var c = 'ps=' + a; + $.post('/data?action=setPs', 'table=' + b + '&id=' + e + '&' + c, function (f) { + if (f == true) { + if (b == 'sites') { + getWeb(1); + } else { + if (b == 'ftps') { + getFtp(1); + } else { + getData(1); + } + } + layer.closeAll(); + layer.msg(lan.public.edit_ok, { + icon: 1, + }); + } else { + layer.msg(lan.public.edit_err, { + icon: 2, + }); + layer.closeAll(); + } + }); +} + +$('.menu-icon').click(function () { + $('.sidebar-scroll').toggleClass('sidebar-close'); + $('.main-content').toggleClass('main-content-open'); + if ($('.sidebar-close')) { + $('.sub-menu').find('.sub').css('display', 'none'); + } +}); +var Upload, percentage; + +Date.prototype.format = function (b) { + var c = { + 'M+': this.getMonth() + 1, + 'd+': this.getDate(), + 'h+': this.getHours(), + 'm+': this.getMinutes(), + 's+': this.getSeconds(), + 'q+': Math.floor((this.getMonth() + 3) / 3), + S: this.getMilliseconds(), + }; + if (/(y+)/.test(b)) { + b = b.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length)); + } + for (var a in c) { + if (new RegExp('(' + a + ')').test(b)) { + b = b.replace(RegExp.$1, RegExp.$1.length == 1 ? c[a] : ('00' + c[a]).substr(('' + c[a]).length)); + } + } + return b; +}; + +function getLocalTime(a) { + a = a.toString(); + if (a.length > 10) { + a = a.substring(0, 10); + } + return new Date(parseInt(a) * 1000).format('yyyy/MM/dd hh:mm:ss'); +} + +function ToSize(a) { + var d = [' B', ' KB', ' MB', ' GB', ' TB', ' PB']; + var e = 1024; + for (var b = 0; b < d.length; b++) { + if (a < e) { + return (b == 0 ? a : a.toFixed(2)) + d[b]; + } + a /= e; + } +} + +function ChangePath(d) { + setCookie('SetId', d); + setCookie('SetName', ''); + var c = layer.open({ + type: 1, + area: '680px', + title: lan.bt.dir, + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "
      " + + lan.bt.path + + ":
      " + + lan.bt.comp + + "
        " + + lan.bt.filename + + "" + + lan.bt.etime + + "" + + lan.bt.access + + "" + + lan.bt.own + + "
        ', + }); + setCookie('ChangePath', c); + var b = $('#' + d).val(); + tmp = b.split('.'); + if (tmp[tmp.length - 1] == 'gz') { + tmp = b.split('/'); + b = ''; + for (var a = 0; a < tmp.length - 1; a++) { + b += '/' + tmp[a]; + } + setCookie('SetName', tmp[tmp.length - 1]); + } + b = b.replace(/\/\//g, '/'); + GetDiskList(b); + ActiveDisk(); +} + +function GetDiskList(b) { + var d = ''; + var a = ''; + var c = 'path=' + b + '&disk=True'; + $.post('/files?action=GetDir', c, function (h) { + if (h.status == false) { + layer.close(layer.index); + layer.msg(h.msg, { icon: 2 }); + return false; + } + if (h.DISK != undefined) { + for (var f = 0; f < h.DISK.length; f++) { + a += '
         " + h.DISK[f].path + '
        '; + } + $('#changecomlist').html(a); + } + for (var f = 0; f < h.DIR.length; f++) { + var g = h.DIR[f].split(';'); + var e = g[0]; + if (e.length > 20) { + e = e.substring(0, 20) + '...'; + } + if (isChineseChar(e)) { + if (e.length > 10) { + e = e.substring(0, 10) + '...'; + } + } + d += + '" + + e + + '' + + getLocalTime(g[2]) + + '' + + g[3] + + '' + + g[4] + + "X'; + } + if (h.FILES != null && h.FILES != '') { + for (var f = 0; f < h.FILES.length; f++) { + var g = h.FILES[f].split(';'); + var e = g[0]; + if (e.length > 20) { + e = e.substring(0, 20) + '...'; + } + if (isChineseChar(e)) { + if (e.length > 10) { + e = e.substring(0, 10) + '...'; + } + } + d += "" + e + '' + getLocalTime(g[2]) + '' + g[3] + '' + g[4] + ''; + } + } + $('.default').hide(); + $('.file-list').show(); + $('#tbody').html(d); + if (h.PATH.substr(h.PATH.length - 1, 1) != '/') { + h.PATH += '/'; + } + $('#PathPlace').find('span').html(h.PATH); + ActiveDisk(); + return; + }); +} + +function CreateFolder() { + var a = + "   '; + if ($('#tbody tr').length == 0) { + $('#tbody').append(a); + } else { + $('#tbody tr:first-child').before(a); + } + $('.newFolderName').focus(); + $('#nameOk').click(function () { + var c = $('#newFolderName').val(); + var b = $('#PathPlace').find('span').text(); + newTxt = b.replace(new RegExp(/(\/\/)/g), '/') + c; + var d = 'path=' + newTxt; + $.post('/files?action=CreateDir', d, function (e) { + if (e.status == true) { + layer.msg(e.msg, { + icon: 1, + }); + } else { + layer.msg(e.msg, { + icon: 2, + }); + } + GetDiskList(b); + }); + }); + $('#nameNOk').click(function () { + $(this).parents('tr').remove(); + }); +} + +function NewDelFile(c) { + var a = $('#PathPlace').find('span').text(); + newTxt = c.replace(new RegExp(/(\/\/)/g), '/'); + var b = 'path=' + newTxt + '&empty=True'; + $.post('/files?action=DeleteDir', b, function (d) { + if (d.status == true) { + layer.msg(d.msg, { + icon: 1, + }); + } else { + layer.msg(d.msg, { + icon: 2, + }); + } + GetDiskList(a); + }); +} + +function ActiveDisk() { + var a = $('#PathPlace').find('span').text().substring(0, 1); + switch (a) { + case 'C': + $('.path-con-left dd:nth-of-type(1)').css('background', '#eee').siblings().removeAttr('style'); + break; + case 'D': + $('.path-con-left dd:nth-of-type(2)').css('background', '#eee').siblings().removeAttr('style'); + break; + case 'E': + $('.path-con-left dd:nth-of-type(3)').css('background', '#eee').siblings().removeAttr('style'); + break; + case 'F': + $('.path-con-left dd:nth-of-type(4)').css('background', '#eee').siblings().removeAttr('style'); + break; + case 'G': + $('.path-con-left dd:nth-of-type(5)').css('background', '#eee').siblings().removeAttr('style'); + break; + case 'H': + $('.path-con-left dd:nth-of-type(6)').css('background', '#eee').siblings().removeAttr('style'); + break; + default: + $('.path-con-left dd').removeAttr('style'); + } +} + +function BackMyComputer() { + $('.default').show(); + $('.file-list').hide(); + $('#PathPlace').find('span').html(''); + ActiveDisk(); +} + +function BackFile() { + var c = $('#PathPlace').find('span').text(); + if (c.substr(c.length - 1, 1) == '/') { + c = c.substr(0, c.length - 1); + } + var d = c.split('/'); + var a = ''; + if (d.length > 1) { + var e = d.length - 1; + for (var b = 0; b < e; b++) { + a += d[b] + '/'; + } + GetDiskList(a.replace('//', '/')); + } else { + a = d[0]; + } + if (d.length == 1) { + } +} + +function GetfilePath() { + var a = $('#PathPlace').find('span').text(); + a = a.replace(new RegExp(/(\\)/g), '/'); + setCookie('path_dir_change', a); + $('#' + getCookie('SetId')).val(a + getCookie('SetName')); + layer.close(getCookie('ChangePath')); +} + +function setCookie(a, c) { + var b = 30; + var d = new Date(); + d.setTime(d.getTime() + b * 24 * 60 * 60 * 1000); + document.cookie = a + '=' + escape(c) + ';expires=' + d.toGMTString(); +} + +function getCookie(b) { + var a, + c = new RegExp('(^| )' + b + '=([^;]*)(;|$)'); + if ((a = document.cookie.match(c))) { + return unescape(a[2]); + } else { + return null; + } +} + +function aotuHeight() { + var a = $('body').height() - 52 - 32; + $('.main-content').css('min-height', a); +} +$(function () { + aotuHeight(); +}); +$(window).resize(function () { + aotuHeight(); +}); + +function showHidePwd() { + var a = 'glyphicon-eye-open', + b = 'glyphicon-eye-close'; + $('.pw-ico').click(function () { + var g = $(this).attr('class'), + e = $(this).prev(); + if (g.indexOf(a) > 0) { + var h = e.attr('data-pw'); + $(this).removeClass(a).addClass(b); + e.text(h); + } else { + $(this).removeClass(b).addClass(a); + e.text('**********'); + } + var d = $(this).next().position().left; + var f = $(this).next().position().top; + var c = $(this).next().width(); + $(this) + .next() + .next() + .css({ + left: d + c + 'px', + top: f + 'px', + }); + }); +} + +function openPath(a) { + setCookie('Path', a); + window.location.href = '/files'; +} + +function OnlineEditFile(k, f) { + if (k != 0) { + var l = $('#PathPlace input').val(); + var h = encodeURIComponent($('#textBody').val()); + var a = $('select[name=encoding]').val(); + var loadT = layer.msg(lan.bt.save_file, { + icon: 16, + time: 0, + }); + $.post('/files?action=SaveFileBody', 'data=' + h + '&path=' + encodeURIComponent(f) + '&encoding=' + a, function (m) { + if (k == 1) { + layer.close(loadT); + } + layer.msg(m.msg, { + icon: m.status ? 1 : 2, + }); + }); + return; + } + var e = layer.msg(lan.bt.read_file, { + icon: 16, + time: 0, + }); + var g = f.split('.'); + var b = g[g.length - 1]; + var d; + switch (b) { + case 'html': + var j = { + name: 'htmlmixed', + scriptTypes: [ + { + matches: /\/x-handlebars-template|\/x-mustache/i, + mode: null, + }, + { + matches: /(text|application)\/(x-)?vb(a|script)/i, + mode: 'vbscript', + }, + ], + }; + d = j; + break; + case 'htm': + var j = { + name: 'htmlmixed', + scriptTypes: [ + { + matches: /\/x-handlebars-template|\/x-mustache/i, + mode: null, + }, + { + matches: /(text|application)\/(x-)?vb(a|script)/i, + mode: 'vbscript', + }, + ], + }; + d = j; + break; + case 'js': + d = 'text/javascript'; + break; + case 'json': + d = 'application/ld+json'; + break; + case 'css': + d = 'text/css'; + break; + case 'php': + d = 'application/x-httpd-php'; + break; + case 'tpl': + d = 'application/x-httpd-php'; + break; + case 'xml': + d = 'application/xml'; + break; + case 'sql': + d = 'text/x-sql'; + break; + case 'conf': + d = 'text/x-nginx-conf'; + break; + default: + var j = { + name: 'htmlmixed', + scriptTypes: [ + { + matches: /\/x-handlebars-template|\/x-mustache/i, + mode: null, + }, + { + matches: /(text|application)\/(x-)?vb(a|script)/i, + mode: 'vbscript', + }, + ], + }; + d = j; + } + $.post('/files?action=GetFileBody', 'path=' + encodeURIComponent(f), function (s) { + if (s.status === false) { + layer.msg(s.msg, { icon: 5 }); + return; + } + layer.close(e); + var u = ['utf-8', 'GBK', 'GB2312', 'BIG5']; + var n = ''; + var m = ''; + var o = ''; + for (var p = 0; p < u.length; p++) { + m = s.encoding == u[p] ? 'selected' : ''; + n += ''; + } + var r = layer.open({ + type: 1, + shift: 5, + closeBtn: 2, + area: ['90%', '90%'], + title: lan.bt.edit_title + '[' + f + ']', + content: + '

        ' + + lan.bt.edit_ps + + '

        '; + $('.taskcon').html(lbody); + var ob = document.getElementById('exec_log'); + ob.scrollTop = ob.scrollHeight; + }); +} + +function get_msg_data(a, fun) { + a = a == undefined ? 1 : a; + $.post('/data?action=getData', 'tojs=remind&table=tasks&result=2,4,6,8&limit=10&search=1&p=' + a, function (g) { + fun(g); + }); +} + +function remind(a) { + get_msg_data(a, function (g) { + var e = ''; + var f = false; + var task_count = 0; + for (var d = 0; d < g.data.length; d++) { + if (g.data[d].status != '1') { + task_count++; + continue; + } + e += + '
        ' + + g.data[d].name + + g.data[d].addtime + + '【' + + lan.bt.task_ok + + '】' + + lan.bt.time + + (g.data[d].end - g.data[d].start) + + lan.bt.s + + '
        ' + + g.data[d].addtime + + ''; + } + var con = + '
        \ + \ + ' + + e + + '\ +
        ' + + lan.bt.task_name + + '' + + lan.bt.task_time + + '
        \ +
        \ + \ +
        \ +
        '; + + var msg_count = g.page.match(/\'Pcount\'>.+<\/span>/)[0].replace(/[^0-9]/gi, ''); + $('.msg_count').text(parseInt(msg_count) - task_count); + $('.taskcon').html(con); + $('#taskPage').html(g.page); + $('#Rs-checkAll').click(function () { + if ($(this).prop('checked')) { + $('#remind').find('input').prop('checked', true); + } else { + $('#remind').find('input').prop('checked', false); + } + }); + }); +} + +function GetReloads() { + var a = 0; + var mm = $('#taskList').html(); + if (mm == undefined || mm.indexOf(lan.bt.task_list) == -1) { + clearInterval(speed); + a = 0; + speed = null; + return; + } + if (speed) return; + speed = setInterval(function () { + var mm = $('#taskList').html(); + if (mm == undefined || mm.indexOf(lan.bt.task_list) == -1) { + clearInterval(speed); + speed = null; + a = 0; + return; + } + a++; + $.post('/files?action=GetTaskSpeed', '', function (h) { + if (h.task == undefined) { + $('.cmdlist').html(lan.bt.task_not_list); + return; + } + + if (h.status === false) { + clearInterval(speed); + speed = null; + a = 0; + return; + } + + var b = ''; + var d = ''; + $('#task').text(h.task.length); + $('.task_count').text(h.task.length); + for (var g = 0; g < h.task.length; g++) { + if (h.task[g].status == '-1') { + if (h.task[g].type != 'download') { + var c = ''; + var f = h.msg.split('\n'); + for (var e = 0; e < f.length; e++) { + c += f[e] + '
        '; + } + if (h.task[g].name.indexOf(lan.public.scan) != -1) { + b = + "
      • " + + h.task[g].name + + "" + + lan.bt.task_scan + + " | ' + + lan.public.close + + "
        " + + c + + '
      • '; + } else { + b = + "
      • " + + h.task[g].name + + "" + + lan.bt.task_install + + " | ' + + lan.public.close + + "
        " + + c + + '
      • '; + } + } else { + b = + "
      • " + + h.task[g].name + + "" + + (ToSize(h.msg.used) + '/' + ToSize(h.msg.total)) + + "" + + h.msg.pre + + "%" + + lan.bt.task_downloading + + " | ' + + lan.public.close + + '
      • '; + } + } else { + d += + "
      • " + + h.task[g].name + + "" + + lan.bt.task_sleep + + " | ' + + lan.public.del + + '
      • '; + } + } + $('.cmdlist').html(b + d); + $('.cmd').html(c); + try { + if ($('.cmd')[0].scrollHeight) $('.cmd').scrollTop($('.cmd')[0].scrollHeight); + } catch (e) { + return; + } + }).error(function () {}); + }, 1000); +} + +//检查选中项 +function RscheckSelect() { + setTimeout(function () { + var checkList = $('#remind').find('input'); + var count = 0; + for (var i = 0; i < checkList.length; i++) { + if (checkList[i].checked) count++; + } + if (count > 0) { + $('.buttongroup .btn').removeAttr('disabled'); + } else { + $('.rs-del,.rs-read').attr('disabled', 'disabled'); + } + }, 5); +} + +function tasklist(a) { + var con = '
          ' + lan.public.task_long_time_not_exec + ''; + $('.taskcon').html(con); + a = a == undefined ? 1 : a; + $.post('/data?action=getData', 'tojs=GetTaskList&table=tasks&limit=10&p=' + a, function (g) { + var e = ''; + var b = ''; + var c = ''; + var f = false; + var task_count = 0; + for (var d = 0; d < g.data.length; d++) { + switch (g.data[d].status) { + case '-1': + f = true; + if (g.data[d].type != 'download') { + b = + "
        • " + + g.data[d].name + + "" + + lan.bt.task_install + + " | ' + + lan.public.close + + "
        • "; + } else { + b = + "
        • " + + g.data[d].name + + "0.0M/12.5M0%" + + lan.bt.task_downloading + + " | ' + + lan.public.close + + '
        • '; + } + task_count++; + break; + case '0': + c += + "
        • " + + g.data[d].name + + "" + + lan.bt.task_sleep + + ' | " + + lan.public.del + + '
        • '; + task_count++; + break; + } + } + + $('.task_count').text(task_count); + + get_msg_data(1, function (d) { + var msg_count = d.page.match(/\'Pcount\'>.+<\/span>/)[0].replace(/[^0-9]/gi, ''); + $('.msg_count').text(parseInt(msg_count)); + }); + + $('.cmdlist').html(b + c); + GetReloads(); + return f; + }); +} + +//检查登陆状态 +function check_login() { + $.post('/ajax?action=CheckLogin', {}, function (rdata) { + if (rdata === true) return; + }); +} + +//登陆跳转 +function to_login() { + layer.confirm(lan.public.login_expire, { title: lan.public.session_expire, icon: 2, closeBtn: 1, shift: 5 }, function () { + location.reload(); + }); +} +//表格头固定 +function table_fixed(name) { + var tableName = document.querySelector('#' + name); + tableName.addEventListener('scroll', scroll_handle); +} + +function scroll_handle(e) { + var scrollTop = this.scrollTop; + $(this) + .find('thead') + .css({ transform: 'translateY(' + scrollTop + 'px)', position: 'relative', 'z-index': '1' }); +} +var clipboard, interval, socket, term, ssh_login, term_box; + +var pdata_socket = { + x_http_token: document.getElementById('request_token_head').getAttribute('token'), +}; + +var Term = { + bws: null, //websocket对象 + route: '/webssh', //被访问的方法 + term: null, + term_box: null, + ssh_info: {}, + last_body: false, + last_cd: null, + config: { + cols: 0, + rows: 0, + fontSize: 12, + }, + + // 缩放尺寸 + detectZoom: (function () { + var ratio = 0, + screen = window.screen, + ua = navigator.userAgent.toLowerCase(); + if (window.devicePixelRatio !== undefined) { + ratio = window.devicePixelRatio; + } else if (~ua.indexOf('msie')) { + if (screen.deviceXDPI && screen.logicalXDPI) { + ratio = screen.deviceXDPI / screen.logicalXDPI; + } + } else if (window.outerWidth !== undefined && window.innerWidth !== undefined) { + ratio = window.outerWidth / window.innerWidth; + } + + if (ratio) { + ratio = Math.round(ratio * 100); + } + return ratio; + })(), + //连接websocket + connect: function () { + if (!Term.bws || Term.bws.readyState == 3 || Term.bws.readyState == 2) { + //连接 + ws_url = (window.location.protocol === 'http:' ? 'ws://' : 'wss://') + window.location.host + Term.route; + + Term.bws = new WebSocket(ws_url); + + //绑定事件 + Term.bws.addEventListener('message', Term.on_message); + Term.bws.addEventListener('close', Term.on_close); + Term.bws.addEventListener('error', Term.on_error); + Term.bws.addEventListener('open', Term.on_open); + + //if (Term.ssh_info) Term.send(JSON.stringify(Term.ssh_info)) + } + }, + //连接服务器成功 + on_open: function (ws_event) { + var http_token = $('#request_token_head').attr('token'); + Term.send(JSON.stringify({ 'x-http-token': http_token })); + if (JSON.stringify(Term.ssh_info) !== '{}') Term.send(JSON.stringify(Term.ssh_info)); + // Term.term.FitAddon.fit(); + // Term.resize(); + // var f_path = $("#fileInputPath").val(); + var f_path = $('#fileInputPath').attr('data-path'); + if (f_path) { + Term.last_cd = 'cd ' + f_path; + Term.send(Term.last_cd + '\n'); + } + }, + + //服务器消息事件 + // on_message: function(ws_event) { + // result = ws_event.data; + // if (result === "\r'Server connection failed'!\r" || result === "\rWrong user name or password!\r") { + // show_ssh_login(result); + // Term.close(); + // return; + // } + // Term.term.write(result); + + // if (result == '\r\n登出\r\n' || result == '登出\r\n' || result == '\r\nlogout\r\n' || result == 'logout\r\n') { + // setTimeout(function() { + // layer.close(Term.term_box); + // }, 500); + // Term.close(); + // Term.bws = null; + // } + // }, + on_message: function (ws_event) { + result = ws_event.data; + if ((result.indexOf('@127.0.0.1:') != -1 || result.indexOf('@localhost:') != -1) && result.indexOf('Authentication failed') != -1) { + Term.term.write(result); + Term.localhost_login_form(result); + Term.close(); + return; + } + if (Term.last_cd) { + if (result.indexOf(Term.last_cd) != -1 && result.length - Term.last_cd.length < 3) { + Term.last_cd = null; + return; + } + } + if (result === '\rServer connection failed!\r' || result == '\rWrong user name or password!\r') { + Term.close(); + return; + } + if (result.length > 1 && Term.last_body === false) { + Term.last_body = true; + } + Term.term.write(result); + if (result == '\r\n登出\r\n' || result == '\r\n注销\r\n' || result == '注销\r\n' || result == '登出\r\n' || result == '\r\nlogout\r\n' || result == 'logout\r\n') { + setTimeout(function () { + layer.close(Term.term_box); + Term.term.dispose(); + }, 500); + Term.close(); + Term.bws = null; + } + }, + //websocket关闭事件 + on_close: function (ws_event) { + Term.bws = null; + }, + + //websocket错误事件 + // on_error: function(ws_event) { + // if(ws_event.target.readyState === 3){ + // var msg = 'Error: unable to create websocket connection, please close 【Developer mode】 on the settings page'; + // layer.msg(msg,{time:5000}) + // if(Term.state === 3) return + // Term.term.write(msg) + // Term.state = 3; + // }else{ + // console.log(ws_event) + // } + // }, + on_error: function (ws_event) { + if (ws_event.target.readyState === 3) { + if (Term.state === 3) return; + Term.term.write(msg); + Term.state = 3; + } else { + console.log(ws_event); + } + }, + + //关闭连接 + close: function () { + if (Term.bws) { + Term.bws.close(); + } + }, + + resize: function () { + setTimeout(function () { + $('#term').height($('.term_box_all .layui-layer-content').height() - 18); + Term.term.FitAddon.fit(); + Term.send(JSON.stringify({ resize: 1, rows: Term.term.rows, cols: Term.term.cols })); + Term.term.focus(); + }, 100); + }, + // resize: function() { + // var m_width = 100; + // var m_height = 34; + // Term.term.resize(m_width, m_height); + // Term.term.scrollToBottom(); + // Term.term.focus(); + // Term.send('new_terminal'); + // }, + + //发送数据 + //@param event 唯一事件名称 + //@param data 发送的数据 + //@param collback 服务器返回结果时回调的函数,运行完后将被回收 + send: function (data, num) { + //如果没有连接,则尝试连接服务器 + if (!Term.bws || Term.bws.readyState == 3 || Term.bws.readyState == 2) { + Term.connect(); + } + + //判断当前连接状态,如果!=1,则100ms后尝试重新发送 + if (Term.bws.readyState === 1) { + Term.bws.send(data); + } else { + if (Term.state === 3) return; + if (!num) num = 0; + if (num < 5) { + num++; + setTimeout(function () { + Term.send(data, num++); + }, 100); + } + } + }, + // run: function (ssh_info) { + // var termCols = 100; + // var termRows = 34; + // var loadT = layer.msg('It is loading the files required by the terminal. Please wait...', { icon: 16, time: 0, shade: 0.3 }); + // loadScript([ + // "/static/build/xterm.min.js?v=1721298337096", + // "/static/build/addons/attach/attach.min.js?v=1721298337096", + // "/static/build/addons/fit/fit.min.js?v=1721298337096", + // "/static/build/addons/fullscreen/fullscreen.min.js?v=1721298337096", + // "/static/build/addons/search/search.min.js?v=1721298337096", + // "/static/build/addons/winptyCompat/winptyCompat.js?v=1721298337096" + // ], function () { + // layer.close(loadT); + // Term.term = new Terminal({ cols: termCols, rows: termRows, screenKeys: true, useStyle: true }); + // Term.term.setOption('cursorBlink', true); + // Term.term_box = layer.open({ + // type: 1, + // title: lan.public.terminal, + // area: ['920px', '630px'], + // closeBtn: 2, + // shadeClose: false, + // content: '\ + // \ + // [' + lan.public.set + ']\ + //
          ', + // cancel: function () { + // Term.term.destroy(); + // }, + // success: function () { + // Term.term.open(document.getElementById('term')); + // Term.resize(); + // } + // }); + // Term.term.on('data', function (data) { + // try { + // Term.bws.send(data) + // } catch (e) { + // Term.term.write('\r\nThe connection is lost and you are trying to reconnect!\r\n') + // Term.connect() + // } + // }); + // if (ssh_info) Term.ssh_info = ssh_info + // Term.connect(); + // }) + + // }, + run: function (ssh_info) { + // if($("#panel_debug").attr("data") == 'True') { + // layer.msg('Error: unable to create websocket connection, please close 【Developer mode】 on the settings page!',{icon:2,time:5000}); + // return; + // } + var loadT = layer.msg('It is loading the files required by the terminal. Please wait...', { icon: 16, time: 0, shade: 0.3 }); + loadScript(['/static/js/xterm.js?v=1721298337096'], function () { + layer.close(loadT); + Term.term = new Terminal({ + rendererType: 'canvas', + cols: 100, + rows: 31, + fontSize: 15, + screenKeys: true, + useStyle: true, + }); + Term.term.setOption('cursorBlink', true); + Term.last_body = false; + Term.term_box = layer.open({ + type: 1, + title: lan.public.terminal, + area: ['925px', '630px'], + closeBtn: 2, + shadeClose: false, + skin: 'term_box_all', + content: + '\ +
          ', + cancel: function (index, lay) { + bt.confirm( + { + msg: '
          Closing the SSH session, the command in progress in the current command line session may be aborted. Continute?
          ', + title: 'Cofirm to close the SSH session?', + }, + function (ix) { + Term.term.dispose(); + layer.close(index); + layer.close(ix); + Term.close(); + } + ); + return false; + }, + success: function () { + $('.term_box_all').css('background-color', '#000'); + Term.term.open(document.getElementById('term')); + Term.term.FitAddon = new FitAddon.FitAddon(); + Term.term.loadAddon(Term.term.FitAddon); + Term.term.WebLinksAddon = new WebLinksAddon.WebLinksAddon(); + Term.term.loadAddon(Term.term.WebLinksAddon); + Term.term.focus(); + }, + }); + Term.term.onData(function (data) { + try { + Term.bws.send(data); + } catch (e) { + Term.term.write('\r\nThe connection is lost and you are trying to reconnect!\r\n'); + Term.connect(); + } + }); + if (ssh_info) Term.ssh_info = ssh_info; + Term.connect(); + }); + }, + reset_login: function () { + var ssh_info = { + data: JSON.stringify({ + host: $("input[name='host']").val(), + port: $("input[name='port']").val(), + username: $("input[name='username']").val(), + password: $("input[name='password']").val(), + }), + }; + $.post('/term_open', ssh_info, function (rdata) { + if (rdata.status === false) { + layer.msg(rdata.msg); + return; + } + layer.closeAll(); + Term.connect(); + Term.term.scrollToBottom(); + Term.term.focus(); + }); + }, + localhost_login_form: function (result) { + var template = + '
          Login failed, please fill the local server information!
          \ +
          \ + Server IP\ +
          \ + \ + \ +
          \ +
          \ +
          \ + SSH account\ +
          \ + \ +
          \ +
          \ +
          \ + Verification\ +
          \ +
          \ + \ + \ +
          \ +
          \ +
          \ +
          \ + Password\ +
          \ + \ +
          \ +
          \ +
          '; + $('.term-box').after(template); + $('.auth_type_checkbox').click(function () { + var index = $(this).index(); + $(this).addClass('btn-success').removeClass('btn-default').siblings().removeClass('btn-success').addClass('btn-default'); + switch (index) { + case 0: + $('.c_password_view').addClass('show').removeClass('hidden'); + $('.c_pkey_view').addClass('hidden').removeClass('show').find('input').val(''); + break; + case 1: + $('.c_password_view').addClass('hidden').removeClass('show').find('input').val(''); + $('.c_pkey_view').addClass('show').removeClass('hidden'); + break; + } + }); + $('.localhost-form-view > button').click(function () { + var form = {}; + $('.localhost-form-view input,.localhost-form-view textarea').each(function (index, el) { + var name = $(this).attr('name'), + value = $(this).val(); + form[name] = value; + switch (name) { + case 'port': + if (!bt.check_port(value)) { + bt.msg({ status: false, msg: 'Server port format error!' }); + return false; + } + break; + case 'username': + if (value == '') { + bt.msg({ status: false, msg: 'Server user name cannot be empty!' }); + return false; + } + break; + case 'password': + if (value == '' && $('.c_password_view').hasClass('show')) { + bt.msg({ status: false, msg: 'Server password cannot be empty!' }); + return false; + } + break; + case 'pkey': + if (value == '' && $('.c_pkey_view').hasClass('show')) { + bt.msg({ status: false, msg: 'The server key cannot be empty!' }); + return false; + } + break; + } + }); + form.ps = 'Local server'; + + if (result) { + if (result.indexOf('@127.0.0.1') != -1) { + var user = result.split('@')[0].split(',')[1]; + var port = result.split('1:')[1]; + $("input[name='username']").val(user); + $("input[name='port']").val(port); + } + } + var loadT = bt.load('Adding server information, please wait...'); + bt.send('create_host', 'xterm/create_host', form, function (res) { + loadT.close(); + bt.msg(res); + if (res.status) { + bt.msg({ status: true, msg: 'Login successful!' }); + $('.layui-layer-shade').remove(); + $('.term_box_all').remove(); + Term.term.dispose(); + Term.close(); + web_shell(); + } + }); + }); + $('.localhost-form-view [name="password"]') + .keyup(function (e) { + if (e.keyCode == 13) { + $('.localhost-form-view > button').click(); + } + }) + .focus(); + }, +}; + +function web_shell() { + Term.run(); +} + +socket = { + emit: function (data, data2) { + if (data === 'webssh') { + data = data2; + } + if (typeof data === 'object') { + return; + } + Term.send(data); + }, +}; + +function show_ssh_login(is_config) { + if ($("input[name='ssh_user']").attr('autocomplete')) return; + var s_body = + '
          \ + \ +
          IP
          \ +
          Port
          \ +
          Username
          \ +
          Method
          \ +
          Password
          \ + \ +
          \ +

          Only support login to this server

          \ +
          '; + ssh_login = layer.open({ + type: 1, + title: is_config ? 'Please fill in the SSH connection configuration' : 'Please enter the SSH login account and password', + area: '500px', + closeBtn: 0, + shadeClose: false, + content: s_body, + }); + + setTimeout(function removeReadonly() { + $("input[name='ssh_user']").removeAttr('readonly'); + $("input[name='ssh_passwd']").removeAttr('readonly'); + $("input[name='ssh_passwd']").focus(); + + $("input[name='ssh_passwd']").keydown(function (e) { + if (e.keyCode == 13) { + $('.ssh-login').click(); + } + }); + }, 500); +} + +function pass_check() { + $('#pass_check').attr('class', 'ssh_check_s2'); + $('#rsa_check').attr('class', 'ssh_check_s1'); + $('.ssh_pkey').hide(); + $('.ssh_passwd').show(); +} + +function rsa_check() { + $('#pass_check').attr('class', 'ssh_check_s1'); + $('#rsa_check').attr('class', 'ssh_check_s2'); + $('.ssh_pkey').show(); + $('.ssh_passwd').hide(); +} + +function send_ssh_info() { + pdata = { + host: $("input[name='ssh_host']").val(), + port: Number($("input[name='ssh_port']").val()), + password: $("input[name='ssh_passwd']").val(), + username: $("input[name='ssh_user']").val(), + pkey: $("textarea[name='ssh_pkey']").val(), + }; + if (pdata['host'] !== '127.0.0.1' && pdata['host'] !== 'localhost') { + layer.msg('Connection address can only be [ 127.0.0.1 or localhost ]'); + $("input[name='ssh_host']").focus(); + return; + } + if (pdata['port'] < 1 || pdata['port'] > 65535) { + layer.msg('Port range is incorrect [1-65535]'); + $("input[name='ssh_port']").focus(); + return; + } + if (!pdata['username']) { + layer.msg('Username can not be empty!'); + $("input[name='ssh_user']").focus(); + return; + } + + if ($('#rsa_check').attr('class') === 'ssh_check_s2') { + pdata['c_type'] = 'True'; + if (!pdata['pkey']) { + layer.msg('Private key cannot be empty!'); + $("input[name='ssh_pkey']").focus(); + return; + } + } else { + if (!pdata['password']) { + layer.msg('Password can not be blank!'); + $("input[name='ssh_passwd']").focus(); + return; + } + } + if ($('#ssh_is_save').prop('checked')) { + pdata['is_save'] = '1'; + } + + var loadT = layer.msg('Trying to log in to SSH...', { icon: 16, time: 0, shade: 0.3 }); + $.post('/term_open', { data: JSON.stringify(pdata) }, function () { + layer.close(loadT); + Term.send('reset_connect'); + layer.close(ssh_login); + Term.term.focus(); + }); +} + +acme = { + speed_msg: "
          [MSG]
          ", + loadT: null, + //获取订单列表 + get_orders: function (callback) { + acme.request( + 'get_orders', + {}, + function (rdata) { + callback(rdata); + }, + 'Getting order list...' + ); + }, + //取指定订单 + get_find: function (index, callback) { + acme.request( + 'get_order_find', + { index: index }, + function (rdata) { + callback(rdata); + }, + 'Getting order information...' + ); + }, + + //下载指定证书包 + download_cert: function (index, callback) { + acme.request( + 'update_zip', + { index: index }, + function (rdata) { + if (!rdata.status) { + bt.msg(rdata); + return; + } + if (callback) { + callback(rdata); + } else { + window.location.href = '/download?filename=' + rdata.msg; + } + }, + 'Preparing to download..' + ); + }, + + //删除订单 + remove: function (index, callback) { + acme.request('remove_order', { index: index }, function (rdata) { + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + + //吊销证书 + revoke: function (index, callback) { + acme.request( + 'revoke_order', + { index: index }, + function (rdata) { + bt.msg(rdata); + if (callback) callback(rdata); + }, + 'Revoking certificate...' + ); + }, + + //验证域名(手动DNS申请) + auth_domain: function (index, callback) { + acme.show_speed_window('Verifying DNS...', function () { + acme.request( + 'apply_dns_auth', + { index: index }, + function (rdata) { + callback(rdata); + }, + false + ); + }); + }, + + //取证书基本信息 + get_cert_init: function (pem_file, siteName, callback) { + acme.request( + 'get_cert_init_api', + { pem_file: pem_file, siteName: siteName }, + function (cert_init) { + callback(cert_init); + }, + 'Getting certificate information...' + ); + }, + + //显示进度 + show_speed: function () { + bt.send( + 'get_lines', + 'ajax/get_lines', + { + num: 10, + filename: '/www/server/panel/logs/letsencrypt.log', + }, + function (rdata) { + if ($('#create_lst').text() === '') return; + if (rdata.status === true) { + $('#create_lst').text(rdata.msg); + $('#create_lst').scrollTop($('#create_lst')[0].scrollHeight); + } + setTimeout(function () { + acme.show_speed(); + }, 1000); + } + ); + }, + + //显示进度窗口 + show_speed_window: function (msg, callback) { + acme.loadT = layer.open({ + title: false, + type: 1, + closeBtn: 0, + shade: 0.3, + area: '500px', + offset: '30%', + content: acme.speed_msg.replace('[MSG]', msg), + success: function (layers, index) { + setTimeout(function () { + acme.show_speed(); + }, 1000); + if (callback) callback(); + }, + }); + }, + + //一键申请 + //domain 域名列表 [] + //auth_type 验证类型 model/http + //auth_to 验证路径 网站根目录或dnsapi + //auto_wildcard 是否自动组合通配符 1.是 0.否 默认0 + apply_cert: function (domains, auth_type, auth_to, auto_wildcard, callback) { + acme.show_speed_window('Applying for a certificate...', function () { + if (auto_wildcard === undefined) auto_wildcard = '0'; + pdata = { + domains: JSON.stringify(domains), + auth_type: auth_type, + auth_to: auth_to, + auto_wildcard: auto_wildcard, + }; + + if (acme.id) pdata['id'] = acme.id; + if (acme.siteName) pdata['siteName'] = acme.siteName; + acme.request( + 'apply_cert_api', + pdata, + function (rdata) { + callback(rdata); + }, + false + ); + }); + }, + + //续签证书 + renew: function (index, callback) { + acme.show_speed_window('Renewing certificate...', function () { + acme.request( + 'renew_cert', + { index: index }, + function (rdata) { + callback(rdata); + }, + false + ); + }); + }, + + //获取用户信息 + get_account_info: function (callback) { + acme.request('get_account_info', {}, function (rdata) { + callback(rdata); + }); + }, + + //设置用户信息 + set_account_info: function (account, callback) { + acme.request('set_account_info', account, function (rdata) { + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + + //发送到请求 + request: function (action, pdata, callback, msg) { + if (msg == undefined) msg = 'Processing, please wait...'; + if (msg) { + var loadT = layer.msg(msg, { icon: 16, time: 0, shade: 0.3 }); + } + $.post('/acme?action=' + action, pdata, function (res) { + if (msg) layer.close(loadT); + if (callback) callback(res); + }); + }, +}; + +/** 消息通道 **/ +function MessageChannelSettings() { + MessageChannel.get_channel_settings(function (rdata) { + layer.open({ + type: 1, + area: '600px', + title: 'Setting up notification', + skin: 'layer-channel-auth', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + '
          \ +
          \ +
          \ +

          Email

          \ +

          Telegram

          \ +
          \ +
          \ +
          \ +
          \ +
          \ +
          \ + \ + \ +
          \ +
          \ +
          \ +
          EmailOperating
          \ +
          \ +
          \ +
          \ +
          \ +
          \ + \ +
          \ +
          \ +
          \ +
          ', + success: function () { + $('.addTelegram').click(function () { + var _id = $('[name=telegram_id]').val(), + _token = $('[name=telegram_token]').val(); + if (_id == '' || _token == '') return layer.msg('input box cannot be empty!'); + var loadT = layer.msg('The notification is being generated, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=set_tg_bot', { bot_token: _token, my_id: _id }, function (rdata) { + layer.close(loadT); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + }); + }); + $('.delTelegram').click(function () { + var loadTs = layer.msg('Deleting notification, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=del_tg_info', function (rdata) { + layer.close(loadTs); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + if (rdata.status) { + $('[name=telegram_id]').val(''); + $('[name=telegram_token]').val(''); + $('.delTelegram').hide(); + } + }); + }); + }, + }); + $('.bt-w-menu p').click(function () { + var index = $(this).index(); + $(this).addClass('bgw').siblings().removeClass('bgw'); + $('.conter_box').eq(index).show().siblings().hide(); + }); + MessageChannel.get_receive_list(); + }); +} +var MessageChannel = { + //获取推送设置 + get_channel_settings: function (callback) { + var loadT = layer.msg('Getting profile, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=get_settings2', function (rdata) { + layer.close(loadT); + if (callback) callback(rdata); + }); + }, + // 获取收件者列表 + get_receive_list: function () { + $.post('/config?action=get_settings2', function (rdata) { + var _html = '', + _list = rdata.user_mail.mail_list; + if (_list.length > 0) { + for (var i = 0; i < _list.length; i++) { + _html += + '\ + ' + + _list[i] + + '\ + Del\ + '; + } + } else { + _html = 'No Data'; + } + $('#receive_table').html(_html); + }); + }, + // 添加收件者 + add_receive_info: function () { + var _this = this; + layer.open({ + type: 1, + area: '400px', + title: 'Add recipient email', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + '
          \ +
          \ + Recipient mailbox\ +
          \ + \ +
          \ +
          \ +
          \ + \ + \ +
          \ +
          ', + success: function (layers, index) { + $('.CreaterReceive').click(function () { + var _receive = $('input[name=creater_email_value]').val(); + if (_receive != '') { + var loadT = layer.msg('Please wait while creating recipient list...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + layer.close(index); + $.post('/config?action=add_mail_address', { email: _receive }, function (rdata) { + layer.close(loadT); + // 刷新收件列表 + _this.get_receive_list(); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + }); + } else { + layer.msg('Recipient mailbox cannot be empty!!', { icon: 2 }); + } + }); + + $('.smtp_closeBtn').click(function () { + layer.close(index); + }); + }, + }); + }, + // 删除收件者 + del_email: function (mail) { + var loadT = layer.msg('Deleting[' + mail + '],please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }), + _this = this; + $.post('/config?action=del_mail_list', { email: mail }, function (rdata) { + layer.close(loadT); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + _this.get_receive_list(); + }); + }, + // 设置发送者邮箱信息 + sender_info_edit: function () { + var loadT = layer.msg('Getting profile, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=get_settings2', function (rdata) { + layer.close(loadT); + var qq_mail = rdata.user_mail.info.msg.qq_mail ? rdata.user_mail.info.msg.qq_mail : '', + qq_stmp_pwd = rdata.user_mail.info.msg.qq_stmp_pwd ? rdata.user_mail.info.msg.qq_stmp_pwd : '', + hosts = rdata.user_mail.info.msg.hosts ? rdata.user_mail.info.msg.hosts : '', + port = rdata.user_mail.info.msg.port ? rdata.user_mail.info.msg.port : '', + is_custom = $.inArray(port, ['25', '465', '587', '']) != -1; //是否自定义 + layer.open({ + type: 1, + area: '460px', + title: 'Set sender email information', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + '
          \ +
          \ + Sender email\ +
          \ + \ +
          \ +
          \ +
          \ + SMTP password\ +
          \ + \ +
          \ +
          \ +
          \ + SMTP server\ +
          \ + \ +
          \ +
          \ +
          \ + SMTP port\ +
          \ + \ + \ +
          \ +
          \ +
            \ +
          • 465 port is recommended, the protocol is SSL/TLS
          • \ +
          • Port 25 is SMTP protocol, port 587 is STARTTLS protocol
          • \ +
          \ +
          \ + ' + + (qq_mail != '' ? '' : '') + + '\ + \ +
          \ +
          ', + success: function (layers, index) { + var _option = ''; + if (is_custom) { + if (port == '465' || port == '') { + _option = ''; + } else if (port == '25') { + _option = ''; + } else { + _option = ''; + } + } else { + _option = ''; + } + $('#port_select').html(_option); + $('#port_select').change(function (e) { + if (e.target.value == 'other') { + $('#port_select').css('width', '100px'); + $('input[name=channel_email_port]').css('display', 'inline-block'); + } else { + $('#port_select').css('width', '300px'); + $('input[name=channel_email_port]').css('display', 'none'); + } + }); + $('.SetChannelEmail').click(function () { + var _email = $('input[name=channel_email_value]').val(); + var _passW = $('input[name=channel_email_password]').val(); + var _server = $('input[name=channel_email_server]').val(), + _port = ''; + if ($('#port_select').val() == 'other') { + _port = $('input[name=channel_email_port]').val(); + } else { + _port = $('#port_select').val(); + } + if (!_email) return layer.msg('Email address cannot be empty!', { icon: 2 }); + if (!_passW) return layer.msg('STMP password cannot be empty!', { icon: 2 }); + if (!_server) return layer.msg('STMP server address cannot be empty!', { icon: 2 }); + if (!_port) return layer.msg('STMP server port cannot be empty!', { icon: 2 }); + + var loadT = layer.msg('The notification is being generated, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=user_mail_send', { email: _email, stmp_pwd: _passW, hosts: _server, port: _port }, function (rdata) { + layer.close(loadT); + if (rdata.status) { + layer.close(index); + MessageChannel.get_channel_settings(); + } + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + }); + }); + $('.smtp_closeBtn').click(function () { + layer.close(index); + }); + $('.set_empty').click(function () { + var loadTs = layer.msg('notification, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=set_empty', { type: 'mail' }, function (rdata) { + layer.close(loadTs); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + if (rdata.status) { + layer.close(index); + } + }); + }); + }, + }); + }); + }, +}; +/** 消息通道 end**/ +var product_recommend = { + data: null, + /** + * @description 初始化 + */ + init: function (callback) { + var _this = this; + if (location.pathname.indexOf('bind') > -1) return; + this.get_product_type(function (rdata) { + _this.data = rdata; + if (callback) callback(rdata); + }); + }, + /** + * @description 获取推荐类型 + * @param {object} type 参数{type:类型} + */ + get_recommend_type: function (type) { + var config = null, + pathname = location.pathname.replace('/', '') || 'home'; + for (var i = 0; i < this.data.length; i++) { + var item = this.data[i]; + if (item.type == type && item.show) config = item; + } + return config; + }, + + /** + * @description 或指定版本事件 + * @param {} name + */ + get_version_event: function (item, param) { + var pay_status = this.get_pay_status(); + bt.soft.get_soft_find(item.name, function (res) { + if ((res.type === 12 && pay_status.is_pay && pay_status.advanced !== 'ltd') || !pay_status.is_pay) { + product_recommend.recommend_product_view(item); + } else if (!res.setup) { + bt.soft.install(item.name); + } else { + bt.plugin.get_plugin_byhtml(item.name, function (html) { + if (typeof html === 'string') { + layer.open({ + type: 1, + shade: 0, + skin: 'hide', + content: html, + success: function () { + var is_event = false; + for (var i = 0; i < item.eventList.length; i++) { + var data = item.eventList[i]; + var oldVersion = data.version.replace('.', ''), + newVersion = res.version.replace('.', ''); + if (newVersion <= oldVersion) { + is_event = true; + setTimeout(function () { + new Function(data.event.replace('$siteName', param))(); + }, 100); + break; + } + } + if (!is_event) new Function(item.eventList[item.eventList.length - 1].event.replace('$siteName', param))(); + }, + }); + } + }); + } + }); + }, + /** + * @description 获取支付状态 + */ + get_pay_status: function () { + var pro_end = parseInt(bt.get_cookie('pro_end') || -1); + var ltd_end = parseInt(bt.get_cookie('ltd_end') || -1); + var is_pay = pro_end > -1 || ltd_end > -1; // 是否购买付费版本 + var advanced = 'pro'; // 已购买,专业版优先显示 + if (pro_end === -2 || pro_end > -1) advanced = 'pro'; + if (ltd_end === -2 || ltd_end > -1) advanced = 'ltd'; + var end_time = advanced === 'ltd' ? ltd_end : pro_end; // 到期时间 + return { advanced: advanced, is_pay: is_pay, end_time: end_time }; + }, + + pay_product_sign: function (type, source) { + switch (type) { + case 'pro': + bt.soft['updata_' + type](source); + break; + case 'ltd': + bt.soft['updata_' + type](false, source); + break; + } + }, + /** + * @description 获取项目类型 + * @param {Function} callback 回调函数 + */ + get_product_type: function (callback) { + bt.send('get_pay_type', 'ajax/get_pay_type', {}, function (rdata) { + bt.set_storage('session', 'get_pay_type', JSON.stringify(rdata)); + if (callback) callback(rdata); + }); + }, + /** + * @description 推荐购买产品 + * @param {Object} pay_id 购买的入口id + */ + recommend_product_view: function (config) { + var name = config.name.split('_')[0]; + var status = this.get_pay_status(); + console.log(status); + bt.open({ + title: false, + area: '650px', + btn: false, + content: + '
          \ +
          \ +
          \ +
          ' + + config.pluginName + + '
          \ +
          ' + + config.ps + + '
          \ +
          \ + 产品预览\ +
          \ +
          \ +
          ', + success: function () { + // 产品预览 + $('.product_view img').click(function () { + layer.open({ + type: 1, + title: '查看图片', + area: ['650px', '450px'], + closeBtn: 2, + btn: false, + content: '', + }); + }); + // 立即购买 + $('.buyNow').click(function () { + bt.set_cookie('pay_source', config.pay); + bt.soft['updata_' + status.advanced](); + }); + }, + }); + }, +}; + +var rsa = { + publicKey: null, + /** + * @name 使用公钥加密 + * @param {string} text + * @returns string + */ + encrypt_public: function (text) { + this.publicKey = document.querySelector('.public_key').attributes.data.value; + if (this.publicKey.length < 10) return text; + var encrypt = new JSEncrypt(); + encrypt.setPublicKey(this.publicKey); + return encrypt.encrypt(text); + }, + /** + * @name 使用公钥解密 + * @param {string} text + * @returns string + */ + decrypt_public: function (text) { + this.publicKey = document.querySelector('.public_key').attributes.data.value; + if (this.publicKey.length < 10) return null; + var decrypt = new JSEncrypt(); + decrypt.setPublicKey(this.publicKey); + return decrypt.decryptp(text); + }, +}; + +/** + * @description 渲染邮箱配置视图 + */ +function renderMailConfigView(data) { + layer.open({ + type: 1, + title: 'Set sender email information', + area: ['470px', '376px'], + btn: [lan.public.save, lan.public.cancel], + skin: 'alert-send-view', + content: + '
          \ +
          \ + Sender email\ +
          \ + \ +
          \ +
          \ +
          \ + SMTP password\ +
          \ + \ +
          \ +
          \ +
          \ + SMTP server\ +
          \ + \ +
          \ +
          \ +
          \ + SMTP port\ +
          \ + \ +
          \ +
          \ +
            \ +
          • 465 port is recommended, the protocol is SSL/TLS
          • \ +
          • Port 25 is SMTP protocol, port 587 is STARTTLS protocol
          • \ +
          \ +
          ', + success: function () { + if (!$.isEmptyObject(data) && !$.isEmptyObject(data.data.send)) { + var send = data.data.send, + mail_ = send.qq_mail || '', + stmp_pwd_ = send.qq_stmp_pwd || '', + hosts_ = send.hosts || '', + port_ = send.port || ''; + + $('input[name=sender_mail_value]').val(mail_); + $('input[name=sender_mail_password]').val(stmp_pwd_); + $('input[name=sender_mail_server]').val(hosts_); + $('input[name=sender_mail_port]').val(port_); + } else { + $('input[name=sender_mail_port]').val('465'); + } + }, + yes: function (indexs) { + var _email = $('input[name=sender_mail_value]').val(), + _passW = $('input[name=sender_mail_password]').val(), + _server = $('input[name=sender_mail_server]').val(), + _port = $('input[name=sender_mail_port]').val(); + + if (_email == '') return layer.msg('Email address cannot be empty!', { icon: 2 }); + if (_passW == '') return layer.msg('STMP password cannot be empty!', { icon: 2 }); + if (_server == '') return layer.msg('STMP server address cannot be empty!', { icon: 2 }); + if (_port == '') return layer.msg('STMP server port cannot be empty!', { icon: 2 }); + + if (!data.setup) { + bt_tools.send( + { url: '/config?action=install_msg_module&name=' + data.name, data: {} }, + function (res) { + if (res.status) { + bt_tools.send( + { url: '/config?action=set_msg_config&name=mail', data: { send: 1, qq_mail: _email, qq_stmp_pwd: _passW, hosts: _server, port: _port } }, + function (configM) { + if (configM.status) { + layer.close(indexs); + layer.msg(configM.msg, { + icon: configM.status ? 1 : 2, + }); + if ($('.alert-view-box').length >= 0) $('.alert-view-box .tab-nav-border span:eq(1)').click(); + } + }, + 'Setting email Settings' + ); + } else { + layer.msg(res.msg, { icon: 2 }); + } + }, + 'Creating ' + data.title + ' module' + ); + } else { + bt_tools.send( + { + url: '/config?action=set_msg_config&name=mail', + data: { + send: 1, + qq_mail: _email, + qq_stmp_pwd: _passW, + hosts: _server, + port: _port, + }, + }, + function (configM) { + if (configM.status) { + layer.close(indexs); + layer.msg(configM.msg, { + icon: configM.status ? 1 : 2, + }); + } + }, + 'Setting email Settings' + ); + } + }, + }); +} + +/** + * @description 渲染url通道方式视图 + */ +function renderAlertUrlTypeChannelView(data) { + var isEmpty = $.isEmptyObject(data.data); + layer.open({ + type: 1, + title: data['title'] + ' robot configuration', + area: ['480px', '345px'], + btn: [lan.public.save, lan.public.cancel], + skin: 'alert-send-view', + content: + '
          \ +
          \ + Name\ +
          \ + \ +
          \ +
          \ +
          \ + URL\ +
          \ + \ +
          \ + \ +
          \ +
          ', + success: function () { + if (!$.isEmptyObject(data.data)) { + var url = data['data'][data.name + '_url'] || ''; + $('textarea[name=channel_url_value]').val(url); + } + }, + yes: function (indexs) { + var _index = $('.alert-view-box span.on').index(); + var _url = $('textarea[name=channel_url_value]').val(), + _name = $('input[name=chatName]').val(); + if (_name == '') return layer.msg('Please enter the robot name or remarks', { icon: 2 }); + if (_url == '') return layer.msg('Please enter the robot url', { icon: 2 }); + if (!data.setup) { + bt_tools.send( + { url: '/config?action=install_msg_module&name=' + data.name, data: {} }, + function (res) { + if (res.status) { + setTimeout(function () { + bt_tools.send( + { + url: '/config?action=set_msg_config&name=' + data.name, + data: { + url: _url, + title: _name, + atall: 'True', + }, + }, + function (rdata) { + layer.close(indexs); + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 2, + }); + if ($('.alert-view-box').length >= 0) { + $('.alert-view-box .tab-nav-border span:eq(' + _index + ')').click(); + } + }, + 'Setting ' + data.title + ' configuration' + ); + }, 100); + } else { + layer.msg(res.msg, { icon: 2 }); + } + }, + 'Creating ' + data.title + ' module' + ); + } else { + bt_tools.send( + { + url: '/config?action=set_msg_config&name=' + data.name, + data: { + url: _url, + title: _name, + atall: 'True', + }, + }, + function (rdata) { + layer.close(indexs); + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 2, + }); + if ($('.alert-view-box').length >= 0) { + $('.alert-view-box .tab-nav-border span:eq(' + _index + ')').click(); + } + }, + 'Setting ' + data.title + ' module' + ); + } + }, + }); +} + +function renderTelegramConfigView(data) { + layer.open({ + type: 1, + title: 'Telegram configuration', + area: ['460px', '320px'], + btn: [lan.public.save, lan.public.cancel], + skin: 'alert-send-view', + content: + '
          \ +
          \ + ID\ +
          \ + \ +
          \ +
          \ +
          \ + TOKEN\ +
          \ + \ +
          \ +
            \ +
          • ID: Your telegram user ID
          • \ +
          • Token: Your telegram bot token
          • \ +
          • e.g: [ 12345677:AAAAAAAAA_a0VUo2jjr__CCCCDDD ] Help
          • \ +
          \ +
          \ +
          ', + success: function () { + var res = data.data; + if (res) { + $('[name="telegram_id"]').val(res.my_id); + $('[name="telegram_token"]').val(res.bot_token); + } + }, + yes: function (indexs) { + var id = $('input[name=telegram_id]').val(); + var token = $('input[name=telegram_token]').val(); + var _index = $('.alert-view-box span.on').index(); + + if (id == '') return layer.msg('Please enter Telegram ID!', { icon: 2 }); + if (token == '') return layer.msg('Please enter Telegram token', { icon: 2 }); + + function saveConfig() { + bt_tools.send( + { + url: '/config?action=set_msg_config&name=' + data.name, + data: { + my_id: id, + bot_token: token, + }, + }, + function (rdata) { + layer.close(indexs); + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 2, + }); + if ($('.alert-view-box').length >= 0) { + $('.alert-view-box .tab-nav-border span:eq(' + _index + ')').click(); + } + }, + 'Setting ' + data.title + ' module' + ); + } + + if (!data.setup) { + bt_tools.send( + { + url: '/config?action=install_msg_module&name=' + data.name, + data: {}, + }, + function (res) { + if (res.status) { + saveConfig(); + } else { + layer.msg(res.msg, { icon: 2 }); + } + }, + 'Creating ' + data.title + ' module' + ); + } else { + saveConfig(); + } + }, + }); +} + +// true: 消息推送 false: 消息通道 +var ConfigIsPush = false; +// 消息推送弹框 +var ConfigIndex = -1; + +// 打开消息通道/消息推送 +function open_three_channel_auth(stype) { + var _title = 'Set Notification'; + var _area = '650px'; + var isPush = false; + var assign = ''; + + if (stype === 'MsgPush') { + // 类型为消息推送 + _title = 'Set message push'; + _area = ['900px', '603px']; + isPush = true; + } else if (typeof stype != 'undefined' && stype) { + // 指定选择消息通道的某个菜单 + assign = stype; + } + + ConfigIsPush = isPush; + + ConfigIndex = layer.open({ + type: 1, + area: _area, + title: _title, + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + '\ +
          \ +
          \ +
          \ +
          \ +
          \ +
          \ +
          \ +
          \ +
          ', + success: function () { + // 获取菜单配置 + getMsgConfig(assign ? assign : ''); + + // 卸载/禁用模块 + $('.alarm-view').on('click', '.btn-uninstall', function () { + uninstallMsgModuleConfig(); + }); + + // 立即更新 + $('.alarm-view').on('click', '.btn-update', function () { + installMsgModuleConfig(); + }); + }, + }); +} + +// 获取模板配置 +function getTemplateMsgConfig(item, shtml) { + $.post( + '/' + (ConfigIsPush ? 'push' : 'config') + '?action=get_module_template', + { + module_name: item.name, + }, + function (res) { + if (res.status) { + // 添加菜单内容 + $('.bt-w-main .plugin_body').html(res.msg.trim()); + // 添加底部内容 + var updateInfo = ''; + // 是否更新 + if (item.version !== item.info.version) { + updateInfo = '【' + item['title'] + '】模块存在新的版本,为了不影响使用,请更新。'; + } + // $(".bt-w-main .plugin_update").html('\ + //
          \ + //
          ' + updateInfo + '
          \ + //
          \ + //
          '); + } else { + $('.bt-w-main .plugin_body').html(shtml); + } + new Function(item.name + '.init()')(); + } + ); +} + +// 获取消息配置 +function getMsgConfig(openType) { + var _api = '/config?action=get_msg_configs'; + if (ConfigIsPush) _api = '/push?action=get_modules_list'; + + $.post(_api, function (rdata) { + var _menu = ''; + var menu_data = $('.alarm-view .bt-w-menu p.bgw').data('data'); + $('.alarm-view .bt-w-menu').html(''); + $.each(rdata, function (index, item) { + var _default = item.data && item.data.default; + var _flag = ''; + if (_default) { + _flag = ''; + } + _menu = $("

          " + item['title'] + _flag + '

          ').data('data', item); + $('.alarm-view .bt-w-menu').append(_menu); + }); + // $('.alarm-view .bt-w-menu').append('更新列表'); + $('.alarm-view .bt-w-menu p').click(function () { + $(this).addClass('bgw').siblings().removeClass('bgw'); + var _item = $(this).data('data'); + + var shtml = + ''; + if (_item['setup']) { + getTemplateMsgConfig(_item, shtml); + } else { + $('.bt-w-main .plugin_body').html(shtml); + $('.bt-w-main .plugin_update').html(''); + } + }); + if (menu_data) { + $('.men_' + menu_data['name']).click(); + } else { + if (typeof openType != 'undefined' && openType) { + $('.alarm-view .bt-w-menu p.men_' + openType).trigger('click'); + } else { + $('.alarm-view .bt-w-menu p').eq(0).trigger('click'); + } + } + }); +} + +function installMsgModuleConfig(name) { + var _api = '/config?action=install_msg_module'; + if (ConfigIsPush) _api = '/push?action=install_module'; + name = name ? '.men_' + name : ''; + var _item = $('.alarm-view .bt-w-menu p.bgw' + name).data('data'); + var spt = '安装'; + if (_item.setup) spt = '更新'; + + layer.confirm( + '是否要' + spt + '【' + _item.title + '】模块', + { + title: '安装模块', + closeBtn: 2, + icon: 0, + }, + function () { + var loadT = layer.msg('正在' + spt + _item.title + '模块中,请稍候...', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + $.post(_api + '&name=' + _item.name + '', function (res) { + getMsgConfig(); + layer.close(loadT); + layer.msg(res.msg, { + icon: res.status ? 1 : 2, + }); + }); + } + ); +} + +function uninstallMsgModuleConfig() { + var _api = '/config?action=uninstall_msg_module'; + if (ConfigIsPush) _api = '/push?action=uninstall_module'; + + var _item = $('.alarm-view .bt-w-menu p.bgw').data('data'); + + layer.confirm( + '是否确定要卸载【' + _item.title + '】模块', + { + title: '卸载模块', + closeBtn: 2, + icon: 0, + }, + function () { + var loadT = layer.msg('正在卸载' + _item.title + '模块中,请稍候...', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + $.post(_api + '&name=' + _item.name + '', function (res) { + layer.close(loadT); + getMsgConfig(); + layer.msg(res.msg, { + icon: res.status ? 1 : 2, + }); + }); + } + ); +} + +function refreshThreeChannelAuth() { + var _api = '/config?action=get_msg_configs'; + if (ConfigIsPush) _api = '/push?action=get_modules_list'; + + var loadT = layer.msg('正在更新模块列表中,请稍候...', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + layer.confirm( + '是否确定获取最新的模块列表', + { + title: '刷新列表', + closeBtn: 2, + icon: 0, + }, + function (index) { + layer.close(index); + layer.close(ConfigIndex); + $.post( + _api, + { + force: 1, + }, + function (rdata) { + layer.close(loadT); + open_three_channel_auth(ConfigIsPush ? 'MsgPush' : ''); + } + ); + } + ); +} diff --git a/BTPanel/static/vite/oldjs/public_backup.js b/BTPanel/static/vite/oldjs/public_backup.js new file mode 100644 index 00000000..cbac4d5f --- /dev/null +++ b/BTPanel/static/vite/oldjs/public_backup.js @@ -0,0 +1,10168 @@ +var bt = { + os: 'Linux', + check_ip: function ( + ip //验证ip + ) { + var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; + return reg.test(ip); + }, + check_ips: function ( + ips //验证ip段 + ) { + var reg = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$/; + return reg.test(ip); + }, + check_url: function ( + url //验证url + ) { + var reg = /^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+/; + return reg.test(url); + }, + check_port: function (port) { + var reg = /^([1-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/; + return reg.test(port); + }, + check_chinese: function (str) { + var reg = /[\u4e00-\u9fa5]/; + return reg.test(str); + }, + check_domain: function ( + domain //验证域名 + ) { + var reg = /^([\w\u4e00-\u9fa5\-\*]{1,100}\.){1,4}([\w\u4e00-\u9fa5\-]{1,24}|[\w\u4e00-\u9fa5\-]{1,24}\.[\w\u4e00-\u9fa5\-]{1,24})$/; + return reg.test(bt.strim(domain)); + }, + check_img: function ( + fileName //验证是否图片 + ) { + var exts = ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'tiff', 'ico']; + var check = bt.check_exts(fileName, exts); + return check; + }, + check_email: function (email) { + var reg = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/; + return reg.test(email); + }, + check_phone: function (phone) { + var reg = /^1(3|4|5|6|7|8|9)\d{9}$/; + return reg.test(phone); + }, + check_zip: function (fileName) { + var ext = fileName.split('.'); + var extName = ext[ext.length - 1].toLowerCase(); + if (extName == 'zip') return 0; + if (extName == 'rar') return 2; + if (extName == 'gz' || extName == 'tgz') return 1; + return -1; + }, + clear_cookie: function (key) { + this.set_cookie(key, '', new Date()); + }, + check_text: function (fileName) { + var exts = ['rar', 'zip', 'tar.gz', 'gz', 'iso', 'xsl', 'doc', 'xdoc', 'jpeg', 'jpg', 'png', 'gif', 'bmp', 'tiff', 'exe', 'so', '7z', 'bz']; + return bt.check_exts(fileName, exts) ? false : true; + }, + check_exts: function (fileName, exts) { + var ext = fileName.split('.'); + if (ext.length < 2) return false; + var extName = ext[ext.length - 1].toLowerCase(); + for (var i = 0; i < exts.length; i++) { + if (extName == exts[i]) return true; + } + return false; + }, + check_version: function (version, cloud_version) { + var arr1 = version.split('.'); // + var arr2 = cloud_version.split('.'); + var leng = arr1.length > arr2.length ? arr1.length : arr2.length; + while (leng - arr1.length > 0) { + arr1.push(0); + } + while (leng - arr2.length > 0) { + arr2.push(0); + } + for (var i = 0; i < leng; i++) { + if (i == leng - 1) { + if (arr1[i] != arr2[i]) return 2; //子版本匹配不上 + } else { + if (arr1[i] != arr2[i]) return -1; //版本匹配不上 + } + } + return 1; //版本正常 + }, + replace_all: function (str, old_data, new_data) { + var reg_str = '/(' + old_data + '+)/g'; + var reg = eval(reg_str); + return str.replace(reg, new_data); + }, + get_file_ext: function (fileName) { + var text = fileName.split('.'); + var n = text.length - 1; + text = text[n]; + return text; + }, + get_file_path: function (filename) { + var arr = filename.split('/'); + path = filename.replace('/' + arr[arr.length - 1], ''); + return path; + }, + get_date: function (a) { + var dd = new Date(); + dd.setTime(dd.getTime() + (a == undefined || isNaN(parseInt(a)) ? 0 : parseInt(a)) * 86400000); + var y = dd.getFullYear(); + var m = dd.getMonth() + 1; + var d = dd.getDate(); + return y + '-' + (m < 10 ? '0' + m : m) + '-' + (d < 10 ? '0' + d : d); + }, + get_form: function (select) { + var sarr = $(select).serializeArray(); + var iarr = {}; + for (var i = 0; i < sarr.length; i++) { + iarr[sarr[i].name] = sarr[i].value; + } + return iarr; + }, + ltrim: function (str, r) { + var reg_str = '/(^\\' + r + '+)/g'; + var reg = eval(reg_str); + str = str.replace(reg, ''); + return str; + }, + rtrim: function (str, r) { + var reg_str = '/(\\' + r + '+$)/g'; + var reg = eval(reg_str); + str = str.replace(reg, ''); + return str; + }, + strim: function (str) { + var reg_str = '/ /g'; + var reg = eval(reg_str); + str = str.replace(reg, ''); + return str; + }, + contains: function (str, substr) { + if (str) { + return str.indexOf(substr) >= 0; + } + return false; + }, + format_size: function ( + bytes, + is_unit, + fixed, + end_unit //字节转换,到指定单位结束 is_unit:是否显示单位 fixed:小数点位置 end_unit:结束单位 + ) { + if (bytes == undefined) return 0; + + if (is_unit == undefined) is_unit = true; + if (fixed == undefined) fixed = 2; + if (end_unit == undefined) end_unit = ''; + + if (typeof bytes == 'string') bytes = parseInt(bytes); + var unit = [' B', ' KB', ' MB', ' GB', 'TB']; + var c = 1024; + for (var i = 0; i < unit.length; i++) { + var cUnit = unit[i]; + if (end_unit) { + if (cUnit.trim() == end_unit.trim()) { + var val = i == 0 ? bytes : fixed == 0 ? bytes : bytes.toFixed(fixed); + if (is_unit) { + return val + cUnit; + } else { + val = parseFloat(val); + return val; + } + } + } else { + if (bytes < c) { + var val = i == 0 ? bytes : fixed == 0 ? bytes : bytes.toFixed(fixed); + if (is_unit) { + return val + cUnit; + } else { + val = parseFloat(val); + return val; + } + } + } + + bytes /= c; + } + }, + format_data: function (tm, format) { + if (format == undefined) format = 'yyyy/MM/dd hh:mm:ss'; + tm = tm.toString(); + if (tm.length > 10) { + tm = tm.substring(0, 10); + } + var data = new Date(parseInt(tm) * 1000); + var o = { + 'M+': data.getMonth() + 1, //month + 'd+': data.getDate(), //day + 'h+': data.getHours(), //hour + 'm+': data.getMinutes(), //minute + 's+': data.getSeconds(), //second + 'q+': Math.floor((data.getMonth() + 3) / 3), //quarter + S: data.getMilliseconds(), //millisecond + }; + if (/(y+)/.test(format)) format = format.replace(RegExp.$1, (data.getFullYear() + '').substr(4 - RegExp.$1.length)); + for (var k in o) if (new RegExp('(' + k + ')').test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)); + + return format; + }, + format_path: function (path) { + var reg = /(\\)/g; + path = path.replace(reg, '/'); + return path; + }, + get_random: function (len) { + len = len || 32; + var $chars = 'AaBbCcDdEeFfGHhiJjKkLMmNnPpRSrTsWtXwYxZyz2345678'; // 默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1 + var maxPos = $chars.length; + var pwd = ''; + for (i = 0; i < len; i++) { + pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); + } + return pwd; + }, + refresh_pwd: function (length, obj) { + if (obj == undefined) obj = 'MyPassword'; + var _input = $('#' + obj); + if (_input.length > 0) { + _input.val(bt.get_random(length)); + } else { + $('.' + obj).val(bt.get_random(length)); + } + }, + get_random_num: function ( + min, + max //生成随机数 + ) { + var range = max - min; + var rand = Math.random(); + var num = min + Math.round(rand * range); //四舍五入 + return num; + }, + + /** + * 生成计算数字(加强计算,用于删除重要数据二次确认) + * */ + get_random_code: function () { + var flist = [20, 21, 22, 23]; + + var num1 = bt.get_random_num(13, 19); + var t1 = num1 % 10; + + var num2 = bt.get_random_num(13, 29); + var t2 = num2 % 10; + + while ($.inArray(num2, flist) >= 0 || t1 + t2 <= 10 || t1 == t2) { + num2 = bt.get_random_num(13, 29); + t2 = num2 % 10; + } + return { num1: num1, num2: num2 }; + }, + /** + * @description 设置本地存储,local和session + * @param {String} type 存储类型,可以为空,默认为session类型。 + * @param {String} key 存储键名 + * @param {String} val 存储键值 + * @return 无返回值 + */ + set_storage: function (type, key, val) { + if (type != 'local' && type != 'session') (val = key), (key = type), (type = 'local'); + window[type + 'Storage'].setItem(key, val); + }, + + /** + * @description 获取本地存储,local和session + * @param {String} type 存储类型,可以为空,默认为session类型。 + * @param {String} key 存储键名 + * @return {String} 返回存储键值 + */ + get_storage: function (type, key) { + if (type != 'local' && type != 'session') (key = type), (type = 'local'); + return window[type + 'Storage'].getItem(key); + }, + + /** + * @description 删除指定本地存储,local和session + * @param {String} type 类型,可以为空,默认为session类型。 + * @param {String} key 键名 + * @return 无返回值 + */ + remove_storage: function (type, key) { + if (type != 'local' && type != 'session') (key = type), (type = 'local'); + window[type + 'Storage'].removeItem(key); + }, + + /** + * @description 删除指定类型的所有存储信息储,local和session + * @param {String} type 类型,可以为空,默认为session类型。 + * @return 无返回值 + */ + clear_storage: function (type) { + if (type != 'local' && type != 'session') (key = type), (type = 'local'); + window[type + 'Storage'].clear(); + }, + set_cookie: function (key, val, time) { + if (time != undefined) { + var exp = new Date(); + exp.setTime(exp.getTime() + time); + time = exp.toGMTString(); + } else { + var Days = 30; + var exp = new Date(); + exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000); + time = exp.toGMTString(); + } + document.cookie = key + '=' + escape(val) + ';expires=' + time; + }, + get_cookie: function (key) { + var arr, + reg = new RegExp('(^| )' + key + '=([^;]*)(;|$)'); + if ((arr = document.cookie.match(reg))) { + var val = unescape(arr[2]); + return val == 'undefined' ? '' : val; + } else { + return null; + } + }, + /** + * @description 选择文件目录或文件 + * @param id {string} 元素ID + * @param type {string || function} 选择方式,文件或目录 + * @param success {function} 成功后的回调 + */ + select_path: function (id, type, success, default_path) { + _this = this; + _this.set_cookie('SetName', ''); + if (typeof type !== 'string') { + success = type; + type = 'dir'; + } + var loadT = bt.open({ + type: 1, + area: '680px', + title: type === 'all' ? 'Select directories or files' : type === 'file' ? lan.bt.file : lan.bt.dir, + closeBtn: 2, + shift: 5, + content: + "
          " + + lan.bt.path + + ":
          " + + lan.bt.comp + + "
            " + + lan.bt.filename + + "" + + lan.bt.etime + + "" + + lan.bt.access + + "" + + lan.bt.own + + "
            ', + success: function () { + $('#btn_back').click(function () { + var path = $('#PathPlace').find('span').text(); + path = bt.rtrim(bt.format_path(path), '/'); + var back_path = bt.get_file_path(path); + _this.get_file_list(back_path, type); + }); + //选择 + $('#bt_select').on('click', function () { + var path = bt.format_path($('#PathPlace').find('span').text()); + if (type === 'file' && !$('#tbody tr.active').length) { + layer.msg('Select the file first!', { icon: 0 }); + return false; + } + if ($('#tbody tr').hasClass('active')) { + path = $('#tbody tr.active .bt_open_dir').attr('path'); + } + path = bt.rtrim(path, '/'); + if (path.length === 0) { + path = [$('#PathPlace').find('span').text()]; + } + $('#' + id) + .val(path) + .change(); + $('.' + id) + .val(path) + .change(); + if (typeof success === 'function') success(path); + loadT.close(); + }); + var element = $('#' + id), + paths = element.val(), + defaultPath = $('#defaultPath'); + if (defaultPath.length > 0 && element.parents('.tab-body').length > 0) { + paths = defaultPath.text(); + } + if (default_path) { + paths = default_path; + } + _this.get_file_list(paths, type); + bt.fixed_table('file-list-table'); + }, + }); + _this.set_cookie('ChangePath', loadT.form); + // var paths = $("#" + id).val(); + // if ($('#defaultPath').length > 0 && $("#" + id).parents('.tab-body').length > 0) { + // paths = $('#defaultPath').text(); + // } + // _this.get_file_list(paths, type); + + // function ActiveDisk() { + // var a = $("#PathPlace").find("span").text().substring(0, 1); + // switch (a) { + // case "C": + // $(".path-con-left dd:nth-of-type(1)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "D": + // $(".path-con-left dd:nth-of-type(2)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "E": + // $(".path-con-left dd:nth-of-type(3)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "F": + // $(".path-con-left dd:nth-of-type(4)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "G": + // $(".path-con-left dd:nth-of-type(5)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "H": + // $(".path-con-left dd:nth-of-type(6)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // default: + // $(".path-con-left dd").removeAttr("style") + // } + // } + }, + get_file_list: function (path, type) { + type = type || 'dir'; + var _that = this; + bt.send('GetDir', 'files/GetDir', { path: path, disk: true }, function (rdata) { + var d = '', + a = '', + disk = rdata.DISK; + if (disk != undefined) { + for (var f = 0; f < disk.length; f++) { + a += + '
            " + + disk[f].path + + '
            '; + } + $('#changecomlist').html(a); + } + for (var f = 0; f < rdata.DIR.length; f++) { + var g = rdata.DIR[f].split(';'); + var e = g[0]; + if (e.length > 20) { + e = e.substring(0, 20) + '...'; + } + if (isChineseChar(e)) { + if (e.length > 10) { + e = e.substring(0, 10) + '...'; + } + } + d += + '' + + (type === 'all' || type === 'dir' ? '' : '') + + '" + + e + + '' + + bt.format_data(g[2]) + + '' + + g[3] + + '' + + g[4] + + ''; + } + + if (rdata.FILES != null && rdata.FILES != '') { + for (var f = 0; f < rdata.FILES.length; f++) { + var g = rdata.FILES[f].split(';'); + var e = g[0]; + if (e.length > 20) { + e = e.substring(0, 20) + '...'; + } + if (isChineseChar(e)) { + if (e.length > 10) { + e = e.substring(0, 10) + '...'; + } + } + d += + '' + + (type === 'all' || type === 'file' ? '' : '') + + '" + + e + + '' + + bt.format_data(g[2]) + + '' + + g[3] + + '' + + g[4] + + ''; + } + } + + $('.default').hide(); + $('.file-list').show(); + $('#tbody').html(d); + if (rdata.PATH.substr(rdata.PATH.length - 1, 1) != '/') { + rdata.PATH += '/'; + } + $('#PathPlace').find('span').html(rdata.PATH); + $('#tbody tr').click(function () { + if ($(this).find('td:eq(0) input').length > 0) { + if ($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).find('td:eq(0) input').prop('checked', false); + } else { + $(this).find('td:eq(0) input').prop('checked', true); + $(this).siblings().find('td:eq(0) input').prop('checked', false); + $(this).addClass('active').siblings().removeClass('active'); + } + } + }); + $('#changecomlist dd').click(function () { + _that.get_file_list($(this).attr('path'), type); + }); + $('.bt_open_dir span').click(function () { + if ($(this).parent().data('type') == 'dir') _that.get_file_list($(this).parent().attr('path'), type); + }); + }); + }, + prompt_confirm: function (title, msg, callback) { + layer.open({ + type: 1, + title: title, + area: '480px', + closeBtn: 2, + btn: ['OK', 'Cancel'], + content: + "
            \ +

            " + + msg + + "

            \ +
            \ + \ +
            If you confirm the operation, enter it manually '" + + title + + "'
            \ +
            \ +
            ", + success: function () { + var black_txt_ = $('#prompt_input_box'); + + $('.placeholder').click(function () { + $(this).hide().siblings('input').focus(); + }); + black_txt_.focus(function () { + $('.prompt_input_tips.placeholder').hide(); + }); + black_txt_.blur(function () { + black_txt_.val() == '' ? $('.prompt_input_tips.placeholder').show() : $('.prompt_input_tips.placeholder').hide(); + }); + black_txt_.keyup(function () { + if (black_txt_.val() == '') { + $('.prompt_input_tips.placeholder').show(); + $('.prompt_input_ps').hide(); + } else { + $('.prompt_input_tips.placeholder').hide(); + } + }); + }, + yes: function (layers, index) { + var result = $('#prompt_input_box').val().trim(); + if (result == title) { + layer.close(layers); + if (callback) callback(); + } else { + $('.prompt_input_ps').show(); + } + }, + }); + }, + show_confirm: function (title, msg, fun, error) { + if (error == undefined) { + error = ''; + } + var d = Math.round(Math.random() * 9 + 1); + var c = Math.round(Math.random() * 9 + 1); + var e = ''; + e = d + c; + sumtext = d + ' + ' + c; + bt.set_cookie('vcodesum', e); + var mess = layer.open({ + type: 1, + title: title, + area: '350px', + closeBtn: 2, + shadeClose: true, + content: + "

            " + + msg + + '

            ' + + error + + "
            " + + lan.bt.cal_msg + + "" + + sumtext + + "=
            ', + success: function ($layer) { + $('#vcodeResult') + .focus() + .keyup(function (a) { + if (a.keyCode == 13) { + $('#toSubmit').click(); + } + }); + }, + }); + + $('.bt-cancel').click(function () { + layer.close(mess); + }); + $('#toSubmit').click(function () { + var a = $('#vcodeResult').val().replace(/ /g, ''); + if (a == undefined || a == '') { + layer.msg(lan.bt.cal_err); + return; + } + if (a != bt.get_cookie('vcodesum')) { + layer.msg(lan.bt.cal_err); + return; + } + layer.close(mess); + fun(); + }); + }, + /** + * @description 计算提示弹窗 + * @param {Object} config 弹窗对象 {title: 提示标题, msg: 提示内容} + * @param {function} callback 回调函数 + */ + compute_confirm: function (config, callback) { + var d = Math.round(Math.random() * 9 + 1), + c = Math.round(Math.random() * 9 + 1), + t = d + ' + ' + c, + e = d + c; + + function submit(index, layero) { + var a = $('#vcodeResult'), + val = a.val().replace(/ /g, ''); + if (val == undefined || val == '') { + layer.msg(lan.bt.cal_err); + return; + } + if (val != a.data('value')) { + layer.msg(lan.bt.cal_err); + return; + } + layer.close(index); + if (callback) callback(); + } + layer.open({ + type: 1, + title: config.title, + area: '430px', + closeBtn: 2, + shadeClose: true, + btn: [lan['public'].ok, lan['public'].cancel], + content: + '
            \ +
            \ + \ +
            ' + + config.msg + + '
            \ +
            \ +
            Result:' + + t + + '=
            \ +
            ', + success: function (layero, index) { + $('#vcodeResult') + .focus() + .keyup(function (a) { + if (a.keyCode == 13) { + submit(index, layero); + } + }); + }, + yes: submit, + }); + }, + to_login: function () { + layer.confirm(lan.public_backup.login_expire, { title: lan.public_backup.session_expire, icon: 2, closeBtn: 1, shift: 5 }, function () { + location.reload(); + }); + }, + do_login: function () { + bt.confirm({ msg: lan.bt.loginout }, function () { + window.location.href = '/login?dologin=True'; + }); + }, + send: function (response, module, data, callback, sType) { + if (sType == undefined) sType = 1; + + module = module.replace('panel_data', 'data'); + sType = 1; + var str = bt.get_random(16); + console.time(str); + if (!response) alert(lan.get('lack_param', ['response'])); + modelTmp = module.split('/'); + if (modelTmp.length < 2) alert(lan.get('lack_param', ['s_module', 'action'])); + if (bt.os == 'Linux' && sType === 0) { + socket.on(response, function (rdata) { + socket.removeAllListeners(response); + var rRet = rdata.data; + if (rRet.status === -1) { + bt.to_login(); + return; + } + console.timeEnd(str); + if (callback) callback(rRet); + }); + if (!data) data = {}; + data = bt.linux_format_param(data); + data['s_response'] = response; + data['s_module'] = modelTmp[0]; + data['action'] = modelTmp[1]; + socket.emit('panel', data); + } else { + data = bt.win_format_param(data); + var url = '/' + modelTmp[0] + '?action=' + modelTmp[1]; + $.post(url, data, function (rdata) { + //会话失效时自动跳转到登录页面 + if (typeof rdata == 'string') { + if ((rdata.indexOf('/static/favicon.ico') != -1 && rdata.indexOf('/static/img/qrCode.png') != -1) || rdata.indexOf('') === 0) { + window.location.href = '/login'; + return; + } + } + + if (callback) callback(rdata); + }); + } + }, + linux_format_param: function (param) { + if (typeof param == 'string') { + var data = {}; + arr = param.split('&'); + var reg = /(^[^=]*)=(.*)/; + for (var i = 0; i < arr.length; i++) { + var tmp = arr[i].match(reg); + if (tmp.length >= 3) data[tmp[1]] = tmp[2] == 'undefined' ? '' : tmp[2]; + } + return data; + } + return param; + }, + win_format_param: function (param) { + if (typeof data == 'object') { + var data = ''; + for (var key in param) { + data += key + '=' + param[key] + '&'; + } + if (data.length > 0) data = data.substr(0, data.length - 1); + return data; + } + return param; + }, + msg: function (config) { + var btnObj = { + title: config.title ? config.title : false, + shadeClose: config.shadeClose ? config.shadeClose : true, + closeBtn: config.closeBtn ? config.closeBtn : 0, + area: config.area ? config.area : 'auto', + scrollbar: true, + shade: 0.3, + }; + if (!config.hasOwnProperty('time')) config.time = 2000; + if (typeof config.msg == 'string' && bt.contains(config.msg, 'ERROR')) config.time = 0; + + if (config.hasOwnProperty('icon')) { + if (typeof config.icon == 'boolean') config.icon = config.icon ? 1 : 2; + } else if (config.hasOwnProperty('status')) { + config.icon = config.status ? 1 : 2; + if (!config.status) { + btnObj.time = 0; + } + } + if (config.icon) btnObj.icon = config.icon; + btnObj.time = config.time; + var msg = ''; + if (config.msg) msg += config.msg; + if (config.msg_error) msg += config.msg_error; + if (config.msg_solve) msg += config.msg_solve; + + layer.msg(msg, btnObj); + }, + confirm: function (config, callback, callback1) { + var btnObj = { + title: config.title ? config.title : false, + time: config.time ? config.time : 0, + shadeClose: config.shadeClose ? config.shadeClose : true, + closeBtn: config.closeBtn ? config.closeBtn : 2, + scrollbar: true, + shade: 0.3, + icon: 3, + area: config.area ? config.area : 'auto', + cancel: config.cancel ? config.cancel : function () {}, + }; + layer.confirm( + config.msg, + btnObj, + function (index) { + if (callback) callback(index); + }, + function (index) { + if (callback1) callback1(index); + } + ); + }, + load: function (msg) { + if (!msg) msg = lan.public.the; + var loadT = layer.msg(msg, { icon: 16, time: 0, shade: [0.3, '#000'] }); + var load = { + form: loadT, + close: function () { + layer.close(load.form); + }, + }; + return load; + }, + open: function (config) { + config.closeBtn = 2; + var loadT = layer.open(config); + var load = { + form: loadT, + close: function () { + layer.close(load.form); + }, + }; + return load; + }, + closeAll: function () { + layer.closeAll(); + }, + check_select: function () { + setTimeout(function () { + var num = $('input[type="checkbox"].check:checked').length; + if (num == 1) { + $('button[batch="true"]').hide(); + $('button[batch="false"]').show(); + } else if (num > 1) { + $('button[batch="true"]').show(); + $('button[batch="false"]').show(); + } else { + $('button[batch="true"]').hide(); + $('button[batch="false"]').hide(); + } + }, 5); + }, + render_help: function (arr) { + var html = '
              '; + for (var i = 0; i < arr.length; i++) { + html += '
            • ' + arr[i] + '
            • '; + } + html += '
            '; + return html; + }, + render_ps: function (item) { + var html = "

            " + item.title + '

            '; + for (var i = 0; i < item.list.length; i++) { + html += '

            ' + item.list[i].title + ':' + item.list[i].val + '

            '; + } + html += '

            '; + return html; + }, + render_table: function (obj, arr, append) { + //渲染表单表格 + var html = ''; + for (var key in arr) { + html += '' + key + ''; + if (typeof arr[key] != 'object') { + html += '' + arr[key] + ''; + } else { + for (var i = 0; i < arr[key].length; i++) { + html += '' + arr[key][i] + ''; + } + } + html += ''; + } + if (append) { + $('#' + obj).append(html); + } else { + $('#' + obj).html(html); + } + }, + + fixed_table: function (name) { + $('#' + name) + .parent() + .bind('scroll', function () { + var scrollTop = this.scrollTop; + $(this) + .find('thead') + .css({ transform: 'translateY(' + scrollTop + 'px)', position: 'relative', 'z-index': '1' }); + }); + }, + render_tab: function (obj, arr) { + var _obj = $('#' + obj).addClass('tab-nav'); + for (var i = 0; i < arr.length; i++) { + var item = arr[i]; + var _tab = $('' + item.title + ''); + if (item.callback) { + _tab.data('callback', item.callback); + _tab.click(function () { + $('#' + obj) + .find('span') + .removeClass('on'); + $(this).addClass('on'); + var _contents = $('#' + obj).next('.tab-con'); + _contents.html(''); + $(this).data('callback')(_contents); + }); + } + _obj.append(_tab); + } + }, + render_form_line: function (item, bs, form) { + var clicks = [], + _html = '', + _hide = '', + is_title_css = ' ml0'; + if (!bs) bs = ''; + if (item.title) { + _html += '' + item.title + ''; + is_title_css = ''; + } + _html += "
            "; + + var _name = item.name; + var _placeholder = item.placeholder; + if (item.items && item.type != 'select') { + for (var x = 0; x < item.items.length; x++) { + var _obj = item.items[x]; + if (!_name && !_obj.name) { + alert(lan.public_backup.name_err); + return; + } + if (_obj.hide) continue; + if (_obj.name) _name = _obj.name; + if (_obj.placeholder) _placeholder = _obj.placeholder; + if (_obj.title) _html += '
            ' + _obj.title + ' '; + var _add_class = _obj.add_class ? ' ' + _obj.add_class : ''; + switch (_obj.type) { + case 'select': + var _width = _obj.width ? _obj.width : '100px'; + _html += ''; + break; + case 'textarea': + var _width = _obj.width ? _obj.width : '330px', + _height = _obj.height ? _obj.height : '100px'; + _html += + ''; + if (_placeholder) _html += '
            ' + _placeholder + '
            '; + break; + case 'button': + var _width = _obj.width ? _obj.width : '330px'; + _html += "'; + break; + case 'radio': + var _v = _obj.value === true ? 'checked' : ''; + _html += + ''; + break; + case 'radio_group': + $.each(_obj.list, function (index, item) { + var id = _name + '_' + index, + _v = _obj.value === item.value ? 'checked' : ''; + _html += + '
            '; + }); + break; + case 'checkbox': + var _v = _obj.value === true ? 'checked' : ''; + _html += + ''; + break; + case 'number': + var _width = _obj.width ? _obj.width : '330px'; + _html += + ""; + _html += _obj.unit ? _obj.unit : ''; + break; + case 'password': + var _width = _obj.width ? _obj.width : '330px'; + _html += + ""; + break; + case 'div': + var _width = _obj.width ? _obj.width : '330px', + _height = _obj.height ? _obj.height : '100px'; + _html += + '
            ' + + (_obj.value ? _obj.value : '') + + '
            '; + if (_placeholder) _html += '
            ' + _placeholder + '
            '; + break; + case 'switch': + _html += + '
            \ + \ + \ +
            '; + break; + case 'html': + _html += _obj.html; + break; + default: + var _width = _obj.width ? _obj.width : '330px'; + _html += + ""; + break; + } + if (_obj.title) _html += '
            '; + if (_obj.callback) clicks.push({ bind: _name + bs, callback: _obj.callback }); + if (_obj.event) { + _html += ''; + if (_obj.event.callback) clicks.push({ bind: 'icon_' + _name + bs, callback: _obj.event.callback }); + } + if (_obj.ps) _html += " " + _obj.ps + ''; + if (_obj.ps_help) _html += "?"; + } + if (item.ps) _html += " " + item.ps + ''; + if (item.ps_help) _html += "?"; + } else { + switch (item.type) { + case 'select': + var _width = item.width ? item.width : '100px'; + _html += ''; + break; + case 'button': + var _width = item.width ? item.width : '330px'; + _html += "'; + break; + case 'number': + var _width = item.width ? item.width : '330px'; + _html += + ""; + break; + case 'checkbox': + var _v = item.value === true ? 'checked' : ''; + _html += + ''; + break; + case 'password': + var _width = item.width ? item.width : '330px'; + _html += + ""; + break; + case 'textarea': + var _width = item.width ? item.width : '330px'; + var _height = item.height ? item.height : '100px'; + _html += + ''; + if (_placeholder) _html += '
            ' + _placeholder + '
            '; + break; + default: + var _width = item.width ? item.width : '330px'; + + _html += + ""; + break; + } + if (item.callback) clicks.push({ bind: _name + bs, callback: item.callback }); + if (item.ps) _html += " " + item.ps + ''; + if (item.ps_help) _html += "?"; + } + _html += '
            '; + if (!item.class) item.class = ''; + if (item.hide) _hide = 'style="display:none;"'; + _html = '
            ' + _html + '
            '; + + if (form) { + form.append(_html); + bt.render_clicks(clicks); + } + return { html: _html, clicks: clicks, data: item }; + }, + render_form: function (data, callback) { + if (data) { + var bs = '_' + bt.get_random(6); + var _form = $("
            "); + var _lines = data.list; + var clicks = []; + for (var i = 0; i < _lines.length; i++) { + var _obj = _lines[i]; + if (_obj.hasOwnProperty('html')) { + _form.append(_obj.html); + } else { + var rRet = bt.render_form_line(_obj, bs); + for (var s = 0; s < rRet.clicks.length; s++) clicks.push(rRet.clicks[s]); + _form.append(rRet.html); + } + } + + var _btn_html = ''; + for (var i = 0; i < data.btns.length; i++) { + var item = data.btns[i]; + var css = item.css ? item.css : 'btn-danger'; + _btn_html += "'; + clicks.push({ bind: item.name + bs, callback: item.callback }); + } + _form.append("
            " + _btn_html + '
            '); + var loadOpen = bt.open({ + type: 1, + skin: data.skin, + area: data.area, + title: data.title, + closeBtn: 2, + content: _form.prop('outerHTML'), + end: data.end ? data.end : false, + success: function () { + if (data.success) data.success(); + }, + }); + setTimeout(function () { + bt.render_clicks(clicks, loadOpen, callback); + }, 100); + } + return bs; + }, + render_clicks: function (clicks, loadOpen, callback) { + for (var i = 0; i < clicks.length; i++) { + var obj = clicks[i]; + + var btn = $('.' + obj.bind); + btn.data('item', obj); + btn.data('load', loadOpen); + btn.data('callback', callback); + + switch (btn.prop('tagName')) { + case 'SPAN': + btn.click(function () { + var _obj = $(this).data('item'); + _obj.callback($(this).attr('data-id')); + }); + break; + case 'SELECT': + btn.change(function () { + var _obj = $(this).data('item'); + _obj.callback($(this)); + }); + break; + case 'TEXTAREA': + case 'INPUT': + case 'BUTTON': + if (btn.prop('tagName') == 'BUTTON' || btn.attr('type') == 'checkbox') { + btn.click(function () { + var _obj = $(this).data('item'); + var load = $(this).data('load'); + var _callback = $(this).data('callback'); + var parent = $(this).parents('.bt-form'); + + if (_obj.callback) { + var data = {}; + parent.find('*').each(function (index, _this) { + var _name = $(_this).attr('name'); + + if (_name) { + if ($(_this).attr('type') == 'checkbox' || $(_this).attr('type') == 'radio') { + data[_name] = $(_this).prop('checked'); + } else { + data[_name] = $(_this).val(); + } + } + }); + _obj.callback(data, load, function (rdata) { + if (_callback) _callback(rdata); + }); + } else { + load.close(); + } + }); + } else { + if (btn.attr('type') == 'radio') { + btn.click(function () { + var _obj = $(this).data('item'); + _obj.callback($(this)); + }); + } else { + btn.on('input', function () { + var _obj = $(this).data('item'); + _obj.callback($(this)); + }); + } + } + break; + } + } + }, + render: function ( + obj //columns 行 + ) { + if (obj.columns) { + var checks = {}; + $(obj.table).html(''); + var thead = ''; + for (var h = 0; h < obj.columns.length; h++) { + var item = obj.columns[h]; + if (item) { + thead += ''; + } + if (item.help) thead += '?'; + + thead += ''; + } + } + thead += ''; + var _tab = $(obj.table).append(thead); + if (obj.data.length > 0) { + for (var i = 0; i < obj.data.length; i++) { + var val = obj.data[i]; + var tr = $(''); + for (var h = 0; h < obj.columns.length; h++) { + var item = obj.columns[h]; + if (item) { + var _val = val[item.field]; + if (typeof _val == 'string') _val = _val.replace(/\\/g, ''); + if (item.hasOwnProperty('templet')) _val = item.templet(val); + if (item.type == 'checkbox') _val = ''; + var td = ''); + tr.data('item', val); + _tab.append(tr); + } + } + } + } else { + _tab.append("" + obj.empty ? obj.empty : lan.bt.no_data + ''); + } + $(obj.table) + .find('.check') + .click(function () { + var checked = $(this).prop('checked'); + if ($(this).parent().prop('tagName') == 'TH') { + $('.check').prop('checked', checked ? 'checked' : ''); + } + }); + var asc = 'glyphicon-triangle-top'; + var desc = 'glyphicon-triangle-bottom'; + + var orderby = bt.get_cookie('order'); + if (orderby != undefined) { + var arrys = orderby.split(' '); + if (arrys.length == 2) { + if (arrys[1] == 'asc') { + $(obj.table) + .find('th span[data-id="' + arrys[0] + '"]') + .removeClass(desc) + .addClass(asc); + } else { + $(obj.table) + .find('th span[data-id="' + arrys[0] + '"]') + .removeClass(asc) + .addClass(desc); + } + } + } + + $(obj.table) + .find('th') + .data('checks', checks) + .click(function () { + var _th = $(this); + var _checks = _th.data('checks'); + var _span = _th.find('span'); + if (_span.length > 0) { + var or = _span.attr('data-id'); + if (_span.hasClass(asc)) { + bt.set_cookie('order', or + ' desc'); + $(obj.table) + .find('th span[data-id="' + or + '"]') + .removeClass(asc) + .addClass(desc); + _checks[or](); + } else if (_span.hasClass(desc)) { + bt.set_cookie('order', or + ' asc'); + $(obj.table) + .find('th span[data-id="' + arrys[0] + '"]') + .removeClass(desc) + .addClass(asc); + _checks[or](); + } + } + }); + } + return _tab; + }, + // ACE编辑配置文件 + aceEditor: function (obj) { + var aEditor = { + ACE: ace.edit(obj.el, { + theme: obj.theme ? obj.theme : 'ace/theme/chrome', // 主题 + mode: 'ace/mode/' + (obj.mode || 'nginx'), // 语言类型 + wrap: true, + showInvisibles: false, + showPrintMargin: false, + showFoldWidgets: false, + useSoftTabs: true, + tabSize: 2, + showPrintMargin: false, + readOnly: false, + }), + path: obj.path, + content: '', + saveCallback: obj.saveCallback, + }, + _this = this; + $('#' + obj.el).css('fontSize', '12px'); + aEditor.ACE.commands.addCommand({ + name: '保存文件', + bindKey: { win: 'Ctrl-S', mac: 'Command-S' }, + exec: function (editor) { + _this.saveEditor(aEditor, aEditor.saveCallback); + }, + readOnly: false, // 如果不需要使用只读模式,这里设置false + }); + if (obj.path != undefined) { + var loadT = layer.msg(lan.soft.get_config, { icon: 16, time: 0, shade: [0.3, '#000'] }); + bt.send('GetFileBody', 'files/GetFileBody', { path: obj.path }, function (res) { + layer.close(loadT); + if (!res.status) { + bt.msg(res); + return false; + } + aEditor.ACE.setValue(res.data); //设置配置文件内容 + aEditor.ACE.moveCursorTo(0, 0); //设置文件光标位置 + aEditor.ACE.resize(); + }); + } else if (obj.content != undefined) { + aEditor.ACE.setValue(obj.content); + aEditor.ACE.moveCursorTo(0, 0); //设置文件光标位置 + aEditor.ACE.resize(); + } + return aEditor; + }, + // 保存编辑器文件 + saveEditor: function (ace) { + if (!ace.saveCallback) { + var loadT = bt.load(lan.soft.the_save); + bt.send('SaveFileBody', 'files/SaveFileBody', { data: ace.ACE.getValue(), path: ace.path, encoding: 'utf-8' }, function (rdata) { + loadT.close(); + bt.msg(rdata); + }); + } else { + ace.saveCallback(ace.ACE.getValue()); + } + }, + /** + * @description 遍历数组和对象 + * @param {Array|Object} obj 遍历数组|对象 + * @param {Function} fn 遍历对象或数组 + * @return 当前对象 + */ + each: function (obj, fn) { + var key, + that = this; + if (typeof fn !== 'function') return that; + obj = obj || []; + if (obj.constructor === Object) { + for (key in obj) { + if (fn.call(obj[key], key, obj[key])) break; + } + } else { + for (key = 0; key < obj.length; key++) { + if (fn.call(obj[key], key, obj[key])) break; + } + } + return that; + }, + /** + * @description 普通提示弹窗 + * @param {Object} config 弹窗对象 {title:标题, msg:提示内容} + * @param {function} callback 确认回调函数 + * @param {function} callback1 取消回调函数 + */ + simple_confirm: function (config, callback, callback1) { + layer.open({ + type: 1, + title: config.title, + area: '430px', + closeBtn: 2, + shadeClose: false, + btn: [lan['public'].ok, lan['public'].cancel], + content: + '
            \ +
            \ + \ +
            ' + + config.msg + + '
            \ +
            \ +
            ', + yes: function (index, layero) { + if (callback && typeof callback(index) === 'undefined') layer.close(index); + }, + btn2: function (index) { + //取消返回回调 + if (callback1 && typeof callback1(index) === 'undefined') layer.close(index); + }, + cancel: function (index) { + //取消返回回调 + if (callback1 && typeof callback1(index) === 'undefined') layer.close(index); + }, + }); + }, + /** + * @description 需求反馈弹窗 + * @param {Object} param 配置对象 {title:标题, placeholder:反馈问题pl(可带标签),recover:input框下方提示语, key:反馈问题key, proType:产品类型} + */ + openFeedback: function (param) { + // 需求反馈 + var openFeed = bt_tools.open({ + area: ['570px', '400px'], + btn: false, + content: + '
            \ +
            \ + ' + + param.title + + ' \ +
            \ +
            \ +
            \ +
            \ +
            \ +
            \ +
            ', + success: function (that) { + var id = 'x66ed9v07MjVjYjczNTUyMDE0Le8BEdl'; + bt_tools.send( + { url: '/config?action=get_nps_new', data: { product_type: param.proType } }, + function (ress) { + //请求回调 + console.log(ress); + if (ress.res) { + id = ress.res[0].id; + } + }, + { load: 'Loading...', verify: false } + ); + //打开弹窗后执行的事件 + that.find('.layui-layer-title').remove(); + bt_tools.form({ + el: '#feedForm', + form: [ + { + group: { + type: 'textarea', + name: 'feed', + style: { + width: '500px', + 'min-width': '500px', + 'min-height': '130px', + 'line-height': '22px', + 'padding-top': '10px', + resize: 'none', + }, + tips: { + //使用hover的方式显示提示 + text: param.placeholder, + style: { top: '126px', left: '50px' }, + }, + }, + }, + { + group: { + name: 'tips', + type: 'other', + boxcontent: '
            ' + param.recover + '
            ', + }, + }, + { + group: { + type: 'button', + size: '', + name: 'submitForm', + class: 'feedBtn', + style: 'margin:10px auto 0;padding:6px 40px;', + title: 'Submit', + event: function (formData, element, that) { + // 触发submit + if (formData.feed == '') { + return bt.msg({ status: false, msg: 'Please fill in the feedback' }); + } + var config = {}; + config[id] = formData.feed; + bt_tools.send( + { url: 'config?action=write_nps_new', data: { questions: JSON.stringify(config), rate: 0, product_type: param.proType } }, + function (ress) { + if (ress.status) { + openFeed.close(); + layer.open({ + title: false, + btn: false, + shadeClose: true, + shade: 0.1, + closeBtn: 0, + skin: 'qa_thank_dialog', + area: '230px', + content: + '

            Thank you for your participation!

            ', + success: function (layero, index) { + $(layero).find('.layui-layer-content').css({ padding: '0', 'border-radius': '5px' }); + $(layero).css({ 'border-radius': '5px', 'min-width': '230px' }); + + setTimeout(function () { + layer.close(index); + }, 3000); + }, + }); + } + }, + 'submit feedback' + ); + }, + }, + }, + ], + }); + }, + yes: function () { + //点击确定时,如果btn:false,当前事件将无法使用 + }, + cancel: function () { + //点击右上角关闭时,如果btn:false,当前事件将无法使用 + }, + }); + }, +}; + +bt.pub = { + get_data: function (data, callback, hide) { + if (!hide) var loading = bt.load(lan.public.the); + bt.send('getData', 'data/getData', data, function (rdata) { + if (loading) loading.close(); + if (callback) callback(rdata); + }); + }, + set_data_by_key: function (tab, key, obj) { + var _span = $(obj); + var _input = $(""); + _span.hide().after(_input); + _input.focus(); + _input.blur(function () { + var item = $(this).parents('tr').data('item'); + var _txt = $(this); + var data = { table: tab, id: item.id }; + data[key] = _txt.val(); + bt.pub.set_data_ps(data, function (rdata) { + if (rdata.status) { + _span.text(_txt.val()); + _span.show(); + _txt.remove(); + } + }); + }); + _input.keyup(function () { + if (event.keyCode == 13) { + _input.trigger('blur'); + } + }); + }, + set_data_ps: function (data, callback) { + bt.send('setPs', 'data/setPs', data, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_server_status: function (serverName, type) { + if (bt.contains(serverName, 'php-')) { + serverName = 'php-fpm-' + serverName.replace('php-', '').replace('.', ''); + } + if (serverName == 'pureftpd') serverName = 'pure-ftpd'; + if (serverName == 'mysql') serverName = 'mysqld'; + serverName = serverName.replace('_soft', ''); + var data = 'name=' + serverName + '&type=' + type; + var msg = lan.bt[type]; + var typeName = ''; + switch (type) { + case 'stop': + typeName = lan.public_backup.stop; + break; + case 'restart': + typeName = lan.public_backup.restart; + break; + case 'reload': + typeName = lan.public_backup.reload; + break; + } + bt.confirm({ msg: lan.get('service_confirm', [msg, serverName]), title: typeName + serverName + lan.public_backup.server }, function () { + var load = bt.load(lan.get('service_the', [msg, serverName])); + bt.send('system', 'system/ServiceAdmin', data, function (rdata) { + load.close(); + var f = rdata.status ? lan.get('service_ok', [serverName, msg]) : lan.get('service_err', [serverName, msg]); + bt.msg({ msg: f, icon: rdata.status }); + + if (type != 'reload' && rdata.status) { + setTimeout(function () { + window.location.reload(); + }, 1000); + } + if (!rdata.status) { + bt.msg(rdata); + } + }); + }); + }, + set_ftp_logs: function (type) { + var serverName = 'pure-ftpd'; + var data = 'exec_name=' + type; + var typeName = 'enabling '; + var TypeName = 'Enabling '; + switch (type) { + case 'stop': + typeName = 'disabling '; + TypeName = 'Disabling '; + break; + } + var status = type == 'stop' ? false : true; + layer.confirm( + 'After ' + + typeName + + 'pure-ftpd Logs management,' + + (status ? 'all login and operation records of FTP users will be recorded.' : 'it will no longer be possible to record all login and operation records of FTP users. ') + + ' Do you want to proceed?', + { + title: TypeName + serverName + ' logs management', + closeBtn: 2, + icon: 3, + cancel: function () { + $('#isFtplog').prop('checked', !status); + }, + }, + function () { + var load = bt.load(TypeName + 'Pure-FTPd logs management, please wait...'); + bt.send('ftp', 'ftp/set_ftp_logs', data, function (rdata) { + load.close(); + bt.msg(rdata); + $('.bt-soft-menu p').eq(3).click(); + }); + }, + function () { + $('#isFtplog').prop('checked', !status); + } + ); + }, + get_ftp_logs: function (callback) { + bt.send('ftp', 'ftp/set_ftp_logs', { exec_name: 'getlog' }, function (res) { + var _status = res.msg === 'start' ? true : false; + if (callback) callback(_status); + }); + }, + set_server_status_by: function (data, callback) { + bt.send('system', 'system/ServiceAdmin', data, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_task_count: function (callback) { + bt.send('GetTaskCount', 'ajax/GetTaskCount', {}, function (rdata) { + $('.task').text(rdata); + if (callback) callback(rdata); + }); + }, + check_install: function (callback) { + bt.send('CheckInstalled', 'ajax/CheckInstalled', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_user_info: function (callback) { + var loading = bt.load(); + bt.send('GetUserInfo', 'ssl/GetUserInfo', {}, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + show_hide_pass: function (obj) { + var a = 'glyphicon-eye-open'; + var b = 'glyphicon-eye-close'; + + if ($(obj).hasClass(a)) { + $(obj).removeClass(a).addClass(b); + $(obj).prev().text($(obj).prev().attr('data-pw')); + } else { + $(obj).removeClass(b).addClass(a); + $(obj).prev().text('**********'); + } + }, + copy_pass: function (password) { + var clipboard = new ClipboardJS('#bt_copys'); + clipboard.on('success', function (e) { + bt.msg({ msg: lan.public_backup.cp_success, icon: 1 }); + }); + + clipboard.on('error', function (e) { + bt.msg({ msg: lan.public_backup.cp_fail, icon: 2 }); + }); + $('#bt_copys').attr('data-clipboard-text', password); + $('#bt_copys').click(); + }, + login_btname: function (username, password, callback) { + var loadT = bt.load(lan.config.token_get); + bt.send('GetToken', 'ssl/GetToken', 'username=' + username + '&password=' + password, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (rdata.status) { + if (callback) callback(rdata); + } + }); + }, + bind_btname: function (callback) { + layer.open({ + type: 1, + title: lan.public_backup.bind_bt_account, + area: ['420px', '360px'], + closeBtn: 2, + shadeClose: false, + content: + '

            ' + + lan.public_backup.bind_bt_account + + '

            ' + + lan.public_backup.no_account + + '

            ', + }); + setTimeout(function () { + $('.login-button').click(function () { + p1 = $('#p1').val(); + p2 = $('#p2').val(); + var loadT = bt.load(lan.config.token_get); + bt.send('GetToken', 'ssl/GetToken', 'username=' + p1 + '&password=' + p2, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (rdata.status) { + if (callback) { + layer.closeAll(); + callback(rdata); + } else { + window.location.reload(); + } + $("input[name='btusername']").val(p1); + } + }); + }); + }, 100); + }, + unbind_bt: function () { + var name = $("input[name='btusername']").val(); + bt.confirm({ msg: lan.config.binding_un_msg, title: lan.config.binding_un_title }, function () { + bt.send('DelToken', 'ssl/DelToken', {}, function (rdata) { + bt.msg(rdata); + $("input[name='btusername']").val(''); + }); + }); + }, + get_menm: function (callback) { + var loading = bt.load(); + bt.send('GetMemInfo', 'system/GetMemInfo', {}, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + on_edit_file: function (type, fileName) { + if (type != 0) { + var l = $('#PathPlace input').val(); + var body = encodeURIComponent($('#textBody').val()); + var encoding = $('select[name=encoding]').val(); + var loadT = bt.load(lan.bt.save_file); + bt.send('SaveFileBody', 'files/SaveFileBody', 'data=' + body + '&path=' + fileName + '&encoding=' + encoding, function (rdata) { + if (type == 1) loadT.close(); + bt.msg(rdata); + }); + return; + } + var loading = bt.load(lan.bt.read_file); + ext = bt.get_file_ext(fileName); + doctype = ''; + switch (ext) { + case 'html': + var mixedMode = { + name: 'htmlmixed', + scriptTypes: [ + { matches: /\/x-handlebars-template|\/x-mustache/i, mode: null }, + { matches: /(text|application)\/(x-)?vb(a|script)/i, mode: 'vbscript' }, + ], + }; + doctype = mixedMode; + break; + case 'htm': + var mixedMode = { + name: 'htmlmixed', + scriptTypes: [ + { matches: /\/x-handlebars-template|\/x-mustache/i, mode: null }, + { matches: /(text|application)\/(x-)?vb(a|script)/i, mode: 'vbscript' }, + ], + }; + doctype = mixedMode; + break; + case 'js': + doctype = 'text/javascript'; + break; + case 'json': + doctype = 'application/ld+json'; + break; + case 'css': + doctype = 'text/css'; + break; + case 'php': + doctype = 'application/x-httpd-php'; + break; + case 'tpl': + doctype = 'application/x-httpd-php'; + break; + case 'xml': + doctype = 'application/xml'; + break; + case 'sql': + doctype = 'text/x-sql'; + break; + case 'conf': + doctype = 'text/x-nginx-conf'; + break; + default: + var mixedMode = { + name: 'htmlmixed', + scriptTypes: [ + { matches: /\/x-handlebars-template|\/x-mustache/i, mode: null }, + { matches: /(text|application)\/(x-)?vb(a|script)/i, mode: 'vbscript' }, + ], + }; + doctype = mixedMode; + break; + } + bt.send('GetFileBody', 'files/GetFileBody', 'path=' + fileName, function (rdata) { + if (!rdata.status) { + bt.msg({ msg: rdata.msg, icon: 5 }); + return; + } + loading.close(); + var u = ['utf-8', 'GBK', 'GB2312', 'BIG5']; + var n = ''; + var m = ''; + var o = ''; + for (var p = 0; p < u.length; p++) { + m = rdata.encoding == u[p] ? 'selected' : ''; + n += ''; + } + var r = bt.open({ + type: 1, + shift: 5, + closeBtn: 1, + //maxmin: true, + area: ['90%', '90%'], + shade: false, + title: lan.bt.edit_title + '[' + fileName + ']', + content: + '

            ' + + lan.bt.edit_ps + + '

            \ +
            \ +
            ' + + lan.public_backup.cret_format + + '
            \ + \ +
            \ +
            \ + \ +
            \ +
            \ + \ + '; + bt.open({ + type: 1, + area: '600px', + title: lan.public_backup.custom_panel_set, + closeBtn: 2, + shift: 5, + shadeClose: false, + content: certBody, + }); + + $('#btn_submit').click(function () { + key = $('#key').val(); + csr = $('#csr').val(); + _this.set_panel_ssl({ privateKey: key, certPem: csr }); + }); + }); + }, + set_panel_ssl: function (data, callback) { + var loadT = bt.load(lan.config.ssl_msg); + bt.send('SavePanelSSL', 'config/SavePanelSSL', data, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + set_username: function (type) { + if (type == 1) { + if (p1 == '' || p1.length < 3) { + bt.msg({ msg: lan.bt.user_len, icon: 2 }); + return; + } + if (p1 != p2) { + bt.msg({ msg: lan.bt.user_err_re, icon: 2 }); + return; + } + var checks = ['admin', 'root', 'admin123', '123456']; + if ($.inArray(p1, checks)) { + bt.msg({ msg: lan.public_backup.usually_user_ban, icon: 2 }); + return; + } + bt.send('setUsername', 'config/setUsername', { username1: p1, username2: p2 }, function (rdata) { + if (rdata.status) { + layer.closeAll(); + $("input[name='username_']").val(p1); + } + bt.msg(rdata); + }); + return; + } + bt.open({ + type: 1, + area: '290px', + title: lan.bt.user_title, + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "
            " + + lan.bt.user + + "
            " + + lan.bt.pass_re + + "
            ', + }); + }, + set_password: function (type) { + if (type == 1) { + p1 = $('#p1').val(); + p2 = $('#p2').val(); + if (p1 == '' || p1.length < 8) { + bt.msg({ msg: lan.bt.pass_err_len, icon: 2 }); + return; + } + + //准备弱口令匹配元素 + var checks = ['admin888', '123123123', '12345678', '45678910', '87654321', 'asdfghjkl', 'password', 'qwerqwer']; + pchecks = 'abcdefghijklmnopqrstuvwxyz1234567890'; + for (var i = 0; i < pchecks.length; i++) { + checks.push(pchecks[i] + pchecks[i] + pchecks[i] + pchecks[i] + pchecks[i] + pchecks[i] + pchecks[i] + pchecks[i]); + } + + //检查弱口令 + cps = p1.toLowerCase(); + var isError = ''; + for (var i = 0; i < checks.length; i++) { + if (cps == checks[i]) { + isError += '[' + checks[i] + '] '; + } + } + if (isError != '') { + bt.msg({ msg: lan.bt.pass_err + isError, icon: 2 }); + return; + } + + if (p1 != p2) { + bt.msg({ msg: lan.bt.pass_err_re, icon: 2 }); + return; + } + bt.send('setPassword', 'config/setPassword', { password1: p1, password2: p2 }, function (rdata) { + layer.closeAll(); + bt.msg(rdata); + }); + return; + } + layer.open({ + type: 1, + area: '290px', + title: lan.bt.pass_title, + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "
            " + + lan.public.pass + + "
            " + + lan.bt.pass_re + + "
            " + + lan.bt.pass_rep_btn + + "
            ', + }); + }, +}; + +bt.system = { + get_total: function (callback) { + bt.send('GetSystemTotal', 'system/GetSystemTotal', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_net: function (callback) { + bt.send('GetNetWork', 'system/GetNetWork', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_disk_list: function (callback) { + bt.send('GetDiskInfo', 'system/GetDiskInfo', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + re_memory: function (callback) { + bt.send('ReMemory', 'system/ReMemory', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + check_update: function (callback, check) { + var data = {}; + if (check == undefined) data = { check: true }; + if (check === false) data = {}; + if (check) var load = bt.load(lan.index.update_get); + bt.send('UpdatePanel', 'ajax/UpdatePanel', data, function (rdata) { + if (check) load.close(); + if (callback) callback(rdata); + }); + }, + to_update: function (callback) { + var load = bt.load(lan.index.update_the); + bt.send('UpdatePanel', 'ajax/UpdatePanel', { toUpdate: 'yes' }, function (rdata) { + load.close(); + if (callback) callback(rdata); + }); + }, + reload_panel: function (callback) { + bt.send('ReWeb', 'system/ReWeb', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + rep_panel: function (callback) { + var loading = bt.load(lan.index.rep_panel_the); + $.ajax({ + type: 'POST', + url: 'system?action=RepPanel', + error: function (err) { + setTimeout(() => { + loading.close(); + bt.system.reload_panel(function () { + location.reload(); + }); + }, 1000 * 60 * 5); + }, + success: function (rdata) { + loading.close(); + if (rdata) { + if (callback) callback({ status: rdata, msg: lan.index.rep_panel_ok }); + bt.system.reload_panel(); + } + }, + }); + }, + get_warning: function (callback) { + bt.send('GetWarning', 'ajax/GetWarning', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + root_reload: function (callback) { + bt.send('RestartServer', 'system/RestartServer', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, +}; + +bt.control = { + get_status: function (callback) { + loading = bt.load(lan.public.read); + bt.send('GetControl', 'control/SetControl', { type: 1 }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_control: function (type, day, callback) { + loadT = bt.load(lan.public.the); + bt.send('SetControl', 'config/SetControl', { type: type, day: day }, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + clear_control: function (callback) { + bt.confirm({ msg: lan.control.close_log_msg, title: lan.control.close_log }, function () { + loadT = bt.load(lan.public.the); + bt.send('SetControl', 'config/SetControl', { type: 'del' }, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }); + }, + get_data: function (type, start, end, callback) { + action = ''; + switch (type) { + case 'cpu': //cpu和内存一起获取 + action = 'GetCpuIo'; + break; + case 'disk': + action = 'GetDiskIo'; + break; + case 'net': + action = 'GetNetWorkIo'; + break; + case 'load': + action = 'get_load_average'; + break; + } + if (!action) bt.msg(lan.get('lack_param', 'type')); + bt.send(action, 'ajax/' + action, { start: start, end: end }, function (rdata) { + if (callback) callback(rdata, type); + }); + }, + format_option: function (obj, type) { + option = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + }, + formatter: obj.formatter, + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: obj.tData, + axisLine: { + lineStyle: { + color: '#666', + }, + }, + }, + yAxis: { + type: 'value', + name: obj.unit, + boundaryGap: [0, '100%'], + min: 0, + splitLine: { + lineStyle: { + color: '#ddd', + }, + }, + axisLine: { + lineStyle: { + color: '#666', + }, + }, + }, + dataZoom: [ + { + type: 'inside', + start: 0, + zoomLock: true, + }, + { + start: 0, + handleIcon: + 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z', + handleSize: '80%', + handleStyle: { + color: '#fff', + shadowBlur: 3, + shadowColor: 'rgba(0, 0, 0, 0.6)', + shadowOffsetX: 2, + shadowOffsetY: 2, + }, + }, + ], + series: [], + }; + if (obj.legend) option.legend = obj.legend; + if (obj.dataZoom) option.dataZoom = obj.dataZoom; + + for (var i = 0; i < obj.list.length; i++) { + var item = obj.list[i]; + series = { + name: item.name, + type: item.type ? item.type : 'line', + smooth: item.smooth ? item.smooth : true, + symbol: item.symbol ? item.symbol : 'none', + showSymbol: item.showSymbol ? item.showSymbol : false, + sampling: item.sampling ? item.sampling : 'average', + areaStyle: item.areaStyle ? item.areaStyle : {}, + lineStyle: item.lineStyle ? item.lineStyle : {}, + itemStyle: item.itemStyle ? item.itemStyle : { normal: { color: 'rgb(0, 153, 238)' } }, + symbolSize: 6, + symbol: 'circle', + data: item.data, + }; + option.series.push(series); + } + return option; + }, +}; + +bt.firewall = { + get_log_list: function (page, search, callback) { + if (page == undefined) page = 1; + search = search == undefined ? '' : search; + var order = bt.get_cookie('order') ? '&order=' + bt.get_cookie('order') : ''; + + var data = 'tojs=firewall.get_log_list&table=logs&limit=10&p=' + page + '&search=' + search + order; + bt.pub.get_data(data, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_list: function (page, search, callback) { + if (page == undefined) page = 1; + search = search == undefined ? '' : search; + var order = bt.get_cookie('order') ? '&order=' + bt.get_cookie('order') : ''; + + var data = 'tojs=firewall.get_list&table=firewall&limit=10&p=' + page + '&search=' + search + order; + bt.pub.get_data(data, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_logs_size: function (callback) { + if (bt.os == 'Linux') { + bt.files.get_dir_size('/www/wwwlogs', function (rdata) { + if (callback) callback(rdata); + }); + } + }, + get_ssh_info: function (callback) { + bt.send('GetSshInfo', 'firewall/GetSshInfo', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_mstsc: function (port, callback) { + bt.confirm({ msg: lan.firewall.ssh_port_msg, title: lan.firewall.ssh_port_title }, function () { + loading = bt.load(lan.public.the); + bt.send('SetSshPort', 'firewall/SetSshPort', { port: port }, function (rdata) { + loading.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }); + }, + ping: function (status, callback) { + var msg = status == 0 ? lan.firewall.ping_msg : lan.firewall.ping_un_msg; + layer.confirm( + msg, + { + closeBtn: 2, + title: lan.firewall.ping_title, + cancel: function () { + if (callback) callback(-1); //取消 + }, + }, + function () { + loading = bt.load(lan.public.the); + bt.send('SetPing', 'firewall/SetPing', { status: status }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + function () { + if (callback) callback(-1); //关闭 + } + ); + }, + set_mstsc_status: function (status, callback) { + var msg = status == 1 ? lan.firewall.ssh_off_msg : lan.firewall.ssh_on_msg; + layer.confirm( + msg, + { + icon: 0, + closeBtn: 2, + title: lan.public.warning, + cancel: function () { + if (callback) callback(-1); //取消 + }, + }, + function () { + loading = bt.load(lan.public.the); + bt.send('SetSshStatus', 'firewall/SetSshStatus', { status: status }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + function () { + if (callback) callback(-1); //关闭 + } + ); + }, + add_accept_port: function (type, port, ps, callback) { + var action = 'AddDropAddress'; + if (type == 'port') { + ports = port.split(':'); + if (port.indexOf('-') != -1) ports = port.split('-'); + for (var i = 0; i < ports.length; i++) { + if (!bt.check_port(ports[i])) { + layer.msg(lan.firewall.port_err, { icon: 5 }); + return; + } + } + action = 'AddAcceptPort'; + } + + // if (ps.length < 1) { + // layer.msg(lan.firewall.ps_err, { icon: 2 }); + // return -1; + // } + loading = bt.load(); + bt.send(action, 'firewall/' + action, { port: port, type: type, ps: ps }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + del_accept_port: function (id, port, callback) { + var action = 'DelDropAddress'; + if (port.indexOf('.') == -1) { + action = 'DelAcceptPort'; + } + bt.confirm({ msg: lan.get('confirm_del', [port]), title: lan.firewall.del_title }, function (index) { + var loadT = bt.load(lan.public.the_del); + bt.send(action, 'firewall/' + action, { id: id, port: port }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }); + }, + clear_logs_files: function (callback) { + var loadT = bt.load(lan.firewall.close_the); + bt.send('CloseLogs', 'files/CloseLogs', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + clear_logs: function (callback) { + bt.confirm({ msg: lan.firewall.close_log_msg, title: lan.firewall.close_log }, function () { + var loadT = bt.load(lan.firewall.close_the); + bt.send('delClose', 'ajax/delClose', {}, function (rdata) { + loadT.close(); + if (callback) { + callback(rdata); + } else { + bt.msg(rdata); + } + }); + }); + }, +}; + +bt.soft = { + SSL_flag: false, + pub: { + wxpayTimeId: 0, + }, + php: { + get_config: function (version, callback) { + //获取禁用函数,扩展列表 + // var loading = bt.load(); + bt.send('GetPHPConfig', 'ajax/GetPHPConfig', { version: version }, function (rdata) { + // loading.close(); + if (callback) callback(rdata); + }); + }, + get_limit_config: function (version, callback) { + //获取超时限制,上传限制 + var loading = bt.load(); + bt.send('get_php_config', 'config/get_php_config', { version: version }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_php_config: function (version, callback) { + var loading = bt.load(); + bt.send('GetPHPConf', 'config/GetPHPConf', { version: version }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + install_php_lib: function (version, name, title, callback) { + bt.confirm({ msg: lan.soft.php_ext_install_confirm.replace('{1}', name), title: lan.public_backup.install + '【' + name + '】' }, function () { + name = name.toLowerCase(); + var loadT = bt.load(lan.soft.add_install); + bt.send('InstallSoft', 'files/InstallSoft', { name: name, version: version, type: '1' }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + fly('bi-btn'); + }); + }, + un_install_php_lib: function (version, name, title, callback) { + bt.confirm({ msg: lan.soft.php_ext_uninstall_confirm.replace('{1}', name), title: lan.public_backup.uninstall + '【' + name + '】' }, function () { + name = name.toLowerCase(); + var data = 'name=' + name + '&version=' + version; + var loadT = bt.load(); + bt.send('UninstallSoft', 'files/UninstallSoft', { name: name, version: version }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }); + }, + set_upload_max: function (version, max, callback) { + var loadT = bt.load(lan.soft.the_save); + bt.send('setPHPMaxSize', 'config/setPHPMaxSize', { version: version, max: max }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + set_php_timeout: function (version, time, callback) { + var loadT = bt.load(lan.soft.the_save); + bt.send('setPHPMaxTime', 'config/setPHPMaxTime', { version: version, time: time }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + disable_functions: function (version, fs, callback) { + var loadT = bt.load(); + bt.send('setPHPDisable', 'config/setPHPDisable', { version: version, disable_functions: fs }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_fpm_config: function (version, callback) { + var loadT = bt.load(); + bt.send('getFpmConfig', 'config/getFpmConfig', { version: version }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + set_fpm_config: function (version, data, callback) { + var loadT = bt.load(); + data.version = version; + bt.send('setFpmConfig', 'config/setFpmConfig', data, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_php_status: function (version, callback) { + var loadT = bt.load(); + bt.send('GetPHPStatus', 'ajax/GetPHPStatus', { version: version }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + // 获取PHP_session + get_php_session: function (version, callback) { + var loadT = bt.load(); + bt.send('GetSessionConf', 'config/GetSessionConf', { version: version }, function (res) { + loadT.close(); + if (callback) callback(res); + }); + }, + // 设置PHP_session文件 + set_php_session: function (obj, callback) { + var loadT = bt.load(); + bt.send('SetSessionConf', 'config/SetSessionConf', obj, function (res) { + loadT.close(); + if (callback) callback(res); + }); + }, + // 获取PHP_session清理信息 + get_session_count: function (callback) { + var loadT = bt.load(); + bt.send('GetSessionCount', 'config/GetSessionCount', {}, function (res) { + loadT.close(); + if (callback) callback(res); + }); + }, + // 清理php_session + clear_session_count: function (obj, callback) { + bt.confirm({ msg: obj.msg, title: obj.title }, function () { + var loadT = bt.load(); + bt.send('DelOldSession', 'config/DelOldSession', {}, function (res) { + loadT.close(); + if (callback) callback(res); + }); + }); + }, + get_fpm_logs: function (version, callback) { + var loadT = bt.load(); + bt.send('GetFpmLogs', 'ajax/GetFpmLogs', { version: version }, function (logs) { + loadT.close(); + if (logs.status !== true) { + logs.msg = ''; + } + if (logs.msg == '') logs.msg = lan.public_backup.no_fpm_log; + if (callback) callback(logs); + }); + }, + get_slow_logs: function (version, callback) { + var loadT = bt.load(); + bt.send('GetFpmSlowLogs', 'ajax/GetFpmSlowLogs', { version: version }, function (logs) { + loadT.close(); + if (logs.status !== true) { + logs.msg = ''; + } + if (logs.msg == '') logs.msg = lan.public_backup.no_slow_log; + if (callback) callback(logs); + }); + }, + }, + redis: { + get_redis_status: function (callback) { + var loadT = bt.load(); + bt.send('GetRedisStatus', 'ajax/GetRedisStatus', {}, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + }, + pro: { + conver_unit: function (name) { + var unit = ''; + switch (name) { + case 'year': + unit = lan.public_backup.year; + break; + case 'month': + unit = lan.public_backup.month; + break; + case 'day': + unit = lan.public_backup.day; + break; + case '1month': + unit = lan.public_backup.month; + break; + case '3month': + unit = lan.public_backup.month3; + break; + case '6month': + unit = lan.public_backup.month6; + break; + case '1year': + unit = lan.public_backup.year1; + break; + case '2year': + unit = lan.public_backup.year2; + break; + case '3year': + unit = lan.public_backup.year3; + break; + case '1': + unit = lan.public_backup.month1; + break; + case '3': + unit = lan.public_backup.month3; + break; + case '6': + unit = lan.public_backup.month6; + break; + case '12': + unit = lan.public_backup.year1; + break; + case '24': + unit = lan.public_backup.year2; + break; + case '36': + unit = lan.public_backup.year3; + break; + case '999': + unit = lan.public_backup.permanent; + break; + } + return unit; + }, + get_product_discount_by: function (product_id, callback) { + if (product_id) { + bt.send('get_plugin_price', 'auth/get_plugin_price', { product_id: product_id }, function (rdata) { + if (callback) callback(rdata); + }); + } else { + bt.send('get_product_discount_by', 'auth/get_product_discount_by', {}, function (rdata) { + if (callback) callback(rdata); + }); + } + }, + get_plugin_coupon: function (pid, callback) { + bt.send('check_pay_status', 'auth/check_pay_status', { id: pid }, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_re_order_status: function (callback) { + bt.send('get_re_order_status', 'auth/get_re_order_status', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_voucher: function (pid, callback) { + if (pid) { + bt.send('get_voucher_plugin', 'auth/get_voucher_plugin', { pid: pid }, function (rdata) { + if (callback) callback(rdata); + }); + } else { + bt.send('get_voucher', 'auth/get_voucher', {}, function (rdata) { + if (callback) callback(rdata); + }); + } + }, + get_check_out_info: function (data, callback) { + bt.send('get_stripe_session_id', 'auth/get_stripe_session_id', data, function (rdata) { + if (callback) callback(rdata); + }); + }, + create_order_voucher: function (pid, code, coupon_id, cycle, cycle_unit, charge_type, callback) { + var loading = bt.load(); + if (pid) { + bt.send( + 'create_order_voucher_plugin', + 'auth/create_order_voucher_plugin', + { pid: pid, coupon_id: coupon_id, cycle: cycle, cycle_unit: cycle_unit, charge_type: charge_type }, + function (rdata) { + loading.close(); + if (callback) callback(rdata); + bt.msg(rdata); + } + ); + } else { + bt.send('create_order_voucher', 'auth/create_order_voucher', { code: code }, function (rdata) { + loading.close(); + if (callback) { + callback(rdata); + } else { + bt.soft.pro.update(); + } + }); + } + }, + create_order: function (data, callback) { + if (data.pid) { + // var loadT = bt.load("Getting product information!"); + bt.soft.get_panel_ssl_status(function (res) { + // loadT.close() + if (res.status) { + // var _cycle_unit = $('#libPay-content .li-con .active span').attr("data-unit"), + // _pay_channel = $('#libPay-mode .pay-cycle-btn span').text().indexOf('Stripe') > -1?'2':'10' + // requestNmae = data.serial_no ? 'renew_product_auth' : 'get_buy_code'; + var requestNmae = 'get_buy_code'; + // if (!data.serial_no) { + // data.charge_type = 1; + // } else { + // data.pay_channel = 2; + // } + bt.send(requestNmae, 'auth/' + requestNmae, data, function (rdata) { + // loadT.close() + if (callback) callback(rdata); + }); + } else { + $('#libPay-content').empty(); + $('.libPay-mask').hide(); + $('#libPay-pay').empty().append( + '\ +
            \ +
            Purchase on the panel:
            \ +
              \ +
            • You need to open the panel SSL
            • \ +
            \ +
            Purchase on the official website:
            \ +
              \ +
            • No need to open panel SSL
            • \ +
            • You can purchase multiple licenses at the same time and get higher discounts
            • \ +
            \ +
            \ +
            \ + \ +
            ' + ); + $('#turn_on_ssl').on('click', function (e) { + setPanelSSL(); + }); + return false; + } + }); + } else { + bt.send( + 'create_order', + 'auth/create_order', + { + cycle: data.cycle, + }, + function (rdata) { + if (callback) callback(rdata); + } + ); + } + }, + }, + updata_commercial_view: function () { + layer.closeAll(); + var html = + '
            \ +
            \ +
            Pro
            \ +
            \ +

            推荐5人以上或企事业单位购买

            \ +
            \ +
            \ +

            包含所有专业版功能和:

            \ +

            1、提供在线客服工单协助

            \ +

            2、多用户管理插件(仅可查看日志)

            \ +

            3、后期还会有10+企业版专用插件

            \ +

            4、官方跟进响应的QQ群(需年付)

            \ +

            4、官方跟进响应的QQ群(需年付)

            \ +

            5、不定期线上运维培训(需年付)

            \ +
            \ +
            \ +
            \ + \ + 148\ + /月\ +
            \ +
            \ +
            \ + \ + 999\ + /年\ +
            \ +
            \ + \ +
            \ +
            '; + layer.open({ + type: 1, + closeBtn: 2, + area: '500px', + title: lan.public_backup.up_pro_use_allplug_free, + shade: 0.6, + anim: 0, + content: html, + success: function (layero, index) { + $('.btn-price').click(function () { + var _type = $(this).attr('data-type'); + if (_type == 'pro') { + bt.soft.updata_pro(); + layer.close(index); + } else { + bt.soft.updata_ltd(); + layer.close(index); + } + }); + }, + }); + }, + get_index_renew: function () { + bt.soft.get_product_renew(function (res) { + var html = $('
            '); + if (res.length > 0) { + bt.soft.each(res, function (index, item) { + html.append( + $( + '

            ' + + item.msg + + '    [ 忽略提示 ]

            ' + ).data(item) + ); + }); + $('#messageError').show().html(html); + $('.set_messages_status').click(function () { + var data = $(this).parent().data(), + that = this; + bt.soft.set_product_renew_status({ id: data.id, state: 0 }, function (rdata) { + if (!res.status) { + $(that).parent().remove(); + } + bt.msg(rdata); + }); + }); + } + }); + }, + // 获取产品续费状态 + get_product_renew: function (callback) { + $.get('/message/get_messages', function (res) { + if (res.status === false) { + layer.msg(res.msg, { icon: 2 }); + return false; + } + if (callback) callback(res); + }); + }, + //获取产品ssl + get_panel_ssl_status: function (callback) { + $.post('/config?action=get_panel_ssl_status', function (res) { + if (callback) callback(res); + }); + }, + set_product_renew_status: function (data, callback) { + $.post('/message/status_message', { id: data.id, state: data.state }, function (res) { + if (res.status === false) { + layer.msg(res.msg, { icon: 2 }); + return false; + } + if (callback) callback(res); + }); + }, + // 产品支付视图(配置参数) + product_pay_view: function (config) { + var that = this; + + $.get('/ssl?action=GetUserInfo', function (b) { + if (!b.status) { + bt.pub.bind_btname(function () { + window.location.reload(); + }); + return; + } + if (config.renew === -1) config.renew = false; + if (typeof config == 'string') config = JSON.parse(config); + + config = $.extend( + { + plugin: null, + renew: null, + active: '', + type: '', + pro: parseInt(bt.get_cookie('pro_end')) || -1, + ltd: parseInt(bt.get_cookie('ltd_end')) || -1, + }, + config + ); + + var totalNum = config.totalNum ? config.totalNum : ''; + if (totalNum) { + bt.set_cookie('pay_source', parseInt(totalNum)); + } + + bt.open({ + type: 1, + title: false, + skin: 'libPay-view', + area: ['1050px', '700px'], + shadeClose: false, + content: + '\ +
            \ +
            \ +
            \ +
            \ +
            \ + \ +
            \ +
            \ +
            \ +
            \ +
            \ + \ +
            \ +
            \ +
            \ +
            \ +
            \ +
            \ +
            Choose your plan
            \ +
            \ +
            \ +
              \ +
              \ +
              \ +
              \ +
              \ +
              Number of authorizations
              \ +
              \ +
                \ +
              \ +
              \ +
              \ +
              \ +
              \ +
              \ +
              \ + \ +
              \ +
              \ + \ +
              \ +
              \ + \ +
              \ +
              \ + \ +
              \ +
              \ +
              \ +
              \ +
              \ +
              \ +
              \ +
              Loading, please wait!
              \ +
              \ +
              \ +
              Account:
              \ +
              --
              \ + Change\ +
              \ +
              \ + Total: \ + $0\ + , After discount \ + $0\ + /1 year\ +
              \ +
              \ + \ + Pay for subscription\ + $0/year\ + \ + $\ + 0\ + \ + /first year\ + ?\ +
              \ + \ +
              \ +
              \ +
              \ +
              \ +
              \ +
              Account:
              \ +
              --
              \ + Change\ +
              \ +
              \ + Total: \ + $0\ + , After discount \ + $0\ + /1 year\ +
              \ +
              \ +
              \ +
              \ +
              \ +
              Vouchers
              \ +
                \ +
                \ + \ +
                \ +
                \ +
                \ +
                Authorization information
                \ +
                  \ +
                  \ + \ +
                  \ +
                  \ +
                  \ +
                  \ + \ +
                  \ +
                  \ +
                  ', + end: function () { + bt.clear_cookie('pay_source'); + }, + success: function ($layer, index) { + $.getScript('https://js.stripe.com/v3/'); + + var layerThat = this; + + layerThat.renderUserInfo(); + layerThat.renderProductMenu(); + + var init = function () { + var removeLoad = layerThat.addLoading('.libPay-layer-item'); + layerThat.renderFeature(); + layerThat.renderAuthList(function (rdata) { + if (rdata.length > 0) { + cutTab('authorization'); + layerThat.renderProductPrice(function () { + removeLoad(); + }); + } else { + layerThat.renderVoucher(function (rdata) { + if (rdata.length > 0) { + cutTab('voucher'); + } + layerThat.renderProductPrice(function () { + removeLoad(); + }); + }); + } + }); + }; + + init(); + + $('.switch-cycle-right').click(function () { + var num = $(this).prev().children().length; + var totalWidth = 80; + $.each($('.pay-pro-cycle .pay-cycle-btns'), function (i, elem) { + totalWidth += elem.offsetWidth; + }); + var libPayWidht = $('.libPay-line-item').width(); + var width = totalWidth - libPayWidht + 40; + var remainder = num % 4; // 获取余数 + $(this) + .prev() + .css('transform', 'translateX(-' + remainder * width + 'px)'); + $('.switch-cycle-left').removeClass('hide'); + $('#libPay-theme-price').css('padding-left', '30px'); + $(this).addClass('hide'); + }); + + $('.switch-cycle-left').click(function (ev) { + if (bt.del_seven_coupon) { + var $children = $('.pay-pro-cycle').children(); + if ($($children[0]).data('data').nums.length == 1) { + $($children[0]).remove(); + bt.del_seven_coupon = false; + } + } + $('#libPay-theme-price').removeAttr('style'); + $('.switch-cycle-right').removeClass('hide'); + $(this).next().removeAttr('style'); + $(this).addClass('hide'); + }); + + $('.pay-btn-group').on('click', 'li', function () { + $(this).addClass('active').siblings('.active').removeClass('active'); + }); + + // 切换菜单 + $('.libPay-menu').on('click', '.libPay-menu-type', function () { + $(this).addClass('active').siblings('.active').removeClass('active'); + var data = $('.libPay-menu .libPay-menu-type.active').data(); + if (data.title !== 'PRO') { + $('.pay-pro-cycle').css('transform', 'translateX(-27px)'); + } + + layerThat.renderProductPrice(); + layerThat.renderAuthNums(); + init(); + }); + + // 产品周期切换 + $('#libPay-theme-price .pay-pro-cycle').on('click', 'li', function () { + $(this).addClass('active').siblings('.active').removeClass('active'); + layerThat.renderAuthNums(); + var numConfig = $('#authorization .auth-num .active').data(); + var condition = $('.libPay-qcode-left .pay-type-btn.active').data('condition'); + if (condition === 'stripe') { + layerThat.renderTotalPrice(numConfig.num, false); + } else if (condition === 'paypal') { + layerThat.renderPaypal(numConfig.num, false); + } + }); + + // 多台授权切换 + $('#authorization .auth-num').on('click', 'li', function () { + $(this).addClass('active').siblings('.active').removeClass('active'); + var numConfig = $('#authorization .auth-num .active').data(); + var condition = $('.libPay-qcode-left .pay-type-btn.active').data('condition'); + if (condition === 'stripe') { + layerThat.renderTotalPrice(numConfig.num, true); + } else if (condition === 'paypal') { + layerThat.renderPaypal(numConfig.num, true); + } + }); + + // 点击支付 + $('#checkout-button').click(function () { + var config = $(this).data('data'); + var loadT = bt.load('Getting the session ID,Please waiting!'); + var stripe = Stripe(config.stripe_publishable_key); + var subscribe = config.subscription_price > 0 && $('.pay-subscription input').prop('checked') ? 1 : 0; + that.pro.get_check_out_info( + { + order_no: config.order_no, + subscribe: subscribe, + }, + function (res) { + loadT.close(); + if (res.id) { + stripe.redirectToCheckout({ sessionId: res.id }); + } else { + layer.msg('Payment order failed, please contact administrator!', { icon: 2 }); + } + } + ); + }); + + // 切换tab + function cutTab(condition) { + var $el = $('.libPay-qcode-left .pay-type-btn[data-condition="' + condition + '"]'); + var index = $el.index(); + $el.addClass('active').siblings('.active').removeClass('active'); + $('.libPay-qcode-right .libPay-qcode-item').eq(index).removeClass('hide').siblings('.libPay-qcode-item').addClass('hide'); + } + + // 支付方式切换 + $('.libPay-qcode-left .pay-type-btn').click(function () { + var condition = $(this).data('condition'); + cutTab(condition); + + var condition = $(this).data('condition'); + + var numConfig = $('#authorization .auth-num .active').data(); + switch (condition) { + case 'stripe': + layerThat.renderTotalPrice(numConfig.num || 1); + break; + case 'paypal': + layerThat.renderPaypal(numConfig.num || 1); + break; + case 'voucher': + layerThat.renderVoucher(); + break; + case 'authorization': + layerThat.renderAuthList(); + break; + } + }); + + // 切换用户 + $('.libPay-qcode-item .userinfo a').click(function () { + bt.pub.bind_btname(function () { + // bt.soft.product_pay_view(config); + window.location.reload(); + }); + }); + + // 使用抵扣卷 + $('#use-voucher').click(function () { + if ($(this).hasClass('disabled')) return false; + + var data = $('.voucher-group .active').data(); + if (!data.serial_no) { + layer.msg('No vouchers'); + return false; + } + bt.soft.pro.create_order_voucher(data.pid, data.code, data.id, data.cycle, data.cycle_unit, data.charge_type, function (rdata) { + layer.closeAll(); + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + bt.msg(rdata); + if (rdata.status) { + getPaymentStatus(); + } + }); + }); + + // 授权 + $('#use-auth').click(function () { + if ($(this).hasClass('disabled')) return false; + + var _serial_no = $('.auth-group .active').attr('data-id'); + if (typeof _serial_no == 'undefined') return false; + var loadU = bt.load('Under licensing!'); + bt.send('auth_activate', 'auth/auth_activate', { serial_no: _serial_no }, function (res) { + loadU.close(); + if (res) { + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + if (res.status) { + window.location.reload(); + } + } + }); + }); + + var layerIndex = -1; + + $('.pay-subscription .bt-ico-ask').hover( + function () { + layerIndex = layer.tips('
                  when subscription creation, your authorization will auto generated
                  when subscription renewal, your authorization will auto renewed
                  ', $(this), { + tips: [1, '#999'], + time: 0, + area: '410px', + }); + }, + function () { + layer.close(layerIndex); + } + ); + }, + addLoading: function (elem) { + var $el = $(elem); + $el.children().addClass('hide'); + if ($el.children('.cloading').length === 0) { + $el.append('
                  Loading, please wait!
                  '); + } + return function removeLoad() { + $el.children('.cloading').remove(); + $el.children().removeClass('hide'); + }; + }, + getConfig: function () { + var data = $('.libPay-menu .libPay-menu-type.active').data(); + return data; + }, + // 渲染菜单 + renderProductMenu: function () { + var menus = []; + if (config.plugin) { + menus.push({ + title: config.name, + name: config.name, + ps: 'Plug-in only', + desc: config.ps, + pid: config.pid, + renew: config.renew || false, + is_pro: false, + active: (config.pro < 0 && config.ltd < 0) || (config.type == 12 && config.ltd < 0) ? true : false, + }); + } + if ( + (((config.pro > 0 || config.pro == -2 || config.ltd < 0) && ((config.ltd > 0 && config.ltd != config.pro) || config.ltd < 0) && config.type != 12) || + config.limit == 'pro' || + (config.ltd < 0 && config.pro == -1)) && + config.type != 12 && + ((config.ltd < 0 && config.pro > 0) || (config.ltd < 0 && config.pro < 0)) + ) { + menus.push({ + title: 'PRO', + name: '', + pid: '100000058', + ps: 'Recommended', + renew: config.renew || false, + is_pro: true, + active: + ((config.type == 8 && !config.plugin) || config.limit == 'pro' || config.pro > 0 || config.pro == -2) && + config.ltd < 0 && + (config.ltd == -2 ? (config.pro == -2 ? false : true) : true), + }); + } + + var $el = null; + $.each(menus, function (index, item) { + $el = $( + '
                  \ +

                  \ + ' + + (item.is_pro ? '' : '') + + '' + + item.title + + '\ +

                  \ +

                  ' + + item.ps + + '

                  \ +
                  ' + ).data(item); + $('.libPay-menu').append($el); + }); + }, + renderFeature: function () { + var config = this.getConfig(); + + if (config.is_pro) { + $('.pro-left-introduce .pro-left-title>div').html('' + config.title + ''); + $('.pro-left-introduce .pro-left-title>span').removeClass('hide').html(config.ps); + $('.pro-left-list-title').text('Pro Feature: '); + $('.pro-price-herf').removeClass('hide'); + bt.send('get_plugin_remarks', 'auth/get_plugin_remarks', { product_id: '100000058' }, function (rdata) { + var html = ''; + $.each(rdata.res, function (index, item) { + html += '
                  ' + item + '
                  '; + }); + $('.pro-left-list-content').removeAttr('style').html(html); + }); + } else { + $('.pro-left-introduce .pro-left-title>div').html('' + config.title + ''); + $('.pro-left-introduce .pro-left-title>span').addClass('hide').html(config.ps); + $('.pro-left-list-title').text('Plug-in description: '); + $('.pro-left-list-content') + .css({ + width: '186px', + 'line-height': '23px', + }) + .html(config.desc); + $('.pro-price-herf').addClass('hide'); + } + }, + + // 默认选中的套餐索引 + // defaultPackageIndex: 0, + + // // 默认选中的授权台数索引 + // defaultAuthIndex: 0, + + // 渲染产品价格 + renderProductPrice: function (callback) { + var layerThat = this; + + // 获取产品价格数据 + var config = layerThat.getConfig(); + + that.get_product_discount_cache(config, function (rdata) { + if (callback) callback(rdata); + + // 大于4个显示左右切换按钮 + var num = rdata.length; + if (num > 4) { + $('.switch-cycle-right').removeClass('hide'); + } else { + $('.switch-cycle-right,.switch-cycle-left').addClass('hide'); + } + + // 遍历渲染产品价格 + var html = ''; + var htmlNum = ''; + $('#libPay-theme-price .pay-pro-cycle').empty(); + $('#authorization .auth-num').empty(); + + that.each(rdata, function (key, item) { + const priceItem = item.children[0]; // 默认选中的授权台数索引 + var keys = item.cycle; + var unit = item.cycle_unit; + var priceByDay = (priceItem.price / ((priceItem.cycle / (priceItem.cycle_unit === 'year' ? 1 : 12)) * 365)).toFixed(2); + // 产品周期 + var cycleUnit = ''; + // 每天低至XX$ + var dayPrice = ''; + if (item.cycle === '999') { + cycleUnit = 'Lifetime'; + dayPrice = 'Best value'; + } else { + cycleUnit = item.cycle + ' ' + item.cycle_unit + (item.cycle > 1 ? 's' : ''); + dayPrice = 'As low as $' + priceByDay + '/day'; + } + + const authNum = item.children; + if (key === 0) { + that.each(authNum, function (key1, numItem) { + htmlNum = + '\ +
                • \ + ' + + numItem.num + + (key1 === 0 ? '' : numItem.discount_rate != 1 ? '' + (100 - numItem.discount_rate * 100).toFixed(2) + '% off' : '') + + '\ +
                • '; + $('#authorization .auth-num').append( + $(htmlNum).data( + $.extend( + { + pid: config.pid, + dom_index: key, + }, + numItem + ) + ) + ); + }); + } + + html = + '\ +
                • \ +
                  \ +
                  \ +
                  $' + + priceItem.price.toFixed(2) + + '
                  \ +
                  /' + + cycleUnit + + '
                  \ +
                  \ +

                  OP: $' + + priceItem.market_price + + '

                  \ +
                  \ +
                  ' + + dayPrice + + '
                  \ + ' + + // (priceItem.discount_rate != 1 ? '' + (100 - priceItem.discount_rate * 100) + '% off' : '') + + (key === 0 ? '' + 'Most popular' + '' : '') + + '\ +
                • '; + $('#libPay-theme-price .pay-pro-cycle').append( + $(html).data( + $.extend( + { + pid: config.pid, + dom_index: key, + }, + priceItem + ) + ) + ); + }); + + layerThat.renderTotalPrice(); + }); + layerThat.renderAuthNums(); + }, + + // 渲染授权数量选项 + renderAuthNums: function () { + var layerThat = this; + + // 获取产品价格数据 + var config = layerThat.getConfig(); + + var selectedCycle = $('#libPay-theme-price .pay-pro-cycle li.active').data('type'); + var selectedunit = $('#libPay-theme-price .pay-pro-cycle li.active').data('unit'); + var selectedAuthIndex = $('#authorization .auth-num li.active').index(); + that.get_product_discount_cache(config, function (rdata) { + // 查找对应周期的授权数量数据 + var authNumData = null; + for (var i = 0; i < rdata.length; i++) { + if (rdata[i].cycle == selectedCycle && rdata[i].cycle_unit == selectedunit) { + authNumData = rdata[i].children; + break; + } + } + + if (authNumData) { + // 清空授权数量选项 + $('#authorization .auth-num').empty(); + + // 遍历渲染授权数量选项 + for (var j = 0; j < authNumData.length; j++) { + var authNumItem = authNumData[j]; + + var htmlNum = + '
                • ' + + authNumItem.num + + (j === 0 ? '' : authNumItem.discount_rate !== 1 ? '' + (100 - authNumItem.discount_rate * 100).toFixed(2) + '% off' : '') + + '
                • '; + + var numItem = { + pid: config.pid, + dom_index: j, + }; + + var authNumElement = $(htmlNum).data($.extend(numItem, authNumItem)); + $('#authorization .auth-num').append(authNumElement); + } + } + }); + }, + + // 渲染用户信息 + renderUserInfo: function () { + var bt_user_info = bt.get_cookie('bt_user_info'); + if (!bt_user_info) { + bt.pub.get_user_info(function (res) { + $('.libPay-qcode-right .userinfo .info_value').html(res.data.username); + }); + } else { + $('.libPay-qcode-right .userinfo .info_value').html(JSON.parse(bt_user_info).data.username); + } + }, + renderEndTime: function () { + var endTime = null; + if (!config.is_alone) { + // 条件:当前为插件 + if (config.plugin) { + title = (!config.renew ? 'Buy ' : '续费') + config.name; + ndTime = !config.renew ? config.renew : null; + } else if (config.pro == -1 && config.ltd == -1) { + // 条件:专业版和企业版都没有购买过 + title = 'Upgrade to Pro, all plugins are free to use'; + } else if (config.ltd > 0) { + // 条件:企业版续费 + title = 'Renew ' + (config.name == '' ? '宝塔专业版' : config.name); + endTime = config.ltd; + } else if (config.pro > 0 || config.pro == -2) { + // 条件:专业版续费 + title = 'Renew ' + (config.name == '' ? '宝塔专业版' : config.name); + endTime = config.pro; + } else if (config.ltd == -2) { + title = 'Renew ' + (config.name == '' ? '宝塔专业版' : config.name); + endTime = config.ltd; + } + } else { + title = (config.ltd > 0 ? '续费' : '购买') + '宝塔企业版'; + } + if (endTime != null) { + $('.endTime span').html(endTime > parseInt(new Date().getTime() / 1000) ? bt.format_data(endTime) : 'Expired'); + } else { + $('.endTime').hide(); + } + }, + // 渲染总价格 + renderTotalPrice: function (num, isNum) { + var condition = $('.libPay-qcode-left .pay-type-btn.active').data('condition'); + if (condition !== 'stripe') { + return; + } + + var layerThat = this; + var removeLoad = layerThat.addLoading('.libPay-qcode-item:not(.hide)'); + + var numActive = $('#authorization .auth-num .active').data(); + var priceActive = $('#libPay-theme-price .pay-pro-cycle li.active').data(); + + var config = null; + if (isNum === true) { + config = $('#authorization .auth-num .active').data(); + } else { + config = $('#libPay-theme-price .pay-pro-cycle .active').data(); + } + config = priceActive; + config.num = numActive; + + var param = { pid: config.pid, cycle: config.cycle, cycle_unit: config.cycle_unit, charge_type: config.charge_type, num: num || 1 }; + param.source = parseInt(bt.get_cookie('pay_source') || 0); + if (param.source === 0) { + if ($('.btpro-gray').length == 1) { + // 是否免费版 + param.source = 27; + } else { + param.source = 28; + } + } + if (!param.pid) delete param.pid; + if (bt.get_cookie('serial_no') != null) param.serial_no = config.serial_no || bt.get_cookie('serial_no'); + that.pro.create_order(param, function (rdata) { + removeLoad(); + if (rdata.status === false) { + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + layer.msg(rdata.msg, { icon: 2 }); + return; + } + + var cycle = ''; + if (config.cycle === 999) { + cycle = 'Lifetime'; + } else { + cycle = config.cycle + ' ' + config.cycle_unit + (config.cycle > 1 ? 's' : ''); + } + // var unit = config.num + ' unit' + (config.num > 1 ? 's' : ''); + // 改成接口请求后获取到的价格 rdata + $('.libPayTotal').html('$' + rdata.price); // + $('.pay-price .org_price').text('$' + rdata.market_price + '/' + cycle); // + $('.pay-price .libPayCycle').html('/' + cycle); + + if (rdata.subscription_price > 0) { + // + $('.pay-subscription').removeClass('hide'); + if (config.first_subscription_price > 0) { + $('.pay-subscription .org-price').removeClass('hide'); + // 改成接口请求后获取到的价格 rdata + $('.pay-subscription .org-price').text('$' + rdata.subscription_price + '/' + config.cycle_unit); // + $('.pay-subscription .first-price .num').text(config.first_subscription_price); // + $('.pay-subscription .cycle-unit').text('/first ' + config.cycle_unit); // + } else { + $('.pay-subscription .org-price').addClass('hide'); + $('.pay-subscription .first-price .num').text(rdata.subscription_price); // + $('.pay-subscription .cycle-unit').text('/' + config.cycle_unit); // + } + } else { + $('.pay-subscription').addClass('hide'); + } + + $('#checkout-button').data('data', rdata); + }); + }, + + // 渲染paypal + renderPaypal: function (num, isNum) { + var layerThat = this; + + var removeLoad = layerThat.addLoading('.libPay-qcode-item:not(.hide)'); + // var config = $('#libPay-theme-price .pay-pro-cycle .active').data(); + + var numActive = $('#authorization .auth-num .active').data(); + var priceActive = $('#libPay-theme-price .pay-pro-cycle li.active').data(); + + var config = null; + if (isNum === true) { + config = $('#authorization .auth-num .active').data(); + } else { + config = $('#libPay-theme-price .pay-pro-cycle .active').data(); + } + config = priceActive; + config.num = numActive; + + var param = { pid: config.pid, cycle: config.cycle, cycle_unit: config.cycle_unit, charge_type: config.charge_type, num: num || 1 }; + param.source = parseInt(bt.get_cookie('pay_source') || 0); + if (param.source === 0) { + if ($('.btpro-gray').length == 1) { + // 是否免费版 + param.source = 27; + } else { + param.source = 28; + } + } + if (!param.pid) delete param.pid; + if (bt.get_cookie('serial_no') != null) param.serial_no = config.serial_no || bt.get_cookie('serial_no'); + that.pro.create_order(param, function (rdata) { + removeLoad(); + + if (rdata.status === false) { + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + layer.msg(rdata.msg, { icon: 2 }); + return; + } + + var cycle = ''; + if (config.cycle === 999) { + cycle = 'Lifetime'; + } else { + cycle = config.cycle + ' ' + config.cycle_unit + (config.cycle > 1 ? 's' : ''); + } + // var unit = config.num + ' unit' + (config.num > 1 ? 's' : ''); + $('.libPayTotal').html('$' + rdata.price); + $('.pay-price .org_price').text('$' + rdata.market_price + '/' + cycle); + $('.pay-price .libPayCycle').html('/' + cycle); + + layerThat.renderPaypalBtn(rdata); + }); + }, + renderPaypalBtn: function (rdata) { + $('#paypal-button-container').empty(); + + jQuery + .ajax({ + url: '/static/js/polyfill.min.js?v=1715756461542', + dataType: 'script', + cache: true, + }) + .done(function () { + var httpPromise = function (config) { + return new Promise((resolve, reject) => { + $.ajax({ + type: 'POST', + url: config.url, + data: config.data, + success: function (res) { + resolve(res); + }, + error: function (err) { + reject(err); + }, + }); + }); + }; + var check_response = function (response) { + return new Promise((resolve, reject) => { + if (response.status) { + return resolve(response.res); + } + bt.msg(response); + reject(response.res); + }); + }; + jQuery + .ajax({ + url: 'https://www.paypal.com/sdk/js?client-id=' + rdata.paypal_client_id, + dataType: 'script', + cache: true, + }) + .done(function () { + var buttons = paypal.Buttons({ + // 只渲染paypal支付按钮 + fundingSource: paypal.FUNDING.PAYPAL, + // 绑定PayPal订单创建事件处理函数,请求paypal订单创建接口 + // 注意: 该函数必须返回Promise对象, 若使用 JQuery.ajax() 函数 + // 请使用 new Promise(fn(resolve(), reject())) 创建Promise对象并将其返回 + // Promise必须返回从接口获取的id + createOrder: () => { + return httpPromise({ + url: '/auth?action=get_paypal_session_id', + data: { oid: rdata.order_id }, + }) + .then(check_response) + .then(res => res); + }, + // 绑定支付确认事件,请求paypal订单支付确认接口并传递orderId + // 注意: 该函数必须返回Promise对象,若使用 JQuery.ajax() 函数 请参照 step3 + // Promise最终请展示支付结果 + onApprove: data => { + return ( + httpPromise({ + url: '/auth?action=check_paypal_status', + data: { paypal_order_id: data.orderID }, + }) + // res是支付成功跳转的url + .then(check_response) + .then(res => { + layer.msg('Payment successful', { icon: 1 }); + setTimeout(() => { + location.reload(); + }, 1500); + }) + ); + }, + }); + buttons.render('#paypal-button-container'); + }); + }); + }, + // 渲染抵扣卷 + renderVoucher: function (callback) { + var layerThat = this; + var config = layerThat.getConfig(); + var removeLoad = layerThat.addLoading('.libPay-qcode-item:not(.hide)'); + bt.soft.pro.get_voucher(config.pid, function (rdata) { + removeLoad(); + + if (callback) callback(rdata); + + $('.voucher-group').empty(); + if (rdata == null && !Array.isArray(rdata)) rdata = []; + if (rdata.length == 0) { + $('.voucher-group').addClass('hide'); + $('#use-voucher').addClass('disabled').text('No vouchers'); + return; + } + + var $li = null; + that.each(rdata, function (index, item) { + var name = item.cycle_unit == 'month' && item.cycle == 999 ? 'Lifetime' : item.cycle + that.pro.conver_unit(item.cycle_unit); + $li = $('
                • ' + name + '
                • ').data($.extend({ pid: config.pid }, item)); + $('.voucher-group').append($li); + }); + + $('.voucher-group').removeClass('hide'); + $('#use-voucher').removeClass('disabled').text('Pay'); + }); + }, + // 渲染授权列表 + renderAuthList: function (callback) { + var layerThat = this; + var config = layerThat.getConfig(); + var removeLoad = layerThat.addLoading('.libPay-qcode-item:not(.hide)'); + bt.send('get_product_auth', 'auth/get_product_auth', { page: 1, pageSize: 15, pid: config.pid }, function (rdata) { + removeLoad(); + + if (callback) callback(rdata); + + $('.auth-group').empty(); + if (rdata == null && !Array.isArray(rdata)) rdata = []; + if (rdata.length == 0) { + $('.auth-group').addClass('hide'); + $('#use-auth').addClass('disabled').text('No authorization'); + return; + } + + var html = ''; + $.each(rdata, function (index, item) { + if (config.pid == item.product_id) { + html += + '
                • ' + + (item.cycle === 999 ? 'Lifetime' : bt.format_data(item.end_time)) + + '
                • '; + } + }); + $('.auth-group').append(html); + $('.auth-group').removeClass('hide'); + $('#use-auth').removeClass('disabled').text('Authorization'); + }); + }, + }); + }); + // if (!bt.get_cookie('bt_user_info')) { + // bt.pub.bind_btname(function () { + // window.location.reload(); + // }); + // return false; + // } + }, + product_cache: {}, //产品周期缓存 + order_cache: {}, + // 获取产品周期 ,并进行对象缓存 + get_product_discount_cache: function (config, callback) { + var that = this; + if (typeof this.product_cache[config.pid] != 'undefined') { + if (callback) callback(this.product_cache[config.pid]); + } else { + bt.soft.pro.get_product_discount_by(config.pid, function (rdata) { + //rdata = {"36": {"discount": 1, "did": 0, "price": 3564, "name": "正常", "sprice": 3564}, "24": {"discount": 1, "did": 0, "price": 2376, "name": "正常", "sprice": 2376}, "12": {"discount": 1, "did": 0, "price": 1188, "name": "正常", "sprice": 1188}, "6": {"discount": 1, "did": 0, "price": 594, "name": "正常", "sprice": 594}, "3": {"discount": 1, "did": 0, "price": 297, "name": "正常", "sprice": 297}, "1": {"discount": 1, "did": 0, "price": 99, "name": "正常", "sprice": 99}, "pid": "100000045"}; + if (typeof rdata.status === 'boolean') { + if (!rdata.status) { + bt.msg(rdata); + return false; + } + } + that.product_cache[config.pid] = rdata; + setTimeout(function () { + delete that.product_cache[config.pid]; + }, 60000); + if (callback) callback(rdata); + }); + } + }, + // 产品页面刷新 + product_pay_page_refresh: function (config) { + var that = this; + var condition = config.condition; + switch (condition) { + case 1: + var loadT = bt.load(); + bt.send('get_product_auth', 'auth/get_product_auth', { page: 1, pageSize: 15, pid: config.pid }, function (res) { + bt.soft.pro.get_voucher(config.pid, function (rdata) { + loadT.close(); + var _arry = [ + { title: '微信支付', condition: 2 }, + { title: 'Stripe', condition: 3 }, + { title: 'voucher', condition: 4 }, + { title: 'Authorization', condition: 7, _pid: config.pid }, + ]; + // if (config.renew) { + // _arry.splice(_arry.length - 1, 1); + // _arry[rdata.length > 0 ? '2' : '1'].active = true; + // config.condition = rdata.length > 0 ? 4 : 2; + // } else { + _arry[res.length > 0 ? '3' : rdata.length > 0 ? '2' : '1'].active = true; + config.condition = res.length > 0 ? 7 : rdata.length > 0 ? 4 : 3; + // } + if (res == null) res = []; + if (rdata == null) rdata = []; + $('#libPay-mode .li-con') + .empty() + .append( + that.product_pay_swicth('payment', { + name: config.name, + pid: config.pid, + data: _arry, + voucher_data: rdata, + }) + ); + if (config.renew) { + if (rdata.length > 0) config.voucher_data = rdata; + } else { + if (rdata.length > 0) config.voucher_data = rdata; + if (res.length > 0) config.voucher_data = res; + } + that.product_pay_page_refresh(config); + }); + }); + break; + case 2: + case 3: + $('#libPay-content .li-tit').text('Choose your plan'); + config.pay = condition; + if (config.pid == '100000030') { + $('#libPay-tips').show(); + } else { + $('#libPay-tips').hide(); + } + var loadT = bt.load(); + bt.soft.get_product_discount_cache(config, function (rdata) { + loadT.close(); + var _arry = []; + var index = 0; + try { + delete rdata.pid; + } catch (error) { + console.log(rdata.pid); + } + that.each(rdata, function (key, item) { + _arry.push($.extend({ cycle: parseInt(key) }, item)); + }); + _arry[index].active = true; + $('#libPay-content .li-con') + .empty() + .append( + that.product_pay_swicth('time', { + name: config.name, + pid: config.pid, + data: _arry, + }) + ); + config.condition = 5; + config = $.extend(config, _arry[index]); + that.product_pay_page_refresh(config); + }); + break; + case 4: + var loadP = bt.load('Getting deduction volume information!'); + clearInterval(bt.soft.pub.wxpayTimeId); + $('#libPay-content').empty().append('
                  '); + $('#libPay-pay').empty(); + $('#libPay-content .li-tit').text('Vouchers'); + $('#libPay-pay').removeAttr('data-qecode'); + $('#libPay-tips').hide(); + + function callback(rdata) { + loadP.close(); + if (rdata == null && !Array.isArray(rdata)) rdata = []; + if (rdata.length == 0) { + $('#libPay-content .li-con').empty(); + that.product_pay_page_refresh({ condition: 6, pid: '', code: false }); + return false; + } + rdata[0].active = true; + $('#libPay-content .li-con').append( + that.product_pay_swicth('voucher', { + name: config.name, + pid: config.pid, + data: rdata, + }) + ); + config.condition = 6; + that.product_pay_page_refresh($.extend(config, rdata[0])); + } + if (config.voucher_data) { + callback(config.voucher_data); + } else { + bt.soft.pro.get_voucher(config.pid, function (rdata) { + callback(rdata); + }); + } + break; + case 5: + $('#libPay-content .li-con').css('height', 'auto'); + $('.libPay-mask').show(); + if ($('#libPay-pay').attr('data-qecode')) { + var qcode = $('#libPay-content li').eq(config.dom_index).data('qrcode-url'); + $('#libPay-pay').find('.sale-price').html(config.price.toFixed(2)); + $('#libPay-pay') + .find('.cost-price') + .css('display', config.sprice > config.price ? 'inline-block' : 'none') + .html('$ ' + config.sprice.toFixed(2)); + $('#libPay-pay').find('#PayQcode').html('
                  Loading, please wait!
                  '); + if (qcode) { + $('#libPay-pay').find('#PayQcode').empty().qrcode(qcode); + that.product_pay_monitor({ pid: config.pid, name: config.name }); + $('.libPay-mask').hide(); + return false; + } + } else { + $('#libPay-pay').html('
                  Loading, please wait!
                  '); + } + var paream = { pid: config.pid, cycle: config.cycle }; + paream.source = parseInt(bt.get_cookie('pay_source') || 0); + console.log(paream.source); + if (paream.source === 0) { + if ($('.btpro-gray').length == 1) { + // 是否免费版 + paream.source = 27; + } else { + paream.source = 28; + } + } + if (!paream.pid) delete paream.pid; + if (bt.get_cookie('serial_no') != null) paream.serial_no = config.serial_no || bt.get_cookie('serial_no'); + that.pro.create_order(paream, function (rdata) { + if (rdata.status === false) { + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + layer.msg(rdata.msg, { icon: 2 }); + return; + } + config.pay = parseInt($('#libPay-mode .pay-cycle-btn.active').data('condition')); + // 二维码显示界面 + $('#libPay-pay') + .empty() + .append(that.product_pay_swicth(config.pay == 2 ? 'wechat' : 'alipay', $.extend({ order_no: rdata.order_no, stripe_publishable_key: rdata.stripe_publishable_key }, config))); + }); + $('#libPay-pay').on('click', '#checkout-button', function () { + var loadT = bt.load('Getting the session ID,Please waiting!'); + var stripe = Stripe($(this).data('keys')); + that.pro.get_check_out_info($(this).data('code'), function (res) { + loadT.close(); + if (res.id) { + stripe.redirectToCheckout({ sessionId: res.id }); + } else { + layer.msg('Payment order failed, please contact administrator!', { icon: 2 }); + } + }); + }); + break; + case 6: + var _html = $('
                  '); + var _button = $( + '' + ); + _button.click(function (ev) { + if (!config.serial_no) { + layer.msg('No vouchers'); + return false; + } + bt.soft.pro.create_order_voucher(config.pid, config.code, config.id, config.cycle, config.cycle_unit, config.charge_type, function (rdata) { + layer.closeAll(); + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + bt.msg(rdata.res); + }); + }); + $('#libPay-pay').empty().append(_html.append(_button)); + break; + case 7: + if (config.renew) return false; + var loadA = bt.load('Obtaining authorization information!'); + $('#libPay-content').empty().append('
                  '); + $('#libPay-pay').empty(); + var _pid = config.pid; + $('#libPay-content .li-tit').text('Authorization information'); + bt.send('get_product_auth', 'auth/get_product_auth', { page: 1, pageSize: 15, pid: config.pid }, function (res) { + loadA.close(); + if (res.status == false) { + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + return false; + } + _html = $('
                    '); + if (res.length == 0) { + _html.append($('
                  • No authorization
                  • ')); + } + var authorization_flag = false; + $.each(res, function (index, item) { + if (_pid == item.product_id) { + _html.append( + $('
                  • ' + bt.format_data(item.end_time) + '
                  • ') + ); + authorization_flag = true; + } + if (authorization_flag == false && index == res.length - 1) { + _html.append($('
                  • No authorization
                  • ')); + } + }); + $('#libPay-content .li-con').empty().append(_html); + $('#libPay-content ul li').click(function () { + $(this).addClass('active').siblings().removeClass('active'); + }); + $('#libPay-pay') + .empty() + .append($('
                    ')); + //授权 + $('#authorization').unbind(); + var html = $('#libPay-content .li-con ul li span').html() || ''; + if (html.indexOf('No authorization') > 0) { + $('#authorization').attr('disabled', 'disabled'); + } else { + $('#authorization').removeAttr('disabled'); + $('#authorization').on('click', function (e) { + var _serial_no = $('#libPay-content .active').attr('data-id'); + if (typeof _serial_no == 'undefined') return false; + var loadU = bt.load('Under licensing!'); + bt.send('auth_activate', 'auth/auth_activate', { serial_no: _serial_no }, function (res) { + loadU.close(); + if (res) { + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + if (res.status) { + window.location.reload(); + } + } + }); + }); + } + }); + break; + } + }, + // 产品购买,渲染方法 + product_pay_swicth: function (type, config) { + var _html = '', + that = this; + switch (type) { + case 'type': // 产品类型(配置参数) + _html = $('
                      '); + this.each(config, function (index, item) { + _html.append( + $( + '
                    • ' + + (item.recommend ? '' : '') + + '' + + item.title + + '' + + '' + + item.ps + + '' + + '
                    • ' + ) + .data(item) + .click(function (ev) { + var data = $(this).data(); + if (!$(this).hasClass('active')) that.product_pay_page_refresh($.extend({ condition: 1 }, data)); + $(this).addClass('active').siblings().removeClass('active'); + }) + ); + }); + break; + case 'payment': // 产品付款方式 + _html = $('
                        '); + this.each(config.data, function (index, item) { + if (item.title != '微信支付') { + _html.append( + $('
                      • ' + item.title + '
                      • ') + .data($.extend({ pid: config.pid, name: config.name }, item)) + .click(function (ev) { + var data = $(this).data(); + if (!$(this).hasClass('active')) that.product_pay_page_refresh($.extend({ condition: $(this).attr('data-condition') }, data)); + $(this).addClass('active').siblings().removeClass('active'); + }) + ); + } + }); + break; + case 'time': // 产品开通时长(配置参数) + _html = $('
                          '); + this.each(config.data, function (index, item) { + _html.append( + $( + '
                        • ' + + that.pro.conver_unit(item.cycle + item.cycle_unit) + + '' + + (item.discount_rate != 1 ? '' + (100 - item.discount_rate * 100) + '% off' : '') + + '
                        • ' + ) + .data( + $.extend( + { + pid: config.pid, + dom_index: index, + }, + item + ) + ) + .click(function (ev) { + var data = $(this).data(); + if (!$(this).hasClass('active')) that.product_pay_page_refresh($.extend({ condition: 5 }, data)); + $(this).addClass('active').siblings().removeClass('active'); + }) + ); + }); + break; + case 'voucher': // 产品抵扣卷(配置参数) + _html = $('
                            '); + + this.each(config.data, function (index, item) { + _html.append( + $( + '
                          • ' + + (item.cycle_unit == 'month' && item.cycle == 999 ? '永久' : item.cycle + that.pro.conver_unit(item.cycle_unit)) + + '
                          • ' + ) + .data($.extend({ pid: config.pid }, item)) + .click(function (ev) { + var data = $(this).data(); + $(this).addClass('active').siblings().removeClass('active'); + that.product_pay_page_refresh($.extend({ condition: 6 }, data)); + }) + ); + }); + break; + case 'wechat': + case 'alipay': + _html = $( + '
                            ' + + 'Total' + + '$' + + config.price.toFixed(2) + + '' + + '$ ' + + config.market_price.toFixed(2) + + '
                            ' + + '
                            ' + + '' + ); + // $(_html).find('#PayQcode').qrcode(config.data); + $('.libPay-mask').hide(); + // that.product_pay_monitor({ pid: config.pid, name: config.name }); + break; + } + return _html; + }, + + // 支付状态监听 + product_pay_monitor: function (config) { + var that = this; + function callback(rdata) { + if (rdata.status) { + clearInterval(bt.soft.pub.wxpayTimeId); + layer.closeAll(); + var title = ''; + if (config.pid == 100000032 || config.pid === '') { + title = config.pid === '' ? '专业版支付成功!' : '企业版支付成功!'; + setTimeout(function () { + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + location.reload(true); + }, 2000); // 需要重服务端重新获取软件列表,并刷新软件管理浏览器页面 + } else { + title = config.name + '插件支付成功!'; + setTimeout(function () { + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + location.reload(true); + }, 2000); // 需要重服务端重新获取软件列表, + } + bt.msg({ msg: title, icon: 1, shade: [0.3, '#000'] }); + } + } + clearInterval(bt.soft.pub.wxpayTimeId); + function intervalFun() { + if (config.pid) { + that.pro.get_plugin_coupon(config.pid, callback); + } else { + that.pro.get_re_order_status(callback); + } + } + intervalFun(); + bt.soft.pub.wxpayTimeId = setInterval(function () { + intervalFun(); + }, 2500); + }, + updata_ltd: function (is_alone) { + var param = { name: '宝塔面板企业版', pid: 100000032, limit: 'ltd' }; + if (is_alone || false) $.extend(param, { source: 5, is_alone: true }); + bt.soft.product_pay_view(param); + }, + //遍历数组和对象 + each: function (obj, fn) { + var key, + that = this; + if (typeof fn !== 'function') return that; + obj = obj || []; + if (obj.constructor === Object) { + for (key in obj) { + if (fn.call(obj[key], key, obj[key])) break; + } + } else { + for (key = 0; key < obj.length; key++) { + if (fn.call(obj[key], key, obj[key])) break; + } + } + return that; + }, + /** + * @description 升级专业版 + */ + updata_pro: function (num) { + var param = { + name: 'Pro', + pid: 100000058, + limit: 'pro', + }; + if (num) param['totalNum'] = num; + bt.set_cookie('pay_source', num); + bt.soft.product_pay_view(param); + }, + /** + * @description 续费专业版 + */ + renew_pro: function () { + var config = { name: 'Pro', pid: '100000058', limit: 'pro', renew: true }; + if (!bt.get_cookie('serial_no')) delete config.renew; + bt.soft.product_pay_view(config); + }, + // updata_pro: function() { + // bt.pub.get_user_info(function(rdata) { + // if (!rdata.status) { + // bt.pub.bind_btname(0, function(rdata) { + // if (rdata.status) bt.soft.updata_pro(); + // }) + // return; + // } + // var payhtml = '
                            \ + //
                            \ + //
                            \ + //

                            ' + lan.public_backup.buy_multiplev_bt_pro + '' + lan.public_backup.goto_bt + '

                            \ + //
                            '; + + // bt.open({ + // type: 1, + // title: lan.public_backup.up_pro_use_allplug_free, + // area: ['616px', '540px'], + // closeBtn: 2, + // shadeClose: false, + // content: payhtml + // }); + // setTimeout(function() { + // bt.soft.get_product_discount('', 0); + // $(".pay-btn-group > li").click(function() { + // $(this).addClass("active").siblings().removeClass("active"); + // }); + // }, 100) + // }) + // }, + re_plugin_pay: function (pluginName, pid, type) { + bt.pub.get_user_info(function (rdata) { + if (!rdata.status) { + bt.pub.bind_btname(0, function (rdata) { + if (rdata.status) bt.soft.re_plugin_pay(pluginName, pid, type); + }); + return; + } + var txt = lan.public_backup.buy; + if (type) txt = lan.public_backup.renew; + var payhtml = + '
                            \ +
                            \ +
                            ' + + lan.public_backup.type + + '
                            \ +
                            \ +
                              \ +
                            • ' + + pluginName + + '' + + lan.public_backup.apiece_of_plug + + '
                            • \ +
                            • ' + + lan.public_backup.up_pro + + '' + + lan.public_backup.use_allplug_free + + '
                            • \ +
                            \ +
                            \ +
                            \ +
                            \ +
                            \ +
                            '; + + layer.open({ + type: 1, + title: txt + pluginName, + area: ['616px', '680px'], + closeBtn: 2, + shadeClose: false, + content: payhtml, + }); + setTimeout(function () { + bt.soft.get_product_discount(pluginName, pid); + $('.li-c-item li').click(function () { + var i = $(this).index(); + $(this).addClass('active').siblings().removeClass('active'); + if (i == 0) { + bt.soft.get_product_discount(pluginName, pid); + $('.pro-info').hide(); + } else { + bt.soft.get_product_discount('', 0); + $('.pro-info').show(); + } + }); + $('.pay-btn-group > li').click(function () { + $(this).addClass('active').siblings().removeClass('active'); + }); + }, 100); + }); + }, + + re_plugin_pay_other: function (pluginName, pid, type, price) { + bt.pub.get_user_info(function (rdata) { + if (!rdata.status) { + bt.pub.bind_btname(0, function (rdata) {}); + return; + } + var txt = lan.public_backup.buy; + if (type) txt = lan.public_backup.renew; + var payhtml = + '
                            \ +
                            \ +
                            \ +
                            \ +
                            \ +
                            ' + + txt + + lan.public_backup.duration + + '
                            \ +
                              \ +
                            • ' + + lan.public_backup.month1 + + '
                            • \ +
                            • ' + + lan.public_backup.month3 + + '
                            • \ +
                            • ' + + lan.public_backup.month6 + + '
                            • \ +
                            • ' + + lan.public_backup.year + + '
                            • \ +
                            \ +
                            \ +
                            ' + + lan.public_backup.total + + '' + + lan.public_backup.rmb + + '
                            \ +
                            \ +
                            \ +
                            ' + + lan.public_backup.pay_by_wechatqrcore + + '
                            \ +
                            \ +
                            \ +
                            \ +
                            \ +
                            '; + + layer.open({ + type: 1, + title: txt + pluginName, + area: ['616px', '450px'], + closeBtn: 2, + shadeClose: false, + content: payhtml, + }); + bt.soft.get_rscode_other(pid, price, 1, type); + setTimeout(function () { + $('.pay-btn-group > li').click(function () { + $(this).addClass('active').siblings().removeClass('active'); + }); + }, 100); + }); + }, + get_rscode_other: function (pid, price, cycle, type) { + var loadT = layer.msg(lan.public_backup.get_payment_info, { icon: 16, time: 0, shade: 0.3 }); + $.post('/auth?action=create_plugin_other_order', { pid: pid, cycle: cycle, type: type }, function (rdata) { + layer.close(loadT); + if (!rdata.status) { + layer.closeAll(); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + return; + } + + if (!rdata.msg.code) { + layer.closeAll(); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + soft.flush_cache(); + return; + } + $('.sale-price').text((price * cycle).toFixed(2)); + $('.pay-wx').html(''); + $('.pay-wx').qrcode(rdata.msg.code); + bt.set_cookie('other_oid', rdata.msg.oid); + bt.soft.get_order_stat(rdata.msg.oid, type); + }); + }, + get_order_stat: function (order_id, type) { + if (bt.get_cookie('other_oid') != order_id) return; + setTimeout(function () { + $.post('/auth?action=get_order_stat', { oid: order_id, type: type }, function (stat) { + if (stat == 1) { + layer.closeAll(); + soft.flush_cache(); + return; + } + + if ($('.pay-btn-group').length > 0) { + bt.soft.get_order_stat(order_id, type); + } + }); + }, 1000); + }, + get_voucher_list: function (pid) { + $('#couponlist').html("
                            " + lan.public_backup.loading + '
                            '); + bt.soft.pro.get_voucher(pid, function (rdata) { + if (rdata != null && rdata.length > 0) { + var con = ''; + var len = rdata.length; + for (var i = 0; i < len; i++) { + if (rdata[i].status != 1) { + var cyc = rdata[i].cycle + bt.soft.pro.conver_unit(rdata[i].unit); + if (rdata[i].cycle == 999) { + cyc = lan.public_backup.permanent; + } + con += '
                          • ' + cyc + '
                          • '; + } + } + $('#couponlist').html('
                              ' + con + '
                            '); + $('.pay-btn-group > li').click(function () { + $(this).addClass('active').siblings().removeClass('active'); + $('.paymethod-submit button').css({ 'background-color': '#20a53a', 'border-color': '#20a53a' }); + }); + $('.paymethod-submit button').click(function () { + var _active = $('#couponlist .pay-btn-group .active'), + code = _active.attr('data-code'), + coupon_id = _active.attr('data-coupon-id'), + charge_type = _active.attr('data-charge-type'); + + var _span = $('#couponlist .pay-btn-group .active span'), + cycle = parseInt(_span.html()), + cycle_unit = _span.html.indexOf('Month') ? 'month' : 'year'; + if (code == undefined) { + layer.msg(lan.public_backup.choose_cash_coupon); + } else { + bt.soft.pro.create_order_voucher(pid, code, coupon_id, cycle, cycle_unit, charge_type, function (rdata) { + layer.closeAll(); + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + bt.msg(rdata.res); + }); + } + }); + } else { + $('#couponlist').html("

                            " + lan.public_backup.no_cash_coupon + '

                            '); + } + }); + }, + get_rscode: function (pid, price, sprice, cycle) { + $('.sale-price').text(price); + if (price == sprice) { + $('.cost-price') + .text(sprice + lan.public_backup.rmb) + .hide(); + } else { + $('.cost-price') + .text(sprice + lan.public_backup.rmb) + .show(); + } + $('.pay-wx').html('' + lan.public_backup.loading + ''); + $('.libPay').append('
                            '); + bt.soft.pro.create_order(pid, cycle, function (rdata) { + $('.payloadingmask').remove(); + if (rdata.status === false) { + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + layer.msg(rdata.msg, { icon: 2 }); + return; + } + $('.pay-wx').html(''); + $('.pay-wx').qrcode(rdata.msg); + clearInterval(bt.soft.pub.wxpayTimeId); + if (pid) { + bt.soft.pub.wxpayTimeId = setInterval(function () { + bt.soft.pro.get_plugin_coupon(pid, function (rdata) { + if (rdata.status) { + layer.closeAll(); + clearInterval(bt.soft.pub.wxpayTimeId); + bt.msg({ msg: lan.public_backup.pay_plug_success, icon: 16, time: 0, shade: [0.3, '#000'] }); + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + return; + } + }); + }, 3000); + } else { + bt.soft.pub.wxpayTimeId = setInterval(function () { + bt.soft.pro.get_re_order_status(function (rdata) { + if (rdata.status) { + layer.closeAll(); + clearInterval(bt.soft.pub.wxpayTimeId); + bt.msg({ msg: lan.public_backup.pay_pro_success, icon: 16, time: 0, shade: [0.3, '#000'] }); + bt.set_cookie('force', 1); + if (soft) soft.flush_cache(); + return; + } + }); + }, 3000); + } + }); + }, + get_product_discount: function (pluginName, pid) { + if (pluginName == undefined) pluginName = ''; + if (pid == undefined) pid = 0; + var con = + '
                            \ +
                            ' + + lan.public_backup.pay_method + + '
                            \ +
                            • ' + + lan.public_backup.pay_by_wechat + + '
                            • ' + + lan.public_backup.cash_coupon + + '
                            \ +
                            \ +
                            \ +
                            \ +
                            \ +
                            ' + + lan.public_backup.opening_time + + '
                            \ +
                            \ +
                            \ +
                            ' + + lan.public_backup.total + + '' + + lan.public_backup.rmb + + '
                            \ +
                            \ +
                            \ +
                            ' + + lan.public_backup.pay_by_wechatqrcore + + '
                            \ +
                            \ +
                            \ + \ +
                            '; + $('.libpay-con').html("
                            " + lan.public_backup.loading + '
                            '); + + bt.soft.pro.get_product_discount_by(pluginName, function (rdata) { + if (rdata != null) { + var coucon = ''; + var qarr = Object.keys(rdata); + var qlen = qarr.length; + if (pluginName) qlen = qlen - 1; + //折扣列表 + for (var i = 0; i < qlen; i++) { + var j = qarr[i]; + var a = rdata[j].price.toFixed(2); + var b = rdata[j].sprice.toFixed(2); + var c = rdata[j].discount; + coucon += + '
                          • ' + + bt.soft.pro.conver_unit(j) + + '' + + (c == 1 ? '' : '' + c * 10 + lan.public_backup.discount + '') + + '
                          • '; + } + $('.libpay-con').html(con); + $('#PayCycle').html('
                              ' + coucon + '
                            '); + $('.pay-btn-group li').click(function () { + $(this).addClass('active').siblings().removeClass('active'); + }); + $('.pay-cycle li').click(function () { + var i = $(this).index(); + $(this).addClass('active').siblings().removeClass('active'); + $('.payment-con > div').eq(i).show().siblings().hide(); + }); + $('#PayCycle .pay-btn-group li').eq(0).click(); + } + }); + }, + get_index_list: function (callback) { + bt.send('get_index_list', 'plugin/get_index_list', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_sort_index: function (data, callback) { + var loading = bt.load(); + bt.send('sort_index', 'plugin/sort_index', { ssort: data }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_soft_list: function (p, type, search, callback) { + if (p == undefined) p = 1; + if (type == undefined) type = 0; + if (search == undefined) search = ''; + var force = bt.get_cookie('force'); + if (force == undefined) force = 0; + p = p + ''; + if (p.indexOf('not_load') == -1) { + var loading = bt.load(lan.public.the, 1); + } else { + var loading = null; + p = p.split('not_load')[0]; + } + + bt.send('get_soft_list', 'plugin/get_soft_list', { p: p, type: type, tojs: 'soft.get_list', force: force, query: search }, function (rdata) { + if (loading) loading.close(); + bt.set_cookie('force', 0); + if (rdata.pro_authorization_sn != null) { + bt.set_cookie('serial_no', rdata.pro_authorization_sn); + } else { + bt.clear_cookie('serial_no'); + } + bt.set_cookie('pro_end', rdata.pro); + if (callback) callback(rdata); + }); + }, + to_index: function (name, callback) { + var status = $('#index_' + name).prop('checked') ? '0' : '1'; + if (name.indexOf('php-') >= 0) { + var verinfo = name.replace(/\./, ''); + status = $('#index_' + verinfo).prop('checked') ? '0' : '1'; + } + if (status == 1) { + bt.send('add_index', 'plugin/add_index', { sName: name }, function (rdata) { + rdata.time = 1000; + if (!rdata.status) bt.msg(rdata); + if (callback) callback(rdata); + }); + } else { + bt.send('remove_index', 'plugin/remove_index', { sName: name }, function (rdata) { + rdata.time = 1000; + if (!rdata.status) bt.msg(rdata); + if (callback) callback(rdata); + }); + } + }, + add_make_args: function (name, init) { + name = bt.soft.get_name(name); + pdata = { + name: name, + args_name: $("input[name='make_name']").val(), + init: init, + ps: $("input[name='make_ps']").val(), + args: $("input[name='make_args']").val(), + }; + if (pdata.args_name.length < 1 || pdata.args.length < 1) { + layer.msg('Custom module name and parameter cannot be empty!'); + return; + } + loadT = bt.load('Adding custom module...'); + bt.send('add_make_args', 'plugin/add_make_args', pdata, function (rdata) { + loadT.close(); + bt.soft.get_make_args(name); + bt.msg(rdata); + if (rdata.status === true) bt.soft.loadOpen.close(); + }); + }, + show_make_args: function (name) { + name = bt.soft.get_name(name); + var _aceEditor = ''; + bt.soft.loadOpen = bt.open({ + type: 1, + title: 'Add custom module', + area: '500px', + btn: [lan.public.submit, lan.public.close], + content: + '
                            \ + \ +
                            \ + Name\ +
                            \ + \ +
                            \ +
                            \ +
                            \ + Details\ +
                            \ + \ +
                            \ +
                            \ +
                            \ + Parameter\ +
                            \ + \ +
                            \ +
                            \ +
                            \ + Prefix script\ +
                            \ +
                            \ +
                            \ +
                            \ +
                            \ +
                            ', + success: function (layer, index) { + _aceEditor = ace.edit('preposition_shell', { + theme: 'ace/theme/chrome', //主题 + mode: 'ace/mode/sh', // 语言类型 + wrap: true, + showInvisibles: false, + showPrintMargin: false, + showFoldWidgets: false, + useSoftTabs: true, + tabSize: 2, + showPrintMargin: false, + readOnly: false, + }); + _aceEditor.setValue('# The shell script content executed before compilation is usually prepared for the dependent installation and source download of the third-party module'); + }, + yes: function () { + bt.soft.add_make_args(name, _aceEditor.getValue()); + }, + }); + }, + modify_make_args: function (name, args_name) { + name = bt.soft.get_name(name); + var _aceEditor = ''; + bt.soft.loadOpen = bt.open({ + type: 1, + title: 'Edit custom option module[' + name + ':' + args_name + ']', + area: '500px', + btn: [lan.public.submit, lan.public.close], + content: + '
                            \ + \ +
                            \ + Module name\ +
                            \ + \ +
                            \ +
                            \ +
                            \ + Module details\ +
                            \ + \ +
                            \ +
                            \ +
                            \ + Module parameter\ +
                            \ + \ +
                            \ +
                            \ +
                            \ + Prefix script\ +
                            \ +
                            \ +
                            \ +
                            \ +
                            \ +
                            ', + success: function (layer, index) { + _aceEditor = ace.edit('preposition_shell', { + theme: 'ace/theme/chrome', //主题 + mode: 'ace/mode/sh', // 语言类型 + wrap: true, + showInvisibles: false, + showPrintMargin: false, + showFoldWidgets: false, + useSoftTabs: true, + tabSize: 2, + showPrintMargin: false, + readOnly: false, + }); + _aceEditor.setValue(bt.soft.make_data[args_name].init); + }, + yes: function () { + bt.soft.add_make_args(name, _aceEditor.getValue()); + }, + }); + }, + set_make_args: function (_this, name, args_name) { + name = bt.soft.get_name(name); + if ($('.args_' + args_name)[0].checked) { + bt.soft.make_config.push(args_name); + } else { + index = bt.soft.make_config.indexOf(args_name); + if (index === -1) return; + bt.soft.make_config.splice(index, 1); + } + index = bt.soft.make_config.indexOf(''); + if (index !== -1) bt.soft.make_config.splice(index, 1); + bt.send('set_make_args', 'plugin/set_make_args', { name: name, args_names: bt.soft.make_config.join('\n') }, function (rdata) { + if (!rdata.status) { + bt.msg(rdata); + } + }); + }, + //遍历数组和对象 + each: function (obj, fn) { + var key, + that = this; + if (typeof fn !== 'function') return that; + obj = obj || []; + if (obj.constructor === Object) { + for (key in obj) { + if (fn.call(obj[key], key, obj[key])) break; + } + } else { + for (key = 0; key < obj.length; key++) { + if (fn.call(obj[key], key, obj[key])) break; + } + } + return that; + }, + del_make_args: function (name, args_name) { + name = bt.soft.get_name(name); + bt.confirm({ msg: 'Confirm delete[' + name + ':' + args_name + ']module?', title: 'Delete[' + name + ':' + args_name + ']module!' }, function () { + loadT = bt.load('Removing module[' + args_name + ']...'); + bt.send('del_make_args', 'plugin/del_make_args', { name: name, args_name: args_name }, function (rdata) { + bt.soft.get_make_args(name); + bt.msg(rdata); + }); + }); + }, + get_make_args: function (name) { + name = bt.soft.get_name(name); + loadT = bt.load('Getting optional modules...'); + bt.send('get_make_args', 'plugin/get_make_args', { name: name }, function (rdata) { + loadT.close(); + var module_html = ''; + bt.soft.make_config = rdata.config.split('\n'); + bt.soft.make_data = {}; + for (var i = 0; i < rdata.args.length; i++) { + bt.soft.make_data[rdata.args[i].name] = rdata.args[i]; + var checked_str = bt.soft.make_config.indexOf(rdata.args[i].name) == -1 ? '' : 'checked="checked"'; + module_html += + '\ + \ + \ + \ + ' + + rdata.args[i].name + + '' + + rdata.args[i].ps + + '\ + \ + Edit\ + | Del\ + \ + '; + } + $('.modules_list').html(module_html); + }); + }, + check_make_is: function (name) { + name = bt.soft.get_name(name); + var shows = ['nginx', 'apache', 'mysql', 'php']; + for (var i = 0; i < shows.length; i++) { + if (name.indexOf(shows[i]) === 0) { + return true; + } + } + return false; + }, + get_name: function (name) { + if (name.indexOf('php-') === 0) { + return 'php'; + } + return name; + }, + install: function (name, that) { + var _this = this; + if (bt.soft.is_install) { + layer.msg('Installing other software, please operate later!', { icon: 0 }); + return false; + } + _this.get_soft_find(name, function (rdata) { + var arrs = ['apache', 'nginx', 'mysql']; + if ($.inArray(name, arrs) >= 0 || name.indexOf('php-') >= 0) { + var SelectVersion = '', + shtml = name; + if (rdata.versions.length > 1) { + for (var i = 0; i < rdata.versions.length; i++) { + var item = rdata.versions[i]; + SelectVersion += ''; + } + shtml = "'; + } else { + shtml = "" + name + ''; + } + var loadOpen = bt.open({ + type: 1, + title: name + lan.soft.install_title, + area: '400px', + content: + "
                            \ +
                            " + + lan.soft.install_version + + ':' + + shtml + + "
                            \ +
                            " + + lan.bt.install_type + + ":
                            \ + \ +
                            \ + \ + \ +
                            \ +
                            ', + success: function ($layer, index) { + $layer.find('.btn-close').click(function () { + layer.close(index); + }); + }, + }); + + $('.fangshi input').click(function () { + $(this).attr('checked', 'checked').parent().siblings().find('input').removeAttr('checked'); + var type = $('.fangshi input:eq(0)').prop('checked') ? '0' : '1'; + if (type === '1') { + $('.install_modules').hide(); + return; + } + + if (bt.soft.check_make_is(name)) { + $('.install_modules').show(); + bt.soft.get_make_args(name); + } + }); + + $('#bi-btn').click(function () { + loadOpen.close(); + var info = $('#SelectVersion').val().toLowerCase(); + name = info.split(' ')[0]; + version = info.split(' ')[1]; + var type = $('.fangshi input:eq(0)').prop('checked') ? '0' : '1'; + if (rdata.versions.length > 1) { + _this.install_soft(rdata, version, type); + } else { + _this.install_soft(rdata, rdata.versions[0].m_version, type, that); + } + }); + } else if (rdata.versions.length > 1) { + var SelectVersion = ''; + for (var i = 0; i < rdata.versions.length; i++) { + var item = rdata.versions[i]; + var v_type = parseInt(item.beta) === 1 ? ' Beta' : ' Stable'; + // SelectVersion += ''; + var version = parseInt(rdata.type) === 5 ? item.m_version : item.full_version; + SelectVersion += ''; + } + var loadOpen = bt.open({ + type: 1, + title: name + lan.soft.install_title, + area: '350px', + content: + "
                            \ +
                            " + + lan.soft.install_version + + ":
                            \ +
                            \ + \ + \ +
                            \ +
                            ', + }); + $('#bi-btn').click(function () { + loadOpen.close(); + var info = $('#SelectVersion').val().toLowerCase(); + name = info.split(' ')[0]; + version = info.split(' ')[1]; + _this.install_soft(rdata, version, 0, that); + }); + } else { + // _this.install_soft(rdata, rdata.versions[0].full_version, 0, that); + _this.install_soft(rdata, parseInt(rdata.type) == 5 ? rdata.versions[0].m_version : rdata.versions[0].full_version, 0, that); + } + }); + }, + is_loop_speed: true, + is_install: false, + //显示进度 + show_speed: function () { + bt.send( + 'get_lines', + 'ajax/get_lines', + { + num: 10, + filename: '/tmp/panelShell.pl', + }, + function (rdata) { + if ($('#install_show').length < 1) return; + if (rdata.status === true) { + $('#install_show').text(rdata.msg); + $('#install_show').scrollTop(1000000000); + } + setTimeout(function () { + bt.soft.show_speed(); + }, 1000); + } + ); + }, + loadT: null, + speed_msg: "
                            [MSG]
                            ", + //显示进度窗口 + // show_speed_window: function(msg, callback) { + // bt.soft.loadT = layer.open({ + // title: false, + // type: 1, + // closeBtn: 0, + // shade: 0.3, + // area: "500px", + // offset: "30%", + // content: bt.soft.speed_msg.replace('[MSG]', msg), + // success: function(layers, index) { + // setTimeout(function() { + // bt.soft.show_speed(); + // }, 1000); + // if (callback) callback(); + // } + // }); + // }, + show_speed_window: function (config, callback) { + if (!config.soft) config['soft'] = { type: 10 }; + if (config.soft.type == 5) { + //使用消息盒子安装 + if (callback) callback(); + return false; + } else if (config.soft.type == 10 && !config.status) { + //第三方安装, 非安装,仅下载安装脚本 + if (callback) callback(); + return false; + } + layer.closeAll(); + bt.soft.loadT = layer.open({ + title: config.title || 'Executing setup script, please wait...', + type: 1, + closeBtn: false, + maxmin: true, + shade: false, + skin: 'install_soft', + area: ['500px', '300px'], + content: + "
                            " +
                            +				config.msg +
                            +				'
                            ', + success: function (layers, index) { + if (typeof config.event === 'string') { + $(config.event).removeAttr('onclick').html('Installing'); + } + $('.layui-layer-max').hide(); + bt.soft.is_loop_speed = true; + bt.soft.is_install = true; + bt.soft.show_speed(); + if (callback) callback(); + }, + end: function () { + bt.soft.is_install = false; + bt.soft.is_loop_speed = false; + }, + min: function () { + $('.layui-layer-max').show(); + }, + restore: function () { + $('.layui-layer-max').hide(); + }, + }); + }, + // install_soft: function(item, version, type,that) { //安装单版本 + // if (type == undefined) type = 0; + // item.title = bt.replace_all(item.title, '-' + version, ''); + // var msg = item.type != 5 ? lan.soft.lib_insatll_confirm.replace('{1}', item.title) : lan.get('install_confirm', [item.title, version]); + + // bt.confirm({ msg: '
                            '+msg+'
                            ', title: item.type != 5 ? lan.soft.lib_install : lan.soft.install_title }, function() { + // bt.soft.show_speed_window(lan.soft.lib_install_the, function() { + // bt.send('install_plugin', 'plugin/install_plugin', { sName: item.name, version: version, type: type }, function(rdata) { + + // if (rdata.size) { + // layer.close(bt.soft.loadT); + // _this.install_other(rdata) + // return; + // } + // layer.close(bt.soft.loadT); + // bt.pub.get_task_count(); + // if (soft) soft.get_list(); + // bt.msg(rdata); + // }) + // }) + // }) + // }, + install_soft: function (item, version, type, that) { + //安装单版本 + if (type == undefined) type = 0; + var loadT = ''; + item.title = bt.replace_all(item.title, '-' + version, ''); + layer.confirm( + item.type != 5 ? lan.soft.lib_insatll_confirm.replace('{1}', item.title) : lan.get('install_confirm', [item.title, version]), + { + btn: [lan.public.confirm, lan.public.close], + title: item.type != 5 ? lan.soft.lib_install : lan.soft.install_title, + icon: 0, + closeBtn: 2, + }, + function () { + layer.closeAll(); + bt.soft.show_speed_window( + { + title: 'Installing ' + item.title + ', please wait...', + msg: lan.soft.lib_install_the, + soft: item, + event: that, + }, + function () { + if (item.type == 10) + loadT = layer.msg('Getting third party installation information, please wait', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + bt.send( + 'install_plugin', + 'plugin/install_plugin', + { + sName: item.name, + version: version, + type: type, + }, + function (rdata) { + if (rdata.size) { + layer.close(loadT); + bt.soft.install_other(rdata, status, that); + return; + } + layer.close(bt.soft.loadT); + bt.pub.get_task_count(function (rdata) { + if (rdata > 0 && item.type === 5) messagebox(); + }); + if (!rdata.status) { + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + } else { + if (window.location.pathname != '/soft') { + window.location.reload(); + } + } + setTimeout(function () { + if (typeof soft != 'undefined') soft.get_list(); + }, 2000); + } + ); + } + ); + } + ); + }, + install_other: function (data, status, callback) { + layer.closeAll(); + var loadT = layer.open({ + type: 1, + area: '500px', + title: (data.update ? lan.public_backup.update : lan.public_backup.install) + lan.public_backup.third_party_plug, + closeBtn: 2, + shift: 5, + shadeClose: false, + btn: [data.update ? lan.public_backup.update : lan.public_backup.install, lan.public_backup.cancel], + content: + '\ +
                            \ + \ +
                              \ + ' + + (data.update ? '
                            • ' + lan.public_backup.update_wait + '
                            • ' : '
                            • ' + lan.public_backup.install_wait + '
                            • ' + lan.public_backup.exist_cover + '
                            • ') + + '\ +
                            \ +
                            ', + yes: function (index, event) { + soft.input_zip(data.name, data.tmp_path, data, callback); + }, + }); + }, + update_soft: function (name, title, version, min_version, update_msg, type) { + var _this = this; + var msg = '
                          • ' + lan.public_backup.update_tips + '
                          • '; + if (name == 'mysql') + msg = "
                            • " + lan.public_backup.db_update_tips + '
                            • ' + lan.public_backup.update_tips1 + '
                            • ' + lan.public_backup.update_tips + '
                            '; + if (update_msg) + msg += + '
                            Update description:
                            ' +
                            +				update_msg.replace(/(_bt_)/g, '\n') +
                            +				'

                            '; + bt.show_confirm( + lan.public_backup.update + '[' + title + ']', + lan.public_backup.update_tips2.replace('{1}', title).replace('{2}', version).replace('{3}', min_version), + function () { + // bt.soft.show_speed_window('Updating to [' + title + '-' + version + '.' + min_version + '],Please wait...', function() { + // bt.send('install_plugin', 'plugin/install_plugin', { sName: name, version: version, upgrade: version }, function(rdata) { + // if (rdata.size) { + // _this.install_other(rdata) + // return; + // } + // layer.close(bt.soft.loadT); + // bt.pub.get_task_count(function(rdata){ + // if(rdata > 0 && item.type === 5) messagebox(); + // }); + // if (soft) soft.get_list(); + // if (rdata.status === true && rdata.msg.indexOf('queue') === -1) rdata.msg = 'Update completed!'; + // bt.msg(rdata); + // }) + // }) + // bt.soft.show_speed_window({ title: 'Updating to [' + title + '-' + version + '.' + min_version + '],Please wait...', status: true, soft: { type: parseInt(type) } }, function () { + // bt.send('install_plugin', 'plugin/install_plugin', { sName: name, version: version + '.' + min_version, upgrade: version + '.' + min_version }, function (rdata) { + // console.log(rdata); + // if (rdata.size) { + // _this.install_other(rdata); + // return; + // } + // layer.close(bt.soft.loadT); + // bt.pub.get_task_count(function (rdata) { + // if (rdata > 0 && item.type === 5) messagebox(); + // }); + // if (typeof soft != 'undefined') soft.get_list(); + // bt.msg(rdata); + // }); + // }); + _this.get_soft_find(name, function (item) { + var full_version = parseInt(item.type) === 5 ? version : version + '.' + min_version; + bt.soft.show_speed_window({ title: 'Updating to [' + title + '-' + version + '.' + min_version + '],Please wait...', status: true, soft: { type: parseInt(type) } }, function () { + bt.send('install_plugin', 'plugin/install_plugin', { sName: name, version: full_version, upgrade: full_version }, function (rdata) { + console.log(rdata); + if (rdata.size) { + _this.install_other(rdata); + return; + } + layer.close(bt.soft.loadT); + bt.pub.get_task_count(function (rdata) { + if (rdata > 0 && parseInt(item.type) === 5) messagebox(); + }); + if (typeof soft != 'undefined') soft.get_list(); + bt.msg(rdata); + }); + }); + }); + }, + msg + ); + }, + un_install: function (name) { + var _this = this; + _this.get_soft_find(name, function (item) { + var version = ''; + for (var i = 0; i < item.versions.length; i++) { + if (item.versions[i].setup && bt.contains(item.version, item.versions[i].m_version)) { + version = item.versions[i].m_version; + if (version.indexOf('.') < 0) version += '.' + item.versions[i].version; + break; + } + } + var title = bt.replace_all(item.title, '-' + version, ''); + bt.confirm({ msg: lan.soft.uninstall_confirm.replace('{1}', title).replace('{2}', version), title: lan.soft.uninstall, icon: 3, closeBtn: 2 }, function () { + var loadT = bt.load(lan.soft.lib_uninstall_the); + bt.send('uninstall_plugin', 'plugin/uninstall_plugin', { sName: name, version: version }, function (rdata) { + loadT.close(); + bt.pub.get_task_count(); + if (soft) soft.get_list(); + bt.msg(rdata); + }); + }); + }); + }, + get_soft_find: function (name, callback) { + var loadT = bt.load(); + bt.send('get_soft_find', 'plugin/get_soft_find', { sName: name }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_config_path: function (name) { + var fileName = ''; + if (bt.os == 'Linux') { + switch (name) { + case 'mysql': + case 'mysqld': + fileName = '/etc/my.cnf'; + break; + case 'nginx': + fileName = '/www/server/nginx/conf/nginx.conf'; + break; + case 'pureftpd': + fileName = '/www/server/pure-ftpd/etc/pure-ftpd.conf'; + break; + case 'apache': + fileName = '/www/server/apache/conf/httpd.conf'; + break; + case 'tomcat': + fileName = '/www/server/tomcat/conf/server.xml'; + break; + case 'memcached': + fileName = '/etc/init.d/memcached'; + break; + case 'redis': + fileName = '/www/server/redis/redis.conf'; + break; + case 'openlitespeed': + fileName = '/usr/local/lsws/conf/httpd_config.conf'; + break; + default: + fileName = '/www/server/php/' + name + '/etc/php.ini'; + break; + } + } + return fileName; + }, + set_lib_config: function (name, title) { + var loadT = bt.load(lan.soft.menu_temp); + bt.send('getConfigHtml', 'plugin/getConfigHtml', { name: name }, function (rhtml) { + loadT.close(); + if (rhtml.status === false) { + if (name == 'phpguard') { + layer.msg(lan.soft.menu_phpsafe, { icon: 1 }); + } else { + layer.msg(rhtml.msg, { icon: 2 }); + } + return; + } + bt.open({ + type: 1, + shift: 5, + offset: '20%', + closeBtn: 2, + area: '700px', + title: '' + title, + content: rhtml.replace('"javascript/text"', '"text/javascript"'), + }); + /*rtmp = rhtml.split('',''); + setTimeout(function(){ + if(!!(window.attachEvent && !window.opera)){ + execScript(rcode); + }else{ + window.eval(rcode); + } + },200)*/ + }); + }, + save_config: function (fileName, data) { + var encoding = 'utf-8'; + var loadT = bt.load(lan.soft.the_save); + bt.send('SaveFileBody', 'files/SaveFileBody', { data: data, path: fileName, encoding: encoding }, function (rdata) { + loadT.close(); + bt.msg(rdata); + }); + }, +}; + +bt.database = { + get_list: function (page, search, callback) { + if (page == undefined) page = 1; + search = search == undefined ? '' : search; + var order = bt.get_cookie('order') ? '&order=' + bt.get_cookie('order') : ''; + + var data = 'tojs=database.get_list&table=databases&limit=15&p=' + page + '&search=' + search + order; + bt.pub.get_data(data, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_root_pass: function (callback) { + bt.send('getKey', 'data/getKey', { table: 'config', key: 'mysql_root', id: 1 }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_root: function (type) { + if (type == 'mongo' || type == 'pgsql') { + var t = bt.data.database.getType(); + bt_tools.send('database/' + t + '/get_root_pwd', function (rdata) { + if (type == 'pgsql') bt.data.database.mongo['list'][0]['title'] = lan.database.admin_password; + var bs = bt.render_form(bt.data.database.mongo); + $('.password' + bs).val(rdata.msg); + }); + } else { + bt.database.get_root_pass(function (rdata) { + var bs = bt.render_form(bt.data.database.root); + $('.password' + bs).val(rdata); + }); + } + }, + set_data_pass: function (callback) { + var bs = bt.render_form(bt.data.database.data_pass, function (rdata) { + if (callback) callback(rdata); + }); + return bs; + }, + set_data_access: function (name) { + var loading = bt.load(); + bt.send('GetDatabaseAccess', 'database/GetDatabaseAccess', { name: name }, function (rdata) { + loading.close(); + var bs = bt.render_form(bt.data.database.data_access); + $('.name' + bs).val(name); + $('.bt-form .line .tname').css('width', '125px'); + setTimeout(function () { + if (rdata.msg.permission == '127.0.0.1' || rdata.msg.permission == '%') { + $('.dataAccess' + bs).val(rdata.msg.permission); + } else { + $('.dataAccess' + bs) + .val('ip') + .trigger('change'); + $('#dataAccess_subid').val(rdata.msg.permission); + } + $('#force_ssl').prop('checked', rdata.msg.ssl ? true : false); + $('#force_ssl').change(function () { + var open_type = $('#force_ssl').prop('checked'); + if (open_type) { + var t = + '
                            \ +

                            Warning! This feature requires Advanced Knowledge!

                            \ +
                              \ +
                            • After enabling the forced SSL connection, it may affect your application connection and database performance.
                            • \ +
                            \ +
                            '; + var loadP = layer.confirm( + t, + { + btn: ['Confirm', 'Cancel'], + icon: 3, + area: '561px', + closeBtn: 2, + title: 'Confirm Open?', + }, + function () { + $('#force_ssl').prop('checked', true); + layer.close(loadP); + }, + function () { + $('#force_ssl').prop('checked', false); + layer.close(loadP); + } + ); + } + }); + }, 100); + }); + }, + add_database: function (cloudList, callback) { + var type = bt.data.database.getType(); + if (type === 'mysql') { + bt.data.database.data_add.list[2].items[0].value = bt.get_random(16); + bt.data.database.data_add.list[4].items[0].items = cloudList; + bt.render_form(bt.data.database.data_add, function (rdata) { + if (callback) callback(rdata); + }); + } else { + var copyDataAdd = $.extend(true, {}, bt.data.database.data_add); + copyDataAdd.list[2].items[0].value = bt.get_random(16); + switch (type) { + case 'sqlserver': + case 'mongodb': + case 'pgsql': + delete copyDataAdd.list[0].items[1]; + copyDataAdd.list.splice(3); + copyDataAdd.list.push(bt.data.database.data_add.list[4]); + copyDataAdd.list.push(bt.data.database.data_add.list[5]); + copyDataAdd.list[3].items[0].items = cloudList; + break; + } + // 没有本地或者远程数据库 + if (cloudList.length == 0) { + copyDataAdd.list[copyDataAdd.list.length - 1].hide = true; + } + bt.render_form($.extend(true, {}, copyDataAdd), function (rdata) { + if (callback) callback(rdata); + }); + } + }, + del_database: function (data, callback) { + var loadT = bt.load(lan.get('del_all_task_the', [data.name])); + var type = bt.data.database.getType(); + var params = { url: 'database?action=DeleteDatabase', data: data }; + if (type != 'mysql') { + params.url = 'database/' + type + '/DeleteDatabase'; + params.data = { data: JSON.stringify(data) }; + } + bt_tools.send(params, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + sync_database: function (sid, callback) { + var loadT = bt.load(lan.database.sync_the); + var type = bt.data.database.getType(); + var params = { url: 'database?action=SyncGetDatabases', data: { sid: sid } }; + if (type != 'mysql') { + params.url = 'database/' + type + '/SyncGetDatabases'; + params.data = { data: JSON.stringify({ sid: sid }) }; + } + bt_tools.send(params, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + sync_to_database: function (data, callback) { + var loadT = bt.load(lan.database.sync_the); + var type = bt.data.database.getType(); + var params = { url: 'database?action=SyncToDatabases', data: data }; + if (type != 'mysql') { + params.url = 'database/' + type + '/SyncToDatabases'; + params.data = { data: JSON.stringify(data) }; + } + bt_tools.send(params, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }, + open_phpmyadmin: function (name, username, password) { + if ($('#toPHPMyAdmin').attr('action').indexOf('phpmyadmin') == -1) { + layer.msg(lan.database.phpmyadmin_err, { icon: 2, shade: [0.3, '#000'] }); + setTimeout(function () { + window.location.href = '/soft'; + }, 3000); + return; + } + $('#toPHPMyAdmin').attr('action', $('#toPHPMyAdmin').attr('public-data')); + var murl = $('#toPHPMyAdmin').attr('action'); + $('#pma_username').val(username); + $('#pma_password').val(password); + $('#db').val(name); + layer.msg(lan.database.phpmyadmin, { icon: 16, shade: [0.3, '#000'], time: 1000 }); + setTimeout(function () { + $('#toPHPMyAdmin').submit(); + layer.closeAll(); + }, 200); + }, + submit_phpmyadmin: function (name, username, password, pub) { + if (pub === true) { + $('#toPHPMyAdmin').attr('action', $('#toPHPMyAdmin').attr('public-data')); + } else { + $('#toPHPMyAdmin').attr('action', '/phpmyadmin/index.php'); + } + var murl = $('#toPHPMyAdmin').attr('action'); + $('#pma_username').val(username); + $('#pma_password').val(password); + $('#db').val(name); + layer.msg(lan.database.phpmyadmin, { icon: 16, shade: [0.3, '#000'], time: 1000 }); + setTimeout(function () { + $('#toPHPMyAdmin').submit(); + layer.closeAll(); + }, 200); + }, + input_sql: function (fileName, dataName) { + bt.confirm({ msg: lan.database.input_confirm, title: lan.database.input_title }, function (index) { + var loading = bt.load(lan.database.input_the); + var type = bt.data.database.getType(); + var params = { url: 'database?action=InputSql', data: { file: fileName, name: dataName } }; + if (type != 'mysql') { + params.url = 'database/' + type + '/InputSql'; + params.data = { data: JSON.stringify({ file: fileName, name: dataName }) }; + } + bt_tools.send(params, function (rdata) { + loading.close(); + bt.msg(rdata); + }); + }); + }, + backup_data: function (id, callback) { + var loadT = bt.load(lan.database.backup_the); + var type = bt.data.database.getType(); + var params = { url: 'database?action=ToBackup', data: { id: id } }; + if (type != 'mysql') { + params.url = 'database/' + type + '/ToBackup'; + params.data = { data: JSON.stringify({ id: id }) }; + } + bt_tools.send(params, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + del_backup: function (id, success, error) { + bt.confirm({ msg: lan.database.backup_del_confirm, title: lan.database.backup_del_title }, function (index) { + var loadT = bt.load(); + bt.send('DelBackup', 'database/DelBackup', { id: id }, function (frdata) { + loadT.close(); + bt.msg(frdata); + if (frdata.status) { + success && success(frdata); + } else { + error && error(frdata); + } + }); + }); + }, +}; + +bt.send('get_config', 'config/get_config', {}, function (rdata) { + bt.config = rdata; +}); + +bt.plugin = { + get_plugin_byhtml: function (name, callback) { + bt.send('getConfigHtml', 'plugin/getConfigHtml', { name: name }, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_firewall_state: function (callback) { + var typename = getCookie('serverType'); + var name = 'btwaf_httpd'; + if (typename == 'nginx') name = 'btwaf'; + bt.send('a', 'plugin/a', { name: name, s: 'get_total_all' }, function (rdata) { + if (callback) callback(rdata); + }); + }, +}; + +bt.site = { + get_list: function (page, search, type, callback) { + if (page == undefined) page = 1; + type = type == undefined ? '&type=-1' : '&type=' + type; + search = search == undefined ? '' : search; + var order = bt.get_cookie('order') ? '&order=' + bt.get_cookie('order') : ''; + var data = 'tojs=site.get_list&table=sites&limit=15&p=' + page + '&search=' + search + order + type; + bt.pub.get_data(data, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_domains: function (id, callback) { + var data = 'table=domain&list=True&search=' + id; + bt.pub.get_data( + data, + function (rdata) { + if (callback) callback(rdata); + }, + 1 + ); + }, + get_type: function (callback) { + bt.send('get_site_types', 'site/get_site_types', '', function (rdata) { + if (callback) callback(rdata); + }); + }, + add_type: function (name, callback) { + bt.send('add_site_type', 'site/add_site_type', { name: name }, function (rdata) { + if (callback) callback(rdata); + }); + }, + edit_type: function (data, callback) { + bt.send('modify_site_type_name', 'site/modify_site_type_name', { id: data.id, name: data.name }, function (rdata) { + if (callback) callback(rdata); + }); + }, + del_type: function (id, callback) { + bt.send('remove_site_type', 'site/remove_site_type', { id: id }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_site_type: function (data, callback) { + bt.send('set_site_type', 'site/set_site_type', { id: data.id, site_ids: data.site_array }, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_site_domains: function (id, callback) { + var loading = bt.load(); + bt.send('GetSiteDomains', 'site/GetSiteDomains', { id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + add_domains: function (id, webname, domains, callback) { + var loading = bt.load(); + bt.send('AddDomain', 'site/AddDomain', { domain: domains, webname: webname, id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }, + del_domain: function (siteId, siteName, domain, port, callback) { + var loading = bt.load(); + bt.send('DelDomain', 'site/DelDomain', { id: siteId, webname: siteName, domain: domain, port: port }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }, + get_dirbind: function (id, callback) { + var loading = bt.load(); + bt.send('GetDirBinding', 'site/GetDirBinding', { id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + add_dirbind: function (id, domain, dirName, callback) { + var loading = bt.load(); + bt.send('AddDirBinding', 'site/AddDirBinding', { id: id, domain: domain, dirName: dirName }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + del_dirbind: function (id, callback) { + var loading = bt.load(); + bt.send('DelDirBinding', 'site/DelDirBinding', { id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_dir_rewrite: function (data, callback) { + var loading = bt.load(); + bt.send('GetDirRewrite', 'site/GetDirRewrite', data, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_site_path: function (id, callback) { + bt.send('getKey', 'data/getKey', { table: 'sites', key: 'path', id: id }, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_dir_userini: function (id, path, callback) { + bt.send('GetDirUserINI', 'site/GetDirUserINI', { id: id, path: path }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_dir_userini: function (path, id, callback) { + var loading = bt.load(); + bt.send('SetDirUserINI', 'site/SetDirUserINI', { path: path, id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_logs_status: function (id, callback) { + var loading = bt.load(); + bt.send('logsOpen', 'site/logsOpen', { id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_site_runpath: function (id, path, callback) { + var loading = bt.load(); + bt.send('SetSiteRunPath', 'site/SetSiteRunPath', { id: id, runPath: path }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_site_path: function (id, path, callback) { + var loading = bt.load(); + bt.send('SetPath', 'site/SetPath', { id: id, path: path }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_site_path_new: function (id, path, name, callback) { + var loading = bt.load(); + bt.send('SetPath', 'site/SetPath', { id: id, path: path, name: name }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_site_pwd: function (id, username, password, callback) { + var loading = bt.load(); + bt.send('SetHasPwd', 'site/SetHasPwd', { id: id, username: username, password: password }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + close_site_pwd: function (id, callback) { + var loading = bt.load(); + bt.send('SetHasPwd', 'site/CloseHasPwd', { id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_limitnet: function (id, callback) { + bt.send('GetLimitNet', 'site/GetLimitNet', { id: id }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_limitnet: function (id, perserver, perip, limit_rate, callback) { + var loading = bt.load(); + bt.send('SetLimitNet', 'site/SetLimitNet', { id: id, perserver: perserver, perip: perip, limit_rate: limit_rate }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + close_limitnet: function (id, callback) { + var loading = bt.load(); + bt.send('CloseLimitNet', 'site/CloseLimitNet', { id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_rewrite_list: function (siteName, callback) { + bt.send('GetRewriteList', 'site/GetRewriteList', { siteName: siteName }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_rewrite_tel: function (name, data, callback) { + var loading = bt.load(lan.site.saving_txt); + bt.send('SetRewriteTel', 'site/SetRewriteTel', { name: name, data: data }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_index: function (id, callback) { + bt.send('GetIndex', 'site/GetIndex', { id: id }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_index: function (id, index, callback) { + var loading = bt.load(); + bt.send('SetIndex', 'site/SetIndex', { id: id, Index: index }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_site_config: function (siteName, callback) { + if (bt.os == 'Linux') { + var sPath = '/www/server/panel/vhost/' + bt.get_cookie('serverType') + '/' + siteName + '.conf'; + bt.files.get_file_body(sPath, function (rdata) { + if (callback) callback(rdata); + }); + } + }, + set_site_config: function (siteName, data, encoding, callback) { + var loading = bt.load(lan.site.saving_txt); + if (bt.os == 'Linux') { + var sPath = '/www/server/panel/vhost/' + bt.get_cookie('serverType') + '/' + siteName + '.conf'; + bt.files.set_file_body(sPath, data, 'utf-8', function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + } + }, + set_phpversion: function (siteName, version, other, callback) { + var loading = bt.load(); + bt.send('SetPHPVersion', 'site/SetPHPVersion', { siteName: siteName, version: version, other: other }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + // 重定向列表 + get_redirect_list: function (name, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('GetRedirectList', 'site/GetRedirectList', { sitename: name }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + // 重定向列表 + get_redirect_list: function (name, callback) { + var loadT = layer.load(); + bt.send('GetRedirectList', 'site/GetRedirectList', { sitename: name }, function (rdata) { + layer.close(loadT); + if (callback) callback(rdata); + }); + }, + create_redirect: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('CreateRedirect', 'site/CreateRedirect', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + modify_redirect: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('ModifyRedirect', 'site/ModifyRedirect', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + remove_redirect: function (sitename, redirectname, callback) { + bt.show_confirm(lan.public_backup.del_rep + '[' + redirectname + ']', lan.public_backup.sure_del_rep, function () { + var loadT = bt.load(lan.site.the_msg); + bt.send('DeleteRedirect', 'site/DeleteRedirect', { sitename: sitename, redirectname: redirectname }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }); + }, + get_redirect_config: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('GetRedirectFile', 'site/GetRedirectFile', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + save_redirect_config: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('SaveProxyFile', 'site/SaveRedirectFile', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_site_proxy: function (siteName, callback) { + bt.send('GetProxy', 'site/GetProxy', { name: siteName }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_site_proxy: function (siteName, type, proxyUrl, toDomain, sub1, sub2, callback) { + var loading = bt.load(); + bt.send('SetProxy', 'site/SetProxy', { name: siteName, type: type, proxyUrl: proxyUrl, toDomain: toDomain, sub1: sub1, sub2: sub2 }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_open_proxy_cache: function (siteName, callback) { + var loading = bt.load(); + bt.send('ProxyCache', 'site/ProxyCache', { siteName: siteName }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_proxy_list: function (name, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('GetProxyList', 'site/GetProxyList', { sitename: name }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + create_proxy: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('CreateProxy', 'site/CreateProxy', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + remove_proxy: function (sitename, proxyname, callback) { + bt.show_confirm(lan.public_backup.del_proxy + '[' + proxyname + ']', lan.public_backup.sure_del_proxy, function () { + var loadT = bt.load(lan.site.the_msg); + bt.send('RemoveProxy', 'site/RemoveProxy', { sitename: sitename, proxyname: proxyname }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }); + }, + modify_proxy: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('ModifyProxy', ' site/ModifyProxy', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_proxy_config: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('GetProxyFile', 'site/GetProxyFile', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + save_proxy_config: function (obj, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('SaveProxyFile', 'site/SaveProxyFile', obj, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_site_security: function (id, name, callback) { + bt.send('GetSecurity', 'site/GetSecurity', { id: id, name: name }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_site_security: function (id, name, fix, domains, status, return_rule, callback) { + var loading = bt.load(lan.site.the_msg); + bt.send('SetSecurity', 'site/SetSecurity', { id: id, name: name, fix: fix, domains: domains, status: status, return_rule: return_rule }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_site_301: function (siteName, callback) { + bt.send('Get301Status', 'site/Get301Status', { siteName: siteName }, function (rdata) { + if (callback) callback(rdata); + }); + }, + set_site_301: function (siteName, srcDomain, toUrl, type, callback) { + var loading = bt.load(); + bt.send('Set301Status', 'site/Set301Status', { siteName: siteName, toDomain: toUrl, srcDomain: srcDomain, type: type }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_tomcat: function (siteName, callback) { + var loading = bt.load(lan.public.config); + bt.send('SetTomcat', 'site/SetTomcat', { siteName: siteName }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_site_logs: function (siteName, callback) { + var loading = bt.load(); + bt.send('GetSiteLogs', 'site/GetSiteLogs', { siteName: siteName }, function (rdata) { + loading.close(); + if (rdata.status !== true) rdata.msg = ''; + if (rdata.msg == '') rdata.msg = lan.public_backup.no_log; + if (callback) callback(rdata); + }); + }, + get_site_error_logs: function (siteName, callback) { + var loading = bt.load(); + bt.send( + 'get_site_err_log', + 'site/get_site_err_log', + { + siteName: siteName, + }, + function (rdata) { + loading.close(); + if (rdata.status !== true) rdata.msg = ''; + if (rdata.msg == '') rdata.msg = lan.public_backup.no_log; + if (callback) callback(rdata); + } + ); + }, + get_site_ssl: function (siteName, callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('GetSSL', 'site/GetSSL', { siteName: siteName }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + create_let: function (data, callback) { + var loadT = layer.open({ + title: false, + type: 1, + closeBtn: 0, + shade: 0.3, + area: '500px', + offset: '30%', + content: + "
                            " + lan.public_backup.preparing_for_cert + '...
                            ', + success: function (layers, index) { + bt.site.get_let_logs(); + bt.send('CreateLet', 'site/CreateLet', data, function (rdata) { + layer.close(loadT); + if (callback) callback(rdata); + }); + }, + }); + }, + get_let_logs: function () { + bt.send( + 'get_lines', + 'ajax/get_lines', + { + num: 10, + filename: '/www/server/panel/logs/letsencrypt.log', + }, + function (rdata) { + if ($('#create_lst').text() === '') return; + if (rdata.status === true) { + $('#create_lst').text(rdata.msg); + $('#create_lst').scrollTop($('#create_lst')[0].scrollHeight); + } + setTimeout(function () { + bt.site.get_let_logs(); + }, 1000); + } + ); + }, + get_dns_api: function (callback) { + var loadT = bt.load(); + bt.send('GetDnsApi', 'site/GetDnsApi', {}, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + set_dns_api: function (data, callback) { + var loadT = bt.load(); + bt.send('SetDnsApi', 'site/SetDnsApi', data, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + verify_domain: function (partnerOrderId, siteName, callback) { + var loadT = bt.load(lan.site.ssl_apply_2); + bt.send('Completed', 'ssl/Completed', { partnerOrderId: partnerOrderId, siteName: siteName }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_dv_ssl: function (domain, path, callback) { + var loadT = bt.load(lan.site.ssl_apply_1); + bt.send('GetDVSSL', 'ssl/GetDVSSL', { domain: domain, path: path }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_module_config: function (param, callback) { + var loadT = bt.load('Obtaining the alarm configuration, please wait...'); + bt.send( + 'get_module_config', + 'push/get_module_config', + { + name: param.name, + type: param.type, + }, + function (rdata) { + loadT.close(); + if (callback) callback(rdata); + } + ); + }, + + // 设置 + set_push_config: function (param, callback) { + var loadT = bt.load('Please wait while setting alarm configuration...'); + bt.send( + 'set_push_config', + 'push/set_push_config', + { + name: param.name, + id: param.id, + data: param.data, + }, + function (rdata) { + loadT.close(); + if (callback) callback(rdata); + } + ); + }, + // 获取消息推送配置 + get_msg_configs: function (callback) { + var loadT = bt.load('Getting the message push configuration, please wait...'); + bt.send('get_msg_configs', 'config/get_msg_configs', {}, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + // 下载证书 + download_cert: function (param, callback) { + var loadT = bt.load('Please wait while downloading the certificate...'); + bt.send( + 'download_cert', + 'site/download_cert', + { + siteName: param.siteName, + ssl_type: param.ssl_type || 'csr', + pem: param.pem, + key: param.key, + pwd: param.pwd || '', //密码,非必填 + }, + function (rdata) { + loadT.close(); + if (callback) callback(rdata); + } + ); + }, + get_ssl_info: function (partnerOrderId, siteName, callback) { + var loadT = bt.load(lan.site.ssl_apply_3); + bt.send('GetSSLInfo', 'ssl/GetSSLInfo', { partnerOrderId: partnerOrderId, siteName: siteName }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + set_cert_ssl: function (certName, siteName, callback) { + var loadT = bt.load(lan.public_backup.deploy_cert); + bt.send('SetCertToSite', 'ssl/SetCertToSite', { certName: certName, siteName: siteName }, function (rdata) { + loadT.close(); + site.reload(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }, + remove_cert_ssl: function (certName, callback) { + bt.show_confirm(lan.public_backup.del_cert, lan.public_backup.sure_del_cert, function () { + var loadT = bt.load(lan.site.the_msg); + bt.send('RemoveCert', 'ssl/RemoveCert', { certName: certName }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }); + }, + set_http_to_https: function (siteName, callback) { + var loading = bt.load(); + bt.send('HttpToHttps', 'site/HttpToHttps', { siteName: siteName }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }, + close_http_to_https: function (siteName, callback) { + var loading = bt.load(); + bt.send('CloseToHttps', 'site/CloseToHttps', { siteName: siteName }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + }, + set_ssl: function (siteName, data, callback) { + if (data.path) { + //iis导入证书 + } else { + var loadT = bt.load(lan.site.saving_txt); + bt.send('SetSSL', 'site/SetSSL', { type: 1, siteName: siteName, key: data.key, csr: data.csr }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + } + }, + set_ssl_status: function (action, siteName, callback) { + var loadT = bt.load(lan.site.get_ssl_list); + bt.send(action, 'site/' + action, { updateOf: 1, siteName: siteName }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_cer_list: function (callback) { + var loadT = bt.load(lan.site.the_msg); + bt.send('GetCertList', 'ssl/GetCertList', {}, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_order_list: function (siteName, callback) { + bt.send('GetOrderList', 'ssl/GetOrderList', { siteName: siteName }, function (rdata) { + if (callback) callback(rdata); + }); + }, + del_site: function (data, callback) { + var loadT = bt.load(lan.get('del_all_task_the', [data.webname])); + bt.send('DeleteSite', 'site/DeleteSite', data, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + add_site: function (callback) { + var _form = $.extend(true, {}, bt.data.site.add); + bt.site.get_all_phpversion(function (rdata) { + bt.site.get_type(function (tdata) { + for (var i = 0; i < _form.list.length; i++) { + if (_form.list[i].name == 'version') { + var items = []; + for (var j = rdata.length - 1; j >= 0; j--) { + var o = rdata[j]; + o.value = o.version; + o.title = o.name; + items.push(o); + } + _form.list[i].items = items; + } else if (_form.list[i].name == 'type_id') { + for (var x = 0; x < tdata.length; x++) _form.list[i].items.push({ value: tdata[x].id, title: tdata[x].name }); + } + } + var bs = bt.render_form(_form, function (rdata) { + if (callback) callback(rdata); + }); + $('.placeholder').click(function () { + $(this).hide(); + $('.webname' + bs).focus(); + }); + $('.path' + bs).val($('#defaultPath').text()); + $('.webname' + bs).focus(function () { + $('.placeholder').hide(); + }); + $('.webname' + bs).blur(function () { + if ($(this).val().length == 0) { + $('.placeholder').show(); + } + }); + $('.webname' + bs).focus(function () { + var _this = $(this), + tips = + 'www will not add by default, if you need to access,please add it like:\ +
                            hostname.com\ +
                            www.hostname.com'; + _this.attr('placeholder', ''); + var loadT = layer.tips(tips, _this, { + tips: [1, '#20a53a'], + time: 0, + area: _this[0].clientWidth + 'px', + }); + $(this).one('blur', function () { + layer.close(loadT); + }); + }); + $('.line').on('mouseenter', '.bt-ico-ask', function () { + var idd = $(this).attr('class').split(' ')[1], + tip = $(this).attr('tip'); + layer.tips(tip, '.' + idd + '', { tips: [1, '#d4d4d4'], time: 0, area: '300px' }); + }); + $('.line').on('mouseleave', '.bt-ico-ask', function () { + layer.closeAll('tips'); + }); + $('.domain_textarea').parents('.bt-form').css({ 'max-height': '565px', overflow: 'auto' }); + }); + }); + }, + get_all_phpversion: function (callback) { + bt.send('GetPHPVersion', 'site/GetPHPVersion', {}, function (rdata) { + if (callback) callback(rdata); + }); + }, + get_site_phpversion: function (siteName, callback) { + bt.send('GetSitePHPVersion', 'site/GetSitePHPVersion', { siteName: siteName }, function (rdata) { + if (callback) callback(rdata); + }); + }, + stop: function (id, name, callback) { + bt.confirm({ title: lan.public_backup.stop_site + ' 【' + name + '】', msg: lan.site.site_stop_txt }, function (index) { + if (index > 0) { + var loadT = bt.load(); + bt.send('SiteStop', 'site/SiteStop', { id: id, name: name }, function (ret) { + loadT.close(); + if (site && typeof callback == 'undefined') { + site.get_list(); + } else { + if (callback) callback(ret); + } + bt.msg(ret); + }); + } + }); + }, + start: function (id, name, callback) { + bt.confirm({ title: lan.public_backup.start_site + ' 【' + name + '】', msg: lan.site.site_start_txt }, function (index) { + if (index > 0) { + var loadT = bt.load(); + bt.send('SiteStart', 'site/SiteStart', { id: id, name: name }, function (ret) { + loadT.close(); + if (site && typeof callback == 'undefined') { + site.get_list(); + } else { + if (callback) callback(ret); + } + bt.msg(ret); + }); + } + }); + }, + backup_data: function (id, callback) { + var loadT = bt.load(lan.database.backup_the); + bt.send('ToBackup', 'site/ToBackup', { id: id }, function (rdata) { + loadT.close(); + bt.msg(rdata); + if (callback) callback(rdata); + }); + }, + del_backup: function (id, siteId, siteName) { + bt.confirm({ msg: lan.site.webback_del_confirm, title: lan.site.del_bak_file }, function (index) { + var loadT = bt.load(); + bt.send('DelBackup', 'site/DelBackup', { id: id }, function (frdata) { + loadT.close(); + if (frdata.status) { + if (site) site.site_detail(siteId, siteName); + } + bt.msg(frdata); + }); + }); + }, + set_endtime: function (id, dates, callback) { + var loadT = bt.load(lan.site.saving_txt); + bt.send('SetEdate', 'site/SetEdate', { id: id, edate: dates }, function (rdata) { + loadT.close(); + if (callback) callback(rdata); + }); + }, + get_default_path: function (type, callback) { + var vhref = ''; + if (bt.os == 'Linux') { + switch (type) { + case 0: + vhref = '/www/server/panel/data/defaultDoc.html'; + break; + case 1: + vhref = '/www/server/panel/data/404.html'; + break; + case 2: + var serverType = bt.get_cookie('serverType'); + vhref = '/www/server/apache/htdocs/index.html'; + if (serverType == 'nginx') vhref = '/www/server/nginx/html/index.html'; + break; + case 3: + vhref = '/www/server/stop/index.html'; + break; + } + } + if (callback) callback(vhref); + }, + get_default_site: function (callback) { + var loading = bt.load(); + bt.send('GetDefaultSite', 'site/GetDefaultSite', {}, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + set_default_site: function (name, callback) { + var loading = bt.load(); + bt.send('SetDefaultSite', 'site/SetDefaultSite', { name: name }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_dir_auth: function (id, callback) { + var loading = bt.load(); + bt.send('get_dir_auth', 'site/get_dir_auth', { id: id }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + get_php_deny: function (website, callback) { + var loading = bt.load(); + bt.send('get_file_deny', 'config/get_file_deny', { website: website }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + edit_php_deny: function (data, callback) { + var loading = bt.load(); + bt.send('set_file_deny', 'config/set_file_deny', data, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + del_php_deny: function (data, callback) { + var loading = bt.load(); + bt.send('del_file_deny', 'config/del_file_deny', data, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + create_dir_guard: function (data, callback) { + var loading = bt.load(); + bt.send('set_dir_auth', 'site/set_dir_auth', { id: data.id, name: data.name, site_dir: data.site_dir, username: data.username, password: data.password }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + edit_dir_account: function (data, callback) { + var loading = bt.load(); + bt.send('modify_dir_auth_pass', 'site/modify_dir_auth_pass', { id: data.id, name: data.name, username: data.username, password: data.password }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }, + delete_dir_guard: function (id, data, callback) { + var loading = bt.load(); + bt.show_confirm(lan.public_backup.del + '[' + data + ']', lan.public_backup.del_dir, function () { + bt.send('delete_dir_auth', 'site/delete_dir_auth', { id: id, name: data }, function (rdata) { + loading.close(); + if (callback) callback(rdata); + }); + }); + }, +}; + +bt.form = { + btn: { + close: function (title, callback) { + var obj = { title: lan.public_backup.turn_off, name: 'btn-danger' }; + if (title) obj.title = title; + if (callback) obj['callback'] = callback; + return obj; + }, + submit: function (title, callback) { + var obj = { title: lan.public_backup.submit, name: 'submit', css: 'btn-success' }; + if (title) obj.title = title; + if (callback) obj['callback'] = callback; + return obj; + }, + }, + item: { + data_access: { + title: 'Permission', + items: [ + { + name: 'dataAccess', + type: 'select', + width: '100px', + items: [ + { title: lan.public_backup.local_server, value: '127.0.0.1' }, + { title: lan.public_backup.every_one, value: '%' }, + { title: lan.public_backup.specify_ip, value: 'ip' }, + ], + callback: function (obj) { + var subid = obj.attr('name') + '_subid'; + $('#' + subid).remove(); + if (obj.val() == 'ip') { + obj + .parent() + .append( + '' + ); + } + }, + }, + ], + }, + password: { + title: lan.public_backup.pass, + name: 'password', + items: [ + { + type: 'text', + width: '311px', + value: bt.get_random(16), + event: { + css: 'glyphicon-repeat', + callback: function (obj) { + bt.refresh_pwd(16, obj); + }, + }, + }, + ], + }, + }, +}; + +bt.data = { + database: { + getType: function () { + return bt.get_cookie('db_page_model') || 'mysql'; + }, + root: { + title: lan.database.edit_pass_title, + area: '530px', + list: [ + { + title: lan.public_backup.rootpass, + name: 'password', + items: [ + { + type: 'text', + width: '311px', + event: { + css: 'glyphicon-repeat', + callback: function (obj) { + bt.refresh_pwd(16, obj); + }, + }, + }, + ], + }, + ], + btns: [ + bt.form.btn.close(), + bt.form.btn.submit(lan.public_backup.submit, function (rdata, load) { + var loading = bt.load(); + var type = bt.data.database.getType(); + var params = { url: 'database?action=SetupPassword', data: rdata }; + if (type != 'mysql') { + params.url = 'database/' + type + '/SetupPassword'; + params.data = { data: JSON.stringify(rdata) }; + } + bt_tools.send(params, function (rRet) { + loading.close(); + bt.msg(rRet); + if (rRet.status) load.close(); + }); + }), + ], + }, + mongo: { + title: lan.database.edit_pass_title, + area: '530px', + list: [ + { + title: lan.public_backup.rootpass, + name: 'password', + items: [ + { + type: 'text', + width: '311px', + event: { + css: 'glyphicon-repeat', + callback: function (obj) { + bt.refresh_pwd(16, obj); + }, + }, + }, + ], + }, + ], + btns: [ + bt.form.btn.close(), + bt.form.btn.submit(lan.public_backup.submit, function (rdata, load) { + var loading = bt.load(); + var type = bt.data.database.getType(); + var url = 'database/' + type + '/set_auth_status'; + if (type == 'pgsql') url = 'database/' + type + '/set_root_pwd'; + bt_tools.send( + { + url: url, + data: { data: JSON.stringify($.extend(rdata, { status: 1 })) }, + }, + function (rRet) { + loading.close(); + bt.msg(rRet); + load.close(); + } + ); + }), + ], + }, + data_add: { + title: lan.database.add_title, + area: '530px', + list: [ + { + title: 'DBName', + items: [ + { + name: 'name', + placeholder: lan.public_backup.new_db_name, + type: 'text', + width: '65%', + callback: function (obj) { + $('input[name="db_user"]').val(obj.val()); + }, + }, + { + name: 'codeing', + type: 'select', + width: '27%', + items: [ + { title: 'utf-8', value: 'utf8' }, + { title: 'utf8mb4', value: 'utf8mb4' }, + { title: 'gbk', value: 'gbk' }, + { title: 'big5', value: 'big5' }, + ], + }, + ], + }, + { + name: 'db_user', + title: lan.public_backup.user_name, + placeholder: lan.public_backup.db_user, + width: '65%', + }, + bt.form.item.password, + bt.form.item.data_access, + { + title: lan.public_backup.add_to, + items: [ + { + name: 'sid', + width: '65%', + type: 'select', + items: [], + }, + ], + }, + { + html: '\ +
                            \ + Force SSL\ +
                            \ + \ + \ +
                            \ +
                            \ + ', + }, + ], + btns: [ + bt.form.btn.close(), + bt.form.btn.submit(lan.public_backup.submit, function (rdata, load, callback) { + if (!rdata.address) rdata.address = rdata.dataAccess; + if (!rdata.ps) rdata.ps = rdata.name; + if (!rdata.ssl) rdata.ssl = $('#check_ssl').prop('checked') ? 'REQUIRE SSL' : ''; + var loading = bt.load(); + var type = bt.data.database.getType(); + var param = { + url: 'database/' + type + '/AddDatabase', + data: { data: JSON.stringify(rdata) }, + }; + if (type == 'mysql') { + rdata['dtype'] = 'MySQL'; + param = { url: 'database?action=AddDatabase', data: rdata }; + } + bt_tools.send(param, function (rRet) { + loading.close(); + if (rRet.status) load.close(); + if (callback) callback(rRet); + bt.msg(rRet); + }); + }), + ], + success: function () { + $('[name=sid]').after('' + lan.public.manage_cloud_server + ''); + var type = bt.data.database.getType(); + // 当前类型为mongodb + if (type == 'mongodb') { + // 是否开启安全认证,没开启隐藏用户名跟密码 + if (!mongodb.mongoDBAccessStatus) { + $('.layui-layer.layui-layer-page .line').eq(1).hide(); + $('.layui-layer.layui-layer-page .line').eq(2).hide(); + } + // 远程服务器类型判断 + $('[name=sid]').change(function () { + // 为远程服务器时,默认开启安全认证 + if ($(this).val() != 0) { + $('.layui-layer.layui-layer-page .line').eq(1).show(); + $('.layui-layer.layui-layer-page .line').eq(2).show(); + } else { + if (!mongodb.mongoDBAccessStatuss) { + $('.layui-layer.layui-layer-page .line').eq(1).hide(); + $('.layui-layer.layui-layer-page .line').eq(2).hide(); + } + } + }); + } + }, + }, + data_access: { + title: lan.public_backup.set_db_permissions, + area: '480px', + list: [ + { title: 'name', name: 'name', hide: true }, + bt.form.item.data_access, + { + title: 'Force SSL', + items: [ + { + name: 'force_ssl', + type: 'switch', + value: 'false', + }, + ], + }, + ], + btns: [ + bt.form.btn.close(), + { + title: lan.public_backup.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load) { + var loading = bt.load(); + rdata.access = rdata.dataAccess; + if (rdata.access == 'ip') rdata.access = rdata.address; + rdata.ssl = $('#force_ssl').prop('checked') ? 'REQUIRE SSL' : ''; + bt.send('SetDatabaseAccess', 'database/SetDatabaseAccess', rdata, function (rRet) { + loading.close(); + bt.msg(rRet); + if (rRet.status) load.close(); + }); + }, + }, + ], + }, + data_pass: { + title: lan.public_backup.change_db_pass, + area: '530px', + list: [ + { title: 'id', name: 'id', hide: true }, + { title: lan.public_backup.user_name, name: 'name', disabled: true }, + { + title: lan.public_backup.pass, + name: 'password', + items: [ + { + type: 'text', + event: { + css: 'glyphicon-repeat', + callback: function (obj) { + bt.refresh_pwd(16, obj); + }, + }, + }, + ], + }, + ], + btns: [ + { title: lan.public_backup.turn_off, name: 'close' }, + { + title: lan.public_backup.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + var loading = bt.load(); + var type = bt.data.database.getType(); + var params = { url: 'database?action=ResDatabasePassword', data: rdata }; + if (type != 'mysql') { + params.url = 'database/' + type + '/ResDatabasePassword'; + params.data = { data: JSON.stringify(rdata) }; + } + bt_tools.send(params, function (rRet) { + loading.close(); + bt.msg(rRet); + if (rRet.status) load.close(); + if (callback) callback(rRet); + }); + }, + }, + ], + }, + }, + site: { + add: { + title: lan.site.site_add, + area: '680px', + list: [ + { + title: lan.public_backup.domain, + name: 'webname', + class: 'domain_textarea', + items: [ + { + type: 'textarea', + width: '420px', + height: '80px', + style: 'padding:10px;line-height: 15px;', + callback: function (obj) { + var array = obj.val().split('\n'); + var ress = array[0].split(':')[0]; + var res = bt.strim(ress.replace(new RegExp(/([-.])/g), '_')); + var ftp_user = res; + var data_user = res; + if (!isNaN(res.substr(0, 1))) { + ftp_user = 'ftp_' + ftp_user; + data_user = 'sql_' + data_user; + } + if (data_user.length > 16) data_user = data_user.substr(0, 16); + obj.data('ftp', ftp_user); + obj.data('database', data_user); + $('.ftp_username').val(ftp_user); + $('.datauser').val(data_user); + var _form = obj.parents('div.bt-form'); + var _path_obj = _form.find('input[name="path"]'); + var path = _path_obj.val(); + var defaultPath = $('#defaultPath').text(); + var dPath = bt.rtrim(defaultPath, '/'); + if (path.substr(0, dPath.length) == dPath) _path_obj.val(dPath + '/' + ress); + _form.find('input[name="ps"]').val(ress); + clearTimeout(bt.setTimeouts); + bt.setTimeouts = setTimeout(function () { + if (bt.check_domain(ress)) { + if (ress.indexOf('www.') !== 0) { + $('.redirect_checkbox label').html('Add [www.' + ress + '] domain name to the main domain name'); + } else if (ress.indexOf('www.') === 0) { + $('.redirect_checkbox label').html('Add ' + ress.replace(/^www\./, '') + ' to the main domain'); + } + $('.redirect_checkbox').show(); + } else { + $('.redirect_checkbox,redirect_tourl').hide(); + } + }, 100); + }, + placeholder: lan.public_backup.domian_tips, + }, + ], + }, + { + title: '', + name: 'redirect', + class: 'redirect_checkbox', + hide: true, + items: [ + { + type: 'checkbox', + text: '', + callback: function (obj) { + var domain = $('.redirect_checkbox').find('span').text(), + domain_textarea = $('.domain_textarea textarea'), + domainList = domain_textarea.val().split('\n'), + domain_one = domainList[0].split(':')[0]; + if (obj.redirect) { + domain_textarea.val(domain_textarea.val() + '\r' + domain); + var line = $( + bt.render_form_line({ + title: 'Redirect', + name: 'tourl', + class: 'redirect_tourl', + items: [ + { + type: 'radio_group', + value: 0, + list: [ + { value: 0, text: 'No' }, + { + value: 1, + text: 'Redirect the main domain name [ ' + domain_one + '] to [' + domain + '] domain name', + }, + { + value: 2, + text: 'Redirect the [' + domain + '] domain name to the main domain [' + domain_one + ']', + }, + ], + }, + ], + }).html + ); + $('.redirect_checkbox.line').after(line); + } else { + for (var i = domainList.length - 1; i >= 0; i--) { + if (domainList[i] === domain) domainList.splice(i, 1); + } + domain_textarea.val(domainList.join('\n')); + $('.redirect_checkbox').next('.redirect_tourl').remove(); + } + }, + }, + ], + }, + { title: lan.public_backup.ps, name: 'ps', placeholder: lan.public_backup.site_ps }, + { + title: lan.public_backup.root_dir, + name: 'path', + items: [ + { + type: 'text', + width: '330px', + event: { + css: 'glyphicon-folder-open', + callback: function (obj) { + bt.select_path(obj); + }, + }, + }, + ], + }, + { + title: 'FTP', + items: [ + { + name: 'ftp', + type: 'select', + items: [ + { value: 'false', title: lan.public_backup.dont_create }, + { value: 'true', title: lan.public_backup.create }, + ], + callback: function (obj) { + var subid = obj.attr('name') + '_subid'; + $('#' + subid).remove(); + if (obj.val() == 'true') { + var _bs = obj.parents('div.bt-form').attr('data-id'); + var ftp_user = $('textarea[name="webname"]').data('ftp'); + var item = { + title: lan.public_backup.set_ftp, + class: 'pb0', + name: 'ftp_tips', + items: [ + { name: 'ftp_username', title: lan.public_backup.user_name, width: '160px', value: ftp_user }, + { name: 'ftp_password', title: lan.public_backup.pass, width: '160px', value: bt.get_random(16) }, + ], + ps_help: lan.public_backup.ftp_tips, + }; + var _tr = bt.render_form_line(item); + + obj.parents('div.line').append('
                            ' + _tr.html + '
                            '); + } + }, + }, + ], + }, + { + title: lan.public_backup.db, + items: [ + { + name: 'sql', + type: 'select', + items: [ + { value: 'false', title: lan.public_backup.dont_create }, + { value: 'MySQL', title: 'MySQL' }, + { value: 'SQLServer', title: 'SQLServer' }, + ], + callback: function (obj) { + var subid = obj.attr('name') + '_subid'; + $('#' + subid).remove(); + if (obj.val() != 'false') { + if (bt.os == 'Linux' && obj.val() == 'SQLServer') { + obj.val('false'); + bt.msg({ msg: lan.public_backup.unsupport_sqlserver, icon: 2 }); + return; + } + var _bs = obj.parents('div.bt-form').attr('data-id'); + var data_user = $('textarea[name="webname"]').data('database'); + var item = { + title: lan.public_backup.db_set, + class: 'pb0', + name: 'sql_tips', + items: [ + { name: 'datauser', title: lan.public_backup.user_name, width: '160px', value: data_user }, + { name: 'datapassword', title: lan.public_backup.pass, width: '160px', value: bt.get_random(16) }, + ], + ps_help: lan.public_backup.create_site_tips, + }; + var _tr = bt.render_form_line(item); + obj.parents('div.line').append('
                            ' + _tr.html + '
                            '); + } + }, + }, + { + name: 'codeing', + type: 'select', + items: [ + { value: 'utf8', title: 'utf-8' }, + { value: 'utf8mb4', title: 'utf8mb4' }, + { value: 'gbk', title: 'gbk' }, + { value: 'big5', title: 'big5' }, + ], + }, + ], + }, + { + title: 'Program type', + type: 'select', + name: 'type', + disabled: bt.contains(bt.get_cookie('serverType'), 'IIS') ? false : true, + items: [ + { value: 'PHP', title: 'PHP' }, + { value: 'Asp', title: 'Asp' }, + { value: 'Aspx', title: 'Aspx' }, + ], + callback: function (obj) { + if (obj.val() == 'Asp' || obj.val() == 'Aspx') { + obj.parents('div.line').next().hide(); + } else { + obj.parents('div.line').next().show(); + } + }, + }, + { + title: lan.public_backup.php_v, + name: 'version', + type: 'select', + items: [{ value: '00', title: lan.public_backup.sitic }], + }, + { + title: lan.public_backup.site_classification, + name: 'type_id', + type: 'select', + width: 'auto', + items: [], + }, + { + title: 'SSL', + class: 'ssl_checkbox', + items: [ + { + type: 'checkbox', + name: 'set_ssl', + text: 'Apply for SSL', + callback: function (obj) { + if (!obj.set_ssl) { + $('[name="force_ssl"]').prop('checked', false); + } + }, + }, + { + type: 'checkbox', + name: 'force_ssl', + text: 'HTTP redirect to HTTPS', + callback: function (obj) { + if (obj.force_ssl) { + $('[name="set_ssl"]').prop('checked', true); + } + }, + }, + { + type: 'html', + html: '
                            • If you need to apply for SSL, please make sure that the domain name has added A record resolution for the domain name
                            ', + }, + ], + }, + ], + btns: [ + { title: lan.public_backup.turn_off, name: 'close' }, + { + title: lan.public_backup.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + var loading = bt.load(); + if (!rdata.webname) { + bt.msg({ msg: lan.public_backup.domain_format_not_right, icon: 2 }); + return; + } + var webname = bt.replace_all(rdata.webname, 'http:\\/\\/', ''); + webname = bt.replace_all(webname, 'https:\\/\\/', ''); + var arrs = webname.split('\n'); + var list = []; + var domain_name, port; + for (var i = 0; i < arrs.length; i++) { + if (arrs[i]) { + var temp = arrs[i].split(':'); + var item = {}; + item['name'] = temp[0]; + item['port'] = temp.length > 1 ? temp[1] : 80; + if (!bt.check_domain(item.name)) { + bt.msg({ msg: lan.site.domain_err_txt, icon: 2 }); + return; + } + if (i > 0) { + list.push(arrs[i]); + } else { + domain_name = item.name; + port = item.port; + } + } + } + var domain = {}; + domain['domain'] = domain_name; + domain['domainlist'] = list; + domain['count'] = list.length; + rdata.webname = JSON.stringify(domain); + rdata.port = port; + rdata.tourl = parseInt($('[name="tourl"]:checked').val()); + if (rdata.redirect) { + if (rdata.tourl) { + var domains = $('#tourl_' + rdata.tourl) + .next() + .find('span'); + rdata.redirect = $(domains[0]).text(); + rdata.tourl = $(domains[1]).text(); + } else { + delete rdata.redirect; + delete rdata.tourl; + } + } else { + delete rdata.redirect; + delete rdata.tourl; + } + rdata.set_ssl = rdata.set_ssl ? 1 : 0; + rdata.force_ssl = rdata.force_ssl ? 1 : 0; + bt.send('AddSite', 'site/AddSite', rdata, function (rRet) { + loading.close(); + if (rRet.siteStatus) load.close(); + if (callback) callback(rRet); + }); + }, + }, + ], + }, + }, + ftp: { + add: { + title: lan.ftp.add_title, + area: '530px', + list: [ + { + title: lan.public_backup.user_name, + name: 'ftp_username', + callback: function (obj) { + var defaultPath = $('#defaultPath').text(); + var wootPath = bt.rtrim(defaultPath, '/'); + if (bt.contains($('input[name="path"]').val(), wootPath)) { + $('input[name="path"]').val(wootPath + '/' + obj.val()); + } + }, + }, + { + title: lan.public_backup.pass, + name: 'ftp_password', + items: [ + { + type: 'text', + width: '330px', + value: bt.get_random(16), + event: { + css: 'glyphicon-repeat', + callback: function (obj) { + bt.refresh_pwd(16, obj); + }, + }, + }, + ], + }, + { + title: lan.public_backup.root_dir, + name: 'path', + items: [ + { + type: 'text', + event: { + css: 'glyphicon-folder-open', + callback: function (obj) { + bt.select_path(obj); + }, + }, + }, + ], + }, + ], + btns: [ + { title: lan.public_backup.turn_off, name: 'close' }, + { + title: lan.public_backup.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + var loading = bt.load(); + if (!rdata.ps) rdata.ps = rdata.ftp_username; + bt.send('AddUser', 'ftp/AddUser', rdata, function (rRet) { + loading.close(); + if (rRet.status) load.close(); + if (callback) callback(rRet); + bt.msg(rRet); + }); + }, + }, + ], + }, + set_port: { + title: lan.ftp.port_title, + skin: '', + area: '500px', + list: [{ title: lan.public_backup.default_port, name: 'port', width: '250px' }], + btns: [ + { title: lan.public_backup.turn_off, name: 'close' }, + { + title: lan.public_backup.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + var loading = bt.load(); + bt.send('setPort', 'ftp/setPort', rdata, function (rRet) { + loading.close(); + if (rRet.status) load.close(); + //if(callback) callback(rRet); + bt.msg(rRet); + }); + }, + }, + ], + }, + set_password: { + title: lan.ftp.pass_title, + area: '530px', + list: [ + { title: 'id', name: 'id', hide: true }, + { title: lan.public_backup.user_name, name: 'ftp_username', disabled: true }, + { + title: lan.public_backup.pass, + name: 'new_password', + items: [ + { + type: 'text', + event: { + css: 'glyphicon-repeat', + callback: function (obj) { + bt.refresh_pwd(16, obj); + }, + }, + }, + ], + }, + ], + btns: [ + { title: lan.public_backup.turn_off, name: 'close' }, + { + title: lan.public_backup.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + bt.confirm({ msg: lan.ftp.pass_confirm, title: lan.ftp.stop_title }, function () { + var loading = bt.load(); + bt.send('SetUserPassword', 'ftp/SetUserPassword', rdata, function (rRet) { + loading.close(); + if (rRet.status) load.close(); + if (callback) callback(rRet); + bt.msg(rRet); + }); + }); + }, + }, + ], + }, + }, +}; +var form_group = { + select_all: function (_arry) { + for (var j = 0; j < _arry.length; j++) { + this.select(_arry[j]); + } + }, + select: function (elem) { + $(elem).after( + '
                            请选择
                              ' + ); + var _html = '', + select_el = $(elem), + select_group = select_el.next(), + select_ul = select_group.find('.bt_select_ul'), + select_val = select_group.find('.select_val'), + select_icon = select_group.find('.glyphicon'); + select_el.find('option').each(function (index, el) { + var active = select_el.val() === $(el).val(), + _val = $(el).val(), + _name = $(el).text(); + _html += '
                            • ' + _name + '
                            • '; + if (active) { + select_val.text(_name); + _val !== '' ? select_val.removeClass('default') : select_val.addClass('default'); + } + }); + select_el.hide(); + select_ul.html(_html); + $(elem) + .next('.bt_select_group') + .find('.bt_select_active') + .unbind('click') + .click(function (e) { + if (!$(this).next().hasClass('active')) { + $(this) + .parents() + .find('li') + .siblings() + .find('.bt_select_ul.active') + .each(function () { + is_show_slect_parent(this); + }); + $(this) + .parents('.rec-box') + .siblings() + .find('.bt_select_ul.active') + .each(function () { + is_show_slect_parent(this); + }); + } + is_show_select_ul($(this).next().hasClass('active')); + $(document).click(function (ev) { + is_show_select_ul(true); + $(this).unbind('click'); + ev.stopPropagation(); + ev.preventDefault(); + }); + e.stopPropagation(); + e.preventDefault(); + }); + $(elem) + .next('.bt_select_group') + .find('.bt_select_ul li') + .unbind('click') + .click(function () { + var _val = $(this).attr('data-val'), + _name = $(this).text(); + $(this).addClass('active').siblings().removeClass('active'); + _val !== '' ? select_val.removeClass('default') : select_val.addClass('default'); + select_val.text(_name); + select_el.val(_val); + $(elem) + .find('option[value="' + _val + '"]') + .change(); + is_show_select_ul(true); + }); + function is_show_slect_parent(that) { + $(that).removeClass('active fadeInUp animated'); + $(that).prev().find('.glyphicon').removeAttr('style'); + $(that).parent().removeAttr('style'); + } + function is_show_select_ul(active) { + if (active) { + select_group.removeAttr('style'); + select_icon.css({ transform: 'rotate(0deg)' }); + select_ul.removeClass('active fadeInUp animated'); + } else { + select_group.css('borderColor', '#20a53a'); + select_icon.css({ transform: 'rotate(180deg)' }); + select_ul.addClass('active fadeInUp animated'); + } + } + }, + checkbox: function () { + $('input[type="checkbox"]').each(function (index, el) { + $(el).hide(); + $(el).after('
                              '); + }); + $('.bt_checkbox_group').click(function () { + $(this).prev().click(); + if ($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).prev().removeAttr('checked'); + } else { + $(this).addClass('active'); + $(this).prev().attr('checked', 'checked'); + } + }); + }, +}; + +bt.public = { + // 设置目录配额 + modify_path_quota: function (data, callback) { + var loadT = bt.load(lan.public.modify_path_quota); + $.post('/project/quota/modify_path_quota', data, function (res) { + loadT.close(); + if (callback) callback(res); + }); + }, + + // 设置mysql配额 + modify_mysql_quota: function (data, callback) { + var loadT = bt.load(lan.public.modify_mysql_quota); + $.post('/project/quota/modify_mysql_quota', data, function (res) { + loadT.close(); + if (callback) callback(res); + }); + }, + + /** + * @description 获取quoto容量 + */ + + get_quota_config: function (type) { + return { + fid: 'quota', + title: lan.public.capacity, + width: 120, + template: function (row, index) { + var quota = row.quota; + if (!quota.size) return '' + lan.public.notConfigured + ''; + var size = quota.size * 1024 * 1024; + var speed = ((quota.used / size) * 100).toFixed(2); + var quotaFull = false; + if (quota.size > 0 && quota.used >= size) quotaFull = true; + return ( + '
                              ' + + '
                              ' + + '
                              ' + ); + }, + event: function (row, index, ev) { + var quota = row.quota; + var size = quota.size * 1024 * 1024; + var usedList = bt.format_size(quota.used).split(' '); + var quotaFull = false; + if (quota.size > 0 && quota.used >= size) quotaFull = true; + var types = { + site: lan.site.website, + ftp: lan.site.add_site.ftp, + database: lan.site.database, + }; + layer.open({ + type: 1, + title: '[' + row.name + '] ' + types[type] + ' ' + lan.public.quotaCapacity, + area: '476px', + closeBtn: 2, + btn: [lan.public.save, lan.public.cancel], + content: + '
                              ' + + '' + + lan.public.currentUsedCapacity + + '' + + '
                              ' + + '' + + (!quotaFull ? (quota.size != 0 ? usedList[1] : 'MB') : '') + + '' + + '
                              ' + + '' + + lan.public.quotaCapacity + + '' + + '
                              ' + + 'MB' + + '
                              ' + + '
                              ' + + '
                                ' + + '
                              • ' + + lan.public.capacityTips1 + + '
                              • ' + + '
                              • ' + + lan.public.capacityTips2 + + '
                              • ' + + '
                              • ' + + lan.public.capacityTips3 + + '
                              • ' + + '
                              • ' + + lan.public.capacityTips4 + + '
                              • ' + + '
                              • ' + + lan.public.capacityTips5 + + '
                              • ' + + '
                              ' + + '
                              ', + yes: function (indexs) { + var quota_size = $('[name="quota_size"]').val(); + if (type === 'site' || type === 'ftp') { + bt.public.modify_path_quota({ data: JSON.stringify({ size: quota_size, path: row.path }) }, function (res) { + if (res.status) { + bt.msg(res); + layer.close(indexs); + setTimeout(function () { + location.reload(); + }, 200); + } else { + layer.msg(res.msg, { icon: res.status ? 1 : 2, area: '650px', time: 0, shade: 0.3, closeBtn: 2 }); + } + }); + } else { + bt.public.modify_mysql_quota({ data: JSON.stringify({ size: quota_size, db_name: row.name }) }, function (res) { + bt.msg(res); + if (res.status) { + layer.close(indexs); + setTimeout(function () { + location.reload(); + }, 200); + } + }); + } + }, + }); + }, + }; + }, +}; + +//设置面板SSL +function setPanelSSL() { + var loadT = layer.msg(lan.config.ssl_msg, { icon: 16, time: 0, shade: [0.3, '#000'] }); + bt.send('get_cert_source', 'config/get_cert_source', {}, function (rdata) { + layer.close(loadT); + var sdata = rdata; + var _data = { + title: 'Panel SSL', + area: '630px', + class: 'ssl_cert_from', + list: [ + { + html: + '

                              ' + + lan.config.ssl_open_ps + + '

                              • ' + + lan.config.ssl_open_ps_1 + + '
                              • ' + + lan.config.ssl_open_ps_2 + + '
                              • ' + + lan.config.ssl_open_ps_3 + + '
                              ', + }, + { + title: 'Cert Type', + name: 'cert_type', + type: 'select', + width: '200px', + value: sdata.cert_type, + items: [ + { value: '1', title: 'Self-signed certificate' }, + { value: '2', title: "Let's Encrypt" }, + ], + callback: function (obj) { + var subid = obj.attr('name') + '_subid'; + $('#' + subid).remove(); + if (obj.val() == '2') { + var _tr = bt.render_form_line({ + title: 'Admin E-Mail', + name: 'email', + width: '320px', + placeholder: 'Admin E-Mail', + value: sdata.email, + }); + obj.parents('div.line').append('
                              ' + _tr.html + '
                              '); + } + }, + }, + { + html: + '
                              ' + + lan.config.ssl_open_ps_5 + + '

                              ', + }, + ], + btns: [ + { + title: 'Close', + name: 'close', + callback: function (rdata, load, callback) { + load.close(); + $('#panelSSL').prop('checked', false); + }, + }, + { + title: 'Submit', + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + if (!$('#checkSSL').is(':checked')) { + bt.msg({ status: false, msg: 'Please confirm the risk first!' }); + return; + } + var confirm = layer.confirm('Whether to open the panel SSL certificate', { title: 'Tips', btn: ['Confirm', 'Cancel'], icon: 0, closeBtn: 2 }, function () { + var loading = bt.load(); + bt.send('SetPanelSSL', 'config/SetPanelSSL', rdata, function (rdata) { + loading.close(); + if (rdata.status) { + layer.msg(rdata.msg, { icon: 1 }); + $.get('/system?action=ReWeb', function () {}); + setTimeout(function () { + window.location.href = (window.location.protocol.indexOf('https') != -1 ? 'http://' : 'https://') + window.location.host + window.location.pathname; + }, 1500); + } else { + layer.msg(rdata.msg, { icon: 2 }); + } + }); + }); + }, + }, + ], + end: function () {}, + }; + + var _bs = bt.render_form(_data); + // setTimeout(function () { + // $('.cert_type' + _bs).trigger('change') + // }, 200); + }); +} + +var dynamic = { + loadList: [], + fileFunList: {}, + load: false, + callback: null, + + // 初始化执行 + execution: function () { + for (var i = 0; i < this.loadList.length; i++) { + var fileName = this.loadList[i]; + if (fileName in this.fileFunList) this.fileFunList[fileName](); + } + }, + + /** + * @description 动态加载js,css文件 + * @param urls {string|array} 文件路径或文件数组 + * @param fn {function|undefined} 回调函数 + */ + require: function (urls, fn) { + if (!Array.isArray(urls)) urls = [urls]; + + this.fileFunList = {}; + + var i = 0; + var that = this; + var total = urls.length; + var callback = function () { + i++; + if (i < total) { + that.loadFile(urls[i], callback); + } else { + fn && fn(); + } + }; + this.loadFile(urls[i], callback); + }, + /** + * @description 加载js,css文件 + * @param {string} url 文件路径 + * @param {function} fn 回调函数 + */ + loadFile: function (url, fn) { + this.load = true; + + var that = this; + var element = this.createElement(url); + + if (element.readyState) { + element.onreadystatechange = function (ev) { + if (element.readyState === 'loaded' || element.readyState === 'complete') { + element.onreadystatechange = null; + that.execution(); + fn && fn.call(that); + that.load = false; + } + }; + } else { + element.onload = function (ev) { + that.execution(); + fn && fn.call(that); + that.load = false; + }; + } + document.getElementsByTagName('head')[0].appendChild(element); + }, + /** + * @description 创建元素 + * @param {string} url 文件路径 + * @returns + */ + createElement: function (url) { + var element = null; + if (url.indexOf('.js?v=1715756461542') > -1) { + element = document.createElement('script'); + element.type = 'text/javascript'; + element.src = bt.url_merge('/vue/' + url); + } else if (url.indexOf('.css?v=1715756461542') > -1) { + element = document.createElement('link'); + element.rel = 'stylesheet'; + element.href = bt.url_merge('/vue/' + url); + } + return element; + }, + /** + * @default 执行延迟文件内容执行 + * @param fileName {string} 文件名称,不要加文件后缀 + * @param callback {function} 回调行数 + */ + delay: function delay(fileName, callback) { + if (!this.load) { + callback(); + return false; + } + this.fileFunList[fileName] = callback; + }, +}; + +// 过滤编码 +bt.htmlEncode = { + /** + * @description 正则转换特殊字符 + * @param {string} layid 字符内容 + */ + htmlEncodeByRegExp: function (str) { + if (typeof str == 'undefined' || str.length == 0) return ''; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/ /g, ' ') + .replace(/\'/g, ''') + .replace(/\"/g, '"') + .replace(/\(/g, '(') + .replace(/\)/g, ')') + .replace(/`/g, '`') + .replace(/=/g, '='); + }, +}; diff --git a/BTPanel/static/vite/oldjs/site.js b/BTPanel/static/vite/oldjs/site.js new file mode 100644 index 00000000..7531772f --- /dev/null +++ b/BTPanel/static/vite/oldjs/site.js @@ -0,0 +1,12912 @@ +$('#cutMode .tabs-item').on('click', function () { + var type = $(this).data('type'); + var index = $(this).index(); + $(this).addClass('active').siblings().removeClass('active'); + $('#site_table_view').find('.tab-con-block').eq(index).removeClass('hide').siblings().addClass('hide'); + switch (type) { + case 'php': + $('#bt_site_table').empty(); + if (!window.isSetup) { + $('.site_table_view .mask_layer') + .removeClass('hide') + .find('.prompt_description') + .html( + 'Web server is not installed,Install Nginx  |  Install Apache' + ); + } else { + $('.site_table_view .mask_layer').addClass('hide'); + } + product_recommend.init(function () { + site.php_table_view(); + }); + // site.get_types(); + break; + case 'nodejs': + $('#bt_node_table').empty(); + $.get('/plugin?action=getConfigHtml', { name: 'nodejs' }, function (res) { + // if(typeof res !== 'string') $('.site_table_view .mask_layer').removeClass('hide').find('.prompt_description').html('Node version manager is not installed,Click install'); + if (typeof res !== 'string') { + $('#bt_node_table+.mask_layer') + .removeClass('hide') + .find('.prompt_description') + .html('Node version manager is not installed,Click install'); + } else { + $('#bt_node_table+.mask_layer').addClass('hide'); + } + }); + site.node_porject_view(); + break; + } + bt.set_cookie('site_model', type); +}); + +var site_table; +var node_table; +var countryList = []; +var site = { + node: { + /** + * @description 选择路径配置 + * @return config {object} 选中文件配置 + * + */ + get_project_select_path: function (path) { + var that = this; + return { + type: 'text', + width: '320px', + name: 'project_script', + value: path, + placeholder: 'Please select the project startup file and enter the startup command. It cannot be empty', + icon: { + type: 'glyphicon-folder-open', + select: 'file', + event: function (ev) {}, + }, + }; + }, + get_project_select: function (path) { + var that = this; + return { + type: 'select', + name: 'project_script', + width: '200px', + disabled: true, + unit: '* Get the startup mode in package.json', + placeholder: 'Select the project continue', + list: path + ? function (configs) { + that.get_project_script_list(path, configs[2], this); + } + : [], + change: function (formData, elements, formConfig) { + var project_script = $("[data-name='project_script']"); + if (formData.project_script === '') { + if ($('#project_script_two').length === 0) { + project_script + .parent() + .after( + '
                              ' + ); + } + } else { + project_script.parent().next().remove(); + } + }, + }; + }, + + /** + * @description 选择启动脚本配置 + * @param path {string} 项目目录 + * @param form {object} 表单元素 + * @param formObject {object} 表单对象 + * @return config {object} 选中文件配置 + */ + get_project_script_list: function (path, form, formObject) { + var that = this; + that.get_start_command( + { project_cwd: path }, + function (res) { + var arry = []; + for (var resKey in res) { + arry.push({ title: resKey + ' 【' + res[resKey] + '】', value: resKey }); + } + arry.push({ title: 'Custom command', value: '' }); + form.group = that.get_project_select(path); + form.group.list = arry; + form.group.disabled = false; + formObject.$replace_render_content(2); + if (arry.length === 1) { + var project_script = $("[data-name='project_script']"); + // form.group.value = ''; + project_script + .parent() + .after( + '
                              ' + ); + } + }, + function () { + form.label = 'Start file/command'; + form.group = that.get_project_select_path(path); + formObject.$replace_render_content(2); + } + ); + return []; + }, + + /** + * + * @description 获取Node版本列表 + * @return {{dataFilter: (function(*): *[]), url: string}} + */ + get_node_version_list: function () { + return { + url: '/project/nodejs/get_nodejs_version', + dataFilter: function (res) { + if (res.length === 0) { + layer.closeAll(); + bt.msg({ + status: false, + msg: 'Please open [Node Version Manager], install at least 1 Node version to continue', + }); + return; + } + var arry = []; + for (var i = 0; i < res.length; i++) { + arry.push({ title: res[i], value: res[i] }); + } + return arry; + }, + }; + }, + + /** + * @description 获取Node通用Form配置 + * @param config {object} 获取配置参数 + * @return form模板 + */ + get_node_general_config: function (config) { + config = config || {}; + var that = this, + formLineConfig = [ + { + label: 'Path', + group: { + type: 'text', + width: '350px', + name: 'project_cwd', + readonly: true, + icon: { + type: 'glyphicon-folder-open', + event: function (ev) {}, + callback: function (path) { + var filename = path.split('/'); + var project_script_config = this.config.form[2], + project_name_config = this.config.form[1], + project_ps_config = this.config.form[6]; + project_name_config.group.value = filename[filename.length - 1]; + project_ps_config.group.value = filename[filename.length - 1]; + project_script_config.group.disabled = false; + this.$replace_render_content(1); + this.$replace_render_content(6); + that.get_project_script_list(path, project_script_config, this); + }, + }, + value: bt.get_cookie('sites_path') ? bt.get_cookie('sites_path') : '/www/wwwroot', + placeholder: 'Please select the project directory', + }, + }, + { + label: 'Name', + group: { + type: 'text', + name: 'project_name', + width: '350px', + placeholder: 'Please enter the name of the Node project', + input: function (formData, formElement, formConfig) { + var project_ps_config = formConfig.config.form[6]; + project_ps_config.group.value = formData.project_name; + formConfig.$replace_render_content(6); + }, + }, + }, + { + label: 'Run opt', + group: (function () { + return that.get_project_select(config.path); + })(), + }, + { + label: 'Port', + group: { + type: 'number', + name: 'port', + width: '200px', + placeholder: 'Port of the project', + unit: '* Port of the project', + }, + }, + { + label: 'User', + group: { + type: 'select', + name: 'run_user', + width: '150px', + unit: '* No special requirements,choose www user', + list: [ + { title: 'www', value: 'www' }, + { title: 'root', value: 'root' }, + ], + tips: 'sssss', + }, + }, + { + label: 'Node', + group: { + type: 'select', + name: 'nodejs_version', + width: '150px', + unit: '* Choose the right Node version, Install other', + list: (function () { + return that.get_node_version_list(); + })(), + }, + }, + { + label: 'Remarks', + group: { + type: 'text', + name: 'project_ps', + width: '420px', + placeholder: 'Please enter project remarks', + value: config.ps, + }, + }, + { + label: 'Domain name', + group: { + type: 'textarea', //当前表单的类型 支持所有常规表单元素、和复合型的组合表单元素 + name: 'domains', //当前表单的name + style: { width: '420px', height: '120px', 'line-height': '22px' }, + tips: { + //使用hover的方式显示提示 + text: 'Please enter the domain name to be bound, this option can be empty
                              One domain name per line, the default is port 80
                              Pan-analysis adding method *.domain.com
                              If the format of the additional port is www.domain.com:88', + style: { top: '10px', left: '15px' }, + }, + }, + }, + { + group: { + type: 'help', + list: [ + '[Run opt]: The scripts list in package.json is read by default, or you can select the [Custom Command] option to manually enter the start command', + '[Custom start]: You can select the startup file or directly enter the startup command. Supported startup methods: npm/node/pm2/yarn', + '[Port]:The wrong port will lead to access to 502, if you dont know the port, you can change to the correct port after starting', + '[User]:For security reasons, the www user is used by default to run, and root user running may bring security risks', + ], + }, + }, + ]; + + if (config.path) { + formLineConfig.splice(-1, 1); + return formLineConfig.concat([ + { + label: 'Boot', + group: { + type: 'checkbox', + name: 'is_power_on', + width: '220px', + title: 'Follow the system to start the service', + }, + }, + { + label: '', + group: { + type: 'button', + name: 'saveNodeConfig', + title: 'Save', + event: function (data, form, that) { + if (data.project_cwd === '') { + bt.msg({ status: false, msg: 'The project directory cannot be empty' }); + return false; + } + var project_script_two = $('[name="project_script_two"]'); + if ((data.project_script === '' && project_script_two.length < 1) || (project_script_two.length > 1 && project_script_two.val() === '')) { + bt.msg({ status: false, msg: 'Start file/command cannot be empty' }); + return false; + } + if (data.port === '') { + bt.msg({ status: false, msg: 'Project port cannot be empty' }); + return false; + } + if (data.project_script === '') { + data.project_script = project_script_two.val(); + delete data.project_script_two; + } + config.callback(data, form, that); + }, + }, + }, + ]); + } + return formLineConfig; + }, + + /** + * @description 添加node项目表单 + * @returns {{form: 当前实例对象, close: function(): void}} + */ + add_node_form: function (callback) { + var that = this; + var add_node_project = bt_tools.open({ + title: 'Add Node project', + area: '700px', + btn: ['Confirm', 'Cancel'], + content: { + class: 'pd30', + form: (function () { + return that.get_node_general_config({ + form: add_node_project, + }); + })(), + }, + yes: function (form, indexs, layers) { + var defaultParam = { + bind_extranet: 0, + is_power_on: 1, + max_memory_limit: 4096, + project_env: '', + }; + if (form.domains !== '') { + var arry = form.domains.replace('\n', '').split('\r'), + newArry = []; + for (var i = 0; i < arry.length; i++) { + var item = arry[i]; + if (bt.check_domain(item)) { + newArry.push(item.indexOf(':') > -1 ? item : item + ':80'); + } else { + bt.msg({ + status: false, + msg: '[' + item + '] The format of the bound domain name is incorrect', + }); + break; + } + } + defaultParam.bind_extranet = 1; + defaultParam.domains = newArry; + } + if (form.project_name === '') { + bt.msg({ status: false, msg: 'Project name cannot be empty' }); + return false; + } + var project_script_two = $('[name="project_script_two"]'); + if (project_script_two.length && project_script_two.val() === '') { + bt.msg({ status: false, msg: 'Please enter a custom startup command, it cannot be empty!' }); + return false; + } + if (form.port === '') { + bt.msg({ status: false, msg: 'Project port cannot be empty' }); + return false; + } + if (form.project_script === null) { + bt.msg({ status: false, msg: 'Please select the project directory to get the start command!' }); + return false; + } + form = $.extend(form, defaultParam); + if (project_script_two.length) { + form.project_script = project_script_two.val(); + delete form.project_script_two; + } + var _command = null; + setTimeout(function () { + if (_command < 0) return false; + _command = that.request_module_log_command({ shell: 'tail -f /www/server/panel/logs/npm-exec.log' }); + }, 500); + site.node.add_node_project(form, function (res) { + if (!res.status) _command = -1; + if (_command > 0) layer.close(_command); + if (callback) callback(res, indexs); + }); + }, + }); + return add_node_project; + }, + + /** + * @description 添加node项目请求 + * @param param {object} 请求参数 + * @param callback {function} 回调函数 + */ + add_node_project: function (param, callback) { + this.http({ create_project: false, verify: false }, param, callback); + }, + + /** + * @description 获取Node环境 + * @param callback {function} 回调函数 + */ + get_node_environment: function (callback) { + bt_tools.send( + { + url: '/project/nodejs/is_install_nodejs', + }, + function (res) { + if (callback) callback(res); + }, + { load: 'Get the Node project environment' } + ); + }, + + /** + * @description 编辑Node项目请求 + * @param param {object} 请求参数 + * @param callback {function} 回调函数 + */ + modify_node_project: function (param, callback) { + this.http({ modify_project: 'Modify Node project configuration' }, param, callback); + }, + + /** + * @description 删除Node项目请求 + * @param param {object} 请求参数 + * @param callback {function} 回调函数 + */ + remove_node_project: function (param, callback) { + this.http({ remove_project: 'Delete Node project' }, param, callback); + }, + + /** + * @description 获取node项目域名 + * @param callback {function} 回调行数 + */ + get_node_project_domain: function (callback) { + this.http({ project_get_domain: 'Get the list of Node project domain names' }, callback); + }, + + /** + * @description 获取启动命令列表 + * @param param {object} 请求参数 + * @param callback {function} 成功回调行数 + * @param callback1 {function} 错误回调行数 + */ + get_start_command: function (params, callback, callback1) { + this.http({ get_run_list: 'Getting project start command' }, params, callback, callback1); + }, + /** + * @description 添加Node项目域名 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + add_node_project_domain: function (param, callback) { + this.http({ project_add_domain: false, verify: false }, param, callback); + }, + + /** + * @description 删除Node项目域名 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + remove_node_project_domain: function (param, callback) { + this.http({ project_remove_domain: 'Delete the Node project domain name' }, param, callback); + }, + + /** + * @description 启动Node项目 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + start_node_project: function (param, callback) { + this.http({ start_project: 'Enable Node project' }, param, callback); + }, + + /** + * @description 停止Node项目 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + stop_node_project: function (param, callback) { + this.http({ stop_project: 'Stop the Node project' }, param, callback); + }, + + /** + * @description 重启Node项目 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + restart_node_project: function (param, callback) { + this.http({ restart_project: 'Restart the Node project' }, param, callback); + }, + + /** + * @description 获取值指定Node项目信息 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + get_node_project_info: function (param, callback) { + this.http({ get_project_info: 'Get Node project information' }, param, callback); + }, + + /** + * @description 绑定外网映射 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + bind_node_project_map: function (param, callback) { + this.http({ bind_extranet: 'Mapping', verify: false }, param, callback); + }, + /** + * @description 绑定外网映射 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + unbind_node_project_map: function (param, callback) { + this.http({ unbind_extranet: 'Unmapping', verify: false }, param, callback); + }, + /** + * @description 安装node项目依赖 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + install_node_project_packages: function (param, callback) { + this.http({ install_packages: false, verify: false }, param, callback); + }, + + /** + * @description 安装指定模块 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + npm_install_node_module: function (param, callback) { + this.http({ install_module: 'Install Node module' }, param, callback); + }, + /** + * @description 更新指定模块 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + upgrade_node_module: function (param, callback) { + this.http({ upgrade_module: 'Update Node module' }, param, callback); + }, + /** + * @description 删除指定模块 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ uninstall_node_module: function (param, callback) { + this.http({ uninstall_module: 'Uninstall the Node module' }, param, callback); + }, + /** + * @description 模拟点击 + */ + simulated_click: function (num) { + $('.bt-w-menu p:eq(' + num + ')').click(); + }, + /** + * @description 获取Node项目信息 + * @param row {object} 当前行,项目信息 + */ + set_node_project_view: function (row) { + var that = this; + bt.open({ + type: 1, + title: 'Node project management-[' + row.name + '], add time [' + row.addtime + ']', + skin: 'node_project_dialog', + area: ['860px', '750px'], + content: + '
                              ' + + '' + + '
                              ' + + '
                              ' + + '
                              Please turn on mapping to viewing the configuration information
                              ' + + '
                              ', + btn: false, + success: function (layers) { + var $layers = $(layers), + $content = $layers.find('#webedit-con'); + + function reander_tab_list(config) { + for (var i = 0; i < config.list.length; i++) { + var item = config.list[i], + tab = $('

                              ' + item.title + '

                              '); + $(config.el).append(tab); + (function (i, item) { + tab.on('click', function (ev) { + $('.mask_module').addClass('hide'); + $(this).addClass('bgw').siblings().removeClass('bgw'); + if ($(this).hasClass('bgw')) { + that.get_node_project_info({ project_name: row.name }, function (res) { + config.list[i].event.call(that, $content, res, ev); + }); + } + }); + if (item.active) tab.click(); + })(i, item); + } + } + + reander_tab_list({ + el: $layers.find('.bt-w-menu'), + list: [ + { + title: 'Project config', + active: true, + event: that.reander_node_project_config, + }, + { + title: 'Domain', + event: that.reander_node_domain_manage, + }, + { + title: 'Mapping', + event: that.reander_node_project_map, + }, + { + title: 'URL rewrite', + event: that.reander_node_project_rewrite, + }, + { + title: 'Config file', + event: that.reander_node_file_config, + }, + { + title: 'SSL', + event: that.reander_node_project_ssl, + }, + { + title: 'Load status', + event: that.reander_node_service_condition, + }, + { + title: 'service status', + event: that.reander_node_service_status, + }, + { + title: 'Module', + event: that.reander_node_project_module, + }, + { + title: 'Project log', + event: that.reander_node_project_log, + }, + { + title: 'Website log', + event: that.reander_node_site_log, + }, + ], + }); + }, + }); + }, + + /** + * @description 渲染Node项目配置视图 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + * @param that {object} 当前node项目对象 + */ + reander_node_project_config: function (el, rows) { + var row = $.extend(true, {}, rows); + var that = this, + edit_node_project = bt_tools.form({ + el: '#webedit-con', + data: row.project_config, + class: 'ptb10', + form: (function () { + var fromConfig = that.get_node_general_config({ + form: edit_node_project, + path: row.path, + ps: row.ps, + callback: function (data, form, formNew) { + data['is_power_on'] = data['is_power_on'] ? 1 : 0; + var project_script_two = $('[name="project_script_two"]'); + if (project_script_two.length && project_script_two.val() === '') { + bt.msg({ + status: false, + msg: 'Please enter a custom startup command, it cannot be empty!', + }); + return false; + } + if (form.port === '') { + bt.msg({ status: false, msg: 'Project port cannot be empty' }); + return false; + } + if (form.project_script === null) { + bt.msg({ + status: false, + msg: 'Please select the project directory to get the start command!', + }); + return false; + } + site.node.modify_node_project(data, function (res) { + if (res.status) { + row['project_config'] = $.extend(row, data); + row['path'] = data.project_script; + row['ps'] = data.ps; + } + bt.msg({ status: res.status, msg: res.data }); + site.node.simulated_click(0); + }); + }, + }); + setTimeout(function () { + var is_existence = false, + list = fromConfig[2].group.list; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + if (item.value === rows.project_config.project_script) { + is_existence = true; + break; + } + } + if (!is_existence && list.length > 1) { + $('[data-name="project_script"] li:eq(' + (list.length - 1) + ')').click(); + $('[name="project_script_two"]').val(rows.project_config.project_script); + } + if (list.length === 1) { + $('[data-name="project_script"] li:eq(0)').click(); + $('[name="project_script_two"]').val(rows.project_config.project_script); + } + }, 250); + + fromConfig[1].group.disabled = true; + fromConfig[fromConfig.length - 3].hide = true; + fromConfig[fromConfig.length - 3].group.disabled = true; + return fromConfig; + })(), + }); + setTimeout(function () { + $(el).append( + '
                                ' + + '
                              • [Run opt]: The scripts list in package.json is read by default, or you can select the [Custom Command] option to manually enter the start command
                              • ' + + '
                              • [Custom command]: You can select the startup file or directly enter the startup command. Supported startup methods: npm/node/pm2/yarn
                              • ' + + '
                              • [Port]:The wrong port will lead to access to 502, if you don’t know the port, you can fill it out at will, and then change to the correct port after starting the project
                              • ' + + '
                              • [User]:For security reasons, the www user is used by default to run, and root user running may bring security risks
                              • ' + + '
                              ' + ); + if (!row.listen_ok) + $(el) + .find('input[name="port"]') + .parent() + .after( + '
                              The project port may be wrong, it is detected that the current project listens to the following ports[ ' + + row.listen.join('/') + + ' ]
                              ' + ); + }, 100); + }, + + /** + * @description 渲染Node项目服务状态 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_service_status: function (el, row) { + var arry = [ + { title: 'Start', event: this.start_node_project }, + { title: 'Stop', event: this.stop_node_project }, + { title: 'Restart', event: this.restart_node_project }, + ], + that = this, + html = $('

                              '); + + function reander_service(status) { + var status_info = status ? ['Start', '#20a53a', 'play'] : ['Stop', 'red', 'pause']; + return 'Status: ' + status_info[0] + ''; + } + + html.find('.status').html(reander_service(row.run)); + el.html(html); + for (var i = 0; i < arry.length; i++) { + var item = arry[i], + btn = $(''); + (function (btn, item, indexs) { + !(row.run && indexs === 0) || btn.addClass('hide'); + !(!row.run && indexs === 1) || btn.addClass('hide'); + btn + .on('click', function () { + bt.confirm( + { + title: item.title + 'Project-[' + row.name + ']', + msg: 'Are you sure you want the' + item.title + 'item,' + (row.run ? 'The project may be affected,' : '') + 'continue?', + }, + function (index) { + layer.close(index); + item.event.call(that, { project_name: row.name }, function (res) { + row.run = indexs === 0 ? true : indexs === 1 ? false : row.run; + html.find('.status').html(reander_service(row.run)); + $('.sfm-opt button').eq(0).addClass('hide'); + $('.sfm-opt button').eq(1).addClass('hide'); + $('.sfm-opt button') + .eq(row.run ? 1 : 0) + .removeClass('hide'); + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + }); + } + ); + }) + .text(item.title); + })(btn, item, i); + el.find('.sfm-opt').append(btn); + } + }, + + /** + * @description 渲染Node项目域名管理 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_domain_manage: function (el, row) { + var that = this, + list = [ + { + class: 'mb0', + items: [ + { + name: 'nodedomain', + width: '340px', + type: 'textarea', + placeholder: + 'Please enter the domain name to be mapped, this option can be empty
                              One domain name per line, default port 80
                              How to add wildcard domain names *.domain.com
                              Specify the port used www.domain.com:88', + }, + { + name: 'btn_node_submit_domain', + text: 'Add', + type: 'button', + callback: function (sdata) { + var arrs = sdata.nodedomain.split('\n'); + var domins = []; + for (var i = 0; i < arrs.length; i++) domins.push(arrs[i]); + that.add_node_project_domain({ project_name: row.name, domains: domins }, function (res) { + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + if (res.status) { + $('[name=nodedomain]').val(''); + $('.placeholder').css('display', 'block'); + project_domian.$refresh_table_list(true); + } + }); + }, + }, + ], + }, + ]; + var _form_data = bt.render_form_line(list[0]), + loadT = null, + placeholder = null; + el.html(_form_data.html + '
                              '); + bt.render_clicks(_form_data.clicks); + // domain样式 + $('.btn_node_submit_domain').addClass('pull-right').css('margin', '30px 35px 0 0'); + $('textarea[name=nodedomain]').css('height', '120px'); + placeholder = $('.placeholder'); + placeholder + .click(function () { + $(this).hide(); + $('.nodedomain').focus(); + }) + .css({ + width: '340px', + heigth: '120px', + left: '0px', + top: '0px', + 'padding-top': '10px', + 'padding-left': '15px', + }); + $('.nodedomain') + .focus(function () { + placeholder.hide(); + loadT = layer.tips(placeholder.html(), $(this), { tips: [1, '#20a53a'], time: 0, area: $(this).width() }); + }) + .blur(function () { + if ($(this).val().length == 0) placeholder.show(); + layer.close(loadT); + }); + var project_domian = bt_tools.table({ + el: '#project_domian_list', + url: '/project/nodejs/project_get_domain', + default: 'No domain name list yet', + param: { project_name: row.name }, + height: 375, + beforeRequest: function (params) { + if (params.hasOwnProperty('data') && typeof params.data === 'string') return params; + return { data: JSON.stringify(params) }; + }, + column: [ + { type: 'checkbox', class: '', width: 20 }, + { + fid: 'name', + title: 'Domain Name', + type: 'text', + template: function (row) { + return '' + row.name + ''; + }, + }, + { + fid: 'port', + title: 'Port', + type: 'text', + }, + { + title: 'OPT', + type: 'group', + width: '100px', + align: 'right', + group: [ + { + title: 'Del', + event: function (rowc, index, ev, key, rthat) { + bt.confirm( + { + title: 'Delete domain [ ' + row.name + ' ]', + msg: lan.site.domain_del_confirm, + }, + function () { + that.remove_node_project_domain( + { + project_name: row.name, + domain: rowc.name + ':' + rowc.port, + }, + function (res) { + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + rthat.$refresh_table_list(true); + } + ); + } + ); + }, + }, + ], + }, + ], + tootls: [ + { + // 批量操作 + type: 'batch', + positon: ['left', 'bottom'], + placeholder: 'Please select bulk operation', + buttonValue: 'Batch operation', + disabledSelectValue: 'Please select the site that needs batch operation!', + selectList: [ + { + title: 'Delete domain name', + load: true, + url: '/project/nodejs/project_remove_domain', + param: function (crow) { + return { + data: JSON.stringify({ + project_name: row.name, + domain: crow.name + ':' + crow.port, + }), + }; + }, + callback: function (that) { + // 手动执行,data参数包含所有选中的站点 + bt.show_confirm('Delete domain names in bulk', "Delete the selected domain name at the same time, do you want to continue?", function () { + var param = {}; + that.start_batch(param, function (list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += + '' + + item.name + + '
                              ' + + (item.request.status ? 'Success' : 'Fail') + + '
                              '; + } + project_domian.$batch_success_table({ + title: 'Batch deletion', + th: 'Delete domain name', + html: html, + }); + project_domian.$refresh_table_list(true); + }); + }); + }, + }, + ], + }, + ], + }); + setTimeout(function () { + $(el).append( + '
                                ' + + '
                              • If yours is an HTTP project and needs to be mapped, please bind at least one domain name
                              • ' + + '
                              • It is recommended that all domain names use the default port 80
                              • ' + + '
                              ' + ); + }, 100); + }, + + /** + * @description 渲染Node项目映射 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_map: function (el, row) { + var that = this; + el.html( + '
                              ' + + ' Mapping' + + '
                              ' + + ' ' + + ' ' + + '
                              ' + + '
                              • If your project is an HTTP project and you need to access the Internet through 80443, please use the mapping
                              • Before using the mapping, please add at least 1 domain name in [Domain Name Management]
                              ' + ); + $('#node_project_map').attr('checked', row['project_config']['bind_extranet'] ? true : false); + $('[name=node_project_map]').click(function () { + var _check = $('#node_project_map').prop('checked'), + param = { project_name: row.name }; + if (!_check) param['domains'] = row['project_config']['domains']; + layer.confirm( + (!_check ? 'Enable' : 'Disable') + ' mapping!,do you want to continue?', + { + btn: ['Confirm', 'Cancel'], + title: 'Mapping', + icon: 0, + closeBtn: 2, + cancel: function () { + $('#node_project_map').attr('checked', _check); + }, + }, + function () { + that[_check ? 'unbind_node_project_map' : 'bind_node_project_map'](param, function (res) { + if (!res.status) $('#node_project_map').attr('checked', _check); + bt.msg({ status: res.status, msg: typeof res.data != 'string' ? res.error_msg : res.data }); + row['project_config']['bind_extranet'] = _check ? 0 : 1; + }); + }, + function () { + $('#node_project_map').attr('checked', _check); + } + ); + }); + }, + + /** + * @description 渲染Node项目模块 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_module: function (el, row) { + var that = this; + el.html( + '
                              ' + + '
                              ' + + '
                              ' + + '
                              ' + ); + var node_project_module_table = bt_tools.table({ + el: '#node_module_list', + url: '/project/nodejs/get_project_modules', + default: 'The module is not installed, click one-click to install the project module, the default prompt when the data is empty', + param: { project_name: row.name, project_cwd: row.path }, + height: '576px', + load: 'Retrieving module list, please wait...', + beforeRequest: function (params) { + if (params.hasOwnProperty('data') && typeof params.data === 'string') return params; + return { data: JSON.stringify(params) }; + }, + column: [ + { + fid: 'name', + title: 'Module', + type: 'text', + }, + { + fid: 'version', + title: 'Ver', + type: 'text', + width: '60px', + }, + { + fid: 'license', + title: 'License', + type: 'text', + template: function (row) { + if (typeof row.license === 'object') return '' + row.license.type + ''; + return '' + row.license + ''; + }, + }, + { + fid: 'description', + title: 'Description', + width: 235, + type: 'text', + template: function (row) { + return '' + row.description + ''; + }, + }, + { + title: 'OPT', + type: 'group', + width: '125px', + align: 'right', + group: [ + { + title: 'Update', + event: function (rowc, index, ev, key, rthat) { + bt.show_confirm('Update module', "Updating the [" + rowc.name + '] module may affect the operation of the project, continue?', function () { + that.upgrade_node_module({ project_name: row.name, mod_name: rowc.name }, function (res) { + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + rthat.$refresh_table_list(true); + }); + }); + }, + }, + { + title: 'Uninstall', + event: function (rowc, index, ev, key, rthat) { + bt.show_confirm('Uninstall the module', "Uninstalling the [" + rowc.name + '] module may affect the operation of the project, continue?', function () { + that.uninstall_node_module( + { + project_name: row.name, + mod_name: rowc.name, + }, + function (res) { + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + rthat.$refresh_table_list(true); + } + ); + }); + }, + }, + ], + }, + ], + success: function (config) { + // 隐藏一键安装 + if (config.data.length > 0) $('.npm_install_node_config').addClass('hide'); + }, + }); + //安装模块 + $('.install_node_module').on('click', function () { + var _mname = $('input[name=mname]').val(); + if (!_mname) return layer.msg('Please enter the module name and version', { icon: 2 }); + that.npm_install_node_module({ project_name: row.name, mod_name: _mname }, function (res) { + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + node_project_module_table.$refresh_table_list(true); + }); + }); + //一键安装项目模块 + $('.npm_install_node_config').on('click', function () { + var _command = that.request_module_log_command({ shell: 'tail -f /www/server/panel/logs/npm-exec.log' }); + that.install_node_project_packages({ project_name: row.name }, function (res) { + if (res.status) { + node_project_module_table.$refresh_table_list(true); + } + layer.close(_command); + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + }); + }); + }, + + /** + * @description 渲染Node项目伪静态 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_rewrite: function (el, row) { + el.empty(); + if (row.project_config.bind_extranet === 0) { + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show(); + return false; + } + site.edit.get_rewrite_list({ name: 'node_' + row.name }, function () { + $('.webedit-box .line:first').remove(); + $('[name=btn_save_to]').remove(); + $('.webedit-box .help-info-text li:first').remove(); + }); + }, + /** + * @description 渲染Node配置文件 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_file_config: function (el, row) { + el.empty(); + if (row.project_config.bind_extranet === 0) { + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show(); + return false; + } + site.edit.set_config({ name: 'node_' + row.name }); + }, + /** + * @description 渲染node项目使用情况 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_service_condition: function (el, row) { + if (!row.run) { + el.html('').next().removeClass('hide'); + if (el.next().find('.node_mask_module_text').length === 1) { + el.next() + .find('.node_mask_module_text') + .hide() + .parent() + .append( + '
                              Please start the service first and try again,Set service status
                              ' + ); + } else { + el.next().find('.node_mask_module_text:eq(1)').show().prev().hide(); + } + return false; + } + el.html( + '
                              PID
                              ' + ); + var _option = '', + tabelCon = ''; + for (var load in row.load_info) { + if (row.load_info.hasOwnProperty(load)) { + _option += ''; + } + } + var node_pid = $('[name=node_project_pid]'); + node_pid.html(_option); + node_pid + .change(function () { + var _pid = $(this).val(), + rdata = row['load_info'][_pid], + fileBody = '', + connectionsBody = ''; + for (var i = 0; i < rdata.open_files.length; i++) { + var itemi = rdata.open_files[i]; + fileBody += + '' + + '' + + itemi['path'] + + '' + + '' + + itemi['mode'] + + '' + + '' + + itemi['position'] + + '' + + '' + + itemi['flags'] + + '' + + '' + + itemi['fd'] + + '' + + ''; + } + for (var k = 0; k < rdata.connections.length; k++) { + var itemk = rdata.connections[k]; + connectionsBody += + '' + + '' + + itemk['client_addr'] + + '' + + '' + + itemk['client_rport'] + + '' + + '' + + itemk['family'] + + '' + + '' + + itemk['fd'] + + '' + + '' + + itemk['local_addr'] + + '' + + '' + + itemk['local_port'] + + '' + + '' + + itemk['status'] + + '' + + ''; + } + + // tabelCon = reand_table_config([ + // [{"名称":rdata.name},{"PID":rdata.pid},{"状态":rdata.status},{"父进程":rdata.ppid}], + // [{"用户":rdata.user},{"Socket":rdata.connects},{"CPU":rdata.cpu_percent},{"线程":rdata.threads}], + // [{"内存":rdata.user},{"io读":rdata.connects},{"io写":rdata.cpu_percent},{"启动时间":rdata.threads}], + // [{"启动命令":rdata.user}], + // ]) + // + // console.log(tabelCon) + // + // + // function reand_table_config(conifg){ + // var html = ''; + // for (var i = 0; i < conifg.length; i++) { + // var item = conifg[i]; + // html += ''; + // for (var j = 0; j < item; j++) { + // var items = config[j],name = Object.keys(items)[0]; + // console.log(items,name) + // html += ''+ name +''+ items[name] +'' + // } + // console.log(html) + // html += '' + // } + // return '
                              '+ html +'
                              '; + // } + + tabelCon = + '
                              ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
                              Name' + + rdata.name + + 'Status' + + rdata.status + + 'User' + + rdata.user + + 'Start Time' + + getLocalTime(rdata.create_time) + + '
                              PID' + + rdata.pid + + 'PPID' + + rdata.ppid + + 'Thread' + + rdata.threads + + 'Socket' + + rdata.connects + + '
                              CPU' + + rdata.cpu_percent + + '%RAM' + + ToSize(rdata.memory_used) + + 'Disk/R' + + ToSize(rdata.io_read_bytes) + + 'Dis/W' + + ToSize(rdata.io_write_bytes) + + '
                              Command' + + rdata.exe + + '
                              ' + + '
                              ' + + '

                              Network

                              ' + + '
                              ' + + '
                              ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + connectionsBody + + '' + + '
                              Client addressClient portProtocolFDlocal addresslocal portStatus
                              ' + + '
                              ' + + '
                              ' + + '

                              Open files

                              ' + + '
                              ' + + '
                              ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + fileBody + + '' + + '
                              Filesmodepositionflagsfd
                              ' + + '
                              ' + + '
                              '; + $('.node_project_pid_datail').html(tabelCon); + bt_tools.$fixed_table_thead('#nodeNetworkList'); + bt_tools.$fixed_table_thead('#nodeFileList'); + }) + .change() + .html(_option); + }, + + /** + * @description 渲染Node项目日志 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_log: function (el, row) { + el.html('
                              '); + bt_tools.send( + { + url: '/project/nodejs/get_project_log', + type: 'GET', + data: { data: JSON.stringify({ project_name: row.name }) }, + }, + function (res) { + $('#webedit-con .node_project_log').html('
                              ' + (typeof res == 'object' ? res.error_msg : res) + '
                              '); + $('.command_output_pre').scrollTop($('.command_output_pre').prop('scrollHeight')); + }, + { load: 'Get Node project log', verify: false } + ); + }, + + reander_node_site_log: function (el, row) { + el.empty(); + if (row.project_config.bind_extranet === 0) { + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show(); + return false; + } + site.edit.get_site_logs({ name: row.name }); + }, + + /** + * @description node项目SSL + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_ssl: function (el, row) { + el.empty(); + if (row.project_config.bind_extranet === 0) { + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show(); + return false; + } + site.set_ssl({ name: row.name, ele: el, id: row.id }); + site.ssl.reload(); + }, + /** + * @description 请求模块日志终端 + * @param config {object} 当前配置数据 + */ + request_module_log_command: function (config) { + var r_command = layer.open({ + title: config.name || 'Module is being installed, please wait...', + type: 1, + closeBtn: 0, + area: ['500px', '342px'], + skin: config.class || 'module_commmand', + shadeClose: false, + content: '
                              ', + success: function () { + bt_tools.command_line_output({ + el: '.site_module_command', + shell: config.shell, + area: config.area || ['100%', '300px'], + }); + }, + }); + return r_command; + }, + + /** + * @description 请求封装 + * @param keyMethod 接口名和loading,键值对 + * @param param {object || function} 参数,可为空,为空则为callback参数 + * @param callback {function} 成功回调函数 + * @param callback1 {function} 错误调函数 + */ + http: function (keyMethod, param, callback, callback1) { + var method = Object.keys(keyMethod), + config = { + url: '/project/nodejs/' + method[0], + data: (param && { data: JSON.stringify(param) }) || {}, + }, + success = function (res) { + callback && callback(res); + }; + if (callback1) { + bt_tools.send(config, success, callback1, { + load: keyMethod[method[0]], + verify: method[1] ? keyMethod[method[1]] : true, + }); + } else { + bt_tools.send(config, success, { + load: keyMethod[method[0]], + verify: method[1] ? keyMethod[method[1]] : true, + }); + } + }, + }, + node_porject_view: function () { + var node_table = bt_tools.table({ + el: '#bt_node_table', + url: '/project/nodejs/get_project_list', + minWidth: '1000px', + autoHeight: true, + default: 'The item list is empty', //数据为空时的默认提示\ + load: 'Getting the list of Node projects, please wait...', + beforeRequest: function (params) { + if (params.hasOwnProperty('data') && typeof params.data === 'string') { + var oldParams = JSON.parse(params['data']); + delete params['data']; + return { data: JSON.stringify($.extend(oldParams, params)) }; + } + return { data: JSON.stringify(params) }; + }, + column: [ + { type: 'checkbox', class: '', width: 20 }, + { + fid: 'name', + title: 'Name', + width: 85, + type: 'link', + event: function (row, index, ev) { + site.node.set_node_project_view(row); + }, + template: function (row, index) { + return ( + '' + + row.name + + '' + ); + }, + }, + { + fid: 'run', + title: 'Status', + width: 85, + config: { + icon: true, + list: [ + [true, 'Running', 'bt_success', 'glyphicon-play'], + [false, 'Stop', 'bt_danger', 'glyphicon-pause'], + ], + }, + type: 'status', + event: function (row, index, ev, key, that) { + var status = row.run; + bt.confirm( + { + title: status ? 'Stop project' : 'Startup project', + msg: status ? 'After stopping the project, the project service will stop running, continue?' : 'Startup Node project [' + row.name + '], continue operation?', + }, + function (index) { + layer.close(index); + site.node[status ? 'stop_node_project' : 'start_node_project']({ project_name: row.name }, function (res) { + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + that.$refresh_table_list(true); + }); + } + ); + }, + }, + { + fid: 'pid', + title: 'PID', + width: 160, + type: 'text', + template: function (row) { + if ($.isEmptyObject(row['load_info'])) return '-'; + var _id = []; + for (var i in row.load_info) { + if (row.load_info.hasOwnProperty(i)) { + _id.push(i); + } + } + return '' + _id.join(',') + ''; + }, + }, + { + title: 'CPU', + type: 'text', + width: 70, + template: function (row) { + if ($.isEmptyObject(row['load_info'])) return '-'; + var _cpu_total = 0; + for (var i in row.load_info) { + _cpu_total += row.load_info[i]['cpu_percent']; + } + return '' + _cpu_total.toFixed(2) + '%'; + }, + }, + { + title: 'RAM', + type: 'text', + width: 80, + template: function (row) { + if ($.isEmptyObject(row['load_info'])) return '-'; + var _cpu_total = 0; + for (var i in row.load_info) { + _cpu_total += row.load_info[i]['memory_used']; + } + return '' + bt.format_size(_cpu_total) + ''; + }, + }, + { + fid: 'path', + title: 'Root directory', + tips: 'Open Directory', + type: 'link', + event: function (row, index, ev) { + openPath(row.path); + }, + template: function (row, index) { + return ( + '' + + row.path + + '' + ); + }, + }, + { + fid: 'node_version', + title: 'Node version', + type: 'text', + width: 102, + template: function (row) { + return '' + row['project_config']['nodejs_version'] + ''; + }, + }, + { + fid: 'ps', + title: 'Remark', + type: 'input', + blur: function (row, index, ev, key, that) { + if (row.ps == ev.target.value) return false; + bt.pub.set_data_ps({ id: row.id, table: 'sites', ps: ev.target.value }, function (res) { + bt_tools.msg(res, { is_dynamic: true }); + }); + }, + keyup: function (row, index, ev) { + if (ev.keyCode === 13) { + $(this).blur(); + } + }, + }, + { + fid: 'ssl', + title: 'SSL', + tips: 'Deployment certificate', + width: 100, + type: 'text', + template: function (row, index) { + var _ssl = row.ssl, + _info = '', + _arry = [ + ['issuer', 'issuer'], + ['notAfter', 'Due date'], + ['notBefore', 'Application date'], + ['dns', 'Available domain names'], + ]; + try { + if (typeof row.ssl.endtime != 'undefined') { + if (row.ssl.endtime < 0) { + return 'Exp in ' + Math.row.ssl.endtime + ' days'; + } + } + } catch (error) {} + for (var i = 0; i < _arry.length; i++) { + var item = _ssl[_arry[i][0]]; + _info += _arry[i][1] + ':' + item + (_arry.length - 1 != i ? '\n' : ''); + } + return row.ssl === -1 + ? 'Not Set' + : 'Exp in ' + row.ssl.endtime + ' days'; + }, + event: function (row) { + site.node.set_node_project_view(row); + setTimeout(function () { + $('.site-menu p:eq(5)').click(); + }, 500); + }, + }, + { + title: 'OPT', + type: 'group', + width: 100, + align: 'right', + group: [ + { + title: 'Set', + event: function (row, index, ev, key, that) { + site.node.set_node_project_view(row); + }, + }, + { + title: 'Del', + event: function (row, index, ev, key, that) { + bt.prompt_confirm('Delete item', 'You are deleting the Node project-[' + row.name + '], continue?', function () { + site.node.remove_node_project({ project_name: row.name }, function (res) { + bt.msg({ status: res.status, msg: res.data || res.error_msg }); + node_table.$refresh_table_list(true); + }); + }); + }, + }, + ], + }, + ], + sortParam: function (data) { + return { order: data.name + ' ' + data.sort }; + }, + // 渲染完成 + tootls: [ + { + // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: 'Add Node project', + active: true, + event: function (ev) { + site.node.add_node_form(function (res, index) { + if (res.status) { + layer.close(index); + node_table.$refresh_table_list(true); + } + bt.msg({ + status: res.status, + msg: (!Array.isArray(res.data) ? res.data : false) || res.error_msg, + }); + }); + }, + }, + { + title: 'Node version manager', + event: function (ev) { + bt.soft.set_lib_config('nodejs', 'Node.js version manager'); + }, + }, + ], + }, + { + // 搜索内容 + type: 'search', + positon: ['right', 'top'], + placeholder: 'Please enter the project name', + searchParam: 'search', //搜索请求字段,默认为 search + value: '', // 当前内容,默认为空 + }, + { + // 批量操作 + type: 'batch', //batch_btn + positon: ['left', 'bottom'], + placeholder: 'Please select bulk operation', + buttonValue: 'Batch operation', + disabledSelectValue: 'Please select the site that needs batch operation!', + selectList: [ + { + title: 'Delete item', + url: '/project/nodejs/remove_project', + param: function (row) { + return { + data: JSON.stringify({ project_name: row.name }), + }; + }, + refresh: true, + callback: function (that) { + bt.prompt_confirm('Delete items in bulk', 'You are deleting the selected Node project. Continue?', function () { + that.start_batch({}, function (list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += + '' + + item.name + + '
                              ' + + (item.requests.status ? item.requests.data : item.requests.error_msg) + + '
                              '; + } + node_table.$batch_success_table({ + title: 'Delete items in bulk', + th: 'project name', + html: html, + }); + node_table.$refresh_table_list(true); + }); + }); + }, + }, + ], + }, + { + //分页显示 + type: 'page', + positon: ['right', 'bottom'], // 默认在右下角 + pageParam: 'p', //分页请求字段,默认为 : p + page: 1, //当前分页 默认:1 + numberParam: 'limit', + //分页数量请求字段默认为 : limit + number: 20, + //分页数量默认 : 20条 + numberList: [10, 20, 50, 100, 200], // 分页显示数量列表 + numberStatus: true, // 是否支持分页数量选择,默认禁用 + jump: true, //是否支持跳转分页,默认禁用 + }, + ], + }); + }, + php_table_view: function () { + var hoverInfo = {}; + $('#bt_site_table').empty(); + + $('.site_table_view').after( + '
                              \ +
                              \ +
                              \ + ' + + lan.site.copy + + '\ + ' + + lan.site.rename + + '\ +
                              \ +
                              ' + ); + + // 点击复制网站 + $('.web_name_copy') + .unbind() + .click(function (e) { + var clipboard = new ClipboardJS('#site_name_copy'); + clipboard.on('success', function (e) { + bt.msg({ + msg: lan.mail_sys.success_copy, + icon: 1, + }); + }); + clipboard.on('error', function (e) { + bt.msg({ + msg: lan.pgsql.cp_fail, + icon: 2, + }); + }); + $('#site_name_copy').attr('data-clipboard-text', hoverInfo.site); + $('#site_name_copy').click(function (e) { + e.stopPropagation(); + }); + $('#site_name_copy').click(); + }); + + // 点击重命名网站 + $('.web_name_rename') + .unbind() + .click(function (e) { + var site_name = hoverInfo.site, + site_id = hoverInfo.id; + $(this) + .parent() + .prev() + .find('.web_name_text') + .html('') + .find('input') + .focus() + .click(function (e) { + e.stopPropagation(); + }); + $(this) + .parent() + .prev() + .find('.web_name_text') + .find('input') + .blur(function () { + var new_name = $(this).val(); + if (new_name == site_name) { + $('.web_name_hover').addClass('hide'); + return; + } + if (new_name != '') { + bt_tools.send( + { + url: '/site?action=site_rname', + data: { id: site_id, rname: escapeXml(new_name) }, + }, + function (res) { + site_table.$refresh_table_list(true); + bt.msg(res); + $('.web_name_hover').addClass('hide'); + } + ); + } + }); + $(this) + .parent() + .prev() + .find('.web_name_text') + .find('input') + .keyup(function (e) { + if (e.keyCode == 13) { + $(this).blur(); + } + }); + // 防止网站xss + function escapeXml(unsafe) { + return unsafe.replace(/[<>&'"]/g, function (c) { + switch (c) { + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + case "'": + return '''; + case '"': + return '"'; + } + }); + } + e.stopPropagation(); + }); + + site_table = bt_tools.table({ + el: '#bt_site_table', + url: '/data?action=getData', + cookiePrefix: 'site_table', // cookie前缀,用于状态存储,如果不设置,着所有状态不存储, + param: { table: 'sites' }, //参数 + minWidth: '1000px', + autoHeight: true, + default: 'Site list is empty', // 数据为空时的默认提示 + beforeRequest: function (param) { + param.type = bt.get_cookie('site_type') || -1; + return param; + }, + column: [ + { type: 'checkbox', class: '', width: 20 }, + { + fid: 'rname', + title: lan.site.site_name, + sort: true, + sortValue: 'asc', + class: 'site_name', + type: 'link', + width: 130, + isDisabled: true, + event: function (row, index, ev) { + site.web_edit(row, true); + }, + // template: function (row, index) { + // return ''; + // } + template: function (row, index) { + var install = false; + var recomConfig = product_recommend.get_recommend_type(5); + if (recomConfig) { + for (var j = 0; j < recomConfig['list'].length; j++) { + var item = recomConfig['list'][j]; + if (item.name == 'btwaf' || item.name == 'btwaf_httpd') { + if (item.install === true) install = true; + } + } + } + if (bt.get_cookie('serverType') == 'openlitespeed') { + return ( + '' + ); + } else { + var color = !$.isEmptyObject(row.waf) && row.waf.status && install ? 'green' : 'grey'; + return ( + '
                              \ + \ + ' + + row.rname + + '\ + \ +
                              ' + ); + } + }, + }, + { + fid: 'status', + title: lan.site.status, + sort: true, + width: 85, + config: { + icon: true, + list: [ + ['1', lan.site.running_text, 'bt_success', 'glyphicon-play'], + ['0', lan.site.stopped, 'bt_danger', 'glyphicon-pause'], + ], + }, + type: 'status', + event: function (row, index, ev, key, that) { + bt.site[parseInt(row.status) ? 'stop' : 'start'](row.id, row.name, function (res) { + if (res.status) that.$modify_row_data({ status: parseInt(row.status) ? '0' : '1' }); + }); + }, + }, + { + fid: 'backup_count', + title: lan.site.backup, + width: 80, + type: 'link', + template: function (row, index) { + var backup = lan.site.backup_no, + _class = 'bt_warning'; + if (row.backup_count > 0) (backup = lan.site.backup_yes), (_class = 'bt_success'); + return '' + backup + (row.backup_count > 0 ? '(' + row.backup_count + ')' : '') + ''; + }, + event: function (row, index) { + site.backup_site_view({ id: row.id, name: row.name }, site_table); + }, + }, + { + fid: 'path', + title: lan.site.root_dir, + tips: 'Open path', + type: 'link', + event: function (row, index, ev) { + openPath(row.path); + }, + template: function (row, index) { + return ''; + }, + }, + bt.public.get_quota_config('site'), + { + fid: 'edate', + title: lan.site.endtime, + width: 115, + class: 'set_site_edate', + sort: true, + type: 'link', + template: function (row, index) { + var _endtime = row.edate || row.endtime; + if (_endtime === '0000-00-00') { + return lan.site.web_end_time; + } else { + if (new Date(_endtime).getTime() < new Date().getTime()) { + return '' + _endtime + ''; + } else { + return _endtime; + } + } + }, + event: function (row) {}, + }, + { + fid: 'ps', + title: lan.site.note, + type: 'input', + blur: function (row, index, ev) { + if (row.ps == ev.target.value) return false; + bt.pub.set_data_ps({ id: row.id, table: 'sites', ps: ev.target.value }, function (res) { + if (!res.status) layer.msg(res.msg, { status: 2 }); + }); + }, + keyup: function (row, index, ev) { + if (ev.keyCode === 13) { + $(this).blur(); + } + }, + }, + { + fid: 'php_version', + title: 'PHP', + tips: 'Selete php version', + width: 57, + type: 'link', + template: function (row, index) { + if (row.php_version.indexOf('static') > -1) return row.php_version; + return row.php_version; + }, + event: function (row, index) { + site.web_edit(row); + setTimeout(function () { + $('.site-menu p:eq(9)').click(); + }, 500); + }, + }, + { + fid: 'site_ssl', + title: 'SSL', + tips: 'Deployment certificate', + width: 130, + sort: true, + type: 'text', + template: function (row, index) { + var _ssl = row.ssl, + _info = '', + _arry = [ + ['issuer', 'Certificate'], + ['notAfter', 'Due date'], + ['notBefore', 'Application date'], + ['dns', 'Domain name'], + ]; + try { + if (typeof row.ssl.endtime != 'undefined') { + if (row.ssl.endtime < 0) { + return 'Exp in ' + Math.row.ssl.endtime + ' days'; + } + } + } catch (error) {} + for (var i = 0; i < _arry.length; i++) { + var item = _ssl[_arry[i][0]]; + _info += _arry[i][1] + ':' + item + (_arry.length - 1 != i ? '\n' : ''); + } + return row.ssl === -1 + ? 'Not Set' + : 'Exp in ' + row.ssl.endtime + ' days'; + }, + event: function (row, index, ev, key, that) { + // console.log(row, '111'); + site.web_edit(row); + setTimeout(function () { + $('.site-menu p:eq(8)').click(); + }, 500); + }, + }, + { + fid: 'attack', + title: 'Attack', + width: 80, + type: 'text', + template: function (row, index) { + return '' + row.attack + ''; + }, + event: function (row) { + site.web_edit(row); + setTimeout(function () { + $('.site-menu p:eq(' + ($('.site-menu p').length - 1) + ')').click(); + setTimeout(function () { + $('#tabLogs span:eq(2)').click(); + }, 500); + }, 500); + }, + }, + { + title: lan.site.operate, + type: 'group', + width: 140, + align: 'right', + group: [ + { + title: 'WAF', + event: function (row, index, ev, key, that) { + site.site_waf(row.name); + }, + }, + { + title: lan.site.set, + event: function (row, index, ev, key, that) { + site.web_edit(row, true); + }, + }, + { + title: 'Del', + event: function (row, index, ev, key, that) { + site.del_site(row.id, row.name, function () { + that.$refresh_table_list(true); + }); + }, + }, + ], + }, + ], + sortParam: function (data) { + return { order: data.name + ' ' + data.sort }; + }, + // 表格渲染完成后 + success: function (that) { + $('.event_edate_' + that.random).each(function () { + var $this = $(this); + laydate.render({ + elem: $this[0], //指定元素 + min: bt.get_date(1), + max: '2099-12-31', + vlue: bt.get_date(365), + type: 'date', + format: 'yyyy-MM-dd', + trigger: 'click', + btns: ['perpetual', 'confirm'], + theme: '#20a53a', + ready: function () { + $this.click(); + }, + done: function (date) { + var item = that.event_rows_model.rows; + bt.site.set_endtime(item.id, date, function (res) { + if (res.status) { + layer.msg(res.msg); + return false; + } + bt.msg(res); + }); + }, + }); + }); + if ($('#bt_site_table table thead th:eq(9) a').length == 0) { + var Attack_tips = + '
                                \ +
                              • Log analysis: Scan the logs(/www/wwwroot/.log) for requests with attack (types include:xss,sql,san,php)
                              • \ +
                              • Analyzed log data contains intercepted requests
                              • \ +
                              • By default, the last scan data is displayed (if not, please click log scan)
                              • \ +
                              • If the log file is too large, scanning may take a long time, please be patient
                              • \ +
                              • aaPanel WAF can effectively block such attacks
                              • \ +
                              '; + $('#bt_site_table table thead th:eq(9)>span').css({ width: '42px', display: 'initial' }); //设置扫描th大小 + //追加tips并设置样式 + $('#bt_site_table table thead th:eq(9)').append( + $('?') + .css({ 'border-color': '#666', color: '#666' }) + .hover( + function () { + $(this).css({ 'border-color': '#fb7d00', color: '#fff' }); + layer.tips(Attack_tips, $(this), { time: 0, tips: [1, '#fff'], area: ['500px', '180px'] }); + }, + function () { + $(this).css({ 'border-color': '#666', color: '#666' }); + layer.closeAll('tips'); + } + ) + ); + } + + $('.web_name').hover( + function (e) { + if ($('.web_name_input').is(':focus')) return; + // 获取元素基于浏览器的位置 + function getElementPosition(element) { + let top = element.offsetTop; //这是获取元素距父元素顶部的距离 + let left = element.offsetLeft; + var current = element.offsetParent; //这是获取父元素 + while (current !== null) { + //当它上面有元素时就继续执行 + top += current.offsetTop; //这是获取父元素距它的父元素顶部的距离累加起来 + left += current.offsetLeft; + current = current.offsetParent; //继续找父元素 + } + return { + top, + left, + }; + } + hoverInfo['site'] = $(this).data('type'); + hoverInfo['id'] = $(this).data('id'); + $('.web_name_hover') + .find('.web_name_title') + .html(lan.site.website_name + '' + $(this).data('type') + ''); + var _that = $(this); + $('.web_name_hover').css({ left: getElementPosition(e.target).left - 110 + _that.width() / 2 + 'px', top: getElementPosition(e.target).top - 80 + 'px' }); + $('.web_name_hover').removeClass('hide'); + }, + function (e) { + $('.web_name_hover').hover( + function () { + // 鼠标进入web_name_hover时显示 + $(this).removeClass('hide'); + }, + function () { + if ($('.web_name_input').is(':focus')) return; + // 鼠标离开web_name_hover时隐藏 + $(this).addClass('hide'); + } + ); + $('.site_name') + .parent() + .mouseleave(function () { + if ($('.web_name_input').is(':focus')) return; + $('.web_name_hover').addClass('hide'); + }); + } + ); + }, + // 渲染完成 + tootls: [ + { + // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: 'Add site', + active: true, + event: function (ev) { + site.add_site(function (res, param) { + var id = bt.get_cookie('site_type'); + if (param) { + // 创建站点 + if (id != -1 && id != param.type_id) { + $('#php_cate_select .bt_select_list .item.active').click(); + } else { + site_table.$refresh_table_list(true); + } + } else { + // 批量添加 + $('#php_cate_select .bt_select_list li[data-id="-1"]').click(); + } + }); + }, + }, + { + title: 'Default Page', + event: function (ev) { + site.set_default_page(); + }, + }, + { + title: 'Default Website', + event: function (ev) { + site.set_default_site(); + }, + }, + { + title: 'PHP CLI version', + event: function (ev) { + site.get_cli_version(); + }, + }, + ], + }, + { + // 搜索内容 + type: 'search', + positon: ['right', 'top'], + placeholder: 'Domain or Remarks', + searchParam: 'search', //搜索请求字段,默认为 search + value: '', // 当前内容,默认为空 + }, + { + // 批量操作 + type: 'batch', //batch_btn + positon: ['left', 'bottom'], + placeholder: 'Select batch operation', + buttonValue: 'Execute', + disabledSelectValue: 'Select the website to execute!', + selectList: [ + { + group: [ + { title: lan.site.enable_website, param: { status: 1 } }, + { + title: 'Disable website', + param: { status: 0 }, + }, + ], + url: '/site?action=set_site_status_multiple', + confirmVerify: false, //是否提示验证方式 + paramName: 'sites_id', //列表参数名,可以为空 + paramId: 'id', // 需要传入批量的id + theadName: 'Name', + refresh: true, + }, + { + title: lan.site.backup_website, + url: '/site?action=ToBackup', + paramId: 'id', + load: true, + theadName: 'Name', + refresh: true, + callback: function (that) { + // 手动执行,data参数包含所有选中的站点 + that.start_batch({}, function (list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += + '' + + item.name + + '' + + item.request.msg + + ''; + } + site_table.$batch_success_table({ title: 'Batch backup', th: 'Site name', html: html }); + site_table.$refresh_table_list(true); + }); + }, + }, + { + title: lan.site.set_expired, + url: '/site?action=set_site_etime_multiple', + paramName: 'sites_id', //列表参数名,可以为空 + paramId: 'id', // 需要传入批量的id + theadName: 'Name', + refresh: true, + confirm: { + title: 'Batch set expired date', + content: + '
                              Expired date
                              ', + success: function () { + laydate.render({ + elem: '#site_edate', + min: bt.format_data(new Date().getTime(), 'yyyy-MM-dd'), + max: '2099-12-31', + vlue: bt.get_date(365), + type: 'date', + format: 'yyyy-MM-dd', + trigger: 'click', + btns: ['perpetual', 'confirm'], + theme: '#20a53a', + }); + }, + yes: function (index, layers, request) { + var site_edate = $('#site_edate'), + site_edate_val = site_edate.val(); + if (site_edate_val != '') { + request({ edate: site_edate_val === 'Forever' ? '0000-00-00' : site_edate_val }); + } else { + layer.tips('Input expired date', '#site_edate', { tips: ['1', 'red'] }); + $('#site_edate').css('border-color', 'red'); + $('#site_edate').click(); + setTimeout(function () { + $('#site_edate').removeAttr('style'); + }, 3000); + return false; + } + }, + }, + }, + { + title: lan.site.set_php_version, + url: '/site?action=set_site_php_version_multiple', + paramName: 'sites_id', //列表参数名,可以为空 + paramId: 'id', // 需要传入批量的id + theadName: 'Name', + refresh: true, + confirm: { + title: 'Batch set php version', + area: '420px', + content: + '
                              PHP version
                              • Please select the version according to your program requirements.
                              • If not necessary, please try not to use PHP 5.2, which will reduce your server security.
                              • PHP 7 does not support mysql extension, mysqli and mysql_pdo will be installed by default.
                              ', + success: function () { + bt.site.get_all_phpversion(function (res) { + var html = ''; + $.each(res, function (index, item) { + html += ''; + }); + $('[name="versions"]').html(html); + }); + }, + yes: function (index, layers, request) { + request({ version: $('[name="versions"]').val() }); + }, + }, + }, + { + title: lan.site.set_category, + url: '/site?action=set_site_type', + paramName: 'site_ids', //列表参数名,可以为空 + paramId: 'id', // 需要传入批量的id + refresh: true, + beforeRequest: function (list) { + var arry = []; + $.each(list, function (index, item) { + arry.push(item.id); + }); + return JSON.stringify(arry); + }, + confirm: { + title: 'Batch set category', + content: + '
                              Site category
                              ', + success: function () { + bt.site.get_type(function (res) { + var html = ''; + $.each(res, function (index, item) { + html += ''; + }); + $('[name="site_types"]').html(html); + }); + }, + yes: function (index, layers, request) { + request({ id: $('[name="site_types"]').val() }); + }, + }, + tips: false, + refresh: true, + success: function (res, list, that) { + var html = ''; + $.each(list, function (index, item) { + html += '' + item.name + '
                              ' + res.msg + '
                              '; + }); + that.$batch_success_table({ title: 'Batch set category', th: 'Site name', html: html }); + that.$refresh_table_list(true); + }, + }, + { + title: lan.site.del_website, + url: '/site?action=DeleteSite', + // paramName:'sites_id', //列表参数名,可以为空 + // paramId:'id', //需要传入批量的id + // theadName:'Name', + refresh: true, + param: function (row) { + return { + id: row.id, + webname: row.name, + }; + }, + load: true, + callback: function (that) { + // bt.show_confirm("Delete site","Confirm delete the FTP、database、root path of the selected site with the same name", function(){ + // var param = {}; + // $('.bacth_options input[type=checkbox]').each(function(){ + // var checked = $(this).is(":checked"); + // if(checked) param[$(this).attr('name')] = checked?1:0; + // }) + // if(callback) callback(param); + // },"
                              "); + var ids = []; + for (var i = 0; i < that.check_list.length; i++) { + ids.push(that.check_list[i].id); + } + site.del_site(ids, function (param) { + that.start_batch(param, function (list) { + layer.closeAll(); + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += + '' + + item.name + + '
                              ' + + item.request.msg + + '
                              '; + } + site_table.$batch_success_table({ + title: 'Batch delete', + th: 'site name', + html: html, + }); + site_table.$refresh_table_list(true); + }); + }); + }, + }, + ], + }, + { + //分页显示 + type: 'page', + positon: ['right', 'bottom'], // 默认在右下角 + pageParam: 'p', //分页请求字段,默认为 : p + page: 1, //当前分页 默认:1 + numberParam: 'limit', //分页数量请求字段默认为 : limit + number: 20, //分页数量默认 : 20条 + numberList: [10, 20, 50, 100, 200], // 分页显示数量列表 + numberStatus: true, // 是否支持分页数量选择,默认禁用 + jump: true, //是否支持跳转分页,默认禁用 + }, + ], + }); + + this.init_site_type(); + }, + + /** + * @description 初始化php分类和需求反馈 + */ + init_site_type: function () { + $('#php_cate_select').remove(); + $('.feedback-btn').remove(); + $('.tootls_group.tootls_top .pull-left').append( + '\ +
                              \ +
                              \ + Classification: \ + \ +
                              \ +
                                \ +
                                \ + ' + ); + bt.site.get_type(function (res) { + site.reader_site_type(res); + }); + }, + reader_site_type: function (res, config) { + var html = '', + active = bt.get_cookie('site_type') || -1, + select = $('#php_cate_select'), + config = site_table; + + if (select.find('.bt_select_list li').length > 1) return false; + + res.unshift({ id: -1, name: 'Category manager' }); + + $.each(res, function (index, item) { + html += '
                              • ' + item.name + '
                              • '; + }); + + html += '
                              • Category set
                              • '; + + select.find('.bt_select_value').on('click', function (ev) { + var $this = this; + $(this).next().show(); + $(document).one('click', function () { + $($this).next().hide(); + }); + ev.stopPropagation(); + }); + + select + .find('.bt_select_list') + .unbind('click') + .on('click', 'li', function () { + var id = $(this).data('id'); + if (id === 'type_sets') { + site.set_class_type(); + } else { + bt.set_cookie('site_type', id); + config.config.page.page = 1; + config.$refresh_table_list(true); + $(this).addClass('active').siblings().removeClass('active'); + // select.find('.bt_select_value .bt_select_content').text('Classification: ' + $(this).text()); + select.find('.bt_select_value .bt_select_content').text($(this).text()); + } + }) + .empty() + .html(html); + + select = $(select[0]); + + if (!select.find('.bt_select_list li.active').length) { + select.find('.bt_select_list li:eq(0)').addClass('active'); + // select.find('.bt_select_value .bt_select_content').text('Classification: 默认分类'); + select.find('.bt_select_value .bt_select_content').text('Default category'); + } else { + // select.find('.bt_select_value .bt_select_content').text('Classification: ' + select.find('.bt_select_list li.active').text()); + select.find('.bt_select_value .bt_select_content').text(select.find('.bt_select_list li.active').text()); + } + }, + get_list: function (page, search, type) { + if (page == undefined) page = 1; + if (type == '-1' || type == undefined) { + type = bt.get_cookie('site_type'); + } + if (!search) search = $('#SearchValue').val(); + bt.site.get_list(page, search, type, function (rdata) { + $('.dataTables_paginate').html(rdata.page); + var data = rdata.data; + var _tab = bt.render({ + table: '#webBody', + columns: [ + { field: 'id', type: 'checkbox', width: 30 }, + { + field: 'name', + title: lan.site.site_name, + width: 150, + templet: function (item) { + return '' + item.name + ''; + }, + sort: function () { + site.get_list(); + }, + }, + { + field: 'status', + title: lan.site.status, + width: 98, + templet: function (item) { + var _status = ''; + _status += '' + lan.site.running_text + ' '; + } else { + _status += ' onclick="bt.site.start(' + item.id + ",'" + item.name + '\')"'; + _status += '' + lan.site.stopped + ' '; + } + return _status; + }, + sort: function () { + site.get_list(); + }, + }, + { + field: 'backup', + title: lan.site.backup, + width: 105, + templet: function (item) { + var backup = lan.site.backup_no; + if (item.backup_count > 0) backup = lan.site.backup_yes; + return '' + backup + ''; + }, + }, + { + field: 'path', + title: lan.site.root_dir, + templet: function (item) { + var _path = bt.format_path(item.path); + return '' + _path + ''; + }, + }, + { + field: 'edate', + title: lan.site.endtime, + width: 127, + templet: function (item) { + var _endtime = ''; + if (item.edate) _endtime = item.edate; + if (item.endtime) _endtime = item.endtime; + _endtime = _endtime == '0000-00-00' ? lan.site.web_end_time : _endtime; + return '' + _endtime + ''; + }, + sort: function () { + site.get_list(); + }, + }, + { + field: 'ps', + title: lan.site.note, + templet: function (item) { + return "" + item.ps + ''; + }, + }, + { + field: 'php_version', + width: 70, + title: 'PHP', + templet: function (item) { + return '' + item.php_version + ''; + }, + }, + { + field: 'ssl', + title: 'SSL', + templet: function (item) { + var _ssl = ''; + if (item.ssl == -1) { + _ssl = 'Not Set'; + } else { + var ssl_info = 'Certificate: ' + item.ssl.issuer + '
                                Due date: ' + item.ssl.notAfter + '
                                Application date: ' + item.ssl.notBefore + '
                                Domain name: ' + item.ssl.dns.join('/'); + if (item.ssl.endtime < 0) { + _ssl = 'Expired'; + } else if (item.ssl.endtime < 20) { + _ssl = 'Exp in ' + (item.ssl.endtime + ' days') + ''; + } else { + _ssl = 'Exp in ' + item.ssl.endtime + ' days'; + } + } + return _ssl; + }, + }, + { + field: 'opt', + width: 90, + title: lan.site.operate, + align: 'right', + templet: function (item) { + var opt = ''; + var _check = ' onclick="site.site_waf(\'' + item.name + '\')"'; + + //if (bt.os == 'Linux') opt += '' + lan.site.firewalld + ' | '; + opt += '' + lan.site.set + ' | '; + opt += '' + lan.site.del + ''; + return opt; + }, + }, + ], + data: data, + }); + var outTime = ''; + $('.ssl_tips').hover( + function () { + var that = this, + tips = $(that).attr('data-tips'); + if (!tips) return false; + outTime = setTimeout(function () { + layer.tips(tips, $(that), { + tips: [2, '#20a53a'], //还可配置颜色 + time: 0, + }); + }, 500); + }, + function () { + outTime != '' ? clearTimeout(outTime) : ''; + layer.closeAll('tips'); + } + ); + $('.ssl_tips').click(function () { + site.web_edit(this); + var timeVal = setInterval(function () { + var content = $('#webedit-con').html(); + if (content != '') { + $('.site-menu p:contains("SSL")').click(); + clearInterval(timeVal); + } + }, 100); + }); + $('.phpversion_tips').click(function () { + site.web_edit(this); + var timeVal = setInterval(function () { + var content = $('#webedit-con').html(); + if (content != '') { + $('.site-menu p:contains("PHP version")').click(); + clearInterval(timeVal); + } + }, 100); + }); + //浏览器窗口大小变化时调整内容宽度 + var ticket_with = $('#webBody').width(), + td_width = (ticket_with - 667 - $('#webBody th:contains("SSL")').width()) / 2; + $('#webBody .webPath').css('max-width', td_width); + $(window).resize(function () { + var ticket_with = $('#webBody').width(), + td_width = (ticket_with - 667 - $('#webBody th:contains("SSL")').width()) / 2; + $('#webBody .webPath').css('max-width', td_width); + }); + //设置到期时间 + $('a.setTimes').each(function () { + var _this = $(this); + var _tr = _this.parents('tr'); + var id = _this.attr('id'); + laydate.render({ + elem: '#' + id, //指定元素 + lang: 'en', + min: bt.get_date(1), + max: '2099-12-31', + vlue: bt.get_date(365), + type: 'date', + format: 'yyyy-MM-dd', + trigger: 'click', + btns: ['perpetual', 'confirm'], + theme: '#20a53a', + done: function (dates) { + var item = _tr.data('item'); + bt.site.set_endtime(item.id, dates, function () {}); + }, + }); + }); + //}) + }); + }, + site_waf: function (siteName) { + try { + site_waf_config(siteName); + } catch (err) { + site.no_firewall(); + } + }, + html_encode: function (html) { + var temp = document.createElement('div'); + //2.然后将要转换的字符串设置为这个元素的innerText(ie支持)或者textContent(火狐,google支持) + temp.textContent != undefined ? (temp.textContent = html) : (temp.innerText = html); + //3.最后返回这个元素的innerHTML,即得到经过HTML编码转换的字符串了 + var output = temp.innerHTML; + temp = null; + return output; + }, + get_types: function (callback) { + bt.site.get_type(function (rdata) { + var optionList = ''; + var t_val = bt.get_cookie('site_type'); + for (var i = 0; i < rdata.length; i++) { + optionList += ''; + } + if ($('.dataTables_paginate').next().hasClass('site_type')) $('.site_type').remove(); + $('.dataTables_paginate').after( + '
                                ' + optionList + '
                                ' + ); + $('.site_type button').click(function () { + var val = $(this).attr('value'); + bt.set_cookie('site_type', val); + site.get_list(0, '', val); + $('.site_type button').removeClass('btn-success').addClass('btn-default'); + $(this).addClass('btn-success'); + }); + if (callback) callback(rdata); + }); + }, + no_firewall: function (obj) { + var typename = bt.get_cookie('serverType'); + layer.confirm( + lan.site.firewalld_nonactivated_tips.replace('{1}', typename).replace('{2}', typename), + { + title: typename + lan.site.site_classification, + icon: 7, + closeBtn: 2, + cancel: function () { + if (obj) $(obj).prop('checked', false); + }, + }, + function () { + window.location.href = '/soft'; + }, + function () { + if (obj) $(obj).prop('checked', false); + } + ); + }, + site_detail: function (id, siteName, page) { + if (page == undefined) page = '1'; + var loadT = bt.load(lan.public.the_get); + bt.pub.get_data('table=backup&search=' + id + '&limit=5&type=0&tojs=site.site_detail&p=' + page, function (frdata) { + loadT.close(); + var ftpdown = ''; + var body = ''; + var port; + frdata.page = frdata.page.replace(/'/g, '"').replace(/site.site_detail\(/g, 'site.site_detail(' + id + ",'" + siteName + "',"); + if ($('#SiteBackupList').length <= 0) { + bt.open({ + type: 1, + skin: 'demo-class', + area: '700px', + title: lan.site.backup_title, + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "
                                • Before restoring data, all data in the root dir of the website will be moved to the panel recycle bin.
                                ", + }); + } + setTimeout(function () { + $('.sitebackup_page').html(frdata.page); + var _tab = bt.render({ + table: '#SiteBackupList', + columns: [ + { + field: 'name', + title: lan.site.filename, + templet: function (item) { + var _opt = '' + item.name + ''; + return _opt; + }, + }, + { + field: 'size', + title: lan.site.filesize, + templet: function (item) { + return bt.format_size(item.size); + }, + }, + { field: 'addtime', title: lan.site.backup_time }, + { + field: 'opt', + title: lan.site.operate, + align: 'right', + templet: function (item) { + var _opt = 'Restore | '; + _opt += '' + lan.site.download + ' | '; + _opt += '' + lan.site.del + ''; + return _opt; + }, + }, + ], + data: frdata.data, + }); + $('#btn_data_backup') + .unbind('click') + .click(function () { + bt.site.backup_data(id, function (rdata) { + if (rdata.status) site.site_detail(id, siteName); + site.get_list(); + }); + }); + $('#SiteBackupList .restore') + .unbind('click') + .click(function () { + var data = {}; + data.file_name = $(this).attr('backup-name'); + data.site_id = $(this).attr('site-id'); + // console.log(data); + layer.confirm( + 'Are you sure to restore backup file?', + { + icon: 0, + closeBtn: 2, + title: 'Restore backup file', + }, + function (index) { + $.post('/files?action=restore_website', data, function (rdata) { + layer.close(index); + site.backup_output_stop = true; + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + }); + site.backup_output_logs(); + } + ); + }); + }, 100); + }); + }, + /** + * @description 备份站点视图 + * @param {object} config 配置参数 + * @param {function} callback 回调函数 + */ + backup_site_view: function (config, thatC, callback) { + bt_tools.open({ + title: lan.site.backup_title + ' - [ ' + config.name + ' ]', + area: '720px', + btn: false, + skin: 'bt_backup_table', + content: '
                                ', + success: function ($layer) { + var backup_table = bt_tools.table({ + el: '#bt_backup_table', + url: '/data?action=getData', + param: { table: 'backup', search: config.id, type: '0' }, + default: '[' + config.name + '] Currently no backup', //数据为空时的默认提示 + column: [ + { type: 'checkbox', class: '', width: 20 }, + { fid: 'name', title: lan.site.filename, width: 250, fixed: true }, + { + fid: 'size', + title: lan.site.filesize, + width: 80, + type: 'text', + template: function (row, index) { + return bt.format_size(row.size); + }, + }, + { fid: 'addtime', width: 150, title: lan.site.backup_time }, + { + title: lan.site.operate, + type: 'group', + width: 165, + align: 'right', + group: [ + { + title: 'Restore', + event: function (row) { + var data = {}; + data.file_name = row.name; + data.site_id = config.id; + // console.log(data); + layer.confirm( + 'Are you sure to restore backup file?', + { + icon: 0, + closeBtn: 2, + title: 'Restore backup file', + }, + function (index) { + $.post('/files?action=restore_website', data, function (rdata) { + layer.close(index); + site.backup_output_stop = true; + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + }); + site.backup_output_logs(); + } + ); + }, + }, + { + title: lan.site.download, + template: function (row, index, ev, key, that) { + return '' + lan.site.download + ''; + }, + }, + { + title: lan.site.del, + event: function (row, index, ev, key, that) { + that.del_site_backup({ name: row.name, id: row.id }, function (rdata) { + bt_tools.msg(rdata); + if (rdata.status) { + thatC.$modify_row_data({ backup_count: thatC.event_rows_model.rows.backup_count - 1 }); + that.$refresh_table_list(); + } + }); + }, + }, + ], + }, + ], + methods: { + /** + * @description 删除站点备份 + * @param {object} config + * @param {function} callback + */ + del_site_backup: function (config, callback) { + bt.confirm( + { + title: lan.site.del_bak_file, + msg: 'The website backup is about to be deleted [' + config.name + '], do you want to continue?', + }, + function () { + bt_tools.send( + 'site/DelBackup', + { id: config.id }, + function (rdata) { + if (callback) callback(rdata); + }, + true + ); + } + ); + }, + }, + success: function () { + if (callback) callback(); + $('.bt_backup_table').css('top', ($(window).height() - $('.bt_backup_table').height()) / 2 + 'px'); + }, + tootls: [ + { + // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: 'Backup', + active: true, + event: function (ev, that) { + bt.site.backup_data(config.id, function (rdata) { + bt_tools.msg(rdata); + if (rdata.status) { + thatC.$modify_row_data({ backup_count: thatC.event_rows_model.rows.backup_count + 1 }); + that.$refresh_table_list(); + } + }); + }, + }, + ], + }, + { + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' Delete', + url: '/site?action=DelBackup', + paramId: 'id', + load: true, + callback: function (that) { + bt.confirm( + { + title: 'Delete site backups in bulk', + msg: 'Do you want to delete selected site backups in batches?', + icon: 0, + }, + function (index) { + layer.close(index); + that.start_batch({}, function (list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += + '' + + item.name + + '' + + item.request.msg + + ''; + } + backup_table.$batch_success_table({ + title: 'Delete site backups in bulk', + th: 'file name', + html: html, + }); + backup_table.$refresh_table_list(true); + thatC.$modify_row_data({ backup_count: thatC.event_rows_model.rows.backup_count - list.length }); + }); + } + ); + }, + }, //分页显示 + }, + { + type: 'page', + positon: ['right', 'bottom'], // 默认在右下角 + pageParam: 'p', //分页请求字段,默认为 : p + page: 1, //当前分页 默认:1 + numberParam: 'limit', + //分页数量请求字段默认为 : limit + number: 10, + //分页数量默认 : 20条 + }, + ], + }); + }, + }); + }, + backup_output_stop: false, + //实时显示过程 + backup_output_logs: function () { + var layerT = layer.open({ + type: 1, + area: '590px', + title: 'Recovering the backup...', + closeBtn: 0, + content: '
                                ', + }); + var show_output = setInterval(function () { + $.post('/files?action=get_progress', function (rdata) { + if (site.backup_output_stop) { + layer.close(layerT); + clearInterval(show_output); + } + $('.backup_logs').html(rdata.msg); + $('.backup_logs').scrollTop($('.backup_logs')[0].scrollHeight); + }); + }, 1000); + }, + /** + * @description 添加站点 + */ + add_site: function (callback) { + var typeId = bt.get_cookie('site_type'); + var add_web = bt_tools.form({ + data: {}, //用于存储初始值和编辑时的赋值内容 + class: '', + form: [ + { + label: lan.site.add_site.domain, + must: '*', + group: [ + { + type: 'textarea', //当前表单的类型 支持所有常规表单元素、和复合型的组合表单元素 + name: 'webname', //当前表单的name + style: { width: '440px', height: '100px', 'line-height': '22px' }, + tips: { + //使用hover的方式显示提示 + text: lan.site.domain_help, + style: { top: '15px', left: '15px' }, + }, + keyup: function (value, form, that, config, ev) { + //键盘事件 + var array = value.webname.split('\n'), + ress = array[0].split(':')[0], + oneVal = bt.strim(ress.replace(new RegExp(/([-.])/g), '_')), + defaultPath = $('#defaultPath').text(), + is_oneVal = ress.length > 0; + that.$set_find_value( + is_oneVal + ? { + ftp_username: 'ftp_' + oneVal, + ftp_password: bt.get_random(16), + datauser: is_oneVal ? 'sql_' + oneVal.substr(0, 16) : '', + datapassword: bt.get_random(16), + ps: oneVal, + path: bt.rtrim(defaultPath, '/') + '/' + ress, + } + : { + ftp_username: '', + ftp_password: '', + datauser: '', + datapassword: '', + ps: '', + path: bt.rtrim(defaultPath, '/'), + } + ); + if (bt.check_domain(ress)) { + form['redirect'].parents('.block').removeClass('hide'); + if (ress.indexOf('www.') !== 0) { + form['redirect'] + .next() + .find('span') + .text('www.' + ress); + } else if (ress.indexOf('www.') === 0) { + form['redirect'] + .next() + .find('span') + .text(ress.replace(/^www\./, '')); + } + } else { + form['tourl'].parents('.line').addClass('hide'); + form['redirect'].parents('.block').addClass('hide'); + } + }, + }, + { + type: 'checkbox', + block: true, + block_class: 'redirect_check', + hide: true, + name: 'redirect', + label_tips: 'Add [] domain name to the main domain name', + event: function (value, form, that, config, ev) { + var domain = form['redirect'].next().find('span').text(), + domain_textarea = form['webname'], + domainList = domain_textarea.val().split('\n'), + domain_one = domainList[0].split(':')[0]; + if (value['redirect'] == 'on') { + domain_textarea.val(domain_textarea.val() + '\r' + domain); + form['tourl'].parents('.line').removeClass('hide'); + var radio_list = form['tourl'].parents('.line').find('.redirect_tourl'); + $('.redirect_tourl:eq(1)') + .find('label') + .html('Redirect the main domain name [ ' + domain_one + '] to [' + domain + '] domain name'); + $('.redirect_tourl:eq(2)') + .find('label') + .html('Redirect the [' + domain + '] domain name to the main domain [' + domain_one + ']'); + } else { + for (var i = domainList.length - 1; i >= 0; i--) { + if (domainList[i] === domain) domainList.splice(i, 1); + } + domain_textarea.val(domainList.join('\n')); + form['tourl'].parents('.line').addClass('hide'); + } + }, + }, + ], + }, + { + label: 'Redirect', + hide: true, + group: [ + { + type: 'radio', + name: 'tourl', + block: true, + block_class: 'redirect_tourl', + label_tips: [ + 'No', + 'Redirect the main domain name [] to [] domain name', + 'Redirect the [] domain name to the main domain []', + ], + }, + ], + }, + { + label: lan.site.add_site.description, + group: { + type: 'text', + name: 'ps', + width: '400px', + placeholder: lan.note_ph, //默认标准备注提示 + }, + }, + { + label: lan.site.add_site.root, + must: '*', + group: { + type: 'text', + width: '400px', + name: 'path', + icon: { + type: 'glyphicon-folder-open', + event: function (ev) {}, + }, + value: '/www/wwwroot', + placeholder: lan.site.add_site.root_ph, + }, + }, + { + label: lan.site.add_site.ftp, + group: [ + { + type: 'select', + name: 'ftp', + width: '135px', + disabled: (function () { + if (bt.config['pure-ftpd']) return !bt.config['pure-ftpd'].setup; + return true; + })(), + list: [ + { title: lan.site.add_site.dont_create, value: false }, + { title: lan.site.add_site.create, value: true }, + ], + change: function (value, form, that, config, ev) { + if (value['ftp'] === 'true') { + form['ftp_username'].parents('.line').removeClass('hide'); + } else { + form['ftp_username'].parents('.line').addClass('hide'); + } + }, + }, + (function () { + if (bt.config['pure-ftpd']['setup']) return {}; + return { + type: 'link', + title: 'FTP is not installed, click Install', + name: 'installed_ftp', + event: function (ev) { + bt.soft.install('pureftpd'); + }, + }; + })(), + ], + }, + { + label: lan.site.add_site.ftp_set, + hide: true, + group: [ + { + type: 'text', + name: 'ftp_username', + placeholder: lan.site.add_site.ftp_ph, + width: '175px', + style: { 'margin-right': '15px' }, + }, + { + label: lan.site.add_site.password, + type: 'text', + placeholder: lan.site.add_site.ftp_password, + name: 'ftp_password', + width: '175px', + }, + ], + help: { + list: [lan.site.ftp_help], + }, + }, + { + label: lan.site.add_site.database, + group: [ + { + type: 'select', + name: 'sql', + width: '135px', + disabled: (function () { + if (bt.config['mysql']) return !bt.config['mysql'].setup; + return true; + })(), + list: [ + { title: lan.site.add_site.dont_create, value: false }, + { title: 'MySQL', value: 'MySQL' }, + { + title: 'SQLServer', + value: 'SQLServer', + disabled: true, + tips: lan.public_backup.unsupport_sqlserver, + }, + ], + change: function (value, form, that, config, ev) { + if (value['sql'] === 'MySQL') { + form['datauser'].parents('.line').removeClass('hide'); + form['codeing'].parents('.bt_select_updown').removeClass('hide'); + } else { + form['datauser'].parents('.line').addClass('hide'); + form['codeing'].parents('.bt_select_updown').addClass('hide'); + } + }, + }, + (function () { + if (bt.config.mysql.setup) return {}; + return { + type: 'link', + title: 'Database not installed, click Install', + name: 'installed_database', + event: function (ev) { + bt.soft.install('mysql'); + }, + }; + })(), + { + type: 'select', + name: 'codeing', + hide: true, + width: '135px', + list: [ + { title: 'utf8', value: 'utf8' }, + { title: 'utf8mb4', value: 'utf8mb4' }, + { title: 'gbk', value: 'gbk' }, + { title: 'big5', value: 'big5' }, + ], + }, + ], + }, + { + label: lan.site.add_site.database_set, + hide: true, + group: [ + { + type: 'text', + name: 'datauser', + placeholder: lan.site.add_site.database_ph, + width: '175px', + style: { 'margin-right': '15px' }, + }, + { + label: lan.site.add_site.password, + type: 'text', + placeholder: lan.site.add_site.database_password, + name: 'datapassword', + width: '175px', + }, + ], + help: { + class: '', + style: '', + list: [lan.site.database_help], + }, + }, + { + label: lan.site.add_site.php_version, + group: [ + { + type: 'select', + name: 'version', + width: '135px', + list: { + url: '/site?action=GetPHPVersion', + dataFilter: function (res) { + var arry = []; + for (var i = res.length - 1; i >= 0; i--) { + var item = res[i]; + arry.push({ title: item.name, value: item.version }); + } + return arry; + }, + }, + }, + ], + }, + { + label: lan.site.add_site.category, + group: [ + { + type: 'select', + name: 'type_id', + width: '135px', + list: { + url: '/site?action=get_site_types', + dataFilter: function (res) { + var arry = []; + $.each(res, function (index, item) { + arry.push({ title: item.name, value: item.id }); + }); + return arry; + }, + success: function (res, formObj) { + setTimeout(function () { + var index = -1; + for (var i = 0; i < res.length; i++) { + if (res[i].id == typeId) { + index = i; + break; + } + } + if (index != -1) formObj.element.find('.bt_select_updown[data-name="type_id"]').find('.bt_select_list li').eq(index).click(); + }, 100); + }, + }, + }, + ], + }, + { + label: 'SSL', + class: 'ssl_checkbox', + help: { + style: 'color: red;line-height: 17px;margin-top: 8px;', + list: ['If you need to apply for SSL, please make sure that the domain name has added A record resolution for the domain name'], + }, + group: [ + { + type: 'checkbox', + name: 'set_ssl', + title: 'Apply for SSL', + class: 'site_ssl_check', + style: { 'margin-right': '10px', 'margin-left': '0' }, + }, + { + type: 'checkbox', + name: 'force_ssl', + class: 'site_ssl_check', + title: 'HTTP redirect to HTTPS', + event: function (value, form, that, config, ev) { + var force_ssl = $(this).is(':checked'); + if (force_ssl) { + $('.site_ssl_check:eq(0)').find('i').addClass('active'); + $('input[name=set_ssl]').prop('checked', force_ssl); + } + }, + }, + ], + }, + ], + }); + var bath_web = bt_tools.form({ + class: 'plr10', + form: [ + { + line_style: { position: 'relative' }, + group: { + type: 'textarea', //当前表单的类型 支持所有常规表单元素、和复合型的组合表单元素 + name: 'bath_code', //当前表单的name + style: { width: '560px', height: '180px', 'line-height': '22px', 'font-size': '13px' }, + value: lan.site.add_site.bath_code_ph, + }, + }, + { + group: { + type: 'help', + style: { 'margin-top': '0' }, + class: 'none-list-style', + list: [ + lan.site.add_site.bath_tips1, + lan.site.add_site.bath_tips2, + lan.site.add_site.bath_tips3, + lan.site.add_site.bath_tips4, + lan.site.add_site.bath_tips5, + lan.site.add_site.bath_tips6, + lan.site.add_site.bath_tips7, + lan.site.add_site.bath_tips8, + ], + }, + }, + ], + }); + var deploy_wp = bt_tools.form({ + form: [ + { + label: 'Domain', + group: { + type: 'text', + name: 'domain', + width: '400px', + placeholder: 'Your website domain name', + }, + }, + { + label: 'Website Title', + group: { + type: 'text', + name: 'weblog_title', + width: '400px', + placeholder: 'Website title for wordpress', + }, + }, + { + label: 'Language', + group: [ + { + type: 'select', + name: 'language', + width: '230px', + list: { + url: '/site?action=get_language', + dataFilter: function (rlist) { + var lan = []; + $.each(rlist.msg, function (index, item) { + lan.push({ title: item, value: index }); + }); + return lan; + }, + }, + }, + ], + }, + { + label: 'PHP version', + group: [ + { + type: 'select', + name: 'php_version', + width: '230px', + list: add_web['config']['form'][8]['group'][0]['list'], + }, + ], + }, + { + label: 'User name', + group: { + type: 'text', + name: 'user_name', + width: '400px', + placeholder: 'WordPress backend user', + }, + }, + { + label: 'Password', + group: [ + { + type: 'text', + name: 'admin_password', + placeholder: 'WordPress backend password', + width: '230px', + style: { 'margin-right': '15px' }, + }, + { + type: 'checkbox', + name: 'pw_weak', + title: 'Allow weak passwords', + }, + ], + }, + { + label: 'Email', + group: { + type: 'text', + name: 'admin_email', + width: '400px', + placeholder: 'Your email address', + }, + }, + { + label: 'Prefix', + group: { + type: 'text', + name: 'prefix', + width: '400px', + value: 'wp_', + placeholder: 'Wordpress table name prefix', + }, + }, + { + label: 'Enable cache', + style: { 'min-height': '0', 'line-height': '15px', 'margin-bottom': ' 0' }, + group: [ + { + type: 'checkbox', + name: 'enable_cache', + title: 'Enable caching, currently only supports nginx', + }, + ], + }, + ], + }); + + var web_tab = bt_tools.tab({ + class: 'pd20', + type: 0, + theme: { nav: 'mlr20' }, + active: 1, //激活TAB下标 + list: [ + { + title: lan.site.add_site.create_site, + name: 'createSite', + content: add_web.$reader_content(), + success: function () { + add_web.$event_bind(); + }, + }, + { + title: lan.site.add_site.batch_creat, + name: 'batchCreation', + content: bath_web.$reader_content(), + success: function () { + bath_web.$event_bind(); + }, + }, + { + title: 'Wordpress deploy', + name: 'wordpressDeploy', + content: '', + success: function (el) { + el.html(deploy_wp.$reader_content()); + deploy_wp.$event_bind(); + $(el).find('form .line:last-child .tname').css({ height: '22px', 'line-height': '22px' }); + }, + }, + ], + }); + bt_tools.open({ + title: lan.site.add_site.add_site_title, + skin: 'custom_layer', + btn: [lan.public.submit, lan.site.no], + content: web_tab.$reader_content(), + success: function ($layer) { + web_tab.$init(); + + $layer.find('.tab-con').scroll(function () { + $layer.find('.bt_select_list').removeClass('show'); + }); + // $layer.find('.layui-layer-content, .tab-con').css('overflow', $(window).height() > $layer.height() ? 'visible' : 'auto'); + }, + yes: function (indexs) { + var tabContent = add_web, + tabActive = web_tab.active; + switch (tabActive) { + case 1: + tabContent = bath_web; + break; + case 2: + tabContent = deploy_wp; + break; + } + var formValue = tabContent.$get_form_value(); + if (tabActive != 2 && formValue.webname === '') { + bt.msg({ status: false, msg: '网站域名不能为空!' }); + return false; + } + + if (tabActive == 0) { + // 创建站点 + var loading = bt.load(); + add_web.$get_form_element(true); + if (formValue.webname === '') { + add_web.form_element.webname.focus(); + bt_tools.msg(lan.public.domain_format_not_right, 2); + return; + } + var webname = bt.replace_all(formValue.webname, 'http[s]?:\\/\\/', ''), + web_list = webname.split('\n'), + param = { webname: { domain: '', domainlist: [], count: 0 }, type: 'PHP', port: 80 }, + arry = ['ps', ['path', lan.site.site_menu_2], 'type_id', 'version', 'ftp', 'sql', 'ftp_username', 'ftp_password', 'datauser', 'datapassword', 'codeing']; + for (var i = 0; i < web_list.length; i++) { + var temps = web_list[i].replace(/\r\n/, '').split(':'); + if (i === 0) { + param['webname']['domain'] = web_list[i]; + if (typeof temps[1] != 'undefined') param['port'] = temps[1]; + } else { + param['webname']['domainlist'].push(web_list[i]); + } + } + param['webname']['count'] = param['webname']['domainlist'].length; + param['webname'] = JSON.stringify(param['webname']); + $.each(arry, function (index, item) { + if (formValue[item] == '' && Array.isArray(item)) { + bt_tools.msg(item[1] + lan.site.add_site.empty_ps, 2); + return false; + } + Array.isArray(item) ? (item = item[0]) : ''; + if (formValue['ftp'] === 'false' && (item === 'ftp_username' || item === 'ftp_password')) return true; + if (formValue['sql'] === 'false' && (item === 'datauser' || item === 'datapassword')) return true; + param[item] = formValue[item]; + }); + param['set_ssl'] = $('input[name=set_ssl]').prop('checked') ? 1 : 0; + param['force_ssl'] = $('input[name=force_ssl]').prop('checked') ? 1 : 0; + var is_redirect = $('.redirect_check').hasClass('hide'); + if (!is_redirect) { + var redirect_check = $('.redirect_check input[name=redirect]').is(':checked'); + if (redirect_check) { + var domains = $('.redirect_tourl input[name=tourl]:checked').next().find('span'); + if (domains.length != 0) { + param.redirect = $(domains[0]).text(); + param.tourl = $(domains[1]).text(); + } + } + } + bt.send('AddSite', 'site/AddSite', param, function (rdata) { + loading.close(); + if (rdata.siteStatus) { + layer.close(indexs); + if (callback) callback(rdata, param); + var html = '', + ftpData = '', + sqlData = ''; + if (rdata.ftpStatus) { + var list = []; + list.push({ title: lan.site.user, val: rdata.ftpUser }); + list.push({ title: lan.site.password, val: rdata.ftpPass }); + var item = {}; + item.title = lan.site.ftp; + item.list = list; + ftpData = bt.render_ps(item); + } + if (rdata.databaseStatus) { + var list = []; + list.push({ title: lan.site.database_name, val: rdata.databaseUser }); + list.push({ title: lan.site.user, val: rdata.databaseUser }); + list.push({ title: lan.site.password, val: rdata.databasePass }); + var item = {}; + item.title = lan.site.database_txt; + item.list = list; + sqlData = bt.render_ps(item); + } + if (ftpData == '' && sqlData == '') { + bt.msg({ msg: lan.site.success_txt, icon: 1 }); + } else { + bt.open({ + type: 1, + area: '600px', + title: lan.site.success_txt, + closeBtn: 2, + shadeClose: false, + content: "
                                " + ftpData + sqlData + '
                                ', + }); + + if ($('.success-msg').height() < 150) { + $('.success-msg').find('img').css({ width: '150px', 'margin-top': '30px' }); + } + } + } else { + bt.msg(rdata); + } + }); + } else if (tabActive == 1) { + //批量创建 + var loading = bt.load(); + if (formValue.bath_code === '') { + bt_tools.msg(lan.site.add_site.batch_site_ps, 2); + return false; + } else { + var arry = formValue.bath_code.split('\n'), + config = '', + _list = []; + for (var i = 0; i < arry.length; i++) { + var item = arry[i], + params = item.split('|'), + _arry = []; + if (item === '') continue; + for (var j = 0; j < params.length; j++) { + var line = i + 1, + items = bt.strim(params[j]); + _arry.push(items); + switch (j) { + case 0: //参数一:域名 + var domainList = items.split(','); + for (var z = 0; z < domainList.length; z++) { + var domain_info = domainList[z], + _domain = domain_info.split(':'); + if (!bt.check_domain(_domain[0])) { + bt_tools.msg(lan.site.add_site.error_line + line + lan.site.add_site.domain_error + '【' + domain_info + '】', 2); + return false; + } + if (typeof _domain[1] !== 'undefined') { + if (!bt.check_port(_domain[1])) { + bt_tools.msg(lan.site.add_site.error_line + line + lan.site.add_site.port_error + '【' + _domain[1] + '】', 2); + return false; + } + } + } + break; + case 1: //参数二:站点目录 + if (items !== '1') { + if (items.indexOf('/') < -1) { + bt_tools.msg(lan.site.add_site.error_line + line + lan.site.add_site.port_error + '【' + items + '】', 2); + return false; + } + } + break; + } + } + _list.push(_arry.join('|').replace(/\r|\n/, '')); + } + } + bt.send( + 'create_type', + 'site/create_website_multiple', + { + create_type: 'txt', + websites_content: JSON.stringify(_list), + }, + function (rdata) { + loading.close(); + if (rdata.status) { + var _html = ''; + layer.close(indexs); + if (callback) callback(rdata); + $.each(rdata.error, function (key, item) { + _html += '' + key + '----' + item + ''; + }); + $.each(rdata.success, function (key, item) { + _html += + '' + + key + + '' + + (item.ftp_status ? '' + lan.site.add_site.success + '' : '' + lan.site.add_site.not_created + '') + + '' + + (item.db_status ? '' + lan.site.add_site.success + '' : '' + lan.site.add_site.not_created + '') + + '' + + lan.site.add_site.created + + ''; + }); + bt.open({ + type: 1, + title: lan.site.add_site.batch_add_site, + area: ['500px', '450px'], + shadeClose: false, + closeBtn: 2, + content: + '
                                ' + + _html + + '
                                ' + + lan.site.add_site.site_name + + 'FTP' + + lan.site.add_site.database + + '' + + lan.site.add_site.opt_result + + '
                                ', + success: function () { + $('.fiexd_thead').scroll(function () { + var scrollTop = this.scrollTop; + this.querySelector('thead').style.transform = 'translateY(' + scrollTop + 'px)'; + }); + }, + }); + } else { + bt.msg(rdata); + } + } + ); + } else { + //wp一键部署 + var param = { webname: { domain: '', domainlist: [], count: 0 }, type: 'PHP', port: 80, type_id: 0, ftp: false, sql: 'MySQL', codeing: 'utf8', set_ssl: 0, force_ssl: 0, project_type: 'WP' }; + if (formValue.domain == '') return layer.msg('Wordpress domain name cannot be empty', { icon: 2 }); + if (formValue.weblog_title == '') return layer.msg('Wordpress site title cannot be empty', { icon: 2 }); + if (formValue.user_name == '') return layer.msg('Wordpress backend user cannot be empty', { icon: 2 }); + if (formValue.admin_password == '') return layer.msg('Wordpress backend password cannot be empty', { icon: 2 }); + if (formValue.admin_email == '') return layer.msg('Email address cannot be empty', { icon: 2 }); + if (formValue.prefix == '') return layer.msg('Wordpress table name prefix cannot be empty', { icon: 2 }); + + var _domain = bt.strim(formValue.domain.replace(new RegExp(/([-.])/g), '_')); + param['webname']['domain'] = formValue.domain; + param['webname'] = JSON.stringify(param['webname']); + + param['path'] = '/www/wwwroot/' + formValue.domain; + param['ps'] = _domain; + param['version'] = formValue.php_version; + param['datauser'] = 'sql_' + _domain; + param['datapassword'] = bt.get_random(16); + + // 密码强度判断 + param['password'] = formValue['admin_password']; + param['pw_weak'] = formValue['pw_weak'] ? 'on' : 'off'; + param['email'] = formValue['admin_email']; + + var loading = bt.load('Creating website, please wait...'); + // 1.通过主域名生成域名文件地址等信息 + bt.send('AddSite', 'site/AddSite', param, function (rdata) { + loading.close(); + if (typeof rdata.status === 'boolean' && !rdata.status) return layer.msg(rdata.msg, { icon: 2, time: 0, shade: 0.3, shadeClose: true }); + // 2.通过回调的siteId,d_id部署wp + if (rdata.databaseStatus) { + formValue['d_id'] = rdata.d_id; + formValue['s_id'] = rdata.siteId; + formValue['enable_cache'] = formValue['enable_cache'] ? 1 : 0; + formValue['pw_weak'] = formValue['pw_weak'] ? 'on' : 'off'; + + var loadingWp = bt.load('Deploying wordpress, please wait...'); + bt.send('deploy_wp', 'site/deploy_wp', formValue, function (deploy) { + loadingWp.close(); + if (deploy.status) { + layer.close(indexs); + if (callback) callback(deploy); + } + layer.msg(deploy.msg, { icon: deploy.status ? 1 : 2 }); + }); + } + }); + } + }, + }); + }, + set_default_page: function () { + bt.open({ + type: 1, + area: '460px', + title: lan.site.change_defalut_page, + closeBtn: 2, + shift: 0, + content: + '
                                ', + }); + setTimeout(function () { + $('.change-default button').click(function () { + bt.site.get_default_path($(this).index(), function (path) { + bt.pub.on_edit_file(0, path); + }); + }); + }, 100); + }, + set_default_site: function () { + bt.site.get_default_site(function (rdata) { + var arrs = []; + arrs.push({ title: lan.site.default_site_not_set, value: '0' }); + for (var i = 0; i < rdata.sites.length; i++) + arrs.push({ + title: rdata.sites[i].name, + value: rdata.sites[i].name, + }); + var form = { + title: lan.site.default_site_yes, + area: '530px', + list: [ + { + title: lan.site.default_site, + name: 'defaultSite', + width: '300px', + value: rdata.defaultSite, + type: 'select', + items: arrs, + }, + ], + btns: [ + bt.form.btn.close(), + bt.form.btn.submit(lan.site.submit, function (rdata, load) { + bt.site.set_default_site(rdata.defaultSite, function (rdata) { + load.close(); + bt.msg(rdata); + }); + }), + ], + }; + bt.render_form(form); + $('.line').after($(bt.render_help([lan.site.default_site_help_1, lan.site.default_site_help_2])).addClass('plr20')); + }); + }, + //PHP-CLI + get_cli_version: function () { + $.post('/config?action=get_cli_php_version', {}, function (rdata) { + if (rdata.status === false) { + layer.msg(rdata.msg, { icon: 2 }); + return; + } + var _options = ''; + for (var i = rdata.versions.length - 1; i >= 0; i--) { + var ed = ''; + if (rdata.select.version == rdata.versions[i].version) ed = 'selected'; + _options += ''; + } + var body = + '
                                \ +
                                \ + ' + + lan.site.php_cli_ver + + '\ +
                                \ + \ +
                                \ +
                                \ +
                                  \ +
                                • ' + + lan.site.php_cli_tips1 + + '
                                • \ +
                                • ' + + lan.site.php_cli_tips2 + + '
                                • \ +
                                \ +
                                '; + + layer.open({ + type: 1, + title: lan.site.set_php_cli_cmd, + area: '560px', + closeBtn: 2, + shadeClose: false, + content: body, + }); + }); + }, + set_cli_version: function () { + var php_version = $("select[name='php_version']").val(); + var loading = bt.load(); + $.post('/config?action=set_cli_php_version', { php_version: php_version }, function (rdata) { + loading.close(); + if (rdata.status) { + layer.closeAll(); + } + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + }); + }, + del_site: function (wid, wname, callback) { + var num1 = bt.get_random_num(1, 9), + num2 = bt.get_random_num(1, 9), + title = ''; + title = typeof wname === 'function' ? 'Deleting sites in batches' : lan.site.site_del_title + ' [ ' + wname + ' ]'; + layer.open({ + type: 1, + title: title, + icon: 0, + skin: 'delete_site_layer', + area: '480px', + closeBtn: 2, + shadeClose: true, + content: + '\ +
                                \ + \ +
                                ' + + lan.site.site_del_info + + '
                                \ +
                                \ + \ + \ + \ +
                                \ +
                                \ + ' + + lan.bt.cal_msg + + '' + + num1 + + ' + ' + + num2 + + '=\ + \ +
                                \ +
                                \ + ', + btn: [lan.public.ok, lan.public.cancel], + success: function (layers, indexs) { + $(layers) + .find('.check_type_group label') + .hover( + function () { + var name = $(this).find('input').attr('name'); + if (name === 'data' && !recycle_bin_db_open) { + layer.tips('Risky operation: the current database recycle bin is not open, delete the database will disappear forever!', this, { + tips: [1, 'red'], + time: 0, + }); + } else if (name === 'path' && !recycle_bin_open) { + layer.tips('Risky operation: The current file recycle bin is not open, delete the site directory will disappear forever!', this, { + tips: [1, 'red'], + time: 0, + }); + } + }, + function () { + layer.closeAll('tips'); + } + ); + }, + yes: function (indexs) { + var vcodeResult = $('#vcodeResult'), + data = { id: wid, webname: wname }; + $('#site_delete_form input[type=checkbox]').each(function (index, item) { + if ($(item).is(':checked')) data[$(item).attr('name')] = 1; + }); + if (vcodeResult.val() === '') { + layer.tips('The result cannot be null', vcodeResult, { tips: [1, 'red'], time: 3000 }); + vcodeResult.focus(); + return false; + } else if (parseInt(vcodeResult.val()) !== num1 + num2) { + layer.tips('The calculation is incorrect', vcodeResult, { tips: [1, 'red'], time: 3000 }); + vcodeResult.focus(); + return false; + } + var is_database = data.hasOwnProperty('database'), + is_path = data.hasOwnProperty('path'), + is_ftp = data.hasOwnProperty('ftp'); + if (!is_database && !is_path && (!is_ftp || is_ftp)) { + if (typeof wname === 'function') { + wname(data); + return false; + } + bt.site.del_site(data, function (rdata) { + layer.close(indexs); + if (callback) callback(rdata); + bt.msg(rdata); + }); + return false; + } + if (typeof wname === 'function') { + delete data.id; + delete data.webname; + } + layer.close(indexs); + var ids = JSON.stringify(wid instanceof Array ? wid : [wid]), + countDown = typeof wname === 'string' ? 4 : 9; + title = typeof wname === 'function' ? 'Verify the information twice and delete sites in batches' : 'Verify information twice, delete site [ ' + wname + ' ]'; + var loadT = bt.load('Checking site data information, please wait...'); + bt.send('check_del_data', 'site/check_del_data', { ids: ids }, function (res) { + loadT.close(); + layer.open({ + type: 1, + title: title, + closeBtn: 2, + skin: 'verify_site_layer_info active', + area: '740px', + content: + '
                                ' + + '' + + '
                                Please calm down for a few seconds and confirm the following data to be deleted.
                                ' + + '
                                ' + + '
                                ' + + '
                                ' + + '
                                ' + + '
                                ' + + '
                                ' + + '
                                Risks: The database recycle bin function is not enabled at present. After the database is deleted, the database will disappear forever!
                                ' + + '
                                Risk: The file recycle bin function is disabled at present. After a site directory is deleted, the site directory will disappear forever!
                                ' + + '
                                Please read the above information to be deleted carefully to prevent site data from being deleted by mistake. Confirm that there are still ' + + countDown + + ' seconds left to delete.
                                ' + + '
                                ', + btn: ['Confirm deletion (continue operation after ' + countDown + 'seconds)', 'Cancel'], + success: function (layers) { + var html = '', + rdata = res.data; + for (var i = 0; i < rdata.length; i++) { + var item = rdata[i], + newTime = parseInt(new Date().getTime() / 1000), + t_icon = ''; + + (site_html = (function (item) { + if (!is_path) return ''; + var is_time_rule = newTime - item.st_time > 86400 * 30 && item.total > 1024 * 10, + is_path_rule = res.file_size <= item.total, + dir_time = bt.format_data(item.st_time, 'yyyy-MM-dd'), + dir_size = bt.format_size(item.total); + + var f_html = + ' ' + (item.limit ? 'More than 50 MB' : dir_size) + ' ' + (is_path_rule ? t_icon : ''); + var f_title = + (is_path_rule ? 'Note: This directory may contain important data. Exercise caution when performing this operation.\n' : '') + + 'directory:' + + item.path + + '(' + + (item.limit ? 'greater than ' : '') + + dir_size + + ')'; + + return ( + '
                                ' + + 'Site: ' + + item.name + + '' + + 'Path: ' + + item.path + + ' (' + + f_html + + ')' + + 'Create: ' + + dir_time + + '' + + '
                                ' + ); + })(item)), + (database_html = (function (item) { + if (!is_database || !item.database) return ''; + var is_time_rule = newTime - item.st_time > 86400 * 30 && item.total > 1024 * 10, + is_database_rule = res.db_size <= item.database.total, + database_time = bt.format_data(item.database.st_time, 'yyyy-MM-dd'), + database_size = bt.format_size(item.database.total); + + var f_size = ' ' + database_size + ' ' + (is_database_rule ? t_icon : ''); + var t_size = 'Note: This database is large and may contain important data. Exercise caution when performing this operation.\ndatabase:' + database_size; + + return ( + '
                                ' + + 'DB: ' + + item.database.name + + '' + + 'Size: ' + + f_size + + '' + + 'Create: ' + + database_time + + '' + + '
                                ' + ); + })(item)); + if (site_html + database_html !== '') html += '
                                ' + site_html + database_html + '
                                '; + } + if (html === '') html = '
                                No data
                                '; + $('.check_layer_content').html(html); + var interVal = setInterval(function () { + countDown--; + $(layers) + .find('.layui-layer-btn0') + .text('Confirm deletion (continue operation after ' + countDown + ' seconds)'); + $(layers).find('.check_layer_message span').text(countDown); + }, 1000); + setTimeout(function () { + $(layers).find('.layui-layer-btn0').text('Confirm the deletion'); + $(layers).find('.check_layer_message').html('Note: please read the above information carefully to prevent site data from being deleted by mistake'); + $(layers).removeClass('active'); + clearInterval(interVal); + }, countDown * 1000); + }, + yes: function (indes, layers) { + if ($(layers).hasClass('active')) { + layer.tips('Please confirm the information and try again later. ' + countDown + ' seconds left', $(layers).find('.layui-layer-btn0'), { + tips: [1, 'red'], + time: 3000, + }); + return; + } + if (typeof wname === 'function') { + wname(data); + } else { + bt.site.del_site(data, function (rdata) { + layer.closeAll(); + if (rdata.status) site.get_list(); + if (callback) callback(rdata); + bt.msg(rdata); + }); + } + }, + }); + }); + }, + }); + }, + batch_site: function (type, obj, result) { + if (obj == undefined) { + obj = {}; + var arr = []; + result = { count: 0, error_list: [] }; + $('input[type="checkbox"].check:checked').each(function () { + var _val = $(this).val(); + if (!isNaN(_val)) arr.push($(this).parents('tr').data('item')); + }); + if (type == 'site_type') { + bt.site.get_type(function (tdata) { + var types = []; + for (var i = 0; i < tdata.length; i++) types.push({ title: tdata[i].name, value: tdata[i].id }); + var form = { + title: lan.site.set_site_classification, + area: '530px', + list: [ + { + title: lan.site.default_site, + name: 'type_id', + width: '300px', + type: 'select', + items: types, + }, + ], + btns: [ + bt.form.btn.close(), + bt.form.btn.submit(lan.site.submit, function (rdata, load) { + var ids = []; + for (var x = 0; x < arr.length; x++) ids.push(arr[x].id); + bt.site.set_site_type( + { + id: rdata.type_id, + site_array: JSON.stringify(ids), + }, + function (rrdata) { + if (rrdata.status) { + load.close(); + site.get_list(); + } + bt.msg(rrdata); + } + ); + }), + ], + }; + bt.render_form(form); + }); + return; + } + var thtml = "
                                '; + bt.show_confirm( + lan.site.all_del_site, + "" + lan.get('del_all_site', [arr.length]) + '', + function () { + if ($('#delpath').is(':checked')) obj.path = '1'; + obj.data = arr; + bt.closeAll(); + site.batch_site(type, obj, result); + }, + thtml + ); + + return; + } + var item = obj.data[0]; + switch (type) { + case 'del': + if (obj.data.length < 1) { + site.get_list(); + bt.msg({ msg: lan.get('del_all_site_ok', [result.count]), icon: 1, time: 5000 }); + return; + } + var data = { id: item.id, webname: item.name, path: obj.path }; + bt.site.del_site(data, function (rdata) { + if (rdata.status) { + result.count += 1; + } else { + result.error_list.push({ name: item.item, err_msg: rdata.msg }); + } + obj.data.splice(0, 1); + site.batch_site(type, obj, result); + }); + break; + } + }, + set_class_type: function () { + var _form_data = bt.render_form_line({ + title: '', + items: [ + { placeholder: lan.site.input_classification_name, name: 'type_name', width: '50%', type: 'text' }, + { + name: 'btn_submit', + text: lan.site.add, + type: 'button', + callback: function (sdata) { + bt.site.add_type(sdata.type_name, function (ldata) { + if (ldata.status) { + $('[name="type_name"]').val(''); + site.get_class_type(); + site.init_site_type(); + } + bt.msg(ldata); + }); + }, + }, + ], + }); + bt.open({ + type: 1, + area: '350px', + title: lan.site.mam_site_classificacion, + closeBtn: 2, + shift: 5, + shadeClose: true, + content: + "
                                " + + _form_data.html + + "
                                ", + success: function () { + bt.render_clicks(_form_data.clicks); + site.get_class_type(function (res) { + $('#type_table').on('click', '.del_type', function () { + var _this = $(this); + var item = _this.parents('tr').data('item'); + if (item.id == 0) { + bt.msg({ icon: 2, msg: lan.site.default_classification_cant_operation }); + return; + } + bt.confirm( + { + msg: lan.site.sure_del_classification, + title: lan.site.del_classification + '【' + item.name + '】', + }, + function () { + bt.site.del_type(item.id, function (ret) { + if (ret.status) { + site.get_class_type(); + site.init_site_type(); + bt.set_cookie('site_type', '-1'); + } + bt.msg(ret); + }); + } + ); + }); + $('#type_table').on('click', '.edit_type', function () { + var item = $(this).parents('tr').data('item'); + if (item.id == 0) { + bt.msg({ icon: 2, msg: lan.site.default_classification_cant_operation }); + return; + } + bt.render_form({ + title: lan.site.edit_classification_mam + '【' + item.name + '】', + area: '350px', + list: [ + { + title: lan.site.classification_name, + width: '150px', + name: 'name', + value: item.name, + }, + ], + btns: [ + { title: lan.site.turn_off, name: 'close' }, + { + title: lan.site.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + bt.site.edit_type({ id: item.id, name: rdata.name }, function (edata) { + if (edata.status) { + load.close(); + site.get_class_type(); + site.init_site_type(); + } + bt.msg(edata); + }); + }, + }, + ], + }); + }); + }); + }, + }); + }, + get_class_type: function (callback) { + site.get_types(function (rdata) { + bt.render({ + table: '#type_table', + columns: [ + { field: 'name', title: lan.site.name }, + { + field: 'opt', + width: '80px', + title: lan.site.operate, + templet: function (item) { + return '' + lan.site.edit + ' | ' + lan.site.del + ''; + }, + }, + ], + data: rdata, + }); + $('.layui-layer-page').css({ + 'margin-top': '-' + $('.layui-layer-page').height() / 2 + 'px', + top: '50%', + }); + if (callback) callback(rdata); + }); + }, + ssl: { + my_ssl_msg: null, + + //续签订单内 + renew_ssl: function (siteName, auth_type, index) { + acme.siteName = siteName; + if (index.length === 32 && index.indexOf('/') === -1) { + acme.renew(index, function (rdata) { + site.ssl.ssl_result(rdata, auth_type, siteName); + }); + } else { + acme.get_cert_init(index, siteName, function (cert_init) { + acme.domains = cert_init.dns; + var options = ''; + for (var i = 0; i < cert_init.dnsapi.length; i++) { + options += ''; + } + acme.select_loadT = layer.open({ + title: "Renew Let's Encrypt Certificate", + type: 1, + closeBtn: 2, + shade: 0.3, + area: '500px', + offset: '30%', + content: + '
                                \ +
                                \ +
                                Please select a verification method:
                                \ +
                                \ + \ + \ + \ +
                                \ +
                                \ +
                                  \ +
                                • Wildcard certificate cannot use [File Authentication], please select DNS authentication
                                • \ +
                                • Use [File Authentication], please make sure that [Enable HTTPS / 301 Redirect / Reverse Proxy] and other functions are not enabled.
                                • \ +
                                • Use [Alibaba Cloud DNS] [DnsPod] and other authentication methods to set the correct key
                                • \ +
                                • After the renewal is successful, the certificate will try to renew automatically 30 days before the next expiration
                                • \ +
                                • Using [DNS Authentication-Manual Resolution] Renewed certificate cannot be automatically renewed 30 days before the next expiration
                                • \ +
                                \ +
                                ', + success: function (layers) { + $("select[name='auth_to']").change(function () { + var dnsapi = $(this).val(); + $('.dnsapi-btn').html(''); + for (var i = 0; i < cert_init.dnsapi.length; i++) { + if (cert_init.dnsapi[i].name !== dnsapi) continue; + acme.dnsapi = cert_init.dnsapi[i]; + if (!cert_init.dnsapi[i].data) continue; + $('.dnsapi-btn').html(''); + if (cert_init.dnsapi[i].data[0].value || cert_init.dnsapi[i].data[1].value) break; + site.ssl.show_dnsapi_setup(); + } + }); + }, + }); + }); + } + }, + //续签其它 + renew_ssl_other: function () { + var auth_to = $("select[name='auth_to']").val(); + var auth_type = 'http'; + if (auth_to === 'http') { + if (JSON.stringify(acme.domains).indexOf('*.') !== -1) { + layer.msg('Domain names containing wildcards cannot use File Authentication (HTTP)!', { icon: 2 }); + return; + } + auth_to = acme.id; + } else { + if (auth_to !== 'dns') { + if (auth_to === 'Dns_com') { + acme.dnsapi.data = [{ value: 'None' }, { value: 'None' }]; + } + if (!acme.dnsapi.data[0].value || !acme.dnsapi.data[1].value) { + layer.msg('Please set [' + acme.dnsapi.title + '] interface information first!', { icon: 2 }); + return; + } + auth_to = auth_to + '|' + acme.dnsapi.data[0].value + '|' + acme.dnsapi.data[1].value; + } + auth_type = 'dns'; + } + layer.close(acme.select_loadT); + acme.apply_cert(acme.domains, auth_type, auth_to, '0', function (rdata) { + site.ssl.ssl_result(rdata, auth_type, acme.siteName); + }); + }, + show_dnsapi_setup: function () { + var dnsapi = acme.dnsapi; + acme.dnsapi_loadT = layer.open({ + title: 'Set [' + dnsapi.title + '] interface', + type: 1, + closeBtn: 0, + shade: 0.3, + area: '550px', + offset: '30%', + content: + '
                                \ +
                                \ + ' + + dnsapi.data[0].key + + '\ +
                                \ + \ +
                                \ +
                                \ +
                                \ + ' + + dnsapi.data[1].key + + '\ +
                                \ + \ +
                                \ +
                                \ +
                                \ + \ + \ +
                                \ +
                                  \ +
                                • ' + + dnsapi.help + + '
                                • \ +
                                \ +
                                ', + success: function (layers) { + $('.dnsapi-save').click(function () { + var dnsapi_key = $('.dnsapi-key'); + var dnsapi_token = $('.dnsapi-token'); + pdata = {}; + pdata[dnsapi_key.attr('name')] = dnsapi_key.val(); + pdata[dnsapi_token.attr('name')] = dnsapi_token.val(); + acme.dnsapi.data[0].value = dnsapi_key.val(); + acme.dnsapi.data[1].value = dnsapi_token.val(); + bt.site.set_dns_api({ pdata: JSON.stringify(pdata) }, function (ret) { + if (ret.status) layer.close(acme.dnsapi_loadT); + bt.msg(ret); + }); + }); + }, + }); + }, + set_cert: function (siteName, res) { + var loadT = bt.load(lan.site.saving_txt); + var pdata = { + type: 1, + siteName: siteName, + key: res.private_key, + csr: res.cert + res.root, + }; + bt.send('SetSSL', 'site/SetSSL', pdata, function (rdata) { + loadT.close(); + site.reload(); + layer.msg(res.msg, { icon: 1 }); + }); + }, + show_error: function (res, auth_type) { + var area_size = '500px'; + var err_info = ''; + if (res.msg[1].challenges === undefined) { + err_info += '

                                Response status:' + res.msg[1].status + '

                                '; + err_info += '

                                Error type:' + res.msg[1].type + '

                                '; + err_info += '

                                Error code:' + res.msg[1].detail + '

                                '; + } else { + if (!res.msg[1].challenges[1]) { + if (res.msg[1].challenges[0]) { + res.msg[1].challenges[1] = res.msg[1].challenges[0]; + } + } + if (res.msg[1].status === 'invalid') { + area_size = '600px'; + var trs = $('#dns_txt_jx tbody tr'); + var dns_value = ''; + + for (var imd = 0; imd < trs.length; imd++) { + if (trs[imd].outerText.indexOf(res.msg[1].identifier.value) == -1) continue; + var s_tmp = trs[imd].outerText.split('\t'); + if (s_tmp.length > 1) { + dns_value = s_tmp[1]; + break; + } + } + + err_info += '

                                Verify domain name:' + res.msg[1].identifier.value + '

                                '; + if (auth_type === 'dns') { + var check_url = '_acme-challenge.' + res.msg[1].identifier.value; + err_info += '

                                Verify record:' + check_url + '

                                '; + err_info += '

                                Verify content:' + dns_value + '

                                '; + err_info += '

                                Error code:' + site.html_encode(res.msg[1].challenges[1].error.detail) + '

                                '; + } else { + var check_url = 'http://' + res.msg[1].identifier.value + '/.well-known/acme-challenge/' + res.msg[1].challenges[0].token; + err_info += "

                                Verify URL:Click to view

                                "; + err_info += '

                                Verify content:' + res.msg[1].challenges[0].token + '

                                '; + err_info += '

                                Error code:' + site.html_encode(res.msg[1].challenges[0].error.detail) + '

                                '; + } + err_info += "

                                Verify results: Verify failed

                                "; + } + } + + layer.msg('
                                ' + res.msg[0] + '' + err_info + '
                                ', { + icon: 2, + time: 0, + shade: 0.3, + shadeClose: true, + area: area_size, + }); + }, + ssl_result: function (res, auth_type, siteName) { + layer.close(acme.loadT); + if (res.status === false && typeof res.msg === 'string') { + bt.msg(res); + return; + } + if (res.status === true || res.status === 'pending' || res.save_path !== undefined) { + if (auth_type == 'dns' && res.status === 'pending') { + var b_load = bt.open({ + type: 1, + area: '700px', + title: 'Manually parse TXT records', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "
                                \ +

                                Please do TXT analysis according to the following list:

                                \ +
                                \ +
                                \ + \ +
                                \ +
                                ", + }); + + //手动验证事件 + $('.btn_check_txt').click(function () { + acme.auth_domain(res.index, function (res1) { + layer.close(acme.loadT); + if (res1.status === true) { + b_load.close(); + site.ssl.set_cert(siteName, res1); + } else { + site.ssl.show_error(res1, auth_type); + } + }); + }); + + //显示手动验证信息 + setTimeout(function () { + var data = []; + acme_txt = '_acme-challenge.'; + for (var j = 0; j < res.auths.length; j++) { + data.push({ + name: acme_txt + res.auths[j].domain.replace('*.', ''), + type: 'TXT', + txt: res.auths[j].auth_value, + force: 'Yes', + }); + data.push({ + name: res.auths[j].domain.replace('*.', ''), + type: 'CAA', + txt: '0 issue "letsencrypt.org"', + force: 'No', + }); + } + bt.render({ + table: '#dns_txt_jx', + columns: [ + { field: 'name', width: '220px', title: 'Resolving domain names' }, + { field: 'txt', title: 'Record value' }, + { field: 'type', title: 'Types of' }, + { field: 'force', title: 'essential' }, + ], + data: data, + }); + $('.div_txt_jx').append( + bt.render_help([ + 'It takes some time to resolve the domain name to take effect. After completing all the resolution operations, please wait 1 minute before clicking the verification button.', + 'You can manually verify whether the domain name resolution is effective through CMD commands: nslookup -q=txt ' + acme_txt + res.auths[0].domain.replace('*.', ''), + 'If you are using Pagoda Cloud Resolution Plugin, Alibaba Cloud DNS, DnsPod as DNS, you can use the DNS interface to automatically resolve', + ]) + ); + }); + return; + } + site.ssl.set_cert(siteName, res); + return; + } + + site.ssl.show_error(res, auth_type); + }, + get_renew_stat: function () { + $.post('/ssl?action=Get_Renew_SSL', {}, function (task_list) { + if (!task_list.status) return; + var s_body = ''; + var b_stat = false; + for (var i = 0; i < task_list.data.length; i++) { + s_body += '

                                ' + task_list.data[i].subject + ' >> ' + task_list.data[i].msg + '

                                '; + if (task_list.data[i].status !== true && task_list.data[i].status !== false) { + b_stat = true; + } + } + + if (site.ssl.my_ssl_msg) { + $('.my-renew-ssl').html(s_body); + } else { + site.ssl.my_ssl_msg = layer.msg('
                                ' + s_body + '
                                ', { + time: 0, + icon: 16, + shade: 0.3, + }); + } + + if (!b_stat) { + setTimeout(function () { + layer.close(site.ssl.my_ssl_msg); + site.ssl.my_ssl_msg = null; + }, 3000); + return; + } + + setTimeout(function () { + site.ssl.get_renew_stat(); + }, 1000); + }); + }, + onekey_ssl: function (partnerOrderId, siteName) { + bt.site.get_ssl_info(partnerOrderId, siteName, function (rdata) { + bt.msg(rdata); + if (rdata.status) site.set_ssl(site.web); + }); + }, + set_ssl_status: function (action, siteName, ssl_id) { + bt.site.set_ssl_status(action, siteName, function (rdata) { + bt.msg(rdata); + if (rdata.status) { + site.set_ssl(site.web); + if (ssl_id != undefined) { + setTimeout(function () { + $('#ssl_tabs span:eq(' + ssl_id + ')').click(); + }, 1000); + } + if (action == 'CloseSSLConf') { + layer.msg(lan.site.ssl_close_info, { icon: 1, time: 5000 }); + } + } + }); + }, + verify_domain: function (partnerOrderId, siteName) { + bt.site.verify_domain(partnerOrderId, siteName, function (vdata) { + bt.msg(vdata); + if (vdata.status) { + if (vdata.data.stateCode == 'COMPLETED') { + site.ssl.onekey_ssl(partnerOrderId, siteName); + } else { + layer.msg('Waiting for CA verification, if it fails to verify successfully for a long time, please log in to the official website and use DNS to re-apply...'); + } + } + }); + }, + reload: function (index) { + if (index == undefined) index = 0; + var _sel = $('#ssl_tabs .on'); + if (_sel.length == 0) _sel = $('#ssl_tabs span:eq(0)'); + _sel.trigger('click'); + }, + set_auto_restart_rph: function (sitename) { + var $checkbox = $('#auto_restart_rph'); + var checked = $checkbox.is(':checked'); + var url = checked ? 'remove_auto_restart_rph' : 'auto_restart_rph'; + var loadT = bt.load(lan.site.the_msg); + $.post( + '/site?action=' + url, + { + sitename: sitename, + }, + function (res) { + loadT.close(); + bt.msg(res); + if (res.status) { + $checkbox.prop('checked', !checked); + } + } + ); + }, + }, + edit: { + update_composer: function () { + loadT = bt.load(); + $.post( + '/files?action=update_composer', + { + repo: $("select[name='repo']").val(), + }, + function (v_data) { + loadT.close(); + bt.msg(v_data); + } + ); + }, + show_composer_log: function () { + $.post( + '/ajax?action=get_lines', + { + filename: '/tmp/composer.log', + num: 30, + }, + function (v_body) { + var log_obj = $('#composer-log'); + if (log_obj.length < 1) return; + log_obj.html(v_body.msg); + var div = document.getElementById('composer-log'); + div.scrollTop = div.scrollHeight; + if (v_body.msg.indexOf('BT-Exec-Completed') != -1) { + //layer.close(site.edit.comp_showlog); + layer.msg('Execution complete', { + icon: 1, + }); + return; + } + + setTimeout(function () { + site.edit.show_composer_log(); + }, 1000); + } + ); + }, + comp_confirm: 0, + comp_showlog: 0, + exec_composer: function () { + site.edit.comp_confirm = layer.confirm( + 'The impact of Composer execution depends on the composer.json configuration file in this directory. Continue?', + { + title: 'Execute composer', + closeBtn: 2, + icon: 3, + }, + function (index) { + layer.close(site.edit.comp_confirm); + var pdata = { + php_version: $("select[name='php_version']").val(), + composer_args: $("select[name='composer_args']").val(), + composer_cmd: $("input[name='composer_cmd']").val(), + repo: $("select[name='repo']").val(), + path: $("input[name='composer_path']").val(), + user: $("select[name='composer_user']").val(), + }; + $.post('/files?action=exec_composer', pdata, function (rdatas) { + if (!rdatas.status) { + layer.msg(rdatas.msg, { + icon: 2, + }); + return false; + } + if (rdatas.status === true) { + site.edit.comp_showlog = layer.open({ + area: '800px', + type: 1, + shift: 5, + closeBtn: 2, + title: 'Execute Composer in the [' + pdata['path'] + '] directory. After execution, please close this window after confirming that there is no problem', + content: "
                                ",
                                +							});
                                +							setTimeout(function () {
                                +								site.edit.show_composer_log();
                                +							}, 200);
                                +						}
                                +					});
                                +				}
                                +			);
                                +		},
                                +		remove_composer_lock: function (path) {
                                +			$.post(
                                +				'/files?action=DeleteFile',
                                +				{
                                +					path: path + '/composer.lock',
                                +				},
                                +				function (rdata) {
                                +					bt.msg(rdata);
                                +					$('.composer-msg').remove();
                                +					$('.composer-rm').remove();
                                +				}
                                +			);
                                +		},
                                +		set_composer: function (web) {
                                +			$.post(
                                +				'/files?action=get_composer_version',
                                +				{
                                +					path: web.path,
                                +				},
                                +				function (v_data) {
                                +					if (v_data.status === false) {
                                +						bt.msg(v_data);
                                +						return;
                                +					}
                                +
                                +					var php_versions = '';
                                +					for (var i = 0; i < v_data.php_versions.length; i++) {
                                +						if (v_data.php_versions[i].version == '00') continue;
                                +						php_versions += '';
                                +					}
                                +
                                +					var msg = '';
                                +					if (v_data.comp_lock) {
                                +						msg += '' + v_data.comp_lock + ' [Delete]';
                                +					}
                                +					if (v_data.comp_json !== true) {
                                +						msg += '' + v_data.comp_json + '';
                                +					}
                                +
                                +					var com_body =
                                +						'' +
                                +						'
                                Version
                                ' + + '
                                PHP
                                ' + + '' + + '
                                ' + + '
                                Parameters
                                ' + + '' + + '
                                ' + + '
                                Extra commands
                                ' + + '' + + '
                                ' + + '
                                Source
                                ' + + '' + + '
                                ' + + '
                                User
                                ' + + '' + + '
                                ' + + '
                                Dir
                                ' + + '' + + '
                                ' + + '
                                ' + + msg + + '
                                ' + + '
                                ' + + '
                                ' + + '
                                  ' + + '
                                • Directory:Website root dir by default, please make sure that the dir contains composer.json
                                • ' + + '
                                • User:The default user www, unless your website is run with root privileges, it is not recommended to use the root user to execute composer
                                • ' + + '
                                • Source:source of composer
                                • ' + + '
                                • Parameters:Install (install dependent package), Update (upgrade dependent package), please select as needed
                                • ' + + '
                                • Extra commands: If this is empty, it will be executed according to the conf in composer.json, Supported fill in the complete composer command
                                • ' + + '
                                • PHP version:The PHP version used to execute composer, it is recommended to try the default, if the installation fails, try to choose another PHP version
                                • ' + + '
                                • Composer version:Composer version, you can click [Upgrade Composer] on the right to upgrade Composer to the latest stable version
                                • ' + + '
                                '; + $('#webedit-con').html(com_body); + } + ); + }, + set_domains: function (web) { + var _this = this; + var list = [ + { + items: [ + { name: 'newdomain', width: '400px', type: 'textarea', placeholder: lan.site.domain_help }, + { + name: 'btn_submit_domain', + text: lan.site.add, + type: 'button', + callback: function (sdata) { + var arrs = sdata.newdomain.split('\n'); + var domins = ''; + for (var i = 0; i < arrs.length; i++) domins += arrs[i] + ','; + bt.site.add_domains(web.id, web.name, bt.rtrim(domins, ','), function (ret) { + if (ret.status) site.reload(0); + }); + }, + }, + ], + }, + ]; + var _form_data = bt.render_form_line(list[0]), + loadT = null, + placeholder = null; + $('#webedit-con').html(_form_data.html + "
                                "); + bt.render_clicks(_form_data.clicks); + $('.btn_submit_domain').addClass('pull-right').css('margin', '30px 35px 0 0'); + placeholder = $('.placeholder'); + placeholder + .click(function () { + $(this).hide(); + $('.newdomain').focus(); + }) + .css({ + width: '340px', + heigth: '100px', + left: '0px', + top: '0px', + 'padding-top': '10px', + 'padding-left': '15px', + }); + $('.newdomain') + .focus(function () { + placeholder.hide(); + loadT = layer.tips(placeholder.html(), $(this), { tips: [1, '#20a53a'], time: 0, area: $(this).width() }); + }) + .blur(function () { + if ($(this).val().length == 0) placeholder.show(); + layer.close(loadT); + }); + + bt_tools.table({ + el: '#domain_table', + url: '/data?action=getData', + param: { table: 'domain', list: 'True', search: web.id }, + dataFilter: function (res) { + return { data: res }; + }, + column: [ + { type: 'checkbox', width: 20, keepNumber: 1 }, + { + fid: 'name', + title: lan.site.domain, + template: function (row) { + return '' + row.name + ''; + }, + }, + { fid: 'port', title: lan.site.port, width: 50, type: 'text' }, + { + title: 'OPT', + width: 80, + type: 'group', + align: 'right', + group: [ + { + title: 'Del', + template: function (row, that) { + return that.data.length === 1 ? 'Inoperable' : 'Del'; + }, + event: function (row, index, ev, key, that) { + if (that.data.length === 1) { + bt.msg({ status: false, msg: 'The last domain name cannot be deleted!' }); + return false; + } + bt.confirm( + { + title: 'Delete domain [ ' + row.name + ' ]', + msg: lan.site.domain_del_confirm, + }, + function () { + bt.site.del_domain(web.id, web.name, row.name, row.port, function (res) { + if (res.status) that.$delete_table_row(index); + bt.msg(res); + }); + } + ); + }, + }, + ], + }, + ], + tootls: [ + { + // 批量操作 + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' delete', + url: '/site?action=delete_domain_multiple', + param: { id: web.id }, + paramId: 'id', + paramName: 'domains_id', + theadName: 'Domain', + confirmVerify: false, //是否提示验证方式 + refresh: true, + }, + }, + ], + }); + $('#domain_table>.divtable').css('max-height', '350px'); + }, + set_dirbind: function (web) { + var _this = this; + $('#webedit-con').html('
                                '); + bt_tools.table({ + el: '#sub_dir_table', + url: '/site?action=GetDirBinding', + param: { id: web.id }, + dataFilter: function (res) { + if ($('#webedit-con').children().length === 2) return { data: res.binding }; + var dirs = []; + for (var n = 0; n < res.dirs.length; n++) dirs.push({ title: res.dirs[n], value: res.dirs[n] }); + var data = { + title: '', + class: 'mb0', + items: [ + { title: lan.site.domain, width: '140px', name: 'domain' }, + { title: lan.site.subdirectories, name: 'dirName', type: 'select', items: dirs }, + { + text: lan.site.add, + type: 'button', + name: 'btn_add_subdir', + callback: function (sdata) { + if (!sdata.domain || !sdata.dirName) { + layer.msg(lan.site.d_s_empty, { icon: 2 }); + return; + } + bt.site.add_dirbind(web.id, sdata.domain, sdata.dirName, function (ret) { + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + if (ret.status) site.reload(1); + }); + }, + }, + ], + }; + var _form_data = bt.render_form_line(data); + $('#webedit-con').prepend(_form_data.html); + bt.render_clicks(_form_data.clicks); + return { data: res.binding }; + }, + column: [ + { type: 'checkbox', width: 20, keepNumber: 1 }, + { fid: 'domain', title: lan.site.domain, type: 'text' }, + { fid: 'port', title: lan.site.port, width: 70, type: 'text' }, + { fid: 'path', title: lan.site.subdirectories, width: 70, type: 'text' }, + { + title: 'Opt', + width: 130, + type: 'group', + align: 'right', + group: [ + { + title: 'URL rewrite', + event: function (row, index, ev, key, that) { + bt.site.get_dir_rewrite({ id: row.id }, function (ret) { + if (!ret.status) { + var confirmObj = layer.confirm( + lan.site.url_rewrite_alter, + { + icon: 3, + closeBtn: 2, + }, + function () { + bt.site.get_dir_rewrite({ id: row.id, add: 1 }, function (ret) { + layer.close(confirmObj); + show_dir_rewrite(ret); + }); + } + ); + return; + } + show_dir_rewrite(ret); + + function get_rewrite_file(name) { + var spath = '/www/server/panel/rewrite/' + (bt.get_cookie('serverType') == 'openlitespeed' ? 'apache' : bt.get_cookie('serverType')) + '/' + name + '.conf'; + if (bt.get_cookie('serverType') == 'nginx') { + if (name == 'default') spath = '/www/server/panel/vhost/rewrite/' + web.name + '_' + row['path'] + '.conf'; + } else { + if (name == 'default') spath = '/www/wwwroot/' + web.name + '/' + row['path'] + '.htaccess'; + } + bt.files.get_file_body(spath, function (sdata) { + $('.dir_config').text(sdata.data); + }); + } + + function show_dir_rewrite(ret) { + var load_form = bt.open({ + type: 1, + area: ['510px', '530px'], + title: lan.site.config_url, + closeBtn: 2, + shift: 5, + skin: 'bt-w-con', + shadeClose: true, + content: "
                                ", + success: function () { + var _html = $('.webedit-dir-box'), + arrs = []; + for (var i = 0; i < ret.rlist.length; i++) { + if (i == 0) { + arrs.push({ title: ret.rlist[i], value: 'default' }); + } else { + arrs.push({ title: ret.rlist[i], value: ret.rlist[i] }); + } + } + var datas = [ + { + name: 'dir_rewrite', + type: 'select', + width: '130px', + items: arrs, + callback: function (obj) { + get_rewrite_file(obj.val()); + }, + }, + { + items: [ + { + name: 'dir_config', + type: 'textarea', + value: ret.data, + width: '470px', + height: '260px', + }, + ], + }, + { + items: [ + { + name: 'btn_save', + text: 'Save', + type: 'button', + callback: function (ldata) { + // console.log(ret) + bt.files.set_file_body(ret.filename, ldata.dir_config, 'utf-8', function (sdata) { + if (sdata.status) load_form.close(); + bt.msg(sdata); + }); + }, + }, + ], + }, + ]; + var clicks = []; + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + _html.append(_form_data.html); + var _other = + bt.os == 'Linux' && i == 0 ? 'Rewrite rule converter:Apache to Nginx' : ''; + _html.find('.info-r').append(_other); + clicks = clicks.concat(_form_data.clicks); + } + _html.append( + bt.render_help([ + 'Please select your application.', + 'If the site cannot be accessed after the rewrite rules set, please try to reset to default.', + 'You are able to modify rewrite rules, just save it after modification.', + ]) + ); + bt.render_clicks(clicks); + get_rewrite_file($('.dir_rewrite option:eq(0)').val()); + }, + }); + } + }); + }, + }, + { + title: 'Del', + event: function (row, index, ev, key, that) { + bt.confirm( + { + title: 'Are you sure to delete this【' + row.path + '】 subdirectory binding?', + msg: lan.site.s_bin_del, + }, + function () { + bt.site.del_dirbind(row.id, function (res) { + if (res.status) that.$delete_table_row(index); + bt.msg(res); + }); + } + ); + }, + }, + ], + }, + ], + tootls: [ + { + // 批量操作 + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' execute', + url: '/site?action=delete_dir_bind_multiple', + param: { id: web.id }, + paramId: 'id', + paramName: 'bind_ids', + theadName: 'Domain', + confirmVerify: false, //是否提示验证方式 + }, + }, + ], + }); + }, + set_dirpath: function (web) { + var loading = bt.load(); + bt.site.get_site_path(web.id, function (path) { + bt.site.get_dir_userini(web.id, path, function (rdata) { + loading.close(); + var dirs = []; + var is_n = false; + for (var n = 0; n < rdata.runPath.dirs.length; n++) { + dirs.push({ title: rdata.runPath.dirs[n], value: rdata.runPath.dirs[n] }); + if (rdata.runPath.runPath === rdata.runPath.dirs[n]) is_n = true; + } + if (!is_n) dirs.push({ title: rdata.runPath.runPath, value: rdata.runPath.runPath }); + var datas = [ + { + title: '', + items: [ + { + name: 'userini', + type: 'checkbox', + text: lan.site.anti_XSS_attack + '(open_basedir)', + value: rdata.userini, + callback: function (sdata) { + bt.site.set_dir_userini(path, web.id, function (ret) { + if (ret.status) site.reload(2); + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + }); + }, + }, + { + name: 'logs', + type: 'checkbox', + text: lan.site.write_access_log, + value: rdata.logs, + callback: function (sdata) { + bt.site.set_logs_status(web.id, function (ret) { + if (ret.status) site.reload(2); + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + }); + }, + }, + ], + }, + { + title: '', + items: [ + { + name: 'path', + title: lan.site.site_menu_2, + width: '240px', + value: path, + add_class: 'ml5', + event: { + css: 'glyphicon-folder-open', + callback: function (obj) { + bt.select_path(obj); + }, + }, + }, + { + name: 'btn_site_path', + type: 'button', + text: lan.site.save, + callback: function (pdata) { + bt.site.set_site_path_new(web.id, pdata.path, web.name, function (ret) { + if (ret.status) site.reload(2); + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + }); + }, + }, + ], + }, + { + title: '', + items: [ + { + title: lan.site.run_dir, + width: '240px', + value: rdata.runPath.runPath, + name: 'dirName', + type: 'select', + add_class: 'ml5 mr20', + items: dirs, + }, + { + name: 'btn_run_path', + type: 'button', + text: lan.site.save, + callback: function (pdata) { + bt.site.set_site_runpath(web.id, pdata.dirName, function (ret) { + if (ret.status) site.reload(2); + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + }); + }, + }, + ], + }, + ]; + var _html = $("
                                "); + var clicks = []; + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + _html.append($(_form_data.html).addClass('line mtb10')); + clicks = clicks.concat(_form_data.clicks); + } + _html.find('input[name="path"]').parent().css('padding-left', '27px'); + _html.find('input[type="checkbox"]').parent().addClass('label-input-group ptb10'); + _html.find('button[name="btn_run_path"]').addClass('ml45'); + _html.find('button[name="btn_site_path"]').addClass('ml33'); + _html.append(bt.render_help([lan.site.specify_subdir])); + if (bt.os == 'Linux') + _html.append( + '
                                ' + + lan.site.pass_visit + + '
                                ' + ); + + $('#webedit-con').append(_html); + bt.render_clicks(clicks); + $('#pathSafe').click(function () { + var val = $(this).prop('checked'); + var _div = $('.user_pw'); + if (val) { + var dpwds = [ + { + title: lan.site.access_account, + width: '250px', + name: 'username_get', + placeholder: lan.site.no_change_set_empty, + }, + { + title: lan.site.pass_visit, + width: '250px', + type: 'password', + name: 'password_get_1', + placeholder: lan.site.no_change_set_empty, + }, + { + title: lan.site.pass_again, + width: '250px', + type: 'password', + name: 'password_get_2', + placeholder: lan.site.no_change_set_empty, + }, + { + name: 'btn_password_get', + text: lan.site.save, + type: 'button', + callback: function (rpwd) { + if (rpwd.password_get_1 != rpwd.password_get_2) { + layer.msg(lan.bt.pass_err_re, { icon: 2 }); + return; + } + bt.site.set_site_pwd(web.id, rpwd.username_get, rpwd.password_get_1, function (ret) { + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + if (ret.status) site.reload(2); + }); + }, + }, + ]; + for (var i = 0; i < dpwds.length; i++) { + var _from_pwd = bt.render_form_line(dpwds[i]); + _div.append('
                                ' + _from_pwd.html + '
                                '); + bt.render_clicks(_from_pwd.clicks); + } + } else { + bt.site.close_site_pwd(web.id, function (rdata) { + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + _div.html(''); + }); + } + }); + if (rdata.pass) $('#pathSafe').trigger('click'); + }); + }); + }, + set_dirguard: function (web) { + $('#webedit-con').html('
                                '); + var tab = + '
                                \ + Limit accessDeny access\ +
                                \ +
                                \ + '; + $('#set_dirguard').html(tab); + var dir_dirguard = bt_tools.table({ + el: '#dir_dirguard', + url: '/site?action=get_dir_auth', + param: { id: web.id }, + dataFilter: function (res) { + return { data: res[web.name] }; + }, + column: [ + { type: 'checkbox', width: 20 }, + { fid: 'name', title: lan.site.name, type: 'text' }, + { fid: 'site_dir', title: 'Path', type: 'text' }, + { + title: lan.site.operate, + width: 110, + type: 'group', + align: 'right', + group: [ + { + title: lan.site.edit, + event: function (row, index, ev, key, that) { + site.edit.template_Dir(web.id, false, row); + }, + }, + { + title: lan.site.del, + event: function (row, index, ev, key, that) { + bt.site.delete_dir_guard(web.id, row.name, function (res) { + if (res.status) that.$delete_table_row(index); + bt.msg(res); + }); + }, + }, + ], + }, + ], + tootls: [ + { + // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: 'Add limit access', + active: true, + event: function (ev) { + site.edit.template_Dir(web.id, true); + }, + }, + ], + }, + { + // 批量操作 + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' delete', + url: '/site?action=delete_dir_auth', + param: function (row) { + return { + id: web.id, + name: row.name, + }; + }, + load: true, + callback: function (that) { + // 手动执行,data参数包含所有选中的站点 + bt.show_confirm('Delete limit access', 'Do you want to delete limit access ?', function () { + that.start_batch({}, function (list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += '' + item.name + '' + item.request.msg + ''; + } + dir_dirguard.$batch_success_table({ + title: 'Limit access', + th: 'Limit access name', + html: html, + }); + dir_dirguard.$refresh_table_list(true); + }); + }); + }, + }, + }, + ], + }); + var php_dirguard = bt_tools.table({ + el: '#php_dirguard', + url: '/config?action=get_file_deny', + param: { + website: web.name, + }, + dataFilter: function (res) { + return { + data: res, + }; + }, + column: [ + { type: 'checkbox', width: 20 }, + { + fid: 'name', + title: lan.site.name, + type: 'text', + }, + { + fid: 'dir', + title: 'Path', + type: 'text', + template: function (row) { + return '' + row.dir + ''; + }, + }, + { + fid: 'suffix', + title: 'Suffix', + template: function (row) { + return '' + row.suffix + ''; + }, + }, + { + title: lan.site.operate, + width: 110, + type: 'group', + align: 'right', + group: [ + { + title: lan.site.edit, + event: function (row, index, ev, key, that) { + site.edit.template_php(web.name, row); + }, + }, + { + title: lan.site.del, + event: function (row, index, ev, key, that) { + site.edit.del_php_deny(web.name, row.name, function (res) { + if (res.status) that.$delete_table_row(index); + bt.msg(res); + }); + }, + }, + ], + }, + ], + tootls: [ + { + // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: 'Add deny access', + active: true, + event: function (ev) { + site.edit.template_php(web.name); + }, + }, + ], + }, + { + // 批量操作 + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' delete', + url: '/site?action=del_file_deny', + param: function (row) { + return { + website: web.name, + deny_name: row.name, + }; + }, + load: true, + callback: function (that) { + // 手动执行,data参数包含所有选中的站点 + bt.show_confirm('Delete deny access', 'Do you want to delete deny access?', function () { + that.start_batch({}, function (list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += + '' + + item.name + + '
                                ' + + item.request.msg + + '
                                '; + } + php_dirguard.$batch_success_table({ + title: 'Deny access', + th: 'Deny access name', + html: html, + }); + php_dirguard.$refresh_table_list(true); + }); + }); + }, + }, + }, + ], + }); + $('#dir_dirguard>.divtable,#php_dirguard>.divtable').css('max-height', '340px'); + $('#dir_dirguard').append( + "
                                  \ +
                                • After setting, you need to enter the password to access it.
                                • \ +
                                • For example, if I set the limit path /test/ , then I need to enter the account password to access http://aaa.com/test/
                                • \ +
                                " + ); + $('#php_dirguard').append( + "
                                  \ +
                                • Suffix: Indicates the suffix that is not allowed to access, if there are more than one, separate with'|'.
                                • \ +
                                • Path: Quote rules in this directory. e.g: /a/
                                • \ +
                                • For Example, if you want to deny http://test.com/a/index.php
                                • \ +
                                • Please fill in [ /a/ ]
                                • \ +
                                " + ); + $('#set_dirguard').on('click', '.tab-nav span', function () { + var index = $(this).index(); + $(this).addClass('on').siblings().removeClass('on'); + if (index == 0) { + $('#dir_dirguard').show(); + $('#php_dirguard').hide(); + } else { + $('#php_dirguard').show(); + $('#dir_dirguard').hide(); + } + }); + }, + ols_cache: function (web) { + bt.send('get_ols_static_cache', 'config/get_ols_static_cache', { id: web.id }, function (rdata) { + var clicks = [], + newkey = [], + newval = [], + checked = false; + Object.keys(rdata).forEach(function (key) { + //for (let key in rdata) { + newkey.push(key); + newval.push(rdata[key]); + }); + var datas = [ + { title: newkey[0], name: newkey[0], width: '30%', value: newval[0] }, + { title: newkey[1], name: newkey[1], width: '30%', value: newval[1] }, + { title: newkey[2], name: newkey[2], width: '30%', value: newval[2] }, + { title: newkey[3], name: newkey[3], width: '30%', value: newval[3] }, + { + name: 'static_save', + text: lan.site.save, + type: 'button', + callback: function (ldata) { + var cdata = {}, + loadT = bt.load(); + Object.assign(cdata, ldata); + delete cdata.static_save; + delete cdata.maxage; + delete cdata.exclude_file; + delete cdata.private_save; + bt.send( + 'set_ols_static_cache', + 'config/set_ols_static_cache', + { + values: JSON.stringify(cdata), + id: web.id, + }, + function (res) { + loadT.close(); + bt.msg(res); + } + ); + }, + }, + { title: 'test', name: 'test', width: '30%', value: '11' }, + { title: 'maxage', name: 'maxage', width: '30%', value: '43200' }, + { title: 'exclude file', name: 'exclude_file', width: '35%', value: 'fdas.php' }, + { + name: 'private_save', + text: lan.site.save, + type: 'button', + callback: function (ldata) { + var edata = {}, + loadT = bt.load(); + if (checked) { + edata.id = web.id; + edata.max_age = parseInt($("input[name='maxage']").val()); + edata.exclude_file = $("textarea[name='exclude_file']").val(); + bt.send('set_ols_private_cache', 'config/set_ols_private_cache', edata, function (res) { + loadT.close(); + bt.msg(res); + }); + } + }, + }, + ], + _html = $('
                                '); + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + _html.append(_form_data.html); + clicks = clicks.concat(_form_data.clicks); + } + $('#webedit-con').append(_html); + $("input[name='exclude_file']").parent().removeAttr('class').html(''); + $("input[name='test']") + .parent() + .parent() + .html( + '
                                private cache
                                ' + ); + var private = $("input[name='maxage'],textarea[name='exclude_file'],button[name='private_save']").parent().parent(); + $('input.bt-input-text').parent().append('sec'); + $("button[name='static_save']") + .parent() + .append(bt.render_help(['The default static file cache time is 604800 seconds', 'If you want to shut down, please change it to 0 seconds'])); + $('.ols').append(bt.render_help(['Private cache only supports page caching for PHP and cache time is 120 seconds by default', 'Exclude files only support files with PHP as the suffix'])); + private.hide(); + bt.send('get_ols_private_cache_status', 'config/get_ols_private_cache_status', { id: web.id }, function (kdata) { + checked = kdata; + if (kdata) { + bt.send('get_ols_private_cache', 'config/get_ols_private_cache', { id: web.id }, function (fdata) { + $("input[name='maxage']").val(fdata.maxage); + var ss = fdata.exclude_file.join(' '); + $("textarea[name='exclude_file']").html(ss); + $('#ols').attr('checked', true); + private.show(); + }); + } + }); + $('#ols').on('click', function () { + var loadT = bt.load(); + bt.send('switch_ols_private_cache', 'config/switch_ols_private_cache', { id: web.id }, function (res) { + loadT.close(); + private.toggle(); + checked = private.is(':hidden') ? false : true; + bt.msg(res); + if (checked) { + bt.send('get_ols_private_cache', 'config/get_ols_private_cache', { id: web.id }, function (fdata) { + private.show(); + $("input[name='maxage']").val(fdata.maxage); + $("textarea[name='exclude_file']").html(fdata.exclude_file.join(' ')); + }); + } + }); + }); + bt.render_clicks(clicks); + $("button[name='private_save']").parent().css('margin-bottom', '-13px'); + $('.ss-text').css('margin-left', '66px'); + $('.ols .btn-success').css('margin-left', '125px'); + }); + }, + limit_network: function (web) { + bt.site.get_limitnet(web.id, function (rdata) { + var limits = [ + { title: lan.site.bbs_or_blog, value: 1, items: { perserver: 300, perip: 25, limit_rate: 512 } }, + { title: lan.site.photo_station, value: 2, items: { perserver: 200, perip: 10, limit_rate: 1024 } }, + { title: lan.site.download_station, value: 3, items: { perserver: 50, perip: 3, limit_rate: 2048 } }, + { title: lan.site.mall, value: 4, items: { perserver: 500, perip: 10, limit_rate: 2048 } }, + { title: lan.site.portal_site, value: 5, items: { perserver: 400, perip: 15, limit_rate: 1024 } }, + { title: lan.site.enterprise, value: 6, items: { perserver: 60, perip: 10, limit_rate: 512 } }, + { title: lan.site.video, value: 7, items: { perserver: 150, perip: 4, limit_rate: 1024 } }, + ]; + var datas = [ + { + items: [ + { + name: 'status', + type: 'checkbox', + value: rdata.perserver != 0 ? true : false, + text: lan.site.limit_net_8, + callback: function (ldata) { + if (ldata.status) { + bt.site.set_limitnet(web.id, ldata.perserver, ldata.perip, ldata.limit_rate, function (ret) { + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + if (ret.status) site.reload(3); + }); + } else { + bt.site.close_limitnet(web.id, function (ret) { + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + if (ret.status) site.reload(3); + }); + } + }, + }, + ], + }, + { + title: lan.site.limit_net_9 + ' ', + width: '160px', + name: 'limit', + type: 'select', + items: limits, + callback: function (obj) { + var data = limits.filter(function (p) { + return p.value === parseInt(obj.val()); + })[0]; + for (var key in data.items) $('input[name="' + key + '"]').val(data.items[key]); + }, + }, + { + title: lan.site.limit_net_10 + ' ', + type: 'number', + width: '200px', + value: rdata.perserver, + name: 'perserver', + }, + { + title: lan.site.limit_net_12 + ' ', + type: 'number', + width: '200px', + value: rdata.perip, + name: 'perip', + }, + { + title: lan.site.limit_net_14 + ' ', + type: 'number', + width: '200px', + value: rdata.limit_rate, + name: 'limit_rate', + }, + { + name: 'btn_limit_get', + text: lan.site.save, + type: 'button', + callback: function (ldata) { + bt.site.set_limitnet(web.id, ldata.perserver, ldata.perip, ldata.limit_rate, function (ret) { + layer.msg(ret.msg, { icon: ret.status ? 1 : 2 }); + if (ret.status) site.reload(3); + }); + }, + }, + ]; + var _html = $("
                                "); + var clicks = []; + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + _html.append(_form_data.html); + clicks = clicks.concat(_form_data.clicks); + } + _html.find('input[type="checkbox"]').parent().addClass('label-input-group ptb10'); + _html.append(bt.render_help([lan.site.limit_net_11, lan.site.limit_net_13, lan.site.limit_net_15])); + $('#webedit-con').append(_html); + $('.newnanme .tname').css('width', '138px'); + bt.render_clicks(clicks); + if (rdata.perserver == 0) $("select[name='limit']").trigger('change'); + $('#status,.btn_limit_get').css('margin-left', '138px'); + }); + }, + get_rewrite_list: function (web) { + var filename = '/www/server/panel/vhost/rewrite/' + web.name + '.conf'; + bt.site.get_rewrite_list(web.name, function (rdata) { + var arrs = [], + webserver = bt.get_cookie('serverType'); + if (webserver == 'apache' || webserver == 'openlitespeed') filename = rdata.sitePath + '/.htaccess'; + if (webserver == 'openlitespeed') webserver = 'apache'; + for (var i = 0; i < rdata.rewrite.length; i++) + arrs.push({ + title: rdata.rewrite[i], + value: rdata.rewrite[i], + }); + var datas = [ + { + name: 'rewrite', + type: 'select', + width: '130px', + items: arrs, + callback: function (obj) { + if (bt.os == 'Linux') { + var spath = filename; + if (obj.val() != lan.site.rewritename) spath = '/www/server/panel/rewrite/' + (webserver == 'openlitespeed' ? 'apache' : webserver) + '/' + obj.val() + '.conf'; + bt.files.get_file_body(spath, function (ret) { + if (ret.status == false) { + layer.msg(ret.msg, { icon: 2 }); + return false; + } + aceEditor.ACE.setValue(ret.data); + aceEditor.ACE.moveCursorTo(0, 0); + aceEditor.path = spath; + }); + } + }, + }, + { items: [{ name: 'config', type: 'div', value: rdata.data, widht: '340px', height: '200px' }] }, + { + items: [ + { + name: 'btn_save', + text: lan.site.save, + type: 'button', + callback: function (ldata) { + // bt.files.set_file_body(filename, editor.getValue(), 'utf-8', function(ret) { + // if (ret.status) site.reload(4) + // bt.msg(ret); + // }) + aceEditor.path = filename; + bt.saveEditor(aceEditor); + }, + }, + { + name: 'btn_save_to', + text: lan.site.save_as_template, + type: 'button', + callback: function (ldata) { + var temps = { + title: lan.site.save_rewrite_temp, + area: '330px', + list: [ + { + title: lan.site.template_name, + placeholder: lan.site.template_name, + width: '160px', + name: 'tempname', + }, + ], + btns: [ + { title: lan.site.turn_off, name: 'close' }, + { + title: lan.site.submit, + name: 'submit', + css: 'btn-success', + callback: function (rdata, load, callback) { + var name = rdata.tempname; + if (name === '') return layer.msg('The template name cannot be empty!', { icon: 2 }); + var isSameName = false; + for (var i = 0; i < arrs.length; i++) { + if (arrs[i].value == name) { + isSameName = true; + break; + } + } + var save_to = function () { + bt.site.set_rewrite_tel(name, aceEditor.ACE.getValue(), function (rRet) { + if (rRet.status) { + load.close(); + site.reload(4); + } + bt.msg(rRet); + }); + }; + if (isSameName) { + return layer.msg('The template name already exists, please re-enter the template name!', { icon: 2 }); + } else { + save_to(); + } + }, + }, + ], + }; + bt.render_form(temps); + }, + }, + ], + }, + ]; + var _html = $("
                                "); + var clicks = []; + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + _html.append(_form_data.html); + var _other = + bt.os == 'Linux' && i == 0 + ? '' + lan.site.rewrite_change_tools + ':' + lan.site.ap_change_ng + '' + : ''; + _html.find('.info-r').append(_other); + clicks = clicks.concat(_form_data.clicks); + } + _html.append(bt.render_help([lan.site.rewrite_tips_1, lan.site.rewrite_tips_2, lan.site.edit_rewrite])); + $('#webedit-con').append(_html); + bt.render_clicks(clicks); + + // $('textarea.config').attr('id', 'config_rewrite'); + // var editor = CodeMirror.fromTextArea(document.getElementById("config_rewrite"), { + // extraKeys: { "Ctrl-Space": "autocomplete" }, + // lineNumbers: true, + // matchBrackets: true, + // }); + + // $(".CodeMirror-scroll").css({ "height": "340px", "margin": 0, "padding": 0 }); + // $(".soft-man-con .CodeMirror").css({ "height": "342px" }); + // setTimeout(function() { + // editor.refresh(); + // }, 250); + $('div.config').attr('id', 'config_rewrite').css({ height: '360px', width: '540px' }); + var aceEditor = bt.aceEditor({ el: 'config_rewrite', content: rdata.data }); + + $('select.rewrite').trigger('change'); + }); + }, + set_default_index: function (web) { + bt.site.get_index(web.id, function (rdata) { + rdata = rdata.replace(new RegExp(/(,)/g), '\n'); + var data = { + items: [ + { name: 'Dindex', height: '230px', width: '50%', type: 'textarea', value: rdata }, + { + name: 'btn_submit', + text: lan.site.add, + type: 'button', + callback: function (ddata) { + var Dindex = ddata.Dindex.replace(new RegExp(/(\n)/g), ','); + bt.site.set_index(web.id, Dindex, function (ret) { + if (!ret.status) { + bt.msg(ret); + return; + } + + site.reload(5); + }); + }, + }, + ], + }; + var _form_data = bt.render_form_line(data); + var _html = $(_form_data.html); + _html.append(bt.render_help([lan.site.default_doc_help])); + $('#webedit-con').append(_html); + $('.btn_submit').addClass('pull-right').css('margin', '90px 100px 0 0'); + bt.render_clicks(_form_data.clicks); + }); + }, + set_config: function (web) { + var con = + '

                                Tips:Ctrl+F Search keywords,Ctrl+S Save,Ctrl+H Search and replace

                                \ + \ +
                                  \ +
                                • This is primary configuration file of the site.
                                • \ +
                                • Do not modify it at will if you do not know configuration rules.
                                • \ +
                                '; + $('#webedit-con').html(con); + var webserve = bt.get_cookie('serverType'), + config = bt.aceEditor({ + el: 'siteConfigBody', + path: '/www/server/panel/vhost/' + (webserve == 'openlitespeed' ? webserve + '/detail' : webserve) + '/' + web.name + '.conf', + }); + $('#OnlineEditFileBtn').click(function (e) { + bt.saveEditor(config); + }); + }, + set_php_version: function (web) { + bt.site.get_site_phpversion(web.name, function (sdata) { + if (sdata.status === false) { + bt.msg(sdata); + return; + } + bt.site.get_all_phpversion(function (vdata) { + var versions = []; + for (var j = vdata.length - 1; j >= 0; j--) { + var o = vdata[j]; + o.value = o.version; + o.title = o.name; + versions.push(o); + } + + // var data = { + // items: [ + // { + // title: 'PHP版本', + // name: 'versions', + // value: sdata.phpversion, + // type: 'select', + // items: versions , + // ps:'' + // }, + // { + // text: '切换', + // name: 'btn_change_phpversion', + // type: 'button', + // callback: function(pdata) { + // var other = $('.other-version').val(); + // if(pdata.versions == 'other' && other == ''){ + // layer.msg('自定义PHP版本时,PHP连接配置不能为空'); + // $('.other-version').focus(); + // return; + // } + // bt.site.set_phpversion(web.name, pdata.versions, other, function(ret) { + // if (ret.status) { + // var versions = $('[name="versions"]').val(); + // versions = versions.slice(0, versions.length - 1) + '.' + versions.slice(-1); + // if (versions == '0.0') versions = '静态'; + // site_table.$refresh_table_list(true); + // site.reload() + // setTimeout(function() { + // bt.msg(ret); + // }, 1000); + // }else{ + // bt.msg(ret); + // } + + // }) + // } + // } + // ] + // } + // var _form_data = bt.render_form_line(data); + // var _html = $(_form_data.html); + // _html.append(bt.render_help([lan.site.switch_php_help1, lan.site.switch_php_help2, lan.site.switch_php_help3])); + // $('#webedit-con').append(_html); + // bt.render_clicks(_form_data.clicks); + // $('#webedit-con').append('
                                ' + lan.site.session_off + '
                                ' + bt.render_help([lan.site.independent_storage])); + + // function get_session_status() { + // var loading = bt.load('Getting session status...'); + // bt.send('get_php_session_path', 'config/get_php_session_path', { id: web.id }, function(tdata) { + // loading.close(); + // $('#session_switch').prop("checked", tdata); + // }) + // }; + // get_session_status() + // $('#session_switch').click(function() { + // var val = $(this).prop('checked'); + // bt.send('set_php_session_path', 'config/set_php_session_path', { id: web.id, act: val ? 1 : 0 }, function(rdata) { + // get_session_status(); + // bt.msg(rdata) + // }); + // }) + + var data = { + items: [ + { + title: 'PHP version', + name: 'versions', + value: sdata.phpversion, + type: 'select', + items: versions, + ps: + '', + }, + { + text: 'Switch', + name: 'btn_change_phpversion', + type: 'button', + callback: function (pdata) { + var other = $('.other-version').val(); + if (pdata.versions == 'other' && other == '') { + layer.msg('When customizing the PHP version, the PHP connection configuration cannot be empty'); + $('.other-version').focus(); + return; + } + bt.site.set_phpversion(web.name, pdata.versions, other, function (ret) { + if (ret.status) { + var versions = $('[name="versions"]').val(); + versions = versions.slice(0, versions.length - 1) + '.' + versions.slice(-1); + if (versions == '0.0') versions = 'Static'; + site_table.$refresh_table_list(true); + site.reload(); + setTimeout(function () { + bt.msg(ret); + }, 1000); + } else { + bt.msg(ret); + } + }); + }, + }, + ], + }; + var _form_data = bt.render_form_line(data); + var _html = $(_form_data.html); + _html.append( + bt.render_help([ + 'Select the version according to your program requirements', + 'Try not to use PHP5.2 unless you have to, as this can reduce your server security', + 'PHP7 does not support the MySQL extension. The default installation is mysqli and mysql-pdo', + '[Customize] You can customize the PHP connection information by selecting the available PHP connection configuration', + '[Customize] Currently only support NGINX', + 'Support TCP or UNIX configuration. Example: 192.168.1.25:9001 or unix:/tmp/php8.sock', + ]) + ); + $('#webedit-con').append(_html); + bt.render_clicks(_form_data.clicks); + if (sdata.phpversion != 'other') { + var tips = bt.render_help([ + 'When enabled, session files will be stored in a separate folder, not in a common storage location with other sites', + 'Do not enable this option if you are saving sessions to caches such as memcache/redis in your PHP configuration', + ]); + + $('#webedit-con').append( + '\ +
                                \ + ' + + lan.site.session_off + + '\ + \ + \ + \ + \ +
                                \ +
                                ' + + tips + ); + } + if (sdata.phpversion != 'other') { + $('.other-version').hide(); + } + setTimeout(function () { + $('select[name="versions"]').change(function () { + var phpversion = $(this).val(); + // console.log(phpversion); + if (phpversion == 'other') { + $('.other-version').show(); + } else { + $('.other-version').hide(); + } + }); + }, 500); + + function get_session_status() { + var loading = bt.load('Please wait while getting session status'); + bt.send('get_php_session_path', 'config/get_php_session_path', { id: web.id }, function (tdata) { + loading.close(); + $('#session_switch').prop('checked', tdata); + }); + } + get_session_status(); + $('#session_switch').click(function () { + var val = $(this).prop('checked'); + bt.send( + 'set_php_session_path', + 'config/set_php_session_path', + { + id: web.id, + act: val ? 1 : 0, + }, + function (rdata) { + bt.msg(rdata); + } + ); + setTimeout(function () { + get_session_status(); + }, 500); + }); + }); + }); + }, + set_wp_config: function (web) { + var loadup = bt.load('Getting Wordpress information, please wait...'); + bt.send('is_update', 'site/is_update', { s_id: web.id }, function (rdata) { + loadup.close(); + var loadin = bt.load('Getting wordpress account information, please wait...'); + bt.send('get_wp_username', 'site/get_wp_username', { s_id: web.id }, function (wlist) { + loadin.close(); + var robj = $('#webedit-con'); + + if (wlist.status === false) { + wlist.time = 0; + wlist.closeBtn = 2; + bt.msg(wlist); + var data = { + items: [ + { + title: 'Database name', + name: 'database', + value: '', + type: 'input', + width: '250px', + placeholder: 'Please enter the database name', + }, + { + text: 'Set', + name: 'btn_change_database', + type: 'button', + callback: function (pdata) { + if (pdata.database == '') { + return layer.msg('The database name cannot be empty', { icon: 2 }); + } + var param = { + site_id: web.id, + db_name: pdata.database, + }; + var load = bt.load('Setting Database name, please wait...'); + bt.send('reset_wp_db', 'site/reset_wp_db', param, function (res) { + load.close(); + bt.msg(res); + if (res.status) { + setTimeout(function () { + robj.html(''); + site.edit.set_wp_config(web); + }, 1500); + } + }); + }, + }, + ], + }; + var _form_data = bt.render_form_line(data); + var _html = $(_form_data.html); + _html.append(bt.render_help(['Please enter the database name for this wordpress website'])); + robj.append(_html); + bt.render_clicks(_form_data.clicks); + return; + } + + var _html = $('
                                '), + user_array = [], + clicks = []; + + $.each(wlist.msg, function (index, item) { + user_array.push({ title: item, value: item }); + }); + var datas = [ + { + title: 'WP Version', + items: [ + { + name: 'wp_version', + type: 'html', + html: rdata['msg']['update'] + ? 'The current version is: ' + + rdata['msg']['local_v'] + + '' + : 'The latest version', + }, + ], + }, + { + title: 'Cache', + items: [ + { + type: 'checkbox', + name: 'cache_switch', + text: ' Open cache', + value: web.cache_status, + callback: function (sdata) { + var loads = bt.load((sdata.cache_switch ? 'Turning on' : 'Turining off') + ' [ ' + web.name + ' ] cache, please wait...'); + bt.send('set_fastcgi_cache', 'site/set_fastcgi_cache', { version: web.php_version, sitename: web.name, act: sdata.cache_switch ? 'enable' : 'disable' }, function (res) { + loads.close(); + bt.msg(res); + if (res.status) { + site.php_table_view(); + web.cache_status = sdata.cache_switch; + } + }); + }, + }, + { + name: 'remove_cache', + text: 'Purge all cache', + type: 'button', + callback: function (sdata) { + var loadC = bt.load('Clearing all caches, please wait...'); + bt.send('purge_all_cache', 'site/purge_all_cache', { s_id: web.id }, function (res) { + loadC.close(); + bt.msg(res); + }); + }, + }, + ], + }, + { + title: 'Reset password', + items: [ + { + name: 'user', + type: 'select', + items: user_array, + width: '200px', + }, + { + title: '', + name: 'new_pass', + placeholder: 'Please enter a new password', + width: '200px', + }, + { + name: 'submit_pw', + text: 'Save password', + type: 'button', + callback: function (sdata) { + var loads = bt.load('Resetting password, please wait...'); + bt.send('reset_wp_password', 'site/reset_wp_password', { s_id: web.id, user: sdata.user, new_pass: sdata.new_pass }, function (res) { + loads.close(); + bt.msg(res); + }); + }, + }, + ], + }, + ]; + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + _html.append(_form_data.html); + clicks = clicks.concat(_form_data.clicks); + } + _html.find('input[type="checkbox"]').parent().addClass('label-input-group'); + _html.find('button[name="submit_pw"]').css('margin', '15px 0'); + robj.append(_html); + bt.render_clicks(clicks); + + //wp版本更新 + $('.update_wp_version').click(function () { + var load_wp = bt.load('Updating Wordpress version, please wait...'); + bt.send('update_wp', 'site/update_wp', { s_id: web.id, version: rdata['msg']['online_v'] }, function (res) { + load_wp.close(); + bt.msg(res); + if (res.status) $('.bt-w-menu.site-menu p.bgw').click(); + }); + }); + }); + }); + }, + templet_301: function (sitename, id, types, obj) { + if (types) { + obj = { + redirectname: new Date().valueOf(), + tourl: 'http://', + redirectdomain: [], + redirectpath: '', + redirecttype: '', + type: 1, + domainorpath: 'domain', + holdpath: 1, + }; + } + var helps = [lan.site.redirect_tips1, lan.site.redirect_tips2, lan.site.redirect_tips3, lan.site.redirect_tips4, lan.site.redirect_tips5, lan.site.redirect_tips6]; + bt.site.get_domains(id, function (rdata) { + var domain_html = ''; + for (var i = 0; i < rdata.length; i++) { + domain_html += ''; + } + var form_redirect = bt.open({ + type: 1, + skin: 'demo-class', + area: '650px', + title: types ? lan.site.create_redirect : lan.site.modify_redirect + '[' + obj.redirectname + ']', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "" + + "
                                " + + "" + + lan.site.open_redirect + + '' + + "
                                " + + "" + + "
                                " + + "" + + lan.site.reserve_url + + '' + + "" + + '
                                ' + + '
                                ' + + '
                                ' + + "' + + "
                                " + + "" + + lan.site.redirect_type + + '' + + "
                                " + + "' + + "" + + lan.site.redirect_mode + + '' + + "
                                ' + + '
                                ' + + "
                                " + + "" + + lan.site.redirect_domain + + '' + + "
                                " + + "' + + '
                                ' + + "" + + lan.site.target_url + + '' + + "
                                " + + "" + + '
                                ' + + '
                                ' + + "
                                " + + "" + + lan.site.redirect_path + + '' + + "
                                " + + "" + + "" + + lan.site.target_url + + '' + + "" + + '
                                ' + + '
                                ' + + "
                                  " + + bt.render_help(helps) + + '
                                ' + + "
                                ' + + '', + }); + setTimeout(function () { + $('.selectpicker').selectpicker({ + noneSelectedText: lan.site.choose_domain, + selectAllText: lan.site.choose_all, + deselectAllText: lan.site.cancel_all, + }); + $('.selectpicker').selectpicker('val', obj.redirectdomain); + $('#form_redirect').parent().css('overflow', 'inherit'); + $('[name="domainorpath"]').change(function () { + if ($(this).val() == 'path') { + $('.redirectpath').show(); + $('.redirectdomain').hide(); + $('.selectpicker').selectpicker('val', []); + } else { + $('.redirectpath').hide(); + $('.redirectdomain').show(); + $('[name="redirectpath"]').val(''); + } + }); + $('.btn-colse-prosy').click(function () { + form_redirect.close(); + }); + $('.btn-submit-redirect').click(function () { + var type = $('[name="type"]').prop('checked') ? 1 : 0; + var holdpath = $('[name="holdpath"]').prop('checked') ? 1 : 0; + var redirectname = $('[name="redirectname"]').val(); + var redirecttype = $('[name="redirecttype"]').val(); + var domainorpath = $('[name="domainorpath"]').val(); + var redirectpath = $('[name="redirectpath"]').val(); + var redirectdomain = JSON.stringify($('.selectpicker').val() || []); + var tourl = $(domainorpath == 'path' ? '[name="tourl1"]' : '[name="tourl"]').val(); + if (!types) { + bt.site.modify_redirect( + { + type: type, + sitename: sitename, + holdpath: holdpath, + redirectname: redirectname, + redirecttype: redirecttype, + domainorpath: domainorpath, + redirectpath: redirectpath, + redirectdomain: redirectdomain, + tourl: tourl, + }, + function (rdata) { + if (rdata.status) { + form_redirect.close(); + site.reload(11); + } + bt.msg(rdata); + } + ); + } else { + bt.site.create_redirect( + { + type: type, + sitename: sitename, + holdpath: holdpath, + redirectname: redirectname, + redirecttype: redirecttype, + domainorpath: domainorpath, + redirectpath: redirectpath, + redirectdomain: redirectdomain, + tourl: tourl, + }, + function (rdata) { + if (rdata.status) { + form_redirect.close(); + site.reload(11); + } + bt.msg(rdata); + } + ); + } + }); + }, 100); + }); + }, + template_Dir: function (id, type, obj) { + if (type) { + obj = { name: '', sitedir: '', username: '', password: '' }; + } else { + obj = { name: obj.name, sitedir: obj.site_dir, username: '', password: '' }; + } + var form_directory = bt.open({ + type: 1, + skin: 'demo-class', + area: '475px', + title: type ? 'Add limit access' : 'Edit limit access', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "
                                " + + "
                                " + + "" + + lan.bt.task_name + + '' + + "
                                " + + '
                                ' + + "
                                " + + "Path" + + "
                                " + + '
                                ' + + "
                                " + + "" + + lan.bt.panel_user + + '' + + "
                                " + + '
                                ' + + "
                                " + + "" + + lan.bt.panel_pass + + '' + + "
                                " + + '
                                ' + + "
                                  " + + '
                                • After the path is protected, you need to enter the account password to access it.
                                • ' + + '
                                • For example, if I set the protection directory /test/ , then I need to enter the account password to access http://aaa.com/test/
                                • ' + + '
                                ' + + "
                                ', + }); + $('.btn-colse-guard').click(function () { + form_directory.close(); + }); + $('.btn-submit-guard').click(function () { + var guardData = {}; + guardData['id'] = id; + guardData['name'] = $('input[name="dir_name"]').val(); + guardData['site_dir'] = $('input[name="dir_sitedir"]').val(); + guardData['username'] = $('input[name="dir_username"]').val(); + guardData['password'] = $('input[name="dir_password"]').val(); + if (type) { + bt.site.create_dir_guard(guardData, function (rdata) { + if (rdata.status) { + form_directory.close(); + site.reload(); + } + bt.msg(rdata); + }); + } else { + bt.site.edit_dir_account(guardData, function (rdata) { + if (rdata.status) { + form_directory.close(); + site.reload(); + } + bt.msg(rdata); + }); + } + }); + setTimeout(function () { + if (!type) { + $('input[name="dir_name"]').attr('disabled', 'disabled'); + $('input[name="dir_sitedir"]').attr('disabled', 'disabled'); + } + }, 500); + }, + template_php: function (website, obj) { + var _type = 'add', + _name = '', + _bggrey = ''; + if (obj == undefined) { + obj = { name: '', suffix: 'php|jsp', dir: '' }; + } else { + obj = { name: obj.name, suffix: obj.suffix, dir: obj.dir }; + _type = 'edit'; + _name = ' readonly'; + _bggrey = 'background: #eee;'; + } + var form_directory = bt.open({ + type: 1, + area: '440px', + title: 'Deny access', + closeBtn: 2, + btn: ['Save', 'Cancel'], + content: + "
                                " + + "
                                " + + "Name" + + "
                                " + + '
                                ' + + "
                                " + + "Suffix" + + "
                                " + + '
                                ' + + "
                                " + + "Path" + + "
                                " + + '
                                ' + + "
                                  " + + '
                                • Name:The rule name.
                                • ' + + "
                                • Suffix: Indicates the suffix that is not allowed to access, if there are more than one, separate with'|'
                                • " + + '
                                • Path: Quote rules in this directory. e.g: /a/
                                • ' + + '
                                • For Example, if you want to deny http://test.com/a/index.php
                                • ' + + '
                                • Please fill in [ /a/ ]' + + '
                                ', + yes: function () { + var dent_data = $('.php_deny').serializeObject(); + dent_data.act = _type; + dent_data.website = website; + var loading = bt.load(); + bt.site.edit_php_deny(dent_data, function (rdata) { + loading.close(); + if (rdata.status) { + form_directory.close(); + site.reload(); + $('#set_dirguard .tab-nav span:eq(1)').click(); + } + bt.msg(rdata); + }); + }, + }); + }, + del_php_deny: function (website, deny_name, callback) { + layer.confirm( + 'Are you sure to delete [ ' + deny_name + ' ] this deny?', + { + icon: 0, + closeBtn: 2, + title: 'Delete deny', + }, + function (index) { + bt.site.del_php_deny({ website: website, deny_name: deny_name }, function (rdata) { + layer.close(index); + if (callback) callback(rdata); + }); + } + ); + }, + set_301_old: function (web) { + bt.site.get_domains(web.id, function (rdata) { + var domains = [{ title: lan.site.site, value: 'all' }]; + for (var i = 0; i < rdata.length; i++) domains.push({ title: rdata[i].name, value: rdata[i].name }); + + bt.site.get_site_301(web.name, function (pdata) { + var _val = pdata.src == '' ? 'all' : pdata.src; + var datas = [ + { + title: lan.site.access_domain, + width: '360px', + name: 'domains', + value: _val, + disabled: pdata.status, + type: 'select', + items: domains, + }, + { title: lan.site.target_url, width: '360px', name: 'toUrl', value: pdata.url }, + { + title: ' ', + text: lan.site.enable_301, + value: pdata.status, + name: 'status', + class: 'label-input-group', + type: 'checkbox', + callback: function (sdata) { + bt.site.set_site_301(web.name, sdata.domains, sdata.toUrl, sdata.status ? '1' : '0', function (ret) { + if (ret.status) site.reload(10); + bt.msg(ret); + }); + }, + }, + ]; + var robj = $('#webedit-con'); + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + robj.append(bt.render_help([lan.site.to301_help_1, lan.site.to301_help_2])); + }); + }); + }, + set_301: function (web) { + $('#webedit-con').html('
                                '); + bt_tools.table({ + el: '#redirect_list', + url: '/site?action=GetRedirectList', + param: { sitename: web.name }, + dataFilter: function (res) { + return { data: res }; + }, + column: [ + { type: 'checkbox', width: 20 }, + { + fid: 'sitename', + title: lan.site.redirect_type, + type: 'text', + template: function (row) { + if (row.domainorpath == 'path') { + conter = row.redirectpath; + } else { + conter = row.redirectdomain ? row.redirectdomain.join('、') : lan.site.empty; + } + return '' + conter + ''; + }, + }, + { fid: 'redirecttype', title: lan.site.redirect_mode, type: 'text' }, + { + fid: 'holdpath', + title: lan.site.reserve_url, + config: { + icon: false, + list: [ + [1, lan.site.turn_on, 'bt_success'], + [0, lan.site.turn_off, 'bt_danger'], + ], + }, + type: 'status', + event: function (row, index, ev, key, that) { + row.holdpath = row.holdpath == 0 ? 1 : 0; + row.redirectdomain = JSON.stringify(row['redirectdomain']); + bt.site.modify_redirect(row, function (res) { + row.redirectdomain = JSON.parse(row['redirectdomain']); + that.$modify_row_data({ holdpath: row.holdpath }); + bt.msg(res); + }); + }, + }, + { + fid: 'type', + title: lan.site.status, + config: { + icon: true, + list: [ + [1, lan.site.running_text, 'bt_success', 'glyphicon-play'], + [0, lan.site.already_stop, 'bt_danger', 'glyphicon-pause'], + ], + }, + type: 'status', + event: function (row, index, ev, key, that) { + row.type = row.type == 0 ? 1 : 0; + row.redirectdomain = JSON.stringify(row['redirectdomain']); + bt.site.modify_redirect(row, function (res) { + row.redirectdomain = JSON.parse(row['redirectdomain']); + that.$modify_row_data({ status: row.type }); + bt.msg(res); + }); + }, + }, + { + title: lan.site.operate, + width: 129, + type: 'group', + align: 'right', + group: [ + { + title: 'Conf', + event: function (row, index, ev, key, that) { + bt.site.get_redirect_config( + { + sitename: web.name, + redirectname: row.redirectname, + webserver: bt.get_cookie('serverType'), + }, + function (rdata) { + if (typeof rdata == 'object' && rdata.constructor == Array) { + if (!rdata[0].status) bt.msg(rdata); + } else { + if (!rdata.status) bt.msg(rdata); + } + var datas = [ + { + items: [ + { + name: 'redirect_configs', + type: 'textarea', + value: rdata[0].data, + widht: '340px', + height: '200px', + }, + ], + }, + { + name: 'btn_config_submit', + text: 'Save', + type: 'button', + callback: function (ddata) { + bt.site.save_redirect_config( + { + path: rdata[1], + data: editor.getValue(), + encoding: rdata[0].encoding, + }, + function (ret) { + if (ret.status) { + site.reload(11); + redirect_config.close(); + } + bt.msg(ret); + } + ); + }, + }, + ]; + redirect_config = bt.open({ + type: 1, + area: ['550px', '550px'], + title: 'Edit profile [' + row.redirectname + ']', + closeBtn: 2, + shift: 0, + content: "
                                ", + }); + var robj = $('#redirect_config_con'); + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + robj.append(bt.render_help(['This is the configuration file of the load balancing. Not modify if you do not understand the configuration rules.'])); + $('textarea.redirect_configs').attr('id', 'configBody'); + var editor = CodeMirror.fromTextArea(document.getElementById('configBody'), { + extraKeys: { 'Ctrl-Space': 'autocomplete' }, + lineNumbers: true, + matchBrackets: true, + }); + $('.CodeMirror-scroll').css({ height: '350px', margin: 0, padding: 0 }); + setTimeout(function () { + editor.refresh(); + }, 250); + } + ); + }, + }, + { + title: lan.site.edit, + event: function (row, index, ev, key, that) { + site.edit.templet_301(web.name, web.id, false, row); + }, + }, + { + title: lan.site.del, + event: function (row, index, ev, key, that) { + bt.site.remove_redirect(web.name, row.redirectname, function (rdata) { + if (rdata.status) that.$delete_table_row(index); + }); + }, + }, + ], + }, + ], + tootls: [ + { + //按钮组 + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: 'Add redirection', + active: true, + event: function (ev) { + site.edit.templet_301(web.name, web.id, true); + }, + }, + ], + }, + { + //批量操作 + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' delete', + url: '/site?action=del_redirect_multiple', + param: { site_id: web.id }, + paramId: 'redirectname', + paramName: 'redirectnames', + theadName: 'Name', + confirmVerify: false, // 是否提示验证方式 + }, + }, + ], + }); + }, + templet_proxy: function (sitename, type, obj) { + if (type) { + obj = { + type: 1, + cache: 0, + proxyname: '', + proxydir: '/', + proxysite: 'http://', + cachetime: 1, + todomain: '$host', + subfilter: [{ sub1: '', sub2: '' }], + }; + } + var sub_conter = ''; + for (var i = 0; i < obj.subfilter.length; i++) { + if (i == 0 || obj.subfilter[i]['sub1'] != '') { + sub_conter += + "
                                " + + "" + + "" + + "Del" + + '
                                '; + } + if (i == 2) $('.add-replace-prosy').attr('disabled', 'disabled'); + } + var helps = [lan.site.proxy_tips1, lan.site.proxy_tips2, lan.site.proxy_tips3, lan.site.proxy_tips4]; + var form_proxy = bt.open({ + type: 1, + skin: 'demo-class', + area: '650px', + title: type ? lan.site.create_proxy : lan.site.modify_proxy + '[' + obj.proxyname + ']', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + "
                                " + + "
                                " + + "" + + lan.site.open_proxy + + '' + + "
                                " + + "" + + "
                                " + + "" + + lan.site.proxy_cache + + '' + + "" + + '
                                ' + + "
                                " + + "" + + lan.site.proxy_adv + + '' + + "" + + '
                                ' + + '
                                ' + + '
                                ' + + "
                                " + + "" + + lan.site.proxy_name + + '' + + "
                                " + + '
                                ' + + "
                                " + + "" + + lan.site.cache_time + + '' + + "
                                " + + lan.site.minute + + '
                                ' + + '
                                ' + + "
                                " + + "" + + lan.site.proxy_dir + + '' + + "
                                " + + '
                                ' + + '
                                ' + + "
                                " + + "" + + lan.site.target_url + + '' + + "
                                " + + "" + + '
                                ' + + '
                                ' + + "
                                " + + "" + + lan.site.proxy_domain + + '' + + "
                                " + + "" + + '
                                ' + + '
                                ' + + "
                                " + + "" + + lan.site.con_rep + + '' + + "
                                " + + sub_conter + + '
                                ' + + '
                                ' + + "
                                " + + "
                                " + + "' + + '
                                ' + + '
                                ' + + "
                                  " + + bt.render_help(helps) + + "
                                  ' + + '', + }); + bt.set_cookie('form_proxy', form_proxy); + $('.add-replace-prosy').click(function () { + var length = $('.replace_conter .sub-groud').length; + if (length == 2) $(this).attr('disabled', 'disabled'); + var conter = + "
                                  " + + "" + + "" + + "" + + lan.site.del + + '' + + '
                                  '; + $('.replace_conter .info-r').append(conter); + }); + $('[name="proxysite"]').keyup(function () { + var val = $(this).val(), + ip_reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; + val = val.replace(/^http[s]?:\/\//, ''); + // val = val.replace(/:([0-9]*)$/, ''); + val = val.replace(/(:|\?|\/|\\)(.*)$/, ''); + if (ip_reg.test(val)) { + $("[name='todomain']").val('$host'); + } else { + $("[name='todomain']").val(val); + } + }); + $('#openAdvanced').click(function () { + if ($(this).prop('checked')) { + $('.advanced').show(); + } else { + $('.advanced').hide(); + } + }); + $('#openNginx').click(function () { + if ($(this).prop('checked')) { + $('.cachetime').show(); + } else { + $('.cachetime').hide(); + } + }); + $('.btn-colse-prosy').click(function () { + form_proxy.close(); + }); + $('.replace_conter').on('click', '.proxy_del_sub', function () { + $(this).parent().remove(); + $('.add-replace-prosy').removeAttr('disabled'); + }); + $('.btn-submit-prosy').click(function () { + var form_proxy_data = {}; + $.each($('#form_proxy').serializeArray(), function () { + if (form_proxy_data[this.name]) { + if (!form_proxy_data[this.name].push) { + form_proxy_data[this.name] = [form_proxy_data[this.name]]; + } + form_proxy_data[this.name].push(this.value || ''); + } else { + form_proxy_data[this.name] = this.value || ''; + } + }); + form_proxy_data['type'] = form_proxy_data['type'] == undefined ? 0 : 1; + form_proxy_data['cache'] = form_proxy_data['cache'] == undefined ? 0 : 1; + form_proxy_data['advanced'] = form_proxy_data['advanced'] == undefined ? 0 : 1; + form_proxy_data['sitename'] = sitename; + form_proxy_data['subfilter'] = JSON.stringify([ + { sub1: form_proxy_data['rep1'] || '', sub2: form_proxy_data['rep2'] || '' }, + { sub1: form_proxy_data['rep3'] || '', sub2: form_proxy_data['rep4'] || '' }, + { sub1: form_proxy_data['rep5'] || '', sub2: form_proxy_data['rep6'] || '' }, + ]); + for (var i in form_proxy_data) { + if (i.indexOf('rep') != -1) { + delete form_proxy_data[i]; + } + } + if (type) { + bt.site.create_proxy(form_proxy_data, function (rdata) { + if (rdata.status) { + form_proxy.close(); + site.reload(12); + } + bt.msg(rdata); + }); + } else { + bt.site.modify_proxy(form_proxy_data, function (rdata) { + if (rdata.status) { + form_proxy.close(); + site.reload(12); + } + bt.msg(rdata); + }); + } + }); + }, + set_proxy: function (web) { + var limit_len = bt.get_cookie('serverType') == 'nginx' ? 'proxy_list_limit_4' : 'proxy_list_limit_3'; + $('#webedit-con').html('
                                  '); + String.prototype.myReplace = function (f, e) { + //吧f替换成e + var reg = new RegExp(f, 'g'); //创建正则RegExp对象 + return this.replace(reg, e); + }; + bt_tools.table({ + el: '#proxy_list', + url: '/site?action=GetProxyList', + param: { sitename: web.name }, + dataFilter: function (res) { + return { data: res }; + }, + column: [ + { type: 'checkbox', width: 20 }, + { + fid: 'proxyname', + title: lan.site.name, + template: function (row, index) { + return '' + row.proxyname + ''; + }, + }, + { + fid: 'proxydir', + title: lan.site.proxy_dir, + template: function (row, index) { + return '' + row.proxydir + ''; + }, + }, + { fid: 'proxysite', title: lan.site.target_url, type: 'link', href: true }, + bt.get_cookie('serverType') == 'nginx' + ? { + fid: 'cache', + title: lan.site.cache, + config: { + icon: false, + list: [ + [1, lan.site.already_open, 'bt_success'], + [0, lan.site.already_close, 'bt_danger'], + ], + }, + type: 'status', + event: function (row, index, ev, key, that) { + row['cache'] = !row['cache'] ? 1 : 0; + row['subfilter'] = JSON.stringify(row['subfilter']); + bt.site.modify_proxy(row, function (rdata) { + row['subfilter'] = JSON.parse(row['subfilter']); + if (rdata.status) that.$modify_row_data({ cache: row['cache'] }); + bt.msg(rdata); + }); + }, + } + : {}, + { + fid: 'type', + title: lan.site.status, + config: { + icon: true, + list: [ + [1, lan.site.running_text, 'bt_success', 'glyphicon-play'], + [0, lan.site.already_stop, 'bt_danger', 'glyphicon-pause'], + ], + }, + type: 'status', + event: function (row, index, ev, key, that) { + row['type'] = !row['type'] ? 1 : 0; + row['subfilter'] = JSON.stringify(row['subfilter']); + bt.site.modify_proxy(row, function (rdata) { + row['subfilter'] = JSON.parse(row['subfilter']); + if (rdata.status) that.$modify_row_data({ type: row['type'] }); + bt.msg(rdata); + }); + }, + }, + { + title: lan.site.operate, + width: 115, + type: 'group', + align: 'right', + group: [ + { + title: 'Conf', + event: function (row, index, ev, key, that) { + bt.site.get_proxy_config( + { + sitename: web.name, + proxyname: row.proxyname, + webserver: bt.get_cookie('serverType'), + }, + function (rdata) { + if (typeof rdata == 'object' && rdata.constructor == Array) { + if (!rdata[0].status) bt.msg(rdata); + } else { + if (!rdata.status) bt.msg(rdata); + } + var datas = [ + { + items: [ + { + name: 'proxy_configs', + type: 'textarea', + value: rdata[0].data, + widht: '340px', + height: '200px', + }, + ], + }, + { + name: 'btn_config_submit', + text: 'Save', + type: 'button', + callback: function (ddata) { + bt.site.save_proxy_config( + { + path: rdata[1], + data: editor.getValue(), + encoding: rdata[0].encoding, + }, + function (ret) { + if (ret.status) { + site.reload(12); + proxy_config.close(); + } + bt.msg(ret); + } + ); + }, + }, + ]; + proxy_config = bt.open({ + type: 1, + area: ['550px', '550px'], + title: 'Edit profile [' + row.proxyname + ']', + closeBtn: 2, + shift: 0, + content: "
                                  ", + }); + var robj = $('#proxy_config_con'); + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + robj.append(bt.render_help(['This is the configuration file of the load balancing. Not modify if you do not understand the configuration rules.'])); + $('textarea.proxy_configs').attr('id', 'configBody'); + var editor = CodeMirror.fromTextArea(document.getElementById('configBody'), { + extraKeys: { 'Ctrl-Space': 'autocomplete' }, + lineNumbers: true, + matchBrackets: true, + }); + $('.CodeMirror-scroll').css({ height: '350px', margin: 0, padding: 0 }); + setTimeout(function () { + editor.refresh(); + }, 250); + } + ); + }, + }, + { + title: 'Edit', + event: function (row, index, ev, key, that) { + site.edit.templet_proxy(web.name, false, row); + }, + }, + { + title: 'Del', + event: function (row, index, ev, key, that) { + bt.site.remove_proxy(web.name, row.proxyname, function (rdata) { + if (rdata.status) that.$delete_table_row(index); + }); + }, + }, + ], + }, + ], + tootls: [ + { + //按钮组 + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: 'Add reverse proxy', + active: true, + event: function (ev) { + site.edit.templet_proxy(web.name, true); + }, + }, + ], + }, + { + //批量操作 + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' delete', + url: '/site?action=del_proxy_multiple', + param: { site_id: web.id }, + paramId: 'proxyname', + paramName: 'proxynames', + theadName: 'Name', + confirmVerify: false, // 是否提示验证方式 + }, + }, + ], + }); + }, + set_security: function (web) { + bt.site.get_site_security(web.id, web.name, function (rdata) { + var robj = $('#webedit-con'); + var datas = [ + { + title: lan.site.url_suffix, + name: 'sec_fix', + value: rdata.fix, + disabled: rdata.status, + width: '300px', + }, + { + title: lan.site.access_domain1, + items: [ + { + text: lan.site.start_anti_leech, + name: 'sec_domains', + width: '300px', + height: '210px', + disabled: rdata.status, + value: rdata.domains.replace(/,/g, '\n'), + type: 'textarea', + }, + ], + }, + { + title: 'Response', + name: 'return_rule', + value: rdata.return_rule, + disabled: rdata.status, + width: '300px', + }, + { + title: ' ', + class: 'label-input-group', + items: [ + { + text: lan.site.start_anti_leech, + name: 'status', + value: rdata.status, + type: 'checkbox', + callback: function (sdata) { + bt.site.set_site_security(web.id, web.name, sdata.sec_fix, sdata.sec_domains.split('\n').join(','), sdata.status, sdata.return_rule, function (ret) { + if (ret.status) site.reload(13); + bt.msg(ret); + }); + }, + }, + { + text: 'Allow empty HTTP_REFERER requests', + name: 'none', + value: rdata.none, + type: 'checkbox', + callback: function (sdata) { + bt.site.set_site_security(web.id, web.name, sdata.sec_fix, sdata.sec_domains.split('\n').join(','), '1', sdata.return_rule, function (ret) { + if (ret.status) site.reload(13); + bt.msg(ret); + }); + }, + }, + ], + }, + ]; + + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + robj.find('#none').css('margin-top', '10px'); + $('#none').before('
                                  '); + var helps = [lan.site.access_empty_ref_default, lan.site.multi_url, lan.site.trigger_return_404]; + robj.append(bt.render_help(helps)); + }); + }, + set_tomact: function (web) { + bt.site.get_site_phpversion(web.name, function (rdata) { + var robj = $('#webedit-con'); + if (!rdata.tomcatversion) { + robj.html('' + lan.site.tomcat_err_msg1 + ''); + layer.msg(lan.site.tomcat_err_msg, { icon: 2 }); + return; + } + var data = { + class: 'label-input-group', + items: [ + { + text: lan.site.enable_tomcat, + name: 'tomcat', + value: rdata.tomcat == -1 ? false : true, + type: 'checkbox', + callback: function (sdata) { + bt.site.set_tomcat(web.name, function (ret) { + if (ret.status) site.reload(9); + bt.msg(ret); + }); + }, + }, + ], + }; + var _form_data = bt.render_form_line(data); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + var helps = [lan.site.tomcat_help1 + ' ' + rdata.tomcatversion + ',' + lan.site.tomcat_help2, lan.site.tomcat_help3, lan.site.tomcat_help4, lan.site.tomcat_help5]; + robj.append(bt.render_help(helps)); + }); + }, + get_site_logs: function (web) { + $('#webedit-con').append('
                                  '); + var serverType = bt.get_cookie('serverType'), + shell = 'tail -n 100 -f /www/wwwlogs/' + web.name; + var _tab = [ + { + title: 'Access log', + on: true, + callback: function (robj) { + bt.site.get_site_logs(web.name, function (rdata) { + var _logs_info = $('
                                  ').text(rdata.msg); + var logs = { class: 'bt-logs', items: [{ name: 'site_logs', height: '560px', value: _logs_info.html(), width: '100%', type: 'textarea' }] }, + _form_data = bt.render_form_line(logs); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + $('textarea[name="site_logs"]').attr('readonly', true); + $('textarea[name="site_logs"]').scrollTop(100000000000); + }); + }, + }, + { + title: 'Error log', + callback: function (robj) { + bt.site.get_site_error_logs(web.name, function (rdata) { + var _logs_info = $('
                                  ').text(rdata.msg); + var logs = { class: 'bt-logs', items: [{ name: 'site_logs', height: '560px', value: _logs_info.html(), width: '100%', type: 'textarea' }] }, + _form_data = bt.render_form_line(logs); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + $('textarea[name="site_logs"]').attr('readonly', true); + $('textarea[name="site_logs"]').scrollTop(100000000000); + }); + }, + }, + { + title: 'Log Security Analysis', + callback: function (robj) { + var _serverType = bt.get_cookie('serverType'), + pathFile = '', + progress = '', //扫描进度 + loadT = bt.load('Getting log analytics data, please wait...'); + + switch (_serverType) { + case 'nginx': + pathFile = web.name + '.log'; + break; + case 'apache': + pathFile = web.name + '-access_log'; + break; + default: + pathFile = web.name + '_ols.access_log'; + break; + } + $.post('/ajax?action=get_result&path=/www/wwwlogs/' + pathFile, function (rdata) { + loadT.close(); + //1.扫描按钮 + var analyes_log_btn = ''; + + //2.功能介绍 + var analyse_help = + '
                                    \ +
                                  • Log analysis: Scan the logs(/www/wwwroot/.log) for requests with attack (types include:xss,sql,san,php)
                                  • \ +
                                  • Analyzed log data contains intercepted requests
                                  • \ +
                                  • By default, the last scan data is displayed (if not, please click log scan)
                                  • \ +
                                  • If the log file is too large, scanning may take a long time, please be patient
                                  • \ +
                                  • aaPanel WAF can effectively block such attacks
                                  • \ +
                                  '; + + robj.append(analyes_log_btn + '
                                  ' + analyse_help); + render_analyse_list(rdata); + + //事件 + $(robj) + .find('.analyes_log') + .click(function () { + bt.confirm( + { + title: 'Scan website logs', + msg: + 'It is recommended to perform security analysis when the server load is low. This time, the [' + + web.name + + '.log] file will be scanned. It may take a long time. Do you want to continue?', + }, + function (index) { + layer.close(index); + progress = layer.open({ + type: 1, + closeBtn: 2, + title: false, + shade: 0, + area: '400px', + content: + '
                                  Scanning, scanning progress...
                                  \ +
                                  \ +
                                  0%
                                  \ +
                                  \ +
                                  ', + success: function () { + // 开启扫描并且持续获取进度 + $.post('/ajax?action=log_analysis&path=/www/wwwlogs/' + pathFile, function (rdata) { + if (rdata.status) { + detect_progress(); + } else { + layer.close(progress); + layer.msg(rdata.msg, { icon: 2, time: 0, shade: 0.3, shadeClose: true }); + } + }); + }, + }); + } + ); + }); + }); + // 渲染分析日志列表 + function render_analyse_list(rdata) { + var analyse_list = + '
                                  \ + \ + '; + if (rdata.is_status) { + //检测是否有扫描数据 + analyse_list += + '\ + \ + \ + \ + \ + \ + \ + \ + \ + '; + } else { + analyse_list += ''; + } + analyse_list += '
                                  DateTimeXSSSQLSacnPHPIP(top100)URL(top100)
                                  ' + + rdata.start_time + + '' + + rdata.time.substring(0, 4) + + ' Sec 0 ? 'style="color:red"' : '') + + ' name="xss">' + + rdata.xss + + ' 0 ? 'style="color:red"' : '') + + ' name="sql">' + + rdata.sql + + ' 0 ? 'style="color:red"' : '') + + ' name="san">' + + rdata.san + + ' 0 ? 'style="color:red"' : '') + + ' name="php">' + + rdata.php + + '' + + rdata.ip + + '' + + rdata.url + + '
                                  no scan data
                                  '; + $('.analyse_log_table').html(analyse_list); + $('.onChangeLogDatail').css('cursor', 'pointer').attr('title', 'Details'); + //查看详情 + $('.onChangeLogDatail').on('click', function () { + get_analysis_data_datail($(this).attr('name')); + }); + } + // 扫描进度 + function detect_progress() { + $.post('/ajax?action=speed_log&path=/www/wwwlogs/' + pathFile, function (res) { + var pro = res.msg; + if (pro !== 100) { + if (pro > 100) pro = 100; + if (pro !== NaN) { + $('.pro_style .progress-bar') + .css('width', pro + '%') + .html(pro + '%'); + } + setTimeout(function () { + detect_progress(); + }, 1000); + } else { + layer.msg('Scan complete', { icon: 1, timeout: 4000 }); + layer.close(progress); + get_analysis_data(); + } + }); + } + // 获取扫描结果 + function get_analysis_data() { + var loadTGA = bt.load('Getting log analytics data, please wait...'); + $.post('/ajax?action=get_result&path=/www/wwwlogs/' + pathFile, function (rdata) { + loadTGA.close(); + render_analyse_list(rdata, true); + }); + } + // 获取扫描结果详情日志 + function get_analysis_data_datail(name) { + layer.open({ + type: 1, + closeBtn: 2, + shadeClose: false, + title: '[ ' + name + ' ] log details', + area: '650px', + content: '
                                  ',
                                  +								success() {
                                  +									var loadTGD = bt.load('Getting log details data, please wait...');
                                  +									$.post('/ajax?action=get_detailed&path=/www/wwwlogs/' + pathFile + '&type=' + name + '', function (logs) {
                                  +										loadTGD.close();
                                  +										$('#analysis_pre').text((name == 'ip' || name == 'url' ? ' [Access Times] [' + name + '] \n' : '') + logs);
                                  +									});
                                  +								},
                                  +							});
                                  +						}
                                  +					},
                                  +				},
                                  +			];
                                  +			bt.render_tab('tabLogs', _tab);
                                  +			$('#tabLogs span:eq(0)').click();
                                  +		},
                                  +	},
                                  +	create_let: function (ddata, callback) {
                                  +		bt.site.create_let(ddata, function (ret) {
                                  +			if (ret.status) {
                                  +				if (callback) {
                                  +					callback(ret);
                                  +				} else {
                                  +					site.ssl.reload(1);
                                  +					bt.msg(ret);
                                  +					return;
                                  +				}
                                  +			} else {
                                  +				if (ret.msg) {
                                  +					if (typeof ret.msg == 'string') {
                                  +						ret.msg = [ret.msg, ''];
                                  +					}
                                  +				}
                                  +				if (!ret.out) {
                                  +					if (callback) {
                                  +						callback(ret);
                                  +						return;
                                  +					}
                                  +					bt.msg(ret);
                                  +					return;
                                  +				}
                                  +				var data = '

                                  ' + ret.msg + '


                                  '; + if (ret.err[0].length > 10) data += '

                                  ' + ret.err[0].replace(/\n/g, '
                                  ') + '

                                  '; + if (ret.err[1].length > 10) data += '

                                  ' + ret.err[1].replace(/\n/g, '
                                  ') + '

                                  '; + + layer.msg(data, { icon: 2, area: '500px', time: 0, shade: 0.3, shadeClose: true }); + } + }); + }, + reload: function (index) { + if (index == undefined) index = 0; + + var _sel = $('.site-menu p.bgw'); + if (_sel.length == 0) _sel = $('.site-menu p:eq(0)'); + _sel.trigger('click'); + }, + plugin_firewall: function (callback) { + var typename = bt.get_cookie('serverType'); + var name = 'btwaf_httpd'; + if (typename == 'nginx') name = 'btwaf'; + + bt.plugin.get_plugin_byhtml(name, function (rhtml) { + if (rhtml.status === false) { + layer.msg(rhtml.msg, { icon: 2 }); + return; + } + + var list = rhtml.split('', ''); + } else { + list = rhtml.split('', ''); + } + rcss = rhtml.split('')[0]; + rcode = rcode.replace(' wafview()', ''); + $('body').append('
                                  '); + + setTimeout(function () { + if (!!(window.attachEvent && !window.opera)) { + execScript(rcode); + } else { + window.eval(rcode); + } + }, 200); + + setTimeout(function () { + if (callback) callback(); + }, 400); + }); + }, + select_site_txt: function (box, value) { + var that = this; + layer.open({ + type: 1, + closeBtn: 2, + title: lan.site.set_ssl.cust_domain, + area: '600px', + btn: [lan.public.ok, lan.public.cancel], + content: + '
                                  Domain name
                                  \ +
                                    \ +
                                  • ' + + lan.site.set_ssl.cust_tip1 + + '
                                  • \ +
                                  • ' + + lan.site.set_ssl.cust_tip2 + + '
                                  • \ +
                                  • ' + + lan.site.set_ssl.cust_tip3 + + '
                                  • \ +
                                  • 1、' + + lan.site.set_ssl.cust_tip4 + + '
                                  • \ +
                                  • 2、' + + lan.site.set_ssl.cust_tip5 + + '
                                  • \ +
                                  • 3、' + + lan.site.set_ssl.cust_tip6 + + '
                                  • \ +
                                  \ +
                                  ', + success: function ($layer) { + $('[name="site_name"]').focus(); + }, + yes: function (layers, index) { + var domain = $('.ssl_site_name_rc').val(), + code = $('.perfect_ssl_info').data('code'); + if (!bt.check_domain(domain)) { + return layer.msg(lan.site.set_ssl.sing_domain_err, { icon: 2 }); + } else if (code.indexOf('wildcard') === -1) { + if (domain.indexOf('*') > -1) { + return layer.msg(lan.site.set_ssl.sing_domain_more, { icon: 2 }); + } + } + layer.close(layers); + $('#' + box).val($('.ssl_site_name_rc').val()); + // that.check_domain_error(domain); + that.check_domain_dns(); + }, + }); + }, + /** + * @descripttion: 选择站点 + * @author: Lifu + * @Date: 2020-08-14 + * @param {String} box 输出时所用ID + * @return: 无返回值 + */ + select_site_list: function (box, code) { + var that = this, + _optArray = [], + all_site_list = []; + bt.send('getData', 'data/getData', { tojs: 'site.get_list', table: 'domain', limit: 10000, search: '', p: 1, order: 'id desc', type: -1 }, function (res) { + var _tbody = ''; + if (res.data.length > 0) { + $.each(res.data, function (index, item) { + _body = + '' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '' + + '' + + item['name'] + + '' + + ''; + if (code.indexOf('wildcard') > -1) { + if (item['name'].indexOf('*.') > -1) { + all_site_list.push(item['name']); + _tbody += _body; + } + } else { + all_site_list.push(item['name']); + _tbody += _body; + } + }); + if (all_site_list.length == 0) { + _tbody = '' + lan.bt.no_data + ''; + } + } else { + _tbody = '' + lan.bt.no_data + ''; + } + + layer.open({ + type: 1, + closeBtn: 2, + title: lan.site.set_ssl.select_domain, + area: ['600px', '650px'], + btn: [lan.public.ok, lan.public.cancel], + content: + '\ +
                                  \ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + \ + \ + \ + \ + \ + ' + + _tbody + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.domain_name + + '
                                  \ +
                                  \ +
                                  \ +
                                    \ +
                                  • ' + + lan.site.set_ssl.cust_tip1 + + '
                                  • \ +
                                  • ' + + lan.site.set_ssl.cust_tip2 + + '
                                  • \ +
                                  • ' + + lan.site.set_ssl.cust_tip3 + + '
                                  • \ +
                                  • 1、' + + lan.site.set_ssl.cust_tip4 + + '
                                  • \ +
                                  • 2、' + + lan.site.set_ssl.cust_tip5 + + '
                                  • \ +
                                  • 3、' + + lan.site.set_ssl.cust_tip6 + + '
                                  • \ +
                                  \ +
                                  ', + success: function ($layer) { + // 固定表格头部 + if (jQuery.prototype.fixedThead) { + $('.dynamic_list_table .divtable').fixedThead({ resize: false }); + } else { + $('.dynamic_list_table .divtable').css({ overflow: 'auto' }); + } + //检索输入 + $('input[name=serach_site]').on('input', function () { + var _serach = $(this).val(); + if (_serach.trim() != '') { + $('.dynamic_list tr').each(function () { + var _td = $(this).find('td').eq(1).html(); + if (_td.indexOf(_serach) == -1) { + $(this).hide(); + } else { + $(this).show(); + } + }); + } else { + $('.dynamic_list tr').show(); + } + }); + + // 单选设置 + $('.dynamic_list').on('click', '.bt_checkbox_groups', function (e) { + var _tr = $(this).parents('tr'); + if ($(this).hasClass('active')) { + $(this).removeClass('active'); + } else { + $('.dynamic_list .bt_checkbox_groups').removeClass('active'); + $(this).addClass('active'); + _optArray = [_tr.find('td').eq(1).text()]; + } + e.preventDefault(); + e.stopPropagation(); + }); + // tr点击时 + $('.dynamic_list').on('click', 'tr', function (e) { + $(this).find('.bt_checkbox_groups').click(); + e.preventDefault(); + e.stopPropagation(); + }); + }, + yes: function (layers, index) { + var _olist = []; + if (_optArray.length > 0) { + $.each(_optArray, function (index, item) { + if ($.inArray(item, _olist) == -1) { + _olist.push(item); + } + }); + } + layer.close(layers); + // 多域名时,将olist过滤并追加到site.domain_dns_list + if (site.domain_dns_type == 'multi') { + var domainList = site.domain_dns_list; + var newDomainList = []; + $.each(_olist, function (index, item) { + if ($.inArray(item, domainList) == -1) { + newDomainList.push(item); + } + }); + domainList = domainList.concat(newDomainList); + } else { + domainList = _olist; + } + $('#' + box).val(domainList.join('\n')); + site.domain_dns_list = domainList; + $('textarea[name=lb_site]').focus(); + + // that.check_domain_error(_olist[0]); + that.check_domain_dns(); + }, + }); + }); + }, + web_edit: function (obj) { + var _this = this; + var item = obj; + bt.open({ + type: 1, + area: ['860px', '740px'], + title: lan.site.website_change + ' [' + item.name + '] -- ' + lan.site.addtime + ' [' + item.addtime + ']', + closeBtn: 2, + shift: 0, + content: "
                                  ", + }); + setTimeout(function () { + var webcache = + bt.get_cookie('serverType') == 'openlitespeed' + ? { + title: 'LS-Cache', + callback: site.edit.ols_cache, + } + : ''; + var menus = [ + { title: lan.site.domain_man, callback: site.edit.set_domains }, + { title: lan.site.site_menu_1, callback: site.edit.set_dirbind }, + { title: lan.site.site_menu_2, callback: site.edit.set_dirpath }, + { title: 'Limit access', callback: site.edit.set_dirguard }, + { title: lan.site.site_menu_3, callback: site.edit.limit_network }, + { title: lan.site.site_menu_4, callback: site.edit.get_rewrite_list }, + { title: lan.site.site_menu_5, callback: site.edit.set_default_index }, + { title: lan.site.site_menu_6, callback: site.edit.set_config }, + { title: lan.site.site_menu_7, callback: site.set_ssl }, + { title: lan.site.php_ver, callback: site.edit.set_php_version }, + { title: 'Composer', callback: site.edit.set_composer }, + // { title: lan.site.site_menu_9, callback: site.edit.set_tomact }, + // { title: lan.site.redirect, callback: site.edit.set_301_old }, + { title: lan.site.redirect_test, callback: site.edit.set_301 }, + { title: lan.site.site_menu_11, callback: site.edit.set_proxy }, + { title: lan.site.site_menu_12, callback: site.edit.set_security }, + { title: lan.site.response_log, callback: site.edit.get_site_logs }, + ]; + if (webcache !== '') menus.splice(3, 0, webcache); + if (item.project_type == 'WP') menus.splice(10, 0, { title: 'Wordpress Setting', callback: site.edit.set_wp_config }); + for (var i = 0; i < menus.length; i++) { + var men = menus[i]; + var _p = $('

                                  ' + men.title + '

                                  '); + _p.data('callback', men.callback); + $('.site-menu').append(_p); + } + $('.site-menu p').click(function () { + $('#webedit-con').html(''); + $(this).addClass('bgw').siblings().removeClass('bgw'); + var callback = $(this).data('callback'); + if (callback) callback(item); + }); + site.reload(0); + }, 100); + }, + domain_dns_list: [], // dns域名列表 + domain_dns_type: '', // dns域名类型[one/multi] + dns_data_treating: [], // 原始dns接口数据 + dns_configured_table: [], // 已配置的dns域名列表 + dns_interface_list: [], // dns接口列表 + dnsForm: null, + //检测域名api配置是否正确 + check_domain_dns: function () { + if (site.domain_dns_type == 'one') { + var domain = $('#apply_site_name').val(); + site.domain_dns_list = [domain]; + if (domain == '') return layer.msg(lan.site.set_ssl.domain_name_pl, { icon: 0 }); + } else if (site.domain_dns_type == 'multi') { + if (site.domain_dns_list.length == 0) return layer.msg(lan.site.set_ssl.domain_name_pla, { icon: 0 }); + } + site.refresh_dns_interface(); + }, + // 管理dns接口 + set_dns_api_open: function () { + bt_tools.open({ + title: lan.site.set_ssl.man_dns, + area: '600px', + btn: false, + content: '
                                  ', + success: function (layers) { + bt_tools.table({ + el: '.dnsManager', + url: '/site?action=GetDnsApi', + height: '400', + dataFilter: function (res) { + var data = []; + for (var i = 1; i < res.length; i++) { + var resI = res[i]; + for (var j = 0; j < resI.data.length; j++) { + var info = {}; + info = { + ps: resI.data[j].ps, + id: resI.data[j].id, + domain: resI.data[j].domain, + conf: resI.data[j].conf, + typeTitle: resI.title, + dns_type: resI.name, + name: resI.data[j].conf[0].value, + value: resI.data[j].conf[1].value, + }; + // 获取dns接口配置信息 + for (var k = 0; k < resI.data[j].conf.length; k++) { + info[resI.data[j].conf[k].name] = resI.data[j].conf[k].value; + } + data.push(info); + } + } + site.dns_configured_table = site.data_treating(res); + return { data: data }; + }, + column: [ + { + fid: 'typeTitle', + title: lan.public_backup.type, + }, + { + fid: 'ps', + title: lan.soft.ps, + }, + { + title: bt.public.action, + align: 'right', + type: 'group', + group: [ + { + title: bt.public.edit, + event: function (row, index, ev, key, _that) { + site.editDns(row, _that); + }, + }, + { + title: bt.public.del, + event: function (row, index, ev, key, _that) { + bt.simple_confirm({ title: 'delete【' + row.ps + '】', msg: lan.site.set_ssl.del_api_confirm + '?' }, function () { + bt_tools.send( + { url: '/site?action=remove_dns_api', data: { dns_type: row.dns_type, api_id: row.id } }, + function (res) { + bt_tools.msg(res); + if (res.status) _that.$refresh_table_list(true); + }, + 'Delete authentication interface' + ); + }); + }, + }, + ], + }, + ], + tootls: [ + { + type: 'group', + positon: ['left', 'top'], + list: [ + { + title: lan.site.set_ssl.add_dns, + active: true, + event: function (row, _that) { + site.editDns(undefined, _that); + }, + }, + ], + }, + ], + }); + }, + }); + }, + + // 添加dns接口 + add_dns_interface: function () { + var _this = this; + if ($('.dns_interface_line').length > 0) return; + $('.check_model_line.line').after('
                                  '); // 插入dns + $('.isdnsbtn').show(); // 显示dns刷新按钮 + var formConfig = [ + { + label: lan.site.set_ssl.select_dns, + group: [ + { + type: 'select', + name: 'dns_select', + width: '250px', + placeholder: lan.site.set_ssl.select_parse, + list: [ + // {title:lan.site.set_ssl.auto_parse,value:'dns#@api'}, + { title: lan.site.set_ssl.manual_parse, value: 'dns' }, + ], + change: function (formData, el, that) { + that.config.form[0].group[0].value = formData.dns_select; + if (formData.dns_select == 'dns#@api') { + that.config.form[0].group[0].suffix = ''; + that.config.form[0].group[1].display = true; + } else { + that.config.form[0].group[0].suffix = '
                                  ' + lan.site.set_ssl.parse_tip + ''; + that.config.form[0].group[1].display = false; + } + that.$replace_render_content(0); + }, + }, + { + display: false, + type: 'button', + class: 'btn-sub-success', + style: { 'margin-left': '10px', 'vertical-align': 'middle' }, + title: lan.site.set_ssl.dns_api_config, + }, + ], + }, + ]; + if ($('#ssl_tabs span.on').text().indexOf('Let') > -1) { + // formConfig插入数据 + formConfig.push( + { + label: '', + group: [ + { + name: 'app_root', + type: 'checkbox', + title: lan.site.set_ssl.auto_more_domain, + }, + ], + }, + { + label: '', + group: [ + { + type: 'help', + style: { margin: '0' }, + list: [lan.site.set_ssl.auto_more_tip], + }, + ], + } + ); + } + // 渲染dns + _this.dnsForm = bt_tools.form({ + el: '.dnsForm', + form: formConfig, + }); + //管理dns点击事件 + $('.dnsForm') + .unbind('click') + .on('click', '.btn-sub-success.btn-success', function () { + site.set_dns_api_open(); + }); + }, + // 添加/编辑dns接口 + editDns: function (row, _that, isGlobal) { + var isEdit = row && row.hasOwnProperty('name') ? true : false; + bt_tools.open({ + title: isEdit ? 'Edit【' + row.ps + '】' : lan.site.set_ssl.add_dns_ver, + area: '530px', + skin: 'dns_layer_form', + content: { + class: 'pd20', + data: row, + form: this.switch_dns_add_key(isEdit ? row.dns_type : site.dns_interface_list[0].value, isGlobal), + }, + success: function (layero) { + $('.dns-help li').eq(3).show().siblings().hide(); + $(layero).find('.layui-layer-content').css('overflow', 'inherit'); + if (isGlobal) { + $('textarea[name=domains]').val(isEdit ? row.domain.join('\n') : ''); + } + }, + yes: function (formData, indexs) { + // 是否验证成功 + var isverify = true, + param = { dns_type: formData.dns_type, ps: formData.ps }, + paramArr = []; + // 限制ps的长度 + if (formData.ps.length > 35) return layer.msg(lan.site.set_ssl.ps_pl, { icon: 0 }); + // dns_layer_form下input如果为空,提示对应的placeholder,然后return false + $('.dns_layer_form input').each(function (index, item) { + var _val = $(item).val(); + if (_val == '' || _val.replace(/\s+/g, '') == '') { + layer.msg($(item).attr('placeholder'), { icon: 0 }); + isverify = false; + return false; + } + }); + if (!isverify) return false; + // 排除formData中的dns_type和ps,循环添加到paramArr中 + for (var key in formData) { + if (key !== 'dns_type' && key !== 'ps') { + paramArr.push({ name: key, value: formData[key] }); + } + } + param['pdata'] = JSON.stringify(paramArr); + + if (isEdit) param['api_id'] = row.id; + if (typeof row != 'undefined' && row.hasOwnProperty('domains')) { + param['domains'] = JSON.stringify([row.domains]); // 没有dns接口,默认已当前域名添加 + } else if (isGlobal) { + // 高级设置类型 + param['domains'] = JSON.stringify(formData.domains.split('\r\n')); + } else { + param['domains'] = JSON.stringify([]); //不设定固定域名,单纯链接dns接口 + } + bt_tools.send( + { url: isEdit ? '/site?action=set_dns_api' : '/site?action=add_dns_api', data: param }, + function (res) { + bt_tools.msg(res); + if (res.status) { + layer.close(indexs); + if (_that) { + _that.$refresh_table_list(true); + } else { + bt_tools.send({ url: '/site?action=GetDnsApi' }, function (data) { + site.dns_configured_table = site.data_treating(data); + }); + } + } + }, + isEdit ? 'Modify authentication interface' : 'Add authentication interface' + ); + }, + }); + }, + /** + * 生成不同dns类型的配置 + * @param {*} type dns|DNSPodDns|AliyunDns|CloudflareDns|GodaddyDns|DNSLADns| + * @returns config 用于重新渲染表单 + */ + switch_dns_add_key: function (type, isGlobal) { + // 在原始数据中查找type相同的数据 + var sthat = this, + firstApi = site.dns_data_treating.find(function (item) { + return item.name === type; + }), + helpObj = {}, + configKey = []; + for (var i = 0; i < site.dns_interface_list.length; i++) { + var help = site.dns_interface_list[i].help; + helpObj[site.dns_interface_list[i].value] = [ + '' + help[0].title + '', + '' + help[1].title + '', + ]; + } + var config = [ + { + label: lan.site.set_ssl.ver_type, + group: [ + { + type: 'select', + name: 'dns_type', + width: '330px', + list: this.dns_interface_list, + change: function (formData, element, that) { + // 根据不同的dns接口,渲染不同的输入框 + that.config.form[0].group[0].value = formData.dns_type; + that.$again_render_form(sthat.switch_dns_add_key(formData.dns_type)); + }, + }, + ], + }, + { + label: lan.soft.ps, + group: [ + { + type: 'text', + name: 'ps', + width: '330px', + placeholder: lan.site.set_ssl.ps_pls, + }, + ], + }, + { + group: [ + { + type: 'help', + class: 'dns-help', + list: helpObj[type], + }, + ], + }, + ]; + //高级设置类型追加显示已添加的域名 + if (isGlobal) { + // 往config的第二个种,追加域名 + config.splice(2, 0, { + label: lan.site.set_ssl.as_domain, + group: [ + { + type: 'textarea', + name: 'domains', + style: { + width: '330px', + 'min-width': '330px', + 'min-height': '130px', + 'line-height': '22px', + 'padding-top': '10px', + resize: 'both', + }, + placeholder: lan.site.set_ssl.more_domain_pl, + }, + ], + }); + } + $.each(firstApi.add_table[0].fields, function (index, item) { + configKey.push({ + label: item, + group: [ + { + type: 'text', + name: item, + width: '330px', + placeholder: 'Please enter' + item, + }, + ], + }); + }); + config = config.slice(0, 1).concat(configKey).concat(config.slice(1)); + return config; + }, + // 深度获取所有dns账号中的域名 + data_treating: function (data) { + var tableData = [], + dns_type = []; + this.dns_data_treating = data.filter(function (item) { + return item.name !== 'dns'; + }); // 原始数据 + for (var i = 0; i < data.length; i++) { + if (data[i].name == 'dns') continue; //手动解析 + var item = data[i]; + if (item.data) { + for (var j = 0; j < item.data.length; j++) { + tableData.push({ + ps: item.data[j].ps, + id: item.data[j].id, + domain: item.data[j].domain, + conf: item.data[j].conf, + typeTitle: item.title, + dns_type: item.name, + name: item.data[j].conf ? item.data[j].conf[0].value : '', + value: item.data[j].conf ? item.data[j].conf[1].value : '', + help: item.help, + add_table: item.add_table, + }); + } + } + dns_type.push({ title: item.title, value: item.name, help: item.help }); + } + this.dns_interface_list = dns_type; + return tableData; + }, + // 移除dns接口 + remove_dns_interface: function () { + $('.dns_interface_line.line').remove(); + $('.isdnsbtn').hide(); + }, + // 自动刷新dns域名情况 + refresh_dns_interface: function () { + // 当前解析类型 + var dns_type = $('select[name=dns_select]').val(), + that = this; + // 是否手动解析 + if (dns_type == 'dns') { + return false; + } + bt_tools.send({ url: 'site?action=test_domains_api', data: { domains: JSON.stringify(site.domain_dns_list) } }, function (res) { + if (site.domain_dns_type == 'one') { + if ($.isEmptyObject(res[0])) { + $('.damin_dns_result').html( + '
                                  ' + ); + } else { + $('.damin_dns_result').html( + '
                                  ' + ); + } + + // 点击配置 + $('.damin_dns_result').on('click', 'button', function () { + // 没有dns接口,直接跳转到添加 + if (site.dns_configured_table.length == 0) return site.editDns({ domains: $('#apply_site_name').val() }); + // 渲染dns列表 + var options = ''; + $.each(site.dns_configured_table, function (index, item) { + options += '
                                • ' + item.typeTitle + '[' + item.ps + ']' + '
                                • '; + }); + $('.damin_dns_result .dropdown-menu').html(options); + }); + // 点击选择 + $('.damin_dns_result .dropdown-menu') + .unbind('click') + .on('click', 'li', function () { + var dns_id = $(this).data('id'), + param = { api_id: dns_id }, + item = site.dns_configured_table.find(function (item) { + return item.id == dns_id; + }); + + param['dns_type'] = item.dns_type; + param['force_domain'] = $('#apply_site_name').val(); + bt_tools.send({ url: 'site?action=set_dns_api', data: param }, function (res) { + that.refresh_dns_interface(); + }); + }); + } else { + var view = ''; + $.each(site.domain_dns_list, function (index, item) { + // 是否空对象(没有设置dns) + var isDnsEmpty = $.isEmptyObject(res[index]); + view += + '
                                  \ +
                                  ' + + item + + '
                                  \ +
                                  \ +
                                  \ + \ + \ +
                                  \ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  '; + }); + $('.dns_domains_multi_list').html(view); + + // 点击配置 + $('.dns_domains_item_btn button').click(function () { + // 没有dns接口,直接跳转到添加 + if (site.dns_configured_table.length == 0) return site.editDns({ domains: $(this).parents('.dns_domains_item').find('.dns_domains_item_title').text() }); + // 渲染dns列表 + var options = ''; + $.each(site.dns_configured_table, function (index, item) { + options += '
                                • ' + item.typeTitle + '[' + item.ps + ']' + '
                                • '; + }); + $('.dns_domains_item_btn .dropdown-menu').html(options); + }); + // 点击选择 + $('.dns_domains_item_btn .dropdown-menu') + .unbind('click') + .on('click', 'li', function () { + var dns_id = $(this).data('id'), + param = { api_id: dns_id }, + item = site.dns_configured_table.find(function (item) { + return item.id == dns_id; + }); + + param['dns_type'] = item.dns_type; + param['force_domain'] = $(this).parents('.dns_domains_item').find('.dns_domains_item_title').text(); + bt_tools.send({ url: 'site?action=set_dns_api', data: param }, function (res) { + that.refresh_dns_interface(); + }); + }); + // 点击删除 + $('.dns_domains_item_close') + .unbind('click') + .click(function () { + // 从site.domain_dns_list中删除当前选中的域名 + var domain = $(this).parents('.dns_domains_item').find('.dns_domains_item_title').text(); + site.domain_dns_list = site.domain_dns_list.filter(function (item) { + return item != domain; + }); + $(this).parents('.dns_domains_item').remove(); + }); + // dns_domains_item鼠标经过时添加active,离开时移除active + $('.dns_domains_item') + .unbind('mouseenter') + .mouseenter(function () { + $(this).addClass('active'); + }); + $('.dns_domains_item') + .unbind('mouseleave') + .mouseleave(function () { + $(this).removeClass('active'); + }); + } + }); + }, + //批量设置站点证书 + setBathSiteSsl: function (batch_list, callback) { + bt_tools.send( + { + url: '/ssl?action=SetBatchCertToSite', + data: { + BatchInfo: JSON.stringify(batch_list), + }, + }, + function (res) { + if (callback) callback(res); + }, + 'Set site certificates in batches' + ); + }, + set_ssl: function (web) { + site.web = web + + //站点/项目名、放置位置 + bt.site.get_site_ssl(web.name, function (rdata) { + var type = rdata.type; // 类型 + var certificate = rdata.cert_data; // 证书信息 + var pushAlarm = rdata.push; // 是否推送告警 + var isStart = rdata.status; // 是否启用 + var layers = null; + var expirationTime = certificate.endtime; // 证书过期时间 + var isRenew = (function () { + // 是否续签 + var state = false; + if (expirationTime <= 30) state = true; + if (type === 2 && expirationTime < 0) state = true; + if (type === 0 || type === -1) state = false; + return state; + })(); + + // 续签视图 + function renewal_ssl_view(item) { + bt.confirm( + { + title: 'Visa renewal letter', + msg: 'The current certificate order needs to be regenerated into a new order, which requires manual renewal and re-deployment of the certificate. Do you want to continue?', + }, + function () { + var loadT = bt.load(lan.site.set_ssl.renew_cert_load); + bt.send( + 'renew_cert_order', + 'ssl/renew_cert_order', + { + pdata: JSON.stringify({ oid: item.oid }), + }, + function (res) { + loadT.close(); + site.reload(); + setTimeout(function () { + bt.msg(res); + }, 1000); + } + ); + } + ); + } + + // 申请宝塔证书 + function apply_bt_certificate() { + var html = ''; + var domains = []; + for (var i = 0; i < rdata.domain.length; i++) { + var item = rdata.domain[i]; + if (item.name.indexOf('*') == -1) domains.push({ title: item.name, value: item.name }); + } + for (var i = 0; i < domains.length; i++) { + var item = domains[i]; + html += ''; + } + bt.open({ + type: 1, + title: lan.site.set_ssl.req_free_cert, + area: '610px', + content: + '
                                  \ +
                                  \ + \ +
                                  提示:尊敬的用户您好,感谢您对宝塔免费SSL证书的支持,由于证书签发机制的调整,我们的免费SSL证书签发将于2023年12月31日进行下架,查看详情
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.cert_info + + '\ +
                                  \ + TrustAsia TLS RSA CA(Free edition)\ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.domain_name + + '\ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.per_name + + '\ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.lo_area + + '\ +
                                  \ + \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.address + + '\ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.public_backup.mobile_phone_or_email + + '\ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.pos_code + + '\ +
                                  \ + \ +
                                  \ +
                                  \ + \ +
                                  \ + \ +
                                  \ + ' + + lan.site.set_ssl.add_tips + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  ', + success: function (layero, index) { + $('.submit_ssl_info').click(function () { + var form = $('.free_ssl_info').serializeObject(); + for (var key in form) { + if (Object.hasOwnProperty.call(form, key)) { + var value = form[key], + el = $('[name="' + key + '"]'); + if (value == '') { + layer.tips(el.attr('placeholder'), el, { tips: ['1', 'red'] }); + el.focus(); + el.css('borderColor', 'red'); + return false; + } else { + el.css('borderColor', ''); + } + switch (key) { + case 'orgPhone': + if (!bt.check_phone(value)) { + layer.tips(lan.site.set_ssl.phone_ver, el, { tips: ['1', 'red'] }); + el.focus(); + el.css('borderColor', 'red'); + return false; + } + break; + case 'orgPostalCode': + if (!/^[0-9]\d{5}(?!\d)$/.test(value)) { + layer.tips(lan.site.set_ssl.postal_ver, el, { tips: ['1', 'red'] }); + el.focus(); + el.css('borderColor', 'red'); + return false; + } + break; + } + } + } + if (form.domain.indexOf('www.') != -1) { + var rootDomain = form.domain.split(/www\./)[1]; + if (!$.inArray(domains, rootDomain)) { + layer.msg(lan.site.set_ssl.no_root_tip(form.domain, rootDomain), { icon: 2, time: 5000 }); + return; + } + } + var loadT = bt.load(lan.site.set_ssl.sub_cert); + bt.send('ApplyDVSSL', 'ssl/ApplyDVSSL', $.extend(form, { path: web.path }), function (tdata) { + loadT.close(); + if (tdata.msg.indexOf('
                                  ') != -1) { + layer.msg(tdata.msg, { time: 0, shadeClose: true, area: '600px', icon: 2, shade: 0.3 }); + } else { + bt.msg(tdata); + } + if (tdata.status) { + layer.close(index); + site.ssl.verify_domain(tdata.data.partnerOrderId, web.name); + } + }); + }); + $('.free_ssl_info input').keyup(function (res) { + var value = $(this).val(); + if (value == '') { + layer.tips($(this).attr('placeholder'), $(this), { tips: ['1', 'red'] }); + $(this).focus(); + $(this).css('borderColor', 'red'); + } else { + $(this).css('borderColor', ''); + } + }); + }, + }); + } + + if (!Array.isArray(certificate.dns)) certificate = { dns: [] }; + + $('#webedit-con').html( + '
                                  ' + + '

                                  ' + + lan.site.set_ssl.tips1 + + '

                                  ' + + '

                                  ' + + lan.site.set_ssl.tips2( + '' + + certificate.dns.join(', ') + + '', + expirationTime < 0 + ) + + '

                                  ' + + '
                                  ' + + '
                                  ' + ); + var tabs = [ + { + title: lan.site.set_ssl.menu1 + ' - [' + (rdata.status ? lan.site.set_ssl.deployed : lan.site.set_ssl.not_deployed) + ']', + callback: function (content) { + acme.id = web.id; + var classify = ''; + var typeList = [lan.site.set_ssl.cert_type1, lan.site.set_ssl.cert_type2, lan.site.set_ssl.cert_type3, lan.site.set_ssl.cert_type4]; + var state = $( + '
                                  ' + + '
                                  ' + + '' + + '
                                  ' + + lan.site.set_ssl.cert_brand + + ':' + + certificate.issuer + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + lan.site.set_ssl.auth_domain + + '' + + certificate.dns.join('、') + + '
                                  ' + + '
                                  ' + + lan.site.set_ssl.expire_time + + '' + + (expirationTime >= 0 ? lan.site.set_ssl.expire_time_text(rdata.cert_data.notAfter, expirationTime.toFixed(0)) : lan.site.set_ssl.expired) + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + lan.site.set_ssl.force_https + + '
                                  ' + + // '
                                  ' + lan.site.set_ssl.expire_reminder + ':Config
                                  ' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + lan.site.set_ssl.ssl_key + + '
                                  ' + + '
                                  ' + + lan.site.set_ssl.ssl_crt + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '' + + '' + + '' + + '' + + '
                                  ' + ); + content.append(state); + content.append( + bt.render_help([lan.site.set_ssl.save_ssl_tips1, lan.public_backup.cret_err, lan.public_backup.pem_format, lan.site.set_ssl.save_ssl_tips2, lan.site.set_ssl.save_ssl_tips3]) + ); + // if(rdata.status) { + // bt_tools.send({url: '/site?action=check_ssl',data: {hostname: web.name}}, function (res) { + // if(!res.status) content.prepend('
                                  '+ res.msg +'
                                  ') + // },{verify:false}) + // } + var setAlarmMode = bt.get_cookie('setAlarmMode'); + if (!pushAlarm.status && rdata.csr && !setAlarmMode) { + // if (true) { + bt.set_cookie('setAlarmMode', 1); + layer.tips(lan.site.set_ssl.set_alarm_mode_tips, '.setAlarmMode', { + tips: [1, '#d9534f'], + area: '380px', + time: 5000, + }); + setTimeout(function () { + $(window).one('click', function () { + layer.closeAll('tips'); + }); + }, 500); + } + var moduleConfig = null; + function cacheModule(callback) { + if (moduleConfig && callback) return callback(moduleConfig); + bt.site.get_module_config({ name: 'site_push', type: 'ssl' }, function (rdata1) { + moduleConfig = rdata1; + if (callback) callback(rdata1); + }); + } + + /** + * 提醒到期弹框 + * @param $check 到期提醒开关 + */ + function alarmMode($check) { + var time = new Date().getTime(); + var isExpiration = pushAlarm.status; + if ($check) isExpiration = $check.is(':checked'); + layer.open({ + type: 1, + title: lan.site.set_ssl.expire_reminder_title, + area: '470px', + closeBtn: 2, + content: + '\ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.expire_reminder + + '\ +
                                  \ + \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.site + + '\ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.remaining_days + + '\ +
                                  \ +
                                  \ + \ + Days\ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.number_ransmissions + + '\ +
                                  \ +
                                  \ + \ + ,' + + lan.site.set_ssl.no_more + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.notification_mode + + '\ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.application_config + + '\ +
                                  \ +
                                  \ +
                                  \ + \ + \ + ' + + lan.site.set_ssl.apply_all + + ' ' + + lan.site.set_ssl.no_config_site + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  ', + btn: [lan.site.set_ssl.save_config, lan.public.cancel], + success: function ($layer) { + cacheModule(function (rdata1) { + // 获取配置 + bt.site.get_msg_configs(function (rdata) { + var html = '', + unInstall = '', + pushList = rdata1.push; + for (var key in rdata) { + var item = rdata[key], + _html = '', + accountConfigStatus = false, + module = pushAlarm.module || []; + if (pushList.indexOf(item.name) === -1) continue; + if (key == 'sms') continue; + if (key === 'wx_account') { + if (!$.isEmptyObject(item.data) && item.data.res.is_subscribe && item.data.res.is_bound) { + accountConfigStatus = true; //安装微信公众号模块且绑定 + } + } + _html = + '
                                  ' + + '
                                  ' + + '' + + '' + + '' + + item.title + + (!item.setup || $.isEmptyObject(item.data) + ? '[' + lan.public_backup.install + ']' + : key == 'wx_account' && !accountConfigStatus + ? '[' + lan.public_backup.install + ']' + : '') + + '' + + '
                                  ' + + '
                                  '; + if (!item.setup) { + unInstall += _html; + } else { + html += _html; + } + } + $('.installPush').html(html + unInstall); + $('.setAllSsl').on('click', function () { + var that = $(this).find('i'); + if (that.hasClass('active')) { + that.removeClass('active'); + that.next().prop('checked', false); + } else { + that.addClass('active'); + that.next().prop('checked', true); + } + }); + if (pushAlarm.project === 'all' && pushAlarm.status) $('.setAllSsl').trigger('click'); + }); + }); + + // 安装消息通道 + $('.installPush').on('click', '.form-checkbox-label', function () { + var that = $(this).find('i'); + if (!that.parent().parent().hasClass('check_disabled')) { + if (that.hasClass('active')) { + that.removeClass('active'); + that.next().prop('checked', false); + } else { + that.addClass('active'); + that.next().prop('checked', true); + } + } + }); + $('.triggerCycle').on('input', function () { + $('.siteSslHelp span').html($(this).val()); + }); + + $('.installPush').on('click', '.installNotice', function () { + var type = $(this).data('type'); + openAlertModuleInstallView(type); + }); + }, + yes: function (index) { + var status = $('input[name="due_alarm"]').is(':checked'); + var cycle = $('.triggerCycle').val(); + var push_count = $('.triggerPushCount').val(); + var arry = []; + var module = ''; + var isAll = $('[name="allSsl"]').is(':checked'); + $('.installPush .active').each(function (item) { + var item = $(this).attr('data-type'); + arry.push(item); + }); + if (!arry.length) return layer.msg('Please select an alarm mode', { icon: 2 }); + if (!parseInt(cycle)) return layer.msg('Remaining days cannot be less than 1', { icon: 2 }); + if (!parseInt(push_count)) return layer.msg('Send times cannot be less than 1', { icon: 2 }); + + // 参数 + var data = { + status: status, + type: 'ssl', + project: web.name, + cycle: parseInt(cycle), + title: 'Website SSL expiration alert', + module: arry.join(','), + interval: 600, + push_count: parseInt(push_count), + }; + + // 判断是否点击全局应用 + if (isAll) { + // 请求设置全局应用告警配置 + var allData = Object.assign({}, data); + allData.status = true; + allData.project = 'all'; + bt.site.set_push_config({ + name: 'site_push', + id: time, + data: JSON.stringify(allData), + }); + } + + // 请求设置本站点告警配置 + bt.site.set_push_config( + { + name: 'site_push', + id: pushAlarm.id ? pushAlarm.id : time, + data: JSON.stringify(data), + }, + function (rdata) { + bt.msg(rdata); + setTimeout(function () { + site.reload(); + }, 1000); + layer.close(index); + } + ); + }, + cancel: function () { + $check && $check.prop('checked', !isExpiration); + }, + btn2: function () { + $check && $check.prop('checked', !isExpiration); + }, + }); + } + // 设置强制HTTPS + $('#https').on('click', function () { + var that = $(this), + isHttps = $(this).is(':checked'); + if (!isHttps) { + layer.confirm( + lan.site.set_ssl.force_https_confirm, + { + icon: 3, + closeBtn: 2, + title: 'Turn off forced HTTPS', + cancel: function () { + that.prop('checked', !isHttps); + }, + btn2: function () { + that.prop('checked', !isHttps); + }, + }, + function () { + bt.site.close_http_to_https(web.name, function (rdata) { + if (rdata.status) { + setTimeout(function () { + site.set_ssl(site.web); + }, 3000); + } else { + that.prop('checked', !isHttps); + } + }); + } + ); + } else { + bt.site.set_http_to_https(web.name, function (rdata) { + if (rdata.status) { + site.set_ssl(site.web); + } else { + that.prop('checked', !isHttps); + layer.confirm(lan.site.set_ssl.open_ssl_comfirm, { icon: 3, title: 'Tips' }, function (index) { + $.ajaxSettings.async = false; + $('.saveCertificate').click(); + $.ajaxSettings.async = true; + $('#https').click(); + layer.close(index); + }); + } + }); + } + }); + + // 设置告警通知 + $('#expiration').on('click', function () { + layer.close(layers); + var _that = $(this); + var isExpiration = $(this).is(':checked'); + var time = new Date().getTime(); + if (isExpiration) { + alarmMode(_that); + } else { + var data = JSON.stringify({ + status: isExpiration, + type: 'ssl', + project: web.name, + cycle: parseInt(pushAlarm.cycle), + title: 'Website SSL expiration alert', + module: pushAlarm.module, + interval: 600, + push_count: 1, + }); + var id = pushAlarm.id ? pushAlarm.id : time; + if (pushAlarm.project === 'all') id = time; + bt.site.set_push_config( + { + name: 'site_push', + id: id, + data: data, + }, + function (rdata) { + bt.msg(rdata); + setTimeout(function () { + site.reload(); + }, 1000); + } + ); + } + }); + + // 保存证书 + $('.saveCertificate').on('click', function () { + var key = $('[name="key"]').val(), + csr = $('[name="csr"]').val(); + function set_ssl() { + if (key === '' || csr === '') return bt.msg({ status: false, msg: 'Please fill in the complete certificate content' }); + bt.site.set_ssl( + web.name, + { + type: rdata.type, + siteName: rdata.siteName, + key: key, + csr: csr, + }, + function (ret) { + if (ret.status) site.set_ssl(web); + if (site.model_table) site.model_table.$refresh_table_list(true); + if (node_table) node_table.$refresh_table_list(true); + if (site_table) site_table.$refresh_table_list(true); + bt.msg(ret); + } + ); + } + + if ((key !== rdata.key && rdata.key) || (csr !== rdata.csr && rdata.key)) { + layer.confirm( + lan.site.set_ssl.edit_cert_comfirm, + { + icon: 3, + closeBtn: 2, + title: 'Certificate saving prompt', + }, + set_ssl + ); + } else { + set_ssl(); + } + }); + + // 告警方式 + $('.setAlarmMode').on('click', function () { + layer.close(layers); + alarmMode(); + }); + + // 续签证书 + $('.renewCertificate') + .unbind('click') + .on('click', function () { + var type = parseInt($(this).attr('data-type')); + switch (type /**/) { + case 3: // 商业证书续签 + renewal_ssl_view({ oid: rdata.oid }); + break; + case 2: // 宝塔证书 续签 + apply_bt_certificate(); + layer.msg('The current certificate type does not support one-click renewal. Please fill in the information application again', { icon: 2, time: 2000 }); + break; + case 1: // Let's Encrypt 续签 + site.ssl.renew_ssl(web.name, rdata.auth_type, rdata.index); + break; + } + }); + + // 关闭证书 + $('.closeCertificate').on('click', function () { + site.ssl.set_ssl_status('CloseSSLConf', web.name); + }); + + // 切换证书类型 + $('.cutSslType').on('click', function () { + var type = $(this).attr('data-type'); + switch (type) { + case '0': + type = 0; + break; + case '1': + type = 3; + break; + case '2': + type = 2; + break; + case '3': + type = 1; + break; + } + $('#ssl_tabs span:eq(' + type + ')').trigger('click'); + }); + + // 下载证书 + $('.downloadCertificate').on('click', function () { + var key = $('[name="key"]').val(), + pem = $('[name="csr"]').val(); + bt.site.download_cert( + { + siteName: web.name, + pem: pem, + key: key, + }, + function (rdata) { + if (rdata.status) { + window.open('/download?filename=' + encodeURIComponent(rdata.msg)); + } else { + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + } + } + ); + }); + }, + }, + { + title: lan.site.set_ssl.menu2 + '', + callback: function (robj) { + $.getScript('https://js.stripe.com/v3/'); + robj = $('#webedit-con .tab-con'); + bt.pub.get_user_info(function (udata) { + if (udata.status) { + var deploy_ssl_info = rdata, + html = '', + deploy_html = '', + product_list, + userInfo, + order_list, + is_check = true, + itemData, + activeData, + loadY, + pay_ssl_layer; + bt.send('get_order_list', 'ssl/get_order_list', {}, function (res) { + var rdata = res.res; + order_list = rdata; + if (rdata.length == 0) { + $('#ssl_order_list tbody').html( + '' + + lan.site.set_ssl.no_cert + + ' ->' + + lan.site.set_ssl.apply_certificate + + '' + ); + return; + } + $.each(rdata, function (index, item) { + if (deploy_ssl_info.type == 3 && deploy_ssl_info.oid === item.uc_id) { + deploy_html += + '' + + '' + + item.domains.join('、') + + '' + + item.title + + '' + + (function () { + var dayTime = new Date().getTime() / 1000, + color = '', + endTiems = ''; + if (item.end_date != '') { + item.end_date = parseInt(item.end_date); + endTiems = parseInt((item.end_date - dayTime) / 86400); + if (endTiems <= 15) color = 'orange'; + if (endTiems <= 7) color = 'red'; + if (endTiems < 0) return '' + lan.site.set_ssl.expired + ''; + return '' + lan.site.set_ssl.expire_date_text(endTiems) + ''; + } else { + return '--'; + } + })() + + '' + + lan.site.set_ssl.order_complate + + '' + + lan.site.set_ssl.deployed + + ' | ' + + lan.public.close + + ''; + } else { + html += + '' + + '' + + (item.domains == null ? '--' : item.domains.join('、')) + + '' + + item.title + + '' + + (function () { + var dayTime = new Date().getTime() / 1000, + color = '', + endTiems = ''; + if (item.end_date != '') { + item.end_date = parseInt(item.end_date); + endTiems = parseInt((item.end_date - dayTime) / 86400); + if (endTiems <= 15) color = 'orange'; + if (endTiems <= 7) color = 'red'; + if (endTiems < 0) return '' + lan.site.set_ssl.expired + ''; + return '' + lan.site.set_ssl.expire_date_text(endTiems) + ''; + } else { + return '--'; + } + })() + + '' + + (function () { + var suggest = ''; + if (!item.install) + suggest = + ' |' + + lan.site.set_ssl.troubleshooting_method + + '?
                                  • ' + + lan.site.set_ssl.check_oneself + + '

                                    ' + + lan.site.set_ssl.check_self_tip + + '

                                  • Labor Service Purchase

                                    Need deployment assistance? Human customer service available.

                                  • ' + + '
                                  '; + if (item.certId == '') { + return '' + lan.site.set_ssl.data_com + '' + suggest; + } else if (item.status === 1) { + switch (item.order_status) { + case 'COMPLETE': + return '' + lan.site.set_ssl.order_complate + ''; + break; + case 'PENDING': + return '' + lan.site.set_ssl.in_verify + '' + suggest; + break; + case 'CANCELLED': + return '' + lan.site.set_ssl.cancelled + ''; + break; + case 'FAILED': + return '' + lan.site.set_ssl.app_fail + ''; + break; + default: + return '' + lan.site.set_ssl.to_verified + ''; + break; + } + } else { + switch (item.status) { + case 0: + return '' + lan.site.set_ssl.no_pay + ''; + break; + case -1: + return '' + lan.site.set_ssl.cancelled + ''; + break; + default: + return '--'; + } + } + })() + + '' + + (function () { + var html = ''; + if (item.renew) html += '' + lan.site.set_ssl.renew_cert + '  |  '; + if (item.certId == '') { + // if (item.install) html += '人工服务 | '; + html += '' + lan.site.set_ssl.complete_data + ''; + return html; + } else if (item.status === 1) { + var html = ''; + switch (item.order_status) { + case 'COMPLETE': //申请成功 + return ( + '' + + lan.site.set_ssl.deploy + + '  |  ' + + lan.site.set_ssl.download + + '' + ); + break; + case 'PENDING': //申请中 + // if (item.install) html += '人工服务 | '; + html += '' + lan.site.set_ssl.verify + ''; + return html; + break; + case 'CANCELLED': //已取消 + return lan.site.set_ssl.no_action; + break; + case 'FAILED': + return '' + lan.site.set_ssl.detail + ''; + break; + default: + // if (item.install) html += '人工服务 | '; + html += '' + lan.site.set_ssl.verify + ''; + return html; + break; + } + } else { + return '--'; + } + })() + + '' + + ''; + } + }); + $('#ssl_order_list tbody').html(deploy_html + html); + //解决方案事件 + $('#ssl_order_list').on('click', '.bt_ssl_suggest', function (e) { + var $this = $(this); + var $cont = $this.find('.suggest_content'); + var rect = $this.offset(); + var top = rect.top + $this.height() + 7; + var left = rect.left - 153; + $cont.css({ + top: top + 'px', + left: left + 'px', + right: 'auto', + bottom: 'auto', + width: '410px', + }); + $('.suggest_content').hide(); + $cont.show(); + $(document).one('click', function () { + $cont.hide(); + }); + e.stopPropagation(); + }); + // 表格滚动隐藏解决方案内容 + $('.ssl_order_list').scroll(function (e) { + $('.suggest_content').hide(); + }); + //人工客服购买 + $('#ssl_order_list').on('click', '.service_buy', function () { + var loads = bt.load('Payment order is being generated, please wait...'); + bt.send( + 'apply_cert_install_pay', + 'ssl/apply_cert_install_pay', + { + uc_id: $(this).data('oid'), + }, + function (res) { + loads.close(); + if (!res.success) { + layer.msg(res.res, { icon: 2 }); + return; + } + var stripe = Stripe(res.res.stripe_public_key); + stripe.redirectToCheckout({ sessionId: res.res.session_id }); + // if (res.status != undefined && !res.status) { + // return layer.msg(res.msg, { time: 0, shadeClose: true, icon: 2, shade: 0.3 }); + // } + // open_service_buy(res); + } + ); + }); + + //人工客服咨询 + $('.service_method').click(function () { + bt.onlineService(); + }); + }); + robj.append( + '
                                  ' + + lan.site.set_ssl.enterprise_certificate + + '
                                  ' + + lan.site.set_ssl.exceptional_application + + '
                                  ' + + lan.site.set_ssl.anti_hijackingTampering + + '
                                  ' + + lan.site.set_ssl.increase_seo + + '
                                  ' + + lan.site.set_ssl.indemnity_guarantee + + '
                                  ' + + lan.site.set_ssl.refund_failure + + '
                                  ' + + lan.site.set_ssl.official_use + + '
                                  \ +
                                  \ + \ +
                                  \ + \ + \ + \ +
                                  ' + + lan.public_backup.domain + + '' + + lan.site.set_ssl.certificate_type + + '' + + lan.site.set_ssl.expire_date + + '' + + lan.public.status + + '' + + lan.public.action + + '
                                  ' + + lan.site.set_ssl.get_certificate_list + + '...
                                  \ +
                                  \ +
                                    \ +
                                  • ' + + lan.site.set_ssl.bus_tip1 + + '
                                  • \ +
                                  • ' + + lan.site.set_ssl.bus_tip2 + + '
                                  • \ +
                                  • ' + + lan.site.set_ssl.bus_tip3 + + '
                                  • \ +
                                  • ' + + lan.site.set_ssl.bus_tip4 + + '
                                  • \ +
                                  ' + ); + $('.service_buy_before').click(function () { + bt.onlineService(); + }); + bt.fixed_table('ssl_order_list'); + /** + * @description 证书购买人工服务 + * @param {Object} param 支付回调参数 + * @returns void + */ + function open_service_buy(param) { + var order_info = {}, + is_check = true; + pay_ssl_layer = bt.open({ + type: 1, + title: '购买人工服务', + area: ['790px', '770px'], + skin: 'service_buy_view', + content: + '
                                  \ +
                                  \ +
                                  微信支付支付宝支付
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + 总计\ + \ +
                                  \ +
                                  \ +
                                  \ + 商品名称\ + \ +
                                  \ +
                                  \ + 下单时间\ + \ +
                                  \ +
                                  \ +
                                  微信扫一扫支付
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  支付成功
                                  \ +
                                  \ +
                                  \ + 商品名称:\ + \ +
                                  \ +
                                  \ +
                                  \ + 商品价格:\ + \ +
                                  \ +
                                  \ + 下单时间:\ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  请打开微信扫一扫联系人工客服
                                  \ +
                                  qrcode
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  ', + success: function (layero, indexs) { + var order_wxoid = null, + qq_info = null; + + $('.guide_nav span').click(function () { + $(this).addClass('active').siblings().removeClass('active'); + $('.lib-prompt span').html($(this).index() == 0 ? '微信扫一扫支付' : '支付宝扫一扫支付'); + $('#PayQcode').empty(); + $('#PayQcode').qrcode({ + render: 'canvas', + width: 200, + height: 200, + text: $(this).index() != 0 ? order_info.alicode : order_info.wxcode, + }); + }); + reader_applay_qcode( + $.extend( + { + name: '证书安装服务', + price: param.price, + time: bt.format_data(new Date().getTime()), + }, + param + ), + function (info) { + check_applay_status(function (rdata) { + $('.order_service_check').addClass('active').siblings().removeClass('active'); + $('.order_service_check .lib-price-detailed .text-right:eq(0)').html(info.name); + $('.order_service_check .lib-price-detailed .text-right:eq(1)').html('¥' + info.price); + $('.order_service_check .lib-price-detailed .text-right:eq(2)').html(info.time); + $('#ssl_tabs .on').click(); + //人工客服二维码 + $('#contact_qcode').qrcode({ + render: 'canvas', + width: 120, + height: 120, + text: 'https://work.weixin.qq.com/kfid/kfc9151a04b864d993f', + }); + //缩小展示窗口 + $('.service_buy_view') + .width(690) + .height(350) + .css({ + //设置最外层弹窗大小 + left: (document.body.clientWidth - 690) / 2 + 'px', + top: (document.body.clientHeight - 350) / 2 + 'px', + }); + }); //检测支付状态 + } + ); //渲染二维码 + + function reader_applay_qcode(data, callback) { + order_wxoid = data.wxoid; + qq_info = data.qq; + order_info = data; + + $('#PayQcode').empty().qrcode({ + render: 'canvas', + width: 240, + height: 240, + text: data.wxcode, + }); + $('.price-txt .sale-price').html(data.price); + $('.lib-price-detailed .info:eq(0) span:eq(1)').html(data.name); + $('.lib-price-detailed .info:eq(1) span:eq(1)').html(data.time); + if (typeof data.qq != 'undefined') { + $('.order_pay_btn a:eq(0)').attr({ + href: data.qq, + target: '_blank', + }); + } else { + $('.order_pay_btn a:eq(0)').remove(); + } + if (callback) callback(data); + } + + function check_applay_status(callback) { + bt.send( + 'get_wx_order_status', + 'auth/get_wx_order_status', + { + wxoid: order_wxoid, + }, + function (res) { + if (res.status) { + is_check = false; + if (callback) callback(res); + } else { + if (!is_check) return false; + setTimeout(function () { + check_applay_status(callback); + }, 2000); + } + } + ); + } + }, + cancel: function (index) { + if (is_check) { + if (confirm('当前正在支付订单,是否取消?')) { + layer.close(index); + is_check = false; + } + return false; + } + }, + }); + } + /** + * @description 对指定表单元素的内容进行效验 + * @param {Object} el jqdom对象 + * @param {String} name 表单元素name名称 + * @param {*} value 表单元素的值 + * @returns 返回当前元素的值 + */ + function check_ssl_user_info(el, name, value, config) { + el.css('borderColor', '#ccc'); + var status; + switch (name) { + case 'domains': + el = site.domain_dns_type == 'multi' ? $('.dns_domains_multi_view') : el; + value = bt.strim(value).replace(/\n*$/, ''); + var list = value.split('\n'); + if (value == '') { + set_info_tips(el, { msg: 'The domain name cannot be empty!', color: 'red' }); + status = false; + } + if (!Array.isArray(list)) list = [list]; + $.each(list, function (index, item) { + if (bt.check_domain(item)) { + var type = item.indexOf(), + index = null; + if (config.code.indexOf('multi') > -1) index = 0; + if (config.code.indexOf('wildcard') > -1) index = 1; + if (config.code.indexOf('wildcard') > -1 && config.code.indexOf('multi') > -1) index = 2; + switch (index) { + case 0: + if (list.length > config.limit) { + set_info_tips(el, { msg: lan.site.set_ssl.more_cert_tip1(config.limit), color: 'red' }); + status = false; + } else if (list.length == 1) { + set_info_tips(el, { msg: lan.site.set_ssl.more_cert_tip2(config.limit), color: 'red' }); + status = false; + } + break; + case 1: + if (item.indexOf('*') != 0) { + set_info_tips(el, { msg: "Wildcard domain name format error, correct writing '*.bt.cn'", color: 'red' }); + status = false; + } + break; + case 2: + if (list.length > config.limit) { + set_info_tips(el, { msg: lan.site.set_ssl.more_cert_tip1(config.limit), color: 'red' }); + status = false; + } else if (list.length == 1) { + set_info_tips(el, { msg: lan.site.set_ssl.more_cert_tip2(config.limit), color: 'red' }); + status = false; + } + if (item.indexOf('*') != 0) { + set_info_tips(el, { msg: "Wildcard domain name format error, correct writing '*.bt.cn'", color: 'red' }); + status = false; + } + break; + } + } else { + if (value != '') { + set_info_tips(el, { msg: '【 ' + item + ' 】' + ',Domain name format error!', color: 'red' }); + } else { + set_info_tips(el, { msg: 'The domain name cannot be empty!', color: 'red' }); + } + status = false; + } + }); + value = list; + break; + case 'state': + if (value == '') { + set_info_tips(el, { msg: 'The province cannot be empty!', color: 'red' }); + status = false; + } + break; + case 'city': + if (value == '') { + set_info_tips(el, { msg: 'Your city/county cannot be empty!', color: 'red' }); + status = false; + } + break; + case 'city': + if (value == '') { + set_info_tips(el, { msg: 'Your city/county cannot be empty!', color: 'red' }); + status = false; + } + break; + case 'organation': + if (value == '') { + set_info_tips(el, { msg: 'The company name cannot be empty, if it is an individual application, please enter your name!', color: 'red' }); + status = false; + } + break; + case 'address': + if (value == '') { + set_info_tips(el, { msg: 'Please enter the company address, cannot be empty, specific requirements see the description', color: 'red' }); + status = false; + } + break; + case 'name': + if (value == '') { + set_info_tips(el, { msg: 'User name cannot be empty!', color: 'red' }); + status = false; + } + break; + case 'email': + if (value == '') { + set_info_tips(el, { msg: 'User email address cannot be empty!', color: 'red' }); + status = false; + } + if (!bt.check_email(value)) { + set_info_tips(el, { msg: 'User email address format error!', color: 'red' }); + status = false; + } + break; + // case 'mobile': + // if (value != '') { + // if (!bt.check_phone(value)) { + // set_info_tips(el, { msg: 'User mobile phone number format error!', color: 'red' }); + // status = false; + // } + // } + // break; + // case 'phonePre': + // if (value != '') { + // var reg = /^\+\d+/ + // if (!reg.test(value)) { + // set_info_tips(el, { msg: 'User mobile phone number format error!', color: 'red' }); + // status = false; + // } + // } + // break; + default: + status = value; + break; + } + if (typeof status == 'boolean' && status === false) return false; + status = value; + return status; + } + + /** + * @description 设置元素的提示和边框颜色 + * @param {Object} el jqdom对象 + * @param {Object} config = { + * @param {String} config.msg 提示内容 + * @param {String} config.color 提示颜色 + * } + */ + function set_info_tips(el, config) { + $('html').append($('' + config.msg + '')); + layer.tips(config.msg, el, { tips: [1, config.color], time: 3000 }); + el.css('borderColor', config.color); + $('#width_test').remove(); + } + /** + * @description 更换域名验证方式 + * @param {Number} oid 域名订单ID + * @returns void + */ + function again_verify_veiw(oid, is_success) { + var loads = bt.load('Please wait while obtaining verification method...'); + bt.send('get_verify_result', 'ssl/get_verify_result', { uc_id: oid }, function (res) { + loads.close(); + var type = res.data.dcvList[0].dcvMethod; + loadT = bt.open({ + type: 1, + title: lan.site.set_ssl.ver_file(type), + area: '520px', + btn: [lan.public.edit, lan.public.cancel], + content: + '
                                  Verification mode
                                  \ +
                                    ' + + lan.site.set_ssl.file_ver_tip + + '
                                  \ +
                                  ', + success: function (layero, index) { + var _option_list = { 'File Validation (HTTP)': 'HTTP_CSR_HASH', 'File Validation (HTTPS)': 'HTTPS_CSR_HASH', 'DNS Authentication (CNAME resolution)': 'CNAME_CSR_HASH' }, + _option = ''; + $.each(_option_list, function (index, item) { + _option += ''; + }); + $('select[name=file_rule]').html(_option); + }, + yes: function (index, layero) { + var new_type = $('select[name=file_rule]').val(); + if (type == new_type) return layer.msg('Duplicate authentication mode', { icon: 2 }); + var loads = bt.load('Changing the verification mode, please wait...'); + bt.send('again_verify', 'ssl/again_verify', { uc_id: oid, dcv_method: new_type }, function (res) { + loads.close(); + if (res.success) layer.close(index); + layer.msg(res.res, { icon: res.success ? 1 : 2 }); + }); + }, + }); + }); + } + /** + * @description 验证域名 + * @param {Number} oid 域名订单ID + * @param {Boolean} openTips 是否展示状态 + * @returns void + */ + function verify_order_veiw(oid, is_success, openTips) { + var loads = bt.load('Obtaining verification results, please wait...'); + bt.send('get_verify_result', 'ssl/get_verify_result', { uc_id: oid }, function (res) { + loads.close(); + if (!res.status) { + bt.msg(res); + return false; + } + if (res.status == 'COMPLETE') { + site.ssl.reload(); + return false; + } + var rdata = res.data; + var domains = [], + type = rdata.dcvList[0].dcvMethod != 'CNAME_CSR_HASH', + info = {}; + $.each(rdata.dcvList, function (key, item) { + domains.push(item['domainName']); + }); + if (type) { + info = { fileName: rdata.DCVfileName, fileContent: rdata.DCVfileContent, filePath: '/.well-known/pki-validation/', paths: res.paths, kfqq: res.kfqq }; + } else { + info = { dnsHost: rdata.DCVdnsHost, dnsType: rdata.DCVdnsType, dnsValue: rdata.DCVdnsValue, paths: res.paths, kfqq: res.kfqq }; + } + if (is_success) { + is_success({ type: type, domains: domains, info: info }); + return false; + } + loadT = bt.open({ + type: 1, + title: lan.site.set_ssl.ver_file(type), + area: '620px', + content: reader_domains_cname_check({ type: type, domains: domains, info: info }), + success: function (layero, index) { + //展示验证状态 + setTimeout(function () { + if (openTips && res.status == 'PENDING') layer.msg('Verification, please wait patiently', { time: 0, shadeClose: true, icon: 0, shade: 0.3 }); + }, 500); + var clipboard = new ClipboardJS('.parsing_info .parsing_icon'); + clipboard.on('success', function (e) { + bt.msg({ status: true, msg: 'Successful copy' }); + e.clearSelection(); + }); + clipboard.on('error', function (e) { + bt.msg({ status: true, msg: 'Copy failed, please manually ctrl+c copy!' }); + }); + $('.verify_ssl_domain').click(function () { + verify_order_veiw(oid, false, true); + layer.close(index); + }); + + $('.set_verify_type').click(function () { + again_verify_veiw(oid); + layer.close(index); + }); + + $('.return_ssl_list').click(function () { + layer.close(index); + $('#ssl_tabs span.on').click(); + }); + + // 重新验证按钮 + $('.domains_table').on('click', '.check_url_results', function () { + var _url = $(this).data('url'), + _con = $(this).data('content'); + check_url_txt(_url, _con, this); + }); + }, + }); + }); + } + + /** + * @description 重新验证 + * @param {String} url 验证地址 + * @param {String} content 验证内容 + * @returns 返回验证状态 + */ + function check_url_txt(url, content, _this) { + var loads = bt.load('Obtaining verification results, please wait...'); + bt.send('check_url_txt', 'ssl/check_url_txt', { url: url, content: content }, function (res) { + loads.close(); + var html = + 'fail[' + + res + + ']?'; + if (res === 1) { + html = 'pass'; + } + $(_this).parents('tr').find('td:nth-child(2)').html(html); + }); + } + /** + * @description 渲染验证模板接口 + * @param {Object} data 验证数据 + * @returns void + */ + function reader_domains_cname_check(data) { + var html = ''; + if (data.type) { + var check_html = + '
                                  '; + var paths = data.info.paths; + for (var i = 0; i < paths.length; i++) { + check_html += + ''; + } + check_html += '
                                  URL' + + lan.site.set_ssl.verification_result + + '' + + lan.public.action + + '
                                  ' + + paths[i].url + + '' + + (paths[i].status == 1 + ? 'pass' + : 'fail[' + + paths[i].status + + ']?') + + 'copy | open | reverify
                                  '; + html = + '
                                  \ +
                                  Please give the following domain name【 ' + + data.domains.join('、') + + ' 】Add a verification file. The verification information is as follows:
                                  \ +
                                  File location:
                                  \ +
                                  File name:
                                  copy
                                  \ +
                                  File content:
                                  copy
                                  ' + + check_html + + '
                                  · The verification result is verified by [this server], and the actual verification will be verified by [CA server]. Please wait patiently
                                  · Please ensure that all items in the above list are successfully verified and click [Verify domain name] to submit verification again
                                  · If the authentication fails for a long time, please change it to [DNS authentication] through [Modify Authentication method].
                                  · SSL Adds the file authentication mode ->> View the tutorial
                                  \ +
                                  \ +
                                  '; + } else { + html = + '
                                  \ +
                                  Please give the following domain name【 ' + + data.domains.join('、') + + ' 】add“' + + data.info.dnsType + + '”the parsing parameters as follows:
                                  \ +
                                  Host record:
                                  copy
                                  \ +
                                  Record type:
                                  \ +
                                  Record value:
                                  copy
                                  \ +
                                  · The verification result is verified by [this server], and the actual verification will be verified by [CA server]. Please wait patiently
                                  · Please ensure that all items in the above list are successfully verified and click [Verify domain name] to submit verification again
                                  · If the authentication fails for a long time, please change it to [DNS authentication] through [Modify Authentication method].
                                  · How to add domain name resolution,And consult the server operator
                                  · How do I verify commercial certificates?
                                  \ +
                                  \ +
                                  '; + } + return html; + } + // 购买证书信息 + function pay_ssl_business() { + var order_info = {}, + user_info = {}, + is_check = false; + var loadT = bt.load(lan.site.set_ssl.get_apply_cert); + bt.send('get_product_list_v2', 'ssl/get_product_list_v2', {}, function (rdata) { + loadT.close(); + var res = rdata.res; + var dataLength = res['data'] && res.data.length, + data_list = res.data, + list = [], + prompt_msg = res.info; + bt.open({ + type: 1, + title: lan.site.set_ssl.buy_cert, + area: ['810px', '832px'], + skin: 'layer-business-ssl', + content: + '\ +
                                  \ +
                                  \ + \ + ' + + lan.site.set_ssl.bus_cert_tip + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.domain_num + + '\ +
                                  \ +
                                  \ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ +

                                  ' + + lan.site.set_ssl.select_domain_num + + '

                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.cert_class + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.ov_cert + + '
                                  \ +
                                  ' + + lan.site.set_ssl.recom_enterprise + + '
                                  \ +
                                  \ +
                                  \ + Hot\ +
                                  ' + + lan.site.set_ssl.dv_cert + + '
                                  \ +
                                  ' + + lan.site.set_ssl.recom_pseson + + '
                                  \ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.ev_cert + + '
                                  \ +
                                  ' + + lan.site.set_ssl.recom_large + + '
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.cert_brand + + '\ +
                                  \ +
                                  \ +
                                  Positive
                                  \ +
                                  sslTrus
                                  \ +
                                  CFCA
                                  \ +
                                  Digicert
                                  \ +
                                  GeoTrust
                                  \ +
                                  Sectigo
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.cert_type + + '\ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.one_domain + + '
                                  \ +
                                  ' + + lan.site.set_ssl.more_domain + + '
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.pur_period + + '\ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.num_year_test(1) + + '
                                  \ +
                                  ' + + lan.site.set_ssl.num_year_test(2) + + '
                                  \ +
                                  ' + + lan.site.set_ssl.num_year_test(3) + + '
                                  \ +
                                  ' + + lan.site.set_ssl.num_year_test(4) + + '
                                  \ +
                                  ' + + lan.site.set_ssl.num_year_test(5) + + '
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.deploy_service + + '\ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.no_necess + + '
                                  \ +
                                  ' + + lan.site.set_ssl.deploy_service + + '
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.goods_include + + ':
                                  \ +
                                  ' + + lan.site.set_ssl.total_cost + + ':
                                  \ + $278.66/1year(' + + lan.site.set_ssl.in_service + + ')\ +
                                  \ +
                                  Original price$342/1year
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.wechat_pay + + '\ + ' + + lan.site.set_ssl.Alipay_pay + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.total + + '\ + $\ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.tar_name + + '\ + \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.order_time + + '\ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.wechat_swipe + + '\ +
                                  \ +
                                  \ +
                                  \ +
                                  ' + + lan.site.set_ssl.pay_sus + + '
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.tar_name + + '\ + \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.com_price + + '\ + \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.order_time + + '\ + \ +
                                  \ +
                                  \ + \ +
                                    \ +
                                  • ' + + lan.site.set_ssl.buy_cert_tip1 + + '
                                  • \ +
                                  \ +
                                  \ +
                                  \ +
                                  ', + //
                                  '+ lan.site.set_ssl.deploy_service +'
                                  \ + //
                                  '+ lan.site.set_ssl.service_secret +'
                                  \ + //
                                • 如果已购买人工服务,请点击“人工服务”咨询帮助。
                                • \ + success: function (layero, indexs) { + $.getScript('https://js.stripe.com/v3/'); + + var numBtn = $('.domain_number_reduce,.domain_number_add'), + ssl_type_item = $('.ssl_type_item'), + ssl_brand_item = $('.ssl_brand_item'), + ssl_service_item = $('.ssl_service_item'), + ssl_price_item = $('.ssl_price_item'), + ssl_year_item = $('.ssl_year_item'), + business_brand_list = $('.business_brand_list'), + input = $('.domain_number_input'), + ssl_type_tips = $('.ssl_type_tips'), + ssl_brand_tips = $('.ssl_brand_tips'), + ssl_year_tips = $('.ssl_year_tips'), + ssl_price_tips = $('.ssl_price_tips'), + ssl_service_unit = $('.ssl_service_unit'), + ssl_service_tips = $('.ssl_service_tips'); + var dataInfo = [], + ylist = [], + is_single = false, //是否存在单域名 + is_worldxml = false, //是否存在泛域名 + year = 1, + serviceprice = 0, + install = 0, + add_domain_number = 0, + order_id = null, + qq_info = null; + + $('.ssl-service').click(function () { + bt.onlineService(); + }); + + // 数量加减 + numBtn.click(function () { + var type = $(this).data('type'), + reduce = input.prev(), + add = input.next(), + min = 1, + max = 99, + input_val = parseInt(input.val()); + if ($(this).hasClass('is_disable')) { + layer.msg(type === 'reduce' ? 'The number of current domain names cannot be 0' : 'The number of domain names cannot be greater than 99'); + return false; + } + switch (type) { + case 'reduce': + input_val--; + if (min > input_val < max) { + input.val(min); + } + break; + case 'add': + input_val++; + if (min > input_val < max) { + input.val(input_val); + add.removeClass('is_disable'); + } + if (input_val == max) $(this).addClass('is_disable'); + break; + } + if (input_val == min) { + reduce.addClass('is_disable'); + } else if (input.val() == max) { + add.addClass('is_disable'); + } else { + reduce.removeClass('is_disable'); + add.removeClass('is_disable'); + } + + reader_product_info({ current_num: parseInt(input_val) }); + }); + $('.domain_number_input').on('input', function () { + var _input = $(this), + input_val = parseInt(_input.val()), + input_min = 1, + input_max = 99, + reduce = _input.prev(), + add = _input.next(); + if (input_val <= input_min) { + _input.val(input_min); + reduce.addClass('is_disable'); + } else if (input_val >= input_max) { + input.val(input_max); + add.addClass('is_disable'); + } else { + reduce.removeClass('is_disable'); + add.removeClass('is_disable'); + } + if (_input.val() == '') { + _input.val(input_min); + input_val = input_min; + reduce.addClass('is_disable'); + } + reader_product_info({ current_num: parseInt(_input.val()) }); + }); + + function automatic_msg() { + layer.msg('The current certificate brand does not support multi-domain certificate, has automatically switched to the supported certificate brand for you!'); + } + + //总计费用信息 + function reader_product_info(config) { + config.current_num = config.current_num !== '' ? config.current_num : 1; + add_domain_number = config.current_num; + input.val(config.current_num !== '' ? config.current_num : 1); + var p_index = $('.ssl_price_item.active').index(); //证书类型下标 + var year_list = ylist.filter(function (s) { + return p_index ? s.code.indexOf('wildcard') > -1 : s.code.indexOf('wildcard') === -1; + }); + var is_flag = year_list.some(function (s) { + return s.code.indexOf('multi') > -1 || s.brand === 'Digicert'; + }); + if (p_index) { + ssl_type_item.eq(2).addClass('disabled'); + } else { + ssl_type_item.eq(2).removeClass('disabled'); + } + if (input.val() > 1) { + // disabled + //证书类型禁用 + var is_type_disabled = ylist + .filter(function (s) { + return !p_index ? s.code.indexOf('wildcard') > -1 : s.code.indexOf('wildcard') === -1; + }) + .some(function (s) { + return s.code.indexOf('multi') > -1 || s.brand === 'Digicert'; + }); + if (!is_type_disabled) { + ssl_price_item.eq(p_index ? 0 : 1).addClass('disabled'); + } else { + ssl_price_item.eq(p_index ? 0 : 1).removeClass('disabled'); + } + //证书品牌禁用 + for (var i = 0; i < ssl_brand_item.length; i++) { + if (ssl_brand_item.eq(i).css('display') !== 'none') { + var brand_data = ssl_brand_item.eq(i).data(); + if (p_index) { + if (!brand_data.is_multi_w) ssl_brand_item.eq(i).addClass('disabled'); + else ssl_brand_item.eq(i).removeClass('disabled'); + } else { + if (!brand_data.is_multi) ssl_brand_item.eq(i).addClass('disabled'); + else ssl_brand_item.eq(i).removeClass('disabled'); + } + } + } + } else { + ssl_price_item.eq(p_index ? 0 : 1).removeClass('disabled'); + ssl_brand_item.removeClass('disabled'); + } + if (!is_flag) { + if (input.val() > 1) { + for (var i = 0; i < ssl_brand_item.length; i++) { + if (ssl_brand_item.eq(i).css('display') !== 'none') { + var brand_data = ssl_brand_item.eq(i).data(); + if (p_index) { + if (brand_data.is_multi_w) { + automatic_msg(); + return ssl_brand_item.eq(i).click(); + } + } else { + if (brand_data.is_multi) { + automatic_msg(); + return ssl_brand_item.eq(i).click(); + } + } + } + } + } + } + //选中的证书信息 + var data_info = year_list.filter(function (s) { + return $('.domain_number_input').val() > 1 ? (s.brand === 'Digicert' ? s.code.indexOf('multi') === -1 : s.code.indexOf('multi') > -1) : s.code.indexOf('multi') === -1; + })[0]; + dataInfo = data_info; + dataInfo['current_num'] = config['current_num']; + //服务费 + var service_price = [0, data_info.deploy_price / 100 || 0, data_info.install_price_v2 || 0]; + for (var i = 0; i < service_price.length; i++) { + ssl_service_item.eq(i).data('serviceprice', service_price[i]); + // if(i == 1){ + // ssl_service_item.eq(i).data('serviceprice', 28.9); + // } + } + serviceprice = $('.ssl_service_item.active').data('serviceprice'); + if ($('.ssl_service_item.active').index()) ssl_service_unit.html('' + lan.site.set_ssl.deploy_cost + '$' + serviceprice + '/1 Time'); + else ssl_service_unit.html(''); + var cur_num = (config.current_num < data_info.num ? data_info.num : config.current_num) - data_info.num; + var p_price = parseFloat(Number(serviceprice) * 100 + (data_info.price + data_info.add_price * cur_num) * year).toFixed(2); + if (config.current_num > 1 || data_info.brand === 'Digicert') { + if (data_info.brand !== 'Digicert') ssl_price_item.eq(0).text('Universal domain'); + if (data_info.brand === 'Digicert') ssl_price_item.eq(0).text('Single domain'); + $('.domain_number_tips').html(lan.site.set_ssl.default_over(data_info.num) + '$' + (data_info.add_price / 100).toFixed(2) + '/one/year'); + } else { + ssl_price_item.eq(0).text('Single domain'); + $('.domain_number_tips').empty(); + } + var pp_html = '$' + Number(p_price) / 100 + '/' + year + 'year' + ($('.ssl_service_item.active').index() ? '(' + lan.site.set_ssl.in_service + ')' : ''), + op_html = + lan.site.set_ssl.or_price + + '$' + + parseFloat(parseFloat((Number(serviceprice) * 100 + (data_info.other_price + data_info.add_price * cur_num) * year) / 100).toFixed(2)) + + '/' + + year + + 'year'; + var price_pack = parseFloat(parseFloat(data_info.price * year).toFixed(2)), + price_extra = data_info.add_price * cur_num * year; + $('.business_ssl_btn .bname span').html( + lan.site.set_ssl.default_domain(data_info.num) + + '$' + + price_pack / 100 + + '/' + + year + + 'year' + + (cur_num ? lan.site.set_ssl.over_domain(cur_num) + '$' + price_extra / 100 + '/' + year + 'year' : '') + ); + $('.business_ssl_btn .present_price').html(pp_html); + $('.business_ssl_btn .original_price').html(op_html); + } + + setTimeout(function () { + ssl_type_item.eq(1).click(); + }, 50); + //证书分类切换 + ssl_type_item.click(function () { + if ($(this).hasClass('disabled')) return layer.msg(lan.site.set_ssl.dis_tip); + if (!$(this).hasClass('active')) $(this).addClass('active').siblings().removeClass('active'); + //证书类型 + var type = $(this).data('type'), + brand_list = []; //品牌类型 + list = dataLength + ? data_list.filter(function (s) { + return s.type.indexOf(type) > -1; + }) + : []; + var type_tips_list = prompt_msg['type'][type.toLowerCase()], + type_tips = ''; + for (var i = 0; i < type_tips_list.length; i++) { + type_tips += '

                                  ' + type_tips_list[i] + '

                                  '; + } + ssl_type_tips.html(type_tips); //提示信息 + $.each(list, function (i, item) { + brand_list.push(item.brand); + }); + brand_list = Array.from(new Set(brand_list)); //去重 + var recommend = prompt_msg['recommend'][type.toLowerCase()]; + business_brand_list.find('em').remove(); + business_brand_list.find('[data-type="' + prompt_msg['recommend'][type.toLowerCase()] + '"]').prepend('Hot'); + ssl_brand_item.hide(); + for (var i = 0; i < brand_list.length; i++) { + business_brand_list.find('[data-type="' + brand_list[i] + '"]').show(); + var b_list = list.filter(function (s) { + return s.brand === brand_list[i]; + }); + //品牌是否存在多域名 + var is_multi = b_list.some(function (s) { + return (s.code.indexOf('wildcard') === -1 && s.code.indexOf('multi') > -1) || s.brand === 'Digicert'; + }); + var is_multi_w = b_list.some(function (s) { + return (s.code.indexOf('wildcard') > -1 && s.code.indexOf('multi') > -1) || s.brand === 'Digicert'; + }); + business_brand_list.find('[data-type="' + brand_list[i] + '"]').data({ is_multi: is_multi, is_multi_w: is_multi_w }); + } + business_brand_list.find('[data-type="' + recommend + '"]').click(); + }); + + //证书品牌 + ssl_brand_item.click(function () { + var p_index = $('.ssl_price_item.active').index(); + if ($(this).hasClass('disabled')) { + if (p_index !== -1) { + for (var i = 0; i < ssl_brand_item.length; i++) { + if (ssl_brand_item.eq(i).css('display') !== 'none') { + var brand_data = ssl_brand_item.eq(i).data(); + if (p_index) { + if (brand_data.is_multi_w) return ssl_brand_item.eq(i).click(); + } else { + if (brand_data.is_multi) return ssl_brand_item.eq(i).click(); + } + } + } + } + return layer.msg('The current certificate brand does not support multi-domain wildcard certificates. Please select another brand certificate'); + } + if (!$(this).hasClass('active')) $(this).addClass('active').siblings().removeClass('active'); + var type = $(this).data('type'), + years_list = [], + max_years = 0, + years_html = '', + cert_html = ''; + ylist = list.filter(function (s) { + return s.brand.indexOf(type) > -1; + }); + brand_type = $('.ssl_type_item.active').data('type'); + var brand_tips_list = prompt_msg['brand'][type === 'Positive' || type === 'sslTrus' ? type : type.toLowerCase()], + brand_tips = ''; + for (var i = 0; i < brand_tips_list.length; i++) { + brand_tips += '

                                  ' + brand_tips_list[i] + '

                                  '; + } + ssl_brand_tips.html(brand_tips); //提示信息 + $.each(ylist, function (i, item) { + years_list.push(item.max_years); + }); + //是否存在单域名/泛域名按钮 + is_single = ylist.some(function (s) { + return s.code.indexOf('wildcard') === -1; + }); + is_worldxml = ylist.some(function (s) { + return s.code.indexOf('wildcard') > -1; + }); + if (is_single) { + ssl_price_item.eq(0).show(); + } else { + ssl_price_item.eq(0).hide(); + } + if (is_worldxml) { + ssl_price_item.eq(1).show(); + } else { + ssl_price_item.eq(1).hide(); + } + max_years = Array.from(new Set(years_list))[0]; + for (var i = 0; i < 5; i++) { + if (i < max_years) { + ssl_year_item.eq(i).show(); + } else { + ssl_year_item.eq(i).hide(); + } + } + //证书类型点击 + var p_index = $('.ssl_price_item.active').index(); + ssl_price_item.eq(p_index !== -1 ? (!is_worldxml && p_index === 1 ? 0 : p_index) : 0).click(); + }); + //证书类型 + $('.business_ssl_form').on('click', '.ssl_price_item', function () { + if ($(this).hasClass('disabled')) return layer.msg(lan.site.set_ssl.dis_tip); + if (!$(this).hasClass('active')) $(this).addClass('active').siblings().removeClass('active'); + var price_tips_list_0 = [lan.site.set_ssl.single_tip1, lan.site.set_ssl.single_tip2], + price_tips_list_1 = [lan.site.set_ssl.more_tip1, lan.site.set_ssl.more_tip2]; + var price_tips_list = $(this).index() ? price_tips_list_1 : price_tips_list_0, + price_tips = ''; + for (var i = 0; i < price_tips_list.length; i++) { + price_tips += '

                                  ' + price_tips_list[i] + '

                                  '; + } + ssl_price_tips.html(price_tips); + //购买年限点击 + var y_index = $('.ssl_year_item.active').index(); + + ssl_year_item.eq(y_index !== -1 && $('.ssl_year_item.active').css('display') !== 'none' ? y_index : 0).click(); + }); + //购买年限 + $('.business_ssl_form').on('click', '.ssl_year_item', function () { + if (!$(this).hasClass('active')) $(this).addClass('active').siblings().removeClass('active'); + year = $(this).data('year'); + var year_tips_list = prompt_msg['times'][year + '_year'], + year_tips = ''; + for (var i = 0; i < year_tips_list.length; i++) { + year_tips += '

                                  ' + year_tips_list[i] + '

                                  '; + } + ssl_year_tips.html(year_tips); //提示信息 + + var ser_index = $('.ssl_service_item.active').index(); + ssl_service_item.eq(ser_index !== -1 ? ser_index : 1).click(); + }); + + //部署服务点击 + ssl_service_item.click(function () { + if (!$(this).hasClass('active')) $(this).addClass('active').siblings().removeClass('active'); + var index = $(this).index(); + (serviceprice = $(this).data('serviceprice')), (install = $(this).data('install')); + ssl_service_tips.html( + index + ? index === 1 + ? 'aaPanel provides manual deployment certificate deployment services from China time 9:00 -18:30 to help customers troubleshoot deployment certificate validity problems and quickly go online' + : '宝塔提供9:00 - 24:00的人工部署国密算法证书部署服务,帮助客户排查部署证书部署生效问题,快速上线' + : '' + ); + var value = $('.domain_number_input').val(); + reader_product_info({ current_num: value === '' ? value : parseInt(value) }); + }); + + //购买事件 + $('.business_ssl_pay').click(function () { + var loadT = bt.load('Payment order is being generated, please wait...'), + num = 0; + add_domain_number = input.val(); + if (dataInfo.add_price !== 0) num = parseInt(dataInfo.current_num - dataInfo.num); + bt.send( + 'apply_cert_order_pay', + 'ssl/apply_cert_order_pay', + { + pdata: JSON.stringify({ + pid: dataInfo.pid, + deploy: install, + years: year, + num: num, + }), + }, + function (rdata) { + loadT.close(); + if (rdata.success) { + is_check = true; + var res = rdata.res; + + var stripe = Stripe(res.stripe_public_key); + stripe.redirectToCheckout({ sessionId: res.session_id }); + } + } + ); + }); + //支付切换 + $('.guide_nav span').click(function () { + var price = $('.business_ssl_btn .present_price span').text(), + is_wx_quota = parseFloat(price) >= 6000; + if ($(this).index() === 0 && is_wx_quota) { + layer.msg('Wechat single transaction limit 6000 yuan, please use Alipay payment', { + icon: 0, + }); + } else { + $(this).addClass('active').siblings().removeClass('active'); + $('.lib-prompt span').html($(this).index() == 0 ? lan.site.set_ssl.wechat_swipe : 'Pay with a swipe on Alipay'); + $('#PayQcode').empty(); + $('#PayQcode').qrcode({ + render: 'canvas', + width: 200, + height: 200, + text: $(this).index() != 0 ? order_info.alicode : order_info.wxcode, + }); + } + }); + $('.order_pay_btn a').click(function () { + switch ($(this).data('type')) { + case 'info': + confirm_certificate_info( + $.extend(dataInfo, { + oid: order_id, + qq: qq_info, + install: install ? true : false, + limit: add_domain_number, + }) + ); + break; + case 'clear': + layer.close(indexs); + break; + } + }); + }, + cancel: function (index) { + if (is_check) { + if (confirm('The order is currently being paid, would you like to cancel it?')) { + layer.close(index); + is_check = false; + } + return false; + } + }, + }); + }); + } + // 确认证书信息 + function confirm_certificate_info(config) { + var userLoad = bt.load('Getting user info, please wait...'); + bt.send('get_cert_admin', 'ssl/get_cert_admin', {}, function (rdata) { + var res = rdata.res; + userLoad.close(); + var html = ''; + var isWildcard = config.code.indexOf('wildcard') > -1; + var isMulti = config.code.indexOf('multi') > -1; + if (typeof pay_ssl_layer != 'undefined') pay_ssl_layer.close(); + if (config.code.indexOf('multi') > -1) { + if (isWildcard) { + placeholder = lan.site.set_ssl.more_cert_pl1(config.limit); + } else { + placeholder = lan.site.set_ssl.more_cert_pl2(config.limit); + } + site.domain_dns_type = 'multi'; + html = + '
                                  Select a website domain name
                                  '; + } else { + if (isWildcard) { + placeholder = 'single domain wildcard certificate, for example, *.bt.cn'; + } else { + placeholder = 'single domain name certificate, for example, www.bt.cn'; + } + site.domain_dns_type = 'one'; + html = + '
                                  '; + } + bt.open({ + type: 1, + title: lan.site.set_ssl.inpro_cert_info, + area: '640px', + content: + '
                                  \ +
                                  \ + Cert Info\ +
                                  \ + ' + + config.title + + (config.limit > 1 ? ',Contains ' + config.limit + 'domain names' : '') + + '\ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.domain_name + + '\ +
                                  ' + + html + + '
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.checking_mode + + '\ +
                                  \ +
                                  \ + \ + \ + \ +
                                  \ +
                                  \ + \ + \ + \ +
                                  \ +
                                  \ + \ + \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ + Created with Pixso.\ + \ + \ + \ + \ +
                                  \ + Show user Info\ + \ +
                                  \ +
                                  \ + Local area\ +
                                  \ + \ + \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.site.set_ssl.address + + '\ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + Company name\ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + Name\ +
                                  \ + \ + \ +
                                  \ +
                                  \ +
                                  \ + Email\ +
                                  \ + \ +
                                  \ +
                                  \ +
                                  \ + ' + + lan.public_backup.mobile_phone_or_email + + '\ +
                                  \ + \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                    \ +
                                  • Wildcard certificates support only DNS authentication
                                  • \ +
                                  • Multiple domain names support only DNS authentication
                                  • \ +
                                  • https or http authentication: Ensure that the website can be accessed through http/https
                                  • \ +
                                  • The domain name prefix is www, reminding users to resolve the upper-level root domain name, such as www.bt.cn, please ensure that the resolution of bt.cn
                                  • \ +
                                  • How do I verify commercial certificates?
                                  • \ +
                                  \ +
                                    \ +
                                  • OV/EV certificate application process conditions:
                                  • \ +
                                  • 1、Fill in the website authentication information (file authentication or DNS authentication)
                                  • \ +
                                  • 2、Complete the email authentication, and improve the email content according to the mail sent by CA (just fill in Chinese)
                                  • \ +
                                  • 3、Enterprise check or love enterprise check, Baidu map, 114best can query relevant enterprise information, and the company name and company address exactly match
                                  • \ +
                                  • 4、The phone number left by the company or other platforms can guarantee that you can hear the CA certification phone from Monday to Friday (7:00-15:00), the phone number belongs to the United States, please pay attention to answer.
                                  • \ +
                                  \ +
                                  ', + check_dns_interface: function (callback) { + var val = $('input[name="dcvMethod"]:radio:checked').val(); + if (val !== 'CNAME_CSR_HASH') { + if (callback) callback(); + return; + } + var dns_val = $('.dns_interface_select').val(); + if (dns_val == 'dns') { + if (callback) callback(); + } else { + bt.site.get_dns_api(function (res) { + var config; + for (var i = 0; i < res.length; i++) { + if (res[i].name == dns_val) { + config = res[i]; + break; + } + } + var check = true; + var title = ''; + if (config && config.data) { + for (var j = 0; j < config.data.length; j++) { + if (config.data[j].value === '') { + check = false; + title = config.title; + break; + } + } + } + if (check) { + if (callback) callback(); + } else { + layer.msg('No key is configured for the selected DNS interface [' + title + ']', { icon: 2 }); + } + }); + } + }, + success: function (layero, index) { + $.ajax({ + type: 'GET', + url: '/static/js/countryCode.json', + data: 'data', + dataType: 'JSON', + success: function (data) { + countryList = data; + var _option = ''; + var couOp = ''; + $.each(data, function (index, item) { + _option += ''; + couOp += ''; + }); + var node_pid = $('.pre[name=phonePre]'); + var countrySelect = $('.cou[name=country]'); + node_pid.html(_option); + node_pid.val(res.tel_prefix); + countrySelect.html(couOp); + countrySelect.val(res.country); + }, + }); + $('.basics-clid').hide(); + if (config.code.indexOf('multi') > -1) { + $('#CNAME_CSR_HASH').click(); + $('#HTTP_CSR_HASH,#HTTPS_CSR_HASH').attr('disabled', 'disabled'); + + // dns_domains_add_block下的input焦点触发时,隐藏selct_site_btn + $('.dns_domains_add_block input').on('focus blur keyup', function (e) { + e = e || window.event; + switch (e.type) { + case 'focus': + $('.selct_site_btn').hide(); + break; + case 'blur': + $('.selct_site_btn').css('display', 'inline-block'); + break; + case 'keyup': + if (e.keyCode != 13 && e.type == 'keyup') return false; + var val = $(this).val(); + if (!bt.check_domain(val)) return layer.msg('Domain name format error', { icon: 2 }); + site.domain_dns_list.push(val); + $(this).val(''); // 清空输入框 + site.refresh_dns_interface(); + break; + } + }); + } + $('.perfect_ssl_info').data('code', config.code); + var _this_layer = this; + bt_tools.send({ url: '/site?action=GetDnsApi' }, function (data) { + site.dns_configured_table = site.data_treating(data); + site.add_dns_interface(); + // 基础信息隐藏显示 + $('.basics-info').click(function () { + if ($('.basics-info').hasClass('active')) { + $('.basics-info').removeClass('active'); + $('.basics-title').text('Show user Info'); + $('.basics-clid').hide(); + } else { + $('.basics-info').addClass('active'); + $('.basics-title').text('Hide user Info'); + $('.basics-clid').show(); + //config.code.含有ov、ev时,隐藏公司详细地址 + // if(config.code.indexOf('ov') == -1 || config.code.indexOf('ev') == -1){ + // $('.basics-clid').eq(1).hide(); + // } + } + $('.ssl_help_info').toggle(); + }); + // 判断基础信息中是否存在空置,将基础信息自动显示 + $('.basics-clid input').each(function () { + // isMulti为false时,不需要验证公司详细地址 + if (!isMulti && $(this).attr('name') == 'address') return true; + if ($(this).val() == '') { + $('.basics-info').click(); + return false; + } + }); + + // 验证方式 + $('input[name="dcvMethod"]').change(function () { + var val = $(this).val(); + if (val == 'CNAME_CSR_HASH') { + site.add_dns_interface(); + } else { + site.remove_dns_interface(); + } + }); + + // 公司详细地址联动 + $('.perfect_ssl_info').on('input', 'input[name="state"], input[name="city"]', function (e) { + var is_ovev = config.code.indexOf('ov') > -1 || config.code.indexOf('ev') > -1; + if (!is_ovev) { + var state = $('.perfect_ssl_info input[name="state"]').val(); + var city = $('.perfect_ssl_info input[name="city"]').val(); + $('.perfect_ssl_info input[name="address"]').val(state + city); + } + }); + $('.perfect_ssl_info') + .on('focus', 'input[type=text],textarea', function () { + $(this).focus() + // var placeholder = $(this).attr('placeholder'); + // $('html').append($('' + placeholder + '')); + // $(this).attr('data-placeholder', placeholder); + // layer.tips(placeholder, $(this), { tips: [1, '#20a53a'], time: 0 }); + // $(this).attr('placeholder', ''); + // $('#width_test').remove(); + }) + .on('blur', 'input[type=text],textarea', function () { + var name = $(this).attr('name'), + val = $(this).val(); + layer.closeAll('tips'); + $(this).attr('placeholder', $(this).attr('data-placeholder')); + check_ssl_user_info($(this), name, val, config); + }); + function btserializeDiv(div) { + var result = {}; + var elements = div.querySelectorAll('input, select, textarea'); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + var name = element.name; + var value = element.value; + if (name && !element.disabled) { + result[name] = value; + } + } + return result; + } + $('.submit_ssl_info').on('click', function () { + var data = {}, + // form = $('.perfect_ssl_info').serializeObject(), + form = btserializeDiv(document.querySelector('.perfect_ssl_info')), + is_ovev = config.code.indexOf('ov') > -1 || config.code.indexOf('ev') > -1, + loadT = null; + // var reg = /^[\u4E00-\u9FA5]+$/; + // if (form.name.length < 2 || !reg.test(form.name)) return layer.msg('The name shall be Chinese and two characters or more in length'); + $('.perfect_ssl_info') + .find('input,textarea') + .each(function () { + var name = $(this).attr('name'), + value = $(this).val(), + value = check_ssl_user_info($(this), name, value, config); + if (typeof value === 'boolean') { + form = false; + return false; + } + form[name] = value; + }); + form.phonePre = $('.perfect_ssl_info [name=phonePre]').val(); + form.phonePre = $('.perfect_ssl_info [name=phonePre]').val(); + if (typeof form == 'boolean') return false; + delete form['undefined']; // 删除undefined + form['domains'] = site.domain_dns_list; + + if (!is_ovev) form['address'] = form['state'] + form['city']; + if (typeof config.limit == 'undefined') config.limit = config.num; + if (form.domains.length < config.limit) { + bt.confirm({ title: 'Tips', msg: 'The current certificate supports ' + config.limit + ' domain names. Do you want to continue to add domain names?' }, function () { + req(true); + }); + return false; + } + req(true); + function req(verify) { + if (verify) { + bt.open({ + title: 'The user information is confirmed twice', + area: ['600px'], + btn: ['Continue to submit', 'Cancel'], + content: + '
                                  ' + + '
                                  ' + + 'Local area' + + '
                                  ' + + '' + + '' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + 'Address' + + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + 'Company name' + + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + 'Name' + + '
                                  ' + + '' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + 'Email' + + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '' + + lan.public_backup.mobile_phone_or_email + + '' + + '
                                  ' + + '' + + '' + + '
                                  ' + + '
                                  ' + + '
                                ' + + '
                                ', + yes: function () { + var isVerify = true; + $('.certificate_confirm') + .find('input') + .each(function () { + var name = $(this).attr('name'), + value = $(this).val(), + value = check_ssl_user_info($(this), name, value, config); + if (typeof value === 'boolean') { + form = false; + return false; + } + form[name] = value; + }); + req(false); + }, + success: function () { + if (countryList.length > 0) { + var _option = ''; + var couOp = ''; + $.each(countryList, function (index, item) { + _option += ''; + couOp += ''; + }); + var node_pid = $('[name=phonePre]'); + var countrySelect = $('[name=country]'); + node_pid.html(_option); + node_pid.val(form.phonePre); + countrySelect.html(couOp); + countrySelect.val(form.country); + } + // $.ajax({ + // type: "GET", + // url: "/static/js/countryCode.json", + // data: "data", + // dataType: "JSON", + // success: function (data) { + // console.log(data); + + // } + // }) + $('.certificate_confirm [name="organation"]').change(function () { + $('.perfect_ssl_info [name="organation"]').val($(this).val()); + form.organation = $(this).val(); + }); + $('.certificate_confirm [name="address"]').change(function () { + $('.perfect_ssl_info [name="address"]').val($(this).val()); + form.address = $(this).val(); + }); + $('.checkInfo').on('click', function (e) { + window.open('https://www.qcc.com/web/search?key=' + $('.certificate_confirm [name="organation"]').val()); + }); + }, + }); + return false; + } + _this_layer.check_dns_interface(function () { + var loadT = bt.load('Please wait while submitting certificate information...'); + var auth_to = $("[name='dns_select']") ? $("[name='dns_select']").val() : ''; + bt.send( + 'apply_order_ca', + 'ssl/apply_order_ca', + { + pdata: JSON.stringify({ + pid: config.pid, + oid: config.oid, + domains: form.domains, + dcvMethod: $("[name='dcvMethod']:checked").val(), + auth_to: auth_to, + uc_id: config.uc_id, + Administrator: { + job: 'General affairs', + postCode: '523000', + country: form.country, + firstName: form.firstName, + lastName: form.name, + state: form.state, + city: form.city, + address: form.address, + organation: form.organation, + email: form.email, + tel_prefix: form.phonePre, + mobile: form.mobile, + lastName: form.name, + }, + }), + }, + function (res) { + loadT.close(); + if (typeof res.msg == 'object') { + for (var key in res.msg.errors) { + if (Object.hasOwnProperty.call(res.msg.errors, key)) { + var element = res.msg.errors[key]; + bt.msg({ + status: false, + msg: element, + }); + } + } + } else { + if (res.caa_list) { + site.show_domain_error_dialog(res.caa_list, res.msg); + } else { + bt.msg({ status: res.success, msg: res.res }); + } + } + if (res.success) { + layer.close(index); + verify_order_veiw(config.uc_id); + $('#ssl_tabs span.on').click(); + } + } + ); + }); + } + }); + + $('.check_method_item label').click(function (e) { + e.stopPropagation(); + }); + + $('.check_method_item').click(function () { + // 选中 + $(this).find('label').trigger('click'); + // 判断是否显示异常 + var show = $(this).data('show-tips'); + if (!show) return; + $(this).data('show-tips', false); + // 判断是否存在异常数据 + var data = $(this).data('error-data'); + if (!data) return; + $(this).find('.error-link').trigger('click'); + }); + + $('.check_method_item').on('click', '.error-link', function (e) { + e.stopPropagation(); + var data = $(this).parents('.check_method_item').data('error-data'); + + if ($.isPlainObject(data)) { + site.show_domain_error_dialog(data); + } + if (Array.isArray(data)) { + var html = ''; + $.each(data, function (i, item) { + html += '

                                ' + item + '

                                '; + }); + layer.msg(html, { + icon: 2, + shade: 0.3, + closeBtn: 2, + time: 0, + success: function ($layer) { + $layer.css({ 'max-width': '560px' }); + var width = $(window).width(); + var lWidth = $layer.width(); + $layer.css({ + left: (width - lWidth) / 2 + 'px', + }); + }, + }); + } + }); + + var Timer = null; + $('.CNAME_CSR_HASH,.HTTP_CSR_HASH,.HTTPS_CSR_HASH').hover( + function () { + var $this = $(this); + var data = $(this).data('error-data'); + if (data) return; + var arry = [ + 'If the website has not been filed, optional [DNS verification]', + 'If the 301, 302, forced HTTPS, and reverse proxy functions are not enabled, select HTTP', + 'If the website enables "mandatory HTTPS", please select "HTTPS verification".', + ]; + var tips = arry[$this.index()]; + clearTimeout(Timer); + Timer = setTimeout(function () { + $this.data({ + tips: layer.tips(tips, $this.find('label'), { tips: 1, time: 0 }), + }); + }, 200); + }, + function () { + clearTimeout(Timer); + layer.close($(this).data('tips')); + } + ); + }); + }, + }); + }); + } + $('.ssl_business_application').click(function () { + pay_ssl_business(); + }); + //订单证书操作 + $('.ssl_order_list') + .unbind('click') + .on('click', '.options_ssl', function () { + var type = $(this).data('type'), + tr = $(this).parents('tr'); + itemData = order_list[tr.data('index')]; + switch (type) { + case 'deploy_ssl': // 部署证书 + bt.confirm( + { + title: 'Deployment certificate', + msg: + 'Whether to deploy the certificate and whether to continue?
                                Certificate type:' + + itemData.title + + '
                                Certificate supported domain name:' + + itemData.domains.join('、') + + '
                                Deployment site name:' + + web.name + + '', + }, + function (index) { + var loads = bt.load('Please wait while certificates are deployed...'); + bt.send('set_cert', 'ssl/set_cert', { uc_id: itemData.uc_id, siteName: web.name }, function (rdata) { + layer.close(index); + $('#webedit-con').empty(); + site.set_ssl(site.web); + site.ssl.reload(); + bt.msg(rdata); + }); + } + ); + break; + case 'verify_order': // 验证订单 + verify_order_veiw(itemData.uc_id); + break; + case 'clear_order': // 取消订单 + bt.confirm( + { + title: 'Cancel order', + msg: 'Whether to cancel the order, the order domain name [' + itemData.domains.join('、') + '], whether to continue?', + }, + function (index) { + var loads = bt.load('Cancelling order, please wait...'); + bt.send('cancel_cert_order', 'ssl/cancel_cert_order', { oid: itemData.oid }, function (rdata) { + layer.close(index); + if (rdata.status) { + $('#ssl_tabs span:eq(2)').click(); + setTimeout(function () { + bt.msg(rdata); + }, 2000); + } + bt.msg(rdata); + }); + } + ); + break; + case 'perfect_user_info': //完善用户信息 + confirm_certificate_info(itemData); + break; + case 'renewal_ssl': + renewal_ssl_view(itemData); + break; + } + }); + } else { + robj.append('
                                ' + lan.site.set_ssl.no_bind + '
                                '); + var datas = [ + { title: lan.public.user, name: 'bt_username', value: '', width: '260px', placeholder: lan.public_backup.mobile_phone_or_email }, + { title: lan.public.pass, type: 'password', name: 'bt_password', value: '', width: '260px' }, + { + title: ' ', + items: [ + { + text: lan.public_backup.login, + name: 'btn_ssl_login', + type: 'button', + callback: function (sdata) { + var username = $('input[name="bt_username"]').val(); + var password = $('input[name="bt_password"]').val(); + bt.pub.login_btname(username, password, function (ret) { + if (ret.status) site.set_ssl(site.web); + }); + }, + }, + { + text: lan.site.set_ssl.register_ac, + name: 'bt_register', + type: 'button', + callback: function (sdata) { + window.open('https://www.aapanel.com/user_admin/register'); + }, + }, + ], + }, + ]; + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + robj.append( + bt.render_help([ + lan.site.set_ssl.bind_tip1 + '' + lan.site.set_ssl.click_view + '', + lan.site.set_ssl.bind_tip2, + ]) + ); + } + }); + }, + }, + { + title: "Let's Encrypt", + callback: function (robj) { + robj = $('#webedit-con .tab-con'); + // console.log(robj,'obj'); + acme.get_account_info(function (let_user) {}); + acme.id = web.id; + if (rdata.status && rdata.type == 1) { + var cert_info = ''; + if (rdata.cert_data['notBefore']) { + cert_info = + '
                                \ + ' + + lan.site.deploy_success_cret + + '' + + lan.site.try_renew_cret + + '\ + \ + ' + + lan.site.cert_brand + + '' + + rdata.cert_data.issuer + + '\ + ' + + lan.site.auth_domain + + ' ' + + (rdata.cert_data.dns ? rdata.cert_data.dns.join(', ') : '') + + '\ + ' + + lan.site.expire_time + + ' ' + + rdata.cert_data.notAfter + + '
                                '; + } + robj.append('
                                ' + cert_info + '
                                ' + lan.site.ssl_key + '' + lan.site.ssl_crt + '
                                '); + var datas = [ + { + items: [ + { name: 'key', width: '48%', height: '220px', type: 'textarea', value: rdata.key }, + { name: 'csr', width: '48%', height: '220px', type: 'textarea', value: rdata.csr }, + ], + }, + { + items: [ + { + text: lan.site.ssl_close, + name: 'btn_ssl_close', + hide: !rdata.status, + type: 'button', + callback: function (sdata) { + site.ssl.set_ssl_status('CloseSSLConf', web.name); + }, + }, + { + text: lan.site.ssl_renew, + name: 'btn_ssl_renew', + hide: !rdata.status, + type: 'button', + callback: function (sdata) { + site.ssl.renew_ssl(web.name, rdata.auth_type, rdata.index); + }, + }, + ], + }, + ]; + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + robj + .find('textarea') + .css({ + 'background-color': '#f6f6f6', + resize: 'none', + }) + .attr('readonly', true); + robj.find('[name=csr]').css('margin-right', '0'); + var helps = [lan.site.ssl_tips1, lan.site.ssl_tips2, lan.site.ssl_tips3, lan.site.ssl_tips4, lan.site.ssl_tips5]; + robj.append(bt.render_help([lan.site.ssl_help_2, lan.site.ssl_help_3])); + return; + } + bt.site.get_site_domains(web.id, function (ddata) { + var helps = [ + [lan.site.bt_ssl_help_5, lan.site.bt_ssl_help_8, lan.site.bt_ssl_help_9, lan.site.ssl_tips5], + [lan.site.dns_check_tips1, lan.site.dns_check_tips2, lan.site.dns_check_tips3, lan.site.dns_check_tips4], + ]; + var datas = [ + { + title: lan.site.checking_mode, + items: [ + { + name: 'check_file', + text: lan.site.file_check, + type: 'radio', + callback: function (obj) { + $('.checks_line').remove(); + $(obj).siblings().removeAttr('checked'); + + $('.help-info-text').html($(bt.render_help(helps[0]))); + //var _form_data = bt.render_form_line({ title: ' ', class: 'checks_line label-input-group', items: [{ name: 'force', type: 'checkbox', value: true, text: '提前校验域名(提前发现问题,减少失败率)' }] }); + //$(obj).parents('.line').append(_form_data.html); + + $('#ymlist li input[type="checkbox"]').each(function () { + if ($(this).val().indexOf('*') >= 0) { + $(this).parents('li').hide(); + } + }); + }, + }, + { + name: 'check_dns', + text: lan.site.check_dns, + type: 'radio', + callback: function (obj) { + $('.checks_line').remove(); + $(obj).siblings().removeAttr('checked'); + $('.help-info-text').html($(bt.render_help(helps[1]))); + $('#ymlist li').show(); + + var arrs_list = [], + arr_obj = {}; + bt.site.get_dns_api(function (api) { + site.dnsapi = {}; + + for (var x = 0; x < api.length; x++) { + site.dnsapi[api[x].name] = {}; + site.dnsapi[api[x].name].s_key = 'None'; + site.dnsapi[api[x].name].s_token = 'None'; + if (api[x].data) { + site.dnsapi[api[x].name].s_key = api[x].data[0].value; + site.dnsapi[api[x].name].s_token = api[x].data[1].value; + } + arrs_list.push({ title: api[x].title, value: api[x].name }); + arr_obj[api[x].name] = api[x]; + } + + var data = [ + { + title: lan.site.choose_dns, + class: 'checks_line', + items: [ + { + name: 'dns_select', + width: 'auto', + type: 'select', + items: arrs_list, + callback: function (obj) { + var _val = obj.val(); + $('.set_dns_config').remove(); + var _val_obj = arr_obj[_val]; + var _form = { + title: '', + area: '530px', + list: [], + btns: [{ title: lan.site.turn_off, name: 'close' }], + }; + + var helps = []; + if (_val_obj.data !== false) { + _form.title = lan.site.set + '【' + _val_obj.title + '】' + lan.site.interface; + if (_val_obj.help == 'How to get API Token') { + _val_obj.help = + '' + + _val_obj.help + + ''; + } + helps.push(_val_obj.help); + var is_hide = true; + for (var i = 0; i < _val_obj.data.length; i++) { + _form.list.push({ + title: _val_obj.data[i].name, + name: _val_obj.data[i].key, + value: _val_obj.data[i].value, + }); + if (!_val_obj.data[i].value) is_hide = false; + } + if (_val_obj.title == 'CloudFlare') { + _form.list.push({ + html: + '
                                API-Limit
                                ', + }); + } + _form.btns.push({ + title: lan.site.save, + css: 'btn-success', + name: 'btn_submit_save', + callback: function (ldata, load) { + bt.site.set_dns_api({ pdata: JSON.stringify(ldata) }, function (ret) { + if (ret.status) { + load.close(); + robj.find('input[type="radio"]:eq(0)').trigger('click'); + robj.find('input[type="radio"]:eq(1)').trigger('click'); + } + bt.msg(ret); + }); + }, + }); + if (is_hide) { + obj.after(''); + $('.set_dns_config').click(function () { + var _bs = bt.render_form(_form); + $('div[data-id="form' + _bs + '"]').append(bt.render_help(helps)); + }); + } else { + var _bs = bt.render_form(_form); + $('div[data-id="form' + _bs + '"]').append(bt.render_help(helps)); + } + } + }, + }, + ], + }, + { + title: ' ', + class: 'checks_line label-input-group', + items: [ + { + css: 'label-input-group ptb10', + text: 'Automatically combine pan-domain names', + name: 'app_root', + type: 'checkbox', + }, + ], + }, + ]; + for (var i = 0; i < data.length; i++) { + var _form_data = bt.render_form_line(data[i]); + $(obj).parents('.line').append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + }); + }, + }, + ], + }, + ]; + + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + var _ul = $( + '
                                ' + ); + for (var i = 0; i < ddata.domains.length; i++) { + if (ddata.domains[i].binding === true) continue; + _ul.append('
                              • ' + ddata.domains[i].name + '
                              • '); + } + var _line = $("
                                "); + _line.append('' + lan.site.domain + ''); + _line.append(_ul); + robj.append(_line); + robj.find('input[type="radio"]').parent().addClass('label-input-group ptb10'); + $('#ymlist li input').click(function (e) { + e.stopPropagation(); + var a = true; + $('#ymlist li input').each(function () { + var o = $(this).prop('checked'); + if (!o) { + a = false; + return false; + } + }); + $('#ymlist div input').prop('checked', a); + }); + $('#ymlist li').click(function () { + var o = $(this).find('input'), + a = true; + if (o.prop('checked')) { + o.prop('checked', false); + } else { + o.prop('checked', true); + } + $('#ymlist li input').each(function () { + var o = $(this).prop('checked'); + if (!o) { + a = false; + return false; + } + }); + $('#ymlist div input').prop('checked', a); + }); + $('#ymlist div').click(function () { + var o = $('#ymlist div input'), + p = $('#ymlist input'); + if (o.prop('checked')) { + p.prop('checked', true); + } else { + p.prop('checked', false); + } + }); + var _btn_data = bt.render_form_line({ + title: ' ', + text: lan.site.btapply, + name: 'letsApply', + type: 'button', + callback: function (ldata) { + ldata['domains'] = []; + $('#ymlist li:visible input[type="checkbox"]:checked').each(function () { + ldata['domains'].push($(this).val()); + }); + ldata.app_root = $('#app_root').prop('checked'); + // console.log(ldata) + var auth_type = 'http'; + var auth_to = web.id; + var auto_wildcard = '0'; + if ($('.check_dns').prop('checked')) { + auth_type = 'dns'; + auth_to = 'dns'; + auto_wildcard = ldata.app_root ? '1' : '0'; + var dns_select = $('.dns_select').val() + if (dns_select !== auth_to) { + if (!site.dnsapi[dns_select].s_key) { + layer.msg('No key information is set for the specified dns interface'); + return; + } + auth_to = dns_select + '|' + site.dnsapi[dns_select].s_key + '|' + site.dnsapi[dns_select].s_token; + } + } + if (ldata['domains'].length <= 0) { + return layer.msg('Need at least a domain name!', { icon: 2 }); + } + site.show_certificate_confirm(web.name, function () { + acme.apply_cert(ldata['domains'], auth_type, auth_to, auto_wildcard, function (res) { + site.ssl.ssl_result(res, auth_type, web.name); + }); + }); + }, + }); + robj.append(_btn_data.html); + bt.render_clicks(_btn_data.clicks); + + robj.append(bt.render_help(helps[0])); + robj.find('input[type="radio"]:eq(0)').trigger('click'); + }); + }, + }, + // { + // title: lan.site.other_ssl, + // callback: function (robj) { + // robj = $('#webedit-con .tab-con') + // var cert_info = ''; + // if (rdata.cert_data['notBefore']) { + // cert_info = '
                                \ + // ' + (rdata.status ? lan.site.deploy_success_tips : lan.site.not_deploy_and_save) + '\ + // ' + lan.site.cert_brand + '' + rdata.cert_data.issuer + '\ + // ' + lan.site.auth_domain + ' ' + (rdata.cert_data.dns ? rdata.cert_data.dns.join(', ') : '') + '\ + // ' + lan.site.expire_time + ' ' + rdata.cert_data.notAfter + '
                                ' + // } + // robj.append('
                                ' + cert_info + '
                                ' + lan.site.ssl_key + '' + lan.site.ssl_crt + '
                                '); + // var datas = [{ + // items: [ + // {name: 'key', width: '48%', height: '220px', type: 'textarea', value: rdata.key}, + // {name: 'csr', width: '48%', height: '220px', type: 'textarea', value: rdata.csr} + // ] + // }, + // { + // items: [{ + // text: lan.site.save, + // name: 'btn_ssl_save', + // type: 'button', + // callback: function (sdata) { + // bt.site.set_ssl(web.name, sdata, function (ret) { + // if (ret.status) site.reload(7); + // bt.msg(ret); + // }) + // } + // }, + // { + // text: lan.site.ssl_close, + // name: 'btn_ssl_close', + // hide: !rdata.status, + // type: 'button', + // callback: function (sdata) { + // site.ssl.set_ssl_status('CloseSSLConf', web.name); + // } + // } + // ] + // } + // ] + // for (var i = 0; i < datas.length; i++) { + // var _form_data = bt.render_form_line(datas[i]); + // robj.append(_form_data.html); + // bt.render_clicks(_form_data.clicks); + // } + // var helps = [ + // lan.site.bt_ssl_help_10, + // lan.public_backup.cret_err, + // lan.public_backup.pem_format, + // lan.site.ssl_tips5, + // ] + // robj.append(bt.render_help(helps)); + // robj.find(".help-info-text").css('margin-top', '0'); + // robj.find('textarea').css('resize', 'none'); + // robj.find('[name=csr]').css('margin-right', '0'); + // } + // }, + // { + // title: lan.site.turn_off, + // callback: function (robj) { + // robj = $('#webedit-con .tab-con'); + // if (rdata.type == -1) { + // robj.html("
                                " + lan.site.ssl_help_1 + "
                                "); + // } else { + // var txt = ''; + // switch (rdata.type) { + // case 1: + // txt = "Let's Encrypt"; + // break; + // case 0: + // txt = lan.site.other_ssl; + // break; + // case 2: + // txt = lan.site.bt_ssl; + // break; + // } + // robj.html('\ + //
                                ' + lan.get('ssl_enable', [txt]) + '
                                \ + //
                                \ + // \ + //
                                \ + // '); + // } + // var loadT = bt.load(lan.site.the_msg); + // $.post('/site?action=get_auto_restart_rph', { + // sitename: web.name + // }, function (res) { + // loadT.close(); + // if (res) { + // var checked_str = res.status ? 'checked="true"' : ''; + // robj.append('\ + //
                                \ + //
                                Auto restart proxy, redirect, http to https when apply or renew SSL
                                \ + //
                                \ + // \ + // \ + //
                                \ + // '); + // } + // }); + // } + // }, + { + title: lan.site.ssl_dir, + callback: function (robj) { + robj = $('#webedit-con .tab-con'); + robj.html("
                                "); + bt.site.get_cer_list(function (rdata) { + bt.render({ + table: '#cer_list_table', + columns: [ + { + field: 'subject', + title: lan.site.domain, + templet: function (item) { + return item.dns.join('
                                '); + }, + }, + { field: 'notAfter', width: '100px', title: lan.site.endtime }, + { field: 'issuer', width: '150px', title: lan.site.brand }, + { + field: 'opt', + width: '100px', + align: 'right', + title: lan.site.operate, + templet: function (item) { + var opt = + '' + + lan.site.deploy + + ' | '; + opt += + '' + + lan.site.del + + ''; + return opt; + }, + }, + ], + data: rdata, + }); + }); + }, + }, + ]; + + bt.render_tab('ssl_tabs', tabs); + + // $('#ssl_tabs').append('
                                ' + lan.site.force_https + '
                                '); + // $("#toHttps").attr('checked', rdata.httpTohttps); + // $('#toHttps').click(function (sdata) { + // var isHttps = $("#toHttps").attr('checked'); + // if (isHttps) { + // layer.confirm('After closing HTTPS, you need to clear your browser cache to see the effect. Continue?', { + // icon: 3, + // title: "Turn off forced HTTPS\"" + // }, function () { + // bt.site.close_http_to_https(web.name, function (rdata) { + // if (rdata.status) { + // setTimeout(function () { + // site.reload(7); + // }, 3000); + // } + // }) + // }); + // } else { + // bt.site.set_http_to_https(web.name, function (rdata) { + // if (!rdata.status) { + // setTimeout(function () { + // site.reload(7); + // }, 3000); + // } + + // }) + // } + // }) + // switch (rdata.type) { + // case 1: + // $('#ssl_tabs span:eq(0)').trigger('click'); + // break; + // case 0: + // $('#ssl_tabs span:eq(0)').trigger('click'); + // break; + // default: + // $('#ssl_tabs span:eq(0)').trigger('click'); + // break; + // } + + $('#ssl_tabs span:eq(' + (rdata.status ? (rdata.csr ? 0 : 1) : 1) + ')').trigger('click'); + + $('.cutTabView').on('click', function () { + $('#ssl_tabs span:eq(1)').trigger('click'); + setTimeout(function () { + $('.ssl_business_application').trigger('click'); + }, 400); + }); + }); + }, + show_certificate_confirm: function (sitename, callback) { + var _this = this; + var auto_restart_rph = function (index, loading) { + if (loading) loadT = bt.load(lan.site.the_msg); + $.post( + '/site?action=auto_restart_rph', + { + sitename: sitename, + }, + function (res) { + loadT.close(); + if (res.status) { + if (index) layer.close(index); + if (callback) callback(res); + } + } + ); + }; + var loadT = bt.load(lan.site.the_msg); + $.post( + '/site?action=get_auto_restart_rph', + { + sitename: sitename, + }, + function (res) { + if (res && res.status) { + auto_restart_rph(); + } else { + loadT.close(); + layer.open({ + type: 1, + area: '530px', + title: 'Apply SSL', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + '\ +
                                \ +
                                \ + \ +

                                Apply or renew SSL

                                \ +
                                  \ +
                                • The reverse proxy, redirection and http to https will be automatically restart during the application or renewal of SSL!
                                • \ +
                                • The application and renewal of SSL will not be affected by the redirection, reverse proxy and http to https
                                • \ +
                                \ +
                                \ +
                                \ + \ + \ +
                                \ +
                                \ + ', + success: function (layers, index) { + $('.submit_cert').click(function () { + auto_restart_rph(index, true); + }); + $('.close_cert').click(function () { + layer.close(index); + }); + }, + }); + } + } + ); + }, +}; + +// $('#cutMode .tabs-item[data-type="' + (bt.get_cookie('site_model') || 'php') + '"]').trigger('click'); +// site.get_types(); + +// $.prototype.serializeObject = function() { +// var a, o, h, i, e; +// a = this.serializeArray(); +// o = {}; +// h = o.hasOwnProperty; +// for (i = 0; i < a.length; i++) { +// e = a[i]; +// if (!h.call(o, e.name)) { +// o[e.name] = e.value; +// } +// } +// return o; +// }; diff --git a/BTPanel/static/vite/oldjs/soft.js b/BTPanel/static/vite/oldjs/soft.js new file mode 100644 index 00000000..ebd4107f --- /dev/null +++ b/BTPanel/static/vite/oldjs/soft.js @@ -0,0 +1,5543 @@ +var soft = { + is_install: false, + trail: 0, //是否试用 + is_setup: false, + is_setup_name: '', + refresh_data: [], + get_list: function (page, type, search) { + if (page == undefined || page == 'null' || page == 'undefined') page = 0; + if (type == undefined || type == 'null' || type == 'undefined') type = 0; + if (!search) search = $('#SearchValue').val(); + if (search == undefined || search == 'null' || search == 'undefined' || search == '') search = undefined; + var _this = this, + commonly_software = $('#commonly_software'); + var istype = getCookie('softType'); + if (istype == 'undefined' || istype == 'null' || !istype) { + istype = 0; + } + if (type == 0) type = bt.get_cookie('softType'); + if (page == 0) page = bt.get_cookie('p' + type); + if (type == '11') { + soft.get_dep_list(1); + commonly_software.hide(); + return; + } + soft.is_install = false; + bt.soft.get_soft_list(page, type, search, function (rdata) { + _this.trail = rdata.trail; + // if (rdata.pro < 0) { + // // $("#updata_pro_info").html(''); + // } else + // if (rdata.pro === -2) { + // $("#updata_pro_info").html('
                                ' + lan.soft.pro_expire + ''); + // } else if (rdata.pro === -1) { + // $("#updata_pro_info").html('
                                ' + lan.soft.upgrade_pro + '\
                                '); + // } + soft.set_soft_tips(rdata, type); + + // if (type == 10) { + // $("#updata_pro_info").html('
                                ' + lan.soft.bt_developer + '' + lan.soft.get_third_party_apps + '
                                ') + // } else if (type == 11) { + // $("#updata_pro_info").html('
                                ' + lan.soft.comingsoon + '
                                ') + // } + var tBody = ''; + rdata.type.unshift({ icon: 'icon', id: 0, ps: lan.soft.all, sort: 1, title: lan.soft.all }, { icon: 'icon', id: -1, ps: 'Installed', sort: 1, title: 'Installed' }); + for (var i = 0; i < rdata.type.length; i++) { + var c = ''; + if (istype == rdata.type[i].id) { + c = 'class="on"'; + } + // 注释软件管理的付费插件,第三方插件,一键部署 + // if (rdata.type[i].id != "11" && rdata.type[i].id != "10" && rdata.type[i].id != "8") { + if (rdata.type[i].id != '11') { + tBody += '' + rdata.type[i].title + ''; + } + } + if (page) bt.set_cookie('p' + type, page); + $('.softtype').html(tBody); + $('.menu-sub span').click(function () { + var _type = $(this).attr('typeid'); + bt.set_cookie('softType', _type); + $(this).addClass('on').siblings().removeClass('on'); + if (_type !== '11') { + soft.get_list(0, _type); + commonly_software.show(); + } else { + soft.get_dep_list(0); + commonly_software.hide(); + } + }); + var data = rdata.list.data; + $('#softPage').html(rdata.list.page); + if (data.length > 0) { + for (var i = 0; i < data.length; i++) { + if (data[i].task == '-1') { + soft.is_setup = true; + soft.is_setup_name = data[i].name; + break; + } else { + soft.is_setup = false; + soft.is_install = false; + soft.is_setup_name = ''; + } + } + } + if (soft.is_setup == true && soft.is_setup_name != '') { + _this.soft_setup_find(); + } + if (soft.refresh_data.length == 0) { + _this.refresh_table(page, type, search, rdata); + soft.refresh_data = data; + } else if (JSON.stringify(data) != JSON.stringify(soft.refresh_data)) { + _this.refresh_table(page, type, search, rdata); + soft.refresh_data = data; + } + bt.set_cookie('load_page', (page + '').split('not_load')[0]); + bt.set_cookie('load_type', type); + bt.set_cookie('load_search', search); + if (soft.is_install && soft.is_setup == false) { + setTimeout(function () { + soft.get_list(bt.get_cookie('load_page') + 'not_load', bt.get_cookie('load_type'), bt.get_cookie('load_search')); + }, 3000); + soft.is_install = false; + } + // if(rdata.recommend){ + // _this.render_promote_list(rdata.recommend); + // } + }); + }, + // 查找正在安装软件的状态 + soft_setup_find: function () { + var _this = this; + if (soft.is_setup == true && soft.is_setup_name != '') { + $.post('plugin?action=get_soft_find', { sName: soft.is_setup_name }, function (rdata) { + if (rdata.task == '-1') { + setTimeout(function () { + _this.soft_setup_find(); + }, 3000); + } else { + soft.is_install = true; + setTimeout(function () { + soft.get_list(bt.get_cookie('load_page') + 'not_load', bt.get_cookie('load_type'), bt.get_cookie('load_search')); + }, 3000); + } + }); + } + }, + // 刷新列表 + refresh_table: function (page, type, search, rdata) { + var _this = this; + var phps = ['php-5.2', 'php-5.3', 'php-5.4']; + var data = rdata.list.data; + var _tab = bt.render({ + table: '#softList', + columns: [ + { + field: 'title', + title: lan.soft.app_name, + width: 165, + templet: function (item) { + var fName = item.name, + version = item.version; + if (bt.contains(item.name, 'php-')) { + fName = 'php'; + version = ''; + } + var click_opt = ' ', + sStyle = ''; + if (item.setup) { + sStyle = ' style="cursor:pointer"'; + if (item.admin) { + if (item.endtime >= 0 || item.price == 0) { + click_opt += 'onclick="bt.soft.set_lib_config(\'' + item.name + "','" + item.title + '\')" '; + } + } else { + click_opt += ' onclick="soft.set_soft_config(\'' + item.name + '\')" '; + } + } + var is_php5 = item.name.indexOf('php-5') >= 0, + webcache = bt.get_cookie('serverType') == 'openlitespeed' ? true : false, + distribution = bt.get_cookie('distribution'); + if (webcache) { + switch (distribution) { + case 'centos8': + if (is_php5 || item.name == 'php-7.0') { + click_opt = ' title="' + lan.soft.ap2_2_not_support + '"'; + } + break; + case 'centos7': + if (item.name == 'php-5.2') { + click_opt = ' title="' + lan.soft.ap2_2_not_support + '"'; + } + break; + default: + if (is_php5) { + click_opt = ' title="' + lan.soft.ap2_2_not_support + '"'; + } + break; + } + } else if (rdata.apache22 && item.name.indexOf('php-') >= 0 && $.inArray(item.name, phps) == -1) { + click_opt = ' title="' + lan.soft.ap2_2_not_support + '"'; + } + //if (rdata.apache22 && item.name.indexOf('php-') >= 0 && $.inArray(item.name, phps) == -1) click_opt = ' title="' + lan.soft.ap2_2_not_support + '"'; + return '' + item.title + ' ' + version + ''; + }, + }, + { + field: 'price', + title: 'Developer', + width: 110, + templet: function (item) { + if (!item.author) return 'official'; + return item.author; + }, + }, + { + field: 'ps', + title: lan.soft.instructions, + templet: function (item) { + var ps = item.ps; + var is_php = item.name.indexOf('php-') >= 0; + + if (is_php && item.setup) { + if (rdata.apache22 && $.inArray(item.name, phps) >= 0) { + if (item.fpm) { + ps += " (" + lan.soft.apache22 + ')'; + } + } else if (!rdata.apache22) { + if (!item.fpm) { + ps += " (" + lan.soft.apache24 + ')'; + } + } + } + return '' + ps + ''; + }, + }, + { + field: 'price', + title: lan.soft.price, + width: 92, + templet: function (item) { + var price = lan.soft.free; + if (item.price > 0) { + price = '$' + item.price + ''; + } + return price; + }, + }, + type == 10 + ? { + field: 'sort', + width: 80, + title: 'Rated', + templet: function (item) { + return item.sort !== undefined + ? '' + + (item.sort <= 0 || item.sort > 5 ? lan.soft.not_rated : item.sort.toFixed(1)) + + '' + : '--'; + }, + } + : '', + { + field: 'endtime', + width: 120, + title: lan.soft.expire_time, + templet: function (item) { + var endtime = '--'; + if (item.pid > 0) { + if (item.endtime > 0) { + if (item.type != 10) { + endtime = bt.format_data(item.endtime, 'yyyy/MM/dd'); + } else { + endtime = bt.format_data(item.endtime, 'yyyy/MM/dd'); + } + } else if (item.endtime === 0) { + endtime = lan.soft.permanent; + } else if (item.endtime === -1) { + endtime = lan.soft.not_open; + } else if (item.endtime === -2) { + if (item.type != 10) { + endtime = lan.soft.already_expire; + } else { + endtime = lan.soft.already_expire; + } + } + } + return endtime; + }, + }, + { + field: 'path', + width: 40, + title: lan.soft.location, + templet: function (item) { + var path = ''; + if (item.setup) { + path = ''; + } + return path; + }, + }, + type != 10 + ? { + field: 'status', + width: 40, + title: lan.soft.status1, + templet: function (item) { + var status = ''; + if (item.setup) { + if (item.status) { + status = ''; + } else { + status = ''; + } + } + return status; + }, + } + : '', + { + field: 'index', + width: 100, + title: lan.soft.display_at_homepage, + templet: function (item) { + var to_index = ''; + if (item.setup) { + var checked = ''; + if (item.index_display) checked = 'checked'; + var item_id = item.name.replace(/\./, ''); + to_index = + '
                                '; + } + return to_index; + }, + }, + { + field: 'opt', + width: 190, + title: lan.soft.operate, + align: 'right', + templet: function (item) { + var option = ''; + + var pay_opt = ''; + if (item.endtime < 0 && item.pid > 0) { + var re_msg = ''; + var re_status = 0; + var buy_type = 0; + switch (item.endtime) { + case -1: + re_msg = lan.soft.buy_now; + buy_type = 31; + break; + case -2: + re_msg = lan.soft.renew_now; + re_status = 1; + buy_type = 32; + break; + } + if (item.type != 10) { + pay_opt = + '" + + re_msg + + ''; + } else { + pay_opt = '' + re_msg + ''; + } + } + var is_php = item.name.indexOf('php-') >= 0, + is_php5 = item.name.indexOf('php-5') >= 0, + webcache = bt.get_cookie('serverType') == 'openlitespeed' ? true : false, + distribution = bt.get_cookie('distribution'); + if (webcache && is_php) { + if ((is_php5 || item.name == 'php-7.0') && distribution == 'centos8') { + option = '' + lan.soft.not_comp + ''; + } else if (distribution == 'centos7' && item.name == 'php-5.2') { + option = '' + lan.soft.not_comp + ''; + } else { + if (distribution != 'centos7' && is_php5) { + option = '' + lan.soft.not_comp + ''; + } else { + if (item.setup && item.task == '1') { + if (pay_opt == '') { + if (item.versions.length > 1) { + for (var i = 0; i < item.versions.length; i++) { + var min_version = item.versions[i]; + var ret = bt.check_version(item.version, min_version.m_version + '.' + min_version.version); + if (ret > 0) { + if (ret == 2) + option += + '' + + lan.soft.update + + ' | '; + break; + } + } + } else { + var min_version = item.versions[0]; + var cloud_version = min_version.m_version + '.' + min_version.version; + if (item.version != cloud_version) + option += + '' + + lan.soft.update + + ' | '; + } + if (item.admin) { + option += '' + lan.soft.setup + ' | '; + } else { + option += '' + lan.soft.setup + ' | '; + } + } else { + option = pay_opt + ' | ' + option; + } + option += '' + lan.soft.uninstall + ''; + } else if (item.task == '-1') { + option = '' + lan.soft.installing + ''; + soft.is_install = true; + } else if (item.task == '0') { + option = '' + lan.soft.wait_install + ''; + soft.is_install = true; + } else if (item.task == '-2') { + option = 'Updating'; + soft.is_install = true; + } else { + if (pay_opt) { + option = pay_opt; + } else { + option = '' + lan.soft.install + ''; + } + } + } + } + } else { + if (rdata.apache22 && is_php && $.inArray(item.name, phps) == -1) { + if (item.setup) { + option = '' + lan.soft.uninstall + ''; + } else { + option = '' + lan.soft.not_comp + ''; + } + } else if (rdata.apache24 && item.name == 'php-5.2') { + if (item.setup) { + option = '' + lan.soft.uninstall + ''; + } else { + option = '' + lan.soft.not_comp + ''; + } + } else { + if (item.setup && item.task == '1') { + if (pay_opt == '') { + if (item.versions.length > 1) { + for (var i = 0; i < item.versions.length; i++) { + var min_version = item.versions[i]; + var ret = bt.check_version(item.version, min_version.m_version + '.' + min_version.version); + if (ret > 0) { + if (ret == 2) + option += + '' + + lan.soft.update + + ' | '; + break; + } + } + } else { + var min_version = item.versions[0]; + var cloud_version = min_version.m_version + '.' + min_version.version; + if (item.version != cloud_version) + option += + '' + + lan.soft.update + + ' | '; + } + if (item.admin) { + option += '' + lan.soft.setup + ' | '; + } else { + option += '' + lan.soft.setup + ' | '; + } + } else { + option = pay_opt + ' | ' + option; + } + option += '' + lan.soft.uninstall + ''; + } else if (item.task == '-1') { + option = '' + lan.soft.installing + ''; + soft.is_install = true; + } else if (item.task == '0') { + option = '' + lan.soft.wait_install + ''; + soft.is_install = true; + } else if (item.task == '-2') { + option = 'Updating'; + soft.is_install = true; + } else { + if (pay_opt) { + option = pay_opt; + } else { + option = '' + lan.soft.install + ''; + } + } + } + } + return option; + }, + }, + ], + data: data, + empty: + 'If the search content is not found, submit the demand feedback', + }); + // 需求反馈 + if (data.length == 0) { + $('.feedback-btn').remove(); + $('.soft-filter-box .soft-search').after( + '' + ); + } + }, + // 渲染列表 + render_promote_list: function (data) { + if ($('#soft_recom_list').length > 0) $('#soft_recom_list').remove(); + var html = $('
                                  '), + that = this; + for (var i = 0; i < data.length; i++) { + var type = '', + item = data[i]; + (function (item) { + switch (item.type) { + case 'link': // 链接推荐 + type = $('' + (item.title || '') + ''); + break; + case 'soft': // 软件推荐 + case 'other': // 第三方推荐 + case 'onekey': // 一键部署推荐 + type = $('' + (item.title || '') + '').click(function () { + that.render_promote_view(item); + }); + break; + } + html.append($('
                                • ').append(type)); + })(item); + // html.append($('
                                • ').append(type)); + } + $('#updata_pro_info').before(html); + }, + // 渲染软件列表 + render_promote_view: function (find) { + var that = this, + is_single_product = find.data.length > 1, + find_data = find.data; + if (is_single_product) { + layer.open({ + title: find.title, + area: '800px', + btn: false, + closeBtn: 2, + shadeClose: false, + content: (function () { + var html = ''; + for (var i = 0; i < find_data.length; i++) { + var item = find_data[i], + thtml = ''; + if (!item.setup) { + thtml = ''; + } else { + if (item.pid != 0) { + if (item.endtime == 0) { + //永久 + thtml = ''; + } else if (item.endtime > 0) { + //已购买 + thtml = ''; + } else if (item.endtime == -1) { + //未购买 + + thtml = + '"; + } else if (item.endtime == -2) { + //已过期 + thtml = + '"; + } + } else { + thtml = ''; + } + } + html += + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + item.title + + ' v' + + item.version + + '
                                  ' + + '
                                  $' + + item.price + + '/month
                                  ' + + '
                                  ' + + '
                                  ' + + item.ps + + '
                                  ' + + '
                                  ' + + thtml + + '
                                  ' + + '
                                  ' + + '
                                  '; + } + return html; + })(), + }); + } + }, + set_soft_tips: function (rdata, type) { + var tips_info = $('
                                  '), + explain = tips_info.find('.soft_tips_text'), + btn_ground = tips_info.find('.btn-ground'), + _this = this, + el = '#updata_pro_info'; + $(el).empty(); + type = parseInt(type); + if (type != 11) $(el).next('.onekey-menu-sub').remove(); + if (type == 10) { + $(el).css('display', 'block'); + explain.text( + 'Security Reminder: aaPanel officially conducted a security audit before the third-party plug-in was put on the shelves, but there may be security risks. Please check it out before using it in the production environment.' + ); + btn_ground = soft.render_tips_btn(btn_ground, [ + //{title:'免费入驻',href:'https://www.bt.cn/developer/',rel:'noreferrer noopener',target:'_blank',btn:'免费入驻',class:'btn btn-success btn-xs va0',style:"margin-left:10px;"}, + { + title: 'Get third-party apps', + rel: 'noreferrer noopener', + href: 'https://www.bt.cn/bbs/forum-40-1.html', + target: '_blank', + btn: 'Get third-party apps', + class: 'btn btn-success btn-xs va0 ml15', + style: 'margin-left:10px;', + }, + { + title: 'Import plugins', + href: 'javascript:;', + btn: 'Import plugins', + class: 'btn btn-success btn-xs va0 ml15', + style: 'margin-left:10px;', + click: function (e) { + var input = $('') + .change(function (e) { + var files = $(this)[0].files; + if (files.length == 0) return; + soft.update_zip(files[0]); + }) + .click(); + }, + }, + ]); + $(el).append(tips_info.addClass('alert-danger')); + } else if (type == 11) { + explain.text('BT one click宝塔一键部署已上线,诚邀全球优秀项目入驻(限项目官方) '); + btn_ground = soft.render_tips_btn(btn_ground, [ + { + title: '免费入驻', + href: 'https://www.bt.cn/bbs/thread-33063-1-1.html', + rel: 'noreferrer noopener', + target: '_blank', + btn: '免费入驻', + class: 'btn btn-success btn-xs va0', + style: 'margin-left:10px;', + }, + { title: '导入项目', href: 'javascript:;', rel: 'noreferrer noopener', btn: '导入项目', class: 'btn btn-success btn-xs va0', style: 'margin-left:10px;', click: soft.input_package }, + ]); + $(el).append(tips_info.addClass('alert-info')); + } else { + var genre = true, + is_buy = false; + if (rdata.ltd > 0 || type === 12) { + genre = false; + } else if (rdata.pro >= 0 || type === 8) { + genre = true; + } + if (rdata.ltd > 0 || rdata.pro >= 0) is_buy = true; + if (type === 12 && rdata.ltd < 0) is_buy = false; + var buy_type = is_buy ? 30 : 29; + var ltd = parseInt(bt.get_cookie('ltd_end') || -1), + pro = parseInt(bt.get_cookie('pro_end') || -1), + todayDate = parseInt(new Date().getTime() / 1000), + _ltd = null; + if ((ltd > 0 && (ltd == pro || pro < 0)) || (ltd < 0 && pro >= 0) || (ltd > 0 && pro >= 0)) { + _ltd = (ltd > 0 && (ltd == pro || pro < 0)) || (ltd > 0 && pro >= 0) ? 1 : 0; + explain.html( + 'The ' + + (_ltd ? 'Pro' : 'Pro') + + ' edition can use the ' + + (_ltd ? '专业版及企业版插件' : 'professional plug-in for free,') + + (!(pro == 0 && ltd < 0) + ? 'expiration time: ' + + bt.format_data(_ltd ? ltd : pro, 'yyyy/MM/dd') + + '' + + ((_ltd ? ltd : pro) - todayDate <= 15 * 24 * 60 * 60 + ? ',Only ' + Math.round(((_ltd ? ltd : pro) - todayDate) / (24 * 60 * 60)) + ' days until expiration' + : '') + : ' Expire: Lifetime') + ); + } else if (ltd == -1 && pro == -1) { + explain.html('Upgrade to Pro edition, all plugins, free to use!'); + } else if (pro == 0 && ltd < 0) { + _ltd = 2; + explain.html( + 'The Pro edition can use the professional plug-in for free, expiration time: 永久授权。' + + (type == 12 ? '  升级企业版,企业可以免费试用企业版插件及专业版插件。' : '') + ); + if (type == 12) { + btn_ground = soft.render_tips_btn(btn_ground, { + title: '立即升级', + href: 'javascript:;', + btn: '立即升级', + class: 'btn btn-success btn-xs va0 ml15', + style: 'margin-left:10px;', + click: bt.soft.updata_ltd, + }); + } + } else if (ltd == -2 || pro == -2) { + _ltd = ltd == -2 ? 1 : 0; + explain.html( + '当前为' + + (_ltd ? '企业版' : '专业版') + + ',' + + (_ltd ? '企业版' : '专业版') + + '可以免费使用' + + (_ltd ? '专业版及企业版插件' : '专业版插件') + + ',' + + (_ltd ? '企业版' : '专业版') + + '已过期' + ); + } + var btn_config = { title: null, href: 'javascript:;', btn: null, class: 'btn btn-success btn-xs va0 ml15', style: 'margin-left:10px;', click: null }; + var set_btn_style = function (res) { + if (!res.status || !res) { + fun = function () { + bt.pub.bind_btname(function () { + window.location.reload(); + }); + }; + $.extend(btn_config, { title: 'Login', btn: 'Login', click: fun }); + } else { + if (type == 12 && ltd < 0 && pro >= 0) { + explain.html( + '企业版可以免费使用专业版及企业版插件,了解专业版和企业版的区别,请点击查看详情《专业版升级企业版教程》' + ); + $(el).append(tips_info.addClass('alert-ltd-success')); + return false; + } else { + // var btn = $('' + (is_buy ? 'Renew Now' : 'Upgrade now') + '') + // btn.on('click', function () { + // genre ? bt.soft.updata_pro(buy_type) : bt.soft.updata_ltd(undefined,buy_type) + // }) + // tips_info.addClass('showprofun').find('.btn-ground').append(btn) + } + } + if (pro !== 0) { + var btn = $( + '' + + (is_buy ? 'Renew Now' : 'Upgrade now') + + '' + ); + btn.on('click', function () { + window.usePay({ + source: buy_type, + }); + // genre ? bt.soft.updata_pro(buy_type) : bt.soft.updata_ltd(undefined, buy_type); + }); + tips_info.addClass('showprofun').find('.btn-ground').append(btn); + } + // if(_ltd != 2){ + // if(!(pro == 0 && ltd < 0)){ + // btn_ground = soft.render_tips_btn(btn_ground); + // } + // } + $(el).append(tips_info.addClass(_ltd == 1 ? 'alert-ltd-success' : 'alert-success')); + if (_this.trail) { + // setTimeout(function (){ + // $('.btn-ground').after('Try the Pro edition for free') + // var trail = $('Click to try'); + // trail.click((!res.status || !res)?fun:function(){ + // var loadT = bt.load() + // bt.confirm({ + // title:"Pro Edition", + // msg:"Get 7-day Pro edition free, get it now?" + // },function (){ + // bt.send('free_trial','auth/free_trial',{},function(res){ + // loadT.close() + // bt.msg(res) + // setTimeout(function () { window.location.reload() },2000) + // }) + // }) + // }) + // $('.pro_trail').after(trail) + // },100) + } + }; + var bt_user_info = bt.get_cookie('bt_user_info'); + if (!bt_user_info) { + bt.pub.get_user_info(function (res) { + if (!res.status) { + set_btn_style(false); + return false; + } + bt.set_cookie('bt_user_info', JSON.stringify(res), 300000); + set_btn_style(res); + }); + } else { + set_btn_style(JSON.parse(bt.get_cookie('bt_user_info'))); + } + } + }, + /** + * @description 设置软件信息 + * @param {object} rdata 软件列表请求数据 + * @param {string} type 列表类型 + */ + render_soft_recommend: function () { + bt.send('get_usually_plugin', 'plugin/get_usually_plugin', {}, function (res) { + var html = ''; + for (var i = 0; i < res.length; i++) { + var item = res[i]; + html += + '
                                  ' + + item.title + + '
                                  '; + } + $('#commonly_software .commonly_software_list').html(html); + }); + }, + render_tips_btn: function (node, arry) { + if (!Array.isArray(arry)) arry = [arry]; + for (var i = 0; i < arry.length; i++) { + var item = arry[i], + btn = ''; + if (item.click) { + btn = $(btn).on('click', item.click); + } + node.append(btn); + } + return node; + }, + get_dep_list: function (p) { + var loadT = layer.msg('Getting list ', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + var pdata = {}; + var search = $('#SearchValue').val(); + if (search != '') { + pdata['search'] = search; + } + var type = ''; + var istype = getCookie('depType'); + if (istype == 'undefined' || istype == 'null' || !istype) { + istype = '0'; + } + pdata['type'] = istype; + + var force = bt.get_cookie('force'); + if (force === '1') { + pdata['force'] = force; + } + bt.set_cookie('force', 0); + $.post('/deployment?action=GetList', pdata, function (rdata) { + layer.close(loadT); + var tBody = ''; + soft.set_soft_tips(rdata, 11); + rdata.type.unshift( + { + icon: 'icon', + id: 0, + ps: 'All', + sort: 1, + title: 'All', + }, + { + icon: 'icon', + id: -1, + ps: 'Installed', + sort: 1, + title: 'Installed', + } + ); + for (var i = 0; i < rdata.type.length; i++) { + var c = ''; + if ('11' == rdata.type[i].id) { + c = 'class="on"'; + } + tBody += '' + rdata.type[i].title + ''; + } + $('.softtype').html(tBody); + + $('.menu-sub span').click(function () { + var _type = $(this).attr('typeid'); + bt.set_cookie('softType', _type); + $(this).addClass('on').siblings().removeClass('on'); + if (_type !== '11') { + soft.get_list(0, _type); + } else { + soft.get_dep_list(1); + } + }); + if ($('.onekey-type').attr('class') === undefined) { + tbody = + '
                                  '; + + rdata.dep_type.unshift({ + tid: 0, + title: 'All', + }); + rdata.dep_type.push({ + tid: 100, + title: 'Other', + }); + for (var i = 0; i < rdata.dep_type.length; i++) { + var c = ''; + if (istype == rdata.dep_type[i].tid) { + c = 'class="on"'; + } + tbody += '' + rdata.dep_type[i].title + ''; + } + tbody += '
                                  '; + $('#updata_pro_info').html(tbody); + $('.onekey-menu-sub span').click(function () { + setCookie('depType', $(this).attr('typeid')); + $(this).addClass('on').siblings().removeClass('on'); + soft.get_dep_list(1); + }); + } + + var zbody = + '\ + \ + Name\ + Version\ + Introduction\ + Support for PHP version\ + Provider\ + Score\ + Operate\ + \ + '; + var icon_other = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAYFJREFUeNpi/P//P8NAAhZkzrVr1zyB1FwglqSiHSZAfBZZQEtLC7sDQJZLSUlJcnNzU8Xm27dvg6jVQByqqqp6FpsaJjS+JCcnJ8O/f/+ogkFATk5uJ8gRQMcYE+MAqgN2dvYMeXn5g0DmGmyOYKJHQmNjY0tQUFA4gc0RLLS0mI+PD5YOQCACSp8BYka6OEBUVJRBXFwcW8IkPgQ+r2lk+PPsJnl5XEqdgTeknvhyABsAWS6Ytwyr3PtJUTjlYPKEAEWJEGQ5MZbQzAGEQoDmDhgNAWITGk0dQCkYcAewUJoIh38IgIpTchMai6Qa5Q4gVJYP+SgYcAcwIjfLHxZo0qWNLj/hOp4GCQs7bo0121D4D1u8wGIwGlkcd/3+k/xyANlgdMcgy8McRbM0QIoFVC8J8VkOCxVSHMdCTZ+TEyooDvjPKfCP8ddXJgZGJqISIskW/v8HtIP/H85seGdWYT3L/eN1jN8/0qR8+M8l8PePgkWzSlp/I1YHDAQACDAAtKS/DHmsv9AAAAAASUVORK5CYII='; + for (var i = 0; i < rdata.list.length; i++) { + var remove_opt = ''; + if (rdata.list[i].id === 0) { + remove_opt = + ' | ' + + lan.public.update + + ' | ' + + lan.public.del + + ''; + rdata.list[i].min_image = icon_other; + } else { + rdata.list[i].min_image += '?t=' + new Date().format('yyyyMMdd'); + } + zbody += + '' + + '' + + rdata.list[i].title + + '' + + '' + + rdata.list[i].version + + '' + + '' + + rdata.list[i].ps + + '' + + '' + + rdata.list[i].php + + '' + + '' + + (rdata.list[i].author == 'aaPanel' ? rdata.list[i].title : rdata.list[i].author) + + '' + + '' + + (rdata.list[i].sort !== undefined + ? '' + + (rdata.list[i].sort <= 0 || rdata.list[i].sort > 5 ? 'No rating' : rdata.list[i].sort.toFixed(1)) + + '' + : '--') + + '' + + 'One-Click' + + remove_opt + + '' + + ''; + } + $('#softList').html(zbody); + $('#softPage').html(''); + $('.searchInput').val(''); + }); + }, + remove_other_dep: function (name) { + bt.show_confirm(lan.soft.del_custom_item, lan.soft.confirm_del.replace('{1}', name), function () { + var loadT = layer.msg(lan.soft.deleting, { + icon: 16, + time: 0, + shade: 0.3, + }); + $.post( + '/deployment?action=DelPackage', + { + dname: name, + }, + function (rdata) { + layer.close(loadT); + if (rdata.status) soft.get_dep_list(); + setTimeout(function () { + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 2, + }); + }, 1000); + } + ); + }); + }, + input_package: function () { + var con = + '
                                  \ +
                                  Index name\ +
                                  \ + Format: [0-9A-Za-z_-]+, Do not have spaces and special characters\ +
                                  \ +
                                  \ +
                                  Name\ +
                                  \ + The name used to display to the list\ +
                                  \ +
                                  \ +
                                  PHP Version\ + \ + Please use multiple "," (comma) to separate, do not use PHP5.2\ +
                                  \ +
                                  Unblocked function\ + \ + Multiples should be separated by "," (comma), only the necessary functions are unblocked.\ +
                                  \ +
                                  Project version\ + \ + Currently imported project version\ +
                                  \ +
                                  Introduction\ +
                                  \ +
                                  \ +
                                  Upload project package\ + \ + Please upload the project package in zip format, which must contain the auto_insatll.json configuration file.\ +
                                  \ +
                                  \ + \ + \ +
                                  \ + '; + layer.open({ + type: 1, + title: 'Import a one-click deployment project package', + area: '600px', + closeBtn: 2, + shadeClose: false, + content: con, + }); + }, + update_package: function (p_name) { + $.post( + '/deployment?action=GetPackageOther', + { + p_name: p_name, + }, + function (rdata) { + var con = + '\ +
                                  Index name\ + \ + Format: [0-9A-Za-z_-]+, Do not have spaces and special characters\ +
                                  \ +
                                  Name\ + \ + The name used to display to the list\ +
                                  \ +
                                  PHP Version\ + \ + Please use multiple "," (comma) to separate, do not use PHP5.2\ +
                                  \ +
                                  Unblocked function\ + \ + Multiples should be separated by "," (comma), only the necessary functions are unblocked.\ +
                                  \ +
                                  Project version\ + \ + Currently imported project version\ +
                                  \ +
                                  Introduction\ +
                                  \ +
                                  \ +
                                  Upload project package\ + \ + Please upload the project package in zip format, which must contain the auto_insatll.json configuration file.\ +
                                  \ +
                                  \ + \ + \ +
                                  \ + '; + layer.open({ + type: 1, + title: 'Update one-click deployment project package', + area: '600px', + closeBtn: 2, + shadeClose: false, + content: con, + }); + } + ); + }, + input_package_to: function () { + var pdata = new FormData($('#input_package')[0]); + if (!pdata.get('name') || !pdata.get('title') || !pdata.get('version') || !pdata.get('php') || !pdata.get('ps')) { + layer.msg('The following are required (Index name / Name / Project version / PHP version / Introduction)', { + icon: 2, + }); + return; + } + var fs = $("input[name='dep_zip']")[0].files; + if (fs.length < 1) { + layer.msg('Please select the project package file', { + icon: 2, + }); + return; + } + var f = fs[0]; + if (f.type.indexOf('zip') == -1) { + layer.msg('Only supports files in zip format!'); + return; + } + if (!pdata.get('dep_zip')) pdata.append('dep_zip', f); + + var loadT = layer.msg('Importing...', { + icon: 16, + time: 0, + shade: 0.3, + }); + + $.ajax({ + url: '/deployment?action=AddPackage', + type: 'POST', + data: pdata, + processData: false, + contentType: false, + success: function (data) { + layer.close(loadT); + if (data.status) { + layer.closeAll(); + setCookie('depType', 100); + soft.get_dep_list(); + setTimeout(function () { + layer.msg('Successfully imported!'); + }, 1000); + } + }, + error: function (responseStr) { + layer.msg('Upload failed 2!', { + icon: 2, + }); + }, + }); + }, + flush_cache: function () { + bt.set_cookie('force', 1); + soft.get_list(); + }, + get_config_menu: function ( + name //获取设置菜单显示 + ) { + var meun = ''; + if (bt.os == 'Linux') { + var datas = { + public: [ + { + type: 'config', + title: lan.soft.config_edit, + }, + { + type: 'change_version', + title: lan.soft.nginx_version, + }, + ], + openlitespeed: [ + { + type: 'openliMa_set', + title: 'OpenLiteSpeed', + }, + ], + mysqld: [ + { + type: 'change_data_path', + title: lan.soft.save_path, + }, + { + type: 'change_mysql_port', + title: lan.site.port, + }, + { + type: 'change_mysql_ssl', + title: lan.site.site_menu_7, + }, + { + type: 'get_mysql_run_status', + title: lan.soft.status, + }, + { + type: 'get_mysql_status', + title: lan.soft.php_main7, + }, + { + type: 'mysql_log', + title: lan.soft.log, + }, + { + type: 'mysql_slow_log', + title: lan.public.slow_log, + }, + ], + phpmyadmin: [ + { + type: 'phpmyadmin_php', + title: lan.soft.php_version, + }, + { + type: 'phpmyadmin_safe', + title: lan.soft.safe, + }, + ], + memcached: [ + { + type: 'memcached_status', + title: lan.soft.php_main8, + }, + { + type: 'memcached_set', + title: lan.soft.php_main7, + }, + ], + redis: [ + { + type: 'get_redis_status', + title: lan.soft.php_main8, + }, + ], + tomcat: [ + { + type: 'log', + title: lan.soft.run_log, + }, + ], + apache: [ + { + type: 'apache_set', + title: lan.soft.php_main7, + }, + { + type: 'apache_status', + title: lan.soft.nginx_status, + }, + { + type: 'apache_format_log', + title: 'Logs format', + }, + { + type: 'log', + title: lan.soft.run_log, + }, + ], + nginx: [ + { + type: 'nginx_set', + title: lan.soft.php_main7, + }, + { + type: 'nginx_status', + title: lan.soft.nginx_status, + }, + { + type: 'nginx_format_log', + title: 'Logs format', + }, + { + type: 'log', + title: lan.soft.err_log, + }, + ], + }; + var arrs = datas.public; + if (name == 'phpmyadmin') arrs = []; + if (name == 'openlitespeed') arrs.length = 1; + if (name === 'pureftpd') arrs.push({ type: 'pureftpd_log', title: 'Logs Manage' }); + arrs = arrs.concat(datas[name]); + if (arrs) { + for (var i = 0; i < arrs.length; i++) { + var item = arrs[i]; + if (item) { + var tit = item.title.length >= 24 ? item.title : ''; + meun += '

                                  ' + item.title + '

                                  '; + } + } + } + } + return meun; + }, + set_soft_config: function (name) { + //软件设置 + var _this = this; + var loading = bt.load(); + bt.soft.get_soft_find(name, function (rdata) { + loading.close(); + + if (name == 'mysql') name = 'mysqld'; + var menuing = bt.open({ + type: 1, + area: '800px', + title: name + lan.soft.admin, + closeBtn: 2, + shift: 0, + content: + '
                                  ', + }); + var menu = $('.bt-soft-menu').data('data', rdata); + setTimeout(function () { + menu.append($('

                                  ' + lan.soft.service + '

                                  ')); + if (rdata.version_coexist) { + var ver = name.split('-')[1].replace('.', ''); + var opt_list = [ + { + type: 'set_php_config', + val: ver, + title: lan.soft.php_main5, + }, + { + type: 'config_edit', + val: ver, + title: lan.soft.config_edit, + }, + { + type: 'set_upload_limit', + val: ver, + title: lan.soft.php_main2, + }, + { + type: 'set_timeout_limit', + val: ver, + title: lan.soft.php_main3, + php53: true, + }, + { + type: 'config', + val: ver, + title: lan.soft.php_main4, + }, + { type: 'fpm_config', val: ver, title: 'FPM profile' }, + { + type: 'set_dis_fun', + val: ver, + title: lan.soft.php_main6, + }, + { + type: 'set_fpm_config', + val: ver, + title: lan.soft.php_main7, + apache24: true, + php53: true, + }, + { + type: 'get_php_status', + val: ver, + title: lan.soft.php_main8, + apache24: true, + php53: true, + }, + { + type: 'get_php_session', + val: ver, + title: lan.soft.php_main9, + apache24: true, + php53: true, + }, + { + type: 'get_fpm_logs', + val: ver, + title: lan.soft.log, + apache24: true, + php53: true, + }, + { + type: 'get_slow_logs', + val: ver, + title: lan.public.slow_log, + apache24: true, + php53: true, + }, + { + type: 'get_phpinfo', + val: ver, + title: 'phpinfo', + }, + ]; + + var phpSort = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + webcache = bt.get_cookie('serverType') == 'openlitespeed' ? true : false; + for (var i = 0; i < phpSort.length; i++) { + var item = opt_list[i]; + if (item) { + if (item.os == undefined || item['os'] == bt.os) { + if (name.indexOf('5.2') >= 0 && item.php53) continue; + if (webcache && (item.type == 'set_fpm_config' || item.type == 'get_php_status')) continue; + var apache24 = item.apache24 ? 'class="apache24"' : ''; + menu.append($('

                                  ' + item.title + '

                                  ').data('item', item)); + } + } + } + } else { + menu.append(soft.get_config_menu(name)); + } + $('.bt-w-menu p').click(function () { + $(this).addClass('bgw').siblings().removeClass('bgw'); + }); + $('.bt-w-menu p:eq(0)').trigger('click'); + bt.soft.get_soft_find('apache', function (rdata) { + if (rdata.setup) { + if (rdata.version.indexOf('2.2') >= 0) { + if (name.indexOf('php-') != -1) { + $('.apache24').hide(); + $('.bt_server').remove(); + $('.bt-w-menu p:eq(0)').trigger('click'); + } + + if (name.indexOf('apache') != -1) { + $('.bt-soft-menu p:eq(3)').remove(); + $('.bt-soft-menu p:eq(3)').remove(); + } + } + } + }); + }, 100); + }); + }, + get_tab_contents: function ( + key, + obj //获取设置菜单操作 + ) { + var data = $(obj).parents('.bt-soft-menu').data('data'); + var version = data.name; + if (data.name.indexOf('php-') >= 0) version = data.name.split('-')[1].replace('.', ''); + switch (key) { + case 'pureftpd_log': //ftp日志管理 + var tabCon = $('.soft-man-con').empty(); + bt.pub.get_ftp_logs(function (_status) { + tabCon.append( + '
                                  \ + Logs manage switch\ +
                                  \ + \ + \ +
                                  \ +
                                  • After enabling it, all login and operation records of FTP users will be logged.
                                  ' + ); + $('.ftp-log .isFtplog') + .unbind('click') + .click(function () { + var status = $(this).prev().prop('checked'); + bt.pub.set_ftp_logs(status ? 'stop' : 'start'); + }); + var pro = parseInt(bt.get_cookie('pro_end') || -1); + if (pro < 0) { + tabCon.append( + '
                                  \ +
                                  This feature is exclusive to the Professional version, Buy Now
                                  ' + ); + } + }); + break; + case 'service': + var tabCon = $('.soft-man-con').empty(); + var status_list = [ + { + opt: data.status ? 'stop' : 'start', + title: data.status ? lan.soft.stop : lan.soft.start, + }, + { + opt: 'restart', + title: lan.soft.restart, + }, + { + opt: 'reload', + title: lan.soft.reload, + }, + ]; + if (data.name == 'phpmyadmin') { + status_list = [status_list[0]]; + } else { + var btns = $('
                                  '); + for (var i = 0; i < status_list.length; i++) + btns.append(''); + tabCon.append( + '

                                  ' + + lan.soft.status + + ':' + + (data.status ? lan.soft.on : lan.soft.off) + + '

                                  '); + // for (var i = 0; i < status_list.length; i++) btns.append(''); + // tabCon.append('

                                  ' + lan.soft.status + ':' + (data.status ? lan.soft.running : lan.soft.stop) + '\ +

                                  \ +

                                  Public access address: ' + + data.ext.url + + '

                                  \ +
                                  ' + ); + tabCon.append( + '
                                    \ +
                                  • PhpMyAdmin enabling public access may have security risks. It is recommended not to enable it unnecessarily!
                                  • \ +
                                  • The current version of phpmyadin no longer relies on Nginx / Apache without requiring public access.
                                  • \ +
                                  • The service state of phpMyAdmin does not affect access to phpMyAdmin through the panel (non-public).
                                  • \ +
                                  • If the public access right is not turned on, the panel will take over the access right, that is, you need to log in to the panel to access.
                                  • \ +
                                  ' + ); + } + + var help = '
                                  • ' + lan.soft.mysql_mem_err + '
                                  '; + if (name == 'mysqld') tabCon.append(help); + break; + case 'config': + var tabCon = $('.soft-man-con').empty(); + tabCon.append('

                                  ' + lan.bt.edit_ps + '

                                  '); + tabCon.append(''); + tabCon.append(''); + tabCon.append(bt.render_help([lan.get('config_edit_ps', [version])])); + + var fileName = bt.soft.get_config_path(version); + if (data.php_ini) fileName = data.php_ini; + var loadT = bt.load(lan.soft.get); + bt.send( + 'GetFileBody', + 'files/GetFileBody', + { + path: fileName, + }, + function (rdata) { + loadT.close(); + $('#textBody').text(rdata.data); + $('.CodeMirror').remove(); + var editor = CodeMirror.fromTextArea(document.getElementById('textBody'), { + extraKeys: { + 'Ctrl-Space': 'autocomplete', + }, + lineNumbers: true, + matchBrackets: true, + }); + editor.focus(); + $('.CodeMirror-scroll').css({ + height: '510px', + margin: 0, + padding: 0, + }); + $('#OnlineEditFileBtn').click(function () { + $('#textBody').text(editor.getValue()); + bt.soft.save_config(fileName, editor.getValue()); + }); + } + ); + break; + case 'fpm_config': + var tabCon = $('.soft-man-con').empty(); + tabCon.append('

                                  ' + lan.bt.edit_ps + '

                                  '); + tabCon.append('
                                  '); + tabCon.append(''); + var _arry = ['If you do not understand the php-fpm configuration file, please do not modify it!']; + tabCon.append(bt.render_help(_arry)); + $('.return_php_info').click(function () { + $('.bt-soft-menu p:eq(12)').click(); + }); + var fileName = bt.soft.get_config_path(version).replace('php.ini', 'php-fpm.conf'); + var loadT = bt.load(lan.soft.get); + var config = bt.aceEditor({ el: 'textBody', path: fileName }); + $('#OnlineEditFileBtn').click(function () { + bt.saveEditor(config); + }); + break; + case 'change_version': + var _list = []; + var opt_version = ''; + for (var i = 0; i < data.versions.length; i++) { + if (data.versions[i].setup) opt_version = data.name + ' ' + data.versions[i].m_version; + _list.push({ + value: data.name + ' ' + data.versions[i].m_version, + title: data.name + ' ' + data.versions[i].m_version, + }); + } + var _form_data = { + title: lan.soft.select_version, + items: [ + { + name: 'phpVersion', + width: '160px', + type: 'select', + value: opt_version, + items: _list, + }, + { + name: 'btn_change_version', + type: 'button', + text: lan.soft.version_to, + callback: function (ldata) { + if (ldata.phpVersion == opt_version) { + bt.msg({ + msg: 'Is already[' + opt_version + ']', + icon: 2, + }); + return; + } + if (data.name == 'mysql') { + var ver = ldata.phpVersion.split('mysql '), + pdata = { sName: 'mysql', version: ver[1], type: 0 }; + $.post('/plugin?action=check_install_limit', pdata, function (rdata) { + if (rdata !== null && rdata.status == false) { + bt.msg({ msg: rdata.msg, icon: 2, time: 3000 }); + return false; + } + bt.database.get_list(1, '', function (ddata) { + if (ddata.data.length > 0) { + bt.msg({ + msg: lan.soft.mysql_d, + icon: 5, + time: 5000, + }); + return; + } + bt.soft.install_soft(data, ldata.phpVersion.split(' ')[1], 0); + }); + }); + } else { + bt.soft.install_soft(data, ldata.phpVersion.split(' ')[1], 0); + } + }, + }, + ], + }; + bt.render_form_line(_form_data, '', $('.soft-man-con').empty()); + break; + case 'change_data_path': + bt.send('GetMySQLInfo', 'database/GetMySQLInfo', {}, function (rdata) { + var form_data = { + items: [ + { + type: 'text', + name: 'datadir', + value: rdata.datadir, + event: { + css: 'glyphicon-folder-open', + callback: function (obj) { + bt.select_path(obj); + }, + }, + }, + { + name: 'btn_change_path', + type: 'button', + text: lan.soft.mysql_to, + callback: function (ldata) { + var loadT = bt.load(lan.soft.mysql_to_msg1); + bt.send( + 'SetDataDir', + 'database/SetDataDir', + { + datadir: ldata.datadir, + }, + function (rdata) { + loadT.close(); + bt.msg(rdata); + } + ); + }, + }, + ], + }; + bt.render_form_line(form_data, '', $('.soft-man-con').empty()); + }); + break; + case 'change_mysql_port': + bt.send('GetMySQLInfo', 'database/GetMySQLInfo', {}, function (rdata) { + var form_data = { + items: [ + { + type: 'text', + width: '100px', + name: 'port', + value: rdata.port, + }, + { + name: 'btn_change_port', + type: 'button', + text: lan.public.edit, + callback: function (ldata) { + var loadT = bt.load(); + bt.send( + 'SetMySQLPort', + 'database/SetMySQLPort', + { + port: ldata.port, + }, + function (rdata) { + loadT.close(); + bt.msg(rdata); + } + ); + }, + }, + ], + }; + bt.render_form_line(form_data, '', $('.soft-man-con').empty()); + }); + break; + case 'change_mysql_ssl': + bt.send('check_mysql_ssl_status', 'database/check_mysql_ssl_status', {}, function (rdata) { + var form_data = { + title: 'Mysql SSL', + items: [ + { + type: 'switch', + name: 'write_ssl', + value: rdata, + }, + ], + }; + bt.render_form_line(form_data, '', $('.soft-man-con').empty()); + var downssl = '/www/server/data/ssl.zip'; + $('.soft-man-con').append( + bt.render_help(['After setting, manually restart the database to take effect', "Download Mysql SSL self-signed certificate【SSL.zip】"]) + ); + $('a.downssl').click(function () { + window.open('/download?filename=' + encodeURIComponent(downssl)); + }); + $('#write_ssl').change(function () { + var loadT = bt.load(); + $.post('/database?action=write_ssl_to_mysql', function (rdata) { + loadT.close(loadT); + var open_type = $('#write_ssl').prop('checked') ? 'turned on' : 'turned off', + loadP = layer.confirm( + 'The SSL setting is ' + open_type + ' successfully.
                                  Do you need to restart the database immediately to make it effective?', + { + btn: ['Restart now', 'Restart later'], + icon: 3, + title: 'Confirm Restart?', + }, + function () { + bt.pub.set_server_status('mysql', 'restart'); + }, + function () { + layer.close(loadP); + } + ); + }); + }); + }); + break; + case 'get_mysql_run_status': + bt.send('GetRunStatus', 'database/GetRunStatus', {}, function (rdata) { + var cache_size = ((parseInt(rdata.Qcache_hits) / (parseInt(rdata.Qcache_hits) + parseInt(rdata.Qcache_inserts))) * 100).toFixed(2) + '%'; + if (cache_size == 'NaN%') cache_size = 'OFF'; + var title10 = ((1 - rdata.Threads_created / rdata.Connections) * 100).toFixed(2); + var title11 = ((1 - rdata.Key_reads / rdata.Key_read_requests) * 100).toFixed(2); + var title12 = ((1 - rdata.Innodb_buffer_pool_reads / rdata.Innodb_buffer_pool_read_requests) * 100).toFixed(2); + var title14 = ((rdata.Created_tmp_disk_tables / rdata.Created_tmp_tables) * 100).toFixed(2); + var Con = + '
                                  \ + \ + \ + \ + \ + \ + \ +
                                  ' + + lan.soft.mysql_status_title1 + + '' + + getLocalTime(rdata.Run) + + '' + + lan.soft.mysql_status_title5 + + '' + + parseInt(rdata.Questions / rdata.Uptime) + + '
                                  ' + + lan.soft.mysql_status_title2 + + '' + + rdata.Connections + + '' + + lan.soft.mysql_status_title6 + + '' + + parseInt((parseInt(rdata.Com_commit) + parseInt(rdata.Com_rollback)) / rdata.Uptime) + + '
                                  ' + + lan.soft.mysql_status_title3 + + '' + + ToSize(rdata.Bytes_sent) + + '' + + lan.soft.mysql_status_title7 + + '' + + rdata.File + + '
                                  ' + + lan.soft.mysql_status_title4 + + '' + + ToSize(rdata.Bytes_received) + + '' + + lan.soft.mysql_status_title8 + + '' + + rdata.Position + + '
                                  \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
                                  ' + + lan.soft.mysql_status_title9 + + '' + + rdata.Threads_running + + '/' + + rdata.Max_used_connections + + '' + + lan.soft.mysql_status_ps1 + + '
                                  ' + + lan.soft.mysql_status_title10 + + '' + + (!isNaN(title10) ? title10 : '0') + + '%' + + lan.soft.mysql_status_ps2 + + '
                                  ' + + lan.soft.mysql_status_title11 + + '' + + (!isNaN(title11) ? title11 : '0') + + '%' + + lan.soft.mysql_status_ps3 + + '
                                  ' + + lan.soft.mysql_status_title12 + + '' + + (!isNaN(title12) ? title12 : '0') + + '%' + + lan.soft.mysql_status_ps4 + + '
                                  ' + + lan.soft.mysql_status_title13 + + '' + + cache_size + + '' + + lan.soft.mysql_status_ps5 + + '
                                  ' + + lan.soft.mysql_status_title14 + + '' + + (!isNaN(title14) ? title14 : '0') + + '%' + + lan.soft.mysql_status_ps6 + + '
                                  ' + + lan.soft.mysql_status_title15 + + '' + + rdata.Open_tables + + '' + + lan.soft.mysql_status_ps7 + + '
                                  ' + + lan.soft.mysql_status_title16 + + '' + + rdata.Select_full_join + + '' + + lan.soft.mysql_status_ps8 + + '
                                  ' + + lan.soft.mysql_status_title17 + + '' + + rdata.Select_range_check + + '' + + lan.soft.mysql_status_ps9 + + '
                                  ' + + lan.soft.mysql_status_title18 + + '' + + rdata.Sort_merge_passes + + '' + + lan.soft.mysql_status_ps10 + + '
                                  ' + + lan.soft.mysql_status_title19 + + '' + + rdata.Table_locks_waited + + '' + + lan.soft.mysql_status_ps11 + + '
                                  '; + $('.soft-man-con').html(Con); + }); + break; + case 'get_mysql_status': + bt.send('GetDbStatus', 'database/GetDbStatus', {}, function (rdata) { + var key_buffer_size = bt.format_size(rdata.mem.key_buffer_size, false, 0, 'MB'); + var query_cache_size = bt.format_size(rdata.mem.query_cache_size, false, 0, 'MB'); + var tmp_table_size = bt.format_size(rdata.mem.tmp_table_size, false, 0, 'MB'); + var innodb_buffer_pool_size = bt.format_size(rdata.mem.innodb_buffer_pool_size, false, 0, 'MB'); + var innodb_additional_mem_pool_size = bt.format_size(rdata.mem.innodb_additional_mem_pool_size, false, 0, 'MB'); + var innodb_log_buffer_size = bt.format_size(rdata.mem.innodb_log_buffer_size, false, 0, 'MB'); + + var sort_buffer_size = bt.format_size(rdata.mem.sort_buffer_size, false, 0, 'MB'); + var read_buffer_size = bt.format_size(rdata.mem.read_buffer_size, false, 0, 'MB'); + var read_rnd_buffer_size = bt.format_size(rdata.mem.read_rnd_buffer_size, false, 0, 'MB'); + var join_buffer_size = bt.format_size(rdata.mem.join_buffer_size, false, 0, 'MB'); + var thread_stack = bt.format_size(rdata.mem.thread_stack, false, 0, 'MB'); + var binlog_cache_size = bt.format_size(rdata.mem.binlog_cache_size, false, 0, 'MB'); + var a = key_buffer_size + query_cache_size + tmp_table_size + innodb_buffer_pool_size + innodb_additional_mem_pool_size + innodb_log_buffer_size; + var b = sort_buffer_size + read_buffer_size + read_rnd_buffer_size + join_buffer_size + thread_stack + binlog_cache_size; + var memSize = a + rdata.mem.max_connections * b; + + var mysql_select = { + 1: { + title: '1-2GB', + data: { + key_buffer_size: 128, + query_cache_size: 64, + tmp_table_size: 64, + innodb_buffer_pool_size: 256, + sort_buffer_size: 768, + read_buffer_size: 768, + read_rnd_buffer_size: 512, + join_buffer_size: 1024, + thread_stack: 256, + binlog_cache_size: 64, + thread_cache_size: 64, + table_open_cache: 128, + max_connections: 100, + }, + }, + 2: { + title: '2-4GB', + data: { + key_buffer_size: 256, + query_cache_size: 128, + tmp_table_size: 384, + innodb_buffer_pool_size: 384, + sort_buffer_size: 768, + read_buffer_size: 768, + read_rnd_buffer_size: 512, + join_buffer_size: 2048, + thread_stack: 256, + binlog_cache_size: 64, + thread_cache_size: 96, + table_open_cache: 192, + max_connections: 200, + }, + }, + 3: { + title: '4-8GB', + data: { + key_buffer_size: 384, + query_cache_size: 192, + tmp_table_size: 512, + innodb_buffer_pool_size: 512, + sort_buffer_size: 1024, + read_buffer_size: 1024, + read_rnd_buffer_size: 768, + join_buffer_size: 2048, + thread_stack: 256, + binlog_cache_size: 128, + thread_cache_size: 128, + table_open_cache: 384, + max_connections: 300, + }, + }, + 4: { + title: '8-16GB', + data: { + key_buffer_size: 512, + query_cache_size: 256, + tmp_table_size: 1024, + innodb_buffer_pool_size: 1024, + sort_buffer_size: 2048, + read_buffer_size: 2048, + read_rnd_buffer_size: 1024, + join_buffer_size: 4096, + thread_stack: 384, + binlog_cache_size: 192, + thread_cache_size: 192, + table_open_cache: 1024, + max_connections: 400, + }, + }, + 5: { + title: '16-32GB', + data: { + key_buffer_size: 1024, + query_cache_size: 384, + tmp_table_size: 2048, + innodb_buffer_pool_size: 4096, + sort_buffer_size: 4096, + read_buffer_size: 4096, + read_rnd_buffer_size: 2048, + join_buffer_size: 8192, + thread_stack: 512, + binlog_cache_size: 256, + thread_cache_size: 256, + table_open_cache: 2048, + max_connections: 500, + }, + }, + }; + var mysql_arrs = [ + { + value: 0, + title: lan.soft.mysql_set_select, + }, + ]; + for (var key in mysql_select) + mysql_arrs.push({ + value: key, + title: mysql_select[key].title, + }); + + var form_datas = [ + { + items: [ + { + title: lan.soft.mysql_set_msg, + name: 'mysql_set', + type: 'select', + items: mysql_arrs, + callback: function (item) { + if (item.val() > 0) { + var data = mysql_select[item.val()].data; + for (var key in data) $('.' + key).val(data[key]); + if (!data.query_cache_size) data['query_cache_size'] = 0; + $("input[name='max_connections']").trigger('change'); + } + }, + }, + { + title: lan.soft.mysql_set_maxmem, + name: 'memSize', + width: '70px', + disabled: true, + value: memSize.toFixed(2), + ps: 'MB', + }, + ], + }, + { + title: 'key_buffer_size', + type: 'number', + name: 'key_buffer_size', + width: '70px', + value: key_buffer_size, + ps: 'MB, ' + lan.soft.mysql_set_key_buffer_size + '', + }, + { + title: 'query_cache_size', + type: 'number', + name: 'query_cache_size', + width: '70px', + value: query_cache_size, + ps: 'MB, ' + lan.soft.mysql_set_query_cache_size + '', + }, + { + title: 'tmp_table_size', + type: 'number', + name: 'tmp_table_size', + width: '70px', + value: tmp_table_size, + ps: 'MB, ' + lan.soft.mysql_set_tmp_table_size + '', + }, + { + title: 'innodb_buffer_pool_size', + type: 'number', + name: 'innodb_buffer_pool_size', + value: innodb_buffer_pool_size, + width: '70px', + ps: 'MB, ' + lan.soft.mysql_set_innodb_buffer_pool_size + '', + }, + { + title: 'innodb_log_buffer_size', + type: 'number', + name: 'innodb_log_buffer_size', + value: innodb_log_buffer_size, + width: '70px', + ps: 'MB, ' + lan.soft.mysql_set_innodb_log_buffer_size + '', + }, + { + title: 'sort_buffer_size', + type: 'number', + name: 'sort_buffer_size', + width: '70px', + value: sort_buffer_size * 1024, + ps: 'KB * ' + lan.soft.mysql_set_conn + ', ' + lan.soft.mysql_set_sort_buffer_size + '', + }, + { + title: 'read_buffer_size', + type: 'number', + name: 'read_buffer_size', + width: '70px', + value: read_buffer_size * 1024, + ps: 'KB * ' + lan.soft.mysql_set_conn + ', ' + lan.soft.mysql_set_read_buffer_size + '', + }, + { + title: 'read_rnd_buffer_size', + type: 'number', + name: 'read_rnd_buffer_size', + width: '70px', + value: read_rnd_buffer_size * 1024, + ps: 'KB * ' + lan.soft.mysql_set_conn + ', ' + lan.soft.mysql_set_read_rnd_buffer_size + '', + }, + { + title: 'join_buffer_size', + type: 'number', + name: 'join_buffer_size', + width: '70px', + value: join_buffer_size * 1024, + ps: 'KB * ' + lan.soft.mysql_set_conn + ', ' + lan.soft.mysql_set_join_buffer_size + '', + }, + { + title: 'thread_stack', + type: 'number', + name: 'thread_stack', + width: '70px', + value: thread_stack * 1024, + ps: 'KB * ' + lan.soft.mysql_set_conn + ', ' + lan.soft.mysql_set_thread_stack + '', + }, + { + title: 'binlog_cache_size', + type: 'number', + name: 'binlog_cache_size', + value: binlog_cache_size * 1024, + width: '70px', + ps: 'KB * ' + lan.soft.mysql_set_conn + ', ' + lan.soft.mysql_set_binlog_cache_size + '', + }, + { + title: 'thread_cache_size', + type: 'number', + name: 'thread_cache_size', + value: rdata.mem.thread_cache_size, + width: '70px', + ps: lan.soft.mysql_set_thread_cache_size, + }, + { + title: 'table_open_cache', + type: 'number', + name: 'table_open_cache', + value: rdata.mem.table_open_cache, + width: '70px', + ps: lan.soft.mysql_set_table_open_cache, + }, + { + title: 'max_connections', + type: 'number', + name: 'max_connections', + value: rdata.mem.max_connections, + width: '70px', + ps: lan.soft.mysql_set_max_connections, + }, + { + items: [ + { + text: lan.soft.mysql_set_restart, + type: 'button', + name: 'bt_mysql_restart', + callback: function (ldata) { + bt.pub.set_server_status('mysqld', 'restart'); + }, + }, + { + text: lan.public.save, + type: 'button', + name: 'bt_mysql_save', + callback: function (ldata) { + ldata.query_cache_type = 0; + if (ldata.query_cache_size > 0) ldata.query_cache_type = 1; + ldata['max_heap_table_size'] = ldata.tmp_table_size; + bt.send('SetDbConf', 'database/SetDbConf', ldata, function (rdata) { + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 2, + }); + }); + }, + }, + ], + }, + ]; + var tabCon = $('.soft-man-con').empty().append("
                                  "); + for (var i = 0; i < form_datas.length; i++) { + bt.render_form_line(form_datas[i], '', $('.tab-db-status')); + } + + $(".tab-db-status input[name*='size'],.tab-db-status input[name='max_connections'],.tab-db-status input[name='thread_stack']").change(function () { + var key_buffer_size = parseInt($("input[name='key_buffer_size']").val()); + var query_cache_size = parseInt($("input[name='query_cache_size']").val()); + var tmp_table_size = parseInt($("input[name='tmp_table_size']").val()); + var innodb_buffer_pool_size = parseInt($("input[name='innodb_buffer_pool_size']").val()); + var innodb_log_buffer_size = parseInt($("input[name='innodb_log_buffer_size']").val()); + + var sort_buffer_size = $("input[name='sort_buffer_size']").val() / 1024; + var read_buffer_size = $("input[name='read_buffer_size']").val() / 1024; + var read_rnd_buffer_size = $("input[name='read_rnd_buffer_size']").val() / 1024; + var join_buffer_size = $("input[name='join_buffer_size']").val() / 1024; + var thread_stack = $("input[name='thread_stack']").val() / 1024; + var binlog_cache_size = $("input[name='binlog_cache_size']").val() / 1024; + var max_connections = $("input[name='max_connections']").val(); + + var a = key_buffer_size + query_cache_size + tmp_table_size + innodb_buffer_pool_size + innodb_additional_mem_pool_size + innodb_log_buffer_size; + var b = sort_buffer_size + read_buffer_size + read_rnd_buffer_size + join_buffer_size + thread_stack + binlog_cache_size; + var memSize = a + max_connections * b; + $("input[name='memSize']").val(memSize.toFixed(2)); + }); + }); + break; + case 'mysql_log': + var loadT = bt.load(); + bt.send( + 'BinLog', + 'database/BinLog', + { + status: 1, + }, + function (rdata) { + loadT.close(); + var limitCon = + '

                                  \ + ' + + lan.soft.mysql_log_bin + + ' ' + + ToSize(rdata.msg) + + '\ + \ +

                                  ' + + lan.soft.mysql_log_err + + '

                                  \ + \ +

                                  '; + $('.soft-man-con').html(limitCon); + + //设置二进制日志 + $('.btn-bin').click(function () { + var loadT = layer.msg(lan.public.the, { + icon: 16, + time: 0, + shade: 0.3, + }); + $.post('/database?action=BinLog', '', function (rdata) { + layer.close(loadT); + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 5, + }); + soft.get_tab_contents('mysql_log'); + }); + }); + + //清空日志 + $('.btn-clear').click(function () { + var loadT = layer.msg(lan.public.the, { + icon: 16, + time: 0, + shade: 0.3, + }); + $.post('/database?action=GetErrorLog', 'close=1', function (rdata) { + layer.close(loadT); + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 5, + }); + soft.get_tab_contents('mysql_log'); + }); + }); + bt.send('GetErrorLog', 'database/GetErrorLog', {}, function (error_body) { + if (error_body.status === false) { + layer.msg(error_body.msg, { + icon: 5, + }); + error_body = lan.soft.mysql_log_ps1; + } + if (error_body == '') error_body = lan.soft.mysql_log_ps1; + $('#error_log').text(error_body); + var ob = document.getElementById('error_log'); + ob.scrollTop = ob.scrollHeight; + }); + } + ); + break; + case 'mysql_slow_log': + var loadT = bt.load(); + bt.send('GetSlowLogs', 'database/GetSlowLogs', {}, function (logs) { + loadT.close(); + if (!logs.status) { + logs.msg = ''; + } + if (logs.msg == '') logs.msg = lan.soft.no_slow_log; + var phpCon = ''; + $('.soft-man-con').html(phpCon); + var ob = document.getElementById('error_log'); + ob.scrollTop = ob.scrollHeight; + }); + break; + case 'log': + var loadT = bt.load(lan.public.the_get); + bt.send( + 'GetOpeLogs', + 'ajax/GetOpeLogs', + { + path: '/www/wwwlogs/nginx_error.log', + }, + function (rdata) { + loadT.close(); + if (rdata.msg == '') rdata.msg = lan.soft.no_log; + var ebody = + '
                                  '; + $('.soft-man-con').html(ebody); + var ob = document.getElementById('error_log'); + ob.scrollTop = ob.scrollHeight; + } + ); + break; + case 'nginx_status': + var loadT = bt.load(); + bt.send('GetNginxStatus', 'ajax/GetNginxStatus', {}, function (rdata) { + loadT.close(); + $('.soft-man-con').html("
                                  "); + var arrs = []; + arrs[lan.bt.nginx_active] = rdata.active; + arrs[lan.bt.nginx_accepts] = rdata.accepts; + arrs[lan.bt.nginx_handled] = rdata.handled; + arrs[lan.bt.nginx_requests] = rdata.requests; + arrs[lan.bt.nginx_reading] = rdata.Reading; + arrs[lan.bt.nginx_writing] = rdata.Writing; + arrs[lan.bt.nginx_waiting] = rdata.Waiting; + arrs[lan.bt.nginx_worker] = rdata.worker; + arrs[lan.bt.nginx_workercpu] = rdata.workercpu; + arrs[lan.bt.nginx_workermen] = rdata.workermen; + bt.render_table('tab-nginx-status', arrs); + }); + break; + case 'nginx_format_log': + var loadT = bt.load(); + bt.send('get_nginx_access_log_format', 'config/get_nginx_access_log_format', {}, function (rdata) { + $('.soft-man-con').html( + "
                                  NameFormatOpt
                                  " + ); + bt.send('get_nginx_access_log_format_parameter', 'config/get_nginx_access_log_format_parameter', {}, function (res) { + loadT.close(); + var _format_ul = '
                                    '; + Object.keys(res).map(function (key) { + _format_ul += '
                                  • ' + key + ' : ' + res[key] + '
                                  • '; + }); + _format_ul += '
                                  '; + for (const j in rdata) { + if (rdata.hasOwnProperty(j)) { + const result = rdata[j]; + var _format = result.map(function (item, index) { + return Object.keys(item)[0]; + }); + var element = '' + _format.join('') + '', + _td = + '' + + j + + '\ + ' + + element + + '\ + Apply | Set | Del'; + $('#tab-nginx-logs-format tbody').append(_td); + //表格头固定 + $('#tab-nginx-logs-format') + .parent() + .on('scroll', function () { + var scrollTop = $('#tab-nginx-logs-format').parent().scrollTop(); + $('#tab-nginx-logs-format thead').css({ transform: 'translateY(' + scrollTop + 'px)', position: 'relative', 'z-index': '1' }); + }); + } + } + $('.table-add-format, .table-set-format').click(function () { + if ($(this).hasClass('table-set-format')) { + var format_title = 'Set format', + first_format = '', + add_type = 'edit', + td_format = $(this).parent().prev().find('.nginx-one-format'); + format_name = $(this).parent().attr('data-name'); + for (var i = 0; i < td_format.length; i++) { + first_format += + '
                                  \ +
                                  \ +
                                  \ +
                                  ' + + td_format.eq(i).text() + + '
                                  \ + \ +
                                  \ +
                                  \ + Del\ +
                                  '; + } + } else { + var format_title = 'Add format', + format_name = '', + add_type = 'add', + first_format = '', + format_list = ['$http_x_forwarded_for', '$remote_addr', '-', '[$time_local]', '$request', '$status', '$body_bytes_sent', '$http_referer', '$http_user_agent']; + for (var i = 0; i < format_list.length; i++) { + first_format += + '
                                  \ +
                                  \ +
                                  \ +
                                  ' + + format_list[i] + + '
                                  \ + \ +
                                  \ +
                                  \ + Del\ +
                                  '; + } + } + layer.open({ + type: 1, + title: format_title, + closeBtn: 2, + area: '375px', + btn: ['Confirm', 'Cancel'], + content: + '
                                  \ +
                                  \ + Name: \ + \ +
                                  \ + Format:\ +
                                  \ +
                                  ' + + first_format + + '
                                  \ + ' + + _format_ul + + '\ +
                                  \ + \ +
                                  • The format are executed in the order of parameters.
                                  \ +
                                  ', + success: function (index, layero) { + $('.nginx-add-format [name=log_format_name]').val(format_name); + $('.nginx-add-format').parents('.layui-layer-content').css('overflow', 'inherit'); + $('.nginx-add-format').on('click', '.bt-select-input', function (e) { + if ($(this).hasClass('active')) { + $('.nginx-add-format .bt-select-list').removeClass('active'); + $(this).removeClass('active').find('.bt-down-icon').css('transform', 'rotate(-45deg)'); + } else { + var _choose = $(this).find('.bt-select-val').text(); + $('.bt-select-list li').removeClass('active'); + $('.active.bt-select-input').removeClass('active').find('.bt-down-icon').css('transform', 'rotate(-45deg)'); + $('.bt-select-list li:contains(' + _choose + ')').addClass('active'); + $('.nginx-add-format .bt-select-list') + .addClass('active') + .css('top', $(this).offset().top - $('.format-table').offset().top + 33); + $(this).addClass('active').find('.bt-down-icon').css('transform', 'rotate(135deg)'); + } + e.stopPropagation(); + $(document).click(function (e) { + $('.active.bt-select-list').removeClass('active'); + $(this).find('.bt-down-icon').css('transform', 'rotate(-45deg)'); + e.preventDefault(); + e.stopPropagation(); + }); + }); + $('.nginx-add-format').on('click', '.bt-select-list li', function (e) { + var _value = $(this).attr('data-val'); + $('.active.bt-select-input').find('.bt-select-val').attr('data-active', _value).text(_value); + $('.nginx-add-format .bt-select-list,.active.bt-select-input').removeClass('active'); + }); + $('.btn-add-format').click(function (e) { + var _new_line = + '
                                  \ +
                                  \ +
                                  \ +
                                  $server_name
                                  \ + \ +
                                  \ +
                                  \ + Del\ +
                                  '; + $('.format-table').append(_new_line); + $('.format-table').scrollTop(10000000); + }); + $('.nginx-add-format').on('click', '.del-format', function (e) { + if ($('.del-format').length == 1) { + layer.msg('This is the last parameter.', { icon: 2 }); + return false; + } + $(this).parent().remove(); + }); + }, + yes: function (index, layero) { + if ($('.nginx-add-format [name=log_format_name]').val() == '') { + layer.msg('The format name cannot be empty!', { icon: 2 }); + return false; + } + var log_format = []; + $('.nginx-add-format .format-table .bt-select-val').each(function () { + log_format.push($(this).attr('data-active')); + }); + var format_data = { + log_format_name: $('.nginx-add-format [name=log_format_name]').val(), + log_format: JSON.stringify(log_format), + act: add_type, + }; + bt.send('add_nginx_access_log_format', 'config/add_nginx_access_log_format', format_data, function (res) { + layer.close(index); + $('.bt-soft-menu p:contains("Logs format")').click(); + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + }); + }, + }); + }); + $('#tab-nginx-logs-format').on('click', '.table-del-format', function (e) { + var log_format_name = $(this).parent().attr('data-name'), + loadP = layer.confirm( + 'Confirm to delete【' + log_format_name + '】this logs format?', + { + title: 'Confirm Delete?', + closeBtn: 2, + }, + function () { + layer.close(loadP); + bt.send('del_nginx_access_log_format', 'config/del_nginx_access_log_format', { log_format_name: log_format_name }, function (res) { + if (res.status) $('.bt-soft-menu p:contains("Logs format")').click(); + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + }); + } + ); + }); + $('#tab-nginx-logs-format').on('click', '.table-apply-format', function (e) { + var log_format_name = $(this).parent().attr('data-name'); + bt.send('get_nginx_access_log_format_parameter', 'config/get_nginx_access_log_format_parameter', { log_format_name: log_format_name }, function (res) { + if (Object.keys(res.site_list).length == 0) { + layer.msg('There is no site can apply!', { icon: 2 }); + return false; + } + var _site_ul = '
                                    '; + Object.keys(res.site_list).map(function (key) { + _site_ul += '
                                  • ' + key + '
                                  • '; + }); + _site_ul += '
                                  '; + layer.open({ + type: 1, + title: 'Website apply format', + closeBtn: 2, + btn: ['Confirm', 'Cancel'], + content: + '
                                  \ +
                                  \ + Site: \ + ' + + _site_ul + + '\ +
                                  \ +
                                  The checked site would used the format.
                                  \ +
                                  ', + success: function (index, layero) { + $('.nginx-add-site').on('click', '.format-site-list li', function (e) { + if ($(this).find('.bt_checkbox_groups').hasClass('active')) { + $(this).find('.bt_checkbox_groups').removeClass('active'); + } else { + $(this).find('.bt_checkbox_groups').addClass('active'); + } + }); + }, + yes: function (index, layero) { + var sites = []; + $('.nginx-add-site .format-site-list .bt_checkbox_groups.active').each(function () { + sites.push($(this).attr('data-val')); + }); + var format_data = { + log_format_name: log_format_name, + sites: JSON.stringify(sites), + }; + bt.send('set_format_log_to_website', 'config/set_format_log_to_website', format_data, function (res) { + layer.close(index); + if (res.status) $('.bt-soft-menu p:contains("Logs format")').click(); + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + }); + }, + }); + }); + }); + }); + }); + break; + case 'apache_format_log': + var loadT = bt.load(); + bt.send('get_httpd_access_log_format', 'config/get_httpd_access_log_format', {}, function (rdata) { + $('.soft-man-con').html( + "
                                  NameFormatOpt
                                  " + ); + bt.send('get_httpd_access_log_format_parameter', 'config/get_httpd_access_log_format_parameter', {}, function (res) { + loadT.close(); + var _format_ul = '
                                    '; + Object.keys(res).map(function (key) { + _format_ul += '
                                  • ' + key + ' : ' + res[key] + '
                                  • '; + }); + _format_ul += '
                                  '; + for (const j in rdata) { + if (rdata.hasOwnProperty(j)) { + const result = rdata[j]; + var _format = result.map(function (item, index) { + return Object.keys(item)[0]; + }); + var element = '' + _format.join('') + '', + _td = + '' + + j + + '\ + ' + + element + + '\ + Apply | Set | Del'; + $('#tab-nginx-logs-format tbody').append(_td); + //表格头固定 + $('#tab-nginx-logs-format') + .parent() + .on('scroll', function () { + var scrollTop = $('#tab-nginx-logs-format').parent().scrollTop(); + $('#tab-nginx-logs-format thead').css({ transform: 'translateY(' + scrollTop + 'px)', position: 'relative', 'z-index': '1' }); + }); + } + } + $('.table-add-format, .table-set-format').click(function () { + if ($(this).hasClass('table-set-format')) { + var format_title = 'Set format', + first_format = '', + add_type = 'edit', + td_format = $(this).parent().prev().find('.nginx-one-format'); + format_name = $(this).parent().attr('data-name'); + for (var i = 0; i < td_format.length; i++) { + first_format += + '
                                  \ +
                                  \ +
                                  \ +
                                  ' + + td_format.eq(i).text() + + '
                                  \ + \ +
                                  \ +
                                  \ + Del\ +
                                  '; + } + } else { + var format_title = 'Add format', + format_name = '', + add_type = 'add', + first_format = '', + format_list = ['%{X-Forwarded-For}i', '%h', '%l', '%u', '%t', '%r', '%>s', '%b', '%{Referer}i', '%{User-agent}i']; + for (var i = 0; i < format_list.length; i++) { + first_format += + '
                                  \ +
                                  \ +
                                  \ +
                                  ' + + format_list[i] + + '
                                  \ + \ +
                                  \ +
                                  \ + Del\ +
                                  '; + } + } + layer.open({ + type: 1, + title: format_title, + closeBtn: 2, + area: '375px', + btn: ['Confirm', 'Cancel'], + content: + '
                                  \ +
                                  \ + Name: \ + \ +
                                  \ + Format:\ +
                                  \ +
                                  ' + + first_format + + '
                                  \ + ' + + _format_ul + + '\ +
                                  \ + \ +
                                  • The format are executed in the order of parameters.
                                  \ +
                                  ', + success: function (index, layero) { + $('.nginx-add-format [name=log_format_name]').val(format_name); + $('.nginx-add-format').parents('.layui-layer-content').css('overflow', 'inherit'); + $('.nginx-add-format').on('click', '.bt-select-input', function (e) { + if ($(this).hasClass('active')) { + $('.nginx-add-format .bt-select-list').removeClass('active'); + $(this).removeClass('active').find('.bt-down-icon').css('transform', 'rotate(-45deg)'); + } else { + var _choose = $(this).find('.bt-select-val').text(); + $('.bt-select-list li').removeClass('active'); + $('.active.bt-select-input').removeClass('active').find('.bt-down-icon').css('transform', 'rotate(-45deg)'); + $('.bt-select-list li:contains(' + _choose + ')').addClass('active'); + $('.nginx-add-format .bt-select-list') + .addClass('active') + .css('top', $(this).offset().top - $('.format-table').offset().top + 33); + $(this).addClass('active').find('.bt-down-icon').css('transform', 'rotate(135deg)'); + } + e.stopPropagation(); + $(document).click(function (e) { + $('.active.bt-select-list').removeClass('active'); + $(this).find('.bt-down-icon').css('transform', 'rotate(-45deg)'); + e.preventDefault(); + e.stopPropagation(); + }); + }); + $('.nginx-add-format').on('click', '.bt-select-list li', function (e) { + var _value = $(this).attr('data-val'); + $('.active.bt-select-input').find('.bt-select-val').attr('data-active', _value).text(_value); + $('.nginx-add-format .bt-select-list,.active.bt-select-input').removeClass('active'); + }); + $('.btn-add-format').click(function (e) { + var _new_line = + '
                                  \ +
                                  \ +
                                  \ +
                                  %>s
                                  \ + \ +
                                  \ +
                                  \ + Del\ +
                                  '; + $('.format-table').append(_new_line); + $('.format-table').scrollTop(10000000); + }); + $('.nginx-add-format').on('click', '.del-format', function (e) { + if ($('.del-format').length == 1) { + layer.msg('This is the last parameter.', { icon: 2 }); + return false; + } + $(this).parent().remove(); + }); + }, + yes: function (index, layero) { + if ($('.nginx-add-format [name=log_format_name]').val() == '') { + layer.msg('The format name cannot be empty!', { icon: 2 }); + return false; + } + var log_format = []; + $('.nginx-add-format .format-table .bt-select-val').each(function () { + log_format.push($(this).attr('data-active')); + }); + var format_data = { + log_format_name: $('.nginx-add-format [name=log_format_name]').val(), + log_format: JSON.stringify(log_format), + act: add_type, + }; + bt.send('add_httpd_access_log_format', 'config/add_httpd_access_log_format', format_data, function (res) { + layer.close(index); + if (res.status) $('.bt-soft-menu p:contains("Logs format")').click(); + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + }); + }, + }); + }); + $('#tab-nginx-logs-format').on('click', '.table-del-format', function (e) { + var log_format_name = $(this).parent().attr('data-name'), + loadP = layer.confirm( + 'Confirm to delete【' + log_format_name + '】this logs format?', + { + title: 'Confirm Delete?', + closeBtn: 2, + }, + function () { + layer.close(loadP); + bt.send('del_httpd_access_log_format', 'config/del_httpd_access_log_format', { log_format_name: log_format_name }, function (res) { + if (res.status) $('.bt-soft-menu p:contains("Logs format")').click(); + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + }); + } + ); + }); + $('#tab-nginx-logs-format').on('click', '.table-apply-format', function (e) { + var log_format_name = $(this).parent().attr('data-name'); + bt.send('get_httpd_access_log_format_parameter', 'config/get_httpd_access_log_format_parameter', { log_format_name: log_format_name }, function (res) { + if (Object.keys(res.site_list).length == 0) { + layer.msg('There is no site can apply!', { icon: 2 }); + return false; + } + var _site_ul = '
                                    '; + Object.keys(res.site_list).map(function (key) { + _site_ul += '
                                  • ' + key + '
                                  • '; + }); + _site_ul += '
                                  '; + layer.open({ + type: 1, + title: 'Website apply format', + closeBtn: 2, + btn: ['Confirm', 'Cancel'], + content: + '
                                  \ +
                                  \ + Site: \ + ' + + _site_ul + + '\ +
                                  \ +
                                  The checked site would used the format.
                                  \ +
                                  ', + success: function (index, layero) { + $('.nginx-add-site').on('click', '.format-site-list li', function (e) { + if ($(this).find('.bt_checkbox_groups').hasClass('active')) { + $(this).find('.bt_checkbox_groups').removeClass('active'); + } else { + $(this).find('.bt_checkbox_groups').addClass('active'); + } + }); + }, + yes: function (index, layero) { + var sites = []; + $('.nginx-add-site .format-site-list .bt_checkbox_groups.active').each(function () { + sites.push($(this).attr('data-val')); + }); + var format_data = { + log_format_name: log_format_name, + sites: JSON.stringify(sites), + }; + bt.send('set_httpd_format_log_to_website', 'config/set_httpd_format_log_to_website', format_data, function (res) { + layer.close(index); + if (res.status) $('.bt-soft-menu p:contains("Logs format")').click(); + layer.msg(res.msg, { icon: res.status ? 1 : 2 }); + }); + }, + }); + }); + }); + }); + }); + break; + case 'apache_status': + var loadT = bt.load(); + bt.send('GetApacheStatus', 'ajax/GetApacheStatus', {}, function (rdata) { + loadT.close(); + $('.soft-man-con').html("
                                  "); + var arrs = []; + arrs[lan.bt.apache_uptime] = rdata.UpTime; + arrs[lan.bt.apache_idleworkers] = rdata.IdleWorkers; + arrs[lan.bt.apache_totalaccesses] = rdata.TotalAccesses; + arrs[lan.bt.apache_totalkbytes] = rdata.TotalKBytes; + arrs[lan.bt.apache_workermem] = rdata.workermem; + arrs[lan.bt.apache_workercpu] = rdata.workercpu; + arrs[lan.bt.apache_reqpersec] = rdata.ReqPerSec; + arrs[lan.bt.apache_restarttime] = rdata.RestartTime; + arrs[lan.bt.apache_busyworkers] = rdata.BusyWorkers; + bt.render_table('tab-Apache-status', arrs); + }); + break; + case 'nginx_set': + var loadT = bt.load(); + bt.send('GetNginxValue', 'config/GetNginxValue', {}, function (rdata) { + loadT.close(); + var form_datas = []; + for (var i = 0; i < rdata.length; i++) { + if (rdata[i].name == 'worker_processes') { + form_datas.push({ + title: rdata[i].name, + name: rdata[i].name, + width: '60px', + value: rdata[i].value, + ps: rdata[i].ps, + text: '', + }); + } else if (rdata[i].name == 'gzip') { + form_datas.push({ + title: rdata[i].name, + type: 'select', + items: [ + { + title: lan.soft.on, + value: 'on', + }, + { + title: lan.soft.off, + value: 'off', + }, + ], + name: rdata[i].name, + width: '60px', + value: rdata[i].value, + ps: rdata[i].ps, + text: '', + }); + } else { + form_datas.push({ + title: rdata[i].name, + type: 'number', + name: rdata[i].name, + width: '60px', + value: rdata[i].value, + ps: rdata[i].ps, + text: '', + }); + } + } + form_datas.push({ + items: [ + { + text: lan.public.save, + type: 'button', + name: 'bt_nginx_save', + callback: function (item) { + delete item['bt_nginx_save']; + bt.send('SetNginxValue', 'config/SetNginxValue', item, function (rdata) { + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 2, + }); + }); + }, + }, + ], + }); + $('.soft-man-con').empty().append('
                                  '); + for (var i = 0; i < form_datas.length; i++) { + bt.render_form_line(form_datas[i], '', $('.soft-man-con .set_nginx_config')); + } + }); + break; + case 'apache_set': + var loadT = bt.load(); + bt.send('GetNginxValue', 'config/GetApacheValue', {}, function (rdata) { + loadT.close(); + var form_datas = []; + for (var i = 0; i < rdata.length; i++) { + if (rdata[i].name == 'KeepAlive') { + form_datas.push({ + title: rdata[i].name, + type: 'select', + items: [ + { + title: lan.soft.on, + value: 'on', + }, + { + title: lan.soft.off, + value: 'off', + }, + ], + name: rdata[i].name, + width: '65px', + value: rdata[i].value, + ps: rdata[i].ps, + text: '', + }); + } else { + form_datas.push({ + title: rdata[i].name, + type: 'number', + name: rdata[i].name, + width: '65px', + value: rdata[i].value, + ps: rdata[i].ps, + text: '', + }); + } + } + form_datas.push({ + items: [ + { + text: lan.public.save, + type: 'button', + name: 'bt_apache_save', + callback: function (item) { + delete item['bt_apache_save']; + bt.send('SetApacheValue', 'config/SetApacheValue', item, function (rdata) { + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 2, + }); + }); + }, + }, + ], + }); + $('.soft-man-con').empty().append('
                                  '); + for (var i = 0; i < form_datas.length; i++) { + bt.render_form_line(form_datas[i], '', $('.soft-man-con .set_Apache_config')); + } + }); + break; + case 'memcached_status': + case 'memcached_set': + var loadT = bt.load(lan.public.get_the); + bt.send('GetMemcachedStatus', 'ajax/GetMemcachedStatus', {}, function (rdata) { + loadT.close(); + if (key == 'memcached_set') { + var form_data = [ + { + title: 'BindIP', + name: 'ip', + width: '120px', + value: rdata.bind, + ps: lan.soft.listen_ip_tips, + }, + { + title: 'PORT', + name: 'port', + type: 'number', + width: '120px', + value: rdata.port, + ps: lan.soft.listen_port_tips, + }, + { + title: 'CACHESIZE', + name: 'cachesize', + type: 'number', + width: '120px', + value: rdata.cachesize, + ps: lan.soft.cache_size, + }, + { + title: 'MAXCONN', + name: 'maxconn', + type: 'number', + width: '120px', + value: rdata.maxconn, + ps: lan.soft.mac_connect, + }, + { + title: ' ', + items: [ + { + text: lan.public.save, + name: 'btn_set_memcached', + type: 'button', + callback: function (ldata) { + if (ldata.ip.split('.').length < 4) { + layer.msg(lan.soft.ip_format_err, { + icon: 2, + }); + return; + } + if (ldata.port < 1 || ldata.port > 65535) { + layer.msg(lan.soft.port_range_err, { + icon: 2, + }); + return; + } + if (ldata.cachesize < 8) { + layer.msg(lan.soft.cache_too_small, { + icon: 2, + }); + return; + } + if (ldata.maxconn < 4) { + layer.msg(lan.soft.connect_too_small, { + icon: 2, + }); + return; + } + var loadT = bt.load(lan.public.the); + bt.send('SetMemcachedCache', 'ajax/SetMemcachedCache', ldata, function (rdata) { + loadT.close(); + bt.msg(rdata); + }); + }, + }, + ], + }, + ]; + var tabCon = $('.soft-man-con').empty(); + for (var i = 0; i < form_data.length; i++) { + bt.render_form_line(form_data[i], '', tabCon); + } + return; + } else { + var arr = {}; + arr['BindIP'] = [rdata.bind, lan.soft.listen_ip]; + arr['PORT'] = [rdata.port, lan.soft.listen_port]; + arr['CACHESIZE'] = [rdata.cachesize + ' MB', lan.soft.max_cache]; + arr['MAXCONN'] = [rdata.maxconn, lan.soft.max_connect_limit]; + arr['curr_connections'] = [rdata.curr_connections, lan.soft.curr_connect]; + arr['cmd_get'] = [rdata.cmd_get, lan.soft.get_request_num]; + arr['get_hits'] = [rdata.get_hits, lan.soft.get_hit_num]; + arr['get_misses'] = [rdata.get_misses, lan.soft.get_miss_num]; + arr['hit'] = [rdata.hit.toFixed(2) + ' %', lan.soft.get_hit_percent]; + arr['curr_items'] = [rdata.curr_items, lan.soft.curr_cache_rows]; + arr['evictions'] = [rdata.evictions, lan.soft.mem_not_enough]; + arr['bytes'] = [ToSize(rdata.bytes), lan.soft.curr_mem_use]; + arr['bytes_read'] = [ToSize(rdata.bytes_read), lan.soft.request_size_total]; + arr['bytes_written'] = [ToSize(rdata.bytes_written), lan.soft.send_size_total]; + + var con = + '
                                  ' + + lan.soft.field + + '' + + lan.soft.curr_val + + '' + + lan.soft.instructions + + '
                                  '; + $('.soft-man-con').html(con); + bt.render_table('tab_memcached_status', arr, true); + } + }); + break; + case 'phpmyadmin_php': + bt.send('GetPHPVersion', 'site/GetPHPVersion', {}, function (rdata) { + var sdata = $('.bt-soft-menu').data('data'); + + var body = + "
                                  " + + lan.soft.php_version + + "
                                  '; + $('.soft-man-con').html(body); + $('.btn-success').click(function () { + var loadT = bt.load(lan.public.the); + bt.send( + 'setPHPMyAdmin', + 'ajax/setPHPMyAdmin', + { + phpversion: $('#get_phpVersion').val(), + }, + function (rdata) { + loadT.close(); + bt.msg(rdata); + if (rdata.status) { + setTimeout(function () { + window.location.reload(); + }, 3000); + } + } + ); + }); + }); + break; + case 'phpmyadmin_safe': + var sdata = $('.bt-soft-menu').data('data'), + sslPortNum = ''; + var con = + '
                                  \ + ' + + lan.soft.pma_port + + '\ + \ + \ +
                                  \ +
                                  \ + Open SSL\ + \ + \ + \ +
                                  \ +
                                  \ + SSL port\ + \ + \ +
                                  \ +
                                  \ + ' + + lan.soft.pma_pass + + '\ + \ + \ + \ +
                                  \ +
                                  \ +

                                  ' + + lan.soft.pma_user + + '

                                  \ +

                                  ' + + lan.soft.pma_pass1 + + '

                                  \ +

                                  ' + + lan.soft.pma_pass2 + + '

                                  \ +

                                  \ +
                                  \ +
                                  • ' + + lan.soft.pma_ps + + '
                                  '; + + $('.soft-man-con').html(con); + if (sdata.ext.port) { + $('.user_pw').show(); + } + + function get_phpmyadmin_ssl() { + var loading = bt.load('Getting SSL Status...'); + bt.send('get_phpmyadmin_ssl', 'ajax/get_phpmyadmin_ssl', {}, function (tdata) { + loading.close(); + $('#ssl_safe_checkbox').prop('checked', tdata.status); + $('#sslport').val(tdata.port); + }); + } + get_phpmyadmin_ssl(); + $('.phpmyadmin_port').click(function () { + var pmport = $('#pmport').val(); + var loadT = bt.load(lan.public.the); + bt.send( + 'setPHPMyAdmin', + 'ajax/setPHPMyAdmin', + { + port: pmport, + }, + function (rdata) { + loadT.close(); + bt.msg(rdata); + } + ); + }); + $('.ssl_safe_label').click(function () { + var stat = $('#ssl_safe_checkbox').prop('checked'); + bt.send( + 'set_phpmyadmin_ssl', + 'ajax/set_phpmyadmin_ssl', + { + v: !stat ? 1 : 0, + }, + function (rdata) { + bt.msg(rdata); + } + ); + setTimeout(function () { + get_phpmyadmin_ssl(); + }, 500); + }); + $('.ssl_port_button').click(function () { + var sslPort = $('#sslport').val(); + if (!bt.check_port(sslPort)) { + layer.msg(lan.firewall.port_err, { + icon: 2, + }); + return; + } + var loadTo = bt.load(lan.public.the); + if (sslPort > 0) { + bt.send( + 'change_phpmyadmin_ssl_port', + 'ajax/change_phpmyadmin_ssl_port', + { + port: sslPort, + }, + function (rdata) { + loadTo.close(); + bt.msg(rdata); + } + ); + } + }); + $('.phpmyadmin_safe').click(function () { + var stat = $('#phpmyadminsafe').prop('checked'); + if (stat) { + $('.user_pw').hide(); + set_phpmyadmin('close'); + } else { + $('.user_pw').show(); + } + }); + $('.phpmyadmin_safe_save').click(function () { + set_phpmyadmin('get'); + }); + + function set_phpmyadmin(msg) { + var type = 'password'; + if (msg == 'close') { + bt.confirm( + { + msg: lan.soft.pma_pass_close, + }, + function () { + var loading = bt.load(lan.public.the); + bt.send( + 'setPHPMyAdmin', + 'ajax/setPHPMyAdmin', + { + password: msg, + siteName: 'phpmyadmin', + }, + function (rdata) { + loading.close(); + bt.msg(rdata); + } + ); + } + ); + return; + } else { + username = $('#username_get').val(); + password_1 = $('#password_get_1').val(); + password_2 = $('#password_get_2').val(); + if (username.length < 1 || password_1.length < 1) { + bt.msg({ + msg: lan.soft.pma_pass_empty, + icon: 2, + }); + return; + } + if (password_1 != password_2) { + bt.msg({ + msg: lan.soft.pass_err_re, + icon: 2, + }); + return; + } + } + var loading = bt.load(lan.public.the); + bt.send( + 'setPHPMyAdmin', + 'ajax/setPHPMyAdmin', + { + password: password_1, + username: username, + siteName: 'phpmyadmin', + }, + function (rdata) { + loading.close(); + bt.msg(rdata); + setTimeout(function () { + location.reload(); + }, 1000); + } + ); + } + break; + case 'set_php_config': + if (!obj.notLoading) var loading = bt.load(lan.public.the); + bt.soft.php.get_config(version, function (rdata) { + if (!obj.notLoading) loading.close(); + obj.notLoading = false; + var divObj = document.getElementById('phpextdiv'); + var scrollTopNum = 0; + if (divObj) scrollTopNum = divObj.scrollTop; + + $('.soft-man-con') + .empty() + .append( + '
                                  ' + ); + + var list = []; + for (var i = 0; i < rdata.libs.length; i++) { + if (rdata.libs[i].versions.indexOf(version) == -1) continue; + list.push(rdata.libs[i]); + } + var _tab = bt.render({ + table: '#tab_phpext', + data: list, + columns: [ + { + field: 'name', + title: lan.soft.php_ext_name, + }, + { + field: 'type', + title: lan.soft.php_ext_type, + width: 64, + }, + { + field: 'msg', + title: lan.soft.php_ext_ps, + }, + { + field: 'status', + title: lan.soft.php_ext_status, + width: 40, + templet: function (item) { + return ''; + }, + }, + { + field: 'opt', + title: lan.public.action, + width: 60, + templet: function (item) { + var opt = '' + lan.soft.install + ''; + if (item['task'] == '-1' && item.phpversions.indexOf(version) != -1) { + opt = '' + lan.soft.the_install + ''; + } else if (item['task'] == '0' && item.phpversions.indexOf(version) != -1) { + opt = '' + lan.soft.sleep_install + ''; + } else if (item.status) { + opt = '' + lan.soft.uninstall + ''; + } + return opt; + }, + }, + ], + }); + var helps = [lan.soft.php_plug_tips1, lan.soft.php_plug_tips2]; + $('.soft-man-con').append(bt.render_help(helps)); + + var divObj = document.getElementById('phpextdiv'); + if (divObj) divObj.scrollTop = scrollTopNum; + $('a').click(function () { + var _obj = $(this); + if (_obj.hasClass('lib-uninstall')) { + bt.soft.php.un_install_php_lib(version, _obj.attr('data-name'), _obj.attr('data-title'), function (rdata) { + setTimeout(function () { + soft.get_tab_contents('set_php_config', obj); + }, 1000); + }); + } else if (_obj.hasClass('lib-install')) { + bt.soft.php.install_php_lib(version, _obj.attr('data-name'), _obj.attr('data-title'), function (rdata) { + setTimeout(function () { + soft.get_tab_contents('set_php_config', obj); + }, 1000); + }); + } + }); + setTimeout(function () { + if ($('.bt-soft-menu .bgw').text() === 'Install extensions') { + obj.notLoading = true; + soft.get_tab_contents('set_php_config', obj); + } + }, 3000); + }); + break; + case 'get_phpinfo': + var con = ''; + var p_status = { + true: 'Yes', + false: 'No', + }; + var loading = bt.load(lan.public.the); + $.post( + '/ajax?action=php_info', + { + php_version: version, + }, + function (php_info) { + loading.close(); + con += ''; + con += '

                                  ' + lan.soft.php_base_info + '

                                  '; + con += '
                                  '; + con += ''; + con += ''; + con += ''; + con += '
                                  ' + lan.soft.version + '' + php_info.phpinfo.php_version + '' + lan.soft.install_path + '' + php_info.phpinfo.php_path + '
                                  php.ini' + php_info.phpinfo.php_ini + '
                                  ' + lan.soft.loaded + '' + php_info.phpinfo.modules + '
                                  '; + Object.keys(php_info) + .sort() + .forEach(function (k) { + if (k !== 'phpinfo') { + con += '

                                  ' + php_info.phpinfo.keys[k] + '

                                  '; + con += ''; + var nkey = 0; + Object.keys(php_info[k]).forEach(function (key) { + if (nkey == 0) con += ''; + con += ''; + nkey++; + if (nkey >= 3) { + nkey = 0; + con += ''; + } + }); + + con += '
                                  ' + key + '' + p_status[php_info[k][key]] + '
                                  '; + } + }); + + $('.soft-man-con').html(con); + + $('#btn_phpinfo').click(function () { + var loadT = bt.load(lan.soft.get); + bt.send( + 'GetPHPInfo', + 'ajax/GetPHPInfo', + { + version: version, + }, + function (rdata) { + loadT.close(); + var content = rdata + .replace('a:link {color: #009; text-decoration: none; background-color: #fff;}', '') + .replace('a:link {color: #000099; text-decoration: none; background-color: #ffffff;}', ''); + bt.open({ + type: 1, + title: 'PHP-' + version + '-PHPINFO', + area: ['73%', '90%'], + closeBtn: 2, + shadeClose: true, + content: '
                                  ' + content + '
                                  ', + }); + } + ); + }); + } + ); + + break; + case 'config_edit': + bt.soft.php.get_php_config(version, function (rdata) { + var mlist = ''; + for (var i = 0; i < rdata.length; i++) { + var w = '70'; + if (rdata[i].name == 'error_reporting') w = '250'; + var ibody = ''; + switch (rdata[i].type) { + case 0: + var selected_1 = rdata[i].value == 1 ? 'selected' : ''; + var selected_0 = rdata[i].value == 0 ? 'selected' : ''; + ibody = + ''; + break; + case 1: + var selected_1 = rdata[i].value == 'On' ? 'selected' : ''; + var selected_0 = rdata[i].value == 'Off' ? 'selected' : ''; + ibody = + ''; + break; + } + mlist += '

                                  ' + rdata[i].name + '' + ibody + ', ' + rdata[i].ps + '

                                  '; + } + var tabCon = $('.soft-man-con').empty(); + tabCon.append('
                                  ' + mlist + '
                                  '); + var datas = { + title: ' ', + items: [ + { + name: 'btn_fresh', + text: lan.public.fresh, + type: 'button', + callback: function (ldata) { + soft.get_tab_contents(key, obj); + }, + }, + { + name: 'btn_save', + text: lan.public.save, + type: 'button', + callback: function (ldata) { + var loadT = bt.load(); + ldata['version'] = version; + bt.send('SetPHPConf', 'config/SetPHPConf', ldata, function (rdata) { + loadT.close(); + soft.get_tab_contents(key, obj); + bt.msg(rdata); + }); + }, + }, + ], + }; + var _form_data = bt.render_form_line(datas); + $('.conf_p').append(_form_data.html); + bt.render_clicks(_form_data.clicks); + $('.conf_p > .line').css('margin-top', '25px'); + }); + break; + case 'set_upload_limit': + bt.soft.php.get_limit_config(version, function (ret) { + var datas = [ + { + items: [ + { + title: '', + type: 'number', + width: '100px', + value: ret.max, + unit: 'MB', + name: 'phpUploadLimit', + }, + { + name: 'btn_limit_get', + text: lan.public.save, + type: 'button', + callback: function (ldata) { + var max = ldata.phpUploadLimit; + if (max < 2) { + layer.msg(lan.soft.php_upload_size, { + icon: 2, + }); + return; + } + bt.soft.php.set_upload_max(version, max, function (rdata) { + if (rdata.status) { + soft.get_tab_contents(key, obj); + } + bt.msg(rdata); + }); + }, + }, + ], + }, + ]; + var clicks = []; + var tabCon = $('.soft-man-con').empty().append("
                                  "); + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + $('.set_upload_limit').append(_form_data.html); + clicks = clicks.concat(_form_data.clicks); + } + bt.render_clicks(clicks); + }); + break; + case 'set_timeout_limit': + bt.soft.php.get_limit_config(version, function (ret) { + var datas = [ + { + items: [ + { + title: '', + type: 'number', + width: '100px', + value: ret.maxTime, + name: 'phpTimeLimit', + unit: 'Sec', + }, + { + name: 'btn_limit_get', + text: lan.public.save, + type: 'button', + callback: function (ldata) { + var max = ldata.phpTimeLimit; + bt.soft.php.set_php_timeout(version, max, function (rdata) { + if (rdata.status) { + soft.get_tab_contents(key, obj); + } + bt.msg(rdata); + }); + }, + }, + ], + }, + ]; + var clicks = []; + var tabCon = $('.soft-man-con').empty().append("
                                  "); + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + $('.set_timeout_limit').append(_form_data.html); + clicks = clicks.concat(_form_data.clicks); + } + bt.render_clicks(clicks); + }); + break; + case 'set_dis_fun': + var loading = bt.load(lan.public.the); + bt.soft.php.get_config(version, function (rdata) { + loading.close(); + var list = []; + var disable_functions = rdata.disable_functions.split(','); + for (var i = 0; i < disable_functions.length; i++) { + if (disable_functions[i] == '') continue; + list.push({ + name: disable_functions[i], + }); + } + var _bt_form = $("
                                  "); + var tabCon = $('.soft-man-con').empty().append(_bt_form); + var _line = bt.render_form_line( + { + title: '', + items: [ + { + name: 'disable_function_val', + placeholder: lan.soft.fun_ps1, + width: '410px', + }, + { + name: 'btn_disable_function_val', + text: lan.public.save, + type: 'button', + callback: function (ldata) { + var disable_functions = rdata.disable_functions.split(','); + if ($.inArray(ldata.disable_function_val, disable_functions) >= 0) { + bt.msg({ + msg: lan.soft.fun_msg, + icon: 5, + }); + return; + } + disable_functions.push(ldata.disable_function_val); + set_disable_functions(version, disable_functions.join(',')); + }, + }, + ], + }, + '', + _bt_form + ); + + bt.render_clicks(_line.clicks); + _bt_form.append("
                                  "); + var _tab = bt.render({ + table: '#blacktable', + data: list, + columns: [ + { + field: 'name', + title: lan.soft.php_ext_name, + }, + { + field: 'opt', + title: lan.public.action, + width: 50, + templet: function (item) { + var new_disable_functions = disable_functions.slice(); + new_disable_functions.splice($.inArray(item.name, new_disable_functions), 1); + return ( + '' + + lan.soft.del + + '' + ); + }, + }, + ], + }); + tabCon.append(bt.render_help([lan.soft.fun_ps2, lan.soft.fun_ps3])); + }); + break; + case 'set_fpm_config': + bt.soft.php.get_fpm_config(version, function (rdata) { + var datas = { + '1GB Ram': { + max_children: 30, + start_servers: 5, + min_spare_servers: 5, + max_spare_servers: 20, + }, + '2GB Ram': { + max_children: 50, + start_servers: 5, + min_spare_servers: 5, + max_spare_servers: 30, + }, + '4GB Ram': { + max_children: 80, + start_servers: 10, + min_spare_servers: 10, + max_spare_servers: 30, + }, + '8GB Ram': { + max_children: 120, + start_servers: 10, + min_spare_servers: 10, + max_spare_servers: 30, + }, + '16GB Ram': { + max_children: 200, + start_servers: 15, + min_spare_servers: 15, + max_spare_servers: 50, + }, + '32GB Ram': { + max_children: 300, + start_servers: 20, + min_spare_servers: 20, + max_spare_servers: 50, + }, + }; + var limits = [], + pmList = []; + var my_selected = ''; + var num_max = Number(rdata.max_children); + for (var k in datas) { + if (datas[k].max_children === num_max) { + my_selected = k; + } + limits.push({ + title: k, + value: k, + }); + } + var _form_datas = [ + { + title: lan.soft.concurrency_type, + name: 'limit', + value: my_selected, + type: 'select', + items: limits, + callback: function (iKey) { + var item = datas[iKey.val()]; + for (var sk in item) $('.' + sk).val(item[sk]); + }, + }, + { + title: 'Connection', + name: 'listen', + value: rdata.unix, + type: 'select', + items: [ + { title: 'UNIX socket', value: 'unix' }, + { title: 'TCP socket', value: 'tcp' }, + ], + ps: '* UNIX socket recommended', + }, + { + title: lan.soft.php_fpm_model, + name: 'pm', + value: rdata.pm, + type: 'select', + items: [ + { + title: lan.bt.static, + value: 'static', + }, + { + title: lan.bt.dynamic, + value: 'dynamic', + }, + { title: 'On-demand', value: 'ondemand' }, + ], + ps: '*' + lan.soft.php_fpm_ps1, + }, + { + title: 'max_children', + name: 'max_children', + value: rdata.max_children, + type: 'number', + width: '100px', + ps: '*' + lan.soft.php_fpm_ps2, + }, + { + title: 'start_servers', + name: 'start_servers', + value: rdata.start_servers, + type: 'number', + width: '100px', + ps: '*' + lan.soft.php_fpm_ps3, + }, + { + title: 'min_spare_servers', + name: 'min_spare_servers', + value: rdata.min_spare_servers, + type: 'number', + width: '100px', + ps: '*' + lan.soft.php_fpm_ps4, + }, + { + title: 'max_spare_servers', + name: 'max_spare_servers', + value: rdata.max_spare_servers, + type: 'number', + width: '100px', + ps: '*' + lan.soft.php_fpm_ps5, + }, + { + title: ' ', + text: lan.public.save, + name: 'btn_children_submit', + css: 'btn-success', + type: 'button', + callback: function (ldata) { + bt.pub.get_menm(function (memInfo) { + var limit_children = parseInt(memInfo['memTotal'] / 8); + if (limit_children < parseInt(ldata.max_children)) { + layer.msg(lan.soft.php_child_process.replace('{1}', limit_children), { + icon: 2, + }); + $("input[name='max_children']").focus(); + return; + } + if (parseInt(ldata.max_children) < parseInt(ldata.max_spare_servers)) { + layer.msg(lan.soft.php_fpm_err1, { + icon: 2, + }); + return; + } + if (parseInt(ldata.min_spare_servers) > parseInt(ldata.start_servers)) { + layer.msg(lan.soft.php_fpm_err2, { + icon: 2, + }); + return; + } + if (parseInt(ldata.max_spare_servers) < parseInt(ldata.min_spare_servers)) { + layer.msg(lan.soft.php_fpm_err3, { + icon: 2, + }); + return; + } + if (parseInt(ldata.max_children) < parseInt(ldata.start_servers)) { + layer.msg(lan.soft.php_fpm_err4, { + icon: 2, + }); + return; + } + if (parseInt(ldata.max_children) < 1 || parseInt(ldata.start_servers) < 1 || parseInt(ldata.min_spare_servers) < 1 || parseInt(ldata.max_spare_servers) < 1) { + layer.msg(lan.soft.php_fpm_err5, { + icon: 2, + }); + return; + } + ldata['version'] = version; + bt.soft.php.set_fpm_config(version, ldata, function (rdata) { + soft.get_tab_contents(key, obj); + bt.msg(rdata); + }); + }); + }, + }, + ]; + var tabCon = $('.soft-man-con').empty(); + var _c_form = $('
                                  '); + var clicks = []; + for (var i = 0; i < _form_datas.length; i++) { + var _form = bt.render_form_line(_form_datas[i]); + _c_form.append(_form.html); + clicks = clicks.concat(_form.clicks); + } + _c_form.append( + '
                                    \ +
                                  • [Max num of child processes] The larger the number, the stronger the concurrency,
                                         but max_children should not exceed 5000.
                                  • \ +
                                  • [Ram] Each PHP child process needs about 20MB of Ram,
                                         too large max_children will cause server instability.
                                  • \ +
                                  • [Static mode] In the static mode, the set number of child processes is always maintained,
                                         which has a large Ram overhead, but has a good concurrency capability.
                                  • \ +
                                  • [Dynamic mode] will recover the process according to the set max number of idle processes,
                                         the Ram overhead is small, it is recommended to use a small Ram machine.
                                  • \ +
                                  • [64GB Ram recommended value] max_children <= 1000, start / min_spare = 50, max_spare <= 200
                                  • \ +
                                  • [Multi-PHP Version] If you have installed multiple PHP versions and are using them,
                                         it is recommended to reduce the concurrent configuration appropriately.
                                  • \ +
                                  • [No database] If no database such as mysql is installed,
                                         it is recommended to set 2 times the recommended concurrency.
                                  • \ +
                                  • [Note] The above are the recommended configuration instructions.
                                         The online projects are complex and diverse. Please adjust according to actual conditions.
                                  • \ +
                                  ' + ); + tabCon.append(_c_form); + + bt.render_clicks(clicks); + }); + break; + case 'get_php_status': + bt.soft.php.get_php_status(version, function (rdata) { + var arr = {}; + arr[lan.bt.php_pool] = rdata.pool; + arr[lan.bt.php_manager] = rdata['process manager'] == 'dynamic' ? lan.bt.dynamic : lan.bt.static; + arr[lan.bt.php_start] = rdata['start time']; + arr[lan.bt.php_accepted] = rdata['accepted conn']; + arr[lan.bt.php_queue] = rdata['listen queue']; + arr[lan.bt.php_max_queue] = rdata['max listen queue']; + arr[lan.bt.php_len_queue] = rdata['listen queue len']; + arr[lan.bt.php_idle] = rdata['idle processes']; + arr[lan.bt.php_active] = rdata['active processes']; + arr[lan.bt.php_total] = rdata['total processes']; + arr[lan.bt.php_max_active] = rdata['max active processes']; + arr[lan.bt.php_max_children] = rdata['max children reached']; + arr[lan.bt.php_slow] = rdata['slow requests']; + + var con = "
                                  "; + $('.soft-man-con').html(con); + bt.render_table('tab_php_status', arr); + }); + break; + case 'get_php_session': + bt.soft.php.get_php_session(version, function (res) { + $('.soft-man-con').html( + '
                                  ' + + '
                                  ' + + '' + + lan.soft.storage_mode + + '' + + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '' + + lan.soft.ip_addr + + '' + + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '' + + lan.soft.port + + '' + + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '' + + lan.soft.passwd + + '' + + '
                                  ' + + '' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + '' + + '
                                  ' + + '
                                    ' + + '
                                  • ' + + lan.soft.php_seesion_tips1 + + '
                                  • ' + + '
                                  • ' + + lan.soft.php_seesion_tips2 + + '
                                  • ' + + '
                                  • ' + + lan.soft.php_seesion_tips3 + + '
                                  • ' + + '
                                  ' + + '
                                  ' + + '
                                  ' + + lan.soft.clear_seesion_files + + '
                                  ' + + '
                                  ' + ); + if (res.save_handler == 'files') { + bt.soft.php.get_session_count(function (res) { + $('.clear_conter').html( + '
                                  ' + + lan.soft.total_seesion_files + + '' + + res.total + + '
                                  ' + + lan.soft.can_clear_seesion + + '' + + res.oldfile + + '
                                  ' + ); + $('.clear_session_file').click(function () { + bt.soft.php.clear_session_count( + { + title: lan.soft.clear_php_seesion_files, + msg: lan.soft.sure_clear_php_seesion_files, + }, + function (res) { + layer.msg(res.msg, { + icon: res.status ? 1 : 2, + }); + setTimeout(function () { + $('.bt-soft-menu p:eq(9)').click(); + }, 2000); + } + ); + }); + }); + } else { + $('.clear_conter').html(lan.soft.only_files_storage_mode_can_clear).attr('style', 'color:#666'); + } + switch_type(res.save_handler); + $('.change_select_session').change(function () { + switch_type($(this).val()); + switch ($(this).val()) { + case 'redis': + $('[name="ip"]').val('127.0.0.1'); + $('[name="port"]').val('6379'); + break; + case 'memcache': + $('[name="ip"]').val('127.0.0.1'); + $('[name="port"]').val('11211'); + break; + case 'memcached': + $('[name="ip"]').val('127.0.0.1'); + $('[name="port"]').val('11211'); + break; + } + }); + $('.btn_conf_save').click(function () { + bt.soft.php.set_php_session( + { + version: version, + save_handler: $('[name="save_handler"]').val(), + ip: $('[name="ip"]').val(), + port: $('[name="port"]').val(), + passwd: $('[name="passwd"]').val(), + }, + function (res) { + layer.msg(res.msg, { + icon: res.status ? 1 : 2, + }); + // setTimeout(function() { + // $('.bt-soft-menu p:eq(9)').click(); + // }, 2000); + } + ); + }); + + function switch_type(type) { + switch (type) { + case 'files': + $('[name="ip"]').attr('disabled', 'disabled').val(''); + $('[name="port"]').attr('disabled', 'disabled').val(''); + $('[name="passwd"]').attr('disabled', 'disabled').val(''); + break; + case 'redis': + $('[name="ip"]').attr('disabled', false); + $('[name="port"]').attr('disabled', false); + $('[name="passwd"]').attr('disabled', false); + break; + case 'memcache': + $('[name="ip"]').attr('disabled', false); + $('[name="port"]').attr('disabled', false); + $('[name="passwd"]').attr('disabled', 'disabled').val(''); + break; + case 'memcached': + $('[name="ip"]').attr('disabled', false); + $('[name="port"]').attr('disabled', false); + $('[name="passwd"]').attr('disabled', 'disabled').val(''); + break; + } + } + }); + break; + case 'get_fpm_logs': + bt.soft.php.get_fpm_logs(version, function (logs) { + var phpCon = ''; + $('.soft-man-con').html(phpCon); + var ob = document.getElementById('error_log'); + ob.scrollTop = ob.scrollHeight; + }); + break; + case 'get_slow_logs': + bt.soft.php.get_slow_logs(version, function (logs) { + var phpCon = ''; + $('.soft-man-con').html(phpCon); + var ob = document.getElementById('error_log'); + ob.scrollTop = ob.scrollHeight; + }); + break; + case 'get_redis_status': + bt.soft.redis.get_redis_status(function (rdata) { + var hit = ((parseInt(rdata.keyspace_hits) / (parseInt(rdata.keyspace_hits) + parseInt(rdata.keyspace_misses))) * 100).toFixed(2); + var arrs = []; + arrs['uptime_in_days'] = [rdata.uptime_in_days, lan.soft.run_days]; + arrs['tcp_port'] = [rdata.tcp_port, lan.soft.curr_listen_port]; + arrs['connected_clients'] = [rdata.connected_clients, lan.soft.connected_clients]; + arrs['used_memory_rss'] = [bt.format_size(rdata.used_memory_rss), lan.soft.used_memory_rss]; + arrs['used_memory'] = [bt.format_size(rdata.used_memory), lan.soft.used_memory]; + arrs['mem_fragmentation_ratio'] = [rdata.mem_fragmentation_ratio, lan.soft.mem_fragmentation_ratio]; + arrs['total_connections_received'] = [rdata.total_connections_received, lan.soft.total_connections_received]; + arrs['total_commands_processed'] = [rdata.total_commands_processed, lan.soft.total_commands_processed]; + arrs['instantaneous_ops_per_sec'] = [rdata.instantaneous_ops_per_sec, lan.soft.instantaneous_ops_per_sec]; + arrs['keyspace_hits'] = [rdata.keyspace_hits, lan.soft.keyspace_hits]; + arrs['keyspace_misses'] = [rdata.keyspace_misses, lan.soft.keyspace_misses]; + arrs['hit'] = [hit, lan.soft.db_his]; + arrs['latest_fork_usec'] = [rdata.latest_fork_usec, lan.soft.latest_fork_usec]; + + var con = + '
                                  ' + + lan.soft.field + + '' + + lan.soft.curr_val + + '' + + lan.soft.instructions + + '
                                  '; + $('.soft-man-con').html(con); + bt.render_table('tab_get_redis_status', arrs, true); + }); + break; + case 'openliMa_set': + var loadT = bt.load(); + $.post('/config?action=get_ols_value', function (rdata) { + loadT.close(); + var _mlist_data = '', + tips_i = 0, + help_tips = [ + '#Enables GZIP/Brotli compression for both static and dynamic responses.', + '#Specifies the level of GZIP compression applied to dynamic content. Ranges from 1 (lowest) to 9 (highest).', + '', + '#Specifies the maximum number of concurrent connections that the server can accept.
                                  \ + #This includes both plain TCP connections and SSL connections', + '#Specifies the maximum number of concurrent SSL connections the server will accept
                                  \ + #Since total concurrent SSL and non-SSL connections cannot exceed the limit specified by “Max Connections”,
                                  \ + #the actual number of concurrent SSL connections allowed must be lower than this limit.', + '#Specifies the maximum connection idle time (seconds) allowed during processing one request', + '#Specifies the maximum number of requests that can be served through a keep-alive (persistent) session', + ]; + for (var i in rdata) { + var mlist = { title: '', items: [] }, + list = {}; + list.name = i; + list.width = '130px'; + list.value = rdata[i]; + list.type = i == 'enableGzipCompress' ? 'switch' : 'input'; + list.ps_help = help_tips[tips_i]; + mlist.items.push(list); + mlist.title = i; + _mlist_data += bt.render_form_line(mlist).html; + tips_i++; + } + var tabCon = $('.soft-man-con').empty(); + tabCon.append('
                                  ' + _mlist_data + '
                                  '); + var datas = { + title: ' ', + class: 'openlite_button', + items: [ + { + name: 'btn_fresh', + text: lan.public.fresh, + type: 'button', + callback: function (ldata) { + soft.get_tab_contents(key, obj); + }, + }, + { + name: 'btn_save', + text: lan.public.save, + type: 'button', + width: '62px', + callback: function (ldata) { + var datal = {}, + loadP = bt.load(); + delete ldata.btn_fresh; + delete ldata.btn_save; + ldata['enableGzipCompress'] = $('#enableGzipCompress').prop('checked') ? 1 : 0; + ldata = JSON.stringify(ldata); + datal = { array: ldata }; + bt.send('set_ols_value', 'config/set_ols_value', datal, function (res) { + loadP.close(); + soft.get_tab_contents(key, obj); + bt.msg(res); + }); + }, + }, + ], + }; + var _form_data = bt.render_form_line(datas); + $('.openlite_set').append(_form_data.html); + bt.render_clicks(_form_data.clicks); + $('.enableGzipCompress_help').css('margin-left', '104px'); + $('.openlite_set').on('mouseenter', '.bt-ico-ask', function () { + var idd = $(this).attr('class').split(' ')[1], + tip = $(this).attr('tip'); + layer.tips(tip, '.' + idd + '', { tips: [1, '#d4d4d4'], time: 0, area: '300px' }); + }); + $('.openlite_set').on('mouseleave', '.bt-ico-ask', function () { + layer.closeAll('tips'); + }); + }); + break; + } + }, + update_zip_open: function () { + $('#update_zip').on('change', function () { + var files = $('#update_zip')[0].files; + if (files.length == 0) { + return; + } + soft.update_zip(files[0]); + $('#update_zip').val(''); + }); + + $('#update_zip').click(); + }, + update_zip: function (file) { + var formData = new FormData(); + formData.append('plugin_zip', file); + $.ajax({ + url: '/plugin?action=update_zip', + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function (data) { + if (data.status === false) { + layer.msg(data.msg, { + icon: 2, + }); + return; + } + var loadT = layer.open({ + type: 1, + area: '500px', + title: lan.soft.install_third_party_apps, + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + '\ +
                                  \ + \ +
                                    \ +
                                  • ' + + lan.soft.third_party_apps_tips1 + + '
                                  • \ +
                                  • ' + + lan.soft.third_party_apps_tips2 + + '
                                  • \ +
                                  • ' + + lan.third_party_apps_tips3 + + '
                                  • \ +
                                  \ +
                                  \ +
                                  ', + }); + }, + error: function (responseStr) { + layer.msg(lan.soft.upload_fail2, { + icon: 2, + }); + }, + }); + }, + + input_zip: function (plugin_name, tmp_path, data, callback) { + bt.soft.show_speed_window({ title: 'Installing, this may take a few minutes...', status: true }, function () { + $.post('/plugin?action=input_zip', { plugin_name: plugin_name, tmp_path: tmp_path }, function (rdata) { + layer.closeAll(); + if (rdata.status) { + soft.get_list(); + } + setTimeout(function () { + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + if (rdata.status) { + setTimeout(function () { + callback && callback(); + }, 1500); + } + }, 1000); + }); + }); + }, +}; + +function soft_td_width_auto() { + var thead_width = '', + winWidth = $(window).width(); + if (winWidth <= 1370 && winWidth > 1280) { + thead_width = winWidth / 4; + } else if (winWidth <= 1280 && winWidth > 1210) { + thead_width = winWidth / 5; + } else if (winWidth <= 1210) { + thead_width = winWidth / 6; + } else { + thead_width = winWidth / 3.5; + } + //$('#softList thead th:eq(2)').width(thead_width); + $('#softList tbody tr td:nth-child(8n+2)>span').width(thead_width + 75); +} + +function set_disable_functions(version, data) { + bt.soft.php.disable_functions(version, data, function (rdata) { + if (rdata.status) { + soft.get_tab_contents('set_dis_fun', $('.bgw')); + } + bt.msg(rdata); + }); +} + +var openId = (add = null); + +function AddDeployment(maction) { + if (maction == 1) { + var pdata = + 'title=' + + $("input[name='title']").val() + + '&dname=' + + $("input[name='name']").val() + + '&ps=' + + $("input[name='ps']").val() + + '&version=' + + $("input[name='version']").val() + + '&rewrite=' + + ($("input[name='rewrite']").attr('checked') ? 1 : 0) + + '&shell=' + + ($("input[name='shell']").attr('checked') ? 1 : 0) + + '&php=' + + $("input[name='php']").val() + + '&md5=' + + $("input[name='md5']").val() + + '&download=' + + $("input[name='download']").val(); + var loadT = layer.msg('Submitting ', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + $.post('/deployment?action=AddPackage', pdata, function (rdata) { + layer.close(loadT); + layer.msg(rdata.msg, { + icon: rdata.status ? 1 : 5, + }); + if (rdata.status) { + GetSrcList(); + layer.close(openId); + } + }); + + return; + } + openId = layer.open({ + type: 1, + skin: 'demo-class', + area: '480px', + title: '添加源码包', + closeBtn: 2, + shift: 5, + shadeClose: false, + content: + 'Title:
                                  \ + Identification:
                                  \ + Description:
                                  \ + Version:
                                  \ + Whether to use URL rewrite:
                                  \ + Whether to execute the installation script:
                                  \ + Supported PHP version:
                                  \ + md5:\ + Download link:
                                  \ + ', + }); +} + +$('.searchInput').keyup(function (e) { + if (e.keyCode == 13) { + GetSrcList(); + } +}); + +function AddSite(codename, title) { + var array; + var str = ''; + var domainlist = ''; + var domain = (array = $('#mainDomain').val().split('\n')); + var Webport = []; + var checkDomain = domain[0].split('.'); + if (checkDomain.length < 1) { + layer.msg('The domain name is not in the correct format. Please re-enter!', { + icon: 2, + }); + return; + } + for (var i = 1; i < domain.length; i++) { + domainlist += '"' + domain[i] + '",'; + } + Webport = domain[0].split(':')[1]; //主域名端口 + if (Webport == undefined) { + Webport = '80'; + } + domainlist = domainlist.substring(0, domainlist.length - 1); //子域名json + mainDomain = domain[0].split(':')[0]; + domain = '{"domain":"' + domain[0] + '","domainlist":[' + domainlist + '],"count":' + domain.length + '}'; //拼接json + var php_version = $("select[name='version']").val(); + var loadT = layer.msg('Creating site ', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + var data = $('#addweb').serialize() + '&port=' + Webport + '&webname=' + domain + '&ftp=false&sql=true&address=localhost&codeing=utf8&version=' + php_version; + $.post('/site?action=AddSite', data, function (ret) { + layer.close(loadT); + if (!ret.siteStatus) { + layer.msg(ret.msg, { + icon: 5, + }); + return; + } + layer.close(add); + var sqlData = ''; + if (ret.databaseStatus) { + sqlData = + "

                                  Database account information

                                  \ +

                                  Database name:" + + ret.databaseUser + + '

                                  \ +

                                  User:' + + ret.databaseUser + + '

                                  \ +

                                  Password:' + + ret.databasePass + + '

                                  \ + '; + } + var pdata = 'dname=' + codename + '&site_name=' + mainDomain + '&php_version=' + php_version; + var loadT = layer.msg('
                                  Submitting
                                  ', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + + setTimeout(function () { + GetSpeed(); + }, 2000); + + $.post('/deployment?action=SetupPackage', pdata, function (rdata) { + layer.close(loadT); + if (!rdata.status) { + layer.msg(rdata.msg, { + icon: 5, + time: 10000, + }); + return; + } + + if (rdata.msg.admin_username != '') { + sqlData = + "

                                  Successfully deployed, no need to install, please login to modify the default account password.

                                  \ +

                                  User:" + + rdata.msg.admin_username + + '

                                  \ +

                                  Password:' + + rdata.msg.admin_password + + '

                                  \ + '; + } + sqlData += "

                                  Visit site:http://" + mainDomain + rdata.msg.success_url + '

                                  '; + + layer.open({ + type: 1, + area: '600px', + title: 'Successfully deployed [' + title + ']', + closeBtn: 2, + shadeClose: false, + content: + "
                                  \ +
                                  \ +
                                  \ + " + + sqlData + + '\ +
                                  \ +
                                  ', + }); + if ($('.success-msg').height() < 150) { + $('.success-msg').find('img').css({ + width: '150px', + 'margin-top': '30px', + }); + } + }); + }); +} + +function GetSpeed() { + if (!$('.depSpeed')) return; + $.get('/deployment?action=GetSpeed', function (speed) { + if (speed.status === false) return; + if (speed.name == 'Download file') { + speed = + '

                                  正在' + + speed.name + + '

                                  \ +
                                  ' + + speed.pre + + '%
                                  \ +

                                  ' + + ToSize(speed.used) + + '/' + + ToSize(speed.total) + + '' + + ToSize(speed.speed) + + '/s

                                  '; + $('.depSpeed').prev().hide(); + $('.depSpeed').css({ + 'margin-left': '-37px', + width: '380px', + }); + $('.depSpeed').parents('.layui-layer').css({ + 'margin-left': '-100px', + }); + } else { + speed = '

                                  ' + speed.name + '

                                  '; + $('.depSpeed').prev().show(); + $('.depSpeed').removeAttr('style'); + $('.depSpeed').parents('.layui-layer').css({ + 'margin-left': '0', + }); + } + + $('.depSpeed').html(speed); + setTimeout(function () { + GetSpeed(); + }, 1000); + }); +} + +function onekeyCodeSite(codename, versions, title, enable_functions) { + $.post('/site?action=GetPHPVersion', function (rdata) { + var php_version = ''; + var n = 0; + for (var i = rdata.length - 1; i >= 0; i--) { + if (versions.indexOf(rdata[i].version) != -1) { + php_version += "'; + n++; + } + } + + if (n == 0) { + layer.msg('Missing supported PHP version, please install!', { + icon: 5, + }); + return; + } + var default_path = bt.get_cookie('sites_path'); + if (!default_path) default_path = '/www/wwwroot'; + + var con = + '\ +
                                  Domain\ +
                                  \ +
                                  Fill in a domain name per line, the default is 80 ports
                                  Pan-analysis add method *.domain.com
                                  If the additional port format is www.domain.com:88
                                  \ +
                                  \ +
                                  \ +
                                  Note\ +
                                  \ +
                                  \ +
                                  Document Root\ +
                                  \ +
                                  \ +
                                  Database\ +
                                  \ + \ + \ +
                                  \ +
                                  \ +
                                  Source code\ + \ + Prepare the source code for your deployment\ +
                                  \ +
                                  PHP Version\ + \ + Please select the php version supported by the source program.\ +
                                  \ +
                                  \ + \ + \ +
                                  \ + '; + add = layer.open({ + type: 1, + title: 'aaPanel One-Click [' + title + ']', + area: '560px', + closeBtn: 2, + shadeClose: false, + content: con, + }); + + if (enable_functions.length > 2) { + layer.msg("Note: The following functions will be released when deploying this project.:
                                  " + enable_functions + '
                                  ', { + icon: 7, + time: 10000, + }); + } + var placeholder = + "
                                  Fill in a domain name per line, the default is 80 ports
                                  Pan-analysis add method *.domain.com
                                  If the additional port format is www.domain.com:88
                                  "; + $('.onekeycodeclose').click(function () { + layer.close(add); + }); + $('#mainDomain').after(placeholder); + $('.placeholder').click(function () { + $(this).hide(); + $('#mainDomain').focus(); + }); + $('#mainDomain').focus(function () { + $('.placeholder').hide(); + }); + + $('#mainDomain').blur(function () { + if ($(this).val().length == 0) { + $('.placeholder').show(); + } + }); + //FTP账号数据绑定域名 + $('#mainDomain').on('input', function () { + var defaultPath = bt.get_cookie('sites_path'); + if (!defaultPath) defaultPath = '/www/wwwroot'; + var array; + var res, ress; + var str = $(this).val(); + var len = str.replace(/[^\x00-\xff]/g, '**').length; + array = str.split('\n'); + ress = array[0].split(':')[0]; + res = ress.replace(new RegExp(/([-.])/g), '_'); + if (res.length > 15) res = res.substr(0, 15); + if ($('#inputPath').val().substr(0, defaultPath.length) == defaultPath) $('#inputPath').val(defaultPath + '/' + ress); + if (!isNaN(res.substr(0, 1))) res = 'sql' + res; + if (res.length > 15) res = res.substr(0, 15); + $('#Wbeizhu').val(ress); + $('#datauser').val(res); + }); + $('#Wbeizhu').on('input', function () { + var str = $(this).val(); + var len = str.replace(/[^\x00-\xff]/g, '**').length; + if (len > 20) { + str = str.substring(0, 20); + $(this).val(str); + layer.msg('Do not exceed 20 characters', { + icon: 0, + }); + } + }); + //获取当前时间时间戳,截取后6位 + var timestamp = new Date().getTime().toString(); + var dtpw = timestamp.substring(7); + $('#datauser').val('sql' + dtpw); + $('#datapassword').val(_getRandomString(10)); + }); +} + +//生成n位随机密码 +function _getRandomString(len) { + len = len || 32; + var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; // 默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1 + var maxPos = $chars.length; + var pwd = ''; + for (i = 0; i < len; i++) { + pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); + } + return pwd; +} +var score = { + total: 1, + type: '', + data: [], + // 获取评论信息 + get_score_info: function (obj, callback) { + var loadT = layer.msg('
                                  Getting comment information
                                  ', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + bt.send( + 'get_score', + 'plugin/get_score', + { + pid: obj.pid, + p: obj.p, + limit_num: obj.limit_num, + }, + function (res) { + layer.close(loadT); + if (res.status === false) { + layer.msg(res.msg, { + icon: 2, + }); + return false; + } + if (callback) callback(res); + } + ); + }, + render_score_info: function (obj, callback) { + var config = { + pid: obj.pid, + }, + _this = this; + obj.p == undefined ? (config.p = 1) : (config.p = parseInt(obj.p)); + obj.limit_num == undefined ? (config.limit_num = '') : (config.limit_num = obj.limit_num); + score.get_score_info(config, function (res) { + var _split_score = res.split.reverse(), + _average_score = (_split_score[4] * 1 + _split_score[3] * 2 + _split_score[2] * 3 + _split_score[1] * 4 + _split_score[0] * 5) / res.total, + _data = res.data, + _html = ''; + _this.total = res.total; + $('.comment_user_count').text(obj.count); + $('.comment_num').text((res.total !== 0 ? _average_score : 0).toFixed(1)); + $('.comment_partake').text(res.total); + $('.comment_rate').text(res.total !== 0 ? ((_split_score[0] + _split_score[1]) / res.total).toFixed(2) * 100 + '%' : '0%'); + for (var i = 0; i < 5; i++) { + $('.comment_star_group:eq(' + i + ')') + .find('.comment_progress .comment_progress_bgw') + .css('width', (_split_score[i] / res.total).toFixed(2) * 100 + '%'); + } + $('.comment_tab span:eq(1)') + .find('i') + .text(_split_score[0] + _split_score[1]); + $('.comment_tab span:eq(2)') + .find('i') + .text(_split_score[2] + _split_score[3]); + $('.comment_tab span:eq(3)').find('i').text(_split_score[4]); + + for (var j = 0; j < _data.length; j++) { + _html += + '
                                  \ +
                                  \ + \ + \ + \ + \ + \ + \ + \ + ' + + _data[j].nickname + + '\ + ' + + timeago(_data[j].addtime * 1000) + + '\ +
                                  \ +
                                  ' + + (getLength(_data[j].ps) > 65 ? reBytesStr(_data[j].ps, 65) + '... Details' : _data[j].ps) + + '
                                  \ +
                                  '; + // console.log(getLength(_data[j].ps)>70?reBytesStr(_data[j].ps,70)+' 详情':_data[j].ps); + } + _this.data = _this.data.concat(_data); + if (res.total > 10 && _data.length === 10) { + _html += '
                                  Click for more comments
                                  '; + } + $('.comment_content').find('.get_next_page').remove(); + $('.comment_content').append(_html); + if ($('.comment_content .comment_box').length > 6) { + $('.comment_content').addClass('box-shadow'); + } else { + $('.comment_content').removeClass('box-shadow'); + } + if (callback) callback(res); + }); + }, + // 设置评论信息 + set_score_info: function (obj, callback) { + var loadT = layer.msg('
                                  Submitting comment
                                  ', { + icon: 16, + time: 0, + shade: [0.3, '#000'], + }); + bt.send( + 'set_score', + 'plugin/set_score', + { + pid: obj.pid, + num: obj.num, + ps: obj.ps, + }, + function (res) { + layer.close(loadT); + if (res.status === false) { + layer.msg(res.msg, { + icon: 2, + }); + return false; + } + if (callback) callback(res); + } + ); + }, + open_score_view: function (_pid, _name, _count) { + layer.open({ + type: 1, + title: '[ ' + _name + '] Score', + area: ['550px', '350px'], + closeBtn: 2, + shadeClose: false, + content: + '
                                  \ +
                                  \ +
                                  --
                                  \ +
                                    \ +
                                  • user count --
                                  • \ +
                                  •  -- people participated in the score
                                  • \ +
                                  • -- Favorable rate
                                  • \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ + \ + \ + \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ + \ + \ + \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ + \ + \ + \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ + \ + \ + \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ + \ + \ + \ + \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + All evaluation\ + Praise -- \ + Average -- \ + Bad review -- \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ +
                                  \ + \ + \ + \ + \ + \ +
                                  \ +
                                  Recommended: 5 points
                                  \ + \ + Can also enter 60 words\ +
                                  \ +
                                  \ + Participate in the score\ +
                                  \ +
                                  ', + success: function (index, layero) { + score.data = []; + score.render_score_info( + { + pid: _pid, + count: _count, + }, + function () { + $('.score_info_view').show(); + } + ); + score.score_icon_time = null; + $('.score_icon_group span').hover(function () { + var _active = $(this).hasClass('active'); + // if($(this).prevAll().length == 0 && $(this).nextAll('.active').length == 0 && _active){ + // $(this).removeClass('active').nextAll().removeClass('active') + // $('.score_icon_group_tips').html('选择以上图标选择评分等级1-5'); + // $('.score_icon_group').attr('data-icon',0) + // }else{ + // $(this).addClass('active').nextAll().removeClass('active'); + // $(this).prevAll().addClass('active'); + // $('.score_icon_group').attr('data-icon',$(this).prevAll().length +1) + // var _title = $(this).attr('title'); + // $('.score_icon_group_tips').text(_title); + // } + }); + $('.score_icon_group span').click(function () { + var _active = $(this).hasClass('active'); + if ($(this).prevAll().length == 0 && $(this).nextAll('.active').length == 0 && _active) { + $('.edit_view').addClass('active'); + $(this).removeClass('active').nextAll().removeClass('active'); + $('.score_icon_group_tips').html('Click on the selection icon to rate 1-5 stars'); + $('.score_icon_group').attr('data-icon', 0); + } else { + $('.edit_view').removeClass('active'); + $(this).addClass('active').nextAll().removeClass('active'); + $(this).prevAll().addClass('active'); + $('.score_icon_group').attr('data-icon', $(this).prevAll().length + 1); + var _title = $(this).attr('title'); + $('.score_icon_group_tips').text(_title); + } + }); + $('.comment_tab span').click(function (e) { + var _num = $(this).attr('data-num'); + $('.comment_content').removeClass('box-shadow'); + $(this).addClass('active').siblings().removeClass('active'); + $('.comment_content').html(''); + score.data = []; + score.type = _num; + score.render_score_info({ + pid: _pid, + limit_num: _num, + count: _count, + }); + }); + $('.comment_content').on('click', '.get_next_page', function () { + var _next_page = $('.comment_content .comment_box').length / 10 + 1; + score.render_score_info({ + pid: _pid, + limit_num: score.type, + p: _next_page, + count: _count, + }); + }); + $('.comment_content').on('click', '.comment_box', function () { + if (!$(this).hasClass('get_next_page')) { + var _index = $(this).attr('data-index'); + layer.open({ + type: 1, + title: false, + area: ['350px', '200px'], + closeBtn: 2, + shadeClose: false, + content: '
                                  ' + $(this).html() + '
                                  ', + success: function (index, layers) { + $('.score_details .comment_box_content').html(score.data[_index]['ps']); + }, + }); + } + }); + $('.edit_view').click(function () { + if ($('.edit_view').hasClass('active')) { + // layer.msg('请选择评分等级',{icon:2}); + $('.score_icon_group_tips').css('color', 'red'); + setTimeout(function () { + $('.score_icon_group_tips').removeAttr('style'); + }, 1000); + return false; + } + var _num = parseInt($('.score_icon_group').attr('data-icon')), + _ps = $('.score_input').val(); + if (_num == 0) { + layer.msg('Rating level cannot be empty', { + icon: 2, + }); + return false; + } + if (120 - getLength(_ps) < 0) { + layer.msg('Evaluation information cannot exceed 60 words', { + icon: 2, + }); + return false; + } + score.set_score_info( + { + pid: _pid, + num: _num, + ps: _ps == '' ? 'User did not make any evaluation' : _ps, + }, + function (res) { + layer.msg(res.msg, { + icon: 1, + }); + score.render_score_info({ + pid: _pid, + limit_num: score.type, + count: _count, + }); + soft.flush_cache(); + layer.close(index); + } + ); + return false; + layer.open({ + type: 1, + title: 'Add review', + area: ['400px', '350px'], + closeBtn: 2, + shadeClose: false, + btn: ['Confirm', 'Cancel'], + content: + '
                                  \ +
                                  \ + \ + \ + \ + \ + \ +
                                  \ +
                                  (Click on the icon above to select rating 1-5)
                                  \ + \ + Can also enter 60 words\ +
                                  ', + success: function () { + $('.score_icon_group span').click(function () { + var _active = $(this).hasClass('active'); + if ($(this).prevAll().length == 0 && $(this).nextAll('.active').length == 0 && _active) { + $(this).removeClass('active').nextAll().removeClass('active'); + $('.score_icon_group_tips').html('(Click on the icon above to select rating 1-5)'); + $('.score_icon_group').attr('data-icon', 0); + } else { + $(this).addClass('active').nextAll().removeClass('active'); + $(this).prevAll().addClass('active'); + $('.score_icon_group').attr('data-icon', $(this).prevAll().length + 1); + var _title = $(this).attr('title'); + $('.score_icon_group_tips').text(_title); + } + }); + $('.score_input').on('keydown keyup focus click', function () { + var _val = $('.score_input').val(), + _size = 120 - getLength(_val); + if (_size > 0) { + $('.score_input_tips i') + .css('color', _size > 20 ? '#666' : 'red') + .text(parseInt(_size / 2)); + $('.score_input').attr('style', ''); + } else { + $('.score_input_tips i').text(0); + $('.score_input').css({ + 'outline-color': 'red', + border: '1px solid red', + }); + } + }); + }, + yes: function (index, layero) { + var _num = parseInt($('.score_icon_group').attr('data-icon')), + _ps = $('.score_input').val(); + if (_num == 0) { + layer.msg('Rating level cannot be empty', { + icon: 2, + }); + return false; + } + if (120 - getLength(_ps) < 0) { + layer.msg('Evaluation information cannot exceed 60 words', { + icon: 2, + }); + return false; + } + score.set_score_info( + { + pid: _pid, + num: _num, + ps: _ps == '' ? 'User did not make any evaluation' : _ps, + }, + function (res) { + layer.msg(res.msg, { + icon: 1, + }); + score.render_score_info({ + pid: _pid, + limit_num: score.type, + count: _count, + }); + soft.flush_cache(); + layer.close(index); + } + ); + }, + }); + }); + }, + }); + }, +}; + +function timeago(dateTimeStamp) { + //dateTimeStamp是一个时间毫秒,注意时间戳是秒的形式,在这个毫秒的基础上除以1000,就是十位数的时间戳。13位数的都是时间毫秒。 + if (dateTimeStamp.toString().length < 10) dateTimeStamp = dateTimeStamp * 1000; + var minute = 1000 * 60, + hour = minute * 60, + day = hour * 24, + week = day * 7, + halfamonth = day * 15, + month = day * 30, + now = new Date().getTime(), //获取当前时间毫秒 + diffValue = now - dateTimeStamp; //时间差 + if (diffValue <= 0) { + return 'Just a moment ago'; + } + var minC = diffValue / minute, //计算时间差的分,时,天,周,月 + hourC = diffValue / hour, + dayC = diffValue / day, + weekC = diffValue / week, + monthC = diffValue / month, + result = 'Just a moment ago'; + if (monthC >= 1 && monthC <= 3) { + result = ' ' + parseInt(monthC) + 'month ago'; + } else if (weekC >= 1 && weekC <= 3) { + result = ' ' + parseInt(weekC) + 'week ago'; + } else if (dayC >= 1 && dayC <= 6) { + result = ' ' + parseInt(dayC) + 'day ago'; + } else if (hourC >= 1 && hourC <= 23) { + result = ' ' + parseInt(hourC) + 'hour ago'; + } else if (minC >= 1 && minC <= 59) { + result = ' ' + parseInt(minC) + 'minute ago'; + } else if (diffValue >= 0 && diffValue <= minute) { + result = 'Just a moment ago'; + } else { + var datetime = new Date(); + datetime.setTime(dateTimeStamp); + var Nyear = datetime.getFullYear(), + Nmonth = datetime.getMonth() + 1 < 10 ? '0' + (datetime.getMonth() + 1) : datetime.getMonth() + 1, + Ndate = datetime.getDate() < 10 ? '0' + datetime.getDate() : datetime.getDate(), + Nhour = datetime.getHours() < 10 ? '0' + datetime.getHours() : datetime.getHours(), + Nminute = datetime.getMinutes() < 10 ? '0' + datetime.getMinutes() : datetime.getMinutes(), + Nsecond = datetime.getSeconds() < 10 ? '0' + datetime.getSeconds() : datetime.getSeconds(), + result = Nmonth + '-' + Ndate; + } + if (!result) result = 'Just a moment ago'; + return result == undefined || result == 'undefined' ? 'Just a moment ago' : result; +} +// 规则转码 +function escapeHTML(val) { + val = '' + val; + return val + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '‘') + .replace(/\(/g, '(') + .replace(/\</g, '<') + .replace(/\>/g, '>') + .replace(/`/g, '`') + .replace(/=/g, '='); +} + +function getLength(val) { + var str = new String(val); + var bytesCount = 0; + for (var i = 0, n = str.length; i < n; i++) { + var c = str.charCodeAt(i); + if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) { + bytesCount += 1; + } else { + bytesCount += 2; + } + } + return bytesCount; +} + +function reBytesStr(str, len) { + if (!str && typeof str != 'undefined') { + return ''; + } + var num = 0; + var str1 = str; + var str = ''; + for (var i = 0, lens = str1.length; i < lens; i++) { + num += str1.charCodeAt(i) > 255 ? 2 : 1; + if (num > len) { + break; + } else { + str = str1.substring(0, i + 1); + } + } + return str; +} diff --git a/BTPanel/static/vite/oldjs/term.js b/BTPanel/static/vite/oldjs/term.js new file mode 100644 index 00000000..9accd602 --- /dev/null +++ b/BTPanel/static/vite/oldjs/term.js @@ -0,0 +1,1238 @@ +function Terms(el,config){ + if(typeof config == "undefined") config = {}; + this.el = el; + this.id = config.ssh_info.id || ''; + this.bws = null; //websocket对象 + this.route ='/webssh'; // 访问的方法 + this.term =null; //term对象 + this.info =null; // 请求数据 + this.last_body =null; + this.fontSize =15; //终端字体大小 + this.ssh_info = config.ssh_info; + this.run(); +} +Terms.prototype = { + // websocket持久化连接 + connect:function(callback){ + var that = this; + // 判断当前websocket连接是否存在 + if(!this.bws || this.bws.readyState == 3 || this.bws.readyState == 2){ + this.bws = new WebSocket((window.location.protocol === 'http:' ? 'ws://' : 'wss://') + window.location.host + this.route); + this.bws.addEventListener('message',function(ev){that.on_message(ev)}); + this.bws.addEventListener('close',function(ev){that.on_close(ev)}); + this.bws.addEventListener('error',function(ev){that.on_error(ev)}); + this.bws.addEventListener('open',function(ev){that.on_open(ev)}); + if(callback) callback(this.bws) + } + }, + + //连接服务器成功 + on_open:function(ws_event){ + var http_token = $("#request_token_head").attr('token'); + this.send(JSON.stringify({'x-http-token':http_token})) + this.send(JSON.stringify(this.ssh_info || {})) + this.term.FitAddon.fit(); + this.resize({cols:this.term.cols, rows:this.term.rows}); + }, + //服务器消息事件 + on_message: function (ws_event){ + result= ws_event.data; + if(!result) return; + that = this; + if ((result.indexOf("@127.0.0.1:") != -1 || result.indexOf("@localhost:") != -1) && result.indexOf('Authentication failed') != -1) { + that.term.write(result); + host_trem.localhost_login_form(result); + that.close(); + return; + } + if(result.length > 1 && that.last_body === false){ + that.last_body = true; + } + that.term.write(result); + that.set_term_icon(1); + if (result == '\r\n登出\r\n' || result == '登出\r\n' || result == '\r\nlogout\r\n' || result == 'logout\r\n') { + that.close(); + that.bws = null; + + } + }, + //websocket关闭事件 + on_close: function (ws_event) { + this.set_term_icon(0); + this.bws = null; + }, + + /** + * @name 设置终端标题状态 + * @author chudong<2020-08-10> + * @param {number} status 终端状态 + * @return void + */ + set_term_icon:function(status){ + var icon_list = ['icon-warning','icon-sucess','icon-info']; + if(status == 1){ + if($("[data-id='"+ this.id +"']").attr("class").indexOf('active') == -1){ + status = 2; + } + } + $("[data-id='"+ this.id +"']").find('.icon').removeAttr('class').addClass(icon_list[status]+' icon'); + if(status == 2){ + that = this; + setTimeout(function(){ + $("[data-id='"+ that.id +"']").find('.icon').removeAttr('class').addClass(icon_list[1]+' icon'); + },200); + } + }, + //websocket错误事件 + on_error: function (ws_event) { + if(ws_event.target.readyState === 3){ + // var msg = '错误: 无法创建WebSocket连接,请在面板设置页面关闭【开发者模式】'; + // layer.msg(msg,{time:5000}) + // if(Term.state === 3) return + // Term.term.write(msg) + // Term.state = 3; + }else{ + console.log(ws_event) + } + }, + //发送数据 + //@param event 唯一事件名称 + //@param data 发送的数据 + //@param callback 服务器返回结果时回调的函数,运行完后将被回收 + send: function (data, num) { + var that = this; + //如果没有连接,则尝试连接服务器 + if (!this.bws || this.bws.readyState == 3 || this.bws.readyState == 2) { + this.connect(); + } + //判断当前连接状态,如果!=1,则100ms后尝试重新发送 + if (this.bws.readyState === 1) { + this.bws.send(data); + } else { + if(this.state === 3) return; + if (!num) num = 0; + if (num < 5) { + num++; + setTimeout(function () { that.send(data, num++); }, 100) + } + } + }, + //关闭连接 + close: function () { + this.bws.close(); + this.set_term_icon(0); + }, + resize: function (size){ + if(this.bws){ + size['resize'] = 1; + this.send(JSON.stringify(size)); + } + + }, + run: function (ssh_info) { + var that = this; + this.term = new Terminal({fontSize:this.fontSize, screenKeys: true, useStyle: true }); + this.term.setOption('cursorBlink', true); + this.last_body = false; + this.term.open($(this.el)[0]); + + this.term.FitAddon = new FitAddon.FitAddon(); + this.term.loadAddon(this.term.FitAddon); + this.term.WebLinksAddon = new WebLinksAddon.WebLinksAddon() + this.term.loadAddon(this.term.WebLinksAddon) + if (ssh_info) this.ssh_info = ssh_info + this.connect(); + that.term.onData(function (data) { + try { + that.bws.send(data) + } catch (e) { + that.term.write('\r\nConnection lost, trying to connect again!\r\n') + that.connect() + } + }); + this.term.focus(); + } +} + +var host_trem = { + host_term:{}, + host_list:[], + command_list:[], + sort_time:null, + is_full:false, + command_form:{ + title:'', + shell:'', + }, + host_form:{ + host:'', + port:'22', //默认端口22 + username:'root', + password:'', + pkey: '', + ps: '' + }, + init:function(){ + var that = this; + Object.defineProperty(host_trem,'is_full',{ + get:function(val){ + return val; + }, + set:function(newValue) { + if(newValue){ + $('body').addClass('full_term_view'); + var win = $(window)[0],win_width = win.innerHeight,win_height = win.innerHeight; + $('.main-content .safe').height(win_height); + $('#term_box_view,.term_tootls').height(win_height); + $('.tootls_host_list').height((win_height - 80) * .75); + $('.tootls_commonly_list').height((win_height - 80) * .25); + $('.tab_tootls .glyphicon').removeClass('glyphicon-resize-full').addClass('glyphicon-resize-small').attr('title','Exit full screen'); + }else{ + $('body').removeClass('full_term_view'); + $('.tab_tootls .glyphicon').removeClass('glyphicon-resize-small').addClass('glyphicon-resize-full').attr('title','Full Screen'); + } + } + }); + document.onkeydown = function(e){ + e = e || window.event; + if ((e.metaKey && e.keyCode == 82) || e.keyCode == 116){ + return false; + } + if(that.is_full && e.keyCode == 27){ + return false; + } + } + //本地存储 + var _tool_status = localStorage.getItem("tool_status"); + _tool_host_height = localStorage.getItem("hostHeight"), + _tool_commonly_height = localStorage.getItem("commonlyHeight"); + if(_tool_commonly_height <= 100 )_tool_commonly_height=100; + $(window).resize(function(ev){ + that.on_resize(that); + }); + $('.tab_tootls').on('click','.glyphicon-resize-full',function(){ + $(this).removeClass('glyphicon-resize-full').addClass('glyphicon-resize-small').attr('title','Exit full Screen'); + $('body').addClass('full_term_view'); + that.requestFullScreen(); + }); + $('.tab_tootls').on('click','.glyphicon-resize-small',function(){ + $(this).removeClass('glyphicon-resize-small').addClass('glyphicon-resize-full').attr('title','Full Screen'); + $('body').removeClass('full_term_view'); + that.exitFullscreen(); + }); + + $(document).ready(function (e) { + var win = $(window)[0],win_width = win.innerHeight,win_height = win.innerHeight,host_commonly = win_height - 185; + $('.main-content .safe').height(win_height - 105); + $('#term_box_view,.term_tootls').height(win_height - 105); + if(_tool_host_height !=0&&_tool_commonly_height !=0){ + $(".tootls_host_list").css("height",_tool_host_height+"px"), + $(".tootls_commonly_list").css("height", _tool_commonly_height+"px"); + }else{ + $('.tootls_host_list').height(host_commonly * .75); + $('.tootls_commonly_list').height(host_commonly * .25); + } + that.open_term_view(); + }); + + // 添加服务器信息 + $('.addServer').on('click',function(){ + that.editor_host_view(); + }); + + // 切换服务器终端视图 + $('.term_item_tab .list').on('click','span.item',function(ev){ + var index = $(this).index(),data = $(this).data(); + if($(this).hasClass('addServer')){ + + }else if($(this).hasClass('tab_tootls')){ + + }else{ + $(this).addClass('active').siblings().removeClass('active'); + $('.term_content_tab .term_item:eq('+ index +')').addClass('active').siblings().removeClass('active'); + that.host_term[data.id]; + var item = that.host_term[data.id]; + item.term.focus(); + item.term.FitAddon.fit(); + item.resize({cols:item.term.cols, rows:item.term.rows}); + } + + }); + //通过本地存储获取显示设置 + if(_tool_status==0){ + $(".term-tool-button").empty(); + $(".term-tool-button").append('').addClass("tool-hide").removeClass("tool-show"); + $(".term_box").css("margin-right","260px"); + $(".term_tootls").css("display","block"); + if(_tool_host_height !=0&&_tool_commonly_height !=0){ + $(".tootls_host_list").css("height",_tool_host_height+"px"), + $(".tootls_commonly_list").css("height", _tool_commonly_height+"px"); + } + }else{ + $(".term-tool-button").empty(); + $(".term-tool-button").append('').addClass("tool-show").removeClass("tool-hide"); + $(".term_box").css("margin-right","0px"); + $(".term_tootls").css("display","none"); + } + + //终端工具栏显示 + $('.term_content_tab').on('click','.tool-show',function(){ + $(this).empty(); + $(this).append('').addClass("tool-hide").removeClass("tool-show"); + $(".term_box").css("margin-right","260px"); + $(".term_tootls").css("display","block"); + localStorage.setItem("tool_status",0); + that.on_resize(that); + }); + + //终端工具栏隐藏 + $('.term_content_tab').on('click','.tool-hide',function(){ + $(this).empty(); + $(this).append('').addClass("tool-show").removeClass("tool-hide"); + $(".term_box").css("margin-right","0px"); + $(".term_tootls").css("display","none"); + localStorage.setItem("tool_status",1); + that.on_resize(that); + }); + + $(".term-move-border").on('mousedown', function (e) { + var hostbox_height = parseInt($(".tootls_host_list").css("height")), + commonlybox_height = parseInt($(".tootls_commonly_list").css("height")), + max_height = hostbox_height + commonlybox_height+38, + move_y = e.clientY; + $(document).on('mousemove', function (ev) { + var offsetY = ev.clientY - move_y, + _host = hostbox_height+offsetY; + _commonly = commonlybox_height-offsetY; + if(_host <= 300){ + _host = 300;_commonly = max_height-_host-38; + }else + if(_commonly <= 100){ + _commonly = 100;_host = max_height-_commonly-38; + } + $(".tootls_host_list").css("height",_host+"px"),$(".tootls_commonly_list").css("height",_commonly+"px"); + }); + $(document).on('mouseup', function (ev) { + var _host_height = parseInt($(".tootls_host_list").css("height")), + _commonly_height = parseInt($(".tootls_commonly_list").css("height")); + localStorage.setItem("hostHeight",_host_height); + localStorage.setItem("commonlyHeight",_commonly_height); + $(this).unbind('mousemove mouseup'); + }); + e.stopPropagation(); + }); + + $('.term_item_tab').on('click','.icon-trem-close',function(){ + var id = $(this).parent().data('id'); + that.remove_term_view(id); + }) + + + // 服务器列表工具箱 + $('.tootls_host_list').on('click','li .tootls span',function(ev){ + var item = $(this).parent().parent(),host = item.data('host'),index = item.data('index'); + if(!$(this).index()){ + that.get_host_find(host,function(rdata){ + if(rdata.status === false){ + bt.msg(rdata); + return false; + } + that.editor_host_view({ + form:rdata, + config: {btn: 'Save', title: 'Edit server information [ '+ host +' ]'} + }); + }); + }else{ + bt.confirm({title:'Delete information',msg:'Delete service information [ '+ host +' ], continue?',icon:0},function(index){ + that.remove_host(host,function(rdata){ + layer.close(index); + that.reader_host_list(function(){ + bt.msg(rdata); + }); + }); + }); + } + ev.stopPropagation(); + }); + // 服务器列表工具箱 + $('.tootls_commonly_list').on('click','li .tootls span',function(ev){ + var item = $(this).parent().parent(),title = item.data('title'),index = item.data('index'); + if(!$(this).index()){ + that.get_command_find(title,function(rdata){ + if(rdata.status === false){ + bt.msg(rdata); + return false; + } + that.editor_command_view({ + form:rdata, + config: {btn: 'Save', title: 'Edit command information【'+ title +'】'} + }); + }); + }else{ + bt.confirm({title:'Delete command',msg:'Delete service command 【'+ title +'】, continue?',icon:0},function(index){ + that.remove_command(title,function(rdata){ + layer.close(index); + that.reader_command_list(function(){ + bt.msg(rdata); + }); + }); + }); + } + ev.stopPropagation(); + }); + // 右键菜单 + $('.term_item_tab .list').on('mousedown','.item',function(ev){ + if(ev.which == 3){ + that.reader_right_menu({ + el:$(this), + position:[ev.clientX,ev.clientY], + list:[ + [{title:'Copy session',event:function(el,data){ + that.open_term_view(that.host_term[data.id].ssh_info); + }}], + [{title:'Close session',event:function(el,data){ + that.remove_term_view(data.id); + }}, + {title:'Close to right',event:function(el,data){ + bt.confirm({msg:'After closing the terminal session, the command in progress in the current command line session may be aborted. Continue?',title: "Close the terminal session?"},function(index){ + that.remove_term_right_view(data.id); + layer.close(index); + }); + }}, + {title:'Close other',event:function(el,data){ + bt.confirm({msg:'After closing the terminal session, the command in progress in the current command line session may be aborted. Continue?',title: "Close the terminal session?"},function(index){ + that.remove_term_other_view(data.id); + layer.close(index); + }); + }}] + ] + },function(){ + $(document).unbind('contextmenu'); + }); + ev.preventDefault(); + $(document).contextmenu(function(e){ + e.preventDefault(); + }); + } + }); + + // 添加服务器和常用秘钥 + $('.term_tootls .tootls_tab a').click(function(){ + var type = $(this).data('type'); + if(type == 'host'){ + that.editor_host_view(); + }else{ + that.editor_command_view(); + } + }); + // var clientX = null,clientY = null; + // $('.tootls_host_list').on('mousedown','li',function(e){ + // clientX = e.clientX,clientY = e.clientY; + // }) + // 模拟触发服务器列表点击事件 + // $('.tootls_host_list').on('mouseup','li',function(e){ + // if(e.button == 0){ + // if(clientX == e.clientX && clientY == e.clientY){ + + // } + // } + // }); + + $('.tootls_host_list').on('click','li',function(e){ + var index = $(this).index(),host = $(this).data('host'); + $(this).find('i').addClass('active'); + if($('.item[data-host="'+ host +'"]').length > 0){ + layer.msg('For multi session window, right click the terminal title to copy the session!',{icon:0,time:3000}) + }else{ + that.open_term_view(that.host_list[index]); + } + }); + // 服务器列表拖动 + $('.tootls_host_list').dragsort({ + dragSelector:'li i', + dragEnd:function(){ + clearTimeout(that.sort_time); + that.sort_time = setTimeout(function(){ + var sort_list = {}; + $('.tootls_host_list li').each(function(index,el){ + sort_list[$(this).data('host')] = index; + }); + that.set_sort(sort_list,function(rdata){ + if(!rdata.status){ + bt.msg(rdata); + } + }); + },500); + }, + dragBetween:false, + }); + this.reader_host_list(); + this.reader_command_list(); + }, + // 判断全屏状态 + isFullScreen:function() { + var is_full = document.isFullScreen || document.mozIsFullScreen || document.webkitIsFullScreen; + this.is_full = is_full + return is_full; + }, + + // 进入全屏 + requestFullScreen:function(element){ + if(element == undefined) element = document.documentElement; + // 判断各种浏览器,找到正确的方法 + var requestMethod = element.requestFullScreen || //W3C + element.webkitRequestFullScreen || //FireFox + element.mozRequestFullScreen || //Chrome等 + element.msRequestFullScreen; //IE11 + if (requestMethod) { + requestMethod.call(element); + } else if (typeof window.ActiveXObject !== "undefined") { //for Internet Explorer + var wscript = new ActiveXObject("WScript.Shell"); + if (wscript !== null) { + wscript.SendKeys("{F11}"); + } + } + this.is_full = true; + }, + // 退出全屏 + exitFullscreen:function(element) { + if(element == undefined) element = document.documentElement; + // 判断各种浏览器,找到正确的方法 + var exitMethod = document.exitFullscreen || //W3C + document.mozCancelFullScreen || //FireFox + document.webkitExitFullscreen || //Chrome等 + document.webkitExitFullscreen; //IE11 + if (exitMethod) { + exitMethod.call(document); + } else if (typeof window.ActiveXObject !== "undefined") { //for Internet Explorer + var wscript = new ActiveXObject("WScript.Shell"); + if (wscript !== null) { + wscript.SendKeys("{F11}"); + } + } + this.is_full = false; + }, + + on_resize:function(that){ + var win = $(window)[0],win_width = win.innerHeight,win_height = win.innerHeight,host_commonly = win_height - 185; + if(that.isFullScreen()){ + $('.main-content .safe').height(win_height); + $('#term_box_view,.term_tootls').height(win_height); + $('.tootls_host_list').height((win_height - 80) * .75); + $('.tootls_commonly_list').height((win_height - 80) * .25); + }else{ + $('.main-content .safe').height(win_height - 105); + $('#term_box_view,.term_tootls').height(win_height - 105); + $('.tootls_host_list').height(host_commonly * .75); + $('.tootls_commonly_list').height(host_commonly * .25); + } + var id = $('.term_item_tab .active').data('id'); + var item_term = that.host_term[id].term; + item_term.FitAddon.fit(); + that.host_term[id].resize({cols:item_term.cols, rows:item_term.rows}); + }, + + /** + * @name 本地服务器登录表单 + * @author chudong<2020-08-10> + * @return void + */ + localhost_login_form: function (result) { + var host_form_view = $('#host_form_view').html() + if (!host_form_view) return + var that = this,form = $(this.render_template({html:host_form_view,data:{form:$.extend(that.host_form,{host:'127.0.0.1'})}})),id = $('.localhost_item').data('id') + form.find('.ssh_ps_tips').remove(); + form.prepend('
                                  Login failed, please fill the local server information!
                                  '); + form.append(''); + $('#'+id).append('
                                  '+ form[0].innerHTML +'
                                  '); + if(result){ + if(result.indexOf('@127.0.0.1') != -1){ + var user = result.split('@')[0].split(',')[1]; + var port = result.split('1:')[1] + $("input[name='username']").val(user); + $("input[name='port']").val(port); + + } + + } + $('.auth_type_checkbox').click(function(){ + var index = $(this).index(); + $(this).addClass('btn-success').removeClass('btn-default').siblings().removeClass('btn-success').addClass('btn-default') + switch(index){ + case 0: + $('.c_password_view').addClass('show').removeClass('hidden'); + $('.c_pkey_view').addClass('hidden').removeClass('show').find('input').val(''); + $('.key_pwd_line').addClass('hidden').removeClass('show'); + break; + case 1: + $('.c_password_view').addClass('hidden').removeClass('show').find('input').val(''); + $('.c_pkey_view').addClass('show').removeClass('hidden'); + $('.key_pwd_line').addClass('show').removeClass('hidden'); + break; + } + }); + $('.localhost-form-view > button').click(function(){ + var form = {}; + $('.localhost-form-view input,.localhost-form-view textarea').each(function(index,el){ + var name = $(this).attr('name'),value = $(this).val(); + form[name] = value; + switch(name){ + case 'port': + if(!bt.check_port(value)){ + bt.msg({status:false,msg:'Server port format error!'}); + return false; + } + break; + case 'username': + if(value == ''){ + bt.msg({status:false,msg:'Server user name cannot be empty!'}); + return false; + } + break; + case 'password': + if(value == '' && $('.c_password_view').hasClass('show')){ + bt.msg({status:false,msg:'Server password cannot be empty!'}); + return false; + } + break; + case 'pkey': + if(value == '' && $('.c_pkey_view').hasClass('show')){ + bt.msg({status:false,msg:'The server key cannot be empty!'}); + return false; + } + break; + } + }); + delete form.sort + form.ps = 'Local server'; + that.create_host(form,function(res){ + bt.msg(res); + if(res.status){ + bt.msg({status:true,msg:'Login successful!'}); + $('.localhost_item .icon-trem-close').click(); + that.open_term_view(); + } + }); + }); + $('.localhost-form-view [name="password"]').keyup(function(e){ + if(e.keyCode == 13){ + $('.localhost-form-view > button').click(); + } + }).focus(); + }, + + reader_right_menu:function(config,callback){ + var menu = $('').css({'top':config.position[1],'left':config.position[0]}),html = ''; + bt.each(config.list,function(index,item){ + bt.each(item,function(indexs,items){ + (function(items){ + menu.append($('
                                • ').append($(''+ items.title +'').click(function(e){ + if(items.event) items.event(config.el,$(config.el).data()) + menu.remove(); + if(callback) callback(); + }))); + }(items)); + }); + if(index != config.list.length - 1){ + menu.append('
                                • '); + } + }); + if(!$('#term_title_menu').length){ + $('body').append(menu); + }else{ + $('#term_title_menu').replaceWith(menu); + } + $(document).click(function(e){ + menu.remove(); + $(this).unbind('click'); + if(callback) callback(); + }) + }, + /** + * @name 主机信息添加或编辑 + * @author chudong<2020-08-10> + * @param {Objeact} obj 需要编辑的form数据,可以为空,为空则添加 + * @return void + */ + editor_host_view:function(obj){ + var that = this; + if (!obj) { + obj = { + form: this.host_form, + config: { + btn: 'Submit', title: 'Add host information' + } + } + } + this.render_template({ + html: host_form_view.innerHTML, + data: obj + }, function (html) { + layer.open({ + type: 1 //Page层类型 + , area: '510px' + , closeBtn: 2 + , title: obj.config.title + , btn: [obj.config.btn, 'Cancel'] + , content: html + , success: function (layers, index) { + $('.auth_type_checkbox').click(function () { + var index = $(this).index(); + $(this).addClass('btn-success').removeClass('btn-default').siblings().removeClass('btn-success').addClass('btn-default') + switch (index) { + case 0: + $('.c_password_view').addClass('show').removeClass('hidden'); + $('.c_pkey_view').addClass('hidden').removeClass('show').find('input').val(''); + $('.key_pwd_line').addClass('hidden').removeClass('show'); + break; + case 1: + $('.c_password_view').addClass('hidden').removeClass('show').find('input').val(''); + $('.c_pkey_view').addClass('show').removeClass('hidden'); + $('.key_pwd_line').addClass('show').removeClass('hidden'); + break; + } + }); + + $('[name="host"]').on('change',function(){ + var host_val = $(this).val(); + if(!host_val) return; + + var s_host,s_port,s_username,s_password; + s_host = host_val; + if(host_val.indexOf('@') != -1){ + var tmp = host_val.split('@') + s_host = tmp[1] + s_username = tmp[0] + + if (s_username.indexOf(':') != -1){ + var tmp = s_username.split(':') + s_username = tmp[0] + s_password = tmp[1] + } + } + if(s_host.indexOf(':')!=-1){ + var tmp = s_host.split(':') + s_host = tmp[0] + s_port = tmp[1] + } + + if(s_host) { + $(this).val(s_host); + $('[name="ps"]').val(s_host); + } + if(s_port) $('[name="port"]').val(s_port); + if(s_username) $('[name="username"]').val(s_username); + if(s_password) $('[name="password"]').val(s_password); + }); + + $('[name="host"]').on('input',function(){ + $('[name="ps"]').val($(this).val()); + }); + $('[name="password"],[name="ps"]').keyup(function(e){ + if(e.keyCode === 13){ + $('#layui-layer'+ index +' .layui-layer-btn0').click(); + } + }); + }, + yes:function(indexs,layero){ + var form = {}; + $('.bt-form input,.bt-form textarea').each(function(index,el){ + var name = $(this).attr('name'),value = $(this).val(); + form[name] = value; + switch(name){ + // case 'host': + // if(!bt.check_ip(value)){ + // bt.msg({status:false,msg:'服务器ip地址格式错误!'}); + // return false; + // } + // break; + case 'port': + if(!bt.check_port(value)){ + bt.msg({status:false,msg:'Server port format error!'}); + return false; + } + break; + case 'username': + if(value == ''){ + bt.msg({status:false,msg:'Server user name cannot be empty!'}); + return false; + } + break; + case 'password': + if(value == '' && $('.c_password_view').hasClass('show')){ + bt.msg({status:false,msg:'Server password cannot be empty!'}); + return false; + } + break; + case 'pkey': + if(value == '' && $('.c_pkey_view').hasClass('show')){ + bt.msg({status:false,msg:'Server key cannot be empty!'}); + return false; + } + break; + } + }); + if(!obj.form.sort){ + delete form.sort; + that.create_host(form,function(res){ + if(res.status){ + that.open_term_view(form); + layer.close(indexs) + that.reader_host_list(function(){ + bt.msg(res); + }) + } + }); + }else{ + form.new_host = form.host; + form.host = obj.form.host; + that.modify_host(form,function(res){ + if(res.status){ + layer.close(indexs) + that.reader_host_list(function(){ + bt.msg(res); + }) + } + }) + } + } + }); + }); + }, + + /** + * @name 常用信息添加或编辑 + * @author chudong<2020-08-10> + * @param {Objeact} obj 需要编辑的form数据,可以为空,为空则添加 + * @return void + */ + editor_command_view: function (obj) { + var that = this; + if (!obj) { + obj = { + form: this.command_form, + config: { + btn: 'Submit', title: 'Add command information' + } + }; + } + this.render_template({ + html: shell_form_view.innerHTML, + data: obj + }, function (html) { + layer.open({ + type: 1 //Page层类型 + , area: '510px' + , closeBtn: 2 + , title: obj.config.title + , btn: [obj.config.btn, 'Cancel'] + , content: html + , yes: function (indexs, layero) { + var shell = $('[name="shell"]').val(), title = $('[name="title"]').val(); + if (title == '') { + bt.msg({status: false, msg: 'Command description cannot be empty!'}); + return false; + } + if (shell == '') { + bt.msg({status: false, msg: 'Command cannot be empty!'}); + return false; + } + + if(!obj.form.title){ + that.create_command({shell:shell,title:title},function(res){ + if(res.status){ + layer.close(indexs); + that.reader_command_list(function(){ + bt.msg(res); + }); + } + }); + }else{ + that.modify_command({new_title:title,title:obj.form.title,shell:shell},function(res){ + if(res.status){ + layer.close(indexs); + that.reader_command_list(function(){ + bt.msg(res); + }); + } + }); + } + }, + + }); + }); + }, + /** + * @name 设置终端标题状态 + * @author chudong<2020-08-10> + * @param {String} id 终端ID + * @param {number} status 终端状态 + * @return void + */ + set_term_icon:function(id,status){ + var icon_list = ['icon-warning','icon-sucess','icon-info']; + $("[data-id='"+ id +"']").find('.icon').removeAttr('class').addClass(icon_list[status]+' icon'); + }, + /** + * @name 打开终端显示视图 + * @author chudong<2020-08-10> + * @param {Objeact} info 终端数据 { + * ps:已添加的备注, + * host:已添加的服务器ip + * } + * @return void + */ + open_term_view:function(info){ + if(typeof info === "undefined") info = {host:'127.0.0.1',ps:'Local server'} + var random = bt.get_random(9),tab_content = $('.term_content_tab'),item_list = $('.term_item_tab .list'); + tab_content.find('.term_item').removeClass('active').siblings().removeClass('active'); + tab_content.append('
                                  '); + item_list.find('.item').removeClass('active'); + item_list.append('
                                  '+ info.ps +'
                                  '); + this.host_term[random] = new Terms('#'+random,{ssh_info:{host:info.host,ps:info.ps,id:random}}); + }, + /** + * @name 关闭终端显示视图 + * @author chudong<2020-08-10> + * @param {String} id 终端id + * @return void + */ + remove_term_view:function(id){ + var item = $('[data-id="'+ id +'"]'),next = item.next(),prev = item.prev(); + $('#'+id).remove(); + item.remove(); + try { + this.host_term[id].bws.close(); + } catch (error) { + } + delete this.host_term[id]; + if(item.hasClass('active')){ + if(next.length > 0){ + next.click(); + }else{ + prev.click(); + } + } + }, + /** + * @name 关闭选中右侧终端显示视图 + * @author chudong<2020-08-10> + * @param {String} id 终端id + * @return void + */ + remove_term_right_view:function(id){ + var arry = [],item = $('[data-id="'+ id +'"]'),nextAll = item.nextAll(),that = this; + if(!nextAll.length){ + return false; + } + nextAll.each(function(index,el){ + var data = $(this).data(); + try { + that.host_term[data.id].bws.close(); + } catch (error) { + } + delete that.host_term[data.id]; + }); + nextAll.remove(); + item.addClass('active'); + $('#'+id).addClass('active').nextAll().remove(); + }, + /** + * @name 关闭其他终端显示视图 + * @author chudong<2020-08-10> + * @param {String} id 终端id + * @return void + */ + remove_term_other_view:function(id){ + var arry = [],item = $('[data-id="'+ id +'"]'),siblings = item.siblings(),that = this; + if(!siblings.length){ + return false; + } + siblings.each(function(index,el){ + var data = $(this).data(); + try { + that.host_term[data.id].bws.close(); + } catch (error) { + } + delete that.host_term[data.id]; + }); + siblings.remove(); + item.addClass('active'); + $('#'+id).addClass('active').siblings().remove(); + }, + /** + * @name 渲染常用命令列表 + * @author chudong<2020-08-10> + * @param {Objeact} callback 回调函数,回调参数1:当前请求内容 + * @return void + */ + reader_command_list:function(callback){ + var that = this,html = ''; + this.get_command_list(function(rdata){ + bt.each(rdata,function(index,item){ + html += '
                                • '+ item.title +''+ + ''+ + ''+ + '
                                • '; + }); + $('.tootls_commonly_list').html(html); + var clipboard = new ClipboardJS('.tootls_commonly_list li'); + clipboard.on('success', function(e) { + layer.msg('Copy succeeded!',{icon:1}); + e.clearSelection(); + }); + + clipboard.on('error', function(e) { + console.error('Action:', e.action); + console.error('Trigger:', e.trigger); + }); + that.command_list = rdata; + if(callback) callback(rdata) + }); + }, + + + + /** + * @name 渲染主机视图列表 + * @author chudong<2020-08-10> + * @param {Objeact} callback 回调函数,回调参数1:当前请求内容 + * @return void + */ + reader_host_list:function(callback){ + var that = this,html = ''; + this.get_host_list(function(rdata){ + bt.each(rdata,function(index,item){ + html += '
                                • '+ (item.ps == item.host?item.ps:(item.ps +'【'+ item.host +'】')) +''+ + ''+ + ''+ + '
                                • '; + }); + $('.tootls_host_list').html(html); + that.host_list = rdata; + if(callback) callback() + }); + }, + /** + * @name 获取host列表 + * @author chudong<2020-08-010> + * @param {Objeact} callback 回调函数,回调参数1:当前请求内容 + * @return void + */ + get_host_list:function(callback){ + var loadT = bt.load('Getting server list, please wait...'); + this.post('get_host_list',{},function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + /** + * @name 获取指定host信息 + * @author hwliang<2020-08-07> + * @param host string host地址 + * @return void + */ + get_host_find:function(host,callback){ + var loadT = bt.load('Getting the specified server information, please wait...'); + this.post('get_host_find',{host:host},function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + + /** + * @name 创建新的host信息 + * @author hwliang<2020-08-07> + * @param ssh_info array ssh信息对象 { + * host: 主机地址, + port: 端口 + ps: 备注 + username: 用户名 + password: 密码 + pkey: 密钥(如果不为空,将使用密钥连接) + * } + * @return void + */ + create_host:function(ssh_info,callback){ + var loadT = bt.load('Adding server information, please wait...'); + this.post('create_host',ssh_info,function(rdata){ + loadT.close(); + if(!rdata.status){ + bt.msg(rdata); + return false; + } + if(callback) callback(rdata); + }); + }, + + /** + * @name 修改host信息 + * @author hwliang<2020-08-07> + * @param ssh_info array ssh信息对象 { + * host: 主机地址, + port: 端口 + ps: 备注 + sort: 排序(可选,默认0) + username: 用户名 + password: 密码 + pkey: 密钥(如果不为空,将使用密钥连接) + * } + * @return void + */ + modify_host:function(ssh_info,callback){ + var loadT = bt.load('Modifying the specified server information, please wait...'); + this.post('modify_host',ssh_info,function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + + /** + * @name 删除host信息 + * @author hwliang<2020-08-07> + * @param host string host地址 + * @return void + */ + remove_host:function(host,callback){ + var loadT = bt.load('Deleting specified server information, please wait...'); + this.post('remove_host',{host:host},function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + + /** + * @name 设置host排序(使用降序) + * @author hwliang<2020-08-07> + * @param sort_list array 排序对象{ + * "127.0.0.1":3, + * "192.168.1.254":2 + * } + * @return void + */ + set_sort:function(sort_list,callabck){ + this.post('set_sort',{sort_list:JSON.stringify(sort_list)},function(rdata){ + if(callabck) callabck(rdata); + }); + }, + + + /** + * @name 获取常用命令列表 + * @author hwliang<2020-08-08> + * @return void + */ + get_command_list: function(callback){ + var loadT = bt.load('Getting list of frequently used commands, please wait...'); + this.post('get_command_list',{},function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + /** + * @name 创建常用命令 + * @author hwliang<2020-08-08> + * @param {title} string 命令标题 + * @parma {shell} string 命令文本 + * @return void + */ + create_command: function(obj,callback){ + var loadT = bt.load('Creating common commands, please wait...'); + this.post('create_command',obj,function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + + /** + * @name 获取指定常用命令 + * @author hwliang<2020-08-08> + * @param {title} string 命令标题 + * @return void + */ + get_command_find: function(title,callback){ + var loadT = bt.load('Getting specified common command data, please wait...'); + this.post('get_command_find',{title:title},function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + /** + * @name 修改常用命令 + * @author hwliang<2020-08-08> + * @param {title} string 命令标题 + * @param {new_title} string 新的命令标题 + * @parma {shell} string 命令文本 + * @return void + */ + modify_command: function(obj,callback){ + var loadT = bt.load('Modifying specified common commands, please wait...'); + this.post('modify_command',obj,function(rdata){ + loadT.close(); + if(callback) callback(rdata); + }); + }, + + /** + * @name 删除指定常用命令 + * @author hwliang<2020-08-08> + * @param {title} string 命令标题 + * @return void + */ + remove_command: function(title,callback){ + var loadT = bt.load('Deleting specified common commands, please wait...'); + this.post('remove_command',{title:title},function(rdata){ + loadT.close(); + // console.log(rdata) + if(callback) callback(rdata); + }); + }, + + + /** + * @name 请求到后端 + * @author hwliang<2020-08-07> + * @param fun_name string 要访问的方法名称 + * @param pdata array POST数据 + * @param callback function 回调函数 + * @return void + */ + post:function(fun_name,pdata,callback){ + bt.send(fun_name,'xterm/'+fun_name,pdata,callback); + }, + + /** + * @author chudong<2020-08-08> + * @param {Object} obj 需要渲染的配置对象 + * @param {*} callback 渲染完成的回调方法 + */ + render_template:function(obj, callback) { + if (!obj.html) return '缺少模板HTML'; + var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', + cursor = 0; + var add = function (line, js) { + js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') : + (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''); + return add; + } + while (match = re.exec(obj.html)) { + add(obj.html.slice(cursor, match.index))(match[1], true); + cursor = match.index + match[0].length; + } + add(obj.html.substr(cursor, obj.html.length - cursor)); + code += 'return r.join("");'; + if (callback) { + callback(new Function(code.replace(/[\r\t\n]/g, '')).apply(obj.data)); + } else { + return new Function(code.replace(/[\r\t\n]/g, '')).apply(obj.data); + } + } +} +host_trem.init(); \ No newline at end of file diff --git a/BTPanel/static/vite/oldjs/tools.js b/BTPanel/static/vite/oldjs/tools.js new file mode 100644 index 00000000..35bbe7ed --- /dev/null +++ b/BTPanel/static/vite/oldjs/tools.js @@ -0,0 +1,3193 @@ +var bt_tools = { + commandConnectionPool: {}, //ws连接池 + /** + * @description 表格渲染 + * @param {object} config 配置对象 参考说明 + * @return 当前实例对象 + */ + table: function (config) { + var that = this, + table = $(config.el), + tableData = table.data('table'); + if (tableData && table.find('table').length > 0) { + if (config.url !== undefined) { + tableData.$refresh_table_list(true); + } else if (config.data !== undefined) { + tableData.$reader_content(config.data); + } + return tableData; + } + function ReaderTable(config) { + this.config = config; + this.$load(); + } + + ReaderTable.prototype = { + style_list: [], // 样式列表 + event_list: {}, // 事件列表,已绑定事件 + checkbox_list: [], // 元素选中列表 + batch_active: {}, + event_rows_model: {}, // 事件行内元素模型,行内元素点击都会将数据临时存放 + data: [], + page: '', + column: [], + batch_thread: [], + random: bt.get_random(5), + init: false, // 是否初始化渲染 + checked: false, // 是否激活,用来判断是否失去焦点 + /** + * @description 加载数据 + * @return void + */ + $load: function () { + var _that = this; + if (this.config.init) this.config.init(this); + $(this.config.el).addClass('bt_table'); + if (this.config.minWidth) + this.style_list.push({ + className: this.config.el + ' table', + css: 'min-width:' + this.config.minWidth, + }); + if (this.config.tootls) { + this.$reader_tootls(this.config.tootls); + } else { + if ($(_that.config.el + '.divtable').length === 0) $(_that.config.el).append('
                                  '); + } + this.$reader_content(); + if (_that.config.url !== undefined) { + this.$refresh_table_list(_that.config.load || false); + } else if (this.config.data !== undefined) { + this.$reader_content(this.config.data); + } else { + alert('缺少data或url参数'); + } + if (this.config.methods) { + //挂载实例方法 + $.extend(this, this.config.methods); + } + if (this.config.height) bt_tools.$fixed_table_thead(this.config.el + ' .divtable'); + }, + + /** + * @description 刷新表格数据 + * @param {boolean} load + * @param {function} callback 回调函数 + * @return void + */ + $refresh_table_list: function (load, callback) { + var _that = this; + this.$http(load, function (data) { + if (callback) callback(data); + _that.$reader_content(data.data, typeof data.total != 'undefined' ? parseInt(data.total) : data.page); + }); + }, + + /** + * @description 渲染内容 + * @param {object} data 渲染的数据 + * @param {number} page 数据分页/总数 + * @return void + */ + $reader_content: function (data, page) { + var _that = this, + thead = '', + tbody = '', + i = 0, + column = this.config.column, + event_list = {}, + checkbox = $(_that.config.el + ' .checkbox_' + _that.random); + data = data || []; + this.data = data; + if (checkbox.length) { + checkbox.removeClass('active selected'); + _that.checkbox_list = []; + _that.$set_batch_view(); + } + do { + var rows = data[i], + completion = 0; + if (data.length > 0) tbody += ''; + for (var j = 0; j < column.length; j++) { + var item = column[j]; + if ($.isEmptyObject(item)) { + completion++; + continue; + } + if (i === 0 && !this.init) { + if (!this.init) this.style_list.push(this.$dynamic_merge_style(item, j - completion)); + var sortName = 'sort_' + this.random + '', + checkboxName = 'checkbox_' + this.random, + sortValue = item.sortValue || 'desc'; + thead += + '' + + (item.type == 'checkbox' + ? '' + : '' + item.title + '') + + (item.sort ? '' : '') + + ''; + if (i === 0) { + if (!event_list[sortName] && item.sort) + event_list[sortName] = { + event: this.config.sortEvent, + eventType: 'click', + type: 'sort', + }; + if (!event_list[checkboxName]) + event_list[checkboxName] = { + event: item.checked, + eventType: 'click', + type: 'checkbox', + }; + } + } + if (rows !== undefined) { + var template = '', + className = 'event_' + item.fid + '_' + this.random; + if (item.template) { + template = _that.$custom_template_render(item, rows, j); + } + if (typeof template === 'undefined' || typeof item.template === 'undefined') { + template = this.$reader_column_type(item, rows); + event_list = $.extend(event_list, template[1]); + template = template[0]; + } + var fixed = false; + if (typeof item.fixed != 'undefined' && item.fixed) { + if (typeof item.class != 'undefined') { + if (item.class.indexOf('fixed') === -1) item.class += ' fixed'; + } else { + item.class = 'fixed'; + } + fixed = true; + } + tbody += + '' + + template + + ''; + if (i === 0) { + if (!event_list[className] && item.event) + event_list[className] = { + event: item.event, + eventType: 'click', + type: 'rows', + }; + } + } + } + if (data.length > 0) tbody += ''; + if (data.length == 0) tbody += '' + (this.config['default'] || lan['public'].empty) + ''; + i++; + } while (i < data.length); + if (!this.init) this.$style_bind(this.style_list); + this.$event_bind(event_list); + if (!this.init) { + $(this.config.el + ' .divtable').append( + '' + thead + '' + tbody + '
                                  ' + ); + } else { + $(this.config.el + ' .divtable tbody').html(tbody); + if (this.config.page) { + $(this.config.el + ' .page').replaceWith(this.$reader_page(this.config.page, page)); + } + } + this.init = true; + if (this.config.success) this.config.success(this); + }, + /** + * @description 自定模板渲染 + * @param {object} item 当前元素模型 + * @param {object} rows 当前元素数据 + * @param {number} j 当前模板index + * @return void + */ + $custom_template_render: function (item, rows, j) { + var className = 'event_' + item.fid + '_' + this.random, + _template = item.template(rows, j), + $template = $(_template); + if ($template.length > 0) { + template = $template.addClass(className)[0].outerHTML; + } else { + if (item.type === 'text') { + template = '' + _template + ''; + } else { + template = '' + _template + ''; + } + } + return template; + }, + + /** + * @description 替换table数据 + * @param {string} newValue 内容数据 + * @return void + */ + $modify_row_data: function (newValue) { + this.event_rows_model.rows = $.extend(this.event_rows_model.rows, newValue); + var row_model = this.event_rows_model, + template = null; + if (typeof row_model.model.template != 'undefined') { + template = $(this.$custom_template_render(row_model.model, row_model.rows, row_model.index)); + if (!template.length) template = $(this.$reader_column_type(row_model.model, row_model.rows)[0]); + } else { + template = $(this.$reader_column_type(row_model.model, row_model.rows)[0]); + } + if (row_model.model.type == 'group') { + $(row_model.el).parent().empty().append(template); + } else { + row_model.el.replaceWith(template); + } + row_model.el = template; + }, + + /** + * @description 批量执行程序 + * @param {object} config 配置文件 + * @return void + */ + $batch_success_table: function (config) { + var _that = this, + length = $(config.html).length; + bt.open({ + type: 1, + title: config.title, + area: config.area || ['400px'], + shadeClose: false, + closeBtn: 2, + content: + config.content || + '
                                  ' + + config.title + + ' ' + + lan['public'].success + + '
                                  ' + + config.html + + '
                                  ' + + config.th + + '' + + lan['public'].result + + '
                                  ', + success: function () { + if (length > 4) _that.$fixed_table_thead('.fiexd_thead'); + }, + }); + }, + /** + * @description 固定表头 + * @param {string} el DOM选择器 + * @return void + */ + $fixed_table_thead: function (el) { + $(el).scroll(function () { + var scrollTop = this.scrollTop; + this.querySelector('thead').style.transform = 'translateY(' + (scrollTop - 1) + 'px)'; + }); + }, + /** + * @description 删除行内数据 + */ + $delete_table_row: function (index) { + this.data.splice(index, 1); + this.$reader_content(this.data); + }, + + /** + * @description 设置批量操作显示 + * @return void 无 + */ + $set_batch_view: function () { + if (typeof this.config.batch != 'undefined') { + //判断是否存在批量操作 + var bt_select_btn = $(this.config.el + ' .set_batch_option'); + if (typeof this.config.batch.config != 'undefined') { + // 判断批量操作是多个还是单个 + if (this.checkbox_list.length > 0) { + bt_select_btn + .removeClass('bt-disabled btn-default') + .addClass('btn-success') + .text(lan['public'].please_choose + this.batch_active.title + '(' + lan['public'].selected + this.checkbox_list.length + ')'); + } else { + bt_select_btn + .addClass('bt-disabled btn-default') + .removeClass('btn-success') + .text(lan['public'].please_choose + this.batch_active.title); + } + } else { + var bt_select_val = $(this.config.el + ' .bt_table_select_group .bt_select_value'); + if (this.checkbox_list.length > 0) { + bt_select_btn.removeClass('bt-disabled btn-default').addClass('btn-success').prev().removeClass('bt-disabled'); + bt_select_val.find('em').html('(' + lan['public'].selected + this.checkbox_list.length + ')'); + } else { + bt_select_btn.addClass('bt-disabled btn-default').removeClass('btn-success').prev().addClass('bt-disabled'); + bt_select_val + .children() + .eq(0) + .html(lan['public'].please_choose + ''); + bt_select_val.next().find('li').removeClass('active'); + this.batch_active = {}; + } + } + } + }, + + /** + * @description 渲染指定类型列内容 + * @param {object} data 渲染的数据 + * @param {object} rows 渲染的模板 + * @return void + */ + $reader_column_type: function (item, rows) { + var value = rows[item.fid], + event_list = {}, + className = 'click_' + item.fid + '_' + this.random, + config = [], + _that = this; + switch (item.type) { + case 'text': //普通文本 + config = [value, event_list]; + break; + case 'checkbox': //单选内容 + config = ['', event_list]; + break; + case 'password': + var _copy = '', + _eye_open = '', + className = 'ico_' + _that.random + '_', + html = '**********'; + if (item.eye_open) { + html += ''; + if (!event_list[className + 'eye_open']) + event_list[className + 'eye_open'] = { + type: 'eye_open_password', + }; + } + if (item.copy) { + html += ''; + if (!event_list[className + 'copy']) + event_list[className + 'copy'] = { + type: 'copy_password', + }; + } + config = [html, event_list]; + break; + case 'link': //超链接类型 + className += '_' + item.fid; + if (!event_list[className] && item.event) + event_list[className] = { + event: item.event, + type: 'rows', + }; + config = [ + '' + + value + + '', + event_list, + ]; + break; + case 'input': //可编辑类型 + blurName = 'blur_' + item.fid + '_' + this.random; + keyupName = 'keyup_' + item.fid + '_' + this.random; + if (!event_list[blurName] && item.blur) + event_list[blurName] = { + event: item.blur, + eventType: 'blur', + type: 'rows', + }; + if (!event_list[keyupName] && item.keyup) + event_list[keyupName] = { + event: item.keyup, + eventType: 'keyup', + type: 'rows', + }; + config = ['', event_list]; + break; + case 'status': // 状态类型 + var active = ''; + $.each(item.config.list, function (index, items) { + if (items[0] === value) active = items; + }); + if (!event_list[className] && item.event) + event_list[className] = { + event: item.event, + type: 'rows', + }; + config = [ + '' + + active[1] + + '' + + (item.config.icon ? '' : '') + + '', + event_list, + ]; + break; + case 'switch': //开关类型 + var active = '', + _random = bt.get_random(5); + active = new Number(value) == true ? 'checked' : ''; + if (!event_list[className] && item.event) + event_list[className] = { + event: item.event, + type: 'rows', + }; + config = [ + '
                                  ', + event_list, + ]; + break; + case 'group': + var _html = ''; + $.each(item.group, function (index, items) { + className = (item.fid ? item.fid : 'group') + '_' + index + '_' + _that.random; + var _hide = false; + if (items.template) { + var _template = items.template(rows, _that), + $template = $(_template); + if ($template.length > 0) { + _html += $template.addClass(className)[0].outerHTML; + } else { + _html += '' + _template + ''; + } + } else { + if (typeof items.hide != 'undefined') { + _hide = typeof items.hide === 'boolean' ? items.hide : items.hide(rows); + if (typeof _hide != 'boolean') return false; + } + _html += '' + items.title + ''; + } + //当前操作按钮长度等于当前所以值时不向后添加分割 + if (!_hide) { + if (items.template) { + var _template = items.template(rows, _that); + if (_template == '') { + _html += ''; + } else { + _html += item.group.length == index + 1 ? '' : ' | '; + } + } else { + _html += item.group.length == index + 1 ? '' : ' | '; + } + } + if (!event_list[className] && items.event) + event_list[className] = { + event: items.event, + type: 'rows', + }; + }); + config = [_html, event_list]; + break; + default: + config = [value, event_list]; + break; + } + return config; + }, + /** + * @description 批量执行程序 + * @param {object} config 配置文件 + * @return void + */ + $batch_success_table: function (config) { + that.$batch_success_table(config); + }, + /** + * @description 渲染工具条 + * @param {object} data 配置参数 + * @return void + */ + $reader_tootls: function (config) { + var _that = this, + event_list = {}; + + /** + * @description 请求方法 + * @param {Function} callback 回调函数 + * @returns void + */ + function request(active, check_list) { + var loadT = bt.load(active.title + lan.site.executing), + batch_config = {}, + list = _that.$get_data_batch_list(active.paramId, check_list); + if (!active.beforeRequest) { + batch_config[active.paramName] = list.join(','); + } else { + batch_config[active.paramName] = active.beforeRequest(check_list); + } + bt_tools.send( + { + url: active.url || _that.config.batch.url, + data: $.extend(active.param || {}, batch_config), + }, + function (res) { + loadT.close(); + if (res.status === false && typeof res.success === 'undefined') { + bt_tools.msg(res); + return false; + } + if (typeof active.tips === 'undefined' || active.tips) { + var html = ''; + $.each(res.error, function (key, item) { + html += + '' + + key + + '
                                  ' + + item + + '
                                  '; + }); + $.each(res.success, function (index, item) { + html += + '' + + item + + '
                                  ' + + lan['public'].success + + '
                                  '; + }); + _that.$batch_success_table({ + title: active.title, + th: active.theadName, + html: html, + }); + if (active.refresh) _that.$refresh_table_list(true); + } else { + if (!active.success) { + var html = ''; + $.each(check_list, function (index, item) { + html += + '' + + item.name + + '
                                  ' + + ((typeof active.theadValue != 'undefined' ? active.theadValue[res.status ? 0 : 1] : null) || res.msg) + + '
                                  '; + }); + _that.$batch_success_table({ + title: 'Batch' + active.title + 'Success', + th: active.theadName, + html: html, + }); + if (active.refresh) _that.$refresh_table_list(true); + } + } + if (active.success) { + active.success(res, check_list, _that); + } + } + ); + } + + /** + * @description 执行批量,包含递归批量和自动化批量 + * @returns void + */ + function execute_batch(active, check_list, success) { + // if(active.recursion) 递归方式 + var bacth = { + loadT: 0, + config: {}, + check_list: check_list, + bacth_status: true, + start_batch: function (param, callback) { + var _this = this; + if (typeof param == 'undefined') param = {}; + if (typeof param == 'function') (callback = param), (param = {}); + if (active.load) + this.loadT = layer.msg( + lan['public'].executeing + active.title + ',' + lan['public'].schedule + ':0/' + this.check_list.length + ',' + lan['public'].please_wait, + { + icon: 16, + skin: 'batch_tips', + shade: 0.3, + time: 0, + area: '400px', + } + ); + this.config = { + param: param, + url: active.url, + }; + this.bacth(callback); + }, + /** + * + * @param {Number} index 递归批量程序 + * @param {Function} callback 回调函数 + * @return void(0) + */ + bacth: function (index, callback) { + var _this = this, + param = {}; + if (typeof index === 'function' || typeof index === 'undefined') (callback = index), (index = 0); + if (index < this.check_list.length) { + 'function' == typeof active.url ? (this.config.url = active.url(check_list[index])) : (this.config.url = active.url); + if (typeof active.param == 'function') { + param = active.param(check_list[index]); + } else { + param = active.param; + } + this.config.param = $.extend(this.config.param, param); + if (typeof active.paramId != 'undefined') _this.config.param[active.paramName || active.paramId] = _this.check_list[index][active.paramId]; + if (typeof active.beforeBacth != 'undefined') this.config.param = $.extend(this.config.param, active.beforeBacth(_this.check_list[index])); + if (this.config.param['bacth'] && index == this.check_list.length - 1) { + delete this.config.param['bacth']; + } + if (!_this.bacth_status) return false; + if (active.load) + $('#layui-layer' + _this.loadT) + .find('.layui-layer-content') + .html( + '' + + lan['public'].executeing + + active.title + + ',' + + lan['public'].schedule + + ':' + + index + + '/' + + _this.check_list.length + + ',' + + lan['public'].please_wait + + (active.clear ? 'cancel' : '') + ); + bt_tools.send( + { + url: this.config.url, + data: this.config.param, + bacth: true, + }, + function (res) { + $.extend( + _this.check_list[index], + { + request: { + status: typeof res.status === 'boolean' ? res.status : false, + msg: res.msg || lan['public'].request_error, + }, + }, + { requests: res } + ); + index++; + _this.bacth(index, callback); + } + ); + } else { + if (success) success(); + if (callback) { + callback(this.check_list); + } + if (active.automatic) { + var html = ''; + for (var i = 0; i < this.check_list.length; i++) { + var item = this.check_list[i]; + html += + '' + + (typeof item[active.paramThead] != 'undefined' ? item[active.paramThead] : item.name) + + '
                                  ' + + item.request.msg + + '
                                  '; + } + _that.$batch_success_table({ + title: 'Batch' + active.title, + th: active.theadName, + html: html, + }); + if (active.refresh) _that.$refresh_table_list(true); + _that.$clear_table_checkbox(); + } + layer.close(this.loadT); + } + }, + clear_bacth: function () { + this.bacth_status = false; + layer.close(this.loadT); + }, + }; + if (active.callback) { + active.callback(bacth); + } else { + if (!active.confirm || active.recursion) { + if (active.confirmVerify) { + bt.show_confirm('批量操作' + active.title + '已选中', '批量' + active.title + ',该操作可能会存在风险,是否继续?', function (index) { + layer.close(index); + if (!active.recursion) { + request(active, check_list); + } else { + bacth.start_batch(); + } + }); + } else { + bt.confirm( + { + title: 'Batch ' + active.title, + msg: 'Please be cautious, The selected item will be [ ' + active.title + ' ] after confirmation', + shadeClose: active.shadeClose ? active.shadeClose : false, + }, + function (index) { + layer.close(index); + if (!active.recursion) { + request(active, check_list); + } else { + bacth.start_batch(); + } + } + ); + } + } else { + request(active, check_list); + } + } + } + + for (var i = 0; i < config.length; i++) { + var template = ''; + var item = config[i]; + var positon = []; + switch (item.type) { + case 'group': + positon = item.positon || ['left', 'top']; + $.each(item.list, function (index, items) { + var _btn = item.type + '_' + _that.random + '_' + index, + html = ''; + if (items.type == 'division') { + template += ''; + } else { + if (!items.group) { + template += + ''; + } else { + template += + '
                                  \ + \ + \ +
                                  '; + if (item.list) { + $.each(item.list, function (index, items) { + html += '
                                • ' + items[item.key] + '
                                • '; + }); + } + if (items.init) + setTimeout(function () { + items.init(_btn); + }, 400); + } + } + if (!event_list[_btn]) + event_list[_btn] = { + event: items.event, + type: 'button', + }; + }); + break; + case 'search': + positon = item.positon || ['right', 'top']; + item.value = item.value || ''; + this.config.search = item; + var _input = 'search_input_' + this.random, + _focus = 'search_focus_' + this.random, + _btn = 'search_btn_' + this.random; + template = + ''; + if (!event_list[_input]) + event_list[_input] = { + eventType: 'keyup', + type: 'search_input', + }; + if (!event_list[_focus]) + event_list[_focus] = { + type: 'search_focus', + eventType: 'focus', + }; + if (!event_list[_btn]) + event_list[_btn] = { + type: 'search_btn', + }; + break; + case 'batch': + positon = item.positon || ['left', 'bottom']; + item.placeholder = item.placeholder || '请选择批量操作'; + item.buttonValue = item.buttonValue || '批量操作'; + this.config.batch = item; + var batch_list = [], + _html = '', + active = item.config; + if (typeof item.config != 'undefined') { + _that.batch_active = active; + $(_that.config.el).on('click', '.set_batch_option', function (e) { + var check_list = []; + for (var i = 0; i < _that.checkbox_list.length; i++) { + check_list.push(_that.data[_that.checkbox_list[i]]); + } + if ($(this).hasClass('bt-disabled')) { + layer.tips(_that.config.batch.disabledTips || 'Select batch operation', $(this), { + tips: [1, 'red'], + time: 2000, + }); + return false; + } + switch (typeof active.confirm) { + case 'function': + active.confirm(active, function (param, callback) { + active.param = $.extend(active.param, param); + execute_batch(active, check_list, callback); + }); + break; + case 'undefined': + execute_batch(active, check_list); + break; + case 'object': + var config = active.confirm; + bt.open({ + title: config.title || 'Batch execute', + area: config.area || '350px', + btn: config.btn || ['Confirm', 'Cancel'], + content: config.content, + success: function (layero, index) { + config.success(layero, index, active); + }, + yes: function (index, layero) { + config.yes(index, layero, function (param, callback) { + active.param = $.extend(active.param, param); + request(active, check_list); + }); + }, + }); + break; + } + }); + } else { + $.each(item.selectList, function (index, items) { + if (items.group) { + $.each(items.group, function (indexs, itemss) { + batch_list.push($.extend({}, items, itemss)); + _html += '
                                • ' + itemss.title + '
                                • '; + }); + delete items.group; + } else { + batch_list.push(items); + _html += '
                                • ' + items.title + '
                                • '; + } + }); + // 打开批量类型列表 + $(_that.config.el) + .unbind() + .on('click', '.bt_table_select_group .bt_select_value', function (e) { + var _this = this, + $parent = $(this).parent(), + bt_selects = $parent.find('.bt_selects'), + area = $parent.offset(), + _win_area = _that.$get_win_area(); + if ($parent.hasClass('bt-disabled')) { + layer.tips(_that.config.batch.disabledSelectValue, $parent, { + tips: [1, 'red'], + time: 2000, + }); + return false; + } + if ($parent.hasClass('active')) { + $parent.removeClass('active'); + } else { + $parent.addClass('active'); + } + if (bt_selects.height() > _win_area[1] - area.top) { + bt_selects.addClass('top'); + } else { + bt_selects.removeClass('top'); + } + $(document).one('click', function () { + $(_that.config.el).find('.bt_table_select_group').removeClass('active'); + return false; + }); + return false; + }); + // 选择批量的类型 + $(_that.config.el).on('click', '.bt_table_select_group .item', function (e) { + var _text = $(this).text(), + _index = $(this).index(); + $(this).addClass('active').siblings().removeClass('active'); + $(_that.config.el + ' .bt_select_tips').html('Batch execute ' + _text + '(' + lan.site.have_been_selected + _that.checkbox_list.length + ')'); + _that.batch_active = batch_list[_index]; + if (!_that.checked) $('.bt_table_select_group').removeClass('active'); + }); + // 执行批量操作 + $(_that.config.el).on('click', '.set_batch_option', function (e) { + var check_list = [], + active = _that.batch_active; + if ($(this).hasClass('bt-disabled')) { + layer.tips(_that.config.batch.disabledSelectValue, $(this), { + tips: [1, 'red'], + time: 2000, + }); + return false; + } + for (var i = 0; i < _that.checkbox_list.length; i++) { + check_list.push(_that.data[_that.checkbox_list[i]]); + } + if (JSON.stringify(active) === '{}') { + var bt_table_select_group = $(_that.config.el + ' .bt_table_select_group'); + layer.tips(lan['public'].select_opt_type, bt_table_select_group, { + tips: [1, 'red'], + time: 2000, + }); + bt_table_select_group.css('border', '1px solid red'); + setTimeout(function () { + bt_table_select_group.removeAttr('style'); + }, 2000); + return false; + } + switch (typeof active.confirm) { + case 'function': + active.confirm(active, function (param, callback) { + active.param = $.extend(active.param, param); + execute_batch(active, check_list, callback); + }); + break; + case 'undefined': + execute_batch(active, check_list); + break; + case 'object': + var config = active.confirm; + bt.open({ + type: 1, + title: config.title || lan['public'].bulk_opt, + area: config.area || '350px', + btn: config.btn || [lan['public'].confirm, lan['public'].cancel], + content: config.content, + success: function (layero, index) { + config.success(layero, index, active); + }, + yes: function (index, layero) { + config.yes(index, layero, function (param, callback) { + active.param = $.extend(active.param, param); + layer.close(index); + request(active, check_list); + }); + }, + }); + break; + } + }); + } + template = + '
                                  ' + + (typeof item.config != 'undefined' + ? '' + : '
                                  ' + + lan['public'].select_opt_type + + '
                                    ' + + _html + + '
                                  ') + + '
                                  '; + break; + case 'page': + positon = item.positon || ['right', 'bottom']; + item.page = config.page || 1; + item.pageParam = item.pageParam || 'p'; + item.number = item.number || 20; + item.numberList = item.numberList || [10, 20, 50, 100, 200]; + item.numberParam = typeof item.numberParam === 'boolean' ? item.numberParam : item.numberParam || 'limit'; + this.config.page = item; + // var pageNumber = bt.get_cookie('page_number') + var pageNumber = this.$get_page_number(); + // if (this.config.cookiePrefix && pageNumber) this.config.page.number = pageNumber + if (pageNumber) this.config.page.number = pageNumber; + template = this.$reader_page(this.config.page, '
                                  1Total 0
                                  '); + break; + } + if (template) { + var tools_group = $(_that.config.el + ' .tootls_' + positon[1]); + if (tools_group.length) { + var tools_item = tools_group.find('.pull-' + positon[0]); + tools_item.append(template); + } else { + var tools_group_elment = + '
                                  ' + + (positon[0] === 'left' ? template : '') + + '
                                  ' + + (positon[0] === 'right' ? template : '') + + '
                                  '; + if (positon[1] === 'top') { + $(_that.config.el).append(tools_group_elment); + if ($(_that.config.el + ' .divtable').length === 0) $(_that.config.el).append('
                                  '); + } else { + if ($(_that.config.el + ' .divtable').length === 0) $(_that.config.el).append('
                                  '); + $(_that.config.el).append(tools_group_elment); + } + } + } + } + if (!this.init) this.$event_bind(event_list); + }, + + $clear_table_checkbox: function () { + $(this.config.el).find('.bt_table .cust—checkbox').removeClass('selected active'); + }, + /** + * @description 获取数据批量列表 + * @param {string} 需要获取的字段 + * @return {array} 当前需要批量列表 + */ + $get_data_batch_list: function (fid, data) { + var arry = []; + $.each(data || this.data, function (index, item) { + arry.push(item[fid]); + }); + return arry; + }, + + /** + * @description 渲染分页 + * @param {object} config 配置文件 + * @param {object} page 分页 + * @return string + */ + $reader_page: function (config, page) { + var template = '', + eventList = {}, + _that = this, + $page = null; + + if (config.number && !page) { + template = + (config.page !== 1 ? 'Start' : '') + + (config.page !== 1 ? 'Prev' : '') + + (_that.data.length === config.number ? 'Next' : '') + + 'Page ' + + config.page + + ''; + eventList['page_link_' + this.random] = { type: 'cut_page_number' }; + } else { + if (typeof page === 'number') page = this.$custom_page(page); + $page = $(page); + $page.find('a').addClass('page_link_' + this.random); + template += $page.html(); + if (config.numberStatus) { + var className = 'page_select_' + this.random, + number = _that.$get_page_number(); + template += ''; + eventList[className] = { eventType: 'change', type: 'page_select' }; + } + if (config.jump) { + var inputName = 'page_jump_input_' + this.random; + var btnName = 'page_jump_btn_' + this.random; + template += + '
                                  ' + + lan['public'].jump_to_page + + '
                                  '; + eventList[inputName] = { + eventType: 'keyup', + type: 'page_jump_input', + }; + eventList[btnName] = { + type: 'page_jump_btn', + }; + } + eventList['page_link_' + this.random] = { + type: 'cut_page_number', + }; + _that.config.page.total = + $page.length === 0 + ? 0 + : typeof page == 'number' + ? page + : parseInt( + $page + .find('.Pcount') + .html() + .match(/([0-9]*)/g)[1] + ); + } + + _that.$event_bind(eventList); + return '
                                  ' + template + '
                                  '; + }, + /** + * @description 渲染样式 + * @param {object|string} data 样式配置 + * @return {string} 样式 + */ + $reader_style: function (data) { + var style = ''; + if (typeof data === 'string') return data; + if (typeof data === 'undefined') return ''; + $.each(data, function (key, item) { + style += key + ':' + item + ';'; + }); + return style; + }, + /** + * @description 自定义分页 + * @param {} + */ + $custom_page: function (total) { + var html = '
                                  ', + config = this.config.page, + page = Math.ceil(total / config.number), + tmpPageIndex = 0; + if (config.page > 1 && page > 1) { + html += 'HomePrev'; + } + if (page <= 10) { + for (var i = 1; i <= page; i++) { + i == config.page ? (html += '' + i + '') : (html += '' + i + ''); + } + } else if (config.page < 10) { + for (var i = 1; i <= 10; i++) i == config.page ? (html += '' + i + '') : (html += '' + i + ''); + html += '...'; + } else if (page - config.page < 7) { + page - 7 > 1 && ((html += '1'), (html += '...')); + for (var i = page - 7; i <= page; i++) + i == config.page ? (html += '' + i + '') : (html += 1 == i ? '...' : '' + i + ''); + } else { + 0 == tmpPageIndex && (tmpPageIndex = config.page), + (tmpPageIndex <= config.page - 5 || tmpPageIndex >= config.page + 5) && (tmpPageIndex = config.page), + (html += '1'), + (html += '...'); + for (var i = tmpPageIndex - 3; i <= tmpPageIndex + 3; i++) + i == config.page ? (html += '' + i + '') : (html += '' + i + ''); + (html += '...'), (html += '' + page + ''); + } + return ( + page > 1 && config.page < page && (html += 'NextLast'), + (html += 'Total ' + total + '
                                  ') + ); + }, + + /** + * @deprecated 动态处理合并行内,css样式 + * @param {object} rows 当前行数据 + * @return {stinrg} className class类名 + * @return void + */ + $dynamic_merge_style: function (column, index) { + var str = ''; + $.each(column, function (key, item) { + switch (key) { + case 'align': + str += 'text-align:' + item + ';'; + break; + case 'width': + str += 'width:' + (typeof item == 'string' ? item : item + 'px') + ';'; + break; + case 'style': + str += item; + break; + case 'minWidth': + str += 'min-width:' + (typeof item == 'string' ? item : item + 'px') + ';'; + break; + case 'maxWidth': + str += 'max-width:' + (typeof item == 'string' ? item : item + 'px') + ';'; + break; + } + }); + return { + index: index, + css: str, + }; + }, + + /** + * @description 事件绑定 + * @param {array} eventList 事件列表 + * @return void + */ + $event_bind: function (eventList) { + var _that = this; + $.each(eventList, function (key, item) { + if (_that.event_list[key] && _that.event_list[key].eventType === item.eventType) return true; + _that.event_list[key] = item; + $(_that.config.el).on(item.eventType || 'click', '.' + key, function (ev) { + var index = $(this).parents('tr').index(), + data1 = $(this).data(), + arry = [], + column_data = _that.config.column[$(this).parents('td').index()]; + switch (item.type) { + case 'rows': + _that.event_rows_model = { + el: $(this), + model: column_data, + rows: _that.data[index], + index: index, + }; + arry = [_that.event_rows_model.rows, _that.event_rows_model.index, ev, key, _that]; + break; + case 'sort': + var model = _that.config.column[data1.index]; + if ($(this).hasClass('sort-active')) + $('.sort_' + _that.random + ' .sort-active').data({ + sort: 'desc', + }); + $('.sort_' + _that.random) + .removeClass('sort-active') + .find('.glyphicon') + .removeClass('glyphicon-triangle-top') + .addClass('glyphicon-triangle-bottom'); + $(this).addClass('sort-active'); + if (data1.sort == 'asc') { + $(this).data({ + sort: 'desc', + }); + $(this).find('.glyphicon').removeClass('glyphicon-triangle-top').addClass('glyphicon-triangle-bottom'); + } else { + $(this).data({ + sort: 'asc', + }); + $(this).find('.glyphicon').removeClass('glyphicon-triangle-bottom').addClass('glyphicon-triangle-top'); + } + _that.config.sort = _that.config.sortParam({ + name: model.fid, + sort: data1.sort, + }); + _that.$refresh_table_list(true); + break; + case 'checkbox': + var all = $(_that.config.el + ' [data-checkbox="all"]'), + checkbox_list = $(_that.config.el + ' tbody .checkbox_' + _that.random); + if (data1.checkbox == undefined) { + if (!$(this).hasClass('active')) { + $(this).addClass('active'); + _that.checkbox_list.push(index); + if (_that.data.length === _that.checkbox_list.length) { + all.addClass('active').removeClass('selected'); + } else if (_that.checkbox_list.length > 0) { + all.addClass('selected'); + } + } else { + $(this).removeClass('active'); + _that.checkbox_list.splice(_that.checkbox_list.indexOf(index), 1); + if (_that.checkbox_list.length > 0) { + all.addClass('selected').removeClass('active'); + } else { + all.removeClass('selected active'); + } + } + } else { + if (_that.checkbox_list.length === _that.data.length) { + _that.checkbox_list = []; + checkbox_list.removeClass('active selected').next().prop('checked', 'checked'); + all.removeClass('active'); + } else { + checkbox_list.each(function (index, item) { + if (!$(this).hasClass('active')) { + $(this).addClass('active').next().prop('checked', 'checked'); + _that.checkbox_list.push(index); + } + }); + all.removeClass('selected').addClass('active'); + } + } + _that.$set_batch_view(); + break; + case 'button': + arry.push(ev, _that); + break; + case 'search_focus': + var search_tips = $(_that.config.el + ' .bt_search_tips'); + if ($(_that.config.el + ' .bt_search_tips').length > 0) { + search_tips.remove(); + } + break; + case 'search_input': + if (ev.keyCode == 13) { + $(_that.config.el + ' .search_btn_' + _that.random).click(); + return false; + } + break; + case 'search_btn': + var _search = $(_that.config.el + ' .search_input'), + val = $(_that.config.el + ' .search_input').val(), + _filterBox = $('
                                  ').text(val), + _filterText = _filterBox.html(); + + val = _filterText; //过滤xss + val = val.replace(/(^\s*)|(\s*$)/g, ''); + _search.text(val); + _that.config.search.value = val; + if (_that.config.page) _that.config.page.page = 1; + _search.append('
                                  ' + val + '
                                  '); + _that.$refresh_table_list(true); + break; + case 'page_select': + var limit = parseInt($(this).val()); + _that.$set_page_number(limit); + _that.config.page.number = limit; + _that.config.page.page = 1; + _that.$refresh_table_list(true); + return false; + break; + case 'page_jump_input': + if (ev.keyCode === 13) { + $(_that.config.el + ' .page_jump_btn_' + _that.random).click(); + $(this).focus(); + } + return false; + break; + case 'page_jump_btn': + var jump_page = parseInt($(_that.config.el + ' .page_jump_input_' + _that.random).val()), + max_number = Math.ceil(_that.config.page.total / _that.config.page.number); + if (isNaN(jump_page)) jump_page = 1; + if (jump_page > max_number) jump_page = _that.config.page.page; + _that.config.page.page = jump_page; + _that.$refresh_table_list(true); + break; + case 'cut_page_number': + var page = + $(this).data('page') || + parseInt( + $(this) + .attr('href') + .match(/([0-9]*)$/)[0] + ); + _that.config.page.page = page; + _that.$refresh_table_list(true); + return false; + break; + case 'eye_open_password': + if ($(this).hasClass('glyphicon-eye-open')) { + $(this).addClass('glyphicon-eye-close').removeClass('glyphicon-eye-open'); + $(this).prev().text(_that.data[index][column_data.fid]); + } else { + $(this).addClass('glyphicon-eye-open').removeClass('glyphicon-eye-close'); + $(this).prev().html('**********'); + } + return false; + break; + case 'copy_password': + bt.pub.copy_pass(_that.data[index][column_data.fid]); + return false; + break; + } + if (item.event) item.event.apply(this, arry); + }); + }); + }, + + /** + * @description 样式绑定 + * @param {array} style_list 样式列表 + * @return void + */ + $style_bind: function (style_list, status) { + var str = '', + _that = this; + $.each(style_list, function (index, item) { + if (item.css != '') { + if (!item.className) { + str += + _that.config.el + + ' thead th:nth-child(' + + (item.index + 1) + + '),' + + _that.config.el + + ' tbody tr td:nth-child' + + (item.span ? ' span' : '') + + '(' + + (item.index + 1) + + '){' + + item.css + + '}'; + } else { + str += item.className + '{' + item.css + '}'; + } + } + }); + if ($('#bt_table_' + _that.random).length == 0) $(_that.config.el).append(''); + }, + + /** + * @deprecated 获取WIN高度或宽度 + * @return 返回当期的宽度和高度 + */ + $get_win_area: function () { + return [window.innerWidth, window.innerHeight]; + }, + + /** + * @description 获取分页条数 + * @return 返回分页条数 + */ + $get_page_number: function () { + var name = this.config.pageName; + if (name) { + return bt.get_cookie(name + '_page_number'); + } + }, + + /** + * @description 设置分页条数 + * @param {object} limit 分页条数 + * @return void + */ + $set_page_number: function (limit) { + var name = this.config.pageName; + if (name) bt.set_cookie(name + '_page_number', limit); + }, + /** + * @description 请求数据, + * @param {object} param 参数和请求路径 + * @return void + */ + $http: function (load, success) { + var page_number = this.$get_page_number(), + that = this, + param = {}, + config = this.config, + _page = config.page, + _search = config.search, + _sort = config.sort || {}; + if (_page) { + if (page_number && !_page.number) _page.number = page_number; + if (_page.defaultNumber) _page.number = _page.defaultNumber; + + if (_page.numberParam) param[_page.numberParam] = _page.number; + param[_page.pageParam] = _page.page; + } + + if (_search) param[_search.searchParam] = _search.value; + var params = $.extend(config.param, param, _sort); + if (this.config.beforeRequest) { + if (this.config.beforeRequest === 'model') { + config.param = (function () { + if (params.hasOwnProperty('data') && typeof params.data === 'string') { + var oldParams = JSON.parse(params['data']); + delete params['data']; + return { data: JSON.stringify($.extend(oldParams, params)) }; + } + return { data: JSON.stringify(params) }; + })(); + } else { + config.param = this.config.beforeRequest(params); + } + } else { + config.param = params; + } + + bt_tools.send( + { + url: config.url, + data: config.param, + }, + function (res) { + if (typeof config.dataFilter != 'undefined') { + var data = config.dataFilter(res, that); + if (typeof data.tootls != 'undefined') data.tootls = parseInt(data.tootls); + if (success) success(data); + } else { + if (void 0 === res.data) { + success && + success({ + data: res, + }); + } else + success && + success({ + data: res.data, + page: res.page, + }); + } + }, + { load: load ? 'Getting Data' : false, verify: typeof config.dataVerify === 'undefined' ? true : !!config.dataVerify } + ); + }, + }; + var example = new ReaderTable(config); + $(config.el).data('table', example); + return example; + }, + /** + * @description 验证表单 + * @param {object} el 节点 + * @param {object} config 验证配置 + * @param {function} callback 验证成功回调 + */ + verifyForm: function (el, config, callback) { + var verify = false, + formValue = this.getFormValue(el, []); + for (var i = 0; i < config.length; i++) { + var item = config[i]; + verify = item.validator.apply(this, [formValue[item.name], formValue]); + if (typeof verify === 'string') { + this.error(verify); + return false; + } + } + callback && callback(typeof verify !== 'string', formValue); + }, + /** + * @description 获取表单值 + * @param {String} el 表单元素 + * @param {Array} filter 过滤列表 + * @returns {Object} 表单值 + */ + getFormValue: function (el, filter) { + var form = $(el).serializeObject(); + filter = filter && []; + for (var key in form) { + if (filter.indexOf(key) > -1) delete form[key]; + } + return form; + }, + + /** + * @description 设置layer定位 + * @param {object} el 节点 + */ + setLayerArea: function (el) { + var $el = $(el), + width = $el.width(), + height = $el.height(), + winWidth = $(window).width(), + winHeight = $(window).height(); + $el.css({ left: (winWidth - width) / 2, top: (winHeight - height) / 2 }); + }, + + /** + * @description 渲染表单行内容 + * @param {object} config 配置参数 + */ + line: function (config) { + var $line = $( + '
                                  ' + + ((typeof config.must !== 'undefined' && config.must != '' ? '' + config.must + '' : '') + config.label || '') + + '
                                  ' + ); + var $form = this.renderLineForm(config); + $form.data({ line: $line }); + $line.find('.info-r').append($form); + return { + $line: $line, + $form: $form, + }; + }, + + /** + * @description 帮助提示 + * @param {object} config 配置参数 + */ + help: function (config) { + var $help = ''; + for (var i = 0; i < config.list.length; i++) { + var item = config.list[i]; + $help += '
                                • ' + item + '
                                • '; + } + return $('
                                    ' + $help + '
                                  '); + }, + + /** + * @description 渲染表单行内容 + * @param {object} config 配置参数 + * @returns {jQuery|HTMLElement|*} + */ + renderLineForm: function (config) { + config.type = config.type || 'text'; + var lineFilter = ['label', 'labelWidth', 'group', 'on', 'width', 'options', 'type']; // 排除渲染这些属性 + var $form = null; + var props = (function () { + var attrs = {}; + for (var key in config) { + if (lineFilter.indexOf(key) === -1) { + attrs[key] = config[key]; + } + } + return attrs; + })(); + var width = config.width ? 'style="width:' + config.width + '"' : ''; + switch (config.type) { + case 'textarea': + $form = ''; + break; + case 'select': + var options = config.options, + optionsHtml = ''; + for (var i = 0; i < options.length; i++) { + var item = options[i], + newItem = item; + if (typeof item === 'string') newItem = { label: item, value: item }; + optionsHtml += ''; + } + $form = ''; + break; + case 'text': + $form = ''; + break; + } + $form = $($form); + $form.width(config.width || '100%').attr(props); + if (!config.on) config.on = {}; + for (var onKey in config.on) { + (function (onKey) { + $form.on(onKey, function (ev) { + config.on[onKey].apply(this, [ev, $(this).val()]); + }); + })(onKey); + } + return $form; + }, + + /** + * @description 渲染表单行组 + * @param {object} el 配置参数 + * @param {object} config 配置参数 + * @param {object|undefined} formData 表单数据 + */ + fromGroup: function (el, config, formData) { + var $el = $(el), + lineList = {}; + for (var i = 0; i < config.length; i++) { + var item = config[i]; + if (item.type === 'tips') { + $el.append(this.help(item)); + } else { + var line = this.line(item); + if (typeof formData != 'undefined') line.$form.val(formData[item.name] || ''); + lineList[line.$form.attr('name')] = line; + $el.append(line.$line); + } + } + return lineList; + }, + + /** + * @description 渲染Form表单 + * @param {*} config + * @return 当前实例对象 + */ + form: function (config) { + var _that = this; + + function ReaderForm(config) { + this.config = config; + this.el = config.el; + this.submit = config.submit; + this.data = config.data || {}; + this.$load(); + } + + ReaderForm.prototype = { + element: null, + style_list: [], // 样式列表 + event_list: {}, // 事件列表,已绑定事件 + event_type: ['click', 'event', 'focus', 'keyup', 'blur', 'change', 'input'], + hide_list: [], + form_element: {}, + form_config: {}, + random: bt.get_random(5), + $load: function () { + var that = this; + if (this.el) { + $(this.el).html(this.$reader_content()); + this.$event_bind(); + } + }, + + /** + * @description 渲染Form内容 + * @param {Function} callback 回调函数 + */ + $reader_content: function (callback) { + var that = this, + html = '', + _content = ''; + $.each(that.config.form, function (index, item) { + if (item.separate) { + html += '
                                  ' + item.separate + '
                                  '; + } else { + html += that.$reader_content_row(index, item); + } + }); + that.element = $('' + html + ''); + _content = $('
                                  '); + _content.append(that.element); + if (callback) callback(); + return _content[0].outerHTML; + }, + + /** + * @description 渲染行内容 + * @param {object} data Form数据 + * @param {number} index 下标 + * @return {string} HTML结构 + */ + $reader_content_row: function (index, data) { + try { + var that = this, + help = data.help || false, + labelWidth = data.formLabelWidth || this.config.formLabelWidth; + if (data.display === false) return ''; + return ( + '
                                  ' + + (typeof data.label !== 'undefined' + ? '' + + (typeof data.must !== 'undefined' && data.must != '' ? '' + data.must + '' : '') + + data.label + + '' + : '') + + '
                                  ' + + that.$reader_form_element(data.group, index) + + (help ? '
                                  ' + help.list.join('
                                  ') + '
                                  ' : '') + + '
                                  ' + + '
                                  ' + ); + } catch (error) { + console.log(error); + } + }, + + /** + * @description 渲染form类型 + * @param {object} data 表单数据 + * @param {number} index 下标 + * @return {string} HTML结构 + */ + $reader_form_element: function (data, index) { + var that = this, + html = ''; + if (!Array.isArray(data)) data = [data]; + $.each(data, function (key, item) { + item.find_index = index; + html += that.$reader_form_find(item); + that.form_config[item.name] = item; + }); + return html; + }, + /** + * @descripttion 渲染单个表单元素 + * @param {Object} item 配置 + * @return: viod + */ + $reader_form_find: function (item) { + var that = this, + html = '', + style = that.$reader_style(item.style) + _that.$verify(item.width, 'width', 'style'), + attribute = that.$verify_group(item, ['name', 'placeholder', 'disabled', 'readonly', 'autofocus', 'autocomplete', 'min', 'max']), + event_group = that.$create_event_config(item), + eventName = '', + index = item.find_index; + if (item.display === false) return html; + html += item.label ? '' + item.label + '' : ''; + if (typeof item['name'] !== 'undefined') { + that.$check_event_bind(item.name, event_group); + } + html += '
                                  '; + var _value = typeof that.data[item.name] !== 'undefined' && that.data[item.name] != '' ? that.data[item.name] : item.value || ''; + switch (item.type) { + case 'text': // 文本选择 + case 'checkbox': // 复选框 + case 'password': // 密码 + case 'radio': // 单选框 + case 'number': // 数字 + var _event = 'event_' + item.name + '_' + that.random; + switch (item.type) { + case 'checkbox': // 复选框 + html += + ''; + if (!(typeof item.disabled != 'undefined' && item.disabled)) { + that.$check_event_bind(_event, { + input: { + type: 'checkbox', + config: item, + event: item.event, + }, + }); + } + break; + case 'radio': + $.each(item.list,function(keys,rItem){ + var radioRandom = _event+'_radio_'+keys + html+= '' + that.$check_event_bind(radioRandom, {'input': {type: 'radio', config: item, event: item.event}}) + }) + break; + default: + html += + ''; + break; + } + if (item.btn && !item.disabled) { + html += '' + item.btn.title + ''; + if (typeof item.btn.event !== 'undefined') { + that.$check_event_bind(item.name + '_btn', { + click: { + config: item, + event: item.btn.event, + }, + }); + } + } + if (item.icon) { + html += + ''; + if (typeof item.icon.event !== 'undefined') { + that.$check_event_bind(item.name + '_icon', { + click: { + type: 'select_path', + select: item.icon.select || '', + config: item, + children: '.' + item.name + '_icon', + event: item.icon.event, + callback: item.icon.callback, + }, + }); + } + } + break; + case 'textarea': + html += ''; + $.each(['blur', 'focus', 'input'], function (index, items) { + if (item.tips) { + var added = null, + event = {}; + switch (items) { + case 'blur': + added = function (ev, item, element) { + if ($(this).val() === '') $(this).next().show(); + layer.close(item.tips.loadT); + $(ev.target).data('layer', ''); + }; + break; + case 'focus': + added = function (ev, item) { + $(this).next().hide(); + item.tips.loadT = layer.tips(tips, $(this), { + tips: [1, '#20a53a'], + time: 0, + area: $(this).width(), + }); + }; + break; + } + } + that.event_list[item.name][items] + ? (that.event_list[item.name][items]['added'] = added) + : (that.event_list[item.name][items] = { + type: item.type, + cust: false, + event: item[items], + added: added, + }); + }); + if (item.tips) { + var tips = ''; + if (typeof item.tips.list === 'undefined') { + tips = item.tips.text; + } else { + tips = item.tips.list.join('
                                  '); + } + html += '
                                  ' + tips + '
                                  '; + that.$check_event_bind(item.name + '_tips', { + click: { + type: 'textarea_tips', + config: item, + }, + }); + } + break; + case 'select': + html += that.$reader_select(item, style, attribute, index); + that.$check_event_bind('custom_select', { + click: { + type: 'custom_select', + children: '.bt_select_value', + }, + }); + that.$check_event_bind('custom_select_item', { + click: { + type: 'custom_select_item', + children: 'li.item', + }, + }); + break; + case 'link': + eventName = 'event_link_' + that.random + '_' + item.name; + html += '' + item.title + ''; + that.$check_event_bind(eventName, { + click: { + type: 'link_event', + event: item.event, + }, + }); + break; + case 'button': + html += + // ''; + break; + case 'help': + var _html = ''; + $.each(item.list, function (index, items) { + _html += '
                                • ' + items + '
                                • '; + }); + html += '
                                    ' + _html + '
                                  '; + break; + case 'other': + html += item.boxcontent; + } + html += item.unit ? '' + item.unit + '' : ''; + html += '
                                  '; + return html; + }, + + /** + * @descripttion 检测检测名称 + * @param {string} eventName 配置 + * @param {object} config 事件配置 + */ + $check_event_bind: function (eventName, config) { + if (!this.event_list[eventName]) { + if (!this.event_list.hasOwnProperty(eventName)) { + this.event_list[eventName] = config; + } + } + }, + /** + * @description 创建事件配置 + * @param {object} item 行内配置 + * @return {object} 配置信息 + */ + $create_event_config: function (item) { + var config = {}; + if (typeof item['name'] === 'undefined') return {}; + $.each(this.event_type, function (key, items) { + if (item[items]) { + config[items === 'event' ? 'click' : items] = { + type: item.type, + event: item[items], + cust: ['select', 'checkbox', 'radio'].indexOf(item.type) > -1, + config: item, + }; + } + }); + return config; + }, + /** + * @description 渲染样式 + * @param {object|string} data 样式配置 + * @return {string} 样式 + */ + $reader_style: function (data) { + var style = ''; + if (typeof data === 'string') return data; + if (typeof data === 'undefined') return ''; + $.each(data, function (key, item) { + style += key + ':' + item + ';'; + }); + return style; + }, + /** + * @descripttion 局部刷新form表单元素 + * @param {String} name 需要刷新的元素 + * @param {String} name 元素新数据 + * @return: viod + */ + $local_refresh: function (name, config) { + var formFind = this.element.find('[data-name=' + name + ']'); + if (this.element.find('[data-name=' + name + ']').length === 0) formFind = this.element.find('[name=' + name + ']'); + formFind.parent().replaceWith(this.$reader_form_find(config)); + }, + /** + * @description 渲染下拉,内容方法 + */ + $reader_select: function (item, style, attribute, index) { + var that = this, + list = '', + option = '', + active = {}; + if (typeof item.list === 'function') { + var event = item.list; + event.call(this, this.config.form); + item.list = []; + } + if (!Array.isArray(item.list)) { + var config = item.list; + bt_tools.send( + { + url: config.url, + data: config.param || config.data || {}, + }, + function (res) { + if (res.status !== false) { + var list = item.list.dataFilter ? item.list.dataFilter(res, that) : res; + if (item.list.success) item.list.success(res, that, that.config.form[index], list); + item.list = list; + if (!item.list.length) { + item.disabled = true; + layer.msg(item.placeholder || '数据获取为空', { icon: 2 }); + } + that.$replace_render_content(index); + } else { + bt.msg(res); + } + } + ); + return false; + } + if (typeof that.data[item.name] === 'undefined') active = item.list[0]; + $.each(item.list, function (key, items) { + if (items.value === item.value || items.value === that.data[item.name]) { + active = items; + return false; + } + }); + $.each(item.list, function (key, items) { + list += '
                                • ' + items.title + '
                                • '; + option += ''; + }); + var title = !Array.isArray(item.list) ? 'Getting data...' : active ? active.title : item.placeholder; + return ( + '
                                  ' + + '' + + (title || item.placeholder) + + '' + + '
                                    ' + + (list || '') + + '
                                  ' + + '' + + (option || '') + + '' + + '
                                  ' + ); + }, + + /** + * @description 替换渲染内容 + */ + $replace_render_content: function (index) { + var that = this, + config = this.config.form[index], + html = that.$reader_content_row(index, config); + $('[data-form=' + that.random + ']') + .find('.line:eq(' + index + ')') + .replaceWith(html); + this.$event_bind(); + }, + + /** + * @description 重新渲染内容 + * @param {object} formConfig 配置 + */ + $again_render_form: function (formConfig) { + var formElement = $('[data-form=' + this.random + ']'), + that = this; + formConfig = formConfig || this.config.form; + formElement.empty(); + for (var i = 0; i < formConfig.length; i++) { + var config = formConfig[i]; + if (config.display === false) continue; + formElement.append(that.$reader_content_row(i, config)); + } + this.config.form = formConfig; + this.$event_bind(); + }, + + /** + * @description 事件绑定功能 + * @param {Object} eventList 事件列表 + * @param {Function} callback 回调函数 + * @return void + */ + $event_bind: function (eventList, callback) { + var that = this, + _event = {}; + that.element = $(typeof eventList === 'object' ? that.element : '[data-form=' + that.random + ']'); + _event = eventList; + if (typeof eventList === 'undefined') _event = that.event_list; + $.each(_event, function (key, item) { + if ($.isEmptyObject(item)) return true; + $.each(item, function (keys, items) { + if (!!item.type) return false; + if (!items.hasOwnProperty('bind')) { + items.bind = true; + } else { + return false; + } + var childNode = ''; + if (typeof items.cust === 'boolean') { + childNode = '[' + (items.cust ? 'data-' : '') + 'name=' + key + ']'; + } else { + childNode = '.' + key; + } + (function (items, key) { + if (items.onEvent === false) { + switch (items.type) { + case 'input_checked': + $(childNode).on(keys != 'event' ? keys : 'click', function (ev) { + items.event.apply(this, [ev, that]); + }); + break; + } + return true; + } else { + if (items.type === 'select') return true; + that.element.on(keys !== 'event' ? keys : 'click', items.children ? items.children : childNode, function (ev) { + var form = that.$get_form_element(true), + config = that.form_config[key]; + switch (items.type) { + case 'textarea_tips': + $(this).hide().prev().focus(); + break; + case 'custom_select': + if ($(this).parent().hasClass('bt-disabled')) return false; + var select_value = $(this).next(); + if (!select_value.hasClass('show')) { + // var $target = $(ev.currentTarget); + // var $layer = $target.parents('.layui-layer'); + // var layerTop = $layer.length > 0 ? parseFloat($layer.css('top')) : 0; + // var layerLeft = $layer.length > 0 ? parseFloat($layer.css('left')) : 0; + // var offset = $target[0].getBoundingClientRect(); + // var top = offset.top - layerTop + offset.height + 2; + // var left = offset.left - layerLeft; + // $('.bt_select_list').css({ + // position: 'fixed', + // width: offset.width + 'px', + // top: top + 'px', + // left: left + 'px', + // }); + $('.bt_select_list').removeClass('show'); + select_value.addClass('show'); + } else { + select_value.removeClass('show'); + } + $(document).click(function () { + that.element.find('.bt_select_list').removeClass('show'); + $(this).unbind('click'); + return false; + }); + return false; + break; + case 'custom_select_item': + config = that.form_config[$(this).parents('.bt_select_updown').attr('data-name')]; + var item_config = config.list[$(this).index()]; + if ($(this).hasClass('disabled')) { + $(this).parent().removeClass('show'); + if (item_config.tips) layer.msg(item_config.tips, { icon: 2 }); + return true; + } + if (!$(this).hasClass('active') && !$(this).hasClass('disabled')) { + var value = item_config.value.toString(); + $(this).parent().prev().find('.bt_select_content').text($(this).text()); + $(this).addClass('active').siblings().removeClass('active'); + $(this).parent().next().val(value); + $(this).parent().removeClass('show'); + } + that.data[config.name] = value; + if (items.event) items.event = null; + if (config.change) items.event = config.change; + break; + case 'select_path': + bt.select_path('event_' + $(this).prev().attr('name') + '_' + that.random, items.select || '', !items.callback || items.callback.bind(that)); + break; + case 'checkbox': + var checked = $(this).is(':checked'); + if (checked) { + $(this).prev().addClass('active'); + } else { + $(this).prev().removeClass('active'); + } + break; + } + if (items.event) items.event.apply(this, [that.$get_form_value(), form, that, config, ev]); // 事件 + if (items.added) items.added.apply(this, [ev, config, form]); + }); + } + })(items, key); + }); + }); + if (callback) callback(); + }, + + /** + * @description 获取表单数据 + * @return {object} 表单数据 + */ + $get_form_value: function () { + var form = {}; + this.element.find('input,textarea[disabled="disabled"]').each(function (index, item) { + var val = $(this).val(); + if ($(this).attr('type') === 'checkbox') { + val = $(this).prop('checked'); + } + form[$(this).attr('name')] = val; + }); + return $.extend({}, this.element.serializeObject(), form); + }, + + /** + * @description 设置指定数据 + * + */ + $set_find_value: function (name, value) { + var config = {}, + that = this; + typeof name != 'string' ? (config = name) : (config[name] = value); + $.each(config, function (key, item) { + that.form_element[key].val(item); + }); + }, + + /** + * @description 获取Form,jquery节点 + * @param {Boolean} afresh 是否强制刷新 + * @return {object} + */ + $get_form_element: function (afresh) { + var form = {}, + that = this; + if (afresh || $.isEmptyObject(that.form_element)) { + this.element.find(':input').each(function (index) { + form[$(this).attr('name')] = $(this); + }); + that.form_element = form; + return form; + } else { + return that.form_element; + } + }, + + /** + * @description 验证值整个列表是否存在,存在则转换成属性字符串格式 + */ + $verify_group: function (config, group) { + var that = this, + str = ''; + $.each(group, function (index, item) { + if (typeof config[item] === 'undefined') return true; + if (['disabled', 'readonly'].indexOf(item) > -1) { + str += ' ' + (config[item] ? item + '="' + item + '"' : ''); + } else { + str += ' ' + item + '="' + config[item] + '"'; + } + }); + return str; + }, + + /** + * @description 验证绑定事件 + * @param {String} value + */ + $verify_bind_event: function (eventName, row, group) { + var event_list = {}; + $.each(group, function (index, items) { + var event_fun = row[items]; + if (event_fun) { + if (typeof event_list[eventName] === 'object') { + if (!Array.isArray(event_list[eventName])) event_list[eventName] = [event_list[eventName]]; + event_list[eventName].push({ + event: event_fun, + eventType: items, + }); + } else { + event_list[eventName] = { + event: event_fun, + eventType: items, + }; + } + } + }); + return event_list; + }, + + /** + * @description 验证值是否存在 + * @param {String} value 内容/值 + * @param {String|Boolean} attr 属性 + * @param {String} type 属性 + */ + $verify: function (value, attr, type) { + if (!value) return ''; + if (type === true) return value ? ' ' + attr : ''; + if (type === 'style') return attr ? attr + ':' + value + ';' : value; + return attr ? ' ' + attr + '="' + value + '"' : ' ' + value; + }, + /** + * @description 验证form表单 + */ + $verify_form: function () { + var form_list = {}, + form = this.config.form, + form_value = this.$get_form_value(), + form_element = this.$get_form_element(true); + for (var key = 0; key < form.length; key++) { + var item = form[key]; + if (!Array.isArray(item.group)) item.group = [item.group]; + if (item.separate) continue; + for (var i = 0; i < item.group.length; i++) { + var items = item.group[i], + name = items.name; + if (items.type === 'help') continue; + if (typeof items.verify != 'undefined') { + var value = items.verify(form_value[name], form_element[name], items, true); + if (value === false && form_value[name] !== false) return false; + form_list[name] = value; + } else { + form_list[name] = typeof form_value[name] === 'undefined' && items.disabled ? $('[name="' + name + '"]').val() : form_value[name]; + } + } + } + return form_list; + }, + /** + * @description 提交内容,需要传入url + * @param {Object|Function} param 附加参数或回调函数 + * @param {Function} callback 回调 + */ + $submit: function (param, callback, tips) { + var form = this.$verify_form(); + if (typeof param === 'function') (tips = callback), (callback = param), (param = {}); + if (!form) return false; + form = $.extend(form, param); + if (typeof this.config.url == 'undefined') { + bt_tools.msg('Request Submission address cannot be empty!', false); + return false; + } + bt_tools.send( + { + url: this.config.url, + data: form, + }, + function (res) { + if (callback) { + callback(res, form); + } else { + bt_tools.msg(res); + } + }, + tips || 'Submit' + ); + }, + }; + return new ReaderForm(config); + }, + /** + * @description tab切换,支持三种模式 + * @param {object} config + * @return 当前实例对象 + */ + tab: function (config) { + var _that = this; + + function ReaderTab(config) { + this.config = config; + this.theme = this.config.theme || {}; + this.$load(); + } + + ReaderTab.prototype = { + type: 1, + theme_list: [ + { + content: 'tab-body', + nav: 'tab-nav', + body: 'tab-con', + active: 'on', + }, + { + content: 'bt-w-body', + nav: 'bt-w-menu', + body: 'bt-w-con', + }, + ], + random: bt.get_random(5), + $init: function () { + var that = this, + active = this.config.active, + config = that.config.list, + _theme = {}; + this.$event_bind(); + if (config[that.active].success) config[that.active].success(); + config[that.active]['init'] = true; + }, + $load: function () { + var that = this; + }, + $reader_content: function () { + var that = this, + _list = that.config.list, + _tab = '', + _tab_con = '', + _theme = that.theme, + config = that.config; + if (typeof that.active === 'undefined') that.active = 0; + if (!$.isEmptyObject(config.theme)) { + _theme = this.theme_list[that.active]; + $.each(config.theme, function (key, item) { + if (_theme[key]) _theme[key] += ' ' + item; + }); + that.theme = _theme; + } + if (config.type && $.isEmptyObject(config.theme)) this.theme = this.theme_list[that.active]; + $.each(_list, function (index, item) { + var active = that.active === index, + _active = _theme['active'] || 'active'; + _tab += '' + item.title + ''; + _tab_con += '
                                  ' + (active ? item.content : '') + '
                                  '; + }); + that.element = $( + '
                                  ' + + _tab + + '
                                  ' + + _tab_con + + '
                                  ' + ); + return that.element[0].outerHTML; + }, + /** + * @description 事件绑定 + * + */ + $event_bind: function () { + var that = this, + _theme = that.theme, + active = _theme['active'] || 'active'; + if (!that.el) that.element = $('#tab_' + that.random); + that.element.on('click', '.' + _theme['nav'].replace(/\s+/g, '.') + ' span', function () { + var index = $(this).index(), + config = that.config.list[index]; + $(this).addClass(active).siblings().removeClass(active); + $('#tab_' + that.random + ' .' + _theme['body'] + '>div:eq(' + index + ')') + .addClass(active) + .siblings() + .removeClass(active); + that.active = index; + if (!config.init) { + // console.log(_theme) + var contentItem = $('#tab_' + that.random + ' .' + _theme['body'] + '>div:eq(' + index + ')'); + contentItem.html(config.content); + if (config.success) config.success(contentItem); + config.init = true; + } + }); + }, + }; + return new ReaderTab(config); + }, + /** + * @description loading过渡 + * @param {*} title + * @param {*} is_icon + * @return void + */ + load: function (title) { + var random = bt.get_random(5), + layel = $( + '
                                  ' + + title + + ',please wait...
                                  ' + ), + mask = '', + loadT = ''; + $('body').append(layel); + var win = $(window), + msak = $('.layer-loading-mask'), + layel = $('#' + random); + layel.css({ top: (win.height() - 64) / 2, left: (win.width() - 320) / 2 }); + if (title === true) loadT = layer.load(); + return { + close: function () { + if (typeof loadT == 'number') { + layer.close(loadT); + } else { + $('body') + .find('#' + random + ',#layer-mask-' + random) + .remove(); + } + }, + }; + }, + /** + * @description 弹窗方法,有默认的参数和重构的参数 + * @param {object} config 和layer参数一致 + * @require 当前关闭弹窗方法 + */ + open: function (config) { + var _config = {}, + layerT = null, + form = null; + _config = $.extend({ type: 1, area: '640px', closeBtn: 2, btn: ['Yes', 'No'] }, config); + if (typeof _config.content == 'object') { + var param = _config.content; + form = bt_tools.form(param); + _config.success = function (layero, indexs) { + form.$event_bind(); + if (typeof config.success != 'undefined') config.success(layero, indexs); + }; + _config.yes = function (indexs, layero) { + var form_val = form.$verify_form(); + if (!form_val) return false; + if (typeof config.yes != 'undefined') { + var yes = config.yes.apply(form, [form_val, indexs, layero]); + if (!yes) return false; + } + }; + _config.content = form.$reader_content(); + } + layerT = layer.open(_config); + return { + close: function () { + layer.close(layerT); + }, + form: form, + }; + }, + /** + * @description 封装msg方法 + * @param {object|string} param1 配置参数,请求方法参数 + * @param {number} param2 图标ID + * @require 当前关闭弹窗方法 + */ + msg: function (param1, param2) { + var layerT = null, + msg = '', + config = {}; + if (typeof param1 === 'object') { + if (typeof param1.status === 'boolean') { + (msg = param1.msg), (config = { icon: param1.status ? 1 : 2 }); + if (!param1.status) config = $.extend(config, { time: !param2 ? 0 : 3000, closeBtn: 2, shade: 0.3 }); + } + } + if (typeof param1 === 'string') { + (msg = param1), + (config = { + icon: typeof param2 !== 'undefined' ? param2 : 1, + }); + } + layerT = layer.msg(msg, config); + return { + close: function () { + layer.close(layerT); + }, + }; + }, + /** + * @description 成功提示 + * @param {string} msg 信息 + */ + success: function (msg) { + this.msg({ msg: msg, status: true }); + }, + + /** + * @description 错误提示 + * @param {string} msg 信息 + */ + error: function (msg) { + this.msg({ msg: msg, status: false }); + }, + /** + * @description 请求封装 + * @param {string|object} conifg ajax配置参数/请求地址 + * @param {function|object} callback 回调函数/请求参数 + * @param {function} callback1 回调函数/可为空 + * @returns void 无 + */ + send: function (param1, param2, param3, param4, param5, param6) { + var params = {}, + success = null, + error = null, + config = [], + param_one = ''; + $.each(arguments, function (index, items) { + config.push([items, typeof items]); + }); + function diff_data(i) { + try { + success = config[i][1] == 'function' ? config[i][0] : null; + error = config[i + 1][1] == 'function' ? config[i + 1][0] : null; + } catch (error) {} + } + param_one = config[0]; + switch (param_one[1]) { + case 'string': + $.each(config, function (index, items) { + var value = items[0], + type = items[1]; + if (index > 1 && (type == 'boolean' || type == 'string' || type == 'object')) { + var arry = param_one[0].split('/'); + params['url'] = '/' + arry[0] + '?action=' + arry[1]; + if (type == 'object') { + params['load'] = value.load; + params['verify'] = value.verify; + if (value.plugin) params['url'] = '/plugin?action=a&name=' + arry[0] + '&s=' + arry[1]; + } else if (type == 'string') { + params['load'] = value; + } + return false; + } else { + params['url'] = param_one[0]; + } + }); + if (config[1][1] === 'object') { + params['data'] = config[1][0]; + diff_data(2); + } else { + diff_data(1); + } + break; + case 'object': + params['url'] = param_one[0].url; + params['data'] = param_one[0].data || {}; + $.each(config, function (index, items) { + var value = items[0], + type = items[1]; + if (index > 1 && (type == 'boolean' || type == 'string' || type == 'object')) { + switch (type) { + case 'object': + params['load'] = value.load; + params['verify'] = value.verify; + break; + case 'string': + params['load'] = value; + break; + } + return true; + } + }); + if (config[1][1] === 'object') { + params['data'] = config[1][0]; + diff_data(2); + } else { + diff_data(1); + } + break; + } + if (params.load) params.load = this.load(params.load); + $.ajax({ + type: params.type || 'POST', + url: params.url, + data: params.data || {}, + dataType: params.dataType || 'JSON', + complete: function (res) { + if (params.load) params.load.close(); + }, + success: function (res) { + if (typeof params.verify == 'boolean' && !params.verify) { + if (success) success(res); + return false; + } + if (typeof res === 'string') { + layer.msg(res, { + icon: 2, + time: 0, + closeBtn: 2, + }); + return false; + } + if (params.batch) { + if (success) success(res); + return false; + } + if (res.status === false && (res.hasOwnProperty('msg') || res.hasOwnProperty('error_msg'))) { + if (error) { + error(res); + } else { + bt_tools.msg({ status: res.status, msg: !res.hasOwnProperty('msg') ? res.error_msg : res.msg }); + } + return false; + } + + if (params.tips) { + bt_tools.msg(res); + } + if (success) success(res); + }, + }); + }, + + /** + * @description 命令行输入 + */ + command_line_output: function (config) { + var _that = this, + uuid = bt.get_random(15); + + /** + * @description 渲染 + * @param config + * @return {object} + * @constructor + */ + function ReaderCommand(config) { + var that = this; + for (var key in _that.commandConnectionPool) { + var item = _that.commandConnectionPool[key], + element = $(item.config.el); + if (config.shell === item.config.shell && element.length) { + item.el = element; + return item; + } + } + if (typeof config === 'undefined') config = {}; + this.config = $.extend({ route: '/sock_shell' }, config); + this.xterm_config = $.extend(this.xterm_config, this.config.xterm); + this.el = $(this.config.el); + this.open = config.open; + this.close = config.close; + this.message = config.message; + if (!this.config.hasOwnProperty('el')) { + _that.msg({ msg: '请输入选择器element,不可为空', status: false }); + return false; + } + if (!this.config.hasOwnProperty('shell')) { + _that.msg({ msg: '请输入命令,不可为空', status: false }); + return false; + } + if (this.config.hasOwnProperty('time')) { + setTimeout(function () { + that.close_connect(); + }, this.config.time); + } + this.init(); + } + + ReaderCommand.prototype = { + socket: null, //websocket 连接保持的对象 + socketToken: null, + timeout: 0, // 连接到期时间,为0代表永久有效 + monitor_interval: 2000, + element_detection: null, + uuid: uuid, + fragment: [], + error: 0, // 错误次数,用于监听元素内容是否还存在 + retry: 0, // 重试次数 + forceExit: false, // 强制断开连接 + /** + * @description 程序初始化 + */ + init: function () { + var oldUUID = bt.get_cookie('commandInputViewUUID'), + that = this; + if (!this.el[0]) { + if (this.error > 10) return false; + setTimeout(function () { + that.init(); + this.error++; + }, 2000); + return false; + } + this.error = 0; + if (this.el[0].localName !== 'pre') { + this.el.append('
                                  ');
                                  +					this.el = this.el.find('pre');
                                  +					this.config.el = this.config.el + ' pre';
                                  +				} else {
                                  +					this.el.addClass('command_output_pre');
                                  +				}
                                  +				if (Array.isArray(this.config.area)) {
                                  +					this.el.css({ width: this.config.area[0], height: this.config.area[1] });
                                  +				} else {
                                  +					this.el.css({ width: '100%', height: '100%' });
                                  +				}
                                  +				if (oldUUID && typeof _that.commandConnectionPool[oldUUID] != 'undefined') {
                                  +					_that.commandConnectionPool[oldUUID].close_connect();
                                  +					delete _that.commandConnectionPool[oldUUID];
                                  +				}
                                  +				bt.set_cookie('commandInputViewUUID', this.uuid);
                                  +				this.element_detection = setInterval(function () {
                                  +					if (!$(that.config.el).length) {
                                  +						clearInterval(that.element_detection);
                                  +						that.forceExit = true;
                                  +						that.close_connect();
                                  +					}
                                  +				}, 1 * 60 * 1000);
                                  +				this.set_full_screen();
                                  +				this.create_websocket_connect(this.config.route, this.config.shell);
                                  +				this.monitor_element();
                                  +			},
                                  +			/**
                                  +			 * @description 创建websocket连接
                                  +			 * @param {string} url websocket连接地址
                                  +			 * @param {string} shell 需要传递的命令
                                  +			 */
                                  +			create_websocket_connect: function (url, shell) {
                                  +				var that = this;
                                  +				this.socket = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + location.host + url);
                                  +				this.socket.addEventListener('open', function (ev) {
                                  +					if (!this.socketToken) {
                                  +						var _token = document.getElementById('request_token_head').getAttribute('token');
                                  +						this.socketToken = { 'x-http-token': _token };
                                  +					}
                                  +					this.send(JSON.stringify(this.socketToken));
                                  +					this.send(shell);
                                  +					if (that.open) that.open();
                                  +					that.retry = 0;
                                  +				});
                                  +				this.socket.addEventListener('close', function (ev) {
                                  +					if (!that.forceExit) {
                                  +						if (ev.code !== 1000 && that.retry <= 10) {
                                  +							that.socket = that.create_websocket_connect(that.config.route, that.config.shell);
                                  +							that.retry++;
                                  +						}
                                  +						if (that.close) that.close(ev);
                                  +					}
                                  +				});
                                  +				this.socket.addEventListener('message', function (ws_event) {
                                  +					var result = ws_event.data;
                                  +					if (!result) return;
                                  +					that.refresh_data(result);
                                  +					if (that.message) that.message(result);
                                  +				});
                                  +				return this.socket;
                                  +			},
                                  +
                                  +			/**
                                  +			 * @description 设置全屏视图
                                  +			 */
                                  +			set_full_screen: function () {
                                  +				// 1
                                  +			},
                                  +
                                  +			htmlEncodeByRegExp: function (str) {
                                  +				if (str.length == 0) return '';
                                  +				return str.replace(/&/g, '&').replace(//g, '>').replace(/ /g, ' ').replace(/\'/g, ''').replace(/\"/g, '"');
                                  +			},
                                  +
                                  +			/**
                                  +			 * @description 刷新Pre数据
                                  +			 * @param {object} data 需要插入的数据
                                  +			 */
                                  +			refresh_data: function (data) {
                                  +				var data = this.htmlEncodeByRegExp(data);
                                  +				this.fragment.push(data);
                                  +				if (this.fragment.length >= 300) {
                                  +					this.fragment.splice(0, 150);
                                  +					this.el.html(this.fragment.join(''));
                                  +				} else {
                                  +					this.el.append(data);
                                  +				}
                                  +				if (this.el.length > 0) {
                                  +					this.el.scrollTop(this.el[0].scrollHeight);
                                  +				}
                                  +			},
                                  +
                                  +			/**
                                  +			 * @description 监听元素状态,判断是否移除当前的ws连接
                                  +			 *
                                  +			 */
                                  +			monitor_element: function () {
                                  +				var that = this;
                                  +				this.monitor_interval = setInterval(function () {
                                  +					if (!that.el.length) {
                                  +						that.close_connect();
                                  +						clearInterval(that.monitor_interval);
                                  +					}
                                  +				}, that.config.monitorTime || 2000);
                                  +			},
                                  +
                                  +			/**
                                  +			 * @description 断开命令响应和websocket连接
                                  +			 */
                                  +			close_connect: function () {
                                  +				this.socket.send('');
                                  +				this.socket.close();
                                  +				delete _that.commandConnectionPool[this.uuid];
                                  +			},
                                  +		};
                                  +		this.commandConnectionPool[uuid] = new ReaderCommand(config);
                                  +		return this.commandConnectionPool[uuid];
                                  +	},
                                  +
                                  +	/**
                                  +	 * @description 清理验证提示样式
                                  +	 * @param {object} element  元素节点
                                  +	 */
                                  +	$clear_verify_tips: function (element) {
                                  +		element.removeClass('bt-border-error bt-border-sucess');
                                  +		layer.close('tips');
                                  +	},
                                  +
                                  +	/**
                                  +	 * @description 验证提示
                                  +	 * @param {object} element  元素节点
                                  +	 * @param {string} tips 警告提示
                                  +	 * @return void
                                  +	 */
                                  +	$verify_tips: function (element, tips, is_error) {
                                  +		if (typeof is_error === 'undefined') is_error = true;
                                  +		element.removeClass('bt-border-error bt-border-sucess').addClass(is_error ? 'bt-border-error' : 'bt-border-sucess');
                                  +		element.focus();
                                  +		layer.tips('' + tips + '', element, {
                                  +			tips: [1, is_error ? 'red' : '#20a53a'],
                                  +			time: 3000,
                                  +			area: element.width(),
                                  +		});
                                  +	},
                                  +	/**
                                  +	 * @description 验证值是否存在
                                  +	 * @param {String} value 内容/值
                                  +	 * @param {String|Boolean} attr 属性
                                  +	 * @param {String} type 属性
                                  +	 */
                                  +	$verify: function (value, attr, type) {
                                  +		if (!value) return '';
                                  +		if (type === true) return value ? ' ' + attr : '';
                                  +		if (type === 'style') return attr ? attr + ':' + value + ';' : value;
                                  +		return attr ? ' ' + attr + '="' + value + '"' : ' ' + value;
                                  +	},
                                  +
                                  +	/**
                                  +	 * @description 批量操作的结果
                                  +	 * @param {*} config
                                  +	 */
                                  +	$batch_success_table: function (config) {
                                  +		var _that = this,
                                  +			length = $(config.html).length;
                                  +		bt.open({
                                  +			type: 1,
                                  +			title: config.title,
                                  +			area: config.area || ['400px'],
                                  +			shadeClose: false,
                                  +			closeBtn: 2,
                                  +			content:
                                  +				config.content ||
                                  +				'
                                  ' + + config.title + + ' ' + + lan['public'].success + + '
                                  ' + + config.html + + '
                                  ' + + config.th + + '' + + lan['public'].result + + '
                                  ', + success: function () { + if (length > 4) _that.$fixed_table_thead('.fiexd_thead'); + }, + }); + }, + /** + * @description 固定表头 + * @param {string} el DOM选择器 + * @return void + */ + $fixed_table_thead: function (el) { + $(el).scroll(function () { + var scrollTop = this.scrollTop; + this.querySelector('thead').style.transform = 'translateY(' + scrollTop + 'px)'; + }); + }, + /** + * @description 插件视图设置 + * @param {object|string} layid dom元素或layer_id + * @param {object} config 插件宽度高度或其他配置 + */ + $piugin_view_set: function (layid, config) { + var element = $(typeof layid === 'string' ? '#layui-layer' + layid : layid).hide(), + win = $(window); + setTimeout(function () { + var width = config.width || element.width(), + height = config.height || element.height(); + element + .css( + $.extend(config, { + left: (win.width() - width) / 2, + top: (win.height() - height) / 2, + }) + ) + .addClass('custom_layer'); + }, 50); + setTimeout(function () { + element.show(); + }, 500); + }, +}; + +setTimeout(function () { + $.fn.serializeObject = function () { + var hasOwnProperty = Object.prototype.hasOwnProperty; + return this.serializeArray().reduce(function (data, pair) { + if (!hasOwnProperty.call(data, pair.name)) { + data[pair.name] = pair.value; + } + return data; + }, {}); + }; +}, 300); + +function arryCopy(arrys) { + var list = arrys.concat(), + arry = []; + for (var i = 0; i < list.length; i++) { + arry.push($.extend(true, {}, list[i])); + } + return arry; +} diff --git a/BTPanel/static/vite/oldjs/upload-drog.js b/BTPanel/static/vite/oldjs/upload-drog.js new file mode 100644 index 00000000..a436c53e --- /dev/null +++ b/BTPanel/static/vite/oldjs/upload-drog.js @@ -0,0 +1,958 @@ +'use strict'; + +/* + * @Description: 上传文件,三合一组件 + * @Version: 1.0 + * @Autor: chudong + * @Date: 2021-10-11 15:54:00 + * @LastEditors: chudong + * @LastEditTime: 2021-11-04 17:12:51 + */ +function UploadFile() { + this.uploadPath = ''; // 上传文件位置 + this.init = false; // 是否初始化 + this.compatible = true; // 是否兼容当前系统,默认全部兼容 + this.uploadElement = null; // 更新视图 + this.isUpload = true; // 是否可上传 + this.isGetFiles = false; // 是否可获取文件,如果当前拦截判断已经触发,则无法继续获取文件 + this.limit = { + size: 30 * 1024 * 1024 * 1024, + number: 1000, + }; + this.init_data(); // 初始化数据 + this.initialize_view(); // 初始化视图 + this.event_bind(); // 事件绑定 +} + +var uploadListhtml = + '
                                  • File nameFile sizeFile status
                                    '; + +// 是否显示内容 +function is_show(el, show) { + el.style.display = show ? 'block' : 'none'; +} + +UploadFile.prototype = { + /** + * @description 创建节点 + */ + createEl: function createEl(name) { + return document.createElement(name); + }, + + /** + * @description 获取节点 + */ + queryEl: function queryEl(el) { + return document.querySelector(el); + }, + + /** + * @description 获取所有节点 + */ + queryElAll: function queryElAll(el) { + return document.querySelectorAll(el); + }, + + /** + * @description 绑定事件 + */ + bind: function bind(el, type, fn) { + if (typeof el === 'string') el = this.queryElAll(el); + if (typeof el.length !== 'number') el = [el]; + if (typeof type === 'function') (fn = type), (type = 'click'); + for (var i = 0; i < el.length; i++) { + var item = el[i]; + (function (item) { + item.addEventListener( + type || 'click', + function (ev) { + var index = [].indexOf.call(el, item); + fn.call(item, ev, index); + }, + false + ); + })(item); + } + }, + + /** + * @description 初始化数据 + */ + init_data: function init_data() { + this.uploadStatus = 0; // 上传状态 0:等待上传 1:上传成功 2:上传中 + this.uploadLimitSize = 1024 * 1024 * 2; // 上传限制字节 + this.uploadList = []; // 上传列表 + this.isUpload = true; + this.uploadTime = { + endTime: 0, + startTime: 0, + }; + this.uploadInfo = { + // 提示信息,用于通知当前上传状态 + estimatedTime: 0, // 上传预计耗时,时间戳 + uploadedSize: 0, // 已上传文件大小 + speedInterval: null, // 平局速度定时器 + speedAverage: 0, //平均速度 + uploadTime: 0, // 上传总耗时 + fileSize: 0, // 文件上传字节 + startTime: 0, // 文件上传开始时间 + endTime: 0, // 文件上传结束时间 + }; + + this.fileList = []; // 文件列表 + this.fileTotalSize = 0; // 全部文件大小 + this.fileTotalNumber = 0; //全部文件数量 + + this.uploadInterval = null; + this.uploadCycleSize = []; // 上传周期的字段 + + this.speedLastTime = 0; + this.timerSpeed = 0; + + this.uploadError = 0; + }, + + /** + * @description 初始化上传路径 + */ + init_upload_path: function (path) { + this.uploadPath = path || bt.get_cookie('Path'); // 上传目录 + }, + + /** + * @description 视图初始化 + */ + initialize_view: function () { + var title = this.createEl('span'); + this.uploadElement = this.createEl('div'); + this.uploadElement.setAttribute( + 'style', + 'position:fixed;top:0;left:0;right:0;bottom:0; background:rgba(255,255,255,0.6);border:3px #ccc dashed;z-index:99999999;color:#999;font-size:40px;text-align:center;overflow:hidden;' + ); + this.uploadElement.id = 'uploadView'; + is_show(this.uploadElement, false); + title.setAttribute('style', 'position: fixed;top: 50%;left: 50%;margin-left: -200px;margin-top: -40px;z-index:99999998'); + title.innerText = 'Upload files to the current directory'; + this.uploadElement.appendChild(title); + document.querySelector('body').appendChild(this.uploadElement); + }, + + /** + * @description 绑定事件 + */ + event_bind: function event_bind() { + var _this = this; + + // 进入目标 + this.bind(document, 'dragenter', function (ev) { + console.log(ev); + _this.file_drag_hover(ev); + }); + + // 在放置目标上 + this.bind(this.uploadElement, 'dragover', function (ev) { + _this.file_drag_hover(ev); + }); + + // 离开放置目录 + this.bind(this.uploadElement, 'dragleave', function (ev) { + if (ev.path[0].id == 'uploadView') { + if (ev.screenX == 0 || ev.screenY == 0) { + ev.path[0].style.display = 'none'; + } + return false; + } + // _this.file_drag_hover(ev); + }); + + // 放置目标 + this.bind(this.uploadElement, 'drop', function (ev) { + _this.isGetFiles = true; + if (_this.uploadStatus === 2) { + layer.msg('Uploading files, please wait...', { + icon: 0, + }); + _this.file_drag_hover(ev); + return false; + } + var path = $('#fileInputPath').attr('data-path'); + $('.againUpload').click(); + _this.init_upload_path(path); + _this.upload_layer(); + _this.isUpload = true; + _this.file_select_handler(ev); + }); + }, + + /** + * @description 文件拖拽悬浮状态 + */ + file_drag_hover: function file_drag_hover(event) { + try { + if (event.dataTransfer.items[0].kind == 'string') return false; + } catch (error) {} + is_show(this.uploadElement, !(event.type === 'dragleave' || event.type === 'drop')); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * @description 上传弹窗 + */ + upload_layer: function (list) { + var _this2 = this; + + if (typeof list === 'undefined') list = []; + if (this.layer) return false; + var layerMax = null, + layerShade = null, + uploadPath = this.uploadPath || bt.get_cookie('Path'); + this.layer = layer.open({ + type: 1, + closeBtn: 1, + maxmin: true, + area: ['650px', '605px'], + title: 'Upload files to [' + uploadPath + '] --- Support breakpoint renewal', + skin: 'file_dir_uploads', + content: + '
                                    \n
                                    \n
                                    \n \n \n \n
                                    \n
                                    \n
                                    \n Total process , uploading ,\n Upload fail \n Speed Getting\n Expect time Getting\n \n
                                    \n
                                    \n
                                    ' + + (list.length > 0 ? uploadListhtml : 'Please drag the file here') + + '
                                    \n
                                    \n
                                    \n \n \n
                                    ', + success: function success(layers, indexs) { + layerMax = _this2.queryEl('.file_dir_uploads').querySelector('.layui-layer-max'); + layerShade = document.querySelector('.layui-layer-shade'); + layerMax.style.display = 'none'; + var cancelUpload = _this2.queryEl('.cancelUpload'), + startUpload = _this2.queryEl('.startUpload'), + uploadFileBtn = _this2.queryEl('.upload_file_btn'), + dropdownItem = _this2.queryElAll('.dropdown-menu li'); + // 下拉选项 + _this2.bind(dropdownItem, function (ev) { + var type = ev.target.dataset.type; + if (type === 'file') { + _this2.queryEl('.upload_file_input').removeAttribute('webkitdirectory'); + } else if (type === 'dir') { + _this2.queryEl('.upload_file_input').setAttribute('webkitdirectory', ''); + } + _this2.queryEl('.upload_file_input').click(); + }); + + _this2.bind('.empty-record', function (ev) { + var $li = _this2.queryElAll('.dropUpLoadFile li'); + if ($li.length <= 0) { + layer.msg('Please select file!', { icon: 0 }); + } else { + layer.confirm( + 'Do you want to clear the upload list?', + { + btn: ['Confirm', 'Cancel'], + title: 'clear the upload list', + icon: 0, + }, + function (indexs) { + _this2.empty_upload_list(); + layer.close(indexs); + } + ); + } + }); + + function create_upload_input() { + // 选择文件或文件夹 + var uploadFileInput = _this2.createEl('input'), + uploadBtnGroud = _this2.queryEl('.upload_btn_groud'); + uploadFileInput.setAttribute('type', 'file'); + uploadFileInput.setAttribute('multiple', 'multiple'); + uploadFileInput.style.display = 'none'; + uploadFileInput.classList.add('upload_file_input'); + uploadBtnGroud.appendChild(uploadFileInput); + _this2.bind(uploadFileInput, 'change', function (ev) { + _this2.isUpload = true; + _this2.isGetFiles = true; + var files = ev.target.files; + // console.log(files) + for (var i = 0; i < files.length; i++) { + if (!_this2.file_upload_limit(files[i])) return false; + } + if (!_this2.isGetFiles) return false; + _this2.render_file_list(_this2.fileList); + uploadBtnGroud.removeChild(uploadFileInput); + create_upload_input(); + }); + } + + create_upload_input(); + + // 点击上传文件按钮 + _this2.bind(uploadFileBtn, function (ev) { + _this2.queryEl('.upload_file_input').click(); + }); + + // 关闭文件视图 + _this2.bind(cancelUpload, function (ev) { + _this2.cancel_upload(ev); + }); + // 开始上传 + _this2.bind(startUpload, function (e) { + var btn = e.target; + if (btn.classList.contains('againUpload')) { + $('.ico-tips-close').click(); + btn.innerText = 'Upload'; + btn.classList.remove('againUpload'); + btn.classList.add('startUpload'); + } else { + if (_this2.fileList.length === 0) { + layer.msg('Please select file!', { icon: 0 }); + return false; + } + _this2.upload_file(); + } + }); + }, + min: function () { + layerMax.style.display = ''; + layerShade.style.display = 'none'; + }, + restore: function () { + layerMax.style.display = 'none'; + layerShade.style.display = ''; + }, + cancel: function () { + _this2.cancel_upload(); + return false; + }, + }); + }, + + // 清空上传列表 + empty_upload_list: function () { + var dropUpLoadFile = this.queryEl('.dropUpLoadFile'); + var $li = this.queryElAll('.dropUpLoadFile li'); + for (var i = 0; i < $li.length; i++) { + dropUpLoadFile.removeChild($li[i]); + } + this.init_data(); + var $head = this.queryEl('.dropUpLoadFileHead'); + if ($head) $head.removeAttribute('style'); + }, + + /** + * @description 获取实时时间 + */ + get_time_real: function get_time_real() { + return new Date().getTime(); + }, + + /** + * @description 上传前检查文件是否存在 + */ + upload_file_exists: function (config) { + var _this = this; + var filename = config.path + config.name; + var files = this.uploaded_files; + if (!files) { + this.uploaded_files = []; + files = []; + } + var data = { + filename: filename, + }; + // 判断是否上传过该文件名 + var isUploaded = false; + for (var i = 0; i < files.length; i++) { + if (files[i] === config.name) { + isUploaded = true; + break; + } + } + // 上传过就直接跳过 + if (isUploaded) { + config.success(); + return; + } + this.uploaded_files.push(config.name); + bt.send('upload_file_exists', 'files/upload_file_exists', data, function (res) { + if (res.status) { + var is_open = _this.is_open_cover_layer; + if (is_open === 0) { + bt.open({ + type: 1, + area: '400px', + title: 'Upload file to [' + config.path.substr(0, config.path.length - 1) + ']', + btn: ['Confirm', 'Skip'], + content: + '\ +
                                    \ +
                                    \ + \ +
                                    A file with the same name [' + + config.name + + '] is detected, do you want to overwrite it?
                                    \ +
                                    \ +
                                    \ +
                                    \ + \ + \ +
                                    \ +
                                    \ +
                                    \ + ', + success: function (layero) { + var fileList = _this.fileList; + if (fileList.length <= 1) { + layero.find('.details').remove(); + } + }, + cancel: function () { + config.error(); + }, + yes: function (index) { + var checked = $('#all_operation').is(':checked'); + if (checked) { + _this.is_open_cover_layer = 1; + } + config.success(); + layer.close(index); + }, + btn2: function (index) { + var checked = $('#all_operation').is(':checked'); + if (checked) { + _this.is_open_cover_layer = 2; + } + config.error(); + layer.close(index); + }, + }); + } else if (is_open === 1) { + config.success(); + } else { + config.error(); + } + } else { + config.success(); + } + }); + }, + + /** + * @description 上传文件 + */ + upload_file: function (fileStart, index) { + var _this3 = this; + this.uploadPath = this.uploadPath || bt.get_cookie('Path'); + if (this.fileList.length === 0) return false; + // 开始上传 + if (fileStart == undefined && this.uploadList.length == 0) { + (fileStart = 0), (index = 0); + this.is_open_cover_layer = 0; + this.uploadStatus = 2; + this.uploaded_files = []; // 上传过的文件 + this.uploadCycleSize = []; // 上传速度匹配 + this.uploadTime.startTime = this.get_time_real(); // 设置上传开始时间 + var startUpload = this.queryEl('.startUpload'); + startUpload.setAttribute('disabled', 'disabled'); + startUpload.innerText = 'Uploading'; + } + // 结束上传 + if (this.fileList.length === index) { + clearTimeout(this.uploadInterval); + this.uploadStatus = 1; + this.uploadTime.endTime = this.get_time_real(); // 设置上传开始时间 + this.set_upload_view(); + this.init_data(); + bt_file.reader_file_list({ path: this.uploadPath }); + return false; + } + + // 创建文件对象和切割文件 + var item = this.fileList[index], + fileEnd = ''; + if (item == undefined) return false; + + // 检测文件是否存在 + var f_path = this.uploadPath + item.path + '/'; + f_path = f_path.replace(/\/\//g, '/'); + this.upload_file_exists({ + name: item.name, + path: f_path, + error: function () { + $('.dropUpLoadFile li').eq(index).find('.fileStatus .upload_info').html('File already exists'); + _this3.upload_file(0, ++index); + }, + success: function () { + // 设置切割文件时间段 + var uploadedSize = _this3.uploadInfo.uploadedSize; + _this3.uploadInterval = setInterval(function () { + if (_this3.uploadCycleSize.length === 4) _this3.uploadCycleSize.splice(0, 1); + _this3.uploadCycleSize.push(Math.abs(_this3.uploadInfo.uploadedSize - uploadedSize)); + }, 1000); + + // 渲染上传时间和上传进度 + _this3.uploadInfo.endTime = _this3.get_time_real(); + _this3.reander_timer_speed(); + _this3.uploadInfo.startTime = _this3.get_time_real(); + + // 获取上传速度 + var speed = _this3.get_update_speed(), + // 获取上传速度 + limitSize = _this3.uploadLimitSize; + // 判断速度是否操作阈值,超过阀值后,采用倍速的方案,最大只能支持4,8MB + var maxDouble = Math.floor(speed / _this3.uploadLimitSize); + if (maxDouble && index > 1) limitSize = (maxDouble > 4 ? 4 : maxDouble) * limitSize; + + // 实时反馈 + if (fileStart == 0) { + _this3.uploadInfo.startTime = _this3.get_time_real(); + item = $.extend(item, { + percent: '0%', + upload: 2, + upload_size: '0B', + }); + } + _this3.set_upload_view(index, item); + + fileEnd = Math.min(item.file.size, fileStart + limitSize); + _this3.uploadInfo.fileSize = fileEnd - fileStart; + + var form = new FormData(); + form.append('f_path', f_path); + form.append('f_name', item.name); + form.append('f_size', item.file.size); + form.append('f_start', fileStart); + form.append('blob', item.file.slice(fileStart, fileEnd)); + + // 发送请求 + $.ajax({ + url: '/files?action=upload', + type: 'POST', + data: form, + async: true, + processData: false, + contentType: false, + success: function success(rdata) { + // 判断是否为数字 + if (typeof rdata === 'number') { + _this3.set_upload_view( + index, + $.extend(item, { + percent: ((rdata / item.file.size) * 100).toFixed(2) + '%', + upload: 2, + upload_size: bt.format_size(rdata), + }) + ); + + // 判断是否为文件结束,已上传文件大小 + if (fileEnd != rdata) { + _this3.uploadInfo.uploadedSize += rdata; + } else { + _this3.uploadInfo.uploadedSize += parseInt(fileEnd - fileStart); + } + + // console.log(rdata, index); + + // 继续上传文件 + _this3.upload_file(rdata, index); + } else { + // 请求状态,判断文件是否上传成功 + if (rdata.status) { + _this3.uploadInfo.endTime = _this3.get_time_real(); + _this3.uploadInfo.uploadedSize += parseInt(fileEnd - fileStart); + _this3.set_upload_view( + index, + $.extend(item, { + upload: 1, + upload_size: item.size, + }) + ); + } else { + _this3.set_upload_view( + index, + $.extend(item, { + upload: -1, + errorMsg: rdata.msg, + }) + ); + _this3.uploadError++; + } + _this3.upload_file(0, ++index); + } + // 实时更新文件上传状态 + }, + error: function (e) { + if (_this3.fileList[index].req_error === undefined) _this3.fileList[index].req_error = 1; + if (_this3.fileList[index].req_error > 2) { + _this3.set_upload_view( + index, + $.extend(_this3.fileList[index], { + upload: -1, + errorMsg: e.statusText == 'error' ? lan.public.network_err : e.statusText, + }) + ); + _this3.uploadError++; + _this3.upload_file(fileStart, (index += 1)); + return false; + } + _this3.fileList[index].req_error += 1; + _this3.upload_file(fileStart, index); + }, + }); + }, + }); + }, + + /** + * @description 设置上传视图 + */ + set_upload_view: function set_upload_view(index, config) { + var _this4 = this; + if (typeof index === 'undefined') { + var file_upload_info = this.queryEl('.file_upload_info'), + time = this.get_time_real(), + s_peed = this.to_size(this.uploadInfo.uploadedSize / ((time - this.uploadTime.startTime) / 1000)); + file_upload_info.innerHTML = + 'Uploading ' + + this.uploadList.length + + ' file(s), ' + + (this.uploadError ? 'Fail ' + this.uploadError + ' file(s), ' : '') + + 'time: ' + + this.diff_time(this.uploadTime.startTime, time) + + ', speed: ' + + s_peed + + '/s'; + this.bind(file_upload_info.querySelector('.ico-tips-close'), function (ev) { + var parent = this.parentNode.parentNode; + parent.querySelector('.btn-group').classList.remove('hide'); + parent.querySelector('.file_upload_info').classList.add('hide'); + _this4.empty_upload_list(); + }); + var startUpload = this.queryEl('.startUpload'); + startUpload.removeAttribute('disabled'); + startUpload.innerText = 'Upload again'; + startUpload.classList.remove('startUpload'); + startUpload.classList.add('againUpload'); + _this4.init_data(); + return false; + } + try { + var item = document.querySelectorAll('.dropUpLoadFile li')[index]; + var file_info = this.queryEl('.file_upload_info'); + + if (file_info.querySelectorAll('.uploadProgress').length === 0) { + file_info.innerHTML = + '\n Total process ,\n uploading , \n Upload fail \n Speed Getting\n Expect time Getting'; + } + + var file_info_parent = file_info.parentElement; + file_info_parent.querySelector('.btn-group').classList.add('hide'); + file_info_parent.querySelector('.file_upload_info').classList.remove('hide'); + + var file_info_error = file_info.querySelector('.uploadError'); + file_info_error.innerText = '(' + this.uploadError + '份)'; + if (this.uploadError > 0) file_info_error.parentElement.style.display = 'block'; + + if (config.upload === 1 || config.upload === -1) { + this.fileList[index].is_upload = true; + this.uploadList.push(this.fileList[index]); + item.querySelector('.fileLoading').setAttribute('style', 'width:100%;opacity:.5;background:' + (config.upload == -1 ? '#ffadad' : '#20a53a21')); + item.querySelector('.filesize').innerText = config.size; + item.querySelector('.fileStatus').innerHTML = this.is_upload_status( + config.upload, + config.upload === 1 ? '(time:' + this.diff_time(this.uploadTime.startTime, this.uploadTime.endTime) + ')' : config.errorMsg + ); + var dropUpLoadFile = this.queryEl('.dropUpLoadFile'); + if (this.uploadList.length === 1) dropUpLoadFile.scrollTop = 0; + if (this.uploadList.length > 1) dropUpLoadFile.scrollTop += 45.5; + } else { + item.querySelector('.fileLoading').setAttribute('style', 'width:' + config.percent); + item.querySelector('.filesize').innerText = config.upload_size + '/' + config.size; + item.querySelector('.fileStatus').innerHTML = this.is_upload_status(config.upload, '(' + config.percent + ')'); + } + + file_info.querySelector('.uploadNumber').innerText = '(' + this.uploadList.length + '/' + this.fileList.length + ')'; + file_info.querySelector('.uploadProgress').innerText = ((this.uploadInfo.uploadedSize / this.fileTotalSize) * 100).toFixed(2) + '%'; + } catch (e) { + console.log(e); + } + }, + + /** + * @description 上传状态 + * @param {*} status + * @param {*} val + * @returns + */ + is_upload_status: function is_upload_status(status, val) { + if (val === undefined) val = ''; + switch (status) { + case -1: + return 'Fail' + (val != '' ? ', ' + val : '') + ''; + case 0: + return 'Waiting to upload'; + case 1: + return 'Uploaded'; + case 2: + return 'Uploading ' + val + ''; + case 3: + return 'Stoped'; + } + }, + + /** + * @description 取10秒内上传平均值 + */ + get_update_speed: function get_update_speed() { + var sum = 0; + for (var i = 0; i < this.uploadCycleSize.length; i++) { + sum += this.uploadCycleSize[i]; + } + var content = sum / this.uploadCycleSize.length; + return isNaN(content) ? 0 : content; + }, + + // 渲染上传速度 + reander_timer_speed: function reander_timer_speed() { + var done_time = new Date().getTime(); + if (done_time - this.speedLastTime > 1000) { + var s_time = (this.uploadInfo.endTime - this.uploadInfo.startTime) / 1000; + this.timerSpeed = (this.uploadInfo.fileSize / s_time).toFixed(2); + if (this.timerSpeed < 2) return; + this.queryEl('.file_upload_info').querySelector('.uploadSpeed').innerText = bt.format_size(isNaN(this.timerSpeed) ? 0 : this.timerSpeed) + '/s'; + var estimateTime = this.time(parseInt(((this.fileTotalSize - this.uploadInfo.uploadedSize) / this.timerSpeed) * 1000)); + if (!isNaN(this.timerSpeed)) this.queryEl('.file_upload_info').querySelector('.uploadEstimate').innerText = estimateTime.indexOf('NaN') == -1 ? estimateTime : '0 sec'; + this.speedLastTime = done_time; + } + }, + to_size: function to_size(a) { + var d = [' B', ' KB', ' MB', ' GB', ' TB', ' PB']; + var e = 1024; + for (var b = 0; b < d.length; b += 1) { + if (a < e) { + var num = a.toFixed(2) + d[b]; + return !isNaN(a.toFixed(2)) && typeof num != 'undefined' ? num : '0B'; + } + a /= e; + } + }, + time: function time(date) { + var hours = Math.floor(date / (60 * 60 * 1000)); + var minutes = Math.floor(date / (60 * 1000)); + var seconds = parseInt((date % (60 * 1000)) / 1000); + var result = seconds + 'sec'; + if (minutes > 0) { + result = minutes + 'min' + seconds + 'sec'; + } + if (hours > 0) { + result = hours + 'hour' + Math.floor((date - hours * (60 * 60 * 1000)) / (60 * 1000)) + 'min'; + } + return result; + }, + diff_time: function diff_time(start_date, end_date) { + if (typeof start_date !== 'number') start_date = start_date.getTime(); + if (typeof end_date !== 'number') end_date = end_date.getTime(); + var diff = end_date - start_date, + minutes = Math.floor(diff / (60 * 1000)), + leave3 = diff % (60 * 1000), + seconds = leave3 / 1000, + result = seconds.toFixed(minutes > 0 ? 0 : 2) + 'sec'; + if (minutes > 0) { + result = minutes + 'min' + seconds.toFixed(0) + 'sec'; + } + return result; + }, + + /** + * @description 取消上传 + */ + cancel_upload: function cancel_upload(index) { + var _this4 = this; + if (this.uploadStatus === 2) { + layer.confirm( + 'Do you want to cancel the upload of files? It need to delete the uploaded files manually. Continue?', + { + title: 'Cancel file upload', + icon: 0, + }, + function (indexs) { + _this4.init_data(); + _this4.cancel_upload_layer(); + layer.close(indexs); + } + ); + } else { + _this4.init_data(); + _this4.cancel_upload_layer(); + } + }, + + /** + * @description 关闭上传弹窗 + */ + cancel_upload_layer: function cancel_upload_layer() { + layer.close(this.layer); + this.layer = false; + this.init_data(); + }, + + /** + * @description 渲染列表 + * + */ + render_file_list: function (list) { + var _this5 = this; + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i], + name = ((item.path || '/') + '/' + item.name).replace(/\/\//g, '/'); + html += + '
                                  • ' + + name + + '\n ' + + item.size + + '\n Waiting to upload\n
                                    \n
                                  • '; + this.uploadInfo.fileSize += item.file.size; + } + var upload_file_body = this.queryEl('.upload_file_body'); + upload_file_body.className = 'upload_file_body'; + upload_file_body.innerHTML = uploadListhtml; + this.queryEl('.upload_file_gourp').className = 'upload_file_gourp'; + this.queryEl('.dropUpLoadFile').innerHTML = html; + var dropUpLoadFileHead = this.queryEl('.dropUpLoadFileHead'); + if (list.length <= 10) { + dropUpLoadFileHead.removeAttribute('style'); + } else { + dropUpLoadFileHead.style.paddingRight = bt_file.upload_file_body_width + 'px'; + dropUpLoadFileHead.style.boxShadow = '0 5px 2px -3px #ececec'; + } + var cancelBtn = this.queryElAll('.cancel-btn'); + this.bind(cancelBtn, function (ev, index) { + _this5.fileTotalSize = _this5.fileTotalSize - _this5.fileList[index].file.size; + _this5.fileTotalNumber = _this5.fileTotalNumber - 1; + _this5.fileList.splice(index, 1); + _this5.queryElAll('.dropUpLoadFile li')[index].remove(); + }); + }, + + /** + * @description 文件上传限制 + * @param {Object} e 文件对象 + */ + file_upload_limit: function (e, path) { + if (!this.isGetFiles) return false; + var extName = e.name.split('.'); + path = path || e.webkitRelativePath; + var paths = path.split('/'); + path = ('/' + paths.slice(0, paths.length - 1).join('/')).replace('//', '/'); + this.fileList.push({ + file: e, + path: path, + name: e.name, + size: bt.format_size(e.size), + type: extName.length > 1 ? extName[extName.length - 1] : 'txt', + status: 0, + }); + this.fileTotalSize += e.size; + this.fileTotalNumber++; + if (this.fileTotalNumber >= this.limit.number) { + layer.msg('The number of files has exceeded the file upload limit ' + this.limit.number + ', please zip the folder and try again!'); + this.init_data(); + this.isGetFiles = false; // 停止文件内容获取 + this.empty_upload_list(); + return false; + } + if (this.fileTotalSize >= this.limit.size) { + layer.msg('The file size has exceeded the file upload' + bt.format_size(e.size) + ' limit, please use tools such as SFTP/FTP to upload the file!'); + this.init_data(); + this.isGetFiles = false; // 停止文件内容获取 + this.empty_upload_list(); + return false; + } + return true; + }, + + /** + * @description 文件夹文件内容递归 + * @param {object} item 文件对象 + */ + traverse_file_tree: function (item) { + var _this6 = this; + + var path = item.fullPath || ''; + if (item.isFile) { + item.file(function (e) { + _this6.file_upload_limit(e, path); + }); + clearTimeout(this.timeNumber); + this.timeNumber = setTimeout(function () { + _this6.load.close(); + if (_this6.isUpload && _this6.isGetFiles) { + _this6.render_file_list(_this6.fileList); + } else { + var layers = _this6.layer; + _this6.init_data(); + _this6.layers = layers; + } + }, 10); + } else if (item.isDirectory) { + var dirReader = item.createReader(); + var fnReadEntries = function (entries) { + [].forEach.call(entries, function (e) { + if (!_this6.isUpload) return false; + _this6.traverse_file_tree(e); + }); + if (entries.length > 0) { + dirReader.readEntries(fnReadEntries); + } + }; + dirReader.readEntries(fnReadEntries); + setTimeout(function () { + if (_this6.fileList.length === 0 && _this6.isGetFiles) { + layer.msg('Drag and drop upload folder content is empty'); + } + }, 500); + } + }, + + /** + * @description 文件选择处理程序 + * @param {object} ev 事件 + */ + file_select_handler: function (ev) { + var _this7 = this; + this.file_drag_hover(ev); + this.load = bt.load('Getting file information, please wait...'); + this.timeNumber = 0; + if (ev.target.files) { + var items = ev.target.files; + [].forEach.call(items, function (item) { + _this7.traverse_file_tree(item); + }); + } else if (ev.dataTransfer.items) { + var items = ev.dataTransfer.items; + [].forEach.call(items, function (ev) { + var getAsEntry = ev.webkitGetAsEntry || ev.getAsEntry; + var item = getAsEntry.call(ev); + if (item) { + if (!_this7.isUpload) return false; + _this7.traverse_file_tree(item); + } + }); + } + }, +}; + +var uploadFiles = new UploadFile(); diff --git a/BTPanel/static/vite/oldjs/xterm.js b/BTPanel/static/vite/oldjs/xterm.js new file mode 100644 index 00000000..e7251c08 --- /dev/null +++ b/BTPanel/static/vite/oldjs/xterm.js @@ -0,0 +1,7 @@ +!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var r=t();for(var i in r)("object"==typeof exports?exports:e)[i]=r[i]}}(window,(function(){return function(e){var t={};function r(i){if(t[i])return t[i].exports;var n=t[i]={i:i,l:!1,exports:{}};return e[i].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,i){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(r.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)r.d(i,n,function(t){return e[t]}.bind(null,n));return i},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=34)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.forwardEvent=t.EventEmitter=void 0;var i=function(){function e(){this._listeners=[],this._disposed=!1}return Object.defineProperty(e.prototype,"event",{get:function(){var e=this;return this._event||(this._event=function(t){return e._listeners.push(t),{dispose:function(){if(!e._disposed)for(var r=0;r>22},t.prototype.getChars=function(){return 2097152&this.content?this.combinedData:2097151&this.content?o.stringFromCodePoint(2097151&this.content):""},t.prototype.getCode=function(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content},t.prototype.setFromCharData=function(e){this.fg=e[s.CHAR_DATA_ATTR_INDEX],this.bg=0;var t=!1;if(e[s.CHAR_DATA_CHAR_INDEX].length>2)t=!0;else if(2===e[s.CHAR_DATA_CHAR_INDEX].length){var r=e[s.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=r&&r<=56319){var i=e[s.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=i&&i<=57343?this.content=1024*(r-55296)+i-56320+65536|e[s.CHAR_DATA_WIDTH_INDEX]<<22:t=!0}else t=!0}else this.content=e[s.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|e[s.CHAR_DATA_WIDTH_INDEX]<<22;t&&(this.combinedData=e[s.CHAR_DATA_CHAR_INDEX],this.content=2097152|e[s.CHAR_DATA_WIDTH_INDEX]<<22)},t.prototype.getAsCharData=function(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]},t}(a.AttributeData);t.CellData=c},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ISoundService=t.ISelectionService=t.IRenderService=t.IMouseService=t.ICoreBrowserService=t.ICharSizeService=void 0;var i=r(14);t.ICharSizeService=i.createDecorator("CharSizeService"),t.ICoreBrowserService=i.createDecorator("CoreBrowserService"),t.IMouseService=i.createDecorator("MouseService"),t.IRenderService=i.createDecorator("RenderService"),t.ISelectionService=i.createDecorator("SelectionService"),t.ISoundService=i.createDecorator("SoundService")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ExtendedAttrs=t.AttributeData=void 0;var i=function(){function e(){this.fg=0,this.bg=0,this.extended=new n}return e.toColorRGB=function(e){return[e>>>16&255,e>>>8&255,255&e]},e.fromColorRGB=function(e){return(255&e[0])<<16|(255&e[1])<<8|255&e[2]},e.prototype.clone=function(){var t=new e;return t.fg=this.fg,t.bg=this.bg,t.extended=this.extended.clone(),t},e.prototype.isInverse=function(){return 67108864&this.fg},e.prototype.isBold=function(){return 134217728&this.fg},e.prototype.isUnderline=function(){return 268435456&this.fg},e.prototype.isBlink=function(){return 536870912&this.fg},e.prototype.isInvisible=function(){return 1073741824&this.fg},e.prototype.isItalic=function(){return 67108864&this.bg},e.prototype.isDim=function(){return 134217728&this.bg},e.prototype.getFgColorMode=function(){return 50331648&this.fg},e.prototype.getBgColorMode=function(){return 50331648&this.bg},e.prototype.isFgRGB=function(){return 50331648==(50331648&this.fg)},e.prototype.isBgRGB=function(){return 50331648==(50331648&this.bg)},e.prototype.isFgPalette=function(){return 16777216==(50331648&this.fg)||33554432==(50331648&this.fg)},e.prototype.isBgPalette=function(){return 16777216==(50331648&this.bg)||33554432==(50331648&this.bg)},e.prototype.isFgDefault=function(){return 0==(50331648&this.fg)},e.prototype.isBgDefault=function(){return 0==(50331648&this.bg)},e.prototype.isAttributeDefault=function(){return 0===this.fg&&0===this.bg},e.prototype.getFgColor=function(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}},e.prototype.getBgColor=function(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}},e.prototype.hasExtendedAttrs=function(){return 268435456&this.bg},e.prototype.updateExtended=function(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456},e.prototype.getUnderlineColor=function(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()},e.prototype.getUnderlineColorMode=function(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()},e.prototype.isUnderlineColorRGB=function(){return 268435456&this.bg&&~this.extended.underlineColor?50331648==(50331648&this.extended.underlineColor):this.isFgRGB()},e.prototype.isUnderlineColorPalette=function(){return 268435456&this.bg&&~this.extended.underlineColor?16777216==(50331648&this.extended.underlineColor)||33554432==(50331648&this.extended.underlineColor):this.isFgPalette()},e.prototype.isUnderlineColorDefault=function(){return 268435456&this.bg&&~this.extended.underlineColor?0==(50331648&this.extended.underlineColor):this.isFgDefault()},e.prototype.getUnderlineStyle=function(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0},e}();t.AttributeData=i;var n=function(){function e(e,t){void 0===e&&(e=0),void 0===t&&(t=-1),this.underlineStyle=e,this.underlineColor=t}return e.prototype.clone=function(){return new e(this.underlineStyle,this.underlineColor)},e.prototype.isEmpty=function(){return 0===this.underlineStyle},e}();t.ExtendedAttrs=n},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addDisposableDomListener=void 0,t.addDisposableDomListener=function(e,t,r,i){e.addEventListener(t,r,i);var n=!1;return{dispose:function(){n||(n=!0,e.removeEventListener(t,r,i))}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Utf8ToUtf32=t.StringToUtf32=t.utf32ToString=t.stringFromCodePoint=void 0,t.stringFromCodePoint=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10))+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)},t.utf32ToString=function(e,t,r){void 0===t&&(t=0),void 0===r&&(r=e.length);for(var i="",n=t;n65535?(o-=65536,i+=String.fromCharCode(55296+(o>>10))+String.fromCharCode(o%1024+56320)):i+=String.fromCharCode(o)}return i};var i=function(){function e(){this._interim=0}return e.prototype.clear=function(){this._interim=0},e.prototype.decode=function(e,t){var r=e.length;if(!r)return 0;var i=0,n=0;this._interim&&(56320<=(a=e.charCodeAt(n++))&&a<=57343?t[i++]=1024*(this._interim-55296)+a-56320+65536:(t[i++]=this._interim,t[i++]=a),this._interim=0);for(var o=n;o=r)return this._interim=s,i;var a;56320<=(a=e.charCodeAt(o))&&a<=57343?t[i++]=1024*(s-55296)+a-56320+65536:(t[i++]=s,t[i++]=a)}else t[i++]=s}return i},e}();t.StringToUtf32=i;var n=function(){function e(){this.interim=new Uint8Array(3)}return e.prototype.clear=function(){this.interim.fill(0)},e.prototype.decode=function(e,t){var r=e.length;if(!r)return 0;var i,n,o,s,a=0,c=0,l=0;if(this.interim[0]){var h=!1,u=this.interim[0];u&=192==(224&u)?31:224==(240&u)?15:7;for(var f=0,_=void 0;(_=63&this.interim[++f])&&f<4;)u<<=6,u|=_;for(var d=192==(224&this.interim[0])?2:224==(240&this.interim[0])?3:4,p=d-f;l=r)return 0;if(128!=(192&(_=e[l++]))){l--,h=!0;break}this.interim[f++]=_,u<<=6,u|=63&_}h||(2===d?u<128?l--:t[a++]=u:3===d?u<2048||u>=55296&&u<=57343||(t[a++]=u):u<65536||u>1114111||(t[a++]=u)),this.interim.fill(0)}for(var v=r-4,g=l;g=r)return this.interim[0]=i,a;if(128!=(192&(n=e[g++]))){g--;continue}if((c=(31&i)<<6|63&n)<128){g--;continue}t[a++]=c}else if(224==(240&i)){if(g>=r)return this.interim[0]=i,a;if(128!=(192&(n=e[g++]))){g--;continue}if(g>=r)return this.interim[0]=i,this.interim[1]=n,a;if(128!=(192&(o=e[g++]))){g--;continue}if((c=(15&i)<<12|(63&n)<<6|63&o)<2048||c>=55296&&c<=57343)continue;t[a++]=c}else if(240==(248&i)){if(g>=r)return this.interim[0]=i,a;if(128!=(192&(n=e[g++]))){g--;continue}if(g>=r)return this.interim[0]=i,this.interim[1]=n,a;if(128!=(192&(o=e[g++]))){g--;continue}if(g>=r)return this.interim[0]=i,this.interim[1]=n,this.interim[2]=o,a;if(128!=(192&(s=e[g++]))){g--;continue}if((c=(7&i)<<18|(63&n)<<12|(63&o)<<6|63&s)<65536||c>1114111)continue;t[a++]=c}}return a},e}();t.Utf8ToUtf32=n},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CHAR_ATLAS_CELL_SPACING=t.DIM_OPACITY=t.INVERTED_DEFAULT_COLOR=void 0,t.INVERTED_DEFAULT_COLOR=257,t.DIM_OPACITY=.5,t.CHAR_ATLAS_CELL_SPACING=1},function(e,t,r){"use strict";var i,n,o,s;function a(e){var t=e.toString(16);return t.length<2?"0"+t:t}function c(e,t){return e>>0}}(i=t.channels||(t.channels={})),(n=t.color||(t.color={})).blend=function(e,t){var r=(255&t.rgba)/255;if(1===r)return{css:t.css,rgba:t.rgba};var n=t.rgba>>24&255,o=t.rgba>>16&255,s=t.rgba>>8&255,a=e.rgba>>24&255,c=e.rgba>>16&255,l=e.rgba>>8&255,h=a+Math.round((n-a)*r),u=c+Math.round((o-c)*r),f=l+Math.round((s-l)*r);return{css:i.toCss(h,u,f),rgba:i.toRgba(h,u,f)}},n.isOpaque=function(e){return 255==(255&e.rgba)},n.ensureContrastRatio=function(e,t,r){var i=s.ensureContrastRatio(e.rgba,t.rgba,r);if(i)return s.toColor(i>>24&255,i>>16&255,i>>8&255)},n.opaque=function(e){var t=(255|e.rgba)>>>0,r=s.toChannels(t),n=r[0],o=r[1],a=r[2];return{css:i.toCss(n,o,a),rgba:t}},n.opacity=function(e,t){var r=Math.round(255*t),n=s.toChannels(e.rgba),o=n[0],a=n[1],c=n[2];return{css:i.toCss(o,a,c,r),rgba:i.toRgba(o,a,c,r)}},(t.css||(t.css={})).toColor=function(e){switch(e.length){case 7:return{css:e,rgba:(parseInt(e.slice(1),16)<<8|255)>>>0};case 9:return{css:e,rgba:parseInt(e.slice(1),16)>>>0}}throw new Error("css.toColor: Unsupported css format")},function(e){function t(e,t,r){var i=e/255,n=t/255,o=r/255;return.2126*(i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4))+.7152*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))+.0722*(o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4))}e.relativeLuminance=function(e){return t(e>>16&255,e>>8&255,255&e)},e.relativeLuminance2=t}(o=t.rgb||(t.rgb={})),function(e){function t(e,t,r){for(var i=e>>24&255,n=e>>16&255,s=e>>8&255,a=t>>24&255,l=t>>16&255,h=t>>8&255,u=c(o.relativeLuminance2(a,h,l),o.relativeLuminance2(i,n,s));u0||l>0||h>0);)a-=Math.max(0,Math.ceil(.1*a)),l-=Math.max(0,Math.ceil(.1*l)),h-=Math.max(0,Math.ceil(.1*h)),u=c(o.relativeLuminance2(a,h,l),o.relativeLuminance2(i,n,s));return(a<<24|l<<16|h<<8|255)>>>0}function r(e,t,r){for(var i=e>>24&255,n=e>>16&255,s=e>>8&255,a=t>>24&255,l=t>>16&255,h=t>>8&255,u=c(o.relativeLuminance2(a,h,l),o.relativeLuminance2(i,n,s));u>>0}e.ensureContrastRatio=function(e,i,n){var s=o.relativeLuminance(e>>8),a=o.relativeLuminance(i>>8);if(c(s,a)>24&255,e>>16&255,e>>8&255,255&e]},e.toColor=function(e,t,r){return{css:i.toCss(e,t,r),rgba:i.toRgba(e,t,r)}}}(s=t.rgba||(t.rgba={})),t.toPaddedHex=a,t.contrastRatio=c},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isLinux=t.isWindows=t.isIphone=t.isIpad=t.isMac=t.isSafari=t.isFirefox=void 0;var i="undefined"==typeof navigator,n=i?"node":navigator.userAgent,o=i?"node":navigator.platform;function s(e,t){return e.indexOf(t)>=0}t.isFirefox=!!~n.indexOf("Firefox"),t.isSafari=/^((?!chrome|android).)*safari/i.test(n),t.isMac=s(["Macintosh","MacIntel","MacPPC","Mac68K"],o),t.isIpad="iPad"===o,t.isIphone="iPhone"===o,t.isWindows=s(["Windows","Win16","Win32","WinCE"],o),t.isLinux=o.indexOf("Linux")>=0},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.C1=t.C0=void 0,function(e){e.NUL="\0",e.SOH="",e.STX="",e.ETX="",e.EOT="",e.ENQ="",e.ACK="",e.BEL="",e.BS="\b",e.HT="\t",e.LF="\n",e.VT="\v",e.FF="\f",e.CR="\r",e.SO="",e.SI="",e.DLE="",e.DC1="",e.DC2="",e.DC3="",e.DC4="",e.NAK="",e.SYN="",e.ETB="",e.CAN="",e.EM="",e.SUB="",e.ESC="",e.FS="",e.GS="",e.RS="",e.US="",e.SP=" ",e.DEL=""}(t.C0||(t.C0={})),function(e){e.PAD="€",e.HOP="",e.BPH="‚",e.NBH="ƒ",e.IND="„",e.NEL="…",e.SSA="†",e.ESA="‡",e.HTS="ˆ",e.HTJ="‰",e.VTS="Š",e.PLD="‹",e.PLU="Œ",e.RI="",e.SS2="Ž",e.SS3="",e.DCS="",e.PU1="‘",e.PU2="’",e.STS="“",e.CCH="”",e.MW="•",e.SPA="–",e.EPA="—",e.SOS="˜",e.SGCI="™",e.SCI="š",e.CSI="›",e.ST="œ",e.OSC="",e.PM="ž",e.APC="Ÿ"}(t.C1||(t.C1={}))},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.BaseRenderLayer=void 0;var i=r(3),n=r(9),o=r(25),s=r(6),a=r(28),c=r(10),l=r(17),h=function(){function e(e,t,r,i,n,o,s,a){this._container=e,this._alpha=i,this._colors=n,this._rendererId=o,this._bufferService=s,this._optionsService=a,this._scaledCharWidth=0,this._scaledCharHeight=0,this._scaledCellWidth=0,this._scaledCellHeight=0,this._scaledCharLeft=0,this._scaledCharTop=0,this._currentGlyphIdentifier={chars:"",code:0,bg:0,fg:0,bold:!1,dim:!1,italic:!1},this._canvas=document.createElement("canvas"),this._canvas.classList.add("xterm-"+t+"-layer"),this._canvas.style.zIndex=r.toString(),this._initCanvas(),this._container.appendChild(this._canvas)}return e.prototype.dispose=function(){var e;l.removeElementFromParent(this._canvas),null===(e=this._charAtlas)||void 0===e||e.dispose()},e.prototype._initCanvas=function(){this._ctx=a.throwIfFalsy(this._canvas.getContext("2d",{alpha:this._alpha})),this._alpha||this._clearAll()},e.prototype.onOptionsChanged=function(){},e.prototype.onBlur=function(){},e.prototype.onFocus=function(){},e.prototype.onCursorMove=function(){},e.prototype.onGridChanged=function(e,t){},e.prototype.onSelectionChanged=function(e,t,r){void 0===r&&(r=!1)},e.prototype.setColors=function(e){this._refreshCharAtlas(e)},e.prototype._setTransparency=function(e){if(e!==this._alpha){var t=this._canvas;this._alpha=e,this._canvas=this._canvas.cloneNode(),this._initCanvas(),this._container.replaceChild(this._canvas,t),this._refreshCharAtlas(this._colors),this.onGridChanged(0,this._bufferService.rows-1)}},e.prototype._refreshCharAtlas=function(e){this._scaledCharWidth<=0&&this._scaledCharHeight<=0||(this._charAtlas=o.acquireCharAtlas(this._optionsService.options,this._rendererId,e,this._scaledCharWidth,this._scaledCharHeight),this._charAtlas.warmUp())},e.prototype.resize=function(e){this._scaledCellWidth=e.scaledCellWidth,this._scaledCellHeight=e.scaledCellHeight,this._scaledCharWidth=e.scaledCharWidth,this._scaledCharHeight=e.scaledCharHeight,this._scaledCharLeft=e.scaledCharLeft,this._scaledCharTop=e.scaledCharTop,this._canvas.width=e.scaledCanvasWidth,this._canvas.height=e.scaledCanvasHeight,this._canvas.style.width=e.canvasWidth+"px",this._canvas.style.height=e.canvasHeight+"px",this._alpha||this._clearAll(),this._refreshCharAtlas(this._colors)},e.prototype._fillCells=function(e,t,r,i){this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,r*this._scaledCellWidth,i*this._scaledCellHeight)},e.prototype._fillBottomLineAtCells=function(e,t,r){void 0===r&&(r=1),this._ctx.fillRect(e*this._scaledCellWidth,(t+1)*this._scaledCellHeight-window.devicePixelRatio-1,r*this._scaledCellWidth,window.devicePixelRatio)},e.prototype._fillLeftLineAtCell=function(e,t,r){this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,window.devicePixelRatio*r,this._scaledCellHeight)},e.prototype._strokeRectAtCell=function(e,t,r,i){this._ctx.lineWidth=window.devicePixelRatio,this._ctx.strokeRect(e*this._scaledCellWidth+window.devicePixelRatio/2,t*this._scaledCellHeight+window.devicePixelRatio/2,r*this._scaledCellWidth-window.devicePixelRatio,i*this._scaledCellHeight-window.devicePixelRatio)},e.prototype._clearAll=function(){this._alpha?this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height):(this._ctx.fillStyle=this._colors.background.css,this._ctx.fillRect(0,0,this._canvas.width,this._canvas.height))},e.prototype._clearCells=function(e,t,r,i){this._alpha?this._ctx.clearRect(e*this._scaledCellWidth,t*this._scaledCellHeight,r*this._scaledCellWidth,i*this._scaledCellHeight):(this._ctx.fillStyle=this._colors.background.css,this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,r*this._scaledCellWidth,i*this._scaledCellHeight))},e.prototype._fillCharTrueColor=function(e,t,r){this._ctx.font=this._getFont(!1,!1),this._ctx.textBaseline="middle",this._clipRow(r),this._ctx.fillText(e.getChars(),t*this._scaledCellWidth+this._scaledCharLeft,r*this._scaledCellHeight+this._scaledCharTop+this._scaledCharHeight/2)},e.prototype._drawChars=function(e,t,r){var o,s,a=this._getContrastColor(e);a||e.isFgRGB()||e.isBgRGB()?this._drawUncachedChars(e,t,r,a):(e.isInverse()?(o=e.isBgDefault()?n.INVERTED_DEFAULT_COLOR:e.getBgColor(),s=e.isFgDefault()?n.INVERTED_DEFAULT_COLOR:e.getFgColor()):(s=e.isBgDefault()?i.DEFAULT_COLOR:e.getBgColor(),o=e.isFgDefault()?i.DEFAULT_COLOR:e.getFgColor()),o+=this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&o<8?8:0,this._currentGlyphIdentifier.chars=e.getChars()||i.WHITESPACE_CELL_CHAR,this._currentGlyphIdentifier.code=e.getCode()||i.WHITESPACE_CELL_CODE,this._currentGlyphIdentifier.bg=s,this._currentGlyphIdentifier.fg=o,this._currentGlyphIdentifier.bold=!!e.isBold(),this._currentGlyphIdentifier.dim=!!e.isDim(),this._currentGlyphIdentifier.italic=!!e.isItalic(),this._charAtlas&&this._charAtlas.draw(this._ctx,this._currentGlyphIdentifier,t*this._scaledCellWidth+this._scaledCharLeft,r*this._scaledCellHeight+this._scaledCharTop)||this._drawUncachedChars(e,t,r))},e.prototype._drawUncachedChars=function(e,t,r,i){if(this._ctx.save(),this._ctx.font=this._getFont(!!e.isBold(),!!e.isItalic()),this._ctx.textBaseline="middle",e.isInverse())if(i)this._ctx.fillStyle=i.css;else if(e.isBgDefault())this._ctx.fillStyle=c.color.opaque(this._colors.background).css;else if(e.isBgRGB())this._ctx.fillStyle="rgb("+s.AttributeData.toColorRGB(e.getBgColor()).join(",")+")";else{var o=e.getBgColor();this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&o<8&&(o+=8),this._ctx.fillStyle=this._colors.ansi[o].css}else if(i)this._ctx.fillStyle=i.css;else if(e.isFgDefault())this._ctx.fillStyle=this._colors.foreground.css;else if(e.isFgRGB())this._ctx.fillStyle="rgb("+s.AttributeData.toColorRGB(e.getFgColor()).join(",")+")";else{var a=e.getFgColor();this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&a<8&&(a+=8),this._ctx.fillStyle=this._colors.ansi[a].css}this._clipRow(r),e.isDim()&&(this._ctx.globalAlpha=n.DIM_OPACITY),this._ctx.fillText(e.getChars(),t*this._scaledCellWidth+this._scaledCharLeft,r*this._scaledCellHeight+this._scaledCharTop+this._scaledCharHeight/2),this._ctx.restore()},e.prototype._clipRow=function(e){this._ctx.beginPath(),this._ctx.rect(0,e*this._scaledCellHeight,this._bufferService.cols*this._scaledCellWidth,this._scaledCellHeight),this._ctx.clip()},e.prototype._getFont=function(e,t){return(t?"italic":"")+" "+(e?this._optionsService.options.fontWeightBold:this._optionsService.options.fontWeight)+" "+this._optionsService.options.fontSize*window.devicePixelRatio+"px "+this._optionsService.options.fontFamily},e.prototype._getContrastColor=function(e){if(1!==this._optionsService.options.minimumContrastRatio){var t=this._colors.contrastCache.getColor(e.bg,e.fg);if(void 0!==t)return t||void 0;var r=e.getFgColor(),i=e.getFgColorMode(),n=e.getBgColor(),o=e.getBgColorMode(),s=!!e.isInverse(),a=!!e.isInverse();if(s){var l=r;r=n,n=l;var h=i;i=o,o=h}var u=this._resolveBackgroundRgba(o,n,s),f=this._resolveForegroundRgba(i,r,s,a),_=c.rgba.ensureContrastRatio(u,f,this._optionsService.options.minimumContrastRatio);if(_){var d={css:c.channels.toCss(_>>24&255,_>>16&255,_>>8&255),rgba:_};return this._colors.contrastCache.setColor(e.bg,e.fg,d),d}this._colors.contrastCache.setColor(e.bg,e.fg,null)}},e.prototype._resolveBackgroundRgba=function(e,t,r){switch(e){case 16777216:case 33554432:return this._colors.ansi[t].rgba;case 50331648:return t<<8;case 0:default:return r?this._colors.foreground.rgba:this._colors.background.rgba}},e.prototype._resolveForegroundRgba=function(e,t,r,i){switch(e){case 16777216:case 33554432:return this._optionsService.options.drawBoldTextInBrightColors&&i&&t<8&&(t+=8),this._colors.ansi[t].rgba;case 50331648:return t<<8;case 0:default:return r?this._colors.background.rgba:this._colors.foreground.rgba}},e}();t.BaseRenderLayer=h},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createDecorator=t.getServiceDependencies=t.serviceRegistry=void 0;function i(e,t,r){t.di$target===t?t.di$dependencies.push({id:e,index:r}):(t.di$dependencies=[{id:e,index:r}],t.di$target=t)}t.serviceRegistry=new Map,t.getServiceDependencies=function(e){return e.di$dependencies||[]},t.createDecorator=function(e){if(t.serviceRegistry.has(e))return t.serviceRegistry.get(e);var r=function(e,t,n){if(3!==arguments.length)throw new Error("@IServiceName-decorator can only be used to decorate a parameter");i(r,e,n)};return r.toString=function(){return e},t.serviceRegistry.set(e,r),r}},function(e,t,r){"use strict";function i(e,t,r,i){if(void 0===r&&(r=0),void 0===i&&(i=e.length),r>=e.length)return e;r=(e.length+r)%e.length,i=i>=e.length?e.length:(e.length+i)%e.length;for(var n=r;n>22,2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):r]},e.prototype.set=function(e,t){this._data[3*e+1]=t[n.CHAR_DATA_ATTR_INDEX],t[n.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[e]=t[1],this._data[3*e+0]=2097152|e|t[n.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*e+0]=t[n.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|t[n.CHAR_DATA_WIDTH_INDEX]<<22},e.prototype.getWidth=function(e){return this._data[3*e+0]>>22},e.prototype.hasWidth=function(e){return 12582912&this._data[3*e+0]},e.prototype.getFg=function(e){return this._data[3*e+1]},e.prototype.getBg=function(e){return this._data[3*e+2]},e.prototype.hasContent=function(e){return 4194303&this._data[3*e+0]},e.prototype.getCodePoint=function(e){var t=this._data[3*e+0];return 2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):2097151&t},e.prototype.isCombined=function(e){return 2097152&this._data[3*e+0]},e.prototype.getString=function(e){var t=this._data[3*e+0];return 2097152&t?this._combined[e]:2097151&t?i.stringFromCodePoint(2097151&t):""},e.prototype.loadCell=function(e,t){var r=3*e;return t.content=this._data[r+0],t.fg=this._data[r+1],t.bg=this._data[r+2],2097152&t.content&&(t.combinedData=this._combined[e]),268435456&t.bg&&(t.extended=this._extendedAttrs[e]),t},e.prototype.setCell=function(e,t){2097152&t.content&&(this._combined[e]=t.combinedData),268435456&t.bg&&(this._extendedAttrs[e]=t.extended),this._data[3*e+0]=t.content,this._data[3*e+1]=t.fg,this._data[3*e+2]=t.bg},e.prototype.setCellFromCodePoint=function(e,t,r,i,n,o){268435456&n&&(this._extendedAttrs[e]=o),this._data[3*e+0]=t|r<<22,this._data[3*e+1]=i,this._data[3*e+2]=n},e.prototype.addCodepointToCell=function(e,t){var r=this._data[3*e+0];2097152&r?this._combined[e]+=i.stringFromCodePoint(t):(2097151&r?(this._combined[e]=i.stringFromCodePoint(2097151&r)+i.stringFromCodePoint(t),r&=-2097152,r|=2097152):r=t|1<<22,this._data[3*e+0]=r)},e.prototype.insertCells=function(e,t,r,i){if((e%=this.length)&&2===this.getWidth(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==i?void 0:i.fg)||0,(null==i?void 0:i.bg)||0,(null==i?void 0:i.extended)||new s.ExtendedAttrs),t=0;--a)this.setCell(e+t+a,this.loadCell(e+a,n));for(a=0;athis.length){var r=new Uint32Array(3*e);this.length&&(3*e=e&&delete this._combined[o]}}else this._data=new Uint32Array(0),this._combined={};this.length=e}},e.prototype.fill=function(e){this._combined={},this._extendedAttrs={};for(var t=0;t=0;--e)if(4194303&this._data[3*e+0])return e+(this._data[3*e+0]>>22);return 0},e.prototype.copyCellsFrom=function(e,t,r,i,n){var o=e._data;if(n)for(var s=i-1;s>=0;s--)for(var a=0;a<3;a++)this._data[3*(r+s)+a]=o[3*(t+s)+a];else for(s=0;s=t&&(this._combined[l-t+r]=e._combined[l])}},e.prototype.translateToString=function(e,t,r){void 0===e&&(e=!1),void 0===t&&(t=0),void 0===r&&(r=this.length),e&&(r=Math.min(r,this.getTrimmedLength()));for(var o="";t>22||1}return o},e}();t.BufferLine=a},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.removeElementFromParent=void 0,t.removeElementFromParent=function(){for(var e,t=[],r=0;r24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}!function(e){e[e.GET_WIN_SIZE_PIXELS=0]="GET_WIN_SIZE_PIXELS",e[e.GET_CELL_SIZE_PIXELS=1]="GET_CELL_SIZE_PIXELS"}(o=t.WindowsOptionsReportType||(t.WindowsOptionsReportType={}));var m=function(){function e(e,t,r,i){this._bufferService=e,this._coreService=t,this._logService=r,this._optionsService=i,this._data=new Uint32Array(0)}return e.prototype.hook=function(e){this._data=new Uint32Array(0)},e.prototype.put=function(e,t,r){this._data=h.concat(this._data,e.subarray(t,r))},e.prototype.unhook=function(e){if(e){var t=u.utf32ToString(this._data);switch(this._data=new Uint32Array(0),t){case'"q':return this._coreService.triggerDataEvent(s.C0.ESC+'P1$r0"q'+s.C0.ESC+"\\");case'"p':return this._coreService.triggerDataEvent(s.C0.ESC+'P1$r61;1"p'+s.C0.ESC+"\\");case"r":var r=this._bufferService.buffer.scrollTop+1+";"+(this._bufferService.buffer.scrollBottom+1)+"r";return this._coreService.triggerDataEvent(s.C0.ESC+"P1$r"+r+s.C0.ESC+"\\");case"m":return this._coreService.triggerDataEvent(s.C0.ESC+"P1$r0m"+s.C0.ESC+"\\");case" q":var i={block:2,underline:4,bar:6}[this._optionsService.options.cursorStyle];return i-=this._optionsService.options.cursorBlink?1:0,this._coreService.triggerDataEvent(s.C0.ESC+"P1$r"+i+" q"+s.C0.ESC+"\\");default:this._logService.debug("Unknown DCS $q %s",t),this._coreService.triggerDataEvent(s.C0.ESC+"P0$r"+s.C0.ESC+"\\")}}else this._data=new Uint32Array(0)},e}(),C=function(e){function t(t,r,i,n,o,l,h,d,v){void 0===v&&(v=new c.EscapeSequenceParser);var y=e.call(this)||this;y._bufferService=t,y._charsetService=r,y._coreService=i,y._dirtyRowService=n,y._logService=o,y._optionsService=l,y._coreMouseService=h,y._unicodeService=d,y._parser=v,y._parseBuffer=new Uint32Array(4096),y._stringDecoder=new u.StringToUtf32,y._utf8Decoder=new u.Utf8ToUtf32,y._workCell=new p.CellData,y._windowTitle="",y._iconName="",y._windowTitleStack=[],y._iconNameStack=[],y._curAttrData=f.DEFAULT_ATTR_DATA.clone(),y._eraseAttrDataInternal=f.DEFAULT_ATTR_DATA.clone(),y._onRequestBell=new _.EventEmitter,y._onRequestRefreshRows=new _.EventEmitter,y._onRequestReset=new _.EventEmitter,y._onRequestScroll=new _.EventEmitter,y._onRequestSyncScrollBar=new _.EventEmitter,y._onRequestWindowsOptionsReport=new _.EventEmitter,y._onA11yChar=new _.EventEmitter,y._onA11yTab=new _.EventEmitter,y._onCursorMove=new _.EventEmitter,y._onLineFeed=new _.EventEmitter,y._onScroll=new _.EventEmitter,y._onTitleChange=new _.EventEmitter,y.register(y._parser),y._parser.setCsiHandlerFallback((function(e,t){y._logService.debug("Unknown CSI code: ",{identifier:y._parser.identToString(e),params:t.toArray()})})),y._parser.setEscHandlerFallback((function(e){y._logService.debug("Unknown ESC code: ",{identifier:y._parser.identToString(e)})})),y._parser.setExecuteHandlerFallback((function(e){y._logService.debug("Unknown EXECUTE code: ",{code:e})})),y._parser.setOscHandlerFallback((function(e,t,r){y._logService.debug("Unknown OSC code: ",{identifier:e,action:t,data:r})})),y._parser.setDcsHandlerFallback((function(e,t,r){"HOOK"===t&&(r=r.toArray()),y._logService.debug("Unknown DCS code: ",{identifier:y._parser.identToString(e),action:t,payload:r})})),y._parser.setPrintHandler((function(e,t,r){return y.print(e,t,r)})),y._parser.setCsiHandler({final:"@"},(function(e){return y.insertChars(e)})),y._parser.setCsiHandler({intermediates:" ",final:"@"},(function(e){return y.scrollLeft(e)})),y._parser.setCsiHandler({final:"A"},(function(e){return y.cursorUp(e)})),y._parser.setCsiHandler({intermediates:" ",final:"A"},(function(e){return y.scrollRight(e)})),y._parser.setCsiHandler({final:"B"},(function(e){return y.cursorDown(e)})),y._parser.setCsiHandler({final:"C"},(function(e){return y.cursorForward(e)})),y._parser.setCsiHandler({final:"D"},(function(e){return y.cursorBackward(e)})),y._parser.setCsiHandler({final:"E"},(function(e){return y.cursorNextLine(e)})),y._parser.setCsiHandler({final:"F"},(function(e){return y.cursorPrecedingLine(e)})),y._parser.setCsiHandler({final:"G"},(function(e){return y.cursorCharAbsolute(e)})),y._parser.setCsiHandler({final:"H"},(function(e){return y.cursorPosition(e)})),y._parser.setCsiHandler({final:"I"},(function(e){return y.cursorForwardTab(e)})),y._parser.setCsiHandler({final:"J"},(function(e){return y.eraseInDisplay(e)})),y._parser.setCsiHandler({prefix:"?",final:"J"},(function(e){return y.eraseInDisplay(e)})),y._parser.setCsiHandler({final:"K"},(function(e){return y.eraseInLine(e)})),y._parser.setCsiHandler({prefix:"?",final:"K"},(function(e){return y.eraseInLine(e)})),y._parser.setCsiHandler({final:"L"},(function(e){return y.insertLines(e)})),y._parser.setCsiHandler({final:"M"},(function(e){return y.deleteLines(e)})),y._parser.setCsiHandler({final:"P"},(function(e){return y.deleteChars(e)})),y._parser.setCsiHandler({final:"S"},(function(e){return y.scrollUp(e)})),y._parser.setCsiHandler({final:"T"},(function(e){return y.scrollDown(e)})),y._parser.setCsiHandler({final:"X"},(function(e){return y.eraseChars(e)})),y._parser.setCsiHandler({final:"Z"},(function(e){return y.cursorBackwardTab(e)})),y._parser.setCsiHandler({final:"`"},(function(e){return y.charPosAbsolute(e)})),y._parser.setCsiHandler({final:"a"},(function(e){return y.hPositionRelative(e)})),y._parser.setCsiHandler({final:"b"},(function(e){return y.repeatPrecedingCharacter(e)})),y._parser.setCsiHandler({final:"c"},(function(e){return y.sendDeviceAttributesPrimary(e)})),y._parser.setCsiHandler({prefix:">",final:"c"},(function(e){return y.sendDeviceAttributesSecondary(e)})),y._parser.setCsiHandler({final:"d"},(function(e){return y.linePosAbsolute(e)})),y._parser.setCsiHandler({final:"e"},(function(e){return y.vPositionRelative(e)})),y._parser.setCsiHandler({final:"f"},(function(e){return y.hVPosition(e)})),y._parser.setCsiHandler({final:"g"},(function(e){return y.tabClear(e)})),y._parser.setCsiHandler({final:"h"},(function(e){return y.setMode(e)})),y._parser.setCsiHandler({prefix:"?",final:"h"},(function(e){return y.setModePrivate(e)})),y._parser.setCsiHandler({final:"l"},(function(e){return y.resetMode(e)})),y._parser.setCsiHandler({prefix:"?",final:"l"},(function(e){return y.resetModePrivate(e)})),y._parser.setCsiHandler({final:"m"},(function(e){return y.charAttributes(e)})),y._parser.setCsiHandler({final:"n"},(function(e){return y.deviceStatus(e)})),y._parser.setCsiHandler({prefix:"?",final:"n"},(function(e){return y.deviceStatusPrivate(e)})),y._parser.setCsiHandler({intermediates:"!",final:"p"},(function(e){return y.softReset(e)})),y._parser.setCsiHandler({intermediates:" ",final:"q"},(function(e){return y.setCursorStyle(e)})),y._parser.setCsiHandler({final:"r"},(function(e){return y.setScrollRegion(e)})),y._parser.setCsiHandler({final:"s"},(function(e){return y.saveCursor(e)})),y._parser.setCsiHandler({final:"t"},(function(e){return y.windowOptions(e)})),y._parser.setCsiHandler({final:"u"},(function(e){return y.restoreCursor(e)})),y._parser.setCsiHandler({intermediates:"'",final:"}"},(function(e){return y.insertColumns(e)})),y._parser.setCsiHandler({intermediates:"'",final:"~"},(function(e){return y.deleteColumns(e)})),y._parser.setExecuteHandler(s.C0.BEL,(function(){return y.bell()})),y._parser.setExecuteHandler(s.C0.LF,(function(){return y.lineFeed()})),y._parser.setExecuteHandler(s.C0.VT,(function(){return y.lineFeed()})),y._parser.setExecuteHandler(s.C0.FF,(function(){return y.lineFeed()})),y._parser.setExecuteHandler(s.C0.CR,(function(){return y.carriageReturn()})),y._parser.setExecuteHandler(s.C0.BS,(function(){return y.backspace()})),y._parser.setExecuteHandler(s.C0.HT,(function(){return y.tab()})),y._parser.setExecuteHandler(s.C0.SO,(function(){return y.shiftOut()})),y._parser.setExecuteHandler(s.C0.SI,(function(){return y.shiftIn()})),y._parser.setExecuteHandler(s.C1.IND,(function(){return y.index()})),y._parser.setExecuteHandler(s.C1.NEL,(function(){return y.nextLine()})),y._parser.setExecuteHandler(s.C1.HTS,(function(){return y.tabSet()})),y._parser.setOscHandler(0,new g.OscHandler((function(e){y.setTitle(e),y.setIconName(e)}))),y._parser.setOscHandler(1,new g.OscHandler((function(e){return y.setIconName(e)}))),y._parser.setOscHandler(2,new g.OscHandler((function(e){return y.setTitle(e)}))),y._parser.setEscHandler({final:"7"},(function(){return y.saveCursor()})),y._parser.setEscHandler({final:"8"},(function(){return y.restoreCursor()})),y._parser.setEscHandler({final:"D"},(function(){return y.index()})),y._parser.setEscHandler({final:"E"},(function(){return y.nextLine()})),y._parser.setEscHandler({final:"H"},(function(){return y.tabSet()})),y._parser.setEscHandler({final:"M"},(function(){return y.reverseIndex()})),y._parser.setEscHandler({final:"="},(function(){return y.keypadApplicationMode()})),y._parser.setEscHandler({final:">"},(function(){return y.keypadNumericMode()})),y._parser.setEscHandler({final:"c"},(function(){return y.fullReset()})),y._parser.setEscHandler({final:"n"},(function(){return y.setgLevel(2)})),y._parser.setEscHandler({final:"o"},(function(){return y.setgLevel(3)})),y._parser.setEscHandler({final:"|"},(function(){return y.setgLevel(3)})),y._parser.setEscHandler({final:"}"},(function(){return y.setgLevel(2)})),y._parser.setEscHandler({final:"~"},(function(){return y.setgLevel(1)})),y._parser.setEscHandler({intermediates:"%",final:"@"},(function(){return y.selectDefaultCharset()})),y._parser.setEscHandler({intermediates:"%",final:"G"},(function(){return y.selectDefaultCharset()}));var b=function(e){S._parser.setEscHandler({intermediates:"(",final:e},(function(){return y.selectCharset("("+e)})),S._parser.setEscHandler({intermediates:")",final:e},(function(){return y.selectCharset(")"+e)})),S._parser.setEscHandler({intermediates:"*",final:e},(function(){return y.selectCharset("*"+e)})),S._parser.setEscHandler({intermediates:"+",final:e},(function(){return y.selectCharset("+"+e)})),S._parser.setEscHandler({intermediates:"-",final:e},(function(){return y.selectCharset("-"+e)})),S._parser.setEscHandler({intermediates:".",final:e},(function(){return y.selectCharset("."+e)})),S._parser.setEscHandler({intermediates:"/",final:e},(function(){return y.selectCharset("/"+e)}))},S=this;for(var C in a.CHARSETS)b(C);return y._parser.setEscHandler({intermediates:"#",final:"8"},(function(){return y.screenAlignmentPattern()})),y._parser.setErrorHandler((function(e){return y._logService.error("Parsing error: ",e),e})),y._parser.setDcsHandler({intermediates:"$",final:"q"},new m(y._bufferService,y._coreService,y._logService,y._optionsService)),y}return n(t,e),Object.defineProperty(t.prototype,"onRequestBell",{get:function(){return this._onRequestBell.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestRefreshRows",{get:function(){return this._onRequestRefreshRows.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestReset",{get:function(){return this._onRequestReset.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestScroll",{get:function(){return this._onRequestScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestSyncScrollBar",{get:function(){return this._onRequestSyncScrollBar.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestWindowsOptionsReport",{get:function(){return this._onRequestWindowsOptionsReport.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yChar",{get:function(){return this._onA11yChar.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yTab",{get:function(){return this._onA11yTab.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onCursorMove",{get:function(){return this._onCursorMove.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onLineFeed",{get:function(){return this._onLineFeed.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onScroll",{get:function(){return this._onScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onTitleChange",{get:function(){return this._onTitleChange.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){e.prototype.dispose.call(this)},t.prototype.parse=function(e){var t=this._bufferService.buffer,r=t.x,i=t.y;if(this._logService.debug("parsing data",e),this._parseBuffer.length131072)for(var n=0;n0&&2===_.getWidth(o.x-1)&&_.setCellFromCodePoint(o.x-1,0,1,f.fg,f.bg,f.extended);for(var p=t;p=c)if(l){for(;o.x=this._bufferService.rows&&(o.y=this._bufferService.rows-1),o.lines.get(o.ybase+o.y).isWrapped=!0),_=o.lines.get(o.ybase+o.y)}else if(o.x=c-1,2===n)continue;if(h&&(_.insertCells(o.x,n,o.getNullCell(f),f),2===_.getWidth(c-1)&&_.setCellFromCodePoint(c-1,d.NULL_CELL_CODE,d.NULL_CELL_WIDTH,f.fg,f.bg,f.extended)),_.setCellFromCodePoint(o.x++,i,n,f.fg,f.bg,f.extended),n>0)for(;--n;)_.setCellFromCodePoint(o.x++,0,0,f.fg,f.bg,f.extended)}else _.getWidth(o.x-1)?_.addCodepointToCell(o.x-1,i):_.addCodepointToCell(o.x-2,i)}r-t>0&&(_.loadCell(o.x-1,this._workCell),2===this._workCell.getWidth()||this._workCell.getCode()>65535?this._parser.precedingCodepoint=0:this._workCell.isCombined()?this._parser.precedingCodepoint=this._workCell.getChars().charCodeAt(0):this._parser.precedingCodepoint=this._workCell.content),o.x0&&0===_.getWidth(o.x)&&!_.hasContent(o.x)&&_.setCellFromCodePoint(o.x,0,1,f.fg,f.bg,f.extended),this._dirtyRowService.markDirty(o.y)},t.prototype.addCsiHandler=function(e,t){var r=this;return"t"!==e.final||e.prefix||e.intermediates?this._parser.addCsiHandler(e,t):this._parser.addCsiHandler(e,(function(e){return!S(e.params[0],r._optionsService.options.windowOptions)||t(e)}))},t.prototype.addDcsHandler=function(e,t){return this._parser.addDcsHandler(e,new y.DcsHandler(t))},t.prototype.addEscHandler=function(e,t){return this._parser.addEscHandler(e,t)},t.prototype.addOscHandler=function(e,t){return this._parser.addOscHandler(e,new g.OscHandler(t))},t.prototype.bell=function(){this._onRequestBell.fire()},t.prototype.lineFeed=function(){var e=this._bufferService.buffer;this._dirtyRowService.markDirty(e.y),this._optionsService.options.convertEol&&(e.x=0),e.y++,e.y===e.scrollBottom+1?(e.y--,this._onRequestScroll.fire(this._eraseAttrData())):e.y>=this._bufferService.rows&&(e.y=this._bufferService.rows-1),e.x>=this._bufferService.cols&&e.x--,this._dirtyRowService.markDirty(e.y),this._onLineFeed.fire()},t.prototype.carriageReturn=function(){this._bufferService.buffer.x=0},t.prototype.backspace=function(){var e,t=this._bufferService.buffer;if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),void(t.x>0&&t.x--);if(this._restrictCursor(this._bufferService.cols),t.x>0)t.x--;else if(0===t.x&&t.y>t.scrollTop&&t.y<=t.scrollBottom&&(null===(e=t.lines.get(t.ybase+t.y))||void 0===e?void 0:e.isWrapped)){t.lines.get(t.ybase+t.y).isWrapped=!1,t.y--,t.x=this._bufferService.cols-1;var r=t.lines.get(t.ybase+t.y);r.hasWidth(t.x)&&!r.hasContent(t.x)&&t.x--}this._restrictCursor()},t.prototype.tab=function(){if(!(this._bufferService.buffer.x>=this._bufferService.cols)){var e=this._bufferService.buffer.x;this._bufferService.buffer.x=this._bufferService.buffer.nextStop(),this._optionsService.options.screenReaderMode&&this._onA11yTab.fire(this._bufferService.buffer.x-e)}},t.prototype.shiftOut=function(){this._charsetService.setgLevel(1)},t.prototype.shiftIn=function(){this._charsetService.setgLevel(0)},t.prototype._restrictCursor=function(e){void 0===e&&(e=this._bufferService.cols-1),this._bufferService.buffer.x=Math.min(e,Math.max(0,this._bufferService.buffer.x)),this._bufferService.buffer.y=this._coreService.decPrivateModes.origin?Math.min(this._bufferService.buffer.scrollBottom,Math.max(this._bufferService.buffer.scrollTop,this._bufferService.buffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._bufferService.buffer.y)),this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype._setCursor=function(e,t){this._dirtyRowService.markDirty(this._bufferService.buffer.y),this._coreService.decPrivateModes.origin?(this._bufferService.buffer.x=e,this._bufferService.buffer.y=this._bufferService.buffer.scrollTop+t):(this._bufferService.buffer.x=e,this._bufferService.buffer.y=t),this._restrictCursor(),this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype._moveCursor=function(e,t){this._restrictCursor(),this._setCursor(this._bufferService.buffer.x+e,this._bufferService.buffer.y+t)},t.prototype.cursorUp=function(e){var t=this._bufferService.buffer.y-this._bufferService.buffer.scrollTop;t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1))},t.prototype.cursorDown=function(e){var t=this._bufferService.buffer.scrollBottom-this._bufferService.buffer.y;t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1)},t.prototype.cursorForward=function(e){this._moveCursor(e.params[0]||1,0)},t.prototype.cursorBackward=function(e){this._moveCursor(-(e.params[0]||1),0)},t.prototype.cursorNextLine=function(e){this.cursorDown(e),this._bufferService.buffer.x=0},t.prototype.cursorPrecedingLine=function(e){this.cursorUp(e),this._bufferService.buffer.x=0},t.prototype.cursorCharAbsolute=function(e){this._setCursor((e.params[0]||1)-1,this._bufferService.buffer.y)},t.prototype.cursorPosition=function(e){this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1)},t.prototype.charPosAbsolute=function(e){this._setCursor((e.params[0]||1)-1,this._bufferService.buffer.y)},t.prototype.hPositionRelative=function(e){this._moveCursor(e.params[0]||1,0)},t.prototype.linePosAbsolute=function(e){this._setCursor(this._bufferService.buffer.x,(e.params[0]||1)-1)},t.prototype.vPositionRelative=function(e){this._moveCursor(0,e.params[0]||1)},t.prototype.hVPosition=function(e){this.cursorPosition(e)},t.prototype.tabClear=function(e){var t=e.params[0];0===t?delete this._bufferService.buffer.tabs[this._bufferService.buffer.x]:3===t&&(this._bufferService.buffer.tabs={})},t.prototype.cursorForwardTab=function(e){if(!(this._bufferService.buffer.x>=this._bufferService.cols))for(var t=e.params[0]||1;t--;)this._bufferService.buffer.x=this._bufferService.buffer.nextStop()},t.prototype.cursorBackwardTab=function(e){if(!(this._bufferService.buffer.x>=this._bufferService.cols))for(var t=e.params[0]||1,r=this._bufferService.buffer;t--;)r.x=r.prevStop()},t.prototype._eraseInBufferLine=function(e,t,r,i){void 0===i&&(i=!1);var n=this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase+e);n.replaceCells(t,r,this._bufferService.buffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),i&&(n.isWrapped=!1)},t.prototype._resetBufferLine=function(e){var t=this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase+e);t.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())),t.isWrapped=!1},t.prototype.eraseInDisplay=function(e){var t;switch(this._restrictCursor(),e.params[0]){case 0:for(t=this._bufferService.buffer.y,this._dirtyRowService.markDirty(t),this._eraseInBufferLine(t++,this._bufferService.buffer.x,this._bufferService.cols,0===this._bufferService.buffer.x);t=this._bufferService.cols&&(this._bufferService.buffer.lines.get(t+1).isWrapped=!1);t--;)this._resetBufferLine(t);this._dirtyRowService.markDirty(0);break;case 2:for(t=this._bufferService.rows,this._dirtyRowService.markDirty(t-1);t--;)this._resetBufferLine(t);this._dirtyRowService.markDirty(0);break;case 3:var r=this._bufferService.buffer.lines.length-this._bufferService.rows;r>0&&(this._bufferService.buffer.lines.trimStart(r),this._bufferService.buffer.ybase=Math.max(this._bufferService.buffer.ybase-r,0),this._bufferService.buffer.ydisp=Math.max(this._bufferService.buffer.ydisp-r,0),this._onScroll.fire(0))}},t.prototype.eraseInLine=function(e){switch(this._restrictCursor(),e.params[0]){case 0:this._eraseInBufferLine(this._bufferService.buffer.y,this._bufferService.buffer.x,this._bufferService.cols);break;case 1:this._eraseInBufferLine(this._bufferService.buffer.y,0,this._bufferService.buffer.x+1);break;case 2:this._eraseInBufferLine(this._bufferService.buffer.y,0,this._bufferService.cols)}this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype.insertLines=function(e){this._restrictCursor();var t=e.params[0]||1,r=this._bufferService.buffer;if(!(r.y>r.scrollBottom||r.yr.scrollBottom||r.yt.scrollBottom||t.yt.scrollBottom||t.yt.scrollBottom||t.yt.scrollBottom||t.y0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent(s.C0.ESC+"[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent(s.C0.ESC+"[?6c"))},t.prototype.sendDeviceAttributesSecondary=function(e){e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent(s.C0.ESC+"[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent(s.C0.ESC+"[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent(s.C0.ESC+"[>83;40003;0c"))},t.prototype._is=function(e){return 0===(this._optionsService.options.termName+"").indexOf(e)},t.prototype.setMode=function(e){for(var t=0;t=2||2===i[1]&&o+n>=5)break;i[1]&&(n=1)}while(++o+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,0===e&&(t.fg&=-268435457),t.updateExtended()},t.prototype.charAttributes=function(e){if(1===e.length&&0===e.params[0])return this._curAttrData.fg=f.DEFAULT_ATTR_DATA.fg,void(this._curAttrData.bg=f.DEFAULT_ATTR_DATA.bg);for(var t,r=e.length,i=this._curAttrData,n=0;n=30&&t<=37?(i.fg&=-50331904,i.fg|=16777216|t-30):t>=40&&t<=47?(i.bg&=-50331904,i.bg|=16777216|t-40):t>=90&&t<=97?(i.fg&=-50331904,i.fg|=16777224|t-90):t>=100&&t<=107?(i.bg&=-50331904,i.bg|=16777224|t-100):0===t?(i.fg=f.DEFAULT_ATTR_DATA.fg,i.bg=f.DEFAULT_ATTR_DATA.bg):1===t?i.fg|=134217728:3===t?i.bg|=67108864:4===t?(i.fg|=268435456,this._processUnderline(e.hasSubParams(n)?e.getSubParams(n)[0]:1,i)):5===t?i.fg|=536870912:7===t?i.fg|=67108864:8===t?i.fg|=1073741824:2===t?i.bg|=134217728:21===t?this._processUnderline(2,i):22===t?(i.fg&=-134217729,i.bg&=-134217729):23===t?i.bg&=-67108865:24===t?i.fg&=-268435457:25===t?i.fg&=-536870913:27===t?i.fg&=-67108865:28===t?i.fg&=-1073741825:39===t?(i.fg&=-67108864,i.fg|=16777215&f.DEFAULT_ATTR_DATA.fg):49===t?(i.bg&=-67108864,i.bg|=16777215&f.DEFAULT_ATTR_DATA.bg):38===t||48===t||58===t?n+=this._extractColor(e,n,i):59===t?(i.extended=i.extended.clone(),i.extended.underlineColor=-1,i.updateExtended()):100===t?(i.fg&=-67108864,i.fg|=16777215&f.DEFAULT_ATTR_DATA.fg,i.bg&=-67108864,i.bg|=16777215&f.DEFAULT_ATTR_DATA.bg):this._logService.debug("Unknown SGR attribute: %d.",t)},t.prototype.deviceStatus=function(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent(s.C0.ESC+"[0n");break;case 6:var t=this._bufferService.buffer.y+1,r=this._bufferService.buffer.x+1;this._coreService.triggerDataEvent(s.C0.ESC+"["+t+";"+r+"R")}},t.prototype.deviceStatusPrivate=function(e){switch(e.params[0]){case 6:var t=this._bufferService.buffer.y+1,r=this._bufferService.buffer.x+1;this._coreService.triggerDataEvent(s.C0.ESC+"[?"+t+";"+r+"R")}},t.prototype.softReset=function(e){this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._bufferService.buffer.scrollTop=0,this._bufferService.buffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=f.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._bufferService.buffer.savedX=0,this._bufferService.buffer.savedY=this._bufferService.buffer.ybase,this._bufferService.buffer.savedCurAttrData.fg=this._curAttrData.fg,this._bufferService.buffer.savedCurAttrData.bg=this._curAttrData.bg,this._bufferService.buffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1},t.prototype.setCursorStyle=function(e){var t=e.params[0]||1;switch(t){case 1:case 2:this._optionsService.options.cursorStyle="block";break;case 3:case 4:this._optionsService.options.cursorStyle="underline";break;case 5:case 6:this._optionsService.options.cursorStyle="bar"}var r=t%2==1;this._optionsService.options.cursorBlink=r},t.prototype.setScrollRegion=function(e){var t,r=e.params[0]||1;(e.length<2||(t=e.params[1])>this._bufferService.rows||0===t)&&(t=this._bufferService.rows),t>r&&(this._bufferService.buffer.scrollTop=r-1,this._bufferService.buffer.scrollBottom=t-1,this._setCursor(0,0))},t.prototype.windowOptions=function(e){if(S(e.params[0],this._optionsService.options.windowOptions)){var t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:2!==t&&this._onRequestWindowsOptionsReport.fire(o.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(o.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(s.C0.ESC+"[8;"+this._bufferService.rows+";"+this._bufferService.cols+"t");break;case 22:0!==t&&2!==t||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),0!==t&&1!==t||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:0!==t&&2!==t||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),0!==t&&1!==t||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}}},t.prototype.saveCursor=function(e){this._bufferService.buffer.savedX=this._bufferService.buffer.x,this._bufferService.buffer.savedY=this._bufferService.buffer.ybase+this._bufferService.buffer.y,this._bufferService.buffer.savedCurAttrData.fg=this._curAttrData.fg,this._bufferService.buffer.savedCurAttrData.bg=this._curAttrData.bg,this._bufferService.buffer.savedCharset=this._charsetService.charset},t.prototype.restoreCursor=function(e){this._bufferService.buffer.x=this._bufferService.buffer.savedX||0,this._bufferService.buffer.y=Math.max(this._bufferService.buffer.savedY-this._bufferService.buffer.ybase,0),this._curAttrData.fg=this._bufferService.buffer.savedCurAttrData.fg,this._curAttrData.bg=this._bufferService.buffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._bufferService.buffer.savedCharset&&(this._charsetService.charset=this._bufferService.buffer.savedCharset),this._restrictCursor()},t.prototype.setTitle=function(e){this._windowTitle=e,this._onTitleChange.fire(e)},t.prototype.setIconName=function(e){this._iconName=e},t.prototype.nextLine=function(){this._bufferService.buffer.x=0,this.index()},t.prototype.keypadApplicationMode=function(){this._logService.debug("Serial port requested application keypad."),this._coreService.decPrivateModes.applicationKeypad=!0,this._onRequestSyncScrollBar.fire()},t.prototype.keypadNumericMode=function(){this._logService.debug("Switching back to normal keypad."),this._coreService.decPrivateModes.applicationKeypad=!1,this._onRequestSyncScrollBar.fire()},t.prototype.selectDefaultCharset=function(){this._charsetService.setgLevel(0),this._charsetService.setgCharset(0,a.DEFAULT_CHARSET)},t.prototype.selectCharset=function(e){2===e.length?"/"!==e[0]&&this._charsetService.setgCharset(b[e[0]],a.CHARSETS[e[1]]||a.DEFAULT_CHARSET):this.selectDefaultCharset()},t.prototype.index=function(){this._restrictCursor();var e=this._bufferService.buffer;this._bufferService.buffer.y++,e.y===e.scrollBottom+1?(e.y--,this._onRequestScroll.fire(this._eraseAttrData())):e.y>=this._bufferService.rows&&(e.y=this._bufferService.rows-1),this._restrictCursor()},t.prototype.tabSet=function(){this._bufferService.buffer.tabs[this._bufferService.buffer.x]=!0},t.prototype.reverseIndex=function(){this._restrictCursor();var e=this._bufferService.buffer;if(e.y===e.scrollTop){var t=e.scrollBottom-e.scrollTop;e.lines.shiftElements(e.ybase+e.y,t,1),e.lines.set(e.ybase+e.y,e.getBlankLine(this._eraseAttrData())),this._dirtyRowService.markRangeDirty(e.scrollTop,e.scrollBottom)}else e.y--,this._restrictCursor()},t.prototype.fullReset=function(){this._parser.reset(),this._onRequestReset.fire()},t.prototype.reset=function(){this._curAttrData=f.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=f.DEFAULT_ATTR_DATA.clone()},t.prototype._eraseAttrData=function(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal},t.prototype.setgLevel=function(e){this._charsetService.setgLevel(e)},t.prototype.screenAlignmentPattern=function(){var e=new p.CellData;e.content=1<<22|"E".charCodeAt(0),e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg;var t=this._bufferService.buffer;this._setCursor(0,0);for(var r=0;r256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}return e.fromArray=function(t){var r=new e;if(!t.length)return r;for(var i=t[0]instanceof Array?1:0;i>8,i=255&this._subParamsIdx[t];i-r>0&&e.push(Array.prototype.slice.call(this._subParams,r,i))}return e},e.prototype.reset=function(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1},e.prototype.addParam=function(e){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>2147483647?2147483647:e}},e.prototype.addSubParam=function(e){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParams[this._subParamsLength++]=e>2147483647?2147483647:e,this._subParamsIdx[this.length-1]++}},e.prototype.hasSubParams=function(e){return(255&this._subParamsIdx[e])-(this._subParamsIdx[e]>>8)>0},e.prototype.getSubParams=function(e){var t=this._subParamsIdx[e]>>8,r=255&this._subParamsIdx[e];return r-t>0?this._subParams.subarray(t,r):null},e.prototype.getSubParamsAll=function(){for(var e={},t=0;t>8,i=255&this._subParamsIdx[t];i-r>0&&(e[t]=this._subParams.slice(r,i))}return e},e.prototype.addDigit=function(e){var t;if(!(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)){var r=this._digitIsSub?this._subParams:this.params,i=r[t-1];r[t-1]=~i?Math.min(10*i+e,2147483647):e}},e}();t.Params=i},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.OscHandler=t.OscParser=void 0;var i=r(23),n=r(8),o=function(){function e(){this._state=0,this._id=-1,this._handlers=Object.create(null),this._handlerFb=function(){}}return e.prototype.addHandler=function(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);var r=this._handlers[e];return r.push(t),{dispose:function(){var e=r.indexOf(t);-1!==e&&r.splice(e,1)}}},e.prototype.setHandler=function(e,t){this._handlers[e]=[t]},e.prototype.clearHandler=function(e){this._handlers[e]&&delete this._handlers[e]},e.prototype.setHandlerFallback=function(e){this._handlerFb=e},e.prototype.dispose=function(){this._handlers=Object.create(null),this._handlerFb=function(){}},e.prototype.reset=function(){2===this._state&&this.end(!1),this._id=-1,this._state=0},e.prototype._start=function(){var e=this._handlers[this._id];if(e)for(var t=e.length-1;t>=0;t--)e[t].start();else this._handlerFb(this._id,"START")},e.prototype._put=function(e,t,r){var i=this._handlers[this._id];if(i)for(var o=i.length-1;o>=0;o--)i[o].put(e,t,r);else this._handlerFb(this._id,"PUT",n.utf32ToString(e,t,r))},e.prototype._end=function(e){var t=this._handlers[this._id];if(t){for(var r=t.length-1;r>=0&&!1===t[r].end(e);r--);for(r--;r>=0;r--)t[r].end(!1)}else this._handlerFb(this._id,"END",e)},e.prototype.start=function(){this.reset(),this._id=-1,this._state=1},e.prototype.put=function(e,t,r){if(3!==this._state){if(1===this._state)for(;t0&&this._put(e,t,r)}},e.prototype.end=function(e){0!==this._state&&(3!==this._state&&(1===this._state&&this._start(),this._end(e)),this._id=-1,this._state=0)},e}();t.OscParser=o;var s=function(){function e(e){this._handler=e,this._data="",this._hitLimit=!1}return e.prototype.start=function(){this._data="",this._hitLimit=!1},e.prototype.put=function(e,t,r){this._hitLimit||(this._data+=n.utf32ToString(e,t,r),this._data.length>i.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))},e.prototype.end=function(e){var t;return this._hitLimit?t=!1:e&&(t=this._handler(this._data)),this._data="",this._hitLimit=!1,t},e}();t.OscHandler=s},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.PAYLOAD_LIMIT=void 0,t.PAYLOAD_LIMIT=1e7},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.DcsHandler=t.DcsParser=void 0;var i=r(8),n=r(21),o=r(23),s=[],a=function(){function e(){this._handlers=Object.create(null),this._active=s,this._ident=0,this._handlerFb=function(){}}return e.prototype.dispose=function(){this._handlers=Object.create(null),this._handlerFb=function(){}},e.prototype.addHandler=function(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);var r=this._handlers[e];return r.push(t),{dispose:function(){var e=r.indexOf(t);-1!==e&&r.splice(e,1)}}},e.prototype.setHandler=function(e,t){this._handlers[e]=[t]},e.prototype.clearHandler=function(e){this._handlers[e]&&delete this._handlers[e]},e.prototype.setHandlerFallback=function(e){this._handlerFb=e},e.prototype.reset=function(){this._active.length&&this.unhook(!1),this._active=s,this._ident=0},e.prototype.hook=function(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||s,this._active.length)for(var r=this._active.length-1;r>=0;r--)this._active[r].hook(t);else this._handlerFb(this._ident,"HOOK",t)},e.prototype.put=function(e,t,r){if(this._active.length)for(var n=this._active.length-1;n>=0;n--)this._active[n].put(e,t,r);else this._handlerFb(this._ident,"PUT",i.utf32ToString(e,t,r))},e.prototype.unhook=function(e){if(this._active.length){for(var t=this._active.length-1;t>=0&&!1===this._active[t].unhook(e);t--);for(t--;t>=0;t--)this._active[t].unhook(!1)}else this._handlerFb(this._ident,"UNHOOK",e);this._active=s,this._ident=0},e}();t.DcsParser=a;var c=function(){function e(e){this._handler=e,this._data="",this._hitLimit=!1}return e.prototype.hook=function(e){this._params=e.clone(),this._data="",this._hitLimit=!1},e.prototype.put=function(e,t,r){this._hitLimit||(this._data+=i.utf32ToString(e,t,r),this._data.length>o.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))},e.prototype.unhook=function(e){var t;return this._hitLimit?t=!1:e&&(t=this._handler(this._data,this._params||new n.Params)),this._params=void 0,this._data="",this._hitLimit=!1,t},e}();t.DcsHandler=c},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.removeTerminalFromCache=t.acquireCharAtlas=void 0;var i=r(26),n=r(43),o=[];t.acquireCharAtlas=function(e,t,r,s,a){for(var c=i.generateConfig(s,a,e,r),l=0;l=0){if(i.configEquals(u.config,c))return u.atlas;1===u.ownedBy.length?(u.atlas.dispose(),o.splice(l,1)):u.ownedBy.splice(h,1);break}}for(l=0;l1)for(var u=this._getJoinedRanges(i,a,o,t,n),f=0;f1)for(u=this._getJoinedRanges(i,a,o,t,n),f=0;f=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new i.CellData)},e.prototype.translateToString=function(e,t,r){return this._line.translateToString(e,t,r)},e}(),f=function(){function e(e){this._core=e}return e.prototype.registerCsiHandler=function(e,t){return this._core.addCsiHandler(e,(function(e){return t(e.toArray())}))},e.prototype.addCsiHandler=function(e,t){return this.registerCsiHandler(e,t)},e.prototype.registerDcsHandler=function(e,t){return this._core.addDcsHandler(e,(function(e,r){return t(e,r.toArray())}))},e.prototype.addDcsHandler=function(e,t){return this.registerDcsHandler(e,t)},e.prototype.registerEscHandler=function(e,t){return this._core.addEscHandler(e,t)},e.prototype.addEscHandler=function(e,t){return this.registerEscHandler(e,t)},e.prototype.registerOscHandler=function(e,t){return this._core.addOscHandler(e,t)},e.prototype.addOscHandler=function(e,t){return this.registerOscHandler(e,t)},e}(),_=function(){function e(e){this._core=e}return e.prototype.register=function(e){this._core.unicodeService.register(e)},Object.defineProperty(e.prototype,"versions",{get:function(){return this._core.unicodeService.versions},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"activeVersion",{get:function(){return this._core.unicodeService.activeVersion},set:function(e){this._core.unicodeService.activeVersion=e},enumerable:!1,configurable:!0}),e}()},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.Terminal=void 0;var o=r(36),s=r(37),a=r(38),c=r(12),l=r(19),h=r(40),u=r(50),f=r(51),_=r(11),d=r(7),p=r(18),v=r(54),g=r(55),y=r(56),b=r(57),S=r(59),m=r(0),C=r(16),w=r(27),E=r(60),L=r(5),A=r(61),R=r(62),k=r(63),x=r(64),D=r(65),T="undefined"!=typeof window?window.document:null,O=function(e){function t(t){void 0===t&&(t={});var r=e.call(this,t)||this;return r.browser=_,r._keyDownHandled=!1,r._onCursorMove=new m.EventEmitter,r._onKey=new m.EventEmitter,r._onRender=new m.EventEmitter,r._onSelectionChange=new m.EventEmitter,r._onTitleChange=new m.EventEmitter,r._onFocus=new m.EventEmitter,r._onBlur=new m.EventEmitter,r._onA11yCharEmitter=new m.EventEmitter,r._onA11yTabEmitter=new m.EventEmitter,r._setup(),r.linkifier=r._instantiationService.createInstance(u.Linkifier),r.linkifier2=r.register(r._instantiationService.createInstance(k.Linkifier2)),r.register(r._inputHandler.onRequestBell((function(){return r.bell()}))),r.register(r._inputHandler.onRequestRefreshRows((function(e,t){return r.refresh(e,t)}))),r.register(r._inputHandler.onRequestReset((function(){return r.reset()}))),r.register(r._inputHandler.onRequestScroll((function(e,t){return r.scroll(e,t||void 0)}))),r.register(r._inputHandler.onRequestWindowsOptionsReport((function(e){return r._reportWindowsOptions(e)}))),r.register(m.forwardEvent(r._inputHandler.onCursorMove,r._onCursorMove)),r.register(m.forwardEvent(r._inputHandler.onTitleChange,r._onTitleChange)),r.register(m.forwardEvent(r._inputHandler.onA11yChar,r._onA11yCharEmitter)),r.register(m.forwardEvent(r._inputHandler.onA11yTab,r._onA11yTabEmitter)),r.register(r._bufferService.onResize((function(e){return r._afterResize(e.cols,e.rows)}))),r}return n(t,e),Object.defineProperty(t.prototype,"options",{get:function(){return this.optionsService.options},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onCursorMove",{get:function(){return this._onCursorMove.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onKey",{get:function(){return this._onKey.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRender",{get:function(){return this._onRender.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onSelectionChange",{get:function(){return this._onSelectionChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onTitleChange",{get:function(){return this._onTitleChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onFocus",{get:function(){return this._onFocus.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onBlur",{get:function(){return this._onBlur.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yChar",{get:function(){return this._onA11yCharEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yTab",{get:function(){return this._onA11yTabEmitter.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){var t,r,i;this._isDisposed||(e.prototype.dispose.call(this),null===(t=this._renderService)||void 0===t||t.dispose(),this._customKeyEventHandler=void 0,this.write=function(){},null===(i=null===(r=this.element)||void 0===r?void 0:r.parentNode)||void 0===i||i.removeChild(this.element))},t.prototype._setup=function(){e.prototype._setup.call(this),this._customKeyEventHandler=void 0},Object.defineProperty(t.prototype,"buffer",{get:function(){return this.buffers.active},enumerable:!1,configurable:!0}),t.prototype.focus=function(){this.textarea&&this.textarea.focus({preventScroll:!0})},t.prototype._updateOptions=function(t){var r,i,n,o;switch(e.prototype._updateOptions.call(this,t),t){case"fontFamily":case"fontSize":null===(r=this._renderService)||void 0===r||r.clear(),null===(i=this._charSizeService)||void 0===i||i.measure();break;case"cursorBlink":case"cursorStyle":this.refresh(this.buffer.y,this.buffer.y);break;case"drawBoldTextInBrightColors":case"letterSpacing":case"lineHeight":case"fontWeight":case"fontWeightBold":case"minimumContrastRatio":this._renderService&&(this._renderService.clear(),this._renderService.onResize(this.cols,this.rows),this.refresh(0,this.rows-1));break;case"rendererType":this._renderService&&(this._renderService.setRenderer(this._createRenderer()),this._renderService.onResize(this.cols,this.rows));break;case"scrollback":null===(n=this.viewport)||void 0===n||n.syncScrollArea();break;case"screenReaderMode":this.optionsService.options.screenReaderMode?!this._accessibilityManager&&this._renderService&&(this._accessibilityManager=new y.AccessibilityManager(this,this._renderService)):(null===(o=this._accessibilityManager)||void 0===o||o.dispose(),this._accessibilityManager=void 0);break;case"tabStopWidth":this.buffers.setupTabStops();break;case"theme":this._setTheme(this.optionsService.options.theme)}},t.prototype._onTextAreaFocus=function(e){this._coreService.decPrivateModes.sendFocus&&this._coreService.triggerDataEvent(c.C0.ESC+"[I"),this.updateCursorStyle(e),this.element.classList.add("focus"),this._showCursor(),this._onFocus.fire()},t.prototype.blur=function(){var e;return null===(e=this.textarea)||void 0===e?void 0:e.blur()},t.prototype._onTextAreaBlur=function(){this.textarea.value="",this.refresh(this.buffer.y,this.buffer.y),this._coreService.decPrivateModes.sendFocus&&this._coreService.triggerDataEvent(c.C0.ESC+"[O"),this.element.classList.remove("focus"),this._onBlur.fire()},t.prototype._syncTextArea=function(){if(this.textarea&&this.buffer.isCursorInViewport&&!this._compositionHelper.isComposing){var e=Math.ceil(this._charSizeService.height*this.optionsService.options.lineHeight),t=this._bufferService.buffer.y*e,r=this._bufferService.buffer.x*this._charSizeService.width;this.textarea.style.left=r+"px",this.textarea.style.top=t+"px",this.textarea.style.width=this._charSizeService.width+"px",this.textarea.style.height=e+"px",this.textarea.style.lineHeight=e+"px",this.textarea.style.zIndex="-5"}},t.prototype._initGlobal=function(){var e=this;this._bindKeys(),this.register(d.addDisposableDomListener(this.element,"copy",(function(t){e.hasSelection()&&a.copyHandler(t,e._selectionService)})));var t=function(t){return a.handlePasteEvent(t,e.textarea,e._coreService)};this.register(d.addDisposableDomListener(this.textarea,"paste",t)),this.register(d.addDisposableDomListener(this.element,"paste",t)),_.isFirefox?this.register(d.addDisposableDomListener(this.element,"mousedown",(function(t){2===t.button&&a.rightClickHandler(t,e.textarea,e.screenElement,e._selectionService,e.options.rightClickSelectsWord)}))):this.register(d.addDisposableDomListener(this.element,"contextmenu",(function(t){a.rightClickHandler(t,e.textarea,e.screenElement,e._selectionService,e.options.rightClickSelectsWord)}))),_.isLinux&&this.register(d.addDisposableDomListener(this.element,"auxclick",(function(t){1===t.button&&a.moveTextAreaUnderMouseCursor(t,e.textarea,e.screenElement)})))},t.prototype._bindKeys=function(){var e=this;this.register(d.addDisposableDomListener(this.textarea,"keyup",(function(t){return e._keyUp(t)}),!0)),this.register(d.addDisposableDomListener(this.textarea,"keydown",(function(t){return e._keyDown(t)}),!0)),this.register(d.addDisposableDomListener(this.textarea,"keypress",(function(t){return e._keyPress(t)}),!0)),this.register(d.addDisposableDomListener(this.textarea,"compositionstart",(function(){return e._compositionHelper.compositionstart()}))),this.register(d.addDisposableDomListener(this.textarea,"compositionupdate",(function(t){return e._compositionHelper.compositionupdate(t)}))),this.register(d.addDisposableDomListener(this.textarea,"compositionend",(function(){return e._compositionHelper.compositionend()}))),this.register(this.onRender((function(){return e._compositionHelper.updateCompositionElements()}))),this.register(this.onRender((function(t){return e._queueLinkification(t.start,t.end)})))},t.prototype.open=function(e){var t=this;if(!e)throw new Error("Terminal requires a parent element.");T.body.contains(e)||this._logService.debug("Terminal.open was called on an element that was not attached to the DOM"),this._document=e.ownerDocument,this.element=this._document.createElement("div"),this.element.dir="ltr",this.element.classList.add("terminal"),this.element.classList.add("xterm"),this.element.setAttribute("tabindex","0"),e.appendChild(this.element);var r=T.createDocumentFragment();this._viewportElement=T.createElement("div"),this._viewportElement.classList.add("xterm-viewport"),r.appendChild(this._viewportElement),this._viewportScrollArea=T.createElement("div"),this._viewportScrollArea.classList.add("xterm-scroll-area"),this._viewportElement.appendChild(this._viewportScrollArea),this.screenElement=T.createElement("div"),this.screenElement.classList.add("xterm-screen"),this._helperContainer=T.createElement("div"),this._helperContainer.classList.add("xterm-helpers"),this.screenElement.appendChild(this._helperContainer),r.appendChild(this.screenElement),this.textarea=T.createElement("textarea"),this.textarea.classList.add("xterm-helper-textarea"),this.textarea.setAttribute("aria-label",p.promptLabel),this.textarea.setAttribute("aria-multiline","false"),this.textarea.setAttribute("autocorrect","off"),this.textarea.setAttribute("autocapitalize","off"),this.textarea.setAttribute("spellcheck","false"),this.textarea.tabIndex=0,this.register(d.addDisposableDomListener(this.textarea,"focus",(function(e){return t._onTextAreaFocus(e)}))),this.register(d.addDisposableDomListener(this.textarea,"blur",(function(){return t._onTextAreaBlur()}))),this._helperContainer.appendChild(this.textarea);var i=this._instantiationService.createInstance(x.CoreBrowserService,this.textarea);this._instantiationService.setService(L.ICoreBrowserService,i),this._charSizeService=this._instantiationService.createInstance(A.CharSizeService,this._document,this._helperContainer),this._instantiationService.setService(L.ICharSizeService,this._charSizeService),this._compositionView=T.createElement("div"),this._compositionView.classList.add("composition-view"),this._compositionHelper=this._instantiationService.createInstance(o.CompositionHelper,this.textarea,this._compositionView),this._helperContainer.appendChild(this._compositionView),this.element.appendChild(r),this._theme=this.options.theme||this._theme,this._colorManager=new w.ColorManager(T,this.options.allowTransparency),this.register(this.optionsService.onOptionChange((function(e){return t._colorManager.onOptionsChange(e)}))),this._colorManager.setTheme(this._theme);var n=this._createRenderer();this._renderService=this.register(this._instantiationService.createInstance(E.RenderService,n,this.rows,this.screenElement)),this._instantiationService.setService(L.IRenderService,this._renderService),this.register(this._renderService.onRenderedBufferChange((function(e){return t._onRender.fire(e)}))),this.onResize((function(e){return t._renderService.resize(e.cols,e.rows)})),this._soundService=this._instantiationService.createInstance(v.SoundService),this._instantiationService.setService(L.ISoundService,this._soundService),this._mouseService=this._instantiationService.createInstance(R.MouseService),this._instantiationService.setService(L.IMouseService,this._mouseService),this.viewport=this._instantiationService.createInstance(s.Viewport,(function(e,r){return t.scrollLines(e,r)}),this._viewportElement,this._viewportScrollArea),this.viewport.onThemeChange(this._colorManager.colors),this.register(this._inputHandler.onRequestSyncScrollBar((function(){return t.viewport.syncScrollArea()}))),this.register(this.viewport),this.register(this.onCursorMove((function(){t._renderService.onCursorMove(),t._syncTextArea()}))),this.register(this.onResize((function(){return t._renderService.onResize(t.cols,t.rows)}))),this.register(this.onBlur((function(){return t._renderService.onBlur()}))),this.register(this.onFocus((function(){return t._renderService.onFocus()}))),this.register(this._renderService.onDimensionsChange((function(){return t.viewport.syncScrollArea()}))),this._selectionService=this.register(this._instantiationService.createInstance(f.SelectionService,this.element,this.screenElement)),this._instantiationService.setService(L.ISelectionService,this._selectionService),this.register(this._selectionService.onRequestScrollLines((function(e){return t.scrollLines(e.amount,e.suppressScrollEvent)}))),this.register(this._selectionService.onSelectionChange((function(){return t._onSelectionChange.fire()}))),this.register(this._selectionService.onRequestRedraw((function(e){return t._renderService.onSelectionChanged(e.start,e.end,e.columnSelectMode)}))),this.register(this._selectionService.onLinuxMouseSelection((function(e){t.textarea.value=e,t.textarea.focus(),t.textarea.select()}))),this.register(this.onScroll((function(){t.viewport.syncScrollArea(),t._selectionService.refresh()}))),this.register(d.addDisposableDomListener(this._viewportElement,"scroll",(function(){return t._selectionService.refresh()}))),this._mouseZoneManager=this._instantiationService.createInstance(g.MouseZoneManager,this.element,this.screenElement),this.register(this._mouseZoneManager),this.register(this.onScroll((function(){return t._mouseZoneManager.clearAll()}))),this.linkifier.attachToDom(this.element,this._mouseZoneManager),this.linkifier2.attachToDom(this.element,this._mouseService,this._renderService),this.register(d.addDisposableDomListener(this.element,"mousedown",(function(e){return t._selectionService.onMouseDown(e)}))),this._coreMouseService.areMouseEventsActive?(this._selectionService.disable(),this.element.classList.add("enable-mouse-events")):this._selectionService.enable(),this.options.screenReaderMode&&(this._accessibilityManager=new y.AccessibilityManager(this,this._renderService)),this._charSizeService.measure(),this.refresh(0,this.rows-1),this._initGlobal(),this.bindMouse()},t.prototype._createRenderer=function(){switch(this.options.rendererType){case"canvas":return this._instantiationService.createInstance(h.Renderer,this._colorManager.colors,this.screenElement,this.linkifier,this.linkifier2);case"dom":return this._instantiationService.createInstance(b.DomRenderer,this._colorManager.colors,this.element,this.screenElement,this._viewportElement,this.linkifier,this.linkifier2);default:throw new Error('Unrecognized rendererType "'+this.options.rendererType+'"')}},t.prototype._setTheme=function(e){var t,r,i;this._theme=e,null===(t=this._colorManager)||void 0===t||t.setTheme(e),null===(r=this._renderService)||void 0===r||r.setColors(this._colorManager.colors),null===(i=this.viewport)||void 0===i||i.onThemeChange(this._colorManager.colors)},t.prototype.bindMouse=function(){var e=this,t=this,r=this.element;function i(e){var r,i,n=t._mouseService.getRawByteCoords(e,t.screenElement,t.cols,t.rows);if(!n)return!1;switch(e.overrideType||e.type){case"mousemove":i=32,void 0===e.buttons?(r=3,void 0!==e.button&&(r=e.button<3?e.button:3)):r=1&e.buttons?0:4&e.buttons?1:2&e.buttons?2:3;break;case"mouseup":i=0,r=e.button<3?e.button:3;break;case"mousedown":i=1,r=e.button<3?e.button:3;break;case"wheel":0!==e.deltaY&&(i=e.deltaY<0?0:1),r=4;break;default:return!1}return!(void 0===i||void 0===r||r>4)&&t._coreMouseService.triggerMouseEvent({col:n.x-33,row:n.y-33,button:r,action:i,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey})}var n={mouseup:null,wheel:null,mousedrag:null,mousemove:null},o=function(t){return i(t),t.buttons||(e._document.removeEventListener("mouseup",n.mouseup),n.mousedrag&&e._document.removeEventListener("mousemove",n.mousedrag)),e.cancel(t)},s=function(t){return i(t),t.preventDefault(),e.cancel(t)},a=function(e){e.buttons&&i(e)},l=function(e){e.buttons||i(e)};this.register(this._coreMouseService.onProtocolChange((function(t){t?("debug"===e.optionsService.options.logLevel&&e._logService.debug("Binding to mouse events:",e._coreMouseService.explainEvents(t)),e.element.classList.add("enable-mouse-events"),e._selectionService.disable()):(e._logService.debug("Unbinding from mouse events."),e.element.classList.remove("enable-mouse-events"),e._selectionService.enable()),8&t?n.mousemove||(r.addEventListener("mousemove",l),n.mousemove=l):(r.removeEventListener("mousemove",n.mousemove),n.mousemove=null),16&t?n.wheel||(r.addEventListener("wheel",s,{passive:!1}),n.wheel=s):(r.removeEventListener("wheel",n.wheel),n.wheel=null),2&t?n.mouseup||(n.mouseup=o):(e._document.removeEventListener("mouseup",n.mouseup),n.mouseup=null),4&t?n.mousedrag||(n.mousedrag=a):(e._document.removeEventListener("mousemove",n.mousedrag),n.mousedrag=null)}))),this._coreMouseService.activeProtocol=this._coreMouseService.activeProtocol,this.register(d.addDisposableDomListener(r,"mousedown",(function(t){if(t.preventDefault(),e.focus(),e._coreMouseService.areMouseEventsActive&&!e._selectionService.shouldForceSelection(t))return i(t),n.mouseup&&e._document.addEventListener("mouseup",n.mouseup),n.mousedrag&&e._document.addEventListener("mousemove",n.mousedrag),e.cancel(t)}))),this.register(d.addDisposableDomListener(r,"wheel",(function(t){if(n.wheel);else if(!e.buffer.hasScrollback){var r=e.viewport.getLinesScrolled(t);if(0===r)return;for(var i=c.C0.ESC+(e._coreService.decPrivateModes.applicationCursorKeys?"O":"[")+(t.deltaY<0?"A":"B"),o="",s=0;s47)},t.prototype._keyUp=function(e){this._customKeyEventHandler&&!1===this._customKeyEventHandler(e)||(function(e){return 16===e.keyCode||17===e.keyCode||18===e.keyCode}(e)||this.focus(),this.updateCursorStyle(e))},t.prototype._keyPress=function(e){var t;if(this._keyDownHandled)return!1;if(this._customKeyEventHandler&&!1===this._customKeyEventHandler(e))return!1;if(this.cancel(e),e.charCode)t=e.charCode;else if(null===e.which||void 0===e.which)t=e.keyCode;else{if(0===e.which||0===e.charCode)return!1;t=e.which}return!(!t||(e.altKey||e.ctrlKey||e.metaKey)&&!this._isThirdLevelShift(this.browser,e))&&(t=String.fromCharCode(t),this._onKey.fire({key:t,domEvent:e}),this._showCursor(),this._coreService.triggerDataEvent(t,!0),!0)},t.prototype.bell=function(){this._soundBell()&&this._soundService.playBellSound()},t.prototype.resize=function(t,r){t!==this.cols||r!==this.rows?e.prototype.resize.call(this,t,r):this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure()},t.prototype._afterResize=function(e,t){var r,i;null===(r=this._charSizeService)||void 0===r||r.measure(),null===(i=this.viewport)||void 0===i||i.syncScrollArea(!0)},t.prototype.clear=function(){if(0!==this.buffer.ybase||0!==this.buffer.y){this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(var e=1;e=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CompositionHelper=void 0;var o=r(5),s=r(1),a=function(){function e(e,t,r,i,n,o){this._textarea=e,this._compositionView=t,this._bufferService=r,this._optionsService=i,this._charSizeService=n,this._coreService=o,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0}}return Object.defineProperty(e.prototype,"isComposing",{get:function(){return this._isComposing},enumerable:!1,configurable:!0}),e.prototype.compositionstart=function(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent="",this._compositionView.classList.add("active")},e.prototype.compositionupdate=function(e){var t=this;this._compositionView.textContent=e.data,this.updateCompositionElements(),setTimeout((function(){t._compositionPosition.end=t._textarea.value.length}),0)},e.prototype.compositionend=function(){this._finalizeComposition(!0)},e.prototype.keydown=function(e){if(this._isComposing||this._isSendingComposition){if(229===e.keyCode)return!1;if(16===e.keyCode||17===e.keyCode||18===e.keyCode)return!1;this._finalizeComposition(!1)}return 229!==e.keyCode||(this._handleAnyTextareaChanges(),!1)},e.prototype._finalizeComposition=function(e){var t=this;if(this._compositionView.classList.remove("active"),this._isComposing=!1,e){var r={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout((function(){if(t._isSendingComposition){t._isSendingComposition=!1;var e=void 0;e=t._isComposing?t._textarea.value.substring(r.start,r.end):t._textarea.value.substring(r.start),t._coreService.triggerDataEvent(e,!0)}}),0)}else{this._isSendingComposition=!1;var i=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(i,!0)}},e.prototype._handleAnyTextareaChanges=function(){var e=this,t=this._textarea.value;setTimeout((function(){if(!e._isComposing){var r=e._textarea.value.replace(t,"");r.length>0&&e._coreService.triggerDataEvent(r,!0)}}),0)},e.prototype.updateCompositionElements=function(e){var t=this;if(this._isComposing){if(this._bufferService.buffer.isCursorInViewport){var r=Math.ceil(this._charSizeService.height*this._optionsService.options.lineHeight),i=this._bufferService.buffer.y*r,n=this._bufferService.buffer.x*this._charSizeService.width;this._compositionView.style.left=n+"px",this._compositionView.style.top=i+"px",this._compositionView.style.height=r+"px",this._compositionView.style.lineHeight=r+"px",this._compositionView.style.fontFamily=this._optionsService.options.fontFamily,this._compositionView.style.fontSize=this._optionsService.options.fontSize+"px";var o=this._compositionView.getBoundingClientRect();this._textarea.style.left=n+"px",this._textarea.style.top=i+"px",this._textarea.style.width=o.width+"px",this._textarea.style.height=o.height+"px",this._textarea.style.lineHeight=o.height+"px"}e||setTimeout((function(){return t.updateCompositionElements(!0)}),0)}},e=i([n(2,s.IBufferService),n(3,s.IOptionsService),n(4,o.ICharSizeService),n(5,s.ICoreService)],e)}();t.CompositionHelper=a},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),o=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Viewport=void 0;var a=r(2),c=r(7),l=r(5),h=r(1),u=function(e){function t(t,r,i,n,o,s,a){var l=e.call(this)||this;return l._scrollLines=t,l._viewportElement=r,l._scrollArea=i,l._bufferService=n,l._optionsService=o,l._charSizeService=s,l._renderService=a,l.scrollBarWidth=0,l._currentRowHeight=0,l._lastRecordedBufferLength=0,l._lastRecordedViewportHeight=0,l._lastRecordedBufferHeight=0,l._lastTouchY=0,l._lastScrollTop=0,l._wheelPartialScroll=0,l._refreshAnimationFrame=null,l._ignoreNextScrollEvent=!1,l.scrollBarWidth=l._viewportElement.offsetWidth-l._scrollArea.offsetWidth||15,l.register(c.addDisposableDomListener(l._viewportElement,"scroll",l._onScroll.bind(l))),setTimeout((function(){return l.syncScrollArea()}),0),l}return n(t,e),t.prototype.onThemeChange=function(e){this._viewportElement.style.backgroundColor=e.background.css},t.prototype._refresh=function(e){var t=this;if(e)return this._innerRefresh(),void(null!==this._refreshAnimationFrame&&cancelAnimationFrame(this._refreshAnimationFrame));null===this._refreshAnimationFrame&&(this._refreshAnimationFrame=requestAnimationFrame((function(){return t._innerRefresh()})))},t.prototype._innerRefresh=function(){if(this._charSizeService.height>0){this._currentRowHeight=this._renderService.dimensions.scaledCellHeight/window.devicePixelRatio,this._lastRecordedViewportHeight=this._viewportElement.offsetHeight;var e=Math.round(this._currentRowHeight*this._lastRecordedBufferLength)+(this._lastRecordedViewportHeight-this._renderService.dimensions.canvasHeight);this._lastRecordedBufferHeight!==e&&(this._lastRecordedBufferHeight=e,this._scrollArea.style.height=this._lastRecordedBufferHeight+"px")}var t=this._bufferService.buffer.ydisp*this._currentRowHeight;this._viewportElement.scrollTop!==t&&(this._ignoreNextScrollEvent=!0,this._viewportElement.scrollTop=t),this._refreshAnimationFrame=null},t.prototype.syncScrollArea=function(e){if(void 0===e&&(e=!1),this._lastRecordedBufferLength!==this._bufferService.buffer.lines.length)return this._lastRecordedBufferLength=this._bufferService.buffer.lines.length,void this._refresh(e);if(this._lastRecordedViewportHeight===this._renderService.dimensions.canvasHeight){var t=this._bufferService.buffer.ydisp*this._currentRowHeight;this._lastScrollTop===t&&this._lastScrollTop===this._viewportElement.scrollTop&&this._renderService.dimensions.scaledCellHeight/window.devicePixelRatio===this._currentRowHeight||this._refresh(e)}else this._refresh(e)},t.prototype._onScroll=function(e){if(this._lastScrollTop=this._viewportElement.scrollTop,this._viewportElement.offsetParent)if(this._ignoreNextScrollEvent)this._ignoreNextScrollEvent=!1;else{var t=Math.round(this._lastScrollTop/this._currentRowHeight)-this._bufferService.buffer.ydisp;this._scrollLines(t,!0)}},t.prototype._bubbleScroll=function(e,t){var r=this._viewportElement.scrollTop+this._lastRecordedViewportHeight;return!(t<0&&0!==this._viewportElement.scrollTop||t>0&&r0?1:-1),this._wheelPartialScroll%=1):e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(t*=this._bufferService.rows),t},t.prototype._applyScrollModifier=function(e,t){var r=this._optionsService.options.fastScrollModifier;return"alt"===r&&t.altKey||"ctrl"===r&&t.ctrlKey||"shift"===r&&t.shiftKey?e*this._optionsService.options.fastScrollSensitivity*this._optionsService.options.scrollSensitivity:e*this._optionsService.options.scrollSensitivity},t.prototype.onTouchStart=function(e){this._lastTouchY=e.touches[0].pageY},t.prototype.onTouchMove=function(e){var t=this._lastTouchY-e.touches[0].pageY;return this._lastTouchY=e.touches[0].pageY,0!==t&&(this._viewportElement.scrollTop+=t,this._bubbleScroll(e,t))},t=o([s(3,h.IBufferService),s(4,h.IOptionsService),s(5,l.ICharSizeService),s(6,l.IRenderService)],t)}(a.Disposable);t.Viewport=u},function(e,t,r){"use strict";function i(e){return e.replace(/\r?\n/g,"\r")}function n(e,t){return t?"[200~"+e+"[201~":e}function o(e,t,r){e=n(e=i(e),r.decPrivateModes.bracketedPasteMode),r.triggerDataEvent(e,!0),t.value=""}function s(e,t,r){var i=r.getBoundingClientRect(),n=e.clientX-i.left-10,o=e.clientY-i.top-10;t.style.width="20px",t.style.height="20px",t.style.left=n+"px",t.style.top=o+"px",t.style.zIndex="1000",t.focus()}Object.defineProperty(t,"__esModule",{value:!0}),t.rightClickHandler=t.moveTextAreaUnderMouseCursor=t.paste=t.handlePasteEvent=t.copyHandler=t.bracketTextForPaste=t.prepareTextForTerminal=void 0,t.prepareTextForTerminal=i,t.bracketTextForPaste=n,t.copyHandler=function(e,t){e.clipboardData&&e.clipboardData.setData("text/plain",t.selectionText),e.preventDefault()},t.handlePasteEvent=function(e,t,r){e.stopPropagation(),e.clipboardData&&o(e.clipboardData.getData("text/plain"),t,r)},t.paste=o,t.moveTextAreaUnderMouseCursor=s,t.rightClickHandler=function(e,t,r,i,n){s(e,t,r),n&&!i.isClickInSelection(e)&&i.selectWordAtCursor(e),t.value=i.selectionText,t.select()}},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.EscapeSequenceParser=t.VT500_TRANSITION_TABLE=t.TransitionTable=void 0;var o=r(2),s=r(15),a=r(21),c=r(22),l=r(24),h=function(){function e(e){this.table=new Uint8Array(e)}return e.prototype.setDefault=function(e,t){s.fill(this.table,e<<4|t)},e.prototype.add=function(e,t,r,i){this.table[t<<8|e]=r<<4|i},e.prototype.addMany=function(e,t,r,i){for(var n=0;n1)throw new Error("only one byte as prefix supported");if((r=e.prefix.charCodeAt(0))&&60>r||r>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(e.intermediates){if(e.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(var i=0;in||n>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");r<<=8,r|=n}}if(1!==e.final.length)throw new Error("final must be a single byte");var o=e.final.charCodeAt(0);if(t[0]>o||o>t[1])throw new Error("final must be in range "+t[0]+" .. "+t[1]);return r<<=8,r|=o},r.prototype.identToString=function(e){for(var t=[];e;)t.push(String.fromCharCode(255&e)),e>>=8;return t.reverse().join("")},r.prototype.dispose=function(){this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null),this._oscParser.dispose(),this._dcsParser.dispose()},r.prototype.setPrintHandler=function(e){this._printHandler=e},r.prototype.clearPrintHandler=function(){this._printHandler=this._printHandlerFb},r.prototype.addEscHandler=function(e,t){var r=this._identifier(e,[48,126]);void 0===this._escHandlers[r]&&(this._escHandlers[r]=[]);var i=this._escHandlers[r];return i.push(t),{dispose:function(){var e=i.indexOf(t);-1!==e&&i.splice(e,1)}}},r.prototype.setEscHandler=function(e,t){this._escHandlers[this._identifier(e,[48,126])]=[t]},r.prototype.clearEscHandler=function(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]},r.prototype.setEscHandlerFallback=function(e){this._escHandlerFb=e},r.prototype.setExecuteHandler=function(e,t){this._executeHandlers[e.charCodeAt(0)]=t},r.prototype.clearExecuteHandler=function(e){this._executeHandlers[e.charCodeAt(0)]&&delete this._executeHandlers[e.charCodeAt(0)]},r.prototype.setExecuteHandlerFallback=function(e){this._executeHandlerFb=e},r.prototype.addCsiHandler=function(e,t){var r=this._identifier(e);void 0===this._csiHandlers[r]&&(this._csiHandlers[r]=[]);var i=this._csiHandlers[r];return i.push(t),{dispose:function(){var e=i.indexOf(t);-1!==e&&i.splice(e,1)}}},r.prototype.setCsiHandler=function(e,t){this._csiHandlers[this._identifier(e)]=[t]},r.prototype.clearCsiHandler=function(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]},r.prototype.setCsiHandlerFallback=function(e){this._csiHandlerFb=e},r.prototype.addDcsHandler=function(e,t){return this._dcsParser.addHandler(this._identifier(e),t)},r.prototype.setDcsHandler=function(e,t){this._dcsParser.setHandler(this._identifier(e),t)},r.prototype.clearDcsHandler=function(e){this._dcsParser.clearHandler(this._identifier(e))},r.prototype.setDcsHandlerFallback=function(e){this._dcsParser.setHandlerFallback(e)},r.prototype.addOscHandler=function(e,t){return this._oscParser.addHandler(e,t)},r.prototype.setOscHandler=function(e,t){this._oscParser.setHandler(e,t)},r.prototype.clearOscHandler=function(e){this._oscParser.clearHandler(e)},r.prototype.setOscHandlerFallback=function(e){this._oscParser.setHandlerFallback(e)},r.prototype.setErrorHandler=function(e){this._errorHandler=e},r.prototype.clearErrorHandler=function(){this._errorHandler=this._errorHandlerFb},r.prototype.reset=function(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingCodepoint=0},r.prototype.parse=function(e,t){for(var r=0,i=0,n=this.currentState,o=this._oscParser,s=this._dcsParser,a=this._collect,c=this._params,l=this._transitions.table,h=0;h>4){case 2:for(var u=h+1;;++u){if(u>=t||(r=e[u])<32||r>126&&r<160){this._printHandler(e,h,u),h=u-1;break}if(++u>=t||(r=e[u])<32||r>126&&r<160){this._printHandler(e,h,u),h=u-1;break}if(++u>=t||(r=e[u])<32||r>126&&r<160){this._printHandler(e,h,u),h=u-1;break}if(++u>=t||(r=e[u])<32||r>126&&r<160){this._printHandler(e,h,u),h=u-1;break}}break;case 3:this._executeHandlers[r]?this._executeHandlers[r]():this._executeHandlerFb(r),this.precedingCodepoint=0;break;case 0:break;case 1:if(this._errorHandler({position:h,code:r,currentState:n,collect:a,params:c,abort:!1}).abort)return;break;case 7:for(var f=this._csiHandlers[a<<8|r],_=f?f.length-1:-1;_>=0&&!1===f[_](c);_--);_<0&&this._csiHandlerFb(a<<8|r,c),this.precedingCodepoint=0;break;case 8:do{switch(r){case 59:c.addParam(0);break;case 58:c.addSubParam(-1);break;default:c.addDigit(r-48)}}while(++h47&&r<60);h--;break;case 9:a<<=8,a|=r;break;case 10:for(var d=this._escHandlers[a<<8|r],p=d?d.length-1:-1;p>=0&&!1===d[p]();p--);p<0&&this._escHandlerFb(a<<8|r),this.precedingCodepoint=0;break;case 11:c.reset(),c.addParam(0),a=0;break;case 12:s.hook(a<<8|r,c);break;case 13:for(var v=h+1;;++v)if(v>=t||24===(r=e[v])||26===r||27===r||r>127&&r<160){s.put(e,h,v),h=v-1;break}break;case 14:s.unhook(24!==r&&26!==r),27===r&&(i|=1),c.reset(),c.addParam(0),a=0,this.precedingCodepoint=0;break;case 4:o.start();break;case 5:for(var g=h+1;;g++)if(g>=t||(r=e[g])<32||r>127&&r<=159){o.put(e,h,g),h=g-1;break}break;case 6:o.end(24!==r&&26!==r),27===r&&(i|=1),c.reset(),c.addParam(0),a=0,this.precedingCodepoint=0}n=15&i}this._collect=a,this.currentState=n},r}(o.Disposable);t.EscapeSequenceParser=u},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),o=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Renderer=void 0;var a=r(41),c=r(47),l=r(48),h=r(49),u=r(29),f=r(2),_=r(5),d=r(1),p=r(25),v=r(0),g=1,y=function(e){function t(t,r,i,n,o,s,f,_,d){var p=e.call(this)||this;p._colors=t,p._screenElement=r,p._bufferService=o,p._charSizeService=s,p._optionsService=f,p._id=g++,p._onRequestRedraw=new v.EventEmitter;var y=p._optionsService.options.allowTransparency;return p._characterJoinerRegistry=new u.CharacterJoinerRegistry(p._bufferService),p._renderLayers=[new a.TextRenderLayer(p._screenElement,0,p._colors,p._characterJoinerRegistry,y,p._id,p._bufferService,f),new c.SelectionRenderLayer(p._screenElement,1,p._colors,p._id,p._bufferService,f),new h.LinkRenderLayer(p._screenElement,2,p._colors,p._id,i,n,p._bufferService,f),new l.CursorRenderLayer(p._screenElement,3,p._colors,p._id,p._onRequestRedraw,p._bufferService,f,_,d)],p.dimensions={scaledCharWidth:0,scaledCharHeight:0,scaledCellWidth:0,scaledCellHeight:0,scaledCharLeft:0,scaledCharTop:0,scaledCanvasWidth:0,scaledCanvasHeight:0,canvasWidth:0,canvasHeight:0,actualCellWidth:0,actualCellHeight:0},p._devicePixelRatio=window.devicePixelRatio,p._updateDimensions(),p.onOptionsChanged(),p}return n(t,e),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return this._onRequestRedraw.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){for(var t=0,r=this._renderLayers;t0&&h===a[0][0]){f=!0;var d=a.shift();u=new l.JoinedCellData(this._workCell,s.translateToString(!0,d[0],d[1]),d[1]-d[0]),_=d[1]-1}!f&&this._isOverlapping(u)&&_this._characterWidth;return this._ctx.restore(),this._characterOverlapCache[t]=r,r},t}(s.BaseRenderLayer);t.TextRenderLayer=u},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.GridCache=void 0;var i=function(){function e(){this.cache=[]}return e.prototype.resize=function(e,t){for(var r=0;r>>24,n=t.rgba>>>16&255,o=t.rgba>>>8&255,s=0;s=this.capacity)r=this._head,this._unlinkNode(r),delete this._map[r.key],r.key=e,r.value=t,this._map[e]=r;else{var i=this._nodePool;i.length>0?((r=i.pop()).key=e,r.value=t):r={prev:null,next:null,key:e,value:t},this._map[e]=r,this.size++}this._appendNode(r)},e}();t.LRUMap=i},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionRenderLayer=void 0;var o=function(e){function t(t,r,i,n,o,s){var a=e.call(this,t,"selection",r,!0,i,n,o,s)||this;return a._clearState(),a}return n(t,e),t.prototype._clearState=function(){this._state={start:void 0,end:void 0,columnSelectMode:void 0,ydisp:void 0}},t.prototype.resize=function(t){e.prototype.resize.call(this,t),this._clearState()},t.prototype.reset=function(){this._state.start&&this._state.end&&(this._clearState(),this._clearAll())},t.prototype.onSelectionChanged=function(e,t,r){if(this._didStateChange(e,t,r,this._bufferService.buffer.ydisp))if(this._clearAll(),e&&t){var i=e[1]-this._bufferService.buffer.ydisp,n=t[1]-this._bufferService.buffer.ydisp,o=Math.max(i,0),s=Math.min(n,this._bufferService.rows-1);if(!(o>=this._bufferService.rows||s<0)){if(this._ctx.fillStyle=this._colors.selectionTransparent.css,r){var a=e[0],c=t[0]-a,l=s-o+1;this._fillCells(a,o,c,l)}else{a=i===o?e[0]:0;var h=o===s?t[0]:this._bufferService.cols;this._fillCells(a,o,h-a,1);var u=Math.max(s-o-1,0);if(this._fillCells(0,o+1,this._bufferService.cols,u),o!==s){var f=n===s?t[0]:this._bufferService.cols;this._fillCells(0,s,f,1)}}this._state.start=[e[0],e[1]],this._state.end=[t[0],t[1]],this._state.columnSelectMode=r,this._state.ydisp=this._bufferService.buffer.ydisp}}else this._clearState()},t.prototype._didStateChange=function(e,t,r,i){return!this._areCoordinatesEqual(e,this._state.start)||!this._areCoordinatesEqual(t,this._state.end)||r!==this._state.columnSelectMode||i!==this._state.ydisp},t.prototype._areCoordinatesEqual=function(e,t){return!(!e||!t)&&(e[0]===t[0]&&e[1]===t[1])},t}(r(13).BaseRenderLayer);t.SelectionRenderLayer=o},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.CursorRenderLayer=void 0;var o=r(13),s=r(4),a=function(e){function t(t,r,i,n,o,a,c,l,h){var u=e.call(this,t,"cursor",r,!0,i,n,a,c)||this;return u._onRequestRedraw=o,u._coreService=l,u._coreBrowserService=h,u._cell=new s.CellData,u._state={x:0,y:0,isFocused:!1,style:"",width:0},u._cursorRenderers={bar:u._renderBarCursor.bind(u),block:u._renderBlockCursor.bind(u),underline:u._renderUnderlineCursor.bind(u)},u}return n(t,e),t.prototype.resize=function(t){e.prototype.resize.call(this,t),this._state={x:0,y:0,isFocused:!1,style:"",width:0}},t.prototype.reset=function(){this._clearCursor(),this._cursorBlinkStateManager&&(this._cursorBlinkStateManager.dispose(),this._cursorBlinkStateManager=void 0,this.onOptionsChanged())},t.prototype.onBlur=function(){this._cursorBlinkStateManager&&this._cursorBlinkStateManager.pause(),this._onRequestRedraw.fire({start:this._bufferService.buffer.y,end:this._bufferService.buffer.y})},t.prototype.onFocus=function(){this._cursorBlinkStateManager?this._cursorBlinkStateManager.resume():this._onRequestRedraw.fire({start:this._bufferService.buffer.y,end:this._bufferService.buffer.y})},t.prototype.onOptionsChanged=function(){var e,t=this;this._optionsService.options.cursorBlink?this._cursorBlinkStateManager||(this._cursorBlinkStateManager=new c(this._coreBrowserService.isFocused,(function(){t._render(!0)}))):(null===(e=this._cursorBlinkStateManager)||void 0===e||e.dispose(),this._cursorBlinkStateManager=void 0),this._onRequestRedraw.fire({start:this._bufferService.buffer.y,end:this._bufferService.buffer.y})},t.prototype.onCursorMove=function(){this._cursorBlinkStateManager&&this._cursorBlinkStateManager.restartBlinkAnimation()},t.prototype.onGridChanged=function(e,t){!this._cursorBlinkStateManager||this._cursorBlinkStateManager.isPaused?this._render(!1):this._cursorBlinkStateManager.restartBlinkAnimation()},t.prototype._render=function(e){if(this._coreService.isCursorInitialized&&!this._coreService.isCursorHidden){var t=this._bufferService.buffer.ybase+this._bufferService.buffer.y,r=t-this._bufferService.buffer.ydisp;if(r<0||r>=this._bufferService.rows)this._clearCursor();else{var i=Math.min(this._bufferService.buffer.x,this._bufferService.cols-1);if(this._bufferService.buffer.lines.get(t).loadCell(i,this._cell),void 0!==this._cell.content){if(!this._coreBrowserService.isFocused){this._clearCursor(),this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css;var n=this._optionsService.options.cursorStyle;return n&&"block"!==n?this._cursorRenderers[n](i,r,this._cell):this._renderBlurCursor(i,r,this._cell),this._ctx.restore(),this._state.x=i,this._state.y=r,this._state.isFocused=!1,this._state.style=n,void(this._state.width=this._cell.getWidth())}if(!this._cursorBlinkStateManager||this._cursorBlinkStateManager.isCursorVisible){if(this._state){if(this._state.x===i&&this._state.y===r&&this._state.isFocused===this._coreBrowserService.isFocused&&this._state.style===this._optionsService.options.cursorStyle&&this._state.width===this._cell.getWidth())return;this._clearCursor()}this._ctx.save(),this._cursorRenderers[this._optionsService.options.cursorStyle||"block"](i,r,this._cell),this._ctx.restore(),this._state.x=i,this._state.y=r,this._state.isFocused=!1,this._state.style=this._optionsService.options.cursorStyle,this._state.width=this._cell.getWidth()}else this._clearCursor()}}}else this._clearCursor()},t.prototype._clearCursor=function(){this._state&&(this._clearCells(this._state.x,this._state.y,this._state.width,1),this._state={x:0,y:0,isFocused:!1,style:"",width:0})},t.prototype._renderBarCursor=function(e,t,r){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillLeftLineAtCell(e,t,this._optionsService.options.cursorWidth),this._ctx.restore()},t.prototype._renderBlockCursor=function(e,t,r){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillCells(e,t,r.getWidth(),1),this._ctx.fillStyle=this._colors.cursorAccent.css,this._fillCharTrueColor(r,e,t),this._ctx.restore()},t.prototype._renderUnderlineCursor=function(e,t,r){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillBottomLineAtCells(e,t),this._ctx.restore()},t.prototype._renderBlurCursor=function(e,t,r){this._ctx.save(),this._ctx.strokeStyle=this._colors.cursor.css,this._strokeRectAtCell(e,t,r.getWidth(),1),this._ctx.restore()},t}(o.BaseRenderLayer);t.CursorRenderLayer=a;var c=function(){function e(e,t){this._renderCallback=t,this.isCursorVisible=!0,e&&this._restartInterval()}return Object.defineProperty(e.prototype,"isPaused",{get:function(){return!(this._blinkStartTimeout||this._blinkInterval)},enumerable:!1,configurable:!0}),e.prototype.dispose=function(){this._blinkInterval&&(window.clearInterval(this._blinkInterval),this._blinkInterval=void 0),this._blinkStartTimeout&&(window.clearTimeout(this._blinkStartTimeout),this._blinkStartTimeout=void 0),this._animationFrame&&(window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)},e.prototype.restartBlinkAnimation=function(){var e=this;this.isPaused||(this._animationTimeRestarted=Date.now(),this.isCursorVisible=!0,this._animationFrame||(this._animationFrame=window.requestAnimationFrame((function(){e._renderCallback(),e._animationFrame=void 0}))))},e.prototype._restartInterval=function(e){var t=this;void 0===e&&(e=600),this._blinkInterval&&window.clearInterval(this._blinkInterval),this._blinkStartTimeout=window.setTimeout((function(){if(t._animationTimeRestarted){var e=600-(Date.now()-t._animationTimeRestarted);if(t._animationTimeRestarted=void 0,e>0)return void t._restartInterval(e)}t.isCursorVisible=!1,t._animationFrame=window.requestAnimationFrame((function(){t._renderCallback(),t._animationFrame=void 0})),t._blinkInterval=window.setInterval((function(){if(t._animationTimeRestarted){var e=600-(Date.now()-t._animationTimeRestarted);return t._animationTimeRestarted=void 0,void t._restartInterval(e)}t.isCursorVisible=!t.isCursorVisible,t._animationFrame=window.requestAnimationFrame((function(){t._renderCallback(),t._animationFrame=void 0}))}),600)}),e)},e.prototype.pause=function(){this.isCursorVisible=!0,this._blinkInterval&&(window.clearInterval(this._blinkInterval),this._blinkInterval=void 0),this._blinkStartTimeout&&(window.clearTimeout(this._blinkStartTimeout),this._blinkStartTimeout=void 0),this._animationFrame&&(window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)},e.prototype.resume=function(){this.pause(),this._animationTimeRestarted=void 0,this._restartInterval(),this.restartBlinkAnimation()},e}()},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.LinkRenderLayer=void 0;var o=r(13),s=r(9),a=r(26),c=function(e){function t(t,r,i,n,o,s,a,c){var l=e.call(this,t,"link",r,!0,i,n,a,c)||this;return o.onShowLinkUnderline((function(e){return l._onShowLinkUnderline(e)})),o.onHideLinkUnderline((function(e){return l._onHideLinkUnderline(e)})),s.onShowLinkUnderline((function(e){return l._onShowLinkUnderline(e)})),s.onHideLinkUnderline((function(e){return l._onHideLinkUnderline(e)})),l}return n(t,e),t.prototype.resize=function(t){e.prototype.resize.call(this,t),this._state=void 0},t.prototype.reset=function(){this._clearCurrentLink()},t.prototype._clearCurrentLink=function(){if(this._state){this._clearCells(this._state.x1,this._state.y1,this._state.cols-this._state.x1,1);var e=this._state.y2-this._state.y1-1;e>0&&this._clearCells(0,this._state.y1+1,this._state.cols,e),this._clearCells(0,this._state.y2,this._state.x2,1),this._state=void 0}},t.prototype._onShowLinkUnderline=function(e){if(e.fg===s.INVERTED_DEFAULT_COLOR?this._ctx.fillStyle=this._colors.background.css:e.fg&&a.is256Color(e.fg)?this._ctx.fillStyle=this._colors.ansi[e.fg].css:this._ctx.fillStyle=this._colors.foreground.css,e.y1===e.y2)this._fillBottomLineAtCells(e.x1,e.y1,e.x2-e.x1);else{this._fillBottomLineAtCells(e.x1,e.y1,e.cols-e.x1);for(var t=e.y1+1;t=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseZone=t.Linkifier=void 0;var o=r(0),s=r(1),a=function(){function e(e,t,r){this._bufferService=e,this._logService=t,this._unicodeService=r,this._linkMatchers=[],this._nextLinkMatcherId=0,this._onShowLinkUnderline=new o.EventEmitter,this._onHideLinkUnderline=new o.EventEmitter,this._onLinkTooltip=new o.EventEmitter,this._rowsToLinkify={start:void 0,end:void 0}}return Object.defineProperty(e.prototype,"onShowLinkUnderline",{get:function(){return this._onShowLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onHideLinkUnderline",{get:function(){return this._onHideLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onLinkTooltip",{get:function(){return this._onLinkTooltip.event},enumerable:!1,configurable:!0}),e.prototype.attachToDom=function(e,t){this._element=e,this._mouseZoneManager=t},e.prototype.linkifyRows=function(t,r){var i=this;this._mouseZoneManager&&(void 0===this._rowsToLinkify.start||void 0===this._rowsToLinkify.end?(this._rowsToLinkify.start=t,this._rowsToLinkify.end=r):(this._rowsToLinkify.start=Math.min(this._rowsToLinkify.start,t),this._rowsToLinkify.end=Math.max(this._rowsToLinkify.end,r)),this._mouseZoneManager.clearAll(t,r),this._rowsTimeoutId&&clearTimeout(this._rowsTimeoutId),this._rowsTimeoutId=setTimeout((function(){return i._linkifyRows()}),e._timeBeforeLatency))},e.prototype._linkifyRows=function(){this._rowsTimeoutId=void 0;var e=this._bufferService.buffer;if(void 0!==this._rowsToLinkify.start&&void 0!==this._rowsToLinkify.end){var t=e.ydisp+this._rowsToLinkify.start;if(!(t>=e.lines.length)){for(var r=e.ydisp+Math.min(this._rowsToLinkify.end,this._bufferService.rows)+1,i=Math.ceil(2e3/this._bufferService.cols),n=this._bufferService.buffer.iterator(!1,t,r,i,i);n.hasNext();)for(var o=n.next(),s=0;s=0;t--)if(e.priority<=this._linkMatchers[t].priority)return void this._linkMatchers.splice(t+1,0,e);this._linkMatchers.splice(0,0,e)}else this._linkMatchers.push(e)},e.prototype.deregisterLinkMatcher=function(e){for(var t=0;t>9&511:void 0;r.validationCallback?r.validationCallback(a,(function(e){n._rowsTimeoutId||e&&n._addLink(l[1],l[0]-n._bufferService.buffer.ydisp,a,r,f)})):c._addLink(l[1],l[0]-c._bufferService.buffer.ydisp,a,r,f)},c=this;null!==(i=o.exec(t));){if("break"===a())break}},e.prototype._addLink=function(e,t,r,i,n){var o=this;if(this._mouseZoneManager&&this._element){var s=this._unicodeService.getStringCellWidth(r),a=e%this._bufferService.cols,l=t+Math.floor(e/this._bufferService.cols),h=(a+s)%this._bufferService.cols,u=l+Math.floor((a+s)/this._bufferService.cols);0===h&&(h=this._bufferService.cols,u--),this._mouseZoneManager.add(new c(a+1,l+1,h+1,u+1,(function(e){if(i.handler)return i.handler(e,r);var t=window.open();t?(t.opener=null,t.location.href=r):console.warn("Opening link blocked as opener could not be cleared")}),(function(){o._onShowLinkUnderline.fire(o._createLinkHoverEvent(a,l,h,u,n)),o._element.classList.add("xterm-cursor-pointer")}),(function(e){o._onLinkTooltip.fire(o._createLinkHoverEvent(a,l,h,u,n)),i.hoverTooltipCallback&&i.hoverTooltipCallback(e,r,{start:{x:a,y:l},end:{x:h,y:u}})}),(function(){o._onHideLinkUnderline.fire(o._createLinkHoverEvent(a,l,h,u,n)),o._element.classList.remove("xterm-cursor-pointer"),i.hoverLeaveCallback&&i.hoverLeaveCallback()}),(function(e){return!i.willLinkActivate||i.willLinkActivate(e,r)})))}},e.prototype._createLinkHoverEvent=function(e,t,r,i,n){return{x1:e,y1:t,x2:r,y2:i,cols:this._bufferService.cols,fg:n}},e._timeBeforeLatency=200,e=i([n(0,s.IBufferService),n(1,s.ILogService),n(2,s.IUnicodeService)],e)}();t.Linkifier=a;var c=function(e,t,r,i,n,o,s,a,c){this.x1=e,this.y1=t,this.x2=r,this.y2=i,this.clickCallback=n,this.hoverCallback=o,this.tooltipCallback=s,this.leaveCallback=a,this.willLinkActivate=c};t.MouseZone=c},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),o=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionService=void 0;var a=r(11),c=r(52),l=r(4),h=r(0),u=r(5),f=r(1),_=r(30),d=r(53),p=r(2),v=String.fromCharCode(160),g=new RegExp(v,"g"),y=function(e){function t(t,r,i,n,o,s,a){var u=e.call(this)||this;return u._element=t,u._screenElement=r,u._bufferService=i,u._coreService=n,u._mouseService=o,u._optionsService=s,u._renderService=a,u._dragScrollAmount=0,u._enabled=!0,u._workCell=new l.CellData,u._mouseDownTimeStamp=0,u._onLinuxMouseSelection=u.register(new h.EventEmitter),u._onRedrawRequest=u.register(new h.EventEmitter),u._onSelectionChange=u.register(new h.EventEmitter),u._onRequestScrollLines=u.register(new h.EventEmitter),u._mouseMoveListener=function(e){return u._onMouseMove(e)},u._mouseUpListener=function(e){return u._onMouseUp(e)},u._coreService.onUserInput((function(){u.hasSelection&&u.clearSelection()})),u._trimListener=u._bufferService.buffer.lines.onTrim((function(e){return u._onTrim(e)})),u.register(u._bufferService.buffers.onBufferActivate((function(e){return u._onBufferActivate(e)}))),u.enable(),u._model=new c.SelectionModel(u._bufferService),u._activeSelectionMode=0,u}return n(t,e),Object.defineProperty(t.prototype,"onLinuxMouseSelection",{get:function(){return this._onLinuxMouseSelection.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return this._onRedrawRequest.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onSelectionChange",{get:function(){return this._onSelectionChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestScrollLines",{get:function(){return this._onRequestScrollLines.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this._removeMouseDownListeners()},t.prototype.reset=function(){this.clearSelection()},t.prototype.disable=function(){this.clearSelection(),this._enabled=!1},t.prototype.enable=function(){this._enabled=!0},Object.defineProperty(t.prototype,"selectionStart",{get:function(){return this._model.finalSelectionStart},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"selectionEnd",{get:function(){return this._model.finalSelectionEnd},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"hasSelection",{get:function(){var e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;return!(!e||!t)&&(e[0]!==t[0]||e[1]!==t[1])},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"selectionText",{get:function(){var e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;if(!e||!t)return"";var r=this._bufferService.buffer,i=[];if(3===this._activeSelectionMode){if(e[0]===t[0])return"";for(var n=e[1];n<=t[1];n++){var o=r.translateBufferLineToString(n,!0,e[0],t[0]);i.push(o)}}else{var s=e[1]===t[1]?t[0]:void 0;i.push(r.translateBufferLineToString(e[1],!0,e[0],s));for(n=e[1]+1;n<=t[1]-1;n++){var c=r.lines.get(n);o=r.translateBufferLineToString(n,!0);c&&c.isWrapped?i[i.length-1]+=o:i.push(o)}if(e[1]!==t[1]){c=r.lines.get(t[1]),o=r.translateBufferLineToString(t[1],!0,0,t[0]);c&&c.isWrapped?i[i.length-1]+=o:i.push(o)}}return i.map((function(e){return e.replace(g," ")})).join(a.isWindows?"\r\n":"\n")},enumerable:!1,configurable:!0}),t.prototype.clearSelection=function(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()},t.prototype.refresh=function(e){var t=this;(this._refreshAnimationFrame||(this._refreshAnimationFrame=window.requestAnimationFrame((function(){return t._refresh()}))),a.isLinux&&e)&&(this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText))},t.prototype._refresh=function(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:3===this._activeSelectionMode})},t.prototype.isClickInSelection=function(e){var t=this._getMouseBufferCoords(e),r=this._model.finalSelectionStart,i=this._model.finalSelectionEnd;return!!(r&&i&&t)&&this._areCoordsInSelection(t,r,i)},t.prototype._areCoordsInSelection=function(e,t,r){return e[1]>t[1]&&e[1]=t[0]&&e[0]=t[0]},t.prototype.selectWordAtCursor=function(e){var t=this._getMouseBufferCoords(e);t&&(this._selectWordAt(t,!1),this._model.selectionEnd=void 0,this.refresh(!0))},t.prototype.selectAll=function(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()},t.prototype.selectLines=function(e,t){this._model.clearSelection(),e=Math.max(e,0),t=Math.min(t,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,e],this._model.selectionEnd=[this._bufferService.cols,t],this.refresh(),this._onSelectionChange.fire()},t.prototype._onTrim=function(e){this._model.onTrim(e)&&this.refresh()},t.prototype._getMouseBufferCoords=function(e){var t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(t)return t[0]--,t[1]--,t[1]+=this._bufferService.buffer.ydisp,t},t.prototype._getMouseEventScrollAmount=function(e){var t=_.getCoordsRelativeToElement(e,this._screenElement)[1],r=this._renderService.dimensions.canvasHeight;return t>=0&&t<=r?0:(t>r&&(t-=r),t=Math.min(Math.max(t,-50),50),(t/=50)/Math.abs(t)+Math.round(14*t))},t.prototype.shouldForceSelection=function(e){return a.isMac?e.altKey&&this._optionsService.options.macOptionClickForcesSelection:e.shiftKey},t.prototype.onMouseDown=function(e){if(this._mouseDownTimeStamp=e.timeStamp,(2!==e.button||!this.hasSelection)&&0===e.button){if(!this._enabled){if(!this.shouldForceSelection(e))return;e.stopPropagation()}e.preventDefault(),this._dragScrollAmount=0,this._enabled&&e.shiftKey?this._onIncrementalClick(e):1===e.detail?this._onSingleClick(e):2===e.detail?this._onDoubleClick(e):3===e.detail&&this._onTripleClick(e),this._addMouseDownListeners(),this.refresh(!0)}},t.prototype._addMouseDownListeners=function(){var e=this;this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=window.setInterval((function(){return e._dragScroll()}),50)},t.prototype._removeMouseDownListeners=function(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0},t.prototype._onIncrementalClick=function(e){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(e))},t.prototype._onSingleClick=function(e){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(e)?3:0,this._model.selectionStart=this._getMouseBufferCoords(e),this._model.selectionStart){this._model.selectionEnd=void 0;var t=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);t&&t.length!==this._model.selectionStart[0]&&0===t.hasWidth(this._model.selectionStart[0])&&this._model.selectionStart[0]++}},t.prototype._onDoubleClick=function(e){var t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=1,this._selectWordAt(t,!0))},t.prototype._onTripleClick=function(e){var t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=2,this._selectLineAt(t[1]))},t.prototype.shouldColumnSelect=function(e){return e.altKey&&!(a.isMac&&this._optionsService.options.macOptionClickForcesSelection)},t.prototype._onMouseMove=function(e){if(e.stopImmediatePropagation(),this._model.selectionStart){var t=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(e),this._model.selectionEnd){2===this._activeSelectionMode?this._model.selectionEnd[1]0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));var r=this._bufferService.buffer;if(this._model.selectionEnd[1]0?(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(e.ydisp+this._bufferService.rows,e.lines.length-1)):(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=e.ydisp),this.refresh()}},t.prototype._onMouseUp=function(e){var t=e.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&t<500&&e.altKey){if(this._bufferService.buffer.ybase===this._bufferService.buffer.ydisp){var r=this._mouseService.getCoords(e,this._element,this._bufferService.cols,this._bufferService.rows,!1);if(r&&void 0!==r[0]&&void 0!==r[1]){var i=d.moveToCellSequence(r[0]-1,r[1]-1,this._bufferService,this._coreService.decPrivateModes.applicationCursorKeys);this._coreService.triggerDataEvent(i,!0)}}}else this.hasSelection&&this._onSelectionChange.fire()},t.prototype._onBufferActivate=function(e){var t=this;this.clearSelection(),this._trimListener.dispose(),this._trimListener=e.activeBuffer.lines.onTrim((function(e){return t._onTrim(e)}))},t.prototype._convertViewportColToCharacterIndex=function(e,t){for(var r=t[0],i=0;t[0]>=i;i++){var n=e.loadCell(i,this._workCell).getChars().length;0===this._workCell.getWidth()?r--:n>1&&t[0]!==i&&(r+=n-1)}return r},t.prototype.setSelection=function(e,t,r){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[e,t],this._model.selectionStartLength=r,this.refresh()},t.prototype._getWordAt=function(e,t,r,i){if(void 0===r&&(r=!0),void 0===i&&(i=!0),!(e[0]>=this._bufferService.cols)){var n=this._bufferService.buffer,o=n.lines.get(e[1]);if(o){var s=n.translateBufferLineToString(e[1],!1),a=this._convertViewportColToCharacterIndex(o,e),c=a,l=e[0]-a,h=0,u=0,f=0,_=0;if(" "===s.charAt(a)){for(;a>0&&" "===s.charAt(a-1);)a--;for(;c1&&(_+=v-1,c+=v-1);d>0&&a>0&&!this._isCharWordSeparator(o.loadCell(d-1,this._workCell));){o.loadCell(d-1,this._workCell);var g=this._workCell.getChars().length;0===this._workCell.getWidth()?(h++,d--):g>1&&(f+=g-1,a-=g-1),a--,d--}for(;p1&&(_+=y-1,c+=y-1),c++,p++}}c++;var b=a+l-h+f,S=Math.min(this._bufferService.cols,c-a+h+u-f-_);if(t||""!==s.slice(a,c).trim()){if(r&&0===b&&32!==o.getCodePoint(0)){var m=n.lines.get(e[1]-1);if(m&&o.isWrapped&&32!==m.getCodePoint(this._bufferService.cols-1)){var C=this._getWordAt([this._bufferService.cols-1,e[1]-1],!1,!0,!1);if(C){var w=this._bufferService.cols-C.start;b-=w,S+=w}}}if(i&&b+S===this._bufferService.cols&&32!==o.getCodePoint(this._bufferService.cols-1)){var E=n.lines.get(e[1]+1);if(E&&E.isWrapped&&32!==E.getCodePoint(0)){var L=this._getWordAt([0,e[1]+1],!1,!1,!0);L&&(S+=L.length)}}return{start:b,length:S}}}}},t.prototype._selectWordAt=function(e,t){var r=this._getWordAt(e,t);if(r){for(;r.start<0;)r.start+=this._bufferService.cols,e[1]--;this._model.selectionStart=[r.start,e[1]],this._model.selectionStartLength=r.length}},t.prototype._selectToWordAt=function(e){var t=this._getWordAt(e,!0);if(t){for(var r=e[1];t.start<0;)t.start+=this._bufferService.cols,r--;if(!this._model.areSelectionValuesReversed())for(;t.start+t.length>this._bufferService.cols;)t.length-=this._bufferService.cols,r++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?t.start:t.start+t.length,r]}},t.prototype._isCharWordSeparator=function(e){return 0!==e.getWidth()&&this._optionsService.options.wordSeparator.indexOf(e.getChars())>=0},t.prototype._selectLineAt=function(e){var t=this._bufferService.buffer.getWrappedRangeForLine(e);this._model.selectionStart=[0,t.first],this._model.selectionEnd=[this._bufferService.cols,t.last],this._model.selectionStartLength=0},t=o([s(2,f.IBufferService),s(3,f.ICoreService),s(4,u.IMouseService),s(5,f.IOptionsService),s(6,u.IRenderService)],t)}(p.Disposable);t.SelectionService=y},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionModel=void 0;var i=function(){function e(e){this._bufferService=e,this.isSelectAllActive=!1,this.selectionStartLength=0}return e.prototype.clearSelection=function(){this.selectionStart=void 0,this.selectionEnd=void 0,this.isSelectAllActive=!1,this.selectionStartLength=0},Object.defineProperty(e.prototype,"finalSelectionStart",{get:function(){return this.isSelectAllActive?[0,0]:this.selectionEnd&&this.selectionStart&&this.areSelectionValuesReversed()?this.selectionEnd:this.selectionStart},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"finalSelectionEnd",{get:function(){if(this.isSelectAllActive)return[this._bufferService.cols,this._bufferService.buffer.ybase+this._bufferService.rows-1];if(this.selectionStart){if(!this.selectionEnd||this.areSelectionValuesReversed()){var e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[e,this.selectionStart[1]]}return this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]?[Math.max(this.selectionStart[0]+this.selectionStartLength,this.selectionEnd[0]),this.selectionEnd[1]]:this.selectionEnd}},enumerable:!1,configurable:!0}),e.prototype.areSelectionValuesReversed=function(){var e=this.selectionStart,t=this.selectionEnd;return!(!e||!t)&&(e[1]>t[1]||e[1]===t[1]&&e[0]>t[0])},e.prototype.onTrim=function(e){return this.selectionStart&&(this.selectionStart[1]-=e),this.selectionEnd&&(this.selectionEnd[1]-=e),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)},e}();t.SelectionModel=i},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.moveToCellSequence=void 0;var i=r(12);function n(e,t,r,i){var n=e-o(r,e),a=t-o(r,t);return l(Math.abs(n-a)-function(e,t,r){for(var i=0,n=e-o(r,e),a=t-o(r,t),c=0;c=0&&tt?"A":"B"}function a(e,t,r,i,n,o){for(var s=e,a=t,c="";s!==r||a!==i;)s+=n?1:-1,n&&s>o.cols-1?(c+=o.buffer.translateBufferLineToString(a,!1,e,s),s=0,e=0,a++):!n&&s<0&&(c+=o.buffer.translateBufferLineToString(a,!1,0,e+1),e=s=o.cols-1,a--);return c+o.buffer.translateBufferLineToString(a,!1,e,s)}function c(e,t){var r=t?"O":"[";return i.C0.ESC+r+e}function l(e,t){e=Math.floor(e);for(var r="",i=0;i0?i-o(s,i):t;var f=i,_=function(e,t,r,i,s,a){var c;c=n(r,i,s,a).length>0?i-o(s,i):t;if(e=r&&ce?"D":"C",l(Math.abs(h-e),c(s,i));s=u>t?"D":"C";var f=Math.abs(u-t);return l(function(e,t){return t.cols-e}(u>t?e:h,r)+(f-1)*r.cols+1+((u>t?h:e)-1),c(s,i))}},function(e,t,r){"use strict";var i=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.SoundService=void 0;var o=r(1),s=function(){function e(e){this._optionsService=e}return Object.defineProperty(e,"audioContext",{get:function(){if(!e._audioContext){var t=window.AudioContext||window.webkitAudioContext;if(!t)return console.warn("Web Audio API is not supported by this browser. Consider upgrading to the latest version"),null;e._audioContext=new t}return e._audioContext},enumerable:!1,configurable:!0}),e.prototype.playBellSound=function(){var t=e.audioContext;if(t){var r=t.createBufferSource();t.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.options.bellSound)),(function(e){r.buffer=e,r.connect(t.destination),r.start(0)}))}},e.prototype._base64ToArrayBuffer=function(e){for(var t=window.atob(e),r=t.length,i=new Uint8Array(r),n=0;n=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseZoneManager=void 0;var a=r(2),c=r(7),l=r(5),h=r(1),u=function(e){function t(t,r,i,n,o,s){var a=e.call(this)||this;return a._element=t,a._screenElement=r,a._bufferService=i,a._mouseService=n,a._selectionService=o,a._optionsService=s,a._zones=[],a._areZonesActive=!1,a._lastHoverCoords=[void 0,void 0],a._initialSelectionLength=0,a.register(c.addDisposableDomListener(a._element,"mousedown",(function(e){return a._onMouseDown(e)}))),a._mouseMoveListener=function(e){return a._onMouseMove(e)},a._mouseLeaveListener=function(e){return a._onMouseLeave(e)},a._clickListener=function(e){return a._onClick(e)},a}return n(t,e),t.prototype.dispose=function(){e.prototype.dispose.call(this),this._deactivate()},t.prototype.add=function(e){this._zones.push(e),1===this._zones.length&&this._activate()},t.prototype.clearAll=function(e,t){if(0!==this._zones.length){e&&t||(e=0,t=this._bufferService.rows-1);for(var r=0;re&&i.y1<=t+1||i.y2>e&&i.y2<=t+1||i.y1t+1)&&(this._currentZone&&this._currentZone===i&&(this._currentZone.leaveCallback(),this._currentZone=void 0),this._zones.splice(r--,1))}0===this._zones.length&&this._deactivate()}},t.prototype._activate=function(){this._areZonesActive||(this._areZonesActive=!0,this._element.addEventListener("mousemove",this._mouseMoveListener),this._element.addEventListener("mouseleave",this._mouseLeaveListener),this._element.addEventListener("click",this._clickListener))},t.prototype._deactivate=function(){this._areZonesActive&&(this._areZonesActive=!1,this._element.removeEventListener("mousemove",this._mouseMoveListener),this._element.removeEventListener("mouseleave",this._mouseLeaveListener),this._element.removeEventListener("click",this._clickListener))},t.prototype._onMouseMove=function(e){this._lastHoverCoords[0]===e.pageX&&this._lastHoverCoords[1]===e.pageY||(this._onHover(e),this._lastHoverCoords=[e.pageX,e.pageY])},t.prototype._onHover=function(e){var t=this,r=this._findZoneEventAt(e);r!==this._currentZone&&(this._currentZone&&(this._currentZone.leaveCallback(),this._currentZone=void 0,this._tooltipTimeout&&clearTimeout(this._tooltipTimeout)),r&&(this._currentZone=r,r.hoverCallback&&r.hoverCallback(e),this._tooltipTimeout=window.setTimeout((function(){return t._onTooltip(e)}),this._optionsService.options.linkTooltipHoverDuration)))},t.prototype._onTooltip=function(e){this._tooltipTimeout=void 0;var t=this._findZoneEventAt(e);t&&t.tooltipCallback&&t.tooltipCallback(e)},t.prototype._onMouseDown=function(e){if(this._initialSelectionLength=this._getSelectionLength(),this._areZonesActive){var t=this._findZoneEventAt(e);(null==t?void 0:t.willLinkActivate(e))&&(e.preventDefault(),e.stopImmediatePropagation())}},t.prototype._onMouseLeave=function(e){this._currentZone&&(this._currentZone.leaveCallback(),this._currentZone=void 0,this._tooltipTimeout&&clearTimeout(this._tooltipTimeout))},t.prototype._onClick=function(e){var t=this._findZoneEventAt(e),r=this._getSelectionLength();t&&r===this._initialSelectionLength&&(t.clickCallback(e),e.preventDefault(),e.stopImmediatePropagation())},t.prototype._getSelectionLength=function(){var e=this._selectionService.selectionText;return e?e.length:0},t.prototype._findZoneEventAt=function(e){var t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows);if(t)for(var r=t[0],i=t[1],n=0;n=o.x1&&r=o.x1||i===o.y2&&ro.y1&&ie;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()},t.prototype._createAccessibilityTreeNode=function(){var e=document.createElement("div");return e.setAttribute("role","listitem"),e.tabIndex=-1,this._refreshRowDimensions(e),e},t.prototype._onTab=function(e){for(var t=0;t0)this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e);else this._charsToAnnounce+=e;"\n"===e&&(this._liveRegionLineCount++,21===this._liveRegionLineCount&&(this._liveRegion.textContent+=o.tooMuchOutput)),s.isMac&&this._liveRegion.textContent&&this._liveRegion.textContent.length>0&&!this._liveRegion.parentNode&&setTimeout((function(){t._accessibilityTreeRoot.appendChild(t._liveRegion)}),0)}},t.prototype._clearLiveRegion=function(){this._liveRegion.textContent="",this._liveRegionLineCount=0,s.isMac&&u.removeElementFromParent(this._liveRegion)},t.prototype._onKey=function(e){this._clearLiveRegion(),this._charsToConsume.push(e)},t.prototype._refreshRows=function(e,t){this._renderRowsDebouncer.refresh(e,t,this._terminal.rows)},t.prototype._renderRows=function(e,t){for(var r=this._terminal.buffer,i=r.lines.length.toString(),n=e;n<=t;n++){var o=r.translateBufferLineToString(r.ydisp+n,!0),s=(r.ydisp+n+1).toString(),a=this._rowElements[n];a&&(0===o.length?a.innerHTML=" ":a.textContent=o,a.setAttribute("aria-posinset",s),a.setAttribute("aria-setsize",i))}this._announceCharacters()},t.prototype._refreshRowsDimensions=function(){if(this._renderService.dimensions.actualCellHeight){this._rowElements.length!==this._terminal.rows&&this._onResize(this._terminal.rows);for(var e=0;e=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DomRenderer=void 0;var a=r(58),c=r(9),l=r(2),h=r(5),u=r(1),f=r(0),_=r(10),d=r(17),p=1,v=function(e){function t(t,r,i,n,o,s,c,l,h){var u=e.call(this)||this;return u._colors=t,u._element=r,u._screenElement=i,u._viewportElement=n,u._linkifier=o,u._linkifier2=s,u._charSizeService=c,u._optionsService=l,u._bufferService=h,u._terminalClass=p++,u._rowElements=[],u._rowContainer=document.createElement("div"),u._rowContainer.classList.add("xterm-rows"),u._rowContainer.style.lineHeight="normal",u._rowContainer.setAttribute("aria-hidden","true"),u._refreshRowElements(u._bufferService.cols,u._bufferService.rows),u._selectionContainer=document.createElement("div"),u._selectionContainer.classList.add("xterm-selection"),u._selectionContainer.setAttribute("aria-hidden","true"),u.dimensions={scaledCharWidth:0,scaledCharHeight:0,scaledCellWidth:0,scaledCellHeight:0,scaledCharLeft:0,scaledCharTop:0,scaledCanvasWidth:0,scaledCanvasHeight:0,canvasWidth:0,canvasHeight:0,actualCellWidth:0,actualCellHeight:0},u._updateDimensions(),u._injectCss(),u._rowFactory=new a.DomRendererRowFactory(document,u._optionsService,u._colors),u._element.classList.add("xterm-dom-renderer-owner-"+u._terminalClass),u._screenElement.appendChild(u._rowContainer),u._screenElement.appendChild(u._selectionContainer),u._linkifier.onShowLinkUnderline((function(e){return u._onLinkHover(e)})),u._linkifier.onHideLinkUnderline((function(e){return u._onLinkLeave(e)})),u._linkifier2.onShowLinkUnderline((function(e){return u._onLinkHover(e)})),u._linkifier2.onHideLinkUnderline((function(e){return u._onLinkLeave(e)})),u}return n(t,e),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return(new f.EventEmitter).event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this._element.classList.remove("xterm-dom-renderer-owner-"+this._terminalClass),d.removeElementFromParent(this._rowContainer,this._selectionContainer,this._themeStyleElement,this._dimensionsStyleElement),e.prototype.dispose.call(this)},t.prototype._updateDimensions=function(){this.dimensions.scaledCharWidth=this._charSizeService.width*window.devicePixelRatio,this.dimensions.scaledCharHeight=Math.ceil(this._charSizeService.height*window.devicePixelRatio),this.dimensions.scaledCellWidth=this.dimensions.scaledCharWidth+Math.round(this._optionsService.options.letterSpacing),this.dimensions.scaledCellHeight=Math.floor(this.dimensions.scaledCharHeight*this._optionsService.options.lineHeight),this.dimensions.scaledCharLeft=0,this.dimensions.scaledCharTop=0,this.dimensions.scaledCanvasWidth=this.dimensions.scaledCellWidth*this._bufferService.cols,this.dimensions.scaledCanvasHeight=this.dimensions.scaledCellHeight*this._bufferService.rows,this.dimensions.canvasWidth=Math.round(this.dimensions.scaledCanvasWidth/window.devicePixelRatio),this.dimensions.canvasHeight=Math.round(this.dimensions.scaledCanvasHeight/window.devicePixelRatio),this.dimensions.actualCellWidth=this.dimensions.canvasWidth/this._bufferService.cols,this.dimensions.actualCellHeight=this.dimensions.canvasHeight/this._bufferService.rows;for(var e=0,t=this._rowElements;et;)this._rowContainer.removeChild(this._rowElements.pop())},t.prototype.onResize=function(e,t){this._refreshRowElements(e,t),this._updateDimensions()},t.prototype.onCharSizeChanged=function(){this._updateDimensions()},t.prototype.onBlur=function(){this._rowContainer.classList.remove("xterm-focus")},t.prototype.onFocus=function(){this._rowContainer.classList.add("xterm-focus")},t.prototype.onSelectionChanged=function(e,t,r){for(;this._selectionContainer.children.length;)this._selectionContainer.removeChild(this._selectionContainer.children[0]);if(e&&t){var i=e[1]-this._bufferService.buffer.ydisp,n=t[1]-this._bufferService.buffer.ydisp,o=Math.max(i,0),s=Math.min(n,this._bufferService.rows-1);if(!(o>=this._bufferService.rows||s<0)){var a=document.createDocumentFragment();if(r)a.appendChild(this._createSelectionElement(o,e[0],t[0],s-o+1));else{var c=i===o?e[0]:0,l=o===s?t[0]:this._bufferService.cols;a.appendChild(this._createSelectionElement(o,c,l));var h=s-o-1;if(a.appendChild(this._createSelectionElement(o+1,0,this._bufferService.cols,h)),o!==s){var u=n===s?t[0]:this._bufferService.cols;a.appendChild(this._createSelectionElement(s,0,u))}}this._selectionContainer.appendChild(a)}}},t.prototype._createSelectionElement=function(e,t,r,i){void 0===i&&(i=1);var n=document.createElement("div");return n.style.height=i*this.dimensions.actualCellHeight+"px",n.style.top=e*this.dimensions.actualCellHeight+"px",n.style.left=t*this.dimensions.actualCellWidth+"px",n.style.width=this.dimensions.actualCellWidth*(r-t)+"px",n},t.prototype.onCursorMove=function(){},t.prototype.onOptionsChanged=function(){this._updateDimensions(),this._injectCss()},t.prototype.clear=function(){for(var e=0,t=this._rowElements;e=n&&(e=0,r++)}},t=o([s(6,h.ICharSizeService),s(7,u.IOptionsService),s(8,u.IBufferService)],t)}(l.Disposable);t.DomRenderer=v},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.DomRendererRowFactory=t.CURSOR_STYLE_UNDERLINE_CLASS=t.CURSOR_STYLE_BAR_CLASS=t.CURSOR_STYLE_BLOCK_CLASS=t.CURSOR_BLINK_CLASS=t.CURSOR_CLASS=t.UNDERLINE_CLASS=t.ITALIC_CLASS=t.DIM_CLASS=t.BOLD_CLASS=void 0;var i=r(9),n=r(3),o=r(4),s=r(10);t.BOLD_CLASS="xterm-bold",t.DIM_CLASS="xterm-dim",t.ITALIC_CLASS="xterm-italic",t.UNDERLINE_CLASS="xterm-underline",t.CURSOR_CLASS="xterm-cursor",t.CURSOR_BLINK_CLASS="xterm-cursor-blink",t.CURSOR_STYLE_BLOCK_CLASS="xterm-cursor-block",t.CURSOR_STYLE_BAR_CLASS="xterm-cursor-bar",t.CURSOR_STYLE_UNDERLINE_CLASS="xterm-cursor-underline";var a=function(){function e(e,t,r){this._document=e,this._optionsService=t,this._colors=r,this._workCell=new o.CellData}return e.prototype.setColors=function(e){this._colors=e},e.prototype.createRow=function(e,r,o,a,l,h,u){for(var f=this._document.createDocumentFragment(),_=0,d=Math.min(e.length,u)-1;d>=0;d--)if(e.loadCell(d,this._workCell).getCode()!==n.NULL_CELL_CODE||r&&d===a){_=d+1;break}for(d=0;d<_;d++){e.loadCell(d,this._workCell);var p=this._workCell.getWidth();if(0!==p){var v=this._document.createElement("span");if(p>1&&(v.style.width=h*p+"px"),r&&d===a)switch(v.classList.add(t.CURSOR_CLASS),l&&v.classList.add(t.CURSOR_BLINK_CLASS),o){case"bar":v.classList.add(t.CURSOR_STYLE_BAR_CLASS);break;case"underline":v.classList.add(t.CURSOR_STYLE_UNDERLINE_CLASS);break;default:v.classList.add(t.CURSOR_STYLE_BLOCK_CLASS)}this._workCell.isBold()&&v.classList.add(t.BOLD_CLASS),this._workCell.isItalic()&&v.classList.add(t.ITALIC_CLASS),this._workCell.isDim()&&v.classList.add(t.DIM_CLASS),this._workCell.isUnderline()&&v.classList.add(t.UNDERLINE_CLASS),this._workCell.isInvisible()?v.textContent=n.WHITESPACE_CELL_CHAR:v.textContent=this._workCell.getChars()||n.WHITESPACE_CELL_CHAR;var g=this._workCell.getFgColor(),y=this._workCell.getFgColorMode(),b=this._workCell.getBgColor(),S=this._workCell.getBgColorMode(),m=!!this._workCell.isInverse();if(m){var C=g;g=b,b=C;var w=y;y=S,S=w}switch(y){case 16777216:case 33554432:this._workCell.isBold()&&g<8&&this._optionsService.options.drawBoldTextInBrightColors&&(g+=8),this._applyMinimumContrast(v,this._colors.background,this._colors.ansi[g])||v.classList.add("xterm-fg-"+g);break;case 50331648:var E=s.rgba.toColor(g>>16&255,g>>8&255,255&g);this._applyMinimumContrast(v,this._colors.background,E)||this._addStyle(v,"color:#"+c(g.toString(16),"0",6));break;case 0:default:this._applyMinimumContrast(v,this._colors.background,this._colors.foreground)||m&&v.classList.add("xterm-fg-"+i.INVERTED_DEFAULT_COLOR)}switch(S){case 16777216:case 33554432:v.classList.add("xterm-bg-"+b);break;case 50331648:this._addStyle(v,"background-color:#"+c(b.toString(16),"0",6));break;case 0:default:m&&v.classList.add("xterm-bg-"+i.INVERTED_DEFAULT_COLOR)}f.appendChild(v)}}return f},e.prototype._applyMinimumContrast=function(e,t,r){if(1===this._optionsService.options.minimumContrastRatio)return!1;var i=this._colors.contrastCache.getColor(this._workCell.bg,this._workCell.fg);return void 0===i&&(i=s.color.ensureContrastRatio(t,r,this._optionsService.options.minimumContrastRatio),this._colors.contrastCache.setColor(this._workCell.bg,this._workCell.fg,null!=i?i:null)),!!i&&(this._addStyle(e,"color:"+i.css),!0)},e.prototype._addStyle=function(e,t){e.setAttribute("style",""+(e.getAttribute("style")||"")+t+";")},e}();function c(e,t,r){for(;e.length"],191:["/","?"],192:["`","~"],219:["[","{"],220:["\\","|"],221:["]","}"],222:["'",'"']};t.evaluateKeyboardEvent=function(e,t,r,o){var s={type:0,cancel:!1,key:void 0},a=(e.shiftKey?1:0)|(e.altKey?2:0)|(e.ctrlKey?4:0)|(e.metaKey?8:0);switch(e.keyCode){case 0:"UIKeyInputUpArrow"===e.key?s.key=t?i.C0.ESC+"OA":i.C0.ESC+"[A":"UIKeyInputLeftArrow"===e.key?s.key=t?i.C0.ESC+"OD":i.C0.ESC+"[D":"UIKeyInputRightArrow"===e.key?s.key=t?i.C0.ESC+"OC":i.C0.ESC+"[C":"UIKeyInputDownArrow"===e.key&&(s.key=t?i.C0.ESC+"OB":i.C0.ESC+"[B");break;case 8:if(e.shiftKey){s.key=i.C0.BS;break}if(e.altKey){s.key=i.C0.ESC+i.C0.DEL;break}s.key=i.C0.DEL;break;case 9:if(e.shiftKey){s.key=i.C0.ESC+"[Z";break}s.key=i.C0.HT,s.cancel=!0;break;case 13:s.key=e.altKey?i.C0.ESC+i.C0.CR:i.C0.CR,s.cancel=!0;break;case 27:s.key=i.C0.ESC,e.altKey&&(s.key=i.C0.ESC+i.C0.ESC),s.cancel=!0;break;case 37:if(e.metaKey)break;a?(s.key=i.C0.ESC+"[1;"+(a+1)+"D",s.key===i.C0.ESC+"[1;3D"&&(s.key=i.C0.ESC+(r?"b":"[1;5D"))):s.key=t?i.C0.ESC+"OD":i.C0.ESC+"[D";break;case 39:if(e.metaKey)break;a?(s.key=i.C0.ESC+"[1;"+(a+1)+"C",s.key===i.C0.ESC+"[1;3C"&&(s.key=i.C0.ESC+(r?"f":"[1;5C"))):s.key=t?i.C0.ESC+"OC":i.C0.ESC+"[C";break;case 38:if(e.metaKey)break;a?(s.key=i.C0.ESC+"[1;"+(a+1)+"A",r||s.key!==i.C0.ESC+"[1;3A"||(s.key=i.C0.ESC+"[1;5A")):s.key=t?i.C0.ESC+"OA":i.C0.ESC+"[A";break;case 40:if(e.metaKey)break;a?(s.key=i.C0.ESC+"[1;"+(a+1)+"B",r||s.key!==i.C0.ESC+"[1;3B"||(s.key=i.C0.ESC+"[1;5B")):s.key=t?i.C0.ESC+"OB":i.C0.ESC+"[B";break;case 45:e.shiftKey||e.ctrlKey||(s.key=i.C0.ESC+"[2~");break;case 46:s.key=a?i.C0.ESC+"[3;"+(a+1)+"~":i.C0.ESC+"[3~";break;case 36:s.key=a?i.C0.ESC+"[1;"+(a+1)+"H":t?i.C0.ESC+"OH":i.C0.ESC+"[H";break;case 35:s.key=a?i.C0.ESC+"[1;"+(a+1)+"F":t?i.C0.ESC+"OF":i.C0.ESC+"[F";break;case 33:e.shiftKey?s.type=2:s.key=i.C0.ESC+"[5~";break;case 34:e.shiftKey?s.type=3:s.key=i.C0.ESC+"[6~";break;case 112:s.key=a?i.C0.ESC+"[1;"+(a+1)+"P":i.C0.ESC+"OP";break;case 113:s.key=a?i.C0.ESC+"[1;"+(a+1)+"Q":i.C0.ESC+"OQ";break;case 114:s.key=a?i.C0.ESC+"[1;"+(a+1)+"R":i.C0.ESC+"OR";break;case 115:s.key=a?i.C0.ESC+"[1;"+(a+1)+"S":i.C0.ESC+"OS";break;case 116:s.key=a?i.C0.ESC+"[15;"+(a+1)+"~":i.C0.ESC+"[15~";break;case 117:s.key=a?i.C0.ESC+"[17;"+(a+1)+"~":i.C0.ESC+"[17~";break;case 118:s.key=a?i.C0.ESC+"[18;"+(a+1)+"~":i.C0.ESC+"[18~";break;case 119:s.key=a?i.C0.ESC+"[19;"+(a+1)+"~":i.C0.ESC+"[19~";break;case 120:s.key=a?i.C0.ESC+"[20;"+(a+1)+"~":i.C0.ESC+"[20~";break;case 121:s.key=a?i.C0.ESC+"[21;"+(a+1)+"~":i.C0.ESC+"[21~";break;case 122:s.key=a?i.C0.ESC+"[23;"+(a+1)+"~":i.C0.ESC+"[23~";break;case 123:s.key=a?i.C0.ESC+"[24;"+(a+1)+"~":i.C0.ESC+"[24~";break;default:if(!e.ctrlKey||e.shiftKey||e.altKey||e.metaKey)if(r&&!o||!e.altKey||e.metaKey)r&&!e.altKey&&!e.ctrlKey&&e.metaKey?65===e.keyCode&&(s.type=1):e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.keyCode>=48&&1===e.key.length?s.key=e.key:e.key&&e.ctrlKey&&"_"===e.key&&(s.key=i.C0.US);else{var c=n[e.keyCode],l=c&&c[e.shiftKey?1:0];if(l)s.key=i.C0.ESC+l;else if(e.keyCode>=65&&e.keyCode<=90){var h=e.ctrlKey?e.keyCode-64:e.keyCode+32;s.key=i.C0.ESC+String.fromCharCode(h)}}else e.keyCode>=65&&e.keyCode<=90?s.key=String.fromCharCode(e.keyCode-64):32===e.keyCode?s.key=i.C0.NUL:e.keyCode>=51&&e.keyCode<=55?s.key=String.fromCharCode(e.keyCode-51+27):56===e.keyCode?s.key=i.C0.DEL:219===e.keyCode?s.key=i.C0.ESC:220===e.keyCode?s.key=i.C0.FS:221===e.keyCode&&(s.key=i.C0.GS)}return s}},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),o=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.RenderService=void 0;var a=r(31),c=r(0),l=r(2),h=r(32),u=r(7),f=r(1),_=r(5),d=function(e){function t(t,r,i,n,o,s){var l=e.call(this)||this;if(l._renderer=t,l._rowCount=r,l._isPaused=!1,l._needsFullRefresh=!1,l._isNextRenderRedrawOnly=!0,l._needsSelectionRefresh=!1,l._canvasWidth=0,l._canvasHeight=0,l._selectionState={start:void 0,end:void 0,columnSelectMode:!1},l._onDimensionsChange=new c.EventEmitter,l._onRender=new c.EventEmitter,l._onRefreshRequest=new c.EventEmitter,l.register({dispose:function(){return l._renderer.dispose()}}),l._renderDebouncer=new a.RenderDebouncer((function(e,t){return l._renderRows(e,t)})),l.register(l._renderDebouncer),l._screenDprMonitor=new h.ScreenDprMonitor,l._screenDprMonitor.setListener((function(){return l.onDevicePixelRatioChange()})),l.register(l._screenDprMonitor),l.register(s.onResize((function(e){return l._fullRefresh()}))),l.register(n.onOptionChange((function(){return l._renderer.onOptionsChanged()}))),l.register(o.onCharSizeChange((function(){return l.onCharSizeChanged()}))),l._renderer.onRequestRedraw((function(e){return l.refreshRows(e.start,e.end,!0)})),l.register(u.addDisposableDomListener(window,"resize",(function(){return l.onDevicePixelRatioChange()}))),"IntersectionObserver"in window){var f=new IntersectionObserver((function(e){return l._onIntersectionChange(e[e.length-1])}),{threshold:0});f.observe(i),l.register({dispose:function(){return f.disconnect()}})}return l}return n(t,e),Object.defineProperty(t.prototype,"onDimensionsChange",{get:function(){return this._onDimensionsChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRenderedBufferChange",{get:function(){return this._onRender.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRefreshRequest",{get:function(){return this._onRefreshRequest.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"dimensions",{get:function(){return this._renderer.dimensions},enumerable:!1,configurable:!0}),t.prototype._onIntersectionChange=function(e){this._isPaused=0===e.intersectionRatio,!this._isPaused&&this._needsFullRefresh&&(this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)},t.prototype.refreshRows=function(e,t,r){void 0===r&&(r=!1),this._isPaused?this._needsFullRefresh=!0:(r||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(e,t,this._rowCount))},t.prototype._renderRows=function(e,t){this._renderer.renderRows(e,t),this._needsSelectionRefresh&&(this._renderer.onSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),this._needsSelectionRefresh=!1),this._isNextRenderRedrawOnly||this._onRender.fire({start:e,end:t}),this._isNextRenderRedrawOnly=!0},t.prototype.resize=function(e,t){this._rowCount=t,this._fireOnCanvasResize()},t.prototype.changeOptions=function(){this._renderer.onOptionsChanged(),this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize()},t.prototype._fireOnCanvasResize=function(){this._renderer.dimensions.canvasWidth===this._canvasWidth&&this._renderer.dimensions.canvasHeight===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.dimensions)},t.prototype.dispose=function(){e.prototype.dispose.call(this)},t.prototype.setRenderer=function(e){var t=this;this._renderer.dispose(),this._renderer=e,this._renderer.onRequestRedraw((function(e){return t.refreshRows(e.start,e.end,!0)})),this._needsSelectionRefresh=!0,this._fullRefresh()},t.prototype._fullRefresh=function(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)},t.prototype.setColors=function(e){this._renderer.setColors(e),this._fullRefresh()},t.prototype.onDevicePixelRatioChange=function(){this._renderer.onDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1)},t.prototype.onResize=function(e,t){this._renderer.onResize(e,t),this._fullRefresh()},t.prototype.onCharSizeChanged=function(){this._renderer.onCharSizeChanged()},t.prototype.onBlur=function(){this._renderer.onBlur()},t.prototype.onFocus=function(){this._renderer.onFocus()},t.prototype.onSelectionChanged=function(e,t,r){this._selectionState.start=e,this._selectionState.end=t,this._selectionState.columnSelectMode=r,this._renderer.onSelectionChanged(e,t,r)},t.prototype.onCursorMove=function(){this._renderer.onCursorMove()},t.prototype.clear=function(){this._renderer.clear()},t.prototype.registerCharacterJoiner=function(e){return this._renderer.registerCharacterJoiner(e)},t.prototype.deregisterCharacterJoiner=function(e){return this._renderer.deregisterCharacterJoiner(e)},t=o([s(3,f.IOptionsService),s(4,_.ICharSizeService),s(5,f.IBufferService)],t)}(l.Disposable);t.RenderService=d},function(e,t,r){"use strict";var i=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CharSizeService=void 0;var o=r(1),s=r(0),a=function(){function e(e,t,r){this._optionsService=r,this.width=0,this.height=0,this._onCharSizeChange=new s.EventEmitter,this._measureStrategy=new c(e,t,this._optionsService)}return Object.defineProperty(e.prototype,"hasValidSize",{get:function(){return this.width>0&&this.height>0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onCharSizeChange",{get:function(){return this._onCharSizeChange.event},enumerable:!1,configurable:!0}),e.prototype.measure=function(){var e=this._measureStrategy.measure();e.width===this.width&&e.height===this.height||(this.width=e.width,this.height=e.height,this._onCharSizeChange.fire())},e=i([n(2,o.IOptionsService)],e)}();t.CharSizeService=a;var c=function(){function e(e,t,r){this._document=e,this._parentElement=t,this._optionsService=r,this._result={width:0,height:0},this._measureElement=this._document.createElement("span"),this._measureElement.classList.add("xterm-char-measure-element"),this._measureElement.textContent="W",this._measureElement.setAttribute("aria-hidden","true"),this._parentElement.appendChild(this._measureElement)}return e.prototype.measure=function(){this._measureElement.style.fontFamily=this._optionsService.options.fontFamily,this._measureElement.style.fontSize=this._optionsService.options.fontSize+"px";var e=this._measureElement.getBoundingClientRect();return 0!==e.width&&0!==e.height&&(this._result.width=e.width,this._result.height=Math.ceil(e.height)),this._result},e}()},function(e,t,r){"use strict";var i=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseService=void 0;var o=r(5),s=r(30),a=function(){function e(e,t){this._renderService=e,this._charSizeService=t}return e.prototype.getCoords=function(e,t,r,i,n){return s.getCoords(e,t,r,i,this._charSizeService.hasValidSize,this._renderService.dimensions.actualCellWidth,this._renderService.dimensions.actualCellHeight,n)},e.prototype.getRawByteCoords=function(e,t,r,i){var n=this.getCoords(e,t,r,i);return s.getRawByteCoords(n)},e=i([n(0,o.IRenderService),n(1,o.ICharSizeService)],e)}();t.MouseService=a},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),o=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Linkifier2=void 0;var a=r(1),c=r(0),l=r(2),h=r(7),u=function(e){function t(t){var r=e.call(this)||this;return r._bufferService=t,r._linkProviders=[],r._linkCacheDisposables=[],r._isMouseOut=!0,r._activeLine=-1,r._onShowLinkUnderline=r.register(new c.EventEmitter),r._onHideLinkUnderline=r.register(new c.EventEmitter),r.register(l.getDisposeArrayDisposable(r._linkCacheDisposables)),r}return n(t,e),Object.defineProperty(t.prototype,"onShowLinkUnderline",{get:function(){return this._onShowLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onHideLinkUnderline",{get:function(){return this._onHideLinkUnderline.event},enumerable:!1,configurable:!0}),t.prototype.registerLinkProvider=function(e){var t=this;return this._linkProviders.push(e),{dispose:function(){var r=t._linkProviders.indexOf(e);-1!==r&&t._linkProviders.splice(r,1)}}},t.prototype.attachToDom=function(e,t,r){var i=this;this._element=e,this._mouseService=t,this._renderService=r,this.register(h.addDisposableDomListener(this._element,"mouseleave",(function(){i._isMouseOut=!0,i._clearCurrentLink()}))),this.register(h.addDisposableDomListener(this._element,"mousemove",this._onMouseMove.bind(this))),this.register(h.addDisposableDomListener(this._element,"click",this._onClick.bind(this)))},t.prototype._onMouseMove=function(e){if(this._lastMouseEvent=e,this._element&&this._mouseService){var t=this._positionFromMouseEvent(e,this._element,this._mouseService);if(t){this._isMouseOut=!1;for(var r=e.composedPath(),i=0;ie?this._bufferService.cols:s.link.range.end.x,l=a;l<=c;l++){if(r.has(l)){n.splice(o--,1);break}r.add(l)}}},t.prototype._checkLinkProviderResult=function(e,t,r){var i,n=this;if(!this._activeProviderReplies)return r;for(var o=this._activeProviderReplies.get(e),s=!1,a=0;a=e&&this._currentLink.link.range.end.y<=t)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,l.disposeArray(this._linkCacheDisposables))},t.prototype._handleNewLink=function(e){var t=this;if(this._element&&this._lastMouseEvent&&this._mouseService){var r=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);r&&this._linkAtPosition(e.link,r)&&(this._currentLink=e,this._currentLink.state={decorations:{underline:void 0===e.link.decorations||e.link.decorations.underline,pointerCursor:void 0===e.link.decorations||e.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,e.link,this._lastMouseEvent),e.link.decorations={},Object.defineProperties(e.link.decorations,{pointerCursor:{get:function(){var e,r;return null===(r=null===(e=t._currentLink)||void 0===e?void 0:e.state)||void 0===r?void 0:r.decorations.pointerCursor},set:function(e){var r,i;(null===(r=t._currentLink)||void 0===r?void 0:r.state)&&t._currentLink.state.decorations.pointerCursor!==e&&(t._currentLink.state.decorations.pointerCursor=e,t._currentLink.state.isHovered&&(null===(i=t._element)||void 0===i||i.classList.toggle("xterm-cursor-pointer",e)))}},underline:{get:function(){var e,r;return null===(r=null===(e=t._currentLink)||void 0===e?void 0:e.state)||void 0===r?void 0:r.decorations.underline},set:function(r){var i,n,o;(null===(i=t._currentLink)||void 0===i?void 0:i.state)&&(null===(o=null===(n=t._currentLink)||void 0===n?void 0:n.state)||void 0===o?void 0:o.decorations.underline)!==r&&(t._currentLink.state.decorations.underline=r,t._currentLink.state.isHovered&&t._fireUnderlineEvent(e.link,r))}}}),this._renderService&&this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange((function(e){var r=0===e.start?0:e.start+1+t._bufferService.buffer.ydisp;t._clearCurrentLink(r,e.end+1+t._bufferService.buffer.ydisp)}))))}},t.prototype._linkHover=function(e,t,r){var i;(null===(i=this._currentLink)||void 0===i?void 0:i.state)&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!0),this._currentLink.state.decorations.pointerCursor&&e.classList.add("xterm-cursor-pointer")),t.hover&&t.hover(r,t.text)},t.prototype._fireUnderlineEvent=function(e,t){var r=e.range,i=this._bufferService.buffer.ydisp,n=this._createLinkUnderlineEvent(r.start.x-1,r.start.y-i-1,r.end.x,r.end.y-i-1,void 0);(t?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(n)},t.prototype._linkLeave=function(e,t,r){var i;(null===(i=this._currentLink)||void 0===i?void 0:i.state)&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!1),this._currentLink.state.decorations.pointerCursor&&e.classList.remove("xterm-cursor-pointer")),t.leave&&t.leave(r,t.text)},t.prototype._linkAtPosition=function(e,t){var r=e.range.start.y===e.range.end.y,i=e.range.start.yt.y;return(r&&e.range.start.x<=t.x&&e.range.end.x>=t.x||i&&e.range.end.x>=t.x||n&&e.range.start.x<=t.x||i&&n)&&e.range.start.y<=t.y&&e.range.end.y>=t.y},t.prototype._positionFromMouseEvent=function(e,t,r){var i=r.getCoords(e,t,this._bufferService.cols,this._bufferService.rows);if(i)return{x:i[0],y:i[1]+this._bufferService.buffer.ydisp}},t.prototype._createLinkUnderlineEvent=function(e,t,r,i,n){return{x1:e,y1:t,x2:r,y2:i,cols:this._bufferService.cols,fg:n}},t=o([s(0,a.IBufferService)],t)}(l.Disposable);t.Linkifier2=u},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CoreBrowserService=void 0;var i=function(){function e(e){this._textarea=e}return Object.defineProperty(e.prototype,"isFocused",{get:function(){return document.activeElement===this._textarea&&document.hasFocus()},enumerable:!1,configurable:!0}),e}();t.CoreBrowserService=i},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.CoreTerminal=void 0;var o=r(2),s=r(1),a=r(66),c=r(67),l=r(68),h=r(74),u=r(75),f=r(0),_=r(76),d=r(77),p=r(78),v=r(80),g=r(81),y=r(19),b=r(82),S=function(e){function t(t){var r=e.call(this)||this;return r._onBinary=new f.EventEmitter,r._onData=new f.EventEmitter,r._onLineFeed=new f.EventEmitter,r._onResize=new f.EventEmitter,r._onScroll=new f.EventEmitter,r._instantiationService=new a.InstantiationService,r.optionsService=new h.OptionsService(t),r._instantiationService.setService(s.IOptionsService,r.optionsService),r._bufferService=r.register(r._instantiationService.createInstance(l.BufferService)),r._instantiationService.setService(s.IBufferService,r._bufferService),r._logService=r._instantiationService.createInstance(c.LogService),r._instantiationService.setService(s.ILogService,r._logService),r._coreService=r.register(r._instantiationService.createInstance(u.CoreService,(function(){return r.scrollToBottom()}))),r._instantiationService.setService(s.ICoreService,r._coreService),r._coreMouseService=r._instantiationService.createInstance(_.CoreMouseService),r._instantiationService.setService(s.ICoreMouseService,r._coreMouseService),r._dirtyRowService=r._instantiationService.createInstance(d.DirtyRowService),r._instantiationService.setService(s.IDirtyRowService,r._dirtyRowService),r.unicodeService=r._instantiationService.createInstance(p.UnicodeService),r._instantiationService.setService(s.IUnicodeService,r.unicodeService),r._charsetService=r._instantiationService.createInstance(v.CharsetService),r._instantiationService.setService(s.ICharsetService,r._charsetService),r._inputHandler=new y.InputHandler(r._bufferService,r._charsetService,r._coreService,r._dirtyRowService,r._logService,r.optionsService,r._coreMouseService,r.unicodeService),r.register(f.forwardEvent(r._inputHandler.onLineFeed,r._onLineFeed)),r.register(r._inputHandler),r.register(f.forwardEvent(r._bufferService.onResize,r._onResize)),r.register(f.forwardEvent(r._coreService.onData,r._onData)),r.register(f.forwardEvent(r._coreService.onBinary,r._onBinary)),r.register(r.optionsService.onOptionChange((function(e){return r._updateOptions(e)}))),r._writeBuffer=new b.WriteBuffer((function(e){return r._inputHandler.parse(e)})),r}return n(t,e),Object.defineProperty(t.prototype,"onBinary",{get:function(){return this._onBinary.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onData",{get:function(){return this._onData.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onLineFeed",{get:function(){return this._onLineFeed.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onResize",{get:function(){return this._onResize.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onScroll",{get:function(){return this._onScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"cols",{get:function(){return this._bufferService.cols},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"rows",{get:function(){return this._bufferService.rows},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"buffers",{get:function(){return this._bufferService.buffers},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){var t;this._isDisposed||(e.prototype.dispose.call(this),null===(t=this._windowsMode)||void 0===t||t.dispose(),this._windowsMode=void 0)},t.prototype.write=function(e,t){this._writeBuffer.write(e,t)},t.prototype.writeSync=function(e){this._writeBuffer.writeSync(e)},t.prototype.resize=function(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,l.MINIMUM_COLS),t=Math.max(t,l.MINIMUM_ROWS),this._bufferService.resize(e,t))},t.prototype.scroll=function(e,t){void 0===t&&(t=!1);var r,i=this._bufferService.buffer;(r=this._cachedBlankLine)&&r.length===this.cols&&r.getFg(0)===e.fg&&r.getBg(0)===e.bg||(r=i.getBlankLine(e,t),this._cachedBlankLine=r),r.isWrapped=t;var n=i.ybase+i.scrollTop,o=i.ybase+i.scrollBottom;if(0===i.scrollTop){var s=i.lines.isFull;o===i.lines.length-1?s?i.lines.recycle().copyFrom(r):i.lines.push(r.clone()):i.lines.splice(o+1,0,r.clone()),s?this._bufferService.isUserScrolling&&(i.ydisp=Math.max(i.ydisp-1,0)):(i.ybase++,this._bufferService.isUserScrolling||i.ydisp++)}else{var a=o-n+1;i.lines.shiftElements(n+1,a-1,-1),i.lines.set(o,r.clone())}this._bufferService.isUserScrolling||(i.ydisp=i.ybase),this._dirtyRowService.markRangeDirty(i.scrollTop,i.scrollBottom),this._onScroll.fire(i.ydisp)},t.prototype.scrollLines=function(e,t){var r=this._bufferService.buffer;if(e<0){if(0===r.ydisp)return;this._bufferService.isUserScrolling=!0}else e+r.ydisp>=r.ybase&&(this._bufferService.isUserScrolling=!1);var i=r.ydisp;r.ydisp=Math.max(Math.min(r.ydisp+e,r.ybase),0),i!==r.ydisp&&(t||this._onScroll.fire(r.ydisp))},t.prototype.scrollPages=function(e){this.scrollLines(e*(this.rows-1))},t.prototype.scrollToTop=function(){this.scrollLines(-this._bufferService.buffer.ydisp)},t.prototype.scrollToBottom=function(){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)},t.prototype.scrollToLine=function(e){var t=e-this._bufferService.buffer.ydisp;0!==t&&this.scrollLines(t)},t.prototype.addEscHandler=function(e,t){return this._inputHandler.addEscHandler(e,t)},t.prototype.addDcsHandler=function(e,t){return this._inputHandler.addDcsHandler(e,t)},t.prototype.addCsiHandler=function(e,t){return this._inputHandler.addCsiHandler(e,t)},t.prototype.addOscHandler=function(e,t){return this._inputHandler.addOscHandler(e,t)},t.prototype._setup=function(){this.optionsService.options.windowsMode&&this._enableWindowsMode()},t.prototype.reset=function(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this._coreService.reset(),this._coreMouseService.reset()},t.prototype._updateOptions=function(e){var t;switch(e){case"scrollback":this.buffers.resize(this.cols,this.rows);break;case"windowsMode":this.optionsService.options.windowsMode?this._enableWindowsMode():(null===(t=this._windowsMode)||void 0===t||t.dispose(),this._windowsMode=void 0)}},t.prototype._enableWindowsMode=function(){var e=this;if(!this._windowsMode){var t=[];t.push(this.onLineFeed(g.updateWindowsModeWrappedState.bind(null,this._bufferService))),t.push(this.addCsiHandler({final:"H"},(function(){return g.updateWindowsModeWrappedState(e._bufferService),!1}))),this._windowsMode={dispose:function(){for(var e=0,r=t;e0?n[0].index:t.length;if(t.length!==u)throw new Error("[createInstance] First service dependency of "+e.name+" at position "+(u+1)+" conflicts with "+t.length+" static arguments");return new(e.bind.apply(e,i([void 0],i(t,s))))},e}();t.InstantiationService=a},function(e,t,r){"use strict";var i=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}},o=this&&this.__spreadArrays||function(){for(var e=0,t=0,r=arguments.length;t=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.BufferService=t.MINIMUM_ROWS=t.MINIMUM_COLS=void 0;var a=r(1),c=r(69),l=r(0),h=r(2);t.MINIMUM_COLS=2,t.MINIMUM_ROWS=1;var u=function(e){function r(r){var i=e.call(this)||this;return i._optionsService=r,i.isUserScrolling=!1,i._onResize=new l.EventEmitter,i.cols=Math.max(r.options.cols,t.MINIMUM_COLS),i.rows=Math.max(r.options.rows,t.MINIMUM_ROWS),i.buffers=new c.BufferSet(r,i),i}return n(r,e),Object.defineProperty(r.prototype,"onResize",{get:function(){return this._onResize.event},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"buffer",{get:function(){return this.buffers.active},enumerable:!1,configurable:!0}),r.prototype.dispose=function(){e.prototype.dispose.call(this),this.buffers.dispose()},r.prototype.resize=function(e,t){this.cols=e,this.rows=t,this.buffers.resize(e,t),this.buffers.setupTabStops(this.cols),this._onResize.fire({cols:e,rows:t})},r.prototype.reset=function(){this.buffers.dispose(),this.buffers=new c.BufferSet(this._optionsService,this),this.isUserScrolling=!1},r=o([s(0,a.IOptionsService)],r)}(h.Disposable);t.BufferService=u},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.BufferSet=void 0;var o=r(70),s=r(0),a=function(e){function t(t,r){var i=e.call(this)||this;return i._onBufferActivate=i.register(new s.EventEmitter),i._normal=new o.Buffer(!0,t,r),i._normal.fillViewportRows(),i._alt=new o.Buffer(!1,t,r),i._activeBuffer=i._normal,i.setupTabStops(),i}return n(t,e),Object.defineProperty(t.prototype,"onBufferActivate",{get:function(){return this._onBufferActivate.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"alt",{get:function(){return this._alt},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"active",{get:function(){return this._activeBuffer},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"normal",{get:function(){return this._normal},enumerable:!1,configurable:!0}),t.prototype.activateNormalBuffer=function(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))},t.prototype.activateAltBuffer=function(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))},t.prototype.resize=function(e,t){this._normal.resize(e,t),this._alt.resize(e,t)},t.prototype.setupTabStops=function(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)},t}(r(2).Disposable);t.BufferSet=a},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.BufferStringIterator=t.Buffer=t.MAX_BUFFER_SIZE=void 0;var i=r(71),n=r(16),o=r(4),s=r(3),a=r(72),c=r(73),l=r(20),h=r(6);t.MAX_BUFFER_SIZE=4294967295;var u=function(){function e(e,t,r){this._hasScrollback=e,this._optionsService=t,this._bufferService=r,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.savedY=0,this.savedX=0,this.savedCurAttrData=n.DEFAULT_ATTR_DATA.clone(),this.savedCharset=l.DEFAULT_CHARSET,this.markers=[],this._nullCell=o.CellData.fromCharData([0,s.NULL_CELL_CHAR,s.NULL_CELL_WIDTH,s.NULL_CELL_CODE]),this._whitespaceCell=o.CellData.fromCharData([0,s.WHITESPACE_CELL_CHAR,s.WHITESPACE_CELL_WIDTH,s.WHITESPACE_CELL_CODE]),this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new i.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}return e.prototype.getNullCell=function(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new h.ExtendedAttrs),this._nullCell},e.prototype.getWhitespaceCell=function(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new h.ExtendedAttrs),this._whitespaceCell},e.prototype.getBlankLine=function(e,t){return new n.BufferLine(this._bufferService.cols,this.getNullCell(e),t)},Object.defineProperty(e.prototype,"hasScrollback",{get:function(){return this._hasScrollback&&this.lines.maxLength>this._rows},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isCursorInViewport",{get:function(){var e=this.ybase+this.y-this.ydisp;return e>=0&&et.MAX_BUFFER_SIZE?t.MAX_BUFFER_SIZE:r},e.prototype.fillViewportRows=function(e){if(0===this.lines.length){void 0===e&&(e=n.DEFAULT_ATTR_DATA);for(var t=this._rows;t--;)this.lines.push(this.getBlankLine(e))}},e.prototype.clear=function(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new i.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()},e.prototype.resize=function(e,t){var r=this.getNullCell(n.DEFAULT_ATTR_DATA),i=this._getCorrectBufferLength(t);if(i>this.lines.maxLength&&(this.lines.maxLength=i),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+s+1?(this.ybase--,s++,this.ydisp>0&&this.ydisp--):this.lines.push(new n.BufferLine(e,r)));else for(a=this._rows;a>t;a--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(i0&&(this.lines.trimStart(c),this.ybase=Math.max(this.ybase-c,0),this.ydisp=Math.max(this.ydisp-c,0),this.savedY=Math.max(this.savedY-c,0)),this.lines.maxLength=i}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),s&&(this.y+=s),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(o=0;othis._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))},e.prototype._reflowLarger=function(e,t){var r=a.reflowLargerGetLinesToRemove(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(n.DEFAULT_ATTR_DATA));if(r.length>0){var i=a.reflowLargerCreateNewLayout(this.lines,r);a.reflowLargerApplyNewLayout(this.lines,i.layout),this._reflowLargerAdjustViewport(e,t,i.countRemoved)}},e.prototype._reflowLargerAdjustViewport=function(e,t,r){for(var i=this.getNullCell(n.DEFAULT_ATTR_DATA),o=r;o-- >0;)0===this.ybase?(this.y>0&&this.y--,this.lines.length=0;s--){var c=this.lines.get(s);if(!(!c||!c.isWrapped&&c.getTrimmedLength()<=e)){for(var l=[c];c.isWrapped&&s>0;)c=this.lines.get(--s),l.unshift(c);var h=this.ybase+this.y;if(!(h>=s&&h0&&(i.push({start:s+l.length+o,newLines:p}),o+=p.length),l.push.apply(l,p);var y=f.length-1,b=f[y];0===b&&(b=f[--y]);for(var S=l.length-_-1,m=u;S>=0;){var C=Math.min(m,b);if(l[y].copyCellsFrom(l[S],m-C,b-C,C,!0),0===(b-=C)&&(b=f[--y]),0===(m-=C)){S--;var w=Math.max(S,0);m=a.getWrappedLineTrimmedLength(l,w,this._cols)}}for(v=0;v0;)0===this.ybase?this.y0){var L=[],A=[];for(v=0;v=0;v--)if(D&&D.start>k+T){for(var O=D.newLines.length-1;O>=0;O--)this.lines.set(v--,D.newLines[O]);v++,L.push({index:k+1,amount:D.newLines.length}),T+=D.newLines.length,D=i[++x]}else this.lines.set(v,A[k--]);var M=0;for(v=L.length-1;v>=0;v--)L[v].index+=M,this.lines.onInsertEmitter.fire(L[v]),M+=L[v].amount;var P=Math.max(0,R+o-this.lines.maxLength);P>0&&this.lines.onTrimEmitter.fire(P)}},e.prototype.stringIndexToBufferIndex=function(e,t,r){for(void 0===r&&(r=!1);t;){var i=this.lines.get(e);if(!i)return[-1,-1];for(var n=r?i.getTrimmedLength():i.length,o=0;o0&&this.lines.get(t).isWrapped;)t--;for(;r+10;);return e>=this._cols?this._cols-1:e<0?0:e},e.prototype.nextStop=function(e){for(null==e&&(e=this.x);!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e},e.prototype.addMarker=function(e){var t=this,r=new c.Marker(e);return this.markers.push(r),r.register(this.lines.onTrim((function(e){r.line-=e,r.line<0&&r.dispose()}))),r.register(this.lines.onInsert((function(e){r.line>=e.index&&(r.line+=e.amount)}))),r.register(this.lines.onDelete((function(e){r.line>=e.index&&r.linee.index&&(r.line-=e.amount)}))),r.register(r.onDispose((function(){return t._removeMarker(r)}))),r},e.prototype._removeMarker=function(e){this.markers.splice(this.markers.indexOf(e),1)},e.prototype.iterator=function(e,t,r,i,n){return new f(this,e,t,r,i,n)},e}();t.Buffer=u;var f=function(){function e(e,t,r,i,n,o){void 0===r&&(r=0),void 0===i&&(i=e.lines.length),void 0===n&&(n=0),void 0===o&&(o=0),this._buffer=e,this._trimRight=t,this._startIndex=r,this._endIndex=i,this._startOverscan=n,this._endOverscan=o,this._startIndex<0&&(this._startIndex=0),this._endIndex>this._buffer.lines.length&&(this._endIndex=this._buffer.lines.length),this._current=this._startIndex}return e.prototype.hasNext=function(){return this._currentthis._endIndex+this._endOverscan&&(e.last=this._endIndex+this._endOverscan),e.first=Math.max(e.first,0),e.last=Math.min(e.last,this._buffer.lines.length);for(var t="",r=e.first;r<=e.last;++r)t+=this._buffer.translateBufferLineToString(r,this._trimRight);return this._current=e.last+1,{range:e,content:t}},e}();t.BufferStringIterator=f},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CircularList=void 0;var i=r(0),n=function(){function e(e){this._maxLength=e,this.onDeleteEmitter=new i.EventEmitter,this.onInsertEmitter=new i.EventEmitter,this.onTrimEmitter=new i.EventEmitter,this._array=new Array(this._maxLength),this._startIndex=0,this._length=0}return Object.defineProperty(e.prototype,"onDelete",{get:function(){return this.onDeleteEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onInsert",{get:function(){return this.onInsertEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onTrim",{get:function(){return this.onTrimEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"maxLength",{get:function(){return this._maxLength},set:function(e){if(this._maxLength!==e){for(var t=new Array(e),r=0;rthis._length)for(var t=this._length;t=e;n--)this._array[this._getCyclicIndex(n+r.length)]=this._array[this._getCyclicIndex(n)];for(n=0;nthis._maxLength){var o=this._length+r.length-this._maxLength;this._startIndex+=o,this._length=this._maxLength,this.onTrimEmitter.fire(o)}else this._length+=r.length},e.prototype.trimStart=function(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)},e.prototype.shiftElements=function(e,t,r){if(!(t<=0)){if(e<0||e>=this._length)throw new Error("start argument out of range");if(e+r<0)throw new Error("Cannot shift elements in list beyond index 0");if(r>0){for(var i=t-1;i>=0;i--)this.set(e+i+r,this.get(e+i));var n=e+t+r-this._length;if(n>0)for(this._length+=n;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(i=0;i=a&&n0&&(S>u||0===h[S].getTrimmedLength());S--)b++;b>0&&(s.push(a+h.length-b),s.push(b)),a+=h.length-1}}}return s},t.reflowLargerCreateNewLayout=function(e,t){for(var r=[],i=0,n=t[i],o=0,s=0;sl&&(s-=l,a++);var h=2===e[a].getWidth(s-1);h&&s--;var u=h?r-1:r;n.push(u),c+=u}return n},t.getWrappedLineTrimmedLength=i},function(e,t,r){"use strict";var i,n=this&&this.__extends||(i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0}),t.Marker=void 0;var o=r(0),s=function(e){function t(r){var i=e.call(this)||this;return i.line=r,i._id=t._nextId++,i.isDisposed=!1,i._onDispose=new o.EventEmitter,i}return n(t,e),Object.defineProperty(t.prototype,"id",{get:function(){return this._id},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onDispose",{get:function(){return this._onDispose.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire())},t._nextId=1,t}(r(2).Disposable);t.Marker=s},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.OptionsService=t.DEFAULT_OPTIONS=t.DEFAULT_BELL_SOUND=void 0;var i=r(0),n=r(11),o=r(33);t.DEFAULT_BELL_SOUND="data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",t.DEFAULT_OPTIONS=Object.freeze({cols:80,rows:24,cursorBlink:!1,cursorStyle:"block",cursorWidth:1,bellSound:t.DEFAULT_BELL_SOUND,bellStyle:"none",drawBoldTextInBrightColors:!0,fastScrollModifier:"alt",fastScrollSensitivity:5,fontFamily:"courier-new, courier, monospace",fontSize:15,fontWeight:"normal",fontWeightBold:"bold",lineHeight:1,linkTooltipHoverDuration:500,letterSpacing:0,logLevel:"info",scrollback:1e3,scrollSensitivity:1,screenReaderMode:!1,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,disableStdin:!1,allowProposedApi:!0,allowTransparency:!1,tabStopWidth:8,theme:{},rightClickSelectsWord:n.isMac,rendererType:"canvas",windowOptions:{},windowsMode:!1,wordSeparator:" ()[]{}',\"`",convertEol:!1,termName:"xterm",cancelEvents:!1});var s=["cols","rows"],a=function(){function e(e){this._onOptionChange=new i.EventEmitter,this.options=o.clone(t.DEFAULT_OPTIONS);for(var r=0,n=Object.keys(e);r=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},s=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreService=void 0;var a=r(1),c=r(0),l=r(33),h=r(2),u=Object.freeze({insertMode:!1}),f=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,origin:!1,reverseWraparound:!1,sendFocus:!1,wraparound:!0}),_=function(e){function t(t,r,i,n){var o=e.call(this)||this;return o._bufferService=r,o._logService=i,o._optionsService=n,o.isCursorInitialized=!1,o.isCursorHidden=!1,o._onData=o.register(new c.EventEmitter),o._onUserInput=o.register(new c.EventEmitter),o._onBinary=o.register(new c.EventEmitter),o._scrollToBottom=t,o.register({dispose:function(){return o._scrollToBottom=void 0}}),o.modes=l.clone(u),o.decPrivateModes=l.clone(f),o}return n(t,e),Object.defineProperty(t.prototype,"onData",{get:function(){return this._onData.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onUserInput",{get:function(){return this._onUserInput.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onBinary",{get:function(){return this._onBinary.event},enumerable:!1,configurable:!0}),t.prototype.reset=function(){this.modes=l.clone(u),this.decPrivateModes=l.clone(f)},t.prototype.triggerDataEvent=function(e,t){if(void 0===t&&(t=!1),!this._optionsService.options.disableStdin){var r=this._bufferService.buffer;r.ybase!==r.ydisp&&this._scrollToBottom(),t&&this._onUserInput.fire(),this._logService.debug('sending data "'+e+'"',(function(){return e.split("").map((function(e){return e.charCodeAt(0)}))})),this._onData.fire(e)}},t.prototype.triggerBinaryEvent=function(e){this._optionsService.options.disableStdin||(this._logService.debug('sending binary "'+e+'"',(function(){return e.split("").map((function(e){return e.charCodeAt(0)}))})),this._onBinary.fire(e))},t=o([s(1,a.IBufferService),s(2,a.ILogService),s(3,a.IOptionsService)],t)}(h.Disposable);t.CoreService=_},function(e,t,r){"use strict";var i=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreMouseService=void 0;var o=r(1),s=r(0),a={NONE:{events:0,restrict:function(){return!1}},X10:{events:1,restrict:function(e){return 4!==e.button&&1===e.action&&(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)}},VT200:{events:19,restrict:function(e){return 32!==e.action}},DRAG:{events:23,restrict:function(e){return 32!==e.action||3!==e.button}},ANY:{events:31,restrict:function(e){return!0}}};function c(e,t){var r=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return 4===e.button?(r|=64,r|=e.action):(r|=3&e.button,4&e.button&&(r|=64),8&e.button&&(r|=128),32===e.action?r|=32:0!==e.action||t||(r|=3)),r}var l=String.fromCharCode,h={DEFAULT:function(e){var t=[c(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?"":""+l(t[0])+l(t[1])+l(t[2])},SGR:function(e){var t=0===e.action&&4!==e.button?"m":"M";return"[<"+c(e,!0)+";"+e.col+";"+e.row+t}},u=function(){function e(e,t){this._bufferService=e,this._coreService=t,this._protocols={},this._encodings={},this._activeProtocol="",this._activeEncoding="",this._onProtocolChange=new s.EventEmitter,this._lastEvent=null;for(var r=0,i=Object.keys(a);r=this._bufferService.cols||e.row<0||e.row>=this._bufferService.rows)return!1;if(4===e.button&&32===e.action)return!1;if(3===e.button&&32!==e.action)return!1;if(4!==e.button&&(2===e.action||3===e.action))return!1;if(e.col++,e.row++,32===e.action&&this._lastEvent&&this._compareEvents(this._lastEvent,e))return!1;if(!this._protocols[this._activeProtocol].restrict(e))return!1;var t=this._encodings[this._activeEncoding](e);return t&&("DEFAULT"===this._activeEncoding?this._coreService.triggerBinaryEvent(t):this._coreService.triggerDataEvent(t,!0)),this._lastEvent=e,!0},e.prototype.explainEvents=function(e){return{down:!!(1&e),up:!!(2&e),drag:!!(4&e),move:!!(8&e),wheel:!!(16&e)}},e.prototype._compareEvents=function(e,t){return e.col===t.col&&(e.row===t.row&&(e.button===t.button&&(e.action===t.action&&(e.ctrl===t.ctrl&&(e.alt===t.alt&&e.shift===t.shift)))))},e=i([n(0,o.IBufferService),n(1,o.ICoreService)],e)}();t.CoreMouseService=u},function(e,t,r){"use strict";var i=this&&this.__decorate||function(e,t,r,i){var n,o=arguments.length,s=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,i);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(o<3?n(s):o>3?n(t,r,s):n(t,r))||s);return o>3&&s&&Object.defineProperty(t,r,s),s},n=this&&this.__param||function(e,t){return function(r,i){t(r,i,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DirtyRowService=void 0;var o=r(1),s=function(){function e(e){this._bufferService=e,this.clearRange()}return Object.defineProperty(e.prototype,"start",{get:function(){return this._start},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"end",{get:function(){return this._end},enumerable:!1,configurable:!0}),e.prototype.clearRange=function(){this._start=this._bufferService.buffer.y,this._end=this._bufferService.buffer.y},e.prototype.markDirty=function(e){ethis._end&&(this._end=e)},e.prototype.markRangeDirty=function(e,t){if(e>t){var r=e;e=t,t=r}ethis._end&&(this._end=t)},e.prototype.markAllDirty=function(){this.markRangeDirty(0,this._bufferService.rows-1)},e=i([n(0,o.IBufferService)],e)}();t.DirtyRowService=s},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeService=void 0;var i=r(0),n=r(79),o=function(){function e(){this._providers=Object.create(null),this._active="",this._onChange=new i.EventEmitter;var e=new n.UnicodeV6;this.register(e),this._active=e.version,this._activeProvider=e}return Object.defineProperty(e.prototype,"onChange",{get:function(){return this._onChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"versions",{get:function(){return Object.keys(this._providers)},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"activeVersion",{get:function(){return this._active},set:function(e){if(!this._providers[e])throw new Error('unknown Unicode version "'+e+'"');this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)},enumerable:!1,configurable:!0}),e.prototype.register=function(e){this._providers[e.version]=e},e.prototype.wcwidth=function(e){return this._activeProvider.wcwidth(e)},e.prototype.getStringCellWidth=function(e){for(var t=0,r=e.length,i=0;i=r)return t+this.wcwidth(n);var o=e.charCodeAt(i);56320<=o&&o<=57343?n=1024*(n-55296)+o-56320+65536:t+=this.wcwidth(o)}t+=this.wcwidth(n)}return t},e}();t.UnicodeService=o},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;var i,n=r(15),o=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],s=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]];var a=function(){function e(){if(this.version="6",!i){i=new Uint8Array(65536),n.fill(i,1),i[0]=0,n.fill(i,0,1,32),n.fill(i,0,127,160),n.fill(i,2,4352,4448),i[9001]=2,i[9002]=2,n.fill(i,2,11904,42192),i[12351]=1,n.fill(i,2,44032,55204),n.fill(i,2,63744,64256),n.fill(i,2,65040,65050),n.fill(i,2,65072,65136),n.fill(i,2,65280,65377),n.fill(i,2,65504,65511);for(var e=0;et[n][1])return!1;for(;n>=i;)if(e>t[r=i+n>>1][1])i=r+1;else{if(!(e=131072&&e<=196605||e>=196608&&e<=262141?2:1},e}();t.UnicodeV6=a},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CharsetService=void 0;var i=function(){function e(){this.glevel=0,this._charsets=[]}return e.prototype.reset=function(){this.charset=void 0,this._charsets=[],this.glevel=0},e.prototype.setgLevel=function(e){this.glevel=e,this.charset=this._charsets[e]},e.prototype.setgCharset=function(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)},e}();t.CharsetService=i},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.updateWindowsModeWrappedState=void 0;var i=r(3);t.updateWindowsModeWrappedState=function(e){var t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1),r=null==t?void 0:t.get(e.cols-1),n=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);n&&r&&(n.isWrapped=r[i.CHAR_DATA_CODE_INDEX]!==i.NULL_CELL_CODE&&r[i.CHAR_DATA_CODE_INDEX]!==i.WHITESPACE_CELL_CODE)}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.WriteBuffer=void 0;var i=function(){function e(e){this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0}return e.prototype.writeSync=function(e){if(this._writeBuffer.length){for(var t=this._bufferOffset;t5e7)throw new Error("write data discarded, use flow control to avoid losing data");this._writeBuffer.length||(this._bufferOffset=0,setTimeout((function(){return r._innerWrite()}))),this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)},e.prototype._innerWrite=function(){for(var e=this,t=Date.now();this._writeBuffer.length>this._bufferOffset;){var r=this._writeBuffer[this._bufferOffset],i=this._callbacks[this._bufferOffset];if(this._bufferOffset++,this._action(r),this._pendingData-=r.length,i&&i(),Date.now()-t>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout((function(){return e._innerWrite()}),0)):(this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0)},e}();t.WriteBuffer=i},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AddonManager=void 0;var i=function(){function e(){this._addons=[]}return e.prototype.dispose=function(){for(var e=this._addons.length-1;e>=0;e--)this._addons[e].instance.dispose()},e.prototype.loadAddon=function(e,t){var r=this,i={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(i),t.dispose=function(){return r._wrappedAddonDispose(i)},t.activate(e)},e.prototype._wrappedAddonDispose=function(e){if(!e.isDisposed){for(var t=-1,r=0;rr.cols;)p-=r.cols,h++;var v={start:{x:l+1,y:d+1},end:{x:p,y:h}};c.push({range:v,text:f,activate:i})}return c},e._translateBufferLineToStringWithWrap=function(e,t,n){var r,i,o="";do{if(!(s=n.buffer.active.getLine(e)))break;s.isWrapped&&e--,i=s.isWrapped}while(i);var a=e;do{var s,u=n.buffer.active.getLine(e+1);if(r=!!u&&u.isWrapped,!(s=n.buffer.active.getLine(e)))break;o+=s.translateToString(!r&&t).substring(0,n.cols),e++}while(r);return[o,a]},e}();t.LinkComputer=i}])})); \ No newline at end of file diff --git a/BTPanel/templates/default/autherr.html b/BTPanel/templates/default/autherr.html index 2cdc7789..67605900 100644 --- a/BTPanel/templates/default/autherr.html +++ b/BTPanel/templates/default/autherr.html @@ -1,168 +1,145 @@ - + + + + + Ingress verification failed + - + .logo { + cursor: pointer; + } + + - -
                                    - - - + + + diff --git a/BTPanel/templates/default/error3.html b/BTPanel/templates/default/error3.html index 7f79dacc..4c3a5130 100644 --- a/BTPanel/templates/default/error3.html +++ b/BTPanel/templates/default/error3.html @@ -1,283 +1,292 @@ -{% extends "layout.html" %} -{% block content %} +{% extends "layout.html" %} {% block content %} -
                                    -
                                    +
                                    +
                                    {% if 'error_msg' in data %} -
                                    -
                                    -
                                    aaPanel WAF
                                    -
                                    +
                                    +
                                    +
                                    aaPanel WAF
                                    -
                                    -
                                    -

                                    Tip: This page can be turned off in the panel settings

                                    -
                                    -
                                    -
                                    - Nginx WAF function introduction -
                                      -
                                    • Only supports Nginx
                                    • -
                                    • Defend against CC attacks
                                    • -
                                    • Keyword blocking
                                    • -
                                    • Block malicious scans
                                    • -
                                    • Stop hackers
                                    • -
                                    -
                                    - Buy now -
                                    +
                                    +
                                    +
                                    +

                                    Tip: This page can be turned off in the panel settings

                                    +
                                    +
                                    +
                                    + Nginx WAF function introduction +
                                      +
                                    • Only supports Nginx
                                    • +
                                    • Defend against CC attacks
                                    • +
                                    • Keyword blocking
                                    • +
                                    • Block malicious scans
                                    • +
                                    • Stop hackers
                                    • +
                                    + -
                                    -
                                      -
                                    • Overview
                                    • -
                                    • Report
                                    • -
                                    • Global
                                    • -
                                    • WebSite
                                    • -
                                    • Blockade
                                    • -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    +
                                    +
                                    +
                                      +
                                    • Overview
                                    • +
                                    • Report
                                    • +
                                    • Global
                                    • +
                                    • WebSite
                                    • +
                                    • Blockade
                                    • +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    {% else %} -
                                    -
                                    -
                                    aaPanel WAF
                                    -
                                    +
                                    +
                                    +
                                    aaPanel WAF
                                    -
                                    -
                                    -

                                    Tip: This page can be turned off in the panel settings

                                    -
                                    -
                                    -
                                    - Nginx WAF function introduction -
                                      -
                                    • Only supports Nginx
                                    • -
                                    • Defend against CC attacks
                                    • -
                                    • Keyword blocking
                                    • -
                                    • Block malicious scans
                                    • -
                                    • Stop hackers
                                    • -
                                    -
                                    - Buy now -
                                    +
                                    +
                                    +
                                    +

                                    Tip: This page can be turned off in the panel settings

                                    +
                                    +
                                    +
                                    + Nginx WAF function introduction +
                                      +
                                    • Only supports Nginx
                                    • +
                                    • Defend against CC attacks
                                    • +
                                    • Keyword blocking
                                    • +
                                    • Block malicious scans
                                    • +
                                    • Stop hackers
                                    • +
                                    + -
                                    -
                                      -
                                    • Overview
                                    • -
                                    • Report
                                    • -
                                    • Global
                                    • -
                                    • WebSite
                                    • -
                                    • Blockade
                                    • -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    +
                                    +
                                    +
                                      +
                                    • Overview
                                    • +
                                    • Report
                                    • +
                                    • Global
                                    • +
                                    • WebSite
                                    • +
                                    • Blockade
                                    • +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    {% endif %} -
                                    +
                                    -{% endblock %} -{% block scripts %} - {{ super() }} - + -{% endblock %} \ No newline at end of file + } + ); + }, 1000); + } + // $('.thumbnail-box').on('click',function(){ + // layer.open({ + // title:false, + // btn:false, + // shadeClose:true, + // closeBtn: 2, + // area:['950px','725px'], + // content:'
                                    ' + // }) + // }) + +{% endblock %} diff --git a/BTPanel/templates/default/firewall.html b/BTPanel/templates/default/firewall.html index b31ca809..54309acb 100644 --- a/BTPanel/templates/default/firewall.html +++ b/BTPanel/templates/default/firewall.html @@ -842,5 +842,6 @@ {% endblock %} {% block scripts %} {{ super() }} + {% endblock %} diff --git a/BTPanel/templates/default/index_new.html b/BTPanel/templates/default/index_new.html new file mode 100644 index 00000000..7fa969ab --- /dev/null +++ b/BTPanel/templates/default/index_new.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + aaPanel Linux Panel + + + + + + + + + + + + + +
                                    + + + + + + diff --git a/BTPanel/templates/default/layout.html b/BTPanel/templates/default/layout.html index 08a851aa..4b76e9b3 100644 --- a/BTPanel/templates/default/layout.html +++ b/BTPanel/templates/default/layout.html @@ -1,257 +1,258 @@ - + - - - - - - {{g.title}} - - - - - {% for css_f in g['other_css'] %} - - {% endfor %} - - - - - -
                                    - - -
                                    - - - {% block content %}{% endblock %} -
                                    - + .contextmenu li:hover { + background: #707070; + border-left: 3px solid #333; + } + + .contextmenu li a { + display: block; + padding: 5px 10px; + color: #000000; + text-decoration: none; + transition: ease 0.3s; + cursor: default; + } + + .contextmenu li:hover a { + color: #fff; + } + .toolbar-right { + width: 35px; + height: 35px; + position: fixed; + right: 0; + bottom: 120px; + display: flex; + background-color: #fff; + box-shadow: 0 0 4px 0 #ccc; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + border-radius: 4px; + } + + .toolbar-right .feedback { + display: inline-block; + height: 35px; + width: 35px; + position: relative; + background-repeat: no-repeat; + background-position: center center; + border-radius: 4px; + background-size: 13.5px; + } + .toolbar-right .feedback { + background-image: url('/static/img/feedback.svg'); + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } + + .feedback-iframe .layui-layer-content { + overflow: hidden !important; + } + + + + +
                                    + + +
                                    + + + {% block content %}{% endblock %} +
                                    + +
                                    +
                                    - -
                                    - + - {% if request.path == '/btwaf/index' %} - - - - - - + {% if request.path == '/btwaf/index' %} + + + + + + - - - - {% endif %} + + + + {% endif %} {% block scripts %} + + + + + + - {% block scripts %} - - - - - - - - - - - - - - - {% for js_f in g['other_js'] %} - - {% endfor %} - + + + + + + {% for js_f in g['other_js'] %} + + {% endfor %} + - {% endblock %} -
                                    - + if (bt.get_cookie('bt_user_info') == null || bt.get_cookie('bt_user_info') == '') { + bt.pub.get_user_info(function (userInfo) { + if (userInfo.status) { + bt.set_cookie('bt_user_info', JSON.stringify(userInfo)); + getPaymentStatus(); + } + }); + } else { + getPaymentStatus(); + } + + {% endblock %} + +
                                    + + diff --git a/BTPanel/templates/default/login.html b/BTPanel/templates/default/login.html index 9e329e80..c02a6578 100644 --- a/BTPanel/templates/default/login.html +++ b/BTPanel/templates/default/login.html @@ -1,750 +1,766 @@ - - - - - {{g.title}} - - + + + +
                                    +
                                    + +
                                    +
                                    {{data['lan']['QR_CORE_LOGIN']}}
                                    +
                                    +
                                    + +
                                    +
                                    +
                                    Scan
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    {{data['lan']['SCAN_MORE_SAFETY']}}
                                    +
                                    +
                                    +
                                    +
                                    + + + + - - -
                                    -
                                    - -
                                    -
                                    {{data['lan']['QR_CORE_LOGIN']}}
                                    -
                                    -
                                    - -
                                    -
                                    -
                                    Scan
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    -
                                    {{data['lan']['SCAN_MORE_SAFETY']}}
                                    -
                                    -
                                    -
                                    -
                                    - - - - - - \ No newline at end of file + callback: function () { + var btn = document.querySelector('#auth_verif_btn'); + var codeInput = document.querySelector('.v_code'); + codeInput.onkeydown = function (e) { + if (e.keyCode == 13) btn.click(); + }; + btn.onclick = function () { + var code = codeInput.value.trim(); + if (is_empty(code)) { + popup.msg('Please enter Verification code!'); + return; + } + data['vcode'] = code; + popup.msg('{{data.lan.JS2}}', { icon: 16, shade: [0.3, '#333'] }); + request.post('/login', data, function (rdata) { + popup.remove_msg(); + if (!rdata.status) { + popup.msg(rdata.msg, { icon: 2 }); + return false; + } + popup.msg( + rdata.msg, + { + icon: 16, + time: 800, + shade: [0.3, '#000'], + }, + function () { + window.location.href = '/'; + } + ); + }); + }; + }, + }); + return; + } + var status = res.status; + if (!status) { + popup.msg(res.msg); + codeInput.value = ''; + passwordInput.value = ''; + errorTipsBox.innerHTML = res.msg; + loginBox.classList.add('code'); + show_check_img(); + return; + } + popup.msg(res.msg, { time: 0, icon: 16, shade: [0.3, '#333'] }); + window.location.href = '/'; + }); + }); + }; + // 切换登录方式 + entrance.onclick = function () { + var tips = this.nextSibling.nextSibling; + var icon = tips.querySelector('.icon'); + var text = tips.querySelector('.text'); + var scanCode = document.querySelector('.scan_code'); + var account = document.querySelector('.account'); + if (this.classList.contains('pc')) { + this.classList.remove('pc'); + icon.classList.add('scan'); + icon.classList.remove('safe'); + text.innerText = '{{data.lan.SCAN_MORE_SAFETY}}'; + account.classList.remove('hide'); + scanCode.classList.add('hide'); + clear_control_time(); + } else { + this.classList.add('pc'); + icon.classList.add('safe'); + icon.classList.remove('scan'); + text.innerText = 'Switch account login'; + account.classList.add('hide'); + scanCode.classList.remove('hide'); + generate_qrcode(); + set_control_time(); + } + }; + window.onresize = function () { + popup.set_msg_center(); + popup.set_open_center(); + }; + if ("{{data['app_login']}}" == 'True') { + entrance.click(); + } + } + /** + * 请求 + */ + function Request() { + this.xhr = new XMLHttpRequest(); + } + Request.prototype = { + post: function (url, data, callback) { + var option = { + method: 'POST', + url: url, + callback: callback, + }; + var form = new FormData(); + for (var key in data) { + if (data[key] !== '' && data[key] !== undefined && data[key] !== null) { + form.append(key, data[key]); + } + } + option.data = form; + this.http(option); + }, + http: function (option) { + var xhr = this.xhr; + var toUrl = window.location.protocol + '//' + window.location.host + option.url; + var method = option.method || 'GET'; + // 打开连接 + xhr.open(method, toUrl, true); + // 发送请求 + if (method == 'GET') { + xhr.send(); + } else if (method == 'POST') { + xhr.send(option.data); + } + // 请求后的回调接口 + xhr.onreadystatechange = function () { + if (xhr.readyState == 4 && xhr.status == 200) { + var res = xhr.responseText; + res = res ? JSON.parse(res) : ''; + if (option.callback) option.callback(res); + } + }; + }, + }; + /** + * 弹框 + */ + function Popup() { + this.zIndex = 200000; + } + Popup.prototype = { + /** + * 弹框 + */ + open: function (option) { + var _this = this; + option = option || {}; + this.set_open_option(option); + this.add_overlay(option); + var container = document.createElement('div'); + container.classList.add('layui-layer'); + var content = document.createElement('div'); + content.classList.add('layui-layer-content'); + var setwin = document.createElement('div'); + setwin.classList.add('layui-layer-setwin'); + var close = document.createElement('a'); + close.classList.add('layui-layer-close'); + if (option.area[0]) container.style.width = option.area[0]; + if (option.area[1]) container.style.height = option.area[1]; + content.innerHTML = option.content; + close.onclick = function () { + _this.remove_open(); + }; + container.style['z-index'] = this.zIndex; + this.zIndex++; + setwin.appendChild(close); + container.appendChild(content); + container.appendChild(setwin); + document.documentElement.appendChild(container); + this.set_open_center('layui-layer'); + if (option.callback) option.callback(); + }, + /** + * 提示框 + */ + msg: function (msg, option, callback) { + var _this = this; + option = option || {}; + this.remove_msg(); + this.add_overlay(option); + this.set_msg_option(option); + var container = document.createElement('div'); + container.className = 'layui-layer-msg'; + container.style['z-index'] = 200000; + var box = document.createElement('div'); + box.className = 'layui-layer-content'; + var icon = document.createElement('i'); + icon.className = 'layui-layer-ico layui-layer-ico' + option.icon; + box.appendChild(icon); + box.innerHTML += msg; + container.style['z-index'] = this.zIndex; + this.zIndex++; + container.appendChild(box); + document.documentElement.appendChild(container); + this.set_msg_center(); + if (option.time !== 0) { + setTimeout(function () { + _this.remove_msg(); + if (callback) callback(); + }, option.time); + } + }, + /** + * 添加遮罩层 + */ + add_overlay: function (option) { + if (option.hasOwnProperty('shade')) { + var shade = option.shade; + var overlay = document.createElement('div'); + overlay.className = 'layui-layer-shade'; + overlay.style.opacity = shade[0]; + overlay.style['background-color'] = shade[1]; + overlay.style['z-index'] = this.zIndex; + this.zIndex++; + document.documentElement.appendChild(overlay); + } + }, + /** + * 弹框配置项 + */ + set_open_option: function (option) { + var defaultOption = { + area: ['390px', '280px'], + shade: [0.3, '#000'], + content: '', + }; + this.set_option(option, defaultOption); + }, + /** + * msg配置项 + */ + set_msg_option: function (option) { + var defaultOption = { + icon: 2, + time: 2000, + }; + this.set_option(option, defaultOption); + }, + /** + * 设置默认配置项 + */ + set_option: function (option, defaultOption) { + for (var key in defaultOption) { + if (option[key] === undefined) { + option[key] = defaultOption[key]; + } + } + }, + /** + * 提示框设置居中 + */ + set_msg_center: function () { + this.set_center('layui-layer-msg'); + }, + /** + * 弹框设置居中 + */ + set_open_center: function () { + this.set_center('layui-layer'); + }, + /** + * 设置居中 + */ + set_open_center: function () { + this.set_center('layui-layer'); + }, + /** + * 居中 + */ + set_center: function (_class) { + var dom = document.querySelector('.' + _class); + if (!dom) return; + var bodyWidth = document.documentElement.clientWidth; + var bodyHeight = document.documentElement.clientHeight; + var domWidth = dom.clientWidth; + var domHeight = dom.clientHeight; + dom.style.left = (bodyWidth - domWidth) / 2 + 'px'; + dom.style.top = (bodyHeight - domHeight) / 2 + 'px'; + }, + /** + * 删除提示框 + */ + remove_msg: function () { + this.remove('layui-layer-msg'); + }, + /** + * 删除弹框 + */ + remove_open: function () { + this.remove('layui-layer'); + }, + /** + * 删除 + */ + remove: function (_class) { + var dom = document.querySelector('.' + _class); + if (!dom) return; + var overlay = dom.previousElementSibling; + if (overlay.classList.contains('layui-layer-shade')) { + document.documentElement.removeChild(overlay); + this.zIndex--; + } + document.documentElement.removeChild(dom); + this.zIndex--; + }, + }; + + + diff --git a/README.md b/README.md index 215dbea3..06368e52 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,28 @@ -
                                    - aaPanel -
                                    -
                                    - -
                                    -aaPanel -
                                    -
                                    -
                                    - -[![BTWAF](https://img.shields.io/badge/aaPanel-aaPanel-blue)](https://github.com/aaPanel/aaPanel) -[![social](https://img.shields.io/github/stars/aaPanel/aaPanel?style=social)](https://github.com/aaPanel/aaPanel) - -
                                    -

                                    - Official | - documentation | - Demo | -

                                    - -## About aaPanel - -**aaPanel is a simple but powerful hosting control panel**, it can manage the web server through web-based GUI(Graphical User Interface). - -* **one-click function:** such as one-click install LNMP/LAMP developing environment and software. -* **save the time:** Our main goal is helping users to save the time of deploying, thus users just focus on their own project that is fine. - -## Demo - -Demo:https://demo.aapanel.com/fdgi87jbn/
                                    - -![image](https://github.com/aaPanel/aaPanel/assets/31841517/c40d68f5-1cbb-4117-ab47-b52b14228cce) - -## What can I do - -aaPanel is a server management software that supports the Linux system. - -It can easily manage the server through the Web terminal, improving the operation and maintenance efficiency. - -## Installation - -> Make sure it is a clean operating system, and have not installed Apache /Nginx/php/MySQL from other environments -> aaPanel is developed based on Centos7+, it is strongly recommended to use centos7 + linux distribution*\** - - Note, please execute the installation command with root authority - -* Memory: 512M or more, 768M or more is recommended (Pure panel for about 60M of system memory) - -* Hard disk: More than 100M available hard disk space (Pure panel for about 20M disk space) - -* System: CentOS 7.1+ (Ubuntu16.04+., Debian9.0+), to ensure that it is a clean operating system, there is no other environment with Apache/Nginx/php/MySQL installed (the existing environment can not be installed) - -**aaPanel Installation Command** - -`URL=https://www.aapanel.com/script/install_6.0_en.sh && if [ -f /usr/bin/curl ];then curl -ksSO "$URL" ;else wget --no-check-certificate -O install_6.0_en.sh "$URL";fi;bash install_6.0_en.sh 66959f96` - -**aaPanel Docker Deployment** - -> The docker image is officially released by aaPanel +#aaPanel Docker Deployment +The docker image is officially released by aaPanel Maintained by: [aaPanel](https://www.aapanel.com) -How to use +##How to use `$docker run -d -p 8886:8888 -p 22:21 -p 443:443 -p 80:80 -p 889:888 -v ~/website_data:/www/wwwroot -v ~/mysql_data:/www/server/data -v ~/vhost:/www/server/panel/vhost aapanel/aapanel:lib` Now you can access aaPanel at http://youripaddress:8886/ from your host system. -* Default username:`aapanel` -* Default password:`aapanel123` +Default username:`aapanel` -Port usage analysis -* Control Panel : 8888 -* Phpmyadmin : 888 +Default password:`aapanel123` -Dir usage analysis -* Website data : /www/wwwroot -* Mysql data : /www/server/data -* Vhost file : /www/server/panel/vhost +####Port usage analysis +Control Panel : 8888 +Phpmyadmin : 888 + +####Dir usage analysis +Website data : /www/wwwroot +Mysql data : /www/server/data +Vhost file : /www/server/panel/vhost **Note: after the deployment is complete, please immediately modify the user name and password in the panel settings and add the installation entry** - diff --git a/check-pip-packs.txt b/check-pip-packs.txt new file mode 100644 index 00000000..00601b9c --- /dev/null +++ b/check-pip-packs.txt @@ -0,0 +1,112 @@ +aliyun-python-sdk-core +aliyun-python-sdk-core-v3 +aliyun-python-sdk-kms +async-timeout +bcrypt +beautifulsoup4 +cachelib +cachetools +certifi +cffi +chardet +charset-normalizer +click +configobj +configparser +cos-python-sdk-v5 +crcmod +cryptography +Cython +decorator +dicttoxml +dnspython +docker +enum34 +Flask +Flask-Session +flask-sock +Flask-SQLAlchemy +future +geoip2 +gevent +gevent-websocket +google-api-core +google-api-python-client +google-auth +google-auth-httplib2 +google-auth-oauthlib +google-cloud-core +google-cloud-storage +google-crc32c +google-resumable-media +googleapis-common-protos +greenlet +h11 +httplib2 +idna +importlib-metadata +iniparse +ipaddress +IPy +itsdangerous +Jinja2 +jmespath +kitchen +MarkupSafe +mongo +natsort +oauthlib +oss2 +packaging +paramiko +peewee +pillow +pip +protobuf +psutil +psycopg2-binary +pyasn1 +pyasn1-modules +pyasyncore +pycparser +pycryptodome +pycurl +Pygments +pyinotify +pymongo +PyMySQL +PyNaCl +pyOpenSSL +pyparsing +pypdf +PySocks +pytz +pyudev +pyxattr +PyYAML +qiniu +qrcode +redis +requests +requests-file +requests-oauthlib +rsa +setuptools +simple-websocket +six +soupsieve +SQLAlchemy +supervisor +typing_extensions +upyun +uritemplate +urlgrabber +urllib3 +websocket-client +Werkzeug +wheel +wsproto +xmltodict +zipp +zope.event +zope.interface \ No newline at end of file diff --git a/class/PluginLoader.aarch64.Python3.12.so b/class/PluginLoader.aarch64.Python3.12.so new file mode 100644 index 00000000..b1150e3e Binary files /dev/null and b/class/PluginLoader.aarch64.Python3.12.so differ diff --git a/class/PluginLoader.x86_64.Python3.12.so b/class/PluginLoader.x86_64.Python3.12.so new file mode 100644 index 00000000..57535509 Binary files /dev/null and b/class/PluginLoader.x86_64.Python3.12.so differ diff --git a/class/acme_v2.py b/class/acme_v2.py index f976632b..a2de6b08 100755 --- a/class/acme_v2.py +++ b/class/acme_v2.py @@ -1,11 +1,11 @@ #!/usr/bin/python -# coding: utf-8 +#coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aapanel # ------------------------------------------------------------------- # Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang a # ------------------------------------------------------------------- # ------------------------------------------------------------------- @@ -18,22 +18,20 @@ import binascii import hashlib import base64 import json +import shutil import time import os import sys - os.chdir('/www/server/panel') if not 'class/' in sys.path: - sys.path.insert(0, 'class/') + sys.path.insert(0,'class/') import http_requests as requests - -requests.DEFAULT_TYPE = 'curl' import public try: import OpenSSL except: - public.ExecShell("btpip install -I pyOpenSSL") + public.ExecShell("btpip install pyopenssl") import OpenSSL try: import dns.resolver @@ -41,6 +39,14 @@ except: public.ExecShell("btpip install dnspython") import dns.resolver +#### +# auth to 格式说明 +# 旧版 auth to 格式: +# 文件验证:/www/server/xxxx/xxxx +# DNS->手动:dns DNS->api: CloudFlareDns|XXXXXXXX|XXXXXXXXX +# +# 新版 auth to 格式: +# DNS->手动:dns DNS->api: dns#@api class acme_v2: _url = None @@ -50,11 +56,11 @@ class acme_v2: _bits = 2048 _acme_timeout = 30 _dns_class = None - _user_agent = "BTPanel" + _user_agent = "BaoTa/1.0 (+https://www.aapanel.com)" _replay_nonce = None _verify = False _digest = "sha256" - _max_check_num = 5 + _max_check_num = 15 _wait_time = 5 _mod_index = {True: "Staging", False: "Production"} _debug = False @@ -62,15 +68,121 @@ class acme_v2: _dnsapi_file = 'config/dns_api.json' _save_path = 'vhost/letsencrypt' _conf_file = 'config/letsencrypt.json' - _stop_rp_file = '{}/data/stop_rp_when_renew_ssl.pl'.format(public.get_panel_path()) - _by_panel = None + _conf_file_v2 = 'config/letsencrypt_v2.json' + _request_type = 'curl' def __init__(self): + if not os.path.exists(self._conf_file_v2) and os.path.exists(self._conf_file): + shutil.copyfile(self._conf_file, self._conf_file_v2) if self._debug: self._url = 'https://acme-staging-v02.api.letsencrypt.org/directory' else: self._url = 'https://acme-v02.api.letsencrypt.org/directory' self._config = self.read_config() + self._nginx_cache_file_auth = {} + self._can_use_lua = None + self._well_known_check_cache = {} + + def can_use_lua_module(self): + if self._can_use_lua is None: + # 查询lua_module 不为空 + self._can_use_lua = public.ExecShell("nginx -V 2>&1 |grep lua_nginx_module")[0].strip() != '' + return self._can_use_lua + + # 返回是否能通过lua 做了文件验证处理, 如果返回True,则表示可以处理了验证文件, 不再走之前的 if 验证方式 + def can_use_lua_for_site(self, site_name: str, site_type: str): + if self._can_use_lua is None: + # 查询lua_module 不为空 + self._can_use_lua = public.ExecShell("nginx -V 2>&1 |grep lua_nginx_module")[0].strip() != '' + + if not self._can_use_lua: + return False + + if site_type.lower() in ("php", "proxy"): + prefix = "" + else: + prefix = site_type.lower() + "_" + + ng_file = "{}/nginx/{}{}.conf".format(public.get_vhost_path(), prefix, site_name) + ng_data = public.readFile(ng_file) + if not ng_data: + return False + + rep_well_known = re.compile( + r"(#.*\n)?\s*include\s+/www/server/panel/vhost/nginx/well-known/.*\.conf;.*\n(#.*\n)?" + r"(.*\n)*?\s*#error_page 404/404\.html;" + ) # 匹配一下引入的外部配置文件,同时保证这个配置在SSL配置之前, 这样避免路由匹配问题 + + if rep_well_known.search(ng_data): + lua_file = "{}/nginx/well-known/{}.conf".format(public.get_vhost_path(), site_name) + lua_data = public.readFile(lua_file) + if not lua_data or "set_by_lua_block $well_known" not in lua_data: + return False + else: + return True + else: + return False + + # 返回是否能通过if 判断方式做了文件验证处理, 如果返回True,则表示可以 + @staticmethod + def can_use_if_for_file_check(site_name: str, site_type: str): + if site_type.lower() in ("php", "proxy"): + prefix = "" + else: + prefix = site_type.lower() + "_" + + # if 方式的文件验证必须是可重载的情况 + if public.checkWebConfig() is not True: + return False + + ng_file = "{}/nginx/{}{}.conf".format(public.get_vhost_path(), prefix, site_name) + ng_data = public.readFile(ng_file) + if not ng_data: + return False + + rep_well_known = re.compile( + r"(#.*\n)?\s*include\s+/www/server/panel/vhost/nginx/well-known/.*\.conf;.*\n(#.*\n)?" + r"(.*\n)*?\s*#error_page 404/404\.html;" + ) # 匹配一下引入的外部配置文件,同时保证这个配置在SSL配置之前, 这样避免路由匹配问题 + + if rep_well_known.search(ng_data): + return True + else: + return False + + # 返回配置文件是否支持使用普通的文件验证 + @staticmethod + def can_use_base_file_check(site_name: str, site_type: str): + if site_type.lower() in ("php", "proxy", "wp", "wp2"): + prefix = "" + else: + prefix = site_type.lower() + "_" + + webserver = public.get_webserver() + if webserver == "nginx": + ng_file = "{}/nginx/{}{}.conf".format(public.get_vhost_path(), prefix, site_name) + ng_data = public.readFile(ng_file) + if not ng_data: + return False + rep_well_known = re.compile(r"location\s+([=~^]*\s*)?/?\\?\.well-known/?\s*{") + if rep_well_known.search(ng_data): + return True + else: + return False + elif webserver == "apache" and prefix: # PHP 不用检查 + ap_file = "{}/apache/{}{}.conf".format(public.get_vhost_path(), prefix, site_name) + ap_data = public.readFile(ap_file) + if not ap_data: + return False + rep_well_known_list = [ + re.compile(r"\s+Alias\s+/\.well-known/\s+\S+\s+"), + re.compile(r"\s*ProxyPass\s+/\.well-known/\s+!", re.M), + ] + for rep_well_known in rep_well_known_list: + if rep_well_known.search(ap_data): + return True + return False + return True # 取接口目录 def get_apis(self): @@ -86,17 +198,13 @@ class acme_v2: return self._apis # 尝试从云端获取 - res = requests.get(self._url, verify=False) + res = requests.get(self._url,s_type=self._request_type) if not res.status_code in [200, 201]: result = res.json() if "type" in result: if result['type'] == 'urn:acme:error:serverInternal': raise Exception(public.get_msg_gettext( 'Service shutdown or internal error due to maintenance, check [ https://letsencrypt.status.io ] see for more details.')) - if not os.path.exists('/www/server/panel/data/http_type.pl'): - public.writeFile('/www/server/panel/data/http_type.pl', 'python') - self.get_apis() - return self._apis raise Exception(res.content) s_body = res.json() self._apis = {} @@ -110,7 +218,7 @@ class acme_v2: self._config['apis'][api_index] = {} self._config['apis'][api_index]['directory'] = self._apis self._config['apis'][api_index]['expires'] = time.time() + \ - 86400 # 24小时后过期 + 86400 # 24小时后过期 self.save_config() return self._apis @@ -254,11 +362,19 @@ class acme_v2: return domains domain_list = [] for domain in domains: - rootDoamin = self.extract_zone(domain)[0] - if not rootDoamin in domain_list: - domain_list.append(rootDoamin) - if not "*." + rootDoamin in domain_list: - domain_list.append("*." + rootDoamin) + root, zone = self.extract_zone(domain) + tmp_list = zone.rsplit(".", 1) + if len(tmp_list) == 1: + if root not in domain_list: + domain_list.append(root) + if not "*." + root in domain_list: + domain_list.append("*." + root) + else: + new_root = "{}.{}".format(tmp_list[1], root) + if new_root not in domain_list: + domain_list.append(new_root) + if not "*." + new_root in domain_list: + domain_list.append("*." + new_root) return domain_list # 构造域名列表 @@ -306,7 +422,7 @@ class acme_v2: # 请求创建订单 res = self.acme_request(self._apis['newOrder'], payload) - if not res.status_code in [201]: # 如果创建失败 + if not res.status_code in [201,200]: # 如果创建失败 e_body = res.json() if 'type' in e_body: # 如果随机数失效 @@ -321,16 +437,16 @@ class acme_v2: self.get_kid() self.get_nonce(force=True) res = self.acme_request(self._apis['newOrder'], payload) - if not res.status_code in [201]: + if not res.status_code in [201,200]: a_auth = res.json() ret_title = self.get_error(str(a_auth)) raise StopIteration( - "{} >>>> {}".format( - ret_title, - json.dumps(a_auth) + "{0} >>>> {1}".format( + ret_title, + json.dumps(a_auth) + ) ) - ) # 返回验证地址和验证 s_json = res.json() @@ -340,7 +456,7 @@ class acme_v2: index = self.save_order(s_json, index) return index - def get_site_run_path_byid(self, site_id): + def get_site_run_path_byid(self,site_id): ''' @name 通过site_id获取网站运行目录 @author hwliang @@ -364,7 +480,7 @@ class acme_v2: else: return False - def get_site_run_path(self, domains): + def get_site_run_path(self,domains): ''' @name 通过域名列表获取网站运行目录 @author hwliang @@ -373,7 +489,7 @@ class acme_v2: ''' site_id = 0 for domain in domains: - site_id = public.M('domain').where("name=?", domain).getField('pid') + site_id = public.M('domain').where("name=?",domain).getField('pid') if site_id: break if not site_id: return None @@ -393,7 +509,7 @@ class acme_v2: site_run_path = self.get_site_run_path(self._config['orders'][index]['domains']) if site_run_path: self._config['orders'][index]['auth_to'] = site_run_path - # 清理旧验证 + #清理旧验证 self.claer_auth_file(index) auths = [] @@ -421,8 +537,9 @@ class acme_v2: identifier_auth['expires'] = s_body['expires'] identifier_auth['auth_to'] = self._config['orders'][index]['auth_to'] identifier_auth['type'] = self._config['orders'][index]['auth_type'] + # 设置验证信息 - self.set_auth_info(identifier_auth) + self.set_auth_info(identifier_auth, index=index) auths.append(identifier_auth) self._config['orders'][index]['auths'] = auths self.save_config() @@ -435,9 +552,9 @@ class acme_v2: self._replay_nonce = replay_nonce # 设置验证信息 - def set_auth_info(self, identifier_auth): + def set_auth_info(self, identifier_auth, index=None): - # 从云端验证 + #从云端验证 if not self.cloud_check_domain(identifier_auth['domain']): self.err = "Cloud verification failed!" @@ -448,39 +565,87 @@ class acme_v2: # 是否文件验证 if identifier_auth['type'] in ['http', 'tls']: self.write_auth_file( - identifier_auth['auth_to'], identifier_auth['token'], identifier_auth['acme_keyauthorization']) + identifier_auth['auth_to'], identifier_auth['token'], identifier_auth['acme_keyauthorization'], index) else: # dnsapi验证 self.create_dns_record( identifier_auth['auth_to'], identifier_auth['domain'], identifier_auth['auth_value']) - # 从云端验证域名是否可访问 - def cloud_check_domain(self, domain): + #从云端验证域名是否可访问 + def cloud_check_domain(self,domain): try: - result = requests.post('https://www.aapanel.com/api/panel/checkDomain', {"domain": domain, "ssl": 1}).json() + result = requests.post('https://www.aapanel.com/api/panel/checkDomain',{"domain":domain,"ssl":1},s_type=self._request_type).json() return result['status'] - except: - return False + except: return False - # 清理验证文件 - def claer_auth_file(self, index): - if not self._config['orders'][index]['auth_type'] in ['http', 'tls']: + + #清理验证文件 + def claer_auth_file(self,index): + if not self._config['orders'][index]['auth_type'] in ['http','tls']: return True acme_path = '{}/.well-known/acme-challenge'.format(self._config['orders'][index]['auth_to']) + acme_path = acme_path.replace("//",'/') write_log(public.get_msg_gettext('|-Verify the dir:{}', (acme_path,))) if os.path.exists(acme_path): public.ExecShell("rm -f {}/*".format(acme_path)) + acme_path = '/www/server/stop/.well-known/acme-challenge' if os.path.exists(acme_path): public.ExecShell("rm -f {}/*".format(acme_path)) + def change_well_known_mod(self, path_dir: str): + path_dir = path_dir.rstrip("/") + if not os.path.isdir(path_dir): + return False + if path_dir in self._well_known_check_cache: + return True + else: + self._well_known_check_cache[path_dir] = True + import stat + + try: + import pwd + uid_data = pwd.getpwnam("www") + uid = uid_data.pw_uid + gid = uid_data.pw_gid + except: + return + + # 逐级给最低访问权限 + while path_dir != "/": + path_dir_stat = os.stat(path_dir) + if path_dir_stat.st_uid == 0 and uid != 0: + old_mod = stat.S_IMODE(path_dir_stat.st_mode) + if not old_mod & (1 << 3): + os.chmod(path_dir, old_mod + (1 << 3)) # chmod g+x + if path_dir_stat.st_uid == uid: + old_mod = stat.S_IMODE(path_dir_stat.st_mode) + if not old_mod & (1 << 6): + os.chmod(path_dir, old_mod + (1 << 6)) # chmod u+x + elif path_dir_stat.st_gid == gid: + old_mod = stat.S_IMODE(path_dir_stat.st_mode) + if not old_mod & (1 << 3): + os.chmod(path_dir, old_mod + (1 << 6)) # chmod g+x + elif 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) # chmod o+x + path_dir = os.path.dirname(path_dir) + # 写验证文件 - def write_auth_file(self, auth_to, token, acme_keyauthorization): + def write_auth_file(self, auth_to, token, acme_keyauthorization, index): + if public.get_webserver() == "nginx": + # 如果是nginx尝试使用配置文件进行验证 + self.write_ngin_authx_file(auth_to, token, acme_keyauthorization, index) + + # 尝试写文件进行验证 try: acme_path = '{}/.well-known/acme-challenge'.format(auth_to) + acme_path = acme_path.replace("//",'/') if not os.path.exists(acme_path): os.makedirs(acme_path) public.set_own(acme_path, 'www') + self.change_well_known_mod(acme_path) wellknown_path = '{}/{}'.format(acme_path, token) public.writeFile(wellknown_path, acme_keyauthorization) public.set_own(wellknown_path, 'www') @@ -489,8 +654,9 @@ class acme_v2: if not os.path.exists(acme_path): os.makedirs(acme_path) public.set_own(acme_path, 'www') - wellknown_path = '{}/{}'.format(acme_path, token) - public.writeFile(wellknown_path, acme_keyauthorization) + self.change_well_known_mod(acme_path) + wellknown_path = '{}/{}'.format(acme_path,token) + public.writeFile(wellknown_path,acme_keyauthorization) public.set_own(wellknown_path, 'www') return True except: @@ -498,19 +664,150 @@ class acme_v2: print(err) raise Exception(public.get_msg_gettext('Writing verification file failed: {}', (err,))) + def write_ngin_authx_file(self, auth_to, token, acme_keyauthorization, index): + site_name, project_type = self.get_site_name_by_domains(self._config["orders"][index]["domains"]) + if site_name is None: + return + + if self.can_use_lua_for_site(site_name, project_type): + return + + if project_type.lower() in ("php", "proxy"): + nginx_conf_path = "{}/vhost/nginx/{}.conf".format(public.get_panel_path(), site_name) + else: + nginx_conf_path = "{}/vhost/nginx/{}_{}.conf".format(public.get_panel_path(), project_type.lower(), site_name) + nginx_conf = public.readFile(nginx_conf_path) + if nginx_conf is False: + return + + file_check_config_path = "/www/server/panel/vhost/nginx/well-known/{}.conf".format(site_name) + if not os.path.exists("/www/server/panel/vhost/nginx/well-known"): + os.makedirs("/www/server/panel/vhost/nginx/well-known", 0o755) + + # 如果主配置中,没有引用则尝试添加,添加失败就跳出 + if not re.search(r"\s*include\s+/www/server/panel/vhost/nginx/well-known/.*\.conf;", nginx_conf, re.M): + ssl_line = re.search(r"(#.*\n\s*)?#error_page 404/404\.html;", nginx_conf) + if ssl_line is None: + return + default_cert_apply_check = ( + "#CERT-APPLY-CHECK--START\n" + " # Configuration related to file verification for SSL certificate application - Do not delete\n" + " include /www/server/panel/vhost/nginx/well-known/{}.conf;\n" + " #CERT-APPLY-CHECK--END\n " + ).format(site_name) + if not os.path.exists(file_check_config_path): + public.writeFile(file_check_config_path, "") + + new_conf = nginx_conf.replace(ssl_line.group(), default_cert_apply_check + ssl_line.group(), 1) + public.writeFile(nginx_conf_path, new_conf) + isError = public.checkWebConfig() + if isError is not True: + public.writeFile(nginx_conf_path, nginx_conf) + return + + # 如果主配置有引用, 不再检测位置关系,因为不能保证用户的自定义配置的优先级, 直接进行文件验证的 lua 方式和 if 方式的尝试 + if self.can_use_lua_module(): + self.write_lua_file_for_site(file_check_config_path) + return + + # 开始尝试if 验证方式 + if auth_to not in self._nginx_cache_file_auth: + self._nginx_cache_file_auth[auth_to] = [] + + self._nginx_cache_file_auth[auth_to].append((token, acme_keyauthorization)) + + tmp_data = [] + for token, acme_key in self._nginx_cache_file_auth[auth_to]: + tmp_data.append(( + 'if ($request_uri ~ "^/\\.well-known/acme-challenge/{}.*"){{\n' + ' return 200 "{}";\n' + '}}\n' + ).format(token, acme_key)) + + public.writeFile(file_check_config_path, "\n".join(tmp_data)) + isError = public.checkWebConfig() + if isError is True: + public.serviceReload() + else: + public.writeFile(file_check_config_path, "") + + @staticmethod + def write_lua_file_for_site(file_check_config_path: str): + old_data = public.readFile(file_check_config_path) + if isinstance(old_data, str) and "set_by_lua_block $well_known" in old_data: + return + + lua_file_data = r""" +set $well_known ''; +if ( $uri ~ "^/.well-known/" ) { + set_by_lua_block $well_known { + --get path + local m,err = ngx.re.match(ngx.var.uri,"/.well-known/(.*)","isjo") + -- If the path matches + if m then + -- Splicing file path + local filename = ngx.var.document_root .. m[0] + -- Determine if the file path is legal + if not ngx.re.find(m[1],"\\\\./","isjo") then + -- Determine if the file exists + local is_exists = io.open(filename, "r") + if not is_exists then + -- Java project? + filename = "/www/wwwroot/java_node_ssl" .. m[0] + end + -- release + if is_exists then is_exists:close() end + -- read file + local fp = io.open(filename,'r') + if fp then + local file_body = fp:read("*a") + fp:close() + if file_body then + ngx.header['content-type'] = 'text/plain' + return file_body + end + end + end + end + return "" + } +} + +if ( $well_known != "" ) { + return 200 $well_known; +} +""" + public.writeFile(file_check_config_path, lua_file_data) + isError = public.checkWebConfig() + if isError is True: + public.serviceReload() + else: + public.writeFile(file_check_config_path, old_data) + # 解析域名 def create_dns_record(self, auth_to, domain, dns_value): # 如果为手动解析 if auth_to == 'dns' or auth_to.find('|') == -1: return None - if not self._dns_class: - import panelDnsapi - dns_name, key, secret = self.get_dnsapi(auth_to) - self._dns_class = getattr(panelDnsapi, dns_name)(key, secret) + + import panelDnsapi + dns_name, key, secret = self.get_dnsapi(auth_to) + self._dns_class = getattr(panelDnsapi, dns_name)(key, secret) self._dns_class.create_dns_record(public.de_punycode(domain), dns_value) self._dns_domains.append({"domain": domain, "dns_value": dns_value}) + return - # 解析DNSAPI信息 + # # 如果为手动解析 + # if auth_to == 'dns' : + # return None + + # from panelDnsapi import DnsMager + + # self._dns_class = DnsMager().get_dns_obj_by_domain(domain) + # self._dns_class.create_dns_record(public.de_punycode(domain), dns_value) + # self._dns_domains.append({"domain": domain, "dns_value": dns_value}) + + # 解析DNSAPI信息 # 不再使用的 def get_dnsapi(self, auth_to): tmp = auth_to.split('|') dns_name = tmp[0] @@ -543,12 +840,14 @@ class acme_v2: public.de_punycode(dns_info['domain']), dns_info['dns_value']) except: pass - # 验证域名 def auth_domain(self, index): - if not index in self._config['orders']: + if index not in self._config['orders']: raise Exception(public.get_msg_gettext('The specified order does not exist!')) + if "auths" not in self._config['orders'][index]: + raise Exception(public.get_msg_gettext('Order verification information is missing, please try reapplying!')) + # 开始验证 for auth in self._config['orders'][index]['auths']: res = self.check_auth_status(auth['url']) # 检查是否需要验证 @@ -565,7 +864,7 @@ class acme_v2: # 检查验证结果 for i in range(len(self._config['orders'][index]['auths'])): self.check_auth_status(self._config['orders'][index]['auths'][i]['url'], [ - 'valid', 'invalid']) + 'valid', 'invalid']) self._config['orders'][index]['status'] = 'valid' # 检查验证状态 @@ -596,7 +895,7 @@ class acme_v2: except: ret_title = str(a_auth) raise StopIteration( - "{} >>>> {}".format( + "{0} >>>> {1}".format( ret_title, json.dumps(a_auth) ) @@ -627,6 +926,9 @@ class acme_v2: elif error.find("The domain name belongs") >= 0: return public.get_msg_gettext( 'The domain name does not belong to this DNS service provider, please make sure the domain name is filled in correctly.') + + elif error.find("domains in the last 168 hours") != -1 and error.find("Error creating new order") != -1: + return public.get_msg_gettext("Issuance failed, the root domain name of domain name %s exceeds the maximum weekly issuance limit!" % re.findall(r"hours:\s+(.+?),", error)) elif error.find('login token ID is invalid') >= 0: return public.get_msg_gettext('DNS server connection failed, please check if the key is correct.') elif error.find('Error getting validation data') != -1: @@ -702,7 +1004,7 @@ class acme_v2: # 发送验证请求 def respond_to_challenge(self, auth): - payload = {"keyAuthorization": "{}".format( + payload = {"keyAuthorization": "{0}".format( auth['acme_keyauthorization'])} respond_to_challenge_response = self.acme_request( auth['dns_challenge_url'], payload) @@ -754,14 +1056,14 @@ class acme_v2: cert['cert_timeout'] = self.get_cert_timeout(cert['cert']) cert['private_key'] = self._config['orders'][index]['private_key'] cert['domains'] = self._config['orders'][index]['domains'] - del (self._config['orders'][index]['private_key']) - del (self._config['orders'][index]['auths']) - del (self._config['orders'][index]['expires']) - del (self._config['orders'][index]['authorizations']) - del (self._config['orders'][index]['finalize']) - del (self._config['orders'][index]['identifiers']) + del(self._config['orders'][index]['private_key']) + del(self._config['orders'][index]['auths']) + del(self._config['orders'][index]['expires']) + del(self._config['orders'][index]['authorizations']) + del(self._config['orders'][index]['finalize']) + del(self._config['orders'][index]['identifiers']) if 'cert' in self._config['orders'][index]: - del (self._config['orders'][index]['cert']) + del(self._config['orders'][index]['cert']) self._config['orders'][index]['status'] = 'valid' self._config['orders'][index]['cert_timeout'] = cert['cert_timeout'] domain_name = self._config['orders'][index]['domains'][0] @@ -775,6 +1077,9 @@ class acme_v2: # 保存证书到文件 def save_cert(self, cert, index): try: + from ssl_manage import SSLManger + SSLManger().save_by_data(cert['cert'] + cert['root'], cert['private_key']) + domain_name = self._config['orders'][index]['domains'][0] path = self._config['orders'][index]['save_path'] if not os.path.exists(path): @@ -789,8 +1094,13 @@ class acme_v2: public.writeFile(path + "/root_cert.csr", cert['root']) # 转为IIS证书 - pfx_buffer = self.dump_pkcs12( - cert['private_key'], cert['cert'] + cert['root'], cert['root'], domain_name) + try: + pfx_buffer = self.dump_pkcs12( + cert['private_key'], cert['cert'] + cert['root'], cert['root'], domain_name) + except: + import ssl_info + pfx_buffer = ssl_info.ssl_info().dump_pkcs12_new( + cert['private_key'], cert['cert'] + cert['root'], cert['root'], domain_name) public.writeFile(path + "/fullchain.pfx", pfx_buffer, 'wb+') ps = '''Document description: @@ -810,21 +1120,22 @@ fullchain.pem Paste into certificate input box write_log(public.get_error_info()) # 通过域名获取网站名称 - def get_site_name_by_domains(self, domains): + def get_site_name_by_domains(self,domains): sql = public.M('domain') site_sql = public.M('sites') - siteName = None + siteName, project_type = None, None for domain in domains: - pid = sql.where('name=?', domain).getField('pid') + pid = sql.where('name=?',domain).getField('pid') if pid: - siteName = site_sql.where('id=?', pid).getField('name') + site_data = site_sql.where('id=?', pid).field('name,project_type').find() + siteName, project_type = site_data["name"], site_data["project_type"] break - return siteName + return siteName, project_type # 替换服务器上的同域名同品牌证书 def sub_all_cert(self, key_file, pem_file): cert_init = self.get_cert_init(pem_file) # 获取新证书的基本信息 - paths = ['/www/server/panel/vhost/cert', '/www/server/panel/vhost/ssl', '/www/server/panel'] + paths = ['/www/server/panel/vhost/cert', '/www/server/panel/vhost/ssl','/www/server/panel'] is_panel = False for path in paths: if not os.path.exists(path): @@ -836,25 +1147,18 @@ fullchain.pem Paste into certificate input box to_info = to_path + '/info.json' # 判断目标证书是否存在 if not os.path.exists(to_pem_file): - if p_name not in ['ssl']: continue + if not p_name in ['ssl']: continue to_pem_file = to_path + '/certificate.pem' to_key_file = to_path + '/privateKey.pem' if not os.path.exists(to_pem_file): continue - # _by_panel None 时通过面板请求时不即使续签面板证书也不重启面板以免导致后续请求出错 - if not os.path.exists('{}/data/ssl.pl'.format(public.get_panel_path())): - continue - if not self._by_panel: - is_panel = True # 获取目标证书的基本信息 to_cert_init = self.get_cert_init(to_pem_file) # 判断证书品牌是否一致 try: - if to_cert_init['issuer'] != cert_init['issuer'] and to_cert_init['issuer'].find( - "Let's Encrypt") == -1 and to_cert_init['issuer'] != 'R3': + if to_cert_init['issuer'] != cert_init['issuer'] and to_cert_init['issuer'].find("Let's Encrypt") == -1 and to_cert_init['issuer'] != 'R3': continue - except: - continue + except: continue # 判断目标证书的到期时间是否较早 if to_cert_init['notAfter'] > cert_init['notAfter']: continue @@ -877,24 +1181,27 @@ fullchain.pem Paste into certificate input box write_log(public.get_msg_gettext( '|-Detected that the certificate under {} overlaps with the certificate of this application and has an earlier expiration time, and has been replaced with a new certificate!', (to_path,))) + if path == paths[-1]: is_panel = True + # 重载web服务 public.serviceReload() - #if is_panel: public.restart_panel() + # if is_panel: public.restart_panel() # 检查指定证书是否在订单列表 def check_order_exists(self, pem_file): try: cert_init = self.get_cert_init(pem_file) - if not cert_init: return None + if not cert_init: + return None + if not (cert_init['issuer'].find("Let's Encrypt") != -1 or cert_init['issuer'] == 'R3'): + return None for index in self._config['orders'].keys(): if not 'save_path' in self._config['orders'][index]: continue for domain in self._config['orders'][index]['domains']: if domain in cert_init['dns']: return index - if cert_init['issuer'].find("Let's Encrypt") != -1 or cert_init['issuer'] == 'R3': - return pem_file - return None + return pem_file except: return None @@ -907,13 +1214,10 @@ fullchain.pem Paste into certificate input box cert_init = self.get_cert_init(args.pem_file) if not cert_init: return public.return_msg_gettext(False, 'Certificate information acquisition failed!') - api_path = './config/dns_api.json' - api_init = './config/dns_api_init.json' - if not os.path.exists(api_path): - if os.path.exists(api_init): - import shutil - shutil.copyfile(api_init, api_path) - cert_init['dnsapi'] = json.loads(public.readFile(self._dnsapi_file)) + try: + cert_init['dnsapi'] = json.loads(public.readFile(self._dnsapi_file)) + except: + cert_init['dnsapi'] = [] return cert_init # 获取指定证书基本信息 @@ -959,8 +1263,7 @@ fullchain.pem Paste into certificate input box else: result['subject'] = result['dns'][0] return result - except: - return None + except: return None # 转换时间 def strf_date(self, sdate): @@ -969,11 +1272,13 @@ fullchain.pem Paste into certificate input box # 证书转为DER def dump_der(self, cert_path): cert = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, public.readFile(cert_path + '/cert.csr')) + OpenSSL.crypto.FILETYPE_PEM, public.readFile(cert_path+'/cert.csr')) return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert) # 证书转为pkcs12 def dump_pkcs12(self, key_pem=None, cert_pem=None, ca_pem=None, friendly_name=None): + # from cryptography.hazmat.primitives.serialization import pkcs12 + p12 = OpenSSL.crypto.PKCS12() if cert_pem: p12.set_certificate(OpenSSL.crypto.load_certificate( @@ -989,7 +1294,7 @@ fullchain.pem Paste into certificate input box return p12.export() # 拆分根证书 - def split_ca_data(self, cert): + def split_ca_data(self,cert): sp_key = '-----END CERTIFICATE-----\n' datas = cert.split(sp_key) return {"cert": datas[0] + sp_key, "root": sp_key.join(datas[1:])} @@ -1039,11 +1344,11 @@ fullchain.pem Paste into certificate input box X509Req = OpenSSL.crypto.X509Req() X509Req.get_subject().CN = domain_name if domain_alt_names: - SAN = "DNS:{}, ".format(domain_name).encode("utf8") + ", ".join( + SAN = "DNS:{0}, ".format(domain_name).encode("utf8") + ", ".join( "DNS:" + i for i in domain_alt_names ).encode("utf8") else: - SAN = "DNS:{}".format(domain_name).encode("utf8") + SAN = "DNS:{0}".format(domain_name).encode("utf8") X509Req.add_extensions( [ @@ -1072,7 +1377,7 @@ fullchain.pem Paste into certificate input box acme_thumbprint = self.calculate_safe_base64( hashlib.sha256(acme_header_jwk_json.encode("utf8")).digest() ) - acme_keyauthorization = "{}.{}".format(token, acme_thumbprint) + acme_keyauthorization = "{0}.{1}".format(token, acme_thumbprint) base64_of_acme_keyauthorization = self.calculate_safe_base64( hashlib.sha256(acme_keyauthorization.encode("utf8")).digest() ) @@ -1152,7 +1457,7 @@ fullchain.pem Paste into certificate input box # 获取kid def get_kid(self, force=False): - # 如果配置文件中不存在kid或force = True时则重新注册新的acme帐户 + #如果配置文件中不存在kid或force = True时则重新注册新的acme帐户 if not 'account' in self._config: self._config['account'] = {} k = self._mod_index[self._debug] @@ -1169,13 +1474,13 @@ fullchain.pem Paste into certificate input box # 注册acme帐户 def register(self, existing=False): if not 'email' in self._config: - self._config['email'] = 'demo@bt.cn' + self._config['email'] = 'demo@aapanel.com' if existing: payload = {"onlyReturnExisting": True} elif self._config['email']: payload = { "termsOfServiceAgreed": True, - "contact": ["mailto:{}".format(self._config['email'])], + "contact": ["mailto:{0}".format(self._config['email'])], } else: payload = {"termsOfServiceAgreed": True} @@ -1199,15 +1504,15 @@ fullchain.pem Paste into certificate input box protected = self.get_acme_header(url) protected64 = self.calculate_safe_base64(json.dumps(protected)) signature = self.sign_message( - message="{}.{}".format(protected64, payload64)) # bytes + message="{0}.{1}".format(protected64, payload64)) # bytes signature64 = self.calculate_safe_base64(signature) # str data = json.dumps( {"protected": protected64, "payload": payload64, - "signature": signature64} + "signature": signature64} ) headers.update({"Content-Type": "application/jose+json"}) response = requests.post( - url, data=data.encode("utf8"), timeout=self._acme_timeout, headers=headers, verify=self._verify + url, data=data.encode("utf8"), timeout=self._acme_timeout, headers=headers, verify=self._verify,s_type=self._request_type ) # 更新随机数 self.update_replay_nonce(response) @@ -1241,7 +1546,8 @@ fullchain.pem Paste into certificate input box self._apis['newNonce'], timeout=self._acme_timeout, headers=headers, - verify=self._verify + verify=self._verify, + s_type=self._request_type ) self._replay_nonce = response.headers["Replay-Nonce"] return self._replay_nonce @@ -1260,7 +1566,7 @@ fullchain.pem Paste into certificate input box public_key_public_numbers = private_key.public_key().public_numbers() exponent = "{0:x}".format(public_key_public_numbers.e) - exponent = "0{}".format(exponent) if len( + exponent = "0{0}".format(exponent) if len( exponent) % 2 else exponent modulus = "{0:x}".format(public_key_public_numbers.n) jwk = { @@ -1320,7 +1626,7 @@ fullchain.pem Paste into certificate input box # 写配置文件 def save_config(self): - fp = open(self._conf_file, 'w+') + fp = open(self._conf_file_v2, 'w+') fcntl.flock(fp, fcntl.LOCK_EX) # 加锁 fp.write(json.dumps(self._config)) fcntl.flock(fp, fcntl.LOCK_UN) # 解锁 @@ -1329,16 +1635,16 @@ fullchain.pem Paste into certificate input box # 读配置文件 def read_config(self): - if not os.path.exists(self._conf_file): + if not os.path.exists(self._conf_file_v2): self._config['orders'] = {} self._config['account'] = {} self._config['apis'] = {} - self._config['email'] = public.M('config').where('id=?', (1,)).getField('email') - if self._config['email'] in ['287962566@qq.com']: + self._config['email'] = public.M('config').where('id=?',(1,)).getField('email') + if self._config['email'] in [public.en_hexb('4d6a67334f5459794e545932514846784c6d4e7662513d3d')]: self._config['email'] = None self.save_config() return self._config - tmp_config = public.readFile(self._conf_file) + tmp_config = public.readFile(self._conf_file_v2) if not tmp_config: return self._config try: @@ -1356,8 +1662,6 @@ fullchain.pem Paste into certificate input box index = None if 'index' in args: index = args['index'] - if 'auto_wildcard' in args: - self._auto_wildcard = 1 if not index: # 判断是否只想验证域名 write_log(public.get_msg_gettext('|-Creating order..')) index = self.create_order(domains, auth_type, auth_to) @@ -1389,223 +1693,121 @@ fullchain.pem Paste into certificate input box # 申请证书 - api def apply_cert_api(self, args): - # 在面板点击申请证书时不要重启面板以防后续请求出错 - self._by_panel = True - # 是否为指定站点 - if public.M('sites').where('id=? and project_type=?', (args.id, 'Java')).count(): - project_info = public.M('sites').where('id=?', (args.id,)).getField('project_config') - try: - project_info = json.loads(project_info) - if not 'ssl_path' in project_info: - return public.return_msg_gettext(False, - 'There is a problem with the current Java project configuration file, please rebuild') - if not os.path.exists(project_info['ssl_path']): - os.makedirs(project_info['ssl_path']) - path = project_info['ssl_path'] - args.auth_to = path - check_result = self.check_auth_env(args) - if check_result: return check_result + """ + @name 申请证书 + @param domains: list 域名列表 + @param auth_type: str 认证方式 + @param auth_to: str 认证路径 + @param auto_wildcard: str 是否自动组合泛域名 + """ + if not 'id' in args: + return public.return_msg_gettext(False,'Website ID cannot be empty!') - if args.auto_wildcard == '1': - self._auto_wildcard = True - self.turnon_redirect_proxy_httptohttps(args) - return self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) + if 'auto_wildcard' in args and args.auto_wildcard == '1': + self._auto_wildcard = True + + find = public.M('sites').where('id=?', (args.id,)).find() + if not find: + return public.return_msg_gettext(False, "Website lost, unable to continue applying for certificate") + + if args.auth_type in ['http', 'tls']: + if not self.can_use_base_file_check(find["name"], find["project_type"]): + webserver: str = public.get_webserver() + msg = "The service ({}) configuration file of the current project has been modified and does not support file verification. Please choose another method or restore the configuration file".format(webserver.title()) + if webserver != 'nginx': + return public.return_msg_gettext(False, msg) + # nginx 检测其他两种方案的可行性 + if not self.can_use_lua_for_site(find["name"], find["project_type"]) and \ + not self.can_use_if_for_file_check(find["name"], find["project_type"]): + + return public.return_msg_gettext(False, msg) + else: + return self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) + + # 是否为指定站点 + count = public.M('sites').where( + 'id=? and project_type in (?,?,?,?)', + (args.id, 'Java', 'Go', 'Other', "Python") + ).count() + if count: + try: + project_info = json.loads(find['project_config']) + if 'ssl_path' not in project_info: + ssl_path = '/www/wwwroot/java_node_ssl' + else: + ssl_path = project_info['ssl_path'] + if not os.path.exists(ssl_path): + os.makedirs(ssl_path) + + args.auth_to = ssl_path except: - return public.return_msg_gettext(False, - 'There is a problem with the current Java project configuration file, please rebuild') - finally: - self.turnon_redirect_proxy_httptohttps(args) + return public.return_msg_gettext(False, 'There is an issue with the current project configuration file, please rebuild it') else: if re.match(r"^\d+$", args.auth_to): import panelSite - path = public.M('sites').where('id=?', (args.id,)).getField('path') - args.auth_to = path + '/' + panelSite.panelSite().GetRunPath(args) + args.auth_to = find['path'] + '/' + panelSite.panelSite().GetRunPath(args) args.auth_to = args.auth_to.replace("//", "/") if args.auth_to[-1] == '/': args.auth_to = args.auth_to[:-1] if not os.path.exists(args.auth_to): - self.turnon_redirect_proxy_httptohttps(args) - return public.return_msg_gettext(False, - 'Invalid site directory, please check if the specified site exists!') + return public.return_msg_gettext(False, 'Invalid site directory, please check if the specified site exists!') - check_result = self.check_auth_env(args, check=True) - if check_result: return check_result - if args.auto_wildcard == '1': - self._auto_wildcard = True - res = self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) - if os.path.exists(self._stop_rp_file): - self.turnon_redirect_proxy_httptohttps(args) - return res + # 检查认证环境 + check_result = self.check_auth_env(args) + if check_result: + return check_result - def turnon_redirect_proxy_httptohttps(self, args): - import panelSite - s = panelSite.panelSite() - if not 'siteName' in args: - args.siteName = public.M('sites').where('id=?', (args.id,)).getField('name') - args.sitename = args.siteName - self.turnon_redirect(args, s) - self.turnon_proxy(args, s) - self.turnon_httptohttps(args, s) - public.serviceReload() - - def turnon_httptohttps(self, args, s): - conf_file = '{}/data/stop_httptohttps.pl'.format(public.get_panel_path()) - if os.path.exists(conf_file): - write_log('|-Turning on http to https') - s.HttpToHttps(args) - try: - os.remove(conf_file) - except: - pass - - def turnon_proxy(self, args, s): - conf_file = '{}/data/stop_p_tmp.pl'.format(public.get_panel_path()) - if not os.path.exists(conf_file): - return - write_log('|-Turning on proxy') - conf = json.loads(public.readFile(conf_file)) - data = s.GetProxyList(args) - for x in data: - if x['sitename'] not in conf: - continue - if x['proxyname'] not in conf[x['sitename']]: - continue - args.type = 1 - args.advanced = x['advanced'] - args.cache = x['cache'] - args.cachetime = x['cachetime'] - args.proxydir = x['proxydir'] - args.proxyname = x['proxyname'] - args.proxysite = x['proxysite'] - args.sitename = x['sitename'] - args.subfilter = json.dumps(x['subfilter']) - args.todomain = x['todomain'] - s.ModifyProxy(args) - try: - os.remove(conf_file) - except: - pass - - def turnon_redirect(self, args, s): - conf_file = '{}/data/stop_r_tmp.pl'.format(public.get_panel_path()) - if not os.path.exists(conf_file): - return - write_log('|-Turning on redirection') - conf = json.loads(public.readFile(conf_file)) - data = s.GetRedirectList(args) - for x in data: - if x['sitename'] not in conf: - continue - if x['redirectname'] not in conf[x['sitename']]: - continue - args.type = 1 - args.sitename = x['sitename'] - args.holdpath = x['holdpath'] - args.redirectname = x['redirectname'] - args.redirecttype = x['redirecttype'] - args.domainorpath = x['domainorpath'] - args.redirectpath = x['redirectpath'] - args.redirectdomain = json.dumps(x['redirectdomain']) - args.tourl = x['tourl'] - s.ModifyRedirect(args) - try: - os.remove(conf_file) - except: - pass + return self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) # 检查认证环境 - def check_auth_env(self, args, check=None): - if not check: - return + def check_auth_env(self,args): for domain in json.loads(args.domains): if public.checkIp(domain): continue - if domain.find('*.') != -1 and args.auth_type in ['http', 'tls']: - raise public.return_msg_gettext(False, - 'Pan domain names cannot apply for a certificate using [File Verification]!') + if domain.find('*.') != -1 and args.auth_type in ['http','tls']: + return public.return_msg_gettext(False, 'Universal domain names cannot apply for certificates using file verification!') + + data = public.M('sites').where('id=?', (args.id,)).find() + if not data: + return public.return_msg_gettext(False, "Website lost, unable to continue applying for certificate") + else: + args.siteName = data['name'] + site_type = data["project_type"] + + use_nginx_conf_to_auth = False + if args.auth_type in ['http', 'tls'] and public.get_webserver() == "nginx": # nginx 在lua验证和可重启的 + if self.can_use_lua_for_site(args.siteName, site_type): + use_nginx_conf_to_auth = True + else: + if self.can_use_if_for_file_check(args.siteName, site_type): + use_nginx_conf_to_auth = True + import panelSite s = panelSite.panelSite() - if args.auth_type in ['http', 'tls']: + if args.auth_type in ['http', 'tls'] and use_nginx_conf_to_auth is False: try: - rp_conf = public.readFile(self._stop_rp_file) - try: - if rp_conf: - rp_conf = json.loads(rp_conf) - except: - write_log('|-Failed to parse configuration file') - if not 'siteName' in args: - args.siteName = public.M('sites').where('id=?', (args.id,)).getField('name') args.sitename = args.siteName data = s.GetRedirectList(args) # 检查重定向是否开启 if type(data) == list: - redirect_tmp = {args.sitename: []} for x in data: - if rp_conf and x['sitename'] in rp_conf: - if str(x['type']) == '0': - continue - args.type = 0 - args.sitename = x['sitename'] - args.holdpath = x['holdpath'] - args.redirectname = x['redirectname'] - args.redirecttype = x['redirecttype'] - args.domainorpath = x['domainorpath'] - args.redirectpath = x['redirectpath'] - args.redirectdomain = json.dumps(x['redirectdomain']) - args.tourl = x['tourl'] - args.notreload = True - write_log("|- Turning off redirection {}".format(args.redirectname)) - s.ModifyRedirect(args) - redirect_tmp[args.sitename].append(x['redirectname']) - else: - if x['type']: return public.return_msg_gettext(False, + if x['type']: return public.return_msg_gettext(False, 'Your site has 301 Redirect on,Please turn it off first!') - if redirect_tmp[args.sitename]: - public.writeFile('{}/data/stop_r_tmp.pl'.format(public.get_panel_path()), - json.dumps(redirect_tmp)) data = s.GetProxyList(args) - # 检查反向代理是否开启 - if type(data) == list: - proxy_tmp = {args.sitename: []} - for x in data: - if rp_conf and x['sitename'] in rp_conf: - if str(x['type']) == '0': - continue - args.type = 0 - args.advanced = x['advanced'] - args.cache = x['cache'] - args.cachetime = x['cachetime'] - args.proxydir = x['proxydir'] - args.proxyname = x['proxyname'] - args.proxysite = x['proxysite'] - args.sitename = x['sitename'] - args.subfilter = json.dumps(x['subfilter']) - args.todomain = x['todomain'] - args.notreload = True - s.ModifyProxy(args) - write_log("|- Turning off proxy {}".format(args.proxyname)) - proxy_tmp[args.sitename].append(x['proxyname']) - else: - if x['type']: return public.return_msg_gettext(False, - 'Sites with reverse proxy turned on cannot apply for SSL!') - if proxy_tmp[args.sitename]: - public.writeFile('{}/data/stop_p_tmp.pl'.format(public.get_panel_path()), json.dumps(proxy_tmp)) + # # 检查反向代理是否开启 + # if type(data) == list: + # for x in data: + # if x['type']: return public.return_msg_gettext(False, + # 'Sites with reverse proxy turned on cannot apply for SSL!') # 检查旧重定向是否开启 data = s.Get301Status(args) if data['status']: return public.return_msg_gettext(False, 'The website has been redirected, please close it before applying!') - # 判断是否强制HTTPS + #判断是否强制HTTPS if s.IsToHttps(args.siteName): - if os.path.exists(self._stop_rp_file): - if rp_conf and args.siteName in rp_conf: - write_log("|- Turning off http to https") - s.CloseToHttps(args) - public.writeFile('{}/data/stop_httptohttps.pl'.format(public.get_panel_path()), '') - else: - return public.return_msg_gettext(False, + return public.return_msg_gettext(False, 'After configuring Force HTTPS, you cannot use [File Verification] to apply for a certificate!') - public.serviceReload() except: return False else: @@ -1617,39 +1819,61 @@ fullchain.pem Paste into certificate input box # DNS手动验证 def apply_dns_auth(self, args): + if not hasattr(args, "index") or not args.index: + return public.return_msg_gettext(False, "Incomplete parameter information, no index parameter [index]") return self.apply_cert([], auth_type='dns', auth_to='dns', index=args.index) - # 创建计划任务 + + #创建计划任务 def set_crond(self): try: echo = public.md5(public.md5('renew_lets_ssl_bt')) - cron_id = public.M('crontab').where('echo=?', (echo,)).getField('id') + find = public.M('crontab').where('echo=?',(echo,)).find() + cron_id = find['id'] if find else None import crontab + import random args_obj = public.dict_obj() if not cron_id: cronPath = public.GetConfigValue('setup_path') + '/cron/' + echo shell = '{} -u /www/server/panel/class/acme_v2.py --renew=1'.format(sys.executable) - public.writeFile(cronPath, shell) - args_obj.id = public.M('crontab').add( - 'name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress', - ("Renew Let's Encrypt Certificate", 'day', '', '0', '10', echo, - time.strftime('%Y-%m-%d %X', time.localtime()), 0, '', 'localhost', 'toShell', '', shell, '')) + public.writeFile(cronPath,shell) + + # 使用随机时间 + hour = random.randint(0, 23) + minute = random.randint(1, 59) + args_obj.id = public.M('crontab').add('name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress',("Renew Let's Encrypt Certificate",'day','',hour,minute,echo,time.strftime('%Y-%m-%d %X',time.localtime()),0,'','localhost','toShell','',shell,'')) crontab.crontab().set_cron_status(args_obj) else: + # 检查任务如果是0点10分执行,改为随机时间 + if find['where_hour'] == 0 and find['where_minute'] == 10: + # print('修改任务时间') + # 使用随机时间 + hour = random.randint(0, 23) + minute = random.randint(1, 59) + public.M('crontab').where('id=?',(cron_id,)).save('where_hour,where_minute,status',(hour,minute,0)) + + # 停用任务 + args_obj.id = cron_id + crontab.crontab().set_cron_status(args_obj) + + # 启用任务 + public.M('crontab').where('id=?',(cron_id,)).setField('status',1) + crontab.crontab().set_cron_status(args_obj) + cron_path = public.get_cron_path() if os.path.exists(cron_path): cron_s = public.readFile(cron_path) if cron_s.find(echo) == -1: - public.M('crontab').where('echo=?', (echo,)).setField('status', 0) + public.M('crontab').where('id=?',(cron_id,)).setField('status',0) args_obj.id = cron_id crontab.crontab().set_cron_status(args_obj) - except: - pass + except:pass + # 获取当前正在使用此证书的网站目录 - def get_ssl_used_site(self, save_path): - pkey_file = '{}/privkey.pem'.format(save_path) + def get_ssl_used_site(self,save_path): + pkey_file = '{}/privkey.pem'.format(save_path) pkey = public.readFile(pkey_file) if not pkey: return False cert_paths = 'vhost/cert' @@ -1657,19 +1881,32 @@ fullchain.pem Paste into certificate input box args = public.dict_obj() args.siteName = '' for c_name in os.listdir(cert_paths): - skey_file = '{}/{}/privkey.pem'.format(cert_paths, c_name) + skey_file = '{}/{}/privkey.pem'.format(cert_paths,c_name) skey = public.readFile(skey_file) if not skey: continue if skey == pkey: args.siteName = c_name + site_info = public.M('sites').where('name=?', c_name).find() + if not site_info or isinstance(site_info, str): + return False + if site_info["project_type"] not in ("PHP", "proxy"): + if not os.path.isdir(site_info["path"]): + return os.path.dirname(site_info["path"]) + else: + return site_info["path"] + run_path = panelSite.panelSite().GetRunPath(args) - if not run_path: continue - sitePath = public.M('sites').where('name=?', c_name).getField('path') - if not sitePath: continue - to_path = "{}/{}".format(sitePath, run_path) + if not run_path: + continue + sitePath = public.M('sites').where('name=?',c_name).getField('path') + if not sitePath: + continue + to_path = "{}/{}".format(sitePath,run_path) + to_path = to_path.replace("//", "/") return to_path return False + def get_site_id(self, domains): site_ids = [] for domain in domains: @@ -1686,28 +1923,13 @@ fullchain.pem Paste into certificate input box return False return site_ids[0] - def get_site_runpath(self, domains): - site_id = self.get_site_id(domains) - if not site_id: - return False - import panelSite - from collections import namedtuple - ps = panelSite.panelSite() - # 构造一个类 - get = namedtuple("get", ["id"]) - get.id = site_id - site_path = public.M('sites').where('id=?', (get.id,)).field('path').select()[0]['path'] - runpath = ps.GetRunPath(get) - return site_path + runpath - def find_site_stopped(self, domains): site_id = self.get_site_id(domains) if not site_id: return False site_status = public.M('sites').where('id=?', (site_id,)).field('status').select()[0]['status'] return site_status - - def get_index(self, domains): + def get_index(self,domains): ''' @name 获取标识 @author hwliang<2022-02-10> @@ -1719,6 +1941,7 @@ fullchain.pem Paste into certificate input box identifiers.append({"type": 'dns', "value": domain_name}) return public.md5(json.dumps(identifiers)) + # 续签同品牌其它证书 def renew_cert_other(self): ''' @@ -1729,33 +1952,31 @@ fullchain.pem Paste into certificate input box cert_path = "{}/vhost/cert".format(public.get_panel_path()) if not os.path.exists(cert_path): return new_time = time.time() + (86400 * 30) - n = 0 + n=0 if not 'orders' in self._config: self._config['orders'] = {} import panelSite siteObj = panelSite.panelSite() args = public.dict_obj() for siteName in os.listdir(cert_path): try: - cert_file = '{}/{}/fullchain.pem'.format(cert_path, siteName) - if not os.path.exists(cert_file): continue # 无证书文件 - siteInfo = public.M('sites').where('name=?', siteName).find() - if not siteInfo: continue # 无网站信息 + cert_file = '{}/{}/fullchain.pem'.format(cert_path,siteName) + if not os.path.exists(cert_file): continue # 无证书文件 + siteInfo = public.M('sites').where('name=?',siteName).find() + if not siteInfo: continue # 无网站信息 cert_init = self.get_cert_init(cert_file) - if not cert_init: continue # 无法获取证书 - end_time = time.mktime(time.strptime(cert_init['notAfter'], '%Y-%m-%d')) - if end_time > new_time: continue # 未到期 + if not cert_init: continue # 无法获取证书 + end_time = time.mktime(time.strptime(cert_init['notAfter'],'%Y-%m-%d')) + if end_time > new_time: continue # 未到期 try: - if not cert_init['issuer'] in ['R3', "Let's Encrypt"] and cert_init['issuer'].find( - "Let's Encrypt") == -1: - continue # 非同品牌证书 - except: - continue + if not cert_init['issuer'] in ['R3',"Let's Encrypt"] and cert_init['issuer'].find("Let's Encrypt") == -1: + continue # 非同品牌证书 + except: continue - if isinstance(cert_init['dns'], str): cert_init['dns'] = [cert_init['dns']] + if isinstance(cert_init['dns'],str): cert_init['dns'] = [cert_init['dns']] index = self.get_index(cert_init['dns']) - if index in self._config['orders'].keys(): continue # 已在订单列表 + if index in self._config['orders'].keys(): continue # 已在订单列表 - n += 1 + n+=1 write_log("|-Renewing additional certificate {}, domain name:{}..".format(n, cert_init['subject'])) write_log("|-Creating order..") args.id = siteInfo['id'] @@ -1765,12 +1986,12 @@ fullchain.pem Paste into certificate input box else: path = siteInfo['path'] - self.renew_cert_to(cert_init['dns'], 'http', path.replace('//', '/')) + self.renew_cert_to(cert_init['dns'],'http',path.replace('//','/')) except: - write_log("|-Renewal failed:") + write_log("|-Renewal failed:") # 关闭强制https - def close_httptohttps(self, siteName): + def close_httptohttps(self,siteName): try: if not siteName: siteName @@ -1786,7 +2007,7 @@ fullchain.pem Paste into certificate input box return False # 恢复强制https - def rep_httptohttps(self, siteName): + def rep_httptohttps(self,siteName): try: if not siteName: return False import panelSite @@ -1799,13 +2020,14 @@ fullchain.pem Paste into certificate input box except: return False - def renew_cert_to(self, domains, auth_type, auth_to, index=None): + + def renew_cert_to(self,domains,auth_type,auth_to,index = None): siteName = None cert = {} if os.path.exists(auth_to): - if public.M('sites').where('path=?', auth_to).count() == 1: - site_id = public.M('sites').where('path=?', auth_to).getField('id') - siteName = public.M('sites').where('path=?', auth_to).getField('name') + if public.M('sites').where('path=?',auth_to).count() == 1: + site_id = public.M('sites').where('path=?',auth_to).getField('id') + siteName = public.M('sites').where('path=?',auth_to).getField('name') import panelSite siteObj = panelSite.panelSite() args = public.dict_obj() @@ -1813,16 +2035,23 @@ fullchain.pem Paste into certificate input box runPath = siteObj.GetRunPath(args) if runPath and not runPath in ['/']: path = auth_to + '/' + runPath - if os.path.exists(path): auth_to = path.replace('//', '/') + if os.path.exists(path): auth_to = path.replace('//','/') else: - siteName = self.get_site_name_by_domains(domains) + siteName, _ = self.get_site_name_by_domains(domains) + + isError = public.checkWebConfig() + if isError is not True and public.get_webserver() == "nginx": + write_log("|- The certificate uses the file verification method, but currently it cannot overload the nginx server configuration file and can only skip renewal.") + write_log("|- The error message in the configuration file is as follows:") + write_log(isError) + is_rep = self.close_httptohttps(siteName) try: index = self.create_order( domains, auth_type, - auth_to.replace('//', '/'), + auth_to.replace('//','/'), index ) @@ -1847,6 +2076,7 @@ fullchain.pem Paste into certificate input box cert['msg'] = 'Renewed successfully!' write_log("|-Renewed successfully!!") except Exception as e: + if str(e).find('please try again later') == -1: # 受其它证书影响和连接CA失败的的不记录重试次数 if index: # 设置下次重试时间 @@ -1865,6 +2095,7 @@ fullchain.pem Paste into certificate input box write_log("-" * 70) return cert + # 续签证书 def renew_cert(self, index): write_log("", "wb+") @@ -1873,9 +2104,7 @@ fullchain.pem Paste into certificate input box if index: if type(index) != str: index = index.index - # 在面板点击申请证书时不要重启面板以防后续请求出错 - self._by_panel = True - if index not in self._config['orders']: + if not index in self._config['orders']: raise Exception( public.get_msg_gettext('The specified order number does not exist and cannot be renewed!')) order_index.append(index) @@ -1889,45 +2118,50 @@ fullchain.pem Paste into certificate input box self._config['orders'][i]['cert_timeout'] = self._config['orders'][i]['cert']['cert_timeout'] if not 'cert_timeout' in self._config['orders'][i]: self._config['orders'][i]['cert_timeout'] = int(time.time()) - if self._config['orders'][i]['cert_timeout'] > s_time or self._config['orders'][i][ - 'auth_to'] == 'dns': + if self._config['orders'][i]['cert_timeout'] > s_time or self._config['orders'][i]['auth_to'] == 'dns': continue if self.find_site_stopped(self._config['orders'][i]['domains']) == '0': write_log("|-The website has been suspended, skip certificate renewal!") continue # 已删除的网站直接跳过续签 - if self._config['orders'][i]['auth_to'].find('|') == -1 and self._config['orders'][i][ - 'auth_to'].find('/') != -1: - #if not os.path.exists(self._config['orders'][i]['auth_to']): - # ^——这个不能判断网站已被删除,但文件夹未删除的问题 + is_file_check = (self._config['orders'][i]['auth_to'].find('|') == -1 or + not self._config['orders'][i]['auth_to'].startswith("dns") + ) and self._config['orders'][i]['auth_to'].find('/') != -1 + if is_file_check: + # if not os.path.exists(self._config['orders'][i]['auth_to']): + # ^^^^^^^^^^——————————这个不能判断网站已被删除的情况下,文件夹未删除时的问题 _auth_to = self.get_ssl_used_site(self._config['orders'][i]['save_path']) - if not _auth_to: continue + if not _auth_to: + continue # 域名不存在? for domain in self._config['orders'][i]['domains']: - if domain.find('*') != -1: break - if not public.M('domain').where("name=?", (domain,)).count() and not public.M( - 'binding').where("domain=?", domain).count(): + if domain.find('*') != -1: + break + if not public.M('domain').where("name=?",(domain,)).count() and not public.M('binding').where("domain=?",domain).count(): _auth_to = None - write_log("|-Skip deleted domains:{}".format(self._config['orders'][i]['domains'])) + write_log("|-Skip deleted domain names: {}".format(self._config['orders'][i]['domains'])) if not _auth_to: continue self._config['orders'][i]['auth_to'] = _auth_to + # 检查网站域名是否存在 + if not public.M('domain').where('`name` IN ({})'.format(', '.join(map(lambda x: "'{}'".format(x), filter(lambda x: x.find('*') < 0, self._config['orders'][i]['domains'])))), ()).count(): + write_log("|-Skip deleted domain names: {}".format(self._config['orders'][i]['domains'])) + continue + # 是否到了允许重试的时间 if 'next_retry_time' in self._config['orders'][i]: timeout = self._config['orders'][i]['next_retry_time'] - int(time.time()) if timeout > 0: - write_log( - '|-The domain name skipped this time: {}, because the last renewal failed, you still need to wait {} hours and try again'.format( - self._config['orders'][i]['domains'], int(timeout / 60 / 60))) + write_log('|-Skipping domain name: {} this time, due to last renewal failure, we need to wait {} hours before trying again'.format(self._config['orders'][i]['domains'],int(timeout / 60 / 60))) continue # # 是否到了最大重试次数 # if 'retry_count' in self._config['orders'][i]: # if self._config['orders'][i]['retry_count'] >= 5: - # write_log('|-Skip the domain name this time: {}, this certificate will not be renewed due to failure to renew 5 times in a row, (The number of errors will be reset after manual renewal and success)'.format(self._config['orders'][i]['domains'])) + # write_log('|-本次跳过域名:{},因连续5次续签失败,不再续签此证书(可尝试手动续签此证书,成功后错误次数将被重置)'.format(self._config['orders'][i]['domains'])) # continue # 加入到续签订单 @@ -1938,52 +2172,24 @@ fullchain.pem Paste into certificate input box self.renew_cert_other() write_log("|-All tasks have been processed!") return - write_log( - public.get_msg_gettext('|-A total of {} certificates need to be renewed', (str(len(order_index)),))) + write_log("|-A total of {} certificates need to be renewed".format(len(order_index))) n = 0 self.get_apis() cert = None - args = public.to_dict_obj({}) for index in order_index: - args.domains = json.dumps(self._config['orders'][index]['domains']) - args.auth_type = self._config['orders'][index]['auth_type'] - args.auth_to = self._config['orders'][index]['auth_to'] - sitename = args.auth_to.split('/')[-1] - if not sitename: - sitename = self._config['orders'][index]['auth_to'].split('/')[-2] - args.siteName = sitename - write_log('|-Renew the visa certificate and start checking the environment') - self.check_auth_env(args, check=True) n += 1 - domains = _test_domains(self._config['orders'][index]['domains'], self._config['orders'][index]['auth_to'],self._config['orders'][index]['auth_type']) if len(domains) == 0: - write_log("|-The domain name under the {} certificate is not used (the domain name is: [%s]) and has been skipped.".format(n, ",".join(self._config['orders'][index]['domains']))) + write_log("|-The domain names under the {} certificate are all unused (these domains are: [%s]) and have been skipped.".format(n, ",".join(self._config['orders'][index]['domains']))) continue else: self._config['orders'][index]['domains'] = domains write_log(public.get_msg_gettext('|-Renewing certificate number of {},domain: {}..', - (str(n), str(self._config['orders'][index]['domains'])))) + (n, str(self._config['orders'][index]['domains'])))) write_log(public.get_msg_gettext('|-Creating order..')) - - cert = self.renew_cert_to(self._config['orders'][index]['domains'], - self._config['orders'][index]['auth_type'], - self._config['orders'][index]['auth_to'], index) - - # write_log(public.get_msg_gettext('|-Renewing certificate number of {},domain: {}..', - # (str(n), str(self._config['orders'][index]['domains'])))) - # write_log(public.get_msg_gettext('|-Creating order..')) - # cert = self.renew_cert_to(self._config['orders'][index]['domains'], - # self._config['orders'][index]['auth_type'], - # self._config['orders'][index]['auth_to'], index) - - # aapanel 用 - try: - self.turnon_redirect_proxy_httptohttps(args) - except: - pass - + cert = self.renew_cert_to(self._config['orders'][index]['domains'],self._config['orders'][index]['auth_type'],self._config['orders'][index]['auth_to'],index) return cert + except Exception as ex: self.remove_dns_record() ex = str(ex) @@ -1999,7 +2205,7 @@ fullchain.pem Paste into certificate input box def _test_domains(domains, auth_to, auth_type): # 检查站点域名变更情况, 若有删除域名,则在续签时,删除已经不使用的域名,再执行续签任务 # 是dns验证的跳过 - if auth_to.find("|") != -1: + if auth_to.find("|") != -1 or auth_to.startswith("dns#@"): return domains # 是泛域名的跳过 for domain in domains: @@ -2017,6 +2223,7 @@ def _test_domains(domains, auth_to, auth_type): if bool(site_id) and str(site_id).isdigit(): site_domains = [i["name"] for i in sql.where('pid=?',(site_id,)).field("name").select()] else: + # 全都查询不到,认为这个站点已经被删除 return [] del_domains = list(set(domains) - set(site_domains)) @@ -2043,10 +2250,9 @@ def write_log(log_str, mode="ab+"): f.close() return True - +# todo:兼容控制台,目前不兼容 if __name__ == "__main__": import argparse - p = argparse.ArgumentParser(usage=public.get_msg_gettext( 'Required parameters: --domain list of domain names, multiple separated by commas!')) p.add_argument('--domain', default=None, @@ -2164,7 +2370,6 @@ if __name__ == "__main__": write_log("=" * 65) write_log(public.get_msg_gettext('|-Certificate obtained successfully!')) write_log("=" * 65) - write_log(public.get_msg_gettext('Certified Domain Name: {}', (','.join(cert['domains']),))) write_log( public.get_msg_gettext('Certificate expiration time: {}', (public.format_date(times=cert['cert_timeout']),))) write_log(public.get_msg_gettext('Certificate saved at: {}/', (cert['save_path'],))) diff --git a/class/ajax.py b/class/ajax.py index 3c8acbef..cdf7f8ee 100755 --- a/class/ajax.py +++ b/class/ajax.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- from flask import session,request @@ -794,19 +794,19 @@ class ajax: if not os.path.exists(filename): return public.return_msg_gettext(False,'Requested PHP version does NOT exist!') phpini = public.readFile(filename) data = {} - rep = "disable_functions\s*=\s{0,1}(.*)\n" + rep = "disable_functions\\s*=\\s{0,1}(.*)\n" tmp = re.search(rep,phpini) if tmp: data['disable_functions'] = tmp.groups()[0] - rep = "upload_max_filesize\s*=\s*([0-9]+)(M|m|K|k)" + rep = r"upload_max_filesize\s*=\s*([0-9]+)(M|m|K|k)" tmp = re.search(rep,phpini) if tmp: data['max'] = tmp.groups()[0] - rep = u"\n;*\s*cgi\.fix_pathinfo\s*=\s*([0-9]+)\s*\n" + rep = u"\n;*\\s*cgi\\.fix_pathinfo\\s*=\\s*([0-9]+)\\s*\n" tmp = re.search(rep,phpini) if tmp: if tmp.groups()[0] == '0': @@ -1048,7 +1048,7 @@ class ajax: #AUTH_END """ # nginx配置文件 - ssl_conf = """server + ssl_conf = r"""server { listen 887 ssl; server_name phpmyadmin; @@ -1095,7 +1095,7 @@ class ajax: #AUTH_END """ # apache配置 - ssl_conf = '''Listen 887 + ssl_conf = r'''Listen 887 ServerAdmin webmaster@example.com DocumentRoot "/www/server/phpmyadmin" @@ -1134,6 +1134,13 @@ class ajax: '''.format(public.get_php_proxy(v["ext"]["phpversion"],'apache'),auth) public.writeFile("/www/server/panel/vhost/apache/phpmyadmin.conf", ssl_conf) + + import firewalls + fw = firewalls.firewalls() + fw.AddAcceptPort(public.to_dict_obj({ + 'port': '887', + 'ps': public.get_msg_gettext('New phpMyAdmin SSL Port'), + })) else: if os.path.exists("/www/server/panel/vhost/nginx/phpmyadmin.conf"): os.remove("/www/server/panel/vhost/nginx/phpmyadmin.conf") @@ -1331,9 +1338,9 @@ class ajax: conf = public.readFile('/etc/init.d/memcached') result = {} result['bind'] = re.search('IP=(.+)',conf).groups()[0] - result['port'] = int(re.search('PORT=(\d+)',conf).groups()[0]) - result['maxconn'] = int(re.search('MAXCONN=(\d+)',conf).groups()[0]) - result['cachesize'] = int(re.search('CACHESIZE=(\d+)',conf).groups()[0]) + result['port'] = int(re.search(r'PORT=(\d+)',conf).groups()[0]) + result['maxconn'] = int(re.search(r'MAXCONN=(\d+)',conf).groups()[0]) + result['cachesize'] = int(re.search(r'CACHESIZE=(\d+)',conf).groups()[0]) tn = telnetlib.Telnet(result['bind'],result['port']) tn.write(b"stats\n") tn.write(b"quit\n") @@ -1358,9 +1365,9 @@ class ajax: confFile = '/etc/init.d/memcached' conf = public.readFile(confFile) conf = re.sub('IP=.+','IP='+get.ip,conf) - conf = re.sub('PORT=\d+','PORT='+get.port,conf) - conf = re.sub('MAXCONN=\d+','MAXCONN='+get.maxconn,conf) - conf = re.sub('CACHESIZE=\d+','CACHESIZE='+get.cachesize,conf) + conf = re.sub(r'PORT=\d+','PORT='+get.port,conf) + conf = re.sub(r'MAXCONN=\d+','MAXCONN='+get.maxconn,conf) + conf = re.sub(r'CACHESIZE=\d+','CACHESIZE='+get.cachesize,conf) public.writeFile(confFile,conf) public.ExecShell(confFile + ' reload') return public.return_msg_gettext(True,'Setup successfully!') @@ -1369,8 +1376,8 @@ class ajax: def GetRedisStatus(self,get): import re c = public.readFile('/www/server/redis/redis.conf') - port = re.findall('\n\s*port\s+(\d+)',c)[0] - password = re.findall('\n\s*requirepass\s+(.+)',c) + port = re.findall('\n\\s*port\\s+(\\d+)',c)[0] + password = re.findall('\n\\s*requirepass\\s+(.+)',c) if password: password = ' -a ' + password[0] else: @@ -1431,113 +1438,9 @@ class ajax: if not os.path.exists(get.path): return public.return_msg_gettext(False,'Log file does NOT exist!') return public.returnMsg(True,public.xsssec(public.GetNumLines(get.path,1000))) - def get_pd(self,get): - from BTPanel import cache - tmp = -1 - try: - import panelPlugin - # get = public.dict_obj() - # get.init = 1 - tmp1 = panelPlugin.panelPlugin().get_cloud_list(get) - except: - tmp1 = None - if tmp1: - tmp = tmp1[public.to_string([112, 114, 111])] - ltd = tmp1.get('ltd', -1) - else: - ltd = -1 - tmp4 = cache.get(public.to_string([112, 95, 116, 111, 107, 101, 110])) - if tmp4: - tmp_f = public.to_string([47, 116, 109, 112, 47]) + tmp4 - if not os.path.exists(tmp_f): public.writeFile(tmp_f, '-1') - tmp = public.readFile(tmp_f) - if tmp: tmp = int(tmp) - if not ltd: ltd = -1 - if tmp == None: tmp = -1 - if ltd < 1: - if ltd == -2: - tmp3 = public.to_string( - [60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 116, 100, - 45, 103, 114, 97, 121, 34, 62, 60, 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, - 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, - 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, - 114, 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, 36807, - 26399, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, - 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, - 102, 116, 46, 117, 112, 100, 97, 116, 97, 95, 108, 116, 100, 40, 41, 34, 62, 82, 69, 78, 69, 87, - 60, 47, 97, - 62, 60, 47, 115, 112, 97, 110, 62]) - elif tmp == -1: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, - 116, 112, 114, 111, 45, 102, 114, 101, 101, 34, 32, 111, 110, 99, 108, 105, 99, - 107, - 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, - 114, - 111, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, 107, - 32, 116, 111, 32, - 103, 101, 116, 32, 80, 82, 79, 34, 62, 20813, 36153, 29256, 60, 47, 115, 112, - 97, 110, 62]) - elif tmp == -2: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, - 112, 114, 111, 45, 103, 114, 97, 121, 34, 62, 60, 115, 112, 97, 110, 32, - 115, 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, - 102, 99, 54, 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, - 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, - 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, 36807, 26399, - 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, - 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, - 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, 114, - 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, 112, 97, - 110, 62]) - if tmp >= 0 and ltd in [-1, -2]: - if tmp == 0: - - tmp2 = public.to_string([76, 105, 102, 101, 116, 105, 109, 101]) - tmp3 = public.to_string( - [60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 112, 114, - 111, 34, 62, 69, 120, 112, 105, 114, 101, 58, 60, 115, 112, 97, 110, 32, 115, - 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, - 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, - 98, 111, 108, 100, 59, 34, 62, 123, 48, 125, 60, 47, 115, 112, 97, 110, 62, 60, - 47, 115, 112, 97, 110, 62]).format(tmp2) - else: - tmp2 = time.strftime(public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), time.localtime(tmp)) - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, - 112, 114, 111, 34, 62, 69, 120, 112, 105, 114, 101, 58, 32, 60, 115, 112, - 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, - 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, - 101, 105, 103, 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, - 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 123, - 48, 125, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, - 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, - 99, - 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, - 95, - 112, 114, 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, - 47, 115, 112, 97, 110, 62]).format(tmp2) - else: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 112, - 114, 111, 45, 103, 114, 97, 121, 34, 32, 111, 110, 99, 108, 105, 99, 107, - 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 117, 112, 100, 97, 116, 97, 95, 112, - 114, 111, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, - 107, 32, 116, - 111, 32, 103, 101, 116, 32, 80, 82, 79, 34, 62, 70, 82, - 69, 69, 60, 47, 115, 112, 97, 110, 62]) - else: - tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 116, - 100, 34, 62, 69, 120, 112, 105, 114, 101, 58, 32, 60, 115, 112, 97, 110, 32, 115, - 116, - 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, - 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, - 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, - 112, 120, 34, 62, 123, 125, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, - 97, 115, 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, - 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, - 112, 114, 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, - 112, 97, 110, 62]).format( - time.strftime(public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), time.localtime(ltd))) - - return tmp3, tmp, ltd + # 获取授权信息 + def get_pd(self, get): + return public.get_pd(get) #检查用户绑定是否正确 def check_user_auth(self,get): diff --git a/class/apache.py b/class/apache.py index 2d4a34f3..3abb64f7 100644 --- a/class/apache.py +++ b/class/apache.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2018 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2018 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -52,7 +52,7 @@ class apache: data = {} # 计算启动时间 - Uptime = re.search("ServerUptimeSeconds:\s+(.*)",result) + Uptime = re.search(r"ServerUptimeSeconds:\s+(.*)",result) if not Uptime: return public.return_msg_gettext(False, "Get worker Uptime False") Uptime = int(Uptime.group(1)) @@ -63,11 +63,11 @@ class apache: min = math.floor(min - (days * 60 * 24) - (hours * 60)) #格式化重启时间 - restarttime = re.search("RestartTime:\s+(.*)",result) + restarttime = re.search(r"RestartTime:\s+(.*)",result) if not restarttime: return public.return_msg_gettext(False, "Get worker Restart Time False") restarttime = restarttime.group(1) - rep = "\w+,\s([\w-]+)\s([\d\:]+)\s\w+" + rep = r"\w+,\s([\w-]+)\s([\d\:]+)\s\w+" date = re.search(rep,restarttime) if not date: return public.return_msg_gettext(False, "Get worker date False") @@ -85,28 +85,28 @@ class apache: date = date.split("-") date = "%s-%s-%s" % (date[2],date[1],date[0]) - reqpersec = re.search("ReqPerSec:\s+(.*)", result) + reqpersec = re.search(r"ReqPerSec:\s+(.*)", result) if not reqpersec: return public.return_msg_gettext(False, "Get worker reqpersec False") reqpersec = reqpersec.group(1) - if re.match("^\.", reqpersec): + if re.match(r"^\.", reqpersec): reqpersec = "%s%s" % (0,reqpersec) data["RestartTime"] = "%s %s" % (date,timedetail) data["UpTime"] = "%s day %s hour %s minute" % (str(int(days)),str(int(hours)),str(int(min))) - total_acc = re.search("Total Accesses:\s+(\d+)",result) + total_acc = re.search(r"Total Accesses:\s+(\d+)",result) if not total_acc: return public.return_msg_gettext(False, "Get worker TotalAccesses False") data["TotalAccesses"] = total_acc.group(1) - total_kb = re.search("Total kBytes:\s+(\d+)",result) + total_kb = re.search(r"Total kBytes:\s+(\d+)",result) if not total_kb: return public.return_msg_gettext(False, "Get worker TotalKBytes False") data["TotalKBytes"] = total_kb.group(1) data["ReqPerSec"] = round(float(reqpersec), 2) - busywork = re.search("BusyWorkers:\s+(\d+)",result) + busywork = re.search(r"BusyWorkers:\s+(\d+)",result) if not busywork: return public.return_msg_gettext(False, "Get worker BusyWorkers False") data["BusyWorkers"] = busywork.group(1) - idlework = re.search("IdleWorkers:\s+(\d+)",result) + idlework = re.search(r"IdleWorkers:\s+(\d+)",result) if not idlework: return public.return_msg_gettext(False, "Get worker IdleWorkers False") data["IdleWorkers"] = idlework.group(1) @@ -119,7 +119,7 @@ class apache: apachempmcontent = public.readFile(self.apachempmfile) if not "mpm_event_module" in apachempmcontent: return public.returnMsg(False,"mpm_event_module conf not found or /www/server/apache/conf/extra/httpd-mpm.conf is empty") - apachempmcontent = re.search("\(\n|.)+?\",apachempmcontent).group() + apachempmcontent = re.search(r"\(\n|.)+?\",apachempmcontent).group() ps = ["%s,%s" % (public.get_msg_gettext('Second'),public.get_msg_gettext('Request timeout')), public.get_msg_gettext('Keep alive'), "%s,%s" % (public.get_msg_gettext('Second'),public.get_msg_gettext('Connection timeout')), @@ -131,7 +131,7 @@ class apache: conflist = [] n = 0 for i in gets: - rep = "(%s)\s+(\w+)" % i + rep = r"(%s)\s+(\w+)" % i k = re.search(rep, apachedefaultcontent) if not k: return public.return_msg_gettext(False, "Get Key {} False",(i,)) @@ -154,7 +154,7 @@ class apache: gets = ["StartServers","MaxSpareThreads","MinSpareThreads","ThreadsPerChild","MaxRequestWorkers","MaxConnectionsPerChild"] n = 0 for i in gets: - rep = "(%s)\s+(\w+)" % i + rep = r"(%s)\s+(\w+)" % i k = re.search(rep, apachempmcontent) if not k: return public.return_msg_gettext(False, "Get Key {} False",(i,)) @@ -175,7 +175,7 @@ class apache: if not "mpm_event_module" in apachempmcontent: return public.return_msg_gettext(False,"mpm_event_module conf not found or /www/server/apache/conf/extra/httpd-mpm.conf is empty") conflist = [] - getdict = get.__dict__ + getdict = get.get_items() for i in getdict.keys(): if i != "__module__" and i != "__doc__" and i != "data" and i != "args" and i != "action": getpost = { @@ -189,11 +189,11 @@ class apache: return public.return_msg_gettext(False, 'Parameter ERROR!') else: print(c["value"]) - if not re.search("\d+", c["value"]): + if not re.search(r"\d+", c["value"]): print(c["name"],c["value"]) return public.return_msg_gettext(False, 'Parameter ERROR!') - rep = "%s\s+\w+" % c["name"] + rep = r"%s\s+\w+" % c["name"] if re.search(rep,apachedefaultcontent): newconf = "%s %s" % (c["name"],c["value"]) apachedefaultcontent = re.sub(rep,newconf,apachedefaultcontent) @@ -214,7 +214,7 @@ class apache: def add_httpd_access_log_format(self,args): ''' @name 添加httpd日志格式 - @author zhwen + @author zhwen @param log_format 需要设置的日志格式["$server_name","$remote_addr","-"....] @param log_format_name @param act 操作方式 add/edit @@ -244,13 +244,13 @@ class apache: def del_httpd_access_log_format(self,args): ''' @name 删除日志格式 - @author zhwen + @author zhwen @param log_format_name ''' conf = public.readFile(self.httpdconf) if not conf: return public.return_msg_gettext(False, 'Configuration file not exist') - reg = '\s*#LOG_FORMAT_BEGIN_{n}(\n|.)+#LOG_FORMAT_END_{n}\n?'.format(n=args.log_format_name) + reg = r'\s*#LOG_FORMAT_BEGIN_{n}(\n|.)+#LOG_FORMAT_END_{n}\n?'.format(n=args.log_format_name) conf = re.sub(reg,'',conf) self._del_format_log_of_website(args.log_format_name) public.writeFile(self.httpdconf,conf) @@ -270,7 +270,7 @@ class apache: if not site_format_log_status[s]: continue website_conf_file = '/www/server/panel/vhost/apache/{}.conf'.format(s) - format_exist_reg = 'CustomLog\s+"/www.*"\s+{}'.format(log_format_name) + format_exist_reg = r'CustomLog\s+"/www.*"\s+{}'.format(log_format_name) conf = public.readFile(website_conf_file) if not conf:continue if not re.search(format_exist_reg,conf):continue @@ -323,7 +323,7 @@ class apache: format_name = [i.split('LOG_FORMAT_BEGIN_')[-1] for i in data] format_log = {} for i in format_name: - format_reg = "#LOG_FORMAT_BEGIN_{n}(\n|.)+LogFormat\s+\'(.*)\'\s+{n}".format(n=i) + format_reg = r"#LOG_FORMAT_BEGIN_{n}(\n|.)+LogFormat\s+\'(.*)\'\s+{n}".format(n=i) tmp = re.search(format_reg,conf) if not tmp: continue @@ -336,7 +336,7 @@ class apache: def set_httpd_format_log_to_website(self,args): ''' @name 设置网站日志格式 - @author zhwen + @author zhwen @param sites aaa.com,bbb.com @param log_format_name ''' @@ -344,13 +344,13 @@ class apache: sites = loads(args.sites) try: all_site = public.M('sites').field('name').select() - reg = 'CustomLog\s+"/www.*{}\s*'.format(args.log_format_name) + reg = r'CustomLog\s+"/www.*{}\s*'.format(args.log_format_name) for site in all_site: website_conf_file = '/www/server/panel/vhost/apache/{}.conf'.format(site['name']) conf = public.readFile(website_conf_file) if not conf: return public.return_msg_gettext(False, 'Configuration file not exist') - format_exist_reg = '(CustomLog\s+"/www.*\_log).*' + format_exist_reg = r'(CustomLog\s+"/www.*\_log).*' access_log = re.search(format_exist_reg, conf).groups()[0] + '" ' + args.log_format_name if site['name'] not in sites and re.search(format_exist_reg,conf): access_log = ' '.join(access_log.split()[:-1]) diff --git a/class/backup_bak.py b/class/backup_bak.py index 32aa117b..42e98757 100644 --- a/class/backup_bak.py +++ b/class/backup_bak.py @@ -1,8 +1,8 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: 1249648969@qq.com # | 主控 备份 @@ -283,7 +283,7 @@ class backup_bak: public.ExecShell("sed -i '/password=/d' /etc/my.cnf") if act: mycnf = public.readFile('/etc/my.cnf'); - rep = "\[mysqldump\]\nuser=root" + rep = "\\[mysqldump\\]\nuser=root" sea = "[mysqldump]\n" subStr = sea + "user=root\npassword=\"" + root + "\"\n"; mycnf = mycnf.replace(sea,subStr) diff --git a/class/cachelib/simple.py b/class/cachelib/simple.py index f07c52d4..83ec8e39 100644 --- a/class/cachelib/simple.py +++ b/class/cachelib/simple.py @@ -47,6 +47,9 @@ class SimpleCache(BaseCache): __session_key = 'BT_:' __session_basedir = '/www/server/panel/data/session' + __SHM_PREFIX = 'SHM_:' + __SHM_BASEDIR = '/dev/shm/aap-shm' + def __init__(self, threshold=500, default_timeout=300): BaseCache.__init__(self, default_timeout) self._cache = {} @@ -109,6 +112,15 @@ class SimpleCache(BaseCache): def get(self, key): if not isinstance(key,str): return None + + try: + # 优先从shm中查找 + _shm_val = self.__get_shm(key) + + if _shm_val is not None: + return _shm_val + except: pass + try: expires, value = self._cache[key] if expires == 0 or expires > time(): @@ -125,6 +137,12 @@ class SimpleCache(BaseCache): if value_type not in type_list: return False + try: + # 优先写入shm + if self.__set_shm(key, value, timeout): + return True + except: pass + # 过期清理 expires = self._normalize_timeout(timeout) self._prune() @@ -148,6 +166,12 @@ class SimpleCache(BaseCache): if value_type not in type_list: return False + try: + # 优先写入shm + if self.__add_shm(key, value, timeout): + return True + except: pass + expires = self._normalize_timeout(timeout) self._prune() try: @@ -162,11 +186,23 @@ class SimpleCache(BaseCache): return True def delete(self, key): + try: + # 优先删除shm + if self.__del_shm(key): + return True + except: pass + result = self._cache.pop(key, None) is not None self.del_session_by_file(key) return result def has(self, key): + try: + # 优先shm + if self.__has_shm(key): + return True + except: pass + try: expires, value = self._cache[key] return expires == 0 or expires > time() @@ -174,7 +210,6 @@ class SimpleCache(BaseCache): if self.get_session_by_file(key): return True return False - def get_expire_time(self, key): try: expires, value = self._cache[key] @@ -194,3 +229,119 @@ class SimpleCache(BaseCache): m.update(strings.encode('utf-8')) return m.hexdigest() + def __set_shm(self, key, value, timeout=None): + ''' + @name 尝试将缓存写入shm目录 + @author Zhj<2022-10-08> + @param key 键名 + @param value 值 + @param timeout 存活时间/秒 + @return bool + ''' + if key[:5] != self.__SHM_PREFIX: + return False + + self.__makesure_shm_basedir() + + expires = struct.pack('f', self._normalize_timeout(timeout)) + filename = '/'.join((self.__SHM_BASEDIR, self.md5(key))) + with open(filename, 'wb') as fp: + fp.write(expires + pickle.dumps(value, pickle.HIGHEST_PROTOCOL)) + os.chmod(filename, 384) + + return True + + def __get_shm(self, key): + ''' + @name 尝试从shm目录下读取缓存 + @author Zhj<2022-10-08> + @param key 键名 + @return mixed|None + ''' + if key[:5] != self.__SHM_PREFIX: + return None + + self.__makesure_shm_basedir() + + filename = '/'.join((self.__SHM_BASEDIR, self.md5(key))) + if not os.path.exists(filename): return None + + with open(filename, 'rb') as fp: + _val = fp.read() + + expires = struct.unpack('f', _val[:4])[0] + + # 过期 删除缓存文件 + if expires > 0 and expires <= time(): + os.remove(filename) + return None + + return pickle.loads(_val[4:]) + + def __del_shm(self, key): + ''' + @name 删除shm目录下的缓存 + @author Zhj<2022-10-08> + @param key 键名 + @return bool + ''' + if key[:5] != self.__SHM_PREFIX: + return False + + self.__makesure_shm_basedir() + + filename = '/'.join((self.__SHM_BASEDIR, self.md5(key))) + if os.path.exists(filename): + os.remove(filename) + + return True + + def __has_shm(self, key): + ''' + @name 检查shm目录下的缓存是否存在 + @author Zhj<2022-10-08> + @param key 键名 + @return bool + ''' + if key[:5] != self.__SHM_PREFIX: + return False + + self.__makesure_shm_basedir() + + filename = '/'.join((self.__SHM_BASEDIR, self.md5(key))) + if not os.path.exists(filename): return False + + # 获取缓存过期时间 + with open(filename, 'rb') as fp: + expires = struct.unpack('f', fp.read(4))[0] + + # 过期 删除缓存文件 + if expires > 0 and expires <= time(): + os.remove(filename) + return False + + return True + + def __add_shm(self, key, value, timeout=None): + ''' + @name 尝试添加缓存到shm目录下 + @author Zhj<2022-10-08> + @param key 键名 + @param value 值 + @param timeout 缓存存活时间/秒 + @return bool + ''' + if self.__has_shm(key): + return False + + return self.__set_shm(key, value, timeout) + + def __makesure_shm_basedir(self): + ''' + @name 确保shm下的缓存目录存在 + @author Zhj<2022-10-08> + @return void + ''' + if not os.path.exists(self.__SHM_BASEDIR): + os.makedirs(self.__SHM_BASEDIR, 384) + diff --git a/class/common.py b/class/common.py index 4e382409..58a4de6a 100755 --- a/class/common.py +++ b/class/common.py @@ -1,12 +1,12 @@ -#coding: utf-8 +# coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- -from BTPanel import session, cache , request, redirect, g,abort +from BTPanel import session, cache , request, redirect, g,abort, Response from datetime import datetime from public import dict_obj import os @@ -21,13 +21,13 @@ class panelSetup: panel_path = public.get_panel_path() if os.getcwd() != panel_path: os.chdir(panel_path) - g.ua = request.headers.get('User-Agent','') + g.ua = request.headers.get('User-Agent', '') if g.ua: ua = g.ua.lower() if ua.find('spider') != -1 or g.ua.find('bot') != -1: return abort(403) - g.version = '6.8.37' + g.version = '7.0.6' g.title = public.GetConfigValue('title') g.uri = request.path g.debug = os.path.exists('data/debug.pl') @@ -55,7 +55,6 @@ class panelSetup: self.other_import() return None - def other_import(self): g.o = public.readFile('data/o.pl') g.other_css = [] @@ -71,7 +70,6 @@ class panelSetup: if os.path.exists(js_file): g.other_js.append('/static/other/{}'.format(js_name)) - class panelAdmin(panelSetup): setupPath = '/www/server' @@ -82,6 +80,7 @@ class panelAdmin(panelSetup): return result result = self.check_login() if result: + # public.print_log("local 2登录检查返回 {}".format(result)) return result result = self.setSession() if result: @@ -100,7 +99,8 @@ class panelAdmin(panelSetup): if request.method == 'GET': g.menus = public.get_menus() g.yaer = datetime.now().year - session["top_tips"] = public.get_msg_gettext("The current IE browser version is too low to display some features, please use another browser. Or if you use a browser developed by a Chinese company, please switch to Extreme Mode!") + session["top_tips"] = public.get_msg_gettext( + "The current IE browser version is too low to display some features, please use another browser. Or if you use a browser developed by a Chinese company, please switch to Extreme Mode!") session["bt_help"] = public.get_msg_gettext("For Support|Suggestions, please visit the aaPanel Forum") session["download"] = public.get_msg_gettext("Downloading:") if not 'brand' in session: @@ -121,7 +121,7 @@ class panelAdmin(panelSetup): # 检查Web服务器类型 def checkWebType(self): - #if request.method == 'GET': + # if request.method == 'GET': if not 'webserver' in session: if os.path.exists('/usr/local/lsws/bin/lswsctrl'): session['webserver'] = 'openlitespeed' @@ -130,11 +130,12 @@ class panelAdmin(panelSetup): else: session['webserver'] = 'nginx' if not 'webversion' in session: - if os.path.exists(self.setupPath+'/'+session['webserver']+'/version.pl'): - session['webversion'] = public.ReadFile(self.setupPath+'/'+session['webserver']+'/version.pl').strip() + if os.path.exists(self.setupPath + '/' + session['webserver'] + '/version.pl'): + session['webversion'] = public.ReadFile( + self.setupPath + '/' + session['webserver'] + '/version.pl').strip() if not 'phpmyadminDir' in session: - filename = self.setupPath+'/data/phpmyadminDirName.pl' + filename = self.setupPath + '/data/phpmyadminDirName.pl' if os.path.exists(filename): session['phpmyadminDir'] = public.ReadFile(filename).strip() return False @@ -144,15 +145,38 @@ class panelAdmin(panelSetup): if os.path.exists('data/close.pl'): return redirect('/close') + # 跳转到登录页面 + def to_login(self, url, msg=" Login has expired, please log in again"): + x_http_token = request.headers.get('X-Http-Token', '') + if x_http_token: + # 如果是ajax请求 + # res = {"status": False, "msg": msg, "redirect": url} + # 修改为通用返回方式 + res = { + "status": -1, + "timestamp": int(time.time()), + "message": { + # "result": "successfully added!", + "msg": msg, + "redirect": url + } + } + + # public.print_log("有x_http_token res --{}".format(res)) + return Response(json.dumps(res), content_type='application/json') + + # public.print_log("无x_http_token 跳转 --{}".format(url)) + return redirect(url) + # 检查登录 - def check_login(self): + def check_login1(self): try: api_check = True g.api_request = False if not 'login' in session: api_check = self.get_sk() if api_check: - if not isinstance(api_check,dict): + if not isinstance(api_check, dict): if public.get_admin_path() == '/login': return redirect('/login?err=1') return api_check @@ -178,12 +202,12 @@ class panelAdmin(panelSetup): if api_check: now_time = time.time() - session_timeout = session.get('session_timeout',0) + session_timeout = session.get('session_timeout', 0) if session_timeout < now_time and session_timeout != 0: session.clear() return redirect(public.get_admin_path()) - login_token = session.get('login_token','') + login_token = session.get('login_token', '') if login_token: if login_token != public.get_login_token_auth(): session.clear() @@ -201,51 +225,120 @@ class panelAdmin(panelSetup): self.check_session() except: - # public.print_log(public.get_error_info()) + public.print_log(public.get_error_info()) session.clear() public.print_error() + return redirect('/login?id=2') + # 检查登录 + def check_login(self): + try: + api_check = True + g.api_request = False + if not 'login' in session: + api_check = self.get_sk() + if api_check: + # if not isinstance(api_check, dict): + # if public.get_admin_path() == '/login': + # return redirect('/login?err=1') + return api_check + g.api_request = True + else: + if session['login'] == False: + session.clear() + return self.to_login(public.get_admin_path()) + + if 'tmp_login_expire' in session: + s_file = 'data/session/{}'.format(session['tmp_login_id']) + if session['tmp_login_expire'] < time.time(): + session.clear() + if os.path.exists(s_file): os.remove(s_file) + return self.to_login(public.get_admin_path(), 'The temporary login has expired, please log in again') + if not os.path.exists(s_file): + session.clear() + return self.to_login(public.get_admin_path(),'The temporary login has expired, please log in again') + + # 检查客户端hash -- 不要删除 + if not public.check_client_hash(): + session.clear() + return self.to_login(public.get_admin_path(),'Client verification failed, please log in again') + + if api_check: + now_time = time.time() + session_timeout = session.get('session_timeout', 0) + if session_timeout < now_time and session_timeout != 0: + session.clear() + return self.to_login(public.get_admin_path(),"Login session has expired, please log in again") + + login_token = session.get('login_token', '') + if login_token: + if login_token != public.get_login_token_auth(): + session.clear() + return self.to_login(public.get_admin_path(),'Login verification failed, please log in again') + + # if api_check: + # filename = 'data/sess_files/' + public.get_sess_key() + # if not os.path.exists(filename): + # session.clear() + # return redirect(public.get_admin_path()) + + # 标记新的会话过期时间 + self.check_session() + + except: + public.print_log(public.get_error_info()) + session.clear() + public.print_error() + public.print_log("except Login has expired, please log in again") + return self.to_login('/login',' Login has expired, please log in again') + + def check_session(self): white_list = ['/favicon.ico', '/system?action=GetNetWork'] if g.uri in white_list: return session['session_timeout'] = time.time() + public.get_session_timeout() - - # 获取sk def get_sk(self): save_path = '/www/server/panel/config/api.json' if not os.path.exists(save_path): - return public.redirect_to_login() + return public.redirect_to_login(None) + # return self.to_login(public.get_admin_path(), "Login session has expired, please log in again") try: api_config = json.loads(public.ReadFile(save_path)) except: os.remove(save_path) - return public.redirect_to_login() + return public.redirect_to_login(None) + # return self.to_login(public.get_admin_path(), "Login session has expired, please log in again") if not api_config['open']: - return public.redirect_to_login() + return public.redirect_to_login(None) + # return self.to_login(public.get_admin_path(), "Login session has expired, please log in again") from BTPanel import get_input get = get_input() client_ip = public.GetClientIp() + if not 'client_bind_token' in get: if not 'request_token' in get or not 'request_time' in get: - return public.redirect_to_login() + return public.redirect_to_login(None) + # return self.to_login(public.get_admin_path(), "Login session has expired, please log in again") num_key = client_ip + '_api' if not public.get_error_num(num_key, 20): - return public.returnJson(False,'20 consecutive verification failures, prohibited for 1 hour') + return public.returnJson(False, '20 consecutive verification failures, prohibited for 1 hour') - if not public.is_api_limit_ip(api_config['limit_addr'], client_ip): # client_ip in api_config['limit_addr']: + if not public.is_api_limit_ip(api_config['limit_addr'], + client_ip): # client_ip in api_config['limit_addr']: public.set_error_num(num_key) - return public.returnJson(False,'%s[' % public.get_msg_gettext("20 consecutive verification failures, prohibited for 1 hour")+client_ip+']') + return public.returnJson(False, '%s[' % public.get_msg_gettext( + "IP validation failed, your access IP is") + client_ip + ']') else: num_key = client_ip + '_app' - if not public.get_error_num(num_key,20): - return public.returnJson(False,'20 consecutive verification failures, prohibited for 1 hour') + if not public.get_error_num(num_key, 20): + return public.returnJson(False, '20 consecutive verification failures, prohibited for 1 hour') a_file = '/dev/shm/' + get.client_bind_token if not public.path_safe_check(get.client_bind_token): @@ -256,8 +349,8 @@ class panelAdmin(panelSetup): import panelApi if not panelApi.panelApi().get_app_find(get.client_bind_token): public.set_error_num(num_key) - return public.returnJson(False,'Unbound device') - public.writeFile(a_file,'') + return public.returnJson(False, 'Unbound device') + public.writeFile(a_file, '') if not 'key' in api_config: public.set_error_num(num_key) @@ -275,17 +368,23 @@ class panelAdmin(panelSetup): g.aes_key = api_config['key'] request_token = public.md5(get.request_time + api_config['token']) if get.request_token == request_token: - public.set_error_num(num_key,True) + public.set_error_num(num_key, True) return False public.set_error_num(num_key) - return public.returnJson(False,'Secret key verification failed') + return public.returnJson(False, 'Secret key verification failed') # 检查系统配置 def checkConfig(self): if not 'config' in session: session['config'] = public.M('config').where("id=?", ('1',)).field( 'webserver,sites_path,backup_path,status,mysql_root').find() - if not 'email' in session['config']: + + # 4.29 修复config可能是空列表导致赋值不上的问题 + if not session['config']: + session['config'] = {} + + # if not 'email' in session['config']: + if session['config'] and not 'email' in session['config']: session['config']['email'] = public.M( 'users').where("id=?", ('1',)).getField('email') if not 'address' in session: @@ -310,8 +409,7 @@ class panelAdmin(panelSetup): session['server_os'] = tmp return False - - def get_osname(self,i_file): + def get_osname(self, i_file): ''' @name 从指定文件中获取系统名称 @author hwliang<2021-04-07> diff --git a/class/config.py b/class/config.py index 6f38691e..d5c8ead8 100755 --- a/class/config.py +++ b/class/config.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x3 +# | aaPanel x3 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import base64 import public,re,os,nginx,apache,json,time,ols @@ -510,7 +510,8 @@ class config: reg = r"^([\w\-\*]{1,100}\.){1,4}(\w{1,10}|\w{1,10}\.\w{1,10})$" if not re.match(reg, get.domain): return public.return_msg_gettext(False,'Format of primary domain is incorrect') if get.address: - if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", get.address): + from public.regexplib import match_ipv4, match_ipv6 + if not match_ipv4.match(get.address) and not match_ipv6.match(get.address) : return public.return_msg_gettext(False, 'Please set the correct Server IP') oldPort = public.GetHost(True) if not 'port' in get: @@ -1307,7 +1308,7 @@ class config: passwd = get.passwd if g != "files": iprep = r"(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})" - rep_domain = "^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" + rep_domain = r"^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" if not re.search(iprep, ip) and not re.search(rep_domain, ip): if ip != "localhost": return public.returnMsg(False, 'Please enter the correct [domain or IP]!') @@ -2441,7 +2442,7 @@ class config: if not 'status_code' in get: return public.return_msg_gettext(False,'Parameter ERROR!') - if re.match("^\d+$", get.status_code): + if re.match(r"^\d+$", get.status_code): status_code = int(get.status_code) if status_code != 0: if status_code < 100 or status_code > 999: @@ -2878,7 +2879,7 @@ class config: } # request发送post请求并指定form_data参数 res = public.httpPost(url, data) - # public.print_log("获取问卷@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ {}".format(res)) + try: res = json.loads(res) except: @@ -3162,7 +3163,7 @@ class config: "USER_AGENT": public.xsssec(request.headers.get('User-Agent')), # 客户端连接信息 "ERROR_INFO": error, # 错误信息 "PACK_TIME": public.readFile("/www/server/panel/config/update_time.pl") if os.path.exists("/www/server/panel/config/update_time.pl") else public.getDate(), # 打包时间 - "TYPE": 1, + "TYPE": 101, "ERROR_ID": "{}_{}".format(error.split("\n")[0].strip(),get.get("uri", "")) } pkey = public.Md5(error_infos["ERROR_INFO"]) @@ -3170,12 +3171,9 @@ class config: # 提交 if not public.cache_get(pkey): try: - public.run_thread(public.httpPost("https://api.bt.cn/bt_error/index.php", error_infos)) + public.run_thread(public.httpPost("https://geterror.aapanel.com/bt_error/index.php", error_infos)) public.cache_set(pkey, 1, 1800) except Exception as e: pass return public.returnMsg(True, "OK") - - - diff --git a/class/crontab.py b/class/crontab.py index ec0d3e70..24afce80 100644 --- a/class/crontab.py +++ b/class/crontab.py @@ -1,13 +1,13 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import public,db,os,time,re, json -from BTPanel import session,cache +from BTPanel import session, cache class crontab: field = 'id,name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sName,sBody,sType,urladdress' field += ",save_local,notice,notice_channel" @@ -401,6 +401,7 @@ class crontab: id = get['id'] find = public.M('crontab').where("id=?",(id,)).field('name,echo').find() if not find: return public.return_msg_gettext(False, 'The specified task does not exist!') + if not self.remove_for_crond(find['echo']): return public.return_msg_gettext(False,'Unable to write to file, please check if system hardening is enabled!') cronPath = public.GetConfigValue('setup_path') + '/cron' sfile = cronPath + '/' + find['echo'] diff --git a/class/crontab_ssl.py b/class/crontab_ssl.py index 35f9a379..172f13df 100644 --- a/class/crontab_ssl.py +++ b/class/crontab_ssl.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2016 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: <290070744@qq.com> +# Author: <290070744@aapanel.com> # ------------------------------------------------------------------- # ------------------------------ diff --git a/class/data.py b/class/data.py index e57b6533..54eeb092 100644 --- a/class/data.py +++ b/class/data.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http:#bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import sys,os,re,time if not 'class/' in sys.path: @@ -223,7 +223,6 @@ class data: * @return Json page.分页数 , count.总行数 data.取回的数据 ''' def getData(self,get): - import one_key_wp # # net_flow_type = { # # "total_flow": "总流量", # # "7_day_total_flow": "近7天流量", @@ -262,14 +261,12 @@ class data: # # get.order = 'id desc' # # # net_flow_dict["order_type"] = order_type # # public.writeFile(net_flow_json_file, json.dumps(net_flow_dict)) - # 如果网站列表包含 rname 字段排序 先检查表内是否有 rname字段 if hasattr(get, "order") and get.table == 'sites': if get.order.startswith('rname'): data = public.M('sites').find() if 'rname' not in data.keys(): public.M('sites').execute("ALTER TABLE 'sites' ADD 'rname' text DEFAULT ''", ()) - table = get.table data = self.GetSql(get) SQL = public.M(table) @@ -331,6 +328,7 @@ class data: data['data'][i]['attack'] = self.get_analysis(get,data['data'][i]) data['data'][i]['project_type'] = SQL.table('sites').where('id=?',(data['data'][i]['id'])).field('project_type').find()['project_type'] if data['data'][i]['project_type'] == 'WP': + import one_key_wp data['data'][i]['cache_status'] = one_key_wp.one_key_wp().get_cache_status(data['data'][i]['id']) if not data['data'][i]['status'] in ['0','1',0,1]: data['data'][i]['status'] = '1' diff --git a/class/database.py b/class/database.py index c4c950ed..b8b0c1af 100644 --- a/class/database.py +++ b/class/database.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------ @@ -418,7 +418,7 @@ ssl-key=/www/server/mysql/mysql-test/std_data/server-key.pem reg = "ssl-ca=/www.*\n.*\n.*server-key.pem\n" conf = re.sub(reg, "", conf) if os.path.exists('/www/server/mysql/mysql-test/std_data/server-cert.pem'): - conf = re.sub('\[mysqld\]', '[mysqld]\nskip_ssl', conf) + conf = re.sub(r'\[mysqld\]', '[mysqld]\nskip_ssl', conf) public.writeFile(conf_file, conf) return public.return_msg_gettext(True, 'Setup successfully!') # create_ssl = None @@ -429,7 +429,7 @@ ssl-key=/www/server/mysql/mysql-test/std_data/server-key.pem # if create_ssl: # self._create_mysql_ssl() if "ssl-ca" not in conf: - conf = re.sub('\[mysqld\]', '[mysqld]' + ssl_original_path, conf) + conf = re.sub(r'\[mysqld\]', '[mysqld]' + ssl_original_path, conf) conf = re.sub('skip_ssl\n', '', conf) public.writeFile(conf_file, conf) # public.ExecShell('chown mysql.mysql /www/server/data/*.pem') @@ -792,7 +792,7 @@ SetLink password = get['password'].strip() try: if not password: return public.return_msg_gettext(False, 'Root password cannot be empty') - rep = "^[\w@\.\?\-\_\>\<\~\!\#\$\%\^\&\*\(\)]+$" + rep = r"^[\w@\.\?\-\_\>\<\~\!\#\$\%\^\&\*\(\)]+$" if not re.match(rep, password): return public.return_msg_gettext(False, 'Database password cannot contain special characters!') self.sid = get.get('sid/d', 0) @@ -864,7 +864,7 @@ SetLink db_find = public.M('databases').where('id=?', (id,)).find() name = db_find['name'] - rep = "^[\w@\.\?\-\_\>\<\~\!\#\$\%\^\&\*\(\)]+$" + rep = r"^[\w@\.\?\-\_\>\<\~\!\#\$\%\^\&\*\(\)]+$" if not re.match(rep, newpassword): return public.return_msg_gettext(False, 'Database password cannot contain special characters!') # 修改MYSQL @@ -1252,7 +1252,7 @@ SetLink ps = public.get_msg_gettext('Test Database') # XSS filter - if not re.match("^[\w+\.-]+$", value[0]): continue + if not re.match(r"^[\w+\.-]+$", value[0]): continue addTime = time.strftime('%Y-%m-%d %X', time.localtime()) @@ -1317,9 +1317,9 @@ SetLink public.CheckMyCnf() myfile = '/etc/my.cnf' mycnf = public.readFile(myfile) - rep = "datadir\s*=\s*(.+)\n" + rep = "datadir\\s*=\\s*(.+)\n" data['datadir'] = re.search(rep, mycnf).groups()[0] - rep = "port\s*=\s*([0-9]+)\s*\n" + rep = "port\\s*=\\s*([0-9]+)\\s*\n" data['port'] = re.search(rep, mycnf).groups()[0] except: data['datadir'] = '/www/server/data' @@ -1338,7 +1338,7 @@ SetLink 'The same as the current storage directory, file cannot be moved!') public.ExecShell('/etc/init.d/mysqld stop') - public.ExecShell('\cp -arf ' + mysqlInfo['datadir'] + '/* ' + get.datadir + '/') + public.ExecShell(r'\cp -arf ' + mysqlInfo['datadir'] + '/* ' + get.datadir + '/') public.ExecShell('chown -R mysql.mysql ' + get.datadir) public.ExecShell('chmod -R 755 ' + get.datadir) public.ExecShell('rm -f ' + get.datadir + '/*.pid') @@ -1365,7 +1365,7 @@ SetLink def SetMySQLPort(self, get): myfile = '/etc/my.cnf' mycnf = public.readFile(myfile) - rep = r"port\s*=\s*([0-9]+)\s*\n" + rep = "port\\s*=\\s*([0-9]+)\\s*\n" mycnf = re.sub(rep, 'port = ' + get.port + '\n', mycnf) public.writeFile(myfile, mycnf) public.ExecShell('/etc/init.d/mysqld restart') @@ -1429,19 +1429,19 @@ SetLink mysql_cnf = re.sub(r"\nlog-bin", "\n#log-bin", mysql_cnf) mysql_cnf = re.sub(r"\nbinlog_format", "\n#binlog_format", mysql_cnf) if not is_off_bin_log: - if re.search("\n#\s*skip-log-bin", mysql_cnf): - mysql_cnf = re.sub("\n#\s*skip-log-bin", "\nskip-log-bin", mysql_cnf) + if re.search("\n#\\s*skip-log-bin", mysql_cnf): + mysql_cnf = re.sub("\n#\\s*skip-log-bin", "\nskip-log-bin", mysql_cnf) else: - mysql_cnf = re.sub("\n#\s*log-bin", "\nskip-log-bin\n#log-bin", mysql_cnf) + mysql_cnf = re.sub("\n#\\s*log-bin", "\nskip-log-bin\n#log-bin", mysql_cnf) # public.ExecShell("rm -f {}/mysql-bin.*".format(mysql_data_dir)) else: # 开启 binlog 日志 - if re.search("\n#\s*log-bin", mysql_cnf): - mysql_cnf = re.sub("\n#\s*log-bin", "\nlog-bin", mysql_cnf) + if re.search("\n#\\s*log-bin", mysql_cnf): + mysql_cnf = re.sub("\n#\\s*log-bin", "\nlog-bin", mysql_cnf) else: mysql_cnf = re.sub("[mysqld]", "[mysqld]\nlog-bin=mysql-bin", mysql_cnf) - if re.search("\n#\s*binlog_format", mysql_cnf): - mysql_cnf = re.sub(r"\n#\s*binlog_format", "\nbinlog_format", mysql_cnf) + if re.search("\n#\\s*binlog_format", mysql_cnf): + mysql_cnf = re.sub("\n#\\s*binlog_format", "\nbinlog_format", mysql_cnf) else: mysql_cnf = re.sub("[mysqld]", "[mysqld]\nbinlog_format=mixed", mysql_cnf) diff --git a/class/databaseModel/base.py b/class/databaseModel/base.py index 2afe6118..d48fbd16 100644 --- a/class/databaseModel/base.py +++ b/class/databaseModel/base.py @@ -178,7 +178,7 @@ class databaseBase: if data_name in checks or len(data_name) < 1: return public.returnMsg(False,'Database name is invalid!'); - reg = "^\w+$" + reg = r"^\w+$" if not re.match(reg, data_name): return public.returnMsg(False,'DATABASE_NAME_ERR_T') diff --git a/class/databaseModel/mongodbModel.py b/class/databaseModel/mongodbModel.py index 0280c750..6e1e4171 100644 --- a/class/databaseModel/mongodbModel.py +++ b/class/databaseModel/mongodbModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #角色说明: #read:允许用户读取指定数据库 @@ -105,7 +105,7 @@ class panelMongoDB(): conf = self.get_config(None) for opt in options: - tmp = re.findall(opt + ":\s+(.+)",conf) + tmp = re.findall(opt + r":\s+(.+)",conf) if not tmp: continue; data[opt] = tmp[0] @@ -177,9 +177,9 @@ class main(databaseBase): conf = public.readFile(self.__conf_path) if status: - conf = re.sub('authorization\s*\:\s*disabled','authorization: enabled',conf) + conf = re.sub(r'authorization\s*\:\s*disabled','authorization: enabled',conf) else: - conf = re.sub('authorization\s*\:\s*enabled','authorization: disabled',conf) + conf = re.sub(r'authorization\s*\:\s*enabled','authorization: disabled',conf) public.writeFile(self.__conf_path,conf) self.restart_services() @@ -201,7 +201,7 @@ class main(databaseBase): if status: if hasattr(get,'password'): password = get['password'].strip() - if not password or not re.search("^[\w@\.]+$", password): + if not password or not re.search(r"^[\w@\.]+$", password): return public.return_msg_gettext(False, 'Database password cannot be empty or have special characters!') # if re.search('[\u4e00-\u9fa5]',password): @@ -670,7 +670,7 @@ class main(databaseBase): try: if not newpassword: return public.returnMsg(False, 'Modify the failure,The database[' + username + ']password cannot be empty.'); - if len(re.search("^[\w@\.]+$", newpassword).groups()) > 0: + if len(re.search(r"^[\w@\.]+$", newpassword).groups()) > 0: return public.returnMsg(False, 'The database password cannot be empty or contain special characters') if re.search('[\u4e00-\u9fa5]',newpassword): diff --git a/class/databaseModel/pgsqlModel.py b/class/databaseModel/pgsqlModel.py index 259c5584..f983128e 100644 --- a/class/databaseModel/pgsqlModel.py +++ b/class/databaseModel/pgsqlModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hezhihong +# Author: hezhihong #------------------------------------------------------------------- #------------------------------ @@ -154,7 +154,7 @@ class main(databaseBase,panelPgsql): if not self.__soft_path:self.__soft_path='{}/pgsql'.format(public.get_setup_path()) conf = public.readFile('{}/data/postgresql.conf'.format(self.__soft_path)) for opt in options: - tmp = re.findall("\s+" +opt + "\s*=\s*(.+)#",conf) + tmp = re.findall(r"\s+" +opt + r"\s*=\s*(.+)#",conf) if not tmp: continue; data[opt] = tmp[0].strip() if opt == 'listen_addresses': @@ -223,7 +223,8 @@ class main(databaseBase,panelPgsql): @return list ''' check_result = os.system('/www/server/pgsql/bin/psql --version') - if check_result !=0 and not public.M('database_servers').where('db_type=?','pgsql').count():return [] + if check_result !=0 and not public.M('database_servers').where('db_type=?','pgsql').count(): + return [] return self.GetBaseCloudServer(args) @@ -302,7 +303,7 @@ class main(databaseBase,panelPgsql): id = get['id'] find = public.M('databases').where("id=?",(id,)).field('id,pid,name,username,password,accept,ps,addtime,db_type,conn_config,sid,type').find(); if not find: return public.returnMsg(False,'The specified database does not exist.') - + name = get['name'] username = find['username'] @@ -323,15 +324,15 @@ class main(databaseBase,panelPgsql): find = public.M('databases').where("id=?",(id,)).find() if not find: return public.returnMsg(False,'Database does not exist!') - + if not find['password'].strip(): return public.returnMsg(False,'The database password is empty. Set the password first.') - + sql_dump = '{}/bin/pg_dump'.format(self.__soft_path) # return sql_dump if not os.path.isfile(sql_dump): return public.returnMsg(False,'Lack of backup tools, please first through the software store PGSQL manager!') - + back_path = session['config']['backup_path'] + '/database/pgsql/' # return back_path if not os.path.exists(back_path): os.makedirs(back_path) @@ -358,7 +359,7 @@ class main(databaseBase,panelPgsql): if os.path.getsize(backupName) < 2048: return public.returnMsg(True, 'The backup file size is smaller than 2Kb. Check the backup integrity.') - else: + else: return public.returnMsg(True, 'BACKUP_SUCCESS') def DelBackup(self,args): @@ -397,7 +398,7 @@ class main(databaseBase,panelPgsql): ext = tmp[len(tmp) -1] if ext not in exts: return public.returnMsg(False, 'DATABASE_INPUT_ERR_FORMAT') - + sql_dump = '{}/bin/psql'.format(self.__soft_path) if not os.path.exists(sql_dump): return public.returnMsg(False,'Lack of recovery tool, please use software management to install PGSQL!') diff --git a/class/databaseModel/redisModel.py b/class/databaseModel/redisModel.py index b7fd8fb9..64a9fef7 100644 --- a/class/databaseModel/redisModel.py +++ b/class/databaseModel/redisModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- # sqlite模型 @@ -76,7 +76,7 @@ class panelRedisDB(): keys = ["bind","port","timeout","maxclients","databases","requirepass","maxmemory"] for k in keys: v = "" - rep = "\n%s\s+(.+)" % k + rep = "\n%s\\s+(.+)" % k group = re.search(rep,redis_conf) if not group: if k == "maxmemory": @@ -102,6 +102,7 @@ class main(databaseBase): pass + def GetCloudServer(self,args): ''' @name 获取远程服务器列表 @@ -109,6 +110,7 @@ class main(databaseBase): @return list ''' return self.GetBaseCloudServer(args) + # return public.return_message(0, 0, self.GetBaseCloudServer(args)) def AddCloudServer(self,args): @@ -373,7 +375,8 @@ class main(databaseBase): nlist = [] cloud_list = {} - for x in self.GetCloudServer({'type':'redis'}): cloud_list['id-' + str(x['id'])] = x + for x in self.GetCloudServer({'type':'redis'}): + cloud_list['id-' + str(x['id'])] = x path = session['config']['backup_path'] + '/database/redis/' if not os.path.exists(path): os.makedirs(path) diff --git a/class/databaseModel/sqliteModel.py b/class/databaseModel/sqliteModel.py index bfd3191d..00ee1955 100644 --- a/class/databaseModel/sqliteModel.py +++ b/class/databaseModel/sqliteModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ diff --git a/class/databaseModel/sqlserverModel.py b/class/databaseModel/sqlserverModel.py index 4e5b05ad..b3054d62 100644 --- a/class/databaseModel/sqlserverModel.py +++ b/class/databaseModel/sqlserverModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -87,6 +87,8 @@ class main(databaseBase): ''' @添加远程数据库 ''' + + return self.AddBaseCloudServer(args) def RemoveCloudServer(self,args): @@ -107,17 +109,18 @@ class main(databaseBase): """ res = self.add_base_database(args) - if not res['status']: return res + if not res['status']: + return res data_name = res['data_name'] username = res['username'] password = res['data_pwd'] - if re.match("^\d+",data_name): + if re.match(r"^\d+",data_name): return public.returnMsg(False,'SQLServer databases cannot start with numbers!') reg_count = 0 - regs = ['[a-z]','[A-Z]','\W','[0-9]'] + regs = ['[a-z]','[A-Z]',r'\W','[0-9]'] for x in regs: if re.search(x,password): reg_count += 1 @@ -361,7 +364,7 @@ class main(databaseBase): try: if not newpassword: return public.returnMsg(False, 'The password of database [' + username + '] cannot be empty.'); - if len(re.search("^[\w@\.]+$", newpassword).groups()) > 0: + if len(re.search(r"^[\w@\.]+$", newpassword).groups()) > 0: return public.returnMsg(False, 'The database password cannot be empty or contain special characters') except : return public.returnMsg(False, 'The database password cannot be empty or contain special characters') @@ -402,7 +405,7 @@ class main(databaseBase): try: if not password: return public.returnMsg(False, 'The password of database [' + username + '] cannot be empty.') - if len(re.search("^[\w@\.]+$", password).groups()) > 0: + if len(re.search(r"^[\w@\.]+$", password).groups()) > 0: return public.returnMsg(False, 'saThe password cannot be empty or have special symbols') except : return public.returnMsg(False, 'saThe password cannot be empty or have special symbols') diff --git a/class/datatool.py b/class/datatool.py index d67383f5..3f99f81b 100644 --- a/class/datatool.py +++ b/class/datatool.py @@ -1,8 +1,8 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: 1249648969@qq.com # ------------------------------------------------------------------- diff --git a/class/db.py b/class/db.py index f1fc0ace..3efc9529 100644 --- a/class/db.py +++ b/class/db.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import sqlite3 @@ -390,3 +390,7 @@ class Sql(): except: pass + def db(self,name): + # 设置数据库名称,用于判断数据库文件 + self.__DB_NAME = name + return self diff --git a/class/db_mysql.py b/class/db_mysql.py index 5531ecd0..5fbc2c05 100644 --- a/class/db_mysql.py +++ b/class/db_mysql.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import re,os,sys,public,json diff --git a/class/downloadFile.py b/class/downloadFile.py index 28b8cdd3..b8e5da7c 100644 --- a/class/downloadFile.py +++ b/class/downloadFile.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import os,sys,public,json,time class downloadFile: diff --git a/class/file_execute_deny.py b/class/file_execute_deny.py index 50aa8a18..677f0b56 100644 --- a/class/file_execute_deny.py +++ b/class/file_execute_deny.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2020 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zhwen +# Author: zhwen #------------------------------------------------------------------- #------------------------------ @@ -24,7 +24,7 @@ class FileExecuteDeny: def get_file_deny(self,args): ''' # 添加某个网站禁止运行PHP - author: zhwen + author: zhwen :param args: website 网站名 str :return: ''' @@ -51,7 +51,7 @@ class FileExecuteDeny: deny_name.append(tmp[-1]) result = [] for i in deny_name: - reg = '#BEGIN_DENY_{}\n\s*location\s*\~\*\s*\^(.*)\.\*.*\((.*)\)\$'.format(i.replace("|","\|")) + reg = '#BEGIN_DENY_{}\n\\s*location\\s*\\~\\*\\s*\\^(.*)\\.\\*.*\\((.*)\\)\\$'.format(i.replace("|",r"\|")) re_tmp = re.search(reg,conf) if re_tmp: deny_directory = re_tmp.groups()[0] @@ -73,7 +73,7 @@ class FileExecuteDeny: deny_name.append(tmp[-1]) result = [] for i in deny_name: - reg = '#BEGIN_DENY_{}\n\s* + author: zhwen :param args: website 网站名 str :param args: deny_name 规则名称 str :param args: suffix 禁止访问的后续名 str @@ -145,7 +145,7 @@ class FileExecuteDeny: if not conf: return False if not dir and not suffix: - reg = '\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\n'.format(n=name) + reg = '\\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\n'.format(n=name) conf = re.sub(reg,'',conf) else: if dir[0] != '/':dir = '/'+dir @@ -168,12 +168,12 @@ class FileExecuteDeny: if not conf: return False if not dir and not suffix: - reg = '\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}'.format(n=name) + reg = '\\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}'.format(n=name) conf = re.sub(reg,'',conf) else: if dir[0] != '/':dir = '/'+dir if dir[-1] != '/':dir = dir+'/' - new = ''' + new = r''' #BEGIN_DENY_{n} Order allow,deny @@ -183,7 +183,7 @@ class FileExecuteDeny: '''.format(n=name,d=dir,s=suffix) if '#BEGIN_DENY_{}'.format(name) in conf: return True - conf = re.sub('#DENY\s*FILES',new+'\n #DENY FILES',conf) + conf = re.sub(r'#DENY\s*FILES',new+'\n #DENY FILES',conf) public.writeFile(self.ap_website_conf,conf) return True @@ -192,17 +192,17 @@ class FileExecuteDeny: if not conf: return False if not dir and not suffix: - reg = '#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\s*'.format(n=name) + reg = '#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\\s*'.format(n=name) conf = re.sub(reg,'',conf) else: - new = ''' + new = r''' #BEGIN_DENY_{n} rules RewriteRule ^{d}.*\.({s})$ - [F,L] #END_DENY_{n} '''.format(n=name,d=dir,s=suffix) if '#BEGIN_DENY_{}'.format(name) in conf: return True - conf = re.sub('autoLoadHtaccess\s*1','autoLoadHtaccess 1'+new,conf) + conf = re.sub(r'autoLoadHtaccess\s*1','autoLoadHtaccess 1'+new,conf) public.writeFile(self.ols_website_conf,conf) return True @@ -210,7 +210,7 @@ class FileExecuteDeny: def del_file_deny(self,args): ''' # 添加某个网站禁止运行PHP - author: zhwen + author: zhwen :param args: website 网站名 str :param args: deny_name 规则名称 str :return: diff --git a/class/files.py b/class/files.py index a29344d3..9a1e914c 100644 --- a/class/files.py +++ b/class/files.py @@ -1,11 +1,11 @@ #!/usr/bin/env python #coding:utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- from base64 import b64encode import sys @@ -124,7 +124,7 @@ session.save_handler = files'''.format(path, sess_path, sess_path) filename = public.get_vhost_path() + '/nginx/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) - rep = '\s*root\s+(.+);' + rep = r'\s*root\s+(.+);' tmp1 = re.search(rep, conf) if tmp1: path = tmp1.groups()[0] @@ -132,7 +132,7 @@ session.save_handler = files'''.format(path, sess_path, sess_path) filename = public.get_vhost_path() + '/apache/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) - rep = '\s*DocumentRoot\s*"(.+)"\s*\n' + rep = '\\s*DocumentRoot\\s*"(.+)"\\s*\n' tmp1 = re.search(rep, conf) if tmp1: path = tmp1.groups()[0] @@ -398,9 +398,86 @@ session.save_handler = files'''.format(path, sess_path, sess_path) for m in ms.keys(): filename = filename.replace(m,ms[m]) return filename + def files_list(self, path, search=None, my_sort='off', reverse=False): + ''' + @name 遍历目录,并获取全量文件信息列表 + @param path 目录路径 + @param search 搜索关键词 + @param my_sort 排序字段 + @param reverse 是否降序 + @return tuple (int,list) + ''' + + nlist = [] + count = 0 + + # 文件不存在 + if not os.path.exists(path): + return count, nlist + + sort_key = -1 + if my_sort == 'off': # 不排序 + sort_key = -1 + elif my_sort == 'name': # 按文件名排序 + sort_key = 0 + elif my_sort == 'size': # 按文件大小排序 + sort_key = 1 + elif my_sort == 'mtime': # 按修改时间排序 + sort_key = 2 + elif my_sort == 'accept': # 按文件权限排序 + sort_key = 3 + elif my_sort == 'user': # 按文件所有者排序 + sort_key = 4 + + with os.scandir(path) as it: + try: + for entry in it: + # 是否搜索 + if search: + if entry.name.lower().find(search) == -1: + continue + + # 是否需要获取文件信息 + sort_val = 0 + if sort_key == 0 or sort_key == -1: + # 通过文件名或不排序时,不获取文件信息 + sort_val = 0 + else: + try: + fstat = entry.stat() + if sort_key == 1: + sort_val = fstat.st_size + elif sort_key == 2: + sort_val = fstat.st_mtime + elif sort_key == 3: + sort_val = fstat.st_mode + elif sort_key == 4: + sort_val = fstat.st_uid + except: + pass + + nlist.append((entry.name, sort_val)) + + # 计数 + count += 1 + except: + pass + + if sort_key == 0: + # 按文件名排序 + nlist = sorted(nlist, key=lambda x: x[0], reverse=reverse) + elif sort_key > 0: + # 按指定字段排序 + nlist = sorted(nlist, key=lambda x: x[1], reverse=reverse) + else: + # 否则文件数量小于10000时,按文件名排序 + if count < 10000: + nlist = sorted(nlist, key=lambda x: x[0], reverse=reverse) + + return count, nlist # 取文件/目录列表 - def GetDir(self, get): + def GetDir(self, get: public.dict_obj): if not hasattr(get, 'path'): # return public.returnMsg(False,'错误的参数!') get.path = public.get_site_path() #'/www/wwwroot' @@ -425,7 +502,6 @@ session.save_handler = files'''.format(path, sess_path, sess_path) if not os.path.isdir(get.path): return public.return_msg_gettext(False,'This is not a directory') - import pwd dirnames = [] filenames = [] @@ -441,7 +517,16 @@ session.save_handler = files'''.format(path, sess_path, sess_path) # 实例化分页类 page = page.Page() info = {} - info['count'] = self.GetFilesCount(get.path, search) + + if not hasattr(get, 'reverse'): get.reverse = 'False' + if not hasattr(get, 'sort'): get.sort = 'off' + reverse = bool(get.reverse) + if get.reverse == 'False': + reverse = False + + info['count'], _nlist = self.files_list(get.path, search, my_sort=get.sort, reverse=reverse) + # 改1 + # info['count'] = self.GetFilesCount(get.path, search) info['row'] = 500 if 'disk' in get: if get.disk == 'true': info['row'] = 2000 @@ -472,13 +557,18 @@ session.save_handler = files'''.format(path, sess_path, sess_path) data['STORE'] = self.get_files_store(None) data['FILE_RECYCLE'] = os.path.exists('data/recycle_bin.pl') - if not hasattr(get, 'reverse'): get.reverse = 'False' - if not hasattr(get, 'sort'): get.sort = 'name' - reverse = bool(get.reverse) - if get.reverse == 'False': - reverse = False - for file_info in self.__list_dir(get.path, get.sort, reverse): - filename = os.path.join(get.path, file_info[0]) + # if info['count'] >= 200 and not os.path.exists('data/max_files_sort.pl'): + # get.reverse = 'False' + # reverse = False + # get.sort = '' + + # _nlist = self.__default_list_dir(get.path,page.SHIFT,page.ROW) + # data['SORT'] = 0 + # else: + # _nlist = self.__list_dir(get.path, get.sort, reverse) + + for file_info in _nlist: + if search: if file_info[0].lower().find(search) == -1: continue @@ -487,19 +577,25 @@ session.save_handler = files'''.format(path, sess_path, sess_path) break if i < page.SHIFT: continue - if not os.path.exists(filename) and not os.path.islink(filename): continue - file_info = self.__format_stat(filename, get.path) - if not file_info: continue - favorite = self.__check_favorite(filename, data['STORE']) - r_file = self.__filename_flater(file_info['name']) + ';' + str(file_info['size']) + ';' + str(file_info['mtime']) + ';' + str( - file_info['accept']) + ';' + file_info['user'] + ';' + file_info['link']+';'\ - + self.get_download_id(filename) + ';' + self.is_composer_json(filename)+';'\ - + favorite+';'+self.__check_share(filename) - if os.path.isdir(filename): - dirnames.append(r_file) - else: - filenames.append(r_file) - n += 1 + + try: + fname = file_info[0].encode('unicode_escape').decode("unicode_escape") + filename = os.path.join(get.path, fname) + if not os.path.exists(filename) and not os.path.islink(filename): continue + file_info = self.__format_stat(filename, get.path) + if not file_info: continue + favorite = self.__check_favorite(filename, data['STORE']) + r_file = self.__filename_flater(file_info['name']) + ';' + str(file_info['size']) + ';' + str(file_info['mtime']) + ';' + str( + file_info['accept']) + ';' + file_info['user'] + ';' + file_info['link']+';'\ + + self.get_download_id(filename) + ';' + self.is_composer_json(filename)+';'\ + + favorite+';'+self.__check_share(filename) + if os.path.isdir(filename): + dirnames.append(r_file) + else: + filenames.append(r_file) + n += 1 + except: + continue data['DIR'] = dirnames data['FILES'] = filenames @@ -536,8 +632,77 @@ session.save_handler = files'''.format(path, sess_path, sess_path) data['search_history'] = public.get_search_history('files','get_list') public.set_dir_history('files','GetDirList',data['PATH']) + # 2023-3-6,增加融入企业级防篡改 + data = self._check_tamper(data) + data = self._get_bt_sync_status_old(data) return data + # ——————————————————— + # 融合企业级防篡改 | + # ——————————————————— + + # 防篡改:获取文件是否在保护列表中 + + def _check_tamper(self, data): + try: + import PluginLoader + except: + return {} + args = public.dict_obj() + args.client_ip = public.GetClientIp() + args.fun = "check_dir_safe" + args.s = "check_dir_safe" + args.file_data = { + "base_path": data['PATH'], + "dirs": [i.split(";", 1)[0] for i in data["DIR"]], + "files": [i.split(";", 1)[0] for i in data["FILES"]] + } + data["tamper_data"] = PluginLoader.plugin_run("tamper_core", "check_dir_safe", args) + + return data + + + + # 获取文件同步状态 + @staticmethod + def _get_bt_sync_status_old(data): + config_file = "{}/plugin/rsync/config4.json".format(public.get_panel_path()) + if not os.path.exists(config_file): + data["bt_sync"] = {} + return data + try: + conf = json.loads(public.readFile(config_file)) + except json.JSONDecodeError: + data["bt_sync"] = {} + return data + + dirs = [data['PATH'] + "/" + i.split(";", 1)[0] for i in data["DIR"]] + res = [{} for _ in range(len(dirs))] + for idx, d in enumerate(dirs): + for value in conf.get("modules", []): + if value.get("path", "").rstrip("/") == d: + res[idx] = { + "type": "modules", + "name": value.get("name", ""), + "status": value.get("recv_status", True), + "path": d, + } + + for idx, d in enumerate(dirs): + for value in conf.get("senders", []): + if value.get("source", "").rstrip("/") == d: + target = value.get("target_list", [{}])[0] + res[idx] = { + "type": "senders", + "name": target.get("name", ""), + "status": target.get("status", True), + "path": d, + } + + data["bt_sync"] = res + return data + + def get_file_ps(self,filename): ''' @@ -548,13 +713,17 @@ session.save_handler = files'''.format(path, sess_path, sess_path) ''' ps_path = public.get_panel_path() + '/data/files_ps' - f_key1 = '/'.join((ps_path,public.md5(filename))) - if os.path.exists(f_key1): - return public.readFile(f_key1) + try: + f_key1 = '/'.join((ps_path,public.md5(filename))) - f_key2 = '/'.join((ps_path,public.md5(os.path.basename(filename)))) - if os.path.exists(f_key2): - return public.readFile(f_key2) + if os.path.exists(f_key1): + return public.readFile(f_key1) + + f_key2 = '/'.join((ps_path,public.md5(os.path.basename(filename)))) + if os.path.exists(f_key2): + return public.readFile(f_key2) + except: + pass pss = { '/www/server/data':'MySQL data storage directory!', @@ -600,10 +769,17 @@ session.save_handler = files'''.format(path, sess_path, sess_path) '/usr/local/sbin': 'System script directory', '/usr/local/bin': 'System script directory' } - if filename in pss: return "PS:" + pss[filename] - if not self.recycle_list: self.recycle_list = public.get_recycle_bin_list() - if filename + '/' in self.recycle_list: return 'PS: Recycle Bin Directory' + if str(filename).endswith(".bt_split_json"): + return "PS: Split the recovery profile" + if str(filename).endswith(".bt_split"): + return "PS: Split unit file" + if filename in pss: return "PS:" + pss[filename] + try: + if not self.recycle_list: self.recycle_list = public.get_recycle_bin_list() + except: + pass + if filename + '/' in self.recycle_list:'PS: Recycle Bin Directory' if filename in self.recycle_list: return 'PS: Recycle Bin Directory' return '' @@ -930,8 +1106,7 @@ session.save_handler = files'''.format(path, sess_path, sess_path) public.WriteLog('TYPE_FILE', 'Successfully deleted directory [{}]!', (get.path,)) self.remove_file_ps(get) return public.return_msg_gettext(True, ' Successfully deleted directory!') - except Exception as e: - public.print_log("DeleteDir error info :{}".format(e)) + except: return public.return_msg_gettext(False, 'Failed to delete directory!') # 删除 空目录 @@ -1256,9 +1431,9 @@ session.save_handler = files'''.format(path, sess_path, sess_path) public.write_log_gettext('File manager','[{}] renamed to [{}]',(get.sfile,get.dfile)) return public.return_msg_gettext(True,'Successfully renamed!') else: - public.write_log_gettext('File manager', 'Database moved!', + public.write_log_gettext('File manager', 'File moved!', (get.sfile, get.dfile)) - return public.return_msg_gettext(True, 'Database moved!') + return public.return_msg_gettext(True, 'File moved!') except: return public.return_msg_gettext(False, 'Failed to move file!') @@ -1795,11 +1970,11 @@ session.save_handler = files'''.format(path, sess_path, sess_path) return self.GetDirSize(get) # 批量操作 - def SetBatchData(self, get): + def SetBatchData(self, get: public.dict_obj): if sys.version_info[0] == 2: get.path = get.path.encode('utf-8') if get.type == '1' or get.type == '2': - session['selected'] = get + session['selected'] = get.get_items() return public.return_msg_gettext(True, 'Successfully marked, please click Paste All button in the target directory!') elif get.type == '3': for key in json.loads(get.data): @@ -2428,7 +2603,7 @@ cd %s pdata['password'] = get.password if len(pdata['password']) < 4 and len(pdata['password']) > 0: return public.return_msg_gettext(False,'The length of the extracted password cannot be less than 4 digits') - if not re.match('^\w+$',pdata['password']): + if not re.match(r'^\w+$',pdata['password']): return public.return_msg_gettext(False,'The password only supports a combination of uppercase and lowercase letters and numbers') if 'ps' in get: pdata['ps'] = get.ps @@ -2455,7 +2630,7 @@ cd %s pdata['token'] += "." + exts[-1] if len(pdata['password']) < 4 and len(pdata['password']) > 0: return public.return_msg_gettext(False,' Please do not enter the following special characters [ ~ ` / = ]') - if not re.match('^\w+$',pdata['password']) and pdata['password']: + if not re.match(r'^\w+$',pdata['password']) and pdata['password']: return public.return_msg_gettext(False,'The password only supports a combination of uppercase and lowercase letters and numbers') #更新 or 插入 token = public.M(my_table).where('filename=?',(get.filename,)).getField('token') @@ -3254,7 +3429,7 @@ CREATE TABLE index_tb( def restore_website(self,args): """ @name 恢复站点文件 - @author zhwen + @author zhwen @parma file_name 备份得文件名 @parma site_id 网站id """ @@ -3265,8 +3440,8 @@ CREATE TABLE index_tb( def get_progress(self,args): """ @name 获取进度日志 - @author zhwen + @author zhwen """ import panel_restore pr=panel_restore.panel_restore() - return pr.get_progress(args) \ No newline at end of file + return pr.get_progress(args) diff --git a/class/filesModel/downModel.py b/class/filesModel/downModel.py index b875fe0c..7f6c06e9 100644 --- a/class/filesModel/downModel.py +++ b/class/filesModel/downModel.py @@ -1,46 +1,46 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# 上传文件至oss -#------------------------------ -from filesModel.base import filesBase -import public - -class main(filesBase): - - - def __init__(self): - pass - - - def get_oss_objects(self,get): - """ - @name 获取可上传的对象存储 - """ - return self.get_all_objects(get) - - - def download_file(self,get): - """ - @name 下载文件 - @param get - file:文件路径 - """ - - info = self.get_soft_find(get.name) - if not info['setup']: - return public.returnMsg(False,'未安装[{}]插件'.format(info['title'])) - - import panelTask - task_obj = panelTask.bt_task() - task_obj.create_task('下载文件', 1, get.url, get.path + '/' + get.filename) - public.set_module_logs('files_down_to_file', 'download_file', 1) - public.WriteLog('TYPE_FILE', '从 [{}] 下载文件 [{}] 到 {}'.format(info['title'],get.filename,get.path)) - return public.returnMsg(True, 'FILE_DOANLOAD') - +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +kEPo6lgxLBQavG1DxRrlnEK5KohxBShsQIeNCGHkEP4= +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +XIfdJ79nObMM+vyAmKbTmw== +1u+XjG/2+GSQRv6EzCaWRQ== +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +9bJD7OGVYIbkQwcqio3pMkEQ5Tepk1KAKJBdik81BOHNSVFwrMdl2gFGwZvvpYDQ +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P5ZvVL4YjAgUVrqKtQwmJSm1F1KbG0/rNY/6xTjx3EA3 +Z8UsPk1Q7HtwjRd4g01ryw== +CAhn8dRUItEbEErp4w+lX9Z2hX2+feDN0U2wxQk1jf6XkRmrNeGBO3gU/TXqpTeF +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +vHI6ym+jnn06PgL99476zxO+uTTrKbsFCbogJPwJML2TG1a7NmZkzv05uK3L/Tc8 +Z8UsPk1Q7HtwjRd4g01ryw== +n3tXewq2IYqiwFREjJjsxZJ3F7EUu5p3TiQaoXfFThA= +ho/Q+jrWDtBeg9J9ZTnKi3+/Efbvadpf+qoH75NuGwk= +PRG/YRzXVD8iVR3bzEcUnsWMvR86r5liyWslQeT39Bs= +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +AN33R+yjfqqAkAsFnmP/r/WGRgGkDEv8Fnc8kjQsJ7fRnV1BKZAu/huNYnIc6I01 +cbvASHjpysrsjdY5RctXmG9wund6qoaXAfnCgO+o1nY= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXHKWKUVVa/rOrR3+fgEd7tp63QHMzA34qukyTQgDFJrLVYVJL/Ye3kNmAIiExi//aTWiBsWXWTJE2/bLQwR3ie +1u+XjG/2+GSQRv6EzCaWRQ== +q4nc/jwATOMUyfSjLibfEelnou8pLYpfebj3/LvnkGg= +7fBvPEAf72Uv1YEDXbbQ3CN3MWrD+3vkr2VJCCzzEPRhwccGf00/aNA1BGgdy8WH +7fBvPEAf72Uv1YEDXbbQ3JX8fOX6IsaFNTXjLCv8fl9nBsjACyGde1V2jqcLtcHMgHmQHXLhOkH7ECCy4Qan/Eap/d5B8ETngKGRLc1dnrFJxLrR59XTXqj5a3KAPh6f +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs8XJaZJ50IVd+4cfwZkMesXHuMlT+97TT2qjLWtQKOwIMYvrQOoIlZ+noM34BKXXbE= +2+RceSNjD2iq0sRzepMyse3K/6KDWrRQWf3bA9DlS0ntu9Qw70LcA2SU6TO7qbj7+BZ00tLn7oRDAJhOp7GBQJ9AdWUuTKC0qj4H0sUep4AFfY3QegcrH7wS+cAOaYcaYNB7YC/AjQj4DFsev5mqOa3aXW4VcIMS3jP/SRer5vo= +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOHo1JRV4mvyfjFAJfZA0Y7fzjR8HN7n40vj/TKQkGxagQ== +1u+XjG/2+GSQRv6EzCaWRQ== diff --git a/class/filesModel/gzModel.py b/class/filesModel/gzModel.py index 6cb7cd7e..fb0b9a83 100644 --- a/class/filesModel/gzModel.py +++ b/class/filesModel/gzModel.py @@ -1,342 +1,342 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# -#------------------------------ - -import os,sys,re -from filesModel.base import filesBase -import public,json -import tarfile,shutil,gzip - -class main(filesBase): - - def __init__(self): - pass - - - def __check_zipfile(self,sfile,is_close = False): - ''' - @name 检查文件是否为zip文件 - @param sfile 文件路径 - @return bool - ''' - - pass - - def get_zip_files(self,args): - ''' - @name 获取压缩包内文件列表 - @param args['path'] 压缩包路径 - @return list - ''' - sfile = args.sfile - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - if not tarfile.is_tarfile(sfile): - return public.returnMsg(False,'Not a valid tar.gz archive file') - - zip_file = tarfile.open(sfile) - data = {} - for item in zip_file.getmembers(): - - sub_data = data - f_name = self.__get_zip_filename(item) - - f_dirs = f_name.split('/') - for d in f_dirs: - if not d: continue - if not d in sub_data: - if d == f_name[-len(d):]: - - sub_data[d] = { - 'file_size': item.size, - 'filename':d, - 'fullpath':f_name, - 'date_time': public.format_date(times=item.mtime), - 'is_dir': 0 - } - if item.isdir(): - sub_data[d]['is_dir'] = 1 - else: - sub_data[d] = {} - sub_data = sub_data[d] - - return data - - - def get_fileinfo_by(self,args): - ''' - @name 获取压缩包内文件信息 - @param args['path'] 压缩包路径 - @param args['filename'] 文件名 - @return dict - ''' - - sfile = args.sfile - filename = args.filename - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - tmp_path = '{}/tmp/{}'.format(public.get_panel_path(),public.md5(sfile + filename)) - result = {} - result['status'] = True - result['data'] = '' - with tarfile.open(sfile,'r') as zip_file: - try: - zip_file.extract(filename,tmp_path) - result['data'] = public.readFile('{}/{}'.format(tmp_path,filename)) - except:pass - try: - public.rmdir(tmp_path) - except:pass - return result - - def delete_zip_file(self,args): - ''' - @name 删除压缩包内文件 - @param args['path'] 压缩包路径 - @param args['filenames'] 文件名列表,数组格式 - @return dict - ''' - sfile = args.sfile - filenames = args.filenames - - if not tarfile.is_tarfile(sfile): - return public.returnMsg(False,'Not a valid tar.gz archive file') - - tmp_path = self.__unzip_tmp_path(sfile) - if not tmp_path: return public.returnMsg(False,'Failed edit!') - - #组装原有的文件 - s_list = [] - src_list = {} - public.get_file_list(tmp_path,s_list) - for f in s_list: - if not os.path.isfile(f): continue - src_file = f.replace(tmp_path,'').strip('/') - if src_file in filenames: - continue - src_list[src_file] = f - - with tarfile.open(sfile,'w') as new_zfile: - try: - for src_file in src_list: - new_zfile.add(src_list[src_file],src_file) - except: - shutil.rmtree(tmp_path, True) - return public.returnMsg(False,'Failed delete file,error:' + public.get_error_info()) - - shutil.rmtree(tmp_path, True) - return public.returnMsg(True,'Compressed package file modified successfully') - - - def write_zip_file(self,args): - ''' - @name 写入压缩包内文件 - @param args['path'] 压缩包路径 - @param args['filename'] 文件名 - @param args['data'] 写入数据 - @return dict - ''' - - sfile = args.sfile - filename = args.filename - data = args.data - - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - tmp_path = self.__unzip_tmp_path(sfile) - if not tmp_path: return public.returnMsg(False,'Failed edit!') - public.writeFile('{}/{}'.format(tmp_path,filename),data) - - #组装原有的文件 - s_list = [] - src_list = {} - public.get_file_list(tmp_path,s_list) - for f in s_list: - if os.path.isdir(f): - continue - src_file = f.replace(tmp_path,'').strip('/') - if src_file in src_list: - continue - src_list[src_file] = f - - with tarfile.open(sfile,'w') as new_zfile: - try: - for src_file in src_list: - new_zfile.add(src_list[src_file],src_file) - except: - shutil.rmtree(tmp_path, True) - return public.returnMsg(False,'Failed modify file,error:' + public.get_error_info()) - - shutil.rmtree(tmp_path, True) - return public.returnMsg(True,'Compressed package file modified successfully') - - - - def extract_byfiles(self,args): - """ - @name 解压部分文件 - @param args['path'] 压缩包路径 - @param args['extract_path'] 解压路径 - @param args['filenames'] 文件名列表,数组格式 - """ - sfile = args.sfile - filenames = args.filenames - extract_path = args.extract_path - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - if not os.path.exists(extract_path): - os.makedirs(extract_path,384) - - tmp_path = '{}/tmp/{}'.format(public.get_panel_path(),public.md5(public.GetRandomString(32))) - if not os.path.exists(tmp_path): - os.makedirs(tmp_path,384) - - with tarfile.open(sfile) as zip_file: - try: - m_list = {} - - f_infos = zip_file.getmembers() - for item in f_infos: - filename = self.__get_zip_filename(item) - - if filename in filenames: - spath = os.path.join(tmp_path,filename).strip('/') - if item.isdir(): - m_list[spath] = [] - else: - if not 'other' in m_list: - m_list['other'] = [] - - dir_key = os.path.dirname(spath) - info = {'src':spath,'dst':'{}/{}'.format(extract_path, filename.strip('/'))} - if dir_key in m_list: - info['dst'] = '{}/{}'.format(extract_path,'/'.join(filename.split('/')[1:])) - s_path = os.path.dirname(info['dst']) - if not os.path.exists(s_path): os.makedirs(s_path,384) - - m_list[dir_key].append(info) - else: - m_list['other'].append(info) - - s_path = os.path.dirname(info['dst']) - if not os.path.exists(s_path): os.makedirs(s_path, 384) - zip_file.extract(filename.strip('/'),tmp_path) - - for key in m_list: - try: - for info in m_list[key]: - if os.getenv('BT_PANEL'): - shutil.copyfile(info['src'],info['dst']) - else: - shutil.copyfile('/' + info['src'],'/' + info['dst']) - except: - pass - shutil.rmtree(tmp_path, True) - except: - return public.returnMsg(False,'Decompression failed,error:' + public.get_error_info()) - return public.returnMsg(True,'File was decompressed successfully') - - def __unzip_tmp_path(self,sfile): - ''' - @name 获取临时解压路径 - @param sfile 压缩包路径 - @return str - ''' - tmp_path = '{}/tmp/{}'.format(public.get_soft_path(),public.md5(public.GetRandomString(32))) - with tarfile.open(sfile) as zip_file: - try: - zip_file.extractall(tmp_path) - except: return False - - return tmp_path - - - def add_zip_file(self,args): - ''' - @name 添加文件到压缩包 - @param args['r_path'] 跟路径 - @param args['filename'] 文件名 - @param args['f_list'] 写入数据 - @return dict - ''' - - sfile = args.sfile - r_path = args.r_path - f_list = args.f_list - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - tmp_path = self.__unzip_tmp_path(sfile) - if not tmp_path: return public.returnMsg(False,'Failed edit!') - - #组装新添加的文件 - src_list = {} - for fname in f_list: - if os.path.isdir(fname): - s_list = [] - public.get_file_list(fname,s_list) - - for f in s_list: - if os.path.isdir(f): - continue - src_file = '{}/{}{}'.format(r_path,os.path.basename(fname),f.replace(fname,'')).replace('//','/') - src_list[src_file] = f - else: - src_file = '{}/{}'.format(r_path, os.path.basename(fname)).replace('//','/') - src_list[src_file] = fname - - #组装原有的文件 - s_list = [] - public.get_file_list(tmp_path,s_list) - for f in s_list: - if os.path.isdir(f): - continue - src_file = f.replace(tmp_path,'').strip('/') - if src_file in src_list: - continue - src_list[src_file] = f - - with tarfile.open(sfile,'w') as new_zfile: - try: - for src_file in src_list: - new_zfile.add(src_list[src_file],src_file) - except: - shutil.rmtree(tmp_path, True) - return public.returnMsg(False,'Failed add file,error:' + public.get_error_info()) - - shutil.rmtree(tmp_path, True) - return public.returnMsg(True,'Compressed package file modified successfully') - - - - def __get_zip_filename(self,item): - ''' - @name 获取压缩包文件名 - @param item 压缩包文件对象 - @return string - ''' - filename = item.name - try: - filename = item.name.encode('cp437').decode('gbk') - except:pass - if item.isdir(): - filename += '/' - return filename - - - - - - +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +Hf+bBTgRv9pVfall8ODpvg== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +1u+XjG/2+GSQRv6EzCaWRQ== +K3QJU/hV27C5FAVoZ+avNNMfDMygeonfI5TFnvUHjhU= +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +hUVhKZtvg+FKessuieMZGkP/5FS8EfJsx7HCvhsTdvM= +y9P3+4KXYacR8m2lL04bxyxkWpCOoKKwyE9jOO9xzDU= +1u+XjG/2+GSQRv6EzCaWRQ== +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhLTeka+iJoxis1kUHdyggy+yXh3Xc8xsyir/5NbKP+Sj41j2UHgL27dZFRsmbh/P+Q== +KZTmaJLp+FU9X93j5Tpqtw== +0LFyB+q/4AUBkCrX53xYJedFoNZHYELHOLu/R25oBrqozNv9g917+/ANMSb6X+WR +yqM179SzW9HnBHozkU+IlaI6kf6Fax7Db/qE4wQMDFqfwVE+4rG3+IJ9S3c+p3Wf +akHibliG6j9uvBEg96vRehUZmhX3a+KyYm/Obf/H764= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +x41zVQ6DCtWXndDIq7U8L+5quM3ARMCnVm34NUX7l/TJzZZ100EvdUXHXI2cFH4H +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P7D7U0y87ntgFdQV5REShmme9ACxsySMBZr2gsEQniKL +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +akHibliG6j9uvBEg96vRer/Fg5zTlM8oj8hSAsYq018= +KZTmaJLp+FU9X93j5Tpqtw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +sV2BMz3qoSZgP7XLsV0u8Ey5TTZbfTeRXmE8Lnkq8ecwI8LY+wsKkeHx5JQmHjbJ +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWW4zkUYYb5UAUX8zS+IvdqmDjQdLy6c68fWPpLBj0d0GY6NLmGX6bobgiWRy9bUsk= +1u+XjG/2+GSQRv6EzCaWRQ== +1VHvxkEcO4espNMpWHmnH39LdUi6HdEZyAx9T9sPSkCOt9wcLgOersS/HmOi2Xel +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +ei/1A+2pKZSasjYF/t4qooATwcryDpeJQCtosnRGcgHSsp9sawkQJUO/Q/xMUnXL +1u+XjG/2+GSQRv6EzCaWRQ== +4nsuLyRybkZg8C3fJCsRPXDWwLMccXwEpFBdFK0JOvg= +ZFD6pnBshb3mpu7xvvuMLemrazA4ssXDDuW5SINX4ZLsvLIwaJ4YyDANze64OGqUEj7NCJs+t+zkcUskHr+gqw== +1u+XjG/2+GSQRv6EzCaWRQ== +g5xFPP9LKBd4uktrRYzGf+p1NdL+k6huN6CrzYXY3fPVQzhtKStMTpRTZpRqmhGR +wlLHv6kT3Q/RmtMBN4nDAbqo0o2IA7mU8WVNqZvoB5I= +VmVrGQo2zRokW/ZuO9bN6+qRbBr2ygG3RGcAIOk727ylzODmV8VRqZ4YKvduSUCu +VmVrGQo2zRokW/ZuO9bN62psx0nYMGJaQ1S1BUtE6l8lgsopQJ3Qf5syQpAasAAf +VmVrGQo2zRokW/ZuO9bN69Woqo49I0Fga24Nj55N9ZL81I98txu56TEAiJ0hswK9 +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69+qNrAfkaoVno2zP/PIMh/5aA4AiEej6zodAVzZFzS0 +VmVrGQo2zRokW/ZuO9bN6xT3wQu8/ZV37F/D/EekYn8lOmX2nmjCcfYXY14ohcOqo/JjN+Uf5m+lGfErLLeSsw== +VmVrGQo2zRokW/ZuO9bN6xT3wQu8/ZV37F/D/EekYn92m0FWAHkcFEifdcSefZI3 +VmVrGQo2zRokW/ZuO9bN6xQU/k6xhXMbxscs7Gix8nNqUkjAknQH3LrGVvAyjYtQ +VmVrGQo2zRokW/ZuO9bN6xOutEc9teLv333RLphPxTSZMBPTZ5nEHEAZCqisJAYdi0Te13nq3FDKoUwPWRFVhcbFGXs/hncVFmPVZNUBUZY= +VmVrGQo2zRokW/ZuO9bN6wv73IH9kSzXKMUmUPacSk1KDsdrjROzTN1kzduuP0l3 +VmVrGQo2zRokW/ZuO9bN69lJPu1G3iX01kmj1/0ynPc= +VmVrGQo2zRokW/ZuO9bN63p1oaV/8/rZOHP3bxdBsLtxlUb0X9CVsj27gKH7yxkc +VmVrGQo2zRokW/ZuO9bN6/kiIWH8hJ6dJOo/fnNj9VLcBHtqpOZ/Rc4AjXcEm1edHKkbtiFAlXrzE+rfp/AeMQ== +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN69+qNrAfkaoVno2zP/PIMh80w0dM3z0phdMS+lT6j/xo +VmVrGQo2zRokW/ZuO9bN65abAZ39yZLyKCl4nd6eov4HdKauw4+toNc5YoXvsAeW +1u+XjG/2+GSQRv6EzCaWRQ== ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +J24jXQQti6wQv5pCoF2ZrRvdf2ev6P6vInKuFAO8eQfewP9/G67aA1dZDLRwjari +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P7D7U0y87ntgFdQV5REShmlP+1p+XjT1dvUjFUZHsqC6 +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4um2nnLtPSMsHb7nUhh0kKQ6CdlvwTSeB4h8LEiuwG9t +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP263Ug7hf7sm5bqxRn1G7j8CZM6KmOHJcbEZNXo2mkEAKAgQMZx8+NhDilcUt2veW2ndED3g1z4Otqk5QlUsRCjvCITtLgXlADwlPZpuWSgD +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +YSlit6erKXylrjSZnvyW8I3FYH6DLcfYpMAlko6lQqVBBd1eDeDGaujrHWIOJkcI +YSlit6erKXylrjSZnvyW8CCccEStX4fvFvduMtCGi54= +D1uzgsuXIKL/yxNsPwFM0QIV7+TILj/tblfnXjJ0GYRo9qF9iYlR/oIixOO4YW9r5sQV57F10J/zF5jBaEFdKw== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6+8scbwlFXmHwUL6N0BWPj6dlHHDEIlkodgo1A0usjGyHhQdtr/CZJG5zDxIr66oew== +VmVrGQo2zRokW/ZuO9bN65rMxzVrOInziG0YF1uvpqBp8ZS5jWaYG3vnBxPtXcWWa9TIx0uiiZmKhmv6T3Z+yciaFXeGtzFwR28zuXWWSf6Fvx4Mh4aXTcAi9R5RGv8f +M0ZySqkmhuHCw6olbCKv992x+E1VMoPx+GJL+VjXXxc= +b4OJVZe8QyIpjuTpKXDL9A== +xWoGNWjKGPfI4gq8aHoTfL6V0pkYKbeBvdfg5x1FYR8uFPtJ2x9MfdlNdGBKn+Bi +usrsES3VVkgHW6tBbEkpHTeLztsByVaJibbFv8kSFIM= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +6JhaWzGWadTJ3yzCk9OQNgkLy/m1ahTUuEy+6FiV49HNwHkAN2ZMxFTsufeIA0ig +KZTmaJLp+FU9X93j5Tpqtw== +dar6N0tUAb9rU8/aX1b3tGumQ1hhfk8O6qr6gmiQ1+eBvZwFkpECn+RP444+i510 +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMlEDkaPg52QqeaL1e16aTiZGdP9MF1i1K/F0icaNF/FRZY8CjFaji8Ecm2WEpXZbMiyduIw57U2lMT6kzLqch3Y= +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4kqIZZgv1yXQHpqfJ4aNJ54bUNUYTYq/0A+dhHby52Au +1u+XjG/2+GSQRv6EzCaWRQ== +sV2BMz3qoSZgP7XLsV0u8Ey5TTZbfTeRXmE8Lnkq8ecwI8LY+wsKkeHx5JQmHjbJ +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWW4zkUYYb5UAUX8zS+IvdqmDjQdLy6c68fWPpLBj0d0GY6NLmGX6bobgiWRy9bUsk= +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP/1JxkOTIR85eefsT8+ZE4aqE0v0U76615IyJqY5UKTuMtUbLm26g8btb6vlq0a/Rg== +sV2BMz3qoSZgP7XLsV0u8FnjXWiNqL5EgZ3mkCPlBXRqd4GmOGu47uyTs1R79OxGn8j+RZZ1XwLWsKhwXR/uRAfKx5AH0WN2iitz8d+Ezds= +1u+XjG/2+GSQRv6EzCaWRQ== +/Bf0c5l+J3LcMje0y2hT6xexfB3PDHGLzFprRXh5sZU= +gCKKzKpjyg80fGP7iU1CosQydbZEVmkhqpAlx45K/ms= +HI5OOAOTfAAnGE3XBxyr2vdUoaKdz9Yp4Em51oFqNhU= +b0AcCU56KvRpMcnxfbDRg2RaKdDnbQt+ar3yTu4ASKKZrazBALhe/1DkqoflPq4v +QOiTGJqcvhOfGILFspJCgFitD1klQoY+1I4jtCCP/XI= +1V2v8QerKOmubvSxgB4eTFLJVKokCn/n7cBzRCyjPmfmpa/q0AQoQFAFtjS1Nskr +JBX7lPwBHKhyRzgJY9OZ2ID8CtKJxnNl/B8H+N4MrEOtqpoairx3dp0kQkKQXLr/i2uDaqSXVIfJ6RfDnJqDlA== +J1UGlXkKhKXD36OrXXhN8J2xIJzaLSbkMLWZBVYhIzKIhtDAsZ6oAssWKRx5BzNP +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +JBX7lPwBHKhyRzgJY9OZ2KI6iis8uoA2jLORIv/XFCtWDyuLfsIYrBQwTzzUuSVO +1u+XjG/2+GSQRv6EzCaWRQ== +D1uzgsuXIKL/yxNsPwFM0QIV7+TILj/tblfnXjJ0GYQlAG5xlrK2k9RIx8apVXk+fYR83glUbhc3FJlByrwJkQ== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6xHuLV+pwxqKLEVIaJlVY1mV5wixDkrlkeQ/46GnHjPJ +VmVrGQo2zRokW/ZuO9bN69ftsX0HT5bqVOOMRQ1dnH6J2cD5LXLKGBEYYVmwvLbOnW/+ho3f4pS5gGHbjP7S6A== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN67XKiXTToYD4Xj8EJf9tU1/Vu+G41TBzTVvAU2KLSeCv +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTLBRO8Ka4sxejzogHbwTSQCRjmbMCS45yRDKakJA+6lygvtX8/AEkLox/HgNQ0WReKAZvStGcdGPMqn6vgZv5Gu5msyaDAcqtS9MyGyGb9+g== +1u+XjG/2+GSQRv6EzCaWRQ== +oCwMuj/95ZXy+xntYImc0fRuDbZyEvkaVbFAkuMpm9yWIfTaDuSAdU/L7ROHPmO1 +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGZIAyEpuVq0D9UpB4o0dY00eifTWefY3mmHrggp7bGLkFRUvNVINpyXDZbESEgd4Pe4G8AvpwDNkWSLrOyNGKY +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +O2CqINTuIULxX7Gu5GskT/Nm15YFovaVXDj8ylNfQi3d2Qbp7uZYhyvrRE1b06DK +KZTmaJLp+FU9X93j5Tpqtw== +t987AUZxQR72IPRcAqvQ/tuxrpVopv9MhEHHX4HHOFnF6WZD8PVxYK60YbaBpzoa +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +c1XU6vxl7MsQKGo3r4amMgGxGyqG90Nz+St+6838nV5Hy3ZAmDi9Ecxx2gyS4yZO +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4um2nnLtPSMsHb7nUhh0kKQ6CdlvwTSeB4h8LEiuwG9t +XKWK9LfYDT5w2LEWWxwkc6l2k+0eByijzzjX3gaj5k8= +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP/1JxkOTIR85eefsT8+ZE4aqE0v0U76615IyJqY5UKTuMtUbLm26g8btb6vlq0a/Rg== +sV2BMz3qoSZgP7XLsV0u8FnjXWiNqL5EgZ3mkCPlBXRqd4GmOGu47uyTs1R79OxGn8j+RZZ1XwLWsKhwXR/uRAfKx5AH0WN2iitz8d+Ezds= +IhuisKim47k91RVt8z8qtAZ7rSCYljkiJH9HzGQ5OPx8FxzBv8dhM3FQlWygMwgCxPuj/L0zzaJH7M9JLtcNyBQAqW+52jDxmeGRDtqtSp8= +1u+XjG/2+GSQRv6EzCaWRQ== +/Bf0c5l+J3LcMje0y2hT6xexfB3PDHGLzFprRXh5sZU= +gCKKzKpjyg80fGP7iU1CosQydbZEVmkhqpAlx45K/ms= +HI5OOAOTfAAnGE3XBxyr2vdUoaKdz9Yp4Em51oFqNhU= +b0AcCU56KvRpMcnxfbDRg2RaKdDnbQt+ar3yTu4ASKKZrazBALhe/1DkqoflPq4v +QOiTGJqcvhOfGILFspJCgFitD1klQoY+1I4jtCCP/XI= +O3CUgrw2GJfB+mDjH5+NdkjK1xGXzP/MBLULODELp6gwPy9qqsLvy+O0nI8WaT4C +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +JBX7lPwBHKhyRzgJY9OZ2ID8CtKJxnNl/B8H+N4MrEOtqpoairx3dp0kQkKQXLr/i2uDaqSXVIfJ6RfDnJqDlA== +J1UGlXkKhKXD36OrXXhN8JAqE/N8+U09yjtfA8xA8yW9SYS4/U88+D8eMNmNqLL6 +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +JBX7lPwBHKhyRzgJY9OZ2KI6iis8uoA2jLORIv/XFCtWDyuLfsIYrBQwTzzUuSVO +1u+XjG/2+GSQRv6EzCaWRQ== +D1uzgsuXIKL/yxNsPwFM0QIV7+TILj/tblfnXjJ0GYQlAG5xlrK2k9RIx8apVXk+fYR83glUbhc3FJlByrwJkQ== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6xHuLV+pwxqKLEVIaJlVY1mV5wixDkrlkeQ/46GnHjPJ +VmVrGQo2zRokW/ZuO9bN69ftsX0HT5bqVOOMRQ1dnH6J2cD5LXLKGBEYYVmwvLbOnW/+ho3f4pS5gGHbjP7S6A== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN67XKiXTToYD4Xj8EJf9tU1/Vu+G41TBzTVvAU2KLSeCv +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTLBRO8Ka4sxejzogHbwTSQBD0QaFQ6mH2ykhpeatV5MFSAvHLSQE3mXQCKlo9Vmyj7/c2aTE5OzSuRlIiCt76fbCvV4M9DgyzOc6QS8FnjVg== +1u+XjG/2+GSQRv6EzCaWRQ== +oCwMuj/95ZXy+xntYImc0fRuDbZyEvkaVbFAkuMpm9yWIfTaDuSAdU/L7ROHPmO1 +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGZIAyEpuVq0D9UpB4o0dY00eifTWefY3mmHrggp7bGLkFRUvNVINpyXDZbESEgd4Pe4G8AvpwDNkWSLrOyNGKY +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +D5nlraEpeGmKL9lxoq591txjX2tGuFl4D7WJZ/91lnMQ6WZrDXvU2okqtgoaqWSS +Z8UsPk1Q7HtwjRd4g01ryw== +VcOpbkIFMz59VlIHGus1M+TodC/Ia97E/RilXZKKBR29V7X/RlCXQzuWibEOEJfo +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMjvzqCkQbmHuG+orSe7QiThO3NRqf8lDLQklcqf6u2x4yWOeax5QyyroyrE443cDvw== +c1XU6vxl7MsQKGo3r4amMlEDkaPg52QqeaL1e16aTiZGdP9MF1i1K/F0icaNF/FRZY8CjFaji8Ecm2WEpXZbMiyduIw57U2lMT6kzLqch3Y= +Z8UsPk1Q7HtwjRd4g01ryw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4kqIZZgv1yXQHpqfJ4aNJ54bUNUYTYq/0A+dhHby52Au +77Mf9+1xBvZiUeNTtU/s60Kyn+UEu/OgKFt23E+wd8BiTmgbWFTzjcyPW/imPN9p +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYmSt8oPS+2lwNla/wCY62SBZ7HAsGrtjJH7dzi+5hXSr +NqhMbY8+EQI8zhTG6Zh2I5csQ+/eG6egKX2sKZJStJZk02bOBi20A8UHGNy6sybt +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP263Ug7hf7sm5bqxRn1G7j8CZM6KmOHJcbEZNXo2mkEAKAgQMZx8+NhDilcUt2veWwUPocpK+8JugxVoHXyHBSRb28kLWdpmZTTf3StM9Y2XDpZ02OI3BeuyFvxLweVX9w== +wgR07xfoapmx6eEnFHXXYm7IOJ/6J1NThWNK2EdPRzYwWPvLP+jlqDp61mwE1mzG +NqhMbY8+EQI8zhTG6Zh2I0BiMnuwr8mLUTT6rraGmzoW2r/izQNInq2UmHNhRbv+ +1u+XjG/2+GSQRv6EzCaWRQ== +D1uzgsuXIKL/yxNsPwFM0bn1b8lc4rkM7jKkoTPs67jZgYEdG3TOZllO4+vZp0ZM +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN61lflwobhmxel33RwHEAzsw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67AsrXUPIcMWmsNJ6skMzcKKCpn1+SzebzO6hgphZ4RaLaD/MDC8FwcBZZJFOSWJmA== +VmVrGQo2zRokW/ZuO9bN66NLXvUSFhBjtRrUbZsPQNOrrRiOaX/5u3wb0Rfqy+pv +VmVrGQo2zRokW/ZuO9bN6wsF8dCAuyXrReoI3w9Yj8ejyyRZrswtbKGtDKu6ywDGElhkS5s5y7BVqVo+FqDhvA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wOXhaCTOJSjUCWUfjx+YyYNeSRhAnvmOzc5j0wt6pWe +VmVrGQo2zRokW/ZuO9bN61ODJTwuVdYa2e77kV4UyyiKC6xmDTYLpzmUEL6iaKIotT6XRfjdpuPhvefHRzC+7lCYolxcDYktKmTY39wnf+k= +VmVrGQo2zRokW/ZuO9bN63p1oaV/8/rZOHP3bxdBsLtxlUb0X9CVsj27gKH7yxkc +VmVrGQo2zRokW/ZuO9bN655297gigIv+4Ufy0bubbx7ffTJUoURR64bKe908x4ap +VmVrGQo2zRokW/ZuO9bN62cJZxyhxghVn78AgYWoneU= +VmVrGQo2zRokW/ZuO9bN63LTkM1jyAeOymc1jas3GbdSOgZt1gFP6QuzR+PijaPfkXI6n5+LDNetR25SN0GXgg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkns33MIHJ07fAs3bEPA9prEegTrYMIIXPrzC4jD9xKXsA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+VE/nQEHJOLSC1DnxvG5TiVhVg6gVuqdGuJQhlv12yWuKoI97jUkrKrXkDjch0nxA== +VmVrGQo2zRokW/ZuO9bN66Stm18E9cFphVzr4DVvkp0eMQP2KdIRCCvQSRwvi/ogRLfVX0ez0AMMWvL6BswFfzkKtdTc18Qoh/tgpwV2o+KvhObTvNhFYenGcSOQ2IfJ/VBsqInH+jm5934kDPyo5Q== +VmVrGQo2zRokW/ZuO9bN6zsPJOFHzvayEpAPvGiJ0lAWXca0TugmxSi8hINZZvoHQ1Y8EIw5ZU/3qx6IfU7mzw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmgImK713UKKRnRLyQBV8gyjNySqhD1ER71FyIggw6WuTb4tOz+zOmUJvBAa9I7hKop6pGwEoB2371+83RgakZhyjMJWosIoeLReEb5GcqHNA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkrikeB3PyYnRyHWKvzXceKTTsB9Sbgf9uM/v0isHHFakzQxpTSrdsYuODmqyQGE3Q= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknz2So+gFu+BijKlvPtsNKCIi0RkX1BFpd3GhHKBMdoB6DedipwufkrnDIRLB493gRtMiXCiHSsTBNmIXqRl2RK +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkqG04WXvd0zYTVxLMDUCWknQtobuOLFvBEBrZieoy/MQ== +VmVrGQo2zRokW/ZuO9bN61TKHwbNGf/2Ub0vd756U9O0Gtl0vapnf7HpdYx4U2ib +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkk6a/inNeVFTn9NarE6P/3LG5P9KtqHegfYsmaf6Actxw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yaURb+J41vJInDMdNBqADJBPzFwU1DWEhzV7Igw3NGvcy7cQ4IWtwYwzl4xxnS4q7aao1GGQ/lpjefMWmVC5io= +VmVrGQo2zRokW/ZuO9bN63LTkM1jyAeOymc1jas3GbfaLzRNSm7AkFFrFp+7ESwXtG1lg7SuOb/Fvk+nDWM/LTeUYMfTnzndr1z2COlogy5E3igzgZFBhcAjJ6avABsS +VmVrGQo2zRokW/ZuO9bN67FJHoI0w8vvrPd3RbddRrD2Em+ygcQFnOdLaXNs8ffOLcZZMjCFzoLC6LOvXRJel9lRMBQd5MAQelNetUjr7a0= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN65aFRI029wWEhVZc+cvXTRpzda0UlKTw9izNui/xkgdD +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN6ylpzdD3c1E8cbuPHoSW5RFJzxWCEvXq+6grREYT2q2eRpfKq4CUkXaSg9PBb07Brg== +VmVrGQo2zRokW/ZuO9bN65qgzI0QqVLALq/Lz0RhHgVj4W6v2iAhy3bpNtZbOHJ8qR+FfOTDkpmrnn3Bg7loWg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknNd/k4iuNp2bbqaDmaAfko6jVO83Qfz+7nZDbM61ZOu8HuZ+ZNu6hMvpayaQR5SAc= +VmVrGQo2zRokW/ZuO9bN61TKHwbNGf/2Ub0vd756U9O0Gtl0vapnf7HpdYx4U2ib +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknNd/k4iuNp2bbqaDmaAfkosj3Q/Uyc4ih1OKYxi2Aoat/XPMKk6ZFw9dS97mdyNCW7sbOTUzQ7vKis/hh/FWBl +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +VmVrGQo2zRokW/ZuO9bN67XKiXTToYD4Xj8EJf9tU1/Vu+G41TBzTVvAU2KLSeCv +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTcB3+DRCAF+pezmRjCID0udaZbU9+Bp25aQzLB8QDwQjJOciYA4ZMGenxD3oVSnPjhf/xssdSo8p33UNNP+7LnwEXEbYt+fXn2F5oiKjC6Gw== +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOF2h+KCvqR3zTaYTe9GwY3cxsP+/BBmVOckz/EpSQTZmvBKIrz2PIwNuNhzK8OCOI4= +1u+XjG/2+GSQRv6EzCaWRQ== +nfBQrFU5Tl0d1VcHhIJkAP9Tx9PwP8cRBjb6FOkkDsaLG6R4uDQDi4xcdtmOXEqB +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P3euInP55YyecjBWhnEnlB7Lw5dMNDV3iHSK0kwD9rha +yqM179SzW9HnBHozkU+IlV+BVKi+rIq6p9thxVddK2wK0J7yGt1J4fq2cywdhxRL +akHibliG6j9uvBEg96vRemf0tAA5Rm6aB+lqKw8WFi0= +KZTmaJLp+FU9X93j5Tpqtw== +IZj4TYq2HlScfSdRkI+QP263Ug7hf7sm5bqxRn1G7j8CZM6KmOHJcbEZNXo2mkEA3MxUp7pEuHhp8IfRMvwD6SZPogdTtHhT5unY8iIrUiU6GaMq6uosuXq8hjxV9nUgwa2iqC1n6Ziz6tiDdPy1mw== +D1uzgsuXIKL/yxNsPwFM0bn1b8lc4rkM7jKkoTPs67jZgYEdG3TOZllO4+vZp0ZM +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6+8scbwlFXmHwUL6N0BWPj5rk9N0kB/j2zz5GqVVZxsc +M0ZySqkmhuHCw6olbCKv9xWw7HeiAhxWgwzOj+uORvYUGO2Wun1zPlqkYizDiyWM +1u+XjG/2+GSQRv6EzCaWRQ== +00qsNOHBbp9Q+MTSOB/jV1gEYraNSaFlJL1NYP7mKBg= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +mObL0pW76xtWwwYeubgb11DPfOo/O10pxYUkfDIVCjm8OCRNT0T/bbX1vgDt/X1W +KZTmaJLp+FU9X93j5Tpqtw== +H7d2MP+WgR9k+129/mRN7ij0WzXIk6KGh6j7E1fgHISsYBh1T2OGWCH8QUwPOkcn +c1XU6vxl7MsQKGo3r4amMvdbawOnt/oBPSHXSrswDVS5Xue1Y7sB0F2vPYt641B/ +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +c1XU6vxl7MsQKGo3r4amMqf9zivp0Qb6thhCnxaiYdHXwUNLWpYP828px7Gieygp +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +g8q6WqkWsk4SUh3d5nzGDIJanaAux29CDdQxdeG/k+U= +Vyh8kMpaq2Q+odra9mDJ3Acz3kaMgWRroPCWRIzYRMY= +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP/1JxkOTIR85eefsT8+ZE4aqE0v0U76615IyJqY5UKTuMtUbLm26g8btb6vlq0a/Rg== +sV2BMz3qoSZgP7XLsV0u8FnjXWiNqL5EgZ3mkCPlBXRqd4GmOGu47uyTs1R79OxGn8j+RZZ1XwLWsKhwXR/uRAfKx5AH0WN2iitz8d+Ezds= +1u+XjG/2+GSQRv6EzCaWRQ== +OKNarVhdXTeaHF7smpo1Fsg5Z6GS4ctUP7SwO9m7ADsrK59qTlLvB8EsnHNRomb5 +HI5OOAOTfAAnGE3XBxyr2vdUoaKdz9Yp4Em51oFqNhU= +mZ9rrP32sP7RJKYMPGILD+78ba2l0mR6sa16eW97WcI= +O3CUgrw2GJfB+mDjH5+NdlB3aQOXnKkk0z1WAt9Jmo/evqziM8DhkT0YrYGb4COh +VmVrGQo2zRokW/ZuO9bN67lx9HSk8eNN2hBCO4HpKRk= +VmVrGQo2zRokW/ZuO9bN69ZjyuXvRyGNy9kZJ0BSUPrXcl5zPRPxpUUNlqBFMfTepJuwObNjyv5s1Bwxw+0zHA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yiPlSW1IkAq8PnO6zy+iP3jJCfd1bhtOH5rekZFEjXi +VmVrGQo2zRokW/ZuO9bN6yIV96dETdHvOTzc1eoxsu7+haDOTWWWdxKhfAdfjn09 +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNuRprwNE+dzFP36dxj7lYWb +VmVrGQo2zRokW/ZuO9bN60+BaOrNhyGitXORdqvsMmwFtJw9x+0nfzpYbP/xMELoydQeOelN/AlsLE8govEqlMQrfUl1PeNk2rX8fh0ZElrak4Q4jLc+GGiQ0DLMMpnH/1J5GKH49RP7rJbs5dxgJ4tOonH1clyp6d9GTs2dKrA= +VmVrGQo2zRokW/ZuO9bN60jWe2OHiR8czCXypGRfxf3KT4lPFL0wUA7wcV6fbrUq +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN62YWnBw/cibruWabHzPuqaAcn7VyIMN4YMuZ3S9CW09ENvrO91BA4ldYfyyQpiR6Cy5R4Fei8M2XzZTUjvQbS6iXg3tLmWav6AtitMV2FQP4 +VmVrGQo2zRokW/ZuO9bN608vqClPKbLpxLXcA30riX215gq1D0BrebU6pLhAhTm2 +1u+XjG/2+GSQRv6EzCaWRQ== +/Bf0c5l+J3LcMje0y2hT6xexfB3PDHGLzFprRXh5sZU= +gCKKzKpjyg80fGP7iU1CosQydbZEVmkhqpAlx45K/ms= +b0AcCU56KvRpMcnxfbDRg2RaKdDnbQt+ar3yTu4ASKKZrazBALhe/1DkqoflPq4v +QOiTGJqcvhOfGILFspJCgFitD1klQoY+1I4jtCCP/XI= +O3CUgrw2GJfB+mDjH5+NdkjK1xGXzP/MBLULODELp6gwPy9qqsLvy+O0nI8WaT4C +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +JBX7lPwBHKhyRzgJY9OZ2ID8CtKJxnNl/B8H+N4MrEOtqpoairx3dp0kQkKQXLr/i2uDaqSXVIfJ6RfDnJqDlA== +J1UGlXkKhKXD36OrXXhN8JAqE/N8+U09yjtfA8xA8yW9SYS4/U88+D8eMNmNqLL6 +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +JBX7lPwBHKhyRzgJY9OZ2KI6iis8uoA2jLORIv/XFCtWDyuLfsIYrBQwTzzUuSVO +1u+XjG/2+GSQRv6EzCaWRQ== +D1uzgsuXIKL/yxNsPwFM0QIV7+TILj/tblfnXjJ0GYQlAG5xlrK2k9RIx8apVXk+fYR83glUbhc3FJlByrwJkQ== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6xHuLV+pwxqKLEVIaJlVY1mV5wixDkrlkeQ/46GnHjPJ +VmVrGQo2zRokW/ZuO9bN69ftsX0HT5bqVOOMRQ1dnH6J2cD5LXLKGBEYYVmwvLbOnW/+ho3f4pS5gGHbjP7S6A== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN67XKiXTToYD4Xj8EJf9tU1/Vu+G41TBzTVvAU2KLSeCv +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTLBRO8Ka4sxejzogHbwTSQIMBV8oz+JZw1lVuk9zsKubda9eiR1++L545YBfEjatGypdQPztU2DAF6OXEg3W2a3ltwtcgo9+IKhfYuqJESBw== +1u+XjG/2+GSQRv6EzCaWRQ== +oCwMuj/95ZXy+xntYImc0fRuDbZyEvkaVbFAkuMpm9yWIfTaDuSAdU/L7ROHPmO1 +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGZIAyEpuVq0D9UpB4o0dY00eifTWefY3mmHrggp7bGLkFRUvNVINpyXDZbESEgd4Pe4G8AvpwDNkWSLrOyNGKY +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmA0y/dvI19s9e1BRFCkk/QzH4UG1JBu620e7CQwjbQOOUvlgFJz4yLxls8aSEEo +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P+yZLrQY+X/LEpSBMw7xwfahIuTImHO5EcUQN9HQmgqX +LikBEoRjt8wwWzUZNw+jJqDVmHo0aqmO8fx4adbRXm90KhRgBH5ZfsJyfEAx+4JR +akHibliG6j9uvBEg96vRei563dWr9knc+JrDspjHc/A= +KZTmaJLp+FU9X93j5Tpqtw== +BIa0ibHnqUboaK9kYHYO4k/Xks2d0kb8EqwPCudb2sE= +b4OJVZe8QyIpjuTpKXDL9A== +PRG/YRzXVD8iVR3bzEcUnu1TFK0huFhoat7c0/vfEZdQ5ejFvZhOUgdVFA1LA3t54/DjYCmHvkQ18q4IM7keqQ== +usrsES3VVkgHW6tBbEkpHTeLztsByVaJibbFv8kSFIM= +6XDh38s4HWrzA9ChHtrpYV0DWWNc3chcp0di+Spm+pg= +PRG/YRzXVD8iVR3bzEcUnpfl+XcdHX8i0iNThoY4QPk= +5wsVwKLrIjIWqeTNpSVp9SZAE79lgehXxNsrtfaN8Wk= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== diff --git a/class/filesModel/logsModel.py b/class/filesModel/logsModel.py index 03592c88..8391b550 100644 --- a/class/filesModel/logsModel.py +++ b/class/filesModel/logsModel.py @@ -1,289 +1,289 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# -#------------------------------ -import os,sys,re -from filesModel.base import filesBase -import public,files,json,time - -from BTPanel import cache - -class main(filesBase): - - - __objs = ['bos'] - def __init__(self): - pass - - - def get_logs_info(self,get): - """ - @查看日志 - @param get - limit:每页显示条数 - file:日志文件 - """ - p = 1 - limit = 200 - search = None - file = get.file - if 'limit' in get: limit = int(get.limit) - if 'p' in get: limit = int(get.p) - if 'search' in get: search = get.search - - if not os.path.exists(file): - return public.returnMsg(False,'Please specify file!') - - res = {} - res['status'] = True - res['data'] = self.GetNumLines(file,limit,p,search) - - res['md5'] = public.md5(res['data']) - res['limit'] = limit - - if not cache.get(file+'_logs_info'): - public.set_module_logs('files_get_logs_info','get_logs_info') - cache.set(file+'_logs_info','1',86400) - return res - - - def set_log_split(self,get): - """ - @name 文件切割 - @param filename 文件路径 - @param stype 切割类型 day:按天切割 size:按大小切割 - @param size 切割大小(stype=size必传) - """ - filename = get.filename - stype = get.stype - limit = int(get.limit) - if not stype in ['day','size']: - return public.returnMsg(False,'Cut type passing error.') - - if not os.path.exists(filename): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - if limit < 3: - return public.returnMsg(False,'The number of reserved copies cannot be less than 3.') - - data = {'type':stype,'limit':limit,'addtime':int(time.time())} - if stype == 'size': - size = int(get.size) - if size < 1024: - return public.returnMsg(False,'Cut size cannot be empty.') - data['size'] = size - - public.set_split_logs(filename,1,data) - - return public.returnMsg(True,'successfully set.') - - - def get_log_split(self,get): - """ - @name 获取文件切割信息 - @param filename 文件路径 - """ - data = {} - sfile = '{}/data/cutting_log.json'.format(public.get_panel_path()) - if os.path.exists(sfile): - try: - data = json.loads(public.readFile(sfile)) - except:pass - - return data - - - def get_file_ext(self,filename): - """ - @name 获取文件扩展名 - @param filename - """ - ss_exts = ['.tar.gz','.tar.bz2','.tar.bz'] - for s in ss_exts: - e_len = len(s) - f_len = len(filename) - if f_len < e_len: continue - if filename[-e_len:] == s: - return filename[:-e_len] ,s - if filename.find('.') == -1: return filename,'' - return os.path.splitext(filename) - - def copy_file_to(self, get): - """ - @name 创建文件副本 - @param get - @return - """ - - sfile = get.sfile - if not os.path.exists(sfile): - return public.returnMsg(False, 'FILE_NOT_EXISTS') - - spath,ext = sfile,'' - if os.path.isfile(get.sfile): - spath,ext = self.get_file_ext(sfile) - - # public.print_log(spath) - for x in range(1,1000): - dfile = '{} - copy ({}){}'.format(spath,x,ext) - if not os.path.exists(dfile): - break - - get.dfile = dfile - f_obj = files.files() - if os.path.isdir(get.sfile): - public.WriteLog("File manager","Create copy of the directory [{}]".format(sfile)) - return f_obj.CopyDir(get) - - import shutil - try: - shutil.copyfile(get.sfile, get.dfile) - public.WriteLog('TYPE_FILE', 'FILE_COPY_SUCCESS', - (get.sfile, get.dfile)) - try: - stat = os.stat(get.sfile) - os.chmod(get.dfile,stat.st_mode) - os.chown(get.dfile, stat.st_uid, stat.st_gid) - except:pass - public.WriteLog("File manager","Create copy of the file[{}]".format(sfile)) - return public.returnMsg(True, 'FILE_COPY_SUCCESS') - except: - return public.returnMsg(False, 'FILE_COPY_ERR') - - - def set_topping_status(self,get): - """ - @name 设置文件或目录置顶 - @param get - file:文件路径 - type:置顶类型 - """ - sfile = get.sfile - status = int(get.status) - if not os.path.exists(sfile): - import html - sfile = html.unescape(sfile) - if not os.path.exists(sfile): - return public.returnMsg(False, 'File or directory does not exist.') - - - data = {} - conf_file = '{}/data/toping.json'.format(public.get_panel_path()) - try : - if os.path.exists(conf_file): - data = json.loads(public.readFile(conf_file)) - except:pass - - if sfile in data: del data[sfile] - - if status: - data[sfile] = status - public.writeFile(conf_file, json.dumps(data)) - public.set_module_logs('files_set_topping_status','set_topping_status') - public.WriteLog("File manager","Modify [{}] top status".format(sfile)) - return public.returnMsg(True, 'Successful set.') - - - def GetNumLines(self,path, num, p=1,search = None): - """ - @name 取文件指定尾行数 - @param path 文件路径 - @param num 取尾行数 - @param p 当前页 - @param search 搜索关键字 - @return list - """ - pyVersion = sys.version_info[0] - max_len = 1024 * 128 - try: - from html import escape - if not os.path.exists(path): return "" - start_line = (p - 1) * num - count = start_line + num - fp = open(path, 'rb') - - buf = "" - fp.seek(-1, 2) - if fp.read(1) == "\n": fp.seek(-1, 2) - data = [] - total_len = 0 - b = True - n = 0 - - for i in range(count): - while True: - newline_pos = str.rfind(str(buf), "\n") - - pos = fp.tell() - if newline_pos != -1: - if n >= start_line: - line = buf[newline_pos + 1:] - - is_res = True - if search: - is_res = False - if line.find(search) >= 0 or re.search(search,line): - is_res = True - - if is_res: - line_len = len(line) - total_len += line_len - sp_len = total_len - max_len - if sp_len > 0: - line = line[sp_len:] - try: - data.insert(0, escape(line)) - except: - pass - buf = buf[:newline_pos] - n += 1 - break - else: - if pos == 0: - b = False - break - to_read = min(4096, pos) - fp.seek(-to_read, 1) - t_buf = fp.read(to_read) - if pyVersion == 3: - try: - if type(t_buf) == bytes: t_buf = t_buf.decode('utf-8',errors='ignore') - except: - try: - if type(t_buf) == bytes: t_buf = t_buf.decode('gbk',errors='ignore') - except: - t_buf = str(t_buf) - buf = t_buf + buf - fp.seek(-to_read, 1) - if pos - to_read == 0: - buf = "\n" + buf - if total_len >= max_len: break - if not b: break - fp.close() - result = "\n".join(data) - - if not result: raise Exception('null') - except: - result = '' - if len(result) > max_len: - result = result[-max_len:] - - try: - try: - result = json.dumps(result) - return json.loads(result).strip() - except: - if pyVersion == 2: - result = result.decode('utf8', errors='ignore') - else: - result = result.encode('utf-8', errors='ignore').decode("utf-8", errors="ignore") - return result.strip() - except: - return "" \ No newline at end of file +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +Hf+bBTgRv9pVfall8ODpvg== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +K3QJU/hV27C5FAVoZ+avNNMfDMygeonfI5TFnvUHjhU= +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +9ewEGhR02rX1CYy9TbJShdc5zOrGhv/95JJVBawwTJI= +1u+XjG/2+GSQRv6EzCaWRQ== +QtywOSt2jU7XEVQfd46hXhlGuDAcY6mFXPkzqinx4hA= +1u+XjG/2+GSQRv6EzCaWRQ== +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +P0xFZRxBgzs6EB2PV1P1Ga8KWkeEOjCa2HwBgtCLnkI= +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +/tIKPI7pxbgiB5DGE+JcX8inA0z7UW6nw0fpA/nuJtJ30NtW1IQQsUdAjwgJ+5zC +Z8UsPk1Q7HtwjRd4g01ryw== +0fJLHIpaUEUEURheAjqV+C5PbuPsfeNtAPgfcFiqP5w= +ho/Q+jrWDtBeg9J9ZTnKi3+/Efbvadpf+qoH75NuGwk= +zxNKIz/Dwj9Harw3dUNEQJhfwzP9e0g+QEHF20IF9QloJJn499jB5lN74CMN7xKF +PRG/YRzXVD8iVR3bzEcUnoM4TQf6qaKLuCt8gJ8NfkQ= +Z8UsPk1Q7HtwjRd4g01ryw== +y+v+I4rQ8oQ+Y894tAwqPQ== +p6i8iK7HDbvmLpoFrhNyv2ns9UjKsYvBLT40sf6Ap28= +Nxw10FAZUk+m0nadC1hdyy162NCelWBcYGf8TIFoMWw= +gw7rFE+f9bVF+DtSprQxBWIvcXcHzAaIYdK9TCMnZFs= +VIHA31zdnuwVlvUv3VOlYVRNGNAMqDxg7g3P55Yig7XGJ0frgqBbhflLu9pZDsGtG7PWJFzYnbAhv0HasJD0eg== +mcQwVxjUw79rwGN6NopbQ6kcPmfht+aHpy4N4vzTb9DbhhPn/FiUW/rz0u7pkKGy +X/Lp4PsbbXulceSXUvGUpDfqLabbqgG4MZE+Cdy4FwlQTZqFS7MLKRTLDMwaDvILIRxQybc5sD1eU9BOwIwsyw== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYsXheOqb+ac7ivrKomECGJjwdSnpWE4PzbAMHxIUoYAb +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmUZFcA4o3wUnGm4YjdorXMeGmKOMmT0MYEr6v6Dtj7fXTQgHn4plKdmzjTMeVlEm5A= +1u+XjG/2+GSQRv6EzCaWRQ== +QeR/vJltSrUa36sdgLk/IfL4q9jp7Kh5fPIA7nHg0hc= +qkpuPeWx54kZb1C5WIa62tKtrwMAUE7xRV1hPFjc0nk= +NUDHKeYglPKQjXCsTjqHkDpPG4Ff4llamNWSKelBxqKmBtoeDOPkBptxNl9ae6xEQqyUrJT9AnSRTpSh7aXAbg== +1u+XjG/2+GSQRv6EzCaWRQ== +ezgwiw1QpepwAFNSsnvQoOJN6u8DqbLWz8qgHsO6aqB6RHHe5HToLp0+UqRpP8fb +z+VhFG0r6WY/HBghnJVPENgMUP6uURPQG10QAuXe9q8= +1u+XjG/2+GSQRv6EzCaWRQ== +o3kxTO4RwuXO7GZ9wpXG+Wq35U1WRWDZvf7dXlWNmacOOTCJLfQ4KEfT1r5nSH7h +xWoGNWjKGPfI4gq8aHoTfEof00xZubKgl7GPVFGSDVd7TSf0tyOZVdADGcIojOkDe5MWF/mzQs4D73pMqY5dZvgzuA9ObZjdKCQqlGE/ZFc= +7jSw4GB3eBRbmC1MzdtYH2nxYtjMgz8klBoQVhVoidWwT8r1LlO9NvZUaeVEEEdTW81nQdiNwJibED1dKYQBkw== +ruTTNFTdswRs4Mc+srnRkxKrfMU4HqZxGxt1RxHe024= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +0J28z5Afxj/RLIBl6j3pAgNq/oTIsu+AgqPjVKJEPvv9NLcDpzlB17tpU0lSBAVA +Z8UsPk1Q7HtwjRd4g01ryw== +HoOSkwUjMH/v4OzYfSLZHLuEmVzOc0Jcac6Q7EBdZLI= +wOl7lRII4ZgqRTj9M//0b0Hmiipql0KqOySDsulvH0Su8cx+3vVrHwSELz95Nt4O +yqM179SzW9HnBHozkU+IlZmT/v3wYMBi43GV3/6zTQaofEdG+x7npWVs++h4r59tCne/rOxDxzdL+ulBEDhdXzPWQJHGam+A14zKSyFK8tY= +yqM179SzW9HnBHozkU+IlSh6plWR6D4Za/srbzCBndvZ2MLXINNMDiNewLwvNACvbuwcvh5NzzpKTCtz6o++Uw== +Z8UsPk1Q7HtwjRd4g01ryw== +BIa0ibHnqUboaK9kYHYO4tix8ZPIpY4+eXNZQL6TNBEr37K1gp2hFIgZaVuDpKry +wUvHMMqfJEAtxgmf7WcojEsJZ/q+trcw4QZczEaX1wc= +p6i8iK7HDbvmLpoFrhNyv+jCXQ+9uuqm20drf0JQzoo= +uTEK8Ng11d3ix2pA+DD/aUSvw2QhKfq2muw9OeWLUPkFXMoY8H1+WYM2ZgNetwFM +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmVoIIr4UQdxImXSR4+nP5rPMN4durEeX4gdmJz+ASa5AAk5l7XFK6IcVVpMUMCMJmI= +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYsXheOqb+ac7ivrKomECGJh6sAOIfkvF5oIqf+7bT65s +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +ksF2R1E+pQ3tpguSadwptSjHYbVoil2NE4uVOms8YOU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWjtdfz4jUHOisd7iLe9SzEDuNihpqofFUHBGmLmre0CFX801z657OYko3yu9pDZ2cMrn7XCoaQHi8TBS6BfnnXRgmoO+AsowNhXyROWGVmDQ== +1u+XjG/2+GSQRv6EzCaWRQ== +JbnXIK/axHVmA2plDNFCpMWEt1OR4iOGZbYxwMVSGXaSRClwJ7H8/1TEbkxkYcSscQPjgl6VzrZ1rA8bbkzLPGLtGrShGu4zlnLbt+BiXXQ= +j9BWZnPkz9pNgv1SXK/8DQ0oIRVkw0dZu+9bbZ9Z2fk= +N5/4MVITy7EOJTlZiwcB9agKfbGr368xpyc0rhumQZEuHxhitKliD0GwkptReLUf +J1UGlXkKhKXD36OrXXhN8NU53NrHG2SKWIiwAUeD9JY= +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrQorUkV/0Z4czhLJwFbLtQgDiot4dt7MCR+IyRl/JXPGwvD97k+zv3alg0zn9SqxNU= +NhnIR3Ilo4H2su9/cTNo/Nhao8jRz1mpKHWzTHDhdaWoB5q4jnCTxvFKzgSknYQc +1u+XjG/2+GSQRv6EzCaWRQ== +Ic1WBp01nLyRxbqJnsu2p0K5AVX4XdWNyelWOIV6Ri/Zcn0aKD/Dw9aXBHAKiMi1 +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOEns7mun1htPlaJ9Dut7srZPs8zLx2mnV8J2BNAbKqHDQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +WjK+gflGKYbL+4lozgCawEUyGCXGDdulnbl2dYZOPGVzG5m1Glq7FFUvGH6sdkQl +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P4RgpdUyasZdSn+oHMc2tG5Ug0ojdlQR9cuRD1kYLMhs +wOl7lRII4ZgqRTj9M//0b0Hmiipql0KqOySDsulvH0Su8cx+3vVrHwSELz95Nt4O +Z8UsPk1Q7HtwjRd4g01ryw== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +YdUmdqAC07VhbSt1h6KCDOQhlWQ9jG4k+MaSOV9j4FHOiSK3ETa4U5I3fxOq7tLF/PYmB1e7iFQcBIVDEiWJ0hG8ZfezZD+o0fXjWq7Tix8= +WHkOzVx7seuLmxs5Hu+l/sD6YH+8F+kQZFoPxa6Xx38LU1lUEpTxHz0FS+pwLXBn +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN63t2MnnwkYHotYsrWH9vRICglWvh1CtLK/YdWGbWd1o9f1tzSxswHQJ7jeusy4I/wA== +M0ZySqkmhuHCw6olbCKv992x+E1VMoPx+GJL+VjXXxc= +1u+XjG/2+GSQRv6EzCaWRQ== ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +J24jXQQti6wQv5pCoF2ZrcZEDr9DJDb9XjoXgIX///kKtoJQ30XXawDWakYG93Yo +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/Px0qyBBTm/Di4HeEkA6LeK0wkzA9oyOBq1r4iQdNVJFx +wOl7lRII4ZgqRTj9M//0b258VwcrBXb9+d6gAS2zZVE= +Z8UsPk1Q7HtwjRd4g01ryw== +YPfxe0G+MGXmXAi6U4BEkvyb2XevHxdc8FMThKGhtzEMysaWtPfSjbuwkZA3pYeOFKhCSFYG+dkVrsVae0IfFA== +AJ6YSai03eOKVxBS+s+A+4Cy+lk/ZeoSfixZxH7jcBI= +h6wp9SZ6w+zAVENITafY7hRljTrwoJCPY0O2fxDuqa0= +ieyNFTb+50j7HJWl7nbiRb4GCc6WHw87gawZDTQSQBAN4boyX2ECQxLm67quRnkg +gZ7BoIAABc+I8g/rYzq+fxkznXPQP3aR2IC/o8MbAWFRcDH+nxQt+h5tN0pl/k/R +gZ7BoIAABc+I8g/rYzq+fxpmPvrlSDpm2/4GV4ZL52N+f3aoouaz0DKjSpxvecRv +VmVrGQo2zRokW/ZuO9bN608SN+rl/c82Fx3gEheLtEPWenSoQZ0ErgcGs7SYPEIE +fE3zmgbfhO9ZwEda5GgwD2xm2m7B0YFtqnQCEsPic9MwVxNnLGn04EN6FjhytfIHBDJPSIwM3+fK2/ZA7HX2Eg== +9uF3R/OfNUGVQlxbTNUS55dJWWqKoFe+2B8l4N6Jc2pEZCkxDuC97C1dDY8PNwf/ +1u+XjG/2+GSQRv6EzCaWRQ== +x22JLf5+9uUTDUdpgwgZ9O83E1bfgaVhykmWdJ+nZXJQ9kCBq7GuTG8v1IJzJD5h +Z8UsPk1Q7HtwjRd4g01ryw== +dar6N0tUAb9rU8/aX1b3tPPCrmt+A77OlLLc0hq85JPAMBfiIm5+TeqFwN72RdG8 +ho/Q+jrWDtBeg9J9ZTnKi3+/Efbvadpf+qoH75NuGwk= +3AcPLYoMMn4rxWOUMA1wJdOmdYprhThTGxGyDnWq840= +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDCoYsQLlWzDGUd9QUR41IHk= +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW+2pW4n/W6qWQ7fvKEZcw6lORQHv34KyeA811LNUCOgw== +1u+XjG/2+GSQRv6EzCaWRQ== +gZq1mJvnqxTNLPjSpQ6bfKeb6fxkOx/+dElrFnEieCM= +WHkOzVx7seuLmxs5Hu+l/v5tNYogyNy2bAogm7myZaPcxZDZSm5ntWmgTrqmpiFB +JlxIfINDpIN74VOPKABZWw1pDjtDv1WACEXIhyxtaYCMs+iDnrosPIR34X2MwwO9+dspbEyZ5RinZUNx6/2rAA== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +NnnRSiFB8Vtr5PXra2GmCcwzujRQX5ogRv1ZA//+TDFXEzK++s5GANm+fB4wZ9b1 +T6Eqs+Bi2aLwH9GlPE5AbHo5JCADBnfOf3YzkqXbPUf7ww5fPYa7xY2H8XgXCiUfUway+41gmA7KI8VTyYCtUQ== +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033YGIXZ5LurDt9cgLUto0D+F +VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM= +1u+XjG/2+GSQRv6EzCaWRQ== +a/jv9CzWHWz0h350oJLqa5TTL4zXej8PqERC4mSIOhc= +e6XLp4vcZ1lGimtmqXOZYQEh7CM4Q/OMFSck6/Q+gT0= +WHkOzVx7seuLmxs5Hu+l/vyBEzeQThILDQKrzavtBosY4yeDSrIgmjG8rDYDt7jf +xWoGNWjKGPfI4gq8aHoTfLS/lZvfoJjLfC/marPQVLnKMOw4Z2B4PM+0VW7zItzI/6l7PfcB1fpqeh4CVpOVaAVtXUo8+KKsnSCrzxMrCgqvl15+qXkkBzeyMR1jFwGa +L0eUthVnpkGsmKFAX6d+uEWqqcpDPrCTTj+Voj6q7co3v+1bz5rI1ebO+u/PYVII +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +b4OJVZe8QyIpjuTpKXDL9A== +CUHO82G5MDpSEiqhNImyK/VmwjDkiSuGC7ZIskUb7gD65hKsx8U/XeYWPTotCZKW34T742v7dfkWr7P/TSMaHw== +xWoGNWjKGPfI4gq8aHoTfAsoDN3ZmNarxbTXL1u4oucttlV0ayU3aRT17FqHoXJQDIiBBbSbAmeppasRVhu84Q== +VmVrGQo2zRokW/ZuO9bN62lH7iQuGMYTOSVMNmHFiRJ/Sw4G+R4bn28hJN4qoweYFOMRMpD0ZDoWuodT/c5B7g== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6/hmmducS1PDoVjLNYIkh7yyxYJu3Yggs3y3xm90fY6f +VmVrGQo2zRokW/ZuO9bN6/+pUnGYHu9kYzSmExtaF9xg/93u33jl028+UV8/J7U0fgjyTfXwYVHPB8cx9YfLBw== +VmVrGQo2zRokW/ZuO9bN62pu9LJ9Byu1GJDSPyIAK5YfjdHOvgBbuNyy8GkgPt06Hy9Wawft7wQQemmjSGO9aA== +M0ZySqkmhuHCw6olbCKv992x+E1VMoPx+GJL+VjXXxc= +xWoGNWjKGPfI4gq8aHoTfLS/lZvfoJjLfC/marPQVLnKMOw4Z2B4PM+0VW7zItzIvXmsV0xjGZqPof0yohbnqV2ZK6WX/JTtF6Rsc2nFkVdqaK9KQFfwsn1rzq0uaEPH +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXAb0WFcZv4HhfILL3G9ms3qCgcSR8133ZQaREk3oJxSA== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW+2pW4n/W6qWQ7fvKEZcw6nnotqX+Ka5wYNWQOIeQDvg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +y0vAsW15ZVdHlspkX2eEyTnrhgxq9w3tH5fYg0+3IYTw+fzqXDNLLSILTETgFx1f +Z8UsPk1Q7HtwjRd4g01ryw== +DzneBA92dU5V88ETMYFQ5zTOuyBrABt5a1RgPheterisyI8z8hB6+Xmqaxszc5OF +ho/Q+jrWDtBeg9J9ZTnKi3+/Efbvadpf+qoH75NuGwk= +PRG/YRzXVD8iVR3bzEcUnsWMvR86r5liyWslQeT39Bs= +SadDvAEU7JBQjzqB/CmwTKt23QO8pBBBQpkoyF+cMuk= +Z8UsPk1Q7HtwjRd4g01ryw== +YdUmdqAC07VhbSt1h6KCDCoYsQLlWzDGUd9QUR41IHk= +2qQ1+UjgiO2OZgsc3Luuhjl38aA5wDGtKWrWNIfYtrBlHRw9FCPi00BufKOnIQnC +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +p9oVsBgcyiBMjDb8CcPOqB5miGjA7k/rR8HoMrfCBh8= +HiVf4YuHR4gw4rFUUeRvK5DcsI9Cfqv2qLzm9sb8unefn5AgzArrWdtqSFcj8Px9 +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033abQzol1FA4tuwFJraDvx2U +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrSo1ZAQ1FHnoPT0QBpT+y//yTdMi/PIio5xW01+SMmEPbzHTSWH2AcPKinWHgjPuzZic3mlhIGEMpw0WGyqlHdR +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +96gahBYKYgdoVkNtss7VEPgCInqEgx8v+sRhs1WPpZm2HnAMO4XvsEY1xpE0BXSYmvq/MLRD+JYKbipt7HmjYeBg/h3NBuTXdf9BwGtIArY= +SY9CIZHY29IN9pslG93gZg== +O3CUgrw2GJfB+mDjH5+NdtbkdTJdRhl+51HFBnR6kaH4/6DbnYid4U0U1SHEYx1n +VmVrGQo2zRokW/ZuO9bN63t2MnnwkYHotYsrWH9vRICglWvh1CtLK/YdWGbWd1o9uxYVCWxZnh7Y3ENahYaMPQ== +usrsES3VVkgHW6tBbEkpHTeLztsByVaJibbFv8kSFIM= +1u+XjG/2+GSQRv6EzCaWRQ== +P8KbMTjfuhARpOL8NoffNq+3uZDJUpCiwqTVP/jFFTbQzIztXfbek7BVBaXkRncL +1u+XjG/2+GSQRv6EzCaWRQ== +UkQQaGHg2Aow1Gtc9bKBau0euapoYTSumuYNb1Ly8sk= +NhnIR3Ilo4H2su9/cTNo/LBal3Bb69aVfp/VrSaJBVopEyKAKnVV8FGgN9p834dg +IhuisKim47k91RVt8z8qtJyWLVz+KjhPuZWupljEG6xxx85dlt92sHkP0dH3RDilXI/lJNqFeQKeKpcIDtD+IA== +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs8AGlXEXTs+FitgdECPA9/lrzW8srVC0vHK9JhZxKlpY1OTyVkakThiIe7k7Hc/3yZ2FjjNKgggOZKRyD6HAzHN +2+RceSNjD2iq0sRzepMysZEtAjuWiKl7QaPq1reOZ3L3mfen1JvnD6MPvKsBpyOKkDYBCw5cZs24QV3TsSDbQcyqIrPAseuaKXpl/OArmSc= +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOERocNC1mWlogqCdtng5segh8OjwBnGkbvs4sXKYd75Mw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +kkhBrrZfa9Lz2ARtxqQ9WISHe4SzqYX3OdpNmbR5L/VBHTenO90IETRFsXol/Jpv6kfKAQ8LAS+ijYGG5x08+Q== +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIllG8L0Za7F6wo/ffbPvOV8mvOIi5W6Px2rfj6NO9ykAT +eeFiPlXUI0Fos82HYztSmZyutxdoUP9KbXnyyGYMqohKcgTCAEZmONzPn/3V/XeU +bWa5NLmMcRfP7/pL4TxwVSNHz9M1H68NEGnEXfZWNdlCgk9i6Y3xP89ubu7Qieu3 +eeFiPlXUI0Fos82HYztSmZz5nVydpyguF0WdL2JyDxQ= +yqM179SzW9HnBHozkU+IlRXsPW+yWAUAvWc+nnoZsRTppRoZDJSnb51AHNRYCykW +akHibliG6j9uvBEg96vRer/Fg5zTlM8oj8hSAsYq018= +Z8UsPk1Q7HtwjRd4g01ryw== +UxWVPzm6Eag/KO1azIHUdx+idAdS+rfPARzR88tVbgYdpuz2ULTF+2vmnBiei7OW +Lb802TYIO8cTPX7RtIYGZ+/BDmfzYBEomwEqSDkd3VI= +b4OJVZe8QyIpjuTpKXDL9A== +I5csN8e3J/KIFkMa5t+SJATQ2qGW0wOfRPtcA5bj1z8nKyxnKr/hfHKXbqiTXRny +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033ZLNQ2XZF4KsFXQ0AJ186dTqy0tFe9yVaa5Nvi4GzvRQQ== +UiubnxD7y7+jD/CLXy/lIzUv+EQIUnxp2+tRaByCudDYzFPn13pFELSDNAgNf0aY +wI4EEWzdSnynhqFxMCzFmvtQa54ffiTU8OKIuM+mUtbRQW9n1JFAWLuXCST3voAI +GL/yLvMwjflJNfDPl5ASjfzFHnOGqpWC8vWJqX+sGuU7UB9p8iAxBS67EStoox2D +1u+XjG/2+GSQRv6EzCaWRQ== +P5fFy0VqKNQcixZC6/gotxklJeUYBp6qUkCM0szi5po= +3HDkd2BFYLPk4K73U5D+BYdx+YPBx8edbX2mUEZUhcw= +gZ7BoIAABc+I8g/rYzq+fw0QRhyOskLowTpBMxzNkkXJfHhElpIZe5wsCdqDFCZvGpC72uDw/Cu+D1FuT4o1ew== +NhnIR3Ilo4H2su9/cTNo/AxXtCXBAtO/DWPyG38GkWo= +SXXsDW1aLF5ljPyJUoRc7t2uV1Ea/KDPsubfQRA1/g0= +i/p1vQ7ugXGBvwbjECApz/Cr9YvVAiEjIabMp4aMpkk= +zo+FVrlHkUeZ8fJX4PVhAH4W08MspSsE1ssI18UNPaA= +1u+XjG/2+GSQRv6EzCaWRQ== +wlLHv6kT3Q/RmtMBN4nDAaQcqH83V4KNJSetgkXN2mpJpLhuyLAaUNt+tKkzaLSI +VmVrGQo2zRokW/ZuO9bN632VflaKbWaV9Wa8IOB78Qk= +VmVrGQo2zRokW/ZuO9bN608eo5JjqlJJ+hYuirBCFcmf4cKrfv149X3GuxINE1xDDJnAj+I17VZiW+JCiwfRwA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67FMFupuL9NnhZUvLdV1qH1jDx/7tB6qL1SG7Pqfwsin +VmVrGQo2zRokW/ZuO9bN65qbK6/I8bncNNV3Fr84lp9CCx8VYIApMdX4h8SHWyk2 +VmVrGQo2zRokW/ZuO9bN6zEL40rbK+o6j49cNVbzKqOsExNYZ+R8lUnZEGfiuhXY +VmVrGQo2zRokW/ZuO9bN6+EP/UfvFvq8hDmkfhk2A+qnnPSHBLYJiyMbFizN9aWEoBeZFZoLhXy50+Ts9Ur1/A== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN62OM8cyvjkpIHunHzlmyApZDgMPN1CpEHj7RB29QdSFu +VmVrGQo2zRokW/ZuO9bN6zDu/0cPxO8iIAxo/MyAOyPtJNDgANrLqcOpy3L5oY1k +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklSomvH/VIQjQOrkHPd2BPf +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklTf9Kt2VbDsiUekTyaR+Eqep0h0uyOXB1iMKSg430G3P3HOaEPs8YtIeaBmiEO8lwzs03jh8/eQSyBAoQdtToH +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkI9vh2RQuknkZgs1Cqb07GeK7Ac8sa2K/nX9m0GTqvYw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69jzSpHadiEPTz774Uy5h5Nmsum61eVUSkdR5LKSFtye +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknaeZX5axwNGbFayb5AUV0mdbRkjhMqcaUp+9h1iRD5Jw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hklp8xpCS2Qa6hQCt5qnOrYkkwQkeIQCwQPcHEA9pzUHDg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hklt0syFuEeIYE3//UfiwS/lcyJ5NAN5s2jGe4Vc5xHKeQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkmzh7YNQJ+XcGeFsDcaBjSz +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknMDNdxIBFQcIw0RGPfFMjZjfh19zPlQyTadlR6rEMpug== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklDS7vlaZKw8aBp1UPbgthY +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmbQcBvdDeaM0tbHayRHb0Q5oDsO40qheoE5NHoJ7eApdm6gYH0+XlN/FV3HdO5VZw= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmO4Xe0u+mz5wM5yC6Q+Zhs +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmpDk5452BsZTuBSuOpo5yN +VmVrGQo2zRokW/ZuO9bN69IgslanOR1qBWJ7u9f8kApHl7m/3x/YUX5TmvRhoWWrhrEvRs3e6Vo03iY4FZoGJQ== +VmVrGQo2zRokW/ZuO9bN6yAU/fW49cRu9QdgvjUnDRs= +VmVrGQo2zRokW/ZuO9bN69qqX19RJUeMAQ5O8TuJCvc= +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN6w7SLxNKr+6YhBMvtqYtIS5B39SOQ9awYMdHGNN7g2HF +VmVrGQo2zRokW/ZuO9bN61ftGV3vPtB2WjF6YrN/0jNHYRNif5kQHZWzQEGp4Mrw +VmVrGQo2zRokW/ZuO9bN68i9AQUAyLa5Izsu2k/9DPtGLI71suphKNgMpp1nHjp3 +VmVrGQo2zRokW/ZuO9bN60FmyZq9YoZep5W4V0xLs2VJw4EwR4WqghfwBKZHKkzFW5QCRYRsMStsmunHScuj3A== +VmVrGQo2zRokW/ZuO9bN6zw+wLmacHPHBj00bNmIGZaQr03AAq32yLrsoy6EYs3C +VmVrGQo2zRokW/ZuO9bN67c57efC+E7a6G1EN/4VXAUA5IVAU8G/FAp8AnNhV+8pDSYVAZUG09Nqr+7c8CWyvg== +VmVrGQo2zRokW/ZuO9bN6x9SPCVoe4PZoqZ6VqYN93GZvfLrMfeSjpwDGE6Mez3z +VmVrGQo2zRokW/ZuO9bN64ApqCXcODvD57AL9oh19dsQTwsNrJDaw5mMC3iBkcYF +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn5CJpDak47Y7NTLN+SVG+JjtSLj0+GTdSHjBiYnUk9ZehN+mSf/Rh2eO5fYqxzSaFaMrHoUfHlS3DLFMFIxzNoSLKwxQP2/fXAWKceACqodA== +VmVrGQo2zRokW/ZuO9bN69/FjVndCR4PL8NhQnFk2GePSUVga2CqkdtTIa+/EIW/ +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklDS7vlaZKw8aBp1UPbgthY +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmhycRnROwAaTF+9pqngLGtjJZz0E57qXffjBTU8zR5ws/8WNuh6zqeTDPmSR/EwdRf/kscgnRpkyfDhI32MBlHgF5oG/UTLtRJTD7kKhRtdQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmO4Xe0u+mz5wM5yC6Q+Zhs +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknbl+0QAy/9I0gZXTmE2tktHVvxDseM0i9LrUUswPEB0g== +VmVrGQo2zRokW/ZuO9bN6yZnMkqRvl1W1z1JA43ooyLsysqnBW0cSn//FgR0VFUn +VmVrGQo2zRokW/ZuO9bN6zw+wLmacHPHBj00bNmIGZaQr03AAq32yLrsoy6EYs3C +VmVrGQo2zRokW/ZuO9bN66U3/6225IbLyIW0FVhZRNEyLZurMXbjR98/i0DXR7zf +VmVrGQo2zRokW/ZuO9bN6xaf2vdYGOU85+dskGN5Zti8/wz7YWu3y/jTkefW+oHw +VmVrGQo2zRokW/ZuO9bN6xkyZls2bS2c0K0h3aqZRdZrjes6+0hX3Wr6T+0mnqitWU1y3v2I2Wt4PYH0XBjT6w== +VmVrGQo2zRokW/ZuO9bN62zrDNonCkxA5BeQHJAPUOvVeRTu47AJZhJxjTJXUg6D +QPiJZBcZo3Zu5kDhrrGzbI3GQxTKN1tlSYOd4wqk4Qk= +o6QhOIN2Sc4SHELnst17uYG57hDyRaN+v5d46f3m9TtvuM1edZME3sYQm1fwKXSE +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTM+LVbDQYpuIwwcEKVLExe6MC8BJb9x5uoyJJlFncEM6rY6CjjYEoI1lGYiKE1nBng== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +o6QhOIN2Sc4SHELnst17uf4l+kb1P+/cnojpDP8CeKM= +ACEEdgL/kNbx7RaZcSRxZClHBggmOTwuoukB6MgtasIiEFle6fCvd15573nsLbI9 +VmVrGQo2zRokW/ZuO9bN67Pocv3BMn87TAB+S8aqZRSL3g30PRHmEkGas7a6CXo4 +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN61efkqHAT/xspGlYy7KhUnGfupbfN6kadJV1FzZSIvpa +VmVrGQo2zRokW/ZuO9bN6+b0SyPGp1VLLfFreWS32CdvJQ0lilsNKGrnKkyex2l6ZoEN1nB9tzGcgrcEFt4mWg== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6zpJcGQDok2bzZbwZ5+2z7FMKwUs1r6SjvZQDsl9E9LF +VmVrGQo2zRokW/ZuO9bN65HmUSl6PKrPLdi3pIfWJgEYDxdXHdYYYJAWxRhWjvkMMBYOgdKbm+/g/JVB0KXlVgkWFQ0d19yx2HQHDjijpFM= +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN65HmUSl6PKrPLdi3pIfWJgGfllFXAtMFaX/YohJyQyfhG9xy/eo7UEqOCihMHvYuSYyU9x8O35sNRwdldlOqO7VLthK8cTiezk0qcNGLbtsxNx/TEJZE7IDz5uyfUbO2Fw== +L0eUthVnpkGsmKFAX6d+uA6wgjrZ2VgEjfvSiyQz6oysGP6R543HWJTxMrxe6FdC +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uGu0U7q5yxq3pNh7Qp1zdP4= diff --git a/class/filesModel/rarModel.py b/class/filesModel/rarModel.py index 923894b2..7521525b 100644 --- a/class/filesModel/rarModel.py +++ b/class/filesModel/rarModel.py @@ -1,243 +1,243 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# -#------------------------------ - -import os,sys,re -from filesModel.base import filesBase -import public,json -import zipfile,shutil -try: - from unrar import rarfile -except: - os.system('btpip install unrar') - from unrar import rarfile - - -class main(filesBase): - - def __init__(self): - pass - - - def __check_zipfile(self,sfile,is_close = False): - ''' - @name 检查文件是否为zip文件 - @param sfile 文件路径 - @return bool - ''' - - zip_file = None - try: - zip_file = rarfile.RarFile(sfile) - except:pass - - if is_close and zip_file: - zip_file.close() - - return zip_file - - def get_zip_files(self,args): - ''' - @name 获取压缩包内文件列表 - @param args['path'] 压缩包路径 - @return list - ''' - sfile = args.sfile - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - zip_file = self.__check_zipfile(sfile) - if not zip_file: - return public.returnMsg(False,'NOT_ZIP_FILE') - - data = {} - for item in zip_file.infolist(): - - sub_data = data - f_name = self.__get_zip_filename(item) - - f_dirs = f_name.split('/') - for d in f_dirs: - if not d: continue - if not d in sub_data: - if d == f_name[-len(d):]: - tmps = item.date_time - - sub_data[d] = { - 'file_size': item.file_size, - 'compress_size': item.compress_size, - 'filename':d, - 'fullpath':f_name, - 'date_time': public.to_date(times = '{}-{}-{} {}:{}:{}'.format(tmps[0],tmps[1],tmps[2],tmps[3],tmps[4],tmps[5])), - 'is_dir': 0 - } - if item.flag_bits == 32: - sub_data[d]['is_dir'] = 1 - else: - sub_data[d] = {} - sub_data = sub_data[d] - - return data - - - def get_fileinfo_by(self,args): - ''' - @name 获取压缩包内文件信息 - @param args['path'] 压缩包路径 - @param args['filename'] 文件名 - @return dict - ''' - - sfile = args.sfile - filename = args.filename - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - result = {} - result['status'] = True - result['data'] = '' - with rarfile.RarFile(sfile,'r') as zip_file: - for item in zip_file.infolist(): - z_filename = self.__get_zip_filename(item) - if z_filename == filename: - - buff = zip_file.read(item.filename) - encoding,srcBody = public.decode_data(buff) - result['encoding'] = encoding - result['data'] = srcBody - break - return result - - def delete_zip_file(self,args): - ''' - @name 删除压缩包内文件 - @param args['path'] 压缩包路径 - @param args['filenames'] 文件名列表,数组格式 - @return dict - ''' - sfile = args.sfile - filenames = args.filenames - - return public.returnMsg(False,'RAR archive files do not support file deletion') - - def write_zip_file(self,args): - ''' - @name 写入压缩包内文件 - @param args['path'] 压缩包路径 - @param args['filename'] 文件名 - @param args['data'] 写入数据 - @return dict - ''' - - sfile = args.sfile - filename = args.filename - data = args.data - return public.returnMsg(False,'RAR archive does not support this function!') - - def extract_byfiles(self,args): - """ - @name 解压部分文件 - @param args['path'] 压缩包路径 - @param args['extract_path'] 解压路径 - @param args['filenames'] 文件名列表,数组格式 - """ - sfile = args.sfile - filenames = args.filenames - extract_path = args.extract_path - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - if not os.path.exists(extract_path): - os.makedirs(extract_path,384) - - tmp_path = '{}/tmp/{}'.format(public.get_soft_path(),public.md5(public.GetRandomString(32))) - if not os.path.exists(tmp_path): - os.makedirs(tmp_path,384) - - with rarfile.RarFile(sfile) as zip_file: - try: - m_list = {} - - f_infos = zip_file.infolist() - f_infos = sorted(f_infos,key=lambda x:x.filename) - for item in f_infos: - filename = self.__get_zip_filename(item) - - if filename in filenames: - spath = os.path.join(tmp_path,filename).strip('/') - if item.flag_bits == 32: - m_list[spath] = [] - else: - if not 'other' in m_list: - m_list['other'] = [] - - dir_key = os.path.dirname(spath) - info = {'src':spath,'dst':'{}/{}'.format(extract_path,os.path.basename(spath))} - if dir_key in m_list: - info['dst'] = '{}/{}'.format(extract_path,'/'.join(filename.split('/')[1:])) - s_path = os.path.dirname(info['dst']) - if not os.path.exists(s_path): os.makedirs(s_path,384) - - m_list[dir_key].append(info) - else: - m_list['other'].append(info) - zip_file.extract(filename.strip('/').replace('/','\\'),tmp_path) - for key in m_list: - try: - # if key != 'other': - # dir_name = '{}/{}'.format(extract_path,os.path.basename(key)) - # if not os.path.exists(dir_name): os.makedirs(dir_name,384) - - for info in m_list[key]: - shutil.copyfile(info['src'],info['dst']) - except:pass - - shutil.rmtree(tmp_path, True) - except: - return public.returnMsg(False,'Decompression failed,error:' + public.get_error_info()) - return public.returnMsg(True,'The file was decompressed successfully') - - def add_zip_file(self,args): - ''' - @name 添加文件到压缩包 - @param args['r_path'] 跟路径 - @param args['filename'] 文件名 - @param args['f_list'] 写入数据 - @return dict - ''' - - sfile = args.sfile - r_path = args.r_path - f_list = args.f_list - return public.returnMsg(False,'RAR archive does not support this function!') - - - - def __get_zip_filename(self,item): - ''' - @name 获取压缩包文件名 - @param item 压缩包文件对象 - @return string - ''' - filename = item.filename - try: - filename = item.filename.encode('cp437').decode('gbk') - except:pass - if item.flag_bits == 32: - filename += '/' - - return filename.replace('\\','/') - - - - - - +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +Hf+bBTgRv9pVfall8ODpvg== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +1u+XjG/2+GSQRv6EzCaWRQ== +K3QJU/hV27C5FAVoZ+avNNMfDMygeonfI5TFnvUHjhU= +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +hUVhKZtvg+FKessuieMZGkP/5FS8EfJsx7HCvhsTdvM= +KbrESL0Rr2jC6vOleUlueMUgBS8ubHMHEf/dewOmXjc= +f3AsbhS4PaN1B4cUkY9T+A== +7slA6OC5VJa1dhNh0P/HjCGJmXVn2u3xkCMlnoSxSoA= +MMZtIZZuuuWy0CLBzKekTg== +0cnYXLPu1n3c2VaQ3tVc+NKPyveC0ERGp532EiLQZU5gtpnLWSMk9qn8OtVlUlvH +7slA6OC5VJa1dhNh0P/HjCGJmXVn2u3xkCMlnoSxSoA= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhLTeka+iJoxis1kUHdyggy+yXh3Xc8xsyir/5NbKP+Sj41j2UHgL27dZFRsmbh/P+Q== +KZTmaJLp+FU9X93j5Tpqtw== +0LFyB+q/4AUBkCrX53xYJedFoNZHYELHOLu/R25oBrqozNv9g917+/ANMSb6X+WR +yqM179SzW9HnBHozkU+IlaI6kf6Fax7Db/qE4wQMDFqfwVE+4rG3+IJ9S3c+p3Wf +akHibliG6j9uvBEg96vRehUZmhX3a+KyYm/Obf/H764= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +1VHvxkEcO4espNMpWHmnH1EgArAJXS2MAairjwNzV9c= +b4OJVZe8QyIpjuTpKXDL9A== +wQn8ViRVbUL8OLSIgBD0IAeRdK41R3m/PKt9hFbBzr88nFZgyZVILp1bIMP5rZUJ +usrsES3VVkgHW6tBbEkpHTeLztsByVaJibbFv8kSFIM= +1u+XjG/2+GSQRv6EzCaWRQ== +l+sL2BKY+z3yDMAsWI51b39VWekdKmbfrOabwvVHTbGfiUobxgPhHW2OIZyeGUJ9 +wQn8ViRVbUL8OLSIgBD0IO53Ya1+ckG2IX47+ykaGJU= +1u+XjG/2+GSQRv6EzCaWRQ== +MxWDzXEQjj6mgdfarjHFtAA6unJa5l9pTo0P8ga3JDU= +1u+XjG/2+GSQRv6EzCaWRQ== +x41zVQ6DCtWXndDIq7U8L+5quM3ARMCnVm34NUX7l/TJzZZ100EvdUXHXI2cFH4H +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P7D7U0y87ntgFdQV5REShmme9ACxsySMBZr2gsEQniKL +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +akHibliG6j9uvBEg96vRer/Fg5zTlM8oj8hSAsYq018= +KZTmaJLp+FU9X93j5Tpqtw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +1VHvxkEcO4espNMpWHmnHy+YX2/U7Ip4Uqt3suxNhindpukzgQ1aM+PshcW/1dV1 +LuO0LAjc92aYQU4tSUEvi4x4NJPWR/eKWjRfKMSRL8o= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmV/41TA4054wrqxXx/Q1i8cmtFJNrxypDZNiWIVVjiYLA== +1u+XjG/2+GSQRv6EzCaWRQ== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +ei/1A+2pKZSasjYF/t4qojBnh7Bi8Fen8Fcf8mKjT9IuWg8ZCZGedub8Eqjf8qUh +1u+XjG/2+GSQRv6EzCaWRQ== +4nsuLyRybkZg8C3fJCsRPXDWwLMccXwEpFBdFK0JOvg= +ZFD6pnBshb3mpu7xvvuMLemrazA4ssXDDuW5SINX4ZLsvLIwaJ4YyDANze64OGqUEj7NCJs+t+zkcUskHr+gqw== +1u+XjG/2+GSQRv6EzCaWRQ== +g5xFPP9LKBd4uktrRYzGf+p1NdL+k6huN6CrzYXY3fPVQzhtKStMTpRTZpRqmhGR +wlLHv6kT3Q/RmtMBN4nDAbqo0o2IA7mU8WVNqZvoB5I= +VmVrGQo2zRokW/ZuO9bN6+qRbBr2ygG3RGcAIOk727ylzODmV8VRqZ4YKvduSUCu +VmVrGQo2zRokW/ZuO9bN62psx0nYMGJaQ1S1BUtE6l8lgsopQJ3Qf5syQpAasAAf +VmVrGQo2zRokW/ZuO9bN69Woqo49I0Fga24Nj55N9ZL81I98txu56TEAiJ0hswK9 +VmVrGQo2zRokW/ZuO9bN61e1I6gZaVwmbMEHc7jd9wFEHpeilcxXyCkd5bmWx/yK +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69+qNrAfkaoVno2zP/PIMh/5aA4AiEej6zodAVzZFzS0 +VmVrGQo2zRokW/ZuO9bN6xT3wQu8/ZV37F/D/EekYn8LNRE5UkGbTG1UxYqNO1dyU2pay/FWt6r8mJiKZ4UU/g== +VmVrGQo2zRokW/ZuO9bN64KhHtDRs0SmcOS1WVIbDs4jrgwkpIqneJssR9hHHlJ3UJd9tZzLLz6C+UQLUJWWGbgxXo0UEEapI14RLzluAUk= +VmVrGQo2zRokW/ZuO9bN6xT3wQu8/ZV37F/D/EekYn92m0FWAHkcFEifdcSefZI3 +VmVrGQo2zRokW/ZuO9bN6xQU/k6xhXMbxscs7Gix8nNqUkjAknQH3LrGVvAyjYtQ +VmVrGQo2zRokW/ZuO9bN6xOutEc9teLv333RLphPxTSZMBPTZ5nEHEAZCqisJAYdx733inB6pPVPHnS53cxkslRzdzhPLD4mmySs0XcaZo8Yb7xMuMGGlmbahCRF6eC3yiiMtXXp0R7BViTpiJ04fqWtzkElISMu7z4pkkwr23TjUMjTjCelUG9Lwgmt+EiK +VmVrGQo2zRokW/ZuO9bN6wv73IH9kSzXKMUmUPacSk1KDsdrjROzTN1kzduuP0l3 +VmVrGQo2zRokW/ZuO9bN69lJPu1G3iX01kmj1/0ynPc= +VmVrGQo2zRokW/ZuO9bN63p1oaV/8/rZOHP3bxdBsLvjL/YDIWVk4lhx62fOdGpioi6WGXoNG40BaJlgn48VjQ== +VmVrGQo2zRokW/ZuO9bN6/kiIWH8hJ6dJOo/fnNj9VLcBHtqpOZ/Rc4AjXcEm1edHKkbtiFAlXrzE+rfp/AeMQ== +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN69+qNrAfkaoVno2zP/PIMh80w0dM3z0phdMS+lT6j/xo +VmVrGQo2zRokW/ZuO9bN65abAZ39yZLyKCl4nd6eov4HdKauw4+toNc5YoXvsAeW +1u+XjG/2+GSQRv6EzCaWRQ== ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +J24jXQQti6wQv5pCoF2ZrRvdf2ev6P6vInKuFAO8eQfewP9/G67aA1dZDLRwjari +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P7D7U0y87ntgFdQV5REShmlP+1p+XjT1dvUjFUZHsqC6 +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4um2nnLtPSMsHb7nUhh0kKQ6CdlvwTSeB4h8LEiuwG9t +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +YSlit6erKXylrjSZnvyW8I3FYH6DLcfYpMAlko6lQqVBBd1eDeDGaujrHWIOJkcI +YSlit6erKXylrjSZnvyW8CCccEStX4fvFvduMtCGi54= +d2Ww7zhXROgo6Qq8ml3vsgVOxob/5TKf0KTWSmheRnqYA/wEs2DzrY8Dpm7Fq44lbdZ2N/U6naG4DD7karinEw== +wlLHv6kT3Q/RmtMBN4nDAdAMeIPpwO0Pgawv937x0Zw4iwEVDD1EIlJA37GxF8mZ +VmVrGQo2zRokW/ZuO9bN64/tCOCyKu41lsl+3BVlFMtjVa1DXtvHbVFGS5luzEIFtQp9eHGpneVvKz65gzXCXQ== +VmVrGQo2zRokW/ZuO9bN69Z4WTbfjflDn+PHJe4HyeBCMRIQIdsxtS/SkQUyTK46 +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64IUBVOpZieySJUi/5Xp7IP9i2hr//841YnIEqNDctKsA4PCQR7FmaZMQHs7tdC+NQ== +VmVrGQo2zRokW/ZuO9bN63KtgQxYoQMLiw/swXgEzq6F5dbUK15QUQFhJ/fqYDb2bGgnr3J8HSW6FkIPXw/QAPGbSGA6euyj6sY6jr9uD88= +VmVrGQo2zRokW/ZuO9bN60+Gj1vDNaFgHQa9qY+WYQ/+8quDVQ7mW+ZFMiCB6dK3zTXaMhAFlgEsY2XTlgH4hQ== +VmVrGQo2zRokW/ZuO9bN60dNkfoHcjUpCVMrBt5WSA9PCuYzYl0QLyyWqlMopPgY +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +6JhaWzGWadTJ3yzCk9OQNgkLy/m1ahTUuEy+6FiV49HNwHkAN2ZMxFTsufeIA0ig +KZTmaJLp+FU9X93j5Tpqtw== +dar6N0tUAb9rU8/aX1b3tGumQ1hhfk8O6qr6gmiQ1+eBvZwFkpECn+RP444+i510 +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMlEDkaPg52QqeaL1e16aTiZGdP9MF1i1K/F0icaNF/FRZY8CjFaji8Ecm2WEpXZbMiyduIw57U2lMT6kzLqch3Y= +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4kqIZZgv1yXQHpqfJ4aNJ54bUNUYTYq/0A+dhHby52Au +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGJEf1Y6VNXDjyNkW1vEjRDaMkNWHl1CoN0rBoEBVHY67QP37rmJsbUJ2x6ftwGYtZYSf3Kk0VjMvB+nFonZq5o +1u+XjG/2+GSQRv6EzCaWRQ== +O2CqINTuIULxX7Gu5GskT/Nm15YFovaVXDj8ylNfQi3d2Qbp7uZYhyvrRE1b06DK +KZTmaJLp+FU9X93j5Tpqtw== +t987AUZxQR72IPRcAqvQ/tuxrpVopv9MhEHHX4HHOFnF6WZD8PVxYK60YbaBpzoa +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +c1XU6vxl7MsQKGo3r4amMgGxGyqG90Nz+St+6838nV5Hy3ZAmDi9Ecxx2gyS4yZO +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4um2nnLtPSMsHb7nUhh0kKQ6CdlvwTSeB4h8LEiuwG9t +XKWK9LfYDT5w2LEWWxwkc6l2k+0eByijzzjX3gaj5k8= +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGJEf1Y6VNXDjyNkW1vEjRD4/QxBrfqq90D/lFkSa20blyxshNDPqg3WrN+80ZCLiv67pSFMB4Brq3qmKK1ZCax +1u+XjG/2+GSQRv6EzCaWRQ== +D5nlraEpeGmKL9lxoq591txjX2tGuFl4D7WJZ/91lnMQ6WZrDXvU2okqtgoaqWSS +Z8UsPk1Q7HtwjRd4g01ryw== +VcOpbkIFMz59VlIHGus1M+TodC/Ia97E/RilXZKKBR29V7X/RlCXQzuWibEOEJfo +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMjvzqCkQbmHuG+orSe7QiThO3NRqf8lDLQklcqf6u2x4yWOeax5QyyroyrE443cDvw== +c1XU6vxl7MsQKGo3r4amMlEDkaPg52QqeaL1e16aTiZGdP9MF1i1K/F0icaNF/FRZY8CjFaji8Ecm2WEpXZbMiyduIw57U2lMT6kzLqch3Y= +Z8UsPk1Q7HtwjRd4g01ryw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4kqIZZgv1yXQHpqfJ4aNJ54bUNUYTYq/0A+dhHby52Au +77Mf9+1xBvZiUeNTtU/s60Kyn+UEu/OgKFt23E+wd8BiTmgbWFTzjcyPW/imPN9p +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYmSt8oPS+2lwNla/wCY62SBZ7HAsGrtjJH7dzi+5hXSr +NqhMbY8+EQI8zhTG6Zh2I5csQ+/eG6egKX2sKZJStJZk02bOBi20A8UHGNy6sybt +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP263Ug7hf7sm5bqxRn1G7j8CZM6KmOHJcbEZNXo2mkEA3MxUp7pEuHhp8IfRMvwD6SZPogdTtHhT5unY8iIrUiU6GaMq6uosuXq8hjxV9nUgwa2iqC1n6Ziz6tiDdPy1mw== +wgR07xfoapmx6eEnFHXXYm7IOJ/6J1NThWNK2EdPRzYwWPvLP+jlqDp61mwE1mzG +NqhMbY8+EQI8zhTG6Zh2I0BiMnuwr8mLUTT6rraGmzoW2r/izQNInq2UmHNhRbv+ +1u+XjG/2+GSQRv6EzCaWRQ== +d2Ww7zhXROgo6Qq8ml3vsgVOxob/5TKf0KTWSmheRnquRJLMVVVt2mgyaZFKv044NjiViQAAPm85lrCa2jWZ2Q== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN61lflwobhmxel33RwHEAzsw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67AsrXUPIcMWmsNJ6skMzcKlx905HWgBAV8CDz0otLpU +VmVrGQo2zRokW/ZuO9bN64dz5HIueoVr5d2RnbNUIG5nWBmTbO/nPqZZ/WWusF8B2fWzSHjB5SEduhXLHWwpbFrG7MHMtEZh0rgim/Nvx4Q= +VmVrGQo2zRokW/ZuO9bN66NLXvUSFhBjtRrUbZsPQNOrrRiOaX/5u3wb0Rfqy+pv +VmVrGQo2zRokW/ZuO9bN6wsF8dCAuyXrReoI3w9Yj8ejyyRZrswtbKGtDKu6ywDGElhkS5s5y7BVqVo+FqDhvA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wOXhaCTOJSjUCWUfjx+YyYNeSRhAnvmOzc5j0wt6pWe +VmVrGQo2zRokW/ZuO9bN61ODJTwuVdYa2e77kV4UyyiKC6xmDTYLpzmUEL6iaKIotT6XRfjdpuPhvefHRzC+7lCYolxcDYktKmTY39wnf+k= +VmVrGQo2zRokW/ZuO9bN63p1oaV/8/rZOHP3bxdBsLvjL/YDIWVk4lhx62fOdGpioi6WGXoNG40BaJlgn48VjQ== +VmVrGQo2zRokW/ZuO9bN655297gigIv+4Ufy0bubbx7ffTJUoURR64bKe908x4ap +VmVrGQo2zRokW/ZuO9bN62cJZxyhxghVn78AgYWoneU= +VmVrGQo2zRokW/ZuO9bN63LTkM1jyAeOymc1jas3GbdSOgZt1gFP6QuzR+PijaPfkXI6n5+LDNetR25SN0GXgg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkns33MIHJ07fAs3bEPA9prEegTrYMIIXPrzC4jD9xKXsA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+VE/nQEHJOLSC1DnxvG5TiVhVg6gVuqdGuJQhlv12yWuKoI97jUkrKrXkDjch0nxA== +VmVrGQo2zRokW/ZuO9bN66Stm18E9cFphVzr4DVvkp0eMQP2KdIRCCvQSRwvi/ogRLfVX0ez0AMMWvL6BswFfzkKtdTc18Qoh/tgpwV2o+ISRqfA+Zum4wzK337qVQa8uzWtdDX/DDCp8lweCdFZUw== +VmVrGQo2zRokW/ZuO9bN6zsPJOFHzvayEpAPvGiJ0lAWXca0TugmxSi8hINZZvoHQ1Y8EIw5ZU/3qx6IfU7mzw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmgImK713UKKRnRLyQBV8gyjNySqhD1ER71FyIggw6WuTb4tOz+zOmUJvBAa9I7hKop6pGwEoB2371+83RgakZhyjMJWosIoeLReEb5GcqHNA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkrikeB3PyYnRyHWKvzXceKTTsB9Sbgf9uM/v0isHHFakzQxpTSrdsYuODmqyQGE3Q= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknz2So+gFu+BijKlvPtsNKCIi0RkX1BFpd3GhHKBMdoB6DedipwufkrnDIRLB493gRtMiXCiHSsTBNmIXqRl2RK +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkqG04WXvd0zYTVxLMDUCWknQtobuOLFvBEBrZieoy/MQ== +VmVrGQo2zRokW/ZuO9bN61TKHwbNGf/2Ub0vd756U9O0Gtl0vapnf7HpdYx4U2ib +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkk6a/inNeVFTn9NarE6P/3LG5P9KtqHegfYsmaf6Actxw== +VmVrGQo2zRokW/ZuO9bN67FJHoI0w8vvrPd3RbddRrD2Em+ygcQFnOdLaXNs8ffOckKjgP3gK8cG/TTtMkpW3DL4StNcG/wbkijZS1ecn2uSboPx9VWF3PIGue3h88Yd +VmVrGQo2zRokW/ZuO9bN65aFRI029wWEhVZc+cvXTRpzda0UlKTw9izNui/xkgdD +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN6yfyDpG+MDWCM+I7xrm6yMNHAsN4/aIr7LH++fbWW227 +VmVrGQo2zRokW/ZuO9bN61RP3YbpVjqNga0la0+UFBO8any5NddZFrxwPV3C0M24EIcWBMjLzVvJvQ44t1iabGq/xokqYehL60ehDQbn9KxCN13a+8iVaZY1iG866Qqj +VmVrGQo2zRokW/ZuO9bN62eRNBkbzSoIoWrmVfjVYrLET/E/3Y6o68HoLFuelGsQJP2af8rBXdv1FclI3SNTly4xPKYQA8oVfyimWnwTn5ty/z6nec9yo4C3p/VcdNxW +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6ylpzdD3c1E8cbuPHoSW5RFJzxWCEvXq+6grREYT2q2eRpfKq4CUkXaSg9PBb07Brg== +VmVrGQo2zRokW/ZuO9bN6zS5woMmxkovFr1YPmCwVjomnwjs8yJd8vJ968XXd0m0pndaSK8+qOGykyY0WQyhZ6POo4IRkaJlDhmmPU4hQ+8= +VmVrGQo2zRokW/ZuO9bN643JcJR2kB926iqWdGz5U79RHIfKNGRE8o6dVdz5ZS7F +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67XKiXTToYD4Xj8EJf9tU1/Vu+G41TBzTVvAU2KLSeCv +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTcB3+DRCAF+pezmRjCID0udaZbU9+Bp25aQzLB8QDwQjJOciYA4ZMGenxD3oVSnPjhf/xssdSo8p33UNNP+7LnwEXEbYt+fXn2F5oiKjC6Gw== +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGez0gsOf0QeJW43sHDXr/9AmAy18p0NZvV4WTviIGdP9rl3wBU2+XWJNGEpDCn6tY= +1u+XjG/2+GSQRv6EzCaWRQ== +mObL0pW76xtWwwYeubgb11DPfOo/O10pxYUkfDIVCjm8OCRNT0T/bbX1vgDt/X1W +KZTmaJLp+FU9X93j5Tpqtw== +H7d2MP+WgR9k+129/mRN7ij0WzXIk6KGh6j7E1fgHISsYBh1T2OGWCH8QUwPOkcn +c1XU6vxl7MsQKGo3r4amMvdbawOnt/oBPSHXSrswDVS5Xue1Y7sB0F2vPYt641B/ +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +c1XU6vxl7MsQKGo3r4amMqf9zivp0Qb6thhCnxaiYdHXwUNLWpYP828px7Gieygp +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +g8q6WqkWsk4SUh3d5nzGDIJanaAux29CDdQxdeG/k+U= +Vyh8kMpaq2Q+odra9mDJ3Acz3kaMgWRroPCWRIzYRMY= +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGJEf1Y6VNXDjyNkW1vEjRD4/QxBrfqq90D/lFkSa20blyxshNDPqg3WrN+80ZCLiv67pSFMB4Brq3qmKK1ZCax +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmA0y/dvI19s9e1BRFCkk/QzH4UG1JBu620e7CQwjbQOOUvlgFJz4yLxls8aSEEo +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P+yZLrQY+X/LEpSBMw7xwfahIuTImHO5EcUQN9HQmgqX +LikBEoRjt8wwWzUZNw+jJqDVmHo0aqmO8fx4adbRXm90KhRgBH5ZfsJyfEAx+4JR +akHibliG6j9uvBEg96vRei563dWr9knc+JrDspjHc/A= +KZTmaJLp+FU9X93j5Tpqtw== +BIa0ibHnqUboaK9kYHYO4qFxMPyUysXdjYA1FqqhK4UMv4PoX1vehXtIZKdqRHbG +b4OJVZe8QyIpjuTpKXDL9A== +PRG/YRzXVD8iVR3bzEcUnlkfSwESl/MBfBcCyRS23XAS+yqB8BD2Fws8ohDlx4dxPS4gXd1cpWdmGRHNT+D6KSwXWu5PYPEs39ffK4i+qWM= +usrsES3VVkgHW6tBbEkpHTeLztsByVaJibbFv8kSFIM= +6XDh38s4HWrzA9ChHtrpYfoDrFj6al2d9qt+zDQaqfaH1G08yJkGG2cvEJO5JVMs +PRG/YRzXVD8iVR3bzEcUngb7XqhS9BqrxYAnnSw8i/s= +1u+XjG/2+GSQRv6EzCaWRQ== +5wsVwKLrIjIWqeTNpSVp9UQ8Pi6XZeLfFubLS2iQfjkZO92jH5vXc8qvq/7p/iKy +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== diff --git a/class/filesModel/searchModel.py b/class/filesModel/searchModel.py index cdb03db1..d3775b98 100644 --- a/class/filesModel/searchModel.py +++ b/class/filesModel/searchModel.py @@ -1,307 +1,307 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# -#------------------------------ - -import os, re -from filesModel.base import filesBase -import public, json -from html import escape - - -class main(filesBase): - - __s_class = [] - - def __init__(self): - for i in range(1, 100): - self.__s_class.append('f-s-%s' % i) - - def get_search_status(self, get): - """ - @name 验证是否可用 - """ - return public.returnMsg(True, '1') - - def get_search_result(self, get): - """ - @name 搜索文件 - @param get - path:搜索路径 - search:搜索关键字 - limit:每页显示条数 - p:页码 - """ - result = {} - is_dir = 0 - search = [] - - if not 'ext' in get: get.ext = '*' - if 'search' in get: search = get.search - if 'is_dir' in get: is_dir = get.is_dir - - public.set_module_logs('searchModel', 'get_search_result') - if not is_dir: - if len(search) == 0: - return public.returnMsg(False, 'Please enter search keywords!') - - if not os.path.exists(get.path): - return public.returnMsg(False, 'Search directory does not exist!') - - slist = self.get_search_files(get) - if is_dir: return slist - num = 0 - total_num = len(slist) - if slist: public.writeSpeed('files_search', num, total_num) - for sfile in slist: - data = self.__check_file_contents(sfile, search) - if data: - result[sfile] = data - num += 1 - public.writeSpeed('files_search', num, total_num) - progress = int(public.getSpeed()['progress']) - if '_ws' in get: - get._ws.send( - public.getJson({ - "end": False if progress < 100 else True, - "ws_callback": get.ws_callback, - "file": sfile, - "progress": progress, - "total": total_num, - "num": num, - "type": "get_search_result" - })) - if not slist and '_ws' in get: - get._ws.send( - public.getJson({ - "end": True, - "ws_callback": get.ws_callback, - "file": '', - "progress": 100, - "total": 0, - "num": 0, - "type": "get_search_result" - })) - return result - - def get_search_files(self, get): - """ - @name 搜索文件 - @param get - path:搜索路径 - search:搜索关键字 - """ - - data = {} - - data['is_sub'] = 0 - if 'is_sub' in get: - data['is_sub'] = int(get.is_sub) - - data['ext'] = [] - for ext in get.ext.split(','): - if ext: data['ext'].append(ext) - - data['s_time'] = 0 - data['e_time'] = 4070880000 - if 's_time' in get: - data['s_time'] = int(get.s_time) - if 'e_time' in get: - data['e_time'] = int(get.e_time) - - data['min_size'] = 0 - data['max_size'] = 1024 * 1024 * 10 - if 'min_size' in get: - data['min_size'] = int(get.min_size) - - if 'max_size' in get: - data['max_size'] = int(get.max_size) - - data['names'] = [] - if 'names' in get: - data['names'] = get.names - - flist = [] - self.__get_file_list(get.path, data, flist) - - return flist - - def __check_file_contents(self, sfile, contents): - """ - @name 验证文件内容 - @param sfile:文件路径 - @param contents:文件内容 - """ - n = 1 - result = {} - try: - for line in open(sfile, 'rb'): - try: - if type(line) == bytes: line = line.decode('utf-8') - except: - line = str(line) - - rep_list = {} - _line = escape(line) - p = 0 - for txt in contents: - if not txt: continue - p += 1 - txt = escape(txt) - if line.find(txt) >= 0: - _line = self.__replace_contents( - _line, txt, p, rep_list) - else: - tmp = re.search('(' + txt + ')', _line, flags=re.I) - if tmp: - _line = self.__replace_contents( - _line, - tmp.groups()[0], p, rep_list) - - for key in rep_list: - # public.print_log(json.dumps(rep_list)) - result[n] = _line.replace(key, rep_list[key]) - n += 1 - except: - pass - return result - - # line = line.replace("BT_SEARCH".format(p), ) - def __replace_contents(self, line, txt, p, rep_list): - """ - @name 替换文件内容 - @param line:文件内容 - @param txt:替换内容 - @param p:替换位置 - """ - n_data = 'BT_SEARCH{}'.format(p) - line = line.replace(txt, n_data) - rep_list[n_data] = "{}".format( - self.__s_class[p - 1], txt) - return line - - def __get_file_list(self, path, data, flist): - """ - @name 获取文件列表 - @param path:文件路径 - @param ext:文件类型 - @param s_time:开始时间 - @param e_time:结束时间 - @param min_size:最小文件大小 - @param max_size:最大文件大小 - @param flist:返回文件列表 - """ - - exts, s_time, e_time, min_size, max_size, names = data['ext'], data[ - 's_time'], data['e_time'], data['min_size'], data[ - 'max_size'], data['names'] - - for name in os.listdir(path): - sfile = os.path.join(path, name) - - if os.path.isdir(sfile): - if not data['is_sub']: continue - - self.__get_file_list(sfile, data, flist) - else: - - #第一步:验证文件名 - if not self.__check_filename(sfile=sfile, names=names): - continue - - #第二步:验证后缀 - if not self.__check_ext(sfile=sfile, exts=exts): - continue - - #第三步:验证时间 - if not self.__check_time( - sfile=sfile, s_time=s_time, e_time=e_time): - continue - - #第四步:验证大小 - if not self.__check_size( - sfile=sfile, min_size=min_size, max_size=max_size): - continue - - flist.append(sfile) - - def __check_filename(self, sfile, names): - """ - @name 验证文件名 - @param sfile:文件路径 - @param names:文件名 - """ - try: - if len(names) == 0: return True - - filename = os.path.basename(sfile) - for name in names: - if filename.find(name) >= 0: - return True - try: - if re.search(name, filename): - return True - except: - pass - except: - pass - return False - - def __check_ext(self, sfile, exts): - """ - @name 验证文件后缀 - @param sfile:文件路径 - @param exts:文件类型 - """ - try: - if "*" in exts: - return True - - spath, ext = os.path.splitext(sfile) - if ext: - if ext[1:] in exts: - return True - else: - if 'no_ext' in exts: - return True - except: - pass - return False - - def __check_time(self, sfile, s_time, e_time): - """ - @name 验证文件时间 - @param sfile:文件路径 - @param s_time:开始时间 - @param e_time:结束时间 - """ - try: - st_time = int(os.stat(sfile).st_mtime) - if st_time >= s_time and st_time <= e_time: - return True - except: - pass - return False - - def __check_size(self, sfile, min_size, max_size): - """ - @name 验证文件大小 - @param sfile:文件路径 - @param min_size:最小文件大小 - @param max_size:最大文件大小 - """ - try: - f_size = os.path.getsize(sfile) - - if f_size >= min_size and f_size <= max_size: - return True - except: - pass - return False +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +Hf+bBTgRv9pVfall8ODpvg== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +1u+XjG/2+GSQRv6EzCaWRQ== +xO7Wpp8LRZfjlLUey8VU9w== +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +Ji07I4FXLA9HlL84f+xIzwVBYHb2UR2JW88XmYqsjV8= +FpGNKKrJzpuJOv3iM0R0pQ5MvNLfEMlC/N/hQ9UzGqI= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +1u+XjG/2+GSQRv6EzCaWRQ== +TRR4OWAtNW2TMCr31HujQ786ngB4F46vN339GISDrdk= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +9zgnmC3+5N9XK5/NRaR4SCWZ3UikXYFnKKlpz6ENZEzWUZDKLOFleg2+OHdqWpsE +32pdC9DD05OE2l0oXazDFOXcNRPwrc0ZAJ7EyyAKw9gZBJ6iOJyN12yiwNtvG/Etj4AUGv20tGUTP756+m4Fmg== +1u+XjG/2+GSQRv6EzCaWRQ== +U28wv7XxIHeRSkqXJHXzR3dYOl05WSZCrwQLHtq7mE+upUObOxjAVH+D2wuzBbzd +Z8UsPk1Q7HtwjRd4g01ryw== +zZQPiCGCqSDO70yiwDNUrtYZbEcZaFTWzu76ev6QF+qbXnZjxFPpMEvGatyQPWPw +Z8UsPk1Q7HtwjRd4g01ryw== +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOFR1+D7xHQXnSFNKqEFQ40g +1u+XjG/2+GSQRv6EzCaWRQ== +U28wv7XxIHeRSkqXJHXzR/CzOXT9fdhh18KnqL31WmtG7mgmXX+KEsVaJ9DtI5Om +Z8UsPk1Q7HtwjRd4g01ryw== +3f+yEXMJpDOPrez5ap+fN71X0yVW9y+W39rIT+wSpcE= +ho/Q+jrWDtBeg9J9ZTnKi3+/Efbvadpf+qoH75NuGwk= +9uZN8r0CKVAbfzGtFDuvLwhOKNflpKM2ZwfKcLjWwCQ= +s2s/1ED0ebJ2NmFlE8kk0/nWNx9HXYglwLmrDDl0SJVEejxjmWbqF4tItLVHJaXz +zxNKIz/Dwj9Harw3dUNEQJhfwzP9e0g+QEHF20IF9QloJJn499jB5lN74CMN7xKF +sziRj7t6JsRtk9qEmKAeGc8r59oagHkoEVKlJap65zo= +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +EAqIF4KpaXVDvs4DDT5V96KZSat3hIJqR6dNVUxhnWo= +Nxw10FAZUk+m0nadC1hdyzNerAYqJLcLvHGGVkpkJIM= +1u+XjG/2+GSQRv6EzCaWRQ== +g9LTwGsiGd7fGczReaq0piwhORjgGOZyOpIjDfyNOXCPNawCc9H7QjiYk9PnEc8K +X/Lp4PsbbXulceSXUvGUpDfqLabbqgG4MZE+Cdy4FwlQTZqFS7MLKRTLDMwaDvILIRxQybc5sD1eU9BOwIwsyw== +R4Jn97/68Irdow38EVIRhDowEdscaTuWxhIXOfqZKT3o/TXmpW6PH4gqeWelHNA33aMiyCRKeO0D7gfs1V70Hg== +1u+XjG/2+GSQRv6EzCaWRQ== +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs/lMRSVWryD1gVdrE+N///hctbXY1m9aLHS8lUrVSnFP5O6PHEXfEjsMokMZJkUfD0= +cbvASHjpysrsjdY5RctXmJXaYs34E4tMRz7722FvpDk= +ACEEdgL/kNbx7RaZcSRxZD3JieACM+9p2ggEO8B4CeOCL6Z+tjHPmOoWvsHltK8C +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrSo1ZAQ1FHnoPT0QBpT+y//Ad3DLkc/FyKmgGPF5OiRLoeUNVCXj1GWzYsscvnb2SZxU4zI4hEOYLKQ5kvjDFyS +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYvriJFohZH5kYuBlhocmFiOq4wyQewMJhJy8xbRXglbx +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmUaLU2ERbIduh0en1Wj1B8Tfd95oiMHCmcX2jALSwoq6RyVU7vVavWtQJUE9viKOUw= +1u+XjG/2+GSQRv6EzCaWRQ== +2/eGB4mNeTC2Lw/I1/B9jClbFP06By8Q6TQXjVRMC5aisNpbx4ptA/UYjC7wxXvn +DUkWJc4dP0F0T3fzlo5ovg+i5a7jjY0MEmCx8kfq2jK+QHBdk0bbr2AxZ3cQqlrr +Djb8zu0gG2jt979SRwXXjOvVYuDaNNQCNmI1OvvoD70= +H72sLFEMWD5UKG0XW5F6nGJKzW/LoRFoOz84reF2UCk= +2RF1tdl20VItbDNfdL2RkNTBOOe8pYfatuolPTwnN8LGADdQl54uUvT1DVF9e+iGV4cafz7X+gg6KtVrt/TXCPI9NWJqgOr1pzq/i1AhR10= +6Kva1wVgBZWnKH8KBbNuqu0I1u+AyN34C41R+ayvUyA= +NhnIR3Ilo4H2su9/cTNo/Fny68/2PqdXcpcFyZsnflsKTqzPOMITq9Y6CpuGnJ1XbcsahDP9y+wRMXSx0waR3Q== +Dlyx6ZDFdkn72YIxkC3bo99d0wqqUWkm5oggFt3pXuw= +VmVrGQo2zRokW/ZuO9bN6wd/KBNDReWGUABD3WNwVs62xWSEQgqeqad2Q73LEQLC +7aT066WRx/cZGdWRKj2TgzQp9AN+Q9kHfKqypeMmdG4= +xWoGNWjKGPfI4gq8aHoTfPXeKqcetXNDNllPDdGzxJIYVQirFuRW+EjhBVgYVjVDRD56A0SEtVDAKN9yNygMzg== +jlXY8vCef9GOpgUZXz0mf9DfhxAODMs6lMdvzBIM/MW1Aq1rMVSrJYB723qLrfih3YWjEp+EB+NTwJh1y6k0Yg== +84Vymj90Wzn5yYuvz1pUuNMKvUAWFp7QE+2Yweu4868= +VmVrGQo2zRokW/ZuO9bN6wEyB2Guh40Pcugoyh0QBDk= +VmVrGQo2zRokW/ZuO9bN66AnqHISKFWgaxa6i1eykZnjUYRjoLmSwgSMUPuifCRX +VmVrGQo2zRokW/ZuO9bN62/0JMqSTjLVEyn+Ej8AxwUnCoc7c66mKn79grgEeCW4lCCimbVM8qjmKRX089HBXQZTubrP4Dlhm4eMwMPvv3E= +VmVrGQo2zRokW/ZuO9bN6+E4b2xiC6ZHjT1m1FpW3M+cImaNbhy13Fhkj9q6wpbsV6pyGO3VUL36gOhAJ+Q9bA== +VmVrGQo2zRokW/ZuO9bN69SEQTu8KN9su1QzyegRt9I0k+TINrUN1jOdFTWatw/A +VmVrGQo2zRokW/ZuO9bN691C3HCcq309X4zk3aYIPPcffxT7OcwObFcDmavXvPD0 +VmVrGQo2zRokW/ZuO9bN65RsFGHNPy4PRufoKux2OhO78Lnr5z+7zzZGg1YcYuSV +VmVrGQo2zRokW/ZuO9bN6x5oB/WlUCb6mmC2LMPppfSCyk+XA4K4GFn/hUbHBSy5 +VmVrGQo2zRokW/ZuO9bN67Ff1ABLPDzqsQSzl/a49rnaHEAmRXtCEzOMwE/sYpXvJqC4BUWuPJeNx91EHEOGRg== +VmVrGQo2zRokW/ZuO9bN6198WF1cuKQrC1Fe1gF45Yc= +uTEK8Ng11d3ix2pA+DD/aX0ELDXTMxMbOqDs4xOCbbtjTvaikhc80beKBuWMfrmy +OO/dMlOTxBikYSnUTxYxGjU1adFuYtvDicPSrTmmd94= +VmVrGQo2zRokW/ZuO9bN6xA57qdw8/aOkwkb2GLtNsM1Gxzw/Ohom0z8ykGCiYSk +VmVrGQo2zRokW/ZuO9bN68s4dlWAY+dRAs2Ywb0QeE1L2XlwGJiaW7efF6E8T2X4 +VmVrGQo2zRokW/ZuO9bN61fiC4FaUq8z+fKBUks6tGgZfs6Lma5kU54uQlswGIQVxfzffKkvm1lDl5rcXHMFFA== +VmVrGQo2zRokW/ZuO9bN6725gn6AE4KlYGXCgdldfTHvVePxt81VDJii+UIRV9So +VmVrGQo2zRokW/ZuO9bN6xP6//zUY+lIi+gH0ZiMAHxrWVK37wh0Dd0rIYDgHxyh +VmVrGQo2zRokW/ZuO9bN65Vu8PVmTF+4Dzea/bvMcEVOXNQxxERI+pswNZxPLgGG +VmVrGQo2zRokW/ZuO9bN66+Mk4GZKdNMe7NCKER0yRk= +VmVrGQo2zRokW/ZuO9bN6+3m7PBc9EdU7lP2yNhVBRJqxygYFe0FGSSnC9/qF3UeaMY1MoDseyFI9vmbOD5IgQ== +VmVrGQo2zRokW/ZuO9bN64vo1FYdzVPQhcC0bXObVxs= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +U28wv7XxIHeRSkqXJHXzR0xpTOufJNSwabqZ3Q3ilnxCv5GA0Lu1QixaJfBttSFg +Z8UsPk1Q7HtwjRd4g01ryw== +3f+yEXMJpDOPrez5ap+fN71X0yVW9y+W39rIT+wSpcE= +ho/Q+jrWDtBeg9J9ZTnKi3+/Efbvadpf+qoH75NuGwk= +9uZN8r0CKVAbfzGtFDuvLwhOKNflpKM2ZwfKcLjWwCQ= +s2s/1ED0ebJ2NmFlE8kk0/nWNx9HXYglwLmrDDl0SJVEejxjmWbqF4tItLVHJaXz +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +1u+XjG/2+GSQRv6EzCaWRQ== +1zXpiGOddO9f/7P9ia16hLkuEytAkOvat/hMfHQ4YOA= +TlcB8lXx3B+v8kN8QpOFdfk9CF2LWs/6yNZPbaaVR70= +NhnIR3Ilo4H2su9/cTNo/ADljVwvrJpst190+kbx61cg2ptEynXXqaFn0MK3A4Gi +1u+XjG/2+GSQRv6EzCaWRQ== +UVXwiL1XIF6sRJ+E2PuG4USOrEdsFvbOxW2jp01zP+M= +APhqKflZUhmgRCCocHNfF8cWLKWpGdW2cBFVB8esfaebcV8Bm5icApbL9u/SmCTp +YA93D7bVXAwMe11nxv6BP/vWRwUT7PwlT8ufhs3UDnY7vkYUBvQW6UL1K9e7/UlQ +1u+XjG/2+GSQRv6EzCaWRQ== +LOnQOFqU6PCy7YYVSPmkA0/gTFRU2H0d6T51/GWKhnQ= +3KmFFXpaFcYe2PH7o19+Yp8xAODhI/vWpqDGX4Xxav/TPftCcwlJ2GSdqZRIVGoo +R8K/Ndjw+sVUFzGFOCCgwyEgZIFazHPxG2rsbkC4Mic= +NhnIR3Ilo4H2su9/cTNo/FK+qNAG19wxGrf6AaX5vF2MYvQpe0FpvROJQN48CKmo +vdsIgxKEnw7wmXARhJ0Oc8wVF+V4koy8W2Mh858ZTYc= +NhnIR3Ilo4H2su9/cTNo/ILZghLpbr5j/IZD/K8v92HhdXliUZ6LIaLGw2w1580v +1u+XjG/2+GSQRv6EzCaWRQ== +4lN0LWBzLjgquvZLEoGU4VcBfkPRXV3AgETwRHgwx0A= +CaVVGVWiTY/j3V1bwWG579Qd9AhVS9Fosq6m4Brd0nDzkmY9JVeKm8YUIUMgLZgj +ww1k/N0gWysBJXBpYKUCHSY84nqsRk88NU0ho54XVr4= +NhnIR3Ilo4H2su9/cTNo/PIphezILghaR2B05A8VC0vv8lXu5MHRtSnDBC+dvcGcG17D53H9BitReg2GU8a9Tw== +1u+XjG/2+GSQRv6EzCaWRQ== +m4a/CtpjJ/nMVgLJWRwZXXRLpxxOovbcYxRSxZCtA6Y= +NhnIR3Ilo4H2su9/cTNo/Gu9KOwC8SCkdYU8skYoE5Jmz6ywJ/AKKeZKyRVis9YBz+4SGikFp/xZOPqocMHyyQ== +1u+XjG/2+GSQRv6EzCaWRQ== +CQqNVWmwukkhI4nkdKzMzwBsOQ+4HbTh/BOPpsEZGY8= +bpVtspxTF2OToZILoJ1fkG7rqYhfXtulnikcgwpk7TI= +NhnIR3Ilo4H2su9/cTNo/HvYNwc7FmYMnVorYtJsgM7mPAX2E472V3DWt24sC8XW +1u+XjG/2+GSQRv6EzCaWRQ== +RAcgdll7MZc9X2PbkeTvEzNyiOpuxHuwyFCiwlCwtBo= +TcUpsBX6+7VrOYYb7aAQxi+cMqvbjHfbkHkvbweeOrI08YeWNRwxkPzTCz4gVzYxXfh+l8lWk7g+IZkT/aj/bQ== +1u+XjG/2+GSQRv6EzCaWRQ== +5wsVwKLrIjIWqeTNpSVp9X49wYIz3gKTqq7jnocq95Q= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhAwjLxzOvQmNis0aVGbhWo1EM3d+uiOHD4+CHNQr7OvntVC01SFgrSaWBjwpxSspMw== +Z8UsPk1Q7HtwjRd4g01ryw== +zZQPiCGCqSDO70yiwDNUrrH8Q4B8051PjWqxvRD/6G+6mZgTSaRF/+5UQJBSfsOu +yqM179SzW9HnBHozkU+IlX5lAmWuzrOJA3YvC3xIfCIyjZshemB/XWvy+C/nSc8W +FFgrrvXvjrcz4wZcb8tZVMfl7bl/3so2ckZ8r41azARzhi5rZ6V/H3mosyOtQ6kI +Z8UsPk1Q7HtwjRd4g01ryw== +eUSWcSfVc151c+2QfqQTIQ== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +b4OJVZe8QyIpjuTpKXDL9A== +wlLHv6kT3Q/RmtMBN4nDAd5ePG9boZZ1puX9EApi7waJ3F0rdpZHQ+QUGogqxN5a +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN684Ix5sFjdtWSuy9M2APYQQ2cloQs45uAs7g4ZITcnIAjpBDTpZ3BrLNJFEVqJ2ojctFGKKm/95hVbuFjYtJPeM= +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN67Ap1Q6VmWnNlLMMqeDi80Doeyvhi/U8GBPp5HHWZTzv +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6/n7n61n7R9/9XTW7LSvopM= +VmVrGQo2zRokW/ZuO9bN6/WTynACEFT8sdNdNup1kalU+7hlVh1zylFr4591OIGa +VmVrGQo2zRokW/ZuO9bN6zPS0z48efxTbmV0uLaJMFk= +VmVrGQo2zRokW/ZuO9bN60IbS8l3rE5TrRrNdfxL+kX3Pzn+FDHqKXiYJg1KgmXF +VmVrGQo2zRokW/ZuO9bN6wAx5J8iv3RazhpXmncqnrreXiCOGawHQMNMjUs2cCVN +VmVrGQo2zRokW/ZuO9bN67Y8U6ALWboELuY6YE4ad78= +VmVrGQo2zRokW/ZuO9bN61Rjjiw/60bucMm5EHZzOcK7qzv28rP7g4n1B0Ie7pOW +VmVrGQo2zRokW/ZuO9bN6w5+dIR4Ypp9Zft93+s5eE5/yPjP1yhzf5/NmchfJr6N +VmVrGQo2zRokW/ZuO9bN623LqJlCkyXYwwCXSySz5FR7MI/uUVbwFRZ9X3HsyodA5+hN+HhvBEXoBRCHeMLs1w== +VmVrGQo2zRokW/ZuO9bN6/TV8HYrXfBS0+5RislOy6/23foxSNNR4dF+wZRb5KjO9N7VJVsNrJmtWvnUeaVjrQ== +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN6x97Jv5sxc/lnlSc+X41DCbYX0L0DMlJm79khasZsmeDQfkyT4p1zevxzJpYBx1DNhDBrlR5/iGnPwmNZ1OvWwg= +VmVrGQo2zRokW/ZuO9bN6ywyqAt0WUYk0wz2RaTbkeMb61E3V3fbNF4IaFu+N7qh +VmVrGQo2zRokW/ZuO9bN6/TV8HYrXfBS0+5RislOy6+U+Hq91CX8KUds7Nh1ZOU8nWqPG53E9RBQ5qeqTEsFmw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkkt5RqaKznfoAozF2brEPS6 +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkmb7MAq4qksNIzY1Eh1Jjt9/P2MbO42SPrEoJqJGO8pbA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN628UWklFrPzPT9a+yjbUprIuuMwW0I9DnLu6OGQ2TxNu +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+Z1kfdIPo4MYDXeXMYYWIYQQ4mrc936ju682bxyn+TqEoaQoeHhJgY1bOPJ6ik4QTAOiG9gx9IRwXfuCJtr3Ko= +VmVrGQo2zRokW/ZuO9bN66gjfjrYMn+hEmToUiACtV8= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +qx/2dn1Vpqs4SJaPnWiGqCi/4MihcwwKbYGKhk4naTAD8DbuTmd7DIaIioL9KkvbIcxbbjYAhP1Y/OUfj1mnGA== +segQHXNRrJyLOuWc8JKC3tmNT7tyg3IVKLwZ3sHcdnRU1rBJqs6RBi6qg5fjkef3qFq/cbKaLNb11SHQv37cDw== +Z8UsPk1Q7HtwjRd4g01ryw== +K0FOgK5mh0B5pEUVGx4wW2bJABs6gWPmt6Q9o3r8p6lGdFGrw5pIphcDNNDI4Pxe +bIts5UhOLbX+5vrkUi2fEpgbk+KLUNd8ANhOjt2CA8S7qBtqBqrK7rtUY4QlsSDu +St8jsnBszzKBMqXhq/bixnxiRXhroGzTpl2GmFXSnRkSxzNQsMXQGk8FOXJV6Y5r +eeFiPlXUI0Fos82HYztSmaHvIBfKy6OirpgreAVZg6s= +Z8UsPk1Q7HtwjRd4g01ryw== +4m1ZiBOyILBTSHQf//vyC0su8pHbpnlRDppIig8J5ysXeSTJP68Ri95UQiepxvh4 +WNTuVs53ESvy99Z9NIHHsQczMhznKv/xDqWjNZ9BqOGfNIc1eIJDK8jae1LRmist +sYeCd0P8QwDZsXD2zsYtpwOQuFBJwQCwG7+G1aTUb7MU2KmH7QjCjojJgGohPv0RKMRm2E/yyqe7h2CjSXfM2VbeNDXk2k3QOsue23kQf9w= +32pdC9DD05OE2l0oXazDFNsU+u4bMtJzvbtUKGSOKeSn9WvZrYyoyjfX7mEYUbe3 +lGY+HGJ/DlLufZHWWlj7U5mXaKcrxjwMdkooNruC2og= +1u+XjG/2+GSQRv6EzCaWRQ== +UNfQheD+zBxMB/8PVcAnPhJH0dABCf0LFLwey6F+PqLQAVJFMjnbXXReUGDMDEqKiYFfTP5la91cu1Qzw3WaLg== +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P2UruqssNk4NnKN+47U1SH36F2BxurMzko0B0uYv45I9 +eeFiPlXUI0Fos82HYztSmSfbblmQRmW8NByKq0O3ixRZii1ssvDqpHtqdGo6blGV +8ehs2Cqsj3GIU4YbS+kqMopjyYZACPJP3McICcxQ7QYvefmSdk7wQxP2oL97Q4vQ +yqM179SzW9HnBHozkU+IlY0McOeVypQ/EYlVcJDFe0GlWr6WqLSH8YZJQ3Zl80pE +8ehs2Cqsj3GIU4YbS+kqMpJNmQ5fFIbSdkmF260fowevNNXJ45tpcmfBJiEEZGw6 +NjU4zQ5DRyOHitK6CHLOgHN7CZ8aB+Mb5HQXfP+yO3ibTcrsbI4BMbGHj3qA0+h0 +NjU4zQ5DRyOHitK6CHLOgAOFCV3qwh/1iA/qlJwZgoGUD8smj9/Bg0H/hh9Oqg6m +wOl7lRII4ZgqRTj9M//0bzhomxn10Rjo41lvXMmPQezzGWk3GyeNMpopZ/PorkQi +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +D9SFxmTjfXKVjBp30+qz3QUFQ0c/jfdN4cWLcYJvWW8cGcuYOqW+zTq/dn4uaKX5XJmBCPK7U+oFvA5D8u9eQEuYG8CwsAxp5IrQkGPd5mY= +MQu48JkHralDtNXBUYl7lV0bLxYB+cVYGPW5I5H4uBmKn+X1vLJuCeoRB5MaCEB5bkAaA21+lVtVi8XnA5uCfg== +VmVrGQo2zRokW/ZuO9bN63kQ7naUTUtv0ESWGx9rzIn/1UUCp4+jfPXGiX14BdBQ +1u+XjG/2+GSQRv6EzCaWRQ== +hxgdAHD9AaIGGBvLsEuyB8PrRl5j0RLWnVrakTTrC+wYlY21ONwMz70K497xaqGJ +HiVf4YuHR4gw4rFUUeRvK+5LSyMNmU1h13+cLC9tnC/y8cnqYfMBKdGAnQDTVZHb +1u+XjG/2+GSQRv6EzCaWRQ== +O3CUgrw2GJfB+mDjH5+NdjDdFbdZzMNFPWLs5nIbjTo1SpgjDweH2KPbFtbB1QDS +VmVrGQo2zRokW/ZuO9bN69uA6kygxk2cG8IYgdG9Pr0+kDHcaDqpPo1aFOqQ6eZcFBrY15aCLHK16jKxHqaXiw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wcnkGR1Qyn1s8+CW5+MNtzRTbQfpmPYWYZ1Zix4ol4Or3JWa7MNuH8FVEl+/LFhBA== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6ydgd98GwzWUciEurhqUbl5zFKBgTvp0P9ayAL5RaMdP +VmVrGQo2zRokW/ZuO9bN65a4OFYGJXxFiOGSJivVxNq5a5scduUEPHPFtX2wTOnBqbjEynLbHNU89K1bs5Z0IYpgdoxUyCGS8wiq0EgiinY= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69NIyxsu9wL3ButjWi8Vd9WFqs0F/cbYpifzswbVj1XE +VmVrGQo2zRokW/ZuO9bN65a4OFYGJXxFiOGSJivVxNqDts0LCwhPoG9qpww/bbUj9JcO334yYaFKIcL8wQwB6/PsreYi8O27v2MuAOqvCx0= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6zLSeF3gHkc70v8wH1lH7V6izJDceK8v1VqgXLP7PPFh +VmVrGQo2zRokW/ZuO9bN65a4OFYGJXxFiOGSJivVxNrIogcyFIfNvtgKGel1Eosz +VmVrGQo2zRokW/ZuO9bN6wJqlk/efd+QgKBb45lQkEUJURJ6AXySYv+T873/0906R7ZDY/OqM+pHGZhlC5mTCLRAh2YSqtgcguEgy7g+lDE= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61W5fU8yZndDfCoUS7iF29y1q1oDpOjeucxethGOXiRJ +VmVrGQo2zRokW/ZuO9bN65a4OFYGJXxFiOGSJivVxNoJZTLcbrqpw1/MqZAUh2h1 +VmVrGQo2zRokW/ZuO9bN6wJqlk/efd+QgKBb45lQkEU/1xIS7OZ8UVIpUgSlst3vnPzuyqFcv172ahiYDGVFzqHNYalk2K2+ZiH00yJrmFg= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6zHe1AdyxhaUooAL+hUKQDTEHrKgluuXbnSBJQrzZUMq +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhFEcylaBQH0IUk+OusBE3my0xp9xwF5QGoWXAy5mxBYi +Z8UsPk1Q7HtwjRd4g01ryw== +zZQPiCGCqSDO70yiwDNUrvU3PfyPPxNosxB0yTLMifo= +yqM179SzW9HnBHozkU+IlX5lAmWuzrOJA3YvC3xIfCIyjZshemB/XWvy+C/nSc8W +bWa5NLmMcRfP7/pL4TxwVVPLg0xyCs7rLJn7q0oXZoI= +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +ACEEdgL/kNbx7RaZcSRxZATKpSMTQ/cWmiIi7MZqRD3iHlJkfgQKP2ZXgOycctOR +1u+XjG/2+GSQRv6EzCaWRQ== +PRG/YRzXVD8iVR3bzEcUnm+txOUxEzRjM53h5sTJkBazFR7SuI0t8ZpzuPGzrZlm +wlLHv6kT3Q/RmtMBN4nDAaa/4pYRsvL7819NLBZ1Jsk= +VmVrGQo2zRokW/ZuO9bN6w1AyfvV+/TKcjbtKlFZIXTurM+4yW7LDThX3d58hOXr +VmVrGQo2zRokW/ZuO9bN69h0W3AGoL6Fh678416+Hu2KoilysLPFqxJJbiCWg0kt +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN66vfklQEmoLc3ecqJIXu87NMH8vbZoK4tpkEq8BGvWvlPTpczQ026/C1jKPPSYdvzg== +VmVrGQo2zRokW/ZuO9bN66SC7guo6F2BUtjo1cx7Weub6mMMOadJGmLmKcw75lpb +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN6/VW4n84N+xmKyiHoJANXzc= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhORIPeJCp4wWVvEEOwBGJbMQ9B8g68lVqaXG7/c8sCgL +Z8UsPk1Q7HtwjRd4g01ryw== +zZQPiCGCqSDO70yiwDNUrn/Ahs5WLg1NPCie3zGShu1/wSHjnFYLODLD25lQA5x1 +yqM179SzW9HnBHozkU+IlX5lAmWuzrOJA3YvC3xIfCIyjZshemB/XWvy+C/nSc8W +8ehs2Cqsj3GIU4YbS+kqMtuucPAdJ1i3hn/BkXRR/b8uF7yzHSGAxk7z1KZFvBTC +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +PWKGbNEPxxxpGpe8THO242QunNNWnvbBCn+iC2oElyc= +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +1u+XjG/2+GSQRv6EzCaWRQ== +JlxIfINDpIN74VOPKABZW2ra44tSxs0ZLHEM/U3b4xykot9XDTMkG244y4YCVtPy2qfh3Sl0bgJRhkuNbAnsLw== +YA93D7bVXAwMe11nxv6BP/8iLYI7Aa2oVDOTO+f6qRg= +VmVrGQo2zRokW/ZuO9bN66Hq/Y+eYT69h50gLi7IyQOOipSIQ7Q/7rFMIt1AIZov +VmVrGQo2zRokW/ZuO9bN69h0W3AGoL6Fh678416+Hu2KoilysLPFqxJJbiCWg0kt +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN69tIcUnkZWsvMTtwyCmn6vvN9UwZdc3KUcaB2lz6Du2A +VmVrGQo2zRokW/ZuO9bN69h0W3AGoL6Fh678416+Hu2KoilysLPFqxJJbiCWg0kt +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhDsDXpgsqOpbKETjou9WOZPcc1elPxQh9cS821Ok3bVbCXSJ8u7r7Ro1XiEX76I/iw== +Z8UsPk1Q7HtwjRd4g01ryw== +zZQPiCGCqSDO70yiwDNUrj777l5BTo9eCcYE6KEhGN3qR2uRts9okQ1lptsdQWtg +yqM179SzW9HnBHozkU+IlX5lAmWuzrOJA3YvC3xIfCIyjZshemB/XWvy+C/nSc8W +yqM179SzW9HnBHozkU+IlY0McOeVypQ/EYlVcJDFe0GlWr6WqLSH8YZJQ3Zl80pE +8ehs2Cqsj3GIU4YbS+kqMpJNmQ5fFIbSdkmF260fowevNNXJ45tpcmfBJiEEZGw6 +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +jPjjou9VNwkRtlQvS7IMwT61DiJZ1n7ABVvC8lImM1GTyercqKulHtnPhz5LMb0WCoJOixq+LHAXxH4az57sJQ== +J1UGlXkKhKXD36OrXXhN8LEiN+9Y+rPtU094/lpWCJkzL91nJbwePJFLFNxcU7a0SlIO5OZ3MwAWum4zIjkPgg== +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhLHqHL4jLTDrGAIPR+WsUzXEFIbEmBGSwvvQLutL0dFcpH0AUPQoXrj49knrv9+d+g== +Z8UsPk1Q7HtwjRd4g01ryw== +zZQPiCGCqSDO70yiwDNUrm7HG9OhPLBhZEBfEtqJxcB/kpanL2zAPOvr56+W9KHv +yqM179SzW9HnBHozkU+IlX5lAmWuzrOJA3YvC3xIfCIyjZshemB/XWvy+C/nSc8W +NjU4zQ5DRyOHitK6CHLOgHN7CZ8aB+Mb5HQXfP+yO3ibTcrsbI4BMbGHj3qA0+h0 +NjU4zQ5DRyOHitK6CHLOgAOFCV3qwh/1iA/qlJwZgoGUD8smj9/Bg0H/hh9Oqg6m +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +YdTM5cWYAsOq9968GjmEIn458PkySNd5GOJg2QjTSgkgpn8VUITEbXvbBG+yQJ0k +1u+XjG/2+GSQRv6EzCaWRQ== +gZ7BoIAABc+I8g/rYzq+f9fH+SgIZsS8Qfv+e480JkPCt3tQj/PL+v5Tjao1wdEQSyGpe2xx/8OpBGm95SqEpA== +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= diff --git a/class/filesModel/sizeModel.py b/class/filesModel/sizeModel.py index 6aa155ce..40449cfc 100644 --- a/class/filesModel/sizeModel.py +++ b/class/filesModel/sizeModel.py @@ -1,287 +1,287 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- -import copy -import random -# 获取目录大小 -#------------------------------ -import sys, os -import json, os, time, re - -import public -from filesModel.base import filesBase - -panelPath = '/www/server/panel' -os.chdir(panelPath) - - -class main(filesBase): - _exe_cmd = 'ncdu' - # 扫描历史 - log_path = '{}/data/scan/'.format(public.get_panel_path()) - # 缓存 - cache_file = '{}/config/scan_disk_cache.json'.format(public.get_panel_path()) - - def __init__(self): - self.is_use = False - if os.path.isdir("{}/plugin/disk_analysis".format(public.get_panel_path())): - self.is_use = True - if not os.path.exists(self.log_path): - os.makedirs(self.log_path) - if not os.path.exists(self.cache_file): - public.writeFile(self.cache_file,"{}") - if os.getenv('BT_PANEL'): - self._exe_cmd = '{}/plugin/disk_analysis/ncdu'.format(panelPath) - - def get_path_size(self, get): - """ - @name 根据排除目录获取路径的总大小 - @param path 目标路径 - """ - if self.is_use is False: - return {"code": 404, "status": False, "msg": 'Please install [Disk analysis] first !'} - path = get.path - is_refresh = get.is_refresh == "true" - - real_path_dict = {} # 软连接处理 - temp_path_list = [] - for path in str(path).split(","): - r_path = os.path.realpath(path) - if r_path != path: - real_path_dict[r_path] = path - path = r_path - if path != "/": path = str(path).rstrip("/") - temp_path_list.append(path) - - try: - cache_data = json.loads(public.readFile(self.cache_file)) - except: - cache_data = {} - - result = {} - - path_list = [] - if is_refresh is True: - path_list = temp_path_list - else: - for path in temp_path_list: - if cache_data.get(path) is not None: - result[path] = cache_data.get(path) - else: - path_list.append(path) - - if path_list: - scan_path = path_list[0] - if os.path.isfile(scan_path): - scan_path = os.path.split(scan_path)[0] - for path in path_list[1:]: - while True: - if path.startswith(scan_path): - break - scan_path = os.path.split(scan_path)[0] - import string - code = "".join(random.sample(string.ascii_letters + string.digits, 8)) - result_file = '{}{}'.format(self.log_path, f"temp_scan_size_{code}") - scan_time = int(time.time()) - exec_shell = "{} '{}' -o '{}' ".format(self._exe_cmd, scan_path, result_file).replace('\\', '/').replace('//','/') - public.ExecShell(exec_shell) - scan_result = self.__get_log_size(result_file, path_list, scan_time, cache_data) - os.remove(result_file) - result.update(scan_result) - public.writeFile(self.cache_file, json.dumps(cache_data)) - for r_path, path in real_path_dict.items(): - result[path] = result[r_path] - del result[r_path] - return result - - @classmethod - def __get_log_size(cls, log_file, path_list, scan_time, cache_data): - """ - @name 获取文件或目录大小 - @param log_file 日志文件 - """ - result = {} - for path in path_list: - result[path] = None - data = public.readFile(log_file) - data = json.loads(data) - data = data[-1] - root_path = data[0]["name"] - if root_path in path_list: - result[root_path] = data - else: - cls.__get_sub_size(data[1:], root_path, path_list, result) - for path,info in result.items(): - if info is None: - continue - if isinstance(info, dict): - info["type"] = 0 - info["asize"] = info.get("asize", 0) - info["dsize"] = info.get("dsize", 0) - info["dir_num"] = 0 - info["file_num"] = 0 - info["total_asize"] = info.get("asize", 0) - info["total_dsize"] = info.get("dsize", 0) - info["stime"] = scan_time - cls.__get_stat(path, info) - cache_data[path] = info - else: - cls.__get_dirs_size(info) - cls.__get_stat(path, info[0]) - result[path] = info[0] - result[path]["stime"] = scan_time - cache_data[path] = result[path] - return result - - @classmethod - def __get_sub_size(cls, data, root_path, path_list, result): - """ - @name 获取子目录数据 - @param id int 记录id - @param path string 目录 - """ - if len(path_list) == 0: return - for val in data: - if isinstance(val, list): - sfile = f"{root_path}/{val[0]['name']}".replace('\\', '/').replace('//', '/') - if sfile in path_list: - result[sfile] = val - path_list.remove(sfile) - if len(val) > 1: - cls.__get_sub_size(val[1:], sfile, path_list, result) - elif isinstance(val, dict): - sfile = f"{root_path}/{val['name']}".replace('\\', '/').replace('//', '/') - if sfile in path_list: - result[sfile] = val - path_list.remove(sfile) - - @classmethod - def __get_dirs_size(cls, dirs_list): - """ - @param info 目录信息 - @param result 结果 - """ - dir_info = dirs_list[0] - dir_info["type"] = 1 - dir_info["asize"] = dir_info.get("asize", 0) - dir_info["dsize"] = dir_info.get("dsize", 0) - dir_info["dirs"] = 0 - dir_info["files"] = 0 - dir_info["dir_num"] = 0 - dir_info["file_num"] = 0 - dir_info["total_asize"] = dir_info.get("asize", 0) - dir_info["total_dsize"] = dir_info.get("dsize", 0) - for info in dirs_list[1:]: - if isinstance(info, list): # 目录 - dir_info["dirs"] += 1 - dir_info["dir_num"] += 1 - cls.__get_dirs_size(info) - temp_info = info[0] - dir_info["dir_num"] += temp_info["dir_num"] - dir_info["file_num"] += temp_info["file_num"] - dir_info["total_asize"] += temp_info["total_asize"] - dir_info["total_dsize"] += temp_info["total_dsize"] - else: - if info.get("excluded") == "pattern": - continue - dir_info["files"] += 1 - dir_info["file_num"] += 1 - if info.get("asize") is None: info["asize"] = 0 - if info.get("dsize") is None: info["dsize"] = 0 - info["type"] = 0 - dir_info["total_asize"] += info["asize"] - dir_info["total_dsize"] += info["dsize"] - - @classmethod - def __get_stat(cls, path, info): - if not os.path.exists(path): - info["accept"] = None - info["user"] = None - info["mtime"] = "--" - info["ps"] = None - return - stat_file = os.stat(path) - - info["accept"] = oct(stat_file.st_mode)[-3:] - import pwd - try: - info["user"] = pwd.getpwuid(stat_file.st_uid).pw_name - except: - info["user"] = str(stat_file.st_uid) - info["atime"] = int(stat_file.st_atime) - info["ctime"] = int(stat_file.st_ctime) - info["mtime"] = int(stat_file.st_mtime) - info["ps"] = cls.get_file_ps(path) - - @classmethod - def get_file_ps(cls,filename): - ''' - @name 获取文件或目录备注 - @author hwliang<2020-10-22> - @param filename 文件或目录全路径 - @return string - ''' - - ps_path = public.get_panel_path() + '/data/files_ps' - f_key1 = '/'.join((ps_path,public.md5(filename))) - if os.path.exists(f_key1): - return public.readFile(f_key1) - - f_key2 = '/'.join((ps_path,public.md5(os.path.basename(filename)))) - if os.path.exists(f_key2): - return public.readFile(f_key2) - - pss = { - '/www/server/data': 'This is the default data directory of the MySQL database, please do not delete it!', - '/www/server/mysql': 'MySQL program directory', - '/www/server/redis': 'Redis program directory', - '/www/server/mongodb': 'MongoDB program directory', - '/www/server/nvm': 'PM2/NVM/NPM program directory', - '/www/server/pass': 'Website BasicAuth authentication password storage directory', - '/www/server/speed': 'Website acceleration data directory', - '/www/server/docker': 'Docker plugin and data directory', - '/www/server/total': 'Website monitoring report data directory', - '/www/server/btwaf': 'WAF firewall data directory', - '/www/server/pure-ftpd': 'ftp program directory', - '/www/server/phpmyadmin': 'phpMyAdmin program directory', - '/www/server/rar': 'rar expansion library directory, will lose support for RAR compressed files after deletion', - '/www/server/stop': 'The website deactivates the page directory, please do not delete it!', - '/www/server/nginx': 'Nginx program directory', - '/www/server/apache': 'Apache program directory', - '/www/server/cron': 'Scheduled task script and log directory', - '/www/server/php': 'PHP directory, all PHP version interpreters are in this directory', - '/www/server/tomcat': 'Tomcat program directory', - '/www/php_session': 'PHP-SESSION isolation directory', - '/www/server/panel': 'aaPanel program directory', - '/proc': 'system process directory', - '/dev': 'system device directory', - '/sys': 'system call directory', - '/tmp': 'system temporary file directory', - '/var/log': 'System log directory', - '/var/run': 'System running log directory', - '/var/spool': 'system queue directory', - '/var/lock': 'system lock directory', - '/var/mail': 'system mail directory', - '/mnt': 'System mount directory', - '/media': 'System multimedia directory', - '/dev/shm': 'system shared memory directory', - '/lib': 'system dynamic library directory', - '/lib64': 'system dynamic library directory', - '/lib32': 'system dynamic library directory', - '/usr/lib': 'system dynamic library directory', - '/usr/lib64': 'system dynamic library directory', - '/usr/local/lib': 'system dynamic library directory', - '/usr/local/lib64': 'system dynamic library directory', - '/usr/local/libexec': 'system dynamic library directory', - '/usr/local/sbin': 'System script directory', - '/usr/local/bin': 'System script directory' - - - } - if filename in pss: return "PS:" + pss[filename] - return None \ No newline at end of file +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +2unYDUJ7AQka0zNDbcMACQ== +PDg4EOFKN+dK2UrpwNWHLw== +svfNqiZI0zsAZVvJZhVKxDnsn+e2c8eSwsWdNJ9ryew= +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +Sm95VfslcvrZUo7dRR9Ibg== +1in3eluyrI5kuyCXT4wxGR0BSfUj5K16kanLx7m3x4E= +1u+XjG/2+GSQRv6EzCaWRQ== +XIfdJ79nObMM+vyAmKbTmw== +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +1u+XjG/2+GSQRv6EzCaWRQ== +VD9zfcC25A/8QOaHOhC/08+yyCMXeBqRf4/ga7jFrTkT7C7mOGjvtRvT5O1nKry9 +P8yT4+bsOs+HQJp87m1F/reOtBiqflci0dOv1dBJ5CE= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +02raNMOqWaNt7xMESPC3lwgVxysXSXHuDQdM35j2P78= +20lLzEXEs+ueAd+bOAB+91r8jzaQl96Rr4UooYVAhek= +9UNCW/IYJm3y0yDOfWJu1hDSEjrLqqinLz9ENpiz7Bp7U6FvhbSHpw2fG7yfwZm2o8XmdX7QFCP6PAgeC3qwLA== +5C9WmrmzTMoEG9WnQlXcow== +7dl/3jjStHv3YKwNaOrbP+Oo5xU5m7MdajxYoJc85S92TEuqkIbRI+ohiQ6vWWtZzZM2CO+G/Tt7tZ9oQFyeGBfi+jfxFGgmnYMxeUi1JaJixQo/7XHqj5ninApCrSrr +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +OV1bY3WjlWolVVGpf7K0XuXsoXQHr87ax8nvHq6xsHk= +WHkOzVx7seuLmxs5Hu+l/t+vqO/0m+n9UCf5LD77H/SwGtkYMMmsF4cVmuhhatEVLh7WBABwVLMyOLWUBB+RadwWiICXHtE1kzcSxcgplF89E4ZHUf3MK6MmlUz6ew99 +32pdC9DD05OE2l0oXazDFBSGVTBsjpcBhCs4oxG+0yc= +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9jMBZ/1Gnw5H1SVochJBBx+ +NqhMbY8+EQI8zhTG6Zh2I8eIwd9vy9oCMj+HlVpAR/l7CFW4GiuliNJFtFnBA50C +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9iDQP8kO4z+Nh9k9xWY+COM/KxrWvWZ4Fal5fq021KjFA== +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+nolHQ42C9PtbiSn1F6OlCtRmnEtWURclz1HQwAElCBzw== +NHEVGZTYwXqY5vJLoK2Wy88Jwk3YGlNccZ+9V9zZnU+eb80uFKXdlJTW19PQSswv +32pdC9DD05OE2l0oXazDFCUyxYReX+Pd+b7Itml48JbsM7EEkGoW/LLDCJHqhTgLDQkOzUwuHM/Ow28K/8sDHIyBKBfp8UiP7Spf6jDWw6w= +1u+XjG/2+GSQRv6EzCaWRQ== +doXPckCdzE7wjT67BB6bRaf5yMsyg97qMshMoQ7p5vyoCIqIGhDYBeIX7pK+6SIf +Z8UsPk1Q7HtwjRd4g01ryw== +2e9aPy7fN0yHEJSt/vm4HRBZEsq1/zwflCBuSjOe7lpc8+97QgE8TmPh9W3s4bjTFqkssBBzUB3kSkpHlHBB8A== +eeFiPlXUI0Fos82HYztSmdKUUr+D7SPt33f2BsZZi0+Z/4tXriyUkCyekpFOTLlk +Z8UsPk1Q7HtwjRd4g01ryw== +MzOj4ZiBOJDNVP8vi/lFQNbYGt1FVk3Is6YVV6wzt87cMUguwHtxV3/Xy4LTsg2e +L0eUthVnpkGsmKFAX6d+uEP20LPBDczIBUCH+eDLR4XWtYJ6VE0yMGgVZeHE55+Q/qVjwg2ycnFdmwmE1t4pQK7rMwd5XeH6SKf0j0LJj1rcjuZWiUfgbRgMQOHfmWeVoHXJd8Gdujzjmu8XVaU3Yg== +5zgvKM85O5RcGwttJ2rWIPdwxXoXLnOjmhL6DYLhvpA= +R7vEIxyjiiVKR30PQVdBXTc3ZCpAzCM1mvOBtDg5HBdxFY32n+U4Vd4HmL3xGJ0J +1u+XjG/2+GSQRv6EzCaWRQ== +ziQs1GyPUQ7zPh2xP+c4C9jWdniwpr48mYVqFpshpfPIMeXmT5sEphPHgd7xjv51 +ygXqV8JYtXUVMKjwSB6Bx/HL/6ZwOKweTtEx9fTOy34= +RDZxR0eU/0YxwhYtovyGeVmHi5rJUJIHwjySNLmeLsS/YgiTjCQe80TqXYEeRodv +diZ40h9D+5PatJtYF1tU0duwNTXVBazmSgO2LE1bzRxihfbnQEKkYw7WpzZBa6M4 +UbJuac0dxLiN+5ocS3w2vVbjWbzmARN1Q6nZRO+hN/U= +VmVrGQo2zRokW/ZuO9bN66bxma0YBg8TGr7zrmdMIaysrwI2tOjz2KGqNWCW4fyv +VmVrGQo2zRokW/ZuO9bN64fCWnvBG95BbHi3cQzmuAc= +n/Vg/an64od1JqfYD8zjtnTgUj5eucdPqy9I4MCTvJdvKMaMV+njWEGl8CrMjCbman94UdMu8eZchGcaRr1oYw== +A7tBmpCaSLjSrSFXFx4rD5DetX8LfpTB33kFqMYK0Od6ZH8Gs/Y11F09RX46+ICL +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +7jSw4GB3eBRbmC1MzdtYH7AtgesWi6JQAvIQf6OzE3nseJy0QnGK6G6JTZ8m4DdlPAzSNTALqHevuq2kA19SCpPCP9OhqpP+gq0PaqyDu7o= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +7jSw4GB3eBRbmC1MzdtYH8hFhUduGUWsUH3mRmUlIWM= +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +1u+XjG/2+GSQRv6EzCaWRQ== +7S/W1w5l8hNVTCb+pjBU6IQBp7oTWR1btD/WKY+MFgg= +nI4TFpKsgYpjAeaQko6KTiS9GERA58xL2uQkIWwz+8U= +9uZN8r0CKVAbfzGtFDuvLxswrINnjWvMwuMvv7mQFLJ/lDrB0Yzi1jeGmHGjb9sx +l2FJPs4YkAmmok1ulDRuSA== +wlLHv6kT3Q/RmtMBN4nDAf5jouZDDJebuZx0e+GqD0kRo3kF/7jgrUa9LjtTIi7k +VmVrGQo2zRokW/ZuO9bN63WXNO21aDfpqHWqTMbiAspwMoEKToJ9evkuxTBcHidMf81BXfmdf+zT9d8k7rZRQQ== +VmVrGQo2zRokW/ZuO9bN6xFqwj56iFFIvII+sEi91i3uqenZ1RukB/iwA5+BqIZ4sn4SaQnBu9P1A4Q+K1sZtQ== +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN6zviB9IGvmdqavzf213hyy5ybw9UFAPNCxKWXRAzML9G +1u+XjG/2+GSQRv6EzCaWRQ== +WJAfjl0WrD5Jtca2LHLz1bmB1KYomdySnhMTj+CfBFQ= +X9N2KsRYiOx20Z7QjlcKwPlYTqzzndgDM6lpBv04BpdP5Pg3w1osXEPQndKx0rQg +O3CUgrw2GJfB+mDjH5+NdjXyt5QeWCsfAO3Xba+4ChG/q7Y+X5y6P+nc7wDmMC42 +VmVrGQo2zRokW/ZuO9bN62VgOORuMBXqkUitwmHVIrDnse5ewM1R/I5hzCpTRGjRPjKi5jw4W/gnKcBGD9YBwg== +wlLHv6kT3Q/RmtMBN4nDAb3TgOhh+jcLpHwNqZ402ohizeRChhJH0mnfBvfdrTFZ +VmVrGQo2zRokW/ZuO9bN632VflaKbWaV9Wa8IOB78Qk= +VmVrGQo2zRokW/ZuO9bN6423Hrz4GlH+nD6sJ78NwiKP8lp8FUcPhY9VQRWbCG1fwt6D/Suz+3Mw4fD7HRaoaw== +VmVrGQo2zRokW/ZuO9bN69qqX19RJUeMAQ5O8TuJCvc= +VmVrGQo2zRokW/ZuO9bN69ZI8jvRenlEUWjnEq2NCPyDOWUWUPrjde05ZcnIjqs3pWRoX/dFNNNs2i/oTDcoYA== +p9oVsBgcyiBMjDb8CcPOqP1vx7YoaEO9HCXBZO2TNso= +Ds0U5vkLcQbSiWM0OBg+t4r9+nkADB6+pTfs6BnR2ZxqURGwpq1kta+TPw+SFYxBQFmX+7Si0OBWn9a+xTaiWnluFS3e3tzgqB5tisWnRARhFcyQwPYfaZhQ4qC+koZE +o6QhOIN2Sc4SHELnst17uev8ILgDsE8F6Lr4r3FiO5kX4kPxG5GWPpKuy8MG+3Pp5ak0QKFbZROCqcYHAtfrS5iit3HYIQ4qteW8Mhs5eEUKNbIOTh/MQvOi25rbe0W/ +X9N2KsRYiOx20Z7QjlcKwJ/jTfZo3KZRc7Jt0Yxl4gCUzaVNOZtVdc33LG13QSPt +dAlD/BDiyhUNR82MoKY54wOQA9Y9TRrid6IAzqKFFYlKwy9skQ3yRe3T15WYhesi1YWjlAGeJABXbkoJuF6IIk7XVJH7co5nkqJxZSD49Qxzny6hNig6oVk/d6uY6w1QCaRfrBs5nTcMZExJVAB7WmDhGKieI8G2bZn9lROj5A4= +xWoGNWjKGPfI4gq8aHoTfM/GWNK1ohgau/P6Yy5hh4NxJd/Z/S8GTdLTsA9JjQHV +X9N2KsRYiOx20Z7QjlcKwI4qbj7KfnKOrIFv7Z4lLtGUYNfIhUqPnWQkQbUEaS3fINZ8F83Vu3A+GF1Unidx3ROcHOnR+dfswdftnV7DDxk2JxMxGEO/rdyz4HhDx1HR +gC7pmugJ3/w5trYRkDiKMZu2feMB3hGfqRh2TQebwmSMQbtzCVrS+b4WQ4vQR9dB +o6QhOIN2Sc4SHELnst17uZEi6P73uFYiKT8ZfdKaq74uZIFYFu8J6CF85QIftAeI +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+kuoHiBgr/0Fv8qNCoSuQiv1gF3L0eu9kJUZmaEmUpN5x4xDhiRRncANufGyvnI5q4= +5eMaCO4xAK2QW4QfX+0YtKbMF1jS3cxAAkXneuE7di9ABuvCVnZ4SdZbeTDi2qoaSmKkDI5m7fh2F112P2pzKA== +o6QhOIN2Sc4SHELnst17udxbOsHgV61axCR3P3wfex8g46ZP1Sdx4i+CYHMTr6rl +pZu1Giln7AYpCOrLbI27d9Gmita51EFD9OUkFL56AbY= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +k7Mgm/6UlihxNIWiDi7/FQWsPKdojVfNRBOtHAgYqheCYcsU4cUBPL/6I95dDtlT9USt45h9FK4d7rl/Yz+V2cXy5SkFT71kfnrU3MZE6nE= +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P3oJY/mwDRD7KWNhsy6/Y2mb4RNPwLuEAcMxo+t3fDjM +bIts5UhOLbX+5vrkUi2fEgVzsPbCG1s61jTt5yW6ACsP1ywsg+01aUDIv4DP01qd +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +RDZxR0eU/0YxwhYtovyGeVX2L0QGVwLQX8TIwLObEVs= +o6QhOIN2Sc4SHELnst17uQC18YJWaV/6CEkrqfg0jS20MlFgiNUSShqPpoyfxdJZ +HNr5/wGY+7coHgowTe94Ll4RZO1qTURSl37kSeltNLP9Vb6jtwiW6YkMWV99p4O4 +gEJ/mwYAYmzZtF+bOEtqdkC8SHP+P7fOYSe7UxEq8rmitn+SghIfUQayzs5YBbRY +nWJfxfGKXK3M33nhNtnE4rhyN+G2GEkqT49HwMRlHEY= +Vh41Si6Af7eSg+/W8dfTM7yg2G8U6j8xv+R+xKztdB06IZseCUPYnwY/NdWIvEVs +emk1Y/Al74+4sZ82aqgV0lhGJemXCaG1r29Gpu+zhWmouexK5AYktd5VzhuOMg2z +o6QhOIN2Sc4SHELnst17ufzdxJH6EiDMn2kcCw/f00XLHkMwjtUD6soxHwPjfI56 +l2FJPs4YkAmmok1ulDRuSA== ++Zjg8k4ZNVjB46NJnSn8ErakeJYqLEw9DhX8QKCz44c1fatMLaKfX4kADtdWsVo0m3DBdTfktt2+zvPnSnSwlvIi2oJRgthe7eFBbm0/pMs= +RDZxR0eU/0YxwhYtovyGeVzRqxe/x+r1aScdsXNjbZEDJM/n9+iJeJ6fjq5jg85x +1D18KWr2hdVsBcdZx1OaPQlScmRtGt3pyfQ0+O6DoOA= +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1D18KWr2hdVsBcdZx1OaPWpmi5eWzbYhJFCY/NY8C/Oa7c/XoIMLf5OfmxNwBC4p +VmVrGQo2zRokW/ZuO9bN69Xj6ooDVrRi+Qx0VVbxxuGwXblBGt+UMa3iQsUS+MLX +VmVrGQo2zRokW/ZuO9bN6z0Wv8dJZfITZaz7/LBnekh8eKiykXRkJuTroXNsk4sHsjMzx3ehYnBiRsLxQ3l1Ew== +VmVrGQo2zRokW/ZuO9bN65vHbxdhaeboWCi24FUvfUKxwpfWxuzCii3Vfl81U5n04eCJ/I9xuyAFs+a+utQoLA== +VmVrGQo2zRokW/ZuO9bN66hhLXcZ90iz+YURPNpCdCsAm2RnkTlvB3/dCAYY43QH +VmVrGQo2zRokW/ZuO9bN68uGmvNP6wueRXu6UnefD+wH+Yr2ww3IH83h4hC1VuyV +VmVrGQo2zRokW/ZuO9bN61sfi9NDWbb3IEw7V243XdGQhqhFSYbmb3HIkuCdE2VLLMjOBsOxdHyxz3Pp+dE5gA== +VmVrGQo2zRokW/ZuO9bN6/jrSn0BMJUjTRmq3s4EygRutAv5minFbaCDNSpGOnuGOGbAGxzYhr5FfgbC2LAzaA== +VmVrGQo2zRokW/ZuO9bN66KELFqGTvIFlqiTgxQshiEJtYy+NLL3drj2rKA5FLSA +VmVrGQo2zRokW/ZuO9bN69qUnlBYp99FW3NU5/GK+PzoHh9gUCjYvnxOqd2JqPw/ +VmVrGQo2zRokW/ZuO9bN69yVdWZ+lX5d/HxJ8n7Oe9lsFn7ww5+jazR8dqP2+S/V +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6+bUoxQpnmE0JVwLD8EwzQ/H7dsGXPHJ9cqQZTkojMQ+ +VmVrGQo2zRokW/ZuO9bN69qUnlBYp99FW3NU5/GK+PyS8CBUEJbhmFgtsc9tZNZa +VmVrGQo2zRokW/ZuO9bN6+EOVn5y2jsFMxK6Zwn+wS/mDi05KQxr36Au3RyQiAZt +VmVrGQo2zRokW/ZuO9bN645GzHl1E1lX49ZzqICVdNd+qq6XjowUV36TmrdcDLlJRAtaa5meR0QErawnLRsIbw== +VmVrGQo2zRokW/ZuO9bN69yVdWZ+lX5d/HxJ8n7Oe9kengQKSCiIkCs6PDQVWcCGpn3+rLZxeaOALBnRhwQmjw== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +Zj8NVVe0emTHliAvlpCAerp3iS8Ldzanll9i5zjVEzHQg8mhLB3GjOH9WUhexCJRTK10ie4Li3ku0ivs8PwncfL0CsJ9wuEyiySAnQ+YkT4= +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/PxQtnmghDNkLHWxwLL5Pfd80TPKT3Fq0L6v1zn15TQIi +LikBEoRjt8wwWzUZNw+jJgWwZ72bx/3Bg25s1ZrnmbQ= +eeFiPlXUI0Fos82HYztSmQX4Tn/GgRQusZFpFlvVnwGj3wnNVeLP8HbDx9FBGXTK +Z8UsPk1Q7HtwjRd4g01ryw== +ERFPcpFJ5x6x1iUKDQh3uuNBeWAGk63K78XGpIjhm2+rBpfY1aEG51pD1dP3DCm1 +mDvBoIQgD/LTzVDzjae4UJqJvNLzaOzwnnIlpmmAT9w= +1D18KWr2hdVsBcdZx1OaPS9U/g0ilrDQencOJy/eayhgybOUx8AzmnF0FRkjYK4+ +VmVrGQo2zRokW/ZuO9bN68DMZ7Jr2bE3tmVz/lTLfwFI/TgA0E/RsREqTEyLRZTtM7CfvidC3a+UoB6eKaAipGC8lyEDiDjU2OcuQXtivS1abYS5vnbaL+qjgg9xvxZW +VmVrGQo2zRokW/ZuO9bN6zAeYkrMwd7SuWC1RgnERpd/CtxQ8iNGHrTJa/V2vhqa +VmVrGQo2zRokW/ZuO9bN64O9/l1Cw+LCUDbA/A3sef0aZa1fkxQn/BGfLBYcyIz6 +VmVrGQo2zRokW/ZuO9bN60zutLkqghuDP33toq5yXb5QoAWEq42gUIT7CcbtapHA +VmVrGQo2zRokW/ZuO9bN615f1QfyDTLI7xBDK4WokeoZ/LXykw7EJtsaIC8dNCUB +VmVrGQo2zRokW/ZuO9bN60/t3y383iQehg4Rf7JHddlsYUFMmzhkXzzyUyBqcUubPvH2W0j6argePbntySyUzU3AV893eEQa7Yym533kWfk= +F9Sftf1PoN5P+lgvc+r10GOWQ9m5fvKDb6g+UBNuAqJgppHlIonewfzaqeYuks8R +VmVrGQo2zRokW/ZuO9bN68DMZ7Jr2bE3tmVz/lTLfwGYa8KblejDwqMGy0HS7E4EgxqdWnOJ4dMlPPf1V/WbB45LajcY2+ifZj2xLXa9OfFHv0/Gwu03ahXrFxuCROiu +VmVrGQo2zRokW/ZuO9bN6zAeYkrMwd7SuWC1RgnERpd/CtxQ8iNGHrTJa/V2vhqa +VmVrGQo2zRokW/ZuO9bN64O9/l1Cw+LCUDbA/A3sef0aZa1fkxQn/BGfLBYcyIz6 +VmVrGQo2zRokW/ZuO9bN60zutLkqghuDP33toq5yXb5QoAWEq42gUIT7CcbtapHA +1u+XjG/2+GSQRv6EzCaWRQ== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +uvcMgENha6hOb5ExkFYzxifcHMl1I6jrVqfUC4eNgIoj4KJOBKY4nAmoO9a/O2O4 +Z8UsPk1Q7HtwjRd4g01ryw== +LikBEoRjt8wwWzUZNw+jJnTofnNhv2W4ginDjUT/BhyY1a6MiWTuSLmwGyGxJyoG +OOMIt/WiNRdlMd3fGCaVnH8L1KXFwR+AJLHaxenwu6I= +Z8UsPk1Q7HtwjRd4g01ryw== +mLDy5md6q2DkMFpvLtFlrbgr2CZgO+U3exRgHQl+WcSH91VecYi0XBGIdUBcdsZg +mLDy5md6q2DkMFpvLtFlrQOIllMJ+wGUeIFDucuWhfM= +mLDy5md6q2DkMFpvLtFlrU7D4Mf1ddl1j/aFZsBH/L00FFBrPDB3T673ZTtvJXi7T4ttNDvQwxI44e41cn9A6A== +mLDy5md6q2DkMFpvLtFlravQqYs0c/bzbQZd9QN3+nxowiHk/rK8+h8Eqv5Ky8lOiVVSR1FIS/LaTiW4is7mBg== +mLDy5md6q2DkMFpvLtFlrbzw3oheqXbD2qwAZMcuA6Q= +mLDy5md6q2DkMFpvLtFlrWGBrB/jcgDQIi0rEIX5vN8= +mLDy5md6q2DkMFpvLtFlrZ1FI9J4zyIUAMeW/0xk+lP3z9xCfVcSg+XRPXHC2cuC +mLDy5md6q2DkMFpvLtFlrZCAR5DUc2ey3Q5f9VfOQLDhu05EqWt2hD1A7vDKSpvg +mLDy5md6q2DkMFpvLtFlrVE4nQqlZKqeRHuhcjYG9cBcbIb6mWq6nCQfXdYqVLBuaNiK7wnpdzENXVQGO4xxag== +mLDy5md6q2DkMFpvLtFlrQsEeGnN4vdParE9CDgiAK4tpdIWqozh7FV+BqtNBoddCimScVPthPo5boSZ2aCWKQ== +6P8mKfouOK1hwXy3MrrCzRw2UxbgP+Mth9DRZFMbu9aRbVUdtXbkT66ZlvG5+Djo +1D18KWr2hdVsBcdZx1OaPWpmi5eWzbYhJFCY/NY8C/MQbhQT7DlATgfnJmvUUqXY/dtM6WhYa2J1aR47iGCprQ== +VmVrGQo2zRokW/ZuO9bN6wPqF0gJwvcSllqTssBN3IQFplxd+VVRZRZSvCVDzHK4 +VmVrGQo2zRokW/ZuO9bN61DdfsWGlVNr7xf50OuAfKCWMv/w8v4ixbracqKZTk/4 +VmVrGQo2zRokW/ZuO9bN6+bUoxQpnmE0JVwLD8EwzQ/H7dsGXPHJ9cqQZTkojMQ+ +VmVrGQo2zRokW/ZuO9bN62MG6pIvkYn7nEiOjeoc64MrwokZLOOcTxaO8jmQiVHe +VmVrGQo2zRokW/ZuO9bN61DdfsWGlVNr7xf50OuAfKAMMGpuitrqnk3bkbSIR8tK74tenFOnrkxVhSzwMR4nkQ== +VmVrGQo2zRokW/ZuO9bN6/Ly/04eT22h1xyl4dSvQSMNqp7Qyw2t5SG7+K+1JJ0tG30ZCAYaAQLvLpQPeNMYHQ== +VmVrGQo2zRokW/ZuO9bN6+BIYCVAZKyRqKArWI1vncDdczYfwtpU5zW2MXjhy/z24GxeAx3QYpL0FUnYH9XQhhs1p0qlJik4yELSlk3g+Jw= +VmVrGQo2zRokW/ZuO9bN6+BIYCVAZKyRqKArWI1vncB97WLhqixtZQpLQVJjO8F9rmnhbOS/OmY6IJG+17UdTrJrlEUKFErdk4ikKnb3BL8= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN65CBdpRIOtptu///2YRyptDJuSvuNBDGWMFm3MShUYhGV2dg4J7PVbUNMjSG7+hBZw== +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN6xRaRh2h2yn72BLBhrbk40FNmSd9pOkjInUT3PKeP00k +VmVrGQo2zRokW/ZuO9bN6/Ly/04eT22h1xyl4dSvQSNU5MWz1FGNCr4JsFoMFY0M +VmVrGQo2zRokW/ZuO9bN60HvClbwMpABlSn73UhadQPjjrFMVzyd6bmtZhfVjiZU0huAoInXX/Q18uefitNtu7dcNYig6Kw0BYoxUhMMT0E= +VmVrGQo2zRokW/ZuO9bN61igX6t9WmvQd//qUjyBL6Batd2BDC4WPD9BlNf7ZSqbQhMB7M2MNINGZFx+z9nGKVdGMF8NhqzIBLeQIOPk9kQ= +VmVrGQo2zRokW/ZuO9bN69Xj6ooDVrRi+Qx0VVbxxuGwXblBGt+UMa3iQsUS+MLX +VmVrGQo2zRokW/ZuO9bN6+BIYCVAZKyRqKArWI1vncBha4twiqqqB+Lkuo23an3nEDeccqba1p3sRs3HYCm9Zw== +VmVrGQo2zRokW/ZuO9bN6+BIYCVAZKyRqKArWI1vncBg1CWm6ODKEO48OETuF/c/M7gx4RLnKkvGw4yWkxfmUw== +1u+XjG/2+GSQRv6EzCaWRQ== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +6tCwW2TgXgdok4IQCfIwE64+sANSz5E9miXDw6QJ0r+y7ra3wf2izxuhjiJCK0VR +wgR07xfoapmx6eEnFHXXYo4se8cEG3Kpe+KryARC+58c1jICgdZ/yfiiPJOB8osn +iZaIsa48RXfE+uf/yF/rQB5Pyu2LLCSwLBPzS3UQNqB6gGSsuwVrKFDeT7S7ipIY +iZaIsa48RXfE+uf/yF/rQJ1gz5XqVdLjUgxnZLiZ0kxneHQUMBy4uvSK/N8oOaAN +iZaIsa48RXfE+uf/yF/rQNi9uyLdvBZCesZvX8KcrGbG22Xqw+geSPuZqdcR8y/X +iZaIsa48RXfE+uf/yF/rQGg6DytIW0WefmXAsCDYL0s= +L0eUthVnpkGsmKFAX6d+uBWSR38I0ooDiDs76zI3rHk= +45hn3FAK6/Q5+BKz66I4FYGZrfj46julOMe5ClqgRDsgDKqc5g62TlKmklK79LSu +1u+XjG/2+GSQRv6EzCaWRQ== +a3HCOwVwgUUsIXy0NI30N1OnTRGwPCMCQnd24LPbvMgw/R3ae9al5MVMPI50MW70reHrwrOjYxRGfyTEKtkxrw== +q4nc/jwATOMUyfSjLibfEQ+nVsCkuVXEdAcBKpgjqkY= +b4OJVZe8QyIpjuTpKXDL9A== +iZaIsa48RXfE+uf/yF/rQOvVhu/ngkyor/C5n/UzkBlCx3epFa1yEIpBPWIrtKEsgWftLDpGjIPx7n5zFnTscEhgReIowtD1ib7ZaN1X1YU= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +iZaIsa48RXfE+uf/yF/rQAZ+EA0c51yFjsX9CPHlh/5ktj0ZrD7uCaj/wgnzHlOCeQCMjWvzIk/DQlZByMiq+Q== +XoHMeaTuJPZ1ZwkBYGxPGNO/fZdFrQeGdb4hk4VRpV0VW7fg9Snb4rb537T74XCuh+3WZ9XaioboPMJTeFhjmA== +RCRD0rTmvzRnTOGXlaehMNmLKF59VFrNXo8fzNysbhTBKccco5cEPB8/dJX6dFqJWYqn10Vvxc/CzhwbE8EJnA== +I9ZqFYKrj9zbehR1kVWMoO1GI6M1qqtG2eBy4D/nldc6VNZO14SHN/sd2voQ0WEXWYIgTZTr4rDtrGXNBr4IBQ== +p50nNlFbWWSM5Jl8TEsk4TnLz+h+3cBHymV4PzApmXUNqbCVGjhE1qvOMYMtCX6R +1u+XjG/2+GSQRv6EzCaWRQ== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +J24jXQQti6wQv5pCoF2ZrcLSPgZELT2A2fhuhSpqDo6BY87dEmAzodysqNgyKvzt +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOYzeYsCbBz/1birJj/JpatlFkFxq27b1MCr3POwO46E7P +YThduOXrtrzATLnwayQWnaA2rNH65m9eJvXKw5GWAAUtcZdLSGJWwaU999R64Cgx +7JQQDTi0T2idhFjao4jEpTcu0vyrf72ks6Zg9LXjcLTZ8NE7V7tyehMaTKgkL7Idboxtgmjsd+qtDVj49PH4pQ== +h2Amc56dmaRo3inGZxN9N3ZLtg8BK5nYtLYDA42Z8AY= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YPE3JL0PMUUnZO0HIdJJMQIGfUXEkt1C3jRrwzATAL//GACiXsL3H69yk+yUtmy1bN8AwykNHE9d/OHOYjevUg== +DarJ6ebtq241LBXqUNSmb8Hia5oTaY/pb+ay91woMws0FeL7ToyJ9YGj7DCzO+EoR/kGoHazwB7LUQuT5rt+Tw== +WHkOzVx7seuLmxs5Hu+l/kOzZi2ouy/XqACgrRboMuAkPXB/7WhDgtDQxPBuCGk/ +L0eUthVnpkGsmKFAX6d+uDbeqTXIh0y0jrTQHkGxk00ztM8l4dexLnzgjvrsNYuo +1u+XjG/2+GSQRv6EzCaWRQ== +xRFNEm+nIUjh4cyIzan96kWy6IA9PfznyfSNsYXjhcyekoNWgDiMNKfLwh7IzzFp6unM4EoEl230CfvpYsKmgM609XzA4rC8JK4FwC/3GZM= +WHkOzVx7seuLmxs5Hu+l/tfRUTtAad8Cb/6s2BEj2MF5sacS9liWC0L9lFX0T/8D +L0eUthVnpkGsmKFAX6d+uDbeqTXIh0y0jrTQHkGxk02YaDJD923/KMmkdZImosxQ +1u+XjG/2+GSQRv6EzCaWRQ== +uqDej/AgJHcP58Evn06kx+BRYuP+Rv/P3yyKDCiEank= +M6xDbqnqlGC4Hwvm9iO9WpYNmQzB5Emv8bNh+GjwifhyGxJaxrM3Nu55hGkrzp3Na0UgKMMZhQFDdB1qBnzZM4GXlShJqy10Xh8Ng3hvM6XmMaUNRpE1sspYrkvl/ok8txPZkGD8+6CimGoeq/gHc3IooOAxEh16uaQ9V4oWzew= +M6xDbqnqlGC4Hwvm9iO9WqiWh7KxdPf1TCqsUz96BP2LPZ5/6OAavVK3NQ5oRvG2WRCLP8CLNwYIKWfO7LKcMQ== +M6xDbqnqlGC4Hwvm9iO9Wscmcj6WdKWpwKUcX7+6wD9qlaetybTVnC7NRyEMXQaUruJxuLP7EQajaE97ybYV+A== +M6xDbqnqlGC4Hwvm9iO9WmrzpW7xqvFOkXRrquG/LdmkWI44+PZZb3h8jfRCzfXDDJpmmDVFpMslk/lOV8ORKMKf4lFQfW6IN3gOezMVNdk= +M6xDbqnqlGC4Hwvm9iO9Wrluw3FaFu0rQlI5v3W4Gahe5M3IH/7altsfw4aQIzFTFEp+kqHOFqW9zknlolTgr2aDwLbq3LeRkrF/cccTrDo= +M6xDbqnqlGC4Hwvm9iO9Wui7tfyp3EhrtndI0VQ+0bDRrlG5yESM9XqTcbJlcOwgCZixtz2l/jHsCLjZop31bNpXX4KYhl+/OzIp1VoECupI8G0NECKhXZGxwFP67OGP +M6xDbqnqlGC4Hwvm9iO9WoTR3NAG7MGBrcMrYbTxwvg2d5zTs9QiVoeexs7vgrp1tLfB/Bf9Au9wHlmv00K5zdOfVB4vDxKGSGToV0vyVNU= +M6xDbqnqlGC4Hwvm9iO9Wsk/YBXEvbh9Uvmnh0QJSiFhxQ69X5Iboy1Wk54I9i1KGX689Qf7r00dKSrjZWYdjLOCaQVypVZpz26m19HQouI= +M6xDbqnqlGC4Hwvm9iO9WkK3xOAmzw0wgWg759KvLhH+T5S/PN6qHRHRQ9T3LKPDzTOSNkmC8BZsFn/KJ+t2aLi6nBteKpQt56QmNdl4dqs= +M6xDbqnqlGC4Hwvm9iO9WicgU1xjseoMDlsA3ET1//BCkdCo5I3potyMnxOy8NaEhbkT/s1OThM0Dsk3VBA/QNrh8++5Afxt13QBvjjNDXI= +M6xDbqnqlGC4Hwvm9iO9WgMokrgL8MxEJjEYatsrUV+m4TFjPvxx1gKKyfrM5ibGqjVXQhxhwIodZHTc6JZeUg== +M6xDbqnqlGC4Hwvm9iO9Wr1kU9VI/yudDc/eHU/ewRmVwLQ8jsS8G8r/7F9bWy1TnWfhsRCr+7KbyaufVfaXfcW7CUaFOxd/zmAVrMdCGCc= +M6xDbqnqlGC4Hwvm9iO9WgnywlPbX5KSPk43KpzNKONPq30CaJS4nUs0mMkP5+hbSivbSe4CLkF0KKyRNsQATD8ju8dPAj+f107ThzNYRGhbxWww+AVK7muD42wODMJWmw/m4J/JHWAWU3/ibv9MTKX/YkfOX0bmEcp9LHx2Qxw= +M6xDbqnqlGC4Hwvm9iO9WmJCBrWMX5jYxKbKR4/u41xMqvIPvD3LZfDUhKcoaKuCpfnCZ+EipXVt8I72Nc/dYrkGyEFkdOLMBXhB0XKMLR6STMFXt34k9atd9Qw9x9lNnqTZZXN2lErP5TZWivV35w== +M6xDbqnqlGC4Hwvm9iO9Wqld6rg2Lx98CYBge5Z6YW6hSCzeqqkJgURG552IJQtYStVrim5S05TrSOKTWH7I0Q== +M6xDbqnqlGC4Hwvm9iO9WtVd4iOTlcBudzv96785RaYKs1fhppqqdhgtHLqtXL7XvQgtx4SyuONpafq5eZA/ig== +M6xDbqnqlGC4Hwvm9iO9WgHNFBZvpw4Ykrar5o7UITLqUwDKmFnO9r5qPTi6XzxGGIRw44iLmxNRh6SGGLj5FfIqITod7iAWgEPz9f8TRmg= +M6xDbqnqlGC4Hwvm9iO9WuZW+fuUZB8WpAoWyO5L3TVNxM59XEoFvdP/J5Mq7mp3CFXj+BrERDZdtku4jEnITQzW/Dt7uaUWhWDMFwB/e23rItPvfJnVza4yTyKuwcrG09UsVKbA37u7vX9Q8N+YMQ== +M6xDbqnqlGC4Hwvm9iO9Wve61oMllAKK7aIpkSBv6ZwWnrC4aacWBYjk6HyqSYHL3Hq0XfzMZLHvhQDh2db3gQ== +M6xDbqnqlGC4Hwvm9iO9WoVS+2ab/4yGLNZfC++bUsOORjTX6oF3vbSWEKIYHfC+YN+4SdEA/Th8xOpeytLoDDgQyYgcj2oY5OlwO/zmBYo= +M6xDbqnqlGC4Hwvm9iO9WjKEpRdLTg7WVPHPtUqJqomzwXwNg8PGbqA/U5P5s2Ph9e4kTSbmMaVhig1rkgK80A== +ThMMHJmbSWymjnzPL8su8sBHn5UE8cpNu9WLRZiCzbQKCkCL03sgs+DNRoTYeavL9RZ2I8MdhFqyZIgb2dUmGw== +7sp6hnTw2OtZMRbJTXg9rCuLplGGh0CaZ+h81UyYXoTfxQz5dfr9h/PfGPK3/x1X +V3FyDbaPyt/GrdnaVxxGP5QP34MBqfVhEo9rUYClIQ8GQn13ZWbZ1M6lAwu32F6B +aTu3nEEJ7aNZbzv6PgxsAMIq5Ov2mq1jFSdTHQbA4AjQAxNj/KCefFN/tFo+IuhQiYS29bChBFdJoonMS2dVyA== +UaZ+UhuuDk5ogKtL0+DbtF7ZZMj2uXAUIl9TJ345Nxmn7StJaC+PsiBVbPv50MBUIixchc/TaeXRoVZZIe8nmg== +UaZ+UhuuDk5ogKtL0+DbtO8lJySRwyixd8VH5w6ddE5sq42a0K/FaZh7d6UNKygbk02lvC2lEd8zp1b07VqucA== +UaZ+UhuuDk5ogKtL0+DbtEs4/ygchHdZq2caT5P7ZAY/kfYVA9rE+SmVnD6+LpWybbHzyvSm6PZuNWfaxF7GPw== +UaZ+UhuuDk5ogKtL0+DbtMpwh8AvPkdmTABoemWOBnMsvPbmXsy2D3mw7ujaSN+uVEXDHvmeZaWIoPTaqnsQVw== +UaZ+UhuuDk5ogKtL0+DbtN/ExXZO6r+1Hcx6QYA2uMFerjP1PMKvvs4KUFhJZHFNY9UPjmpmdJT54wPfO3Unnw== +hQ7Zur2CVZat0LHXIoOqQEKciq0mRp3cUUC7gBemh69qrx4U+SnMEWhzDrNvYhgm +OcTtZEQzjmqOEoLAu9WIDBnMum7894CWxyFLkwGu5IwtyjTHaWA2GKONXV4r9vTbg7+1p4tBESO9qkBU74HTEw== +7sp6hnTw2OtZMRbJTXg9rCpKNGJwajbjYG7HLQCEEEXdDjiGpseZYp9Crn852qiTgxwj5jsQKnu7vZaHhMdVCw== +aeUTuKRk4Cv0a6/hj5z/Azbob5vVUjA8C9zwLz131NFQ9+8QpUnpNMjZSkEDyzqvNJOEE7EErl0S1qQyJp0S5A== +aeUTuKRk4Cv0a6/hj5z/A0ufTREaztlpquv0pmAqO4eQBh/OIVHNrMu+G8nKcxX6NRI3YvUe7O2X95abzQ0iTA== +aeUTuKRk4Cv0a6/hj5z/A66msItV6S9si1UO/Jz4XEtSerCl4q49UQ9M8vbWbqsSRL7I2Hi6gUZNJBpgAByHeg== +Yh5IC9IfOdzCK3e5XDQpq7zb2TsOXHn+3J5O2euHIXfq/mGF5OhOVbuFMPjXnQ+M0Dg5Ohnqz2pi0eTNKIzU3g== +Yh5IC9IfOdzCK3e5XDQpq+2odTci4K1zbNd8NJCQULk+QCw5/atmY2M9xvTShAOrmRgT9J1cZGUyLM7hRG6T9Q== +Yh5IC9IfOdzCK3e5XDQpqxA3qd3TcYSVTzrAlmiWJldxDn1jD1ym4yTsgSajOLNbDK+wnMHISwB8YRnXcqofniSCYnGaRqMvj3MURlsXp1w= +Yh5IC9IfOdzCK3e5XDQpq4DKOluGK9CtCgb/1zx0eeCNnM5UGmgXLCeepHrrYTn7VFMw8Fm+mwdLVExOl2tjpZOppE4njUrAe4tqn0RXteY= +Yh5IC9IfOdzCK3e5XDQpq5Ifj6xfZnDox3MD5utXFuixhN8RmZlHKUvwYnDADhnNXPn0TzBaUVp8y3F1IayBhZc9me9kQSHvBQ7soWODUbs= +Yh5IC9IfOdzCK3e5XDQpqzRhjv/5t8mv7bNNVXSZA0n4tEX8ATMW6aOFzRYG1NzH8pTBJQ/1EU81Zjuy4E0Uaw== +Yh5IC9IfOdzCK3e5XDQpq5aQfSbh2TPEVX96QEtEYaDiuEyvpOigBao3+w795EB9JATH6Wr2GIoRnqzXQgdrIA== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +661hZf7vhUQ+50okfwfTXw== +fE3zmgbfhO9ZwEda5GgwD0R+fpJ7JqroVfr4B5FO/AhYM3WK1zBVVzIzdn4lt3dirmMTx0U2aKsHZMW89EKgpg== +aPdLEV6HY2SBr9CPW/3NlfBbk//L4rv3UKu5v766u0A= diff --git a/class/filesModel/uploadModel.py b/class/filesModel/uploadModel.py index 916957cc..8eb83e32 100644 --- a/class/filesModel/uploadModel.py +++ b/class/filesModel/uploadModel.py @@ -1,170 +1,170 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# 上传文件至oss -#------------------------------ -import os -from filesModel.base import filesBase -import public,smtplib - -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from email.utils import formataddr - -class main(filesBase): - - - def __init__(self): - pass - - - def get_oss_objects(self,get): - """ - @name 获取可上传的对象存储 - """ - return self.get_all_objects(get) - - - - def get_file_list(self,get): - """ - @name 获取可上传的对象存储 - """ - return self.get_base_objects(get) - - - - def check_email_config(self,get): - """ - @name 检测邮箱是否配置 - """ - import config - - c_obj = config.config() - mail_config = c_obj.get_msg_configs(get)['mail'] - - return mail_config - - def send_to_email(self,get): - """ - @name 发送文件到邮件 - @flist list 文件列表 - @msg string 邮件正文 - @to string 邮件接收人,多个逗号隔开 - """ - - import config - c_obj = config.config() - - try: - mail_config = c_obj.get_msg_configs(get)['mail']['data'] - if not mail_config : - return public.returnMsg(False,'未正确配置邮箱信息。') - - if not mail_config['send']['qq_mail']: - return public.returnMsg(False,'未正确配置邮箱信息。') - except: - return public.returnMsg(False,'未正确配置邮箱信息。') - - msg = get.msg - receive_list = get.to_email.split(',') - if len(receive_list) <= 0: - return public.returnMsg(False,'发送失败,接收者不能为空.') - - - #附件文件 - flist = [] - if 'flist' in get: flist = get.flist - - result = {} - result['status'] = True - result['list'] = {} - for email in receive_list: - slist = {} - try: - data = MIMEMultipart() - data['From'] = formataddr([mail_config['send']['qq_mail'], mail_config['send']['qq_mail']]) - data['To'] = formataddr([mail_config['send']['qq_mail'], email.strip()]) - data['Subject'] = '宝塔面板消息通知' - if int(mail_config['send']['port']) == 465: - server = smtplib.SMTP_SSL(str(mail_config['send']['hosts']), str(mail_config['send']['port'])) - else: - server = smtplib.SMTP(str(mail_config['send']['hosts']), str(mail_config['send']['port'])) - - data.attach(MIMEText(msg, 'html', 'utf-8')) - - slist['error'] = {} - #添加附件 - for filename in flist: - if not os.path.exists(filename): - slist['error'][filename] = '文件不存在' - continue - - #超过50M无法发送 - if os.path.getsize(filename) > 50 * 1024 *1024: - slist['error'][filename] = '文件大于50M' - continue - - #中文无法发送 - if public.check_chinese(filename): - slist['error'][filename] = '文件名包含中文,发送失败.' - continue - - att1 = MIMEText(open(filename, 'rb').read(), 'base64', 'utf-8') - att1["Content-Type"] = 'application/octet-stream' - att1["Content-Disposition"] = 'attachment; filename="' + os.path.basename(filename) + '"' - data.attach(att1) - - server.login(mail_config['send']['qq_mail'], mail_config['send']['qq_stmp_pwd']) - server.sendmail(mail_config['send']['qq_mail'], [email.strip(), ], data.as_string()) - server.quit() - slist['status'] = True - except : - slist = '发送失败,' + public.get_error_info() - - result['list'][email] = slist - public.set_module_logs('files_send_to_email', 'send_to_email', 1) - return result - - - - def upload_file(self,args): - """ - @name 上传文件到指定的对象存储 - """ - - name = args.name - filename = args.filename - bucket = args.object_name.rstrip('/') - - if not os.path.exists(filename): - return public.returnMsg(False,'FILE_NOT_EXIST') - - info = self.get_soft_find(name) - if not info['setup']: - return public.returnMsg(False,'未安装[{}]插件'.format(info['title'])) - - sfile = '{path}/plugin/{name}/{name}_main.py'.format(path=public.get_panel_path(),name=name) - if public.readFile(sfile).find('upload_to') == -1: - return public.returnMsg(False,'暂不支持该操作,请将[{}]插件升级到最新版'.format(info['title'])) - - #创建任务 - import panelTask - task_obj = panelTask.bt_task() - msg = '上传文件{}到{}'.format(filename,info['title']) - exec_shell = 'btpython -u {spath} upload_to {file} {bucket}/{filename}'.format(spath=sfile,file=filename,bucket=bucket,filename=os.path.basename(filename)) - task_obj.create_task(msg, 0, exec_shell) - - public.set_module_logs('files_upload_to_file', 'upload_file', 1) - public.WriteLog('TYPE_FILE', msg) - return public.returnMsg(True, '已添加到上传队列.') - - - - +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +kEPo6lgxLBQavG1DxRrlnEK5KohxBShsQIeNCGHkEP4= +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +dTLQ8Wr3wPn7w6pte1B1YA== +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +bPP96Leq4/6bN59hBCaYlI8pXwmlcWoHnMhS0JRYMuI= +1u+XjG/2+GSQRv6EzCaWRQ== +NbgOlQKOVjgcHXr2EWkJqQWzgWH6Vq8RJ/AUSzSss8IVUY2HzghyE71ZaXPBTLGv +NbgOlQKOVjgcHXr2EWkJqeTgbDhQ7kNNY/wz8y2GqwZTfVZH1snrz7MBnfOT91tS +E9KwiiXGE37hdLvk/mY+UIznMbuZ/AbAoo9N1G0YvoLz61bIHX5Q0KQJryQ9TSuJ +1u+XjG/2+GSQRv6EzCaWRQ== +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +9bJD7OGVYIbkQwcqio3pMkEQ5Tepk1KAKJBdik81BOHNSVFwrMdl2gFGwZvvpYDQ +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P5ZvVL4YjAgUVrqKtQwmJSm1F1KbG0/rNY/6xTjx3EA3 +Z8UsPk1Q7HtwjRd4g01ryw== +lt0ONPc5ulpyg8e5cXCrlcNbN4AJdkj3/jMJ6CSe6OnifSM9+lA1O2PeRoAR/q0t +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +J24jXQQti6wQv5pCoF2ZrcF30x6jIElrdtnLwh7xjCTUby5LexzwAUmLxkZ6OZp9 +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P5ZvVL4YjAgUVrqKtQwmJSm1F1KbG0/rNY/6xTjx3EA3 +Z8UsPk1Q7HtwjRd4g01ryw== +CAhn8dRUItEbEErp4w+lX9NUcXl4jGRSijSFq7galTkLvLnalDlhEukOn7KLDmPy +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +mz/fdzicrSUPVLp6o7PwCSAYjWkN2zpyX6N0H3XQ+Re/2H8C5nEqNaf5oXzlLd5r +Z8UsPk1Q7HtwjRd4g01ryw== +0LFyB+q/4AUBkCrX53xYJZNkGwqwopNQ7HR7+KY/2yjfRImCHWTpijZdrdo/d8l8 +Z8UsPk1Q7HtwjRd4g01ryw== +KbLSYqE/CR9TL3ZELtBVg8UiZep5zt9moZHOJ5ffCHo= +1u+XjG/2+GSQRv6EzCaWRQ== +iTSUWsaHz8xtfxXo170oO/0vr9reANk6j9ykbM5WiCYFw3OvMYMGyJ3KgPGMrF0H +PXCtqxQPPqk7Lcqhz/3AoGaqmPHjN4rP7NSesY70x/92ifCLZ8Okvt9nT27s6jtrmbW9pJXGLgLNf+3meuER5A== +1u+XjG/2+GSQRv6EzCaWRQ== +FEZSdYhAV26PfOeirhfda9fPkPkpgcLIU1bSxCG9qHs= +1u+XjG/2+GSQRv6EzCaWRQ== +DJtqiO8w/dKi7HwLds9peNr7IF6LPtUssSL3IuNOYGLlHjOwFmfwe9OK3nxxRRgN +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIliYA3yrJMlMcLCMVYxV0uObw5q3Q+Zn4+NYyZBHBqFEE ++kvn6XYS2Dcvt6Ptafb+egXt7PQiX4xuk1EJQthbw8LHHFksw4XlK/hEZYEN6MEz +Nl6FAwmBymcnkmQ2v1saoWguYo5PAJb10t78p9wNmQ967hJsaE/VyX2nuWwy3sK2 +JEQfGov5ldbBWaI3aZWXDNAOlBZZiYaTkM41OkhTw7DxlEAMKtlD3FDpetB/Ra/HrMZWBT2ISI5K4C7FovZIWg== +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +KbLSYqE/CR9TL3ZELtBVg8UiZep5zt9moZHOJ5ffCHo= +iTSUWsaHz8xtfxXo170oO/0vr9reANk6j9ykbM5WiCYFw3OvMYMGyJ3KgPGMrF0H +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +HNqEbxya75q97PmRdMU8Fq0RpPGtQqmyQcXNysvYcIzmzwtTL2CB6nQWsuCPipUki6fsBjosP5fyCX/lXfWtP2aHIN8g6JRrW4TZbYQNKv4= +1V2v8QerKOmubvSxgB4eTATuLP/YkzXuFS9s9v29gfLs7HzcbbjmTiso+zmsCZ3S +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrRQLiV7W1lz/PImX//PWNLkFIuv7VUgU3+a6PwWzgXXaB4nsglac/VuhWclPIvVlgOIAaNg2YXbEjwkZqivaweq +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTJ841yfu7l5arGk2XF/kO8mJfg3iuq4dIggjGJ90dwx8t4CE+4HLudLp33Sh9joOLg== +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrRQLiV7W1lz/PImX//PWNLkFIuv7VUgU3+a6PwWzgXXaB4nsglac/VuhWclPIvVlgOIAaNg2YXbEjwkZqivaweq +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWg3MlyzvlsDpJ2tFZppFlDIgMeiVDQ2HRCla7COtNJPjhTSrDI29qVP4xDeSndmdE= +1u+XjG/2+GSQRv6EzCaWRQ== +uIl0RcuWgRCPZivR9HE5K6P0KAPh0+c0Ci+5o0mjZKg= +KHVPPgRdeAM1K9qeWQ6AiqCJ1c6DyhB9VCKPe+SB8o/S5eT66VcxZqE1Abf23JR5 +gApoKqs53F0hB+0nTxFBUK0sDmJFt2pcRuqhtiHPJ+voEfz/6grsNrnqbl6KL3aq +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmVf/86mCDVHuaSE/lwim7QrFi5TEaeF6XshC8H9LAxRJInVUSAqtWixvfFY8aKz+m1a1RsFEpCSFGYL1Q+qZONa +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +gHZvnjbK0+c2DWpdjJTIKjYmDJs6pOpneqykoeukYiI= +RAcgdll7MZc9X2PbkeTvEzNyiOpuxHuwyFCiwlCwtBo= +NgkU9z71J5Awi50+35yVrVFRMlqzLfxgW8oRH+8xszId0swAa3xYf+cQPC3GoRX8 +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +YSlit6erKXylrjSZnvyW8I3FYH6DLcfYpMAlko6lQqVBBd1eDeDGaujrHWIOJkcI +YSlit6erKXylrjSZnvyW8Dke2RKJMhjXyxnQzpVZgOM= +Ys4BPYSOoHeIx+ql4jJylD2VLnaOQOBr+k3Ytq5owd23kxsNazPnVuATSOUovBrq +bWoUv86VLMtyRQlo14Ahu3xfaGKmZUw7MPoURoMwHGI= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN68uMQcDaeKFBnSol/Upw31Zbihi4vGENQHlL+dj6DCvT +VmVrGQo2zRokW/ZuO9bN61HRAGek/auc8b4JvIZlfzGQ/b8xNkzKFp1KEGLxgbuwAYkeWczxvXCQWc0loQzSHyq5aP4vAsBP8KIObacZsHW2l0eK/68p8LwM/KD04HbjIFDKWSdnqDmGI5E/oEv2+A== +VmVrGQo2zRokW/ZuO9bN6/0hFCJypSh6x2UIBkV7VvXsU+cO8B7S9xx1VVZWBeu0eYDfkAI5IGvcY5NDn0MqTPYuqy5Bl3zY8pT547cBMFbo51+sa93m7/eHEKuy5GXE +VmVrGQo2zRokW/ZuO9bN6wHsvvxh4guxHr80r9yaINtJrvRK6UdvNL8JUA1IT/spxWK1U/dlzvPRDq60WZSm+w== +VmVrGQo2zRokW/ZuO9bN6xd9hVVPG/da3jOneq2RZCcce997vOo+rQywLRgLWiWuNr25+4z1FY+siXTO3eViyQ== +VmVrGQo2zRokW/ZuO9bN6/CxiXZULdhjyOp1jTEufd+jVXQuLPUTInFNcnpN1I4wKnR+4QeoyJLsqr/3r41c5otdZB5U+KnTPi6U0yHupXO5fT2t9XsGdz3dQ6SMRN8yg9FL1qHp4ThJ4cz/lg4X6o4uGI+UWHWKQ4ipM/V7GcM= +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN6/CxiXZULdhjyOp1jTEufd8fOViMKvV/4EBLt4qPjTM3P8imZTOW0FDMijb9jSuw0FVDpqK7f2sMpzWe9pEkHJdj7JuaHPwQym1UOAXnaGko///EYYbAEydMFZn+m+korg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN63WsPkEtKdp9rQFEV0mc0Bp0wD1zMIehj9MyHLROd0aILFdAc3MAmaemnxPAUiPekw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6zsXuunPbx71ngX6BaQenPNbA+Mg7i1nazF95o2HQtU5 +VmVrGQo2zRokW/ZuO9bN6xTkk9WeCniuLJmLBef2m+U= +VmVrGQo2zRokW/ZuO9bN65Mk3nfYCJEpzMz8EnuHOsgreh18KDp4UtHG8fOCTUhO +VmVrGQo2zRokW/ZuO9bN62oRed6hCMosT7UP35UqsiKbQbJoCKHWLyJn/Vx814Ro/Ee5KiD6gqXP3nh6dppi/w== +VmVrGQo2zRokW/ZuO9bN6waLqc54ChZODKGJgS1pLojVY2evr/6W6eipUfB4kUV4tIbbM82aET5PVAdbZXXdUO/Z4wJ1V7uPGDDu8Cb4YmQ= +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNuRprwNE+dzFP36dxj7lYWb +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6x8dPCIFym0qQ4B6l0auqv3VSU3SG/i9eUcnpJrb1L+X +VmVrGQo2zRokW/ZuO9bN6ziVGg/SZJQ2a2ljdz3XhcVn97lFqgG8ziUZ47BfMFG9UXbT2kS6y93cABsoh/aDUSuKrmA11kGt+hTSze7FVyk= +VmVrGQo2zRokW/ZuO9bN6waLqc54ChZODKGJgS1pLojVY2evr/6W6eipUfB4kUV4jBljvu51ZnW8hS8WXSpu7pv1/kMj8Ak/oIB5g6oKubA= +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNuRprwNE+dzFP36dxj7lYWb +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+Hp+GvFtSASnTbptRcIg2lSnsLRR4J22UX96CUCq+KR +VmVrGQo2zRokW/ZuO9bN64qagL+89Aq4rR/hcuQ6/QaZLgJxtVesokbhCUzfxCwfKcyVGoV3zyER2vrKLj8C4A== +VmVrGQo2zRokW/ZuO9bN6waLqc54ChZODKGJgS1pLojVY2evr/6W6eipUfB4kUV40d40RaKmxMWpfhvj8ZyruQ/Q/AdiPvG5CMZanDD6YE98WLACskd36bpmVVS7VPAS +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNuRprwNE+dzFP36dxj7lYWb +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6xUIvvF7vKpIldg0eDQ2GLdixkmDxxbU+OXh2/259dgakzCXh2oDJ0AfTKNrUbZ5d31ZrBNmK8mELWMQzPcKOzPm5KFJw1Mu/Hkb1SkNQs1d +VmVrGQo2zRokW/ZuO9bN69P4p4urRz6LNH8T7D85tUmvT6fU8K2BiEON885R0KsYZZwTue89GlNh5Rj+HJ/0l8p8Xk8+ZEyH9zTSoSVrZuM= +VmVrGQo2zRokW/ZuO9bN69P4p4urRz6LNH8T7D85tUnJY2h08qZtVSMViVJ80aOFEB7NOmEnSXBDfiqhTqlpxqE7hd3sxipptIDNtiux052BIE9Ab59zGW7HA4ampTWIfcrnofD4U94ciAzYjH7cMQ== +VmVrGQo2zRokW/ZuO9bN68SNBDbgryQsl/pB0VGbLvqWKmuDEMs3IPB8IEm2OG3x +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+UgxYwTsTpiHdnLypOQ/kZoFZH3aRRa3jGRdR795Y1mA6vXFA8gZ85TG3MqCavIWvAJnmCjVt23ZkwLlEM33Qocr5vG2sgJeT2xmYYZ/iICcOfrgbcyhaRW17sPNkIRlw== +VmVrGQo2zRokW/ZuO9bN6yYgZ33nPjoy4tOZmNaX7mQaiu/N7s/Mw1Ei0aMstGjWDlGg9LgiJMyZKxaW8bZyIjtyH8MUjBVG9J4w3mR65uirBBRQcKVp1rVnwRuAvlAOObnzQPL0B/HA/xlJ0/WDfA== +VmVrGQo2zRokW/ZuO9bN60vhRuPY/LJZF8HROzC0dFI= +VmVrGQo2zRokW/ZuO9bN6/6qy3lEE/QMSwWTRtFJoaMcGV0W2eYWCTCr8Hkveve9 +M0ZySqkmhuHCw6olbCKv99p0Om1sGOCLOPcvWBGiKm4= +VmVrGQo2zRokW/ZuO9bN6wgdH+0DZQge5QIlnRbbyGVIwpFYFAZXcG4ciFkbD8xGTtzX6dDhJzsy6JEW1M67sQDPORs3az4NjR88iM8Lcnk= +1u+XjG/2+GSQRv6EzCaWRQ== +o6QhOIN2Sc4SHELnst17uQbQrK2Z34ZMKBCsEar9NNzRcJW4KMeQ4yhsPKTJaw1j +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs9jY9CEkJ+mtN1RRkcQDmMZqWpTNzdr+Hy3p75kNCSdGSik+k6MbKPBA27LVqGIPTg= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +jGPLgFRMa/gLI5pn0C8oszLqa5isI224m2QOfckbxLELIVt69gRzqdxrFQlcGXIa +Z8UsPk1Q7HtwjRd4g01ryw== +n3tXewq2IYqiwFREjJjsxQv5FBS89YO3jVo+BgMdPGUBLkZx5+ZpOpx7LWu6PFKo8b9SqRNn6c5JZVzlsIkp+A== +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +xsCYviGksjEQaKpL56on/X7AnEB6uKwmFpBflqwZHQ4= +BIa0ibHnqUboaK9kYHYO4um2nnLtPSMsHb7nUhh0kKQ6CdlvwTSeB4h8LEiuwG9t +a7gbuXC0s2yINkxKhLLBokQAN48UxYxa6jCVIMQ9FeJoGd91ZEhhcfiymqUdYGqf +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYsXheOqb+ac7ivrKomECGJh6sAOIfkvF5oIqf+7bT65s +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQ/iPzQ+uFBITVsVoGpItD5Q== +1u+XjG/2+GSQRv6EzCaWRQ== +AN33R+yjfqqAkAsFnmP/r/WGRgGkDEv8Fnc8kjQsJ7c32mbK3YgEYOi8MYXpOqJy +cbvASHjpysrsjdY5RctXmG9wund6qoaXAfnCgO+o1nY= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXHKWKUVVa/rOrR3+fgEd7tp63QHMzA34qukyTQgDFJrLVYVJL/Ye3kNmAIiExi//aTWiBsWXWTJE2/bLQwR3ie +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDGWrMrUVsu+cay2sf0G/q6n9TVdK0jEcTi9z8Mi4uhlIHBQoCZCJNLutARg3AcC9GZkJHoO5OtM5wGrnT579QQ/33LGKQp33LdowTPKOj8mg+5XvwqiamHNJ4b6qkc35ZA== +TQRmC0vOvJ1P+lfyS3Os4fdA9rnIPjSDuS8zcTDjbSEVtc81+Y4AUEIsuAgPRMzVhLwEUrXGM6DCeZ1rhkVK8w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmUzJgj0ZVdf8DkqZrnrpLRzKvaQBgq7m7DBF8Wb/aWuxeFqQz2lv84Je+M0d4O8q9Ux2G7KTGW8VqvvCK6513pJrAuADi3mTtgVX9Y0JiEdnx9iCVypIiCmLG0BR48sQeM= +1u+XjG/2+GSQRv6EzCaWRQ== +y9uLKqgxuwiJJuaISH8LNlJR2o2Cr2uNLeYPxMDYl/0= +q4nc/jwATOMUyfSjLibfEelnou8pLYpfebj3/LvnkGg= +7fBvPEAf72Uv1YEDXbbQ3CN3MWrD+3vkr2VJCCzzEPRhwccGf00/aNA1BGgdy8WH +59Mj0OVU6q9AFARXxvxfN1dW2G3zJI+4zsdXTWh4XwFG18UDXnpLmLIfLIZ41jcpe1yBIt+f+JDrMrCbBNf/9Xy836/xgiba8x96DZU3Wlg= +pcGeDqO6jWjADfc8Oyd0b2PTNAiWtNjPy+4ZuJym09tcrllO4bi9TB9YXcFHDN8ulWbcZDzkd3vYSjeW6cb+7a07iV7nvIzEjupMBYIINGsixBBRFVjl2m3gGdMptbNGHA9dcrK5ewH6POHu2UFt2t23FwcWAqOHzR3sL67HAErpzje2T7IZlttmY5tQ/RPJ4QDTGGJ3YSogtm9/Z9ZSs7wMVRGX8M2Eia5bIBLZuxk= +7fBvPEAf72Uv1YEDXbbQ3Kl5asOP7Hk9Z+oJF57QC0c//DIVOxrgJlRJ07hlZuLdNPXnVpbKF/o/zidPewH4xQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs8ZqmZBUF1OtUXbF0cnfKWhyLN47Y9VJ4gMAghwAahsZHCVXCB6/L26wmeLJb9nC4o= +2+RceSNjD2iq0sRzepMyse3K/6KDWrRQWf3bA9DlS0lC8P1AVP/ryfVnqkju1FOT +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOEvWExeU8Y5S6VzOcEFIF/zQLgeURpx14jzr1s57LjK94c4cdiNmekvORipQcUMeV4= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== diff --git a/class/filesModel/zipModel.py b/class/filesModel/zipModel.py index 9542fa4f..4ecf80a7 100644 --- a/class/filesModel/zipModel.py +++ b/class/filesModel/zipModel.py @@ -1,327 +1,327 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# -#------------------------------ - -import os,sys,re -from filesModel.base import filesBase -import public,json -import zipfile,shutil -from pathlib import Path -class main(filesBase): - - def __init__(self): - pass - - - def __check_zipfile(self,sfile,is_close = False): - ''' - @name 检查文件是否为zip文件 - @param sfile 文件路径 - @return bool - ''' - - zip_file = None - try: - zip_file = zipfile.ZipFile(sfile) - except:pass - - if is_close and zip_file: - zip_file.close() - - return zip_file - - def get_zip_files(self,args): - ''' - @name 获取压缩包内文件列表 - @param args['path'] 压缩包路径 - @return list - ''' - sfile = args.sfile - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - zip_file = self.__check_zipfile(sfile) - if not zip_file: - return public.returnMsg(False,'NOT_ZIP_FILE') - - data = {} - for item in zip_file.infolist(): - sub_data = data - f_name = self.__get_zip_filename(item) - - f_dirs = f_name.split('/') - - d_idx = 0 - for d in f_dirs: - if not d: continue - if not d in sub_data: - if d == f_name[-len(d):] and d_idx == len(f_dirs) - 1: - tmps = item.date_time - sub_data[d] = { - 'file_size': item.file_size, - 'compress_size': item.compress_size, - 'compress_type': item.compress_type, - 'filename':d, - 'fullpath':f_name, - 'date_time': public.to_date(times = '{}-{}-{} {}:{}:{}'.format(tmps[0],tmps[1],tmps[2],tmps[3],tmps[4],tmps[5])), - 'is_dir': 0 - } - if item.is_dir(): - sub_data[d]['is_dir'] = 1 - else: - sub_data[d] = {} - d_idx += 1 - sub_data = sub_data[d] - - zip_file.close() - return data - - - def get_fileinfo_by(self,args): - ''' - @name 获取压缩包内文件信息 - @param args['path'] 压缩包路径 - @param args['filename'] 文件名 - @return dict - ''' - - sfile = args.sfile - filename = args.filename - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - result = {} - result['status'] = True - result['data'] = '' - with zipfile.ZipFile(sfile,'r') as zip_file: - for item in zip_file.infolist(): - z_filename = self.__get_zip_filename(item) - if z_filename == filename: - - buff = zip_file.read(item.filename) - encoding,srcBody = public.decode_data(buff) - result['encoding'] = encoding - result['data'] = srcBody - break - return result - - def delete_zip_file(self,args): - ''' - @name 删除压缩包内文件 - @param args['path'] 压缩包路径 - @param args['filenames'] 文件名列表,数组格式 - @return dict - ''' - sfile = args.sfile - filenames = args.filenames - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - with zipfile.ZipFile(sfile,'r') as zip_file: - with zipfile.ZipFile(sfile + '.tmp','w',zipfile.ZIP_DEFLATED) as new_zfile: - for item in zip_file.infolist(): - filename = self.__get_zip_filename(item) - - if filename in filenames: - continue - src_name = item.filename - item.filename = filename - new_zfile.writestr(item,zip_file.read(src_name)) - shutil.move(sfile + '.tmp',sfile) - return public.returnMsg(True,'File deleted successfully') - - def write_zip_file(self,args): - ''' - @name 写入压缩包内文件 - @param args['path'] 压缩包路径 - @param args['filename'] 文件名 - @param args['data'] 写入数据 - @return dict - ''' - - sfile = args.sfile - filename = args.filename - data = args.data - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - with zipfile.ZipFile(sfile,'r') as zip_file: - with zipfile.ZipFile(sfile + '.tmp','w',zipfile.ZIP_DEFLATED) as new_zfile: - for item in zip_file.infolist(): - z_filename = self.__get_zip_filename(item) - if z_filename == filename: - continue - - new_zfile.writestr(item,zip_file.read(item.filename)) - new_zfile.writestr(filename, data=data, compress_type=zipfile.ZIP_DEFLATED) - - shutil.move(sfile + '.tmp',sfile) - return public.returnMsg(True,'File written successfully') - - - def extract_byfiles(self,args): - """ - @name 解压部分文件 - @param args['path'] 压缩包路径 - @param args['extract_path'] 解压路径 - @param args['filenames'] 文件名列表,数组格式 - """ - - zip_path = '' - if 'zip_path' in args: zip_path = args.zip_path - sfile = args.sfile - filenames = args.filenames - extract_path = args.extract_path - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - if not os.path.exists(extract_path): - os.makedirs(extract_path,384) - - tmp_path = '{}/tmp/{}'.format(public.get_soft_path(),public.md5(public.GetRandomString(32))) - if not os.path.exists(tmp_path): - os.makedirs(tmp_path,384) - - with zipfile.ZipFile(sfile) as zip_file: - try: - m_list = {} - for item in zip_file.infolist(): - filename = self.__get_zip_filename(item) - - if filename in filenames: - spath = os.path.join(tmp_path,filename).strip('/') - if item.is_dir(): - m_list[spath] = [] - else: - if not 'other' in m_list: - m_list['other'] = [] - - dir_key = os.path.dirname(spath) - info = {'src':spath,'dst':'{}/{}'.format(extract_path,filename.strip('/'))} - if zip_path: - info['dst'] = '{}/{}'.format(extract_path,filename.replace(zip_path,'').strip('/')) - - s_path = os.path.dirname(info['dst']) - if not os.path.exists(s_path): os.makedirs(s_path,384) - - if dir_key in m_list: - m_list[dir_key].append(info) - else: - m_list['other'].append(info) - - item.filename = filename - zip_file.extract(item,tmp_path) - - for key in m_list: - try: - for info in m_list[key]: - if os.getenv('BT_PANEL'): - shutil.copyfile(info['src'],info['dst']) - else: - shutil.copyfile('/' + info['src'],'/' + info['dst']) - except: - pass - - shutil.rmtree(tmp_path, True) - except: - return public.returnMsg(False,'Decompression failed,error:' + public.get_error_info()) - return public.returnMsg(True,'The file was decompressed successfully') - - def add_zip_file(self,args): - ''' - @name 添加文件到压缩包 - @param args['r_path'] 跟路径 - @param args['filename'] 文件名 - @param args['f_list'] 写入数据 - @return dict - ''' - - sfile = args.sfile - r_path = args.r_path - f_list = args.f_list - if not os.path.exists(sfile): - return public.returnMsg(False,'FILE_NOT_EXISTS') - - #追加原路径 - src_list = {} - for fname in f_list: - if os.path.isdir(fname): - s_list = [] - public.get_file_list(fname,s_list) - - for f in s_list: - if os.path.isdir(f): - continue - src_file = '{}/{}{}'.format(r_path,os.path.basename(fname),f.replace(fname,'')) - src_list[src_file] = f - else: - src_file = r_path + '/' + os.path.basename(fname) - src_list[src_file] = fname - - tmp_path = sfile + '.tmp' - if os.path.exists(tmp_path): - os.remove(tmp_path) - - with zipfile.ZipFile(sfile,'r') as zip_file: - with zipfile.ZipFile(tmp_path,'w',zipfile.ZIP_DEFLATED) as new_zfile: - try: - #过滤旧文件 - for item in zip_file.namelist(): - if item in src_list: - continue - new_zfile.writestr(item,zip_file.read(item)) - - #追加新文件 - for src_file in src_list: - new_zfile.write(src_list[src_file],src_file) - except: - return public.returnMsg(False,'Failed add file,error:' + public.get_error_info()) - - shutil.move(tmp_path,sfile) - return public.returnMsg(True,'Compressed package file modified successfully') - - - # def __get_zip_filename(self,item): - # ''' - # @name 获取压缩包文件名 - # @param item 压缩包文件对象 - # @return string - # ''' - # path = item.filename - # try: - # path_name = path.decode('utf-8') - # except: - # path_name = path.encode('cp437').decode('gbk') - # path_name = path_name.encode('utf-8').decode('utf-8') - # return path_name - - - - - def __get_zip_filename(self,item): - ''' - @name 获取压缩包文件名 - @param item 压缩包文件对象 - @return string - ''' - - - filename = item.filename - try: - filename = item.filename.encode('cp437').decode('gbk') - except:pass - return filename - - - - - - +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +Hf+bBTgRv9pVfall8ODpvg== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +1u+XjG/2+GSQRv6EzCaWRQ== +K3QJU/hV27C5FAVoZ+avNNMfDMygeonfI5TFnvUHjhU= +X9BNoFOwiQAnMPqEk6FnbzU/oA+chL/aEAzGH80mBrNNxRMBdiGPqgDUeLF6rer2 +hUVhKZtvg+FKessuieMZGkP/5FS8EfJsx7HCvhsTdvM= +KbrESL0Rr2jC6vOleUlueMUgBS8ubHMHEf/dewOmXjc= +yUFFATi+ZVxTJRU9zfoit8QIFboIm1z3wG2Pz/1LIy0= +CjJS7KrzKM18mlLqJK9jKTB717Vvrubu5ix9tb047ao= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhLTeka+iJoxis1kUHdyggy+yXh3Xc8xsyir/5NbKP+Sj41j2UHgL27dZFRsmbh/P+Q== +KZTmaJLp+FU9X93j5Tpqtw== +0LFyB+q/4AUBkCrX53xYJedFoNZHYELHOLu/R25oBrqozNv9g917+/ANMSb6X+WR +yqM179SzW9HnBHozkU+IlaI6kf6Fax7Db/qE4wQMDFqfwVE+4rG3+IJ9S3c+p3Wf +akHibliG6j9uvBEg96vRehUZmhX3a+KyYm/Obf/H764= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +1VHvxkEcO4espNMpWHmnH1EgArAJXS2MAairjwNzV9c= +b4OJVZe8QyIpjuTpKXDL9A== +wQn8ViRVbUL8OLSIgBD0ID21VtRI2rJC6jqthFugWvWVef56vW1JZ4xiXoQ3GDDo +usrsES3VVkgHW6tBbEkpHTeLztsByVaJibbFv8kSFIM= +1u+XjG/2+GSQRv6EzCaWRQ== +l+sL2BKY+z3yDMAsWI51b39VWekdKmbfrOabwvVHTbGfiUobxgPhHW2OIZyeGUJ9 +wQn8ViRVbUL8OLSIgBD0IO53Ya1+ckG2IX47+ykaGJU= +1u+XjG/2+GSQRv6EzCaWRQ== +MxWDzXEQjj6mgdfarjHFtAA6unJa5l9pTo0P8ga3JDU= +1u+XjG/2+GSQRv6EzCaWRQ== +x41zVQ6DCtWXndDIq7U8L+5quM3ARMCnVm34NUX7l/TJzZZ100EvdUXHXI2cFH4H +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P7D7U0y87ntgFdQV5REShmme9ACxsySMBZr2gsEQniKL +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +akHibliG6j9uvBEg96vRer/Fg5zTlM8oj8hSAsYq018= +KZTmaJLp+FU9X93j5Tpqtw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +1VHvxkEcO4espNMpWHmnHy+YX2/U7Ip4Uqt3suxNhindpukzgQ1aM+PshcW/1dV1 +LuO0LAjc92aYQU4tSUEvi4x4NJPWR/eKWjRfKMSRL8o= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmV/41TA4054wrqxXx/Q1i8cmtFJNrxypDZNiWIVVjiYLA== +1u+XjG/2+GSQRv6EzCaWRQ== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +ei/1A+2pKZSasjYF/t4qojBnh7Bi8Fen8Fcf8mKjT9IuWg8ZCZGedub8Eqjf8qUh +4nsuLyRybkZg8C3fJCsRPXDWwLMccXwEpFBdFK0JOvg= +ZFD6pnBshb3mpu7xvvuMLemrazA4ssXDDuW5SINX4ZLsvLIwaJ4YyDANze64OGqUEj7NCJs+t+zkcUskHr+gqw== +1u+XjG/2+GSQRv6EzCaWRQ== +g5xFPP9LKBd4uktrRYzGf+p1NdL+k6huN6CrzYXY3fPVQzhtKStMTpRTZpRqmhGR +1u+XjG/2+GSQRv6EzCaWRQ== +wkESzX/Mo7zNrX6wCsHIMgfYkIJy+Dcvs4tK//49fTc= +wlLHv6kT3Q/RmtMBN4nDAbqo0o2IA7mU8WVNqZvoB5I= +VmVrGQo2zRokW/ZuO9bN6+qRbBr2ygG3RGcAIOk727ylzODmV8VRqZ4YKvduSUCu +VmVrGQo2zRokW/ZuO9bN62psx0nYMGJaQ1S1BUtE6l8lgsopQJ3Qf5syQpAasAAf +VmVrGQo2zRokW/ZuO9bN69Woqo49I0Fga24Nj55N9ZL6pyjjGewhvRJVBn8x2mvRw+9QouFIpzWY1J2Ub+ZqxCSNlCNDuqqN6YMHyK4kxCg= +VmVrGQo2zRokW/ZuO9bN61e1I6gZaVwmbMEHc7jd9wFEHpeilcxXyCkd5bmWx/yK +VmVrGQo2zRokW/ZuO9bN69+qNrAfkaoVno2zP/PIMh/5aA4AiEej6zodAVzZFzS0 +VmVrGQo2zRokW/ZuO9bN6xT3wQu8/ZV37F/D/EekYn8LNRE5UkGbTG1UxYqNO1dyU2pay/FWt6r8mJiKZ4UU/g== +VmVrGQo2zRokW/ZuO9bN64KhHtDRs0SmcOS1WVIbDs4jrgwkpIqneJssR9hHHlJ3UJd9tZzLLz6C+UQLUJWWGbgxXo0UEEapI14RLzluAUk= +VmVrGQo2zRokW/ZuO9bN64KhHtDRs0SmcOS1WVIbDs6tF3Tt2+gNwTBcUtu+YYQKSt4+UQPUctZ0/+hfCqv3GxDub0pA8qeNN8AxvOE+5lk= +VmVrGQo2zRokW/ZuO9bN6xT3wQu8/ZV37F/D/EekYn92m0FWAHkcFEifdcSefZI3 +VmVrGQo2zRokW/ZuO9bN6xQU/k6xhXMbxscs7Gix8nNqUkjAknQH3LrGVvAyjYtQ +VmVrGQo2zRokW/ZuO9bN6xOutEc9teLv333RLphPxTSZMBPTZ5nEHEAZCqisJAYdx733inB6pPVPHnS53cxkslRzdzhPLD4mmySs0XcaZo8Yb7xMuMGGlmbahCRF6eC3yiiMtXXp0R7BViTpiJ04fqWtzkElISMu7z4pkkwr23TjUMjTjCelUG9Lwgmt+EiK +VmVrGQo2zRokW/ZuO9bN6wv73IH9kSzXKMUmUPacSk1KDsdrjROzTN1kzduuP0l3 +VmVrGQo2zRokW/ZuO9bN69lJPu1G3iX01kmj1/0ynPc= +VmVrGQo2zRokW/ZuO9bN63p1oaV/8/rZOHP3bxdBsLvxN3810q5P2uCqBbefHbrK +VmVrGQo2zRokW/ZuO9bN6/kiIWH8hJ6dJOo/fnNj9VLcBHtqpOZ/Rc4AjXcEm1edHKkbtiFAlXrzE+rfp/AeMQ== +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN69+qNrAfkaoVno2zP/PIMh80w0dM3z0phdMS+lT6j/xo +VmVrGQo2zRokW/ZuO9bN65rWaakWr3uiXA345yT9jOU= +VmVrGQo2zRokW/ZuO9bN65abAZ39yZLyKCl4nd6eov4HdKauw4+toNc5YoXvsAeW +1u+XjG/2+GSQRv6EzCaWRQ== +1VHvxkEcO4espNMpWHmnHx0/qyT1YxdKfIVIaA4qitI= ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +J24jXQQti6wQv5pCoF2ZrRvdf2ev6P6vInKuFAO8eQfewP9/G67aA1dZDLRwjari +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P7D7U0y87ntgFdQV5REShmlP+1p+XjT1dvUjFUZHsqC6 +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4um2nnLtPSMsHb7nUhh0kKQ6CdlvwTSeB4h8LEiuwG9t +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +YSlit6erKXylrjSZnvyW8I3FYH6DLcfYpMAlko6lQqVBBd1eDeDGaujrHWIOJkcI +YSlit6erKXylrjSZnvyW8CCccEStX4fvFvduMtCGi54= +6Vd1Vz4KEbM/6qVZBGLAGuJhapKEU8TBPzOQYI4q82gnJ7Pu1Da4cxfKJ3J95+e7Z75/5e7XrUkjQFagToxKXA== +wlLHv6kT3Q/RmtMBN4nDAdAMeIPpwO0Pgawv937x0Zw4iwEVDD1EIlJA37GxF8mZ +VmVrGQo2zRokW/ZuO9bN64/tCOCyKu41lsl+3BVlFMtjVa1DXtvHbVFGS5luzEIFtQp9eHGpneVvKz65gzXCXQ== +VmVrGQo2zRokW/ZuO9bN69Z4WTbfjflDn+PHJe4HyeBCMRIQIdsxtS/SkQUyTK46 +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64IUBVOpZieySJUi/5Xp7IP9i2hr//841YnIEqNDctKsA4PCQR7FmaZMQHs7tdC+NQ== +VmVrGQo2zRokW/ZuO9bN63KtgQxYoQMLiw/swXgEzq6F5dbUK15QUQFhJ/fqYDb2bGgnr3J8HSW6FkIPXw/QAPGbSGA6euyj6sY6jr9uD88= +VmVrGQo2zRokW/ZuO9bN60+Gj1vDNaFgHQa9qY+WYQ/+8quDVQ7mW+ZFMiCB6dK3zTXaMhAFlgEsY2XTlgH4hQ== +VmVrGQo2zRokW/ZuO9bN60dNkfoHcjUpCVMrBt5WSA9PCuYzYl0QLyyWqlMopPgY +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +6JhaWzGWadTJ3yzCk9OQNgkLy/m1ahTUuEy+6FiV49HNwHkAN2ZMxFTsufeIA0ig +KZTmaJLp+FU9X93j5Tpqtw== +dar6N0tUAb9rU8/aX1b3tGumQ1hhfk8O6qr6gmiQ1+eBvZwFkpECn+RP444+i510 +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMlEDkaPg52QqeaL1e16aTiZGdP9MF1i1K/F0icaNF/FRZY8CjFaji8Ecm2WEpXZbMiyduIw57U2lMT6kzLqch3Y= +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4kqIZZgv1yXQHpqfJ4aNJ54bUNUYTYq/0A+dhHby52Au +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +6Vd1Vz4KEbM/6qVZBGLAGuJhapKEU8TBPzOQYI4q82gnJ7Pu1Da4cxfKJ3J95+e7Z75/5e7XrUkjQFagToxKXA== +1hs1FfIPISXmd7TJ/r+Vl5VOoCywAZyr7zSPYceKQIcrfmFscSLitBH8rHfUTRshPd3qsDNW2TEFVB/BherqRUPAAaKNdUCadxkxouQ603/UPQa2mdlVSAGn2R+CvNrg +VmVrGQo2zRokW/ZuO9bN650CvvO4YLL+rKCQnmVaPF2En1jKNRAIMYaPmWw4DTM2qNuk40r70LOd5/jqlUfMYw== +VmVrGQo2zRokW/ZuO9bN6wsF8dCAuyXrReoI3w9Yj8ejyyRZrswtbKGtDKu6ywDGElhkS5s5y7BVqVo+FqDhvA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wOXhaCTOJSjUCWUfjx+YyYNeSRhAnvmOzc5j0wt6pWe +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNuRprwNE+dzFP36dxj7lYWb +VmVrGQo2zRokW/ZuO9bN6xrJVNh48JOJRHCQYI4xAH6seRNi5O7iARUSsCPjTbt2 +VmVrGQo2zRokW/ZuO9bN6/GmqY1U6uFDyEbXCwKITV7VrKjU57dBsJG4vNk6MQiq +VmVrGQo2zRokW/ZuO9bN657WYPgEitoQAm574YXmF9lUB867LK9es4C6vHCx401D0rMP+Q/+dVAUlvxCaivz4QRggUPK+VUMIqZGEHb1ljU= +u8vW5y2SRttIHP9hhx+fqksl4XiiUx+QjIOk2+hITXxAagiDFp3uE2PeHg2zFP6D +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOH6nbHs2WDQfzsBPtsBHumfOcU290Wam9UwXjcW8zT/eaFb4gRZNo5rRuNz1TQ6cJU= +1u+XjG/2+GSQRv6EzCaWRQ== +O2CqINTuIULxX7Gu5GskT/Nm15YFovaVXDj8ylNfQi3d2Qbp7uZYhyvrRE1b06DK +KZTmaJLp+FU9X93j5Tpqtw== +t987AUZxQR72IPRcAqvQ/tuxrpVopv9MhEHHX4HHOFnF6WZD8PVxYK60YbaBpzoa +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +c1XU6vxl7MsQKGo3r4amMgGxGyqG90Nz+St+6838nV5Hy3ZAmDi9Ecxx2gyS4yZO +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4um2nnLtPSMsHb7nUhh0kKQ6CdlvwTSeB4h8LEiuwG9t +XKWK9LfYDT5w2LEWWxwkc6l2k+0eByijzzjX3gaj5k8= +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +6Vd1Vz4KEbM/6qVZBGLAGuJhapKEU8TBPzOQYI4q82gnJ7Pu1Da4cxfKJ3J95+e7Z75/5e7XrUkjQFagToxKXA== +1hs1FfIPISXmd7TJ/r+Vl5VOoCywAZyr7zSPYceKQIcrfmFscSLitBH8rHfUTRshPd3qsDNW2TEFVB/BherqRUPAAaKNdUCadxkxouQ603/UPQa2mdlVSAGn2R+CvNrg +VmVrGQo2zRokW/ZuO9bN650CvvO4YLL+rKCQnmVaPF2En1jKNRAIMYaPmWw4DTM2qNuk40r70LOd5/jqlUfMYw== +VmVrGQo2zRokW/ZuO9bN6xIOVX/jmHApY1vYi7p62efnJA3RwDkPnY0p8K402cIDtEidZgORRD2w1kwOy8ktqQ== +VmVrGQo2zRokW/ZuO9bN655gCVY8shnXyf3BTAhOCEseA+FmLUdf2zTZRuj4Gshw +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNuRprwNE+dzFP36dxj7lYWb +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN657WYPgEitoQAm574YXmF9lUB867LK9es4C6vHCx401DoXSgI87+dv1IXTMOs5pogfgyGxM8q3ngzmVKGd4oLv0= +VmVrGQo2zRokW/ZuO9bN61dxACaOXPUs+kKrrvH8m6V6uiphjbaKMJwgGptqRuCR9Cj1NHc2Yj+FdsrtTHaQoM49Sy0pv/Om5EdLdtFFjJ5lg/dZmMsubirsi45j9AT4 +1u+XjG/2+GSQRv6EzCaWRQ== +u8vW5y2SRttIHP9hhx+fqksl4XiiUx+QjIOk2+hITXxAagiDFp3uE2PeHg2zFP6D +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOFPUBPOYID993hmow4PfQhItCgegxeWNdVD4M5OUfiqV7yqGqj3L0yy/roD2hGd6ho= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +D5nlraEpeGmKL9lxoq591txjX2tGuFl4D7WJZ/91lnMQ6WZrDXvU2okqtgoaqWSS +Z8UsPk1Q7HtwjRd4g01ryw== +VcOpbkIFMz59VlIHGus1M+TodC/Ia97E/RilXZKKBR29V7X/RlCXQzuWibEOEJfo +c1XU6vxl7MsQKGo3r4amMsSWef3BumXlCWI6QCsp6pMACoHb/XNQfx2cpKV7J/MH +c1XU6vxl7MsQKGo3r4amMjvzqCkQbmHuG+orSe7QiThO3NRqf8lDLQklcqf6u2x4yWOeax5QyyroyrE443cDvw== +c1XU6vxl7MsQKGo3r4amMlEDkaPg52QqeaL1e16aTiZGdP9MF1i1K/F0icaNF/FRZY8CjFaji8Ecm2WEpXZbMiyduIw57U2lMT6kzLqch3Y= +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +0NowFhvxcpa35RgS0fkY9ejQNksclvfpv75ehCpzJxQ= +rTZ4GehPWBQfXf82D1FIzy9S8wkwIsZ3K9dIGaM8SqbP43GsEZhJZUm22nJ+2gPqOOxkmco8qaHhW3SBF16WUg== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +BIa0ibHnqUboaK9kYHYO4kqIZZgv1yXQHpqfJ4aNJ54bUNUYTYq/0A+dhHby52Au +77Mf9+1xBvZiUeNTtU/s60Kyn+UEu/OgKFt23E+wd8BiTmgbWFTzjcyPW/imPN9p +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYmSt8oPS+2lwNla/wCY62SBZ7HAsGrtjJH7dzi+5hXSr +NqhMbY8+EQI8zhTG6Zh2I5csQ+/eG6egKX2sKZJStJZk02bOBi20A8UHGNy6sybt +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP263Ug7hf7sm5bqxRn1G7j8CZM6KmOHJcbEZNXo2mkEA3MxUp7pEuHhp8IfRMvwD6SZPogdTtHhT5unY8iIrUiU6GaMq6uosuXq8hjxV9nUgwa2iqC1n6Ziz6tiDdPy1mw== +wgR07xfoapmx6eEnFHXXYm7IOJ/6J1NThWNK2EdPRzYwWPvLP+jlqDp61mwE1mzG +NqhMbY8+EQI8zhTG6Zh2I0BiMnuwr8mLUTT6rraGmzoW2r/izQNInq2UmHNhRbv+ +1u+XjG/2+GSQRv6EzCaWRQ== +6Vd1Vz4KEbM/6qVZBGLAGuJhapKEU8TBPzOQYI4q82iJ2tHfDbjNfVtFa8KsTVYDHjxHgDB1l5TRkSasuLGs5g== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN61lflwobhmxel33RwHEAzsw= +VmVrGQo2zRokW/ZuO9bN650CvvO4YLL+rKCQnmVaPF2En1jKNRAIMYaPmWw4DTM2qNuk40r70LOd5/jqlUfMYw== +VmVrGQo2zRokW/ZuO9bN6wsF8dCAuyXrReoI3w9Yj8ejyyRZrswtbKGtDKu6ywDGElhkS5s5y7BVqVo+FqDhvA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wOXhaCTOJSjUCWUfjx+YyYNeSRhAnvmOzc5j0wt6pWe +VmVrGQo2zRokW/ZuO9bN61ODJTwuVdYa2e77kV4UyyiKC6xmDTYLpzmUEL6iaKIotT6XRfjdpuPhvefHRzC+7lCYolxcDYktKmTY39wnf+k= +VmVrGQo2zRokW/ZuO9bN63p1oaV/8/rZOHP3bxdBsLvxN3810q5P2uCqBbefHbrK +VmVrGQo2zRokW/ZuO9bN655297gigIv+4Ufy0bubbx7ffTJUoURR64bKe908x4ap +VmVrGQo2zRokW/ZuO9bN62cJZxyhxghVn78AgYWoneU= +VmVrGQo2zRokW/ZuO9bN63LTkM1jyAeOymc1jas3GbdSOgZt1gFP6QuzR+PijaPfkXI6n5+LDNetR25SN0GXgg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkns33MIHJ07fAs3bEPA9prEegTrYMIIXPrzC4jD9xKXsA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+VE/nQEHJOLSC1DnxvG5TiVhVg6gVuqdGuJQhlv12yWuKoI97jUkrKrXkDjch0nxA== +VmVrGQo2zRokW/ZuO9bN66Stm18E9cFphVzr4DVvkp0eMQP2KdIRCCvQSRwvi/ogRLfVX0ez0AMMWvL6BswFfzkKtdTc18Qoh/tgpwV2o+Kht3IBHRneKuZIpe6ZGcaJTVPTytE4Bqe8/Zl36QIobQ== +VmVrGQo2zRokW/ZuO9bN6xSxF3eqlbsmTCkJFu+pH46xsXmb4t0jdQQcKfcGkEaM +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmgImK713UKKRnRLyQBV8gyjNySqhD1ER71FyIggw6WuQR8Ewb2c166ROgirDfRPbjt18mJAcMOqb3NN1zON9EtMy2glRMsNpgCk7fMSHxR7K6cqkD4EtrpnRF/O2N/bUo= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yaURb+J41vJInDMdNBqADJBPzFwU1DWEhzV7Igw3NGvcy7cQ4IWtwYwzl4xxnS4q7aao1GGQ/lpjefMWmVC5io= +VmVrGQo2zRokW/ZuO9bN63LTkM1jyAeOymc1jas3GbfaLzRNSm7AkFFrFp+7ESwXtG1lg7SuOb/Fvk+nDWM/LdSTEbaou9dv1kw9992cO1wzuimxPOSJKkAKKU5hd+6f +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6zsPJOFHzvayEpAPvGiJ0lAWXca0TugmxSi8hINZZvoHQ1Y8EIw5ZU/3qx6IfU7mzw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkqG04WXvd0zYTVxLMDUCWknQtobuOLFvBEBrZieoy/MQ== +VmVrGQo2zRokW/ZuO9bN61TKHwbNGf/2Ub0vd756U9O0Gtl0vapnf7HpdYx4U2ib +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkk6a/inNeVFTn9NarE6P/3LG5P9KtqHegfYsmaf6Actxw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN68jWU1VqsROJtSXOaVTpeKWZDZ536km13XBycpNTV7Nhu/w7ITcYY+83JHaD4GV4Ww== +VmVrGQo2zRokW/ZuO9bN67FJHoI0w8vvrPd3RbddRrDBThH0/mxaArrt9hDAJ1SFmBLkHrD34oMhy7mWtOnyYg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN65aFRI029wWEhVZc+cvXTRpzda0UlKTw9izNui/xkgdD +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN6ylpzdD3c1E8cbuPHoSW5RFJzxWCEvXq+6grREYT2q2eRpfKq4CUkXaSg9PBb07Brg== +VmVrGQo2zRokW/ZuO9bN65qgzI0QqVLALq/Lz0RhHgVj4W6v2iAhy3bpNtZbOHJ8qR+FfOTDkpmrnn3Bg7loWg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknNd/k4iuNp2bbqaDmaAfko6jVO83Qfz+7nZDbM61ZOu8HuZ+ZNu6hMvpayaQR5SAc= +VmVrGQo2zRokW/ZuO9bN61TKHwbNGf/2Ub0vd756U9O0Gtl0vapnf7HpdYx4U2ib +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknNd/k4iuNp2bbqaDmaAfkosj3Q/Uyc4ih1OKYxi2Aoat/XPMKk6ZFw9dS97mdyNCW7sbOTUzQ7vKis/hh/FWBl +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67XKiXTToYD4Xj8EJf9tU1/Vu+G41TBzTVvAU2KLSeCv +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTcB3+DRCAF+pezmRjCID0udaZbU9+Bp25aQzLB8QDwQjJOciYA4ZMGenxD3oVSnPjhf/xssdSo8p33UNNP+7LnwEXEbYt+fXn2F5oiKjC6Gw== +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGez0gsOf0QeJW43sHDXr/9AmAy18p0NZvV4WTviIGdP9rl3wBU2+XWJNGEpDCn6tY= +1u+XjG/2+GSQRv6EzCaWRQ== +mObL0pW76xtWwwYeubgb11DPfOo/O10pxYUkfDIVCjm8OCRNT0T/bbX1vgDt/X1W +KZTmaJLp+FU9X93j5Tpqtw== +H7d2MP+WgR9k+129/mRN7ij0WzXIk6KGh6j7E1fgHISsYBh1T2OGWCH8QUwPOkcn +c1XU6vxl7MsQKGo3r4amMvdbawOnt/oBPSHXSrswDVS5Xue1Y7sB0F2vPYt641B/ +c1XU6vxl7MsQKGo3r4amMpPnl/Q0nxs4Ul/v+JmLHipRvS1iO3+JhmWzafMyTI0p +c1XU6vxl7MsQKGo3r4amMqf9zivp0Qb6thhCnxaiYdHXwUNLWpYP828px7Gieygp +akHibliG6j9uvBEg96vResIm2pvfeBRQxNU2J0BQeUc= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +YdUmdqAC07VhbSt1h6KCDASPIXTwTSkv8cm4ToJ+lpo= +g8q6WqkWsk4SUh3d5nzGDIJanaAux29CDdQxdeG/k+U= +Vyh8kMpaq2Q+odra9mDJ3Acz3kaMgWRroPCWRIzYRMY= +wgR07xfoapmx6eEnFHXXYmnjxw2aWD9jyym26bSe8wdUky12ZBuFKfAUxS++Pix0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW/27EoqfC0X59kkH7imoHQb/TQW5074PHVFRKqgD5aSA== +1u+XjG/2+GSQRv6EzCaWRQ== +nZKk1bbhIicHthktrQ3ySmJb3nXruK0WPj1O/nD1Op8= +HI5OOAOTfAAnGE3XBxyr2vdUoaKdz9Yp4Em51oFqNhU= +mZ9rrP32sP7RJKYMPGILD+78ba2l0mR6sa16eW97WcI= +O3CUgrw2GJfB+mDjH5+NdlB3aQOXnKkk0z1WAt9Jmo/evqziM8DhkT0YrYGb4COh +VmVrGQo2zRokW/ZuO9bN67lx9HSk8eNN2hBCO4HpKRk= +VmVrGQo2zRokW/ZuO9bN69ZjyuXvRyGNy9kZJ0BSUPrXcl5zPRPxpUUNlqBFMfTepJuwObNjyv5s1Bwxw+0zHA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yiPlSW1IkAq8PnO6zy+iP3jJCfd1bhtOH5rekZFEjXi +VmVrGQo2zRokW/ZuO9bN6yIV96dETdHvOTzc1eoxsu7+haDOTWWWdxKhfAdfjn09 +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNuRprwNE+dzFP36dxj7lYWb +VmVrGQo2zRokW/ZuO9bN60+BaOrNhyGitXORdqvsMmwFtJw9x+0nfzpYbP/xMELoydQeOelN/AlsLE8govEqlMQrfUl1PeNk2rX8fh0ZElrak4Q4jLc+GGiQ0DLMMpnHYH/V4LeOzo0DpkYIcND3BA== +VmVrGQo2zRokW/ZuO9bN60jWe2OHiR8czCXypGRfxf3KT4lPFL0wUA7wcV6fbrUq +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6xYvvPJ68rqA+nqtjvksoecuaI1I6HrJYyQuBkEcxg38GjzrZVp9q6JUUtfh0kiUIfnKe6wTljkMngNv5DLroHo= +VmVrGQo2zRokW/ZuO9bN608vqClPKbLpxLXcA30riX215gq1D0BrebU6pLhAhTm2 +1u+XjG/2+GSQRv6EzCaWRQ== +IZj4TYq2HlScfSdRkI+QP1DXXDz3reXEAHM/ysmIbguf7VfJ/dsB+EE4b/l5XAWj +WHkOzVx7seuLmxs5Hu+l/tg64oMMngzQdC6c5uFYkJSfhDT5Ar0GKZd268ltWLHQ +gC7pmugJ3/w5trYRkDiKMWt651l7qmaQDKAvk9X5I0aA2tjC5DMB6fPkHfzLvY+S +1u+XjG/2+GSQRv6EzCaWRQ== +6Vd1Vz4KEbM/6qVZBGLAGuJhapKEU8TBPzOQYI4q82gnJ7Pu1Da4cxfKJ3J95+e7Z75/5e7XrUkjQFagToxKXA== +1hs1FfIPISXmd7TJ/r+Vl5VOoCywAZyr7zSPYceKQIdJVXu7JYnBUb+iQtMPqcsojEatZy5pDdbDBezG5DA1qLdz7Wh2R5B0piUodv86TpthC/OIBMMyceg05hLT05OZ +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN63uaPsI9SGIYFo4qu6fQZHL9GGcTmbqkN3kKfV4iG89B +VmVrGQo2zRokW/ZuO9bN6364+6D+dT33Zk25tJjqlVgtoL29ZI9bEfBKQ6cfJNwStfozG9mThVS88sNzbEnfeg== +VmVrGQo2zRokW/ZuO9bN69uIuNlhHDMeqLOAnCu5uPWT8evwqXAwcfDwsmWoxBJB +VmVrGQo2zRokW/ZuO9bN6yMdhcWuddd6xv9F64cBYywreWSVvaR5B3yfWI7OYuMP +VmVrGQo2zRokW/ZuO9bN60D33f2rnzf/qOtpO3Ttt+1LbZoGyDvOeulSnp2R0ilck0EERjIpT/ktNM4srJBraTngKI5B04/LBl3323wp0nE= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69ypGNbHyZMpYxMtQ3L+IMga3+inL7nbSVVsRnlPcc0U +VmVrGQo2zRokW/ZuO9bN64Ads2ODFtiVcSlOcVp7F8BTH9nWa2Xc95X7mNQD1kCu +VmVrGQo2zRokW/ZuO9bN60D33f2rnzf/qOtpO3Ttt+12NIPaRofJCp377XaLKfiybd59Ihi7pD1c3XhLcDanb1TM1C1QH4BPTOHZUBJeWHk= +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUikKCfZfG5oV8DWImIHip5MlpGXTmv9J1XV3V25fcmp5aa8KtmY5GAfonB0YuJLBm3Qyg6ipnG5U8MSK0eJ5IXtPW102IBhcOrIDStubnpSgQ== +1u+XjG/2+GSQRv6EzCaWRQ== +u8vW5y2SRttIHP9hhx+fqmx0hSbFOYRP5+aWkTy93ZFnIE/WAAXjKYo8eEgVBv85 +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGZIAyEpuVq0D9UpB4o0dY00eifTWefY3mmHrggp7bGLkFRUvNVINpyXDZbESEgd4Pe4G8AvpwDNkWSLrOyNGKY +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Y9+a9leOyTyP83EF6Ql2IDgu816zIZ8AFaS4gCHUHX0nvp4HPB59n314/EkGNhAM +dZw+CO0w3i/S02Fo8A/bXw== +Z9W5TG7rzK3ZhbnWjdfJ37W7cUH0ZUZ4etA9G1eKq/5LlOl/dP9tIF16dAZSZX24 +kjVV/zwZdFNcXb0/4WIo43yY3WhctfxqYlJeIJT/Zt4raVptLNgdCJ85x2QCj7en +v9zi26p7nzNT8ihOFEIGAPHAc1CmBd9eN42Km9GL48E= +dZw+CO0w3i/S02Fo8A/bXw== +OMrLNahybxYaZATa0EbsUraPl9ZQXb7EqnmkiDHNafM= +GMvSy/NpCRnN8GukyNJXGQ== +ZurH5ZOEBjKhuKKsOJuaZcaZjwgsQXIB71JtU6gEl5anuc8gN8IUF/VRflvF49gf +tb7N17ccXzCvhV9hZH479H/o3ElcTFJ2wxaYkBX0qHE= +ZurH5ZOEBjKhuKKsOJuaZT9OROQzGwZwPFsfOlMOqlW/n1s9EF2CXY7nIzE2BeWQUoQiVGUxtmYToskldhUSJg== +ZurH5ZOEBjKhuKKsOJuaZZOj4gtB0ZybAayCdpeA+rzvR2xX3QPBG1G+d2mABcg83+eEtNBqMdpVGWRkdFdjju+Fp/5HLdmKRV5bbO3vpgI= +xF/vKN+xQbwK2H7vBii2bVr0r4cs4Y9Wc0/WOSJt5a4= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmA0y/dvI19s9e1BRFCkk/QzH4UG1JBu620e7CQwjbQOOUvlgFJz4yLxls8aSEEo +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P+yZLrQY+X/LEpSBMw7xwfahIuTImHO5EcUQN9HQmgqX +LikBEoRjt8wwWzUZNw+jJqDVmHo0aqmO8fx4adbRXm90KhRgBH5ZfsJyfEAx+4JR +akHibliG6j9uvBEg96vRei563dWr9knc+JrDspjHc/A= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +BIa0ibHnqUboaK9kYHYO4qFxMPyUysXdjYA1FqqhK4UMv4PoX1vehXtIZKdqRHbG +b4OJVZe8QyIpjuTpKXDL9A== +PRG/YRzXVD8iVR3bzEcUnlkfSwESl/MBfBcCyRS23XAS+yqB8BD2Fws8ohDlx4dxPS4gXd1cpWdmGRHNT+D6KSwXWu5PYPEs39ffK4i+qWM= +usrsES3VVkgHW6tBbEkpHTeLztsByVaJibbFv8kSFIM= +5wsVwKLrIjIWqeTNpSVp9SZAE79lgehXxNsrtfaN8Wk= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== diff --git a/class/firewallModel/app/appBase.py b/class/firewallModel/app/appBase.py new file mode 100644 index 00000000..3a991af8 --- /dev/null +++ b/class/firewallModel/app/appBase.py @@ -0,0 +1,78 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - 底层基类 +# ------------------------------ + +import sys +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public + + +class Base(object): + + def __init__(self): + pass + + # 2024/3/22 下午 3:18 通用返回 + def _result(self, status: bool, msg: str) -> dict: + ''' + @name 通用返回 + @author wzz <2024/3/22 下午 3:19> + @param status: True/False + msg: 提示信息 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return {"status": status, "msg": msg} + + # 2024/3/22 下午 4:55 检查是否设置了net.ipv4.ip_forward = 1,没有则设置 + def check_ip_forward(self) -> dict: + ''' + @name 检查是否设置了net.ipv4.ip_forward = 1,没有则设置 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("sysctl net.ipv4.ip_forward") + if "net.ipv4.ip_forward = 1" not in stdout: + # 2024/3/22 下午 4:56 永久设置 + stdout, stderr = public.ExecShell("echo net.ipv4.ip_forward=1 >> /etc/sysctl.conf") + if stderr: + return self._result(False, "设置net.ipv4.ip_forward失败, err: {}".format(stderr)) + + stdout, stderr = public.ExecShell("sysctl -p") + if stderr: + return self._result(False, "设置net.ipv4.ip_forward失败, err: {}".format(stderr)) + return self._result(True, "设置net.ipv4.ip_forward成功") + return self._result(True, "net.ipv4.ip_forward已经设置") + + # 2024/3/18 上午 11:35 处理192.168.1.100-192.168.1.200这种ip范围 + # 返回192.168.1.100,192.168.1.101,192.168.1...,192.168.1.200列表 + def handle_ip_range(self, ip): + ''' + @name 处理192.168.1.100-192.168.1.200这种ip范围的ip列表 + @author wzz <2024/3/19 下午 4:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + ip_range = ip.split("-") + ip_start = ip_range[0] + ip_end = ip_range[1] + ip_start = ip_start.split(".") + ip_end = ip_end.split(".") + ip_start = [int(i) for i in ip_start] + ip_end = [int(i) for i in ip_end] + ip_list = [] + for i in range(ip_start[0], ip_end[0] + 1): + for j in range(ip_start[1], ip_end[1] + 1): + for k in range(ip_start[2], ip_end[2] + 1): + for l in range(ip_start[3], ip_end[3] + 1): + ip_list.append("{}.{}.{}.{}".format(i, j, k, l)) + return ip_list diff --git a/class/firewallModel/app/firewalld.py b/class/firewallModel/app/firewalld.py new file mode 100644 index 00000000..7e796a81 --- /dev/null +++ b/class/firewallModel/app/firewalld.py @@ -0,0 +1,750 @@ +#!/www/server/panel/pyenv/bin/python3.7 +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - firewalld封装库 +# ------------------------------ + +import os +import sys + + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public +from firewallModel.app.appBase import Base + + +class Firewalld(Base): + def __init__(self): + super().__init__() + self.cmd_str = self._set_cmd_str() + + def _set_cmd_str(self) -> str: + return "firewall-cmd" + + # 2024/3/20 下午 12:00 获取防火墙状态 + def status(self) -> bool: + ''' + @name 获取防火墙状态 + @author wzz <2024/3/20 下午 12:01> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + stdout, stderr = public.ExecShell("systemctl is-active firewalld") + if "not running" in stdout: + return False + return True + except Exception as e: + return False + + # 2024/3/20 下午 12:00 获取防火墙版本号 + def version(self) -> str: + ''' + @name 获取防火墙版本号 + @author wzz <2024/3/20 下午 12:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("firewall-cmd --version") + if "FirewallD is not running" in stdout: + return "Firewalld 没有启动,请先启动再试" + if stderr: + return "获取firewalld版本失败, err: {}".format(stderr) + return stdout.strip() + + # 2024/3/20 下午 12:08 启动防火墙 + def start(self) -> dict: + ''' + @name 启动防火墙 + @author wzz <2024/3/20 下午 12:08> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("systemctl start firewalld") + if stderr: + return self._result(False, "启动防火墙失败, err: {}".format(stderr)) + return self._result(True, "启动防火墙成功") + + # 2024/3/20 下午 12:10 停止防火墙 + def stop(self) -> dict: + ''' + @name 停止防火墙 + @author wzz <2024/3/20 下午 12:10> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("systemctl stop firewalld") + if stderr: + return self._result(False, "停止防火墙失败, err: {}".format(stderr)) + return self._result(True, "停止防火墙成功") + + # 2024/3/20 下午 12:11 重启防火墙 + def restart(self) -> dict: + ''' + @name 重启防火墙 + @author wzz <2024/3/20 下午 12:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("systemctl restart firewalld") + if stderr: + return self._result(False, "重启防火墙失败, err: {}".format(stderr)) + return self._result(True, "重启防火墙成功") + + # 2024/3/20 下午 12:11 重载防火墙 + def reload(self) -> dict: + ''' + @name 重载防火墙 + @author wzz <2024/3/20 下午 12:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("firewall-cmd --reload") + if stderr: + return self._result(False, "重载防火墙失败, err: {}".format(stderr)) + return self._result(True, "重载防火墙成功") + + # 2024/3/20 下午 12:12 获取所有防火墙端口列表 + def list_port(self) -> list: + ''' + @name 获取所有防火墙端口列表 + @author wzz <2024/3/20 下午 12:12> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["ports"] + self.list_output_port() + + # 2024/3/20 下午 12:12 获取防火墙端口INPUT列表 + def list_input_port(self) -> list: + ''' + @name 获取防火墙端口列表 + @author wzz <2024/3/20 下午 12:12> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["ports"] + + # 2024/3/22 上午 11:28 获取所有OUTPUT的direct 端口规则 + def list_output_port(self) -> list: + ''' + @name 获取所有OUTPUT的direct 端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + list_direct_rules = self.parse_direct_xml()["ports"] + datas = [] + for rule in list_direct_rules: + if rule.get("Chain") == "OUTPUT": + datas.append(rule) + return datas + + # 2024/3/20 下午 12:21 获取防火墙的rule的ip规则列表 + def list_address(self) -> list: + ''' + @name 获取防火墙的rule的ip规则列表 + @author wzz <2024/3/20 下午 2:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["rules"] + self.list_output_address() + + # 2024/3/20 下午 12:21 获取防火墙的rule input的ip规则列表 + def list_input_address(self) -> list: + ''' + @name 获取防火墙的rule input的ip规则列表 + @author wzz <2024/3/20 下午 2:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["rules"] + + # 2024/3/22 下午 4:07 获取所有OUTPUT的direct ip规则 + def list_output_address(self) -> list: + ''' + @name 获取所有OUTPUT的direct ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + list_direct_rules = self.parse_direct_xml()["rules"] + datas = [] + for rule in list_direct_rules: + if rule.get("Chain") == "OUTPUT": + datas.append(rule) + return datas + + # 2024/3/20 下午 5:34 添加或删除防火墙端口 + def input_port(self, info: dict, operation: str) -> dict: + ''' + @name 添加或删除防火墙端口 + @author wzz <2024/3/20 下午 5:34> + @param info:{"Port": args[2], "Protocol": args[3]} + operation: add/remove + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + # 2024/3/25 下午 6:00 处理tcp/udp双协议的端口 + if info['Protocol'].find("/") != -1: + stdout, stderr = public.ExecShell( + "{cmd_str} --zone=public --{operation}-port={port}/{prot} --permanent" + .format( + cmd_str=self.cmd_str, + operation=operation, + port=info['Port'], + prot="tcp" + ) + ) + if stderr: + return self._result(False, "设置端口失败, err: {}".format(stderr)) + stdout, stderr = public.ExecShell( + "{cmd_str} --zone=public --{operation}-port={port}/{prot} --permanent" + .format( + cmd_str=self.cmd_str, + operation=operation, + port=info['Port'], + prot="udp" + ) + ) + if stderr: + return self._result(False, "设置端口失败, err: {}".format(stderr)) + else: + # 2024/3/25 下午 6:00 处理单协议的端口 + stdout, stderr = public.ExecShell( + "{cmd_str} --zone=public --{operation}-port={port}/{prot} --permanent" + .format( + cmd_str=self.cmd_str, + operation=operation, + port=info['Port'], + prot=info['Protocol'] + ) + ) + if stderr: + return self._result(False, "设置端口失败, err: {}".format(stderr)) + return self._result(True, "设置入站端口成功") + + # 2024/3/20 下午 6:02 设置output的防火墙端口规则 + def output_port(self, info: dict, operation: str) -> dict: + ''' + @name 设置output的防火墙端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + if info['Strategy'] == "accept": + info['Strategy'] = "ACCEPT" + elif info['Strategy'] == "drop": + info['Strategy'] = "DROP" + elif info['Strategy'] == "reject": + info['Strategy'] = "REJECT" + else: + return self._result(False, "不支持的策略: {}".format(info['Strategy'])) + + info['Port'] = info['Port'].replace("-", ":") + + if "/" in info['Protocol']: + info['Protocol'] = info['Protocol'].split("/") + for pp in info['Protocol']: + if not pp in ["tcp", "udp"]: + return self._result(False, "设置出站端口失败, err: 协议不支持 {}".format(pp)) + + stdout, stderr = public.ExecShell( + "{cmd_str} --permanent --direct --{operation}-rule ipv4 filter OUTPUT {priority} -p {prot} --dport {port} -j {strategy}" + .format( + cmd_str=self.cmd_str, + operation=operation, + priority=info['Priority'], + prot=pp, + port=info['Port'], + strategy=info['Strategy'] + ) + ) + if stderr: + return self._result(False, "设置出站端口失败, err: {}".format(stderr)) + else: + stdout, stderr = public.ExecShell( + "{cmd_str} --permanent --direct --{operation}-rule ipv4 filter OUTPUT {priority} -p {prot} --dport {port} -j {strategy}" + .format( + cmd_str=self.cmd_str, + operation=operation, + priority=info['Priority'], + prot=info['Protocol'], + port=info['Port'], + strategy=info['Strategy'] + ) + ) + + if stderr: + return self._result(False, "设置出站端口失败, err: {}".format(stderr)) + + return self._result(True, "设置出站端口成功") + + def set_rich_rule(self, info: dict, operation: str) -> dict: + ''' + @name 添加或删除复杂规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + rule_str = "rule family={}".format(info['Family'].lower()) + if "Address" in info and info["Address"] != "all": + rule_str += " source address={}".format(info['Address']) + if info.get("Port"): + rule_str += " port port={}".format(info['Port']) + if info.get("Protocol"): + rule_str += " protocol={}".format(info['Protocol']) + rule_str += " {}".format(info['Strategy']) + + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-rich-rule='{}' --permanent" + .format(self.cmd_str, operation, rule_str)) + + if stderr: + return self._result(False, "设置规则:{} 失败, err: {}".format(operation, rule_str, stderr)) + return self._result(True, "设置规则成功".format(operation)) + + # 2024/3/22 上午 11:35 添加或删除复杂规则 + def rich_rules(self, info: dict, operation: str) -> dict: + ''' + @name 添加或删除复杂规则 + @author wzz <2024/3/22 上午 11:35> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的规则操作: {}".format(operation)) + + if "Protocol" in info and info["Protocol"] == "all": + info["Protocol"] = "tcp/udp" + + if "Protocol" in info and info['Protocol'].find("/") != -1: + result_list = [] + for protocol in info['Protocol'].split("/"): + info['Protocol'] = protocol + result_list.append(self.set_rich_rule(info, operation)) + + return {"status": True, "msg": result_list} + else: + return self.set_rich_rule(info, operation) + + # 2024/3/24 下午 10:43 设置output的防火墙ip规则 + def output_rich_rules(self, info: dict, operation: str) -> dict: + ''' + @name 设置output的防火墙ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + if info['Strategy'] == "accept": + info['Strategy'] = "ACCEPT" + elif info['Strategy'] == "drop": + info['Strategy'] = "DROP" + elif info['Strategy'] == "reject": + info['Strategy'] = "REJECT" + else: + return self._result(False, "不支持的策略: {}".format(info['Strategy'])) + + rich_rules = self.cmd_str + " --permanent --direct --{0}-rule ipv4 filter OUTPUT".format(operation) + if "Priority" in info: + rich_rules += " {}".format(info["Priority"]) + if "Address" in info: + rich_rules += " -d {}".format(info["Address"]) + if "Protocol" in info: + rich_rules += " -p {}".format(info["Protocol"]) + if "Port" in info: + info["Port"] = info["Port"].replace("-", ":") + rich_rules += " --dport {}".format(info["Port"]) + if "Strategy" in info: + rich_rules += " -j {}".format(info["Strategy"]) + + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + return self._result(False, "设置出站地址失败, err: {}".format(stderr)) + if "NOT_ENABLED" in stderr: + return self._result(False, "规则不存在") + return self._result(True, "设置出站地址成功") + + # 2024/3/22 下午 12:22 解析public区域的防火墙规则 + def parse_public_zone(self) -> dict: + ''' + @name 解析public区域的防火墙规则 + @author wzz <2024/3/22 下午 12:22> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"services": services, "ports": ports, "rules": rules} rules是ip规则 + ''' + try: + import xml.etree.ElementTree as ET + file_path = "/etc/firewalld/zones/public.xml" + if not os.path.exists(file_path): + return {"services": [], "ports": [], "rules": [], "forward_ports": []} + + services = [] + ports = [] + rules = [] + forward_ports = [] + + tree = ET.parse(file_path) + root = tree.getroot() + + for elem in root: + # 2024/3/22 下午 3:01 服务规则 + if elem.tag == "service": + services.append(elem.attrib['name']) + # 2024/3/22 下午 3:01 端口规则 + elif elem.tag == "port": + port = { + "Protocol": elem.attrib["protocol"], + "Port": elem.attrib["port"], + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "INPUT", + } + ports.append(port) + # 2024/3/22 下午 3:01 复杂的规则配置 + elif elem.tag == "rule": + rule = {"Family": elem.attrib["family"] if "family" in elem.attrib else "ipv4"} + for subelem in elem: + rule["Strategy"] = "accept" + if subelem.tag == "source": + if "address" in subelem.attrib: + rule["Address"] = subelem.attrib["address"] + else: + continue + rule["Address"] = "all" if rule["Address"] == "Anywhere" else rule["Address"] + elif subelem.tag == "port": + rule["port"] = {"protocol": subelem.attrib["protocol"], "port": subelem.attrib["port"]} + elif subelem.tag == "drop": + rule["Strategy"] = "drop" + elif subelem.tag == "accept": + rule["Strategy"] = "accept" + elif subelem.tag == "forward-port": + rule["forward-port"] = { + "protocol": subelem.attrib["protocol"], + "S_Port": subelem.attrib["port"], + "T_Address": subelem.attrib["to-addr"], + "T_Port": subelem.attrib["to-port"], + } + + # 2024/3/22 下午 3:02 如果端口在里面,就放到端口规则列表中,否则就是ip规则 + if "port" in rule: + ports.append({ + "Protocol": rule["port"]["protocol"] if "protocol" in rule else "tcp", + "Port": rule["port"]["port"], + "Strategy": rule["Strategy"] if "Strategy" in rule else "accept", + "Family": rule["Family"] if "Family" in rule else "ipv4", + "Address": rule["Address"] if "Address" in rule else "all", + "Chain": "INPUT", + }) + # 2024/3/25 下午 5:01 处理带源ip的端口转发规则 + elif "forward-port" in rule: + forward_ports.append({ + "type": "port_forward", + "number": len(forward_ports) + 1, + "Protocol": rule["forward-port"]["protocol"], + "S_Address": rule["Address"], + "S_Port": rule["forward-port"]["S_Port"], + "T_Address": rule["forward-port"]["T_Address"], + "T_Port": rule["forward-port"]["T_Port"], + }) + else: + if "Address" not in rule: + continue + + rule["Chain"] = "INPUT" + rules.append(rule) + # 2024/3/25 下午 2:57 端口转发规则 + elif elem.tag == "forward-port": + port = { + "type": "port_forward", + "number": len(forward_ports) + 1, + "Protocol": elem.attrib["protocol"] if "protocol" in elem.attrib else "tcp", + "S_Address": "", + "S_Port": elem.attrib["port"] if "port" in elem.attrib else "", + "T_Address": elem.attrib["to-addr"] if "to-addr" in elem.attrib else "", + "T_Port": elem.attrib["to-port"] if "to-port" in elem.attrib else "", + } + forward_ports.append(port) + + return {"services": services, "ports": ports, "rules": rules, "forward_ports": forward_ports} + except Exception as e: + return {"services": [], "ports": [], "rules": [], "forward_ports": []} + + # 2024/3/22 下午 2:32 解析direct.xml的防火墙规则 + def parse_direct_xml(self) -> dict: + ''' + @name 解析direct.xml的防火墙规则 + @author wzz <2024/3/22 下午 2:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + try: + import xml.etree.ElementTree as ET + file_path = "/etc/firewalld/direct.xml" + if not os.path.exists(file_path): + return {"ports": [], "rules": []} + + ports = [] + rules = [] + + tree = ET.parse(file_path) + root = tree.getroot() + + for elem in root: + if elem.tag == "rule": + protocol = "tcp" + port = "" + strategy = "" + address = "" + + elem_t = elem.text.split(" ") + # 2024/3/22 下午 4:14 解析 Options 得到端口,策略,地址,协议 + for i in elem_t: + if i == "-p": + protocol = elem_t[elem_t.index(i) + 1] # 如果找到匹配项,结果为索引+1的值,-p tcp,值为tcp + elif i == "--dport": + port = elem_t[elem_t.index(i) + 1] + elif i == "-j": + strategy = elem_t[elem_t.index(i) + 1] + elif i == "-d": + address = elem_t[elem_t.index(i) + 1] + + rule = { + "Family": elem.attrib["ipv"], + "Chain": elem.attrib["chain"], + "Strategy": strategy.lower(), + "Address": address if address != "" else "all", + # "Options": elem.text + } + + # 2024/3/22 下午 4:13 如果端口不为空,就是端口规则 + if port != "": + rule["Port"] = port + rule["Protocol"] = protocol + + ports.append(rule) + # 2024/3/22 下午 4:14 如果端口为空,就是ip规则 + else: + rules.append(rule) + + return {"ports": ports, "rules": rules} + except Exception as e: + return {"ports": [], "rules": []} + + # 2024/3/22 下午 4:54 检查是否开启了masquerade,没有则开启 + def check_masquerade(self) -> dict: + ''' + @name 检查是否开启了masquerade,没有则开启 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("firewall-cmd --query-masquerade") + if "no" in stdout: + stdout, stderr = public.ExecShell("firewall-cmd --add-masquerade") + if stderr: + return self._result(False, "开启masquerade失败, err: {}".format(stderr)) + return self._result(True, "开启masquerade成功") + return self._result(True, "masquerade已经开启") + + # 2024/3/22 下午 4:57 设置端口转发 + def port_forward(self, info: dict, operation: str) -> dict: + ''' + @name 设置端口转发 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + if operation == "add": + check_masquerade = self.check_masquerade() + if not check_masquerade["status"]: + return check_masquerade + + # 2024/3/25 下午 6:07 处理有源地址的情况 + if "S_Address" in info and info["S_Address"] != "": + # 2024/3/25 下午 6:05 处理tcp/udp双协议的情况 + if info['Protocol'].find("/") != -1: + rich_rules = self.cmd_str + " --zone=public" + rich_rules += " --{0}-rich-rule='rule family=\"{1}\" source address=\"{2}\" forward-port port=\"{3}\" protocol=\"tcp\" to-port=\"{4}\" to-addr=\"{5}\"' --permanent".format( + operation, + info['Family'], + info['S_Address'], + info['S_Port'], + info['T_Port'], + info['T_Address'], + ) + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + rich_rules = self.cmd_str + " --zone=public" + rich_rules += " --{0}-rich-rule='rule family=\"{1}\" source address=\"{2}\" forward-port port=\"{3}\" protocol=\"udp\" to-port=\"{4}\" to-addr=\"{5}\"' --permanent".format( + operation, + info['Family'], + info['S_Address'], + info['S_Port'], + info['T_Port'], + info['T_Address'], + ) + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + # 2024/3/25 下午 6:05 处理单协议的情况 + else: + rich_rules = self.cmd_str + " --zone=public" + rich_rules += " --{0}-rich-rule='rule family=\"{1}\" source address=\"{2}\" forward-port port=\"{3}\" protocol=\"{4}\" to-port=\"{5}\" to-addr=\"{6}\"'".format( + operation, + info['Family'], + info['S_Address'], + info['S_Port'], + info['Protocol'], + info['T_Port'], + info['T_Address'], + ) + rich_rules += " --permanent" + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + # 2024/3/25 下午 6:08 处理没有源地址的情况 + else: + # 2024/3/25 下午 6:05 处理tcp/udp双协议的情况 + if info['Protocol'].find("/") != -1: + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-forward-port='port={}:proto={}:toport={}:toaddr={}' --permanent" + .format(self.cmd_str, operation, info['S_Port'], "udp", info['T_Port'], info['T_Address']) + ) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-forward-port='port={}:proto={}:toport={}:toaddr={}' --permanent" + .format(self.cmd_str, operation, info['S_Port'], "tcp", info['T_Port'], info['T_Address']) + ) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + # 2024/3/25 下午 6:09 处理单协议的情况 + else: + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-forward-port='port={}:proto={}:toport={}:toaddr={}' --permanent" + .format(self.cmd_str, operation, info['S_Port'], info['Protocol'], info['T_Port'], info['T_Address']) + ) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + return self._result(True, "设置端口转发成功") + + # 2024/3/25 下午 2:37 获取所有端口转发规则 + def list_port_forward(self) -> list: + ''' + @name 获取所有端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + return self.parse_public_zone()["forward_ports"] + + +if __name__ == '__main__': + args = sys.argv + firewall = Firewalld() + Firewalld_status = firewall.status() + if len(args) < 2: + print("Welcome to the Firewalld command-line interface!") + print("Firewall status is :", Firewalld_status) + print("Firewall version: ", firewall.version()) + if Firewalld_status == "not running": + print("Firewalld未启动,请启动Firewalld后再执行命令!") + print("启动命令: start") + print() + sys.exit(1) + print("Usage: ") + print("status: Get the status of the firewall.") + print("version: Get the version of the firewall.") + print("start: Start the firewall.") + print("stop: Stop the firewall.") + print("restart: Restart the firewall.") + print("reload: Reload the firewall.") + print("list_input_port: List all input ports.") + print("list_input_address: List all input address rules.") + print("input_port: Add or remove input port.") + print("output_port: Add or remove output port.") + print("list_output_port: List all output ports.") + print("list_output_address: List all output address rules.") + print("rich_rules: Add or remove rich rules.") + sys.exit(1) + + if args[1] == "status": + print("Firewall status is :", Firewalld_status) + elif args[1] == "version": + print("Firewall version: ", firewall.version()) + elif args[1] == "start": + print(firewall.start()) + elif args[1] == "stop": + print(firewall.stop()) + elif args[1] == "restart": + print(firewall.restart()) + elif args[1] == "reload": + print(firewall.reload()) + elif args[1] == "list_input_port": + print(firewall.list_input_port()) + elif args[1] == "list_input_address": + print(firewall.list_input_address()) + elif args[1] == "input_port": + if len(args) < 4: + print("Usage: input_port Port Protocol") + sys.exit(1) + print(firewall.input_port({"Port": args[2], "Protocol": args[3]}, args[4])) + elif args[1] == "output_port": + if len(args) < 6: + print("Usage: output_port Port Protocol Strategy Priority") + sys.exit(1) + print(firewall.output_port({"Port": args[2], "Protocol": args[3], "Strategy": args[4], "Priority": args[5]}, args[6])) + elif args[1] == "list_output_port": + print(firewall.list_output_port()) + elif args[1] == "list_output_address": + print(firewall.list_output_address()) + elif args[1] == "rich_rules": + if len(args) < 4: + print("Usage: rich_rules Family Address Port Protocol Strategy") + sys.exit(1) + print(firewall.rich_rules({"Family": args[2], "Address": args[3], "Port": args[4], "Protocol": args[5], "Strategy": args[6]}, args[7])) + elif args[1] == "output_rich_rules": + if len(args) < 6: + print("Usage: output_rich_rules Family Address Port Protocol Strategy Priority") + sys.exit(1) + print(firewall.output_rich_rules({"Family": args[2], "Address": args[3], "Port": args[4], "Protocol": args[5], "Strategy": args[6], "Priority": args[7]}, args[8])) + elif args[1] == "port_forward": + if len(args) < 7: + print("Usage: port_forward Port Protocol ToPort ToAddr") + sys.exit(1) + print(firewall.port_forward({"Port": args[2], "Protocol": args[3], "ToPort": args[4], "ToAddr": args[5]}, args[6])) + else: + print("Command not found!") + sys.exit(1) + diff --git a/class/firewallModel/app/iptables.py b/class/firewallModel/app/iptables.py new file mode 100644 index 00000000..3a49fd8d --- /dev/null +++ b/class/firewallModel/app/iptables.py @@ -0,0 +1,939 @@ +#!/www/server/panel/pyenv/bin/python3.7 +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - iptables封装库 +# ------------------------------ + +import re +import subprocess +import os +import sys + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public +from firewallModel.app.appBase import Base + + +# import re + +class Iptables(Base): + def __init__(self): + self.cmd_str = self._set_cmd_str() + self.protocol = { + "6": "tcp", + "17": "udp", + "0": "all" + } + + def _set_cmd_str(self): + return "iptables" + + # 2024/3/19 下午 5:00 获取系统防火墙的运行状态 + def status(self): + ''' + @name 获取系统防火墙的运行状态 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return "running" + + # 2024/3/19 下午 5:00 获取系统防火墙的版本号 + def version(self): + ''' + @name 获取系统防火墙的版本号 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = public.ExecShell("iptables -v 2>&1|awk '{print $2}'|head -1")[0].replace("\n", "") + if result == "": + return "未知的iptables版本" + return result + except Exception as e: + return "未知版本" + + # 2024/3/19 下午 5:00 启动防火墙 + def start(self): + ''' + @name 启动防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持设置状态") + + # 2024/3/19 下午 5:00 停止防火墙 + def stop(self): + ''' + @name 停止防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持停止") + + # 2024/3/19 下午 4:59 重启防火墙 + def restart(self): + ''' + @name 重启防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持重启") + + # 2024/3/19 下午 4:59 重载防火墙 + def reload(self): + ''' + @name 重载防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持重载") + + # 2024/3/19 下午 3:36 检查表名是否合法 + def check_table_name(self, table_name): + ''' + @name 检查表名是否合法 + @param "table_name": "filter/nat/mangle/raw/security" + @return dict{"status":True/False,"msg":"提示信息"} + ''' + table_names = ['filter', 'nat', 'mangle', 'raw', 'security'] + if table_name not in table_names: + return False + return True + + # 2024/3/19 下午 3:55 解析规则列表输出,返回规则列表字典 + def parse_rules(self, stdout): + ''' + @name 解析规则列表输出,返回规则列表字典 + @author wzz <2024/3/19 下午 3:53> + 字段含义: + "number": 规则编号,对应规则在链中的顺序。 + "chain": 规则所属的链的名称。 + "pkts": 规则匹配的数据包数量。 + "bytes": 规则匹配的数据包字节数。 + "target": 规则的目标动作,表示数据包匹配到该规则后应该执行的操作。 + "prot": 规则适用的协议类型。 + "opt": 规则的选项,包括规则中使用的匹配条件或特定选项。 + "in": 规则匹配的数据包的输入接口。 + "out": 规则匹配的数据包的输出接口。 + "source": 规则匹配的数据包的源地址。 + "destination": 规则匹配的数据包的目标地址。 + "options": 规则的其他选项或说明,通常是规则中的注释或附加信息。 + + protocol(port协议头中数字对应的协议类型): + 0: 表示所有协议 + 1: ICMP(Internet 控制消息协议) + 6: TCP(传输控制协议) + 17: UDP(用户数据报协议) + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + lines = stdout.strip().split('\n') + rules = [] + current_chain = None + for line in lines: + if line.startswith("Chain"): + current_chain = line.split()[1] + elif (line.startswith("target") or line.strip() == "" or "source" in line or + "Warning: iptables-legacy tables present" in line): + # 过滤表头,空行,警告 + continue + else: + rule_info = line.split() + rule = { + "number": rule_info[0], + "chain": current_chain, + "pkts": rule_info[1], + "bytes": rule_info[2], + "target": rule_info[3], + "prot": rule_info[4], + "opt": rule_info[5], + "in": rule_info[6], + "out": rule_info[7], + "source": rule_info[8], + "destination": rule_info[9], + "options": " ".join(rule_info[10:]).strip() + } + rules.append(rule) + return rules + + # 2024/3/19 下午 3:02 列出指定表的指定链的规则 + def list_rules(self, parm): + ''' + @name 列出指定表的指定链的规则 + @author wzz <2024/3/19 下午 3:02> + @param + @return + ''' + try: + if not self.check_table_name(parm['table']): + return "错误: 不支持的表名." + stdout = subprocess.check_output( + [self.cmd_str, '-t', parm['table'], '-L', parm['chain_name'], '-nv', '--line-numbers'], + stderr=subprocess.STDOUT, universal_newlines=True + ) + return self.parse_rules(stdout) + except Exception as e: + return [] + + # 2024/4/29 下午12:16 列出iptables中所有INPUT和OUTPUT的端口规则 + def list_port(self): + ''' + @name 列出iptables中所有INPUT和OUTPUT的端口规则 + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "INPUT" + }] + ''' + try: + list_port = self.list_input_port() + self.list_output_port() + for i in list_port: + i["Strategy"] = i["Strategy"].lower() + return list_port + except Exception as e: + return [] + + # 2024/4/29 下午2:39 列出防火墙中所有的INPUT端口规则 + def list_input_port(self): + ''' + @name 列出防火墙中所有的INPUT端口规则 + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "INPUT" + }] + ''' + try: + list_port = self.get_chain_port("INPUT") + for i in list_port: + i["Strategy"] = i["Strategy"].lower() + return list_port + except Exception as e: + return [] + + # 2024/4/29 下午2:39 列出防火墙中所有的OUTPUT端口规则 + def list_output_port(self): + ''' + @name 列出防火墙中所有的OUTPUT端口规则 + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "OUTPUT" + }] + ''' + try: + list_port = self.get_chain_port("OUTPUT") + for i in list_port: + i["Strategy"] = i["Strategy"].lower() + return list_port + except Exception as e: + return [] + + # 2024/4/29 下午3:28 根据链来获取端口规则,暂时只支持INPUT/OUTPUT链 + def get_chain_port(self, chain): + ''' + @name 根据链来获取端口规则 + @author wzz <2024/4/29 下午3:29> + @param chain = INPUT/OUTPUT + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "OUTPUT" + }] + ''' + if chain not in ["INPUT", "OUTPUT"]: + return [] + + try: + stdout = self.get_chain_data(chain) + if stdout == "": + return [] + + lines = stdout.strip().split('\n') + rules = [] + for line in lines: + if line.startswith("Chain"): + continue + if not "dpt:" in line and not "multiport sports" in line: + continue + rule_info = line.split() + if rule_info[0] == "num": + continue + if not rule_info[3] in ["ACCEPT", "DROP", "REJECT"]: + continue + if not rule_info[4] in self.protocol: + continue + if not "dpt" in rule_info[-1] and not "-" in rule_info[-1] and not ":" in rule_info[-1]: + continue + + if ":" in rule_info[-1] and not "dpt" in rule_info[-1]: + Port = rule_info[-1] + elif "-" in rule_info[-1]: + Port = rule_info[-5].split(":")[1] + else: + Port = rule_info[-1].split(":")[1] + + if "source IP range" in line and "multiport sports" in line: + Address = rule_info[-4] + elif not "0.0.0.0/0" in rule_info[8]: + Address = rule_info[8] + elif "-" in rule_info[-1]: + Address = rule_info[-1] + else: + Address = "all" + + rule = { + "Protocol": self.protocol[rule_info[4]], + "Port": Port, + "Strategy": rule_info[3], + "Family": "ipv4", + "Address": Address, + "Chain": chain, + } + rules.append(rule) + + return rules + except Exception as e: + return [] + + # 2024/4/29 下午3:28 根据链来获取IP规则,暂时只支持INPUT/OUTPUT链 + def get_chain_ip(self, chain): + ''' + @name 根据链来获取端口规则 + @author wzz <2024/4/29 下午3:29> + @param chain = INPUT/OUTPUT + @return [ + { + "Family": "ipv4", + "Address": "192.168.1.190", + "Strategy": "accept", + "Chain": "INPUT" + } + ] + ''' + if chain not in ["INPUT", "OUTPUT"]: + return [] + + try: + stdout = self.get_chain_data(chain) + if stdout == "": + return [] + + lines = stdout.strip().split('\n') + rules = [] + for line in lines: + if line.startswith("Chain"): + continue + if "dpt:" in line or "multiport sports" in line: + continue + rule_info = line.split() + if rule_info[0] == "num": + continue + if not rule_info[3] in ["ACCEPT", "DROP", "REJECT"]: + continue + if not rule_info[4] in self.protocol: + continue + + Address = "" + if not "0.0.0.0/0" in rule_info[8]: + Address = rule_info[8] + elif "0.0.0.0/0" in rule_info[8] and "-" in rule_info[-1]: + Address = rule_info[-1] + + if Address == "": + continue + + rule = { + "Family": "ipv4", + "Address": Address, + "Strategy": rule_info[3], + "Chain": chain, + } + rules.append(rule) + + return rules + except Exception as e: + return [] + + # 2024/4/29 下午4:01 获取指定链的数据 + def get_chain_data(self, chain): + ''' + @name 获取指定链的数据 + @author wzz <2024/4/29 下午4:01> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + cmd = "{} -t filter -L {} -nv --line-numbers".format(self.cmd_str, chain) + stdout, stderr = public.ExecShell(cmd) + return stdout + except Exception as e: + return "" + + # 2024/4/29 下午2:46 列出防火墙中所有的INPUT和OUTPUT的ip规则 + def list_address(self): + ''' + @name 列出防火墙中所有的ip规则 + @author wzz <2024/4/29 下午2:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return + ''' + try: + list_address = self.get_chain_ip("INPUT") + self.get_chain_ip("OUTPUT") + for i in list_address: + i["Strategy"] = i["Strategy"].lower() + return list_address + except Exception as e: + return [] + + # 2024/4/29 下午2:48 列出防火墙中所有input的ip规则 + def list_input_address(self): + ''' + @name 列出防火墙中所有input的ip规则 + @author wzz <2024/4/29 下午2:48> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + list_address = self.get_chain_ip("INPUT") + for i in list_address: + i["Strategy"] = i["Strategy"].lower() + return list_address + except Exception as e: + return [] + + # 2024/4/29 下午2:49 列出防火墙中所有output的ip规则 + def list_output_address(self): + ''' + @name 列出防火墙中所有output的ip规则 + @author wzz <2024/4/29 下午2:49> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + list_address = self.get_chain_ip("OUTPUT") + for i in list_address: + i["Strategy"] = i["Strategy"].lower() + return list_address + except Exception as e: + return [] + + # 2024/4/29 下午2:49 添加INPUT端口规则 + def input_port(self, info, operation): + ''' + @name 添加INPUT端口规则 + @author wzz <2024/4/29 下午2:50> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return self.set_chain_port(info, operation, "INPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午2:50 设置output端口策略 + def output_port(self, info, operation): + ''' + @name 设置output端口策略 + @author wzz <2024/4/29 下午2:50> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return self.set_chain_port(info, operation, "OUTPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午4:49 添加/删除指定链的端口规则 + def set_chain_port(self, info, operation, chain): + ''' + @name 添加/删除指定链的端口规则 + @author wzz <2024/4/29 下午4:49> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not chain in ["INPUT", "OUTPUT"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的链类型")) + + if info['Protocol'] not in ["tcp", "udp"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的协议类型")) + if info["Strategy"] == "accept": + info["Strategy"] = "ACCEPT" + elif info["Strategy"] == "drop": + info["Strategy"] = "DROP" + elif info["Strategy"] == "reject": + info["Strategy"] = "REJECT" + else: + return self._result(False, "设置端口规则失败:{}".format("不支持的策略类型")) + + if operation == "add": + operation = "-I" + elif operation == "remove": + operation = "-D" + + rule = "{} -t filter {} {} -p {} --dport {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Strategy'] + ) + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "设置端口规则失败:{}".format(stderr)) + return self._result(True, "设置端口规则成功") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午5:01 添加/删除指定链的复杂端口规则 + def set_chain_rich_port(self, info, operation, chain): + ''' + @name 添加/删除指定链的复杂端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not chain in ["INPUT", "OUTPUT"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的链类型")) + + if "Address" in info and info["Address"] == "": + info["Address"] = "all" + if "Address" in info and public.is_ipv6(info['Address']): + return self._result(False, "设置端口规则失败:{}".format("不支持的IPV6地址")) + + if info['Protocol'] not in ["tcp", "udp"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的协议类型")) + if info["Strategy"] == "accept": + info["Strategy"] = "ACCEPT" + elif info["Strategy"] == "drop": + info["Strategy"] = "DROP" + elif info["Strategy"] == "reject": + info["Strategy"] = "REJECT" + else: + return self._result(False, "设置端口规则失败:{}".format("不支持的策略类型")) + + if operation == "add": + operation = "-I" + elif operation == "remove": + operation = "-D" + + info['Port'] = info['Port'].replace("-", ":") + info["Address"] = info["Address"].replace(":", "-") + if ":" in info['Port'] or "-" in info['Port']: + if ":" in info["Address"] or "-" in info["Address"]: + # iptables -t filter -I INPUT -m iprange --src-range 192.168.1.100-192.168.1.200 -p tcp -m multiport --sports 8000:9000 -j ACCEPT + rule = "{} -t filter {} {} -m iprange --src-range {} -p {} -m multiport --sports {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Address'], + info['Protocol'], + info['Port'], + info['Strategy'] + ) + else: + # iptables -t filter -I INPUT -p tcp -m multiport --sports 8000:9000 -s 192.168.1.100 -j ACCEPT + rule = "{} -t filter {} {} -p {} -m multiport --sports {} -s {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Address'], + info['Strategy'] + ) + else: + if ":" in info["Address"] or "-" in info["Address"]: + # iptables -t filter -I OUTPUT -p tcp --dport 22333 -m iprange --src-range 192.168.1.100-192.168.1.200 -j ACCEPT + rule = "{} -t filter {} {} -p {} --dport {} -m iprange --src-range {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Address'], + info['Strategy'] + ) + else: + # iptables -t filter -I OUTPUT -p tcp --dport 22333 -s 192.168.1.0/24 -j ACCEPT + rule = "{} -t filter {} {} -p {} --dport {} -s {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Address'], + info['Strategy'] + ) + + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "设置端口规则失败:{}".format(stderr)) + return self._result(True, "设置端口规则成功") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午5:01 添加/删除指定链的复杂ip规则 + def set_chain_rich_ip(self, info, operation, chain): + ''' + @name 添加/删除指定链的复杂ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not chain in ["INPUT", "OUTPUT"]: + return self._result(False, "设置规则失败:{}".format("不支持的链类型")) + + if "Address" in info and info["Address"] == "": + info["Address"] = "all" + if "Address" in info and public.is_ipv6(info['Address']): + return self._result(False, "设置规则失败:{}".format("不支持的IPV6地址")) + + if info["Strategy"] == "accept": + info["Strategy"] = "ACCEPT" + elif info["Strategy"] == "drop": + info["Strategy"] = "DROP" + elif info["Strategy"] == "reject": + info["Strategy"] = "REJECT" + else: + return self._result(False, "设置规则失败:{}".format("不支持的策略类型")) + + if operation == "add": + operation = "-I" + elif operation == "remove": + operation = "-D" + + if ":" in info["Address"] or "-" in info["Address"]: + # iptables -t filter -I INPUT -m iprange --src-range 192.168.1.100-192.168.1.200 -j ACCEPT + rule = "{} -t filter {} {} -m iprange --src-range {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Address'], + info['Strategy'] + ) + else: + # iptables -t filter -I INPUT -s 192.168.1.100 -j ACCEPT + rule = "{} -t filter {} {} -s {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Address'], + info['Strategy'] + ) + + + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "设置规则失败:{}".format(stderr)) + return self._result(True, "设置规则成功") + except Exception as e: + return self._result(False, "设置规则失败:{}".format(str(e))) + + # 2024/4/29 下午2:51 INPUT复杂一些的规则管理 + def rich_rules(self, info, operation): + ''' + @name + @author wzz <2024/4/29 下午2:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if "Priority" in info and not "Port" in info: + return self.set_chain_rich_ip(info, operation, "INPUT") + else: + return self.set_chain_rich_port(info, operation, "INPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午2:52 OUTPUT复杂一些的规则管理 + def output_rich_rules(self, info, operation): + ''' + @name OUTPUT复杂一些的规则管理 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if "Priority" in info and not "Port" in info: + return self.set_chain_rich_ip(info, operation, "OUTPUT") + else: + return self.set_chain_rich_port(info, operation, "OUTPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/3/19 下午 3:03 清空指定链中的所有规则 + def flush_chain(self, chain_name): + ''' + @name 清空指定链中的所有规则 + @author wzz <2024/3/19 下午 3:03> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + subprocess.check_output( + [self.cmd_str, '-F', chain_name], stderr=subprocess.STDOUT, universal_newlines=True + ) + return chain_name + " chain flushed successfully." + except Exception as e: + return "Failed to flush " + chain_name + " chain." + + # 2024/3/19 下午 3:03 获取当前系统中可用的链的名称列表 + def get_chain_names(self, parm): + ''' + @name 获取当前系统中可用的链的名称列表 + @author wzz <2024/3/19 下午 3:03> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not self.check_table_name(parm['table']): + return "错误: 不支持的表名." + stdout = subprocess.check_output( + [self.cmd_str, '-t', parm['table'], '-L'], stderr=subprocess.STDOUT, universal_newlines=True + ) + chain_names = re.findall(r"Chain\s([A-Z]+)", stdout) + return chain_names + except Exception as e: + return [] + + # 2024/3/19 下午 3:17 构造端口转发规则,然后调用insert_rule方法插入规则 + def port_forward(self, info, operation): + ''' + @name 构造端口转发规则,然后调用insert_rule方法插入规则 + @param "info": { + "Protocol": "tcp/udp", + "S_Port": "80", + "T_Address": "0.0.0.0/0", + "T_Port": "8080" + } + @param "operation": "add" or "remove" + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + rule = " -p {}".format(info['Protocol']) + + if "S_Address" in info and info['S_Address'] != "": + rule += " -s {}".format(info['S_Address']) + + rule += " --dport {0} -j DNAT --to-destination {1}:{2}".format( + info['S_Port'], + info['T_Address'], + info['T_Port'], + ) + + parm = { + "table": "nat", + "chain_name": "PREROUTING", + "rule": rule + } + if operation not in ["add", "remove"]: + return "请输入正确的操作类型. (add/remove)" + + if operation == "add": + parm['type'] = "-I" + elif operation == "remove": + parm['type'] = "-D" + return self.rule_manage(parm) + except Exception as e: + return self._result(False, "设置端口转发规则失败:{}".format(str(e))) + + # 2024/3/19 下午 3:03 在指定链中管理规则 + def rule_manage(self, parm): + ''' + @name 在指定链中管理规则 + @author wzz <2024/3/19 下午 3:03> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not self.check_table_name(parm['table']): + return self._result(False, "不支持的表名{}".format(parm['table'])) + + rule = "{} -t {} {} {} {}".format( + self.cmd_str, parm['table'], parm['type'], parm['chain_name'], parm['rule'] + ) + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "规则设置失败:{}".format(stderr)) + + return self._result(True, "规则设置成功") + except Exception as e: + return self._result(False, "规则设置失败: {}".format(str(e))) + + # 2024/4/29 下午5:55 获取所有端口转发列表 + def list_port_forward(self): + ''' + @name 获取所有端口转发列表 + @author wzz <2024/4/29 下午5:55> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.get_nat_prerouting_rules() + + # 2024/3/19 下午 4:00 调用list_rules获取所有nat表中的PREROUTING链的规则(端口转发规则),并分析成字典返回 + def get_nat_prerouting_rules(self): + ''' + @name 调用list_rules获取所有nat表中的PREROUTING链的规则(端口转发规则),并分析成字典返回 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + port_forward_rules = self.list_rules({"table": "nat", "chain_name": "PREROUTING"}) + rules = [] + for rule in port_forward_rules: + if rule["target"] != "DNAT": continue + options = rule["options"].split(' ') + + protocol = "TCP" + if rule["prot"] == "6" or rule["prot"] == "tcp": + protocol = "TCP" + elif rule["prot"] == "17" or rule["prot"] == "udp": + protocol = "UDP" + elif rule["prot"] == "0" or rule["prot"] == "all": + protocol = "TCP/UDP" + rules.append({ + "type": "port_forward", + "number": rule["number"], + "S_Address": rule["source"], + "S_Port": options[1].split("dpt:")[1], + "T_Address": options[2].split("to:")[1].split(":")[0], + "T_Port": options[2].split("to:")[1].split(":")[1], + "Protocol": protocol.lower() + }) + return rules + except Exception as e: + return [] + + # 2024/4/29 下午2:43 格式化输出json + def format_json(self, data): + ''' + @name 格式化输出json + @param "data": json数据 + @return json字符串 + ''' + import json + from pygments import highlight, lexers, formatters + + formatted_json = json.dumps(data, indent=3) + colorful_json = highlight(formatted_json.encode('utf-8'), lexers.JsonLexer(), + formatters.TerminalFormatter()) + return colorful_json + + +if __name__ == '__main__': + args = sys.argv + firewall = Iptables() + if len(args) < 2: + print("Welcome to the iptables command-line interface!") + print() + print("Available options:") + print("list_rules: list_rules ") + print("flush_chain: flush_chain
                                    ") + print("get_chain_names: get_chain_names
                                    ") + print("port_forward: port_forward ") + print("get_nat_prerouting_rules: get_nat_prerouting_rules") + print() + sys.exit(1) + if args[1] == "list_rules": + # firewall.list_rules({"table": args[2], "chain_name": args[3]}) + print(firewall.list_rules({"table": args[2], "chain_name": args[3]})) + elif args[1] == "flush_chain": + print(firewall.flush_chain(args[2])) + elif args[1] == "get_chain_names": + table = args[2] if len(args) > 2 else "filter" + print(firewall.get_chain_names({"table": table})) + elif args[1] == "port_forward": + if len(args) < 8: + print("传参使用方法: port_forward ") + sys.exit(1) + + info = { + "S_Address": args[2], + "S_Port": args[3], + "T_Address": args[4], + "T_Port": args[5], + "Protocol": args[6] + } + print(firewall.port_forward(info, args[7])) + elif args[1] == "get_nat_prerouting_rules": + import json + from pygments import highlight, lexers, formatters + + formatted_json = json.dumps(firewall.get_nat_prerouting_rules(), indent=3) + colorful_json = highlight(formatted_json.encode('utf-8'), lexers.JsonLexer(), + formatters.TerminalFormatter()) + print(colorful_json) + # print(firewall.get_nat_prerouting_rules()) + elif args[1] == "list_port": + print(firewall.format_json(firewall.list_port())) + elif args[1] == "list_input_port": + print(firewall.format_json(firewall.list_input_port())) + elif args[1] == "list_output_port": + print(firewall.format_json(firewall.list_output_port())) + elif args[1] == "list_address": + print(firewall.format_json(firewall.list_address())) + elif args[1] == "list_input_address": + print(firewall.format_json(firewall.list_input_address())) + elif args[1] == "list_output_address": + print(firewall.format_json(firewall.list_output_address())) + elif args[1] == "input_port": + info = { + "Protocol": args[2], + "Port": args[3], + "Strategy": args[4] + } + print(firewall.input_port(info, args[5])) + elif args[1] == "output_port": + info = { + "Protocol": args[2], + "Port": args[3], + "Strategy": args[4] + } + print(firewall.output_port(info, args[5])) + elif args[1] == "rich_rules": + info = { + "Protocol": args[2], + "Port": args[3], + "Address": args[4], + "Strategy": args[5] + } + print(firewall.rich_rules(info, args[6])) + elif args[1] == "output_rich_rules": + info = { + "Protocol": args[2], + "Port": args[3], + "Address": args[4], + "Strategy": args[5] + } + print(firewall.output_rich_rules(info, args[6])) + else: + print("不支持的传参: " + args[1]) + sys.exit(1) diff --git a/class/firewallModel/app/ufw.py b/class/firewallModel/app/ufw.py new file mode 100644 index 00000000..dfc8717c --- /dev/null +++ b/class/firewallModel/app/ufw.py @@ -0,0 +1,729 @@ +#!/www/server/panel/pyenv/bin/python3.7 +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - ufw封装库 +# ------------------------------ + +import subprocess +import os +import sys +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public +# import re +from firewallModel.app.appBase import Base + +class Ufw(Base): + def __init__(self): + self.cmd_str = self._set_cmd_str() + + def _set_cmd_str(self): + return "ufw" + + # 2024/3/19 下午 5:00 获取系统防火墙的运行状态 + def status(self): + ''' + @name 获取系统防火墙的运行状态 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run([self.cmd_str, "status"], capture_output=True, text=True, check=True) + if "Status: active" in result.stdout: + return "running" + elif "状态: 激活" in result.stdout: + return "running" + else: + return "not running" + except subprocess.CalledProcessError: + return "not running" + except Exception as e: + return "not running" + + # 2024/3/19 下午 5:00 获取系统防火墙的版本号 + def version(self): + ''' + @name 获取系统防火墙的版本号 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run([self.cmd_str, "version"], capture_output=True, text=True, check=True) + info = result.stdout.replace("\n", "") + return info.replace("ufw ", "") + except Exception as e: + return "未知版本" + + # 2024/3/19 下午 5:00 启动防火墙 + def start(self): + ''' + @name 启动防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + stdout, stderr = public.ExecShell("echo y | {} enable".format(self.cmd_str)) + if stderr: + return self._result(False, "启动防火墙失败:{}".format(stderr)) + return self._result(True, "启动防火墙成功") + except Exception as e: + return self._result(False, "启动防火墙失败:{}".format(str(e))) + + # 2024/3/19 下午 5:00 停止防火墙 + def stop(self): + ''' + @name 停止防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + stdout, stderr = public.ExecShell("{} disable".format(self.cmd_str)) + if stderr: + return self._result(False, "停止防火墙失败:{}".format(stderr)) + return self._result(True, "停止防火墙成功") + except Exception as e: + return self._result(False, "停止防火墙失败:{}".format(str(e))) + + # 2024/3/19 下午 4:59 重启防火墙 + def restart(self): + ''' + @name 重启防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + self.stop() + self.start() + except Exception as e: + return self._result(False, "重启防火墙失败:{}".format(str(e))) + + # 2024/3/19 下午 4:59 重载防火墙 + def reload(self): + ''' + @name 重载防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + subprocess.run([self.cmd_str, "reload"], check=True, stdout=subprocess.PIPE) + except Exception as e: + return self._result(False, "重载防火墙失败:{}".format(str(e))) + + # 2024/3/19 上午 10:39 列出防火墙中所有端口规则 + def list_port(self): + ''' + @name 列出防火墙中所有端口规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "port") + if item_fire.get("Port") and item_fire["Port"] != "Anywhere" and "." not in item_fire["Port"]: + item_fire["Port"] = item_fire["Port"].replace(":", "-") + item_fire["Address"] = "all" if item_fire["Address"] == "Anywhere" else item_fire["Address"] + + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有input端口规则 + def list_input_port(self): + ''' + @name 列出防火墙中所有input端口规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "port") + if item_fire.get("Port") and item_fire["Port"] != "Anywhere" and "." not in item_fire["Port"]: + item_fire["Port"] = item_fire["Port"].replace(":", "-") + + if item_fire["Chain"] == "INPUT": + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有output端口规则 + def list_output_port(self): + ''' + @name 列出防火墙中所有output端口规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "port") + if item_fire.get("Port") and item_fire["Port"] != "Anywhere" and "." not in item_fire["Port"]: + item_fire["Port"] = item_fire["Port"].replace(":", "-") + + if item_fire["Chain"] == "OUTPUT": + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有的ip规则 + def list_address(self): + ''' + @name 列出防火墙中所有的ip规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "address") + if "Port" in item_fire: continue + if item_fire.get("Address"): + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有input的ip规则 + def list_input_address(self): + ''' + @name 列出防火墙中所有input的ip规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + if " IN" not in line: + continue + item_fire = self._load_info(line, "address") + if "Port" in item_fire: continue + if item_fire.get("Address"): + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有output的ip规则 + def list_output_address(self): + ''' + @name 列出防火墙中所有output的ip规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + if " OUT" not in line: + continue + item_fire = self._load_info(line, "address") + if "Port" in item_fire: continue + if item_fire.get("Address"): + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 下午 4:59 添加端口规则 + def input_port(self, info, operation): + ''' + @name 添加端口规则 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + + if info["Port"].find('-') != -1: + info["Port"] = info["Port"].replace('-', ':') + + if operation == "add": + if info['Protocol'].find("/") != -1: + rich_rule = self.cmd_str + " insert 1 {} {}".format(info['Strategy'], info['Port']) + stdout, stderr = public.ExecShell(rich_rule) + elif info['Protocol'] == "tcp/udp": + stdout, stderr = public.ExecShell(self.cmd_str + " allow " + info['Port']) + else: + stdout, stderr = public.ExecShell(self.cmd_str + " allow " + info['Port'] + "/" + info['Protocol']) + else: + if info['Protocol'].find("/") != -1: + rich_rule = "{} delete {} {}".format(self.cmd_str, info['Strategy'], info['Port']) + stdout, stderr = public.ExecShell(rich_rule) + elif info['Protocol'] == "tcp/udp": + stdout, stderr = public.ExecShell(self.cmd_str + " delete allow " + info['Port']) + else: + stdout, stderr = public.ExecShell(self.cmd_str + " delete allow " + info['Port'] + "/" + info['Protocol']) + + if stderr: + if "setlocale" in stderr: + return self._result(True, "设置端口规则成功") + return self._result(False, "设置端口规则失败:{}".format(stderr)) + + return self._result(True, "设置端口规则成功") + + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置端口规则成功") + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/3/24 下午 11:28 设置output端口策略 + def output_port(self, info, operation): + ''' + @name 设置output端口策略 + @param info: 端口号 + @param operation: 操作 + @return None + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + + if operation == "add": + if info['Protocol'].find('/') != -1: + cmd = "{} {} out {}".format(self.cmd_str, info['Strategy'], info['Port']) + else: + cmd = "{} {} out {}/{}".format(self.cmd_str, info['Strategy'], info['Port'], info['Protocol']) + else: + if info['Protocol'].find('/') != -1: + cmd = "{} delete {} out {}".format(self.cmd_str, info['Strategy'], info['Port']) + else: + cmd = "{} delete {} out {}/{}".format(self.cmd_str, info['Strategy'], info['Port'], info['Protocol']) + stdout, stderr = public.ExecShell(cmd) + if stderr: + if "setlocale" in stderr: + return self._result(True, "设置端口规则成功") + return self._result(False, "设置output端口规则失败:{}".format(stderr)) + return self._result(True, "设置output端口规则成功") + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置output端口规则成功") + return self._result(False, "设置output端口规则失败:{}".format(str(e))) + + # 2024/3/19 下午 4:58 复杂一些的规则管理 + def rich_rules(self, info, operation): + ''' + @name 复杂一些的规则管理 + @author wzz <2024/3/19 下午 4:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + else: + return self._result(False, "未知的策略参数:{}".format(info["Strategy"])) + + rule_str = "{} insert 1 {} ".format(self.cmd_str, info["Strategy"]) + if "Address" in info and public.is_ipv6(info['Address']): + rule_str = "{} {} ".format(self.cmd_str, info["Strategy"]) + if operation == "remove": + rule_str = "{} delete {} ".format(self.cmd_str, info["Strategy"]) + + if "Address" in info and info['Address'] != "all": + rule_str += "from {} ".format(info['Address']) + if len(info.get("Protocol", "")) != 0 and "/" not in info['Protocol']: + rule_str += "proto {} ".format(info['Protocol']) + if len(info.get("Port", "")) != 0: + rule_str += "to any port {} ".format(info['Port']) + stdout, stderr = public.ExecShell(rule_str) + if stderr: + if "Rule added" in stdout or "Rule deleted" in stdout or "Rule updated" in stdout or "Rule inserted" in stdout or "Skipping adding existing rule" in stdout: + return self._result(True, "设置规则成功") + if "setlocale" in stderr: + return self._result(True, "设置规则成功") + return self._result(False, "规则设置失败:{}".format(stderr)) + return self._result(True, "设置规则成功") + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置规则成功") + return self._result(False, "规则设置失败:{}".format(e)) + + # 2024/3/24 下午 11:29 设置output rich_rules + def output_rich_rules(self, info, operation): + ''' + @name 设置output rich_rules + @param info: 规则 + @param operation: 操作 + @return None + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + else: + return self._result(False, "未知的策略: {}".format(info["Strategy"])) + + rule_str = "{} insert 1 {} ".format(self.cmd_str, info["Strategy"]) + if "Address" in info and public.is_ipv6(info['Address']): + rule_str = "{} {} ".format(self.cmd_str, info["Strategy"]) + if operation == "remove": + rule_str = "{} delete {} ".format(self.cmd_str, info["Strategy"]) + + if len(info.get("Address", "")) != 0: + rule_str += "out from {} ".format(info['Address']) + if len(info.get("Protocol", "")) != 0: + rule_str += "proto {} ".format(info['Protocol']) + if len(info.get("Port", "")) != 0: + rule_str += "to any port {} ".format(info['Port']) + stdout, stderr = public.ExecShell(rule_str) + if stderr: + if "Rule added" in stdout or "Rule deleted" in stdout or "Rule updated" in stdout or "Rule inserted" in stdout or "Skipping adding existing rule" in stdout: + return self._result(True, "设置output规则成功") + if "setlocale" in stderr: + return self._result(True, "设置规则成功") + return self._result(False, "outpu规则设置失败:{}".format(stderr)) + return self._result(True, "设置output规则成功") + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置output规则成功") + return self._result(False, "outpu规则设置失败:{}".format(e)) + + # 2024/3/19 下午 5:01 解析防火墙规则信息,返回字典格式数据,用于添加或删除防火墙规则 + def _load_info(self, line, fire_type): + ''' + @name 解析防火墙规则信息,返回字典格式数据,用于添加或删除防火墙规则 + @author wzz <2024/3/19 上午 10:38> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + fields = line.split() + item_info = {} + if "LIMIT" in line or "ALLOW FWD" in line: + return item_info + if len(fields) < 4: + return item_info + if fields[0] == "Anywhere" and fire_type != "port": + item_info["Strategy"] = "drop" + + if fields[1] != "(v6)": + if fields[1] == "ALLOW": + item_info["Strategy"] = "accept" + if fields[2] == "IN": + item_info["Chain"] = "INPUT" + elif fields[2] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[3] + item_info["Family"] = "ipv4" + else: + if fields[2] == "ALLOW": + item_info["Strategy"] = "accept" + if fields[3] == "IN": + item_info["Chain"] = "INPUT" + elif fields[3] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[4] + item_info["Family"] = "ipv6" + + return item_info + + if "/" in fields[0]: + item_info["Port"] = fields[0].split("/")[0] + item_info["Protocol"] = fields[0].split("/")[1] + else: + item_info["Port"] = fields[0] + item_info["Protocol"] = "tcp/udp" + + if "v6" in fields[1]: + item_info["Family"] = "ipv6" + if fields[2] == "ALLOW": + item_info["Strategy"] = "accept" + else: + item_info["Strategy"] = "drop" + + if fields[3] == "IN": + item_info["Chain"] = "INPUT" + elif fields[3] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[4] + + else: + item_info["Family"] = "ipv4" if ":" not in fields[3] else "ipv6" + + if fields[1] == "ALLOW": + item_info["Strategy"] = "accept" + else: + item_info["Strategy"] = "drop" + + if fields[2] == "IN": + item_info["Chain"] = "INPUT" + elif fields[2] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[3] + + return item_info + + # 2024/3/25 下午 2:29 设置端口转发 + def port_forward(self, info, operation): + ''' + @name 设置端口转发 + @param port: 端口号 + @param ip: ip地址 + @param operation: 操作 + @return None + ''' + from firewallModel.app.iptables import Iptables + self.firewall = Iptables() + return self.firewall.port_forward(info, operation) + + # 2024/3/25 下午 2:34 获取所有端口转发列表 + def list_port_forward(self): + ''' + @name 获取所有端口转发列表 + @return None + ''' + from firewallModel.app.iptables import Iptables + self.firewall = Iptables() + return self.firewall.get_nat_prerouting_rules() + + +if __name__ == '__main__': + args = sys.argv + firewall = Ufw() + ufw_status = firewall.status() + if len(args) < 2: + print("Welcome to the UFW (Uncomplicated Firewall) command-line interface!") + print("Firewall status is :", ufw_status) + print("Firewall version: ", firewall.version()) + if ufw_status == "not running": + print("ufw未启动,请启动ufw后再执行命令!") + print("启动命令: start") + print() + sys.exit(1) + print() + print("Available options:") + print("1. Check Firewall Status: status") + print("2. Check Firewall Version: version") + print("3. Start Firewall: start") + print("4. Stop Firewall: stop") + print("5. Restart Firewall: restart") + print("6. Reload Firewall: reload") + print("7. List All Ports: list_port") + print("8. List All IP Addresses: list_address") + print("9. Add Port: add_port ") + print("10. Remove Port: remove_port ") + print("11. Add Port Rule: add_port_rule
                                    ") + print("12. Remove Port Rule: remove_port_rule
                                    ") + print("13. Add IP Rule: add_ip_rule
                                    ") + print("14. Remove IP Rule: remove_ip_rule
                                    ") + print() + sys.exit(1) + if args[1] == "status": + print(firewall.status()) + elif args[1] == "version": + print(firewall.version()) + elif args[1] == "start": + error = firewall.start() + if error: + print(f"Error: {error}") + else: + print("Firewall started successfully.") + elif args[1] == "stop": + error = firewall.stop() + if error: + print(f"Error: {error}") + else: + print("Firewall stopped successfully.") + elif args[1] == "restart": + error = firewall.restart() + if error: + print(f"Error: {error}") + else: + print("Firewall restarted successfully.") + elif args[1] == "reload": + error = firewall.reload() + if error: + print(f"Error: {error}") + else: + print("Firewall reloaded successfully.") + elif args[1] == "list_input_port": + ports = firewall.list_input_port() + for p in ports: + print(p) + elif args[1] == "list_output_port": + ports = firewall.list_output_port() + for p in ports: + print(p) + elif args[1] == "list_input_address": + addresses = firewall.list_input_address() + for a in addresses: + print(a) + elif args[1] == "list_output_address": + addresses = firewall.list_output_address() + for a in addresses: + print(a) + elif args[1] == "add_port": + port = args[2] + protocol = args[3] + error = firewall.input_port(f"{port}/{protocol}", "allow") + if error: + print(f"Error: {error}") + else: + print(f"Port {port}/{protocol} added successfully.") + elif args[1] == "remove_port": + port = args[2] + protocol = args[3] + error = firewall.input_port(f"{port}/{protocol}", "remove") + if error: + print(f"Error: {error}") + else: + print(f"Port {port}/{protocol} removed successfully.") + elif args[1] == "add_port_rule": + address = args[2] + port = args[3] + protocol = args[4] + strategy = args[5] + operation = args[6] + error = firewall.rich_rules( + {"Address": address, "Port": port, "Protocol": protocol, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule added successfully.") + elif args[1] == "remove_port_rule": + address = args[2] + port = args[3] + protocol = args[4] + strategy = args[5] + operation = args[6] + error = firewall.rich_rules( + {"Address": address, "Port": port, "Protocol": protocol, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule removed successfully.") + elif args[1] == "add_ip_rule": + address = args[2] + strategy = args[3] + operation = args[4] + error = firewall.rich_rules( + {"Address": address, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule added successfully.") + elif args[1] == "remove_ip_rule": + address = args[2] + strategy = args[3] + operation = args[4] + error = firewall.rich_rules( + {"Address": address, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule removed successfully.") + elif args[1] == "output_port": + port = args[2] + operation = args[3] + error = firewall.output_port(port, operation) + if error: + print(f"Error: {error}") + else: + print(f"Output port {port} {operation} successfully.") + elif args[1] == "output_rich_rules": + address = args[2] + port = args[3] + protocol = args[4] + strategy = args[5] + operation = args[6] + error = firewall.output_rich_rules( + {"Address": address, "Port": port, "Protocol": protocol, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Output rich rule added successfully.") + else: + print("Invalid args") + sys.exit(1) + diff --git a/class/firewallModel/comModel.py b/class/firewallModel/comModel.py new file mode 100644 index 00000000..89154fa2 --- /dev/null +++ b/class/firewallModel/comModel.py @@ -0,0 +1,1718 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import json +import re +import time +import os + +# ------------------------------ +# 系统防火墙模型 - 业务接口类 +# ------------------------------ +import public +from firewallModel.firewallBase import Base + + +class main(Base): + + def __init__(self): + super().__init__() + + # 2024/3/14 下午 12:01 获取防火墙状态信息 + def get_firewall_info(self, get): + """ + @name 获取防火墙统计 + """ + data = {} + data['port'] = len(self.firewall.list_port()) + data['ip'] = len(self.firewall.list_address()) + data['trans'] = len(self.firewall.list_port_forward()) + data['country'] = public.M('firewall_country').count() + + isPing = True + try: + file = '/etc/sysctl.conf' + conf = public.readFile(file) + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)\n" + tmp = re.search(rep, conf).groups(0)[0] + if tmp == '1': isPing = False + except: + isPing = True + + data['ping'] = isPing + return data + + # 2024/3/26 下午 3:40 获取防火墙状态 + def get_status(self, get): + ''' + @name 获取防火墙状态 + @author wzz <2024/3/26 下午 3:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.get_firewall_status() + + # 2024/3/26 下午 3:42 设置防火墙状态 + def set_status(self, get): + ''' + @name 设置防火墙状态 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.status = get.get('status/s', '1') + if get.status not in ['0', '1']: + return public.returnMsg(False, '参数错误') + + if get.status == '1': + return self.firewall.start() + else: + return self.firewall.stop() + + # 2024/5/13 下午3:50 检查指定端口是否已经存在,如果存在则返回False,否则返回True + def check_port_exist(self, get): + ''' + @name 检查指定端口是否已经存在,如果存在则返回False,否则返回True + @author wzz <2024/5/13 下午3:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + port_rules_list = self.port_rules_list(get) + for item in port_rules_list: + if item["Port"] == get.port and item["Address"] == get.address and item["Protocol"] == get.protocol and \ + item["Strategy"] == get.strategy and item["Chain"] == get.chain: + return False + + return True + + # 2024/5/13 下午4:13 检查指定ip规则是否已经存在,如果存在则返回False,否则返回True + def check_ip_exist(self, get): + ''' + @name 检查指定ip规则是否已经存在,如果存在则返回False,否则返回True + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + ip_rules_list = self.ip_rules_list(get) + for item in ip_rules_list: + if item["Address"] == get.address and item["Strategy"] == get.strategy and item["Chain"] == get.chain: + return False + + return True + + # 2024/5/13 下午4:17 检查指定端口转发规则是否已经存在,如果存在则返回False,否则返回True + def check_forward_exist(self, get): + ''' + @name 检查指定端口转发规则是否已经存在,如果存在则返回False,否则返回True + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + forward_rules_list = self.port_forward_list(get) + for item in forward_rules_list: + if item["S_Address"] == get.S_Address and item["S_Port"] == get.S_Port and item["T_Address"] == get.T_Address and \ + item["T_Port"] == get.T_Port: + return False + + return True + + # 2024/3/26 下午 6:09 从数据库中获取端口规则列表 + def get_port_db(self, get): + ''' + @name 从数据库中获取端口规则列表 + @author wzz <2024/3/26 下午 6:13> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + where = '1=1' + sql = public.M('firewall_new') + data = sql.where(where, ()).select() + + domain_sql = public.M('firewall_domain') + domain_data = domain_sql.where(where, ()).select() + for i in range(len(data)): + if not "ports" in data[i]: + data[i]['status'] = -1 + continue + + if "brief" in data[i]: + data[i]['brief'] = public.xssdecode(data[i]['brief']) + + if not "chain" in data[i]: + data[i]['chain'] = "INPUT" + if "chain" in data[i] and data[i]['chain'] == "": + data[i]['chain'] = "INPUT" + + for j in range(len(domain_data)): + if "domain" in domain_data[j] and data[i]['address'] in domain_data[j]['domain']: + data[i]['domain'] = domain_data[j]['domain'] + break + + return data + except Exception as e: + return [] + + # 2024/3/26 下午 11:53 从数据库中获取ip规则列表 + def get_ip_db(self, get): + ''' + @name 从数据库中获取ip规则列表 + @author wzz <2024/3/26 下午 11:53> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + where = '1=1' + sql = public.M('firewall_ip') + + ip_data = sql.where(where, ()).select() + + for i_data in ip_data: + if "brief" in i_data: + i_data['brief'] = public.xssdecode(i_data['brief']) + if not "chain" in i_data: + i_data['chain'] = "INPUT" + if "chain" in i_data and i_data['chain'] == "": + i_data['chain'] = "INPUT" + + return ip_data + except Exception as e: + return [] + + # 2024/3/26 下午 11:58 从数据库中获取端口转发规则列表 + def get_forward_db(self, get): + ''' + @name 从数据库中获取端口转发规则列表 + @author wzz <2024/3/26 下午 11:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + where = '1=1' + sql = public.M('firewall_forward') + + if hasattr(get, 'query'): + where = " S_Address like '%{search}%' or S_Port like '%{search}%' or T_Address like '%{search}%' or T_Port like '%{search}%'".format( + search=get.query + ) + res = sql.where(where, ()).select() + if type(res) != list: + return [] + return res + except Exception as e: + return [] + + # 2024/3/26 下午 10:46 构造端口规则返回数据 + def structure_port_return_data(self, list_port, rule_db, query): + ''' + @name 构造返回数据 + @author wzz <2024/3/26 下午 10:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + new_list = [] + for j in range(len(list_port)): + list_port[j]['id'] = 0 + list_port[j]['sid'] = 0 + list_port[j]['brief'] = "" + list_port[j]['domain'] = "" + if (list_port[j]['Port'].find(":") != -1 or list_port[j]['Port'].find("-") != -1 or + list_port[j]['Port'].find("/") != -1 or list_port[j]['Port'].find(".") != -1): + list_port[j]['status'] = 1 + else: + try: + if not ":" in list_port[j]['Port'] or not "-" in list_port[j]['Port']: + list_port[j]['status'] = self.CheckPort(int(list_port[j]['Port']), list_port[j]['Protocol']) + else: + list_port[j]['status'] = -1 + except: + list_port[j]['status'] = -1 + + if "Chain" in list_port[j] and list_port[j]['Chain'] == "OUTPUT": + list_port[j]['status'] = -1 + + list_port[j]['addtime'] = "0000-00-00 00:00:00" + + list_port[j]['Port'] = list_port[j]['Port'].replace(":", "-") + for i in range(len(rule_db)): + if (rule_db[i]['ports'] == list_port[j]['Port'] and + rule_db[i]['protocol'] == list_port[j]['Protocol'] and + rule_db[i]['address'].lower() == list_port[j]['Address'].lower() and + rule_db[i]['types'] == list_port[j]['Strategy'] and + rule_db[i]['chain'] == list_port[j]['Chain']): + list_port[j]['id'] = rule_db[i]['id'] + list_port[j]['sid'] = rule_db[i]['sid'] + list_port[j]['brief'] = rule_db[i]['brief'] + list_port[j]['addtime'] = rule_db[i]['addtime'] + + if "domain" in rule_db[i]: + list_port[j]['domain'] = rule_db[i]['domain'] + + break + + if query != "": + if query in list_port[j]['Port'] or query in list_port[j]['brief'] or query in list_port[j]['Address']: + new_list.append(list_port[j]) + + if len(new_list) > 0 or query != "": + return sorted(new_list, key=lambda x: x['addtime'], reverse=True) + + return sorted(list_port, key=lambda x: x['addtime'], reverse=True) + + # 2024/3/27 上午 12:01 构造ip规则返回数据 + def structure_ip_return_data(self, list_ip, rule_db, query): + ''' + @name 构造ip规则返回数据 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + new_list = [] + for j in range(len(list_ip)): + list_ip[j]['id'] = 0 + list_ip[j]['sid'] = 0 + list_ip[j]['brief'] = "" + list_ip[j]['domain'] = "" + list_ip[j]['addtime'] = "0000-00-00 00:00:00" + + for i in range(len(rule_db)): + if (rule_db[i]['address'] == list_ip[j]['Address'] and + rule_db[i]['types'] == list_ip[j]['Strategy'] and + rule_db[i]['chain'] == list_ip[j]['Chain']): + list_ip[j]['id'] = rule_db[i]['id'] + list_ip[j]['sid'] = rule_db[i]['sid'] + list_ip[j]['brief'] = rule_db[i]['brief'] + list_ip[j]['addtime'] = rule_db[i]['addtime'] + + if "domain" in rule_db[i]: + list_ip[j]['domain'] = rule_db[i]['domain'] + + break + if query != "": + if query in list_ip[j]['brief'] or query in list_ip[j]['Address']: + new_list.append(list_ip[j]) + + if len(new_list) > 0 or query != "": + return public.return_area(sorted(new_list, key=lambda x: x['addtime'], reverse=True), "Address") + + return public.return_area(sorted(list_ip, key=lambda x: x['addtime'], reverse=True), "Address") + + # 2024/3/27 上午 12:11 构造端口转发规则返回数据 + def structure_forward_return_data(self, list_forward, rule_db, query): + ''' + @name 构造端口转发规则返回数据 + @author wzz <2024/3/27 上午 12:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + new_list = [] + for j in range(len(list_forward)): + list_forward[j]['id'] = 0 + list_forward[j]['brief'] = "" + list_forward[j]['addtime'] = "0000-00-00 00:00:00" + + for i in range(len(rule_db)): + if (rule_db[i]['T_Address'] == list_forward[j]['T_Address'] and + rule_db[i]['S_Port'] == list_forward[j]['S_Port'] and + rule_db[i]['T_Port'] == list_forward[j]['T_Port']): + list_forward[j]['id'] = rule_db[i]['id'] + list_forward[j]['brief'] = rule_db[i]['brief'] + list_forward[j]['addtime'] = rule_db[i]['addtime'] + break + + if query != "": + if (query in list_forward[j]['brief'] or query in list_forward[j]['S_Address'] or query in + list_forward[j]['S_Port'] or + query in list_forward[j]['T_Address'] or query in list_forward[j]['T_Port']): + new_list.append(list_forward[j]) + + if len(new_list) > 0 or query != "": + return sorted(new_list, key=lambda x: x['addtime'], reverse=True) + + return sorted(list_forward, key=lambda x: x['addtime'], reverse=True) + + # 2024/3/25 上午 11:05 获取所有端口规则列表 + def port_rules_list(self, get): + ''' + @name 获取所有端口规则列表 + @author wzz <2024/3/25 上午 11:06> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + get.chain = get.get('chain/s', 'ALL') + get.query = get.get('query/s', '') + + rule_db = self.get_port_db(get) + + if get.chain == "INPUT": + list_port = self.firewall.list_input_port() + elif get.chain == "OUTPUT": + list_port = self.firewall.list_output_port() + else: + list_port = self.firewall.list_port() + + return self.structure_port_return_data(list_port, rule_db, query=get.query) + + # 2024/3/26 下午 3:17 导出规则 + def export_rules(self, get): + ''' + @name 导出规则 + @author wzz <2024/3/26 下午 3:17> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.rule = get.get('rule/s', 'port') + if get.rule == "port": + return self.export_port_rules(get) + elif get.rule == "ip": + return self.export_ip_rules(get) + else: + return self.export_port_forward(get) + + # 2024/3/26 下午 3:18 导入规则 + def import_rules(self, get): + ''' + @name 导入规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置导入规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再导入规则!') + + get.rule = get.get('rule/s', 'port') + if get.rule == "port": + return self.import_port_rules(get) + elif get.rule == "ip": + return self.import_ip_rules(get) + else: + return self.import_port_forward(get) + + # 2024/3/26 下午 2:38 导出所有端口规则 + def export_port_rules(self, get): + ''' + @name 导出所有端口规则 + @author wzz <2024/3/26 下午 2:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.chain = get.get('chain/s', 'all') + + if get.chain == "INPUT": + file_name = "input_port_rules_{}".format(int(time.time())) + elif get.chain == "OUTPUT": + file_name = "output_port_rules_{}".format(int(time.time())) + else: + file_name = "port_rules_{}".format(int(time.time())) + + data = self.port_rules_list(get) + if not data: + return public.returnMsg(False, '没有规则无法导出') + if not os.path.exists(self.config_path): + os.makedirs(self.config_path, exist_ok=True) + file_path = "{}/{}.json".format(self.config_path, file_name) + + public.writeFile(file_path, public.GetJson(data)) + public.WriteLog("系统防火墙", "导出端口规则") + return public.returnMsg(True, file_path) + + # 2024/3/26 下午 2:41 导入端口规则 + def import_port_rules(self, get): + ''' + @name 导入端口规则 + @author wzz <2024/3/26 下午 2:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.file = get.get('file/s', '') + + if not get.file: + return public.returnMsg(False, '文件不能为空') + + if not os.path.exists(get.file): + return public.returnMsg(False, '文件不存在') + + try: + data = public.readFile(get.file) + if "|" in data and not "{" in data: + get.rule_name = "port_rule" + return self.import_rules_old(get) + + data = json.loads(data) + # 2024/4/10 下午2:51 反转数据 + data.reverse() + except: + return public.returnMsg(False, '文件内容异常或格式错误') + + args = public.dict_obj() + for item in data: + args.operation = 'add' + args.protocol = item['Protocol'] + args.port = item['Port'] + args.strategy = item['Strategy'] + args.chain = item['Chain'] + args.address = item.get('Address', 'all') + args.brief = item.get('brief', '') + args.reload = "0" + + self.set_port_rule(args) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '导入成功') + + # 2024/5/14 上午10:27 调用旧的导入规则方法 + def import_rules_old(self, get): + ''' + @name 调用旧的导入规则方法 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + from safeModel.firewallModel import main as firewall + firewall_obj = firewall() + get.file_name = get.file.split("/")[-1] + firewall_obj.import_rules(get) + + return public.returnMsg(True, '导入成功') + except Exception as e: + return public.returnMsg(False, str(e)) + + # 2024/3/26 上午 9:30 处理多个ip以换行的方式添加/删除 + def set_nline_port_ip(self, get): + ''' + @name 处理多个ip以换行的方式添加/删除 + @author wzz <2024/3/26 上午 9:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split("\n") + failed_list = [] + for addr in address: + if not public.checkIp(addr): + return public.returnMsg(False, '目标地址格式错误') + + get.address = addr + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + # if self._isFirewalld: + # self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:30 处理多个ip以逗号的方式添加/删除 + def set_tline_port_ip(self, get): + ''' + @name 处理多个ip以逗号的方式添加 + @author wzz <2024/3/26 上午 9:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split(",") + failed_list = [] + for addr in address: + if not public.checkIp(addr): + return public.returnMsg(False, '目标地址格式错误') + + get.address = addr + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + # if self._isFirewalld: + # self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:34 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + def set_range_port_ip(self, get): + ''' + @name 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + @author wzz <2024/3/26 上午 9:35> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = self.firewall.handle_ip_range(get.address) + failed_list = [] + for addr in address: + get.address = addr + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + # if self._isFirewalld: + # self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/25 下午 6:27 设置端口规则 + def set_port_rule(self, get): + ''' + @name 设置端口规则 + @author wzz <2024/3/25 下午 6:28> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置端口规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.address = get.get('address/s', 'all') + get.port = get.get('port/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + get.reload = get.get('reload/s', "1") + get.brief = get.get('brief/s', '') + + if get.address == "Anywhere" or get.address == "": + get.address = "all" + + if get.protocol == "all": + get.protocol = "tcp/udp" + + if get.port == "": + return public.returnMsg(False, '目标端口不能为空') + + if get.address != "all" and "," in get.address: + import copy + args = copy.deepcopy(get) + address_list = get.address.split(",") + for address in address_list: + args.address = address + result = self.more_prot_rule(args) + if not result['status']: + return result + if get.address != "all" and "\n" in get.address: + import copy + args = copy.deepcopy(get) + address_list = get.address.split("\n") + for address in address_list: + args.address = address + result = self.more_prot_rule(args) + if not result['status']: + return result + else: + result = self.more_prot_rule(get) + if not result['status']: + return result + + if self._isFirewalld and get.reload == "1": + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/29 下午 4:04 处理多个ip的端口规则情况 + def more_prot_rule(self, get): + ''' + @name 处理多个ip的端口规则情况 + @author wzz <2024/3/29 下午 4:02> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.port.find(",") != -1: + import copy + args = copy.deepcopy(get) + port_list = get.port.split(",") + for port in port_list: + args.port = port + result = self.exec_port_rule(args) + if not result['status']: + return result + return public.returnMsg(True, '设置成功') + else: + return self.exec_port_rule(get) + + # 2024/3/28 下午 6:29 执行端口设置 + def exec_port_rule(self, get): + ''' + @name 执行端口设置 + @author wzz <2024/3/28 下午 6:29> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add" and not self.check_port_exist(get): + return public.returnMsg(False, '端口{}已存在,请勿重复添加'.format(get.port)) + + self.set_port_db(get) + + # 2024/3/25 下午 8:23 处理多个ip的情况,例如出现每行一个ip + if get.address != "all" and "\n" in get.address: + return self.set_nline_port_ip(get) + elif get.address != "all" and "-" in get.address: + return self.set_range_port_ip(get) + elif get.address != "all" and "," in get.address: + return self.set_tline_port_ip(get) + elif get.address != "all" and "/" in get.address: + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + elif get.address != "all" and not public.checkIp(get.address) and not public.is_ipv6(get.address): + return public.returnMsg(False, '指定IP地址格式错误') + else: + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + return result + + # 2024/5/14 上午10:40 前置检测ip是否合法 + def check_ips(self, get): + ''' + @name 前置检测ip是否合法 + @author wzz <2024/5/14 上午10:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.address != "all" and "\n" in get.address: + address = get.address.split("\n") + for addr in address: + if addr != "all" and not public.checkIp(addr) and not public.is_ipv6(addr): + return public.returnMsg(False, '指定IP地址格式错误') + elif get.address != "all" and "," in get.address: + address = get.address.split(",") + for addr in address: + if addr != "all" and not public.checkIp(addr) and not public.is_ipv6(addr): + return public.returnMsg(False, '指定IP地址格式错误') + else: + if get.address != "all" and not public.checkIp(get.address) and not public.is_ipv6(get.address): + return public.returnMsg(False, '指定IP地址格式错误') + + return public.returnMsg(True, 'ok') + + # 2024/3/27 上午 9:37 修改端口规则 + def modify_port_rule(self, get): + ''' + @name 修改端口规则 + @author wzz <2024/3/27 上午 9:38> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + if "address" in get.new_data: + get.address = get.new_data['address'] + if not self.check_ips(get)["status"]: + return public.returnMsg(False, '修改后的指定IP地址格式错误') + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.port = get.old_data['Port'] + args1.protocol = get.old_data['Protocol'] + args1.address = get.old_data['Address'] + args1.strategy = get.old_data['Strategy'] + args1.chain = get.old_data['Chain'] + args1.id = get.old_data['id'] + args1.sid = get.old_data['sid'] + args1.reload = "0" + self.set_port_rule(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + args2.port = get.new_data['port'] + args2.protocol = get.new_data['protocol'] + args2.address = get.new_data['address'] if "address" in get.new_data else "all" + args2.strategy = get.new_data['strategy'] + args2.chain = get.new_data['chain'] + args2.brief = get.new_data['brief'] if 'brief' in get.new_data else "" + args2.reload = "1" + self.set_port_rule(args2) + + return public.returnMsg(True, '修改成功') + + # 2024/3/27 下午 4:03 修改域名端口规则 + def modify_domain_port_rule(self, get): + ''' + @name 修改域名端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + address = self.get_a_ip(get.new_data['domain']) + + if address == "": + return public.returnMsg(False, '域名: 【{}】解析失败'.format(get.domain)) + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.port = get.old_data['Port'] + args1.protocol = get.old_data['Protocol'] + args1.address = get.old_data['Address'] + args1.strategy = get.old_data['Strategy'] + args1.chain = get.old_data['Chain'] + args1.id = get.old_data['id'] + args1.sid = get.old_data['sid'] + self.set_port_rule(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + args2.port = get.new_data['port'] + args2.protocol = get.new_data['protocol'] + args2.address = address + args2.strategy = get.new_data['strategy'] + args2.chain = get.new_data['chain'] + args2.brief = get.new_data['brief'] if "brief" in get.new_data else "" + args2.domain = get.new_data['domain'] + self.set_port_rule(args2) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '修改成功') + + # 2024/3/26 下午 5:10 设置域名端口规则 + def set_domain_port_rule(self, get): + ''' + @name 设置域名端口规则 + @author wzz <2024/3/26 下午 5:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.domain = get.get('domain/s', '') + get.port = get.get('port/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + + if get.domain == "": + return public.returnMsg(False, '目标域名不能为空') + + if get.port == "": + return public.returnMsg(False, '目标端口不能为空') + + if not public.is_domain(get.domain): + return public.returnMsg(False, '目标域名格式错误') + + if "|" in get.domain: + get.domain = get.domain.split("|")[0] + + address = self.get_a_ip(get.domain) + + if address == "": + return public.returnMsg(False, '域名: 【{}】解析失败'.format(get.domain)) + + get.address = address + self.set_port_db(get) + + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if result['status'] and self._isFirewalld: + self.firewall.reload() + + return result + + # 2024/5/13 下午4:46 添加端口规则到指定数据库 + def add_port_db(self, get, protocol, addtime, domain): + ''' + @name 添加端口规则到指定数据库 + @author wzz <2024/5/13 下午4:46> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + add_sid = public.M('firewall_new').add( + 'ports,brief,protocol,address,types,addtime,domain,sid,chain', + ( + get.port, public.xsssec(get.brief), protocol, get.address, get.strategy, addtime, domain, 0, + get.chain) + ) + + if get.domain != "": + domain_sid = public.M('firewall_domain').add( + 'types,domain,port,address,brief,addtime,sid,protocol,domain_total', + (get.strategy, domain, get.port, get.address, public.xsssec(get.brief), + addtime, add_sid, get.protocol, get.domain) + ) + public.M('firewall_new').where("id=?", (add_sid,)).save('sid', domain_sid) + self.check_resolve_crontab() + + # 2024/5/13 下午4:49 从指定数据库删除端口规则 + def remove_port_db(self, get, protocol, addtime, domain): + ''' + @name 从指定数据库删除端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + public.M('firewall_new').where("ports=? and protocol=? and address=? and types=? and chain=?", ( + get.port, protocol, get.address, get.strategy, get.chain + )).delete() + get.domain = get.get('domain/s', '') + if get.domain != "": + public.M('firewall_domain').where("domain=?", (get.domain)).delete() + + self.remove_resolve_crontab() + + # 2024/3/26 下午 5:52 添加/删除数据库的端口规则 + def set_port_db(self, get): + ''' + @name 添加/删除数据库的端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add": + # 检测端口是否已经添加过 + query_result = public.M('firewall_new').where( + 'ports=? and address=? and protocol=? and types=? and chain=?', + (get.port, get.address, get.protocol, get.strategy, get.chain) + ).find() + + if not query_result: + get.domain = get.get('domain/s', '') + domain = "{}|{}".format(get.domain, get.address) if get.domain != "" else "" + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + + if get.protocol == "tcp/udp" and self._isFirewalld: + self.add_port_db(get, "tcp", addtime, domain) + self.add_port_db(get, "udp", addtime, domain) + else: + self.add_port_db(get, get.protocol, addtime, domain) + else: + query_result = public.M('firewall_new').where( + 'ports=? and address=? and protocol=? and types=? and chain=?', + (get.port, get.address, get.protocol, get.strategy, get.chain) + ).find() + if query_result: + if get.protocol == "tcp/udp" and self._isFirewalld: + self.remove_port_db(get, "tcp", query_result['addtime'], query_result['domain']) + self.remove_port_db(get, "udp", query_result['addtime'], query_result['domain']) + else: + self.remove_port_db(get, get.protocol, query_result['addtime'], query_result['domain']) + + # 2024/3/26 下午 11:23 添加/删除数据库的ip规则 + def set_ip_db(self, get): + ''' + @name 添加/删除数据库的ip规则 + @author wzz <2024/3/26 下午 11:24> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add": + get.domain = get.get('domain/s', '') + domain = "{}|{}".format(get.domain, get.address) if get.domain != "" else "" + query_result = public.M('firewall_ip').where("address=? and types=? and domain=? and chain=?", + (get.address, get.strategy, domain, get.chain)).find() + + if not query_result: + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + self._add_sid = public.M('firewall_ip').add( + 'address,types,brief,addtime,domain,sid,chain', + (get.address, get.strategy, public.xsssec(get.brief), addtime, domain, 0, get.chain) + ) + + if get.domain != "": + domain_sid = public.M('firewall_domain').add( + 'types,domain,port,address,brief,addtime,sid,protocol,domain_total', + (get.strategy, domain, '', get.address, public.xsssec(get.brief), addtime, self._add_sid, '', + get.domain) + ) + public.M('firewall_ip').where("id=?", (self._add_sid,)).save('sid', domain_sid) + self.check_resolve_crontab() + else: + get.address = get.get("address/s", '') + get.strategy = get.get("strategy/s", '') + if get.address == "": + return public.returnMsg(False, '请传入id') + public.M('firewall_ip').where("address=? and types=? and chain=?", (get.address, get.strategy, get.chain)).delete() + get.domain = get.get('domain/s', '') + if get.domain != "": + public.M('firewall_domain').where("domain=?", (get.domain)).delete() + + self.remove_resolve_crontab() + + # 2024/3/26 下午 11:40 添加/删除数据库的端口转发规则 + def set_forward_db(self, get): + ''' + @name 添加/删除数据库的端口转发规则 + @author wzz <2024/3/26 下午 11:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add": + query_result = public.M('firewall_forward').where("S_Port=? and T_Address=? and T_Port=? and Protocol=?", ( + get.S_Port, get.T_Address, get.T_Port, get.protocol + )).find() + + if not query_result: + get.brief = get.get('brief/s', '') + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + self._add_sid = public.M('firewall_forward').add( + 'S_Port,T_Address,T_Port,Protocol,Family,addtime,brief', + (get.S_Port, get.T_Address, get.T_Port, get.protocol, "ipv4", addtime, get.brief) + ) + else: + public.M('firewall_forward').where( + "S_Port=? and T_Address=? and T_Port=? and Protocol=?", + (get.S_Port, get.T_Address, get.T_Port, get.protocol) + ).delete() + + # 2024/3/25 下午 6:55 入站端口规则 + def input_port(self, get): + ''' + @name 入站端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 dabao + @return list[dict{}...] + ''' + info = { + "Port": get.port, + "Protocol": get.protocol.lower(), + "Strategy": get.strategy.lower(), + "Family": "ipv4", + } + + if ":" in get.address: + info["Family"] = "ipv6" + + if get.address != "all": + info["Address"] = get.address + result = self.firewall.rich_rules(info=info, operation=get.operation) + elif get.address == "all" and get.strategy == "drop": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.input_port(info=info, operation=get.operation) + + return result + + # 2024/3/25 下午 6:56 出站端口规则 + def output_port(self, get): + ''' + @name 出站端口规则 + @author wzz <2024/3/25 下午 6:56> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + info = { + "Port": get.port, + "Protocol": get.protocol.lower(), + "Strategy": get.strategy.lower(), + "Priority": "0", + "Family": "ipv4", + } + + if ":" in get.address: + info["Family"] = "ipv6" + + if get.address != "all": + info["Address"] = get.address + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_port(info=info, operation=get.operation) + + return result + + # 2024/3/26 上午 9:40 处理多个ip的情况,例如出现每行一个ip + def set_nline_ip_rule(self, get): + ''' + @name 处理多个ip的情况,例如出现每行一个ip + @author wzz <2024/3/26 上午 9:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split("\n") + failed_list = [] + import copy + for addr in address: + if not public.is_ipv4(addr) and not public.is_ipv6(addr): + continue + + args = copy.deepcopy(get) + args.address = addr + args.family = "ipv4" if ":" not in addr else "ipv6" + + if self.check_is_user_ip(args): + continue + + if get.operation == "add" and not self.check_ip_exist(args): + continue + + self.set_ip_db(args) + + info = { + "Address": args.address, + "Family": args.family, + "Strategy": args.strategy, + "Priority": args.priority, + } + + if args.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=args.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=args.operation) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:40 处理多个ip的情况,例如出现逗号隔开ip + def set_tline_ip_rule(self, get): + ''' + @name 处理多个ip的情况,例如出现逗号隔开ip + @author wzz <2024/3/26 上午 9:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split(",") + failed_list = [] + import copy + for addr in address: + if not public.checkIp(addr): + return public.returnMsg(False, '目标地址格式错误') + + args = copy.deepcopy(get) + args.address = addr + + if self.check_is_user_ip(args): + continue + + if get.operation == "add" and not self.check_ip_exist(args): + continue + + self.set_ip_db(args) + + info = { + "Address": args.address, + "Family": args.family, + "Strategy": args.strategy, + "Priority": args.priority, + } + + if args.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=args.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=args.operation) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:43 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + def set_range_ip_rule(self, get): + ''' + @name 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + @author wzz <2024/3/26 上午 9:43> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = self.firewall.handle_ip_range(get.address) + failed_list = [] + import copy + for addr in address: + args = copy.deepcopy(get) + args.address = addr + + if self.check_is_user_ip(args): + continue + + if get.operation == "add" and not self.check_ip_exist(args): + continue + + self.set_ip_db(args) + + info = { + "Address": args.address, + "Family": args.family, + "Strategy": args.strategy, + "Priority": args.priority, + } + + if args.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=args.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=args.operation) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:46 设置带掩码的ip段 + def set_mask_ip_rule(self, get): + ''' + @name 设置带掩码的ip段 + @author wzz <2024/3/26 上午 9:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add" and not self.check_ip_exist(get): + return public.returnMsg(False, '目标地址{}已存在,请勿重复添加'.format(get.address)) + + self.set_ip_db(get) + info = { + "Address": get.address, + "Family": get.family, + "Strategy": get.strategy, + "Priority": get.priority, + } + + if get.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld: + self.firewall.reload() + + return result + + # 2024/4/9 下午11:31 检查是否会自己的ip,如果是则返回 + def check_is_user_ip(self, get): + ''' + @name + @author wzz <2024/4/9 下午11:31> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/3/26 下午 11:27 处理用户当前的远程ip,如果添加的ip与此ip一直则返回 + try: + from flask import request + user_ip = request.remote_addr + if user_ip in get.address: + return True + return False + except: + return False + + # 2024/3/25 下午 7:16 设置ip规则 + def set_ip_rule(self, get): + ''' + @name 设置ip规则 + @author wzz <2024/3/25 下午 7:16> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.address = get.get('address/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + get.family = get.get('family/s', 'ipv4') + get.priority = get.get('priority/s', '0') + get.reload = get.get('reload/s', "1") + + if get.address == "": + return public.returnMsg(False, '目标ip不能为空') + + # 2024/3/25 下午 8:23 处理多个ip的情况,例如出现每行一个ip + if get.address != "all" and "\n" in get.address: + return self.set_nline_ip_rule(get) + elif get.address != "all" and "-" in get.address: + return self.set_range_ip_rule(get) + elif get.address != "all" and "/" in get.address: + return self.set_mask_ip_rule(get) + elif get.address != "all" and "," in get.address: + return self.set_tline_ip_rule(get) + elif get.address != "all" and not public.checkIp(get.address) and not public.is_ipv6(get.address): + return public.returnMsg(False, '目标地址格式错误') + else: + if get.operation == "add" and get.strategy == "drop": + if self.check_is_user_ip(get): + return public.returnMsg(False, '不能添加自己的ip') + + if get.operation == "add" and not self.check_ip_exist(get): + return public.returnMsg(False, '目标地址{}已存在,请勿重复添加'.format(get.address)) + + self.set_ip_db(get) + + info = { + "Address": get.address, + "Family": get.family, + "Strategy": get.strategy.lower(), + "Priority": get.priority.lower(), + } + + if get.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld and get.reload == "1": + self.firewall.reload() + + return result + + # 2024/3/27 上午 9:44 修改ip规则 + def modify_ip_rule(self, get): + ''' + @name 修改ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.address = get.old_data['Address'] + args1.strategy = get.old_data['Strategy'] + args1.family = get.old_data['Family'] + args1.chain = get.old_data['Chain'] + args1.id = get.old_data['id'] + args1.sid = get.old_data['sid'] + args1.reload = "0" + self.set_ip_rule(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + args2.address = get.new_data['address'] + args2.strategy = get.new_data['strategy'] + args2.family = get.new_data['family'] + args2.chain = get.new_data['chain'] + args2.brief = get.new_data['brief'] + args2.reload = "0" + self.set_ip_rule(args2) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '修改成功') + + # 2024/3/26 下午 5:18 设置域名ip规则 + def set_domain_ip_rule(self, get): + ''' + @name 设置域名ip规则 + @author wzz <2024/3/26 下午 5:19> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.domain = get.get('domain/s', '') + get.port = get.get('port/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + + if get.domain == "": + return public.returnMsg(False, '目标域名不能为空') + + if get.port == "": + return public.returnMsg(False, '目标端口不能为空') + + if not public.is_domain(get.domain): + return public.returnMsg(False, '目标域名格式错误') + + address = self.get_a_ip(get.domain) + + if address == "": + return public.returnMsg(False, '域名: 【{}】解析失败'.format(get.domain)) + + if not public.checkIp(get.address): + return public.returnMsg(False, '目标地址格式错误') + + info = { + "Address": address, + "Family": get.family, + "Strategy": get.strategy.lower(), + "Priority": get.priority.lower(), + } + if get.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld: + self.firewall.reload() + + return result + + # 2024/3/25 上午 11:18 获取所有ip规则列表 + def ip_rules_list(self, get): + ''' + @name 获取所有ip规则列表 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + get.chain = get.get('chain/s', 'all') + get.query = get.get('query/s', '') + ip_db = self.get_ip_db(get) + + if get.chain == "INPUT": + list_address = self.firewall.list_input_address() + elif get.chain == "OUTPUT": + list_address = self.firewall.list_output_address() + else: + list_address = self.firewall.list_address() + + return self.structure_ip_return_data(list_address, ip_db, query=get.query) + + # 2024/3/26 下午 3:03 导出所有ip规则 + def export_ip_rules(self, get): + ''' + @name 导出所有ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.chain = get.get('chain/s', 'all') + + if get.chain == "INPUT": + file_name = "input_ip_rules_{}".format(int(time.time())) + elif get.chain == "OUTPUT": + file_name = "output_ip_rules_{}".format(int(time.time())) + else: + file_name = "ip_rules_{}".format(int(time.time())) + + data = self.ip_rules_list(get) + if not data: + return public.returnMsg(False, '没有规则无法导出') + + file_path = "{}/{}.json".format(self.config_path, file_name) + + public.writeFile(file_path, public.GetJson(data)) + public.WriteLog("系统防火墙", "导出ip规则") + return public.returnMsg(True, file_path) + + # 2024/3/26 下午 3:05 导入ip规则 + def import_ip_rules(self, get): + ''' + @name 导入ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.file = get.get('file/s', '') + + if not get.file: + return public.returnMsg(False, '文件不能为空') + + if not os.path.exists(get.file): + return public.returnMsg(False, '文件不存在') + + try: + data = public.readFile(get.file) + if "|" in data and not "{" in data: + get.rule_name = "ip_rule" + return self.import_rules_old(get) + + data = json.loads(data) + # 2024/4/10 下午2:51 反转数据 + data.reverse() + except: + return public.returnMsg(False, '文件内容异常或格式错误') + + args = public.dict_obj() + for item in data: + args.operation = 'add' + args.address = item['Address'] + args.strategy = item['Strategy'] + args.chain = item['Chain'] + args.family = item['Family'] + args.brief = item['brief'] + args.reload = "0" + + self.set_ip_rule(args) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '导入成功') + + # 2024/3/25 下午 2:34 获取端口转发列表 + def port_forward_list(self, get): + ''' + @name 获取端口转发列表 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + get.query = get.get('query/s', '') + list_port_forward = self.firewall.list_port_forward() + forward_db = self.get_forward_db(get) + if type(list_port_forward) == list and type(forward_db) == list: + return self.structure_forward_return_data(list_port_forward, forward_db, query=get.query) + return [] + + # 2024/3/26 下午 3:08 导出所有端口转发规则 + def export_port_forward(self, get): + ''' + @name 导出所有端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + data = self.firewall.list_port_forward() + if not data: + return public.returnMsg(False, '没有规则无法导出') + file_name = "port_forward_{}".format(int(time.time())) + file_path = "{}/{}.json".format(self.config_path, file_name) + + public.writeFile(file_path, public.GetJson(data)) + public.WriteLog("系统防火墙", "导出端口转发规则") + return public.returnMsg(True, file_path) + + # 2024/3/26 下午 3:10 导入端口转发规则 + def import_port_forward(self, get): + ''' + @name 导入端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.file = get.get('file/s', '') + + if not get.file: + return public.returnMsg(False, '文件不能为空') + + if not os.path.exists(get.file): + return public.returnMsg(False, '文件不存在') + + try: + data = public.readFile(get.file) + if "|" in data and not "{" in data: + get.rule_name = "trans_rule" + return self.import_rules_old(get) + + data = json.loads(data) + # 2024/4/10 下午2:51 反转数据 + data.reverse() + except: + return public.returnMsg(False, '文件内容异常或格式错误') + + args = public.dict_obj() + for item in data: + args.operation = 'add' + args.protocol = item['Protocol'] + args.S_Address = item['S_Address'] + args.S_Port = item['S_Port'] + args.T_Address = item['T_Address'] + args.T_Port = item['T_Port'] + args.reload = "0" + + self.set_port_forward(args) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '导入成功') + + # 2024/3/25 下午 3:43 设置端口转发 + def set_port_forward(self, get): + ''' + @name 设置端口转发 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.S_Address = get.get('S_Address/s', '') + get.S_Port = get.get('S_Port/s', '') + get.T_Address = get.get("T_Address/s", '') + get.T_Port = get.get("T_Port/s", '') + get.reload = get.get('reload/s', "1") + + if get.S_Port == "": + return public.returnMsg(False, '源端口不能为空') + # if get.T_Address == "": + # return public.returnMsg(False, '目标地址不能为空') + if get.T_Port == "": + return public.returnMsg(False, '目标端口不能为空') + + # 2024/3/25 下午 5:49 前置检测 + if get.operation == "add": + check_ip_forward = self.firewall.check_ip_forward() + if not check_ip_forward["status"]: + return check_ip_forward + + if not self.check_forward_exist(get): + return public.returnMsg(False, '端口转发规则已存在,请勿重复添加') + + self.set_forward_db(get) + + # 2024/3/25 下午 5:50 构造传参,调用底层方法设置端口转发 + if get.protocol == "tcp/udp" or get.protocol == "all": + info = { + "Family": "ipv4", + "Protocol": "tcp", + "S_Address": get.S_Address, + "S_Port": get.S_Port, + "T_Address": get.T_Address, + "T_Port": get.T_Port, + } + result = self.firewall.port_forward(info=info, operation=get.operation) + if not result['status']: + return result + + info = { + "Family": "ipv4", + "Protocol": "udp", + "S_Address": get.S_Address, + "S_Port": get.S_Port, + "T_Address": get.T_Address, + "T_Port": get.T_Port, + } + result = self.firewall.port_forward(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld and get.reload == "1": + self.firewall.reload() + else: + info = { + "Family": "ipv4", + "Protocol": get.protocol, + "S_Address": get.S_Address, + "S_Port": get.S_Port, + "T_Address": get.T_Address, + "T_Port": get.T_Port, + } + result = self.firewall.port_forward(info=info, operation=get.operation) + + # 2024/3/25 下午 5:50 如果设置成功才重载防火墙 + if result['status'] and self._isFirewalld and get.reload == "1": + self.firewall.reload() + + return result + + # 2024/3/27 上午 9:44 修改端口转发规则 + def modify_forward_rule(self, get): + ''' + @name 修改端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.S_Address = get.old_data['S_Address'] + args1.S_Port = get.old_data['S_Port'] + args1.T_Address = get.old_data['T_Address'] + args1.T_Port = get.old_data['T_Port'] + args1.Protocol = get.old_data['Protocol'] + args1.id = get.old_data['id'] + args1.reload = "0" + self.set_port_forward(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + # args2.S_Address = get.new_data['S_Address'] + args2.S_Port = get.new_data['S_Port'] + args2.T_Address = get.new_data['T_Address'] + args2.T_Port = get.new_data['T_Port'] + args2.Protocol = get.new_data['protocol'] + args2.brief = get.new_data['brief'] + args2.reload = "0" + self.set_port_forward(args2) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '修改成功') diff --git a/class/firewallModel/firewallBase.py b/class/firewallModel/firewallBase.py new file mode 100644 index 00000000..97f83843 --- /dev/null +++ b/class/firewallModel/firewallBase.py @@ -0,0 +1,220 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - 基类 +# ------------------------------ + +import os +import re +from typing import Dict, Union, Any +from xml.etree.ElementTree import ElementTree + +import public + + +class Base(object): + + def __init__(self): + self.config_path = "{}/class/firewallModel/config".format(public.get_panel_path()) + self._isUfw = False + self._isFirewalld = False + self._isIptables = False + if os.path.exists('/usr/sbin/ufw'): + self._isUfw = True + from firewallModel.app.ufw import Ufw + self.firewall = Ufw() + elif os.path.exists('/usr/sbin/firewalld'): + self._isFirewalld = True + from firewallModel.app.firewalld import Firewalld + self.firewall = Firewalld() + elif not self._isUfw and not self._isFirewalld: + self._isIptables = True + from firewallModel.app.iptables import Iptables + self.firewall = Iptables() + _months = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', 'Jul': '07', + 'Aug': '08', 'Sep': '09', 'Sept': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'} + + # 2024/3/14 上午 11:27 获取防火墙运行状态 + def get_firewall_status(self) -> bool: + ''' + @name 获取防火墙运行状态 + @author wzz <2024/3/14 上午 11:27> + @param + @return bool True/False + ''' + if self._isUfw: + res = public.ExecShell("systemctl is-active ufw")[0] + if res == "active": return True + res = public.ExecShell("systemctl list-units | grep ufw")[0] + if res.find('active running') != -1: return True + res = public.ExecShell('/lib/ufw/ufw-init status')[0] + if res.find("Firewall is not running") != -1: return False + res = public.ExecShell('ufw status verbose')[0] + if res.find('inactive') != -1: return False + return True + if self._isFirewalld: + res = public.ExecShell("ps -ef|grep firewalld|grep -v grep")[0] + if res: return True + res = public.ExecShell("systemctl is-active firewalld")[0] + if res == "active": return True + res = public.ExecShell("systemctl list-units | grep firewalld")[0] + if res.find('active running') != -1: return True + return False + else: + res = public.ExecShell("/etc/init.d/iptables status")[0] + if res.find('not running') != -1: return False + res = public.ExecShell("systemctl is-active iptables")[0] + if res == "active": return True + return True + + # 2024/3/14 上午 11:30 设置禁ping + def set_ping(self, get) -> dict: + ''' + @name 设置禁ping + @author wzz <2024/3/14 上午 11:31> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.status = get.get("status", "1") + get.status = str(get.status) if str(get.status) in ['0', '1'] else '1' + + filename = '/etc/sysctl.conf' + conf = public.readFile(filename) + if conf.find('net.ipv4.icmp_echo') != -1: + rep = r"net\.ipv4\.icmp_echo.*" + conf = re.sub(rep, 'net.ipv4.icmp_echo_ignore_all=' + get.status + "\n", conf) + else: + conf += "\nnet.ipv4.icmp_echo_ignore_all=" + get.status + "\n" + + if public.writeFile(filename, conf): + public.ExecShell('sysctl -p') + return public.returnMsg(True, 'SUCCESS') + else: + return public.returnMsg( + False, + '错误:设置失败,sysctl.conf不可写!
                                    ' + '1、如果安装了[宝塔系统加固],请先关闭
                                    ' + '2、如果安装了云锁,请关闭[系统加固]功能
                                    ' + '3、如果安装了安全狗,请关闭[系统防护]功能
                                    ' + '4、如果使用了其它安全软件,请先卸载
                                    ' + ) + + # 2024/3/14 上午 11:37 获取网站日志目录的大小 + def get_www_logs_size(self, get) -> Dict[str, Union[str, Any]]: + ''' + @name 获取网站日志目录的大小 + @author wzz <2024/3/14 上午 11:37> + @param + @return dict{"status":True/False,"msg":"提示信息"} + ''' + path_size = public.get_size_total("/www/wwwlogs") + if not path_size: + return {"log_path": "/www/wwwlogs", "size": "0B"} + + return {"log_path": "/www/wwwlogs", "size": public.to_size(path_size["/www/wwwlogs"])} + + # 2024/3/25 上午 10:50 获取防火墙类型,firewall或ufw + def _get_firewall_type(self) -> str: + ''' + @name 获取防火墙类型,firewall或ufw + @return str firewall/ufw + ''' + import os + if os.path.exists('/usr/sbin/ufw'): + return 'ufw' + if os.path.exists('/usr/sbin/firewalld'): + return 'firewall' + return 'iptables' + + # 2024/3/26 下午 5:01 获取指定域名的A记录 + def get_a_ip(self, domain: str) -> str: + ''' + @name 获取指定域名的A记录 + @param domain: 域名 + @return str + ''' + try: + import socket + return socket.gethostbyname(domain) + except Exception as e: + return "" + + # 2024/3/26 下午 5:40 检查是否已添加计划任务,如果没有则添加 + def check_resolve_crontab(self): + ''' + @name 检查是否已添加计划任务 + @author wzz <2024/3/26 下午 5:41> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + python_path = "{}/pyenv/bin/python".format(public.get_panel_path()) + + if not public.M('crontab').where('name=?', ('[勿删]系统防火墙域名解析检测任务',)).count(): + cmd = '{} {}'.format(python_path, '/www/server/panel/script/firewall_domain.py') + args = {"name": "[勿删]系统防火墙域名解析检测任务", "type": 'minute-n', "where1": '5', "hour": '', + "minute": '', "sName": "", + "sType": 'toShell', "notice": '', "notice_channel": '', "save": '', "save_local": '1', + "backupTo": '', "sBody": cmd, + "urladdress": ''} + import crontab + res = crontab.crontab().AddCrontab(args) + if res and "id" in res.keys(): + return True + return False + return True + + # 2024/3/26 下午 11:37 当没有域名解析时,删除域名解析的计划任务 + def remove_resolve_crontab(self): + ''' + @name 当没有域名解析时,删除域名解析的计划任务 + @author wzz <2024/3/26 下午 11:37> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + if not public.M('firewall_domain').count(): + pdata = public.M('crontab').where('name=?', '[勿删]系统防火墙域名解析检测任务').select() + if pdata: + import crontab + for i in pdata: + args = {"id": i['id']} + crontab.crontab().DelCrontab(args) + + # 2024/3/26 下午 6:22 端口扫描 + def CheckPort(self, port, protocol): + ''' + @name 端口扫描 + @author wzz <2024/3/26 下午 6:22> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + import socket + localIP = '127.0.0.1' + temp = {} + temp['port'] = port + temp['local'] = True + + try: + if 'tcp' in protocol.lower(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + s.connect((localIP, port)) + s.close() + if 'udp' in protocol.lower(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0.01) + s.sendto(b'', (localIP, port)) + s.close() + except: + temp['local'] = False + + result = 0 + if temp['local']: result += 2 + return result diff --git a/class/firewall_new.py b/class/firewall_new.py index 426ea322..3fe29ef4 100644 --- a/class/firewall_new.py +++ b/class/firewall_new.py @@ -1,8 +1,8 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x5 +# | aaPanel x5 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2018 宝塔软件(http:#bt.cn) All rights reserved. +# | Copyright (c) 2015-2018 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: 1249648969@qq.com # +------------------------------------------------------------------- @@ -138,7 +138,7 @@ class firewalls: def AddDropAddress(self,get): import time import re - rep = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" if not re.search(rep,get.port): return public.return_msg_gettext(False,'IP address youve entered is illegal!'); address = get.port if public.M('firewall').where("port=?",(address,)).count() > 0: return public.return_msg_gettext(False,'The IP exists in block list, no need to repeat processing!') @@ -189,7 +189,7 @@ class firewalls: def AddAcceptPort(self,get): flag=False import re - rep = "^\d{1,5}(:\d{1,5})?$" + rep = r"^\d{1,5}(:\d{1,5})?$" if not re.search(rep,get.port): return public.return_msg_gettext(False,'Port range is incorrect!'); import time port = get.port @@ -297,7 +297,7 @@ class firewalls: filename = '/etc/sysctl.conf' conf = public.readFile(filename) if conf.find('net.ipv4.icmp_echo') != -1: - rep = u"net\.ipv4\.icmp_echo.*" + rep = r"net\.ipv4\.icmp_echo.*" conf = re.sub(rep,'net.ipv4.icmp_echo_ignore_all='+get.status,conf) else: conf += "\nnet.ipv4.icmp_echo_ignore_all="+get.status @@ -320,7 +320,7 @@ class firewalls: file = '/etc/ssh/sshd_config' conf = public.readFile(file) - rep = "#*Port\s+([0-9]+)\s*\n" + rep = "#*Port\\s+([0-9]+)\\s*\n" conf = re.sub(rep, "Port "+port+"\n", conf) public.writeFile(file,conf) @@ -345,7 +345,7 @@ class firewalls: def GetSshInfo(self,get): file = '/etc/ssh/sshd_config' conf = public.readFile(file) - rep = "#*Port\s+([0-9]+)\s*\n" + rep = "#*Port\\s+([0-9]+)\\s*\n" port = re.search(rep,conf).groups(0)[0] import system panelsys = system.system(); @@ -367,7 +367,7 @@ class firewalls: try: file = '/etc/sysctl.conf' conf = public.readFile(file) - rep = "#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)" + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)" tmp = re.search(rep,conf).groups(0)[0] if tmp == '1': isPing = False except: @@ -397,11 +397,11 @@ class firewalls: flag = False import re # 判断端口是否正确 - rep = "^\d{1,5}(:\d{1,5})?$" + rep = r"^\d{1,5}(:\d{1,5})?$" if not re.search(rep, get.port): return public.return_msg_gettext(False, 'Port range is incorrect! should be between 100-65535'); # 判断IP是否正确 - rep2 = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" if not re.search(rep2, get.address): return public.return_msg_gettext(False, 'IP address is illegal!'); import time ports = get.port diff --git a/class/firewalld.py b/class/firewalld.py index 125cf45a..0c8fb111 100644 --- a/class/firewalld.py +++ b/class/firewalld.py @@ -1,9 +1,9 @@ #!/usr/bin/env python # coding:utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: 1249648969@qq.com, # +------------------------------------------------------------------- diff --git a/class/firewalls.py b/class/firewalls.py index b417e24d..57232920 100644 --- a/class/firewalls.py +++ b/class/firewalls.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x3 +# | aaPanel x3 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http:#bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import sys,os,public,re,firewalld,time @@ -310,15 +310,13 @@ class firewalls: #改远程端口 def SetSshPort(self,get): + port = get.port - if not port: - return public.return_msg_gettext(False, 'Port range must be between 22 and 65535!') - try: - if int(port) < 22 or int(port) > 65535: return public.return_msg_gettext(False,'Port range must be between 22 and 65535!') - except: - return public.return_msg_gettext(False, 'Port range must be between 22 and 65535!') + ports = ['21','25','80','443','8080','888','8888','7800'] - if port in ports: return public.return_msg_gettext(False,'Do NOT use common default port!') + if port in ports: + return public.return_msg_gettext(False,'Do NOT use common default port!') + # return public.return_message(-1, 0, 'Do NOT use common default port!') file = '/etc/ssh/sshd_config' conf = public.readFile(file) @@ -343,6 +341,7 @@ class firewalls: public.M('firewall').add('port,ps,addtime',(port,'SSH remote service',time.strftime('%Y-%m-%d %X',time.localtime()))) public.WriteLog("TYPE_FIREWALL", "FIREWALL_SSH_PORT",(port,)) return public.return_msg_gettext(True,'Setup successfully!') + # return public.return_message(0, 0, 'Setup successfully!') #取SSH信息 def GetSshInfo(self,get): diff --git a/class/ftp.py b/class/ftp.py index 0f8f1a7c..b048b3a1 100644 --- a/class/ftp.py +++ b/class/ftp.py @@ -1,10 +1,10 @@ #coding: utf-8 # + ------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # + ------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http:#bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # + ------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # + ------------------------------------------------------------------- import public,db,re,os,firewalls try: @@ -24,13 +24,13 @@ class ftp: import files,time fileObj=files.files() if get['ftp_username'].strip().find(' ') != -1: return public.returnMsg(False,'Username cannot contain spaces') - if re.search("\W+",get['ftp_username']): return {'status':False,'code':501,'msg':public.get_msg_gettext('Username is illegal, special characters are NOT allowed!')} + if re.search(r"\W+",get['ftp_username']): return {'status':False,'code':501,'msg':public.get_msg_gettext('Username is illegal, special characters are NOT allowed!')} if len(get['ftp_username']) < 3: return {'status':False,'code':501,'msg':public.get_msg_gettext('Username is illegal, cannot be less than 3 characters!')} if not fileObj.CheckDir(get['path']): return {'status':False,'code':501,'msg':public.get_msg_gettext('System critical directory cannot be used as FTP directory!')} if public.M('ftps').where('name=?',(get.ftp_username.strip(),)).count(): return public.return_msg_gettext(False,'User [{}] exists!',(get.ftp_username,)) username = get['ftp_username'].strip() - if re.search("[\/\\\:\*\?\"\'\<\>\|]+",username): - return public.return_msg_gettext(False,"Name cannot contain /\:*?\"<>| symbol") + if re.search("[\\/\\\\:\\*\\?\"\'\\<\\>\\|]+",username): + return public.return_msg_gettext(False,"Name cannot contain /\\:*?\"<>| symbol") password = get['ftp_password'].strip() if len(password) < 6: return public.return_msg_gettext(False, 'Password must be at least [{}] characters',("6",)) get.path = get['path'].replace(' ','') @@ -122,7 +122,7 @@ class ftp: if int(port) < 1 or int(port) > 65535: return public.return_msg_gettext(False,'Port range is incorrect!') file = '/www/server/pure-ftpd/etc/pure-ftpd.conf' conf = public.readFile(file) - rep = u"\n#?\s*Bind\s+[0-9]+\.[0-9]+\.[0-9]+\.+[0-9]+,([0-9]+)" + rep = u"\n#?\\s*Bind\\s+[0-9]+\\.[0-9]+\\.[0-9]+\\.+[0-9]+,([0-9]+)" #preg_match(rep,conf,tmp) conf = re.sub(rep,"\nBind 0.0.0.0," + port,conf) public.writeFile(file,conf) diff --git a/class/ftplog.py b/class/ftplog.py index fd08b633..0077aa4b 100644 --- a/class/ftplog.py +++ b/class/ftplog.py @@ -1,8 +1,8 @@ #coding: utf-8 # + ------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # + ------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http:#bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # + ------------------------------------------------------------------- # | Author: hezhihong <272267659@@qq.cn> # + ------------------------------------------------------------------- @@ -30,6 +30,8 @@ month_list = { class ftplog: + __AUTH_MSG =public.to_string ([84 ,104 ,105 ,115 ,32 ,102 ,101 ,97 ,116 ,117 ,114 ,101 ,32 ,105 ,115 ,32 ,101 ,120 ,99 ,108 ,117 ,115 ,105 ,118 ,101 ,32 ,116 ,111 ,32 ,116 ,104 ,101 ,32 ,112 ,114 ,111 ,32 ,101 ,100 ,105 ,116 ,105 ,111 ,110 ,44 ,32 ,112 ,108 ,101 ,97 ,115 ,101 ,32 ,97 ,99 ,116 ,105 ,118 ,97 ,116 ,101 ,32 ,105 ,116 ,32 ,102 ,105 ,114 ,115 ,116 ]) + def __init__(self): self.__messages_file = "/var/log/" self.__ftp_backup_path = public.get_backup_path() + '/pure-ftpd/' @@ -75,128 +77,44 @@ class ftplog: reverse=False) return file_name_list + def __check_auth(self): + from plugin_auth_v2 import Plugin as Plugin + plugin_obj = Plugin(False) + plugin_list = plugin_obj.get_plugin_list() + import PluginLoader + self.__IS_PRO_MEMBER = PluginLoader.get_auth_state() > 0 + return int(plugin_list["pro"]) > time.time() or self.__IS_PRO_MEMBER + def set_ftp_log(self, get): """ - @name 开启、关闭、获取日志状态 + @name 开启、关闭、获取日志状态 @author hezhihong @param get.exec_name 执行的动作 """ + if not self.__check_auth(): + return public.returnMsg(False, self.__AUTH_MSG) if not hasattr(get, 'exec_name'): return public.returnMsg(False, 'The parameter is incorrect!') - conf_path = '/etc/rsyslog.conf' - conf = public.readFile(conf_path) - import re - search_str = r"ftp\.\*.*\t*.*\t*.*-/var/log/pure-ftpd.log" - search_str_two = "ftp.none" - rep_str = '\nftp.*\t\t-/var/log/pure-ftpd.log\n' - result = re.search(search_str, conf) + ftp_file='/etc/rsyslog.d/pure-ftpd.conf' + write_string = '\nftp.*\t\t-/var/log/pure-ftpd.log\n' #获取日志状态 if get.exec_name == 'getlog': - if result: - return_result = 'start' + if os.path.exists(ftp_file): + return public.returnMsg(True, 'start') else: - return_result = 'stop' - return public.returnMsg(True, return_result) - #开启日志审计 + return public.returnMsg(True, 'stop') + + # 开启日志审计 elif get.exec_name == 'start': - # 兼容之前开启,会将配置文件搞坏 - if conf.count('ftp.nonenftp') > 5: - conf = ''' -# /etc/rsyslog.conf configuration file for rsyslog -# -# For more information install rsyslog-doc and see -# /usr/share/doc/rsyslog-doc/html/configuration/index.html -# -# Default logging rules can be found in /etc/rsyslog.d/50-default.conf - - -################# -#### MODULES #### -################# - -module(load="imuxsock") # provides support for local system logging -#module(load="immark") # provides --MARK-- message capability - -# provides UDP syslog reception -#module(load="imudp") -#input(type="imudp" port="514") - -# provides TCP syslog reception -#module(load="imtcp") -#input(type="imtcp" port="514") - -# provides kernel logging support and enable non-kernel klog messages -module(load="imklog" permitnonkernelfacility="on") - -########################### -#### GLOBAL DIRECTIVES #### -########################### - -# -# Use traditional timestamp format. -# To enable high precision timestamps, comment out the following line. -# -$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat - -# Filter duplicated messages -$RepeatedMsgReduction on - -# -# Set the default permissions for all log files. -# -$FileOwner syslog -$FileGroup adm -$FileCreateMode 0640 -$DirCreateMode 0755 -$Umask 0022 -$PrivDropToUser syslog -$PrivDropToGroup syslog - - - -# -# Where to place spool and state files -# -$WorkDirectory /var/spool/rsyslog - -# -# Include all config files in /etc/rsyslog.d/ -# -$IncludeConfig /etc/rsyslog.d/*.conf - - - - ''' - public.writeFile(conf_path, conf) - return self.set_ftp_log(get) - if '*.info;mail.none;authpriv.none;' not in conf: - conf += '\n*.info;mail.none;authpriv.none;cron.none /var/log/messages\n' - if result: - conf = conf.replace(search_str, rep_str) - else: - conf += rep_str - #禁止ftp日志写入/var/log/messages - - d_conf = conf[conf.rfind('info;'):] - d_conf = d_conf[:d_conf.find('/')] - s_conf = d_conf.replace(',', ';') - if s_conf.find(search_str_two) == -1: - str_index = s_conf.rfind(';') - s_conf = s_conf[:str_index + - 1] + search_str_two + s_conf[str_index + 1:] - conf = conf.replace(d_conf, s_conf) + public.writeFile(ftp_file, write_string) self.add_crontab() - #关闭日志审计 + # 关闭日志审计 elif get.exec_name == 'stop': - if result: - conf = re.sub(search_str, '', conf) - #取消禁止ftp日志写入/var/log/messages - if conf.find(search_str_two) != -1: - conf = conf.replace(search_str_two, '') - for i in [';;', ',,', ';,', ',;']: - if conf.find(i) != -1: conf = conf.replace(i, '') + if os.path.exists(ftp_file): + os.remove(ftp_file) + if os.path.exists(ftp_file): + return public.returnMsg(False, 'failed to close the log') self.del_crontab() - public.writeFile(conf_path, conf) public.ExecShell('systemctl restart rsyslog') return public.returnMsg(True, 'successfully set') @@ -404,6 +322,7 @@ $IncludeConfig /etc/rsyslog.d/*.conf if search_str not in line: continue tmp_v = line.split(search_str) + if len(tmp_v[0].strip().split())<3:continue hostname = tmp_v[0].strip().split()[3].strip() action_time = tmp_v[0].replace(hostname, '').strip() action_info['time'] = self.get_format_time(action_time) diff --git a/class/http_requests.py b/class/http_requests.py index 9c168ba1..fe8632e6 100644 --- a/class/http_requests.py +++ b/class/http_requests.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- # +------------------------------------------------------------------- @@ -440,13 +440,19 @@ exit($header."\r\n\r\n".json_encode($body)); match = re.search("(.|\n)+\r\n\r\n",req) if not match: return req,{},0 tmp = match.group().split("\r\n") - i = 0 - if tmp[i].find('Continue') != -1: i+=1 - if not tmp[i]: i+=1 - try: - status_code = int(tmp[i].split()[1]) - except: - status_code = 0 + + status_code = 0 + + from public.regexplib import search_http_response_status_line + + for i in range(len(tmp) - 1): + m = search_http_response_status_line.search(tmp[i]) + if not m: + continue + + status_code = int(m.group(1)) + break + body = req.replace(match.group(),'') return body,tmp,status_code diff --git a/class/jobs.py b/class/jobs.py index 91bfb5c6..b264df58 100755 --- a/class/jobs.py +++ b/class/jobs.py @@ -1,15 +1,16 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import time,public,db,os,sys,json,re,shutil os.chdir('/www/server/panel') def control_init(): + update_py312() public.chdck_salt() clear_other_files() sql_pacth() @@ -21,6 +22,7 @@ def control_init(): clean_max_log('/root/.pm2/pm2.log',1024*1024*20) remove_tty1() clean_hook_log() + acme_crond_reinit() run_new() clean_max_log('/www/server/cron',1024*1024*5,20) clean_max_log("/www/server/panel/plugin/webhook/script",1024*1024*1) @@ -31,7 +33,7 @@ def control_init(): set_pma_access() # public.set_open_basedir() clear_fastcgi_safe() - update_py37() + # update_py37() run_script() set_php_cli_env() check_enable_php() @@ -45,6 +47,7 @@ def control_init(): #hide_docker() rep_pyenv_link() rm_apache_cgi_test() + uninstall_pip_lxml() def rm_apache_cgi_test(): ''' @@ -56,6 +59,32 @@ def rm_apache_cgi_test(): if os.path.exists(test_cgi_file): os.remove(test_cgi_file) +def uninstall_pip_lxml(): + ''' + @name 如果lxml版本大于5.0.0卸载它,解决部分机器 import bs4 程序崩溃的问题 + ''' + try: + import lxml + if lxml.__version__ > '5.0.0': + public.ExecShell('/www/server/panel/pyenv/bin/pip3 uninstall lxml -y') + except: + pass + +def acme_crond_reinit(): + ''' + @name 修复acme定时任务 + @return void + ''' + try: + lets_config_file = os.path.join(public.get_panel_path(),'config/letsencrypt_v2.json') + if not os.path.exists(lets_config_file): return + + import acme_v2 + acme_v2.acme_v2().set_crond() + except: + pass + + def rep_pyenv_link(): ''' @name 修复pyenv环境软链 @@ -156,7 +185,7 @@ def clear_other_files(): shutil.copyfile(src_file,init_file) if os.path.getsize(init_file) < 10: public.ExecShell("chattr -i " + init_file) - public.ExecShell("\cp -arf %s %s" % (src_file,init_file)) + public.ExecShell(r"\cp -arf %s %s" % (src_file,init_file)) public.ExecShell("chmod +x %s" % init_file) except:pass public.writeFile('/var/bt_setupPath.conf','/www') @@ -484,7 +513,7 @@ def set_php_cli_env(): if not os.path.exists(php_fpm) and os.path.exists(php_fpm_src): os.symlink(php_fpm_src,php_fpm) if not os.path.exists(php_pecl) and os.path.exists(php_pecl_src): os.symlink(php_pecl_src,php_pecl) if not os.path.exists(php_pear) and os.path.exists(php_pear_src): os.symlink(php_pear_src,php_pear) - public.ExecShell("\cp -f {} {}".format(php_ini,php_cli_ini)) # 每次复制新的php.ini到php-cli.ini + public.ExecShell(r"\cp -f {} {}".format(php_ini,php_cli_ini)) # 每次复制新的php.ini到php-cli.ini public.ExecShell('sed -i "/disable_functions/d" {}'.format(php_cli_ini)) # 清理禁用函数 bashrc_body += "alias php{}='php{} -c {}'\n".format(php_version,php_version,php_cli_ini) # 设置别名 else: @@ -507,7 +536,7 @@ def check_enable_php(): for php_v in php_versions: ngx_php_conf = public.get_setup_path() + '/nginx/conf/enable-php-{}.conf'.format(php_v) if os.path.exists(ngx_php_conf): continue - enable_conf = ''' + enable_conf = r''' location ~ [^/]\.php(/|$) {{ try_files $uri =404; @@ -549,7 +578,7 @@ def run_script(): if not os.path.exists(script_info['script_file']) \ or script_info['script_file'].find('/www/server/panel/plugin/') != 0 \ - or not re.match('^\w+$',script_info['script_file']): + or not re.match(r'^\w+$',script_info['script_file']): os.remove(script_conf_file) if os.path.exists(exec_log_file): os.remove(exec_log_file) continue @@ -594,6 +623,7 @@ def files_set_mode(): ["/www/backup","","root",600,True], ["/www/wwwlogs","","www",700,True], ["/www/enterprise_backup","","root",600,True], + ["/www/server/panel/webserver", "", "root", 755, False], ["/www/server/cron","","root",700,True], ["/www/server/cron","/*.log","root",600,True], ["/www/server/stop","","root",755,True], @@ -602,7 +632,7 @@ def files_set_mode(): ["/www/server/panel/class","","root",600,True], ["/www/server/panel/data","","root",600,True], ["/www/server/panel/plugin","","root",600,False], - ["/www/server/panel/BTPanel","","root",600,True], + ["/www/server/panel/BTPanel","","root",755,True], ["/www/server/panel/vhost","","root",600,True], ["/www/server/panel/rewrite","","root",600,True], ["/www/server/panel/config","","root",600,True], @@ -617,6 +647,7 @@ def files_set_mode(): ["/www/server/panel/BT-Panel","","root",700,False], ["/www/server/panel/BT-Task","","root",700,False], ["/www/server/panel","/*.py","root",600,False], + ["/www/server/panel","","root",755,False], ["/dev/shm/session.db","","root",600,False], ["/dev/shm/session_py3","","root",600,True], ["/dev/shm/session_py2","","root",600,True], @@ -706,7 +737,7 @@ def set_pma_access(): -#尝试升级到独立环境 +#尝试升级到独立环境3.7 def update_py37(): pyenv='/www/server/panel/pyenv/bin/python3' pyenv_exists='/www/server/panel/data/pyenv_exists.pl' @@ -716,6 +747,16 @@ def update_py37(): public.writeFile(pyenv_exists,'True') return True +#尝试升级到独立环境3.12 +def update_py312(): + pyenv='/www/server/panel/pyenv/bin/python3.12' + if os.path.exists(pyenv): return False + download_url = public.get_url() + only_update_pyenv312 = '/tmp/only_update_pyenv312.pl' + public.writeFile(only_update_pyenv312, 'True') + public.ExecShell("nohup curl -k {}/install/update_7.x_en.sh|bash &>/tmp/panelUpdate.pl &".format(download_url)) + return True + def test_ping(): _f = '/www/server/panel/data/ping_token.pl' if os.path.exists(_f): os.remove(_f) @@ -855,7 +896,7 @@ def disable_putenv(fun_name): if os.path.exists(is_set_disable): return True php_vs = public.get_php_versions() php_ini = "/www/server/php/{0}/etc/php.ini" - rep = "disable_functions\s*=\s*.*" + rep = r"disable_functions\s*=\s*.*" for pv in php_vs: php_ini_path = php_ini.format(pv) if not os.path.exists(php_ini_path): continue diff --git a/class/log_analysis.py b/class/log_analysis.py index 3c6fd646..83b29e95 100644 --- a/class/log_analysis.py +++ b/class/log_analysis.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: lkq +# | Author: lkq # | # | 日志分析工具 # +------------------------------------------------------------------- @@ -21,7 +21,7 @@ class log_analysis: def __init__(self): if not os.path.exists(self.path + '/log/'): os.makedirs(self.path + '/log/') if not os.path.exists(self.log_analysis_path): - log_analysis_data = '''help(){ + log_analysis_data = r'''help(){ echo "Usage: ./action.sh [options] [FILE] [OUTFILE] " echo "Options:" echo "xxx.sh san_log [FILE] Get the log list with the keywords xss|sql|mingsense information|php code execution in the successful request [OUTFILE] 11" @@ -231,7 +231,7 @@ echo "[*] shut down" result['is_status'] = True else: result['is_status'] = False - if os.path.exists(speed+".time"): + if os.path.exists(speed+".time") and os.path.getsize(speed+".time") > 0: time_data, start_time, status = public.ReadFile(self.path + '/log/' + log_path + ".time").split("[]") if status == '1' or start_time==1: result['time']=time_data @@ -257,4 +257,4 @@ echo "[*] shut down" type_list = ['xss', 'sql', 'san', 'php', 'ip', 'url'] if get.type not in type_list: return public.ReturnMsg(False, 'Type mismatch') if not os.path.exists(speed + get.type + '.log'): return public.ReturnMsg(False, 'Record does not exist') - return self.get_log_count(speed + get.type + '.log', is_body=True) \ No newline at end of file + return self.get_log_count(speed + get.type + '.log', is_body=True) diff --git a/class/logsModel/ftpModel.py b/class/logsModel/ftpModel.py index 1fee7fef..c6d09304 100644 --- a/class/logsModel/ftpModel.py +++ b/class/logsModel/ftpModel.py @@ -1,469 +1,452 @@ -#coding: utf-8 -# + ------------------------------------------------------------------- -# | 宝塔Linux面板 -# + ------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http:#bt.cn) All rights reserved. -# + ------------------------------------------------------------------- -# | Author: hezhihong <272267659@@qq.cn> -# + ------------------------------------------------------------------- -import public, os, time -from logsModel.base import logsBase - -try: - from BTPanel import session -except: - pass -#英文转月份缩写 -month_list = { - "Jan": "1", - "Feb": "2", - "Mar": "3", - "Apr": "4", - "May": "5", - "Jun": "6", - "Jul": "7", - "Aug": "8", - "Sept": "9", - "Sep": "9", - "Oct": "10", - "Nov": "11", - "Dec": "12" -} - - -class main(logsBase): - def __init__(self): - self.__messages_file = "/var/log/" - self.__ftp_backup_path = public.get_backup_path() + '/pure-ftpd/' - if not os.path.isdir(self.__ftp_backup_path): - public.ExecShell('mkdir -p {}'.format(self.__ftp_backup_path)) - self.__script_py = public.get_panel_path() + '/script/ftplogs_cut.py' - - def get_file_list(self, path, is_bakcup=False): - """ - @name 取所有messages日志文件 - @param path: 日志文件路径 - @return: 返回日志文件列表 - """ - files = os.listdir(path) - if is_bakcup: - file_name_list = [{ - "file": "/var/log/pure-ftpd.log", - "time": int(time.time()) - }] - else: - file_name_list = [] - for i in files: - tmp_dict = {} - if not i: continue - file_path = path + i - tmp_dict['file'] = file_path - if is_bakcup: - if os.path.isfile(file_path) and i.find('pure-ftpd.log') != -1: - tmp_dict['time'] = int( - public.to_date( - times=os.path.basename(file_path).split('_')[0] + - ' 00:00:00')) - file_name_list.append(tmp_dict) - else: - if os.path.isfile(file_path) and i.find('messages') != -1: - tmp_dict['time'] = int( - public.to_date( - times=os.path.basename(file_path).split('-')[1] + - ' 00:00:00')) - file_name_list.append(tmp_dict) - file_name_list = sorted(file_name_list, - key=lambda x: x['time'], - reverse=False) - return file_name_list - - def set_ftp_log(self, get): - """ - @name 开启、关闭、获取日志状态 - @author hezhihong - @param get.exec_name 执行的动作 - """ - if not hasattr(get, 'exec_name'): - return public.returnMsg(False, 'The parameter is incorrect!') - conf_path = '/etc/rsyslog.conf' - conf = public.readFile(conf_path) - import re - search_str = r"ftp\.\*.*\t*.*\t*.*-/var/log/pure-ftpd.log" - search_str_two = "ftp.none" - rep_str = '\nftp.*\t\t-/var/log/pure-ftpd.log\n' - result = re.search(search_str, conf) - #获取日志状态 - if get.exec_name == 'getlog': - if result: - return_result = 'start' - else: - return_result = 'stop' - return public.returnMsg(True, return_result) - #开启日志审计 - elif get.exec_name == 'start': - if result: - conf = conf.replace(search_str, rep_str) - else: - conf += rep_str - #禁止ftp日志写入/var/log/messages - - d_conf = conf[conf.rfind('info;'):] - d_conf = d_conf[:d_conf.find('/')] - s_conf = d_conf.replace(',', ';') - if s_conf.find(search_str_two) == -1: - str_index = s_conf.rfind(';') - s_conf = s_conf[:str_index + - 1] + search_str_two + s_conf[str_index + 1:] - conf = conf.replace(d_conf, s_conf) - self.add_crontab() - #关闭日志审计 - elif get.exec_name == 'stop': - if result: - conf = re.sub(search_str, '', conf) - #取消禁止ftp日志写入/var/log/messages - if conf.find(search_str_two) != -1: - conf = conf.replace(search_str_two, '') - for i in [';;', ',,', ';,', ',;']: - if conf.find(i) != -1: conf = conf.replace(i, '') - self.del_crontab() - public.writeFile(conf_path, conf) - public.ExecShell('systemctl restart rsyslog') - return public.returnMsg(True, 'successfully set') - - def get_format_time(self, englist_time): - """ - @name 时间英文转换 - """ - chinanese_time = '' - try: - for i in month_list.keys(): - if i in englist_time: - tmp_time = englist_time.replace(i, month_list[i]) - tmp_time = tmp_time.split() - chinanese_time = '{}-{} {}'.format(tmp_time[0], tmp_time[1], - tmp_time[2]) - break - return chinanese_time - except: - return chinanese_time - - def get_login_log(self, get): - """ - @name 取登录日志 - @author hezhihong - @param get.user_name ftp用户名 - return - """ - - search_str = 'pure-ftpd:' - search_str2 = 'pure-ftpd[' - if not hasattr(get, 'user_name'): - return public.returnMsg(False, 'The parameter is incorrect!') - args = public.dict_obj() - args.exec_name = 'getlog' - file_name = self.__ftp_backup_path - is_backup = True - if self.set_ftp_log(get) == 'stop': - file_name = self.__messages_file - is_backup = False - file_list = self.get_file_list(file_name, is_backup) - data = [] - sortid = 0 - tmp_dict = {} - login_all = [] - for file in file_list: - - if not os.path.isfile(file['file']): continue - conf = public.readFile(file['file']) - lines = conf.split('\n') - for line in lines: - if not line: continue - login_info = {} - if search_str not in line and search_str2 not in line: - continue - tmp_value = ' is now logged in' - info = line[:line.find(search_str)].strip() - if not info: - info = line[:line.find(search_str2)].strip() - hostname = info.split()[-1] - exec_time = info.split(hostname)[0].strip() - exec_time = self.get_format_time(exec_time) - ip = line[line.find('(') + 1:line.find(')')].split('@')[1] - - #取登录成功日志 - if tmp_value in line: - user = line.split(tmp_value)[0].strip().split()[-1] - if user == '?' or user != get.user_name: continue - dict_index = '{}__{}'.format(user, ip) - if dict_index not in tmp_dict: - tmp_dict[dict_index] = [] - tmp_dict[dict_index].append(exec_time) - - #取登出日志 - tmp_value = '[INFO] Logout.' - tmp_value_two = 'Timeout - try typing a little faster next time' - if tmp_value in line or tmp_value_two in line: - user = line[line.find('(') + - 1:line.find(')')].split('@')[0] - if user == '?' or user != get.user_name: continue - dict_index = '{}__{}'.format(user, ip) - try: - login_info['out_time'] = exec_time - login_info['in_time'] = tmp_dict[dict_index][0] - login_info['user'] = user - login_info['ip'] = ip - login_info['status'] = 'Success' #0为登录失败,1为登录成功 - login_info['sortid'] = sortid - login_all.append(login_info) - tmp_dict[dict_index] = [] - sortid += 1 - except: - pass - #取登录失败日志 - tmp_value = 'Authentication failed for user' - if tmp_value in line: - user = line.split(tmp_value)[-1].replace('[', '').replace( - ']', '').strip() - if user == '?' or user != get.user_name: continue - login_info['user'] = user - login_info['ip'] = ip - login_info['status'] = 'Failure' #0为登录失败,1为登录成功 - login_info['in_time'] = exec_time - login_info['out_time'] = exec_time - login_info['sortid'] = sortid - login_all.append(login_info) - sortid += 1 - - if tmp_dict: - for item in tmp_dict.keys(): - if not tmp_dict[item]: continue - info = { - "status": "login successful", - "in_time": tmp_dict[item][0], - "out_time": "connecting", - "user": item.split('__')[0], - "ip": item.split('__')[1], - "sortid": sortid - } - sortid += 1 - login_all.append(info) - #搜索过滤 - if login_all and 'search' in get and get.search and get.search.strip(): - for info in login_all: - try: - search_str = str(get.search).strip().lower() - # public.writeFile('/tmp/aa.aa', get.search) - if info['ip'].find(search_str) != -1 or info['user'].lower( - ).find(search_str) != -1 or info['status'].find( - search_str) != -1 or info['in_time'].find( - search_str) != -1: - data.append(info) - elif info['out_time'] and info['out_time'].find( - search_str) != -1: - data.append(info) - except: - pass - else: - for info2 in login_all: - data.append(info2) - - data = sorted(data, key=lambda x: x['sortid'], reverse=True) - return self.get_page(data, get) - - def get_page(self, data, get): - """ - @name 取分页 - @author hezhihong - @param data 需要分页的数据 list - @param get.p 第几页 - @return 指定分页数据 - """ - # 包含分页类 - import page - # 实例化分页类 - page = page.Page() - - info = {} - info['count'] = len(data) - info['row'] = 10 - info['p'] = 1 - if hasattr(get, 'p'): - info['p'] = int(get['p']) - info['uri'] = {} - info['return_js'] = '' - # 获取分页数据 - result = {} - result['page'] = page.GetPage(info, limit='1,2,3,4,5,8') - n = 0 - result['data'] = [] - for i in range(info['count']): - if n >= page.ROW: break - if i < page.SHIFT: continue - n += 1 - result['data'].append(data[i]) - return result - - def get_action_log(self, get): - """ - @name 取操作日志 - @author hezhihong - @param get.user_name ftp用户名 - return {"upload":[],"download":[],"rename":[],"delete":[]} - """ - search_str = 'pure-ftpd:' - args = public.dict_obj() - args.exec_name = 'getlog' - file_name = self.__ftp_backup_path - is_backup = True - if self.set_ftp_log(get) == 'stop': - file_name = self.__messages_file - is_backup = False - file_list = self.get_file_list(file_name, is_backup) - if not hasattr(get, 'user_name'): - return public.returnMsg(False, 'The parameter is incorrect!') - data = [] - tmp_data = [] - sortid = 0 - for file in file_list: - if not os.path.isfile(file['file']): continue - conf = public.readFile(file['file']) - lines = conf.split('\n') - for line in lines: - if not line: continue - action_info = {} - if search_str not in line: continue - - tmp_v = line.split(search_str) - hostname = tmp_v[0].strip().split()[3].strip() - action_time = tmp_v[0].replace(hostname, '').strip() - action_info['time'] = self.get_format_time(action_time) - - upload_value = ' uploaded ' - download_value = ' downloaded ' - rename_value = 'successfully renamed or moved:' - delete_value = ' Deleted ' - ip = line[line.find('(') + 1:line.find(')')].split('@')[1] - action_info['ip'] = ip - action_info['type'] = '' - #取操作用户 - user = '' - if upload_value in line or download_value in line or rename_value in line or delete_value in line: - user = line[line.find('(') + - 1:line.find(')')].split('@')[0] - action_info['sortid'] = sortid - sortid = sortid + 1 - if not user or user != get.user_name: continue - #取上传日志 - if (get.type == 'all' - or get.type == 'upload') and upload_value in line: - line_list = line.split() - upload_index = line_list.index('uploaded') - # action_info['file'] = line_list[upload_index - 1].replace( - # '//', '/') - action_info['file'] = line[line.find(']') + - 1:line.rfind('(')].replace( - 'uploaded', - '').replace('//', - '/').strip() - action_info['type'] = 'upload' - tmp_data.append(action_info) - #取下载日志 - if (get.type == 'all' - or get.type == 'download') and download_value in line: - line_list = line.split() - upload_index = line_list.index('downloaded') - action_info['file'] = line_list[upload_index - 1].replace( - '//', '/') - action_info['type'] = 'download' - tmp_data.append(action_info) - #取重命名日志 - if (get.type == 'all' - or get.type == 'rename') and rename_value in line: - action_info['file'] = line.split(rename_value)[1].replace( - '->', 'Renamed to').strip().replace('//', '/') - action_info['type'] = 'rename' - tmp_data.append(action_info) - #取删除日志 - if (get.type == 'all' - or get.type == 'delete') and delete_value in line: - action_info['file'] = line.split()[-1].strip().replace( - '//', '/') - action_info['type'] = 'delete' - tmp_data.append(action_info) - # f.close - #搜索过滤 - if tmp_data and 'search' in get and get.search and get.search.strip(): - for info in tmp_data: - search_str = str(get.search).strip().lower() - if info['ip'].find(search_str) != -1 or info['file'].lower( - ).find(search_str) != -1 or info['type'].find( - search_str) != -1 or info['time'].find( - search_str) != -1 or get.user_name.lower().find( - search_str) != -1: - data.append(info) - else: - for info2 in tmp_data: - data.append(info2) - data = sorted(data, key=lambda x: x['sortid'], reverse=True) - return self.get_page(data, get) - - def del_crontab(self): - """ - @name 删除项目定时清理任务 - @auther hezhihong<2022-10-31> - @return - """ - cron_name = '[Do not delete] FTP audit log cutting task' - cron_path = public.GetConfigValue('setup_path') + '/cron/' - cron_list = public.M('crontab').where("name=?", (cron_name, )).select() - if cron_list: - for i in cron_list: - if not i: continue - cron_echo = public.M('crontab').where( - "id=?", (i['id'], )).getField('echo') - args = {"id": i['id']} - import crontab - crontab.crontab().DelCrontab(args) - del_cron_file = cron_path + cron_echo - public.ExecShell( - "crontab -u root -l| grep -v '{}'|crontab -u root -". - format(del_cron_file)) - - def add_crontab(self): - """ - @name 构造日志切割任务 - """ - python_path = '' - try: - python_path = public.ExecShell('which btpython')[0].strip("\n") - except: - try: - python_path = public.ExecShell('which python')[0].strip("\n") - except: - pass - if not python_path: return False - if not public.M('crontab').where('name=?', - ('[Do not delete] FTP audit log cutting task', )).count(): - cmd = '{} {}'.format(python_path, self.__script_py) - args = { - "name": "[Do not delete] FTP audit log cutting task", - "type": 'day', - "where1": '', - "hour": '0', - "minute": '1', - "sName": "", - "sType": 'toShell', - "notice": '0', - "notice_channel": '', - "save": '', - "save_local": '1', - "backupTo": '', - "sBody": cmd, - "urladdress": '' - } - import crontab - res = crontab.crontab().AddCrontab(args) - if res and "id" in res.keys(): - return True - return False - return True +QRASP55VO/1DQ98p1csw9A== +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +5G1X0WJyak7IcriKROwRfg== +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +ZuLVraRZUj2RhJbyALJsxNMuhZe1AKXjIZKX1s/bahYi6PnNCfy1Qv0MKdv1oxSXYGJY2PXP7NLNatPW4VSB/zJjWGw5HqJLpeYo/MseDrI= +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +mlbcpvT0kUVOtns2yTGydDt4+07xDvuvRqS/JgkUon3SrcxUlm2Z4rG4Pa9PwOq+ +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +iOUsBGgOHEqzdAdQgfAp4trfzBztc5RmfOjqgmGickU= +XQTKBPclCgMYfJTNC1Qwx9JOU5PR8V/GzRhXc9amMLuHdAcID+TXUZzz7SdDuJA9 +1u+XjG/2+GSQRv6EzCaWRQ== +f3AsbhS4PaN1B4cUkY9T+A== +vlNl0s/hLJ4MLdvF5a1XATBpmRe09mwg70GHdHdh7mTmKAaerhorUT+w+dAajOtb +MMZtIZZuuuWy0CLBzKekTg== +i8F7pUrlHPBNsIux2MgfYA== +hAZTEkbxoxowQJisJSulNX6xYijfsoIV2aWlEpje85k= +Fzf1WPeHT+phfW8FPaBhNw== +iwW5Y10vLm5bol1PMh2K5PcgF/7PR7VaxhpNPLVQXiQ= +iiFBxU1BEtsymTSqJJqljqN1RY1RLiUspBdLrvd0CW0= +ldCxpJy7GYU9Z3wc/nhJ/te4XvT+sJUYSl50ZnBKy3w= +CrJuPSINqYUMS/s5z4adcU1LJqJ2Sojj3C2wjmLuRsM= +Psg6WqdUeMsPYvW+jnYn0evEHl+1XoE/rnCfo+c6WdA= +1z8iprMiHgI6LYZftXGjKDYPI7FVqZWCQ09oWcM3Pag= +o1ftBjL5WX5IAh9xWVdqd9wdMBvoYRdNfl+IAvPWFp8= +QUUBWS4HdgaVs9J11lpeBKBH5NkT6WbMMMnumMF/7Aw= +tfRWYnUvHUwnwQYB+ywLpslVnsH3tMXHAPD/cYoTpT4= +AtAHlguqAHnQsUP0meqLH3VU1D/0sLjQMR/6J/fyRbM= +7MgotmZFCo6+Kg3s1/uhRtwRAi7VKRh0cRfONXMb3yI= +CvGG8CijcRlYyvz5dGomfhXk6DzjYPOBtu63/Veri24= +/PTfuW9bPdAz/6cUQf+E55cP43jnvaceBCRBneohYbA= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Tz7Nk5H535VRrRyTrsKtQL8WpBb8+0Czjnl0ON67RWc= +gSinJxQ2Dl02IUzuK5alezKMVZMklWE7tWiUg2329L/n+lRYCQ9UCFbErLWlw54SDdzf2aW7xcFo9f6yWsFr+Gmxv7QW7ILh8TsYOZ9znNp1PFfZW6D80dgQ9cC19CuCsN/nORHFj+ZzOKgvaEAfFiLoGt0ZpN5vpGxumpW0gpcWn6x7Ldft+bCe8OO/4vq4eE9RKaqjTgoUu0FLyAKLMsUrm9spdqinKdfS+0njcmvjNfpj5WOsg4REfjqMdCjT+eYrQYJpSEhzTFpPXaGYrdCg7TnFabrpVoJ3F5uhjQdkKGWqXHHHV/nbuQtIHnCtT8dwjmznuv5sSlpRCVqsgj9kw0VHrZYCN0PnqZ18QBTTb6aQzq2vcHCtyHbfnTgZYpXapiGZtR0S/U7DzfJCjnAyqRZxt4/wKHbAGYy8PQ8OUSXjFZJkxujMHXXUoxC2tVM7rRO1UyMTyERrP/FjuBzgj8YxTjyaxV4GnCT02jD45RbtdAK6txhyTaDZXcGS +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +NRG8WiBknxy8LZ7uM7Rtwjpor9jB6CRSN8s/tRh0bCGCT3O3yIYveV/kC3whqCOj +g+9Bvcv6QScOWqD1KWI83lDWKDEvFH24UumsH9uh9SSEzyoQuAQyJOm+k2nYRQYDLlXqKegz+5H8/A6yDuU4hTssV9YA2l/qH0zdlTP5IOI= +wgR07xfoapmx6eEnFHXXYsrtutdMRxYffYBJpExAFLL3LFn2eCspav2L+PBokDEhe88eat5Ab0fx9+MVZjZtHQ== +xWoGNWjKGPfI4gq8aHoTfOgPIu3QGZOJ5qEujzh3pzi1Iz2/AHJ8GvmS3bUx5254nOirctK/k6BFZA2v8ayK/EDVY6Wbr2d7jYxq8UAugp4= +uRzTWU2MnopGqqEdWeT6ZU1hlM1Dw8+qKY/ieJj+NsUqvH0CTNguPTeX+Laan8WHKtl7c/+1YA4rF+Fa3pfogwkGJNEhZb49H+rX+WZ7S1s= +1u+XjG/2+GSQRv6EzCaWRQ== +J24jXQQti6wQv5pCoF2ZrQbQF1j1bF4/QmmX1CwZUVEjmug7a9JAroEXBBTfhYssEYHy8XwASHc+rw3shghfWA== +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIlhC5ovOIKOhQYI+YFVzTxDyBspL/Zz/EA3ekwqZLC22t +eeFiPlXUI0Fos82HYztSmTsAYhvcpYqGtZuapEgzpCBypVgTjJ6W0KJC4ipcw897 +WuRvIBaPNRsVGEMoM4NSTioH6DPOvYqQxMaOqX/7OKFY0pBl49Sar+3IeBmADhmj +Z8UsPk1Q7HtwjRd4g01ryw== +N5xldEoitd9eWUThCvdOTB6LOXhNE9TOsjnkpc/9SMN2L8WJXh22t578ODk/HhdL +oxiSpIWwD4wuBub3KU1/cF5fchu1Vqpk+nU9bn11yzo= +PRG/YRzXVD8iVR3bzEcUnjHro+U+4TucaVNI9ygPWuVU3EqLuxmqZkTk2CyNK7O/ +VmVrGQo2zRokW/ZuO9bN68uCgzKuG5LF3dEm8osmV0HPcWbbdr6DBOymTEvfp0P/ftdl4LLYfLwuPpwsYazhEA== +VmVrGQo2zRokW/ZuO9bN68sblSJDKz0OqxMt0bHs/NUCob2DYRF35HVY0+LpxHBb +iDFAoKi0IJZ7t2pJuBivrw== +l2FJPs4YkAmmok1ulDRuSA== +PRG/YRzXVD8iVR3bzEcUnt6EfhtcL4GrcBoio2oUko9nPAoPP0N4kWpuhhh+oYdV +9zgnmC3+5N9XK5/NRaR4SMVNup0N5fm76yGD/GNlNnU= +sDlCNtGYD6nPzg4cZ+6YAIVaGRKK1htDJsqNdoebWm8= +1V2v8QerKOmubvSxgB4eTCXRIGj4wwBQxqmi+7HBYgg= +PRG/YRzXVD8iVR3bzEcUntHGmZ5qTlWv7ywh0UaUxJzXifYQX4pUcW7z5PPhLeh5 +sDlCNtGYD6nPzg4cZ+6YAOK/4Cv++bqYgS9TyEF32cuA5AzQdBq8lyKWQabFTagT +1D18KWr2hdVsBcdZx1OaPWkr8ytK7qVWaaza8e1vU6k= +VmVrGQo2zRokW/ZuO9bN66Vh5+Ci920JkrzQsTHn083JJPLf45hGeTPWU0PgjupO+yaDkxvgzGWeJhaeVem9Wq+8oeyKDNsYcPKXT94yeZ0UymKFveKN91eF38ApXJp2 +VmVrGQo2zRokW/ZuO9bN625bd4BNT0j+VcP1nTIY6XaDTRy5+5AWHUZPakkiglNT +VmVrGQo2zRokW/ZuO9bN69TcL00if5hvJ8+IR0VhUuvoRwQz2rzPWjBJm1Mk3z5a +VmVrGQo2zRokW/ZuO9bN62AVcq3plEcj4rxrsvQ9pPDU08M8azrqNxOOot3ghWHctbl7KH0YZqLimlQWQDerlojzSftx5vWNNRCgaO4RyF8= +VmVrGQo2zRokW/ZuO9bN6xD6PoCXoaoICnYYUV03lnrKjmQB9MEOOIg0j4Z9zWge +VmVrGQo2zRokW/ZuO9bN6/wMRobeZASkZ7UgaRv/zbQEquXghr8pcxoQVAx9J5G/e9b3CtH5KoqK0sb3R99OsA== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66Vh5+Ci920JkrzQsTHn083JJPLf45hGeTPWU0PgjupOJvco2FZSOBWgiEqCcFM7Q/vW4PqCZLGYaAAy6qf2tko= +VmVrGQo2zRokW/ZuO9bN625bd4BNT0j+VcP1nTIY6XaDTRy5+5AWHUZPakkiglNT +VmVrGQo2zRokW/ZuO9bN69TcL00if5hvJ8+IR0VhUuvoRwQz2rzPWjBJm1Mk3z5a +VmVrGQo2zRokW/ZuO9bN62AVcq3plEcj4rxrsvQ9pPDU08M8azrqNxOOot3ghWHctbl7KH0YZqLimlQWQDerlpd+8N/8ymIPDrStsyiyEo4= +VmVrGQo2zRokW/ZuO9bN6xD6PoCXoaoICnYYUV03lnrKjmQB9MEOOIg0j4Z9zWge +VmVrGQo2zRokW/ZuO9bN6/wMRobeZASkZ7UgaRv/zbQEquXghr8pcxoQVAx9J5G/e9b3CtH5KoqK0sb3R99OsA== +ojv9MyHa70Aio7SDrWcjwSYYqgfeHLgPvpjtVJfUmX0s6OSNU+WrIf9mNVPQ5TfqPavTidvffxVkcshQNOCsRA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklNjZdFug/ngOpMcvCKxEBrAN68NaDKAtp2GLglVerAbQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkllTuznFui81R4CASaEKJI9 +5wsVwKLrIjIWqeTNpSVp9YtfZPXXv5krNmxeXAjq+NQ= +1u+XjG/2+GSQRv6EzCaWRQ== +JKgfL+COM6qpIwe0Wm0zY0qrBwJPhJivgZdbq2ek/MNyEskG/o3RxQuD1murI70P +Z8UsPk1Q7HtwjRd4g01ryw== +9JiWXiRNWDTg+XstQ0v3tAQjzhKVhg9yH4FpL7cmNHMby1dZvf+KLKXDFxiL/GyHwt+mo7J9Eh26MMHXk7dZ7A== +vyYQukiyWticSXOwUY8MiznZqoLnnCC/RUpXDOGxEsg= +ho/Q+jrWDtBeg9J9ZTnKizg5CYSqIH+8noUWvD9u7YuJKKfD4Zw1m+s2/8nfOg5c +Z8UsPk1Q7HtwjRd4g01ryw== +fLETQ22HuDIh4ibW1C50WajryFpGmdAvdbylaFXR0PYbVChIKwUBviTdPnbJl3rp +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmVtxbXBpGlFIU9ZBe0L9nDB3cJqXtkVNxiOuaaYNBuGag== +c0jePRxtTVZYop75Q5JCFonnV+X8W6zX0oXQ+sQuZtHIAMgLt2jbmRWY4GrPgmiW +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW7Akukb+j0lqloIv/j7uSM4oazJqFVNfgaZWfRnkezm/n/qRAFeZXm0pd4Y650TvY= +Mmz9tB5RaVIWQerkiDs2BePvXl4dNWSjl+vJFVGrvpac5kzICL/kBoClsURQxOPYTWXamhuvoo8Pwhw/Asx8vw== +R14ZYm6mELJXNFBxHSBttdYfQPT7lYz3sNsgm+2cAVEVZqqsWoLADHy1tzgaDLwVuLUQU00i5jhUkarevZBgbw== +jpkMvZxe0AU7LK72hOoSV9O2+nkRjDR2qPsiciba+P0= +SSb4svay1O2En913Q8HWTI5XiKI6KpCz9PP4OKkiVgIZ9PAiv190X/Q6cvCmIvEn +O3CUgrw2GJfB+mDjH5+NdgM4cG182DjbaaqgGRQTDbbHFEBxIQHWpD3HPFUKruz8 +VmVrGQo2zRokW/ZuO9bN62aET6XweFEf8bCs0QU19kFV77Lm2Kxx3MCAPMJIlJSp +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN62aET6XweFEf8bCs0QU19kF1ezkxxQftj/4sG8qLYubO +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmVhrSS9BSOA1ZLru6nt/7dm75/vFI6+H/XoIqtvVBWgGA== +13vYlKuO6A3n1KjBvCLS2cZOnSAYizlRW6NVt55Nvpg= +Rc/EJuEm/I9INM8fD5RDm8V6NM+R609UAWBHMDxIGH1KB0feE4DfP+R5GNLxg7AM +xWoGNWjKGPfI4gq8aHoTfBMUl5q05yLZWa9BqWZ+hHXNzfrvrNJv7lUhLv58UG50furKaKDp9/7/vwDMn7xgPA== +32pdC9DD05OE2l0oXazDFBLSw6QGkeD2EftJ95N/ZFU= +9Ky7E5Jxg7bFh45xr+PtLAQzRENCwucwjn+oXpXkeQE= +Rc/EJuEm/I9INM8fD5RDm8V6NM+R609UAWBHMDxIGH3FuQzm6WS6LLN0bq9R/qzA +O3CUgrw2GJfB+mDjH5+NdgM4cG182DjbaaqgGRQTDbbHFEBxIQHWpD3HPFUKruz8 +VmVrGQo2zRokW/ZuO9bN6zRl0iz3kGEFiV+wGJc1Rx+/HiOt/vX0mu63k3Gx19QQ +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJmq0nitHFfMxYtPtCzV22AE +VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUikKCfZfG5oV8DWImIHip5MshUySi7lDaZb63pdnAbPrhvWTvaNXMG/2J1IdORcGPQ= +32pdC9DD05OE2l0oXazDFFXAy1MI/uPhakkzCdBdnMA= +4+wKB+Tel1iow8au3cOIJaTLjj3pysITCcJJ7ec43Sg8lqI/J7o8CtRAAtYI0AOu3JqoT8kI2t89ug8sZ1eCiQ== +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOHsyhzYJSgj0XYfDyZD/qpmTBlCMqtpJ0emRNzsjaERyA== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +f+rqXRH5RpetwpbV8cB3LNb4H/TF90wkaKs48d1M7L+avSymddn7mKYz0NItjsSS +Z8UsPk1Q7HtwjRd4g01ryw== +6Jpks4lpI/jb7zQY9ZwZK1zkFQHFLO1SftfqDQPgq1EBsFoAqyniZa+4+xJKzZfT +Z8UsPk1Q7HtwjRd4g01ryw== +IOQ7R1QbtHtN/omAE4BWfAjZgPJASxuTDruVD3whaNc= +b4OJVZe8QyIpjuTpKXDL9A== +wlLHv6kT3Q/RmtMBN4nDAQ8YSs8Zt0gDBRyWHhM8ed+GGdWzPmL+jZ6S+F5YV+22 +VmVrGQo2zRokW/ZuO9bN6xjRZ/jgghl2/5XRKMIuEuFfgv1283QcodM/1ZOCBV+E +VmVrGQo2zRokW/ZuO9bN6x+Y6lBOlB/5zr7yqielcHbAt1j/vhhHLiR0XLFVJ4MYIB7qEVFJCIX6M64zazFiQeEMWR6HA8xoXyo6nAZJhlk= +VmVrGQo2zRokW/ZuO9bN6zKW75ke8QGqFd99GlEdhOmQAXdMhXbfkO0SVCSKXZhAgrRq5E4FlbZVLyjCi8XIzw== +VmVrGQo2zRokW/ZuO9bN6/VA0tcNP0B0yWPLZItZlkbpytNLRqAe/ddM0lgyhD/+AVdhq9Y31ZAG+aChEtUpGjK+qk8XXC1AHcYIovJNZ+PRowAmFi0HJrFUfIQN6E7Q +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpXsF9xMLoel3FaVnpw+fNuSwrGC7RSUXTIrW2j2yH61A= +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +L0eUthVnpkGsmKFAX6d+uK/AWodUm3mcxHH2qymBAgItW7Ziboyd2OX6uMRxQUgs +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uK/AWodUm3mcxHH2qymBAgItW7Ziboyd2OX6uMRxQUgs +1u+XjG/2+GSQRv6EzCaWRQ== +69Uik37T733MFTHEnugne9sIK0q/BrAEinbmIsH25IyzJWj8GWaHG6Tu8rx0i8YI +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIluXzbJqLPwB2hMZiB6idlsw= +vyYQukiyWticSXOwUY8MiznZqoLnnCC/RUpXDOGxEsg= +ho/Q+jrWDtBeg9J9ZTnKi7Z8oMuCLRtnjC35Cw+4aVDyKydByU+COt6lumOoVZyj +1xJ0clp+39cOWW64WGs9Tw== +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +avTFp0LCGrKbHzgXWCq/pYahaBNfx164yVsSAVTJDN4lWK0zoj6Eh3CajVgk0hPL +avTFp0LCGrKbHzgXWCq/pSoIEj7PXeiBI2BkNQsojArLvnQoeeQAiRG4S9Ely2YX +c0jePRxtTVZYop75Q5JCFsMjGu62TZDSljeQjMtCShNsz0Y9SFIv08dPVdTjI95b +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW7Akukb+j0lqloIv/j7uSM4oazJqFVNfgaZWfRnkezm/n/qRAFeZXm0pd4Y650TvY= +TzinVBSdupkFZuXDgmwYfiSs5GTHKq13dTr2kdVzFPy0Jh42rAak6FUbANl5whZO +W7OoNBP/IWbYzUOvpg/slY2NVhp515LKHRWJIoHB+V62OipmiB60YwRsLUoQ2Yhe +ojv9MyHa70Aio7SDrWcjwQ0VSHrTkMcwddE36Wz7lCz16K6ytw4GpaCto2OYzTO/ +IfOrW6dR51K0HWsQz64KwMZLSub5xEzT/cQoJpZiUus= +MzOj4ZiBOJDNVP8vi/lFQIcd3YW2BaDb4kSPJzNVOdlx1Fa3HoeBVOYpiRWE7XX1 +PRG/YRzXVD8iVR3bzEcUno4Z8XHjrzDgKJNn9oJou7lkrRw3h7on47id8HDtKTRy +j7S8zldDFIkYlZuEh7uh1S+jR/V5xbpfKOdCgELD/V8= +yqOVJCkAGOhqcQYBOo7yqReCj9ixiaIB5tdlZ21DAUB9HlVyjl18y9Mfwf2BKrxO1mdZmBSOas0Z9YsNQ75XTg== +NYuttixIfaRvF0QtYZhNf933obSmTd0Sfv1RLv5CWew= +jg/rCd1ltkosbKN5ZG2fjyfZDh9bj6LoXpZWnO+Jm3I= +kTToPYSGc065HLDqbjV0HFp+qk2oCg3rcMPzGznvj4s= +UsNFlHPXrsQOqjn4RzHaA2d+Dsggsz8fegt1C0el50M= +m5EI3+BXxkQEYKYaxORD+qnAwk1i08GBypJtuz97N8s= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTFLJVKokCn/n7cBzRCyjPmemCDNpavHplLKoYC5ZrVB11XPhYbqw8m0ksUK1fSNNJw== +k1ktu/l6HFULD7Vyr8Cv0HMlLvOl3L1DvBU4YpKX0wAfHx0c0egKY2YtT74Sqa68k3g5TvkiAzu1NRNlkcoBZw== +rtgmVFvoY2d3wTFk50ruC6zmYmoALMFLQOc+xc+vXmJzN60uRcu12WAXC28NrZ2x +wlLHv6kT3Q/RmtMBN4nDAesM/ZGOfx7RLjx/t+u4GQE= +VmVrGQo2zRokW/ZuO9bN692Dxe6/pJb49NcEGwdYFHp1ykWA3uXz8Dyrp2BHgNzY +VmVrGQo2zRokW/ZuO9bN65W0SAM+Lz+ixIA1yfBIlyIfbjLiw8kUEWgRJErCtT/B +VmVrGQo2zRokW/ZuO9bN63sbeay11c+Azj5VEIm5hpE6JlT9cIzf6HYvRvtfkHtUzfa7mLb2YRp06Fq3uXR7IgrEOFQK+h9IpdXLW6HxD8Q= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN68NGdAuCULKcdnQ9QIPhYF2d+4WoU/gSRVS6rllapIH/JcXSlRMgq/Lahda1FjioQA== +VmVrGQo2zRokW/ZuO9bN6/D3pD6pMXGNmGG8sGEgUx6XUzf02GbZTCaXui9nRWWO8tkj2O9evUoVTjlZbcp3bw== +VmVrGQo2zRokW/ZuO9bN656ysh/dA/L9/oKKFnGsliQ= +VmVrGQo2zRokW/ZuO9bN62VJEYtP1Byl1VdEwj5br8q1Cxt1YWn0/LGOVI2rmkKmLTqDuNfg3z4w6nFv1fa0OQ6VY4XRiSyXGr0pXrL72no= +VmVrGQo2zRokW/ZuO9bN682I1WfoM5O31Eu4y7eErmS1UbchPX+RbJH8okRw6uaf +VmVrGQo2zRokW/ZuO9bN6wGhkWn/nbtYgnRrfABp3Wx1AshDUYpYVsgVW1JY2KGZFsqdt6fYlLewntOhfc3+yQ== +VmVrGQo2zRokW/ZuO9bN61QRx6pmjScEC6Mg8X6jMiYZxM1o8NnnHqaeAW/oXr0H2CGH5XEtUznATSrKZ2lZEQ== +VmVrGQo2zRokW/ZuO9bN69ytvi1kzpknGdq50rt6Uk+ubFehg+T5L/Wn9jlHITZARQHXl2+Us7ChTWXRkBLidjmBIhghC83F2q0QovPWPAU= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+WmmMTtHxp83MPGxzXMHzRsy/BAZUo3PX2S6H3yS5eG +VmVrGQo2zRokW/ZuO9bN6+lw59pqaZzeIqhj6WMFLKB/zu4YnIm31wuOcKLBoPXS +VmVrGQo2zRokW/ZuO9bN63uBz94NTvSB4ZlhYbDfvddgnVY5f5JKOORH7Tw2EYEWgIaiQrzxYa4sTWMTgESGdzp52DXDe+CM2icfFsZP8BQ= +VmVrGQo2zRokW/ZuO9bN6yiRU09M/IrGNAMi1AT8LbUYEEDidCw2lDVT2RUEf/k9qsU1Wf5Nkpaqk5UUK1L+GYF2qfhogkOZK7QKKEBb1pw= +VmVrGQo2zRokW/ZuO9bN61tUoNETw2Gi1D/Pq4qmNf5zhx/EQVRufI8LJkPSSi0Ih/xPpxJTJaW6aikR8po+Gw== +VmVrGQo2zRokW/ZuO9bN60TADvNmgaySXS1m1WJNuPeBE1HygRWbriOlU7U2Ssu3klRVzuCKqRzIyX8uoMGm2Q== +VmVrGQo2zRokW/ZuO9bN6+NxpNK9XYUauw2abZQW3LIA6UmYzalir4YjKFLEIVPk8tEmxjCF+UBrUqEASEZ7LQ== +VmVrGQo2zRokW/ZuO9bN69V5vRMZphCh9ZgEve1d0/wJCCaWNPSysYTVRsjVkHV1kaWCUu2teK5jcDOFuBzPQg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN62Ywq26Utqu0XTnaZ+HQKopdamM5KbwMRVVnvfRPuEM1 +VmVrGQo2zRokW/ZuO9bN69qq0Yi+mSeaf9labGycI4pPobf7Rk2EV5UV2RidC6jX +VmVrGQo2zRokW/ZuO9bN63BfTcQ4tGBocvZdkD6/kO+b2BWi71GtoR82tq7ZzboaxwEKuBjLI8hR7w2Vz8MwvmfTeIGeWGGODE9Xm7plcChd9CESFtAhCbsVbUZfRATe +VmVrGQo2zRokW/ZuO9bN6+lw59pqaZzeIqhj6WMFLKD5/dQ70v3ovug8BVS13b+a04fDuz4GqTVSudpfL0PZ3Q== +VmVrGQo2zRokW/ZuO9bN6/JsORMmbxegVkakcQYSuOtPUInnHZyjGgz46mtOv7qSxkpfnDglI1Zm24CHtbp9uw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknACUIF0SHq3Wk7UPJPdvLnBytT0bs3KMa3b9ztxfxoAOuFAPEC6llaTPvEl5nkxFo= +VmVrGQo2zRokW/ZuO9bN6yiRU09M/IrGNAMi1AT8LbUYEEDidCw2lDVT2RUEf/k9qsU1Wf5Nkpaqk5UUK1L+GYF2qfhogkOZK7QKKEBb1pw= +VmVrGQo2zRokW/ZuO9bN61tUoNETw2Gi1D/Pq4qmNf5zhx/EQVRufI8LJkPSSi0Ih/xPpxJTJaW6aikR8po+Gw== +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH6/zCWmOuloAo6ldSTiaAbum79T3ZyCN2uTGYeWaRk8ZDw== +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH6996q3aNX+Q/j+AZP8Qvo1SsqECC6pGPNuHt7GBg6OuU7Z1kO5CyxL0yZW1h1ERJGY= +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH69ya1Zc1M+KUYAJsz6AJ8F9YFPnDyZrQbgW0mxsmgLccg== +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH69tjxVx99Q6mXAvsEky2j6E +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH6+u5zuOiB74Y40ABQeHY7iFbajQdIlVw8YH0S9x9fm/0gGxi5N3i0IL4ZOQgrP8+OX40wVL+NzPVrUtKnd1s0s9 +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH693/pJPvxX08JK6/fU1NpPhvWQO/SYGDQsMBTlZKO7NUA== +VmVrGQo2zRokW/ZuO9bN65UApnVIJ5/vPXwprQ1J61CB61rainG+jx/I7BmKhaXNbQAK6argkLKn8aYOSqMyZg== +VmVrGQo2zRokW/ZuO9bN6+NxpNK9XYUauw2abZQW3LIA6UmYzalir4YjKFLEIVPk8tEmxjCF+UBrUqEASEZ7LQ== +VmVrGQo2zRokW/ZuO9bN64RgViN5TsYlk5QzRuGR7Q/OP4DOltXpxjAehZXCVn5t +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +VmVrGQo2zRokW/ZuO9bN69wvhLovBGRbT42dQt1px1AK/igYwlUjVrS2AJALzGdr +VmVrGQo2zRokW/ZuO9bN6xfwiyo+r5sBtLLY/yC2JQuHAqyNIUWdS8mL+/TK0tDkDYK6sWUkPNuSjWFfywjVYg== +VmVrGQo2zRokW/ZuO9bN6+lw59pqaZzeIqhj6WMFLKB/zu4YnIm31wuOcKLBoPXS +VmVrGQo2zRokW/ZuO9bN63uBz94NTvSB4ZlhYbDfvddgnVY5f5JKOORH7Tw2EYEWfecK1ZYcY5aRkTVMuXTf4H0ornquPXZt6sNbcV9pvdE= +VmVrGQo2zRokW/ZuO9bN69fMKIOt3Fz+VdjDCrbU0LCl+bFI5Faw9xdIi1QHkSiy +VmVrGQo2zRokW/ZuO9bN6yiRU09M/IrGNAMi1AT8LbUYEEDidCw2lDVT2RUEf/k9qsU1Wf5Nkpaqk5UUK1L+GYF2qfhogkOZK7QKKEBb1pw= +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLwPXg3jnnr3ZgO5MqTOchYl +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLymwotzxdwFuDwNOf7s1SnK +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLxqHw0Wm+3Oz89atXNvpO+3MXp3Qtmegk6NHmWrFwnYkOvDHfuz5NaWnAG6esuIjESLWMw8mumKWR+OGLFQatxn +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLyUNjqRPrStY0n7cNwmxO8c2/xYLBixqHfcg+3JdMiYfw== +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLx3KKJ5e9ptEgMTxJyuDzDUvQprgIXqnEOJyY8Wjzx/7g== +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLzg8EOHacHSOV2zn2Ehx3MZFJJGKychYjDMMiIIfaMNUg== +VmVrGQo2zRokW/ZuO9bN6zSaI2hF+nruhyzPFkleUq8kEqJLrAmChIOBKZkxcqwkGxR20IThZlGA1Dw2fFbmvg== +VmVrGQo2zRokW/ZuO9bN61yX1Dmgg5/3/nXVEduWvGRjAxUjUGE7S9oN2liQJclq +1u+XjG/2+GSQRv6EzCaWRQ== +M8WJLh8/G3HKYOcF0o/j+ZydjE/y4s9kCBsZH9Ydiys= +wlLHv6kT3Q/RmtMBN4nDAR8Bo0ERF5FTL0peq/6tsQxyyboaug1HqBM4U72iapnH +VmVrGQo2zRokW/ZuO9bN68F9oo0dUBAgf3zfH+UjZ2vBnQt9FOIIe/Mpc+VcY0hPyb7Y4hQhLdZIwPaG+7+qSw== +VmVrGQo2zRokW/ZuO9bN6wO+YLo8joUjMSrMMYsU2gc= +VmVrGQo2zRokW/ZuO9bN6wxqWDjyux4j5kWMbM/YT04A7rFtRY3ZvKwNmal8z607JZ0N1ek63JQAbp1/rMmDdg== +VmVrGQo2zRokW/ZuO9bN66+oTs/ylSo2IwYwivOcQ5QBeGO3/qhUXmuWRmyY22oicnIskjhngFIDfDK5fq2/pg== +VmVrGQo2zRokW/ZuO9bN6wPyViMJrLgq6QE06AJQ/mAxMxhT6z5+FEPokBQMNnem +VmVrGQo2zRokW/ZuO9bN61Ayy1rssK205MlM60bu6RKTG0Yfp2Lha8/dhaOOY7W18jbXMCbohsZ+i3OCAo0UDg== +VmVrGQo2zRokW/ZuO9bN63oDITMAygWGYYDyry55YDO/Rs3iFoOYCQPRB0Vo6QHG +VmVrGQo2zRokW/ZuO9bN65wPRQBNUUIF+K895b5fOrPxf0bXsfEkNdHCrolD4R0g +VmVrGQo2zRokW/ZuO9bN654Enj8yhWFV20SJ83piZWE= +VmVrGQo2zRokW/ZuO9bN63D/Hc2z6pQlInT4BP7h+Iw= +VmVrGQo2zRokW/ZuO9bN6yUmZOZSHWWMW4cj+zuxv+kqaCT5BWcHH/D3f8p49LGM +zvzyAjCZxViDIIg5wIBcfv3Z+YCuz/oELxYEQudPAfA= +/jVx2Sfk8Nv4tQDKNVrh5YQDDOyw39n94XpeN+Fc18Kb/ocx6XA91E4DLjnGHUVOp/hG38KiBHnsJF7rC+FrGh3/Y7Yjh+e73kBNop061ts4ALDF136iqb3NjiK6eB/P +wlLHv6kT3Q/RmtMBN4nDARhCtgHyXaIoGDNPdzxanK1C9qqxbuBKdK1MCBW9e3yd +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN65v9BdTfHDX5h5JoIA/XwnnexMAistxqcEkO+FfI33OD8/CZEhqElXeJdGaAFQ+8dlBsrteF3JVNER2SAEya4ro= +VmVrGQo2zRokW/ZuO9bN60wy8tZH9NZmPUZU3ymx7Rp230YC0uhO/IEmXkv95OC+kYR59ne0kzYbTAnbjiDQU/tfbdzA12veXVBzv74WMwI= +VmVrGQo2zRokW/ZuO9bN6w1QwjDxqY23oBLfy+yRgv/pNEUynCImsZYJyFdRKiY3f9n/tdlMbWaxhorqdeT5IvbcJCzD8QfNSTYnlePbg2dOcteS5jjxFSQ3VLx5bqO0 +VmVrGQo2zRokW/ZuO9bN6xDmLuIA9/MQHFSh5NeXQIQ+yxAa1KwUsFahRjuiZ8Mht4CYtcVKfJe+264KUvItRwPdoTCmQExpQVyY7LXmw64= +VmVrGQo2zRokW/ZuO9bN6zj/WJ/j3Gpxi/RO5HrCvbVpMoEipT3erObxJsWCoytEOGK/hbak+7Shcx1wGKtjvE9kvRNNO0FTZ9ViojU52Bs= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkkdh44SEE/n2JvhNVaLLGFkAT0r5NcHasfhHQIuEbaS/w== +VmVrGQo2zRokW/ZuO9bN6yC3ZNj/jGmx0c5FxYA8mMYFovl3/pDr9xKbkD9+awkq +VmVrGQo2zRokW/ZuO9bN6y282QljYt9u/aACctfJxlfOZSOWj+ka4WVOhE7+gYPHIAVL1wqdRYXpc242KdGWJ/V8BbFe5Mk6POcVdzCcc/E= +VmVrGQo2zRokW/ZuO9bN6zj/WJ/j3Gpxi/RO5HrCvbUT/1fI7DpxplEbSq3o7wy5 +VmVrGQo2zRokW/ZuO9bN6yC3ZNj/jGmx0c5FxYA8mMYFovl3/pDr9xKbkD9+awkq +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN6/VW4n84N+xmKyiHoJANXzc= +l2FJPs4YkAmmok1ulDRuSA== +wlLHv6kT3Q/RmtMBN4nDAU4MoUw37WOeYAqXUx8nKXV08D/vyeLYo268Q6TQAkcy +VmVrGQo2zRokW/ZuO9bN68rwZzz6qT+BRwAM0w/VgNam6voCMhL/LyfmhkE+4WOq +1u+XjG/2+GSQRv6EzCaWRQ== +YabJCT93a3/IohXaIkr3oSIBZqGhFAEcGH7Ca/SaLLaG+fOp90QxSi85tpj3adUdyNOydRy2vjmM402XZpsX3+nkFnHf2XtWx5xSFzCScjM= +CAhn8dRUItEbEErp4w+lX6BuaW4lR2EIOU/Z+PBe2bh+we0izGB82w9NP/FQvC5X +1u+XjG/2+GSQRv6EzCaWRQ== +oSOaxL22S+/P+2LLtATJSAaArdQ7fQZ1yguM5QAidIUD9pZGaNnuSKZ/6l+iYdvR +Z8UsPk1Q7HtwjRd4g01ryw== +hpr9H7QXrORqNT7P49jOY2MKXv3knmxSuBVQsYMFcyo= +YThduOXrtrzATLnwayQWnd8n5YD62KOD6YsiYJw+RAk= +7JQQDTi0T2idhFjao4jEpWRxFagB+y/O50vW7a0bZYhQWe9QbLzaMInaqQiRp7EGliGMBr4A0vhy+GCOuLQR2g== +7JQQDTi0T2idhFjao4jEpVM5kDnYAXzeJH6AUYrQZAHIQ4Zi/CV9839Nkf/Sc8cE +h2Amc56dmaRo3inGZxN9N4t4UB6ZIYdgw3I8UrLsiK9g6KbQ85/a9qF4mH08k4ge +rErGIG0jHuEUhPzM/Cvn7eDAiXr3299blfSBm1Bfub0= +B3iNup9pdvLGx6CAo6KC6KE/v+ibyUyH8Yud04Lz810= +q4nc/jwATOMUyfSjLibfEenY5RFzfDAlBFkea5rX8Ic= +nhEO6jGYTLTxNd7tfL8haGb6dIE98SqR0mAOJokWbaA= +2gqFvhn8R1IfPGeV2rMswbHUHFXVUzGBLmhsu02wzqE= +1u+XjG/2+GSQRv6EzCaWRQ== +C3JSV38/w2nvM3I7TZ5+4epkOMkaWYvoOErk6ygROHs= +to+v3MAswOY2NqesdSm5E+gSyJU0y7Xs7zqrUefMUuCWwwmO2rJrPN7egFiRQVKU +vJtKpUSg5Ukl4nJbF6QjQJ7EE1taDQMnZThgUYKjS1A= +m64JmVQ8ZxnJzJGTbbSLFgYYjwLvqe5k1J852jWCJ20= +GDxnwLueKeYNT0/dIp4TaMZIrCOXt9Fadk7QEvmi3LY= +iZaIsa48RXfE+uf/yF/rQBFFBTbLDCVgmOpzzd9fNSoU5yqcis6eMa2AhGtSBk42 +8+kEPw2kbWRhVZANSTiFn91S4x0IfqnYB21Go0MrYHk= +ed2GXKkXmwU2EQk+6MUOb5wVZ0JLOm9I7uChKGZVytg= +ojcUvW+Z2ehEJ6yMJpmY+4qFMPDKWboTeo1Y4LE86Gs= +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +YSlit6erKXylrjSZnvyW8KJQ5WBrDhjuDH3szUt3rF4TtLL3u+2vhmfGxql7dElVlZ+FltHCrJLtURHR876Z+5QJwoIiDk96EuxXOkKMTdk= +DaK4moDemtv24P20mOaUzQ== +YSlit6erKXylrjSZnvyW8F3ElaLeR/Rlppv/IAE6pDs= +9zgnmC3+5N9XK5/NRaR4SOWz2y/YEwWVWSZltrwvNrSfE3PSPwQMDu0BXDhBm14C +1V2v8QerKOmubvSxgB4eTCxJBqBlZI1ptPijcUHCDz5sYoASyVMieGEeAAQXWrck +1D18KWr2hdVsBcdZx1OaPfHgcwFUa7nBQO7bfH5tm74XTDXVMzBwLoLCrGgXhe0X +fqq/Y0gPbKQeEXzqLGS0q/J+017f+uH388qCfSeGlh8= +o6QhOIN2Sc4SHELnst17uYigovIGLGw9Sk8MSoNStgLGPEqp/JNtpdjWEUUK4pQG +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +lkFGPJ6keZ1Jd10HSdN0i5ZJfFnQV/nGEGCTaGt06s58zYz056g6djTo00sC9scb +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIlhrCEedAYfpQnUktuEYoFlw= +vyYQukiyWticSXOwUY8MiznZqoLnnCC/RUpXDOGxEsg= +ho/Q+jrWDtBeg9J9ZTnKi7Z8oMuCLRtnjC35Cw+4aVDyKydByU+COt6lumOoVZyj +AWid6G8ypXqQe9XxPPYED6EG/d+wAZpLmHn10ofRnJm2zuVMuvU9qhZPIJsbg007g9BmQxGLRAPjJyKkcrVzW+hTwBZcS8FDXPlLD0f/q/4= +Z8UsPk1Q7HtwjRd4g01ryw== +avTFp0LCGrKbHzgXWCq/pYahaBNfx164yVsSAVTJDN4lWK0zoj6Eh3CajVgk0hPL +TzinVBSdupkFZuXDgmwYfiSs5GTHKq13dTr2kdVzFPy0Jh42rAak6FUbANl5whZO +W7OoNBP/IWbYzUOvpg/slY2NVhp515LKHRWJIoHB+V62OipmiB60YwRsLUoQ2Yhe +ojv9MyHa70Aio7SDrWcjwQ0VSHrTkMcwddE36Wz7lCz16K6ytw4GpaCto2OYzTO/ +IfOrW6dR51K0HWsQz64KwMZLSub5xEzT/cQoJpZiUus= +MzOj4ZiBOJDNVP8vi/lFQIcd3YW2BaDb4kSPJzNVOdlx1Fa3HoeBVOYpiRWE7XX1 +PRG/YRzXVD8iVR3bzEcUno4Z8XHjrzDgKJNn9oJou7lkrRw3h7on47id8HDtKTRy +j7S8zldDFIkYlZuEh7uh1S+jR/V5xbpfKOdCgELD/V8= +yqOVJCkAGOhqcQYBOo7yqReCj9ixiaIB5tdlZ21DAUB9HlVyjl18y9Mfwf2BKrxO1mdZmBSOas0Z9YsNQ75XTg== +c0jePRxtTVZYop75Q5JCFsMjGu62TZDSljeQjMtCShNsz0Y9SFIv08dPVdTjI95b +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmW7Akukb+j0lqloIv/j7uSM4oazJqFVNfgaZWfRnkezm/n/qRAFeZXm0pd4Y650TvY= +NYuttixIfaRvF0QtYZhNf933obSmTd0Sfv1RLv5CWew= +z7RR81hY6MAA2voH8Z2gcSMl4BzicmRRwwcx6alhVes= +jg/rCd1ltkosbKN5ZG2fjyfZDh9bj6LoXpZWnO+Jm3I= +m5EI3+BXxkQEYKYaxORD+qnAwk1i08GBypJtuz97N8s= +1V2v8QerKOmubvSxgB4eTFLJVKokCn/n7cBzRCyjPmemCDNpavHplLKoYC5ZrVB11XPhYbqw8m0ksUK1fSNNJw== +k1ktu/l6HFULD7Vyr8Cv0HMlLvOl3L1DvBU4YpKX0wAfHx0c0egKY2YtT74Sqa68k3g5TvkiAzu1NRNlkcoBZw== +rtgmVFvoY2d3wTFk50ruC6zmYmoALMFLQOc+xc+vXmJzN60uRcu12WAXC28NrZ2x +wlLHv6kT3Q/RmtMBN4nDAesM/ZGOfx7RLjx/t+u4GQE= +VmVrGQo2zRokW/ZuO9bN692Dxe6/pJb49NcEGwdYFHp1ykWA3uXz8Dyrp2BHgNzY +VmVrGQo2zRokW/ZuO9bN69YWkPGheqxTevWr4koVetNO2nXtSSYd+VohbeMtpTJM +VmVrGQo2zRokW/ZuO9bN63sbeay11c+Azj5VEIm5hpFeadOrAd8WGDCLEKR35dm2edAnPxHas9j394knOwJ8NA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69nNx2wsmuj5rj8dMZU/ic5BLawNF/f6lthvaeVh/Ls+ +VmVrGQo2zRokW/ZuO9bN69Fm+oKgY4oZ96N2lUN0xCYIR65p/6S+rx79lvSqg2I47knqkaZTvrsnLR+tEwCTnA== +VmVrGQo2zRokW/ZuO9bN6+qkEKPDxNwYFnkXN8TuQuYkotp65nYQ0fa3zEFof2zYnFi/LEybvPyjh48Z1idmyjl0NtdcsjLBkBJ13a8Xf98= +VmVrGQo2zRokW/ZuO9bN6wm11lyKsRHgTY7hsLdIWagcAA4Fqj10A0nlHnCL31hRaLimdtsSr1rF3vfwAkIE9UNe1Oq6AC93jtsLEBybVC4= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69fMYEokQlwk/9w9fdzOny6b4sFM5OW1KAT1oeECeG7J +VmVrGQo2zRokW/ZuO9bN6x9kSHAfsYQbQ95eUk47zCSU++ESIivk4zGTm21DHKkHEqyRv+yZpHFD0LXHT4m58A== +VmVrGQo2zRokW/ZuO9bN68s9bgaf6E2UUkkykIzwZHpz3P+5wRaqeeTuq1ofqFvFKnCBTa5TbqmgBM/RmZJPNAUu69T5cyQoQbj9yc2u8n0= +VmVrGQo2zRokW/ZuO9bN65llTn0pe1t030hd6gBN+zBd3IDBl9SSuthuY26B8/Pu +VmVrGQo2zRokW/ZuO9bN69ytvi1kzpknGdq50rt6Uk+ubFehg+T5L/Wn9jlHITZARQHXl2+Us7ChTWXRkBLidjmBIhghC83F2q0QovPWPAU= +VmVrGQo2zRokW/ZuO9bN62HF7Hu1aUGvgMj3NxvOaP7lJ1t3oULkhfzDpf5obr4i +VmVrGQo2zRokW/ZuO9bN6yfJ0tQlmRWb/s+hHO4yegbY9OZJu7Pz1YpWO35g7fWo +VmVrGQo2zRokW/ZuO9bN63coKIwm0os5grqf/FNTOQs15MAmmbu+Svk4r/hr3Mwt +VmVrGQo2zRokW/ZuO9bN633IXjOdT/W54PqLLNSn1Vg= +VmVrGQo2zRokW/ZuO9bN60VxqaV005HoUzQg7og2uQzzkGvTFfQImHgCDcOTreTvBj0gdrkAAkZ1tFN5kmALjU7kVG7jii3XUUaGJzXU1Dm0brYcNqsnFgqAbRd+hLig734vVILUrNX+DvQbOCpPeCtyQ8YEDj2QHDc0gf1laMU= +VmVrGQo2zRokW/ZuO9bN6/JsORMmbxegVkakcQYSuOtPUInnHZyjGgz46mtOv7qSxkpfnDglI1Zm24CHtbp9uw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknACUIF0SHq3Wk7UPJPdvLnBytT0bs3KMa3b9ztxfxoAOuFAPEC6llaTPvEl5nkxFo= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH13yvcZl75di1EfS2yW7gx+JTVNJQF0HOMQVAegRfGv1eA== +VmVrGQo2zRokW/ZuO9bN64OaX2n92Ly7KbozWq7a7t04nFfOkcL8NsC84xQi78iQ +VmVrGQo2zRokW/ZuO9bN6+a8J14HUH3L5V5KblyMysRYdesKR+fGd24F6C9j1l7wwo/p5lCfK41p1tvTNJzD/Q== +VmVrGQo2zRokW/ZuO9bN6yZNwtKaS0KLSStVEVMnEqjMcCKBADGrWUZZOFSQE2G8 +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN661ybCBlxEvwailf4eTD9bqZ+RlmOwSQwtD/IjfE4TEXKm1zXESIuOIjor2vta6yx+sUjFgCbNTyLF8DtvW/9/Y= +VmVrGQo2zRokW/ZuO9bN61EVRyU6dsJr2EqyMVPbztJnTc0M4smwvC4e37Qh7Ms8 +VmVrGQo2zRokW/ZuO9bN67E2Rg2DTyty5YPAV4Joqwwqw9NcbRGp5GK4uyK1s8dkEGH9RxcsHHlq/24VKap3dQ== +VmVrGQo2zRokW/ZuO9bN6xRDOkMGch8OU8UdKKuA98T78S/RtIEO+vPGquNUPYP8j7Hqi5x7cOT1rq7lCPTbFRrq+ZOiuhVZC6d2uE6b5Y4RMkjy5kdxgYDR8lyh08y6 +VmVrGQo2zRokW/ZuO9bN6x+CQDkpZHTSzVJB0vr6/owqo2rDPdhhBctC47HwoQJI +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH12i/zuDq/sxwawfAUxLzsmyvkFQXYvSK+lgmw3Dfruf8oUMKRDLCW0YoRSFNGJnVRA= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmJ6/ykoozm7YfC70113tbsRK8vQxRXP6m1Lk5Pq4lJ71yrM2OyBQ2a20YT6ATnjGs= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehp008Hpwo+KIS41Bq+plw28g== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpCUt6sS2ZM7/SgZvw2/D7KN2T4F8mibCUg/Vu5XXI0q8= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehp369EMxzMKwrpZ1Fsc6Rzihs0R+CDz1UtQsB7s3e2X1Y= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH12q9ECvLOWcUe9cfTPHyuxucjjrMsKMpYA6OLWCOLB2Qw== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +VmVrGQo2zRokW/ZuO9bN68uqYuUF7HS6ZsAigA5dAhhdPk0G+p/b9PgaumDuRt89 +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN661ybCBlxEvwailf4eTD9bpaS2w5sSDIfS2hUtvDW/ZPwVwLAcDR+Kc98dntjxh559OJdKONTmZolY5AjSf4KMU= +VmVrGQo2zRokW/ZuO9bN61EVRyU6dsJr2EqyMVPbztJnTc0M4smwvC4e37Qh7Ms8 +VmVrGQo2zRokW/ZuO9bN67E2Rg2DTyty5YPAV4Joqwwqw9NcbRGp5GK4uyK1s8dkfYflZHE0nkkYN92Iz4DvtrB7pbXI8nJ1B+v8kBcTL9k= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH10h4UbnbgTDNlKt0H8bnNKHH19OWWvR1gRk3zXeKMJ+R8jqCu7/rIb/rNBOG+zFbIM= +VmVrGQo2zRokW/ZuO9bN6wRsqCOcYvLI1Bp/40+s8jeuGD+dib11yOYzha+J15yw +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH11DboSZV8b8syS3y20nGfXmJMrA3OSI5dV3PIVmDHiETg== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +VmVrGQo2zRokW/ZuO9bN66uuxBZGV0E5w7uGfD+hzU4AmPi0rGaOz7BvwaEoiXgK +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN661ybCBlxEvwailf4eTD9brMUFxnvPbybqGRmv07gv34NbyJ44u8fMbC6Sq1JkAEaNjiyTMCoEHy6fKKN/tEOOE= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH112Cwgr1gup7jex1yHReVWxiyvsKVsK3znbe1ecdgFhbIZqxwirWCHagb7ADC5VmGM= +VmVrGQo2zRokW/ZuO9bN6yc8qBfgAsioQiqSI3s7WIFM5rHpAQSYfkhRNfXBk2JBPvIvjRzS10i3GMc0J2bxTrSvtMX11tg5EOzI+VjAy9E= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH12W5piSXwXcBTxZsgj+fR5PRj+yKlsuMRhvQaQszRXFiA== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +VmVrGQo2zRokW/ZuO9bN69jndazZ1JZSETYUFSRjpTYuSd0gU4R+ef5NLXuDtL3Q +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN661ybCBlxEvwailf4eTD9bqkLLlR3IpZgSDiX5ChzlR6NnjTJlDwKlsxY/AdyE1faRiMQGjhE0oX8Ie0uAI0rvI= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH112Cwgr1gup7jex1yHReVWxP38Ud5r+2JgPLAafH7N4i6SpRZsaYMAGNiTp9HaQsd4= +VmVrGQo2zRokW/ZuO9bN6wRsqCOcYvLI1Bp/40+s8jeuGD+dib11yOYzha+J15yw +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH13ETFhTDjpNg4UjDJ/ZX/5m5v/P88Ser/GIltB+CyiPFA== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +SS3eUI6O4Am57+zsCenaFkHmh+pwM+FJ/cut0UtoxJ0= +zvzyAjCZxViDIIg5wIBcfv3Z+YCuz/oELxYEQudPAfA= +M8WJLh8/G3HKYOcF0o/j+UKQix7E5QZpJV0Tfc/XRq/AKN5kkYE+IMwFcAyVsz2M5wHJAN89+nJ1b7GSY68O6925BBXUxFwqdflokenOUZU= +wlLHv6kT3Q/RmtMBN4nDAZf8v50ttctcXvJzAQ4D3y4VerbRFUVUK+NjcpNFPsa+ +VmVrGQo2zRokW/ZuO9bN60s8cEnb0+qXY0vQh1/7elp8TJKn/o/Cr8ab/L4KpzVyx+zZfHPJlblcJVM2evPA8Q== +VmVrGQo2zRokW/ZuO9bN62wFNZfsNF2gO4y5vOS6dGtOhTIohM0/9iaTTdt4VAkwr3U4JR6xB2mGWrp5KaqxgOX+iIeHDUU+B09yhkuBbeQ= +VmVrGQo2zRokW/ZuO9bN6zJHGvDJmOJJYHHdXzByxKsyeKpya/o0dns6NEjvZ9FhBqfG56Ge8FxUmH1UPM8WYg== +VmVrGQo2zRokW/ZuO9bN641hlBSX1Vyx3lcgSR68G0pIVXm8+RXV4rlYHPfD1CE2Ifl6RiQnkn3hT82X+tXXDQim3jnyNRBLGfZDXhejLG8= +VmVrGQo2zRokW/ZuO9bN6zj/WJ/j3Gpxi/RO5HrCvbVpMoEipT3erObxJsWCoytE96eId0MgNxxUF/+ybzG/f+gvdjS4IHHuGSQJeLB3dqc= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkkdh44SEE/n2JvhNVaLLGFkAT0r5NcHasfhHQIuEbaS/w== +VmVrGQo2zRokW/ZuO9bN67WLIC8ccIPl/iWyXLvwqc6z89/fim1Zup64yhd9bw8m +l2FJPs4YkAmmok1ulDRuSA== +wlLHv6kT3Q/RmtMBN4nDATXcKKq3YicYjT+ilqwnePASEk6UJ4kb2FJ4sCzHQMkb +VmVrGQo2zRokW/ZuO9bN68rwZzz6qT+BRwAM0w/VgNam6voCMhL/LyfmhkE+4WOq +YabJCT93a3/IohXaIkr3oSIBZqGhFAEcGH7Ca/SaLLaG+fOp90QxSi85tpj3adUdyNOydRy2vjmM402XZpsX3+nkFnHf2XtWx5xSFzCScjM= +CAhn8dRUItEbEErp4w+lX6BuaW4lR2EIOU/Z+PBe2bh+we0izGB82w9NP/FQvC5X +1u+XjG/2+GSQRv6EzCaWRQ== +VEyEXJTNTVvV1TGW5V2yEMzjBDxvV9j1MdPjo5gCe0Q= +Z8UsPk1Q7HtwjRd4g01ryw== +dar6N0tUAb9rU8/aX1b3tI5lWSbcXQgKxiMzJeLasCmdJgT+u3YcH7fkhDzLb+1X +9BuKX7c+cBUFGH/xEQqKwDJwv6pCDrQbGrc/aEaAxEizXN1tDiWRXfYhvFOYGiCD +3AcPLYoMMn4rxWOUMA1wJdOmdYprhThTGxGyDnWq840= +Z8UsPk1Q7HtwjRd4g01ryw== +90/F6C3nHF3UrQ/Er80CwwMY0Oi3JHbGAYQNb/w2V49c1F+wL9hoGKNjAjwN6pWeeHbxvhz6JqsdWpYj5f3cjKeHXg+8Lul7hoesFngMXfE= +g9lRvj200ApJT2I/NDdmaNS2bs03/Yy1N4LL8JCPrSEGtYUIbFGqz+9Nvsx/WW8DAPlJEAOAh1mN1Q2H5srR9aN9BcyfgbXU677udPpvmJs= +AC/CEOsuoGTyXbh39EUlAxckdwNHSoPmK9UbpHaeRy5+9wXscQv0OinnRdatf4o5vPdsr8lUrlC4beVwlerQ34wo2xApt+NOGbWFjuh6p10OnBiFzXIsBBGS2CKsc8ur +HBkACpLXsHV9FQoDE2PMYwSntxSga34Rul8QLEjV96M= +wlLHv6kT3Q/RmtMBN4nDAeWjc9eUr6obACCnQsg7vueRPByWE2DayjQ0xFllv1tW +VmVrGQo2zRokW/ZuO9bN65VlXQyBOMicxNyTkfSbDVVHhvBq22DM+S7/byvVxhpE +VmVrGQo2zRokW/ZuO9bN6yititPhxMgpb/nuYcU2bQxlVrgi32/YGL7BfDcYBS6ZrhF8DKeSq4/xyWh1l402NQ== +VmVrGQo2zRokW/ZuO9bN60VWeNWzP5MtK6lHtmFWfiOlcDBQw592yq0YxYnQPyb2cY8haKBhRx3ogIkozyLzGA== +VmVrGQo2zRokW/ZuO9bN61P2OVHnBm22rHl5VcwespdSVFvlh6jWmhNQGuWaiPCn +VmVrGQo2zRokW/ZuO9bN6/C3rj8+QHPPw24V/a4Ay7A= +VmVrGQo2zRokW/ZuO9bN61MqA9WsfKOg/tExmVwPpl7QnDnpNlS+CkeEkfTa9kINLQXgJYeYvwm13Q5goIzeLg== +VmVrGQo2zRokW/ZuO9bN632LGHZY9uic2w89Yj9RNWLrcjnK57CYRX2bXewhAHx7FVtzwI0ZOgY/T/V/6qaQlw== +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjakEuF8eNiysMFwArESQEwY +VmVrGQo2zRokW/ZuO9bN6yg8DfkEZba0oHLbAQabDs+4/JxGHZrKdgs6bMyBPLRMxwPtWLarWOkPgDMMkW2/RUdxOLVZk3+IQpAGkMdAX/s= +VmVrGQo2zRokW/ZuO9bN62dh+FzI+VGLu7ulc+ItFAbFXemabBEE/LbP+46UYm60 +1u+XjG/2+GSQRv6EzCaWRQ== +JEnsSsP7ogTJg1OXQ2YdoeqaOYYcnxFN3j3ktV+Kpps= +Z8UsPk1Q7HtwjRd4g01ryw== +8xWkPvEiREBWC3F3wdDkfHfKi35MXWHSVRhGdhf5R5T9XB9HNeEomkbAWdDKXpBP +Z8UsPk1Q7HtwjRd4g01ryw== +tsXNkoeF46BtVmFZ50qzKkC9C9rJjI8DYYqtZ5sTRNk= +b4OJVZe8QyIpjuTpKXDL9A== +cjZRE5RYY9TGg8THaNbKeimi7CKPJrP0YrozyGJJ6po8VG8Og36dzm29TqPtykmq3ShDIRxTK5G7NnirQ35JIwA7rlx3Yk1mP0YkpIDY3Ec= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN60gMmPI+qllGkwxOA9qbMmMB1m4JoBAOFYQ6YXkMo1VXquwSJ3IOKilGLS5oWPYO5OYeeloJXPkrrYqwK73A4D8= +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN64/n5EL8V7CIqhp9FPzHEuQ= +W3A5Dt+8AxWSKsTYeXZoL1vLKDueV7P0Ut7HIASfZlp4D0IrBt2bwp8YHjJXXxXp +W3A5Dt+8AxWSKsTYeXZoLzB4TUiScS3D8yuLKBmm3AwWtYcxV6f46K0T0Egvi5WKkEtlHJveHdb2egjl/+1LDg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkknASfUzt1VKHSynjO/3GCHs8NYZ0lBfA3YkEhN9KGBNrviqTxIwFkI8b8tfQOhuu8e1qX/wTQkIHS4gFt6ErXtomVQ+AVI6XYbacH6pVLAEQ== +3ihEq+EcGY3EkKhgSscgbxPe6xF3X8bYDWyX0qt6BtkDpzyqs5cxV+fqGAxgw4n0F0ocWo63+pTEB2B02sSu997O+t/1VUp8dx53F4oaaq8= +1S1xjYS0s+Ph1PyvECx3QMdMFDZ5B3+BEK8kyCfI1RM= +VmVrGQo2zRokW/ZuO9bN67pnuem2Cn+Z/63FRYUcFkLoOrSR0T8WP6DifYG5JOXMPR4AFoHBPiKvg8hnpKw+PCDgxml+g92wfGPN8zZ2WJk= +VmVrGQo2zRokW/ZuO9bN64Ga+02XCL+A28NNybmlSnI= +VmVrGQo2zRokW/ZuO9bN65a8NYx2zqK2mFD4wjj1HmQ= +VmVrGQo2zRokW/ZuO9bN61jw9zMkfm0nvQi9tveiWV0= +VmVrGQo2zRokW/ZuO9bN6zYzNJFTIYiz/a08bSUNc5M= +VmVrGQo2zRokW/ZuO9bN6ztX+cJqy4yqv63JnfA/mqI= +VmVrGQo2zRokW/ZuO9bN60jLKcJEMj36X6NZvvwxF/XvqrmJF3W3RArdDHrkgZZ6 +VmVrGQo2zRokW/ZuO9bN6zskyREk1B9SpIIVpfE36zU= +VmVrGQo2zRokW/ZuO9bN63leNhHVk6qBYEqAElCrxOXVPHIpJ8U4k7mou4tbIR/7 +VmVrGQo2zRokW/ZuO9bN68tloVdndHDn6gJZRayJG+k= +VmVrGQo2zRokW/ZuO9bN617eR9WvYqaE7y/ppCzNL1570CCRJc668VdVufs+tRvC +VmVrGQo2zRokW/ZuO9bN6x59qEj9owkguOGj8QrHQSLi1Q/1C75rARTXu4TGt8DP +VmVrGQo2zRokW/ZuO9bN60Bq4PDCei1vxH18OtSGKCg= +VmVrGQo2zRokW/ZuO9bN67biU+BUR7/d7BkQj4RgpT8E7BfCa3N4KcxGo7ivT2Hg +YH231WTDnzQG3bFilBiqIg== +p9oVsBgcyiBMjDb8CcPOqP2sLQ/zn5Dzo5lCtrgxqTQ= +lOh2GtzHjjMM8E9J40AuOcPN2eZCahfvScF49QHmlNXYE5PQWq+WYPigt3dG9DzvOjcs8WVNkHQ6P1pZps2Ehg== +UbJuac0dxLiN+5ocS3w2vTpUyzdtCQ6EQJg8V3275Z+ztU2/bGujfta5s3IG3yXD +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= diff --git a/class/logsModel/panelModel.py b/class/logsModel/panelModel.py index 5af8d160..4f9c9986 100644 --- a/class/logsModel/panelModel.py +++ b/class/logsModel/panelModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: cjxin +# Author: cjxin #------------------------------------------------------------------- #------------------------------ @@ -198,7 +198,7 @@ class main(logsBase): try: if self.find_line_str(_line,search): #根据用户名查找 - if username and not re.search('-\s+({})\s+\('.format(username),_line): + if username and not re.search(r'-\s+({})\s+\('.format(username),_line): continue find_idx += 1 diff --git a/class/logsModel/siteModel.py b/class/logsModel/siteModel.py index cb7221a5..7013e17f 100644 --- a/class/logsModel/siteModel.py +++ b/class/logsModel/siteModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: cjxin +# Author: cjxin #------------------------------------------------------------------- #------------------------------ diff --git a/class/monitor.py b/class/monitor.py index 84c1c484..fb7e1117 100644 --- a/class/monitor.py +++ b/class/monitor.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: wzjie +# | Author: wzjie # +------------------------------------------------------------------- import os diff --git a/class/monitorModel/process_managementModel.py b/class/monitorModel/process_managementModel.py index 53a243c0..8cc08f5b 100644 --- a/class/monitorModel/process_managementModel.py +++ b/class/monitorModel/process_managementModel.py @@ -1,556 +1,556 @@ -import json -import os -import time -import traceback -import re -import psutil - -import public -from monitorModel.base import monitorBase -from pluginAuth import Plugin - - -class main(monitorBase): - setupPath = '/www/server' - __panel_path = '/www/server/panel/class/monitorModel' - __data_path = os.path.join(__panel_path, 'data') - cpu_old_path = os.path.join(__data_path, 'cpu_old.json') - disk_read_old_path = os.path.join(__data_path, 'disk_read_old.json') - disk_write_old_path = os.path.join(__data_path, 'disk_write_old.json') - old_net_path = os.path.join(__data_path, 'network_old.json') - old_disk_path = os.path.join(__data_path, 'disk_old.json') - old_site_path = os.path.join(__data_path, 'site_old.json') - nethogs_out = os.path.join(__data_path, 'process_flow.log') - - disk_write_new_info = {} - disk_write_old_info = {} - disk_read_new_info = {} - disk_read_old_info = {} - cpu_new_info = {} - old_disk_info = {} - new_disk_info = {} - cpu_old_info = {} - log_path = { - "mongod": "/www/server/mongodb/log/config.log", - "nginx": "/www/wwwlogs/nginx_error.log", - "httpd": "/www/wwwlogs/nginx_error.log", - "mysqld": "/www/server/data/mysql-slow.log", - } - - pids = None - __cpu_time = None - panel_pid = None - task_pid = None - processPs = { - 'bioset': '用于处理块设备上的I/O请求的进程', - 'BT-MonitorAgent': '面板程序的进程', - 'rngd': '一个熵守护的进程', - 'master': '用于管理和协调子进程的活动的进程', - 'irqbalance': '一个IRQ平衡守护的进程', - 'rhsmcertd': '主要用于管理Red Hat订阅证书,并维护系统的订阅状态的进程', - 'auditd': '是Linux审计系统中用户空间的一个组的进程', - 'chronyd': '调整内核中运行的系统时钟和时钟服务器同步的进程', - 'qmgr': 'PBS管理器的进程', - 'oneavd': '面板微步木马检测的进程', - 'postgres': 'PostgreSQL数据库的进程', - 'grep': '一个命令行工具的进程', - 'lsof': '一个命令行工具的进程', - 'containerd-shim-runc-v2': 'Docker容器的一个组件的进程', - 'pickup': '用于监听Unix域套接字的进程', - 'cleanup': '邮件传输代理(MTA)中的一个组件的进程', - 'trivial-rewrite': '邮件传输代理(MTA)中的一个组件的进程', - 'containerd': 'docker依赖服务的进程', - 'redis-server': 'redis服务的进程', - 'rcu_sched': 'linux系统rcu机制服务的进程', - 'jsvc': '面板tomcat服务的进程', - 'oneav': '面板微步木马检测的进程', - 'mysqld': 'MySQL服务的进程', - 'php-fpm': 'PHP的子进程', - 'php-cgi': 'PHP-CGI的进程', - 'nginx': 'Nginx服务的进程', - 'httpd': 'Apache服务的进程', - 'sshd': 'SSH服务的进程', - 'pure-ftpd': 'FTP服务的进程', - 'sftp-server': 'SFTP服务的进程', - 'mysqld_safe': 'MySQL服务的进程', - 'firewalld': '防火墙服务的进程', - 'BT-Panel': '宝塔面板-主的进程', - 'BT-Task': '宝塔面板-后台任务的进程', - 'NetworkManager': '网络管理服务的进程', - 'svlogd': '日志守护的进程', - 'memcached': 'Memcached缓存器的进程', - 'gunicorn': "宝塔面板的进程", - "BTPanel": '宝塔面板的进程', - 'baota_coll': "堡塔云控-主控端的进程", - 'baota_client': "堡塔云控-被控端的进程", - 'node': 'Node.js程序的进程', - 'supervisord': 'Supervisor的进程', - 'rsyslogd': 'rsyslog日志服务的进程', - 'crond': '计划任务服务的进程', - 'cron': '计划任务服务的进程', - 'rsync': 'rsync文件同步的进程', - 'ntpd': '网络时间同步服务的进程', - 'rpc.mountd': 'NFS网络文件系统挂载服务的进程', - 'sendmail': 'sendmail邮件服务的进程', - 'postfix': 'postfix邮件服务的进程', - 'npm': 'Node.js NPM管理器的进程', - 'PM2': 'Node.js PM2进程管理器的进程', - 'htop': 'htop进程监控软件的进程', - 'btpython': '宝塔面板-独立Python环境的进程', - 'btappmanagerd': '宝塔应用管理器插件的进程', - 'dockerd': 'Docker容器管理器的进程', - 'docker-proxy': 'Docker容器管理器的进程', - 'docker-registry': 'Docker容器管理器的进程', - 'docker-distribution': 'Docker容器管理器的进程', - 'docker-network': 'Docker容器管理器的进程', - 'docker-volume': 'Docker容器管理器的进程', - 'docker-swarm': 'Docker容器管理器的进程', - 'docker-systemd': 'Docker容器管理器的进程', - 'docker-containerd': 'Docker容器管理器的进程', - 'docker-containerd-shim': 'Docker容器管理器的进程', - 'docker-runc': 'Docker容器管理器的进程', - 'docker-init': 'Docker容器管理器的进程', - 'docker-init-systemd': 'Docker容器管理器的进程', - 'docker-init-upstart': 'Docker容器管理器的进程', - 'docker-init-sysvinit': 'Docker容器管理器的进程', - 'docker-init-openrc': 'Docker容器管理器的进程', - 'docker-init-runit': 'Docker容器管理器的进程', - 'docker-init-systemd-resolved': 'Docker容器管理器的进程', - 'rpcbind': 'NFS网络文件系统服务的进程', - 'dbus-daemon': 'D-Bus消息总线守护的进程', - 'systemd-logind': '登录管理器的进程', - 'systemd-journald': 'Systemd日志管理服务的进程', - 'systemd-udevd': '系统设备管理服务的进程', - 'systemd-timedated': '系统时间日期服务的进程', - 'systemd-timesyncd': '系统时间同步服务的进程', - 'systemd-resolved': '系统DNS解析服务的进程', - 'systemd-hostnamed': '系统主机名服务的进程', - 'systemd-networkd': '系统网络管理服务的进程', - 'systemd-resolvconf': '系统DNS解析服务的进程', - 'systemd-local-resolv': '系统DNS解析服务的进程', - 'systemd-sysctl': '系统系统参数服务的进程', - 'systemd-modules-load': '系统模块加载服务的进程', - 'systemd-modules-restore': '系统模块恢复服务的进程', - 'agetty': 'TTY登陆验证程序的进程', - 'sendmail-mta': 'MTA邮件传送代理的进程', - '(sd-pam)': '可插入认证模块的进程', - 'polkitd': '授权管理服务的进程', - 'mongod': 'MongoDB数据库服务的进程', - 'mongodb': 'MongoDB数据库服务的进程', - 'mongodb-mms-monitor': 'MongoDB数据库服务的进程', - 'mongodb-mms-backup': 'MongoDB数据库服务的进程', - 'mongodb-mms-restore': 'MongoDB数据库服务的进程', - 'mongodb-mms-agent': 'MongoDB数据库服务的进程', - 'mongodb-mms-analytics': 'MongoDB数据库服务的进程', - 'mongodb-mms-tools': 'MongoDB数据库服务的进程', - 'mongodb-mms-backup-agent': 'MongoDB数据库服务的进程', - 'mongodb-mms-backup-tools': 'MongoDB数据库服务的进程', - 'mongodb-mms-restore-agent': 'MongoDB数据库服务的进程', - 'mongodb-mms-restore-tools': 'MongoDB数据库服务的进程', - 'mongodb-mms-analytics-agent': 'MongoDB数据库服务的进程', - 'mongodb-mms-analytics-tools': 'MongoDB数据库服务的进程', - 'dhclient': 'DHCP协议客户端的进程', - 'dhcpcd': 'DHCP协议客户端的进程', - 'dhcpd': 'DHCP服务器的进程', - 'isc-dhcp-server': 'DHCP服务器的进程', - 'isc-dhcp-server6': 'DHCP服务器的进程', - 'dhcp6c': 'DHCP服务器的进程', - 'dhcpcd': 'DHCP服务器的进程', - 'dhcpd': 'DHCP服务器的进程', - 'avahi-daemon': 'Zeroconf守护的进程', - 'login': '登录的进程', - 'systemd': '系统管理服务的进程', - 'systemd-sysv': '系统管理服务的进程', - 'systemd-journal-gateway': '系统管理服务的进程', - 'systemd-journal-remote': '系统管理服务的进程', - 'systemd-journal-upload': '系统管理服务的进程', - 'systemd-networkd': '系统网络管理服务的进程', - 'rpc.idmapd': 'NFS网络文件系统相关服务的进程', - 'cupsd': '打印服务的进程', - 'cups-browsed': '打印服务的进程', - 'sh': 'shell的进程', - 'php': 'PHP CLI模式的进程', - 'blkmapd': 'NFS映射服务的进程', - 'lsyncd': '文件同步服务的进程', - 'sleep': '延迟的进程', - } - - def __init__(self): - if not os.path.isdir(self.__data_path): - os.makedirs(self.__data_path, 384) - plugin_obj = Plugin(False) - plugin_list = plugin_obj.get_plugin_list() - public.print_log(plugin_list['ltd']) - ped = int(plugin_list['ltd']) > time.time() - if ped: - self.add_nethogs_task() - - def specific_resource_load_type(self, get): - """ - 查询具体资源类型负载 - :param get: None - :return: 资源占用字典 - """ - try: - plugin_obj = Plugin(False) - plugin_list = plugin_obj.get_plugin_list() - public.print_log(plugin_list['ltd']) - ped = int(plugin_list['ltd']) > time.time() - if not ped: return {'status': False, 'msg': "该功能为企业版专享!"} - infos = {} - load_avg = os.getloadavg() - infos['info'] = {} - infos['info']['physical_cpu'] = psutil.cpu_count(logical=False) - infos['info']['logical_cpu'] = psutil.cpu_count(logical=True) - c_tmp = public.readFile('/proc/cpuinfo') - d_tmp = re.findall("physical id.+", c_tmp) - cpuW = len(set(d_tmp)) - infos['info']['cpu_name'] = public.getCpuType() + " * {}".format(cpuW) - infos['info']['num_phys_cores'] = cpuW - infos['info']['load_avg'] = {"1": load_avg[0], "5": load_avg[1], "15": load_avg[2]} - infos['info']['active_processes'] = len( - [p for p in psutil.process_iter() if p.status() == psutil.STATUS_RUNNING]) - infos['info']['total_processes'] = len(psutil.pids()) - cpu_percent = self.get_process_cpu(get) - cpu_proc = cpu_percent["process_list"] - mem = self.get_mem_info() - infos['CPU_percentage_of_load'] = cpu_percent["info"]["cpu"] - infos['percentage_of_memory_usage'] = round(mem['memRealUsed'] / mem['memTotal'] * 100, 2) - infos['CPU_high_occupancy_software_list'] = {} - for i in range(5): - try: - infos['CPU_high_occupancy_software_list'][i] = {"name": cpu_proc[i]['name'], - 'pid': cpu_proc[i]['pid'], - 'cpu_percent': cpu_proc[i]['cpu_percent'], - 'proc_survive': cpu_proc[i]['proc_survive']} - except: - pass - b = [] - for i, j in infos['CPU_high_occupancy_software_list'].items(): - cpu_info = {'proc_name': infos['CPU_high_occupancy_software_list'][i]['name'], - 'pid': infos['CPU_high_occupancy_software_list'][i]['pid'], - 'cpu_percent': str(infos['CPU_high_occupancy_software_list'][i]['cpu_percent']) + "%"} - cpu_info['explain'], cpu_info['num_threads'], cpu_info['exe_path'], cpu_info['cwd_path'], cpu_info[ - 'important'], cpu_info['proc_survive'] = self.__process_analysis( - infos['CPU_high_occupancy_software_list'][i]['pid']) - b.append(cpu_info) - infos['CPU_high_occupancy_software_list'] = b - infos["memory_high_occupancy_software_list"] = self.__use_mem_list() - c = [] - for i, j in infos["memory_high_occupancy_software_list"].items(): - mem_info = {'proc_name': i, 'pid': infos['memory_high_occupancy_software_list'][i]['pid'], - "memory_usage": infos["memory_high_occupancy_software_list"][i]['memory_usage']} - mem_info['explain'], mem_info['num_threads'], mem_info['exe_path'], mem_info['cwd_path'], mem_info[ - 'important'], mem_info['proc_survive'] = self.__process_analysis( - infos["memory_high_occupancy_software_list"][i]['pid']) - c.append(mem_info) - infos["memory_high_occupancy_software_list"] = c - return infos - except: - public.print_log(traceback.format_exc()) - - # 按cpu资源获取进程列表 - def get_process_cpu(self, get): - self.pids = psutil.pids() - process_list = [] - if type(self.cpu_new_info) != dict: self.cpu_new_info = {} - self.cpu_new_info['cpu_time'] = self.get_cpu_time() - self.cpu_new_info['time'] = time.time() - - if 'sort' not in get: get.sort = 'cpu_percent' - get.reverse = bool(int(get.reverse)) if 'reverse' in get else True - info = {} - info['activity'] = 0 - info['cpu'] = 0.00 - status_ps = {'sleeping': '睡眠', 'running': '活动'} - limit = 1000 - for pid in self.pids: - tmp = {} - try: - p = psutil.Process(pid) - except: - continue - with p.oneshot(): - p_cpus = p.cpu_times() - p_state = p.status() - if p_state == 'running': info['activity'] += 1 - if p_state in status_ps: - p_state = status_ps[p_state] - else: - continue - tmp['exe'] = p.exe() - timestamp = time.time() - p.create_time() - time_info = {} - time_info["天"] = int(timestamp // (24 * 3600)) - time_info["小时"] = int((timestamp - time_info['天'] * 24 * 3600) // 3600) - time_info["分钟"] = int((timestamp - time_info['天'] * 24 * 3600 - time_info['小时'] * 3600) // 60) - ll = [str(v) + k for k, v in time_info.items() if v != 0] - tmp['proc_survive'] = ''.join(ll) - tmp['name'] = p.name() - tmp['pid'] = pid - tmp['ppid'] = p.ppid() - # tmp['create_time'] = int(p.create_time()) - tmp['status'] = p_state - tmp['user'] = p.username() - tmp['cpu_percent'] = self.get_cpu_percent(str(pid), p_cpus, self.cpu_new_info['cpu_time']) - tmp['threads'] = p.num_threads() - tmp['ps'] = self.get_process_ps(tmp['name'], pid) - if tmp['cpu_percent'] > 100: tmp['cpu_percent'] = 0.1 - info['cpu'] += tmp['cpu_percent'] - process_list.append(tmp) - limit -= 1 - if limit <= 0: break - del p - del tmp - public.writeFile(self.cpu_old_path, json.dumps(self.cpu_new_info)) - # process_list = self.handle_process_list(process_list) - process_list = sorted(process_list, key=lambda x: x[get.sort], reverse=get.reverse) - info['load_average'] = self.get_load_average() - data = {} - data['process_list'] = process_list[:10] - info['cpu'] = round(info['cpu'], 2) - data['info'] = info - return data - - # 获取负载 - def get_load_average(self, get=None): - b = public.ExecShell("uptime")[0].replace(',', '') - c = b.split() - data = {} - data['1'] = float(c[-3]) - data['5'] = float(c[-2]) - data['15'] = float(c[-1]) - return data - - # 获取总的cpu时间 - def get_cpu_time(self, get=None): - if self.__cpu_time: return self.__cpu_time - self.__cpu_time = 0.00 - s = psutil.cpu_times() - self.__cpu_time = s.user + s.system + s.nice + s.idle - return self.__cpu_time - - # 获取进程cpu利用率 - def get_cpu_percent(self, pid, cpu_times, cpu_time): - self.get_cpu_old() - percent = 0.00 - process_cpu_time = self.get_process_cpu_time(cpu_times) - if not self.cpu_old_info: self.cpu_old_info = {} - if pid not in self.cpu_old_info: - self.cpu_new_info[pid] = {} - self.cpu_new_info[pid]['cpu_time'] = process_cpu_time - return percent - percent = round(100.00 * (process_cpu_time - self.cpu_old_info[pid]['cpu_time']) / ( - cpu_time - self.cpu_old_info['cpu_time']), 2) - self.cpu_new_info[pid] = {} - self.cpu_new_info[pid]['cpu_time'] = process_cpu_time - if percent > 0: return percent - return 0.00 - - # 获取信息,如果存在返回true,不存在读取gson后存在true:不存在flase - def get_cpu_old(self): - if self.cpu_old_info: return True - if not os.path.exists(self.cpu_old_path): return False - data = public.readFile(self.cpu_old_path) - if not data: return False - data = json.loads(data) - if not data: return False - self.cpu_old_info = data - del data - return True - - # 获取进程占用的cpu时间 - def get_process_cpu_time(self, cpu_times): - cpu_time = 0.00 - for s in cpu_times: cpu_time += s - return cpu_time - - def get_process_ps(self, name, pid): - if name in self.processPs: return self.processPs[name] - - # 增加使用nethogs收集进程流量定时任务 - def add_nethogs_task(self, get=None): - # self.add_process_white('nethogs') - import crontab - if public.M('crontab').where('name=?', u'[勿删]资源管理器-获取进程流量').count(): - return public.returnMsg(True, '定时任务已存在!') - - s_body = '''ps -ef | grep nethogs | grep -v grep | awk '{print $2}' | xargs kill 2>/dev/null -count=0 -while [ $count -lt 2 ] -do - count=$(($count+1)) - /usr/sbin/nethogs -t -a -d 2 -c 5 > %s 2>/dev/null - if [[ $count == 2 ]];then - exit - else - sleep 20 - fi -done''' % self.nethogs_out - - p = crontab.crontab() - args = { - "name": u'[勿删]资源管理器-获取进程流量', - "type": 'minute-n', - "where1": 5, - "hour": '', - "minute": '', - "week": '', - "sType": "toShell", - "sName": "", - "backupTo": "", - "save": '', - "sBody": s_body, - "urladdress": "undefined" - } - p.AddCrontab(args) - return public.returnMsg(True, '设置成功!') - - # 获取内存情况 - def get_mem_info(self, get=None): - mem = psutil.virtual_memory() - memInfo = {'memTotal': int(mem.total / 1024 / 1024), 'memFree': int(mem.free / 1024 / 1024), - 'memBuffers': int(mem.buffers / 1024 / 1024), 'memCached': int(mem.cached / 1024 / 1024)} - memInfo['memRealUsed'] = memInfo['memTotal'] - memInfo['memFree'] - memInfo['memBuffers'] - memInfo['memCached'] - return memInfo - - def __use_mem_list(self): - processes = [] - for proc in psutil.process_iter(): - try: - # 获取进程详细信息 - pinfo = proc.as_dict(attrs=['pid', 'name', 'memory_info']) - # 添加到进程列表 - processes.append(pinfo) - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): - pass - processes = sorted(processes, key=lambda p: p['memory_info'].rss, reverse=True) - l = {} - mem_total = psutil.virtual_memory().total - for p in processes: - l[p['name']] = {'pid': p['pid'], - 'memory_usage': '%.2f' % (int(p['memory_info'].rss) / int(mem_total) * 100) + "%"} - if len(l) >= 5: - break - return l - - # 常用软件分析 - def __process_analysis(self, pid): - process = psutil.Process(pid) - important = 0 - explain = self.processPs.get(process.name(), - '未知程序的进程') - if 'BT-Panel' == process.name() or 'BT-Task' == process.name(): - important = 1 - num_threads = process.num_threads() - exe_path = process.exe() - cwd_path = process.cwd() - timestamp = time.time() - process.create_time() - time_info = {} - time_info["天"] = int(timestamp // (24 * 3600)) - time_info["小时"] = int((timestamp - time_info['天'] * 24 * 3600) // 3600) - time_info["分钟"] = int((timestamp - time_info['天'] * 24 * 3600 - time_info['小时'] * 3600) // 60) - ll = [str(v) + k for k, v in time_info.items() if v != 0] - pro_time = ''.join(ll) - if ''.join(ll) == '': - pro_time = '小于1分钟' - return explain, num_threads, exe_path, cwd_path, important, pro_time - - def kill_process_all(self, get): - pid = int(get.pid) - if pid < 30: return public.returnMsg(False, '不能结束系统关键进程!') - if pid not in psutil.pids(): return public.returnMsg(False, '指定进程不存在!') - p = psutil.Process(pid) - if self.is_panel_process(pid): return public.returnMsg(False, '不能结束面板服务进程') - p.kill() - return self.kill_process_tree_all(pid) - - # 结束进程树 - def kill_process_tree_all(self, pid): - if pid < 30: return public.returnMsg(True, '已结束此进程树!') - if self.is_panel_process(pid): return public.returnMsg(False, '不能结束面板服务进程') - try: - if pid not in psutil.pids(): public.returnMsg(True, '已结束此进程树!') - p = psutil.Process(pid) - ppid = p.ppid() - name = p.name() - p.kill() - public.ExecShell('pkill -9 ' + name) - if name.find('php-') != -1: - public.ExecShell("rm -f /tmp/php-cgi-*.sock") - elif name.find('mysql') != -1: - public.ExecShell("rm -f /tmp/mysql.sock") - elif name.find('mongod') != -1: - public.ExecShell("rm -f /tmp/mongod*.sock") - self.kill_process_lower(pid) - if ppid: return self.kill_process_all(ppid) - except: - pass - return public.returnMsg(True, '已结束此进程树!') - - def kill_process_lower(self, pid): - pids = psutil.pids() - for lpid in pids: - if lpid < 30: continue - if self.is_panel_process(lpid): continue - p = psutil.Process(lpid) - ppid = p.ppid() - if ppid == pid: - p.kill() - return self.kill_process_lower(lpid) - return True - - # 判断是否是面板进程 - def is_panel_process(self, pid): - if not self.panel_pid: - self.panel_pid = os.getpid() - if pid == self.panel_pid: return True - if not self.task_pid: - try: - self.task_pid = int( - public.ExecShell("ps aux | grep 'python task.py'|grep -v grep|head -n1|awk '{print $2}'")[0]) - except: - self.task_pid = -1 - if pid == self.task_pid: return True - return False - - def __get_number_of_processes(self): - import psutil - from collections import Counter - ll = [] - processes = psutil.process_iter() - process_names = [process.name() for process in processes] - process_item = Counter(process_names) - process_item = dict(sorted(process_item.items(), key=lambda item: item[1], reverse=True)[:5]) - for key, value in process_item.items(): - procs = {'proc_name': key, 'proc_description': '此进程的进程数有' + str(value) + '个,进程是{}'.format( - self.processPs.get(key, "未知进程"))} - ll.append(procs) - return ll - - def process_description(self, get): - try: - updatas = json.loads(get.information_collection) - data = json.loads(public.readFile('/www/server/panel/class/monitorModel/common_process.json')) - data.update(updatas) - public.writeFile('/www/server/panel/class/monitorModel/common_process.json', json.dumps(data)) - return public.returnMsg(True, "进程添加成功") - except: - return public.returnMsg(False, "进程添加失败") - - def universal(self, get): - method = { - "题目1": "遇到未知进程解决办法。", - "1.1": "观察进程可执行目录和运行目录,是否与BT、项目名、常用软件相关,若与项目或者系统相关的进程且占用资源不大可不管。", - "1.2": "去‘百度’上搜索进程名,查看进程的归属,以及是否有害。ps:https://www.baidu.com", - "1.3": "咨询项目开发人员,看此进程是否由部署的项目所创建,若是,可加入到常见进程列表中。", - "1.4": "实在判断不了进程的性质,可到宝塔论坛发帖求助.ps:https://www.bt.cn/bbs/portal.php", - "1.5": "对进程做出详细的判断后,无用且占用资源较高,可关闭该进程。", - "1.6": "若占用资源较多的是使用当中的软件或项目,则可以尝试适当的优化,比如mysql优化、适当限制php的并发等。", - "题目2": "内存,cpu使用率不高,但负载很高解决办法", - "2.1": "负载高低还与线程数量、IO使用率、服务器本身有联系,可查看线程数量以及磁盘使用情况进行综合判断", - "2.2": "若本身服务器的配置较低,可以适当的考虑升级服务器配置", - "2.3": "若是遭受到网络攻击,也可导致服务器的负载偏高,可以开启宝塔防火墙以及安全插件进行防护。", - "2.4": "若服务器使用的是云服务器,也可能是服务器商家限制,可以咨询一下服务器商家的客服。" - } - return method +piG6BsMF31u4R4iA6R4SRA== +dTLQ8Wr3wPn7w6pte1B1YA== +SbQQ5SqO9QrBwZ9ObpMYLw== +aB1r/Hl41eeDYHjvKNsDN+fwXnHZ5cKnjNQNkvt811Q= +wiYVtfO/yzajW1Tv5z7hyA== +bbgfjwWjzPNOxnsxTb5hzQ== +1u+XjG/2+GSQRv6EzCaWRQ== +XIfdJ79nObMM+vyAmKbTmw== +DjrnV2ugcXpCQCRtr1AhjGZ1uAagxBZY4p+tagUZ+g789PFU3G5RrAfrJSwrmwJC +VEQLjjua2Z0/nl0SqvlgpQZHKj35t4rRyTE0nOHE87Y= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +GC18aI/weGPndT9J1gQkOXLINKNA+KXP+Dhe1VxZyWA= +HuLSKhYFyga2gCsAbUvtZHLP0BAfxDGimJZciagBXbE= +i5Pjb3wFWjgWhO/Oy7348x0KCcyi6IVF8qqP7v32njhzG+8BdTBQyqlCg3CRdkP+hkP1ySPJM/rBG9E6T++rIg== +rdSLxsl+Jo+9wGF1wR0xFfkiBqqHhxwN5kl0Lj1eqZJZBJ0Un+czbpqSSOCnL00rgrs3FzTKbr1MIAbXu6UVGg== +0SQnak5isFUZ7qBrHG5nONf0/iIYxo+ossW+sIeJPVqc06D+NOpjaE6Uqq4RDZ6D+X1V5oSWDCNufz1Z5NsruQ== +QoXjlU/yrq3LjBi536E1lAtUbyzoaPGqmdMscHtc82Eqnx3xaK7ND//Aee6pjueUoSkSd7Kn/GTgusZOZkazWr735TNoBy8E2i+TeT5QpBw= +EnTcTKmPyFkaGN9DqDzaSceq8uDBrg212ADWAEQ4IZ7RrbWtCNgCxsKxhOwBdNTIiDKGNrYM1guBNGIg+GP4oH9ldbTHxPoem41Xx3gcVH4= +VdrjM3tESbDpF9uC89FIBP9Gd1akXGqOvVf2yxPgOSm6+6n45/Opv1Gr2Z6wLcSiweXQCxy9SU/Ps8OhjacAOibd0MvWgOEF/Qj8Hg1OpD4= +23L+yCPrnnJuqxnn47seC6mWE/7wPUK89o8BQ4gkIOBjwQKKbOoe6bUJw6g30fgZLJKIywqZgDEqKLNb6Wk4ZA== +ls1D8gXPZvKpYcbhjFq/angELE3042eHnGYIIdOc3EqVbEv8X3JJ8d4kjYJRVOzknDfBdJPlavOFcLuc1C3RlQ== +Ucs0nOwLNKLYeguD5Ho6We236+pzwCXfjIf8zCbNBPUj8Trs/3tdQqbzpyL2y/A3nGNz3pdXfnPIeWgvZPJdJCQ3oPUcIKLTYx+hNA8fP3A= +1u+XjG/2+GSQRv6EzCaWRQ== +PdlH+lu7jNu2OvYS8I8SIGv8UV7r+RCWsVIOF9/nLLA= +EnTcTKmPyFkaGN9DqDzaSeIxT+jPdlkzsjUgVGeo67M= +lfXDVmh5N4vkcCpP16rRNU+nS75ACFRPief37RRqVEQ= +QoXjlU/yrq3LjBi536E1lNTH28NopdOB61QQlHyGFcA= +UrqM+fZEnAnh676kBTiBqQ2F5qpH1BZ72+KLbvQGDMU= +zKCrIlbP/DDhVHiZ3fIxklV8gb38KK+GbBXLALoFwPQ= +VoVkMP9tW2A1crYS1JPcez/T5g9qr04wEneJMF3RRlA= +40d0Kjq9eFveZcVdlpFWHqsMLb6Cbgq596PvxkzyPPs= +KIZi5iGAZ1ERVsYyf9tRFc+B9Yz08eywa9GtzLEWwb0= +CCWp04qZQAG0xvBg/GxA1ac8M6aDoxCupSpEbINrd14SIsQMCX2yg35fZKfSZ45bUlwsnEKO6CphPTsCe870BQ== +ELPZs21xKKoOnovAKJosLe8y3drsV3LCJ9rBZ59KMXaSIKv2MS1cInuGvowyQR6/da+MfD6aJgPi0OIyjKl/TA== +0aJdVaqIprJqIF2ACVD434ZUgkgWJjA1UrDXUzIX7hLHfRqO7xfTfRdmPwLbMjnBsyI29qYoF8SnX188Ei+AEA== +cEDtw6uMb2/A6mWG+mGficxASjA1PsBLQqmyk6avgrA5NU4ovgWemvxpF4tbZuYyAvelvkrb1my6fx7rBEPUgA== +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +oKVaGzjnS0ibnrCRJ6VhOXjlPFbbLUbXwFxCuM1lBik= +x4aYGEXHUiyydN0TvGTj6vQYATZwfiFdhjHTFnA8Gao= +C/AADmj7KKNzSBTrmgjwHtkMmAZdvtiecPcfgdxsdp4= +Oonoanjo57cFaJw3N8v3+Re7+iV0uPfFwq4o6iaSCBM= +sTBwU4/6vbNIKaonWCsmPxBuizocf0lQLoMzCcsQdOE= +GQ8bP5FSB8U92LYKcbOqVOyusqkt7x3hnzxKCHCGHFuYPxYJaktAZ3ui01vGLpRoDf0dBr+MRE3SP4hUL7XxWSh/k6f2maS9LSwwdicF4yc= +6XekAdRuN93speBaqeGdSwFHDCPkTtGFh21Nhkd6RlbVsrd3+42TNXjDmQXYItc0eG3VUBTA0arBcuR6ynpbKw== +Ct7YEe25Op6jhkRit6HXSS9R9HrQIjqhCqSX+ee73JHfZ5J55br5lF3uB0DPPA1Y +hTUqYvcYvtmpKOewm481/MQZEgItO4vOyj5qmO8Is2I/FUdYUR2LRXEA0bvcem68tGf2rMuDkJVgN87keG95dDdbsSJwMmpyoBKp89kGpgU= +QP9mdDvW8SzUpgVgx/NNyykpcPN+PRmA76dtTDgOphdIcANFGc9Lc1H+IPwlHbnCF8ZWbhFJxGOC3NwCQqRKWA== +mtgwn5Yb9q2LR0oKaA1GDD6dKVcIfEsUGTnL1x7Se6F27uWBDZjB1zpIV4BB9SuvOACzeafKG/8RCnLDgL7hUDMaSrphxgNVLKIhSNMuEA4rIhdIKVhFskcIN6ZauI9DdHrx/ohao8XSb7Bms2iSWA== +/P08j/NQ3V/6NH/K2OPqHUeruiobFj0/uXohCUqtHCIqf67tmCphQozpUid3/pMXZJJZ4WV+sOtmU5DRKL34w5AK3PWk4OrqzNNs6S/4bmc= +DFIcnybg9Q4e8lvNk2V2upP6Hr7xhNXqIhviOYpVB3raB8ZoZWqooeaOFlPW97CvXdi5l6SG8hM8ae5YhafZMnviwbNiJgG36bYj9aMJvfHfuerZTUunqjWjKtVf1gxT +fBUMMACbLfIUiv22DBztC2X8HTxvBDwI2uJvKztXFjHgPgMkx1KLn/iS/Rz90Vtg +KpJ00HDVp573NWedwG3qjGqRAe30y85buGnJhVevfJm3oQkLHz3hrV90wpkkyGnTYy5MOm/pSRF/WrydQlXKCA== +NYEZAf0CPJOvudyjBgyMzPr9RZ7nBiZlHyvHZ6psZ4EgokJyAlzdjdnp920Q7b6kGI48rfekYgiiDo2uvYO3pA== ++wBlhY7T5u3USVjT9x1efKnQWlx/oGnzkkf6zFBt9Afs7O/ns5xfaaJBL+Zm0S0nHdqHmJuae5ZknLenRHyJxA== +vX3aw/VpCn2kdUdQ401ThgqbPZjOUtBDCyHHc0hFIKQ9KgriBJ9/gPZNfcuSLe7A2jFLkMQ0ox50NmmrzT9/nA== +Rwr5WRRWJc49eH5/ngaIBiWYeyNcMjokcXjDWWMdBpQrqGeet+DbXOd6PKMpitDQQvpS+fsW0LpUBhz3AC+l9VeeNNbYYP06g1AXgsuzzyk= +ZUOeDkg+GlcyKcw8Uhn9GhzBXyPqNYdX9wx0uRWoOFBRu7BPoEbD8g0jpM4NMA0W6fNiH+9CmstbuuqaxZjuXw== +ogK1joGXApEMwUMZDKuBPRM0Z0BG5Gd1D6LTT02x0YXw1YVPNRIHfaMphOIQmn7D9kr2yVIL0bSBQVkRyhGoKAnDYDm6Y3vMUpTTHyRhFAE= +kHKnlmRHLSoRZ3FlW7VxUDkE445yMx1gpDyqtMBOugd88lX0F6rketv+5EQ5rk/ua73CnzFhtiUf3HI6EwhucJ5x6HlVFX/lyIdLjt14Wsz5TohcQ1yNJJkHvVWTWiXV +Rwr5WRRWJc49eH5/ngaIBmd/jgM0o9xcXjNmkQ7ojlWHqQYbvRSksD+YYI3UqYt5arsC7JF9ywvKPiwi14GCzQ== +UymC8Fo8j5Vfm3c3mvSqVCadgEL91qmjAufL18gREC8zFOvraqdOTFkx4Q8QR8DaHyZKqWK/befuh7X9PMbVIw== +Cqu/R4uxTK+Rjxynhwj8CbkMvLquN/WmcJqBhmSX+Uav1ikgqpF9c2lfpNakMq4U1f6TY8HJd3+YcM7ncuENlw== +hsSLeDBq70w4vj6GUYkiYbh4J1qeIJqUq989eIhWYj85DR2PV0lMqymDGJmf2ZL0 +yoNfTnPxkT2ArtuDpRb26Xft0K/cNfiaz3RRuiM2MAI6zFyefakcodovpWSvFro3cGtk5GVoSq0peR7Zow/RFw== +PEQnpcHCCkNy3Q7rsdi/TDlLh1hH5jGLBD/P8NhI1yeXk5rBZmq2rPIvTOtE1IQM +XCHGYowgcWiXCPE3uFJ0ZbmCVeK+z7+QiyULQeM/Mfqgvd2ITOeeUUunR/W0eUgw +ijJqnkItaijg8Xxs/K8JAja7EaqPc7KCGoNyxafnXX1GfQLiO7hMfC4trRt8GBCh +U31Q5qsC0ZXwyq8Zvtv34/Y4w020wjEmrhhBg6B3ag9Peb7eoOVQ2RtXr9peyeoc +Uma9eTOXwECuwd3yDyjkccjKNPBHZO90NiSH1qcJQ1x/tFFJNp7yhVpfbDkTfOHg +fKhgtgnW1lEA4yy3gPB948NQpKwJr7zKV5C2horyhdjwskODl2bNwS6dipXYQ7PM +i2sw6+ZuogTNbw/oNKxMgXWiqY+aZvoMLpccLvBZI5aaY3QlTOwrb7QPOGk54xJA +F2DWu3kKh2SE9esMcSzIGZs4RKyGxgWAnrhrGFCFhyYGj+sUGXNUG/dippyVFmDY +oDtnF93s1JkfTVQ9UzfoipWioLUWstRD7jomiUg61SVBA++lOLLh5NS+jaLBu7OC +JPxDl53ivISpEUZ+1kQXQdwn6NDhZRhnvq4Kk4Dg1F78sf4e2VcDCafHiSYcmh4FNR3mcNE3togApnOLUomA2Q== +RrtdXHpDf8ftCQ4plQxw0Oh0rdfx53ARjXk+r8csJmUThuK0+0oUUUsGp3YckosrrXeo2LoNn5c6UqyBUWQ+CA== +c33sbT/GTUpquVFIngMO885zLxbCXVnvexYBDVOqU+iMb6nbaP4TrFnF0sZQpz5CiHdLyhtvzXNx8TUkLN9lXQ== +D7Xe+CnrKuTWLm74s5CdUP8kGk2RBzR4MwDZOP8HMBWM7vrwiBc7rfg1YuRvnLkp080JZrF6wd/Cjd7buGH3fw== +t607yNsl+Xt4IUZUKCdg10Hshd8XtxOblG6lAjn8lTH8FIQPvzkvdbqtLIVpeg2o +v1YvZLHufhLKFxIHRhqo1yVklxc3rJlgV5c1oxuurRWLcATM374XavSj+WtB5S7v5MspC01i6B4GEzgYnQWGLg== +O0mtTdlcYfDttKJPtVyrjWTsesoi2Yrh7TXd3he1E61o5u2D65rQDBCUAXYd/EsJ +Zs0AOqbSh/pxVla9Y6PkDEgfzwwsnZGC1KlACL5BI7yFhlcNAgIImY+NzajllBSA +eP9y1UpA8RKgKDUIdVctv8RYlCX5DX3ByYiFFPcsYMtfq2Hi6ygMIqz08ehknPq3kLbHBezWrZBjAPN5AvD6Dg== +eP9y1UpA8RKgKDUIdVctvwNj3jgyiyWh1YuqZQrMAkQkCjQFc/kByE3s0UrgVKYvxwbAOHxQG9JHjNJ07hSSEQ== +jlyatDnTvJkXgFSCTGo3tiGAMuSVHfuYvRETjlSvHGXrQDWFYrXS1QRqEORJ8isz +ECQx4hhql9YPrq7JsTDQaMrs8kiVlfn9Z8Q2Ifc0RUwzgtJ7FSS6YU4fGZoI+vTM +yUEvl2lmwCONcgsqq7zSbw+XXPkspwcz/0kaT5QF/6UXREdbTeC16ytf0AHP41KXTYT8aY2W8cVMXH4kf/nrZg== +u+YxRTYN8EJVeuURXtdcZv02D0KVvAStvANxEWCv9sf8Fm7jHpRdK1cQJTfqlVewQQb0soY1dkSd+EZKffT1SA== +AS9+AURcEG1BucPfCwzeHUkiiH/X9fPuUC8GLr00X65+TGusfO5LkBvlfu3iuth5 +o4QTqehA3AlJXkkcJZDUJj+Jzlgd8GAcYeL5PjckJKlACYIxVuJH3hlfpsyx1yOn +GH1HhyXKGfEMcZXYE219kNP5r+5V3l7u36q6KqB5CiGuj4WsUy67R41L7Dob2OTrscp4iTsL/eajIwP8P3twQg== +yL3V7JujJ3KF9f8XtukZsaEMjPxcpgiq3vTVqhL0re8ldG7x2wrWgut0XubluxOT8r8dMH1ueJ8JX2jFRbadq8jpJ0cSaQBow1cKZJbOIik= +Optb+ZZvcgzfoopw/H9ohTDk0ctvD8SKkfapSkLhlCbvQoYhDSlRZhjkNHBWVOmHOTyBUsvtZudPrsD8P4eShQ== +1Gt/p2zFJnwB8mRUNG4E25SVCJpZa9Jrtsib+8dSFPyCHRw/LA63qa2Ty7rzSKElPcZ7W+EtSbNpbzcFW4dHVA== +iQWcT+NnjKoF31XI9cw2YeoDvOh09cumpPHE3C6spMUKCU88YaIWvJc71nUHOV2TXitxGG/DTyVD6PrE3XoQlg== +IXYtIlQBIioKIAehYu+n3FZcrjiXCvJKfLpalDl+PAQqgDVfnM4v6ryxYGPhPquxlXjsxuh067VFhK7JA7Zm2A== +Fic+PF+8pyenEu8liCSk1ZGYJrf34OrxiQ/kSbZJf+w+WgdvDRsp3GynkcX5kvRpktiMsGDDQdldXlj3E+K9Iw== +XjBVJ2fDvhqj13zHSPhNq2bYXFR0+8Rj5YB3l6Zn8SuOfSLeh8LvRfi+okfPlg/Bldv5dQDk/TV1buG3oe/830ywdWKkgu7VvUpmCA2B9Bk= +mb5ArhJqY8U7Nhj3n51LRtWAGnitzRz6hRGBLHH3PfObqyyzPGD9ukhudhkGOEDquhrpyhRpigzbUoXmpN0Yp5BmZ23NRuLlmZSsIsRip7M= +eIAbDBBMnbQNURiQ/SQSj2OL9VTyEv4fx+MVb1toHyJoIXhKC9eqW368GSpEcq6wJkbA88+fkXsxOh6XvN87yA== +WMryAG7s9FKjtjrG4x+ATG/yGGAOpQ5Mu9Sw79MAMt60bIv7/nLjmvJhjeSHaYYGXC3AGr/NvpTvnYMS68tGnQ== +WMryAG7s9FKjtjrG4x+ATMr19LOPxyT30q6tXv+Ai3a2P7Cg7P2d0oqeko7smZPj07DwtrlzOgQRtH9War0TKw== +WMryAG7s9FKjtjrG4x+ATA1XxevnK+neTT4KR3WqQSsCRausMSrVxJIXPabGNYO5PGcgayhgtwVFCqBih65mzvRqg6yx+WU5qUn2dAFfJRM= +WMryAG7s9FKjtjrG4x+ATK/LBrspDdnvHPnyOH0X43Md+CHKEHD9MqfCnJZTWL7LDFbPXj6YxDlhGpfsWqSxIw== +WMryAG7s9FKjtjrG4x+ATIKWbV5uWpBQjHCbzC21ThGHCMrZ6/rqrTk/OZoTkW+TeGdfnhdLs504olThDPMVnQ== +WMryAG7s9FKjtjrG4x+ATMO+vflXSDy9uzArXKqz4Djtq7h0GBwNWgH3OH8NAYsunB5E5RsFoqVFU20mRdSyvQ== +WMryAG7s9FKjtjrG4x+ATKE3uOlGXG/ai7M+pRzUU3joyAGlrn9Xvw6rBO6Xt4RBMPqFM6sjtJQmELC32ZuPXA== +WMryAG7s9FKjtjrG4x+ATEI+NCcj39CbYNvbfjuI2r+Ca/kX+1V80nr4TNaHpeCLoDAztMXxfGIc4BGt2SEslQ== +WMryAG7s9FKjtjrG4x+ATM/EbaHuaIT2sybliDIfD7g5G0nrrtSXGitConozYNm1l5RlLE4IBxGMfEcTooYzvg4ZYfhEGo9BLutg/FDb9Pc= +WMryAG7s9FKjtjrG4x+ATCBp2NIRXySRT6QN665odL+FnoUTyKwi/Wof1FoE1i392rQ00v+Tj8JZY5q2WPYv8Q== +WMryAG7s9FKjtjrG4x+ATOpEoOjVwgJZnZq4QnYnom2telS+0ITfvtKzBcFGGjCOV0m+zvWTM1tiOGcnY7fo4A== +WMryAG7s9FKjtjrG4x+ATOcQ0lVkg1tu+xrgjlmF/syK5hWuiwO4HT1xF72fAH+HjNqKw1apci6P+Hrri+xHVya0hQBeyzP98+MViRv4YNQ= +WMryAG7s9FKjtjrG4x+ATKjZ3EBOshBqZzeSpOExlRpSJt8xScVUTN4XvIHfKhd8ThyGKISNelgnZ2T0ecpc+caVfc2yPlPf+ytGzTm+Rr8= +WMryAG7s9FKjtjrG4x+ATKqF6F/kMLWl7MmcuE4WEnOv9/pLTI6TIBlDkNHgIKZazk8Qj02OmN45aR75YotAQuwAXfD/u/S7O3ycI3UW+10= +WMryAG7s9FKjtjrG4x+ATEVYhjgt49n9T8GXTStDP96GUI/P1L3eerDdknnuGDYUwJKd7RpThJ4f6pNtKs0w2HazqyMbkmNDGqgyfFso3eg= +WMryAG7s9FKjtjrG4x+ATNN83k65N7TcpEhdUSdXIr+isOFgGyPHGd2J7uhQdjgz5pavR1hfPZm0NRzMtdCX1A== +WMryAG7s9FKjtjrG4x+ATMgv+MtjO7FtemUMEZb9FoiEY65thzTYGMd2DG88RPHV8qAYx6yMo9mtPPDigDaASeWIkt1rj6bveGXNnTEXllI= +cW6VfR5aZO4LK69BZsjV9UpTvkEwgCSbRhjScMiqRO0iWxW2KX4PIEVOpNAm1myH/iDckSaXxf8xpidXGbBZxQ== +qXz/P6tVzdUn0oeNwT5P5KbO6OhS5xA3Kmz/5XUFu/I1cVsE+5O1VhCyUHpZYfJT2pwHBNSBtzJ1Oq3xwjq1TA== +u9mvWPj2GQyeh4Qfg/3JkwVeHSQF1+4KWUXsEhiLbU2vguDvPbovjWRLj2zZUnOaB07bXePGlviviNg7AQvIdg== +u9mvWPj2GQyeh4Qfg/3Jk+tGURZ6qNJ2J+xWOI0Bn5CuMg5ytf9jNjlGFFpw4DGNLT9Wn9gBrbElVifp/pMH7YmIpsuqe2hc92tY3h9kAyU= +u9mvWPj2GQyeh4Qfg/3Jk2lThgawC4szMX2s/bx+aPMNiE4iMYB7wv2LuUJhg/FQKp02IokgaklB9sZxvq7ydw== +u9mvWPj2GQyeh4Qfg/3JkxYsRJytDV0UNBwwmqkjyn2ijGJMGLLpbXK72x8n/yg+O/+N2LoAJgZnh/xBFoQ8n0DHcwQbom6c95k7ps/GkCI= +u9mvWPj2GQyeh4Qfg/3Jk0OHZ7VjmYhZ7lnCmXEc3X3qtCMnqW/RQ9yS0JYIUmbme9mqsaJHWcY3poE6o/ne/cFUb2Oaz58fELEHG4XrsXM= +u9mvWPj2GQyeh4Qfg/3Jk0Z0U1kJeRDilIw3Kjupxgsrjia4ZIHtR0AIKrEIV6ne2s1SX/XFyqNVeXyu5Jn2/Q== +u9mvWPj2GQyeh4Qfg/3JkzP+lseasJtzUpa7Yj1DRMuUfcg+Hp5N5bfdDY0coeTH5Yq4C4w5O3SwZ7qhWXyy2g== +u9mvWPj2GQyeh4Qfg/3JkwXiSGsxe0w3LmGE8blrG9SReSPhQPVdsWHzUalY1ICjL+4EAPt5q5mx/7PfkcC/9APSJRHysqtB4ndtQUZJ5zQ= +u9mvWPj2GQyeh4Qfg/3Jk68W6b7rhQM4E+OcfhL6eem4t4asg8Ztp99XWwTH3rpMjYl45f+sOtHSL5Cv5VLL5WZ57M07rU9bPcjMKl1KHZs= +u9mvWPj2GQyeh4Qfg/3Jk6XzYErLQRL1wLbMINJRJVqQbO8phW1756ARycM9dbmHdlT6CMnpn9Eq6iGf0i9DCwy0T1XVqO657kI1KicL/T4= +u9mvWPj2GQyeh4Qfg/3Jk8u9aLlj97rhmCOXufwoHkDGOKvcNwCIaXf+yqS9+xn5uBZKay3rI9gpQ+YFNhrq5g== +u9mvWPj2GQyeh4Qfg/3JkxI3FsCFY/d+zw1RFtJNfsM9Y0JZY0zwu9KposEwLxbTO32jVkmuEGIEEKBVUuMxaYo+cgfuNmpwnrjU4xSgT/I= +u9mvWPj2GQyeh4Qfg/3Jk/4Xxo+6RU8qO11KZpuw8BgsqdDGhk9wiSWyfFvNp37zCApmBXC9CJZoCupKL93HmITFPlMzrW3Li4DMPZz4Q9w= +k8mQATL2QGzdHVb/ZqU0sdv0TlLp/CuYNtJgvarcxrImVKie8q/K2IP8APf+DytIv2EALQvbMEAWFPj+uLnoWQ== +Optb+ZZvcgzfoopw/H9ohU8tljhlXj21QQ+gRCOyX1OySc3/UttfSVVyX+Hit9Pl9lD9hpdd6+eDlDZvY/BY/A== +O0H12djxOPpYv3DI+nktXWNVtMvswMeYWU8pNnbQ1ARb5mEFsfbugUsGgqjyPIOsW/YXoqzwQYaBeQI/PX0k6w== +L2qllRaufT5w7PPs814S6jdO0T9dO6I03d4q9MOWqvJghGaC80NN1STq02T+i7NHzjVHQQqYXvwj03kWVZgvAw== +GCa+LjTuIcQFdoFSHacKrqfvmlQ6RuQhQzYlgtCK+Mbnx/xc9twHveEqaKLHjAqo+w6O3hTC4YAiLHFMrgjbLg== +tp5g+osoOV/pmK+tBcXKV3TEvJQnuWktOx35NAOLpVu5SeTQu2OUIKD2RCNQx4glgboDAtP2DmgDNx4AolsMJw== +tp5g+osoOV/pmK+tBcXKV9hLL8C8BEn9jkAQJ+zdPFATD08P4lTbwWzYHe6aZg7yLI/VSVuK2O5VWMd8noAQfRXxVAXihxg6tU9xMNxLz3w= +tp5g+osoOV/pmK+tBcXKV2aqyPF8qSL8lCeArzgYEhIuNLRWWta9ukfuU7Y879WpQ1///S3UQo97mRN5zqPy7/rDvia0pu0R5bob1Kog1oc= +tp5g+osoOV/pmK+tBcXKV+3nkvNSPZsmRCiIYaYDGbq7/4/fjilKmBrEKu4acUGrPw8BUkgJVWOg2PjrIr+zlVmmSMIuRdGwqTFasCKm88M= +tp5g+osoOV/pmK+tBcXKV8dcfRVV+3iCHtG9QUDz4uEX0PDI/QNVkJ8bhWwwu2OMK2KsJGf31NGgAdgO7PLYfZnLSPJTSbwNoKyAHIz/MOE= +tp5g+osoOV/pmK+tBcXKV+0kNyfVosEvu1R5eIdu4mp9RD8DVTwhInq+ZEC40TCsm9+wOl+JMFsCdo3+EAZ+d0zW4kjUy4PwBARjXscN7SM= +tp5g+osoOV/pmK+tBcXKVxaDwti7mWG6/L3BiJN0oGE+KsXn/VMGG10NyH0u8rFe1dt8uv6dJo7PhgTzgrjyUlC9yHxZBApt2/aSMtTQqM0= +tp5g+osoOV/pmK+tBcXKV23o/6FCCKq253U3hcS90I3OV6BKTo40KNpqERe4dqM0oeQ/LOt4q/CkWNfeZohxR03nPBZYx9oU2tYFaER5lx0= +tp5g+osoOV/pmK+tBcXKV5NkfIfu6lyMg9b8khIrQtZYsmf2xFO3Ll8iCOqEXhtb/HpJFbeWFHSTz8k3hcMZC+u6JTgdRO77eYTfziE8/M0= +tp5g+osoOV/pmK+tBcXKVzTZueI/mphDci1n7i4FvjFgNHcEAtd0j9fvOK5z5vBciy6n+sOJ9At9zuT0m3/KxZJFG0yGmBOBOXfAGhK8zjY= +tp5g+osoOV/pmK+tBcXKV/R1y1G15U2L/+mO+1q9xsmO7UYo8wUqdZnPiNsqikyMiCweoAPY/CPiU165YCDP+/OzVGULF9IYxTp3EmqR0QU= +tp5g+osoOV/pmK+tBcXKV1nrdSo4U5sImCiOzub/YfeCySVZJlO5XwlzdKY9IUlsJg1Qw1flNH1tnMaUPEx1pjQu7NWQLnE/+Hdbo95/rm8= +tp5g+osoOV/pmK+tBcXKV2XIfCmcFG05EPSUV1i+BCwrJcHy4HfpZCdiXtwn6xgjIIrMpOOiDx3z6iet/EaSPTQ7yIIhqXN1BviRqHNRqfA= +iaAodRlbbDbdxCarNwkcQUTO0nhGXrSlD7BKY9U/+h66Pp7oK8CK+2g0VusNn/WL988GCPaGLufuUmZ5X3BNxg== +phX6+xCiuQFZuok+RnQpZC0EWCXYMWhejnYj1yT8kSxBypjvfxeKkMPpyP6TyBU8+DJYIEw+o7fuNkVcGJ/2rA== +6dJZtsb8CUPLf6XAX0fHy6aa+F/Hy0+FSnai7BAvMbkZ1kngZkQmNufaUhYOzTH/ +lpMeOmzCLPmsgqJkG7PE0nFddPBnF9WHa9x+DYoD7F9QIcTbi73discSSa9t5LcsFXGXXmts8HnluacE0HmiZw== +lpMeOmzCLPmsgqJkG7PE0uNRxY/2Wd1eN+U5+P3pylI9OATxz5ysUrFFxQIoidMewDxl+pTFBs+ufGWBKUMFNA== +dDQ2U1xB29TjkFiRc3OY91fq42wVlRDJBslIUC5VOwebrdzYaqYfQhKRsb3Cgza3 +phX6+xCiuQFZuok+RnQpZMyP4ejz8dP5Rv1WDyPhhA/mTZrpMNtoDSAGhVA8OACk +6dJZtsb8CUPLf6XAX0fHy6aa+F/Hy0+FSnai7BAvMbkZ1kngZkQmNufaUhYOzTH/ +glsVHMdFb/IeJsxHuz5fO2qoCX0bHv8KxFpiL7f44o8f9OvgYRSRBzbUzk0UvWRhmVbMLLBc2ny1lzSl+dOPNA== +LllQdNg6CwTfXdYMyq0nDvlbmqRSNt2wHU7z0L1BKHuJFK9vEEUqpeoN10MSEIuV +u9mvWPj2GQyeh4Qfg/3Jk2ZO6P8hGnyy/zY+jhpPTExHxZm4Bj8vU8VgMgQAcdRKiWx0v7Qs2ZqBBehqzjEtXA== +u9mvWPj2GQyeh4Qfg/3Jk8ZDTBQks0/zArhCOqlMX9YSyNH+xfsBRCf33b/2BtXsLd83FYHzqGKVviPkeMIqfA== +u9mvWPj2GQyeh4Qfg/3Jk5Bfw3Qg1JNky6Qzkq0rIuCvd4nlwBcmQ2rEWWvb3pqt4Qc+DiJMDyQTJgQN4r1Qk2y6JuH4MTZbH7fgmmkXQ1Y= +u9mvWPj2GQyeh4Qfg/3Jk0o90/9mCvvXcHLwVxWpPy0cI1q/hR/kcgRbTv4vHEQMSvjtaEXD8qxqjBxMFE/FVfRQxFURW8Y6QO5hEjEQ8KM= +u9mvWPj2GQyeh4Qfg/3Jk0n0O4lJoUim6Eqj3aXcB98U+jsNkYpIN3VNqIbVV8o468MIUuo8ThVmrXfllG/hehHWfYgFxFvK9krwdaJfFWM= +u9mvWPj2GQyeh4Qfg/3JkwXiSGsxe0w3LmGE8blrG9SReSPhQPVdsWHzUalY1ICjL+4EAPt5q5mx/7PfkcC/9APSJRHysqtB4ndtQUZJ5zQ= +nrGCUtZXO8lhMUzpsw7flX5OFTgCrwZmIp7z4/aubyjFV1DrMaZGkwbq+0TXi4/HWwgBhq5Eu8bpgtFbLSvCGc2vHCb1vWe1wQ0pmNMfV/s= +OlA3Ms276mZTcqwKAwXvu56bhPdCtw5GGNVjM1oM2e+M5sRPVjVsfUNxcKyhB0vz +8p4c/a5VsUSqHSeB2usqZ4qruwtfsgO+2SoHERyfupq1ztB9EMTtVlBuFntiG7U97LE2j6LU5+xJirtn924K+A== +G9T1Nfsl7hOYPUDmJtp6nN22GmHI7VyiriEhZPJCJWNPjQPq4CNx87iIuy3wK0gK +AdPVIE5vJ65wZA9MH/IxkPFx17bD/WkfQCRLJN1PaYLkEFnLaqeffLEWDXiLQRJZ +2Poxxdy9o8AvAQ8T2zP50KH3rg6/ouFD/nYQU/f+IIp8ActTeg0uR4fevWWvfjth +6I/H1Zpit1TSo4efV6bxh+EAqFiiiS0FnW9oFr2u/GGPf4gC8A5mP6Sem+ISyA4J5tO3CbUIrmqOfzZcuUOqLg== +ztzuq6nAH2ogL/nP4kxvN69SRiKraaWyWBK/ip93LYc95fIJZuN0J2GAk0DYtX9u +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +wgR07xfoapmx6eEnFHXXYsrtutdMRxYffYBJpExAFLKvOOsdc0KV0E0yQAAQBZkqO2dMmc/KvncCFdasMp/nIQ== +NqhMbY8+EQI8zhTG6Zh2I9n9ZL/a0DhWUREDi0vuSmk/BBGIzgfE/zwz/+PpUEnm +ET0y6SUMPE2Ml4dS8viRTJixJpYvSUNaJEQ3wgnwmDR/Dl0Tv2W4MPgVxUJMaUl1 +/VraJuu2EmN1fJjV8ELbStoy+NvUd5C4U3l5SFxwBYqHlGghTAj+KvzQIxH+57GjdeVm46KwnUg95Vg08AS+OA== +fLETQ22HuDIh4ibW1C50WVHnbZXsGAhArGdfK3tcG1pK3MbEtlt4g1YMg//GB50t +Nc90b6Vm+KhgeJLTsdGO0k1J8yfJ4oYcbdpVDV0CZaqGvHTkW3ri/kIS4tuhLtXq/yA4+EzCs7wLomarmDvHow== +8v88LHwZLe8C6EIyeMyGCHmIdCyFG+evh5a94fiR+mg= +32pdC9DD05OE2l0oXazDFBeY6+wg6WQKfWFWUO0P6kUhOidDEfWd/HzYk7lVbEtQ +1u+XjG/2+GSQRv6EzCaWRQ== +flULj/LdXe4ShFGOST+HfgiEQqJL0kKKn6iUffraNcUfW9gxdwW9wmntTAcmF7KVOzRDVCHXtZHOS4D78Zob1Q== +Z8UsPk1Q7HtwjRd4g01ryw== +v4wZTNeqSt7hKjjs5rimNMLgdpIKpbxvujNwNW9pnkHVQ8lv2VVqQHVVc0GPFdfP +6/YdOldN7ILWufE3TpQIbCIbF/kDgCBlZUKNWM/prTU= +Ys4bozvJBgtB7q16qcvtBxfJUhKmEI4H3T9j7f29CdE7rNQ1+w5tPWDUNgUfBW/n +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +8eMNwOFVbk4AtLbMoBZ3WTJylC8gGuKXwsoa33uaVOilo+eTav4x4B/CBPvAjTgn +8eMNwOFVbk4AtLbMoBZ3WXGvCdkUE1lCv740F/AYpZTVXZWXz/0Lz1ev0SYqBf0gjG5iXFTmyoAlt7NDxvLnEA== +xWoGNWjKGPfI4gq8aHoTfNkqombeCH/8EMbOTha72rQFp5IL2Vny2x86v+62gwW+6tyAPvX761EuG/sZKbYPlA== +PqAs8ZYMxXN3H+LYNsNggUUCDiwhZ5kF7Kc0dxYPIfvvLyQAP717ErA0zvIWk7YEplmqjfbDzWRoNOKyjvnrdQ== +1V2v8QerKOmubvSxgB4eTELr4a2ZreHX5suyB5yxG62fHomWaQJE4fHrHlvISVn5FPy+5D7AmhPQ7W8szu3lxBPfmtavK+0V1LcZFVr6D4cBm4eSd2et/WWZXL0AJZ6H +iZaIsa48RXfE+uf/yF/rQHaH39lOhItuA9XlMsZGwHM= +PxQyEFNgEUrjep/ov+AWf+oM8xywLq7k8twOsAgmDnhwxbYPjvM1HR7ZmztlWsVP +iZaIsa48RXfE+uf/yF/rQFDamMuGYoHcHvig6IR7L5g= +iZaIsa48RXfE+uf/yF/rQBHzBiyCImLvDpqO+6hTyP3m9lHPXPQIIotfEF+lox/PQnwFku2M550Yae3EjKfdwSo2zaLVsBrXdy2HzGwErWc= +iZaIsa48RXfE+uf/yF/rQH2dIzSy3gkyzLeqVCbm3eTH9zmgrK/v9VFjQHkrschdvsu31TnW9AJ98XajAcXV/xJabWM9DvIsHVJ0epK6pbw= +vlw44yX3fCVt6eRmFa/S8+zcDVCNlbkZPwywv2/TkEdjzIX1a7+Qv5GdodWIWjhtmXWPTEcqpjmiAgwl6lc01A== +e1fY1YJpbz170V5bcvidMMj8zZEMpvaxNEwOhd5zodzg9VC9MnufwNdaYU25e3Ap0TjTYIXJp3aftiAH9jPWNw== +S1U+ozOPAZj3pbrAwiivwV+OvqAz1d1sZB0EPsOnolf+IN6OHYpFNOp6+2SjXYmV +iZaIsa48RXfE+uf/yF/rQDuGVEzsmF3CtJPekR383wxEc8OumBnbu+FZpS1sbexmU4IRN0hBGAisopPlzdzamXnnzur1Wji8UNl30wxAtMET/A4MwljXqYiRMkHmVxJs +iZaIsa48RXfE+uf/yF/rQBHEMyc95ROne+WTG39WCVaS+cXX9BOUlzXxE0w6ot+Oc3VNAQ+DhKgGIvQZHJ5c6g== +iZaIsa48RXfE+uf/yF/rQO/ErLeTILJ36v76UKS54Jw2UX3OGfKAG7yS6qfIxugHyp2S/OL5tJKEWjUVuW4IUuzQc20IvoIJtKzwy4FpzeobJXfdFFF6CmRLQ8jKXRiVYQeTwTiaZHk9oJGEGYazRQ== +iZaIsa48RXfE+uf/yF/rQFvnSgMMK7IUZnw+ay1bgOmW6qSuUZQTz2zXyezTI5+d7Fsrlel7B7kVccTcSm9Pfg== +VmVrGQo2zRokW/ZuO9bN698MHz+PRq+TRlhzLxtWGMygpukHf5d24j778t/zhHGidh9OMwub9c9vsi9i+zZN4Dri9QnMhFeT7rUsrCUJo5TwdvrO6yF6zrAu1/LNEjX/ +iZaIsa48RXfE+uf/yF/rQBogmq+jEWWC54y/H6MlNh4rp0+JMOgYT2mZY+TYp75FCm5W06pUVqWJPDAukRPl+ZRIueWws/l752pV//pYffU= +cdjMZGd+oj+E1aBgYuadXWtaC7aSClO5q/k5YLsaTcSQLEvmsyDwCAB+ds3GneYkZe6H1FsAUlpWz82XtXdOog== +cdjMZGd+oj+E1aBgYuadXaukekpOTL5WA5BiDC9vCxPuawlZz6j7AAZlEbdZdLQ48OZE5ekQYbVCOAhL/BJpig== +2YIp4yADnjMGtY1lwBdIGuJOMZTFEHseAqpUotVOIPwfj4SeqVivEGJskF8OkEcx +iZaIsa48RXfE+uf/yF/rQANiUB6WFTzoHZ3OwjBwtM2/j5761QTS3AJCOB+qIm5mr/5/eVreUywu46shO608Fxk3JiSLg1QkYCinrxbBW8I= +iZaIsa48RXfE+uf/yF/rQKIqqcSbvyJ6nKs1nu85+fMRjwAg8Y6Nh993Kne9OE8bYXDQ+LuRcreEaUiZDN8aOyIbEKMTvERwYtJMKUES04k8EwtY/LwkeYK3mxxvwexvEDmRXbKbEGBLl52UPJVJfw== +iZaIsa48RXfE+uf/yF/rQJ2YbiBFucc3UQ/ZX8J50oEXgugXrduKe8SAiE/mA2k+T3N5ZO8BVpH8YAKvC4ZR0Q== +wlLHv6kT3Q/RmtMBN4nDAe8+imtRBuiJubS60XFrMJg= +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN6yl77/0MnObCK5XlSs9DWF8eqyYhe1FWHc8xaEa0H0urV7ygkURnv6BiALfKwfgNY7qBO4kuhmbf8nJ1oKsf+gyy3EG6rLIgfnG4sQZKYsDRT9AFAoDMtdIUBl7ovtqbuQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw5bx4JTsUXc9GQD7jUveTF+h1GxK2r9hBJbi4mkX/JP2 +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw3p30izicGP0/3QQynjVV8QfCMdvBT/iicXrGxILnxT7Flbhb0PBJRFV0JDAAWvxHQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw+sgN+NoknzWuDmsXkDIpaqQj2Lw0lrCOo1L/C3blJaAgKXMvFjXCS584QOcKTtHOfudZ11Mj38X+gn040NpusM= +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN6/VW4n84N+xmKyiHoJANXzc= +i/p1vQ7ugXGBvwbjECApz6tQ3fYSsRgetdpGSwwJdY0= +wlLHv6kT3Q/RmtMBN4nDAfKuWKTPnQzOUo8b39dYynKTIm9yduqnJKjcYCIOYNl2Ve+9X84vGlhWQ4dv1oTdSWR22oAktUUFZmgLi+55bm4= +VmVrGQo2zRokW/ZuO9bN6+zshBZO9d61HkhcnvN87E/esTkTggViecgSKvZl2/jilsnJj15aKiHc7zivB4GiRjN+hiLn89qackKeknKgOAkBsBvnq+NL8RWD/NHkUD0E +VmVrGQo2zRokW/ZuO9bN67EO2w8TNG1LHiP/tGFb6V2Gt2yQRtjzazeNr74m0atzaPdvLFX7XyVVRswklh1k9b4jQHovg+TYl7Ff9JiJYPcw7bsPs4ffMxTkD62b1LTR +VmVrGQo2zRokW/ZuO9bN6x1lQigDbibIoTlVRYg/sjBbtNt2NXe9ly6pYDchoxSFm8eCigCwEbYkrmTqSpR66Px+l9G0RWse6wD1iaMwlGH17ZZODY6LHJwXbnnoehfY3dgu5gHZ1QEdRO3HBSEC/BmkcWwlcqCj5HLhmnA7jVY= +VmVrGQo2zRokW/ZuO9bN65ZXaaTN9OdZLZFqE1+KS6RnzprQpjuF686uW9l1a1Xa7mf8nAsb5dvwf2nXb6MyyeGLjC6K1kWHmAxqB0N5Lhqbqh8cjSw9EakEYrt1pQmzU6bWNZ792Zty9yar13+Cvp95ctSpW2nDI1vJq91wABs= +VmVrGQo2zRokW/ZuO9bN67sdU0BGc9v1NKjQ+sFJyyAVqCbBH9iZ1H+7Xb4dU4gwDiJcnbDr43Qq212mGJoiC2DMD5ackhh4eB3W7YaQThFl4no6PKCk63hukqrWGYjX +VmVrGQo2zRokW/ZuO9bN6yl77/0MnObCK5XlSs9DWF8eqyYhe1FWHc8xaEa0H0urV7ygkURnv6BiALfKwfgNYyI+kia9//G28k57ZB6qNEQ= +VmVrGQo2zRokW/ZuO9bN68m4dRJxwc9vmoYO9XfNu8JoAL0ANoHBA6wRLssRJG9d +iZaIsa48RXfE+uf/yF/rQJ2YbiBFucc3UQ/ZX8J50oEXgugXrduKe8SAiE/mA2k+ZzCTJlHCGGr+o1zyX0J7dQ== +iZaIsa48RXfE+uf/yF/rQEW/HnjzbDWg5QiOMKFgUs166OxnL4KP5EwHDMXQoCQJRRha1BVSLbU7v0IGpgEo3Ow9rETT+gtPN+XqbTD9O4VSY9LLGQrxzpdHx4ypB9/+ +PMWbmPRKnSEEQmuoINHGWsR4WdhOdaCB8ldWDbzQF20= +wlLHv6kT3Q/RmtMBN4nDAVAwwjXMi7bsf2Urn5Fa7BXRsDUkBXTBYPEpaEfSPA3CViaW+eHPpTOr1sqaxfsUEJXr0r8RinHLoMwf+yDuYSs= +VmVrGQo2zRokW/ZuO9bN64Jqehn36bioUDDk5ORDx1UfKJPWwYobkwrB7mW4yC0E/yspyDpCiSv62gKc5GAi9T+y34+JwM/Ix71XqqYfqk6wuSlZIytENGwwf/qssogjxCLORgiVriYpcnIi5PgXEg== +VmVrGQo2zRokW/ZuO9bN69lOsOGGAluHtoQsF4JTTD91qLyShUeY2zrHp66doa7mrqRGy6XrHMPUUlJQ9jQMJN0k5EJO95P+i0RrsO6IfE9KsdjGbXe06Ke4++h4/3UVjycijEWFMnGV6uvGaxuhCw== +VmVrGQo2zRokW/ZuO9bN66Bx+QcaotFFc25usOdeBnbTRU45vVMOgan2bt8crvVKWdMPA2l5zQM2ZMjHmie4Ostb2ZlRvEpt7614r2AV4HLy37BZMk+x2Nk/Kbj+WVTFsFKkwVRGLBsFJ6yqXmiY71otfHLUQGgM8V8B/4spf6Y= +VmVrGQo2zRokW/ZuO9bN67sdU0BGc9v1NKjQ+sFJyyCMLjq+xWwbZ8c+ddHAwB7BKoukMsMxYs2sswO22RXml2KSsLww/KZ/8G2d/WiAx/rgyHeWDCW9UJufFHa7xinD +VmVrGQo2zRokW/ZuO9bN62cg3MFjF3Xh1e0EF0hJpnU2g0qslcYflC7df6LH5ycv96oJb3iOdLAMnhFiSSXOjfbj/n1Y70ip284H5cNfF+k= +VmVrGQo2zRokW/ZuO9bN66RJ3wohQFXs+QBlSfWZpaf0OWtwRJ6BrDTWIqv5NVGm +iZaIsa48RXfE+uf/yF/rQEW/HnjzbDWg5QiOMKFgUs166OxnL4KP5EwHDMXQoCQJDHEHPrnpFeN1JnNSnouOcA== +L0eUthVnpkGsmKFAX6d+uJTdC3GdscKd99Ans+Bqi+E= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +xWoGNWjKGPfI4gq8aHoTfBVO0rtzJGwPNNCAKcvoQ7YGnBVlRRzBkPuuYdnd1drISaN/5vpFnf0ao1Hkw6ztew== +1u+XjG/2+GSQRv6EzCaWRQ== +piplqnZD2qKjxqq5BxcYpBpGRwKsiHJxwLqnhEe2D48YtmIu0TblS7JCQluznId9 +uCeQtkgRBtN0HiqPTvhZHeALjPy6nS+/Py9QQjy3Ju9z9JZP7KSWkptweIa2bYE2 +cgFNMoW/KsgkKsovM6n6KO+C6KjDPWoUE/hR0PgyXA7qIltJH6LvNkqkyka0kjkn +v4Cu02hZRDsW+85CLRBC581P6PmAWmaUk3n76dbnkVw= +NmkgFF4ZsyEHu9KVT53La4WGMwIVFvT8dzZ/QJmAvrfSZPlY+eRgJ/IGmWYM3DiSmAIvy2CFU0T3w56mhC3RAurFjyBbAOsoZluPEh0M/sY= +YoCN/YJ3FGpKuVn0NXtXTv3XCu2B9Ep5OJG8o8tyakhU5BRvAk6zZXlPRk9etr/09ajT8+g6VldNtH2UYJBlUw== +YoCN/YJ3FGpKuVn0NXtXTsWIqvly5o9YazCVgAEKZW0PTnKV1DWqLu0lRYHDz/YcsBnA7SOwZuYWWRON2HScNg== +1u+XjG/2+GSQRv6EzCaWRQ== +PyAFsma1q0hLhwE/7yEwvlLDizBd59bTOK0yaWmk5wKN2EpZqyKHG2wdBEZW3kAlb+ZRqbiaA2kRmKB/ClkTYA== +DB6Dm0fkcUc76J51zO5BsTwaw9bnJ1hMDPyvzSAiR99N1sM5KJNB3yakje8btu8/QBHP8RxgWzfFi8SXTQxdwc2MFwLBmPC8Kwdv5Qni9ns= +C3JSV38/w2nvM3I7TZ5+4epkOMkaWYvoOErk6ygROHs= +UaGDE9swb13AVV1HxfNyqi1KJFeHDs773v7JZyf+ME4= +mbg4a7OqVLuFyb4lBv/8ihC29C2cveBqDqu2E6PhaRE= +meZhlBQhcKhD0kZGoC7z20E7608oJ67KyHB8wGHG8NCp7pjQf+LhcA7qy6D0G1bFxtX29fKaijtpwJqI9cAO0v9A2SbJKZ6YEwsGeas8a+0= +p6i8iK7HDbvmLpoFrhNyvxdkpuCzYcAgEgoD8gO9QRA= +rYd5Q6Wra4KSueZqSRRqN4gDfkYJFjFLoq8Y/uFOPjk= +0aOe1PRcA3iTQeyd7fu/UT3OH8TgnUZIetc0kIThlMY= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN63igchzsu5zTaq9IXvBjrNVT0Ypem/6Ctem4lX8VHAu6 +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1hs1FfIPISXmd7TJ/r+Vl/cQigT5kdrpKyW4rHdGgtQ= +VmVrGQo2zRokW/ZuO9bN60XiiQBl5FEkQ6DHRSd+Tw111DT04Hh8GF8qhcK0KSh/ +VmVrGQo2zRokW/ZuO9bN65hNsUFyX6poB60L31EhX7HkJSxkQfY10wTx/PiHvRMW +VmVrGQo2zRokW/ZuO9bN65s61AIgyJ8ql62IoneEj2fAfjFSAde2JX230qlgjnH2vV81My82sV2kTGC+8zV2TQ== +VmVrGQo2zRokW/ZuO9bN60EOQAX3/UH5DkusEZxHTXPrNTCzuXya4iloKJBnudc0 +VmVrGQo2zRokW/ZuO9bN689+A3i/MUSda6G7q0qItIjO89bRKTthTU14pzjp1wZx/IYyafQwuUcVBEAoyeksHA== +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN67Ta1/lbp6yqDJ2hGru87rzFFPN0VimnlC4upIFz9Ysl +VmVrGQo2zRokW/ZuO9bN69twiVqU/YruzaZ31Ozd8kyG92yD5lEmoQmdiZOEP8KmMgIvjkFJw9Dw8H1UfQ7mow== +VmVrGQo2zRokW/ZuO9bN6wMQRwD6rXhLUXMdURcL5Ag= +VmVrGQo2zRokW/ZuO9bN63dWRPw8YokoZZBoOkV+KLyGghp0raIHV9VjtnANG9KIUctvuM1jrb2u+oAvO+PyNX/pN1YjyTM2QU+QQeyrSQs= +VmVrGQo2zRokW/ZuO9bN68YCI3L+9iFlHcXIBMaDhSoBHAzOkS/E3fCGnDSizJXU2WZoltpBbK3cP92X2enr2wtzMw7JWe0lgiVXiazz4T/Z4bUwnAkwMMGjZJNpyPjM +VmVrGQo2zRokW/ZuO9bN6xdWa+gbDteV6y2dDYzvdHXNdvwv6O4BWZ9K2JMaaIkBryl3NjlyCMPeat24ub9YK6v5Aanri0Vw3GBZzV334T23bfdQVyLiqBefgdn1Bbn/k2+E2Jdx6KrEuRr17CCy7q0GnxQtnc2syvPtkXQBtoY= +VmVrGQo2zRokW/ZuO9bN6wNy3dMSKzw1+znxXLOAZLh5FEFcOrAX3cntmkTItZyKKVGgHnm92b7h7ork2RuLWUmLbdVgqwxu7Wl8pXJk83I= +VmVrGQo2zRokW/ZuO9bN6wRTb++fBMmvLx2k6/MEJBys2MeDsfiTOU27PQGTtFwGzoxQQOeDpqoXCiSV9odxKw== +VmVrGQo2zRokW/ZuO9bN65OUJfJvgNPZipZ9GCOTw7gT/VBpBNqR6eHMzDCD+qxc +VmVrGQo2zRokW/ZuO9bN6/6DN1Rj8W+FylDHNZarY8GmZYJIJsOXkIFPArsJz88a +VmVrGQo2zRokW/ZuO9bN60b5Dlj8qSCLiYdFYQye1vxhQfzXLk5iO7kCBpmwj0d3 +VmVrGQo2zRokW/ZuO9bN66Vj2MTf6PwKatUHlMDNJnAflwnI/tv9py6bv+AQb/3Mh1ASIGKNXpINw5P13Fnriw== +VmVrGQo2zRokW/ZuO9bN6zJK7VoTu9IkzGKsOyFHUih3nKOsBegITZ98OzfLK1Ug +VmVrGQo2zRokW/ZuO9bN6wragKme4d+RqlA7wSuKiun6Xqgs7hKkILO2biA4qzho +VmVrGQo2zRokW/ZuO9bN6924SEXfGwykaPMoxSMlsEcQAwBCGiNnpl4AiLwthDY0ogr81kYcskyrJDT5/Q87vD/ybRw7C5VoTHeL2G9HfhQinJgPrUBEI1svbLGHdxDN1m3m+AH9xwXwqMSg3chqEg== +VmVrGQo2zRokW/ZuO9bN67yPtu0N927f4HIlE8Q/3KEjZKgRfvyBEFqTjZArZjS6wWjCk37Cjc7Be61nCtFfog== +VmVrGQo2zRokW/ZuO9bN67oV8rbNeQxyRr5iXWNRT7jCGot63P1U0aZqnfo9iER3g0jsNJ4bdPmlVkmSovfUKEE8j1q098bPnAUmTwYv2U4= +VmVrGQo2zRokW/ZuO9bN61XvIee7O+pzg0zO1do+jmn7+YnxEi9X2xYlEBv1SXfIIfgDFycnDEX3zYsNHvW2uWPjibLCY7dmVLtuDnx9lOI= +VmVrGQo2zRokW/ZuO9bN6y82k0/7L8OC4IRsil2RtO1tWsTIDg48NsS7LlukbCaWEQq1DKn4luZ24BCSmDqhgg== +kJxoTVctk+bGjMYgKxsjjUpPRSfC+WsPwpMn5FGcCWkNpbjYvwc8oT1ZalJ4jAtf +zxNKIz/Dwj9Harw3dUNEQET0V05gcgtC1p+K2Ihkd0Q= +ACEEdgL/kNbx7RaZcSRxZGgMaBrP/U1Wsj+kqixOX3U0XMejpRw/rYT5i4ihGDYP +pZu1Giln7AYpCOrLbI27dxkWuQIzVUpxHeSHCtkSzeY= +pZu1Giln7AYpCOrLbI27d9FNbDQGKqlmV3/n549EBNY= +IhuisKim47k91RVt8z8qtCsumyIVSOp8xbUu5duXman+sOE6qwlPIuSaUbeysSjj1/coIZqlKQRM13ITsP3JTZKUmxD+QBryE7TKwJbeYvU= +DMKsax7BkTxm29PYxP4a3tCEAD0O5XmbLDroN8+cKNkbya4muwQehRDf9xiCNtXi5mN/bz7SmZm6fjy6PG4iu68u9ENCgu/bVtrtkv80Tvw= +v4Cu02hZRDsW+85CLRBC52OF7Q2oFtJnu5QwKIMu0BP97gowulUFTCsZQKMleZfSYIjeGZBAO1CNNX4e5jvdjNKUV0vrbVg3URKLSmZQu9pDsE/+LHEfAr1t4b9ExGJ+ +lfQMhYa2PjTGY/59jLRb898KNkZ/P9VvrG4wbt0MN94SnvQg6MxkeddSbK4PThoOt6FGGqNr+N4GXY+i/CDWzA== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +aEZ3meBRg3VU8t9WienCaIcMPcHcv2YGqbGshoEf2+BjZfFHOMPLysvgDh5kRgilD8P3YgAMi/iJ67yNmlvr2w== +mbg4a7OqVLuFyb4lBv/8ijnDf2TRJk/x5VtAuD8Lei+9UFA+RJ6F6QToHUji16bw +XisXz5DCD82wMRFAyU2YnBIQChWrgJ4+9q/ztStU2Rc= ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +2STnKtQfadtZuOOXKAyG97fXc39ov+eVBLi4v0OMsFg= +RGF+I9YWwlm/IovqReh8KzDsktfMik+IEctOJHKJWNfLhJfd7lO0N+PqYXq68hXW +wa6Uqxieufh+16mIo2wUWZ1mtKb/vzs433ud/yv/qeNzIjpgHyPB2b9qHji9uppV6FpACIOY4mogpGhOh+bSYg== +kFmg/dBFMjlvuWIcUrXktJM3uaPpOUszNWRf97JB6UU= +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +c0BvLCajy/2XMKFpBZTl+Iu/SkO0dvuX1US9n740cfboKQmRZYgu55JFFv3bWRzs +xM38J/T7qHoyh2Dm7AS1PeofVv7nUvjVw8J08A7tPoHKI8AfdUEMMT36J2TuAkXa +DjJQKue4V6m4p8kNUg01bEfz/+OabzCmVKUlV+DraSJuEtoAE53HRREuE0nz/mi5 ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +8asfyBO1JetUFwe4WyYO6Y7zXLxXmxgBzDu5xRXkg3A= +JkzYefpr/Q8/FMT+PiabUnsjVaeV4SXIGL0ZfwaksKSkUz13n6Mkp8jdbSsukdJu +MzOj4ZiBOJDNVP8vi/lFQAuCz8rMEK3CmFN5hj1hieddwhYaM3nQrsLOT48yBJI/oO/+O9N0Sgl2RvuU7kpn7w== +k92oDIqD3u/zM2W83z4PpVdM7SBGKtVUm1Jd80kknUc= +gyj0tuy4SMPFYgH4w+8wBY3IXPM0uWG/GrgGCA34VZw= +k92oDIqD3u/zM2W83z4Ppf0NYHDpAym7jFb0+rpC0vUFZlWzp5yZj+qpxxcuNbrk6Vcu1GyjgOQWSHm+7DcKEQ== +CAhn8dRUItEbEErp4w+lX7mnZWJD/TsN9WfGAjJJW1U= +1u+XjG/2+GSQRv6EzCaWRQ== +dq8ytOoyRUv/BeomAnpfcgN82WObFNw9OWpp+ashMco= +JkzYefpr/Q8/FMT+PiabUsLDCIfcYA9Gq0gv0IiQYerPKih0OKW4CHu8oewW1bF89ddkSdnMNn9sQNfx6cbWWA== +pTLYmNVSpbV89ewuJgN8s5DQJklspf+KlgaHmhcv0nA= +e1rLLbcLg7HKd8syQ0gecaIghAO5DHIkRtrdHrEdJec= +v4Cu02hZRDsW+85CLRBC56CB7hEAmjGZ+HomKkZKk+ti0sMqpAvIK7hwFc1+6mkIIxY3pr099j+UqOBi9ZahczUxI7FqRqgGWZSM6Xxqdbg= +uTEK8Ng11d3ix2pA+DD/aVQ101h9KjleSwZ0SYcRXd3eEzJzX3PloA8wBw8sN35krPKr/MyJpFx412GjefpG0Q== +uIsEpyb/BmTr4pwkQVVUf0RwjhiJQJf/hK04tdATmnh19x2HCfTqNvTq2rqHc5Gq +32pdC9DD05OE2l0oXazDFAdV2J9n5ffNkz5f36qCNytDp0iqq6ihW0KAr5+/dls1 +32pdC9DD05OE2l0oXazDFAdV2J9n5ffNkz5f36qCNyvubBPOn8LTVbiMDS+ibP+zvdPiwG7MZt2aBqQCUoiqByNRY4+XgGEEEtqLhpLRFzk= +L0eUthVnpkGsmKFAX6d+uOiK47GeV9U0IUhNuINx/fs= +e1rLLbcLg7HKd8syQ0gecfbZMbyjfRdQUOPnf5zIbTPr+V4LqLNiJCjGESBeFgYCEsvn7CupaoEzKXkwj2CZJsK7E0mq6ZE14nq0QU0PXcR3In/aUlH4AeU8sl7MkPKn +VmVrGQo2zRokW/ZuO9bN6/6UgeiotxsmEebrhxlDDLc8nC6Lwpn76wNAK3afDRnhMLV2RBSQaGynOLl28c/t+A== +YoCN/YJ3FGpKuVn0NXtXTgdEcSPVapklApkwW0i70PxTxKlglILzchhWiOPKZgOd +YoCN/YJ3FGpKuVn0NXtXToix6MqFL/QoVtdwEBkml6COQw8TpndvWpwBpvKNQMmTnxpUY/oF7+S3o40O72xFIA== +qxpZEz7yGh2snp/cK+0Ipk2Qv4TuHjOzatUb8F4u/nzLmmMsGUaOag81kxmq7Mlj ++jcV4UADuSsKDRH/jIi95dA2FW0TSUBn0rCfhGruLSE= +1u+XjG/2+GSQRv6EzCaWRQ== +glzTAHn8hffaBHtijzYL5UK18RwV0frcOEbqdQEdG+Pt92+be5zhscNFhL/eAiR9Tc711rcl7gLGuXJ+jzgMU+aJxXXBy1+OdZ5rcxvu5Fekj6TIakCimT7wMqEJ0BifU67RUtePcHtqliChztpO0A== +JkzYefpr/Q8/FMT+PiabUtwVJbCg1UHVw6o0ZG8VQbs= +MzOj4ZiBOJDNVP8vi/lFQAcd/8FSbCN3RDG7aQCgrl3NnBpIPZP1CuuiXlKwBhUw +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9gIulSKHdQPn1rt3eTIMEsl38gs8lF57/xlhqgd9ITkeg== +HNr5/wGY+7coHgowTe94LlheFV7l4nvszHiASjC/wO5DiwIYi0sK4qdLmDmUfgwDgE7PElyE7dR0LMY6vNGFCg== +vxZRPtvgnx9GXrXB4G/UM0PDJAHnJfAH1EU1B/RGx3flsrprR4JXQdk2AZEllj6S +gEJ/mwYAYmzZtF+bOEtqdkC8SHP+P7fOYSe7UxEq8rmitn+SghIfUQayzs5YBbRY +vxZRPtvgnx9GXrXB4G/UM0PDJAHnJfAH1EU1B/RGx3flsrprR4JXQdk2AZEllj6S +YoCN/YJ3FGpKuVn0NXtXTnef+ICKXNxnrkPoLNoM6bkjgMdyZRztWKGvv67a690e +vUSdI+D7GGPa55xHnexsw+oNL/ikozXaKZGm51a6QXc= +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +dq8ytOoyRUv/BeomAnpfcvejXPvE7NiytOq8XOUMnvml0KjFGpiByzoIln/KufdW +uCeQtkgRBtN0HiqPTvhZHcPnx9uQcC8BFwZstE/shHLyfSjSJNhoIYesa6Py6Tvp +7V1Z5ICwJM9j1fYg0cpccfMkJlFKvknmjzfobvmEo2Q= +AJ6YSai03eOKVxBS+s+A+/x4xEbw8mjQ70I4HsvrlCiUwZbEJ9HrEH9cZywfcLSS +auZi0vV8Th1M5xUz1rxru4RteY+AtM3Jk2k0FxXVoO8= +1u+XjG/2+GSQRv6EzCaWRQ== +uCeQtkgRBtN0HiqPTvhZHRHF1iwllZSPFSMptoCh8TWuJckkTFe3/T7PShD60DdF +mOVDVEmzgX6HnqY4rlOl8Zf58jGyWJmgY+rfwoP7Nv05pvO42TLHLIh+VtAUs/DRVclc15+ZQbtHBFMvjOH4+w== +1u+XjG/2+GSQRv6EzCaWRQ== +bxxa5unxulFhVIdgYf2zPoytXjzesmqgb/je+IsUbJ6JAnsRAoeAvcznZO+AYYoOKvJU0MVF4EHkDTvwO3xckQ== +0TsdrJvJroIDiAiNTYLCm/OphXNS2tzCddUWOSuFG0kG0hMQku2PUAi1ctdkxSUc +99nPav/MjT1hGMzUpnNFmmALRQAqua4s0cHovmRI5xqw0Ofo7PqaOiLu3iG2GYZ1 +KbLSYqE/CR9TL3ZELtBVgxmWrwAXvlf+ZAt0WDpGZ0g= +TQRmC0vOvJ1P+lfyS3Os4RfKsX8MHWdsIncvcLq3grDmjzVmBg58wHqSjU0RBeX1aaOFThv+gxFDdjCIpUMu5/gCMPc8mAb3StkoBMislhZ6tOMK+rAXGThdJP8neMaM/NQdddCrnGqi3mjbf1dTHw== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmUiGFQjtZHsM+K9kWsIdYhz6dBHCdfNBfcOUe5Em2j4R8+VSeT0UUYoi1LUHVP8q+o= +1u+XjG/2+GSQRv6EzCaWRQ== +ZPttXNTCGDK18VzoR8DzYKyka11GnDaWIOOQV28/TIXqzJRwd+oVPa9D5jLMJ3az8mUKvRlRSJMZkBfK19w0h+r4ZtAuH86gZfkH0Tx/+KydTvaOG7bIgsn7F2KqkC3cCZfprRednK2p0WuLxxKOyg== +HJa3BH4TkRX/a5B9uAn8vA== +zfLEQOz1g23nqtChgBMTbhyiGXGhWhAY2+qwZtKU2zs= +ypyfLFF6HqkDeg1sXel/og== +w3rdXVV9x6u3KSOstnEsm4PLlBhDTI+8FdH5E2Z7OE0= +IwoYJLA2ek7vWcssU2p6YbPP0gHTLA4HmzobpfTCWYhvHXK7lgr2+CDhRu7uMpje93SAESWlp6DbIzT7VQNW/w== +jKpcG7quCcerLli2mJ39vBolbg2notAcBVKYoG/EO8o= +2SlvqX/WQojOSKqcazOiBA== +cBMX/M+ODyL+hH/po9GjAA== +EL5E2qD00QeSF0FNMBo12pQWeIyq9cNwgIZqd4VEy2c= +ZVauRl8efRxWkkXsUWoKPg== +HfHZ0x6IhQ8gr8sh9V0YAAATEqIM95DuxsRArsnn8ys= +1u+XjG/2+GSQRv6EzCaWRQ== +ydM+flUAneoazE7ph9Hof3rbIj+sluwHYaxCWsNBFhE= +P5FSiL1+Pqt0jNjlHNtw3BfOokQdNQF4so/qFjceHA8= +YnAdY30Lg/9fjxdn+Sc57MTWQhZBfGNMp2YwNaHbwexla5xKL0PxQdFUmaw2L63qX8NsU0BzZj1gDBPhy4JSbPxUVe2xocQ2RpGbAoz4W5k= +h4lKXT0d0p8lHVvwNQMn+LKoo6D0qtuqywubpeGhgjP1wmiYwY0D/HtlehtN3aCW +4xkKxP0XIvHb1D4sJsHh2gZjH1Vo4xijw1ZC+9Rm+a0= +NzUVwIScy4hrAjo551BWpxLBVrqGQSD9yBLKdRgxYx0= +ft4O+SUgqJFurYX9lapEio+F4WkORg7Yc5dRgf0GAKI= +CDLUY0WWS1opA7rPYYGAHqCEWLmN7ESaQ3cddP6FtEs= +D3tDGThV5uK73D09MDbkHD3JqWj3V3TF8FWWecx67z6FQM8aWFJD78O2J9r7oGW+ +fFIb+ccq5lrZDdMOZKeSYxPj8RxEZloL9mf43eMdLFg= +DV35hS1I55lzc8Rx3j2gKq3qpS4DGEY60WsG/QAoTXw= +tbVOTg+NfLlTyWxlgN7BTrKZG0+dC3Pjrp6KzKvtS4A= +u/w7lYvOA4sXyLYt/Ceg+dy7vbWqHKAUinXlMI+xkz0= +8NeoLWUN/HbOvEj4zYku64g6MsBHB3pIxUY9k/g46pm3Nw8eblpWcMTKpfvJRflp +661hZf7vhUQ+50okfwfTXw== +o+/LpOe0noTt0q9uY7caTYSaVB9I1gwyK4s4NWQ7NUA= +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOERIoWZY07F1DaLs8KSF2NE1WKeF83y1sHol2N4IutXXg== +1u+XjG/2+GSQRv6EzCaWRQ== +sDEbfywocoesN89hhGJfbn+Up/JgL1Qg2VHfdBaVSh8= +bdMrmpuFQ7yM1zhu+h/eLpcnNtalyd8b0N7ueBW8qxJW2cs9cWz74j8D2CGsBGSf +7ruGyOsuKMUu2VJjD7qOMeaAkd8lbFxmFgrVLxFs0oPIjIIPvkx0ZFyMjf6oRSyt +w4I7mVFZGDKiBbcb41eQkxM4nnk+e7mIaguT0WGkkVtz+XhNeocu4Dwlis2SGs+uEVlFlJ1TTROvWNVda6V51XiWTQVNeeq+mwE7orSeGPlP88A2APkdQ7WXcpGsRZSeQTuqKraUSJATMjQccjEKUw== +VmVrGQo2zRokW/ZuO9bN646KfasAt/rPZ4Mzei/x9X4PK6kawVkqvHaQdE9ItYIrN0R1SNQyDXs9q/cle8yTU7duHnPCfZk7g+UKQZaDYdoQ1egSYJLdZGNCNlpNm7lOjvo7TlphOadcZr0WUZLKDg== +a3jbUXPmyR9Vb7Xvsw0w8VcvNLwUIPucqyBxSewCjvpNGf4qVC9bC/Kq8Ub7+IjB541pebM9XViq+TAyzXBMtnoEC9wqaxeFrEhoP73Ucb8zxTbkd8fs/i4yUk10fuvtF4yZG4ou3qTgQ9i1iaCFJRjDi7GvIiufx6zM8N7OwL4= +FEZSdYhAV26PfOeirhfda8xXkIgxZCfPmuVck1Top9Y= +1u+XjG/2+GSQRv6EzCaWRQ== +dPf7g+b79gcCgal9DX4pOJHyAfVR/z7s6ET0GpBby7E= +0OsCC+YpIPphU/YpLoF2Kzv8rZ15USoyP9EDEDNoe4E= +lNoJsK2hWEGOaXNpZFMEKYAMKxXzTQglIlFqVZ/qv90ift2NxK3uK30zwQMR/g2E +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN68kG9x3f+w5aFN8e4/3SB7hYxvzV8Iokc8k375ZmlVH5 +VmVrGQo2zRokW/ZuO9bN69n2IH9Vunz7hXYmWIVxfwPXrXJCVKsHyYzEby8GZS9u0PX7VZIVelGtG52pT513uo3a3aGreyDkMtDppIaQp1w= +VmVrGQo2zRokW/ZuO9bN64wauWr3TvqFolPCIg7VtlOeTNs/abgaVrpCuUHCHrpv +VmVrGQo2zRokW/ZuO9bN6xoSQ3etHchjGzx9GgAoZUY1EPK486h/g4GcxqyTNCwQ +M0ZySqkmhuHCw6olbCKv97KlG7pHnkVH5PzxCv3SOm8hBQ62OlY+Ty/yX79KJJrdNu+CHQd3CPIkR67gsrZxGblpwz5ac1JVqyFJQsp3MvnS9K1ZYGSHoLMHlhFGV3zU +VmVrGQo2zRokW/ZuO9bN64/n5EL8V7CIqhp9FPzHEuQ= +0OsCC+YpIPphU/YpLoF2K6FQb9eutkS0W+twNd64F+O1GZYjvNnJ0aTvS0PBP4BFzU9cR9pgLcItJ3/8oa4lp9NS50ZiYATfufhv/OY4HvuGJ1DtfPl2w5Pl47TRv+1Q +nOflcriAnpFPxVtzU4aISA== +xUiJWUyMvL3eKTICfxM7FHlF8+ynsJSeQ8zvbn8P6ldzdd+1cSzmI5LPDCIBQ0j1BMY3iQTiHHXKXbMlTNzbIg== +4lm0ybc3aVxLaNQODc9aBanfiyaNlwa5Y83cl2wzTJM= +GJNoiDRZuv2t3D6WjWIl+qITVcmlxn4NEeTza92NOQJaBcd0iFalRE3MrJWhGMGx +VmVrGQo2zRokW/ZuO9bN63BJBJEXm+rc5N8L1OHvM55cAO+Ku+qj4muqvW20+pVMp1wYb7OOSX3teOhakn3wUJeQSJL3mZUL2AZMqu0/OdPAAjhra4OMDXmn6v6Poq55Oql860T4eKYpMhnUuGUa3A== +ACEEdgL/kNbx7RaZcSRxZL+LzF2dVZa5hwngNS0Drfw= +VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM= +lGY+HGJ/DlLufZHWWlj7U7tQubWSU1u4v7fNn+PU7V0= +1u+XjG/2+GSQRv6EzCaWRQ== +N2ewPkaF4vBztOl5yvdzXb12XWHEkfuTMQQ/S/IatrU= +DyDHCcahxG8x9eot9BhGGoaSx73c+e9U0Tp+L9OcbF/XbjTh88pSzp7kgiuYM1B2 +7ui7GKVu+hA1zn3iOmo+va7LW68AgDWItaWEVH6a7oq2MogBYQqB9a0ERfgAfeqp ++4NbF4lLQ35m5UiB1/0oM4hG33zvxiKsPHbFeRE//WI= +ErYWeNPhZeZLxfLfOGSlIroR342Y4yDE+5gd4kGeGi+Cxk34mAtGJIhUD+XL5o6JTvUKzpV2ggBzciaY2dSBkA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl6FOpSEGBrcIkh+/0LAnZWFUAa1eWAXdpgj9j9poO/lw== +xljVybfujf1U6lUBrfLztokiw0p3LjxCtcQVbdFJCh55nLdKbOwDf5U9mlEssvrw9UuksdylMWv6nUbn2eAV3yXUyFjGRXqSEHmnN9KQFEI= +p9oVsBgcyiBMjDb8CcPOqN8UTrGCDDQu68V59h0pkaI= +L5I5tdTeebwyRuKjqW19YSTNGjiTv77wwjwChdXruX5PCPlB01g/8mzPNbcCV4Ej +JXk5JLKM/1JqEbD+ffdNPhq/wJlzDLI+uOyyhRBAqdkc8glvKXYuquXWB3Up9JjK +rybirQdlAeHusp/N7GM0sKNwBki4q4kuOQGfi5/04eredJsBktFstkBd+UVmjZ93 +/VHZ8wcOZ8J40IbgQT8a2I6Oa7H1i8uh0gQGQdXA45vcac/EKnJbhl/dTCny6O14DZIAcyGMJrA3ITncE4RtoQ== +XUWmzg0XlHIPZ8hF18QkYN0Aztefgf1QQvjhvSQd5GE= +XUWmzg0XlHIPZ8hF18QkYC7r88uJMe9MygkEWgedRMbFHgDUTHr50CWK0NfmfpFCc6v2SCxA+oYHMSSmCYEOQg== +XUWmzg0XlHIPZ8hF18QkYF7IYI93/oShp2QkjWEeQ6Ylogc+jUYWb7HAIj/dGE0fQYvQvYXUxQRCOx7MQgdI/IR1Y3tGV9ak6FKSK7TxiFkwQPI6UjElsQAwX6TenXLq +XUWmzg0XlHIPZ8hF18QkYHN/4/UmmyCivB+qOWB/viXjMw2wutt9QBKK/B8cA/K+4lFQn9SaCsqotrw0lofRsyLyy7wkeHruuK8v22leO67Nc2KhqApN6oCYo+Pi0nM/f9QBmbzkNPV3VfiLJiCN4/pIseXM4tngIvCXd8ZM5Kw= +jU/UoCJH//JN+eJXlaFsvzU8G2xBEArehtnC0GEU3kba88wtG61nlrgiA1vZDQqQbOZZmS5jYHUfwpgNaCOdal4h0gBZdfVDpEG4TnwcSco= +LjE79g4isZOsOnkgBUQGVvXGCw/Wz+rnAp975tiAu3U= +1WJNrGwGz37apsG1QXCNVX6sT/6pveI0wT7pr1N/pmU= +0kmS0E/2gTphgwOZpwAE2xWwxocpcu6AhSCb7aTGMvUAH0AwAt/+sZO/gEL4yiLF +4uuueBhid9aJeYpW9TknMTKFhi7c1ZuCDkAynCPWLEE4f5zCoL+aW6sgf4jEjnJZkRv4BgsIwv5h+Nn8Ll5k4uJ3HJUccyzNzNMSKu4HZ3E= +1u+XjG/2+GSQRv6EzCaWRQ== +1zukBHMguVb5WvA8m8HQjPjAhqCqUt1LhX5PoNgDmCwZWDEDFPUr61tKw2fBGv8i +R2NB8DN3pOWXcW6vM0ClmEMoF71ke+0fKsaH+rPi+7Q= +Gbp0us/o/RYxNCpypte8wkDs+L9ibC0lB6ODeIGeBUP1ypIAedJqMAUiTZngRtyPG7/dM83k04CPmAFPULQl+BfCYLWRfSKljUfitGeJbyO49RScFIauKLN77UJb69nA +uIsEpyb/BmTr4pwkQVVUf4sU8Bq17UUXFXa/GD5UzvsLsnfQOtQoQPl3At3HFxd9/jb+Yo4rOUkT5964mXVmAiL4B94ZUQ2ZYbYDeUMpDvSd6WZhArMZhfRTWFLLnLhM +9AUqsxjSX9s1jg3GUS+NB5ufD2BIObuhk4izDv68g49BKR0/lv0Y5YYfBQ98x9ip +MzOj4ZiBOJDNVP8vi/lFQHphDQLizMa0vztkd1r5mDy3IP/1XcaniiKAbPj57C+GJJ3I3CaODTQcpMe9YJWXTdxmyXbeuTmk1k/H/vMUQZdYd39tKeEk9fc7cJ5GDQYxYBbkxgZV9sHPPW7wy7uurg== +4QQ/zTZZdnopElj3JQHNa9PygZa4ZCO9taMSrns8R7Q= +CAhn8dRUItEbEErp4w+lX55YSYfu77vGi60UCUzYYfp8S9MgKw/toqSe6FMs2M1v +1u+XjG/2+GSQRv6EzCaWRQ== +1XSH7jd9+l3kxs8yJf4VMU+0Br69HiFgxoemVlEviZQ= +1zukBHMguVb5WvA8m8HQjOPEJCJ9+AzZ9DJkuqA/jaCsGXgslAqdvnFUngarGbeG +Gbp0us/o/RYxNCpypte8wkDs+L9ibC0lB6ODeIGeBUMOi+Fkj8eNjbINDM9sx2l63D4tJlRS3ZzbvYtl64FLqNsgxiYtNOdd98lUkskaJlI= +MzOj4ZiBOJDNVP8vi/lFQHphDQLizMa0vztkd1r5mDy3IP/1XcaniiKAbPj57C+GJJ3I3CaODTQcpMe9YJWXTdxmyXbeuTmk1k/H/vMUQZdYd39tKeEk9fc7cJ5GDQYxYBbkxgZV9sHPPW7wy7uurg== +b4OJVZe8QyIpjuTpKXDL9A== +n/Vg/an64od1JqfYD8zjtsZlIrtOVreSEto4JRdo972fXSPXC9glbleBdvuvJyBc1FrgvcEbB4s3L7BZSj5T3/i8V/IrVnojvAizmFbsfXyL5wPDxuQqWfNYU+/A8JC/ +iUEDlyYF71b358pIL4FKL4uQouNP3knkritXeMCFqD6RcAocvu7FrPVBCb4SQVlH +JomK3KjQVhm7axi7Z5yMpzSzedclpQ0EPWf4a5slcp0= +ynmt6S+38NUiCOOHWGlzRziAza4a5udNX5tgg2hzQ1k= +MIOAnnldxAFaiMyLbIEH0zOpkRUjLKhsZU8SfkygKU8= +xWoGNWjKGPfI4gq8aHoTfCbeqO6O7OYgG1AC8Y9kBQW0BOAdclPvoPuB3UjdhN0TRKx8JizDm/NQEXrzbcwHmA== +1V2v8QerKOmubvSxgB4eTDu/hxOotI87lxS6veME3Crs2k0u4BS95hFjA9PZArPg +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjaVq63HF67baKhTwF5Wt9FBPiGnoli/Hkn+AIFnauccJA== +F9Sftf1PoN5P+lgvc+r10OJ80Vg6iPphyrOoh/5x4bwMegQKx1lxJt2/4i1MN1Vk +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjY9DrK1RwfWAFBMl0E5L7BGp6Et4s9ptYJscetnOWBhjg== +F9Sftf1PoN5P+lgvc+r10GxubuveiTDZyIVny6m+75S9YktzS7qqSq3X2ySJI1VX +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjYzzneH/VPa53Tze+qkheMyIZg41dkiAdfgOiX7zd+woA== +32pdC9DD05OE2l0oXazDFNZg85sdZ06B0olaVOjcsr2UCUkKahTATxT0kBv7/uMo +n/Vg/an64od1JqfYD8zjtvTGvwYcoCTsdR9KI4oJEXjHrKKo/lK6NN3u1IyRnVmJxAC02FuWxa549NUc0lWSZQ== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOGOyBnf2jbqVHlcfZ4m8qhump3EwFJZCmZalXR+GylZU7Im9Vmh/sqbHznArZthWWo= +1u+XjG/2+GSQRv6EzCaWRQ== +1zukBHMguVb5WvA8m8HQjDdAZWNCBhb/LRFKU6AoQAPnDsBbWhpceRgHN0OMdoTg +XBwzd37QS0Dep4YK0TJlr1JCCGVIs7DD9eBryOC6u50= +mX7fgLhy/TJmbe7Pm0t6D4rgTdc0/jMdS8+hY9YrX9k= +ACEEdgL/kNbx7RaZcSRxZC+bHLMedAa4IlPYPErxwbQbb2bCimUzVAoWiGPSZjhG +J1UGlXkKhKXD36OrXXhN8DRNLBzSnpnR9zem4Hl46aYT4DaL8TwLah64w6PefGG35pZu+O411OAUqUyJw60qew== +iUEDlyYF71b358pIL4FKL042CE27KtREMjdeW1oXex7KFuFnXXodSBFBSaoKtMYZ +JomK3KjQVhm7axi7Z5yMpzSzedclpQ0EPWf4a5slcp0= +n/Vg/an64od1JqfYD8zjtgR84pQoEprp4i1Z7jZ8hf4= +VmVrGQo2zRokW/ZuO9bN6xBSBKdKa/mCsbRch2EchZY= +VmVrGQo2zRokW/ZuO9bN64/3v10sr+z8K0riZpxmj3dhtt9I77P5EN2qjRUN9+HZNXT5BzWZZUZQ1+4tzdGNlQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +7CWMhMuDAW1zoceP42HFI7JkOskde8GnIX1Kh6Fk5FtQiq0ZdpGNGcR5RDi7Pioy +ORph4c0b9qayU8qN/Z+ZPvufkaK4ZP6Z5pDSWcXufAEkqWlcZmYJvpUATBsncavX +uTEK8Ng11d3ix2pA+DD/aZwn7g4EisNaVgVRLiZl0yo= +32pdC9DD05OE2l0oXazDFJkxspwkL8t0IItwxjfnC8nUjRvvtzFDV48AlYBtpE1m +4gO44pHRPczUcmpB4bg0pnQGVtS7ogyMz4MAeBHcMtAxe2eF9/DiCFsLtpuKiL7B +uTEK8Ng11d3ix2pA+DD/aQrOlBP0FfiwhlnNZOyjgoo= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6+Iz01H3FqtnAgb7mhzu25zCCKQUPgasOEYO93p0OIwe +VmVrGQo2zRokW/ZuO9bN65rD4nI0MfwzlsBII0cdPaa77aMSTcf8piJKRZe3l5hL4FCHh2Pq7Gs/Wmj2RyJLsNkLZy9ia0AMqcf6dGGGEbVe0xvfm155GJTo9663VjJzFiKsJi7SJETzvnTYIwcltfuO8Uiv6IhgN87wNdwKZck= +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6+Iz01H3FqtnAgb7mhzu25zjxGKQtsXlvqkagztbW1AG +4gO44pHRPczUcmpB4bg0pklZgQJXm0A2Am+4pJfHwxIxdfNIgu3xCJb1c+nvhf0I +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +SIVhMVja35cNhEoyyFHyU5ueFbyMySbyi9mnmmhpxVU8hpsEV8BTtrKQwQni3PGF +q4nc/jwATOMUyfSjLibfEW5DCAn6VBbRQRnaOjh7eA0= +WA9gm/QP+j5PVCbJRTSxYwp3kGb4cMqf+qwJUWVEEPnmqCBjvCtbmGcOopjB+jck +UBbQHMxDYmM54VXbqfW20S22f4jK6BUOrIYt632vyLE= +0OsCC+YpIPphU/YpLoF2K65mO2fOEgg9BDORJsJnsukfPkOuC1C9HL5KjkD4ozjN +v4Cu02hZRDsW+85CLRBC5+QhixIrX1sjRbc+k4FIGXQqvaBnxkvwl7N6zF1jmyIx+QaQW6LMymcXOKxyLELGjoDaY5dHZslh+zvOJ3nOF4o= +v4Cu02hZRDsW+85CLRBC55x7KgPFiTIBJuK7Bj8UUn6kT88eAAMKmh4eP+KGfruG +v4Cu02hZRDsW+85CLRBC52M44iq5ZlKYvAT50T1VVUxbP+lYYf2HLRnTkcnOv/7RLTiXmmYbKFrtds9nTvmC78/4+GyXpy//qMRxrz1Dp81yXvKLN7kSquN/lGm0CbyIuiiLCxeHe7ZblRuSW5w/ew== +tWSLqD1lvEP9Rpp6I27PpvXrraKAQUvuzpkWxLOkdYVAxT6x12Fs+vuA/HccQbq+SMpYqdMtX58R+H1Pdla69g== +kJxoTVctk+bGjMYgKxsjjSmeY05Z0U8C6JFZmjUMMCSxIzLqQNB1ftjf5Kn92nPVt550MwxW8IyDCqJ4Q9eC3FQUIBmzYkMvbBDJNih80rnEO1NVUGYABtrZq/94OTpqywj+vwL3UkXPUL3+35U/jA8nAEIOSPnzN1fJFjnTR3Mx+gPRivpe27HbNFgHbZPg +VmVrGQo2zRokW/ZuO9bN679dY2ryxqp4ouOaU1fOIulOqWd5KUkmwSOGfFCgfWHwgteTq1jrO4iW0E88BvDnPw== +f58SBztD4UCgJ/6kPztVo29CIZDUF3sUhAF1SPuV/14= +lGY+HGJ/DlLufZHWWlj7U9tymVCSNLDh3vOFZaRUBuc= +1u+XjG/2+GSQRv6EzCaWRQ== +nfkJ3QMSmeMN6/r/kF1fGVfrMpks76Z9i1HsARq3TTrMiMLoOh4ZCWV7E4Vz9LdZ +b4OJVZe8QyIpjuTpKXDL9A== +HUxaWbdFNk9pZ+7x3i/kLN34rJMFESnZNqgFnOTtmncZ9MojyT4ik/ZLHKgOBvG5jYiwtWKmCbv5/tg2EykmXA== +NhnIR3Ilo4H2su9/cTNo/Ps3vYSOiOY6esn1xeoWmAlrdRFFnm+r4q+F9O7KjdciOkRcYwkz4orBueV5Pha+Cse7WK7UEu4tjn7yjq09/pc+cqG1kgwWKp3q5L/lI5vjJjeYoh0VOhmY1zWisiB2GA== +NhnIR3Ilo4H2su9/cTNo/IbXc6jmjXpdg3/b5Q0M//5RfYowE2A9IKSdANsXKONc +xWoGNWjKGPfI4gq8aHoTfDMe4+Xx9fgEfvyQdRt49XptGFFda7+W0fuqWp8j+ScVv1QIofgDM/c9givgAZheB/DaAF8Pow88H+SBfdPZZ4jcj49xI9n48vcxq/0T0GE740KKNSWuK4oLY4A7yJGVoQ== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmVQbPz0K1B+JfSV5r/HF6jqLH8cmj7UBpEkhkC5vwSdJiX6F5/J3ugOxIGdW0OYNc0= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU15HDjUiZ7JjFyYpKbXtItUI7uFAiW+e3188BVvbtoYsMsrFB5QSqjIo1lExvyACE= +1u+XjG/2+GSQRv6EzCaWRQ== +KiWYwpJCvVh7pD2s6nbEY7wXqZOCFGVUVZew5Gnpwt8= +1FfqRJIaTv+Hy0UdnrEpXkay2ZR4P6JEneOylPoQjT4= +zuZzq5dzvL2MRNAsfG0GjGS3XRmeqy+aZeovGa0ddfmPuMT372mCGqv5UCgCM70U96xDK5ggO1vY7bxu4j7xZw== +UV6WV80R3POE+S2K3cNp7P8Mv8jJnUdC1uM4InSlYRdgWjmsFAY1Yv6SAghLmt+Z1uNYJTE0FCHSaq2WCw+hAD1VlStivegR9nS5eYiwWTbHvruE50WCRVePmr2csL9FBcZyhOMdNkh6yyet8BjdTWVSVE2DvEeOPGZweOjUuVfdyssacYMY8kpmMeAXJCQZzEKf8FtQyhxu4jwtGrWL0zOkZ0wc31ErPE5IJA9N7PaGOrVXr0cAuoe41Ln59cR8 +Z6/7wkt3BMqq5Y+jFo+XDteIB3YuTrio4xDrY/2mxFGTQQw8rj+Pf/07PQWC6gVc4/AgxP21txHQ55SSLduaLntVy0GJ2oZ1QThqCJYi/ufkZzyq/RuE2mhC9XeesVnY7KYQNLHsRETfFbW9qQwBcou327Gu6KyBZX0FCiCKdOkrgi08pT1UhEfkPl2xtl6n +OlFvTKqdeKF9w1tvvZyIDIlrvmeU2j5RIq361TZK32CoUU7Sp0y2L69Ua/cywMdp+xGru68zvCs9UX+9aSDAWdcgeqHTt075jI3L1avZ+IJzUumjKqoMSy/0Gz0O2WXL/A4k0Uy0LdNUuoGLQnik3CKOXgkYz4ehwQy6SXtjp1oxsqwM9s9nWL2zXBq84gip +gIITPXcfsbW14W9koRVg6+ks1MbgMFupDxmIAXQD1+wQRYZtxytn4ZO0xf8FvXU7v6FMbDhOVAMvZ2gNlztbwouh64TAdrICnyRUdjS9gKDjFXle/XzV7PYyDITAX8Ncgj+hmjXsFIkGWeOBwcvW/CVhFE7DKlDfxEh8ey+9cbo= +AE2D69YCySZnqC/KuUyd6AGsybxWLngSTsMfLA1YPCDQ8OFTsaFWXrQdusJJC4BgaslHwTfLZ4zb+ogwukgpSIAIhczFRgh/FEDYi0hg+gD2GhIBJ0qhSVsEPRLkD8eLCfeGs6UeU/NnAjJ9oDU1cg== +q86vJ1Gk5zoQJkWiDC3EDHW4ie7aSVy+iRJ3e5uBreLs6ZQtRFcPVSGF/trh4arUs8mDj8ng6qc2rwvI6mN7jTnI71Exksa2VyI54IXv4nv+BB+4yTO6aodPKVdzify2IBKNqfXxRXk6ilDU1vHO2rkx95moNbUc6kyrj8IbUqd60QYXhI8CjvMxq7HH1Q3MzHDUyw6fbHDq7qnF+Rv3lo9GFL9SrjekFvd0SOebGZI= +zuZzq5dzvL2MRNAsfG0GjKackRzN4eqfmNCRlFdHKtoBdzMtMyHhousMEQBwm83oe/xgM3D+/pZgpf1SjYBf60uCeaj83bT8cJail/HfXaCx3s3PPxj8nCUhsIV2rZth +VwBeeHn0i7Rn77yuYYaoYAGyL2u8AE+N89j694UfCTBSuvToVWInwIjwpY3O5TTlXMYhYkXrEoWyIlLdm7CYSHbtthxJ8Pj0kfd4KtUDR0Aecm8yBUCIL4LcpHPJTU5bWz/wO1BA+kb2c6vsNVsZnA5YkucErGu/sIb5b9FARGMgb2gbl/S1DHuet5hagyTl0HWyCrmt9gWiJJCZWlFaMBajwj0n5Hz3Mfd0v5n+jEo= +LDoWyYEVft11g2fMaUoWVhpfUwmL6xqf/f8HKcv/obIjZ0iSUxIWeEALc64BWdSPh2owyFLaPfUr6VN+kAteQbvubruXVrEfzmIiKpMrJ3KJUhLMFh4qb9B9mZbeDsjWMF47MT2oithWcDJ/Gz/RJA== +qzAWBJSCd8usbTDm9R1x/MXzPL0m0Ud34Ai1Rm7vYQS0Z/5HPh7Ulgw2tiTRxDZoyNlyAAsEZLXIOjGA8iawFQLTD8emXrTP9qRXpa6atJPleFgDNTG9RcjjeaHO0zVFO3yo4ivEgRqjHAW8Ia5LYGC64Ot0c0E9ei+Ff9nFBWM886RF+jVAdZonG3r+IoU/R8wlmHi0CJufxZVaAzkMHw== +gDAZChPAe4zQXRH7y194sqbv3RpQagsxOjI/3tEGN2kNVglD/S6+7T+d1UEDnLy3FbRxzTJc4aRq7ttywGcL75jdKodi/K4ANqywo6BII1NpkgMWQPyhWLLkMtLdIJiG04maEg0DkaXb2mWJtdKQ7BI4wJ1cxITvgvzlLe4c9yc+hOc1qKaBJXezPFVrYafL +661hZf7vhUQ+50okfwfTXw== +FEZSdYhAV26PfOeirhfda8qGB+2DE0ehPq4imkXwDHE= diff --git a/class/msg/dingding_msg.py b/class/msg/dingding_msg.py index 033138eb..504b07da 100644 --- a/class/msg/dingding_msg.py +++ b/class/msg/dingding_msg.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # | Author: lx # | 消息通道邮箱模块 # +------------------------------------------------------------------- diff --git a/class/msg/feishu_msg.py b/class/msg/feishu_msg.py index 2a489478..c71877fc 100644 --- a/class/msg/feishu_msg.py +++ b/class/msg/feishu_msg.py @@ -1,8 +1,8 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: lx # | 消息通道飞书通知模块 diff --git a/class/msg/mail_msg.py b/class/msg/mail_msg.py index 488ab061..9f14bf93 100644 --- a/class/msg/mail_msg.py +++ b/class/msg/mail_msg.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # | Author: lx # | 消息通道邮箱模块 # +------------------------------------------------------------------- diff --git a/class/msg/sms_msg.py b/class/msg/sms_msg.py index 13653c54..ee0396e1 100644 --- a/class/msg/sms_msg.py +++ b/class/msg/sms_msg.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # | Author: lx # | 消息通道邮箱模块 # +------------------------------------------------------------------- diff --git a/class/msg/tg_msg.py b/class/msg/tg_msg.py index 4df51fe2..d0c8f9c6 100644 --- a/class/msg/tg_msg.py +++ b/class/msg/tg_msg.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: jose +# | Author: jose # | 消息通道电报模块 # +------------------------------------------------------------------- diff --git a/class/msg/weixin_msg.py b/class/msg/weixin_msg.py index ded85e7e..c001347a 100644 --- a/class/msg/weixin_msg.py +++ b/class/msg/weixin_msg.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # | Author: lx # | 消息通道邮箱模块 # | 常用功能 diff --git a/class/msg/wx_account_msg.py b/class/msg/wx_account_msg.py index 9f1a0cc0..5d25e681 100644 --- a/class/msg/wx_account_msg.py +++ b/class/msg/wx_account_msg.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # | Author: lx # | 消息通道邮箱模块 # +------------------------------------------------------------------- diff --git a/class/nginx.py b/class/nginx.py index 44bfcf22..c96361f1 100644 --- a/class/nginx.py +++ b/class/nginx.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -38,7 +38,7 @@ class nginx: gets = ["worker_processes","worker_connections","keepalive_timeout","gzip","gzip_min_length","gzip_comp_level","client_max_body_size","server_names_hash_bucket_size","client_header_buffer_size"] n = 0 for i in gets: - rep = "(%s)\s+(\w+)" % i + rep = r"(%s)\s+(\w+)" % i k = re.search(rep, ngconfcontent) if not k: return public.return_msg_gettext(False,"Get key {} False".format(k)) @@ -64,7 +64,7 @@ class nginx: gets = ["client_body_buffer_size"] n = 0 for i in gets: - rep = "(%s)\s+(\w+)" % i + rep = r"(%s)\s+(\w+)" % i k = re.search(rep, proxycontent) if not k: return public.return_msg_gettext(False,"Get key {} False".format(k)) @@ -88,14 +88,14 @@ class nginx: n+=1 return conflist - def SetNginxValue(self,get): + def SetNginxValue(self, get: public.dict_obj): ngconfcontent = public.readFile(self.nginxconf) proxycontent = public.readFile(self.proxyfile) if public.get_webserver() == 'nginx': shutil.copyfile(self.nginxconf, '/tmp/ng_file_bk.conf') shutil.copyfile(self.proxyfile, '/tmp/proxyfile_bk.conf') conflist = [] - getdict = get.__dict__ + getdict = get.get_items() for i in getdict.keys(): if i != "__module__" and i != "__doc__" and i != "data" and i != "args" and i != "action": getpost = { @@ -105,13 +105,13 @@ class nginx: conflist.append(getpost) for c in conflist: - rep = "%s\s+[^kKmMgG\;\n]+" % c["name"] + rep = r"%s\s+[^kKmMgG\;\n]+" % c["name"] if c["name"] == "worker_processes" or c["name"] == "gzip": - if not re.search("auto|on|off|\d+", c["value"]): - return public.return_msg_gettext(False, 'Parameter ERROR!') + if not re.search(r"auto|on|off|\d+", c["value"]): + return public.return_msg_gettext(False, 'Parameter ERROR! -1') else: - if not re.search("\d+", c["value"]): - return public.return_msg_gettext(False, 'Parameter ERROR!') + if not re.search(r"\d+", c["value"]): + return public.return_msg_gettext(False, 'Parameter ERROR! -2') if re.search(rep,ngconfcontent): newconf = "%s %s" % (c["name"],c["value"]) ngconfcontent = re.sub(rep,newconf,ngconfcontent) @@ -132,7 +132,7 @@ class nginx: def add_nginx_access_log_format(self,args): ''' @name 添加日志格式 - @author zhwen + @author zhwen @param log_format 需要设置的日志格式["$server_name","$remote_addr","-"....] @param log_format_name @param act 操作方式 add/edit @@ -151,7 +151,7 @@ class nginx: conf = public.readFile(self.nginxconf) if not conf: return public.return_msg_gettext(False,'Nginx configuration file does not exist!') - reg = 'http(\n|\s)+{' + reg = r'http(\n|\s)+{' conf = re.sub(reg,'http\n\t{'+data,conf) public.writeFile(self.nginxconf,conf) public.serviceReload() @@ -162,14 +162,14 @@ class nginx: def del_nginx_access_log_format(self,args): ''' @name 删除日志格式 - @author zhwen + @author zhwen @param log_format_name ''' log_format_name = args.log_format_name conf = public.readFile(self.nginxconf) if not conf: return public.return_msg_gettext(False, 'Nginx configuration file does not exist!') - reg = '\s*#LOG_FORMAT_BEGIN_{n}(\n|.)+#LOG_FORMAT_END_{n}\n?'.format(n=args.log_format_name) + reg = r'\s*#LOG_FORMAT_BEGIN_{n}(\n|.)+#LOG_FORMAT_END_{n}\n?'.format(n=args.log_format_name) conf = re.sub(reg,'',conf) self._del_format_log_of_website(log_format_name) public.writeFile(self.nginxconf,conf) @@ -227,7 +227,7 @@ class nginx: format_name = [i.split('LOG_FORMAT_BEGIN_')[-1] for i in data] format_log = {} for i in format_name: - format_reg = "#LOG_FORMAT_BEGIN_{n}(\n|.)+log_format\s+{n}\s*(.*);".format(n=i) + format_reg = r"#LOG_FORMAT_BEGIN_{n}(\n|.)+log_format\s+{n}\s*(.*);".format(n=i) tmp = re.search(format_reg,conf) if not tmp: continue @@ -240,7 +240,7 @@ class nginx: def set_format_log_to_website(self,args): ''' @name 设置日志格式 - @author zhwen + @author zhwen @param sites aaa.com,bbb.com @param log_format_name ''' @@ -248,13 +248,13 @@ class nginx: sites = loads(args.sites) try: all_site = public.M('sites').field('name').select() - reg = 'access_log\s+/www.*{}\s*;'.format(args.log_format_name) + reg = r'access_log\s+/www.*{}\s*;'.format(args.log_format_name) for site in all_site: website_conf_file = '/www/server/panel/vhost/nginx/{}.conf'.format(site['name']) conf = public.readFile(website_conf_file) if not conf: return public.return_msg_gettext(False, 'Nginx configuration file does not exist!') - format_exist_reg = '(access_log\s+/www.*\.log).*;' + format_exist_reg = r'(access_log\s+/www.*\.log).*;' access_log = self.get_nginx_access_log(conf) if not access_log: continue @@ -272,7 +272,7 @@ class nginx: def get_nginx_access_log(self,nginx_conf): try: - reg = 'access_log\s+(.*\.log)' + reg = r'access_log\s+(.*\.log)' log_path = re.findall(reg, nginx_conf) if not log_path: return False @@ -309,7 +309,7 @@ class nginx: if not site_format_log_status[s]: continue website_conf_file = '/www/server/panel/vhost/nginx/{}.conf'.format(s) - format_exist_reg = 'access_log\s+/www.*\.log\s+{};'.format(log_format_name) + format_exist_reg = r'access_log\s+/www.*\.log\s+{};'.format(log_format_name) conf = public.readFile(website_conf_file) if not conf:continue if not re.search(format_exist_reg,conf):continue diff --git a/class/ols.py b/class/ols.py index 8fb22f7c..cb0364e7 100644 --- a/class/ols.py +++ b/class/ols.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: zhwwen +# Author: zhwwen # ------------------------------------------------------------------- # # ------------------------------ @@ -26,7 +26,7 @@ class ols: conf = public.readFile(self._main_conf_path) data = {} for k in keys: - rep = '{}\s+(\w+)'.format(k) + rep = r'{}\s+(\w+)'.format(k) tmp = re.search(rep,conf) if tmp: data[k] = tmp.groups(1) @@ -43,7 +43,7 @@ class ols: conf = public.readFile(self._main_conf_path) data = json.loads(get.array) for k in data: - rep = '{}\s+(\w+)'.format(k) + rep = r'{}\s+(\w+)'.format(k) tmp = re.search(rep,conf) if tmp: conf = re.sub('{}.*'.format(k),'{} {}'.format(k,data[k]),conf) @@ -58,24 +58,24 @@ class ols: data = {} sitename = public.M('sites').where("id=?", (get.id,)).getField('name') conf = public.readFile(self._detail_conf_path.format(sitename)) - rep = 'expiresByType\s+(.*)' + rep = r'expiresByType\s+(.*)' tmp = re.search(rep,conf) if tmp: tmp = tmp.groups(1)[0].split(',') for i in tmp: if 'image' in i: - rep = 'image\/\*=A(\d+)' + rep = r'image\/\*=A(\d+)' data['image'] = re.search(rep,conf).groups(1)[0] print(data['image']) if 'css' in i: - rep = 'css=A(\d+)' + rep = r'css=A(\d+)' data['css'] = re.search(rep, conf).groups(1)[0] if 'javascript' in i: - rep = 'javascript=A(\d+)' + rep = r'javascript=A(\d+)' data['javascript'] = re.search(rep, conf).groups(1)[0] if 'font' in i: - rep = 'font.*=A(\d+)' + rep = r'font.*=A(\d+)' data['font'] = re.search(rep, conf).groups(1)[0] return data @@ -92,12 +92,12 @@ class ols: print(self._detail_conf_path.format(sitename)) values = json.loads(get.values) for k in values: - rep = '{}/[\*\w+=]=A\d+'.format(k) + rep = r'{}/[\*\w+=]=A\d+'.format(k) old_cache = re.search(rep,conf) if not old_cache: continue old_cache = old_cache.group() - new_cache = re.sub('\d+',values[k],old_cache) + new_cache = re.sub(r'\d+',values[k],old_cache) conf = conf.replace(old_cache,new_cache) public.writeFile(self._detail_conf_path.format(sitename),conf) public.serviceReload() @@ -127,13 +127,13 @@ class ols: if tmp: # 获取排除文件 tmp = tmp.group() - rep = '#\s*excluding.*\n.*\((.*)\)' + rep = r'#\s*excluding.*\n.*\((.*)\)' tmp = re.search(rep,tmp) if tmp: data['exclude_file'] = [i+'.php' for i in tmp.groups(1)[0].split('|')] print(data) # 获取缓存时间 - rep = 'max-age=(\d+)' + rep = r'max-age=(\d+)' tmp = re.search(rep,conf) if tmp: data['maxage'] = tmp.groups(1)[0] @@ -154,7 +154,7 @@ class ols: conf = self.get_private_cache_conf(get.id)[0] file = self.get_private_cache_conf(get.id)[1] if 'BTLSCACHE_BEGIN' not in conf: - confstr = """#######################BTLSCACHE_BEGIN####################### + confstr = r"""#######################BTLSCACHE_BEGIN####################### RewriteEngine on @@ -206,10 +206,10 @@ RewriteRule (.*\.php)?$ - [E=Cache-Control:private] for file in get.exclude_file.split('\n'): exclude_files.append(file.split('.')[0]) exclude_file = "|".join(exclude_files) - old_exc_rep = 'RewriteCond\s+\%\{REQUEST_URI\}\s+\!/\(.*\)\\\.php\$' - new_exc = 'RewriteCond %{REQUEST_URI} !/(' + exclude_file + ')\.php$' + old_exc_rep = r'RewriteCond\s+\%\{REQUEST_URI\}\s+\!/\(.*\)\\\.php\$' + new_exc = 'RewriteCond %{REQUEST_URI} !/(' + exclude_file + r')\.php$' bt_conf = re.sub(old_exc_rep,new_exc,bt_conf) - old_max_age_rep = 'max-age=\d+' + old_max_age_rep = r'max-age=\d+' new_max_age = 'max-age={}'.format(int(get.max_age)) bt_conf = re.sub(old_max_age_rep,new_max_age,bt_conf) conf = re.sub(bt_conf_rep,bt_conf,conf) @@ -247,11 +247,11 @@ RewriteRule (.*\.php)?$ - [E=Cache-Control:private] conf = public.readFile(path) ap_path = '/www/server/panel/vhost/apache/{}.conf'.format(s['sitename']) ap_conf = public.readFile(ap_path) - tmp = re.search('ServerName\s+SSL\.(.*)',ap_conf) + tmp = re.search(r'ServerName\s+SSL\.(.*)',ap_conf) s['phpv'] = re.search(phpv_reg,conf).groups(1)[0] s['rundir'] = re.search(rundir_reg.format(s),conf).groups(1)[0] s['ssl_domain'] =tmp.groups(1)[0] if tmp else None - s['port'] = re.search('listen\s+(\d+);',conf).groups(1)[0] + s['port'] = re.search(r'listen\s+(\d+);',conf).groups(1)[0] return siteinfo def _make_args(self): diff --git a/class/one_key_wp.py b/class/one_key_wp.py index 2eba30e8..d9ca1c7d 100644 --- a/class/one_key_wp.py +++ b/class/one_key_wp.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: zhwen +# | Author: zhwen # +------------------------------------------------------------------- import os import time @@ -73,7 +73,7 @@ class one_key_wp: else: self.get_plugin_page() resp = self.__plugin_page_content - _ajax_nonce_rep = '"ajax_nonce\\\"\:\\\"(\w+)' + _ajax_nonce_rep = '"ajax_nonce\\\"\\:\\\"(\\w+)' rex = re.search(_ajax_nonce_rep, resp) if rex: self.__ajax_nonce = rex.group(1) @@ -130,7 +130,7 @@ class one_key_wp: # hosts = public.readFile('/etc/hosts') # if not hosts: # return False - # if not re.search('127.0.0.1\s+{}'.format(self.__domain),hosts): + # if not re.search(r'127.0.0.1\s+{}'.format(self.__domain),hosts): # public.writeFile('/etc/hosts','\n127.0.0.1 {}'.format(self.__domain),'a+') self.__session_resp = retry( @@ -270,7 +270,7 @@ class one_key_wp: hosts = public.readFile('/etc/hosts') if not hosts: return False - if not re.search('127.0.0.1\s+{}'.format(values['site_name']), hosts): + if not re.search(r'127.0.0.1\s+{}'.format(values['site_name']), hosts): public.writeFile('/etc/hosts', '\n127.0.0.1 {}'.format(values['site_name']), 'a+') self.write_logs("|-Start initializing Wordpress...") @@ -362,7 +362,7 @@ class one_key_wp: def get_update_wp_nonce(self): url = "http://{}/wp-admin/update-core.php".format(self.__domain) # res = self.action_plugin_get(url) - _wp_nonce_rep = '"_wpnonce\\\"\svalue=\\\"(\w+)' + _wp_nonce_rep = '"_wpnonce\\\"\\svalue=\\\"(\\w+)' # rex = re.search(_wp_nonce_rep,res) # if rex: # self.__wp_nonce = rex.group(1) @@ -414,7 +414,7 @@ class one_key_wp: def get_smart_http_expire_form_nonce(self, url): public.writeFile('/tmp/2', str(self.action_plugin_get(url))) - smart_http_expire_form_nonce = '"smart_http_expire_form_nonce\\\"\svalue=\\\"(\w+)' + smart_http_expire_form_nonce = '"smart_http_expire_form_nonce\\\"\\svalue=\\\"(\\w+)' return self.get_wp_nonce(self.action_plugin_get(url), smart_http_expire_form_nonce) def set_nginx_helper(self, values): @@ -557,7 +557,7 @@ class one_key_wp: conf_file = "{}/wp-includes/version.php".format(path) conf = public.readFile(conf_file) try: - version = re.search('\$wp_version\s*=\s*[\'\"]{1}([\d\.]*)[\'\"]{1}', conf).groups(1)[0] + version = re.search('\\$wp_version\\s*=\\s*[\'\"]{1}([\\d\\.]*)[\'\"]{1}', conf).groups(1)[0] except: version = "00" return version @@ -569,7 +569,7 @@ class one_key_wp: 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36' } result = requests.get(url, headers=headers) - result = re.search('Download\s+WordPress\s+([\d\.]+)', result.text) + result = re.search(r'Download\s+WordPress\s+([\d\.]+)', result.text) if result: return result.group(1) return "00" @@ -793,13 +793,13 @@ class one_key_wp: rep_domain = r"^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" values = {} if hasattr(args, 'd_id'): - if re.search('\d+', args.d_id): + if re.search(r'\d+', args.d_id): values["d_id"] = args.d_id else: return public.return_msg_gettext(False, "Please check if the [{}] format is correct For example: {}", ("d_id", "99")) if hasattr(args, 's_id'): - if re.search('\d+', args.s_id): + if re.search(r'\d+', args.s_id): values["s_id"] = args.s_id else: return public.return_msg_gettext(False, "Please check if the [{}] format is correct For example: {}", @@ -1218,7 +1218,7 @@ class optimize_db: class fast_cgi: def get_fastcgi_conf(self, version): - conf = """ + conf = r""" set $skip_cache 0; if ($request_method = POST) { set $skip_cache 1; @@ -1294,7 +1294,7 @@ class fast_cgi: one_key_wp().write_logs("|-Nginx FastCgi cache configuration already exists") print("Nginx FastCgi cache configuration already exists") return public.return_msg_gettext(True, "Nginx FastCgi cache configuration already exists") - rep = "http\s*\n\s*{" + rep = "http\\s*\n\\s*{" content = re.sub(rep, "http\n\t{" + conf, content) public.writeFile(conf_path, content) @@ -1328,7 +1328,7 @@ class fast_cgi: print("Nginx init FastCgi cache configuration already exists") return public.return_msg_gettext(True, "Nginx init FastCgi cache configuration already exists") # content_init = re.sub(r"\$NGINX_BIN -c \$CONFIGFILE", + conf2, content_init) - rep2 = "\$NGINX_BIN -c \$CONFIGFILE" + rep2 = r"\$NGINX_BIN -c \$CONFIGFILE" content_init = re.sub(rep2, conf2 + " $NGINX_BIN -c $CONFIGFILE", content_init) public.writeFile(init_path, content_init) @@ -1366,7 +1366,7 @@ class fast_cgi: print("FastCgi configuration does not exist in website configuration") one_key_wp().write_logs("|-FastCgi configuration does not exist in website configuration, skip") return public.return_msg_gettext(False, "FastCgi configuration does not exist in website configuration") - rep = "include\s+enable-php-{}-wpfastcgi.conf;".format(version) + rep = r"include\s+enable-php-{}-wpfastcgi.conf;".format(version) conf = re.sub(rep, "include enable-php-{}.conf;".format(version), conf) else: fastcgi_conf = "include enable-php-{}-wpfastcgi.conf;".format(version) @@ -1375,7 +1375,7 @@ class fast_cgi: "|-The FastCgi configuration already exists in the website configuration, skip it") return public.return_msg_gettext(True, "The FastCgi configuration already exists in the website configuration") - rep = "include\s+enable-php-{}.conf;".format(version) + rep = r"include\s+enable-php-{}.conf;".format(version) conf = re.sub(rep, fastcgi_conf, conf) public.writeFile(conf_path, conf) conf_pass = public.checkWebConfig() diff --git a/class/page.py b/class/page.py index e8ebf7a3..fecddd26 100644 --- a/class/page.py +++ b/class/page.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import math,string,public,re @@ -200,8 +200,8 @@ class Page(): def __SetUri(self,request_uri): #构造URI try: - request_uri = re.sub("&p=\d+",'&',request_uri) - request_uri = re.sub("\?p=\d+",'?',request_uri) + request_uri = re.sub(r"&p=\d+",'&',request_uri) + request_uri = re.sub(r"\?p=\d+",'?',request_uri) if request_uri.find('&') == -1: if request_uri[-1] != '?': request_uri += '?' else: diff --git a/class/panelApi.py b/class/panelApi.py index 9d6a5b8f..823922a9 100644 --- a/class/panelApi.py +++ b/class/panelApi.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import public,os,json,time class panelApi: diff --git a/class/panelAuth.py b/class/panelAuth.py index 142059e7..9765bb0a 100644 --- a/class/panelAuth.py +++ b/class/panelAuth.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2019 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2019 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -60,8 +60,8 @@ class panelAuth: def check_serverid(self,get): if get.serverid != self.create_serverid(get): return False return True - # 旧接口 没有永久版 - def get_plugin_price2(self, get): + + def get_plugin_price(self, get): try: userPath = 'data/userInfo.json' if not 'pluginName' in get and not 'product_id' in get: return public.return_msg_gettext(False,'Parameter ERROR!') @@ -71,7 +71,7 @@ class panelAuth: params['product_id'] = self.get_plugin_info(get.pluginName)['id'] else: params['product_id'] = get.product_id - data = self.send_cloud('{}/api/product/prices'.format(self.__official_url), params) + data = self.send_cloud('{}/api/product/pricesV3'.format(self.__official_url), params) if not data: return public.return_msg_gettext(False, 'Please log in to your aaPanel account on the panel first!') if not data['success']: @@ -82,61 +82,7 @@ class panelAuth: except: del(session['get_product_list']) return public.return_msg_gettext(False,'Syncing information, please try again!\n {}',(public.get_error_info(),)) - - # 获取永久版信息 - def get_plugin_price3(self, get): - try: - userPath = 'data/userInfo.json' - if not 'pluginName' in get and not 'product_id' in get: - return public.return_msg_gettext(False,'Parameter ERROR!') - if not os.path.exists(userPath): - return public.return_msg_gettext(False,'Please login with account first') - params = {} - if not hasattr(get,'product_id'): - params['product_id'] = self.get_plugin_info(get.pluginName)['id'] - else: - params['product_id'] = get.product_id - - data = self.send_cloud('{}/api/product/pricesV2'.format(self.__official_url), params) - - if not data: - return public.return_msg_gettext(False, 'Please log in to your aaPanel account on the panel first!') - if not data['success']: - return public.return_msg_gettext(False,data['msg']) - return data['res'] - except: - # del(session['get_product_list']) - return public.return_msg_gettext(False,'Syncing information, please try again!\n {}',(public.get_error_info(),)) - - # 获取价格列表 新增多机购买 - def get_plugin_price(self, get): - try: - userPath = 'data/userInfo.json' - if not 'pluginName' in get and not 'product_id' in get: - return public.return_msg_gettext(False, 'Parameter ERROR!') - if not os.path.exists(userPath): - return public.return_msg_gettext(False, 'Please login with account first') - params = {} - if not hasattr(get, 'product_id'): - params['product_id'] = self.get_plugin_info(get.pluginName)['id'] - else: - params['product_id'] = get.product_id - - data = self.send_cloud('{}/api/product/pricesV3'.format(self.__official_url), params) - - if not data: - return public.return_msg_gettext(False, 'Please log in to your aaPanel account on the panel first!') - if not data['success']: - return public.return_msg_gettext(False, data['msg']) - return data['res'] - except Exception as ex: - public.print_log("获取价格报错 {}".format(ex)) - # del(session['get_product_list']) - return public.return_msg_gettext(False, 'Syncing information, please try again!\n {}', - (public.get_error_info(),)) - - - + def get_plugin_info(self,pluginName): data = self.get_business_plugin(None) if not data: return None @@ -183,7 +129,6 @@ class panelAuth: return public.return_msg_gettext(False, data['res']) return data['res'] - def get_stripe_session_id(self,get): params = {} @@ -413,8 +358,7 @@ class panelAuth: data = self.send_cloud('{}/api/user/productAuthorizes'.format(self.__official_url), params) if not data: return [] - if not data['success']: - return [] + if not data['success']: return [] data = data['res'] # return [i for i in data['list'] if i['status'] != 'activated' and get.pid == i['product_id']] res = list() @@ -444,7 +388,7 @@ class panelAuth: if hasattr(get,'coupon_id') and get.pay_channel == '10': params['coupon_id'] = get.coupon_id data = self.send_cloud('{}/api/authorize/product/renew'.format(self.__official_url), params) - # public.print_log('############ 续费接口 {}'.format(data)) + if not data['success']: data['res'] = 'Invalid authorize OR authorize not found!' return data diff --git a/class/panelBackup.py b/class/panelBackup.py index a5e98f8b..6e21c50c 100644 --- a/class/panelBackup.py +++ b/class/panelBackup.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------ @@ -422,10 +422,30 @@ class backup: def backup_site(self, siteName, save=3, exclude=[], echo_id=None): try: self.echo_start() - find = public.M('sites').where('name=?', (siteName,)).field('id,path').find() + find = public.M('sites').where('name=?', (siteName,)).field('id,path,project_type').find() public.print_log(find) - if not find: + if not find or not isinstance(find, dict): raise Exception(' The directory for does not exist') + + # Wordpress + if find['project_type'] == 'WP2': + import PluginLoader + if PluginLoader.get_auth_state() < 1: + self.echo_info(public.get_msg_gettext('Authorization is invalid or expired')) + return None + + try: + from wp_toolkit import wpbackup + bak_info = wpbackup(find['id']).backup_full_get_data() + self.echo_info(public.get_msg_gettext('Backup wordpress [{}] successfully', (siteName,))) + self.echo_end() + return bak_info.bak_file + except Exception as e: + public.print_error() + self.echo_info(str(e)) + self.echo_end() + return None + spath = find['path'] pid = find['id'] fname = 'web_{}_{}.tar.gz'.format(siteName, public.format_date("%Y%m%d_%H%M%S")) diff --git a/class/panelController.py b/class/panelController.py index 0c6a9d56..de08d879 100755 --- a/class/panelController.py +++ b/class/panelController.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -32,8 +32,8 @@ class Controller: if args['mod_name'] in ['base']: return public.return_status_code(1000,'错误的调用!') public.exists_args('def_name,mod_name',args) if args['def_name'].find('__') != -1: return public.return_status_code(1000,'调用的方法名称中不能包含“__”字符') - if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,'调用的模块名称中不能包含\w以外的字符') - if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,'调用的方法名称中不能包含\w以外的字符') + if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,r'调用的模块名称中不能包含\w以外的字符') + if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,r'调用的方法名称中不能包含\w以外的字符') except: return public.get_error_object() # 参数处理 diff --git a/class/panelDatabaseController.py b/class/panelDatabaseController.py index be1cf3c3..ffe87b59 100644 --- a/class/panelDatabaseController.py +++ b/class/panelDatabaseController.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -32,8 +32,8 @@ class DatabaseController: if args['mod_name'] in ['base']: return public.return_status_code(1000,'Bad call!') public.exists_args('def_name,mod_name',args) if args['def_name'].find('__') != -1: return public.return_status_code(1000,'The called method name cannot contain the "__" character') - if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,'The called module name cannot contain characters other than \w') - if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,'The called module name cannot contain characters other than \w') + if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,r'The called module name cannot contain characters other than \w') except: return public.get_error_object() # 参数处理 diff --git a/class/panelDefense.py b/class/panelDefense.py index 63e1b031..a8e3943d 100644 --- a/class/panelDefense.py +++ b/class/panelDefense.py @@ -1,8 +1,8 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: hwliang # +------------------------------------------------------------------- diff --git a/class/panelDnsapi.py b/class/panelDnsapi.py index bd1d3337..06e28701 100644 --- a/class/panelDnsapi.py +++ b/class/panelDnsapi.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # +------------------------------------------------------------------- import public,os,sys,json,time,random import requests diff --git a/class/panelHttpProxy.py b/class/panelHttpProxy.py index a9df1f2e..34b32b53 100644 --- a/class/panelHttpProxy.py +++ b/class/panelHttpProxy.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -29,9 +29,9 @@ class HttpProxy: ''' headers = {} for h in p_res.headers.keys(): - if h in ['Content-Encoding','Transfer-Encoding']: continue + if h in ['content-encoding', 'Content-Encoding', 'transfer-encoding', 'Transfer-Encoding']: continue headers[h] = p_res.headers[h] - if h in ['Location']: + if h in ['location', 'Location']: if headers[h].find('phpmyadmin_') != -1: if not self._pma_path: diff --git a/class/panelLets.py b/class/panelLets.py index 8d431a4e..61abfa4f 100644 --- a/class/panelLets.py +++ b/class/panelLets.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # +------------------------------------------------------------------- import os,sys,json,time,re setup_path = '/www/server/panel' @@ -763,7 +763,7 @@ class panelLets: tlist = [] for siteName in old_list: if not siteName in cron_list: tlist.append(siteName) - print(public.get_msg_gettext('|-[{}] Not expired or the site does not use the Let\s Encrypt certificate.',(','.join(tlist),))) + print(public.get_msg_gettext(r'|-[{}] Not expired or the site does not use the Let\s Encrypt certificate.',(','.join(tlist),))) print(public.get_msg_gettext('|-{} Waiting for renewal [{}].',(time.strftime('%Y-%m-%d %X',time.localtime()),str(len(cron_list))))) sucess_list = [] diff --git a/class/panelMessage.py b/class/panelMessage.py index 0cabed94..67465e49 100644 --- a/class/panelMessage.py +++ b/class/panelMessage.py @@ -1,8 +1,8 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: hwliang <2020-05-18> # +------------------------------------------------------------------- diff --git a/class/panelModel/backupModel.py b/class/panelModel/backupModel.py index d234c2b1..e681e5e9 100644 --- a/class/panelModel/backupModel.py +++ b/class/panelModel/backupModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: cjxin +# Author: cjxin #------------------------------------------------------------------- # 备份 diff --git a/class/panelModel/base.py b/class/panelModel/base.py index 0738f632..ccbceed5 100644 --- a/class/panelModel/base.py +++ b/class/panelModel/base.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: cjxin +# Author: cjxin #------------------------------------------------------------------- # 面板其他模型新增功能 diff --git a/class/panelModel/publicModel.py b/class/panelModel/publicModel.py new file mode 100644 index 00000000..b659d64c --- /dev/null +++ b/class/panelModel/publicModel.py @@ -0,0 +1,274 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: cjxin +# ------------------------------------------------------------------- + +# 备份 +# ------------------------------ +import os, sys, re, json, shutil, psutil, time +from panelModel.base import panelBase +import public, config, panelTask + +try: + from BTPanel import cache +except:pass + +class main(panelBase): + __table = 'task_list' + # public.check_database_field("ssl_data.db","ssl_info") + task_obj = panelTask.bt_task() + + def __init__(self): + pass + + + """ + @name 获取面板日志 + """ + def get_update_logs(self,get): + try: + + skey = 'panel_update_logs' + res = cache.get(skey) + if res: return res + + res = public.httpPost('https://www.bt.cn/Api/getUpdateLogs?type=Linux',{}) + + start_index = res.find('(') + 1 + end_index = res.rfind(')') + json_data = res[start_index:end_index] + + res = json.loads(json_data) + cache.set(skey,res,60) + except: + res = [] + + return res + + def get_public_config(self, args): + """ + @name 获取公共配置 + """ + public.print_log("error 3366666 原始方法: ") + _config_obj = config.config() + data = _config_obj.get_config(args) + + data['task_list'] = self.task_obj.get_task_lists(args) + data['task_count'] = public.M('tasks').where("status!=?", ('1',)).count() + data['get_pd'] = self.get_pd(args) + data['ipv6'] = '' + if _config_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' + data['is_local'] = '' + if public.is_local(): data['is_local'] = 'checked' + + if data['get_pd'] and data['get_pd'][2] != -1: + time_diff = (data['get_pd'][2]-int(time.time())) % (365*86400) + data['active_pro_time'] = int(time.time()) - (365*86400 - time_diff) + else: + data['active_pro_time'] = 0 + data['status_code'] = _config_obj.get_not_auth_status() + if os.path.exists('/www/server/panel/config/api.json'): + try: + res = json.loads(public.readFile('/www/server/panel/config/api.json')) + data['api'] = 'checked' if res['open'] else '' + except: + public.ExecShell('rm -f /www/server/panel/config/api.json') + data['api'] = '' + else: + data['api'] = '' + return data + + def get_pd(self, get): + from BTPanel import cache + tmp = -1 + try: + import panelPlugin + # get = public.dict_obj() + # get.init = 1 + tmp1 = panelPlugin.panelPlugin().get_cloud_list(get) + except: + tmp1 = None + if tmp1: + tmp = tmp1[public.to_string([112, 114, 111])] + ltd = tmp1.get('ltd', -1) + else: + ltd = -1 + tmp4 = cache.get( + public.to_string([112, 95, 116, 111, 107, 101, 110])) + if tmp4: + tmp_f = public.to_string([47, 116, 109, 112, 47]) + tmp4 + if not os.path.exists(tmp_f): public.writeFile(tmp_f, '-1') + tmp = public.readFile(tmp_f) + if tmp: tmp = int(tmp) + if not ltd: ltd = -1 + if tmp == None: tmp = -1 + if ltd < 1: + if ltd == -2: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, + 98, 116, 108, 116, 100, 45, 103, 114, 97, 121, 34, 62, 60, + 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, + 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, + 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, + 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, + 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, + 24050, 36807, 26399, 60, 47, 115, 112, 97, 110, 62, 60, 97, + 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 105, 110, + 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, + 116, 46, 115, 111, 102, 116, 46, 117, 112, 100, 97, 116, + 97, 95, 108, 116, 100, 40, 41, 34, 62, 32493, 36153, 60, + 47, 97, 62, 60, 47, 115, 112, 97, 110, 62 + ]) + elif tmp == -1: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, + 98, 116, 112, 114, 111, 45, 102, 114, 101, 101, 34, 32, + 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, + 111, 102, 116, 46, 117, 112, 100, 97, 116, 97, 95, 99, 111, + 109, 109, 101, 114, 99, 105, 97, 108, 95, 118, 105, 101, + 119, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, + 28857, 20987, 21319, 32423, 21040, 21830, 19994, 29256, 34, + 62, 20813, 36153, 29256, 60, 47, 115, 112, 97, 110, 62 + ]) + elif tmp == -2: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, + 98, 116, 112, 114, 111, 45, 103, 114, 97, 121, 34, 62, 60, + 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, + 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, + 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, + 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, + 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, + 24050, 36807, 26399, 60, 47, 115, 112, 97, 110, 62, 60, 97, + 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 105, 110, + 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, + 116, 46, 115, 111, 102, 116, 46, 117, 112, 100, 97, 116, + 97, 95, 112, 114, 111, 40, 41, 34, 62, 32493, 36153, 60, + 47, 97, 62, 60, 47, 115, 112, 97, 110, 62 + ]) + if tmp >= 0 and ltd in [-1, -2]: + if tmp == 0: + tmp2 = public.to_string([27704, 20037, 25480, 26435]) + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, + 34, 98, 116, 112, 114, 111, 34, 62, 123, 48, 125, 60, + 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, + 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, + 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, + 104, 116, 58, 32, 98, 111, 108, 100, 59, 34, 62, 123, + 49, 125, 60, 47, 115, 112, 97, 110, 62, 60, 47, 115, + 112, 97, 110, 62 + ]).format( + public.to_string([21040, 26399, 26102, 38388, 65306]), + tmp2) + else: + tmp2 = time.strftime( + public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), + time.localtime(tmp)) + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, + 34, 98, 116, 112, 114, 111, 34, 62, 21040, 26399, + 26102, 38388, 65306, 60, 115, 112, 97, 110, 32, 115, + 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, + 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, 110, + 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, + 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, + 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 123, 48, + 125, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, + 108, 97, 115, 115, 61, 34, 98, 116, 108, 105, 110, 107, + 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, + 116, 46, 115, 111, 102, 116, 46, 117, 112, 100, 97, + 116, 97, 95, 112, 114, 111, 40, 41, 34, 62, 32493, + 36153, 60, 47, 97, 62, 60, 47, 115, 112, 97, 110, 62 + ]).format(tmp2) + else: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, + 98, 116, 108, 116, 100, 45, 103, 114, 97, 121, 34, 32, 111, + 110, 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, + 102, 116, 46, 117, 112, 100, 97, 116, 97, 95, 108, 116, + 100, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, + 28857, 20987, 21319, 32423, 21040, 20225, 19994, 29256, 34, + 62, 20813, 36153, 29256, 60, 47, 115, 112, 97, 110, 62 + ]) + else: + tmp3 = public.to_string([ + 60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, + 116, 108, 116, 100, 34, 62, 21040, 26399, 26102, 38388, 65306, + 60, 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, + 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, + 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, + 98, 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, + 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 123, 125, 60, 47, + 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, 115, 61, + 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, + 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 117, + 112, 100, 97, 116, 97, 95, 108, 116, 100, 40, 41, 34, 62, + 32493, 36153, 60, 47, 97, 62, 60, 47, 115, 112, 97, 110, 62 + ]).format( + time.strftime( + public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), + time.localtime(ltd))) + + return tmp3, tmp, ltd + + @staticmethod + def set_backup_path(get): + try: + backup_path = get.backup_path.strip().rstrip("/") + except AttributeError: + return public.returnMsg(False, "参数错误") + + if not os.path.exists(backup_path): + return public.returnMsg(False, "指定目录不存在") + + if backup_path[-1] == "/": + backup_path = backup_path[:-1] + + import files + try: + from BTPanel import session + except: + session = None + fs = files.files() + + if not fs.CheckDir(get.backup_path): + return public.returnMsg(False, '不能使用系统关键目录作为默认备份目录') + if session is not None: + session['config']['backup_path'] = os.path.join('/', backup_path) + db_backup = backup_path + '/database' + site_backup = backup_path + '/site' + + if not os.path.exists(db_backup): + try: + os.makedirs(db_backup, 384) + except: + public.ExecShell('mkdir -p ' + db_backup) + + if not os.path.exists(site_backup): + try: + os.makedirs(site_backup, 384) + except: + public.ExecShell('mkdir -p ' + site_backup) + + public.M('config').where("id=?", ('1',)).save('backup_path', (get.backup_path,)) + public.WriteLog('TYPE_PANEL', 'PANEL_SET_SUCCESS', (get.backup_path,)) + + public.restart_panel() + return public.returnMsg(True, "设置成功") + + def get_soft_status(self,get): + if not hasattr(get,'name'): return public.returnMsg(False,'参数错误') + name = get.name.strip() + if name == 'sqlite': + return public.returnMsg(True,'符合!') + if os.path.exists('/www/server/{}'.format(name)) and len(os.listdir('/www/server/{}'.format(name))) > 2: + return public.returnMsg(True,'符合!') + if name == ['mysql','pgsql','sqlserver','mongodb','redis']: + count = public.M('database_servers').where("LOWER(db_type)=LOWER(?)", (name,)).count() + if count > 0: return public.returnMsg(True,'符合!') + return public.returnMsg(False,'不符合!') \ No newline at end of file diff --git a/class/panelMssql.py b/class/panelMssql.py index 2e1ac68d..307a2c5e 100644 --- a/class/panelMssql.py +++ b/class/panelMssql.py @@ -2,9 +2,9 @@ # +------------------------------------------------------------------- # | 宝塔Windows面板 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # +------------------------------------------------------------------- import re,os,sys,public @@ -43,7 +43,8 @@ class panelMssql: try: import pymssql except : - os.system("btpip install pymssql==2.1.4") + os.system("btpip install pymssql==2.3.0") + # os.system("btpip install pymssql==2.1.4") import pymssql @@ -53,17 +54,29 @@ class panelMssql: self.__DB_PORT = self.get_port() try: - - if self.__DB_CLOUD: - self.__DB_CONN = pymssql.connect(server = self.__DB_HOST, port= str(self.__DB_PORT),user=self.__DB_USER,password=self.__DB_PASS,database = None,login_timeout = 30,timeout = 0,autocommit = True) + + if self.__DB_CLOUD: + try: + self.__DB_CONN = pymssql.connect(server=self.__DB_HOST, port=str(self.__DB_PORT), + user=self.__DB_USER, + password=self.__DB_PASS, database=None, login_timeout=30, + timeout=0, autocommit=True, charset="CP936", tds_version='7.0') + except: + self.__DB_ERR = 'Failed to connect to database! Check that the remote database information is correct' + return False + # self.__DB_CONN = pymssql.connect(server=self.__DB_HOST, port=str(self.__DB_PORT), + # user=self.__DB_USER, + # password=self.__DB_PASS, database=None, login_timeout=30, + # timeout=0, autocommit=True, charset="CP936", tds_version='7.0') else: - self.__DB_CONN = pymssql.connect(server = self.__DB_HOST, port= str(self.__DB_PORT),login_timeout = 30,timeout = 0,autocommit = True) - self.__DB_CUR = self.__DB_CONN.cursor() #将数据库连接信息,赋值给cur。 - self.__DB_CUR = self.__DB_CONN.cursor() #将数据库连接信息,赋值给cur。 + self.__DB_CONN = pymssql.connect(server=self.__DB_HOST, port=str(self.__DB_PORT), login_timeout=30, + timeout=0, autocommit=True, charset="CP936", tds_version='7.0') + self.__DB_CUR = self.__DB_CONN.cursor() # 将数据库连接信息,赋值给cur。 + self.__DB_CUR = self.__DB_CONN.cursor() # 将数据库连接信息,赋值给cur。 if self.__DB_CUR: return True else: - self.__DB_ERR = '连接数据库失败,请检查是否安装SQL Server' + self.__DB_ERR = 'Failed to connect to the database, please check whether SQL Server is installed' return False except Exception as ex: self.__DB_ERR = public.get_error_info() diff --git a/class/panelMysql.py b/class/panelMysql.py index bfb23517..f89d11cf 100644 --- a/class/panelMysql.py +++ b/class/panelMysql.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import re,os,sys,public diff --git a/class/panelPHP.py b/class/panelPHP.py index f8723422..e3fe0652 100644 --- a/class/panelPHP.py +++ b/class/panelPHP.py @@ -1,10 +1,10 @@ #coding:utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- # +------------------------------------------------------------------- @@ -199,18 +199,18 @@ class panelPHP: # if o_pma_root: # if not os.path.exists(pma_root): # os.makedirs(pma_root) -# public.ExecShell("\cp -arf {}/* {}/".format(o_pma_root,pma_root)) +# public.ExecShell(r"\cp -arf {}/* {}/".format(o_pma_root,pma_root)) # public.ExecShell("chown -R www:www {}".format(pma_root)) # public.ExecShell("chmod -R 700 {}".format(pma_root)) -# public.ExecShell("\cp -arf {} {}".format(pma_version_f1,pma_version_f2)) +# public.ExecShell(r"\cp -arf {} {}".format(pma_version_f1,pma_version_f2)) # index = public.readFile(pma_root + '/index.php') # if index: # if index.find("use PhpMyAdmin\\Util") != -1: # resp = "use PhpMyAdmin\\Util;\nif(function_exists('opcache_invalidate')) opcache_invalidate('/www/server/phpmyadmin/pma/config.inc.php');" # index = index.replace("use PhpMyAdmin\\Util;",resp) -# elif index.find("use PMA\libraries\LanguageManager;") != -1: -# resp = "use PMA\libraries\LanguageManager;\nif(function_exists('opcache_invalidate')) opcache_invalidate('/www/server/phpmyadmin/pma/config.inc.php');" -# index = index.replace("use PMA\libraries\LanguageManager;",resp) +# elif index.find(r"use PMA\libraries\LanguageManager;") != -1: +# resp = "use PMA\\libraries\\LanguageManager;\nif(function_exists('opcache_invalidate')) opcache_invalidate('/www/server/phpmyadmin/pma/config.inc.php');" +# index = index.replace(r"use PMA\libraries\LanguageManager;",resp) # elif index.find("require_once 'libraries/common.inc.php';") != -1: # resp = "if(function_exists('opcache_invalidate')) opcache_invalidate('/www/server/phpmyadmin/pma/config.inc.php');\nrequire_once 'libraries/common.inc.php';" # index = index.replace("require_once 'libraries/common.inc.php';",resp) @@ -364,7 +364,7 @@ class panelPHP: # dst_path = '/www/server/adminer' # if os.path.exists(src_path): # if not os.path.exists(dst_path): os.makedirs(dst_path) -# public.ExecShell("\cp -arf {}/* {}/".format(src_path, dst_path)) +# public.ExecShell(r"\cp -arf {}/* {}/".format(src_path, dst_path)) # public.ExecShell("chown -R www:www {}".format(dst_path)) # public.ExecShell("chmod -R 700 {}".format(dst_path)) # public.ExecShell("rm -rf {}".format(src_path)) diff --git a/class/panelPlugin.py b/class/panelPlugin.py index e1e38394..faef308f 100644 --- a/class/panelPlugin.py +++ b/class/panelPlugin.py @@ -1,36 +1,83 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- -import public,os,sys,json,time,psutil,py_compile,re -from BTPanel import session,cache,send_file +import public +import os +import sys +import json +import time +import psutil +import re +import shutil +import requests +from BTPanel import session, cache, send_file + + if sys.version_info[0] == 3: from importlib import reload + + class mget: pass + + class panelPlugin: - __isTable = None - __install_path = None - __tasks = None __list = 'data/list.json' __type = 'data/type.json' __index = 'config/index.json' __link = 'config/link.json' - __product_list = None - __plugin_list = None - __exists_names = {} - __plugin_s_list = [] + __official_url = 'https://www.aapanel.com' # __official_url = 'http://dev.aapanel.com' - pids = None - ROWS = 15 def __init__(self): + self.__isTable = None + self.__tasks = None + self.__product_list = None + self.__plugin_list = None + self.__exists_names = {} + self.__plugin_s_list = [] + self.__plugin_info = None + self.__plugin_name = None + self.__plugin_object = None + self.__plugin_list = None + self.__panel_path = '/www/server/panel' + self.__plugin_path = self.__panel_path + '/plugin/' + self.__plugin_save_file = self.__panel_path + '/data/plugin_bin.pl' + self.__api_root_url = self.__official_url + '/api' + self.__api_url = self.__api_root_url + '/panel/get_plugin_list' + self.__download_url = self.__api_root_url + '/panel/download_plugin' + self.__download_d_main_url = self.__api_root_url + '/panel/download_plugin_main' + self._check_url = self.__api_root_url + '/panel/get_soft_list_status' + self._unbinding_url = self.__api_root_url + '/panel/get_unbinding' + self.__tmp_path = self.__panel_path + '/temp/' + self.__plugin_timeout = 3600 + self.__is_php = False + self.__install_opt = 'i' + self.__pid = 0 + self.__path_error = self.__panel_path + '/data/error_pl.pl' + self.__error_html = '/www/server/panel/BTPanel/templates/default/block_error.html' + self.__sub_rules = [] + self.__replace_rule = [] + + self.pids = None + self.ROWS = 15 + self.__install_path = '/www/server/panel/plugin' - #检查依赖 + if not self.__tasks: + try: + self.__tasks = public.M('tasks').where("status!=?", ('1',)).field('status,name').select() + except: + self.__tasks = [] + + if not os.path.exists(self.__tmp_path): + os.makedirs(self.__tmp_path, 0o755) + + # 检查依赖 def check_deps(self,get): cacheKey = 'plugin_lib_list' if not 'force' in get: @@ -79,11 +126,11 @@ class panelPlugin: return True #检查依赖 - def check_dependnet(self,dependnet): - if not dependnet: return True - dependnets = dependnet.split(',') + def check_dependent(self,dependent): + if not dependent: return True + dependents = dependent.split(',') status = True - for dep in dependnets: + for dep in dependents: if not dep: continue if dep.find('|') != -1: names = dep.split('|') @@ -137,7 +184,7 @@ class panelPlugin: if not self.check_mutex(pluginInfo['mutex']): return public.return_msg_gettext(False, 'Please uninstall [{}] first', (self.mutex_title,)) if not hasattr(get, 'id'): - if not self.check_dependnet(pluginInfo['dependent']): return public.return_msg_gettext(False, 'Depends on the following software, please install [{}] first', + if not self.check_dependent(pluginInfo['dependent']): return public.return_msg_gettext(False, 'Depends on the following software, please install [{}] first', (pluginInfo['dependent'],)) if 'version' in get: for versionInfo in pluginInfo['versions']: @@ -153,7 +200,32 @@ class panelPlugin: m_ps = {0: "All", 1: "Centos", 2: "Ubuntu/Debian"} return public.return_msg_gettext(False, 'Only supports [{}] system',(m_ps[int(versionInfo['os_limit'])],)) if not hasattr(get, 'id'): - if not self.check_dependnet(versionInfo['dependent']): return public.return_msg_gettext(False,'Depend on the following software, please install first [{}]',(versionInfo['dependent'],)) + if not self.check_dependent(versionInfo['dependent']): return public.return_msg_gettext(False,'Depend on the following software, please install first [{}]',(versionInfo['dependent'],)) + + # 获取插件安装包下载进度 + def get_download_speed(self, get): + ''' + @name 获取插件下载进度 + @author hwliang<2021-06-25> + @param plugin_name 插件名称 + @return dict + ''' + result = self.__get_download_speed(get.plugin_name) + return result + + # 取消下载 + def close_install(self, get): + ''' + @name 取消指定插件安装过程 + @author hwliang<2021-07-07> + @param plugin_name 插件名称 + @return void + ''' + plugin_name = get.plugin_name.strip() + tmp_path = '{}/{}'.format(self.__tmp_path, plugin_name) + if os.path.exists(tmp_path): shutil.rmtree(tmp_path) + return public.returnMsg(False, '安装过程已取消!') + #安装插件 def install_plugin(self,get): str1 = public.get_msg_gettext("System critical directory is not writable!") @@ -173,6 +245,7 @@ class panelPlugin: return check_result if pluginInfo['name'] in ['dns_manager','mail_sys']: pluginInfo['type'] = 5 + if pluginInfo['type'] != 5: result = self.install_sync(pluginInfo,get) else: @@ -209,16 +282,21 @@ class panelPlugin: if os.path.exists(pluginInfo['install_checks']): update =pluginInfo['versions'][0]['version_msg'] return self.update_zip(None,toFile,update) else: - download_url = public.get_url() + '/install/plugin/' + pluginInfo['name'] + '_en/install.sh' - toFile = '/tmp/%s.sh' % pluginInfo['name'] - public.downloadFile(download_url,toFile) - self.set_pyenv(toFile) - public.ExecShell('/bin/bash ' + toFile + ' install &> /tmp/panelShell.pl') - if os.path.exists(pluginInfo['install_checks']): - public.write_log_gettext('Installer','Successfully installed plugin [{}]',(pluginInfo['title'],)) - if os.path.exists(toFile): os.remove(toFile) - return public.return_msg_gettext(True,'Installation succeeded!') - return public.return_msg_gettext(False,'Installation failed') + # download_url = public.get_url() + '/install/plugin/' + pluginInfo['name'] + '_en/install.sh' + # toFile = '/tmp/%s.sh' % pluginInfo['name'] + # public.downloadFile(download_url,toFile) + # self.set_pyenv(toFile) + # public.ExecShell('/bin/bash ' + toFile + ' install &> /tmp/panelShell.pl') + # if os.path.exists(pluginInfo['install_checks']): + # public.write_log_gettext('Installer','Successfully installed plugin [{}]',(pluginInfo['title'],)) + # if os.path.exists(toFile): os.remove(toFile) + # return public.return_msg_gettext(True,'Installation succeeded!') + # return public.return_msg_gettext(False,'Installation failed') + + if hasattr(get, 'min_version'): + get.version += '.' + get.min_version + + return self.__install_plugin(pluginInfo['name'], get.version) # 设置Python环境变量 def set_pyenv(self, filename): @@ -235,7 +313,11 @@ class panelPlugin: #异步安装 def install_async(self,pluginInfo,get): - mtype = 'install'; + # # 只取主版本号与子版本号,忽略修订号 + # if 'version' in get: + # get.version = '.'.join(str(get.version).split('.')[:2]) + + mtype = 'install' mmsg = public.get_msg_gettext('Install') if hasattr(get, 'upgrade'): mtype = 'update' @@ -267,6 +349,11 @@ class panelPlugin: execstr = "cd /www/server/panel/install && /bin/bash install_soft.sh {} {} {} {} {}".format( get.type, mtype, get.sName, get.version, ols_execstr) if get.sName == "phpmyadmin": + # 当面板开启SSL时,标记下次打开phpmyadmin时需要设置SSL + if os.path.exists('{}/data/ssl.pl'.format(public.get_panel_path())): + with open('{}/data/phpmyadmin_ssl.mark'.format(public.get_panel_path()), 'w') as fp: + fp.write('1') + execstr += "&> /tmp/panelExec.log" if public.get_webserver() == 'openlitespeed': execstr += " && sleep 1 && /usr/local/lsws/bin/lswsctrl restart" @@ -322,74 +409,45 @@ class panelPlugin: return public.return_msg_gettext(True,"Uninstallation succeeded") #从云端取列表 - def get_cloud_list(self,get=None): - lcoalTmp = 'data/plugin.json' - softList = None - listTmp = public.readFile(lcoalTmp) - force_refresh = 0 - try: - if listTmp: softList = json.loads(listTmp) - if 'success' in softList and not softList['success']: - if os.path.exists(lcoalTmp): os.remove(lcoalTmp) - force_refresh = 1 - except: - if os.path.exists(lcoalTmp): os.remove(lcoalTmp) + def get_cloud_list(self, get=None): + force = 0 + if get and hasattr(get, 'force'): + force = int(get.force) + + if 'focre_cloud' in session: + if session['focre_cloud']: + force = 1 + session['focre_cloud'] = False + + if 'init_cloud' not in session: + force = 1 + session['init_cloud'] = True + + softList = public.load_soft_list(True if force == 1 else False) if get and 'init' in get: if softList: if 'success' not in softList: return softList - focre = 0 - if hasattr(get,'force'): focre = int(get.force) - if session: - if 'focre_cloud' in session: - if session['focre_cloud']: - focre = 1 - session['focre_cloud'] = False - - if not 'init_cloud' in session: - focre = 1 - session['init_cloud'] = True - if force_refresh == 1: - focre = 1 - if not softList or focre > 0: - self.clean_panel_log() - # cloudUrl = 'https://console.aapanel.com/api/panel/get_soft_list' - cloudUrl = '{}/api/panel/getSoftList'.format(self.__official_url) - import panelAuth - import requests - pdata = panelAuth.panelAuth().create_serverid(None) - # listTmp = public.httpPost(cloudUrl,pdata,6) - url_headers={} - if 'token' in pdata: - url_headers = {"authorization": "bt {}".format(pdata['token'])} - pdata['environment_info'] = json.dumps(public.fetch_env_info()) - - # listTmp = requests.post(cloudUrl, params=pdata, headers=url_headers,verify=False,timeout=10) - # listTmp=listTmp.json() - - try: - listTmp = requests.post(cloudUrl, params=pdata, headers=url_headers, verify=False, timeout=10) - listTmp.raise_for_status() # 检查请求是否成功,如果不成功会抛出异常 - listTmp = listTmp.json() - except: - listTmp = False - - if listTmp is False: - listTmp = public.readFile(lcoalTmp) - try: - softList = listTmp - except: - pass - if softList: public.writeFile(lcoalTmp,json.dumps(softList)) + if force > 0: public.ExecShell('rm -f /tmp/bmac_*') public.run_thread(self.getCloudPHPExt) # 专业版和企业版到期提醒,aaPanel目前没有先注释 # self.expire_msg(softList) + try: - public.writeFile("/tmp/" + cache.get('p_token'),str(softList['pro'])) - except:pass + p_token = cache.get('p_token') + + if p_token is None: + p_token = 'bmac_' + public.Md5(public.get_mac_address()) + cache.set('p_token', p_token) + + public.writeFile("/tmp/" + p_token, str(softList['pro'])) + public.writeFile('/tmp/{}.time'.format(p_token), str(int(time.time()))) + except: + pass + sType = 0 try: if hasattr(get,'type'): sType = int(get['type']) @@ -414,14 +472,13 @@ class panelPlugin: sType = 0 except:pass - - try: - softList = json.loads(softList) - except: - pass + # 扫描本地插件并追加到软件列表中 softList['list'] = self.get_local_plugin(softList['list']) - softList['list'] = self.get_types(softList['list'],sType) - if hasattr(get,'query'): + + # 软件列表分类处理 + softList['list'] = self.get_types(softList['list'], sType) + + if hasattr(get, 'query'): if get.query: get.query = get.query.lower() tmpList = [] @@ -431,11 +488,11 @@ class panelPlugin: softInfo['ps'].lower().find(get.query) != -1: tmpList.append(softInfo) softList['list'] = tmpList + for softInfo in softList['list']: if 'uninsatll_checks' not in softInfo: softInfo['uninsatll_checks'] = softInfo['uninstall_checks'] - # if not softList['list']: - # if os.path.exists(lcoalTmp): os.remove(lcoalTmp) + return softList #取提醒标记 @@ -656,7 +713,7 @@ class panelPlugin: "s_version": "0", "manager_version": "1", "c_manager_version": "1", - "dependnet": "", + "dependent": "", "mutex": "", "install_checks": "/www/server/panel/plugin/" + info['name'], "uninsatll_checks": "/www/server/panel/plugin/" + info['name'], @@ -666,7 +723,7 @@ class panelPlugin: { "m_version": m_version[0], "version": m_version[1], - "dependnet": "", + "dependent": "", "mem_limit": 32, "cpu_limit": 1, "os_limit": 0, @@ -756,20 +813,14 @@ class panelPlugin: return softList #取首页软件列表 - def get_index_list(self,get=None): + def get_index_list(self, get=None): softList = self.get_cloud_list(get)['list'] if not softList: get.force = 1 softList = self.get_cloud_list(get)['list'] if not softList: return public.return_msg_gettext(False,'Failed to get software list ({})',"401") softList = self.set_coexist(softList) - # # 只取应用名 - # lista = [i['name'] for i in softList] - # - # public.print_log("23454566777^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^_______________________________" - # "列表 {}".format(lista)) - if not os.path.exists(self.__index): - public.writeFile(self.__index,'[]') + if not os.path.exists(self.__index): public.writeFile(self.__index,'[]') try: indexList = json.loads(public.ReadFile(self.__index)) except Exception: @@ -1021,11 +1072,18 @@ class panelPlugin: if softInfo['name'] == sName: if sName == 'phpmyadmin': + # 检查是否需要开启SSL + if os.path.exists('{}/data/phpmyadmin_ssl.mark'.format(public.get_panel_path())) and os.path.exists('{}/phpmyadmin'.format(public.get_setup_path())): + os.remove('{}/data/phpmyadmin_ssl.mark'.format(public.get_panel_path())) + from ajax_v2 import ajax + ajax().set_phpmyadmin_ssl(public.to_dict_obj({'v': '1'})) + from BTPanel import get_phpmyadmin_dir pmd = get_phpmyadmin_dir() softInfo['ext'] = self.getPHPMyAdminStatus() if softInfo['ext'] and pmd: - softInfo['ext']['url'] = 'http://' + public.GetHost() + ':'+ pmd[1] + '/' + pmd[0] + port = softInfo['ext']['ssl_port'] if softInfo['ext'].get('ssl_enabled', False) else pmd[1] + softInfo['ext']['url'] = 'http' + ('s' if softInfo['ext'].get('ssl_enabled', False) else '') + '://' + public.GetHost() + ':'+ port + '/' + pmd[0] if "php-" in sName: v = softInfo["versions"][0]["m_version"] v1 = v.replace(".", "") @@ -1036,6 +1094,7 @@ class panelPlugin: else: softInfo["php_ini"] = "/www/server/php/{}/etc/php.ini".format(v1) return self.check_status(softInfo) + return False @@ -1181,7 +1240,13 @@ class panelPlugin: page = page.Page() info = {} info['count'] = len(data) - info['row'] = self.ROWS + info['row'] = self.ROWS + if hasattr(get,'row'): + try: + info['row'] = int(get['row']) + except: + info['row'] = self.ROWS + info['p'] = 1 if hasattr(get,'p'): try: @@ -1637,7 +1702,7 @@ class panelPlugin: if os.path.exists('/www/server/apache/bin/httpd'): v1 = session.get('httpdv') if not v1: - v1 = public.ExecShell("/www/server/apache/bin/httpd -v|grep Apache|awk '{print $3}'|sed 's/Apache\///'")[0].strip(); + v1 = public.ExecShell(r"/www/server/apache/bin/httpd -v|grep Apache|awk '{print $3}'|sed 's/Apache\///'")[0].strip(); session['httpdv'] = v1 #if name == 'mysql': # if os.path.exists('/www/server/mysql/bin/mysql'): v1 = public.ExecShell("mysql -V|awk '{print $5}'|sed 's/,//'")[0].strip(); @@ -1749,6 +1814,8 @@ class panelPlugin: pstatus = False phpversion = "54" phpport = '888' + ssl_port = '887' + ssl_enabled = False if os.path.exists(configFile): conf = public.readFile(configFile) rep = r"listen\s+([0-9]+)\s*;" @@ -1756,6 +1823,15 @@ class panelPlugin: if rtmp: phpport = rtmp.groups()[0] + # SSL配置文件查看 + ssl_config_file = '{}/vhost/nginx/phpmyadmin.conf'.format(public.get_panel_path()) + if os.path.exists(ssl_config_file) and os.path.getsize(ssl_config_file) > 10: + tmps = public.readFile(ssl_config_file) + m = re.search(r"listen\s*(\d+)", tmps) + if m is not None: + ssl_enabled = True + ssl_port = m.group(1) + if conf.find('AUTH_START') != -1: pauth = True if conf.find(setupPath + '/stop') == -1: pstatus = True configFile = setupPath + '/nginx/conf/enable-php.conf' @@ -1786,8 +1862,19 @@ class panelPlugin: rtmp = re.search(rep,conf) if rtmp: phpport = rtmp.groups()[0] + + # SSL配置文件查看 + ssl_config_file = '{}/vhost/apache/phpmyadmin.conf'.format(public.get_panel_path()) + if os.path.exists(ssl_config_file) and os.path.getsize(ssl_config_file) > 10: + tmps = public.readFile(ssl_config_file) + m = re.search(r"Listen\s*(\d+)", tmps) + if m is not None: + ssl_enabled = True + ssl_port = m.group(1) + if conf.find('AUTH_START') != -1: pauth = True if conf.find('/www/server/stop') == -1: pstatus = True + if os.path.exists('/usr/local/lsws/bin/lswsctrl'): result = self._get_ols_myphpadmin_info() if result: @@ -1808,7 +1895,9 @@ class panelPlugin: tmp['run'] = pstatus tmp['phpversion'] = phpversion + tmp['ssl_enabled'] = ssl_enabled tmp['port'] = phpport + tmp['ssl_port'] = ssl_port tmp['auth'] = pauth except Exception as ex: tmp['status'] = False @@ -1819,18 +1908,18 @@ class panelPlugin: filename = "/www/server/panel/vhost/openlitespeed/detail/phpmyadmin.conf" conf = public.readFile(filename) if not conf:return False - reg = '/usr/local/lsws/lsphp(\d+)/bin/lsphp' + reg = r'/usr/local/lsws/lsphp(\d+)/bin/lsphp' php_v = re.search(reg,conf) phpversion = '73' phpport = '888' if php_v: - phpversion = php_v.groups(1) + phpversion = php_v.group(1) filename = '/www/server/panel/vhost/openlitespeed/listen/888.conf' conf = public.readFile(filename) - reg = 'address\s+\*\:(\d+)' + reg = r'address\s+\*\:(\d+)' php_port = re.search(reg,conf) if php_port: - phpport = php_port.groups(1) + phpport = php_port.group(1) pauth = False pstatus = False if conf.find('/www/server/stop') == -1: pstatus = True @@ -2164,43 +2253,12 @@ class panelPlugin: return find['title'] - - #请求插件事件 - def a(self,get): - if not hasattr(get,'name'): return public.return_msg_gettext(False,'Input name of plugin!') + def a(self, get): try: - if not public.path_safe_check("%s/%s" % (get.name,get.s)): return public.return_msg_gettext(False,'Requested method [{}] does not exist!') - path = self.__install_path + '/' + get.name - - # aaa = path + '/'+get.name+'_main.py' - # public.print_log("@@@@@@@@@@@@@@@@@aaa {}".format(aaa)) - - if not os.path.exists(path + '/'+get.name+'_main.py'): - if os.path.exists(path+'/index.php'): - # bbb = path+'/index.php' - # public.print_log("@@@@@@@@@@@@@@@@@bbb {}".format(bbb)) - - - import panelPHP - return panelPHP.panelPHP(get.name).exec_php_script(get) - return public.return_msg_gettext(False,'This plugin does NOT have extend function!') - - if not self.check_accept(get):return public.return_msg_gettext(False,"You did not purchase [ {} ] or the authorization has expired", (self.get_title_byname(get),)) - public.package_path_append(path) - plugin_main = __import__(get.name+'_main') - try: - reload(plugin_main) - except: pass - - pluginObject = eval('plugin_main.' + get.name + '_main()') - if not hasattr(pluginObject,get.s): return public.return_msg_gettext(False,'Requested method [{}] does not exist!',(get.s,)) - execStr = 'pluginObject.' + get.s + '(get)' - return eval(execStr) + return public.run_plugin(get.name, get.s, get) except: - import traceback - errorMsg = traceback.format_exc() - public.writeFile('logs/done.log',errorMsg) - return public.return_msg_gettext(False,'%s:
                                    %s ' % (public.get_msg_gettext('Sorry, something went wrong'),errorMsg.replace('\n','
                                    '))) + return public.get_error_object(None, plugin_name=get.name) + #上传插件包 def update_zip(self,get = None,tmp_file = None, update = False): tmp_path = '/www/server/panel/temp' @@ -2266,7 +2324,7 @@ class panelPlugin: if not os.path.exists(get.tmp_path): return public.return_msg_gettext(False,'TEM_FILE_NOT_EXIST!') plugin_path = '/www/server/panel/plugin/' + get.plugin_name if not os.path.exists(plugin_path): os.makedirs(plugin_path) - public.ExecShell("\cp -a -r " + get.tmp_path + '/* ' + plugin_path + '/') + public.ExecShell(r"\cp -a -r " + get.tmp_path + '/* ' + plugin_path + '/') public.ExecShell('chmod -R 600 ' + plugin_path) self.set_pyenv(plugin_path + '/install.sh') public.ExecShell('cd ' + plugin_path + ' && bash install.sh install &> /tmp/panelShell.pl') @@ -2384,4 +2442,403 @@ class panelPlugin: config_data.append(args_name) public.writeFile(config_file,"\n".join(config_data)) public.write_log_gettext('Software manager','Setup software: Custom compilation parameters for {} are configured as: {}'.format(get.name,config_data)) - return public.return_msg_gettext(True,'Setup successfully!') \ No newline at end of file + return public.return_msg_gettext(True,'Setup successfully!') + + # 安装插件 + def __install_plugin(self, upgrade_plugin_name, upgrade_version=None): + ''' + @name 安装指定插件 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 版本号.指定版本号 / tls.最新正式版 / beta.最新测试版 + @return dict + ''' + self.__plugin_name = upgrade_plugin_name + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: + raise public.PanelError('指定插件不存在,无法安装!') + if not plugin_info['versions']: + raise public.PanelError('指定插件当前未发布版本信息,请稍候再安装!') + if not upgrade_version: + upgrade_version = '{}.{}'.format( + plugin_info['versions'][0]['m_version'], + plugin_info['versions'][0]['version']) + filename = self.__download_plugin(upgrade_plugin_name, upgrade_version) + # 如果下载失败 + if isinstance(filename, dict): + return filename + return self.__unpackup_plugin(filename) + + # 修复插件 + def __repair_plugin(self, upgrade_plugin_name, upgrade_version=None): + ''' + @name 修复指定插件 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 版本号.指定版本号 / tls.最新正式版 / beta.最新测试版 + @return dict + ''' + self.__install_opt = 'r' + return self.__install_plugin(upgrade_plugin_name, upgrade_version) + + # 升级插件版本 + def __upgrade_plugin(self, upgrade_plugin_name, upgrade_version=None): + ''' + @name 升级到指定版本 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 版本号.指定版本号 / tls.最新正式版 / beta.最新测试版 + @return dict + ''' + self.__install_opt = 'u' + return self.__install_plugin(upgrade_plugin_name, upgrade_version) + + # 获取插件信息 + def __get_plugin_info(self, upgrade_plugin_name): + ''' + @name 获取插件信息 + @author hwliang<2021-06-15> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + plugin_info_file = '{}/{}/info.json'.format(self.__plugin_path, + upgrade_plugin_name) + if not os.path.exists(plugin_info_file): return {} + info_body = self.__read_file(plugin_info_file) + if not info_body: return {} + plugin_info = json.loads(info_body) + return plugin_info + + # 获取插件最近1条更新日志 + def __get_update_msg(self, upgrade_plugin_name, upgrade_version): + ''' + @name 检查指定插件版本更新日志 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 + @return string + ''' + plugin_update_msg = '' + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: return plugin_update_msg + for _version_info in plugin_info['versions']: + l_version = '{}.{}'.format(_version_info['m_version'], + _version_info['version']) + if l_version == upgrade_version: + plugin_update_msg = _version_info['update_msg'] + break + return plugin_update_msg + + # 获取插件最近10条更新日志 + def __get_plugin_upgrades(self, upgrade_plugin_name): + ''' + @name 检查指定插件最近10条更新日志 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @return list + ''' + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: return [] + + try: + upgrade_list = public.httpPost( + self.__api_root_url + '/down/get_update_msg', + {'soft_id': plugin_info['id']}) + return json.loads(upgrade_list) + except: + return [] + + # 获取指定软件信息 + def __get_plugin_find(self, upgrade_plugin_name=None): + ''' + @name 获取指定软件信息 + @author hwliang<2021-06-15> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + self.__ensure_plugin_list_obtained(True) + + for p_data_info in self.__plugin_list['list']: + if p_data_info['name'] == upgrade_plugin_name: + upgrade_plugin_name = p_data_info['name'] + return p_data_info + + # 如果不在插件列表中 + return self.__get_plugin_info(upgrade_plugin_name) + + # 检查插件依赖 + def __check_dependent(self, upgrade_plugin_name): + ''' + @name 检查指定插件的依赖安装情况 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: return {} + if not plugin_info['dependent']: return {} + deployment_list = {} + for dependent_plu_name in plugin_info['dependent'].split(','): + p_info = self.__get_plugin_find(dependent_plu_name) + if not p_info: continue + deployment_list[dependent_plu_name] = os.path.exists( + p_info['install_checks']) + return deployment_list + + # 读取指定文件 + def __read_file(self, filename, open_mode='r'): + ''' + @name 读取指定文件 + @author hwliang<2021-06-16> + @param filename 文件名 + @param mode 打开模式, 默认: r + @return bytes or string + ''' + f_object = open(filename, mode=open_mode) + file_body = f_object.read() + f_object.close() + return file_body + + # 解包插件压缩包 + def __unpackup_plugin(self, tmp_file): + ''' + @name 解包插件包 + @author hwliang<2021-06-21> + @param tmp_file 下载好的保存路径,从self.download_plugin方法中获取 + @return dict + ''' + if type(tmp_file) == dict: + return tmp_file + + if "false" in tmp_file or "错误" in tmp_file: + return json.loads(tmp_file) + + s_tmp_path = self.__tmp_path + if not os.path.exists(s_tmp_path): + os.makedirs(s_tmp_path, mode=384) + + if tmp_file: + if not os.path.exists(tmp_file): + return public.returnMsg(False, '文件下载失败!') + import panelTask as plu_panelTask + plu_panelTask.bt_task()._unzip(tmp_file, s_tmp_path, '', + '/dev/null') + if os.path.exists(tmp_file): + os.remove(tmp_file) + + s_tmp_path = os.path.join(s_tmp_path, self.__plugin_name) + + p_info = os.path.join(s_tmp_path, 'info.json') + if not os.path.exists(p_info): + d_path = None + for plugin_df in os.walk(s_tmp_path): + if len(plugin_df[2]) < 3: continue + if not 'info.json' in plugin_df[2]: continue + if not 'install.sh' in plugin_df[2]: continue + if not os.path.exists(plugin_df[0] + '/info.json'): continue + d_path = plugin_df[0] + if d_path: + s_tmp_path = d_path + p_info = s_tmp_path + '/info.json' + try: + try: + plugin_data_info = json.loads(public.ReadFile(p_info)) + except: + plugin_data_info = json.loads(self.__read_file(p_info)) + + plugin_data_info['size'] = public.get_path_size(s_tmp_path) + if not 'author' in plugin_data_info: + plugin_data_info['author'] = 'aapanel' + if not 'home' in plugin_data_info: + plugin_data_info['home'] = 'https://www.aapanel.com' + + p_info_file = self.__plugin_path + plugin_data_info[ + 'name'] + '/info.json' + plugin_data_info['old_version'] = '0' + plugin_data_info['tmp_path'] = s_tmp_path + if os.path.exists(p_info_file): + try: + old_info = json.loads(public.ReadFile(p_info_file)) + plugin_data_info['old_version'] = old_info['versions'] + except: + pass + except: + public.ExecShell("rm -rf " + s_tmp_path + '/*') + return public.get_error_object(plugin_name=self.__plugin_name) + plugin_data_info['install_opt'] = self.__install_opt + plugin_data_info['dependent'] = self.__check_dependent(plugin_data_info['name']) + plugin_data_info['update_msg'] = self.__get_update_msg( + plugin_data_info['name'], plugin_data_info['versions']) + not_check = self.not_cpu_or_bit(plugin_data_info) + if not_check: + if os.path.exists(s_tmp_path): shutil.rmtree(s_tmp_path) + return not_check + + return plugin_data_info + + # 检测是否为不支持的平台和系统位数 + def not_cpu_or_bit(self, plugin_data_info): + ''' + @name 检测是否为不支持的平台和系统位数 + @author hwliang<2021-07-07> + @param plugin_data_info 插件信息数据 + @return dict or None + ''' + if 'not_os_bit' in plugin_data_info: + if public.get_sysbit() == int(plugin_data_info['not_os_bit']): + return public.returnMsg( + False, + '该应用不支持{}位系统'.format(plugin_data_info['not_os_bit'])) + if 'not_cpu_type' in plugin_data_info: + if not plugin_data_info['not_cpu_type']: return None + machine = os.uname().machine + for c_type in plugin_data_info['not_cpu_type']: + c_type = c_type.lower() + result = public.returnMsg( + False, '该应用不支持{}平台,{}'.format(c_type, machine)) + if c_type in ['arm', 'aarch64', 'aarch']: + if machine in ['aarch64', 'aarch']: + return result + elif c_type in ['mips', 'mips64', 'mips64el']: + if machine.find('mips') != -1: + return result + elif c_type in ['x86', 'x86-64']: + if machine in ['x86', 'x86-64']: + return result + return None + + # 下载插件安装包 + def __download_plugin(self, upgrade_plugin_name, upgrade_version): + ''' + @name 下载插件包 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 + @return string 保存路径 + ''' + + pkey = '{}_pre'.format(upgrade_plugin_name) + pdata = public.get_user_info() + pdata['name'] = upgrade_plugin_name + pdata['version'] = upgrade_version + pdata['os'] = 'Linux' + pdata['environment_info'] = json.dumps(public.fetch_env_info(), ensure_ascii=False) + filename = '{}/{}.zip'.format(self.__tmp_path, upgrade_plugin_name) + if not os.path.exists(self.__tmp_path): + os.makedirs(self.__tmp_path, 384) + + if not cache.get(pkey): + import config, socket + import requests.packages.urllib3.util.connection as urllib3_conn + _ip_type = config.config().get_request_iptype() + old_family = urllib3_conn.allowed_gai_family + if _ip_type == 'ipv4': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + elif _ip_type == 'ipv6': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + try: + download_res = requests.post( + self.__download_url, + pdata, + headers=public.get_requests_headers(), + timeout=(60, 1800), + stream=True) + except Exception as ex: + str_ex = str(ex) + if 'Name or service not known' in str_ex: + return public.returnMsg(False, + '下载软件包时DNS解析失败,请检查服务器网络配置是否正常:

                                    Error: Name or service not known

                                    ') + elif 'Failed to establish a new connection' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Failed to establish a new connection

                                    ') + elif 'Read timed out' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Read timed out

                                    ') + elif 'Connection refused' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Connection refused

                                    ') + elif 'Remote end closed connection without response' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Remote end closed connection without response

                                    ') + else: + return public.returnMsg(False, '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: {}

                                    '.format(str_ex)) + finally: + urllib3_conn.allowed_gai_family = old_family + + try: + headers_total_size = int(download_res.headers['File-size']) + except: + try: + return json.loads(download_res.text).json() + except: + if download_res.text.find('') != -1: + raise public.PanelError( + public.error_conn_cloud(download_res.text)) + raise public.PanelError(download_res.text) + + res_down_size = 0 + res_chunk_size = 8192 + last_time = time.time() + with open(filename, 'wb+') as with_res_f: + try: + for download_chunk in download_res.iter_content( + chunk_size=res_chunk_size): + if download_chunk: + with_res_f.write(download_chunk) + speed_last_size = len(download_chunk) + res_down_size += speed_last_size + res_start_time = time.time() + res_timeout = (res_start_time - last_time) + res_sec_speed = int(res_down_size / res_timeout) + pre_text = '{}/{}/{}'.format(res_down_size, + headers_total_size, + res_sec_speed) + cache.set(pkey, pre_text, 3600) + + except Exception as ex: + ex_str = str(ex) + if "Read timed out" in ex_str: + return public.returnMsg(False, '软件包下载超时,请重试: {}'.format(ex_str)) + if "No space left on device" in ex_str: + return public.returnMsg(False, "磁盘空间不足,请先清理后再试!") + finally: + with_res_f.close() + if cache.get(pkey): cache.delete(pkey) + + if public.FileMd5(filename) != download_res.headers['Content-md5']: + return public.returnMsg(False, '软件包校验失败,请更新软件列表,并重试') + else: + while True: + time.sleep(1) + if not cache.get(pkey): break + return '' + return filename + + # 获取插件安装包下载进度 + def __get_download_speed(self, upgrade_plugin_name): + ''' + @name 取插件下载进度 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + pkey = '{}_pre'.format(upgrade_plugin_name) + pre_text = cache.get(pkey) + if not pre_text: + return public.returnMsg(False, '指定进度信息不存在!') + result = {"status": True} + pre_tmp = pre_text.split('/') + result['down_size'], result['total_size'] = (int(pre_tmp[0]), + int(pre_tmp[1])) + result['down_pre'] = round( + result['down_size'] / result['total_size'] * 100, 1) + result['sec_speed'] = int(float(pre_tmp[2])) + result['need_time'] = int( + (result['total_size'] - result['down_size']) / result['sec_speed']) + return result + + + # 获取插件与授权信息 + def __ensure_plugin_list_obtained(self, force: bool = False): + if force or not self.__plugin_list: + self.__plugin_list = public.load_soft_list(force) diff --git a/class/panelPmd.py b/class/panelPmd.py index 197831d1..f38037da 100644 --- a/class/panelPmd.py +++ b/class/panelPmd.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2019 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2019 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- import public,os from BTPanel import request,abort,send_file diff --git a/class/panelProjectController.py b/class/panelProjectController.py index e0053547..395038f8 100644 --- a/class/panelProjectController.py +++ b/class/panelProjectController.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -32,8 +32,8 @@ class ProjectController: if args['mod_name'] in ['base']: return public.return_status_code(1000,'wrong call!') public.exists_args('def_name,mod_name',args) if args['def_name'].find('__') != -1: return public.return_status_code(1000,'Called method name cannot contain [ __ ] characters') - if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,'The called module name cannot contain characters other than \w') - if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,'The called module name cannot contain characters other than \w') + if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,r'The called module name cannot contain characters other than \w') except: return public.get_error_object() diff --git a/class/panelPush.py b/class/panelPush.py index 2a7d73a6..01199ae7 100644 --- a/class/panelPush.py +++ b/class/panelPush.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://www.bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # | Author: lx # | 消息推送管理 # | 对外方法 get_modules_list、install_module、uninstall_module、get_module_template、set_push_config、get_push_config、del_push_config diff --git a/class/panelRedirect.py b/class/panelRedirect.py index 09f4894c..a564f2a6 100644 --- a/class/panelRedirect.py +++ b/class/panelRedirect.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2018 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2018 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -21,7 +21,7 @@ class panelRedirect: #匹配目标URL的域名并返回 def GetToDomain(self,tourl): if tourl: - rep = "https?://([\w\-\.]+)" + rep = r"https?://([\w\-\.]+)" tu = re.search(rep, tourl) return tu.group(1) @@ -105,7 +105,7 @@ class panelRedirect: if os.path.exists(ng_file): ng_conf = public.readFile(ng_file) if not p_conf: - rep = "#SSL-END(\n|.)*\/redirect\/.*\*.conf;" + rep = "#SSL-END(\n|.)*\\/redirect\\/.*\\*.conf;" ng_conf = re.sub(rep, '#SSL-END', ng_conf) public.writeFile(ng_file, ng_conf) return @@ -114,13 +114,13 @@ class panelRedirect: sitenamelist.append(i["sitename"]) if get.sitename in sitenamelist: - rep = "include.*\/redirect\/.*\*.conf;" + rep = r"include.*\/redirect\/.*\*.conf;" if not re.search(rep,ng_conf): ng_conf = ng_conf.replace("#SSL-END","#SSL-END\n\t%s\n\t" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') + "include " + ng_redirectfile + ";") public.writeFile(ng_file,ng_conf) else: - rep = "#SSL-END(\n|.)*\/redirect\/.*\*.conf;" + rep = "#SSL-END(\n|.)*\\/redirect\\/.*\\*.conf;" ng_conf = re.sub(rep,'#SSL-END',ng_conf) public.writeFile(ng_file, ng_conf) @@ -134,18 +134,18 @@ class panelRedirect: if os.path.exists(ap_file): ap_conf = public.readFile(ap_file) if p_conf == "[]": - rep = "\n*%s\n+\s+IncludeOptiona[\s\w\/\.\*]+" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') ap_conf = re.sub(rep, '', ap_conf) public.writeFile(ap_file, ap_conf) return if sitename in p_conf: - rep = "%s(\n|.)+IncludeOptional.*\/redirect\/.*conf" % public.get_msg_gettext('#referenced redirect rule') + rep = "%s(\n|.)+IncludeOptional.*\\/redirect\\/.*conf" % public.get_msg_gettext('#referenced redirect rule') rep1 = "combined" if not re.search(rep,ap_conf): ap_conf = ap_conf.replace(rep1, rep1 + "\n\t%s" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') +"\n\tIncludeOptional " + ap_redirectfile) public.writeFile(ap_file,ap_conf) else: - rep = "\n*%s\n+\s+IncludeOptiona[\s\w\/\.\*]+" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') ap_conf = re.sub(rep,'', ap_conf) public.writeFile(ap_file, ap_conf) @@ -167,7 +167,7 @@ class panelRedirect: if self.__CheckRedirect(get.sitename,get.redirectname,is_error_page): return public.return_msg_gettext(False, 'Specified redirect name already exists') #检测目标URL格式 - rep = "http(s)?\:\/\/([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+([a-zA-Z0-9][a-zA-Z0-9]{0,62})+.?" + 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 'tourl' in get and not re.match(rep, get.tourl): return public.returnMsg(False, 'Target URL format is wrong %s' + get.tourl) @@ -180,12 +180,12 @@ class panelRedirect: else: if not get.redirectpath: return public.return_msg_gettext(False, 'Please enter redirected path') - #repte = "[\?\=\[\]\)\(\*\&\^\%\$\#\@\!\~\`{\}\>\<\,\',\"]+" + #repte = "[\\?\\=\\[\\]\\)\\(\\*\\&\\^\\%\\$\\#\\@\\!\\~\\`{\\}\\>\\<\\,\',\"]+" # 检测路径格式 if "/" not in get.redirectpath: return public.return_msg_gettext(False, 'Path format is incorrect, the format is /xxx') #if re.search(repte, get.redirectpath): - # return public.return_msg_gettext(False, "代理目录不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\,',\"]") + # return public.return_msg_gettext(False, "代理目录不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]") #检测域名是否已经存在配置文件 repeatdomain = self.__CheckRepeatDomain(get,action) if repeatdomain: @@ -384,7 +384,7 @@ class panelRedirect: """ if self.__firsturl:redirect_path=self.__firsturl add_type=',R={}]'.format(str(r_type)) - add_str='#REWRITE-START\n\n RewriteEngine on\n RewriteCond %\{REQUEST_FILENAME\} !-f\n RewriteCond %{REQUEST_FILENAME} !-d\n RewriteRule . '+redirect_path+' [L'+add_type+'\n\n#REWRITE-END' + add_str='#REWRITE-START\n\n RewriteEngine on\n RewriteCond %\\{REQUEST_FILENAME\\} !-f\n RewriteCond %{REQUEST_FILENAME} !-d\n RewriteRule . '+redirect_path+' [L'+add_type+'\n\n#REWRITE-END' redirectname_md5 = self.__calc_md5(redirectname) file_path= "%s/panel/vhost/apache/redirect/%s" % (self.setupPath,site_name) public.ExecShell("mkdir -p %s" % file_path) @@ -620,9 +620,9 @@ class panelRedirect: old_conf = public.readFile(conf_path) rep ="" if i == "nginx": - rep += "#301-START\n+[\s\w\:\/\.\;\$]+#301-END" + rep += "#301-START\n+[\\s\\w\\:\\/\\.\\;\\$]+#301-END" if i == "apache": - rep += "#301-START[\n\<\>\w\.\s\^\*\$\/\[\]\(\)\:\,\=]+#301-END" + rep += "#301-START[\n\\<\\>\\w\\.\\s\\^\\*\\$\\/\\[\\]\\(\\)\\:\\,\\=]+#301-END" conf = re.sub(rep, "", old_conf) public.writeFile(conf_path, conf) public.serviceReload() diff --git a/class/panelRun.py b/class/panelRun.py index 692b957f..78508bfb 100644 --- a/class/panelRun.py +++ b/class/panelRun.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ diff --git a/class/panelSSL.py b/class/panelSSL.py index 16eb14e4..d0ee40e0 100644 --- a/class/panelSSL.py +++ b/class/panelSSL.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2016 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------ @@ -34,7 +34,7 @@ class panelSSL: __BINDURL = 'https://www.aapanel.com/api/user' # 获取token 获取官网token - # __BINDURL = 'https:///dev.aapanel.com/api/user' # 获取token 获取官网token + # __BINDURL = 'http://dev.aapanel.com/api/user' # 获取token 获取官网token __CODEURL = 'https://api.bt.cn/Auth/GetBindCode' # 获取绑定验证码 __UPATH = 'data/userInfo.json' @@ -66,7 +66,6 @@ class panelSSL: else: self.__userInfo = {} - # public.print_log('初始化 !!!!!!!!!!!!!!!!!!!用户信息: {}'.format(self.__userInfo)) try: if self.__userInfo: # 记录里没有这两个key @@ -141,7 +140,7 @@ class panelSSL: # public.print_log("写入用户信息 @@@@222 {}".format(self.__APIURL + '/user/login')) result = json.loads(rtmp) # public.print_log("写入用户信息 @@@@ {}".format(rtmp)) - public.print_log("写入用户信息 @@@@ {}".format(result)) + # public.print_log("写入用户信息 @@@@ {}".format(result)) if result['success']: bind = 'data/bind.pl' if os.path.exists(bind): os.remove(bind) @@ -1056,7 +1055,7 @@ class panelSSL: if cert_list and all_domain: for cert in cert_list: d_cert = '' - if re.match("^\*\..*", cert): + if re.match(r"^\*\..*", cert): d_cert = cert.replace('*.', '') for domain in all_domain: if cert == domain: diff --git a/class/panelSafe.py b/class/panelSafe.py index 745ac227..1656bc55 100644 --- a/class/panelSafe.py +++ b/class/panelSafe.py @@ -14,26 +14,26 @@ class safe: ev = public.GetMsg("EXPLOITABLE_VULNERABILITIES") dc = public.GetMsg("DANGEROUS_CITATION") rulelist = [ - {'msg':get_post_ev,'level':danger,'code':'(\$_(GET|POST|REQUEST)\[.{0,15}\]\s{0,10}\(\s{0,10}\$_(GET|POST|REQUEST)\[.{0,15}\]\))'}, - {'msg':one_word_th,'level':high_risk,'code':'((eval|assert)(\s|\n)*\((\s|\n)*\$_(POST|GET|REQUEST)\[.{0,15}\]\))'}, - {'msg':one_word_th,'level':high_risk,'code':'(eval(\s|\n)*\(base64_decode(\s|\n)*\((.|\n){1,200})'}, - {'msg':webshell,'level':danger,'code':'(function\_exists\s*\(\s*[\'|\"](shell\_exec|system|popen|exec|proc\_open|passthru)+[\'|\"]\s*\))'}, - {'msg':webshell,'level':danger,'code':'((exec|shell\_exec|passthru)+\s*\(\s*\$\_(\w+)\[(.*)\]\s*\))'}, - {'msg':ev,'level':danger,'code':'(\$(\w+)\s*\(\s.chr\(\d+\)\))'}, - {'msg':webshell,'level':danger,'code':'(\$(\w+)\s*\$\{(.*)\})'}, - {'msg':get_post_cookie_ev,'level':danger,'code':'(\$(\w+)\s*\(\s*\$\_(GET|POST|REQUEST|COOKIE|SERVER)+\[(.*)\]\s*\))'}, - {'msg':get_post_cookie_ev,'level':danger,'code':'(\$\_(GET|POST|REQUEST|COOKIE|SERVER)+\[(.*)\]\(\s*\$(.*)\))'}, - {'msg':webshell,'level':danger,'code':'(\$\_\=(.*)\$\_)'}, - {'msg':webshell,'level':danger,'code':'(\$(.*)\s*\((.*)\/e(.*)\,\s*\$\_(.*)\,(.*)\))'}, - {'msg':webshell,'level':danger,'code':'(new com\s*\(\s*[\'|\"]shell(.*)[\'|\"]\s*\))'}, - {'msg':webshell,'level':danger,'code':'(echo\s*curl\_exec\s*\(\s*\$(\w+)\s*\))'}, - {'msg':public.GetMsg("HAZARDOUS_FILE_OPERATION_VULNERABILITIES"),'level':high_risk,'code':'((fopen|fwrite|fputs|file\_put\_contents)+\s*\((.*)\$\_(GET|POST|REQUEST|COOKIE|SERVER)+\[(.*)\](.*)\))'}, - {'msg':public.GetMsg("DANGEROUS_UPLOAD_VULNERABILITIES"),'level':danger,'code':'(\(\s*\$\_FILES\[(.*)\]\[(.*)\]\s*\,\s*\$\_(GET|POST|REQUEST)+\[(.*)\]\[(.*)\]\s*\))'}, - {'msg':dc,'level':high_risk,'code':'(\$\_(\w+)(.*)(eval|assert|include|require|include\_once|require\_once)+\s*\(\s*\$(\w+)\s*\))'}, - {'msg':dc,'level':high_risk,'code':'((include|require|include\_once|require\_once)+\s*\(\s*[\'|\"](\w+)\.(jpg|gif|ico|bmp|png|txt|zip|rar|htm|css|js)+[\'|\"]\s*\))'}, - {'msg':ev,'level':danger,'code':'(eval\s*\(\s*\(\s*\$\$(\w+))'}, - {'msg':one_word_th,'level':high_risk,'code':'((eval|assert|include|require|include\_once|require\_once|array\_map|array\_walk)+\s*\(\s*\$\_(GET|POST|REQUEST|COOKIE|SERVER|SESSION)+\[(.*)\]\s*\))'}, - {'msg':one_word_th,'level':danger,'code':'(preg\_replace\s*\((.*)\(base64\_decode\(\$)'} + {'msg':get_post_ev,'level':danger,'code':r'(\$_(GET|POST|REQUEST)\[.{0,15}\]\s{0,10}\(\s{0,10}\$_(GET|POST|REQUEST)\[.{0,15}\]\))'}, + {'msg':one_word_th,'level':high_risk,'code':'((eval|assert)(\\s|\n)*\\((\\s|\n)*\\$_(POST|GET|REQUEST)\\[.{0,15}\\]\\))'}, + {'msg':one_word_th,'level':high_risk,'code':'(eval(\\s|\n)*\\(base64_decode(\\s|\n)*\\((.|\n){1,200})'}, + {'msg':webshell,'level':danger,'code':'(function\\_exists\\s*\\(\\s*[\'|\"](shell\\_exec|system|popen|exec|proc\\_open|passthru)+[\'|\"]\\s*\\))'}, + {'msg':webshell,'level':danger,'code':r'((exec|shell\_exec|passthru)+\s*\(\s*\$\_(\w+)\[(.*)\]\s*\))'}, + {'msg':ev,'level':danger,'code':r'(\$(\w+)\s*\(\s.chr\(\d+\)\))'}, + {'msg':webshell,'level':danger,'code':r'(\$(\w+)\s*\$\{(.*)\})'}, + {'msg':get_post_cookie_ev,'level':danger,'code':r'(\$(\w+)\s*\(\s*\$\_(GET|POST|REQUEST|COOKIE|SERVER)+\[(.*)\]\s*\))'}, + {'msg':get_post_cookie_ev,'level':danger,'code':r'(\$\_(GET|POST|REQUEST|COOKIE|SERVER)+\[(.*)\]\(\s*\$(.*)\))'}, + {'msg':webshell,'level':danger,'code':r'(\$\_\=(.*)\$\_)'}, + {'msg':webshell,'level':danger,'code':r'(\$(.*)\s*\((.*)\/e(.*)\,\s*\$\_(.*)\,(.*)\))'}, + {'msg':webshell,'level':danger,'code':'(new com\\s*\\(\\s*[\'|\"]shell(.*)[\'|\"]\\s*\\))'}, + {'msg':webshell,'level':danger,'code':r'(echo\s*curl\_exec\s*\(\s*\$(\w+)\s*\))'}, + {'msg':public.GetMsg("HAZARDOUS_FILE_OPERATION_VULNERABILITIES"),'level':high_risk,'code':r'((fopen|fwrite|fputs|file\_put\_contents)+\s*\((.*)\$\_(GET|POST|REQUEST|COOKIE|SERVER)+\[(.*)\](.*)\))'}, + {'msg':public.GetMsg("DANGEROUS_UPLOAD_VULNERABILITIES"),'level':danger,'code':r'(\(\s*\$\_FILES\[(.*)\]\[(.*)\]\s*\,\s*\$\_(GET|POST|REQUEST)+\[(.*)\]\[(.*)\]\s*\))'}, + {'msg':dc,'level':high_risk,'code':r'(\$\_(\w+)(.*)(eval|assert|include|require|include\_once|require\_once)+\s*\(\s*\$(\w+)\s*\))'}, + {'msg':dc,'level':high_risk,'code':'((include|require|include\\_once|require\\_once)+\\s*\\(\\s*[\'|\"](\\w+)\\.(jpg|gif|ico|bmp|png|txt|zip|rar|htm|css|js)+[\'|\"]\\s*\\))'}, + {'msg':ev,'level':danger,'code':r'(eval\s*\(\s*\(\s*\$\$(\w+))'}, + {'msg':one_word_th,'level':high_risk,'code':r'((eval|assert|include|require|include\_once|require\_once|array\_map|array\_walk)+\s*\(\s*\$\_(GET|POST|REQUEST|COOKIE|SERVER|SESSION)+\[(.*)\]\s*\))'}, + {'msg':one_word_th,'level':danger,'code':r'(preg\_replace\s*\((.*)\(base64\_decode\(\$)'} ] ruleFile = '/www/server/panel/data/ruleList.conf'; @@ -101,7 +101,7 @@ class safe: def checkPHPINI(self): setupPath = '/www/server'; phps = public.get_php_versions() - rep = "disable_functions\s*=\s*(.+)\n" + rep = "disable_functions\\s*=\\s*(.+)\n" defs = ['passthru','exec','system','chroot','chgrp','chown','shell_exec','popen','ini_alter','ini_restore','dl','openlog','syslog','readlink','symlink','popepassthru'] data = [] for phpv in phps: diff --git a/class/panelSafeController.py b/class/panelSafeController.py index 8495dc91..bac8c8bf 100644 --- a/class/panelSafeController.py +++ b/class/panelSafeController.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -32,8 +32,8 @@ class SafeController: if args['mod_name'] in ['base']: return public.return_status_code(1000,'wrong call!') public.exists_args('def_name,mod_name',args) if args['def_name'].find('__') != -1: return public.return_status_code(1000,'The called method name cannot contain the "__" character') - if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,'The called module name cannot contain characters other than \w') - if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,'The called module name cannot contain characters other than \w') + if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,r'The called module name cannot contain characters other than \w') except: return public.get_error_object() # 参数处理 diff --git a/class/panelSearch.py b/class/panelSearch.py index 10cb22ec..6e76552f 100644 --- a/class/panelSearch.py +++ b/class/panelSearch.py @@ -2,7 +2,7 @@ # +------------------------------------------------------------------- # | version :1.0 # +------------------------------------------------------------------- -# | Author: 梁凯强 <1249648969@qq.com> +# | Author: 梁凯强 <1249648969@aapanel.com> # +------------------------------------------------------------------- # | 快速检索 # +-------------------------------------------------------------------- diff --git a/class/panelSite.py b/class/panelSite.py index bd832078..6bb91bb9 100644 --- a/class/panelSite.py +++ b/class/panelSite.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------ @@ -55,6 +55,8 @@ class panelSite(panelRedirect): if os.path.exists(self.apache_conf_bak): os.remove(self.apache_conf_bak) self.is_ipv6 = os.path.exists(self.setupPath + '/panel/data/ipv6.pl') sys.setrecursionlimit(1000000) + self._proxy_path = '/www/server/proxy_project' + self._proxy_config_path = self._proxy_path + '/sites' # 默认配置文件 def check_default(self): @@ -141,7 +143,7 @@ class panelSite(panelRedirect): ''' % (public.get_php_proxy(self.phpVersion, 'apache'),) apaOpt = 'Require all granted' - conf = '''%s + conf = r'''%s ServerAdmin webmaster@example.com DocumentRoot "%s" ServerName %s.%s @@ -182,7 +184,7 @@ class panelSite(panelRedirect): listen_ipv6 = '' if self.is_ipv6: listen_ipv6 = "\n listen [::]:%s;" % self.sitePort - conf = '''server + conf = r'''server {{ listen {listen_port};{listen_ipv6} server_name {site_name}; @@ -806,7 +808,14 @@ set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) 'success': del_successfully} # 删除站点 - def DeleteSite(self, get, multiple=None): + def DeleteSite(self, get: public.dict_obj, multiple=None): + # 请求参数校验 + get.validate([ + public.validate.Param('id').Require().Integer(), + public.validate.Param('webname').Require().SafePath(), + public.validate.Param('path').Integer(), + ], [public.validate.trim_filter()]) + proxyconf = self.__read_config(self.__proxyfile) id = get.id if public.M('sites').where('id=?', (id,)).count() < 1: return public.return_msg_gettext(False, 'Specified site does NOT exist') @@ -964,7 +973,7 @@ set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) conf = public.readFile(file_name) if not conf: continue - map_rep = 'map\s+{}.*'.format(sitename) + map_rep = r'map\s+{}.*'.format(sitename) conf = re.sub(map_rep, '', conf) if "map" not in conf: public.ExecShell('rm -f {}*'.format(file_name)) @@ -1069,7 +1078,7 @@ set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) return public.return_msg_gettext(False,'Domain name format is incorrect!') # 判断域名格式 - reg = "^([\w\-\*]{1,100}\.){1,24}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$" + reg = r"^([\w\-\*]{1,100}\.){1,24}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$" if not re.match(reg, get.domain): return public.return_msg_gettext(False, 'Format of domain is invalid!') # 获取自定义端口 @@ -1133,7 +1142,7 @@ set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) # 添加openlitespeed 80端口监听 def openlitespeed_set_80_domain(self, get, conf): - rep = 'map\s+{}.*'.format(get.webname) + rep = r'map\s+{}.*'.format(get.webname) domains = get.webname.strip().split(',') if conf: map_tmp = re.search(rep, conf) @@ -1145,12 +1154,12 @@ set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) conf = re.sub(rep, new_map, conf) else: map_tmp = '\tmap\t{d} {d}\n'.format(d=domains[0]) - listen_rep = "secure\s*0" + listen_rep = r"secure\s*0" conf = re.sub(listen_rep, "secure 0\n" + map_tmp, conf) return conf else: - rep_default = 'listener\s+Default\{(\n|[\s\w\*\:\#\.\,])*' + rep_default = 'listener\\s+Default\\{(\n|[\\s\\w\\*\\:\\#\\.\\,])*' tmp = re.search(rep_default, conf) # domains = get.webname.strip().split(',') if tmp: @@ -1177,7 +1186,7 @@ set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) pass if listen_conf: # 添加域名 - rep = 'map\s+{}.*'.format(get.webname) + rep = r'map\s+{}.*'.format(get.webname) map_tmp = re.search(rep, listen_conf) if map_tmp: map_tmp = map_tmp.group() @@ -1188,7 +1197,7 @@ set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) else: domains = get.webname.strip().split(',') map_tmp = '\tmap\t{d} {d}'.format(d=domains[0]) - listen_rep = "secure\s*0" + listen_rep = r"secure\s*0" listen_conf = re.sub(listen_rep, "secure 0\n" + map_tmp, listen_conf) else: listen_conf = """ @@ -1277,7 +1286,7 @@ listener Default%s{ apaOpt = "Order allow,deny\n\t\tAllow from all" else: vName = "" - # rep = "php-cgi-([0-9]{2,3})\.sock" + # rep = r"php-cgi-([0-9]{2,3})\.sock" # version = re.search(rep,conf).groups()[0] version = public.get_php_version_conf(conf) if len(version) < 2: return public.return_msg_gettext(False, 'Failed to get PHP version!') @@ -1289,7 +1298,7 @@ listener Default%s{ ''' % (public.get_php_proxy(version, 'apache'),) apaOpt = 'Require all granted' - newconf = ''' + newconf = r''' ServerAdmin webmaster@example.com DocumentRoot "%s" ServerName %s.%s @@ -1397,12 +1406,12 @@ listener Default%s{ rep = r"\n*(.|\n)*" tmp = re.search(rep, conf).group() - rep1 = "ServerAlias\s+(.+)\n" + rep1 = "ServerAlias\\s+(.+)\n" tmp1 = re.findall(rep1, tmp) tmp2 = tmp1[0].split(' ') if len(tmp2) < 2: conf = re.sub(rep, '', conf) - rep = "NameVirtualHost.+\:" + port + "\n" + rep = r"NameVirtualHost.+\:" + port + "\n" conf = re.sub(rep, '', conf) else: newServerName = tmp.replace(' ' + get['domain'] + "\n", "\n") @@ -1432,7 +1441,7 @@ listener Default%s{ if os.path.isdir(file_name): continue conf = public.readFile(file_name) - map_rep = 'map\s+{}\s+(.*)'.format(get.webname) + map_rep = r'map\s+{}\s+(.*)'.format(get.webname) domains = re.search(map_rep, conf) if domains: domains = domains.group(1).split(',') @@ -1640,7 +1649,7 @@ listener Default%s{ if not apis[i]['data']: continue for j in range(len(apis[i]['data'])): if apis[i]['data'][j]['value']: continue - match = re.search(apis[i]['data'][j]['key'] + "\s*=\s*'(.+)'", account) + match = re.search(apis[i]['data'][j]['key'] + r"\s*=\s*'(.+)'", account) if match: apis[i]['data'][j]['value'] = match.groups()[0] if apis[i]['data'][j]['value']: is_write = True if is_write: public.writeFile('./config/dns_api.json', json.dumps(apis)) @@ -1695,7 +1704,7 @@ listener Default%s{ def GetFormatSSLResult(self, result): try: import re - rep = "\s*Domain:.+\n\s+Type:.+\n\s+Detail:.+" + rep = "\\s*Domain:.+\n\\s+Type:.+\n\\s+Detail:.+" tmps = re.findall(rep, result) statusList = [] @@ -1716,16 +1725,16 @@ listener Default%s{ def get_tls13(self): nginx_bin = '/www/server/nginx/sbin/nginx' nginx_v = public.ExecShell(nginx_bin + ' -V 2>&1')[0] - nginx_v_re = re.findall("nginx/(\d\.\d+).+OpenSSL\s+(\d\.\d+)",nginx_v,re.DOTALL) + nginx_v_re = re.findall(r"nginx/(\d\.\d+).+OpenSSL\s+(\d\.\d+)",nginx_v,re.DOTALL) if nginx_v_re: if nginx_v_re[0][0] in ['1.8','1.9','1.7','1.6','1.5','1.4']: return '' if float(nginx_v_re[0][0]) >= 1.15 and float(nginx_v_re[0][-1]) >= 1.1: return ' TLSv1.3' else: - _v = re.search('nginx/1\.1(5|6|7|8|9).\d',nginx_v) + _v = re.search(r'nginx/1\.1(5|6|7|8|9).\d',nginx_v) if not _v: - _v = re.search('nginx/1\.2\d\.\d',nginx_v) + _v = re.search(r'nginx/1\.2\d\.\d',nginx_v) openssl_v = public.ExecShell(nginx_bin + ' -V 2>&1|grep OpenSSL')[0].find('OpenSSL 1.1.') != -1 if _v and openssl_v: return ' TLSv1.3' @@ -1733,7 +1742,7 @@ listener Default%s{ # 获取apache反向代理 def get_apache_proxy(self, conf): - rep = "\n*#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid\n+\s+IncludeOptiona.*" + rep = "\n*#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid\n+\\s+IncludeOptiona.*" proxy = re.search(rep, conf) if proxy: return proxy.group() @@ -1795,7 +1804,7 @@ listener SSL443 { """ else: - rep = 'listener\s*SSL443\s*{' + rep = r'listener\s*SSL443\s*{' map = '\n map {s} {s}'.format(s=siteName) conf = re.sub(rep, 'listener SSL443 {' + map, conf) domain = ",".join(self._get_site_domains(siteName)) @@ -1808,6 +1817,21 @@ listener SSL443 { if ap_static_security: return ap_static_security.group() return '' + + def write_json_conf(self, siteName,status): + conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=siteName + ) + try: + proxy_json_conf = json.loads(public.readFile(conf_path)) + proxy_json_conf['ssl_info']['ssl_status']=status + #将proxy_json_conf 写入文件 + public.WriteFile + except Exception as e: + proxy_json_conf = {} + + return public.return_message(0,0,proxy_json_conf) # 添加SSL配置 def SetSSLConf(self, get): @@ -1857,7 +1881,7 @@ listener SSL443 { conf = re.sub(r"\s+\#SSL\-END","\n\t\t#SSL-END",conf) # 添加端口 - rep = "listen.*[\s:]+(\d+).*;" + rep = r"listen.*[\s:]+(\d+).*;" tmp = re.findall(rep, conf) if not public.inArray(tmp, '443'): listen_re = re.search(rep,conf) @@ -1925,7 +1949,7 @@ listener SSL443 { ''' % (public.get_php_proxy(version, 'apache'),) apaOpt = 'Require all granted' - sslStr = '''%s + sslStr = r'''%s ServerAdmin webmaster@example.com DocumentRoot "%s" ServerName SSL.%s @@ -2071,16 +2095,16 @@ listener SSL443 { file = self.setupPath + '/panel/vhost/nginx/node_'+siteName+'.conf' conf = public.readFile(file) if conf: - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" conf = re.sub(rep, '', conf) - rep = "\s+if.+server_port.+\n.+\n\s+\s*}" + rep = "\\s+if.+server_port.+\n.+\n\\s+\\s*}" conf = re.sub(rep, '', conf) public.writeFile(file, conf) file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' conf = public.readFile(file) if conf: - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" conf = re.sub(rep, '', conf) public.writeFile(file, conf) # OLS @@ -2111,41 +2135,41 @@ listener SSL443 { file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' conf = public.readFile(file) if conf: - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" conf = re.sub(rep, '', conf) - rep = "\s+ssl_certificate\s+.+;\s+ssl_certificate_key\s+.+;" + rep = r"\s+ssl_certificate\s+.+;\s+ssl_certificate_key\s+.+;" conf = re.sub(rep, '', conf) - rep = "\s+ssl_protocols\s+.+;\n" + rep = "\\s+ssl_protocols\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_ciphers\s+.+;\n" + rep = "\\s+ssl_ciphers\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_prefer_server_ciphers\s+.+;\n" + rep = "\\s+ssl_prefer_server_ciphers\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_session_cache\s+.+;\n" + rep = "\\s+ssl_session_cache\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_session_timeout\s+.+;\n" + rep = "\\s+ssl_session_timeout\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_ecdh_curve\s+.+;\n" + rep = "\\s+ssl_ecdh_curve\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_session_tickets\s+.+;\n" + rep = "\\s+ssl_session_tickets\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_stapling\s+.+;\n" + rep = "\\s+ssl_stapling\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl_stapling_verify\s+.+;\n" + rep = "\\s+ssl_stapling_verify\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+add_header\s+.+;\n" + rep = "\\s+add_header\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+add_header\s+.+;\n" + rep = "\\s+add_header\\s+.+;\n" conf = re.sub(rep, '', conf) - rep = "\s+ssl\s+on;" + rep = r"\s+ssl\s+on;" conf = re.sub(rep, '', conf) - rep = "\s+error_page\s497.+;" + rep = r"\s+error_page\s497.+;" conf = re.sub(rep, '', conf) - rep = "\s+if.+server_port.+\n.+\n\s+\s*}" + rep = "\\s+if.+server_port.+\n.+\n\\s+\\s*}" conf = re.sub(rep, '', conf) - rep = "\s+listen\s+443.*;" + rep = r"\s+listen\s+443.*;" conf = re.sub(rep, '', conf) - rep = "\s+listen\s+\[::\]:443.*;" + rep = r"\s+listen\s+\[::\]:443.*;" conf = re.sub(rep, '', conf) public.writeFile(file, conf) @@ -2154,9 +2178,9 @@ listener SSL443 { file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' conf = public.readFile(file) if conf: - rep = "\n(.|\n)*<\/VirtualHost>" + rep = "\n(.|\n)*<\\/VirtualHost>" conf = re.sub(rep, '', conf) - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,250}#HTTP_TO_HTTPS_END" + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,250}#HTTP_TO_HTTPS_END" conf = re.sub(rep, '', conf) rep = "NameVirtualHost *:443\n" conf = conf.replace(rep, '') @@ -2188,7 +2212,7 @@ listener SSL443 { file = "/www/server/panel/vhost/openlitespeed/listen/443.conf" conf = public.readFile(file) if conf: - rep = '\n\s*map\s*{}.*'.format(sitename) + rep = '\n\\s*map\\s*{}.*'.format(sitename) conf = re.sub(rep, '', conf) if not "map " in conf: public.ExecShell('rm -f {}*'.format(file)) @@ -2380,7 +2404,7 @@ listener SSL443 { file = self.setupPath + '/panel/vhost/openlitespeed/' + get.name + '.conf' conf = public.readFile(file) if conf: - rep = 'vhRoot\s*{}'.format(Path) + rep = r'vhRoot\s*{}'.format(Path) new_content = 'vhRoot {}'.format(sitePath) conf = re.sub(rep, new_content, conf) public.writeFile(file, conf) @@ -2402,7 +2426,7 @@ listener SSL443 { if not conf: return False try: - really_path = re.search('root\s+(.*);', conf).group(1) + really_path = re.search(r'root\s+(.*);', conf).group(1) tmp = stop_path + '/' + really_path.replace(website_path + '/', '') public.ExecShell('mkdir {t} && ln -s {s}/index.html {t}/index.html'.format(t=tmp, s=stop_path)) except: @@ -2455,7 +2479,7 @@ listener SSL443 { file = self.setupPath + '/panel/vhost/openlitespeed/' + get.name + '.conf' conf = public.readFile(file) if conf: - rep = 'vhRoot\s*{}'.format(sitePath) + rep = r'vhRoot\s*{}'.format(sitePath) new_content = 'vhRoot {}'.format(path) conf = re.sub(rep, new_content, conf) public.writeFile(file, conf) @@ -2478,17 +2502,17 @@ listener SSL443 { data = {} conf = public.readFile(filename) try: - rep = "\s+limit_conn\s+perserver\s+([0-9]+);" + rep = r"\s+limit_conn\s+perserver\s+([0-9]+);" tmp = re.search(rep, conf).groups() data['perserver'] = int(tmp[0]) # IP并发限制 - rep = "\s+limit_conn\s+perip\s+([0-9]+);" + rep = r"\s+limit_conn\s+perip\s+([0-9]+);" tmp = re.search(rep, conf).groups() data['perip'] = int(tmp[0]) # 请求并发限制 - rep = "\s+limit_rate\s+([0-9]+)\w+;" + rep = r"\s+limit_rate\s+([0-9]+)\w+;" tmp = re.search(rep, conf).groups() data['limit_rate'] = int(tmp[0]) except: @@ -2525,15 +2549,15 @@ listener SSL443 { if (conf.find('limit_conn perserver') != -1): # 替换总并发 - rep = "limit_conn\s+perserver\s+([0-9]+);" + rep = r"limit_conn\s+perserver\s+([0-9]+);" conf = re.sub(rep, perserver, conf) # 替换IP并发限制 - rep = "limit_conn\s+perip\s+([0-9]+);" + rep = r"limit_conn\s+perip\s+([0-9]+);" conf = re.sub(rep, perip, conf) # 替换请求流量限制 - rep = "limit_rate\s+([0-9]+)\w+;" + rep = r"limit_rate\s+([0-9]+)\w+;" conf = re.sub(rep, limit_rate, conf) else: conf = conf.replace('#error_page 404/404.html;', @@ -2559,15 +2583,15 @@ listener SSL443 { filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' conf = public.readFile(filename) # 清理总并发 - rep = "\s+limit_conn\s+perserver\s+([0-9]+);" + rep = r"\s+limit_conn\s+perserver\s+([0-9]+);" conf = re.sub(rep, '', conf) # 清理IP并发限制 - rep = "\s+limit_conn\s+perip\s+([0-9]+);" + rep = r"\s+limit_conn\s+perip\s+([0-9]+);" conf = re.sub(rep, '', conf) # 清理请求流量限制 - rep = "\s+limit_rate\s+([0-9]+)\w+;" + rep = r"\s+limit_rate\s+([0-9]+)\w+;" conf = re.sub(rep, '', conf) public.writeFile(filename, conf) public.serviceReload() @@ -2597,9 +2621,9 @@ listener SSL443 { result['status'] = False result['url'] = "http://" return result - rep = "return\s+301\s+((http|https)\://.+);" + rep = r"return\s+301\s+((http|https)\://.+);" arr = re.search(rep, conf).groups()[0] - rep = "'\^(([\w-]+\.)+[\w-]+)'" + rep = r"'\^(([\w-]+\.)+[\w-]+)'" tmp = re.search(rep, conf) src = '' if tmp: src = tmp.groups()[0] @@ -2611,9 +2635,9 @@ listener SSL443 { result['status'] = False result['url'] = "http://" return result - rep = "RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" + rep = r"RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" arr = re.search(rep, conf).groups()[0] - rep = "\^((\w+\.)+\w+)\s+\[NC" + rep = r"\^((\w+\.)+\w+)\s+\[NC" tmp = re.search(rep, conf) src = '' if tmp: src = tmp.groups()[0] @@ -2626,9 +2650,9 @@ listener SSL443 { result['status'] = False result['url'] = "http://" return result - rep = "RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" + rep = r"RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" arr = re.search(rep, conf).groups()[0] - rep = "\^((\w+\.)+\w+)\s+\[NC" + rep = r"\^((\w+\.)+\w+)\s+\[NC" tmp = re.search(rep, conf) src = '' if tmp: src = tmp.groups()[0] @@ -2650,7 +2674,7 @@ listener SSL443 { srcDomain = get.srcDomain toDomain = get.toDomain type = get.type - rep = "(http|https)\://.+" + rep = r"(http|https)\://.+" if not re.match(rep, toDomain): return public.return_msg_gettext(False, 'URL address is invalid!') # nginx @@ -2664,7 +2688,7 @@ listener SSL443 { if type == '1': mconf = mconf.replace("#error_page 404/404.html;", "#error_page 404/404.html;\n" + conf301) else: - rep = "\s+#301-START(.|\n){1,300}#301-END" + rep = "\\s+#301-START(.|\n){1,300}#301-END" mconf = re.sub(rep, '', mconf) public.writeFile(filename, mconf) @@ -2680,7 +2704,7 @@ listener SSL443 { rep = "combined" mconf = mconf.replace(rep, rep + "\n\t" + conf301) else: - rep = "\n\s+#301-START(.|\n){1,300}#301-END\n*" + rep = "\n\\s+#301-START(.|\n){1,300}#301-END\n*" mconf = re.sub(rep, '\n\n', mconf, 1) mconf = re.sub(rep, '\n\n', mconf, 1) @@ -2775,7 +2799,7 @@ listener SSL443 { if not hasattr(get, 'dirName'): public.return_msg_gettext(False, 'Directory cannot be empty!') dirName = get.dirName - reg = "^([\w\-\*]{1,100}\.){1,4}([\w\-]{1,100}|[\w\-]{1,100}\.[\w\-]{1,100})$" + reg = r"^([\w\-\*]{1,100}\.){1,4}([\w\-]{1,100}|[\w\-]{1,100}\.[\w\-]{1,100})$" if not re.match(reg, domain): return public.return_msg_gettext(False, 'Format of primary domain is incorrect') siteInfo = public.M('sites').where("id=?",(id,)).field('id,path,name').find() @@ -2804,16 +2828,16 @@ listener SSL443 { listen_ipv6 = '' if self.is_ipv6: listen_ipv6 = "\n listen [::]:%s;" % port try: - rep = "enable-php-(\w{2,5})\.conf" + rep = r"enable-php-(\w{2,5})\.conf" tmp = re.search(rep,conf) if not tmp: - rep = "enable-php-(\d+-wpfastcgi).conf" + rep = r"enable-php-(\d+-wpfastcgi).conf" tmp = re.search(rep, conf) except: return public.returnMsg(False,"Get enable php config failed!") tmp = tmp.groups() version = tmp[0] - bindingConf = ''' + bindingConf = r''' #BINDING-%s-START server { @@ -2871,7 +2895,7 @@ server phpConfig = "" apaOpt = "Order allow,deny\n\t\tAllow from all" else: - # rep = "php-cgi-([0-9]{2,3})\.sock" + # rep = r"php-cgi-([0-9]{2,3})\.sock" # tmp = re.search(rep,conf).groups() # version = tmp[0] version = public.get_php_version_conf(conf) @@ -2883,8 +2907,9 @@ server ''' % (public.get_php_proxy(version, 'apache'),) apaOpt = 'Require all granted' - bindingConf = ''' -\n#BINDING-%s-START + bindingConf = r''' + +#BINDING-%s-START ServerAdmin webmaster@example.com DocumentRoot "%s" @@ -2925,7 +2950,7 @@ server listen_file = self.setupPath + "/panel/vhost/openlitespeed/listen/80.conf" listen_conf = public.readFile(listen_file) if listen_conf: - rep = 'secure\s*0' + rep = r'secure\s*0' map = '\tmap {}_{} {}'.format(siteInfo['name'], dirName, domain) listen_conf = re.sub(rep, 'secure 0\n' + map, listen_conf) public.writeFile(listen_file, listen_conf) @@ -2978,7 +3003,7 @@ server filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' conf = public.readFile(filename) if conf: - rep = "\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" + rep = r"\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" conf = re.sub(rep, '', conf) public.writeFile(filename, conf) @@ -2986,14 +3011,14 @@ server filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' conf = public.readFile(filename) if conf: - rep = "\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" + rep = r"\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" conf = re.sub(rep, '', conf) public.writeFile(filename, conf) # openlitespeed filename = self.setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' conf = public.readFile(filename) - rep = "#SUBDIR\s*{s}_{d}\s*START(\n|.)+#SUBDIR\s*{s}_{d}\s*END".format(s=siteName, d=binding['path']) + rep = "#SUBDIR\\s*{s}_{d}\\s*START(\n|.)+#SUBDIR\\s*{s}_{d}\\s*END".format(s=siteName, d=binding['path']) if conf: conf = re.sub(rep, '', conf) public.writeFile(filename, conf) @@ -3006,7 +3031,7 @@ server listen_file = self.setupPath + "/panel/vhost/openlitespeed/listen/80.conf" listen_conf = public.readFile(listen_file) if listen_conf: - map_reg = '\s*map\s*{}_{}.*'.format(siteName, binding['path']) + map_reg = r'\s*map\s*{}_{}.*'.format(siteName, binding['path']) listen_conf = re.sub(map_reg, '', listen_conf) public.writeFile(listen_file, listen_conf) # 清理detail文件 @@ -3074,11 +3099,11 @@ server conf = public.readFile(file) if conf == False: return public.return_msg_gettext(False, 'Configuration file not exist') if public.get_webserver() == 'nginx': - rep = "\s+index\s+(.+);" + rep = r"\s+index\s+(.+);" elif public.get_webserver() == 'apache': - rep = "DirectoryIndex\s+(.+)\n" + rep = "DirectoryIndex\\s+(.+)\n" else: - rep = "indexFiles\s+(.+)\n" + rep = "indexFiles\\s+(.+)\n" if re.search(rep, conf): tmp = re.search(rep, conf).groups() if public.get_webserver() == 'openlitespeed': @@ -3105,7 +3130,7 @@ server file = self.setupPath + '/panel/vhost/nginx/' + Name + '.conf' conf = public.readFile(file) if conf: - rep = "\s+index\s+.+;" + rep = r"\s+index\s+.+;" conf = re.sub(rep, "\n\tindex " + Index_L + ";", conf) public.writeFile(file, conf) @@ -3113,7 +3138,7 @@ server file = self.setupPath + '/panel/vhost/apache/' + Name + '.conf' conf = public.readFile(file) if conf: - rep = "DirectoryIndex\s+.+\n" + rep = "DirectoryIndex\\s+.+\n" conf = re.sub(rep, 'DirectoryIndex ' + Index_L + "\n", conf) public.writeFile(file, conf) @@ -3121,7 +3146,7 @@ server file = self.setupPath + '/panel/vhost/openlitespeed/detail/' + Name + '.conf' conf = public.readFile(file) if conf: - rep = "indexFiles\s+.+\n" + rep = "indexFiles\\s+.+\n" Index = Index.split(',') Index = [i for i in Index if i] Index = ",".join(Index) @@ -3155,9 +3180,9 @@ server file = self.setupPath + '/panel/vhost/apache/' + Name + '.conf' conf = public.readFile(file) if conf: - rep = "DocumentRoot\s+.+\n" + rep = "DocumentRoot\\s+.+\n" conf = re.sub(rep, 'DocumentRoot "' + Path + '"\n', conf) - rep = "\n", conf) public.writeFile(file, conf) @@ -3328,7 +3353,7 @@ server if not public.check_tcp(other_tmp[0],int(other_tmp[1])): return public.return_msg_gettext(False,'Unable to connect to [{}], please check whether the machine can connect to the target server'.format(get.other)) - other_conf = '''location ~ [^/]\.php(/|$) + other_conf = r'''location ~ [^/]\.php(/|$) {{ try_files $uri =404; fastcgi_pass {}; @@ -3338,19 +3363,19 @@ server }}'''.format(get.other) public.writeFile(other_rep,other_conf) conf = conf.replace(other_rep,dst) - rep = "include\s+enable-php-(\w{2,5})\.conf" + rep = r"include\s+enable-php-(\w{2,5})\.conf" tmp = re.search(rep,conf) if tmp: conf = conf.replace(tmp.group(),'include ' + dst) - elif re.search("enable-php-\d+-wpfastcgi.conf",conf): + elif re.search(r"enable-php-\d+-wpfastcgi.conf",conf): dst = 'enable-php-{}-wpfastcgi.conf'.format(version) conf = conf.replace(other_rep,dst) - rep = "enable-php-\d+-wpfastcgi.conf" + rep = r"enable-php-\d+-wpfastcgi.conf" tmp = re.search(rep, conf) if tmp:conf = conf.replace(tmp.group(),dst) else: dst = 'enable-php-'+version+'.conf' conf = conf.replace(other_rep,dst) - rep = "enable-php-(\w{2,5})\.conf" + rep = r"enable-php-(\w{2,5})\.conf" tmp = re.search(rep,conf) if tmp: conf = conf.replace(tmp.group(),dst) public.writeFile(file,conf) @@ -3372,7 +3397,7 @@ server file = self.setupPath + '/panel/vhost/apache/'+siteName+'.conf' conf = public.readFile(file) if conf and version != 'other': - rep = "(unix:/tmp/php-cgi-(\w{2,5})\.sock\|fcgi://localhost|fcgi://127.0.0.1:\d+)" + rep = r"(unix:/tmp/php-cgi-(\w{2,5})\.sock\|fcgi://localhost|fcgi://127.0.0.1:\d+)" tmp = re.search(rep,conf).group() conf = conf.replace(tmp,public.get_php_proxy(version,'apache')) public.writeFile(file,conf) @@ -3381,7 +3406,7 @@ server file = self.setupPath + '/panel/vhost/openlitespeed/detail/'+siteName+'.conf' conf = public.readFile(file) if conf: - rep = 'lsphp\d+' + rep = r'lsphp\d+' tmp = re.search(rep, conf) if tmp: conf = conf.replace(tmp.group(), 'lsphp' + version) @@ -3456,7 +3481,7 @@ server return public.return_msg_gettext(True, 'Base directory turned off!') if conf and "session.save_path" in conf: - rep = "session.save_path\s*=\s*(.*)" + rep = r"session.save_path\s*=\s*(.*)" s_path = re.search(rep, conf).groups(1)[0] public.writeFile(filename, conf + '\nopen_basedir={}/:/tmp/:{}'.format(path, s_path)) else: @@ -3478,12 +3503,12 @@ server c = public.readFile(f) if not c: return False if f: - rep = '\nphp_admin_value\s*open_basedir.*' + rep = '\nphp_admin_value\\s*open_basedir.*' result = re.search(rep, c) s = 'on' if not result: s = 'off' - rep = '\n#php_admin_value\s*open_basedir.*' + rep = '\n#php_admin_value\\s*open_basedir.*' result = re.search(rep, c) result = result.group() if s == 'on': @@ -3525,8 +3550,8 @@ server if os.path.exists(conf_path): old_conf = public.readFile(conf_path) rep = "(#PROXY-START(\n|.)+#PROXY-END)" - url_rep = "proxy_pass (.*);|ProxyPass\s/\s(.*)|Host\s(.*);" - host_rep = "Host\s(.*);" + url_rep = r"proxy_pass (.*);|ProxyPass\s/\s(.*)|Host\s(.*);" + host_rep = r"Host\s(.*);" if re.search(rep, old_conf): # 构造代理配置 if w == "nginx": @@ -3656,7 +3681,7 @@ server def __CheckUrl(self, get): sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk.settimeout(5) - rep = "(https?)://([\w\.\-]+):?([\d]+)?" + rep = r"(https?)://([\w\.\-]+):?([\d]+)?" h = re.search(rep, get.proxysite).group(1) d = re.search(rep, get.proxysite).group(2) try: @@ -3699,16 +3724,16 @@ server except: return public.return_msg_gettext(False, 'Please enter number') - rep = "http(s)?\:\/\/" - # repd = "http(s)?\:\/\/([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+([a-zA-Z0-9][a-zA-Z0-9]{0,62})+.?" + rep = r"http(s)?\:\/\/" + # repd = r"http(s)?\:\/\/([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+([a-zA-Z0-9][a-zA-Z0-9]{0,62})+.?" tod = "[a-zA-Z]+$" - repte = "[\?\=\[\]\)\(\*\&\^\%\$\#\@\!\~\`{\}\>\<\,\',\"]+" + repte = "[\\?\\=\\[\\]\\)\\(\\*\\&\\^\\%\\$\\#\\@\\!\\~\\`{\\}\\>\\<\\,\',\"]+" # 检测代理目录格式 if re.search(repte, get.proxydir): - return public.return_msg_gettext(False, "PROXY_DIR_ERR", ("?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\,',\"]",)) + return public.return_msg_gettext(False, "PROXY_DIR_ERR", ("?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]",)) # 检测发送域名格式 if get.todomain: - if re.search("[\}\{\#\;\"\']+",get.todomain): + if re.search("[\\}\\{\\#\\;\"\']+",get.todomain): return public.return_msg_gettext(False, 'Sent Domain format error :'+get.todomain+'
                                    The following special characters cannot exist [ } { # ; \" \' ] ') if public.get_webserver() != 'openlitespeed' and not get.todomain: get.todomain = "$host" @@ -3717,7 +3742,7 @@ server if not re.match(rep, get.proxysite): return public.return_msg_gettext(False, 'Sent domain format ERROR {}', (get.proxysite,)) if re.search(repte, get.proxysite): - return public.return_msg_gettext(False, "PROXY_URL_ERR", ("?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\,',\"]",)) + return public.return_msg_gettext(False, "PROXY_URL_ERR", ("?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]",)) # 检测目标url是否可用 # if re.match(repd, get.proxysite): # if self.__CheckUrl(get): @@ -3752,9 +3777,9 @@ server self.CheckProxy(get) ng_conf = public.readFile(ng_file) if not p_conf: - # rep = "%s[\w\s\~\/\(\)\.\*\{\}\;\$\n\#]+.{1,66}[\s\w\/\*\.\;]+include enable-php-" % public.GetMsg( + # rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.{1,66}[\\s\\w\\/\\*\\.\\;]+include enable-php-" % public.GetMsg( # "CLEAR_CACHE") - rep = "%s[\w\s\~\/\(\)\.\*\{\}\;\$\n\#]+.*\n.*" % public.get_msg_gettext("#Clear cache") + rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.*\n.*" % public.get_msg_gettext("#Clear cache") # ng_conf = re.sub(rep, 'include enable-php-', ng_conf) ng_conf = re.sub(rep, '', ng_conf) oldconf = '''location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ @@ -3770,7 +3795,7 @@ server access_log /dev/null; }''' if "(gif|jpg|jpeg|png|bmp|swf)$" not in ng_conf: - ng_conf = re.sub('access_log\s*/www', oldconf + "\n\taccess_log /www",ng_conf) + ng_conf = re.sub(r'access_log\s*/www', oldconf + "\n\taccess_log /www",ng_conf) public.writeFile(ng_file, ng_conf) return sitenamelist = [] @@ -3778,9 +3803,9 @@ server sitenamelist.append(i["sitename"]) if get.sitename in sitenamelist: - rep = "include.*\/proxy\/.*\*.conf;" + rep = r"include.*\/proxy\/.*\*.conf;" if not re.search(rep, ng_conf): - rep = "location.+\(gif[\w\|\$\(\)\n\{\}\s\;\/\~\.\*\\\\\?]+access_log\s+/" + rep = "location.+\\(gif[\\w\\|\\$\\(\\)\n\\{\\}\\s\\;\\/\\~\\.\\*\\\\\\?]+access_log\\s+/" ng_conf = re.sub(rep, 'access_log /', ng_conf) ng_conf = ng_conf.replace("include enable-php-", "%s\n" % public.get_msg_gettext( "#Clear cache") + cureCache + "\n\t%s\n\t" % public.get_msg_gettext( @@ -3788,9 +3813,9 @@ server public.writeFile(ng_file, ng_conf) else: - # rep = "%s[\w\s\~\/\(\)\.\*\{\}\;\$\n\#]+.{1,66}[\s\w\/\*\.\;]+include enable-php-" % public.GetMsg( + # rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.{1,66}[\\s\\w\\/\\*\\.\\;]+include enable-php-" % public.GetMsg( # "CLEAR_CACHE") - rep = "%s[\w\s\~\/\(\)\.\*\{\}\;\$\n\#]+.*\n.*" % public.get_msg_gettext("#Clear cache") + rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.*\n.*" % public.get_msg_gettext("#Clear cache") # ng_conf = re.sub(rep, 'include enable-php-', ng_conf) ng_conf = re.sub(rep,'',ng_conf) oldconf = '''location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ @@ -3806,7 +3831,7 @@ server access_log /dev/null; }''' if "(gif|jpg|jpeg|png|bmp|swf)$" not in ng_conf: - ng_conf = re.sub('access_log\s*/www', oldconf + "\n\taccess_log /www",ng_conf) + ng_conf = re.sub(r'access_log\s*/www', oldconf + "\n\taccess_log /www",ng_conf) public.writeFile(ng_file, ng_conf) # 设置apache配置 @@ -3821,20 +3846,20 @@ server if os.path.exists(ap_file): ap_conf = public.readFile(ap_file) if p_conf == "[]": - rep = "\n*%s\n+\s+IncludeOptiona[\s\w\/\.\*]+" % public.get_msg_gettext('#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext('#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') ap_conf = re.sub(rep, '', ap_conf) public.writeFile(ap_file, ap_conf) return if sitename in p_conf: - rep = "combined(\n|.)+IncludeOptional.*\/proxy\/.*conf" + rep = "combined(\n|.)+IncludeOptional.*\\/proxy\\/.*conf" rep1 = "combined" if not re.search(rep, ap_conf): ap_conf = ap_conf.replace(rep1, rep1 + "\n\t%s\n\t" % public.get_msg_gettext( '#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') + "\n\tIncludeOptional " + ap_proxyfile) public.writeFile(ap_file, ap_conf) else: - # rep = "\n*#引用反向代理(\n|.)+IncludeOptional.*\/proxy\/.*conf" - rep = "\n*%s\n+\s+IncludeOptiona[\s\w\/\.\*]+" % public.get_msg_gettext('#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') + # rep = "\n*#引用反向代理(\n|.)+IncludeOptional.*\\/proxy\\/.*conf" + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext('#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') ap_conf = re.sub(rep, '', ap_conf) public.writeFile(ap_file, ap_conf) @@ -3877,7 +3902,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] # vhost文件 vhostpath = "%s/panel/vhost/nginx/%s.conf" % (self.setupPath, get.sitename) - rep = "location\s+/[\n\s]+{" + rep = "location\\s+/[\n\\s]+{" for i in [rewriteconfpath, nginxconfpath, vhostpath]: conf = public.readFile(i) @@ -3954,12 +3979,12 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] # 检查是否存在#Set Nginx Cache def check_annotate(self, data): - rep = "\n\s*#Set\s*Nginx\s*Cache" + rep = "\n\\s*#Set\\s*Nginx\\s*Cache" if re.search(rep, data): return True def old_proxy_conf(self,conf,ng_conf_file,get): - rep = 'location\s*\~\*.*gif\|png\|jpg\|css\|js\|woff\|woff2\)\$' + rep = r'location\s*\~\*.*gif\|png\|jpg\|css\|js\|woff\|woff2\)\$' if not re.search(rep,conf): return conf @@ -4005,23 +4030,23 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri php_pass_proxy = get.proxysite if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: - php_pass_proxy = re.search('(https?\:\/\/[\w\.]+)', get.proxysite).group(0) - ng_conf = re.sub("location\s+[\^\~]*\s?%s" % conf[i]["proxydir"], "location ^~ " + get.proxydir, ng_conf) - ng_conf = re.sub("proxy_pass\s+%s" % conf[i]["proxysite"], "proxy_pass " + get.proxysite, ng_conf) - ng_conf = re.sub("location\s+\~\*\s+\\\.\(php.*\n\{\s*proxy_pass\s+%s.*" % (php_pass_proxy), - "location ~* \.(php|jsp|cgi|asp|aspx)$\n{\n\tproxy_pass %s;" % php_pass_proxy,ng_conf) - ng_conf = re.sub("location\s+\~\*\s+\\\.\(gif.*\n\{\s*proxy_pass\s+%s.*" % (php_pass_proxy), - "location ~* \.(gif|png|jpg|css|js|woff|woff2)$\n{\n\tproxy_pass %s;" % php_pass_proxy,ng_conf) + php_pass_proxy = re.search(r'(https?\:\/\/[\w\.]+)', get.proxysite).group(0) + ng_conf = re.sub(r"location\s+[\^\~]*\s?%s" % conf[i]["proxydir"], "location ^~ " + get.proxydir, ng_conf) + ng_conf = re.sub(r"proxy_pass\s+%s" % conf[i]["proxysite"], "proxy_pass " + get.proxysite, ng_conf) + ng_conf = re.sub("location\\s+\\~\\*\\s+\\\\.\\(php.*\n\\{\\s*proxy_pass\\s+%s.*" % (php_pass_proxy), + "location ~* \\.(php|jsp|cgi|asp|aspx)$\n{\n\tproxy_pass %s;" % php_pass_proxy,ng_conf) + ng_conf = re.sub("location\\s+\\~\\*\\s+\\\\.\\(gif.*\n\\{\\s*proxy_pass\\s+%s.*" % (php_pass_proxy), + "location ~* \\.(gif|png|jpg|css|js|woff|woff2)$\n{\n\tproxy_pass %s;" % php_pass_proxy,ng_conf) backslash = "" if "Host $host" in ng_conf: backslash = "\\" - ng_conf = re.sub("\sHost\s+%s" % backslash + conf[i]["todomain"], " Host " + get.todomain, ng_conf) + ng_conf = re.sub(r"\sHost\s+%s" % backslash + conf[i]["todomain"], " Host " + get.todomain, ng_conf) cache_rep = r"proxy_cache_valid\s+200\s+304\s+301\s+302\s+\d+m;((\n|.)+expires\s+\d+m;)*" if int(get.cache) == 1: if re.search(cache_rep, ng_conf): - expires_rep = "\{\n\s+expires\s+12h;" + expires_rep = "\\{\n\\s+expires\\s+12h;" ng_conf = re.sub(expires_rep, "{", ng_conf) ng_conf = re.sub(cache_rep, "proxy_cache_valid 200 304 301 302 {0}m;".format(get.cachetime), ng_conf) @@ -4031,7 +4056,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] # proxy_cache cache_one; # proxy_cache_key $host$uri$is_args$args; # proxy_cache_valid 200 304 301 302 %sm;""" % (get.cachetime) - ng_cache = """ + ng_cache = r""" if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) { expires 1m; @@ -4041,16 +4066,16 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] proxy_cache_key $host$uri$is_args$args; proxy_cache_valid 200 304 301 302 %sm;""" % (get.cachetime) if self.check_annotate(ng_conf): - cache_rep = '\n\s*#Set\s*Nginx\s*Cache(.|\n)*no-cache;\s*\n*\s*\}' + cache_rep = '\n\\s*#Set\\s*Nginx\\s*Cache(.|\n)*no-cache;\\s*\n*\\s*\\}' ng_conf = re.sub(cache_rep, '\n\t#Set Nginx Cache\n' + ng_cache, ng_conf) else: - # cache_rep = '#proxy_set_header\s+Connection\s+"upgrade";' + # cache_rep = r'#proxy_set_header\s+Connection\s+"upgrade";' cache_rep = r"proxy_set_header\s+REMOTE-HOST\s+\$remote_addr;" ng_conf = re.sub(cache_rep, r"\n\tproxy_set_header\s+REMOTE-HOST\s+\$remote_addr;\n\t#Set Nginx Cache" + ng_cache, ng_conf) else: - no_cache = """ + no_cache = r""" #Set Nginx Cache set $static_file%s 0; if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) @@ -4081,7 +4106,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] subfilter = json.loads(get.subfilter) if str(conf[i]["subfilter"]) != str(subfilter) or ng_conf.find('sub_filter_once') == -1: if re.search(sub_rep, ng_conf): - sub_rep = "\s+proxy_set_header\s+Accept-Encoding(.|\n)+off;" + sub_rep = "\\s+proxy_set_header\\s+Accept-Encoding(.|\n)+off;" ng_conf = re.sub(sub_rep, "", ng_conf) # 构造替换字符串 @@ -4102,21 +4127,21 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] ng_sub_filter = ng_sub_filter % (ng_subdata) else: ng_sub_filter = '' - sub_rep = '#Set\s+Nginx\s+Cache' + sub_rep = r'#Set\s+Nginx\s+Cache' ng_conf = re.sub(sub_rep, '#Set Nginx Cache\n' + ng_sub_filter, ng_conf) # 修改apache配置 ap_conf = public.readFile(ap_conf_file) - ap_conf = re.sub("ProxyPass\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]), + ap_conf = re.sub(r"ProxyPass\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]), "ProxyPass %s %s" % (get.proxydir, get.proxysite), ap_conf) - ap_conf = re.sub("ProxyPassReverse\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]), + ap_conf = re.sub(r"ProxyPassReverse\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]), "ProxyPassReverse %s %s" % (get.proxydir, get.proxysite), ap_conf) # 修改OLS配置 p = "{p}/panel/vhost/openlitespeed/proxy/{s}/{n}_{s}.conf".format(p=self.setupPath, n=proxyname_md5, s=get.sitename) c = public.readFile(p) if c: - rep = 'address\s+(.*)' + rep = r'address\s+(.*)' new_proxysite = 'address\t{}'.format(get.proxysite) c = re.sub(rep, new_proxysite, c) public.writeFile(p, c) @@ -4124,7 +4149,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] # p = "{p}/panel/vhost/openlitespeed/proxy/{s}/urlrewrite/{n}_{s}.conf".format(p=self.setupPath,n=proxyname_md5,s=get.sitename) c = public.readFile(ols_conf_file) if c: - rep = 'RewriteRule\s*\^{}\(\.\*\)\$\s+http://{}/\$1\s*\[P,E=Proxy-Host:{}\]'.format( + rep = r'RewriteRule\s*\^{}\(\.\*\)\$\s+http://{}/\$1\s*\[P,E=Proxy-Host:{}\]'.format( conf[i]["proxydir"], get.proxyname, conf[i]["todomain"]) new_content = 'RewriteRule ^{}(.*)$ http://{}/$1 [P,E=Proxy-Host:{}]'.format(get.proxydir, get.proxyname, @@ -4184,7 +4209,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] # 构造清理缓存连接 # 构造缓存配置 - ng_cache = """ + ng_cache = r""" if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) { expires 1m; @@ -4193,7 +4218,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] proxy_cache cache_one; proxy_cache_key $host$uri$is_args$args; proxy_cache_valid 200 304 301 302 %sm;""" % (cachetime) - no_cache = """ + no_cache = r""" set $static_file%s 0; if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) { @@ -4204,7 +4229,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] { add_header Cache-Control no-cache; }""" % (random_string,random_string,random_string) - # rep = "(https?://[\w\.]+)" + # rep = r"(https?://[\w\.]+)" # proxysite1 = re.search(rep,get.proxysite).group(1) ng_proxy = ''' #PROXY-START%s @@ -4259,7 +4284,7 @@ location %s # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri # php_pass_proxy = get.proxysite # if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: - # php_pass_proxy = re.search('(https?\:\/\/[\w\.]+)', get.proxysite).group(0) + # php_pass_proxy = re.search(r'(https?\:\/\/[\w\.]+)', get.proxysite).group(0) if advanced == 1: if proxydir[-1] != '/': proxydir = '{}/'.format(proxydir) @@ -4359,7 +4384,7 @@ location %s file = self.setupPath + "/nginx/conf/nginx.conf" conf = public.readFile(file) if (conf.find('include proxy.conf;') == -1): - rep = "include\s+mime.types;" + rep = r"include\s+mime.types;" conf = re.sub(rep, "include mime.types;\n\tinclude proxy.conf;", conf) public.writeFile(file, conf) @@ -4530,13 +4555,13 @@ location %s if name.find('.') == -1: return conf = public.readFile(filename) # 取域名 - rep = "server_name\s+(.+);" + rep = r"server_name\s+(.+);" tmp = re.search(rep, conf) if not tmp: return domains = tmp.groups()[0].split(' ') # 取根目录 - rep = "root\s+(.+);" + rep = r"root\s+(.+);" tmp = re.search(rep, conf) if not tmp: return path = tmp.groups()[0] @@ -4552,13 +4577,13 @@ location %s conf = public.readFile(filename) # 取域名 - rep = "ServerAlias\s+(.+)\n" + rep = "ServerAlias\\s+(.+)\n" tmp = re.search(rep, conf) if not tmp: return domains = tmp.groups()[0].split(' ') # 取根目录 - rep = u"DocumentRoot\s+\"(.+)\"\n" + rep = u"DocumentRoot\\s+\"(.+)\"\n" tmp = re.search(rep, conf) if not tmp: return path = tmp.groups()[0] @@ -4612,7 +4637,7 @@ location %s if conf.find(rep) != -1: conf = conf.replace(rep, "/dev/null") else: - # conf = re.sub('}\n\s+access_log\s+off', '}\n\taccess_log ' + rep, conf) + # conf = re.sub('}\n\\s+access_log\\s+off', '}\n\taccess_log ' + rep, conf) conf = conf.replace('access_log /dev/null', 'access_log ' + rep) public.writeFile(filename, conf) @@ -4620,12 +4645,12 @@ location %s filename = public.GetConfigValue('setup_path') + '/panel/vhost/openlitespeed/detail/' + get.name + '.conf' conf = public.readFile(filename) if conf: - rep = "\nerrorlog(.|\n)*compressArchive\s*1\s*\n}" + rep = "\nerrorlog(.|\n)*compressArchive\\s*1\\s*\n}" tmp = re.search(rep, conf) s = 'on' if not tmp: s = 'off' - rep = "\n#errorlog(.|\n)*compressArchive\s*1\s*\n#}" + rep = "\n#errorlog(.|\n)*compressArchive\\s*1\\s*\n#}" tmp = re.search(rep, conf) tmp = tmp.group() if tmp: @@ -4652,7 +4677,7 @@ location %s conf = public.readFile(filename) if not conf: return True if conf.find('#ErrorLog') != -1: return False - #if re.search("}\n*\s*access_log\s+off", conf): + #if re.search("}\n*\\s*access_log\\s+off", conf): if conf.find("access_log /dev/null") != -1: return False if re.search('\n#accesslog', conf): return False @@ -4748,7 +4773,7 @@ location %s if os.path.exists(get.configFile): conf = public.readFile(get.configFile) - rep = "\n\s*#AUTH_START(.|\n){1,200}#AUTH_END" + rep = "\n\\s*#AUTH_START(.|\n){1,200}#AUTH_END" conf = re.sub(rep, '', conf) public.writeFile(get.configFile, conf) @@ -4759,7 +4784,7 @@ location %s if os.path.exists(get.configFile): conf = public.readFile(get.configFile) - rep = "\n\s*#AUTH_START(.|\n){1,200}#AUTH_END" + rep = "\n\\s*#AUTH_START(.|\n){1,200}#AUTH_END" conf = re.sub(rep, '', conf) conf = conf.replace(' #Require all granted', " Require all granted") public.writeFile(get.configFile, conf) @@ -4772,7 +4797,7 @@ location %s siteName = get.siteName name = siteName.replace('.', '_') - rep = "^(\d{1,3}\.){3,3}\d{1,3}$" + rep = r"^(\d{1,3}\.){3,3}\d{1,3}$" if re.match(rep, siteName): return public.return_msg_gettext(False, 'ERROR, primary domain cannot be IP address!') # nginx @@ -4780,7 +4805,7 @@ location %s if os.path.exists(filename): conf = public.readFile(filename) if conf.find('#TOMCAT-START') != -1: return self.CloseTomcat(get) - tomcatConf = '''#TOMCAT-START + tomcatConf = r'''#TOMCAT-START location / { proxy_pass "http://%s:8080"; @@ -4843,7 +4868,7 @@ location %s filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) - rep = "\s*#TOMCAT-START(.|\n)+#TOMCAT-END" + rep = "\\s*#TOMCAT-START(.|\n)+#TOMCAT-END" conf = re.sub(rep, '', conf) public.writeFile(filename, conf) @@ -4851,7 +4876,7 @@ location %s filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) - rep = "\s*#TOMCAT-START(.|\n)+#TOMCAT-END" + rep = "\\s*#TOMCAT-START(.|\n)+#TOMCAT-END" conf = re.sub(rep, '', conf) public.writeFile(filename, conf) public.ExecShell('rm -rf ' + self.setupPath + '/panel/vhost/tomcat/' + name) @@ -4876,7 +4901,7 @@ location %s filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) - rep = '\s*root\s+(.+);' + rep = r'\s*root\s+(.+);' path = re.search(rep, conf) if not path: return public.return_msg_gettext(False, 'Get Site run path false') @@ -4885,7 +4910,7 @@ location %s filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) - rep = '\s*DocumentRoot\s*"(.+)"\s*\n' + rep = '\\s*DocumentRoot\\s*"(.+)"\\s*\n' path = re.search(rep, conf) if not path: return public.return_msg_gettext(False, 'Get Site run path false') @@ -4894,7 +4919,7 @@ location %s filename = self.setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) - rep = "vhRoot\s*(.*)" + rep = r"vhRoot\s*(.*)" path = re.search(rep, conf) if not path: return public.return_msg_gettext(False, 'Get Site run path false') @@ -4936,7 +4961,7 @@ location %s if os.path.exists(filename): conf = public.readFile(filename) if conf: - rep = '\s*root\s+(.+);' + rep = r'\s*root\s+(.+);' tmp = re.search(rep,conf) if tmp: path = tmp.groups()[0] @@ -4948,7 +4973,7 @@ location %s if os.path.exists(filename): conf = public.readFile(filename) if conf: - rep = '\s*DocumentRoot\s*"(.+)"\s*\n' + rep = '\\s*DocumentRoot\\s*"(.+)"\\s*\n' tmp = re.search(rep,conf) if tmp: path = tmp.groups()[0] @@ -4974,11 +4999,11 @@ location %s ols_conf = public.readFile(ols_conf_file) if not ols_conf: return - reg = '#VHOST\s*{s}\s*START(.|\n)+#VHOST\s*{s}\s*END'.format(s=sitename) + reg = '#VHOST\\s*{s}\\s*START(.|\n)+#VHOST\\s*{s}\\s*END'.format(s=sitename) tmp = re.search(reg, ols_conf) if not tmp: return - reg = "vhRoot\s*(.*)" + reg = r"vhRoot\s*(.*)" # tmp = re.search(reg,tmp.group()) # if not tmp: # return @@ -5002,13 +5027,13 @@ location %s path = self.setupPath + '/panel/vhost/nginx/' + defaultSite + '.conf' if os.path.exists(path): conf = public.readFile(path) - rep = "listen\s+80.+;" + rep = r"listen\s+80.+;" conf = re.sub(rep, 'listen 80;', conf, 1) - rep = "listen\s+\[::\]:80.+;" + rep = r"listen\s+\[::\]:80.+;" conf = re.sub(rep, 'listen [::]:80;', conf, 1) - rep = "listen\s+443.+;" + rep = r"listen\s+443.+;" conf = re.sub(rep, 'listen 443 ssl' + http2 + ';', conf, 1) - rep = "listen\s+\[::\]:443.+;" + rep = r"listen\s+\[::\]:443.+;" conf = re.sub(rep, 'listen [::]:443 ssl' + http2 + ';', conf, 1) public.writeFile(path, conf) @@ -5035,13 +5060,13 @@ location %s path = self.setupPath + '/panel/vhost/nginx/' + get.name + '.conf' if os.path.exists(path): conf = public.readFile(path) - rep = "listen\s+80\s*;" + rep = r"listen\s+80\s*;" conf = re.sub(rep, 'listen 80 default_server;', conf, 1) - rep = "listen\s+\[::\]:80\s*;" + rep = r"listen\s+\[::\]:80\s*;" conf = re.sub(rep, 'listen [::]:80 default_server;', conf, 1) - rep = "listen\s+443\s*ssl\s*\w*\s*;" + rep = r"listen\s+443\s*ssl\s*\w*\s*;" conf = re.sub(rep, 'listen 443 ssl' + http2 + ' default_server;', conf, 1) - rep = "listen\s+\[::\]:443\s*ssl\s*\w*\s*;" + rep = r"listen\s+\[::\]:443\s*ssl\s*\w*\s*;" conf = re.sub(rep, 'listen [::]:443 ssl' + http2 + ' default_server;', conf, 1) public.writeFile(path, conf) @@ -5142,12 +5167,12 @@ location %s if conf.find('SECURITY-START') != -1: rep = "#SECURITY-START(\n|.)+#SECURITY-END" tmp = re.search(rep, conf).group() - data['fix'] = re.search("\(.+\)\$", tmp).group().replace('(', '').replace(')$', '').replace('|', ',') + data['fix'] = re.search(r"\(.+\)\$", tmp).group().replace('(', '').replace(')$', '').replace('|', ',') try: data['domains'] = ','.join( - list(set(re.search("valid_referers\s+none\s+blocked\s+(.+);\n", tmp).groups()[0].split()))) + list(set(re.search("valid_referers\\s+none\\s+blocked\\s+(.+);\n", tmp).groups()[0].split()))) except: - data['domains'] = ','.join(list(set(re.search("valid_referers\s+(.+);\n", tmp).groups()[0].split()))) + data['domains'] = ','.join(list(set(re.search("valid_referers\\s+(.+);\n", tmp).groups()[0].split()))) data['status'] = True data['none'] = tmp.find('none blocked') != -1 try: @@ -5187,10 +5212,10 @@ location %s if conf.find('SECURITY-START') != -1: # 先替换域名部分,防止域名过多导致替换失败 - rep = "\s+valid_referers.+" + rep = r"\s+valid_referers.+" conf = re.sub(rep,'',conf) # 再替换配置部分 - rep = "\s+#SECURITY-START(\n|.){1,500}#SECURITY-END\n?" + rep = "\\s+#SECURITY-START(\n|.){1,500}#SECURITY-END\n?" conf = re.sub(rep,'\n',conf) public.write_log_gettext('Site manager', "Hotlink Protection for site [{}] disabled!", (get.name,)) else: @@ -5203,7 +5228,7 @@ location %s if get.return_rule[0] != '/': return public.return_msg_gettext(False, 'Response resources should use URI path or HTTP status code, such as: /test.png or 404') return_rule = 'rewrite /.* {} break'.format(get.return_rule) - rconf = '''%s + rconf = r'''%s location ~ .*\.(%s)$ { expires 30d; @@ -5216,7 +5241,7 @@ location %s #SECURITY-END include enable-php-''' % (public.get_msg_gettext('#SECURITY-START Hotlink protection configuration'), get.fix.strip().replace(',', '|'), get.domains.strip().replace(',', ' '), return_rule) - conf = re.sub("include\s+enable-php-", rconf, conf) + conf = re.sub(r"include\s+enable-php-", rconf, conf) public.write_log_gettext('Site manager', "Hotlink Protection for site [{}] enabled!", (get.name,)) public.writeFile(file, conf) @@ -5262,7 +5287,7 @@ location %s os.makedirs(cond_dir) file = cond_dir + get.name + '.conf' if get.status == '1': - conf = """ + conf = r""" RewriteCond %{HTTP_REFERER} !^$ RewriteCond %{HTTP_REFERER} !BTDOMAIN_NAME [NC] RewriteRule \.(BTPFILE)$ /404.html [R,NC] @@ -5270,7 +5295,7 @@ RewriteRule \.(BTPFILE)$ /404.html [R,NC] conf = conf.replace('BTDOMAIN_NAME', get.domains.replace(',', ' ')).replace('BTPFILE', get.fix.replace(',', '|')) else: - conf = """ + conf = r""" RewriteCond %{HTTP_REFERER} !BTDOMAIN_NAME [NC] RewriteRule \.(BTPFILE)$ /404.html [R,NC] """ diff --git a/class/panelSiteController.py b/class/panelSiteController.py index ef725f00..54d7ef27 100644 --- a/class/panelSiteController.py +++ b/class/panelSiteController.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ diff --git a/class/panelTask.py b/class/panelTask.py index 9904a49c..89d676de 100644 --- a/class/panelTask.py +++ b/class/panelTask.py @@ -1,10 +1,10 @@ #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2019-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2019-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------ @@ -369,7 +369,7 @@ class bt_task: self.install_rar() pass_opt = '-p-' if password: - password = password.replace("&","\&").replace('"','\"') + password = password.replace("&",r"\&").replace('"','\"') pass_opt = '-p"{}"'.format(password) public.ExecShell(rar_file + ' x '+ pass_opt +' -u -y "' + sfile + '" "' + dfile + '" &> ' + log_file) @@ -571,7 +571,7 @@ class bt_task: public.ExecShell("sed -i '/password=/d' " + my_cnf) if act: mycnf = public.readFile(my_cnf) - rep = "\[mysqldump\]\nuser=root" + rep = "\\[mysqldump\\]\nuser=root" sea = "[mysqldump]\n" subStr = sea + "user=root\npassword=\"" + root + "\"\n" mycnf = mycnf.replace(sea, subStr) diff --git a/class/panelVideo.py b/class/panelVideo.py index 22bad697..3298d85c 100644 --- a/class/panelVideo.py +++ b/class/panelVideo.py @@ -1,8 +1,8 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: hwliang <2020-05-18> # +------------------------------------------------------------------- diff --git a/class/panelWaf.py b/class/panelWaf.py index b76b3f71..ba03af41 100644 --- a/class/panelWaf.py +++ b/class/panelWaf.py @@ -1,6 +1,6 @@ #!/usr/bin/python #coding: utf-8 -# Author: lkqiang +# Author: lkqiang # panelWaf.py # code: 面板基础安全类 # +------------------------------------------------------------------- diff --git a/class/panelWarning.py b/class/panelWarning.py index 2399b3a8..47e6edd8 100644 --- a/class/panelWarning.py +++ b/class/panelWarning.py @@ -1,8 +1,8 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: hwliang <2020-08-04> # +------------------------------------------------------------------- @@ -227,7 +227,7 @@ class panelWarning: if not os.path.exists(self.new_vul_list): if zip_file != '': downfile = self.__path+'/'+zip_file - public.downloadFile("/safe_warning/{}".format(public.get_url(),zip_file), downfile) + public.downloadFile("{}/safe_warning/{}".format(public.get_url(), zip_file), downfile) o, e = public.ExecShell("unzip -o {} -d {}".format(downfile, self.__path)) # 解压报错 if e != "": @@ -1433,9 +1433,9 @@ if __name__ == "__main__": # #coding: utf-8 # # +------------------------------------------------------------------- -# # | 宝塔Linux面板 +# # | aaPanel # # +------------------------------------------------------------------- -# # | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# # | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # # +------------------------------------------------------------------- # # | Author: hwliang <2020-08-04> # # +------------------------------------------------------------------- diff --git a/class/panel_php_run.php b/class/panel_php_run.php index 3babd4bd..9885b037 100644 --- a/class/panel_php_run.php +++ b/class/panel_php_run.php @@ -1,10 +1,10 @@ +// | Author: hwliang // +------------------------------------------------------------------- // +------------------------------------------------------------------- diff --git a/class/panel_restore.py b/class/panel_restore.py index 485b3f3b..adf4df60 100644 --- a/class/panel_restore.py +++ b/class/panel_restore.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: zhwwen +# Author: zhwwen # ------------------------------------------------------------------- # # ------------------------------ @@ -127,7 +127,7 @@ class panel_restore: def restore_website_backup(self,args): """ @name 恢复站点文件 - @author zhwen + @author zhwen @parma file_name 备份得文件名 @parma site_id 网站id """ @@ -171,7 +171,7 @@ class panel_restore: def get_progress(self, get): """ @name 获取进度日志 - @author zhwen + @author zhwen """ # result = public.GetNumLines(self._progress_file, 20) result = public.ExecShell('tail -n 20 {}'.format(self._progress_file))[0] @@ -183,7 +183,7 @@ class panel_restore: def restore_db_backup(self,args): """ @name 恢复站点文件 - @author zhwen + @author zhwen @parma file_name 备份得文件名 /www/backup/database/db_test_com_20200817_112722.sql.gz|Google Drive|db_test_com_20200817_112722.sql.gz @parma obj_name 数据库名 """ diff --git a/class/panel_search.py b/class/panel_search.py index 2b820413..84f74d0b 100644 --- a/class/panel_search.py +++ b/class/panel_search.py @@ -2,7 +2,7 @@ # +------------------------------------------------------------------- # | version :1.0 # +------------------------------------------------------------------- -# | Author: 梁凯强 <1249648969@qq.com> +# | Author: 梁凯强 <1249648969@aapanel.com> # +------------------------------------------------------------------- # | 快速检索 # +-------------------------------------------------------------------- diff --git a/class/panel_telegram_bot.py b/class/panel_telegram_bot.py index 6c447970..c9b3b4e6 100644 --- a/class/panel_telegram_bot.py +++ b/class/panel_telegram_bot.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x3 +# | aaPanel x3 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: zhw +# | Author: zhw # +------------------------------------------------------------------- import public @@ -59,12 +59,42 @@ class panel_telegram_bot: # 使用tg机器人发送消息 def send_by_tg_bot(self,content,parse_mode=None): + """ + @author hezhihong + """ + "parse_mode 消息格式 html/markdown/markdownv2" - content = self.process_character(content) + + # content = self.process_character(content) conf = self.get_tg_conf() try: - bot = telegram.Bot(conf['bot_token']) - result = bot.send_message(text=content, chat_id=int(conf['my_id']), parse_mode="MarkdownV2") - return result + result= self.send_message(conf['bot_token'],conf['my_id'],content) + if not result['status']: + return False + return True except: - return False \ No newline at end of file + return False + + + def send_message(self, bot_token, chat_id, msg): + """ + tg发送信息 + @msg 消息正文 + @author hezhihong + """ + msg = self.process_character(msg) + url = 'https://api.telegram.org/bot{}/sendMessage'.format(bot_token) + data = { + 'chat_id': chat_id, + 'text': msg, + 'parse_mode':'MarkdownV2', + } + try: + import requests + response = requests.post(url, json=data) + if response.status_code == 200: + return public.returnMsg(True,0,response.json()) + else: + return public.returnMsg(False,json.loads(response.text)) + except Exception as e: + return public.returnMsg(False,str(e)) diff --git a/class/password.py b/class/password.py index c0d78e2f..68b43a6d 100644 --- a/class/password.py +++ b/class/password.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: lkqiang +# | Author: lkqiang # +------------------------------------------------------------------- # +-------------------------------------------------------------------- # | 密码管理 @@ -88,7 +88,7 @@ class password: # 开启密码登陆 def SetPassword(self, get): - ssh_password = '\n#?PasswordAuthentication\s\w+' + ssh_password = '\n#?PasswordAuthentication\\s\\w+' file = public.readFile('/etc/ssh/sshd_config') if len(re.findall(ssh_password, file)) == 0: file_result = file + '\nPasswordAuthentication yes' @@ -114,15 +114,15 @@ class password: public.ExecShell("ssh-keygen -t %s -P '' -f ~/.ssh/id_rsa |echo y" % type) if os.path.exists(file[0]): public.ExecShell('cat %s >%s && chmod 600 %s' % (file[0], file[-1], file[-1])) - rec = '\n#?RSAAuthentication\s\w+' - rec2 = '\n#?PubkeyAuthentication\s\w+' + rec = '\n#?RSAAuthentication\\s\\w+' + rec2 = '\n#?PubkeyAuthentication\\s\\w+' file = public.readFile('/etc/ssh/sshd_config') if len(re.findall(rec, file)) == 0: file = file + '\nRSAAuthentication yes' if len(re.findall(rec2, file)) == 0: file = file + '\nPubkeyAuthentication yes' file_ssh = re.sub(rec, '\nRSAAuthentication yes', file) file_result = re.sub(rec2, '\nPubkeyAuthentication yes', file_ssh) if ssh == 'no': - ssh_password = '\n#?PasswordAuthentication\s\w+' + ssh_password = '\n#?PasswordAuthentication\\s\\w+' if len(re.findall(ssh_password, file_result)) == 0: file_result = file_result + '\nPasswordAuthentication no' else: @@ -137,8 +137,8 @@ class password: # 关闭sshkey def StopKey(self, get): file = ['/root/.ssh/id_rsa.pub', '/root/.ssh/id_rsa', '/root/.ssh/authorized_keys'] - rec = '\n#?RSAAuthentication\s\w+' - rec2 = '\n#?PubkeyAuthentication\s\w+' + rec = '\n#?RSAAuthentication\\s\\w+' + rec2 = '\n#?PubkeyAuthentication\\s\\w+' file = public.readFile('/etc/ssh/sshd_config') file_ssh = re.sub(rec, '\n#RSAAuthentication no', file) file_result = re.sub(rec2, '\n#PubkeyAuthentication no', file_ssh) @@ -151,9 +151,9 @@ class password: def GetConfig(self, get): result = {} file = public.readFile('/etc/ssh/sshd_config') - rec = '\n#?RSAAuthentication\s\w+' - pubkey = '\n#?PubkeyAuthentication\s\w+' - ssh_password = '\nPasswordAuthentication\s\w+' + rec = '\n#?RSAAuthentication\\s\\w+' + pubkey = '\n#?PubkeyAuthentication\\s\\w+' + ssh_password = '\nPasswordAuthentication\\s\\w+' ret = re.findall(ssh_password, file) if not ret: result['password'] = 'no' @@ -183,7 +183,7 @@ class password: # 关闭密码方式 def StopPassword(self, get): file = public.readFile('/etc/ssh/sshd_config') - ssh_password = '\n#?PasswordAuthentication\s\w+' + ssh_password = '\n#?PasswordAuthentication\\s\\w+' file_result = re.sub(ssh_password, '\nPasswordAuthentication no', file) self.Wirte('/etc/ssh/sshd_config', file_result) self.RestartSsh() diff --git a/class/pay.py b/class/pay.py index 1af3d111..11d98a44 100644 --- a/class/pay.py +++ b/class/pay.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2019 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2019 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- # # ┏┓ ┏┓ diff --git a/class/php_execute_deny.py b/class/php_execute_deny.py index bb5b191a..60609f91 100644 --- a/class/php_execute_deny.py +++ b/class/php_execute_deny.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2020 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zhwen +# Author: zhwen #------------------------------------------------------------------- #------------------------------ @@ -24,7 +24,7 @@ class PhpExecuteDeny: def get_php_deny(self,args): ''' # 添加某个网站禁止运行PHP - author: zhwen + author: zhwen :param args: website 网站名 str :return: ''' @@ -45,7 +45,7 @@ class PhpExecuteDeny: deny_name = [i.split('_')[-1] for i in data] result = [] for i in deny_name: - reg = '#BEGIN_DENY_{}\n\s*location\s*\~\*\s*\^(.*)\.\*.*\((.*)\)\$'.format(i) + reg = '#BEGIN_DENY_{}\n\\s*location\\s*\\~\\*\\s*\\^(.*)\\.\\*.*\\((.*)\\)\\$'.format(i) deny_directory = re.search(reg,conf).groups()[0] deny_suffix = re.search(reg,conf).groups()[1] result.append({'name':i,'dir':deny_directory,'suffix':deny_suffix}) @@ -59,7 +59,7 @@ class PhpExecuteDeny: deny_name = [i.split('_')[-1] for i in data] result = [] for i in deny_name: - reg = '#BEGIN_DENY_{}\n\s* + author: zhwen :param args: website 网站名 str :param args: deny_name 规则名称 str :param args: suffix 禁止访问的后续名 str @@ -122,7 +122,7 @@ class PhpExecuteDeny: if not conf: return False if not dir and not suffix: - reg = '\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\n'.format(n=name) + reg = '\\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\n'.format(n=name) conf = re.sub(reg,'',conf) else: new = ''' @@ -143,10 +143,10 @@ class PhpExecuteDeny: if not conf: return False if not dir and not suffix: - reg = '\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}'.format(n=name) + reg = '\\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}'.format(n=name) conf = re.sub(reg,'',conf) else: - new = ''' + new = r''' #BEGIN_DENY_{n} Order allow,deny @@ -156,7 +156,7 @@ class PhpExecuteDeny: '''.format(n=name,d=dir,s=suffix) if '#BEGIN_DENY_{}'.format(name) in conf: return True - conf = re.sub('#DENY\s*FILES',new+'\n #DENY FILES',conf) + conf = re.sub(r'#DENY\s*FILES',new+'\n #DENY FILES',conf) public.writeFile(self.ap_website_conf,conf) return True @@ -165,17 +165,17 @@ class PhpExecuteDeny: if not conf: return False if not dir and not suffix: - reg = '#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\s*'.format(n=name) + reg = '#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\\s*'.format(n=name) conf = re.sub(reg,'',conf) else: - new = ''' + new = r''' #BEGIN_DENY_{n} rules RewriteRule ^{d}.*\.({s})$ - [F,L] #END_DENY_{n} '''.format(n=name,d=dir,s=suffix) if '#BEGIN_DENY_{}'.format(name) in conf: return True - conf = re.sub('autoLoadHtaccess\s*1','autoLoadHtaccess 1'+new,conf) + conf = re.sub(r'autoLoadHtaccess\s*1','autoLoadHtaccess 1'+new,conf) public.writeFile(self.ols_website_conf,conf) return True @@ -183,7 +183,7 @@ class PhpExecuteDeny: def del_php_deny(self,args): ''' # 添加某个网站禁止运行PHP - author: zhwen + author: zhwen :param args: website 网站名 str :param args: deny_name 规则名称 str :return: diff --git a/class/plugin_deployment.py b/class/plugin_deployment.py index 10f32f1b..1eb40cde 100644 --- a/class/plugin_deployment.py +++ b/class/plugin_deployment.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- #+-------------------------------------------------------------------- @@ -309,7 +309,7 @@ class plugin_deployment: if type(pinfo['enable_functions']) == str : pinfo['enable_functions'] = pinfo['enable_functions'].strip().split(',') php_f = public.GetConfigValue('setup_path') + '/php/' + php_version + '/etc/php.ini' php_c = public.readFile(php_f) - rep = "disable_functions\s*=\s{0,1}(.*)\n" + rep = "disable_functions\\s*=\\s{0,1}(.*)\n" tmp = re.search(rep,php_c).groups() disable_functions = tmp[0].split(',') for fun in pinfo['enable_functions']: @@ -441,7 +441,7 @@ class plugin_deployment: os.remove(p_info) i_ndex_html = path + '/index.html' if os.path.exists(i_ndex_html): os.remove(i_ndex_html) - if not self.copy_to(p_tmp,path): public.ExecShell(("\cp -arf " + p_tmp + '/. ' + path + '/').replace('//','/')) + if not self.copy_to(p_tmp,path): public.ExecShell((r"\cp -arf " + p_tmp + '/. ' + path + '/').replace('//','/')) except: pass public.ExecShell("rm -rf " + self.__tmp + '/*') return p_config diff --git a/class/process_task.py b/class/process_task.py index 7ecc9794..ca38f65c 100644 --- a/class/process_task.py +++ b/class/process_task.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- #-------------------------------- diff --git a/class/projectModel/bt_docker/dk_compose.py b/class/projectModel/bt_docker/dk_compose.py index eedf3a4e..21723a85 100644 --- a/class/projectModel/bt_docker/dk_compose.py +++ b/class/projectModel/bt_docker/dk_compose.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ @@ -128,7 +128,7 @@ class main :#line:22 import re #line:293 import projectModel .bt_docker .dk_container as dc #line:294 OO00OOOOO0O0OOOOO =[]#line:295 - O0OOOO000O00OOO0O =re .findall ("container_name\s*:\s*[\"\']+(.*)[\'\"]",OO0OOOO0O00OO0OO0 )#line:296 + O0OOOO000O00OOO0O =re .findall ("container_name\\s*:\\s*[\"\']+(.*)[\'\"]",OO0OOOO0O00OO0OO0 )#line:296 O0O0OO0O00OOO0O00 =dc .main ().get_list (OOO0OO00OO0OO00OO )#line:297 if not O0O0OO0O00OOO0O00 ["status"]:#line:298 return public .return_msg_gettext (False ,"Error getting container list!")#line:299 @@ -138,7 +138,7 @@ class main :#line:22 OO00OOOOO0O0OOOOO .append (O0OOO0O0OO0O0OO00 ['name'])#line:303 if OO00OOOOO0O0OOOOO :#line:304 return public .return_msg_gettext (False ,"The container name in the template:
                                    [{}] already exists!".format (", ".join (OO00OOOOO0O0OOOOO )))#line:305 - OO0000O0OO00OO0O0 ="(\d+):\d+"#line:307 + OO0000O0OO00OO0O0 =r"(\d+):\d+"#line:307 O00O00OOOO0OO00O0 =re .findall (OO0000O0OO00OO0O0 ,OO0OOOO0O00OO0OO0 )#line:308 for O0OOO00O00O0OOOOO in O00O00OOOO0OO00O0 :#line:309 if dp .check_socket (O0OOO00O00O0OOOOO ):#line:310 diff --git a/class/projectModel/bt_docker/dk_container.py b/class/projectModel/bt_docker/dk_container.py index 1c10d29f..ff1cce07 100644 --- a/class/projectModel/bt_docker/dk_container.py +++ b/class/projectModel/bt_docker/dk_container.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/bt_docker/dk_host.py b/class/projectModel/bt_docker/dk_host.py index be52a4be..e1db7cf2 100644 --- a/class/projectModel/bt_docker/dk_host.py +++ b/class/projectModel/bt_docker/dk_host.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw # ------------------------------------------------------------------- # ------------------------------ diff --git a/class/projectModel/bt_docker/dk_image.py b/class/projectModel/bt_docker/dk_image.py index 38f8a06d..6bd4c953 100644 --- a/class/projectModel/bt_docker/dk_image.py +++ b/class/projectModel/bt_docker/dk_image.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/bt_docker/dk_monitor.py b/class/projectModel/bt_docker/dk_monitor.py index c29757e2..5f884a1e 100644 --- a/class/projectModel/bt_docker/dk_monitor.py +++ b/class/projectModel/bt_docker/dk_monitor.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/bt_docker/dk_network.py b/class/projectModel/bt_docker/dk_network.py index b542a74e..cd6088bc 100644 --- a/class/projectModel/bt_docker/dk_network.py +++ b/class/projectModel/bt_docker/dk_network.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/bt_docker/dk_public.py b/class/projectModel/bt_docker/dk_public.py index 9ead8ba9..beb2c462 100644 --- a/class/projectModel/bt_docker/dk_public.py +++ b/class/projectModel/bt_docker/dk_public.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ @@ -42,7 +42,7 @@ def docker_client_low (url ="unix:///var/run/docker.sock"):#line:25 def get_cpu_count ():#line:39 import re #line:40 OOO00O00O0000O00O =open ('/proc/cpuinfo','r').read ()#line:41 - OO00O0000OO000OOO ="processor\s*:"#line:42 + OO00O0000OO000OOO =r"processor\s*:"#line:42 OO0OOO0O00O0O0OOO =re .findall (OO00O0000OO000OOO ,OOO00O00O0000O00O )#line:43 if not OO0OOO0O00O0O0OOO :#line:44 return 0 #line:45 diff --git a/class/projectModel/bt_docker/dk_registry.py b/class/projectModel/bt_docker/dk_registry.py index b2f83bd1..8a787c15 100644 --- a/class/projectModel/bt_docker/dk_registry.py +++ b/class/projectModel/bt_docker/dk_registry.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/bt_docker/dk_screen.py b/class/projectModel/bt_docker/dk_screen.py index be2f8165..688ce9d4 100644 --- a/class/projectModel/bt_docker/dk_screen.py +++ b/class/projectModel/bt_docker/dk_screen.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/bt_docker/dk_setup.py b/class/projectModel/bt_docker/dk_setup.py index 44aded50..5d3afc3e 100644 --- a/class/projectModel/bt_docker/dk_setup.py +++ b/class/projectModel/bt_docker/dk_setup.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ @@ -41,7 +41,7 @@ class main :#line:1 O00OO000O0OO0000O ="SAVE={}".format (OOO00O0000000OO0O )#line:36 public .writeFile (OOOO0O0OO00O00O00 ,O00OO000O0OO0000O )#line:37 return public .returnMsg (True ,"Set up successfully!")#line:38 - O00OO000O0OO0000O =re .sub ("SAVE\s*=\s*\d+","SAVE={}".format (OOO00O0000000OO0O ),O00OO000O0OO0000O )#line:39 + O00OO000O0OO0000O =re .sub (r"SAVE\s*=\s*\d+","SAVE={}".format (OOO00O0000000OO0O ),O00OO000O0OO0000O )#line:39 public .writeFile (OOOO0O0OO00O00O00 ,O00OO000O0OO0000O )#line:40 dp .write_log ("Set the monitoring time to [] days!".format (OOO00O0000000OO0O ))#line:41 return public .returnMsg (True ,"Set up successfully!")#line:42 diff --git a/class/projectModel/bt_docker/dk_status.py b/class/projectModel/bt_docker/dk_status.py index 0d23e631..b0a1ed1b 100644 --- a/class/projectModel/bt_docker/dk_status.py +++ b/class/projectModel/bt_docker/dk_status.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/bt_docker/dk_volume.py b/class/projectModel/bt_docker/dk_volume.py index 951b1a02..ded652ee 100644 --- a/class/projectModel/bt_docker/dk_volume.py +++ b/class/projectModel/bt_docker/dk_volume.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw #------------------------------------------------------------------- #------------------------------ diff --git a/class/projectModel/dockerModel.py b/class/projectModel/dockerModel.py index 35a02184..6a79438b 100644 --- a/class/projectModel/dockerModel.py +++ b/class/projectModel/dockerModel.py @@ -1,10 +1,10 @@ # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: zouhw +# Author: zouhw # ------------------------------------------------------------------- # ------------------------------ @@ -27,8 +27,8 @@ class main :#line:15 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 ,'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 ,'The called module name cannot contain characters other than \w')#line:47 + 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 diff --git a/class/projectModel/nodejsModel.py b/class/projectModel/nodejsModel.py index fb2d93d0..ba552046 100644 --- a/class/projectModel/nodejsModel.py +++ b/class/projectModel/nodejsModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -464,7 +464,7 @@ export PATH return public.return_error('Please install nodejs version manager first') project_name = get.project_name.strip() - if not re.match("^\w+$",project_name): + if not re.match(r"^\w+$",project_name): return public.return_error('The project name format is incorrect and supports letters, numbers, underscores, and expressions: ^[0-9A-Za-z_]$') if public.M('sites').where('name=?',(get.project_name,)).count(): diff --git a/class/projectModel/quotaModel.py b/class/projectModel/quotaModel.py index 92452283..2168f9e6 100644 --- a/class/projectModel/quotaModel.py +++ b/class/projectModel/quotaModel.py @@ -1,304 +1,304 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: hwliang -#------------------------------------------------------------------- - -#------------------------------ -# 磁盘配额管理 -#------------------------------ -import os ,public ,psutil ,json ,time ,re -from projectModel .base import projectBase -class main (projectBase ): - xfs_quota ="xfs_quota" - __O0O000000OO000000 ='{}/config/quota.json'.format (public .get_panel_path ()) - __O00OOOO00OO0OO0OO ='{}/config/mysql_quota.json'.format (public .get_panel_path ()) - __O0O00000000OOO0O0 =public .to_string ([84 ,104 ,105 ,115 ,32 ,102 ,101 ,97 ,116 ,117 ,114 ,101 ,32 ,105 ,115 ,32 ,101 ,120 ,99 ,108 ,117 ,115 ,105 ,118 ,101 ,32 ,116 ,111 ,32 ,116 ,104 ,101 ,32 ,112 ,114 ,111 ,32 ,101 ,100 ,105 ,116 ,105 ,111 ,110 ,44 ,32 ,112 ,108 ,101 ,97 ,115 ,101 ,32 ,97 ,99 ,116 ,105 ,118 ,97 ,116 ,101 ,32 ,105 ,116 ,32 ,102 ,105 ,114 ,115 ,116 ]) - def __init__ (OOO0000OO0OO0O00O ): - _O0OOO0O00O0O0O000 ='{}/data/quota_install.pl'.format (public .get_panel_path ()) - if not os .path .exists (_O0OOO0O00O0O0O000 ): - O0OO0OOO0OO0O0O00 ='/usr/sbin/xfs_quota' - if not os .path .exists (O0OO0OOO0OO0O0O00 ): - if os .path .exists ('/usr/bin/apt-get'): - public .ExecShell ('nohup apt-get install xfsprogs -y > /dev/null &') - else : - public .ExecShell ('nohup yum install xfsprogs -y > /dev/null &') - public .writeFile (_O0OOO0O00O0O0O000 ,'True') - if os .path .exists ("/sbin/xfs_quota"): - OOO0000OO0OO0O00O .xfs_quota ="/sbin/xfs_quota" - def __OO00OO00O0000O0OO (O00OOO0000000000O ,args =None ): - "" - OO00000OO0O00O0O0 =[] - for OO00O0OO0O0000OOO in psutil .disk_partitions (): - if OO00O0OO0O0000OOO .fstype =='xfs': - OO00000OO0O00O0O0 .append ((OO00O0OO0O0000OOO .mountpoint ,OO00O0OO0O0000OOO .device ,psutil .disk_usage (OO00O0OO0O0000OOO .mountpoint ).free ,OO00O0OO0O0000OOO .opts .split (','))) - return OO00000OO0O00O0O0 - def __O00OOOO0OO00O0OO0 (O000O00O0O0000O00 ,args =None ): - "" - return O000O00O0O0000O00 .__O000O0O0000OOOOOO (args .path ) - def __O000000O0O0000OOO (O00OOO00000OOOO00 ,O000O00O00O00OOOO ): - "" - O0OOO000OO000OOO0 =O00OOO00000OOOO00 .__OO00OO00O0000O0OO () - OO0OO000OO00O0OO0 =None - for O0O0OOOO00O00OOO0 in O0OOO000OO000OOO0 : - if O0O0OOOO00O00OOO0 [0 ]=="/": - OO0OO000OO00O0OO0 =O0O0OOOO00O00OOO0 - if O000O00O00O00OOOO .find (O0O0OOOO00O00OOO0 [0 ]+'/')==0 : - if not 'prjquota'in O0O0OOOO00O00OOO0 [3 ]: - return O0O0OOOO00O00OOO0 - return O0O0OOOO00O00OOO0 [1 ] - if OO0OO000OO00O0OO0 and O000O00O00O00OOOO .find (OO0OO000OO00O0OO0 [0 ])==0 : - if not 'prjquota'in OO0OO000OO00O0OO0 [3 ]: - return OO0OO000OO00O0OO0 - return OO0OO000OO00O0OO0 [1 ] - return '' - def __O000O0O0000OOOOOO (OO0OOO00OO000000O ,O0O0O0OOOOO0OOO0O ): - "" - if not os .path .exists (O0O0O0OOOOO0OOO0O ):return -1 - if not os .path .isdir (O0O0O0OOOOO0OOO0O ):return -2 - OO0O0OO0O0000000O =OO0OOO00OO000000O .__OO00OO00O0000O0OO () - O0O00OO00O0O0O00O =None - for O0OO0O0O00O00OO00 in OO0O0OO0O0000000O : - if O0OO0O0O00O00OO00 [0 ]=="/": - O0O00OO00O0O0O00O =O0OO0O0O00O00OO00 - if O0O0O0OOOOO0OOO0O .find (O0OO0O0O00O00OO00 [0 ]+'/')==0 : - return O0OO0O0O00O00OO00 [2 ]/1024 /1024 - if O0O00OO00O0O0O00O and O0O0O0OOOOO0OOO0O .find (O0O00OO00O0O0O00O [0 ])==0 : - return O0O00OO00O0O0O00O [2 ]/1024 /1024 - return -3 - def get_quota_path_list (OOOO000OO000O000O ,args =None ,get_path =None ): - "" - if not os .path .exists (OOOO000OO000O000O .__O0O000000OO000000 ): - public .writeFile (OOOO000OO000O000O .__O0O000000OO000000 ,'[]') - O00OO00OO000O0OO0 =json .loads (public .readFile (OOOO000OO000O000O .__O0O000000OO000000 )) - OO00OOO0O00OOOO00 =[] - for O0OOOO0O0O000O0O0 in O00OO00OO000O0OO0 : - if not os .path .exists (O0OOOO0O0O000O0O0 ['path'])or not os .path .isdir (O0OOOO0O0O000O0O0 ['path'])or os .path .islink (O0OOOO0O0O000O0O0 ['path']):continue - if get_path : - if O0OOOO0O0O000O0O0 ['path']==get_path : - OOO00OO0OOO0O0OO0 =psutil .disk_usage (O0OOOO0O0O000O0O0 ['path']) - O0OOOO0O0O000O0O0 ['used']=OOO00OO0OOO0O0OO0 .used - O0OOOO0O0O000O0O0 ['free']=OOO00OO0OOO0O0OO0 .free - return O0OOOO0O0O000O0O0 - else : - continue - OOO00OO0OOO0O0OO0 =psutil .disk_usage (O0OOOO0O0O000O0O0 ['path']) - O0OOOO0O0O000O0O0 ['used']=OOO00OO0OOO0O0OO0 .used - O0OOOO0O0O000O0O0 ['free']=OOO00OO0OOO0O0OO0 .free - OO00OOO0O00OOOO00 .append (O0OOOO0O0O000O0O0 ) - if get_path : - return {'size':0 ,'used':0 ,'free':0 } - if len (OO00OOO0O00OOOO00 )!=len (O00OO00OO000O0OO0 ): - public .writeFile (OOOO000OO000O000O .__O0O000000OO000000 ,json .dumps (OO00OOO0O00OOOO00 )) - return O00OO00OO000O0OO0 - def get_quota_mysql_list (OO000O0O00OOOOO0O ,args =None ,get_name =None ): - "" - if not os .path .exists (OO000O0O00OOOOO0O .__O00OOOO00OO0OO0OO ): - public .writeFile (OO000O0O00OOOOO0O .__O00OOOO00OO0OO0OO ,'[]') - OO00OO0000O0O0OOO =json .loads (public .readFile (OO000O0O00OOOOO0O .__O00OOOO00OO0OO0OO )) - O00O00OOO0OOO00OO =[] - OOOO000000OO0O0O0 =public .M ('databases') - for OOO0OO0O000O0O00O in OO00OO0000O0O0OOO : - if get_name : - if OOO0OO0O000O0O00O ['db_name']==get_name : - OOO0OO0O000O0O00O ['used']=OOO0OO0O000O0O00O ['used']=int (public .get_database_size_by_name (OOO0OO0O000O0O00O ['db_name'])) - _O0OO00O000O0OOO00 =OOO0OO0O000O0O00O ['size']*1024 *1024 - if (OOO0OO0O000O0O00O ['used']>_O0OO00O000O0OOO00 and OOO0OO0O000O0O00O ['insert_accept'])or (OOO0OO0O000O0O00O ['used']<_O0OO00O000O0OOO00 and not OOO0OO0O000O0O00O ['insert_accept']): - OO000O0O00OOOOO0O .mysql_quota_check () - return OOO0OO0O000O0O00O - else : - if OOOO000000OO0O0O0 .where ('name=?',OOO0OO0O000O0O00O ['db_name']).count (): - if args :OOO0OO0O000O0O00O ['used']=int (public .get_database_size_by_name (OOO0OO0O000O0O00O ['db_name'])) - O00O00OOO0OOO00OO .append (OOO0OO0O000O0O00O ) - OOOO000000OO0O0O0 .close () - if get_name : - return {'size':0 ,'used':0 } - if len (O00O00OOO0OOO00OO )!=len (OO00OO0000O0O0OOO ): - public .writeFile (OO000O0O00OOOOO0O .__O00OOOO00OO0OO0OO ,json .dumps (O00O00OOO0OOO00OO )) - return O00O00OOO0OOO00OO - def __OOO000O00000O0OO0 (OOOOOOOO0OOO000OO ,OOO0000OOO00O000O ,OO0OOO0000000OOO0 ,O0OOO0OO00O00O0OO ,OOOO00000OOOO0OO0 ): - "" - O0OO0O00O000O00O0 =OOO0000OOO00O000O .execute ("REVOKE ALL PRIVILEGES ON `{}`.* FROM '{}'@'{}';".format (O0OOO0OO00O00O0OO ,OO0OOO0000000OOO0 ,OOOO00000OOOO0OO0 )) - if O0OO0O00O000O00O0 :raise public .PanelError ('Failed to remove insert permission for database user:{}'.format (O0OO0O00O000O00O0 )) - O0OO0O00O000O00O0 =OOO0000OOO00O000O .execute ("GRANT SELECT, DELETE, CREATE, DROP, REFERENCES, INDEX, CREATE TEMPORARY TABLES, LOCK TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `{}`.* TO '{}'@'{}';".format (O0OOO0OO00O00O0OO ,OO0OOO0000000OOO0 ,OOOO00000OOOO0OO0 )) - if O0OO0O00O000O00O0 :raise public .PanelError ('Failed to remove insert permission for database user:{}'.format (O0OO0O00O000O00O0 )) - OOO0000OOO00O000O .execute ("FLUSH PRIVILEGES;") - return True - def __O000O000O0OOOOOOO (OO00OOO00OO0OOO00 ,O000OOOO0OO0O00OO ,O0OOO0OOO0OO00O00 ,O00OOO0OO0OO0OOOO ,OOOOOOOO0OO00O000 ): - "" - OOO0O000OOOOO0O00 =O000OOOO0OO0O00OO .execute ("REVOKE ALL PRIVILEGES ON `{}`.* FROM '{}'@'{}';".format (O00OOO0OO0OO0OOOO ,O0OOO0OOO0OO00O00 ,OOOOOOOO0OO00O000 )) - if OOO0O000OOOOO0O00 :raise public .PanelError ('Failed to restore insert privileges for database user:{}'.format (OOO0O000OOOOO0O00 )) - OOO0O000OOOOO0O00 =O000OOOO0OO0O00OO .execute ("GRANT ALL PRIVILEGES ON `{}`.* TO '{}'@'{}';".format (O00OOO0OO0OO0OOOO ,O0OOO0OOO0OO00O00 ,OOOOOOOO0OO00O000 )) - if OOO0O000OOOOO0O00 :raise public .PanelError ('Failed to restore insert privileges for database user:{}'.format (OOO0O000OOOOO0O00 )) - O000OOOO0OO0O00OO .execute ("FLUSH PRIVILEGES;") - return True - def mysql_quota_service (O00000OO00O0O0000 ): - "" - while 1 : - time .sleep (600 ) - O00000OO00O0O0000 .mysql_quota_check () - def __O0OO00O00O0OOO0OO (O0OO0O0000OOO0O0O ,OO0O0OOOOO0O00O0O ): - try : - if type (OO0O0OOOOO0O00O0O )!=list and type (OO0O0OOOOO0O00O0O )!=str :OO0O0OOOOO0O00O0O =list (OO0O0OOOOO0O00O0O ) - return OO0O0OOOOO0O00O0O - except :return [] - def mysql_quota_check (OO0O0OOO00O0OO00O ): - "" - if not OO0O0OOO00O0OO00O .__OO000OOO0OO0O00OO ():return public .returnMsg (False ,OO0O0OOO00O0OO00O .__O0O00000000OOO0O0 ) - O00000OOO00OO000O =OO0O0OOO00O0OO00O .get_quota_mysql_list () - for OO0O0O0O0OO00OOO0 in O00000OOO00OO000O : - try : - if OO0O0O0O0OO00OOO0 ['size']<1 : - if not OO0O0O0O0OO00OOO0 ['insert_accept']: - OO0O0OOO00O0OO00O .__O000O000O0OOOOOOO (O0OOOO000OOO0O0OO ,O0000O00OO0O00O0O ,OO0O0O0O0OO00OOO0 ['db_name'],O000O00O0000OOO0O [0 ]) - OO0O0O0O0OO00OOO0 ['insert_accept']=True - public .WriteLog ('Quota','Database [{}] quota has been closed, restore insert privileges'.format (OO0O0O0O0OO00OOO0 ['db_name'])) - continue - O000O000O00000O0O =public .get_database_size_by_name (OO0O0O0O0OO00OOO0 ['db_name'])/1024 /1024 - O0000O00OO0O00O0O =public .M ('databases').where ('name=?',(OO0O0O0O0OO00OOO0 ['db_name'],)).getField ('username') - O0OOOO000OOO0O0OO =public .get_mysql_obj (OO0O0O0O0OO00OOO0 ['db_name']) - O0O0OOO0O0O00OOOO =OO0O0OOO00O0OO00O .__O0OO00O00O0OOO0OO (O0OOOO000OOO0O0OO .query ("select Host from mysql.user where User='"+O0000O00OO0O00O0O +"'")) - if O000O000O00000O0O time .time () - def modify_mysql_quota (OO00OO0OOOO00O000 ,OOOO00O0OO000OOOO ): - "" - if not OO00OO0OOOO00O000 .__OO000OOO0OO0O00OO ():return public .returnMsg (False ,OO00OO0OOOO00O000 .__O0O00000000OOO0O0 ) - if not os .path .exists (OO00OO0OOOO00O000 .__O00OOOO00OO0OO0OO ): - public .writeFile (OO00OO0OOOO00O000 .__O00OOOO00OO0OO0OO ,'[]') - if not re .match (r"^\d+$",OOOO00O0OO000OOOO .size ):return public .returnMsg (False ,'Quota size must be an integer!') - OO0O0OOOO00000OOO =int (OOOO00O0OO000OOOO ['size']) - OO0OO0OO000O0O00O =OOOO00O0OO000OOOO .db_name .strip () - O0OOOO0OOOO0O0000 =json .loads (public .readFile (OO00OO0OOOO00O000 .__O00OOOO00OO0OO0OO )) - O0O00O0O000OOO0O0 =False - for OO00O0000O0OOOOO0 in O0OOOO0OOOO0O0000 : - if OO00O0000O0OOOOO0 ['db_name']==OO0OO0OO000O0O00O : - OO00O0000O0OOOOO0 ['size']=OO0O0OOOO00000OOO - O0O00O0O000OOO0O0 =True - break - if O0O00O0O000OOO0O0 : - public .writeFile (OO00OO0OOOO00O000 .__O00OOOO00OO0OO0OO ,json .dumps (O0OOOO0OOOO0O0000 )) - public .WriteLog ('Quota','Modify the quota limit of database [{db_name}] to: {size}MB'.format (db_name =OO0OO0OO000O0O00O ,size =OO0O0OOOO00000OOO )) - OO00OO0OOOO00O000 .mysql_quota_check () - return public .returnMsg (True ,'Successfully modified') - return OO00OO0OOOO00O000 .__O0000OO00OO00O0O0 (OOOO00O0OO000OOOO ) - def __O0OOOO00O00000OOO (O0OO0O0O00O0OO00O ,O0O0O0O0O000O0OOO ): - "" - O0OOO0OO0O0000O00 =[] - OO0000OO0OO00O0O0 =public .ExecShell ("{xfs_quota} -x -c report {mountpoint}|awk '{{print $1}}'|grep '#'".format (xfs_quota =O0OO0O0O00O0OO00O .xfs_quota ,mountpoint =O0O0O0O0O000O0OOO ))[0 ] - if not OO0000OO0OO00O0O0 :return O0OOO0OO0O0000O00 - for O0OO0O00OO0O0OO0O in OO0000OO0OO00O0O0 .split ('\n'): - if O0OO0O00OO0O0OO0O :O0OOO0OO0O0000O00 .append (int (O0OO0O00OO0O0OO0O .split ('#')[-1 ])) - return O0OOO0OO0O0000O00 - def __OO00O000OOOOOOO00 (O0OOO0O00OOOO0OOO ,OO0000O0OOOOOOO0O ,OOO0000OO0O0000OO ): - "" - O00O00OO0O000O000 =1001 - if not OO0000O0OOOOOOO0O :return O00O00OO0O000O000 - O00O00OO0O000O000 =OO0000O0OOOOOOO0O [-1 ]['id']+1 - O0OO00OO00O00O000 =sorted (O0OOO0O00OOOO0OOO .__O0OOOO00O00000OOO (OOO0000OO0O0000OO )) - if O0OO00OO00O00O000 : - if O0OO00OO00O00O000 [-1 ]>O00O00OO0O000O000 : - O00O00OO0O000O000 =O0OO00OO00O00O000 [-1 ]+1 - return O00O00OO0O000O000 - def __OOOO0O000OOOO00O0 (OO0OO00O0O0000O0O ,O000O0OOOO0OOO00O ): - "" - if not OO0OO00O0O0000O0O .__OO000OOO0OO0O00OO ():return public .returnMsg (False ,OO0OO00O0O0000O0O .__O0O00000000OOO0O0 ) - OOOO000000O00OO00 =O000O0OOOO0OOO00O .path .strip () - OO000OOO0000OO00O =int (O000O0OOOO0OOO00O .size ) - if not os .path .exists (OOOO000000O00OO00 ):return public .returnMsg (False ,'The specified directory does not exist') - if os .path .isfile (OOOO000000O00OO00 ):return public .returnMsg (False ,'this is not a valid directory!') - if os .path .islink (OOOO000000O00OO00 ):return public .returnMsg (False ,'The specified directory is a soft link!') - OOOOO000OO0000OOO =OO0OO00O0O0000O0O .get_quota_path_list () - for O00O0OOOO0O00O000 in OOOOO000OO0000OOO : - if O00O0OOOO0O00O000 ['path']==OOOO000000O00OO00 :return public .returnMsg (False ,'The specified directory has already set a quota!') - OO000O0OO0000OO00 =OO0OO00O0O0000O0O .__O000O0O0000OOOOOO (OOOO000000O00OO00 ) - if OO000O0OO0000OO00 ==-3 :return public .returnMsg (False ,'The partition where the specified directory is located is not an XFS partition and does not support directory quotas!') - if OO000O0OO0000OO00 ==-2 :return public .returnMsg (False ,'this is not a valid directory!') - if OO000O0OO0000OO00 ==-1 :return public .returnMsg (False ,'The specified directory does not exist!') - if OO000OOO0000OO00O >OO000O0OO0000OO00 :return public .returnMsg (False ,'Insufficient quota capacity available for the specified disk!') - OOO0O0O0O0O000O0O =OO0OO00O0O0000O0O .__O000000O0O0000OOO (OOOO000000O00OO00 ) - if not OOO0O0O0O0O000O0O :return public .returnMsg (False ,'The specified directory is not in the xfs disk partition!') - if isinstance (OOO0O0O0O0O000O0O ,tuple ):return public .returnMsg (False ,'The directory quota function is not enabled for this xfs partition, please increase the [prjquota] parameter when mounting the partition

                                    /etc/fstab File configuration example:

                                    {mountpoint}       {path}           xfs             defaults,prjquota       0 0

                                    Note: You need to remount the partition or reboot the server to take effect


                                    The setup is completed and the error still occurs, please refer to
                                    How to enable disk quota for root directory'.format (mountpoint =OOO0O0O0O0O000O0O [1 ],path =OOO0O0O0O0O000O0O [0 ])) - O0000OO000OO00O0O =OO0OO00O0O0000O0O .__OO00O000OOOOOOO00 (OOOOO000OO0000OOO ,OOO0O0O0O0O000O0O ) - OO0O0O0O0OOOOO00O =public .ExecShell ("{xfs_quota} -x -c 'project -s -p {path} {quota_id}'".format (path =OOOO000000O00OO00 ,quota_id =O0000OO000OO00O0O ,xfs_quota =OO0OO00O0O0000O0O .xfs_quota )) - if OO0O0O0O0OOOOO00O [1 ]:return public .returnMsg (False ,OO0O0O0O0OOOOO00O [1 ]) - OO0O0O0O0OOOOO00O =public .ExecShell ("{xfs_quota} -x -c 'limit -p bhard={size}m {quota_id}' {mountpoint}".format (quota_id =O0000OO000OO00O0O ,size =OO000OOO0000OO00O ,mountpoint =OOO0O0O0O0O000O0O ,xfs_quota =OO0OO00O0O0000O0O .xfs_quota )) - if OO0O0O0O0OOOOO00O [1 ]:return public .returnMsg (False ,OO0O0O0O0OOOOO00O [1 ]) - OOOOO000OO0000OOO .append ({'path':O000O0OOOO0OOO00O .path ,'size':OO000OOO0000OO00O ,'id':O0000OO000OO00O0O }) - public .writeFile (OO0OO00O0O0000O0O .__O0O000000OO000000 ,json .dumps (OOOOO000OO0000OOO )) - public .WriteLog ('Quota','The quota limit for creating directory [{path}] is: {size}MB'.format (path =OOOO000000O00OO00 ,size =OO000OOO0000OO00O )) - return public .returnMsg (True ,'Added successfully') - def modify_path_quota (O0O0OOOOO0OOO0OOO ,OO0O0000000OO0OO0 ): - "" - if not O0O0OOOOO0OOO0OOO .__OO000OOO0OO0O00OO ():return public .returnMsg (False ,O0O0OOOOO0OOO0OOO .__O0O00000000OOO0O0 ) - O00O0O00OOO0O0O0O =OO0O0000000OO0OO0 .path .strip () - if not re .match (r"^\d+$",OO0O0000000OO0OO0 .size ):return public .returnMsg (False ,'Quota size must be an integer!') - O0O00O0OOO0O0O0OO =int (OO0O0000000OO0OO0 .size ) - if not os .path .exists (O00O0O00OOO0O0O0O ):return public .returnMsg (False ,'The specified directory does not exist') - if os .path .isfile (O00O0O00OOO0O0O0O ):return public .returnMsg (False ,'This is not a valid directory!') - if os .path .islink (O00O0O00OOO0O0O0O ):return public .returnMsg (False ,'The specified directory is a soft link!') - OO0O00O00OO0000OO =O0O0OOOOO0OOO0OOO .get_quota_path_list () - OOOO0OO0OOOOO0OOO =0 - for OOO00000OOOOO0OOO in OO0O00O00OO0000OO : - if OOO00000OOOOO0OOO ['path']==O00O0O00OOO0O0O0O : - OOOO0OO0OOOOO0OOO =OOO00000OOOOO0OOO ['id'] - break - if not OOOO0OO0OOOOO0OOO :return O0O0OOOOO0OOO0OOO .__OOOO0O000OOOO00O0 (OO0O0000000OO0OO0 ) - O0O0O00OOOOOO00O0 =O0O0OOOOO0OOO0OOO .__O000O0O0000OOOOOO (O00O0O00OOO0O0O0O ) - if O0O0O00OOOOOO00O0 ==-3 :return public .returnMsg (False ,'The partition where the specified directory is located is not an XFS partition, and directory quotas are not supported!') - if O0O0O00OOOOOO00O0 ==-2 :return public .returnMsg (False ,'This is not a valid directory!') - if O0O0O00OOOOOO00O0 ==-1 :return public .returnMsg (False ,'The specified directory does not exist!') - if O0O00O0OOO0O0O0OO >O0O0O00OOOOOO00O0 :return public .returnMsg (False ,'Insufficient quota capacity available for the specified disk!') - OOO0OO0O0000O0OO0 =O0O0OOOOO0OOO0OOO .__O000000O0O0000OOO (O00O0O00OOO0O0O0O ) - if not OOO0OO0O0000O0OO0 :return public .returnMsg (False ,'The specified directory is not in the xfs disk partition!') - if isinstance (OOO0OO0O0000O0OO0 ,tuple ):return public .returnMsg (False ,'The directory quota function is not enabled for this xfs partition, please increase the [prjquota] parameter when mounting the partition

                                    /etc/fstab File configuration example:

                                    {mountpoint}       {path}           xfs             defaults,prjquota       0 0

                                    Note: After the configuration is complete, you need to remount the partition or restart the server to take effect

                                    '.format (mountpoint =OOO0OO0O0000O0OO0 [1 ],path =OOO0OO0O0000O0OO0 [0 ])) - OO00O0OOO00OOOO0O =public .ExecShell ("{xfs_quota} -x -c 'project -s -p {path} {quota_id}'".format (path =O00O0O00OOO0O0O0O ,quota_id =OOOO0OO0OOOOO0OOO ,xfs_quota =O0O0OOOOO0OOO0OOO .xfs_quota )) - if OO00O0OOO00OOOO0O [1 ]:return public .returnMsg (False ,OO00O0OOO00OOOO0O [1 ]) - OO00O0OOO00OOOO0O =public .ExecShell ("{xfs_quota} -x -c 'limit -p bhard={size}m {quota_id}' {mountpoint}".format (quota_id =OOOO0OO0OOOOO0OOO ,size =O0O00O0OOO0O0O0OO ,mountpoint =OOO0OO0O0000O0OO0 ,xfs_quota =O0O0OOOOO0OOO0OOO .xfs_quota )) - if OO00O0OOO00OOOO0O [1 ]:return public .returnMsg (False ,OO00O0OOO00OOOO0O [1 ]) - for OOO00000OOOOO0OOO in OO0O00O00OO0000OO : - if OOO00000OOOOO0OOO ['path']==O00O0O00OOO0O0O0O : - OOO00000OOOOO0OOO ['size']=O0O00O0OOO0O0O0OO - break - public .writeFile (O0O0OOOOO0OOO0OOO .__O0O000000OO000000 ,json .dumps (OO0O00O00OO0000OO )) - public .WriteLog ('Quota','Modify the quota limit of directory [{path}] to:{size}MB'.format (path =O00O0O00OOO0O0O0O ,size =O0O00O0OOO0O0O0OO )) - return public .returnMsg (True ,'Successfully modified') +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfoMbif5SSuhU+ED0z8LQ0uRP+8tAmQ/sx0zSKtQtV+DZAsSdFCQjtILVBd7FvDmtwc6THtaa0pvFowRX+Q+yEWI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +PEKPgJeDDCLnL9UcS39EYQ0oxS0YHncLtyklh46m5EBkNzGCoSotSCFeO4A9wJZj +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +Z71ucSR75ppLp8TcGPXjF3pMbrAmHfUi73meeu5lk5g= +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 ++aWWMzKsJO+aM6tUDo7l22gaN/EfQwY9pVk1jkrGHubq5BMU7tPycF7U4lfA+PnV +NyzlU2YOTFNZFPd7RK11YaUw7ZYsBQH6PJOb5a5HaopbJMLTygrp/eIUmAMGN6Od +s+bHc/90a3lxFTPvWEu+yQ4hdETfvFmr96jd16JVhvU= +a4WDhREt5YBbYTTI0+NOTlxdCabhZWLzl3dzPJCL2ns= +H4fJMREGje1RhDKY1u71WkFaRhA2/BXughjkfaZp1RetweG/KCGDvlR75NN9AWxqG7njwjkItebPDjCvvD0ScUXpMVMO8Q7fKdfHqxQvhc0Qo+XFxK2fsZP0OPVxNa30 +agP6Y0IXgzGYLtZLVcCCZ/0T599xXC8rG/FDWEro2DxfJQNabVUQQfe30RgJll2GWgYLMccciONnZHIUS3Z5qWHn2s7WlDckd+TogFxnamkRlczRvd0o9wecbthheLVo +I3U+Q+NUzPWPEuq1msm2jktlvsiPJjAhTZasqurt+e02ugu/S01xy+8g1TfxXg2xBw7u6ZrdCIKv6h8lGzSaB2XtdEhETr84GetEdGL9JNROgGPC7MQ7C+tI+RwFXBStB1/PylFqV2Y5eewvjz6yzB0KARJjv+PUr3VN5C1ldweqNOD6WfVOtgYZlJOd/RKai2gm1Xe/1qHEsrMqBKfzR0r88ShNiOdlXNPN5Q2Kc+9CXEnQQ5w/3oU63fxP1wk3R02xH78wobUwoMnSpGoiOFRVJcXce2/cO/j80Cr9xp/Bq36qcHrFNfxxLOo4NoQeITeD12nvkWwSaNgmpkti9tZqh3rrD0bz/KPjsePqDqaSBQuy8Z+bZn9RP82p+6QscQO8YmQ8zBEN9H1QouaZMs033/PTbriEeQYkdZyPQmaWKMjODPdHjGM3AvUXrex5lNXifEog6jVp9jpCvLXiQWKOl6vCXdhA6N09FEV5+hc23vD2eelgSUOeS/CVWnQn +9GxZpCRwMRDPejWR2Vvf+BGs2TNDawMKvXw7wkGFKycwYV+KrGJBLOWE5ZKcXjfZ +hqlhGn9T+umTkW8M7AGCyf53pAfC7nEQD4HNVdEJQZLIkmHZl8U57qY1DeVTGYl+nUTy7H49ZrUNzIpKIAr9zZIIyN/9N+wehqLU+wEqAEsnKcNuw/Damkm1gJmpUF/q +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1YwSw+qPU1x9H7h5sO7rqpwAq9aEd9/ZDyB7sxwH9WQaDw== +TP9zhKzoqP2ZyB/hk4eVp4PgFV6qyVcVcXyg9pUYTH+fe1NKLaf5spkcfWXgRsYJtnS4hcBQTfomcVa3NGK2kw== +1V2v8QerKOmubvSxgB4eTBhWeQzn26oNLaz4XtiKd3AdDQBUXiqXQwhmu5WlhFn60xMYWLG5JhZsJWdFpyqQ7A== +VmVrGQo2zRokW/ZuO9bN6w0DuXITxu54bKEKszNtCJvIZ0v/QRc2IeaSOENH3l9T90Rkz5x6Un2VmLIOzr8dwA== +VmVrGQo2zRokW/ZuO9bN65gGPMYTOZHI9ZQoxsnmP7KLEtSB0GLS5Se79m+TG2t9135KBsD2gO3FZdgx0O9OKmGhezJAz/k4uf64WYthAg9CvHenbGCdg+rCL1QdVGZO +VmVrGQo2zRokW/ZuO9bN63uze4Lnmw99IoHHLSLuFlE= +VmVrGQo2zRokW/ZuO9bN65gGPMYTOZHI9ZQoxsnmP7JlthBTUL7R5HqxLcjsSXIrwynkyFYqgigXe0guno1SD0MxRQkoinrozEUjd2i76KQ2FdVaMOaG6NgLEeroNbqY +xWoGNWjKGPfI4gq8aHoTfCMtL0f3r/mGFy+NqqYlupPtN4LM6Lh/S65ElrzIM2mEFR4wbzAXWHZrnhVLLLAO0g== +ut2tmPKYoa0Hz0cuVsUj3tMSGh1ko3z0+MlUSmSyMwhIb1zJinXvIFDxLfLhTecS/1MzMv2RHX+TvRordfgZoA== +x2LOCSeSzacmh81ySUhNRyMnvRWiL5oO6Qgq1srO26J5oIdOF2MB2jFyE3kJu+XCO3xi5v0zS0Bck5oPwGmqdg== +5nKwQL/1xmQIH02KLn2uCz4YF8qrIN87bFE85HkLc/i7k2M794x2Yq0LfVLigc6ht6OQMPr12G5ehRdntmVxAA== +cr04jPiHcZs6CKfXkpLJpg== +k2Zl28bOLN38qJfNGTaCZ6rMM0mTyJFOy3hLy7u57NY= +yfhcGVf2iYc+Ztxqwy9MUIKrmif2LWU+zznzn9UpLXI/s+FPgY2+Hxtx2sMMpbTlTne9WXFpqgJJ6gPk5Wav9w== +2wtz2EJ9gcVOn6ULRb1bGIe07hjE+74SGaG0w588uApK/2t5XjeyKC8OdX+1C8HjQ+IfmVBZwTDF0pqgdxhjkQ== +VmVrGQo2zRokW/ZuO9bN6/lqCmGJfh2uNEvtBuw26X8w+5enulKar/ZSvLijxaNb/dPvHp6PgHlhD6KLaQ5LS4HdQeIvfc9DPai/jd4+mwsBcN1vPq7nE76tV7UQ7HVwLJrb0xura69ySsodzPM1j9yiS4WA+QFYcOiZSLVhiLqfCr9XBVFfJx44SoYr2facnIdAeqw2baAk/6b5GJTW0bSxIFm9YutewCUa95yzA1QYUb+WlbdF5fohXe/pPENEyoNRGxO1JUaGh6nK9g7YnA== +KQe2cpNa8FkcC9SS/NMi8CpV7JOOSbLjDX5EVV0mMeZBU6EHWzQukb4HO6oqSoSn +GI0tVzKIRtvBR9LqilOl4U0EMIK6j0pKu8mjRYtcrdG7dZ269LI7frBhzviGc3tyjRVIj7Ajag15BCDZNPPiCQ== +cr04jPiHcZs6CKfXkpLJpg== +KQe2cpNa8FkcC9SS/NMi8G0P9GY6qNLNYbwjnq7t4XZr0KjhChiSWxjHizc34qaJqim4buwZW/fRlFpl/CdD8LcP/CgbTeIlzjwrd8jnfS4= +UrJP/jKni+bt3Mjt/LR2/JixjGffYHjGQSIIYHxxdbx6x+W3qD4ODikQky2/dTb0FJxL5jKdPUwgMlZ4WeWfOn9MU6Yj+i91j0dzx6dZUvM= +cr04jPiHcZs6CKfXkpLJpg== +JLLU9+nVY1JUMr5gL/q4iFLnR8rZZKgJ3OwXpvTNq3REjghdQYFTNphtNqdZRRnh+6KIUHLhYejxdcDDAMpidcvFYi6gVlk0eEAx627Syho= +h5FvPAwG81WNEoTw22EDzkR3HrRLEnwFXkWthPsBXv5FxJO4EU4fGo07ytQwj7zA +H4vRWcZMdeGTkaux2duG85KYo6NtqzfaByFi2mNPcgBBHwoC77PGMeI/lbIfKdFGFKByKlbEQzoPmxasTNih9g== +2wtz2EJ9gcVOn6ULRb1bGDs+vEyftqMNg0ecms5Hu3Ns+s3h9c+vz6ytuh+HVVQR +VmVrGQo2zRokW/ZuO9bN64dGT517FFoHJ0TEwdOhtfnc+CAijAojPGqiIIwkNeAf+ikp4vrFkNt/evEciFGybQ== +2wtz2EJ9gcVOn6ULRb1bGNqBvm70dZ7HtlEosE/Nvm4WGWQyuwgvUYRCgxKPD3IXayptF1loB8Wt5TBeELqccO+40wL0pvNylzYrUHw6JBE= +VmVrGQo2zRokW/ZuO9bN6x4A95AfXu9BB2SGVN+xyx0QdXN/K+kB5RwpHBtFUdMco7LWTmbDJvRzHVtYD+qEVQ== +VmVrGQo2zRokW/ZuO9bN67ezeGNjgojkyMvJrElaSuLp1VoIPcnvjrt1egmDrxsT +VmVrGQo2zRokW/ZuO9bN62PFnbgPIhp3A8iSXWSsu5IPoEqGgdDHCzJcdTWYiFwo +T09EhIon/1QoA8oqVWmf9YDvrNcJ40cqakka+PgK4WIoJfqJFeo+lh65ATFyUEMyF0Y/+zwyDgyEpxfdJC1T9s7UhkpoJHbxUBQZk4Fz00RqB4T9ckqgL8prAZIi3eST +1V2v8QerKOmubvSxgB4eTBy6rynLclEPfDHFwfFdGkrqZYjti0bOFlAKQAZdWvRRBTq4Tw3TkA7zqlFYV3GEGw== +VmVrGQo2zRokW/ZuO9bN631PXfqa0WsPxY3cPKsjN++sf3pPhl4PmY0D+iiZJP6K +L0eUthVnpkGsmKFAX6d+uKiZA2NxmniTgCRjkzRry5tHeci7yRgrIj81Qcc56UWP +4hdB7RJambPsQ0dtPl5R2R2cbNN2oigWOYu+sXAe6NE= +Z8EhB+ghYGoKD9XY4aLecICNYZH3MI+WkCe55V3zbmE92inO+KpNM/U7zaoDuAPEuoRzraAIHccyyiUiZq0KCn4SIx0pgaa5qCV5DyiUzg8= +cr04jPiHcZs6CKfXkpLJpg== +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1YyVGBH6aNnzPjcK4RvfX4hfYve9n5OSl18xA0qaFTGh/FvjgivZL+gAYkkYN7wuGkE= +wgR07xfoapmx6eEnFHXXYpiq7dd9WD1xT5fQ5Vehci+ppnSgw3Uh/4UFjdGtp4C7cvhGp5bS426UifixsSPqnA== ++rSqPmR0H16Evq8k1dmRlB4xIbvaYG+O11hO82myw91Ydmx3LodFeccWDl136pyS+nAQAVxikLOkQvSOY9kJQAL4dTpsdxfBRhjl8YNkSe0= +YY+NVOVvcj0xKLmCGeWNx2BestjgssDXb997jOzwt9Aq32N7lFRvKaEk5Fl/ha6S +tqyC1MJ6XiYh+GD0a1kac5dSvZffsX+eThfLrN6YbFmpi1kaiU7RgZCp0rtM29PiZ3vWLi+ljJuWig8NiQ+4bg== +2wtz2EJ9gcVOn6ULRb1bGKZE1Zcf2OrvDEjNWK0WFBUgbaWhDaV89uXGofJoOVRr +VmVrGQo2zRokW/ZuO9bN601Odt2qBpBvUnMq3yF58HwhhNM7lVHcWsw71hZ1B8rwe2VgzE/Ygfs/ES/u0phTJg== +2wtz2EJ9gcVOn6ULRb1bGE3fWxG5qHjknC55f8ermFzus/o/QnSnnp1lNx6tgemIWgwHLhJMu5mRfNxKVC4tGUagweF1aVyTeSeOgr2pV0E= +VmVrGQo2zRokW/ZuO9bN6/3050nTbENCuAHMJP8rPTyx4Hr09t3MP1sylZ2k9TWT1Z/y9kt5XpMdcbz6I/4KRQ== +1cV64uRoDtvn/9K8xFmBBX9ZdUkOEyIMST38LHOp7fSKs2ImPRP0uyopElCHPi7HMVolZMF8O0XYdhXxBgTZgEqtCxmaQbs6wC+46PgWeNDCrmBUCzHdmv+1vOWqpreU +L0eUthVnpkGsmKFAX6d+uKJozLiMP5XT821uPNJ8RPjEdXEmGnrZWY/p8GPQZsgGWBxCipm0x1x/vOwPH25IOA== +9LvBrxm/uKypfgjBGJGYtcBuhWDF5ev4FlLJBPqtxn4= +rqA8uevZ84obkmE4+PiALpn7E9+AzYGlYfGJdMimSUSjnNUvkpCHlBESha0byLU3ObnMbt+xpHSu7mkeF29b/TGgdBW9AIpou7zfLCAKfCg= +cr04jPiHcZs6CKfXkpLJpg== +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1YwJt+VY8UEHNTi++WTqHhGFcQLxNOcaxrVugGzPuGlKxDpuTXtsBiJx6HKGQdLz9CA= +xWoGNWjKGPfI4gq8aHoTfPboh14laZFSUPEgtjSKUWMqK7Soh3XPs5AU7ubhT3vOqORY+HJbq5ao42T+oXB2TxMpFXIxfI/VHCXdH5U/gk8= +poFDBYdcnIoac22Rpx3wOFqmecX/dbM0cwOE9JcdOUxhOvVL/xyrkb8pvE3XuSq/AF6vTbgqAOzaclvnzUP78zNBCUHIe9viMRGZD64lExwdRQ37I52ZP24wl2h+HEYnkzG37tN6AaS9pxgDJLGXWA== +9Vq3hrd3Q3VGJ7tCv0+KbRjjCn/SrIsjP/N9ZQ31H8s= +tqyC1MJ6XiYh+GD0a1kac+C23p8m5Q8Czgkq36h7kR9597Aftw1MKkys8jKCT8dDNm0OhJLEvszQGKdpR/tUZg== +1V2v8QerKOmubvSxgB4eTBhWeQzn26oNLaz4XtiKd3DBM6qqjMAtodeg9N3wCm9Gq2yeZjXnN4HfG2ObtojCyuFuu4uJxjeY08uVocnJAWzulcdvmrB+9jE1iVskGH1AwKoc/O1U4u7wqM1+ViPfz4Nc7TcCBSHiHSfnNKMbMSgFTRrNcK1VUSeA+xqt+7Gps9biwxLJJkfYwpud7rzzLTIQYO3dgrSEgNopaKP1wHQ= +89eRayX3G8tCX3FDU5IcjcI7XW0AOTd7+zV/GdfWl4M= +VmVrGQo2zRokW/ZuO9bN69cX0oe78riTFf2DR0iSouZv8VJVkm4PXJpjAZ8dMu85ue9EvVlif7D0Hfkl6Hg6wA== +VmVrGQo2zRokW/ZuO9bN6x2UBXrziXsE1YruI8yEIdpEkt5u9hKDhDnaxZI1tKV/1MiYuJ9V5LAPFKhhY6L1DPpcIKwtbw6ZwveQkIvw4MONL6VbKw7Qe3GYzo57G9ez +VmVrGQo2zRokW/ZuO9bN63aR3YocYJrJURmHuOn6NQyL2YNwJCHOOO4dVXSO1OrOu57rWNUOaY/g9Z4DxYYIAw+FuROKSt7S7gbygqMsdJM= +VmVrGQo2zRokW/ZuO9bN63aR3YocYJrJURmHuOn6NQxt5CXBImKEYcRbajp+HfFQgcG4V6mNlUB9TfRtQuR8trry1sH16bjkYZEVMTwcB6w= +VmVrGQo2zRokW/ZuO9bN66TcS6o6l70yqYAEaLGRTwyJxvl18TfQzpu1GNA+7HjH +VmVrGQo2zRokW/ZuO9bN63uze4Lnmw99IoHHLSLuFlE= +VmVrGQo2zRokW/ZuO9bN6zhHFvchcMiaUYiV8FEKyco= +x2LOCSeSzacmh81ySUhNR9w5+XreI5pXDQTDZ2h9wlRlZm0n1nilVwIkZR412HMxDvSJoPfkBBhn4MVgLI7ors/3tML7Yd+6oLX9YHq7J8c= +TP9zhKzoqP2ZyB/hk4eVp5dwJ3AjKE3bduAuqPgvqjLT78U+Dqd5CSCjq+oD1f0ZsvntMFuytKQk/UqvAUPWuxQYcRKHE4yIrWtNgj+/1e4= +TP9zhKzoqP2ZyB/hk4eVp5dwJ3AjKE3bduAuqPgvqjJAEyYSNUdFNS6Lh4D94nUUUjoRv+eD3l2E/Q+p9vL5xMs2E2yAFDG+aCuGb5VjBn4= +UTxJ4jWO6dHQO8pTjqkurgCMD/1jMHNO7c5IGagQ/I2QGV7PCdztnRenkMZGlh9+0iLnOQuFMDxZCa1M85F7cg== +XX9hoBCxjod6pWwbTwYew9PA9JlQNG5VtJfkL0XPTA4= +L0eUthVnpkGsmKFAX6d+uOSGjw9k4+ECXaFBolnRCxXrkljYR7gvNUjTs+cnRe7RhvtQwTDG80WvhjGs0l3aVw== +GrDu9Oao7I2P1akJgF09eDtLRscASUn24HyLWqaEGOoLFd3Pyl1FDaBsa3nmHXJlV6TKQFpWc3mjwlMasU2QuQ== +xWoGNWjKGPfI4gq8aHoTfPboh14laZFSUPEgtjSKUWMqK7Soh3XPs5AU7ubhT3vOqORY+HJbq5ao42T+oXB2T+dPZSHoCsu8Rvh//+BFVlqkbhy5clkrm5xd5xWnatOtX/i00VnO9Du8rLpc5vQP+Q== +KQe2cpNa8FkcC9SS/NMi8BFvFUJwV+XI/wdURlfBJWAKHDHHyrOU8iCWTLbvkh9K +rqA8uevZ84obkmE4+PiALkqLDY5CT7nUo6WhuYTmudfvPnqJdzDNw7liPPnKRWgCH5aS6+1HbVYGvtufo0BSXKqCmMResFIg8Yd1Urarz6E= +cr04jPiHcZs6CKfXkpLJpg== +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1YxTcmv/VfxPFlecQ2Xnotqt2IE52L3gC7dCe4SN49+Z4J0rlz5DNFH0zxI1SMhseJ0= +xWoGNWjKGPfI4gq8aHoTfPboh14laZFSUPEgtjSKUWNJQB7DVYymMOWmjTgS4eEyfy+l4kioIRjPlfjna0D49HQWKrqNczuGOV2UnBCSpJk= +npCRXd+WLSmlk7JiNtxtm9gbYh/Jik1FIDXG9IEelYlUIDsISM+xNL1kwJmj6EVRBZZmSfp4awYJxDtGhLbmSnyPSsssdTNdjVh8miFF4Jn/qor6xUfnuQIA6ZtKmpvNSOx1PBvgevr8PiuUrmOT+w== +6r4ECPT4o7OwHqIcEmFYzlEbZOlulJHLZOuh0/zLs3M= +XmWcjbejqXjijvSDMMApGon2xDjBspqtUFw+zce5TQt7ub7089lB2nVxtLvJitZEjjV+Ih920BeA+wbO6kaybA== +8qTxQznEcsQMgFW26Dqik8Cp05AEJsEUSbVdG+IW7IfjrtNpbPkvFbMSrg7fTklWMTS1MlUC5TsFldxvdJejTw== +89eRayX3G8tCX3FDU5IcjYVpWBUTJe+6dnMemLr9pO4= +VmVrGQo2zRokW/ZuO9bN6w6POkpptsbLbDcwAs1qGzyVhhDiOEGExsweZmFRMA6uit7+z8HqafOBm+f1YAnPFg== +VmVrGQo2zRokW/ZuO9bN63pkTmxnYm4QxVHRndiusm21eM9HuqziUJfSxSPrf2PAsXwHDxb8ftSlHSZHnsh24GzGc87Dpn2aHIxSb/ojqvUGBW4iJF9d9xK+17b+BpD5Rw+OA6q5DJ7d0o4S/u9IHjFW8HcRAUi/mlM43BsUCJh/jk3jWn71snZbWPixdQAZzjNUireEtL4ye5p5yYA8qA== +VmVrGQo2zRokW/ZuO9bN6xatn4pQf6Qylq3DHGXCyT1/gtrxi1hEFizOSLgOoO67H2dMVxci0va3+/BS4a24dQs62DYcvihjvbPqFll4EGU= +VmVrGQo2zRokW/ZuO9bN65da9+cSQosYOCAUllk+pAhHrZNpey+rOYfxZixSyiq9e/vKx2xioCH8T35uXU2iNeWRXpdbAL8h4Lkvzght0U/Ts2rWOX49CLOXyoRKCpwcy8TuX5rPw+VOr42ds1YAPLFlCCg21RSbk7iu+fTC50sk182IVPHmk1sBhgAs9M+bs1klsfK7JJlGrew/cX8A5sVVCSyoYIFcZI1fqs8NZdj8zjoyXLLEDzm6KRkc5qrvSI29znlAPdb0TUzeex0G1Q== +VmVrGQo2zRokW/ZuO9bN65mTvxewxktldzaTIh0ruseAf79XDFvSH+mS/WNQRDu58Jt2SHiNf3FiIIVL9XD6qksS8Pi50Znk2FgPbjr91GE= +VmVrGQo2zRokW/ZuO9bN67BmtbRZu7DbWxJyGfNqdp2DE5GklxTMrqrBNoUflNgJ +sobCGpmMf4/g7+HpPqBjC6JAoG0FhLZpTt8WF4LoCdc= +VmVrGQo2zRokW/ZuO9bN6ycgigIJ0vi/BKT5iDenSw2GswKcw3nZbhDFVP46d9mwIKxz8tsOD+PKOJ6VxbkS+Doa2QcmIKejv2R92AsreFRo1/svGGmZidNFI7nhWcXh +VmVrGQo2zRokW/ZuO9bN6+wwJYTJogYbw0PvRmy4lbbxRYV2gypJbeoLmseomGGpvhP1RsTU2N4c3EIRExz7t0+QX7ho+8KK9sCVUVaQujq9zlh3iv0vxDOxrHxVoWL0vPQy6OANUn3YB+xIhGV3YCx+4Xg4wSM30SDGMDNrxRAq7ROxx1v4Nz9VcDQJnf3l +VmVrGQo2zRokW/ZuO9bN68W7kg6GMlDfsyj9oqABfICnFRJY4e3Ft7u0FDEeTLb+Tttpv2vgL2k8tL9C6IQypvRiZXCeHsld0iOwrv5oWUk= +XmWcjbejqXjijvSDMMApGqg66JQONXEM/znZP+hhHbvJqwnVLtBpSTFW1C8zjERP +f9qliXFiuzgVqdx2xlu28laTsVt0v0AcLDLGFGh4ei4= +L0eUthVnpkGsmKFAX6d+uOSGjw9k4+ECXaFBolnRCxVXWqXRfRFrlYkhQXdVympE +GrDu9Oao7I2P1akJgF09eIPmKz1Z36SfVzbX5EtIVBSJLXuLhRL9bU8hNSIy2ELM7gHJn+xHvxDr0YymbaxYQw== +xWoGNWjKGPfI4gq8aHoTfPboh14laZFSUPEgtjSKUWNJQB7DVYymMOWmjTgS4eEyfy+l4kioIRjPlfjna0D49BiYt9QMwyQ4CP42XHTRd2Eae4u3klBtdxzhjxB2tp/fpTXxrSMhqObBE8An2UfNyw== +KQe2cpNa8FkcC9SS/NMi8I+ywyBJxxH3fLZbImuOz741MPpebUEOYQ++4urBwfNp +v88P/mqO0QCsEtt5FQxQSW16A/YQqwPQpq1pkQgukNC1qdmOmTbKBeiUamM5fkCyU04u6JgeNbqszxMDnR3gWGQltPPE/JiBntG4yiEJOhFE81/yTcpkQzySU1/dU0WkEn1B6J44c2f3ZUeAWZFpMwGnZPjHe4mEty4JAddSTnY= +cr04jPiHcZs6CKfXkpLJpg== +4ILYwbt7Vw8dM6uHbkq0vJKFyJOT6EmZCVi5TPscIMcKGdQWeHEhTG56vR6HfHecSYcaTVmYfDlfVruxO4vUILrx4YEZmPUd2JfQRcEJJDSvMDclu/6DXDm3D0QaCRqiqvrLHQOib2Vy31zzzYp3RQ4H8P1U8KU0rqQvuCGHL2xnlpGCfIwSNh8v/VjP8cnigUeJ+QAptPmy2DVB/2BwNPs8k2ztWHWvl9bc+7p1e7Q= +4IH4b6ywJwYs95/tFiL8lfnVM8cGC/TcQN6wOHKJkru7qKTJqyg+NRdMZZjqWBn3szh6PMq7v8cN3fvQg3xbZuKkD7c/LeXguV4QIOtOzQmInXNic0zqAbqxWrq8yun8i/kQI6c1s/Em6ebGCLvt3rsKACzVCUV/nEuuE364wWwIqGjKqWfzxiUAA73uNvIn +4ILYwbt7Vw8dM6uHbkq0vJKFyJOT6EmZCVi5TPscIMcKGdQWeHEhTG56vR6HfHec6/zuyKD4TNiS7umIUQ1eh4mx53BbRpEGDorZQsRxUIdr/Yovh7zG9leoCSODHOex0/be1Sr+cfB5VTJKpjqus0DNc2HZNsgg0ccmxlW6HR6l5lCNTGQD1izZ3oo6Vc3Zvd0NHBGxCrhCEGjroOZL/qmPNTCSc79RhJ/im8x06v42AlgXJtJmMDVKBFY2Ki9kxzkecSlRQld/40PT4PjR5E3/NtH1pOL/to5r7Lfk+/hRui6SIs2FvUkBA6/kIiO4UP5/F0C8bD8DS+HXSEi1JuUaz9t3t6e44P8Q70xu+QOk840HWTXQU9mxACc5PsyDVQ4GOY0Ph0hF72l/O9pPph7Da737jtPgBihJKejdB9cyNG1scb2M0BPnYqR55Hpp +4IH4b6ywJwYs95/tFiL8lfnVM8cGC/TcQN6wOHKJkru7qKTJqyg+NRdMZZjqWBn3szh6PMq7v8cN3fvQg3xbZuKkD7c/LeXguV4QIOtOzQmInXNic0zqAbqxWrq8yun8i/kQI6c1s/Em6ebGCLvt3rsKACzVCUV/nEuuE364wWwIqGjKqWfzxiUAA73uNvIn +GCWEveFNOjCMyiiJnvM3cj/vlLOzJEJeXu4Wc4MzTT9m/lr3kKqDifw8VnjgCbmzEVvoSNnOlUy5clRfnyQg1A== +guSZID0bFQuDFoWO2uxAJsjFYYfXNGsMOQ7vVBnR2Zc= +Z8EhB+ghYGoKD9XY4aLecBk1+N/K2p7q6loOj5hO/AY/OY2IChkLDqVzJEzXKrKk5Dhqc8kgf7n8kn3bYhf6mSyxjI41IXmkSnhG277VRrK9M6jwUqP0hzg+3dxRvJy/5bLZrUrlKLLAqdOB1DfhD8hInusaFJKOPu0wf70s1wc= +cr04jPiHcZs6CKfXkpLJpg== +tf1Oq1wYbINEkyucXtee1cMgJMhHf1FsUsP2LRuuwug1o39iAStOUDwI0RHQuoo1DY6kPK9KpPHWiGmnoKUpnaFv5CqtlCnc9iB52wcO11EZ+gBlPecinoTKWp2vNyQXL+jk+xFCOP33oQlYIz0BugeM/Xs8WmaxUp+YAPU/27ZVp5rbBob67xkA4H0ahEQtvyWXuaWjvb6vC+TD1+mEEZfT4zwsKh1vwbvDluhn5ng= +wyvncB/3/67oBT3vhbMVkleD3cwd9I6ljdxbr1LJWaBalpGbxaXxP/vvYmQVlTq6AXqr+pB5OJA9v+pwNTkRIUjhIPP3l7NehRhk/YXgDF5I6D1WciLFei3jd03xok7qhRg/Cr5NRwF3IWBai3fmz9CCYlyMGnypU8zhPd57O3J+PYMqQcD4Zcup/TfAGEaN0P5evWJFcQLIOV9zlKJ7QQ== +tf1Oq1wYbINEkyucXtee1cMgJMhHf1FsUsP2LRuuwug1o39iAStOUDwI0RHQuoo11dIkX7kqHzlkFxHSyD6tnXs+xhWWWFAgjgE6iZVyEvVxS3hDQ1kZ7mZO69wsx7qXMiK++axVuM7kDei6Uc8fdG2Ij3V95ndg3NGhF5c8Ve+M43nLzx5EEadj3AIL988up2jz6IXskD13juE3HHLxstJO7++UKrO3Fph0H0YgADU= +wyvncB/3/67oBT3vhbMVkleD3cwd9I6ljdxbr1LJWaBalpGbxaXxP/vvYmQVlTq6AXqr+pB5OJA9v+pwNTkRIUjhIPP3l7NehRhk/YXgDF5I6D1WciLFei3jd03xok7qhRg/Cr5NRwF3IWBai3fmz9CCYlyMGnypU8zhPd57O3J+PYMqQcD4Zcup/TfAGEaN0P5evWJFcQLIOV9zlKJ7QQ== +BsAbgDLrhQj2uYn3yuDnJfTmJeo1JkS+iXr7NiMr2ZGVgoDFcakJ59CcFxnHHC81E4rjC26F25zO4kie0jx4Nw== +guSZID0bFQuDFoWO2uxAJsjFYYfXNGsMOQ7vVBnR2Zc= +QWxBDaw3VAaL036sX4hCHMARERUPqvMRF5uWXT0klt4M8uDrT3wrTUNGrzlsT3SHynX/h4n6qICxWvRyD6/zjg== +cr04jPiHcZs6CKfXkpLJpg== +CjeKMqexO85OxFkNTLsfVe7SITCOKaZp1LGMwqVNe4A= +R65WfMhum3uW9cFR5COFLNNgF+xCIveKPUqFxBGX9W8= +wbI88h5UWpmLfMPZm1GHGsVMKwcuDJu56WFkvk/fR2TtwNrgHZQRoxBog1rgNcsJMOWUeXIf2/8hfo8KTaG/XQ== +JGZY1C2+nS9wZYWgd+gvh082R8I97omw8iSHX8Km7LmoB4CSGmPMOwkUtKpjByEk5vVvPtxTZbu3mWhXtEeW6uJ1VBaEFl9c7YBx5ZTGNMA= +SY9CIZHY29IN9pslG93gZg== +pVMCrZ7PAAHymZ70WROm/xN7uc37HHCphJNP6wBWNFFkfCrTO3/zWdRXL0JcM+29WG0o7tsUYtBK+YT4qlbtoHBEcSL+oYQx8umm33tHtvxlOnew1yGrW+9tUb/3mIzCXV+bwMsE5oXss2lYpTO3GNQd55Kh2tKBwIfi983jfE5WfGpdHOOq3NWmcvgx1IQ9 +L0eUthVnpkGsmKFAX6d+uM37yNPNIFu5Sy9mvfTDYR40t895iJsTBk2DH1n3Un8e +TTmoxB5HuP9wmwf8x1QYvrn+3uJVMIWBGgMruE5M1BU= +QWxBDaw3VAaL036sX4hCHMx8zffAVdfqNc5+di1I5xBdsKxDZKivqDomKrgNbiQV42S8BdMCjk5yRAW6CwZB8g== +cr04jPiHcZs6CKfXkpLJpg== +fnnuZD0QAdxvV2PveMWL8FOZqBJQkO5fkDyErcdv3sHBCTMsJBafZRCNzddg0fI312AFmwseKma+xsssgq5CNQAjmSvAJCGuc0eXbxvMQOdAMf1OlFFfed4k7hRAM3At/KtVPJ5SelXm8whoB/FEOXQ3qFgH/HpWgizR1mjK/e6ou5uOv4f39VcQKtXRn2Jd +3eIpXgbBonaoheCPx8AFY6umFI8slLWq/upaa1Uk2jpSVyMvPJ4mcBuZLgraO+q9GQedAucc4xrSwIqyV/B6gCTFpRYHtmfOoqOLaWkwYvc= +4/4+Wm3Rsh2mkp+PCxs2s6VV4mJNPEuvR7w91QHZ557aHaU/Cb+THTNE07rfYLXxrdZDRZPikp+SWMJYE4H3Ig== +dID/HuTFI39RGoF22OwzKyXop25iN/6Cgmas1ryG/r0= +VmVrGQo2zRokW/ZuO9bN6yIQVC/ANBR72Zf+NN0mt0yltbzDkWCCV7TF+F4d/yUWRi3IdQCvPfYZK+rRnnypTQ== +VmVrGQo2zRokW/ZuO9bN60bK2UgxkvkWzml09hTN25JfpBLx03bYbZyT1mbcJLoM9IlxgO6bebtiUCLu238r87hHo/BkL3I8dZPhAAdHYzQ= +VmVrGQo2zRokW/ZuO9bN69XWNCdW2VwaznplNhiELZFhveDssDscfLsEq3UYW8Iw4usKj8FTxO0dSaukGj8cnv8+vtmaKEgZ0bN/GMY3GQDbfxr0gYg6NdbH2kpceCeziSX72rUP2GIAP/ebCA3AjE8/vyUHOlZUHuT08r4QEqVOFV6VX0kqtotk/D+bdiQoW4FGuVaT65K0qS+w4pQd/A== +VmVrGQo2zRokW/ZuO9bN61k54EIrXJ5YDEJYX7KVSMQJb2yB2/tKePc0AJNb7scfmBx3ZMDkULLl85vjDrKPDsRbYCCYbmQ4JM/pYb5gQtg= +VmVrGQo2zRokW/ZuO9bN6+4h+8nsMn2hxb5D0Q6u6aGCqIPaFsLj6fC1TahQFgsNH4kuSQ+lduIVprP5UXJaS/58J7CKl19/ZhwZDgV8O6QU8iHj73adX3xjmEJZkf6Qrfvc431ZzHai1xsHjSFJgmjI+xwBn+vfmSyEbXYaPyIavlSyj91iOeD8FqpN4irSOGSrBibcPvxEkbNEBWdfzw== +VmVrGQo2zRokW/ZuO9bN68wHZtGYQdFVE6PwKTgvaNtYzBFUcYX3fyY/6M9/dXzj +VmVrGQo2zRokW/ZuO9bN60GIVQb1dVeSTDjJFx4iZ8K1YL/MjcMgl0IDPsbhoWcM7yQ9EOiw907XJNWeGVgujqehYoLtC9EudSNuKe99+CWrrATAnvnn66j4nlkv3EcPJUeAgTtZo77f5zqZOUsiGesKkQIGJJ6zd/3K7c2P48s= +VmVrGQo2zRokW/ZuO9bN6/o4zScwQoTUi+tr1czRySSTBNwD3hHM9vC0XVvu0iF/uL9dBtNk/w6BB5ctKdmT5CcwaDY3ncMWkTQB13YuBkvaC3/J+X6jTdOr7PDuX2PSynf0+gGLE/QAUz3VFC0aHU1OhnUAoA9NI005K47zs9vF8K4GOfiRNWxWRaEo61bK +VmVrGQo2zRokW/ZuO9bN6y0EoUp3iWRH9OmX39lPFAedKJOagVu1hc7h3Pe4GIyhE4KZU3Jx0OYEExfBCrAKqagP9yem43/r6C4T8+tDACodjJey9cFkvf//RgViLJEY +VmVrGQo2zRokW/ZuO9bN696Qz5c9i0uv9aeGTx01h1hC3aUrUxbV7GyHYuGQp68o9ge64GJUGHt5FQuweqvNs7qVTiNgWwYE10vLvinIRsMYIM/myPIdDv/AqzuwHVzOOae1yPP1GqFmx4OoiOna/EGkKRt3Z8kNd3k4/rs4wwzo61sa5wcv2NX4XAwqnj1CEvFdWPd/bPz5yy3bIgVjgSZPiSvSqJXgxQGFENnWTi4= +VmVrGQo2zRokW/ZuO9bN67tgFk4kAxPV2NXuFIabsssnHcnxv5PUS7w7nqdNDlZmad2xlF197WUG6enWS1ddeO2lu2wARsOMM3olmtoDpSE= +VmVrGQo2zRokW/ZuO9bN60bK2UgxkvkWzml09hTN25JfpBLx03bYbZyT1mbcJLoM9IlxgO6bebtiUCLu238r87hHo/BkL3I8dZPhAAdHYzQ= +VmVrGQo2zRokW/ZuO9bN68mt+2TzTj2DszDWHbL/Vx+mrPtVnbW+SD1pBk7cc7rsaJwQNo4QU7LoHvOicKqoaUNdoG5e+AzVGaXRW1n1Nqg= +VmVrGQo2zRokW/ZuO9bN67wpbXh8GxRtxxUdyv9+tcnQYOPzlH126m/rftVMv5cRl/hQkptryk/t8Gtg8ajJsb8lwpAI2nvgWtGpl8TpQ+K9nHJFRXKol1tZrc9/ipu8kaOocsRDzkPn1olW6CDnbALkALXUpIrfjvDKrygdIdVqX4xoNpvqmFtSkD/wGtSGVW8x0++ZkFMLlzwBleSXIofOOFMqbdm8kP/g/2Q9cnM= +VmVrGQo2zRokW/ZuO9bN61k54EIrXJ5YDEJYX7KVSMQJb2yB2/tKePc0AJNb7scfmBx3ZMDkULLl85vjDrKPDsRbYCCYbmQ4JM/pYb5gQtg= +VmVrGQo2zRokW/ZuO9bN6+4h+8nsMn2hxb5D0Q6u6aGCqIPaFsLj6fC1TahQFgsNc8McXaMNkcQsuyQNX9sTxBDIVJELewreELhbA7ExKAhYHdGr0vUfJ656qPdz61pEMCuI1svU236YOtcqeeWw9hqHZiYtOwsjiw0R7ymDlNoj5gwm+V1FBszZXgoQ/hN6BxqCiqLFSweS/VzkreCfWAHbQNqrDsEDcEKlGn64ivJUmuj9zyfGuDZOIHE/SGBUK2SlyF9yV95YTadwSATEnw== +VmVrGQo2zRokW/ZuO9bN6/AO4eShpSZK4uRSltMUWur6UmoAJ3uYmBh6Hb4u8+QZuLnJtd07ycHQVITOJweo3B01crh9ZOyXk+RD4zH9h3dxaFHiZPGzCPB05djMX/pZ +VmVrGQo2zRokW/ZuO9bN6zhHFvchcMiaUYiV8FEKyco= +VmVrGQo2zRokW/ZuO9bN6yIQVC/ANBR72Zf+NN0mt0w5vOuC994hsp0d+uCS6/55EZLchuUQTeEJwheE06nZPQ== +VmVrGQo2zRokW/ZuO9bN6z1vYAYiDcZJ2goOT3IWOLIPUh30Y1JucFlnMHb1teNZ16yXgRYuOnoJWnC9DJL+4Zj/1ue9Mx4yCXqgEarMjmo= +VmVrGQo2zRokW/ZuO9bN69XWNCdW2VwaznplNhiELZEzh9jk6I0q3rmZW4u4dtEB5X/k82/6F0+ZL0tohkmBOkIUMP4Pr0PSa2rrEIkJpyeCRFXjMs6Jo7fo5NVOVuWlM5YByI4VbPYwOI9kC8H+zNIdR0QyalN2tczBM2yXnr+zlXuJh0AhmzcDReqsIiJHe8wIa9fruyQvytkiCOU0rw== +VmVrGQo2zRokW/ZuO9bN60CwpxhzHd5Tsu3VuWdEIGbC5Eco8WcHvSLIMGZrC4J8HM7Mhe6VWR6L/Sg/mo+5Ug== +VmVrGQo2zRokW/ZuO9bN60+OYD9Lp6v7rDnBJufgyu0dtNrYkL8H7zhFQ+vQrlqhvCE6NtoBH6ioyORLFJjNRF9SGfkFntthQsprIkcvCtCK/JhjIZowEZYnAX2rXG2bsXd3yGimQl5GilD4VC9gNoyJDrUbIX84KmcwTFd8/LRErn07610YcZNUf1UtMXy9vd3qDn0Q/AVxLd1KCmzh4jDf+6iTl3HBqq6d9N7CJeNtaOYmfKxXO3OYOtgspRP3 +VmVrGQo2zRokW/ZuO9bN66jTdv1hTu+4fFiv1YBwZue7lv6Aufd7luwy7B+0aC0dbzx6VRSrf3fzINUzlq8XMNkGKWBWvxEGkmN/9UAOa6dKPNyUHrHmxW5rH1tExZwQ +M0ZySqkmhuHCw6olbCKv99p0Om1sGOCLOPcvWBGiKm4= +VmVrGQo2zRokW/ZuO9bN6/IJQworJPCLdiBBDQz9p5RwSTJBcPPTw2MHkE7h2GTjhvnVFr+q/CK5r4h+EYXiyQ== +iwvjn39WsWBJqN2zZdgrYJ/brbUvLQT8xFC2K/CMfMpSLWjSyTEJEwqWm0w226ndjFtOnyhoGwu9FfNq6tbV24x74jC3NVVQJ/zXWM50PNKF5ZjzeY6CK05/XMakHiEDBjCNUSVpX7NFuVqFBQoXFQ== +dKwZPlLLroeAXoKoRvFRMPkQmp49Bc6BILmWMD0egRbJufQJJVHeUyhmpinBOzpu7lZkSTQPR3sLPWAb1XrOdBks43Ja3veKdkPBsYEtP+4= +cr04jPiHcZs6CKfXkpLJpg== +fnnuZD0QAdxvV2PveMWL8GG3YPzBkIz9Ea9idqA0hv7KzY3K3PuUh9EVgCFxsnuQ948EJfpPabGHe6RQ1WLXqx3Sw1oJIpfRNJqpmc8NXbiDy0sb/I/Fx6BFCgQNeSfWkMOO46FZGiOPxWAfogF5t95B5JeJd/sxMl1fwroGSOQGzaf7YZCSCW9ezDLi1u3T +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1YxTYMkXt8S0Nah7w8r9TF300puk+OnsqWzfPRMs3Mbi7zOrp3cqhM0qeqORamoAJBM= +xWoGNWjKGPfI4gq8aHoTfPboh14laZFSUPEgtjSKUWNGuMooTRErT71B83/M8nD4aRWyyME7sQDlOs9XPKbRnkn/lNp5zeuNE5AF5qUcgYw= +XmWcjbejqXjijvSDMMApGpsWw+e7nEJCTIvfvDqrkKXboOR4vzQVwubyAa+M5U2ai7wdT1soKMjQ5oH6v9r1qQ== +7iJfWS/hi94/vke2djffqmjBjwDgzRyWSoEw1Q3z3GT4V7Je3CZ1mm5s206h0RWJN8GlLviN43HL/e8mdYfKB8xyWTX906NaD7r9udlkATM= +WTPaI3KC72IatMBY2cUf8DhINsfbwu2z6BUbAhOsimUwCOTh2mByfFmolL6LGlAzznx9YHBnysnjX5w3K0Aa4lU42GWBOTkXRGOsJGciABerQH0fIN3Cc0Ha2WQAmXrtVkuiFi7WSacqVGM92Wz0RA== +yfhcGVf2iYc+Ztxqwy9MUGl2MlVdixWiPkpdkDIaDTR2fL2bGJ0eG/T3S3mFK3I1yjUKdBI/7/TjB+C0Q1TF7A== +2wtz2EJ9gcVOn6ULRb1bGHs/k+FAu74ujznevW9tehQxxh+/BQiuLboLlLsvhekSf7bU2kLTTL5K33sDNiX0trhbG01wTa4j5HspvJM+CjY= +VmVrGQo2zRokW/ZuO9bN62zAi72PGE+cjoOBruJcs80PAY9N3iH7ke6TUqsi1sSy8D9ganeo6V2zi4SBqW2bdOnBzTgDx5DTrEDZzIkiR6xafY1QNC1k21Wx2o1UPd5i +WTPaI3KC72IatMBY2cUf8GcB7Gs9lcHGWbUjG0C9vSs70Ygjj7wUoEbxnNP2i9zrzgxlxVIkk0U+7ydFZHSoEzXRRuiyI8zt4O/5cZ2lMz2J5pBt8vqrB5dxECjlX6ZE0SioSpP6KutnXcCf1ehJKTCWzRi2xhcBQ0FshyUTLQg= +iwvjn39WsWBJqN2zZdgrYIdzLsQ825kgPBf5VIBdj3uoMjS29MAx3RxOuWvlnXiosQ7KPIpTLW0MUcNlsu/ulyBkoOXWnXGiVxFPDjplOPmOZH25r/PenV+QVxeQxMCQfVpOT+W5VTimeAhxNV8PQw== +iwvjn39WsWBJqN2zZdgrYJapEN/AKnY2zLbcAIzyrBBU1RXGfgWqmb+6/1Y8honwoq8vQFNWL5QXG2xnJPe0LBPl/ycfTdBL8st+Ep1mgsYzpmdsWR6INN9Bz4yFZESMs7wbMHG0SzC3fg0QgFp086x0icWfwhosFLdQWu1ZDy/D1nu3zd6GRjYEiiID4hWyqMGLVMlVhlgucIyj8rJvGGu4F9qYL8XJEbUPtev2y4Q= +j950dDFgNocMT/8FQS6TYJHV9Kyoac68bruP5hOu5qmaOOkbdmWW75ouQPYNKTGSSa2ykeEdUHfzKTkJdx/Ccg== +96orka/uERLyRst14azQwifX4NgxGrWxr0Kq7A5Rg2HnATx3FBLn85SCdMGIOm1E0hlma+GxHkgZc93eWmwVjA== +bqFhodE9GHP09Cm0jqHUfmkdtpV9kuGe/MGw5ZaaoCp81F/rxUszv1KfXRZWtOlE3la7p6gT6SCyEt8mcZzS2A== +q4nc/jwATOMUyfSjLibfEbtbU6LFt5q4ewa9MuGiJMo= +jsE17/UlKDH9XrhQJJowi15/y5gn5lH8c7Gqcnf3Esr7fxvcWtKirtM5TT2hHcoLOOiNTfJ5a7izuUZ8eDTQRw== +jsE17/UlKDH9XrhQJJowi9yxaM1QuCYxVef9FbIoMZ+eKsxoKPzwQ/wwh+qx24Hi +AaCYXSBoNzzxOXBdbJWxCPghQQ+zxvQLtEy2BM3tYqHuDq6yKijsnOS5h2YQlWHI00dhyYcLovqVLkdh0Dy8fMQOez3M0K6+uE8weXP9Ap5DYQqbnNlIhatzkP9KpuAj +jceVTICIbdeQFZqTJKGutSjbVQ97l0vRfz6aCyyExd8BAD7ORBXAStcGQntGkH9VeVHgeQxgUAn9i5xQBjtmtg== +w07xSMqKvzErqU8Z4gNDycTDkv3aRFmhNSvV3Fxu24BqPo3ptCHykzMf9nG/CGMxQaiucRiWiW3Flz8o49yWP5RB3d68MNFb5mogSyWvDQA= +cr04jPiHcZs6CKfXkpLJpg== +fnnuZD0QAdxvV2PveMWL8CSqz+/2LSHUFrClcf5BdQrTv1c0XJfhDdpDKKPo0z0b14+xocsVY8vCZj/69cLwfoiB1FEWbmyYbrlCddxFGEsCX5QCEQvSd95BTrhq1nlZCNY9ga7YYTrfSnw+46RbFP+ovv8K0iZCj+u7EFMHJ/IMXwLlnh3z3jdyNDu7TFNo +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1YxDlh4Xxnj94+dsoExgikc543ZsABYd7Ls6YBhmFxb45wha5ZOThRb9RHRh4ve9w0I= +xWoGNWjKGPfI4gq8aHoTfPboh14laZFSUPEgtjSKUWO/PpcmZKW2bAAbmrDnw1SCP4uaE0ZYA6R3BnA+nWDqAy1mL3cUFeM8frCNXgKcWp0= +ncUn2TP8ZQ4fZMBYk+CUrbejaOCU8cNhJKyCBZWCqq1sY02wl0mD872qKy8RvJgnAbv7KCxyej8W9Q+U+qS+nMiBgdyd4po5Df4qqDwKnLOe/UUDOA1H47u9gqHf8WHl6B1PCla277lpLOkFu7GkR/mj98MnPUXQEt54mzwtDe71lGzby4KJ9Ma/n9OkWQnE +A4iM5LeR0j/aNdRd2ACS4ny8WZLXU05tQMpWxiCT9JuuAqjT43MG9lWrq5R0d6qpJwthsL4nn9qcoduCQeoOlw== +5REqqEJCYV1z7x5neHLGFDGAviuWIaE/TYt4LSZwDiMXm/53mOKPZnbzemXU4+GZEUcMNcyvWJGIOUQ5RIkIIpC70r0TF2jbHjMwqOoUldc= +3DnhOSKd6qzCIAY6ANKkgJYd0P/22B0l1MZyem52ut9bERvo0PX/T68xhBrjCbLsd45eA+op38niJOuRPomiESt7gvsqdzn5rexbsh2MuoA0EOCYOneAtVYTW8225GFO+VCWMzhTlT1SXl7twpHYnQ== +R6vHhJwn2j7IZ8/oBXXPlT1VLHYksWD6DKGcKNO8kyDSYJaiXzcsBEJQzsggxUu3 +yfhcGVf2iYc+Ztxqwy9MUFkJFa9YS88pkqSw34PcNf4lRXLRKL7BLErDKD8oJEUa8Is5OQTKCIYC80QzJUv+ow== +2wtz2EJ9gcVOn6ULRb1bGC0c4KGSGGqyU0mFPA7k9/zMuX9TZw/BP1yH6IjAzrb0UW4BCVFU5p42K5W86y/mOUKYixfq2yNN6gEZO27U3aI= +VmVrGQo2zRokW/ZuO9bN62alG5rsG3QEZzQ33/QBje3tmhSRMsf9NUXv9udZovEkoqejbYgifjLzt4+lAxk98Q== +VmVrGQo2zRokW/ZuO9bN61IpWQW8LyV5vGxMqJtLAMG4GidKSYVzYqLlwKnkYGh3 +VmVrGQo2zRokW/ZuO9bN66B2cWRLetUcWMU+aw+SZbQ= +1cV64uRoDtvn/9K8xFmBBdHLi7PMpkqFYqY7BMozZdA= +xWoGNWjKGPfI4gq8aHoTfPboh14laZFSUPEgtjSKUWO/PpcmZKW2bAAbmrDnw1SCP4uaE0ZYA6R3BnA+nWDqA8JKb1hHUBhmO1q9RpqWkO65x899CtNFF6+cegZlLYKatosIHzYd1sSurD0D440aNw== +xWoGNWjKGPfI4gq8aHoTfPSftyw23aWBm0SalP6bSZ5uFgWb+TdmXVXjEBOfs4SIpa/FkCphz9Tm5ZgfbL0jqHLzP164gR1SD2C8+k5o8kpAwYIJv84ufmqQyFKhd1DZCis68M8tRgaB6ZXGCaNrCkbBbv1rEFnJ3jwN4L9DvSeA0NBccCpd8oVxmdplQ24eWXMmssJ+SewTSv3wCcPzXui4VvS88RJY8GZHoEgTtE4= +UTxJ4jWO6dHQO8pTjqkurrjGYhp2vBYcOlWBI2li3lQTi9a9ylfr2bPqUUBeRL9Lp7KgysTqDJQBOFjwXnAT7A== +L0eUthVnpkGsmKFAX6d+uG2V4ODQQv2yL6nL1etYQzYzTVLBKVfwwGviW9wMWGbPvgdbWuIi8fuBLcL8k+2CkM8xJKi/40nY+5NrgF6Um8o= +KQe2cpNa8FkcC9SS/NMi8CwZjefuYFdgIVltRQ6d/OEFpkMvPQIEkY8jmyRMDoerHhdkzdV+Z4+gMSYNTzKt6xu+JQBCWWXi1l89v4epCfY= ++oS0XSKoqGXJre3Q/1v1BlvSlGCveCkN/Z8cmI+VqTrpmsmHgDb9KkDKC6ZVLS20UIZPb95vU7cgxrlJ21Bc2ar9RFcrYmzQRZ8SjSyJJ84= +cr04jPiHcZs6CKfXkpLJpg== +nCZEvlHDeEU72IAmy9hXaHLXXyBaHBxe+RST4mPX/1s= +JsM1B8QNr50RvpRbP1YnPuFx5C4ai7Wud51VQmeOJhtTbi2K1dw95XnWrJpGmome1iYmoZa6Oc4Q6EBuZoNV+fX+bUk82IAFMAM49zKO3cBTybz8r0H5oeeZD2PG2vz9YvwAvdCggT5OaK2dzb9qtkyl9EQWU/5TuiTRRLK9wPBcfCiv8BBTrzpbXxUyEJkAlKS7V+HzybJN3Z1X6AT+PKpq6HL9uxam8gsZOZHM2/HTQ3g4puzMy372Km4u6jdZortgp3ZYADH65j98Z05y5Q== +fnnuZD0QAdxvV2PveMWL8ChOX9d1DU+esVC4Z+TLQn0LGt4/lA9B1nK95LwQUMwDyBIvMUEQynEPvkKTsUPmBA== +tqyC1MJ6XiYh+GD0a1kaczblP9d+7MbugTMefIcU5knRIIUxS/2Wcv7v0aIZ+2c9FzydBTSgZglAaMKN7kCI/+weTX/7RdLptz4gfoDeOVA= +2wtz2EJ9gcVOn6ULRb1bGJbVRPKXqKnqQ6mDrr8yTxwFA9NUMvAWHGQpE/FJKpCSlbP5Zg80d4pY2gBPsk7i8ozLuZO1ozYdjzrgJw2U5OUEKIiU1L82Uq2rBkRVdg0FCh+0rlh1SAarvhCyIPm4eg== +KQe2cpNa8FkcC9SS/NMi8MCNQ/S9KLk1s7xLXNfFHdSzUR+lpVwNBVZYrPMpnyol +oV6EeCQGmWRpCjav7p0xHU9dTzjHsDMOBcmP4S9Cd0Sm/BfUaDvQq6+oeGDChPW7TipeycZUvdKZKFIi70R6ce9xK8Y1ET3qsvaYpUbml/t0t7fn8Z81Ufs3uMSqjXwn +cr04jPiHcZs6CKfXkpLJpg== +6r4ECPT4o7OwHqIcEmFYzuoVsBFhqfPJAaTURpnU6VwScF3eicqvf7HjZ11RI4NP +fnnuZD0QAdxvV2PveMWL8AuKdHixQT66Gc00Gg1vJS6+GO0xBdztvdUhCI5Ge5N555Yujja5lRVAwH+nvKDo0g== +6r4ECPT4o7OwHqIcEmFYzheDtvHlqty/0KdCgKl0ylz/1KEPPjnNCy+Qa3YCQV4ej0URjhxYyA78RnBZQa+b7w== +KkG36smI6M1sDeGTVVD+qSP00dlWGnbBakrMMitnwAHUW1Dpt7HTXNcTRJlaQP8kip102wRtA7YBJRxVLYKDJU8D8qE4aeWEhK4dD0Pl6fVL3wtOaxsjGuUlU3uOMl80q62tcnhnh6ZpLjlBbduGpw== +4IH4b6ywJwYs95/tFiL8lY1GyBaN9rJuTeVxWOfmJ6U= +2wtz2EJ9gcVOn6ULRb1bGM8IQ2dJE0FhTsv41jhWplnZWgQQDp3w/UtYWI2rgs6eeyYGfdf/TyqbhqX4p71ooQ== +VmVrGQo2zRokW/ZuO9bN64SEuusKjyuHabj781H+tb1pbX8IQ6fviKyAJ4bIwo6f1MEOhr9OA2EPLA0laTC+bQ== +KQe2cpNa8FkcC9SS/NMi8EFKVhLSE9F1GFAOmsqhoz0nSuR5P8d9BeQpjUGmCKYl +PUlTSkp7cRpcs3JwkQy2WnJTeXdbhFjvuSced9pxt6YzgizBmOcyjfpim0HV2MrFzan/z6jOh7I71bA4NdyyuRUOhVQJVxU+8UkcayIiIfY= +cr04jPiHcZs6CKfXkpLJpg== +fnnuZD0QAdxvV2PveMWL8JrP786E2VWv6OEVjvqfqal1F0tGEw4g6I+m/uUhNjRha+6HO1sT2iO4abtaK4OE1Vy7eO++Khxe/SuLdIKHyJcUbwrJ8UwpyBO+PNDZ52RrEVi8iM258cNgoOb0YAZQfN7kK2V3iyKgd/GnMPZXMTL4JzhUkcRyggyZfDNgDqT2 +XmWcjbejqXjijvSDMMApGmQVBFK+3Fu7ffDX8KPyOyo5Q0Ti2ZZ2YMkf71vb/W909tViVHOCaDHTsuIQl/bZlA== +HyB13FOAEbSI2KAABytWG1lJdYY/yjhXtGuYXK23U9uctTZthkax3pqCq+KZWhToqgQK7rULc+rIh5RV3cT5QA== +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1Yx/jb+vZHJt3XPEP0sYLVm5vGGxbG9PnkJrq0RBLRyPIHfCkYLqlrQewmKTeUugXsTUbZM0+Bq0egdvyjbtYB3RDS3E8+1Of5ENemfFFdPgZTU4bGrWNIhvEpJReOGDokjH++X8r7oe9AMTBA/AQnDw +ut2tmPKYoa0Hz0cuVsUj3qcXVeFDr/mvvHeu4hSOYIh9yLoIX9tq9Y5O/hZBb0tffbu4/T77YQFbF6NFadqzOjISdrhc269PQWx3OJ7ouTW/1PToQZX1+XjOsQbRA6FVDFry8tD5zNbLBVLnSpaRBYyncHoM/p0AK6AxDDXFQ0s= +ut2tmPKYoa0Hz0cuVsUj3sGyPnE6nUmywzlUF1eIcgMYiYMpPOa4pahwX32jHUUbAHbImsTgL9Rk7Q5kcJIpqFEt3V081Pokv5C1DXYg8pqGB9PcgYc/0ZKPNh5eLzBu+Y2u8nxkPk+kysiE08xWJ/yoGtaacIJybZbO95Rb254= +CTDlCq/Yx3kXyawzsUjqPn4DoEOXY94GKmJFYozlJ3MZVnazWs2aCAAG7WaeGpIc1rom6k73cpvAMPH+Q//OZpGilDx8wm+pDjOdrv5qwBM= +qgvPLYf8+6pituXauG2lHnpUgYTRBVrfogZrz8+a40teOzfvu+2n8c/F+VGRE5UBE+95iEDTw4AWhkst55N38A== +2wtz2EJ9gcVOn6ULRb1bGC1vdhobTwHQicpopnpHvZykrJyTIAmAai0BhcHk+7gzy73WaZ2MEopQp2fV5AohLeqaAoV6nx/xVq+kH9sAGKaH4qZ78s4pETjbffhT67+2hs20W5A4KJiH5y3gyd7fe6NKlulpdwtFzGl88VxNvpEEnITo4gbXmnmsr8ygIv8n3g91w2QrdIzkQn0ufjy3bw== +GWDYUI0c8Ct3i6L4tejkFH+T+usUscBWHul4+WUmgwbOzbAkPgcaVWU00rXS/6TINxRI9WPCNgbBpH3y0BmNIlDXzXFXd8smuo4pyHtd6TXp+IWr6p1Yms4KzV7K+7d9 +LiVO4OAZQ41GCrj1VFqy49tJnp9BlG/aec6OCmYKY+09xxIr6eMh2alPog27Xzy1z2oKXLS73HcJeh1hUDN7H85yZJB5KxuBlzR2QZnnv/WK+a3HqEG2/aHMlFkAVe3Esv/bm7MQwzNHTVwwyX4+6N+/zduqbDASrbgp8JW6r5U8s+FwOGJKV3Reqnqs+/XncY8o8wAEYx8at6gS1KqPmr0887m7q3O+Ee7ToQpXAfZjupafNVMg8Y8z1mpq7au2 +LiVO4OAZQ41GCrj1VFqy49tJnp9BlG/aec6OCmYKY+39fBu+1OUNo9hSWZFf45Fcr6iPppgD4DcuTJOEcdbftxfoxWo+dL7xmuaL72fiDyJXXIbckZwwxve32cXNdfbkRrzY5+C6FmNBvbKFIVZHoA== +LiVO4OAZQ41GCrj1VFqy49tJnp9BlG/aec6OCmYKY+1n2xOSacmX62EVJC46g/rjd6q6leIqAfA2wvM9hBNUnsUjYma72h95TF0VScg5YB/mF3paIQlxoFvcT9133xB0byBDM3NyEvDFj3YDOsQIzA== +LiVO4OAZQ41GCrj1VFqy40xXTJtMuJpqKqDGzO//HWCYHnWQl+/6dVTnz8Uv/mP3KmU2LGm9S9Y68bkk6wUkdrOAEmwaqRGxgGr4fpcThJo3C8cyEwdyh/XtjLf+FpGV2WAR9PWURWOjY+3Dsv/AZJYvWnrAn0zEx2VtrCjwEzW6kWWTTynhagWnOHllrPEmWkZfNHCg5BgDtz8zp+oNUA== +6S1DTW1TCVbUfeFz1/zAP8MlMUQ+iq+gDP31FgA75hZF23hEP2cwNRG64dw0lVR68qDE3/oN8uznLfLYYOujJRyjCktuJsEmBB7fgUvhf6KfqI2UVTxHqF9WT82gADkW +fnnuZD0QAdxvV2PveMWL8MnPURZP7EzUIFSNSkR/DP3nRYXb0fUDE4U7M1r3zRQtQl7HxMPsKK69DQPV524wlWgMtz+Ny/G0Pe5DM3zxSvEXMEiQRG9A/Zj7S8KL6NnajyFMooBUqX66G8CR8bMScicsJ75E0Yd9Cze0RNKU9Wza3aIdiEmdkoJJHprWviuT +oacxXEoPih9NKsd5qduwcBiuy7u2Ow2MIJfCF1pBvq+l5eyzjf2YC17f5yyPsixULDkW15S/zqjX1ADtgbpcxaPhfDdsZoOAh4jgcFf2hLfK5yksS8k7tRsztEy/dLNF1rwP1aVLBoDTQCcVq4DrJQEM40DKF8m5PpGaChnts2mp/Acv07Y+1SGRblH0AiyrFhwdzM31v3+snTzxybhzqh3fcofAFa/1YdUVAUvETCuO9MReNXR/ti5B4CEPZcu1CVDXm0iCcoCE2zgQZBAFl94T2Cum0QSYBb0+iro9jd/nItGRClUCZa1bTCuqbX/O8d6o77kuW416R/Ylj7EogtYaTw+Nj7s4g7j3vFgNlzDarsz+8doOepdfvAEN4+BOfX0kFvzsGw+BZvhVQsiFdYe5vtFiL2iELYzBnoDcptzwmqgcgyDVVOsMynvbIjFPM1zF1gxYKs63V9iT5VO4yFXtAf87QNjvvyitSpTdIoDijHo5xNeCCIz586+rNedRp/92kdW+Soi4SKEHSO3+myTZEV9rgTwzVT+nb7xcD7j/TBg/LIl1rnEh9xXfubdPmGA3DVwS7bWo3OHWlBhKyvBfqQcsIcvV80+2O3MHeYFuF28roWFw6QhGfFGcHoCrwgaukoJVKuJgHNrirJoelrvpKHC0QNwVRr73WlZSwyIh45/Ilyg3N/Sx6JET5iz080hktNKrJLUui0w7UBjTLGcQhAOJ/ExFiqD+shKGVW+Omn0I60Fa1AL3048OE7P+JYJo3d2KocToiEQ7EDx5mGzt2Aof1brVNuzkaBxmCmB8+pgf625W3oBZ3l2T0XtHI6DBqAddf2ue2yWW2VErfnsjCHxEp9RlGPsrJnuLq2+YMvPyH9heuL9TvnMKPKHdMm2pOvTac8wOO05I+o9tbwHaQrrzYs1KjvW8Ogl3vmYEtvi999H/PJrHRNdvCZOvfpcbLd306BhEpxAXscITxTqfrXc3v9YpzMcRsI7mGnLJjgioA2RCnpEgXgYsPiNM +IL9sA67H0z06wFBWIR8QkaTnKJDOXWBfh9EK16Yt5HjpiShKLm24lByCsmMOp/+nm6sbfuAiTHANiUGCrj3O6YJUoGlXCUdJNawa7R4ydgTXxF1mtteD59hEDOyF5+eaneP8lM1d5A4NQGGoWiVG8g== +NREvnNIDTttHan/FJweGj/msBpf/p1aKu8LZw94+KUqSn1c+F+iZlZgS/VL7fr/48X68sN6vyPH3CMR6/5oTo9qAC+Aq27vo62T00ZLfVanRZyH61IW97E6RRG1aY8G9PylFnqmEkg0hwE00IeHsj8B2FWSQPVs2IQsDmtv2d0jkn+EfMgHJJpItKcvIvvSjuTUXyAh+teqKf6BoWr1i9HmflGa+pBxysP8DY2jVav7fOiF26qxzGUTuyAkvh4bBiPPOkc1z0f2Kfh54NmOGmQ== +yYOGmvXvGt/cRLLVkO4eKqa+hM3PuRZfD7b8rMYq/UT5o+JnFaIJw/yVufWRJ3GYE6JJV8UyWObkwgX0nomex4N7hMwH0p/mvytxHRM0LsXoHlzxAgZ68TlBXX9wMqO+ +NREvnNIDTttHan/FJweGj/msBpf/p1aKu8LZw94+KUqSn1c+F+iZlZgS/VL7fr/48X68sN6vyPH3CMR6/5oTo6pzILMLc9S2Abfp3x11erswXy+6aKQxkAOV3JNDJy/u4bISLULyNHieYZYXNxvYW/Dtf+nQ/d3q+8B6w+EKAiJUPNmPW8Mx5AGhPkKWWMQsz7ioFJ2CQjBvlTk/2hfy1MbIqtmWyx3r6g2Vvg1+UQ4ptd3QByZwW46+nwoFVVe7u8OdItBRe7okhek+hhTpiYfdw7mDXnQfcpCHAC7y4BjGXo0CurX7AjM06m7n6D1EA7tHc6gqHSOm5/cGUTKXig== +yYOGmvXvGt/cRLLVkO4eKqa+hM3PuRZfD7b8rMYq/UT5o+JnFaIJw/yVufWRJ3GYE6JJV8UyWObkwgX0nomex4N7hMwH0p/mvytxHRM0LsXoHlzxAgZ68TlBXX9wMqO+ +CTDlCq/Yx3kXyawzsUjqPj/1UgvtLUwbSocBDSTGdlQdGlfuxYW1zktAmMlWajLEJlycf3QM6d3m2/8WV/7qap+0+9bbcfKO31Ha3+gfc0v/wa//RxuB/CTGHWThqgBLKsNaADG5PBVOlQNWxwKSNUS+55fKYqn7UbOGJUsBt08= +iwvjn39WsWBJqN2zZdgrYH73io9UFYsVQIpyJHGNPolTowZ3ES6qaIlYZbd0+9c8k/3/kVFrQkiBM1cR8ZRiGoxZK4ShV6FY0bULRqyn93CV5P1j8chjs7oBotB9I/BtcUB3JYzsnV+37ppDY6xcDg== +iwvjn39WsWBJqN2zZdgrYJapEN/AKnY2zLbcAIzyrBBU1RXGfgWqmb+6/1Y8honwoq8vQFNWL5QXG2xnJPe0LK1Ma+Uxp4BV2qI3ukXHtQYKXoJzDPI9GCy7FvK4NRqoVIY2d3UNJZ6JrGvDp8Itv8UtbTtOKRZzMGesfAFfpr8M2F7b1sde3FoHIbX53bIajKUCfv6EdR2qn5YyhmO/0Q== +96orka/uERLyRst14azQwifX4NgxGrWxr0Kq7A5Rg2HnATx3FBLn85SCdMGIOm1E0hlma+GxHkgZc93eWmwVjA== +6we+bQ+BNhpl74ICwbzDD6SJhA5UxkUBfCZmu1pDNmRCVuoiUdtV/LjGUcclBb9x1lO73/pbYlp7Qn7laP/ALybE3LW0N3KSwXGpr6c9FY4= +cr04jPiHcZs6CKfXkpLJpg== +fnnuZD0QAdxvV2PveMWL8FnhE/Qbu1TUcwt3ycoXzAb63R8Bw+Kjhj/s+Ugn6uM2VYtJ5GD3NvbvtZB/8CpMknz6mUrg1bAfFvVuTUch8Fa1XKdHthZX+SiOeT190cbuzf1PMxPOpzbt2lZhBmtWnV5tc/shXzdOMbVzBF3CVbwpT3MP6rpratp4mnHz3RuO +SE5xU/Z0JSLRg+BF0JZlQ351DCSneiU0Ro4ul8s6YQ0+e9ClxvBwl8Z8jDAAh4Pw29EBMojpWeiQDQQ3je1JBQ== +ncUn2TP8ZQ4fZMBYk+CUrbejaOCU8cNhJKyCBZWCqq3ZxbWlzmOTv+kBIJsZsjljcT1q3oO0cOKLsE5F65U+psAkCkXFw1+rnPdHpl45RtMkIcHFBbTnubK1Yw2j5SiSMTQaANcjGIXkA5pbNDFojl1kkOkaPHt6uO54XZwjB9PlZ8XdctMBp+wpTD06crdB +R6vHhJwn2j7IZ8/oBXXPlciOa3G4bauQKWbklU12JP0CWdxdVaZIoNzpWfrmbe5i5gswcTWd6DqXeLXmYLSydw== +wgR07xfoapmx6eEnFHXXYv6sj3jsGK5jp+XNGADM1YwZ3X8erO8tF+9rF+ud3CmG2DzTMUCy19ltKZJBZVeJ2G3b7Y/tUzeVbHaaGaNkyQryOXyVCJelyU3NO9pBJXZwU4csk3bxLN74sMPi17loDmo4qNbgIL5dTT7IV6iCpcJf45etCMNP7ojLR/iLdfFt +ut2tmPKYoa0Hz0cuVsUj3l2A/S9h8YCNZoTpXi3WyNmbvxKV9hRgROXsqHqZmvm0sTMN+4iSaKr2yT/X+fgBC4YDIVZ1uBFHcV7UgCMzOjI2lx9+St+VE5ivldn9lPHV3rb5YkHv11D4TUNLk3DCWm7kTSih4pt3xjcF9tbSGek= +ut2tmPKYoa0Hz0cuVsUj3iSUOfYfZogjpziC8PUGLSE9MjZBw4GuEUF0vb4KA4CRuxc4tpvehR3tbZvfDCzpxJksbJGmNWFIh7s9WlNnDc+qD7wQpCT3NbfBgRmblirSNOVpyIgLflRUGrvOFeOG9zFm3PjGlOZ00Vv3qWkURL8= +oF+52jOqU38B3IbYFCrd4N3GYJGn+k9zqnS5QNsGjaRnqfzqGwgeL+xZ/I7WiPbOLIGF8wwQthufZuWJLZ8TV8PVM+kG0CKewnf99F2dx5s= +p4mlb5HXUU40EYWFAnjifyb31sUX+0BXv/YW9j2K+gI= +8qTxQznEcsQMgFW26Dqik37xPQpnAepb+3VTmXSvTHkBXZvjIZwIqQeUzzujDsnU8/MAPAbj2oDa9K3AqwPTFQ== +2wtz2EJ9gcVOn6ULRb1bGDulJXIx4y56v5IQGpDjYFZ136nD/ZkugeoB6wP9mFEheXByEOtBdpbVts7POYAC1Q== +VmVrGQo2zRokW/ZuO9bN60HnLn+7qb3BEYgduIkYBusMNq2qQ2m7PTCE41vqIcK/Cewvi49S3FU5bVmsIU/TRg== +VmVrGQo2zRokW/ZuO9bN66B2cWRLetUcWMU+aw+SZbQ= +fnnuZD0QAdxvV2PveMWL8GzimDATsb9PCxhUZX36WPceYPd8+C+DZiTdw0oWYeDzPAREthfc0Ab+8X6ybl3VEmmQvWGs9zAYVneC2Hy2egkTCe1Ob78xdnB0S18HF60ZIM1v7YQAQ8UB+CC4cZs7fQ== +AaCYXSBoNzzxOXBdbJWxCI9qK/O2dxQuwFCqR22/ccmrRlWPB13BOQzWf+aWkGS+2+eb+G2UGJ030Ldkc/TRa6QakPcSXSNOI3gUmbZsFE56ZNHp130Cn7JwRwpSNvyF +Igyj+TKK+I9SELYdi+D74wrFQ1TlNAWhWGzDssoXLbghxc1cj/mwrtUBlQ6VRLTuoSRRar3R+eQrcrTodxnolxO11AHYzchAkrT97uXxfomkpOzQL5Tmlw5oI/hsnk0R3C+lgPstP5/4bDc90+iRQT3cMxKnuQv1rM9z6oW2nPBssswhEAPdwxcmplZu4C3JnPxdZByVx6FbRN2Jgg4Y1CUhjhiIBddhAyjAv1YWNpTM8o5GlDeqrvWm2293br1F +Igyj+TKK+I9SELYdi+D74wrFQ1TlNAWhWGzDssoXLbjeoHs/8Mje8tsYdNHFBscqwQug2YW9a6jBvQV2kMslbbKaz2EQLgEjn6frBGO4TfxHRuA1CCfJmWabVkIQMFxrCJix3c+0lMgO1+8zPIjvrg== +Igyj+TKK+I9SELYdi+D74wrFQ1TlNAWhWGzDssoXLbiZex8VHIziY50edMtwBriCkbhwV2pZ9K3FDuHuJkHYxeLxgByj1SOSg2vK7OLlpEhM2jDZRUpXVynPlw8R1bP7ZLPJI+hMXZWTM2qTMVW4Bg== +1cV64uRoDtvn/9K8xFmBBSyIwLLAyZtDmWjNCGSK35oV7Lln1obVdrK86aC2P/5d2IugIo65luFTN3ITzWeIAojASipLuDAxR0vKGzse+0ZM659ojBqm1tuvWsd/P5Hs8sc9RblCikWUy4X3c3w5R9/jB0lLIJ44vKAW2Z/MoB4f+A5ULgHqCz5/JSrpUeMfl/E8YVza9ME/7/ufufeoNA== +/wrjOJbIgR3iizz9ZfsYFV1S0QEoNDYDB0tPgYYyunCzrR8+mwbHh2ZHlRO+dgFF1df50fLmTkW5bI44PHknwmKwbKoYj8Y9Uyhuh9nMKzL8yi5sZyw9SFrwXay2nHnR +fnnuZD0QAdxvV2PveMWL8DHcrBrw7XfIoGItmS/X1LRvez55+VhH4ORJHoks3tPiigwsWjzGIQ1+EqGmE/pzsSG8L1Qo4r3de+ZwnWu4x5aYGJBCnAtBWXOAjrBM7bRdQX4XYwovgtmfpuO6eZBDlcrPlsAE0RxjrJ/E/BsdBTRGuen7gVcgLjFTZvWMO4Fa +oacxXEoPih9NKsd5qduwcLPkuMecFSQLme+U3RYD4k6vG2vB69hZeZTj75RiG2qUS2vKp6FFekTp22dTzpmqrhqwF3gZSut9skRswVAOd+ey+p8hkk31WVViZKUsvFoZcQzf5qFZ5bMTM/a7hA53WqJlzxxMkdA2km9UpRf8a2WRvbBa6KD/jTkeYiZl/nHyHjmA2tqxhDLike6xDAbA2AQYYTbkiirIVH9/A+P08TmTNHCZUryZAOMBqpffFuhKa4uoh4D0WBbjEPrFdoiEz3ocw5bKhTwdKD2RQWcIFihi+UnmV2ApR3o26gPYgi6HRSeiKrZEwhcp5m+whI7lnng5/abuOGGbabfAagwFRP8HkguHuJ2vfjmYtJiVXkgrpjYSJsDZHFwhgfPxj9XqVRNzREoMxO0S/b0FC90Z6w0r1eMRmzBF5N2Zn/4JOzfvVymJ+MEoKHZyQjjwZIIOXp4KFKnmvUZ9q4C//bBvwEaBQJ6Qteg7RZ0QQkafEG/NBncU1vOMCYFIZTOG/ym+H39xC51Omll5vt64U6rtJO7lsIO+WKd9WZAUYATPv1J1g1aRzsJycWilmD+jWXpI23ZcbmmTf4VctIpeCNZwnm+aQm7J30HLgU2PnkluZdq744bQ4GCZQVgHkLM++NGVUmDB97Rx/4FIZ9i83MNYpndJPYOe2yYnQ5vThEHFeanLo115DYYct09qBrRVGeM9hxfN7UP1MWDKFeAbZF5flQE= +nPNZk+Xvg1c6GZFzrp1IbMVt0EpHjdQMD/KiHv3lEICoTqIbAmvUQ+gVypzCdBB8Gs0AbP+Zh9pMLpuFBMJVhrRL3RKTfSSRfVHFNiMAjiuK48g7qWvs2jmegZXfmVGc+gXJ9imIODTMBbyPmgyrR0qZFdlSN4+ocHLB0gjsQGmIRDYaJNrNAnS4USkTNOXVJ+CnobHbEUrDvenXe4GcGwb6lwpI0j1vr6tVraqgemOc+yVhnFDBFX0eaTQPMUsDqeqEbxv/r/mhMEfOmRGhUA== +wu62rpGT52ZTxeUUs2GeuGgAZsk/tzh/isMkRRQuL7nq3eaiw1Qb9M0+Qc8KElN+7mEiLyeftYZBbZDOnzNciRm5TdJahLGV1drbGS1rn8dSqfam6zfdyDZ+0qHR+jnX +nPNZk+Xvg1c6GZFzrp1IbMVt0EpHjdQMD/KiHv3lEICoTqIbAmvUQ+gVypzCdBB8Gs0AbP+Zh9pMLpuFBMJVhlpvc3VMIX/ToG+YV/rNHgA6KSwDvFN3sEa/wtjnlB7Gwj6zOH2itskLF4HrIvxMLthUmGUfGpnVqtHOpKzBOhMdQMK3ECYTLkqJgqQIjZkxcxvKH33BB8iIKHhbL/7VeuzthB99YJSe3mQ0vuX2vvcBZTIlX/khXl8eo63Qod7ozf7o47f41YKmUdwsO/JMXjeiD0NMGOFxwR4AREJ50Invb5AF4D9pp2kJTvEUlKGD+L4b4hSYjM37qFKD1zXD1A== +wu62rpGT52ZTxeUUs2GeuGgAZsk/tzh/isMkRRQuL7nq3eaiw1Qb9M0+Qc8KElN+7mEiLyeftYZBbZDOnzNciRm5TdJahLGV1drbGS1rn8dSqfam6zfdyDZ+0qHR+jnX +8qTxQznEcsQMgFW26Dqik37xPQpnAepb+3VTmXSvTHkBXZvjIZwIqQeUzzujDsnU8/MAPAbj2oDa9K3AqwPTFQ== +2wtz2EJ9gcVOn6ULRb1bGDulJXIx4y56v5IQGpDjYFZ136nD/ZkugeoB6wP9mFEheXByEOtBdpbVts7POYAC1Q== +VmVrGQo2zRokW/ZuO9bN65CLqRUEWJ03MnVfT48m2QyxHgYbgNM+mUWp/RHlScL707Hwd5hAXrcCQuNOHzWRmw== +VmVrGQo2zRokW/ZuO9bN66B2cWRLetUcWMU+aw+SZbQ= +iwvjn39WsWBJqN2zZdgrYG5NBDXw/UERaMa5qn6uxHIlNsNh7PeHEY5YZ4B/EHd4pgIscKVIIX7ioUSholIaIyLKidrEOF20CDJX/iz34wBbW9zWqoCbzoD6tzk7oYnY1Lb5dkZX86F/rOGlcmjsIQ== +iwvjn39WsWBJqN2zZdgrYJapEN/AKnY2zLbcAIzyrBAAFOSsyeU1IHqhRAAJmxek18L8hhYV0nclwoNTInf9BxJ1mKeAbWt9n4txx96fjdNoTICL7WaSPRAd+PHZ9Rckg0kFG02IFUMdzt0r5RApu6mM0z1QqDZmZc2kp00MWUTfdnwFkK6MLDk+Drcqk5fnWaio6MEpLq2BY4/d9cl99Q== +96orka/uERLyRst14azQwifX4NgxGrWxr0Kq7A5Rg2GgtWLCloikT548er10NVqyOU/URIs3U0nX+iki5Rj91UImIzSUQ3706pgDdxepy+E= diff --git a/class/public/PluginLoader.py b/class/public/PluginLoader.py new file mode 100644 index 00000000..4c401d2e --- /dev/null +++ b/class/public/PluginLoader.py @@ -0,0 +1,24 @@ +from .exceptions import NoAuthorizationException, HintException + + +def get_module(filename: str): + import PluginLoader + + module_obj = PluginLoader.get_module(filename) + + if not module_obj: + raise Exception('importError: {}'.format(filename)) + + if isinstance(module_obj, dict): + if 'msg' not in module_obj: + raise Exception('importError: {}'.format(filename)) + + if module_obj['msg'] == 'Sorry. This feature is professional member only.': + raise NoAuthorizationException(module_obj['msg']) + + if str(module_obj['msg']).find('Traceback ') > -1: + raise HintException(module_obj['msg']) + + raise Exception('importError: {}\n{}'.format(filename, module_obj['msg'])) + + return module_obj diff --git a/class/public/__init__.py b/class/public/__init__.py new file mode 100644 index 00000000..c99c507b --- /dev/null +++ b/class/public/__init__.py @@ -0,0 +1,20 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +# -------------------------------- +# 宝塔公共库 +# -------------------------------- + +from .common import * +from .exceptions import * + + +def is_bind(): + # if not os.path.exists('{}/data/bind.pl'.format(get_panel_path())): return True + return not not get_user_info() diff --git a/class/public/common.py b/class/public/common.py new file mode 100644 index 00000000..6b022b52 --- /dev/null +++ b/class/public/common.py @@ -0,0 +1,8277 @@ +# 公共模块 +# @author Zhj<2024/06/15> +import contextlib +import json, os, sys, time, re, socket, importlib, binascii, base64, io, string, psutil +import gettext +import typing +import werkzeug.datastructures +from .validate import Param, trim_filter +from .regexplib import match_ipv4, match_ipv6, match_class_private_property, match_safe_path, match_based_host, \ + find_url_root +from .tools import is_number + + +import collections + +# Common structures +aap_t_simple_result = collections.namedtuple('aap_t_simple_result', ['success', 'msg']) +aap_t_mysql_dump_info = collections.namedtuple('aap_t_mysql_dump_info', ['db_name', 'file', 'dump_time']) + + +es = gettext.translation('en', localedir='/www/server/panel/BTPanel/static/language', languages=['en']) +es.install() +_ = es.gettext + +_LAN_PUBLIC = None +_LAN_LOG = None +_LAN_TEMPLATE = None + +if sys.version_info[0] == 2: + reload(sys) + sys.setdefaultencoding('utf8') +else: + from importlib import reload + + +def M(table): + """ + @name 访问面板数据库 + @author hwliang + @table 被访问的表名(必需) + @return db.Sql object + + ps: 默认访问data/default.db + """ + import db + with db.Sql() as sql: + # sql = db.Sql() + return sql.table(table) + + +# 连接MYSQL数据库 +def MysqlConn(db_name: typing.Optional[str] = None, db_user: str = 'root', db_pwd: typing.Optional[str] = None, db_host: str = 'localhost'): + from panel_mysql_v2 import PanelMysqlWithContext + return PanelMysqlWithContext(db_name, db_user, db_pwd, db_host) + + +def HttpGet(url, timeout=6, headers={}): + """ + @name 发送GET请求 + @author hwliang + @url 被请求的URL地址(必需) + @timeout 超时时间默认60秒 + @return string + """ + if url.find('GetAuthToken') == -1: + if is_local(): return False + # rep_home_host() + import http_requests + res = http_requests.get(url, timeout=timeout, headers=headers, verify=False) + if res.status_code == 0: + if headers: return False + s_body = res.text + return s_body + s_body = res.text + del res + return s_body + + +def http_get_home(url, timeout, ex): + """ + @name Get方式使用优选节点访问官网 + @author hwliang + @param url 当前官网URL地址 + @param timeout 用于测试超时时间 + @param ex 上一次错误的响应内容 + @return string 响应内容 + + 如果已经是优选节点,将直接返回ex + """ + try: + home = 'www.bt.cn' + if url.find(home) == -1: return ex + hosts_file = "config/hosts.json" + if not os.path.exists(hosts_file): return ex + hosts = json.loads(readFile(hosts_file)) + headers = {"host": home} + for host in hosts: + new_url = url.replace(home, host) + res = HttpGet(new_url, timeout, headers) + if res: + writeFile("data/home_host.pl", host) + # set_home_host(host) + return res + return ex + except: + return ex + + +# def set_home_host(host): +# """ +# @name 设置官网hosts +# @author hwliang +# @param host IP地址 +# @return void +# """ +# ExecShell('sed -i "/www.bt.cn/d" /etc/hosts') +# ExecShell("echo '' >> /etc/hosts") +# ExecShell("echo '%s www.bt.cn' >> /etc/hosts" % host) +# ExecShell(r'sed -i "/^\s*$/d" /etc/hosts') + +def httpGet(url, timeout=6): + return HttpGet(url, timeout) + + +def HttpPost(url, data, timeout=6, headers={}): + """ + 发送POST请求 + @url 被请求的URL地址(必需) + @data POST参数,可以是字符串或字典(必需) + @timeout 超时时间默认60秒 + return string + """ + if url.find('GetAuthToken') == -1: + if is_local(): return False + # rep_home_host() + import http_requests + res = http_requests.post(url, data=data, timeout=timeout, headers=headers) + if res.status_code == 0: + if headers: return False + s_body = res.text + return s_body + s_body = res.text + return s_body + + +def httpPost(url, data, headers={}, timeout=6): + """ + @name 发送POST请求 + @author hwliang + @param url 被请求的URL地址(必需) + @param data POST参数,可以是字符串或字典(必需) + @param timeout 超时时间默认60秒 + @return string + """ + return HttpPost(url, data, timeout, headers) + + +def check_home(): + return True + + +def Md5(strings): + """ + @name 生成MD5 + @author hwliang + @param strings 要被处理的字符串 + @return string(32) + """ + if type(strings) != bytes: + strings = strings.encode() + import hashlib + m = hashlib.md5() + m.update(strings) + return m.hexdigest() + + +def md5(strings): + return Md5(strings) + + +def FileMd5(filename): + """ + @name 生成文件的MD5 + @author hwliang + @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 GetRandomString(length): + """ + @name 取随机字符串 + @author hwliang + @param length 要获取的长度 + @return string(length) + """ + from random import Random + strings = '' + chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789' + chrlen = len(chars) - 1 + random = Random() + for i in range(length): + strings += chars[random.randint(0, chrlen)] + return strings + + +def ReturnJson(status, msg, args=()): + """ + @name 取通用Json返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return string(json) + """ + # return GetJson(ReturnMsg(status, msg, args)) + return GetJson(return_msg_gettext(status, msg, args)) + + +def returnJson(status, msg, args=()): + """ + @name 取通用Json返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return string(json) + """ + return ReturnJson(status, msg, args) + + +def ReturnMsg(status, msg, args=()): + """ + @name 取通用dict返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return dict {"status":bool,"msg":string} + """ + try: + log_message = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/public.json')) + except: + log_message = {} + keys = log_message.keys() + if type(msg) == str: + if msg in keys: + msg = log_message[msg] + for i in range(len(args)): + rep = '{' + str(i + 1) + '}' + msg = msg.replace(rep, args[i]) + return {'status': status, 'msg': msg} + + +def return_msg_gettext(status, msg, args=()): + """ + @name 取通用dict返回 + @author hwliang + @date 2022.9.20 + """ + msg = gettext_msg(msg, args) + return {'status': status, 'msg': msg} + + +def returnMsg(status, msg, args=()): + """ + @name 取通用dict返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return dict {"status":bool,"msg":string} + """ + return ReturnMsg(status, msg, args) + + +def return_message(status, types, message, args=(), play="", requests=()): + """ + @name 统一请求响应函数 + @author hezhihong + @param status 返回状态 + @param message 返回消息 + @return dict {"status":0/-1,"message":any}/下载对象 + """ + from flask import g + g.return_message = True + # 非文件下载 + if types == 0: + return_message = {'status': status, "timestamp": int(time.time()), "message": {}} + try: + log_message = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/public.json')) + except: + log_message = {} + keys = log_message.keys() + if type(message) == str: + if message in keys: + message = log_message[message] + for i in range(len(args)): + rep = '{' + str(i + 1) + '}' + message = message.replace(rep, args[i]) + return_message["message"]["result"] = message + elif type(message) == int: + return_message["message"]["result"] = message + elif type(message) == bool: + return_message["message"]["result"] = message + elif type(message) == float: + return_message["message"]["result"] = message + elif type(message) == dict: + return_message["message"] = message + elif type(message) == list: + return_message["message"] = message + elif type(message) == tuple: + return_message["message"] = message + else: + try: + return_message["message"] = message + except: + return_message["message"] = {} + return return_message + # # 文件下载 + # elif types == 1: + # # from flask import requests as requests + # if play == 'true': + # import panelVideo + # # start, end = panelVideo.get_range(requests) + # # return panelVideo.partial_response(filename, start, end) + # else: + # mimetype = "application/octet-stream" + # extName = filename.split('.')[-1] + # if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None + # public.WriteLog("TYPE_FILE", 'FILE_DOWNLOAD', + # (filename, public.GetClientIp())) + # return send_file(filename, + # mimetype=mimetype, + # as_attachment=True, + # etag=True, + # conditional=True, + # download_name=os.path.basename(filename), + # max_age=0) + + # html响应对象 + elif types == 2: + return_message = {'status': status, "timestamp": int(time.time()), "message": {}} + if type(message) == str: + return_message["message"]["result"] = message + return return_message + + +# V2版本的成功响应函数 +def success_v2(res, format_args=()): + """ + @name V2版本的成功响应函数 + @author Zhj<2024-06-05> + @param res 响应数据 + @param format_args 响应文本提示时的format参数 + @return dict + """ + # 对文本响应做多语言转换处理 + if isinstance(res, str): + res = gettext_msg(res, format_args) + + return return_message(0, 0, res) + + +# V2版本的失败响应函数 +def fail_v2(res, format_args=()): + """ + @name V2版本的失败响应函数 + @author Zhj<2024-06-05> + @param res 响应数据 + @param format_args 响应文本提示时的format参数 + @return dict + """ + # 对文本响应做多语言转换处理 + if isinstance(res, str): + res = gettext_msg(res, format_args) + + return return_message(-1, 0, res) + + +def GetFileMode(filename): + """ + @name 取文件权限字符串 + @author hwliang + @param filename 文件全路径 + @return string 如:644/777/755 + """ + stat = os.stat(filename) + accept = str(oct(stat.st_mode)[-3:]) + return accept + + +def get_mode_and_user(path): + '''取文件或目录权限信息''' + import pwd + data = {} + if not os.path.exists(path): return None + stat = os.stat(path) + data['mode'] = str(oct(stat.st_mode)[-3:]) + try: + data['user'] = pwd.getpwuid(stat.st_uid).pw_name + except: + data['user'] = str(stat.st_uid) + return data + + +class ijson: + def loads(self, data): + return json.loads(data) + + def dumps(self, data): + try: + try: + return json.dumps(data) + except: + return json.dumps(data, ensure_ascii=False) + except: + return json.dumps({'status': False, 'msg': "wrong response: %s" % str(data)}) + + +def GetJson(data): + """ + 将对象转换为JSON + @data 被转换的对象(dict/list/str/int...) + """ + if data == bytes: data = data.decode('utf-8') + ijson_obj = ijson() + data = ijson_obj.dumps(data) + del (ijson_obj) + return data + + +def getJson(data): + return GetJson(data) + + +def gettext_msg(msg, args=()): + try: + msg = _(msg).format(*args) + except: + pass + finally: + return msg + + +def write_log_gettext(type, logmsg, args=(), not_web=False): + # 写日志 + logmsg = gettext_msg(logmsg, args) + try: + import time, db, json + username = 'system' + uid = 1 + tmp_msg = '' + if not not_web: + try: + from BTPanel import session + if 'username' in session: + username = session['username'] + uid = session['uid'] + if session.get('debug') == 1: return + except: + pass + sql = db.Sql() + mDate = time.strftime('%Y-%m-%d %X', time.localtime()) + data = (uid, username, _(type), xssencode2(logmsg + tmp_msg), mDate) + result = sql.table('logs').add('uid,username,type,log,addtime', data) + except: + pass + + +def WriteLog(type, logMsg, args=(), not_web=False): + # 写日志 + try: + import time, db, json + username = 'system' + uid = 1 + tmp_msg = '' + if not not_web: + try: + from BTPanel import session + if 'username' in session: + username = session['username'] + uid = session['uid'] + if session.get('debug') == 1: return + except: + pass + global _LAN_LOG + if not _LAN_LOG: + _LAN_LOG = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/log.json')) + keys = _LAN_LOG.keys() + if logMsg in keys: + logMsg = _LAN_LOG[logMsg] + for i in range(len(args)): + rep = '{' + str(i + 1) + '}' + logMsg = logMsg.replace(rep, args[i]) + if type in keys: type = _LAN_LOG[type] + + try: + if 'login_address' in session: + logMsg = '{} {}'.format(session['login_address'], logMsg) + except: + pass + + sql = db.Sql() + mDate = time.strftime('%Y-%m-%d %X', time.localtime()) + data = (uid, username, type, logMsg + tmp_msg, mDate) + result = sql.table('logs').add('uid,username,type,log,addtime', data) + return result + except: + return None + + +def GetLanguage(): + ''' + 取语言 + ''' + return GetConfigValue("language") + + +def get_language(): + return GetLanguage() + + +def GetConfigValue(key): + ''' + 取配置值 + ''' + config = GetConfig() + if not key in config.keys(): + if key == 'download': return 'http://node.aapanel.com' + return None + return config[key] + + +def SetConfigValue(key, value): + config = GetConfig() + config[key] = value + WriteConfig(config) + + +def GetConfig(): + ''' + 取所有配置项 + ''' + path = "config/config.json" + if not os.path.exists(path): return {} + f_body = ReadFile(path) + if not f_body: return {} + return json.loads(f_body) + + +def WriteConfig(config): + path = "config/config.json" + WriteFile(path, json.dumps(config)) + + +def GetLan(key): + """ + 取提示消息 + """ + global _LAN_TEMPLATE + if not _LAN_TEMPLATE: + _LAN_TEMPLATE = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/template.json')) + keys = _LAN_TEMPLATE.keys() + msg = None + if key in keys: + msg = _LAN_TEMPLATE[key] + return msg + + +def getLan(key): + return GetLan(key) + + +def GetMsg(key, args=()): + try: + global _LAN_PUBLIC + if not _LAN_PUBLIC: + _LAN_PUBLIC = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/public.json')) + keys = _LAN_PUBLIC.keys() + msg = None + if key in keys: + msg = _LAN_PUBLIC[key] + for i in range(len(args)): + rep = '{' + str(i + 1) + '}' + msg = msg.replace(rep, args[i]) + return msg + except: + return key + + +def get_msg_gettext(msg, args=()): + return gettext_msg(msg, args) + + +def getMsg(key, args=()): + return GetMsg(key, args) + + +# 获取Web服务器 +def GetWebServer(): + if os.path.exists('{}/apache/bin/apachectl'.format(get_setup_path())): + webserver = 'apache' + elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): + webserver = 'openlitespeed' + else: + webserver = 'nginx' + return webserver + + +def get_webserver(): + return GetWebServer() + + +def ServiceReload(): + # 重载Web服务配置 + if os.path.exists('{}/nginx/sbin/nginx'.format(get_setup_path())): + result = ExecShell('/etc/init.d/nginx reload') + if result[1].find('nginx.pid') != -1: + ExecShell('pkill -9 nginx && sleep 1') + ExecShell('/etc/init.d/nginx start') + elif os.path.exists('{}/apache/bin/apachectl'.format(get_setup_path())): + result = ExecShell('/etc/init.d/httpd reload') + else: + result = ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart') + return result + + +def serviceReload(): + return ServiceReload() + + +def get_preexec_fn(run_user): + ''' + @name 获取指定执行用户预处理函数 + @author hwliang<2021-08-19> + @param run_user 运行用户 + @return 预处理函数 + ''' + import pwd + pid = pwd.getpwnam(run_user) + uid = pid.pw_uid + gid = pid.pw_gid + + def _exec_rn(): + os.setgid(gid) + os.setuid(uid) + + return _exec_rn + + +def ExecShell(cmdstring, timeout=None, shell=True, cwd=None, env=None, user=None): + ''' + @name 执行命令 + @author hwliang<2021-08-19> + @param cmdstring 命令 [必传] + @param timeout 超时时间 + @param shell 是否通过shell运行 + @param cwd 进入的目录 + @param env 环境变量 + @param user 执行用户名 + @return 命令执行结果 + ''' + a = '' + e = '' + import subprocess, tempfile + preexec_fn = None + tmp_dir = '/dev/shm' + if user: + preexec_fn = get_preexec_fn(user) + tmp_dir = '/tmp' + try: + rx = md5(cmdstring) + succ_f = tempfile.SpooledTemporaryFile(max_size=4096, mode='wb+', suffix='_succ', prefix='btex_' + rx, + dir=tmp_dir) + err_f = tempfile.SpooledTemporaryFile(max_size=4096, mode='wb+', suffix='_err', prefix='btex_' + rx, + dir=tmp_dir) + sub = subprocess.Popen(cmdstring, close_fds=True, shell=shell, bufsize=128, stdout=succ_f, stderr=err_f, + cwd=cwd, env=env, preexec_fn=preexec_fn) + if timeout: + s = 0 + d = 0.01 + while sub.poll() == None: + time.sleep(d) + s += d + if s >= timeout: + if not err_f.closed: err_f.close() + if not succ_f.closed: succ_f.close() + return 'Timed out' + else: + sub.wait() + + err_f.seek(0) + succ_f.seek(0) + a = succ_f.read() + e = err_f.read() + if not err_f.closed: err_f.close() + if not succ_f.closed: succ_f.close() + except: + return '', get_error_info() + try: + # 编码修正 + if type(a) == bytes: a = a.decode('utf-8') + if type(e) == bytes: e = e.decode('utf-8') + except: + a = str(a) + e = str(e) + + return a, e + + +def GetLocalIp(): + # 取本地外网IP + try: + filename = 'data/iplist.txt' + ipaddress = readFile(filename) + if not ipaddress: + url = 'https://ifconfig.me/ip' + m_str = HttpGet(url) + if isinstance(m_str, bytes): + ipaddress = match_ipv4.match(m_str.decode('utf-8')).group(0) + else: + ipaddress = match_ipv4.match(m_str).group(0) + WriteFile(filename, ipaddress) + c_ip = check_ip(ipaddress) + if not c_ip: return GetHost() + return ipaddress + except Exception as e: + try: + url = 'https://www.aapanel.com/api/common/getClientIP' + ipaddress = HttpGet(url) + WriteFile(filename, ipaddress) + return ipaddress + except: + return GetHost() + + +def is_ipv4(ip): + ''' + @name 是否是IPV4地址 + @author hwliang + @param ip IP地址 + @return True/False + ''' + # 验证基本格式 + if not match_ipv4.match(ip): + return False + + # 验证每个段是否在合理范围 + try: + socket.inet_pton(socket.AF_INET, ip) + except AttributeError: + try: + socket.inet_aton(ip) + except socket.error: + return False + except socket.error: + return False + return True + + +def is_ipv6(ip): + ''' + @name 是否为IPv6地址 + @author hwliang + @param ip 地址 + @return True/False + ''' + # 验证基本格式 + if not match_ipv6.match(ip): + return False + + # 验证IPv6地址 + try: + socket.inet_pton(socket.AF_INET6, ip) + except socket.error: + return False + return True + + +def check_ip(ip): + return is_ipv4(ip) or is_ipv6(ip) + + +def GetHost(port=False): + from flask import request + host_tmp = request.headers.get('host') + + # 验证基本格式 + if host_tmp: + if not match_based_host.match(host_tmp): + host_tmp = '' + + if not host_tmp: + if request.url_root: + tmp = find_url_root.findall(request.url_root) + if tmp: host_tmp = tmp[0][1] + if not host_tmp: + host_tmp = '127.0.0.1:' + readFile('data/port.pl').strip() + try: + if host_tmp.find(':') == -1: host_tmp += ':80' + except: + host_tmp = "127.0.0.1:7800" + h = host_tmp.split(':') + if port: return h[-1] + return ':'.join(h[0:-1]) + + +def GetClientIp(): + from flask import request + ipaddr = request.remote_addr.replace('::ffff:', '') + if not check_ip(ipaddr): return 'Unknown IP address' + return ipaddr + + +def get_remote_port(): + ''' + @name 获取客户端端口号 + @return int + ''' + from flask import request + port = request.headers.get('X-Real-Port', '0') + if port == '0': port = request.environ.get('REMOTE_PORT') + return str(port) + + +def get_client_ip(): + return GetClientIp() + + +def phpReload(version): + # 重载PHP配置 + import os + if os.path.exists(get_setup_path() + '/php/' + version + '/libphp5.so'): + ExecShell('/etc/init.d/httpd reload') + else: + ExecShell('/etc/init.d/php-fpm-' + version + ' reload') + ExecShell("/etc/init.d/php-fpm-{} start".format(version)) + + +def get_timeout(url, timeout=3): + try: + start = time.time() + result = int(httpGet(url, timeout)) + return result, int((time.time() - start) * 1000 - 500) + except: + return 0, False + + +def get_url(timeout=0.5): + return 'https://node.aapanel.com' + + import json + try: + pkey = 'node_url' + node_url = cache_get(pkey) + if node_url: return node_url + nodeFile = 'data/node.json' + node_list = json.loads(readFile(nodeFile)) + mnode1 = [] + mnode2 = [] + mnode3 = [] + new_node_list = {} + for node in node_list: + node['net'], node['ping'] = get_timeout( + node['protocol'] + node['address'] + ':' + node['port'] + '/net_test', 1) + new_node_list[node['address']] = node['ping'] + if not node['ping']: continue + if node['ping'] < 100: # 当响应时间<100ms且可用带宽大于1500KB时 + if node['net'] > 1500: + mnode1.append(node) + elif node['net'] > 1000: + mnode3.append(node) + else: + if node['net'] > 1000: # 当响应时间>=100ms且可用带宽大于1000KB时 + mnode2.append(node) + if node['ping'] < 100: + if node['net'] > 3000: break # 有节点可用带宽大于3000时,不再检查其它节点 + if mnode1: # 优选低延迟高带宽 + mnode = sorted(mnode1, key=lambda x: x['net'], reverse=True) + elif mnode3: # 备选低延迟,中等带宽 + mnode = sorted(mnode3, key=lambda x: x['net'], reverse=True) + else: # 终选中等延迟,中等带宽 + mnode = sorted(mnode2, key=lambda x: x['ping'], reverse=False) + + if not mnode: return 'https://node.aapanel.com' + + new_node_keys = new_node_list.keys() + for i in range(len(node_list)): + if node_list[i]['address'] in new_node_keys: + node_list[i]['ping'] = new_node_list[node_list[i]['address']] + else: + node_list[i]['ping'] = 500 + + new_node_list = sorted(node_list, key=lambda x: x['ping'], reverse=False) + writeFile(nodeFile, json.dumps(new_node_list)) + node_url = mnode[0]['protocol'] + mnode[0]['address'] + ':' + mnode[0]['port'] + cache_set(pkey, node_url, 86400) + return node_url + except: + return 'https://node.aapanel.com' + + +# 过滤输入 +def checkInput(data): + if not data: return data + if type(data) != str: return data + checkList = [ + {'d': '<', 'r': '<'}, + {'d': '>', 'r': '>'}, + {'d': '\'', 'r': '‘'}, + {'d': '"', 'r': '“'}, + {'d': '&', 'r': '&'}, + {'d': '#', 'r': '#'}, + {'d': '<', 'r': '<'} + ] + for v in checkList: + data = data.replace(v['d'], v['r']) + return data + + +# 取文件指定尾行数 +def GetNumLines(path, num: int, p=1): + if not os.path.exists(path): return "" + if not is_number(num): + return "" + + pyVersion = sys.version_info[0] + max_len = 1024 * 1024 * 10 + try: + + start_line = (p - 1) * num + count = start_line + num + fp = open(path, 'rb') + buf = "" + fp.seek(-1, 2) + if fp.read(1) == "\n": fp.seek(-1, 2) + data = [] + total_len = 0 + b = True + n = 0 + for i in range(count): + while True: + newline_pos = str.rfind(str(buf), "\n") + pos = fp.tell() + if newline_pos != -1: + if n >= start_line: + line = buf[newline_pos + 1:] + line_len = len(line) + total_len += line_len + sp_len = total_len - max_len + if sp_len > 0: + line = line[sp_len:] + try: + data.insert(0, line) + except: + pass + buf = buf[:newline_pos] + n += 1 + break + else: + if pos == 0: + b = False + break + to_read = min(4096, pos) + fp.seek(-to_read, 1) + t_buf = fp.read(to_read) + if pyVersion == 3: + t_buf = t_buf.decode('utf-8', errors='ignore') + + buf = t_buf + buf + fp.seek(-to_read, 1) + if pos - to_read == 0: + buf = "\n" + buf + if total_len >= max_len: break + if not b: break + fp.close() + result = "\n".join(data) + except: + if re.match(r"[`\$\&\;]+", path): return "" + result = ExecShell("tail -n {} {}".format(num, path))[0] + if len(result) > max_len: + result = result[-max_len:] + + try: + try: + result = json.dumps(result) + return json.loads(result).strip() + except: + if pyVersion == 2: + result = result.decode('utf8', errors='ignore') + else: + result = result.encode('utf-8', errors='ignore').decode("utf-8", errors="ignore") + return result.strip() + except: + return "" + + +# 验证证书 +def CheckCert(certPath='ssl/certificate.pem'): + try: + return get_cert_data(certPath) + except: + openssl = '/usr/local/openssl/bin/openssl' + if not os.path.exists(openssl): openssl = 'openssl' + certPem = readFile(certPath) + s = "\n-----BEGIN CERTIFICATE-----" + tmp = certPem.strip().split(s) + res = True + for tmp1 in tmp: + if tmp1.find('-----BEGIN CERTIFICATE-----') == -1: tmp1 = s + tmp1 + writeFile(certPath, tmp1) + result = ExecShell(openssl + " x509 -in " + certPath + " -noout -subject") + if result[1].find('-bash:') != -1: res = True + if len(result[1]) > 2: res = False + if result[0].find('error:') != -1: res = False + return res + + +# 获取面板地址 +def getPanelAddr(): + from flask import request + protocol = 'https://' if os.path.exists("data/ssl.pl") else 'http://' + return protocol + request.headers.get('host') + + +# 字节单位转换 +def to_size(size): + if not size: return '0.00 b' + size = float(size) + d = ('b', 'KB', 'MB', 'GB', 'TB') + s = d[0] + for b in d: + if size < 1024: return ("%.2f" % size) + ' ' + b + size = size / 1024 + s = b + return ("%.2f" % size) + ' ' + b + + +def checkCode(code, outime=120): + # 校验验证码 + from BTPanel import session, cache + try: + codeStr = cache.get('codeStr') + cache.delete('codeStr') + if not codeStr: + session['login_error'] = GetMsg('CODE_TIMEOUT') + return False + + if md5(code.lower()) != codeStr: + session['login_error'] = GetMsg('CODE_ERR') + return False + return True + except: + session['login_error'] = GetMsg('CODE_NOT_EXISTS') + return False + + +# 写进度 +def writeSpeed(title, used, total, speed=0): + import json + if not title: + data = {'title': None, 'progress': 0, 'total': 0, 'used': 0, 'speed': 0} + else: + try: + progress = int((100.0 * used / total)) + except: + progress = 0 + data = {'title': title, 'progress': progress, 'total': total, 'used': used, 'speed': speed} + writeFile('/tmp/panelSpeed.pl', json.dumps(data)) + return True + + +# 取进度 +def getSpeed(): + import json + data = readFile('/tmp/panelSpeed.pl') + if not data: + data = json.dumps({'title': None, 'progress': 0, 'total': 0, 'used': 0, 'speed': 0}) + writeFile('/tmp/panelSpeed.pl', data) + return json.loads(data) + + +def get_requests_headers(): + return {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "BT-Panel"} + + +def downloadFile(url, filename): + try: + if sys.version_info[0] == 2: + import requests + headers = { + 'User-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36'} + r = requests.get(url, headers=headers, verify=False) + with open(filename, "wb") as f: + f.write(r.content) + else: + import urllib.request + import ssl + ssl._create_default_https_context = ssl._create_unverified_context + opener = urllib.request.build_opener() + opener.addheaders = [('User-agent', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36')] + urllib.request.install_opener(opener) + urllib.request.urlretrieve(url, filename=filename) + except: + ExecShell("wget -O --no-check-certificate {} {}".format(filename, url)) + + +def exists_args(args, get): + ''' + @name 检查参数是否存在 + @author hwliang<2021-06-08> + @param args 参数列表 允许是列表或字符串 + @param get 参数对像 + @return bool 都存在返回True,否则抛出KeyError异常 + ''' + if type(args) == str: + args = args.split(',') + for arg in args: + if not arg in get: + raise KeyError('Required parameters are missing:{}'.format(arg)) + return True + + +def get_error_info(): + import traceback + errorMsg = traceback.format_exc() + return errorMsg + + +def get_plugin_replace_rules(): + ''' + @name 获取插件文件内容替换规则 + @author hwliang<2021-06-28> + @return list + ''' + return [ + { + "find": "[PATH]", + "replace": "[PATH]" + } + ] + + +def get_plugin_title(plugin_name): + ''' + @name 获取插件标题 + @author hwliang<2021-06-24> + @param plugin_name 插件名称 + @return string + ''' + + info_file = '{}/{}/info.json'.format(get_plugin_path(), plugin_name) + try: + return json.loads(readFile(info_file))['title'] + except: + return plugin_name + + +def get_error_object(plugin_title=None, plugin_name=None): + ''' + @name 获取格式化错误响应对像 + @author hwliang<2021-06-21> + @return Resp + ''' + if not plugin_title: plugin_title = get_plugin_title(plugin_name) + try: + from BTPanel import request, Resp + is_cli = False + except: + is_cli = True + + if is_cli: + raise get_error_info() + ss = '''404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +During handling of the above exception, another exception occurred:''' + error_info = get_error_info().strip().split(ss)[-1].strip() + request_info = '''REQUEST_DATE: {request_date} + PAN_VERSION: {panel_version} + OS_VERSION: {os_version} + REMOTE_ADDR: {remote_addr} + REQUEST_URI: {method} {full_path} +REQUEST_FORM: {request_form} + USER_AGENT: {user_agent}'''.format( + request_date=getDate(), + remote_addr=GetClientIp(), + method=request.method, + full_path=url_encode(xsssec(request.full_path)), + request_form=xsssec(str(request.form.to_dict())), + user_agent=xsssec(request.headers.get('User-Agent')), + panel_version=version(), + os_version=get_os_version() + ) + + result = readFile('{}/BTPanel/templates/default/plugin_error.html'.format(get_panel_path())).format( + plugin_name=plugin_title, + request_info=request_info, + error_title=error_info.split("\n")[-1], + error_msg=error_info + ) + return Resp(result, 500) + + +# 搜索数据中是否存在 +def inArray(arrays, searchStr): + for key in arrays: + if key == searchStr: return True + + return False + + +# 格式化指定时间戳 +def format_date(format="%Y-%m-%d %H:%M:%S", times=None): + if not times: times = int(time.time()) + time_local = time.localtime(times) + return time.strftime(format, time_local) + + +# # 检查Web服务器配置文件是否有错误 +# def checkWebConfig(): +# f1 = '{}/'.format(get_vhost_path()) +# f2 = '{}/'.format(get_plugin_path()) +# setup_path = get_setup_path() +# if not os.path.exists(f2 + 'btwaf'): +# f3 = f1 + 'nginx/btwaf.conf' +# if os.path.exists(f3): os.remove(f3) +# # if not os.path.exists(f2 + 'btwaf_httpd'): +# # f3 = f1 + 'apache/btwaf.conf' +# # if os.path.exists(f3): os.remove(f3) +# +# if not os.path.exists(f2 + 'total'): +# f3 = f1 + 'apache/total.conf' +# if os.path.exists(f3): os.remove(f3) +# f3 = f1 + 'nginx/total.conf' +# if os.path.exists(f3): os.remove(f3) +# else: +# if os.path.exists(setup_path + '/apache/modules/mod_lua.so'): +# writeFile(f1 + 'apache/btwaf.conf', 'LoadModule lua_module modules/mod_lua.so') +# writeFile(f1 + 'apache/total.conf', 'LuaHookLog {}/total/httpd_log.lua run_logs'.format(setup_path)) +# else: +# f3 = f1 + 'apache/total.conf' +# if os.path.exists(f3): os.remove(f3) +# +# if get_webserver() == 'nginx': +# result = ExecShell( +# "ulimit -n 8192 ; {setup_path}/nginx/sbin/nginx -t -c {setup_path}/nginx/conf/nginx.conf".format( +# setup_path=setup_path)) +# searchStr = 'successful' +# elif get_webserver() == 'apache': +# # else: +# result = ExecShell("ulimit -n 8192 ; {setup_path}/apache/bin/apachectl -t".format(setup_path=setup_path)) +# searchStr = 'Syntax OK' +# else: +# result = ["1", "1"] +# searchStr = "1" +# if result[1].find(searchStr) == -1: +# WriteLog("TYPE_SOFT", 'CONF_CHECK_ERR', (result[1],)) +# return result[1] +# return True + + +# 获取nginx版本,没有获取到版本时返回None,获取到时,返回一个3位长度的列表,如[1, 25, 1], 表示1.25.1版本 +def nginx_version(): + out, _ = ExecShell("/www/server/nginx/sbin/nginx -V 2>&1 | grep 'version'") + out: str = out.strip() + if not out: + return None + + rep_ver = re.compile(r"nginx\s+version.*/(?P\d+\.\d+(\.\d+)*)") + res = rep_ver.search(out) + if not res: + return None + ver = res.group("ver") + ver_list = [int(i) for i in ver.split(".")] + if len(ver_list) < 3: + ver_list.extend([0] * (3 - len(ver_list))) + if len(ver_list) > 3: + ver_list = ver_list[:3] + return ver_list + + +def is_change_nginx_http2() -> bool: + nginx_ver = nginx_version() + if not nginx_ver: + return False + + if nginx_ver >= [1, 25, 1]: + return True + + return False + + +def is_change_nginx_old_http2() -> bool: + nginx_ver = nginx_version() + if not nginx_ver: + return False + if nginx_ver < [1, 25, 1]: + return True + + return False + + +def is_nginx_http3(): + return ExecShell("nginx -V 2>&1| grep 'http_v3_module'")[0].strip() != '' + + +def remove_nginx_quic(): + nginx_file_path = "/www/server/panel/vhost/nginx" + for i in os.listdir(nginx_file_path): + if not i.endswith(".conf"): + continue + nginx_file = os.path.join(nginx_file_path, i) + remove_nginx_server_quic(nginx_file) + + +def remove_nginx_server_quic(nginx_file: str): + if not os.path.isfile(nginx_file): + return + data = ReadFile(nginx_file) + if not isinstance(data, str): + return + + rep_listen_quic = re.compile(r"\s*listen\s+.*quic;", re.M) + if not rep_listen_quic.search(data): + return + + new_conf = rep_listen_quic.sub('', data) + writeFile(nginx_file, new_conf) + + +def change_nginx_http2(): + import os + nginx_file_path = "/www/server/panel/vhost/nginx" + for i in os.listdir(nginx_file_path): + if not i.endswith(".conf"): + continue + nginx_file = os.path.join(nginx_file_path, i) + change_nginx_server_http2(nginx_file) + + +def change_nginx_server_http2(nginx_file: str): + if not os.path.isfile(nginx_file): + return + data = ReadFile(nginx_file) + rep_listen = re.compile(r"\s*listen\s+[\[\]:]*([0-9]+).*;[^\n]*\n", re.M) + + conf_list = [] + start_idx, last_listen_idx = 0, -1 + for tmp in rep_listen.finditer(data): + listen_str = tmp.group() + if "http2" in listen_str: + listen_str = listen_str.replace("http2", "") + last_listen_idx = len(conf_list) + 2 + + conf_list.append(data[start_idx:tmp.start()]) + conf_list.append(listen_str) + start_idx = tmp.end() + + conf_list.append(data[start_idx:]) + if last_listen_idx > 0: + conf_list.insert(last_listen_idx, " http2 on;\n") + + new_conf = "".join(conf_list) + writeFile(nginx_file, new_conf) + + +def is_change_nginx_old_http2() -> bool: + nginx_ver = nginx_version() + if not nginx_ver: + return False + if nginx_ver < [1, 25, 1]: + return True + + return False + + +def change_nginx_old_http2(): + nginx_file_path = "/www/server/panel/vhost/nginx" + for i in os.listdir(nginx_file_path): + if not i.endswith(".conf"): + continue + nginx_file = os.path.join(nginx_file_path, i) + change_nginx_server_old_http2(nginx_file) + + +def read_file_lines_range(filename, start_line: int, end_line: int): + """ + 读取文件指定行数范围内容 + + Args: + filename: 文件名 + start_line: 开始行号 + end_line: 结束行号 + + Returns: + list: 指定行数范围内容列表 + """ + try: + with open(filename, 'r') as f: + lines = f.readlines() + return "".join(lines[start_line:end_line]) + except: + return "获取文件内容报错了" + + +def change_nginx_server_old_http2(nginx_file: str): + if not os.path.isfile(nginx_file): + return + data = ReadFile(nginx_file) + if not isinstance(data, str): + return + + rep_http2_on = re.compile(r"\s*http2\s+on;[^\n]*\n", re.M) + if not rep_http2_on.search(data): + return + else: + data = rep_http2_on.sub("\n", data) + + rep_listen = re.compile(r"\s*listen\s+[\[\]:]*443.*;[^\n]*\n", re.M) + conf_list = [] + start_idx = 0 + for tmp in rep_listen.finditer(data): + listen_str = tmp.group() + conf_list.append(data[start_idx:tmp.start()]) + conf_list.append(listen_str.replace(";", " http2;")) + start_idx = tmp.end() + + conf_list.append(data[start_idx:]) + new_conf = "".join(conf_list) + writeFile(nginx_file, new_conf) + + +# 错误收集适配 +# 检查Web服务器配置文件是否有错误 +def checkWebConfig(repair_num=2): + f1 = '{}/'.format(get_vhost_path()) + f2 = '{}/'.format(get_plugin_path()) + setup_path = get_setup_path() + if not os.path.exists(f2 + 'btwaf'): + f3 = f1 + 'nginx/btwaf.conf' + if os.path.exists(f3): os.remove(f3) + # if not os.path.exists(f2 + 'btwaf_httpd'): + # f3 = f1 + 'apache/btwaf.conf' + # if os.path.exists(f3): os.remove(f3) + + if not os.path.exists(f2 + 'total'): + f3 = f1 + 'apache/total.conf' + if os.path.exists(f3): os.remove(f3) + f3 = f1 + 'nginx/total.conf' + if os.path.exists(f3): os.remove(f3) + else: + if os.path.exists(setup_path + '/apache/modules/mod_lua.so'): + writeFile(f1 + 'apache/btwaf.conf', 'LoadModule lua_module modules/mod_lua.so') + writeFile(f1 + 'apache/total.conf', 'LuaHookLog {}/total/httpd_log.lua run_logs'.format(setup_path)) + else: + f3 = f1 + 'apache/total.conf' + if os.path.exists(f3): os.remove(f3) + + web_s = get_webserver() + if web_s == 'nginx': + result = ExecShell( + "ulimit -n 8192 ; {setup_path}/nginx/sbin/nginx -t -c {setup_path}/nginx/conf/nginx.conf".format( + setup_path=setup_path)) + writeFile('/tmp/nginx_new.conf', readFile('/www/server/nginx/conf/nginx.conf')) + print_log('checkWebConfig--result:{}'.format(result)) + searchStr = 'successful' + nginx_version = ExecShell("{}/nginx/sbin/nginx -v".format(setup_path)) + print_log('nginx') + version_info = nginx_version[1] + elif web_s == 'apache': + print_log('apache') + # else: + result = ExecShell("ulimit -n 8192 ; {setup_path}/apache/bin/apachectl -t".format(setup_path=setup_path)) + searchStr = 'Syntax OK' + apache_version = ExecShell("{}/apache/bin/httpd -v".format(setup_path)) + version_info = apache_version[1] + else: + print_log('other') + result = ["1", "1"] + searchStr = "1" + version_info = "Unknow" + print_log('checkWebConfig--result1:{}'.format(result)) + if result[1].find( + 'the "listen ... http2" directive is deprecated, use the "http2" directive instead') != -1 and web_s == "nginx" and is_change_nginx_http2(): + if repair_num > 0: + repair_num -= 1 + change_nginx_http2() + print_log('nginx----1') + return checkWebConfig(repair_num) + + if result[1].find(searchStr) == -1: + if result[1].find( + '[emerg] unknown directive "http2"') != -1 and web_s == "nginx" and is_change_nginx_old_http2(): + if repair_num > 0: + repair_num -= 1 + change_nginx_old_http2() + print_log('nginx----2') + return checkWebConfig(repair_num) + + if result[1].find('[emerg] invalid parameter "quic" in') != -1 and web_s == "nginx" and not is_nginx_http3(): + if repair_num > 0: + repair_num -= 1 + remove_nginx_quic() + print_log('nginx----3') + return checkWebConfig(repair_num) + + WriteLog("TYPE_SOFT", 'CONF_CHECK_ERR', (result[1],)) + try: + match = re.search(r"in (.*):(\d+)", result[1]) + if match: + err_infos = read_file_lines_range(match.group(1), int(match.group(2)) - 5, + int(match.group(2)) + 5) if int( + match.group(2)) >= 5 else read_file_lines_range(match.group(1), 1, int(match.group(2))) + err_collect("{} \n 报错信息: \n {} \n 版本信息:{} \n报错文件路径:{}:{}".format( + err_infos, result[1], version_info, match.group(1), match.group(2)), + 0, result[1].split("\n")[0].strip()) + except Exception as e: + err_collect(result[1], 0, result[1].split("\n")[0].strip()) + print_log('nginx----4') + return result[1] + print_log('nginx----5') + return True + + +def err_collect(error_info, type, error_id): + ''' + @error_info 错误信息 + @type 错误类型 + @error_id 错误ID + ''' + + from flask import redirect, request, Response + _form = request.form.to_dict() + if 'username' in _form: _form['username'] = '******' + if 'password' in _form: _form['password'] = '******' + if 'phone' in _form: _form['phone'] = '******' + + # 错误信息 + error_infos = { + "REQUEST_DATE": getDate(), # 请求时间 + "PANEL_VERSION": version(), # 面板版本 + "OS_VERSION": get_os_version(), # 操作系统版本 + "REMOTE_ADDR": GetClientIp(), # 请求IP + "REQUEST_URI": request.method + request.full_path, # 请求URI + "REQUEST_FORM": xsssec(str(_form)), # 请求表单 + "USER_AGENT": xsssec(request.headers.get('User-Agent')), # 客户端连接信息 + "ERROR_INFO": error_info, # 错误信息 + "PACK_TIME": readFile("/www/server/panel/config/update_time.pl") if os.path.exists( + "/www/server/panel/config/update_time.pl") else getDate(), # 打包时间 + "TYPE": type, + "ERROR_ID": error_id, + } + pkey = Md5(error_infos["ERROR_ID"]) + + # 提交异常报告 + if not cache_get(pkey): + try: + run_thread(httpPost, ("https://geterror.aapanel.com/bt_error/index.php", error_infos)) + cache_set(pkey, 1, 1800) + except Exception as e: + pass # 错误信息 + + +############################### 错误收集适配 ^^上方 + + +# 检查是否为IPv4地址 +def checkIp(ip): + if match_ipv4.match(ip): + return True + else: + return False + + +# 检查端口是否合法 +def checkPort(port): + if not is_number(port): return False + ports = ['21', '25', '443', '8080', '888', '8888', '8443', '7800'] + if port in ports: return False + intport = int(port) + if intport < 1 or intport > 65535: return False + return True + + +# 字符串取中间 +def getStrBetween(startStr, endStr, srcStr): + start = srcStr.find(startStr) + if start == -1: return None + end = srcStr.find(endStr) + if end == -1: return None + return srcStr[start + 1:end] + + +# 取CPU类型 +def getCpuType(): + cpuinfo = open('/proc/cpuinfo', 'r').read() + rep = r"model\s+name\s+:\s+(.+)" + tmp = re.search(rep, cpuinfo, re.I) + cpuType = '' + if tmp: + cpuType = tmp.groups()[0] + else: + cpuinfo = ExecShell('LANG="en_US.UTF-8" && lscpu')[0] + rep = r"Model\s+name:\s+(.+)" + tmp = re.search(rep, cpuinfo, re.I) + if tmp: cpuType = tmp.groups()[0] + return cpuType + + +# 检查是否允许重启 +def IsRestart(): + num = M('tasks').where('status!=?', ('1',)).count() + if num > 0: return False + return True + + +# 加密密码字符 +def hasPwd(password): + import crypt + return crypt.crypt(password, password) + + +def getDate(format='%Y-%m-%d %X'): + # 取格式时间 + return time.strftime(format, time.localtime()) + + +# 处理MySQL配置文件 +def CheckMyCnf(): + import os + confFile = '/etc/my.cnf' + if os.path.exists(confFile): + conf = readFile(confFile) + if conf.find('[mysqld]') != -1: return True + versionFile = get_setup_path() + '/mysql/version.pl' + if not os.path.exists(versionFile): return False + + versions = ['5.1', '5.5', '5.6', '5.7', '8.0', 'AliSQL'] + version = readFile(versionFile) + for key in versions: + if key in version: + version = key + break + + shellStr = ''' +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +# CF='node.aapanel.com' +# HK='download.bt.cn' +# HK2='103.224.251.67' +# US='128.1.164.196' +# sleep 0.5; +# CN_PING=`ping -c 1 -w 1 $CF|grep time=|awk '{print $7}'|sed "s/time=//"` +# HK_PING=`ping -c 1 -w 1 $HK|grep time=|awk '{print $7}'|sed "s/time=//"` +# HK2_PING=`ping -c 1 -w 1 $HK2|grep time=|awk '{print $7}'|sed "s/time=//"` +# US_PING=`ping -c 1 -w 1 $US|grep time=|awk '{print $7}'|sed "s/time=//"` +# +# echo "$HK_PING $HK" > ping.pl +# echo "$HK2_PING $HK2" >> ping.pl +# echo "$US_PING $US" >> ping.pl +# echo "$CF_PING $CF" >> ping.pl +# nodeAddr=`sort -V ping.pl|sed -n '1p'|awk '{print $2}'` +# if [ "$nodeAddr" == "" ];then +# nodeAddr=$CF +# fi + +Download_Url=https://node.aapanel.com + + +MySQL_Opt() +{ + MemTotal=`free -m | grep Mem | awk '{print $2}'` + if [[ ${MemTotal} -gt 1024 && ${MemTotal} -lt 2048 ]]; then + sed -i "s#^key_buffer_size.*#key_buffer_size = 32M#" /etc/my.cnf + sed -i "s#^table_open_cache.*#table_open_cache = 128#" /etc/my.cnf + sed -i "s#^sort_buffer_size.*#sort_buffer_size = 768K#" /etc/my.cnf + sed -i "s#^read_buffer_size.*#read_buffer_size = 768K#" /etc/my.cnf + sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 8M#" /etc/my.cnf + sed -i "s#^thread_cache_size.*#thread_cache_size = 16#" /etc/my.cnf + sed -i "s#^query_cache_size.*#query_cache_size = 16M#" /etc/my.cnf + sed -i "s#^tmp_table_size.*#tmp_table_size = 32M#" /etc/my.cnf + sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 128M#" /etc/my.cnf + sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 32M#" /etc/my.cnf + elif [[ ${MemTotal} -ge 2048 && ${MemTotal} -lt 4096 ]]; then + sed -i "s#^key_buffer_size.*#key_buffer_size = 64M#" /etc/my.cnf + sed -i "s#^table_open_cache.*#table_open_cache = 256#" /etc/my.cnf + sed -i "s#^sort_buffer_size.*#sort_buffer_size = 1M#" /etc/my.cnf + sed -i "s#^read_buffer_size.*#read_buffer_size = 1M#" /etc/my.cnf + sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 16M#" /etc/my.cnf + sed -i "s#^thread_cache_size.*#thread_cache_size = 32#" /etc/my.cnf + sed -i "s#^query_cache_size.*#query_cache_size = 32M#" /etc/my.cnf + sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" /etc/my.cnf + sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 256M#" /etc/my.cnf + sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 64M#" /etc/my.cnf + elif [[ ${MemTotal} -ge 4096 && ${MemTotal} -lt 8192 ]]; then + sed -i "s#^key_buffer_size.*#key_buffer_size = 128M#" /etc/my.cnf + sed -i "s#^table_open_cache.*#table_open_cache = 512#" /etc/my.cnf + sed -i "s#^sort_buffer_size.*#sort_buffer_size = 2M#" /etc/my.cnf + sed -i "s#^read_buffer_size.*#read_buffer_size = 2M#" /etc/my.cnf + sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 32M#" /etc/my.cnf + sed -i "s#^thread_cache_size.*#thread_cache_size = 64#" /etc/my.cnf + sed -i "s#^query_cache_size.*#query_cache_size = 64M#" /etc/my.cnf + sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" /etc/my.cnf + sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 512M#" /etc/my.cnf + sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 128M#" /etc/my.cnf + elif [[ ${MemTotal} -ge 8192 && ${MemTotal} -lt 16384 ]]; then + sed -i "s#^key_buffer_size.*#key_buffer_size = 256M#" /etc/my.cnf + sed -i "s#^table_open_cache.*#table_open_cache = 1024#" /etc/my.cnf + sed -i "s#^sort_buffer_size.*#sort_buffer_size = 4M#" /etc/my.cnf + sed -i "s#^read_buffer_size.*#read_buffer_size = 4M#" /etc/my.cnf + sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 64M#" /etc/my.cnf + sed -i "s#^thread_cache_size.*#thread_cache_size = 128#" /etc/my.cnf + sed -i "s#^query_cache_size.*#query_cache_size = 128M#" /etc/my.cnf + sed -i "s#^tmp_table_size.*#tmp_table_size = 128M#" /etc/my.cnf + sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 1024M#" /etc/my.cnf + sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 256M#" /etc/my.cnf + elif [[ ${MemTotal} -ge 16384 && ${MemTotal} -lt 32768 ]]; then + sed -i "s#^key_buffer_size.*#key_buffer_size = 512M#" /etc/my.cnf + sed -i "s#^table_open_cache.*#table_open_cache = 2048#" /etc/my.cnf + sed -i "s#^sort_buffer_size.*#sort_buffer_size = 8M#" /etc/my.cnf + sed -i "s#^read_buffer_size.*#read_buffer_size = 8M#" /etc/my.cnf + sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 128M#" /etc/my.cnf + sed -i "s#^thread_cache_size.*#thread_cache_size = 256#" /etc/my.cnf + sed -i "s#^query_cache_size.*#query_cache_size = 256M#" /etc/my.cnf + sed -i "s#^tmp_table_size.*#tmp_table_size = 256M#" /etc/my.cnf + sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 2048M#" /etc/my.cnf + sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 512M#" /etc/my.cnf + elif [[ ${MemTotal} -ge 32768 ]]; then + sed -i "s#^key_buffer_size.*#key_buffer_size = 1024M#" /etc/my.cnf + sed -i "s#^table_open_cache.*#table_open_cache = 4096#" /etc/my.cnf + sed -i "s#^sort_buffer_size.*#sort_buffer_size = 16M#" /etc/my.cnf + sed -i "s#^read_buffer_size.*#read_buffer_size = 16M#" /etc/my.cnf + sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 256M#" /etc/my.cnf + sed -i "s#^thread_cache_size.*#thread_cache_size = 512#" /etc/my.cnf + sed -i "s#^query_cache_size.*#query_cache_size = 512M#" /etc/my.cnf + sed -i "s#^tmp_table_size.*#tmp_table_size = 512M#" /etc/my.cnf + sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 4096M#" /etc/my.cnf + sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 1024M#" /etc/my.cnf + fi +} + +wget -O /etc/my.cnf $Download_Url/install/conf/mysql-%s.conf -T 5 +chmod 644 /etc/my.cnf +MySQL_Opt +''' % (version,) + ExecShell(shellStr) + # 判断是否迁移目录 + if os.path.exists('data/datadir.pl'): + newPath = readFile('data/datadir.pl') + if os.path.exists(newPath): + mycnf = readFile('/etc/my.cnf') + mycnf = mycnf.replace('/www/server/data', newPath) + writeFile('/etc/my.cnf', mycnf) + WriteLog('TYPE_SOFE', 'MYSQL_CHECK_ERR') + return True + + +def GetSSHPort(): + try: + file = '/etc/ssh/sshd_config' + conf = ReadFile(file) + rep = r"#*Port\s+([0-9]+)\s*\n" + port = re.search(rep, conf).groups(0)[0] + return int(port) + except: + return 22 + + +def get_sshd_port(): + ''' + @name 获取sshd端口 + @author hwliang + @return int + ''' + # 先尝试从进程中获取当前实际的监听端口 + sshd_port = 22 + is_ok = 0 + pid = get_sshd_pid_of_pidfile() + if not pid: pid = get_sshd_pid_of_binfile() + if pid: + try: + import psutil + p = psutil.Process(pid) + for conn in p.connections(): + if conn.status == 'LISTEN': + sshd_port = conn.laddr[1] + is_ok = 1 + break + except: + pass + + # 如果从进程获取失败,则尝试从配置文件获取 + if not is_ok: sshd_port = GetSSHPort() + + return sshd_port + + +def get_sshd_pid_of_pidfile(): + ''' + @name 通过PID文件获取SSH状态 + @author hwliang + @return int 0:关闭 pid:开启 + ''' + sshd_pid_list = ['/run/sshd.pid', '/var/run/sshd.pid', '/run/ssh.pid', '/var/run/ssh.pid'] + sshd_pid_file = None + for spid_file in sshd_pid_list: + if os.path.exists(spid_file): + sshd_pid_file = spid_file + break + + if sshd_pid_file: + sshd_pid = readFile(sshd_pid_file) + if not sshd_pid: return 0 + try: + sshd_pid = int(sshd_pid) + if not sshd_pid: return 0 + if pid_exists(sshd_pid): + return sshd_pid + except: + pass + return 0 + + +def get_sshd_pid_of_binfile(): + ''' + @name 通过执行文件获取SSH状态 + @author hwliang + @return int 进程pid + ''' + sshd_bin_list = ['/usr/sbin/sshd', '/usr/bin/sshd', '/usr/sbin/ssh', '/usr/bin/ssh'] + sshd_bin = None + pid = 0 + for sbin in sshd_bin_list: + if os.path.exists(sbin): + sshd_bin = sbin + break + + if sshd_bin: + pid = get_process_pid(sshd_bin.split('/')[-1], sshd_bin, '-D') + + return pid + + +def GetSSHStatus(): + ''' + @name 获取SSH状态 + @author hwliang + @return bool + ''' + if get_sshd_pid_of_pidfile(): + return True + elif get_sshd_pid_of_binfile(): + return True + return False + + +def get_sshd_status(): + ''' + @name 获取SSH状态 + @author hwliang + @return bool + ''' + return GetSSHStatus() + + +# 检查端口是否合法 +def CheckPort(port, other=None): + if type(port) == str: port = int(port) + if port < 1 or port > 65535: return False + if other: + checks = [22, 20, 21, 8888, 3306, 11211, 888, 25, 7800] + if port in checks: return False + return True + + +# 获取Token +def GetToken(): + try: + from json import loads + tokenFile = 'data/token.json' + if not os.path.exists(tokenFile): return False + token = loads(readFile(tokenFile)) + return token + except: + return False + + +def to_btint(string): + m_list = [] + for s in string: + m_list.append(ord(s)) + return m_list + + +def load_module(pluginCode): + from imp import new_module + from BTPanel import cache + p_tk = 'data/%s' % md5(pluginCode + get_uuid()) + pluginInfo = None + skey = md5(pluginCode + 'code') + if cache: pluginInfo = cache.get(skey) + if not pluginInfo: + import panelAuth + pdata = panelAuth.panelAuth().create_serverid(None) + pdata['pid'] = pluginCode + url = GetConfigValue('home') + '/api/panel/get_py_module' + pluginTmp = httpPost(url, pdata) + try: + pluginInfo = json.loads(pluginTmp) + except: + if not os.path.exists(p_tk): return False + pluginInfo = json.loads(ReadFile(p_tk)) + if pluginInfo['status'] == False: return False + WriteFile(p_tk, json.dumps(pluginInfo)) + os.chmod(p_tk, 384) + if cache: cache.set(skey, pluginInfo, 1800) + + mod = sys.modules.setdefault(pluginCode, new_module(pluginCode)) + code = compile(pluginInfo['msg'].encode('utf-8'), pluginCode, 'exec') + mod.__file__ = pluginCode + mod.__package__ = '' + exec(code, mod.__dict__) + return mod + + +# 解密数据 +def auth_decode(data): + token = GetToken() + # 是否有生成Token + if not token: return returnMsg(False, 'REQUEST_ERR') + + # 校验access_key是否正确 + if token['access_key'] != data['btauth_key']: return returnMsg(False, 'REQUEST_ERR') + + # 解码数据 + import binascii, hashlib, urllib, hmac, json + tdata = binascii.unhexlify(data['data']) + + # 校验signature是否正确 + signature = binascii.hexlify(hmac.new(token['secret_key'], tdata, digestmod=hashlib.sha256).digest()) + if signature != data['signature']: return returnMsg(False, 'REQUEST_ERR') + + # 返回 + return json.loads(urllib.unquote(tdata)) + + +# 数据加密 +def auth_encode(data): + token = GetToken() + pdata = {} + + # 是否有生成Token + if not token: return returnMsg(False, 'REQUEST_ERR') + + # 生成signature + import binascii, hashlib, urllib, hmac, json + tdata = urllib.quote(json.dumps(data)) + # 公式 hex(hmac_sha256(data)) + pdata['signature'] = binascii.hexlify(hmac.new(token['secret_key'], tdata, digestmod=hashlib.sha256).digest()) + + # 加密数据 + pdata['btauth_key'] = token['access_key'] + pdata['data'] = binascii.hexlify(tdata) + pdata['timestamp'] = time.time() + + # 返回 + return pdata + + +# 检查Token +def checkToken(get): + tempFile = 'data/tempToken.json' + if not os.path.exists(tempFile): return False + import json, time + tempToken = json.loads(readFile(tempFile)) + if time.time() > tempToken['timeout']: return False + if get.token != tempToken['token']: return False + return True + + +# 获取识别码 +def get_uuid(): + import uuid + return uuid.UUID(int=uuid.getnode()).hex[-12:] + + +# 取计算机名 +def get_hostname(): + import socket + return socket.gethostname() + + +# 取mysql datadir +def get_datadir(): + mycnf_file = '/etc/my.cnf' + if not os.path.exists(mycnf_file): return '' + mycnf = readFile(mycnf_file) + import re + tmp = re.findall(r"datadir\s*=\s*(.+)", mycnf) + if not tmp: return '' + return tmp[0] + + +# 进程是否存在 +def process_exists(pname, exe=None, cmdline=None): + try: + import psutil + pids = psutil.pids() + for pid in pids: + try: + p = psutil.Process(pid) + if p.name() == pname: + if not exe and not cmdline: + return True + else: + if exe: + if p.exe() == exe: + return True + if cmdline: + if cmdline in p.cmdline(): return True + except: + pass + + return False + except: + return True + + +def get_process_pid(pname, exe=None, cmdline=None): + ''' + @name 通过进程名获取进程PID + @author hwliang + @param pname 进程名 + @param exe 进程路径 + @param cmdline 进程任意命令行参数 + @return int 返回进程PID + ''' + import psutil + pids = psutil.pids() + for pid in pids: + try: + p = psutil.Process(pid) + if p.name() == pname: + if not exe and not cmdline: + return pid + else: + if exe: + if p.exe() == exe: + if not cmdline: + return pid + return 0 + if cmdline: + if cmdline in p.cmdline(): return pid + except: + pass + return 0 + + +# pid是否存在 +def pid_exists(pid): + if os.path.exists('/proc/{}/exe'.format(pid)): + return True + return False + + +# 重启面板 +def restart_panel(): + import system + return system.system().ReWeb(None) + + +# 获取mac +def get_mac_address(): + import uuid + mac = uuid.UUID(int=uuid.getnode()).hex[-12:] + return ":".join([mac[e:e + 2] for e in range(0, 11, 2)]) + + +# 转码 +def to_string(lites): + if type(lites) != list: lites = [lites] + m_str = '' + for mu in lites: + if sys.version_info[0] == 2: + m_str += unichr(mu).encode('utf-8') + else: + m_str += chr(mu) + return m_str + + +# 解码 +def to_ord(string): + o = [] + for s in string: + o.append(ord(s)) + return o + + +# xss 防御 +def xssencode(text): + try: + from cgi import html + list = ['`', '~', '&', '#', '/', '*', '$', '@', '<', '>', '\"', '\'', ';', '%', ',', '.', '\\u'] + ret = [] + for i in text: + if i in list: + i = '' + ret.append(i) + str_convert = ''.join(ret) + text2 = html.escape(str_convert, quote=True) + return text2 + except: + return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') + + +def html_decode(text): + ''' + @name HTML解码 + @author hwliang + @param text 要解码的HTML + @return string 返回解码后的HTML + ''' + try: + from cgi import html + text2 = html.unescape(text) + return text2 + except: + return text + + +def html_encode(text): + ''' + @name HTML编码 + @author hwliang + @param text 要编码的HTML + @return string 返回编码后的HTML + ''' + try: + from cgi import html + text2 = html.escape(text) + return text2 + except: + return text + + +# xss 防御 +def xsssec(text): + return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') + + +# xss 防御 +def xsssec2(text): + return text.replace('<', '<').replace('>', '>') + + +# xss version +def xss_version(text): + try: + if not text or not isinstance(text, str): return text + text = text.strip() + list = ['`', '~', '&', '#', '/', '*', '$', '@', '<', '>', '\"', '\'', ';', '%', ',', '\\u'] + ret = [] + for i in text: + if i in list: + i = '' + ret.append(i) + str_convert = ''.join(ret) + return str_convert + except: + return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') + + +# 获取数据库配置信息 +def get_mysql_info(): + data = {} + try: + CheckMyCnf() + myfile = '/etc/my.cnf' + mycnf = readFile(myfile) + rep = r"datadir\s*=\s*(.+)\n" + data['datadir'] = re.search(rep, mycnf).groups()[0] + rep = r"port\s*=\s*([0-9]+)\s*\n" + data['port'] = re.search(rep, mycnf).groups()[0] + except: + data['datadir'] = '/www/server/data' + data['port'] = '3306' + return data + + +# xss 防御 +def xssencode2(text): + try: + from cgi import html + text2 = html.escape(text, quote=True) + return text2 + except: + return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') + + +# 取缓存 +def cache_get(key, default=None): + from BTPanel import cache + + res = cache.get(key) + + if res is None: + return default + + return res + + +def add_security_logs(type, log, is_ip=True): + try: + if is_ip: + from flask import request + log = GetClientIp() + ":" + str(request.environ.get('REMOTE_PORT')) + log + M('security').add('type,log,addtime', (type, log, time.strftime('%Y-%m-%d %X', time.localtime()))) + except: + pass + + +# 设置缓存 +def cache_set(key, value, timeout=None): + from BTPanel import cache + if value == 'check': + admin_path = "/www/server/panel/data/admin_path.pl" + path = ReadFile(admin_path) + if path and len(path) > 3: + if not cache.get(GetClientIp() + 'admin_path_info'): + add_security_logs("Security entrance correct", "Successfully accessed the security entrance") + cache.set(GetClientIp() + 'admin_path_info', 1, 60) + return cache.set(key, value, timeout) + + +# 删除缓存 +def cache_remove(key): + from BTPanel import cache + return cache.delete(key) + + +# 取session值 +def sess_get(key): + from BTPanel import session + if key in session: return session[key] + return None + + +# 设置或修改session值 +def sess_set(key, value): + from BTPanel import session + session[key] = value + return True + + +# 删除指定session值 +def sess_remove(key): + from BTPanel import session + if key in session: del (session[key]) + return True + + +# 构造分页 +def get_page(count, p=1, rows=12, callback='', result='1,2,3,4,5,8'): + import page + try: + from BTPanel import request + uri = url_encode(request.full_path) + except: + uri = '' + page = page.Page() + info = {'count': count, 'row': rows, 'p': p, 'return_js': callback, 'uri': uri} + data = {'page': page.GetPage(info, result), 'shift': str(page.SHIFT), 'row': str(page.ROW)} + return data + + +# 取面板版本 +def version(): + try: + comm = ReadFile('{}/common.py'.format(get_class_path())) + return re.search(r"g\.version\s*=\s*'(\d+\.\d+\.\d+)'", comm).groups()[0] + except: + return get_panel_version() + + +def get_panel_version(): + comm = ReadFile('{}/common.py'.format(get_class_path())) + s_key = 'g.version = ' + s_len = len(s_key) + s_leff = comm.find(s_key) + s_len + version = comm[s_leff:s_leff + 10].strip().strip("'") + return version + + +def get_os_version(): + ''' + @name 取操作系统版本 + @author hwliang<2021-08-07> + @return string + ''' + p_file = '/etc/.productinfo' + if os.path.exists(p_file): + s_tmp = readFile(p_file).split("\n") + if s_tmp[0].find('Kylin') != -1 and len(s_tmp) > 1: + version = s_tmp[0] + ' ' + s_tmp[1].split('/')[0].strip() + else: + version = readFile('/etc/redhat-release') + if not version: + version = readFile('/etc/issue').strip().split("\n")[0].replace('\\n', '').replace(r'\l', '').strip() + else: + version = version.replace('release ', '').replace('Linux', '').replace('(Core)', '').strip() + v_info = sys.version_info + try: + version = "{} {}(Py{}.{}.{})".format(version, os.uname().machine, v_info.major, v_info.minor, v_info.micro) + except: + version = "{} (Py{}.{}.{})".format(version, v_info.major, v_info.minor, v_info.micro) + return xsssec(version) + + +# 取文件或目录大小 +def get_path_size(path, exclude=[]): + """根据排除目录获取路径的总大小 + + :path 目标路径 + :exclude 排除路径单个字符串或者多个列表。匹配路径是基于path的相对路径,规则是 + tar命令的--exclude规则的子集。 + """ + import fnmatch + if not os.path.exists(path): return 0 + if os.path.isfile(path): return os.path.getsize(path) + if type(exclude) != type([]): + exclude = [exclude] + + path = path[0:-1] if path[-1] == "/" else path + path = os.path.normcase(path) + # print("path:"+ path) + # print("exclude:"+ str(exclude)) + _exclude = exclude[0:] + for i, e in enumerate(_exclude): + if not e.startswith(path): + basename = os.path.basename(path) + if not e.startswith(basename): + exclude.append(os.path.join(path, e)) + else: + new_exc = e.replace(basename + "/", "") + new_exc = os.path.join(path, new_exc) + exclude.append(new_exc) + + # print(exclude) + total_size = 0 + count = 0 + for root, dirs, files in os.walk(path, topdown=True): + # filter path + for exc in exclude: + for d in dirs: + sub_dir = os.path.normcase(root + os.path.sep + d) + if fnmatch.fnmatch(sub_dir, exc) or d == exc: + # print("排除目录:"+sub_dir) + dirs.remove(d) + count += 1 + for f in files: + to_exclude = False + count += 1 + filename = os.path.normcase(root + os.path.sep + f) + if not os.path.exists(filename): continue + if os.path.islink(filename): continue + # filter file + norm_filename = os.path.normcase(filename) + for fexc in exclude: + if fnmatch.fnmatch(norm_filename, fexc) or fexc == f: + to_exclude = True + # print("排除文件:"+norm_filename) + break + if to_exclude: + continue + total_size += os.path.getsize(filename) + return total_size + + +# 写关键请求日志 +def write_request_log(reques=None): + try: + from BTPanel import request, g, session + if session.get('debug') == 1: return + log_path = '{}/logs/request'.format(get_panel_path()) + log_file = getDate(format='%Y-%m-%d') + '.json' + if not os.path.exists(log_path): os.makedirs(log_path) + + log_data = [] + log_data.append(getDate()) + log_data.append(GetClientIp() + ':' + get_remote_port()) + log_data.append(request.method) + log_data.append(request.full_path) + log_data.append(request.headers.get('User-Agent')) + if request.method == 'POST': + args = request.form.to_dict() + for k in args.keys(): + if k.find('pass') != -1 or k.find('user') != -1: + args[k] = '******' + if len(args[k]) > 4096: + args[k] = args[k][0:1024] + " -- >4096" + log_data.append(str(args)) + else: + log_data.append('{}') + log_data.append(int((time.time() - g.request_time) * 1000)) + log_data.append(g.response.status_code) + log_data.append(g.response.content_length) + log_data.append(g.response.headers.get('Content-Type')) + log_data.append(request.headers.get('Host')) + log_data.append(str(reques)) + log_msg = json.dumps(log_data) + "\n" + WriteFile(log_path + '/' + log_file, log_msg, 'a+') + rep_sys_path() + except: + pass + + +# 重载模块 +def mod_reload(mode): + if not mode: return False + try: + if sys.version_info[0] == 2: + reload(mode) + else: + import imp + imp.reload(mode) + return True + except: + return False + + +# 设置权限 +def set_mode(filename, mode): + if not os.path.exists(filename): return False + mode = int(str(mode), 8) + try: + os.chmod(filename, mode) + except: + return False + return True + + +def create_linux_user(user, group): + ''' + @name 创建系统用户 + @author hwliang<2022-01-15> + @param user 用户名 + @param group 所属组 + @return bool + ''' + ExecShell("groupadd {}".format(group)) + ExecShell('useradd -s /sbin/nologin -g {} {}'.format(user, group)) + return True + + +# 设置用户组 +def set_own(filename, user, group=None): + if not os.path.exists(filename): return False + from pwd import getpwnam + try: + user_info = getpwnam(user) + user = user_info.pw_uid + if group: + user_info = getpwnam(group) + group = user_info.pw_gid + except: + if user == 'www': create_linux_user(user, group) + # 如果指定用户或组不存在,则使用www + try: + user_info = getpwnam('www') + except: + create_linux_user(user, group) + user_info = getpwnam('www') + user = user_info.pw_uid + group = user_info.pw_gid + os.chown(filename, user, group) + return True + + +# 校验路径安全 +def path_safe_check(path, force=True): + if len(path) > 256: return False + checks = ['..', './', '\\', '%', '$', '^', '&', '*', '~', '"', "'", ';', '|', '{', '}', '`'] + for c in checks: + if path.find(c) != -1: return False + if force: + if not match_safe_path.match(path): return False + return True + + +# 取数据库字符集 +def get_database_character(db_name): + try: + db_obj = get_mysql_obj(db_name) + tmp = db_obj.query("show create database `%s`" % db_name.strip()) + c_type = str(re.findall(r"SET\s+([\w\d-]+)\s", tmp[0][1])[0]) + c_types = ['utf8', 'utf-8', 'gbk', 'big5', 'utf8mb4'] + if not c_type.lower() in c_types: return 'utf8' + return c_type + except: + return 'utf8' + + +# 取mysql数据库对象 +def get_mysql_obj(db_name): + is_cloud_db = False + if db_name: + db_find = M('databases').where("name=?", db_name).find() + if db_find['sid']: + return get_mysql_obj_by_sid(db_find['sid']) + is_cloud_db = db_find['db_type'] in ['1', 1] + if is_cloud_db: + import db_mysql + db_obj = db_mysql.panelMysql() + conn_config = json.loads(db_find['conn_config']) + try: + db_obj = db_obj.set_host(conn_config['db_host'], conn_config['db_port'], conn_config['db_name'], + conn_config['db_user'], conn_config['db_password']) + except Exception as e: + raise PanelError(GetMySQLError(e)) + else: + import panelMysql + db_obj = panelMysql.panelMysql() + return db_obj + + +# 取mysql数据库对像 By sid +def get_mysql_obj_by_sid(sid=0, conn_config=None): + if sid in ['0', '']: sid = 0 + if sid: + if not conn_config: conn_config = M('database_servers').where("id=?", sid).find() + import db_mysql + db_obj = db_mysql.panelMysql() + try: + db_obj = db_obj.set_host(conn_config['db_host'], conn_config['db_port'], None, conn_config['db_user'], + conn_config['db_password']) + except Exception as e: + raise PanelError(GetMySQLError(e)) + else: + import panelMysql + db_obj = panelMysql.panelMysql() + return db_obj + + +def GetMySQLError(e): + res = '' + if e.args[0] == 1045: + res = get_msg_gettext('Database username or password is wrong!') + if e.args[0] == 1049: + res = get_msg_gettext('database does not exist!') + if e.args[0] == 1044: + res = get_msg_gettext('No permission, or the specified database does not exist!') + if e.args[0] == 1062: + res = get_msg_gettext('Database already exists!') + if e.args[0] == 1146: + res = get_msg_gettext('Table does not exist!') + if e.args[0] == 2003: + res = get_msg_gettext('Database server connection failed!') + if e.args[0] == 1142: + res = get_msg_gettext('Insufficient user rights!') + if res: + res = res + "
                                    " + str(e) + "
                                    " + else: + res = str(e) + return res + + +def get_database_codestr(codeing): + wheres = { + 'utf8': 'utf8_general_ci', + 'utf8mb4': 'utf8mb4_general_ci', + 'gbk': 'gbk_chinese_ci', + 'big5': 'big5_chinese_ci' + } + return wheres[codeing] + + +def get_database_size(name=None): + """ + @获取数据库大小 + """ + data = {} + try: + mysql_obj = get_mysql_obj(name) + tables = mysql_obj.query( + "select table_schema, (sum(DATA_LENGTH)+sum(INDEX_LENGTH)) as data from information_schema.TABLES group by table_schema") + if type(tables) == list: + for x in tables: + if len(x) < 2: continue + if x[1] == None: continue + data[x[0]] = int(x[1]) + except: + return data + return data + + +def get_database_size_by_name(name): + """ + @获取数据库大小 + """ + data = 0 + try: + mysql_obj = get_mysql_obj(name) + tables = mysql_obj.query( + "select table_schema, (sum(DATA_LENGTH)+sum(INDEX_LENGTH)) as data from information_schema.TABLES WHERE table_schema='{}' group by table_schema".format( + name)) + data = tables[0][1] + if not data: data = 0 + except: + return data + return data + + +def get_database_size_by_id(id): + """ + @获取数据库大小 + """ + data = 0 + try: + name = M('databases').where('id=?', id).getField('name') + mysql_obj = get_mysql_obj(name) + tables = mysql_obj.query( + "select table_schema, (sum(DATA_LENGTH)+sum(INDEX_LENGTH)) as data from information_schema.TABLES WHERE table_schema='{}' group by table_schema".format( + name)) + data = tables[0][1] + if not data: data = 0 + except: + return data + return data + + +def en_punycode(domain): + if sys.version_info[0] == 2: + domain = domain.encode('utf8') + tmp = domain.split('.') + newdomain = '' + for dkey in tmp: + if dkey == '*': continue + # 匹配非ascii字符 + match = re.search(u"[\x80-\xff]+", dkey) + if not match: match = re.search(u"[\u4e00-\u9fa5]+", dkey) + if not match: + newdomain += dkey + '.' + else: + if sys.version_info[0] == 2: + newdomain += 'xn--' + dkey.decode('utf-8').encode('punycode') + '.' + else: + newdomain += 'xn--' + dkey.encode('punycode').decode('utf-8') + '.' + if tmp[0] == '*': newdomain = "*." + newdomain + return newdomain[0:-1] + + +# punycode 转中文 +def de_punycode(domain): + tmp = domain.split('.') + newdomain = '' + for dkey in tmp: + if dkey.find('xn--') >= 0: + newdomain += dkey.replace('xn--', '').encode('utf-8').decode('punycode') + '.' + else: + newdomain += dkey + '.' + return newdomain[0:-1] + + +# 取计划任务文件路径 +def get_cron_path(): + u_file = '/var/spool/cron/crontabs/root' + if not os.path.exists(u_file): + file = '/var/spool/cron/root' + else: + file = u_file + return file + + +# 加密字符串 +def en_crypt(key, strings): + try: + if type(strings) != bytes: strings = strings.encode('utf-8') + from cryptography.fernet import Fernet + f = Fernet(key) + result = f.encrypt(strings) + return result.decode('utf-8') + except: + # print(get_error_info()) + return strings + + +# 解密字符串 +def de_crypt(key, strings): + try: + if type(strings) != bytes: strings = strings.decode('utf-8') + from cryptography.fernet import Fernet + f = Fernet(key) + result = f.decrypt(strings).decode('utf-8') + return result + except: + # print(get_error_info()) + return strings + + +# 获取IP限制列表 +def get_limit_ip(): + iplong_list = [] + try: + ip_file = 'data/limitip.conf' + if not os.path.exists(ip_file): return iplong_list + + from BTPanel import cache + ikey = 'limit_ip' + iplong_list = cache.get(ikey) + if iplong_list: return iplong_list + + iplong_list = [] + iplist = ReadFile(ip_file) + if not iplist: return iplong_list + iplist = iplist.strip() + for limit_ip in iplist.split(','): + if not limit_ip: continue + limit_ip = limit_ip.split('-') + iplong = {} + iplong['min'] = ip2long(limit_ip[0]) + if len(limit_ip) > 1: + iplong['max'] = ip2long(limit_ip[1]) + else: + iplong['max'] = iplong['min'] + iplong_list.append(iplong) + + cache.set(ikey, iplong_list, 3600) + except: + pass + return iplong_list + + +def is_api_limit_ip(ip_list, client_ip): + ''' + @name 判断IP是否在限制列表中 + @author hwliang<2022-02-10> + @param ip_list 限制IP列表 + @param client_ip 客户端IP + @return bool + ''' + iplong_list = [] + for limit_ip in ip_list: + if not limit_ip: continue + if limit_ip in ['*', 'all', '0.0.0.0', '0.0.0.0/0', '0.0.0.0/24', '0.0.0.0/32']: return True + limit_ip = limit_ip.split('-') + iplong = {} + iplong['min'] = ip2long(limit_ip[0]) + if len(limit_ip) > 1: + iplong['max'] = ip2long(limit_ip[1]) + else: + iplong['max'] = iplong['min'] + iplong_list.append(iplong) + + client_ip_long = ip2long(client_ip) + for limit_ip in iplong_list: + if client_ip_long >= limit_ip['min'] and client_ip_long <= limit_ip['max']: + return True + return False + + +# 检查IP白名单 +def check_ip_panel(): + iplong_list = get_limit_ip() + if not iplong_list: return False + client_ip = GetClientIp() + if client_ip in ['127.0.0.1', 'localhost', '::1']: return False + client_ip_long = ip2long(client_ip) + for limit_ip in iplong_list: + if client_ip_long >= limit_ip['min'] and client_ip_long <= limit_ip['max']: + return False + + # errorStr = ReadFile('./BTPanel/templates/' + GetConfigValue('template') + '/error2.html') + # try: + # errorStr = errorStr.format(getMsg('PAGE_ERR_TITLE'),getMsg('PAGE_ERR_IP_H1'),getMsg('PAGE_ERR_IP_P1',(GetClientIp(),)),getMsg('PAGE_ERR_IP_P2'),getMsg('PAGE_ERR_IP_P3'),getMsg('NAME'),getMsg('PAGE_ERR_HELP')) + # except IndexError:pass + # return error_not_login(errorStr,True) + return error_403(None) + + +# 检查面板域名 +def check_domain_panel(): + tmp = GetHost() + domain = ReadFile('data/domain.conf') + if domain: + client_ip = GetClientIp() + if client_ip in ['127.0.0.1', 'localhost', '::1']: return False + if tmp.strip().lower() != domain.strip().lower(): + if check_client_info(): + try: + from flask import render_template + return render_template('error2.html') + except: + pass + + return error_403(None) + return False + + +# 是否离线模式 +def is_local(): + s_file = '{}/data/not_network.pl'.format(get_panel_path()) + return os.path.exists(s_file) + + +# 自动备份面板数据 +def auto_backup_panel(): + try: + panel_paeh = get_panel_path() + paths = panel_paeh + '/data/not_auto_backup.pl' + if os.path.exists(paths): return False + b_path = '{}/panel'.format(get_backup_path()) + day_date = format_date('%Y-%m-%d') + backup_path = b_path + '/' + day_date + backup_file = backup_path + '.zip' + if os.path.exists(backup_path) or os.path.exists(backup_file): return True + ignore_default = '' + ignore_system = '' + max_size = 100 * 1024 * 1024 + if os.path.getsize('{}/data/default.db'.format(panel_paeh)) > max_size: + ignore_default = 'default.db' + if os.path.getsize('{}/data/system.db'.format(panel_paeh)) > max_size: + ignore_system = 'system.db' + os.makedirs(backup_path, 384) + import shutil + shutil.copytree(panel_paeh + '/data', backup_path + '/data', + ignore=shutil.ignore_patterns(ignore_system, ignore_default)) + shutil.copytree(panel_paeh + '/config', backup_path + '/config') + shutil.copytree(panel_paeh + '/vhost', backup_path + '/vhost') + ExecShell("cd {} && zip {} -r {}/".format(b_path, backup_file, day_date)) + ExecShell("chmod -R 600 {path};chown -R root.root {path}".format(path=backup_file)) + if os.path.exists(backup_path): shutil.rmtree(backup_path) + + time_now = time.time() - (86400 * 30) + for f in os.listdir(b_path): + if f.endswith(".zip") and time.mktime(time.strptime(f, "%Y-%m-%d.zip")) < time_now: + path = b_path + '/' + f + b_file = path # + '.zip' + # if os.path.exists(path): + # shutil.rmtree(path) + if os.path.exists(b_file): + os.remove(b_file) + set_php_cli_env() + except: + pass + + +def set_php_cli_env(): + ''' + @name 重新设置php-cli.ini配置 + ''' + import jobs + jobs.set_php_cli_env() + + +# 检查端口状态 +def check_port_stat(port, localIP='127.0.0.1'): + import socket + temp = {} + temp['port'] = port + temp['local'] = True + try: + s = socket.socket() + s.settimeout(0.15) + s.connect((localIP, port)) + s.close() + except: + temp['local'] = False + + result = 0 + if temp['local']: result += 2 + return result + + +# 同步时间 +def sync_date(): + tip_file = "/dev/shm/last_sync_time.pl" + s_time = int(time.time()) + try: + if os.path.exists(tip_file): + if s_time - int(readFile(tip_file)) < 60: return False + os.remove(tip_file) + time_str = HttpGet(GetConfigValue('home') + '/api/index/get_time') + new_time = int(time_str) + time_arr = time.localtime(new_time) + date_str = time.strftime("%Y-%m-%d %H:%M:%S", time_arr) + ExecShell('date -s "%s"' % date_str) + writeFile(tip_file, str(s_time)) + return True + except: + if os.path.exists(tip_file): os.remove(tip_file) + return False + + +# 重载模块 +def reload_mod(mod_name=None): + # 是否重载指定模块 + modules = [] + if mod_name: + if type(mod_name) == str: + mod_names = mod_name.split(',') + + for mod_name in mod_names: + if mod_name in sys.modules: + print(mod_name) + try: + if sys.version_info[0] == 2: + reload(sys.modules[mod_name]) + else: + importlib.reload(sys.modules[mod_name]) + modules.append([mod_name, True]) + except: + modules.append([mod_name, False]) + else: + modules.append([mod_name, False]) + return modules + + # 重载所有模块 + for mod_name in sys.modules.keys(): + if mod_name in ['BTPanel']: continue + f = getattr(sys.modules[mod_name], '__file__', None) + if f: + try: + if f.find('panel/') == -1: continue + if sys.version_info[0] == 2: + reload(sys.modules[mod_name]) + else: + importlib.reload(sys.modules[mod_name]) + modules.append([mod_name, True]) + except: + modules.append([mod_name, False]) + return modules + + +def de_hexb(data): + if sys.version_info[0] != 2: + if type(data) == str: data = data.encode('utf-8') + pdata = base64.b64encode(data) + if sys.version_info[0] != 2: + if type(pdata) == str: pdata = pdata.encode('utf-8') + return binascii.hexlify(pdata) + + +def en_hexb(data): + if sys.version_info[0] != 2: + if type(data) == str: data = data.encode('utf-8') + result = base64.b64decode(binascii.unhexlify(data)) + if type(result) != str: result = result.decode('utf-8') + return result + + +# def upload_file_url(filename): +# try: +# if os.path.exists(filename): +# data = ExecShell('/usr/bin/curl https://scanner.baidu.com/enqueue -F archive=@%s' % filename) +# data = json.loads(data[0]) +# time.sleep(1) +# import requests +# default_headers = { +# 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' +# } +# data_list = requests.get(url=data['url'], headers=default_headers, verify=False) +# return (data_list.json()) +# else: +# return False +# except: +# return False + +# 直接请求到PHP-FPM +# version php版本 +# uri 请求uri +# filename 要执行的php文件 +# args 请求参数 +# method 请求方式 +def request_php(version, uri, document_root, method='GET', pdata=b''): + import panelPHP + if type(pdata) == dict: pdata = url_encode(pdata) + fpm_address = get_fpm_address(version) + p = panelPHP.FPM(fpm_address, document_root) + result = p.load_url_public(uri, pdata, method) + return result + + +def get_fpm_address(php_version, bind=False): + ''' + @name 获取FPM请求地址 + @author hwliang<2020-10-23> + @param php_version string PHP版本 + @return tuple or string + ''' + fpm_address = '/tmp/php-cgi-{}.sock'.format(php_version) + php_fpm_file = '{}/php/{}/etc/php-fpm.conf'.format(get_setup_path(), php_version) + try: + fpm_conf = readFile(php_fpm_file) + tmp = re.findall(r"listen\s*=\s*(.+)", fpm_conf) + if not tmp: return fpm_address + if tmp[0].find('sock') != -1: return fpm_address + if tmp[0].find(':') != -1: + listen_tmp = tmp[0].split(':') + if bind: + fpm_address = (listen_tmp[0], int(listen_tmp[1])) + else: + fpm_address = ('127.0.0.1', int(listen_tmp[1])) + else: + fpm_address = ('127.0.0.1', int(tmp[0])) + return fpm_address + except: + return fpm_address + + +def get_php_proxy(php_version, webserver='nginx'): + ''' + @name 获取PHP代理地址 + @author hwliang<2020-10-24> + @param php_version string php版本 (52|53|54|55|56|70|71|72|73|74) + @param webserver string web服务器类型 (nginx|apache|ols) + return string + ''' + php_address = get_fpm_address(php_version) + if isinstance(php_address, str): + if webserver == 'nginx': + return 'unix:{}'.format(php_address) + elif webserver == 'apache': + return 'unix:{}|fcgi://localhost'.format(php_address) + else: + if webserver == 'nginx': + return '{}:{}'.format(php_address[0], php_address[1]) + elif webserver == 'apache': + return 'fcgi://{}:{}'.format(php_address[0], php_address[1]) + + +def get_php_version_conf(conf): + ''' + @name 从指定配置文件获取PHP版本 + @author hwliang<2020-10-24> + @param conf string 配置文件内容 + @return string + ''' + if not conf: return '00' + if conf.find('enable-php-') != -1: + rep = r"enable-php-(\w{2,5})[-\w]*\.conf" + tmp = re.findall(rep, conf) + if not tmp: return '00' + elif conf.find('/usr/local/lsws/lsphp') != -1: + rep = r"path\s*/usr/local/lsws/lsphp(\d+)/bin/lsphp" + tmp = re.findall(rep, conf) + if not tmp: return '00' + else: + rep = r"php-cgi-([0-9]{2,3})\.sock" + tmp = re.findall(rep, conf) + if not tmp: + rep = r'\d+\.\d+\.\d+\.\d+:10(\d{2,2})1' + tmp = re.findall(rep, conf) + if not tmp: + return '00' + return tmp[0] + + +def get_site_php_version(siteName): + ''' + @name 获取指定网站当前使用的PHP版本 + @author hwliang<2020-10-24> + @param siteName string 网站名称 + @return string + ''' + web_server = get_webserver() + vhost_path = get_vhost_path() + conf = readFile(vhost_path + '/' + web_server + '/' + siteName + '.conf') + if web_server == 'openlitespeed': + conf = readFile(vhost_path + '/' + web_server + '/detail/' + siteName + '.conf') + return get_php_version_conf(conf) + + +def check_tcp(ip, port): + ''' + @name 使用TCP的方式检测指定IP:端口是否能连接 + @author hwliang<2021-06-01> + @param ip IP地址 + @param port 端口 + @return bool + ''' + import socket + try: + s = socket.socket() + s.settimeout(5) + s.connect((ip.strip(), int(port))) + s.close() + except: + return False + return True + + +def sub_php_address(conf_file, rep, tsub, php_version): + ''' + @name 替换新的PHP配置到配置文件 + @author hwliang<2020-10-24> + @param conf_file string 配置文件全路径 + @param rep string 用于查找目标替换内容的正则表达式 + @param tsub string 新的内容 + @param php_version string 指定PHP版本 + @return bool + ''' + if not os.path.isfile(conf_file): return False + if not os.path.exists(conf_file): return False + conf = readFile(conf_file) + if not conf: return False + # if conf.find('#PHP') == -1 and conf.find('pathinfo.conf') == -1: return False + if conf_file.split('-')[-1].find(php_version + ".") != 0: + phpv = get_php_version_conf(conf) + if phpv != php_version: return False + + tmp = re.search(rep, conf) + if not tmp: return False + if tmp.group() == tsub: return False + conf = conf.replace(tmp.group(), tsub) # re.sub(rep,php_proxy,conf) + writeFile(conf_file, conf) + return True + + +def sync_all_address(): + ''' + @name 同步所有PHP版本配置到配置文件 + @author hwliang<2020-10-24> + @return void + ''' + php_versions = get_php_versions() + for phpv in php_versions: + sync_php_address(phpv) + + +def sync_php_address(php_version): + ''' + @name 同步PHP版本配置到所有配置文件 + @author hwliang<2020-10-24> + @param php_version string PHP版本 + @return void + ''' + if not os.path.exists('{}/php/{}/bin/php'.format(get_setup_path(), php_version)): # 指定PHP版本是否安装 + return False + ngx_rep = r"(unix:/tmp/php-cgi.*\.sock|\d+\.\d+\.\d+\.\d+:\d+)" + apa_rep = r"(unix:/tmp/php-cgi.*\.sock\|fcgi://localhost|fcgi://\d+\.\d+\.\d+\.\d+:\d+)" + ngx_proxy = get_php_proxy(php_version, 'nginx') + apa_proxy = get_php_proxy(php_version, 'apache') + is_write = False + + # nginx的PHP配置文件 + nginx_conf_path = '{}/nginx/conf'.format(get_setup_path()) + + if os.path.exists(nginx_conf_path): + for f_name in os.listdir(nginx_conf_path): + if f_name.find('enable-php') != -1: + conf_file = '/'.join((nginx_conf_path, f_name)) + if sub_php_address(conf_file, ngx_rep, ngx_proxy, php_version): + is_write = True + # nginx的phpmyadmin + # conf_file = '/www/server/nginx/conf/nginx.conf' + # if os.path.exists(conf_file): + # if sub_php_address(conf_file,ngx_rep,ngx_proxy,php_version): + # is_write = True + + # apache的网站配置文件 + apache_conf_path = '{}/apache'.format(get_vhost_path()) + if os.path.exists(apache_conf_path): + for f_name in os.listdir(apache_conf_path): + conf_file = '/'.join((apache_conf_path, f_name)) + if sub_php_address(conf_file, apa_rep, apa_proxy, php_version): + is_write = True + # apache的phpmyadmin + conf_file = '{}/apache/conf/extra/httpd-vhosts.conf'.format(get_setup_path()) + if os.path.exists(conf_file): + if sub_php_address(conf_file, apa_rep, apa_proxy, php_version): + is_write = True + + if is_write: serviceReload() + return True + + +def url_encode(data): + if type(data) != str: return data + if sys.version_info[0] != 2: + import urllib.parse + pdata = urllib.parse.quote(data) + else: + import urllib + pdata = urllib.urlencode(data) + return pdata + + +def url_decode(data): + if type(data) != str: return data + if sys.version_info[0] != 2: + import urllib.parse + pdata = urllib.parse.unquote(data) + else: + import urllib + pdata = urllib.urldecode(data) + return pdata + + +def unicode_encode(data): + try: + if sys.version_info[0] == 2: + result = unicode(data, errors='ignore') + else: + result = data.encode('utf8', errors='ignore') + return result + except: + return data + + +def unicode_decode(data, charset='utf8'): + try: + if sys.version_info[0] == 2: + result = unicode(data, errors='ignore') + else: + result = data.decode('utf8', errors='ignore') + return result + except: + return data + + +def import_cdn_plugin(): + plugin_path = 'plugin/static_cdn' + if not os.path.exists(plugin_path): return True + try: + import static_cdn_main + except: + package_path_append(plugin_path) + import static_cdn_main + + +def get_cdn_hosts(): + try: + if import_cdn_plugin(): return [] + import static_cdn_main + return static_cdn_main.static_cdn_main().get_hosts(None) + except: + return [] + + +def get_cdn_url(): + try: + if os.path.exists('plugin/static_cdn/not_open.pl'): + return False + from BTPanel import cache + cdn_url = cache.get('cdn_url') + if cdn_url: return cdn_url + if import_cdn_plugin(): return False + import static_cdn_main + cdn_url = static_cdn_main.static_cdn_main().get_url(None) + cache.set('cdn_url', cdn_url, 3) + return cdn_url + except: + return False + + +def set_cdn_url(cdn_url): + if not cdn_url: return False + import_cdn_plugin() + get = dict_obj() + get.cdn_url = cdn_url + import static_cdn_main + static_cdn_main.static_cdn_main().set_url(get) + return True + + +def get_python_bin(): + bin_file = '{}/pyenv/bin/python3'.format(get_panel_path()) + if os.path.exists(bin_file): + return bin_file + return '/usr/bin/python' + + +def get_pip_bin(): + bin_file = '{}/pyenv/bin/pip'.format(get_panel_path()) + if os.path.exists(bin_file): + return bin_file + return '/usr/bin/pip' + + +def aes_encrypt(data, key): + import panelAes + if sys.version_info[0] == 2: + aes_obj = panelAes.aescrypt_py2(key) + return aes_obj.aesencrypt(data) + else: + aes_obj = panelAes.aescrypt_py3(key) + return aes_obj.aesencrypt(data) + + +def aes_decrypt(data, key): + import panelAes + if sys.version_info[0] == 2: + aes_obj = panelAes.aescrypt_py2(key) + return aes_obj.aesdecrypt(data) + else: + aes_obj = panelAes.aescrypt_py3(key) + return aes_obj.aesdecrypt(data) + + +# 清理大日志文件 +def clean_max_log(log_file, max_size=100, old_line=100): + if not os.path.exists(log_file): return False + max_size = 1024 * 1024 * max_size + if os.path.getsize(log_file) > max_size: + try: + old_body = GetNumLines(log_file, old_line) + writeFile(log_file, old_body) + except: + print(get_error_info()) + + +# 获取证书哈希 +def get_cert_data(path): + import panelSSL + get = dict_obj() + get.certPath = path + data = panelSSL.panelSSL().GetCertName(get) + return data + + +# 获取系统发行版 +def get_linux_distribution(): + distribution = 'ubuntu' + redhat_file = '/etc/redhat-release' + if os.path.exists(redhat_file): + try: + tmp = readFile(redhat_file).split()[3][0] + distribution = 'centos{}'.format(tmp) + except: + distribution = 'centos7' + return distribution + + +def long2ip(ips): + ''' + @name 将整数转换为IP地址 + @author hwliang<2020-06-11> + @param ips string(ip地址整数) + @return ipv4 + ''' + i1 = int(ips / (2 ** 24)) + i2 = int((ips - i1 * (2 ** 24)) / (2 ** 16)) + i3 = int(((ips - i1 * (2 ** 24)) - i2 * (2 ** 16)) / (2 ** 8)) + i4 = int(((ips - i1 * (2 ** 24)) - i2 * (2 ** 16)) - i3 * (2 ** 8)) + return "{}.{}.{}.{}".format(i1, i2, i3, i4) + + +def ip2long(ip): + ''' + @name 将IP地址转换为整数 + @author hwliang<2020-06-11> + @param ip string(ipv4) + @return long + ''' + ips = ip.split('.') + if len(ips) != 4: return 0 + iplong = 2 ** 24 * int(ips[0]) + 2 ** 16 * int(ips[1]) + 2 ** 8 * int(ips[2]) + int(ips[3]) + return iplong + + +def is_local_ip(ip): + ''' + @name 判断是否为本地(内网)IP地址 + @author hwliang<2021-03-26> + @param ip string(ipv4) + @return bool + ''' + patt = r"^(192\.168|127|10|172\.(16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31))\." + if re.match(patt, ip): return True + return False + + +# 获取debug日志 +def get_debug_log(): + from BTPanel import request + return GetClientIp() + ':' + str(request.environ.get('REMOTE_PORT')) + '|' + str( + int(time.time())) + '|' + get_error_info() + + +# 获取sessionid +def get_session_id(): + from BTPanel import request, app + session_id = request.cookies.get(app.config['SESSION_COOKIE_NAME'], '') + if not re.findall(r"^([\w\.-]{64,64})$", session_id): + return GetRandomString(64) + return session_id + + +# 尝试自动恢复面板数据库 +def rep_default_db(): + db_path = '{}/data/'.format(get_panel_path()) + db_file = db_path + 'default.db' + db_tmp_backup = db_path + 'default_' + format_date("%Y%m%d_%H%M%S") + ".db" + + panel_backup = '{}/panel'.format(get_backup_path()) + bak_list = os.listdir(panel_backup) + if not bak_list: return False + bak_list = sorted(bak_list, reverse=True) + db_bak_file = '' + for d_name in bak_list: + db_bak_file = panel_backup + '/' + d_name + '/data/default.db' + if not os.path.exists(db_bak_file): continue + if os.path.getsize(db_bak_file) < 17408: continue + break + + if not db_bak_file: return False + ExecShell(r"\cp -arf {} {}".format(db_file, db_tmp_backup)) + ExecShell(r"\cp -arf {} {}".format(db_bak_file, db_file)) + return True + + +def chdck_salt(): + ''' + @name 检查所有用户密码是否加盐,若没有则自动加上 + @author hwliang<2020-07-08> + @return void + ''' + + if not M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'users', '%salt%')).count(): + M('users').execute("ALTER TABLE 'users' ADD 'salt' TEXT", ()) + u_list = M('users').where('salt is NULL', ()).field('id,username,password,salt').select() + if isinstance(u_list, str): + if u_list.find('no such table: users') != -1: + rep_default_db() + if not M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'users', '%salt%')).count(): + M('users').execute("ALTER TABLE 'users' ADD 'salt' TEXT", ()) + u_list = M('users').where('salt is NULL', ()).field('id,username,password,salt').select() + + for u_info in u_list: + salt = GetRandomString(12) # 12位随机 + pdata = {} + pdata['password'] = md5(md5(u_info['password'] + '_bt.cn') + salt) + pdata['salt'] = salt + M('users').where('id=?', (u_info['id'],)).update(pdata) + + +def get_login_token(): + token_s = readFile('{}/data/login_token.pl'.format(get_panel_path())) + if not token_s: return GetRandomString(32) + return token_s + + +def get_sess_key(): + return md5(get_login_token() + get_csrf_sess_html_token_value()) + + +def password_salt(password, username=None, uid=None): + ''' + @name 为指定密码加盐 + @author hwliang<2020-07-08> + @param password string(被md5加密一次的密码) + @param username string(用户名) 可选 + @param uid int(uid) 可选 + @return string + ''' + chdck_salt() + if not uid: + if not username: + raise Exception('username或uid必需传一项') + uid = M('users').where('username=?', (username,)).getField('id') + salt = M('users').where('id=?', (uid,)).getField('salt') + return md5(md5(password + '_bt.cn') + salt) + + +# 备份配置文件 +def back_file(file, act=None): + """ + @name 备份配置文件 + @author zhwen + @param file 需要备份的文件 + @param act 如果存在,则备份一份作为默认配置 + """ + file_type = "_bak" + if act: + file_type = "_def" + ExecShell("/usr/bin/cp -p {0} {1}".format(file, file + file_type)) + + +# 还原配置文件 +def restore_file(file, act=None): + """ + @name 还原配置文件 + @author zhwen + @param file 需要还原的文件 + @param act 如果存在,则还原默认配置 + """ + file_type = "_bak" + if act: + file_type = "_def" + ExecShell("/usr/bin/cp -p {1} {0}".format(file, file + file_type)) + + +def package_path_append(path): + if not path in sys.path: + sys.path.insert(0, path) + + +def rep_sys_path(): + sys_path = [] + for p in sys.path: + if p in sys_path: continue + sys_path.append(p) + sys.path = sys_path + + +def get_ssh_port(): + ''' + @name 获取本机SSH端口 + @author hwliang<2020-08-07> + @return int + ''' + s_file = '/etc/ssh/sshd_config' + conf = readFile(s_file) + if not conf: conf = '' + port_all = re.findall(r".*Port\s+[0-9]+", conf) + ssh_port = 22 + for p in port_all: + rep = r"^\s*Port\s+([0-9]+)\s*" + tmp1 = re.findall(rep, p) + if tmp1: + ssh_port = int(tmp1[0]) + + return ssh_port + + +def set_error_num(key, empty=False, expire=3600): + ''' + @name 设置失败次数(每调用一次+1) + @author hwliang<2020-08-21> + @param key 索引 + @param empty 是否清空计数 + @param expire 计数器生命周期(秒) + @return bool + ''' + from BTPanel import cache + key = md5(key) + num = cache.get(key) + if not num: + num = 0 + else: + if empty: + cache.delete(key) + return True + cache.set(key, num + 1, expire) + return True + + +def get_error_num(key, limit=False): + ''' + @name 获取失败次数 + @author hwliang<2020-08-21> + @param key 索引 + @param limit 如果为False,则直接返回失败次数,否则与失败次数比较,若大于失败次数返回True,否则返回False + @return int or bool + ''' + from BTPanel import cache + key = md5(key) + num = cache.get(key) + if not num: num = 0 + if not limit: + return num + if limit > num: + return True + return False + + +def get_menus(): + ''' + @name 获取菜单列表 + @author hwliang<2020-08-31> + @return list + ''' + from BTPanel import session + data = json.loads(ReadFile('config/menu.json')) + hide_menu = ReadFile('config/hide_menu.json') + debug = session.get('debug') + if hide_menu: + hide_menu = json.loads(hide_menu) + show_menu = [] + for i in range(len(data)): + if data[i]['id'] in hide_menu: continue + if data[i]['id'] == "memuAxterm": + if debug: continue + show_menu.append(data[i]) + data = show_menu + del (hide_menu) + del (show_menu) + menus = sorted(data, key=lambda x: x['sort']) + return menus + + +# 取CURL路径 +def get_curl_bin(): + ''' + @name 取CURL执行路径 + @author hwliang<2020-09-01> + @return string + ''' + c_bin = ['/usr/local/curl2/bin/curl', '/usr/local/curl/bin/curl', '/usr/bin/curl'] + for cb in c_bin: + if os.path.exists(cb): return cb + return 'curl' + + +# 设置防跨站配置 +def set_open_basedir(): + try: + fastcgi_file = '{}/nginx/conf/fastcgi.conf'.format(get_setup_path()) + + if os.path.exists(fastcgi_file): + fastcgi_body = readFile(fastcgi_file) + if fastcgi_body.find('bt_safe_dir') == -1: + fastcgi_body = fastcgi_body + "\n" + 'fastcgi_param PHP_ADMIN_VALUE "$bt_safe_dir=$bt_safe_open";' + writeFile(fastcgi_file, fastcgi_body) + + proxy_file = '{}/nginx/conf/proxy.conf'.format(get_setup_path()) + if os.path.exists(proxy_file): + proxy_body = readFile(proxy_file) + if proxy_body.find('bt_safe_dir') == -1: + proxy_body = proxy_body + "\n" + '''map "baota_dir" $bt_safe_dir { + default "baota_dir"; +} +map "baota_open" $bt_safe_open { + default "baota_open"; +} ''' + writeFile(proxy_file, proxy_body) + + open_basedir_path = '{}/open_basedir/nginx'.format(get_vhost_path()) + if not os.path.exists(open_basedir_path): + os.makedirs(open_basedir_path, 384) + + site_list = M('sites').field('id,name,path').select() + for site_info in site_list: + set_site_open_basedir_nginx(site_info['name']) + except: + return + + +# 处理指定站点的防跨站配置 for Nginx +def set_site_open_basedir_nginx(siteName): + try: + return + open_basedir_path = '/www/server/panel/vhost/open_basedir/nginx' + if not os.path.exists(open_basedir_path): + os.makedirs(open_basedir_path, 384) + config_file = '/www/server/panel/vhost/nginx/{}.conf'.format(siteName) + open_basedir_file = "/".join( + (open_basedir_path, '{}.conf'.format(siteName)) + ) + if not os.path.exists(config_file): return + if not os.path.exists(open_basedir_file): + writeFile(open_basedir_file, '') + config_body = readFile(config_file) + if config_body.find(open_basedir_path) == -1: + config_body = config_body.replace("include enable-php", + "include {};\n\t\tinclude enable-php".format(open_basedir_file)) + writeFile(config_file, config_body) + + root_path = re.findall(r"root\s+(.+);", config_body)[0] + if not root_path: return + userini_file = root_path + '/.user.ini' + if not os.path.exists(userini_file): + writeFile(open_basedir_file, '') + return + userini_body = readFile(userini_file) + if not userini_body: return + if userini_body.find('open_basedir') == -1: + writeFile(open_basedir_file, '') + return + + open_basedir_conf = re.findall(r"open_basedir=(.+)", userini_body) + if not open_basedir_conf: return + open_basedir_conf = open_basedir_conf[0] + open_basedir_body = '''set $bt_safe_dir "open_basedir"; +set $bt_safe_open "{}";'''.format(open_basedir_conf) + writeFile(open_basedir_file, open_basedir_body) + except: + return + + +def run_thread(fun, args=(), daemon=False): + ''' + @name 使用线程执行指定方法 + @author hwliang<2020-10-27> + @param fun {def} 函数对像 + @param args {tuple} 参数元组 + @param daemon {bool} 是否守护线程 + @return bool + ''' + import threading + p = threading.Thread(target=fun, args=args) + p.setDaemon(daemon) + p.start() + return True + + +def check_domain_cloud(domain): + run_thread(cloud_check_domain, (domain,)) + + +def count_wp(): + run_thread(httpPost('http://brandnew.aapanel.com/api/setupCount/setupWP', {})) + + +def cloud_check_domain(domain): + ''' + @name 从云端验证域名的可访问性,并将结果保存到文件 + @author hwliang<2020-12-10> + @param domain {string} 被验证的域名 + @return void + ''' + try: + check_domain_path = '{}/data/check_domain/'.format(get_panel_path()) + if not os.path.exists(check_domain_path): + os.makedirs(check_domain_path, 384) + pdata = get_user_info() + pdata['domain'] = domain + result = httpPost('https://www.aapanel.com/api/panel/checkDomain', {"domain": domain}) + cd_file = check_domain_path + domain + '.pl' + writeFile(cd_file, result) + except: + pass + + +def get_mac_address(): + import uuid + mac = uuid.UUID(int=uuid.getnode()).hex[-12:] + return ":".join([mac[e:e + 2] for e in range(0, 11, 2)]) + + +def get_user_info(): + user_file = '{}/data/userInfo.json'.format(get_panel_path()) + if not os.path.exists(user_file): return {} + userInfo = {} + try: + userTmp = json.loads(readFile(user_file)) + userInfo['uid'] = userTmp['id'] + userInfo['username'] = userTmp['username'] + userInfo['serverid'] = userTmp['serverid'] + userInfo['oem'] = get_oem_name() + userInfo['o'] = userInfo['oem'] + except: + pass + return userInfo + + +def send_file(data, fname='', mimetype=''): + ''' + @name 以文件流的形式返回 + @author heliang<2020-10-27> + @param data {bytes|string} 文件数据或路径 + @param mimetype {string} 文件类型 + @param fname {string} 文件名 + @return Response + ''' + d_type = type(data) + from io import BytesIO, StringIO + from flask import send_file as send_to + if d_type == bytes: + fp = BytesIO(data) + else: + if len(data) < 128: + if os.path.exists(data): + fp = data + if not fname: + fname = os.path.basename(fname) + else: + fp = StringIO(data) + else: + fp = StringIO(data) + + if not mimetype: mimetype = "application/octet-stream" + if not fname: fname = 'doan.txt' + + import flask + if flask.__version__ < "2.1.0": + return send_to(fp, + mimetype=mimetype, + as_attachment=True, + add_etags=True, + conditional=True, + attachment_filename=fname, + cache_timeout=0) + else: + return send_to(fp, + mimetype=mimetype, + as_attachment=True, + etag=True, + conditional=True, + download_name=fname, + max_age=0) + + +def gen_password(length=8, chars=string.ascii_letters + string.digits): + from random import choice + return ''.join([choice(chars) for i in range(length)]) + + +def get_ipaddress(): + ''' + @name 获取本机IP地址 + @author hwliang<2020-11-24> + @return list + ''' + ipa_tmp = ExecShell( + "ip a |grep inet|grep -v inet6|grep -v 127.0.0.1|grep -v 'inet 192.168.'|grep -v 'inet 10.'|awk '{print $2}'|sed 's#/[0-9]*##g'")[ + 0].strip() + iplist = ipa_tmp.split('\n') + return iplist + + +def get_oem_name(): + ''' + @name 获取OEM名称 + @author hwliang<2021-03-24> + @return string + ''' + oem = '' + oem_file = '{}/data/o.pl'.format(get_panel_path()) + if os.path.exists(oem_file): + oem = readFile(oem_file) + if oem: oem = oem.strip() + return oem + + +def get_pdata(): + ''' + @name 构造POST基础参数 + @author hwliang<2021-03-24> + @return dict + ''' + import panelAuth + pdata = panelAuth.panelAuth().create_serverid(None) + pdata['oem'] = get_oem_name() + return pdata + + +# 刷新授权信息 +def refresh_pd(): + from BTPanel import cache + + try: + p_token = cache.get('p_token') + + if p_token is None: + p_token = 'bmac_' + public.Md5(public.get_mac_address()) + cache.set('p_token', p_token) + + softList = load_soft_list(False) + + public.writeFile("/tmp/" + p_token, str(softList['pro'])) + public.writeFile('/tmp/{}.time'.format(p_token), str(int(time.time()))) + except: + pass + + +# 获取授权信息 +def get_pd(args=None): + """ + @name 获取授权信息 + @param args: + @return tuple[html, pro, ltd] + """ + from BTPanel import cache + + # 专业版到期时间 -1.过期 0.永久授权 >0.到期时间 + pro = -1 + + # 企业版到期时间 -1.过期 0.永久授权 >0.到期时间 + ltd = -1 + + # HTML文本 + htm = '' + + # 获取当前时间 + cur_time = int(time.time()) + + p_token = cache.get('p_token') + + if p_token is None: + p_token = 'bmac_' + Md5(get_mac_address()) + cache.set('p_token', p_token) + + tmp_f = '/tmp/' + p_token + p_token_time_f = '/tmp/{}.time'.format(p_token) + + # 检查缓存是否失效 + if not os.path.exists(tmp_f) or not os.path.exists(p_token_time_f) or int(readFile(p_token_time_f).strip()) + 86400 <= cur_time: + # 检查用户是否登录,登录后才获取授权信息 + userinfo_f = '{}/data/userInfo.json'.format(get_panel_path()) + if os.path.exists(userinfo_f) and os.path.getsize(userinfo_f) > 10: + # 缓存失效时重新获取授权信息 + plugin_list = load_soft_list() + + if isinstance(plugin_list, dict): + pro = plugin_list.get('pro', -1) + # ltd = plugin_list.get('ltd', -1) + + writeFile(tmp_f, str(pro), 'w') + writeFile(p_token_time_f, str(cur_time), 'w') + + tmp = readFile(tmp_f) + if tmp: + pro = int(tmp) + + if ltd < 1: + if ltd == -2: + htm = 'EXPIREDRENEW' + elif pro == -1: + htm = 'FREE' + elif pro == -2: + htm = 'EXPIREDRENEW' + if pro >= 0 and ltd in [-1, -2]: + if pro == 0: + tmp2 = 'Lifetime' + htm = 'Expire:{0}'.format( + tmp2) + else: + tmp2 = time.strftime('%Y-%m-%d', time.localtime(pro)) + htm = 'Expire: {0}RENEW'.format( + tmp2) + else: + htm = 'FREE' + else: + htm = 'Expire: {}RENEW'.format( + time.strftime('%Y-%m-%d', time.localtime(ltd))) + + return htm, pro, ltd + + +# 名称输入系列化 +def xssdecode(text): + try: + cs = {""": '"', """: '"', "'": "'", "'": "'"} + for c in cs.keys(): + text = text.replace(c, cs[c]) + + str_convert = text + if sys.version_info[0] == 3: + import html + text2 = html.unescape(str_convert) + else: + text2 = cgi.unescape(str_convert) + return text2 + except: + return text + + +def fetch_disk_SN(): + r, e = ExecShell("fdisk -l |grep 'Disk identifier' |awk {'print $3'}") + if r: + return r.split('\n')[0] + + +def fetch_cpu_ID(): + r, e = ExecShell("cat /proc/cpuinfo|grep microcode|awk -F':' '{print $2}'") + if r: + return r.split('\n')[0] + + +def get_platform(): + import platform + return platform.platform() + + +def get_memory(): + import psutil + return psutil.virtual_memory().total + + +def fetch_env_info(): + import panelAuth + userInfo = panelAuth.panelAuth().create_serverid(None) + try: + return {'ip': GetLocalIp(), + 'is_ipv6': 0, + 'os': get_platform(), + 'mac': get_mac_address(), + 'hdid': fetch_disk_SN(), + 'ramid': get_memory(), + 'cpuid': fetch_cpu_ID(), + 'server_name': get_hostname(), + 'install_code': userInfo['server_id'] + } + except: + return {} + + +def arequests(method, url, data=None, timeout=3): + import threading + if method == 'post': + method = httpPost + else: + method = httpGet + threading.Thread(target=method, args=(url, data, timeout)).start() + + +# 取通用对象 +re_key_match = re.compile(r'^[\w\s\[\]\-.]+$') +re_key_match2 = re.compile(r'^\.?__[\w\s[\]\-]+__\.?$') +key_filter_list = ['get', 'set', 'get_items', 'exists', '__contains__', '__setitem__', '__getitem__', '__delitem__', + '__delattr__', '__setattr__', '__getattr__', '__class__', 'get_file'] + + +class dict_obj: + def __init__(self): + # 存放数据 + self.__store = {} + + # 检测数据是否经过校验 + self.__validated = set() + + def __contains__(self, key): + return hasattr(self, key) + + def __setitem__(self, key, value): + if key in key_filter_list: + raise ValueError("wrong field name") + + if not re_key_match.match(key) or re_key_match2.match(key): + raise ValueError("wrong field name") + + self.__store[key] = value + + def __getitem__(self, key): + return getattr(self, key) + + def __delitem__(self, key): + delattr(self, key) + + def __delattr__(self, key): + delattr(self, key) + + def __setattr__(self, key, value): + if match_class_private_property.match(key): + object.__setattr__(self, key, value) + return + + self.__store[key] = value + + def __getattr__(self, key): + if key in self.__store: + # 未经过校验的数据不允许获取 + # if key not in self.__validated: + # raise ValueError('参数值获取失败:参数 {} 尚未通过校验,请先调用 validate() 完成校验后再尝试重新获取参数值'.format(key)) + return self.__store[key] + + raise AttributeError('\'{}\' object has no attribute \'{}\''.format(self.__class__.__name__, key)) + + @property + def __dict__(self): + return self.__store + + def get_items(self): + return self.__store + + def validate(self, validate_rules: typing.List[Param], filters: typing.List[callable] = (trim_filter(),)) -> None: + """ + @name 验证请求参数 + @param validate_rules: list[validate.Param] 参数验证规则 + @param filters: list[callable] 参数过滤器 + @raise Error + """ + filters = list(filters) + + for v in validate_rules: + v.do_validate(self.__store) + + if v.name in self.__store: + self.__store[v.name] = v.do_filter(self.__store[v.name], filters) + + self.__validated.add(v.name) + + def exists(self, keys): + return exists_args(keys, self) + + def set(self, key, value): + if not isinstance(value, str) or not isinstance(key, str): return False + if key in key_filter_list: + raise ValueError("wrong field name") + if not re_key_match.match(key) or re_key_match2.match(key): + raise ValueError("wrong field name") + return setattr(self, key, value) + + def get(self, key, default='', format='', limit=[]): + ''' + @name 获取指定参数 + @param key 参数名称,允许在/后面限制参数格式,请参考参数值格式(format) + @param default 默认值,默认空字符串 + @param format 参数值格式(int|str|port|float|json|xss|path|url|ip|ipv4|ipv6|letter|mail|phone|正则表达式|>1|<1|=1),默认为空 + @param limit 限制参数值内容 + @param return mixed + ''' + if key.find('/') != -1: + key, format = key.split('/') + result = getattr(self, key, default) + if isinstance(result, str): result = result.strip() + if format: + if format in ['str', 'string', 's']: + result = str(result) + elif format in ['int', 'd']: + try: + result = int(result) + except: + raise ValueError("Parameters: {}, requires int type data".format(key)) + elif format in ['float', 'f']: + try: + result = float(result) + except: + raise ValueError("Parameters: {}, float type data required".format(key)) + elif format in ['json', 'j']: + try: + result = json.loads(result) + except: + raise ValueError("Parameters: {}, requires JSON string".format(key)) + elif format in ['xss', 'x']: + result = xssencode(result) + elif format in ['path', 'p']: + if not path_safe_check(result): + raise ValueError("Parameters: {}, the correct path format is required".format(key)) + result = result.replace('//', '/') + elif format in ['url', 'u']: + regex = re.compile( + r'^(?:http|ftp)s?://' + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' + r'localhost|' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + if not re.match(regex, result): + raise ValueError('Parameters: {}, the correct URL format is required'.format(key)) + elif format in ['ip', 'ipaddr', 'i', 'ipv4', 'ipv6']: + if format == 'ipv4': + if not is_ipv4(result): + raise ValueError('Parameters: {}, the correct ipv4 address is required'.format(key)) + elif format == 'ipv6': + if not is_ipv6(result): + raise ValueError('Parameters: {}, the correct ipv6 address is required'.format(key)) + else: + if not is_ipv4(result) and not is_ipv6(result): + raise ValueError('Parameters: {}, the correct ipv4/ipv6 address is required'.format(key)) + elif format in ['w', 'letter']: + if not re.match(r'^\w+$', result): + raise ValueError( + 'Parameters: {}, the requirement can only be composed of English letters'.format(key)) + elif format in ['email', 'mail', 'm']: + if not re.match(r'^.+@(\[?)[a-zA-Z0-9\-.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(]?)$', result): + raise ValueError("Parameters: {}, the correct email address format is required".format(key)) + elif format in ['phone', 'mobile', 'm']: + if not re.match(r"^1[3-9]\d{9}$", result): + raise ValueError("Parameters: {}, mobile phone number format required".format(key)) + elif format in ['port']: + result_port = int(result) + if result_port > 65535 or result_port < 0: + raise ValueError("Parameters: {}, the required port number is 0-65535".format(key)) + result = result_port + elif re.match(r"^[<>=]\d+$", result): + operator = format[0] + length = int(format[1:].strip()) + result_len = len(result) + error_obj = ValueError("Parameters: {}, the required length is {}".format(key, format)) + if operator == '=': + if result_len != length: + raise error_obj + elif operator == '>': + if result_len < length: + raise error_obj + else: + if result_len > length: + raise error_obj + elif format[0] in ['^', '(', '[', '\\', '.'] or format[-1] in ['$', ')', ']', '+', '}']: + if not re.match(format, result): + raise ValueError("The format of the specified parameter is incorrect, {}:{}".format(key, format)) + + if limit: + if not result in limit: + raise ValueError("The specified parameter value range is incorrect, {}:{}".format(key, limit)) + return result + + def get_file(self, key: str) -> werkzeug.datastructures.FileStorage: + """ + @name 获取上传文件对象 + @param key: str 参数名 + @return: werkzeug.datastructures.FileStorage + """ + if 'FILES' not in self.__store or key not in self.__store['FILES']: + raise ValueError('not found file with param name {}'.format(key)) + + return self.__store['FILES'][key] + + +# 实例化定目录下的所有模块 +class get_modules: + + def __contains__(self, key): + return self.get_attr(key) + + def __setitem__(self, key, value): + setattr(self, key, value) + + def get_attr(self, key): + ''' + 尝试获取模块,若为字符串,则尝试实例化模块,否则直接返回模块对像 + ''' + res = getattr(self, key) + if isinstance(res, str): + try: + tmp_obj = __import__(key) + reload(tmp_obj) + setattr(self, key, tmp_obj) + return tmp_obj + except: + raise Exception(get_error_info()) + return res + + def __getitem__(self, key): + return self.get_attr(key) + + def __delitem__(self, key): + delattr(self, key) + + def __delattr__(self, key): + delattr(self, key) + + def get_items(self): + return self + + def __init__(self, path="class", limit=None): + ''' + @name 加载指定目录下的模块 + @author hwliang<2020-08-03> + @param path 指定目录,可指定绝对目录,也可指定相对于/www/server/panel的相对目录 默认加载class目录 + @param limit 指定限定加载的模块名称,默认加载path目录下的所有模块 + @param object + + @example + p = get_modules('class') + if 'public' in p: + md5_str = p.public.md5('test') + md5_str = p['public'].md5('test') + md5_str = getattr(p['public'],'md5')('test') + else: + print(p.__dict__) + ''' + os.chdir(get_panel_path()) + exp_files = ['__init__.py', '__pycache__'] + if not path in sys.path: + sys.path.insert(0, path) + for fname in os.listdir(path): + if fname in exp_files: continue + filename = '/'.join([path, fname]) + if os.path.isfile(filename): + if not fname[-3:] in ['.py', '.so']: continue + mod_name = fname[:-3] + else: + c_file = '/'.join((filename, '__init__.py')) + if not os.path.exists(c_file): + continue + mod_name = fname + + if limit: + if not isinstance(limit, list) and not isinstance(limit, tuple): + limit = (limit,) + if not mod_name in limit: + continue + + setattr(self, mod_name, mod_name) + + +# 检查App和小程序的绑定 +def check_app(check='app'): + path = get_panel_path() + '/' + if check == 'app': + try: + if not os.path.exists("/www/server/panel/plugin/btapp/btapp_main.py"): return False + if not os.path.exists(path + 'config/api.json'): return False + if os.path.exists(path + 'config/api.json'): + btapp_info = json.loads(readFile(path + 'config/api.json')) + if not btapp_info['open']: return False + if not 'apps' in btapp_info: return False + if not btapp_info['apps']: return False + return True + return False + except: + return False + elif check == 'app_bind': + if not cache_get(Md5(os.uname().version)): return False + if not os.path.exists("/www/server/panel/plugin/btapp/btapp_main.py"): return False + if not os.path.exists(path + 'config/api.json'): return False + btapp_info = json.loads(readFile(path + 'config/api.json')) + if not btapp_info: return False + if not btapp_info['open']: return False + return True + elif check == 'wxapp': + if not os.path.exists(path + 'plugin/app/user.json'): return False + app_info = json.loads(readFile(path + 'plugin/app/user.json')) + if not app_info: return False + return True + + +# #宝塔邮件报警 +# def send_mail(title,body,is_logs=False,is_type="aapanel login reminder"): +# if is_logs: +# try: +# import send_mail +# send_mail22 = send_mail.send_mail() +# tongdao = send_mail22.get_settings() +# if tongdao['user_mail']['mail_list']==0:return False +# if not tongdao['user_mail']['info']: return False +# if len(tongdao['user_mail']['mail_list'])==1: +# send_mail=tongdao['user_mail']['mail_list'][0] +# send_mail22.qq_smtp_send(send_mail, title=title, body=body) +# else: +# send_mail22.qq_smtp_send(tongdao['user_mail']['mail_list'], title=title, body=body) +# if is_logs: +# WriteLog2(is_type, body) +# except: +# return False +# else: +# try: +# import send_mail +# send_mail22 = send_mail.send_mail() +# tongdao = send_mail22.get_settings() +# if tongdao['user_mail']['mail_list'] == 0: return False +# if not tongdao['user_mail']['info']: return False +# if len(tongdao['user_mail']['mail_list']) == 1: +# send_mail = tongdao['user_mail']['mail_list'][0] +# return send_mail22.qq_smtp_send(send_mail, title=title, body=body) +# else: +# return send_mail22.qq_smtp_send(tongdao['user_mail']['mail_list'], title=title, body=body) +# except: +# return False + +# 宝塔邮件报警 +def send_mail(title, body, is_logs=False, is_type="aapanel login reminder"): + try: + import panelPush + msg_data = { + "msg": body.replace("\n", "
                                    "), + "title": title + } + if is_logs: WriteLog2(is_type, body) + return panelPush.panelPush().push_message_immediately({"mail": msg_data}) + except Exception as ex: + return returnMsg(False, 'Failed to send: {}'.format(ex)) + + +# 发送钉钉告警 +def send_dingding(body, is_logs=False, is_type="aapanel login reminder"): + try: + import panelPush + if is_logs: WriteLog2(is_type, body) + return panelPush.panelPush().push_message_immediately({"dingding": {"msg": body}}) + except Exception as ex: + return returnMsg(False, 'Failed to send: {}'.format(ex)) + + +# 发送微信告警 +def send_weixin(body, is_logs=False, is_type="aapanel login reminder"): + try: + import panelPush + if is_logs: WriteLog2(is_type, body) + return panelPush.panelPush().push_message_immediately({"weixin": {"msg": body}}) + except Exception as ex: + return returnMsg(False, 'Failed to send: {}'.format(ex)) + + +# 发送飞书告警 +def send_feishu(body, is_logs=False, is_type="aapanel login reminder"): + try: + import panelPush + if is_logs: WriteLog2(is_type, body) + return panelPush.panelPush().push_message_immediately({"feishu": {"msg": body}}) + except Exception as ex: + return returnMsg(False, 'Failed to send: {}'.format(ex)) + + +# 发送除短信以外的所有告警通道 +def send_all(body, title=None): + try: + import panelPush + msg_dict = {"msg": body} + msg_all = { + "feishu": msg_dict, + "weixin": msg_dict, + "dingding": msg_dict, + "mail": { + "msg": body.replace("\n", "
                                    "), + "title": title + } + } + return panelPush.panelPush().push_message_immediately(msg_all) + except Exception as ex: + return returnMsg(False, 'Failed to send: {}'.format(ex)) + + +# 获取服务器IP +def get_ip(): + iplist_file = '{}/data/iplist.txt'.format(get_panel_path()) + if os.path.exists(iplist_file): + data = ReadFile(iplist_file) + return data.strip() + else: + return '127.0.0.1' + + +# 获取服务器内网Ip +def get_local_ip(): + try: + ret = ExecShell( + r"ip addr | grep -E -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | grep -E -v \"^127\.|^255\.|^0\.\" | head -n 1") + local_ip = ret[0].strip() + return local_ip + except: + return '127.0.0.1' + + +def create_logs(): + import db + sql = db.Sql() + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'logs2')).count(): + csql = '''CREATE TABLE `logs2` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `type` TEXT, + `log` TEXT, + `addtime` TEXT +, uid integer DEFAULT '1', username TEXT DEFAULT 'system')''' + sql.execute(csql, ()) + + +def WriteLog2(type, logMsg, args=(), not_web=False): + import db + create_logs() + username = 'system' + uid = 1 + tmp_msg = '' + sql = db.Sql() + mDate = time.strftime('%Y-%m-%d %X', time.localtime()) + data = (uid, username, type, logMsg + tmp_msg, mDate) + result = sql.table('logs2').add('uid,username,type,log,addtime', data) + + +def check_ip_white(path, ip): + if os.path.exists(path): + try: + path_json = json.loads(ReadFile(path)) + except: + WriteFile(path, '[]') + return False + if ip in path_json: + return True + else: + return False + else: + return False + + +def check_login_area(login_ip, login_type='panel'): + """ + @name 检测登录地区 + @login_type 登录类型 panel:宝塔面板登录, ssh:ssh登录 + """ + + login_ip_area = '' + ip_info = get_ips_area([login_ip]) + + if 'status' in ip_info: + login_ip_area = '****(Pro exclusive)' + else: + ip_info = ip_info[login_ip] + if not 'city' in ip_info: + login_ip_area = ip_info['info'] + + data = {} + status = False + sfile = '{}/data/{}_login_area.pl'.format(get_panel_path(), login_type) + s_conf = '{}/data/{}_login_area.json'.format(get_panel_path(), login_type) + if os.path.exists(sfile): + status = True + + data = {} + try: + data = json.loads(readFile(s_conf)) + except: + pass + + if not login_ip_area and 'city' in ip_info: + city = ip_info['city'] + login_ip_area = ip_info['info'] + + if not city in data: + data[city] = 0 + + if data[city] < 3: + login_ip_area += '(异地)' + data[city] += 1 + + writeFile(s_conf, json.dumps(data)) + + data['login_ip_area'] = login_ip_area + return status, data + + +def get_free_ips_area(ips): + ''' + @name 免费IP库 获取ip地址所在地 + @author cjxin + @param ips + @return list + ''' + import PluginLoader + args = dict_obj() + args.model_index = 'safe' + args.ips = ips + res = PluginLoader.module_run("freeip", "get_ip_area", args) + return res + + +def get_free_ip_info(address): + ''' + @name 免费IP库 获取ip地址所在地 + @param ip + @return dict + ''' + ip = address.split(':')[0] + if not is_ipv4(ip): + return {'info': 'unknow'} + if is_local_ip(ip): + return {'info': 'intranet', 'local': True} + + ip_info = {} + sfile = '{}/data/ip_area.json'.format(get_panel_path()) + try: + ip_info = json.loads(readFile(sfile)) + except: + pass + + if ip in ip_info: + return ip_info[ip] + try: + param = get_user_info() + param['ip'] = address + res = json.loads(httpPost('https://www.bt.cn/api/ip/info', param)) + + if address in res: + info = res[address] + ip_info[ip] = info + ip_info[ip]['info'] = '{} {} {} {}'.format(info['carrier'], info['country'], info['province'], + info['city']).strip() + ip_info[ip]['ip'] = ip + writeFile(sfile, json.dumps(ip_info)) + return res[address] + except: + pass + + return {'info': 'Unknown'} + + +# 使用免费IP库获取IP地区 +def free_login_area(login_ip, login_type='panel'): + """ + @name 使用免费IP库获取IP地区 + @login_type 登录类型 panel:宝塔面板登录, ssh:ssh登录 + """ + # 判断是否开启免费IP库 + if os.path.exists('{}/data/{}_login_area.pl'.format(get_panel_path(), 'btpanel')): + return False, {} + login_ip_area = '' + ip_info = get_free_ips_area([login_ip]) + if not login_ip in ip_info: + return False, {} + + ip_info = ip_info[login_ip] + if not 'city' in ip_info: + login_ip_area = ip_info['info'] + status = True + s_conf = '{}/data/{}_login_area.json'.format(get_panel_path(), login_type) + data = {} + try: + data = json.loads(readFile(s_conf)) + except: + pass + if not login_ip_area and 'city' in ip_info: + city = ip_info['city'] + login_ip_area = ip_info['info'] + if len(city) >= 1 and not city in data: + data[city] = 0 + if data[city] < 3: + if city == 'Local': + login_ip_area += '(Intranet)' + else: + login_ip_area += '(Abnormal login)' + data[city] += 1 + writeFile(s_conf, json.dumps(data)) + data['login_ip_area'] = login_ip_area + return status, data + + +# 登陆告警 +def login_send_body(is_type, username, login_ip, port): + send_type = "" + panel_path = get_panel_path() + login_send_type_conf = "/www/server/panel/data/panel_login_send.pl" + if os.path.exists(login_send_type_conf): + send_type = ReadFile(login_send_type_conf).strip() + else: + # 兼容之前的 + if os.path.exists("/www/server/panel/data/login_send_type.pl"): + send_type = readFile("/www/server/panel/data/login_send_type.pl") + else: + if os.path.exists('/www/server/panel/data/login_send_mail.pl'): + send_type = "mail" + if os.path.exists('/www/server/panel/data/login_send_dingding.pl'): + send_type = "dingding" + + # 增加异地登录告警 + server_ip_area = login_ip + ":" + port + login_aera_status, login_aera = free_login_area(login_ip=server_ip_area, login_type='panel') + if login_aera_status: + login_ip_area = ">Location:" + login_aera['login_ip_area'] + # 如果存在归属地则修改日志内容 + time.sleep(0.2) + if cache_get(server_ip_area): + id = cache_get(server_ip_area) + logs = M("logs").where("id=?", id).getField("log") + data = M("logs").where("id=?", id).setField("log", logs + login_ip_area) + else: + login_ip_area = '' + + add_security_logs('login successful', server_ip_area + login_ip_area, False) + if not send_type: + return False + + object = init_msg(send_type.strip()) + if not object: return + + send_login_white = '{}/data/send_login_white.json'.format(panel_path) + if check_ip_white(send_login_white, login_ip): + return False + + if send_type == 'sms': + data = {} + data['ip'] = get_server_ip() + data['local_ip'] = get_network_ip() + # 不加内网IP,否则短信模板参数长度超过限制 + ip = "{}(外)".format(data['ip']) + sm_args = {'name': '[' + ip + ']', 'time': time.strftime('%Y-%m-%d %X', time.localtime()), + 'type': '[' + is_type + ']', + 'user': username} + rdata = object.send_msg('login_panel', check_sms_argv(sm_args)) + else: + if login_ip_area: + plist = [ + ">Login type:" + is_type, + ">Account:" + username, + ">IP address:" + login_ip + ":" + port, + login_ip_area, + ">Login status:Success" + ] + else: + plist = [ + ">Login type:" + is_type, + ">Account:" + username, + ">IP address:" + login_ip + ":" + port, + ">Login status:Success" + ] + info = get_push_info("Panel Login Alert", plist) + object.push_data(info) + + # if send_type == "dingding": + # msg = "#### 堡塔登录提醒\n\n > 服务器 :"+get_ip()+"\n\n > 登录方式:"+is_type+"\n\n > 登录账号:"+username+"\n\n > 登录IP:"+login_ip+":"+port+"\n\n > 登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime())+'\n\n > 登录状态: 成功' + # send_dingding(msg, False) + # elif send_type == "weixin": + # msg = "#### 堡塔登录提醒\n\n > 服务器 :"+get_ip()+"\n\n > 登录方式:"+is_type+"\n\n > 登录账号:"+username+"\n\n > 登录IP:"+login_ip+":"+port+"\n\n > 登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime())+'\n\n > 登录状态: 成功' + # send_weixin(msg, False) + # elif send_type == "feishu": + # msg = "堡塔登录提醒\n > 服务器 :"+get_ip()+"\n > 登录方式:"+is_type+"\n > 登录账号:"+username+"\n > 登录IP:"+login_ip+":"+port+"\n > 登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime())+'\n > 登录状态: 成功' + # send_feishu(msg, False) + + +# 普通模式下调用发送消息【设置登陆告警后的设置】 +# title= 发送的title +# body= 发送的body +# is_logs= 是否记录日志 +# is_type=发送告警的类型 +def send_to_body(title, body, is_logs=False, is_type="aaPanel email alert"): + login_send_mail = "{}/data/login_send_mail.pl".format(get_panel_path()) + login_send_dingding = "{}/data/login_send_dingding.pl".format(get_panel_path()) + if os.path.exists(login_send_mail): + if is_logs: + send_mail(title, body, True, is_type) + send_mail(title, body) + + if os.path.exists(login_send_dingding): + if is_logs: + send_dingding(body, True, is_type) + send_dingding(body) + + +# 普通发送消息 +# send_type= ["mail","dingding"] +# title =发送的头 +# body= 发送消息的内容 +def send_body_words(send_type, title, body): + if send_type == 'mail': + return send_mail(title, body) + if send_type == 'dingding': + return send_dingding(body) + + +def return_is_send_info(): + import send_mail + send_mail22 = send_mail.send_mail() + tongdao = send_mail22.get_settings() + ret = {} + ret['mail'] = tongdao['user_mail']['user_name'] + ret['dingding'] = tongdao['dingding']['dingding'] + return ret + + +def get_sys_path(): + ''' + @name 关键目录 + @author hwliang<2021-06-11> + @return tuple + ''' + a = ['/www', '/usr', '/', '/dev', '/home', '/media', '/mnt', '/opt', '/tmp', '/var'] + c = ['/www/.Recycle_bin/', '/www/backup/', '/www/php_session/', '/www/wwwlogs/', '/www/server/', '/etc/', '/usr/', + '/var/', '/boot/', '/proc/', '/sys/', '/tmp/', '/root/', '/lib/', '/bin/', '/sbin/', '/run/', '/lib64/', + '/lib32/', '/srv/'] + return a, c + + +def check_site_path(site_path): + ''' + @name 检查网站根目录是否为系统关键目录 + @author hwliang<2021-05-31> + @param site_path 网站根目录全路径 + @return bool + ''' + try: + if site_path in ['/', '/usr', '/dev', '/home', '/media', '/mnt', '/opt', '/tmp', '/var']: + return False + whites = ['/www/server/tomcat', '/www/server/stop', '/www/server/phpmyadmin'] + for w in whites: + if site_path.find(w) == 0: return True + a, error_paths = get_sys_path() + site_path = site_path.strip() + if site_path[-1] == '/': site_path = site_path[:-1] + if site_path in a: + return False + site_path += '/' + for ep in error_paths: + if site_path.find(ep) == 0: return False + return True + except: + return False + + +def is_debug(): + debug_file = "{}/data/debug.pl".format(get_panel_path()) + return os.path.exists(debug_file) + + +class PanelError(Exception): + ''' + @name 宝塔通用异常对像 + @author hwliang<2021-06-25> + ''' + + def __init__(self, value): + self.value = value + + def __str__(self): + return ("An error occurred while the panel was running: {}".format(str(self.value))) + + +def sys_path_append(path): + ''' + @name 追加引用路径 + @author hwliang<2021-07-07> + @param path 路径 + @return void + ''' + try: + if not path in sys.path: + sys.path.insert(0, path) + except: + pass + + +def get_sysbit(): + ''' + @name 获取操作系统位数 + @author hwliang<2021-07-07> + @return int 32 or 64 + ''' + import struct + return struct.calcsize('P') * 8 + + +def get_plugin_path(plugin_name=None): + ''' + @name 取指定插件目录 + @author hwliang<2021-07-14> + @param plugin_name 插件名称 不传则返回插件根目录 + @return string + ''' + + root_path = "{}/plugin".format(get_panel_path()) + if not plugin_name: return root_path + return "{}/{}".format(root_path, plugin_name) + + +def get_class_path(): + ''' + @name 取类库所在路径 + @author hwliang<2021-07-14> + @return string + ''' + return "{}/class".format(get_panel_path()) + + +def decode_data(srcBody): + """ + 遍历解码字符串 + """ + arrs = ['utf-8', 'GBK', 'ANSI', 'BIG5'] + for encoding in arrs: + try: + data = srcBody.decode(encoding) + return encoding, data + except: + pass + return False, None + + +def get_logs_path(): + ''' + @name 取日志目录 + @author hwliang<2021-07-14> + @return string + ''' + return '/www/wwwlogs' + + +def get_vhost_path(): + ''' + @name 取虚拟主机目录 + @author hwliang<2021-08-14> + @return string + ''' + return '{}/vhost'.format(get_panel_path()) + + +def get_backup_path(): + ''' + @name 取备份目录 + @author hwliang<2021-07-14> + @return string + ''' + default_backup_path = '/www/backup' + backup_path = M('config').where("id=?", (1,)).getField('backup_path') + if not backup_path: return default_backup_path + if os.path.exists(backup_path): return backup_path + return default_backup_path + + +def get_site_path(): + ''' + @name 取站点默认存储目录 + @author hwliang<2021-07-14> + @return string + ''' + default_site_path = '/www/wwwroot' + site_path = M('config').where("id=?", (1,)).getField('sites_path') + if not site_path: return default_site_path + if os.path.exists(site_path): return site_path + return default_site_path + + +def read_config(config_name, ext_name='json'): + ''' + @name 读取指定配置文件 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param ext_name 配置文件扩展名,默认为json + @return string 如果发生错误,将抛出PanelError异常 + ''' + config_file = "{}/config/{}.{}".format(get_panel_path(), config_name, ext_name) + if not os.path.exists(config_file): + raise PanelError('The specified configuration file {} does not exist'.format(config_name)) + + config_str = readFile(config_file) + if ext_name == 'json': + try: + config_body = json.loads(config_str) + except Exception as ex: + raise PanelError('Configuration files are not standard parsable JSON content!\n{}'.format(ex)) + return config_body + return config_str + + +def save_config(config_name, config_body, ext_name='json'): + ''' + @name 保存配置文件 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param config_body 被保存的内容, ext_name为json,请传入可解析为json的参数类型,如list,dict,int,str等 + @param ext_name 配置文件扩展名,默认为json + @return string 如果发生错误,将抛出PanelError异常 + ''' + + config_file = "{}/config/{}.{}".format(get_panel_path(), config_name, ext_name) + if ext_name == 'json': + try: + config_body = json.dumps(config_body) + except Exception as ex: + raise PanelError('The configuration content cannot be converted to json format!\n{}'.format(ex)) + + return writeFile(config_file, config_body) + + +def get_config_value(config_name, key, default='', ext_name='json'): + ''' + @name 获取指定配置文件的指定配置项 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param key 配置项 + @param default 获不存在则返回的默认值,默认为空字符串 + @param ext_name 配置文件扩展名,默认为json + @return mixed 如果发生错误,将抛出PanelError异常 + ''' + config_data = read_config(config_name, ext_name) + return config_data.get(key, default) + + +def set_config_value(config_name, key, value, ext_name='json'): + ''' + @name 设置指定配置文件的指定配置项 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param key 配置项 + @param value 配置值 + @param ext_name 配置文件扩展名,默认为json + @return mixed 如果发生错误,将抛出PanelError异常 + ''' + config_data = read_config(config_name, ext_name) + config_data[key] = value + return save_config(config_name, config_data, ext_name) + + +def return_data(status, data={}, status_code=None, error_msg=None): + ''' + @name 格式化响应内容 + @author hwliang<2021-07-14> + @param status 状态 + @param data 响应数据 + @param status_code 状态码 + @param error_msg 错误消息内容 + @return dict + + ''' + if status_code == None: + status_code = 1 if status else 0 + if error_msg == None: + error_msg = '' if status else 'unknown error' + + result = { + 'status': status, + "status_code": status_code, + 'error_msg': str(error_msg), + 'data': data + } + return result + + +def return_error(error_msg, status_code=-1, data=[]): + ''' + @name 格式化错误响应内容 + @author hwliang<2021-07-15> + @param error_msg 错误消息 + @param status_code 状态码,默认为-1 + @param data 响应数据 + @return dict + ''' + if not data: data = error_msg + return return_data(False, data, status_code, str(error_msg)) + + +def error(error_msg, status_code=-1, data=[]): + ''' + @name 格式化错误响应内容 + @author hwliang<2021-07-15> + @param error_msg 错误消息 + @param status_code 状态码,默认为-1 + @param data 响应数据 + @return dict + ''' + if not data: data = error_msg + return return_error(error_msg, status_code, data) + + +def success(data=[], status_code=1, error_msg=''): + ''' + @name 格式化成功响应内容 + @author hwliang<2021-07-15> + @param data 响应数据 + @param status_code 状态码,默认为0 + @return dict + ''' + return return_data(True, data, status_code, error_msg) + + +def return_status_code(status_code, format_body, data=[]): + ''' + @name 按状态码返回 + @author hwliang<2021-07-15> + @param status_code 状态码 + @param format_body 错误内容 + @param data 响应数据 + @return dict + ''' + error_msg = get_config_value('status_code', str(status_code)) + if not error_msg: raise PanelError('invalid status_code') + return return_data(error_msg[0], data, status_code, error_msg[1].format(format_body)) + + +def to_dict_obj(data: dict) -> dict_obj: + ''' + @name 将dict转换为dict_obj + @author hwliang<2021-07-15> + @param data 要被转换的数据 + @return dict_obj + ''' + if not isinstance(data, dict): + raise PanelError('parameter error: only support transform dict to dict_obj.') + pdata = dict_obj() + for key in data.keys(): + pdata[key] = data[key] + return pdata + + +def get_script_object(filename): + ''' + @name 从脚本文件获取对像 + @author hwliang<2021-07-19> + @param filename 文件名 + @return object + ''' + _obj = sys.modules.get(filename, None) + if _obj: return _obj + from types import ModuleType + _obj = sys.modules.setdefault(filename, ModuleType(filename)) + _code = readFile(filename) + _code_object = compile(_code, filename, 'exec') + _obj.__file__ = filename + _obj.__package__ = '' + exec(_code_object, _obj.__dict__) + return _obj + + +def check_hooks(): + ''' + @name 自动注册HOOK + @author hwliang<2021-07-19> + @return void + ''' + hooks_path = '{}/hooks'.format(get_panel_path()) + if not os.path.exists(hooks_path): + return + for hook_name in os.listdir(hooks_path): + if hook_name[-3:] != '.py': continue + filename = os.path.join(hooks_path, hook_name) + _obj = get_script_object(filename) + _main = getattr(_obj, 'main', None) + if not _main: continue + _main() + + +def register_hook(hook_index, hook_def): + ''' + @name 注册HOOK + @author hwliang<2021-07-15> + @param hook_index HOOK位置 + @param hook_def HOOK函数对像 + @return void + ''' + from BTPanel import hooks + hook_keys = hooks.keys() + if not hook_index in hook_keys: + hooks[hook_index] = [] + if not hook_def in hooks[hook_index]: + hooks[hook_index].append(hook_def) + + +def exec_hook(hook_index, data): + r''' + @name 执行HOOk + @author hwliang<2021-07-15> + @param hook_index HOOK索引位置,格式限制:^\w+$ + @param data 运行数据 + @return mixed + ''' + + from BTPanel import hooks + hook_keys = hooks.keys() + if not hook_index in hook_keys: + return data + + for hook_def in hooks[hook_index]: + data = hook_def(data) + return data + + +def get_hook_index(mod_name, def_name): + ''' + @name 获取HOOK位置 + @author hwliang<2021-07-19> + @param mod_name 模块名称 + @param def_name 方法名称 + @return tuple + ''' + mod_name = mod_name.upper() + def_name = def_name.upper() + last_index = '{}_{}_LAST'.format(mod_name, def_name) + end_index = '{}_{}_END'.format(mod_name, def_name) + return last_index, end_index + + +def flush_plugin_list(): + ''' + @name 刷新插件列表 + @author hwliang<2021-07-22> + @return bool + ''' + skey = 'TNaMJdG3mDHKRS6Y' + from BTPanel import cache + if cache.get(skey): cache.delete(skey) + load_soft_list() + return True + + +def get_session_timeout(): + ''' + @name 获取session过期时间 + @author hwliang<2021-07-28> + @return int + ''' + from BTPanel import cache + skey = 'session_timeout' + session_timeout = cache.get(skey) + if not session_timeout == None: return session_timeout + + sess_out_path = '{}/data/session_timeout.pl'.format(get_panel_path()) + session_timeout = 86400 + if not os.path.exists(sess_out_path): + return session_timeout + try: + session_timeout = int(readFile(sess_out_path)) + except: + session_timeout = 86400 + cache.set(skey, session_timeout, 3600) + return session_timeout + + +def get_login_token_auth(): + ''' + @name 获取登录token + @author hwliang<2021-07-28> + @return string + ''' + from BTPanel import cache + skey = 'login_token' + login_token = cache.get(skey) + if not login_token == None: return login_token + + login_token_file = '{}/data/login_token.pl'.format(get_panel_path()) + login_token = '1234567890' + if not os.path.exists(login_token_file): + return login_token + login_token = readFile(login_token_file) + cache.set(skey, login_token, 3600) + return login_token + + +def listen_ipv6(): + ''' + @name 是否监听ipv6 + @author hwliang<2021-08-12> + @return bool + ''' + ipv6_file = '{}/data/ipv6.pl'.format(get_panel_path()) + return os.path.exists(ipv6_file) + + +def get_panel_log_file(): + ''' + @name 获取panel日志文件 + @author hwliang<2021-08-12> + @return string + ''' + return "{}/logs/error.log".format(get_panel_path()) + + +def print_log(_info, _level='DEBUG'): + ''' + @name 写入日志 + @author hwliang<2021-08-12> + @param _info 要写入到日志文件的信息 + @param _level 日志级别 + @return void + ''' + if type(_info) == dict: + _info = json.dumps(_info) + log_body = "[{}][{}] - {}\n".format(format_date(), _level.upper(), _info) + return WriteFile(get_panel_log_file(), log_body, 'a+') + + +def print_error(): + ''' + @name 打印错误信息到日志文件 + @author hwliang + @return void + ''' + print_log(get_error_info(), 'ERROR') + + +def to_date(format="%Y-%m-%d %H:%M:%S", times=None): + ''' + @name 格式时间转时间戳 + @author hwliang<2021-08-17> + @param format 时间格式 + @param times 时间 + @return int + ''' + if times: + if isinstance(times, int): return times + if isinstance(times, float): return int(times) + if is_number(times): return int(times) + else: + return 0 + ts = time.strptime(times, format) + return time.mktime(ts) + + +def get_glibc_version(): + ''' + @name 获取glibc版本 + @author hwliang<2021-08-17> + @return string + ''' + try: + cmd_result = ExecShell("ldd --version")[0] + if not cmd_result: return '' + glibc_version = cmd_result.split("\n")[0].split()[-1] + except: + return '' + return glibc_version + + +def is_apache_nginx(): + ''' + @name 是否是apache或nginx + @author hwliang<2021-08-17> + @return bool + ''' + setup_path = get_setup_path() + return os.path.exists(setup_path + '/apache') or os.path.exists(setup_path + '/nginx') + + +def error_not_login(e=None, _src=None): + ''' + @name 未登录时且未输入正确的安全入口时的响应 + @author hwliang<2021-12-16> + @return Response + ''' + from BTPanel import Response, render_template, redirect, request + client_status = check_client_info() + x_http_token = request.headers.get('x-http-token') + if client_status == 1: + if x_http_token: + # result = {"status": False, "code": -8888, "redirect": get_admin_path(), + # "msg": "The current login session has been invalid, please login again!"} + + # 修改为aapanel通用返回方式 + result = { + "status": -1, + "timestamp": int(time.time()), + "message": { + "msg": "The current login session has been invalid, please login again!", + "redirect": get_admin_path() + } + } + + return Response(json.dumps(result), mimetype='application/json', status=200) + return redirect(get_admin_path()) + elif client_status == 2: + if x_http_token: + # result = {"status": False, "code": -8888, "redirect": "/login", + # "msg": "The current login session has been invalid, please login again!"} + + # 修改为aapanel通用返回方式 + result = { + "status": -1, + "timestamp": int(time.time()), + "message": { + "msg": "The current login session has been invalid, please login again!", + "redirect": "/login" + } + } + + return Response(json.dumps(result), mimetype='application/json', status=200) + return render_template('autherr.html') + + try: + abort_code = read_config('abort') + if not abort_code in [None, 1, 0, '0', '1']: + if abort_code == 404: return error_404(e) + if abort_code == 403: return error_403(e) + return Response(status=int(abort_code)) + except: + pass + + if e in ['/login']: + return redirect(e) + + if _src: + return e + else: + return error_404(e) + + +def error_403(e): + from BTPanel import Response, session + # if not session.get('login',None): return error_not_login() + errorStr = ''' +403 Forbidden + +

                                    403 Forbidden

                                    +
                                    nginx
                                    + +''' + headers = { + "Content-Type": "text/html" + } + return Response(errorStr, status=403, headers=headers) + + +def error_404(e): + from BTPanel import Response, session + # if not session.get('login',None): return error_not_login() + errorStr = ''' +404 Not Found + +

                                    404 Not Found

                                    +
                                    nginx
                                    + +''' + headers = { + "Content-Type": "text/html" + } + return Response(errorStr, status=404, headers=headers) + + +def error_401(e): + from BTPanel import Response, session + # if not session.get('login',None): return error_not_login() + errorStr = ''' +401 Unauthorized + +

                                    401 Unauthorized

                                    +
                                    You must enter a valid login ID and password to access this page.
                                    + +''' + headers = { + "Content-Type": "text/html" + } + return Response(errorStr, status=401, headers=headers) + + +def get_password_config(): + ''' + @name 获取密码安全配置 + @author hwliang<2021-10-18> + @return int + ''' + import config + return config.config().get_password_config(None) + + +def password_expire_check(): + ''' + @name 密码过期检查 + @author hwliang<2021-10-18> + @return bool + ''' + p_config = get_password_config() + if p_config['expire'] == 0: return True + if time.time() > p_config['expire_time']: return False + return True + + +def stop_status_mvore(): + flag = False + try: + nginx_path = '/www/server/panel/vhost/nginx/btwaf.conf' + if os.path.exists(nginx_path): + ExecShell('mv %s %s.bak' % (nginx_path, nginx_path)) + flag = True + nginx_path = '/www/server/panel/vhost/nginx/free_waf.conf' + if os.path.exists(nginx_path): + ExecShell('mv %s %s.bak' % (nginx_path, nginx_path)) + flag = True + apache_path = '/www/server/panel/vhost/apache/btwaf.conf' + if os.path.exists(apache_path): + ExecShell('chattr -i %s && mv %s %s.bak' % (apache_path, apache_path, apache_path)) + flag = True + if flag: + serviceReload() + except: + pass + + +def is_error_path(): + if os.path.exists("/www/server/panel/data/error_pl.pl"): + stop_status_mvore() + return True + return False + + +def get_php_versions(reverse=False): + ''' + @name 取PHP版本列表 + @author hwliang<2021-12-16> + @param reverse 是否降序 + @return list + ''' + _file = get_panel_path() + '/config/php_versions.json' + if os.path.exists(_file): + version_list = json.loads(readFile(_file)) + else: + version_list = ['52', '53', '54', '55', '56', '70', '71', '72', '73', '74', '80', '81', '82', '83', '84'] + + return sorted(version_list, reverse=reverse) + + +def get_full_session_file(): + ''' + @name 获取临时SESSION文件 + @author hwliang<2021-12-28> + @return string + ''' + from BTPanel import app + full_session_key = app.config['SESSION_KEY_PREFIX'] + get_session_id() + sess_path = get_panel_path() + '/data/session/' + return sess_path + '/' + md5(full_session_key) + + +def install_mysql_client(): + ''' + @name 安装mysql客户端 + @author hwliang<2022-01-14> + @return void + ''' + if os.path.exists('/usr/bin/yum'): + os.system("yum install mariadb -y") + if not os.path.exists('/usr/bin/mysql'): + os.system("yum reinstall mariadb -y") + elif os.path.exists('/usr/bin/apt-get'): + os.system('apt-get install mariadb-client -y') + if not os.path.exists('/usr/bin/mysql'): + os.system('apt-get reinstall mariadb-client* -y') + + +def get_mysqldump_bin(): + ''' + @name 获取mysqldump路径 + @author hwliang<2022-01-14> + @return string + ''' + bin_files = [ + '{}/mysql/bin/mysqldump'.format(get_setup_path()), + '/usr/bin/mysqldump', + '/usr/local/bin/mysqldump', + '/usr/sbin/mysqldump', + '/usr/local/sbin/mysqldump' + ] + + for bin_file in bin_files: + if os.path.exists(bin_file): + return bin_file + + install_mysql_client() + + for bin_file in bin_files: + if os.path.exists(bin_file): + return bin_file + + return bin_files[0] + + +def get_mysql_bin(): + ''' + @name 获取mysql路径 + @author hwliang<2022-01-14> + @return string + ''' + bin_files = [ + '{}/mysql/bin/mysql'.format(get_setup_path()), + '/usr/bin/mysql', + '/usr/local/bin/mysql', + '/usr/sbin/mysql', + '/usr/local/sbin/mysql' + ] + + for bin_file in bin_files: + if os.path.exists(bin_file): + return bin_file + + install_mysql_client() + + for bin_file in bin_files: + if os.path.exists(bin_file): + return bin_file + return bin_files[0] + + +def error_conn_cloud(text): + ''' + @name 连接云端失败 + @author hwliang<2021-12-18> + @return void + ''' + code_msg = '' + if text.find("502 Bad Gateway") != -1: + code_msg = '502 Bad Gateway' + if text.find("504 Bad Gateway") != -1: + code_msg = '504 Bad Gateway' + elif text.find("Connection refused") != -1: + code_msg = 'Connection refused' + elif text.find("Connection timed out") != -1: + code_msg = 'Connection timed out' + elif text.find("Connection reset by peer") != -1: + code_msg = 'Connection reset by peer' + elif text.find("Name or service not known") != -1: + code_msg = 'Name or service not known' + elif text.find("No route to host") != -1: + code_msg = 'No route to host' + elif text.find("No such file or directory") != -1: + code_msg = 'No such file or directory' + elif text.find("404 Not Found") != -1: + code_msg = '404 Not Found' + elif text.find("403 Forbidden") != -1: + code_msg = '403 Forbidden' + elif text.find("401 Unauthorized") != -1: + code_msg = '401 Unauthorized' + elif text.find("400 Bad Request") != -1: + code_msg = '400 Bad Request' + elif text.find("Remote end closed connection without response") != -1: + code_msg = 'Remote end closed connection' + err_template_file = '{}/BTPanel/templates/default/error_connect.html'.format(get_panel_path()) + msg = readFile(err_template_file) + msg = msg.format(code=code_msg) + return PanelError(msg) + + +def get_mountpoint_list(): + ''' + @name 获取挂载点列表 + @author hwliang<2021-12-18> + @return list + ''' + import psutil + mount_list = [] + for mount in psutil.disk_partitions(): + mountpoint = mount.mountpoint if mount.mountpoint[-1] == '/' else mount.mountpoint + '/' + mount_list.append(mountpoint) + # 根据挂载点字符长度排序 + mount_list.sort(key=lambda i: len(i), reverse=True) + return mount_list + + +def get_path_in_mountpoint(path): + ''' + @name 获取文件或目录目录所在挂载点 + @author hwliang<2022-03-30> + @param path 文件或目录路径 + @return string + ''' + # 判断是否是绝对路径 + if path.find('./') != -1 or path[0] != '/': raise PanelError("cannot use relative path") + if not path: raise PanelError("path cannot be empty") + + # 在目录尾加/ + if os.path.isdir(path): + path = path if path[-1] == '/' else path + '/' + + # 匹配挂载点 + mount_list = get_mountpoint_list() + for mountpoint in mount_list: + if path.startswith(mountpoint): + return mountpoint + + # 没有匹配到挂载点 + return '/' + + +def get_recycle_bin_path(path): + ''' + @name 获取指定文件或目录的回收站路径 + @author hwliang<2022-03-30> + @param path 文件或目录路径 + @return string + ''' + mountpoint = get_path_in_mountpoint(path) + recycle_bin_path = '{}/.Recycle_bin/'.format(mountpoint) + try: + if not os.path.exists(recycle_bin_path): + os.mkdir(recycle_bin_path, 384) + except: + return '/www/.Recycle_bin/' + return recycle_bin_path + + +def get_recycle_bin_list(): + ''' + @name 获取回收站列表 + @author hwliang<2022-03-30> + @return list + ''' + # 旧的回收站重命名为.Recycle_bin + default_path = '/www/.Recycle_bin' + default_path_src = '/www/Recycle_bin' + if os.path.exists(default_path_src) and not os.path.exists(default_path): + try: + os.rename(default_path_src, default_path) + except: + ExecShell("mv {} {}".format(default_path_src, default_path)) + + if not os.path.exists(default_path): + os.makedirs(default_path, 384) + + # 获取回收站列表 + recycle_bin_list = [] + mtime_list = [] # 修改时间 + for mountpoint in get_mountpoint_list(): + recycle_bin_path = '{}.Recycle_bin/'.format(mountpoint) + + try: + if not os.path.exists(recycle_bin_path): + os.mkdir(recycle_bin_path, 384) + if not os.path.exists(recycle_bin_path): continue + mtime = os.path.getmtime(recycle_bin_path) + if mtime in mtime_list: continue # 通过修改时间去重 + mtime_list.append(mtime) + recycle_bin_list.append(recycle_bin_path) + except: + continue + + # 包含默认回收站路径? + if not default_path + '/' in recycle_bin_list: + recycle_bin_list.append(default_path + '/') + + return recycle_bin_list + + +def check_password(password): + """ + 密码强度: + 0 弱 + 1 中 + 2 强 + """ + l = 0 + low = False + up = False + symbol = False + digit = False + p_len = len(password) + if p_len < 8: + return l + for i in password: + if i.islower(): + low = True + if i.isupper(): + up = True + if i in ['~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '=', '+', '<', '>', ',', '.', '/', + '"', '|', '\\', "'", '?']: + symbol = True + if i.isdigit(): + digit = True + # 判断重复出现 + tmp = len(set([i for i in password])) + if tmp >= 2: + if low and up and symbol and digit: + l = 2 + if p_len >= 11: + l = 1 + return l + + +def set_module_logs(mod_name, fun_name, count=1): + """ + @模块使用次数 + @mod_name 模块名称 + @fun_name 函数名 + """ + import datetime + data = {} + path = '{}/data/mod_log.json'.format(get_panel_path()) + if os.path.exists(path): + try: + data = json.loads(readFile(path)) + except: + pass + + if type(data) != dict: + data = {} + + key = datetime.datetime.now().strftime("%Y-%m-%d") + if not key in data: data[key] = {} + + if not mod_name in data[key]: + data[key][mod_name] = {} + + if not fun_name in data[key][mod_name]: + data[key][mod_name][fun_name] = 0 + + data[key][mod_name][fun_name] += count + + writeFile(path, json.dumps(data)) + return True + + +headers_filter_rules = None + + +def filter_headers(): + ''' + @name 过滤请求头 + @author hwliang<2021-12-18> + @return dict + ''' + global headers_filter_rules + + # 预编译过滤规则 + if not headers_filter_rules: + headers_filter_rules = { + 'host': re.compile(r'^[\w\.\-\:]+$'), + 'accept': re.compile(r'^[\w\s\.\-\*\/\,\=\;\+]+$'), + 'accept-encoding': re.compile(r'^[\w\s\.\-\*\/\,]+$'), + 'accept-language': re.compile(r'^[\w\s\.\-\*\/\,\=\:\;]+$'), + 'cache-control': re.compile(r'^[\w\s\.\-\=\;]+$'), + 'connection': re.compile(r'^[\w\s\.\-]+$'), + 'content-length': re.compile(r'^[\d]+$'), + 'cookie': re.compile(r'^[\w\s\=\%\+\&\;\:\@\$\,\.\-\_\*\/\?\!\~\#]+$'), + 'origin': re.compile(r'^(http|https)://[\w\.\-\?\=\&\/\:]+$'), + 'pragma': re.compile(r'^[\w\s\.\-]+$'), + 'referer': re.compile(r'^(http|https)://[\w\.\-\?\=\&\/\:\%\#\~\!\*\+\@]+$'), + 'user-agent': re.compile(r'^[\w\s\.\-\*\/\,\(\)\=\+\;\:\@\$\,\.\-\_\~\#]+$'), + 'x-cookie-token': re.compile(r'^\w+$'), + 'x-http-token': re.compile(r'^\w+$'), + 'X-KL-Ajax-Request': re.compile(r'^\w+$'), + 'X-Requested-With': re.compile(r'^\w+$') + } + from flask import request + headers = request.headers + skeys = headers_filter_rules.keys() + + for k in skeys: + v = headers.get(k, None) + if not v: continue + if not headers_filter_rules[k].match(v): + return False + return True + + +def trim(data): + """ + @去除所有空格 + """ + return data.replace(' ', '').strip() + + +def get_os(_os='windows'): + """ + @验证系统版本 + """ + src_os = 'windows' + if os.path.exists('/www/server/panel'): src_os = 'linux' + if src_os == _os: return True + return False + + +def get_file_list(path, flist): + """ + 递归获取目录所有文件列表 + @path 目录路径 + @flist 返回文件列表 + """ + if os.path.exists(path): + files = os.listdir(path) + flist.append(path) + for file in files: + if os.path.isdir(path + '/' + file): + get_file_list(path + '/' + file, flist) + else: + flist.append(path + '/' + file) + + +def writeFile2(filename, s_body, mode='w+'): + """ + 写入字节文件内容 + @filename 文件名 + @s_body 欲写入的内容 + + """ + try: + fp = open(filename, mode); + fp.write(s_body) + fp.close() + return True + except: + return False + + +def check_obj_upgrade(_obj, filename=None): + ''' + @name 检查指定模块是否修改 + @author hwliang + @param 文件名 + @param 模块对象 + @return void + ''' + + # 引用缓存 + try: + from BTPanel import cache + except: + return + + # 是否传递文件名? + if not filename: + filename = _obj.__file__ + + # 获取文件修改时间 + skey = "obj_up_{}".format(md5(filename)) + mtime = os.path.getmtime(filename) # 当前 + old_mtime = cache.get(skey) # 旧的 + + # 直接设置当前修改时间 + if not old_mtime: + cache.set(skey, mtime) + return + + # 检查是否修改 + if old_mtime == mtime: + return + + # 重新加载模块 + import importlib + importlib.reload(_obj) + cache.set(skey, mtime) + + +def version_to_tuple(version): + ''' + @name 将版本号转为元组 + @version 字符串版本号 + @return 元组版本号 + ''' + if not version: + return () + if not isinstance(version, str): + return version + version = re.sub(r"[^\.\d]+", "", version) + version = version.split('.') + version = tuple(map(int, version)) + return version + + +def set_search_history(mod_name, key, val): + """ + @保存搜索历史 + @mod_name 模块名称 + @key 关键字 + @val string 搜索内容 + """ + if not val: return False + + max = 10 + p_file = get_panel_path() + m_file = p_file + '/data/search.limit' + d_file = p_file + '/data/search.json' + try: + sdata = int(readFile(m_file)) + if sdata: max = sdata + except: + pass + + result = {} + try: + result = json.loads(readFile(d_file)) + except: + pass + + if not mod_name in result: result[mod_name] = {} + if not key in result[mod_name]: result[mod_name][key] = [] + + n_list = [] + for item in result[mod_name][key]: + if item['val'].strip() != val.strip(): n_list.append(item) + + n_list.append({'val': val, 'time': int(time.time())}) + + result[mod_name][key] = n_list[len(n_list) - max:] + + writeFile(d_file, json.dumps(result)) + return True + + +def get_search_history(mod_name, key): + """ + @获取搜索历史 + @mod_name string 模块名称 + @key string 关键字 + """ + print(mod_name, key) + result = [] + d_file = get_panel_path() + '/data/search.json' + try: + result = json.loads(readFile(d_file))[mod_name][key] + except: + pass + + result = sorted(result, key=lambda x: x['time'], reverse=True) + + return result + + +def set_dir_history(mod_name, key, val): + """ + @设置目录打开历史 + @mod_name string 模块名称 + @key string 函数名 + @val string 路径 + """ + if not val: return False + + max = 10 + result = {} + d_file = get_panel_path() + '/data/dir_history.json' + try: + result = json.loads(readFile(d_file)) + except: + pass + + if not mod_name in result: result[mod_name] = {} + if not key in result[mod_name]: result[mod_name][key] = [] + + data = result[mod_name][key] + for info in data: + if val.find(info['val']) >= 0: + if time.time() - info['time'] < 15: + data.remove(info) + + data.append({'val': val, 'time': int(time.time())}) + result[mod_name][key] = data[0:max] + writeFile(d_file, json.dumps(result)) + return True + + +def get_dir_history(mod_name, key): + """ + @获取目录打开历史 + @mod_name string 模块名称 + @key string 关键字 + """ + result = [] + d_file = get_panel_path() + '/data/dir_history.json' + try: + result = json.loads(readFile(d_file))[mod_name][key] + except: + pass + return result + + +def get_run_pip(): + pass + + +def install_pip(shell): + """ + @name 安装pip模块 + @author cjxin + @param shell 安装命令 + """ + if get_os('windows'): + os.system(get_run_pip(shell.replace('pip', '[PIP]'))) + else: + os.system(shell.replace('pip', 'btpip')) + + +def is_domain(domain): + """ + @验证是否域名 + """ + reg = r"^([\w\-\*]{1,100}\.){1,10}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$"; + if re.match(reg, domain): return True + return False + + +def init_msg(module): + """ + 初始化消息通道 + @module 消息通道模块名称 + """ + import os, sys + if not os.path.exists('class/msg'): os.makedirs('class/msg') + panelPath = get_panel_path() + + sfile = 'class/msg/{}_msg.py'.format(module) + if not os.path.exists(sfile): return False + sys.path.insert(0, "{}/class/msg".format(panelPath)) + + msg_main = __import__('{}_msg'.format(module)) + try: + mod_reload(msg_main) + except: + pass + return eval('msg_main.{}_msg()'.format(module)) + + +def push_argv(msg): + """ + @处理短信参数,否则会被拦截 + """ + if is_ipv4(msg): + tmp1 = msg.split('.') + msg = '{}.***.***.{}'.format(tmp1[0], tmp1[3]) + else: + if is_domain(msg): + msg = msg.replace('.', '_') + return msg + + +def check_sms_argv(data): + """ + @批量处理短信参数,否则会被拦截 + """ + for key in data: + val = data[key] + if type(val) == str: + data[key] = push_argv(val) + return data + + +""" +@获取推送ip +""" + + +def get_push_address(): + ip = push_argv(GetLocalIp()) + return ip + + +def get_ips_area(ips): + ''' + @name 获取ip地址所在地 + @author cjxin + @param ips + @return list + ''' + import PluginLoader + + args = dict_obj() + args.model_index = 'safe' + args.ips = ips + + res = PluginLoader.module_run("ips", "get_ip_area", args) + return res + + +def return_area(result, key): + """ + @name 格式化返回带IP归属地的数组 + @param result 数据数组 + @param key ip所在字段 + @return list + """ + tmps = [] + for data in result: + data['area'] = '' + tmps.append(data[key]) + + res = get_ips_area(tmps) + if 'status' in res: return result + + for data in result: + if data[key] in res: + data['area'] = res[data[key]] + return result + + +def get_network_ip(): + """ + @name 获取本机ip + @return string + """ + + import socket + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + return ip + finally: + s.close() + return '127.0.0.1' + + +def get_server_ip(): + """ + @获取服务器外网ip + """ + + user_file = '{}/data/userInfo.json'.format(get_panel_path()) + if os.path.exists(user_file): + try: + userTmp = json.loads(readFile(user_file)) + return userTmp['address'] + except: + pass + + return GetLocalIp() + + +def get_push_info(title, slist=[]): + """ + @name 获取推送信息 + @param title 推送标题 + @param slist 推送追加的列表 + 如:slist = ['>发送内容:xxx'] + @return dict + """ + data = {} + data['title'] = title + data['ip'] = get_server_ip() + + data['local_ip'] = get_network_ip() + data['time'] = format_date() + data['server_name'] = GetConfigValue('title') + + dlist = [ + "#### {}".format(data['title']), + ">ServerHost: " + data['server_name'], + ">IP Address: {}(Internet) {}(Internal)".format(data['ip'], data['local_ip']), + ">Send Time: " + data['time'] + ] + dlist.extend(slist) + msg = "\n\n".join(dlist) + data['msg'] = msg + data['list'] = dlist + return data + + +def write_push_log(module, msg, res): + """ + @name 写推送日志 + @module string 模块名称 + @msg string 消息内容 + @res dict 推送结果 + """ + user = '' + for key in res: + status = 'Success' + if res[key] == 0: status = 'Fail' + user += '[ {}:{} ] '.format(key, status) + + if not user: user = '[ Default ] ' + try: + msg_obj = init_msg(module) + if msg_obj: module = msg_obj.get_version_info(None)['title'] + except: + pass + + log = 'Title:[{}],method to informe:[{}],recipient:{}'.format(xsssec(msg), module, user) + WriteLog('Alarm notification', log) + return True + + +def push_msg(module, data): + """ + @name 推送消息 + @param module 模块名称 + @param msg 消息内容 + @return dict + """ + msg_obj = init_msg(module) + if not msg_obj: + returnMsg(False, 'Module {} does not exist!'.format(module)) + + res = msg_obj.push_data(data) + return res + + +def check_chinese(data): + """ + @name 判断字符串是否包含中文 + """ + if re.search(u'[\u4e00-\u9fa5]', data): + return True + return False + + +def is_ssl(): + ''' + @name 是否开启SSL + @author hwliang + @return bool + ''' + return os.path.exists(get_panel_path() + '/data/ssl.pl') + + +def get_cookie(key, default=None): + ''' + @name 获取指定Cookie值 + @author hwliang + @param key Cookie键 + @param default 默认值 + @return str + ''' + from flask import request + return request.cookies.get(key, default) + + +def get_csrf_cookie_token_key(): + ''' + @name 获取CSRF Cookie Key + @author hwliang + @return string + ''' + if is_ssl(): + token_key = 'request_token' + else: + token_key = 'request_token' + return token_key + + +def get_csrf_cookie_token_value(): + ''' + @name 获取CSRF Cookie Value + @author hwliang + @return string + ''' + token_key = get_csrf_cookie_token_key() + return get_cookie(token_key) + + +def get_csrf_html_token_key(): + ''' + @name 获取CSRF HTML Key + @author hwliang + @return string + ''' + if is_ssl(): + token_key = 'request_token_head' + else: + token_key = 'request_token_head' + return token_key + + +def get_csrf_html_token_value(): + ''' + @name 获取CSRF HTML Value + @author hwliang + @return string + ''' + token_key = get_csrf_html_token_key() + return get_cookie(token_key) + + +def get_csrf_sess_html_token_value(): + ''' + @name 从SESSION获取CSRF HTML value + @author hwliang + @return string + ''' + from flask import session + return session.get(get_csrf_html_token_key(), "") + + +def get_csrf_sess_cookie_token_value(): + ''' + @name 从SESSION获取CSRF Cookie value + @author hwliang + @return string + ''' + from flask import session + return session.get(get_csrf_cookie_token_key(), "") + + +def get_sys_install_bin(): + ''' + @name 获取系统包管理器命令 + @author hwliang + @return string + ''' + install_bins = ['/usr/bin/yum', '/usr/bin/apt-get', '/usr/bin/dnf'] + for bin in install_bins: + if os.path.exists(bin): + return bin + return '' + + +def get_firewall_status(): + ''' + @name 获取系统防火墙状态 + @author hwliang + @return int 0.关闭 1.开启 -1.未安装 + ''' + import psutil + firewall_files = {'/usr/sbin/firewalld': "pid", '/usr/bin/firewalld': "pid", + '/usr/sbin/ufw': "/usr/sbin/ufw status|grep 'Status: active'", + '/sbin/ufw': "/sbin/ufw status |grep 'Status: active'", + '/usr/sbin/iptables': "service iptables status|grep 'Chain INPUT'"} + for f in firewall_files.keys(): + if not os.path.exists(f): continue + _cmd = firewall_files[f] + if _cmd != "pid": + res = ExecShell(_cmd) + if res[0].strip(): + return 1 + else: + return 0 + for pid in psutil.pids(): + try: + p = psutil.Process(pid) + if f in p.cmdline(): + return 1 + except: + pass + return 0 + return -1 + + +def get_panel_port(): + ''' + @name 获取面板端口 + @author hwliang + @return int + ''' + port_file = '{}/data/port.pl'.format(get_panel_path()) + if not os.path.exists(port_file): + return 8888 + try: + return int(readFile(port_file)) + except: + return 8888 + + +def install_sys_firewall(): + ''' + @name 安装系统防火墙 + @author hwliang + @return bool + ''' + if get_firewall_status() != -1: return True + + install_bin = get_sys_install_bin() + if not install_bin: return False + if install_bin.find('apt-get') != -1: + ExecShell("{} install -y ufw".format(install_bin)) + if get_firewall_status() != -1: + _cmd = '''ufw allow 20/tcp +ufw allow 21/tcp +ufw allow 22/tcp +ufw allow 80/tcp +ufw allow 443/tcp +ufw allow ${panelPort}/tcp +ufw allow ${sshPort}/tcp +ufw allow 39000:40000/tcp +ufw_status=`ufw status` +echo y|ufw enable +ufw default deny +ufw reload +'''.format(panelPort=get_panel_port(), sshPort=get_ssh_port()) + ExecShell(_cmd) + elif install_bin.find('yum') != -1 or install_bin.find('dnf') != -1: + ExecShell("{} install -y firewalld".format(install_bin)) + if get_firewall_status() != -1: + _cmd = '''systemctl enable firewalld + systemctl start firewalld + firewall-cmd --set-default-zone=public > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port=20/tcp > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port=21/tcp > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port=22/tcp > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port=80/tcp > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port=443/tcp > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port={panelPort}/tcp > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port={sshPort}/tcp > /dev/null 2>&1 + firewall-cmd --permanent --zone=public --add-port=39000-40000/tcp > /dev/null 2>&1 + firewall-cmd --reload +'''.format(panelPort=get_panel_port(), sshPort=get_ssh_port()) + ExecShell(_cmd) + + return False + + +def check_firewall_rule(port): + ''' + @name 检测防火墙是否已经添加规则 + @author cjxin + @param port int 端口号 + ''' + + args = dict_obj() + args.model_index = 'safe' + args.port = port + + import PluginLoader + res = PluginLoader.module_run("firewall", "check_firewall_rule", args) + return res + + +def add_firewall_rule(port, protocol='tcp', types='accept', address='0.0.0.0/0', brief=None): + """ + @name 添加防火墙规则 + @author cjxin + @param port int 端口号 + @param protocol string 协议类型 tcp udp + @param types string 添加类型 accept reject + @param address string 地址 + @param brief string 描述 + """ + + args = dict_obj() + args.model_index = 'safe' + args.port = port + args.protocol = protocol + args.types = types + args.address = address + args.brief = brief + if not brief: args.brief = str(port) + + import PluginLoader + res = PluginLoader.module_run("firewall", "create_rules", args) + return res + + +def del_firewall_rule(port, protocol='tcp', types='accept', address='0.0.0.0/0'): + ''' + @name 删除防火墙规则 + @author cjxin + @param port int 端口号 + @param protocol str 协议 + @param types str 类型 + @param address str 地址 + ''' + + args = dict_obj() + args.model_index = 'safe' + args.port = port + args.protocol = protocol + args.types = types + args.address = address + import PluginLoader + res = PluginLoader.module_run("firewall", "remove_rules", args) + return res + + +def is_aarch(): + ''' + @name 是否是arm架构 + @author hwliang + @return bool + ''' + uname = None + if hasattr(os, 'uname'): uname = os.uname() + aarch_list = ['aarch64', 'aarch'] + try: + return uname.machine in aarch_list + except: + if uname: + return uname[-1] in aarch_list + return False + + +def is_process_exists_by_cmdline(_cmd): + ''' + @name 根据命令行参数查找进程是否存在 + @author hwliang + @param _cmd 命令行 + @return bool + ''' + if isinstance(_cmd, str): + _cmd = [_cmd] + if not isinstance(_cmd, list): + return False + for pid in psutil.pids(): + try: + p = psutil.Process(pid) + cmd_line = p.cmdline() + for _c in _cmd: + if _c in cmd_line: + return True + except: + continue + return False + + +def is_process_exists_by_exe(_exe): + ''' + @name 根据执行文件路径查找进程是否存在 + @author hwliang + @param _exe 命令行 + @return bool + ''' + if isinstance(_exe, str): + _exe = [_exe] + if not isinstance(_exe, list): + return False + + for process in psutil.process_iter(): + try: + _exe_bin = process.exe() + for _e in _exe: + if _exe_bin.find(_e) != -1: return True + except: + continue + return False + + +def is_process_exists_by_name(_name): + ''' + @name 根据进程名查找进程是否存在 + @author hwliang + @param _name 命令行 + @return bool + ''' + if isinstance(_name, str): + _name = [_name] + if not isinstance(_name, list): + return False + for pid in psutil.pids(): + try: + p = psutil.Process(pid) + name = p.name() + for _n in _name: + if name == _n: return True + except: + continue + return False + + +def is_mysql_process_exists(): + ''' + @name 检查mysql进程是否存在 + @author hwliang + @return bool + ''' + _exe = ['server/mysql/bin/mysqld_safe', 'server/mysql/bin/mariadbd', 'server/mysql/bin/mysqld'] + return is_process_exists_by_exe(_exe) + + +def is_redis_process_exists(): + ''' + @name 检查redis进程是否存在 + @author hwliang + @return bool + ''' + _exe = ['server/redis/src/redis-server'] + return is_process_exists_by_exe(_exe) + + +def is_pure_ftpd_process_exists(): + ''' + @name 检查pure-ftpd进程是否存在 + @author hwliang + @return bool + ''' + _exe = ['server/pure-ftpd/sbin/pure-ftpd'] + return is_process_exists_by_exe(_exe) + + +def is_php_fpm_process_exists(name): + ''' + @name 检查php-fpm进程是否存在 + @author hwliang + @return bool + ''' + _php_version = name.split('-')[-1] + _exe = ['server/php/{}/sbin/php-fpm'.format(_php_version)] + return is_process_exists_by_exe(_exe) + + +def is_nginx_process_exists(): + ''' + @name 检查nginx进程是否存在 + @author hwliang + @return bool + ''' + _exe = ('server/nginx/sbin/nginx', 'server/nginx/nginx/sbin/nginx') + for i in _exe: + result = is_process_exists_by_exe(i) + if result: return result + return False + + +def is_httpd_process_exists(): + ''' + @name 检查httpd进程是否存在 + @author hwliang + @return bool + ''' + _exe = ['server/apache/bin/httpd'] + return is_process_exists_by_exe(_exe) + + +def is_memcached_process_exists(): + ''' + @name 检查memcached进程是否存在 + @author hwliang + @return bool + ''' + _exe = ['/usr/local/memcached/bin/memcached'] + return is_process_exists_by_exe(_exe) + + +def is_mongodb_process_exists(): + ''' + @name 检查mongodb进程是否存在 + @author hwliang + @return bool + ''' + _exe = ['server/mongodb/bin/mongod'] + return is_process_exists_by_exe(_exe) + + +def check_auth_ip(): + """ + @name 检测api和www的服务器ip是否一致 + @auther cjxin 2022-09-13 + @return bool + """ + import http_requests + result = {'www': '', 'api': ''} + res = http_requests.post('https://www.bt.cn/api/getIpAddress', data={}, timeout=5, headers={}) + if res.status_code == 200: + result['www'] = res.text + + res1 = http_requests.post('https://api.bt.cn/api/getIpAddress', data={}, timeout=5, headers={}) + if res1.status_code == 200: + result['api'] = res1.text + + return result + + +def set_func(key, count=0): + """ + 设置指定key的操作时间 + @key 面板访问函数 + """ + path = 'data/func.json' + data = {} + try: + data = json.loads(readFile(path)) + except: + pass + if not key in data: + data[key] = {} + data[key]['time'] = 0 + data[key]['count'] = 0 + data[key]['time'] = int(time.time()) + + if count > 0: + data[key]['count'] = count + else: + data[key]['count'] += 1 + writeFile(path, json.dumps(data)) + + +def get_func(key): + """ + 获取指定功能的操作时间 + @key 面板函数 + """ + path = 'data/func.json' + ret = {} + ret['count'] = 0 + ret['time'] = 0 + if not os.path.exists(path): return ret + data = {} + try: + data = json.loads(readFile(path)) + except: + pass + if not key in data: return ret + return data[key] + + +def set_cache_func(key, info): + """ + 设置指定key的操作时间 + @key 缓存的key函数 + """ + data = {} + path = '{}/data/cache_func.json'.format(get_panel_path()) + + try: + data = json.loads(readFile(path)) + except: + pass + if not key in data: data[key] = {} + + data[key]['time'] = int(time.time()) + data[key]['data'] = info + + writeFile(path, json.dumps(data)) + + +def get_cache_func(key): + """ + 获取指定功能的操作时间 + @key 缓存的key函数 + """ + + ret = {} + data = {} + ret['data'] = '' + ret['time'] = 0 + path = '{}/data/cache_func.json'.format(get_panel_path()) + if not os.path.exists(path): + return ret + try: + data = json.loads(readFile(path)) + except: + pass + if not key in data: return ret + return data[key] + + +def set_split_logs(path, status=1, info=None): + """ + @name 添加日志切割 + @path 日志路径, + @data dict + { + 'type':'day/size' + 'limit': 180,保留份数 + 'size': 日志超过多少进行切割,type=size时生效 + 'callback': 回调命令,部分日志切割后需要重启服务 + } + """ + + data = {} + sfile = '{}/data/cutting_log.json'.format(get_panel_path()) + if os.path.exists(sfile): + try: + data = json.loads(readFile(sfile)) + except: + pass + + if path in data: + del data[path] + + if status: + if not info: + return False + if not 'type' in info or not 'limit' in info: + return False + data[path] = info + writeFile(sfile, json.dumps(data)) + + # 计划任务切割 + echo = md5(md5('set_split_logs')) + find = M('crontab').where('echo=?', (echo,)).find() + + try: + import crontab + args_obj = dict_obj() + + if not find: + cronPath = GetConfigValue('setup_path') + '/cron/' + echo + shell = '{} -u /www/server/panel/script/logSplit.py'.format(sys.executable) + writeFile(cronPath, shell) + + args_obj.id = M('crontab').add( + 'name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress', + ("[删除]切割日志文件", 'minute-n', '10', '0', '0', echo, time.strftime('%Y-%m-%d %X', time.localtime()), 0, '', + 'localhost', 'toShell', '', shell, '')) + crontab.crontab().set_cron_status(args_obj) + else: + cron_path = get_cron_path() + if os.path.exists(cron_path): + cron_s = readFile(cron_path) + if cron_s.find(echo) == -1: + M('crontab').where('echo=?', (echo,)).setField('status', 0) + args_obj.id = find['id'] + crontab.crontab().set_cron_status(args_obj) + return True + except: + pass + + return False + + +def get_admin_path(): + ''' + @name 取安全入口 + @author hwliang + @return string + ''' + login_path = '/login' + path = '{}/data/admin_path.pl'.format(get_panel_path()) + if not os.path.exists(path): return login_path + admin_path = readFile(path) + if not admin_path: return login_path + admin_path = admin_path.strip() + if admin_path in ['', '/']: + return login_path + if admin_path[-1] == '/': admin_path = admin_path[:-1] + return admin_path + + +def get_improvement(): + ''' + @name 获取用户体验改进计划状态 + @author hwliang + @return bool + ''' + tip_file = '{}/data/improvement.pl'.format(get_panel_path()) + tip_file_set = '{}/data/is_set_improvement.pl'.format(get_panel_path()) + if not os.path.exists(tip_file_set): + return True + return os.path.exists(tip_file) + + +def is_spider(): + ''' + @name 判断是否为爬虫 + @return bool + ''' + from BTPanel import request + import panelDefense + p = panelDefense.bot_safe() + return not p.spider(request.headers.get('User-Agent'), request.remote_addr) + + +# def get_rsa_public_key_file(): +# ''' +# @name 获取RSA公钥文件路径 +# @author hwliang +# @return str +# ''' +# return '{}/data/rsa_public_key.pem'.format(get_panel_path()) + +# def get_rsa_private_key_file(): +# ''' +# @name 获取RSA私钥文件路径 +# @author hwliang +# @return str +# ''' +# return '{}/data/rsa_private_key.pem'.format(get_panel_path()) + + +def get_rsa_public_key(): + ''' + @name 获取RSA公钥内容 + @author hwliang + @return str + ''' + from BTPanel import session + pub_key = 'rsa_public_key' + public_key = session.get(pub_key) + if not public_key: + create_rsa_key() + public_key = session.get(pub_key) + return public_key + + # path = get_rsa_public_key_file() + # if not os.path.exists(path): create_rsa_key() + # if not os.path.exists(path): return '' + # return readFile(path) + + +def get_rsa_private_key(): + ''' + @name 获取RSA私钥内容 + @author hwliang + @return str + ''' + from BTPanel import session + prv_key = 'rsa_private_key' + private_key = session.get(prv_key) + if not private_key: + create_rsa_key() + private_key = session.get(prv_key) + return private_key + + +def create_rsa_key(): + ''' + @name 创建RSA密钥 + @author hwliang + @return bool + ''' + try: + # private_key_file = get_rsa_private_key_file() + # public_key_file = get_rsa_public_key_file() + # if os.path.exists(private_key_file) and os.path.exists(public_key_file): return True + from BTPanel import session + pub_key = 'rsa_public_key' + prv_key = 'rsa_private_key' + if pub_key in session and prv_key in session: + return True + try: + from Crypto.PublicKey import RSA + key = RSA.generate(1024) + private_key = key.exportKey("PEM") + public_key = key.publickey().exportKey("PEM") + except: + is_re_install = '{}/data/pycryptodome_re_install.pl'.format(get_panel_path()) + if not os.path.exists(is_re_install): + os.system("nohup btpip install pycryptodome -I &> /dev/null &") + writeFile(is_re_install, 'True') + + priv_pem = '/tmp/private.pem' + pub_pem = '/tmp/public.pem' + ExecShell("openssl genrsa -out {} 1024".format(priv_pem)) + ExecShell("openssl rsa -pubout -in {} -out {}".format(priv_pem, pub_pem)) + if not os.path.exists(priv_pem) or not os.path.exists(pub_pem): + return False + + private_key = readFile(priv_pem, 'rb') + public_key = readFile(pub_pem, 'rb') + + if os.path.exists(priv_pem): os.remove(priv_pem) + if os.path.exists(pub_pem): os.remove(pub_pem) + + session[pub_key] = public_key.decode('utf-8').replace("\n", "") + session[prv_key] = private_key.decode('utf-8') + + # writeFile(private_key_file,private_key,'wb+') + # writeFile(public_key_file,public_key,'wb+') + return True + except: + print_log(get_error_info()) + return False + + +def rsa_encrypt(data): + ''' + @name RSA加密数据 + @param data str 要加密的数据 + @return str + ''' + # 分片长度 1024 / 8 - 11 = 117 + split_length = 117 + try: + from Crypto.PublicKey import RSA + from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs + + # 初始化RSA加密对象 + public_key = get_rsa_public_key() + cipher_public = Cipher_pkcs.new(RSA.importKey(public_key)) + + # 分片加密 + data = data.encode('utf-8') + encrypted_arr = [] + for i in range(0, len(data), split_length): + d = data[i:i + split_length] + encrypted_data = cipher_public.encrypt(d) + encrypted_base64 = base64.b64encode(encrypted_data).decode() + encrypted_arr.append(encrypted_base64) + + # 用换行符拼接 + return "\n".join(encrypted_arr) + except: + print_log(get_error_info()) + return '' + + +def rsa_decrypt(data): + ''' + @name RSA解密数据 + @param data str 要解密的数据 + @return str + ''' + try: + from Crypto.PublicKey import RSA + from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs + + # 初始化RSA解密对象 + private_key = get_rsa_private_key() + cipher_private = Cipher_pkcs.new(RSA.importKey(private_key)) + + # 分片解密 + decrypted_str = b"" + for d in data.split("\n"): + if not d: continue + res = base64.b64decode(d) + if not res: continue + decrypted_data = cipher_private.decrypt(res, None) + decrypted_str += decrypted_data + return decrypted_str.decode('utf-8') + except: + print_log(get_error_info()) + return '' + + +def rsa_encrypt_for_private_key(data): + ''' + @name RSA私钥加密数据 + @author hwliang + @param data str 要加密的数据 + @return str + ''' + # 分片长度 1024 / 8 - 11 = 117 + split_length = 117 + try: + from Crypto.PublicKey import RSA + from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs + + # 初始化RSA加密对象 + private_key = get_rsa_private_key() + cipher_private = Cipher_pkcs.new(RSA.importKey(private_key)) + + # 分片加密 + data = data.encode('utf-8') + encrypted_arr = [] + for i in range(0, len(data), split_length): + d = data[i:i + split_length] + encrypted_data = cipher_private.encrypt(d) + encrypted_base64 = base64.b64encode(encrypted_data).decode() + encrypted_arr.append(encrypted_base64) + + # 用换行符拼接 + return "\n".join(encrypted_arr) + except: + print_log(get_error_info()) + return '' + + +def get_client_hash(): + ''' + @name 获取客户端HASH + @author hwliang + @return str + ''' + from flask import session, request + is_tmp_login = session.get('tmp_login') + if is_tmp_login: + client_hash = md5(request.remote_addr) + else: + skey = 'client_ips' + ckey = 'client_sync_count' + client_ips = session.get(skey, []) + client_sync_count = session.get(ckey, 0) + + # 是否唯一IP + if len(client_ips) <= 1: + # 唯一IP连续访问次数超过100次,使用IP+UA生成HASH + r_max = 101 + if client_sync_count >= r_max - 1: + client_hash = md5(request.remote_addr) + if client_sync_count < r_max: + session['client_hash'] = client_hash + client_sync_count += 1 + session[ckey] = client_sync_count + return client_hash + + # 记录IP + if not request.remote_addr in client_ips: + client_ips.append(request.remote_addr) + session[skey] = client_ips + + # 记录访问次数 + client_sync_count += 1 + session[ckey] = client_sync_count + + # 非唯一IP,使用UA生成HASH + client_hash = md5('') + + return client_hash + + +def check_client_hash(): + ''' + @name 验证客户端HASH + @author hwliang + @return bool + ''' + # 是否关闭验证 + not_tip = '{}/data/not_check_ip.pl'.format(get_panel_path()) + if os.path.exists(not_tip): return True + from BTPanel import session, request + # 如果未开启SSL,不验证 + if request.scheme == 'https': return True + skey = 'client_hash' + client_hash = get_client_hash() + if not skey in session: + session[skey] = client_hash + return True + + if session[skey] != client_hash: + WriteLog('User login', 'Client HASH verification failed, has been forced to log out!') + return False + return True + + +def shell_quote(cmd): + ''' + @name shell转义 + @author hwliang + @param cmd str 要转义的命令 + @return str + ''' + if not cmd: return '' + if isinstance(cmd, bytes): cmd = cmd.decode('utf-8') + if not isinstance(cmd, str): return cmd + try: + import shlex + return shlex.quote(cmd) + except: + try: + import pipes + return pipes.quote(cmd) + except: + return cmd + + +def get_div(div): + sql = M('sqlite_master') + if not sql.where('type=? AND name=? AND sql LIKE ?', ('table', 'div_list', '%div%')).count(): + sql_str = '''CREATE TABLE IF NOT EXISTS `div_list` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`div` TEXT +)''' + sql.execute(sql_str) + my_div = sql.table('div_list').where('id=1', ()).getField('div') + if not my_div: + sql.table('div_list').insert({'div': div}) + my_div = div + return my_div + + +def set_tasks_run(data): + ''' + @name 设置运行时间 + @param data dict 数据 + @param data.type int 类型 1:面板 2:插件 + @param data.time int 执行时间(必传) + @param data.name str 插件名称、模块名称(必传) + @param data.title str 插件中文名(必传) + @param data.fun str 执行方法(必传) + @param data.args dict 参数 + + ''' + spath = '{}/data/tasks'.format(get_panel_path()) + if not os.path.exists(spath): os.makedirs(spath, 384) + + task_file = '{}/{}'.format(spath, md5(str(time.time()))) + writeFile(task_file, json.dumps(data)) + return returnMsg(True, task_file) + + +def Get_ip_info(get_speed=False, get_user=True): + ''' + 获取bt官网ip归属地列表 + @author wzz + @return: list[dict{}] + ''' + host_list = json.loads(readFile("config/hosts_dict.json")) + print("host_list: ", host_list) + + # 推荐,一般,较差,不推荐,不测速时,ipv6,用户服务器IP + level = (1, 2, 3, 4, 5, 6, 0) + user_server_ipaddress = [] + if get_user: + user_server_ipaddress = get_user_server_ipaddress(host_list, level) + bt_host = get_bt_hosts(get_speed, host_list, level) + ips_result = user_server_ipaddress + bt_host + if ips_result: return ips_result + + +def get_user_server_ipaddress(host_list, level): + ''' + 获取服务器公网ip归属地信息 + @param host_list: host列表 + @param level: 等级元组 + @return: + ''' + ips_result = [] + headers = {"host": "www.bt.cn"} + for host in host_list: + try: + new_url = "https://{}/Api/getIpAddress".format(host["ip"]) + m_str = HttpGet(new_url, 1, headers=headers) + ipaddress = re.search(r"^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$", m_str).group(0) + s_ip_info = get_free_ip_info("{}".format(ipaddress)) + if "ip" in s_ip_info.keys(): + s_ip_info["info"] = "本服务器公网IP归属地信息" + s_ip_info["level"] = level[-1] + ips_result.append(s_ip_info) + return ips_result + except: + continue + if not ips_result: + return [{'continent': '', 'country': '未知地区', 'province': '', 'city': '', 'region': '', 'carrier': '', + 'division': '', 'en_country': '', 'en_short_code': '', 'longitude': '', + 'latitude': '', 'info': '本服务器公网IP归属地信息', 'ip': GetLocalIp(), 'level': 0}] + + +def get_bt_hosts(get_speed, host_list, level): + ''' + 获取bt官网ip归属地列表 + @param get_speed: 是否测速 + @param host_list: 传host列表 + @param level: 传等级元组 + @return: + ''' + ips_result = [] + + for ip in host_list: + ipv6 = { + "continent": "", + "country": "", + "province": "", + "city": "ipv6 地址", + "region": "", + "carrier": "", + "division": "", + "en_country": "", + "en_short_code": "", + "longitude": "", + "latitude": "", + "info": "该节点为ipv6地址,若服务器无ipv6请勿选择!", + "ip": "", + "level": None + } + try: + # 获取节点响应延迟 + if get_speed: n_net, n_ping = get_timeout("https://{}".format(ip["ip"]) + ':80/net_test', 1) + if not is_ipv4(ip["ip"]): + ipv6['ip'] = ip["ip"] + # ipv6地址默认一般推荐 + ipv6['level'] = level[-2] + if get_speed: ipv6['speed'] = "" + ips_result.append(ipv6) + continue + ip_result = get_free_ip_info("{}".format(ip["ip"])) + if "ip" in ip_result.keys(): + ip_result['level'] = level[-3] + if get_speed: + if int(n_ping) < 100: ip_result['level'] = level[0] + if 100 < int(n_ping) < 500: ip_result['level'] = level[1] + if int(n_ping) > 500: ip_result['level'] = level[2] + ip_result["speed"] = n_ping + 500 + ips_result.append(ip_result) + continue + if "info" in ip_result.keys(): + if ip_result["info"] == "未知归属地": + ipv6['ip'] = ip["ip"] + ipv6["city"] = ip["area"] + ipv6['level'] = level[1] + if get_speed: ipv6['speed'] = "" + ipv6["info"] = "节点无法测速,请选择离您服务器最近的尝试!" + ips_result.append(ipv6) + except: + continue + if len(ips_result) < 2 and ips_result[-1]["city"] == "ipv6 地址": ips_result.pop(-1) + return ips_result + + +def set_home_host2(host): + """ + @name 设置官网hosts + @author wzz + @param host IP地址 + @return void + """ + msg = "请尝试点击【清理旧节点】,如果仍然不行,请联系堡塔运维! https://www.bt.cn/bbs" + www_set = ExecShell("echo \"{} www.bt.cn\" >> /etc/hosts".format(host)) + api_set = ExecShell("echo \"{} api.bt.cn\" >> /etc/hosts".format(host)) + if not www_set[1] and not api_set[1]: return returnMsg(True, "节点设置成功") + if www_set[1]: return returnMsg(False, "节点设置失败: {}, {}".format(www_set[1], msg)) + if api_set[1]: return returnMsg(False, "节点设置失败: {}, {}".format(api_set[1], msg)) + return returnMsg(False, "节点设置失败: {}".format(msg)) + + +def Clean_bt_host(): + ''' + 删除bt.cn相关的hosts绑定信息 + @author wzz + @return: + ''' + check_hosts = ExecShell("grep \"bt.cn\" /etc/hosts") + if check_hosts[0]: + result = ExecShell("sed -i \"/bt.cn/d\" /etc/hosts") + if result[1]: return returnMsg(False, "旧节点清理失败: {}".format(result[1])) + return returnMsg(True, "旧节点已清理") + return returnMsg(True, "hosts没有绑定旧节点无需清理") + + +def Set_bt_host(ip=None): + ''' + 设置bt官网(www && api)指定hosts节点 + @author wzz + @param get: 手动设置 get.ip 官网传ip地址,从public.Get_ip_info方法获取 | 自动设置 + @return: + ''' + Clean_bt_host() + + if ip: return set_home_host2(ip) + # 如果不传ip则自动设置 + ips_info = Get_ip_info(get_user=False) + headers = {"host": "www.bt.cn"} + for host in ips_info: + new_url = "https://{}".format(host['ip']) + res = HttpGet(new_url, 1, headers=headers) + if res: + writeFile("{}/data/home_host.pl".format(get_panel_path()), host["ip"]) + result = set_home_host2(host["ip"]) + if result["status"]: + return returnMsg(True, "已自动选择为{}{}的最优节点,运营商是: {}" + .format(host['province'], host['city'], host['carrier'])) + return returnMsg(False, "自动选择节点失败,请尝试手动设置") + + +def set_ownership(directory, user): + ''' + 设置指定目录及目录下所有文件、子目录所属为user + @param directory: + @param user: + @return: + ''' + import pwd + uid = pwd.getpwnam(user).pw_uid + gid = pwd.getpwnam(user).pw_gid + + os.chown(directory, uid, gid) + + for root, dirs, files in os.walk(directory): + for d in dirs: + dir_path = os.path.join(root, d) + os.chown(dir_path, uid, gid) + for f in files: + file_path = os.path.join(root, f) + os.chown(file_path, uid, gid) + + +def set_permissions(directory, permissions): + ''' + 设置指定目录及目录下所有文件、子目录权限为permissions + @param directory: + @param permissions: 传八进制,如0o755 + @return: + ''' + os.chmod(directory, permissions) + + for root, dirs, files in os.walk(directory): + for d in dirs: + dir_path = os.path.join(root, d) + os.chmod(dir_path, permissions) + for f in files: + file_path = os.path.join(root, f) + os.chmod(file_path, permissions) + + +def check_ssl_verify(certPath='ssl/ca.pem'): + ''' + 校验面板设置SSL双向认证证书格式 + @param certPath: + @return: + ''' + if "crl.pem" in certPath: + certKey = readFile(certPath) + if "-----BEGIN X509 CRL-----" not in certKey: + return False + return True + + res = False + openssl = '/usr/local/openssl/bin/openssl' + if not os.path.exists(openssl): openssl = 'openssl' + certPem = readFile(certPath) + if "-----BEGIN CERTIFICATE-----" in certPem and "Certificate" in certPem: + result = ExecShell(openssl + " x509 -in " + certPath + " -noout -subject") + res = True + if len(result[1]) > 2: res = False + if result[0].find('error:') != -1: res = False + return res + + +def is_write_file(): + ''' + @name 测试是否能写入文件 + @return void + ''' + test_file = '/etc/init.d/bt_10000100.pl' + writeFile(test_file, 'True') + if os.path.exists(test_file): + if readFile(test_file) == 'True': + os.remove(test_file) + return True + os.remove(test_file) + return False + + +def stop_syssafe(): + ''' + @name 临时停用系统加固 + @return bool + ''' + # 检测是否可写 + ret = is_write_file() + is_stop_syssafe_file = '{}/data/is_stop_syssafe.pl'.format(get_panel_path()) + # 如果不可写,则尝试停用系统加固 + if not ret: + syssafe_path = get_plugin_path('syssafe') + if os.path.exists(syssafe_path): + writeFile(is_stop_syssafe_file, 'True') + ExecShell("/etc/init.d/bt_syssafe stop") + + # 停用系统加固后再检测一次 + ret = is_write_file() + return ret + + return ret + + +def start_syssafe(): + ''' + @name 恢复系统加固的运行状态 + @return void + ''' + is_stop_syssafe_file = '{}/data/is_stop_syssafe.pl'.format(get_panel_path()) + if os.path.exists(is_stop_syssafe_file): + ExecShell("/etc/init.d/bt_syssafe start") + if os.path.exists(is_stop_syssafe_file): + os.remove(is_stop_syssafe_file) + + +def check_sys_write(): + ''' + @name 检查关键目录是否可写 + @return bool + ''' + return stop_syssafe() + + +def get_root_domain(domain_name): + ''' + @name 根据域名查询根域名和记录值 + @author cjxin<2020-12-17> + @param domain {string} 被验证的根域名 + @return void + ''' + top_domain_list = ['.ac.cn', '.ah.cn', '.bj.cn', '.com.cn', '.cq.cn', '.fj.cn', '.gd.cn', + '.gov.cn', '.gs.cn', '.gx.cn', '.gz.cn', '.ha.cn', '.hb.cn', '.he.cn', + '.hi.cn', '.hk.cn', '.hl.cn', '.hn.cn', '.jl.cn', '.js.cn', '.jx.cn', + '.ln.cn', '.mo.cn', '.net.cn', '.nm.cn', '.nx.cn', '.org.cn', '.cn.com'] + old_domain_name = domain_name + top_domain = "." + ".".join(domain_name.rsplit('.')[-2:]) + new_top_domain = "." + top_domain.replace(".", "") + is_tow_top = False + if top_domain in top_domain_list: + is_tow_top = True + domain_name = domain_name[:-len(top_domain)] + new_top_domain + + if domain_name.count(".") > 1: + zone, middle, last = domain_name.rsplit(".", 2) + if is_tow_top: + last = top_domain[1:] + root = ".".join([middle, last]) + else: + zone = "" + root = old_domain_name + return root, zone + + +def check_area_panel(): + ''' + @name: 检查地区限制 + @return: + ''' + import contextlib + areas_dict = get_limit_area() + + # 关闭状态直接返回false + if areas_dict["limit_area_status"] == "false": return False + # 2024/1/3 下午 2:23 兼容配置文件如果为空或者地区为空或配置文件异常,则直接跳过验证 + if len(areas_dict["limit_area"]) == 0: return False + if len(areas_dict["limit_area"]["city"]) == 0: + if "province" in areas_dict["limit_area"] and len(areas_dict["limit_area"]["province"]) == 0: + if "country" in areas_dict["limit_area"] and len(areas_dict["limit_area"]["country"]) == 0: + return False + + client_ip = GetClientIp() + # 本地访问直接返回false + if client_ip in ['127.0.0.1', 'localhost', '::1']: return False + ip_area_dict = get_ip_location(client_ip) + # 没有查询到地区返回false,内网地址直接返回false + if not ip_area_dict: return False + if ip_area_dict.raw["country"]["country"] == "Internal network address": return False + try: + error_str = "

                                    {}

                                    {}
                                    {}
                                    {}".format( + getMsg('PAGE_ERR_IP_AREA_H1'), + getMsg('PAGE_ERR_IP_AREA_P1', ("{} {} {}".format( + ip_area_dict.raw["country"]["country"], + ip_area_dict.raw["country"]["province"], + ip_area_dict.raw["country"]["city"] + ),)), + getMsg('PAGE_ERR_IP_AREA_P2'), + getMsg('PAGE_ERR_IP_AREA_P3') + ) + + # 仅允许allow列表中的地区,其他地区都不可以访问 + if areas_dict["limit_type"] == "allow": + for city in areas_dict["limit_area"]["city"]: + if len(ip_area_dict.raw["country"]["city"].strip()) == 0: + break + + if ip_area_dict.raw["country"]["city"].strip() in city["name"]: + return False + + for province in areas_dict["limit_area"]["province"]: + if len(ip_area_dict.raw["country"]["province"].strip()) == 0: + break + + if ip_area_dict.raw["country"]["province"].strip() in province["name"]: + return False + + for country in areas_dict["limit_area"]["country"]: + if len(ip_area_dict.raw["country"]["country"].strip()) == 0: + break + + if ip_area_dict.raw["country"]["country"].strip() in country["name"]: + return False + + return error_str + + # 仅禁止deny列表中的地区,其他地区都可以访问 + if areas_dict["limit_type"] == "deny": + for city in areas_dict["limit_area"]["city"]: + if len(ip_area_dict.raw["country"]["city"].strip()) == 0: + break + + if ip_area_dict.raw["country"]["city"].strip() in city["name"]: + return error_str + + for province in areas_dict["limit_area"]["province"]: + if len(ip_area_dict.raw["country"]["province"].strip()) == 0: + break + + if ip_area_dict.raw["country"]["province"].strip() in province["name"]: + return error_str + + for country in areas_dict["limit_area"]["country"]: + if len(ip_area_dict.raw["country"]["country"].strip()) == 0: + break + + if ip_area_dict.raw["country"]["country"].strip() in country["name"]: + return error_str + except: + import traceback + print(traceback.format_exc()) + return False + + +def get_ip_location(ip_address): + ''' + 获取ip地址的地理位置 + @param ip_address: + @return: + ''' + try: + from geoip2 import database + except: + ExecShell("{}/pyenv/bin/pip install -U pip".format(get_panel_path())) + ExecShell("{}/pyenv/bin/pip install geoip2".format(get_panel_path())) + writeFile('data/restart.pl', 'True') + from geoip2 import database + + data_path = '{}/config/GeoLite2-City.mmdb'.format(get_panel_path()) + reader = database.Reader(data_path) + response = reader.city(ip_address) + reader.close() + return response + + +def get_limit_area(): + ''' + 获取地区限制列表 + @return: + ''' + empty_content = { + "limit_area": { + "city": [], + "province": [], + "country": [] + }, + "limit_area_status": "false", + "limit_type": "deny" + } + try: + areas_file = 'data/limit_area.json' + if not os.path.exists(areas_file): return empty_content + + try: + areas_dict = json.loads(ReadFile(areas_file)) + except json.decoder.JSONDecodeError: + return empty_content + + return areas_dict + except: + public.get_error_info() + return empty_content + + +# 密码复杂度验证 +def check_password_safe(password: str) -> bool: + ''' + @name 密码复杂度验证 + @param password(string) 密码 + @return bool + ''' + # 是否检测密码复杂度 + + # 密码长度验证 + if len(password) < 8: return False + + num = 0 + # 密码是否包含数字 + if re.search(r'[0-9]+', password): num += 1 + # 密码是否包含小写字母 + if re.search(r'[a-z]+', password): num += 1 + # 密码是否包含大写字母 + if re.search(r'[A-Z]+', password): num += 1 + # 密码是否包含特殊字符 + if re.search(r'[^\w\s]+', password): num += 1 + # 密码是否包含以上任意3种组合 + if num < 3: return False + return True + + +def show_menu(menu_id, status): + """ + 设置显示隐藏菜单 + :param menu_id: 菜单id + :param status: 0 | 1 + :return: + """ + + show_menu_file = '/www/server/panel/config/show_menu.json' + hide_menu_file = '/www/server/panel/config/hide_menu.json' + defanlt_data = ['memuA', 'memuAsite', 'memuAftp', 'memuAdatabase', 'memuDocker', 'memuAcontrol', 'memuAfirewall', + 'memuAfiles', 'memuAlogs', 'memuAxterm', 'memuAcrontab', 'memuAsoft', + 'memuAconfig', 'dologin'] + show_menu_data = defanlt_data + if os.path.exists(show_menu_file): + show_menu_data = json.loads(ReadFile(show_menu_file)) + + # 获取之前设置的隐藏页面 + try: + if os.path.exists(hide_menu_file): + hide_menu = ReadFile(hide_menu_file) + show_menu_data = [i for i in show_menu_data if i not in hide_menu] + ExecShell("rm -rf {}".format(hide_menu_file)) + WriteFile(show_menu_file, json.dumps(show_menu_data)) + except: + pass + if status == 1 and menu_id not in show_menu_data: + show_menu_data.append(menu_id) + elif status == 0 and menu_id in show_menu_data: + show_menu_data.remove(menu_id) + WriteFile(show_menu_file, json.dumps(show_menu_data)) + return returnMsg(True, 'Success') + + +def task_service_status(): + ''' + @name 检查后台任务服务状态 + @return bool + ''' + pid_file = '{}/logs/task.pid'.format(get_panel_path()) + if not os.path.exists(pid_file): return False + pid = readFile(pid_file) + if not pid: return False + if not os.path.exists('/proc/{}'.format(pid)): return False + return True + + +def reload_panel(): + ''' + @name 重载面板 + @return void + ''' + + # 重载面板 + WriteFile('{}/data/reload.pl'.format(get_panel_path()), 'True') + if not task_service_status(): + ExecShell("bash {}/init.sh start") + + +# 2024/1/24 上午 10:38 通用响应对象 +def returnResult(code=0, status=True, msg="OK", data=None, timestamp=None, args=None): + ''' + 通用响应对象 + @param code: 0:成功 1:失败 2:警告 ... + @param status: + @param msg: 只传msg,不传需要前端处理的数据 + @param data: 只传需要前端处理的数据 + @param timestamp: 秒级时间戳 + @return: + + 使用示例: + 成功:return dp.returnResult(data=data) + 失败:return dp.returnResult(code=1, status=False, msg="获取失败!", data=[]) + 失败:return dp.returnResult(code=1, status=False, msg="获取失败!") + 警告:return dp.returnResult(code=2, status=False, msg="警告,xxxxxxxxxxx!") + ... + ''' + import time + if timestamp is None: + timestamp = int(time.time()) + + log_message = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/public.json')) + keys = log_message.keys() + if type(msg) == str: + if msg in keys: + msg = log_message[msg] + for i in range(len(args)): + rep = '{' + str(i + 1) + '}' + msg = msg.replace(rep, args[i]) + + return { + "code": code, + "status": status, + "msg": msg, + "data": data, + "timestamp": timestamp + } + + +# 2024/1/24 上午 11:56 取指定模型目录 +def get_mod_path(mod_name=None): + ''' + @name 取指定插件目录 + @author hwliang<2021-07-14> + @param mod_name 模型名称 不传则返回模型根目录 + @return string + ''' + + root_path = "{}/mod/project".format(get_panel_path()) + if not mod_name: return root_path + return "{}/{}".format(root_path, mod_name) + + +def get_client_info_db_obj(): + ''' + @name 获取客户端信息数据库对象 + @return object + ''' + db_path = '{}/data/db'.format(get_panel_path()) + db_file = '{}/client_info.db'.format(db_path) + if not os.path.exists(db_path): os.makedirs(db_path, 384) + db_obj = M('') + db_obj._Sql__DB_FILE = db_file + # 如果数据库文件不存在则创建 + if not os.path.exists(db_file): + db_obj.execute('''CREATE TABLE client_info ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + remote_addr VARCHAR(50) NOT NULL, + remote_port INTEGER DEFAULT 0, + session_id VARCHAR(32) NOT NULL, + user_agent TEXT NOT NULL, + login_time INTEGER DEFAULT 0 + )''') + + # 创建索引 + db_obj.execute('CREATE INDEX client_ip_index ON client_info(client_ip)') + db_obj.execute('CREATE INDEX session_id_index ON client_info(session_id)') + db_obj.execute('CREATE INDEX login_time_index ON client_info(login_time)') + return db_obj + + +def record_client_info(): + ''' + @name 记录客户端信息 + @return void + ''' + from flask import request + from BTPanel import cache + db_obj = get_client_info_db_obj() + remote_addr = GetClientIp() + user_agent = request.headers.get('User-Agent', '') + pdata = { + 'remote_addr': remote_addr, + 'remote_port': request.environ.get('REMOTE_PORT'), + 'session_id': md5(remote_addr + user_agent), + 'user_agent': user_agent, + 'login_time': int(time.time()) + } + db_obj.table('client_info').insert(pdata) + db_obj.close() + + # 设置缓存 + cache.set('last_client_session_id', pdata['session_id'], 86400 * 2) + + +def check_client_info(): + ''' + @name 检查客户端信息 + @return int 0:陌生IP,1:上次登录的IP且UA一致,2:近30天内登录过的IP + ''' + from flask import request + from BTPanel import cache + remote_addr = GetClientIp() + # remote_addr = request.environ.get('REMOTE_ADDR', '0.0.0.0') + # 如果是本地访问或为未来IP则当作陌生IP + if remote_addr in ['0.0.0.0', '127.0.0.1', '::1', '::']: + return 0 + user_agent = request.headers.get('User-Agent', '') + # 如果UA不是浏览器则当作陌生IP + if user_agent.find('Mozilla') == -1: + return 0 + + session_id = md5(remote_addr + user_agent) + + if cache.get('last_client_session_id') == session_id: + return 1 + + db_obj = get_client_info_db_obj() + if not db_obj: + return 0 + + last_login_info = db_obj.table('client_info').order('id desc').field('remote_addr,session_id,login_time').find() + if not last_login_info: + return 0 + + # 如果上次登录的IP且UA一致 + now_time = int(time.time()) + if last_login_info['session_id'] == session_id: + s_time = now_time - last_login_info['login_time'] + if s_time < (86400 * 2): + cache.set('last_client_session_id', session_id, 86400 * 2 - s_time) + return 1 + if s_time < (86400 * 30): + return 2 + return 0 + + # 如果近30天内登录过的IP + if remote_addr == last_login_info['remote_addr'] and now_time - last_login_info['login_time'] < 2592000: + return 2 + if db_obj.table('client_info').where('remote_addr=?', remote_addr).count(): + return 2 + + # 陌生IP + return 0 + + +def redirect_to_login(default_callback_def=None): + ''' + @name 重定向到登录页面 + @return void + ''' + from flask import redirect, request, Response + client_status = check_client_info() + + # 获取请求头 + x_http_token = request.headers.get('x-http-token', '') + if client_status == 0: + if default_callback_def: + return default_callback_def(None) + # print_log("redirect_to_login 方法{1}") 登录过期会进入 + return error_404(None) + elif client_status == 1: + if x_http_token: + # result = {"status": False, "code": -8888, "redirect": get_admin_path(), + # "msg": "The current login session has been invalid, please login again!"} + # 修改为aapanel通用返回方式 + result = { + "status": -1, + "timestamp": int(time.time()), + "message": { + "msg": "The current login session has been invalid, please login again!", + "redirect": get_admin_path() + } + } + + return Response(json.dumps(result), mimetype='application/json', status=200) + return redirect(get_admin_path()) + elif client_status == 2: + if x_http_token: + # result = {"status": False, "code": -8888, "redirect": "/login", + # "msg": "The current login session has been invalid, please login again!"} + + # 修改为aapanel通用返回方式 + result = { + "status": -1, + "timestamp": int(time.time()), + "message": { + "msg": "The current login session has been invalid, please login again!", + "redirect": "/login" + } + } + + return Response(json.dumps(result), mimetype='application/json', status=200) + return redirect('/login') + + if default_callback_def: + return default_callback_def(None) + return error_404(None) + + +def ws_send(data: str): + try: + if '/www/server/panel' not in sys.path: + sys.path.insert(0, '/www/server/panel') + from BTPanel import WS_OBJ + ws_obj = {i: j for i, j in WS_OBJ.items() if j['timeout'] > int(time.time())} + if ws_obj == {}: return False + for i, j in ws_obj.items(): + j['ws_obj'].send(data) + return True + except: + return False + + +def get_plugin_info(upgrade_plugin_name): + ''' + @name 获取插件信息 + @author hwliang<2021-06-15> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + plugin_path = get_plugin_path() + plugin_info_file = '{}/{}/info.json'.format(plugin_path, upgrade_plugin_name) + if not os.path.exists(plugin_info_file): return {} + info_body = readFile(plugin_info_file) + if not info_body: return {} + plugin_info = json.loads(info_body) + return plugin_info + + +def get_plugin_find(upgrade_plugin_name=None): + ''' + @name 获取指定软件信息 + @author hwliang<2021-06-15> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + plugin_list_data = load_soft_list() + + for p_data_info in plugin_list_data['list']: + if p_data_info['name'] == upgrade_plugin_name: + # upgrade_plugin_name = p_data_info['name'] + return p_data_info + + return get_plugin_info(upgrade_plugin_name) + + +def get_plugin_value(plugin_name, key): + ''' + @name 获取插件配置值 + @author hwliang + @param plugin_name 插件名称 + @param key 字段名 + @return mixed + ''' + plugin_info = get_plugin_find(plugin_name) + return plugin_info.get(key, None) + + +def get_plugin_pid(plugin_name): + ''' + @name 获取指定插件的pid + @author hwliang<2021-06-15> + @param plugin_name 插件名称 + @return string + ''' + plugin_info = get_plugin_find(plugin_name) + if not plugin_info: return 0 + if 'pid' in plugin_info: + return plugin_info['pid'] + return 0 + + +# 下载插件主文件 +def download_main(upgrade_plugin_name, upgrade_version): + ''' + @name 下载插件主程序文件 + @author hwliang<2021-06-25> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 + @return void + ''' + import requests, shutil + plugin_path = get_plugin_path() + tmp_path = '{}/temp'.format(get_panel_path()) + + if not os.path.exists(tmp_path): + os.makedirs(tmp_path, 0o755) + + download_d_main_url = '{}/api/panel/download_plugin_main'.format(OfficialApiBase()) + pdata = get_user_info() + pdata['name'] = upgrade_plugin_name + pdata['version'] = upgrade_version + pdata['os'] = 'Linux' + pdata['environment_info'] = json.dumps(fetch_env_info(), ensure_ascii=False) + import config, socket + import requests.packages.urllib3.util.connection as urllib3_conn + _ip_type = config.config().get_request_iptype() + old_family = urllib3_conn.allowed_gai_family + if _ip_type == 'ipv4': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + elif _ip_type == 'ipv6': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + try: + download_res = requests.post(download_d_main_url, pdata, timeout=30, headers=get_requests_headers()) + + print_log(pdata) + print_log(download_res.content) + except Exception as ex: + raise PanelError(error_conn_cloud(str(ex))) + finally: + urllib3_conn.allowed_gai_family = old_family + + # 下载失败提示文本处理 + if download_res.status_code != 200: + try: + raise PanelError(download_res.json().get('res', 'download plugin source code error')) + except PanelError: + raise + except: + raise PanelError('download plugin source code error') + + filename = '{}/{}.py'.format(tmp_path, upgrade_plugin_name) + with open(filename, 'wb+') as save_script_f: + save_script_f.write(download_res.content) + save_script_f.close() + if md5(download_res.content) != download_res.headers.get('Content-md5'): + raise PanelError('Package file Hash verification failed.') + dst_file = '{plugin_path}/{plugin_name}/{plugin_name}_main.py'.format(plugin_path=plugin_path, + plugin_name=upgrade_plugin_name) + shutil.copyfile(filename, dst_file) + if os.path.exists(filename): os.remove(filename) + WriteLog('Software manager', + "Plugin [{}] was corrupted, try automatic repair.".format(get_plugin_info(upgrade_plugin_name)['title'])) + + +# 重新下载插件主文件 +def re_download_main(plugin_name, plugin_path=None): + if not plugin_path: + plugin_path = get_panel_path() + '/plugin/' + plugin_name + + plugin_file = '{plugin_path}/{name}/{name}_main.py'.format(plugin_path=plugin_path, name=plugin_name) + plugin_info = get_plugin_info(plugin_name) + if 'versions' in plugin_info: + version = plugin_info['versions'] + download_main(plugin_name, version) + plugin_body = readFile(plugin_file, 'rb') + return plugin_body + + return b'' + + +# 运行插件API +def run_plugin(plugin_name: str, def_name: str, args: dict_obj): + import PluginLoader + res = PluginLoader.plugin_run(plugin_name, def_name, args) + if isinstance(res, dict): + if 'status' in res and res['status'] == False and 'msg' in res: + if isinstance(res['msg'], str): + if res['msg'].find('Traceback ') != -1: + raise PanelError(res['msg']) + return res + +def run_plugin_v2(plugin_name: str, def_name: str, args: dict_obj): + import PluginLoader + res = PluginLoader.plugin_run(plugin_name, def_name, args) + # print_log(res) + if isinstance(res, dict): + if 'status' in res and res['status'] == False and 'msg' in res: + if isinstance(res['msg'], str): + if res['msg'].find('Traceback ') != -1: + raise PanelError(res['msg']) + if isinstance(res, dict): + if 'status' in res and 'msg' in res: + status = 0 if res['status'] else -1 + # 改返回 + res = return_message(status, 0, res['msg']) + else: + # 改返回 + res = return_message(0, 0, res) + if isinstance(res, list): + res = return_message(0, 0, res) + return res +# 加载插件列表与授权列表 +def load_soft_list(force: bool = True): + local_cache_file = '{}/data/plugin_bin.pl'.format(get_panel_path()) + + if force or not os.path.exists(local_cache_file) or os.path.getsize(local_cache_file) < 10: + cloudUrl = '{}/api/panel/getSoftListEn'.format(OfficialApiBase()) + import panelAuth + import requests + pdata = panelAuth.panelAuth().create_serverid(None) + url_headers = {} + if 'token' in pdata: + url_headers = {"authorization": "bt {}".format(pdata['token'])} + pdata['environment_info'] = json.dumps(fetch_env_info()) + + resp = requests.post(cloudUrl, params=pdata, headers=url_headers, verify=False, timeout=10) + + # 请求成功后将授权密文信息写入本地文件 + if resp.status_code == 200: + with open(local_cache_file, 'w') as fp: + fp.write(resp.text) + + import PluginLoader + + if force: + if hasattr(PluginLoader, 'parse_plugin_list'): + PluginLoader.parse_plugin_list(1) + else: + import importlib + importlib.reload(PluginLoader) + + plugin_list_data = PluginLoader.get_plugin_list(0) + + if not isinstance(plugin_list_data, dict): + raise PanelError('Sorry. failed to load soft list. please check the network and try again later.') + + if 'status' in plugin_list_data and 'msg' in plugin_list_data and plugin_list_data['status'] == False: + raise PanelError(str(plugin_list_data['msg'])) + + return plugin_list_data + + +# 官网API根地址 +def OfficialApiBase(): + return 'https://www.aapanel.com' + + +# 获取安装路径 +def get_setup_path(): + ''' + @name 获取安装路径 + @author hwliang<2021-07-22> + @return string + ''' + return '/www/server' + + +# 获取面板根目录 +def get_panel_path(): + ''' + @name 取面板根目录 + @author hwliang<2021-07-14> + @return string + ''' + return '{}/panel'.format(get_setup_path()) + + +# 读取文件 +def ReadFile(filename, mode='r'): + """ + 读取文件内容 + @filename 文件名 + return string(bin) 若文件不存在,则返回None + """ + import os + if not os.path.exists(filename): return False + fp = None + try: + fp = open(filename, mode) + f_body = fp.read() + except Exception as ex: + if sys.version_info[0] != 2: + try: + fp = open(filename, mode, encoding="utf-8", errors='ignore') + f_body = fp.read() + except: + fp = open(filename, mode, encoding="GBK", errors='ignore') + f_body = fp.read() + else: + return False + finally: + if fp and not fp.closed: + fp.close() + return f_body + + +# 读取文件 +def readFile(filename, mode='r'): + ''' + @name 读取指定文件数据 + @author hwliang<2021-06-09> + @param filename 文件名 + @param mode 文件打开模式,默认r + @return string or bytes or False 如果返回False则说明读取失败 + ''' + return ReadFile(filename, mode) + + +# 写入文件 +def WriteFile(filename, s_body, mode='w+'): + """ + 写入文件内容 + @filename 文件名 + @s_body 欲写入的内容 + return bool 若文件不存在则尝试自动创建 + """ + try: + fp = open(filename, mode) + fp.write(s_body) + fp.close() + return True + except: + try: + fp = open(filename, mode, encoding="utf-8") + fp.write(s_body) + fp.close() + return True + except: + return False + + +# 写入文件 +def writeFile(filename, s_body, mode='w+'): + ''' + @name 写入到指定文件 + @author hwliang<2021-06-09> + @param filename 文件名 + @param s_boey 被写入的内容,字节或字符串 + @param mode 文件打开模式,默认w+ + @return bool + ''' + return WriteFile(filename, s_body, mode) + + +# 创建临时目录 +def make_panel_tmp_path() -> str: + tmp_path = '{}/temp/tmp_{}_{}'.format(get_panel_path(), int(time.time()), GetRandomString(32)) + if not os.path.exists(tmp_path): + os.makedirs(tmp_path, 0o755) + return tmp_path + + +# 创建临时目录(使用上下文管理器) +@contextlib.contextmanager +def make_panel_tmp_path_with_context(): + tmp_path = make_panel_tmp_path() + + import shutil + + try: + yield tmp_path + finally: + # 删除临时目录 + shutil.rmtree(tmp_path) + + diff --git a/class/public/exceptions.py b/class/public/exceptions.py new file mode 100644 index 00000000..5901e7a2 --- /dev/null +++ b/class/public/exceptions.py @@ -0,0 +1,11 @@ +# 异常类 +# @author Zhj<2024/06/27> + +# 提示类异常 会正常响应 +class HintException(Exception): + pass + + +# 无授权异常 +class NoAuthorizationException(HintException): + pass diff --git a/class/public/mysqlmgr.py b/class/public/mysqlmgr.py new file mode 100644 index 00000000..cb440242 --- /dev/null +++ b/class/public/mysqlmgr.py @@ -0,0 +1,136 @@ +# MySQL管理公共模块 +# @author Zhj<2024/07/01> +import os +import json +import time +import typing + +from .common import M, aap_t_simple_result, aap_t_mysql_dump_info, to_dict_obj, get_msg_gettext, get_mysqldump_bin, MysqlConn, get_database_character, ExecShell +from .exceptions import HintException + + +# 备份MySQL数据库 +def backup(database_id: int) -> aap_t_simple_result: + from database_v2 import database + data = database().ToBackup(to_dict_obj({'id': database_id})) + + if int(data.get('status', 0)) != 0: + return aap_t_simple_result(False, data.get('message', {})['result']) + + return aap_t_simple_result(True, M('backup').where('type = 1 and pid=?', (database_id,)).order('id desc').getField('filename')) + + +# 还原MySQL数据库 +def restore(db_name: str, bak_file: str) -> aap_t_simple_result: + from database_v2 import database + data = database().InputSql(to_dict_obj({'name': db_name, 'file': bak_file})) + return aap_t_simple_result(int(data.get('status', 0)) == 0, data.get('message', {})['result']) + + +# 删除MySQL数据库备份文件 +def del_bak(bak_file: str) -> aap_t_simple_result: + # aapanel内部备份 + bak_id_dict = M('backup').where('`type`=1 and `filename`=?', (bak_file,)).field('id').find() + + if isinstance(bak_id_dict, dict): + from database_v2 import database + data = database().DelBackup(to_dict_obj({'id': int(bak_id_dict['id'])})) + return aap_t_simple_result(int(data.get('status', 0)) == 0, data.get('message', {})['result']) + + # 其它途径备份 + if not os.path.exists(bak_file): + return aap_t_simple_result(False, get_msg_gettext('File not exists')) + + os.remove(bak_file) + + return aap_t_simple_result(True, get_msg_gettext('Remove backup successfully')) + + +# 数据库导出 +def dumpsql_with_aap(database_id: int, backup_path: typing.Optional[str] = None) -> aap_t_mysql_dump_info: + import shlex + db_find = M('databases').where("id=?", (database_id,)).find() + + if not isinstance(db_find, dict): + raise HintException(get_msg_gettext('Table {} has been corrupted', ('databases',))) + + if backup_path is None: + backup_path_tmp = M('config').order('`id` desc').limit(1).field('backup_path').find() + + if not isinstance(backup_path_tmp, dict): + raise HintException(get_msg_gettext('Table {} has been corrupted', ('config',))) + + backup_path = os.path.join(str(backup_path_tmp['backup_path']), 'database') + + name = db_find['name'] + fileName = name + '_' + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.sql.gz' + backupName = os.path.join(backup_path, fileName) + mysqldump_bin = get_mysqldump_bin() + + from database_v2 import database + database_obj = database() + + if db_find['db_type'] in ['0', 0]: + # 本地数据库 + # 测试数据库连接 + with MysqlConn() as conn: + conn.execute("show databases") + + root = M('config').where('id=?', (1,)).getField('mysql_root') + if not os.path.exists(backup_path): + os.makedirs(backup_path, 0o600) + + if not database_obj.mypass(True, root): + raise HintException(get_msg_gettext("Database configuration file failed to get checked, please check " + "if MySQL configuration file exists [/etc/my.cnf]")) + + try: + password = M('config').where('id=?', (1,)).getField('mysql_root') + if not password: + raise HintException(get_msg_gettext("Database password cannot be empty")) + + password = shlex.quote(str(password)) + os.environ["MYSQL_PWD"] = password + ExecShell(mysqldump_bin + " -R -E --triggers=false --default-character-set=" + get_database_character(name) + " --force --opt \"" + name + "\" -u root -p" + password + " | gzip > " + backupName) + finally: + os.environ["MYSQL_PWD"] = "" + + database_obj.mypass(False, root) + + elif db_find['db_type'] in ['1', 1]: + # 远程数据库 + try: + conn_config = json.loads(db_find['conn_config']) + res = database_obj.CheckCloudDatabase(conn_config) + if isinstance(res, dict): + raise HintException(res.get('msg', get_msg_gettext('Cannot connect to remote MySQL'))) + + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = password + ExecShell(mysqldump_bin + " -h " + conn_config['db_host'] + " -P " + str(int(conn_config['db_port'])) + " -R -E --triggers=false --default-character-set=" + get_database_character(name) + " --force --opt \"" + str(db_find['name']) + "\" -u " + str(conn_config['db_user']) + " -p" + password + " | gzip > " + backupName) + finally: + os.environ["MYSQL_PWD"] = "" + + elif db_find['db_type'] in ['2', 2]: + try: + conn_config = M('database_servers').where('id=?', db_find['sid']).find() + res = database_obj.CheckCloudDatabase(conn_config) + if isinstance(res, dict): + raise HintException(res.get('msg', get_msg_gettext('Cannot connect to remote MySQL'))) + + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = password + ExecShell(mysqldump_bin + " -h " + conn_config['db_host'] + " -P " + str(int(conn_config['db_port'])) + " -R -E --triggers=false --default-character-set=" + get_database_character(name) + " --force --opt \"" + str(db_find['name']) + "\" -u " + str(conn_config['db_user']) + " -p" + str(conn_config['db_password']) + " | gzip > " + backupName) + finally: + os.environ["MYSQL_PWD"] = "" + + else: + raise HintException(get_msg_gettext("Unsupported database type")) + + if not os.path.exists(backupName): + raise HintException(get_msg_gettext("Backup error")) + + # # 将备份信息添加到数据库中 + # bak_id = M('backup').add('type,name,pid,filename,size,addtime', (1, fileName, id, backupName, 0, time.strftime('%Y-%m-%d %X', time.localtime()))) + + return aap_t_mysql_dump_info(db_name=str(db_find['name']), file=backupName, dump_time=int(time.time())) diff --git a/class/public/regexplib.py b/class/public/regexplib.py new file mode 100644 index 00000000..8dea51f6 --- /dev/null +++ b/class/public/regexplib.py @@ -0,0 +1,29 @@ +import re + +# 匹配IP地址 +match_ipv4 = re.compile(r'^(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)$') +match_ipv6 = re.compile(r'^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|(?:(?:[0-9a-fA-F]{1,4}:){1,7}:)|(?:(?:[0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})|(?:(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2})|(?:(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3})|(?:(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4})|(?:(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5})|(?:(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6})|(?::(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))') + +# 安全文件路径 +match_safe_path = re.compile(r'^[\w\s./\-]*$') + +# 匹配类私有属性名称 +match_class_private_property = re.compile(r'^(?:_\w+)?__\w+') + +# HOST基本格式 +match_based_host = re.compile(r'^[\w.:\-]+$') + +# 抓取URL根路径 +find_url_root = re.compile(r'(https|http)://([\w:.\-]+)', re.IGNORECASE) + +# 匹配首条PHP Fatal error信息 +search_php_first_fatal_error = re.compile(r'PHP Fatal error: \s*([^\r\n]+)') + +# 匹配HTTP响应报文中的状态行 (Status Line) +search_http_response_status_line = re.compile(r'HTTP/\d+(?:\.\d+)? (\d{1,3}) \S*') + +# 通用版本号格式验证 major.minor[.patch]/主版本.子版本[.修订号] +match_general_version_format = re.compile(r'^\d+(?:\.\d+){1,2}$') + +# md5格式验证 +match_md5_format = re.compile(r'^[a-fA-F0-9]{32}$') diff --git a/class/public/tools.py b/class/public/tools.py new file mode 100644 index 00000000..acd1ac21 --- /dev/null +++ b/class/public/tools.py @@ -0,0 +1,35 @@ +import typing + + +def my_pipe(val: any, fs: typing.List[callable]) -> any: + """ + 管道数据过滤函数 + @param val: any + @param fs: callable 数据过滤函数 + @return: any + """ + from functools import reduce + return reduce(lambda x, y: y(x), fs, val) + + +def is_number(s) -> bool: + """ + @name 判断输入参数是否一个数字 + @author Zhj<2022-07-18> + @param s 输入参数 + @return bool + """ + try: + float(s) + return True + except ValueError: + pass + + try: + import unicodedata + unicodedata.numeric(s) + return True + except (TypeError, ValueError): + pass + + return False diff --git a/class/public/translations.py b/class/public/translations.py new file mode 100644 index 00000000..2bd53eeb --- /dev/null +++ b/class/public/translations.py @@ -0,0 +1,24 @@ +# Language translations +import os +import glob +import json +import public + +translations = {} + + +# Load translations +def load_translations(): + if len(translations.keys()) > 0: + return translations + + scan_pattern = '{}/BTPanel/static/vite/lang/*/*.json'.format(public.get_panel_path()) + + for path in glob.glob(scan_pattern): + lan = os.path.basename(os.path.dirname(path)) + if lan not in translations: + translations[lan] = {} + with open(path, 'r') as fp: + translations[lan].update(json.loads(fp.read())) + + return translations diff --git a/class/public/validate.py b/class/public/validate.py new file mode 100644 index 00000000..92d48a32 --- /dev/null +++ b/class/public/validate.py @@ -0,0 +1,1253 @@ +import re +import json +import socket +import os +import typing +from .regexplib import match_ipv4, match_ipv6, match_safe_path, match_based_host +from .exceptions import HintException + + +class Param: + __VALIDATE_OPTS = [ + '>', + '<', + '>=', + '<=', + '=', + 'in', + 'not in', + ] + + def __init__(self, name: str): + self.name: str = name + self.__validate_rules: typing.List[_ValidateRule] = [] + self.__filters: typing.List[callable] = [] + + # 验证器 Begin -----> + + def Require(self): + """ + 必选参数 + @return: self + """ + self.__validate_rules.append(_RequireValidation(self.name)) + return self + + def Date(self): + """ + 日期字符串 + @return: self + """ + self.__validate_rules.append(_DateValidation(self.name)) + return self + + def Timestamp(self): + """ + Unix时间戳 + @return: self + """ + self.__validate_rules.append(_TimestampValidation(self.name)) + return self + + def Url(self): + """ + URL + @return: self + """ + self.__validate_rules.append(_UrlValidation(self.name)) + return self + + def Ip(self): + """ + IP地址 + @return: self + """ + self.__validate_rules.append(_IpValidation(self.name)) + return self + + def Ipv4(self): + """ + IPv4地址 + @return: self + """ + self.__validate_rules.append(_Ipv4Validation(self.name)) + return self + + def Ipv6(self): + """ + IPv6地址 + @return: self + """ + self.__validate_rules.append(_Ipv6Validation(self.name)) + return self + + def Host(self): + """ + 主机地址(可以包含端口号) + @return: self + """ + self.__validate_rules.append(_HostValidation(self.name)) + return self + + def Port(self): + """ + 端口号 + @return: self + """ + return self.Integer('between', [1, 65535]) + + def Json(self): + """ + JSON字符串 + @return: self + """ + self.__validate_rules.append(_JsonValidation(self.name)) + return self + + def Array(self): + """ + JSON-Array字符串 + @return: self + """ + self.__validate_rules.append(_ArrayValidation(self.name)) + return self + + def Object(self): + """ + JSON-Object字符串 + @return: self + """ + self.__validate_rules.append(_ObjectValidation(self.name)) + return self + + def List(self): + """ + 限制参数数据类型:list + @return: self + """ + self.__validate_rules.append(_ListValidation(self.name)) + return self + + def Tuple(self): + """ + 限制参数数据类型:tuple + @return: self + """ + self.__validate_rules.append(_TupleValidation(self.name)) + return self + + def Dict(self): + """ + 限制参数数据类型:dict + @return: self + """ + self.__validate_rules.append(_DictValidation(self.name)) + return self + + def Bool(self): + """ + 布尔值或boolean字符串 true/false + @return: self + """ + self.__validate_rules.append(_BoolValidation(self.name)) + return self + + def String(self, opt: typing.Optional[str] = None, length_or_list: typing.Optional[typing.Union[int, typing.List[typing.Union[int, str]]]] = None): + """ + 字符串 + @param opt: str 运算符 + @param length_or_list: int|list[int|str]|None 字符串长度或字符串集合 + @return: self + """ + self.__validate_rules.append(_StringValidation(self.name, opt, length_or_list)) + return self + + def Number(self, opt: typing.Optional[str] = None, num: typing.Optional[typing.Union[int, float, typing.List[typing.Union[int, float]]]] = None): + """ + 数值 + @param opt: str 运算符 + @param num: int 数值大小 + @return: self + """ + self.__validate_rules.append(_NumberValidation(self.name, opt, num)) + return self + + def Integer(self, opt: typing.Optional[str] = None, num: typing.Optional[typing.Union[typing.Union[int, typing.List[int]]]] = None): + """ + 整数 + @param opt: str 运算符 + @param num: int 数值大小 + @return: self + """ + self.__validate_rules.append(_IntegerValidation(self.name, opt, num)) + return self + + def Float(self, opt: typing.Optional[str] = None, num: typing.Optional[typing.Union[int, float, typing.List[typing.Union[int, float]]]] = None): + """ + 浮点数 + @param opt: str 运算符 + @param num: int 数值大小 + @return: self + """ + self.__validate_rules.append(_FloatValidation(self.name, opt, num)) + return self + + def Alpha(self, opt: typing.Optional[str] = None, length_or_list: typing.Optional[typing.Union[int, typing.List[typing.Union[int, str]]]] = None): + """ + 纯字母 + @param opt: str 运算符 + @param length_or_list: int|list[int|str]|None 字符串长度或字符串集合 + @return: self + """ + self.__validate_rules.append(_AlphaValidation(self.name, opt, length_or_list)) + return self + + def Alphanum(self, opt: typing.Optional[str] = None, length_or_list: typing.Optional[typing.Union[int, typing.List[typing.Union[int, str]]]] = None): + """ + 字母+数字 + @param opt: str 运算符 + @param length_or_list: int|list[int|str]|None 字符串长度或字符串集合 + @return: self + """ + self.__validate_rules.append(_AlphanumValidation(self.name, opt, length_or_list)) + return self + + def Mobile(self): + """ + (中国)手机号码 + @return: self + """ + self.__validate_rules.append(_MobileValidation(self.name)) + return self + + def Email(self): + """ + 邮箱地址 + @return: self + """ + self.__validate_rules.append(_EmailValidation(self.name)) + return self + + def Regexp(self, exp: str): + """ + 正则表达式 + @param exp: str 正则表达式 + @return: self + """ + self.__validate_rules.append(_RegexpValidation(self.name, exp)) + return self + + def File(self): + """ + 文件上传 + @return: self + """ + self.__validate_rules.append(_FileValidation(self.name)) + return self + + def Size(self, opt: typing.Optional[str] = None, size: typing.Optional[typing.Union[int, typing.List[int]]] = None): + """ + 上传文件大小 + @param opt: str 运算符 + @param size: int 上传文件大小bytes + @return: self + """ + self.__validate_rules.append(_SizeValidation(self.name, opt, size)) + return self + + def Mime(self, opt: typing.Optional[str] = None, mime_type: typing.Optional[typing.Union[str, typing.List[str]]] = None): + """ + 上传文件Mimetype + @param opt: str 运算符 + @param mime_type: str 上传文件Mimetype + @return: self + """ + self.__validate_rules.append(_MimeValidation(self.name, opt, mime_type)) + return self + + def Ext(self, opt: typing.Optional[str] = None, ext: typing.Optional[typing.Union[str, typing.List[str]]] = None): + """ + 上传文件后缀名 + @param opt: str 运算符 + @param ext: str 上传文件后缀名 + @return: self + """ + self.__validate_rules.append(_ExtValidation(self.name, opt, ext)) + return self + + def SafePath(self): + """ + 文件路径 + @return: self + """ + self.__validate_rules.append(_SafePathValidation(self.name)) + return self + + # <------- 验证器 End + + # 过滤器 Begin ------> + + def Trim(self): + """ + 去除字符串两端空白字符 + @return: self + """ + self.__filters.append(lambda x: str(x).strip()) + return self + + def Xss(self): + """ + XSS过滤 + @return: self + """ + self.__filters.append(_xssencode) + return self + + def Filter(self, f: callable): + """ + 自定义参数过滤器 + @param f: callable func(x: any) -> any + @return: self + """ + self.__filters.append(f) + return self + + # <------- 过滤器 End + + def do_validate(self, args: dict): + """ + 执行验证器 + @param args: dict 请求参数列表 + @return: self + """ + for v in self.__validate_rules: + v.validate(args) + + return self + + def do_filter(self, val, extra_filters: typing.List[callable] = []) -> any: + """ + 执行参数过滤器 + @param val: any + @param extra_filters: list[callable] + @return: any + """ + from functools import reduce + return reduce(lambda x, y: y(x), extra_filters + self.__filters, val) + + +class _ValidateRule: + def validate(self, args: dict): + raise NotImplementedError('method validate() not implemented.') + + +class _RequireValidation(_ValidateRule): + """ + 必选参数验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} is required' + + def validate(self, args: dict): + if self.name in args: + return + + if 'FILES' in args and self.name in args['FILES']: + return + + raise HintException(self.errmsg.format(self.name)) + + +class _DateValidation(_ValidateRule): + """ + 日期字符串验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid datetime' + + def validate(self, args: dict): + if self.name not in args: + return + + if re.match(r'^(?:\d{2}-\d{2}-\d{2}|\d{2}/\d{2}/\d{2})(?: \d{2}:\d{2}(?::\d{2})?)?$', + str(args[self.name]).strip()): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _TimestampValidation(_ValidateRule): + """ + Unix时间戳验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid timestamp' + + def validate(self, args: dict): + if self.name not in args: + return + + if re.match(r'^\d{10}$', str(args[self.name]).strip()): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _UrlValidation(_ValidateRule): + """ + URL地址验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid URL' + + def validate(self, args: dict): + if self.name not in args: + return + + regex_obj = re.compile( + r'^(?:http|ftp)s?://' + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' + r'localhost|' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + + if regex_obj.match(str(args[self.name]).strip()): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _IpValidation(_ValidateRule): + """ + IP地址验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid IP' + + def validate(self, args: dict): + if self.name not in args: + return + + ipstr = str(args[self.name]).strip() + + if _is_ipv4(ipstr) or _is_ipv6(ipstr): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _Ipv4Validation(_ValidateRule): + """ + IPv4地址验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid IPv4' + + def validate(self, args: dict): + if self.name not in args: + return + + if _is_ipv4(str(args[self.name]).strip()): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _Ipv6Validation(_ValidateRule): + """ + IPv6地址验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid IPv4' + + def validate(self, args: dict): + if self.name not in args: + return + + if _is_ipv6(str(args[self.name]).strip()): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _HostValidation(_ValidateRule): + """ + 主机地址验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid HOST' + + def validate(self, args: dict): + if self.name not in args: + return + + if match_based_host.match(str(args[self.name]).strip()): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _JsonValidation(_ValidateRule): + """ + JSON字符串验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid JSON' + + def validate(self, args: dict): + if self.name not in args: + return + + try: + json.loads(str(args[self.name]).strip()) + return + except: + pass + + raise HintException(self.errmsg.format(self.name)) + + +class _ArrayValidation(_ValidateRule): + """ + JSON-Array字符串验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid JSON Array' + + def validate(self, args: dict): + if self.name not in args: + return + + try: + obj = json.loads(str(args[self.name]).strip()) + + if isinstance(obj, list): + return + except: + pass + + raise HintException(self.errmsg.format(self.name)) + + +class _ObjectValidation(_ValidateRule): + """ + JSON-Object字符串验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid JSON Object' + + def validate(self, args: dict): + if self.name not in args: + return + + try: + obj = json.loads(str(args[self.name]).strip()) + + if isinstance(obj, dict): + return + except: + pass + + raise HintException(self.errmsg.format(self.name)) + + +class _BoolValidation(_ValidateRule): + """ + bool字符串验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} must be bool' + + def validate(self, args: dict): + if self.name not in args: + return + + val = args[self.name] + + if isinstance(val, bool) or re.match(r'^true|false$', str(args[self.name]).strip(), re.IGNORECASE): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _ListValidation(_ValidateRule): + """ + list数据类型验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} must be list' + + def validate(self, args: dict): + if self.name not in args: + return + + if isinstance(args[self.name], list): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _TupleValidation(_ValidateRule): + """ + tuple数据类型验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} must be tuple' + + def validate(self, args: dict): + if self.name not in args: + return + + if isinstance(args[self.name], tuple): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _DictValidation(_ValidateRule): + """ + dict数据类型验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} must be dict' + + def validate(self, args: dict): + if self.name not in args: + return + + if isinstance(args[self.name], dict): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _OperationHelper: + """ + 运算辅助类 + """ + + def __init__(self, name: str, opt: typing.Optional[str], operand: typing.Optional[typing.Union[str, int, float, typing.List[typing.Union[str, int, float]]]], data_type): + self.name: str = name + self.opt = opt + self.operand = operand + self.data_type = data_type + self._opt_check() + self._data_type_check() + + def do(self, val: typing.Union[str, int, float], data_type=None): + val = str(val).strip() + + if data_type is not None: + self.data_type = data_type + self._data_type_check() + + if self.opt is None: + return + + if self.operand is None: + return + + if self.opt == '=': + self._eq(self._calc_num(val)) + elif self.opt == '>': + self._gt(self._calc_num(val)) + elif self.opt == '>=': + self._gte(self._calc_num(val)) + elif self.opt == '<': + self._lt(self._calc_num(val)) + elif self.opt == '<=': + self._lte(self._calc_num(val)) + elif self.opt == 'between': + self._between(self._calc_num(val)) + elif self.opt == 'in': + self._in(self.data_type(val)) + elif self.opt == 'not in': + self._not_in(self.data_type(val)) + + def _calc_num(self, val: str) -> typing.Union[int, float]: + if self.data_type is str: + return len(val) + + return self.data_type(val) + + def _opt_check(self): + if self.opt is None: + return + + self.opt = self.opt.lower() + + if self.operand is None: + return + + if self.opt in ['=', '>', '<', '>=', '<='] and not isinstance(self.operand, int): + raise HintException('当运算符opt是 \'{}\' 时,运算数只能是int类型或float类型,当前类型 {}'.format(self.opt, type(self.operand))) + + if self.opt in ['in', 'not in', 'between'] and not isinstance(self.operand, list): + raise HintException('当运算符opt是 \'{}\' 时,运算数只能是list类型,当前类型 {}'.format(self.opt, type(self.operand))) + + def _data_type_check(self): + if self.data_type is str: + return + + if self.data_type is int: + return + + if self.data_type is float: + return + + raise HintException('data_type只能是str、int、float 当前 {}'.format(self.data_type)) + + def _eq(self, num: typing.Union[int, float]): + if num == self.operand: + return + + raise HintException( + '{}{} must equal {}'.format(self.name, ' length' if isinstance(self.data_type, str) else '', self.operand)) + + def _gt(self, num: typing.Union[int, float]): + if num > self.operand: + return + + raise HintException( + '{}{} must greater than {}'.format(self.name, ' length' if isinstance(self.data_type, str) else '', + self.operand)) + + def _gte(self, num: typing.Union[int, float]): + if num >= self.operand: + return + + raise HintException( + '{}{} must greater than or equal {}'.format(self.name, ' length' if isinstance(self.data_type, str) else '', + self.operand)) + + def _lt(self, num: typing.Union[int, float]): + if num < self.operand: + return + + raise HintException( + '{}{} must less than {}'.format(self.name, ' length' if isinstance(self.data_type, str) else '', + self.operand)) + + def _lte(self, num: typing.Union[int, float]): + if num <= self.operand: + return + + raise HintException( + '{}{} must less than or equal {}'.format(self.name, ' length' if isinstance(self.data_type, str) else '', + self.operand)) + + def _between(self, num: typing.Union[int, float]): + if len(self.operand) != 2: + raise HintException('当运算符opt是 \'between\' 时,运算数只能是list类型,并且list的长度只能是2,当前list长度 {}'.format(len(self.operand))) + + if num >= self.operand[0] and num <= self.operand[1]: + return + + raise HintException( + '{}{} must between {} and {}'.format(self.name, ' length' if isinstance(self.data_type, str) else '', + self.operand[0], self.operand[1])) + + def _in(self, item: typing.Union[int, float, str]): + if len(self.operand) < 1: + raise HintException('当运算符opt是 \'{}\' 时,运算数只能是list类型,并且list的长度必须大于0,当前list长度 0'.format(self.opt)) + + if item in self.operand: + return + + raise HintException('{} must in {}'.format(self.name, self.operand)) + + def _not_in(self, item: typing.Union[int, float, str]): + if len(self.operand) < 1: + raise HintException('当运算符opt是 \'{}\' 时,运算数只能是list类型,并且list的长度必须大于0,当前list长度 0'.format(self.opt)) + + if item in self.operand: + raise HintException('{} must not in {}'.format(self.name, self.operand)) + + +class _StringValidation(_ValidateRule): + """ + 字符串验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, v: typing.Optional[typing.Union[int, typing.List[typing.Union[int, str]]]] = None): + self.name: str = name + self.errmsg: str = '{} must be string' + self.op = _OperationHelper(name, opt, v, str) + + def validate(self, args: dict): + if self.name not in args: + return + + s = args[self.name] + + if isinstance(s, str): + self.op.do(s) + return + + raise HintException(self.errmsg.format(self.name)) + + +class _NumberValidation(_ValidateRule): + """ + 数字验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, num: typing.Optional[typing.Union[int, float, typing.List[typing.Union[int, float]]]] = None): + self.name: str = name + self.errmsg: str = '{} must be number' + self.op = _OperationHelper(name, opt, num, float) + + def validate(self, args: dict): + if self.name not in args: + return + + num = args[self.name] + + if _is_number(num): + self.op.do(num, _get_number_data_type(num)) + return + + raise HintException(self.errmsg.format(self.name)) + + +class _IntegerValidation(_ValidateRule): + """ + 整数验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, num: typing.Optional[typing.Union[int, typing.List[int]]] = None): + self.name: str = name + self.errmsg: str = '{} must be integer' + self.op = _OperationHelper(name, opt, num, int) + + def validate(self, args: dict): + if self.name not in args: + return + + num = args[self.name] + + if _is_int(num): + self.op.do(num) + return + + raise HintException(self.errmsg.format(self.name)) + + +class _FloatValidation(_ValidateRule): + """ + 浮点数验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, num: typing.Optional[typing.Union[float, typing.List[float]]] = None): + self.name: str = name + self.errmsg: str = '{} must be float' + self.op = _OperationHelper(name, opt, num, float) + + def validate(self, args: dict): + if self.name not in args: + return + + num = args[self.name] + + if _is_float(num): + self.op.do(num) + return + + raise HintException(self.errmsg.format(self.name)) + + +class _AlphaValidation(_ValidateRule): + """ + 纯字母验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, v: typing.Optional[typing.Union[int, typing.List[typing.Union[int, str]]]] = None): + self.name: str = name + self.errmsg: str = '{} must be alpha' + self.op = _OperationHelper(name, opt, v, str) + + def validate(self, args: dict): + if self.name not in args: + return + + s = str(args[self.name]).strip() + + if re.match(r'^[a-zA-Z]+$', s): + self.op.do(s) + return + + raise HintException(self.errmsg.format(self.name)) + + +class _AlphanumValidation(_ValidateRule): + """ + 字母数字验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, v: typing.Optional[typing.Union[int, typing.List[typing.Union[int, str]]]] = None): + self.name: str = name + self.errmsg: str = '{} must be alphanum' + self.op = _OperationHelper(name, opt, v, str) + + def validate(self, args: dict): + if self.name not in args: + return + + s = str(args[self.name]).strip() + + if re.match(r'^[a-zA-Z0-9]+$', s): + self.op.do(s) + return + + raise HintException(self.errmsg.format(self.name)) + + +class _MobileValidation(_ValidateRule): + """ + (中国)手机号码验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid mobile' + + def validate(self, args: dict): + if self.name not in args: + return + + s = str(args[self.name]).strip() + + if re.match(r'^1[3-9]\d{9}$', s): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _EmailValidation(_ValidateRule): + """ + 邮箱地址验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid email' + + def validate(self, args: dict): + if self.name not in args: + return + + s = str(args[self.name]).strip() + + if re.match(r'^.+@(\[?)[a-zA-Z0-9\-.]+\.(?:[a-zA-Z]{2,}|\d{1,3})\1$', s): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _RegexpValidation(_ValidateRule): + """ + 正则表达式验证类 + """ + + def __init__(self, name: str, regexp: str): + self.name: str = name + self.errmsg: str = '{} not success verified by regexp' + self.regexp: str = regexp + + def validate(self, args: dict): + if self.name not in args: + return + + s = str(args[self.name]).strip() + + if re.match(self.regexp, s): + return + + raise HintException(self.errmsg.format(self.name)) + + +class _FileValidation(_ValidateRule): + """ + 文件上传验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg: str = '{} not valid file' + + def validate(self, args: dict): + if 'FILES' in args and self.name in args['FILES']: + return + + raise HintException(self.errmsg.format(self.name)) + + +class _SizeValidation(_ValidateRule): + """ + 文件大小验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, size: typing.Optional[typing.Union[int, typing.List[int]]] = None): + self.name: str = name + self.op = _OperationHelper(name, opt, size, int) + + def validate(self, args: dict): + if 'FILES' not in args or self.name not in args['FILES']: + return + + self.op.do(args['FILES'][self.name].content_length) + + +class _MimeValidation(_ValidateRule): + """ + 文件mimetype验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, mime_type: typing.Optional[typing.Union[str, typing.List[str]]] = None): + self.name: str = name + self.op = _OperationHelper(name, opt, mime_type, str) + + def validate(self, args: dict): + if 'FILES' not in args or self.name not in args['FILES']: + return + + self.op.do(args['FILES'][self.name].mimetype) + + +class _ExtValidation(_ValidateRule): + """ + 文件后缀名验证类 + """ + + def __init__(self, name: str, opt: typing.Optional[str] = None, ext: typing.Optional[typing.Union[str, typing.List[str]]] = None): + self.name: str = name + self.op = _OperationHelper(name, opt, ext, str) + + def validate(self, args: dict): + if 'FILES' not in args or self.name not in args['FILES']: + return + + f = args['FILES'][self.name] + + self.op.do(os.path.splitext(f.filename)[-1]) + + +class _SafePathValidation(_ValidateRule): + """ + 文件路径名验证类 + """ + + def __init__(self, name: str): + self.name: str = name + self.errmsg = '{} not safe path' + + def validate(self, args: dict): + if self.name not in args: + return + + if _is_safe_path(str(args[self.name]).strip()): + return + + raise HintException(self.errmsg.format(self.name)) + + +def trim_filter() -> callable: + """ + 获取Trim参数过滤器 + @return: callable + """ + return lambda x: str(x).strip() + + +def xss_filter() -> callable: + """ + 获取XSS参数过滤器 + @return: callable + """ + return _xssencode + + +def _is_ipv4(ip: str) -> bool: + ''' + @name 是否是IPV4地址 + @author hwliang + @param ip IP地址 + @return True/False + ''' + # 验证基本格式 + if not match_ipv4.match(ip): + return False + + # 验证每个段是否在合理范围 + try: + socket.inet_pton(socket.AF_INET, ip) + except AttributeError: + try: + socket.inet_aton(ip) + except socket.error: + return False + except socket.error: + return False + return True + + +def _is_ipv6(ip: str) -> bool: + ''' + @name 是否为IPv6地址 + @author hwliang + @param ip 地址 + @return True/False + ''' + # 验证基本格式 + if not match_ipv6.match(ip): + return False + + # 验证IPv6地址 + try: + socket.inet_pton(socket.AF_INET6, ip) + except socket.error: + return False + return True + + +def _xssencode(text: str) -> str: + """ + XSS过滤 + @param text: str + @return bool + """ + try: + from cgi import html + list = ['`', '~', '&', '#', '/', '*', '$', '@', '<', '>', '\"', '\'', ';', '%', ',', '.', '\\u'] + ret = [] + for i in text: + if i in list: + i = '' + ret.append(i) + str_convert = ''.join(ret) + text2 = html.escape(str_convert, quote=True) + return text2 + except: + return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') + + +def _is_safe_path(path: str, force: bool = True) -> bool: + """ + 文件路径过滤 + @param path: str + @param force: bool + @return: bool + """ + if len(path) > 256: + return False + + checks = ['..', './', '\\', '%', '$', '^', '&', '*', '~', '"', "'", ';', '|', '{', '}', '`'] + + for c in checks: + if path.find(c) > -1: + return False + + if force: + if not match_safe_path.match(path): + return False + + return True + + +def _is_number(s) -> bool: + """ + @name 判断输入参数是否一个数字 + @author Zhj<2022-07-18> + @param s 输入参数 + @return bool + """ + try: + float(s) + return True + except ValueError: + pass + + try: + import unicodedata + unicodedata.numeric(s) + return True + except (TypeError, ValueError): + pass + + return False + + +def _is_int(s) -> bool: + """ + 判断输入是否是整数 + @param s: any + @return bool + """ + try: + int(s) + return True + except ValueError: + pass + + return False + + +def _is_float(s) -> bool: + """ + 判断输入是否是浮点数 + @param s: any + @return bool + """ + try: + float(s) + return True + except ValueError: + pass + + return False + + +def _get_number_data_type(s): + """ + 获取数字的数据类型 + @param s 输入参数 + @return int|float + """ + try: + int(s) + return int + except ValueError: + pass + + return float diff --git a/class/public/websitemgr.py b/class/public/websitemgr.py new file mode 100644 index 00000000..8a8912f0 --- /dev/null +++ b/class/public/websitemgr.py @@ -0,0 +1,181 @@ +# 网站管理公共模块 +# @author Zhj<2024/06/15> +import os +import re +import json +import typing + +import public +from .common import M, aap_t_simple_result, to_dict_obj, dict_obj, get_msg_gettext, get_setup_path, readFile +from .exceptions import HintException + + +import collections + + +# 简单网站信息 +aap_t_simple_site_info = collections.namedtuple('aap_t_simple_site_info', ['site_id', 'database_id']) + + +# 获取当前部署的Web服务器 +def get_webserver(): + if os.path.exists('{}/apache/bin/apachectl'.format(get_setup_path())): + webserver = 'apache' + elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): + webserver = 'openlitespeed' + else: + webserver = 'nginx' + return webserver + + +# 查询网站对应的PHP版本 +def get_site_php_version(siteName: str) -> str: + try: + webserver = get_webserver() + setup_path = get_setup_path() + + conf = readFile( + '{setup_path}/panel/vhost/{webserver}/{siteName}.conf'.format(setup_path=setup_path, webserver=webserver, + siteName=siteName)) + if webserver == 'openlitespeed': + conf = readFile(setup_path + '/panel/vhost/' + webserver + '/detail/' + siteName + '.conf') + if webserver == 'nginx': + rep = r"enable-php-(\w{2,5})[-\w]*\.conf" + elif webserver == 'apache': + rep = r"php-cgi-(\w{2,5})\.sock" + else: + rep = r"path\s*/usr/local/lsws/lsphp(\d+)/bin/lsphp" + + tmp = re.search(rep, conf).groups() + + if tmp[0] == '00': + return 'Static' + + if tmp[0] == 'other': + return 'Other' + + return tmp[0][0] + '.' + tmp[0][1] + except: + return 'Static' + + +# 修复网站文件权限 +def fix_permissions(site_root_path_or_site_file: str) -> aap_t_simple_result: + """ + :param site_root_path_or_site_file: str 网站根目录或者单一网站文件 + :return: + """ + from files_v2 import files + data = files().fix_permissions(to_dict_obj({'path': site_root_path_or_site_file})) + + if int(data.get('status', 0)) != 0: + return aap_t_simple_result(False, data.get('msg', 'Failed to fix permission')) + + return aap_t_simple_result(True, data.get('msg', 'Fix permission successfully')) + + +# 备份网站文件 +def backup_files(site_id: int) -> aap_t_simple_result: + from panel_site_v2 import panelSite + data = panelSite().ToBackup(to_dict_obj({'id': site_id})) + + if int(data.get('status', 0)) != 0: + return aap_t_simple_result(False, data.get('message', {})['result']) + + return aap_t_simple_result(True, M('backup').where('type = 0 and pid=?', (site_id,)).order('id desc').getField('filename')) + + +# 还原网站文件 +def restore_files(site_id: int, bak_file: str) -> aap_t_simple_result: + from panel_restore_v2 import panel_restore + data = panel_restore().restore_website_backup(to_dict_obj({'site_id': site_id, 'file_name': os.path.basename(bak_file)})) + return aap_t_simple_result(int(data.get('status', 0)) == 0, data.get('message', {})['result']) + + +# 删除网站备份文件 +def del_bak(bak_file: str) -> aap_t_simple_result: + # aapanel内部备份 + bak_id_dict = M('backup').where('`type`=0 and `filename` = ?', (bak_file,)).field('id').find() + + if isinstance(bak_id_dict, dict): + from panel_site_v2 import panelSite + data = panelSite().DelBackup(to_dict_obj({'id': int(bak_id_dict['id'])})) + return aap_t_simple_result(int(data.get('status', 0)) == 0, data.get('message', {})['result']) + + # 其它途径备份 + if not os.path.exists(bak_file): + return aap_t_simple_result(False, get_msg_gettext('File not exists')) + + os.remove(bak_file) + + return aap_t_simple_result(True, get_msg_gettext('Remove backup successfully')) + + +# 创建PHP站点 +def create_php_site_with_mysql(domain: str, site_path: str, php_ver_short: str, db_user: str, db_pwd: str, another_domains: typing.List = ()) -> aap_t_simple_site_info: + """ + :param domain: str 网站主域名 + :param site_path: str 网站根目录(绝对路径) + :param php_ver_short: str PHP版本号缩写 54、74、80、81... + :param db_user: str 数据库用户名 + :param db_pwd: str 数据库用户密码 + :param another_domains: list 网站其它解析域名 + :return: aap_t_simple_site_info + """ + from panel_site_v2 import panelSite + data = panelSite().AddSite(to_dict_obj({ + 'webname': json.dumps({ + 'domain': domain, + 'domainlist': list(another_domains), + 'count': 0, + }), + 'type': 'PHP', + 'version': php_ver_short, + 'port': '80', + 'path': site_path, + 'sql': 'MySQL', + 'datauser': db_user, + 'datapassword': db_pwd, + 'codeing': 'utf8mb4', + 'ps': domain.replace('.', '_').replace('-', '_'), + })) + + if int(data.get('status', 0)) != 0: + raise HintException(data.get('message', {})['result']) + + data = data.get('message', {}) + + if int(data.get('databaseStatus', 0)) != 1: + raise HintException(public.get_msg_gettext('Database creation failed. Please check mysql running status and try again.')) + + return aap_t_simple_site_info(data['siteId'], data['d_id']) + + +# 删除站点 +def remove_site(site_id: int) -> public.aap_t_simple_result: + site_info = M('sites').where('`id` = ?', (site_id,)).field('name').find() + + if not isinstance(site_info, dict): + return public.aap_t_simple_result(False, public.get_msg_gettext('No found site-info with id {}'.format(site_id))) + + from panel_site_v2 import panelSite + data = panelSite().DeleteSite(to_dict_obj({ + 'id': site_id, + 'webname': site_info['name'], + 'ftp': '1', + 'path': '1', + 'database': '1', + })) + + return aap_t_simple_result(int(data.get('status', 0)) == 0, data.get('message', {})['result']) + + +# 获取可用的PHP版本列表 +def get_available_php_ver_shorts(without_static: bool = True) -> typing.List[str]: + from panel_site_v2 import panelSite + lst = panelSite().GetPHPVersion(to_dict_obj({}), False) + + if without_static: + lst = filter(lambda x: x['version'] != '00', lst) + + return list(map(lambda x: x['version'], lst)) diff --git a/class/push/base_push.py b/class/push/base_push.py index a3e1a91a..a3e49155 100644 --- a/class/push/base_push.py +++ b/class/push/base_push.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- # | Copyright (c) 2015-2020 宝塔软件(https://www.bt.cn) All rights reserved. # +------------------------------------------------------------------- -# | Author: baozi +# | Author: baozi # | Author: baozi # +------------------------------------------------------------------- import sys,os,re,json diff --git a/class/push/panel_push.py b/class/push/panel_push.py index 709ade3f..fe46517f 100644 --- a/class/push/panel_push.py +++ b/class/push/panel_push.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- # | Copyright (c) 2015-2020 宝塔软件(https://www.bt.cn) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # | Author: lx # +------------------------------------------------------------------- import sys,os,time,psutil,re @@ -93,7 +93,7 @@ class panel_push: return status elif name == 'mysql': res = public.ExecShell("service mysqld status") - if res and not re.search("not\s+running", res[0]): + if res and not re.search(r"not\s+running", res[0]): return True return False elif name == 'tomcat': diff --git a/class/push/site_push.py b/class/push/site_push.py index 3101f0c8..e907df27 100644 --- a/class/push/site_push.py +++ b/class/push/site_push.py @@ -4,7 +4,7 @@ # +------------------------------------------------------------------- # | Copyright (c) 2015-2020 宝塔软件(https://www.bt.cn) All rights reserved. # +------------------------------------------------------------------- -# | Author: 沐落 +# | Author: 沐落 # +------------------------------------------------------------------- import sys, os, time, json, re, psutil @@ -158,7 +158,7 @@ class site_push: return status elif name == 'mysql': res = public.ExecShell("service mysqld status") - if res and not re.search("not\s+running", res[0]): + if res and not re.search(r"not\s+running", res[0]): return True return False elif name == 'tomcat': diff --git a/class/push/tamper_push.py b/class/push/tamper_push.py index 15222214..de28d7bf 100644 --- a/class/push/tamper_push.py +++ b/class/push/tamper_push.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- # | Copyright (c) 2015-2020 宝塔软件(https://www.bt.cn) All rights reserved. # +------------------------------------------------------------------- -# | Author: baozi +# | Author: baozi # | Author: baozi # +------------------------------------------------------------------- import sys,os,re,json diff --git a/class/safeModel/base.py b/class/safeModel/base.py index ee3d0546..3af5d5b9 100644 --- a/class/safeModel/base.py +++ b/class/safeModel/base.py @@ -15,7 +15,7 @@ class safeBase: #转换时间格式 def to_date(self,date_str): - tmp = re.split('\s+',date_str) + tmp = re.split(r'\s+',date_str) if len(tmp) < 3: return date_str s_date = str(datetime.now().year) + '-' + self._months.get(tmp[0]) + '-' + tmp[1] + ' ' + tmp[2] time_array = time.strptime(s_date, "%Y-%m-%d %H:%M:%S") diff --git a/class/safeModel/firewallModel.py b/class/safeModel/firewallModel.py index 56deb089..cf51c4e0 100644 --- a/class/safeModel/firewallModel.py +++ b/class/safeModel/firewallModel.py @@ -1,14 +1,14 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: hwliang -#------------------------------------------------------------------- +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- # 系统防火墙 -#------------------------------ +# ------------------------------ import sys, os, json, re, time, sqlite3 import contextlib import traceback @@ -23,7 +23,6 @@ import public class main(safeBase): - __isFirewalld = False __isUfw = False __firewall_obj = None @@ -133,32 +132,48 @@ class main(safeBase): def get_firewall_status(self): if self.__isUfw: res = public.ExecShell("systemctl is-active ufw")[0] - if res == "active": return True + if res == "active": + return True + res = public.ExecShell("systemctl list-units | grep ufw")[0] - if res.find('active running') != -1: return True + if res.find('active running') != -1: + return True + res = public.ExecShell('/lib/ufw/ufw-init status')[0] - if res.find("Firewall is not running") != -1: return False + if res.find("Firewall is not running") != -1: + return False + res = public.ExecShell('ufw status verbose')[0] - if res.find('inactive') != -1: return False + if res.find('inactive') != -1: + return False + return True if self.__isFirewalld: res = public.ExecShell("ps -ef|grep firewalld|grep -v grep")[0] - if res: return True + if res: + return True + res = public.ExecShell("systemctl is-active firewalld")[0] - if res == "active": return True + if res == "active": + return True + res = public.ExecShell("systemctl list-units | grep firewalld")[0] - if res.find('active running') != -1: return True + if res.find('active running') != -1: + return True return False + else: res = public.ExecShell("/etc/init.d/iptables status")[0] - if res.find('not running') != -1: return False + if res.find('not running') != -1: + return False + res = public.ExecShell("systemctl is-active iptables")[0] if res == "active": return True return True - def SetPing(self, get): + def SetPing(self, get): if get.status == '1': get.status = '0' else: @@ -189,6 +204,7 @@ class main(safeBase): '4. If you use other security software, please uninstall it first
                                    ' ) + # 服务状态控制 def firewall_admin(self, get): order = ['reload', 'restart', 'stop', 'start'] @@ -211,7 +227,7 @@ class main(safeBase): filename = '/etc/sysctl.conf' conf = public.readFile(filename) if conf.find('net.ipv4.icmp_echo') != -1: - public.ExecShell("sysctl -p") + public.ExecShell("sysctl -p") public.WriteLog("system firewall", "firewall {}".format(result[get.status])) return public.returnMsg(True, 'firewall has {}'.format(result[get.status])) if self.__isFirewalld: @@ -223,6 +239,7 @@ class main(safeBase): public.WriteLog("system firewall", "firewall {}".format(result[get.status])) return public.returnMsg(True, 'firewall has {}'.format(result[get.status])) + # 重载防火墙配置 def FirewallReload(self): if self.__isUfw: @@ -238,7 +255,8 @@ class main(safeBase): public.ExecShell('/etc/init.d/iptables save') public.ExecShell('/etc/init.d/iptables restart') - #端口扫描 + + # 端口扫描 def CheckPort(self, port, protocol): import socket localIP = '127.0.0.1' @@ -263,8 +281,10 @@ class main(safeBase): if temp['local']: result += 2 return result - # 查询入栈规则 + + # 查询入栈规则 def get_rules_list(self, args): + if self.__isFirewalld: self.__firewall_obj = firewalld() self.GetList() @@ -299,7 +319,7 @@ class main(safeBase): '-') != -1: d['status'] = -1 else: - d['status'] = self.CheckPort(int(_port), _protocol) + d['status'] = self.CheckPort(int(_port), _protocol) for i in res_data: if 'brief' in i: i['brief'] = public.xsssec(i['brief']) @@ -307,20 +327,22 @@ class main(safeBase): except: return [] + def check_firewall_rule(self, args): """ - @检测防火墙规则 - """ + @检测防火墙规则 + """ port = args['port'] - find = public.M('firewall_new').where('ports=?', (str(port), )).find() + find = public.M('firewall_new').where('ports=?', (str(port),)).find() if find: return True return False + # 端口检查 def check_port(self, port_list): - rep1 = "^\d{1,5}(:\d{1,5})?$" - # rep1 = '^[0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]$' + rep1 = r"^\d{1,5}(:\d{1,5})?$" + # rep1 = r'^[0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]$' for port in port_list: if port.find('-') != -1: ports = port.split('-') @@ -338,18 +360,19 @@ class main(safeBase): if not re.search(rep1, port): return public.returnMsg(False, 'PORT_CHECK_RANGE') + def parse_ip_interval(self, ip_str): """解析区间IP - author: lx - date: 2022/10/25 + author: lx + date: 2022/10/25 - Returns: - list : IP列表 - """ + Returns: + list : IP列表 + """ ips = [] try: - rep2 = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" searchor = re.compile(rep2) if ip_str.find("-") != -1: pre_ip, end_ip = ip_str.split("-") @@ -372,6 +395,7 @@ class main(safeBase): pass return ips + # 判断是否为ipv6网段 @staticmethod def is_ipv6_network_segment_or_ipv6_address(ip_datas: str) -> bool: @@ -385,17 +409,18 @@ class main(safeBase): return False return True + # 添加入栈规则 def create_rules2(self, get): ''' - get 里面 有 protocol port type address brief 五个参数 - protocol == ['tcp','udp'] - port = 端口 - types == [accept、drop] # 放行和禁止 - address 地址,允许放行的ip,如果全部就是:0.0.0.0/0;另外可以包含“,"或者"-" - 表示区间IP - brief 备注说明 - ''' + get 里面 有 protocol port type address brief 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types == [accept、drop] # 放行和禁止 + address 地址,允许放行的ip,如果全部就是:0.0.0.0/0;另外可以包含“,"或者"-" + 表示区间IP + brief 备注说明 + ''' protocol = get.protocol ports = get.ports.strip() types = get.types @@ -409,7 +434,7 @@ class main(safeBase): sources = [ sip.strip() for sip in address.split(",") if sip.strip() ] - rep2 = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" _ips = [] for source_ip in sources: if source_ip.find("-") != -1: @@ -440,17 +465,18 @@ class main(safeBase): for port in port_list: self.add_iptables_rule(source_ip, protocol, port, types) + # 添加入栈规则 def create_rules(self, get): ''' - get 里面 有 protocol port type address brief 五个参数 - protocol == ['tcp','udp'] - port = 端口 - types == [accept、drop] # 放行和禁止 - address 地址,允许放行的ip,如果全部就是:0.0.0.0/0;另外可以包含“,"或者"-" - 表示区间IP - brief 备注说明 - ''' + get 里面 有 protocol port type address brief 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types == [accept、drop] # 放行和禁止 + address 地址,允许放行的ip,如果全部就是:0.0.0.0/0;另外可以包含“,"或者"-" + 表示区间IP + brief 备注说明 + ''' protocol = get.protocol ports = get.ports.strip() types = get.types @@ -466,7 +492,7 @@ class main(safeBase): allow_ips = [] if address: sources = [sip.strip() for sip in address.split(",") if sip.strip()] - rep2 = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" _ips = [] for source_ip in sources: if source_ip.find("-") != -1: @@ -532,21 +558,25 @@ class main(safeBase): strategy = "accept" elif types == 'drop': strategy = "drop" - public.WriteLog("system firewall", "Add port rules: Protocol:{}, Port:{}, Policy:{}, IP:{}".format(protocol, ports, strategy, log_ip)) + public.WriteLog("system firewall", + "Add port rules: Protocol:{}, Port:{}, Policy:{}, IP:{}".format(protocol, ports, strategy, log_ip)) # 如果有忽略的端口,返回忽略的端口 if ignore_list: - return public.returnMsg(True, 'Added successfully, {} The same rule exists for the port and has been skipped'.format(', '.join(ignore_list))) + return public.returnMsg(True, + 'Added successfully, {} The same rule exists for the port and has been skipped'.format( + ', '.join(ignore_list))) return public.returnMsg(True, 'ADD_SUCCESS') + # 删除入栈规则 def remove_rules(self, get): ''' - get 里面有 id protocol port type address 五个参数 - protocol == ['tcp','udp'] - port = 端口 - types == [accept、drop] # 放行和禁止 - address 地址,允许放行的ip - ''' + get 里面有 id protocol port type address 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types == [accept、drop] # 放行和禁止 + address 地址,允许放行的ip + ''' # 检测是否开启防火墙 hezhihong if not self.get_firewall_status(): return public.returnMsg(False, 'Please enable the firewall before proceeding.') @@ -557,7 +587,7 @@ class main(safeBase): ports = get.ports types = get.types self._del_firewall_rules(address, protocol, ports, types) - public.M('firewall_new').where("id=?", (id, )).delete() + public.M('firewall_new').where("id=?", (id,)).delete() self.FirewallReload() if not get.address: log_ip = "All IPs" @@ -567,18 +597,21 @@ class main(safeBase): strategy = "accept" elif types == 'drop': strategy = "drop" - public.WriteLog("system firewall", "Delete port rules: Protocol:{}, Port:{}, Policy:{}, IP:{}".format(get.protocol, get.ports, types, log_ip)) + public.WriteLog("system firewall", + "Delete port rules: Protocol:{}, Port:{}, Policy:{}, IP:{}".format(get.protocol, get.ports, types, + log_ip)) return public.returnMsg(True, 'DEL_SUCCESS') + # 修改入栈规则 def modify_rules(self, get, addtime=None): ''' - get 里面有 id protocol port type address 五个参数 - protocol == ['tcp','udp'] - port = 端口 - types==['reject','accept'] # 放行和禁止 - address 地址,允许放行的ip,如果全部就是:0.0.0.0/0 - ''' + get 里面有 id protocol port type address 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types==['reject','accept'] # 放行和禁止 + address 地址,允许放行的ip,如果全部就是:0.0.0.0/0 + ''' # 检测是否开启防火墙 hezhihong if not self.get_firewall_status(): return public.returnMsg(False, 'Please enable the firewall before proceeding.') @@ -592,10 +625,10 @@ class main(safeBase): domain_total = domain.split('|')[0] sid = 0 if 'sid' not in get else get.sid if address: - rep = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" if not re.search(rep, get.source) and self.is_ipv6_network_segment_or_ipv6_address(get.source): return public.returnMsg(False, 'FIREWALL_IP_FORMAT') - data = public.M('firewall_new').where('id=?', (id, )).field( + data = public.M('firewall_new').where('id=?', (id,)).field( 'id,address,protocol,ports,types,brief,addtime,domain' ).find() if data: @@ -613,10 +646,10 @@ class main(safeBase): 'ports': ports, 'types': types, 'brief': brief, - 'addtime': addtime, - 'sid': sid, - 'domain': domain - } + 'addtime': addtime, + 'sid': sid, + 'domain': domain + } ) if domain: public.M('firewall_domain').where("id=?", (sid,)).save( @@ -635,8 +668,11 @@ class main(safeBase): strategy = "accept" elif get.types == 'drop': strategy = "drop" - public.WriteLog("system firewall", "修改端口规则: 协议:{}, 端口:{}, 策略:{}, IP:{}".format(get.protocol, get.ports.strip(), get.types, log_ip)) - return public.returnMsg(True, '操作成功') + public.WriteLog("system firewall", + "Modify port rules: Protocol:{}, Port:{}, Policy:{}, IP:{}".format(get.protocol, get.ports.strip(), get.types, + log_ip)) + return public.returnMsg(True, 'ADD_SUCCESS') + # firewall端口规则添加 def add_firewall_rule(self, address, protocol, ports, types): @@ -700,6 +736,7 @@ class main(safeBase): % (address, protocol, ports, types)) return True + # firewall端口规则删除 def del_firewall_rule(self, address, protocol, ports, types): if not address: @@ -766,6 +803,7 @@ class main(safeBase): % (address, protocol, ports, types)) return True + # firewall端口规则编辑 def edit_firewall_rule(self, _address, _protocol, _port, _type, address, protocol, ports, types): @@ -893,6 +931,7 @@ class main(safeBase): % (address, protocol, ports, types)) return True + # ufw 端口规则添加 def add_ufw_rule(self, address, protocol, ports, types): rule = "allow" if types == "accept" else "deny" @@ -912,6 +951,7 @@ class main(safeBase): public.ExecShell( 'ufw ' + rule + ' proto ' + protocol + ' from ' + address + ' to any port ' + ports + '') + # ufw 端口规则删除 def del_ufw_rule(self, address, protocol, ports, types): rule = "allow" if types == "accept" else "deny" @@ -933,6 +973,7 @@ class main(safeBase): ) self.update_panel_data(ports) + # ufw 端口规则修改 def edit_ufw_rule(self, _address, _protocol, _port, _type, address, protocol, ports, types): @@ -971,6 +1012,7 @@ class main(safeBase): 'ufw ' + rules + ' proto ' + protocol + ' from ' + address + ' to any port ' + ports + '' ) + # iptables端口规则添加 def add_iptables_rule(self, address, protocol, ports, types): rule = "ACCEPT" if types == "accept" else "DROP" @@ -1000,6 +1042,7 @@ class main(safeBase): rule + '') return True + # iptables端口规则删除 def del_iptables_rule(self, address, protocol, ports, types): rule = "ACCEPT" if types == "accept" else "DROP" @@ -1029,6 +1072,7 @@ class main(safeBase): rule + '') return True + # iptables端口规则编辑 def edit_iptables_rule(self, _address, _protocol, _port, _type, address, protocol, ports, types): @@ -1084,9 +1128,11 @@ class main(safeBase): rule2 + '') return True + # 修改面板数据 def update_panel_data(self, ports): - res = public.M('firewall').where("port=?", (ports, )).delete() + res = public.M('firewall').where("port=?", (ports,)).delete() + # 查询IP规则 def get_ip_rules_list(self, args): @@ -1110,11 +1156,12 @@ class main(safeBase): data['data'] = public.return_area(data['data'], 'address') return data + def check_a_ip(self, address): """ - @name 检测A记录是否为域名 - @author hezhihong - """ + @name 检测A记录是否为域名 + @author hezhihong + """ if address: if public.is_ipv4(address) or public.is_ipv6(address): return address @@ -1123,15 +1170,16 @@ class main(safeBase): if public.is_domain(address): return self.get_a_ip(address) return address + def get_a_ip(self, hostname): ''' - @name 检测主机名是否有A记录 - @author hezhihong - :param hostname: - :return: - ''' + @name 检测主机名是否有A记录 + @author hezhihong + :param hostname: + :return: + ''' if not self.install_dnspython(): - return public.returnMsg(False, '请先安装dnspython模块') + return public.returnMsg(False, 'Please install dnspython module first: btpip install dnspython') import dns.resolver # 尝试3次 a_ip = [] @@ -1167,11 +1215,12 @@ class main(safeBase): a_ip.remove(i2) return a_ip + def install_dnspython(self): """ - @name 安装dnspython模块 - @author hezhihong - """ + @name 安装dnspython模块 + @author hezhihong + """ # 检测dns解析 try: import dns.resolver @@ -1187,11 +1236,12 @@ class main(safeBase): except: return False + def del_domain_ip(self, args): """ - @name 删除域名设置 - @author hezhihong - """ + @name 删除域名设置 + @author hezhihong + """ if 'id' not in args or not args.id or 'sid' not in args: return public.returnMsg(False, 'Parameter error') @@ -1210,7 +1260,8 @@ class main(safeBase): # 当没有域名解析时,删除计划任务 if not public.M('firewall_domain').count(): - pdata = public.M('crontab').where('name=?', '[Do not delete] System firewall domain name resolution detection task').select() + pdata = public.M('crontab').where('name=?', + '[Do not delete] System firewall domain name resolution detection task').select() if pdata: for i in pdata: args = {"id": i['id']} @@ -1219,11 +1270,12 @@ class main(safeBase): return public.returnMsg(True, 'successfully deleted') + def add_crontab(self): """ - @name 构造日志切割任务 - @author hezhihong - """ + @name 构造日志切割任务 + @author hezhihong + """ python_path = '' try: python_path = public.ExecShell('which btpython')[0].strip("\n") @@ -1233,9 +1285,11 @@ class main(safeBase): except: pass if not python_path: return False - if not public.M('crontab').where('name=?', ('[Do not delete] System firewall domain name resolution detection task',)).count(): + if not public.M('crontab').where('name=?', ( + '[Do not delete] System firewall domain name resolution detection task',)).count(): cmd = '{} {}'.format(python_path, '/www/server/panel/script/firewall_domain.py') - args = {"name": "[Do not delete] System firewall domain name resolution detection task", "type": 'minute-n', "where1": '5', "hour": '', + args = {"name": "[Do not delete] System firewall domain name resolution detection task", "type": 'minute-n', + "where1": '5', "hour": '', "minute": '', "sName": "", "sType": 'toShell', "notice": '', "notice_channel": '', "save": '', "save_local": '1', "backupTo": '', "sBody": cmd, @@ -1247,6 +1301,7 @@ class main(safeBase): return False return True + def __check_auth(self): try: from pluginAuth import Plugin @@ -1258,11 +1313,12 @@ class main(safeBase): except: return False + def set_domain_ip2(self, args): """ - @name 设置域名规则 - @author hezhihong - """ + @name 设置域名规则 + @author hezhihong + """ pay = self.__check_auth() if not pay: return public.returnMsg(False, 'Current features are exclusive to the professional version') if not args.domain: return public.returnMsg(False, 'Please enter domain name') @@ -1278,7 +1334,8 @@ class main(safeBase): args.source = ip if ports: if public.is_ipv6(ip): - return public.returnMsg(False, 'The domain name is resolved to an IPv6 address and port rules are not supported.') + return public.returnMsg(False, + 'The domain name is resolved to an IPv6 address and port rules are not supported.') self.create_rules2(args) # 添加IP规则 else: @@ -1287,11 +1344,12 @@ class main(safeBase): return public.returnMsg(True, 'Domain name {} resolution added successfully'.format(args.domain)) + def set_domain_ip(self, args): """ - @name 设置域名规则 - @author hezhihong - """ + @name 设置域名规则 + @author hezhihong + """ pay = self.__check_auth() if not pay: return public.returnMsg(False, 'Current features are exclusive to the professional version') if not args.domain: return public.returnMsg(False, 'Please enter domain name') @@ -1305,7 +1363,8 @@ class main(safeBase): a_ip = [self.check_a_ip(a_ip[0])] # return a_ip if not a_ip: - return public.returnMsg(False, 'The domain name resolution has not been resolved or the resolution has not taken effect. If it has been resolved, please try again after 10 minutes.') + return public.returnMsg(False, + 'The domain name resolution has not been resolved or the resolution has not taken effect. If it has been resolved, please try again after 10 minutes.') if public.M('firewall_domain').where("domain=? and types=? and port=? and protocol=?", (args.domain, args.types, ports, protocol,)).count(): return public.returnMsg(False, 'Domain name {} already exists'.format(args.domain)) @@ -1319,7 +1378,8 @@ class main(safeBase): args.source = ip if ports: if public.is_ipv6(ip): - return public.returnMsg(False, 'The domain name is resolved to an IPv6 address and port rules are not supported.') + return public.returnMsg(False, + 'The domain name is resolved to an IPv6 address and port rules are not supported.') self.create_rules(args) # 添加IP规则 else: @@ -1328,11 +1388,12 @@ class main(safeBase): return public.returnMsg(True, 'Domain name {} resolution added successfully'.format(args.domain)) + def modify_domain_ip(self, args): """ - @name 修改域名规则(当修改为指定域名或从指定域名修改为其他时,需要调用此方法) - @name hezhihong - """ + @name 修改域名规则(当修改为指定域名或从指定域名修改为其他时,需要调用此方法) + @name hezhihong + """ pay = self.__check_auth() if not pay: return public.returnMsg(False, 'Current features are exclusive to the professional version') @@ -1393,9 +1454,10 @@ class main(safeBase): modify_args.domain = pdata['domain'] return self.modify_ip_rules(modify_args) + # IP地址检测 def check_ip(self, address_list): - rep = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" for address in address_list: address = address.split('/')[0] if address.find('-') != -1: @@ -1416,6 +1478,7 @@ class main(safeBase): if not re.search(rep, address) and not public.is_ipv6(address): return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + # 获取IP范围 def get_ip(self, address): result = [] @@ -1428,6 +1491,7 @@ class main(safeBase): result.append(head_s_ip + str(num + int(s_ips[-1]))) return result + def handle_firewall_ip(self, address, types): ip_list = self.get_ip(address) if isinstance(ip_list, dict): @@ -1450,6 +1514,7 @@ class main(safeBase): 'firewall-cmd --permanent --zone=public --add-rich-rule=\'rule source ipset="' + address + '" ' + types + '\'') + def handle_ufw_ip(self, address, types): ip_list = self.get_ip(address) if isinstance(ip_list, dict): @@ -1460,6 +1525,7 @@ class main(safeBase): public.ExecShell('iptables -I INPUT -m set --match-set ' + address + ' src -j ' + types.upper()) + # 检查IP地址是否在范围内 def ip_in_range(self, ip, ip_range): import ipaddress @@ -1493,7 +1559,8 @@ class main(safeBase): return result # 先处理用户的IP地址 - old_login_ip = public.M('firewall_ip').where("brief=?", ("IP that allows users to log in",)).field('id, address').select() + old_login_ip = public.M('firewall_ip').where("brief=?", ("IP that allows users to log in",)).field( + 'id, address').select() # public.print_log('############ ip接口user_ip {}'.format(user_ip)) for ip_range in address_list: @@ -1511,20 +1578,18 @@ class main(safeBase): # 然后处理其他的IP地址 for address in address_list: - self.add_rule(address, original_types, brief, domain, domain_total) self.FirewallReload() - public.WriteLog("system firewall", "Add IP rules: IP: {}, policy: {}".format(_address, original_types)) return public.returnMsg(True, 'ADD_SUCCESS') + # 添加单个IP规则 def add_rule(self, address, types, brief, domain, domain_total): if public.M('firewall_ip').where("address=? and types=? and domain=?", (address, types, domain)).count() > 0: - return if self.__isUfw: # public.print_log('############ ip接口 1') @@ -1631,11 +1696,12 @@ class main(safeBase): else: public.ExecShell('iptables -D INPUT -s ' + address + ' -j ' + types.upper()) - public.M('firewall_ip').where("id=?", (id, )).delete() + public.M('firewall_ip').where("id=?", (id,)).delete() self.update_panel_data(address) # 删除面板自带防火墙的表数据 self.FirewallReload() return public.returnMsg(True, 'All IP rules have been removed.') + # 删除IP规则 def remove_ip_rules(self, get): id = get.id @@ -1685,10 +1751,10 @@ class main(safeBase): else: public.ExecShell('iptables -D INPUT -s ' + address + ' -j ' + types.upper()) - public.M('firewall_ip').where("id=?", (id, )).delete() + public.M('firewall_ip').where("id=?", (id,)).delete() self.update_panel_data(address) # 删除面板自带防火墙的表数据 self.FirewallReload() - strategy= '' + strategy = '' if get.types == 'accept': strategy = "accept" elif get.types == 'drop': @@ -1696,6 +1762,7 @@ class main(safeBase): public.WriteLog("system firewall", "Delete IP rules: IP:{}, policy:{}".format(get.address, strategy)) return public.returnMsg(True, 'DEL_SUCCESS') + # 修改IP规则 def modify_ip_rules(self, get): id = get.id @@ -1710,7 +1777,7 @@ class main(safeBase): if result: return result data = public.M('firewall_ip').where( - 'id=?', (id, )).field('id,address,types,brief,addtime').find() + 'id=?', (id,)).field('id,address,types,brief,addtime').find() _address = data.get("address", "") _type = data.get("types", "") if self.__isUfw: @@ -1813,9 +1880,12 @@ class main(safeBase): strategy = "accept" elif get.types == 'drop': strategy = "drop" - public.WriteLog("system firewall", "修改规则, IP:{}, 策略:{} -> IP:{}, 策略:{}".format(_address, old_strategy, get.address.strip(), get.types)) + public.WriteLog("system firewall", + "修改规则, IP:{}, 策略:{} -> IP:{}, 策略:{}".format(_address, old_strategy, get.address.strip(), + get.types)) return public.returnMsg(True, 'Successful operation') + # 查看端口转发状态 def trans_status(self): content = dict() @@ -1829,6 +1899,7 @@ class main(safeBase): fw.write(json.dumps(content)) return True + # 查询端口转发 def get_forward_list(self, args): result = self.trans_status() @@ -1850,18 +1921,19 @@ class main(safeBase): data['shift'], data['row'])).order('addtime desc').select() return data + # 添加端口转发 def create_forward(self, get): s_port = get.s_ports.strip() # 起始端口 d_port = get.d_ports.strip() # 目的端口 d_ip = get.d_address.strip() # 目的ip protocol = get.protocol - rep1 = "^\d{1,5}(:\d{1,5})?$" + rep1 = r"^\d{1,5}(:\d{1,5})?$" if not re.search(rep1, s_port): return public.returnMsg(False, 'PORT_CHECK_RANGE') if not re.search(rep1, d_port): return public.returnMsg(False, 'PORT_CHECK_RANGE') - rep = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" if d_ip: if not re.search(rep, get.d_address) and not public.is_ipv6( get.d_address): @@ -1869,7 +1941,7 @@ class main(safeBase): if d_ip in ["127.0.0.1", "localhost"]: d_ip = "" if public.M('firewall_trans').where("start_port=?", - (s_port, )).count() > 0: + (s_port,)).count() > 0: return public.returnMsg(False, 'This port already exists, please do not add it again!') if self.__isUfw: content = self.ufw_handle_add(s_port, d_port, d_ip, protocol) @@ -1884,9 +1956,13 @@ class main(safeBase): 'start_port, ended_ip, ended_port, protocol, addtime', (s_port, d_ip, d_port, protocol, addtime)) self.FirewallReload() - public.WriteLog("system firewall", "Add port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {}".format(s_port, d_port, d_ip)) + public.WriteLog("system firewall", + "Add port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {}".format(s_port, + d_port, + d_ip)) return public.returnMsg(True, 'ADD_SUCCESS') + # 删除端口转发 def remove_forward(self, get): id = get.id @@ -1902,11 +1978,14 @@ class main(safeBase): self.firewall_handle_del(s_port, d_port, d_ip, protocol) else: self.iptables_handle_del(s_port, d_port, d_ip, protocol) - public.M('firewall_trans').where("id=?", (id, )).delete() + public.M('firewall_trans').where("id=?", (id,)).delete() self.FirewallReload() - public.WriteLog("system firewall", "Delete port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {}".format(s_port, d_port, d_ip)) + public.WriteLog("system firewall", + "Delete port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {}".format( + s_port, d_port, d_ip)) return public.returnMsg(True, 'DEL_SUCCESS') + # 修改端口转发 def modify_forward(self, get): id = get.id @@ -1914,19 +1993,19 @@ class main(safeBase): d_port = get.d_ports.strip() d_ip = get.d_address.strip() pool = get.protocol - rep1 = "^\d{1,5}(:\d{1,5})?$" + rep1 = r"^\d{1,5}(:\d{1,5})?$" if not re.search(rep1, s_port): return public.returnMsg(False, 'PORT_CHECK_RANGE') if not re.search(rep1, d_port): return public.returnMsg(False, 'PORT_CHECK_RANGE') - data = public.M('firewall_trans').where('id=?', (id, )).field( + data = public.M('firewall_trans').where('id=?', (id,)).field( 'id,start_port,ended_ip,ended_port,protocol,addtime').find() start_port = data.get("start_port", "") ended_ip = data.get("ended_ip", "") ended_port = data.get("ended_port", "") protocol = data.get("protocol", "") if d_ip: - rep = "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" if not re.search(rep, get.d_address) and not public.is_ipv6( get.d_address): return public.returnMsg(False, 'FIREWALL_IP_FORMAT') @@ -1950,9 +2029,12 @@ class main(safeBase): public.M('firewall_trans').where('id=?', id).update( {'start_port': s_port, "ended_ip": d_ip, "ended_port": d_port, "protocol": pool}) self.FirewallReload() - public.WriteLog("system firewall", "Modify port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {} -> Start port: {}, Destination port: {}, Destination IP: {}".format(start_port, ended_port, ended_ip, s_port, d_port, d_ip)) + public.WriteLog("system firewall", + "Modify port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {} -> Start port: {}, Destination port: {}, Destination IP: {}".format( + start_port, ended_port, ended_ip, s_port, d_port, d_ip)) return public.returnMsg(True, 'Successful operation.') + # 处理ufw的端口转发添加 def ufw_handle_add(self, s_port, d_port, d_ip, protocol): content = self.get_profile(self._ufw_before) @@ -1976,6 +2058,7 @@ class main(safeBase): array.insert(result + 1, _string) return '\n'.join(array) + # 处理ufw的端口转发删除 def ufw_handle_del(self, s_port, d_port, d_ip, protocol): content = self.get_profile(self._ufw_before) @@ -1989,6 +2072,7 @@ class main(safeBase): content = content.replace(_string, "") return content + # 处理ufw的端口转发修改 def ufw_handle_update(self, start_port, ended_ip, ended_port, protocol, s_port, d_ip, d_port, pool): @@ -2010,6 +2094,7 @@ class main(safeBase): content = content.replace(s_string, d_string) return content + # 处理firewall的端口转发添加 def firewall_handle_add(self, s_port, d_port, d_ip, protocol): if protocol.find('/') != -1: @@ -2025,6 +2110,7 @@ class main(safeBase): cmd = "firewall-cmd --permanent --zone=public --add-forward-port=port=" + s_port + ":proto=" + protocol + ":toaddr=" + d_ip + ":toport=" + d_port + "" public.ExecShell(cmd) + # 处理firewall的端口转发删除 def firewall_handle_del(self, s_port, d_port, d_ip, protocol): if protocol.find('/') != -1: @@ -2042,6 +2128,7 @@ class main(safeBase): + s_port + ":proto=" + protocol + ":toaddr=" + d_ip + ":toport=" + d_port + "") + # 处理firewall的端口转发修改 def firewall_handle_update(self, start_port, ended_ip, ended_port, protocol, s_port, d_ip, d_port, pool): @@ -2074,6 +2161,7 @@ class main(safeBase): + s_port + ":proto=" + pool + ":toaddr=" + d_ip + ":toport=" + d_port + "") + # 处理iptables的端口转发添加 def iptables_handle_add(self, s_port, d_port, d_ip, protocol): if d_ip == "": @@ -2104,6 +2192,7 @@ class main(safeBase): public.ExecShell("iptables -t nat -A POSTROUTING -j MASQUERADE") return True + # 处理iptables的端口转发删除 def iptables_handle_del(self, s_port, d_port, d_ip, protocol): if d_ip == "": @@ -2131,6 +2220,7 @@ class main(safeBase): public.ExecShell("iptables -t nat -D POSTROUTING -j MASQUERADE") return True + # 处理iptables的端口转发删除 def iptables_handle_update(self, start_port, ended_ip, ended_port, protocol, s_port, d_ip, d_port, pool): @@ -2182,6 +2272,7 @@ class main(safeBase): public.ExecShell("iptables -t nat -A POSTROUTING -j MASQUERADE") return True + # 开启端口转发 def open_forward(self): if self.__isUfw: @@ -2207,6 +2298,7 @@ class main(safeBase): self.FirewallReload() return True + # 开启或关闭端口转发 def open_close_forward(self, get): if not get.status in ["open", "close"]: @@ -2242,11 +2334,12 @@ class main(safeBase): public.ExecShell('sysctl -p /etc/sysctl.conf') return public.returnMsg(True, "Turn off port forwarding") + def get_host_ip(self): """ - 查询本机ip地址 - :return: - """ + 查询本机ip地址 + :return: + """ try: import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -2257,6 +2350,7 @@ class main(safeBase): return ip + def load_white_list(self): try: if not self._white_list: @@ -2272,6 +2366,7 @@ class main(safeBase): public.WriteLog("firewall", "Failed to load whitelist!") return [] + def verify_ip(self, ip_entry): """检查规则IP是否和内网IP重叠""" try: @@ -2304,6 +2399,7 @@ class main(safeBase): except: return False + def handle_firewall_country(self, brief, ip_list, types, port_list): try: public.ExecShell( @@ -2333,6 +2429,7 @@ class main(safeBase): except Exception as e: return {"status": "error", "msg": e} + def handle_ufw_country(self, brief, ip_list, types, port_list): tmp_path = '/tmp/firewall_tmp.sh' tmp_file = open(tmp_path, 'w') @@ -2353,9 +2450,9 @@ class main(safeBase): public.ExecShell('iptables -I INPUT -m set --match-set ' + brief + ' src -j ' + types.upper()) + # 查询区域规则 def get_country_list(self, args): - p = 1 limit = 15 if 'p' in args: p = args.p @@ -2374,6 +2471,7 @@ class main(safeBase): data['shift'], data['row'])).order('addtime desc').select() return data + def create_countrys(self, get): try: if not hasattr(get, 'country'): @@ -2411,13 +2509,14 @@ class main(safeBase): print(traceback.format_exc()) return public.returnMsg(False, 'Add failed') + # 添加区域规则 def create_country(self, get, is_mutil=False, _ips_paths=None): brief = get.brief types = get.types # types in [accept, drop] ports = get.ports country = get.country - rep = "^\d{1,5}(:\d{1,5})?$" + rep = r"^\d{1,5}(:\d{1,5})?$" port_list = [] # 检测该区域是否已添加过全部端口规则 hezhihong @@ -2485,9 +2584,11 @@ class main(safeBase): # elif get.types == 'drop': # strategy = "drop" public.print_log('############ 地区接口5'.format()) - public.WriteLog("system firewall", "Add regional rules: Region:{}, Policy:{}, Port:{}".format(get.country, get.types, log_port)) + public.WriteLog("system firewall", + "Add regional rules: Region:{}, Policy:{}, Port:{}".format(get.country, get.types, log_port)) return public.returnMsg(True, 'ADD_SUCCESS') + # 删除区域规则 def remove_country(self, get): id = get.id @@ -2498,7 +2599,7 @@ class main(safeBase): reload = True if "not_reload" in get: reload = get.not_reload.lower() == "true" - public.M('firewall_country').where("id=?", (id, )).delete() + public.M('firewall_country').where("id=?", (id,)).delete() if self.__isUfw: if not ports: public.ExecShell('iptables -D INPUT -m set --match-set ' + @@ -2508,7 +2609,7 @@ class main(safeBase): brief + ' src -p tcp --destination-port ' + ports + ' -j ' + types.upper()) if not public.M('firewall_country').where("country=?", - (country, )).count() > 0: + (country,)).count() > 0: public.ExecShell('ipset destroy ' + brief) else: if self.__isFirewalld: @@ -2522,7 +2623,7 @@ class main(safeBase): + brief + '" port port="' + ports + '" protocol=tcp ' + types + '\'') if not public.M('firewall_country').where( - "country=?", (country, )).count() > 0: + "country=?", (country,)).count() > 0: public.ExecShell( 'firewall-cmd --permanent --zone=public --delete-ipset=' + brief) @@ -2536,7 +2637,7 @@ class main(safeBase): ' src -p tcp --destination-port ' + ports + ' -j ' + types.upper()) if not public.M('firewall_country').where( - "country=?", (country, )).count() > 0: + "country=?", (country,)).count() > 0: public.ExecShell('ipset destroy ' + brief) if reload: get.status = "restart" @@ -2550,9 +2651,11 @@ class main(safeBase): strategy = "accept" elif get.types == 'drop': strategy = 'drop' - public.WriteLog("system firewall", "Delete zone rules: Region:{}, Policy:{}, Port:{}".format(get.country, strategy, log_port)) + public.WriteLog("system firewall", + "Delete zone rules: Region:{}, Policy:{}, Port:{}".format(get.country, strategy, log_port)) return public.returnMsg(True, 'DEL_SUCCESS') + # 编辑区域规则 def modify_country(self, get): # 2022/11/24 修复编辑地区端口规则问题 lx @@ -2562,7 +2665,7 @@ class main(safeBase): # country = get.country data = public.M('firewall_country').where( 'id=?', - (id, )).field('id,country,types,brief,ports,addtime').find() + (id,)).field('id,country,types,brief,ports,addtime').find() ori_get = public.dict_obj() ori_get.id = id ori_get.types = data.get("types", "") @@ -2577,6 +2680,7 @@ class main(safeBase): return public.returnMsg(True, "Successful operation") return public.returnMsg(False, "operation failed") + # 获取服务端列表:centos def GetList(self): try: @@ -2615,6 +2719,7 @@ class main(safeBase): file = open('error.txt', 'w') return public.returnMsg(False, e) + # 获取服务端列表:ufw def get_ufw_list(self): data = public.M('firewall').field('id,port,ps,addtime').select() @@ -2638,6 +2743,7 @@ class main(safeBase): except: pass + # 检查数据库是否存在 def check_db_exists(self, ports, address, types): if ports: @@ -2653,6 +2759,7 @@ class main(safeBase): if dt["address"] == address and dt["types"] == types: return dt return False + def check_trans_data(self, ports): data = public.M('firewall_trans').field( 'id,start_port,ended_ip,ended_port,protocol,addtime').select() @@ -2660,6 +2767,7 @@ class main(safeBase): if dt['start_port'] == ports: return dt return False + # 规则导出:服务器 def export_rules(self, get): rule_name = get.rule_name @@ -2689,9 +2797,10 @@ class main(safeBase): write_string += str(i[v]) + "|" write_string += '\n' public.writeFile(filename, write_string) - public.WriteLog("system firewall", "导出端口规则") + public.WriteLog("system firewall", "Export port rules") return public.returnMsg(True, filename) + # 规则导出:本地 def get_file(self, args): filename = args.filename @@ -2703,6 +2812,7 @@ class main(safeBase): attachment_filename=os.path.basename(filename), cache_timeout=0) + # 规则导入:json def import_rules(self, get): try: @@ -2788,12 +2898,15 @@ class main(safeBase): not_pay_list = ("
                                    " + "-" * 20 + "
                                    ").join(not_pay_list) return public.ReturnMsg( result["status"], - "{}
                                    The designated domain name function is exclusive to the Enterprise Edition, and the following rules are not imported:
                                    {}".format(result["msg"], not_pay_list) + "{}
                                    The designated domain name function is exclusive to the Enterprise Edition, and the following rules are not imported:
                                    {}".format( + result["msg"], not_pay_list) ) public.WriteLog("system firewall", "Import port rules") return public.ReturnMsg(result["status"], result["msg"]) except Exception: - return public.ReturnMsg(False, "The import failed. The format of the rules is wrong. Please try again according to the format of the export rules!") + return public.ReturnMsg(False, + "The import failed. The format of the rules is wrong. Please try again according to the format of the export rules!") + # 处理规则导入,读取json文件内容 def hand_import_rules(self, rule_name, data_list): @@ -2802,7 +2915,7 @@ class main(safeBase): if rule_name == "port_rule": table_head = ["id", "protocol", "ports", "types", "address", "brief", "addtime", "domain", ] for data in data_list: - #兼容一行一条规则格式文件导入 hezhihong + # 兼容一行一条规则格式文件导入 hezhihong try: data = json.loads(data) except: @@ -2891,25 +3004,26 @@ class main(safeBase): return {"status": False, "msg": "Import failed!"} return {"status": True, "msg": "Imported successfully!"} + def get_countrys(self, get): result = [] content = self.get_profile(self._country_path) result = json.loads(content) - result = sorted(result, key=lambda x : x['CH'], reverse=True); + result = sorted(result, key=lambda x: x['CH'], reverse=True); if isinstance(result, list): result.insert(0, {"CH": "Except China", "brief": "OTHER"}) return result + # 读取配置文件 def get_profile(self, path): - if not os.path.exists(path): b_path = os.path.dirname(path) if not os.path.exists(b_path): os.makedirs(b_path) if path in [ - self._ips_path, self._country_path, self._white_list_file + self._ips_path, self._country_path, self._white_list_file ]: public.downloadFile( 'https://download.bt.cn/install/lib/{}'.format( @@ -2920,22 +3034,26 @@ class main(safeBase): content = fr.read() return content + # 保存配置文件 def save_profile(self, path, data): with open(path, "w") as fw: fw.write(data) + # 读取配置文件 def update_profile(self, path): import files f = files.files() return f.GetFileBody(path) + # 获取端口规则列表 def get_port_rules(self, get): rule_list = public.M('firewall_new').order("id desc").select() return public.returnMsg(True, rule_list) + # 整理配置文件格式 def format(self, em, level=0): i = "\n" + level * " " @@ -2949,6 +3067,7 @@ class main(safeBase): if level and (not em.tail or not em.tail.strip()): em.tail = i + def check_table(self): if public.M('sqlite_master').where('type=? AND name=?', ('table', 'firewall_new')).count(): @@ -2956,13 +3075,14 @@ class main(safeBase): 'type=? AND name=?', ('table', 'firewall_ip')).count(): if public.M('sqlite_master').where( 'type=? AND name=?', - ('table', 'firewall_trans')).count(): + ('table', 'firewall_trans')).count(): if public.M('sqlite_master').where( 'type=? AND name=?', - ('table', 'firewall_country')).count(): + ('table', 'firewall_country')).count(): return True return Sqlite() + def delete_service(self): if self.__isUfw: public.ExecShell('ufw delete allow ssh') @@ -2975,6 +3095,7 @@ class main(safeBase): pass return True + # 获取系统类型(具体到哪个版本) def get_os_info(self): tmp = {"osname": "", "version": ""} @@ -3000,14 +3121,15 @@ class main(safeBase): public.ExecShell("systemctl restart firewalld") return True + # 新加代码----- start def sync_must_ports(self, get): ''' - 同步必须放行的端口 - @param get: - @return: - ''' + 同步必须放行的端口 + @param get: + @return: + ''' protocol = "tcp" ports = get.ports.strip() print(ports) @@ -3055,11 +3177,12 @@ class main(safeBase): print(traceback.format_exc()) return public.returnMsg(False, 'ADD_ERROR') + def _get_webserver(self): ''' - 获取web服务器类型 - @return: - ''' + 获取web服务器类型 + @return: + ''' webserver = '' if os.path.exists('/www/server/nginx/sbin/nginx'): webserver = 'nginx' @@ -3069,13 +3192,14 @@ class main(safeBase): webserver = 'lswsctrl' return webserver + def get_port_info(self, get): ''' - 获取面板防火墙关键服务端口放行状态信息 - 判断服务是否存在,能读取文件就读取文件,配置文件不大不会影响性能,这种方式能最大缩短接口响应时间,公网测试80ms - @param get: - @return: - ''' + 获取面板防火墙关键服务端口放行状态信息 + 判断服务是否存在,能读取文件就读取文件,配置文件不大不会影响性能,这种方式能最大缩短接口响应时间,公网测试80ms + @param get: + @return: + ''' ports_list = [] result_list = [{"name": "FTP passive port", "status": 0, "port": "39000-40000"}] @@ -3113,13 +3237,14 @@ class main(safeBase): return self._get_firewall_port_status(ports_list, result_list) return {} + def _get_firewall_port_status(self, ports_list, result_list): ''' - 获取firewalld防火墙端口状态 - @param ports_list: - @param result_list: - @return: - ''' + 获取firewalld防火墙端口状态 + @param ports_list: + @param result_list: + @return: + ''' with contextlib.suppress(Exception): _firewalld_ports, _ = self.__firewall_obj.GetAcceptPortList() # print("_firewalld_ports: ", _firewalld_ports) @@ -3131,13 +3256,14 @@ class main(safeBase): break return result_list + def _get_ufw_port_status(self, ports_list, result_list): ''' - 获取ufw防火墙端口状态 - @param ports_list: - @param result_list: - @return: - ''' + 获取ufw防火墙端口状态 + @param ports_list: + @param result_list: + @return: + ''' with contextlib.suppress(Exception): rules_result = self._get_ufw_port_info() # print("rules_result: ", rules_result) @@ -3154,11 +3280,12 @@ class main(safeBase): if not ports_set: break return result_list + def _get_ufw_port_info(self): ''' - 获取ufw防火墙端口信息 - @return: - ''' + 获取ufw防火墙端口信息 + @return: + ''' with open('/etc/ufw/user.rules', 'r') as f: content = f.read() start_index = content.find('### RULES ###') @@ -3182,15 +3309,16 @@ class main(safeBase): rules = [dict(item) for item in unique_set] return rules + @staticmethod - def get_listening_processes(get): + def get_listening_processes(get: public.dict_obj): ''' - 获取指定端口的进程信息 - @param get: - @return: - ''' - print("get.__dict__.keys(): ", get.__dict__.keys()) - if 'port' not in get.__dict__.keys(): return public.returnMsg(False, 'Parameter passing error, please pass the port field') + 获取指定端口的进程信息 + @param get: + @return: + ''' + if 'port' not in get.get_items().keys(): return public.returnMsg(False, + 'Parameter passing error, please pass the port field') if len(get.port) == 0: return public.returnMsg(False, 'Port cannot be empty') if get.port.find('-') != -1 or get.port.find(':') != -1: return public.returnMsg(False, 'Range ports not supported') if not get.port.isdigit(): return public.returnMsg(False, 'Port must be numeric') @@ -3215,11 +3343,12 @@ class main(safeBase): "process_cmd": process_cmd } + def get_diff_panel_firewall_rules(self, get): ''' - 对比面板防火墙规则数据库和防火墙配置文件,取出差异的规则 - @return: - ''' + 对比面板防火墙规则数据库和防火墙配置文件,取出差异的规则 + @return: + ''' # 获取面板防火墙规则数据库 panel_firewall_rules = self.get_panel_firewall_rules() # 获取防火墙配置文件 @@ -3228,32 +3357,35 @@ class main(safeBase): diff_rules = self._get_diff_rules(panel_firewall_rules, firewall_rules) return diff_rules + def get_panel_firewall_rules(self): ''' - 获取面板防火墙规则数据库 - @return: - ''' + 获取面板防火墙规则数据库 + @return: + ''' all_ports = public.M('firewall_new').field('protocol,ports,types,address').order('addtime desc').select() unique_set = set(tuple(sorted(item.items())) for item in all_ports) new_ports = [dict(item) for item in unique_set] return new_ports + def get_sys_firewall_rules(self): ''' - 获取防火墙配置文件 - @return: - ''' + 获取防火墙配置文件 + @return: + ''' if self.__isUfw: return self._get_ufw_port_info() if self.__isFirewalld: return self.__firewall_obj.recombine_rules() return [] + def _diff_dict_list(self, list1, list2): ''' - 比较两个dict类型的list,返回list1中有,而list2中没有的元素 - @param list1: - @param list2: - @return: - ''' + 比较两个dict类型的list,返回list1中有,而list2中没有的元素 + @param list1: + @param list2: + @return: + ''' list1_not_in_list2 = [] for item1 in list1: found = False @@ -3268,13 +3400,14 @@ class main(safeBase): if not found: list1_not_in_list2.append(item1) return list1_not_in_list2 + def _get_diff_rules(self, panel_firewall_rules, firewall_rules): ''' - 取出差异的规则 - @param panel_firewall_rules: - @param firewall_rules: - @return: - ''' + 取出差异的规则 + @param panel_firewall_rules: + @param firewall_rules: + @return: + ''' firewall_diff_rules_name = 'firewall_diff_rules' firewall_diff_rules = {} if os.path.isfile("config/{}.json".format(firewall_diff_rules_name)): @@ -3309,16 +3442,17 @@ class main(safeBase): public.save_config(firewall_diff_rules_name, firewall_diff_rules) return firewall_diff_rules + def exclude_diff_rules(self, get): ''' - 排除firewall_diff_rules的规则,并写入配置文件 - @param get: - @return: - ''' + 排除firewall_diff_rules的规则,并写入配置文件 + @param get: + @return: + ''' try: - panel_excludes = get.panel_exclude if "panel_exclude" in get.__dict__.keys() else {} - sys_excludes = get.sys_exclude if "sys_exclude" in get.__dict__.keys() else {} - status = get.status if "status" in get.__dict__.keys() else {} + panel_excludes = get.panel_exclude if "panel_exclude" in get.get_items().keys() else {} + sys_excludes = get.sys_exclude if "sys_exclude" in get.get_items().keys() else {} + status = get.status if "status" in get.get_items().keys() else {} # print("panel_excludes: ", panel_excludes) if status == 'add': @@ -3329,13 +3463,14 @@ class main(safeBase): # print(e) return public.returnMsg(False, 'Ignore rule failed,{}!'.format(e)) + def _add_exclude(self, panel_excludes, sys_excludes): ''' - 添加排除规则 - @param panel_exclude: - @param sys_exclude: - @return: - ''' + 添加排除规则 + @param panel_exclude: + @param sys_exclude: + @return: + ''' firewall_diff_rules_name = 'firewall_diff_rules' firewall_diff_rules = {} if os.path.isfile("config/{}.json".format(firewall_diff_rules_name)): @@ -3357,13 +3492,14 @@ class main(safeBase): public.save_config(firewall_diff_rules_name, firewall_diff_rules) return public.returnMsg(True, 'Ignore rules successfully!') + def _del_exclude(self, panel_excludes, sys_excludes): ''' - 删除排除规则 - @param panel_excludes: - @param sys_excludes: - @return: - ''' + 删除排除规则 + @param panel_excludes: + @param sys_excludes: + @return: + ''' firewall_diff_rules_name = 'firewall_diff_rules' firewall_diff_rules = {} if os.path.isfile("config/{}.json".format(firewall_diff_rules_name)): @@ -3384,15 +3520,16 @@ class main(safeBase): public.save_config(firewall_diff_rules_name, firewall_diff_rules) return public.returnMsg(True, 'Cancel ignore rule successfully!') + def _add_firewall_rules(self, source_ip, protocol, port, types): ''' - 添加防火墙规则 - @param source_ip: - @param protocol: - @param port: - @param types: - @return: - ''' + 添加防火墙规则 + @param source_ip: + @param protocol: + @param port: + @param types: + @return: + ''' if self.__isUfw: if port.find('-') != -1: port = port.replace('-', ':') @@ -3404,15 +3541,16 @@ class main(safeBase): else: self.add_iptables_rule(source_ip, protocol, port, types) + def _del_firewall_rules(self, source_ip, protocol, port, types): ''' - 删除防火墙规则 - @param source_ip: - @param protocol: - @param port: - @param types: - @return: - ''' + 删除防火墙规则 + @param source_ip: + @param protocol: + @param port: + @param types: + @return: + ''' if self.__isUfw: self.del_ufw_rule(source_ip, protocol, port, types) elif self.__isFirewalld: @@ -3420,19 +3558,20 @@ class main(safeBase): else: self.del_iptables_rule(source_ip, protocol, port, types) + def _modify_firewall_rules(self, address, protocol, port, type, source_ip, source_protocol, ports, types): ''' - 修改防火墙规则1 - @param address: - @param protocol: - @param port: - @param type: - @param source_ip: - @param source_protocol: - @param ports: - @param types: - @return: - ''' + 修改防火墙规则1 + @param address: + @param protocol: + @param port: + @param type: + @param source_ip: + @param source_protocol: + @param ports: + @param types: + @return: + ''' if self.__isUfw: self.edit_ufw_rule(address, protocol, port, type, source_ip, source_protocol, ports, types) elif self.__isFirewalld: @@ -3440,14 +3579,15 @@ class main(safeBase): else: self.edit_iptables_rule(address, protocol, port, type, source_ip, source_protocol, ports, types) + # 新加代码----- end # 端口防扫描 --- start def _get_server_lists_scan(self): """ - @name 获取服务器常用端口 - @return: - """ + @name 获取服务器常用端口 + @return: + """ return { "sshd": "{}".format(public.get_sshd_port()), "mysql": "{}".format(public.get_mysql_info()["port"]), @@ -3456,12 +3596,13 @@ class main(safeBase): "postfix": "25,465,587", } + def get_anti_scan_logs(self, get): """ - @name 获取防扫描日志 - @param get: - @return: - """ + @name 获取防扫描日志 + @param get: + @return: + """ get = public.dict_obj() server_lists = self._get_server_lists_scan() result_dict = { @@ -3486,11 +3627,12 @@ class main(safeBase): return result_dict + def get_anti_scan_status(self, get): """ - @name 获取端口防扫描 - @return: - """ + @name 获取端口防扫描 + @return: + """ plugin_path = "/www/server/panel/plugin/fail2ban" result_data = {"status": 0, "installed": 1} if not os.path.exists("{}".format(plugin_path)): @@ -3522,6 +3664,7 @@ class main(safeBase): return result_data + def set_anti_scan_status(self, get): """ @name 设置常用端口防扫描 @@ -3582,6 +3725,7 @@ class main(safeBase): public.WriteLog("Port Scanning Prevention", "[Security]-[System Firewall]-[Set Port Scanning Prevention]") return public.returnMsg(True, "Setup successful!") + def del_ban_ip(self, get): """ 删除封锁IP @@ -3598,7 +3742,7 @@ class main(safeBase): return public.returnMsg(True, "Unlocked successfully") -# 端口放扫描 --- end111 +# 端口防扫描 --- end111 class firewalld: @@ -3809,7 +3953,6 @@ class Sqlite(): "addtime" TEXT DEFAULT '');''') public.M('').execute('CREATE INDEX firewall_domain_addr ON firewall_domain (domain);') - # 修复之前已经创建的 firewall_domain 表无 domain_total 字段的问题 create_table_str = public.M('firewall_new').table('sqlite_master').where( 'type=? AND name=?', ('table', 'firewall_new')).getField('sql') @@ -3858,4 +4001,4 @@ sql = """ END; """ s = Sqlite() -s.create_trigger(sql) \ No newline at end of file +s.create_trigger(sql) diff --git a/class/safeModel/freeipModel.py b/class/safeModel/freeipModel.py index c00471e2..edc4545e 100644 --- a/class/safeModel/freeipModel.py +++ b/class/safeModel/freeipModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: cjxin +# Author: cjxin #------------------------------------------------------------------- # 免费IP库 diff --git a/class/safeModel/ipsModel.py b/class/safeModel/ipsModel.py index a5d28ee4..2d6963f3 100644 --- a/class/safeModel/ipsModel.py +++ b/class/safeModel/ipsModel.py @@ -1,118 +1,118 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: cjxin -#------------------------------------------------------------------- - -# 商用IP库 -#------------------------------ -import os,re,json,time -from safeModel.base import safeBase -import public - - -class main(safeBase): - - _sfile = '{}/data/ip_area.json'.format(public.get_panel_path()) - def __init__(self): - try: - self.user_info = public.get_user_info() - except: - self.user_info = None - - def get_ip_area(self,get): - """ - @获取IP地址所在地 - @param get: dict/array - """ - ips = get['ips'] - arrs,result = [],{} - for ip in ips: - info = {} - res = self.__check_ip_area(ip) - if res: - if type(res) == str: - info['info'] = res - else: - info = res - result[ip] = info - else: - arrs.append(ip) - - if len(arrs) > 0: - data = self.__get_cloud_ip_info(arrs) - for ip in data: - result[ip] = data[ip] - return result - - def __check_ip_area(self,ip): - """ - @检查IP地址所在地 - @param ip: - """ - - if not public.is_ipv4(ip): - return 'Unknown' - if public.is_local_ip(ip): - return 'Intranet' - - data = self.get_ip_area_cache() - if ip in data: - return data[ip] - return False - - - def __get_cloud_ip_info(self,ips): - """ - @获取IP地址所在地 - @param ips: - """ - result = {} - try: - - data = {} - data['ip'] = ','.join(ips) - data['uid'] = self.user_info['uid'] - data["serverid"]=self.user_info["serverid"] - res = public.httpPost('https://www.bt.cn/api/panel/get_ip_info',data) - res = json.loads(res) - - data = self.get_ip_area_cache() - for key in res: - if not public.is_ipv4(key): continue - - info = res[key] - if not res[key]['city'].strip() and not res[key]['continent'].strip(): - info = {'info':'Intranet'} - else: - info['info'] = '{} {} {} {}'.format(info['carrier'],info['country'],info['province'],info['city']).strip() - - data[key] = info - result[key] = info - self.set_ip_area_cache(data) - except: pass - return result - - - def get_ip_area_cache(self): - """ - @获取IP地址所在地 - @param get: - """ - data = {} - try: - data = json.loads(public.readFile(self._sfile)) - except: - public.writeFile(self._sfile,json.dumps({})) - return data - - def set_ip_area_cache(self,data): - """ - @设置IP地址所在地 - @param data: - """ - public.writeFile(self._sfile,json.dumps(data)) - return True \ No newline at end of file +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +rUv3hasppUA6pOugYcQZyA== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +iEprTHe70MI/8Rhzd8EK+QnVQn6aZyZ8dBODiF5pySg= +1w/Bux0eSLh4laruMV+kEu4fb+1yCwxQ0hOHwrLNXXRaWZyv1p2zipoavmFan6NZ +XIfdJ79nObMM+vyAmKbTmw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +hbDorme8BqzUEmeXCAWmYa52umm3PBCcfSvATzTG02w= +1u+XjG/2+GSQRv6EzCaWRQ== +giysigZ4bhExZB4emZkX3a9fe6PGGMNE9ZZnk7xPqW8QW2Ij1yNFf5XEj5pPPlMwdhA6dIcO2ZX23qJszvnq1fcl6NckihyZ+Uf83rbYjeQ= +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +b4OJVZe8QyIpjuTpKXDL9A== +32pdC9DD05OE2l0oXazDFI+hlwIzzkFTo7tZH3axSGoKNgh7EevAzaB+FO2ZLVS/maKOtFAjIeKErPBj7dsfmg== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +32pdC9DD05OE2l0oXazDFOQO8mg+tgs322u5NSxm1hnE+dYVd/hkfCsat3ixeRAk +1u+XjG/2+GSQRv6EzCaWRQ== +Fq/4dKD9ssEKrNEHrSuygxu04P7BPF8PboiRMtr4FMM= +Z8UsPk1Q7HtwjRd4g01ryw== +KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD +ho/Q+jrWDtBeg9J9ZTnKi7MMSdcDWkuDSRbkOa0slC0= +Z8UsPk1Q7HtwjRd4g01ryw== +1aJ5h9ef6qScYRSEXHxMz/JV+hrqnP7g6CgzmGbTA34= +CH5utP+NORdjI2nqATw4gJ30bQaw4oV4TkWtZlCiO9A= +lbIj6ug3LX3xS019kmbRSTcfm4XASPCYnVO8MD2z14s= +iZaIsa48RXfE+uf/yF/rQD9SK8CJ49+yPAMIKmMZPD4= +lOh2GtzHjjMM8E9J40AuOc+/vc1yhUL+xJx/Mivlb25pg5HBJ1HJ91Rfq30bJq9S +UbJuac0dxLiN+5ocS3w2vRp92UK1M7Voei4ApHZyTgY= +VmVrGQo2zRokW/ZuO9bN69BDpCzHoJtTjTgNlAOe5sUQUaSnZ1Z1hl5M9Ym+7tCG +VmVrGQo2zRokW/ZuO9bN61V3/TpT/zrd2QvdUtMgGKRq8f0CD0RNnZ4rTqSInsSj +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN657tYrblt+z3q06zgiQZcYA= +VmVrGQo2zRokW/ZuO9bN66aFKP1IdEjWKaMooH0pyYReSxgrF6gfc+R+CrZAggrY +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN69KBqHrgU1XKz6C1sxuKNo2/9c/EcmFt/gfo+iaOu2+K +1u+XjG/2+GSQRv6EzCaWRQ== +n+FHKmWLbioNpC38yMj4WmiXmXqyLuzKUmIAM1Ft5eM= +NhnIR3Ilo4H2su9/cTNo/MME7jTfPzQcknP67wyItNzcCacoNjdvCuiW0x8udsSO/edftRARPJ4xwm1wQrVT2A== +wlLHv6kT3Q/RmtMBN4nDATwL/9Oa0sOvSxwVwQfq99U= +VmVrGQo2zRokW/ZuO9bN68Jg91voF9Ce3nf2o3e+jkofqu9SSuRXcnpG7smrmLLo +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhMRKs67DB9ZOIiBDrB02YSQctSNS1aqQlPvqVprQ2WDG +Z8UsPk1Q7HtwjRd4g01ryw== +fXZfnC6cxxgiN+onu+xJ1wUhirDdf4kByrL5vhQHFnCYKlzy/eVgdjzy55WiEFTz +LikBEoRjt8wwWzUZNw+jJgsad0LKWFNZK+YO+SfR6UU= +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +W3A5Dt+8AxWSKsTYeXZoL+tHA8UsZgQuAUkDUOPJ9p9irMcLKjcsuwS2q6lHXAKR +L0eUthVnpkGsmKFAX6d+uOlJx8vHVMiotg8sk086vHQ= +TQRmC0vOvJ1P+lfyS3Os4X1xcTKiQ06lXIJEwRwWbq4ieECEmB4BmDhbitv1CsLg +L0eUthVnpkGsmKFAX6d+uLqKlmsh+FpVhvWy0B6mjhM= +1u+XjG/2+GSQRv6EzCaWRQ== +YabJCT93a3/IohXaIkr3oS6/BpCE446GJgDFFvo/yjjYAG9E6229ssUIWmf519v1 +hBWY30cr8RshUe3F5HK1cRZ9KmhsMz0lNR6pLxg9eoM= +L0eUthVnpkGsmKFAX6d+uFd45MXPp4WlUdlXMx7fhE8= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +avrdBvQ7h/91pEC4PcvqMSR2TJ0t+8EMPSsnARb+XVHHkBrQO1HsCg6QcbTZvZ2W +Z8UsPk1Q7HtwjRd4g01ryw== +KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD +LikBEoRjt8wwWzUZNw+jJpxJaQXGUN8g/Hr0khQ9A1Q= +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +b4OJVZe8QyIpjuTpKXDL9A== +1u+XjG/2+GSQRv6EzCaWRQ== +NhnIR3Ilo4H2su9/cTNo/F9IByyW2CHxX2+3PSbgZmE= +NhnIR3Ilo4H2su9/cTNo/JEWIdEQUX/MlMcOaFZ8tGgjCfBcJNHhmedart7PDRhF +NhnIR3Ilo4H2su9/cTNo/E4e/PRk3DEtgyjsUDeOpSOwGpZehbHYaLN6kP4SrhN+PFbXCI6E8Z+8865ULB7IOw== +NhnIR3Ilo4H2su9/cTNo/C/mkLay+Rx5WJjwGcYV7pSktyrOzy78W2NcadJ49lO1rNrUNnnzcDMri5NWEH6lGQ== +lOh2GtzHjjMM8E9J40AuOVVeyoh6rSy98QMdDBotkjU+CpzMJCQMfJrCkE4uEcQAgmW0jhOnZHI5mtglpnoAuAijuP+2lAOsmb6+b+801pIjLK0hXXo2u4rmG7jkFs3R +lOh2GtzHjjMM8E9J40AuOeMCiZGQsphriaBgvIdFiyOx9f7gw874ih7YoSVMDwrD +1u+XjG/2+GSQRv6EzCaWRQ== +NhnIR3Ilo4H2su9/cTNo/A5SzFv4xHkReni2nLCW9wf/+pP3k795kKoMZAQXwXD6 +wlLHv6kT3Q/RmtMBN4nDASCRBoPIX3WHiVt75UTjznI= +VmVrGQo2zRokW/ZuO9bN63WVsNPEwwprYYkAUqx5H2WQM74qCCEzrB3qJy/Mpn1CTpqeZGmRvqbYKmI/uSdTZw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61LJQniFugoi6x2ujjnJM7tldXKimSA8e3cVKdvY80W2 +VmVrGQo2zRokW/ZuO9bN64CPlXy2HmOeksJNoNOmCTRN2iWrExIMUdvTGpj/NNWq+ALT/bwKyDsjcfkGB9eQcrQ3H8UC+ywFIkXx32JcLCKuolLfVZKLqPTsQHTpYF9P +VmVrGQo2zRokW/ZuO9bN6yDw6frMtVHHQaPzsZSKK0aES6D4Kud9zFd68X/jO6wJ +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN61V3/TpT/zrd2QvdUtMgGKQFi9VDtwL4Vc83iegeMAft36zpJ+t/eeWtmA4Eamfb77jKlCL/2MWCO4n+tmFfKcs5bo8I5tFr5Qu31v4FEN0K0c0Au7xRuJmPMknxTzJ1AQ4L8Z8QbLuQ3EJTDAgG6zw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wKjnvaTwlMeHWSuJ/EAxZ8Z1KtkmFIaU/d9KNNMifcN +VmVrGQo2zRokW/ZuO9bN62ExR0OHxzNY3oC4Mfi694VYJyilbRPMd6JDlCBDdbCe +32pdC9DD05OE2l0oXazDFLdmyVEUr33LQ7qI5CZQN57igfVF4gan9s5C0Jnc3Ki1 +6ZPJI/HSoc4xA2zncU65FpfH+eGtPlOYNU1YCnnAis8= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Fq/4dKD9ssEKrNEHrSuyg9mm2ubmmEvtKER/ZGhfGKuM0YKPX8D/je6PexvVvgfs +Z8UsPk1Q7HtwjRd4g01ryw== +KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD +ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE= +Z8UsPk1Q7HtwjRd4g01ryw== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +b4OJVZe8QyIpjuTpKXDL9A== +NhnIR3Ilo4H2su9/cTNo/Ps3vYSOiOY6esn1xeoWmAlmLwmaOoQ9T7lnCb6TRVZ67prVtxc96eRcwG0EieQAHA== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+nhmNgO2Yqkfbla+bgPzSp/ulULnjCxA+skRNBtWTg0BA== ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +0jp9NuG0oWLkMfJ/MsYOmfu7WkSnZjHksXr6BBYLZSUOzHDXl9XjsxiUKNVbrHEG +Z8UsPk1Q7HtwjRd4g01ryw== ++kAMkUztiJJUlfW5H0qbp+x6TgEEQKPe4CNld5ATADH0jjvxLnFDBqMEQxxyT+xg +vPgTNKnHCM+ykPDJGfC5NIdfn4GuPGcMgxOHmcFE4sA= +Z8UsPk1Q7HtwjRd4g01ryw== +IhuisKim47k91RVt8z8qtK4Hi8QoBKGCIdLYuWbuvVULY/yxOc14YsJd+ymvu1M2ua6mSguBgb6gPVMjpVflaQ== +guSZID0bFQuDFoWO2uxAJhef0KHvUEISzfxiK9WGidA= diff --git a/class/safeModel/sshModel.py b/class/safeModel/sshModel.py index 1354f415..2676473e 100644 --- a/class/safeModel/sshModel.py +++ b/class/safeModel/sshModel.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- # ssh信息 @@ -43,6 +43,7 @@ class main(safeBase): result['error'] = int(public.ExecShell("journalctl -u ssh --no-pager |grep -a 'Failed password for' |grep -v 'invalid' |wc -l")[0]) + int(public.ExecShell("journalctl -u ssh --no-pager|grep -a 'Connection closed by authenticating user' |grep -a 'preauth' |wc -l")[0]) result['success'] = int(public.ExecShell("journalctl -u ssh --no-pager|grep -a 'Accepted' |wc -l")[0]) return result + # return public.return_message(0, 0, result) data = self.get_ssh_cache() for sfile in self.get_ssh_log_files(None): for stype in result.keys(): @@ -69,10 +70,11 @@ class main(safeBase): result[stype] += count self.set_ssh_cache(data) return result + # return public.return_message(0, 0, result) def get_ssh_cache(self): """ - @获取换成ssh记录 + @获取缓存ssh记录 """ file = '{}/data/ssh_cache.json'.format(public.get_panel_path()) if not os.path.exists(file): diff --git a/class/safeModel/syslogModel.py b/class/safeModel/syslogModel.py index 6dada622..8a1d876a 100644 --- a/class/safeModel/syslogModel.py +++ b/class/safeModel/syslogModel.py @@ -1,939 +1,993 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: hwliang -#------------------------------------------------------------------- - -# 系统日志 -#------------------------------ -import datetime -import json -import os -import re -import sys -import time - -import public -from safeModel.base import safeBase - - -class main(safeBase): - - def __init__(self): - ssh_cache_path = "{}/data/ssh".format(public.get_panel_path()) - if not os.path.exists(ssh_cache_path): os.makedirs(ssh_cache_path,384) - - - #*********************************************** start ssh收费模块 ****************************************************** - - def get_ssh_list(self,get): - """ - @获取SSH登录 - @param get: - count :数量 - """ - select_pl = ['Accepted', 'Failed password for'] - if hasattr(get, 'select'): - if get.select == "Accepted": - select_pl = ['Accepted'] - elif get.select == "Failed": - select_pl = ['Failed password for'] - - p = 1 - count = 20 - - if 'count' in get: count = int(get['count']) - if 'p' in get: p = int(get['p']) - - result = [] - min,max = (p -1) * count, p * count - - log_list = self.get_log_byfile(self.get_ssh_log_files(get)[0], min, max, - self.__get_search_list(get, select_pl)) - for log in log_list: - data = self.get_ssh_log_line(log['log'],log['time']) - if not data: continue - - result.append(data) - - if p < 1000000: public.set_module_logs('ssh_log','get_ssh_list') - return public.return_area(result,'address') - - - - def get_ssh_error(self,get): - """ - @获取SSH错误次数 - @param get: - count :数量 - """ - p = 1 - count = 20 - if 'count' in get: count = int(get['count']) - if 'p' in get: p = int(get['p']) - - result = [] - min,max = (p -1) * count, p * count - - log_list = self.get_log_byfile(self.get_ssh_log_files(get)[0],min,max,self.__get_search_list(get,['Failed password for'])) - - for log in log_list: - data = self.get_ssh_log_line(log['log'],log['time']) - if not data:continue - result.append(data) - - if p < 1000000: public.set_module_logs('ssh_log','get_ssh_list') - return public.return_area(result,'address') - - def get_ssh_success(self,get): - """ - @获取SSH登录成功次数 - @param get: - count :数量 - """ - p = 1 - count = 20 - if 'count' in get: count = int(get['count']) - if 'p' in get: p = int(get['p']) - - result = [] - min,max = (p -1) * count,p * count - - log_list = self.get_log_byfile(self.get_ssh_log_files(get)[0],min,max,self.__get_search_list(get,['Accepted'])) - - for log in log_list: - data = self.get_ssh_log_line(log['log'],log['time']) - if not data: continue - - result.append(data) - if p < 1000000: public.set_module_logs('ssh_log','get_ssh_list') - return public.return_area(result,'address') - - - def __get_search_list(self,get,slist): - """ - @组合搜索条件 - return list 查询组合 - status 1 增加查询条件 - """ - res = [] - if 'search' in get and get['search'].strip(): - search = get['search'].strip() - for info in slist: - res.append(info +'&' + search) - if len(res) == 0: - return search - return res - return slist - - - def get_ssh_log_line(self,log,log_time): - ''' - @name 获取ssh日志行 - @param log 日志行 - @param log_time 前一条记录的日志时间 - ''' - tmps = log.replace(' ',' ').split(' ') - if len(tmps) < 3: - return False - - data = {} - data['time'] = log_time - if log.find('closed by authenticating user') != -1: - data['user'] = tmps[10] - data['address'] = tmps[11] - data['port'] = tmps[13] - else: - data['user'] = tmps[8] - data['address'] = tmps[10] - data['port'] = tmps[12] - - data['status'] = 0 - if log.find('Accepted') >= 0: - data['status'] = 1 - return data - - #*********************************************** end ssh收费模块 ****************************************************** - - - - def get_curr_log_file(self,filename,search,min,max,log_list): - """ - @name 获取当前日志文件 - @param filename: 日志文件名 - @param search: 搜索条件 - @param min: 最小值 - @param max: 最大值 - @param log_list: 匹配的日志列表 - """ - - log_list.clear() - limit_max = '| tail -n {}'.format(max) - if search[0].find('&') >= 0: limit_max = '' - - shells = [ - "cat {}|grep -a 'Failed password for' |grep -v 'invalid' {} ".format(filename,limit_max), #登录失败 - "cat {}|grep -a 'Accepted' {}".format(filename,limit_max), - "cat {}|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid' {} ".format(filename,limit_max), - "cat {}|grep -a 'Connection closed by authenticating user' |grep -a 'preauth' {} ".format(filename, - limit_max) - # 登录失败 - ] - if filename == 'journalctl': - shells = [ - "journalctl -u ssh --no-pager|grep -a 'Failed password for' |grep -v 'invalid' {} ".format(limit_max), - # 登录失败 - "journalctl -u ssh --no-pager|grep -a 'Accepted' {}".format(limit_max), - "journalctl -u ssh --no-pager|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid' {} ".format( - limit_max), - "journalctl -u ssh --no-pager|grep -a 'Connection closed by authenticating user' |grep -a 'preauth' {} ".format( - limit_max) - # 登录失败 - ] - if len(search) == 1: - if search[0].find('Accepted') >= 0: - shells = [shells[1]] - else: - shells = [shells[0],shells[3]] - else: - shells = [shells[2]] - - result = [] - for shell in shells: - - res = public.ExecShell(shell)[0].strip().split('\n') - res.reverse() - for log in res: - result.append(log) - - find_idx = 0 - log_time = 0 - limit = max - min - for log in result: - log_time = self.get_log_pre_time(filename,log,log_time) - if len(log_list) >= limit: - break - - if self.__find_line_str(log,search): - - find_idx += 1 - if find_idx > min: - log = public.xssencode2(log.replace(' ',' ')) - log_list.append({'log':log,'time':log_time}) - - return find_idx - - - def get_sys_datetime(self,pre_time,log_time): - """ - @name 对比日志时间,日志时间保存年份,日志时间比前一条时间大,则判断为上一年, - @param pre_time 上一条日志时间 - @param log_time 当前日志时间 - @return 当前日志时间(校准年份) - """ - - if type(log_time) == str: - log_time = self.__get_to_date(log_time) - - if type(pre_time) == str: - pre_time = self.__get_to_date(pre_time) - - if (log_time > pre_time and pre_time > 0) or log_time >= time.time(): - d = datetime.datetime.strptime(public.format_date(times =log_time), "%Y-%m-%d %H:%M:%S") - n_date = self.__get_to_date('{}-{}-{} {}:{}:{}'.format(d.year-1,d.month,d.day,d.hour,d.minute,d.second)) - return public.format_date(times=n_date) - if type(log_time) == int: - return public.format_date(times=log_time) - return log_time - - - - def get_log_pre_time(self,log_file,_line,pre_time): - """ - @name 计算上次日志时间 - @param log_file 日志文件 - @param _line 日志行 - @param pre_time 上次日志时间 - @auther cjxin - """ - log_time = 0 - if _line[:3] in self._months: - log_time = self.to_date4(_line[:16].strip()) - elif _line[:2] in ['19','20','21','22']: - log_time = _line[:19].strip() - elif log_file.find('alternatives') >= 0: - _tmp = _line.split(": ") - _last = _tmp[0].split(" ") - log_time = ' '.join(_last[1:]).strip() - - log_time = self.get_sys_datetime(pre_time,log_time) - return log_time - - - def get_user(self): - ''' - @name 获取系统用户名 - :return: - ''' - pass_file = public.readFile("/etc/passwd") - pass_file = pass_file.split('\n') - user_list = [] - for p in pass_file: - p = p.split(':', 1) - user_list.append(p[0]) - return user_list - - - def get_log_byfile(self,sfile,min_num,max_num,search = None): - """ - @name 获取日志文件的日志 - @param sfile:日志文件 - @param min_num:起始行数 - @param max_num:结束行数 - @param search:搜索关键字 - """ - log_list = [] - h_find = None - #获取归档文件列表 - for info in self.get_sys_logfiles(None): - if info['log_file'] == sfile: - h_find = info - break - if os.path.exists('/etc/debian_version'): - version = public.readFile('/etc/debian_version').strip() - if 'bookworm' in version or 'jammy' in version or 'impish' in version: - version = 12 - else: - try: - version = float(version) - except: - version = 11 - if version >= 12: - h_find = {'log_file': 'journalctl', "list": [], 'uptime': time.time(), 'title': '授权日志', - 'size': 10000} - if not h_find: - return log_list - - #获取遍历文件列表 - file_list = [h_find['log_file']] - for info in h_find['list']: - file_list.append(info['log_file']) - - find_idx = 0 - log_time = 0 - limit = max_num - min_num - user_list = self.get_user() - - for filename in file_list: - #处理最新文件 - - if filename in ['/var/log/secure', '/var/log/auth.log', 'journalctl'] and search: - find_idx = self.get_curr_log_file(filename,search,min_num,max_num,log_list) - continue - - p = 0 #分页计数器 - next_file = False - sfile = filename - if filename[-3:] in ['.gz','.xz']: sfile = sfile[:-3] - - check_file,is_cache = self.__check_other_search(filename,search) - if check_file: - - cache_path = '{}/data/ssh/{}{}'.format(public.get_panel_path(),os.path.basename(sfile),check_file) - if not os.path.exists(cache_path): - self.__set_ssh_log(filename,check_file) - filename = cache_path - - #数据不够,则解压归档文件进行查询 - if filename[-3:] in ['.gz','.xz']: - public.ExecShell("gunzip -c " + filename + " > " + filename[:-3]) - filename = filename[:-3] - - while not next_file: - if not os.path.exists(filename): continue # 文件不存在? - if len(log_list) >= limit or os.path.getsize(filename) == 0: - break - p += 1 - - #public.print_log('读取文件:{},第{}页'.format(filename,p)) - #每次读取10000行,不足10000行跳转下个文件 - result = self.GetNumLines(filename,10001,p).split("\n") - if len(result) < 10000: - next_file = True - - result.reverse() - for _line in result: - if not _line.strip(): continue - - log_time = self.get_log_pre_time(filename,_line,log_time) - #处理搜索关键词 - is_search = False - if self.__find_line_str(_line,search): - is_search = True - - #读取数量超过最大值,跳出 - if len(log_list) >= limit: - break - - if is_search: - find_idx += 1 - if find_idx > min_num: - _line = public.xssencode2(_line.replace(' ',' ')) - # 解决SSH登录日志搜索ip或用户名不准确的情况 - if type(search) == list and len(search[0].split("&")) > 1: - rep_str = search[0].split("&") - rep = "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" - re_result = re.search(rep, _line).group() - if rep_str[1] in re_result: - log_list.append({'log': _line, 'time': log_time}) - elif rep_str[1] in user_list: - log_list.append({'log': _line, 'time': log_time}) - else: - log_list.append({'log':_line,'time':log_time}) - - return log_list - - def __set_ssh_log(self,filename,check_file): - """ - @name 缓存SSH登录日志 - @param filename 缓存文件 - @param check_file 文件类型 - """ - - cache_path = '{}/data/ssh/{}{}'.format(public.get_panel_path(),os.path.basename(filename),check_file) - - if check_file == '_success': - if filename == "journalctl": - shell = "journalctl -u ssh --no-pager|grep -a 'Accepted'" - else: - shell = "cat {}|grep -a 'Accepted'".format(filename) - elif check_file == '_error': - if filename == "journalctl": - shell = "journalctl -u ssh --no-pager|grep -E 'Failed password for|Connection closed by authenticating user' |grep -v 'invalid'" - else: - shell = "cat {}|grep -E 'Failed password for|Connection closed by authenticating user' |grep -v 'invalid'".format( - filename) - else: - if filename == "journalctl": - shell = "journalctl -u ssh --no-pager|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid'" - else: - shell = "cat {}|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid'".format(filename) - - if not os.path.exists(cache_path): - res = public.ExecShell(shell)[0] - public.writeFile(cache_path,res) - return True - - def __check_other_search(self,filename,search): - """ - @检测是否需要缓存ssh登录日志 - @filename 文件名 - @search 搜索关键字 - """ - if not search: return False,False - - if filename.find('secure-') >= 0 or filename.find('auth.log.') >= 0: - res = search - if type(search) == list: - res = ' '.join(search) - - is_cache = False - if res.find('&') == -1: is_cache = True - - if len(search) == 2: - return '_all',is_cache - if search[0].find('Accepted') >= 0: - return '_success',is_cache - return '_error',is_cache - return False,False - - def __find_line_str(self,__line,find_str): - """ - @ 批量搜索文件 - @ __line 文件行 - @ find_str 搜索关键字 - """ - if type(find_str) == list: - if len(find_str) == 0: - return True - for search in find_str: - if self.__find_str(__line,search.strip()): - return True - return False - else: - if find_str: - return self.__find_str(__line,find_str.strip()) - return True - - def __find_str(self,_line,find_str): - """ - @查找关键词 - @_line 文件行 - @find_str 搜索关键字 - """ - is_num = 0 - slist = find_str.split("&") - for search in slist: - if search == 'Failed password for': - #兼容多个系统的登录失败 - if _line.find(search) >= 0 and _line.find('invalid') == -1: - is_num += 1 - elif _line.find('Connection closed by authenticating user') >= 0 and _line.find('preauth') >= 0: #debian系统使用宝塔SSH终端登录失败 - is_num += 1 - else: - if _line.find(search) >= 0: - is_num += 1 - - if is_num == len(slist): - return True - return False - - - def get_log_title(self,log_name): - ''' - @name 获取日志标题 - @author hwliang<2021-09-03> - @param log_name 日志名称 - @return 日志标题 - ''' - log_name = log_name.replace('.1','') - if log_name in ['auth.log','secure'] or log_name.find('auth.') == 0: - return 'Authorization log' - if log_name in ['dmesg'] or log_name.find('dmesg') == 0: - return 'kernel buffer log' - if log_name in ['syslog'] or log_name.find('syslog') == 0: - return 'System warning/error log' - if log_name in ['btmp']: - return 'failed login record' - if log_name in ['utmp','wtmp']: - return 'Logon and restart records' - if log_name in ['lastlog']: - return 'User last logged in' - if log_name in ['yum.log']: - return 'yum package manager log' - if log_name in ['anaconda.log']: - return 'Anaconda log' - if log_name in ['dpkg.log']: - return 'dpkg package manager log' - if log_name in ['daemon.log']: - return 'System background daemon log' - if log_name in ['boot.log']: - return 'Boot oog' - if log_name in ['kern.log']: - return 'Kern log' - if log_name in ['maillog','mail.log']: - return 'Mail log' - if log_name.find('Xorg') == 0: - return 'Xorg log' - if log_name in ['cron.log']: - return 'Scheduled task log' - if log_name in ['alternatives.log']: - return 'Update alternate information' - if log_name in ['debug']: - return 'Debug log' - if log_name.find('apt') == 0: - return 'apt-get related logs' - if log_name.find('installer') == 0: - return 'System installation related logs' - if log_name in ['messages']: - return 'Comprehensive log' - return '{} log'.format(log_name.split('.')[0]) - - - def get_history_filename(self,filepath): - ''' - @name 获取归档文件名称 - @filepath 文件路径 - ''' - log_name = os.path.basename(filepath) - - #归档压缩文件,auth.log.1.gz - if filepath[-3:] in ['.gz','.xz']: - log_file = filepath[:-3] - if os.path.exists(log_file): - return False - log_name = os.path.basename(log_file) - - #处理auth.log-20221024 - if re.search('-(\d{8})',log_name): - arrs = log_name.split('-') - arrs = arrs[0 : len(arrs)-1] - return '-'.join(arrs) - #处理auth.log.1 - if re.search('.\d{1,10}$',log_name): - arrs = log_name.split('.') - arrs = arrs[0 : len(arrs)-1] - return '.'.join(arrs) - return log_name - - - - def get_sys_logfiles(self,get): - ''' - @name 获取系统日志文件列表 - @author hwliang<2021-09-02> - @param get - @return list - ''' - res = {} - log_dir = '/var/log' - for log_file in os.listdir(log_dir): - if log_file in ['.','..','faillog','fontconfig.log','unattended-upgrades','tallylog']: continue - - filename = os.path.join(log_dir,log_file) - if os.path.isfile(filename): - #归档文件原名 - log_name = self.get_history_filename(filename) - if not log_name: continue - - if not log_name in res: - filepath = os.path.join(log_dir,log_name) - if not os.path.exists(filepath): continue - - res[log_name] = { - 'name':log_name, - 'log_file':filepath, - 'size':os.path.getsize(filepath), - 'title': self.get_log_title(log_name), - 'uptime': os.path.getmtime(filepath), - 'list':[] - } - - if log_name != log_file: - res[log_name]['list'].append({ - 'name':log_file, - 'size':os.path.getsize(filename), - 'uptime': os.path.getmtime(filename), - 'log_file':filename - }) - else: - for next_name in os.listdir(filename): - next_file = os.path.join(filename,next_name) - if not os.path.isfile(next_file): continue - - log_name = self.get_history_filename(next_file) - if not log_name: continue - - if not log_name in res: - - filepath = os.path.join(filename,log_name) - if not os.path.exists(filepath): continue - - res[log_name] = { - 'name':log_name, - 'log_file':filepath, - 'size':os.path.getsize(filepath), - 'title': self.get_log_title(log_name), - 'uptime': os.path.getmtime(filepath), - 'list':[] - } - - if log_name != next_name: - res[log_name]['list'].append({ - 'name':next_name, - 'size':os.path.getsize(next_file), - 'uptime': os.path.getmtime(next_file), - 'log_file':next_file - }) - - - log_files = [] - for key in res: - res[key]['list'] = sorted(res[key]['list'],key=lambda x:x['name'],reverse=True) - log_files.append(res[key]) - log_files = sorted(log_files,key=lambda x:x['name'],reverse=True) - return log_files - - - def get_lastlog(self,get): - ''' - @name 获取lastlog日志 - @author hwliang<2021-09-02> - @param get - @return list - ''' - cmd = '''LANG=en_US.UTF-8 -lastlog|grep -v Username''' - result = public.ExecShell(cmd) - lastlog_list = [] - - p = 1 - count = 20 - if 'count' in get: count = int(get['count']) - if 'p' in get: p = int(get['p']) - - search = '' - if 'search' in get: - search = get.search - - idx = 0 - min,max = (p -1) * count, p * count - for _line in result[0].split("\n"): - if not _line: continue - if search and _line.find(search) == -1: continue - - _line = public.xssencode2(_line) - tmp = {} - sp_arr = _line.split() - tmp['User'] = sp_arr[0] - # tmp['_line'] = _line - if _line.find('Never logged in') != -1: - tmp['last login time'] = '0' - tmp['Last login source'] = '-' - tmp['Last login port'] = '-' - - else: - tmp['last login time'] = sp_arr[2] - tmp['Last login source'] = sp_arr[1] - tmp['Last login port'] = self.to_date2(' '.join(sp_arr[3:])) - - if idx >= min and idx < max: - lastlog_list.append(tmp) - - idx += 1 - lastlog_list = sorted(lastlog_list,key=lambda x:x['Last login port'],reverse=True) - for i in range(len(lastlog_list)): - if lastlog_list[i]['Last login port'] == '0': lastlog_list[i]['Last login port'] = 'never logged in' - return lastlog_list - - - def get_last(self,get): - ''' - @name 获取用户会话日志 - @author hwliang<2021-09-02> - @param get - @return list - ''' - cmd = '''LANG=en_US.UTF-8 -last -n 1000 -x -f {}|grep -v 127.0.0.1|grep -v " begins"'''.format(get.log_name) - result = public.ExecShell(cmd) - lastlog_list = [] - - search = '' - if 'search' in get: - search = get.search - - p = 1 - count = 20 - if 'count' in get: count = int(get['count']) - if 'p' in get: p = int(get['p']) - - idx = 0 - min,max = (p -1) * count, p * count - - for _line in result[0].split("\n"): - if not _line: continue - if search and _line.find(search) == -1: continue - - _line = public.xssencode2(_line) - tmp = {} - sp_arr = _line.split() - tmp['User'] = sp_arr[0] - if sp_arr[0] == 'runlevel': - tmp['Source'] = sp_arr[4] - tmp['Port'] = ' '.join(sp_arr[1:4]) - tmp['Time'] = self.to_date3(' '.join(sp_arr[5:])) + ' ' +' '.join(sp_arr[-2:]) - elif sp_arr[0] in ['reboot','shutdown']: - tmp['Source'] = sp_arr[3] - tmp['Port'] = ' '.join(sp_arr[1:3]) - if sp_arr[-3] == '-': - tmp['Time'] = self.to_date3(' '.join(sp_arr[4:])) + ' ' +' '.join(sp_arr[-3:]) - else: - tmp['Time'] = self.to_date3(' '.join(sp_arr[4:])) + ' ' +' '.join(sp_arr[-2:]) - elif sp_arr[1] in ['tty1','tty','tty2','tty3','hvc0','hvc1','hvc2'] or len(sp_arr) == 9: - tmp['Source'] = '' - tmp['Port'] = sp_arr[1] - tmp['Time'] = self.to_date3(' '.join(sp_arr[2:])) + ' ' +' '.join(sp_arr[-3:]) - else: - tmp['Source'] = sp_arr[2] - tmp['Port'] = sp_arr[1] - tmp['Time'] = self.to_date3(' '.join(sp_arr[3:])) + ' ' +' '.join(sp_arr[-3:]) - if idx >= min and idx < max: - lastlog_list.append(tmp) - idx += 1 - # lastlog_list = sorted(lastlog_list,key=lambda x:x['时间'],reverse=True) - return lastlog_list - - - - def __get_to_date(self,times): - """ - 日期转时间戳 - """ - try: - return int(time.mktime(time.strptime(times, "%Y-%m-%d %H:%M:%S"))) - except: - try: - return int(time.mktime(time.strptime(times, "%Y/%m/%d %H:%M:%S"))) - except: - return 0 - - - - def get_sys_log(self,get): - ''' - @name 获取指定系统日志 - @author hwliang<2021-09-02> - @param get - @return list - ''' - - log_file = get.log_name - - p,limit,search = 1,5,'' - if 'p' in get: p = int(get.p) - if 'limit' in get: limit = int(get.limit) - if 'search' in get: search = get.search - - sfile_name = os.path.basename(get.log_name) - if sfile_name in ['wtmp','btmp','utmp'] : - return self.get_last(get) - - if sfile_name in ['lastlog']: - return self.get_lastlog(get) - - if get.log_name.find('sa/sa') >= 0: - if get.log_name.find('sa/sar') == -1: - return public.xssencode2(public.ExecShell("sar -f /var/log/{}".format(get.log_name))[0]) - - is_string = True - - result = [] - min,max = (p-1) * limit , p * limit #最小值,最大值 - log_list = self.get_log_byfile(log_file,min,max,search) - - for info in log_list: - _line = info['log'] - if _line[:3] in self._months: - _tmps = _line.split(' ') - _msg = ' '.join(_tmps[3:]) - _tmp = _msg.split(": ") - _act = '' - if len(_tmp) > 1: - _act = _tmp[0] - _msg = _tmp[1] - else: - _msg = _tmp[0] - _line = { "Time": info['time'], "Role":_act, "Even":_msg } - is_string = False - elif _line[:2] in ['19','20','21','22']: - _msg = _line[19:] - _tmp = _msg.split(" ") - _act = _tmp[1] - _msg = ' '.join(_tmp[2:]) - _line = { "Time":info['time'], "Role":_act, "Even":_msg } - is_string = False - elif log_file.find('alternatives') >= 0: - _tmp = _line.split(": ") - _last = _tmp[0].split(" ") - _act = _last[0] - _msg = ' '.join(_tmp[1:]) - _line = { "Time":info['time'], "Role":_act, "Even":_msg } - is_string = False - else: - if not is_string: - if type(_line) != dict: continue - - result.append(_line) - - public.set_module_logs('sys_log','get_sys_log') - try: - _string = [] - _dict = [] - _list = [] - for _line in result: - if isinstance(_line,str): - _string.append(_line.strip()) - elif isinstance(_line,dict): - _dict.append(_line) - elif isinstance(_line,list): - _list.append(_line) - else: - continue - _str_len = len(_string) - _dict_len = len(_dict) - _list_len = len(_list) - if _str_len >= _dict_len + _list_len: - return _string - elif _dict_len >= _str_len + _list_len: - return _dict - else: - return _list - except: - return '\n'.join(result) - - - - #取文件指定尾行数 - def GetNumLines(self,path,num,p=1): - pyVersion = sys.version_info[0] - max_len = 1024*1024 * 2 - try: - from cgi import html - if not os.path.exists(path): return "" - start_line = (p - 1) * num - count = start_line + num - fp = open(path,'rb') - buf = "" - fp.seek(-1, 2) - if fp.read(1) == "\n": fp.seek(-1, 2) - data = [] - total_len = 0 - b = True - n = 0 - for i in range(count): - while True: - newline_pos = str.rfind(str(buf), "\n") - pos = fp.tell() - if newline_pos != -1: - if n >= start_line: - line = buf[newline_pos + 1:] - line_len = len(line) - total_len += line_len - sp_len = total_len - max_len - if sp_len > 0: - line = line[sp_len:] - try: - data.insert(0,line) - except: pass - buf = buf[:newline_pos] - n += 1 - break - else: - if pos == 0: - b = False - break - to_read = min(4096, pos) - fp.seek(-to_read, 1) - t_buf = fp.read(to_read) - if pyVersion == 3: - t_buf = t_buf.decode('utf-8') - - buf = t_buf + buf - fp.seek(-to_read, 1) - if pos - to_read == 0: - buf = "\n" + buf - if total_len >= max_len: break - if not b: break - fp.close() - result = "\n".join(data) - if not result: raise Exception('null') - except: - result = public.ExecShell("tail -n {} {}".format(num,path))[0] - if len(result) > max_len: - result = result[-max_len:] - - try: - try: - result = json.dumps(result) - return json.loads(result).strip() - except: - if pyVersion == 2: - result = result.decode('utf8',errors='ignore') - else: - result = result.encode('utf-8',errors='ignore').decode("utf-8",errors="ignore") - return result.strip() - except: return "" \ No newline at end of file +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +PEKPgJeDDCLnL9UcS39EYQ0oxS0YHncLtyklh46m5EBkNzGCoSotSCFeO4A9wJZj +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +izOKcEjLnmyxxrod80R5Ow== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +9/AEQy6sYem8wkQNCggJi4i9kWw0XQ381/dYG6HKfZo= +piG6BsMF31u4R4iA6R4SRA== +dTLQ8Wr3wPn7w6pte1B1YA== +wiYVtfO/yzajW1Tv5z7hyA== +bDrBrYFBEwTqHDQlSf5Wqw== +SbQQ5SqO9QrBwZ9ObpMYLw== +1u+XjG/2+GSQRv6EzCaWRQ== +XIfdJ79nObMM+vyAmKbTmw== +1w/Bux0eSLh4laruMV+kEu4fb+1yCwxQ0hOHwrLNXXRaWZyv1p2zipoavmFan6NZ +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +hbDorme8BqzUEmeXCAWmYa52umm3PBCcfSvATzTG02w= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +hxPPiT2HRNyGCUbYWjiGemMsho9zzoERNvAKutOOCEWVhC1sbjDeebh+SGn5u8GmkL2g41VpzeVyv9m/J//MuDXVnCmww1qdZncqaHpwyg8= +wgR07xfoapmx6eEnFHXXYlFDtUVt+yJ+tU8VHk3FJTvnJUFpUNld/t84QFPEYKTpJU156tin3Vc241jPdNebVm6HK/Ydm5jv2Mb3PtLSJC4= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +0feE7ECax+/ZCUSs3gQ/3AaKzCTzDvnrH+EOl140p3Ejvkj/qRKNVgZY6MNTd6YR+5IenuNCFRCqo568dB1SxUvcHBcdz25PdNciWuU0T5T0+kclUUtQxfNacEShYTanibNFhUnMe4HHLWOUhn13GDCqcyIADTbNu8ZEuFklZzsT8l2mK3j3yk9iI88+Tz8U +1u+XjG/2+GSQRv6EzCaWRQ== +TC/LaX35Eb0V/GmW3Zr2UpJqD2VXFNDkddRfcdBjn5Xo11cfHVg4MHTU1ECnQkBR +Z8UsPk1Q7HtwjRd4g01ryw== +wa8O1mcGGxpEvPPRX3ikmvykeeKhIH9HZybe0SWYg6M= +ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE= +wI4EEWzdSnynhqFxMCzFmo1bZxLdYghu7tPjNNa83Yw= +Z8UsPk1Q7HtwjRd4g01ryw== +SvL+08tQNi0qJOHjInKZY6RndgMQo6zUlLYIZu8iQNU261HNxXIEnmCJLHd/cAn682cyzkJMzokqlyn5ZLB0wQ== +GDxnwLueKeYNT0/dIp4TaA2EgKkJaFhgdpXqI89CP9JxudfN4YO5Bip/xZCOAyUl +89eRayX3G8tCX3FDU5IcjV0+/dsDUitdv1ScoFSstoWJGpFlcQ4x5TphzsFb2IQj +VmVrGQo2zRokW/ZuO9bN66izANLHguDX6h0ZMylPTZjSNwJjUmKceY0nGArlnnjq +F9Sftf1PoN5P+lgvc+r10NBQ12btoHUSbewJsWZIkLA4htiDy2/7V46D3DsQdHGR +VmVrGQo2zRokW/ZuO9bN61Go4AfJcv9+G+6Yze5rakTqOEVt8xxc04JEb0WvhXiiyOuFu/FvRpO928WIX0wTxw== +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +1u+XjG/2+GSQRv6EzCaWRQ== +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P3lRQWSpZzKNexozsWlQSmt +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidZcC3xryH5q1zGGaoBvvKTdC1AgWcdepqNrVhCT6tqt7k+JxPMwVGPnSqxhvLgv1DQhFmLlBc8XKdIY0AICqQQB +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmzHeTpF9D/4KEYPN1ABlSXLB+bxGaCCDvXrlgokvNEu+QTyZ0OkNbOjawGeCOhOAY= +1u+XjG/2+GSQRv6EzCaWRQ== +IyCWxP6fYDO4E5XteRNwiDXKEE7gzrOdM346GEQCZO8= +wlLHv6kT3Q/RmtMBN4nDAddbPv2l9qdO18ddV+SHYW6sapFy5qC4Sj9YrQq+XZS6 +VmVrGQo2zRokW/ZuO9bN64+cUfXVRogUENm4c6ZYTPLju4UDx+l86nzBCmwvVd0uwMYnjNJoL6js1zgp7gkYPENBFYuO7E5Nvxpmku1cGUU= +VmVrGQo2zRokW/ZuO9bN66z/+UOEROJ3qlfGXBlB86SWuPnq0AXG2phBzwXE7v1v +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN68lNxH5zpt5sybOcTXtNnpfItHRxcKV1GN9wgLL6Y/1R +1u+XjG/2+GSQRv6EzCaWRQ== ++kSw/JERkIrPRLyCm/D7AdGS0jaXYQYSNkJ6PDaS5kKRDw+QnaOx8ss81acdnppHhvRGi5fhyW54Z5gPLzuFGbO8BuY6dL1EqHs8DpgsB0c= +96orka/uERLyRst14azQwkEOOUsX1Ai0XqkbdTxnOingAid639l5vKz82fyzWZRhvWwi6CRkoGDeVwcGakosUg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +TC/LaX35Eb0V/GmW3Zr2UpCUU06JrEEfNymvLzRwUVgF/R4iaOcaI2S3O3N7OAwl +Z8UsPk1Q7HtwjRd4g01ryw== +wa8O1mcGGxpEvPPRX3ikmjl4u4bTOurjxSyetFOSLpQ= +5Ch5Cy/OpWh+1WaF8Su/eDwl93O5klNCtuxXaiypyoA= +wI4EEWzdSnynhqFxMCzFmo1bZxLdYghu7tPjNNa83Yw= +Z8UsPk1Q7HtwjRd4g01ryw== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P1DHXJ/TJdbmPH2ckSraWgG +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidZcC3xryH5q1zGGaoBvvKTdC1AgWcdepqNrVhCT6tqt7pCgU8fcpxsvRgulBE1NZu9SSmus2oz4HzvvEcXgYZbN/5tlDq8RdAY5WQkwLWPTq/dVzZBv6z2B8FhigMKfcMnFomnMj1iyhyb10pjIliz/ +1u+XjG/2+GSQRv6EzCaWRQ== +esnxVrQTJckUcXi8Vt8zdyY4i7Esn3+rv1ghcUNo4q0= +NhnIR3Ilo4H2su9/cTNo/L0HJQSj7xDirJmInN4ogA7N4utqCGavJw/zWl3BqVBOcu0yh4310yAJbcS5M5y7KLT8b6U9cXXn+DOi/El+juU= +1V2v8QerKOmubvSxgB4eTBzsiAfEZawGIv/PJDVZk8DLw/veVTQK3qL0m1VX1XdQ +o6QhOIN2Sc4SHELnst17ubkalL7sSCEY8nh1Aau3kAESZfNThdx/u6aA+dfb26kB +1u+XjG/2+GSQRv6EzCaWRQ== ++kSw/JERkIrPRLyCm/D7AdGS0jaXYQYSNkJ6PDaS5kKRDw+QnaOx8ss81acdnppHhvRGi5fhyW54Z5gPLzuFGbO8BuY6dL1EqHs8DpgsB0c= +96orka/uERLyRst14azQwkEOOUsX1Ai0XqkbdTxnOingAid639l5vKz82fyzWZRhvWwi6CRkoGDeVwcGakosUg== +1u+XjG/2+GSQRv6EzCaWRQ== +TC/LaX35Eb0V/GmW3Zr2UtYsmXWasld7EcVPoh9dGjGt+2Xjb/ezeoPiUvh51Scc +Z8UsPk1Q7HtwjRd4g01ryw== +wa8O1mcGGxpEvPPRX3ikmj1BsUFLbf8jUkWlQ6+15/GeyFemD4bVxaphwo/ntI75 +ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE= +wI4EEWzdSnynhqFxMCzFmo1bZxLdYghu7tPjNNa83Yw= +Z8UsPk1Q7HtwjRd4g01ryw== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcOuWMW7q96BiVbhOlHj8k0LS+GLTgLVoUNjFkoOOd6Q/ +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidZcC3xryH5q1zGGaoBvvKTdC1AgWcdepqNrVhCT6tqt7pCgU8fcpxsvRgulBE1NZu9SSmus2oz4HzvvEcXgYZbNsAyiUdVbe7Hjmg9ET03bHboNn3w+ITWBFzpEUndWW90= +1u+XjG/2+GSQRv6EzCaWRQ== +esnxVrQTJckUcXi8Vt8zdyY4i7Esn3+rv1ghcUNo4q0= +NhnIR3Ilo4H2su9/cTNo/L0HJQSj7xDirJmInN4ogA7N4utqCGavJw/zWl3BqVBOcu0yh4310yAJbcS5M5y7KLT8b6U9cXXn+DOi/El+juU= +1V2v8QerKOmubvSxgB4eTCJqrBg4vZgNAdzlsRAbXfGCflRUO5Z4sXw8H0VTfzWe +1u+XjG/2+GSQRv6EzCaWRQ== +o6QhOIN2Sc4SHELnst17ubkalL7sSCEY8nh1Aau3kAESZfNThdx/u6aA+dfb26kB ++kSw/JERkIrPRLyCm/D7AdGS0jaXYQYSNkJ6PDaS5kKRDw+QnaOx8ss81acdnppHhvRGi5fhyW54Z5gPLzuFGbO8BuY6dL1EqHs8DpgsB0c= +96orka/uERLyRst14azQwkEOOUsX1Ai0XqkbdTxnOingAid639l5vKz82fyzWZRhvWwi6CRkoGDeVwcGakosUg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +gn9R9ypc7EQr3UZGxM7R2AzpBmjpt//gk3VbIVrmsWPUgmmJziYMAJ3SBavXHHpg +Z8UsPk1Q7HtwjRd4g01ryw== +IafCwgGxn6pY+tyyf7k+k1uPwgoyKLoGy1T2IHFPUrs= +lGY+HGJ/DlLufZHWWlj7U8JUjESN387GevZrf0/dwpoTGJutrsojcsEElOQwuKhR +ZCo7BKJgBzBoR5QyWRMODvuiXuF1YyjhIRa499f9oeDFOwCSNimfzEv6xwOiYENc +Z8UsPk1Q7HtwjRd4g01ryw== +NYUPEpAMYcAgMU1lywXh+APRTYQ7bT0FxrwWNK9r+kE= +X/Lp4PsbbXulceSXUvGUpAixZi3IzpQX5iLz2HxFdWwS5A14Kie1TLFHz4QUExEMrjBXCKSQ2mS07fFxKBoIRQ== +s2s/1ED0ebJ2NmFlE8kk05gdRKkjvH+bphLoBZkO6vMkQmee56NmkDMz3ewSmOSg +wlLHv6kT3Q/RmtMBN4nDASYMmJRC8P+40dq2mnWeVhc= +VmVrGQo2zRokW/ZuO9bN6+vO0T5LxpdIZ6hmr5+BXMwvx1h4H3tu9h3Lw+gtPHVT +ACEEdgL/kNbx7RaZcSRxZKy0zW5JN3zc9G0K673Kt8E= +VmVrGQo2zRokW/ZuO9bN6+CU0Uzm+BLGwlKn+xqu8O0= +L0eUthVnpkGsmKFAX6d+uOW5BVuM+7tcrCiEwvU64kY= +CAhn8dRUItEbEErp4w+lX3cOtVXg8rs8OR96pWoJZvY= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +TC/LaX35Eb0V/GmW3Zr2Uq12VTZnVAgLNUPRVpboscSkdq/rWesDEOFnnwfIdOM2 +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY8qDyry3eeYEnSUnFHqFOEdqLpNRCL3M5LaBhSQtDro7 +7JQQDTi0T2idhFjao4jEpWyyzmLHEngKhI6rkPYC+OEQ3v3507uiPdl6O4aO+4ER +7JQQDTi0T2idhFjao4jEpV/go6iO1qzsLRmLYXsC2awnSWcIsvPYVwnYvfMjZaDtow5+WSfBeFo+pvL7hARzXDxRko1EzJaJ+fi1oy8D1/8= +KZTmaJLp+FU9X93j5Tpqtw== +CRgPeWAvMGMS2vfLmB+XqAPNkWJygDEUlCyJRk0QVvtGXcHrIivyKjsAViAs3OCtr/wdXA9sspltDH7P0695hw== +/R3t0fIiNxFa1+EM4KdcC/Tq9YNQxX2rnc682C9+fe8= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +gmymfXJ+E4e4NAtQtupPJnNetLAHixU3GutHyzvm5FjbdeuSmaUynxh9ANB6mVzZ +/2yBdrTJDE3TFtcSLCcx/9sJSd4kIdV7hBv4DW/k5kQjLCSiOqb/ooMNKbVZJjgekfdghInCCdI6qQz/jIgZgA== +NhnIR3Ilo4H2su9/cTNo/FhhB6MZ5HlL8ilxek9dnzUbA0zrRzxd/m81iyHyj8Jn +NhnIR3Ilo4H2su9/cTNo/KlYN/bsxuWzFF+nWhu3hBR2BWGqsiqwtBlTpURXFcpL +NhnIR3Ilo4H2su9/cTNo/G1hu91GucoyUk5CAa7Se62ja+Iodh3ArUEpNw9/IIap +l2FJPs4YkAmmok1ulDRuSA== +NhnIR3Ilo4H2su9/cTNo/FhhB6MZ5HlL8ilxek9dnzWFsroGkpAbYhhbP16I8llP +NhnIR3Ilo4H2su9/cTNo/KlYN/bsxuWzFF+nWhu3hBQYlkX5gl9hxYgaKXgpyNbH +NhnIR3Ilo4H2su9/cTNo/G1hu91GucoyUk5CAa7Se62jqdhNgRiCY2VhbppcAkgq +1u+XjG/2+GSQRv6EzCaWRQ== +eDDKQ1PxEOXD40WdPkjAt+4gIw5LFJ2JffPLp1i0RM0= +/2yBdrTJDE3TFtcSLCcx/7i0c5WFgpYA4Y1T5vIUHVO5oUJ3yHw6al3zQOD6XGLm +NhnIR3Ilo4H2su9/cTNo/B0KkXyoVKsrbyorgJnjAt4= ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +0feE7ECax+/ZCUSs3gQ/3AaKzCTzDvnrH+EOl140p3Ejvkj/qRKNVgZY6MNTd6YRirlilOXAKg9O2fXCfHGfBtMDPeLErFs86v0ZeSLWbiUlxSp8Q6JZIdSDtsOukEmI9CkgD9cr1YKi5zNr/8OAB6nq4n5Nx0RnnUUavr/JMa8eGXsXCu3G6OAO8lGrXUZm +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +ISoywpaLHuQBxWIUflU2WE6goDl6H+SMTlvAvhjEOunmRWoDI71YFjPGTq2PREVfgI/0kDXgAM1iC59/IdE0zUqUlwg/visqR/ji4CUCNls= +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P2neUS+qyt4fxBbg7aS5U8ZVSEGgWJcksomV0iOXfr3S +wOl7lRII4ZgqRTj9M//0b1qhFBAHPMvcKJHX8xF2dd3LFwNZYH3pF0t/6DGxSkRF +yqM179SzW9HnBHozkU+IlZLJm7366WyNnx7ZMsu9exUEQG5/9gzkoWdK3lm2KNSi +NjU4zQ5DRyOHitK6CHLOgNNxRKGkyV3kGqu2T3oe3wk= +NjU4zQ5DRyOHitK6CHLOgMJ4vze7G/BcNjyu30dz0dI= +bIts5UhOLbX+5vrkUi2fEhpPnL7BGuZkarYaOytlgoinj9jnlLh+lL3x0ZV5fo0E +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTafshnKFviV+idW9zO4AP3io= +1P+dKgYYLCQ5O0NZwxSOaJcUBjFbQdzF9CNcdagyAI18a21hAHVeicWVY9WgSU2K +zrSEP+cwIY0LGAkn1FqokI6RwnSJpjctW3UvricDZLxItXuL73USA+6pRr2HvpJaC7RiR4yb1PJQkbGlH0YRWQ== +1u+XjG/2+GSQRv6EzCaWRQ== ++xLLwGkVmjlAlAN2wYY0FOnVwRrXqNlRBemygIVo5xo= +p0E//cFR8EOf13yDhOlObMD3Yuxj4bTt6EnfF43vdnLk7mp9E28enjNVoyEj00PVBw63PNtJHh7nYjCP57FGPqozYy2TzONryzbzwkoUMNwvUkxvlVRrGo2rxAnE/g0V4Cmmkw+dKL0nhwQooy8bnVmuDHamveEqMpiknfK8NBA= +p0E//cFR8EOf13yDhOlObP3FQjpN4YYsA4YJPXooDzrxOF7DYxRlXeKipAKSaevnHWB9P8oJq6upWDy/MUOvLA2G18HPNK28orPv/JxpaCU= +p0E//cFR8EOf13yDhOlObJwu0OCz0PycRy7pO+A7qycPBfJfS4wcckld5TKXEJcQ8LTGY/BgOJ8/Ey+ycvnkaPNCF8x8ePPtPFV33M0BgGR33MbsHGevwAuVMKg7zRUDIXwgrTQUFIdujw+lXXU2GmtNOEJjkhHjhR2dXDsYQl+qxS0SKUpVIYzDr1eRI12GUQFYJwR76xewQvSowM7l2Q== +p0E//cFR8EOf13yDhOlObNQNgN99cJOreDX6FzaHhxv3V4pef0BAT3QaZNKMIUivldoeOBbHNkFVO4BMgqzu4fDHYcR1BKL6eBdwt3OBu1kVdmgx3s4fTZ8LAlEPq2BRzuN4F8ZeuA1ppvWiDhACcEHyX6Pz6yKUiqxmQn0qung= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw58tqRCidnUtuhiV/SQek1vZ6sNmdinwpH5+NOEy1mNl8yusqwsRmAnJlzr3FKfnstUDfM7vtqOWitJUQd0uNl0= +tmi7c6vWl6N6sdrwujX3GReSPhJ5ssuXUZtdOTkPGXc= +4E0YNxQKZtqIoiEqkAJApA== +fE3zmgbfhO9ZwEda5GgwDzwSc+DUx0Ss08sG6egCcJP+XUG+DzlYynYbT/JVkU7P +EsliW4ECGA8kkEJ0BUSIKZ+6kvYYxaoa29sn6RE+30Q= +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUqoJYM2k3wpGIGNfe4ZVylRY2CmgGV6o3LCAp54cduWSIwMrRMTIErqM/4y21/qV2s2Bxs/+ycPzzZdeNzsz9/q4sZS2NAVCp5rDAV2WNLaY= +VmVrGQo2zRokW/ZuO9bN6xCL2jO48J3n3RHpsxsFW0A= +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUtD4Dy7F2C5KfRvcNVwIaIQRcd7Fy2ZXHLWGZJPl8MX9RBwYra8rs4CAKfeGMN3Bb +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUgC86euXJOlFyHZyqDAOLUAdeaHFM4IYtg18Uequ7wafHVvZue+VX8ISJjUr1sqwFRVRgsWgqIDu5NviNkV3pauDs1a5D7tn5bIpmJcr1KCm6cN6DLgXTjktT05vWK1srWo2mj1pylV4R46zpVCmFGA== +VmVrGQo2zRokW/ZuO9bN66Op6JyhixbDicezrxsJdmvmw/JvaaHtlFkTsLFyz3qi +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUv3HBNQdTfHHC76HdkzkeKhcn8l+7nqNJm8fOH+30rrfwQzm8hP4An/vuWNJxgVfdBK/7rxGOROS+FKukjRDAA7v4+O6TFSTWLNyfZEFdSHAACyn19p4G4SaBK+kNp7Y4 +VmVrGQo2zRokW/ZuO9bN63hl7oGUPd8q+ErnISkgMco= +VmVrGQo2zRokW/ZuO9bN6xCL2jO48J3n3RHpsxsFW0A= +4E0YNxQKZtqIoiEqkAJApA== +AaV0Q13NpbJz1j/27jY/ubg6CfL/9D/rk9oHG6vv7Tk= +J1UGlXkKhKXD36OrXXhN8PbGS4CCkC/QcSnejunC4s1HNXfKIR7pGHCoZHkqc2al9dG3/LRVuqxK1/lHGulhpw== +VmVrGQo2zRokW/ZuO9bN64ywnuovLuNhCiDoP2wQAWzHwT2J7ZgEwYI2DHVR0db7 +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN64ywnuovLuNhCiDoP2wQAWxQoOqVjvwf024i/wGnySdC +l2FJPs4YkAmmok1ulDRuSA== +EsliW4ECGA8kkEJ0BUSIKVKU5W04dIhCWYbq1NBglGIbd3FwxA+IXISbsRb5KU8x +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +6xO+ubEwkQTp/D6ATXPZaR9RFvPZlxoFFcMoTEBWzdY= +1u+XjG/2+GSQRv6EzCaWRQ== +lOh2GtzHjjMM8E9J40AuOWo7iSH0P5z8Y9QjLJCJBGjZZ/Q9S9r5t3lPlsYorgw68Y18oFZuov4IXxgtwIU+fyOxgNLXV9uwsqyK3UQIuC0= +T0lwjDK5GymcJe/LbHEB+5LTXLLonGWNuuTQfjCTPqg= +wlLHv6kT3Q/RmtMBN4nDAfGIYwX5xDRj7uztmlOephI= +VmVrGQo2zRokW/ZuO9bN615pZCvbchosnTxjAGDSTuu5F/3dP6AEc2sI/MY5xwN3 +1u+XjG/2+GSQRv6EzCaWRQ== +YnP/gQVEpWES+RQ7J7RJRlUivABjbdmquJjPlRdBH7s= +Wd7OXFQxn2HMHBqE448Pi6S8+/GBA34d3QZMmVea2Tk= +p6i8iK7HDbvmLpoFrhNyvxqRSTuyLOZi037vACDsfa8= +esnxVrQTJckUcXi8Vt8zdytTNOUxGpVlE+9gz0v5AKo= +topgAunkM89n84cH9D9pHIJ0dN6Gk7b/C+bsk/3fJRquhc7fUjMhZYvLbsfG2DMWzaSd6qLjQSoUeAO/UtXhP1Yel7kB7XwKvSI9CIFJqS8= +ACEEdgL/kNbx7RaZcSRxZPvmvyPIJl0tNCG2z5oQFSMF3Rtz2Zl3Uf+ip6mRNkxm +VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM= +1u+XjG/2+GSQRv6EzCaWRQ== +J1UGlXkKhKXD36OrXXhN8DolGRp6NkzBMOOoPdzZSROBDCr+gph8JmqTmJ+BwzEcAyCmQfvb6v5GI6hFw38sPQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+M80RvQb5VsoyLkSgRiqcM= +VmVrGQo2zRokW/ZuO9bN61RJ7HPvuDQyY/7jnqZlstvr14lD1Nsc1GBMciUP56h8 +VmVrGQo2zRokW/ZuO9bN67mWWDHj8CSfv92pyS9sLERjFtkVPnpdPLZSP2NPs1gXeeQZjNsawPLfcnlKBwAakNgFVuo2Yd5N4wsDtWM07aE= +VmVrGQo2zRokW/ZuO9bN68V03MBz2tWTRGBIaN8QcnWa3+54ie/uRDPpsaNfYNUhfJWkoMWEjN141ojxjPQz7iUbeWa1gJ4fwE2A4QfkFyY= +1u+XjG/2+GSQRv6EzCaWRQ== +5wsVwKLrIjIWqeTNpSVp9QDHrlhrdh8VDOVcdAVCwxo= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +QdTjTxbShl+8mrARISGoAew+M6wRwESTJ4GivNwwWTIyDaIjNgge4ZpF0WBvDJqZRgXN2RgjWkE0y98VXQbEpQ== +Z8UsPk1Q7HtwjRd4g01ryw== +EauAFvhEfQAlaCCYJmuW5eV4DJOW7GNwqk+bbKrKpSaH1MvmX7D3DvPHyH3oySvOnRXE5AlMLgwUDhfDZANse1idoOMd3rJKanSLH7Uc123RRwtjA5eu4NckUBCieqKNHkfIlUn5nFjMA1ICSzTCQsv5pY+TV/1yO9BG5IofZXg= +eeFiPlXUI0Fos82HYztSmaNe9in2SzXpHGr5hBhAinnNHyxMDiBuvbazGbrnCMky +bIts5UhOLbX+5vrkUi2fEhOK0qYcUWJPnPtVafbmigUdzXiHWAONlKw9KDOwOlPM +akHibliG6j9uvBEg96vRelg6b5pjOK0azc0J/Ev8D6EPHt5rKichbRoGlTycmehx+Th3o7fyS7lnrCT4ZXS1gA== +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +NmkgFF4ZsyEHu9KVT53La1lz7XNNET0A9zdhd9gFT9fBSLnem4K6iQOWTy9x8XwI +topgAunkM89n84cH9D9pHITAncWu+e82byWKRCcayzEuvdX6re3IAygjiK0dtvwH5pfePRD6LD7p9ALfCp41GA== +1u+XjG/2+GSQRv6EzCaWRQ== +NmkgFF4ZsyEHu9KVT53La1eD1XgGKKkTpXLEHn6AsToySvZkHOsdmdS0PUJccZzo +sVk6QS4Pf3ntiMkl4LqRRdNSXsQEDaTnzm0yHVvLcNydLD7JFUCuBLvtEtInfM9cLHKhemuIRU5S45GYI+zKfw== +1u+XjG/2+GSQRv6EzCaWRQ== +JkdoXgqzqMYpkKh82pPju9o4/urRwxg/lFTkhxBEG5hgOJ5vGO/GPYvjcLXE248o1JQGij+4m8UEtIcbHbGSRRVhEZVih+RN/xjwceaGFpM= +mgV9MrvUK0fssdmv3ApVy7k8mJdR/B+7eG8ccjV2Ab788JJLQjLAWgAvu87PYMIxrNY3ii6KXngsif22LUSmMDbdoMq8P1ubKINGjlYCA7gTuIGpBMZsNYzFMa5NWPLopAgMwMESku+TbkFwChLHaw== +K9fxAkAAgWE70Il8AJdd4rdBcU0my9+uNHCVuDkF28NZN3ATTy62g9PIqSFIMzetO7VcoZ3+2dLF7JWJz2PwmrKOhy7hLolj7vwIWSqWgW1E/24aK4BeKO/DZ6lIwCW2E0Ly26J/TZrEe4nSAsvnhCF2/Sb4A2swT7NQC9kADyg= +L0eUthVnpkGsmKFAX6d+uCiYQic7T+3704A5xRjtHHV7D1K7sA7PU5Lx63H+wuEdqWGBURjxs/fUqKDjPX8aHg== +NmkgFF4ZsyEHu9KVT53La25kspERjj3Kd1WtR5F3DjUeyufiu9OPGHQHCZOJDZd/ +L0eUthVnpkGsmKFAX6d+uCiYQic7T+3704A5xRjtHHULvPeJ8pPVUmfUVgaJGNCIp4sjsG53ijiz9EipY86UOQ== +lGY+HGJ/DlLufZHWWlj7U/wgW3T0y9TOw/q89uQqE+c= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +WjK+gflGKYbL+4lozgCawFxAQnjo2U7M7PxwwYRzZjsaB1ggdhAibLBZPcfdEwOx2KWulak8d6g87sjhm9UYeQ== +Z8UsPk1Q7HtwjRd4g01ryw== +DzneBA92dU5V88ETMYFQ57uX0+Soy2svl62wHi59Ele/p/Fr3qr6/65PhVKxF6Ph +bIts5UhOLbX+5vrkUi2fEuV05t7kiBXke4QQt4fGEIi1pyM37lJMRHRDxQmK3gOZ +Tn3rOfE+s6NRy9yVv10cExsGK3PlpvQ/21Omt6hqVOjP06IQhzjsmDO6W8MidErE +eeFiPlXUI0Fos82HYztSmXW23eSyH2bXlOYpYhyb1RKLhaGx2IoCJa24JWsHrcMAg8yR7zX1oWHc14AqlOrK0g== +9BuKX7c+cBUFGH/xEQqKwAu6MbJNm8+b4qD0aAxmgDU= +Z8UsPk1Q7HtwjRd4g01ryw== +Wd7OXFQxn2HMHBqE448Pi6S8+/GBA34d3QZMmVea2Tk= +LxCLgek/xo8kXqFx9hyUVJPL3TFbh8u2ikhOGSAV0rg85G9Y5wYNCPljvLZlShFM +topgAunkM89n84cH9D9pHIV4dJ5fXtVEdOnz8/oeQYqujphF6DcnuFFxsQW+tGqMts90IYJNxLKB0izTwoEhvQ== +MzoJ1HANqAion0JXNkOKLkUXCYWzU8bckEHdk0zTAVCxXpXc/N2/2C4s2HOzit+j8Cu9wnFj9h55D4ASfhbhaA== +topgAunkM89n84cH9D9pHIjK3O9J9W+DroJme0vc0dxLsI4ZMqq+7wxYZZro3x0X +T8ztCulMhN50eAMwonrN+mnIYb5fDs3vnEBct26rKaEcORGfRo9zgegCK5TefOmJQqk4fO89rFCV3QP2Wzahfg== +bo0XlOt+6S78LT3IsBdT1dEroBAtDg8pr1Pybq8zhQJAu/VoI80oqogQomMUUlPM +v9uxr4HcPPk5UqMUuJoEoB7nY2X+tzb9PZyPToR8I6vD2vPuybQkfI0RMQ5SpdHZ +topgAunkM89n84cH9D9pHLhIfpf4IEOslMiXh31h17WTVnRZP8H/e9ND9t9DHG3EOcgqF1/s/qPfl2fGKEz3rA== +1u+XjG/2+GSQRv6EzCaWRQ== +Wd7OXFQxn2HMHBqE448Pi+XpPmaTTyeqpJxLnY2yRin/EZIgzqJ3CViWAhoEpU6erObfPycKdvcVekGvCE4Fyw== +lGY+HGJ/DlLufZHWWlj7U/wgW3T0y9TOw/q89uQqE+c= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +xzdWmbiM4p5/aGFV8YqP4XLLyrbPzLln0rwZDoXcnKc= +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P4+OZYX1oZ6IDO9Bb8L9jOTF/Bt2J4quQTw6RN85Bumk +Ys4bozvJBgtB7q16qcvtB0RluR7RwJXaqe7bJfwI9hU= +KZTmaJLp+FU9X93j5Tpqtw== +eW+3TRHX+r8wYGmvO/rbmQKRB+qqZDGER9PE45Hh6DhKWFPeFSKD/CBlJ+rZhJXE/6NzadTbUrA2WD4xQ/NtZg== +eW+3TRHX+r8wYGmvO/rbmQA+K0RVUGWoC1b+gP2mkFaXSKIZdVNEI2dyM5Nc324r +m00MPwDBB9sb6xb6db18s1+VrxrV3/MYAhac4NSUZUk= +4lm0ybc3aVxLaNQODc9aBcT0vlFE00Wx+6lONISyLb4= +iUEDlyYF71b358pIL4FKLxKByAvC7nSDmBVkTcIWC+9RPGSBJZRzJ2JFx9aDM6YT +zxz/Z9yjFalbpeH0OEk06wb+y3G26TwFK0BePbfiFxswJa+xbCabv6iKd6mKX8sM +zrSvdYautDwgfoprAQtW7Mh3DupVdGrOOgUnOp6J1n0= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +WjK+gflGKYbL+4lozgCawHo0YqPGy4jvJqzaJIsqw8aht/a5La2JHCiBs7MPYkJNnkttfrNlz3AZpwB+QDSF06CacZvErF4UHWUUepAZDW4= +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P5s4ZWJ5nCZ67hdzagwPZfnK+vAzOOBUPTQOeTz/eFRP +yqM179SzW9HnBHozkU+IlS7E9Jat0KMy3qoE1n5uiMAJ7/ZYgCvtzu/Qi+/1mAim +NjU4zQ5DRyOHitK6CHLOgMz13P9khDg6jBdTbmByKXOddMHKhILqXmmCVMC8sS3X +NjU4zQ5DRyOHitK6CHLOgKqZU0dP/fC3Nw6diKT/uI3N4y0eHljA4CIE4JRl3a/I +yqM179SzW9HnBHozkU+IlZ+sLT0JkWbe6QF8HumAIuHDqd+ZqJq4nJMu+r1QY06/ +Z8UsPk1Q7HtwjRd4g01ryw== +XEKtPwhadR+kpKQN/+UTaW/KEaZ8xQsb0J4R4+wy+qw= +F3HmfyKnf+xR8iqi6kYc0jeDpsR33ZKtlDiexNRZYLA= +3hCm6yDF8QSQ/yEbAtwtdmUoSfAgsymzLlnjfiQPPgQUC7w3DY9Wc7Bn+2+99jPC +6P8mKfouOK1hwXy3MrrCzRBPdWeF7NFSQT8rGlPCjGB/+XWHqqTbqSpG6ZavymMALgzEpY4QOg7MZRCal+ePJw== +1D18KWr2hdVsBcdZx1OaPddn9pCR79IrJK89K1q817H5A2Tia2JPGCrrHgOkp8RV +VmVrGQo2zRokW/ZuO9bN67r54/S5K8MNluBiVrjXDNs= +VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM= +MzOj4ZiBOJDNVP8vi/lFQDc/aaaueRS943aPBbBVTruOJ0ScKqVF1VjeXVd3nD3y +Znf5RlJ6zVewwmUeSxfIGsO2yacXhB2H+eI6Ejvle2oa4C44s8dSYzlT06bYrJ7jLO6BjHZXLU70mL78XNfc/MgsennnOw8awpTQyz9lgKi9M4NOYpOpkfu9y2pjpDXxj1+2/9JZ5FGVbAjvUWVbI+MnYHew7WdlkltXdIcw7ds= +c0jePRxtTVZYop75Q5JCFsHjToEqjqqXfvyUun7oxD4= +L0eUthVnpkGsmKFAX6d+uHvX7jXsjikZBMo/rKrjiBw= +1u+XjG/2+GSQRv6EzCaWRQ== +RpR4QysGv9QPcAuUlmZB/ldqYJzcdfQBNZ1Th5SM6QO+POEY2DGleYbCe5l6UfNu +yqOVJCkAGOhqcQYBOo7yqQoTxFeI3v/YH5mpkidrW9HyNTAJRD02Ymur9pCntQEY +6P8mKfouOK1hwXy3MrrCzYZqDmfnLEVSCjSn5KIdRmc4dkXAA/1uvzDl3l1PUWtt +PRG/YRzXVD8iVR3bzEcUnllZ+cz9udLBY+J74ckAM+cZ2hSzyyfq/FybzwKXxMpO +1u+XjG/2+GSQRv6EzCaWRQ== +YnP/gQVEpWES+RQ7J7RJRlUivABjbdmquJjPlRdBH7s= +Wd7OXFQxn2HMHBqE448Pi6S8+/GBA34d3QZMmVea2Tk= +p6i8iK7HDbvmLpoFrhNyv9D601DxXdSOTjmgiyd3Nqr1byx7HtjSclWDwuzUoQV7 +m00MPwDBB9sb6xb6db18swVJnbf7H4WMvnIczhPQZ+0aVjp7owaSvIW84fWof7uc +1u+XjG/2+GSQRv6EzCaWRQ== +m5EI3+BXxkQEYKYaxORD+g/7lNafasyk405EhIyuvnNqv6n1h2jC986RGfvXjJkv +50HSHdrYmraajnW93vvabzwR6Glv3j7i9vFV1sPcZ7KgQtnqa8GX9KUqxMUlXQ3m +1u+XjG/2+GSQRv6EzCaWRQ== +gZ7BoIAABc+I8g/rYzq+f81Dacxy6eezCrTC1ekZwPEW9OSpvgIrYa0KGCOwwYXcTIWHKwChc+OzuGx8c8CYxqz7F329o74YYNCcaYwSM9V4eKMtsG8h6eORGPf5+1XV +VmVrGQo2zRokW/ZuO9bN66Yu9w4eCdqZ8p4k0NS0g/mr6t4fHQqW9//3beDBilCZmROWIZ1UbGT0bwXRWX+ibj7PTXWgbRivwGWChIIkZpWmZxWQE8b/GcSOusQIDbAt +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1u+XjG/2+GSQRv6EzCaWRQ== +iUEDlyYF71b358pIL4FKLzQOhNOG5YF9nS6QdrhVDB90W++YzeWFyqsFYbjeCsHl +3puL9F6TU32sbZzfbgCef8z8OvYCxvmTlQzeetx7rGA= +HiVf4YuHR4gw4rFUUeRvKw8qvbDmX+tfMbpiIaNfsno= +gZ7BoIAABc+I8g/rYzq+f+9HHgvBGOxBt37ysBPwI13uPk52FI6uvFjvWgHf7Jko6NUiSbwIYvUdo44a+xTir+OvHDqhK3py8D4bbLHhU7E= +1u+XjG/2+GSQRv6EzCaWRQ== +uWQGv+LShBH8u8S26pv5nPnM8ff4ZGji1vcSAAFUnfWxWXseAysF38MM+4hJ5dUZiFZbZax0zk7JjR3dSb08FV21UNIPkNYIE3W2R6DJCOc= +ltIZHHUgft++nMUXBsBki6T1wXBeAb/L2jS/24mdBRE= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69uRnj8/r8BaXEMGSBfrjESJouhp3OTlYGDThMnU5TB6K0SkOsqpVqbZDNGFz5szCw/AgnLXsS7/vstntyyZyJXLi5GL2bMqOI9UfzQHPSFIzK98I+cwwynqIM0yyc5o2IXjcbGmabsIRO4LkX3gb0k= +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvOMI4vw48TaUq4aRLYrmsXaJb+h7/y2WxDdspRfKEXnQw== +VmVrGQo2zRokW/ZuO9bN6/jFu99KJ+MquaMXJxls+pzVuGNN+Sl79RauAvg76B1wsGLU4dxJTOlUT9dMZ1w1ug== +VmVrGQo2zRokW/ZuO9bN60SggNWucpg/S6RxW0/YnaA1skwCA0ZpXLeYREWmU+BS +1u+XjG/2+GSQRv6EzCaWRQ== +c3KN//OVINDLfTVXAO/Lbk2Hd5TU1RVmBdDgvw8QHupIMpn754UzwYg885IkGmJJnOM8kM3rDIg7hTLxdJmlTw== +gZ7BoIAABc+I8g/rYzq+f+9HHgvBGOxBt37ysBPwI135+k2vlF9wrhYiCZmHN8IE +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjYKmRtsBhPWippExvZOW0/LJEaf8OI42VUCkcmkYiDNBmW4Tnaa50/2shkMsiFA/ejpSEFIqAkBQ53j0BA08zWZ +VmVrGQo2zRokW/ZuO9bN69z/Qlonskd1/GLdnc2k+VvhZQyzqI/fgAtkLK8iM1Hn +1u+XjG/2+GSQRv6EzCaWRQ== +jGJZZqYXcDE83UImRHGU4lffa63XK0BuEvGrV/OrPdOAgULF7Tuf5t89bW0q2hMA +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvOvKbZ3tEv+5Q+mTyYmHi9v32uKyu6CDih1wbEzENWW8Q== +VmVrGQo2zRokW/ZuO9bN63K0zvLacibwYj8m7wsfeqyyOyT4etluzfxFxaI5NIERaBBVwUw/TC8TfLKh0did/A== +VmVrGQo2zRokW/ZuO9bN62BySiMpDH9w/XNtJREDLzDV1N7g8dL9ueDFUmh6GeUDDFVqtUWhLZ39rABIECio6GXmpVIpQAKy8dcZ2qCvhpg= +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +VmVrGQo2zRokW/ZuO9bN6+g5JNrv8rEbMLqUNj28Cdo= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69C6A2K67LgD1eJvP3N6AWwUURoCkGGIg7SORUpL6Seh8a2ykGijyqDWzo86U586lwaKy2+VvF67QGk3R/UXPiXiVzWtIisu9t+GqCz0r6aF +VmVrGQo2zRokW/ZuO9bN6xigJm8qa4WpsQUsklfpWpWMUosdOYpkkLOs84FK2jeGjVjPCXRS43orE0WYC4dCfwQQxhdDG3yLsvZlftXvV0o= +VmVrGQo2zRokW/ZuO9bN63a0R/S59cuVV4xXEauJnWRz3xaZ2HgPpWq/a72oFYZVDKhEoXsn2iHq60nHoc8QreeySx+sbJT56yXV0YT5mB8= +VmVrGQo2zRokW/ZuO9bN6z6oXkNlIbiWTEz21chkjli+NoZjhxN9ORJwLCGQEnpn +VmVrGQo2zRokW/ZuO9bN62SJgGjT/WmMIIxZ/ruMwWWisTQI+r0CB+ZPSVt0T7SS +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6/x9mVEQ39E9CmRqJ+FlnL9EqemW9t23C9OdbGoCQ8cM +VmVrGQo2zRokW/ZuO9bN64w6dpups4cXvLDqooOrnJyvr6Av+x6T4G801ox+KJHC +VmVrGQo2zRokW/ZuO9bN60tueB5yS1mXpf65ThW2BRcr02wDxH+Ue+yMjEpkVNPJk/hP4oWqVTQE7JZlCIIrmA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69Hh4k/KN5p4WHNMxzu2tx2JIw6LXMR5NzQxKIVS864yY10isQvmwGVQ2DlSTPoctMbfzw5uVWHhvpOBeLt9pKI= +VmVrGQo2zRokW/ZuO9bN6wJmwpIiDUqk/W5Xvci/WyF4XF2UWEAYoz09NC+em7kI +VmVrGQo2zRokW/ZuO9bN6+LmY8Wp+WHHBK/N/eg16Q15+pIZBGb6sScWJhvIma2k +VmVrGQo2zRokW/ZuO9bN62L/LKqacP9lunLs/sZVgAVfljLX6fxmEJSvrO4zXddcQ4NElU+FCAcEPT2EZ880Cw== +VmVrGQo2zRokW/ZuO9bN66YYIRANoQmJRNKXv2xg3pCaJdGg4CQVst48mAlOjitX +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN631Ij+DuOT66h6R4m3bVN3TwDZnJGvuPIa0DLVLfOxX/JTIYUqYRpbQ52ElbP7VAkg== +VmVrGQo2zRokW/ZuO9bN66PmE0xfRAboNx2TeC9P7J4NhzSV+m0EkNzefjR+mpgN +VmVrGQo2zRokW/ZuO9bN69qqX19RJUeMAQ5O8TuJCvc= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6y49h0FPe0mXa/MRz9rhxplV7doJhsM1E7TfB0kuuQSh +VmVrGQo2zRokW/ZuO9bN6xwhOaVQWDpfnMF6LA0XCFstMJg50MMXLnnkQUEa/wNn +VmVrGQo2zRokW/ZuO9bN68sDl/nq5E1fo4CRxhlKiT6ewilZZCn5nRtj5+srI5B2 +VmVrGQo2zRokW/ZuO9bN6/TV8HYrXfBS0+5RislOy68JgSfzi3n359kKXZzzkBVKkRF/lY4XEsYndx7+YjTP0jz72pF5WNcrFfW3cbITPNg= +VmVrGQo2zRokW/ZuO9bN619JyKoLhHxa0i2TABYxWfIzBj4xXUmyUuFPMVlHNfolzXARazZQnnKzxz8Qgx2TFd/VDzNwQ8p8VUgiV3DidQAni8mtUG7OUvdH9rqfLaPy +VmVrGQo2zRokW/ZuO9bN6x5O5549z42ZbPZdzwy2tzg5ykzCAsDBEFgAKnqr/bsWy+wofZoh6groBJA7PTdK/0btaorXdA/sR+o+7A1E5hLNkjWbWomUfcFC39spL93x +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkllVUyblmrBXx4KTxlxVkh4bcW2se2uqmrpRH3UsBE2hQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklScjVEbWucpHFf4jnfOHB52z9dx42c56L6XC4wp2g2ruIrt2feNZhOUsfn6mmpqAU= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklOzDQU2UmHOWsnNbrLjDmizGDPlPu+SoalSQx/sN2EQIT/3N3nh5LNd2fZegmgZDo= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklS6OX30XitSf1zWXDoSBdzuvFrWI1gEiyE0eWnthSNpA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkngkTGQ8tITagcno1If0T633KRDqrO43O4ckFcdiv4j57M7qt2UPUTmNG9R+ArOTMhYo2cTmli5bm6RYaKnxaZE +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkkyr3u3tmRRdTjbXFN3bPPu9wRFkGc8MqoonARkL67arQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkngkTGQ8tITagcno1If0T633KRDqrO43O4ckFcdiv4j57M7qt2UPUTmNG9R+ArOTMhYo2cTmli5bm6RYaKnxaZE +VmVrGQo2zRokW/ZuO9bN61TKHwbNGf/2Ub0vd756U9O0Gtl0vapnf7HpdYx4U2ib +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklEcvH4r4MfmtfJUGH+dpI5pxEL8+TV2kZl/s7yEkRSBkV6R2mDWz6QJPXIhWnq/4s= +1u+XjG/2+GSQRv6EzCaWRQ== +lGY+HGJ/DlLufZHWWlj7U0N/DF45+V15HT5Nu+KAqWE= +X/N4cXLQiqE5H/UsPg7N6H39QsvvRJt1vu7ZPXzjPbs= +WHkOzVx7seuLmxs5Hu+l/mrLDQ6BBXRseSNCn19UXP3v5lC2O2w45xLgjCqNwLEY +EiP/K054tbB05pkitbcV38EFfoKmw9zT8E8ybvzd9nORsP53TL7HxHzZVMv+d/I4CDSAjMP4VR6rzvujLxO+Zg== +EiP/K054tbB05pkitbcV3wfMjZ/GVvL48TFa+UH0AR0= +8/sla5spc9gZfiuUDEb2iHAhv7KgE/gh3O5zT4rZATU= +yTSwdlYSxlm840GOKG0jBNReEOgHRyBZMZj7qfJDVM8= +wlLHv6kT3Q/RmtMBN4nDAa1kCyeDPbBUXj1x4murSfY= +VmVrGQo2zRokW/ZuO9bN67tOtPUCVHilnHLpGSFH8/lE0zEZ1of6jPE4GNF1YsreLF88QhiYDrDpqY0GhYtYZg== +VmVrGQo2zRokW/ZuO9bN6wws8n1WvlPG4f5+ctB/4Wkw73gLVVi46HbD4YepM87BK9AtBo3X96VBGJabZEJGcWnaGrOIgSVP+1G9XJuOItw4Z3CBoIUDDL57ry88UL0Y +VmVrGQo2zRokW/ZuO9bN67tOtPUCVHilnHLpGSFH8/kVcZoulWP34aHJD6jlKFoM +VmVrGQo2zRokW/ZuO9bN62iPo1XY0vYrVL4PRMJ5H6bWKRzMN2ZPY8rDUIAMybuc +VmVrGQo2zRokW/ZuO9bN6zE8q/SdlXIvI0x0m8La72coiNnSoO6pKX5F0hOobfV+bRuzyAeOWi4FR3XbqMUOYA== +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN65sE0dfavTgZrT8VHJw1CbtVGdXJNjwPzVktduaNysJ/ +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +oJQSCA4LmnTse1ofrS7jTxSxJllCPFpdE5ycy6XFstzmvMPRParED6juI7yImDCRQ2mIYnHQP22rwfersmNpHiTQorKYISnL1hv5Snt1uxMyhP/Dkw3FjUFnb3ozCCH5ti6v+RsOhn/keCLsS+Rsvg== +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +aH8osMxZgIfRkNShGiaQ8KeostDT+ZPsWsXljTu4fBJd1fM2igANyxWLADUf8Q/PaQu53yzgmI+X5rE72QX5HA== +Z8UsPk1Q7HtwjRd4g01ryw== +8+pJybctMsfvqVW7hafi21j617S4ZJBcnVSvovNJf2fofEHOzMbs09NxooOPyRza +wOl7lRII4ZgqRTj9M//0b3Na+XUNXsGthFHJZn+iwk9DLm8ehZxPVZsuck2aDMbz +FFgrrvXvjrcz4wZcb8tZVBfFxJJtUNj66FBgYGeUBuNe1QYojsfdMMTfmUWbDp4P +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +0yVj5tN/frSXacyX2PgyeT5E+HbZCBESO3Xe8mtOrLxt4iljR9cXdbATNOKhHq7wMxj1yRwJ7OaljkHeJlnzK3LEcWydZBw++GRPav2xpkPrnDffgdhIqRhLBkni7ydMmI2eX/TuJRps99MKfJtUAA== +1u+XjG/2+GSQRv6EzCaWRQ== +PWLW/WTodw3tqQQSBBHInw4g49GUi/PQoXrJnMOKMcv3Wp6VztQYqiLzaEcDdgSs +gZ7BoIAABc+I8g/rYzq+f/E+IDZ6NsXsUDbHValdfzb/Kruq/Vjk28atdn7C+Siu +VmVrGQo2zRokW/ZuO9bN6/8yGXI8x1DvWcxmMbVGl2/d+UnadBjumrSvrXqCFvxL/dQ1IF3GPQDGm1Za9My9dqpvvZxqoUl6zH2mT03wugk= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66QEbsrD511ragqq/ntAN9ip4Cl7MQ5ucFmM7oBrE7CwtFofMzsbhBUBYHWjnO4+Gm4RaVlUCL2nZMQ4ZlUMHk4= +gwLNi9yZl3Cgb1vmhIukcDiwvybm4po7xCREzBIXUUWMhzq1uZtLMFENJQeoM6bf +gZ7BoIAABc+I8g/rYzq+f/E+IDZ6NsXsUDbHValdfzb/Kruq/Vjk28atdn7C+Siu +VmVrGQo2zRokW/ZuO9bN6/8yGXI8x1DvWcxmMbVGl2/d+UnadBjumrSvrXqCFvxL05RBhHMkSHCThcMnvcrtBL755t4Y28qLxX95H9SOw5MIMfwwm+AX7jFrLSlPFL7z/LG23Z7Qi0ZBkt5yi3J065WfKpBp7ljnujnyRI5KxS3WLULIWKJfkqhy13iwfEXTWU5aH1Cdj3ES1NC50RTweA== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66QEbsrD511ragqq/ntAN9gn6I8xS/ec2ORcznjf8NbtWmOguhtVlMEA8/tuxJYlc+EcZYOpOz/OOJi7ykbx5GuqFl+UVDS0tpV5JPDnbAvDoPyg5AP415q+Nhiu9uo2uVlZvJcu7WWtFPCoNTx+1lGiW3wClWt7AZYCeza9v3va +VmVrGQo2zRokW/ZuO9bN63oVhYTQuQYZZogmVe/TzMw= +l2FJPs4YkAmmok1ulDRuSA== +gZ7BoIAABc+I8g/rYzq+f/E+IDZ6NsXsUDbHValdfzb/Kruq/Vjk28atdn7C+Siu +VmVrGQo2zRokW/ZuO9bN6/8yGXI8x1DvWcxmMbVGl2/d+UnadBjumrSvrXqCFvxL05RBhHMkSHCThcMnvcrtBL755t4Y28qLxX95H9SOw5NBHzoK2EBm81zr5nBdZfIqY9xIQsE1T6+1rIBc9RtxWrAgPHnUSyFwCGVIYz6lZga1UtCe6E+ND5HnA71abf4+8Mx/rNnpalgInue9MBDnlQ== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66QEbsrD511ragqq/ntAN9gn6I8xS/ec2ORcznjf8Nbtv6k9SWAza517BnGnZCkv2xN0WdZ4ZLw0aPWE+30+q6fOg28LzTGXOUhu+Ve6HvJaj4cxoQ4JYcacpAzNMdlTCWLZM+Uptv/BbJhem6UisCnkHFC8acpX/dMFcmgzOKuIaIG9LX5bT7btboBMh7V/7w== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYmirComgoMouw9fWv+9wQLbQn3IxZ0+fHgSwwrVEz19D +lOh2GtzHjjMM8E9J40AuOWo7iSH0P5z8Y9QjLJCJBGiaE95pK7CRTqvzzc1f82PP +xWoGNWjKGPfI4gq8aHoTfMo8UWPcZxcLStbRn3vuZvrvRuO8gwYogoCldXZi6VGk +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhOcA9ux5Auf73oocslB7sDeWu0fDEoiafZo2yMYtO6chT1HFJerygYO/wT5DIOkSKA== +Z8UsPk1Q7HtwjRd4g01ryw== +JLWwEPnWK0L+CzhaEIjbQU4GwWA558QpsjOq+5nz4DgxhiCXwzz0jGRGKxh8dUTODJr+s6+Ah0R9doWb6zPLQQ== +L5bpcfZyn5Pc75WQ8ineZfusv0+lxTTZjtC4b06AF3zKUPd2R/NTwAVMY8WVnZ7X +EhfxP26BarXubxBeyusn3NfNOtb3HPBGFTHMHZMXSqvtCtihhBnDLGI6VsAxQd4V +Z8UsPk1Q7HtwjRd4g01ryw== +uTEK8Ng11d3ix2pA+DD/aXy+DXgD07P/dsANZ49KBIoQ9DDyAcW8u95okz+vDaVb +1u+XjG/2+GSQRv6EzCaWRQ== +fE3zmgbfhO9ZwEda5GgwD/RsUOXfY9pzpW9xRZl2fW0PbW4wjRnTUOggbHQB//acwA0Dv9aDYg0PNWy2YecNQfNF05hJabP1q+/C4qoAENQ= +lOh2GtzHjjMM8E9J40AuOZ7zZZM0vaCjhH3uRcP/NrM= +pVMCrZ7PAAHymZ70WROm/67KCrp/ppOHMaNTwq4h7pocNe5jswE5ov4hxkNlgfBC +VmVrGQo2zRokW/ZuO9bN63ZBl+ZLQVwzJ8aZTH8bazNsMnfZ+bm+E1rvhx3AfI2H +1u+XjG/2+GSQRv6EzCaWRQ== +CmFznH7ArXzhNsFBIGOkr5Vsg/ycHXuLeeSShlENgnA= +UbJuac0dxLiN+5ocS3w2vfHOkOj7T66O79wy+D+TgtoNfPdzdywGX8tpg28oiCYnv83pW/YxuQwN1apN//aLdw== +1u+XjG/2+GSQRv6EzCaWRQ== +ACEEdgL/kNbx7RaZcSRxZENcLifoZmLdDLAaOT7aBW7RjTwy4Q7KIvPTg8m/9qBC +VmVrGQo2zRokW/ZuO9bN62YaWBnXKkSoB0qO9pLfxSSPe4WYW+NQbuQnofC6bhb9 +J1UGlXkKhKXD36OrXXhN8PbGS4CCkC/QcSnejunC4s1HNXfKIR7pGHCoZHkqc2al9dG3/LRVuqxK1/lHGulhpw== +VmVrGQo2zRokW/ZuO9bN6/d2CLxxt41IFX+A1fbH83na68t62fa3cjMWGKFnkaMR +L0eUthVnpkGsmKFAX6d+uPmf6IqlOWYOMRLeTkcrllRN4/sFJgvvL5z1UEeHE8+W +VMfVrQporTLzVAs+KagENgnYKzmIrs4+BBVOAKxaFac= +1u+XjG/2+GSQRv6EzCaWRQ== +rKj1u6Uot58r2z9vVGpkYPs6jYzSiEQNQ0Vlqh8zH1eUMBI5lpqMlLhl/4lO3nQu +Z8UsPk1Q7HtwjRd4g01ryw== +ewqdfdhwKtr+fTAdDLdXxNoKltdD5CW97QCrf+yOioE= +Sc8e9jvbHiy5hwiy0HtAoOvGcehWBaW/6Q54bzic4ooH1vds49RieexSGCVDRfXT +1B9Eo1v10Ho41P86Qsxs+Rf3h6kOJxgKbqyJXd2SQDCZaqaZocZ40EQpaNjgDGb2 +Z8UsPk1Q7HtwjRd4g01ryw== +NmkgFF4ZsyEHu9KVT53La24afMe7Hpz+AvRb0pPLT5Y+DtSbSZQ9wXQJ9PdlRpbh +ACEEdgL/kNbx7RaZcSRxZDbs+G2PAEacg2XavjihjHkGDr+yC4TGqKna/D6MU25t +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +wlLHv6kT3Q/RmtMBN4nDAcHzCoHA7fx8ge824KyD6j2RGkLqKD0SoqmJdGVHetKh +VmVrGQo2zRokW/ZuO9bN6za89G+hm2DHEo1o7nC89kWHww92mvNp+cT0E6zNNzmXwifWbhsAF7H7xfRYH1Tcsg== +VmVrGQo2zRokW/ZuO9bN69h0W3AGoL6Fh678416+Hu2KoilysLPFqxJJbiCWg0kt +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +l2FJPs4YkAmmok1ulDRuSA== +gZ7BoIAABc+I8g/rYzq+f+vaPyTBeRc/jgIGrjGoV1I= +VmVrGQo2zRokW/ZuO9bN64Vu/H9alPnu3iLUUUBoSK+kyhoBM510sPwyNCX8Yr+ofuNO9OIf8TJMlD7EUz7RELcHLIPBGPTTiECymRVomBM= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +vF0ZftWrAGfdsoFfHqmHDJuEsO4R/FX2ga77ewUw7Y11IGYw63nwjmoZNzNj628o +Z8UsPk1Q7HtwjRd4g01ryw== +GlXLieqbP3oJrHHHl1oqk5GfuHjv+M1uY5kQiwNKOz0= +cCPEDb5F9Qd4JMGAsR7UfZfXQMkZOQ9q6AipKLgN9qA= +wzhSc3VGUoAQ+07oznSZPLPT5vU/alKRgCd2xATHoIdgjG3VPCXSpthiTEqckgdX +Z8UsPk1Q7HtwjRd4g01ryw== +rLwQ5yQ9bNuG44la3oQ+3afvJa6cgkEkDaPubWhiBa8= +2/eGB4mNeTC2Lw/I1/B9jHReQG3Sf6fhE0IH64g/YfeE/Ws6fbXaJX/LLa/JASMj +9L3jIB734YKv4S9AqEY35Kh1jnwS0FJtLaDJkqWU7Zs= +J1UGlXkKhKXD36OrXXhN8A/6KQQIEyEAY+cUx85urfskVuM/oTrnEBFsI3zXMuwQtyvGFxwsnrTAlyx/0hytcw== +VmVrGQo2zRokW/ZuO9bN68dKk9G97rhJCB3VEv0S3qjWifbCFeCN1sSryrl3vtTYdLbCLZieWWRVdoajo7IOpA== +VmVrGQo2zRokW/ZuO9bN69r44+V6JJX/2NLSGlXYr1YD313HJ7qeRebm2/AdMhL11t96l/ZN3bscCjY1669x54LQrad8RoESQEFGNJ7tr8I= +VmVrGQo2zRokW/ZuO9bN62MvivPt6ucjDY6Y4UwXjRGX+MgrdU7vOEwe6NO1KSXK +VmVrGQo2zRokW/ZuO9bN60Sd7dFtgik0J339YqGp+LqOWzrYRhuwK3n/DwPxquLs+N36KFtUHhWKI9VFqVraYlnKaqKXuvwu19LED1GmG2Kkna5Ve0z1QySEyTbnYwpj3mq3A5LUCr6VTgwESp9FYrceQzV/js0BIa16dwTapHE06PsyAeRntvguLENP7jvaYu2A79wYGT81tc7XklvAEdronju9kPx+7a+fzFUwV5U= +VmVrGQo2zRokW/ZuO9bN62MvivPt6ucjDY6Y4UwXjRGX+MgrdU7vOEwe6NO1KSXK +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN69r44+V6JJX/2NLSGlXYr1Z5XOqt9InPZIx/uA7fZePS +VmVrGQo2zRokW/ZuO9bN62MvivPt6ucjDY6Y4UwXjRGX+MgrdU7vOEwe6NO1KSXK +1u+XjG/2+GSQRv6EzCaWRQ== +sVwPh1y7MInnwEDK61kL2ZgurqcOtElG452IbPDSARaEvm4o7F6Vljqg3s4Vb98N +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +WjK+gflGKYbL+4lozgCawEfi7RDXlvnIiPUmGf6XyPurYR5IRGH6Jhoi1u8dhC6C +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY3e5Z5xEtbMc4JrlUL6rftaZUmLPHCH16nEnJiDRycKT +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQG9CJ/mCl9wdy9aVT0qKaa8 +7JQQDTi0T2idhFjao4jEpdluONpwOUUVh7y/o1Lzsm4a2FL3SufObmrWF3NyQ11G7uUUREDf5s1H4nVGAyEIiQ== +h2Amc56dmaRo3inGZxN9N0Mh3da6J2tf8CU4p/n8Ca2A7uHSizxBYlbR4VDvVrH8 +KZTmaJLp+FU9X93j5Tpqtw== +W08jtqcK2gqkconJfg+WHLJlYDaZnYglU+gPmeaeJz0dXPQc4GmpJ18lTWeF8Jvc +IsslvnYVqGkrqD0qU5QGYf5J/O0JjEzsRDI+7c68kAPbhQeof0lnZloVKmPJ0/OPwERg1AgDmZ5jUeNQ6jfaldScBwiDIsOdWtI9+ChcDxI= +L0eUthVnpkGsmKFAX6d+uK14dMm1+GhG8c2gpiVyR+E1Z2/jkCwp6Os25PRP8RSk +IsslvnYVqGkrqD0qU5QGYbCFSj6VHiK8jGEp4Z7SGzCVaeuBljBtpL3ckAqxImzQ1d5MvnFkYYWoGsfPJ/WPv8swi2RH+jUjIGOKkl3usaE= +L0eUthVnpkGsmKFAX6d+uH51QgM5kWHywxiE6Ty0F6BCQoJXs5zaJJqYGuXoD81B +IsslvnYVqGkrqD0qU5QGYcjXjwdbef/ruiHWGoRx3+tWAsJVYvcBpbkV5AJPt5MOcPZrXMgXPIOVr93qjXogTtDc9vraJd/N0pYe5+Z4eNM= +L0eUthVnpkGsmKFAX6d+uM+i6uIzzgNIsrvcJ0xQsTE06WRnqMHo4Q1g/bkpbg6d +IsslvnYVqGkrqD0qU5QGYbP084sfOENgLhN/XMGReZg0zyyRimxRdtJtSm6Sqdya +L0eUthVnpkGsmKFAX6d+uDQtcaiDMT/if2pHlZtAF6w7EuP6p/BouaL69QDJbMlX +IsslvnYVqGkrqD0qU5QGYbrfmZ+lxyGmJzabndV+bycc7U0Wl5SH5mS4H3CFQFF5 +L0eUthVnpkGsmKFAX6d+uEpYxmDfNJv/38PnR3IuvUzG7TueShUPZjSOSr1mkV90 +IsslvnYVqGkrqD0qU5QGYdNGxot71haXjS99OqNqqQ074sGxXHCxg0T7gUwpV061 +L0eUthVnpkGsmKFAX6d+uBEBFHibxFuwHX5Xun9xifvXFUxIe1hqpBHRh1Lw72g9 +IsslvnYVqGkrqD0qU5QGYacx+GIk+O8p/nDetEHOBxNO8G66dVdD3XUsN1D4L+DX +L0eUthVnpkGsmKFAX6d+uDKFjFlH66scjoKkr0HS3PwzqDQpIUplT5Z0WdNifCR8 +IsslvnYVqGkrqD0qU5QGYTZbaTDAr7WzW3U05y7JjCJE+JWxxlyG4/oGHZQRzxpM +L0eUthVnpkGsmKFAX6d+uKR3GJL+5DYQD9ef2JvARGDOf7TUitZF3uJDvhf4SXd6 +IsslvnYVqGkrqD0qU5QGYT4PypAZWVuTa3yx/tngA8BLwrQemYJ/HPN/7QEqxYRG +L0eUthVnpkGsmKFAX6d+uOhvQuK51ItTe89peTAEMXOQ5smjp/DG2mf1guds/omG +IsslvnYVqGkrqD0qU5QGYRnGdaaoC1M1g7ZAadH1EZcM7x8K1ujx/VKQzwSiV3gl +L0eUthVnpkGsmKFAX6d+uJ+E0oqu/x++zQJcSBL0VOekQHmZVACJJPl8TTRzVI8m9M3y+IGr3/7RTyRMpjpcig== +IsslvnYVqGkrqD0qU5QGYZTJr0cGLieJcZu1a1iY/RUTgK39OfOzDT+/AdeI2wCL +L0eUthVnpkGsmKFAX6d+uH07G4h7QHuhkW15eHMk0eA= +IsslvnYVqGkrqD0qU5QGYQn8o/Js3mP8t+fBdcR0TgAwd00zhld6rOJZQ6+yFxbe +L0eUthVnpkGsmKFAX6d+uIXRRpClQCuJqr/OY3HUOQs= +IsslvnYVqGkrqD0qU5QGYQheScjm8xnwil+c9CUIBAIfDVr4aEwxf02axHs55dTj +L0eUthVnpkGsmKFAX6d+uAX3W7tCeHuM0iu6taGk0Ik= +IsslvnYVqGkrqD0qU5QGYUyoMIjJLmAF5ojMLgMpyaE41ZKkM8Gjl3APdZx0Hj59 +L0eUthVnpkGsmKFAX6d+uEmIBdMpCCkqkvpD1pJK/Fc= +IsslvnYVqGkrqD0qU5QGYRNYwVLAovvSjrixyYk8aT36EarzpP9AgYTB5zNgeLWC +L0eUthVnpkGsmKFAX6d+uJfb3o2MxlDhM0IFL0qo2QevpUkPYKMcpvvjGL4e+KUJ +IsslvnYVqGkrqD0qU5QGYXSqTc4qTS7hPO4iPJv++5lyNhEn9/t3xBtXLeIofwMp +L0eUthVnpkGsmKFAX6d+uLOTVM8QXQomM/ScmIi0Aurqqbx0I74fDjhRNAJFYEPJ3pYwtdrIJSDHigUgbAk2dg== +IsslvnYVqGkrqD0qU5QGYXG6NlsXd/YqSaoeZGsOge0O+VwQiRXmNu6YtlNYN82K +L0eUthVnpkGsmKFAX6d+uK/VpHdm7KMAlOrmSjRBOrc= +IsslvnYVqGkrqD0qU5QGYU5Lk4PmA/HmYwjmHWabZ6hHV55xwbkRmwNx/z168Oti +L0eUthVnpkGsmKFAX6d+uHf7H9Z69Fy6EtLuPiv9GmjWSmDXIF8makwzDG3As2Xu +IsslvnYVqGkrqD0qU5QGYZszVUnULbYAsg6SI1v40Ie3ai1tISljWyUaHEuTyTeq +L0eUthVnpkGsmKFAX6d+uOMCsU8cjJLqb/iyzhRPI/ckwr99ZMBbGMnD7MK89CzJiEYNWtqeqQk216KxbOgGvQ== +IsslvnYVqGkrqD0qU5QGYQAmKpTYece2KpTt3d1NBCNYX8Ap/8fRHVUzkhz0ko4H +L0eUthVnpkGsmKFAX6d+uG8+ZBh4qlZRJvEzt5lBDV+PtTrj7mpKSjT2XGDtk0Vh +4hdB7RJambPsQ0dtPl5R2akq0BODLVZotsDNh2wSunpksKRZQGda2QfFL3yC7r9FNUGWj4cTVasPp4knqjntXg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +21/52BX1a38vl6/d4QTVP0hoUi6PwJEXFQ6wphCzPr6fXDo3Z5Vg+a7TcMA5dHOO +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P/pyukZf+uIGpurCH+0HOPz4Yr/Z3jIpQQmOXWHeoYrG +ZtsV8vaHEXutlQxVeZ819plsRMEMfUI0BG6ieJYZQ0SZ5n31ETTM1BuyD7ejcndH +KZTmaJLp+FU9X93j5Tpqtw== +W08jtqcK2gqkconJfg+WHMWCpDQbDVBXW3mpkNRn3lIXAExnTItYo1kq5iRG4GcR +1u+XjG/2+GSQRv6EzCaWRQ== +CTVkyTnZxhWMnQdl2TPfNtZypvunYlImqx0NqMHaPXRBRXlCxO+cXNezSDuvkCTx +TADZGw0D6PLYGUHKmgeZL4HNEC8FTcIShYtTIXIxyo+0tdoxkJP4HbuHSul3u35j +topgAunkM89n84cH9D9pHAfyyGx8htIfqZCXE8ucjJj/OUSO2V7ozSUs83ljlvy2 +O3CUgrw2GJfB+mDjH5+NdkkEd9P97hGK1RTeRfwhysCroE1sFZzj1cl2Wj01k4MM +VmVrGQo2zRokW/ZuO9bN65x96E+pC4TupOHurj34PBY= +topgAunkM89n84cH9D9pHKjRNdYyRqpYiqgVsH37hnqzQCcFnmPRP7x+3/JqeKZ+haxhWTIMsEjUYMkHZbb60w== +1u+XjG/2+GSQRv6EzCaWRQ== +YcIT2RppFBANuzyiUKo/XbqbC0hnpSADZ2Xd7Kwfuo91ygsOr2hFjcoPoo/9iE1B +mEOKzB0uJs35kZrdrKxIkCclaJRcU34BXh5nvgXkUmhXqzXNaToiOYCIt8Xptl1d +W/Rdcl/m9EziNRa9XlvfZH5kAFMc6PQ4eF7Rsh/gbQLJ9bIjLmL0JAuq36SAsgMj +W/Rdcl/m9EziNRa9XlvfZCdb6qvEMYvH5wnxbbWCd1+DR0Cf3qqNnJRCqdPpXwVK +L0eUthVnpkGsmKFAX6d+uMzHH+Rw5vYjEEqS9kb+ft733Qn8E+MZcbFtFKTonvDe +YcIT2RppFBANuzyiUKo/XfLqIAjoBYCaWlb87AUyhxA= +mEOKzB0uJs35kZrdrKxIkAwLXKX1c/aY0fGJeKgMKfhPtxAt6BH1o9t+1mdRbz8z +W/Rdcl/m9EziNRa9XlvfZH5kAFMc6PQ4eF7Rsh/gbQKgUj6yNAchV8t2eLfKtDtG +W/Rdcl/m9EziNRa9XlvfZCdb6qvEMYvH5wnxbbWCd1+DR0Cf3qqNnJRCqdPpXwVK +L0eUthVnpkGsmKFAX6d+uPK1shPIFD6x9kjjo2oaS3fd8Xa6knSu/TMUZumMZllv +lGY+HGJ/DlLufZHWWlj7U5YwItAjxeXXjqfky42+km8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +QdTjTxbShl+8mrARISGoAcUhgZRi1ym8p5oZAX1ZOiFX408x5lL2opLCXVnt/vA2 +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY933z5nYV5e5Vw9OGrr8WfRmBaVCVOfV3FtWbmWGq0nhK9Hbx3/RcbihQUwUjoECJw== +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +QeR/vJltSrUa36sdgLk/IfL4q9jp7Kh5fPIA7nHg0hc= +f1uTQJeAsYOuzXSplbSkIbPpvJXETldnXXS37p1dj3s= +h9U6jl4cZ4C22C1wpKu8ve1yUcQ+dIn1vMmQcAZ0Z0w= +MzOj4ZiBOJDNVP8vi/lFQDc/aaaueRS943aPBbBVTruOJ0ScKqVF1VjeXVd3nD3y +ynmt6S+38NUiCOOHWGlzR4HdkOhFQMEHtI5N9m9UmAHvhYD65+zL3Xa+TZcF+A7JkuvH7bgBenSEp3R5N7mEEVopWYSftAoMBIzCFFUM8wBcyO+Zn8uwyrC/eV4OLSQ1eHRk79nfwEq8G5g7LP9amaF8mveR7Yn2dtZqMe9UGBmE1kgEX+yal6xBpHRIT4lOwwPGunRAoGoHVO+jKBIPUW8TSA+jTQ+0yT0rehF06w8= +lfyVGovXk7iJwBH9N9PEdVdmkroBj+ONqDvmQCUdy6k= +VmVrGQo2zRokW/ZuO9bN69sw02l8wweyC+FH5l+wLv+Ee7lQUMZkk6JxPCn2KC+R +VmVrGQo2zRokW/ZuO9bN606ykrEkLL7rkIYoLIEGRKa/IO3yIPucaHCAbBRsYEvA +VmVrGQo2zRokW/ZuO9bN65fvpP+bM+ZO3sDwcWPsPc3jVHDm/dlBMFRHAuHLqEE8 +VmVrGQo2zRokW/ZuO9bN66iijmlBDGu/7d75ZWU/kWG35wZGP6R0A94kBvpc+n0R +YH231WTDnzQG3bFilBiqIg== +wlLHv6kT3Q/RmtMBN4nDAT6Yus/X/bPaDoTTmgVYHsPCgsnUT+b8w2rTsMRKLl+L +VmVrGQo2zRokW/ZuO9bN66EqyN/XX98hHc/stlewQpHbm+gnf5+GoPEYTHk/wm4E +VmVrGQo2zRokW/ZuO9bN648PngVnKTtx+x3sa9gCvVU= +VmVrGQo2zRokW/ZuO9bN66ld9vu6DiCgxopF+eL/NaAa8rqG7ls2TJquJKvUWI/k +VmVrGQo2zRokW/ZuO9bN6xO985T9pwQ7sN4pEaMuKffCIPEXjYOQ0lTzWJ5AJvHO5YrWawAA1po8HVlyajFBHA== +VmVrGQo2zRokW/ZuO9bN6/iiJ6ywWPFmc7rDzl3SAiaNlKRacu4gJ2ukVSgw5Xe0 +VmVrGQo2zRokW/ZuO9bN69SQQTccEgrQxwYplaazClQ6D2QNKN0egHCbyag6gxMojLSb3oPvSnAw7DYO1vRK+g== +VmVrGQo2zRokW/ZuO9bN6+dlyOY32uq88jvIbgRzKiDMm0ganjfVKY7FpygLryja +VmVrGQo2zRokW/ZuO9bN61peK/nN9U69DY3VRr6FObLOlhZnqahXz1HoUOLXn9G2 +VmVrGQo2zRokW/ZuO9bN6x9pxWmgkQy2kh90U0Cezas= +L0eUthVnpkGsmKFAX6d+uP5wwwCosax2ir0Hv1xHLf4= +Ugk5+soYHCBsUKBc634TSP5vAwrOjcBahLrsDvmb5FWZu1d3ORGf9BCh0nq1o7jN +ACEEdgL/kNbx7RaZcSRxZIZ7jqIFcKvTxbeHCZUGxKDJtaY2uw5Wg70BfPJBD9aWShCUnQQRQXXWR+PHAmREhq/pitnanpldXtISZM6BIHXSJKcThqhLjhdomc7tD9FE0gFGYn2hL/fyorY87ERXwg== ++Y0hdVhe4P6kxySw/AbSEZezH4XpLoMqZDEtodezEwe+sEK3EwdeWCG/rHzQHNjH2Djgten3Kp0mQBOVU4DrJjvX6vMnGqwK0A26Zj5Z7laJLYyp9s1ePS7FEvx/5h2q +ACEEdgL/kNbx7RaZcSRxZMBkaNKtMOvmOtY+mNP1/ddwTQFHX+uWMEbHiI2ZxpqLI5C5Pk/24zEzu7w1EJmeXQ== +IWUeBsODWZbVEdrzqAjEqtizWLn+FujeuQdgkB69tIMjQJNvecUOjPbaAE5iRYFq +bjpI32qzKze0nfrRYYQXmykS5Jf5w8l6vEzLuK/nTbZ+CVgh2EB5eMaTrJ3F/3hWmR3/6JZoH732hBosDRZY/A== +1u+XjG/2+GSQRv6EzCaWRQ== +PRG/YRzXVD8iVR3bzEcUnoHre2ym25NbVgAhCcm2A1hoiCeOLVi33wtvGBo3MOPE2Cb54XyHCFdK3+BCNa1Dpg== +O3CUgrw2GJfB+mDjH5+NdiRXh8cEweT1cRWwOW9QX5fh6Yaj08E7nEo3hPidINlo +VmVrGQo2zRokW/ZuO9bN65b6RdMyfA4zumGrUHCFg65RTBrSUTABEpsq9PJ4JCel +VmVrGQo2zRokW/ZuO9bN6w9tUPRz4/J0Cgrs+FV7Tr/YcWK0KgvLa4A8Zh6i8yg+uRoNBCckO5193pFh3dc5mA== +VmVrGQo2zRokW/ZuO9bN6+YGqEY/sG/2DL6NHBGgZFPn8HU0P0VRGqmcpSfpPR9W +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61YlA8R/kneqHpa4ue21nrXaE+mh5Y+vq6iFNzk+uuvg +VmVrGQo2zRokW/ZuO9bN69Ej+WUzR36sZsG0nGLhsTjFUTA171Py2jCOZGaWjhVUzTwgwMBDdiIDiZBt0M49pQ== +VmVrGQo2zRokW/ZuO9bN62oRed6hCMosT7UP35UqsiJ+neIey4O/p83yxE/Xz8Vq7FTWnyXgWBqC2Ofg/IQMLA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6xv6VKf5v25Gv35Ngxt7+SxexyFaNW8qsenwXUokPM6b +VmVrGQo2zRokW/ZuO9bN64a61Rpqu4F50qZSKzf6C34rN8pqiwxpLduFB9njxLiv +VmVrGQo2zRokW/ZuO9bN6xO985T9pwQ7sN4pEaMuKfefQO3H0FO740MZBVb4uwOf +VmVrGQo2zRokW/ZuO9bN6xr/qvm+5ucCO1hkB5N9exATtIjyk7lQ3ExTkIcU12jOt9fONgTPPb03RIYeiIzThg== +VmVrGQo2zRokW/ZuO9bN69SQQTccEgrQxwYplaazClQ6D2QNKN0egHCbyag6gxMo/CPqg8bW9jLetQduGzVa8g== +VmVrGQo2zRokW/ZuO9bN6+dlyOY32uq88jvIbgRzKiD+c9puldgGvnigwOeCT81wJWkhhBMtEqqc8b+vZxO8tQ== +VmVrGQo2zRokW/ZuO9bN6/bbff3RPIfdJD9Tz8nKTkS8AWh7qG4PAFWqinn3GS+B +VmVrGQo2zRokW/ZuO9bN68cDhwAKTM2Cy3z7y8RSN78= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6463nYA6JOgSgcM3CVjNTJznMLYNsYHB/6aPvDLy4Q80 +VmVrGQo2zRokW/ZuO9bN6xv6VKf5v25Gv35Ngxt7+Sx0bNvv17oN8olvRoRNFEX7V2BUAqQk1w9AG+fHfA3mjg== +VmVrGQo2zRokW/ZuO9bN69dQDCddUjZfqhLWgtMTA+6lr1BUsQBWcHEfSvRE3879 +VmVrGQo2zRokW/ZuO9bN659sVHCRNKwi5EybWXGjIFHDG+wKjWhy7PLQCmhQOSV4291MUSOlSEESLCcvl7/Fng== +VmVrGQo2zRokW/ZuO9bN62+mfM1KxuYR9GMEOHSn12FMvAE6TgNv6jSr3+eIjIkftGBTCi5VuuIi6ta2B1ahrhcNQtCYZ8yLHfkFzehQCbU= +VmVrGQo2zRokW/ZuO9bN68xhA6nn3nm5MXR5GCHMXtcxC3eMgYv7+sQYRGoRMUKJ/NtYdiS6ZTF06NLLhQeZ3Q== +VmVrGQo2zRokW/ZuO9bN68M2OVEeH8lXEDh7ocUvFZ4= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN60ntial7L47tWO4rV4Icn7HpBltblYWBCieqT4UVgq9CFSyUKzN/U7U1y808EhW5Qw== +VmVrGQo2zRokW/ZuO9bN62SJgGjT/WmMIIxZ/ruMwWUGgOzwXkp7Yl61rDQTwCRFc6zTS2NxH91SNGo4fvq8c0/QMcscIcl8fl2DAYz9BEM= +VmVrGQo2zRokW/ZuO9bN62oRed6hCMosT7UP35UqsiK29Hs7QixVm5i7kdWxory/eb/Qqnm5/jUpYwUUUkLhDA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+6X3Z2WgPscDVJea7oFkkBvFOwA8ZIliXzYpwCO5w8mCjx0uV1YsGYftfAWvGpnZgdXDsovKcS9qBn/5dy9/jU= +VmVrGQo2zRokW/ZuO9bN60TFHropKQybmgi/Ewhnmc7xtekqzThyNJcyQykKtAL8 +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN60TFHropKQybmgi/Ewhnmc7PyGF61LfQUSq+4E3rbm1p +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yOPAnkhPEHUxQR8wIbPiwSTWlEsAD2rF3bnj5NhAv9XbaRnFLHata2faDtCqOdjdb1U7MCISF8/6F97G7mquUM= +VmVrGQo2zRokW/ZuO9bN6w3mLqzdYJP4YnG22gQyoLmoBk43np2N2P04jX+kASBb9gClLRXQLQzop3WjvHjIKKMsPzTJmkWildPSC3LXw1E= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6y7h5D3hwYRtgfvir5w8hacLALFT2aKPT+72WX0CphB7 +VmVrGQo2zRokW/ZuO9bN69dQDCddUjZfqhLWgtMTA+7x3DyGBidwiLe8ycxBOOYA +VmVrGQo2zRokW/ZuO9bN68xhA6nn3nm5MXR5GCHMXtfY9183Ko8rfO9qF7oaQasTgQyzsj3jWJAdaiSjnzCWMw== +VmVrGQo2zRokW/ZuO9bN659sVHCRNKwi5EybWXGjIFHDG+wKjWhy7PLQCmhQOSV4ZrV+dIrpw07fAyDbieibNw== +VmVrGQo2zRokW/ZuO9bN64E7kV0oe1KNCWtfO+2IzG2FPrJbLwlL61MXY6h19yCgwlN05/jodOhk+oUWPJqcNN6IBI7oumAfwTtn3AERBME= +VmVrGQo2zRokW/ZuO9bN62+mfM1KxuYR9GMEOHSn12FMvAE6TgNv6jSr3+eIjIkfOEwcPjcDLjN9tD0xweBslcAkKKcfqK+vUKrRm3X7Nl4= +VmVrGQo2zRokW/ZuO9bN64OojIhoKNJ1F73QGw4VuTsSq1t0rpx7OXXkl8uQ30KM +VmVrGQo2zRokW/ZuO9bN69lJPu1G3iX01kmj1/0ynPc= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN60NuI+3rwmZPRXBatW5je5ashCp3DeAz4mwJFRsD2uIz +VmVrGQo2zRokW/ZuO9bN6y7h5D3hwYRtgfvir5w8hacofEZFxYSxFBNP4nrNMDKlU4v51PkdjGnN4kullijdzQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn3k4igWZGKmftHXEMWcn7kly21b/nYMgSJlcJhkvDi9w== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkliTSPh1fvkS/ScWR2yLc4+HTPJwBf81Hy/L5fQgnRrH7bNCsflDO0k2jj/2A1Vbck= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn84mKwOupbY4jKHqhGVSsSRWvM73UP+iiVN5SxtdLKWMg4JgXS/q/83v0PECO1Oxw= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklAMkk+cPHwsBtRXYD12TtxwEYLEKXTLpcd7/0JsjoVdw== +VmVrGQo2zRokW/ZuO9bN66XK99UTYPeH5P9OwsB1SDc= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +h9U6jl4cZ4C22C1wpKu8ve1yUcQ+dIn1vMmQcAZ0Z0w= +FBy3vCcAP87b/+M8ydgUJRF52I47z/TZBzA6Bymagn0= +xOhAHwpsatV+hYHn6t6A6bUPiUFFzlk01p4yvWK50jyy3U69zJPFTOkEngdeeh4dV2ZsmN/celU41tVeKyZCPKR9o4ij9I2DDit4hQ8yk8OvD6qwHkCfsNW43rtkacx8 +topgAunkM89n84cH9D9pHBHJGKogzkmjYZitDzWAv+GuAYoOOkSChVYahktCovv+ +h9U6jl4cZ4C22C1wpKu8vZOLikErPbvUJn108FkBgu9+60+OG57l8ccneiDXTrwOoz7y2s/Cah0Aj4t1RxHPgznaDgjVKZs3ALXpKjeJ0vo= +lGY+HGJ/DlLufZHWWlj7U6go50WaXphLgme8Jt6GCTs= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +IE+9Uft2qvVuoU6zfHciG3wzRZH5DHb0iYqeBG8ShAk= +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY1lJUaDlPvjJcgqNTD10CNjS+JFlHmmzAW7C9TIFGPZZ +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +2A27FrJel7LwQVQbnj4q4hlgih3qBk24vCQh3ZEzZH1D9Te7T5q1mDbPLZp0o0v6 +wXC0PXkDt+//2q1Gfu8pw1omD2s1DPC9/eHWLru0T7M= +/0ULpLqgTvInFD0r5hHANlt/A4HN48BaWWKSY1CtQvClxZHrVHBtcJRLbba3CP1w +9kMYV6mizqMbt4N+qTZtN4zB76eoGATkjA0lU5cQ2Ak= +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +Nxw10FAZUk+m0nadC1hdy7a2+IPpKTLzQ6uOO+E1Dbs= +X/Lp4PsbbXulceSXUvGUpEwg42OTOSx1VxHM46BBNX0= +s2s/1ED0ebJ2NmFlE8kk02x6FisgSkWfiVYBaeNNhGvko7bwZ1FD5aZ3vGnu9kSm +1u+XjG/2+GSQRv6EzCaWRQ== +p+qW8e8p1zrxOyU05vOA0oTKH5zqNz+PCgf62f9tApk= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P3lRQWSpZzKNexozsWlQSmt +FFvLZRifY4ofMFjBuFasFxWXCYrK9vqCc5dq5VE0iw6DSlYwafvxu5DYwNYrMthu +1V2v8QerKOmubvSxgB4eTBZTEixtIaCeLifMkliDnl1FSzgrzmlMEyvFnWK3RGd/ +J1UGlXkKhKXD36OrXXhN8BVlTjNvt2FG2CiPqdl199JaWrFGr3c4jWvQXS12vU6gFbGt23o5BdLlHOVX3j/o5Q== +1u+XjG/2+GSQRv6EzCaWRQ== +2J/cJdNHXZ2Qsa37Fo6/Z/gM9h4l3NzxQe1VHPUoNCj50z035sIRNuNbaAptxn44 +0aOe1PRcA3iTQeyd7fu/UT3OH8TgnUZIetc0kIThlMY= +V79F1HDTm8z6yGnQm/hDJr2dkX3jv5pKaOjk74dknTfkNo8du33ifDa8c3SEgcfl +mWtqqlZvPd5u+MDOxVnfq7x81G9ia1eZdDpax7kDimPICwqRAAp4SD95uu4DP+3q +ftwgBqNbwagslv/gBU4tpEE5GDJfsuYmSwXmJa3+o1fA58g3B92Kp8T+2Ht6OVkf +K/3TjoKXtlVKQEXdtPO0uifsTNFU0eFt8+2eD6sIbn2PmRNN2I5GHAjvsDqrxu3awe7s1IGxmpgEyF5PTdi4Wg== +VmVrGQo2zRokW/ZuO9bN6xz5iGlq9bNMsveN3G8E1JikmXOYVXY7T1MoO8bJd/lt +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P0b5qliwag+wNT+s/UEtf4f +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P3vHLK4yXJh/Y+S+1XhH9rt +1u+XjG/2+GSQRv6EzCaWRQ== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6xz5iGlq9bNMsveN3G8E1JisN1fUkIMjwSqaHMkv+2OFIPr5q4vWGJVqp1MZusIqwg== +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P1Oxzn1tUamy4I7kxYIUhQU3NM1tr9mAx/yIud930NXFA== +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P2Ei14aMB20i0YI/RCMc9vC587JhNV12B308q8WzisouLfqdSseWY0+wqtG9K74mqs= +1u+XjG/2+GSQRv6EzCaWRQ== +1D18KWr2hdVsBcdZx1OaPfvwrE7g7+awLCd9GHCg8UHyIrzitpsEab7Put5dPSX9 +VmVrGQo2zRokW/ZuO9bN654Optg5TGZB1gMGxxgScexFS1RR1Ks0UsELmW81E6uK +1u+XjG/2+GSQRv6EzCaWRQ== +o1bT2zrSxs12aPyNKpukk+RBjtOXgYwyhl9BlbuoPII= +9kMYV6mizqMbt4N+qTZtN73ymQjbbWCfVaP1lIPcs1OU1AQgg6+83fISm2I1XX/N50H2FN/cmR/jUdb8rfxdgjyYdsNSEsociL+R9aPXHUMPalIxO0ZQEAVocqvn4o/N +9zgnmC3+5N9XK5/NRaR4SIX5nH4iJWB9r8DAPkHCqMUc7KHT6GbFzm1/ZZDfEDnV +ACEEdgL/kNbx7RaZcSRxZNCtRTkFUngsUPEeQy/+UyWj7cI78bO+VYj4wVqG+FraCRyNCLzTSU5fw732DiiY4Bs6NS4JZHrotgQbzgu/NHg0XC2ZDt2Tdq3I1jVs24bycirX7+k1becWKOkHHvZ60ZXv1saJ5rczadDgDE9Qj+k= +lGY+HGJ/DlLufZHWWlj7UzT71dQgCsiwDOluCIjxD+k= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +IE+9Uft2qvVuoU6zfHciG5u6HyMWvuiuseR1J2M4pfs= +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY++vWRAhgONxDuLxgRuRo4NAs9c+F3KZcSg+B4cy6yyb +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +2A27FrJel7LwQVQbnj4q4hlgih3qBk24vCQh3ZEzZH1D9Te7T5q1mDbPLZp0o0v6 +/p6t8/yEODj2SpyYrFogNPdnJ598mYDI9OQG3SKrs2s0xEMb9jmts3FpPIdSfwUlsOkZH7GoGPHgLihjl0yD5koXY2E5+OCPEh6lFcT5FWGdmJWettOxpcoMW5CbJ+ym +/0ULpLqgTvInFD0r5hHANlt/A4HN48BaWWKSY1CtQvClxZHrVHBtcJRLbba3CP1w +9kMYV6mizqMbt4N+qTZtN4zB76eoGATkjA0lU5cQ2Ak= +1u+XjG/2+GSQRv6EzCaWRQ== +Nxw10FAZUk+m0nadC1hdy7a2+IPpKTLzQ6uOO+E1Dbs= +X/Lp4PsbbXulceSXUvGUpEwg42OTOSx1VxHM46BBNX0= +s2s/1ED0ebJ2NmFlE8kk02x6FisgSkWfiVYBaeNNhGvko7bwZ1FD5aZ3vGnu9kSm +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +p+qW8e8p1zrxOyU05vOA0oTKH5zqNz+PCgf62f9tApk= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P3lRQWSpZzKNexozsWlQSmt +1u+XjG/2+GSQRv6EzCaWRQ== +FFvLZRifY4ofMFjBuFasFxWXCYrK9vqCc5dq5VE0iw6DSlYwafvxu5DYwNYrMthu +1V2v8QerKOmubvSxgB4eTBZTEixtIaCeLifMkliDnl1FSzgrzmlMEyvFnWK3RGd/ +J1UGlXkKhKXD36OrXXhN8BVlTjNvt2FG2CiPqdl199JaWrFGr3c4jWvQXS12vU6gFbGt23o5BdLlHOVX3j/o5Q== +1u+XjG/2+GSQRv6EzCaWRQ== +2J/cJdNHXZ2Qsa37Fo6/Z/gM9h4l3NzxQe1VHPUoNCj50z035sIRNuNbaAptxn44 +0aOe1PRcA3iTQeyd7fu/UT3OH8TgnUZIetc0kIThlMY= +V79F1HDTm8z6yGnQm/hDJr2dkX3jv5pKaOjk74dknTfkNo8du33ifDa8c3SEgcfl +mWtqqlZvPd5u+MDOxVnfq7x81G9ia1eZdDpax7kDimPICwqRAAp4SD95uu4DP+3q +J1UGlXkKhKXD36OrXXhN8IJbvphSi+7vstAaJ7toEgm4zZMxFe/rKS4u9y2Ssvzh +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAcBxCAXlvfzQFBJgoPBTJbj +VmVrGQo2zRokW/ZuO9bN67iVlFAU8hAYMafjpVDx4oOy1GxC/YZz9MGGC5hXHifDIzzMbooqVq803bDkm/ITJg== +VmVrGQo2zRokW/ZuO9bN66Bl6UaWMaQpzwFzTULo/tY6jlTwP4gsV1QVwm+H2RE+ygziQ7qyYUv2jZGrZghIXhaI9IfXLM4+/7ZEaTQK0+JudXe43jFBuxq9uaQVanpR +F9Sftf1PoN5P+lgvc+r10DzCXpZBs2+NQLZrUg4+WkxCye8nWiOXw8mDAYUsbMktOZPiLEpeqFuRtHXGZ2OIbA== +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAeRwlwRNQErNfyGxNziVpfT +VmVrGQo2zRokW/ZuO9bN67iVlFAU8hAYMafjpVDx4oOy1GxC/YZz9MGGC5hXHifDCkvImVt+p1wORLo6xFoArA== +VmVrGQo2zRokW/ZuO9bN6xYoLnmVOS/ahrj5uuRdV0EWsnEizWRxlOrvM8wENlN4 +VmVrGQo2zRokW/ZuO9bN65e0KdmCuDXHyRDbH0mrOLKZuncKdgINFZSmlvo5RFWwygn3vSbFEX3KrOzxXciCHQemoYWznnH0DlDnPaok5SJghk4Gvzt1lP8Nd8xypH1gdpU29AuUlNmUasQNQYvk1Q== +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN65e0KdmCuDXHyRDbH0mrOLKZuncKdgINFZSmlvo5RFWwygn3vSbFEX3KrOzxXciCHQemoYWznnH0DlDnPaok5SLNfnGfLb1wbPgdBMy9o4VF+look+gYRrvZgUaelLl4lQ== +F9Sftf1PoN5P+lgvc+r10D7o64ZLbi78S/TgPxw/f4SOC++uRT8tIfgwb8Omo7vu6chE1yw4OMlsbZQVGt7v1YdICPBImfmbQa8l34QUTWP5nPueh8DOOl+YjipCgyfiah1QuO33ZZN9u1isdHNgaw== +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAdgqSKLQvYqIyYU7AJpR7fK +VmVrGQo2zRokW/ZuO9bN65X+HRZP26yr5JJcJjbroHgY2yQZCC2iz/EentJIOM14 +VmVrGQo2zRokW/ZuO9bN66Bl6UaWMaQpzwFzTULo/tY6jlTwP4gsV1QVwm+H2RE+U2dhdllU017qeNlwV/t3qMxoN3b6ZEwtypHeRVok5QLeq7jBhGNGeUsnEKGRoGiN +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAebii1mp4p3I7SDA1b7UujH +VmVrGQo2zRokW/ZuO9bN65X+HRZP26yr5JJcJjbroHgY2yQZCC2iz/EentJIOM14 +VmVrGQo2zRokW/ZuO9bN66Bl6UaWMaQpzwFzTULo/tY6jlTwP4gsV1QVwm+H2RE+0ESEOFCVWY3zpdLKkxogIjfbS+mp5BcT25dPwPqol/ewrCICKac8rraGy87WhRhdSJ6N6R7PX4AbXUWbAtjbLA== +1D18KWr2hdVsBcdZx1OaPfvwrE7g7+awLCd9GHCg8UHyIrzitpsEab7Put5dPSX9 +VmVrGQo2zRokW/ZuO9bN654Optg5TGZB1gMGxxgScexFS1RR1Ks0UsELmW81E6uK +o1bT2zrSxs12aPyNKpukk+RBjtOXgYwyhl9BlbuoPII= +axUGGc1VN8059OpAvKXjQWop4g5wEilwDML73fTR0aROShh1A/PpsX89P62Fxwd4hZKM+zVAPfmRsWcP03u9Ku3VD03o8PL+LDYwCELmIUqQ9SwDTtOHpMHkHqyUOUm1 +lGY+HGJ/DlLufZHWWlj7UzT71dQgCsiwDOluCIjxD+k= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +OtFYf3L1SCFz3eV8uxxLQDcWdIw+tWys4K3uAhWrSOIHz+Hf+pmJOLXaeG2mKqnJ +Z8UsPk1Q7HtwjRd4g01ryw== +x1Dv0cSaQmavN9ft+ex9jCSwM0+ru2NYHY8G/siatT0= +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +L0eUthVnpkGsmKFAX6d+uCuT47MPwULi2lpyJh4ryFDW9qwV9g5joBbVHwA9hMBSw4eJiwzIZ2bMkcNfWxzADeutkGRiY7X7EdjbySNl+rA= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN67Z4GyvH1s+PEZuudjntZcEEAEOgkqKv/lySCowLggPbbCf4J73tOz2c1J7SATG4GzNyDiMvQ17bky1qOfBwo79dgSMSwZ1eu3+WvIgk3sqo +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN64mARHpcJLoYD+9rQ7Ln85U= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +QdTjTxbShl+8mrARISGoAbKd/0Om5LDKQaniNR51X/Q= +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY6IrtSknVO/GcflctAZsVFrYCUa5MPG1XS1Xvxi+13Qa +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +h9U6jl4cZ4C22C1wpKu8vSM7bTtvy9o2g7Di3K7jTo2HJl2CG2yVZIJgod88bc9l +70InS28HLacTjXElRM5nzDgBR0IPsK1rd3U17Ol/cDLFLPbymx3eOxXlhbV+4Rah +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YK+xvdtabAjOdZa8rH/UawJ +VIHA31zdnuwVlvUv3VOlYVRNGNAMqDxg7g3P55Yig7XGJ0frgqBbhflLu9pZDsGtG7PWJFzYnbAhv0HasJD0eg== +X/Lp4PsbbXulceSXUvGUpDfqLabbqgG4MZE+Cdy4FwlQTZqFS7MLKRTLDMwaDvILIRxQybc5sD1eU9BOwIwsyw== +1u+XjG/2+GSQRv6EzCaWRQ== +3CRPlw3jrcmcxl55IvNWHzpHT+6v7kWYl0ApHHoxKMJoX/gB5r4x4K9yipZS1RbVmH3Bni4Adtio/jyOu7/Xky/SKbiStP0vIIU3iLQZKYxRFiMvP4KcugEoJmswVAtcNQNIl85wHb5F+uF64zFndw== +1SbkTSil8XzgXK/aUkDPBugDeezHMR05FNSDcP/zAI0EyOvkngQrNpe9Q/zEiKi6 +ynmt6S+38NUiCOOHWGlzR+5LF2yk1cZiM+CH3D6A1LQl/LBPT3dK/LV2jXWp4pRO +rZ6/dyVUzW/Y6/ismBQRsvai7eL5Elhq0TRluGLB1iQCjISRGPWdI/DC4yKUCuZH +ZZ53txuqZwjSqUUoQFkxA8vxD0O4EQP3Gj3bk9l6kkf75Nhh4D9nFVIyddS3RTiu +vp1kd8TYnGnnwiRkbX+cNGmevUM3IizQMssm2G2VjPQPgaBN9dNxHkp29IH1TdEq +1V2v8QerKOmubvSxgB4eTPtnzcT/cbp3ANG+lRJ84FblNIiML31gaOFnx0iDFfMHFprvq8suc76LP6Nv4rjsTid6VorF8J3rPdddQqu2ZO0= +VmVrGQo2zRokW/ZuO9bN6wYSJfAvINthaFx/9kAbIX4t4n7u7tZTTsBIGg2Qyp9A +VmVrGQo2zRokW/ZuO9bN62K+Pj/G4KSvFtRlZdyfBhiB0llpWvlaCQsNJWvpmIpnE8q8slXtmx/jGYLxEVGETG+NYh6Q/nXeUjy8GYFyxE7awePOtgMs7chfALOimNQRQEr9LgVtnt+NcdVKIVdxAA== +VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUi3xlrmD1YKuD9vITYmwXw2LQFhXrmbjxoc7PIhRGtlxg== +VmVrGQo2zRokW/ZuO9bN64ontDfjzG02xckDeU0RX1dlOr0aDDh1ClJhaUJQQEzI +VmVrGQo2zRokW/ZuO9bN62K+Pj/G4KSvFtRlZdyfBhiB0llpWvlaCQsNJWvpmIpnE8q8slXtmx/jGYLxEVGETJ/m6IY4mJzQJ+V47Dnk665/atEpCgAH3VqlGooJcbGY +VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUi3xlrmD1YKuD9vITYmwXw2LQFhXrmbjxoc7PIhRGtlxg== +VmVrGQo2zRokW/ZuO9bN61wyMShiXniwkoczPQZqKjfgIhCq708yG+xQp3DrRld/ +VmVrGQo2zRokW/ZuO9bN62K+Pj/G4KSvFtRlZdyfBhgNCj9Yj6N4yd2ZIBHnP0lzjzLtu4SleXbeNFqXOKNpEZmlBRA0F3hFmzJkw+OA2vIXkDIGIVhBQplDJFB/j1ZZ +VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUi3xlrmD1YKuD9vITYmwXw2LQFhXrmbjxoc7PIhRGtlxg== +VmVrGQo2zRokW/ZuO9bN60mAu6yBx4d6kfvOGCLxZ3YAjG6cu/92rOpni9SYu3Uo +VmVrGQo2zRokW/ZuO9bN62K+Pj/G4KSvFtRlZdyfBhgNCj9Yj6N4yd2ZIBHnP0lzjzLtu4SleXbeNFqXOKNpEYCoUT2GLVT4RJj7Ye7vqiRw6J4Hh+4SHU4QbWrOzkiCsoppZ9Egg5/7bWlwfrFqBg== +VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUi3xlrmD1YKuD9vITYmwXw2LQFhXrmbjxoc7PIhRGtlxg== +lOh2GtzHjjMM8E9J40AuOWo7iSH0P5z8Y9QjLJCJBGhZkrpt38uR3iX7EUOZ+eOR+aAqr73auptB295rHBA4QDmoJ45/cdwzvUGosMaFPoFRPmvLy3TrBNLtV7Ktn4sG8Ogrd11C48+dLen4KuB1kg== +L0eUthVnpkGsmKFAX6d+uMrw1kmlfAOxw9snwIOJEE5vzwAr5lC4+rbX2BqiWH/dpzzD2IlSKVcXrDtzOCu1+g== +HTOfkyRnnkvrq92C2ykyOPO4d7gErRgYL7aScZTpzxREn6lD4QDtxAH0B30SgN6E3F22VdO8GzELDUsICPiSeg== +P8KbMTjfuhARpOL8NoffNro45O1U8oEoZJkv8WBE9IqPq9Tpvqti2IlFB1FFEDKEayTHih7/O8HeyhvI7Rb1xQ== +L0eUthVnpkGsmKFAX6d+uFjjhUy+MCKFNT4NrCL5yNnhcA/pEoLZALqem+hbKHoY +1u+XjG/2+GSQRv6EzCaWRQ== +P8KbMTjfuhARpOL8NoffNom7jch4ZQTU5jfIo7wG8ScR4XnHaBOnM0U+2vh4JNta +L0eUthVnpkGsmKFAX6d+uFjjhUy+MCKFNT4NrCL5yNmWWojcHbeJQ61OMhTGFdlC +1u+XjG/2+GSQRv6EzCaWRQ== +i4c/4ocK9c+0kOv/NeukhzRCRIDY4b1IcwhCyS6fwKaxL1iz5YdtgQ8ur54xnSve +89eRayX3G8tCX3FDU5IcjcECGHtpMfMxvoO/SDLARSENffTD+JaXDY9RnayEnHLnlI8b7pgVZ6VRMA8KffpRHg== +VmVrGQo2zRokW/ZuO9bN67AUBZ3/XEKHpa/wOGbEOnmtIF6uAz0tBlf7rcOxUQPuytPu9bJw1t4kl86ncxKWUr6UFGd4R8GkpK1204AYmG4bJSHkhwXQJ4SNyep2Orx8zpIZXaQwin0CO9B7uaPk1A== +1u+XjG/2+GSQRv6EzCaWRQ== +swRuTHNu5hN+oxWHfH2uTRaafzF+WZloRON/s6FBnLM= +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcJ0dAhlHHuuDGrGW0z9/mG1YgeQhelMeN8fM9H05Q7bFoBNC//YD8IgCAMKgkLtISr7jJb8ysmAn68sYqB3zYKY= +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidaKrkdw//NbtiPyzZIdYJRwkO1XfoVngfneyEI34hiEIjZDJsEWIi+RYy+5PJ52y4M= +6P8mKfouOK1hwXy3MrrCzYNv1bj48gIxdX69IR058kk= +2J/cJdNHXZ2Qsa37Fo6/Z45HJGTQKJ2XarF9YWFlYi/NS7Y1/C8WI42zDRQS65sB +K/3TjoKXtlVKQEXdtPO0urKjWEFV9hqYUzZS4hx2t3vgmB4P0sEnPBB9+cxnHYt4 +VmVrGQo2zRokW/ZuO9bN6zPTlJYqiIwYIHWMLx6MVG4A/jqaX4tahdPmgshFSQG4 +VmVrGQo2zRokW/ZuO9bN60KbPALjpXkxdnMsVQY2qiQ2mn3irY/rZFqtKYoHLxRE +VmVrGQo2zRokW/ZuO9bN60Xvz4OmVOCjpkbTCNQIATsOCVbKnePG0cTDCTWdue9j +VmVrGQo2zRokW/ZuO9bN66guoSIjBQ2hp+h2iWlRadc= +VmVrGQo2zRokW/ZuO9bN6855zfg/ZnrwyfrD1ClJJctc8wQUc7t8LPNPEB5FCa4H +VmVrGQo2zRokW/ZuO9bN6yrrO27yfi+aG1SaNFn36A8CnO5yCexFXRxrEFBi6HH8 +VmVrGQo2zRokW/ZuO9bN6/m/X7phwv/rEzEofJRELW/qETCx/ewa2vv2I76GwEWB +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN6/m/X7phwv/rEzEofJRELW+ZhbjEfjOJN1QgOkdHcgfg +VmVrGQo2zRokW/ZuO9bN69t0x0NhvKGOHr2+EEBw8Jek/n491mR4LKuB/UuUgzMUy7YY4ELecU+vhof4LvKC/Bhkvz+bT+jSRSy/qFVu/F0= +VmVrGQo2zRokW/ZuO9bN6wIR+5HpCjcoa898tUp4mpJqO8Na+qweZg8vpjfBiLDS +F9Sftf1PoN5P+lgvc+r10HYBS8twYBi0U/DCh6ocj+X6RD0HNlSpwv37yho4nl4Ot2LOLOWFJbMnayi7wAK30w== +VmVrGQo2zRokW/ZuO9bN67yPVwtUvdC1tj4awelda/fkZ9dUXYCEUeCyL3Yrqyn/ +VmVrGQo2zRokW/ZuO9bN60Xvz4OmVOCjpkbTCNQIATsXOlREGrlEeF/zy2qslZ1j +VmVrGQo2zRokW/ZuO9bN67HiZr3Mu5Xx/pLDDclKRz8= +VmVrGQo2zRokW/ZuO9bN60KbPALjpXkxdnMsVQY2qiQyq2q2A/0gHrZHUNJvsm2k +VmVrGQo2zRokW/ZuO9bN69t0x0NhvKGOHr2+EEBw8JfSNIZvkvl8jIdEvRQwBwJ8qc6p773V8w3sbnJ7sNRCjGOJmoa2y21Lbs/qYLxw7ZE= +VmVrGQo2zRokW/ZuO9bN6wIR+5HpCjcoa898tUp4mpJqO8Na+qweZg8vpjfBiLDS +F9Sftf1PoN5P+lgvc+r10NgITD/y7z0I4C2wYBhOQf9kEr4e0+0VrZmNsbWDAMY531ZzaC8lUc4x5xBNRPYlTw== +VmVrGQo2zRokW/ZuO9bN6yH86FWoLic2FxcRUV4zebxf0/M2UvczoadL9jeQKWgM +VmVrGQo2zRokW/ZuO9bN69so/u5P+e4KbXGeD0Dm7to/lSyHyW7MicMJ0jDa2eXr +VmVrGQo2zRokW/ZuO9bN6xQjxHnk9JeJcV3ebvE1Z2ZFylERkWn2HmPyx+oo40NY +VmVrGQo2zRokW/ZuO9bN60KbPALjpXkxdnMsVQY2qiQQ1X8tQ+wf4ZCJUp0oMXLr +VmVrGQo2zRokW/ZuO9bN69t0x0NhvKGOHr2+EEBw8JfSNIZvkvl8jIdEvRQwBwJ8qc6p773V8w3sbnJ7sNRCjGOJmoa2y21Lbs/qYLxw7ZE= +VmVrGQo2zRokW/ZuO9bN6wIR+5HpCjcoa898tUp4mpJqO8Na+qweZg8vpjfBiLDS +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6zPiagaoQNUZ6Q8MukMCasPCfaBG6YJFzlL84vUM1kV8 +VmVrGQo2zRokW/ZuO9bN69eu9LBWEB6PDbGO+0TyWwBhVpoFqMPkT8HlHX8RMHrR+75Z9K8m2JUCWTBuR0Tk9Q== +1u+XjG/2+GSQRv6EzCaWRQ== +o6QhOIN2Sc4SHELnst17uXpnZ5EAUQuq8Epej7P3sTRveRgNRdPH82tKZu7g4Eiq +1u+XjG/2+GSQRv6EzCaWRQ== +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs+glmD/7BTlR5C7Ujg9dOtQxQc0uyS/kT/ILSqYhwQGNA== +b4OJVZe8QyIpjuTpKXDL9A== +KToqm5Or/ymuVO66QU/2pu7oKyhGYWQ+0pgKsF2sKsQ= +S76gqn57AVIGnu8xVYuZaO27N6yYhZ4GNoiBGQP9DKg= +K9/ExlRvl0KpaOPuc49gtg1nR3Bc0+g9UN7uwKhXbdM= +wlLHv6kT3Q/RmtMBN4nDAVircipW/U7HQDc2UPUyQ/vmRhc3HaKPFi1NtnisK6xX +VmVrGQo2zRokW/ZuO9bN64JOsgztOsHxYHKGCeX474Qcqbriq9LDc2GKDAaXpEva +VmVrGQo2zRokW/ZuO9bN6173lsXiCfzKN2E9+9sH2KYDQWUma1tCh1mCQLO6go9pew9ww96U7ujnre6SjyCuxQ== +VmVrGQo2zRokW/ZuO9bN6/EQSa+RVcntmzafI6w4hrXeQefCDpqhGzMMDJocinCp +VmVrGQo2zRokW/ZuO9bN64ViM2XLR1EOvB6UVgIoYuF8fdMuzlzZir170SdeoO1N +VmVrGQo2zRokW/ZuO9bN6/EQSa+RVcntmzafI6w4hrVZKx2YSK2fHbBecH4ladDU +VmVrGQo2zRokW/ZuO9bN66vpbXlJpZkCo8bNa8hfdTtD0OOBen8N9PlZvb6sHluL +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +KToqm5Or/ymuVO66QU/2puiKplVgTGTPZqNeAeKoYCRnGqCo0URmUW6QuDrFvxhR +S76gqn57AVIGnu8xVYuZaLg1MWy1KmN4+LZJN+yDmqCMssHrDpVNECVIttB2y5oe +K9/ExlRvl0KpaOPuc49gtoyfPotIPHp16XgJV6eRYyrbA+h8XwwZ0jIgIeiV5WJH +K/3TjoKXtlVKQEXdtPO0uuuN7F0BMuO/Q3cMv6UmDSFakb80AgleKsv9apABSHy8BJ9UVzoo8dLM78KhLfy4Pw== +VmVrGQo2zRokW/ZuO9bN65LhaN63q3ApjnzlgAtjYqw= +F9Sftf1PoN5P+lgvc+r10C1yj1HZBpntAx/i13/oYeYaK3wfHrLLbHW7duBhTqbF+VuvZwLaBegIbS5rvj5IKg== +VmVrGQo2zRokW/ZuO9bN65ZTH8ABglrqk9nDO7nL0rs= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN64De6ITZeavbNwRiqFkMysk= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uAST/oojvSe5vOTCDj9ZTSci5aesa7iYDqIo84xNyUjr +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +9OQ4F/6xMvSNwNUGdXyQ9F1NqMgCz0f8DrpmEaOqG0E= +kkhBrrZfa9Lz2ARtxqQ9WI07s+twubQO6K+lkziI1UrpPBayzeeEXSMVuzem4Nzq +UxWVPzm6Eag/KO1azIHUdx+idAdS+rfPARzR88tVbgYdpuz2ULTF+2vmnBiei7OW +Lb802TYIO8cTPX7RtIYGZ+6eFx+9GqXwRvUIq98SK68Eo/uW/nIxVIKX8k1Y1OBk +b4OJVZe8QyIpjuTpKXDL9A== +I5csN8e3J/KIFkMa5t+SJCqL3g3fUtFpMCVgdY7uBLC2hDdwb73LgrQLVQ3aMtK6 +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033ZLNQ2XZF4KsFXQ0AJ186dTqy0tFe9yVaa5Nvi4GzvRQQ== +UiubnxD7y7+jD/CLXy/lIzUv+EQIUnxp2+tRaByCudDYzFPn13pFELSDNAgNf0aY +wI4EEWzdSnynhqFxMCzFmvtQa54ffiTU8OKIuM+mUtbRQW9n1JFAWLuXCST3voAI +GL/yLvMwjflJNfDPl5ASjQ2UrwSPDpUhyRkgeiXFg2U8KeI5JLJSQLyyHw7AauiQ +P5fFy0VqKNQcixZC6/gotxklJeUYBp6qUkCM0szi5po= +3HDkd2BFYLPk4K73U5D+BYdx+YPBx8edbX2mUEZUhcw= +gZ7BoIAABc+I8g/rYzq+fw0QRhyOskLowTpBMxzNkkXJfHhElpIZe5wsCdqDFCZvGpC72uDw/Cu+D1FuT4o1ew== +NhnIR3Ilo4H2su9/cTNo/AxXtCXBAtO/DWPyG38GkWo= +SXXsDW1aLF5ljPyJUoRc7t2uV1Ea/KDPsubfQRA1/g0= +i/p1vQ7ugXGBvwbjECApz/Cr9YvVAiEjIabMp4aMpkk= +zo+FVrlHkUeZ8fJX4PVhAH4W08MspSsE1ssI18UNPaA= +wlLHv6kT3Q/RmtMBN4nDAaQcqH83V4KNJSetgkXN2mpJpLhuyLAaUNt+tKkzaLSI +VmVrGQo2zRokW/ZuO9bN632VflaKbWaV9Wa8IOB78Qk= +VmVrGQo2zRokW/ZuO9bN608eo5JjqlJJ+hYuirBCFcmf4cKrfv149X3GuxINE1xDDJnAj+I17VZiW+JCiwfRwA== +VmVrGQo2zRokW/ZuO9bN67FMFupuL9NnhZUvLdV1qH1jDx/7tB6qL1SG7Pqfwsin +VmVrGQo2zRokW/ZuO9bN65qbK6/I8bncNNV3Fr84lp9CCx8VYIApMdX4h8SHWyk2 +VmVrGQo2zRokW/ZuO9bN6zEL40rbK+o6j49cNVbzKqOsExNYZ+R8lUnZEGfiuhXY +VmVrGQo2zRokW/ZuO9bN6+EP/UfvFvq8hDmkfhk2A+qnnPSHBLYJiyMbFizN9aWEoBeZFZoLhXy50+Ts9Ur1/A== +VmVrGQo2zRokW/ZuO9bN6+EP/UfvFvq8hDmkfhk2A+pmn/JN+SNJ6dRlNT3kh3N/3tDs7zMtD+kCYf9bfutOkQ== +VmVrGQo2zRokW/ZuO9bN60xwqMjDC3vPo6L4vEr/pBaJQkw/7oJUptDcMB6Fr5Cj4S+qQ9QZRL24FoS5lS3+qQ== +VmVrGQo2zRokW/ZuO9bN61G/tYB+vcuOiiaR8UbkscqXrSG+YeXqyTcHAiaZKnMP/R8AE4EFWhpiT+I3n+NNhw== +VmVrGQo2zRokW/ZuO9bN6zDu/0cPxO8iIAxo/MyAOyO3mi+GHsPcbGU9H8sA9FWz +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm5Kes26rz2vkjDRpZY9y8CG+MKLyqwcOLQaC8uFAcQhA== +VmVrGQo2zRokW/ZuO9bN64ApqCXcODvD57AL9oh19dsQTwsNrJDaw5mMC3iBkcYF +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkmssm/VPICxxxORTX5OIKSeBFVbEgJs/oO6D+R5qX3b7w== +VmVrGQo2zRokW/ZuO9bN69/FjVndCR4PL8NhQnFk2Gew+8ZLbD2plI32rzUojOkk +VmVrGQo2zRokW/ZuO9bN69IgslanOR1qBWJ7u9f8kApHl7m/3x/YUX5TmvRhoWWrhrEvRs3e6Vo03iY4FZoGJQ== +VmVrGQo2zRokW/ZuO9bN6yAU/fW49cRu9QdgvjUnDRs= +VmVrGQo2zRokW/ZuO9bN69qqX19RJUeMAQ5O8TuJCvc= +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN6w7SLxNKr+6YhBMvtqYtIS5B39SOQ9awYMdHGNN7g2HF +VmVrGQo2zRokW/ZuO9bN61ftGV3vPtB2WjF6YrN/0jNHYRNif5kQHZWzQEGp4Mrw +VmVrGQo2zRokW/ZuO9bN68i9AQUAyLa5Izsu2k/9DPtGLI71suphKNgMpp1nHjp3 +VmVrGQo2zRokW/ZuO9bN60FmyZq9YoZep5W4V0xLs2VJw4EwR4WqghfwBKZHKkzFW5QCRYRsMStsmunHScuj3A== +VmVrGQo2zRokW/ZuO9bN6zw+wLmacHPHBj00bNmIGZaQr03AAq32yLrsoy6EYs3C +VmVrGQo2zRokW/ZuO9bN67c57efC+E7a6G1EN/4VXAUA5IVAU8G/FAp8AnNhV+8pDSYVAZUG09Nqr+7c8CWyvg== +VmVrGQo2zRokW/ZuO9bN6x9SPCVoe4PZoqZ6VqYN93GZvfLrMfeSjpwDGE6Mez3z +VmVrGQo2zRokW/ZuO9bN6yf4ml6i++/GqH/wSBe2JUimuAJuO3PPz0XEqqC+kEwmq2P2CCy/D5JDk3aKmyunXg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yZnMkqRvl1W1z1JA43ooyLsysqnBW0cSn//FgR0VFUn +VmVrGQo2zRokW/ZuO9bN6zw+wLmacHPHBj00bNmIGZaQr03AAq32yLrsoy6EYs3C +VmVrGQo2zRokW/ZuO9bN66U3/6225IbLyIW0FVhZRNEyLZurMXbjR98/i0DXR7zf +VmVrGQo2zRokW/ZuO9bN6xaf2vdYGOU85+dskGN5Zti8/wz7YWu3y/jTkefW+oHw +VmVrGQo2zRokW/ZuO9bN6xkyZls2bS2c0K0h3aqZRdZrjes6+0hX3Wr6T+0mnqitWU1y3v2I2Wt4PYH0XBjT6w== +VmVrGQo2zRokW/ZuO9bN62zrDNonCkxA5BeQHJAPUOvVeRTu47AJZhJxjTJXUg6D +QPiJZBcZo3Zu5kDhrrGzbI3GQxTKN1tlSYOd4wqk4Qk= +o6QhOIN2Sc4SHELnst17uYG57hDyRaN+v5d46f3m9TtvuM1edZME3sYQm1fwKXSE +1V2v8QerKOmubvSxgB4eTM+LVbDQYpuIwwcEKVLExe6MC8BJb9x5uoyJJlFncEM6rY6CjjYEoI1lGYiKE1nBng== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +o6QhOIN2Sc4SHELnst17ucZEhQfEUee5E6C4gEbgFRMehTYHPr1sbQ4k/qB7LJ8r1OhV8ece9Zkwgx68RDZgODP4asgDCZmEmIilOSuj+JI= +ACEEdgL/kNbx7RaZcSRxZClHBggmOTwuoukB6MgtasIiEFle6fCvd15573nsLbI9 +VmVrGQo2zRokW/ZuO9bN67Pocv3BMn87TAB+S8aqZRSL3g30PRHmEkGas7a6CXo4 +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN61efkqHAT/xspGlYy7KhUnGfupbfN6kadJV1FzZSIvpa +VmVrGQo2zRokW/ZuO9bN6+b0SyPGp1VLLfFreWS32CdvJQ0lilsNKGrnKkyex2l6ZoEN1nB9tzGcgrcEFt4mWg== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6zpJcGQDok2bzZbwZ5+2z7FMKwUs1r6SjvZQDsl9E9LF +VmVrGQo2zRokW/ZuO9bN65HmUSl6PKrPLdi3pIfWJgEYDxdXHdYYYJAWxRhWjvkMURIUYeBTgS8tgmIZ7YENDxgg1Zs4OdrlyVu+VDCE+oc= +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN65HmUSl6PKrPLdi3pIfWJgGfllFXAtMFaX/YohJyQyfhm+vA31HUxmdUtlkayzAZUKQbFld81ekbiffgRsrQqQB1xVb1v/rxv/O8tBET/TaPCdwsyclhJhXcaG/6gaDkZg== +L0eUthVnpkGsmKFAX6d+uA6wgjrZ2VgEjfvSiyQz6oysGP6R543HWJTxMrxe6FdC +6ZPJI/HSoc4xA2zncU65FgoTZnuGr10wlo0fDu1cdoU= diff --git a/class/safe_warning/sw_alias_ls_rm.py b/class/safe_warning/sw_alias_ls_rm.py index 3ce06416..b1d3b31b 100644 --- a/class/safe_warning/sw_alias_ls_rm.py +++ b/class/safe_warning/sw_alias_ls_rm.py @@ -28,11 +28,11 @@ def check_run(): if not os.path.exists(cfile): return True, 'Risk-free' conf = public.readFile(cfile) - # rep1 = 'alias(\s*)ls(\s*)=(\s*)[\'\"]ls(\s*)-.*[alh].*[alh].*[alh]' + # rep1 = 'alias(\\s*)ls(\\s*)=(\\s*)[\'\"]ls(\\s*)-.*[alh].*[alh].*[alh]' # tmp1 = re.search(rep1, conf) # if not tmp1: # result_list.append('ls') - rep2 = 'alias(\s*)rm(\s*)=(\s*)[\'\"]rm(\s*)-.*[i?].*' + rep2 = 'alias(\\s*)rm(\\s*)=(\\s*)[\'\"]rm(\\s*)-.*[i?].*' tmp2 = re.search(rep2, conf) if not tmp2: result_list.append('rm') diff --git a/class/safe_warning/sw_audit_log_keep.py b/class/safe_warning/sw_audit_log_keep.py index 36c8e7a6..90d70b5f 100644 --- a/class/safe_warning/sw_audit_log_keep.py +++ b/class/safe_warning/sw_audit_log_keep.py @@ -23,7 +23,7 @@ def check_run(): return False, 'Risky,The auditd audit tool is not installed' result = public.ReadFile(cfile) # 默认是rotate,日志满了后循环日志,keep_logs会保留旧日志 - rep = 'max_log_file_action\s*=\s(.*)' + rep = r'max_log_file_action\s*=\s(.*)' tmp = re.search(rep, result) if tmp: if 'keep_logs'.lower() == tmp.group(1).lower(): diff --git a/class/safe_warning/sw_bashrc.py b/class/safe_warning/sw_bashrc.py index a6129703..ace926a0 100644 --- a/class/safe_warning/sw_bashrc.py +++ b/class/safe_warning/sw_bashrc.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_cshrc.py b/class/safe_warning/sw_cshrc.py index 24c681a0..ef7ce1a7 100644 --- a/class/safe_warning/sw_cshrc.py +++ b/class/safe_warning/sw_cshrc.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_cve_2019_5736.py b/class/safe_warning/sw_cve_2019_5736.py index 19ed04a8..f25b9599 100644 --- a/class/safe_warning/sw_cve_2019_5736.py +++ b/class/safe_warning/sw_cve_2019_5736.py @@ -24,7 +24,7 @@ def check_run(): docker = public.ExecShell("docker version --format=\'{{ .Server.Version }}\'")[0].strip() if 'command not found' in docker or 'Command not found' in docker: return True, 'Risk-free,docker is not installed' - if not re.search('\d+.\d+.\d+', docker): + if not re.search(r'\d+.\d+.\d+', docker): return True, 'Risk-free' docker = docker.split('.') if len(docker[0]) < 2: diff --git a/class/safe_warning/sw_cve_2021_4034.py b/class/safe_warning/sw_cve_2021_4034.py index 02bed4c7..37620303 100644 --- a/class/safe_warning/sw_cve_2021_4034.py +++ b/class/safe_warning/sw_cve_2021_4034.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- @@ -32,7 +32,7 @@ def check_run(): ''' @name CVE-2021-4034 polkit pkexec 本地提权漏洞检测 @time 2022-08-12 - @author lkq@bt.cn + @author lkq@aapanel.com ''' st = os.stat('/usr/bin/pkexec') diff --git a/class/safe_warning/sw_database_backup.py b/class/safe_warning/sw_database_backup.py index 45107b75..c88b3d48 100644 --- a/class/safe_warning/sw_database_backup.py +++ b/class/safe_warning/sw_database_backup.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_database_priv.py b/class/safe_warning/sw_database_priv.py index 98350b92..51657e5a 100644 --- a/class/safe_warning/sw_database_priv.py +++ b/class/safe_warning/sw_database_priv.py @@ -1,9 +1,9 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: linxiao # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_debug_mode.py b/class/safe_warning/sw_debug_mode.py index 2272c99d..d7e71b5a 100644 --- a/class/safe_warning/sw_debug_mode.py +++ b/class/safe_warning/sw_debug_mode.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_dir_mode.py b/class/safe_warning/sw_dir_mode.py index 69e5e8b8..c9199b86 100644 --- a/class/safe_warning/sw_dir_mode.py +++ b/class/safe_warning/sw_dir_mode.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_docker_api.py b/class/safe_warning/sw_docker_api.py index 5d2b1f15..ee54217d 100644 --- a/class/safe_warning/sw_docker_api.py +++ b/class/safe_warning/sw_docker_api.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- @@ -45,7 +45,7 @@ def check_run(): ''' @name 面板登录告警是否开启 @time 2022-08-12 - @author lkq@bt.cn + @author lkq@aapanel.com ''' try: if os.path.exists("/lib/systemd/system/docker.service"): diff --git a/class/safe_warning/sw_file_lock.py b/class/safe_warning/sw_file_lock.py index 9a2f331e..7b7f5f8e 100644 --- a/class/safe_warning/sw_file_lock.py +++ b/class/safe_warning/sw_file_lock.py @@ -28,14 +28,14 @@ def check_run(): # 执行lsattr -l查看文件特殊属性,若存在特殊属性,则判断是否为“追加属性”,若为否,则加入到result_list,最终显示到面板中 for tl1 in tmp_list1: if not "Append_Only" in tl1: - log1 = re.search('.*?\s', tl1) + log1 = re.search(r'.*?\s', tl1) result_list.append(log1.group().strip()) result_str2 = public.ExecShell('lsattr -l /etc/passwd /etc/shadow /etc/group /etc/gshadow')[0].strip() tmp_list2 = result_str2.split('\n') # immutable判断是否为锁属性 for tl2 in tmp_list2: if not "Immutable" in tl2: - log2 = re.search('.*?\s', tl2) + log2 = re.search(r'.*?\s', tl2) result_list.append(log2.group().strip()) if result_list: return False, '以下文件未配置适当的底层属性:{}'.format('、'.join(result_list)) diff --git a/class/safe_warning/sw_file_mod.py b/class/safe_warning/sw_file_mod.py index a61babf0..3064a2c9 100644 --- a/class/safe_warning/sw_file_mod.py +++ b/class/safe_warning/sw_file_mod.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_files_recycle_bin.py b/class/safe_warning/sw_files_recycle_bin.py index 0a197ec8..1192ad7e 100644 --- a/class/safe_warning/sw_files_recycle_bin.py +++ b/class/safe_warning/sw_files_recycle_bin.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_firewall_open.py b/class/safe_warning/sw_firewall_open.py index 26058da7..c804a831 100644 --- a/class/safe_warning/sw_firewall_open.py +++ b/class/safe_warning/sw_firewall_open.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_ftp_login.py b/class/safe_warning/sw_ftp_login.py index dea78dda..b080de46 100644 --- a/class/safe_warning/sw_ftp_login.py +++ b/class/safe_warning/sw_ftp_login.py @@ -21,7 +21,7 @@ def check_run(): try: info_data = public.ReadFile('/www/server/pure-ftpd/etc/pure-ftpd.conf') if info_data: - if re.search('.*NoAnonymous\s*yes', info_data): + if re.search(r'.*NoAnonymous\s*yes', info_data): return True, 'Risk-free' else: return False, 'Currently pure-ftpd does not disable anonymous login, modify/add the value of NoAnonymous to yes in the [pure-ftpd.conf] file' diff --git a/class/safe_warning/sw_ftp_pass.py b/class/safe_warning/sw_ftp_pass.py index 4c0e524d..d8e4bf7e 100644 --- a/class/safe_warning/sw_ftp_pass.py +++ b/class/safe_warning/sw_ftp_pass.py @@ -1,9 +1,9 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: linxiao # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_kernel_space.py b/class/safe_warning/sw_kernel_space.py index 11507463..8b7cd6f0 100644 --- a/class/safe_warning/sw_kernel_space.py +++ b/class/safe_warning/sw_kernel_space.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_login_message.py b/class/safe_warning/sw_login_message.py index be981c0f..1061cedc 100644 --- a/class/safe_warning/sw_login_message.py +++ b/class/safe_warning/sw_login_message.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_login_user.py b/class/safe_warning/sw_login_user.py index 274bf5b9..c2189e41 100644 --- a/class/safe_warning/sw_login_user.py +++ b/class/safe_warning/sw_login_user.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_memcached_port.py b/class/safe_warning/sw_memcached_port.py index 1cefc107..df1f8899 100644 --- a/class/safe_warning/sw_memcached_port.py +++ b/class/safe_warning/sw_memcached_port.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_mysql_pass.py b/class/safe_warning/sw_mysql_pass.py index c57484ac..37756e82 100644 --- a/class/safe_warning/sw_mysql_pass.py +++ b/class/safe_warning/sw_mysql_pass.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- @@ -29,7 +29,7 @@ def check_run(): ''' @name Mysql 弱口令检测 @time 2022-08-12 - @author lkq@bt.cn + @author lkq@aapanel.com ''' pass_info = public.ReadFile("/www/server/panel/config/weak_pass.txt") if not pass_info: return True, 'Risk-free' diff --git a/class/safe_warning/sw_mysql_port.py b/class/safe_warning/sw_mysql_port.py index 06ac8688..f8b9637a 100644 --- a/class/safe_warning/sw_mysql_port.py +++ b/class/safe_warning/sw_mysql_port.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_mysql_priv.py b/class/safe_warning/sw_mysql_priv.py index 9b529c17..c76a584d 100644 --- a/class/safe_warning/sw_mysql_priv.py +++ b/class/safe_warning/sw_mysql_priv.py @@ -1,9 +1,9 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: linxiao # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_nginx_server.py b/class/safe_warning/sw_nginx_server.py index 13ba7615..969d765b 100644 --- a/class/safe_warning/sw_nginx_server.py +++ b/class/safe_warning/sw_nginx_server.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_panel_control.py b/class/safe_warning/sw_panel_control.py index 8de58d04..dc600fb7 100644 --- a/class/safe_warning/sw_panel_control.py +++ b/class/safe_warning/sw_panel_control.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- @@ -28,7 +28,7 @@ def check_run(): ''' @name 面板未开启监控 @time 2022-08-12 - @author lkq@bt.cn + @author lkq@aapanel.com ''' global _tips send_type = "" diff --git a/class/safe_warning/sw_panel_pass.py b/class/safe_warning/sw_panel_pass.py index 0fe48580..ec111b2c 100644 --- a/class/safe_warning/sw_panel_pass.py +++ b/class/safe_warning/sw_panel_pass.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_panel_path.py b/class/safe_warning/sw_panel_path.py index 5a1b52fc..e57fdc33 100644 --- a/class/safe_warning/sw_panel_path.py +++ b/class/safe_warning/sw_panel_path.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_panel_port.py b/class/safe_warning/sw_panel_port.py index 94129830..6a346b06 100644 --- a/class/safe_warning/sw_panel_port.py +++ b/class/safe_warning/sw_panel_port.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_panel_swing.py b/class/safe_warning/sw_panel_swing.py index 0d8ddb3f..e166914b 100644 --- a/class/safe_warning/sw_panel_swing.py +++ b/class/safe_warning/sw_panel_swing.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- @@ -28,7 +28,7 @@ def check_run(): ''' @name 面板登录告警是否开启 @time 2022-08-12 - @author lkq@bt.cn + @author lkq@aapanel.com ''' send_type = "" tip_files = ['panel_login_send.pl','login_send_type.pl','login_send_mail.pl','login_send_dingding.pl'] diff --git a/class/safe_warning/sw_php_disable_functions.py b/class/safe_warning/sw_php_disable_functions.py index 4e404ced..b4ffed7f 100644 --- a/class/safe_warning/sw_php_disable_functions.py +++ b/class/safe_warning/sw_php_disable_functions.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_php_expose.py b/class/safe_warning/sw_php_expose.py index de6d8932..7272dd11 100644 --- a/class/safe_warning/sw_php_expose.py +++ b/class/safe_warning/sw_php_expose.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_ping.py b/class/safe_warning/sw_ping.py index dc23d239..358cf7ad 100644 --- a/class/safe_warning/sw_ping.py +++ b/class/safe_warning/sw_ping.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_pingin.py b/class/safe_warning/sw_pingin.py index 5e0bb2dd..e6974f29 100644 --- a/class/safe_warning/sw_pingin.py +++ b/class/safe_warning/sw_pingin.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_redis_pass.py b/class/safe_warning/sw_redis_pass.py index 11ad179a..6c29b3ba 100644 --- a/class/safe_warning/sw_redis_pass.py +++ b/class/safe_warning/sw_redis_pass.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_redis_port.py b/class/safe_warning/sw_redis_port.py index f9372705..ab838433 100644 --- a/class/safe_warning/sw_redis_port.py +++ b/class/safe_warning/sw_redis_port.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_site_spath.py b/class/safe_warning/sw_site_spath.py index 152e74aa..cdc34c97 100644 --- a/class/safe_warning/sw_site_spath.py +++ b/class/safe_warning/sw_site_spath.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_site_ssl.py b/class/safe_warning/sw_site_ssl.py index caf5f0fc..1c52f7a3 100644 --- a/class/safe_warning/sw_site_ssl.py +++ b/class/safe_warning/sw_site_ssl.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_site_ssl_expire.py b/class/safe_warning/sw_site_ssl_expire.py index 34942f17..bbc2b3ee 100644 --- a/class/safe_warning/sw_site_ssl_expire.py +++ b/class/safe_warning/sw_site_ssl_expire.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_ssh_clientalive.py b/class/safe_warning/sw_ssh_clientalive.py index 84862d1d..51fe207e 100644 --- a/class/safe_warning/sw_ssh_clientalive.py +++ b/class/safe_warning/sw_ssh_clientalive.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- @@ -40,8 +40,8 @@ def check_run(): try: info_data=public.ReadFile('/etc/ssh/sshd_config') if info_data: - if re.search('ClientAliveInterval\s+\d+',info_data): - clientalive=re.findall('ClientAliveInterval\s+\d+',info_data)[0] + if re.search(r'ClientAliveInterval\s+\d+',info_data): + clientalive=re.findall(r'ClientAliveInterval\s+\d+',info_data)[0] #clientalive 需要大于600 小于900 if int(clientalive.split(' ')[1]) >= 600 and int(clientalive.split(' ')[1]) <= 900: return True,'Rick-free' diff --git a/class/safe_warning/sw_ssh_forward.py b/class/safe_warning/sw_ssh_forward.py index 90b76e8e..47c913e7 100644 --- a/class/safe_warning/sw_ssh_forward.py +++ b/class/safe_warning/sw_ssh_forward.py @@ -20,7 +20,7 @@ def check_run(): if not os.path.exists(conf): return True, 'Risk-free' result = public.ReadFile(conf) - rep = '.*?X11Forwarding\s*?yes' + rep = r'.*?X11Forwarding\s*?yes' tmp = re.search(rep, result) if tmp: if tmp.group()[0] == '#': diff --git a/class/safe_warning/sw_ssh_maxauth.py b/class/safe_warning/sw_ssh_maxauth.py index 5b3a03e1..65f2f40a 100644 --- a/class/safe_warning/sw_ssh_maxauth.py +++ b/class/safe_warning/sw_ssh_maxauth.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- @@ -40,8 +40,8 @@ def check_run(): try: info_data = public.ReadFile('/etc/ssh/sshd_config') if info_data: - if re.search('MaxAuthTries\s+\d+', info_data): - maxauth = re.findall('MaxAuthTries\s+\d+', info_data)[0] + if re.search(r'MaxAuthTries\s+\d+', info_data): + maxauth = re.findall(r'MaxAuthTries\s+\d+', info_data)[0] # max 需要大于3 小于6 if int(maxauth.split(' ')[1]) >= 3 and int(maxauth.split(' ')[1]) <= 6: return True, 'Rick-free' diff --git a/class/safe_warning/sw_ssh_minclass.py b/class/safe_warning/sw_ssh_minclass.py index c7ef2a58..f7790a9d 100644 --- a/class/safe_warning/sw_ssh_minclass.py +++ b/class/safe_warning/sw_ssh_minclass.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- @@ -34,7 +34,7 @@ def check_run(): p_file = '/etc/security/pwquality.conf' p_body = public.readFile(p_file) if not p_body: return True, 'Risk-free' - tmp = re.findall("\s*minclass\s+=\s+(.+)", p_body, re.M) + tmp = re.findall(r"\s*minclass\s+=\s+(.+)", p_body, re.M) if not tmp: return True, 'Risk-free' minlen = tmp[0].strip() if int(minlen) <3: diff --git a/class/safe_warning/sw_ssh_notpass.py b/class/safe_warning/sw_ssh_notpass.py index 512f9fc9..07257536 100644 --- a/class/safe_warning/sw_ssh_notpass.py +++ b/class/safe_warning/sw_ssh_notpass.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: lkq +# Author: lkq # ------------------------------------------------------------------- # Time: 2022-08-10 # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_ssh_passmax.py b/class/safe_warning/sw_ssh_passmax.py index ece46e5d..a1f008a0 100644 --- a/class/safe_warning/sw_ssh_passmax.py +++ b/class/safe_warning/sw_ssh_passmax.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- @@ -34,7 +34,7 @@ def check_run(): p_file = '/etc/login.defs' p_body = public.readFile(p_file) if not p_body: return True, 'Risk-free' - tmp = re.findall("\nPASS_MAX_DAYS\s+(.+)", p_body, re.M) + tmp = re.findall("\nPASS_MAX_DAYS\\s+(.+)", p_body, re.M) if not tmp: return True, 'Risk-free' maxdays = tmp[0].strip() #60-180之间 diff --git a/class/safe_warning/sw_ssh_passmin.py b/class/safe_warning/sw_ssh_passmin.py index 32347915..f7c0465a 100644 --- a/class/safe_warning/sw_ssh_passmin.py +++ b/class/safe_warning/sw_ssh_passmin.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- @@ -34,7 +34,7 @@ def check_run(): p_file = '/etc/login.defs' p_body = public.readFile(p_file) if not p_body: return True, 'Risk-free' - tmp = re.findall("\nPASS_MIN_DAYS\s+(.+)", p_body, re.M) + tmp = re.findall("\nPASS_MIN_DAYS\\s+(.+)", p_body, re.M) if not tmp: return True, 'Risk-free' maxdays = tmp[0].strip() #7-14 diff --git a/class/safe_warning/sw_ssh_port.py b/class/safe_warning/sw_ssh_port.py index 99aafbaf..6e0ef578 100644 --- a/class/safe_warning/sw_ssh_port.py +++ b/class/safe_warning/sw_ssh_port.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- @@ -49,7 +49,7 @@ def check_run(): version = public.readFile('/etc/redhat-release') if not version: - version = public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace('\l','').strip() + version = public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace(r'\l','').strip() else: version = version.replace('release ','').replace('Linux','').replace('(Core)','').strip() diff --git a/class/safe_warning/sw_ssh_root.py b/class/safe_warning/sw_ssh_root.py index 90da797d..e3898507 100644 --- a/class/safe_warning/sw_ssh_root.py +++ b/class/safe_warning/sw_ssh_root.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- @@ -36,7 +36,7 @@ def check_run(): try: info_data = public.ReadFile('/etc/ssh/sshd_config') if info_data: - if re.search('PermitRootLogin\s+no', info_data): + if re.search(r'PermitRootLogin\s+no', info_data): return True, 'Risk-free' else: return True, 'Risk-free' diff --git a/class/safe_warning/sw_ssh_security.py b/class/safe_warning/sw_ssh_security.py index d845af29..93bb76d9 100644 --- a/class/safe_warning/sw_ssh_security.py +++ b/class/safe_warning/sw_ssh_security.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- @@ -35,7 +35,7 @@ def check_run(): p_file = '/etc/security/pwquality.conf' p_body = public.readFile(p_file) if not p_body: return True, 'Risk-free' - tmp = re.findall("\s*minlen\s+=\s+(.+)", p_body, re.M) + tmp = re.findall(r"\s*minlen\s+=\s+(.+)", p_body, re.M) if not tmp: return True, 'Risk-free' minlen = tmp[0].strip() if int(minlen) < 9: diff --git a/class/safe_warning/sw_umask.py b/class/safe_warning/sw_umask.py index 6393083e..e1ce9284 100644 --- a/class/safe_warning/sw_umask.py +++ b/class/safe_warning/sw_umask.py @@ -1,11 +1,11 @@ #!/usr/bin/python # coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/safe_warning/sw_waf_install.py b/class/safe_warning/sw_waf_install.py index d1ea4cdd..c5008946 100644 --- a/class/safe_warning/sw_waf_install.py +++ b/class/safe_warning/sw_waf_install.py @@ -1,11 +1,11 @@ #!/usr/bin/python #coding: utf-8 # ------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel # ------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang # ------------------------------------------------------------------- # ------------------------------------------------------------------- diff --git a/class/san_baseline.py b/class/san_baseline.py index 0124e45a..01af82e3 100644 --- a/class/san_baseline.py +++ b/class/san_baseline.py @@ -327,7 +327,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s expose_php的值修改为Off中修改" % (php_path + i + '/etc/php.ini'), "repair": "expose_php = Off", "rule": [ - {"re": "\nexpose_php\s*=\s*(\w+)", "check": {"type": "string", "value": ['Off']}}] + {"re": "\nexpose_php\\s*=\\s*(\\w+)", "check": {"type": "string", "value": ['Off']}}] } if not self.check_san_baseline(php_data): ret.append(php_data) @@ -354,7 +354,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s 中 disable_functions= 修改成如下:" % (php_path + i + '/etc/php.ini'), "repair": "disable_functions = passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv", "rule": [ - {"re": "\ndisable_functions\s?=\s?(.+)", "check": {"type": "string", "value": [ + {"re": "\ndisable_functions\\s?=\\s?(.+)", "check": {"type": "string", "value": [ 'passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv']}}] } if not self.check_san_baseline(php_data): @@ -402,7 +402,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s expose_php的值修改为Off中修改" % (php_path + i + '/etc/php.ini'), "repair": "expose_php = Off", "rule": [ - {"re": "\nexpose_php\s*=\s*(\w+)", "check": {"type": "string", "value": ['Off']}}] + {"re": "\nexpose_php\\s*=\\s*(\\w+)", "check": {"type": "string", "value": ['Off']}}] } if not self.check_san_baseline(php_data): ret.append(php_data) @@ -422,7 +422,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s 中 disable_functions= 修改成如下:" % (php_path + i + '/etc/php.ini'), "repair": "disable_functions = passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv", "rule": [ - {"re": "\ndisable_functions\s?=\s?(.+)", "check": {"type": "string", "value": [ + {"re": "\ndisable_functions\\s?=\\s?(.+)", "check": {"type": "string", "value": [ 'passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv']}}] } if not self.check_san_baseline(php_data): @@ -460,7 +460,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s 中的监听IP设置为127.0.0.1 例如" % ('/www/server/redis/redis.conf'), "repair": "bind 127.0.0.1", "rule": [ - {"re": "\nbind\s*(.+)", "check": {"type": "string", "value": ['0.0.0.0']}}] + {"re": "\nbind\\s*(.+)", "check": {"type": "string", "value": ['0.0.0.0']}}] } if self.check_san_baseline(redis_server_ip): ret.append(redis_server_ip) @@ -478,7 +478,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s 中的为未设置密码 例如" % ('/www/server/redis/redis.conf'), "repair": "requirepass requirepassQWERQQQQQQQ", "rule": [ - {"re": "\nrequirepass\s*(.+)", "check": {"type": "string", "value": []}}] + {"re": "\nrequirepass\\s*(.+)", "check": {"type": "string", "value": []}}] } if not self.check_san_baseline(redis_server_not_pass): ret.append(redis_server_not_pass) @@ -496,7 +496,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s 中requirepass 设置为强密码" % ('/www/server/redis/redis.conf'), "repair": "requirepass requirepassQWERQQQQQQQ", "rule": [ - {"re": "\nrequirepass\s*(.+)", "check": {"type": "string", "value": ['123456', 'admin', 'damin888']}}] + {"re": "\nrequirepass\\s*(.+)", "check": {"type": "string", "value": ['123456', 'admin', 'damin888']}}] } if not self.check_san_baseline(redis_server_pass): ret.append(redis_server_pass) @@ -534,7 +534,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s 中的监听IP设置为127.0.0.1 例如" % ('/etc/init.d/memcached'), "repair": "IP=127.0.0.1", "rule": [ - {"re": "\nIP\s?=\s?(.+)", "check": {"type": "string", "value": ['0.0.0.0']}}] + {"re": "\nIP\\s?=\\s?(.+)", "check": {"type": "string", "value": ['0.0.0.0']}}] } if self.check_san_baseline(self.__repair['46']): ret.append(self.__repair['46']) @@ -629,7 +629,7 @@ class san_baseline: "name": "存在非root 的管理员用户(危险)", "ps": "除root以为的其他的UID为0的用户的应该删除。或者为其分配新的UID", "cmd": '''cat /etc/passwd | awk -F: '($3 == 0) { print $1 }'|grep -v '^root$' ''', - "find": {"re": "\w+"} + "find": {"re": r"\w+"} } if not self.check_san_baseline(get_root_0): result.append(get_root_0) @@ -845,7 +845,7 @@ class san_baseline: tls = [] if os.path.exists('/www/server/panel/vhost/nginx/%s.conf' % siteName): ret = public.ReadFile('/www/server/panel/vhost/nginx/%s.conf' % siteName) - valuse = re.findall('ssl_protocols\s+(.+)', ret) + valuse = re.findall(r'ssl_protocols\s+(.+)', ret) print(valuse) if not valuse: return tls if not valuse[0]: return tls @@ -1071,7 +1071,7 @@ class san_baseline: "Suggestions": "加固建议, 在%s expose_php的值修改为Off中修改" % ('/www/server/nginx/conf/nginx.conf'), "repair": "expose_php = Off", "rule": [ - {"re": "server_tokens\s*(.+)", "check": {"type": "string", "value": ['off;']}}] + {"re": r"server_tokens\s*(.+)", "check": {"type": "string", "value": ['off;']}}] } if not self.check_san_baseline(Nginx_Get_version): ret.append(Nginx_Get_version) diff --git a/class/send_mail.py b/class/send_mail.py index 8e2180b6..2c930a60 100644 --- a/class/send_mail.py +++ b/class/send_mail.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x3 +# | aaPanel x3 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: lkqiang +# | Author: lkqiang # +------------------------------------------------------------------- # +-------------------------------------------------------------------- # | 宝塔内置消息通道 @@ -181,9 +181,9 @@ class send_mail: opener = urllib2.urlopen(url) m_str = opener.read() if isinstance(m_str, bytes): - ipaddress = re.search('\d+.\d+.\d+.\d+', m_str.decode('utf-8')).group(0) + ipaddress = re.search(r'\d+.\d+.\d+.\d+', m_str.decode('utf-8')).group(0) else: - ipaddress = re.search('\d+.\d+.\d+.\d+', m_str).group(0) + ipaddress = re.search(r'\d+.\d+.\d+.\d+', m_str).group(0) public.WriteFile(filename, ipaddress) c_ip = public.check_ip(ipaddress) if not c_ip: diff --git a/class/send_to_user.py b/class/send_to_user.py index 3dcf9be6..f1daeb74 100644 --- a/class/send_to_user.py +++ b/class/send_to_user.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x3 +# | aaPanel x3 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: lkq <1249648969@qq.com> +# | Author: lkq <1249648969@aapanel.com> # +------------------------------------------------------------------- # +-------------------------------------------------------------------- # | 告警消息队列 diff --git a/class/setPanelLets.py b/class/setPanelLets.py index 971baa51..859a12ea 100644 --- a/class/setPanelLets.py +++ b/class/setPanelLets.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: zhwen +# | Author: zhwen # +------------------------------------------------------------------- import os os.chdir("/www/server/panel") diff --git a/class/sewer/cli.py b/class/sewer/cli.py index 542f7b73..5d5d0c9f 100644 --- a/class/sewer/cli.py +++ b/class/sewer/cli.py @@ -8,7 +8,7 @@ from .config import ACME_DIRECTORY_URL_STAGING, ACME_DIRECTORY_URL_PRODUCTION def main(): - """ + r""" Usage: 1. To get a new certificate: CLOUDFLARE_EMAIL=example@example.com \ @@ -29,7 +29,7 @@ def main(): """ parser = argparse.ArgumentParser( prog="sewer", - description="""Sewer is a Let's Encrypt(ACME) client. + description=r"""Sewer is a Let's Encrypt(ACME) client. Example usage:: CLOUDFLARE_EMAIL=example@example.com \ CLOUDFLARE_API_KEY=api-key \ diff --git a/class/site_dir_auth.py b/class/site_dir_auth.py index c861b5d2..475cb078 100644 --- a/class/site_dir_auth.py +++ b/class/site_dir_auth.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zhwen +# Author: zhwen #------------------------------------------------------------------- #------------------------------ @@ -126,13 +126,13 @@ class SiteDirAuth: try: conf = public.readFile(self.setup_path + '/panel/vhost/'+public.get_webserver()+'/'+siteName+'.conf'); if public.get_webserver() == 'nginx': - rep = "enable-php-(\w{2,5})\.conf" + rep = r"enable-php-(\w{2,5})\.conf" tmp = re.search(rep,conf) if not tmp: - rep = "enable-php-(\d+-wpfastcgi).conf" + rep = r"enable-php-(\d+-wpfastcgi).conf" re.search(rep, conf) else: - rep = "php-cgi-(\w{2,5})\.sock" + rep = r"php-cgi-(\w{2,5})\.sock" tmp = re.search(rep,conf).groups() if tmp: return tmp[0] @@ -216,24 +216,24 @@ class SiteDirAuth: conf = public.readFile(file) if i == "apache": if act == "create": - rep = "IncludeOptional.*\/dir_auth\/.*conf(\n|.)+<\/VirtualHost>" + rep = "IncludeOptional.*\\/dir_auth\\/.*conf(\n|.)+<\\/VirtualHost>" rep1 = "" if not re.search(rep, conf): conf = conf.replace(rep1, "\n\t#Directory protection rules, do not manually delete\n\tIncludeOptional {}\n".format( dir_auth_file)) else: - rep = "\n*#Directory protection rules, do not manually delete\n+\s+IncludeOptional[\s\w\/\.\*]+" + rep = "\n*#Directory protection rules, do not manually delete\n+\\s+IncludeOptional[\\s\\w\\/\\.\\*]+" conf = re.sub(rep, '', conf) public.writeFile(file, conf) else: if act == "create": - rep = "#SSL-END(\n|.)+include.*\/dir_auth\/.*conf;" + rep = "#SSL-END(\n|.)+include.*\\/dir_auth\\/.*conf;" rep1 = "#SSL-END" if not re.search(rep,conf): conf = conf.replace(rep1, rep1 + "\n\t#Directory protection rules, do not manually delete\n\tinclude {};".format(dir_auth_file)) else: - rep = "\n*#Directory protection rules, do not manually delete\n+\s+include[\s\w\/\.\*]+;" + rep = "\n*#Directory protection rules, do not manually delete\n+\\s+include[\\s\\w\\/\\.\\*]+;" conf = re.sub(rep, '', conf) public.writeFile(file, conf) @@ -338,7 +338,7 @@ class SiteDirAuth: password = get.password.strip() if len(password) < 3: return public.returnMsg(False, 'Password cannot be less than 3 characters') - if re.search('\s', password): + if re.search(r'\s', password): return public.returnMsg(False, 'Password cannot contain spaces') values['password'] = password @@ -348,7 +348,7 @@ class SiteDirAuth: username = get.username.strip() if len(username) < 3: return public.returnMsg(False, 'Username cannot be less than 3 characters') - if re.search('\s', username): + if re.search(r'\s', username): return public.returnMsg(False, 'Username cannot contain spaces') values['username'] = username @@ -358,10 +358,10 @@ class SiteDirAuth: name = get.name.strip() if len(name) < 3: return public.returnMsg(False, 'Name cannot be less than 3 characters') - if re.search('\s', name): + if re.search(r'\s', name): return public.returnMsg(False, 'Name cannot contain spaces') - if re.search('[\/\"\'\!@#$%^&*()+={}\[\]\:\;\?><,./\\\]+', name): + if re.search('[\\/\"\'\\!@#$%^&*()+={}\\[\\]\\:\\;\\?><,./\\\\]+', name): return public.returnMsg(False, 'Name format must be [ aaa_bbb ]') values['name'] = name - return public.returnMsg(True, values) \ No newline at end of file + return public.returnMsg(True, values) diff --git a/class/sites.py b/class/sites.py index 1e3b3bf6..b25088da 100644 --- a/class/sites.py +++ b/class/sites.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ @@ -42,7 +42,7 @@ class sites: return self._generate_nginx_conf(pdata['siteName']) - ''' + r''' server { listen 80; @@ -142,7 +142,7 @@ server #检查域名格式是否不正确 def domain_format(self,domains): if type(domains) == str: domains = [domains] - reg = "^([\w\-\*]{1,100}\.){1,8}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$"; + reg = r"^([\w\-\*]{1,100}\.){1,8}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$"; for domain in domains: if not re.match(reg,domain): return domain return False diff --git a/class/ssh_authentication.py b/class/ssh_authentication.py index 4b77fc48..baaf87d1 100644 --- a/class/ssh_authentication.py +++ b/class/ssh_authentication.py @@ -2,7 +2,7 @@ # +------------------------------------------------------------------- # | version :1.0 # +------------------------------------------------------------------- -# | Author: 梁凯强 <1249648969@qq.com> +# | Author: 梁凯强 <1249648969@aapanel.com> # +------------------------------------------------------------------- # | SSH 双因子认证 # +-------------------------------------------------------------------- @@ -150,7 +150,7 @@ class ssh_authentication: #设置SSH应答模式 def set_ssh_login_user(self): - ssh_password = '\nChallengeResponseAuthentication\s\w+' + ssh_password = '\nChallengeResponseAuthentication\\s\\w+' file = public.readFile(self.__SSH_CONFIG) if isinstance(file, str): if len(re.findall(ssh_password, file)) == 0: @@ -164,7 +164,7 @@ class ssh_authentication: #关闭SSH应答模式 def close_ssh_login_user(self): file = public.readFile(self.__SSH_CONFIG) - ssh_password = '\nChallengeResponseAuthentication\s\w+' + ssh_password = '\nChallengeResponseAuthentication\\s\\w+' if isinstance(file, str): file_result = re.sub(ssh_password, '\nChallengeResponseAuthentication no', file) self.wirte(self.__SSH_CONFIG, file_result) @@ -174,7 +174,7 @@ class ssh_authentication: #查看SSH应答模式 def check_ssh_login_user(self): file = public.readFile(self.__SSH_CONFIG) - ssh_password = '\nChallengeResponseAuthentication\s\w+' + ssh_password = '\nChallengeResponseAuthentication\\s\\w+' if isinstance(file, str): ret = re.findall(ssh_password, file) if not ret: @@ -230,7 +230,7 @@ class ssh_authentication: 无参数传递 ''' file = public.readFile(self.__SSH_CONFIG) - ssh_password = '\nPasswordAuthentication\s\w+' + ssh_password = '\nPasswordAuthentication\\s\\w+' if isinstance(file, str): ret = re.findall(ssh_password, file) if not ret: diff --git a/class/ssh_security.py b/class/ssh_security.py index 7453da1a..ab7afb9f 100644 --- a/class/ssh_security.py +++ b/class/ssh_security.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: lkqiang +# Author: lkqiang #------------------------------------------------------------------- # SSH 安全类 #------------------------------ @@ -173,7 +173,7 @@ class ssh_security: def get_ssh_port(self): conf = public.readFile(self.__SSH_CONFIG) if not conf: conf = '' - rep = "#*Port\s+([0-9]+)\s*\n" + rep = r"#*Port\s+([0-9]+)\s*\n" tmp1 = re.search(rep,conf) port = '22' if tmp1: @@ -304,7 +304,7 @@ class ssh_security: def login_last(self): self.check_files() data=public.ExecShell('last -n 50') - data=re.findall("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",data[0]) + data=re.findall(r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",data[0]) if data>=1: data2=list(set(data)) for i in data2: @@ -316,7 +316,7 @@ class ssh_security: #获取ROOT当前登陆的IP def get_ip(self): data = public.ExecShell(''' who am i |awk ' {print $5 }' ''') - data = re.findall("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",data[0]) + data = re.findall(r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",data[0]) return data def get_logs(self, get): @@ -421,7 +421,7 @@ class ssh_security: #监控状态 def get_jian(self,get): data = public.ReadFile(self.return_profile()) - #if re.search('{}\/www\/server\/panel\/class\/ssh_security.py\s+login'.format(".*python\s+"), data): + #if re.search(r'{}\/www\/server\/panel\/class\/ssh_security.py\s+login'.format(r".*python\s+"), data): if re.search('/www/server/panel/class/ssh_security.py login', data): return public.returnMsg(True, '1') else: @@ -432,7 +432,7 @@ class ssh_security: 开启密码登陆 get: 无需传递参数 ''' - ssh_password = '\n#?PasswordAuthentication\s\w+' + ssh_password = r'\n#?PasswordAuthentication\s\w+' file = public.readFile(self.__SSH_CONFIG) if not file: return public.returnMsg(False,'ERROR: sshd config configuration file does not exist, cannot continue!') if len(re.findall(ssh_password, file)) == 0: @@ -459,13 +459,13 @@ class ssh_security: file = ['/root/.ssh/id_{}.pub'.format(s_type), '/root/.ssh/id_{}'.format(s_type)] for i in file: if os.path.exists(i): - public.ExecShell('sed -i "\~$(cat %s)~d" %s' % (file[0], authorized_keys)) + public.ExecShell(r'sed -i "\~$(cat %s)~d" %s' % (file[0], authorized_keys)) os.remove(i) os.system("ssh-keygen -t {s_type} -P '' -f /root/.ssh/id_{s_type} |echo y".format(s_type = s_type)) if os.path.exists(file[0]): public.ExecShell('cat %s >> %s && chmod 600 %s' % (file[0], authorized_keys, authorized_keys)) - rec = '\n#?RSAAuthentication\s\w+' - rec2 = '\n#?PubkeyAuthentication\s\w+' + rec = r'\n#?RSAAuthentication\s\w+' + rec2 = r'\n#?PubkeyAuthentication\s\w+' file = public.readFile(self.__SSH_CONFIG) if not file: return public.returnMsg(False, 'ERROR: sshd config configuration file does not exist, cannot continue!') @@ -474,7 +474,7 @@ class ssh_security: file_ssh = re.sub(rec, '\nRSAAuthentication yes', file) file_result = re.sub(rec2, '\nPubkeyAuthentication yes', file_ssh) if ssh == 'no': - ssh_password = '\n#?PasswordAuthentication\s\w+' + ssh_password = r'\n#?PasswordAuthentication\s\w+' if len(re.findall(ssh_password, file_result)) == 0: file_result = file_result + '\nPasswordAuthentication no' else: @@ -592,8 +592,8 @@ class ssh_security: 无需参数传递 ''' is_ssh_status=self.GetSshInfo() - rec = '\n\s*#?\s*RSAAuthentication\s+\w+' - rec2 = '\n\s*#?\s*PubkeyAuthentication\s+\w+' + rec = r'\n\s*#?\s*RSAAuthentication\s+\w+' + rec2 = r'\n\s*#?\s*PubkeyAuthentication\s+\w+' file = public.readFile(self.__SSH_CONFIG) if not file: return public.returnMsg(False,'错误:sshd_config配置文件不存在,无法继续!') file_ssh = re.sub(rec, '\nRSAAuthentication no', file) @@ -693,7 +693,7 @@ class ssh_security: 开启密码登陆 get: 无需传递参数 ''' - ssh_password = '\n\s*PermitRootLogin\s+\w+' + ssh_password = r'\n\s*PermitRootLogin\s+\w+' file = public.readFile(self.__SSH_CONFIG) if len(re.findall(ssh_password, file)) == 0: file_result = file + '\nPermitRootLogin no' @@ -710,7 +710,7 @@ class ssh_security: 无参数传递 ''' file = public.readFile(self.__SSH_CONFIG) - ssh_password = '\n#?PasswordAuthentication\s\w+' + ssh_password = r'\n#?PasswordAuthentication\s\w+' file_result = re.sub(ssh_password, '\nPasswordAuthentication no', file) self.wirte(self.__SSH_CONFIG, file_result) self.restart_ssh() diff --git a/class/ssh_terminal.py b/class/ssh_terminal.py index fbae21a9..c4f6b270 100644 --- a/class/ssh_terminal.py +++ b/class/ssh_terminal.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import json import time @@ -434,7 +434,7 @@ class ssh_terminal: ''' version = public.readFile('/etc/redhat-release') if not version: - version = public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace('\l','').strip() + version = public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace(r'\l','').strip() else: version = version.replace('release ','').replace('Linux','').replace('(Core)','').strip() return version diff --git a/class/ssl_info.py b/class/ssl_info.py new file mode 100644 index 00000000..3dea7c7a --- /dev/null +++ b/class/ssl_info.py @@ -0,0 +1,220 @@ + +import os,sys,re,time,subprocess + +panelPath = '/www/server/panel/' +os.chdir(panelPath) + +sys.path.insert(0, panelPath + "class/") +import public +from datetime import date, datetime + +is_openssl = True +try: + import OpenSSL +except: + is_openssl = False + + + +class ssl_info: + + def __init__(self) -> None: + pass + + + def create_key(self,bits=2048): + """ + @name 创建RSA密钥 + @param bits 密钥长度 + """ + if is_openssl: + + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) + private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + return private_key + else: + tmp_pk_file = "/tmp/private_key_{}.pem".format(int(time.time())) + cmd = ["openssl", "genpkey", "-algorithm", "RSA", "-out", tmp_pk_file, "-pkeyopt", f"rsa_keygen_bits:{bits}"] + subprocess.run(cmd, check=True) + with open(tmp_pk_file, "r") as f: + private_key = f.read() + try: + os.remove("private_key.pem") + except: + pass + return private_key + + def load_ssl_info_by_data(self, pem_data: str): + if not isinstance(pem_data, (str, bytes)): + return None + + if is_openssl: + return self.__get_cert_info(pem_data) + + # 使用命令行解析 + pem_file = "/tmp/fullchain_{}.pem".format(int(time.time())) + public.writeFile(pem_file, pem_data) + res = public.ExecShell("openssl x509 -in {} -noout -text".format(pem_file))[0] + try: + result = {} + issuer_match = re.search(r"Issuer: (.*)", res) + if issuer_match: + data = {} + issuer = issuer_match.group(1) + for key, val in re.findall(r"(\w+\s*)=([^,]+)", issuer): + data[key.strip()] = val + + if "CN" in data: + result["issuer"] = data['CN'] + if "O" in data: + s = data['O'].encode().decode('unicode_escape') + result["issuer"] = bytes(s, 'latin1').decode('utf-8') + + validity_match = re.search(r"Not After\s*:\s*(.*)", res) + if validity_match: + not_after = validity_match.group(1) + dt_after = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z") + result['notAfter'] = dt_after.strftime("%Y-%m-%d %H:%M:%S") + result['endtime'] = (dt_after - datetime.now()).days + else: + result['endtime'] = 0 + + validity_match = re.search(r"Not Before\s*:\s*(.*)", res) + if validity_match: + not_befoer = validity_match.group(1) + dt_befoer = datetime.strptime(not_befoer, "%b %d %H:%M:%S %Y %Z") + result['notBefore'] = dt_befoer.strftime("%Y-%m-%d %H:%M:%S") + + subject_match = re.search(r"Subject: (.*)", res) + if subject_match: + subject = subject_match.group(1) + for key, val in re.findall(r"(\w+\s*)=([^,]+)", subject): + if key.strip() == 'CN': + s = val.encode().decode('unicode_escape') + result["subject"] = bytes(s, 'latin1').decode('utf-8') + # 取可选名称 + result['dns'] = [] + dns_match = re.findall(r"DNS:([^\s,]+)", res) + for dns in dns_match: + result['dns'].append(dns) + except: + result = None + + if os.path.exists(pem_file): + os.remove(pem_file) + return result + + def load_ssl_info(self,pem_file): + """ + @name 获取证书详情 + """ + if not os.path.exists(pem_file): + return None + + pem_data = public.readFile(pem_file) + if not pem_data: + return None + return self.load_ssl_info_by_data(pem_data) + + def __get_cert_info(self,pem_data): + """ + @name 通过python的openssl模块获取证书信息 + @param pem_data 证书内容 + """ + result = {} + try: + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem_data) + except: # 证书格式可能是错的,就没有办法读取证书内容 + return None + + issuer = x509.get_issuer() + result['issuer'] = '' + if hasattr(issuer, 'CN'): + result['issuer'] = issuer.CN + if not result['issuer']: + is_key = [b'0', '0'] + issue_comp = issuer.get_components() + if len(issue_comp) == 1: + is_key = [b'CN', 'CN'] + for iss in issue_comp: + if iss[0] in is_key: + result['issuer'] = iss[1].decode() + break + if not result['issuer']: + if hasattr(issuer, 'O'): + result['issuer'] = issuer.O + # 取到期时间 + result['notAfter'] = self.strf_date( + bytes.decode(x509.get_notAfter())[:-1]) + # 取申请时间 + result['notBefore'] = self.strf_date( + bytes.decode(x509.get_notBefore())[:-1]) + # 取可选名称 + result['dns'] = [] + for i in range(x509.get_extension_count()): + s_name = x509.get_extension(i) + if s_name.get_short_name() in [b'subjectAltName', 'subjectAltName']: + s_dns = str(s_name).split(',') + for d in s_dns: + result['dns'].append(d.split(':')[1]) + subject = x509.get_subject().get_components() + # 取主要认证名称 + if len(subject) == 1: + result['subject'] = subject[0][1].decode() + else: + if not result['dns']: + for sub in subject: + if sub[0] == b'CN': + result['subject'] = sub[1].decode() + break + if 'subject' in result: + result['dns'].append(result['subject']) + else: + result['subject'] = result['dns'][0] + result['endtime'] = int(int(time.mktime(time.strptime(result['notAfter'], "%Y-%m-%d")) - time.time()) / 86400) + return result + + # 转换时间 + def strf_date(self, sdate): + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + + #转换时间 + def strfToTime(self,sdate): + import time + return time.strftime('%Y-%m-%d',time.strptime(sdate,'%b %d %H:%M:%S %Y %Z')) + + + def dump_pkcs12_new(self, key_pem=None, cert_pem=None, ca_pem=None, friendly_name=""): + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates + from cryptography.x509 import load_pem_x509_certificate + + private_key = serialization.load_pem_private_key( + key_pem.encode(), + password=None, # 如果私钥有密码,请在此处提供密码 + backend=default_backend() + ) + + cert = load_pem_x509_certificate((cert_pem + ca_pem).encode(), default_backend()) + + # 将证书和私钥组合成PKCS12格式的文件 + p12 = serialize_key_and_certificates( + name=friendly_name.encode() if friendly_name else None, + key=private_key, + cert=cert, + encryption_algorithm=serialization.NoEncryption(), + cas=[load_pem_x509_certificate(ca_pem.encode(), default_backend())] + ) + return p12 + +#class ssl: + + + + + + + + diff --git a/class/ssl_manage.py b/class/ssl_manage.py index 55d57f64..722f82cb 100644 --- a/class/ssl_manage.py +++ b/class/ssl_manage.py @@ -1,8 +1,8 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x3 +# | aaPanel x3 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: baozi # +------------------------------------------------------------------- diff --git a/class/system.py b/class/system.py index 303207dd..1063d413 100644 --- a/class/system.py +++ b/class/system.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 x3 +# | aaPanel x3 # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import psutil,time,os,public,re,sys try: @@ -61,7 +61,7 @@ class system: try: if os.path.exists(configFile): conf = public.readFile(configFile) - rep = "listen\s+([0-9]+)\s*;" + rep = r"listen\s+([0-9]+)\s*;" rtmp = re.search(rep,conf) if rtmp: phpport = rtmp.groups()[0] @@ -70,7 +70,7 @@ class system: if conf.find(self.setupPath + '/stop') == -1: pstatus = True configFile = self.setupPath + '/nginx/conf/enable-php.conf' conf = public.readFile(configFile) - rep = "php-cgi-([0-9]+)\.sock" + rep = r"php-cgi-([0-9]+)\.sock" rtmp = re.search(rep,conf) if rtmp: phpversion = rtmp.groups()[0] @@ -85,11 +85,11 @@ class system: try: if os.path.exists(configFile): conf = public.readFile(configFile) - rep = "php-cgi-([0-9]+)\.sock" + rep = r"php-cgi-([0-9]+)\.sock" rtmp = re.search(rep,conf) if rtmp: phpversion = rtmp.groups()[0] - rep = "Listen\s+([0-9]+)\s*\n" + rep = "Listen\\s+([0-9]+)\\s*\n" rtmp = re.search(rep,conf) if rtmp: phpport = rtmp.groups()[0] @@ -105,12 +105,12 @@ class system: try: if os.path.exists(configFile): conf = public.readFile('/www/server/panel/vhost/openlitespeed/detail/phpmyadmin.conf') - rep = "/usr/local/lsws/lsphp(\d+)/bin/lsphp" + rep = r"/usr/local/lsws/lsphp(\d+)/bin/lsphp" rtmp = re.search(rep,conf) if rtmp: phpversion = rtmp.groups()[0] conf = public.readFile('/www/server/panel/vhost/openlitespeed/listen/888.conf') - rep = "address\s+\*\:(\d+)" + rep = r"address\s+\*\:(\d+)" rtmp = re.search(rep,conf) if rtmp: phpport = rtmp.groups()[0] @@ -206,13 +206,13 @@ class system: phpfpm = public.readFile(file) data = {} try: - rep = "upload_max_filesize\s*=\s*([0-9]+)M" + rep = r"upload_max_filesize\s*=\s*([0-9]+)M" tmp = re.search(rep,phpini).groups() data['max'] = tmp[0] except: data['max'] = '50' try: - rep = "request_terminate_timeout\s*=\s*([0-9]+)\n" + rep = "request_terminate_timeout\\s*=\\s*([0-9]+)\n" tmp = re.search(rep,phpfpm).groups() data['maxTime'] = tmp[0] except: @@ -564,6 +564,7 @@ class system: networkInfo['downPackets'] = 0 networkInfo['upPackets'] = 0 networkIo_list = psutil.net_io_counters(pernic = True) + for net_key in networkIo_list.keys(): networkIo = networkIo_list[net_key][:4] up_key = "{}_up".format(net_key) @@ -623,7 +624,9 @@ class system: networkInfo['database_total'] = public.M('databases').count() networkInfo['system'] = self.GetSystemVersion() networkInfo['installed'] = self.CheckInstalled() + import panelSSL + networkInfo['user_info'] = panelSSL.panelSSL().GetUserInfo(None) networkInfo['up'] = round(float(networkInfo['up']),2) networkInfo['down'] = round(float(networkInfo['down']),2) @@ -683,7 +686,7 @@ class system: #取网络流量信息 import time; pnet = public.readFile('/proc/net/dev') - rep = '([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)' + rep = r'([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)' pnetall = re.findall(rep,pnet) networkInfo = {} networkInfo['upTotal'] = networkInfo['downTotal'] = networkInfo['up'] = networkInfo['down'] = networkInfo['downPackets'] = networkInfo['upPackets'] = 0 @@ -777,7 +780,7 @@ class system: if not os.path.exists(mypath): return False public.set_mode(mypath,644) mycnf = public.readFile(mypath) - tmp = re.findall('datadir\s*=\s*(.+)',mycnf) + tmp = re.findall(r'datadir\s*=\s*(.+)',mycnf) if not tmp: return False datadir = tmp[0] @@ -988,12 +991,12 @@ class system: #修复面板 def RepPanel(self,get): public.writeFile('data/js_random.pl','1') - public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update6_en.sh && bash update.sh") + public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update_7.x_en.sh && bash update.sh") self.ReWeb(None) return True #升级到专业版 def UpdatePro(self,get): - public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update6_en.sh && bash update.sh") + public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update_7.x_en.sh && bash update.sh") self.ReWeb(None) return True diff --git a/class/tomcat.py b/class/tomcat.py index 4456e8bf..81439c8a 100644 --- a/class/tomcat.py +++ b/class/tomcat.py @@ -1,11 +1,11 @@ #!/usr/bin/env python #coding:utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- #------------------------------ diff --git a/class/tools.py b/class/tools.py index d925a170..e3082a9b 100644 --- a/class/tools.py +++ b/class/tools.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- #------------------------------ @@ -72,7 +72,7 @@ def set_panel_pwd(password,ncli = False): #设置数据库目录 def set_mysql_dir(path): - mysql_dir = '''#!/bin/bash + mysql_dir = r'''#!/bin/bash PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH oldDir=`cat /etc/my.cnf |grep 'datadir'|awk '{print $3}'` @@ -459,7 +459,7 @@ def bt_cli(): return import re - rep = "^[\w@\._]+$" + rep = r"^[\w@\._]+$" if not re.match(rep, input_mysql): print(public.get_msg_gettext('|-ERROR, password cannot contain special characters')) return diff --git a/class/userRegister.py b/class/userRegister.py new file mode 100644 index 00000000..bcf51816 --- /dev/null +++ b/class/userRegister.py @@ -0,0 +1,174 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: lotk +# +------------------------------------------------------------------- + +import public,os,sys,db,time,json,re +from BTPanel import session,cache,json_header +from flask import request,redirect,g + +from Crypto import Random +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher +import base64 + +try: + from BTPanel import cache, session +except: + pass + +class userRegister: + # __official_url = 'https://dev.aapanel.com' + # # __official_url = 'https://www.aapanel.com' + + def toRegister(self, post): + try: + # 参数检测 + if not hasattr(post, 'email') or not hasattr(post, 'password'): + return public.return_msg_gettext(False, 'User email or password cannot be empty!') + post.email = post.email.strip() + post.password = post.password.strip() + # 检测 email + emailformat = re.compile(r'[a-zA-Z0-9.-_+%]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+') + if not emailformat.search(post.email): + return public.return_msg_gettext(False, 'Please enter your vaild email') + + post.email = self.en_code_rsa(post.email) + post.password = self.en_code_rsa(post.password) + + params = {} + params['email'] = post.email + params['password'] = post.password + + env_info = self.fetch_env_info() + params['environment_info'] = json.dumps(env_info) + + params['install_code'] = env_info['install_code'] + except Exception as e: + return public.return_msg_gettext(False, "error info : {}".format(e)) + + + # 调用官网注册 + try: + + # public.print_log("传参2 {}".format(params)) + # sUrl = 'http://dev.aapanel.com/api/user/register_on_panel' + sUrl = 'https://www.aapanel.com/api/user/register_on_panel' + aa = public.httpPost(sUrl, params, timeout=60) + data = json.loads(aa) + + + if not data['success']: + if data['res'].startswith("[code: 400] Account is exists!"): + data['res'] = '[code: 400] Account is exists!' + + return public.return_msg_gettext(False, data['res']) + except Exception as e: + return public.return_msg_gettext(False, "error info2 : {}".format(e)) + + # 注册成功调用登录 + try: + self.getToken(post) + return public.return_msg_gettext(True, "Register successfully") + except Exception as e: + return public.return_msg_gettext(False, "error info6 : {}".format(e)) + + + + + # 绑定登录 + def getToken(self, get): + + rtmp = "" + data = {} + data['identification'] = get.email + data['password'] = get.password + data['from_panel'] = self.en_code_rsa('1') # 1 代表从面板登录 + try: + # APIURL1 = 'http://dev.aapanel.com/api/user/login' + APIURL1 = 'https://www.aapanel.com/api/user/login' + rtmp = public.httpPost(APIURL1, data) + result = json.loads(rtmp) + + if result['success']: + bind = 'data/bind.pl' + if os.path.exists(bind): os.remove(bind) + userinfo = result['res']['user_data'] + userinfo['token'] = result['res']['access_token'] + # 用户信息写入文件 + public.writeFile('data/userInfo.json', json.dumps(userinfo)) + + session['focre_cloud'] = True + return public.return_msg_gettext(True, 'Bind successfully') + + else: + return public.return_msg_gettext(False, + 'Invalid username or email or password! please check and try again!') + except Exception as ex: + bind = 'data/bind.pl' + if os.path.exists(bind): + os.remove(bind) + return public.return_msg_gettext(False, '%s
                                    %s' % ( + public.get_msg_gettext('Failed to connect server!'), str(rtmp))) + + + def get_cpuname(self): + return public.ExecShell("cat /proc/cpuinfo|grep 'model name'|cut -d : -f2")[0].strip() + + + def fetch_env_info(self,): + + # 获取机器码 + try: + userPath = 'data/userInfo.json' + if not os.path.exists(userPath): + s1 = public.get_mac_address() + public.get_hostname() + s2 = self.get_cpuname() + serverid = public.md5(s1) + public.md5(s2) + data1 = {} + data1['server_id'] = serverid + public.writeFile(userPath, json.dumps(data1)) + + tmp = public.readFile(userPath) + if len(tmp) < 2: + tmp = '{}' + data = json.loads(tmp) + + if not 'server_id' in data: + s1 = public.get_mac_address() + public.get_hostname() + s2 = self.get_cpuname() + serverid = public.md5(s1) + public.md5(s2) + data['server_id'] = serverid + public.writeFile(userPath, json.dumps(data)) + + server_id = data['server_id'] + except Exception as e: + return {} + + return {'ip': public.GetLocalIp(), + 'is_ipv6': 0, + 'os': public.get_platform(), + 'mac': public.get_mac_address(), + 'hdid': public.fetch_disk_SN(), + 'ramid': public.get_memory(), + 'cpuid': public.fetch_cpu_ID(), + 'server_name': public.get_hostname(), + 'install_code': server_id + } + + + # RSA 加密 + def en_code_rsa(self, data): + pk = public.readFile('data/public.key') + if not pk: + return False + + pub_k = RSA.importKey(pk) + cipher = PKCS1_cipher.new(pub_k) + rsa_text = base64.b64encode(cipher.encrypt(bytes(data.encode("utf8")))) + return str(rsa_text, encoding='utf-8') + diff --git a/class/userlogin.py b/class/userlogin.py index d98fc168..9da8e1a6 100755 --- a/class/userlogin.py +++ b/class/userlogin.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import public,os,sys,db,time,json,re @@ -13,123 +13,9 @@ from flask import request,redirect,g class userlogin: limit_expire_time = 0 - def request_post(self,post): - if not hasattr(post, 'username') or not hasattr(post, 'password'): - return public.returnJson(False,'User name or password cannot be empty!'),json_header - self.error_num(False) - if self.limit_address('?') < 1: return public.returnJson(False,'You have failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header - post.username = post.username.strip() - format_error = 'Parameter format error' - - # 核验用户名密码格式 - post.username = public.rsa_decrypt(post.username) - - if len(post.username) != 32: - return public.returnMsg(False,format_error+"1"),json_header - post.password = public.rsa_decrypt(post.password) - if len(post.password) != 32: - return public.returnMsg(False,format_error+"2"),json_header - - if not re.match(r"^\w+$",post.username): return public.return_msg_gettext(False,'Disk inode has been exhausted, the panel has attempted to release the inode. Please try again ...'),json_header - if not re.match(r"^\w+$",post.password): return public.return_msg_gettext(False,'Disk inode has been exhausted, the panel has attempted to release the inode. Please try again ...'),json_header - last_login_token = session.get('last_login_token',None) - if not last_login_token: - public.WriteLog('TYPE_LOGIN','LOGIN_ERR_CODE',('****','****',public.GetClientIp())) - return public.returnJson(False,"Verification failed, please refresh the page and log in again!"),json_header - - public.chdck_salt() - sql = db.Sql() - userInfo = None - user_plugin_file = '{}/users_main.py'.format(public.get_plugin_path('users')) - if os.path.exists(user_plugin_file): - user_list = sql.table('users').field('id,username,password,salt').select() - for u_info in user_list: - if public.md5(public.md5(u_info['username'] + last_login_token)) == post.username: - userInfo = u_info - else: - userInfo = sql.table('users').where('id=?',1).field('id,username,password,salt').find() - - - if 'code' in session: - if session['code'] and not 'is_verify_password' in session: - if not hasattr(post, 'code'): return public.returnJson(False,'Verification code can not be empty!'),json_header - if not re.match(r"^\w+$",post.code): return public.returnJson(False,'Verification code is incorrect, please try again!'),json_header - if not public.checkCode(post.code): - public.write_log_gettext('Login','Verification code is incorrect, Username:{}, Verification Code:{}, Login IP:{}',('****','****',public.GetClientIp())) - return public.returnJson(False,'Verification code is incorrect, please try again!'),json_header - try: - if not userInfo: - public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp())) - num = self.limit_address('+') - if not num: return public.returnJson(False,'You have failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header - return public.returnJson(False,'wrong user name or password,please refresh the page and try again,You can retry {} more times'.format(num)),json_header - - if userInfo and not userInfo['salt']: - public.chdck_salt() - userInfo = sql.table('users').where('id=?',(userInfo['id'],)).field('id,username,password,salt').find() - - password = public.md5(post.password.strip() + userInfo['salt']) - s_username = public.md5(public.md5(userInfo['username'] + last_login_token)) - if s_username != post.username or userInfo['password'] != password: - public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) - num = self.limit_address('+') - if not num: return public.returnJson(False,'You failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header - return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header - _key_file = "/www/server/panel/data/two_step_auth.txt" - - # 密码过期检测 - if sys.path[0] != 'class/': sys.path.insert(0,'class/') - if not public.password_expire_check(): - session['password_expire'] = True - - #登陆告警 - #public.run_thread(public.login_send_body,("账号密码",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) - # public.login_send_body("账号密码",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT'))) - if hasattr(post,'vcode'): - if not re.match(r"^\d+$",post.vcode): return public.returnJson(False,'Incorrect format of verification code'),json_header - if self.limit_address('?',v="vcode") < 1: return public.returnJson(False,'You have failed verification many times, forbidden for 10 minutes'),json_header - import pyotp - secret_key = public.readFile(_key_file) - if not secret_key: - return public.returnJson(False, "Did not find the key, please close Google verification on the command line and trun on again"),json_header - t = pyotp.TOTP(secret_key) - result = t.verify(post.vcode) - if not result: - if public.sync_date(): result = t.verify(post.vcode) - if not result: - num = self.limit_address('++',v="vcode") - return public.returnJson(False, 'Invalid Verification code. You have [{}] times left to try!'.format(num)), json_header - now = int(time.time()) - # public.run_thread(public.login_send_body,("account",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) - public.writeFile("/www/server/panel/data/dont_vcode_ip.txt",json.dumps({"client_ip":public.GetClientIp(),"add_time":now})) - self.limit_address('--',v="vcode") - self.set_cdn_host(post) - return self._set_login_session(userInfo) - - acc_client_ip = self.check_two_step_auth() - - if not os.path.exists(_key_file) or acc_client_ip: - public.run_thread(public.login_send_body,("account",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) - self.set_cdn_host(post) - return self._set_login_session(userInfo) - self.limit_address('-') - session['is_verify_password'] = True - return "1" - except Exception as ex: - stringEx = str(ex) - if stringEx.find('unsupported') != -1 or stringEx.find('-1') != -1: - public.ExecShell("rm -f /tmp/sess_*") - public.ExecShell("rm -f /www/wwwlogs/*log") - public.ServiceReload() - return public.returnJson(False,'USER_INODE_ERR'),json_header - public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) - num = self.limit_address('+') - if not num: return public.returnJson(False,'You have failed to log in many times, please wait {} seconds and try again!'.format(int(self.limit_expire_time - time.time()))),json_header - return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header - - # 错误收集 适配 # def request_post(self,post): # if not hasattr(post, 'username') or not hasattr(post, 'password'): + # public.print_log('登录拦截 11') # return public.returnJson(False,'User name or password cannot be empty!'),json_header # self.error_num(False) # if self.limit_address('?') < 1: return public.returnJson(False,'You have failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header @@ -192,9 +78,6 @@ class userlogin: # return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header # _key_file = "/www/server/panel/data/two_step_auth.txt" # - # area_check = public.check_area_panel() - # if area_check: return area_check - # # # 密码过期检测 # if sys.path[0] != 'class/': sys.path.insert(0,'class/') # if not public.password_expire_check(): @@ -229,7 +112,7 @@ class userlogin: # if not os.path.exists(_key_file) or acc_client_ip: # public.run_thread(public.login_send_body,("account",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) # self.set_cdn_host(post) - # return self._set_login_session(userInfo, acc_client_ip) + # return self._set_login_session(userInfo) # self.limit_address('-') # session['is_verify_password'] = True # return "1" @@ -242,51 +125,169 @@ class userlogin: # return public.returnJson(False,'USER_INODE_ERR'),json_header # public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) # num = self.limit_address('+') - # if not num: - # return public.returnJson(False,'You have failed to log in many times, please wait {} seconds and try again!'.format(int(self.limit_expire_time - time.time()))),json_header - # # return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header - # - # # 2024/1/3 下午 2:31 记录登录时捕捉不到合适的错误,记录到文件中易于排查 - # import traceback - # public.writeFile( - # '/www/server/panel/data/login_err.log', - # public.getDate() + '\n' + str(traceback.format_exc() + "\n"), - # mode='a+' - # ) - # - # # 提交错误登录信息 - # _form = request.form.to_dict() - # if 'username' in _form: _form['username'] = '******' - # if 'password' in _form: _form['password'] = '******' - # if 'phone' in _form: _form['phone'] = '******' - # - # # 错误信息 - # error_infos = { - # "REQUEST_DATE": public.getDate(), # 请求时间 - # "PANEL_VERSION": public.version(), # 面板版本 - # "OS_VERSION": public.get_os_version(), # 操作系统版本 - # "REMOTE_ADDR": public.GetClientIp(), # 请求IP - # "REQUEST_URI": request.method + request.full_path, # 请求URI - # "REQUEST_FORM": public.xsssec(str(_form)), # 请求表单 - # "USER_AGENT": public.xsssec(request.headers.get('User-Agent')), # 客户端连接信息 - # "ERROR_INFO": str(traceback.format_exc()), # 错误信息 - # "PACK_TIME": public.readFile("/www/server/panel/config/update_time.pl") if os.path.exists("/www/server/panel/config/update_time.pl") else public.getDate(), # 打包时间 - # "TYPE": 2, - # "ERROR_ID": str(ex) - # } - # pkey = public.Md5(error_infos["ERROR_INFO"]) - # - # # 提交 - # if not public.cache_get(pkey): - # try: - # public.run_thread(public.httpPost, ("https://api.bt.cn/bt_error/index.php", error_infos)) - # public.cache_set(pkey, 1, 1800) - # except Exception as e: - # pass - # - # return (public.returnJson( - # False, 'Login error, details:【{}】'.format(stringEx)), - # json_header) + # if not num: return public.returnJson(False,'You have failed to log in many times, please wait {} seconds and try again!'.format(int(self.limit_expire_time - time.time()))),json_header + # return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header + # 错误收集 适配 + def request_post(self,post): + if not hasattr(post, 'username') or not hasattr(post, 'password'): + return public.returnJson(False,'User name or password cannot be empty!'),json_header + self.error_num(False) + if self.limit_address('?') < 1: return public.returnJson(False,'You have failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header + post.username = post.username.strip() + format_error = 'Parameter format error' + + # 核验用户名密码格式 + post.username = public.rsa_decrypt(post.username) + + if len(post.username) != 32: + return public.returnMsg(False,format_error+"1"),json_header + post.password = public.rsa_decrypt(post.password) + if len(post.password) != 32: + return public.returnMsg(False,format_error+"2"),json_header + + if not re.match(r"^\w+$",post.username): return public.return_msg_gettext(False,'Disk inode has been exhausted, the panel has attempted to release the inode. Please try again ...'),json_header + if not re.match(r"^\w+$",post.password): return public.return_msg_gettext(False,'Disk inode has been exhausted, the panel has attempted to release the inode. Please try again ...'),json_header + last_login_token = session.get('last_login_token',None) + if not last_login_token: + public.WriteLog('TYPE_LOGIN','LOGIN_ERR_CODE',('****','****',public.GetClientIp())) + return public.returnJson(False,"Verification failed, please refresh the page and log in again!"),json_header + + public.chdck_salt() + sql = db.Sql() + userInfo = None + user_plugin_file = '{}/users_main.py'.format(public.get_plugin_path('users')) + if os.path.exists(user_plugin_file): + user_list = sql.table('users').field('id,username,password,salt').select() + for u_info in user_list: + if public.md5(public.md5(u_info['username'] + last_login_token)) == post.username: + userInfo = u_info + else: + userInfo = sql.table('users').where('id=?',1).field('id,username,password,salt').find() + + if 'code' in session: + if session['code'] and not 'is_verify_password' in session: + if not hasattr(post, 'code'): return public.returnJson(False,'Verification code can not be empty!'),json_header + if not re.match(r"^\w+$",post.code): return public.returnJson(False,'Verification code is incorrect, please try again!'),json_header + if not public.checkCode(post.code): + public.write_log_gettext('Login','Verification code is incorrect, Username:{}, Verification Code:{}, Login IP:{}',('****','****',public.GetClientIp())) + return public.returnJson(False,'Verification code is incorrect, please try again!'),json_header + + try: + if not userInfo: + public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp())) + num = self.limit_address('+') + if not num: return public.returnJson(False,'You have failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header + return public.returnJson(False,'wrong user name or password,please refresh the page and try again,You can retry {} more times'.format(num)),json_header + + if userInfo and not userInfo['salt']: + public.chdck_salt() + userInfo = sql.table('users').where('id=?',(userInfo['id'],)).field('id,username,password,salt').find() + + password = public.md5(post.password.strip() + userInfo['salt']) + s_username = public.md5(public.md5(userInfo['username'] + last_login_token)) + if s_username != post.username or userInfo['password'] != password: + public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) + num = self.limit_address('+') + if not num: return public.returnJson(False,'You failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header + return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header + _key_file = "/www/server/panel/data/two_step_auth.txt" + + area_check = public.check_area_panel() + if area_check: return area_check + + # 密码过期检测 + if sys.path[0] != 'class/': sys.path.insert(0,'class/') + if not public.password_expire_check(): + session['password_expire'] = True + + #登陆告警 + #public.run_thread(public.login_send_body,("账号密码",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) + # public.login_send_body("账号密码",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT'))) + if hasattr(post,'vcode'): + if not re.match(r"^\d+$",post.vcode): return public.returnJson(False,'Incorrect format of verification code'),json_header + if self.limit_address('?',v="vcode") < 1: return public.returnJson(False,'You have failed verification many times, forbidden for 10 minutes'),json_header + import pyotp + secret_key = public.readFile(_key_file) + if not secret_key: + return public.returnJson(False, "Did not find the key, please close Google verification on the command line and trun on again"),json_header + t = pyotp.TOTP(secret_key) + result = t.verify(post.vcode) + if not result: + if public.sync_date(): result = t.verify(post.vcode) + if not result: + num = self.limit_address('++',v="vcode") + return public.returnJson(False, 'Invalid Verification code. You have [{}] times left to try!'.format(num)), json_header + now = int(time.time()) + # public.run_thread(public.login_send_body,("account",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) + public.writeFile("/www/server/panel/data/dont_vcode_ip.txt",json.dumps({"client_ip":public.GetClientIp(),"add_time":now})) + self.limit_address('--',v="vcode") + self.set_cdn_host(post) + return self._set_login_session(userInfo) + + acc_client_ip = self.check_two_step_auth() + + if not os.path.exists(_key_file) or acc_client_ip: + public.run_thread(public.login_send_body,("account",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) + self.set_cdn_host(post) + return self._set_login_session(userInfo, acc_client_ip) + + self.limit_address('-') + session['is_verify_password'] = True + return "1" + except Exception as ex: + stringEx = str(ex) + if stringEx.find('unsupported') != -1 or stringEx.find('-1') != -1: + public.ExecShell("rm -f /tmp/sess_*") + public.ExecShell("rm -f /www/wwwlogs/*log") + public.ServiceReload() + return public.returnJson(False,'USER_INODE_ERR'),json_header + public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) + num = self.limit_address('+') + if not num: + return public.returnJson(False,'You have failed to log in many times, please wait {} seconds and try again!'.format(int(self.limit_expire_time - time.time()))),json_header + # return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header + + # 2024/1/3 下午 2:31 记录登录时捕捉不到合适的错误,记录到文件中易于排查 + import traceback + public.writeFile( + '/www/server/panel/data/login_err.log', + public.getDate() + '\n' + str(traceback.format_exc() + "\n"), + mode='a+' + ) + + # 提交错误登录信息 + _form = request.form.to_dict() + if 'username' in _form: _form['username'] = '******' + if 'password' in _form: _form['password'] = '******' + if 'phone' in _form: _form['phone'] = '******' + + # 错误信息 + error_infos = { + "REQUEST_DATE": public.getDate(), # 请求时间 + "PANEL_VERSION": public.version(), # 面板版本 + "OS_VERSION": public.get_os_version(), # 操作系统版本 + "REMOTE_ADDR": public.GetClientIp(), # 请求IP + "REQUEST_URI": request.method + request.full_path, # 请求URI + "REQUEST_FORM": public.xsssec(str(_form)), # 请求表单 + "USER_AGENT": public.xsssec(request.headers.get('User-Agent')), # 客户端连接信息 + "ERROR_INFO": str(traceback.format_exc()), # 错误信息 + "PACK_TIME": public.readFile("/www/server/panel/config/update_time.pl") if os.path.exists("/www/server/panel/config/update_time.pl") else public.getDate(), # 打包时间 + "TYPE": 100, + "ERROR_ID": str(ex) + } + pkey = public.Md5(error_infos["ERROR_INFO"]) + + # 提交 + if not public.cache_get(pkey): + try: + public.run_thread(public.httpPost, ("https://geterror.aapanel.com/bt_error/index.php", error_infos)) + public.cache_set(pkey, 1, 1800) + except Exception as e: + pass + + return (public.returnJson( + False, 'Login error, details:【{}】'.format(stringEx)), + json_header) def request_tmp(self,get): try: @@ -300,7 +301,6 @@ class userlogin: if not 'tmp_token' in data or not 'tmp_time' in data: return public.returnJson(False,'Verification failed'),json_header if (time.time() - data['tmp_time']) > 120: return public.returnJson(False,'Expired Token'),json_header if get.tmp_token != data['tmp_token']: return public.returnJson(False,'Invalid Token!'),json_header - userInfo = public.M('users').where("id=?",(1,)).field('id,username').find() session['login'] = True session['username'] = userInfo['username'] @@ -325,7 +325,7 @@ class userlogin: def request_temp(self,get): try: - if len(get.__dict__.keys()) > 2: return public.get_msg_gettext('Parameter ERROR!') + if len(get.get_items().keys()) > 2: return public.get_msg_gettext('Parameter ERROR!') if not hasattr(get,'tmp_token'): return public.get_msg_gettext('Parameter ERROR!') if len(get.tmp_token) != 48: return public.get_msg_gettext('Parameter ERROR!') if not re.match(r"^\w+$",get.tmp_token):return public.get_msg_gettext('Parameter ERROR!') @@ -516,7 +516,7 @@ class userlogin: # res = public.returnMsg(True,'LOGIN_SUCCESS') # 返回增加登录地区 res = public.returnMsg(True, 'LOGIN_SUCCESS') if not acc_client_ip else public.returnMsg(True, - 'Login success, your ip: [{}] has been dynamic password authentication, authentication free within 24 hours!'.format( + 'Login success, your ip: [{}] has been dynamic password authentication, authentication free within 24 hours!'.format( address)) res['login_time'] = time.time() diff --git a/class/vilidate.py b/class/vilidate.py index ed37f06e..c7e29e95 100644 --- a/class/vilidate.py +++ b/class/vilidate.py @@ -1,11 +1,11 @@ #!/usr/bin/env python # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- # | Copyright (c) 2015-2099 宝塔(http://bt.cn) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import random, math diff --git a/class/webserver.py b/class/webserver.py new file mode 100644 index 00000000..a8ab83b2 --- /dev/null +++ b/class/webserver.py @@ -0,0 +1,429 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwl@bt.cn +# +------------------------------------------------------------------- +# +-------------------------------------------------------------------- +# | 前置web服务器控制器 +# +-------------------------------------------------------------------- + +import os +from sys import path as sys_path +from platform import machine + +# 设置运行目录 +os.chdir('/www/server/panel') + +# 添加自定义公共模块路径 +if not 'class/' in sys_path: + sys_path.insert(0,'class/') + +# 引入自定义公共模块 +import public + + +class webserver: + + + def __init__(self): + ''' + @name 初始化 + ''' + if not hasattr(public,'get_panel_path'): + self.__panel_path = '/www/server/panel' + else: + self.__panel_path = public.get_panel_path() + + + self.__webserver_bin = os.path.join(self.__panel_path,'webserver/sbin/webserver') # webserver二进制文件 + self.__webserver_conf = os.path.join(self.__panel_path,'webserver/conf/webserver.conf') # webserver配置文件 + self.__webserver_pid = os.path.join(self.__panel_path,'webserver/logs/webserver.pid') # webserver进程PID + self.__webserver_ctl = os.path.join(self.__panel_path,'script/webserver-ctl.sh') # webserver控制脚本 + self.__panel_port_file = os.path.join(self.__panel_path, 'data/port.pl') # 面板端口文件 + self.__ssl_key_file = os.path.join(self.__panel_path, 'ssl/privateKey.pem') # SSL私钥文件 + self.__ssl_crt_file = os.path.join(self.__panel_path,'ssl/certificate.pem') # SSL证书文件 + self.__is_ssl_file = os.path.join(self.__panel_path,'data/ssl.pl') # 是否开启SSL文件 + self.__examples_path = os.path.join(self.__panel_path, 'webserver/tpls') # 配置文件模板目录 + self.__webserver_conf_example = os.path.join(self.__examples_path ,'webserver.conf') # webserver配置文件模板 + self.__webserver_ssl_conf_example = os.path.join(self.__examples_path ,'webserver_ssl.conf') # webserver SSL配置文件模板 + self.__webserver_listen_conf_example = os.path.join(self.__examples_path ,'webserver_listen.conf') # webserver监听配置文件模板 + self.__webserver_listen_ssl_conf_example = os.path.join(self.__examples_path ,'webserver_listen_ssl.conf') # webserver SSL监听配置文件模板 + self.__default_port = 8888 # 默认端口 + self.__log_error = 'ERROR' + self.__log_debug = 'DEBUG' + self.__log_info = 'INFO' + self.__log_warning = 'WARNING' + + + def print_log(self,msg,level='INFO'): + ''' + @name 打印日志 + @param msg str 日志内容 + @param level str 日志级别 + @return void + ''' + if not hasattr(public,'print_log'): + print("[{}]".format(level),msg) + else: + public.print_log(msg,level) + + def exec_shell(self,cmd): + ''' + @name 执行shell命令 + @param cmd str shell命令 + @return tuple + ''' + res = public.ExecShell(cmd) + if isinstance(res,tuple): + return res + else: + return ('',res) + + + + + # 获取系统架构是X86还是ARM + def get_machine(self): + arch = machine() + return arch + + def bin_exists(self): + ''' + @name 判断webserver二进制文件是否存在 + @return bool + ''' + return os.path.exists(self.__webserver_bin) + + def conf_exists(self): + ''' + @name 判断webserver配置文件是否存在 + @return bool + ''' + return os.path.exists(self.__webserver_conf) + + def conf_example_exists(self): + ''' + @name 判断webserver配置文件模板是否存在 + @return bool + ''' + return os.path.exists(self.__webserver_conf_example) + + def pid_exists(self): + ''' + @name 判断webserver进程PID是否存在 + @return bool + ''' + return os.path.exists(self.__webserver_pid) + + def ctl_exists(self): + ''' + @name 判断webserver-ctl.sh是否存在 + @return bool + ''' + return os.path.exists(self.__webserver_ctl) + + def hasattr_public(self,defname): + ''' + @name 判断public模块是否存在 + @return bool + ''' + if hasattr(public,defname): + return True + + self.print_log('public.{} not found.'.format(defname),self.__log_error) + return False + + def process_status(self): + ''' + @name 判断webserver进程是否存在 + @return bool + ''' + if not self.pid_exists(): + return False + + res = self.exec_shell("bash {} status".format(self.__webserver_ctl)) + + if res[0].find('not running') != -1: + return False + return True + + def start(self): + ''' + @name 启动webserver + @return bool + ''' + if self.process_status(): + return False + + + + res = self.exec_shell("bash {} start".format(self.__webserver_ctl)) + if res[0].find('Failed') != -1: + self.print_log(res,self.__log_warning) + return False + + return True + + def stop(self): + ''' + @name 停止webserver + @return bool + ''' + if not self.process_status(): + return False + res = self.exec_shell("bash {} stop".format(self.__webserver_ctl)) + if res[0].find('Failed') != -1: + self.print_log(res,self.__log_warning) + return False + return True + + def restart(self): + ''' + @name 重启webserver + @return bool + ''' + res = self.exec_shell("bash {} restart".format(self.__webserver_ctl)) + if res[0].find('Failed') != -1: + self.print_log(res,self.__log_warning) + return False + return True + + def reload(self): + ''' + @name 重载webserver + @return bool + ''' + res = self.exec_shell("bash {} reload".format(self.__webserver_ctl)) + if res[0].find('Failed') != -1: + self.print_log(res,self.__log_warning) + return False + return True + + def get_status(self): + ''' + @name 获取webserver状态 + @return bool + ''' + return self.process_status() + + def configtest(self): + ''' + @name 测试配置文件是否正确 + @return bool + ''' + res = self.exec_shell("bash {} configtest".format(self.__webserver_ctl)) + result = "\n".join(res) + + if result.find('successful') != -1: + return True + + self.print_log(result,self.__log_warning) + return False + + def get_panel_port(self): + ''' + @name 获取面板端口 + @return int + ''' + + # 如果面板端口文件不存在,则使用默认端口 + if not os.path.exists(self.__panel_port_file): + return self.__default_port + + # 读取面板端口文件 + port = public.ReadFile(self.__panel_port_file) + if not port: + return self.__default_port + + try: + # 判断端口是否在1-65535之间 + port = int(port) + if port < 1 or port > 65535: + return self.__default_port + except: + return self.__default_port + + return port + + def is_ssl(self): + ''' + @name 是否开启了SSL + @return bool + ''' + if os.path.exists(self.__is_ssl_file): + # 如果开启了SSL,则判断SSL证书和私钥文件是否存在 + if os.path.exists(self.__ssl_key_file) and os.path.exists(self.__ssl_crt_file): + return True + return False + + def get_ssl_config(self): + ''' + @name 获取SSL配置 + @return dict + ''' + # 如果没有开启SSL,则返回空字符串 + if not self.is_ssl(): + return '' + + ssl_conf = public.ReadFile(self.__webserver_ssl_conf_example) + if not ssl_conf: + self.print_log('Ssl configuration file not found.',self.__log_warning) + return '' + + return ssl_conf + + def get_http3_header(self): + ''' + @name 获取HTTP3头部 + @return dict + ''' + http3_header = '' + + # 如果开启了SSL,则添加HTTP3头部 + if self.is_ssl(): + '''h3=":443"; ma=2592000,h3-29=":443"; ma=2592000''' + http3_header = "add_header Alt-Svc 'h3=\":{PORT}\"; ma=86400,h3-29=\":{PORT}\"; ma=86400';".format(PORT=self.get_panel_port()) + + return http3_header + + def get_listen_config(self): + ''' + @name 获取监听配置 + @return dict + ''' + default_listen = ''' + listen {PORT}; + listen [::]:{PORT}; +''' + # 是否开启了SSL + if self.is_ssl(): + + listen_conf = public.ReadFile(self.__webserver_listen_ssl_conf_example) + else: + listen_conf = public.ReadFile(self.__webserver_listen_conf_example) + + # 如果没有找到监听配置文件,则使用默认监听配置 + if not listen_conf: + listen_conf = default_listen + + # 替换面板端口 + port = self.get_panel_port() + listen_conf = listen_conf.format(PORT=port) + + return listen_conf + + + def create_conf(self): + ''' + @name 创建配置文件 + @return bool + ''' + if not self.conf_example_exists(): + self.print_log('Web server configuration file template not found.',self.__log_error) + return False + + # 读取配置文件模板 + config_example = public.ReadFile(self.__webserver_conf_example) + if not config_example: + self.print_log('Web server configuration file template read failed.',self.__log_error) + return False + + # 获取监听配置 + listen_conf = self.get_listen_config() + # 获取SSL配置 + ssl_conf = self.get_ssl_config() + # 获取HTTP3头部 + http3_header = self.get_http3_header() + + config = config_example.format(LISTEN=listen_conf, SSL_CONFIG=ssl_conf, HTTP3_HEADER=http3_header) + res = public.WriteFile(self.__webserver_conf, config) + if not res: + self.print_log("Write Web server configuration file failed",self.__log_error) + return False + + # 测试配置文件是否正确 + if not self.configtest(): + self.print_log('Web server configuration file test failed.',self.__log_error) + return False + + return True + + def chmod_static_files(self): + ''' + @name 设置静态文件权限 + @return bool + ''' + # 设置静态目录链权限 + res = self.exec_shell('chmod 755 /www /www/server /www/server/panel /www/server/panel/BTPanel') + if res[1] != '': + self.print_log('\n'.join(res),self.__log_warning) + return False + + # 递归设置静态文件权限 + res = self.exec_shell('chmod -R 755 /www/server/panel/BTPanel/static') + if res[1] != '': + self.print_log('\n'.join(res),self.__log_warning) + return False + + return True + + def run_webserver(self): + ''' + @name 启动web服务器 + @return bool 是否成功 + ''' + + try: + # 判断public模块是否符合要求 + public_function = ['ExecShell','ReadFile','WriteFile','get_error_info','get_panel_path','get_panel_port','print_log'] + for func in public_function: + if not self.hasattr_public(func): + return False + + # 判断webserver二进制文件是否存在,如果不存在则尝试调用下载脚本下载 + if not self.bin_exists(): + if os.path.exists(self.__webserver_ctl): + self.exec_shell("bash {} download".format(self.__webserver_ctl)) + self.print_log('Web server binary file not found.',self.__log_error) + return False + + # 判断cli工具脚本是否存在 + if not self.ctl_exists(): + self.print_log('Web server command line tool not found.',self.__log_error) + return False + + # 设置静态文件权限 + if not self.chmod_static_files(): + self.print_log('Web server static file permission setting failed.',self.__log_error) + return False + + # 创建配置文件 + if not self.create_conf(): + self.print_log('Web server configuration file creation failed.',self.__log_error) + return False + + # 检查webserver进程是否存在,如果存在则重载,否则启动 + if self.process_status(): + res = self.reload() + if not res: + self.print_log('Web server reload failed.',self.__log_warning) + else: + res = self.start() + # 检查进程是否正常运行 + if not res: + self.print_log('Web server start failed.',self.__log_error) + return False + except: + # 将错误信息写入到日志 + self.print_log(public.get_error_info(),self.__log_error) + return False + + return True + +if __name__ == '__main__': + + web = webserver() + print(web.run_webserver()) diff --git a/class/webshell_check.py b/class/webshell_check.py index efbfb59b..9d374585 100644 --- a/class/webshell_check.py +++ b/class/webshell_check.py @@ -1,10 +1,10 @@ # # coding: utf-8 # # +------------------------------------------------------------------- -# # | 宝塔Linux面板 x6 +# # | aaPanel x6 # # +------------------------------------------------------------------- -# # | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# # | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. # # +------------------------------------------------------------------- -# # | Author: lkqiang +# # | Author: lkqiang # # +------------------------------------------------------------------- # # +-------------------------------------------------------------------- # # | 宝塔webshell 内置扫描 diff --git a/class/website_auto_index.py b/class/website_auto_index.py index b748e98d..854c871d 100644 --- a/class/website_auto_index.py +++ b/class/website_auto_index.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2020 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: zhwen +# Author: zhwen #------------------------------------------------------------------- #------------------------------ @@ -25,7 +25,7 @@ class website_auto_index: def get_auto_index(self, args): ''' # 添加某个网站禁止运行PHP - author: zhwen + author: zhwen :param args: website 网站名 str :return: ''' @@ -46,7 +46,7 @@ class website_auto_index: deny_name = [i.split('_')[-1] for i in data] result = [] for i in deny_name: - reg = '#BEGIN_AUTOINDEX_{}\n\s*location\s*\~\*\s*\^(.*)\.\*.*\((.*)\)\$'.format(i) + reg = '#BEGIN_AUTOINDEX_{}\n\\s*location\\s*\\~\\*\\s*\\^(.*)\\.\\*.*\\((.*)\\)\\$'.format(i) deny_directory = re.search(reg, conf).groups()[0] deny_suffix = re.search(reg, conf).groups()[1] result.append({'name': i, 'dir': deny_directory, 'suffix': deny_suffix}) @@ -60,7 +60,7 @@ class website_auto_index: deny_name = [i.split('_')[-1] for i in data] result = [] for i in deny_name: - reg = '#BEGIN_AUTOINDEX_{}\n\s* + author: zhwen :param args: website 网站名 str :param args: index_name 规则名称 str :param args: dir 自动索引目录 str @@ -108,7 +108,7 @@ class website_auto_index: if not conf: return False if not dir: - reg = '\s*#BEGIN_AUTOINDEX_{n}\n(.|\n)*#END_AUTOINDEX_{n}\n'.format(n=name) + reg = '\\s*#BEGIN_AUTOINDEX_{n}\n(.|\n)*#END_AUTOINDEX_{n}\n'.format(n=name) conf = re.sub(reg, '', conf) else: new = ''' @@ -133,7 +133,7 @@ class website_auto_index: if not conf: return False if not dir: - reg = '\s*#BEGIN_AUTOINDEX_{n}\n(.|\n)*#END_AUTOINDEX_{n}'.format(n=name) + reg = '\\s*#BEGIN_AUTOINDEX_{n}\n(.|\n)*#END_AUTOINDEX_{n}'.format(n=name) conf = re.sub(reg, '', conf) else: new = ''' @@ -147,7 +147,7 @@ class website_auto_index: '''.format(n=name, d=dir) if '#BEGIN_AUTOINDEX_{}'.format(name) in conf: return True - conf = re.sub('#DENY\s*FILES', new + '\n #DENY FILES', conf) + conf = re.sub(r'#DENY\s*FILES', new + '\n #DENY FILES', conf) public.writeFile(self.ap_website_conf, conf) return True @@ -155,7 +155,7 @@ class website_auto_index: def del_auto_index(self, args): ''' # 添加某个网站禁止运行PHP - author: zhwen + author: zhwen :param args: website 网站名 str :param args: deny_name 规则名称 str :return: diff --git a/class/wxapp.py b/class/wxapp.py index 11b0d16e..890d62a1 100644 --- a/class/wxapp.py +++ b/class/wxapp.py @@ -1,10 +1,10 @@ # coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2019 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2019 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- import os import sys diff --git a/class_v2/acme_v3.py b/class_v2/acme_v3.py new file mode 100644 index 00000000..3f20ab47 --- /dev/null +++ b/class_v2/acme_v3.py @@ -0,0 +1,2208 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# ACME v2客户端 +# ------------------------------------------------------------------- +import re +import fcntl +import datetime +import binascii +import hashlib +import base64 +import json +import time +import os +import sys + +os.chdir('/www/server/panel') +if not 'class/' in sys.path: + sys.path.insert(0, 'class/') +import http_requests as requests +# + +requests.DEFAULT_TYPE = 'curl' +import public +from public.validate import Param +try: + import OpenSSL +except: + public.ExecShell("btpip install -I pyOpenSSL") + import OpenSSL +try: + import dns.resolver +except: + public.ExecShell("btpip install dnspython") + import dns.resolver + + +class acme_v2: + _url = None + _apis = None + _config = {} + _dns_domains = [] + _bits = 2048 + _acme_timeout = 30 + _dns_class = None + _user_agent = "BTPanel" + _replay_nonce = None + _verify = False + _digest = "sha256" + _max_check_num = 5 + _wait_time = 5 + _mod_index = {True: "Staging", False: "Production"} + _debug = False + _auto_wildcard = False + _dnsapi_file = 'config/dns_api.json' + _save_path = 'vhost/letsencrypt' + _conf_file = 'config/letsencrypt.json' + _stop_rp_file = '{}/data/stop_rp_when_renew_ssl.pl'.format(public.get_panel_path()) + _by_panel = None + + def __init__(self): + if self._debug: + self._url = 'https://acme-staging-v02.api.letsencrypt.org/directory' + else: + self._url = 'https://acme-v02.api.letsencrypt.org/directory' + self._config = self.read_config() + + # 取接口目录 + def get_apis(self): + if not self._apis: + # 尝试从配置文件中获取 + api_index = self._mod_index[self._debug] + if not 'apis' in self._config: + self._config['apis'] = {} + if api_index in self._config['apis']: + if 'expires' in self._config['apis'][api_index] and 'directory' in self._config['apis'][api_index]: + if time.time() < self._config['apis'][api_index]['expires']: + self._apis = self._config['apis'][api_index]['directory'] + return self._apis + + # 尝试从云端获取 + res = requests.get(self._url, verify=False) + if not res.status_code in [200, 201]: + result = res.json() + if "type" in result: + if result['type'] == 'urn:acme:error:serverInternal': + raise Exception(public.get_msg_gettext( + 'Service shutdown or internal error due to maintenance, check [ https://letsencrypt.status.io ] see for more details.')) + if not os.path.exists('/www/server/panel/data/http_type.pl'): + public.writeFile('/www/server/panel/data/http_type.pl', 'python') + self.get_apis() + return self._apis + raise Exception(res.content) + s_body = res.json() + self._apis = {} + self._apis['newAccount'] = s_body['newAccount'] + self._apis['newNonce'] = s_body['newNonce'] + self._apis['newOrder'] = s_body['newOrder'] + self._apis['revokeCert'] = s_body['revokeCert'] + self._apis['keyChange'] = s_body['keyChange'] + + # 保存到配置文件 + self._config['apis'][api_index] = {} + self._config['apis'][api_index]['directory'] = self._apis + self._config['apis'][api_index]['expires'] = time.time() + \ + 86400 # 24小时后过期 + self.save_config() + return self._apis + + # 获取帐户信息 + def get_account_info(self, args): + try: + if not 'account' in self._config: + return {} + k = self._mod_index[self._debug] + if not k in self._config['account']: + self.get_apis() + self.get_kid() + account = self._config['account'][k] + account['email'] = self._config['email'] + self.set_crond() + return public.return_message(0,0,account) + except Exception as ex: + return_message=public.return_msg_gettext(False, str(ex)) + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + # 设置帐户信息 + def set_account_info(self, args): + if not 'account' in self._config: + return public.return_msg_gettext(False, 'The specified account does not exist') + account = json.loads(args.account) + if 'email' in account: + self._config['email'] = account['email'] + del (account['email']) + self._config['account'][self._mod_index[self._debug]] = account + self.save_config() + return public.return_msg_gettext(True, 'Setup successfully!') + + # 获取订单列表 + def get_orders(self, args): + if not 'orders' in self._config: + return [] + s_orders = [] + for index in self._config['orders'].keys(): + tmp_order = self._config['orders'][index] + tmp_order['index'] = index + s_orders.append(tmp_order) + return s_orders + + # 删除订单 + def remove_order(self, args): + if not 'orders' in self._config: + return public.return_msg_gettext(False, 'The specified order does not exist!') + if not args.index in self._config['orders']: + return public.return_msg_gettext(False, 'The specified order does not exist!') + del (self._config['orders'][args.index]) + self.save_config() + return public.return_msg_gettext(True, 'Order deleted successfully!') + + # 取指定订单数据 + def get_order_find(self, args): + if not 'orders' in self._config: + return public.return_msg_gettext(False, 'The specified order does not exist!') + if not args.index in self._config['orders']: + return public.return_msg_gettext(False, 'The specified order does not exist!') + result = self._config['orders'][args.index] + result['cert'] = self.get_cert_info(args.index) + return result + + # 获取证书信息 + def get_cert_info(self, index): + cert = {} + path = self._config['orders'][index]['save_path'] + if not os.path.exists(path): + self.download_cert(index) + cert['private_key'] = public.readFile(path + "/privkey.pem") + cert['fullchain'] = public.readFile(path + "/fullchain.pem") + return cert + + # 更新证书压缩包 + def update_zip(self, args): + path = self._config['orders'][args.index]['save_path'] + if not os.path.exists(path): # 尝试重新下载证书 + self.download_cert(args.index) + if not os.path.exists(path): + return public.return_msg_gettext(False, 'Certificate read failed, directory does not exist!') + import panelTask + bt_task = panelTask.bt_task() + zip_file = path + '/cert.zip' + result = bt_task._zip(path, '.', path + '/cert.zip', '/dev/null', 'zip') + if not os.path.exists(zip_file): + return result + return public.return_msg_gettext(True, zip_file) + + # 吊销证书 + def revoke_order(self, index): + if type(index) != str: + index = index.index + if not index in self._config['orders']: + raise Exception(public.get_msg_gettext('The specified order does not exist!')) + cert_path = self._config['orders'][index]['save_path'] + if not os.path.exists(cert_path): + raise Exception(public.get_msg_gettext('No certificate found for the specified order!')) + cert = self.dump_der(cert_path) + if not cert: + raise Exception(public.get_msg_gettext('Certificate read failed!')) + payload = { + "certificate": self.calculate_safe_base64(cert), + "reason": 4 + } + res = self.acme_request(self._apis['revokeCert'], payload) + if res.status_code in [200, 201]: + if os.path.exists(cert_path): + public.ExecShell("rm -rf {}".format(cert_path)) + del (self._config['orders'][index]) + self.save_config() + return public.return_msg_gettext(True, "Certificate revoked!") + return res.json() + + # 取根域名和记录值 + def extract_zone(self, domain_name): + top_domain_list = public.readFile('{}/config/domain_root.txt'.format(public.get_panel_path())) + if top_domain_list: + top_domain_list = top_domain_list.strip().split('\n') + else: + top_domain_list = [] + old_domain_name = domain_name + top_domain = "." + ".".join(domain_name.rsplit('.')[-2:]) + new_top_domain = "." + top_domain.replace(".", "") + is_tow_top = False + if top_domain in top_domain_list: + is_tow_top = True + domain_name = domain_name[:-len(top_domain)] + new_top_domain + + if domain_name.count(".") > 1: + zone, middle, last = domain_name.rsplit(".", 2) + if is_tow_top: + last = top_domain[1:] + root = ".".join([middle, last]) + else: + zone = "" + root = old_domain_name + return root, zone + + # 自动构造通配符 + def auto_wildcard(self, domains): + if not domains: + return domains + domain_list = [] + for domain in domains: + rootDoamin = self.extract_zone(domain)[0] + if not rootDoamin in domain_list: + domain_list.append(rootDoamin) + if not "*." + rootDoamin in domain_list: + domain_list.append("*." + rootDoamin) + return domain_list + + # 构造域名列表 + def format_domains(self, domains): + if type(domains) != list: + return [] + # 是否自动构造通配符 + if self._auto_wildcard: + domains = self.auto_wildcard(domains) + wildcard = [] + tmp_domains = [] + for domain in domains: + domain = domain.strip() + if domain in tmp_domains: + continue + # 将通配符域名转为验证正则表达式 + f_index = domain.find("*.") + if f_index not in [-1, 0]: + continue + if f_index == 0: + wildcard.append(domain.replace( + "*", r"^[\w-]+").replace(".", r"\.")) + # 添加到申请列表 + tmp_domains.append(domain) + + # 处理通配符包含 + apply_domains = tmp_domains[:] + for domain in tmp_domains: + for w in wildcard: + if re.match(w, domain): + apply_domains.remove(domain) + + return apply_domains + + # 创建订单 + def create_order(self, domains, auth_type, auth_to, index=None): + domains = self.format_domains(domains) + if not domains: + raise Exception(public.get_msg_gettext('Need at least a domain name!')) + # 构造标识 + identifiers = [] + for domain_name in domains: + identifiers.append({"type": 'dns', "value": domain_name}) + payload = {"identifiers": identifiers} + + # 请求创建订单 + res = self.acme_request(self._apis['newOrder'], payload) + if not res.status_code in [201]: # 如果创建失败 + e_body = res.json() + if 'type' in e_body: + # 如果随机数失效 + if e_body['type'].find('error:badNonce') != -1: + self.get_nonce(force=True) + res = self.acme_request(self._apis['newOrder'], payload) + + # 如果帐户失效 + if e_body['detail'].find('KeyID header contained an invalid account URL') != -1: + k = self._mod_index[self._debug] + del (self._config['account'][k]) + self.get_kid() + self.get_nonce(force=True) + res = self.acme_request(self._apis['newOrder'], payload) + if not res.status_code in [201]: + a_auth = res.json() + + ret_title = self.get_error(str(a_auth)) + raise StopIteration( + "{} >>>> {}".format( + ret_title, + json.dumps(a_auth) + ) + ) + + # 返回验证地址和验证 + s_json = res.json() + s_json['auth_type'] = auth_type + s_json['domains'] = domains + s_json['auth_to'] = auth_to + index = self.save_order(s_json, index) + return index + + def get_site_run_path_byid(self, site_id): + ''' + @name 通过site_id获取网站运行目录 + @author hwliang + @param site_id 网站标识 + @return None or string + ''' + if public.M('sites').where('id=? and project_type=?', (site_id, 'PHP')).count() >= 1: + site_path = public.M('sites').where('id=?', site_id).getField('path') + if not site_path: return None + if not os.path.exists(site_path): return None + args = public.dict_obj() + args.id = site_id + import panelSite + run_path = panelSite.panelSite().GetRunPath(args) + if run_path in ['/']: run_path = '' + if run_path: + if run_path[0] == '/': run_path = run_path[1:] + site_run_path = os.path.join(site_path, run_path) + if not os.path.exists(site_run_path): return site_path + return site_run_path + else: + return False + + def get_site_run_path(self, domains): + ''' + @name 通过域名列表获取网站运行目录 + @author hwliang + @param domains 域名列表 + @return None or string + ''' + site_id = 0 + for domain in domains: + site_id = public.M('domain').where("name=?", domain).getField('pid') + if site_id: break + + if not site_id: return None + return self.get_site_run_path_byid(site_id) + + # 获取验证信息 + def get_auths(self, index): + if not index in self._config['orders']: + raise Exception(public.get_msg_gettext('The specified order does not exist!')) + + # 检查是否已经获取过授权信息 + if 'auths' in self._config['orders'][index]: + # 检查授权信息是否过期 + if time.time() < self._config['orders'][index]['auths'][0]['expires']: + return self._config['orders'][index]['auths'] + if self._config['orders'][index]['auth_type'] != 'dns': + site_run_path = self.get_site_run_path(self._config['orders'][index]['domains']) + if site_run_path: self._config['orders'][index]['auth_to'] = site_run_path + + # 清理旧验证 + self.claer_auth_file(index) + + auths = [] + for auth_url in self._config['orders'][index]['authorizations']: + res = self.acme_request(auth_url, "") + if res.status_code not in [200, 201]: + raise Exception("ACEM_AUTH_ERR", (res.json(),)) + + s_body = res.json() + if 'status' in s_body: + if s_body['status'] in ['invalid']: + raise Exception('ACME_INVALID_ORDER') + if s_body['status'] in ['valid']: # 跳过无需验证的域名 + continue + + s_body['expires'] = self.utc_to_time(s_body['expires']) + identifier_auth = self.get_identifier_auth(index, auth_url, s_body) + if not identifier_auth: + raise Exception('ACME_V_INFO_ERR') + + acme_keyauthorization, auth_value = self.get_keyauthorization( + identifier_auth['token']) + identifier_auth['acme_keyauthorization'] = acme_keyauthorization + identifier_auth['auth_value'] = auth_value + identifier_auth['expires'] = s_body['expires'] + identifier_auth['auth_to'] = self._config['orders'][index]['auth_to'] + identifier_auth['type'] = self._config['orders'][index]['auth_type'] + # 设置验证信息 + self.set_auth_info(identifier_auth) + auths.append(identifier_auth) + self._config['orders'][index]['auths'] = auths + self.save_config() + return auths + + # 更新随机数 + def update_replay_nonce(self, res): + replay_nonce = res.headers.get('Replay-Nonce') + if replay_nonce: + self._replay_nonce = replay_nonce + + # 设置验证信息 + def set_auth_info(self, identifier_auth): + + # 从云端验证 + if not self.cloud_check_domain(identifier_auth['domain']): + self.err = "Cloud verification failed!" + + # 是否手动验证DNS + if identifier_auth['auth_to'] == 'dns': + return None + + # 是否文件验证 + if identifier_auth['type'] in ['http', 'tls']: + self.write_auth_file( + identifier_auth['auth_to'], identifier_auth['token'], identifier_auth['acme_keyauthorization']) + else: + # dnsapi验证 + self.create_dns_record( + identifier_auth['auth_to'], identifier_auth['domain'], identifier_auth['auth_value']) + + # 从云端验证域名是否可访问 + def cloud_check_domain(self, domain): + try: + result = requests.post('https://www.aapanel.com/api/panel/checkDomain', {"domain": domain, "ssl": 1}).json() + return result['status'] + except: + return False + + # 清理验证文件 + def claer_auth_file(self, index): + if not self._config['orders'][index]['auth_type'] in ['http', 'tls']: + return True + acme_path = '{}/.well-known/acme-challenge'.format(self._config['orders'][index]['auth_to']) + write_log(public.get_msg_gettext('|-Verify the dir:{}', (acme_path,))) + if os.path.exists(acme_path): + public.ExecShell("rm -f {}/*".format(acme_path)) + acme_path = '/www/server/stop/.well-known/acme-challenge' + if os.path.exists(acme_path): + public.ExecShell("rm -f {}/*".format(acme_path)) + + # 写验证文件 + def write_auth_file(self, auth_to, token, acme_keyauthorization): + try: + acme_path = '{}/.well-known/acme-challenge'.format(auth_to) + if not os.path.exists(acme_path): + os.makedirs(acme_path) + public.set_own(acme_path, 'www') + wellknown_path = '{}/{}'.format(acme_path, token) + public.writeFile(wellknown_path, acme_keyauthorization) + public.set_own(wellknown_path, 'www') + + acme_path = '/www/server/stop/.well-known/acme-challenge' + if not os.path.exists(acme_path): + os.makedirs(acme_path) + public.set_own(acme_path, 'www') + wellknown_path = '{}/{}'.format(acme_path, token) + public.writeFile(wellknown_path, acme_keyauthorization) + public.set_own(wellknown_path, 'www') + return True + except: + err = public.get_error_info() + print(err) + raise Exception(public.get_msg_gettext('Writing verification file failed: {}', (err,))) + + # 解析域名 + def create_dns_record(self, auth_to, domain, dns_value): + # 如果为手动解析 + if auth_to == 'dns' or auth_to.find('|') == -1: + return None + if not self._dns_class: + import panelDnsapi + dns_name, key, secret = self.get_dnsapi(auth_to) + self._dns_class = getattr(panelDnsapi, dns_name)(key, secret) + self._dns_class.create_dns_record(public.de_punycode(domain), dns_value) + self._dns_domains.append({"domain": domain, "dns_value": dns_value}) + + # 解析DNSAPI信息 + def get_dnsapi(self, auth_to): + tmp = auth_to.split('|') + dns_name = tmp[0] + key = "None" + secret = "None" + if len(tmp) < 3: + try: + dnsapi_config = json.loads(public.readFile(self._dnsapi_file)) + for dc in dnsapi_config: + if dc['name'] != dns_name: + continue + if not dc['data']: + continue + key = dc['data'][0]['value'] + secret = dc['data'][1]['value'] + except: + raise Exception(public.get_msg_gettext('No valid DNSAPI key information found')) + else: + key = tmp[1] + secret = tmp[2] + return dns_name, key, secret + + # 删除域名解析 + def remove_dns_record(self): + if not self._dns_class: + return None + for dns_info in self._dns_domains: + try: + self._dns_class.delete_dns_record( + public.de_punycode(dns_info['domain']), dns_info['dns_value']) + except: + pass + + # 验证域名 + def auth_domain(self, index): + if not index in self._config['orders']: + raise Exception(public.get_msg_gettext('The specified order does not exist!')) + + # 开始验证 + for auth in self._config['orders'][index]['auths']: + res = self.check_auth_status(auth['url']) # 检查是否需要验证 + if res.json()['status'] == 'pending': + if auth['type'] == 'dns': # 尝试提前验证dns解析 + self.check_dns( + "_acme-challenge.{}".format( + auth['domain'].replace('*.', '')), + auth['auth_value'], + "TXT" + ) + self.respond_to_challenge(auth) + + # 检查验证结果 + for i in range(len(self._config['orders'][index]['auths'])): + self.check_auth_status(self._config['orders'][index]['auths'][i]['url'], [ + 'valid', 'invalid']) + self._config['orders'][index]['status'] = 'valid' + + # 检查验证状态 + def check_auth_status(self, url, desired_status=None): + desired_status = desired_status or ["pending", "valid", "invalid"] + number_of_checks = 0 + while True: + if desired_status == ['valid', 'invalid']: + write_log(public.get_msg_gettext('|-{} Query verification results..', (str(number_of_checks + 1),))) + time.sleep(self._wait_time) + check_authorization_status_response = self.acme_request(url, "") + a_auth = check_authorization_status_response.json() + authorization_status = a_auth["status"] + number_of_checks += 1 + if authorization_status in desired_status: + if authorization_status == "invalid": + write_log("|-" + public.get_msg_gettext('Verification failed')) + try: + if 'error' in a_auth['challenges'][0]: + ret_title = a_auth['challenges'][0]['error']['detail'] + elif 'error' in a_auth['challenges'][1]: + ret_title = a_auth['challenges'][1]['error']['detail'] + elif 'error' in a_auth['challenges'][2]: + ret_title = a_auth['challenges'][2]['error']['detail'] + else: + ret_title = str(a_auth) + ret_title = self.get_error(ret_title) + except: + ret_title = str(a_auth) + raise StopIteration( + "{} >>>> {}".format( + ret_title, + json.dumps(a_auth) + ) + ) + break + + if number_of_checks == self._max_check_num: + raise StopIteration( + public.get_msg_gettext( + 'Error: Attempted verification {} times. The maximum number of verifications is {}. The verification interval is {} seconds.', + ( + str(number_of_checks), + str(self._max_check_num), + str(self._wait_time) + ))) + if desired_status == ['valid', 'invalid']: + write_log(public.get_msg_gettext('|-Verification succeeded!')) + return check_authorization_status_response + + # 格式化错误输出 + def get_error(self, error): + write_log("error_result: " + str(error)) + if error.find("Max checks allowed") >= 0: + return public.get_msg_gettext( + 'CA cannot verify your domain name, please check if the domain name resolution is correct, or wait 5-10 minutes and try again.') + elif error.find("Max retries exceeded with") >= 0 or error.find('status_code=0 ') != -1: + return public.get_msg_gettext('CA server connection timed out, please try again later.') + elif error.find("The domain name belongs") >= 0: + return public.get_msg_gettext( + 'The domain name does not belong to this DNS service provider, please make sure the domain name is filled in correctly.') + elif error.find('login token ID is invalid') >= 0: + return public.get_msg_gettext('DNS server connection failed, please check if the key is correct.') + elif error.find('Error getting validation data') != -1: + return public.get_msg_gettext( + 'Data validation failed and the CA was unable to get the correct captcha from the authenticated connection.') + elif "too many certificates already issued for exact set of domains" in error: + return public.get_msg_gettext('Issuing failed, the domain {} has exceeded the limit of weekly reissues!', + (str(re.findall("exact set of domains: (.+):", error)),)) + elif "Error creating new account :: too many registrations for this IP" in error: + return public.get_msg_gettext( + 'Issuing failed, the current server IP has reached the limit of creating up to 10 accounts every 3 hours.') + elif "DNS problem: NXDOMAIN looking up A for" in error: + return public.get_msg_gettext( + 'Validation failed, domain name was not resolved, or resolution did not take effect!') + elif "Invalid response from" in error: + return public.get_msg_gettext( + 'Verification failed, domain name resolution error or verification URL cannot be accessed!') + elif error.find('TLS Web Server Authentication') != -1: + return public.get_msg_gettext('Connection to CA server failed, please try again later.') + elif error.find('Name does not end in a public suffix') != -1: + return public.get_msg_gettext('Unsupported domain name {}, please check the domain name is correct!', + (str(re.findall("Cannot issue for \"(.+)\":", error)),)) + elif error.find('No valid IP addresses found for') != -1: + return public.get_msg_gettext( + 'No resolution record was found for domain name {}, please check if the domain name resolution takes effect!', + (str(re.findall("No valid IP addresses found for (.+)", error)),)) + elif error.find('No TXT record found at') != -1: + return public.get_msg_gettext( + 'No valid TXT resolution record was found in the domain name {}, please check whether the TXT record is parsed correctly. If it is applied by DNSAPI, please try again in 10 minutes!', + (str(re.findall("No TXT record found at (.+)", error)),)) + elif error.find('Incorrect TXT record') != -1: + return public.get_msg_gettext( + 'A wrong TXT record was found on {}: {}, please check whether the TXT resolution is correct, if it is applied by DNSAPI, please try again in 10 minutes!', + (str(re.findall("found at (.+)", error)), str(re.findall("Incorrect TXT record \"(.+)\"", error)))) + elif error.find('Domain not under you or your user') != -1: + return public.get_msg_gettext( + 'This domain name does not exist under this dnspod account, adding resolution failed!') + elif error.find('SERVFAIL looking up TXT for') != -1: + return public.get_msg_gettext( + 'No valid TXT resolution record was found in the domain name {}, please check whether the TXT record is parsed correctly. If it is applied by DNSAPI, please try again in 10 minutes!', + (str(re.findall("looking up TXT for (.+)", error)),)) + elif error.find('Timeout during connect') != -1: + return public.get_msg_gettext( + 'The connection timed out and the CA server was unable to access your website!') + elif error.find("DNS problem: SERVFAIL looking up CAA for") != -1: + return public.get_msg_gettext( + 'Domain name {} is currently required to verify the CAA record, please parse the CAA record manually, or retry the application after 1 hour!', + (str(re.findall("looking up CAA for (.+)", error)),)) + elif error.find("Read timed out.") != -1: + return public.get_msg_gettext( + 'The verification timed out. Please check if the domain name is resolved correctly. If it is resolved correctly, the connection between the server and LetsEncrypt may be abnormal. Please try again later!') + elif error.find('Cannot issue for') != -1: + return public.get_msg_gettext( + 'Cannot issue a certificate for {}, cannot apply for a wildcard certificate with a domain name suffix directly!', + (str(re.findall(r'for\s+"(.+)"', error)),)) + elif error.find('too many failed authorizations recently'): + return public.get_msg_gettext( + 'The account has more than 5 failed orders within 1 hour, please wait 1 hour and try again!') + elif error.find("Error creating new order") != -1: + return public.get_msg_gettext('Order creation failed, please try again later!') + elif error.find("Too Many Requests") != -1: + return public.get_msg_gettext( + 'More than 5 verification failures in 1 hour, the application is temporarily banned, please try again later!') + elif error.find('HTTP Error 400: Bad Request') != -1: + return public.get_msg_gettext('CA server denied access, please try again later!') + elif error.find('Temporary failure in name resolution') != -1: + return public.get_msg_gettext( + 'The DNS of the server is faulty and the domain name cannot be resolved. Please use the Linux toolbox to check the DNS configuration') + elif error.find('Too Many Requests') != -1: + return public.get_msg_gettext('Too many requests for this domain name. Please try again 3 hours later') + else: + return error + + # 发送验证请求 + def respond_to_challenge(self, auth): + payload = {"keyAuthorization": "{}".format( + auth['acme_keyauthorization'])} + respond_to_challenge_response = self.acme_request( + auth['dns_challenge_url'], payload) + return respond_to_challenge_response + + # 发送CSR + def send_csr(self, index): + csr = self.create_csr(index) + payload = {"csr": self.calculate_safe_base64(csr)} + send_csr_response = self.acme_request( + url=self._config['orders'][index]['finalize'], payload=payload) + if send_csr_response.status_code not in [200, 201]: + if send_csr_response.status_code == 0: + raise ValueError( + "Error: [Connection reset by peer], the request process may be accidentally intercepted, if only this domain name cannot apply, then the domain name may be abnormal!") + raise ValueError( + "Error: Sending CSR: Response Status {status_code} Response:{response}".format( + status_code=send_csr_response.status_code, + response=send_csr_response.json(), + ) + ) + send_csr_response_json = send_csr_response.json() + certificate_url = send_csr_response_json["certificate"] + self._config['orders'][index]['certificate_url'] = certificate_url + self.save_config() + return certificate_url + + # 获取证书到期时间 + def get_cert_timeout(self, cret_data): + try: + x509 = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, cret_data) + cert_timeout = bytes.decode(x509.get_notAfter())[:-1] + return int(time.mktime(time.strptime(cert_timeout, '%Y%m%d%H%M%S'))) + except: + return int(time.time() + (86400 * 90)) + + # 下载证书 + def download_cert(self, index): + res = self.acme_request( + self._config['orders'][index]['certificate_url'], "") + if res.status_code not in [200, 201]: + raise Exception(public.get_msg_gettext('Failed to download certificate: {}', (str(res.json()),))) + + pem_certificate = res.content + if type(pem_certificate) == bytes: + pem_certificate = pem_certificate.decode('utf-8') + cert = self.split_ca_data(pem_certificate) + cert['cert_timeout'] = self.get_cert_timeout(cert['cert']) + cert['private_key'] = self._config['orders'][index]['private_key'] + cert['domains'] = self._config['orders'][index]['domains'] + del (self._config['orders'][index]['private_key']) + del (self._config['orders'][index]['auths']) + del (self._config['orders'][index]['expires']) + del (self._config['orders'][index]['authorizations']) + del (self._config['orders'][index]['finalize']) + del (self._config['orders'][index]['identifiers']) + if 'cert' in self._config['orders'][index]: + del (self._config['orders'][index]['cert']) + self._config['orders'][index]['status'] = 'valid' + self._config['orders'][index]['cert_timeout'] = cert['cert_timeout'] + domain_name = self._config['orders'][index]['domains'][0] + self._config['orders'][index]['save_path'] = '{}/{}'.format( + self._save_path, domain_name) + cert['save_path'] = self._config['orders'][index]['save_path'] + self.save_config() + self.save_cert(cert, index) + return cert + + # 保存证书到文件 + def save_cert(self, cert, index): + try: + domain_name = self._config['orders'][index]['domains'][0] + path = self._config['orders'][index]['save_path'] + if not os.path.exists(path): + os.makedirs(path, 384) + + # 存储证书 + key_file = path + "/privkey.pem" + pem_file = path + "/fullchain.pem" + public.writeFile(key_file, cert['private_key']) + public.writeFile(pem_file, cert['cert'] + cert['root']) + public.writeFile(path + "/cert.csr", cert['cert']) + public.writeFile(path + "/root_cert.csr", cert['root']) + + # 转为IIS证书 + pfx_buffer = self.dump_pkcs12( + cert['private_key'], cert['cert'] + cert['root'], cert['root'], domain_name) + public.writeFile(path + "/fullchain.pfx", pfx_buffer, 'wb+') + + ps = '''Document description: +privkey.pem Certificate private key +fullchain.pem PEM format certificate with certificate chain (nginx/apache) +root_cert.csr Root certificate +cert.csr Domain name certificate +fullchain.pfx Certificate format for IIS + +How to use in the aaPanel: +privkey.pem Paste into the key entry box +fullchain.pem Paste into certificate input box +''' + public.writeFile(path + '/Description.txt', ps) + self.sub_all_cert(key_file, pem_file) + except: + write_log(public.get_error_info()) + + # 通过域名获取网站名称 + def get_site_name_by_domains(self, domains): + sql = public.M('domain') + site_sql = public.M('sites') + siteName = None + for domain in domains: + pid = sql.where('name=?', domain).getField('pid') + if pid: + siteName = site_sql.where('id=?', pid).getField('name') + break + return siteName + + # 替换服务器上的同域名同品牌证书 + def sub_all_cert(self, key_file, pem_file): + cert_init = self.get_cert_init(pem_file) # 获取新证书的基本信息 + paths = ['/www/server/panel/vhost/cert', '/www/server/panel/vhost/ssl', '/www/server/panel'] + is_panel = False + for path in paths: + if not os.path.exists(path): + continue + for p_name in os.listdir(path): + to_path = path + '/' + p_name + to_pem_file = to_path + '/fullchain.pem' + to_key_file = to_path + '/privkey.pem' + to_info = to_path + '/info.json' + # 判断目标证书是否存在 + if not os.path.exists(to_pem_file): + if p_name not in ['ssl']: continue + to_pem_file = to_path + '/certificate.pem' + to_key_file = to_path + '/privateKey.pem' + if not os.path.exists(to_pem_file): + continue + # _by_panel None 时通过面板请求时不即使续签面板证书也不重启面板以免导致后续请求出错 + if not os.path.exists('{}/data/ssl.pl'.format(public.get_panel_path())): + continue + if not self._by_panel: + is_panel = True + # 获取目标证书的基本信息 + to_cert_init = self.get_cert_init(to_pem_file) + # 判断证书品牌是否一致 + try: + if to_cert_init['issuer'] != cert_init['issuer'] and to_cert_init['issuer'].find( + "Let's Encrypt") == -1 and to_cert_init['issuer'] != 'R3': + continue + except: + continue + # 判断目标证书的到期时间是否较早 + if to_cert_init['notAfter'] > cert_init['notAfter']: + continue + # 判断认识名称是否一致 + if len(to_cert_init['dns']) != len(cert_init['dns']): + continue + is_copy = True + for domain in to_cert_init['dns']: + if not domain in cert_init['dns']: + is_copy = False + if not is_copy: + continue + + # 替换新的证书文件和基本信息 + public.writeFile( + to_pem_file, public.readFile(pem_file, 'rb'), 'wb') + public.writeFile( + to_key_file, public.readFile(key_file, 'rb'), 'wb') + public.writeFile(to_info, json.dumps(cert_init)) + write_log(public.get_msg_gettext( + '|-Detected that the certificate under {} overlaps with the certificate of this application and has an earlier expiration time, and has been replaced with a new certificate!', + (to_path,))) + # 重载web服务 + public.serviceReload() + #if is_panel: public.restart_panel() + + # 检查指定证书是否在订单列表 + def check_order_exists(self, pem_file): + try: + cert_init = self.get_cert_init(pem_file) + if not cert_init: return None + for index in self._config['orders'].keys(): + if not 'save_path' in self._config['orders'][index]: + continue + for domain in self._config['orders'][index]['domains']: + if domain in cert_init['dns']: + return index + if cert_init['issuer'].find("Let's Encrypt") != -1 or cert_init['issuer'] == 'R3': + return pem_file + return None + except: + return None + + # 取证书基本信息API + def get_cert_init_api(self, args): + if not os.path.exists(args.pem_file): + args.pem_file = 'vhost/cert/{}/fullchain.pem'.format(args.siteName) + if not os.path.exists(args.pem_file): + return public.return_msg_gettext(False, 'The specified certificate file does not exist!') + cert_init = self.get_cert_init(args.pem_file) + if not cert_init: + return public.return_msg_gettext(False, 'Certificate information acquisition failed!') + api_path = './config/dns_api.json' + api_init = './config/dns_api_init.json' + if not os.path.exists(api_path): + if os.path.exists(api_init): + import shutil + shutil.copyfile(api_init, api_path) + cert_init['dnsapi'] = json.loads(public.readFile(self._dnsapi_file)) + return cert_init + + # 获取指定证书基本信息 + def get_cert_init(self, pem_file): + if not os.path.exists(pem_file): + return None + try: + result = {} + x509 = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, public.readFile(pem_file)) + # 取产品名称 + issuer = x509.get_issuer() + result['issuer'] = '' + if hasattr(issuer, 'CN'): + result['issuer'] = issuer.CN + if not result['issuer']: + is_key = [b'0', '0'] + issue_comp = issuer.get_components() + if len(issue_comp) == 1: + is_key = [b'CN', 'CN'] + for iss in issue_comp: + if iss[0] in is_key: + result['issuer'] = iss[1].decode() + break + # 取到期时间 + result['notAfter'] = self.strf_date( + bytes.decode(x509.get_notAfter())[:-1]) + # 取申请时间 + result['notBefore'] = self.strf_date( + bytes.decode(x509.get_notBefore())[:-1]) + # 取可选名称 + result['dns'] = [] + for i in range(x509.get_extension_count()): + s_name = x509.get_extension(i) + if s_name.get_short_name() in [b'subjectAltName', 'subjectAltName']: + s_dns = str(s_name).split(',') + for d in s_dns: + result['dns'].append(d.split(':')[1]) + subject = x509.get_subject().get_components() + # 取主要认证名称 + if len(subject) == 1: + result['subject'] = subject[0][1].decode() + else: + result['subject'] = result['dns'][0] + return result + except: + return None + + # 转换时间 + def strf_date(self, sdate): + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + + # 证书转为DER + def dump_der(self, cert_path): + cert = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, public.readFile(cert_path + '/cert.csr')) + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert) + + # 证书转为pkcs12 + def dump_pkcs12(self, key_pem=None, cert_pem=None, ca_pem=None, friendly_name=None): + p12 = OpenSSL.crypto.PKCS12() + if cert_pem: + p12.set_certificate(OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, cert_pem.encode())) + if key_pem: + p12.set_privatekey(OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, key_pem.encode())) + if ca_pem: + p12.set_ca_certificates((OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, ca_pem.encode()),)) + if friendly_name: + p12.set_friendlyname(friendly_name.encode()) + return p12.export() + + # 拆分根证书 + def split_ca_data(self, cert): + sp_key = '-----END CERTIFICATE-----\n' + datas = cert.split(sp_key) + return {"cert": datas[0] + sp_key, "root": sp_key.join(datas[1:])} + + # 构造可选名称 + def get_alt_names(self, index): + domain_name = self._config['orders'][index]['domains'][0] + domain_alt_names = [] + if len(self._config['orders'][index]['domains']) > 1: + domain_alt_names = self._config['orders'][index]['domains'][1:] + return domain_name, domain_alt_names + + # 检查DNS记录 + def check_dns(self, domain, value, s_type='TXT'): + write_log(public.get_msg_gettext( + '|-Attempt to verify DNS records locally, domain name: {}, type: {} record value: {}', + (domain, s_type, value))) + time.sleep(10) + n = 0 + while n < 20: + n += 1 + try: + import dns.resolver + ns = dns.resolver.query(domain, s_type) + for j in ns.response.answer: + for i in j.items: + txt_value = i.to_text().replace('"', '').strip() + write_log( + public.get_msg_gettext('|-Number of verifications: {}, value: {}', (str(n), txt_value))) + if txt_value == value: + write_log(public.get_msg_gettext('|-Local authentication succeeded!')) + return True + except: + try: + import dns.resolver + except: + return False + time.sleep(3) + write_log(public.get_msg_gettext('|-Local authentication failed!')) + return True + + # 创建CSR + def create_csr(self, index): + if 'csr' in self._config['orders'][index]: + return self._config['orders']['csr'] + domain_name, domain_alt_names = self.get_alt_names(index) + X509Req = OpenSSL.crypto.X509Req() + X509Req.get_subject().CN = domain_name + if domain_alt_names: + SAN = "DNS:{}, ".format(domain_name).encode("utf8") + ", ".join( + "DNS:" + i for i in domain_alt_names + ).encode("utf8") + else: + SAN = "DNS:{}".format(domain_name).encode("utf8") + + X509Req.add_extensions( + [ + OpenSSL.crypto.X509Extension( + "subjectAltName".encode("utf8"), critical=False, value=SAN + ) + ] + ) + pk = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, self.create_certificate_key( + index).encode() + ) + X509Req.set_pubkey(pk) + try: + X509Req.set_version(2) + except ValueError as e: # pyOpenSSL 新版本需要必须设置版本为0 + X509Req.set_version(0) + X509Req.sign(pk, self._digest) + return OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_ASN1, X509Req) + + # 构造域名验证头和验证值 + def get_keyauthorization(self, token): + acme_header_jwk_json = json.dumps( + self.get_acme_header("GET_THUMBPRINT")["jwk"], sort_keys=True, separators=(",", ":") + ) + acme_thumbprint = self.calculate_safe_base64( + hashlib.sha256(acme_header_jwk_json.encode("utf8")).digest() + ) + acme_keyauthorization = "{}.{}".format(token, acme_thumbprint) + base64_of_acme_keyauthorization = self.calculate_safe_base64( + hashlib.sha256(acme_keyauthorization.encode("utf8")).digest() + ) + + return acme_keyauthorization, base64_of_acme_keyauthorization + + # 构造验证信息 + def get_identifier_auth(self, index, url, auth_info): + s_type = self.get_auth_type(index) + write_log(public.get_msg_gettext('|-Verification type: {}', (s_type,))) + domain = auth_info['identifier']['value'] + wildcard = False + # 处理通配符 + if 'wildcard' in auth_info: + wildcard = auth_info['wildcard'] + if wildcard: + domain = "*." + domain + + for auth in auth_info['challenges']: + if auth['type'] != s_type: + continue + identifier_auth = { + "domain": domain, + "url": url, + "wildcard": wildcard, + "token": auth['token'], + "dns_challenge_url": auth['url'], + } + return identifier_auth + return None + + # 获取域名验证方式 + def get_auth_type(self, index): + if not index in self._config['orders']: + raise Exception(public.get_msg_gettext('The specified order does not exist!')) + s_type = 'http-01' + if 'auth_type' in self._config['orders'][index]: + if self._config['orders'][index]['auth_type'] == 'dns': + s_type = 'dns-01' + elif self._config['orders'][index]['auth_type'] == 'tls': + s_type = 'tls-alpn-01' + else: + s_type = 'http-01' + return s_type + + # 保存订单 + def save_order(self, order_object, index): + if not 'orders' in self._config: + self._config['orders'] = {} + renew = False + if not index: + index = public.md5(json.dumps(order_object['identifiers'])) + else: + renew = True + order_object['certificate_url'] = self._config['orders'][index]['certificate_url'] + order_object['save_path'] = self._config['orders'][index]['save_path'] + + order_object['expires'] = self.utc_to_time(order_object['expires']) + self._config['orders'][index] = order_object + self._config['orders'][index]['index'] = index + if not renew: + self._config['orders'][index]['create_time'] = int(time.time()) + self._config['orders'][index]['renew_time'] = 0 + self.save_config() + return index + + # UTC时间转时间戳 + def utc_to_time(self, utc_string): + try: + utc_string = utc_string.split('.')[0] + utc_date = datetime.datetime.strptime( + utc_string, "%Y-%m-%dT%H:%M:%S") + # 按北京时间返回 + return int(time.mktime(utc_date.timetuple())) + (3600 * 8) + except: + return int(time.time() + 86400 * 7) + + # 获取kid + def get_kid(self, force=False): + # 如果配置文件中不存在kid或force = True时则重新注册新的acme帐户 + if not 'account' in self._config: + self._config['account'] = {} + k = self._mod_index[self._debug] + if not k in self._config['account']: + self._config['account'][k] = {} + + if not 'kid' in self._config['account'][k]: + self._config['account'][k]['kid'] = self.register() + self.save_config() + time.sleep(3) + self._config = self.read_config() + return self._config['account'][k]['kid'] + + # 注册acme帐户 + def register(self, existing=False): + if not 'email' in self._config: + self._config['email'] = 'demo@aapanel.com' + if existing: + payload = {"onlyReturnExisting": True} + elif self._config['email']: + payload = { + "termsOfServiceAgreed": True, + "contact": ["mailto:{}".format(self._config['email'])], + } + else: + payload = {"termsOfServiceAgreed": True} + + res = self.acme_request(url=self._apis['newAccount'], payload=payload) + + if res.status_code not in [201, 200, 409]: + raise Exception(public.get_msg_gettext('Registration for ACME account failed: {}', (str(res.json()),))) + kid = res.headers["Location"] + return kid + + # 请求到ACME接口 + def acme_request(self, url, payload): + headers = {"User-Agent": self._user_agent} + payload = self.stringfy_items(payload) + + if payload == "": + payload64 = payload + else: + payload64 = self.calculate_safe_base64(json.dumps(payload)) + protected = self.get_acme_header(url) + protected64 = self.calculate_safe_base64(json.dumps(protected)) + signature = self.sign_message( + message="{}.{}".format(protected64, payload64)) # bytes + signature64 = self.calculate_safe_base64(signature) # str + data = json.dumps( + {"protected": protected64, "payload": payload64, + "signature": signature64} + ) + headers.update({"Content-Type": "application/jose+json"}) + response = requests.post( + url, data=data.encode("utf8"), timeout=self._acme_timeout, headers=headers, verify=self._verify + ) + # 更新随机数 + self.update_replay_nonce(response) + return response + + # 计算signature + def sign_message(self, message): + pk = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, self.get_account_key().encode()) + return OpenSSL.crypto.sign(pk, message.encode("utf8"), self._digest) + + # 系列化payload + def stringfy_items(self, payload): + if isinstance(payload, str): + return payload + + for k, v in payload.items(): + if isinstance(k, bytes): + k = k.decode("utf-8") + if isinstance(v, bytes): + v = v.decode("utf-8") + payload[k] = v + return payload + + # 获取随机数 + def get_nonce(self, force=False): + # 如果没有保存上一次的随机数或force=True时则重新获取新的随机数 + if not self._replay_nonce or force: + headers = {"User-Agent": self._user_agent} + response = requests.get( + self._apis['newNonce'], + timeout=self._acme_timeout, + headers=headers, + verify=self._verify + ) + self._replay_nonce = response.headers["Replay-Nonce"] + return self._replay_nonce + + # 获请ACME请求头 + def get_acme_header(self, url): + header = {"alg": "RS256", "nonce": self.get_nonce(), "url": url} + if url in [self._apis['newAccount'], 'GET_THUMBPRINT']: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + private_key = serialization.load_pem_private_key( + self.get_account_key().encode(), + password=None, + backend=default_backend(), + ) + public_key_public_numbers = private_key.public_key().public_numbers() + + exponent = "{0:x}".format(public_key_public_numbers.e) + exponent = "0{}".format(exponent) if len( + exponent) % 2 else exponent + modulus = "{0:x}".format(public_key_public_numbers.n) + jwk = { + "kty": "RSA", + "e": self.calculate_safe_base64(binascii.unhexlify(exponent)), + "n": self.calculate_safe_base64(binascii.unhexlify(modulus)), + } + header["jwk"] = jwk + else: + header["kid"] = self.get_kid() + return header + + # 转为无填充的Base64 + def calculate_safe_base64(self, un_encoded_data): + if sys.version_info[0] == 3: + if isinstance(un_encoded_data, str): + un_encoded_data = un_encoded_data.encode("utf8") + r = base64.urlsafe_b64encode(un_encoded_data).rstrip(b"=") + return r.decode("utf8") + + # 获用户取密钥对 + def get_account_key(self): + if not 'account' in self._config: + self._config['account'] = {} + k = self._mod_index[self._debug] + if not k in self._config['account']: + self._config['account'][k] = {} + + if not 'key' in self._config['account'][k]: + self._config['account'][k]['key'] = self.create_key() + if type(self._config['account'][k]['key']) == bytes: + self._config['account'][k]['key'] = self._config['account'][k]['key'].decode() + self.save_config() + return self._config['account'][k]['key'] + + # 获取证书密钥对 + def create_certificate_key(self, index): + # 判断是否已经创建private_key + if 'private_key' in self._config['orders'][index]: + return self._config['orders'][index]['private_key'] + # 创建新的私钥 + private_key = self.create_key() + if type(private_key) == bytes: + private_key = private_key.decode() + # 保存私钥到订单配置文件 + self._config['orders'][index]['private_key'] = private_key + self.save_config() + return private_key + + # 创建Key + def create_key(self, key_type=OpenSSL.crypto.TYPE_RSA): + key = OpenSSL.crypto.PKey() + key.generate_key(key_type, self._bits) + private_key = OpenSSL.crypto.dump_privatekey( + OpenSSL.crypto.FILETYPE_PEM, key) + return private_key + + # 写配置文件 + def save_config(self): + fp = open(self._conf_file, 'w+') + fcntl.flock(fp, fcntl.LOCK_EX) # 加锁 + fp.write(json.dumps(self._config)) + fcntl.flock(fp, fcntl.LOCK_UN) # 解锁 + fp.close() + return True + + # 读配置文件 + def read_config(self): + if not os.path.exists(self._conf_file): + self._config['orders'] = {} + self._config['account'] = {} + self._config['apis'] = {} + self._config['email'] = public.M('config').where('id=?', (1,)).getField('email') + if self._config['email'] in ['287962566@qq.com']: + self._config['email'] = None + self.save_config() + return self._config + tmp_config = public.readFile(self._conf_file) + if not tmp_config: + return self._config + try: + self._config = json.loads(tmp_config) + except: + self.save_config() + return self._config + return self._config + + # 申请证书 + def apply_cert(self, domains, auth_type='dns', auth_to='Dns_com|None|None', **args): + write_log("", "wb+") + try: + self.get_apis() + index = None + if 'index' in args: + index = args['index'] + if 'auto_wildcard' in args: + self._auto_wildcard = 1 + if not index: # 判断是否只想验证域名 + write_log(public.get_msg_gettext('|-Creating order..')) + index = self.create_order(domains, auth_type, auth_to) + write_log(public.get_msg_gettext('|-Getting verification information..')) + self.get_auths(index) + if auth_to == 'dns' and len(self._config['orders'][index]['auths']) > 0: + return public.return_message(0,0,self._config['orders'][index]) + write_log(public.get_msg_gettext('|-Verifying domain name..')) + self.auth_domain(index) + self.remove_dns_record() + write_log(public.get_msg_gettext('|-Sending CSR..')) + self.send_csr(index) + write_log(public.get_msg_gettext('|-Downloading certificate..')) + cert = self.download_cert(index) + cert['status'] = True + cert['msg'] = public.get_msg_gettext('Application successful!') + write_log(public.get_msg_gettext('|-Successful application, deploying to site..')) + return public.return_message(0,0,cert) + except Exception as ex: + self.remove_dns_record() + ex = str(ex) + if ex.find(">>>>") != -1: + msg = ex.split(">>>>") + msg[1] = json.loads(msg[1]) + else: + msg = ex + write_log(public.get_error_info()) + return_message=public.return_msg_gettext(False, msg) + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + # 申请证书 - api + def apply_cert_api(self, args): + # 校验参数 + try: + get = args + get.validate([ + Param('domains').Array(), + Param('auth_type').String(), + Param('auth_to').String(), + Param('auto_wildcard').Integer(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # 在面板点击申请证书时不要重启面板以防后续请求出错 + self._by_panel = True + # 是否为指定站点 + if public.M('sites').where('id=? and project_type=?', (args.id, 'Java')).count(): + project_info = public.M('sites').where('id=?', (args.id,)).getField('project_config') + try: + project_info = json.loads(project_info) + if not 'ssl_path' in project_info: + return_message=public.return_msg_gettext(False, + 'There is a problem with the current Java project configuration file, please rebuild') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + if not os.path.exists(project_info['ssl_path']): + os.makedirs(project_info['ssl_path']) + path = project_info['ssl_path'] + args.auth_to = path + check_result = self.check_auth_env(args) + if check_result['message']['result'] !="": + return check_result + + if args.auto_wildcard == '1': + self._auto_wildcard = True + self.turnon_redirect_proxy_httptohttps(args) + return self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) + except: + return public.return_message(-1,0, 'There is a problem with the current Java project configuration file, please rebuild') + finally: + self.turnon_redirect_proxy_httptohttps(args) + else: + if re.match(r"^\d+$", args.auth_to): + import panel_site_v2 as panelSite + path = public.M('sites').where('id=?', (args.id,)).getField('path') + args.auth_to = path + '/' + panelSite.panelSite().GetRunPath(args)['message']['result'] + args.auth_to = args.auth_to.replace("//", "/") + if args.auth_to[-1] == '/': + args.auth_to = args.auth_to[:-1] + + if not os.path.exists(args.auth_to): + self.turnon_redirect_proxy_httptohttps(args) + return public.return_message(-1,0, 'Invalid site directory, please check if the specified site exists!') + check_result = self.check_auth_env(args, check=True) + if check_result['message']['result'] !="": + return check_result + if args.auto_wildcard == '1': + self._auto_wildcard = True + res = self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) + if os.path.exists(self._stop_rp_file): + self.turnon_redirect_proxy_httptohttps(args) + return res + + def turnon_redirect_proxy_httptohttps(self, args): + import panelSite + s = panelSite.panelSite() + if not 'siteName' in args: + args.siteName = public.M('sites').where('id=?', (args.id,)).getField('name') + args.sitename = args.siteName + self.turnon_redirect(args, s) + self.turnon_proxy(args, s) + self.turnon_httptohttps(args, s) + public.serviceReload() + + def turnon_httptohttps(self, args, s): + conf_file = '{}/data/stop_httptohttps.pl'.format(public.get_panel_path()) + if os.path.exists(conf_file): + write_log('|-Turning on http to https') + s.HttpToHttps(args) + try: + os.remove(conf_file) + except: + pass + + def turnon_proxy(self, args, s): + conf_file = '{}/data/stop_p_tmp.pl'.format(public.get_panel_path()) + if not os.path.exists(conf_file): + return + write_log('|-Turning on proxy') + conf = json.loads(public.readFile(conf_file)) + data = s.GetProxyList(args) + for x in data: + if x['sitename'] not in conf: + continue + if x['proxyname'] not in conf[x['sitename']]: + continue + args.type = 1 + args.advanced = x['advanced'] + args.cache = x['cache'] + args.cachetime = x['cachetime'] + args.proxydir = x['proxydir'] + args.proxyname = x['proxyname'] + args.proxysite = x['proxysite'] + args.sitename = x['sitename'] + args.subfilter = json.dumps(x['subfilter']) + args.todomain = x['todomain'] + s.ModifyProxy(args) + try: + os.remove(conf_file) + except: + pass + + def turnon_redirect(self, args, s): + conf_file = '{}/data/stop_r_tmp.pl'.format(public.get_panel_path()) + if not os.path.exists(conf_file): + return + write_log('|-Turning on redirection') + conf = json.loads(public.readFile(conf_file)) + data = s.GetRedirectList(args) + for x in data: + if x['sitename'] not in conf: + continue + if x['redirectname'] not in conf[x['sitename']]: + continue + args.type = 1 + args.sitename = x['sitename'] + args.holdpath = x['holdpath'] + args.redirectname = x['redirectname'] + args.redirecttype = x['redirecttype'] + args.domainorpath = x['domainorpath'] + args.redirectpath = x['redirectpath'] + args.redirectdomain = json.dumps(x['redirectdomain']) + args.tourl = x['tourl'] + s.ModifyRedirect(args) + try: + os.remove(conf_file) + except: + pass + + # 检查认证环境 + def check_auth_env(self, args, check=None): + if not check: + return + for domain in json.loads(args.domains): + if public.checkIp(domain): continue + if domain.find('*.') != -1 and args.auth_type in ['http', 'tls']: + return_message=public.return_msg_gettext(False, + 'Pan domain names cannot apply for a certificate using [File Verification]!') + del return_message['status'] + raise public.return_message(-1,0, return_message['msg']) + import panel_site_v2 as panelSite + s = panelSite.panelSite() + if args.auth_type in ['http', 'tls']: + try: + rp_conf = public.readFile(self._stop_rp_file) + try: + if rp_conf: + rp_conf = json.loads(rp_conf) + except: + write_log('|-Failed to parse configuration file') + if not 'siteName' in args: + args.siteName = public.M('sites').where('id=?', (args.id,)).getField('name') + args.sitename = args.siteName + data = s.GetRedirectList(args) + # 检查重定向是否开启 + if type(data) == list: + redirect_tmp = {args.sitename: []} + for x in data: + if rp_conf and x['sitename'] in rp_conf: + if str(x['type']) == '0': + continue + args.type = 0 + args.sitename = x['sitename'] + args.holdpath = x['holdpath'] + args.redirectname = x['redirectname'] + args.redirecttype = x['redirecttype'] + args.domainorpath = x['domainorpath'] + args.redirectpath = x['redirectpath'] + args.redirectdomain = json.dumps(x['redirectdomain']) + args.tourl = x['tourl'] + args.notreload = True + write_log("|- Turning off redirection {}".format(args.redirectname)) + s.ModifyRedirect(args) + redirect_tmp[args.sitename].append(x['redirectname']) + else: + if x['type']: return public.return_message(-1,0, + 'Your site has 301 Redirect on,Please turn it off first!') + if redirect_tmp[args.sitename]: + public.writeFile('{}/data/stop_r_tmp.pl'.format(public.get_panel_path()), + json.dumps(redirect_tmp)) + data = s.GetProxyList(args) + # 检查反向代理是否开启 + if type(data) == list: + proxy_tmp = {args.sitename: []} + for x in data: + if rp_conf and x['sitename'] in rp_conf: + if str(x['type']) == '0': + continue + args.type = 0 + args.advanced = x['advanced'] + args.cache = x['cache'] + args.cachetime = x['cachetime'] + args.proxydir = x['proxydir'] + args.proxyname = x['proxyname'] + args.proxysite = x['proxysite'] + args.sitename = x['sitename'] + args.subfilter = json.dumps(x['subfilter']) + args.todomain = x['todomain'] + args.notreload = True + s.ModifyProxy(args) + write_log("|- Turning off proxy {}".format(args.proxyname)) + proxy_tmp[args.sitename].append(x['proxyname']) + else: + if x['type']: return public.return_message(-1,0, + 'Sites with reverse proxy turned on cannot apply for SSL!') + if proxy_tmp[args.sitename]: + public.writeFile('{}/data/stop_p_tmp.pl'.format(public.get_panel_path()), json.dumps(proxy_tmp)) + # 检查旧重定向是否开启 + data = s.Get301Status(args) + if data['status']: + return public.return_message(-1,0, + 'The website has been redirected, please close it before applying!') + # 判断是否强制HTTPS + if s.IsToHttps(args.siteName): + if os.path.exists(self._stop_rp_file): + if rp_conf and args.siteName in rp_conf: + write_log("|- Turning off http to https") + s.CloseToHttps(args) + public.writeFile('{}/data/stop_httptohttps.pl'.format(public.get_panel_path()), '') + else: + return public.return_message(-1,0, + 'After configuring Force HTTPS, you cannot use [File Verification] to apply for a certificate!') + public.serviceReload() + except: + return public.return_message(-1,0,"") + else: + if args.auth_to.find('Dns_com') != -1: + if not os.path.exists('plugin/dns/dns_main.py'): + return public.return_message(-1,0, + 'Please go to the software store to install [cloud analysis], and complete the domain name NS binding.') + return public.return_message(-1,0,"") + + # DNS手动验证 + def apply_dns_auth(self, args): + # 校验参数 + try: + args.validate([ + Param('index').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + return self.apply_cert([], auth_type='dns', auth_to='dns', index=args.index) + + # 创建计划任务 + def set_crond(self): + try: + echo = public.md5(public.md5('renew_lets_ssl_bt')) + cron_id = public.M('crontab').where('echo=?', (echo,)).getField('id') + + import crontab + args_obj = public.dict_obj() + if not cron_id: + cronPath = public.GetConfigValue('setup_path') + '/cron/' + echo + shell = '{} -u /www/server/panel/class/acme_v2.py --renew=1'.format(sys.executable) + public.writeFile(cronPath, shell) + args_obj.id = public.M('crontab').add( + 'name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress', + ("Renew Let's Encrypt Certificate", 'day', '', '0', '10', echo, + time.strftime('%Y-%m-%d %X', time.localtime()), 0, '', 'localhost', 'toShell', '', shell, '')) + crontab.crontab().set_cron_status(args_obj) + else: + cron_path = public.get_cron_path() + if os.path.exists(cron_path): + cron_s = public.readFile(cron_path) + if cron_s.find(echo) == -1: + public.M('crontab').where('echo=?', (echo,)).setField('status', 0) + args_obj.id = cron_id + crontab.crontab().set_cron_status(args_obj) + except: + pass + + # 获取当前正在使用此证书的网站目录 + def get_ssl_used_site(self, save_path): + pkey_file = '{}/privkey.pem'.format(save_path) + pkey = public.readFile(pkey_file) + if not pkey: return False + cert_paths = 'vhost/cert' + import panelSite + args = public.dict_obj() + args.siteName = '' + for c_name in os.listdir(cert_paths): + skey_file = '{}/{}/privkey.pem'.format(cert_paths, c_name) + skey = public.readFile(skey_file) + if not skey: continue + if skey == pkey: + args.siteName = c_name + run_path = panelSite.panelSite().GetRunPath(args) + if not run_path: continue + sitePath = public.M('sites').where('name=?', c_name).getField('path') + if not sitePath: continue + to_path = "{}/{}".format(sitePath, run_path) + return to_path + return False + + def get_site_id(self, domains): + site_ids = [] + for domain in domains: + if '*' in domain: + continue + site_id = public.M('domain').where('name=?', (domain,)).field('pid').select() + if not site_id: + continue + site_ids.append(site_id[0]['pid']) + if not site_ids: + return False + site_ids = list(set(site_ids)) + if not len(site_ids) == 1: + return False + return site_ids[0] + + def get_site_runpath(self, domains): + site_id = self.get_site_id(domains) + if not site_id: + return False + import panelSite + from collections import namedtuple + ps = panelSite.panelSite() + # 构造一个类 + get = namedtuple("get", ["id"]) + get.id = site_id + site_path = public.M('sites').where('id=?', (get.id,)).field('path').select()[0]['path'] + runpath = ps.GetRunPath(get) + return site_path + runpath + + def find_site_stopped(self, domains): + site_id = self.get_site_id(domains) + if not site_id: + return False + site_status = public.M('sites').where('id=?', (site_id,)).field('status').select()[0]['status'] + return site_status + + def get_index(self, domains): + ''' + @name 获取标识 + @author hwliang<2022-02-10> + @param domains 域名列表 + @return string + ''' + identifiers = [] + for domain_name in domains: + identifiers.append({"type": 'dns', "value": domain_name}) + return public.md5(json.dumps(identifiers)) + + # 续签同品牌其它证书 + def renew_cert_other(self): + ''' + @name 续签同品牌其它证书 + @author hwliang<2022-02-10> + @return void + ''' + cert_path = "{}/vhost/cert".format(public.get_panel_path()) + if not os.path.exists(cert_path): return + new_time = time.time() + (86400 * 30) + n = 0 + if not 'orders' in self._config: self._config['orders'] = {} + import panelSite + siteObj = panelSite.panelSite() + args = public.dict_obj() + for siteName in os.listdir(cert_path): + try: + cert_file = '{}/{}/fullchain.pem'.format(cert_path, siteName) + if not os.path.exists(cert_file): continue # 无证书文件 + siteInfo = public.M('sites').where('name=?', siteName).find() + if not siteInfo: continue # 无网站信息 + cert_init = self.get_cert_init(cert_file) + if not cert_init: continue # 无法获取证书 + end_time = time.mktime(time.strptime(cert_init['notAfter'], '%Y-%m-%d')) + if end_time > new_time: continue # 未到期 + try: + if not cert_init['issuer'] in ['R3', "Let's Encrypt"] and cert_init['issuer'].find( + "Let's Encrypt") == -1: + continue # 非同品牌证书 + except: + continue + + if isinstance(cert_init['dns'], str): cert_init['dns'] = [cert_init['dns']] + index = self.get_index(cert_init['dns']) + if index in self._config['orders'].keys(): continue # 已在订单列表 + + n += 1 + write_log("|-Renewing additional certificate {}, domain name:{}..".format(n, cert_init['subject'])) + write_log("|-Creating order..") + args.id = siteInfo['id'] + runPath = siteObj.GetRunPath(args) + if runPath and not runPath in ['/']: + path = siteInfo['path'] + '/' + runPath + else: + path = siteInfo['path'] + + self.renew_cert_to(cert_init['dns'], 'http', path.replace('//', '/')) + except: + write_log("|-Renewal failed:") + + # 关闭强制https + def close_httptohttps(self, siteName): + try: + + if not siteName: siteName + import panelSite + site_obj = panelSite.panelSite() + if not site_obj.IsToHttps(siteName): + return False + get = public.dict_obj() + get.siteName = siteName + site_obj.CloseToHttps(get) + return True + except: + return False + + # 恢复强制https + def rep_httptohttps(self, siteName): + try: + if not siteName: return False + import panelSite + site_obj = panelSite.panelSite() + if not site_obj.IsToHttps(siteName): + get = public.dict_obj() + get.siteName = siteName + site_obj.HttpToHttps(get) + return True + except: + return False + + def renew_cert_to(self, domains, auth_type, auth_to, index=None): + siteName = None + cert = {} + if os.path.exists(auth_to): + if public.M('sites').where('path=?', auth_to).count() == 1: + site_id = public.M('sites').where('path=?', auth_to).getField('id') + siteName = public.M('sites').where('path=?', auth_to).getField('name') + import panelSite + siteObj = panelSite.panelSite() + args = public.dict_obj() + args.id = site_id + runPath = siteObj.GetRunPath(args) + if runPath and not runPath in ['/']: + path = auth_to + '/' + runPath + if os.path.exists(path): auth_to = path.replace('//', '/') + + else: + siteName = self.get_site_name_by_domains(domains) + is_rep = self.close_httptohttps(siteName) + try: + index = self.create_order( + domains, + auth_type, + auth_to.replace('//', '/'), + index + ) + + write_log("|-Getting verification information..") + self.get_auths(index) + write_log("|-Verifying domain name..") + self.auth_domain(index) + write_log("|-Sending CSR..") + self.remove_dns_record() + self.send_csr(index) + write_log("|-Downloading certificate..") + cert = self.download_cert(index) + self._config['orders'][index]['renew_time'] = int(time.time()) + + # 清理失败重试记录 + self._config['orders'][index]['retry_count'] = 0 + self._config['orders'][index]['next_retry_time'] = 0 + + # 保存证书配置 + self.save_config() + cert['status'] = True + cert['msg'] = 'Renewed successfully!' + write_log("|-Renewed successfully!!") + except Exception as e: + if str(e).find('please try again later') == -1: # 受其它证书影响和连接CA失败的的不记录重试次数 + if index: + # 设置下次重试时间 + self._config['orders'][index]['next_retry_time'] = int(time.time() + (86400 * 2)) + # 记录重试次数 + if not 'retry_count' in self._config['orders'][index].keys(): + self._config['orders'][index]['retry_count'] = 1 + self._config['orders'][index]['retry_count'] += 1 + # 保存证书配置 + self.save_config() + msg = str(e).split('>>>>')[0] + write_log("|-" + msg) + return public.returnMsg(False, msg) + finally: + if is_rep: self.rep_httptohttps(siteName) + write_log("-" * 70) + return cert + + # 续签证书 + def renew_cert(self, index): + write_log("", "wb+") + try: + order_index = [] + if index: + if type(index) != str: + index = index.index + # 在面板点击申请证书时不要重启面板以防后续请求出错 + self._by_panel = True + if index not in self._config['orders']: + raise Exception( + public.get_msg_gettext('The specified order number does not exist and cannot be renewed!')) + order_index.append(index) + else: + s_time = time.time() + (30 * 86400) + if not 'orders' in self._config: self._config['orders'] = {} + for i in self._config['orders'].keys(): + if not 'save_path' in self._config['orders'][i]: + continue + if 'cert' in self._config['orders'][i]: + self._config['orders'][i]['cert_timeout'] = self._config['orders'][i]['cert']['cert_timeout'] + if not 'cert_timeout' in self._config['orders'][i]: + self._config['orders'][i]['cert_timeout'] = int(time.time()) + if self._config['orders'][i]['cert_timeout'] > s_time or self._config['orders'][i][ + 'auth_to'] == 'dns': + continue + if self.find_site_stopped(self._config['orders'][i]['domains']) == '0': + write_log("|-The website has been suspended, skip certificate renewal!") + continue + + # 已删除的网站直接跳过续签 + if self._config['orders'][i]['auth_to'].find('|') == -1 and self._config['orders'][i][ + 'auth_to'].find('/') != -1: + #if not os.path.exists(self._config['orders'][i]['auth_to']): + # ^——这个不能判断网站已被删除,但文件夹未删除的问题 + _auth_to = self.get_ssl_used_site(self._config['orders'][i]['save_path']) + if not _auth_to: continue + + # 域名不存在? + for domain in self._config['orders'][i]['domains']: + if domain.find('*') != -1: break + if not public.M('domain').where("name=?", (domain,)).count() and not public.M( + 'binding').where("domain=?", domain).count(): + _auth_to = None + write_log("|-Skip deleted domains:{}".format(self._config['orders'][i]['domains'])) + if not _auth_to: continue + + self._config['orders'][i]['auth_to'] = _auth_to + + # 是否到了允许重试的时间 + if 'next_retry_time' in self._config['orders'][i]: + timeout = self._config['orders'][i]['next_retry_time'] - int(time.time()) + if timeout > 0: + write_log( + '|-The domain name skipped this time: {}, because the last renewal failed, you still need to wait {} hours and try again'.format( + self._config['orders'][i]['domains'], int(timeout / 60 / 60))) + continue + + # # 是否到了最大重试次数 + # if 'retry_count' in self._config['orders'][i]: + # if self._config['orders'][i]['retry_count'] >= 5: + # write_log('|-Skip the domain name this time: {}, this certificate will not be renewed due to failure to renew 5 times in a row, (The number of errors will be reset after manual renewal and success)'.format(self._config['orders'][i]['domains'])) + # continue + + # 加入到续签订单 + order_index.append(i) + if not order_index: + write_log(public.get_msg_gettext('|-No SSL certificate found within 30 days!')) + self.get_apis() + self.renew_cert_other() + write_log("|-All tasks have been processed!") + return public.return_message(0,0,'No SSL certificate found within 30 days!') + write_log( + public.get_msg_gettext('|-A total of {} certificates need to be renewed', (str(len(order_index)),))) + n = 0 + self.get_apis() + cert = None + args = public.to_dict_obj({}) + for index in order_index: + args.domains = json.dumps(self._config['orders'][index]['domains']) + args.auth_type = self._config['orders'][index]['auth_type'] + args.auth_to = self._config['orders'][index]['auth_to'] + sitename = args.auth_to.split('/')[-1] + if not sitename: + sitename = self._config['orders'][index]['auth_to'].split('/')[-2] + args.siteName = sitename + write_log('|-Renew the visa certificate and start checking the environment') + self.check_auth_env(args, check=True) + n += 1 + + domains = _test_domains(self._config['orders'][index]['domains'], self._config['orders'][index]['auth_to'],self._config['orders'][index]['auth_type']) + if len(domains) == 0: + write_log("|-The domain name under the {} certificate is not used (the domain name is: [%s]) and has been skipped.".format(n, ",".join(self._config['orders'][index]['domains']))) + continue + else: + self._config['orders'][index]['domains'] = domains + write_log(public.get_msg_gettext('|-Renewing certificate number of {},domain: {}..', + (str(n), str(self._config['orders'][index]['domains'])))) + write_log(public.get_msg_gettext('|-Creating order..')) + + cert = self.renew_cert_to(self._config['orders'][index]['domains'], + self._config['orders'][index]['auth_type'], + self._config['orders'][index]['auth_to'], index) + + # write_log(public.get_msg_gettext('|-Renewing certificate number of {},domain: {}..', + # (str(n), str(self._config['orders'][index]['domains'])))) + # write_log(public.get_msg_gettext('|-Creating order..')) + # cert = self.renew_cert_to(self._config['orders'][index]['domains'], + # self._config['orders'][index]['auth_type'], + # self._config['orders'][index]['auth_to'], index) + + # aapanel 用 + try: + self.turnon_redirect_proxy_httptohttps(args) + except: + pass + + return cert + except Exception as ex: + self.remove_dns_record() + ex = str(ex) + if ex.find(">>>>") != -1: + msg = ex.split(">>>>") + msg[1] = json.loads(msg[1]) + else: + msg = ex + write_log(public.get_error_info()) + return_message=public.return_msg_gettext(False, msg) + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + +def _test_domains(domains, auth_to, auth_type): + # 检查站点域名变更情况, 若有删除域名,则在续签时,删除已经不使用的域名,再执行续签任务 + # 是dns验证的跳过 + if auth_to.find("|") != -1: + return domains + # 是泛域名的跳过 + for domain in domains: + if domain.find("*.") != -1: + return domains + sql = public.M('domain') + site_sql = public.M('sites') + for domain in domains: + pid = sql.where('name=?', domain).getField('pid') + if pid and site_sql.where('id=?',pid).find(): + site_domains = [i["name"] for i in sql.where('pid=?',(pid,)).field("name").select()] + break + else: + site_id = site_sql.where('path=?', auth_to).getField('id') + if bool(site_id) and str(site_id).isdigit(): + site_domains = [i["name"] for i in sql.where('pid=?',(site_id,)).field("name").select()] + else: + return [] + + del_domains = list(set(domains) - set(site_domains)) + for i in del_domains: + domains.remove(i) + return domains + + +def echo_err(msg): + write_log("\033[31m=" * 65) + write_log("|-error: {}\033[0m".format(msg)) + exit() + + +# 写日志 +def write_log(log_str, mode="ab+"): + if __name__ == "__main__": + print(log_str) + return + _log_file = 'logs/letsencrypt.log' + f = open(_log_file, mode) + log_str += "\n" + f.write(log_str.encode('utf-8')) + f.close() + return True + + +if __name__ == "__main__": + import argparse + + p = argparse.ArgumentParser(usage=public.get_msg_gettext( + 'Required parameters: --domain list of domain names, multiple separated by commas!')) + p.add_argument('--domain', default=None, + help=public.get_msg_gettext('Please specify the domain name to apply for a certificate'), + dest="domains") + p.add_argument('--type', default=None, help=public.get_msg_gettext('Please specify verification type'), + dest="auth_type") + p.add_argument('--path', default=None, help=public.get_msg_gettext('Please specify the website document root'), + dest="path") + p.add_argument('--dnsapi', default=None, help=public.get_msg_gettext('Please specify DNSAPI'), dest="dnsapi") + p.add_argument('--dns_key', default=None, help=public.get_msg_gettext('Please specify DNSAPI key'), dest="key") + p.add_argument('--dns_secret', default=None, help=public.get_msg_gettext('Please specify DNSAPI secret'), + dest="secret") + p.add_argument('--index', default=None, help=public.get_msg_gettext('Specify the order index'), dest="index") + p.add_argument('--renew', default=None, help=public.get_msg_gettext('renew certificate'), dest="renew") + p.add_argument('--revoke', default=None, help=public.get_msg_gettext('Revoke certificate'), dest="revoke") + args = p.parse_args() + cert = None + if args.revoke: + if not args.index: + echo_err( + public.get_msg_gettext('Please enter the index of the order to be revoked in the --index parameter')) + p = acme_v2() + result = p.revoke_order(args.index) + write_log(result) + exit() + + if args.renew: + p = acme_v2() + p.renew_cert(args.index) + else: + try: + if not args.index: + if not args.domains: + echo_err(public.get_msg_gettext( + 'Please specify the domain name for which you want to apply for a certificate in the --domain parameter, multiple separated by commas (,)')) + if not args.auth_type in ['http', 'tls', 'dns']: + echo_err(public.get_msg_gettext( + 'Please specify the correct authentication type in the --type parameter, supporting dns and http')) + auth_to = '' + if args.auth_type in ['http', 'tls']: + if not args.path: + echo_err( + public.get_msg_gettext('Please specify the website document root in the --path parameter!')) + if not os.path.exists(args.path): + echo_err(public.get_msg_gettext('The specified site root does not exist, please check: {}', + (args.path,))) + auth_to = args.path + else: + if args.dnsapi == '0': + auth_to = 'dns' + else: + if not args.key: + echo_err(public.get_msg_gettext( + 'When applying using dnsapi, specify the dnsapi key in the --dns_key parameter!')) + if not args.secret: + echo_err(public.get_msg_gettext( + 'When applying using dnsapi, specify the secret of dnsapi in the --dns_secret parameter!')) + auth_to = "{}|{}|{}".format( + args.dnsapi, args.key, args.secret) + + domains = args.domains.strip().split(',') + p = acme_v2() + cert = p.apply_cert( + domains, auth_type=args.auth_type, auth_to=auth_to) + if args.dnsapi == '0': + acme_txt = '_acme-challenge.' + acme_caa = '1 issue letsencrypt.org' + write_log("=" * 65) + write_log("\033[32m" + public.get_msg_gettext( + '|-Manual order submission is successful, please resolve DNS records according to the following tips: ') + "\033[0m") + write_log("=" * 65) + write_log(public.get_msg_gettext('|-Order index: {}', (cert['index'],))) + write_log(public.get_msg_gettext('|-Retry the command') + ": ./acme_v2.py --index=\"{}\"".format( + cert['index'])) + write_log(public.get_msg_gettext( + '|-A total of \033[36m{}\033[0m domain name records need to be resolved.', + (len(cert['auths']),))) + for i in range(len(cert['auths'])): + write_log('-' * 70) + write_log(public.get_msg_gettext( + '|-The \033[36m{}\033[0m domain names are: {}, please resolve the following information: ', + (str(i + 1), cert['auths'][i]['domain']))) + write_log(public.get_msg_gettext( + '|-Record Type: TXT Record Name: \033[41m{}\033[0m Record Value: \033[41m{}\033 [0m [Required]', + (acme_txt + cert['auths'][i]['domain'].replace('*.', ''), cert['auths'][i]['auth_value']))) + write_log(public.get_msg_gettext( + '|-Record type: CAA Record name: \033[41m{}\033[0m Record value: \033[41m{}\033[0m [Optional]', + (cert['auths'][i]['domain'].replace('*.', ''), acme_caa))) + write_log('-' * 70) + input_data = "" + while input_data not in ['y', 'Y', 'n', 'N']: + input_msg = public.get_msg_gettext( + 'Please wait 2-3 minutes after completing the resolution and enter Y and press Enter to continue verifying the domain name: ') + if sys.version_info[0] == 2: + input_data = raw_input(input_msg) + else: + input_data = input(input_msg) + if input_data in ['n', 'N']: + write_log("=" * 65) + write_log(public.get_msg_gettext('|-The user abandons the application and exits the program!')) + exit() + cert = p.apply_cert( + [], auth_type=args.auth_type, auth_to='dns', index=cert['index']) + else: + # 重新验证 + p = acme_v2() + cert = p.apply_cert([], auth_type='dns', + auth_to='dns', index=args.index) + except Exception as ex: + write_log("|-{}".format(public.get_error_info())) + exit() + if not cert: + exit() + write_log("=" * 65) + write_log(public.get_msg_gettext('|-Certificate obtained successfully!')) + write_log("=" * 65) + write_log(public.get_msg_gettext('Certified Domain Name: {}', (','.join(cert['domains']),))) + write_log( + public.get_msg_gettext('Certificate expiration time: {}', (public.format_date(times=cert['cert_timeout']),))) + write_log(public.get_msg_gettext('Certificate saved at: {}/', (cert['save_path'],))) diff --git a/class_v2/ajax_v2.py b/class_v2/ajax_v2.py new file mode 100644 index 00000000..9315f9a5 --- /dev/null +++ b/class_v2/ajax_v2.py @@ -0,0 +1,1956 @@ + #coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +from flask import session,request +import public,os,json,time,apache,psutil +from public.validate import Param +class ajax: + __official_url = 'https://brandnew.aapanel.com' + + def GetApacheStatus(self,get): + a = apache.apache() + return a.GetApacheStatus() + + def GetProcessCpuPercent(self,i,process_cpu): + try: + pp = psutil.Process(i) + if pp.name() not in process_cpu.keys(): + process_cpu[pp.name()] = float(pp.cpu_percent(interval=0.01)) + process_cpu[pp.name()] += float(pp.cpu_percent(interval=0.01)) + except: + pass + def GetNginxStatus(self,get): + try: + if not os.path.exists('/www/server/nginx/sbin/nginx'): return public.return_msg_gettext(False,'Nginx is not install') + process_cpu = {} + worker = int(public.ExecShell("ps aux|grep nginx|grep 'worker process'|wc -l")[0])-1 + workermen = int(public.ExecShell("ps aux|grep nginx|grep 'worker process'|awk '{memsum+=$6};END {print memsum}'")[0]) / 1024 + for proc in psutil.process_iter(): + if proc.name() == "nginx": + self.GetProcessCpuPercent(proc.pid,process_cpu) + time.sleep(0.1) + #取Nginx负载状态 + self.CheckStatusConf() + result = public.httpGet('http://127.0.0.1/nginx_status') + is_curl = False + tmp = [] + if result: + tmp = result.split() + if len(tmp) < 15: is_curl = True + + if is_curl: + result = public.ExecShell( + 'curl http://127.0.0.1/nginx_status')[0] + tmp = result.split() + data = {} + if "request_time" in tmp: + data['accepts'] = tmp[8] + data['handled'] = tmp[9] + data['requests'] = tmp[10] + data['Reading'] = tmp[13] + data['Writing'] = tmp[15] + data['Waiting'] = tmp[17] + else: + data['accepts'] = tmp[9] + data['handled'] = tmp[7] + data['requests'] = tmp[8] + data['Reading'] = tmp[11] + data['Writing'] = tmp[13] + data['Waiting'] = tmp[15] + data['active'] = tmp[2] + data['worker'] = worker + data['workercpu'] = round(float(process_cpu["nginx"]), 2) + data['workermen'] = "%s%s" % (int(workermen), "MB") + return data + except Exception as ex: + public.write_log_gettext('Get Info','Nginx load status acquisition failed:{}',(ex,)) + return public.return_msg_gettext(False,'Data acquisition failed!') + + def GetPHPStatus(self,get): + #取指定PHP版本的负载状态 + try: + version = get.version + uri = "/phpfpm_"+version+"_status?json" + result = public.request_php(version,uri,'') + tmp = json.loads(result) + fTime = time.localtime(int(tmp['start time'])) + tmp['start time'] = time.strftime('%Y-%m-%d %H:%M:%S',fTime) + return tmp + except Exception as ex: + public.write_log_gettext('Get Info',"PHP load status acquisition failed: {}",(public.get_error_info(),)) + return public.return_msg_gettext(False,'PHP load status acquisition failed!') + + def CheckStatusConf(self): + if public.get_webserver() != 'nginx': return + filename = session['setupPath'] + '/panel/vhost/nginx/phpfpm_status.conf' + if os.path.exists(filename): + if public.ReadFile(filename).find('nginx_status')!=-1: return + + conf = '''server { + listen 80; + server_name 127.0.0.1; + allow 127.0.0.1; + location /nginx_status { + stub_status on; + access_log off; + } +}''' + public.writeFile(filename,conf) + public.serviceReload() + + + def GetTaskCount(self,get): + # 校验参数 + try: + get.validate([ + Param('action').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + #取任务数量 + return public.return_message(0,0,public.M('tasks').where("status!=?",('1',)).count()) + + def GetSoftList(self,get): + #取软件列表 + import json,os + tmp = public.readFile('data/softList.conf') + data = json.loads(tmp) + tasks = public.M('tasks').where("status!=?",('1',)).field('status,name').select() + for i in range(len(data)): + data[i]['check'] = public.GetConfigValue('root_path')+'/'+data[i]['check'] + for n in range(len(data[i]['versions'])): + #处理任务标记 + isTask = '1' + for task in tasks: + tmp = public.getStrBetween('[',']',task['name']) + if not tmp:continue + tmp1 = tmp.split('-') + if data[i]['name'] == 'PHP': + if tmp1[0].lower() == data[i]['name'].lower() and tmp1[1] == data[i]['versions'][n]['version']: isTask = task['status']; + else: + if tmp1[0].lower() == data[i]['name'].lower(): isTask = task['status'] + + #检查安装状态 + if data[i]['name'] == 'PHP': + data[i]['versions'][n]['task'] = isTask + checkFile = data[i]['check'].replace('VERSION',data[i]['versions'][n]['version'].replace('.','')) + else: + data[i]['task'] = isTask + version = public.readFile(public.GetConfigValue('root_path')+'/server/'+data[i]['name'].lower()+'/version.pl') + if not version:continue + if version.find(data[i]['versions'][n]['version']) == -1:continue + checkFile = data[i]['check'] + data[i]['versions'][n]['status'] = os.path.exists(checkFile) + return public.return_message(0,0,data) + + + def GetLibList(self,get): + #取插件列表 + import json,os + tmp = public.readFile('data/libList.conf') + data = json.loads(tmp) + for i in range(len(data)): + data[i]['status'] = self.CheckLibInstall(data[i]['check']) + data[i]['optstr'] = self.GetLibOpt(data[i]['status'], data[i]['opt']) + return data + + def CheckLibInstall(self,checks): + for cFile in checks: + if os.path.exists(cFile): return public.GetMsg('Already installed') + return public.GetMsg('Not installed') + + #取插件操作选项 + def GetLibOpt(self,status,libName): + optStr = '' + if status == public.GetMsg('Not installed'): + optStr = ''+public.GetMsg('Uninstallaton succeeded')+'' + else: + libConfig = public.GetMsg('Old configuration') + if(libName == 'beta'): libConfig = public.GetMsg('Beta tester profile') + + optStr = ''+libConfig+' | '+public.get_msg_gettext("Uninstallaton succeeded")+''; + return optStr + + #取插件AS + def GetQiniuAS(self,get): + filename = public.GetConfigValue('setup_path') + '/panel/data/'+get.name+'As.conf' + if not os.path.exists(filename): public.writeFile(filename,'') + data = {} + data['AS'] = public.readFile(filename).split('|') + data['info'] = self.GetLibInfo(get.name) + if len(data['AS']) < 3: + data['AS'] = ['','','',''] + return data + + + #设置插件AS + def SetQiniuAS(self,get): + info = self.GetLibInfo(get.name) + filename = public.GetConfigValue('setup_path') + '/panel/data/'+get.name+'As.conf' + conf = get.access_key.strip() + '|' + get.secret_key.strip() + '|' + get.bucket_name.strip() + '|' + get.bucket_domain.strip() + public.writeFile(filename,conf) + if not os.path.exists(filename): + return public.return_msg_gettext(False, 'write file failed!') + public.ExecShell("chmod 600 " + filename) + result = public.ExecShell(public.get_python_bin() + " " + public.GetConfigValue('setup_path') + "/panel/script/backup_"+get.name+".py list") + + if result[0].find("ERROR:") == -1: + public.write_log_gettext("Plugin manager","Set plugin [" +info['name']+ "]AS!") + return public.return_msg_gettext(True, 'Successfully set') + return public.return_msg_gettext(False,'ERROR: Unable to connect to the {} server, please check if the [AK/SK/Storage] setting is correct!',(info['name'],)) + + #设置内测 + def SetBeta(self,get): + data = {} + data['username'] = get.bbs_name + data['qq'] = get.qq + data['email'] = get.email + result = public.httpPost(public.GetConfigValue('home') + '/Api/LinuxBeta',data) + import json + data = json.loads(result) + if data['status']: + public.writeFile('data/beta.pl',get.bbs_name + '|' + get.qq + '|' + get.email) + return data + #取内测资格状态 + def GetBetaStatus(self,get): + try: + return public.readFile('data/beta.pl').strip() + except: + return 'False' + + + #获取指定插件信息 + def GetLibInfo(self,name): + import json + tmp = public.readFile('data/libList.conf') + data = json.loads(tmp) + for lib in data: + if name == lib['opt']: return lib + return False + + #获取文件列表 + def GetQiniuFileList(self,get): + try: + import json + result = public.ExecShell(public.get_python_bin() + " " + public.GetConfigValue('setup_path') + "/panel/script/backup_"+get.name+".py list") + return json.loads(result[0]) + except: + return public.return_msg_gettext(False, 'Failed to get the list, please check if the [AK/SK/Storage] setting is correct!') + + + + #取网络连接列表 + def GetNetWorkList(self,get): + import psutil + netstats = psutil.net_connections() + networkList = [] + for netstat in netstats: + tmp = {} + if netstat.type == 1: + tmp['type'] = 'tcp' + else: + tmp['type'] = 'udp' + tmp['family'] = netstat.family + tmp['laddr'] = netstat.laddr + tmp['raddr'] = netstat.raddr + tmp['status'] = netstat.status + p = psutil.Process(netstat.pid) + tmp['process'] = p.name() + tmp['pid'] = netstat.pid + networkList.append(tmp) + del (p) + del (tmp) + networkList = sorted(networkList, + key=lambda x: x['status'], + reverse=True) + return networkList + + #取进程列表 + def GetProcessList(self, get): + import psutil, pwd + Pids = psutil.pids() + + processList = [] + for pid in Pids: + try: + tmp = {} + p = psutil.Process(pid) + if p.exe() == "": continue + + tmp['name'] = p.name() + #进程名称 + if self.GoToProcess(tmp['name']): continue + + tmp['pid'] = pid + #进程标识 + tmp['status'] = p.status() + #进程状态 + tmp['user'] = p.username() + #执行用户 + cputimes = p.cpu_times() + tmp['cpu_percent'] = p.cpu_percent(0.1) + tmp['cpu_times'] = cputimes.user #进程占用的CPU时间 + tmp['memory_percent'] = round(p.memory_percent(), + 3) #进程占用的内存比例 + pio = p.io_counters() + tmp['io_write_bytes'] = pio.write_bytes #进程总共写入字节数 + tmp['io_read_bytes'] = pio.read_bytes #进程总共读取字节数 + tmp['threads'] = p.num_threads() #进程总线程数 + + processList.append(tmp) + del (p) + del (tmp) + except: + continue + import operator + processList = sorted(processList, + key=lambda x: x['memory_percent'], + reverse=True) + processList = sorted(processList, + key=lambda x: x['cpu_times'], + reverse=True) + return processList + + #结束指定进程 + def KillProcess(self, get): + #return public.returnMsg(False,'演示服务器,禁止此操作!'); + import psutil + p = psutil.Process(int(get.pid)) + name = p.name() + if name == 'python': return public.return_msg_gettext(False,'Error, cannot end task processes!') + + p.kill() + public.write_log_gettext('Task manager','Ended processes[{}][{}] Successfully!',(get.pid,name)) + return public.return_msg_gettext(True,'Ended processes[{}][{}] Successfully!',(get.pid,name)) + + def GoToProcess(self,name): + ps = ['sftp-server','login','nm-dispatcher','irqbalance','qmgr','wpa_supplicant','lvmetad','auditd','master','dbus-daemon','tapdisk','sshd','init','ksoftirqd','kworker','kmpathd','kmpath_handlerd','python','kdmflush','bioset','crond','kthreadd','migration','rcu_sched','kjournald','iptables','systemd','network','dhclient','systemd-journald','NetworkManager','systemd-logind','systemd-udevd','polkitd','tuned','rsyslogd'] + + for key in ps: + if key == name: return True + + return False + + + def GetNetWorkIo(self,get): + #取指定时间段的网络Io + data = public.M('network').dbfile('system').where( + "addtime>=? AND addtime<=?", (get.start, get.end) + ).field( + 'id,up,down,total_up,total_down,down_packets,up_packets,addtime' + ).order('id desc').select() + return self.ToAddtime(data, None) + + def GetDiskIo(self, get): + #取指定时间段的磁盘Io + __OPT_FIELD = "*" + tmp_cols = public.M('diskio').dbfile('system').query( + 'PRAGMA table_info(diskio)', ()) + cols = [] + for col in tmp_cols: + if len(col) > 2: cols.append('`' + col[1] + '`') + if len(cols) > 0: + cols.append("disk_top") + __OPT_FIELD = ','.join(cols) + data = public.M('diskio').dbfile('system').query( + "SELECT diskio.*,process_top_list.disk_top from diskio inner join process_top_list on diskio.addtime=process_top_list.addtime where diskio.addtime>={} AND diskio.addtime<={} ORDER BY diskio.addtime desc;" + .format(get.start, get.end), ()) + if isinstance(data, str) and data.find( + 'error: no such table: process_top_list') != -1: + return public.M('diskio').dbfile('system').where( + "addtime>=? AND addtime<=?", (get.start, get.end) + ).field( + 'id,read_count,write_count,read_bytes,write_bytes,read_time,write_time,addtime' + ).order('id asc').select() + try: + if __OPT_FIELD != "*": + fields = self.__format_field(__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 + except: + return [] + return self.ToAddtime(data, True, 'disk') + + + 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 GetCpuIo(self, get): + #取指定时间段的CpuIo + __OPT_FIELD = "*" + tmp_cols = public.M('cpuio').dbfile('system').query( + 'PRAGMA table_info(cpuio)', ()) + cols = [] + for col in tmp_cols: + if len(col) > 2: cols.append('`' + col[1] + '`') + if len(cols) > 0: + cols.append("cpu_top") + cols.append("memory_top") + __OPT_FIELD = ','.join(cols) + data = public.M('cpuio').dbfile('system').query( + "SELECT cpuio.*,process_top_list.cpu_top,process_top_list.memory_top from cpuio inner join process_top_list on cpuio.addtime=process_top_list.addtime where cpuio.addtime>={} AND cpuio.addtime<={} ORDER BY cpuio.addtime desc;" + .format(get.start, get.end), ()) + if isinstance(data, str) and data.find( + 'error: no such table: process_top_list') != -1: + return public.M('cpuio').dbfile('system').where( + "addtime>=? AND addtime<=?", + (get.start, get.end + )).field('id,pro,mem,addtime').order('id asc').select() + try: + if __OPT_FIELD != "*": + fields = self.__format_field(__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 + except: + return [] + return self.ToAddtime(data, True, 'cpu') + + def get_load_average(self, get): + __OPT_FIELD = "*" + tmp_cols = public.M('load_average').dbfile('system').query( + 'PRAGMA table_info(load_average)', ()) + cols = [] + for col in tmp_cols: + if len(col) > 2: cols.append('`' + col[1] + '`') + if len(cols) > 0: + cols.append("cpu_top") + __OPT_FIELD = ','.join(cols) + data = public.M('load_average').dbfile('system').query( + "SELECT load_average.*,process_top_list.cpu_top from load_average inner join process_top_list on load_average.addtime=process_top_list.addtime where load_average.addtime>={} AND load_average.addtime<={} ORDER BY load_average.addtime desc;" + .format(get.start, get.end), ()) + if isinstance(data, str) and data.find( + 'error: no such table: process_top_list') != -1: + return public.M('load_average').dbfile('system').where( + "addtime>=? AND addtime<=?", + (get.start, get.end)).field('id,pro,one,five,fifteen,addtime' + ).order('id asc').select() + try: + if __OPT_FIELD != "*": + fields = self.__format_field(__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 + except: + return [] + return self.ToAddtime(data, True, 'cpu') + + def get_process_tops(self, get): + ''' + @name 获取进程开销排行 + @author hwliang<2021-09-07> + @param get{ + start: int<开始时间> + end: int<结束时间> + } + @return list + ''' + data = public.M('process_tops').dbfile('system').where( + "addtime>=? AND addtime<=?", + (get.start, get.end + )).field('id,process_list,addtime').order('id asc').select() + return self.ToAddtime(data) + + def get_process_cpu_high(self, get): + ''' + @name 获取CPU占用高的进程列表 + @author hwliang<2021-09-07> + @param get{ + start: int<开始时间> + end: int<结束时间> + } + @return list + ''' + data = public.M('process_high_percent').dbfile('system').where( + "addtime>=? AND addtime<=?", (get.start, get.end)).field( + 'id,name,pid,cmdline,cpu_percent,memory,cpu_time_total,addtime' + ).order('id asc').select() + return self.ToAddtime(data) + + def ToAddtime(self, data, tomem=False, types=None): + import time + #格式化addtime列 + + if tomem: + import psutil + mPre = (psutil.virtual_memory().total / 1024 / 1024) / 100 + length = len(data) + he = 1 + if length > 100: he = 1 + if length > 1000: he = 3 + if length > 10000: he = 15 + if he == 1: + for i in range(length): + try: + if types: + key = '{}_top'.format(types) + if key in data[i]: + data[i][key] = json.loads(data[i][key]) + if 'memory_top' in data[i]: + data[i]['memory_top'] = json.loads( + data[i]['memory_top']) + data[i]['addtime'] = time.strftime( + '%m/%d %H:%M', + time.localtime(float(data[i]['addtime']))) + if 'process_list' in data[i]: + data[i]['process_list'] = json.loads( + data[i]['process_list']) + if tomem and data[i]['mem'] > 100: + data[i]['mem'] = data[i]['mem'] / mPre + if tomem in [None]: + if type(data[i]['down_packets']) == str: + data[i]['down_packets'] = json.loads( + data[i]['down_packets']) + data[i]['up_packets'] = json.loads( + data[i]['up_packets']) + except: + continue + return data + else: + count = 0 + tmp = [] + couns = 0 + for value in data: + if count < he: # 0 1 2 + count += 1 + #cpu大于60的时候,随机取 + if types == "cpu" and 'pro' in value and value['pro'] > 60: + couns += 1 + #he等于3 的时候 百分之50的概率取 当he等于15的时候 百分之33的概率取 + if (he == 3 + and couns % 2 == 0) or (he == 15 + and couns % 3 == 0): + if types: + key = '{}_top'.format(types) + if key in value: + value[key] = json.loads(value[key]) + if 'memory_top' in value: + value['memory_top'] = json.loads( + value['memory_top']) + value['addtime'] = time.strftime( + '%m/%d %H:%M', + time.localtime(float(value['addtime']))) + if tomem and 'mem' in value and value['mem'] > 100: + value['mem'] = value['mem'] / mPre + if tomem in [None]: + if type(value['down_packets']) == str: + value['down_packets'] = json.loads(value['down_packets']) + value['up_packets'] = json.loads(value['up_packets']) + tmp.append(value) + continue + try: + if types: + key='{}_top'.format(types) + if key in value: + value[key] = json.loads(value[key]) + if 'memory_top' in value: + value['memory_top'] = json.loads(value['memory_top']) + value['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(value['addtime']))) + if tomem and 'mem' in value and value['mem'] > 100: value['mem'] = value['mem'] / mPre + if tomem in [None]: + if type(value['down_packets']) == str: + value['down_packets'] = json.loads(value['down_packets']) + value['up_packets'] = json.loads(value['up_packets']) + tmp.append(value) + count = 0 + except: continue + return tmp + + + + def GetInstalleds(self,softlist): + softs = '' + for soft in softlist['data']: + try: + for v in soft['versions']: + if v['status']: softs += soft['name'] + '-' + v['version'] + '|' + except: + pass + return softs + + + + #获取SSH爆破次数 + def get_ssh_intrusion(self): + fp = open('/var/log/secure','rb') + l = fp.readline() + intrusion_total = 0 + while l: + if l.find('Failed password for root') != -1: intrusion_total += 1 + l = fp.readline() + fp.close() + return intrusion_total + + #申请内测版 + def apple_beta(self,get): + try: + # userInfo = json.loads(public.ReadFile('data/userInfo.json')) + # p_data = {} + # p_data['uid'] = userInfo['uid'] + # p_data['access_key'] = userInfo['access_key'] + # p_data['username'] = userInfo['username'] + # result = public.HttpPost(public.GetConfigValue('home') + '/api/panel/apple_beta',p_data,5) + public.writeFile('/www/server/panel/data/is_beta.pl','true') + try: + return public.return_message(0,0,"Successful application!") + except: return public.return_message(-1,0,'Fail to connect to the server!') + except: return public.return_message(-1,0,'Please bind your account first!') + + def to_not_beta(self,get): + try: + # userInfo = json.loads(public.ReadFile('data/userInfo.json')) + # p_data = {} + # p_data['uid'] = userInfo['uid'] + # p_data['access_key'] = userInfo['access_key'] + # p_data['username'] = userInfo['username'] + # result = public.HttpPost(public.GetConfigValue('home') + '/api/panel/to_not_beta',p_data,5) + try: + beta_file = '/www/server/panel/data/is_beta.pl' + if os.path.exists(beta_file): + os.remove(beta_file) + return public.return_message(0,0, "Successful application!") + except: return public.return_message(-1,0,'Fail to connect to the server!') + except: return public.return_message(-1,0,'Please bind your account first!') + + def to_beta(self): + try: + userInfo = json.loads(public.ReadFile('data/userInfo.json')) + p_data = {} + p_data['uid'] = userInfo['uid'] + p_data['access_key'] = userInfo['access_key'] + p_data['username'] = userInfo['username'] + public.HttpPost(public.GetConfigValue('home') + '/api/panel/to_beta',p_data,5) + except: pass + + def get_uid(self): + try: + userInfo = json.loads(public.ReadFile('data/userInfo.json')) + return userInfo['uid'] + except: return 0 + + #获取最新的5条测试版更新日志 + def get_beta_logs(self,get): + try: + data = json.loads(public.HttpGet('{}/api/panel/getBetaVersionLogs'.format(self.__official_url))) + return public.return_message(0,0,data) + except: + return public.return_message(-1,0,'Fail to connect to the server!') + + def get_other_info(self): + other = {} + other['ds'] = [] + ds = public.M('domain').field('name').select() + for d in ds: + other['ds'].append(d['name']) + return ','.join(other['ds']) + + + + # 更新面板 + def UpdatePanel(self,get): + if 'check' in get: + # 校验参数 + try: + get.validate([ + Param('check').Bool(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + import json + conf_status = public.M('config').where("id=?",('1',)).field('status').find() + if int(session['config']['status']) == 0 and int(conf_status['status']) == 0: + public.arequests('get', '{}/api/setupCount/setupPanel?type=Linux'.format(self.__official_url)) + public.M('config').where("id=?",('1',)).setField('status',1) + + #取回远程版本信息 + if 'updateInfo' in session and hasattr(get,'check') == False: + updateInfo = session['updateInfo'] + else: + logs = public.get_debug_log() + import psutil,system,sys + mem = psutil.virtual_memory() + import panelPlugin + mplugin = panelPlugin.panelPlugin() + + mplugin.ROWS = 10000 + panelsys = system.system() + data = {} + data['ds'] = ''#self.get_other_info() + data['sites'] = str(public.M('sites').count()) + data['ftps'] = str(public.M('ftps').count()) + data['databases'] = str(public.M('databases').count()) + data['system'] = panelsys.GetSystemVersion() + '|' + str(mem.total / 1024 / 1024) + 'MB|' + str(public.getCpuType()) + '*' + str(psutil.cpu_count()) + '|' + str(public.get_webserver()) + '|' +session['version'] + data['system'] += '||'+self.GetInstalleds(mplugin.getPluginList(None)) + data['logs'] = logs + data['client'] = request.headers.get('User-Agent') + data['oem'] = '' + data['intrusion'] = 0 + data['uid'] = self.get_uid() + #msg = public.getMsg('Current version is stable version and already latest. Update cycle of stable version is generally 2 months,while developer version will update every Wednesday!'); + data['o'] = public.get_oem_name() + sUrl = '{}/api/panel/updateLinuxEn'.format(self.__official_url) + + updateInfoRaw = public.httpPost(sUrl, data, timeout=60) + + if not updateInfoRaw or len(updateInfoRaw) == 0: + return public.return_message(-1, 0, 'Failed to connect server! -1') + + try: + updateInfo = json.loads(updateInfoRaw) + except: + return public.return_message(-1, 0, 'Failed to connect server! -2') + + session['updateInfo'] = updateInfo + + # 判断是否测试版 + updateInfo['is_beta'] = 0 + + if os.path.exists('/www/server/panel/data/is_beta.pl'): + updateInfo['is_beta'] = 1 + + # 输出忽略的版本 + updateInfo['ignore'] = [] + no_path = '{}/data/no_update.pl'.format(public.get_panel_path()) + if os.path.exists(no_path): + try: + updateInfo['ignore'] = json.loads(public.readFile(no_path)) + except: + pass + + # 重启面板 默认开启系统监控 + public.writeFile('data/control.conf', '30') + + # 判断本地版本是否最新 + updateInfo['local_is_latest'] = False + + #检查是否需要升级 + if not hasattr(get,'toUpdate'): + if updateInfo['is_beta'] == 1: + if updateInfo['beta']['version'] == session['version']: + updateInfo['local_is_latest'] = True + else: + if updateInfo['version'] == session['version']: + updateInfo['local_is_latest'] = True + + return public.return_message(0, 0, updateInfo) + + + #是否执行升级程序 + if(updateInfo['force'] == True or hasattr(get,'toUpdate') == True or os.path.exists('data/autoUpdate.pl') == True): + if not public.IsRestart(): + return public.return_message(-1, 0, 'Please run the program when all install tasks finished!') + + if updateInfo['is_beta'] == 1: updateInfo['version'] = updateInfo['beta']['version'] + setupPath = public.GetConfigValue('setup_path') + uptype = 'update' + httpUrl = public.get_url() + if httpUrl: updateInfo['downUrl'] = httpUrl + '/install/' + uptype + '/LinuxPanel_EN-' + updateInfo['version'] + '.zip' + public.downloadFile(updateInfo['downUrl'],'panel.zip') + if os.path.getsize('panel.zip') < 1048576: return public.return_message(-1,0,'File download failed, please try again or update manually!') + public.ExecShell('unzip -o panel.zip -d ' + setupPath + '/') + # import compileall + + # 清除pycache编译缓存 + remove_py_caches = [ + '{}/__pycache__'.format(public.get_panel_path()), + '{}/class/__pycache__'.format(public.get_panel_path()), + '{}/class_v2/__pycache__'.format(public.get_panel_path()), + ] + + for pycache_dir in remove_py_caches: + if os.path.exists(pycache_dir): + os.system('rm -rf {}'.format(pycache_dir)) + + # if os.path.exists('/www/server/panel/runserver.py'): public.ExecShell('rm -f /www/server/panel/*.pyc') + # if os.path.exists('/www/server/panel/class/common.py'): public.ExecShell('rm -f /www/server/panel/class/*.pyc') + + if os.path.exists('panel.zip'):os.remove("panel.zip") + session['version'] = updateInfo['version'] + if 'getCloudPlugin' in session: del(session['getCloudPlugin']) + if updateInfo['is_beta'] == 1: self.to_beta() + public.ExecShell("/etc/init.d/bt start") + public.writeFile('data/restart.pl','True') + return public.return_message(0,0, public.gettext_msg('Successful to update to {}',(updateInfo['version'],))) + + public.ExecShell('rm -rf /www/server/phpinfo/*') + return public.return_message(0,0,updateInfo) + except Exception as ex: + return public.return_message(-1,0,public.get_error_info()) + # return public.return_message(-1,0,'Failed to connect server!') + + #检查是否安装任何 + def CheckInstalled(self,get): + checks = ['nginx','apache','php','pure-ftpd','mysql'] + import os + for name in checks: + filename = public.GetConfigValue('root_path') + "/server/" + name + if os.path.exists(filename): return True + return False + + + #取已安装软件列表 + def GetInstalled(self,get): + import system + data = system.system().GetConcifInfo() + return data + + #取PHP配置 + def GetPHPConfig(self,get): + import re,json + filename = public.GetConfigValue('setup_path') + '/php/' + get.version + '/etc/php.ini' + if public.get_webserver() == 'openlitespeed': + filename = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version,get.version[0],get.version[1]) + if os.path.exists('/etc/redhat-release'): + filename = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + if not os.path.exists(filename): return public.return_msg_gettext(False,'Requested PHP version does NOT exist!') + phpini = public.readFile(filename) + data = {} + rep = "disable_functions\\s*=\\s{0,1}(.*)\n" + + tmp = re.search(rep,phpini) + if tmp: + data['disable_functions'] = tmp.groups()[0] + + rep = r"upload_max_filesize\s*=\s*([0-9]+)(M|m|K|k)" + + tmp = re.search(rep,phpini) + if tmp: + data['max'] = tmp.groups()[0] + + rep = u"\n;*\\s*cgi\\.fix_pathinfo\\s*=\\s*([0-9]+)\\s*\n" + tmp = re.search(rep,phpini) + if tmp: + if tmp.groups()[0] == '0': + data['pathinfo'] = False + else: + data['pathinfo'] = True + + self.getCloudPHPExt(get) + phplib = json.loads(public.readFile('data/phplib.conf')) + libs = [] + tasks = public.M('tasks').where("status!=?",('1',)).field('status,name').select() + phpini_ols = None + for lib in phplib: + lib['task'] = '1' + for task in tasks: + tmp = public.getStrBetween('[',']',task['name']) + if not tmp:continue + tmp1 = tmp.split('-') + if tmp1[0].lower() == lib['name'].lower(): + lib['task'] = task['status'] + lib['phpversions'] = [] + lib['phpversions'].append(tmp1[1]) + if public.get_webserver() == 'openlitespeed': + lib['status'] = False + get.php_version = "{}.{}".format(get.version[0],get.version[1]) + if not phpini_ols: + phpini_ols = self.php_info(get)['phpinfo']['modules'].lower() + phpini_ols = phpini_ols.split() + for i in phpini_ols: + if lib['check'][:-3].lower() == i : + lib['status'] = True + break + if "ioncube" in lib['check'][:-3].lower() and "ioncube" == i: + lib['status'] = True + break + else: + if phpini.find(lib['check']) == -1: + lib['status'] = False + else: + lib['status'] = True + + libs.append(lib) + + data['libs'] = libs + return data + + #获取PHP扩展 + def getCloudPHPExt(self,get): + try: + self._process_chinese_ext_description() + if 'php_ext' in session: return True + if not self._get_cloud_phplib(): + return False + session['php_ext'] = True + return True + except: + return False + + # 处理PHP插件变描述中文 + def _process_chinese_ext_description(self): + chinese = None + phplib = json.loads(public.readFile('data/phplib.conf')) + for p in phplib: + if "缓存器" in p['type']: + chinese = True + break + if chinese: + self._get_cloud_phplib() + + # 下载云端php扩展配置 + def _get_cloud_phplib(self): + if not session.get('download_url'): session['download_url'] = 'https://node.aapanel.com' + download_url = session['download_url'] + '/install/lib/phplib_en.json' + tstr = public.httpGet(download_url) + data = json.loads(tstr) + if not data: return False + public.writeFile('data/phplib.conf', json.dumps(data)) + return True + + #取PHPINFO信息 + def GetPHPInfo(self,get): + if public.get_webserver() == "openlitespeed": + shell_str = "/usr/local/lsws/lsphp{}/bin/php -i".format(get.version) + return public.ExecShell(shell_str)[0] + sPath = '/www/server/phpinfo' + if os.path.exists(sPath): + public.ExecShell("rm -rf " + sPath) + p_file = '/dev/shm/phpinfo.php' + public.writeFile(p_file,'') + phpinfo = public.request_php(get.version,'/phpinfo.php','/dev/shm') + if os.path.exists(p_file): os.remove(p_file) + return phpinfo.decode() + + #清理日志 + def delClose(self,get): + if not 'uid' in session: session['uid'] = 1 + if session['uid'] != 1: return public.return_msg_gettext(False,'Permission denied!') + if 'tmp_login_id' in session: + return public.return_msg_gettext(False,'Permission denied!') + + # 备份近100条日志 + new_bak = public.M('logs').limit('100').select() + if len(new_bak) > 3: + bak_file = '{}/data/logs.bak'.format(public.get_panel_path()) + public.writeFile(bak_file,json.dumps(new_bak)) + public.add_security_logs("清空日志", '清空所有日志条数为:{}'.format(public.M('logs').count())) + # 清空日志 + public.M('logs').where('id>?',(0,)).delete() + public.write_log_gettext('Panel setting','Panel Logs emptied!') + return public.return_msg_gettext(True,'Panel Logs emptied!') + + def __get_webserver_conffile(self): + webserver = public.get_webserver() + if webserver == 'nginx': + filename = public.GetConfigValue('setup_path') + '/nginx/conf/nginx.conf' + elif webserver == 'openlitespeed': + filename = public.GetConfigValue('setup_path') + "/panel/vhost/openlitespeed/detail/phpmyadmin.conf" + else: + filename = public.GetConfigValue('setup_path') + '/apache/conf/extra/httpd-vhosts.conf' + return filename + + # 获取phpmyadmin ssl配置 + def get_phpmyadmin_conf(self): + if public.get_webserver() == "nginx": + conf_file = "/www/server/panel/vhost/nginx/phpmyadmin.conf" + rep = r"listen\s*(\d+)" + else: + conf_file = "/www/server/panel/vhost/apache/phpmyadmin.conf" + rep = r"Listen\s*(\d+)" + return {"conf_file":conf_file,"rep":rep} + + # 设置phpmyadmin路径 + def set_phpmyadmin_session(self): + import re + conf_file = self.get_phpmyadmin_conf() + conf = public.readFile(conf_file["conf_file"]) + rep = conf_file["rep"] + if conf: + port = re.search(rep,conf).group(1) + if session['phpmyadminDir']: + path = session['phpmyadminDir'].split("/")[-1] + ip = public.GetHost() + session['phpmyadminDir'] = "https://{}:{}/{}".format(ip, port, path) + + # 获取phpmyadmin ssl状态 + def get_phpmyadmin_ssl(self,get): + import re + conf_file = self.get_phpmyadmin_conf() + conf = public.readFile(conf_file["conf_file"]) + rep = conf_file["rep"] + if conf: + port = re.search(rep, conf).group(1) + return public.success_v2({"status":True,"port":port}) + + return public.success_v2({"status":False,"port":""}) + + # 修改php ssl端口 + def change_phpmyadmin_ssl_port(self,get): + if public.get_webserver() == "openlitespeed": + return public.fail_v2('The current web server is openlitespeed. This function is not supported yet.') + + import re + try: + port = int(get.port) + if 1 > port > 65535: + return public.fail_v2('Port range is incorrect!') + except: + return public.fail_v2('Please enter the correct port number') + + for i in ["nginx","apache"]: + file = "/www/server/panel/vhost/{}/phpmyadmin.conf".format(i) + conf = public.readFile(file) + if not conf: + return public.fail_v2('Did not find the {} configuration file, please try to close the ssl port settings before opening',(i,)) + rulePort = ['80', '443', '21', '20', '8080', '8081', '8089', '11211', '6379'] + if get.port in rulePort: + return public.fail_v2('Please do NOT use the usual port as the phpMyAdmin port!') + + if i == "nginx": + if not os.path.exists("/www/server/panel/vhost/apache/phpmyadmin.conf"): + return public.fail_v2('Did not find the apache phpmyadmin ssl configuration file, please try to close the ssl port settings before opening') + + rep = r"listen\s*([0-9]+)\s*.*;" + oldPort = re.search(rep, conf) + if not oldPort: + return public.fail_v2('Did not detect the port that nginx phpmyadmin listens, please confirm whether the file has been manually modified.') + + oldPort = oldPort.groups()[0] + conf = re.sub(rep, 'listen ' + get.port + ' ssl;', conf) + else: + rep = r"Listen\s*([0-9]+)\s*\n" + oldPort = re.search(rep, conf) + if not oldPort: + return public.fail_v2('Did not detect the port that apache phpmyadmin listens, please confirm whether the file has been manually modified.') + + oldPort = oldPort.groups()[0] + conf = re.sub(rep, "Listen " + get.port + "\n", conf, 1) + rep = r"VirtualHost\s*\*:[0-9]+" + conf = re.sub(rep, "VirtualHost *:" + get.port, conf, 1) + if oldPort == get.port: + return public.fail_v2('Port [{}] is in use!',(get.port,)) + + public.writeFile(file, conf) + public.serviceReload() + if i=="apache": + import firewalls + # aapanel 使用 get_msg_gettext + get.ps = public.get_msg_gettext('New phpMyAdmin SSL Port') + fw = firewalls.firewalls() + fw.AddAcceptPort(get) + public.serviceReload() + public.write_log_gettext('Software manager', 'Modified access port to {} for phpMyAdmin!', (get.port,)) + get.id = public.M('firewall').where('port=?', (oldPort,)).getField('id') + get.port = oldPort + fw.DelAcceptPort(get) + + return public.success_v2('Setup successfully!') + + def _get_phpmyadmin_auth(self): + import re + nginx_conf = '/www/server/nginx/conf/nginx.conf' + reg = '#AUTH_START(.|\n)*#AUTH_END' + if os.path.exists(nginx_conf): + nginx_conf = public.readFile(nginx_conf) + auth_tmp = re.search(reg, nginx_conf) + if auth_tmp: + return True + apache_conf = '/www/server/apache/conf/extra/httpd-vhosts.conf' + if os.path.exists(apache_conf): + apache_conf = public.readFile(apache_conf) + auth_tmp = re.search(reg, apache_conf) + if auth_tmp: + return True + + # 设置phpmyadmin ssl + def set_phpmyadmin_ssl(self,get): + if public.get_webserver() == "openlitespeed": + return public.return_message(-1, 0, public.gettext_msg('The current web server is openlitespeed. This function is not supported yet.')) + + if not os.path.exists("/www/server/panel/ssl/certificate.pem"): + return public.return_message(-1, 0, public.gettext_msg('The panel certificate does not exist. Please apply for the panel certificate and try again.')) + + if get.v == "1": + # 获取auth信息 + auth = "" + if self._get_phpmyadmin_auth(): + auth = """ + #AUTH_START + auth_basic "Authorization"; + auth_basic_user_file /www/server/pass/phpmyadmin.pass; + #AUTH_END +""" + # nginx配置文件 + ssl_conf = r"""server + { + listen 887 ssl; + server_name phpmyadmin; + index index.html index.htm index.php; + root /www/server/phpmyadmin; + #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则 + #error_page 404/404.html; + ssl_certificate /www/server/panel/ssl/certificate.pem; + ssl_certificate_key /www/server/panel/ssl/privateKey.pem; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + error_page 497 https://$host$request_uri; + #SSL-END + %s + include enable-php.conf; + location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + } + location ~ .*\.(js|css)?$ + { + expires 12h; + } + location ~ /\. + { + deny all; + } + access_log /www/wwwlogs/access.log; + }""" % auth + public.writeFile("/www/server/panel/vhost/nginx/phpmyadmin.conf",ssl_conf) + import panelPlugin + get.sName = "phpmyadmin" + v = panelPlugin.panelPlugin().get_soft_find(get) + if self._get_phpmyadmin_auth(): + auth = """ + #AUTH_START + AuthType basic + AuthName "Authorization " + AuthUserFile /www/server/pass/phpmyadmin.pass + Require user jose + #AUTH_END + """ + # apache配置 + ssl_conf = r'''Listen 887 + + ServerAdmin webmaster@example.com + DocumentRoot "/www/server/phpmyadmin" + ServerName 0b842aa5.phpmyadmin + ServerAlias phpmyadmin.com + #ErrorLog "/www/wwwlogs/BT_default_error.log" + #CustomLog "/www/wwwlogs/BT_default_access.log" combined + + #SSL + SSLEngine On + SSLCertificateFile /www/server/panel/ssl/certificate.pem + SSLCertificateKeyFile /www/server/panel/ssl/privateKey.pem + SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH + SSLProtocol All -SSLv2 -SSLv3 + SSLHonorCipherOrder On + + #PHP + + SetHandler "proxy:{}" + + + #DENY FILES + + Order allow,deny + Deny from all + + + #PATH + +{} + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + Require all granted + DirectoryIndex index.php index.html index.htm default.php default.html default.htm + +'''.format(public.get_php_proxy(v["ext"]["phpversion"],'apache'),auth) + public.writeFile("/www/server/panel/vhost/apache/phpmyadmin.conf", ssl_conf) + + import firewalls + fw = firewalls.firewalls() + fw.AddAcceptPort(public.to_dict_obj({ + 'port': '887', + 'ps': public.get_msg_gettext('New phpMyAdmin SSL Port'), + })) + else: + if os.path.exists("/www/server/panel/vhost/nginx/phpmyadmin.conf"): + os.remove("/www/server/panel/vhost/nginx/phpmyadmin.conf") + if os.path.exists("/www/server/panel/vhost/apache/phpmyadmin.conf"): + os.remove("/www/server/panel/vhost/apache/phpmyadmin.conf") + public.serviceReload() + return public.return_message(0, 0, public.gettext_msg('Setup successfully!')) + public.serviceReload() + return public.return_message(0, 0, public.gettext_msg('Open successfully, please manually release phpmyadmin ssl port')) + + + #设置PHPMyAdmin + def setPHPMyAdmin(self,get): + import re + #try: + filename = self.__get_webserver_conffile() + if public.get_webserver() == 'openlitespeed': + filename = "/www/server/panel/vhost/openlitespeed/detail/phpmyadmin.conf" + conf = public.readFile(filename) + + if not conf: + return public.fail_v2('Operation failed') + + if hasattr(get,'port'): + mainPort = public.readFile('data/port.pl').strip() + rulePort = ['80','443','21','20','8080','8081','8089','11211','6379'] + oldPort = "888" + if get.port in rulePort: + return public.fail_v2('Please do NOT use the usual port as the phpMyAdmin port!') + + if public.get_webserver() == 'nginx': + rep = r"listen\s+([0-9]+)\s*;" + oldPort = re.search(rep,conf).groups()[0] + conf = re.sub(rep,'listen ' + get.port + ';\n',conf) + elif public.get_webserver() == 'apache': + rep = r"Listen\s+([0-9]+)\s*\n" + oldPort = re.search(rep,conf).groups()[0] + conf = re.sub(rep,"Listen " + get.port + "\n",conf,1) + rep = r"VirtualHost\s+\*:[0-9]+" + conf = re.sub(rep,"VirtualHost *:" + get.port,conf,1) + else: + filename = '/www/server/panel/vhost/openlitespeed/listen/888.conf' + conf = public.readFile(filename) + reg = r"address\s+\*:(\d+)" + tmp = re.search(reg,conf) + if tmp: + oldPort = tmp.groups(1) + + ## 修复 openlitespeed 修改端口报错 + oldPort = oldPort[0] + + conf = re.sub(reg,"address *:{}".format(get.port),conf) + + if oldPort == get.port: + return public.fail_v2('Port [{}] is in use!', (get.port,)) + + public.writeFile(filename,conf) + import firewalls + get.ps = public.get_msg_gettext('New phpMyAdmin Port') + fw = firewalls.firewalls() + fw.AddAcceptPort(get) + public.serviceReload() + public.write_log_gettext('Software manager','Modified access port to {} for phpMyAdmin!',(get.port,)) + get.id = public.M('firewall').where('port=?',(oldPort,)).getField('id') + get.port = oldPort + fw.DelAcceptPort(get) + return public.success_v2('Setup successfully!') + + if hasattr(get,'phpversion'): + if public.get_webserver() == 'nginx': + filename = public.GetConfigValue('setup_path') + '/nginx/conf/enable-php.conf' + conf = public.readFile(filename) + rep = r"(unix:/tmp/php-cgi.*\.sock|127.0.0.1:\d+)" + conf = re.sub(rep,public.get_php_proxy(get.phpversion,'nginx'),conf,1) + elif public.get_webserver() == 'apache': + rep = r"(unix:/tmp/php-cgi.*\.sock\|fcgi://localhost|fcgi://127.0.0.1:\d+)" + conf = re.sub(rep,public.get_php_proxy(get.phpversion,'apache'),conf,1) + else: + reg = r'/usr/local/lsws/lsphp\d+/bin/lsphp' + conf = re.sub(reg,'/usr/local/lsws/lsphp{}/bin/lsphp'.format(get.phpversion),conf) + public.writeFile(filename,conf) + public.serviceReload() + public.write_log_gettext('Software manager','Modified PHP runtime version to PHP-{} for phpMyAdmin!',(get.phpversion,)) + + return public.success_v2('Setup successfully!') + + if hasattr(get,'password'): + import panel_site_v2 + if(get.password == 'close'): + return panel_site_v2.panelSite().CloseHasPwd(get) + else: + return panel_site_v2.panelSite().SetHasPwd(get) + + if hasattr(get,'status'): + pma_path = public.GetConfigValue('setup_path') + '/phpmyadmin' + stop_path = public.GetConfigValue('setup_path') + '/stop' + + + webserver = public.get_webserver() + if conf.find(stop_path) != -1: + conf = conf.replace(stop_path,pma_path) + msg = public.getMsg('START') + + if webserver == 'nginx': + sub_string = '''{}; + allow 127.0.0.1; + allow ::1; + deny all'''.format(pma_path) + if conf.find(sub_string) != -1: + conf = conf.replace(sub_string,pma_path) + msg = public.getMsg('START') + else: + conf = conf.replace(pma_path,sub_string) + msg = public.getMsg('STOP') + elif webserver == 'apache': + src_string = 'AllowOverride All' + sub_string = '''{} + Deny from all + Allow from 127.0.0.1 ::1 localhost'''.format(src_string,pma_path) + if conf.find(sub_string) != -1: + conf = conf.replace(sub_string,src_string) + msg = public.getMsg('START') + else: + conf = conf.replace(src_string,sub_string) + msg = public.getMsg('STOP') + else: + if conf.find(stop_path) != -1: + conf = conf.replace(stop_path,pma_path) + msg = public.getMsg('START') + else: + conf = conf.replace(pma_path,stop_path) + msg = public.getMsg('STOP') + + public.writeFile(filename,conf) + public.serviceReload() + public.write_log_gettext('Software manager','phpMyAdmin already {}!',(msg,)) + + return public.success_v2('phpMyAdmin already {}!', (msg,)) + #except: + #return public.returnMsg(False,'ERROR'); + + def ToPunycode(self,get): + import re + get.domain = get.domain.encode('utf8') + tmp = get.domain.split('.') + newdomain = '' + for dkey in tmp: + #匹配非ascii字符 + match = re.search(u"[\x80-\xff]+",dkey) + if not match: + newdomain += dkey + '.' + else: + newdomain += 'xn--' + dkey.decode('utf-8').encode('punycode') + '.' + + return newdomain[0:-1] + + #保存PHP排序 + def phpSort(self,get): + if public.writeFile('/www/server/php/sort.pl',get.ssort): return public.return_msg_gettext(True,'Setup successfully!') + return public.return_msg_gettext(False,'Operation failed') + + #获取广告代码 + def GetAd(self,get): + try: + return public.HttpGet(public.GetConfigValue('home') + '/Api/GetAD?name='+get.name + '&soc=' + get.soc) + except: + return '' + + #获取进度 + def GetSpeed(self,get): + return public.getSpeed() + + #检查登陆状态 + def CheckLogin(self,get): + return True + + #获取警告标识 + def GetWarning(self,get): + warningFile = 'data/warning.json' + if not os.path.exists(warningFile): return public.return_msg_gettext(False,'Warning list does NOT exist!') + import json,time; + wlist = json.loads(public.readFile(warningFile)) + wlist['time'] = int(time.time()) + return wlist + + #设置警告标识 + def SetWarning(self,get): + wlist = self.GetWarning(get) + id = int(get.id) + import time,json; + for i in xrange(len(wlist['data'])): + if wlist['data'][i]['id'] == id: + wlist['data'][i]['ignore_count'] += 1 + wlist['data'][i]['ignore_time'] = int(time.time()) + + warningFile = 'data/warning.json' + public.writeFile(warningFile,json.dumps(wlist)) + return public.return_msg_gettext(True,'Setup successfully!') + + #获取memcached状态 + def GetMemcachedStatus(self,get): + import telnetlib,re; + conf = public.readFile('/etc/init.d/memcached') + result = {} + result['bind'] = re.search('IP=(.+)',conf).groups()[0] + result['port'] = int(re.search(r'PORT=(\d+)',conf).groups()[0]) + result['maxconn'] = int(re.search(r'MAXCONN=(\d+)',conf).groups()[0]) + result['cachesize'] = int(re.search(r'CACHESIZE=(\d+)',conf).groups()[0]) + tn = telnetlib.Telnet(result['bind'],result['port']) + tn.write(b"stats\n") + tn.write(b"quit\n") + data = tn.read_all() + if type(data) == bytes: data = data.decode('utf-8') + data = data.replace('STAT','').replace('END','').split("\n") + res = ['cmd_get','get_hits','get_misses','limit_maxbytes','curr_items','bytes','evictions','limit_maxbytes','bytes_written','bytes_read','curr_connections']; + for d in data: + if len(d)<3: continue + t = d.split() + if not t[0] in res: continue + result[t[0]] = int(t[1]) + result['hit'] = 1 + if result['get_hits'] > 0 and result['cmd_get'] > 0: + result['hit'] = float(result['get_hits']) / float(result['cmd_get']) * 100 + + return result + + #设置memcached缓存大小 + def SetMemcachedCache(self,get): + import re + confFile = '/etc/init.d/memcached' + conf = public.readFile(confFile) + conf = re.sub('IP=.+','IP='+get.ip,conf) + conf = re.sub(r'PORT=\d+','PORT='+get.port,conf) + conf = re.sub(r'MAXCONN=\d+','MAXCONN='+get.maxconn,conf) + conf = re.sub(r'CACHESIZE=\d+','CACHESIZE='+get.cachesize,conf) + public.writeFile(confFile,conf) + public.ExecShell(confFile + ' reload') + return public.return_msg_gettext(True,'Setup successfully!') + + #取redis状态 + def GetRedisStatus(self,get): + import re + c = public.readFile('/www/server/redis/redis.conf') + port = re.findall('\n\\s*port\\s+(\\d+)',c)[0] + password = re.findall('\n\\s*requirepass\\s+(.+)',c) + if password: + password = ' -a ' + password[0] + else: + password = '' + data = public.ExecShell('/www/server/redis/src/redis-cli -p ' + port + password + ' info')[0]; + res = [ + 'tcp_port', + 'uptime_in_days', #已运行天数 + 'connected_clients', #连接的客户端数量 + 'used_memory', #Redis已分配的内存总量 + 'used_memory_rss', #Redis占用的系统内存总量 + 'used_memory_peak', #Redis所用内存的高峰值 + 'mem_fragmentation_ratio', #内存碎片比率 + 'total_connections_received',#运行以来连接过的客户端的总数量 + 'total_commands_processed', #运行以来执行过的命令的总数量 + 'instantaneous_ops_per_sec', #服务器每秒钟执行的命令数量 + 'keyspace_hits', #查找数据库键成功的次数 + 'keyspace_misses', #查找数据库键失败的次数 + 'latest_fork_usec' #最近一次 fork() 操作耗费的毫秒数 + ] + data = data.split("\n") + result = {} + for d in data: + if len(d)<3: continue + t = d.strip().split(':') + if not t[0] in res: continue + result[t[0]] = t[1] + return result + + #取PHP-FPM日志 + def GetFpmLogs(self,get): + import re + fpm_path = '/www/server/php/' + get.version + '/etc/php-fpm.conf' + if not os.path.exists(fpm_path): return public.return_msg_gettext(False,'Log file does NOT exist!') + fpm_conf = public.readFile(fpm_path) + log_tmp = re.findall(r"error_log\s*=\s*(.+)",fpm_conf) + if not log_tmp: return public.return_msg_gettext(False,'Log file does NOT exist!') + log_file = log_tmp[0].strip() + if log_file.find('var/log') == 0: + log_file = '/www/server/php/' +get.version + '/'+ log_file + return public.returnMsg(True,public.GetNumLines(log_file,1000)) + + #取PHP慢日志 + def GetFpmSlowLogs(self,get): + import re + fpm_path = '/www/server/php/' + get.version + '/etc/php-fpm.conf' + if not os.path.exists(fpm_path): return public.return_msg_gettext(False,'Log file does NOT exist!') + fpm_conf = public.readFile(fpm_path) + log_tmp = re.findall(r"slowlog\s*=\s*(.+)",fpm_conf) + if not log_tmp: return public.return_msg_gettext(False,'Log file does NOT exist!') + log_file = log_tmp[0].strip() + if log_file.find('var/log') == 0: + log_file = '/www/server/php/' +get.version + '/'+ log_file + return public.returnMsg(True,public.GetNumLines(log_file,1000)) + + #取指定日志 + def GetOpeLogs(self,get): + if not os.path.exists(get.path): return public.return_msg_gettext(False,'Log file does NOT exist!') + return public.returnMsg(True,public.xsssec(public.GetNumLines(get.path,1000))) + + def get_pd(self,get): + # 校验参数 + try: + get.validate([ + Param('status').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + from BTPanel import cache + tmp = -1 + try: + import panelPlugin + # get = public.dict_obj() + # get.init = 1 + tmp1 = panelPlugin.panelPlugin().get_cloud_list(get) + except: + tmp1 = None + if tmp1: + tmp = tmp1[public.to_string([112, 114, 111])] + ltd = tmp1.get('ltd', -1) + else: + ltd = -1 + tmp4 = cache.get(public.to_string([112, 95, 116, 111, 107, 101, 110])) + if tmp4: + tmp_f = public.to_string([47, 116, 109, 112, 47]) + tmp4 + if not os.path.exists(tmp_f): public.writeFile(tmp_f, '-1') + tmp = public.readFile(tmp_f) + if tmp: tmp = int(tmp) + if not ltd: ltd = -1 + if tmp == None: tmp = -1 + if ltd < 1: + if ltd == -2: + tmp3 = public.to_string( + [60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 116, 100, + 45, 103, 114, 97, 121, 34, 62, 60, 115, 112, 97, 110, 32, 115, 116, 121, 108, 101, + 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, + 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, + 114, 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, 36807, + 26399, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, + 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, 34, 98, 116, 46, 115, 111, + 102, 116, 46, 117, 112, 100, 97, 116, 97, 95, 108, 116, 100, 40, 41, 34, 62, 82, 69, 78, 69, 87, + 60, 47, 97, + 62, 60, 47, 115, 112, 97, 110, 62]) + elif tmp == -1: + tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, + 116, 112, 114, 111, 45, 102, 114, 101, 101, 34, 32, 111, 110, 99, 108, 105, 99, + 107, + 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, + 114, + 111, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, 107, + 32, 116, 111, 32, + 103, 101, 116, 32, 80, 82, 79, 34, 62, 20813, 36153, 29256, 60, 47, 115, 112, + 97, 110, 62]) + elif tmp == -2: + tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, + 112, 114, 111, 45, 103, 114, 97, 121, 34, 62, 60, 115, 112, 97, 110, 32, + 115, 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, + 102, 99, 54, 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, + 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, + 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 24050, 36807, 26399, + 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, 115, 61, 34, + 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, 99, 107, 61, + 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, 112, 114, + 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, 112, 97, + 110, 62]) + if tmp >= 0 and ltd in [-1, -2]: + if tmp == 0: + tmp2 = public.to_string([27704, 20037, 25480, 26435]) + tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, + 112, 114, 111, 34, 62, 123, 48, 125, 60, 115, 112, 97, 110, 32, 115, 116, + 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, + 100, + 50, 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, + 58, 32, 98, 111, 108, 100, 59, 34, 62, 123, 49, 125, 60, 47, 115, + 112, 97, 110, 62, 60, 47, 115, 112, 97, 110, 62]).format( + public.to_string([21040, 26399, 26102, 38388, 65306]), tmp2) + else: + tmp2 = time.strftime(public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), time.localtime(tmp)) + tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, + 112, 114, 111, 34, 62, 69, 120, 112, 105, 114, 101, 58, 32, 60, 115, 112, + 97, 110, 32, 115, 116, 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, + 58, 32, 35, 102, 99, 54, 100, 50, 54, 59, 102, 111, 110, 116, 45, 119, + 101, 105, 103, 104, 116, 58, 32, 98, 111, 108, 100, 59, 109, 97, 114, + 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, 112, 120, 34, 62, 123, + 48, 125, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, 97, 115, + 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, + 99, + 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, + 95, + 112, 114, 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, + 47, 115, 112, 97, 110, 62]).format(tmp2) + else: + tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 112, + 114, 111, 45, 103, 114, 97, 121, 34, 32, 111, 110, 99, 108, 105, 99, 107, + 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 117, 112, 100, 97, 116, 97, 95, 112, + 114, 111, 40, 41, 34, 32, 116, 105, 116, 108, 101, 61, 34, 67, 108, 105, 99, + 107, 32, 116, + 111, 32, 103, 101, 116, 32, 80, 82, 79, 34, 62, 70, 82, + 69, 69, 60, 47, 115, 112, 97, 110, 62]) + else: + tmp3 = public.to_string([60, 115, 112, 97, 110, 32, 99, 108, 97, 115, 115, 61, 34, 98, 116, 108, 116, + 100, 34, 62, 69, 120, 112, 105, 114, 101, 58, 32, 60, 115, 112, 97, 110, 32, 115, + 116, + 121, 108, 101, 61, 34, 99, 111, 108, 111, 114, 58, 32, 35, 102, 99, 54, 100, 50, + 54, 59, 102, 111, 110, 116, 45, 119, 101, 105, 103, 104, 116, 58, 32, 98, 111, + 108, 100, 59, 109, 97, 114, 103, 105, 110, 45, 114, 105, 103, 104, 116, 58, 53, + 112, 120, 34, 62, 123, 125, 60, 47, 115, 112, 97, 110, 62, 60, 97, 32, 99, 108, + 97, 115, 115, 61, 34, 98, 116, 108, 105, 110, 107, 34, 32, 111, 110, 99, 108, 105, + 99, 107, 61, 34, 98, 116, 46, 115, 111, 102, 116, 46, 114, 101, 110, 101, 119, 95, + 112, 114, 111, 40, 41, 34, 62, 82, 69, 78, 69, 87, 60, 47, 97, 62, 60, 47, 115, + 112, 97, 110, 62]).format( + time.strftime(public.to_string([37, 89, 45, 37, 109, 45, 37, 100]), time.localtime(ltd))) + return_message={"bt_pro":tmp3,"time_stamp":tmp,"itd":ltd} + + return public.return_message(0,0,return_message) + + #检查用户绑定是否正确 + def check_user_auth(self,get): + # import requests + m_key = 'check_user_auth' + if m_key in session: return session[m_key] + u_path = 'data/userInfo.json' + try: + userInfo = json.loads(public.ReadFile(u_path)) + except: + if os.path.exists(u_path): os.remove(u_path) + return public.return_msg_gettext(False,'Account binding has expired, please re-bind on the [Settings] page!') + url_headers = {"authorization":"bt {}".format(userInfo['token'])} + # resp = requests.post('{}/api/user/verifyToken'.format(self.__official_url),headers=url_headers,verify=False) + resp = public.HttpPost.post('{}/api/user/verifyToken'.format(self.__official_url), headers=url_headers, verify=False) + resp = resp.json() + if not resp['success']: + if os.path.exists(u_path): os.remove(u_path) + return public.return_msg_gettext(False,'Account binding has expired, please re-bind on the [Settings] page!') + else: + session[m_key] = public.return_msg_gettext(True,'Binding is valid!') + return session[m_key] + + + #PHP探针 + def php_info(self,args): + php_version = args.php_version.replace('.','') + php_path = '/www/server/php/' + if public.get_webserver() == 'openlitespeed': + php_path = '/usr/local/lsws/lsphp' + php_bin = php_path + php_version + '/bin/php' + php_ini = php_path + php_version + '/etc/php.ini' + if not os.path.exists('/etc/redhat-release') and public.get_webserver() == 'openlitespeed': + php_ini = php_path + php_version + '/etc/php/'+args.php_version+'/litespeed/php.ini' + tmp = public.ExecShell(php_bin + ' -c {} /www/server/panel/class/php_info.php'.format(php_ini))[0] + if tmp.find('Warning: JIT is incompatible') != -1: + tmp = tmp.strip().split('\n')[-1] + result = json.loads(tmp) + result['phpinfo'] = {} + result['phpinfo']['php_version'] = result['php_version'] + result['phpinfo']['php_path'] = php_path + result['phpinfo']['php_bin'] = php_bin + result['phpinfo']['php_ini'] = php_ini + result['phpinfo']['modules'] = ' '.join(result['modules']) + result['phpinfo']['ini'] = result['ini'] + result['phpinfo']['keys'] = { "1cache": "Buffer", "2crypt": "Encryption and decryption library", "0db": "Database-driven", "4network": "Network Communication Library", "5io_string": "File and string processing libraries", "3photo":"Image processing library","6other":"Other third-party libraries"} + del(result['php_version']) + del(result['modules']) + del(result['ini']) + return result + + #取指定行 + def get_lines(self,args): + # 校验参数 + try: + args.validate([ + Param('filename').String(), + Param('num').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + if not os.path.exists(args.filename): return public.return_message(-1,0,'Logs emptied') + num = args.get('num/d',10) + s_body = public.GetNumLines(args.filename,num) + return public.return_message(0,0,s_body) + + def log_analysis(self,get): + public.set_module_logs('log_analysis', 'log_analysis', 1) + import log_analysis_v2 as log_analysis + log_analysis=log_analysis.log_analysis() + return log_analysis.log_analysis(get) + + + def speed_log(self,get): + import log_analysis_v2 as log_analysis + log_analysis=log_analysis.log_analysis() + return log_analysis.speed_log(get) + + + + def get_result(self,get): + # 校验参数 + try: + get.validate([ + Param('path').String(), + Param('action').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + import log_analysis_v2 as log_analysis + log_analysis=log_analysis.log_analysis() + return log_analysis.get_result(get) + + def get_detailed(self,get): + import log_analysis_v2 as log_analysis + log_analysis=log_analysis.log_analysis() + return log_analysis.get_detailed(get) + + def download_pay_type(self, path): + public.downloadFile(public.get_url() + '/install/lib/pay_type_en.json', path) + return True + + def get_pay_type(self, get): + """ + @name 获取推荐列表 + """ + #未传参,无需统一参数校验 + + + # spath = '{}/data/pay_type.json'.format(public.get_panel_path()) + # if not os.path.exists(spath): + # public.run_thread(self.download_pay_type,(spath,)) + # try: + # data = json.loads(public.readFile("data/pay_type.json")) + # except: + # public.run_thread(self.download_pay_type, (spath,)) + # data = {} + # + # import panelPlugin + # plu_panel = panelPlugin.panelPlugin() + # plugin_list = plu_panel.get_cloud_list() + # if not 'pro' in plugin_list: plugin_list['pro'] = -1 + # + # for item in data: + # if 'list' in item: + # item['list'] = self.__get_home_list(item['list'], item['type'], plugin_list, plu_panel) + # if item['type'] == 1: + # if len(item['list']) > 4: item['list'] = item['list'][:4] + # # if item['type'] == 0 and plugin_list['pro'] >= 0: + # # item['show'] = False + # + # return data + + spath = '{}/data/pay_type.json'.format(public.get_panel_path()) + if os.path.exists(spath) and os.path.getsize(spath) <= 0: + os.remove(spath) + + if not os.path.exists(spath): + public.run_thread(self.download_pay_type, (spath,)) + try: + data = json.loads(public.readFile("data/pay_type.json")) + except json.decoder.JSONDecodeError: + os.remove(spath) + public.run_thread(self.download_pay_type, (spath,)) + data = json.loads(public.readFile("data/pay_type.json")) + except Exception: + data = self.get_default_pay_type() + + import panelPlugin + plu_panel = panelPlugin.panelPlugin() + plugin_list = plu_panel.get_cloud_list() + if not 'pro' in plugin_list: plugin_list['pro'] = -1 + + for item in data: + if 'list' in item: + item['list'] = self.__get_home_list(item['list'], item['type'],plugin_list, plu_panel) + if item['type'] == 1: + if len(item['list']) > 4: item['list'] = item['list'][:4] + # if item['type'] == 0 and plugin_list['pro'] >= 0: + # item['show'] = False + return public.return_message(0,0,data) + + + @staticmethod + def get_default_pay_type(): + spath = '{}/data/default_pay_type.json'.format(public.get_panel_path()) + default = [{"type": -1}, {"type": -1}, {"type": -1}, {"type": -1}, + {"type": -1}, { + "type": 5, + "describe": "网站-设置推荐", + "show": True, + "list": [ + { + "title": "防火墙", + "name": "btwaf", + "pay": "46", + "pluginName": "Nginx网站防火墙", + "ps": "有效拦截SQL 注入、XSS跨站、恶意代码、网站挂马等常见攻击,过滤恶意访问,降低数据泄露的风险,保障网站的可用性。", + "preview": "https://www.bt.cn/new/product_nginx_firewall.html", + "dependent": "nginx", + "pluginType": "pro", + "eventList": [ + { + "event": "site_waf_config('$siteName')", + "version": "5.2.0" + } + ] + }, + { + "title": "防火墙", + "name": "btwaf_httpd", + "pay": "46", + "pluginName": "网站防火墙", + "ps": "有效拦截SQL 注入、XSS跨站、恶意代码、网站挂马等常见攻击,过滤恶意访问,降低数据泄露的风险,保障网站的可用性。", + "preview": "https://www.bt.cn/new/product_nginx_firewall.html", + "dependent": "apache", + "pluginType": "pro", + "eventList": [ + { + "event": "site_waf_config('$siteName')", + "version": "5.2.0" + } + ] + }, + { + "title": "统计", + "name": "total", + "pay": "47", + "pluginName": "网站监控报表", + "ps": "快速分析网站运行状况,实时精确统计网站流量、ip、uv、pv、请求、蜘蛛等数据,网站SEO优化利器", + "preview": "https://www.bt.cn/new/product_website_total.html", + "dependent": "apache", + "pluginType": "pro", + "eventList": [ + { + "event": "WebsiteReport('$siteName')", + "version": "5.0" + } + ] + }, + { + "title": "统计", + "name": "total", + "pay": "47", + "pluginName": "网站监控报表", + "ps": "快速分析网站运行状况,实时精确统计网站流量、ip、uv、pv、请求、蜘蛛等数据,网站SEO优化利器", + "preview": "https://www.bt.cn/new/product_website_total.html", + "dependent": "nginx", + "pluginType": "pro", + "eventList": [ + { + "event": "WebsiteReport('$siteName')", + "version": "5.0" + } + ] + } + ] + }, {"type": -1}, {"type": -1}] + if os.path.isfile(spath): + try: + res_data = json.loads(public.readFile(spath)) + if isinstance(res_data, list): + return res_data + except json.JSONDecodeError: + pass + # 再次出错时,保障网站列表可以展示 + return default + return default + + + + + def __get_home_list(self, sList, stype, plugin_list, plu_panel): + """ + @name 获取首页软件列表推荐 + """ + nList = [] + webserver = public.get_webserver() + for x in sList: + for plugin_info in plugin_list['list']: + if x['name'] == plugin_info['name']: + if not 'endtime' in plugin_info or plugin_info['endtime'] >= 0: + x['isBuy'] = True + is_check = False + if 'dependent' in x: + if x['dependent'] == webserver: is_check = True + else: + is_check = True + if is_check: + info = plu_panel.get_soft_find(x['name']) + if info: + if stype == 1: + # if plugin_list['pro'] >= 0: continue + if not info['setup']: + x['install'] = info['setup'] + nList.append(x) + else: + x['install'] = info['setup'] + nList.append(x) + return nList + + def ignore_version(self, get): + """ + @忽略版本更新 + :param version 忽略的版本号 + """ + # 校验参数 + try: + get.validate([ + Param('version').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + version = get.version + path = '{}/data/no_update.pl'.format(public.get_panel_path()) + try: + data = json.loads(public.readFile(path)) + except: + data = [] + + if not version in data: data.append(version) + + public.writeFile(path, json.dumps(data)) + try: + del (session['updateInfo']) + except: + pass + + return public.return_message(0,0, "Ignore success, this version will no longer be reminded to update.") diff --git a/class_v2/apache_v2.py b/class_v2/apache_v2.py new file mode 100644 index 00000000..4f0b536c --- /dev/null +++ b/class_v2/apache_v2.py @@ -0,0 +1,381 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2018 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# Apache管理模块 +#------------------------------ +import public,os,re,shutil,math,psutil,time +from json import loads +os.chdir("/www/server/panel") + +class apache: + setupPath = '/www/server' + apachedefaultfile = "%s/apache/conf/extra/httpd-default.conf" % (setupPath) + apachempmfile = "%s/apache/conf/extra/httpd-mpm.conf" % (setupPath) + httpdconf = "%s/apache/conf/httpd.conf" % (setupPath) + + + def GetProcessCpuPercent(self,i,process_cpu): + try: + pp = psutil.Process(i) + if pp.name() not in process_cpu.keys(): + process_cpu[pp.name()] = float(pp.cpu_percent(interval=0.1)) + process_cpu[pp.name()] += float(pp.cpu_percent(interval=0.1)) + except: + pass + + def GetApacheStatus(self): + process_cpu = {} + apacheconf = "%s/apache/conf/httpd.conf" % (self.setupPath) + confcontent = public.readFile(apacheconf) + rep = "#Include conf/extra/httpd-info.conf" + if re.search(rep,confcontent): + confcontent = re.sub(rep,"Include conf/extra/httpd-info.conf",confcontent) + public.writeFile(apacheconf,confcontent) + public.serviceReload() + result = public.HttpGet('http://127.0.0.1/server-status?auto') + try: + workermen = int(public.ExecShell("ps aux|grep httpd|grep 'start'|awk '{memsum+=$6};END {print memsum}'")[0]) / 1024 + except: + return public.return_msg_gettext(False,"Get worker RAM False") + for proc in psutil.process_iter(): + if proc.name() == "httpd": + self.GetProcessCpuPercent(proc.pid,process_cpu) + time.sleep(0.5) + + data = {} + + # 计算启动时间 + Uptime = re.search(r"ServerUptimeSeconds:\s+(.*)",result) + if not Uptime: + return public.return_msg_gettext(False, "Get worker Uptime False") + Uptime = int(Uptime.group(1)) + min = Uptime / 60 + hours = min / 60 + days = math.floor(hours / 24) + hours = math.floor(hours - (days * 24)) + min = math.floor(min - (days * 60 * 24) - (hours * 60)) + + #格式化重启时间 + restarttime = re.search(r"RestartTime:\s+(.*)",result) + if not restarttime: + return public.return_msg_gettext(False, "Get worker Restart Time False") + restarttime = restarttime.group(1) + rep = r"\w+,\s([\w-]+)\s([\d\:]+)\s\w+" + date = re.search(rep,restarttime) + if not date: + return public.return_msg_gettext(False, "Get worker date False") + date = date.group(1) + timedetail = re.search(rep,restarttime) + if not timedetail: + return public.return_msg_gettext(False, "Get worker time detail False") + timedetail=timedetail.group(2) + monthen = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] + n = 0 + for m in monthen: + if m in date: + date = re.sub(m,str(n+1),date) + n+=1 + date = date.split("-") + date = "%s-%s-%s" % (date[2],date[1],date[0]) + + reqpersec = re.search(r"ReqPerSec:\s+(.*)", result) + if not reqpersec: + return public.return_msg_gettext(False, "Get worker reqpersec False") + reqpersec = reqpersec.group(1) + if re.match(r"^\.", reqpersec): + reqpersec = "%s%s" % (0,reqpersec) + data["RestartTime"] = "%s %s" % (date,timedetail) + data["UpTime"] = "%s day %s hour %s minute" % (str(int(days)),str(int(hours)),str(int(min))) + total_acc = re.search(r"Total Accesses:\s+(\d+)",result) + if not total_acc: + return public.return_msg_gettext(False, "Get worker TotalAccesses False") + data["TotalAccesses"] = total_acc.group(1) + total_kb = re.search(r"Total kBytes:\s+(\d+)",result) + if not total_kb: + return public.return_msg_gettext(False, "Get worker TotalKBytes False") + data["TotalKBytes"] = total_kb.group(1) + data["ReqPerSec"] = round(float(reqpersec), 2) + busywork = re.search(r"BusyWorkers:\s+(\d+)",result) + if not busywork: + return public.return_msg_gettext(False, "Get worker BusyWorkers False") + data["BusyWorkers"] = busywork.group(1) + idlework = re.search(r"IdleWorkers:\s+(\d+)",result) + if not idlework: + return public.return_msg_gettext(False, "Get worker IdleWorkers False") + data["IdleWorkers"] = idlework.group(1) + data["workercpu"] = round(float(process_cpu["httpd"]),2) + data["workermem"] = "%s%s" % (int(workermen),"MB") + return data + + def GetApacheValue(self): + apachedefaultcontent = public.readFile(self.apachedefaultfile) + apachempmcontent = public.readFile(self.apachempmfile) + if not "mpm_event_module" in apachempmcontent: + return public.returnMsg(False,"mpm_event_module conf not found or /www/server/apache/conf/extra/httpd-mpm.conf is empty") + apachempmcontent = re.search("\\(\n|.)+?\\",apachempmcontent).group() + ps = ["%s,%s" % (public.get_msg_gettext('Second'),public.get_msg_gettext('Request timeout')), + public.get_msg_gettext('Keep alive'), + "%s,%s" % (public.get_msg_gettext('Second'),public.get_msg_gettext('Connection timeout')), + public.get_msg_gettext('Max keep-alive requests per connection')] + gets = ["Timeout","KeepAlive","KeepAliveTimeout","MaxKeepAliveRequests"] + if public.get_webserver() == 'apache': + shutil.copyfile(self.apachedefaultfile, '/tmp/apdefault_file_bk.conf') + shutil.copyfile(self.apachempmfile, '/tmp/apmpm_file_bk.conf') + conflist = [] + n = 0 + for i in gets: + rep = r"(%s)\s+(\w+)" % i + k = re.search(rep, apachedefaultcontent) + if not k: + return public.return_msg_gettext(False, "Get Key {} False",(i,)) + k = k.group(1) + v = re.search(rep, apachedefaultcontent) + if not v: + return public.return_msg_gettext(False, "Get Value {} False",(v,)) + v = v.group(2) + psstr = ps[n] + kv = {"name":k,"value":v,"ps":psstr} + conflist.append(kv) + n += 1 + + ps = [public.get_msg_gettext('Default processes'), + public.get_msg_gettext('Maximum number of idle threads'), + public.get_msg_gettext('Minimum number of idle threads available to handle request spikes'), + public.get_msg_gettext('Number of threads created by each child process'), + public.get_msg_gettext('Maximum number of connections that will be processed simultaneously'), + public.get_msg_gettext('Limit on the number of connections that an individual child server will handle during its life')] + gets = ["StartServers","MaxSpareThreads","MinSpareThreads","ThreadsPerChild","MaxRequestWorkers","MaxConnectionsPerChild"] + n = 0 + for i in gets: + rep = r"(%s)\s+(\w+)" % i + k = re.search(rep, apachempmcontent) + if not k: + return public.return_msg_gettext(False, "Get Key {} False",(i,)) + k = k.group(1) + v = re.search(rep, apachempmcontent) + if not v: + return public.return_msg_gettext(False, "Get Value {} False",(v,)) + v = v.group(2) + psstr = ps[n] + kv = {"name": k, "value": v, "ps": psstr} + conflist.append(kv) + n += 1 + return(conflist) + + def SetApacheValue(self,get): + apachedefaultcontent = public.readFile(self.apachedefaultfile) + apachempmcontent = public.readFile(self.apachempmfile) + if not "mpm_event_module" in apachempmcontent: + return public.return_msg_gettext(False,"mpm_event_module conf not found or /www/server/apache/conf/extra/httpd-mpm.conf is empty") + conflist = [] + getdict = get.get_items() + for i in getdict.keys(): + if i != "__module__" and i != "__doc__" and i != "data" and i != "args" and i != "action": + getpost = { + "name": i, + "value": str(getdict[i]) + } + conflist.append(getpost) + for c in conflist: + if c["name"] == "KeepAlive": + if not re.search("on|off", c["value"]): + return public.return_msg_gettext(False, 'Parameter ERROR!') + else: + print(c["value"]) + if not re.search(r"\d+", c["value"]): + print(c["name"],c["value"]) + return public.return_msg_gettext(False, 'Parameter ERROR!') + + rep = r"%s\s+\w+" % c["name"] + if re.search(rep,apachedefaultcontent): + newconf = "%s %s" % (c["name"],c["value"]) + apachedefaultcontent = re.sub(rep,newconf,apachedefaultcontent) + elif re.search(rep,apachempmcontent): + newconf = "%s\t\t\t%s" % (c["name"], c["value"]) + apachempmcontent = re.sub(rep, newconf , apachempmcontent) + public.writeFile(self.apachedefaultfile,apachedefaultcontent) + public.writeFile(self.apachempmfile, apachempmcontent) + isError = public.checkWebConfig() + if (isError != True): + shutil.copyfile('/tmp/_file_bk.conf', self.apachedefaultfile) + shutil.copyfile('/tmp/proxyfile_bk.conf', self.apachempmfile) + return public.returnMsg(False, 'ERROR: %s
                                    ' % public.get_msg_gettext('Configuration ERROR') + isError.replace("\n", + '
                                    ') + '
                                    ') + public.serviceReload() + return public.return_msg_gettext(True, 'Setup successfully!') + + def add_httpd_access_log_format(self,args): + ''' + @name 添加httpd日志格式 + @author zhwen + @param log_format 需要设置的日志格式["$server_name","$remote_addr","-"....] + @param log_format_name + @param act 操作方式 add/edit + ''' + try: + log_format = loads(args.log_format) + data = """ + #LOG_FORMAT_BEGIN_{n} + LogFormat '{c}' {n} + #LOG_FORMAT_END_{n} +""".format(n=args.log_format_name,c=' '.join(log_format)) + data = data.replace('%{User-agent}i','"%{User-agent}i"') + data = data.replace('%{Referer}i', '"%{Referer}i"') + if args.act == 'edit': + self.del_httpd_access_log_format(args) + conf = public.readFile(self.httpdconf) + if not conf: + return public.return_msg_gettext(False,'Configuration file not exist') + reg = '' + conf = re.sub(reg,''+data,conf) + public.writeFile(self.httpdconf,conf) + public.serviceReload() + return public.return_msg_gettext(True, 'Setup successfully!') + except: + return public.returnMsg(False, str(public.get_error_info())) + + def del_httpd_access_log_format(self,args): + ''' + @name 删除日志格式 + @author zhwen + @param log_format_name + ''' + conf = public.readFile(self.httpdconf) + if not conf: + return public.return_msg_gettext(False, 'Configuration file not exist') + reg = '\\s*#LOG_FORMAT_BEGIN_{n}(\n|.)+#LOG_FORMAT_END_{n}\n?'.format(n=args.log_format_name) + conf = re.sub(reg,'',conf) + self._del_format_log_of_website(args.log_format_name) + public.writeFile(self.httpdconf,conf) + public.serviceReload() + return public.return_msg_gettext(True, 'Setup successfully!') + + def del_all_log_format(self,args): + all_format = self.get_httpd_access_log_format(args) + for i in all_format: + args.log_format_name = i + self.del_httpd_access_log_format(args) + + def _del_format_log_of_website(self,log_format_name): + site_format_log_status = self._get_format_log_to_website(log_format_name) + try: + for s in site_format_log_status.keys(): + if not site_format_log_status[s]: + continue + website_conf_file = '/www/server/panel/vhost/apache/{}.conf'.format(s) + format_exist_reg = r'CustomLog\s+"/www.*"\s+{}'.format(log_format_name) + conf = public.readFile(website_conf_file) + if not conf:continue + if not re.search(format_exist_reg,conf):continue + access_log = re.search(format_exist_reg,conf).group().split() + access_log = access_log[0] + ' ' +access_log[1] + 'combined' + conf = re.sub(format_exist_reg,access_log,conf) + public.writeFile(website_conf_file,conf) + return True + except: + return False + + def get_httpd_access_log_format_parameter(self,args=None): + data = { + "%h":"Client's IP address", + "%r":"Request agreement", + "%t":"Request time", + "%>s":"http status code", + "%b":"Send data size", + "%{Referer}i":"http referer", + "%{User-agent}i":"http user agent", + "%{X-Forwarded-For}i":"The real ip of the client", + "%l":"Remote login name", + "%u":"Remote user", + "-":"-" + } + if hasattr(args,'log_format_name'): + site_list = self._get_format_log_to_website(args.log_format_name) + return {'site_list':site_list,'format_log':data} + else: + return data + + def _process_log_format(self,tmp): + log_tips = self.get_httpd_access_log_format_parameter() + data = [] + for t in tmp: + t = t.replace('\"','') + t = t.replace("'", "") + if t not in log_tips: + continue + data.append({t:log_tips[t]}) + return data + + def get_httpd_access_log_format(self,args=None): + try: + reg = "#LOG_FORMAT_BEGIN.*" + conf = public.readFile(self.httpdconf) + if not conf: + return public.return_msg_gettext(False, 'Configuration file not exist') + data = re.findall(reg,conf) + format_name = [i.split('LOG_FORMAT_BEGIN_')[-1] for i in data] + format_log = {} + for i in format_name: + format_reg = "#LOG_FORMAT_BEGIN_{n}(\n|.)+LogFormat\\s+\'(.*)\'\\s+{n}".format(n=i) + tmp = re.search(format_reg,conf) + if not tmp: + continue + tmp = tmp.groups()[1].split() + format_log[i] = self._process_log_format(tmp) + return format_log + except: + return public.returnMsg(False,public.get_error_info()) + + def set_httpd_format_log_to_website(self,args): + ''' + @name 设置网站日志格式 + @author zhwen + @param sites aaa.com,bbb.com + @param log_format_name + ''' + # sites = args.sites.split(',') + sites = loads(args.sites) + try: + all_site = public.M('sites').field('name').select() + reg = r'CustomLog\s+"/www.*{}\s*'.format(args.log_format_name) + for site in all_site: + website_conf_file = '/www/server/panel/vhost/apache/{}.conf'.format(site['name']) + conf = public.readFile(website_conf_file) + if not conf: + return public.return_msg_gettext(False, 'Configuration file not exist') + format_exist_reg = r'(CustomLog\s+"/www.*\_log).*' + access_log = re.search(format_exist_reg, conf).groups()[0] + '" ' + args.log_format_name + if site['name'] not in sites and re.search(format_exist_reg,conf): + access_log = ' '.join(access_log.split()[:-1]) + conf = re.sub(reg, access_log, conf) + public.writeFile(website_conf_file,conf) + continue + conf = re.sub(format_exist_reg,access_log,conf) + public.writeFile(website_conf_file,conf) + public.serviceReload() + return public.return_msg_gettext(True, 'Setup successfully!') + except: + return public.returnMsg(False, str(public.get_error_info())) + + def _get_format_log_to_website(self,log_format_name): + tmp = public.M('sites').field('name').select() + reg = 'CustomLog.*{}'.format(log_format_name) + data = {} + for i in tmp: + website_conf_file = '/www/server/panel/vhost/apache/{}.conf'.format(i['name']) + conf = public.readFile(website_conf_file) + if not conf: + data[i['name']] = False + continue + if re.search(reg,conf): + data[i['name']] = True + else: + data[i['name']] = False + return data diff --git a/class_v2/backup_bak_v2.py b/class_v2/backup_bak_v2.py new file mode 100644 index 00000000..42e98757 --- /dev/null +++ b/class_v2/backup_bak_v2.py @@ -0,0 +1,617 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 1249648969@qq.com +# | 主控 备份 +# +--------------------------------------- +import sys, os +if sys.version_info[0] == 2: + reload(sys) + sys.setdefaultencoding('utf-8') +os.chdir('/www/server/panel') +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import time,hashlib,sys,os,json,requests,re,public,random,string,panelMysql,downloadFile +python_bin=public.get_python_bin() +class backup_bak: + _chek_site_file='/tmp/chekc_site.json' + _check_database = '/www/server/panel/data/check_database.json' + _check_site = '/www/server/panel/data/check_site_data.json' + _chekc_path='/www/server/panel/data/check_path_data.json' + _down_path='/www/server/panel/data/download_path_data.json' + _check_database_data=[] + _check_site_data=[] + _check_path_data=[] + _down_path_data=[] + #备份所有站点的进度 + _check_all_site = '/www/server/panel/data/check_site_data_all.json' + _check_site_all_data=[] + #备份所有数据库的进度 + _check_all_date = '/www/server/panel/data/check_date_data_all.json' + _check_date_all_data=[] + def __init__(self): + if not os.path.exists(self._check_all_site): + ret = [] + public.writeFile(self._check_all_site, json.dumps(ret)) + else: + ret = public.ReadFile(self._check_all_site) + self._check_site_all_data = json.loads(ret) + + if not os.path.exists(self._check_all_date): + ret = [] + public.writeFile(self._check_all_date, json.dumps(ret)) + else: + ret = public.ReadFile(self._check_all_date) + self._check_date_all_data = json.loads(ret) + if not os.path.exists('/www/backup/site_backup'): + public.ExecShell('mkdir /www/backup/site_backup -p') + if not os.path.exists('/www/backup/database_backup'): + public.ExecShell('mkdir /www/backup/database_backup') + if not os.path.exists(self._check_database): + ret = [] + public.writeFile(self._check_database, json.dumps(ret)) + else: + ret = public.ReadFile(self._check_database) + self._check_database_data = json.loads(ret) + if not os.path.exists(self._check_site): + ret = [] + public.writeFile(self._check_site, json.dumps(ret)) + else: + ret = public.ReadFile(self._check_site) + self._check_site_data = json.loads(ret) + if not os.path.exists(self._chekc_path): + ret = [] + public.writeFile(self._chekc_path, json.dumps(ret)) + else: + ret = public.ReadFile(self._chekc_path) + self._check_path_data = json.loads(ret) + + #下载所需要的 + if not os.path.exists(self._down_path): + ret = [] + public.writeFile(self._down_path, json.dumps(ret)) + else: + ret = public.ReadFile(self._down_path) + self._down_path_data = json.loads(ret) + + + #判断是否在_check_database_data 中 + def check_database_data(self,data,ret): + if len(data)==0:return False + for i in data: + if int(i['id']) == int(ret['id']): + return True + else: + return False + + def check_database_data2(self,data,ret): + if len(data)==0:return False + for i in data: + if i['id'] == ret['id']: + return True + else: + return False + + #写入_database_data到里面去 + def set_database_data(self,ret): + if len(self._check_database_data) == 0: + self._check_database_data.append(ret) + else: + if self.check_database_data(self._check_database_data,ret): + for i in self._check_database_data: + if int(i['id'])==int(ret['id']): + i['name'] = ret['name'] + i['path']=ret['path'] + i['status']=ret['status'] + else: + self._check_database_data.append(ret) + public.writeFile(self._check_database, json.dumps(self._check_database_data)) + return True + + #写入_site_data到里面去 + def set_site_data(self,ret): + if len(self._check_site_data) == 0: + self._check_site_data.append(ret) + else: + if self.check_database_data(self._check_site_data,ret): + for i in self._check_site_data: + if int(i['id']) == int(ret['id']): + i['name'] = ret['name'] + i['path']=ret['path'] + i['status']=ret['status'] + else: + self._check_site_data.append(ret) + public.writeFile(self._check_site, json.dumps(self._check_site_data)) + return True + + #写入_site_data到里面去 + def set_path_data(self,ret): + if len(self._check_path_data) == 0: + self._check_path_data.append(ret) + else: + if self.check_database_data2(self._check_path_data,ret): + for i in self._check_path_data: + if i['id']==ret['id']: + i['name'] = ret['name'] + i['path']=ret['path'] + i['status']=ret['status'] + else: + self._check_path_data.append(ret) + public.writeFile(self._chekc_path, json.dumps(self._check_path_data)) + return True + + # 显示所有网站信息 + def get_sites(self,get): + data= public.M('sites').field('id,name,path,status,ps,addtime,edate').select() + for i in data: + data2=self.GetSSL(i['name']) + i['ssl']=data2['status'] + if data2['status']: + i['time'] = data2['cert_data'] + else: + i['time'] =False + return data + + + def get_databases(self,get): + data= public.M('databases').field('id,name,username,password,accept,ps,addtime').select() + return data + + # 是否跳转到https + def IsToHttps(self, siteName): + file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf'; + conf = public.readFile(file); + if conf: + if conf.find('HTTP_TO_HTTPS_START') != -1: return True; + if conf.find('$server_port !~ 443') != -1: return True; + return False; + + # 取SSL状态 + def GetSSL(self, siteName): + self.setupPath = '/www/server' + path = os.path.join('/www/server/panel/vhost/cert/', siteName) + if not os.path.isfile(os.path.join(path, "fullchain.pem")) and not os.path.isfile(os.path.join(path, "privkey.pem")): + path = os.path.join('/etc/letsencrypt/live/', siteName) + type = 0; + if os.path.exists(path + '/README'): type = 1; + if os.path.exists(path + '/partnerOrderId'): type = 2; + csrpath = path + "/fullchain.pem"; # 生成证书路径 + keypath = path + "/privkey.pem"; # 密钥文件路径 + key = public.readFile(keypath); + csr = public.readFile(csrpath); + file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + siteName + '.conf'; + conf = public.readFile(file); + keyText = 'SSLCertificateFile' + if public.get_webserver() == 'nginx': keyText = 'ssl_certificate'; + status = True + if not conf or conf.find(keyText) == -1: + status = False + type = -1 + toHttps = self.IsToHttps(siteName) + id = public.M('sites').where("name=?", (siteName,)).getField('id') + domains = public.M('domain').where("pid=?", (id,)).field('name').select() + cert_data= {} + if csr: + cert_data = self.GetCertName(csrpath) + email = public.M('users').where('id=?',(1,)).getField('email') + if email == '287962566@qq.com': email = '' + return {'status': status, 'cert_data':cert_data} + + #转换时间 + def strfToTime(self,sdate): + import time + return time.strftime('%Y-%m-%d',time.strptime(sdate,'%b %d %H:%M:%S %Y %Z')) + + #获取证书名称 + def GetCertName(self,certPath): + try: + openssl = '/usr/local/openssl/bin/openssl'; + if not os.path.exists(openssl): openssl = 'openssl'; + result = public.ExecShell(openssl + " x509 -in "+certPath+" -noout -subject -enddate -startdate -issuer") + tmp = result[0].split("\n"); + data = {} + data['subject'] = tmp[0].split('=')[-1] + data['notAfter'] = self.strfToTime(tmp[1].split('=')[1]) + data['notBefore'] = self.strfToTime(tmp[2].split('=')[1]) + if tmp[3].find('O=') == -1: + data['issuer'] = tmp[3].split('CN=')[-1] + else: + data['issuer'] = tmp[3].split('O=')[-1].split(',')[0] + if data['issuer'].find('/') != -1: data['issuer'] = data['issuer'].split('/')[0]; + result = public.ExecShell(openssl + " x509 -in "+certPath+" -noout -text|grep DNS") + data['dns'] = result[0].replace('DNS:','').replace(' ','').strip().split(','); + return data; + except: + print(public.get_error_info()) + return None; + + + # 显示所有网站信息 + def get_sites_or_ssl(self,get): + data= public.M('sites').field('id,name,path,status,ps,addtime,edate').select() + for i in data: + i['ssl']=self.GetSSL(i['name']) + return data + + + #backup_database + def backup_database(self,get): + if not public.M('databases').where("name=?",(get.name,)).count():return public.returnMsg(False,'The database does not exist') + id=public.M('databases').where("name=?", (get.name,)).getField('id') + if not id:return public.returnMsg(False,'The database does not exist') + if os.path.exists(self._chek_site_file): + return public.returnMsg(False, 'A backup task already exists in this time period. It is recommended that you choose another time period') + public.ExecShell( python_bin + ' /www/server/panel/class/backup_bak.py database %s &'%id) + return public.returnMsg(True,'OK') + + # backup_database + def backup_site(self, get): + if not public.M('sites').where("name=?", (get.name,)).count(): return public.returnMsg(False, "Website does not exist") + id = public.M('sites').where('name=?',(get.name,)).getField('id') + if not id:return public.returnMsg(False, "Website does not exist") + + #监测是否存在任务 + if os.path.exists(self._chek_site_file): + return public.returnMsg(False, 'A backup task already exists in this time period. It is recommended that you choose another time period') + + + public.ExecShell(python_bin+ ' /www/server/panel/class/backup_bak.py sites %s &' % id) + return public.returnMsg(True, 'OK') + + # backup_path + def backup_path_data(self, get): + if not os.path.exists(get.path):return public.returnMsg(False, "Directory does not exist") + public.ExecShell(python_bin+ ' /www/server/panel/class/backup_bak.py path %s &' % get.path) + return public.returnMsg(True, 'OK') + + #检测数据库执行错误 + def IsSqlError(self,mysqlMsg): + mysqlMsg=str(mysqlMsg) + if "MySQLdb" in mysqlMsg: return False + if "2002," in mysqlMsg or '2003,' in mysqlMsg: return False + if "using password:" in mysqlMsg: return False + if "Connection refused" in mysqlMsg: return False + if "1133" in mysqlMsg: return False + if "libmysqlclient" in mysqlMsg:return False + + #配置 + def mypass(self,act,root): + public.ExecShell("sed -i '/user=root/d' /etc/my.cnf") + public.ExecShell("sed -i '/password=/d' /etc/my.cnf") + if act: + mycnf = public.readFile('/etc/my.cnf'); + rep = "\\[mysqldump\\]\nuser=root" + sea = "[mysqldump]\n" + subStr = sea + "user=root\npassword=\"" + root + "\"\n"; + mycnf = mycnf.replace(sea,subStr) + if len(mycnf) > 100: public.writeFile('/etc/my.cnf',mycnf); + + def backup_database2(self,id): + if not public.M('databases').where("id=?", (id,)).count(): + ret = {} + ret['id'] = id + ret['name'] = False + ret['status'] = False + ret['path'] = False + ret['chekc']=False + self.set_site_data(ret) + return public.returnMsg(False, 'The database does not exist') + id=int(id) + # 添加到chekc_database 中 + ret={} + ret['id']=id + ret['name']=public.M('databases').where("id=?", (id,)).getField('name') + ret['status']=False + ret['path']=False + ret['chekc'] = True + self.set_database_data(ret) + if not os.path.exists(self._chek_site_file): public.ExecShell('touch %s' % self._chek_site_file) + path=self.backup_database_data(id) + os.remove(self._chek_site_file) + + ret['status'] = True + ret['path'] = path + self.set_database_data(ret) + + def backup_path_data2(self,path): + id=''.join(random.sample(string.ascii_letters + string.digits, 4)) + if not os.path.exists(path): + ret = {} + ret['id'] = id + ret['name'] = False + ret['status'] = False + ret['path'] = False + ret['chekc']=False + self.set_path_data(ret) + return public.returnMsg(False, "Directory does not exist") + # 添加到chekc_database 中 + ret={} + ret['id']=id + ret['name']=path + ret['status']=False + ret['path']=False + ret['chekc'] = True + self.set_path_data(ret) + path2=self.backup_path(path) + ret['status'] = True + ret['path'] = path2 + self.set_path_data(ret) + return True + + def backup_site2(self,id): + if not public.M('sites').where("id=?", (id,)).count(): + ret = {} + ret['id'] = id + ret['name'] = False + ret['status'] = False + ret['path'] = False + ret['chekc']=False + self.set_site_data(ret) + return public.returnMsg(False, "Site does not exist") + id=int(id) + # 添加到chekc_database 中 + ret={} + ret['id']=id + ret['name']=public.M('sites').where("id=?", (id,)).getField('name') + ret['status']=False + ret['path']=False + ret['chekc'] = True + self.set_site_data(ret) + if not os.path.exists(self._chek_site_file): public.ExecShell('touch %s' % self._chek_site_file) + path=self.backup_site_data(id) + os.remove(self._chek_site_file) + ret['status'] = True + ret['path'] = path + self.set_site_data(ret) + return True + + #备份数据库 + def backup_database_data(self,id): + result = panelMysql.panelMysql().execute("show databases") + isError =self.IsSqlError(result) + if isError: return isError + name = public.M('databases').where("id=?", (id,)).getField('name') + root = public.M('config').where('id=?', (1,)).getField('mysql_root') + if not os.path.exists('/www/server/panel/BTPanel/static' + '/database'): public.ExecShell( + 'mkdir -p ' + '/www/server/panel/BTPanel/static' + '/database'); + self.mypass(True, root) + path_id = ''.join(random.sample(string.ascii_letters + string.digits, 20)) + fileName = path_id+'DATA'+name + '_' + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.sql.gz' + backupName = '/www/server/panel/BTPanel/static'+ '/database/' + fileName + public.ExecShell("/www/server/mysql/bin/mysqldump --default-character-set=" + public.get_database_character( + name) + " --force --opt \"" + name + "\" | gzip > " + backupName) + if not os.path.exists(backupName): return public.returnMsg(False, 'BACKUP_ERROR') + self.mypass(False, root) + sql = public.M('backup') + addTime = time.strftime('%Y-%m-%d %X', time.localtime()) + sql.add('type,name,pid,filename,size,addtime', (1, fileName, id, backupName, 0, addTime)) + public.WriteLog("TYPE_DATABASE", "DATABASE_BACKUP_SUCCESS", (name,)) + return backupName + + #备份网站 + def backup_site_data(self,id): + path_id = ''.join(random.sample(string.ascii_letters + string.digits, 20)) + find = public.M('sites').where("id=?",(id,)).field('name,path,id').find() + import time + fileName = path_id+'WEB'+find['name']+'_'+time.strftime('%Y%m%d_%H%M%S',time.localtime())+'.zip' + backupPath = '/www/server/panel/BTPanel/static'+ '/site' + zipName = backupPath + '/'+fileName + if not (os.path.exists(backupPath)): os.makedirs(backupPath) + tmps = '/tmp/panelExec.log' + execStr = "cd '" + find['path'] + "' && zip '" + zipName + "' -x .user.ini -r ./ > " + tmps + " 2>&1" + public.ExecShell(execStr) + sql = public.M('backup').add('type,name,pid,filename,size,addtime',(0,fileName,find['id'],zipName,0,public.getDate())) + public.WriteLog('TYPE_SITE', 'SITE_BACKUP_SUCCESS',(find['name'],)) + return zipName + + #备份目录 + def backup_path(self,path): + import time + path_id = ''.join(random.sample(string.ascii_letters + string.digits, 20)) + fileName =path_id+ path.replace('/','_')+'_'+time.strftime('%Y%m%d_%H%M%S',time.localtime())+'.zip' + backupPath = '/www/server/panel/BTPanel/static'+ '/path' + zipName = backupPath + '/'+fileName + if not (os.path.exists(backupPath)): os.makedirs(backupPath) + tmps = '/tmp/panelExec.log' + execStr = "cd '" + path + "' && zip '" + zipName + "' -x .user.ini -r ./ > " + tmps + " 2>&1" + public.ExecShell(execStr) + public.WriteLog('TYPE_FILE', 'Backup folder [ % s] succeeded'%path) + print(zipName) + return zipName + + #查看数据备份进度 + def get_database_progress(self,get): + id=get.id + for i in self._check_database_data: + if int(i['id'])==int(id): + return public.returnMsg(True, i) + else: + return public.returnMsg(False,'False') + + #查看网站备份进度 + def get_site_progress(self,get): + id=get.id + for i in self._check_site_data: + if int(i['id']) == int(id): + return public.returnMsg(True, i) + else: + return public.returnMsg(False, 'False') + + #查看网站备份进度 + def get_path_progress(self,get): + id=get.id + for i in self._check_path_data: + if i['id'] == id: + return public.returnMsg(True, i) + else: + return public.returnMsg(False, 'False') + + ###########文件下载 + # 判断是否在_check_database_data 中 + def check_down_data(self, data, ret): + if len(data) == 0: return False + for i in data: + if i['id'] == ret['id'] and i['type']==ret['type']: + return True + else: + return False + + def set_down_data(self, ret): + if len(self._down_path_data) == 0: + self._down_path_data.append(ret) + else: + if self.check_database_data(self._down_path_data, ret): + for i in self._down_path_data: + if i['id'] == ret['id'] and i['type']==ret['type']: + i['name'] = ret['name'] + i['url']=ret['url'] + i['filename']=ret['filename'] + i['status'] = ret['status'] + else: + self._down_path_data.append(ret) + public.writeFile(self._down_path, json.dumps(self._down_path_data)) + return True + + #下载对方的备份文件 + def download_path(self,get): + filename=get.filename + ret = {} + ret['type']=get.type + ret['id']=get.id + ret['name']=get.name + ret['url']=get.url + ret['filename']=filename + ret['status']=False + self.set_down_data(ret) + print(python_bin+ ' /www/server/panel/class/backup_bak.py down %s %s %s %s %s &'%(get.url,filename,get.type,get.id,get.name)) + public.ExecShell(python_bin+ ' /www/server/panel/class/backup_bak.py down %s %s %s %s %s &'%(get.url,filename,get.type,get.id,get.name)) + return True + + def down2(self,url,filename,type,id,name): + self.down(url,filename) + ret={} + ret['url'] = url + ret['type']=type + ret['id']=id + ret['name']=name + ret['filename'] = filename + ret['status']=True + self.set_down_data(ret) + + #测试下载 + def down(self,url,filename): + print(url) + print("Download to %s"%filename) + down=downloadFile.downloadFile() + ret=down.DownloadFile(url,filename) + print('Download completed') + return True + + #查看网站备份进度 + def get_down_progress(self,get): + id=get.id + type=get.type + for i in self._down_path_data: + if i['id'] == id and i['type']==type: + return public.returnMsg(True, i) + else: + return public.returnMsg(False, 'False') + + # backup_database + def backup_site_all(self, get): + #监测是否存在任务 + if os.path.exists(self._chek_site_file): + return public.returnMsg(False, 'A backup task already exists in this time period. It is recommended that you choose another time period') + + public.ExecShell(python_bin+ ' /www/server/panel/class/backup_bak.py sites_ALL 11 &') + return public.returnMsg(True, 'OK') + + def set_backup_all(self): + data = public.M('sites').field('id,name,path,status,ps,addtime,edate').select() + site_list = [] + #进度格式 总数量 当前数量 和返回的结果 + jindu={} + jindu['start_count']=len(data) + jindu['end_count']=0 + jindu['resulit']=site_list + public.writeFile(self._check_all_site, json.dumps(jindu)) + if not os.path.exists(self._chek_site_file): public.ExecShell('touch %s' % self._chek_site_file) + for i in data: + path = self.backup_site_data(i['id']) + if path: + resulit = {} + resulit['id'] = i['id'] + resulit['path'] = path + resulit['type'] = 'sites' + resulit['name']=i['name'] + jindu['resulit'].append(resulit) + jindu['end_count'] +=1 + print(jindu) + public.writeFile(self._check_all_site, json.dumps(jindu)) + os.remove(self._chek_site_file) + return site_list + + #查看网站备份进度 + def get_all_site_progress(self,get): + return self._check_site_all_data + + # backup_database + def backup_date_all(self, get): + if os.path.exists(self._chek_site_file): + return public.returnMsg(False, 'A backup task already exists in this time period. It is recommended that you choose another time period') + public.ExecShell(python_bin+ ' /www/server/panel/class/backup_bak.py database_ALL 11 &') + return public.returnMsg(True, 'OK') + + def backup_all_database(self): + data = public.M('databases').field('id,name,username,password,accept,ps,addtime').select() + site_list = [] + #进度格式 总数量 当前数量 和返回的结果 + jindu={} + jindu['start_count']=len(data) + jindu['end_count']=0 + jindu['resulit']=site_list + public.writeFile(self._check_all_date, json.dumps(jindu)) + if not os.path.exists(self._chek_site_file): public.ExecShell('touch %s' % self._chek_site_file) + for i in data: + path = self.backup_database_data(i['id']) + if path: + resulit = {} + resulit['id'] = i['id'] + resulit['path'] = path + resulit['type'] = 'sites' + resulit['name'] = i['name'] + jindu['resulit'].append(resulit) + jindu['end_count'] +=1 + print(jindu) + public.writeFile(self._check_all_date, json.dumps(jindu)) + os.remove(self._chek_site_file) + return site_list + + #查看网站备份进度 + def get_all_date_progress(self,get): + return self._check_date_all_data + +if __name__ == '__main__': + p = backup_bak() + ret = sys.argv[1] + type = sys.argv[2] + if ret =='sites': + p.backup_site2(type) + elif ret=='sites_ALL': + p.set_backup_all() + elif ret=='database_ALL': + p.backup_all_database() + elif ret=='database': + p.backup_database2(type) + elif ret=='path': + p.backup_path_data2(type) + elif ret=='down': + filename = sys.argv[3] + down_type=sys.argv[4] + down_id=sys.argv[5] + down_name=sys.argv[6] + p.down2(type,filename,down_type,down_id,down_name) + diff --git a/class_v2/btdockerModelV2/appModel.py b/class_v2/btdockerModelV2/appModel.py new file mode 100644 index 00000000..7fd3476d --- /dev/null +++ b/class_v2/btdockerModelV2/appModel.py @@ -0,0 +1,79 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 - Docker应用 +# ------------------------------ +import public +import os +import time +import json +import re +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +class main(dockerBase): + + def __init__(self): + pass + + # 2024/2/20 下午 4:31 获取/搜索docker应用的列表 + def get_app_list(self, get=None): + ''' + @name 获取docker应用的列表 + @author wzz <2024/2/20 下午 4:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + try: + from btdockerModelV2 import registryModel as dr + dr.main().registry_list(get) + + from panelPlugin import panelPlugin + pp = panelPlugin() + get.type = 10 + # get.type = 16 # dev docker + # get.type = 14 # www docker + get.force = get.force if "force" in get and get.force else 0 + if not hasattr(get, "query"): + get.query = "" + get.tojs = "soft.get_list" + # softList = pp.get_soft_list(get) + if get.query != "": + get.row = 1000 + softList = pp.get_soft_list(get) + softList['list'] = self.struct_list(softList['list']) + softList['list'] = pp.get_page(softList['list']['data'], get) + else: + + softList = pp.get_soft_list(get) + return public.return_message(0, 0, softList['list']) + except Exception as e: + # public.print_log("1111111111 进方法") + return public.return_message(-1, 0, e) + + # 2024/2/20 下午 4:47 处理云端软件列表,只需要list中type=13的数据 + def struct_list(self, softList: dict): + ''' + @name 处理云端软件列表,只需要list中type=13的数据 + @param softList: + @return: + ''' + new_list = [] + for i in softList['data']: + # if i['type'] == 14: # www docker + # if i['type'] == 16: # dev docker + if i['type'] == 10: + new_list.append(i) + + softList['data'] = new_list + + return softList diff --git a/class_v2/btdockerModelV2/backupModel.py b/class_v2/btdockerModelV2/backupModel.py new file mode 100644 index 00000000..98461fd7 --- /dev/null +++ b/class_v2/btdockerModelV2/backupModel.py @@ -0,0 +1,185 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import os +import time +import traceback +import gettext +_ = gettext.gettext + +# ------------------------------ +# Docker模型 +# ------------------------------ +import public +from btdockerModelV2 import containerModel as dc +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +class main(dockerBase): + + # 2023/12/22 上午 9:56 备份指定容器的mount v11olume + def backup_volume(self, get): + ''' + @name 备份指定容器的mount volume + @author wzz <2023/12/22 上午 11:19> + @param "data":{"container_id":"容器ID"} + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + # 校验参数 + try: + get.validate([ + Param('container_id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + client = dp.docker_client() + container = client.containers.get(get.container_id) + volume_list = container.attrs["Mounts"] + volume_list = [v["Source"] for v in volume_list] + + if not volume_list: + return public.return_message(-1, 0, _("There is no volume to back up")) + + backup_path = "/www/backup/btdocker/volumes/{}".format(container.name) + if not os.path.exists(backup_path): + os.makedirs(backup_path, 0o755) + + import subprocess + public.ExecShell("echo -n > {}".format(self._backup_log)) + + for v in volume_list: + backup_name = os.path.basename(v) + # 2023/12/22 上午 10:34 每个压缩包命名都用v的目录名,如果是文件则用文件名 + tar_name = "{}_{}_{}.tar.gz".format( + container.name, + backup_name, + time.strftime("%Y%m%d_%H%M%S", time.localtime()) + ) + backup_file = os.path.join(backup_path, tar_name) + source_path = os.path.dirname(v) + cmd = "cd {} && tar zcvf {} {}".format(source_path, backup_file, backup_name) + cmd = ("nohup echo 'To start backing up {} of container {}, it may take more than 1-5 minutes...' >> {};" + "{} >> {} 2>&1 &&" + "echo 'bt_successful' >> {} || echo 'bt_failed' >> {} &" + .format( + container.name, + tar_name, + self._backup_log, + cmd, + self._backup_log, + self._backup_log, + self._backup_log, + )) + subprocess.Popen(cmd, shell=True) + + # 2023/12/22 下午 12:17 添加到数据库 + dp.sql('dk_backup').add( + 'type,name,container_id,container_name,filename,size,addtime', + (3, tar_name, container.id, container.name, backup_file, 0, time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime() + )) + ) + public.WriteLog("Docker module", "The {} of the backup container {} succeeds!".format(container.name, tar_name)) + + return public.return_message(0, 0, _("The backup task was created successfully.")) + except Exception as e: + print(traceback.format_exc()) + return public.return_message(-1, 0, _("Failed to create a backup task {}".format(str(e)))) + + # 2023/12/22 上午 11:23 获取指定容器的备份列表 + def get_backup_list(self, get): + ''' + @name 获取指定容器的备份列表 + @param "data":{"container_id":"容器ID"} + @return list[dict{"":""}] + ''' + # 校验参数 + try: + get.validate([ + Param('container_id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + # 2023/12/22 下午 12:24 从数据库中获取已备份的指定容器 + backup_list = dp.sql('dk_backup').where('container_id=?', (get.container_id,)).field( + 'name,container_id,container_name,filename,size,addtime' + ).select() + + for l in backup_list: + if not os.path.exists(l['filename']): + l['size'] = 0 + l['ps'] = 'file does not exist' + continue + + l['size'] = os.path.getsize(l['filename']) + l['ps'] = 'local backup' + + return public.return_message(0, 0, backup_list) + + except Exception as e: + print(traceback.format_exc()) + return public.return_message(0, 0, []) + + # 2023/12/22 下午 2:25 删除指定容器的备份 + def remove_backup(self, get): + ''' + @name 删除指定容器的备份 + @param "data":{"container_id":"容器ID","container_name":"容器名","name":"文件名"} + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + # 校验参数 + try: + get.validate([ + Param('container_id').Require().String(), + Param('container_name').Require().String(), + Param('name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + try: + # 2023/12/22 下午 2:26 从数据库中删除指定容器的备份 + dp.sql('dk_backup').where('container_id=? and name=?', (get.container_id, get.name)).delete() + + # 2023/12/22 下午 2:27 删除本地备份文件 + backup_path = "/www/backup/btdocker/volumes/{}".format(get.container_name) + file_path = os.path.join(backup_path, get.name) + if not os.path.exists(file_path): + return public.return_message(0, 0, _("successfully delete")) + os.remove(file_path) + return public.return_message(0, 0, _("successfully delete")) + except Exception as e: + print(traceback.format_exc()) + return public.return_message(-1, 0, _("{} Failed to delete the file, reason: {}".format(get.name, str(e)))) + + def get_pull_log(self, get): + """ + 获取镜像拉取日志,websocket + @param get: + @return: + """ + get.wsLogTitle = "Start container directory backup, please wait..." + get._log_path = self._backup_log + return self.get_ws_log(get) \ No newline at end of file diff --git a/class_v2/btdockerModelV2/composeModel.py b/class_v2/btdockerModelV2/composeModel.py new file mode 100644 index 00000000..4172a4fb --- /dev/null +++ b/class_v2/btdockerModelV2/composeModel.py @@ -0,0 +1,1044 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +import json +import os +import time +import gettext +_ = gettext.gettext + +import public +from btdockerModelV2 import containerModel as dc +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + + +class main(dockerBase): + + def get_docker_compose_version(self): + try: + import subprocess + result = subprocess.run(["docker-compose", "version", "--short"], capture_output=True, text=True) + version_str = result.stdout.strip() + major, minor, patch = map(int, version_str.split('.')) + return major, minor, patch + except Exception as e: + print("Error:", e) + return None + + # 验证配置文件 + def check_conf(self, path): + # 2024/3/21 下午 5:58 检测path是否存在中文,如果有就false return + if public.check_chinese(path): + return public.returnMsg(False, "The file path cannot contain Chinese characters!") + + # 2024/3/20 下午 4:33 获取docker-compose的版本,如果大于v2.24.7则不需要检测,比如是v2.25,解决高版本的docker-compose + version = self.get_docker_compose_version() + if version and version > (2, 24, 7): + tmpfile = public.md5(path) + public.ExecShell(r"\cp -r {} /tmp/{}.yml".format(path, public.md5(path))) + public.ExecShell("sed -i '/version:/d' /tmp/{}.yml".format(tmpfile)) + path = "/tmp/{}.yml".format(tmpfile) + + shell = "/usr/bin/docker-compose -f {} config".format(path) + a, e = public.ExecShell(shell) + if e and "setlocale: LC_ALL: cannot change locale (en_US.UTF-8)" not in e: + return public.returnMsg(False, "Detection failed: {}".format(e)) + return public.returnMsg(True, "Detection passes!") + + # 用引导方式创建模板 + def add_template_gui(self, get): + """ + 用引导方式创建模板 + :param name 模板名 + :param description 模板描述 + :param data 模板内容 {"version":3,"services":{...}...} + :param get: + 模板文件参数: + version 2/3version + 2: 仅支持单机 + 3:支持单机和多机模式 + services: + 多个容器的集合 + 下一层执行服务名 + 如web1,服务名下面指定服务的变量 + web1: + build: . 基于dockerfile构建一个镜像 + image: nginx 服务所使用的镜像为nginx + container_name: "web" 容器名 + depends_on: 该服务在db服务启动后再启动 + - db + ports: + - "6061:80" 将容器的80端口映射到主机的6061端口 + networks: + - frontend 该容器所在的网络 + deploy: 指定与部署和运行服务相关的配置(在使用 swarm时才会生效) + replicas: 6 6个副本 + update_config: + parallelism: 2 + delay: 10s + restart_policy: + condition: on-failure + 其他详细描述可以参考 https://docs.docker.com/compose/compose-file/compose-file-v3 + :return: + """ + import yaml + path = "{}/template".format(self.compose_path) + file = "{}/{}.yaml".format(path, get.name) + if not os.path.exists(path): + os.makedirs(path) + data = json.loads(get.data) + yaml.dump(data, file) + + def get_template_kw(self, get): + data = { + "version": "", + "services": { + "server_name_str": { # 用户输入 + "build": { + "context": "str", + "dockerfile": "str", + "get": [], + "cache_from": [], + "labels": [], + "network": "str", + "shm_size": "str", + "target": "str" + }, + "cap_add": "", + "cap_drop": "", + "cgroup_parent": "str", + "command": "str", + "configs": { + "my_config_str": [] + }, + "container_name": "str", + "credential_spec": { + "file": "str", + "registry": "str" + }, + "depends_on": [], + "deploy": { + "endpoint_mode": "str", + "labels": { + "key": "value" + }, + "mode": "str", + "placement": [{"key": "value"}], + "max_replicas_per_node": "int", + "replicas": "int", + "resources": { + "limits": { + "cpus": "str", + "memory": "str", + }, + "reservations": { + "cpus": "str", + "memory": "str", + }, + "restart_policy": { + "condition": "str", + "delay": "str", + "max_attempts": "int", + "window": "str" + } + } + } + } + } + } + + # 创建项目配置文件 + def add_template(self, get): + """ + 添加一个模板文件 + :param name 模板名 + :param remark 模板描述 + :param data 模板内容 + :param get: + :return: + """ + import re + name = get.name + if not re.search(r"^[\w\.\-]+$", name): + return public.return_message(-1, 0, + "Template names cannot contain special characters; only letters, numbers, underscores, dots, and underscores are supported") + + template_list = self._template_list(get) + for template in template_list: + if name == template['name']: + return public.return_message(-1, 0, _("This template name already exists!")) + + path = "{}/{}/template".format(self.compose_path, name) + file = "{}/{}.yaml".format(path, name) + if not os.path.exists(path): + os.makedirs(path) + public.writeFile(file, get.data) + + check_res = self.check_conf(file) + if not check_res['status']: + if os.path.exists(file): + os.remove(file) + return public.return_message(-1, 0, check_res['msg']) + + pdata = { + "name": name, + "remark": public.xsssec(get.remark), + "path": file + } + dp.sql("templates").insert(pdata) + dp.write_log("Added template [{}] successfully!".format(name)) + public.set_module_logs('docker', 'add_template', 1) + return public.return_message(0, 0,_("Template added successfully!")) + + def edit_template(self, get): + """ + :param id 模板id + :param data 模板内容 + :param remark 模板描述 + :param get: + :return: + """ + template_info = dp.sql("templates").where("id=?", (get.id,)).find() + if not template_info: + return public.return_message(-1, 0, _("Did not change the template!")) + + if "data" not in get: + return public.return_message(-1, 0, + "Template content format error, please enter a valid docker-compose template!") + + if "version" not in get.data: + return public.return_message(-1, 0, + "Template content format error, please enter a valid docker-compose template!") + + public.writeFile(template_info['path'], get.data) + check_res = self.check_conf(template_info['path']) + if not check_res['status']: + return public.return_message(-1, 0,check_res['msg']) + pdata = { + "name": get.name, + "remark": public.xsssec(get.remark), + "path": template_info['path'] + } + dp.sql("templates").where("id=?", (get.id,)).update(pdata) + dp.write_log("Edit template [{}] successful!".format(template_info['name'])) + return public.return_message(0, 0, _("Modified template successfully!")) + + def get_template(self, get): + """ + id 模板ID + 获取模板内容 + :return: + """ + + # 校验参数 + try: + get.validate([ + Param('template_id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + template_info = dp.sql("templates").where("id=?", (get.template_id,)).find() + if not template_info: + return public.return_message(-1, 0, _("This template was not found!")) + + return public.return_message(0, 0, public.readFile(template_info['path'])) + + def template_list(self, get): + """ + 获取所有模板 + :param get: + :return: + """ + template = dp.sql("templates").select()[::-1] + if not isinstance(template, list): + template = [] + + return public.return_message(0, 0, template) + + # 内部调用 不改响应格式 + def _template_list(self, get): + """ + 获取所有模板 + :param get: + :return: + """ + template = dp.sql("templates").select()[::-1] + if not isinstance(template, list): + template = [] + + return template + + def remove_template(self, get): + """ + 删除模板 + :param template_id + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('template_id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + data = dp.sql("templates").where("id=?", (get.template_id,)).find() + if not data: + return public.return_message(-1, 0, _("This template was not found!")) + if os.path.exists(data['path']): + os.remove(data['path']) + dp.sql("templates").delete(id=get.template_id) + dp.write_log("Delete template [{}] successfully!".format(data['name'])) + return public.return_message(0, 0, _("successfully delete!")) + + def edit_project_remark(self, get): + """ + 编辑项目 + :param project_id 项目 + :param remark备注 + :param get: + :return: + """ + stacks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not stacks_info: + return public.returnMsg(False, "The item was not found!") + pdata = { + "remark": public.xsssec(get.remark) + } + dp.write_log("Comment for project [{}] changed successfully [{}] --> [{}]!".format(stacks_info['name'], + stacks_info['remark'], + public.xsssec(get.remark))) + dp.sql("stacks").where("id=?", (get.project_id,)).update(pdata) + + def edit_template_remark(self, get): + """ + 编辑项目 + :param templates_id 项目 + :param remark备注 + :param get: + :return: + """ + stacks_info = dp.sql("templates").where("id=?", (get.templates_id,)).find() + if not stacks_info: + return public.returnMsg(False, "The template was not found!") + pdata = { + "remark": public.xsssec(get.remark) + } + dp.write_log( + "Modify template [{}] Remark successful [{}] --> [{}]!".format(stacks_info['name'], stacks_info['remark'], + public.xsssec(get.remark))) + dp.sql("templates").where("id=?", (get.templates_id,)).update(pdata) + + def create_project_in_path(self, name, path): + shell = "cd {} && /usr/bin/docker-compose -p {} up -d &> {}".format("/".join(path.split("/")[:-1]), name, + self._log_path) + public.ExecShell(shell) + + def create_project_in_file(self, project_name, file): + project_path = "{}/{}".format(self.compose_path, project_name) + project_file = "{}/docker-compose.yaml".format(project_path) + if not os.path.exists(project_path): + os.makedirs(project_path) + template_content = public.readFile(file) + public.writeFile(project_file, template_content) + shell = "/usr/bin/docker-compose -p {} -f {} up -d &> {}".format(project_name, project_file, self._log_path) + public.ExecShell(shell) + + def check_project_container_name(self, template_data, get): + """ + 检测模板文件中的容器名是否已经存在 + :return: + """ + import re + data = [] + template_container_name = re.findall("container_name\\s*:\\s*[\"\']+(.*)[\'\"]", template_data) + # 调用容器列表接口 选择不改统一返回的 + container_list = dc.main()._get_list(get) + + container_list = container_list['container_list'] + for container in container_list: + if container['name'] in template_container_name: + data.append(container['name']) + if data: + return public.returnMsg(False, "The container name already exists!:
                                    [{}]".format(", ".join(data))) + # 获取模板所使用的端口 + rep = r"(\d+):\d+" + port_list = re.findall(rep, template_data) + for port in port_list: + if dp.check_socket(port): + return public.returnMsg(False, "This port [{}] is already used by other templates".format(port)) + + # 创建项目 + def create(self, get): + """ + :param project_name 项目名 + :param remark 描述 + :param template_id 模板ID + :param rags: + :return: + """ + # {"template_id": "13", "project_name": "dedf2f", "remark": "从本地添加asd"} + # 校验参数 + try: + get.validate([ + Param('template_id').Require().Integer(), + Param('project_name').Require().String().Xss(), + Param('remark').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + project_name = public.md5(get.project_name) + # if "template_id" not in get: + # return public.returnMsg(False, "Parameter error, please pass in template_id!") + template_id = get.template_id + template_info = dp.sql("templates").where("id=?", template_id).find() + if len(template_info) < 1: + return public.return_message(-1, 0, _("This template was not found, or file is corrupt!")) + + if not os.path.exists(template_info['path']): + return public.return_message(-1, 0, _("Template file does not exist")) + + template_exist = dp.sql("stacks").where("template_id=?", (template_id,)).find() + if template_exist: + return public.return_message(-1, 0, + "Template [{}] has been deployed by project: [{}], please change a template and try again!".format( + template_info['name'], template_exist['name'])) + + name_exist = self.check_project_container_name(public.readFile(template_info['path']), get) + if name_exist: + return public.return_message(-1, 0, name_exist['msg']) + + stacks_info = dp.sql("stacks").where("name=?", (project_name)).find() + if not stacks_info: + pdata = { + "name": public.xsssec(get.project_name), + "status": "1", + "path": template_info['path'], + "template_id": template_id, + "time": time.time(), + "remark": public.xsssec(get.remark) + } + dp.sql("stacks").insert(pdata) + else: + return public.return_message(-1, 0, _("The project name already exists!")) + + if template_info['add_in_path'] == 1: + self.create_project_in_path( + project_name, + template_info['path'] + ) + else: + self.create_project_in_file( + project_name, + template_info['path'] + ) + dp.write_log("Project [{}] deployed successfully!".format(project_name)) + public.set_module_logs('docker', 'add_project', 1) + return public.return_message(0, 0, _("Successful deployment!")) + except Exception as ex: + public.print_log(traceback.format_exc()) + return public.return_message(-1, 0, str(ex)) + + def compose_project_list(self, get): + """ + 获取所有已部署的项目列表 + @param get: + """ + compose_project = dp.sql("stacks").select() + # public.print_log("部署项目 {}".format(compose_project)) + try: + cmd_result = public.ExecShell("/usr/bin/docker-compose ls -a --format json")[0] + if "Segmentation fault" in cmd_result: + return public.returnMsg(False, "docker-compose is too low, please upgrade to the latest version!") + result = json.loads(cmd_result) + except: + result = [] + + for i in compose_project: + for j in result: + if public.md5(i['name']) in j['Name']: + i['run_status'] = j['Status'].split("(")[0].lower() + break + else: + i['run_status'] = "exited" + + return public.return_message(0, 0, compose_project) + + def project_container_count(self, get): + """ + 获取项目容器数量 + @param get: + @return: + """ + from btdockerModelV2.dockerSock import container + sk_container = container.dockerContainer() + sk_container_list = sk_container.get_container() + + stacks_info = dp.sql("stacks").select() + net_info = [] + + for i in stacks_info: + count = 0 + for c in sk_container_list: + if public.md5(i['name']) in c["Names"][0].replace("/", ""): + count += 1 + continue + + if 'com.docker.compose.project' in c.keys(): + if public.md5(i['name']) in c['com.docker.compose.project.config_files']: + count += 1 + continue + + if public.md5(i['name']) in public.md5(c['com.docker.compose.project.config_files']): + count += 1 + continue + + if 'com.docker.compose.project' in c['Labels'].keys(): + if public.md5(i['name']) in c['Labels']['com.docker.compose.project.config_files']: + count += 1 + continue + + if public.md5(i['name']) in public.md5(c['Labels']['com.docker.compose.project.config_files']): + count += 1 + continue + + net_info.append(count) + + return public.return_message(0, 0, net_info) + + def get_compose_container(self, get): + """ + 目前仅支持本地 url: unix:///var/run/docker.sock + """ + # 校验参数 + try: + get.validate([ + Param('name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + from btdockerModelV2.dockerSock import container + sk_container = container.dockerContainer() + sk_container_list = sk_container.get_container() + + project_container_list = [] + for c in sk_container_list: + if public.md5(get.name) in dp.rename(c["Names"][0].replace("/", "")): + project_container_list.append(dc.main().struct_container_list(c)) + continue + + if 'com.docker.compose.project' in c.keys(): + if public.md5(get.name) in c['com.docker.compose.project.config_files']: + project_container_list.append(dc.main().struct_container_list(c)) + + if public.md5(get.name) in public.md5(c['com.docker.compose.project.config_files']): + project_container_list.append(dc.main().struct_container_list(c)) + + if 'com.docker.compose.project' in c['Labels'].keys(): + if public.md5(get.name) in c['Labels']['com.docker.compose.project.config_files']: + project_container_list.append(dc.main().struct_container_list(c)) + + if public.md5(get.name) in public.md5(c['Labels']['com.docker.compose.project.config_files']): + project_container_list.append(dc.main().struct_container_list(c)) + + return public.return_message(0, 0, project_container_list) + + # 删除项目 + def remove(self, get): + """ + project_id 数据库记录的项目ID + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('project_id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + statcks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not statcks_info: + return public.return_message(-1, 0, _("The project name was not found!")) + container_name = public.ExecShell("docker ps --format \"{{.Names}}\"") + if statcks_info['name'] in container_name[0]: + shell = f"/usr/bin/docker-compose -p {statcks_info['name']} -f {statcks_info['path']} down &> {self._log_path}" + else: + shell = f"/usr/bin/docker-compose -p {public.md5(statcks_info['name'])} -f" \ + f" {statcks_info['path']} down &> {self._log_path}" + public.ExecShell(shell) + dp.sql("stacks").delete(id=get.project_id) + dp.write_log("Delete project [{}] success!".format(statcks_info['name'])) + return public.return_message(0, 0, _("successfully delete!")) + + def prune(self, get): + """ + 删除所有没有容器的项目 + @param get: + @return: + """ + stacks_info = dp.sql("stacks").select() + container_name = public.ExecShell("docker ps --format \"{{.Names}}\"")[0] + container_name = container_name.split("\n") + for i in stacks_info: + # 2024/3/21 下午 6:26 如果i['name']在container_name[0]中,说明容器还在运行,不删除 + is_run = False + docker_name = public.ExecShell("grep 'container_name' {}".format(i["path"]))[0] + + for j in container_name: + if j == "": continue + if public.md5(i['name']) in j or j in docker_name: + is_run = True + break + + if is_run: continue + shell = "/usr/bin/docker-compose -f {} down &> {}".format(i['path'], self._log_path) + public.ExecShell(shell) + dp.sql("stacks").delete(id=i['id']) + dp.write_log("Cleanup project [{}] successful!".format(i['name'])) + return public.return_message(0, 0, _("Clean up successfully!")) + + + def set_compose_status(self, get): + """ + 设置项目状态 + @param get: + @return: + """ + try: + get.validate([ + + Param('status').Require().String('in', ['start', 'stop','restart','pause','unpause','kill']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if get.status == 'start': + data = self.start(get) + + elif get.status == 'stop': + data = self.stop(get) + elif get.status == 'restart': + data = self.restart(get) + elif get.status == 'pause': + data = self.pause(get) + elif get.status == 'unpause': + data = self.unpause(get) + else: + data = self.kill(get) + + if data["status"]: + return public.return_message(0, 0, data['msg']) + else: + return public.return_message(-1, 0, data['msg']) + + def kill(self, get): + """ + 强制停止项目 + @param get: + @return: + """ + statcks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not statcks_info: + return public.returnMsg(False, "Project configuration not found!") + shell = "/usr/bin/docker-compose -f {} kill &> {}".format( + "{}/data/compose/{}/docker-compose.yaml".format(public.get_panel_path(), public.md5(statcks_info['name'])), + self._log_path + ) + a, e = public.ExecShell(shell) + if e: + return public.returnMsg(False, "Stopping project failed: {}".format(e)) + dp.write_log("Stopping project [{}] succeeded".format(statcks_info['name'])) + return public.returnMsg(True, "Setup successful!") + + def stop(self, get): + """ + 停止项目 + project_id 数据库记录的项目ID + kill 强制停止项目 0/1 + :param get: + :return: + """ + statcks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not statcks_info: + return public.returnMsg(False, "Project configuration not found!") + + shell = "/usr/bin/docker-compose -f {} stop &> {}".format( + "{}/data/compose/{}/docker-compose.yaml".format(public.get_panel_path(), public.md5(statcks_info['name'])), + self._log_path + ) + a, e = public.ExecShell(shell) + if e: + return public.returnMsg(False, "Stopping project failed: {}".format(e)) + dp.write_log("Stopping project [{}] succeeded!".format(statcks_info['name'])) + return public.returnMsg(True, "Setup successful!") + + def start(self, get): + """ + 启动项目 + project_id 数据库记录的项目ID + :param get: + :return: + """ + statcks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not statcks_info: + return public.returnMsg(False, "Project configuration not found!") + + shell = "/usr/bin/docker-compose -f {} start &> {}".format( + "{}/data/compose/{}/docker-compose.yaml".format(public.get_panel_path(), public.md5(statcks_info['name'])), + self._log_path + ) + a, e = public.ExecShell(shell) + if e: + return public.returnMsg(False, "Failed to start project: {}".format(e)) + dp.write_log("Start project [{}] successful!".format(statcks_info['name'])) + return public.returnMsg(True, "Setup successful!") + + def restart(self, get): + """ + 拉取项目内需要的镜像 + project_id 数据库记录的项目ID + :param get: + :return: + """ + statcks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not statcks_info: + return public.returnMsg(False, "Project configuration not found!") + + shell = "/usr/bin/docker-compose -f {} restart &> {}".format( + "{}/data/compose/{}/docker-compose.yaml".format(public.get_panel_path(), public.md5(statcks_info['name'])), + self._log_path + ) + a, e = public.ExecShell(shell) + if e: + return public.returnMsg(False, "Failed to restart project: {}".format(e)) + dp.write_log("Restart project [{}] successfully!".format(statcks_info['name'])) + return public.returnMsg(True, "Successfully set!") + + def pull(self, get): + """ + 拉取模板内需要的镜像 + template_id 数据库记录的项目ID + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('template_id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + statcks_info = dp.sql("templates").where("id=?", (get.template_id,)).find() + if not statcks_info: + return public.return_message(0, 0, _("The template was not found!")) + + os.system( + "nohup /usr/bin/docker-compose -f {} pull >> {} 2>&1 " + "&& echo 'bt_successful' >> {} " + "|| echo 'bt_failed' >> {} &".format( + statcks_info['path'], + self._log_path, + self._log_path, + self._log_path, + )) + dp.write_log("The image inside the template [{}] was pulled successfully !".format(statcks_info['name'])) + return public.return_message(0, 0, _("Pull successfully!")) + + def pause(self, get): + """ + 暂停项目 + project_id 数据库记录的项目ID + :param get: + :return: + """ + statcks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not statcks_info: + return public.returnMsg(False, "Project configuration not found!") + shell = "/usr/bin/docker-compose -f {} pause &> {}".format( + "{}/data/compose/{}/docker-compose.yaml".format(public.get_panel_path(), public.md5(statcks_info['name'])), + self._log_path + ) + a, e = public.ExecShell(shell) + if e: + return public.returnMsg(False, "Failed to suspend project: {}".format(e)) + dp.write_log("Pause [{}] success!".format(statcks_info['name'])) + return public.returnMsg(True, "Successfully set!") + + def unpause(self, get): + """ + 取消暂停项目 + project_id 数据库记录的项目ID + :param get: + :return: + """ + statcks_info = dp.sql("stacks").where("id=?", (get.project_id,)).find() + if not statcks_info: + return public.returnMsg(False, "Project configuration not found!") + shell = "/usr/bin/docker-compose -f {} unpause &> {}".format( + "{}/data/compose/{}/docker-compose.yaml".format(public.get_panel_path(), public.md5(statcks_info['name'])), + self._log_path + ) + a, e = public.ExecShell(shell) + if e: + return public.returnMsg(False, "Failed to unpause project: {}".format(e)) + dp.write_log("Unpause [{}] success!".format(statcks_info['name'])) + return public.returnMsg(True, "Successfully set!") + + def scan_compose_file(self, path, data): + """ + 递归扫描目录下的compose文件 + :param path 需要扫描的目录 + :param data 需要返回的数据 一个字典 + :param get: + :return: + """ + file_list = os.listdir(path) + for file in file_list: + current_path = os.path.join(path, file) + # 判断是否是文件夹 + if os.path.isdir(current_path): + self.scan_compose_file(current_path, data) + else: + if file == "docker-compose.yaml" or file == "docker-compose.yam" or file == "docker-compose.yml": + if "/www/server/panel/data/compose" in current_path: + continue + data.append(current_path) + if ".yaml" in file or ".yam" in file or ".yml" in file: + if "/www/server/panel/data/compose" in current_path: + continue + data.append(current_path) + return data + + def get_compose_project(self, get): + """ + :param path 需要获取的路径 是一个目录 + :param sub_dir 扫描子目录 + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('path').Require().SafePath(), + Param('sub_dir').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + data = list() + suffix = ["yaml", "yam", "yml"] + if get.path == "/": + return public.return_message(-1, 0, _("Unable to scan the root directory")) + + if get.path[-1] == "/": + get.path = get.path[:-1] + if str(get.sub_dir) == "1": + res = self.scan_compose_file(get.path, data) + if not res: + res = [] + else: + tmp = list() + p_name_tmp = list() + for i in res: + if i.split(".")[1] not in suffix: + continue + + project_name = i.split("/")[-1].split(".")[0] + if project_name in p_name_tmp: + project_name = "{}_{}".format(project_name, i.split("/")[-2]) + + tmp_data = { + "project_name": project_name, + "conf_file": "/".join(i.split("/")), + "remark": "Add locally" + } + + tmp.append(tmp_data) + p_name_tmp.append(tmp_data['project_name']) + res = tmp + p_name_tmp.clear() + else: + yaml = "{}/docker-compose.yaml".format(get.path) + yam = "{}/docker-compose.yam".format(get.path) + yml = "{}/docker-compose.yml".format(get.path) + if os.path.exists(yaml): + res = [{ + "project_name": get.path.split("/")[-1], + "conf_file": yaml, + "remark": "Add locally" + }] + elif os.path.exists(yam): + res = [{ + "project_name": get.path.split("/")[-1], + "conf_file": yam, + "remark": "Add locally" + }] + elif os.path.exists(yml): + res = [{ + "project_name": get.path.split("/")[-1], + "conf_file": yml, + "remark": "Add locally" + }] + else: + res = list() + + if not os.path.isdir(get.path): + return public.return_message(0, 0, res) + + dir_list = os.listdir(get.path) + + for i in dir_list: + if i.rsplit(".")[-1] in suffix: + res.append({ + "project_name": i.rsplit(".")[0], + "conf_file": "/".join(get.path.split("/") + [i]), + "remark": "Add locally" + }) + + return public.return_message(0, 0, res) + + # 从现有目录中添加模板 + def add_template_in_path(self, get): + """ + :param template_list list [{"project_name":"pathtest_template","conf_file":"/www/dockerce/mysecent-project/docker-compose.yaml","remark":"描述描述"}] + :param get: + :return: + """ + + create_failed = dict() + create_successfully = dict() + for template in get.template_list: + path = template['conf_file'] + name = template['project_name'] + remark = template['remark'] + exists = self._template_list(get) + for i in exists: + if name == i['name']: + create_failed[name] = "Template already exists!" + continue + if not os.path.exists(path): + create_failed[name] = "This template was not found!" + continue + check_res = self.check_conf(path) + if not check_res['status']: + create_failed[name] = "Template validation failed, possibly malformed!" + continue + pdata = { + "name": name, + "remark": remark, + "path": path, + "add_in_path": 1 + } + dp.sql("templates").insert(pdata) + create_successfully[name] = "Template added successfully!" + + for i in create_failed: + if i in create_successfully: + del (create_successfully[i]) + else: + dp.write_log("Template added successfully from path [{}]!".format(i)) + if not create_failed and create_successfully: + # return {'status': True, 'msg': 'Template added successfully: [{}]'.format(','.join(create_successfully))} + return public.return_message(0, 0, + 'Template added successfully: [{}]'.format(','.join(create_successfully))) + elif not create_successfully and create_failed: + + # return {'status': False, + # 'msg': 'Failed to add template: template name already exists or is incorrectly formatted [{}],Use docker-compose -f [specify compose.yml file] config to check' + # .format(','.join(create_failed))} + + return public.return_message(-1, 0, + 'Failed to add template: template name already exists or is incorrectly formatted [{}],Use docker-compose -f [specify compose.yml file] config to check' + .format(','.join(create_failed))) + + # return {'status': False, 'msg': 'These templates succeed: [{}]
                                    These templates fail: the template name already exists or is incorrectly formatted [{}]'.format( + # ','.join(create_successfully), ','.join(create_failed))} + return public.return_message(-1, 0, + 'These templates succeed: [{}]
                                    These templates fail: the template name already exists or is incorrectly formatted [{}]'.format( + ','.join(create_successfully), ','.join(create_failed))) + + def get_pull_log(self, get): + """ + 获取镜像拉取日志,websocket + @param get: + @return: + """ + get.wsLogTitle = "Start to pull the template image, please wait..." + get._log_path = self._log_path + return self.get_ws_log(get) + + # 编辑项目 todo 根据删除适配 + def edit(self, get): + """ + :param project_id: 要编辑的项目的ID + :param project_name: 新的项目名 + :param remark: 新的描述 + :param template_id: 新的模板ID + :return: + """ + # {"project_id": 1, "template_id": 2, "project_name": "福达坊", "remark": ""} + # 校验参数 + try: + get.validate([ + Param('project_id').Require().Integer(), + Param('project_name').Require().String(), + Param('remark').String(), + Param('template_id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # 删除旧的项目 + remove_result = self.remove(get) + # if not remove_result['status']: + # return public.return_message(-1, 0, _("Fail to modify!")) + + # 创建新的项目 + self.create(get) + return public.return_message(0, 0, _("Modify successfully!")) diff --git a/class_v2/btdockerModelV2/config/com_reg_mirror.json b/class_v2/btdockerModelV2/config/com_reg_mirror.json new file mode 100644 index 00000000..da28faa7 --- /dev/null +++ b/class_v2/btdockerModelV2/config/com_reg_mirror.json @@ -0,0 +1,3 @@ +{ + "https://docker.m.daocloud.io": "Third party image accelerator" +} \ No newline at end of file diff --git a/class_v2/btdockerModelV2/config/com_registry.json b/class_v2/btdockerModelV2/config/com_registry.json new file mode 100644 index 00000000..83c3bc3c --- /dev/null +++ b/class_v2/btdockerModelV2/config/com_registry.json @@ -0,0 +1,11 @@ +{ + "docker.io": "docker official mirror site", + "swr.cn-north-4.myhuaweicloud.com": "Huawei Cloud Mirror Station (North China-Beijing 4)", + "ccr.ccs.tencentyun.com": "Tencent Cloud Mirror Station", + "registry.cn-hongkong.aliyuncs.com": "Alibaba Cloud Mirror Station (Hong Kong)", + "registry.ap-southeast-1.aliyuncs.com": "Alibaba Cloud Mirror Station (Singapore)", + "registry.us-west-1.aliyuncs.com": "Alibaba Cloud Mirror Station (Silicon Valley, USA)", + "registry.eu-west-1.aliyuncs.com": "Alibaba Cloud Mirror Station (London, UK)", + "registry.eu-central-1.aliyuncs.com": "Alibaba Cloud Mirror Station (Frankfurt, Germany)", + "registry.ap-northeast-1.aliyuncs.com": "Alibaba Cloud Mirror Station (Japan)" +} \ No newline at end of file diff --git a/class_v2/btdockerModelV2/config/docker_hub_last_update.pl b/class_v2/btdockerModelV2/config/docker_hub_last_update.pl new file mode 100644 index 00000000..8d76e180 --- /dev/null +++ b/class_v2/btdockerModelV2/config/docker_hub_last_update.pl @@ -0,0 +1 @@ +1710901654 \ No newline at end of file diff --git a/class_v2/btdockerModelV2/config/docker_hub_repos.db b/class_v2/btdockerModelV2/config/docker_hub_repos.db new file mode 100644 index 00000000..09f66fa1 Binary files /dev/null and b/class_v2/btdockerModelV2/config/docker_hub_repos.db differ diff --git a/class_v2/btdockerModelV2/containerModel.py b/class_v2/btdockerModelV2/containerModel.py new file mode 100644 index 00000000..678a2811 --- /dev/null +++ b/class_v2/btdockerModelV2/containerModel.py @@ -0,0 +1,1743 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import json +import os +import traceback +import time + +import crontab +import docker.errors + +import gettext +_ = gettext.gettext +# ------------------------------ +# Docker模型 +# ------------------------------ +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + + +class main(dockerBase): + + def __init__(self): + super().__init__() + self.alter_table() + if public.M('sqlite_master').db('docker_log_split').where('type=? AND name=?', ('table', 'docker_log_split')).count(): + # if public.M('sqlite_master').where('type=? AND name=?',('table', 'docker_log_split')).count(): + p = crontab.crontab() + llist = p.GetCrontab(None) + add_crond = True + if type(llist) == list: + for i in llist: + if i['name'] == "[Do not delete] Docker log cuts": + add_crond = False + break + else: + add_crond = True + + if add_crond: + get = { + "name": "[Do not delete] Docker log cuts", + "type": "minute-n", + "where1": 5, + "hour": "", + "minute": "", + "week": "", + "sType": "toShell", + "sName": "", + "backupTo": "localhost", + "save": '', + "sBody": "btpython /www/server/panel/script/dk_log_split.py", + "urladdress": "undefined" + } + p.AddCrontab(get) + + def alter_table(self): + if not dp.sql('sqlite_master').db('container').where('type=? AND name=? AND sql LIKE ?', + ('table', 'container', '%sid%')).count(): + dp.sql('container').execute("alter TABLE container add container_name VARCHAR DEFAULT ''", ()) + + def docker_client(self, url): + # unix:///var/run/docker.sock + return dp.docker_client(url) + + def get_cmd_log(self, get): + """ + 获取命令运行容器的日志,websocket + @param get: + @return: + """ + get.wsLogTitle = "Please wait to execute the command..." + get._log_path = self._rCmd_log + return self.get_ws_log(get) + + def run_cmd(self, get): + """ + 命令行创建运行容器(docker run),需要做危险命令校验,存在危险命令则不执行 + @param get: + @return: + """ + try: + get.validate([ + Param('cmd').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + import re + # if not hasattr(get, 'cmd'): + # return public.return_message(-1, 0, 'cmd parameter error') + if "docker run" not in get.cmd: + return public.return_message(-1, 0, _('Only the docker run command can be executed')) + + danger_cmd = ['rm', 'rmi', 'kill', 'stop', 'pause', 'unpause', 'restart', 'update', 'exec', 'init', + 'shutdown', 'reboot', 'chmod', 'chown', 'dd', 'fdisk', 'killall', 'mkfs', 'mkswap', 'mount', + 'swapoff', 'swapon', 'umount', 'userdel', 'usermod', 'passwd', 'groupadd', 'groupdel', + 'groupmod', 'chpasswd', 'chage', 'usermod', 'useradd', 'userdel', 'pkill'] + + danger_symbol = ['&', '&&', '||', '|', ';'] + + for d in danger_cmd: + if get.cmd.startswith(d) or re.search(r'\s{}\s'.format(d), get.cmd): + return public.return_message(-1, 0, _( + 'Dangerous command exists: [{}],Execution is not allowed!'.format(d))) + + for d in danger_symbol: + if d in get.cmd: + return public.return_message(-1, 0, _( + 'There are danger symbols: [{}],Execution is not allowed!'.format(d))) + + os.system("echo -n > {}".format(self._rCmd_log)) + os.system("nohup {} >> {} 2>&1 && echo 'bt_successful' >> {} || echo 'bt_failed' >> {} &".format( + get.cmd, + self._rCmd_log, + self._rCmd_log, + self._rCmd_log, + )) + + return public.return_message(0, 0, _("The command has been executed!")) + + # 添加容器 + def run(self, get): + """ + :param name:容器名 + :param image: 镜像 + :param publish_all_ports 暴露所有端口 1/0 + :param ports 暴露某些端口 {'1111/tcp': ('127.0.0.1', 1111)} + :param command 命令 + :param entrypoint 配置容器启动后执行的命令 + :param environment 环境变量 xxx=xxx 一行一条 + :param auto_remove 当容器进程退出时,在守护进程端启用自动移除容器。 0/1 + + :param get: + :return: + """ + # {"name": "cgroupg1", "image": "nginx:1.19.6", "publish_all_ports": "0", "ports": "", "network": "", + # "ip_address": "", "command": "", "entrypoint": "", "auto_remove": "0", "privileged": "0", + # "restart_policy": {"Name": ""}, "mem_reservation": "8855MB", "cpu_quota": 6, "mem_limit": "15555MB", + # "labels": "", "environment": ""} + + # 校验参数 + try: + get.validate([ + Param('name').Require().String(), + Param('image').Require().String(), + Param('publish_all_ports').Require().Integer(), + Param('command').String(), + Param('entrypoint').String(), + Param('environment').String(), + Param('auto_remove').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + config_path = "{}/config/name_map.json".format(public.get_panel_path()) + if not os.path.exists(config_path): + public.writeFile(config_path, json.dumps({})) + + if public.readFile(config_path) == '': + public.writeFile(config_path, json.dumps({})) + + # 2024/2/20 下午 3:21 如果检测到是中文的容器名,则自动转换为英文 + name_map = json.loads(public.readFile(config_path)) + import re + if re.findall(r"[\u4e00-\u9fa5]", get.name): + name_str = 'q18q' + public.GetRandomString(10).lower() + name_map[name_str] = get.name + get.name = name_str + public.writeFile(config_path, json.dumps(name_map)) + + cPorts = get.ports if "ports" in get and get.ports != "" else False + nPorts = {} + if not cPorts is False: + if ":" in cPorts.keys(): + return public.return_message(-1, 0, _( "The port format is wrong, this method is not supported!")) + if "-" in cPorts.keys(): + return public.return_message(-1, 0, _( "The port format is wrong, this method is not supported!")) + + for i in cPorts.keys(): + if cPorts[i] == "": continue + if dp.check_socket(cPorts[i]): + return public.return_message(-1, 0, _( + "Server port [{}] is occupied, please change to another port!".format( + cPorts[i]))) + + if "tcp/udp" in i: + cPort = i.split('/')[0] + nPorts[str(cPort) + "/tcp"] = cPorts[i] + nPorts[str(cPort) + "/udp"] = cPorts[i] + else: + nPorts[i] = cPorts[i] + del cPorts + + if "image" not in get or not get.image: + return public.return_message(-1, 0, _( + "Please pull the image you need")) + + if get.image == "" or "" in get.image: + return public.return_message(-1, 0, _( + "The image does not exist!")) + + mem_limit = get.mem_limit if "mem_limit" in get and get.mem_limit != "0" else None + if not mem_limit is None: + mem_limit_byte = dp.byte_conversion(get.mem_limit) + if mem_limit_byte > dp.get_mem_info(): + return public.return_message(-1, 0, _( "The memory quota has exceeded the available amount!")) + if mem_limit_byte < 6291456: + return public.return_message(-1, 0, _( "The memory quota cannot be less than 6MB!")) + + try: + if "force_pull" in get and get.force_pull == "0": + self.docker_client(self._url).images.get(get.image) + except docker.errors.ImageNotFound as e: + return public.return_message(-1, 0, _( + "Image [{}] does not exist, You can try [Forced Pull]!".format( + get.image))) + except docker.errors.APIError as e: + return public.return_message(-1, 0, _( + "Image [{}] does not exist, You can try [Forced Pull]!".format( + get.image))) + + # 2024/4/16 上午11:40 检查镜像是否存在并且处理镜像如果是非应用容器的情况 + try: + from btdockerModelV2.dockerSock import image + sk_image = image.dockerImage() + image_inspect = sk_image.inspect(get.image) + if type(image_inspect) != dict: + return public.return_message(-1, 0, _( "Image [{}] does not exist!".format(get.image))) + + if "Config" in image_inspect and "Cmd" in image_inspect["Config"]: + sh_list = ("bash", "sh", "dash", "/bin/sh", "/bin/bash", "/bin/dash") + if len(image_inspect["Config"]["Cmd"]) == 1 and image_inspect["Config"]["Cmd"][0] in sh_list: + get.tty = "1" + get.stdin_open = "1" + except Exception as e: + pass + + cpu_quota = get.cpu_quota if "cpu_quota" in get and get.cpu_quota != "0" else 0 + if int(cpu_quota) != 0: + cpu_quota = float(get.cpu_quota) * 100000 + + if int(cpu_quota) / 100000 > dp.get_cpu_count(): + return public.return_message(-1, 0, _( "cpu quota has exceeded available cores!")) + + df_restart_policy = {"Name": "unless-stopped", "MaximumRetryCount": 0} + restart_policy = get.restart_policy if "restart_policy" in get and get.restart_policy else df_restart_policy + if restart_policy['Name'] == "always": + restart_policy = {"Name": "always"} + + mem_reservation = get.mem_reservation if "mem_reservation" in get and get.mem_reservation != "" else None + # 2023/12/19 下午 3:08 检测如果小于6MB则报错 + if not mem_reservation is None and mem_reservation != "0": + mem_reservation_byte = dp.byte_conversion(mem_reservation) + if mem_reservation_byte < 6291456: + return public.return_message(-1, 0, _( "Memory reservation cannot be less than 6MB!")) + + network = get.network if "network" in get and get.network != "" else "bridge" + ip_address = get.ip_address if "ip_address" in get and get.ip_address != "" else None + + try: + res = self.docker_client(self._url).containers.run( + name=get.name, + image=get.image, + detach=True, + # cpuset_cpus=get.cpuset_cpus ,#指定容器使用的cpu个数 + tty=True if "tty" in get and get.tty == "1" else False, + stdin_open=True if "stdin_open" in get and get.stdin_open == "1" else False, + publish_all_ports=True if "publish_all_ports" in get and get.publish_all_ports != "0" else False, + ports=nPorts if len(nPorts) > 0 else None, + cpu_quota=int(cpu_quota) or 0, + mem_reservation=mem_reservation, # b,k,m,g + mem_limit=mem_limit, # b,k,m,g + restart_policy=restart_policy, + command=get.command if "command" in get and get.command != "" else None, + volume_driver=get.volume_driver if "volume_driver" in get and get.volume_driver != "" else None, + volumes=get.volumes if "volumes" in get and get.volumes != "" else None, + auto_remove=True if "auto_remove" in get and get.auto_remove != "0" else False, + privileged=True if "privileged" in get and get.privileged != "0" else False, + environment=dp.set_kv(get.environment), # "HOME=/value\nHOME11=value1" + labels=dp.set_kv(get.labels), # "key=value\nkey1=value1" + network=network, + ) + + except docker.errors.APIError as e: + if "invalid reference format" in str(e): + return public.return_message(-1, 0, _( + "The image name format is incorrect.such as:nginx:latest")) + if "failed to create task for container" in str(e) or "failed to create shim task" in str(e): + return public.return_message(-1, 0, _( "Container creation failed, details: {}!".format(str(e)))) + if "Minimum memory limit can not be less than memory reservation limit, see usage" in str(e): + return public.return_message(-1, 0, _( "The memory quota cannot be less than the memory reserve!")) + if "already exists in network bridge" in str(e): + return public.return_message(-1, 0, _( + "The container name or network bridge already exists. Please change the container name and try again!")) + if "No command specified" in str(e): + return public.return_message(-1, 0, _( + "There is no startup command in this image, please specify the container startup command!")) + if "permission denied" in str(e): + return public.return_message(-1, 0, _( "Permission exception! Details:{}".format(str(e)))) + if "Internal Server Error" in str(e): + return public.return_message(-1, 0, _( + "Container creation failed! Please restart the docker service at the appropriate time!")) + if "repository does not exist or may require 'docker login'" in str(e): + return public.return_message(-1, 0, _( + "Image [{}] does not exist!".format( + get.image))) + if "Minimum memory reservation allowed is 6MB" in str(e): + return public.return_message(-1, 0, _( "Memory reservation cannot be less than 6MB!")) + if "container to be able to reuse that name." in str(e): + return public.return_message(-1, 0, _( "Container name already exists!")) + if "Invalid container name" in str(e): + return public.return_message(-1, 0, _( "The container name is invalid")) + if "bind: address already in use" in str(e): + port = "" + for i in get.ports: + if ":{}:".format(get.ports[i]) in str(e): + port = get.ports[i] + get.id = get.name + self.del_container(get) + return public.return_message(-1, 0, _( "Server port {} in use! Change the other port".format(port))) + return public.return_message(-1, 0, _( 'Creation failure! {}'.format(public.get_error_info()))) + except Exception as a: + public.print_log(traceback.format_exc()) + self.del_container(get) + if "Read timed out" in str(a): + return public.return_message(-1, 0, _( + "The container creation failed and the connection to docker timed out!")) + return public.return_message(-1, 0, _( 'Container failed to run! {}'.format(str(a)))) + + if res: + # print(res.status) + # print(res.id) + # dk_config = self.docker_client(self._url).containers.get(res.id) + # print(dk_config.attrs['NetworkSettings']['Networks'][network]['IPAddress']) + # 将容器的ip改成用户指定的ip + pdata = { + "cpu_limit": str(get.cpu_quota), + "container_name": get.name + } + dp.sql('container').insert(pdata) + public.set_module_logs('docker', 'run_container', 1) + dp.write_log("Container creation [{}] successful!".format(get.name)) + + # 2024/2/26 下午 6:00 添加备注 + self.check_remark_table() + dp.sql('dk_container_remark').insert({ + "container_id": res.id, + "container_name": get.name, + "remark": public.xssencode2(get.remark), + "addtime": int(time.time()) + }) + + if not ip_address is None: + try: + self.docker_client(self._url).networks.get(network).disconnect(res.id) + self.docker_client(self._url).networks.get(network).connect(res.id, ipv4_address=ip_address) + # dk_config = self.docker_client(self._url).containers.get(res.id) + # print(dk_config.attrs['NetworkSettings']['Networks'][network]['IPAddress']) + # logs = res.logs(stdout=True, stderr=True) + # print(logs.decode()) + except docker.errors.APIError as e: + if "Invalid IPv4 address" in str(e): + return public.return_message(0, 0, _( + "Created successfully, but the IP [{}] is illegal and the IP has been automatically assigned.".format( + ip_address))) + + # 返回包含容器的id和name + return public.return_message(0, 0, { + "status": True, + "result": "Successfully created!", + "id": res.id, + "name": dp.rename(res.name), + }) + + # return public.returnMsg(True, "容器创建成功!") + return public.return_message(-1, 0, _( 'Creation failure!')) + + def upgrade_container(self, get): + """ + 更新正在运行的容器镜像(重建) + @param get: + @return: + """ + try: + if "id" not in get: + return public.return_message(-1, 0, _( "Container ID is abnormal")) + + container = self.docker_client(self._url).containers.get(get.id) + + old_container_config = self.save_container_config(container) + new_image = get.new_image if "new_image" in get and get.new_image else "latest" + if new_image is None: + return public.return_message(-1, 0, _( "The new image name cannot be empty!")) + + if "upgrade" in get and get.upgrade == "1": + get.new_image = "{}:{}".format(old_container_config["image"].split(':')[0], new_image) + + try: + if "force_pull" in get and get.force_pull == "1": + public.ExecShell("docker pull {}".format(get.new_image)) + except docker.errors.ImageNotFound as e: + return public.return_message(-1, 0, _( "The mirror does not exist!")) + except docker.errors.APIError as e: + return public.return_message(-1, 0, "The mirror does not exist!") + # except Exception as e: + # public.print_log(traceback.format_exc()) + # return public.return_message(-1, 0, e) + + + get.old_container_config = old_container_config + new_container_config = self.structure_new_container_conf(get) + if type(new_container_config) != dict: + return public.return_message(-1, 0, _( new_container_config['msg'])) + + container.stop() + container.remove() + + try: + new_container = self.docker_client(self._url).containers.create( + name=new_container_config["name"], + image=new_container_config["image"], + detach=new_container_config["detach"], + cpu_quota=new_container_config["cpu_quota"], + mem_limit=new_container_config["mem_limit"], + tty=new_container_config["tty"], + stdin_open=new_container_config["stdin_open"], + publish_all_ports=new_container_config["publish_all_ports"], + ports=new_container_config["ports"], + command=new_container_config["command"], + entrypoint=new_container_config["entrypoint"], + environment=new_container_config["environment"], + labels=new_container_config["labels"], + auto_remove=new_container_config["auto_remove"], + privileged=new_container_config["privileged"], + volumes=new_container_config["volumes"], + volume_driver=new_container_config["volume_driver"], + mem_reservation=new_container_config["mem_reservation"], + restart_policy=new_container_config["restart_policy"], + network=new_container_config["network"], + ) + except Exception as e: + public.print_log(traceback.format_exc()) + public.print_log("修改 创建新容器报错--") + + if "Read timed out" in str(e): + return public.return_message(-1, 0, _( + "Container editing failed and the connection to docker timed out.")) + # print(traceback.format_exc()) + return public.return_message(-1, 0, _( "Update failed!{}".format(str(e)))) + + new_container.start() + if not "upgrade" in get or get.upgrade != "1": + new_ip_address = get.new_ip_address if "new_ip_address" in get and get.new_ip_address else \ + old_container_config["ip_address"] + new_network = get.new_network if "new_network" in get and get.new_network else \ + old_container_config["network"] + + if new_network != "bridge": + try: + self.docker_client(self._url).networks.get(new_network).disconnect(new_container.id) + self.docker_client(self._url).networks.get(new_network).connect( + new_container.id, ipv4_address=new_ip_address + ) + + except docker.errors.APIError as e: + if ("user specified IP address is supported only when " + "connecting to networks with user configured subnets") in str(e): + self.docker_client(self._url).networks.get(new_network).connect(new_container.id) + return public.return_message(0, 0, _( + "Editing successful, [{}] has not specified a subnet, and the IP has been automatically assigned!" + .format(str(new_network)))) + + except Exception as e: + public.print_log(traceback.format_exc()) + self.docker_client(self._url).networks.get(new_network).connect(new_container.id) + print(traceback.format_exc()) + return public.return_message(0, 0, _( + "Editing successful, network [{}] setting failed, IP has been automatically assigned to you, error details: {}!" + .format(new_network, str(e)))) + + return public.return_message(0, 0, _("Update successfully!")) + except docker.errors.NotFound as e: + if "No such container" in str(e): + return public.return_message(-1, 0, _("Container does not exist!")) + return public.return_message(-1, 0, _("Update failed!{}".format(str(e)))) + except docker.errors.APIError as e: + if "No such container" in str(e): + return public.return_message(-1, 0, _("Container does not exist!")) + return public.return_message(-1, 0, _("Update failed!{}".format(str(e)))) + except Exception as a: + public.print_log(traceback.format_exc()) + if "Read timed out" in str(a): + return public.return_message(-1, 0, + _("Container editing failed and the connection to docker timed out.")) + return public.return_message(-1, 0, _("Update failed!{}".format(str(a)))) + + def save_container_config(self, container): + """ + 保存容器的配置信息 + """ + ip_address, network = None, None + if len(container.attrs['NetworkSettings']['Networks']) != 0: + Networks = container.attrs['NetworkSettings']['Networks'][list(container.attrs['NetworkSettings']['Networks'].keys())[0]] + ip_address = Networks['IPAddress'] + network = Networks['NetworkID'] + + container_config = { + "image": container.attrs['Config']['Image'], + "name": container.attrs['Name'], + "detach": True, + "cpu_quota": container.attrs['HostConfig']['CpuQuota'], + "mem_limit": container.attrs['HostConfig']['Memory'], + "tty": container.attrs['Config']['Tty'], + "stdin_open": container.attrs['Config']['OpenStdin'], + "publish_all_ports": container.attrs['HostConfig']['PublishAllPorts'], + "ports": container.attrs['NetworkSettings']['Ports'], + "command": container.attrs['Config']['Cmd'], + "entrypoint": container.attrs['Config']['Entrypoint'], + "environment": container.attrs['Config']['Env'], + "labels": container.attrs['Config']['Labels'], + "auto_remove": container.attrs['HostConfig']['AutoRemove'], + "privileged": container.attrs['HostConfig']['Privileged'], + "volumes": container.attrs['HostConfig']['Binds'], + "volume_driver": container.attrs['HostConfig']['VolumeDriver'], + "mem_reservation": container.attrs['HostConfig']['MemoryReservation'], + "restart_policy": container.attrs['HostConfig']['RestartPolicy'], + "network": network, + "ip_address": ip_address, + } + return container_config + + def structure_new_container_conf(self, get): + """ + 构造新的容器配置 + @param get: + @return: + """ + new_image = get.new_image if hasattr(get, "new_image") and get.new_image else get.old_container_config["image"] + new_name = get.new_name if hasattr(get, "new_name") and get.new_name else get.old_container_config["name"].replace("/", "") + new_cpu_quota = get.new_cpu_quota if hasattr(get, "new_cpu_quota") and get.new_cpu_quota != 0 else get.old_container_config["cpu_quota"] + if int(new_cpu_quota) != 0: + new_cpu_quota = float(new_cpu_quota) * 100000 + + if int(new_cpu_quota) / 100000 > dp.get_cpu_count(): + return public.returnMsg(False, _( "cpu quota has exceeded available cores!")) + + new_mem_limit = get.new_mem_limit if hasattr(get, "new_mem_limit") and get.new_mem_limit else get.old_container_config["mem_limit"] + new_tty = get.new_tty if hasattr(get, "new_tty") and get.new_tty else get.old_container_config["tty"] + new_stdin_open = get.new_stdin_open if hasattr(get, "new_stdin_open") and get.new_stdin_open else get.old_container_config["stdin_open"] + new_publish_all_ports = get.new_publish_all_ports if hasattr(get, "new_publish_all_ports") and get.new_publish_all_ports != '0' else get.old_container_config["publish_all_ports"] + new_ports = get.new_ports if hasattr(get, "new_ports") and get.new_ports else get.old_container_config["ports"] + new_command = get.new_command if hasattr(get, "new_command") else get.old_container_config["command"] + new_entrypoint = get.new_entrypoint if hasattr(get, "new_entrypoint") else get.old_container_config["entrypoint"] + new_environment = get.new_environment if hasattr(get, "new_environment") and get.new_environment != '' else get.old_container_config["environment"] + new_labels = get.new_labels if hasattr(get, "new_labels") and get.new_labels != '' else get.old_container_config["labels"] + new_auto_remove = True if hasattr(get, "new_auto_remove") and get.new_auto_remove != '0' else get.old_container_config["auto_remove"] + new_privileged = True if hasattr(get, "new_privileged") and get.new_privileged != '0' else get.old_container_config["privileged"] + new_volumes = get.new_volumes if hasattr(get, "new_volumes") and get.new_volumes else get.old_container_config["volumes"] + new_volume_driver = get.new_volume_driver if hasattr(get, "new_volume_driver") and get.new_volume_driver else get.old_container_config["volume_driver"] + new_mem_reservation = get.new_mem_reservation if hasattr(get, "new_mem_reservation") and get.new_mem_reservation else get.old_container_config["mem_reservation"] + new_restart_policy = get.new_restart_policy if hasattr(get, "new_restart_policy") and get.new_restart_policy else get.old_container_config["restart_policy"] + new_network = get.new_network if hasattr(get, "new_network") and get.new_network else get.old_container_config["network"] + + container_config = { + "image": new_image, + "name": new_name, + "detach": True, + "cpu_quota": int(new_cpu_quota), + "mem_limit": new_mem_limit, + "tty": new_tty, + "stdin_open": new_stdin_open, + "publish_all_ports": new_publish_all_ports, + "ports": new_ports, + "command": new_command, + "entrypoint": new_entrypoint, + "environment": dp.set_kv(new_environment) if type(new_environment) != list else new_environment, + "labels": dp.set_kv(new_labels) if type(new_labels) != dict else new_labels, + "auto_remove": new_auto_remove, + "privileged": new_privileged, + "volumes": new_volumes, + "volume_driver": new_volume_driver, + "mem_reservation": new_mem_reservation, + "restart_policy": new_restart_policy, + "network": new_network, + } + return container_config + + def commit(self, get): + """ + 保存为镜像 + :param repository 推送到的仓库 + :param tag 镜像标签 jose:v1 + :param message 提交的信息 + :param author 镜像作者 + :param changes + :param conf dict + :param path 导出路径 + :param name 导出文件名 + :param get: + :return: + """ + try: + if not hasattr(get, 'conf') or not get.conf: + get.conf = None + if get.repository == "docker.io": + get.repository = "" + + container = self.docker_client(self._url).containers.get(get.id) + container.commit( + repository=get.repository if "repository" in get else None, + tag=get.tag if "tag" in get else None, + message=get.message if "message" in get else None, + author=get.author if "author" in get else None, + # changes=get.changes if get.changes else None, + conf=get.conf + ) + dp.write_log("Submitted container [{}] as image [{}] successfully".format(container.attrs['Name'], get.tag)) + + if hasattr(get, "path") and get.path: + get.id = "{}:{}".format(get.repository, get.tag) + from btdockerModelV2 import imageModel as di + result = di.main().save(get) + if result['status']: + return public.return_message(0, 0, _("The image has been generated, and{}".format(result['msg']))) + return public.return_message(0, 0, result) + + return public.return_message(0, 0, _("submit successfully!")) + except Exception as e: + return public.return_message(-1, 0, _( "Submission Failed!{}".format(str(e)))) + + + def docker_shell(self, get): + """ + 容器执行命令 + :param get: + :return: + """ + # {"id": "b414fd6b5d5a1e36e3246dc8bb4b4518bd0adbcd62180abc0e146ccc4b8731db", "shell": "bash", "sudo_i": 1} + try: + get.validate([ + Param('id').Require().String(), + Param('shell').Require().String('in', ['bash', 'sh']), + # Param('sudo_i').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + try: + # if "id" not in get: + # return public.returnMsg(False, "The container ID is abnormal, please refresh the page and try again!") + + # shell_list = ('bash', 'sh') + # if "shell" not in get: + # return public.returnMsg(False, "Select the shell type!") + + # if get.shell not in shell_list: + # return public.returnMsg(False, "This shell is not supported-choose bash or sh!") + + cmd = 'docker container exec -it {} {}'.format(get.id, get.shell) + return public.return_message(0, 0, cmd) + except docker.errors.APIError as ex: + return public.return_message(-1, 0, _( 'Failed to get container')) + + def export(self, get): + """ + 导出容器为tar 没有导入方法,目前弃用 + :param get: + :return: + """ + from os import path as ospath + from os import makedirs as makedirs + try: + if "tar" in get.name: + file_name = '{}/{}'.format(get.path, get.name) + else: + file_name = '{}/{}.tar'.format(get.path, get.name) + if not ospath.exists(get.path): + makedirs(get.path) + public.writeFile(file_name, '') + f = open(file_name, 'wb') + container = self.docker_client(self._url).containers.get(get.id) + data = container.export() + for i in data: + f.write(i) + f.close() + return public.returnMsg(True, _( "Successfully exported to:{}".format(file_name))) + except: + return public.returnMsg(False, _( 'operation failure:' + str(public.get_error_info()))) + + def del_container(self, get): + """ + 删除指定容器 + @param get: + @return: + """ + # try: + # # 删除站点 + # a = public.M('sites').find() + # b = public.M('domain').find() + # + # # 删除数据库记录 + # c = dp.sql('dk_domain').find() + # d = dp.sql('dk_sites').find() + # except Exception as e: + # public.print_log(traceback.format_exc()) + # + # public.print_log("a| {}".format(a)) + # public.print_log("b| {}".format(b)) + # public.print_log("c| {}".format(c)) + # public.print_log("d| {}".format(d)) + + try: + get.validate([ + Param('id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + import sys + sys.path.insert(0, '/www/server/panel/class') + from btdockerModelV2.proxyModel import main + from panelSite import panelSite + try: + container = self.docker_client(self._url).containers.get(get.id) + config_path = "{}/config/name_map.json".format(public.get_panel_path()) + if not os.path.exists(config_path): + public.writeFile(config_path, json.dumps({})) + if public.readFile(config_path) == '': + public.writeFile(config_path, json.dumps({})) + config_data = json.loads(public.readFile(config_path)) + if container.name in config_data.keys(): + config_data.pop(container.name) + public.writeFile(config_path, json.dumps(config_data)) + container.remove(force=True) + dp.sql("cpu_stats").where("container_id=?", (get.id,)).delete() + dp.sql("io_stats").where("container_id=?", (get.id,)).delete() + dp.sql("mem_stats").where("container_id=?", (get.id,)).delete() + dp.sql("net_stats").where("container_id=?", (get.id,)).delete() + dp.sql("container").where("container_nam=?", (container.attrs['Name'])).delete() + dp.write_log("Delete container [{}] successfully!".format(container.attrs['Name'])) + get.container_id = get.id + # todo 此处导入注意适配 info返回已改 + info = main().get_proxy_info(get) + if info and 'name' in info and 'id' in info: + print(info['name']) + print(info['id']) + args = public.to_dict_obj({ + 'id': info['id'], + 'webname': info['name'] + + }) + panelSite().DeleteSite(args) + # domain_id = dp.sql('dk_domain').where('id=?', (info['id'],)).find() + + # 删除站点 改默认数据库 + # dp.sql('sites').where('name=?', (info['name'],)).delete() + # dp.sql('domain').where("name=?", (info['name'],)).delete() + public.M('sites').where('name=?', (info['name'],)).delete() + public.M('domain').where("name=?", (info['name'],)).delete() + + # 删除数据库记录 + dp.sql('dk_domain').where('id=?', (info['id'],)).delete() + dp.sql('dk_sites').where('container_id=?', (get.id,)).delete() + + return public.return_message(0, 0, _("Successfully deleted!")) + except Exception as e: + return public.return_message(-1, 0, _( "Delete failed! {}".format(str(e)))) + + # 设置容器状态 + def set_container_status(self, get): + """ + 设置容器状态 + @param get: + @return: + """ + try: + get.validate([ + Param('id').Require().String(), + Param('status').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + try: + container = self.docker_client(self._url).containers.get(get.id) + result = {"status": True, "msg": "Successfully set!"} + if get.status == "start": + result = self.start(get) + elif get.status == "stop": + result = self.stop(get) + elif get.status == "pause": + result = self.pause(get) + elif get.status == "unpause": + result = self.unpause(get) + elif get.status == "reload": + result = self.reload(get) + elif get.status == "kill": + container.kill() + else: + container.restart() + + try: + + if result['status']: + return public.return_message(0, 0,result['msg']) + else: + return public.return_message(-1, 0, result['msg']) + + + except: + return public.return_message(-1, 0, str(result['msg'])) + except Exception as e: + if "No such container" in str(e): + return public.return_message(-1, 0, _( "The container has been deleted!")) + if "port is already allocated" in str(e) or "address already in use" in str(e): + if "[::]" in str(e): + str_port = str(e).split("[::]:")[1].split(":")[0] + return public.return_message(-1, 0, _( "ipv6 server port [{}] is occupied!".format(str_port))) + else: + str_port = str(e).split("0.0.0.0")[1].split(":")[1].split(" ")[0] + return public.return_message(-1, 0, _( "ipv4 server port [{}] is occupied!".format(str_port))) + return public.return_message(-1, 0, _( "Setup failed! {}".format(str(e)))) + + # 停止容器 + def stop(self, get): + """ + 停止指定容器 (内部调用) + :param get: + :return: + """ + try: + get.status = "stop" + container = self.docker_client(self._url).containers.get(get.id) + container.stop() + time.sleep(1) + data = self.docker_client(self._url).containers.get(get.id) + if data.attrs['State']['Status'] != "exited": + return public.returnMsg(False, _( "Stop failing!")) + dp.write_log("Stopping container [{}] success!".format(data.attrs['Name'].replace('/', ''))) + return public.returnMsg(True, _("Stop succeeding!")) + except docker.errors.APIError as e: + if "is already paused" in str(e): + return public.returnMsg(False, _( "The container has paused.")) + if "No such container" in str(e): + return public.returnMsg(True, _( "Container stopped and deleted")) + return public.returnMsg(False, _( "Stop failing!{}".format(e))) + + def start(self, get): + """ + 启动指定容器 (内部调用) + :param get: + :return: + """ + try: + get.status = "start" + container = self.docker_client(self._url).containers.get(get.id) + container.start() + time.sleep(1) + data = self.docker_client(self._url).containers.get(get.id) + if data.attrs['State']['Status'] != "running": + return public.returnMsg(False, _( "boot failed!")) + dp.write_log("Starting container [{}] was successful!".format(data.attrs['Name'].replace('/', ''))) + return public.returnMsg(True, _( "starting success!")) + except docker.errors.APIError as e: + if "cannot start a paused container, try unpause instead" in str(e): + return self.unpause(get) + except Exception as a: + print(traceback.format_exc()) + raise Exception(a) + + def pause(self, get): + """ + 暂停此容器内的所有进程 (内部调用) + :param get: + :return: + """ + try: + get.status = "pause" + container = self.docker_client(self._url).containers.get(get.id) + container.pause() + time.sleep(1) + data = self.docker_client(self._url).containers.get(get.id) + if data.attrs['State']['Status'] != "paused": + return public.returnMsg(False, _( "Container pause failed!")) + dp.write_log("Pause container [{}] success!".format(data.attrs['Name'].replace('/', ''))) + return public.returnMsg(True, _( "Container pause successfully!")) + except docker.errors.APIError as e: + if "is already paused" in str(e): + return public.returnMsg(False, _( "The container has been suspended!")) + if "is not running" in str(e): + return public.returnMsg(False, _( "The container is not started and cannot be paused!")) + if "is not paused" in str(e): + return public.returnMsg(False, _( + "The container is not paused or has been deleted. Check if the container has the option to delete immediately after stopping!")) + return str(e) + except Exception as a: + print(traceback.format_exc()) + raise Exception(a) + + def unpause(self, get): + """ + 取消暂停该容器内的所有进程 (内部调用) + :param get: + :return: + """ + try: + get.status = "unpause" + container = self.docker_client(self._url).containers.get(get.id) + container.unpause() + time.sleep(1) + data = self.docker_client(self._url).containers.get(get.id) + if data.attrs['State']['Status'] != "running": + return public.returnMsg(False, _( "boot failed!")) + dp.write_log("Unpause container [{}] success!".format(data.attrs['Name'].replace('/', ''))) + return public.returnMsg(True, _( "The container unpaused successfully")) + except docker.errors.APIError as e: + if "is already paused" in str(e): + return public.returnMsg(False, _( "The container has paused.")) + if "is not running" in str(e): + return public.returnMsg(False, _( "The container is not started and cannot be paused!")) + if "is not paused" in str(e): + return public.returnMsg(False, _( + "The container is not paused or has been deleted. Check if the container has the option to delete immediately after stopping!")) + return str(e) + except Exception as a: + print(traceback.format_exc()) + raise Exception(a) + + def reload(self, get): + """ + 再次从服务器加载此对象并使用新数据更新 attrs (内部调用) + :param get: + :return: + """ + get.status = "reload" + container = self.docker_client(self._url).containers.get(get.id) + container.reload() + time.sleep(1) + data = self.docker_client(self._url).containers.get(get.id) + if data.attrs['State']['Status'] != "running": + return public.returnMsg(False, _( "boot failed!")) + dp.write_log("Reloading container [{}] succeeded!".format(data.attrs['Name'].replace('/', ''))) + return public.returnMsg(True, _( "The container was reloaded successfully!")) + + def restart(self, get): + """ + 重新启动这个容器。类似于 docker restart 命令 + :param get: + :return: + """ + try: + get.status = "restart" + container = self.docker_client(self._url).containers.get(get.id) + container.restart() + time.sleep(1) + data = self.docker_client(self._url).containers.get(get.id) + if data.attrs['State']['Status'] != "running": + return public.returnMsg(False, _( "boot failed!")) + dp.write_log("Restart container [{}] successfully!".format(data.attrs['Name'].replace('/', ''))) + return public.returnMsg(True, _( "Container restarts successfully!")) + except docker.errors.APIError as e: + if "container is marked for removal and cannot be started" in str(e): + return public.returnMsg(False, _( + "The container has been stopped and deleted because containers have the option to automatically delete when stopped")) + if "is already paused" in str(e): + return public.returnMsg(False, _( "The container has paused")) + return str(e) + + def get_container_ip(self, container_networks): + """ + 获取容器IP + @param container_networks: + @return: + """ + data = list() + for network in container_networks: + data.append(container_networks[network]['IPAddress']) + return data + + def get_container_path(self, detail): + """ + 获取容器路径 + @param detail: + @return: + """ + try: + import os + if not "GraphDriver" in detail: + return False + if "Data" not in detail["GraphDriver"]: + return False + if "MergedDir" not in detail["GraphDriver"]["Data"]: + return False + path = detail["GraphDriver"]["Data"]["MergedDir"] + if not os.path.exists(path): + return "" + return path + except: + return False + + def get_container_info(self, get): + """ + 获取容器信息 + @param get: + @return: + """ + try: + if "id" not in get or get.id == "": + return public.return_message(-1, 0, _( "The container ID is empty")) + + from btdockerModelV2.dockerSock import container + sk_container = container.dockerContainer() + sk_container_info = sk_container.get_container_inspect(get.id) + + info_path = "/var/lib/docker/containers/{}/container_info.json".format(get.id) + public.writeFile(info_path, json.dumps(sk_container_info, indent=3)) + sk_container_info['container_info'] = info_path + return public.return_message(0, 0, sk_container_info) + except Exception as e: + if "No such container" in str(e): + return public.return_message(-1, 0, _( "Container does not exist!")) + return public.return_message(-1, 0, _( "Failed to get container information!{}".format(str(e)))) + + def struct_container_ports(self, ports): + """ + 构造容器ports + @param ports: + @return: + """ + data = dict() + for port in ports: + key = str(port["PrivatePort"]) + "/" + port["Type"] + if key not in data.keys(): + data[str(port["PrivatePort"]) + "/" + port["Type"]] = [{ + "HostIp": port["IP"], + "HostPort": str(port["PublicPort"]) + }] if "IP" in port else None + else: + data[str(port["PrivatePort"]) + "/" + port["Type"]].append({ + "HostIp": port["IP"], + "HostPort": str(port["PublicPort"]) + }) + return data + + def struct_container_list(self, container): + ''' + @name 构造容器列表 + @author wzz <2024/3/13 下午 5:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + tmp = { + "id": container["Id"], + "name": dp.rename(container['Names'][0].replace("/", "")), + "status": container["State"], + "image": container["Image"], + "created_time": container["Created"], + "ip": self.get_container_ip(container["NetworkSettings"]['Networks']), + "ports": self.struct_container_ports(container["Ports"]), + } + + return tmp + + # 2024/4/11 下午2:46 获取 merged 目录 + def get_container_merged(self, get): + ''' + @name 获取容器 merged 目录 + @author wzz <2024/4/11 下午2:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + get.id = get.get("id", "") + if get.id == "": + return public.return_message(-1, 0, _( "The container ID is empty")) + return public.return_message(0, 0, { + "path": public.ExecShell("docker inspect -f \"{{json .GraphDriver.Data.MergedDir}}\" " + get.id)[ + 0].strip().strip('"')}) + + except Exception as e: + return public.return_message(0, 0, {"path": ""}) + + # 2024/4/11 下午3:44 获取其他容器列表的数据 + def get_other_container_data(self, get): + ''' + @name 获取其他容器列表的数据 + @author wzz <2024/4/11 下午3:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + from btdockerModelV2.dockerSock import container + + sk_container = container.dockerContainer() + container_list = sk_container.get_container() + + data = [] + # 获取前先检测数据库是否存在 + self.check_remark_table() + dk_container_remark = dp.sql("dk_container_remark").select() + self.check_table_dk_backup() + dk_backup = dp.sql("dk_backup").select() + for sk_c in container_list: + try: + remark = [i['remark'] for i in dk_container_remark if i['container_id'] == sk_c["Id"]][0] + except Exception as e: + remark = "" + + + # 计算备份数量 + backup_count = 0 + for i in dk_backup: + if i['container_id'] == sk_c["Id"]: + backup_count += 1 + + data.append({ + "id": sk_c["Id"], + "name": dp.rename(sk_c['Names'][0].replace("/", "")), + "backup_count": backup_count, + "remark": remark, + }) + + return public.return_message(0, 0, data) + except Exception as e: + public.print_log(traceback.format_exc()) + return public.return_message(0, 0, []) + + # 获取容器列表 + def get_list(self, get): + """ + 获取所有容器列表 1 + :param get + :return: + """ + from btdockerModelV2.dockerSock import container + sk_container = container.dockerContainer() + sk_container_list = sk_container.get_container() + + data = { + "online_cpus": dp.get_cpu_count(), + "mem_total": dp.get_mem_info(), + "container_list": [], + } + + container_detail = list() + grouped_by_status = dict() + for sk_c in sk_container_list: + struct_container = self.struct_container_list(sk_c) + status = struct_container['status'] + grouped_by_status.setdefault(status, []).append(struct_container) + container_detail.append(struct_container) + + data['grouped_by_status'] = grouped_by_status + data['container_list'] = sorted(container_detail, key=lambda x: x['created_time'], reverse=True) + return public.return_message(0, 0, data) + + def _get_list(self, get): + """ + 获取所有容器列表 1 + :param get + :return: + """ + from btdockerModelV2.dockerSock import container + sk_container = container.dockerContainer() + sk_container_list = sk_container.get_container() + + data = { + "online_cpus": dp.get_cpu_count(), + "mem_total": dp.get_mem_info(), + "container_list": [], + } + + container_detail = list() + grouped_by_status = dict() + for sk_c in sk_container_list: + struct_container = self.struct_container_list(sk_c) + status = struct_container['status'] + grouped_by_status.setdefault(status, []).append(struct_container) + container_detail.append(struct_container) + + data['grouped_by_status'] = grouped_by_status + data['container_list'] = sorted(container_detail, key=lambda x: x['created_time'], reverse=True) + return data + + # 获取容器的attr + def get_container_attr(self, containers): + c_list = containers.list(all=True) + return [container_info.attrs for container_info in c_list] + + # 获取容器日志 + def get_logs(self, get): + """ + 获取指定容器日志 + :param get: + :return: + """ + # {"id": "4b270b6b6dd3cb50b4bc5c5c51ceade67de0754d39ad7a312d06e3fabf4d89b1","time_search": [1701273600, 1701829064]} + + res = { + "logs": "", + 'split_status': False, + 'split_type': 'day', + 'split_size': 1000, + 'split_hour': 2, + 'split_minute': 0, + 'save': '180' + } + + try: + container = self.docker_client(self._url).containers.get(get.id) + if hasattr(get, 'time_search') and get.time_search != '': + if not os.path.exists(container.attrs['LogPath']): + return public.return_message(-1, 0, "No container logging") + # return public.return_message(0, 0, None) + + time_search = json.loads(str(get.time_search)) + since = int(time_search[0]) + until = int(time_search[1]) + r_logs = container.logs(since=since, until=until).decode() + + else: + if not os.path.exists(container.attrs['LogPath']): + return public.return_message(-1, 0, "No container logging") + # return public.return_message(0, 0, None) + + size = os.stat(container.attrs['LogPath']).st_size + if size < 1048576: + r_logs = container.logs().decode() + else: + tail = int(get.tail) if "tail" in get else 3000 + r_logs = container.logs(tail=tail).decode() + + if hasattr(get, 'search') and get.search != '': + if get.search: + r_logs = r_logs.split("\n") + r_logs = [i for i in r_logs if get.search in i] + r_logs = "\n".join(r_logs) + + res['logs'] = r_logs + res['id'] = get.id + res['name'] = dp.rename(container.attrs['Name'][1:]) + res['logs_path'] = container.attrs['LogPath'] + res['size'] = os.stat(container.attrs['LogPath']).st_size + + if public.M('sqlite_master').db('docker_log_split').where('type=? AND name=?', + ('table', 'docker_log_split')).count(): + res['split_status'] = True if public.M('docker_log_split').where('pid=?', (get.id,)).count() else False + data = public.M('docker_log_split').where('pid=?', (get.id,)).select() + if data: + res['split_type'] = data[0]['split_type'] + res['split_size'] = data[0]['split_size'] + res['split_hour'] = data[0]['split_hour'] + res['split_minute'] = data[0]['split_minute'] + res['save'] = data[0]['save'] + else: + res['split_type'] = 'day' + res['split_size'] = 1000 + res['split_hour'] = 2 + res['split_minute'] = 0 + res['save'] = '180' + + return public.return_message(0, 0, res) + + except Exception: + return public.return_message(0, 0, res) + + def get_logs_all(self, get): + """ + 获取所有容器的日志 + @param get: + @return: + """ + try: + client = self.docker_client(self._url) + if not client: + return public.return_message(-1, 0, _( 'docker connection failed')) + containers = client.containers + clist = [i.attrs for i in containers.list(all=True)] + clist = [{'id': i['Id'], 'name': dp.rename(i['Name'][1:]), 'log_path': i['LogPath']} for i in clist] + for i in clist: + if os.path.exists(i['log_path']): + i['size'] = os.stat(i['log_path']).st_size + else: + i['size'] = 0 + return public.return_message(0, 0, clist) + except Exception as e: + return public.return_message(-1, 0, e) + + def docker_split(self, get): + """ + 设置容器日志切割 + @param get: + @return: + """ + # {"pid": "3f225d2b41bcadb1b4bb73dc7521a35d1933cb1e1caa8ac61bd53cf718b9910c", "type": "del"} + try: + get.validate([ + Param('pid').Require().String(), + Param('type').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + try: + client = self.docker_client(self._url) + if not client: + return public.return_message(-1, 0, _( 'docker connection failed')) + containers = client.containers + clist = [i.attrs for i in containers.list(all=True)] + name = [dp.rename(i['Name'][1:]) for i in clist if i['Id'] == get.pid] + if name: + name = name[0] + else: + name = '' + if not hasattr(get, 'type'): + return public.return_message(-1, 0, _( 'parameter error,Pass: type')) + if not public.M('sqlite_master').db('docker_log_split').where('type=? AND name=?', + ('table', 'docker_log_split')).count(): + public.M('docker_log_split').execute('''CREATE TABLE IF NOT EXISTS docker_log_split ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name text default '', + pid text default '', + log_path text default '', + split_type text default '', + split_size INTEGER default 0, + split_hour INTEGER default 2, + split_minute INTEGER default 0, + save INTEGER default 180)''', ()) + if get.type == 'add': + if "log_path" not in get or not get.log_path: + return public.return_message(-1, 0, _( + 'Container log directory does not exist, log cut cannot be set!')) + + if not (hasattr(get, 'pid') and hasattr(get, 'log_path') and + hasattr(get, 'split_type') and hasattr(get, 'split_size') and + hasattr(get, 'split_minute') and + hasattr(get, 'split_hour') and hasattr(get, 'save')): + return public.return_message(-1, 0, _( 'parameter error')) + data = { + 'name': name, + 'pid': get.pid, + 'log_path': get.log_path, + 'split_type': get.split_type, + 'split_size': get.split_size, + 'split_hour': get.split_hour, + 'split_minute': get.split_minute, + 'save': get.save + } + if public.M('docker_log_split').where('pid=?', (get.pid,)).count(): + id = public.M('docker_log_split').where('pid=?', (get.pid,)).select() + public.M('docker_log_split').delete(id[0]['id']) + public.M('docker_log_split').insert(data) + return public.return_message(0, 0, _("Opened successfully!")) + elif get.type == 'del': + id = public.M('docker_log_split').where('pid=?', (get.pid,)).getField('id') + public.M('docker_log_split').where('id=?', (id,)).delete() + return public.return_message(0, 0, _("Closed successfully!")) + except: + return public.return_message(-1, 0, traceback.format_exc()) + + def clear_log(self, get): + """ + 清空日志 + @param get: + @return: + """ + if not hasattr(get, 'log_path'): + return public.return_message(-1, 0, _( 'parameter error')) + if not os.path.exists(get.log_path): + return public.return_message(-1, 0, _( 'The log file does not exist')) + public.writeFile(get.log_path, '') + return public.return_message(0, 0, _("Log cleaning was successful!")) + + # 清理无用已停止未使用的容器 + def prune(self, get): + """ + :param get: + :return: + """ + try: + type = get.get("type/d", 0) + if type == 0: + res = self.docker_client(self._url).containers.prune() + if not res['ContainersDeleted']: + return public.return_message(-1, 0, _( "No useless containers!")) + dp.write_log("Delete useless containers successfully!") + return public.return_message(0, 0, _( "successfully deleted!")) + else: + import docker + client = docker.from_env() + containers = client.containers.list(all=True) + for container in containers: + container.remove(force=True) + dp.write_log("Delete useless containers successfully!") + return public.return_message(0, 0, _( "successfully deleted!")) + except Exception as e: + if "operation not permitted" in str(e): + return public.return_message(-1, 0, _( "Please turn off enterprise tamper protection before trying again!")) + return public.return_message(-1, 0, _( "failed to delete! {}".format(e))) + + + def update_restart_policy(self, get): + """ + 更新容器重启策略 + @param get: + @return: + """ + try: + get.validate([ + Param('id').Require().String(), + Param('restart_policy').Require(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + try: + # if "restart_policy" not in get: + # return public.returnMsg(False, "Parameter error, please pass in restart policy restart_policy!") + + container = self.docker_client(self._url).containers.get(get.id) + # 修复偶尔 go反序列化 报错 + if isinstance(get.restart_policy, str): + import json + restart_policy = get.restart_policy.replace("'", "\"") + restart_policy = json.loads(restart_policy) + else: + restart_policy = get.restart_policy + + container.update(restart_policy=restart_policy) + # container.update(restart_policy=get.restart_policy) + # container.update(restart_policy= json.dumps(get.restart_policy)) + dp.write_log("Update container [{}] Restart policy successful!".format(container.attrs['Name'])) + return public.return_message(0, 0, _("Update successfully!")) + # except docker.errors.APIError as e: + except Exception as e: + public.print_log(traceback.format_exc()) + return public.return_message(-1, 0, _( "Update failed! {}".format(e))) + + ''' + @name 重命名指定容器 + @author wzz <2023/12/1 下午 3:13> + @param 参数名<数据类型> 参数描述 + @return 数据类型 + ''' + + def rename_container(self, get): + """ + 重命名指定容器 + @param get: + @return: + """ + try: + get.validate([ + Param('id').Require().String(), + Param('name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + try: + # 2023/12/6 上午 10:54 容器未启动时,不允许重命名 + container = self.docker_client(self._url).containers.get(get.id) + if container.attrs['State']['Status'] != "running": + return public.return_message(-1, 0, _( "The container is not started and cannot be renamed!")) + config_path = "{}/config/name_map.json".format(public.get_panel_path()) + if not os.path.exists(config_path): + public.writeFile(config_path, json.dumps({})) + + if public.readFile(config_path) == '': + public.writeFile(config_path, json.dumps({})) + + name_map = json.loads(public.readFile(config_path)) + name_str = 'q18q' + public.GetRandomString(10).lower() + name_map[name_str] = get.name + get.name = name_str + public.writeFile(config_path, json.dumps(name_map)) + + container.rename(get.name) + dp.write_log("Renaming container [{}] succeeded!".format(get.name)) + return public.return_message(0, 0, _("Rename successfully!")) + except docker.errors.APIError as e: + return public.return_message(-1, 0, _( "Renaming failed! {}".format(e))) + + # 2024/2/23 上午 9:58 设置容器列表置顶 + def set_container_to_top(self, get): + """ + 设置容器列表置顶 + @param get: + @return: + """ + # {"type": "add", "container_name": "dk_wordpress-wordpress-1"} + + set_type = get.type if "type" in get else "" + container_name = get.container_name if "container_name" in get else None + + if set_type not in ['add', 'del']: return public.returnMsg(False, _( 'The type only supports add/del')) + if container_name is None: return public.returnMsg(False, _( 'Please select a container')) + + _conf_path = "{}/class_v2/btdockerModelV2/config/container_top.json".format(public.get_panel_path()) + if os.path.exists(_conf_path): + container_top_conf = json.loads(public.readFile(_conf_path)) + else: + container_top_conf = [] + + if set_type == "add": + container_top_conf = [i for i in container_top_conf if i != container_name] + container_top_conf.insert(0, container_name) + public.writeFile(_conf_path, json.dumps(container_top_conf)) + return public.return_message(0, 0, _('Set top successfully!')) + elif set_type == "del": + container_top_conf.remove(container_name) + public.writeFile(_conf_path, json.dumps(container_top_conf)) + return public.return_message(0, 0, _('Unpinned successfully!')) + + # 2024/2/23 上午 9:58 获取容器列表置顶 + def _get_container_to_top(self): + """ + 获取容器列表置顶 + @return: + """ + _conf_path = "{}/class_v2/btdockerModelV2/config/container_top.json".format(public.get_panel_path()) + if os.path.exists(_conf_path): + container_top_conf = json.loads(public.readFile(_conf_path)) + else: + container_top_conf = [] + + return container_top_conf + # 2024/3/13 下午 5:46 检查并创建备注的表 + def check_remark_table(self): + ''' + @name 检查并创建备注的表 + @author wzz <2024/2/26 下午 5:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if not dp.sql('sqlite_master').where('type=? AND name=?', + ('table', 'dk_container_remark')).count(): + dp.sql('dk_container_remark').execute( + "CREATE TABLE `dk_container_remark` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `container_id` TEXT, `container_name` TEXT, `remark` TEXT, `addtime` TEXT)", + () + ) + + + + + # 2024/2/26 下午 6:11 修改备注 + def set_container_remark(self, get): + """ + 设置容器备注 + @param get: + @return: + """ + + try: + get.validate([ + Param('id').Require().String(), + Param('remark').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + # if not hasattr(get, 'remark'): + # return public.returnMsg(False, 'Parameter error, Please pass in: remark!') + # if not hasattr(get, 'id'): + # return public.returnMsg(False, 'Parameter error, Please pass in: id!') + + container_id = get.id + container_remark = public.xssencode2(get.remark) + + if not dp.sql("dk_container_remark").where("container_id=?", (container_id,)).count(): + dp.sql("dk_container_remark").insert({ + "container_id": container_id, + "remark": container_remark, + }) + else: + dp.sql("dk_container_remark").where("container_id=?", (container_id,)).setField("remark", container_remark) + + return public.return_message(0, 0, _("Setup successful!")) + + # 2024/2/27 上午 11:03 通过cgroup获取所有容器的cpu和内存使用情况 + def get_all_stats(self, get): + """ + # 通过cgroup获取所有容器的cpu和内存使用情况 + @param get: + @return: + """ + if not hasattr(get, 'ws_callback'): + return public.returnMsg(False, _( 'Parameter error, Please pass in: ws_callback!')) + if not hasattr(get, '_ws'): + return public.returnMsg(False, _( 'Parameter error, Please pass in: _ws!')) + + from system import system + syst = system() + data = dict() + data["cpu_info"] = syst.GetCpuInfo() + data["mem_info"] = syst.GetMemInfo() + + get._ws.send(public.getJson( + { + "data": data, + "ws_callback": get.ws_callback, + "msg": "Start getting the cpu and memory usage of all containers!", + "status": True, + "end": False, + })) + + try: + # 获取所有容器列表 + container_list = self._get_list(get)["container_list"] + + while True: + docker_stats_result = public.ExecShell( + "docker stats --no-stream --format " + "'{{.ID}},{{.Name}},{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}},{{.NetIO}},{{.BlockIO}},{{.PIDs}};'" + )[0] + + data["cpu_info"] = syst.GetCpuInfo() + data["mem_info"] = syst.GetMemInfo() + + if not docker_stats_result: + get._ws.send(public.getJson( + { + "data": {}, + "ws_callback": get.ws_callback, + "msg": "No running container resource information has been obtained yet!", + "status": True, + "end": True, + })) + return + + container_stats_data = list() + + for i in docker_stats_result.split(";"): + if not i: continue + tmp = i.strip().split(",") + if len(tmp) == 0: continue + if len(tmp) == 1 and not tmp[0]: continue + + # 获取容器使用的image + id = image = create_time = "" + + for cl in container_list: + if cl["name"] in dp.rename(tmp[1]): + id = cl["id"] + image = cl["image"] + create_time = cl["created_time"] + break + + container_stats_data.append({ + "id": id, + "name": dp.rename(tmp[1]), + "image": image, + "create_time": create_time, + "cpu_usage": tmp[2].strip("%"), + "mem_percent": tmp[4].strip("%"), + "mem_usage": { + "mem_usage": dp.byte_conversion(tmp[3].split("/")[0]), + "mem_limit": dp.byte_conversion(tmp[3].split("/")[1]), + }, + "net_io": { + "net_in": dp.byte_conversion(tmp[5].split("/")[0]), + "net_out": dp.byte_conversion(tmp[5].split("/")[1]), + }, + "block_io": { + "block_in": dp.byte_conversion(tmp[6].split("/")[0]), + "block_out": dp.byte_conversion(tmp[6].split("/")[1]), + }, + "pids": tmp[7], + }) + + data["container_stats_data"] = container_stats_data + data["container_count"] = len(container_stats_data) + end = False + msg = "Obtaining the cpu and memory usage of all containers was successful!" + if len(container_stats_data) == 0: + end = True + msg = "No running container resource information has been obtained yet!" + + get._ws.send(public.getJson( + { + "data": data, + "ws_callback": get.ws_callback, + "msg": msg, + "status": True, + "end": end, + })) + + time.sleep(0.1) + + except: + print(traceback.format_exc()) + get._ws.send(public.getJson( + { + "data": {}, + "ws_callback": get.ws_callback, + "msg": "Failed to get cpu and memory usage of all containers!", + "status": True, + "end": True, + })) + return + + def check_table_dk_backup(self): + ''' + @name 检查并创建表 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if not dp.sql('sqlite_master').where('type=? AND name=?', ('table', 'dk_backup')).count(): + dp.sql('dk_backup').execute( + "CREATE TABLE `dk_backup` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` INTEGER, `name` TEXT, `container_id` TEXT, `container_name` TEXT, `filename` TEXT, `size` INTEGER, `addtime` TEXT, `ps` STRING DEFAULT 'none', `cron_id` INTEGER DEFAULT 0 )", + () + ) diff --git a/class_v2/btdockerModelV2/dk_public.py b/class_v2/btdockerModelV2/dk_public.py new file mode 100644 index 00000000..73748bca --- /dev/null +++ b/class_v2/btdockerModelV2/dk_public.py @@ -0,0 +1,293 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +from datetime import datetime, timezone, timedelta +import json +import os + +import db +# from class_v2.dk_db import db +import public +from public.validate import Param + +# db_path = "/www/server/panel/data/db/docker.db" +db_path = "/www/server/panel/data/docker.db" + + +def check_db(): + if not os.path.exists(db_path) or os.path.getsize(db_path) == 0: + execstr = "wget -O {} {}/install/src/docker_en.db".format(db_path, public.get_url()) + public.ExecShell(execstr) + + +def sql(table): + check_db() + with db.Sql() as sql: + sql.dbfile(db_path) + return sql.table(table) + + +# 实例化docker +def docker_client(url="unix:///var/run/docker.sock"): + """ + 目前仅支持本地服务器 + :param url: unix:///var/run/docker.sock + :return: + """ + try: + import docker + except: + public.ExecShell("btpip install docker") + import docker + + try: + client = docker.DockerClient(base_url=url) + if client: + return client + return False + except: + return False + + +def docker_client_low(url="unix:///var/run/docker.sock"): + """ + docker 低级接口 + :param url: + :return: + """ + try: + import docker + except: + public.ExecShell("btpip install docker") + import docker + + try: + client = docker.APIClient(base_url=url) + return client + except docker.errors.DockerException: + return False + + +# 取CPU类型 +def get_cpu_count(): + import re + with open('/proc/cpuinfo', 'r') as f: + cpuinfo = f.read() + rep = r"processor\s*:" + tmp = re.findall(rep, cpuinfo) + if not tmp: + return 0 + return len(tmp) + + +def set_kv(kv_str): + """ + 将键值字符串转为对象 + :param data: + :return: + """ + if not kv_str: + return None + res = kv_str.split('\n') + data = dict() + for i in res: + i = i.strip() + if not i: + continue + if i.find('=') == -1: + continue + if i.find('=') > 1: + k, v = i.split('=', 1) + + data[k] = v + continue + + k, v = i.split('=') + data[k] = v + return data + + +def get_mem_info(): + # 取内存信息 + import psutil + mem = psutil.virtual_memory() + memInfo = int(mem.total) + return memInfo + + +def byte_conversion(data): + data = data.lower() # 将数据转换为小写字母形式 + if "gib" in data: + return float(data.replace('gib', '')) * 1024 * 1024 * 1024 + elif "mib" in data: + return float(data.replace('mib', '')) * 1024 * 1024 + elif "kib" in data: + return float(data.replace('kib', '')) * 1024 + elif "gb" in data: + return float(data.replace('gb', '')) * 1024 * 1024 * 1024 + elif "mb" in data: + return float(data.replace('mb', '')) * 1024 * 1024 + elif "kb" in data: + return float(data.replace('kb', '')) * 1024 + elif "b" in data: + return float(data.replace('b', '')) + else: + return False + + +def bytes_to_human_readable(bytes_num): + """ + 将字节数转换为人类可读的格式(KB、MB、GB等) + :param bytes_num: 字节数 + :return: 格式化后的字符串 xxx mb + """ + suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + index = 0 + while bytes_num >= 1024 and index < len(suffixes) - 1: + bytes_num /= 1024.0 + index += 1 + return "{:.2f} {}".format(bytes_num, suffixes[index]) + + +def log_docker(generator, task_name): + __log_path = '/tmp/dockertmp.log' + while True: + try: + output = generator.__next__() + try: + output = json.loads(output) + if 'status' in output: + output_str = "{}\n".format(output['status']) + public.writeFile(__log_path, output_str, 'a+') + except: + public.writeFile(__log_path, public.get_error_info(), 'a+') + if 'stream' in output: + output_str = output['stream'] + public.writeFile(__log_path, output_str, 'a+') + except StopIteration: + public.writeFile(__log_path, f'{task_name} complete.', 'a+') + break + except ValueError: + public.writeFile(__log_path, f'Error parsing output from {task_name}: {output}', 'a+') + except Exception as e: + public.writeFile(__log_path, f'Error from {task_name}: {e}', 'a+') + break + + +def docker_conf(): + """ + 解析docker配置文件 + KEY=VAULE + KEY1=VALUE1 + :return: + """ + docker_conf = public.readFile("{}/data/docker.conf".format(public.get_panel_path())) + if not docker_conf: + return {"SAVE": 30} + data = dict() + for i in docker_conf.split("\n"): + if not i: + continue + k, v = i.split("=") + if k == "SAVE": + v = int(v) + data[k] = v + return data + + +def get_process_id(pname, cmd_line): + import psutil + pids = psutil.pids() + for pid in pids: + try: + p = psutil.Process(pid) + if p.name() == pname and cmd_line in p.cmdline(): + return pid + except: + pass + return False + + +def write_log(log_data): + public.WriteLog("Docker module", log_data) + + +def check_socket(port): + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + location = ("127.0.0.1", int(port)) + result_of_check = s.connect_ex(location) + s.close() + if result_of_check == 0: + return True + else: + return False + + +def download_file(url, filename): + ''' + 下载方法 + @param url: + @param filename: + @return: + ''' + return public.ExecShell(f"wget -O {filename} {url} --no-check-certificate") + +def convert_timezone_str_to_iso8601(timestamp_str): + # 解析时间字符串为 datetime 对象 + dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S %z %Z') + + # 转换时区为 UTC + dt_utc = dt.astimezone(timezone.utc) + + # 格式化为 ISO 8601 格式 + iso8601_str = dt_utc.strftime('%Y-%m-%dT%H:%M:%S.%fZ') + + return iso8601_str + +def timestamp_to_string(timestamp): + # 将时间戳转换为 datetime 对象 + dt_object = datetime.fromtimestamp(timestamp) + # 格式化为字符串 + formatted_string = dt_object.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + return formatted_string + +def rename(name: str): + """ + 重命名容器名,兼容中文命名 + @param name: + @return: + """ + try: + if name[:4] != 'q18q': + return name + config_path = "{}/config/name_map.json".format(public.get_panel_path()) + config_data = json.loads(public.readFile(config_path)) + name_l = name.split('_') + if name_l[0] in config_data.keys(): + name_l[0] = config_data[name_l[0]] + return '_'.join(name_l) + except: + return name + +def convert_timezone_str_to_timestamp(timestamp_str: str): + import re + # 解析时间字符串为 2024-05-16T06:18:23.915547557-04:00 时间戳 + timestamp_str = re.sub(r'\.\d+', '', timestamp_str) + dt = datetime.strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S%z") + # 转换时区为 UTC + dt_utc = dt.astimezone(timezone.utc) + + # 转换为时间戳 + timestamp = dt_utc.timestamp() + + return timestamp diff --git a/class_v2/btdockerModelV2/dkgroupModel_.py b/class_v2/btdockerModelV2/dkgroupModel_.py new file mode 100644 index 00000000..a3c4b908 --- /dev/null +++ b/class_v2/btdockerModelV2/dkgroupModel_.py @@ -0,0 +1,459 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: zhengweibiao +# ------------------------------------------------------------------- + +# ------------------------------ +# 容器项目编排 +# ------------------------------ +import os, sys, re, json, shutil, psutil, time +import datetime +import public +from btdockerModelV2.containerModel import main as docker +import subprocess + + + + + +class main(): + + next_id = 1 # 将 next_id 设为类变量,而不是实例变量 + def __init__(self): + # self.next_id = 1 + pass + + def load_project_data(self): + json_file = "/www/server/panel/class_v2/btdockerModelV2/docker_project_groups.json" + try: + with open(json_file, 'r') as f: + return json.load(f) + except FileNotFoundError: + data = [] + with open(json_file, 'w') as f: + json.dump(data, f) + + return data + except Exception as e: + return None + + def write_to_json(self, data): + json_file = "/www/server/panel/class_v2/btdockerModelV2/docker_project_groups.json" + try: + with open(json_file, 'w') as f: + json.dump(data, f) + return True + except Exception as e: + print("写入失败!{}".format(e)) + return False + + def get_project_groups(self, get): + + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + + # 获取项目列表 + project_list = docker().get_list(get) + + # 更新每个项目组的状态 + for group in data: + # 检查项目排序是否为空 + if not group['order']: + group['status'] = 0 # 如果项目排序为空,状态为停止 + continue + + all_running = True # 假设所有的项目都在运行 + for project in group['projects']: + for p in project_list['container_list']: + if p['name'] == project['project_name']: + if p['status']!="running": # 如果项目没有运行 + all_running = False + break + if not all_running: + break + + if all_running: + group['status'] = 1 # 如果所有的项目都在运行,状态为启动 + else: + group['status'] = 0 + + return public.returnMsg(True, data) + + def get_group_data(self, get): + try: + data = self.load_project_data() + if data is None: + return None, public.returnMsg(False, "获取配置文件失败!") + + project_list = docker().get_list(get) + for group in data: + if group['id'] == int(get.id): + for project in group['projects']: + + print(project) + # print(project_list['container_list']) + for p in project_list['container_list']: + if p['name'] == project['project_name']: + print(3333) + print(p) + project['status'] = p['status'] + return group, None + return None, public.ReturnMsg(False, "项目不存在!") + + except Exception as e: + return None, public.returnMsg(False, "获取失败!" + str(e)) + + def get_project_details(self, get): + print(33333) + group, error_msg = self.get_group_data(get) + if error_msg: + return error_msg + + group['projects'].sort( + key=lambda x: group['order'].index(x['project_name'])) + return public.returnMsg(True, group['projects']) + + def get_project_group_details(self, get): + group, error_msg = self.get_group_data(get) + if error_msg: + return error_msg + return public.returnMsg(True, group) + + def add_project_group(self, get): + + try: + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + + # 检查是否已存在 + for group in data: + if group['group_name'] == get.group_name: + return public.returnMsg(False, "项目已存在!") + + # 添加新的项目 + new_group = { + "id": self.next_id, # 使用 next_id 作为新的 id + "group_name": get.group_name, + # "status": 0, + "interval": 30, + "projects": [], + "order": [], + } + data.append(new_group) + + # 更新 next_id + + main.next_id += 1 + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + + return public.returnMsg(True, "项目添加成功!") + + except Exception as e: + + return public.returnMsg(False, "添加失败!" + str(e)) + + def edit_project_order(self, get): + + try: + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + # 找到指定的项目组 + for group in data: + if group['id']==int(get.id): + new_order=get.order.split(',') + if sorted(new_order) != sorted(group['order']): + return public.returnMsg(False, "无效的容器顺序!") + group['order']=new_order + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + return public.returnMsg(True,"容器顺序修改成功!") + return public.returnMsg(False,"项目不存在!") + except Exception as e: + return public.returnMsg(False,"修改失败:"+str(e)) + + def edit_group_interval(self, get): + try: + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + + + # 找到指定的项目 + for group in data: + if group['id'] == int(get.id): + # 检查新的项目顺序是否有效 + group['interval'] = get.interval + break + else: + return public.returnMsg(False, "项目不存在!") + + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + + return public.returnMsg(True, "项目时间间隔修改成功!") + except Exception as e: + return public.returnMsg(False, "修改失败!" + str(e)) + + def start_projects_in_order(self, get): + + try: + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + project_list=docker().get_list(get)['container_list'] + + for group in data: + if group['id']==int(get.id): + # 如果pid存在并且进程仍在运行,那么就不允许用户再次启动项目 + if 'start_pid' in group and self.is_process_running(group['start_pid']): + return public.returnMsg(False,"正在依次启动容器中!") + + project_order=group['order'] + + running_projects=[project for project in project_order if self.is_project_running(project,project_list)] + + if running_projects and not get.get("force_stop",False): + return public.returnMsg(False,"以下容器正在运行:{}。是否允许先强制停止再启动?您也可以选择自己手动停止运行中的容器!".format(",".join(running_projects))) + + with open('/dev/null','w') as devnull: + process=subprocess.Popen(['btpython','/www/server/panel/script/set_docker_project_groups.py','--id',str(group['id']),"--action","start"],stdout=devnull,stderr=devnull) + + + + pid=process.pid + group['start_pid']=pid + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + + return public.returnMsg(True, "开始依次启动容器!") + except Exception as e: + return public.returnMsg(False, "启动失败!"+str(e)) + + def is_process_running(self,pid): + try: + os.kill(pid,0) + except OSError: + return False + else: + return True + + + def is_project_running(self,project_name,project_list): + for project in project_list: + if project['name']==project_name and project['status']=="running": + return True + return False + + def stop_projects_in_order(self, get): + try: + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + for group in data: + if group['id']==int(get.id): + # 如果pid存在并且进程仍在运行,那么就不允许用户再次停stop_pid止项目 + if 'stop_pid' in group and self.is_process_running(group['stop_pid']): + return public.returnMsg(False,"正在依次停止容器中!") + with open("/dev/null","w") as devnull: + process=subprocess.Popen(['btpython','/www/server/panel/script/set_docker_project_groups.py','--id',str(group['id']),"--action","stop"],stdout=devnull,stderr=devnull) + + pid=process.pid + group['stop_pid']=pid + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + return public.returnMsg(True,"容器开始按顺序停止") + + except Exception as e: + return public.returnMsg(False, "停止失败!" + str(e)) + + + def start_group(self, args_id): + + try: + get = public.dict_obj() + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + # 找到指定的项目 + for group in data: + if group['id'] == int(args_id): + # 获取项目排序 + project_order = group['order'] + + # 停止所有项目 + for project_name in project_order: + container_id = None + for project in group['projects']: + if project['project_name'] == project_name: + container_id = project['project_id'] + break + if container_id: + # print() + # docker().set_container_status(public.dict_obj({ + # "id": container_id, + # "status": "stop" + # })) + print(33333333333333) + get.status = "stop" + get.id = container_id + docker().set_container_status(get) + # time.sleep(30) + # 依次启动项目 + for project_name in project_order: + container_id = None + for project in group['projects']: + if project['project_name'] == project_name: + container_id = project['project_id'] + break + if container_id: + # print() + # docker().set_container_status(public.dict_obj({ + # "id": container_id, + # "status": "stop" + # })) + get.status = "start" + get.id = container_id + start_result=docker().set_container_status(get) + + if not start_result['status']: + return start_result # 如果启动失败,立即返回错误信息 + + # 暂停指定的时间间隔 + time.sleep(int(group['interval'])) + if not self.is_process_running(group['pid']): + # 删除pid + del group['pid'] + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + except Exception as e: + print("启动失败!" + str(e)) + + + def stop_group(self, args_id): + + + try: + get = public.dict_obj() + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + # 找到指定的项目 + for group in data: + if group['id'] == int(args_id): + # 获取项目排序 + project_order = group['order'] + + # 停止所有项目 + for project_name in project_order: + container_id = None + for project in group['projects']: + if project['project_name'] == project_name: + container_id = project['project_id'] + break + if container_id: + get.status = "stop" + get.id = container_id + docker().set_container_status(get) + except Exception as e: + print("启动失败!" + str(e)) + + def modify_group_status(self, get): + group_ids=[int(id) for id in get.id.split(",")] + + for group_id in group_ids: + get.id=group_id + if get.status=="1": + return self.start_projects_in_order(get) + elif get.status=="0": + return self.stop_projects_in_order(get) + else: + return public.returnMsg(False,"无效的状态!") + + + + def add_project_to_group(self, get): + + try: + + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + # 检查项目是否已被其他项目组添加 + for group in data: + for project in group['projects']: + if project['project_name']==get.project_name: + return public.returnMsg(False,"容器 {} 已经被项目组 {} 添加了!".format(get.project_name,group['group_name'])) + for group in data: + if group['id']==int(get.id): + new_project={ + "project_id":get.project_id, + "project_name":get.project_name, + + + } + group['projects'].append(new_project) + group['order'].append(get.project_name) + break + + + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + + return public.returnMsg(True, "容器添加成功!") + except Exception as e: + return public.returnMsg(False, "添加失败!" + str(e)) + + def remove_project_from_group(self, get): + try: + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + + # 将 get.project_name 分割成一个列表 + project_names=get.project_name.split(",") + + # 找到指定的项目 + for group in data: + if group['id']==int(get.id): + # 删除指定的项目组 + group['projects']=[project for project in group['projects'] if project['project_name'] not in project_names] + # 同时更新order列表,移除已删除的项目名称 + group['order'] = [project_name for project_name in group['order'] if project_name not in project_names] + break + else: + return public.returnMsg(False,"项目不存在!") + + # 将更新后的数据写回文件 + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + + return public.returnMsg(True, "容器删除成功!") + except Exception as e: + return public.returnMsg(False, "删除失败!" + str(e)) + + def delete_project_group(self, get): + + try: + data = self.load_project_data() + if data is None: + return public.returnMsg(False, "获取配置文件失败") + + # 将 get.id 分割成一个列表 + group_ids=[int(id) for id in get.id.split(",")] + # 找到并删除指定的项目 + data=[group for group in data if group['id'] not in group_ids] + # 将更新后的数据写回文件 + if not self.write_to_json(data): + return public.returnMsg(False, "写入失败!") + + return public.returnMsg(True, "项目删除成功!") + except Exception as e: + return public.returnMsg(False, "删除失败!" + str(e)) diff --git a/class_v2/btdockerModelV2/dockerBase.py b/class_v2/btdockerModelV2/dockerBase.py new file mode 100644 index 00000000..11260e67 --- /dev/null +++ b/class_v2/btdockerModelV2/dockerBase.py @@ -0,0 +1,117 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +import public, os +from btdockerModelV2 import dk_public as dp + + +class dockerBase(object): + + def __init__(self): + # self._db_path = "/www/server/panel/data/db/docker.db" + self._db_path = "/www/server/panel/data/docker.db" + self._backup_log = '/tmp/backup.log' + self._log_path = '/tmp/dockertmp.log' + self._rCmd_log = '/tmp/dockerRun.log' + self._url = "unix:///var/run/docker.sock" + self.compose_path = "{}/data/compose".format(public.get_panel_path()) + self.aes_key = "btdockerModel_QWERAS" + self.moinitor_lock = "/tmp/bt_docker_monitor.lock" + + def get_ws_log(self, get): + """ + 获取日志,websocket + @param get: + @return: + """ + if not hasattr(get, "_ws"): + return True + + import time + sum = 0 + + with open(get._log_path, "r") as file: + position = file.tell() + get._ws.send("{}\r\n".format(get.wsLogTitle)) + + while True: + current_position = file.tell() + line = file.readline() + if current_position > position: + file.seek(position) + new_content = file.read(current_position - position) + if "nohup" not in new_content: + for i in new_content.split('\n'): + if i == "": continue + get._ws.send(i.strip("\n") + "\r\n") + + position = current_position + + if "bt_successful" in line: + get._ws.send("bt_successful\r\n") + del get._ws + break + elif "bt_failed" in line: + get._ws.send("bt_failed\r\n") + del get._ws + break + + if sum > 0: + sum = 0 + else: + sum += 1 + + if sum >= 6000: + get._ws.send("\r\nNo response for more than 10 minutes!\r\n") + break + + time.sleep(0.1) + + return True + + # 命令行创建 拉取容器 + def run_cmd(self, get): + """ + 命令行创建运行容器(docker run / docker pull),需要做危险命令校验,存在危险命令则不执行 + @param get: + @return: + """ + import re + if not hasattr(get, 'cmd'): + return public.return_message(-1, 0, _("Please pass in cmd")) + + if "docker run" not in get.cmd and "docker pull" not in get.cmd: + return public.return_message(-1, 0, _('Only docker run or docker pull commands can be executed')) + + danger_cmd = ['rm', 'rmi', 'kill', 'stop', 'pause', 'unpause', 'restart', 'update', 'exec', 'init', + 'shutdown', 'reboot', 'chmod', 'chown', 'dd', 'fdisk', 'killall', 'mkfs', 'mkswap', 'mount', + 'swapoff', 'swapon', 'umount', 'userdel', 'usermod', 'passwd', 'groupadd', 'groupdel', + 'groupmod', 'chpasswd', 'chage', 'usermod', 'useradd', 'userdel', 'pkill'] + + danger_symbol = ['&', '&&', '||', '|', ';'] + + for d in danger_cmd: + if get.cmd.startswith(d) or re.search(r'\s{}\s'.format(d), get.cmd): + return public.return_message(-1, 0, _( 'Dangerous command exists: [{}], execution is not allowed!'.format(d))) + + for d in danger_symbol: + if d in get.cmd: + return public.return_message(-1, 0, _( 'Dangerous symbol exists: [{}], execution is not allowed!'.format(d))) + + os.system("echo -n > {}".format(self._rCmd_log)) + os.system("nohup {} >> {} 2>&1 && echo 'bt_successful' >> {} || echo 'bt_failed' >> {} &".format( + get.cmd, + self._rCmd_log, + self._rCmd_log, + self._rCmd_log, + )) + return public.return_message(0, 0, _("The command has been executed!")) diff --git a/class_v2/btdockerModelV2/dockerSock/__init__.py b/class_v2/btdockerModelV2/dockerSock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/class_v2/btdockerModelV2/dockerSock/container.py b/class_v2/btdockerModelV2/dockerSock/container.py new file mode 100644 index 00000000..bd78f054 --- /dev/null +++ b/class_v2/btdockerModelV2/dockerSock/container.py @@ -0,0 +1,48 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +# docker模型sock 封装库 容器库 +# ------------------------------------------------------------------- +import json + +import public +from btdockerModelV2.dockerSock.sockBase import base + + +class dockerContainer(base): + def __init__(self): + super(dockerContainer, self).__init__() + + # 2024/3/13 上午 11:20 获取所有容器列表 + def get_container(self): + ''' + @name 获取所有容器列表 + @author wzz <2024/3/13 上午 10:54> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return json.loads(public.ExecShell("curl -s --unix-socket {} http:/{}/containers/json?all=1".format(self._sock, self.get_api_version()))[0]) + + except Exception as e: + print(public.get_error_info()) + return [] + + # 2024/3/28 下午 11:37 获取指定容器的inspect + def get_container_inspect(self, container_id: str): + ''' + @name 获取指定容器的inspect + @param container_id: 容器id + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return json.loads(public.ExecShell("curl -s --unix-socket {} http:/{}/containers/{}/json" + .format(self._sock, self.get_api_version(), container_id))[0]) + except Exception as e: + print(public.get_error_info()) + return [] diff --git a/class_v2/btdockerModelV2/dockerSock/image.py b/class_v2/btdockerModelV2/dockerSock/image.py new file mode 100644 index 00000000..8215e872 --- /dev/null +++ b/class_v2/btdockerModelV2/dockerSock/image.py @@ -0,0 +1,80 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +# docker模型sock 封装库 镜像库 +# ------------------------------------------------------------------- +import json + +import public +from btdockerModelV2.dockerSock.sockBase import base + + +class dockerImage(base): + def __init__(self): + super(dockerImage, self).__init__() + + # 2024/3/13 上午 11:20 获取所有镜像列表 + def get_images(self): + ''' + @name 获取所有镜像列表 + @author wzz <2024/3/13 上午 10:54> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return json.loads(public.ExecShell("curl -s --unix-socket {} http:/{}/images/json?all=1" + .format(self._sock, self.get_api_version()))[0]) + except Exception as e: + print(public.get_error_info()) + return [] + + # 2023/12/13 上午 11:08 镜像搜索 + def search(self, name): + ''' + @name 镜像搜索 + @author wzz <2023/12/13 下午 3:41> + @param 参数名<数据类型> 参数描述 + @return 数据类型 + ''' + try: + return json.loads(public.ExecShell("curl -s --unix-socket {} http:/{}/images/search?term={}" + .format(self._sock, self.get_api_version(), name))[0],) + except Exception as e: + # if os.path.exists('data/debug.pl'): + # print(public.get_error_info()) + # public.print_log(public.get_error_info()) + return [] + + # 2024/4/1 下午 2:47 image load + def load_image(self, path): + ''' + @name 加载镜像 + @param path 镜像名称 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return json.loads(public.ExecShell("curl -s --unix-socket {} -X POST http:/{}/images/load -H \"Content-Type: application/x-tar\" --data-binary @{}" + .format(self._sock, self.get_api_version(), path))[0]) + except Exception as e: + print(public.get_error_info()) + return False + + # 2024/4/16 上午11:39 获取指定image的inspect信息 + def inspect(self, image): + ''' + @name 获取指定image的inspect信息 + @param image 镜像名称 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return json.loads(public.ExecShell("curl -s --unix-socket {} http:/{}/images/{}/json" + .format(self._sock, self.get_api_version(), image))[0]) + except Exception as e: + print(public.get_error_info()) + return {} + diff --git a/class_v2/btdockerModelV2/dockerSock/sockBase.py b/class_v2/btdockerModelV2/dockerSock/sockBase.py new file mode 100644 index 00000000..13c73dee --- /dev/null +++ b/class_v2/btdockerModelV2/dockerSock/sockBase.py @@ -0,0 +1,19 @@ +class dockerSock(object): + def __init__(self): + self._sock = "/var/run/docker.sock" + self._url = "unix://{}".format(self._sock) + self._api_version = "/127.0.0.1" + + def get_sock(self): + return self._sock + + def get_url(self): + return self._url + + def get_api_version(self): + return self._api_version + + +class base(dockerSock): + def __init__(self): + super(base, self).__init__() diff --git a/class_v2/btdockerModelV2/dockerSock/volume.py b/class_v2/btdockerModelV2/dockerSock/volume.py new file mode 100644 index 00000000..43f16dac --- /dev/null +++ b/class_v2/btdockerModelV2/dockerSock/volume.py @@ -0,0 +1,34 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +# docker模型sock 封装库 存储库 +# ------------------------------------------------------------------- +import json + +import public +from btdockerModelV2.dockerSock.sockBase import base + + +class dockerVolume(base): + def __init__(self): + super(dockerVolume, self).__init__() + + # 2024/3/13 上午 11:20 获取所有存储卷列表 + def get_volumes(self): + ''' + @name 获取所有存储卷列表 + @author wzz <2024/3/13 上午 10:54> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return json.loads(public.ExecShell("curl -s --unix-socket {} http:/{}/volumes" + .format(self._sock, self.get_api_version()))[0]) + except Exception as e: + print(public.get_error_info()) + return [] diff --git a/class_v2/btdockerModelV2/host.py b/class_v2/btdockerModelV2/host.py new file mode 100644 index 00000000..3f22b457 --- /dev/null +++ b/class_v2/btdockerModelV2/host.py @@ -0,0 +1,60 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: zouhw +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +import public +import dk_public as dp + +class main: + + # 获取docker主机列表 + def get_list(self,args=None): + info = dp.sql("hosts").select() + for i in info: + if dp.docker_client(i['url']): + i['status'] = True + else: + i['status'] = False + return info + + # 添加docker主机 + def add(self,args): + """ + :param url 连接主机的url + :param remark 主机备注 + :return: + """ + import time + host_lists = self.get_list() + for h in host_lists: + if h['url'] == args.url: + return public.returnMsg(False,"The host already exists!") + # 测试连接 + if not dp.docker_client(args.url): + return public.returnMsg(False,"Failed to connect to the server, please check if docker is started!") + pdata = { + "url": args.url, + "remark": public.xsssec(args.remark), + "time": int(time.time()) + } + dp.write_log("Add host [{}] successful!".format(args.url)) + dp.sql('hosts').insert(pdata) + return public.returnMsg(True,"Add docker host successfully!") + + def delete(self,args): + """ + :param id 连接主机id + :return: + """ + data = dp.sql('hosts').where('id=?',args(args.id,)).find() + dp.sql('hosts').delete(id=args.id) + dp.write_log("Delete host [{}] successful!".format(data['url'])) + return public.returnMsg(True,"Delete host successfully!") \ No newline at end of file diff --git a/class_v2/btdockerModelV2/imageModel.py b/class_v2/btdockerModelV2/imageModel.py new file mode 100644 index 00000000..e36d803a --- /dev/null +++ b/class_v2/btdockerModelV2/imageModel.py @@ -0,0 +1,733 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +import os +import json +import traceback + +import docker.errors +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +import gettext +_ = gettext.gettext + +class main(dockerBase): + + def docker_client(self, url): + return dp.docker_client(url) + + # 导出 + def save(self, get): + """ + :param path 要镜像tar要存放的路径 + :param name 包名 + :param id 镜像 + :param + :param get: + :return: + """ + + # 校验参数 + try: + get.validate([ + Param('path').Require().SafePath(), + Param('name').Require().String(), + Param('id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + # if "name" not in get or get.name == "": + # return public.returnMsg(False, "Image name cannot be empty") + # if "path" not in get or get.path == "": + # return public.returnMsg(False, "Mirror path cannot be empty") + # if "id" not in get or get.id == "": + # return public.returnMsg(False, "Image ID cannot be empty") + + if "/" in get.name: + return public.return_message(-1, 0, _("The image name cannot contain /")) + + if "tar" in get.name: + filename = '{}/{}'.format(get.path, get.name) + else: + filename = '{}/{}.tar'.format(get.path, get.name) + + if not os.path.exists(get.path): os.makedirs(get.path) + + public.writeFile(filename, "") + with open(filename, 'wb') as f: + image = self.docker_client(self._url).images.get(get.id) + print(image) + for chunk in image.save(named=True): + f.write(chunk) + dp.write_log("Image [{}] exported to [{}] successfully".format(get.id, filename)) + return public.return_message(0, 0, "Successfully saved to:{}".format(filename)) + + except docker.errors.APIError as e: + if "empty export - not implemented" in str(e): + return public.return_message(-1, 0, "Cannot export image!") + return public.get_error_info() + except Exception as e: + if "Read timed out" in str(e): + return public.return_message(-1, 0, + "Exporting the image failed and the connection to docker timed out. Please try restarting docker and try again!") + return public.return_message(-1, 0, "Failed to export image!
                                    {}".format(e)) + + # 导入 + def load(self, get): + """ + :param path: 需要导入的镜像路径具体到文件名 + :param get: + :return: + """ + + # 校验参数 + try: + get.validate([ + Param('path').Require().SafePath(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + if "path" not in get and get.path == "": + return public.return_message(-1, 0, "Please enter the image path!") + + # 2023/12/20 下午 4:12 判断如果path后缀不为.tar则返回错误 + if not get.path.endswith(".tar"): + return public.return_message(-1, 0, "Failed to import the image. The file extension must be.tar!") + + from btdockerModelV2.dockerSock import image + sk_image = image.dockerImage() + sk_image.load_image(get.path) + + dp.write_log("Image [{}] imported successfully!".format(get.path)) + return public.return_message(0, 0, "Image import was successful!{}".format(get.path)) + except Exception as e: + if "Read timed out" in str(e): + return public.return_message(-1, 0, + "Exporting the image failed and the connection to docker timed out. Please try restarting docker and try again!") + if "no such file or directory" in str(e): + return public.return_message(-1, 0, + "The image import failed and the temporary directory of the container failed to be created. Please check whether the protection software has an interception record!") + return public.return_message(-1, 0, "Failed to import image!
                                    {}".format(e)) + + # 列出所有镜像 + def image_list(self, get): + """ + :param url + :param get: + :return: + """ + try: + from btdockerModelV2.dockerSock import image + sk_image = image.dockerImage() + sk_images_list = sk_image.get_images() + + from btdockerModelV2.dockerSock import container + sk_container = container.dockerContainer() + container_list = sk_container.get_container() + # if not container_list: + # return public.return_message(0, 0, data) + + + data = list() + # public.print_log("data000 : {}".format(data)) + # public.print_log("sk_images_镜像列表 sk_images_list: {}".format(sk_images_list)) + for image in sk_images_list: + # public.print_log("image if111111: {}".format(image)) + # {'Containers': -1, 'Created': 1717026901, + # 'Id': 'sha256:4f67c83422ec747235357c04556616234e66fc3fa39cb4f40b2d4441ddd8f100', + # 'Labels': {'maintainer': 'NGINX Docker Maintainers '}, 'ParentId': '', + # 'RepoDigests': ['nginx@sha256:0f04e4f646a3f14bf31d8bc8d885b6c951fdcf42589d06845f64d18aec6a3c4d'], + # 'RepoTags': ['nginx:latest'], 'SharedSize': -1, 'Size': 187667860} + if image is None: + continue + + if image['RepoTags'] is not None and len(image['RepoTags']) != 0: + # public.print_log("data2 if111111: {}".format(data)) + for tag in image['RepoTags']: + tmp = { + "id": image['Id'], + "tags": tag, + "name": tag, + "digest": image['RepoDigests'][0].split("@")[1] if image['RepoDigests'] else "", + "time": image['Created'] if type(image['Created']) == int else None, + "size": image['Size'], + "created_at": image['Created'], + "used": 0, + "containers": [], + } + # public.print_log("tmp tmp tmp: {}".format(tmp)) + # {'id': 'sha256:4f67c83422ec747235357c04556616234e66fc3fa39cb4f40b2d4441ddd8f100', + # 'tags': 'nginx:latest', 'name': 'nginx:latest', + # 'digest': 'sha256:0f04e4f646a3f14bf31d8bc8d885b6c951fdcf42589d06845f64d18aec6a3c4d', + # 'time': 1717026901, 'size': 187667860, 'created_at': 1717026901, 'used': 0, 'containers': []} + # public.print_log("container_list if: {}".format(container_list)) + self.structure_images_list(container_list, tmp) + # public.print_log("data jhshs哈666666 if: {}".format(data)) + data.append(tmp) + # public.print_log("data2 if: {}".format(data)) + else: + # public.print_log("data2 if: {}".format(data)) + tmp = { + "id": image['Id'], + "tags": "", + "name": "", + "digest": image['RepoDigests'][0].split("@")[1] if image['RepoDigests'] else "", + "time": image['Created'] if type(image['Created']) == int else None, + "size": image['Size'], + "created_at": image['Created'], + "used": 0, + "containers": [], + } + + self.structure_images_list(container_list, tmp) + # public.print_log("data2333 if: {}".format(data)) + data.append(tmp) + # public.print_log("data2 else : {}".format(data)) + + + # public.print_log("data2kjefa : {}".format(type(data))) + return public.return_message(0, 0, data) + except Exception as ex: + import traceback + # public.print_log("尺码个| info: {}".format(ex)) + public.print_log(traceback.format_exc()) + return public.return_message(0, 0, data) + + def structure_images_list(self, container_list, image_info): + ''' + @name + @author wzz <2024/5/22 下午5:53> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + for container in container_list: + if image_info['id'] in container['ImageID']: + image_info['used'] = 1 + image_info['containers'].append({ + "container_id": container['Id'], + "container_name": dp.rename(container['Names'][0].replace("/", "")), + }) + except: + pass + + + def get_image_attr(self, images): + image = images.list() + return [i.attrs for i in image] + + def get_logs(self, get): + # 校验参数 + try: + get.validate([ + Param('logs_file').Require().SafePath(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import files + logs_file = get.logs_file + return public.return_message(0, 0, files.files().GetLastLine(logs_file, 20)) + + # 构建镜像 + def build(self, get): + """ + :param path dockerfile dir + :param pull 如果引用的镜像有更新自动拉取 + :param tag 标签 jose:v1 + :param data 在线编辑配置 + :param get: + :return: + """ + + # # 校验参数 + # try: + # get.validate([ + # Param('path').Require().SafePath(), + # ], [ + # public.validate.trim_filter(), + # ]) + # except Exception as ex: + # public.print_log("error info: {}".format(ex)) + # return public.return_message(-1, 0, str(ex)) + + public.writeFile(self._log_path, "Start building the image!") + if not hasattr(get, "pull"): + get.pull = False + + min_time = None + if hasattr(get, "data") and get.data: + min_time = public.format_date("%Y%m%d%H%M") + get.path = "/tmp/{}/Dockerfile".format(min_time) + os.makedirs("/tmp/{}".format(min_time), exist_ok=True) + public.writeFile(get.path, get.data) + + if not os.path.exists(get.path): + return public.return_message(-1, 0, "Please enter the correct DockerFile path!") + + try: + # 2024/1/18 下午 12:05 取get.path的目录 + get.path = os.path.dirname(get.path) + image_obj, generator = self.docker_client(self._url).images.build( + path=get.path, + pull=True if get.pull == "1" else False, + tag=get.tag, + forcerm=True + ) + + if min_time is not None: + public.ExecShell("rm -rf {}".format(get.path)) + + dp.log_docker(generator, "Docker Build tasks!") + dp.write_log("Build image [{}] successful!".format(get.tag)) + return public.return_message(0, 0, "Build image successfully!") + except docker.errors.BuildError as e: + if "TLS handshake timeout" in str(e): + return public.return_message(-1, 0, "Build failed, connection timed out") + return public.return_message(-1, 0, "Build failed! {}".format(e)) + except docker.errors.APIError as e: + if "Cannot locate specified Dockerfile" in str(e): + return public.return_message(-1, 0, "Build failed!The specified Dockerfile was not found") + return public.return_message(-1, 0, "Build failed!{}".format(e)) + except Exception as e: + return public.return_message(-1, 0, "Build failed!{}".format(e)) + + # 删除镜像 + def remove(self, get): + """ + :param url + :param id 镜像id + :param name 镜像tag + :force 0/1 强制删除镜像 + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('force').Require().Integer(), + Param('name').Require().String(), + Param('id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + from btdockerModelV2.dockerSock import image + sk_image = image.dockerImage() + image_inspect = sk_image.inspect(get.name) + if not image_inspect: + self.docker_client(self._url).images.remove(get.id) + else: + self.docker_client(self._url).images.remove(get.name) + + dp.write_log("Deletion of image【{}】successful!".format(get.name)) + return public.return_message(0, 0, "Mirror deleted successfully!") + + except docker.errors.ImageNotFound as e: + return public.return_message(-1, 0, "The delete failed and the image may not exist!") + + except docker.errors.APIError as e: + if "image is referenced in multiple repositories" in str(e): + return public.return_message(-1, 0, + "The image ID is used in more than one image, force the image to be deleted!") + if ("using its referenced image" in str(e) or + "image is being used by stopped container" in str(e) or + "image is being used by running container" in str(e)): + return public.return_message(-1, 0, + "The image is in use. Please delete the container before deleting the image!") + + return public.return_message(-1, 0, "Failed to delete image!
                                    {}".format(e)) + except Exception as e: + if "Read timed out" in str(e): + return public.return_message(-1, 0, + "Failed to delete image,The connection to docker timed out, please restart and try again!") + return public.return_message(-1, 0, "Failed to delete image!
                                    {}".format(e)) + + # 拉取指定仓库镜像 + def pull_from_some_registry(self, get): + """ + :param name 仓库名11 + :param url + :param image + :param get: + :return: + """ + if not hasattr(get, "_ws"): + return True + + from btdockerModelV2 import registryModel as dr + + try: + if get.name == "Docker public repository": + login = dr.main().login(self._url, "docker.io", None, None)['status'] + if not login: + get._ws.send( + "bt_failed, Login to the repository [docker.io] failed, please try to log in to this repository again!\r\n") + return login + + r_info = { + "url": "docker.io", + "username": None, + "password": None, + "namespace": "library" + } + else: + r_info = dr.main().registry_info(get.name) + r_info['username'] = public.aes_decrypt(r_info['username'], self.aes_key) + r_info['password'] = public.aes_decrypt(r_info['password'], self.aes_key) + login = dr.main().login(self._url, r_info['url'], r_info['username'], r_info['password'])['status'] + if not login: + get._ws.send("bt_failed, {}\r\n".format(login['msg'])) + return login + except Exception as e: + get._ws.send( + "bt_failed, Login to repository [{}] failed, please try to log in to this repository again!\r\n".format( + get.name)) + return public.returnMsg(False, + "bt_failed, Login to repository [{}] failed, please try to log in to this repository again!".format( + get.name)) + + get.username = r_info['username'] + get.password = r_info['password'] + get.registry = r_info['url'] + get.namespace = r_info['namespace'] + + # public.print_log('准备拉取镜像 123--') + + return self.pull(get) + + # 推送镜像到指定仓库 + def push(self, get): + """ + :param id 镜像ID + :param url 连接docker的url + :param tag 标签 镜像名+版本号v1 + :param name 仓库名 + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('tag').Require().String(), + Param('name').Require().String(), + Param('id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if "/" in get.tag: + return public.return_message(-1, 0, "The pushed image cannot contain [/], please use the following " + "format: image:v1 (image name: version)") + if ":" not in get.tag: + get.tag = "{}:latest".format(get.tag) + + public.writeFile(self._log_path, "Start pushing the image!\n") + + from btdockerModelV2 import registryModel as dr + r_info = dr.main().registry_info(get.name) + r_info['username'] = public.aes_decrypt(r_info['username'], self.aes_key) + r_info['password'] = public.aes_decrypt(r_info['password'], self.aes_key) + + if get.name == "docker official" and r_info['url'] == "docker.io": + public.writeFile(self._log_path, "The image cannot be pushed to the Docker public repository!\n") + return public.return_message(-1, 0, "Unable to push to Docker public repository!") + + try: + login = dr.main().login(self._url, r_info['url'], r_info['username'], r_info['password'])['status'] + if not login: + return public.return_message(-1, 0, "Repository [{}] Login failed!".format(r_info['url'])) + + auth_conf = { + "username": r_info['username'], + "password": r_info['password'], + "registry": r_info['url'] + } + # repository namespace/image + + repository = r_info['url'] + image = "{}/{}/{}".format(repository, r_info['namespace'], get.tag) + + self.tag(self._url, get.id, image) + ret = self.docker_client(self._url).images.push( + repository=image.split(":")[0], + tag=image.split(":")[1], + auth_config=auth_conf, + stream=True + ) + + dp.log_docker(ret, "Image push task") + # 删除自动打标签的镜像 + get.name = image + self.remove(get) + + except docker.errors.APIError as e: + if "invalid reference format" in str(e): + return public.return_message(-1, 0, "Push failed, image label error, please enter such as: v1.0.1") + if "denied: requested access to the resource is denied" in str(e): + return public.return_message(-1, 0, "Push failed, do not have permission to push to this repository!") + return public.return_message(-1, 0, "Push failure!{}".format(e)) + + dp.write_log("Image [{}] pushed successfully!".format(image)) + return public.return_message(0, 0, "Push successfully, mirror:{}".format(image)) + + def tag(self, url, image_id, tag): + """ + 为镜像打标签 + :param repository 仓库namespace/images + :param image_id: 镜像ID + :param tag: 镜像标签jose:v1 + :return: + """ + image = tag.split(":")[0] + tag_ver = tag.split(":")[1] + self.docker_client(url).images.get(image_id).tag( + repository=image, + tag=tag_ver + ) + return public.returnMsg(True, "Successfully set!") + + def pull(self, get): + """ + :param image + :param url + :param registry + :param username 拉取私有镜像时填写 1 + :param password 拉取私有镜像时填写 + :param get: + :return: + """ + get._ws.send("Pulling the image, please wait...\r\n") + + try: + get._ws.send("Pulling the image, please wait...\r\n") + import docker.errors + import time + time.sleep(0.1) + get._ws.send("Pull or search for images...\r\n") + try: + get.image = '{}:latest'.format(get.image) if ':' not in get.image else get.image + auth_data = { + "username": get.username, + "password": get.password, + "registry": get.registry if get.registry else None + } + auth_conf = auth_data if get.username else None + + if not hasattr(get, "tag"): get.tag = get.image.split(":")[-1] + + if get.registry != "docker.io": + get.image = "{}/{}/{}".format(get.registry, get.namespace, get.image) + + ret = dp.docker_client_low(self._url).pull( + repository=get.image.split(":")[0], + auth_config=auth_conf, + tag=get.tag, + stream=True + ) + + if not ret: + get._ws.send("bt_failed, pull failed!\r\n") + return + + while True: + try: + output = next(ret) + output = json.loads(output) + if 'status' in output: + output_str = output['status'] + get._ws.send(output_str + "\r\n") + time.sleep(0.1) + except StopIteration: + get._ws.send("bt_successful, Image pull [{}] successful\r\n".format(get.image)) + return public.returnMsg(True, "Image pulled successfully!") + except ValueError: + get._ws.send("bt_failed, Failed to pull image!\r\n") + return public.returnMsg(False, "Failed to pull image!") + + except docker.errors.ImageNotFound as e: + if "pull access denied for" in str(e): + get._ws.send( + "bt_failed, pull failed,The image does not exist, or the image may be a private image. You need to enter your dockerhub account password!\r\n") + return + get._ws.send("bt_failed, pull failed!{}\r\n".format(e)) + return + + except docker.errors.NotFound as e: + if "not found: manifest unknown" in str(e): + get._ws.send("bt_failed, pull failed,There is no such image in the repository!\r\n") + return + get._ws.send("bt_failed, pull failed!{}\r\n".format(e)) + return + + except docker.errors.APIError as e: + if "invalid tag format" in str(e): + get._ws.send("bt_failed, pull failed, The image format is wrong, such as: nginx:v 1!\r\n") + return + get._ws.send("bt_failed, pull failed!{}\r\n".format(e)) + return + + except Exception as e: + # public.print_log("拉取镜像 -- {}".format(e)) + public.print_log(traceback.format_exc()) + + # 拉取镜像 + def pull_high_api(self, get): + """ + :param image + :param url + :param registry + :param username 拉取私有镜像时填写 + :param password 拉取私有镜像时填写 + :param get: + :return: + """ + import docker.errors + try: + if ':' not in get.image: + get.image = '{}:latest'.format(get.image) + auth_data = { + "username": get.username, + "password": get.password, + "registry": get.registry if get.registry else None + } + + auth_conf = auth_data if get.username else None + + if get.registry != "docker.io": + get.image = "{}/{}/{}".format(get.registry, get.namespace, get.image) + + ret = self.docker_client(get.url).images.pull(repository=get.image, auth_config=auth_conf) + if ret: + return public.returnMsg(True, 'The image was pulled successfully.') + else: + return public.returnMsg(False, 'There may not be this mirror image.') + + except docker.errors.ImageNotFound as e: + if "pull access denied for" in str(e): + return public.returnMsg(False, + "Failed to pull the image, this is a private image, please enter the account password!") + return public.returnMsg(False, "Pull image failure

                                    Reason: {}".format(e)) + + def image_for_host(self, get): + """ + 获取镜像大小和获取镜像数量 + :param get: + :return: + """ + res = self.image_list(get) + if not res['status']: return res + + num = len(res['msg']['images_list']) + size = 0 + + for i in res['msg']['images_list']: + size += i['size'] + return public.returnMsg(True, {'num': num, 'size': size}) + + def prune(self, get): + """ + 删除无用的镜像 + :param get: + :return: + """ + dang_ling = True if "filters" in get and get.filters == "0" else False + + try: + res = self.docker_client(self._url).images.prune(filters={'dangling': dang_ling}) + + if not res['ImagesDeleted']: + return public.return_message(0, 0, "No useless images!") + + dp.write_log("Delete useless image successfully!") + return public.return_message(0, 0, "successfully delete!") + + except docker.errors.APIError as e: + return public.return_message(-1, 0, "failed to delete!{}".format(e)) + except Exception as e: + if error.find("Read timed out") != -1: + return public.return_message(-1, 0, + "Deletion of useless images failed and the connection to docker timed" + " out. Please try restarting the docker service and try again!") + return public.return_message(-1, 0, "failed to delete!{}".format(e)) + + # 2023/12/13 上午 11:08 镜像搜索 todo 关键字查询调用ws接口 暂时没查到 + def search(self, get): + ''' + @name 镜像搜索,docker hub官方镜像列表 + 从docker hub官方镜像列表获取最新排序镜像 + 数据库在/www/server/panel/class_v2/btdockerModelV2/config/docker_hub_repos.db + 每隔1个月从官网同步一次 + 脚本在/www/server/panel/class_v2/btdockerModelV2/script/syncreposdb.py + @author wzz <2023/12/13 下午 3:41> + @param 参数名<数据类型> 参数描述 + @return 数据类型 + ''' + try: + get.name = get.get("name/s", "") + if get.name == "": + # 2024/3/20 上午 10:10 如果get.name是空,则返回docker_hub_repos.db中results表的所有镜像 + import db, os + + sql = db.Sql() + sql.dbfile('{}/class_v2/btdockerModelV2/config/docker_hub_repos.db'.format(public.get_panel_path())) + # 2024/3/20 上午 10:24 按照star_count排序 + results = sql.table('results').field('name,description,star_count,is_official').order( + 'star_count desc').select() + + if not results: + return public.return_message(0, 0, []) + + return public.return_message(0, 0, results) + + from btdockerModelV2.dockerSock import image + sk_image = image.dockerImage() + + return public.return_message(0, 0, sk_image.search(get.name)) + except Exception as e: + # if os.path.exists('data/debug.pl'): + # print(public.get_error_info()) + public.print_log(public.get_error_info()) + return public.return_message(-1, 0, []) + + # 拉取容器日志 + def get_cmd_log(self, get): + """ + 拉取容器日志 + @param get: + @return: + """ + get.wsLogTitle = "Start executing the command, please wait..." + get._log_path = self._rCmd_log + return self.get_ws_log(get) diff --git a/class_v2/btdockerModelV2/monitorModel.py b/class_v2/btdockerModelV2/monitorModel.py new file mode 100644 index 00000000..010b755a --- /dev/null +++ b/class_v2/btdockerModelV2/monitorModel.py @@ -0,0 +1,129 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: zouhw +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +import sys +import threading +import time + +sys.path.insert(0, "/www/server/panel/class_v2/") +sys.path.insert(1, "/www/server/panel/") +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2 import containerModel as dc +from btdockerModelV2 import statusModel as ds +from btdockerModelV2 import imageModel as di +from public.validate import Param + +class main: + __save_date = None + __day_sec = 86400 + + def __init__(self, save_date): + if not save_date: + self.__save_date = 30 + else: + self.__save_date = save_date + + def docker_client(self, url): + return dp.docker_client(url) + + def get_all_host_stats(self, fun): + """ + 获取所有主机信息并获取该主机下的容器状态 + :param fun: 需要调用的方法,用于获取并记录容器状态 + :return: + """ + hosts = dp.sql('hosts').select() + for i in hosts: + t = threading.Thread(target=fun, args=(i,)) + t.setDaemon(True) + t.start() + + # 获取所有docker容器的状态信息 + def container_status_for_all_hosts(self): + """ + 获取所有服务器的容器数量 + :return: + """ + # while True: + args = public.to_dict_obj({}) + container_list = dc.main().get_list(args) + for c in container_list['container_list']: + args.id = c['id'] + args.write = 1 + args.save_date = self.__save_date + ds.main().stats(args) + # time.sleep(60) + + # 获取所有服务器的容器数量 + def container_count(self): + # while True: + hosts = dp.sql('hosts').select() + n = 0 + for i in hosts: + args = public.to_dict_obj({}) + args.url = i['url'] + container_list = dc.main().get_list(args) + n += len(container_list) + pdata = { + "time": int(time.time()), + "container_count": n + } + expired = time.time() - (self.__save_date * self.__day_sec) + dp.sql("container_count").where("time +# ------------------------------------------------------------------- + +import docker.errors + +import gettext +_ = gettext.gettext +# ------------------------------ +# Docker模型 +# ------------------------------ +import public + +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +class main(dockerBase): + + def docker_client(self, url): + return dp.docker_client(url) + + def get_network_id(self, get): + """ + asdf + @param get: + @return: + """ + networks = self.docker_client(self._url).networks + network = networks.get(get.id) + return network.attrs + + def get_host_network(self, get): + """ + 获取服务器的docker网络 + :param get: + :return: + """ + try: + client = self.docker_client(self._url) + if not client: + return public.return_message(-1, 0, []) + + networks = client.networks + network_attr = self.get_network_attr(networks) + data = list() + + for attr in network_attr: + get.id = attr["Id"] + c_result = self.get_network_id(get) + subnet = "" + gateway = "" + if attr["IPAM"]["Config"]: + if "Subnet" in attr["IPAM"]["Config"][0]: + subnet = attr["IPAM"]["Config"][0]["Subnet"] + if "Gateway" in attr["IPAM"]["Config"][0]: + gateway = attr["IPAM"]["Config"][0]["Gateway"] + + tmp = { + "id": attr["Id"], + "name": attr["Name"], + "time": dp.convert_timezone_str_to_timestamp(attr["Created"]), + "driver": attr["Driver"], + "subnet": subnet, + "gateway": gateway, + "labels": attr["Labels"], + "used": 1 if c_result["Containers"] else 0, + "containers": c_result["Containers"], + } + data.append(tmp) + + return public.return_message(0, 0, sorted(data, key=lambda x: x['time'], reverse=True)) + except Exception as e: + err = str(e) + if "Connection reset by peer" in err: + return public.return_message(-1, 0, _( + "The docker service is running abnormally, please restart and try again!")) + return public.return_message(-1, 0, []) + + def get_network_attr(self, networks): + network = networks.list() + return [i.attrs for i in network] + + def add(self, get): + """ + :param name 网络名称 + :param driver bridge/ipvlan/macvlan/overlay + :param options Driver options as a key-value dictionary + :param subnet '124.42.0.0/16' + :param gateway '124.42.0.254' + :param iprange '124.42.0.0/24' + :param labels Map of labels to set on the network. Default None. + :param remarks 备注 + :param get: + :return: + """ + # {"name": "23sdff223f", "driver": "overlay", "options": "", "subnet": "192.168.13.0/24", + # "gateway": "192.168.13.1", "iprange": "192.168.13.0/24", "labels": ""} + # 校验参数 + try: + get.validate([ + Param('name').Require().String(), + Param('subnet').Require(), + Param('gateway').Require(), + Param('iprange').Require(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import docker + + ipam_pool = docker.types.IPAMPool( + subnet=get.subnet, + gateway=get.gateway, + iprange=get.iprange + ) + + ipam_config = docker.types.IPAMConfig( + pool_configs=[ipam_pool] + ) + + try: + self.docker_client(self._url).networks.create( + name=get.name, + options=dp.set_kv(get.options), + driver="bridge", + ipam=ipam_config, + labels=dp.set_kv(get.labels) + ) + except docker.errors.APIError as e: + print(str(e)) + if "failed to allocate gateway" in str(e): + return public.return_message(-1, 0, _( + "The gateway setting is wrong, Please enter a gateway that matches the subnet: {}".format( + get.subnet))) + if "invalid CIDR address" in str(e): + return public.return_message(-1, 0, _( + "Subnet address format error, please enter for example: 172.16.0.0/16")) + if "invalid Address SubPool" in str(e): + return public.return_message(-1, 0, _( + "IP range format error, please enter the appropriate IP range for this subnet:".format( + get.subnet))) + if "Pool overlaps with other one on this address space" in str(e): + return public.return_message(-1, 0, _( "IP range [{}] already exists!".format(get.subnet))) + return public.return_message(-1, 0, _( "Failed to add network! {}".format(str(e)))) + + dp.write_log("Added network [{}] [{}] successful!".format(get.name, get.iprange)) + return public.return_message(0, 0, _( "Added network successfully!")) + + def del_network(self, get): + """ + :param id + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + networks = self.docker_client(self._url).networks.get(get.id) + attrs = networks.attrs + if attrs['Name'] in ["bridge", "none"]: + return public.return_message(-1, 0, _( "The system default network cannot be deleted!")) + + networks.remove() + dp.write_log("Delete network [{}] successfully!".format(attrs['Name'])) + return public.return_message(0, 0, _( "successfully delete!")) + + except docker.errors.APIError as e: + if " has active endpoints" in str(e): + return public.return_message(-1, 0, _( "The network cannot be deleted while it is in use!")) + return public.return_message(-1, 0, _( "Delete failed! {}".format(str(e)))) + + def prune(self, get): + """ + 删除无用的网络 + :param get: + :return: + """ + try: + res = self.docker_client(self._url).networks.prune() + if not res['NetworksDeleted']: + return public.return_message(-1, 0, _( "There are no useless networks!")) + + dp.write_log("Delete useless network successfully!") + return public.return_message(0, 0, _( "successfully delete!")) + + except docker.errors.APIError as e: + return public.return_message(-1, 0, _( "Delete failed! {}".format(str(e)))) + + def disconnect(self, get): + """ + 断开某个容器的网络 + :param id + :param container_id + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('id').Require().String(), + Param('container_id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + get.id = get.get("id/s", "") + get.container_id = get.get("container_id/s", "") + if get.id == "": + return public.return_message(-1, 0, _( "Network ID cannot be empty")) + if get.container_id == "": + return public.return_message(-1, 0, _( "Container ID cannot be empty")) + + networks = self.docker_client(self._url).networks.get(get.id) + networks.disconnect(get.container_id) + dp.write_log("Network disconnection [{}] successful!".format(get.id)) + return public.return_message(0, 0, _( "Network disconnection was successful!")) + except docker.errors.APIError as e: + if "No such container" in str(e): + return public.return_message(-1, 0, _( "Container ID: {}, does not exist!".format(get.container_id))) + if "network" in str(e) and "Not Found" in str(e): + return public.return_message(-1, 0, _( "Network ID: {}, does not exist!".format(get.id))) + return public.return_message(-1, 0, _( "Network disconnection failed! {}".format(str(e)))) + + def connect(self, get): + """ + 连接到指定网络 + :param id + :param container_id + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('id').Require().String(), + Param('container_id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + networks = self.docker_client(self._url).networks.get(get.id) + networks.connect(get.container_id) + dp.write_log("Network connection [{}] successful!".format(get.id)) + return public.return_message(0, 0, _( "Network connection successful!")) + except docker.errors.APIError as e: + if "No such container" in str(e): + return public.return_message(-1, 0, _( "Container ID: {}, does not exist!".format(get.container_id))) + if "network" in str(e) and "Not Found" in str(e): + return public.return_message(-1, 0, _( "Network ID: {}, does not exist!".format(get.id))) + return public.return_message(-1, 0, _( "Failed to connect to network! {}".format(str(e)))) diff --git a/class_v2/btdockerModelV2/projectModel.py b/class_v2/btdockerModelV2/projectModel.py new file mode 100644 index 00000000..32221284 --- /dev/null +++ b/class_v2/btdockerModelV2/projectModel.py @@ -0,0 +1,540 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + + +import gettext +_ = gettext.gettext +# ------------------------------ +# Docker模型 +# ------------------------------ +import public +import os +import time +import json +import re +from btdockerModelV2 import dk_public as dp +from btdockerModelV2 import setupModel as ds +from btdockerModelV2 import volumeModel as dv +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +class main(dockerBase): + + compose_path = "{}/data/compose".format(public.get_panel_path()) + project_path = "/www/dk_project" + templates_path = "{}/templates".format(project_path) + config_path = "{}/config".format(public.get_panel_path()) + info_path = "{}/docker_project_info.json".format(config_path) + __first_pl = "{}/first.pl".format(project_path) + + def __init__(self): + self.log_file = "/tmp/dk_project_run.log" + self.docker_setup = ds.main() + if not os.path.exists(self.templates_path): os.system("mkdir -p {}".format(self.templates_path)) + self.compose_cmd = "/usr/bin/docker-compose" if self.docker_setup.check_docker_compose_service()[0] \ + else "/usr/local/bin/docker-compose" + + def __check_conf(self, filename): + ''' + 验证配置文件是否可执行 + @param filename: docker-compose.yml文件路劲 + @return: + ''' + return public.ExecShell("{} -f {} config".format(self.compose_cmd, filename)) + + def sync_item(self, get): + ''' + 同步官方可以一键部署的项目 + @param get: 空对象 + @return: + ''' + os.remove(self.info_path) + project_info = self._get_project_list(get) + failed_list = [] + successes_list = [] + for info in project_info: + if info["server_name"]: + down_project_yml = self.__download_project_yml(info["server_name"]) + if not down_project_yml["status"]: + failed_list.append(info["server_name"]) + continue + successes_list.append(info["server_name"]) + data = [{"successes": len(successes_list), "server_name": successes_list}, + {"failed": len(failed_list), "server_name": failed_list}] + return public.return_message(0, 0, data) + + def __first_sync_item(self, project_info): + ''' + 同步官方可以一键部署的项目 + @param get: 空对象 + @return: + ''' + failed_list = [] + successes_list = [] + for info in project_info: + if info["server_name"]: + down_project_yml = self.__download_project_yml(info["server_name"]) + if not down_project_yml["status"]: + failed_list.append(info["server_name"]) + continue + successes_list.append(info["server_name"]) + data = [{"successes": len(successes_list), "server_name": successes_list}, + {"failed": len(failed_list), "server_name": failed_list}] + return data + + def get_project_list(self, get): + ''' + 获取支持一键部署的项目列表 + @param get: + @return: + ''' + project_info = [] + try: + if not os.path.exists(self.info_path): + down_info = self.__download_info(self.info_path) + if not down_info["status"]: + return public.return_message(0, 0, project_info) + + project_info = json.loads(public.readFile(self.info_path)) + project_info.sort(key=lambda x: x["sort"]) + + if not os.path.exists(self.__first_pl): + sync_result = self.__first_sync_item(project_info) + for result in sync_result: + if result.get("successes") and result["successes"] <= 0: + return public.return_message(0, 0, project_info) + public.ExecShell("echo \"first\" > {}".format(self.__first_pl)) + + except Exception as e: + project_info = [] + + return public.return_message(0, 0, project_info) + + def _get_project_list(self, get): + ''' + 获取支持一键部署的项目列表 + @param get: + @return: + ''' + project_info = [] + try: + if not os.path.exists(self.info_path): + down_info = self.__download_info(self.info_path) + if not down_info["status"]: + return project_info + + project_info = json.loads(public.readFile(self.info_path)) + project_info.sort(key=lambda x: x["sort"]) + + if not os.path.exists(self.__first_pl): + sync_result = self.__first_sync_item(project_info) + for result in sync_result: + if result.get("successes") and result["successes"] <= 0: + return project_info + public.ExecShell("echo \"first\" > {}".format(self.__first_pl)) + + except Exception as e: + project_info = [] + + return project_info + + def __get_docker_status(self, args): + ''' + 获取docker安装和启动状态 + @param args: + @return: + ''' + return { + "installed": self.docker_setup.check_docker_compose_service(), + "service_status": self.docker_setup.get_service_status() + } + + def __download_info(self, info_path): + ''' + 下载版本信息: info.json + @param info_path: string info.json文件的路劲 + @return: + ''' + url = "{}/install/lib/docker_project/docker_project_info.json".format(public.get_url()) + dp.download_file(url, info_path) + if os.path.exists(info_path): + return public.returnMsg(True, "info.json is downloaded!") + return public.returnMsg(False, "The info.json download failed!") + + def __download_project_yml(self, server_name): + ''' + 下载指定项目压缩包 + @param server_name: string 模板名称,如nextcloud + @return: + ''' + try: + path = "{}/{}".format(self.templates_path, server_name) + filename = "{}/{}.tar.gz".format(self.templates_path, server_name) + compose_file = "{}/docker-compose.yml".format(path) + url = "{}/install/lib/docker_project/templates/{}.tar.gz".format(public.get_url(), server_name) + dp.download_file(url, filename) + if not os.path.exists(filename): + return public.returnMsg(False, "{} Download failed, please resync!".format(server_name)) + if os.path.getsize(filename) == 0: + os.remove(filename) + return public.returnMsg(False, "{} Download failed, please resync!".format(server_name)) + self.__tar_x_yml(server_name, path, filename) + if os.path.exists(compose_file): + check_conf = self.__check_conf(compose_file) + if check_conf[1]: + return public.returnMsg(False, "{}yml file test failed,{}".format(server_name, check_conf[1])) + return public.returnMsg(True, "{} Download completed!".format(server_name)) + except: + return public.returnMsg(False, "{} Download failed, please resync!".format(server_name)) + + def __tar_x_yml(self, server_name, path=None, filename=None): + ''' + 解压项目模板方法 + @param server_name: 模板名称,如nextcloud + @param path: 项目模板路劲,如/www/dk_project/templates/nextcloud + @param filename: 项目模板压缩包,如/www/dk_project/templates/nextcloud.tar.gz + @return: + ''' + tar_result = public.ExecShell("tar xvf {} -C {}".format(filename, self.templates_path)) + if tar_result[1]: + os.remove(path) + os.remove(filename) + return public.returnMsg(False, "{} Decompression failed".format(server_name)) + return public.returnMsg(True, "{} extracted successfully".format(server_name)) + + def create_project_volume(self, server_name, project_name, dir_names, volume_path): + ''' + 创建指定项目的数据存储卷 + @param volume_path: + @param project_name: string + @param dir_names: list [dir_name,dir_name,...] + @return: + ''' + args = public.dict_obj() + args.url = "unix:///var/run/docker.sock" + # volumes = dv.main().get_volume_list(args) + # {'status': True, 'msg': {'volume': [], 'installed': True, 'service_status': True}} + # if volumes['status']: + # volumes = volumes['msg']['volume'] + # else: + # volumes = list() + # volume的值,一个list: [] + for dir_name in dir_names: + # # 如果已经存在就跳过 + # for volume in volumes: + # if dir_name == volume["Name"]: + # continue + if volume_path == "": + path = "{}/projects/{}/data/{}".format(self.project_path, project_name, dir_name) + else: + path = "{}/data/{}".format(volume_path, dir_name) + is_mkdir = public.ExecShell("mkdir -p {}".format(path)) + if is_mkdir[1]: return public.returnMsg(False, "Directory creation failed for the following reasons: {}".format(is_mkdir[1])) + args.name = "{}_{}_{}".format(project_name, server_name, dir_name) + args.driver = "local" + args.driver_opts = {'type': 'none', 'device': path, 'o': 'bind'} + args.labels = {} + dv.main().add(args) + return public.returnMsg(True, "The storage volume has been created") + + def get_project(self, get): + ''' + 获取指定一键部署项目的配置信息 + @param get: get.server_name + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('server_name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + server_name = getattr(get, "server_name") + info_path = "{}/{}/conf.json".format(self.templates_path, server_name) + project_info = json.loads(public.readFile(info_path)) + volume_placeholder = "Default: {}/projects/ your project name /data/".format(self.project_path) + total_sum = len(project_info) + volume_path = {"id": total_sum + 1, "sort": total_sum + 1, "type": "string", + "key": "VOLUME_PATH", "value": "", "placeholder": volume_placeholder, + "ps": "Data storage directory"} + project_info.append(volume_path) + except: + project_info = [] + return public.return_message(0, 0, project_info) + + def _get_project(self, get): + ''' + 获取指定一键部署项目的配置信息 + @param get: get.server_name + @return: + ''' + try: + server_name = getattr(get, "server_name") + info_path = "{}/{}/conf.json".format(self.templates_path, server_name) + project_info = json.loads(public.readFile(info_path)) + volume_placeholder = "Default: {}/projects/ your project name /data/".format(self.project_path) + total_sum = len(project_info) + volume_path = {"id": total_sum + 1, "sort": total_sum + 1, "type": "string", + "key": "VOLUME_PATH", "value": "", "placeholder": volume_placeholder, + "ps": "Data storage directory"} + project_info.append(volume_path) + except: + project_info = [] + return project_info + + def __get_server_ps(self, project_conf, conf_key): + ''' + 获取对应服务名的标题 + @param project_conf: + @param conf_key: + @return: + ''' + get = public.dict_obj() + for conf in project_conf: + if conf["key"] == "SERVER_NAME": + get.server_name = conf["value"] + server_conf = self._get_project(get) + for server in server_conf: + if conf_key == server["key"]: + return server["ps"] + return conf_key + + def get_project_logs(self, get): + """ + 获取一键部署日志,websocket + @param get: + @return: + """ + get.wsLogTitle = "Please wait to execute the command..." + print(self.log_file) + get._log_path = self.log_file + return self.get_ws_log(get) + + def create_project(self, get): + ''' + 创建一键部署的项目 + @param get: + @return: + ''' + + # {"project_conf": [{"key": "PROJECT_NAME", "value": "sdfasdf"}, {"key": "PORT", "value": "8180"}, + # {"key": "DB_ROOT_PASS", "value": "bt_nextcloud"}, {"key": "DB_NAME", "value": "nextcloud"}, + # {"key": "DB_USER", "value": "nextcloud"}, {"key": "DB_PASS", "value": "bt_nextcloud"}, + # {"key": "VOLUME_PATH", "value": "/www/dk_project/projects/sdfasdf"}, + # {"key": "REMARK", "value": "SDFADSF"}, {"key": "SERVER_NAME", "value": "nextcloud"}, + # {"key": "VOLUMES", "value": ["nextcloud", "db"]}]} + + + # 校验参数 + try: + get.validate([ + Param('project_conf').Require().List(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_conf = getattr(get, "project_conf") + remark = "" + for conf in project_conf: + if conf["key"] != "REMARK" and type(conf["value"]) != list: + if re.search(r'\s', conf["value"]): + server_ps = self.__get_server_ps(project_conf, conf["key"]) + return public.return_message(-1, 0, _( "{} cannot contain Spaces".format(server_ps))) + if conf["key"] != "VOLUME_PATH" and conf["key"] != "REMARK": + if conf["value"] == "": + server_ps = self.__get_server_ps(project_conf, conf["key"]) + return public.return_message(-1, 0, _( "{} cannot be null!".format(server_ps))) + if conf["key"].upper() == "PROJECT_NAME": project_name = conf["value"].strip() + if conf["key"].upper() == "VOLUME_PATH": project_volume = conf["value"].strip() + if conf["key"].upper() == "SERVER_NAME": server_name = conf["value"].strip() + if conf["key"].upper() == "VOLUMES": # VOLUMES = list + volumes = conf["value"] + if conf["key"].upper() == "PORT": + if dp.check_socket(conf["value"]): + return public.return_message(-1, 0, _( "Server port [{}] is occupied, please change to another port!".format(conf['value']))) + project_port = conf["value"] + if conf["key"] == "REMARK": remark = conf["value"] + + config_path = "{}/config/name_map.json".format(public.get_panel_path()) + if not os.path.exists(config_path): + public.writeFile(config_path, json.dumps({})) + + if public.readFile(config_path) == '': + public.writeFile(config_path, json.dumps({})) + + name_map = json.loads(public.readFile(config_path)) + name_str = 'q18q' + public.GetRandomString(10).lower() + name_map[name_str] = project_name + project_name = name_str + public.writeFile(config_path, json.dumps(name_map)) + server_dir = "{}/{}".format(self.templates_path, server_name) + project_dir = "{}/projects/{}/{}_{}".format(self.project_path, project_name, project_name, server_name) + public.set_module_logs('docker_project', 'create_project', 1) + check_result = self.__create_dir(project_dir, project_name, server_name, server_dir) + # todo 修改返回内容 只取msg 测试是否取到 + if not check_result["status"]: + return public.return_message(-1, 0, check_result["msg"]) + + self.__write_config(project_dir, project_name, server_name, project_conf) + self.create_project_volume(server_name, project_name, volumes, project_volume) + run_result = self.__project_run(project_dir, project_name) + + if run_result["status"]: + self.__add_sql(project_dir, project_name, server_name, remark) + dp.write_log("One-click deployment project [{}] successful!".format(server_name)) + return public.return_message(-1, 0, self.__return_msg(project_port)) + return public.return_message(-1, 0, run_result) + # return public.return_message(-1, 0, run_result["msg"]) + + def __project_run(self, project_dir, project_name): + ''' + 运行项目 + @param project_dir: 项目运行目录 + @param server_name: 服务名称 + @return: + ''' + filename = "{}/docker-compose.yml".format(project_dir) + check_result = self.__check_conf(filename) + if check_result[1]: + return public.returnMsg(False, "Project startup failed {}".format(check_result[1])) + + public.ExecShell("echo -n > {}".format(self.log_file)) + public.ExecShell("nohup {} -f {}/docker-compose.yml up -d >> {} 2>&1 &&" + " echo 'bt_successful' >> {} || echo 'bt_failed' >> {} &" + .format( + self.compose_cmd, + project_dir, + self.log_file, + self.log_file, + self.log_file + )) + return public.returnMsg(True, "Start creating the project") + + def __create_dir(self, project_dir, project_name, server_name, server_dir): + ''' + 创建项目目录 + @param project_dir: 项目目录 + @param project_name: 项目名称 + @param server_dir: 服务源目录 + @return: + ''' + if self.__check_repeat(project_dir, project_name, server_name): + return public.returnMsg(False, "{} already exists, please change the project name".format(project_name)) + mk_result = public.ExecShell("mkdir -p {}".format(project_dir)) + if mk_result[1]: return public.returnMsg(False, "User project directory failed to create,details: {}".format(mk_result[1])) + cp_result = public.ExecShell("cp -a {}/. {}/".format(server_dir, project_dir)) + if cp_result[1]: return public.returnMsg(False, "Failed to copy project directory. Details: {}".format(cp_result[1])) + return public.returnMsg(True, "") + + def __add_sql(self, project_dir, project_name, server_name, remark): + ''' + 添加项目到docker数据库中 + @param project_dir: 项目路劲 + @param project_name: 项目名称 + @return: + ''' + pdata = { + "name": public.xsssec("{}_{}".format(project_name, server_name)), + "status": "1", + "path": "{}/docker-compose.yml".format(project_dir), + "template_id": "", + "time": time.time(), + "remark": public.xsssec(remark) + } + dp.sql("stacks").insert(pdata) + + def __return_msg(self, project_port): + ''' + 创建成功后返回给用户的数据 + @param project_port: + @return: + ''' + server_ip = public.get_server_ip() + local_ip = public.GetLocalIp() + data = {"protocol": "http", "server_ip": server_ip, "local_ip": local_ip, + "port": project_port} + return public.returnMsg(True, data) + + def __check_repeat(self, project_dir, project_name, server_name): + ''' + 检查是否存在相同项目 + @param project_dir: 项目路劲 + @return: + ''' + # if os.path.exists(project_dir): + # return True + stacks_info = dp.sql("stacks").where("name=?", ("{}_{}".format(project_name, server_name),)).find() + if stacks_info: + return True + return False + + def __write_config(self, project_dir, project_name, server_name, project_conf): + ''' + 写配置文件 + @param project_dir: 用户项目目录 + @param project_name: 项目名称 + @param server_name: 服务名称,如nextcloud + @param project_conf: 新的配置文件内容 + @return: + ''' + old_env_path = "{}/{}/.env".format(self.templates_path, server_name) + new_env_path = "{}/.env".format(project_dir) + env_conf = "" + if not os.path.exists(old_env_path): + public.ExecShell("echo > {}".format(old_env_path)) + with open(old_env_path) as env: + lines = env.readlines() + # 取旧文件转字典 + old_dict = {} + for line in lines: + if "=" in line: + temp = line.split("=") + old_dict[temp[0]] = temp[1] + # 新数据转字典 + new_dict = {} + for conf in project_conf: + if conf["key"] == "VOLUME_PATH": + project_volume = conf["value"] + if "Default path" in project_volume: + conf["value"] = "{}/{}/data/".format(self.project_path, project_name) + continue + if conf["key"] == "VOLUMES": continue + new_dict[conf["key"].upper()] = conf["value"] + # 旧字典更新新字典的内容 + old_dict.update(new_dict) + # 拼接成新的环境变量文件 + for key, value in old_dict.items(): + env_conf += "{}={}\n".format(key, value.strip()) + public.writeFile(new_env_path, env_conf) + return True + + def sync_compose_template(self, server_name): + ''' + 同步模板到项目模板页面 + @param server_name: 模板名称 + @return: + ''' + data = dp.sql("templates").where("name=?", (server_name,)).find() + # if data: dp.sql("templates").delete(id=data["id"]) + if data: return + pdata = { + "name": server_name, + "remark": "aaPanel Docker Quick Deployment templates only [Do not delete them and use them separately to create projects]", + "path": "{}/{}/docker-compose.yml".format(self.templates_path, server_name) + } + dp.sql("templates").insert(pdata) + dp.write_log("Add template [{}] successful!".format(server_name)) diff --git a/class_v2/btdockerModelV2/proxyModel.py b/class_v2/btdockerModelV2/proxyModel.py new file mode 100644 index 00000000..268529e4 --- /dev/null +++ b/class_v2/btdockerModelV2/proxyModel.py @@ -0,0 +1,301 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import json +import os +import traceback +from datetime import datetime + +import gettext +_ = gettext.gettext +# 未处理关键字 + +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param +class main(dockerBase): + + # 2023/12/27 下午 2:56 创建容器反向代理 + def create_proxy(self, get): + ''' + @name 创建容器反向代理 + @author wzz <2023/12/27 下午 2:57> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + # 校验参数 + try: + get.validate([ + Param('domain').Require(), + Param('container_port').Require(), + Param('container_name').Require(), + Param('container_id').Require(), + Param('privateKey').Require(), + Param('certPem').Require(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + try: + if not (os.path.exists('/etc/init.d/nginx') or os.path.exists('/etc/init.d/httpd')): + return public.return_message(-1, 0, 'nginx or apache server was not detected, please install one first!') + + # if not hasattr(get, 'domain'): + # return public.return_message(-1, 0, 'parameter error') + # + # if not hasattr(get, 'container_port'): + # return public.return_message(-1, 0, 'parameter error') + + self.siteName = get.domain.strip() + self.check_table_dk_sites() + if dp.sql('dk_sites').where('container_id=?', (get.container_id,)).order('id desc').find(): + self.close_proxy(get) + # 2024/2/23 下午 12:05 如果其他地方有这个域名,则禁止添加 + newpid = public.M('domain').where("name=? and port=?", (self.siteName, 80)).getField('pid') + if newpid: + result = public.M('sites').where("id=? and ps!=?", + (newpid, 'Reverse proxy for the container [{}]'.format(get.container_name))).find() + if result: + return public.return_message(-1, 0, _( + 'Project Type [{}] Existing Domain: {}'.format(result['project_type'], + self.siteName))) + + self.container_port = get.container_port + if not dp.check_socket(self.container_port): + return public.return_message(-1, 0, _( "Server port [{}] is not used, please enter the port in use to reverse!".format(self.container_port))) + + self.sitePath = '/www/wwwroot/' + self.siteName + + from panelSite import panelSite + args = public.to_dict_obj({ + 'webname': '{"domain":"'+ self.siteName +'","domainlist":[],"count":0}', + 'type': 'docker', + 'port': "80", + 'ps': self.siteName, + 'path': self.sitePath, + 'type_id': 111, + 'version': "00", + 'ftp': False, + 'sql': False, + }) + panelSite().AddSite(args) + + args = public.to_dict_obj({ + 'type': 1, + 'proxyname': get.container_name + '_dk_proxy', + 'cachetime': 1, + 'proxydir': '/', + 'cache': 0, + 'subfilter': '[{"sub1":"","sub2":""},{"sub1":"","sub2":""},{"sub1":"","sub2":""}]', + 'sitename': self.siteName, + 'advanced': 0, + 'proxysite': 'http://127.0.0.1:' + self.container_port, + 'todomain': '$host', + }) + import projectModel.proxyModel as proxyModel + proxyModel = proxyModel.main() + proxyModel.CreateProxy(args) + + # 设置面板SSL + if hasattr(get, "privateKey") and hasattr(get, "certPem"): + args = public.to_dict_obj({ + 'type': '1', + 'siteName': self.siteName, + 'key': get.privateKey, + 'csr': get.certPem, + }) + panelSite().SetSSL(args) + + # 写入数据库 + newpid = public.M('domain').where("name=? and port=?", (self.siteName, 80)).getField('pid') + if newpid: + # 更新ps和project_type字段 + public.M('sites').where("id=?", (newpid,)).save('ps,project_type', ( + 'Reverse proxy for the container [{}]'.format(get.container_name), + 'proxy')) + + site_pid = dp.sql('dk_sites').add( + 'name,path,ps,addtime,container_id,container_name,container_port', + (self.siteName, self.sitePath, 'Reverse proxy for the container [{}]'.format(get.container_name), + datetime.now().strftime("%Y-%m-%d %H:%M:%S"), get.container_id, get.container_name, self.container_port) + ) + if not site_pid: + return public.return_message(-1, 0, _( 'Add failure, database cannot be written!')) + # 检查数据库是否存在 + self.check_table_dk_domain() + domain_id = dp.sql('dk_domain').where('id=?', (site_pid,)).find() + if not domain_id: + dp.sql('dk_domain').add( + 'pid,name,addtime', + (site_pid, self.siteName, datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ) + + return public.return_message(0, 0, _( 'successfully added!')) + except Exception as e: + return public.return_message(0, 0, _( 'Add failed, error {}!'.format(str(e)))) + + # 2024/1/2 下午 5:34 获取容器的反向代理信息 + def get_proxy_info(self, get): + ''' + @name 获取容器的反向代理信息 + @author wzz <2024/1/2 下午 5:34> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('container_id').Require(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + + try: + # if not hasattr(get, 'container_id'): + # return public.return_message(-1, 0, 'parameter error') + + container_id = get.container_id + self.check_table_dk_sites() + proxy_info = dp.sql('dk_sites').where('container_id=?', (container_id,)).order('id desc').find() + # 没找到表 + if isinstance(proxy_info, dict): + return public.return_message(-1, 0, proxy_info) + + + + path = '/www/server/panel/vhost/cert/' + proxy_info['name'] + csrpath = path + "/fullchain.pem" + keypath = path + "/privkey.pem" + if os.path.exists(csrpath) and os.path.exists(keypath): + try: + proxy_info['cert'] = public.readFile(csrpath) + proxy_info['key'] = public.readFile(keypath) + except: + proxy_info['cert'] = "" + proxy_info['key'] = "" + + if not proxy_info: + return public.return_message(-1, 0, _( 'No reverse proxy information was detected!')) + return public.return_message(0, 0, proxy_info) + except Exception as ex: + print(traceback.format_exc()) + public.print_log("error: {}".format(ex)) + return public.return_message(-1, 0, {}) + + # 2024/1/2 下午 5:43 关闭容器的反向代理 + def close_proxy(self, get): + ''' + @name 关闭容器的反向代理 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + # 校验参数 + try: + get.validate([ + Param('container_id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + try: + # if not hasattr(get, 'container_id'): + # return public.return_message(-1, 0, 'parameter error!') + + container_id = get.container_id + proxy_info = dp.sql('dk_sites').where('container_id=?', (container_id,)).order('id desc').find() + + if not proxy_info: + return public.return_message(-1, 0, _( 'No reverse proxy information was detected!')) + + newpid = public.M('domain').where("name=? and port=?", (proxy_info["name"], 80)).getField('pid') + if not newpid: + return public.return_message(-1, 0, _( 'No reverse proxy information was detected!')) + + result = public.M('sites').where("id=? and ps=?", (newpid, 'Reverse proxy for the container [{}]'.format(proxy_info["container_name"]))).find() + # 删除反向代理 + import projectModel.proxyModel as proxyModel + proxyModel = proxyModel.main() + + args = public.to_dict_obj({ + 'id': result['id'], + 'webname': proxy_info['name'], + 'type': 1, + }) + proxyModel.DeleteSite(args) + + # 删除站点 + public.M('sites').where("name=?", (proxy_info['name'],)).delete() + public.M('domain').where("name=?", (proxy_info['name'],)).delete() + + # 删除数据库记录 + dp.sql('dk_sites').where('container_id=?', (container_id,)).delete() + dp.sql('dk_domain').where('pid=?', (proxy_info['id'],)).delete() + + return public.return_message(0, 0, _( 'successfully delete!')) + except: + return traceback.format_exc() + + # 2024/1/2 下午 5:57 获取指定域名的证书内容 + def get_cert_info(self, get): + ''' + @name 获取指定域名的证书内容 + @author wzz <2024/1/2 下午 5:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not hasattr(get, 'cert_name'): return public.return_message(-1, 0, _( 'parameter error!')) + cert_name = get.cert_name + # 2024/1/3 下午 4:50 处理通配符域名,将*.spider.com替换成spider.com + if cert_name.startswith('*.'): + cert_name = cert_name.replace('*.', '') + if not os.path.exists('/www/server/panel/vhost/ssl/{}'.format(cert_name)): + return public.return_message(-1, 0, _( 'Certificate does not exist!')) + cert_data = {} + cert_data['cert_name'] = cert_name + cert_data['cert'] = public.readFile('/www/server/panel/vhost/ssl/{}/fullchain.pem'.format(cert_name)) + cert_data['key'] = public.readFile('/www/server/panel/vhost/ssl/{}/privkey.pem'.format(cert_name)) + cert_data['info'] = json.loads( + public.readFile('/www/server/panel/vhost/ssl/{}/info.json'.format(cert_name))) + return public.return_message(-1, 0, cert_data) + except: + return public.return_message(-1, 0, traceback.format_exc()) + + def check_table_dk_domain(self): + ''' + @name 检查并创建表 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if not dp.sql('sqlite_master').where('type=? AND name=?', ('table', 'dk_domain')).count(): + dp.sql('dk_domain').execute( + "CREATE TABLE `dk_backup` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `pid` INTEGER, `name` TEXT, `addtime` TEXT )", + () + ) + + def check_table_dk_sites(self): + ''' + @name 检查并创建表 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if not dp.sql('sqlite_master').where('type=? AND name=?', ('table', 'dk_sites')).count(): + dp.sql('dk_sites').execute( + "CREATE TABLE `dk_backup` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `path` TEXT, `status` TEXT DEFAULT 1, `ps` TEXT, `addtime` TEXT, `type_id` integer DEFAULT 111, `edate` integer DEFAULT '0000-00-00', `project_type` STRING DEFAULT 'dk_proxy', `container_id` TEXT DEFAULT '', `container_name` TEXT DEFAULT '', `container_port` TEXT DEFAULT '')", + () + ) \ No newline at end of file diff --git a/class_v2/btdockerModelV2/registryModel.py b/class_v2/btdockerModelV2/registryModel.py new file mode 100644 index 00000000..8ea1999c --- /dev/null +++ b/class_v2/btdockerModelV2/registryModel.py @@ -0,0 +1,293 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: zouhw +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +import json + +import gettext +_ = gettext.gettext + +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +class main(dockerBase): + + def docker_client(self, url): + return dp.docker_client(url) + + def add(self, args): + """ + 添加仓库 + :param registry 仓库URL docker.io + :param name + :parma username + :parma password + :param namespace 仓库命名空间 + :param remark 备注 + :param args: + :return: + """ + # {"registry": "docker.io", "name": "wzznb", "username": "akaishuichi", "password": "xiuyi999..", + # "namespace": "akaishuichi", "remark": "wzz_docker_io"} + + # 校验参数 + try: + args.validate([ + Param('name').Require().String(), + Param('username').Require().String(), + Param('password').Require().String(), + Param('namespace').Require().String(), + Param('remark').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + # 验证登录 + if not args.registry: + args.registry = "docker.io" + res = self.login(self._url, args.registry, args.username, args.password) + if not res['status']: + return public.return_message(-1, 0, res) + r_list = self.registry_list(args) + if len(r_list) > 0: + for r in r_list: + if r['name'] == args.name: + return public.return_message(-1, 0, _( "The name already exists!

                                    name: {}".format(args.name))) + if r['username'] == args.username and args.registry == r['url']: + return public.return_message(-1, 0, _( "Repository information already exists!")) + pdata = { + "name": args.name, + "url": args.registry, + "namespace": args.namespace, + "username": public.aes_encrypt(args.username, self.aes_key), + "password": public.aes_encrypt(args.password, self.aes_key), + "remark": public.xsssec(args.remark) + } + dp.sql("registry").insert(pdata) + dp.write_log("Added repository [{}] [{}] success!".format(args.name, args.registry)) + return public.return_message(0, 0, _( "successfully added!")) + + def edit(self, args): + """ + 编辑仓库 + :param registry 仓库URL docker.io + :param id 仓库id + :parma username + :parma password + :param namespace + :param remark + :param args: + :return: + """ + + # 校验参数 + try: + args.validate([ + Param('id').Require().Integer(), + Param('username').Require().String(), + Param('password').Require().String(), + Param('namespace').Require().String(), + Param('remark').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # 验证登录 + # if str(args.id) == "1": + # return public.return_message(-1, 0, "[Official Docker repository] Not editable!") + if not args.registry: + args.registry = "docker.io" + + # 2023/12/13 上午 11:40 处理加密的编辑 + try: + is_encrypt = False + res = self.login(self._url, args.registry, args.username, args.password) + if not res['status']: + res = self.login( + self._url, + args.registry, + public.aes_decrypt(args.username, self.aes_key), + public.aes_decrypt(args.password, self.aes_key) + ) + if not res['status']: + return public.return_message(-1, 0, res['msg']) + is_encrypt = True + except Exception as e: + if "binascii.Error: Incorrect padding" in str(e): + return public.return_message(-1, 0, _( + "Editing failed! Reason: Account password decryption failed! Please delete the repository and add it again")) + return public.return_message(-1, 0, _( "Editing failed! Reason:{}".format(e))) + + res = dp.sql("registry").where("id=?", (args.id,)).find() + if not res: + return public.return_message(-1, 0, _( "This repository could not be found")) + pdata = { + "name": args.name, + "url": args.registry, + "username": public.aes_encrypt(args.username, self.aes_key) if is_encrypt is False else args.username, + "password": public.aes_encrypt(args.password, self.aes_key) if is_encrypt is False else args.password, + "namespace": args.namespace, + "remark": args.remark + } + dp.sql("registry").where("id=?", (args.id,)).update(pdata) + dp.write_log("Edit repository [{}][{}] Success!".format(args.name, args.registry)) + return public.return_message(0, 0, _( "Edit success!")) + + def remove(self, args): + """ + 删除某个仓库 + :param id + :param rags: + :return: + """ + # 校验参数 + try: + args.validate([ + Param('id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # if str(args.id) == "1": + # return public.return_message(-1, 0, "[Official Docker repository] can not be removed!") + + data = dp.sql("registry").where("id=?", (args.id)).find() + + if len(data) < 1: + return public.return_message(0, 0, _( "Delete failed,The repository id may not exist!")) + + dp.sql("registry").where("id=?", (args.id,)).delete() + + dp.write_log("Delete repository [{}][{}] Success!".format(data['name'], data['url'])) + return public.return_message(0, 0, _( "Successfully deleted!")) + def registry_list(self, get): + """ + 获取仓库列表 + :return: + """ + + db_obj = dp.sql("registry") + # 2024/1/3 下午 6:00 检测数据库是否存在并且表健康 + search_result = db_obj.where('id=? or name=?', (1, "Docker public repository")).select() + + # if db_obj.ERR_INFO: + # return [] + + + if len(search_result) == 0: + dp.sql("registry").insert({ + "name": "Docker public repository", + "url": "docker.io", + "username": "", + "password": "", + "namespace": "", + "remark": "Docker public repository" + }) + if "error: no such table: registry" in search_result or len(search_result) == 0: + # public.ExecShell("mv -f /www/server/panel/data/docker.db /www/server/panel/data/db/docker.db") + public.ExecShell("mv -f /www/server/panel/data/db/docker.db /www/server/panel/data/docker.db") + dp.check_db() + + res = dp.sql("registry").select() + if not isinstance(res, list): + res = [] + return res + # 改返回 + def registry_listV2(self, get): + """ + 获取仓库列表 + :return: + """ + db_obj = dp.sql("registry") + # 2024/1/3 下午 6:00 检测数据库是否存在并且表健康 + search_result = db_obj.where('id=? or name=?', (1, "Docker public repository")).select() + # search_result = db_obj.where('id=? ', (1)).select() + # if db_obj.ERR_INFO: + # return public.return_message(0, 0, []) + + + if len(search_result) == 0: + dp.sql("registry").insert({ + "name": "Docker public repository", + "url": "docker.io", + "username": "", + "password": "", + "namespace": "", + "remark": "Docker public repository" + }) + if "error: no such table: registry" in search_result or len(search_result) == 0: + # public.ExecShell("mv -f /www/server/panel/data/docker.db /www/server/panel/data/db/docker.db") + public.ExecShell("mv -f /www/server/panel/data/db/docker.db /www/server/panel/data/docker.db") + dp.check_db() + + res = dp.sql("registry").select() + if not isinstance(res, list): + res = [] + + return public.return_message(0, 0, res) + + def get_com_registry(self, get): + """ + 获取常用仓库列表 + @param get: + @return: + """ + com_registry_file = "{}/class/btdockerModelV2/config/com_registry.json".format(public.get_panel_path()) + try: + com_registry = json.loads(public.readFile(com_registry_file)) + except: + com_registry = { + "docker.io": "Docker public repository", + "swr.cn-north-4.myhuaweicloud.com": "Huawei Cloud mirror station", + "ccr.ccs.tencentyun.com": "Tencent cloud mirror station", + "registry.cn-hangzhou.aliyuncs.com": "Alibaba Cloud Mirror Station (Hangzhou)" + } + + return public.return_message(0, 0, com_registry) + + def registry_info(self, name): + return dp.sql("registry").where("name=?", (name,)).find() + + def login(self, url, registry, username, password): + """ + 仓库登录测试 + :param args: + :return: + """ + import docker.errors + try: + res = self.docker_client(url).login( + registry=registry, + username=username, + password=password, + reauth=False + ) + return public.returnMsg(True, str(res)) + except docker.errors.APIError as e: + if "authentication required" in str(e): + return public.returnMsg(False, + "Login test failed! Reason: May be account password error, please check!") + if "unauthorized: incorrect username or password" in str(e): + return public.returnMsg(False, + "Login test failed! Reason: May be account password error, please check!") + return public.returnMsg(False, "Login test failed! Reason: {}".format(e)) diff --git a/class_v2/btdockerModelV2/screen.py b/class_v2/btdockerModelV2/screen.py new file mode 100644 index 00000000..552fb3ef --- /dev/null +++ b/class_v2/btdockerModelV2/screen.py @@ -0,0 +1,84 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: zouhw +#------------------------------------------------------------------- + +#------------------------------ +# Docker模型 +#------------------------------ +import dk_public as dp +import time + +class main: + + def get_status(self,args): + """ + start_time + stop_time + :param args: + :return: + """ + data = dict() + # 容器总数 + data['container_count'] = self.__get_container_count(args) + # 镜像信息,镜像总数,占用空间大小 + data['image_info'] = dp.sql("image_infos").where("time>=? and time<=?",(args.start_time,args.stop_time)).select() + # 主机信息 + data['host'] = len(dp.sql('hosts').select()) + # 1小时内容器占用资源前三平均值 + data['container_top'] = {"cpu":self.__get_cpu_avg(),"mem":self.__get_mem_avg()} + return data + + def __get_container_count(self,args): + count = dp.sql('container_count').where("time>=? and time<=?", (args.start_time, args.stop_time)).select() + if not count: + return 0 + return count[-1] + + def __get_mem_avg(self): + now = int(time.time()) + start_time = now - 3600 + data = dp.sql("mem_stats").where("time>=? and time<=?",(start_time,now)).select() + containers = list() + info = dict() + # 获取容器ID + for d in data: + containers.append(d['container_id']) + # 获取每个容器1小时内的cpu使用率总和 + containers = set(containers) + for c in containers: + num = 0 + usage = 0 + for d in data: + if d['container_id'] == c: + num += 1 + usage += float(d['usage']) + if num != 0: + info[c] = usage / num + return info + + def __get_cpu_avg(self): + now = int(time.time()) + start_time = now - 3600 + data = dp.sql("cpu_stats").where("time>=? and time<=?",(start_time,now)).select() + containers = list() + info = dict() + # 获取容器ID + for d in data: + containers.append(d['container_id']) + # 获取每个容器1小时内的cpu使用率总和 + containers = set(containers) + for c in containers: + num = 0 + cpu_usage = 0 + for d in data: + if d['container_id'] == c: + num += 1 + cpu_usage += float(0 if d['cpu_usage'] == '0.0' else d['cpu_usage']) + if num != 0: + info[c] = cpu_usage / num + return info \ No newline at end of file diff --git a/class_v2/btdockerModelV2/script/flush_plugin_.py b/class_v2/btdockerModelV2/script/flush_plugin_.py new file mode 100644 index 00000000..a10e9912 --- /dev/null +++ b/class_v2/btdockerModelV2/script/flush_plugin_.py @@ -0,0 +1,134 @@ +# coding: utf-8 +import sys, os + +os.chdir('/www/server/panel/') +sys.path.insert(0, "class/") +import PluginLoader +import public +import time + + +def clear_hosts(): + """ + @name 清理hosts文件中的bt.cn记录 + @return: + """ + remove = 0 + try: + import requests + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + url = 'https://www.bt.cn/api/ip/info_json' + res = requests.post(url, verify=False) + + if res.status_code == 404: + remove = 1 + elif res.status_code == 200 or res.status_code == 400: + res = res.json() + if res != "[]": + remove = 1 + except: + result = public.ExecShell("curl -sS --connect-timeout 3 -m 60 -k https://www.bt.cn/api/ip/info_json")[0] + if result != "[]": + remove = 1 + + hosts_file = '/etc/hosts' + if remove == 1 and os.path.exists(hosts_file): + public.ExecShell('sed -i "/www.bt.cn/d" /etc/hosts') + +def flush_cache(): + ''' + @name 更新缓存 + @author hwliang + @return void + ''' + try: + # start_time = time.time() + res = PluginLoader.get_plugin_list(1) + spath = '{}/data/pay_type.json'.format(public.get_panel_path()) + public.downloadFile(public.get_url() + '/install/lib/pay_type.json', spath) + import plugin_deployment + plugin_deployment.plugin_deployment().GetCloudList(None) + + # timeout = time.time() - start_time + if 'ip' in res and res['ip']: + pass + else: + if isinstance(res, dict) and not 'msg' in res: res['msg'] = 'Connection failure!' + except: + pass + + +def flush_php_order_cache(): + """ + 更新软件商店php顺序缓存 + @return: + """ + spath = '{}/data/php_order.json'.format(public.get_panel_path()) + public.downloadFile(public.get_url() + '/install/lib/php_order.json', spath) + + +def flush_msg_json(): + """ + @name 更新消息json + """ + try: + spath = '{}/data/msg.json'.format(public.get_panel_path()) + public.downloadFile(public.get_url() + '/linux/panel/msg/msg.json', spath) + except: + pass + + +def flush_docker_project_info(): + ''' + @name 更新docker_project版本信息 + @author wzz + @return void + ''' + msg = "docker_projcet version information" + try: + # start_time = time.time() + res = PluginLoader.get_plugin_list(1) + config_path = f"{public.get_panel_path()}/config" + spath = f"{config_path}/docker_project_info.json" + url = "/install/lib/docker_project/docker_project_info.json" + public.downloadFile(f"{public.get_url()}{url}", spath) + import plugin_deployment + plugin_deployment.plugin_deployment().GetCloudList(None) + + # timeout = time.time() - start_time + if 'ip' in res and res['ip']: + pass + else: + if isinstance(res, dict) and not 'msg' in res: res['msg'] = 'Connection failure!' + except: + pass + + +# 2024/3/20 上午 11:09 更新docker_hub镜像排行数据 +def flush_docker_hub_repos(): + ''' + @name 更新docker_hub镜像排行数据 + @author wzz <2024/3/20 上午 11:09> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + public.ExecShell("/www/server/panel/pyenv/bin/python3 /www/server/panel/class_v2/btdockerModelV2/script/syncreposdb.py") + + +if __name__ == '__main__': + tip_date_tie = '/tmp/.fluah_time' + if os.path.exists(tip_date_tie): + last_time = int(public.readFile(tip_date_tie)) + timeout = time.time() - last_time + if timeout < 600: + print("Execution interval is too short, exit - {}!".format(timeout)) + sys.exit() + clear_hosts() + flush_cache() + flush_php_order_cache() + flush_msg_json() + flush_docker_project_info() + flush_docker_hub_repos() + + public.writeFile(tip_date_tie, str(int(time.time()))) diff --git a/class_v2/btdockerModelV2/script/syncreposdb.py b/class_v2/btdockerModelV2/script/syncreposdb.py new file mode 100644 index 00000000..e363f108 --- /dev/null +++ b/class_v2/btdockerModelV2/script/syncreposdb.py @@ -0,0 +1,92 @@ +#!/www/server/panel/pyenv/bin/python3 +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +# docker模型sock 封装库 镜像库 +# ------------------------------------------------------------------- +import os +import sys +import time + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public + +if "/www/server/panel/class_v2" not in sys.path: + sys.path.insert(0, "/www/server/panel/class_v2") +import btdockerModelV2.dk_public as dp + +db_file = '{}/class_v2/btdockerModelV2/config/docker_hub_repos.db'.format(public.get_panel_path()) +last_update_pl = "{}/class_v2/btdockerModelV2/config/docker_hub_last_update.pl".format(public.get_panel_path()) + + +# 2024/3/20 上午 9:47 获取docker hub最新的镜像排行数据 +def get_docker_hub_repos(): + ''' + @name 获取docker hub最新的镜像排行数据 + @author wzz <2024/3/20 上午 9:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + url = "{}/src/docker/docker_hub_repos.db".format(public.get_url()) + dp.download_file(url, db_file) + if not os.path.exists(db_file): + return public.returnMsg(False, "info.json download failed") + + # 写一个最后更新的标记文件,里面有时间戳 + public.writeFile(last_update_pl, str(int(time.time()))) + + return + except Exception as e: + if os.path.exists('data/debug.pl'): + print(public.get_error_info()) + public.print_log(public.get_error_info()) + + +# 2024/3/20 上午 9:34 如果当前时间减去这个时间戳大于30天,就执行 get_docker_hub_repos +def check_last_update(): + ''' + @name 如果当前时间减去这个时间戳大于30天,就执行 get_docker_hub_repos + @author wzz <2024/3/20 上午 9:46> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if os.path.exists(last_update_pl): + last_update_time = int(public.readFile(last_update_pl)) + if int(time.time()) - last_update_time > 2592000: + public.ExecShell("rm -f {}".format(db_file)) + public.ExecShell("rm -f {}".format(last_update_pl)) + get_docker_hub_repos() + + if os.path.exists(db_file) and (os.path.getsize(db_file) == 0 or os.path.getsize(db_file) < 80): + public.ExecShell("rm -f {}".format(db_file)) + public.ExecShell("rm -f {}".format(last_update_pl)) + get_docker_hub_repos() + + if not os.path.exists(db_file): + public.ExecShell("rm -f {}".format(last_update_pl)) + get_docker_hub_repos() + else: + if not os.path.exists(db_file): + get_docker_hub_repos() + + if os.path.exists(db_file) and (os.path.getsize(db_file) == 0 or os.path.getsize(db_file) < 80): + public.ExecShell("rm -f {}".format(db_file)) + public.ExecShell("rm -f {}".format(last_update_pl)) + get_docker_hub_repos() + except Exception as e: + public.ExecShell("rm -f {}".format(db_file)) + public.ExecShell("rm -f {}".format(last_update_pl)) + if os.path.exists('data/debug.pl'): + print(public.get_error_info()) + public.print_log(public.get_error_info()) + + +check_last_update() diff --git a/class_v2/btdockerModelV2/securityModel.py b/class_v2/btdockerModelV2/securityModel.py new file mode 100644 index 00000000..3fe5fcb0 --- /dev/null +++ b/class_v2/btdockerModelV2/securityModel.py @@ -0,0 +1,1058 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: lwh +# ------------------------------------------------------------------- +import fnmatch +import os, sys, time +from _stat import S_ISREG, S_ISDIR, S_ISLNK + +# ------------------------------ +# Docker安全检测 +# ------------------------------ +BASE_PATH = "/www/server/panel" +os.chdir(BASE_PATH) +sys.path.insert(0, "class/") +import public, re +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +def auto_progress(func): + """ + @name 自动增长进度条(装饰器) + @author lwh<2024-1-23> + """ + def wrapper(self, *args, **kwargs): + result = func(self, *args, **kwargs) + self.progress_percent += self.scan_percent + return result + return wrapper + + +class main(dockerBase): + progress_percent = 0 # 扫描进度条 + ids_percent = 0 + scan_percent = 0 + scan_score = 100 # 扫描分数 + progress_content = "Initializing scan..." # 扫描内容 + sys_ver = "" # 系统类型 + requirements_list = ["veinmind", "veinmind-common"] + docker_obj = "" # docker对象 + image_name = "" # 镜像名称 + send_time = "" # 记录发送时间 + start_time = "" # 开始时间 + + def short_string(self, text): + """ + @name 缩短字符串为40个字符 + """ + if len(text) <= 40: + return text + else: + return text[:37] + "..." + + def short_string1(self, text): + """ + @name 缩短字符串中间部分保留前后 + """ + if len(text) <= 30: + return text + else: + return text[:15] + "..." + text[-15:] + + def get_image_list(self, get): + """ + @name 获取镜像id列表 + @author lwh<2024-1-23> + @return + """ + public.print_log("Get the image id list") + if not hasattr(get, "_ws"): + return True + from btdockerModelV2 import imageModel + image_list = imageModel.main().image_list(get=public.dict_obj()) + # public.print_log(image_list) + get._ws.send(public.GetJson({"end": True, "image_list": image_list})) + + def send_image_ws(self, get, msg, detail="", repair="", status=1, end=False): + """ + @name 发送ws信息 + @author lwh<2024-01-23> + @param msg string 扫描内容 + @param status int 风险情况:1无风险,2告警,3危险 + @param repair string 修复方案 + @param end bool 是否结束 + """ + now_time = time.time() + # 判断间隔时间是否小于100ms + if now_time-self.send_time <= 0.1 and not end and status == 1: + return + self.send_time = now_time + # 根据风险情况进行扣分 + if status == 2: + score = 2 + elif status == 3: + score = 5 + else: + score = 0 + # msg:扫描内容,score:当前分数,type:类型docker/image + get._ws.send(public.GetJson({"end": end, "image_name": self.image_name, "status": status, "detail": detail, + "msg": msg, "repair": repair, "score": score, "type": "image"})) + + def send_docker_ws(self, get, msg, repair="", status=1, end=True): + """ + @name 发送ws信息 + @author lwh<2024-01-23> + @param msg string 扫描内容 + @param status int 风险情况:1无风险,2告警,3危险 + @param repair string 修复方案 + @param end bool 是否结束 + """ + # 根据风险情况进行扣分 + if status == 2: + score = 2 + elif status == 3: + score = 5 + else: + score = 0 + # msg:扫描内容,progress:扫描进度,score:当前分数 + get._ws.send(public.GetJson({"end": end, "image_name": self.image_name, "status": status, "msg": msg, "repair": repair, "score": score, "type": "docker"})) + + def reduce_core(self, score): + """ + @name 减少总分 + @author lwh<2024-01-23> + @param score int 需要减少的分数 + @return self.scan_score int 所剩分数 + """ + self.scan_score -= score + if self.scan_score < 0: + return 0 + return self.scan_score + + def image_safe_scan(self, get): + """ + @name 镜像安全扫描入口函数 + @author lwh@aapanel.com + @time 2024-01-22 + @param _ws + @return 返回服务器扫描项 + """ + public.set_module_logs('docker', 'image_safe_scan', 1) + if not hasattr(get, "_ws"): + return True + if not hasattr(get, "image_id"): + return True + # 获取检测镜像 + image_id = get.image_id + # 初始化时间 + self.send_time = time.time() + + # 初始化安装检测SDK + try: + from veinmind import docker + except Exception as e: + public.print_log("Importing veinmind failed:{}".format(e)) + # requirements_list = ["veinmind"] + self.send_image_ws(get, msg="The detection engine is being initialized. The first load may take a long time....", status=1) + shell_command = "btpip install --no-dependencies {}".format("veinmind") + public.ExecShell(shell_command) + sys_ver = public.get_os_version() + if "Ubuntu" in sys_ver or "Debian" in sys_ver: + public.WriteFile("/etc/apt/sources.list.d/libveinmind.list", + "deb [trusted=yes] https://download.veinmind.tech/libveinmind/apt/ ./") + self.send_image_ws(get, msg="Apt-get is being updated. The first load may take a long time....", status=1) + public.ExecShell("apt-get update") + time.sleep(1) + self.send_image_ws(get, msg="Detection engine being installed, first execution may take thousands of years...", status=1) + public.ExecShell("apt-get install -y libveinmind-dev") + time.sleep(1) + elif "CentOS" in sys_ver: + public.WriteFile("/etc/yum.repos.d/libveinmind.repo", """[libveinmind] +name=libVeinMind SDK yum repository +baseurl=https://download.veinmind.tech/libveinmind/yum/ +enabled=1 +gpgcheck=0""") + self.send_image_ws(get, msg="The yum cache is being updated. The first load may take a long time....", status=1) + public.ExecShell("yum makecache") + self.send_image_ws(get, msg="Detection engine being installed, first execution may take thousands of years...", status=1) + public.ExecShell("yum install -y libveinmind-devel") + else: + self.send_image_ws(get, msg="Unsupported system version {}".format(sys_ver), status=1) + return public.returnMsg(False, "Unsupported system version {}\nCurrently only supports Debian, Ubuntu, Centos".format(sys_ver)) + self.send_image_ws(get, msg="Checking libdl.so dependent libraries...", status=1) + result, err = public.ExecShell("whereis libdl.so") + result = result.strip().split(" ") + # public.print_log("The situation of libdl.so library:{}".format(result)) + if len(result) <= 1: + # public.print_log("Missing libdl.so library") + result, err = public.ExecShell("whereis libdl.so.2") + result = result.strip().split(" ") + # public.print_log("The situation of libdl.so.2 library:{}".format(result)) + if len(result) <= 1: + public.print_log("Missing libdl.so library,Requires libdl.so or libdl.so2 to be installed") + public.returnMsg(False, "Missing libdl.so library,Requires libdl.so or libdl.so2 to be installed") + else: + # 建立libdl.so软链接至libdl.so.2 + for lib in result[1:]: + ln_command = "ln -s {} {}".format(lib, lib[:-2]) + # public.print_log("Soft link in progress:{}".format(ln_command)) + public.ExecShell(ln_command) + from veinmind import docker + + # 开始检测 + # 获取docker对象 + docker_obj = docker.Docker() + # # 获取所有镜像id + # ids = docker_obj.list_image_ids() + # # 计算镜像进度占比 + # self.ids_percent = math.floor(100 / len(ids)) + # # 计算每个镜像扫描进度占比 + # self.scan_percent = math.floor(self.ids_percent / 3) + # 开始镜像检测 + # for key, id in enumerate(ids): + image = docker_obj.open_image_by_id(image_id=image_id) + # 获取ref镜像名 + refs = image.reporefs() + if len(refs) > 0: + self.image_name = refs[0] + else: + self.image_name = image.id() + public.print_log("Detecting:{}".format(self.image_name)) + self.send_image_ws(get, msg="Scanning {} exception history command".format(self.image_name)) + self.scan_history(get, image) + self.send_image_ws(get, msg="Scanning {} sensitive information".format(self.image_name)) + self.scan_sensitive(get, image) + self.send_image_ws(get, msg="Scanning {} backdoor".format(self.image_name)) + self.scan_backdoor(get, image) + self.send_image_ws(get, msg="Scanning {} container escapes") + self.scan_escape(get, image) + self.send_image_ws(get, msg="{}Scan completed".format(self.image_name), end=True) + + def scan_history(self, get, image): + """ + @name 异常历史命令 + @author lwh@aapanel.com + @time 2024-01-22 + """ + instruct_set = ( + "FROM", "CMD", "RUN", "LABEL", "MAINTAINER", "EXPOSE", "ENV", "ADD", "COPY", "ENTRYPOINT", "VOLUME", "USER", + "WORKDIR", "ARG", "ONBUILD", "STOPSIGNAL", "HEALTHCHECK", "SHELL") + rules = { + "rules": [{"description": "Miner Repo", "instruct": "RUN", "match": ".*(xmrig|ethminer|miner)\\.git.*"}, + {"description": "Unsafe Path", "instruct": "ENV", "match": "PATH=.*(|:)(/tmp|/dev/shm)"}]} + + ocispec = image.ocispec_v1() + if 'history' in ocispec.keys() and len(ocispec['history']) > 0: + for history in ocispec['history']: + if 'created_by' in history.keys(): + created_by = history['created_by'] + created_by_split = created_by.split("#(nop)") + if len(created_by_split) > 1: + command = "#(nop)".join(created_by_split[1:]) + command = command.lstrip() + command_split = command.split() + if len(command_split) == 2: + instruct = command_split[0] + command_content = command_split[1] + for r in rules["rules"]: + if r["instruct"] == instruct: + if re.match(r["match"], command_content): + self.send_image_ws(get, msg="Suspicious abnormal history command found", detail="It was found that the image has an abnormal historical command [{}], which may implant malware or code into the host system when the container is running, causing security risks.".format(self.short_string(command_content)), repair="1.It is recommended to check whether the command is required for normal business
                                    2.It is recommended to choose official and reliable infrastructure to avoid unnecessary losses.") + break + else: + instruct = command_split[0] + command_content = " ".join(command_split[1:]) + for r in rules["rules"]: + if r["instruct"] == instruct: + if re.match(r["match"], command_content): + self.send_image_ws(get, msg="Suspicious abnormal history command found", detail="It was found that the image has an abnormal historical command [{}], which may implant malware or code into the host system when the container is running, causing security risks.".format(self.short_string(command_content)), repair="1.It is recommended to check whether the command is required for normal business
                                    2.It is recommended to choose official and reliable infrastructure to avoid unnecessary losses.") + break + else: + command_split = created_by.split() + if command_split[0] in instruct_set: + for r in rules["rules"]: + if r["instruct"] == command_split[0]: + if re.match(r["match"], " ".join(command_split[1:])): + self.send_image_ws(get, msg="Suspicious abnormal history command found", detail="It was found that the image has an abnormal historical command [{}], which may implant malware or code into the host system when the container is running, causing security risks.".format(self.short_string(" ".join(command_split[1:]))), repair="1.It is recommended to check whether the command is required for normal business
                                    2.It is recommended to choose official and reliable infrastructure to avoid unnecessary losses.") + break + else: + for r in rules["rules"]: + if r["instruct"] == "RUN": + if re.match(r["match"], created_by): + self.send_image_ws(get, msg="Suspicious abnormal history command found", detail="It was found that the image has an abnormal historical command [{}], which may implant malware or code into the host system when the container is running, causing security risks.".format(self.short_string(created_by)), repair="1.It is recommended to check whether the command is required for normal business
                                    2.It is recommended to choose official and reliable infrastructure to avoid unnecessary losses.") + break + + def scan_sensitive(self, get, image): + """ + @name 扫描镜像敏感数据 + @author lwh<2024-1-22> + """ + # 敏感数据规则 + rules = {"whitelist": { + "paths": ["/usr/**", "/lib/**", "/lib32/**", "/bin/**", "/sbin/**", "/var/lib/**", "/var/log/**", + "**/node_modules/**/*.md", "**/node_modules/**/test/**", "**/service/iam/examples_test.go", + "**/grafana/public/build/*.js"]}, "rules": [ + {"id": 1, "name": "gitlab_personal_access_token", "description": "GitLab Personal Access Token", + "match": "glpat-[0-9a-zA-Z_\\-]{20}", "level": "high"}, + {"id": 2, "name": "AWS", "description": "AWS Access Token", "match": "AKIA[0-9A-Z]{16}", "level": "high"}, + {"id": 3, "name": "PKCS8 private key", "description": "PKCS8 private key", + "match": "-----BEGIN PRIVATE KEY-----", "level": "high"}, + {"id": 4, "name": "RSA private key", "description": "RSA private key", + "match": "-----BEGIN RSA PRIVATE KEY-----", "level": "high"}, + {"id": 5, "name": "SSH private key", "description": "SSH private key", + "match": "-----BEGIN OPENSSH PRIVATE KEY-----", "level": "high"}, + {"id": 6, "name": "PGP private key", "description": "PGP private key", + "match": "-----BEGIN PGP PRIVATE KEY BLOCK-----", "level": "high"}, + {"id": 7, "name": "Github Personal Access Token", "description": "Github Personal Access Token", + "match": "ghp_[0-9a-zA-Z]{36}", "level": "high"}, + {"id": 8, "name": "Github OAuth Access Token", "description": "Github OAuth Access Token", + "match": "gho_[0-9a-zA-Z]{36}", "level": "high"}, + {"id": 9, "name": "SSH (DSA) private key", "description": "SSH (DSA) private key", + "match": "-----BEGIN DSA PRIVATE KEY-----", "level": "high"}, + {"id": 10, "name": "SSH (EC) private key", "description": "SSH (EC) private key", + "match": "-----BEGIN EC PRIVATE KEY-----", "level": "high"}, + {"id": 11, "name": "Github App Token", "description": "Github App Token", + "match": "(ghu|ghs)_[0-9a-zA-Z]{36}", "level": "high"}, + {"id": 12, "name": "Github Refresh Token", "description": "Github Refresh Token", + "match": "ghr_[0-9a-zA-Z]{76}", "level": "high"}, + {"id": 13, "name": "Shopify shared secret", "description": "Shopify shared secret", + "match": "shpss_[a-fA-F0-9]{32}", "level": "high"}, + {"id": 14, "name": "Shopify access token", "description": "Shopify access token", + "match": "shpat_[a-fA-F0-9]{32}", "level": "high"}, + {"id": 15, "name": "Shopify custom app access token", "description": "Shopify custom app access token", + "match": "shpca_[a-fA-F0-9]{32}", "level": "high"}, + {"id": 16, "name": "Shopify private app access token", "description": "Shopify private app access token", + "match": "shppa_[a-fA-F0-9]{32}", "level": "high"}, + {"id": 17, "name": "Slack token", "description": "Slack token", "match": "xox[baprs]-([0-9a-zA-Z]{10,48})?", + "level": "high"}, + {"id": 18, "name": "Stripe", "description": "Stripe", "match": "(?i)(sk|pk)_(test|live)_[0-9a-z]{10,32}", + "level": "high"}, {"id": 19, "name": "PyPI upload token", "description": "PyPI upload token", + "match": "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}", "level": "high"}, + {"id": 20, "name": "Google (GCP) Service-account", "description": "Google (GCP) Service-account", + "match": "\\\"type\\\": \\\"service_account\\\"", "level": "medium"}, + {"id": 21, "name": "Password in URL", "description": "Password in URL", + "match": "[a-zA-Z]{3,10}:\\/\\/[^$][^:@\\/\\n]{3,20}:[^$][^:@\\n\\/]{3,40}@.{1,100}", "level": "high"}, + {"id": 22, "name": "Heroku API Key", "description": "Heroku API Key", + "match": "(?i)(?:heroku)(?:[0-9a-z\\-_\\t .]{0,20})(?:[\\s|']|[\\s|\"]){0,3}(?:=|>|:=|\\|\\|:|<=|=>|:)(?:'|\\\"|\\s|=|\\x60){0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:['|\\\"|\\n|\\r|\\s|\\x60]|$)", + "level": "high"}, {"id": 23, "name": "Slack Webhook", "description": "Slack Webhook", + "match": "https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}", + "level": "medium"}, + {"id": 24, "name": "Twilio API Key", "description": "Twilio API Key", "match": "SK[0-9a-fA-F]{32}", + "level": "high"}, {"id": 25, "name": "Age secret key", "description": "Age secret key", + "match": "AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}", "level": "high"}, + {"id": 26, "name": "Facebook token", "description": "Facebook token", + "match": "(?i)(facebook[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-f0-9]{32})['\\\"]", + "level": "high"}, {"id": 27, "name": "Twitter token", "description": "Twitter token", + "match": "(?i)(twitter[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-f0-9]{35,44})['\\\"]", + "level": "high"}, + {"id": 28, "name": "Adobe Client ID (Oauth Web)", "description": "Adobe Client ID (Oauth Web)", + "match": "(?i)(adobe[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-f0-9]{32})['\\\"]", + "level": "medium"}, {"id": 29, "name": "Adobe Client Secret", "description": "Adobe Client Secret", + "match": "(p8e-)(?i)[a-z0-9]{32}", "level": "high"}, + {"id": 30, "name": "Alibaba AccessKey ID", "description": "Alibaba AccessKey ID", + "match": "(LTAI5t)(?i)[a-z0-9]{18}", "level": "medium", "lock": True}, + {"id": 31, "name": "Alibaba Secret Key", "description": "Alibaba Secret Key", + "match": "(?i)(alibaba[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{30})['\\\"]", + "level": "high"}, {"id": 32, "name": "Asana Client ID", "description": "Asana Client ID", + "match": "(?i)(asana[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([0-9]{16})['\\\"]", + "level": "medium"}, + {"id": 33, "name": "Asana Client Secret", "description": "Asana Client Secret", + "match": "(?i)(asana[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{32})['\\\"]", + "level": "high"}, {"id": 34, "name": "Atlassian API token", "description": "Atlassian API token", + "match": "(?i)(atlassian[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{24})['\\\"]", + "level": "high"}, + {"id": 35, "name": "Bitbucket client ID", "description": "Bitbucket client ID", + "match": "(?i)(bitbucket[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{32})['\\\"]", + "level": "medium"}, {"id": 36, "name": "Bitbucket client secret", "description": "Bitbucket client secret", + "match": "(?i)(bitbucket[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9_\\-]{64})['\\\"]", + "level": "high"}, + {"id": 37, "name": "Beamer API token", "description": "Beamer API token", + "match": "(?i)(beamer[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"](b_[a-z0-9=_\\-]{44})['\\\"]", + "level": "high"}, {"id": 38, "name": "Clojars API token", "description": "Clojars API token", + "match": "(CLOJARS_)(?i)[a-z0-9]{60}", "level": "high"}, + {"id": 39, "name": "Contentful delivery API token", "description": "Contentful delivery API token", + "match": "(?i)(contentful[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9\\-=_]{43})['\\\"]", + "level": "high"}, + {"id": 40, "name": "Contentful preview API token", "description": "Contentful preview API token", + "match": "(?i)(contentful[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9\\-=_]{43})['\\\"]", + "level": "high"}, {"id": 41, "name": "Databricks API token", "description": "Databricks API token", + "match": "dapi[a-h0-9]{32}", "level": "high"}, + {"id": 42, "name": "Discord API key", "description": "Discord API key", + "match": "(?i)(discord[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-h0-9]{64})['\\\"]", + "level": "high"}, {"id": 43, "name": "Discord client ID", "description": "Discord client ID", + "match": "(?i)(discord[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([0-9]{18})['\\\"]", + "level": "medium"}, + {"id": 44, "name": "Discord client secret", "description": "Discord client secret", + "match": "(?i)(discord[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9=_\\-]{32})['\\\"]", + "level": "high"}, {"id": 45, "name": "Doppler API token", "description": "Doppler API token", + "match": "['\\\"](dp\\.pt\\.)(?i)[a-z0-9]{43}['\\\"]", "level": "high"}, + {"id": 46, "name": "Dropbox API secret/key", "description": "Dropbox API secret/key", + "match": "(?i)(dropbox[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{15})['\\\"]", + "level": "high"}, + {"id": 47, "name": "Dropbox short lived API token", "description": "Dropbox short lived API token", + "match": "(?i)(dropbox[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"](sl\\.[a-z0-9\\-=_]{135})['\\\"]", + "level": "high"}, + {"id": 48, "name": "Dropbox long lived API token", "description": "Dropbox long lived API token", + "match": "(?i)(dropbox[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"][a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\\-_=]{43}['\\\"]", + "level": "high"}, {"id": 49, "name": "Duffel API token", "description": "Duffel API token", + "match": "['\\\"]duffel_(test|live)_(?i)[a-z0-9_-]{43}['\\\"]", "level": "high"}, + {"id": 50, "name": "Dynatrace API token", "description": "Dynatrace API token", + "match": "['\\\"]dt0c01\\.(?i)[a-z0-9]{24}\\.[a-z0-9]{64}['\\\"]", "level": "high"}, + {"id": 51, "name": "EasyPost API token", "description": "EasyPost API token", + "match": "['\\\"]EZAK(?i)[a-z0-9]{54}['\\\"]", "level": "high"}, + {"id": 52, "name": "EasyPost test API token", "description": "EasyPost test API token", + "match": "['\\\"]EZTK(?i)[a-z0-9]{54}['\\\"]", "level": "high"}, + {"id": 53, "name": "Fastly API token", "description": "Fastly API token", + "match": "(?i)(fastly[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9\\-=_]{32})['\\\"]", + "level": "high"}, {"id": 54, "name": "Finicity client secret", "description": "Finicity client secret", + "match": "(?i)(finicity[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{20})['\\\"]", + "level": "high"}, + {"id": 55, "name": "Finicity API token", "description": "Finicity API token", + "match": "(?i)(finicity[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-f0-9]{32})['\\\"]", + "level": "high"}, {"id": 56, "name": "Flutterweave public key", "description": "Flutterweave public key", + "match": "(?i)FLWPUBK_TEST-[a-h0-9]{32}-X", "level": "medium"}, + {"id": 57, "name": "Flutterweave secret key", "description": "Flutterweave secret key", + "match": "(?i)FLWSECK_TEST-[a-h0-9]{32}-X", "level": "high"}, + {"id": 58, "name": "Flutterweave encrypted key", "description": "Flutterweave encrypted key", + "match": "FLWSECK_TEST[a-h0-9]{12}", "level": "high"}, + {"id": 59, "name": "Frame.io API token", "description": "Frame.io API token", + "match": "fio-u-(?i)[a-z0-9-_=]{64}", "level": "high"}, + {"id": 60, "name": "GoCardless API token", "description": "GoCardless API token", + "match": "['\\\"]live_(?i)[a-z0-9-_=]{40}['\\\"]", "level": "high"}, + {"id": 61, "name": "Grafana API token", "description": "Grafana API token", + "match": "['\\\"]eyJrIjoi(?i)[a-z0-9-_=]{72,92}['\\\"]", "level": "high"}, + {"id": 62, "name": "Hashicorp Terraform user/org API token", + "description": "Hashicorp Terraform user/org API token", + "match": "['\\\"](?i)[a-z0-9]{14}\\.atlasv1\\.[a-z0-9-_=]{60,70}['\\\"]", "level": "high"}, + {"id": 63, "name": "Hashicorp Vault batch token", "description": "Hashicorp Vault batch token", + "match": "b\\.AAAAAQ[0-9a-zA-Z_-]{156}", "level": "high"}, + {"id": 64, "name": "Hubspot API token", "description": "Hubspot API token", + "match": "(?i)(hubspot[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\\\"]", + "level": "high"}, {"id": 65, "name": "Intercom API token", "description": "Intercom API token", + "match": "(?i)(intercom[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9=_]{60})['\\\"]", + "level": "high"}, + {"id": 66, "name": "Intercom client secret/ID", "description": "Intercom client secret/ID", + "match": "(?i)(intercom[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\\\"]", + "level": "high"}, + {"id": 67, "name": "Ionic API token", "description": "Ionic API token", "match": "ion_(?i)[a-z0-9]{42}", + "level": "high"}, {"id": 68, "name": "Linear API token", "description": "Linear API token", + "match": "lin_api_(?i)[a-z0-9]{40}", "level": "high"}, + {"id": 69, "name": "Linear client secret/ID", "description": "Linear client secret/ID", + "match": "(?i)(linear[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-f0-9]{32})['\\\"]", + "level": "high"}, {"id": 70, "name": "Lob API Key", "description": "Lob API Key", + "match": "(?i)(lob[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]((live|test)_[a-f0-9]{35})['\\\"]", + "level": "high"}, + {"id": 71, "name": "Lob Publishable API Key", "description": "Lob Publishable API Key", + "match": "(?i)(lob[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]((test|live)_pub_[a-f0-9]{31})['\\\"]", + "level": "high"}, {"id": 72, "name": "Mailchimp API key", "description": "Mailchimp API key", + "match": "(?i)(mailchimp[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-f0-9]{32}-us20)['\\\"]", + "level": "high"}, + {"id": 73, "name": "Mailgun private API token", "description": "Mailgun private API token", + "match": "(?i)(mailgun[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"](key-[a-f0-9]{32})['\\\"]", + "level": "high"}, + {"id": 74, "name": "Mailgun public validation key", "description": "Mailgun public validation key", + "match": "(?i)(mailgun[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"](pubkey-[a-f0-9]{32})['\\\"]", + "level": "high"}, + {"id": 75, "name": "Mailgun webhook signing key", "description": "Mailgun webhook signing key", + "match": "(?i)(mailgun[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})['\\\"]", + "level": "high"}, {"id": 76, "name": "Mapbox API token", "description": "Mapbox API token", + "match": "(?i)(pk\\.[a-z0-9]{60}\\.[a-z0-9]{22})", "level": "high"}, + {"id": 77, "name": "messagebird-api-token", "description": "MessageBird API token", + "match": "(?i)(messagebird[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{25})['\\\"]", + "level": "high"}, + {"id": 78, "name": "MessageBird API client ID", "description": "MessageBird API client ID", + "match": "(?i)(messagebird[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\\\"]", + "level": "medium"}, {"id": 79, "name": "New Relic user API Key", "description": "New Relic user API Key", + "match": "['\\\"](NRAK-[A-Z0-9]{27})['\\\"]", "level": "high"}, + {"id": 80, "name": "New Relic user API ID", "description": "New Relic user API ID", + "match": "(?i)(newrelic[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([A-Z0-9]{64})['\\\"]", + "level": "medium"}, {"id": 81, "name": "New Relic ingest browser API token", + "description": "New Relic ingest browser API token", + "match": "['\\\"](NRJS-[a-f0-9]{19})['\\\"]", "level": "high"}, + {"id": 82, "name": "npm access token", "description": "npm access token", + "match": "['\\\"](npm_(?i)[a-z0-9]{36})['\\\"]", "level": "high"}, + {"id": 83, "name": "Planetscale password", "description": "Planetscale password", + "match": "pscale_pw_(?i)[a-z0-9\\-_\\.]{43}", "level": "high"}, + {"id": 84, "name": "Planetscale API token", "description": "Planetscale API token", + "match": "pscale_tkn_(?i)[a-z0-9\\-_\\.]{43}", "level": "high"}, + {"id": 85, "name": "Postman API token", "description": "Postman API token", + "match": "PMAK-(?i)[a-f0-9]{24}\\-[a-f0-9]{34}", "level": "high"}, + {"id": 86, "name": "Pulumi API token", "description": "Pulumi API token", "match": "pul-[a-f0-9]{40}", + "level": "high"}, {"id": 87, "name": "Rubygem API token", "description": "Rubygem API token", + "match": "rubygems_[a-f0-9]{48}", "level": "high"}, + {"id": 88, "name": "Sendgrid API token", "description": "Sendgrid API token", + "match": "SG\\.(?i)[a-z0-9_\\-\\.]{66}", "level": "high"}, + {"id": 89, "name": "Sendinblue API token", "description": "Sendinblue API token", + "match": "xkeysib-[a-f0-9]{64}\\-(?i)[a-z0-9]{16}", "level": "high"}, + {"id": 90, "name": "Shippo API token", "description": "Shippo API token", + "match": "shippo_(live|test)_[a-f0-9]{40}", "level": "high"}, + {"id": 91, "name": "Linkedin Client secret", "description": "Linkedin Client secret", + "match": "(?i)(linkedin[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z]{16})['\\\"]", + "level": "high"}, {"id": 92, "name": "Linkedin Client ID", "description": "Linkedin Client ID", + "match": "(?i)(linkedin[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{14})['\\\"]", + "level": "medium"}, + {"id": 93, "name": "Twitch API token", "description": "Twitch API token", + "match": "(?i)(twitch[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-z0-9]{30})['\\\"]", + "level": "high"}, {"id": 94, "name": "Typeform API token", "description": "Typeform API token", + "match": "(?i)(typeform[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}(tfp_[a-z0-9\\-_\\.=]{59})", + "level": "high"}, + {"id": 95, "name": "Social Security Number", "description": "Social Security Number", + "match": "\\d{3}-\\d{2}-\\d{4}", "level": "low"}, + {"id": 96, "name": "Version Control File", "description": "Version Control File", + "filepath": ".*\\/\\.(git|svn)$", "level": "high"}, + {"id": 97, "name": "Config File", "description": "Config File", "filepath": ".*\\/config\\.ini$", + "level": "medium"}, + {"id": 99, "name": "Desktop Services Store", "description": "Desktop Services Store", + "filepath": " .*\\/\\.DS_Store$", "level": "low"}, + {"id": 100, "name": "MySQL client command history file", "description": "MySQL client command history file", + "filepath": ".*\\/\\.(mysql|psql|irb)_history$", "level": "low"}, + {"id": 101, "name": "Recon-ng web reconnaissance framework API key database", + "description": "Recon-ng web reconnaissance framework API key database", + "filepath": ".*\\/\\.recon-ng\\/keys\\.db$", "level": "medium"}, + {"id": 102, "name": "DBeaver SQL database manager configuration file", + "description": "DBeaver SQL database manager configuration file", + "filepath": ".*\\/\\.dbeaver-data-sources\\.xml$", "level": "low"}, + {"id": 103, "name": "S3cmd configuration file", "description": "S3cmd configuration file", + "filepath": ".*\\/\\.s3cfg$", "level": "low"}, + {"id": 104, "name": "Ruby On Rails secret token configuration file", + "description": "If the Rails secret token is known, it can allow for remote code execution. (http://www.exploit-db.com/exploits/27527/)", + "filepath": ".*\\/secret_token\\.rb$", "level": "high"}, {"id": 105, "name": "OmniAuth configuration file", + "description": "The OmniAuth configuration file might contain client application secrets.", + "filepath": ".*\\/omniauth\\.rb$", + "level": "high"}, + {"id": 106, "name": "Carrierwave configuration file", + "description": "Can contain credentials for online storage systems such as Amazon S3 and Google Storage.", + "filepath": ".*\\/carrierwave\\.rb$", "level": "high"}, + {"id": 107, "name": "Potential Ruby On Rails database configuration file", + "description": "Might contain database credentials.", "filepath": ".*\\/database\\.yml$", "level": "high"}, + {"id": 108, "name": "Django configuration file", + "description": "Might contain database credentials, online storage system credentials, secret keys, etc.", + "filepath": ".*\\/settings\\.py$", "level": "low"}, + {"id": 109, "name": "PHP configuration file", "description": "Might contain credentials and keys.", + "filepath": ".*\\/config(\\.inc)?\\.php$", "level": "low"}, + {"id": 110, "name": "Jenkins publish over SSH plugin file", + "description": "Jenkins publish over SSH plugin file", + "filepath": ".*\\/jenkins\\.plugins\\.publish_over_ssh\\.BapSshPublisherPlugin\\.xml$", "level": "high"}, + {"id": 111, "name": "Potential Jenkins credentials file", + "description": "Potential Jenkins credentials file", "filepath": ".*\\/credentials\\.xml$", + "level": "high"}, {"id": 112, "name": "Apache htpasswd file", "description": "Apache htpasswd file", + "filepath": ".*\\/\\.htpasswd$", "level": "low"}, + {"id": 113, "name": "Configuration file for auto-login process", + "description": "Might contain username and password.", "filepath": ".*\\/\\.(netrc|git-credentials)$", + "level": "high"}, {"id": 114, "name": "Potential MediaWiki configuration file", + "description": "Potential MediaWiki configuration file", + "filepath": ".*\\/LocalSettings\\.php$", "level": "high"}, + {"id": 115, "name": "Rubygems credentials file", + "description": "Might contain API key for a rubygems.org account.", + "filepath": ".*\\/\\.gem\\/credentials$", "level": "high"}, + {"id": 116, "name": "Potential MSBuild publish profile", "description": "Potential MSBuild publish profile", + "filepath": ".*\\/\\.pubxml(\\.user)?$", "level": "low"}, + {"id": 117, "name": "Potential Tencent Accesskey", "description": "Might contain Tencent Accesskey", + "match": "AKID(?i)[a-z0-9]{32}", "level": "high"}, + {"id": 118, "name": "Potential aws Accesskey", "description": "Might contain aws Accesskey", + "match": "(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}", "level": "high"}, + {"id": 119, "name": "Potential UCloud Accesskey", "description": "Might contain UCloud Accesskey", + "match": "JDC_[A-z,0-9]{28}", "level": "high"}, + {"id": 120, "name": "JWT TOKEN", "description": "Might JWT Token ", + "match": "ey[0-9a-zA-Z]{30,34}\\.ey[0-9a-zA-Z-\\/_]{30,500}\\.[0-9a-zA-Z-\\/_]{10,200}={0,2}", + "level": "medium"}, {"id": 121, "name": "Google API", "description": "Might Google API Key ", + "match": "AIza[0-9A-Za-z\\-_]{35}", "level": "medium"}, + {"id": 122, "name": "gitlab_pipeline_trigger_token", "description": "GitLab Pipeline Trigger Token", + "match": "glptt-[0-9a-zA-Z_\\-]{20}", "level": "high"}, + {"id": 123, "name": "gitlab_runner_registration_token", "description": "GitLab Runner Registration Token", + "match": "GR1348941[0-9a-zA-Z_\\-]{20}", "level": "high"}, + {"id": 124, "name": "Flutterwave public key", "description": "Flutterwave public key", + "match": "FLWPUBK_TEST-(?i)[a-h0-9]{32}-X", "level": "high"}, + {"id": 125, "name": "Flutterwave secret key", "description": "Flutterwave secret key", + "match": "FLWSECK_TEST-(?i)[a-h0-9]{32}-X", "level": "high"}, + {"id": 126, "name": "Flutterwave encrypted key", "description": "Flutterwave encrypted key", + "match": "FLWSECK_TEST[a-h0-9]{12}", "level": "high"}, + {"id": 127, "name": "github-app-token", "description": "GitHub App Token", + "match": "(ghu|ghs)_[0-9a-zA-Z]{36}", "level": "high"}, + {"id": 128, "name": "github-fine-grained-pat", "description": "GitHub Fine-Grained Personal Access Token", + "match": "github_pat_[0-9a-zA-Z_]{82}", "level": "high"}, + {"id": 129, "name": "grafana-cloud-api-token", "description": "Grafana cloud api token", + "match": "glc_[A-Za-z0-9+/]{32,400}={0,2}", "level": "high"}, + {"id": 130, "name": "grafana-service-account-token", "description": "Grafana service account token", + "match": "glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}", "level": "high"}, + {"id": 131, "name": "prefect-api-token", "description": "Prefect API token", "match": "pnu_[a-z0-9]{36}", + "level": "medium"}]} + + # refs = image.reporefs() + # if len(refs) > 0: + # ref = refs[0] + # else: + # ref = image.id() + # public.print_log("start scan sensitive: " + ref) + + # 检测docker历史(另外有扫描项) + # ocispec = image.ocispec_v1() + # if 'history' in ocispec.keys() and len(ocispec['history']) > 0: + # for history in ocispec['history']: + # command_content = history['created_by'] + # report_rule_list = [] + # for r in rules["rules"]: + # # 正则选择 可以选择docker history的正则是由哪些模块检测 + # for i in ['env', 'match', 'filepath']: + # regexp_s = r.get(i) + # if not regexp_s: + # continue + # if re.search(regexp_s, command_content, re.IGNORECASE): + # self.send_image_ws(get, msg="镜像OCI存在敏感信息", detail=r["description"]+"镜像OCI存在敏感信息{}".format(command_content), repair="使用此镜像可能存在危险,建议更换同类型镜像或是部署容器时清理敏感信息", status=2) + # if not report_rule_list: + # continue + # 检测env环境变量 + # ocispec = image.ocispec_v1() + # if 'config' in ocispec.keys() and 'Env' in ocispec['config'].keys(): + # env_list = image.ocispec_v1()['config']['Env'] + # for env in env_list: + # env_split = env.split("=") + # if len(env_split) >= 2: + # for r in rules["rules"]: + # if "env" in r.keys(): + # env_regex = r["env"] + # if re.match(env_regex, env, re.IGNORECASE): + # self.send_image_ws(get, msg="镜像OCI存在敏感信息", detail=r["description"]+"\nenv存在敏感信息,可能会造成数据泄露", repair="使用该镜像部署容器时,及时修改默认密码或者鉴权值,防止被黑客利用入侵", status=2) + # break + # 排除大型项目 + large_dir = ["usr", "lib"] + sensitive_dirs = [] + try: + for filename in image.listdir("/"): + if filename not in large_dir: + sensitive_dirs.append("/"+filename) + except Exception as e: + public.print_log("Failed to obtain mirror directory{}".format(e)) + sensitive_dirs = ["/"] + + # 检测存在敏感数据的路径 + for sensitive_dir in sensitive_dirs: + for root, dirs, files in image.walk(sensitive_dir): + # 短暂停留0.004s,防止占用太高影响其他接口响应 + time.sleep(0.004) + # 遍历深度不超过3 + if len(root.split("/")) > 3: + self.send_image_ws(get, msg=self.short_string1("Scanning:{}".format(root))) + # public.print_log("跳过{}".format(root)) + continue + for dir in dirs: + try: + dirpath = os.path.join(root, dir) + self.send_image_ws(get, msg="Scanning:{}...".format(dirpath)) + # public.print_log("扫描目录{}".format(dirpath)) + # detect filepath or filename + for r in rules["rules"]: + if "filepath" in r.keys(): + filepath_match_regex = r["filepath"] + if re.match(filepath_match_regex, dirpath): + self.send_image_ws(get, msg="Sensitive directories found{}".format(dirpath), detail="The image exists in a sensitive directory:{}, may be used by attackers to steal sensitive data or source code, leading to further security issues.".format(dirpath), repair="1. Enter the container deployed using the image and delete the directory without affecting the business
                                    2. If it cannot be deleted, restrict access to the directory".format(dirpath), status=2) + break + except Exception as e: + public.print_log("Match sensitive information to catch exceptions{}".format(e)) + for filename in files: + try: + filepath = os.path.join(root, filename) + self.send_image_ws(get, msg="Scanning:{}...".format(filename)) + # public.print_log("扫描文件{}".format(filepath)) + # 跳过白名单 + whitelist = rules["whitelist"] + white_match = False + white_paths = whitelist["paths"] + for wp in white_paths: + if fnmatch.filter([filepath], wp): + white_match = True + break + if white_match: + continue + + try: + # 跳过非常规文件,超过10m + f_stat = image.stat(filepath) + if not S_ISREG(f_stat.st_mode): + continue + if f_stat.st_size > 10 * 1024 * 1024: + continue + + f = image.open(filepath, mode="rb") + f_content_byte = f.read() + except FileNotFoundError as e: + public.print_log("Error while traversing sensitive files:{}".format(e)) + continue + except BaseException as e: + public.print_log("Error while traversing sensitive files:{}".format(e)) + continue + # 检测文件路径及文件名 + match = False + for r in rules["rules"]: + if "filepath" in r.keys(): + filepath_match_regex = r["filepath"] + if re.match(filepath_match_regex, filepath): + match = True + self.send_image_ws(get, msg="Sensitive files found{}".format(filepath), detail=r["description"] + ":
                                    It was found that the image contains sensitive files {}, which may cause leakage.".format(filepath), repair="1. Enter the container deployed using the image, and it is recommended to delete the file if it does not affect the business
                                    2. If it cannot be deleted, restrict the access rights of the file
                                    3. Use a password to protect sensitive files from being easily accessed read", status=2) + break + if match: + continue + # chardet_guess = chardet.detect(f_content_byte[0:64]) + # if chardet_guess["encoding"] != None: + # try: + # f_content = f_content_byte.decode(chardet_guess["encoding"]) + # except: + # continue + # else: + # f_content = str(f_content_byte) + # mime_guess = magic.from_buffer(f_content_byte, mime=True) + # for r in rules["rules"]: + # # mime + # mime_find = False + # if "mime" in r.keys(): + # if r["mime"] == mime_guess: + # mime_find = True + # else: + # if mime_guess.startswith("text/"): + # mime_find = True + # if mime_find: + # if "match" in r.keys(): + # match = r["match"] + # if match.startswith("$contains:"): + # keyword = match.lstrip("$contains:") + # if keyword in f_content: + # file_stat = image.stat(filepath) + # self.send_image_ws(get, msg="镜像路径{}存在敏感信息".format(filepath), + # repair="建议使用该镜像部署容器后,检查该文件是否有用,无用则清理", + # status=2) + # + # else: + # if re.match(match, f_content): + # file_stat = image.stat(filepath) + # self.send_image_ws(get, msg="镜像路径{}存在敏感信息".format(filepath), + # repair="建议使用该镜像部署容器后,检查该文件是否有用,无用则清理", + # status=2) + except Exception as e: + public.print_log("Error while traversing sensitive files:{}".format(e)) + + def scan_backdoor(self, get, image): + """ + @name 扫描后门 + @author lwh<2024-1-23> + """ + backdoor_regex_list1 = [ + # reverse shell + r'''(nc|ncat|netcat)\b.*(-e|--exec|-c)\b.*?\b(ba|da|z|k|c|a|tc|fi|sc)?sh\b''', + r'''python\w*\b.*\bsocket\b.*?\bconnect\b.*?\bsubprocess\b.*?\bsend\b.*?\bstdout\b.*?\bread\b''', + r'''python\w*\b.*\bsocket\b.*?\bconnect\b.*?\bos\.dup2\b.*?\b(call|spawn|popen)\b\s*\([^)]+?\b(ba|da|z|k|c|a|tc|fi|sc)?sh\b''', + r'''(sh|bash|dash|zsh)\b.*-c\b.*?\becho\b.*?\bsocket\b.*?\bwhile\b.*?\bputs\b.*?\bflush\b.*?\|\s*tclsh\b''', + r'''(sh|bash|dash|zsh)\b.*-c\b.*?\btelnet\b.*?\|&?.*?\b(ba|da|z|k|c|a|tc|fi|sc)?sh\b.*?\|&?\s*telnet\b''', + r'''(sh|bash|dash|zsh)\b.*-c\b.*?\bcat\b.*?\|&?.*?\b(ba|da|z|k|c|a|tc|fi|sc)?sh\b.*?\|\s*(nc|ncat)\b''', + r'''(sh|bash|dash|zsh)\b.*sh\s+(-i)?\s*>&?\s*/dev/(tcp|udp)/.*?/\d+\s+0>&\s*(1|2)''', + ] + + def bashrc(): + """ + @name bashrc后门检测 + """ + backdoor_regex_list = [r'''alias\s+ssh=[\'\"]{0,1}strace''', r'''alias\s+sudo='''] + bashrc_dirs = ["/home", "/root"] + for bashrc_dir in bashrc_dirs: + for root, dirs, files in image.walk(bashrc_dir): + for file in files: + if re.match(r'''^\.[\w]*shrc$''', file): + filepath = os.path.join(root, file) + else: + continue + try: + f = image.open(filepath, mode="r") + f_content = f.read() + for backdoor_regex in backdoor_regex_list: + if re.search(backdoor_regex, f_content): + self.send_image_ws(get, msg="Found bashrc backdoor{}".format(filepath), detail="It was found that the image has bashrc backdoor: [{}], malicious code content:
                                    {}".format(filepath, self.short_string(f_content)), repair="1. Enter the container deployed using the image and delete the malicious code under the file
                                    2. Check whether the container has been invaded, and update the access token or account password of the business in the container
                                    3. It is recommended to replace the official image or Other trusted image deployment containers", status=3) + for backdoor_regex in backdoor_regex_list1: + if re.search(backdoor_regex, f_content): + self.send_image_ws(get, msg="Backdoor file found{}".format(filepath), detail="It was found that the bashrc backdoor file [{}] exists in the image, and the malicious code content is:
                                    {}".format(filepath, self.short_string(f_content)), repair="1. Enter the container deployed using the image and delete the malicious code under the file
                                    2. Check whether the container has been invaded, and update the access token or account password of the business in the container
                                    3. It is recommended to replace the official image or Other trusted image deployment containers", status=3) + except FileNotFoundError: + continue + except BaseException as e: + public.print_log(e) + + def crontab(): + """ + @name crontab后门检测 + """ + cron_list = ["/etc/crontab", "/etc/cron.hourly", "/etc/cron.daily", "/etc/cron.weekly", "/etc/cron.monthly", + "/etc/cron.d"] + environment_regex = r'''[a-zA-Z90-9]+\s*=\s*[^\s]+$''' + cron_regex = r'''((\d{1,2}|\*)\s+){5}[a-zA-Z0-9]+\s+(.*)''' + backdoor_regex_list = [ + # download + r'''^(wget|curl)\b''' + # mrig + r'''^([\w0-9]*mrig[\w0-9]*)\b''' + ] + + def detect_crontab_content(cron_f): + """ + @name 检查crontab内容 + """ + result_dict = {} + for line in cron_f.readlines(): + # preprocess + line = line.strip() + line = line.replace("\n", "") + # environment + if re.match(environment_regex, line): continue + m = re.match(cron_regex, line) + if m: + if len(m.groups()) == 3: + cmdline1 = m.group(3) + # for backdoor_regex in backdoor_regex_list: + # if re.search(backdoor_regex, cmdline1): + # result_dict[backdoor_regex] = cmdline1 + for backdoor_regex in backdoor_regex_list1: + if re.search(backdoor_regex, cmdline1): + self.send_image_ws(get, msg="cron backdoor discovered{}".format(filepath), + detail="It was found that the cron backdoor [{}] exists in the image, and the malicious code content is:
                                    {}".format( + filepath, self.short_string(cmdline1)), + repair="1. Enter the container deployed using the image and delete the malicious code under the file
                                    2. Check whether the container has been invaded, and update the access token or account password of the business in the container
                                    3. It is recommended to replace the official image or Other trusted image deployment containers", + status=3) + result_dict[backdoor_regex] = cmdline1 + else: continue + return result_dict + + for cron in cron_list: + try: + # filetype + cron_stat = image.stat(cron) + if S_ISDIR(cron_stat.st_mode): + for root, dirs, files in image.walk(cron): + for file in files: + filepath = os.path.join(root, file) + with image.open(filepath) as f: + result_dict = detect_crontab_content(f) + # if len(result_dict) > 0: + # for regex, cmdline in result_dict.items(): + # self.send_image_ws(get, msg="cron backdoor discovered{}".format(filepath), detail="镜像发现crontab后门:{}".format(filepath), repair="文件命中恶意特征{},建议删除此镜像,并及时排查相关使用该镜像部署的容器,删除文件中的恶意代码".format(cmdline), status=3) + elif S_ISREG(cron_stat.st_mode): + with image.open(cron) as f: + result_dict = detect_crontab_content(f) + # if len(result_dict) > 0: + # for regex, cmdline in result_dict.items(): + # self.send_image_ws(get, msg="cron backdoor discovered{}".format(cron), detail="发现crontab后门:{}".format(cron), + # repair="文件命中恶意特征{},建议删除此镜像,并及时排查相关使用该镜像部署的容器,删除文件中的恶意代码".format( + # cmdline), status=3) + except FileNotFoundError: + continue + + def service(): + """ + @name 服务后门 + """ + service_dir_list = ["/etc/systemd/system"] + for service_dir in service_dir_list: + for root, dirs, files in image.walk(service_dir): + for file in files: + try: + filepath = os.path.join(root, file) + f = image.open(filepath, mode="r") + f_content = f.read() + for backdoor_regex in backdoor_regex_list1: + if re.search(backdoor_regex, f_content): + self.send_image_ws(get, msg="Found system backdoor:{}".format(filepath), + detail="It was found that the systemd backdoor file [{}] exists in the image, and the malicious code content is:
                                    {}".format( + filepath, self.short_string(f_content)), + repair="1. Enter the container deployed using the image and delete the malicious code under the file
                                    2. Check whether the container has been invaded, and update the access token or account password of the business in the container
                                    3. It is recommended to replace the official image or Other trusted image deployment containers", + status=3) + except FileNotFoundError: + continue + except BaseException as e: + public.print_log(e) + + def sshd(): + """ + @name sshd软链接后门检测,支持检测常规软连接后门 + """ + rootok_list = ("su", "chsh", "chfn", "runuser") + sshd_dirs = ["/home", "/root", "/tmp"] + for sshd_dir in sshd_dirs: + for root, dirs, files in image.walk(sshd_dir): + for f in files: + try: + filepath = os.path.join(root, f) + f_lstat = image.lstat(filepath) + if S_ISLNK(f_lstat.st_mode): + f_link = image.evalsymlink(filepath) + f_exename = filepath.split("/")[-1] + f_link_exename = f_link.split("/")[-1] + if f_exename in rootok_list and f_link_exename == "sshd": + self.send_image_ws(get, msg="Found sshd backdoor{}".format(filepath), detail="Found the sshd soft link backdoor: {}, the file hits the malicious feature [exe={};link_file={}]".format(filepath, f_exename, f_link), + repair="1. Enter the container deployed using the image and delete the malicious code under the file
                                    2. Check whether the container has been invaded, and update the access token or account password of the business in the container
                                    3. It is recommended to replace the official image or Other trusted image deployment containers", status=3) + except FileNotFoundError: + continue + except BaseException as e: + public.print_log(e) + + def tcpwrapper(): + """ + @name tcpwrapper后门检测 + """ + wrapper_config_file_list = ['/etc/hosts.allow', '/etc/hosts.deny'] + for config_filepath in wrapper_config_file_list: + try: + with image.open(config_filepath, mode="r") as f: + f_content = f.read() + for backdoor_regex in backdoor_regex_list1: + if re.search(backdoor_regex, f_content): + self.send_image_ws(get, msg="Found the tcpwrapper backdoor{}".format(config_filepath), detail="It was found that the tcpwrapper backdoor file [{}] exists in the image, and the malicious code content is:
                                    {}".format(config_filepath, self.short_string(f_content)), + repair="1. Enter the container deployed using the image and delete the malicious code under the file
                                    2. Check whether the container has been invaded, and update the access token or account password of the business in the container
                                    3. It is recommended to replace the official image or Other trusted image deployment containers", status=3) + except FileNotFoundError: + continue + except BaseException as e: + public.print_log(e) + + # 执行后门检测函数 + bashrc() + crontab() + service() + sshd() + tcpwrapper() + + def scan_privilege_escalation(self, get, image): + """ + @name 提权风险 + @author lwh<2024-01-24> + """ + + def scan_escape(self, get, image): + """ + @name 逃逸风险 + @author lwh<2024-01-24> + """ + def sudoers(): + """ + @name sudo逃逸 + """ + sudo_regex = r"(\w{1,})\s\w{1,}=\(.*\)\s(.*)" + unsafe_sudo_files = ["wget", "find", "cat", "apt", "zip", "xxd", "time", "taskset", "git", "sed", "pip", "tmux", "scp", "perl", "bash", "less", "awk", "man", "vim", "env", "ftp"] + try: + with image.open("/etc/sudoers", mode="r") as f: + lines = f.readlines() + for line in lines: + line = line.strip() + if line.startswith("#"): + continue + matches = re.findall(sudo_regex, line) + if len(matches) == 1: + user, sudo_command = matches[0] + if user.lower() in ["admin", "sudo", "root"]: + continue + for unsafe_sudo_file in unsafe_sudo_files: + if unsafe_sudo_file in sudo_command.lower(): + self.send_image_ws(get, msg="Users found to be at risk of escape{}".format(user), detail="The username {} may complete container escape through the command [{}], allowing the attacker to obtain access rights to the host or other containers. Malicious content:
                                    {}".format(user, sudo_command, line), status=3, repair="1.建议删除镜像或不再使用
                                    2.若已有业务使用该镜像,则进入容器环境,删除/etc/sudoers文件内包含用户{}的内容".format(user)) + break + except Exception as e: + # public.print_log(e) + return + # 开始检测 + sudoers() + + def scan_log4j2(self, get, image): + """ + @name 扫描是否存在log4j漏洞 + @author lwh<2024-01-26> + """ + +def veinmind(): + from veinmind import docker + client = docker.Docker() + ids = client.list_image_ids() + # public.print_log(ids) + for id in ids: + image = client.open_image_by_id(id) + # public.print_log("image id: " + image.id()) + for ref in image.reporefs(): + public.print_log("image ref: " + ref) + for repo in image.repos(): + public.print_log("image repo: " + repo) + # public.print_log("image ocispec: " + str(image.ocispec_v1())) + + +if __name__ == '__main__': + # obj = main() + # get = public.dict_obj() + # obj.get_safe_scan(get=get) + # sudo_regex = r"(\w{1,})\s\w{1,}=\(.*\)\s(.*)" + # unsafe_sudo_files = ["wget", "find", "cat", "apt", "zip", "xxd", "time", "taskset", "git", "sed", "pip", "ed", + # "tmux", "scp", "perl", "bash", "less", "awk", "man", "vi", "vim", "env", "ftp", "all"] + # try: + # with open("/tmp/sudoers.test", "r") as f: + # lines = f.readlines() + # for line in lines: + # line = line.strip() + # if line.startswith("#"): + # continue + # matches = re.findall(sudo_regex, line) + # if len(matches) == 1: + # user, sudo_file = matches[0] + # if user.lower() in ["admin", "sudo", "root"]: + # continue + # print(user.lower(), sudo_file.lower()) + # for unsafe_sudo_file in unsafe_sudo_files: + # if unsafe_sudo_file in sudo_file.lower(): + # print("用户有问题:{}".format(user)) + # break + # except Exception as e: + # print(e) + # 初始化安装检测SDK + try: + from veinmind import docker + except Exception as e: + # public.print_log("Importing veinmind failed:{}".format(e)) + requirements_list = ["veinmind"] + shell_command = "btpip install --no-dependencies {}".format(" ".join(requirements_list)) + public.ExecShell(shell_command) + sys_ver = public.get_os_version() + # self.send_image_ws(get, msg="正在初始化检测引擎中,首次加载耗时较长...", status=1) + if "Ubuntu" in sys_ver or "Debian" in sys_ver: + public.WriteFile("/etc/apt/sources.list.d/libveinmind.list", + "deb [trusted=yes] https://download.veinmind.tech/libveinmind/apt/ ./") + # public.print_log("Updating apt-get") + public.ExecShell("apt-get update") + time.sleep(1) + public.ExecShell("apt-get install -y libveinmind-dev") + time.sleep(1) + elif "CentOS" in sys_ver: + public.WriteFile("/etc/yum.repos.d/libveinmind.repo", """[libveinmind] +name=libVeinMind SDK yum repository +baseurl=https://download.veinmind.tech/libveinmind/yum/ +enabled=1 +gpgcheck=0""") + public.ExecShell("yum makecache", timeout=10) + public.ExecShell("yum install -y libveinmind-devel", timeout=10) + else: + pass + # public.print_log("不支持的系统版本") + # return public.returnMsg(False, "不支持的系统版本") + # self.send_image_ws(get, msg="正在检查依赖库是否存在...", status=1) + result, err = public.ExecShell("whereis libdl.so") + result = result.strip().split(" ") + # public.print_log("libdl.so库的情况:{}".format(result)) + if len(result) <= 1: + # public.print_log("缺少libdl.so库") + result, err = public.ExecShell("whereis libdl.so.2") + result = result.strip().split(" ") + # public.print_log("libdl.so.2库的情况:{}".format(result)) + if len(result) <= 1: + # public.print_log("缺少libdl.so库,需要安装libdl.so或libdl.so2") + public.returnMsg(False, "The libdl.so library is missing, you need to install libdl.so or libdl.so2") + else: + # 建立libdl.so软链接至libdl.so.2 + for lib in result[1:]: + ln_command = "ln -s {} {}".format(lib, lib[:-2]) + public.print_log("Soft link in progress:{}".format(ln_command)) + public.ExecShell(ln_command) + from veinmind import docker + # public.print_log("执行成功") + + diff --git a/class_v2/btdockerModelV2/setupModel.py b/class_v2/btdockerModelV2/setupModel.py new file mode 100644 index 00000000..a46d3b5b --- /dev/null +++ b/class_v2/btdockerModelV2/setupModel.py @@ -0,0 +1,686 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# Docker模型 +# ------------------------------ +import gettext +_ = gettext.gettext + +import json +import os + +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + + + +class main(dockerBase): + def get_config(self, get): + """ + 获取设置配置信息 + @param get: + @return: + """ + check_docker_compose = self.check_docker_compose_service() + try: + installing = public.M('tasks').where('name=? and status=?', ("Install Docker Service", "-1")).count() + if not installing: + installing = public.M('tasks').where('name=? and status=?', ("Install Docker Service", "-1")).count() + except: + installing = 0 + + # if not os.path.exists("/www/server/panel/data/db/docker.db"): + # public.ExecShell("mv -f /www/server/panel/data/docker.db /www/server/panel/data/db/docker.db") + + if not os.path.exists("/www/server/panel/data/docker.db"): + public.ExecShell("mv -f /www/server/panel/data/db/docker.db /www/server/panel/data/docker.db") + + service_status = self.get_service_status() + if not service_status: + service_status = self.get_service_status() + + data = { + "service_status": service_status, + "docker_installed": self.check_docker_service(), + "docker_compose_installed": check_docker_compose[0], + "docker_compose_path": check_docker_compose[1], + "monitor_status": self.get_monitor_status(), + "monitor_save_date": dp.docker_conf()['SAVE'], + "daemon_path": "/etc/docker/daemon.json", + "installing": installing, + } + return public.return_message(0, 0, data) + + @staticmethod + def _get_com_registry_mirrors(): + """ + 获取常用加速配置 + @return: + """ + com_reg_mirror_file = "{}/class_v2/btdockerModelV2/config/com_reg_mirror.json".format(public.get_panel_path()) + try: + com_reg_mirror = json.loads(public.readFile(com_reg_mirror_file)) + except: + com_reg_mirror = { + "https://docker.m.daocloud.io": "Third party image accelerator", + } + + return com_reg_mirror + + def set_monitor_save_date(self, get): + """ + :param save_date: int 例如30 表示 30天 + :param get: + :return: + """ + # 校验参数 + try: + get.validate([ + Param('save_date').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import re + conf_path = "{}/data/docker.conf".format(public.get_panel_path()) + docker_conf = public.readFile(conf_path) + try: + save_date = int(get.save_date) + except: + return public.return_message(-1, 0, _( "The monitoring save time needs to be a positive integer!")) + if save_date > 999: + return public.return_message(-1, 0, _( "Monitoring data cannot be retained for more than 999 days!")) + if not docker_conf: + docker_conf = "SAVE={}".format(save_date) + public.writeFile(conf_path, docker_conf) + return public.return_message(0, 0, _( "Successfully set!")) + docker_conf = re.sub(r"SAVE\s*=\s*\d+", "SAVE={}".format(save_date), + docker_conf) + public.writeFile(conf_path, docker_conf) + dp.write_log("et the monitoring time to [{}] days!".format(save_date)) + return public.return_message(0, 0, _( "Successfully set!")) + + def get_service_status(self): + sock = '/var/run/docker.pid' + if os.path.exists(sock): + try: + client = dp.docker_client() + if client: + return True + return False + except: + return False + else: + return False + + # docker服务状态设置 + def docker_service(self, get): + """ + :param act start/stop/restart + :param get: + :return: + """ + + import public + act_dict = {'start': 'start', 'stop': 'stop', 'restart': 'restart'} + if get.act not in act_dict: + return public.return_message(-1, 0, _( "There's no way to do that")) + exec_str = 'systemctl {} docker'.format(get.act) + if get.act == "stop": + exec_str += ";systemctl {} docker.socket".format(get.act) + stdout, stderr = public.ExecShell(exec_str) + if stderr and not "but it can still be activated by:\n docker.socket\n" in stderr: + dp.write_log("Setting the Docker service status to [{}] failed, failure reason:{}".format(act_dict[get.act], stderr)) + + jou_stdout, jou_stderr = public.ExecShell("journalctl -xe -u docker -n 100 --no-pager|grep libusranalyse.so") + if jou_stdout != "": + return public.return_message(-1, 0, _("Docker service setup failed, please turn off aapanel anti-intrusion and try again!")) + + return public.return_message(-1, 0, _("Setup failed! Reason for failure:{}".format(stderr))) + + if get.act != "stop": + service_status = self.get_service_status() + if not service_status: + import time + public.ExecShell("systemctl stop docker") + public.ExecShell("systemctl stop docker.socket") + time.sleep(1) + public.ExecShell("systemctl start docker") + + dp.write_log("Set the Docker service status to [{}]".format(act_dict[get.act])) + return public.return_message(0, 0, _("{} success".format(act_dict[get.act]))) + + # 获取加速配置 + def get_registry_mirrors(self, get): + """ + 获取镜像加速信息 + @param get: + @return: + """ + try: + if not os.path.exists('/etc/docker/daemon.json'): + reg_mirrors = [] + else: + conf = json.loads(public.readFile('/etc/docker/daemon.json')) + if "registry-mirrors" not in conf: + reg_mirrors = [] + else: + reg_mirrors = conf['registry-mirrors'] + except: + reg_mirrors = [] + + com_reg_mirrors = self._get_com_registry_mirrors() + + # return { + # "registry_mirrors": reg_mirrors, + # "com_reg_mirrors": com_reg_mirrors + # } + + data = { + "registry_mirrors": reg_mirrors, + "com_reg_mirrors": com_reg_mirrors + } + return public.return_message(0, 0, data) + + # 设置加速配置 + def set_registry_mirrors(self, get): + """ + :param registry_mirrors_address registry.docker-cn.com\nhub-mirror.c.163.com + :param get: + :return: + """ + # {"registry_mirrors_address": "https://wzz1sdf11nb.com", "remarks": ""} + # 校验参数 + try: + get.validate([ + Param('registry_mirrors_address').Require().String(), + Param('remarks').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not os.path.exists('/etc/docker/'): + os.makedirs('/etc/docker', 755, True) + + import re + try: + get.registry_mirrors_address = get.get("registry_mirrors_address/s", "") + conf = {} + if os.path.exists('/etc/docker/daemon.json'): + try: + conf = json.loads(public.readFile('/etc/docker/daemon.json')) + except Exception as e: + return public.return_message(-1, 0, _( "Global configuration file error, please check {}!".format(str(e)))) + + if not get.registry_mirrors_address.strip(): + if "registry-mirrors" in conf: + del (conf['registry-mirrors']) + else: + registry_mirrors = get.registry_mirrors_address.strip() + if registry_mirrors == "": + # 2024/4/16 下午12:10 双重保险 + if 'registry-mirrors' in conf: + del (conf['registry-mirrors']) + else: + if not re.search('https?://', registry_mirrors): + return public.return_message(-1, 0, _( 'Speedup address [{}] Format error
                                    Reference: https://mirror.ccs.tencentyun.com'.format(registry_mirrors))) + + conf['registry-mirrors'] = public.xsssec2(registry_mirrors) + if isinstance(conf['registry-mirrors'], str): + conf['registry-mirrors'] = [conf['registry-mirrors']] + + public.writeFile('/etc/docker/daemon.json', json.dumps(conf, indent=2)) + if get.registry_mirrors_address != "": + self.update_com_registry_mirrors(get) + + dp.write_log("Setup Docker acceleration successful!") + return public.return_message(0, 0, _( 'successfully set')) + + except: + return public.return_message(-1, 0, _('Setup failed! Failure reason :{}'.format(public.get_error_info()))) + + def update_com_registry_mirrors(self, get): + """ + 更新常用加速配置 + @param get: + @return: + """ + import time + com_reg_mirror_file = "{}/class_v2/btdockerModelV2/config/com_reg_mirror.json".format(public.get_panel_path()) + try: + com_reg_mirror = json.loads(public.readFile(com_reg_mirror_file)) + except: + com_reg_mirror = { + "https://docker.m.daocloud.io": "Third party image accelerator", + } + + if get.registry_mirrors_address in com_reg_mirror: + return public.return_message(0, 0, _( "Successfully set!")) + + remarks = get.remarks if "remarks" in get and get.remarks != "" else ("Custom mirrors" + str(int(time.time()))) + + com_reg_mirror.update({"{}".format(get.registry_mirrors_address): remarks}) + public.writeFile(com_reg_mirror_file, json.dumps(com_reg_mirror, indent=2)) + dp.write_log("Updated common acceleration configuration successfully!") + return public.return_message(0, 0, _( "Update successfully!")) + + def del_com_registry_mirror(self, get): + """ + 删除常用加速配置 + @param get: + @return: + """ + com_reg_mirror_file = "{}/class_v2/btdockerModelV2/config/com_reg_mirror.json".format(public.get_panel_path()) + try: + com_reg_mirror = json.loads(public.readFile(com_reg_mirror_file)) + except: + com_reg_mirror = { + "https://docker.m.daocloud.io": "Third party image accelerator", + } + + if get.registry_mirrors_address not in com_reg_mirror: + return public.return_message(0, 0, _( "successfully delete!")) + + del com_reg_mirror["{}".format(get.registry_mirrors_address)] + public.writeFile(com_reg_mirror_file, json.dumps(com_reg_mirror, indent=2)) + dp.write_log("Remove common acceleration configuration successfully!") + return public.return_message(0, 0, _( "successfully delete!")) + + def get_monitor_status(self): + """ + 获取docker监控状态 + @return: + """ + try: + from BTPanel import cache + except: + from cachelib import SimpleCache + cache = SimpleCache() + + skey = "docker_monitor_status" + result = cache.get(skey) + if isinstance(result, bool): + return result + + import psutil + is_monitor = False + for proc in psutil.process_iter(): + try: + pinfo = proc.as_dict(attrs=['pid', 'name']) + if "monitorModel.py" in pinfo['name']: + is_monitor = True + except psutil.NoSuchProcess: + pass + cache.set(skey, is_monitor, 86400) + return is_monitor + + def set_docker_monitor(self, get): + """ + 开启docker监控获取docker相取资源信息 + :param act: start/stop + :return: + """ + # 校验参数 + try: + get.validate([ + Param('act').Require().String('in', ['start', 'stop']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import time + python = "/www/server/panel/pyenv/bin/python" + if not os.path.exists(python): + python = "/www/server/panel/pyenv/bin/python3" + cmd_line = "/www/server/panel/class_v2/btdockerModelV2/monitorModel.py" + if get.act == "start": + self.stop_monitor(get) + if not os.path.exists(self.moinitor_lock): + public.writeFile(self.moinitor_lock, "1") + + shell = "nohup {} {} &".format(python, cmd_line) + public.ExecShell(shell) + time.sleep(1) + if self.get_monitor_status(): + dp.write_log("Docker started monitoring successfully!") + self.add_monitor_cron(get) + return public.return_message(0, 0, _( "Start monitoring successfully!")) + return public.return_message(-1, 0, _( "Failed to start monitoring!")) + else: + from BTPanel import cache + skey = "docker_monitor_status" + cache.set(skey, False) + + if os.path.exists(self.moinitor_lock): + os.remove(self.moinitor_lock) + + self.stop_monitor(get) + return public.return_message(0, 0, _( "Docker monitoring stopped successfully!")) + + # 2024/1/4 上午 9:32 停止容器监控进程 + def stop_monitor(self, get): + ''' + @name 名称/描述 + @param 参数名<数据类型> 参数描述 + @return 数据类型 + ''' + cmd_line = [ + "/www/server/panel/class_v2/btdockerModelV2/monitorModel.py", + "/www/server/panel/class/projectModel/bt_docker/dk_monitor.py" + ] + + for cmd in cmd_line: + in_pid = True + sum = 0 + while in_pid: + in_pid = False + pid = dp.get_process_id( + "python", + "{}".format(cmd)) + if pid: + in_pid = True + + if not pid: + pid = dp.get_process_id( + "python3", + "{}".format(cmd) + ) + if pid: + in_pid = True + public.ExecShell("kill -9 {}".format(pid)) + sum += 1 + if sum > 100: + break + + import os + + # 指定目录路径 + directory = "/www/server/cron/" + if not os.path.exists(directory): + os.makedirs(directory) + + # 遍历目录下的所有非.log结尾的文件 + for filename in os.listdir(directory): + if not filename.endswith(".log"): + filepath = os.path.join(directory, filename) + if os.path.isdir(filepath): + continue + # 检查文件内容是否包含 "monitorModel.py" + with open(filepath, 'r') as file: + content = file.read() + if "monitorModel.py" in content or "dk_monitor.py" in content: + # 删除原文件和对应的.log文件 + if os.path.exists(filepath): + os.remove(filepath) + if os.path.exists(os.path.join(directory, "{}.log".format(filename))): + os.remove(os.path.join(directory, "{}.log".format(filename))) + public.ExecShell("crontab -l | sed '/{}/d' | crontab -".format(filename)) + + dp.write_log("Docker monitoring stopped successfully!") + + public.M('crontab').where('name=?', ("[Do not delete] docker monitoring daemon",)).delete() + return public.returnMsg(True, "Docker monitoring stopped successfully!") + + # 2023/12/7 下午 6:24 创建计划任务,监听监控进程是否存在,如果不存在则添加 + def add_monitor_cron(self, get): + ''' + @name 名称/描述 + @author wzz <2023/12/7 下午 6:24> + @param 参数名<数据类型> 参数描述 + @return 数据类型 + ''' + try: + import crontab + if public.M('crontab').where('name', ("[Do not delete] docker monitoring daemon",)).count() == 0: + p = crontab.crontab() + llist = p.GetCrontab(None) + + if type(llist) == list: + for i in llist: + if i['name'] == '[Do not delete] docker monitoring daemon': + return + + get = { + "name": "[Do not delete] docker monitoring daemon", + "type": "minute-n", + "where1": 5, + "hour": "", + "minute": "", + "week": "", + "sType": "toShell", + "sName": "", + "backupTo": "localhost", + "save": '', + "sBody": """ +if [ -f {} ]; then + new_mt=`ps aux|grep monitorModel.py|grep -v grep` + old_mt=`ps aux|grep dk_monitor.py|grep -v grep` + + if [ -z "$new_mt" ] && [ -z "$old_mt" ]; then + nohup /www/server/panel/pyenv/bin/python /www/server/panel/class_v2/btdockerModelV2/monitorModel.py & + fi +fi + """.format(self.moinitor_lock), + "urladdress": "undefined" + } + p.AddCrontab(get) + except Exception as e: + return False + + def check_docker_compose_service(self): + """ + 检查docker-compose是否已经安装 + :return: + """ + docker_compose = "/usr/bin/docker-compose" + + docker_compose_path = "{}/class_v2/btdockerModelV2/config/docker_compose_path.pl".format(public.get_panel_path()) + if os.path.exists(docker_compose_path): + docker_compose = public.readFile(docker_compose_path).strip() + + if not os.path.exists(docker_compose): + # public.print_log("mwmwmwmwm 没文件") + dk_compose_list = ["/usr/libexec/docker/cli-plugins/docker-compose", "/usr/local/docker-compose"] + for i in dk_compose_list: + if os.path.exists(i): + public.ExecShell("ln -sf {} {}".format(i, "/usr/bin/docker-compose")) + break + + if not os.path.exists(docker_compose): + return False, "" + + return True, docker_compose + + def check_docker_service(self): + """ + 检查docker是否安装 + @return: + """ + docker = "/usr/bin/docker" + if not os.path.exists(docker): + return False + return True + + def set_docker_compose_path(self, get): + """ + 设置docker-compose的路径 + @param get: + @return: + """ + docker_compose_file = get.docker_compose_path if "docker_compose_path" in get else "" + if docker_compose_file == "": + return public.return_message(-1, 0, _( "docker-compose file path cannot be empty!")) + + if not os.path.exists(docker_compose_file): + return public.return_message(-1, 0, _( "docker-compose file does not exist!")) + + public.ExecShell("chmod +x {}".format(docker_compose_file)) + cmd_result = public.ExecShell("{} --version".format(docker_compose_file)) + if not cmd_result[0]: + return public.return_message(-1, 0, _( "docker-compose file is not executable or is not a docker-compose file!")) + + docker_compose_path = "{}/class_v2/btdockerModelV2/config/docker_compose_path.pl".format(public.get_panel_path()) + + public.writeFile(docker_compose_path, docker_compose_file) + dp.write_log("Set docker-compose path successfully!") + return public.return_message(0, 0, _( "Successfully set!")) + + def install_docker_program(self, get): + """ + 安装docker和docker-compose + :param get: + :return: + """ + import time + url = get.get("url/s", "") + type = get.get("type/d", 0) + + # 2024/3/28 上午 10:36 检测是否已存在安装任务 + if public.M('tasks').where('name=? and status=?', ("Install Docker Service", "-1")).count(): + return public.return_message(-1, 0, _( "The installation task already exists, please do not add it again!")) + + mmsg = "Install Docker Service" + if type == 0 and url == "": + # 默认安装 + execstr = ("wget -O /tmp/docker_install.sh {}/install/0/docker_install.sh && " + "bash /tmp/docker_install.sh install ").format(public.get_url()) + elif type == 0 and url != "": + # 选择镜像源安装 + execstr = ("wget -O /tmp/docker_install.sh {}/install/0/docker_install.sh && " + "bash /tmp/docker_install.sh install {} ").format(public.get_url(), url.strip('"')) + else: + # 二进制安装 + execstr = "/bin/bash /www/server/panel/install/install_soft.sh 0 install docker_bin " + + public.M('tasks').add('id,name,type,status,addtime,execstr', + (None, mmsg, 'execshell', '0', + time.strftime('%Y-%m-%d %H:%M:%S'), execstr)) + public.httpPost( + public.GetConfigValue('home') + '/api/panel/plugin_total', { + "pid": "1111111", + 'p_name': "Docker commercial module" + }, 3) + return public.return_message(0, 0, _( "The installation task has been added to the queue!")) + + def repair_docker(self, get): + """ + 修复docker + @param get: + @return: + """ + import time + mmsg = "Repair Docker service" + execstr = "curl -fsSL https://get.docker.com -o /tmp/get-docker.sh && sed -i '/sleep 20/d' /tmp/get-docker.sh && /bin/bash /tmp/get-docker.sh" + public.M('tasks').add('id,name,type,status,addtime,execstr', + (None, mmsg, 'execshell', '0', + time.strftime('%Y-%m-%d %H:%M:%S'), execstr)) + public.httpPost( + public.GetConfigValue('home') + '/api/panel/plugin_total', { + "pid": "1111111", + 'p_name': "Docker commercial module" + }, 3) + return public.return_message(0, 0, _( "The repair task has been added to the queue!")) + + def get_daemon_json(self, get): + """ + 获取daemon.json配置信息 + @param get: + @return: + """ + daemon_json = "/etc/docker/daemon.json" + if not os.path.exists(daemon_json): + return public.return_message(0, 0, "") + + try: + return public.return_message(0, 0, json.loads(public.readFile(daemon_json))) + except Exception as e: + print(e) + return public.return_message(-1, 0, "") + + def save_daemon_json(self, get): + """ + 保存daemon.json配置信息,保存前备份,验证可以成功执行后再替换 + @param get: + @return: + """ + daemon_json = "/etc/docker/daemon.json" + if getattr(get, "daemon_json", "") == "": + public.ExecShell("rm -f {}".format(daemon_json)) + return public.return_message(0, 0, _( "Saved successfully!")) + + try: + conf = json.loads(get.daemon_json) + public.writeFile(daemon_json, json.dumps(conf, indent=2)) + dp.write_log("Save daemon.json configuration successfully!") + return public.return_message(0, 0, _( "Saved successfully!")) + except Exception as e: + public.print_log("err: {}".format(e)) + if "Expecting property name enclosed in double quotes" in str(e): + return public.return_message(-1, 0, _( "Saving failed, reason: daemon.json configuration file format error!")) + + return public.return_message(-1, 0, _( "Save failed, reason: {}".format(e))) + def uninstall_status(self, get): + """ + 检测docker是否可以卸载 + :param get: + :return: + """ + from btdockerModelV2 import containerModel + docker_list = containerModel.main().get_list(get) + from btdockerModelV2 import imageModel + images_list = imageModel.main().image_list(get) + if len(images_list) > 0 or len(docker_list["container_list"]) > 0: + return public.return_message(0, 0, {"status": False, + "msg": "Please manually delete all containers and images before uninstalling!"}) + return public.return_message(0, 0, "Allow uninstallation") + + def uninstall_status1(self, get): + """ + 检测docker是否可以卸载 + :param get: + :return: + """ + from btdockerModelV2 import containerModel + docker_list = containerModel.main().get_list(get) + from btdockerModelV2 import imageModel + images_list = imageModel.main().image_list(get) + if len(images_list) > 0 or len(docker_list["container_list"]) > 0: + return False + return True + def uninstall_docker_program(self, get): + """ + 卸载docker和docker-compose + :param get: + :return: + """ + type = get.get("type/d", 0) + if type == 0: + uninstall_status = self.uninstall_status1(get) + if not uninstall_status["status"]: + return public.return_message(-1, 0, _( "Please manually delete all containers and images before uninstalling!")) + + public.ExecShell( + "wget -O /tmp/docker_install.sh {}/install/0/docker_install.sh && bash /tmp/docker_install.sh uninstall" + .format(public.get_url() + )) + public.ExecShell("rm -rf /usr/bin/docker-compose") + + return public.return_message(0, 0, "Uninstall successfully!") diff --git a/class_v2/btdockerModelV2/statusModel.py b/class_v2/btdockerModelV2/statusModel.py new file mode 100644 index 00000000..08ba8c3e --- /dev/null +++ b/class_v2/btdockerModelV2/statusModel.py @@ -0,0 +1,258 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import gettext +_ = gettext.gettext + +import time + +# ------------------------------ +# Docker模型 +# ------------------------------ +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +class main(dockerBase): + __stats_tmp = dict() + __docker = None + + def docker_client(self, url): + if not self.__docker: + self.__docker = dp.docker_client(url) + return self.__docker + + def io_stats(self, stats, write=None): + drive_io = stats['blkio_stats']['io_service_bytes_recursive'] + if drive_io: + if len(drive_io) <= 2: + try: + now = drive_io[0]['value'] + self.__stats_tmp['read_total'] = now + except: + self.__stats_tmp['read_total'] = 0 + try: + now = drive_io[1]['value'] + self.__stats_tmp['write_total'] = now + except: + self.__stats_tmp['write_total'] = 0 + else: + try: + now = drive_io[0]['value'] + drive_io[2]['value'] + self.__stats_tmp['read_total'] = now + except: + self.__stats_tmp['read_total'] = 0 + try: + now = drive_io[1]['value'] + drive_io[3]['value'] + self.__stats_tmp['write_total'] = now + except: + self.__stats_tmp['write_total'] = 0 + if write: + self.__stats_tmp['container_id'] = stats['id'] + self.write_io(self.__stats_tmp) + + def net_stats(self, stats, cache, write=None): + try: + net_io = stats['networks']['eth0'] + net_io_old = cache['networks']['eth0'] + except: + self.__stats_tmp['rx_total'] = 0 + self.__stats_tmp['rx'] = 0 + self.__stats_tmp['tx_total'] = 0 + self.__stats_tmp['tx'] = 0 + if write: + self.__stats_tmp['container_id'] = stats['id'] + self.write_net(self.__stats_tmp) + return + time_now = stats["time"] + time_old = cache["time"] + try: + now = net_io["rx_bytes"] + self.__stats_tmp['rx_total'] = now + old = net_io_old["rx_bytes"] + self.__stats_tmp['rx'] = int((now - old) / (time_now - time_old)) + except: + self.__stats_tmp['rx_total'] = 0 + self.__stats_tmp['rx'] = 0 + try: + now = net_io["tx_bytes"] + old = net_io_old["tx_bytes"] + self.__stats_tmp['tx_total'] = now + self.__stats_tmp['tx'] = int((now - old) / (time_now - time_old)) + except: + self.__stats_tmp['tx_total'] = 0 + self.__stats_tmp['tx'] = 0 + if write: + self.__stats_tmp['container_id'] = stats['id'] + self.write_net(self.__stats_tmp) + # return data + + def mem_stats(self, stats, write=None): + mem = stats['memory_stats'] + try: + self.__stats_tmp['limit'] = mem['limit'] + self.__stats_tmp['usage_total'] = mem['usage'] + if 'cache' not in mem['stats']: + mem['stats']['cache'] = 0 + self.__stats_tmp['usage'] = mem['usage'] - mem['stats']['cache'] + self.__stats_tmp['cache'] = mem['stats']['cache'] + # data['mem_useage'] = round(mem['usage'] * 100 / data['limit'],2) + except: + # return public.get_error_info() + self.__stats_tmp['limit'] = 0 + self.__stats_tmp['usage'] = 0 + self.__stats_tmp['cache'] = 0 + self.__stats_tmp['usage_total'] = 0 + # data['mem_useage'] = 0 + if write: + self.__stats_tmp['container_id'] = stats['id'] + self.write_mem(self.__stats_tmp) + # return data + + def cpu_stats(self, stats, write=None): + # cpu_limit = dp.sql('container').where("c_id=?",(stats['id'],)).find() + # if cpu_limit: + # cpu_limit = cpu_limit['cpu_limit'] + # else: + # cpu_limit = 1 + try: + cpu = stats['cpu_stats']['cpu_usage']['total_usage'] - stats[ + 'precpu_stats']['cpu_usage']['total_usage'] + except: + cpu = 0 + try: + system = stats['cpu_stats']['system_cpu_usage'] - stats[ + 'precpu_stats']['system_cpu_usage'] + except: + system = 0 + try: + self.__stats_tmp['online_cpus'] = stats['cpu_stats']['online_cpus'] + except: + self.__stats_tmp['online_cpus'] = 0 + if cpu > 0 and system > 0: + self.__stats_tmp['cpu_usage'] = round( + (cpu / system) * 100 * self.__stats_tmp['online_cpus'], 2) + else: + self.__stats_tmp['cpu_usage'] = 0.0 + if write: + self.__stats_tmp['container_id'] = stats['id'] + self.write_cpu(self.__stats_tmp) + # return data + + def stats(self, args): + """ + 获取某个容器的cpu,内存,网络io,磁盘io. + :param url + :param id + :param args: + :return: + """ + # {"id": "d58097084d43324643efde5cc8d30643901c27366d35238801a2119509352ab7", "dk_status": "running"} + + try: + args.validate([ + Param('id').Require().String(), + Param('dk_status').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, ex) + try: + container = self.docker_client(self._url).containers.get(args.id) + stats = container.stats(decode=None, stream=False) + stats['time'] = time.time() + cache = public.cache_get('stats') + if not cache: + cache = stats + public.cache_set('stats', stats) + write = None + if hasattr(args, "write"): + write = args.write + self.__stats_tmp['expired'] = time.time() - (args.save_date * 86400) + stats['id'] = args.id + import json + from pygments import highlight, lexers, formatters + formatted_json = json.dumps(stats, indent=3) + colorful_json = highlight(formatted_json.encode('utf-8'), lexers.JsonLexer(), formatters.TerminalFormatter()) + print(colorful_json) + self.io_stats(stats, write) + self.net_stats(stats, cache, write) + self.cpu_stats(stats, write) + self.mem_stats(stats, write) + public.cache_set('stats', stats) + self.__stats_tmp['detail'] = stats + if 'dk_status' in args and args.dk_status != 'running': + self.__stats_tmp['read_total'] = 0 + self.__stats_tmp['write_total'] = 0 + return public.return_message(0, 0, self.__stats_tmp) + except Exception as ex: + if "No such container" in str(ex): + return public.return_message(-1, 0, _('The container does not exist, please refresh the browser and try again!')) + return public.return_message(-1, 0, _('Failed to get container status: ' + str(ex))) + + def top(self, get): + """ + 获取容器内进程信息 + @param get: + @return: + """ + container = self.docker_client(self._url).containers.get(get.id) + return public.return_message(0, 0, container.top()) + + def write_cpu(self, data): + pdata = { + "time": time.time(), + "cpu_usage": data['cpu_usage'], + "online_cpus": data['online_cpus'], + "container_id": data['container_id'] + } + dp.sql("cpu_stats").where("time +# ------------------------------------------------------------------- +import gettext +_ = gettext.gettext + +# ------------------------------ +# Docker模型 +# ------------------------------ +import docker.errors +import public +from btdockerModelV2 import dk_public as dp +from btdockerModelV2.dockerBase import dockerBase +from public.validate import Param + +class main(dockerBase): + + def docker_client(self, url): + return dp.docker_client(url) + + def get_volume_container_name(self, volume_detail, container_list): + ''' + 拼接对应的容器名与卷名 + @param volume_detail: 卷字典 + @param container_list: 容器详情列表 + @return: + ''' + try: + for container in container_list: + if not container['Mounts']: + continue + for mount in container['Mounts']: + if "Name" not in mount: + continue + if volume_detail['Name'] == mount['Name']: + volume_detail['container'] = container['Names'][0].replace("/", "") + if 'container' not in volume_detail: + volume_detail['container'] = '' + except: + volume_detail['container'] = '' + + return volume_detail + + def get_volume_list(self, args): + """ + :param self._url: 链接docker的URL + :return: + """ + try: + data = list() + from btdockerModelV2.dockerSock import volume + sk_volume = volume.dockerVolume() + volume_list = sk_volume.get_volumes() + + from btdockerModelV2.dockerSock import container + sk_container = container.dockerContainer() + container_list = sk_container.get_container() + + if "Volumes" in volume_list and type(volume_list["Volumes"]) == list: + for v in volume_list["Volumes"]: + data.append(self.get_volume_container_name(v, container_list)) + + return public.return_message(0, 0, sorted(data, key=lambda x: x['CreatedAt'], reverse=True)) + else: + return public.return_message(0, 0, []) + except Exception as e: + return public.return_message(-1, 0, []) + + def add(self, args): + """ + 添加一个卷 + :param name + :param driver local + :param driver_opts (dict) – Driver options as a key-value dictionary + :param labels str + :return: + """ + try: + args.driver_opts = args.get("driver_opts", "") + args.labels = args.get("labels", "") + if args.driver_opts != "": + args.driver_opts = dp.set_kv(args.driver_opts) + if args.labels != "": + args.labels = dp.set_kv(args.labels) + + if len(args.name) < 2: + return public.return_message(-1, 0, _( "Volume names can be no less than 2 characters long!")) + + self.docker_client(self._url).volumes.create( + name=args.name, + driver=args.driver, + driver_opts=args.driver_opts if args.driver_opts else None, + labels=args.labels if args.labels != "" else None + ) + dp.write_log("Add storage volume [{}] success!".format(args.name)) + return public.return_message(0, 0, _( "successfully added!")) + except docker.errors.APIError as e: + if "volume name is too short, names should be at least two alphanumeric characters" in str(e): + return public.return_message(-1, 0, _( "Volume names can be no less than 2 characters long!")) + if "volume name" in str(e): + return public.return_message(-1, 0, _( "Volume name already exists!")) + return public.return_message(-1, 0, _( "addition failed {}".format(e))) + + except Exception as e: + if "driver_opts must be a dictionary" in str(e): + return public.return_message(-1, 0, _( "Driver option tags must be dictionary/key-value pairs!")) + return public.return_message(-1, 0, _( "Add failed! {}".format(e))) + + def remove(self, args): + """ + 删除一个卷 + :param name volume name + :param args: + :return: + """ + + # 校验参数 + try: + args.validate([ + Param('name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + obj = self.docker_client(self._url).volumes.get(args.name) + obj.remove() + dp.write_log("Delete volume [{}] successful!".format(args.name)) + return public.return_message(0, 0, _( "successfully delete")) + + except docker.errors.APIError as e: + if "volume is in use" in str(e): + return public.return_message(-1, 0, _( "The storage volume is in use and cannot be deleted!")) + if "no such volume" in str(e): + return public.return_message(-1, 0, _( "The storage volume does not exist!")) + return public.return_message(-1, 0, _( "Delete failed! {}".format(e))) + + def prune(self, args): + """ + 删除无用的卷 + :param args: + :return: + """ + try: + res = self.docker_client(self._url).volumes.prune() + if not res['VolumesDeleted']: + return public.return_message(-1, 0, _( "No useless storage volumes!")) + + dp.write_log("Delete useless storage volume successfully!") + return public.return_message(0, 0, _( "successfully delete!")) + except docker.errors.APIError as e: + return public.return_message(-1, 0, _( "Delete failed! {}".format(e))) diff --git a/class_v2/common_v2.py b/class_v2/common_v2.py new file mode 100644 index 00000000..58087c2c --- /dev/null +++ b/class_v2/common_v2.py @@ -0,0 +1,324 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +from BTPanel import session, cache , request, redirect, g,abort +from datetime import datetime +from public import dict_obj +import os +import public +import json +import sys +import time + + +class panelSetup: + def init(self): + panel_path = public.get_panel_path() + if os.getcwd() != panel_path: os.chdir(panel_path) + + g.ua = request.headers.get('User-Agent','') + if g.ua: + ua = g.ua.lower() + if ua.find('spider') != -1 or g.ua.find('bot') != -1: + return abort(403) + + g.version = '6.8.36' + g.title = public.GetConfigValue('title') + g.uri = request.path + g.debug = os.path.exists('data/debug.pl') + g.pyversion = sys.version_info[0] + session['version'] = g.version + + if not public.get_improvement(): session['is_flush_soft_list'] = 1 + if request.method == 'GET': + if not g.debug: + g.cdn_url = public.get_cdn_url() + if not g.cdn_url: + g.cdn_url = '/static' + else: + g.cdn_url = '//' + g.cdn_url + '/' + g.version + else: + g.cdn_url = '/static' + session['title'] = g.title + + g.recycle_bin_open = 0 + if os.path.exists("data/recycle_bin.pl"): g.recycle_bin_open = 1 + + g.recycle_bin_db_open = 0 + if os.path.exists("data/recycle_bin_db.pl"): g.recycle_bin_db_open = 1 + g.is_aes = False + self.other_import() + return None + + + def other_import(self): + g.o = public.readFile('data/o.pl') + g.other_css = [] + g.other_js = [] + if g.o: + s_path = 'BTPanel/static/other/{}' + css_name = "css/{}.css".format(g.o) + css_file = s_path.format(css_name) + if os.path.exists(css_file): g.other_css.append('/static/other/{}'.format(css_name)) + + js_name = "js/{}.js".format(g.o) + js_file = s_path.format(js_name) + if os.path.exists(js_file): g.other_js.append('/static/other/{}'.format(js_name)) + + + +class panelAdmin(panelSetup): + setupPath = '/www/server' + + # 本地请求 + def local(self): + result = panelSetup().init() + if result: + return result + result = self.check_login() + if result: + return result + result = self.setSession() + if result: + return result + result = self.checkClose() + if result: + return result + result = self.checkWebType() + if result: + return result + result = self.checkConfig() + self.GetOS() + + # 设置基础Session + def setSession(self): + if request.method == 'GET': + g.menus = public.get_menus() + g.yaer = datetime.now().year + session["top_tips"] = public.get_msg_gettext("The current IE browser version is too low to display some features, please use another browser. Or if you use a browser developed by a Chinese company, please switch to Extreme Mode!") + session["bt_help"] = public.get_msg_gettext("For Support|Suggestions, please visit the aaPanel Forum") + session["download"] = public.get_msg_gettext("Downloading:") + if not 'brand' in session: + session['brand'] = public.GetConfigValue('brand') + session['product'] = public.GetConfigValue('product') + session['rootPath'] = '/www' + session['download_url'] = 'https://node.aapanel.com' + session['setupPath'] = session['rootPath'] + '/server' + session['logsPath'] = '/www/wwwlogs' + session['yaer'] = datetime.now().year + if not 'menu' in session: + session['menu'] = public.GetLan('menu') + if not 'lan' in session: + session['lan'] = public.GetLanguage() + if not 'home' in session: + session['home'] = 'https://www.aapanel.com' + return False + + # 检查Web服务器类型 + def checkWebType(self): + #if request.method == 'GET': + if not 'webserver' in session: + if os.path.exists('/usr/local/lsws/bin/lswsctrl'): + session['webserver'] = 'openlitespeed' + elif os.path.exists(self.setupPath + '/apache/bin/apachectl'): + session['webserver'] = 'apache' + else: + session['webserver'] = 'nginx' + if not 'webversion' in session: + if os.path.exists(self.setupPath+'/'+session['webserver']+'/version.pl'): + session['webversion'] = public.ReadFile(self.setupPath+'/'+session['webserver']+'/version.pl').strip() + + if not 'phpmyadminDir' in session: + filename = self.setupPath+'/data/phpmyadminDirName.pl' + if os.path.exists(filename): + session['phpmyadminDir'] = public.ReadFile(filename).strip() + return False + + # 检查面板是否关闭 + def checkClose(self): + if os.path.exists('data/close.pl'): + return redirect('/close') + + # 检查登录 + def check_login(self): + try: + api_check = True + g.api_request = False + if not 'login' in session: + api_check = self.get_sk() + if api_check: + if not isinstance(api_check,dict): + if public.get_admin_path() == '/login': + return redirect('/login?err=1') + return api_check + g.api_request = True + else: + if session['login'] == False: + session.clear() + return redirect(public.get_admin_path()) + + if 'tmp_login_expire' in session: + s_file = 'data/session/{}'.format(session['tmp_login_id']) + if session['tmp_login_expire'] < time.time(): + session.clear() + if os.path.exists(s_file): os.remove(s_file) + return redirect(public.get_admin_path()) + if not os.path.exists(s_file): + session.clear() + return redirect(public.get_admin_path()) + + if not public.check_client_hash(): + session.clear() + return redirect(public.get_admin_path()) + + if api_check: + now_time = time.time() + session_timeout = session.get('session_timeout',0) + if session_timeout < now_time and session_timeout != 0: + session.clear() + return redirect(public.get_admin_path()) + + login_token = session.get('login_token','') + if login_token: + if login_token != public.get_login_token_auth(): + session.clear() + return redirect(public.get_admin_path()) + + # if api_check: + # filename = 'data/sess_files/' + public.get_sess_key() + # if not os.path.exists(filename): + # session.clear() + # return redirect(public.get_admin_path()) + + # 标记新的会话过期时间 + # session['session_timeout'] = time.time() + public.get_session_timeout() + # 标记新的会话过期时间 + self.check_session() + + except: + # public.print_log(public.get_error_info()) + session.clear() + public.print_error() + return redirect('/login?id=2') + + def check_session(self): + white_list = ['/favicon.ico', '/system?action=GetNetWork'] + if g.uri in white_list: + return + session['session_timeout'] = time.time() + public.get_session_timeout() + + + + # 获取sk + def get_sk(self): + save_path = '/www/server/panel/config/api.json' + if not os.path.exists(save_path): + return public.redirect_to_login() + + try: + api_config = json.loads(public.ReadFile(save_path)) + except: + os.remove(save_path) + return public.redirect_to_login() + + if not api_config['open']: + return public.redirect_to_login() + from BTPanel import get_input + get = get_input() + client_ip = public.GetClientIp() + if not 'client_bind_token' in get: + if not 'request_token' in get or not 'request_time' in get: + return public.redirect_to_login() + + num_key = client_ip + '_api' + if not public.get_error_num(num_key, 20): + return public.returnJson(False,'20 consecutive verification failures, prohibited for 1 hour') + + if not public.is_api_limit_ip(api_config['limit_addr'], client_ip): # client_ip in api_config['limit_addr']: + public.set_error_num(num_key) + return public.returnJson(False,'%s[' % public.get_msg_gettext("20 consecutive verification failures, prohibited for 1 hour")+client_ip+']') + else: + num_key = client_ip + '_app' + if not public.get_error_num(num_key,20): + return public.returnJson(False,'20 consecutive verification failures, prohibited for 1 hour') + a_file = '/dev/shm/' + get.client_bind_token + + if not public.path_safe_check(get.client_bind_token): + public.set_error_num(num_key) + return public.returnJson(False, 'illegal request') + + if not os.path.exists(a_file): + import panelApi + if not panelApi.panelApi().get_app_find(get.client_bind_token): + public.set_error_num(num_key) + return public.returnJson(False,'Unbound device') + public.writeFile(a_file,'') + + if not 'key' in api_config: + public.set_error_num(num_key) + return public.returnJson(False, 'Key verification failed') + if not 'form_data' in get: + public.set_error_num(num_key) + return public.returnJson(False, 'No form_data data found') + + g.form_data = json.loads(public.aes_decrypt(get.form_data, api_config['key'])) + + get = get_input() + if not 'request_token' in get or not 'request_time' in get: + return public.error_not_login('/login') + g.is_aes = True + g.aes_key = api_config['key'] + request_token = public.md5(get.request_time + api_config['token']) + if get.request_token == request_token: + public.set_error_num(num_key,True) + return False + public.set_error_num(num_key) + return public.returnJson(False,'Secret key verification failed') + + # 检查系统配置 + def checkConfig(self): + if not 'config' in session: + session['config'] = public.M('config').where("id=?", ('1',)).field( + 'webserver,sites_path,backup_path,status,mysql_root').find() + if not 'email' in session['config']: + session['config']['email'] = public.M( + 'users').where("id=?", ('1',)).getField('email') + if not 'address' in session: + session['address'] = public.GetLocalIp() + return False + + # 获取操作系统类型 + def GetOS(self): + if not 'server_os' in session: + tmp = {} + issue_file = '/etc/issue' + redhat_release = '/etc/redhat-release' + if os.path.exists(redhat_release): + tmp['x'] = 'RHEL' + tmp['osname'] = self.get_osname(redhat_release) + elif os.path.exists('/usr/bin/yum'): + tmp['x'] = 'RHEL' + tmp['osname'] = self.get_osname(issue_file) + elif os.path.exists(issue_file): + tmp['x'] = 'Debian' + tmp['osname'] = self.get_osname(issue_file) + session['server_os'] = tmp + return False + + + def get_osname(self,i_file): + ''' + @name 从指定文件中获取系统名称 + @author hwliang<2021-04-07> + @param i_file 指定文件全路径 + @return string + ''' + if not os.path.exists(i_file): return '' + issue_str = public.ReadFile(i_file).strip() + if issue_str: return issue_str.split()[0] + return '' diff --git a/class_v2/config_v2.py b/class_v2/config_v2.py new file mode 100644 index 00000000..55a97c27 --- /dev/null +++ b/class_v2/config_v2.py @@ -0,0 +1,3352 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel x3 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import base64 +import public,re,os,nginx,apache,json,time,ols +from public.validate import Param +try: + import pyotp +except: + public.ExecShell("pip install pyotp &") +try: + from BTPanel import session,admin_path_checks,g,request,cache + import send_mail +except:pass +class config: + _setup_path = "/www/server/panel" + _key_file = _setup_path+"/data/two_step_auth.txt" + _bk_key_file = _setup_path + "/data/bk_two_step_auth.txt" + _username_file = _setup_path + "/data/username.txt" + _core_fle_path = _setup_path + '/data/qrcode' + __mail_config = _setup_path+'/data/stmp_mail.json' + __mail_list_data = _setup_path+'/data/mail_list.json' + __dingding_config = _setup_path+'/data/dingding.json' + __mail_list = [] + __weixin_user = [] + + def __init__(self): + try: + self.mail = send_mail.send_mail() + if not os.path.exists(self.__mail_list_data): + ret = [] + public.writeFile(self.__mail_list_data, json.dumps(ret)) + else: + try: + mail_data = json.loads(public.ReadFile(self.__mail_list_data)) + self.__mail_list = mail_data + except: + ret = [] + public.writeFile(self.__mail_list_data, json.dumps(ret)) + except:pass + # 返回配置邮件地址 + def return_mail_list(self, get): + return public.return_msg_gettext(True, self.__mail_list) + + # 删除邮件接口 + def del_mail_list(self, get): + emial = get.email.strip() + if emial in self.__mail_list: + self.__mail_list.remove(emial) + public.writeFile(self.__mail_list_data, json.dumps(self.__mail_list)) + return public.return_msg_gettext(True, 'Successfully deleted') + else: + return public.return_msg_gettext(True, 'Email does not exist') + + def del_tg_info(self,get): + import panel_telegram_bot + return panel_telegram_bot.panel_telegram_bot().del_tg_bot(get) + + def set_tg_bot(self,get): + import panel_telegram_bot + return panel_telegram_bot.panel_telegram_bot().set_tg_bot(get) + + #添加接受邮件地址 + def add_mail_address(self, get): + if not hasattr(get, 'email'): return public.return_msg_gettext(False, 'Please input your email') + emailformat = re.compile(r'[a-zA-Z0-9.-_+%]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+') + if not emailformat.search(get.email): return public.return_msg_gettext(False, 'Please enter your vaild email') + # 测试发送邮件 + if get.email.strip() in self.__mail_list: return public.return_msg_gettext(True, 'Email already exists') + self.__mail_list.append(get.email.strip()) + public.writeFile(self.__mail_list_data, json.dumps(self.__mail_list)) + return public.return_msg_gettext(True, 'Setup successfully!') + + # 添加自定义邮箱地址 + def user_mail_send(self, get): + if not (hasattr(get, 'email') or hasattr(get, 'stmp_pwd') or hasattr(get, 'hosts') or hasattr(get, 'port')): + return public.return_msg_gettext(False, 'Please complete the information') + # 自定义邮件 + self.mail.qq_stmp_insert(get.email.strip(), get.stmp_pwd.strip(), get.hosts.strip(),get.port.strip()) + # 测试发送 + if self.mail.qq_smtp_send(get.email.strip(), public.get_msg_gettext('aaPanel Alert Test Email'), public.get_msg_gettext('aaPanel Alert Test Email')): + if not get.email.strip() in self.__mail_list: + self.__mail_list.append(get.email.strip()) + public.writeFile(self.__mail_list_data, json.dumps(self.__mail_list)) + return public.return_msg_gettext(True, 'Setup successfully!') + else: + ret = [] + public.writeFile(self.__mail_config, json.dumps(ret)) + return public.return_msg_gettext(False, 'Email sending failed, please check if the STMP password is correct or the hosts are correct') + + # 查看自定义邮箱配置 + def get_user_mail(self, get): + qq_mail_info = json.loads(public.ReadFile(self.__mail_config)) + if len(qq_mail_info) == 0: + return public.return_msg_gettext(False, 'No Data') + if not 'port' in qq_mail_info:qq_mail_info['port']=465 + return public.return_msg_gettext(True, qq_mail_info) + + #清空数据 + def set_empty(self,get): + type=get.type.strip() + if type=='dingding': + ret = [] + public.writeFile(self.__dingding_config, json.dumps(ret)) + return public.return_msg_gettext(True, 'Empty successfully') + else: + ret = [] + public.writeFile(self.__mail_config, json.dumps(ret)) + return public.return_msg_gettext(True, 'Empty successfully') + + + # 用户自定义邮件发送 + def user_stmp_mail_send(self, get): + if not (hasattr(get, 'email')): return public.return_msg_gettext(False, 'Please input your email') + emailformat = re.compile(r'[a-zA-Z0-9.-_+%]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+') + if not emailformat.search(get.email): return public.return_msg_gettext(False, 'Please enter your vaild email') + # 测试发送邮件 + if not get.email.strip() in self.__mail_list: return public.return_msg_gettext(True, 'The mailbox does not exist, please add it to the mailbox list') + if not (hasattr(get, 'title')): return public.return_msg_gettext(False, 'Please fill in the email title') + if not (hasattr(get, 'body')): return public.return_msg_gettext(False, 'Please enter the email content') + # 先判断是否存在stmp信息 + qq_mail_info = json.loads(public.ReadFile(self.__mail_config)) + if len(qq_mail_info) == 0: + return public.return_msg_gettext(False, 'STMP information was not found, please re-add custom mail STMP information in the settings') + if self.mail.qq_smtp_send(get.email.strip(), get.title.strip(), get.body): + # 发送成功 + return public.return_msg_gettext(True, 'Sent successfully') + else: + return public.return_msg_gettext(False, 'Failed to send') + + # 查看能使用的告警通道 + def get_settings(self, get): + sm = send_mail.send_mail() + return sm.get_settings() + + def get_settings2(self, get=None): + import panel_telegram_bot + tg = panel_telegram_bot.panel_telegram_bot() + tg = tg.get_tg_conf() + conf = self.get_settings(get) + conf['telegram'] = tg + return conf + + # 设置钉钉报警 + def set_dingding(self, get): + if not (hasattr(get, 'url') or hasattr(get, 'atall')): + return public.return_msg_gettext(False, 'Please complete the information') + if get.atall=='True' or get.atall=='1': + get.atall = 'True' + else: get.atall = 'False' + push_url = get.url.strip() + channel = "dingding" + if push_url.find("weixin.qq.com") != -1: + channel = "weixin" + msg = "" + try: + from panelMessage import panelMessage + pm = panelMessage() + if hasattr(pm, "init_msg_module"): + msg_module = pm.init_msg_module(channel) + if msg_module: + _res = msg_module.set_config(get) + if _res["status"]: + return _res + except Exception as e: + msg = str(e) + print("设置钉钉配置异常: {}".format(msg)) + if not msg: + return public.returnMsg(False, 'Add failed, please check if the URL is correct') + else: + return public.returnMsg(False, msg) + + # 查看钉钉 + def get_dingding(self, get): + sm = send_mail.send_mail() + return sm.get_dingding() + + # 使用钉钉发送消息 + def user_dingding_send(self, get): + qq_mail_info = json.loads(public.ReadFile(self.__dingding_config)) + if len(qq_mail_info) == 0: + return public.return_msg_gettext(False, 'The configuration information of the nails you configured was not found, please add in the settings') + if not (hasattr(get, 'content')): return public.return_msg_gettext(False, 'Please enter the data you need to send') + if self.mail.dingding_send(get.content): + return public.return_msg_gettext(True, 'Sent successfully') + else: + return public.return_msg_gettext(False, 'Failed to send') + + + def getPanelState(self,get): + return os.path.exists(self._setup_path+'/data/close.pl') + + def reload_session(self): + userInfo = public.M('users').where("id=?",(1,)).field('username,password').find() + token = public.Md5(userInfo['username'] + '/' + userInfo['password']) + public.writeFile(self._setup_path+'/data/login_token.pl',token) + skey = 'login_token' + cache.set(skey,token) + sess_path = 'data/sess_files' + if not os.path.exists(sess_path): + os.makedirs(sess_path,384) + self.clean_sess_files(sess_path) + sess_key = public.get_sess_key() + sess_file = os.path.join(sess_path,sess_key) + public.writeFile(sess_file,str(int(time.time()+86400))) + public.set_mode(sess_file,'600') + session['login_token'] = token + + def clean_sess_files(self,sess_path): + ''' + @name 清理过期的sess_file + @auther hwliang<2020-07-25> + @param sess_path(string) sess_files目录 + @return void + ''' + s_time = time.time() + for fname in os.listdir(sess_path): + try: + if len(fname) != 32: continue + sess_file = os.path.join(sess_path,fname) + if not os.path.isfile(sess_file): continue + sess_tmp = public.ReadFile(sess_file) + if not sess_tmp: + if os.path.exists(sess_file): + os.remove(sess_file) + if s_time > int(sess_tmp): + os.remove(sess_file) + except: + pass + + def get_password_safe_file(self): + ''' + @name 获取密码复杂度配置文件 + @auther hwliang<2021-10-18> + @return string + ''' + return public.get_panel_path() + '/data/check_password_safe.pl' + + def check_password_safe(self,password): + ''' + @name 密码复杂度验证 + @auther hwliang<2021-10-18> + @param password(string) 密码 + @return bool + ''' + # 是否检测密码复杂度 + is_check_file = self.get_password_safe_file() + if not os.path.exists(is_check_file): return True + + # 密码长度验证 + if len(password) < 8: return False + + num = 0 + # 密码是否包含数字 + if re.search(r'[0-9]+',password): num += 1 + # 密码是否包含小写字母 + if re.search(r'[a-z]+',password): num += 1 + # 密码是否包含大写字母 + if re.search(r'[A-Z]+',password): num += 1 + # 密码是否包含特殊字符 + if re.search(r'[^\w\s]+',password): num += 1 + # 密码是否包含以上任意3种组合 + if num < 3: return False + return True + + def set_password_safe(self,get): + ''' + @name 设置密码复杂度 + @auther hwliang<2021-10-18> + @param get(string) 参数 + @return dict + ''' + is_check_file = self.get_password_safe_file() + if os.path.exists(is_check_file): + os.remove(is_check_file) + public.WriteLog('TYPE_PANEL','Disable password complexity verification') + return public.returnMsg(True,'Password complexity verification is disabled') + else: + public.writeFile(is_check_file,'True') + public.WriteLog('TYPE_PANEL','Enable password complexity verification') + return public.returnMsg(True,'Password complexity verification has been enabled') + + def get_password_safe(self,get): + ''' + @name 获取密码复杂度 + @auther hwliang<2021-10-18> + @param get(string) 参数 + @return bool + ''' + is_check_file = self.get_password_safe_file() + return os.path.exists(is_check_file) + + + def get_password_expire_file(self): + ''' + @name 获取密码过期配置文件 + @auther hwliang<2021-10-18> + @return string + ''' + return public.get_panel_path() + '/data/password_expire.pl' + + + def set_password_expire(self,get): + ''' + @name 设置密码过期时间 + @auther hwliang<2021-10-18> + @param get{ + expire: int<密码过期时间> 单位:天, + } + @return dict + ''' + expire = int(get.expire) + expire_file = self.get_password_expire_file() + if expire <= 0: + if os.path.exists(expire_file): + os.remove(expire_file) + public.WriteLog('TYPE_PANEL','Disable password expiration authentication') + return public.returnMsg(True,'Password expiration authentication is disabled') + min_expire = 10 + max_expire = 365 * 5 + if expire < min_expire: return public.returnMsg(False,'The password expiration period cannot be less than {} days'.format(min_expire)) + if expire > max_expire: return public.returnMsg(False,'The password expiration period cannot be longer than {} days'.format(max_expire)) + + public.writeFile(self.get_password_expire_file(),str(expire)) + + if expire > 0: + expire_time_file = public.get_panel_path() + '/data/password_expire_time.pl' + public.writeFile(expire_time_file,str(int(time.time()) + (expire * 86400))) + + public.WriteLog('TYPE_PANEL','Set the password expiration time to [{}] days'.format(expire)) + return public.returnMsg(True,'The password expiration time is set to [{}] days'.format(expire)) + + def setlastPassword(self, get): + public.add_security_logs("Change Password", "Successfully used last password!") + self.reload_session() + # 密码过期时间 + expire_time_file = public.get_panel_path() + '/data/password_expire_time.pl' + if os.path.exists(expire_time_file): os.remove(expire_time_file) + self.get_password_config(None) + if session.get('password_expire', False): + session['password_expire'] = False + return public.returnMsg(True, 'USER_PASSWORD_SUCCESS') + + def get_password_config(self,get=None): + ''' + @name 获取密码配置 + @auther hwliang<2021-10-18> + @param get 参数 + @return dict{expire:int,expire_time:int,password_safe:bool} + ''' + expire_file = self.get_password_expire_file() + expire = 0 + expire_time=0 + if os.path.exists(expire_file): + expire = public.readFile(expire_file) + try: + expire = int(expire) + except: + expire = 0 + + # 检查密码过期时间文件是否存在 + expire_time_file = public.get_panel_path() + '/data/password_expire_time.pl' + if not os.path.exists(expire_time_file) and expire > 0: + public.writeFile(expire_time_file,str(int(time.time()) + (expire * 86400))) + + expire_time = public.readFile(expire_time_file) + if expire_time: + expire_time = int(expire_time) + else: + expire_time = 0 + + data = {} + data['expire'] = expire + data['expire_time'] = expire_time + data['password_safe'] = self.get_password_safe(get) + data['ps'] = 'Password expiration configuration is not enabled. For your panel security, please consider enabling it!' + if data['expire_time']: + data['expire_day'] = int((expire_time - time.time()) / 86400) + if data['expire_day'] < 10: + if data['expire_day'] <= 0: + data['ps'] = 'Your password has expired. In case you fail to log in next time, please change your password immediately.' + else: + data['ps'] = "Your panel password will expire in {} days, in order not to affect your normal login, please change the password as soon as possible!".format(data['expire_day']) + else: + data['ps'] = "Your panel password has {} days left to expire!".format(data['expire_day']) + return data + + + def setPassword(self,get): + get.password1 = public.url_decode(public.rsa_decrypt(get.password1)) + get.password2 = public.url_decode(public.rsa_decrypt(get.password2)) + if get.password1 != get.password2: return public.return_msg_gettext(False,'The passwords entered twice are inconsistent, please try again!') + if len(get.password1) < 5: return public.return_msg_gettext(False,'Password cannot be less than 5 characters!') + if not self.check_password_safe(get.password1): return public.returnMsg(False,'The password must be at least eight characters in length and contain at least three combinations of digits, uppercase letters, lowercase letters, and special characters') + public.M('users').where("username=?",(session['username'],)).setField('password',public.password_salt(public.md5(get.password1.strip()),username=session['username'])) + public.write_log_gettext('Panel configuration','Successfully modified password for user [{0}]!',(session['username'],)) + self.reload_session() + + # 密码过期时间 + expire_time_file = public.get_panel_path() + '/data/password_expire_time.pl' + if os.path.exists(expire_time_file): os.remove(expire_time_file) + self.get_password_config(None) + if session.get('password_expire',False): + session['password_expire'] = False + return public.return_msg_gettext(True,'Setup successfully!') + + def setUsername(self,get): + get.username1 = public.url_decode(public.rsa_decrypt(get.username1)) + get.username2 = public.url_decode(public.rsa_decrypt(get.username2)) + if get.username1 != get.username2: return public.return_msg_gettext(False,'The usernames entered twice are inconsistent, plesea try again!') + if len(get.username1) < 3: return public.return_msg_gettext(False,'Username cannot be less than 3 characters') + public.M('users').where("username=?",(session['username'],)).setField('username',get.username1.strip()) + public.write_log_gettext('Panel configuration','Username is modified from [{}] to [{}]',(session['username'],get.username2)) + session['username'] = get.username1 + self.reload_session() + return public.return_msg_gettext(True,'Setup successfully!') + + #取用户列表 + def get_users(self,args): + data = public.M('users').field('id,username').select() + return data + + # 创建新用户 + def create_user(self,args): + args.username = public.url_decode(args.username) + args.password = public.url_decode(args.password) + if session['uid'] != 1: return public.return_msg_gettext(False,'Permission denied!') + if len(args.username) < 2: return public.return_msg_gettext(False,'User name must be at least 2 characters') + if len(args.password) < 8: return public.return_msg_gettext(False,'Password must be at least 8 characters') + pdata = { + "username": args.username.strip(), + "password": public.password_salt(public.md5(args.password.strip()),username=args.username.strip()) + } + + if(public.M('users').where('username=?',(pdata['username'],)).count()): + return public.return_msg_gettext(False,'The specified username already exists!') + + if(public.M('users').insert(pdata)): + public.write_log_gettext('User Management','Create new user {}',(pdata['username'],)) + return public.return_msg_gettext(True,'Create new user {} success!',(pdata['username'],)) + return public.return_msg_gettext(False,'Create new user failed!') + + # 删除用户 + def remove_user(self,args): + if session['uid'] != 1: return public.return_msg_gettext(False,'Permission denied!') + if int(args.id) == 1: return public.return_msg_gettext(False,'Cannot delete initial default user!') + username = public.M('users').where('id=?',(args.id,)).getField('username') + if not username: return public.return_msg_gettext(False,'The specified username not exists!') + if(public.M('users').where('id=?',(args.id,)).delete()): + public.write_log_gettext('User Management','Delete users [{}]',(username)) + return public.return_msg_gettext(True,'Delete user {} success!',(username,)) + return public.return_msg_gettext(False,'User deletion failed!') + + # 修改用户 + def modify_user(self,args): + if session['uid'] != 1: return public.return_msg_gettext(False,'Permission denied!') + username = public.M('users').where('id=?',(args.id,)).getField('username') + pdata = {} + if 'username' in args: + args.username = public.url_decode(args.username) + if len(args.username) < 2: return public.return_msg_gettext(False,'User name must be at least 2 characters') + pdata['username'] = args.username.strip() + + if 'password' in args: + if args.password: + args.password = public.url_decode(args.password) + if len(args.password) < 8: return public.return_msg_gettext(False,'Password must be at least 8 characters') + pdata['password'] = public.password_salt(public.md5(args.password.strip()),username=username) + + if(public.M('users').where('id=?',(args.id,)).update(pdata)): + public.write_log_gettext('User Management',"Edit user {}",(username,)) + return public.return_msg_gettext(True,'Setup successfully!') + return public.return_msg_gettext(False,'No changes submitted') + + def setPanel(self,get): + if not public.IsRestart(): return public.return_msg_gettext(False,'Please run the program when all install tasks finished!') + if 'limitip' in get: + if get.limitip.find('/') != -1: + return public.return_msg_gettext(False,'The authorized IP format is incorrect, and the subnet segment writing is not supported') + isReWeb = False + sess_out_path = 'data/session_timeout.pl' + if 'session_timeout' in get: + try: + session_timeout = int(get.session_timeout) + except: + return public.returnMsg(False,"Timeout must be an integer!") + s_time_tmp = public.readFile(sess_out_path) + if not s_time_tmp: s_time_tmp = '0' + if int(s_time_tmp) != session_timeout: + if session_timeout < 300 or session_timeout > 86400: return public.return_msg_gettext(False,'The timeout time needs to be between 300-86400') + public.writeFile(sess_out_path,str(session_timeout)) + isReWeb = True + else: + return public.returnMsg(False,'Timeout must be an integer!') + + workers_p = 'data/workers.pl' + if 'workers' in get: + workers = int(get.workers) + if int(public.readFile(workers_p)) != workers: + if workers < 1 or workers > 1024: return public.return_msg_gettext(False,public.get_msg_gettext('The number of panel threads should be between 1-1024')) + public.writeFile(workers_p,str(workers)) + isReWeb = True + + if get.domain: + if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", get.domain): return public.return_msg_gettext(False, 'Domain cannot bind ip address') + reg = r"^([\w\-\*]{1,100}\.){1,4}(\w{1,10}|\w{1,10}\.\w{1,10})$" + if not re.match(reg, get.domain): return public.return_msg_gettext(False,'Format of primary domain is incorrect') + if get.address: + if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", get.address): + return public.return_msg_gettext(False, 'Please set the correct Server IP') + oldPort = public.GetHost(True) + if not 'port' in get: + get.port = oldPort + newPort = get.port + if oldPort != get.port: + get.port = str(int(get.port)) + if self.IsOpen(get.port): + return public.return_msg_gettext(False,'Port [{}] is in use!',(get.port,)) + if int(get.port) >= 65535 or int(get.port) < 100: return public.return_msg_gettext(False,'Port range is incorrect! should be between 100-65535') + public.writeFile('data/port.pl',get.port) + import firewalls + get.ps = public.get_msg_gettext('New panel port') + fw = firewalls.firewalls() + fw.AddAcceptPort(get) + get.port = oldPort + get.id = public.M('firewall').where("port=?",(oldPort,)).getField('id') + fw.DelAcceptPort(get) + isReWeb = True + + if get.webname != session['title']: + session['title'] = public.xssencode2(get.webname) + public.SetConfigValue('title',public.xssencode2(get.webname)) + + limitip = public.readFile('data/limitip.conf') + if get.limitip != limitip: + public.writeFile('data/limitip.conf',get.limitip) + cache.set('limit_ip',[]) + + public.writeFile('data/domain.conf',public.xssencode2(get.domain).strip()) + public.writeFile('data/iplist.txt',get.address) + + import files + fs = files.files() + if not fs.CheckDir(get.backup_path): return public.returnMsg(False,'Cannot use system critical directory as default backup directory') + if not fs.CheckDir(get.sites_path): return public.returnMsg(False,'Cannot use system critical directory as default site directory') + public.M('config').where("id=?",('1',)).save('backup_path,sites_path',(get.backup_path,get.sites_path)) + session['config']['backup_path'] = os.path.join('/',get.backup_path) + session['config']['sites_path'] = os.path.join('/',get.sites_path) + db_backup = get.backup_path + '/database' + if not os.path.exists(db_backup): + try: + os.makedirs(db_backup,384) + except: + public.ExecShell('mkdir -p ' + db_backup) + site_backup = get.backup_path + '/site' + if not os.path.exists(site_backup): + try: + os.makedirs(site_backup,384) + except: + public.ExecShell('mkdir -p ' + site_backup) + + mhost = public.GetHost() + if get.domain.strip(): mhost = get.domain + data = {'uri':request.path,'host':mhost+':'+newPort,'status':True,'isReWeb':isReWeb,'msg':public.get_msg_gettext('Saved')} + public.write_log_gettext('Panel configuration','Set panel port [{}], domain [{}], default backup directory [{}], default site directory [{}], server IP [{}], authorized IP [{}]!',(newPort,get.domain,get.backup_path,get.sites_path,get.address,get.limitip)) + if isReWeb: public.restart_panel() + return data + + + def set_admin_path(self,get): + get.admin_path = public.rsa_decrypt(get.admin_path.strip()).strip() + if len(get.admin_path) < 6: return public.return_msg_gettext(False,'Security Entrance cannot be less than 6 characters!') + if get.admin_path in admin_path_checks: return public.return_msg_gettext(False,'This entrance has been used by the panel, please set another entrances!') + if not public.path_safe_check(get.admin_path) or get.admin_path[-1] == '.': return public.returnMsg(False,'Entrance address format is incorrect, e.g. /my_panel') + if get.admin_path[0] != '/': return public.return_msg_gettext(False,'Entrance address format is incorrect, e.g. /my_panel') + if get.admin_path.find("//") != -1: + return public.return_msg_gettext(False, 'Entrance address format is incorrect, e.g. /my_panel') + admin_path_file = 'data/admin_path.pl' + admin_path = '/' + if os.path.exists(admin_path_file): admin_path = public.readFile(admin_path_file).strip() + if get.admin_path != admin_path: + public.writeFile(admin_path_file,get.admin_path) + public.restart_panel() + return public.return_msg_gettext(True, 'Setup successfully!') + + + def setPathInfo(self,get): + #设置PATH_INFO + version = get.version + type = get.type + if public.get_webserver() == 'nginx': + path = public.GetConfigValue('setup_path')+'/nginx/conf/enable-php-'+version+'.conf' + conf = public.readFile(path) + rep = r"\s+#*include\s+pathinfo.conf;" + if type == 'on': + conf = re.sub(rep,'\n\t\t\tinclude pathinfo.conf;',conf) + else: + conf = re.sub(rep,'\n\t\t\t#include pathinfo.conf;',conf) + public.writeFile(path,conf) + public.serviceReload() + + path = public.GetConfigValue('setup_path')+'/php/'+version+'/etc/php.ini' + conf = public.readFile(path) + rep = r"\n*\s*cgi\.fix_pathinfo\s*=\s*([0-9]+)\s*\n" + status = '0' + if type == 'on':status = '1' + conf = re.sub(rep,"\ncgi.fix_pathinfo = "+status+"\n",conf) + public.writeFile(path,conf) + public.write_log_gettext("PHP configuration", "Set PATH_INFO module to [{}] for PHP-{}!",(version,type)) + public.phpReload(version) + return public.return_msg_gettext(True,'Setup successfully!') + + + #设置文件上传大小限制 + def setPHPMaxSize(self,get): + version = get.version + max = get.max + if int(max) < 2: return public.return_msg_gettext(False,'Limit of upload size cannot be less than 2 MB') + #设置PHP + path = public.GetConfigValue('setup_path')+'/php/'+version+'/etc/php.ini' + ols_php_path = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version, get.version[0],get.version[1]) + if os.path.exists('/etc/redhat-release'): + ols_php_path = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + for p in [path,ols_php_path]: + if not p: + continue + if not os.path.exists(p): + continue + conf = public.readFile(p) + rep = r"\nupload_max_filesize\s*=\s*[0-9]+M?m?" + conf = re.sub(rep,r'\nupload_max_filesize = '+max+'M',conf) + rep = r"\npost_max_size\s*=\s*[0-9]+M?m?" + conf = re.sub(rep,r'\npost_max_size = '+max+'M',conf) + public.writeFile(p,conf) + + if public.get_webserver() == 'nginx': + #设置Nginx + path = public.GetConfigValue('setup_path')+'/nginx/conf/nginx.conf' + conf = public.readFile(path) + rep = r"client_max_body_size\s+([0-9]+)m?M?" + tmp = re.search(rep,conf).groups() + if int(tmp[0]) < int(max): + conf = re.sub(rep,'client_max_body_size '+max+'m',conf) + public.writeFile(path,conf) + + public.serviceReload() + public.phpReload(version) + public.write_log_gettext("PHP configuration", "Set max upload size to [{} MB] for PHP-{}!",(version,max)) + return public.return_msg_gettext(True,'Setup successfully!') + + #设置禁用函数 + def setPHPDisable(self,get): + filename = public.GetConfigValue('setup_path') + '/php/' + get.version + '/etc/php.ini' + ols_php_path = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version, get.version[0],get.version[1]) + if os.path.exists('/etc/redhat-release'): + ols_php_path = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + if not os.path.exists(filename): return public.return_msg_gettext(False,'Requested PHP version does NOT exist!') + for file in [filename,ols_php_path]: + if not os.path.exists(file): + continue + phpini = public.readFile(file) + rep = r"disable_functions\s*=\s*.*\n" + phpini = re.sub(rep, 'disable_functions = ' + get.disable_functions + "\n", phpini) + public.write_log_gettext('PHP configuration','Modified disabled function to [{}] for PHP-{}',(get.version,get.disable_functions)) + public.writeFile(file,phpini) + public.phpReload(get.version) + public.serviceReload() + return public.return_msg_gettext(True,'Setup successfully!') + + #设置PHP超时时间 + def setPHPMaxTime(self,get): + time = get.time + version = get.version + if int(time) < 30 or int(time) > 86400: return public.return_msg_gettext(False,'Please fill in the value between 30 and 86400!') + file = public.GetConfigValue('setup_path')+'/php/'+version+'/etc/php-fpm.conf' + conf = public.readFile(file) + rep = r"request_terminate_timeout\s*=\s*([0-9]+)\n" + conf = re.sub(rep,"request_terminate_timeout = "+time+"\n",conf) + public.writeFile(file,conf) + + file = '/www/server/php/'+version+'/etc/php.ini' + phpini = public.readFile(file) + rep = r"max_execution_time\s*=\s*([0-9]+)\r?\n" + phpini = re.sub(rep,"max_execution_time = "+time+"\n",phpini) + rep = r"max_input_time\s*=\s*([0-9]+)\r?\n" + phpini = re.sub(rep,"max_input_time = "+time+"\n",phpini) + public.writeFile(file,phpini) + + if public.get_webserver() == 'nginx': + #设置Nginx + path = public.GetConfigValue('setup_path')+'/nginx/conf/nginx.conf' + conf = public.readFile(path) + rep = r"fastcgi_connect_timeout\s+([0-9]+);" + tmp = re.search(rep, conf).groups() + if int(tmp[0]) < int(time): + conf = re.sub(rep,'fastcgi_connect_timeout '+time+';',conf) + rep = r"fastcgi_send_timeout\s+([0-9]+);" + conf = re.sub(rep,'fastcgi_send_timeout '+time+';',conf) + rep = r"fastcgi_read_timeout\s+([0-9]+);" + conf = re.sub(rep,'fastcgi_read_timeout '+time+';',conf) + public.writeFile(path,conf) + + public.write_log_gettext("PHP configuration", "Set maximum time of script to [{} second] for PHP-{}!",(version,time)) + public.serviceReload() + public.phpReload(version) + return public.return_msg_gettext(True, 'Setup successfully!') + + + #取FPM设置 + def getFpmConfig(self,get): + version = get.version + file = public.GetConfigValue('setup_path')+"/php/"+version+"/etc/php-fpm.conf" + conf = public.readFile(file) + data = {} + rep = r"\s*pm.max_children\s*=\s*([0-9]+)\s*" + tmp = re.search(rep, conf).groups() + data['max_children'] = tmp[0] + + rep = r"\s*pm.start_servers\s*=\s*([0-9]+)\s*" + tmp = re.search(rep, conf).groups() + data['start_servers'] = tmp[0] + + rep = r"\s*pm.min_spare_servers\s*=\s*([0-9]+)\s*" + tmp = re.search(rep, conf).groups() + data['min_spare_servers'] = tmp[0] + + rep = r"\s*pm.max_spare_servers \s*=\s*([0-9]+)\s*" + tmp = re.search(rep, conf).groups() + data['max_spare_servers'] = tmp[0] + + rep = r"\s*pm\s*=\s*(\w+)\s*" + tmp = re.search(rep, conf).groups() + data['pm'] = tmp[0] + + rep = r"\s*listen.allowed_clients\s*=\s*([\w\.,/]+)\s*" + tmp = re.search(rep, conf).groups() + data['allowed'] = tmp[0] + + + data['unix'] = 'unix' + data['port'] = '' + data['bind'] = '/tmp/php-cgi-{}.sock'.format(version) + + fpm_address = public.get_fpm_address(version,True) + if not isinstance(fpm_address,str): + data['unix'] = 'tcp' + data['port'] = fpm_address[1] + data['bind'] = fpm_address[0] + + return data + + + #设置 + def setFpmConfig(self,get): + version = get.version + max_children = get.max_children + start_servers = get.start_servers + min_spare_servers = get.min_spare_servers + max_spare_servers = get.max_spare_servers + pm = get.pm + if not pm in ['static','dynamic','ondemand']: + return public.return_msg_gettext(False,'Wrong operating mode') + file = public.GetConfigValue('setup_path')+"/php/"+version+"/etc/php-fpm.conf" + conf = public.readFile(file) + + rep = r"\s*pm.max_children\s*=\s*([0-9]+)\s*" + conf = re.sub(rep, "\npm.max_children = "+max_children, conf) + + rep = r"\s*pm.start_servers\s*=\s*([0-9]+)\s*" + conf = re.sub(rep, "\npm.start_servers = "+start_servers, conf) + + rep = r"\s*pm.min_spare_servers\s*=\s*([0-9]+)\s*" + conf = re.sub(rep, "\npm.min_spare_servers = "+min_spare_servers, conf) + + rep = r"\s*pm.max_spare_servers \s*=\s*([0-9]+)\s*" + conf = re.sub(rep, "\npm.max_spare_servers = "+max_spare_servers+"\n", conf) + + rep = r"\s*pm\s*=\s*(\w+)\s*" + conf = re.sub(rep, "\npm = "+pm+"\n", conf) + if pm == 'ondemand': + if conf.find('listen.backlog = -1') != -1: + rep = r"\s*listen\.backlog\s*=\s*([0-9-]+)\s*" + conf = re.sub(rep, "\nlisten.backlog = 8192\n", conf) + + if get.listen == 'unix': + listen = '/tmp/php-cgi-{}.sock'.format(version) + else: + default_listen = '127.0.0.1:10{}1'.format(version) + if 'bind_port' in get: + if get.bind_port.find('sock') != -1: + listen = default_listen + else: + listen = get.bind_port + else: + listen = default_listen + + + rep = r'\s*listen\s*=\s*.+\s*' + conf = re.sub(rep, "\nlisten = "+listen+"\n", conf) + + if 'allowed' in get: + if not get.allowed: get.allowed = '127.0.0.1' + rep = r"\s*listen.allowed_clients\s*=\s*([\w\.,/]+)\s*" + conf = re.sub(rep, "\nlisten.allowed_clients = "+get.allowed+"\n", conf) + + public.writeFile(file,conf) + public.phpReload(version) + public.sync_php_address(version) + public.write_log_gettext("PHP configuration",'Set concurrency of PHP-{}, max_children={}, start_servers={}, min_spare_servers={}, max_spare_servers={}', (version,max_children,start_servers,min_spare_servers,max_spare_servers)) + return public.return_msg_gettext(True, 'Setup successfully!') + + #同步时间 + def syncDate(self,get): + """ + @name 同步时间 + @author hezhihong + """ + #取国际标准0时时间戳 + time_str = public.HttpGet(public.GetConfigValue('home') + '/api/index/get_time') + try: + new_time = int(time_str)-28800 + except: + return public.returnMsg(False,'Failed to connect to the time server!') + if not new_time: public.returnMsg(False,'Failed to connect to the time server!') + #取所在时区偏差秒数 + add_time='+0000' + try: + add_time=public.ExecShell('date +"%Y-%m-%d %H:%M:%S %Z %z"')[0].replace('\n','').strip().split()[-1] + print(add_time) + except:pass + add_1=False + if add_time[0]=='+': + add_1=True + add_v=int(add_time[1:-2])*3600+int(add_time[-2:])*60 + if add_1: + new_time+=add_v + else:new_time-=add_v + #设置所在时区时间 + date_str = public.format_date(times=new_time) + public.ExecShell('date -s "%s"' % date_str) + public.write_log_gettext("Panel configuration", 'Update Succeeded!') + return public.return_msg_gettext(True,'Setup successfully!') + + def IsOpen(self,port): + #检查端口是否占用 + import socket + s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + try: + s.connect(('127.0.0.1',int(port))) + s.shutdown(2) + return True + except: + return False + + #设置是否开启监控 + def SetControl(self,get): + try: + if hasattr(get,'day'): + get.day = int(get.day) + get.day = str(get.day) + if(get.day < 1): return public.return_msg_gettext(False,'Number of saving days is illegal!') + except: + pass + + filename = 'data/control.conf' + if get.type == '1': + public.writeFile(filename,get.day) + public.write_log_gettext("Panel configuration",'Turned on monitory service, save for [{}] day!',(get.day,)) + elif get.type == '0': + if os.path.exists(filename): os.remove(filename) + public.write_log_gettext("Panel configuration", 'Monitor service turned off!') + elif get.type == 'del': + if not public.IsRestart(): return public.return_msg_gettext(False,'Please run the program when all install tasks finished!') + os.remove("data/system.db") + import db + sql = db.Sql() + sql.dbfile('system').create('system') + public.write_log_gettext("Panel configuration", 'Monitor service turned off!') + return public.return_msg_gettext(True,'Setup successfully!') + + else: + data = {} + if os.path.exists(filename): + try: + data['day'] = int(public.readFile(filename)) + except: + data['day'] = 30 + data['status'] = True + else: + data['day'] = 30 + data['status'] = False + return data + + return public.return_msg_gettext(True,'Successfully set') + + #关闭面板 + def ClosePanel(self,get): + filename = 'data/close.pl' + if os.path.exists(filename): + os.remove(filename) + return public.returnMsg(True, 'Setup successfully!') + public.writeFile(filename, 'True') + public.ExecShell("chmod 600 " + filename) + public.ExecShell("chown root.root " + filename) + return public.return_msg_gettext(True,'Setup successfully!') + + + #设置自动更新 + def AutoUpdatePanel(self,get): + #return public.returnMsg(False,'体验服务器,禁止修改!') + filename = 'data/autoUpdate.pl' + if os.path.exists(filename): + os.remove(filename) + else: + public.writeFile(filename,'True') + public.ExecShell("chmod 600 " + filename) + public.ExecShell("chown root.root " + filename) + return public.return_msg_gettext(True,'Setup successfully!') + + #设置二级密码 + def SetPanelLock(self,get): + path = 'data/lock' + if not os.path.exists(path): + public.ExecShell('mkdir ' + path) + public.ExecShell("chmod 600 " + path) + public.ExecShell("chown root.root " + path) + + keys = ['files','tasks','config'] + for name in keys: + filename = path + '/' + name + '.pl' + if hasattr(get,name): + public.writeFile(filename,'True') + else: + if os.path.exists(filename): os.remove(filename); + + #设置PHP守护程序 + def Set502(self,get): + filename = 'data/502Task.pl' + if os.path.exists(filename): + public.ExecShell('rm -f ' + filename) + else: + public.writeFile(filename,'True') + + return public.return_msg_gettext(True,'Setup successfully!') + + #设置模板 + def SetTemplates(self,get): + public.writeFile('data/templates.pl',get.templates) + return public.return_msg_gettext(True,'Setup successfully!') + + #设置面板SSL + def SetPanelSSL(self, get): + if not os.path.exists("/www/server/panel/ssl/"): + os.makedirs("/www/server/panel/ssl/") + if hasattr(get, "cert_type") and str(get.cert_type) == "2": + # rep_mail = r"[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?" + # if not re.search(rep_mail,get.email): + # return public.return_msg_gettext(False,'The E-Mail format is illegal') + import setPanelLets + sp = setPanelLets.setPanelLets() + sps = sp.set_lets(get) + return sps + else: + sslConf = self._setup_path + '/data/ssl.pl' + if os.path.exists(sslConf) and not 'cert_type' in get: + public.ExecShell('rm -f ' + sslConf + '&& rm -f /www/server/panel/ssl/*') + g.rm_ssl = True + return public.return_msg_gettext(True, 'SSL turned off,Please use http protocol to access the panel!') + else: + public.ExecShell('btpip install cffi') + public.ExecShell('btpip install cryptography') + public.ExecShell('btpip install pyOpenSSL') + if not 'cert_type' in get: + return public.returnMsg(False,'Please refresh the page and try again!') + if get.cert_type in [0,'0']: + result = self.SavePanelSSL(get) + if not result['status']: return result + public.writeFile(sslConf,'True') + public.writeFile('data/reload.pl','True') + try: + if not self.CreateSSL(): + return public.return_msg_gettext(False, + 'Error, unable to auto install pyOpenSSL!

                                    Plesea try to manually install: pip install pyOpenSSL

                                    ') + public.writeFile(sslConf, 'True') + except: + return public.return_msg_gettext(False, + 'Error, unable to auto install pyOpenSSL!

                                    Plesea try to manually install: pip install pyOpenSSL

                                    ') + return public.return_msg_gettext(True, + 'SSL is turned on, plesea use https protocol to access the panel!') + #自签证书 + # def CreateSSL(self): + # if os.path.exists('ssl/input.pl'): return True + # import OpenSSL + # key = OpenSSL.crypto.PKey() + # key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) + # cert = OpenSSL.crypto.X509() + # cert.set_serial_number(0) + # cert.get_subject().CN = public.GetLocalIp() + # cert.set_issuer(cert.get_subject()) + # cert.gmtime_adj_notBefore( 0 ) + # cert.gmtime_adj_notAfter(86400 * 3650) + # cert.set_pubkey( key ) + # cert.sign( key, 'md5' ) + # cert_ca = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) + # private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + # if len(cert_ca) > 100 and len(private_key) > 100: + # public.writeFile('ssl/certificate.pem',cert_ca,'wb+') + # public.writeFile('ssl/privateKey.pem',private_key,'wb+') + # return True + # return False + # 自签证书 + + def CreateSSL(self): + import base64 + userInfo = public.get_user_info() + if not userInfo: + userInfo['uid'] = 0 + userInfo['access_key'] = 'B' * 32 + domains = self.get_host_all() + pdata = { + "action": "get_domain_cert", + "company": "aapanel.com", + "domain": ','.join(domains), + "uid": userInfo['uid'], + "access_key": 'B' * 32, + "panel": 1 + } + cert_api = 'https://api.aapanel.com/aapanel_cert' + result = json.loads(public.httpPost(cert_api, {'data': json.dumps(pdata)})) + if 'status' in result: + if result['status']: + if os.path.exists('ssl/certificate.pem'): + os.remove('ssl/certificate.pem') + if os.path.exists('ssl/privateKey.pem'): + os.remove('ssl/privateKey.pem') + if os.path.exists('ssl/baota_root.pfx'): + os.remove('ssl/baota_root.pfx') + if os.path.exists('ssl/root_password.pl'): + os.remove('ssl/root_password.pl') + public.writeFile('ssl/certificate.pem', result['cert']) + public.writeFile('ssl/privateKey.pem', result['key']) + public.writeFile('ssl/baota_root.pfx', base64.b64decode(result['pfx']), 'wb+') + public.writeFile('ssl/root_password.pl', result['password']) + public.writeFile('data/ssl.pl', 'True') + # public.ExecShell("/etc/init.d/bt reload") + print('1') + return True + if os.path.exists('ssl/input.pl'): return True + import OpenSSL + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) + cert = OpenSSL.crypto.X509() + cert.set_serial_number(0) + cert.get_subject().CN = public.GetLocalIp() + cert.set_issuer(cert.get_subject()) + cert.gmtime_adj_notBefore( 0 ) + cert.gmtime_adj_notAfter(86400 * 3650) + cert.set_pubkey( key ) + cert.sign( key, 'md5' ) + cert_ca = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) + private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + + if len(cert_ca) > 100 and len(private_key) > 100: + public.writeFile('ssl/certificate.pem',cert_ca,'wb+') + public.writeFile('ssl/privateKey.pem',private_key,'wb+') + return True + return False + + def get_ipaddress(self): + ''' + @name 获取本机IP地址 + @author hwliang<2020-11-24> + @return list + ''' + ipa_tmp = \ + public.ExecShell("ip a |grep inet|grep -v inet6|grep -v 127.0.0.1|awk '{print $2}'|sed 's#/[0-9]*##g'")[ + 0].strip() + iplist = ipa_tmp.split('\n') + return iplist + + def get_host_all(self): + local_ip = ['127.0.0.1', '::1', 'localhost'] + ip_list = [] + bind_ip = self.get_ipaddress() + + for ip in bind_ip: + ip = ip.strip() + if ip in local_ip: continue + if ip in ip_list: continue + ip_list.append(ip) + net_ip = public.httpGet("https://www.aapanel.com/api/common/getClientIP") + + if net_ip: + net_ip = net_ip.strip() + if not net_ip in ip_list: + ip_list.append(net_ip) + ip_list = [ip_list[-1], ip_list[0]] + return ip_list + #生成Token + def SetToken(self,get): + data = {} + data[''] = public.GetRandomString(24) + + #取面板列表 + def GetPanelList(self,get): + try: + data = public.M('panel').field('id,title,url,username,password,click,addtime').order('click desc').select() + if type(data) == str: data[111] + return public.return_message(0,0,data) + except: + sql = '''CREATE TABLE IF NOT EXISTS `panel` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `title` TEXT, + `url` TEXT, + `username` TEXT, + `password` TEXT, + `click` INTEGER, + `addtime` INTEGER +);''' + public.M('sites').execute(sql,()) + return public.return_message(0,0,[]) + + #添加面板资料 + def AddPanelInfo(self,get): + + #校验是还是重复 + isAdd = public.M('panel').where('title=? OR url=?',(get.title,get.url)).count() + if isAdd: return public.return_msg_gettext(False,'Notes or panel address duplicate!') + import time,json + isRe = public.M('panel').add('title,url,username,password,click,addtime',(public.xssencode2(get.title),public.xssencode2(get.url),public.xssencode2(get.username),get.password,0,int(time.time()))) + if isRe: return public.return_msg_gettext(True,'Setup successfully!') + return public.return_msg_gettext(False,'Failed to add') + + #修改面板资料 + def SetPanelInfo(self,get): + #校验是还是重复 + isSave = public.M('panel').where('(title=? OR url=?) AND id!=?',(get.title,get.url,get.id)).count() + if isSave: return public.return_msg_gettext(False,'Notes or panel address duplicate!') + import time,json + + #更新到数据库 + isRe = public.M('panel').where('id=?',(get.id,)).save('title,url,username,password',(get.title,get.url,get.username,get.password)) + if isRe: return public.return_msg_gettext(True,'Setup successfully!') + return public.return_msg_gettext(False,'Failed to modify') + + #删除面板资料 + def DelPanelInfo(self,get): + isExists = public.M('panel').where('id=?',(get.id,)).count() + if not isExists: return public.return_msg_gettext(False,'Requested panel info does NOT exist!') + public.M('panel').where('id=?',(get.id,)).delete() + return public.return_msg_gettext(True,'Successfully deleted') + + #点击计数 + def ClickPanelInfo(self,get): + click = public.M('panel').where('id=?',(get.id,)).getField('click') + public.M('panel').where('id=?',(get.id,)).setField('click',click+1) + return True + + #获取PHP配置参数 + def GetPHPConf(self,get): + gets = [ + {'name':'short_open_tag','type':1,'ps':public.get_msg_gettext('Short tag support')}, + {'name':'asp_tags','type':1,'ps':public.get_msg_gettext('ASP tag support')}, + {'name':'max_execution_time','type':2,'ps':public.get_msg_gettext('Max time of running script')}, + {'name':'max_input_time','type':2,'ps':public.get_msg_gettext('Max time of input')}, + {'name':'memory_limit','type':2,'ps':public.get_msg_gettext('Limit of script memory')}, + {'name':'post_max_size','type':2,'ps':public.get_msg_gettext('Max size of POST data')}, + {'name':'file_uploads','type':1,'ps':public.get_msg_gettext('Whether to allow upload file')}, + {'name':'upload_max_filesize','type':2,'ps':public.get_msg_gettext('Max size of upload file')}, + {'name':'max_file_uploads','type':2,'ps':public.get_msg_gettext('Max value of simultaneously upload file')}, + {'name':'default_socket_timeout','type':2,'ps':public.get_msg_gettext('Socket over time')}, + {'name':'error_reporting','type':3,'ps':public.get_msg_gettext('Level of error')}, + {'name':'display_errors','type':1,'ps':public.get_msg_gettext('Whether to output detailed error info')}, + {'name':'cgi.fix_pathinfo','type':0,'ps':public.get_msg_gettext('Whether to turn on pathinfo')}, + {'name':'date.timezone','type':3,'ps':public.get_msg_gettext('Timezone')} + ] + phpini_file = '/www/server/php/' + get.version + '/etc/php.ini' + if public.get_webserver() == 'openlitespeed': + phpini_file = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version, get.version[0],get.version[1]) + if os.path.exists('/etc/redhat-release'): + phpini_file = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + phpini = public.readFile(phpini_file) + if not phpini: + return public.return_msg_gettext(False,"Error reading PHP configuration file, please try to reinstall this PHP!") + result = [] + for g in gets: + rep = g['name'] + r'\s*=\s*([0-9A-Za-z_&/ ~]+)(\s*;?|\r?\n)' + tmp = re.search(rep,phpini) + if not tmp: continue + g['value'] = tmp.groups()[0] + result.append(g) + + return result + + + def get_php_config(self,get): + #取PHP配置 + get.version = get.version.replace('.','') + file = session['setupPath'] + "/php/"+get.version+"/etc/php.ini" + if public.get_webserver() == 'openlitespeed': + file = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version, get.version[0],get.version[1]) + if os.path.exists('/etc/redhat-release'): + file = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + phpini = public.readFile(file) + file = session['setupPath'] + "/php/"+get.version+"/etc/php-fpm.conf" + phpfpm = public.readFile(file) + data = {} + try: + rep = r"upload_max_filesize\s*=\s*([0-9]+)M" + tmp = re.search(rep,phpini).groups() + data['max'] = tmp[0] + except: + data['max'] = '50' + try: + rep = r"request_terminate_timeout\s*=\s*([0-9]+)\n" + tmp = re.search(rep,phpfpm).groups() + data['maxTime'] = tmp[0] + except: + data['maxTime'] = 0 + + try: + rep = r"\n;*\s*cgi\.fix_pathinfo\s*=\s*([0-9]+)\s*\n" + tmp = re.search(rep,phpini).groups() + + if tmp[0] == '1': + data['pathinfo'] = True + else: + data['pathinfo'] = False + except: + data['pathinfo'] = False + + return data + + #提交PHP配置参数 + def SetPHPConf(self,get): + gets = ['display_errors','cgi.fix_pathinfo','date.timezone','short_open_tag','asp_tags','max_execution_time','max_input_time','memory_limit','post_max_size','file_uploads','upload_max_filesize','max_file_uploads','default_socket_timeout','error_reporting'] + filename = '/www/server/php/' + get.version + '/etc/php.ini' + reload_str = '/etc/init.d/php-fpm-' + get.version + ' reload' + ols_php_path = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version, get.version[0],get.version[1]) + if os.path.exists('/etc/redhat-release'): + ols_php_path = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + reload_ols_str = '/usr/local/lsws/bin/lswsctrl restart' + for p in [filename,ols_php_path]: + if not p: + continue + if not os.path.exists(p): + continue + phpini = public.readFile(p) + for g in gets: + try: + rep = g + r'\s*=\s*(.+)\r?\n' + val = g+' = ' + get[g] + '\n' + phpini = re.sub(rep,val,phpini) + except: continue + + public.writeFile(p,phpini) + public.ExecShell(reload_str) + public.ExecShell(reload_ols_str) + return public.return_msg_gettext(True,'Setup successfully!') + + + # 取Session缓存方式 + def GetSessionConf(self,get): + filename = '/www/server/php/' + get.version + '/etc/php.ini' + if public.get_webserver() == 'openlitespeed': + filename = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version,get.version[0],get.version[1]) + if os.path.exists('/etc/redhat-release'): + filename = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + phpini = public.readFile(filename) + rep = r'session.save_handler\s*=\s*([0-9A-Za-z_& ~]+)(\s*;?|\r?\n)' + save_handler = re.search(rep, phpini) + if save_handler: + save_handler = save_handler.group(1) + else: + save_handler = "files" + + reppath = r'\nsession.save_path\s*=\s*"tcp\:\/\/([\w\.]+):(\d+).*\r?\n' + passrep = r'\nsession.save_path\s*=\s*"tcp://[\w\.\?\:]+=(.*)"\r?\n' + memcached = r'\nsession.save_path\s*=\s*"([\w\.]+):(\d+)"' + save_path = re.search(reppath, phpini) + if not save_path: + save_path = re.search(memcached, phpini) + passwd = re.search(passrep, phpini) + port = "" + if passwd: + passwd = passwd.group(1) + else: + passwd = "" + if save_path: + port = save_path.group(2) + save_path = save_path.group(1) + + else: + save_path = "" + return {"save_handler": save_handler, "save_path": save_path, "passwd": passwd, "port": port} + + # 设置Session缓存方式 + def SetSessionConf(self, get): + import glob + g = get.save_handler + ip = get.ip.strip() + port = get.port + passwd = get.passwd + if g != "files": + iprep = r"(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})" + rep_domain = r"^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" + if not re.search(iprep, ip) and not re.search(rep_domain, ip): + if ip != "localhost": + return public.returnMsg(False, 'Please enter the correct [domain or IP]!') + try: + port = int(port) + if port >= 65535 or port < 1: + return public.return_msg_gettext(False, 'Port range is incorrect! should be between 100-65535') + except: + return public.return_msg_gettext(False, 'Port range is incorrect! should be between 100-65535') + prep = r"[\~\`\/\=]" + if re.search(prep, passwd): + return public.return_msg_gettext(False, 'Please do NOT enter the following special characters {}', ('" ~ ` / = "')) + filename = '/www/server/php/' + get.version + '/etc/php.ini' + filename_ols = None + ols_exist = os.path.exists("/usr/local/lsws/bin/lswsctrl") + if ols_exist: + filename_ols = '/usr/local/lsws/lsphp{}/etc/php/{}.{}/litespeed/php.ini'.format(get.version, get.version[0], + get.version[1]) + if os.path.exists('/etc/redhat-release'): + filename_ols = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini' + try: + ols_php_os_path = glob.glob("/usr/local/lsws/lsphp{}/lib/php/20*".format(get.version))[0] + except: + ols_php_os_path = None + if os.path.exists("/etc/redhat-release"): + ols_php_os_path = '/usr/local/lsws/lsphp{}/lib64/php/modules/'.format(get.version) + ols_so_list = os.listdir(ols_php_os_path) + else: + ols_so_list = [] + for f in [filename,filename_ols]: + if not f: + continue + phpini = public.readFile(f) + rep = r'session.save_handler\s*=\s*(.+)\r?\n' + val = r'session.save_handler = ' + g + '\n' + phpini = re.sub(rep, val, phpini) + if not ols_exist: + if g == "memcached": + if not re.search("memcached.so", phpini): + return public.return_msg_gettext(False, 'Please install the {} extension first.', (g,)) + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "%s:%s" \n' % (ip,port) + if re.search(rep, phpini): + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + if g == "memcache": + if not re.search("memcache.so", phpini): + return public.return_msg_gettext(False, 'Please install the {} extension first.', (g,)) + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "tcp://%s:%s"\n' % (ip, port) + if re.search(rep, phpini): + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + if g == "redis": + if not re.search("redis.so", phpini): + return public.return_msg_gettext(False, 'Please install the {} extension first.', (g,)) + if passwd: + passwd = "?auth=" + passwd + else: + passwd = "" + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "tcp://%s:%s%s"\n' % (ip, port, passwd) + res = re.search(rep, phpini) + if res: + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + if g == "files": + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "/tmp"\n' + if re.search(rep, phpini): + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + else: + if g == "memcached": + if "memcached.so" not in ols_so_list: + return public.return_msg_gettext(False, 'Please install the {} extension first.', (g,)) + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "%s:%s" \n' % (ip,port) + if re.search(rep, phpini): + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + if g == "memcache": + if "memcache.so" not in ols_so_list: + return public.return_msg_gettext(False, 'Please install the {} extension first.', (g,)) + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "tcp://%s:%s"\n' % (ip, port) + if re.search(rep, phpini): + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + if g == "redis": + if "redis.so" not in ols_so_list: + return public.return_msg_gettext(False, 'Please install the {} extension first.', (g,)) + if passwd: + passwd = "?auth=" + passwd + else: + passwd = "" + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "tcp://%s:%s%s"\n' % (ip, port, passwd) + res = re.search(rep, phpini) + if res: + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + if g == "files": + rep = r'\nsession.save_path\s*=\s*(.+)\r?\n' + val = r'\nsession.save_path = "/tmp"\n' + if re.search(rep, phpini): + phpini = re.sub(rep, val, phpini) + else: + phpini = re.sub('\n;session.save_path = "/tmp"', '\n;session.save_path = "/tmp"' + val, phpini) + public.writeFile(f, phpini) + public.ExecShell('/etc/init.d/php-fpm-' + get.version + ' reload') + public.serviceReload() + return public.return_msg_gettext(True, 'Setup successfully!') + + # 获取Session文件数量 + def GetSessionCount(self, get): + d=["/tmp","/www/php_session"] + + count = 0 + for i in d: + if not os.path.exists(i): public.ExecShell('mkdir -p %s'%i) + list = os.listdir(i) + for l in list: + if os.path.isdir(i+"/"+l): + l1 = os.listdir(i+"/"+l) + for ll in l1: + if "sess_" in ll: + count += 1 + continue + if "sess_" in l: + count += 1 + + s = "find /tmp -mtime +1 |grep 'sess_'|wc -l" + old_file = int(public.ExecShell(s)[0].split("\n")[0]) + + s = "find /www/php_session -mtime +1 |grep 'sess_'|wc -l" + old_file += int(public.ExecShell(s)[0].split("\n")[0]) + + return {"total":count,"oldfile":old_file} + + # 删除老文件 + def DelOldSession(self,get): + s = "find /tmp -mtime +1 |grep 'sess_'|xargs rm -f" + public.ExecShell(s) + s = "find /www/php_session -mtime +1 |grep 'sess_'|xargs rm -f" + public.ExecShell(s) + # s = "find /tmp -mtime +1 |grep 'sess_'|wc -l" + # old_file_conf = int(public.ExecShell(s)[0].split("\n")[0]) + old_file_conf = self.GetSessionCount(get)["oldfile"] + if old_file_conf == 0: + return public.return_msg_gettext(True, 'Successfully deleted') + else: + return public.return_msg_gettext(True, 'Failed to delete') + + #获取面板证书 + def GetPanelSSL(self,get): + cert = {} + key_file = 'ssl/privateKey.pem' + cert_file = 'ssl/certificate.pem' + if not os.path.exists(key_file): + self.CreateSSL() + cert['privateKey'] = public.readFile(key_file) + cert['certPem'] = public.readFile(cert_file) + cert['download_root'] = False + cert['info'] = {} + if not cert['privateKey']: + cert['privateKey'] = '' + cert['certPem'] = '' + else: + cert['info'] = public.get_cert_data(cert_file) + if not cert['info']: + self.CreateSSL() + cert['info'] = public.get_cert_data(cert_file) + if cert['info']: + if cert['info']['issuer'] == '宝塔面板': + if os.path.exists('ssl/baota_root.pfx'): + cert['download_root'] = True + cert['root_password'] = public.readFile('ssl/root_password.pl') + + + + cert['rep'] = os.path.exists('ssl/input.pl') + return cert + + #保存面板证书 + def SavePanelSSL(self,get): + keyPath = 'ssl/privateKey.pem' + certPath = 'ssl/certificate.pem' + checkCert = '/tmp/cert.pl' + ssl_pl = 'data/ssl.pl' + if not 'certPem' in get: return public.returnMsg(False,'The certPem parameter is missing!') + if not 'privateKey' in get: return public.returnMsg(False,'The privateKey parameter is missing!') + public.writeFile(checkCert,get.certPem) + if get.privateKey: + public.writeFile(keyPath,get.privateKey) + if get.certPem: + public.writeFile(certPath, get.certPem) + if not public.CheckCert(checkCert): return public.return_msg_gettext(False, 'Certificate ERROR, please check!') + public.writeFile('ssl/input.pl','True') + if os.path.exists(ssl_pl): public.writeFile('data/reload.pl','True') + return public.return_msg_gettext(True, 'Certificate saved!') + + # 获取ftp端口 + def get_ftp_port(self): + # 获取FTP端口 + if 'port' in session: return session['port'] + import re + try: + file = public.GetConfigValue('setup_path') + '/pure-ftpd/etc/pure-ftpd.conf' + conf = public.readFile(file) + rep = r"\n#?\s*Bind\s+[0-9]+\.[0-9]+\.[0-9]+\.+[0-9]+,([0-9]+)" + port = re.search(rep, conf).groups()[0] + except: + port = '21' + session['port'] = port + return port + + # #获取配置 + # def get_config(self,get): + # data = {} + # if 'config' in session: + # session['config']['distribution'] = public.get_linux_distribution() + # session['webserver'] = public.get_webserver() + # session['config']['webserver'] = session['webserver'] + # data = session['config'] + # if not data: + # data = public.M('config').where("id=?",('1',)).field('webserver,sites_path,backup_path,status,mysql_root').find() + # data['webserver'] = public.get_webserver() + # data['distribution'] = public.get_linux_distribution() + # data['request_iptype'] = self.get_request_iptype() + # data['request_type'] = self.get_request_type() + # # return data + # return public.return_message(0, 0, data) + + + def get_config(self, get): + public.print_log('v2-get_config') + import system_v2 as system + data = {} + data.update(system.system().GetConcifInfo()) + if 'config' in session: + session['config']['distribution'] = public.get_linux_distribution() + session['webserver'] = public.get_webserver() + session['config']['webserver'] = session['webserver'] + if "basic_auth" in session['config'].keys(): + session['config']['basic_auth'] = self.get_basic_auth_stat(None) + data = session['config'] + if not data: + data = public.M('config').where("id=?", ('1',)).field( + 'webserver,sites_path,backup_path,status,mysql_root').find() + data['webserver'] = public.get_webserver() + data['distribution'] = public.get_linux_distribution() + data['request_iptype'] = self.get_request_iptype() + data['request_type'] = self.get_request_type() + data['improvement'] = public.get_improvement() + data['isSetup'] = True + data['ftpPort'] = int(self.get_ftp_port()) + data['basic_auth'] = self.get_basic_auth_stat(None) + data['recycle_bin'] = os.path.exists("data/recycle_bin.pl") + if os.path.exists(public.GetConfigValue('setup_path') + '/nginx') == False \ + and os.path.exists(public.GetConfigValue('setup_path') + '/apache') == False \ + and os.path.exists('/usr/local/lsws/bin/lswsctrl') == False \ + and os.path.exists(public.GetConfigValue('setup_path') + '/php') == False: + # 查询安装过或者是有正在安装的任务就不显示推荐安装 + num = public.M('tasks').where("name like ? or name like ? or name like ? or name like ? or name like ?", ('%nginx%', '%apache%', '%php%', '%mysql%', '%pureftpd%')).count() + if not num: + data['isSetup'] = False + p_token = self.get_token(None) + + data['app'] = {'open': p_token['open'], 'apps': p_token['apps']} + + if 'api' not in data: data['api'] = 'checked' if p_token['open'] else '' + + import panel_ssl_v2 as panelSSL + data['user_info'] = panelSSL.panelSSL().GetUserInfo(None) + data['user'] = {} + try: + data['user']['username'] = public.get_user_info()['username'] + except: + pass + data['table_header'] = self.get_table_header(None) + data["debug"] = "checked" if self.get_debug() else "" + data["left_title"] = public.readFile("data/title.pl") if os.path.exists("data/title.pl") else "" + + from password import password + data["username"] = password().get_panel_username(get) + data["webname"] = public.GetConfigValue("title") + + sess_out_path = 'data/session_timeout.pl' + if not os.path.exists(sess_out_path): + public.writeFile(sess_out_path, '86400') + s_time_tmp = public.readFile(sess_out_path) + try: + s_time = int(s_time_tmp) + except: + public.writeFile(sess_out_path, '86400') + s_time = 86400 + + # data['session_timeout_source'] = s_time + data['session_timeout'] = s_time + data['SSL'] = os.path.exists('data/ssl.pl') + + #增加防篡改安装信息 + data['tamper_core']={} + data['tamper_core']['setup']=os.path.exists('/www/server/panel/plugin/tamper_core') + + return public.return_message(0,0,data) + + @staticmethod + def show_time_by_seconds(seconds: int) -> str: + if seconds // (60 * 60 * 24) >= 4: # 4天及以上的,使用天+小时展示 + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + if hours == 0: + return "%d天" % days + if minutes == 0: + return "%d天%d小时" % (days, hours) + hours = hours + minutes / 60 + return "%d天%.1f小时" % (days, hours) + + elif seconds // (60 * 60) >= 1: # 3天以内1小时以上,用小时+分钟表示 + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + if minutes == 0: + return "%d小时" % hours + return "%d小时%d分钟" % (hours, minutes) + else: # 1小时以内,用分钟表示 + minutes, seconds = divmod(seconds, 60) + return "%d分钟" % minutes + + + # 获取debug状态 + def get_debug(self): + debug_path = 'data/debug.pl' + return ((os.path.exists(debug_path) and (public.readFile(debug_path) == "True"))) + + + def get_table_header(self, get): + """获取表头""" + table_name = "PHP_Site" + if hasattr(get, "table_name"): + table_name = get.table_name + header_file = "{}/data/table_header_conf.json".format(public.get_panel_path()) + if not os.path.exists(header_file): + header_data = {table_name: ''} + else: + try: + header_data = json.loads(public.readFile(header_file)) + if not isinstance(header_data, dict): + header_data = {} + header_data = {table_name: header_data.get(table_name, '')} + except: + header_data = {table_name: ''} + + if header_data.get("PHP_Site", None): + try: + s_table = json.loads(header_data["PHP_Site"]) + for d in s_table: + if d["title"] == "流量": + if public.GetWebServer() == "openlitespeed": + d["disabled"] = True + d["value"] = False + else: + d["disabled"] = False + header_data["PHP_Site"] = json.dumps(s_table) + except: + pass + + return header_data + + + #取面板错误日志 + def get_error_logs(self,get): + return public.GetNumLines('logs/error.log',2000) + + def is_pro(self,get): + import panelAuth,json + pdata = panelAuth.panelAuth().create_serverid(None) + url = public.GetConfigValue('home') + '/api/panel/is_pro' + pluginTmp = public.httpPost(url,pdata) + pluginInfo = json.loads(pluginTmp) + return pluginInfo + + def get_token(self,get): + import panelApi + return panelApi.panelApi().get_token(get) + + def set_token(self,get): + import panelApi + return panelApi.panelApi().set_token(get) + + def get_tmp_token(self,get): + import panelApi + return panelApi.panelApi().get_tmp_token(get) + + def GetNginxValue(self,get): + n = nginx.nginx() + return n.GetNginxValue() + + def SetNginxValue(self,get): + n = nginx.nginx() + return n.SetNginxValue(get) + + def GetApacheValue(self,get): + a = apache.apache() + return a.GetApacheValue() + + def SetApacheValue(self,get): + a = apache.apache() + return a.SetApacheValue(get) + + def get_ols_value(self,get): + a = ols.ols() + return a.get_value(get) + + def set_ols_value(self,get): + a = ols.ols() + return a.set_value(get) + + def get_ols_private_cache(self,get): + a = ols.ols() + return a.get_private_cache(get) + + def get_ols_static_cache(self,get): + a = ols.ols() + return a.get_static_cache(get) + + def set_ols_static_cache(self,get): + a = ols.ols() + return a.set_static_cache(get) + + def switch_ols_private_cache(self,get): + a = ols.ols() + return a.switch_private_cache(get) + + def set_ols_private_cache(self,get): + a = ols.ols() + return a.set_private_cache(get) + + def get_ols_private_cache_status(self,get): + a = ols.ols() + return a.get_private_cache_status(get) + + def get_ipv6_listen(self,get): + return os.path.exists('data/ipv6.pl') + + def set_ipv6_status(self,get): + ipv6_file = 'data/ipv6.pl' + if self.get_ipv6_listen(get): + os.remove(ipv6_file) + public.write_log_gettext('Panel setting', 'Disable IPv6 compatibility of the panel!') + else: + public.writeFile(ipv6_file, 'True') + public.write_log_gettext('Panel setting', 'Enable IPv6 compatibility of the panel!') + public.restart_panel() + return public.return_msg_gettext(True, 'Setup successfully!') + + #自动补充CLI模式下的PHP版本 + def auto_cli_php_version(self,get): + import panel_site_v2 as panelSite + php_versions = panelSite.panelSite().GetPHPVersion(get,False) + php_bin_src = "/www/server/php/%s/bin/php" % php_versions[-1]['version'] + if not os.path.exists(php_bin_src): return public.return_message(-1,0,'PHP is not installed') + get.php_version = php_versions[-1]['version'] + self.set_cli_php_version(get) + return public.return_message(0,0,php_versions[-1]) + + #获取CLI模式下的PHP版本 + def get_cli_php_version(self,get): + php_bin = '/usr/bin/php' + if not os.path.exists(php_bin) or not os.path.islink(php_bin): return self.auto_cli_php_version(get) + link_re = os.readlink(php_bin) + if not os.path.exists(link_re): return self.auto_cli_php_version(get) + import panel_site_v2 as panelSite + php_versions = panelSite.panelSite().GetPHPVersion(get,False) + if len(php_versions)==0: + return public.return_message(-1,0,'Failed to get php version!') + del(php_versions[0]) + for v in php_versions: + if link_re.find(v['version']) != -1: return public.return_message(0,0,{"select":v,"versions":php_versions}) + return_message=self.auto_cli_php_version(get) + return public.return_message(0,0,{"select":return_message['message'],"versions":php_versions}) + + #设置CLI模式下的PHP版本 + def set_cli_php_version(self,get): + # 校验参数 + try: + get.validate([ + Param('php_version').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + php_bin = '/usr/bin/php' + php_bin_src = "/www/server/php/%s/bin/php" % get.php_version + php_ize = '/usr/bin/phpize' + php_ize_src = "/www/server/php/%s/bin/phpize" % get.php_version + php_fpm = '/usr/bin/php-fpm' + php_fpm_src = "/www/server/php/%s/sbin/php-fpm" % get.php_version + php_pecl = '/usr/bin/pecl' + php_pecl_src = "/www/server/php/%s/bin/pecl" % get.php_version + php_pear = '/usr/bin/pear' + php_pear_src = "/www/server/php/%s/bin/pear" % get.php_version + php_cli_ini = '/etc/php-cli.ini' + php_cli_ini_src = "/www/server/php/%s/etc/php-cli.ini" % get.php_version + if not os.path.exists(php_bin_src): return public.return_message(-1, 0,'Specified PHP version not installed') + is_chattr = public.ExecShell('lsattr /usr|grep /usr/bin')[0].find('-i-') + if is_chattr != -1: public.ExecShell('chattr -i /usr/bin') + public.ExecShell("rm -f " + php_bin + ' '+ php_ize + ' ' + php_fpm + ' ' + php_pecl + ' ' + php_pear + ' ' + php_cli_ini) + public.ExecShell("ln -sf %s %s" % (php_bin_src,php_bin)) + public.ExecShell("ln -sf %s %s" % (php_ize_src,php_ize)) + public.ExecShell("ln -sf %s %s" % (php_fpm_src,php_fpm)) + public.ExecShell("ln -sf %s %s" % (php_pecl_src,php_pecl)) + public.ExecShell("ln -sf %s %s" % (php_pear_src,php_pear)) + public.ExecShell("ln -sf %s %s" % (php_cli_ini_src,php_cli_ini)) + import jobs_v2 as jobs + jobs.set_php_cli_env() + if is_chattr != -1: public.ExecShell('chattr +i /usr/bin') + public.write_log_gettext('Panel settings','Set the PHP-CLI version to: {}',(get.php_version,)) + return public.return_message(0, 0,'Setup successfully!') + + + #获取BasicAuth状态 + def get_basic_auth_stat(self,get): + path = 'config/basic_auth.json' + is_install = True + result = {"basic_user":"","basic_pwd":"","open":False,"is_install":is_install} + if not os.path.exists(path): return result + try: + ba_conf = json.loads(public.readFile(path)) + except: + os.remove(path) + return result + ba_conf['is_install'] = is_install + return ba_conf + + #设置BasicAuth + def set_basic_auth(self,get): + is_open = False + if get.open == 'True': is_open = True + tips = '_bt.cn' + path = 'config/basic_auth.json' + ba_conf = None + if is_open: + if not get.basic_user.strip() or not get.basic_pwd.strip(): return public.returnMsg(False,'BasicAuth authentication username and password cannot be empty!') + if os.path.exists(path): + try: + ba_conf = json.loads(public.readFile(path)) + except: + os.remove(path) + + if not ba_conf: + ba_conf = {"basic_user":public.md5(get.basic_user.strip() + tips),"basic_pwd":public.md5(get.basic_pwd.strip() + tips),"open":is_open} + else: + if get.basic_user: ba_conf['basic_user'] = public.md5(get.basic_user.strip() + tips) + if get.basic_pwd: ba_conf['basic_pwd'] = public.md5(get.basic_pwd.strip() + tips) + ba_conf['open'] = is_open + + public.writeFile(path,json.dumps(ba_conf)) + os.chmod(path,384) + public.write_log_gettext('Panel settings','Set the BasicAuth status to: {}', (is_open,)) + public.add_security_logs('Panel settings',' Set the BasicAuth status to: %s' % is_open) + public.writeFile('data/reload.pl','True') + return public.return_msg_gettext(True,'Setup successfully!') + + # xss 防御 + def xsssec(self,text): + return text.replace('<', '<').replace('>', '>') + + #取面板运行日志 + def get_panel_error_logs(self,get): + filename = 'logs/error.log' + if not os.path.exists(filename): return public.return_msg_gettext(False,"Logs emptied") + result = public.GetNumLines(filename,2000) + return public.returnMsg(True,self.xsssec(result)) + + #清空面板运行日志 + def clean_panel_error_logs(self,get): + filename = 'logs/error.log' + public.writeFile(filename,'') + public.write_log_gettext('Panel settings','Clearing log info') + public.add_security_logs('Panel settings', 'Clearing log info') + return public.return_msg_gettext(True,'Cleared!') + + # 获取lets证书 + def get_cert_source(self,get): + import setPanelLets + sp = setPanelLets.setPanelLets() + spg = sp.get_cert_source() + return spg + + #设置debug模式 + def set_debug(self,get): + debug_path = 'data/debug.pl' + if os.path.exists(debug_path): + t_str = 'Close' + os.remove(debug_path) + else: + t_str = 'Open' + public.writeFile(debug_path,'True') + public.write_log_gettext('Panel configuration','{} Developer mode(DeBug)',(t_str,)) + public.restart_panel() + return public.return_msg_gettext(True,'Setup successfully!') + + + #设置离线模式 + def set_local(self,get): + d_path = 'data/not_network.pl' + if os.path.exists(d_path): + t_str = 'Close' + os.remove(d_path) + else: + t_str = 'Open' + public.writeFile(d_path,'True') + public.write_log_gettext('Panel configuration','{} Offline mode',(t_str,)) + return public.return_msg_gettext(True,'Setup successfully!') + + # 修改.user.ini文件 + def _edit_user_ini(self,file,s_conf,act,session_path): + public.ExecShell("chattr -i {}".format(file)) + conf = public.readFile(file) + if act == "1": + if "session.save_path" in conf: + return False + conf = conf + ":{}/".format(session_path) + conf = conf + "\n" + s_conf + else: + rep = "\n*session.save_path(.|\n)*files" + rep1 = ":{}".format(session_path) + conf = re.sub(rep,"",conf) + conf = re.sub(rep1,"",conf) + public.writeFile(file, conf) + public.ExecShell("chattr +i {}".format(file)) + + # 设置php_session存放到独立文件夹 + def set_php_session_path(self,get): + ''' + get.id site id + get.act 0/1 + :param get: + :return: + ''' + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + Param('act').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + if public.get_webserver() == 'openlitespeed': + return public.return_message(-1,0, 'The current web server is openlitespeed. This function is not supported yet.') + import panel_site_v2 + site_info = public.M('sites').where('id=?', (get.id,)).field('name,path').find() + session_path = "/www/php_session/{}".format(site_info["name"]) + if not os.path.exists(session_path): + os.makedirs(session_path) + public.ExecShell('chown www.www {}'.format(session_path)) + run_path = panel_site_v2.panelSite().GetSiteRunPath(get)['message']["runPath"] + user_ini_file = "{site_path}{run_path}/.user.ini".format(site_path=site_info["path"], run_path=run_path) + conf = "session.save_path={}/\nsession.save_handler = files".format(session_path) + if get.act == "1": + if not os.path.exists(user_ini_file): + public.writeFile(user_ini_file,conf) + public.ExecShell("chattr +i {}".format(user_ini_file)) + public.print_log("5") + return public.return_message(0,0,'Setup successfully!') + self._edit_user_ini(user_ini_file,conf,get.act,session_path) + return public.return_message(0,0, 'Setup successfully!') + else: + self._edit_user_ini(user_ini_file,conf,get.act,session_path) + return public.return_message(0,0, 'Setup successfully!') + + # 获取php_session是否存放到独立文件夹 + def get_php_session_path(self,get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import panelSite + site_info = public.M('sites').where('id=?', (get.id,)).field('name,path').find() + if site_info: + run_path = panelSite.panelSite().GetSiteRunPath(get)["runPath"] + user_ini_file = "{site_path}{run_path}/.user.ini".format(site_path=site_info["path"], run_path=run_path) + conf = public.readFile(user_ini_file) + if conf and "session.save_path" in conf: + return public.return_message(0,0,True) + return public.return_message(0,0,False) + + def _create_key(self): + get_token = pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator + public.writeFile(self._key_file,get_token) + username = self.get_random() + public.writeFile(self._username_file, username) + + def get_key(self,get): + key = public.readFile(self._key_file) + username = public.readFile(self._username_file) + if not key: + return public.return_msg_gettext(False, 'The key does not exist. Please turn on and try again.') + if not username: + return public.return_msg_gettext(False, 'The username does not exist. Please turn on and try again.') + return {"key":key,"username":username} + + def get_random(self): + import random + seed = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + sa = [] + for _ in range(8): + sa.append(random.choice(seed)) + salt = ''.join(sa) + return salt + + def set_two_step_auth(self,get): + if not hasattr(get,"act") or not get.act: + return public.return_msg_gettext(False, 'Please enter the operation mode') + if get.act == "1": + if not os.path.exists(self._core_fle_path): + os.makedirs(self._core_fle_path) + username = public.readFile(self._username_file) + if not os.path.exists(self._bk_key_file): + secret_key = public.readFile(self._key_file) + if not secret_key or not username: + self._create_key() + else: + os.rename(self._bk_key_file,self._key_file) + secret_key = public.readFile(self._key_file) + username = public.readFile(self._username_file) + local_ip = public.GetLocalIp() + if not secret_key: + return public.return_msg_gettext(False,"Failed to generate key or username. Please check if the hard disk space is insufficient or the directory cannot be written.[ {} ]",(self._setup_path+"/data/",)) + try: + try: + panel_name = json.loads(public.readFile(self._setup_path+'/config/config.json'))['title'] + except: + panel_name = 'aaPanel' + data = pyotp.totp.TOTP(secret_key).provisioning_uri(username, issuer_name='{}--{}'.format(panel_name,local_ip)) + public.writeFile(self._core_fle_path+'/qrcode.txt',str(data)) + return public.return_msg_gettext(True, 'Setup successfully!') + except Exception as e: + return public.return_msg_gettext(False, e) + else: + if os.path.exists(self._key_file): + os.rename(self._key_file,self._bk_key_file) + return public.return_msg_gettext(True, 'Setup successfully!') + + # 检测是否开启双因素验证 + def check_two_step(self,get): + secret_key = public.readFile(self._key_file) + if not secret_key: + return public.return_msg_gettext(False, 'Did not open Google authentication') + return public.return_msg_gettext(True, 'Google authentication has been turned on') + + # 读取二维码data + def get_qrcode_data(self,get): + data = public.readFile(self._core_fle_path + '/qrcode.txt') + if data: + return data + return public.return_msg_gettext(True, 'No QR code data, please re-open') + + # 设置是否云控打开 + def set_coll_open(self,get): + if not 'coll_show' in get: return public.return_msg_gettext(False,'Parameter ERROR!') + if get.coll_show == 'True': + session['tmp_login'] = True + else: + session['tmp_login'] = False + return public.return_msg_gettext(True,'Setup successfully!') + + + # 是否显示软件推荐 + def show_recommend(self,get): + pfile = 'data/not_recommend.pl' + if os.path.exists(pfile): + os.remove(pfile) + else: + public.writeFile(pfile,'True') + return public.return_msg_gettext(True,'Setup successfully!') + + # 获取菜单列表 + def get_menu_list(self, get): + ''' + @name 获取菜单列表 + @author hwliang<2020-08-31> + @param get + @return list + ''' + menu_file = 'config/menu.json' + hide_menu_file = 'config/hide_menu.json' + data = json.loads(public.ReadFile(menu_file)) + if not os.path.exists(hide_menu_file): + public.writeFile(hide_menu_file, '[]') + hide_menu = public.ReadFile(hide_menu_file) + if not hide_menu: + hide_menu = [] + else: + hide_menu = json.loads(hide_menu) + result = [] + for d in data: + tmp = {} + tmp['id'] = d['id'] + tmp['title'] = d['title'] + tmp['show'] = not d['id'] in hide_menu + tmp['sort'] = d['sort'] + result.append(tmp) + + menus = sorted(result, key=lambda x: x['sort']) + return menus + + # 设置隐藏菜单列表 + def set_hide_menu_list(self, get): + ''' + @name 设置隐藏菜单列表 + @author hwliang<2020-08-31> + @param get { + hide_list: json 所有不显示的菜单ID + } + @return dict + ''' + hide_menu_file = 'config/hide_menu.json' + not_hide_id = ["dologin", "memuAconfig", "memuAsoft", "memuA"] # 禁止隐藏的菜单 + + hide_list = json.loads(get.hide_list) + hide_menu = [] + for h in hide_list: + if h in not_hide_id: continue + hide_menu.append(h) + public.writeFile(hide_menu_file, json.dumps(hide_menu)) + public.write_log_gettext('Panel setting', 'Successfully modify the panel menu display list') + return public.return_msg_gettext(True, 'Setup successfully!') + + # 获取临时登录列表 + def get_temp_login(self, args): + ''' + @name 获取临时登录列表 + @author hwliang<2020-09-2> + @return dict + ''' + if 'tmp_login_expire' in session: return public.return_msg_gettext(False, 'Permission denied!') + public.M('temp_login').where('state=? and expire + @return dict + ''' + s_time = int(time.time()) + expire_time = get.expire_time if "expire_time" in get else s_time + 3600 + if 'tmp_login_expire' in session: return public.return_msg_gettext(False, 'Permission denied!') + public.M('temp_login').where('state=? and expire>?', (0, s_time)).delete() + token = public.GetRandomString(48) + salt = public.GetRandomString(12) + + pdata = { + 'token': public.md5(token + salt), + 'salt': salt, + 'state': 0, + 'login_time': 0, + 'login_addr': '', + 'expire': int(expire_time), + 'addtime': s_time + } + + if not public.M('temp_login').count(): + pdata['id'] = 101 + + if public.M('temp_login').insert(pdata): + public.write_log_gettext('Panel setting', 'Generate temporary connection, expiration time: {}',(public.format_date(times=pdata['expire']),)) + return {'status': True, 'msg': public.get_msg_gettext('Temporary login URL has been generated'), 'token': token, 'expire': pdata['expire']} + return public.return_msg_gettext(False, 'Failed to generate temporary login URL') + + # 删除临时登录 + def remove_temp_login(self, args): + ''' + @name 删除临时登录 + @author hwliang<2020-09-2> + @param args{ + id: int<临时登录ID> + } + @return dict + ''' + if 'tmp_login_expire' in session: return public.return_msg_gettext(False, 'Permission denied!') + id = int(args.id) + if public.M('temp_login').where('id=?', (id,)).delete(): + public.write_log_gettext('Panel setting', 'Delete temporary login URL') + return public.return_msg_gettext(True, 'Successfully deleted') + return public.return_msg_gettext(False, 'Failed to delete') + + # 强制弹出指定临时登录 + def clear_temp_login(self, args): + ''' + @name 强制登出 + @author hwliang<2020-09-2> + @param args{ + id: int<临时登录ID> + } + @return dict + ''' + if 'tmp_login_expire' in session: return public.return_msg_gettext(False, 'Permission denied!') + id = int(args.id) + s_file = 'data/session/{}'.format(id) + if os.path.exists(s_file): + os.remove(s_file) + public.write_log_gettext('Panel setting', 'Force logout of temporary users:{1}',(str(id),)) + return public.return_msg_gettext(True, 'Temporary user has been forcibly logged out:{}',(str(id),)) + public.return_msg_gettext(False, 'The specified user is not currently logged in!') + + # 查看临时授权操作日志 + def get_temp_login_logs(self, args): + ''' + @name 查看临时授权操作日志 + @author hwliang<2020-09-2> + @param args{ + id: int<临时登录ID> + } + @return dict + ''' + if 'tmp_login_expire' in session: return public.return_msg_gettext(False, 'Permission denied!') + id = int(args.id) + data = public.M('logs').where('uid=?', (id,)).order('id desc').select() + return data + + def add_nginx_access_log_format(self,args): + n = nginx.nginx() + return n.add_nginx_access_log_format(args) + + def del_nginx_access_log_format(self,args): + n = nginx.nginx() + return n.del_nginx_access_log_format(args) + + def get_nginx_access_log_format(self,args): + n = nginx.nginx() + return n.get_nginx_access_log_format(args) + + def set_format_log_to_website(self,args): + n = nginx.nginx() + return n.set_format_log_to_website(args) + + def get_nginx_access_log_format_parameter(self,args): + n = nginx.nginx() + return n.get_nginx_access_log_format_parameter(args) + + def add_httpd_access_log_format(self,args): + a = apache.apache() + return a.add_httpd_access_log_format(args) + + def del_httpd_access_log_format(self,args): + a = apache.apache() + return a.del_httpd_access_log_format(args) + + def get_httpd_access_log_format(self,args): + a = apache.apache() + return a.get_httpd_access_log_format(args) + + def set_httpd_format_log_to_website(self,args): + a = apache.apache() + return a.set_httpd_format_log_to_website(args) + + def get_httpd_access_log_format_parameter(self,args): + a = apache.apache() + return a.get_httpd_access_log_format_parameter(args) + + def get_file_deny(self,args): + import file_execute_deny_v2 + p = file_execute_deny_v2.FileExecuteDeny() + return p.get_file_deny(args) + + def set_file_deny(self,args): + import file_execute_deny_v2 + p = file_execute_deny_v2.FileExecuteDeny() + return p.set_file_deny(args) + + def del_file_deny(self,args): + import file_execute_deny_v2 + p = file_execute_deny_v2.FileExecuteDeny() + return p.del_file_deny(args) + + #查看告警 + def get_login_send(self, get): + send_type = "" + login_send_type_conf = "/www/server/panel/data/panel_login_send.pl" + if os.path.exists(login_send_type_conf): + send_type = public.ReadFile(login_send_type_conf).strip() + else: + + if os.path.exists("/www/server/panel/data/login_send_type.pl"): + send_type = public.readFile("/www/server/panel/data/login_send_type.pl") + else: + if os.path.exists('/www/server/panel/data/login_send_mail.pl'): + send_type = "mail" + if os.path.exists('/www/server/panel/data/login_send_dingding.pl'): + send_type = "dingding" + return public.returnMsg(True, send_type) + + + #取消告警 + def clear_login_send(self,get): + type = get.type.strip() + if type == 'mail': + if os.path.exists("/www/server/panel/data/login_send_mail.pl"): + os.remove("/www/server/panel/data/login_send_mail.pl") + elif type == 'dingding': + if os.path.exists("/www/server/panel/data/login_send_dingding.pl"): + os.remove("/www/server/panel/data/login_send_dingding.pl") + + login_send_type_conf = "/www/server/panel/data/login_send_type.pl" + if os.path.exists(login_send_type_conf): + os.remove(login_send_type_conf) + login_send_type_conf = "/www/server/panel/data/panel_login_send.pl" + if os.path.exists(login_send_type_conf): + os.remove(login_send_type_conf) + return public.returnMsg(True, 'Canceling the login alarm succeeded.!') + + + def get_login_area(self,get): + """ + @获取面板登录告警 + @return + login_status 是否开启面板登录告警 + login_area 是否开启面板异地登录告警 + """ + result = {} + result['login_status'] = self.get_login_send(get)['msg'] + + result['login_area'] = '' + sfile = '{}/data/panel_login_area.pl'.format(public.get_panel_path()) + if os.path.exists(sfile): + result['login_area_status'] = public.readFile(sfile) + return result + + + def set_login_area(self,get): + """ + @name 设置异地登录告警 + @param get + """ + sfile = '{}/data/panel_login_area.pl'.format(public.get_panel_path()) + set_type=get.type.strip() + obj = public.init_msg(set_type) + if not obj: + return public.returnMsg(False, "The alarm module is not installed.") + + public.writeFile(sfile, set_type) + return public.returnMsg(True, 'successfully set') + + def get_login_area_list(self,get): + """ + @name 获取面板常用地区登录 + + """ + data = {} + sfile = '{}/data/panel_login_area.json'.format(public.get_panel_path()) + try: + data = json.loads(public.readFile(sfile)) + except:pass + + result = [] + for key in data.keys(): + result.append({'area':key,'count':data[key]}) + + result = sorted(result, key=lambda x: x['count'], reverse=True) + return result + + def clear_login_list(self,get): + """ + @name 清理常用登录地区 + """ + sfile = '{}/data/panel_login_area.json'.format(public.get_panel_path()) + if os.path.exists(sfile): + os.remove(sfile) + return public.returnMsg(True,'Successful operation.') + + + + + + # def get_login_send(self,get): + # result={} + # import time + # time.sleep(0.01) + # if os.path.exists('/www/server/panel/data/login_send_mail.pl'): + # result['mail']=True + # else: + # result['mail']=False + # if os.path.exists('/www/server/panel/data/login_send_dingding.pl'): + # result['dingding']=True + # else: + # result['dingding']=False + # if result['mail'] or result['dingding']: + # return public.returnMsg(True, result) + # return public.returnMsg(False, result) + + #设置告警 + def set_login_send(self,get): + login_send_type_conf = "/www/server/panel/data/panel_login_send.pl" + + set_type=get.type.strip() + msg_configs = self.get_msg_configs(get) + if set_type not in msg_configs.keys(): + return public.returnMsg(False,'This send type is not supported') + _conf = msg_configs[set_type] + if "data" not in _conf or not _conf["data"]: + return public.returnMsg(False, "This channel is not configured, please select again.") + + from panelMessage import panelMessage + pm = panelMessage() + obj = pm.init_msg_module(set_type) + if not obj: + return public.returnMsg(False, "The message channel is not installed.") + + public.writeFile(login_send_type_conf, set_type) + return public.returnMsg(True, 'successfully set') + + # if type=='mail': + # if not os.path.exists("/www/server/panel/data/login_send_mail.pl"): + # os.mknod("/www/server/panel/data/login_send_mail.pl") + # if os.path.exists("/www/server/panel/data/login_send_dingding.pl"): + # os.remove("/www/server/panel/data/login_send_dingding.pl") + # return public.returnMsg(True, '设置成功') + # elif type=='dingding': + # if not os.path.exists("/www/server/panel/data/login_send_dingding.pl"): + # os.mknod("/www/server/panel/data/login_send_dingding.pl") + # if os.path.exists("/www/server/panel/data/login_send_mail.pl"): + # os.remove("/www/server/panel/data/login_send_mail.pl") + # return public.returnMsg(True, '设置成功') + # else: + # return public.returnMsg(False,'不支持该发送类型') + + + + #告警日志 + def get_login_log(self,get): + public.create_logs() + import page + page = page.Page() + count = public.M('logs2').where('type=?', (u'aapanel login reminder',)).field('log,addtime').count() + limit = 7 + info = {} + info['count'] = count + info['row'] = limit + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + info['uri'] = get + info['return_js'] = '' + if hasattr(get, 'tojs'): + info['return_js'] = get.tojs + data = {} + # 获取分页数据 + data['page'] = page.GetPage(info, '1,2,3,4,5,8') + data['data'] = public.M('logs2').where('type=?', (u'aapanel login reminder',)).field('log,addtime').order('id desc').limit( + str(page.SHIFT) + ',' + str(page.ROW)).field('log,addtime').select() + return data + + #白名单设置 + def login_ipwhite(self,get): + type=get.type + if type=='get': + return self.get_login_ipwhite(get) + if type=='add': + return self.add_login_ipwhite(get) + if type=='del': + return self.del_login_ipwhite(get) + if type=='clear': + return self.clear_login_ipwhite(get) + + #查看IP白名单 + def get_login_ipwhite(self,get): + try: + path='/www/server/panel/data/send_login_white.json' + ip_white=json.loads(public.ReadFile('/www/server/panel/data/send_login_white.json')) + if not ip_white:return public.return_msg_gettext(True, []) + return public.return_msg_gettext(True, ip_white) + except: + public.WriteFile(path, '[]') + return public.return_msg_gettext(True, []) + + def add_login_ipwhite(self,get): + ip=get.ip.strip() + try: + path = '/www/server/panel/data/send_login_white.json' + ip_white = json.loads(public.ReadFile('/www/server/panel/data/send_login_white.json')) + if not ip in ip_white: + ip_white.append(ip) + public.WriteFile(path, json.dumps(ip_white)) + return public.return_msg_gettext(True, "Setup successfully!") + except: + public.WriteFile(path, json.dumps([ip])) + return public.return_msg_gettext(True, "Setup successfully!") + + def del_login_ipwhite(self,get): + ip = get.ip.strip() + try: + path = '/www/server/panel/data/send_login_white.json' + ip_white = json.loads(public.ReadFile('/www/server/panel/data/send_login_white.json')) + if ip in ip_white: + ip_white.remove(ip) + public.WriteFile(path, json.dumps(ip_white)) + return public.return_msg_gettext(True, "Successfully deleted!") + except: + public.WriteFile(path, json.dumps([])) + return public.return_msg_gettext(True, "Successfully deleted!") + + def clear_login_ipwhite(self,get): + path = '/www/server/panel/data/send_login_white.json' + public.WriteFile(path, json.dumps([])) + return public.return_msg_gettext(True, "Successfully created") + + def get_panel_ssl_status(self,get): + import os + if os.path.exists(self._setup_path+'/data/ssl.pl'): + return public.return_message(0,0,'success') + return public.return_message(-1,0,'false') + + def set_ssl_verify(self,get): + """ + 设置双向认证 + """ + sslConf = 'data/ssl_verify_data.pl' + status = int(get.status) + if status: + if not os.path.exists('data/ssl.pl'): return public.returnMsg(False,'The panel SSL function needs to be enabled first!') + public.writeFile(sslConf,'True') + else: + if os.path.exists(sslConf): os.remove(sslConf) + if 'crl' in get and 'ca' in get: + crl = 'ssl/crl.pem' + ca = 'ssl/ca.pem' + if get.crl: + public.writeFile(crl,get.crl.strip()) + if get.ca: + public.writeFile(ca,get.ca.strip()) + return public.returnMsg(True,'The panel two-way authentication certificate has been saved!') + else: + msg = 'Enable' + if not status:msg = 'Disable' + return public.returnMsg(True,'Panel two-way authentication {} succeeded!'.format(msg)) + + def get_ssl_verify(self, get): + """ + 获取双向认证 + """ + result = {'status': False, 'ca': '', 'crl': ''} + sslConf = 'data/ssl_verify_data.pl' + if os.path.exists(sslConf): result['status'] = True + + ca = 'ssl/ca.pem' + crl = 'ssl/crl.pem' + if os.path.exists(crl): + result['crl'] = public.readFile(crl) + if os.path.exists(crl): + result['ca'] = public.readFile(ca) + return result + + + def set_not_auth_status(self,get): + ''' + @name 设置未认证时的响应状态 + @author hwliang<2021-12-16> + @param status_code 状态码 + @return dict + ''' + if not 'status_code' in get: + return public.return_msg_gettext(False,'Parameter ERROR!') + + if re.match(r"^\d+$", get.status_code): + status_code = int(get.status_code) + if status_code != 0: + if status_code < 100 or status_code > 999: + return public.return_msg_gettext(False,'Parameter ERROR!') + else: + return public.return_msg_gettext(False,'Parameter ERROR!') + + public.save_config('abort',get.status_code) + public.write_log_gettext('Panel configuration','Set the unauthorized response status to:{}'.format(get.status_code)) + return public.return_msg_gettext(True,'Setup successfully!') + + def get_not_auth_status(self): + ''' + @name 获取未认证时的响应状态 + @author hwliang<2021-12-16> + @return int + ''' + try: + status_code = int(public.read_config('abort')) + return status_code + except: + return 404 + + def get_request_iptype(self,get = None): + ''' + @name 获取云端请求线路 + @author hwliang<2022-02-09> + @return auto/ipv4/ipv6 + ''' + + v4_file = '{}/data/v4.pl'.format(public.get_panel_path()) + if not os.path.exists(v4_file): return 'auto' + iptype = public.readFile(v4_file).strip() + if not iptype: return 'auto' + if iptype == '-4': return 'ipv4' + return 'ipv6' + + def get_request_type(self,get= None): + ''' + @name 获取云端请求方式 + @author hwliang<2022-02-09> + @return python/curl/php + ''' + http_type_file = '{}/data/http_type.pl'.format(public.get_panel_path()) + if not os.path.exists(http_type_file): return 'python' + http_type = public.readFile(http_type_file).strip() + if not http_type: + os.remove(http_type_file) + return 'python' + return http_type + + def get_msg_configs(self,get): + """ + 获取消息通道配置列表 + """ + cpath = 'data/msg.json' + + #cpath = '{}/data/msg.json'.format(public.get_panel_path()) + example = 'config/examples/msg.example.json' + + if not os.path.exists(cpath) and os.path.exists(example): + import shutil + shutil.copy(example, cpath) + try: + # 配置文件异常处理 + json.loads(public.readFile(cpath)) + except: + if os.path.exists(cpath): os.remove(cpath) + data = {} + if os.path.exists(cpath): + msgs = json.loads(public.readFile(cpath)) + + for x in msgs: + x['data'] = {} + x['setup'] = False + x['info'] = False + key = x['name'] + try: + obj = public.init_msg(x['name']) + if obj: + x['setup'] = True + x['data'] = obj.get_config(None) + x['info'] = obj.get_version_info(None) + except : + pass + data[key] = x + return data + + def get_module_template(self,get): + """ + 获取模块模板 + """ + panelPath = public.get_panel_path() + module_name = get.module_name + sfile = '{}/class/msg/{}.html'.format(panelPath, module_name) + if not os.path.exists(sfile): + return public.returnMsg(False, 'The template file does not exist.') + + if module_name in ["sms"]: + + obj = public.init_msg(module_name) + if obj: + args = public.dict_obj() + args.reload = True + data = obj.get_config(args) + from flask import render_template_string + shtml = public.readFile(sfile) + return public.returnMsg(True, render_template_string(shtml, data=data)) + else: + shtml = public.readFile(sfile) + return public.returnMsg(True, shtml) + + + + def set_default_channel(self,get): + """ + 设置默认消息通道 + """ + default_channel_pl = "/www/server/panel/data/default_msg_channel.pl" + + new_channel = get.channel + default = False + if "default" in get: + _default = get.default + if not _default or _default in ["false"]: + default = False + else: + default = True + + ori_default_channel = "" + if os.path.exists(default_channel_pl): + ori_default_channel = public.readFile(ori_default_channel) + + if default: + # 设置为默认 + from panelMessage import panelMessage + pm = panelMessage() + obj = pm.init_msg_module(new_channel) + if not obj: return public.returnMsg(False, 'Setup failed, [{}] is not installed'.format(new_channel)) + + public.writeFile(default_channel_pl, new_channel) + if ori_default_channel: + return public.returnMsg(True, 'Successfully changed [{}] to [{}] panel default notification.'.format(ori_default_channel, new_channel)) + else: + return public.returnMsg(True, '[{}] has been set as the default notification.'.format(new_channel)) + else: + # 取消默认设置 + if os.path.exists(default_channel_pl): + os.remove(default_channel_pl) + return public.returnMsg(True, "[{}] has been removed as panel default notification.".format(new_channel)) + + def set_msg_config(self,get): + """ + 设置消息通道配置 + """ + from panelMessage import panelMessage + pm = panelMessage() + obj = pm.init_msg_module(get.name) + if not obj: return public.returnMsg(False, 'Setup failed, [{}] is not installed'.format(get.name)) + return obj.set_config(get) + + # def install_msg_module(self,get): + # """ + # 安装/更新消息通道模块 + # @name 需要安装的模块名称 + # """ + # module_name = "" + # try: + # module_name = get.name + # down_url = public.get_url() + # + # local_path = '{}/class/msg'.format(public.get_panel_path()) + # if not os.path.exists(local_path): os.makedirs(local_path) + # + # import panelTask + # task_obj = panelTask.bt_task() + # + # sfile1 = '{}/{}_msg.py'.format(local_path,module_name) + # down_url1 = '{}/linux/panel/msg/{}_msg.py'.format(down_url,module_name) + # + # sfile2 = '{}/class/msg/{}.html'.format(public.get_panel_path(),module_name) + # down_url2 = '{}/linux/panel/msg/{}.html'.format(down_url,module_name) + # + # public.WriteLog('Install module', 'Install [{}]'.format(module_name)) + # task_obj.create_task('Download file', 1, down_url1, sfile1) + # task_obj.create_task('Download file', 1, down_url2, sfile2) + # + # timeout = 0 + # is_install = False + # while timeout < 5: + # try: + # if os.path.exists(sfile1) and os.path.exists(sfile2): + # msg_obj = public.init_msg(module_name) + # if msg_obj and msg_obj.get_version_info: + # is_install = True + # break + # except: pass + # time.sleep(0.1) + # is_install = True + # + # if not is_install: + # return public.returnMsg(False, 'Failed to install [{}] module. Please check the network.'.format(module_name)) + # + # public.set_module_logs('msg_push', 'install_module', 1) + # return public.returnMsg(True, '[{}] Module is installed successfully.'.format(module_name)) + # except: + # pass + # return public.returnMsg(False, '[{}] Module installation failed.'.format(module_name)) + + def install_msg_module(self,get): + """ + aapanel 不与面板相同,不删除通道模块 + 安装/更新消息通道模块 + @name 需要安装的模块名称 + """ + module_name = "" + try: + module_name = get.name + + local_path = '{}/class/msg'.format(public.get_panel_path()) + if not os.path.exists(local_path): os.makedirs(local_path) + + sfile1 = '{}/{}_msg.py'.format(local_path,module_name) + + if os.path.exists(sfile1): + return public.returnMsg(True, '[{}] Module is installed successfully.'.format(module_name)) + + except: + return public.returnMsg(False, '[{}] Module installation failed.'.format(module_name)) + + + # def uninstall_msg_module(self,get): + # """ + # 卸载消息通道模块 + # @name 需要卸载的模块名称 + # @is_del 是否需要删除配置文件 + # """ + # module_name = get.name + # obj = public.init_msg(module_name) + # if 'is_del' in get: + # try: + # obj.uninstall() + # except:pass + # + # sfile = '{}/class/msg/{}_msg.py'.format(public.get_panel_path(),module_name) + # if os.path.exists(sfile): os.remove(sfile) + # + # # public.print_log(sfile) + # default_channel_pl = "{}/data/default_msg_channel.pl".format(public.get_panel_path()) + # default_channel = public.readFile(default_channel_pl) + # if default_channel and default_channel == module_name: + # os.remove(default_channel_pl) + # return public.returnMsg(True, '[{}] Module uninstallation succeeds'.format(module_name)) + + def uninstall_msg_module(self,get): + """ + aapanel 不与面板相同,不删除通道模块,只删除配置文件 + @module_name 是删除配置文件 + """ + module_name = get.name + + # sfile = '{}/class/msg/{}_msg.py'.format(public.get_panel_path(),module_name) + # if os.path.exists(sfile): os.remove(sfile) + + if module_name in ["dingding", "feishu", "weixin"]: + msg_conf_file = "{{}}/data/{}.json".format(module_name).format(public.get_panel_path()) + if os.path.exists(msg_conf_file): os.remove(msg_conf_file) + elif module_name == "mail": + for conf_file in ["stmp_mail", "mail_list"]: + msg_conf_file = "{}/data/{}.json".format(public.get_panel_path(), conf_file) + if os.path.exists(msg_conf_file): os.remove(msg_conf_file) + elif module_name == "tg": + msg_conf_file = "{}/data/tg_bot.json".format(public.get_panel_path()) + if os.path.exists(msg_conf_file): os.remove(msg_conf_file) + + + # public.print_log(sfile) + default_channel_pl = "{}/data/default_msg_channel.pl".format(public.get_panel_path()) + default_channel = public.readFile(default_channel_pl) + if default_channel and default_channel == module_name: + os.remove(default_channel_pl) + return public.returnMsg(True, '[{}] Module uninstallation succeeds'.format(module_name)) + + + def get_msg_fun(self,get): + """ + @获取消息模块指定方法 + @auther: cjxin + @date: 2022-08-16 + @param: get.module_name 消息模块名称(如:sms,weixin,dingding) + @param: get.fun_name 消息模块方法名称(如:send_sms,push_msg) + """ + module_name = get.module_name + fun_name = get.fun_name + + m_objs = public.init_msg(module_name) + if not m_objs: return public.returnMsg(False, 'Setup failed, [{}] is not installed'.format(module_name)) + + return getattr(m_objs,fun_name)(get) + + + def get_msg_configs_by(self,get): + """ + @name 获取单独消息通道配置 + @auther: cjxin + @date: 2022-08-16 + @param: get.name 消息模块名称(如:sms,weixin,dingding) + """ + name = get.name + res = {} + res['data'] = {} + res['setup'] = False + res['info'] = False + try: + obj = public.init_msg(name) + if obj: + res['setup'] = True + res['data'] = obj.get_config(None) + res['info'] = obj.get_version_info(None); + except: pass + return res + + + + def get_msg_push_list(self,get): + """ + @name 获取消息通道配置列表 + @auther: cjxin + @date: 2022-08-16 + """ + cpath = 'data/msg.json' + try: + if 'force' in get or not os.path.exists(cpath): + if not 'download_url' in session: session['download_url'] = public.get_url() + public.downloadFile('{}/linux/panel/msg/msg.json'.format(session['download_url']),cpath) + except : pass + + data = {} + if os.path.exists(cpath): + msgs = json.loads(public.readFile(cpath)) + for x in msgs: + x['setup'] = False + x['info'] = False + key = x['name'] + try: + obj = public.init_msg(x['name']) + if obj: + x['setup'] = True + x['info'] = obj.get_version_info(None) + except : + print(public.get_error_info()) + pass + data[key] = x + return data + + + # 检查是否提交过问卷 + def check_nps(self, get): + # 校验参数 + try: + get.validate([ + Param('product_type').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + if 'product_type' not in get: + return public.return_message(-1,0, 'Parameter error') + ikey = 'check_nps' + result = cache.get(ikey) + if result: + return public.return_message(0,0,result) + + url = "https://www.aapanel.com/api/panel/nps/check" + + data = { + 'product_type': get.get('product_type', 1), + #'server_id': user_info['server_id'], + } + + try: + user_info = json.loads(public.ReadFile("{}/data/userInfo.json".format(public.get_panel_path()))) + data['server_id'] = user_info['server_id'] + + except: + pass + res = public.httpPost(url, data) + try: + res = json.loads(res) + except: + pass + + # 连不上官网时使用默认数据 + if not isinstance(res, dict): + res = { + "nonce": 0, + "success": False, + "res": False + } + + + # 判断运行天数 + safe_day = 0 + cur_timestamp = int(time.time()) + # if os.path.exists("data/%s_nps_time.pl" % software_name): + if os.path.exists("/www/server/panel/data/panel_nps_time.pl"): + try: + # nps_time = float(public.ReadFile("/www/server/panel/data/panel_nps_time.pl")) + nps_time = float(public.ReadFile("data/panel_nps_time.pl")) + safe_day = int((cur_timestamp - nps_time) / 86400) + + except: + public.WriteFile("data/panel_nps_time.pl", "%s" % cur_timestamp) + else: + public.WriteFile("data/panel_nps_time.pl", "%s" % cur_timestamp) + + datas = {'nonce': res.get('nonce', 0), + 'success': res.get('success', False), + 'res': { + 'safe_day': safe_day, + 'is_submit': res.get('res', False) + }} + + cache.set(ikey, datas, 3600) + # if res['success']: + # return public.returnMsg(True, 'Questionnaire has been submitted') + + # return public.returnMsg(False, 'No questionnaire has been submitted') + return public.return_message(0,0,datas) + + + def get_nps_new(self, get): + """ + 获取问卷 + """ + # 校验参数 + try: + get.validate([ + Param('product_type').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + # 官网接口 需替换 + url = "https://www.aapanel.com/api/panel/nps/questions" + data = { + 'product_type': get.get('product_type', 1), + # 'action': "list", + # 'version': get.get('version', -1) + } + # request发送post请求并指定form_data参数 + res = public.httpPost(url, data) + # public.print_log("获取问卷@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ {}".format(res)) + try: + res = json.loads(res) + except: + pass + return public.return_message(0,0,res) + + except: + return public.return_message(-1,0,"Failed to obtain questionnaire") + + def write_nps_new(self, get): + ''' + @name nps 提交 + @param rate 评分 + @param feedback 反馈内容 + ''' + # 校验参数 + try: + get.validate([ + Param('questions').String(), + Param('product_type').Integer(), + Param('rate').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if 'product_type' not in get: + return public.return_message(-1,0, 'Parameter error') + + # if 'questions' not in get: + # public.returnMsg(False, '参数错误') + if 'rate' not in get: + return public.return_message(-1,0, 'Parameter error') + + # try: + # if not hasattr(get, 'software_name'): + # public.returnMsg(False, '参数错误') + + # software_name = get['software_name'] + # public.WriteFile("data/{}_nps.pl".format(software_name), "1") + + data = { + # 'action': "submit", + # 'uid': user_info['uid'], # 用户ID + # 'access_key': user_info['access_key'], # 用户密钥 + # 'is_paid': get['is_paid'], # 是否付费 + # 'phone_back': get['back_phone'], # 是否回访 + # 'feedback': get['feedback'] # 反馈内容 + # 'reason_tags': get['reason_tags'], # 问题标签 + 'rate': get.get('rate', 1), # 评分 1~10 + 'product_type': get.get('product_type', 1), # 产品类型 + #'server_id': user_info['server_id'], # 服务器ID + 'questions': get['questions'], # 问题列表 + 'panel_version': public.version(), # 面板版本 + + } + url_headers = { + # "authorization": "bt {}".format(user_info['token']) + } + + try: + user_info = json.loads(public.ReadFile("{}/data/userInfo.json".format(public.get_panel_path()))) + data[ 'server_id']= user_info['server_id'] + url_headers = { + "authorization": "bt {}".format(user_info['token']) + } + except: + pass + + url = 'https://www.aapanel.com/api/panel/nps/submit' + if not hasattr(get, 'questions'): + return public.return_message(-1,0, "questions Parameter error") + else: + try: + content = json.loads(get.questions) + for _, i in content.items(): + if len(i) > 512: + # public.ExecShell("rm -f data/{}_nps.pl".format(software_name)) + return public.return_message(-1,0, "The submitted text is too long, please adjust and resubmit (MAX: 512)") + except: + return public.return_message(-1,0, "questions Parameter error") + # if not hasattr(get, 'product_type'): + # return public.returnMsg(False, "参数错误") + # if not hasattr(get, 'rate'): + # return public.returnMsg(False, "参数错误") + # if not hasattr(get, 'reason_tags'): + # get['reason_tags'] = "1" + # if not hasattr(get, 'is_paid'): + # get['is_paid'] = 0 # 是否付费 + # if not hasattr(get, 'phone_back'): + # get.phone_back = 0 + # if not hasattr(get, 'phone_back'): + # get.feedback = "" + + res = public.httpPost(url, data=data, headers=url_headers) + try: + res = json.loads(res) + except: + pass + + # 连不上官网时使用默认数据 + if not isinstance(res, dict): + res = { + "nonce": 0, + "success": False, + "res": "The submission failed, please check to connect to the node" + } + + if res['success']: + return public.return_message(0,0, "Submitted successfully") + + return public.return_message(-1,0, res['res'] if 'res' in res else "The submission failed, please check to connect to the node") + + # Get all translations + def get_translations(self, args: public.dict_obj): + from BTPanel import load_translations + return load_translations() + + # # nps问卷 + # def stop_nps(self, get): + # if 'software_name' not in get: + # public.returnMsg(False, '参数错误') + # if get.software_name == "panel": + # self._stop_panel_nps() + # else: + # public.WriteFile("data/%s_nps.pl" % get.software_name, "") + # return public.returnMsg(True, '关闭成功') + + # def get_nps(self, get): + # if 'software_name' not in get: public.returnMsg(False, '参数错误') + # software_name = get.software_name + # if software_name == "panel": + # return self._get_panel_nps() + # data = {'safe_day': 0} + # # conf = self.get_config(None) + # # 判断运行天数 + # if os.path.exists("data/%s_nps_time.pl" % software_name): + # try: + # nps_time = float(public.ReadFile("data/%s_nps_time.pl" % software_name)) + # data['safe_day'] = int((time.time() - nps_time) / 86400) + # + # except: + # public.WriteFile("data/%s_nps_time.pl" % software_name, "%s" % time.time()) + # else: + # public.WriteFile("data/%s_nps_time.pl" % software_name, "%s" % time.time()) + # + # if not os.path.exists("data/%s_nps.pl" % software_name): + # # 如果安全运行天数大于5天 并且没有没有填写过nps的信息 + # data['nps'] = False + # else: + # data['nps'] = True + # return data + + # def write_nps(self, get): + # ''' + # @name nps 提交 + # @param rate 评分 + # @param feedback 反馈内容 + # + # ''' + # if 'product_type' not in get: public.returnMsg(False, '参数错误') + # if 'software_name' not in get: public.returnMsg(False, '参数错误') + # software_name = get.software_name + # product_type = get.product_type + # import json, requests + # api_url = 'https://www.bt.cn/api/v2/contact/nps/submit' + # user_info = json.loads(public.ReadFile("{}/data/userInfo.json".format(public.get_panel_path()))) + # if 'rate' not in get: + # return public.returnMsg(False, "参数错误") + # if 'feedback' not in get: + # get.feedback = "" + # if 'phone_back' not in get: + # get.phone_back = 0 + # else: + # if get.phone_back == 1: + # get.phone_back = 1 + # else: + # get.phone_back = 0 + # + # if 'questions' not in get: + # return public.returnMsg(False, "参数错误") + # + # try: + # get.questions = json.loads(get.questions) + # except: + # return public.returnMsg(False, "参数错误") + # + # data = { + # "uid": user_info['uid'], + # "access_key": user_info['access_key'], + # "server_id": user_info['server_id'], + # "product_type": product_type, + # "rate": get.rate, + # "feedback": get.feedback, + # "phone_back": get.phone_back, + # "questions": json.dumps(get.questions) + # } + # try: + # requests.post(api_url, data=data, timeout=10).json() + # if software_name == "panel": + # self._stop_panel_nps(is_complete=True) + # else: + # public.WriteFile("data/{}_nps.pl".format(software_name), "1") + # except: + # pass + # return public.returnMsg(True, "提交成功") + + + + + # @staticmethod + # def _get_panel_nps_data(): + # panel_path = public.get_panel_path() + # try: + # nps_file = "{}/data/btpanel_nps_data".format(panel_path) + # if os.path.exists(nps_file): + # with open(nps_file, mode="r") as fp: + # nps_data = json.load(fp) + # else: + # time_file = "{}/data/panel_nps_time.pl".format(panel_path) + # post_nps_done = "{}/data/panel_nps.pl".format(panel_path) + # if not os.path.exists(time_file): + # install_time = time.time() + # else: + # with open(time_file, mode="r") as fp: + # install_time = float(fp.read()) + # nps_data = { + # "time": install_time, + # "status": "complete" if os.path.exists(post_nps_done) else "waiting", + # "popup_count": 0 + # } + # + # with open(nps_file, mode="w") as fp: + # fp.write(json.dumps(nps_data)) + # except: + # nps_data = { + # "time": time.time(), + # "status": "waiting", + # "popup_count": 0 + # } + # + # return nps_data + # + # @staticmethod + # def _save_panel_nps_data(nps_data): + # panel_path = public.get_panel_path() + # nps_file = "{}/data/btpanel_nps_data".format(panel_path) + # with open(nps_file, mode="w") as fp: + # fp.write(json.dumps(nps_data)) + # + # def _get_panel_nps(self): + # nps_data = self._get_panel_nps_data() + # safe_day = int((time.time() - nps_data["time"]) / 86400) + # res = {'safe_day': safe_day} + # if nps_data["status"] == "complete": + # res["nps"] = False + # elif nps_data["status"] == "stopped": + # res["nps"] = False + # else: + # if safe_day >= 10: + # res["nps"] = True + # return res + # + # def _stop_panel_nps(self, is_complete: bool = False): + # nps_data = self._get_panel_nps_data() + # if is_complete: + # nps_data["status"] = "complete" + # else: + # nps_data["status"] = "stopped" + # + # self._save_panel_nps_data(nps_data) + diff --git a/class_v2/crontab_ssl_v2.py b/class_v2/crontab_ssl_v2.py new file mode 100644 index 00000000..172f13df --- /dev/null +++ b/class_v2/crontab_ssl_v2.py @@ -0,0 +1,58 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: <290070744@aapanel.com> +# ------------------------------------------------------------------- + +# ------------------------------ +# ssl自动续订定时任务脚本 +# ------------------------------ +import os, json, sys, time + +os.chdir("/www/server/panel") +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import public + +sys.path.append(".") +import panelSSL +import panelSite + + +class dict_obj: + def __contains__(self, key): + return getattr(self, key, None) + + def __setitem__(self, key, value): setattr(self, key, value) + + def __getitem__(self, key): return getattr(self, key, None) + + def __delitem__(self, key): delattr(self, key) + + def __delattr__(self, key): delattr(self, key) + + def get_items(self): return self + + +if __name__ == "__main__": + get = dict_obj() + obj = panelSSL.panelSSL() + CertList = obj.GetCertList(get) + cmd_list = json.loads(public.ReadFile("/www/server/panel/vhost/crontab.json")) + panelSite_=panelSite.panelSite() + for i in CertList: + timeArray = time.strptime(i['notAfter'], "%Y-%m-%d") + timestamp = time.mktime(timeArray) + if int(timestamp) - time.time() < 86400 * 30: # 如果证书到期时间小于多少天就续订 + subject = i['subject'] + for j in cmd_list: + if subject == j['siteName']: + cmd = j['cmd'] + public.ExecShell(cmd) + # 保存证书 + get.siteName=subject + result = panelSite_.save_cert(get) + public.serviceReload() diff --git a/class_v2/crontab_v2.py b/class_v2/crontab_v2.py new file mode 100644 index 00000000..b55718d0 --- /dev/null +++ b/class_v2/crontab_v2.py @@ -0,0 +1,564 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import public,db,os,time,re, json +from BTPanel import session,cache +class crontab: + field = 'id,name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sName,sBody,sType,urladdress' + field += ",save_local,notice,notice_channel" + #取计划任务列表 + def GetCrontab(self,get): + self.checkBackup() + self.__clean_log() + cront = public.M('crontab').order("id desc").field(self.field).select() + if type(cront) == str: + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'status' INTEGER DEFAULT 1",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'save' INTEGER DEFAULT 3",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'backupTo' TEXT DEFAULT off",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sName' TEXT",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sBody' TEXT",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sType' TEXT",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'urladdress' TEXT",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'save_local' INTEGER DEFAULT 0",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'notice' INTEGER DEFAULT 0",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'notice_channel' TEXT DEFAULT ''",()) + cront = public.M('crontab').order("id desc").field(self.field).select() + data=[] + for i in range(len(cront)): + tmp = {} + tmp=cront[i] + if cront[i]['type']=="day": + tmp['type']=public.get_msg_gettext('Per Day') + tmp['cycle']= public.get_msg_gettext('Per Day, run at {} Hour {} Min',(str(cront[i]['where_hour']),str(cront[i]['where_minute']))) + elif cront[i]['type']=="day-n": + tmp['type']=public.get_msg_gettext('Every {} Days',(str(cront[i]['where1']),)) + tmp['cycle']=public.get_msg_gettext('Every {} Days, run at {} Hour {} Min',(str(cront[i]['where1']),str(cront[i]['where_hour']),str(cront[i]['where_minute']))) + elif cront[i]['type']=="hour": + tmp['type']=public.get_msg_gettext('Per Hour') + tmp['cycle']=public.get_msg_gettext('Per Hour, run at {} Min',(str(cront[i]['where_minute']),)) + elif cront[i]['type']=="hour-n": + tmp['type']=public.get_msg_gettext('Every {} Hours',(str(cront[i]['where1']),)) + tmp['cycle']=public.get_msg_gettext('Every {} Hours, run at {} Min',(str(cront[i]['where1']),str(cront[i]['where_minute']))) + elif cront[i]['type']=="minute-n": + tmp['type']=public.get_msg_gettext('Every {} Minutes',(str(cront[i]['where1']),)) + tmp['cycle']=public.get_msg_gettext('Run Every {} Minutes',(str(cront[i]['where1']),)) + elif cront[i]['type']=="week": + tmp['type']=public.get_msg_gettext('Weekly') + if not cront[i]['where1']: cront[i]['where1'] = '0' + tmp['cycle']= public.get_msg_gettext('Every {}, run at {} Hour {} Min',(self.toWeek(int(cront[i]['where1'])),str(cront[i]['where_hour']),str(cront[i]['where_minute']))) + elif cront[i]['type']=="month": + tmp['type']=public.get_msg_gettext('Monthly') + tmp['cycle']=public.get_msg_gettext('Monthly, run on {}Day {} Hour {}Min',(str(cront[i]['where1']),str(cront[i]['where_hour']),str(cront[i]['where_minute']))) + + log_file = '/www/server/cron/{}.log'.format(tmp['echo']) + if os.path.exists(log_file): + tmp['addtime'] = self.get_last_exec_time(log_file) + data.append(tmp) + return data + + def get_backup_list(self, args): + ''' + @name 获取指定备份任务的备份文件列表 + @author hwliang + @param args 参数{ + cron_id 任务ID 必填 + p 页码 默认1 + rows 每页显示条数 默认10 + callback jsonp回调函数 默认为空 + } + @return { + page 分页HTML + data 数据列表 + } + ''' + + p = args.get('p/d', 1) + rows = args.get('rows/d', 10) + tojs = args.get('tojs/s', '') + callback = args.get('callback/s', '') if tojs else tojs + + cron_id = args.get('cron_id/d') + count = public.M('backup').where('cron_id=?', (cron_id,)).count() + data = public.get_page(count, p, rows, callback) + data['data'] = public.M('backup').where('cron_id=?', (cron_id,)).limit(data['row'], data['shift']).select() + return data + + def get_last_exec_time(self,log_file): + ''' + @name 获取上次执行时间 + @author hwliang + @param log_file 日志文件路径 + @return format_date + ''' + exec_date = '' + try: + log_body = public.GetNumLines(log_file,20) + if log_body: + log_arr = log_body.split('\n') + date_list = [] + for i in log_arr: + if i.find('★') != -1 and i.find('[') != -1 and i.find(']') != -1: + date_list.append(i) + if date_list: + exec_date = date_list[-1].split(']')[0].split('[')[1] + except: + pass + + finally: + if not exec_date: + exec_date = public.format_date(times=int(os.path.getmtime(log_file))) + return exec_date + + + #清理日志 + def __clean_log(self): + try: + log_file = '/www/server/cron' + if not os.path.exists(log_file): return False + for f in os.listdir(log_file): + if f[-4:] != '.log': continue + filename = log_file + '/' + f + if os.path.getsize(filename) < 1048576 /2: continue + tmp = public.GetNumLines(filename,100) + public.writeFile(filename,tmp) + except: + pass + + + #转换大写星期 + def toWeek(self,num): + wheres={ + 0 : public.get_msg_gettext('Sunday'), + 1 : public.get_msg_gettext('Monday'), + 2 : public.get_msg_gettext('Tuesday'), + 3 : public.get_msg_gettext('Wednesday'), + 4 : public.get_msg_gettext('Thursday'), + 5 : public.get_msg_gettext('Friday'), + 6 : public.get_msg_gettext('Saturday') + } + try: + return wheres[num] + except: + return '' + + #检查环境 + def checkBackup(self): + if cache.get('check_backup'): return None + + # 检查备份表是否正确 + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'backup','%cron_id%')).count(): + public.M('backup').execute("ALTER TABLE 'backup' ADD 'cron_id' INTEGER DEFAULT 0",()) + + #检查备份脚本是否存在 + filePath=public.GetConfigValue('setup_path')+'/panel/script/backup' + if not os.path.exists(filePath): + public.downloadFile(public.GetConfigValue('home') + '/linux/backup.sh',filePath) + #检查日志切割脚本是否存在 + filePath=public.GetConfigValue('setup_path')+'/panel/script/logsBackup' + if not os.path.exists(filePath): + public.downloadFile(public.GetConfigValue('home') + '/linux/logsBackup.py',filePath) + #检查计划任务服务状态 + import system + sm = system.system() + if os.path.exists('/etc/init.d/crond'): + if not public.process_exists('crond'): public.ExecShell('/etc/init.d/crond start') + elif os.path.exists('/etc/init.d/cron'): + if not public.process_exists('cron'): public.ExecShell('/etc/init.d/cron start') + elif os.path.exists('/usr/lib/systemd/system/crond.service'): + if not public.process_exists('crond'): public.ExecShell('systemctl start crond') + cache.set('check_backup',True,3600) + + + #设置计划任务状态 + def set_cron_status(self,get): + id = get['id'] + cronInfo = public.M('crontab').where('id=?',(id,)).field(self.field).find() + status_msg = ['Stop','Start'] + status = 1 + if cronInfo['status'] == status: + status = 0 + self.remove_for_crond(cronInfo['echo']) + else: + cronInfo['status'] = 1 + if not self.sync_to_crond(cronInfo): + return public.return_msg_gettext(False,'Unable to write to file, please check if system hardening is enabled!') + + public.M('crontab').where('id=?',(id,)).setField('status',status) + public.WriteLog('TYPE_CRON',"MODIFY_CRON_STATUS",(cronInfo['name'],str(status_msg[status]))) + return public.return_msg_gettext(True,'Setup successfully!') + + #修改计划任务 + def modify_crond(self,get): + if len(get['name'])<1: + return public.return_msg_gettext(False,'Name of task cannot be empty!') + id = get['id'] + cuonConfig,get,name = self.GetCrondCycle(get) + cronInfo = public.M('crontab').where('id=?',(id,)).field(self.field).find() + if not get['where1']: get['where1'] = get['week'] + del(cronInfo['id']) + del(cronInfo['addtime']) + cronInfo['name'] = get['name'] + cronInfo['type'] = get['type'] + cronInfo['where1'] = get['where1'] + cronInfo['where_hour'] = get['hour'] + cronInfo['where_minute'] = get['minute'] + cronInfo['save'] = get['save'] + cronInfo['backupTo'] = get['backupTo'] + cronInfo['sBody'] = get['sBody'] + cronInfo['urladdress'] = get['urladdress'] + columns = 'name,type,where1,where_hour,where_minute,save,backupTo,sBody,urladdress' + values = (get['name'],get['type'],get['where1'],get['hour'], + get['minute'],get['save'],get['backupTo'],get['sBody'] + ,get['urladdress']) + if 'save_local' in get: + columns += ",save_local, notice, notice_channel" + values = (get['name'],get['type'],get['where1'],get['hour'], + get['minute'],get['save'],get['backupTo'],get['sBody'], + get['urladdress'],get['save_local'],get["notice"], + get["notice_channel"]) + self.remove_for_crond(cronInfo['echo']) + if cronInfo['status'] == 0: return public.return_msg_gettext(False, 'The current task is Disable status, please open the task before modifying!') + if not self.sync_to_crond(cronInfo): + return public.return_msg_gettext(False,'Unable to write to file, please check if system hardening is enabled!') + public.M('crontab').where('id=?',(id,)).save(columns,values) + + public.WriteLog('TYPE_CRON',"MODIFY_CRON",(cronInfo['name'],)) + return public.return_msg_gettext(True,'Setup successfully!') + + + #获取指定任务数据 + def get_crond_find(self,get): + id = int(get.id) + data = public.M('crontab').where('id=?',(id,)).field(self.field).find() + return data + + #同步到crond + def sync_to_crond(self,cronInfo): + if not 'status' in cronInfo: return False + if 'where_hour' in cronInfo: + cronInfo['hour'] = cronInfo['where_hour'] + cronInfo['minute'] = cronInfo['where_minute'] + cronInfo['week'] = cronInfo['where1'] + cuonConfig,cronInfo,name = self.GetCrondCycle(cronInfo) + cronPath=public.GetConfigValue('setup_path')+'/cron' + cronName=self.GetShell(cronInfo) + if type(cronName) == dict: return cronName + #if cronInfo['status'] == 0: return False + cuonConfig += ' ' + cronPath+'/'+cronName+' >> '+ cronPath+'/'+cronName+'.log 2>&1' + wRes = self.WriteShell(cuonConfig) + if type(wRes) != bool: return False + self.CrondReload() + return True + + #添加计划任务 + def AddCrontab(self,get): + if len(get['name'])<1: + return public.return_msg_gettext(False,'Name of task cannot be empty!') + cuonConfig,get,name = self.GetCrondCycle(get) + cronPath=public.GetConfigValue('setup_path')+'/cron' + cronName=self.GetShell(get) + if type(cronName) == dict: return cronName + cuonConfig += ' ' + cronPath+'/'+cronName+' >> '+ cronPath+'/'+cronName+'.log 2>&1' + + wRes = self.WriteShell(cuonConfig) + if type(wRes) != bool: return wRes + self.CrondReload() + columns = 'name,type,where1,where_hour,where_minute,echo,addtime,\ + status,save,backupTo,sType,sName,sBody,urladdress' + values = (public.xssencode2(get['name']),get['type'],get['where1'],get['hour'], + get['minute'],cronName,time.strftime('%Y-%m-%d %X',time.localtime()), + 1,get['save'],get['backupTo'],get['sType'],get['sName'],get['sBody'], + get['urladdress']) + if "save_local" in get: + columns += ",save_local,notice,notice_channel" + values = (public.xssencode2(get['name']),get['type'],get['where1'],get['hour'], + get['minute'],cronName,time.strftime('%Y-%m-%d %X',time.localtime()), + 1,get['save'],get['backupTo'],get['sType'],get['sName'],get['sBody'], + get['urladdress'], get["save_local"], get['notice'], get['notice_channel']) + addData=public.M('crontab').add(columns,values) + public.add_security_logs('TYPE_CRON','Add Cron tasks ['+get['name']+'] success'+str(values)) + if type(addData) == str: + return public.return_msg_gettext(False, addData) + public.WriteLog('TYPE_CRON', 'Add Cron tasks [' + get['name'] + '] success') + if addData>0: + result = public.return_msg_gettext(True,'Setup successfully!') + result['id'] = addData + return result + return public.return_msg_gettext(False,'Failed to add') + + #构造周期 + def GetCrondCycle(self,params): + cuonConfig="" + name = "" + if params['type']=="day": + cuonConfig = self.GetDay(params) + name = public.get_msg_gettext('Per Day') + elif params['type']=="day-n": + cuonConfig = self.GetDay_N(params) + name = public.get_msg_gettext('Every {0} Days',(params['where1'],)) + elif params['type']=="hour": + cuonConfig = self.GetHour(params) + name = public.get_msg_gettext('Per Hour') + elif params['type']=="hour-n": + cuonConfig = self.GetHour_N(params) + name = public.get_msg_gettext('Per Hour') + elif params['type']=="minute-n": + cuonConfig = self.Minute_N(params) + elif params['type']=="week": + params['where1']=params['week'] + cuonConfig = self.Week(params) + elif params['type']=="month": + cuonConfig = self.Month(params) + return cuonConfig,params,name + + #取任务构造Day + def GetDay(self,param): + cuonConfig ="{} {} * * * ".format(param['minute'],param['hour']) + return cuonConfig + #取任务构造Day_n + def GetDay_N(self,param): + cuonConfig ="{} {} */{} * * ".format(param['minute'],param['hour'],param['where1']) + return cuonConfig + + #取任务构造Hour + def GetHour(self,param): + cuonConfig ="{} * * * * ".format(param['minute']) + return cuonConfig + + #取任务构造Hour-N + def GetHour_N(self,param): + cuonConfig ="{} */{} * * * ".format(param['minute'],param['where1']) + return cuonConfig + + #取任务构造Minute-N + def Minute_N(self,param): + cuonConfig ="*/{} * * * * ".format(param['where1']) + return cuonConfig + + #取任务构造week + def Week(self,param): + cuonConfig ="{} {} * * {}".format(param['minute'],param['hour'],param['week']) + return cuonConfig + + #取任务构造Month + def Month(self,param): + cuonConfig = "{} {} {} * * ".format(param['minute'],param['hour'],param['where1']) + return cuonConfig + + #取数据列表 + def GetDataList(self,get): + data = {} + if get['type'] == 'databases': + data['data'] = public.M(get['type']).where("type=?","MySQL").field('name,ps').select() + else: + data['data'] = public.M(get['type']).field('name,ps').select() + for i in data['data']: + if 'ps' in i: + i['ps'] = public.xsssec(i['ps']) + data['orderOpt'] = [] + import json + tmp = public.readFile('data/libList.conf') + if not tmp: return data + libs = json.loads(tmp) + for lib in libs: + if not 'opt' in lib: continue + filename = 'plugin/{}'.format(lib['opt']) + if not os.path.exists(filename): continue + tmp = {} + tmp['name'] = lib['name'] + tmp['value']= lib['opt'] + data['orderOpt'].append(tmp) + return data + + #取任务日志 + def GetLogs(self,get): + id = get['id'] + echo = public.M('crontab').where("id=?",(id,)).field('echo').find() + logFile = public.GetConfigValue('setup_path')+'/cron/'+echo['echo']+'.log' + if not os.path.exists(logFile):return public.return_msg_gettext(False, 'log is empty') + log = public.GetNumLines(logFile,2000) + return public.return_msg_gettext(True, log) + + #清理任务日志 + def DelLogs(self,get): + try: + id = get['id'] + echo = public.M('crontab').where("id=?",(id,)).getField('echo') + logFile = public.GetConfigValue('setup_path')+'/cron/'+echo+'.log' + os.remove(logFile) + return public.return_msg_gettext(True, 'Logs emptied') + except: + return public.return_msg_gettext(False, 'Failed to empty task logs!') + + #删除计划任务 + def DelCrontab(self,get): + try: + id = get['id'] + find = public.M('crontab').where("id=?",(id,)).field('name,echo').find() + if not find: return public.return_msg_gettext(False, 'The specified task does not exist!') + if not self.remove_for_crond(find['echo']): return public.return_msg_gettext(False,'Unable to write to file, please check if system hardening is enabled!') + cronPath = public.GetConfigValue('setup_path') + '/cron' + sfile = cronPath + '/' + find['echo'] + if os.path.exists(sfile): os.remove(sfile) + sfile = cronPath + '/' + find['echo'] + '.log' + if os.path.exists(sfile): os.remove(sfile) + + public.M('crontab').where("id=?",(id,)).delete() + public.add_security_logs("Delete cron", "Delete cron:" + find['name']) + public.WriteLog('TYPE_CRON', 'CRONTAB_DEL',(find['name'],)) + return public.return_msg_gettext(True, 'Successfully deleted') + except: + return public.return_msg_gettext(False, 'Failed to delete') + + #从crond删除 + def remove_for_crond(self,echo): + file = self.get_cron_file() + if not os.path.exists(file): + return False + conf=public.readFile(file) + if not conf: return False + if conf.find(str(echo)) == -1: return True + rep = ".+" + str(echo) + ".+\n" + conf = re.sub(rep, "", conf) + try: + if not public.writeFile(file,conf): return False + except: + return False + self.CrondReload() + return True + + #取执行脚本 + def GetShell(self,param): + #try: + type=param['sType'] + if not 'echo' in param: + cronName=public.md5(public.md5(str(time.time()) + '_bt')) + else: + cronName = param['echo'] + if type=='toFile': + shell=param.sFile + else : + head="#!/bin/bash\nPATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin\nexport PATH\n" + python_bin = "{} -u".format(public.get_python_bin()) + if public.get_webserver()=='nginx': + log='.log' + elif public.get_webserver()=='apache': + log = '-access_log' + else: + log = '_ols.access_log' + if type in ['site','path'] and param['sBody'] != 'undefined' and len(param['sBody']) > 1: + exports = param['sBody'].replace("\r\n","\n").replace("\n",",") + head += "BT_EXCLUDE=\"" + exports.strip() + "\"\nexport BT_EXCLUDE\n" + attach_param = " " + cronName + wheres={ + 'path': head + python_bin +" " + public.GetConfigValue('setup_path')+"/panel/script/backup.py path "+param['sName']+" "+str(param['save'])+attach_param, + 'site' : head +python_bin+ " " + public.GetConfigValue('setup_path')+"/panel/script/backup.py site "+param['sName']+" "+str(param['save'])+attach_param, + 'database': head +python_bin+ " " + public.GetConfigValue('setup_path')+"/panel/script/backup.py database "+param['sName']+" "+str(param['save'])+attach_param, + 'logs' : head +python_bin+ " " + public.GetConfigValue('setup_path')+"/panel/script/logsBackup "+param['sName']+log+" "+str(param['save']), + 'rememory' : head + "/bin/bash " + public.GetConfigValue('setup_path') + '/panel/script/rememory.sh', + 'webshell': head +python_bin+ " " + public.GetConfigValue('setup_path') + '/panel/class/webshell_check.py site ' + param['sName'] +' ' +param['urladdress'] + } + if param['backupTo'] != 'localhost': + cfile = public.GetConfigValue('setup_path') + "/panel/plugin/" + param['backupTo'] + "/" + param['backupTo'] + "_main.py" + if not os.path.exists(cfile): cfile = public.GetConfigValue('setup_path') + "/panel/script/backup_" + param['backupTo'] + ".py" + wheres={ + 'path': head + python_bin+" " + cfile + " path " + param['sName'] + " " + str(param['save'])+attach_param, + 'site' : head + python_bin+" " + cfile + " site " + param['sName'] + " " + str(param['save'])+attach_param, + 'database': head + python_bin+" " + cfile + " database " + param['sName'] + " " + str(param['save'])+attach_param, + 'logs' : head + python_bin+" " + public.GetConfigValue('setup_path')+"/panel/script/logsBackup "+param['sName']+log+" "+str(param['save']), + 'rememory' : head + "/bin/bash " + public.GetConfigValue('setup_path') + '/panel/script/rememory.sh', + 'webshell': head + python_bin+" " + public.GetConfigValue('setup_path') + '/panel/class/webshell_check.py site ' + param['sName'] +' ' +param['urladdress'] + } + + try: + shell=wheres[type] + except: + if type == 'toUrl': + shell = head + "curl -sS --connect-timeout 10 -m 3600 '" + param['urladdress']+"'" + else: + shell=head+param['sBody'].replace("\r\n","\n") + + shell += ''' +echo "----------------------------------------------------------------------------" +endDate=`date +"%Y-%m-%d %H:%M:%S"` +echo "★[$endDate] Successful" +echo "----------------------------------------------------------------------------" +''' + cronPath=public.GetConfigValue('setup_path')+'/cron' + if not os.path.exists(cronPath): public.ExecShell('mkdir -p ' + cronPath) + file = cronPath+'/' + cronName + public.writeFile(file,self.CheckScript(shell)) + public.ExecShell('chmod 750 ' + file) + return cronName + #except Exception as ex: + #return public.return_msg_gettext(False, 'Failed to write in file!' + str(ex)) + + #检查脚本 + def CheckScript(self,shell): + keys = ['shutdown','init 0','mkfs','passwd','chpasswd','--stdin','mkfs.ext','mke2fs'] + for key in keys: + shell = shell.replace(key,'[***]') + return shell + + #重载配置 + def CrondReload(self): + if os.path.exists('/etc/init.d/crond'): + public.ExecShell('/etc/init.d/crond reload') + elif os.path.exists('/etc/init.d/cron'): + public.ExecShell('service cron restart') + else: + public.ExecShell("systemctl reload crond") + + #将Shell脚本写到文件 + def WriteShell(self,config): + u_file = '/var/spool/cron/crontabs/root' + file = self.get_cron_file() + if not os.path.exists(file): public.writeFile(file,'') + conf = public.readFile(file) + if type(conf)==bool:return public.return_msg_gettext(False,'Failed to read file!') + conf += config + "\n" + if public.writeFile(file,conf): + if not os.path.exists(u_file): + public.ExecShell("chmod 600 '" + file + "' && chown root.root " + file) + else: + public.ExecShell("chmod 600 '" + file + "' && chown root.crontab " + file) + return True + return public.return_msg_gettext(False,'Unable to write to file, please check if system hardening is enabled!') + + #立即执行任务 + def StartTask(self,get): + echo = public.M('crontab').where('id=?',(get.id,)).getField('echo') + execstr = public.GetConfigValue('setup_path') + '/cron/' + echo + public.ExecShell('chmod +x ' + execstr) + public.ExecShell('nohup ' + execstr + ' >> ' + execstr + '.log 2>&1 &') + return public.return_msg_gettext(True,'Task has been executed!') + + #获取计划任务文件位置 + def get_cron_file(self): + u_path = '/var/spool/cron/crontabs' + u_file = u_path + '/root' + c_file = '/var/spool/cron/root' + cron_path = c_file + if not os.path.exists(u_path): + cron_path=c_file + + if os.path.exists("/usr/bin/apt-get"): + cron_path = u_file + elif os.path.exists('/usr/bin/yum'): + cron_path = c_file + + if cron_path == u_file: + if not os.path.exists(u_path): + os.makedirs(u_path,472) + public.ExecShell("chown root:crontab {}".format(u_path)) + if not os.path.exists(cron_path): + public.writeFile(cron_path,"") + return cron_path + + + diff --git a/class_v2/data_v2.py b/class_v2/data_v2.py new file mode 100644 index 00000000..646416fd --- /dev/null +++ b/class_v2/data_v2.py @@ -0,0 +1,892 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import sys,os,re,time +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import db,public,panelMysql +import json +import public +from public.validate import Param + +class data: + __ERROR_COUNT = 0 + #自定义排序字段 + __SORT_DATA = ['site_ssl','php_version','backup_count'] + DB_MySQL = None + web_server = None + setupPath = '/www/server' + siteorder_path = '/www/server/panel/data/siteorder.pl' + limit_path = '/www/server/panel/data/limit.pl' + + # 删除排序记录 + def del_sorted(self, get): + public.ExecShell("rm -rf {}".format(self.siteorder_path)) + return public.returnMsg(True, '清除排序成功!') + + + ''' + * 设置备注信息 + * @param String _GET['tab'] 数据库表名 + * @param String _GET['id'] 条件ID + * @return Bool + ''' + def setPs(self,get): + # 校验参数 + try: + get.validate([ + Param('table').Require().String().Xss(), + Param('ps').Require().String(), + Param('id').Require().Integer().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + get.ps = public.xssencode2(get.ps) + if public.M(get.table).where("id=?",(id,)).setField('ps',get.ps): + # return public.return_msg_gettext(True,'Setup successfully!') + return public.return_message(0, 0, "Setup successfully") + # return public.return_msg_gettext(False,'Failed to modify') + return public.return_message(-1, 0, 'Failed to modify') + + #端口扫描 + def CheckPort(self,port): + import socket + localIP = '127.0.0.1' + temp = {} + temp['port'] = port + temp['local'] = True + try: + s = socket.socket() + s.settimeout(0.01) + s.connect((localIP,port)) + s.close() + except: + temp['local'] = False + + result = 0 + if temp['local']: result +=2 + return result + + # 转换时间 + def strf_date(self, sdate): + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + + def get_cert_end(self,pem_file): + try: + import OpenSSL + result = {} + x509 = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, public.readFile(pem_file)) + # 取产品名称 + issuer = x509.get_issuer() + result['issuer'] = '' + if hasattr(issuer, 'CN'): + result['issuer'] = issuer.CN + if not result['issuer']: + is_key = [b'0', '0'] + issue_comp = issuer.get_components() + if len(issue_comp) == 1: + is_key = [b'CN', 'CN'] + for iss in issue_comp: + if iss[0] in is_key: + result['issuer'] = iss[1].decode() + break + # 取到期时间 + result['notAfter'] = self.strf_date( + bytes.decode(x509.get_notAfter())[:-1]) + # 取申请时间 + result['notBefore'] = self.strf_date( + bytes.decode(x509.get_notBefore())[:-1]) + # 取可选名称 + result['dns'] = [] + for i in range(x509.get_extension_count()): + s_name = x509.get_extension(i) + if s_name.get_short_name() in [b'subjectAltName', 'subjectAltName']: + s_dns = str(s_name).split(',') + for d in s_dns: + result['dns'].append(d.split(':')[1]) + subject = x509.get_subject().get_components() + # 取主要认证名称 + if len(subject) == 1: + result['subject'] = subject[0][1].decode() + else: + result['subject'] = result['dns'][0] + return result + except: + return public.get_cert_data(pem_file) + + + def get_site_ssl_info(self,siteName): + try: + s_file = 'vhost/nginx/{}.conf'.format(siteName) + is_apache = False + if not os.path.exists(s_file): + s_file = 'vhost/apache/{}.conf'.format(siteName) + is_apache = True + + if not os.path.exists(s_file): + return -1 + + s_conf = public.readFile(s_file) + if not s_conf: return -1 + ssl_file = None + if is_apache: + if s_conf.find('SSLCertificateFile') == -1: + return -1 + s_tmp = re.findall(r"SSLCertificateFile\s+(.+\.pem)",s_conf) + if not s_tmp: return -1 + ssl_file = s_tmp[0] + else: + if s_conf.find('ssl_certificate') == -1: + return -1 + s_tmp = re.findall(r"ssl_certificate\s+(.+\.pem);",s_conf) + if not s_tmp: return -1 + ssl_file = s_tmp[0] + ssl_info = self.get_cert_end(ssl_file) + if not ssl_info: return -1 + ssl_info['endtime'] = int(int(time.mktime(time.strptime(ssl_info['notAfter'], "%Y-%m-%d")) - time.time()) / 86400) + return ssl_info + except: return -1 + #return "{}:{}".format(ssl_info['issuer'],ssl_info['notAfter']) + + # 查询网站对应的PHP版本 + def get_php_version(self,siteName): + try: + + if not self.web_server: + self.web_server = public.get_webserver() + + conf = public.readFile(self.setupPath + '/panel/vhost/'+self.web_server+'/'+siteName+'.conf') + if self.web_server == 'openlitespeed': + conf = public.readFile( + self.setupPath + '/panel/vhost/' + self.web_server + '/detail/' + siteName + '.conf') + if self.web_server == 'nginx': + rep = r"enable-php-(\w{2,5})[-\w]*\.conf" + elif self.web_server == 'apache': + rep = r"php-cgi-(\w{2,5})\.sock" + else: + rep = r"path\s*/usr/local/lsws/lsphp(\d+)/bin/lsphp" + tmp = re.search(rep,conf).groups() + if tmp[0] == '00': + return 'Static' + if tmp[0] == 'other': + return 'Other' + + return tmp[0][0] + '.' + tmp[0][1] + except: + return 'Static' + + def map_to_list(self,map_obj): + try: + if type(map_obj) != list and type(map_obj) != str: map_obj = list(map_obj) + return map_obj + except: return [] + + def get_database_size(self,databaseName): + try: + if not self.DB_MySQL:self.DB_MySQL = panelMysql.panelMysql() + db_size = self.map_to_list(self.DB_MySQL.query("select sum(DATA_LENGTH)+sum(INDEX_LENGTH) from information_schema.tables where table_schema='{}'".format(databaseName)))[0][0] + if not db_size: return 0 + return int(db_size) + except: + return 0 + + def get_site_quota(self,path): + ''' + @name 获取网站目录配额信息 + @author hwliang<2022-02-15> + @param path 网站目录 + @return dict + ''' + res = { + "used": 0, + "size": 0, + "quota_push": { + "size": 0, + "used": 0, + }, + "quota_storage": { + "size": 0, + "used": 0, + } + } + try: + from public.PluginLoader import get_module + quota_info = getattr(get_module('{}/class_v2/projectModelV2/quotaModel.py'.format(public.get_panel_path())), + 'main')().get_quota_path(path) + if isinstance(quota_info, dict): + res.update(quota_info) + res['size'] = int(quota_info['quota_push']['size']) + int(quota_info['quota_storage']['size']) + return res + return res + except: + from traceback import format_exc + public.print_log(format_exc()) + return res + #最新版本v2版本 + # try: + # from projectModelV2.quotaModel import main + # quota_info = main().get_quota_path(path) + # if isinstance(quota_info,dict): + # return quota_info + # return res + # except: return res + + def get_database_quota(self,db_name): + ''' + @name 获取网站目录配额信息 + @author hwliang<2022-02-15> + @param path 网站目录 + @return dict + ''' + res = { + "used": 0, + "size": 0, + "quota_push": { + "size": 0, + "used": 0, + }, + "quota_storage": { + "size": 0, + "used": 0, + } + } + try: + from public.PluginLoader import get_module + quota_info = getattr(get_module('{}/class_v2/projectModelV2/quotaModel.py'.format(public.get_panel_path())), + 'main')().get_quota_mysql(db_name) + if isinstance(quota_info, dict): + res.update(quota_info) + res['size'] = int(quota_info['quota_push']['size']) + int(quota_info['quota_storage']['size']) + return res + + return res + except: + return res + #最新版本v2版本 + # try: + # from projectModelV2.quotaModel import main + # quota_info = main().get_quota_mysql(db_name) + # if isinstance(quota_info,dict): + # return quota_info + # return res + # except: return res + + ''' + * 取数据列表 + * @param String _GET['tab'] 数据库表名 + * @param Int _GET['count'] 每页的数据行数 + * @param Int _GET['p'] 分页号 要取第几页数据 + * @return Json page.分页数 , count.总行数 data.取回的数据 + ''' + def getData(self, get): + + # 校验参数 + try: + get.validate([ + Param('table').Require().String().Xss(), + Param('search').String(), + Param('limit').Integer(), + Param('p').Integer(), + Param('type').String().Xss(), + Param('project_type').Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # # net_flow_type = { + # # "total_flow": "总流量", + # # "7_day_total_flow": "近7天流量", + # # "one_day_total_flow": "近1天流量", + # # "one_hour_total_flow": "近1小时流量" + # # } + # # net_flow_json_file = "/www/server/panel/plugin/total/panel_net_flow.json" + # + # if get.table == 'sites': + # if not hasattr(get, 'order'): + # if os.path.exists(self.siteorder_path): + # order = public.readFile(self.siteorder_path) + # if order.split(' ')[0] in self.__SORT_DATA: + # get.order = order + # + # if not hasattr(get, 'limit') or get.limit == '' or int(get.limit) == 0: + # try: + # if os.path.exists(self.limit_path): + # get.limit = int(public.readFile(self.limit_path)) + # else: + # get.limit = 20 + # except: + # get.limit = 20 + # if "order" in get: + # order = get.order + # if get.table == 'sites': + # public.writeFile(self.siteorder_path, order) + # # o_list = order.split(' ') + # # net_flow_dict = {} + # # order_type = None + # # if o_list[0].strip() in net_flow_type.keys(): + # # # net_flow_dict["flow_type"] = o_list[0].strip() + # # if len(o_list) > 1: + # # order_type = o_list[1].strip() + # # else: + # # get.order = 'id desc' + # # # net_flow_dict["order_type"] = order_type + # # public.writeFile(net_flow_json_file, json.dumps(net_flow_dict)) + # 如果网站列表包含 rname 字段排序 先检查表内是否有 rname字段 + if hasattr(get, "order") and get.table == 'sites': + if get.order.startswith('rname'): + data = public.M('sites').find() + if 'rname' not in data.keys(): + public.M('sites').execute("ALTER TABLE 'sites' ADD 'rname' text DEFAULT ''", ()) + table = get.table + data = self.GetSql(get) + SQL = public.M(table) + user_Data = self.get_user_power() + if user_Data != 'all' and table in ['sites', 'databases', 'ftps']: + data['data'] = [i for i in data['data'] if str(i['id']) in user_Data.get(table, [])] + + try: + # table = get.table + # data = self.GetSql(get) + # SQL = public.M(table) + if table == 'backup': + import os + backup_path = public.M('config').where('id=?',(1,)).getField('backup_path') + for i in range(len(data['data'])): + if data['data'][i]['size'] == 0: + if os.path.exists(data['data'][i]['filename']): + data['data'][i]['size'] = os.path.getsize(data['data'][i]['filename']) + else: + if not os.path.exists(data['data'][i]['filename']): + if (data['data'][i]['filename'].find('/www/') != -1 or data['data'][i]['filename'].find(backup_path) != -1) and data['data'][i]['filename'][0] == '/' and data['data'][i]['filename'].find('|') == -1: + data['data'][i]['size'] = 0 + data['data'][i]['ps'] = '文件不存在' + if data['data'][i]['ps'] in ['','无']: + if data['data'][i]['name'][:3] == 'db_' or (data['data'][i]['name'][:4] == 'web_' and data['data'][i]['name'][-7:] == '.tar.gz'): + data['data'][i]['ps'] = '自动备份' + else: + data['data'][i]['ps'] = '手动备份' + #判断本地文件是否存在,以确定能否下载 + data['data'][i]['local']=data['data'][i]['filename'].split('|')[0] + data['data'][i]['localexist']=0 if os.path.isfile(data['data'][i]['local']) else 1 + + elif table == 'sites' or table == 'databases': + type = '0' + if table == 'databases': + type = '1' + for i in range(len(data['data'])): + backup_count = 0 + try: + backup_count = SQL.table('backup').where("pid=? AND type=?",(data['data'][i]['id'],type)).count() + except: + pass + + data['data'][i]['backup_count'] = backup_count + if table == 'databases': data['data'][i]['conn_config'] = json.loads(data['data'][i]['conn_config']) + data['data'][i]['quota'] = self.get_database_quota(data['data'][i]['name']) + + if table == 'sites': + for i in range(len(data['data'])): + + data['data'][i]['domain'] = SQL.table('domain').where("pid=?",(data['data'][i]['id'],)).count() + # data['data'][i]['ssl'] = self.get_site_ssl_info(data['data'][i]['name']) + + ssl_info = self.get_site_ssl_info(data['data'][i]['name']) + data['data'][i]['ssl'] = ssl_info + data['data'][i]['site_ssl'] = ssl_info['endtime'] if ssl_info != -1 else -1 + + data['data'][i]['php_version'] = self.get_php_version(data['data'][i]['name']) + data['data'][i]['attack'] = self.get_analysis(get,data['data'][i]) + data['data'][i]['project_type'] = SQL.table('sites').where('id=?',(data['data'][i]['id'])).field('project_type').find()['project_type'] + if data['data'][i]['project_type'] in ['WP', 'WP2']: + import one_key_wp + one_key_wp_obj = one_key_wp.one_key_wp() + data['data'][i]['cache_status'] = one_key_wp_obj.get_cache_status(data['data'][i]['id']) + data['data'][i]['login_url'] = '/v2/wp/login/{}'.format(data['data'][i]['id']) + data['data'][i]['wp_version'] = one_key_wp_obj.get_wp_version(data['data'][i]['id']) + + if data['data'][i]['project_type'] == 'WP2': + from wp_toolkit import wpbackup + data['data'][i]['backup_count'] = wpbackup(data['data'][i]['id']).backup_count() + + if not data['data'][i]['status'] in ['0','1',0,1]: + data['data'][i]['status'] = '1' + data['data'][i]['quota'] = self.get_site_quota(data['data'][i]['path']) + site1 = SQL.table('sites').where('id=?', (data['data'][i]['id'])).find() + if hasattr(site1, 'rname'): + data['data'][i]['rname'] = \ + SQL.table('sites').where('id=?', (data['data'][i]['id'])).field('rname').find()['rname'] + if not data['data'][i].get('rname', ''): + data['data'][i]['rname'] = data['data'][i]['name'] + data["net_flow_info"] = {} + # try: + # net_flow_json_info = json.loads(public.readFile(net_flow_json_file)) + # data["net_flow_info"] = net_flow_json_info + # except Exception: + # data["net_flow_info"] = {} + + + elif table == 'firewall': + for i in range(len(data['data'])): + if data['data'][i]['port'].find(':') != -1 or data['data'][i]['port'].find('.') != -1 or data['data'][i]['port'].find('-') != -1: + data['data'][i]['status'] = -1 + else: + data['data'][i]['status'] = self.CheckPort(int(data['data'][i]['port'])) + + elif table == 'ftps': + for i in range(len(data['data'])): + data['data'][i]['quota'] = self.get_site_quota(data['data'][i]['path']) + + try: + for _find in data['data']: + _keys = _find.keys() + for _key in _keys: + _find[_key] = public.xsssec(_find[_key]) + except: + pass + + #返回 + res = self.get_sort_data(data) + return public.return_message(0, 0, res) + except: + res = public.get_error_info() + # return public.get_error_info() + return public.return_message(0, 0, res) + + def get_data_list(self, get): + + try: + self.check_and_add_stop_column() + if get.table == 'sites': + if not hasattr(get, 'order'): + if os.path.exists(self.siteorder_path): + order = public.readFile(self.siteorder_path) + if order.split(' ')[0] in self.__SORT_DATA: + get.order = order + else: + public.writeFile(self.siteorder_path, get.order) + if not hasattr(get, 'limit') or get.limit == '' or int(get.limit) == 0: + try: + if os.path.exists(self.limit_path): + get.limit = int(public.readFile(self.limit_path)) + else: + get.limit = 20 + except: + get.limit = 20 + else: + public.writeFile(self.limit_path, get.limit) + if not hasattr(get, 'order'): + get.order = 'addtime desc' + get = self._get_args(get) + try: + s_list = self.func_models(get, 'get_data_where') + except: + s_list = [] + + where_sql, params = self.get_where(get, s_list) + data = self.get_page_data(get, where_sql, params) + get.data_list = data['data'] + try: + data['data'] = self.func_models(get, 'get_data_list') + except : + print(traceback.format_exc()) + if get.table == 'sites': + if isinstance(data, dict): + file_path = os.path.join(public.get_panel_path(), "data/sort_list.json") + if os.path.exists(file_path): + sort_list_raw = public.readFile(file_path) + sort_list = json.loads(sort_list_raw) + sort_list_int = [int(item) for item in sort_list["list"]] + + for i in range(len(data['data'])): + if int(data['data'][i]['id']) in sort_list_int: + data['data'][i]['sort'] = 1 + else: + data['data'][i]['sort'] = 0 + + top_list = sort_list["list"] + if top_list: + top_list = top_list[::-1] + top_data = [item for item in data["data"] if str(item['id']) in top_list] + data1 = [item for item in data["data"] if str(item['id']) not in top_list] + top_data.sort(key=lambda x: top_list.index(str(x['id']))) + data['data'] = top_data + data1 + public.set_search_history(get.table, get.search_key, get.search) # 记录搜索历史 + # 字段排序 + data = self.get_sort_data(data) + if 'type_id' in get: + type_id=int(get['type_id']) + if type_id: + filtered_data = [] + target_type_id = type_id + # print(data['data']) + for item in data['data']: + if item.get('type_id') == target_type_id: + filtered_data.append(item) + data['data'] = filtered_data + if get.get("db_type",""): + if type_id < 0: + filtered_data = [] + target_type_id = type_id + for item in data['data']: + if item.get('type_id') == target_type_id: + filtered_data.append(item) + data['data'] = filtered_data + return data + except: + return traceback.format_exc() + + + # 获取用户权限列表 + def get_user_power(self, get=None): + user_Data = 'all' + try: + uid = session.get('uid') + if uid != 1 and uid: + plugin_path = '/www/server/panel/plugin/users' + if os.path.exists(plugin_path): + user_authority = os.path.join(plugin_path, 'authority') + if os.path.exists(user_authority): + if os.path.exists(os.path.join(user_authority, str(uid))): + try: + data = json.loads(self._decrypt(public.ReadFile(os.path.join(user_authority, str(uid))))) + if data['role'] == 'administrator': + user_Data = 'all' + else: + user_Data = json.loads(self._decrypt(public.ReadFile(os.path.join(user_authority, str(uid) + '.data')))) + except: + user_Data = {} + else: + user_Data = {} + except: + pass + return user_Data + + + def get_sort_data(self,data): + """ + @获取自定义排序数据 + @param data: 数据 + """ + if 'plist' in data: + plist = data['plist'] + o_list = plist['order'].split(' ') + + reverse = False + sort_key = o_list[0].strip() + + if o_list[1].strip() == 'desc': + reverse = True + + if sort_key in ['site_ssl']: + for info in data['data']: + if type(info['ssl']) == int: + info[sort_key] = info['ssl'] + else: + try: + info[sort_key] = info['ssl']['endtime'] + except : + info[sort_key] = '' + + data['data'] = sorted(data['data'],key=lambda x:x[sort_key],reverse=reverse) + data['data'] = data['data'][plist['shift'] : plist['row'] ] + return data + + ''' + * 取数据库行 + * @param String _GET['tab'] 数据库表名 + * @param Int _GET['id'] 索引ID + * @return Json + ''' + def getFind(self,get): + tableName = get.table + id = get.id + field = self.GetField(get.table) + SQL = public.M(tableName) + where = "id=?" + find = SQL.where(where,(id,)).field(field).find() + try: + _keys = find.keys() + for _key in _keys: + find[_key] = public.xsssec(find[_key]) + except: + pass + return find + + + ''' + * 取字段值 + * @param String _GET['tab'] 数据库表名 + * @param String _GET['key'] 字段 + * @param String _GET['id'] 条件ID + * @return String + ''' + def getKey(self,get): + + # 校验参数 + try: + get.validate([ + Param('table').Require().String().Xss(), + Param('key').Require().String().Xss(), + Param('id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + tableName = get.table + keyName = get.key + id = get.id + SQL = db.Sql().table(tableName) + where = "id=?" + retuls = SQL.where(where,(id,)).getField(keyName) + res = public.xsssec(retuls) + if type(res)==dict: + if res.get("message", None): + res = res.get("message") + if res.get("msg", None): + res = res.get("msg") + # return public.xsssec(retuls) + return public.return_message(0, 0, res) + + + ''' + * 获取数据与分页 + * @param string table 表 + * @param string where 查询条件 + * @param int limit 每页行数 + * @param mixed result 定义分页数据结构 + * @return array + ''' + def GetSql(self,get,result = '1,2,3,4,5,8'): + #判断前端是否传入参数 + order = 'id desc' + if hasattr(get,'order'): + # 验证参数格式 + if re.match(r"^[\w\s\-\.]+$",get.order): + order = get.order + + search_key = 'get_list' + limit = 20 + if hasattr(get,'limit'): + limit = int(get.limit) + if limit < 1: limit = 20 + + if hasattr(get,'result'): + # 验证参数格式 + if re.match(r"^[\d\,]+$",get.result): + result = get.result + + SQL = db.Sql() + data = {} + #取查询条件 + where = '' + search = '' + param = () + if hasattr(get,'search'): + search = get.search + if sys.version_info[0] == 2: get.search = get.search.encode('utf-8') + where,param = self.GetWhere(get.table,get.search) + if get.table == 'backup': + where += " and type='{}'".format(int(get.type)) + + if get.table == 'sites' and get.search: + conditions = '' + if '_' in get.search: + cs = '' + for i in get.search: + if i == '_': + cs += '/_' + else: + cs += i + get.search = cs + conditions = " escape '/'" + pid = SQL.table('domain').where("name LIKE ?{}".format(conditions),("%{}%".format(get.search),)).getField('pid') + if pid: + if where: + where += " or id=" + str(pid) + else: + where += "id=" + str(pid) + + if get.table == 'sites': + search_key = 'php' + + # 额外对 project_type 字段做处理 + if 'project_type' in get: + extra_where = "`project_type` = '{}'".format(get.project_type) + if where: + where = r"({}) AND {}".format(where, extra_where) + else: + where = extra_where + else: + extra_where = "`project_type` IN ('PHP', 'WP')" + if where: + where = r"({}) AND {}".format(where, extra_where) + else: + where = extra_where + + if hasattr(get,'type'): + if get.type != '-1': + where += " AND type_id={}".format(int(get.type)) + + if get.table == 'databases': + if hasattr(get,'db_type'): + if where: + where += " AND db_type='{}'".format(int(get.db_type)) + else: + where = "db_type='{}'".format(int(get.db_type)) + if hasattr(get,'sid'): + if where: + where += " AND sid='{}'".format(int(get.sid)) + else: + where = "sid='{}'".format(int(get.sid)) + + if where: + where += " and type='MySQL'" + else: + where = 'type = "MySQL"' + + field = self.GetField(get.table) + #实例化数据库对象 + + public.set_search_history(get.table,search_key,search) #记录搜索历史 + + #是否直接返回所有列表 + if hasattr(get,'list'): + data = SQL.table(get.table).where(where,param).field(field).order(order).select() + return data + + #取总行数 + count = SQL.table(get.table).where(where,param).count() + #get.uri = get + #包含分页类 + import page + #实例化分页类 + page = page.Page() + + info = {} + info['count'] = count + info['row'] = limit + + info['p'] = 1 + if hasattr(get,'p'): + info['p'] = int(get['p']) + if info['p'] <1: info['p'] = 1 + + try: + from flask import request + info['uri'] = public.url_encode(request.full_path) + except: + info['uri'] = '' + info['return_js'] = '' + if hasattr(get,'tojs'): + if re.match(r"^[\w\.\-]+$",get.tojs): + info['return_js'] = get.tojs + + data['where'] = where + + #获取分页数据 + data['page'] = page.GetPage(info,result) + #取出数据 + #data['data'] = SQL.table(get.table).where(where,param).order(order).field(field).limit(str(page.SHIFT)+','+str(page.ROW)).select() + + o_list = order.split(' ') + if o_list[0] in self.__SORT_DATA: + data['data'] = SQL.table(get.table).where(where,param).field(field).select() + data['plist'] = {'shift':page.SHIFT,'row':page.ROW,'order':order} + else: + data['data'] = SQL.table(get.table).where(where,param).order(order).field(field).limit(str(page.SHIFT)+','+str(page.ROW)).select() #取出数据 + + data['search_history'] = public.get_search_history(get.table,search_key) + + return data + + #获取条件 + def GetWhere(self,tableName,search): + if not search: return "",() + + if type(search) == bytes: search = search.encode('utf-8').strip() + try: + search = re.search(r"[\w\x80-\xff\.\_\-]+",search).group() + except: + return '',() + conditions = '' + if '_' in search: + cs = '' + for i in search: + if i == '_': + cs += '/_' + else: + cs += i + search = cs + conditions = " escape '/'" + wheres = { + 'sites': ("name LIKE ? OR ps LIKE ?{}".format(conditions), ('%' + search + '%', '%' + search + '%')), + 'ftps': ("name LIKE ? OR ps LIKE ?{}".format(conditions), ('%' + search + '%', '%' + search + '%')), + 'databases': ( + "(name LIKE ? {} OR ps LIKE ?{})".format(conditions, conditions), + ("%" + search + "%", "%" + search + "%")), + 'crontab': ("name LIKE ?{}".format(conditions), ('%' + (search) + '%')), + 'logs': ("username=? OR type LIKE ?{} OR log LIKE ?{}".format(conditions, conditions), + (search, '%' + search + '%', '%' + search + '%')), + 'backup' : ("pid=?",(search,)), + 'users' : ("id='?' OR username=?",(search,search)), + 'domain' : ("pid=? OR name=?",(search,search)), + 'tasks' : ("status=? OR type=?",(search,search)), + } + + # wheres = { + # 'sites' : ("name LIKE ? OR ps LIKE ?",('%'+search+'%','%'+search+'%')), + # 'ftps' : ("name LIKE ? OR ps LIKE ?",('%'+search+'%','%'+search+'%')), + # 'databases' : ("(name LIKE ? OR ps LIKE ?)",("%"+search+"%","%"+search+"%")), + # 'logs' : ("username=? OR type LIKE ? OR log LIKE ?",(search,'%'+search+'%','%'+search+'%')), + # 'backup' : ("pid=?",(search,)), + # 'users' : ("id='?' OR username=?",(search,search)), + # 'domain' : ("pid=? OR name=?",(search,search)), + # 'tasks' : ("status=? OR type=?",(search,search)), + # } + + try: + return wheres[tableName] + except: + return '',() + + # 获取返回的字段 + def GetField(self,tableName): + fields = { + 'sites' : "id,name,path,status,ps,addtime,edate", + 'ftps' : "id,pid,name,password,status,ps,addtime,path", + 'databases' : "id,sid,pid,name,username,password,accept,ps,addtime,db_type,conn_config", + 'logs' : "id,uid,username,type,log,addtime", + 'backup' : "id,pid,name,filename,addtime,size,ps", + 'users' : "id,username,phone,email,login_ip,login_time", + 'firewall' : "id,port,ps,addtime", + 'domain' : "id,pid,name,port,addtime", + 'tasks' : "id,name,type,status,addtime,start,end" + } + try: + return fields[tableName] + except: + return '' + + def get_analysis(self,get,i): + import log_analysis + get.path = '/www/wwwlogs/{}.log'.format(i['name']) + get.action = 'get_result' + data = log_analysis.log_analysis().get_result(get) + return int(data['php']) + int(data['san']) + int(data['sql']) + int(data['xss']) diff --git a/class_v2/databaseModelV2/__init__.py b/class_v2/databaseModelV2/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/class_v2/databaseModelV2/__init__.py @@ -0,0 +1 @@ + diff --git a/class_v2/databaseModelV2/base.py b/class_v2/databaseModelV2/base.py new file mode 100644 index 00000000..ded0dd21 --- /dev/null +++ b/class_v2/databaseModelV2/base.py @@ -0,0 +1,542 @@ +# coding: utf-8 +import os, sys, time, json + +panelPath = '/www/server/panel' +os.chdir(panelPath) +if not panelPath + "/class/" in sys.path: + sys.path.insert(0, panelPath + "/class/") +import public, re + + +class databaseBase: + + def get_base_list(self, args, sql_type='mysql'): + """ + @获取数据库列表 + @type:数据库类型,MySQL,SQLServer + """ + + search = '' + if 'search' in args: search = args['search'] + + SQL = public.M('databases'); + + where = "lower(type) = lower('{}')".format(sql_type) + if search: + where += "AND (name like '%{search}%' or ps like '%{search}%')".format(search=search) + + if 'db_type' in args: + where += " AND db_type='{}'".format(args['db_type']) + + if 'sid' in args: + where += " AND sid='{}'".format(args['sid']) + + order = "id desc" + if hasattr(args, 'order'): order = args.order + + info = {} + rdata = {} + + info['p'] = 1 + info['row'] = 20 + result = '1,2,3,4,5,8' + info['count'] = SQL.where(where, ()).count(); + + if hasattr(args, 'limit'): info['row'] = int(args.limit) + if hasattr(args, 'result'): result = args.result; + if hasattr(args, 'p'): info['p'] = int(args['p']) + + import page + # 实例化分页类 + page = page.Page(); + + info['uri'] = args + info['return_js'] = '' + if hasattr(args, 'tojs'): info['return_js'] = args.tojs + + rdata['where'] = where; + + # 获取分页数据 + rdata['page'] = page.GetPage(info, result) + # 取出数据 + rdata['data'] = SQL.where(where, ()).order(order).field( + 'id,sid,pid,name,username,password,accept,ps,addtime,type,db_type,conn_config').limit( + str(page.SHIFT) + ',' + str(page.ROW)).select() + + for sdata in rdata['data']: + sdata['backup_count'] = public.M('backup').where("pid=? AND type=1", (sdata['id'])).count() + + sdata['conn_config'] = json.loads(sdata['conn_config']) + return rdata; + + def get_databaseModel(self): + ''' + 获取数据库模型对象 + @db_type 数据库类型 + ''' + # from panelDatabaseController import DatabaseController + from panelDatabaseControllerV2 import DatabaseController + + project_obj = DatabaseController() + + return project_obj + + def get_average_num(self, slist): + """ + @批量删除获取平均值 + """ + count = len(slist) + limit_size = 1 * 1024 * 1024 + if count <= 0: return limit_size + + if len(slist) > 1: + slist = sorted(slist) + limit_size = int((slist[0] + slist[-1]) / 2 * 0.85) + return limit_size + + def get_database_size(self, ids, is_pid=False): + """ + 获取数据库大小 + """ + result = {} + p = self.get_databaseModel() + for id in ids: + if not is_pid: + x = public.M('databases').where('id=?', id).field('id,sid,pid,name,type,ps,addtime').find() + else: + x = public.M('databases').where('pid=?', id).field('id,sid,pid,name,type,ps,addtime').find() + if not x: continue + x['backup_count'] = public.M('backup').where("pid=? AND type=?", (x['id'], '1')).count() + if x['type'] == 'MySQL': + x['total'] = int(public.get_database_size_by_id(id)) + else: + try: + + get = public.dict_obj() + get['data'] = {'db_id': x['id']} + get['mod_name'] = x['type'].lower() + get['def_name'] = 'get_database_size_by_id' + + x['total'] = p.model(get) + except: + x['total'] = 0 + result[x['name']] = x + return result + + def check_base_del_data(self, get): + """ + @删除数据库前置检测 + """ + ids = json.loads(get.ids) + slist = {}; + result = []; + db_list_size = [] + db_data = self.get_database_size(ids) + + for key in db_data: + data = db_data[key] + if not data['id'] in ids: continue + + db_addtime = public.to_date(times=data['addtime']) + data['score'] = int(time.time() - db_addtime) + data['total'] + data['st_time'] = db_addtime + + if data['total'] > 0: db_list_size.append(data['total']) + result.append(data) + + slist['data'] = sorted(result, key=lambda x: x['score'], reverse=True) + slist['db_size'] = self.get_average_num(db_list_size) + return slist + + def get_test(self, args): + + p = self.get_databaseModel() + get = public.dict_obj() + get['data'] = {'db_id': 18} + get['mod_name'] = args['type'].lower() + get['def_name'] = 'get_database_size_by_id' + + return p.model(get) + + def add_base_database(self, get): + """ + @添加数据库前置检测 + @return username 用户名 + data_name 数据库名 + data_pwd:数据库密码 + """ + data_name = get['name'].strip().lower() + if self.check_recyclebin(data_name): + return public.returnMsg(False, + 'Database [' + data_name + '] is already in recycle bin, please restore from recycle bin!'); + + if len(data_name) > 16: + return public.returnMsg(False, 'DATABASE_NAME_LEN') + + if not hasattr(get, 'db_user'): get.db_user = data_name; + username = get.db_user.strip(); + checks = ['root', 'mysql', 'test', 'sys', 'panel_logs'] + if username in checks or len(username) < 1: + return public.returnMsg(False, 'Database username is invalid!'); + if data_name in checks or len(data_name) < 1: + return public.returnMsg(False, 'Database name is invalid!'); + + reg = r"^\w+$" + if not re.match(reg, data_name): + return public.returnMsg(False, 'DATABASE_NAME_ERR_T') + + data_pwd = get['password'] + if len(data_pwd) < 1: + data_pwd = public.md5(str(time.time()))[0:8] + + if public.M('databases').where("name=? or username=?", (data_name, username)).count(): + return public.returnMsg(False, 'DATABASE_NAME_EXISTS') + + res = { + 'data_name': data_name, + 'username': username, + 'data_pwd': data_pwd, + 'status': True + } + return res + + def delete_base_backup(self, get): + """ + @删除备份文件 + """ + + name = '' + id = get.id + where = "id=?" + filename = public.M('backup').where(where, (id,)).getField('filename') + if os.path.exists(filename): os.remove(filename) + + if filename == 'qiniu': + name = public.M('backup').where(where, (id,)).getField('name'); + + public.ExecShell(public.get_run_python("[PYTHON] " + public.GetConfigValue( + 'setup_path') + '/panel/script/backup_qiniu.py delete_file ' + name)) + public.M('backup').where(where, (id,)).delete() + public.WriteLog("TYPE_DATABASE", 'DATABASE_BACKUP_DEL_SUCCESS', (name, filename)) + public.return_message(0, 0, 'DEL_SUCCESS') + + # 检查是否在回收站 + def check_recyclebin(self, name): + try: + for n in os.listdir('{}/Recycle_bin'.format(public.get_soft_path())): + if n.find('BTDB_' + name + '_t_') != -1: return True; + return False; + except: + return False; + + # map to list + def map_to_list(self, map_obj): + try: + if type(map_obj) != list and type(map_obj) != str: map_obj = list(map_obj) + return map_obj + except: + return [] + + # ******************************************** 远程数据库 ******************************************/ + + def check_cloud_args(self, get, nlist=[]): + """ + 验证参数是否合法 + @get param + @args 参数列表 + """ + for key in nlist: + if not key in get: + return public.return_message(-1, 0, 'Parameter passing error, missing parameter {}!'.format(key)) + # return public.returnMsg(False, 'Parameter passing error, missing parameter {}!'.format(key)) + return True + + def check_cloud_database(self, args): + ''' + @检查远程数据库是否存在 + @conn_config param + ''' + + p = self.get_databaseModel() + + get = public.dict_obj() + # get['db_name'] = 'localhost' + get['data'] = args + get['mod_name'] = args['type'] + get['def_name'] = 'check_cloud_database_status' + # public.print_log("-------------------进入检测远程数据库是否存在: {}".format(p.model(get))) + return p.model(get) + + def AddBaseCloudServer(self, get): + """ + @name 添加远程服务器 + @author hwliang<2021-01-10> + @param db_host 服务器地址 + @param db_port 数据库端口 + @param db_user 用户名 + @param db_password 数据库密码 + @param db_ps 数据库备注 + @param type 数据库类型,mysql/sqlserver/sqlite + @return dict + """ + # mongodb {"db_host":"192.168.168.12","db_port":"27017","db_user":"root","db_password":"8thA5dgB8lr5ACfx","db_ps":"cecee","type":"mongodb"} + # sqlserver {"db_host":"192.168.1.23","db_port":"1433","db_user":"sa","db_password":"MfyDytnjXBTD8e6x","db_ps":"666","type":"sqlserver"} + + + arrs = ['db_host', 'db_port', 'db_user', 'db_password', 'db_ps', 'type'] + if get.type == 'redis': + arrs = ['db_host', 'db_port', 'db_password', 'db_ps', 'type'] + # try: + cRet = self.check_cloud_args(get, arrs) + if isinstance(cRet, dict): + return cRet + + # try: + get['db_name'] = None + try: + res = self.check_cloud_database(get) + except BaseException as ex: + # public.print_log("获取远程数据库状态00: {}".format(ex)) + return public.return_message(-1, 0, 'Database connection failed') + + + # # mongodb 远程检测有问题 暂时跳过检测 + # if get.type != 'mongodb': + # if res['message'].get('result', '') == '' or res['message'].get('result', '') == False: + # return public.return_message(-1, 0, "The remote database could not be connected") + # {'status': 0, 'timestamp': 1715394490, 'message': AttributeError("'str' object has no attribute 'command'")} + + # 检测数据库连接状态 + try: + if not isinstance(res['message'], dict): + return public.return_message(-1, 0, "The remote database could not be connected") + if res['message'].get('result', '') == '' or res['message'].get('result', '') == False: + return public.return_message(-1, 0, "The remote database could not be connected") + if res['status'] == -1: + return public.return_message(-1, 0, "The remote database could not be connected") + except Exception as e: + # public.print_log("获取远程数据库状态22: {}".format(e)) + return public.return_message(-1, 0, "The remote database could not be connected") + + if public.M('database_servers').where('db_host=? AND db_port=?', (get['db_host'], get['db_port'])).count(): + return public.return_message(-1, 0, 'The specified server already exists: [{}:{}]'.format(get['db_host'], + get['db_port'])) + get['db_port'] = int(get['db_port']) + pdata = { + 'db_host': get['db_host'], + 'db_port': int(get['db_port']), + 'db_user': get['db_user'], + 'db_password': get['db_password'], + 'db_type': get['type'], + 'ps': public.xssencode2(get['db_ps'].strip()), + 'addtime': int(time.time()) + } + result = public.M("database_servers").insert(pdata) + + if isinstance(result, int): + public.WriteLog('Database manager', 'Add remote MySQL server[{}:{}]'.format(get['db_host'], get['db_port'])) + # return public.returnMsg(True,'Added successfully!') + return public.return_message(0, 0, 'Added successfully!') + # return public.returnMsg(False,'Add failed: {}'.format(result)) + return public.return_message(0, 0, 'Add failed: {}'.format(result)) + # except Exception as ex: + # public.print_log("error info777: {}".format(ex)) + # return public.return_message(-1, 0, str(ex)) + + def GetBaseCloudServer(self, get): + ''' + @name 获取远程服务器列表 + @author hwliang<2021-01-10> + @return list + ''' + where = '1=1' + if 'type' in get: where = "db_type = '{}'".format(get['type']) + + data = public.M('database_servers').where(where, ()).select() + + if not isinstance(data, list): data = [] + + if get['type'] == 'mysql': + bt_mysql_bin = public.get_mysql_info()['path'] + '/bin/mysql.exe' + if os.path.exists(bt_mysql_bin): + data.insert(0, {'id': 0, 'db_host': '127.0.0.1', 'db_port': 3306, 'db_user': 'root', 'db_password': '', + 'ps': 'local server', 'addtime': 0, 'db_type': 'mysql'}) + elif get['type'] == 'sqlserver': + pass + elif get['type'] == 'mongodb': + if os.path.exists('/www/server/mongodb/bin'): + data.insert(0, {'id': 0, 'db_host': '127.0.0.1', 'db_port': 27017, 'db_user': 'root', 'db_password': '', + 'ps': 'local server', 'addtime': 0, 'db_type': 'mongodb'}) + elif get['type'] == 'redis': + if os.path.exists('/www/server/redis'): + data.insert(0, {'id': 0, 'db_host': '127.0.0.1', 'db_port': 6379, 'db_user': 'root', 'db_password': '', + 'ps': 'local server', 'addtime': 0, 'db_type': 'redis'}) + elif get['type'] == 'pgsql': + if os.path.exists('/www/server/pgsql'): + data.insert(0, + {'id': 0, 'db_host': '127.0.0.1', 'db_port': 5432, 'db_user': 'postgres', 'db_password': '', + 'ps': 'local server', 'addtime': 0, 'db_type': 'pgsql'}) + return data + + def RemoveBaseCloudServer(self, get): + ''' + @name 删除远程服务器 + @author hwliang<2021-01-10> + @param id 远程服务器ID + @return dict + ''' + + id = int(get.id) + if not id: + return public.return_message(-1, 0, 'Parameter passed error, please try again!') + db_find = public.M('database_servers').where('id=?', (id,)).find() + if not db_find: + return public.return_message(-1, 0, 'The specified remote server does not exist!') + public.M('databases').where('sid=?', id).delete() + result = public.M('database_servers').where('id=?', id).delete() + if isinstance(result, int): + public.WriteLog('Database manager', + 'Delete remote MySQL server [{}:{}]'.format(db_find['db_host'], int(db_find['db_port']))) + return public.return_message(0, 0, 'Successfully deleted!') + return public.return_message(0, 0, 'Successfully deleted: {}'.format(result)) + + def ModifyBaseCloudServer(self, get): + ''' + @name 修改远程服务器 + @author hwliang<2021-01-10> + @param id 远程服务器ID + @param db_host 服务器地址 + @param db_port 数据库端口 + @param db_user 用户名 + @param db_password 数据库密码 + @param db_ps 数据库备注 + @return dict + ''' + + arrs = ['db_host', 'db_port', 'db_user', 'db_password', 'db_ps', 'type'] + if get.type == 'redis': + arrs = ['db_host', 'db_port', 'db_password', 'db_ps', 'type'] + + cRet = self.check_cloud_args(get, arrs) + if isinstance(cRet, dict): + return cRet + # if not cRet['status']: + # return public.return_message(-1, 0,) + # return cRet + get['db_name'] = None + id = int(get.id) + get['db_port'] = int(get['db_port']) + db_find = public.M('database_servers').where('id=?', (id,)).find() + if not db_find: + return public.return_message(-1, 0, 'The specified remote server does not exist!') + _modify = False + if db_find['db_host'] != get['db_host'] or db_find['db_port'] != get['db_port']: + _modify = True + if public.M('database_servers').where('db_host=? AND db_port=?', (get['db_host'], get['db_port'])).count(): + return public.return_message(-1, 0, + 'The specified server already exists: [{}:{}]'.format(get['db_host'], + get['db_port'])) + + if db_find['db_user'] != get['db_user'] or db_find['db_password'] != get['db_password']: + _modify = True + _modify = True + + if _modify: + try: + res = self.check_cloud_database(get) + except BaseException as ex: + # public.print_log("获取远程数据库链接状态报错: {}".format(ex)) + return public.return_message(-1, 0, 'Database connection failed') + + if res['message'].get('result', '') == '' or res['message'].get('result', '') == False: + return public.return_message(-1, 0, "The remote database could not be connected") + + pdata = { + 'db_host': get['db_host'], + 'db_port': int(get['db_port']), + 'db_user': get['db_user'], + 'db_password': get['db_password'], + 'db_type': get['type'], + 'ps': public.xssencode2(get['db_ps'].strip()) + } + + result = public.M("database_servers").where('id=?', (id,)).update(pdata) + if isinstance(result, int): + public.WriteLog('Database manager', + 'Modify the remote MySQL server[{}:{}]'.format(get['db_host'], get['db_port'])) + + return public.return_message(0, 0, 'Successfully modified!') + return public.return_message(-1, 0, 'Fail to edit: {}'.format(result)) + + # 检测数据库执行错误 + def IsSqlError(self, mysqlMsg): + + if mysqlMsg: + mysqlMsg = str(mysqlMsg) + if "MySQLdb" in mysqlMsg: + return public.return_message(-1, 0, 'DATABASE_ERR_MYSQLDB') + if "2002," in mysqlMsg: + return public.return_message(-1, 0, 'DATABASE_ERR_CONNECT') + if "2003," in mysqlMsg: + return public.return_message(-1, 0, + 'Database connection timed out, please check if the configuration is correct.') + if "1045," in mysqlMsg: + return public.return_message(-1, 0, 'MySQL password error.') + if "1040," in mysqlMsg: + return public.return_message(-1, 0, + 'Exceeded maximum number of connections, please try again later.') + if "1130," in mysqlMsg: + return public.return_message(-1, 0, + 'Database connection failed, please check whether the root user is authorized to access 127.0.0.1.') + if "using password:" in mysqlMsg: + return public.return_message(-1, 0, 'DATABASE_ERR_PASS') + if "Connection refused" in mysqlMsg: + return public.return_message(-1, 0, 'DATABASE_ERR_CONNECT') + if "1133" in mysqlMsg: + return public.return_message(-1, 0, 'DATABASE_ERR_NOT_EXISTS') + if "2005_login_error" == mysqlMsg: + return public.return_message(-1, 0, + 'The connection times out, please manually enable the TCP/IP function (Start Menu->SQL 2005->Configuration Tools->2005 Network Configuration->TCP/IP->Enable)') + if 'already exists' in mysqlMsg: + return public.return_message(-1, 0, + 'The specified database already exists, please do not add it repeatedly.') + if 'Cannot open backup device' in mysqlMsg: + return public.return_message(-1, 0, + 'The operation failed, the remote database does not support the operation.') + + if '1142' in mysqlMsg: + return public.return_message(-1, 0, 'Insufficient permissions, please use root user.') + + if "DB-Lib error message 20018" in mysqlMsg: + return public.return_message(-1, 0, + 'Create failed, SQL Server requires GUI support') + + return None + + # ******************************************** 数据库公用方法 ******************************************/ + + +if __name__ == "__main__": + # get = {} + # get['db_host'] = '192.168.1.37' + # get['db_port'] = '3306' + # get['db_user'] = 'root' + + # get['db_password'] = 'HLANEMJFRbPE7Ny2' + # get['db_ps'] = '2' + # get['type'] = 'mysql' + # bt = databaseBase() + # ret = bt.AddCloudServer(get) + # print(ret) + + get = {} + get['db_host'] = '192.168.66.73' + get['db_port'] = '1433' + get['db_user'] = 'sa' + + get['db_password'] = 'dPYi6Gt8GC7SL58C' + get['db_ps'] = '2' + get['type'] = 'sqlserver' + bt = databaseBase() + ret = bt.get_test(get) + print(ret) diff --git a/class_v2/databaseModelV2/mongodbModel.py b/class_v2/databaseModelV2/mongodbModel.py new file mode 100644 index 00000000..5edc43a9 --- /dev/null +++ b/class_v2/databaseModelV2/mongodbModel.py @@ -0,0 +1,933 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- +#角色说明: +#read:允许用户读取指定数据库 +#readWrite:允许用户读写指定数据库 +#dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profile +#userAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户 +#clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限。 +#readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限 +#readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限 +#userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限 +#dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限。 +#root:只在admin数据库中可用。超级账号,超级权限 + +# sqlite模型 +#------------------------------ +import os,re,json,time +from databaseModelV2.base import databaseBase +import public +from public.validate import Param +try: + import pymongo +except: + public.ExecShell("btpip install pymongo") + import pymongo +try: + from BTPanel import session +except :pass + + +class panelMongoDB(): + + __DB_PASS = None + __DB_USER = None + __DB_PORT = 27017 + __DB_HOST = '127.0.0.1' + __DB_CONN = None + __DB_ERR = None + + __DB_CLOUD = None + def __init__(self): + self.__config = self.get_options(None) + + def __Conn(self,auth): + + if not self.__DB_CLOUD: + path = '{}/data/mongo.root'.format(public.get_panel_path()) + if os.path.exists(path): self.__DB_PASS = public.readFile(path) + self.__DB_PORT = int(self.__config['port']) + + try: + if not self.__DB_USER and auth: + self.__DB_USER = "root" + self.__DB_CONN = pymongo.MongoClient(host=self.__DB_HOST, port=self.__DB_PORT, username = self.__DB_USER, password=self.__DB_PASS) + self.__DB_CONN.admin.command({"listDatabases":1}) + return True + except : + try: + self.__DB_CONN = pymongo.MongoClient(host=self.__DB_HOST, port=self.__DB_PORT, username = self.__DB_USER, password=self.__DB_PASS) + self.__DB_CONN.admin.authenticate('root', self.__DB_PASS) + return True + except : + self.__DB_ERR = public.get_error_info() + return False + + + def get_db_obj(self,db_name = 'admin',auth=0): + """ + @获取连接对象 + """ + if not self.__Conn(auth): return self.__DB_ERR + + return self.__DB_CONN[db_name] + + def set_host(self,host,port,name,username,password,prefix = ''): + self.__DB_HOST = host + self.__DB_PORT = int(port) + self.__DB_NAME = name + if self.__DB_NAME: self.__DB_NAME = str(self.__DB_NAME) + self.__DB_USER = str(username) + self._USER = str(username) + self.__DB_PASS = str(password) + self.__DB_PREFIX = prefix + self.__DB_CLOUD = 1 + return self + + + + #获取配置文件 + def get_config(self,get): + filename = '{}/mongodb/config.conf'.format(public.get_setup_path()) + if os.path.exists(filename): + return public.readFile(filename) + return "" + + #获取配置项 + def get_options(self,get): + options = ['port','bind_ip','logpath','dbpath','authorization'] + data = {} + conf = self.get_config(None) + + for opt in options: + tmp = re.findall(opt + r":\s+(.+)",conf) + if not tmp: continue; + data[opt] = tmp[0] + + if not 'authorization' in data:data['authorization'] = "disabled" + + # public.writeFile('/www/server/1.txt',json.dumps(data)) + return data + + +class main(databaseBase): + + __conf_path = '{}/mongodb/config.conf'.format(public.get_setup_path()) + def __init__(self): + pass + + + def get_list(self,args): + """ + @获取数据库列表 + @sql_type = sqlserver + """ + # {"table": "databases", "search": "", "limit": 20, "p": 1} + # 校验参数 + try: + args.validate([ + Param('table').Require().String().Xss(), + Param('search').String(), + Param('order').String().Xss(), + Param('limit').Integer(), + Param('p').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # return self.get_base_list(args, sql_type = 'mongodb') + return public.return_message(0, 0, self.get_base_list(args, sql_type = 'mongodb')) + + def GetCloudServer(self,args): + ''' + @name 获取远程服务器列表 + @author hwliang<2021-01-10> + @return list + ''' + # return self.GetBaseCloudServer(args) + return public.return_message(0, 0, self.GetBaseCloudServer(args)) + + + def AddCloudServer(self,args): + ''' + @添加远程数据库 + ''' + return self.AddBaseCloudServer(args) + # return public.return_message(0, 0, self.AddBaseCloudServer(args)) + + def RemoveCloudServer(self,args): + ''' + @删除远程数据库 + ''' + return self.RemoveBaseCloudServer(args) + # return public.return_message(0, 0, self.RemoveBaseCloudServer(args)) + + def ModifyCloudServer(self,args): + ''' + @修改远程数据库 + ''' + return self.ModifyBaseCloudServer(args) + # return public.return_message(0, 0, self.ModifyBaseCloudServer(args)) + + #获取数据库列表 + def exists_databases(self,get): + db_name = get + if type(get) != str:db_name = get['db_name'] + auth_status = self.get_local_auth(get) + db_obj = self.get_obj_by_sid(self.sid).get_db_obj('admin',auth=auth_status) + data = db_obj.command({"listDatabases":1}) + if 'databases' in data: + for x in data['databases']: + if x['name'] == db_name: + return True + return False + + def __set_auth_open(self,status): + """ + @设置数据库密码访问开关 + @状态 status:1 开启,2:关闭 + """ + + conf = public.readFile(self.__conf_path) + if status: + conf = re.sub(r'authorization\s*\:\s*disabled','authorization: enabled',conf) + else: + conf = re.sub(r'authorization\s*\:\s*enabled','authorization: disabled',conf) + + public.writeFile(self.__conf_path,conf) + self.restart_services() + + return True + + + def set_auth_status(self,get): + """ + @设置密码认证状态 + @status int 0:关闭,1:开启 + """ + + if not public.process_exists("mongod"): + # return public.returnMsg(False,"Mongodb service has not been started yet!") + return public.return_message(-1, 0, "Mongodb service has not been started yet!") + + status = int(get.status) + path = '{}/data/mongo.root'.format(public.get_panel_path()) + if status: + if hasattr(get,'password'): + password = get['password'].strip() + if not password or not re.search(r"^[\w@\.]+$", password): + # return public.return_msg_gettext(False, 'Database password cannot be empty or have special characters!') + return public.return_message(-1, 0, 'Database password cannot be empty or have special characters!') + + # if re.search('[\u4e00-\u9fa5]',password): + # return public.returnMsg(False,'Database password cannot be Chinese, please change the name!') + else: + password = public.GetRandomString(16) + self.__set_auth_open(0) + + _client = panelMongoDB().get_db_obj('admin') + try: + _client.command("dropUser", "root") + except : pass + + _client.command("createUser", "root", pwd=password, roles=[ + {'role':'root','db':'admin'}, + {'role':'clusterAdmin','db':'admin'}, + {'role':'readAnyDatabase','db':'admin'}, + {'role':'readWriteAnyDatabase','db':'admin'}, + {'role':'userAdminAnyDatabase','db':'admin'}, + {'role':'dbAdminAnyDatabase','db':'admin'}, + {'role':'userAdmin','db':'admin'}, + {'role':'dbAdmin','db':'admin'} + ]) + + self.__set_auth_open(1) + + public.writeFile(path,password) + else: + if os.path.exists(path): os.remove(path) + self.__set_auth_open(0) + + # return public.return_msg_gettext(True,'Setup successfully!') + return public.return_message(0, 0, 'Setup successfully!') + + def restart_services(self): + """ + @重启服务 + """ + public.ExecShell('/etc/init.d/mongodb restart') + return True + + def get_obj_by_sid(self,sid = 0,conn_config = None): + """ + @取mssql数据库对像 By sid + @sid 数据库分类,0:本地 + """ + if type(sid) == str: + try: + sid = int(sid) + except :sid = 0 + + if sid: + if not conn_config: conn_config = public.M('database_servers').where("id=?" ,sid).find() + db_obj = panelMongoDB() + + try: + db_obj = db_obj.set_host(conn_config['db_host'],conn_config['db_port'],None,conn_config['db_user'],conn_config['db_password']) + except Exception as e: + raise public.PanelError(e) + else: + db_obj = panelMongoDB() + return db_obj + + + + def get_local_auth(self,get): + """ + @验证本地数据库是否需要密码 + """ + self.sid = get.get('sid/d',0) + if self.sid != 0: return True + + conf = panelMongoDB().get_options(None) + if conf['authorization'] == 'enabled': + return True + return False + + def AddDatabase(self,args): + """ + @添加数据库 + """ + + # 校验参数 + try: + args.validate([ + Param('sid').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # try: + # int(args.sid) + # except: + # return public.returnMsg(False, 'Database type sid needs int type!') + + + if not int(args.sid) and not public.process_exists("mongod"): + # return public.returnMsg(False,"Mongodb service has not been started yet!") + return public.return_message(-1, 0, "Mongodb service has not been started yet!") + + # try: + username = '' + password = '' + auth_status = self.get_local_auth(args) #auth为true时如果__DB_USER为空则将它赋值为 root,用于开启本地认证后数据库用户为空的情况 + data_name = args.name.strip() + # 检测重复添加 + find = public.M('databases').where("name=?", (data_name,)).find() + if find: + return public.return_message(-1, 0, 'Cannot be added repeatedly') + if not data_name: + # return public.returnMsg(False, "Database name cannot be empty!") + return public.return_message(-1, 0, "Database name cannot be empty!") + if auth_status: + res = self.add_base_database(args) + if not res['status']: + # return res + return public.return_message(-1, 0, res['msg']) + + data_name = res['data_name'] + username = res['username'] + password = res['data_pwd'] + else: + username = data_name + db_obj = self.get_obj_by_sid(self.sid).get_db_obj(data_name,auth=auth_status) + dtype = 'MongoDB' + if not hasattr(args,'ps'): args['ps'] = public.getMsg('INPUT_PS'); + addTime = time.strftime('%Y-%m-%d %X',time.localtime()) + + pid = 0 + if hasattr(args,'pid'): pid = args.pid + + if hasattr(args,'contact'): + site = public.M('sites').where("id=?",(args.contact,)).field('id,name').find() + if site: + pid = int(args.contact) + args['ps'] = site['name'] + + db_type = 0 + if self.sid: db_type = 2 + + db_obj.chat.insert_one({}) + if auth_status: + db_obj.command("createUser", username, pwd=password, roles=[{'role':'dbOwner','db':data_name},{'role':'userAdmin','db':data_name}]) + + public.set_module_logs('linux_mongodb','AddDatabase',1) + + #添加入SQLITE + public.M('databases').add('pid,sid,db_type,name,username,password,accept,ps,addtime,type',(pid,self.sid,db_type,data_name,username,password,'127.0.0.1',args['ps'],addTime,dtype)) + public.WriteLog("TYPE_DATABASE", 'DATABASE_ADD_SUCCESS',(data_name,)) + # return public.returnMsg(True,'ADD_SUCCESS') + return public.return_message(0, 0, 'ADD_SUCCESS') + # except Exception as ex: + # public.print_log("error info66: {}".format(ex)) + # return public.return_message(-1, 0, str(ex)) + + def DeleteDatabase(self,args): + """ + @删除数据库 + """ + # 校验参数 + try: + args.validate([ + Param('id').Require().Integer(), + Param('name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = args['id'] + find = public.M('databases').where("id=?",(id,)).field('id,pid,name,username,password,type,accept,ps,addtime,sid,db_type').find() + if not find: + # return public.returnMsg(False,'The specified database does not exist.') + return public.return_message(-1, 0, 'The specified database does not exist.') + + + # try: + # int(find['sid']) + # except: + # return public.returnMsg(False, 'Database type sid needs int type!') + + + if not public.process_exists("mongod") and not int(find['sid']): + # return public.returnMsg(False,"Mongodb service has not been started yet!") + return public.return_message(-1, 0, "Mongodb service has not been started yet!") + + name = args['name'] + username = find['username'] + auth_status = self.get_local_auth(args) + db_obj = self.get_obj_by_sid(find['sid']).get_db_obj(name,auth_status) + try: + db_obj.command("dropUser", username) + except : + pass + + db_obj.command('dropDatabase') + #删除SQLITE + public.M('databases').where("id=?",(id,)).delete() + public.WriteLog("Database manager", 'Successfully deleted!',(name,)) + # return public.returnMsg(True, 'Successfully deleted!') + return public.return_message(0, 0, 'Successfully deleted!') + + + def get_info_by_db_id(self,db_id): + """ + @获取数据库连接详情 + @db_id 数据库id + """ + find = public.M('databases').where("id=?" ,db_id).find() + if not find: return False + + data = { + 'db_host':'127.0.0.1', + 'db_port':int(panelMongoDB().get_options(None)['port']), + 'db_user':find['username'], + 'db_password':find['password'] + } + + if int(find['sid']): + conn_config = public.M('database_servers').where("id=?" ,find['sid']).find() + + data['db_host'] = conn_config['db_host'] + data['db_port'] = int(conn_config['db_port']) + + + return data + + #导入 + def InputSql(self,args): + + # 校验参数 + try: + args.validate([ + Param('file').SafePath(), # 文件路径 + Param('name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + name = args.name + file = args.file + # try: + if not os.path.exists(file): + # return public.returnMsg(False,'The import path does not exist!') + return public.return_message(-1, 0, 'The import path does not exist!') + if not os.path.isfile(file): + # return public.returnMsg(False,'Only importing compressed files is supported!') + return public.return_message(-1, 0, 'Only importing compressed files is supported!') + find = public.M('databases').where("name=? AND LOWER(type)=LOWER('MongoDB')",(name,)).find() + if not find: + # return public.returnMsg(False,'This database was not found!') + return public.return_message(-1, 0, 'This database was not found!') + + get = public.dict_obj() + get.sid = find['sid'] + if not public.process_exists("mongod") and not int(find['sid']): + # return public.returnMsg(False,"Mongodb service has not been started yet!") + return public.return_message(-1, 0, "Mongodb service has not been started yet!") + info = self.get_info_by_db_id(find['id']) + mongorestore_obj = '{}/mongodb/bin/mongorestore'.format(public.get_setup_path()) + mongoimport_obj = '{}/mongodb/bin/mongoimport'.format(public.get_setup_path()) + if not os.path.exists(mongorestore_obj): + # return public.returnMsg(False,'Lack of backup tools, please install MongoDB through [APP Store] first!') + return public.return_message(-1, 0, 'Lack of backup tools, please install MongoDB through [APP Store] first!') + + dir_tmp, file_tmp = os.path.split(file) + split_tmp = file_tmp.split(".") + ext = split_tmp[-1] + + ext_err = ".".join(split_tmp[1:]) + if len(split_tmp[1:]) == 2 and split_tmp[1] not in ['json', 'csv']: + # return public.returnMsg(False, f'.{ext_err} This file format is not currently supported!') + return public.return_message(-1, 0, f'.{ext_err} This file format is not currently supported!') + if ext not in ['json', 'csv', 'gz', 'zip']: + # return public.returnMsg(False, f'.{ext_err} This file format is not currently supported!') + return public.return_message(-1, 0, f'.{ext_err} This file format is not currently supported!') + + tmpFile = ".".join(split_tmp[:-1]) + isgzip = False + if ext != '': # gz zip + if tmpFile == '': + # return public.returnMsg(False, 'FILE_NOT_EXISTS', (tmpFile,)) + return public.return_message(-1, 0, 'FILE_NOT_EXISTS', (tmpFile,)) + isgzip = True + + # 面板默认备份路径 + backupPath = session['config']['backup_path'] + '/database' + input_path = os.path.join(backupPath, tmpFile) + # 备份文件的路径 + input_path2 = os.path.join(dir_tmp, tmpFile) + + if ext == 'zip': # zip + public.ExecShell("cd " + backupPath + " && unzip " + '"' + file + '"') + else: # gz + public.ExecShell("cd " + backupPath + " && tar zxf " + '"' + file + '"') + if not os.path.exists(input_path): + # 兼容从备份文件所在目录恢复 + if not os.path.exists(input_path2): + public.ExecShell("cd " + backupPath + " && gunzip -q " + '"' + file + '"') + else: + input_path = input_path2 + + if not os.path.exists(input_path) and os.path.isfile(input_path2): + input_path = input_path2 + else: + input_path = file + + if os.path.isdir(input_path): # zip,gz,bson + if self.get_local_auth(get): + for temp_file in os.listdir(input_path): + shell = f""" + {mongorestore_obj} \ + --host={info['db_host']} \ + --port={info['db_port']} \ + --db={find['name']} \ + --username={info['db_user']} \ + --password={info['db_password']} \ + --drop \ + {os.path.join(input_path, temp_file)} + """ + public.ExecShell(shell) + else: + for temp_file in os.listdir(input_path): + shell = f""" + {mongorestore_obj} \ + --host={info['db_host']} \ + --port={info['db_port']} \ + --db={find['name']} \ + --drop \ + {os.path.join(input_path, temp_file)} + """ + public.ExecShell(shell) + if isgzip is True: + public.ExecShell("rm -f " + input_path) + else:# json,csv + file_tmp = os.path.basename(input_path) + file_name = file_tmp.split(".")[0] + ext = file_tmp.split(".")[-1] + + if ext not in ["json","csv"]: + # return public.returnMsg(False, 'File format is incorrect!') + return public.return_message(-1, 0, 'File format is incorrect!') + + shell_txt = "" + if ext == "csv": + fp = open(input_path, "r") + fields_list = fp.readline() + fp.close() + shell_txt = f"--fields={fields_list}" + if self.get_local_auth(get): + shell = f""" + {mongoimport_obj} \ + --host={info['db_host']} \ + --port={info['db_port']} \ + --db={find['name']} \ + --username={info['db_user']} \ + --password={info['db_password']} \ + --collection={file_name} \ + --file={input_path} \ + --type={ext} \ + --drop + """ + else: + shell = f""" + {mongoimport_obj} \ + --host={info['db_host']} \ + --port={info['db_port']} \ + --db={find['name']} \ + --collection={file_name} \ + --file={input_path} \ + --type={ext} \ + --drop + """ + shell = f"{shell} {shell_txt}" + public.ExecShell(shell) + public.WriteLog("Database manager", 'Import database [{}] succeeded'.format(name)) + # return public.returnMsg(True, 'Successfully imported database!') + return public.return_message(0, 0, 'Successfully imported database!') + # except Exception as ex: + # public.print_log("error info66: {}".format(ex)) + # return public.return_message(-1, 0, str(ex)) + +####################### + def ToBackup(self,args): + """ + @备份数据库 id 数据库id + """ + + # 校验参数 + try: + args.validate([ + Param('id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = args['id'] + find = public.M('databases').where("id=? AND LOWER(type)=LOWER('MongoDB')",(id,)).find() + if not find: + # return public.returnMsg(False,'The specified database does not exist.') + return public.return_message(-1, 0, 'The specified database does not exist.') + + fileName = f"{find['name']}_mongodb_data_{time.strftime('%Y%m%d_%H%M%S',time.localtime())}" + backupName = session['config']['backup_path'] + '/database/mongodb/' + fileName + + spath = os.path.dirname(backupName) + if not os.path.exists(spath): os.makedirs(spath) + + get = public.dict_obj() + get.sid = find['sid'] + try: + sid = int(find['sid']) + except: + # return public.returnMsg(False, 'Database type sid needs int type!') + return public.return_message(-1, 0, 'Database type sid needs int type!') + + + if not public.process_exists("mongod") and not int(find['sid']): + # return public.returnMsg(False,"Mongodb service has not been started yet!") + return public.return_message(-1, 0, "Mongodb service has not been started yet!") + info = self.get_info_by_db_id(id) + + sql_dump = '{}/mongodb/bin/mongodump'.format(public.get_setup_path()) + if not os.path.exists(sql_dump): + # return public.returnMsg(False,'Lack of backup tools, please install MongoDB through [APP Store] first!') + return public.return_message(-1, 0, 'Lack of backup tools, please install MongoDB through [APP Store] first!') + + if self.get_local_auth(get): + if not info['db_password']: + # return public.returnMsg(False,'Password authentication has been enabled. The password cannot be empty when the database is backed up. Please set a password and try again!') + return public.return_message(-1, 0, 'Password authentication has been enabled. The password cannot be empty when the database is backed up. Please set a password and try again!') + shell = "{} -h {} --port {} -u {} -p {} -d {} -o {} ".format(sql_dump,info['db_host'],info['db_port'],info['db_user'],info['db_password'],find['name'] ,backupName) + else: + shell = "{} -h {} --port {} -d {} -o {} ".format(sql_dump,info['db_host'],info['db_port'],find['name'] ,backupName) + + ret = public.ExecShell(shell) + if not os.path.exists(backupName): + # return public.returnMsg(False,'Database backup failed, file does not exist'); + return public.return_message(-1, 0, 'Database backup failed, file does not exist'); + + + backupFile = f"{backupName}.zip" + public.ExecShell(f"cd {spath} && zip {backupFile} -r {fileName}") + fileName = f"{fileName}.zip" + public.M('backup').add('type,name,pid,filename,size,addtime',(1,fileName,id,backupFile,0,time.strftime('%Y-%m-%d %X',time.localtime()))) + public.WriteLog("TYPE_DATABASE", "DATABASE_BACKUP_SUCCESS",(find['name'],)) + + public.ExecShell(f"rm -rf {backupName}") + if not os.path.exists(backupFile): + # return public.returnMsg(True, 'Backup failed,{}.'.format(ret[0])) + return public.return_message(0, 0, 'Backup failed,{}.'.format(ret[0])) + if os.path.getsize(backupFile) < 1: + # return public.returnMsg(True, 'The backup is executed successfully, the backup file is less than 1b, please check the backup integrity.') + return public.return_message(0, 0, 'The backup is executed successfully, the backup file is less than 1b, please check the backup integrity.') + else: + # return public.returnMsg(True, 'BACKUP_SUCCESS') + return public.return_message(0, 0, 'BACKUP_SUCCESS') + + def DelBackup(self,args): + """ + @删除备份文件 + """ + return self.delete_base_backup(args) + # return public.return_message(0, 0, self.delete_base_backup(args)) + + #同步数据库到服务器 + def SyncToDatabases(self,get): + type = int(get['type']) + n = 0 + sql = public.M('databases') + if type == 0: + data = sql.field('id,name,username,password,accept,type,sid,db_type').where('type=?',('MongoDB',)).select() + + for value in data: + if value['db_type'] in ['1',1]: + continue # 跳过远程数据库 + result = self.ToDataBase(value) + if result == 1: n +=1 + else: + import json + data = json.loads(get.ids) + for value in data: + find = sql.where("id=?",(value,)).field('id,name,username,password,sid,db_type,accept,type').find() + result = self.ToDataBase(find) + if result == 1: n +=1 + if n == 1: + # return public.returnMsg(True, 'Synchronization succeeded') + return public.return_message(0, 0, 'Synchronization succeeded') + elif n == 0: + # return public.returnMsg(False,'Sync failed') + return public.return_message(-1, 0, 'No synchronized database') + # return public.returnMsg(True,'DATABASE_SYNC_SUCCESS',(str(n),)) + return public.return_message(0, 0, 'DATABASE_SYNC_SUCCESS {}'.format(n)) + + #添加到服务器 + def ToDataBase(self,find): + if find['username'] == 'bt_default': + # return 0 + return public.return_message(0, 0, 0) + if len(find['password']) < 3 : + find['username'] = find['name'] + find['password'] = public.md5(str(time.time()) + find['name'])[0:10] + public.M('databases').where("id=?",(find['id'],)).save('password,username',(find['password'],find['username'])) + + self.sid = find['sid'] + try: + int(find['sid']) + except: + return public.return_message(-1, 0, 'Database type sid needs int type!!') + # return public.returnMsg(False, 'Database type sid needs int type!!') + if not public.process_exists("mongod") and not int(find['sid']): + # return public.returnMsg(False,"Mongodb service has not been started yet!!") + return public.return_message(-1, 0, "Mongodb service has not been started yet!!") + + + get = public.dict_obj() + get.sid = self.sid + auth_status = self.get_local_auth(get) + if auth_status: + db_obj = self.get_obj_by_sid(self.sid).get_db_obj(find['name'], auth_status) + try: + db_obj.chat.insert_one({}) + db_obj.command("dropUser", find['username']) + except :pass + try: + db_obj.command("createUser", find['username'], pwd=find['password'], roles=[{'role':'dbOwner','db':find['name']},{'role':'userAdmin','db':find['name']}]) + except: + pass + # return 1 + return public.return_message(0, 0, 1) + + def SyncGetDatabases(self,get): + """ + @从服务器获取数据库 + """ + n = 0;s = 0; + db_type = 0 + self.sid = get.get('sid/d',0) + if self.sid: db_type = 2 + try: + int(get.sid) + except: + # return public.returnMsg(False, 'The database type SID requires an INT!') + return public.return_message(-1, 0, 'The database type SID requires an INT!') + if not public.process_exists("mongod") and not int(get.sid): + # return public.returnMsg(False,"The Mongodb service is not enabled!") + return public.return_message(-1, 0, "The Mongodb service is not enabled!") + auth_status = self.get_local_auth(get) + data = self.get_obj_by_sid(self.sid).get_db_obj('admin',auth=auth_status).command({"listDatabases":1}) + + sql = public.M('databases') + nameArr = ['information_schema','performance_schema','mysql','sys','master','model','msdb','tempdb','config','local','admin'] + for item in data['databases']: + dbname = item['name'] + if sql.where("name=?",(dbname,)).count(): continue + if not dbname in nameArr: + if sql.table('databases').add('name,username,password,accept,ps,addtime,type,sid,db_type',(dbname,dbname,'','',public.getMsg('INPUT_PS'),time.strftime('%Y-%m-%d %X',time.localtime()),'MongoDB',self.sid,db_type)): n +=1 + + # return public.returnMsg(True,'DATABASE_GET_SUCCESS',(str(n),)) + return public.return_message(0, 0, 'DATABASE_GET_SUCCESS {}'.format(n)) + + + def ResDatabasePassword(self,args): + """ + @修改用户密码 + """ + id = args['id'] + username = args['name'].strip() + newpassword = public.trim(args['password']) + + try: + if not newpassword: + # return public.returnMsg(False, 'Modify the failure,The database[' + username + ']password cannot be empty.'); + return public.return_message(-1, 0, 'Modify the failure,The database[' + username + ']password cannot be empty.'); + if len(re.search(r"^[\w@\.]+$", newpassword).groups()) > 0: + # return public.returnMsg(False, 'The database password cannot be empty or contain special characters') + return public.return_message(-1, 0, 'The database password cannot be empty or contain special characters') + + if re.search('[\u4e00-\u9fa5]',newpassword): + # return public.returnMsg(False,'Database password cannot be Chinese, please change the name!') + return public.return_message(-1, 0, 'Database password cannot be Chinese, please change the name!') + except : + # return public.returnMsg(False, 'The database password cannot be empty or contain special characters') + return public.return_message(-1, 0, 'The database password cannot be empty or contain special characters') + + find = public.M('databases').where("id=?",(id,)).field('id,pid,name,username,password,type,accept,ps,addtime,sid').find(); + if not find: + # return public.returnMsg(False, 'The modification failed because the specified database does not exist.'); + return public.return_message(-1, 0, 'The modification failed because the specified database does not exist.'); + + get = public.dict_obj() + get.sid = find['sid'] + try: + int(find['sid']) + except: + # return public.returnMsg(False, 'The database type SID requires an INT!') + return public.return_message(-1, 0, 'The database type SID requires an INT!') + if not public.process_exists("mongod") and not int(find['sid']): + # return public.returnMsg(False,"The Mongodb service is not enabled!") + return public.return_message(-1, 0, "The Mongodb service is not enabled!") + auth_status = self.get_local_auth(args) + if auth_status: + db_obj = self.get_obj_by_sid(find['sid']).get_db_obj(username,auth=auth_status) + try: + print(db_obj.command("updateUser", username, pwd = newpassword)) + except : + print(db_obj.command("createUser", username, pwd=newpassword, roles=[{'role':'dbOwner','db':find['name']},{'role':'userAdmin','db':find['name']}])) + else: + # return public.returnMsg(False, 'Password access is not enabled for the database.') + return public.return_message(-1, 0, 'Password access is not enabled for the database.') + + #修改SQLITE + public.M('databases').where("id=?",(id,)).setField('password',newpassword) + + public.WriteLog("TYPE_DATABASE",'DATABASE_PASS_SUCCESS',(find['name'],)) + # return public.returnMsg(True,'DATABASE_PASS_SUCCESS',(find['name'],)) + return public.return_message(0, 0, 'DATABASE_PASS_SUCCESS',(find['name'],)) + + def get_root_pwd(self,args): + """ + @获取root密码 + """ + config = panelMongoDB().get_options(None) + sa_path = '{}/data/mongo.root'.format(public.get_panel_path()) + if os.path.exists(sa_path): + config['msg'] = public.readFile(sa_path) + else: + config['msg'] = '' + config['root'] = config['msg'] + # return config + return public.return_message(0, 0, config) + + def get_database_size_by_id(self, args): + """ + @获取数据库尺寸(批量删除验证) + @args json/int 数据库id + """ + # if not public.process_exists("mongod"): + # return public.returnMsg(False,"The Mongodb service is not enabled!") + total = 0 + db_id = args + if not isinstance(args, int): db_id = args['db_id'] + + find = public.M('databases').where('id=?', db_id).find() + try: + int(find['sid']) + except: + # return 0 + return public.return_message(0, 0, 0) + if not public.process_exists("mongod") and not int(find['sid']): + # return 0 + return public.return_message(0, 0, 0) + try: + auth_status = self.get_local_auth(args) + db_obj = self.get_obj_by_sid(find['sid']).get_db_obj(find['name'], auth=auth_status) + print(db_obj) + print(db_obj.stats()) + + total = tables[0][1] + if not total: total = 0 + except: + print(public.get_error_info()) + + # return total + return public.return_message(0, 0, total) + + def check_del_data(self,args): + """ + @删除数据库前置检测 + """ + # return self.check_base_del_data(args) + return public.return_message(0, 0, self.check_base_del_data(args)) + + + def check_cloud_database_status(self,conn_config): + """ + @检测远程数据库是否连接 + @conn_config 远程数据库配置,包含host port pwd等信息 + """ + try: + if not 'db_name' in conn_config: conn_config['db_name'] = None + sql_obj = panelMongoDB().set_host(conn_config['db_host'],conn_config['db_port'],conn_config['db_name'],conn_config['db_user'],conn_config['db_password']) + + db_obj = sql_obj.get_db_obj('admin') + data = db_obj.command({"listDatabases":1}) + + if 'databases' in data: + # return True + return public.return_message(0, 0, True) + # return False + return public.return_message(-1, 0, False) + except Exception as ex: + # return public.returnMsg(False,ex) + return public.return_message(0, 0, ex) diff --git a/class_v2/databaseModelV2/pgsqlModel.py b/class_v2/databaseModelV2/pgsqlModel.py new file mode 100644 index 00000000..65029070 --- /dev/null +++ b/class_v2/databaseModelV2/pgsqlModel.py @@ -0,0 +1,769 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hezhihong +# ------------------------------------------------------------------- + +# ------------------------------ +# postgresql模型 +# ------------------------------ +import os, re, json, time +from databaseModelV2.base import databaseBase +import public + +try: + from BTPanel import session +except: + pass +try: + import psycopg2 + from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT +except: + pass + + +class panelPgsql: + __DB_PASS = None + __DB_USER = 'postgres' + __DB_PORT = 5432 + __DB_HOST = 'localhost' + __DB_CONN = None + __DB_CUR = None + __DB_ERR = None + + __DB_CLOUD = 0 # 远程数据库 + + def __init__(self): + self.__DB_CLOUD = 0 + if self.__DB_USER == 'postgres' and self.__DB_HOST == 'localhost' and self.__DB_PASS == None: + tmp_args = public.dict_obj() + tmp_args.is_True = True + self.__DB_PASS = main().get_root_pwd(tmp_args) + + def set_host(self, host, port, name, username, password, prefix=''): + self.__DB_HOST = host + self.__DB_PORT = int(port) + self.__DB_NAME = name + if self.__DB_NAME: self.__DB_NAME = str(self.__DB_NAME) + self.__DB_USER = str(username) + self._USER = str(username) + self.__DB_PASS = str(password) + self.__DB_PREFIX = prefix + self.__DB_CLOUD = 1 + return self + + def check_psycopg(self): + """ + @name检测依赖是否正常 + """ + try: + import psycopg2 + from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + except: + os.system('btpip install psycopg2-binary') + try: + import psycopg2 + from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + except: + return False + return True + + # 连接MYSQL数据库 + def __Conn(self): + self.check_psycopg() + try: + import psycopg2 + except: + self.__DB_ERR = public.get_error_info() + return False + try: + if self.__DB_USER == 'postgres' and self.__DB_HOST == 'localhost': + if not self.__DB_PASS: + tmp_args = public.dict_obj() + try: + self.__DB_PASS == main().get_root_pwd(tmp_args)['msg'] + except: + pass + self.__DB_CONN = psycopg2.connect(user=self.__DB_USER, password=self.__DB_PASS, host=self.__DB_HOST, + port=self.__DB_PORT) + self.__DB_CONN.autocommit = True + self.__DB_CONN.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) # <-- ADD THIS LINE + + self.__DB_CUR = self.__DB_CONN.cursor() + return True + except: + + self.__DB_ERR = public.get_error_info() + print(self.__DB_ERR) + return False + + def execute(self, sql): + # 执行SQL语句返回受影响行 + if not self.__Conn(): return self.__DB_ERR + try: + # print(sql) + result = self.__DB_CUR.execute(sql) + self.__DB_CONN.commit() + self.__Close() + return result + except Exception as ex: + + return ex + + def query(self, sql): + + # 执行SQL语句返回数据集 + if not self.__Conn(): return self.__DB_ERR + try: + self.__DB_CUR.execute(sql) + result = self.__DB_CUR.fetchall() + + data = list(map(list, result)) + self.__Close() + return data + except Exception as ex: + return ex + + # 关闭连接 + def __Close(self): + self.__DB_CUR.close() + self.__DB_CONN.close() + + +class main(databaseBase, panelPgsql): + __ser_name = None + __soft_path = '/www/server/pgsql' + __setup_path = '/www/server/panel/' + __dbuser_info_path = "{}plugin/pgsql_manager_dbuser_info.json".format(__setup_path) + __plugin_path = "{}plugin/pgsql_manager/".format(__setup_path) + + def __init__(self): + + s_path = public.get_setup_path() + v_info = public.readFile("{}/pgsql/version.pl".format(s_path)) + if v_info: + ver = v_info.split('.')[0] + self.__ser_name = 'postgresql-x64-{}'.format(ver) + self.__soft_path = '{}/pgsql/{}'.format(s_path) + + # 获取配置项 + def get_options(self, get): + data = {} + options = ['port', 'listen_addresses'] + if not self.__soft_path: self.__soft_path = '{}/pgsql'.format(public.get_setup_path()) + conf = public.readFile('{}/data/postgresql.conf'.format(self.__soft_path)) + for opt in options: + tmp = re.findall(r"\s+" + opt + r"\s*=\s*(.+)#", conf) + if not tmp: continue; + data[opt] = tmp[0].strip() + if opt == 'listen_addresses': + data[opt] = data[opt].replace('\'', '') + data['password'] = self.get_root_pwd(None)['msg'] + # return data + return public.return_message(0, 0, data) + + def get_list(self, args): + """ + @获取数据库列表 + @sql_type = pgsql + """ + # return self.get_base_list(args, sql_type = 'pgsql') + return public.return_message(0, 0, self.get_base_list(args, sql_type='pgsql')) + + def get_sql_obj_by_sid(self, sid=0, conn_config=None): + """ + @取pgsql数据库对像 By sid + @sid 数据库分类,0:本地 + """ + if type(sid) == str: + try: + sid = int(sid) + except: + sid = 0 + + if sid: + if not conn_config: conn_config = public.M('database_servers').where("id=?", sid).find() + db_obj = panelPgsql() + + try: + db_obj = db_obj.set_host(conn_config['db_host'], conn_config['db_port'], None, conn_config['db_user'], + conn_config['db_password']) + except Exception as e: + raise public.PanelError(e) + else: + db_obj = panelPgsql() + return db_obj + # return public.return_message(0, 0, db_obj) + + def get_sql_obj(self, db_name): + """ + @取pgsql数据库对象 + @db_name 数据库名称 + """ + is_cloud_db = False + if db_name: + db_find = public.M('databases').where("name=?", db_name).find() + if db_find['sid']: + # return self.get_sql_obj_by_sid(db_find['sid']) + return public.return_message(0, 0, self.get_sql_obj_by_sid(db_find['sid'])) + is_cloud_db = db_find['db_type'] in ['1', 1] + + if is_cloud_db: + + db_obj = panelPgsql() + conn_config = json.loads(db_find['conn_config']) + try: + db_obj = db_obj.set_host(conn_config['db_host'], conn_config['db_port'], conn_config['db_name'], + conn_config['db_user'], conn_config['db_password']) + except Exception as e: + raise public.PanelError(e) + else: + db_obj = panelPgsql() + # return db_obj + return public.return_message(0, 0, db_obj) + + def GetCloudServer(self, args): + ''' + @name 获取远程服务器列表 + @author hwliang<2021-01-10> + @return list + ''' + + check_result = os.system('/www/server/pgsql/bin/psql --version') + if check_result != 0 and not public.M('database_servers').where('db_type=?', 'pgsql').count(): + # return [] + return public.return_message(0, 0, []) + # return self.GetBaseCloudServer(args) + return public.return_message(0, 0, self.GetBaseCloudServer(args)) + + def AddCloudServer(self, args): + ''' + @添加远程数据库 + ''' + return self.AddBaseCloudServer(args) + # return public.return_message(0, 0, self.AddBaseCloudServer(args)) + + def RemoveCloudServer(self, args): + ''' + @删除远程数据库 + ''' + return self.RemoveBaseCloudServer(args) + # return public.return_message(0, 0, self.RemoveBaseCloudServer(args)) + + def ModifyCloudServer(self, args): + ''' + @修改远程数据库 + ''' + return self.ModifyBaseCloudServer(args) + + + def AddDatabase(self, args): + """ + @添加数据库 + """ + # try: + if not args.get('name/str', 0): + # rreturn public.returnMsg(False, 'Database name cannot be empty!') + return public.return_message(-1, 0, 'Database name cannot be empty!') + import re + test_str = re.search(r"\W", args.name) + if test_str != None: + # rreturn public.returnMsg(False, 'The database name cannot contain special characters') + return public.return_message(-1, 0, 'The database name cannot contain special characters') + res = self.add_base_database(args) + if not res['status']: + # return res + return public.return_message(-1, 0, res['msg']) + + data_name = res['data_name'] + username = res['username'] + password = res['data_pwd'] + try: + self.sid = int(args['sid']) + except: + self.sid = 0 + + dtype = 'PgSql' + + sql_obj = self.get_sql_obj_by_sid(self.sid) + result = sql_obj.execute("CREATE DATABASE {};".format(data_name)) + + isError = self.IsSqlError(result) + + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + + # 添加用户 + self.__CreateUsers(data_name, username, password, '127.0.0.1') + + if not hasattr(args, 'ps'): args['ps'] = public.getMsg('INPUT_PS'); + addTime = time.strftime('%Y-%m-%d %X', time.localtime()) + + pid = 0 + if hasattr(args, 'pid'): pid = args.pid + + if hasattr(args, 'contact'): + site = public.M('sites').where("id=?", (args.contact,)).field('id,name').find() + if site: + pid = int(args.contact) + args['ps'] = site['name'] + + db_type = 0 + if self.sid: db_type = 2 + + public.set_module_logs('pgsql', 'AddDatabase', 1) + # 添加入SQLITE + public.M('databases').add('pid,sid,db_type,name,username,password,accept,ps,addtime,type', ( + pid, self.sid, db_type, data_name, username, password, '127.0.0.1', args['ps'], addTime, dtype)) + public.WriteLog("TYPE_DATABASE", 'DATABASE_ADD_SUCCESS', (data_name,)) + # return public.returnMsg(True,'ADD_SUCCESS') + return public.return_message(0, 0, 'ADD_SUCCESS') + # except Exception as ex: + # public.print_log("error info55: {}".format(ex)) + # return public.return_message(-1, 0, str(ex)) + def DeleteDatabase(self, get): + """ + @删除数据库 + """ + id = get['id'] + find = public.M('databases').where("id=?", (id,)).field( + 'id,pid,name,username,password,accept,ps,addtime,db_type,conn_config,sid,type').find(); + if not find: + # return public.returnMsg(False,'The specified database does not exist.') + return public.return_message(-1, 0, 'The specified database does not exist.') + + name = get['name'] + username = find['username'] + + sql_obj = self.get_sql_obj_by_sid(find['sid']) + result = sql_obj.execute("drop database {};".format(name)) + sql_obj.execute("drop user {};".format(username)) + # 删除SQLITE + public.M('databases').where("id=?", (id,)).delete() + public.WriteLog("TYPE_DATABASE", 'DATABASE_DEL_SUCCESS', (name,)) + # return public.returnMsg(True, 'DEL_SUCCESS') + return public.return_message(0, 0, 'DEL_SUCCESS') + + def ToBackup(self, args): + """ + @备份数据库 id 数据库id + """ + id = args['id'] + + find = public.M('databases').where("id=?", (id,)).find() + if not find: + # return public.returnMsg(False,'Database does not exist!') + return public.return_message(-1, 0, 'Database does not exist!') + + if not find['password'].strip(): + # return public.returnMsg(False,'The database password is empty. Set the password first.') + return public.return_message(-1, 0, 'The database password is empty. Set the password first.') + + sql_dump = '{}/bin/pg_dump'.format(self.__soft_path) + # return sql_dump + if not os.path.isfile(sql_dump): + # return public.returnMsg(False,'Lack of backup tools, please first through the software store PGSQL manager!') + return public.return_message(-1, 0, + 'Lack of backup tools, please first through the software store PGSQL manager!') + + back_path = session['config']['backup_path'] + '/database/pgsql/' + # return back_path + if not os.path.exists(back_path): os.makedirs(back_path) + + fileName = find['name'] + '_' + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.sql' + + backupName = back_path + fileName + + if int(find['sid']): + info = self.get_info_by_db_id(id) + shell = '{} "host={} port={} user={} dbname={} password={}" > {}'.format(sql_dump, info['db_host'], + info['db_port'], info['db_user'], + find['name'], info['db_password'], + backupName) + else: + args_one = public.dict_obj() + port = self.get_port(args_one) + shell = '{} "host=127.0.0.1 port={} user={} dbname={} password={}" > {}'.format(sql_dump, port['data'], + find['username'], + find['name'], + find['password'], + backupName) + + ret = public.ExecShell(shell) + if not os.path.exists(backupName): + # return public.returnMsg(False,'BACKUP_ERROR'); + return public.return_message(-1, 0, 'BACKUP_ERROR') + + public.M('backup').add('type,name,pid,filename,size,addtime', + (1, fileName, id, backupName, 0, time.strftime('%Y-%m-%d %X', time.localtime()))) + public.WriteLog("TYPE_DATABASE", "DATABASE_BACKUP_SUCCESS", (find['name'],)) + + if os.path.getsize(backupName) < 2048: + # return public.returnMsg(True, 'The backup file size is smaller than 2Kb. Check the backup integrity.') + return public.return_message(0, 0, 'The backup file size is smaller than 2Kb. Check the backup integrity.') + else: + # return public.returnMsg(True, 'BACKUP_SUCCESS') + return public.return_message(0, 0, 'BACKUP_SUCCESS') + + def DelBackup(self, args): + """ + @删除备份文件 + """ + return self.delete_base_backup(args) + # return public.return_message(0, 0, self.delete_base_backup(args)) + + def get_port(self, args): # 获取端口号 + str_shell = '''netstat -luntp|grep postgres|head -1|awk '{print $4}'|awk -F: '{print $NF}' ''' + try: + port = public.ExecShell(str_shell)[0] + if port.strip(): + return {'data': port.strip(), "status": True} + else: + return {'data': 5432, "status": False} + except: + return {'data': 5432, "status": False} + + # 导入 + def InputSql(self, get): + + name = get.name + file = get.file + # return name + + find = public.M('databases').where("name=?", (name,)).find() + if not find: + # return public.returnMsg(False,'Database does not exist!') + return public.return_message(-1, 0, 'Database does not exist!') + # return find + if not find['password'].strip(): + # return public.returnMsg(False,'The database password is empty. Set the password first.') + return public.return_message(-1, 0, 'The database password is empty. Set the password first.') + + tmp = file.split('.') + exts = ['sql'] + ext = tmp[len(tmp) - 1] + if ext not in exts: + # return public.returnMsg(False, 'DATABASE_INPUT_ERR_FORMAT') + return public.return_message(-1, 0, 'DATABASE_INPUT_ERR_FORMAT') + + sql_dump = '{}/bin/psql'.format(self.__soft_path) + if not os.path.exists(sql_dump): + # return public.returnMsg(False,'Lack of recovery tool, please use software management to install PGSQL!') + return public.return_message(-1, 0, + 'Lack of recovery tool, please use software management to install PGSQL!') + + if int(find['sid']): + info = self.get_info_by_db_id(find['id']) + shell = '{} "host={} port={} user={} dbname={} password={}" < {}'.format(sql_dump, info['db_host'], + info['db_port'], info['db_user'], + find['name'], info['db_password'], + file) + else: + args_one = public.dict_obj() + port = self.get_port(args_one) + shell = '{} "host=127.0.0.1 port={} user={} dbname={} password={}" < {}'.format(sql_dump, port['data'], + find['username'], + find['name'], + find['password'], file) + + ret = public.ExecShell(shell) + + public.WriteLog("TYPE_DATABASE", 'Description Succeeded in importing database [{}]'.format(name)) + # return public.returnMsg(True, 'DATABASE_INPUT_SUCCESS'); + return public.return_message(0, 0, 'DATABASE_INPUT_SUCCESS') + + def SyncToDatabases(self, get): + """ + @name同步数据库到服务器 + """ + tmp_type = int(get['type']) + n = 0 + sql = public.M('databases') + if tmp_type == 0: + where = "lower(type) = lower('pgsql')" + # data = sql.field('id,name,username,password,accept,type,sid,db_type').where('type=?',('pgsql',)).select() + data = sql.field('id,name,username,password,accept,type,sid,db_type').where(where, ()).select() + print(data) + for value in data: + if value['db_type'] in ['1', 1]: + continue # 跳过远程数据库 + result = self.ToDataBase(value) + if result == 1: n += 1 + else: + import json + data = json.loads(get.ids) + for value in data: + find = sql.where("id=?", (value,)).field('id,name,username,password,sid,db_type,accept,type').find() + result = self.ToDataBase(find) + if result == 1: n += 1 + if n == 1: + # return public.returnMsg(True, 'Synchronization succeeded') + return public.return_message(0, 0, 'Synchronization succeeded') + elif n == 0: + # return public.returnMsg(False, 'Sync failed') + return public.return_message(-1, 0, 'Sync failed') + # return public.returnMsg(True, 'DATABASE_SYNC_SUCCESS', (str(n),)) + return public.return_message(0, 0, 'DATABASE_SYNC_SUCCESS {}'.format(n)) + + def ToDataBase(self, find): + """ + @name 添加到服务器 + """ + if find['username'] == 'bt_default': return 0 + if len(find['password']) < 3: + find['username'] = find['name'] + find['password'] = public.md5(str(time.time()) + find['name'])[0:10] + public.M('databases').where("id=?", (find['id'],)).save('password,username', + (find['password'], find['username'])) + + self.sid = find['sid'] + sql_obj = self.get_sql_obj_by_sid(self.sid) + result = sql_obj.execute("CREATE DATABASE {};".format(find['name'])) + isError = self.IsSqlError(result) + if isError != None and isError['status'] == False and isError[ + 'msg'] == 'The specified database already exists, please do not add it repeatedly.': return 1 + + self.__CreateUsers(find['name'], find['username'], find['password'], '127.0.0.1') + + return 1 + + def SyncGetDatabases(self, get): + """ + @name 从服务器获取数据库 + @param sid 0为本地数据库 1为远程数据库 + """ + n = 0; + s = 0; + db_type = 0 + self.sid = get.get('sid/d', 0) + if self.sid: db_type = 2 + + sql_obj = self.get_sql_obj_by_sid(self.sid) + data = sql_obj.query('SELECT datname FROM pg_database;') # select * from pg_database order by datname; + isError = self.IsSqlError(data) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + if type(data) == str: + # return public.returnMsg(False, data) + return public.return_message(-1, 0, data) + + sql = public.M('databases') + nameArr = ['information_schema', 'postgres', 'template1', 'template0', 'performance_schema', 'mysql', 'sys', + 'master', 'model', 'msdb', 'tempdb', 'ReportServerTempDB', 'YueMiao', 'ReportServer'] + for item in data: + + dbname = item[0] + + if sql.where("name=?", (dbname,)).count(): continue + if not dbname in nameArr: + if sql.table('databases').add('name,username,password,accept,ps,addtime,type,sid,db_type', ( + dbname, dbname, '', '', public.getMsg('INPUT_PS'), time.strftime('%Y-%m-%d %X', time.localtime()), + 'pgsql', self.sid, db_type)): n += 1 + + # return public.returnMsg(True, 'DATABASE_GET_SUCCESS', (str(n),)) + return public.return_message(0, 0, 'DATABASE_GET_SUCCESS {}'.format(n)) + + def ResDatabasePassword(self, args): + """ + @修改用户密码 + """ + id = args['id'] + username = args['name'].strip() + newpassword = public.trim(args['password']) + if not newpassword: + # return public.returnMsg(False, 'The database password cannot be empty.'); + return public.return_message(-1, 0, 'The database password cannot be empty') + + find = public.M('databases').where("id=?", (id,)).field( + 'id,pid,name,username,password,type,accept,ps,addtime,sid').find() + if not find: + # return public.returnMsg(False, 'Modify the failure,The specified database does not exist.'); + return public.return_message(-1, 0, 'Modify the failure,The specified database does not exist') + + sql_obj = self.get_sql_obj_by_sid(find['sid']) + result = sql_obj.execute("alter user {} with password '{}';".format(username, newpassword)) + isError = self.IsSqlError(result) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + + # 修改SQLITE + public.M('databases').where("id=?", (id,)).setField('password', newpassword) + + public.WriteLog("TYPE_DATABASE", 'DATABASE_PASS_SUCCESS', (find['name'],)) + # return public.returnMsg(True, 'DATABASE_PASS_SUCCESS', (find['name'],)) + return public.return_message(0, 0, 'DATABASE_PASS_SUCCESS {}'.format(find['name'],)) + + def get_root_pwd(self, args): + """ + @获取sa密码 + """ + check_result = os.system('/www/server/pgsql/bin/psql --version') + if check_result != 0: + # return public.returnMsg(False,'If PgSQL is not installed or started, install or start it first') + return public.return_message(-1, 0,'If PgSQL is not installed or started, install or start it first') + password = '' + path = '{}/data/postgresAS.json'.format(public.get_panel_path()) + if os.path.isfile(path): + try: + password = json.loads(public.readFile(path))['password'] + print('333333333') + print(password) + except: + pass + if 'is_True' in args and args.is_True: + # return password + return public.return_message(-1, 0, password) + # return public.returnMsg(True, password) + return public.return_message(0, 0, password) + + def set_root_pwd(self, args): + """ + @设置sa密码 + """ + password = public.trim(args['password']) + if len(password) < 8: + # return public.returnMsg(False, 'The password must not be less than 8 digits.') + return public.return_message(-1, 0, 'The password must not be less than 8 digits.') + check_result = os.system('/www/server/pgsql/bin/psql --version') + if check_result != 0: + # return public.returnMsg(False,'If PgSQL is not installed or started, install or start it first') + return public.return_message(-1, 0, 'If PgSQL is not installed or started, install or start it first') + sql_obj = self.get_sql_obj_by_sid('0') + data = sql_obj.query('SELECT datname FROM pg_database;') + isError = self.IsSqlError(data) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + + path = '{}/data/pg_hba.conf'.format(self.__soft_path) + p_path = '{}/data/postgresAS.json'.format(public.get_panel_path()) + if not os.path.isfile(path): + # return public.returnMsg(False,'{}File does not exist, please check the installation is complete!'.format(path)) + return public.return_message(-1, 0, '{}File does not exist, please check the installation is complete!'.format(path)) + src_conf = public.readFile(path) + add_conf = src_conf.replace('md5', 'trust') + # public.writeFile(path,public.readFile(path).replace('md5','trust')) + public.writeFile(path, add_conf) + + pg_obj = panelPgsql() + pg_obj.execute("ALTER USER postgres WITH PASSWORD '{}';".format(password)) + data = {"username": "postgres", "password": ""} + try: + data = json.loads(public.readFile(p_path)) + except: + pass + data['password'] = password + public.writeFile(p_path, json.dumps(data)) + public.writeFile(path, src_conf) + # return public.returnMsg(True, 'The administrator password is successfully changed. Procedure.') + return public.return_message(0, 0, 'The administrator password is successfully changed. Procedure.') + + def get_info_by_db_id(self, db_id): + """ + @获取数据库连接详情 + @db_id 数据库id + """ + # print(db_id,'111111111111') + find = public.M('databases').where("id=?", db_id).find() + # return find + if not find: return False + # print(find) + data = { + 'db_host': '127.0.0.1', + 'db_port': 5432, + 'db_user': find['username'], + 'db_password': find['password'] + } + + if int(find['sid']): + conn_config = public.M('database_servers').where("id=?", find['sid']).find() + + data['db_host'] = conn_config['db_host'] + data['db_port'] = int(conn_config['db_port']) + return data + + def get_database_size_by_id(self, args): + """ + @获取数据库尺寸(批量删除验证) + @args json/int 数据库id + """ + total = 0 + db_id = args + if not isinstance(args, int): db_id = args['db_id'] + + try: + name = public.M('databases').where('id=?', db_id).getField('name') + sql_obj = self.get_sql_obj(name) + tables = sql_obj.query( + "select name,size,type from sys.master_files where type=0 and name = '{}'".format(name)) + + total = tables[0][1] + if not total: total = 0 + except: + pass + + return total + + def check_del_data(self, args): + """ + @删除数据库前置检测 + """ + # return self.check_base_del_data(args) + return public.return_message(0, 0, self.check_base_del_data(args)) + + # 本地创建数据库 + def __CreateUsers(self, data_name, username, password, address): + """ + @创建数据库用户 + """ + sql_obj = self.get_sql_obj_by_sid(self.sid) + sql_obj.execute("CREATE USER {} WITH PASSWORD '{}';".format(username, password)) + sql_obj.execute("GRANT ALL PRIVILEGES ON DATABASE {} TO {};".format(data_name, username)) + return True + + def __get_db_list(self, sql_obj): + """ + 获取pgsql数据库列表 + """ + data = [] + ret = sql_obj.query('SELECT datname FROM pg_database;') + if type(ret) == list: + for x in ret: + data.append(x[0]) + return data + + def check_cloud_database_status(self, conn_config): + """ + @检测远程数据库是否连接 + @conn_config 远程数据库配置,包含host port pwd等信息 + """ + try: + + if not 'db_name' in conn_config: conn_config['db_name'] = None + sql_obj = panelPgsql().set_host(conn_config['db_host'], conn_config['db_port'], conn_config['db_name'], + conn_config['db_user'], conn_config['db_password']) + + data = sql_obj.query("SELECT datname FROM pg_database;") + if type(data) == str: + # return public.returnMsg(False,'Connecting to remote PGSQL fails. Perform the following operations to rectify the fault:
                                    1、The database port is correct and the firewall allows access
                                    2、Check whether the database account password is correct
                                    3、pg_hba.confWhether to add a client release record
                                    4、postgresql.conf Add listen_addresses to the correct server IP address.') + return public.return_message(-1, 0, 'Connecting to remote PGSQL fails. Perform the following operations to rectify the fault:
                                    1、The database port is correct and the firewall allows access
                                    2、Check whether the database account password is correct
                                    3、pg_hba.confWhether to add a client release record
                                    4、postgresql.conf Add listen_addresses to the correct server IP address.') + + if not conn_config['db_name']: + # return True + return public.return_message(0, 0, True) + for i in data: + if i[0] == conn_config['db_name']: + # return True + return public.return_message(0, 0, True) + # return public.returnMsg(False, 'The specified database does not exist!') + return public.return_message(-1, 0, 'The specified database does not exist!') + except Exception as ex: + + # return public.returnMsg(False, ex) + return public.return_message(-1, 0, ex) diff --git a/class_v2/databaseModelV2/redisModel.py b/class_v2/databaseModelV2/redisModel.py new file mode 100644 index 00000000..529e953b --- /dev/null +++ b/class_v2/databaseModelV2/redisModel.py @@ -0,0 +1,611 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +# sqlite模型 +#------------------------------ +import os,re,json,shutil,time +from databaseModelV2.base import databaseBase +import public +from public.validate import Param +try: + import redis +except: + public.ExecShell("btpip install redis") + import redis +try: + from BTPanel import session +except :pass + + +class panelRedisDB(): + + __DB_PASS = None + __DB_USER = None + __DB_PORT = 6379 + __DB_HOST = '127.0.0.1' + __DB_CONN = None + __DB_ERR = None + + __DB_CLOUD = None + def __init__(self): + self.__config = self.get_options(None) + + def redis_conn(self,db_idx = 0): + + if self.__DB_HOST in ['127.0.0.1','localhost']: + if not os.path.exists('/www/server/redis'): return False + + if not self.__DB_CLOUD: + self.__DB_PASS = self.__config['requirepass'] + self.__DB_PORT = int(self.__config['port']) + + try: + redis_pool = redis.ConnectionPool(host=self.__DB_HOST, port= self.__DB_PORT, password= self.__DB_PASS, db= db_idx) + self.__DB_CONN = redis.Redis(connection_pool= redis_pool) + return self.__DB_CONN + except : + self.__DB_ERR = public.get_error_info() + return False + + + def set_host(self,host,port,name,username,password,prefix = ''): + self.__DB_HOST = host + self.__DB_PORT = int(port) + self.__DB_NAME = name + if self.__DB_NAME: self.__DB_NAME = str(self.__DB_NAME) + self.__DB_USER = str(username) + self._USER = str(username) + self.__DB_PASS = str(password) + self.__DB_PREFIX = prefix + self.__DB_CLOUD = 1 + return self + + + #获取配置项 + def get_options(self,get = None): + + result = {} + redis_conf = public.readFile("{}/redis/redis.conf".format(public.get_setup_path())) + if not redis_conf: return False + + keys = ["bind","port","timeout","maxclients","databases","requirepass","maxmemory"] + for k in keys: + v = "" + rep = "\n%s\\s+(.+)" % k + group = re.search(rep,redis_conf) + if not group: + if k == "maxmemory": + v = "0" + if k == "maxclients": + v = "10000" + if k == "requirepass": + v = "" + else: + if k == "maxmemory": + v = int(group.group(1)) / 1024 / 1024 + else: + v = group.group(1) + result[k] = v + return result + + + +class main(databaseBase): + + _db_max = 16 #最大redis数据库 + def __init__(self): + pass + + + def GetCloudServer(self,args): + ''' + @name 获取远程服务器列表 + @author hwliang<2021-01-10> + @return list + ''' + # # 校验参数 + # try: + # args.validate([ + # Param('type').Require().String('in', ['redis']), + # ], [ + # public.validate.trim_filter(), + # ]) + # except Exception as ex: + # public.print_log("error info: {}".format(ex)) + # return public.return_message(-1, 0, str(ex)) + # return self.GetBaseCloudServer(args) + return public.return_message(0, 0, self.GetBaseCloudServer(args)) + + + def AddCloudServer(self,args): + ''' + @添加远程数据库 + ''' + # {"db_host":"192.168.66.129","db_port":"6379","db_user":"root","db_password":"password1","db_ps":"192.168.66.129","type":"redis"} + # 校验参数 + try: + args.validate([ + + Param('db_host').Require().Ip(), + Param('db_port').Require().Number(">=", 1).Number("<=", 65535), + Param('db_user').Require().String().Xss(), + Param('db_password').Require().String().Xss(), + Param('db_ps').Require().String(), + Param('type').Require().String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + return self.AddBaseCloudServer(args) + # return public.return_message(0, 0, self.AddBaseCloudServer(args)) + + def RemoveCloudServer(self,args): + ''' + @删除远程数据库 + ''' + return self.RemoveBaseCloudServer(args) + # return public.return_message(0, 0, self.RemoveBaseCloudServer(args)) + + def ModifyCloudServer(self,args): + ''' + @修改远程数据库 + ''' + return self.ModifyBaseCloudServer(args) + + # return public.return_message(0, 0, self.ModifyBaseCloudServer(args)) + + def get_obj_by_sid(self,sid = 0,conn_config = None): + """ + @取mssql数据库对像 By sid + @sid 数据库分类,0:本地 + """ + if type(sid) == str: + try: + sid = int(sid) + except :sid = 0 + + if sid: + if not conn_config: conn_config = public.M('database_servers').where("id=?" ,sid).find() + db_obj = panelRedisDB() + + try: + db_obj = db_obj.set_host(conn_config['db_host'],conn_config['db_port'],None,conn_config['db_user'],conn_config['db_password']) + except Exception as e: + raise public.PanelError(e) + else: + db_obj = panelRedisDB() + return db_obj + + + + def get_list(self,args): + """ + @获取数据库列表 + @sql_type = redis + """ + + # 校验参数 + try: + args.validate([ + Param('sid').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + result = [] + self.sid = args.get('sid/d',0) + for x in range(0,self._db_max): + + data = {} + data['id'] = x + data['name'] = 'DB{}'.format(x) + + + try: + redis_obj = self.get_obj_by_sid(self.sid).redis_conn(x) + + data['keynum'] = redis_obj.dbsize() + if data['keynum'] > 0: + result.append(data) + except Exception as ex: + public.print_log("error info2: {}".format(ex)) + pass + + #result = sorted(result,key= lambda x:x['keynum'],reverse=True) + # return result + return public.return_message(0, 0, result) + + + def set_redis_val(self,args): + """ + @设置或修改指定值 + """ + # {"val":"bbbbbb12","endtime":"30","name":"aaaaa","db_idx":0,"sid":0} + # 校验参数 + try: + args.validate([ + + Param('db_idx').Require().Integer(), + Param('sid').Require().Integer(), + Param('name').Require().String().Xss(), # 键 + Param('val').Require().String().Xss(), # 值 + Param('endtime').Integer(), # 过期时间 + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + self.sid = args.get('sid/d',0) + if not 'name' in args or not 'val' in args: + # return public.returnMsg(False,'Parameter passing error.') + return public.return_message(-1, 0, 'Parameter passing error') + + endtime = 0 + if 'endtime' in args : endtime = int(args.endtime) + + redis_obj = self.get_obj_by_sid(self.sid).redis_conn(args.db_idx) + if endtime: + redis_obj.set(args.name, args.val, endtime) + else: + redis_obj.set(args.name, args.val) + public.set_module_logs('linux_redis','set_redis_val',1) + # return public.returnMsg(True,'Operation is successful.') + return public.return_message(0, 0, 'Operation is successful') + + def del_redis_val(self,args): + """ + @删除key值 + """ + # {"db_idx":0,"key":"qq","sid":0} + # 校验参数 + try: + args.validate([ + + Param('db_idx').Require().Integer(), + Param('sid').Require().Integer(), + Param('key').Require().String().Xss(), # 键 + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + self.sid = args.get('sid/d',0) + if not 'key' in args: + # return public.returnMsg(False,'Parameter passing error.') + return public.return_message(-1, 0, 'Parameter passing error') + + redis_obj = self.get_obj_by_sid(self.sid).redis_conn(args.db_idx) + redis_obj.delete(args.key) + + # return public.returnMsg(True,'Operation is successful.') + return public.return_message(0, 0, 'Operation is successful') + + + def clear_flushdb(self,args): + """ + 清空数据库 + @ids 清空数据库列表,不传则清空所有 + """ + # 校验参数 + try: + args.validate([ + + Param('ids').String(), # "ids":"[0,1]" + Param('sid').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + self.sid = args.get('sid/d',0) + ids = json.loads(args.ids) + #ids = [] + if len(ids) == 0: + for x in range(0,self._db_max): + ids.append(x) + + for x in ids: + redis_obj = self.get_obj_by_sid(self.sid).redis_conn(x) + redis_obj.flushdb() + + # return public.returnMsg(True,'Operation is successful.') + return public.return_message(0, 0, 'Operation is successful') + + def get_db_keylist(self,args): + """ + @获取指定数据库key集合 + """ + # 校验参数 + try: + args.validate([ + Param('db_type').Integer(), + Param('db_idx').Require().Integer(), + Param('limit').Integer(), + Param('p').Integer(), + Param('search').String(), + Param('tojs').String(), # 不知道 + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + search = '*' + if 'search' in args: search = "*" + args.search+"*" + db_idx = args.db_idx + self.sid = args.get('sid/d',0) + + redis_obj = self.get_obj_by_sid(self.sid).redis_conn(db_idx) + try: + keylist = sorted(redis_obj.keys(search)) + except : + keylist = [] + + + info = {'p':1,'row':10,'count':len(keylist)} + + if hasattr(args,'limit'): info['row'] = int(args.limit) + if hasattr(args,'p'): info['p'] = int(args['p']) + + import page + #实例化分页类 + page = page.Page() + + info['uri'] = args + info['return_js'] = '' + if hasattr(args,'tojs'): + info['return_js'] = args.tojs + + slist = keylist[(info['p']-1) * info['row']:info['p'] * info['row']] + + rdata = {} + rdata['page'] = page.GetPage(info,'1,2,3,4,5,8') + rdata['where'] = '' + rdata['data'] = [] + + idx = 0 + for key in slist: + item = {} + try: + item['name'] = key.decode() + except: + item['name'] = str(key) + + item['endtime'] = redis_obj.ttl(key) + if item['endtime'] == -1: item['endtime'] = 0 + + item['type'] = redis_obj.type(key).decode() + + if item['type'] == 'string': + try: + item['val'] = redis_obj.get(key).decode() + except: + item['val'] = str(redis_obj.get(key)) + elif item['type'] == 'hash': + item['val'] = str(redis_obj.hgetall(key)) + elif item['type'] == 'list': + item['val'] = str(redis_obj.lrange(key, 0, -1)) + elif item['type'] == 'set': + item['val'] = str(redis_obj.smembers(key)) + elif item['type'] == 'zset': + item['val'] = str(redis_obj.zrange(key, 0, 1, withscores=True)) + else: + item['val'] = '' + try: + item['len'] = redis_obj.strlen(key) + except: + item['len'] = len(item['val']) + item['val'] = public.xsssec(item['val']) + item['name'] = public.xsssec(item['name']) + rdata['data'].append(item) + idx += 1 + # return rdata + return public.return_message(0, 0, rdata) + + + def ToBackup(self,args): + """ + @备份数据库 + """ + + # 校验参数 + try: + args.validate([ + Param('sid').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + self.sid = args.get('sid/d',0) + + redis_obj = self.get_obj_by_sid(self.sid).redis_conn(0) + redis_obj.save() + + src_path = '{}/dump.rdb'.format(redis_obj.config_get()['dir']) + if not os.path.exists(src_path): + # return public.returnMsg(False,'BACKUP_ERROR') + return public.return_message(-1, 0, 'BACKUP_ERROR') + + backup_path = session['config']['backup_path'] + '/database/redis/' + if not os.path.exists(backup_path): os.makedirs(backup_path) + + fileName = backup_path + str(self.sid) + '_db_' + time.strftime('%Y%m%d_%H%M%S',time.localtime()) +'.rdb' + + shutil.copyfile(src_path,fileName) + if not os.path.exists(fileName): + # return public.returnMsg(False,'BACKUP_ERROR') + return public.return_message(-1, 0, 'BACKUP_ERROR') + + # return public.returnMsg(True, 'BACKUP_SUCCESS') + return public.return_message(0, 0, 'BACKUP_SUCCESS') + + except Exception as ex: + public.print_log("error info22: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + def DelBackup(self,args): + """ + @删除备份文件 + """ + # 校验参数 + try: + args.validate([ + Param('file').Require().SafePath(), + # Param('sid').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + file = args.file + + if os.path.exists(file): + os.remove(file) + + # return public.returnMsg(True, 'DEL_SUCCESS') + return public.return_message(0, 0, 'DEL_SUCCESS') + + def InputSql(self,get): + """ + @导入数据库 + """ + + # 校验参数 + try: + get.validate([ + Param('file').Require().SafePath(), + Param('sid').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + file = get.file + self.sid = get.get('sid/d',0) + + redis_obj = self.get_obj_by_sid(self.sid).redis_conn(0) + + rpath = redis_obj.config_get()['dir'] + dst_path = '{}/dump.rdb'.format(rpath) + public.ExecShell("/etc/init.d/redis stop") + if os.path.exists(dst_path): os.remove(dst_path) + shutil.copy2(file, dst_path) + public.ExecShell("chown redis.redis {dump} && chmod 644 {dump}".format(dump=dst_path)) + # self.restart_services() + public.ExecShell("/etc/init.d/redis start") + if os.path.exists(dst_path): + # return public.returnMsg(True, 'Restore Successful.') + return public.return_message(0, 0, 'Restore Successful.') + # return public.returnMsg(False, 'Restore failure.') + return public.return_message(-1, 0, 'Restore failure.') + + + def get_backup_list(self,get): + """ + @获取备份文件列表 + """ + # 校验参数 + try: + get.validate([ + Param('search').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + search = '' + if hasattr(get, 'search'): + search = get['search'].strip().lower() + + nlist = [] + cloud_list = {} + # try: + # 修改返回后接口适配 + listm = self.GetCloudServer({'type': 'redis'}) + + for x in listm['message']: + cloud_list['id-' + str(x['id'])] = x + + path = session['config']['backup_path'] + '/database/redis/' + if not os.path.exists(path): + os.makedirs(path) + # except Exception as ex: + # public.print_log("error 扥就: {}".format(ex)) + + for name in os.listdir(path): + if search: + if name.lower().find(search) == -1: continue; + + arrs = name.split('_') + + filepath = '{}/{}'.format(path,name).replace('//','/') + stat = os.stat(filepath) + + item = {} + item['name'] = name + item['filepath'] = filepath + item['size'] = stat.st_size + item['mtime'] = int(stat.st_mtime) + item['sid'] = arrs[0] + item['conn_config'] = cloud_list['id-' + str(arrs[0])] + + nlist.append(item) + if hasattr(get, 'sort'): + nlist = sorted(nlist, key=lambda data: data['mtime'], reverse=get["sort"] == "desc") + # return nlist + return public.return_message(0, 0, nlist) + + + + def restart_services(self): + """ + @重启服务 + """ + public.ExecShell('net stop redis') + public.ExecShell('net start redis') + return True + + + def check_cloud_database_status(self,conn_config): + """ + @检测远程数据库是否连接 + @conn_config 远程数据库配置,包含host port pwd等信息 + """ + try: + + sql_obj = panelRedisDB().set_host(conn_config['db_host'],conn_config['db_port'],conn_config['db_name'],conn_config['db_user'],conn_config['db_password']) + keynum = sql_obj.redis_conn(0).dbsize() + # return True + return public.return_message(0, 0, True) + except Exception as ex: + + # return public.returnMsg(False,ex) + return public.return_message(-1, 0, ex) diff --git a/class_v2/databaseModelV2/sqliteModel.py b/class_v2/databaseModelV2/sqliteModel.py new file mode 100644 index 00000000..5cbd3172 --- /dev/null +++ b/class_v2/databaseModelV2/sqliteModel.py @@ -0,0 +1,22 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# sqlite模型 +#------------------------------ +import os,sys,re,json,shutil,psutil,time +from databaseModelV2.base import databaseBase +import public + + +class main(databaseBase): + + def get_list(self,args): + + return [] diff --git a/class_v2/databaseModelV2/sqlserverModel.py b/class_v2/databaseModelV2/sqlserverModel.py new file mode 100644 index 00000000..4a2603d2 --- /dev/null +++ b/class_v2/databaseModelV2/sqlserverModel.py @@ -0,0 +1,667 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# sqlite模型 +#------------------------------ +import os,re,json,time +from databaseModelV2.base import databaseBase + + +# import public,panelMssql +import public +import panel_mssql_v2 as panelMssql + + +from public.validate import Param +try: + from BTPanel import session +except :pass + + +class main(databaseBase): + + def get_list(self,args): + """ + @获取数据库列表 + @sql_type = sqlserver + """ + # {"table":"databases","search":"","limit":"10","p":1,"order":"username desc"} + # 校验参数 + try: + args.validate([ + Param('table').Require().String().Xss(), + Param('search').String(), + Param('order').String().Xss(), + Param('limit').Integer(), + Param('p').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # return self.get_base_list(args, sql_type = 'sqlserver') + return public.return_message(0, 0, self.get_base_list(args, sql_type = 'sqlserver')) + + + def get_mssql_obj_by_sid(self,sid = 0,conn_config = None): + """ + @取mssql数据库对像 By sid + @sid 数据库分类,0:本地 + """ + if type(sid) == str: + try: + sid = int(sid) + except :sid = 0 + + if sid: + if not conn_config: conn_config = public.M('database_servers').where("id=?" ,sid).find() + db_obj = panelMssql.panelMssql() + + try: + db_obj = db_obj.set_host(conn_config['db_host'],conn_config['db_port'],None,conn_config['db_user'],conn_config['db_password']) + except Exception as e: + raise public.PanelError(e) + else: + db_obj = panelMssql.panelMssql() + return db_obj + + def get_mssql_obj(self,db_name): + """ + @取mssql数据库对象 + @db_name 数据库名称 + """ + is_cloud_db = False + if db_name: + db_find = public.M('databases').where("name=?" ,db_name).find() + if db_find['sid']: + return self.get_mssql_obj_by_sid(db_find['sid']) + is_cloud_db = db_find['db_type'] in ['1',1] + + if is_cloud_db: + + db_obj = panelMssql.panelMssql() + conn_config = json.loads(db_find['conn_config']) + try: + db_obj = db_obj.set_host(conn_config['db_host'],conn_config['db_port'],conn_config['db_name'],conn_config['db_user'],conn_config['db_password']) + except Exception as e: + raise public.PanelError(e) + else: + db_obj = panelMssql.panelMssql() + return db_obj + + def GetCloudServer(self,args): + ''' + @name 获取远程服务器列表 + @author hwliang<2021-01-10> + @return list + ''' + # {"type":"sqlserver"} + # 校验参数 + try: + args.validate([ + + Param('type').Require().String('in', ['sqlserver']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # return self.GetBaseCloudServer(args) + return public.return_message(0, 0, self.GetBaseCloudServer(args)) + + + def AddCloudServer(self,args): + ''' + @添加远程数据库 + ''' + + return self.AddBaseCloudServer(args) + # return public.return_message(0, 0, self.AddBaseCloudServer(args)) + + def RemoveCloudServer(self,args): + ''' + @删除远程数据库 + ''' + return self.RemoveBaseCloudServer(args) + # return public.return_message(0, 0, self.RemoveBaseCloudServer(args)) + + def ModifyCloudServer(self,args): + ''' + @修改远程数据库 + ''' + return self.ModifyBaseCloudServer(args) + # return public.return_message(0, 0, self.ModifyBaseCloudServer(args)) + + def AddDatabase(self,args): + """ + @添加数据库 + """ + # {"name":"test1","db_user":"test1","password":"nnMCp2ccJcBahJec","sid":"3","active":true,"ps":"test1","ssl":"REQUIRE SSL"} + # 校验参数 + try: + args.validate([ + Param('name').Require().String(), + Param('db_user').Require().String(), + Param('password').Require().String().Xss(), + Param('sid').Require().Integer(), + Param('active').Require().Bool(), + Param('ps').String(), + Param('ssl').String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + try: + res = self.add_base_database(args) + if not res['status']: + # return res + return public.return_message(-1, 0, res['msg']) + + data_name = res['data_name'] + username = res['username'] + password = res['data_pwd'] + + if re.match(r"^\d+",data_name): + # return public.returnMsg(False,'SQLServer databases cannot start with numbers!') + return public.return_message(-1, 0, 'SQLServer databases cannot start with numbers!') + + reg_count = 0 + regs = ['[a-z]','[A-Z]',r'\W','[0-9]'] + for x in regs: + if re.search(x,password): reg_count += 1 + + if len(password) < 8 or len(password) >128 or reg_count < 3 : + # return public.returnMsg(False,'SQLServer password complexity policy does not match, should be 8-128 characters, and contain any 3 of them in upper case, lower case, digits, special symbols!') + return public.return_message(-1, 0, 'SQLServer password complexity policy does not match, should be 8-128 ' + 'characters, and contain any 3 of them in upper case, lower case, ' + 'digits, special symbols!') + + try: + self.sid = int(args['sid']) + except : + self.sid = 0 + + dtype = 'SQLServer' + #添加SQLServer + mssql_obj = self.get_mssql_obj_by_sid(self.sid) + result = mssql_obj.execute("CREATE DATABASE %s" % data_name) + isError = self.IsSqlError(result) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + + mssql_obj.execute("DROP LOGIN %s" % username) + + #添加用户 + self.__CreateUsers(data_name,username,password,'127.0.0.1') + + if not hasattr(args,'ps'): args['ps'] = public.getMsg('INPUT_PS'); + addTime = time.strftime('%Y-%m-%d %X',time.localtime()) + + pid = 0 + if hasattr(args,'pid'): pid = args.pid + + if hasattr(args,'contact'): + site = public.M('sites').where("id=?",(args.contact,)).field('id,name').find() + if site: + pid = int(args.contact) + args['ps'] = site['name'] + + db_type = 0 + if self.sid: db_type = 2 + + public.set_module_logs('linux_sqlserver','AddDatabase',1) + #添加入SQLITE + public.M('databases').add('pid,sid,db_type,name,username,password,accept,ps,addtime,type',(pid,self.sid,db_type,data_name,username,password,'127.0.0.1',args['ps'],addTime,dtype)) + public.WriteLog("TYPE_DATABASE", 'DATABASE_ADD_SUCCESS',(data_name,)) + # return public.returnMsg(True,'ADD_SUCCESS') + return public.return_message(0, 0, 'ADD_SUCCESS') + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + def DeleteDatabase(self,args): + """ + @删除数据库 + """ + # {"id":128,"name":"失去了23"} + # 校验参数 + try: + args.validate([ + Param('name').Require().String(), + Param('id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = args['id'] + find = public.M('databases').where("id=?",(id,)).field('id,pid,name,username,password,type,accept,ps,addtime,sid,db_type').find(); + if not find: + # return public.returnMsg(False,'The specified database does not exist.') + return public.return_message(-1, 0, 'The specified database does not exist.') + + name = args['name'] + username = find['username'] + + mssql_obj = self.get_mssql_obj_by_sid(find['sid']) + mssql_obj.execute("ALTER DATABASE %s SET SINGLE_USER with ROLLBACK IMMEDIATE" % name) + result = mssql_obj.execute("DROP DATABASE %s" % name) + + if self.get_database_size_by_id(find['sid']): + isError = self.IsSqlError(result) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + + mssql_obj.execute("DROP LOGIN %s" % username) + + #删除SQLITE + public.M('databases').where("id=?",(id,)).delete() + public.WriteLog("TYPE_DATABASE", 'DATABASE_DEL_SUCCESS',(name,)) + # return public.returnMsg(True, 'DEL_SUCCESS') + return public.return_message(0, 0, 'DEL_SUCCESS') + + + + def ToBackup(self,args): + """ + @备份数据库 id 数据库id + """ + + + id = args['id'] + find = public.M('databases').where("id=?",(id,)).find() + if not find: + # return public.returnMsg(False,'Database does not exist!') + return public.return_message(-1, 0, 'Database does not exist!') + + self.CheckBackupPath(args) + + fileName = find['name'] + '_' + time.strftime('%Y%m%d_%H%M%S',time.localtime()) + '.bak' + backupName = session['config']['backup_path'] + '/database/sqlserver/' + fileName + + mssql_obj = self.get_mssql_obj_by_sid(find['sid']) + + if not int(find['sid']): + ret = mssql_obj.execute("backup database %s To disk='%s'" % (find['name'],backupName)) + isError=self.IsSqlError(ret) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + else: + #远程数据库 + # return public.returnMsg(False,'Operation failed. Remote database cannot be backed up.') + return public.return_message(-1, 0, 'Operation failed. Remote database cannot be backed up.') + + if not os.path.exists(backupName): + # return public.returnMsg(False,'BACKUP_ERROR') + return public.return_message(-1, 0, 'BACKUP_ERROR') + + public.M('backup').add('type,name,pid,filename,size,addtime',(1,fileName,id,backupName,0,time.strftime('%Y-%m-%d %X',time.localtime()))) + public.WriteLog("TYPE_DATABASE", "DATABASE_BACKUP_SUCCESS",(find['name'],)) + + if os.path.getsize(backupName) < 2048: + # return public.returnMsg(True, 'The backup file size is smaller than 2Kb. Check the backup integrity.') + return public.return_message(0, 0, 'The backup file size is smaller than 2Kb. Check the backup integrity.') + else: + # return public.returnMsg(True, 'BACKUP_SUCCESS') + return public.return_message(0, 0, 'BACKUP_SUCCESS') + + def DelBackup(self,args): + """ + @删除备份文件 + """ + return self.delete_base_backup(args) + # return public.return_message(0, 0, self.delete_base_backup(args)) + + + #导入 + def InputSql(self,get): + # 校验参数 + try: + get.validate([ + Param('file').SafePath(), # 文件路径 + Param('name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + name = get.name + file = get.file + + find = public.M('databases').where("name=?",(name,)).find() + if not find: + # return public.returnMsg(False,'Database does not exist!') + return public.return_message(-1, 0, 'Database does not exist!') + + tmp = file.split('.') + exts = ['sql','zip','bak'] + ext = tmp[len(tmp) -1] + if ext not in exts: + # return public.returnMsg(False, 'DATABASE_INPUT_ERR_FORMAT') + return public.return_message(-1, 0, 'DATABASE_INPUT_ERR_FORMAT') + + backupPath = session['config']['backup_path'] + '/database' + + if ext == 'zip': + try: + fname = os.path.basename(file).replace('.zip','') + dst_path = backupPath + '/' +fname + if not os.path.exists(dst_path): os.makedirs(dst_path) + + public.unzip(file,dst_path) + for x in os.listdir(dst_path): + if x.find('bak') >= 0 or x.find('sql') >= 0: + file = dst_path + '/' + x + break + except : + # return public.returnMsg(False,'The import failed because the file is not a valid ZIP file.') + return public.return_message(-1, 0, 'The import failed because the file is not a valid ZIP file.') + + mssql_obj = self.get_mssql_obj_by_sid(find['sid']) + data = mssql_obj.query("use %s ;select filename from sysfiles" % find['name']) + + isError = self.IsSqlError(data) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + if type(data) == str: + # return public.returnMsg(False,data) + return public.return_message(-1, 0, data) + + mssql_obj.execute("ALTER DATABASE %s SET OFFLINE WITH ROLLBACK IMMEDIATE" % (find['name'])) + mssql_obj.execute("use master;restore database %s from disk='%s' with replace, MOVE N'%s' TO N'%s',MOVE N'%s_Log' TO N'%s' " % (find['name'],file,find['name'],data[0][0],find['name'],data[1][0])) + mssql_obj.execute("ALTER DATABASE %s SET ONLINE" % (find['name'])) + + public.WriteLog("TYPE_DATABASE", 'Description Succeeded in importing database [{}]'.format(name)) + # return public.returnMsg(True, 'DATABASE_INPUT_SUCCESS') + return public.return_message(0, 0, 'DATABASE_INPUT_SUCCESS') + + + #同步数据库到服务器 + def SyncToDatabases(self,get): + + # 校验? + + type = int(get['type']) + n = 0 + sql = public.M('databases') + if type == 0: + data = sql.field('id,name,username,password,accept,type,sid,db_type').where('type=?',('SQLServer',)).select() + + for value in data: + if value['db_type'] in ['1',1]: + continue # 跳过远程数据库 + result = self.ToDataBase(value) + if result == 1: n +=1 + else: + import json + data = json.loads(get.ids) + for value in data: + find = sql.where("id=?",(value,)).field('id,name,username,password,sid,db_type,accept,type').find() + result = self.ToDataBase(find) + if result == 1: n +=1 + + if n == 1: + # return public.returnMsg(True, 'Synchronization succeeded') + return public.return_message(0, 0, 'Synchronization succeeded') + + elif n == 0: + # return public.returnMsg(False,'Sync failed') + return public.return_message(-1, 0, 'Sync failed') + + # return public.returnMsg(True,'DATABASE_SYNC_SUCCESS',(str(n),)) + return public.return_message(0, 0, 'DATABASE_SYNC_SUCCESS'.format(str(n))) + + #添加到服务器 + def ToDataBase(self,find): + + if find['username'] == 'bt_default': + # return 0 + return public.return_message(0, 0, 0) + if len(find['password']) < 3 : + find['username'] = find['name'] + find['password'] = public.md5(str(time.time()) + find['name'])[0:10] + public.M('databases').where("id=?",(find['id'],)).save('password,username',(find['password'],find['username'])) + + self.sid = find['sid'] + mssql_obj = self.get_mssql_obj_by_sid(self.sid) + result = mssql_obj.execute("CREATE DATABASE %s" % find['name']) + isError = self.IsSqlError(result) + if isError != None and not 'already exists' in result: + # return -1 + return public.return_message(0, 0, -1) + + self.__CreateUsers(find['name'],find['username'],find['password'],'127.0.0.1') + + # return 1 + return public.return_message(0, 0, 1) + + + #从服务器获取数据库 + def SyncGetDatabases(self,get): + + n = 0 + s = 0 + db_type = 0 + self.sid = get.get('sid/d',0) + if self.sid: db_type = 2 + + mssql_obj = self.get_mssql_obj_by_sid(self.sid) + + data = mssql_obj.query('SELECT name FROM MASTER.DBO.SYSDATABASES ORDER BY name') + isError = self.IsSqlError(data) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + if type(data) == str: + # return public.returnMsg(False,data) + return public.return_message(-1, 0, data) + + sql = public.M('databases') + nameArr = ['information_schema','performance_schema','mysql','sys','master','model','msdb','tempdb','ReportServerTempDB','YueMiao','ReportServer'] + for item in data: + dbname = item[0] + if sql.where("name=?",(dbname,)).count(): continue + if not dbname in nameArr: + if sql.table('databases').add('name,username,password,accept,ps,addtime,type,sid,db_type',(dbname,dbname,'','',public.getMsg('INPUT_PS'),time.strftime('%Y-%m-%d %X',time.localtime()),'SQLServer',self.sid,db_type)): n +=1 + + # return public.returnMsg(True,'DATABASE_GET_SUCCESS',(str(n),)) + return public.return_message(0, 0, 'DATABASE_GET_SUCCESS'.format(n)) + + def ResDatabasePassword(self,args): + """ + @修改用户密码 + """ + # 校验参数 + try: + args.validate([ + Param('password').Require().String().Xss(), + Param('name').Require().String(), + Param('id').Require().Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + id = args['id'] + username = args['name'].strip() + newpassword = public.trim(args['password']) + + try: + # if not newpassword: + # return public.returnMsg(False, 'The password of database [' + username + '] cannot be empty.') + if len(re.search(r"^[\w@\.]+$", newpassword).groups()) > 0: + # return public.returnMsg(False, 'The database password cannot be empty or contain special characters') + return public.return_message(-1, 0, 'The database password cannot be empty or contain special characters') + except : + # return public.returnMsg(False, 'The database password cannot be empty or contain special characters') + return public.return_message(-1, 0, 'The database password cannot be empty or contain special characters') + + find = public.M('databases').where("id=?",(id,)).field('id,pid,name,username,password,type,accept,ps,addtime,sid').find() + if not find: + # return public.returnMsg(False, 'Modify the failure,The specified database does not exist.'); + return public.return_message(-1, 0, 'Modify the failure,The specified database does not exist.') + + mssql_obj = self.get_mssql_obj_by_sid(find['sid']) + mssql_obj.execute("EXEC sp_password NULL, '%s', '%s'" % (newpassword,username)) + + #修改SQLITE + public.M('databases').where("id=?",(id,)).setField('password',newpassword) + + public.WriteLog("TYPE_DATABASE",'DATABASE_PASS_SUCCESS',(find['name'],)) + # return public.returnMsg(True, 'DATABASE_PASS_SUCCESS',(find['name'],)) + return public.return_message(0, 0, 'DATABASE_PASS_SUCCESS'.format(find['name'])) + + + def get_root_pwd(self,args): + """ + @获取sa密码 + """ + mssql_obj = panelMssql.panelMssql() + ret = mssql_obj.get_sql_name() + if not ret : return public.returnMsg(False, 'The SQL Server is not installed or started. Install or start it first') + + sa_path = '{}/data/sa.pl'.format(public.get_panel_path()) + if os.path.exists(sa_path): + password = public.readFile(sa_path) + return public.returnMsg(True,password) + return public.returnMsg(True,'') + + + def set_root_pwd(self,args): + """ + @设置sa密码 + """ + password = public.trim(args['password']) + try: + if not password: + return public.returnMsg(False, 'The password of database [' + username + '] cannot be empty.') + if len(re.search(r"^[\w@\.]+$", password).groups()) > 0: + return public.returnMsg(False, 'saThe password cannot be empty or have special symbols') + except : + return public.returnMsg(False, 'saThe password cannot be empty or have special symbols') + + mssql_obj = panelMssql.panelMssql() + result = mssql_obj.execute("EXEC sp_password NULL, '%s', 'sa'" % password) + + isError = self.IsSqlError(result) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + + public.writeFile('data/sa.pl',password) + session['config']['mssql_sa'] = password + return public.returnMsg(True,'The password of sa is changed successfully.') + + + + def get_database_size_by_id(self,args): + """ + @获取数据库尺寸(批量删除验证) + @args json/int 数据库id + """ + total = 0 + db_id = args + if not isinstance(args,int): db_id = args['db_id'] + + try: + name = public.M('databases').where('id=?',db_id).getField('name') + mssql_obj = self.get_mssql_obj(name) + tables = mssql_obj.query("select name,size,type from sys.master_files where type=0 and name = '{}'".format(name)) + + total = tables[0][1] + if not total: total = 0 + except :pass + + return total + + def check_del_data(self,args): + """ + @删除数据库前置检测 + """ + # {"ids": "[128]"} + + # return self.check_base_del_data(args) + return public.return_message(0, 0, self.check_base_del_data(args)) + + #本地创建数据库 + def __CreateUsers(self,data_name,username,password,address): + """ + @创建数据库用户 + """ + mssql_obj = self.get_mssql_obj_by_sid(self.sid) + mssql_obj.execute("use %s create login %s with password ='%s' , default_database = %s" % (data_name,username,password,data_name)) + mssql_obj.execute("use %s create user %s for login %s with default_schema=dbo" % (data_name,username,username)) + mssql_obj.execute("use %s exec sp_addrolemember 'db_owner','%s'" % (data_name,data_name)) + mssql_obj.execute("ALTER DATABASE %s SET MULTI_USER" % data_name) + + + #检测备份目录并赋值权限(MSSQL需要Authenticated Users) + def CheckBackupPath(self,get): + backupFile = session['config']['backup_path'] + '/database/sqlserver' + if not os.path.exists(backupFile): + os.makedirs(backupFile) + get.filename = backupFile + get.user = 'Authenticated Users' + get.access = 2032127 + import files + files.files().SetFileAccess(get) + + def check_cloud_database_status(self,conn_config): + """ + @检测远程数据库是否连接 + @conn_config 远程数据库配置,包含host port pwd等信息 + """ + try: + + import panelMssql + if not 'db_name' in conn_config: conn_config['db_name'] = None + sql_obj = panelMssql.panelMssql().set_host(conn_config['db_host'],conn_config['db_port'],conn_config['db_name'],conn_config['db_user'],conn_config['db_password']) + data = sql_obj.query("SELECT name FROM MASTER.DBO.SYSDATABASES ORDER BY name") + + isError = self.IsSqlError(data) + if isError != None: + return isError + # return public.return_message(-1, 0, isError) + if type(data) == str: + # return public.returnMsg(False,data) + return public.return_message(-1, 0, data) + + if not conn_config['db_name']: + # return True + return public.return_message(0, 0, True) + for i in data: + if i[0] == conn_config['db_name']: + # return True + return public.return_message(0, 0, True) + # return public.returnMsg(False,'The specified database does not exist!') + return public.return_message(-1, 0, 'The specified database does not exist!') + except Exception as ex: + + # return public.returnMsg(False,ex) + return public.return_message(-1, 0, ex) diff --git a/class_v2/database_v2.py b/class_v2/database_v2.py new file mode 100644 index 00000000..8d019258 --- /dev/null +++ b/class_v2/database_v2.py @@ -0,0 +1,2187 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------ +# 数据库管理类 +# ------------------------------ +import os +import time +import json +import re +import sys + +import public, db, panelMysql +import datatool_v2 as datatool +import db_mysql +from public.validate import Param + +class database(datatool.datatools): + _MYSQL_CNF = "/etc/my.cnf" + _DB_BACKUP_DIR = os.path.join(public.M("config").where("id=?", (1,)).getField("backup_path"), "database") + _MYSQL_BACKUP_DIR = os.path.join(_DB_BACKUP_DIR, "mysql") + _MYSQLDUMP_BIN = public.get_mysqldump_bin() + _MYSQL_BIN = public.get_mysql_bin() + + sqlite_connection = None + + def __init__(self): + if not os.path.exists(self._MYSQL_BACKUP_DIR): + os.makedirs(self._MYSQL_BACKUP_DIR) + + sid = 0 + + # mysql 在使用 + def AddCloudServer(self, get): + ''' + @name 添加远程服务器 + @author hwliang<2021-01-10> + @param db_host 服务器地址 + @param db_port 数据库端口 + @param db_user 用户名 + @param db_password 数据库密码 + @param db_ps 数据库备注 + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('db_host').Require().Ip(), + Param('db_port').Require().Number(">=", 1).Number("<=", 65535), + Param('db_user').Require().String().Xss(), + Param('db_password').Require().String().Xss(), + Param('db_ps').Require().String(), + Param('type').Require().String().Xss(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # if not hasattr(get, 'db_host'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_port'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_user'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_password'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_ps'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + get.db_name = None + res = self.CheckCloudDatabase(get) + if isinstance(res, dict): + # return res + info1 = res.get("msg", "Database unable to connect!") + return public.return_message(-1, 0, info1) + # return public.return_message(-1, 0, "Database unable to connect!") + if public.M('database_servers').where('db_host=? AND db_port=?', (get.db_host, get.db_port)).count(): + # return public.return_msg_gettext(False, 'The specified server already exists: [{}:{}]', (get.db_host, get.db_port)) + return public.return_message(-1, 0, 'The specified server already exists: [{}:{}]'.format(get.db_host, get.db_port)) + + get.db_port = int(get.db_port) + pdata = { + 'db_host': get.db_host, + 'db_port': get.db_port, + 'db_user': get.db_user, + 'db_password': get.db_password, + 'ps': public.xssencode2(get.db_ps.strip()), + 'addtime': int(time.time()) + } + + result = public.M("database_servers").insert(pdata) + + if isinstance(result, int): + public.write_log_gettext('Database manager', 'Add remote MySQL server [{}:{}]', (get.db_host, get.db_port)) + # return public.return_msg_gettext(True, 'Setup successfully!') + return public.return_message(0, 0, 'Setup successfully!') + # return public.return_msg_gettext(False, 'Add failed: {}', (result,)) + return public.return_message(-1, 0, 'Add failed: {}', (result,)) + + def GetCloudServer(self, get): + ''' + @name 获取远程服务器列表 + @author hwliang<2021-01-10> + @return list + ''' + data = public.M('database_servers').where("db_type=?", ("mysql",)).select() + bt_mysql_bin = '{}/mysql/bin/mysql'.format(public.get_setup_path()) + + if not isinstance(data, list): data = [] + if os.path.exists(bt_mysql_bin): + data.insert(0, {'id': 0, 'db_host': '127.0.0.1', 'db_port': 3306, 'db_user': 'root', 'db_password': '', + 'ps': 'LocalServer', 'addtime': 0}) + # return data + return public.return_message(0, 0, data) + + def RemoveCloudServer(self, get): + ''' + @name 删除远程服务器 + @author hwliang<2021-01-10> + @param id 远程服务器ID + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + id = int(get.id) + # if not id: + # return public.return_msg_gettext(False, 'Parameter ERROR!') + db_find = public.M('database_servers').where('id=?', (id,)).find() + if not db_find: + # return public.returnMsg(False, 'The specified server dose not exists!') + return public.return_message(-1, 0, 'The specified server dose not exists!') + public.M('databases').where('sid=?', id).delete() + result = public.M('database_servers').where('id=?', id).delete() + if isinstance(result, int): + public.WriteLog('Database manager', 'Delete the remote MySQL server [{}:{}]', + (db_find['db_host'], int(db_find['db_port']))) + # return public.return_msg_gettext(True, 'Successfully deleted!') + return public.return_message(0, 0, 'Successfully deleted!') + # return public.return_msg_gettext(False, 'Failed to delete: {}', (result,)) + return public.return_message(-1, 0, 'Failed to delete: {}', (result,)) + + def ModifyCloudServer(self, get): + ''' + @name 修改远程服务器 + @author hwliang<2021-01-10> + @param id 远程服务器ID + @param db_host 服务器地址 + @param db_port 数据库端口 + @param db_user 用户名 + @param db_password 数据库密码 + @param db_ps 数据库备注 + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('db_host').Require().Ip(), + Param('db_port').Require().Number(">=", 1).Number("<=", 65535), + Param('db_user').Require().String().Xss(), + Param('db_password').Require().String().Xss(), + Param('db_ps').Require().String(), + Param('id').Require().Integer().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + # if not hasattr(get, 'id'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_host'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_port'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_user'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_password'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_ps'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + + id = int(get.id) + get.db_port = int(get.db_port) + db_find = public.M('database_servers').where('id=?', (id,)).find() + if not db_find: + # return public.return_msg_gettext(False, 'The specified server dose not exists!') + return public.return_message(-1, 0, 'The specified server dose not exists!') + _modify = False + if db_find['db_host'] != get.db_host or db_find['db_port'] != get.db_port: + _modify = True + if public.M('database_servers').where('db_host=? AND db_port=?', (get.db_host, get.db_port)).count(): + # return public.return_msg_gettext(False, 'The specified server already exists: [{}:{}]',(get.db_host, get.db_port)) + return public.return_message(-1, 0, 'The specified server already exists: [{}:{}]'.format(get.db_host, get.db_port)) + + if db_find['db_user'] != get.db_user or db_find['db_password'] != get.db_password: + _modify = True + + if _modify: + res = self.CheckCloudDatabase(get) + if isinstance(res, dict): + # return res + info1 = res.get("msg", "Database unable to connect") + return public.return_message(-1, 0, info1) + + pdata = { + 'db_host': get.db_host, + 'db_port': get.db_port, + 'db_user': get.db_user, + 'db_password': get.db_password, + 'ps': public.xssencode2(get.db_ps.strip()) + } + + result = public.M("database_servers").where('id=?', (id,)).update(pdata) + if isinstance(result, int): + public.WriteLog('Database manager', 'Edit remote MySQL server [{}:{}]', (get.db_host, get.db_port)) + # return public.return_msg_gettext(True, 'Setup successfully!') + return public.return_message(0, 0, 'Setup successfully!') + # return public.return_msg_gettext(False, 'Fail to edit: {}', (result)) + return public.return_message(-1, 0, 'Fail to edit: {}'.format(result)) + + def AddCloudDatabase(self, get): + ''' + @name 添加远程数据库 + @author hwliang<2022-01-06> + @param db_host 服务器地址 + @param db_port 数据库端口 + @param db_user 用户名 + @param db_name 数据库名称 + @param db_password 数据库密码 + @param db_ps 数据库备注 + @return dict + ''' + + # 校验参数 + try: + get.validate([ + Param('db_host').Require().Ip(), + Param('db_port').Require().Number(">=", 1).Number("<=", 65535), + Param('db_user').Require().String(), + Param('db_password').Require().String(), + Param('db_name').Require().String(), + Param('db_ps').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # if not hasattr(get, 'db_host'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_port'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_user'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_name'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_password'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + # if not hasattr(get, 'db_ps'): + # return public.return_msg_gettext(False, 'Parameter ERROR!') + + # 检查数据库是否能连接 + res = self.CheckCloudDatabase(get) + if isinstance(res, dict): + # return res + info1 = res.get("msg", "Database unable to connect") + return public.return_message(-1, 0, info1) + + if public.M('databases').where('name=?', (get.db_name,)).count(): + # return public.return_msg_gettext(False, "A database with the same name already exists: [{}]",(get.db_name,)) + return public.return_msg_gettext(False, "A database with the same name already exists: [{}]".format(get.db_name)) + get.db_port = int(get.db_port) + conn_config = { + 'db_host': get.db_host, + 'db_port': get.db_port, + 'db_user': get.db_user, + 'db_password': get.db_password, + 'db_name': get.db_name + } + + pdata = { + 'name': get.db_name, + 'ps': get.db_ps, + 'conn_config': json.dumps(conn_config), + 'db_type': '1', + 'username': get.db_user, + 'password': get.db_password, + 'accept': '127.0.0.1', + 'addtime': time.strftime('%Y-%m-%d %X', time.localtime()), + 'pid': 0 + } + + result = public.M('databases').insert(pdata) + if isinstance(result, int): + public.write_log_gettext('Database manager', 'Add remote MySQL database [{}] successfully', (get.db_name,)) + # return public.return_msg_gettext(True, 'Setup successfully!') + return public.return_message(0, 0, 'Setup successfully!') + # return public.return_msg_gettext(False, 'Add failed: {}', (result,)) + return public.return_message(-1, 0, 'Add failed: {}', (result,)) + + def CheckCloudDatabase(self, conn_config): + ''' + @name 检查远程数据库信息是否正确 + @author hwliang<2022-01-06> + @param conn_config 连接信息 + db_host 服务器地址 + db_port 数据库端口 + db_user 用户名 + db_name 数据库名称 + db_password 数据库密码 + @return True / dict + ''' + try: + if not 'db_name' in conn_config: conn_config['db_name'] = None + mysql_obj = db_mysql.panelMysql() + mysql_obj.set_host(conn_config['db_host'], conn_config['db_port'], conn_config['db_name'], + conn_config['db_user'], conn_config['db_password']) + result = mysql_obj.query("show databases") + if isinstance(result, str): + if mysql_obj._ex: + return public.returnMsg(False, self.GetMySQLError(mysql_obj._ex)) + else: + return public.returnMsg(False, self.GetMySQLError(result)) + if not conn_config['db_name']: return True + for i in result: + if i[0] == conn_config['db_name']: + return True + return public.returnMsg(False, 'The specified database does not exist!') + except Exception as ex: + res = self.GetMySQLError(ex) + if not res: res = str(ex) + return public.returnMsg(False, res) + + def GetMySQLError(self, e): + if isinstance(e, str): + return e + res = '' + if e.args[0] == 1045: + res = public.gettext_msg("Wrong user name or password!") + if e.args[0] == 1049: + res = public.gettext_msg("Database does NOT exist!") + if e.args[0] == 1044: + res = public.gettext_msg("No access rights, or the database does not exist!") + if e.args[0] == 1062: + res = public.gettext_msg("Database exists!") + if e.args[0] == 1146: + res = public.gettext_msg('Database table does not exist!') + if e.args[0] == 2003: + res = public.gettext_msg('Fail to connect to the server!') + if res: + res = res + "
                                    " + str(e) + "
                                    " + else: + res = str(e) + return res + + # 检查mysql是否存在空用户密码 + def _check_empty_user_passwd(self): + mysql_obj = panelMysql.panelMysql() + mysql_obj.execute("delete from mysql.user where user='' and password=''") + + # 添加数据库 + def AddDatabase(self, get): + try: + # 校验参数 + try: + get.validate([ + # Param('name').Require().String('in', ['root', 'mysql', 'test', 'sys', 'panel_logs']), + # Param('db_user').Require().String('in', ['root', 'mysql', 'test', 'sys', 'panel_logs']), + Param('name').Require().String(), + Param('db_user').Require().String().Xss(), + Param('codeing').Require().String().Xss(), + Param('password').Require().String(), + # Param('dataAccess').Require().Ip(), + Param('sid').Require().Integer(), + Param('active').Require().Bool(), + Param('address').Require(), + Param('ps').Require().String(), + Param('ssl').String().Xss(), + Param('dtype').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # try: + self._check_empty_user_passwd() + ssl = "" + if hasattr(get, "ssl"): + ssl = get.ssl + if ssl == "REQUIRE SSL" and not self.check_mysql_ssl_status(get): + # return public.return_msg_gettext(False,'SSL is not enabled in the database, please open it in the Mysql manager first') + return public.return_message(-1, 0, "SSL is not enabled in the database, please open it in the Mysql manager first") + data_name = get['name'].strip().lower() + # if not data_name: + # return public.return_msg_gettext(False, 'The database name cannot be empty') + if self.CheckRecycleBin(data_name): + # return public.return_msg_gettext(False,'Database [{}] already at the recycle bin, please recover from the recycle bin!', + # (data_name,)) + return public.return_message(-1, 0, "Database [{}] already at the recycle bin, please recover from the recycle bin!".format(data_name)) + if len(data_name) > 64: + # return public.return_msg_gettext(False,'Database name cannot be more than 16 characters!') + return public.return_message(-1, 0, "Database name cannot be more than 16 characters") + reg = r"^[\w\.-]+$" + username = get.db_user.strip() + # if not username: + # return public.return_msg_gettext(False, 'The database user name cannot be empty') + if not re.match(reg, data_name): + # return public.return_msg_gettext(False,'Database name cannot contain special characters!') + return public.return_message(-1, 0, "Database name cannot contain special characters") + + if not re.match(reg, username): + # return public.return_msg_gettext(False, 'Database name is illegal!') + return public.return_message(-1, 0, "Database name is illegal") + if not hasattr(get, 'db_user'): + get.db_user = data_name + + checks = ['root', 'mysql', 'test', 'sys', 'panel_logs'] + if username in checks or len(username) < 1: + return public.return_msg_gettext(False, 'Database username is illegal!') + if data_name in checks or len(data_name) < 1: + return public.return_msg_gettext(False, 'Database name is illegal!') + data_pwd = get['password'] + if len(data_pwd) < 1: + data_pwd = public.md5(str(time.time()))[0:16] + + sql = public.M('databases') + if sql.where("name=?", (data_name)).count(): + # return public.return_msg_gettext(False, 'Database exists!') + return public.return_message(-1, 0, 'Database exists!') + if sql.where("username=?", (username)).count(): + # return public.return_msg_gettext(False,'The user name already exists. For security reasons, we do not allow one database user to manage multiple databases') + return public.return_message(-1, 0, 'The user name already exists. For security reasons, we do not allow ' + 'one database user to manage multiple databases') + address = get['address'].strip() + if address in ['', 'ip']: + # return public.return_msg_gettext(False,'If the access permission is [Specified IP], you need to enter the IP address!') + return public.return_message(-1, 0, 'If the access permission is [Specified IP], ' + 'you need to enter the IP address!') + + user = '是' + password = data_pwd + + codeing = get['codeing'] + + wheres = { + 'utf8': 'utf8_general_ci', + 'utf8mb4': 'utf8mb4_general_ci', + 'gbk': 'gbk_chinese_ci', + 'big5': 'big5_chinese_ci' + } + codeStr = wheres[codeing] + # 添加MYSQL + self.sid = get.get('sid/d', 0) + mysql_obj = public.get_mysql_obj_by_sid(self.sid) + if not mysql_obj: + # return public.returnMsg(False, 'Failed to connect to the specified database') + return public.return_message(-1, 0, 'Failed to connect to the specified database') + + # 从MySQL验证是否存在 + if self.database_exists_for_mysql(mysql_obj, data_name): + # return public.return_msg_gettext(False,'The specified database already exists in MySQL, please change the name!') + return public.return_message(-1, 0, 'The specified database already exists in MySQL,please change the name!') + + result = mysql_obj.execute( + "create database `" + data_name + "` DEFAULT CHARACTER SET " + codeing + " COLLATE " + codeStr) + + + isError = self.IsSqlError(result) + if isError != None: + # return isError + return public.return_message(-1, 0, isError) + mysql_obj.execute("drop user '" + username + "'@'localhost'") + for a in address.split(','): + mysql_obj.execute("drop user '" + username + "'@'" + a + "'") + self.__CreateUsers(data_name, username, password, address, ssl) + + if get['ps'] == '': get['ps'] = public.get_msg_gettext('Edit notes') + get['ps'] = public.xssencode2(get['ps']) + addTime = time.strftime('%Y-%m-%d %X', time.localtime()) + + pid = 0 + if hasattr(get, 'pid'): pid = get.pid + # 添加入SQLITE + db_type = 0 + if self.sid: db_type = 2 + sql.add('pid,sid,db_type,name,username,password,accept,ps,addtime', + (pid, self.sid, db_type, data_name, username, password, address, get['ps'], addTime)) + public.write_log_gettext("Database manager", 'Successfully added database [{}]!', (data_name,)) + # return public.return_msg_gettext(True, 'Setup successfully!') + return public.return_message(0, 0, 'Setup successfully!') + # except Exception as ex: + # public.write_log_gettext("Database manager",'Failed to add database [{}]!, {}', (data_name,str(ex))) + # return public.return_msg_gettext(False,'Failed to add') + + except Exception as ex: + public.print_log("error info666: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # 生成mysql证书 + def _create_mysql_ssl(self): + ip = public.readFile("/www/server/panel/data/iplist.txt") + openssl_command = """ +cd /www/server/data +openssl genrsa 2048 > ca-key.pem +openssl req -sha1 -new -x509 -nodes -subj "/C=CA/ST=CA/L=CA/O=CA/OU=CA/CN={ip}BT" -days 3650 -key ca-key.pem > ca.pem +openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -subj "/C=CA/ST=CA/L=CA/O=CA/OU=CA/CN={ip}" -keyout server-key.pem > server-req.pem +openssl rsa -in server-key.pem -out server-key.pem +openssl x509 -sha1 -req -in server-req.pem -days 3650 -CA ca.pem -CAkey ca-key.pem -set_serial 01 > server-cert.pem +openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -subj "/C=CA/ST=CA/L=CA/O=CA/OU=CA/CN={ip}" -keyout client-key.pem > client-req.pem +openssl rsa -in client-key.pem -out client-key.pem +openssl x509 -sha1 -req -in client-req.pem -days 3650 -CA ca.pem -CAkey ca-key.pem -set_serial 01 > client-cert.pem +zip -q ssl.zip client-cert.pem client-key.pem ca.pem +""".format(ip=ip) + public.ExecShell(openssl_command) + + # 写入mysqlssl到配置 + def write_ssl_to_mysql(self, get): + # ssl_conf = """ + # ssl-ca=/www/server/data/ca.pem + # ssl-cert=/www/server/data/server-cert.pem + # ssl-key=/www/server/data/server-key.pem + # """ + ssl_original_path = """ +ssl-ca=/www/server/mysql/mysql-test/std_data/cacert.pem +ssl-cert=/www/server/mysql/mysql-test/std_data//server-cert.pem +ssl-key=/www/server/mysql/mysql-test/std_data/server-key.pem +""" + conf_file = "/etc/my.cnf" + conf = public.readFile(conf_file) + if not conf: + return public.return_msg_gettext(False, 'Configuration file not exist') + if self.check_mysql_ssl_status(get): + reg = "ssl-ca=/www.*\n.*\n.*server-key.pem\n" + conf = re.sub(reg, "", conf) + if os.path.exists('/www/server/mysql/mysql-test/std_data/server-cert.pem'): + conf = re.sub(r'\[mysqld\]', '[mysqld]\nskip_ssl', conf) + public.writeFile(conf_file, conf) + return public.return_msg_gettext(True, 'Setup successfully!') + # create_ssl = None + # for i in ['5.5','5.6','10.1','10.2','10.3']: + # if i not in public.readFile('/www/server/mysql/version_check.pl'): + # continue + # create_ssl = True + # if create_ssl: + # self._create_mysql_ssl() + if "ssl-ca" not in conf: + conf = re.sub(r'\[mysqld\]', '[mysqld]' + ssl_original_path, conf) + conf = re.sub('skip_ssl\n', '', conf) + public.writeFile(conf_file, conf) + # public.ExecShell('chown mysql.mysql /www/server/data/*.pem') + return public.return_msg_gettext(True, 'Open successfully, take effect after manually restarting the database') + + # 检查mysqlssl状态 + def check_mysql_ssl_status(self, get): + mysql_obj = panelMysql.panelMysql() + result = mysql_obj.query("show variables like 'have_ssl';") + if not os.path.exists('/www/server/data/ssl.zip'): + if os.path.exists('/www/server/mysql/mysql-test/std_data/client-cert.pem'): + public.ExecShell( + "cd /www/server/mysql/mysql-test/std_data/ && zip -q /www/server/data/ssl.zip client-cert.pem client-key.pem cacert.pem") + try: + if result and result[0][1] == "YES": + return True + return False + except: + return False + + # 判断数据库是否存在—从MySQL + def database_exists_for_mysql(self, mysql_obj, dataName): + databases_tmp = self.map_to_list(mysql_obj.query('show databases')) + if not isinstance(databases_tmp, list): + return True + + for i in databases_tmp: + if i[0] == dataName: + return True + return False + + # 创建用户 + def __CreateUsers(self, dbname, username, password, address, ssl=None): + mysql_obj = public.get_mysql_obj_by_sid(self.sid) + if not mysql_obj: return public.returnMsg(False, 'Failed to connect to the specified database') + mysql_obj.execute("CREATE USER `%s`@`localhost` IDENTIFIED BY '%s'" % (username, password)) + result = mysql_obj.execute("grant all privileges on `%s`.* to `%s`@`localhost`" % (dbname, username)) + if str(result).find('1044') != -1: + mysql_obj.execute( + "grant SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,INDEX,ALTER,CREATE TEMPORARY TABLES,LOCK TABLES,EXECUTE,CREATE VIEW,SHOW VIEW,EVENT,TRIGGER on `%s`.* to `%s`@`localhost`" % ( + dbname, username)) + if not ssl: + mysql_obj.execute("update mysql.user set ssl_type='' where user='%s' and host='localhost'" % (username)) + for a in address.split(','): + mysql_obj.execute("CREATE USER `%s`@`%s` IDENTIFIED BY '%s'" % (username, a, password)) + result = mysql_obj.execute("grant all privileges on `%s`.* to `%s`@`%s`" % (dbname, username, a)) + if str(result).find('1044') != -1: + mysql_obj.execute( + "grant SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,INDEX,ALTER,CREATE TEMPORARY TABLES,LOCK TABLES,EXECUTE,CREATE VIEW,SHOW VIEW,EVENT,TRIGGER on `%s`.* to `%s`@`%s` %s" % ( + dbname, username, a, ssl)) + mysql_obj.execute("flush privileges") + + # 检查是否在回收站 + def CheckRecycleBin(self, name): + try: + u_name = self.db_name_to_unicode(name) + for n in os.listdir('/www/.Recycle_bin'): + if n.find('BTDB_' + name + '_t_') != -1: return True + if n.find('BTDB_' + u_name + '_t_') != -1: return True + return False + except: + return False + + # 检测数据库执行错误 + def IsSqlError(self, mysqlMsg): + mysqlMsg = str(mysqlMsg) + if "MySQLdb" in mysqlMsg: + return 'MySQLdb component is missing!
                                    Please enter SSH and run the command: pip install mysql-python' + # return public.return_msg_gettext(False,'MySQLdb component is missing!
                                    Please enter SSH and run the command: pip install mysql-python') + if "2002," in mysqlMsg or '2003,' in mysqlMsg: + return 'ERROR to connect database, pls check database status!' + # return public.return_msg_gettext(False,'ERROR to connect database, pls check database status!') + if "using password:" in mysqlMsg: + return 'Mysql root or user password is incorrect, please try to reset!' + # return public.return_msg_gettext(False,'Mysql root or user password is incorrect, please try to reset!') + if "Connection refused" in mysqlMsg: + return 'ERROR to connect database, pls check database status!' + # return public.return_msg_gettext(False,'ERROR to connect database, pls check database status!') + if "1133" in mysqlMsg: + return 'Database user does NOT exist!' + # return public.return_msg_gettext(False, 'Database user does NOT exist!') + if "3679" in mysqlMsg: + return'Slave database deletion failed, data directory does not exist!' + # return public.returnMsg(False,'Slave database deletion failed, data directory does not exist!') + if "libmysqlclient" in mysqlMsg: + self.rep_lnk() + public.ExecShell("pip uninstall mysql-python -y") + public.ExecShell("pip install pymysql") + public.writeFile('data/restart.pl', 'True') + return 'Execution failed, attempted auto repair, please try again later!' + # return public.return_msg_gettext(False, 'Execution failed, attempted auto repair, please try again later!') + return None + + def rep_lnk(self): + shell_cmd = ''' +Setup_Path=/www/server/mysql +#删除软链 +DelLink() +{ + rm -f /usr/bin/mysql* + rm -f /usr/lib/libmysql* + rm -f /usr/lib64/libmysql* + rm -f /usr/bin/myisamchk + rm -f /usr/bin/mysqldump + rm -f /usr/bin/mysql + rm -f /usr/bin/mysqld_safe + rm -f /usr/bin/mysql_config +} +#设置软件链 +SetLink() +{ + ln -sf ${Setup_Path}/bin/mysql /usr/bin/mysql + ln -sf ${Setup_Path}/bin/mysqldump /usr/bin/mysqldump + ln -sf ${Setup_Path}/bin/myisamchk /usr/bin/myisamchk + ln -sf ${Setup_Path}/bin/mysqld_safe /usr/bin/mysqld_safe + ln -sf ${Setup_Path}/bin/mysqlcheck /usr/bin/mysqlcheck + ln -sf ${Setup_Path}/bin/mysql_config /usr/bin/mysql_config + + rm -f /usr/lib/libmysqlclient.so.16 + rm -f /usr/lib64/libmysqlclient.so.16 + rm -f /usr/lib/libmysqlclient.so.18 + rm -f /usr/lib64/libmysqlclient.so.18 + rm -f /usr/lib/libmysqlclient.so.20 + rm -f /usr/lib64/libmysqlclient.so.20 + rm -f /usr/lib/libmysqlclient.so.21 + rm -f /usr/lib64/libmysqlclient.so.21 + + if [ -f "${Setup_Path}/lib/libmysqlclient.so.18" ];then + ln -sf ${Setup_Path}/lib/libmysqlclient.so.18 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.18 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.18 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.20 + elif [ -f "${Setup_Path}/lib/mysql/libmysqlclient.so.18" ];then + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.18 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.18 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.18 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.20 + elif [ -f "${Setup_Path}/lib/libmysqlclient.so.16" ];then + ln -sf ${Setup_Path}/lib/libmysqlclient.so.16 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.16 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.16 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.16 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.16 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.16 /usr/lib64/libmysqlclient.so.20 + elif [ -f "${Setup_Path}/lib/mysql/libmysqlclient.so.16" ];then + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.16 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.16 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.16 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.16 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.16 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.16 /usr/lib64/libmysqlclient.so.20 + elif [ -f "${Setup_Path}/lib/libmysqlclient_r.so.16" ];then + ln -sf ${Setup_Path}/lib/libmysqlclient_r.so.16 /usr/lib/libmysqlclient_r.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient_r.so.16 /usr/lib64/libmysqlclient_r.so.16 + elif [ -f "${Setup_Path}/lib/mysql/libmysqlclient_r.so.16" ];then + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient_r.so.16 /usr/lib/libmysqlclient_r.so.16 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient_r.so.16 /usr/lib64/libmysqlclient_r.so.16 + elif [ -f "${Setup_Path}/lib/libmysqlclient.so.20" ];then + ln -sf ${Setup_Path}/lib/libmysqlclient.so.20 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.20 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.20 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.20 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.20 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.20 /usr/lib64/libmysqlclient.so.20 + elif [ -f "${Setup_Path}/lib/libmysqlclient.so.21" ];then + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib64/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib/libmysqlclient.so.21 + ln -sf ${Setup_Path}/lib/libmysqlclient.so.21 /usr/lib64/libmysqlclient.so.21 + elif [ -f "${Setup_Path}/lib/libmariadb.so.3" ]; then + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib64/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib/libmysqlclient.so.21 + ln -sf ${Setup_Path}/lib/libmariadb.so.3 /usr/lib64/libmysqlclient.so.21 + elif [ -f "${Setup_Path}/lib/mysql/libmysqlclient.so.20" ];then + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.20 /usr/lib/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.20 /usr/lib64/libmysqlclient.so.16 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.20 /usr/lib/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.20 /usr/lib64/libmysqlclient.so.18 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.20 /usr/lib/libmysqlclient.so.20 + ln -sf ${Setup_Path}/lib/mysql/libmysqlclient.so.20 /usr/lib64/libmysqlclient.so.20 + fi +} +DelLink +SetLink +''' + return public.ExecShell(shell_cmd) + + # 删除数据库 + def DeleteDatabase(self, get): + + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + Param('name').Require().String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # try: + id = get['id'] + name = get['name'] + find = public.M('databases').where("id=?", (id,)).field( + 'id,sid,pid,name,username,password,accept,ps,addtime,db_type').find() + if not find: + # return public.return_msg_gettext(False, 'Database [{}] does not exist!'.format(name)) + return public.return_message(-1, 0, 'Database [{}] does not exist!'.format(name)) + self.sid = find['sid'] + if find['db_type'] in ['0', 0] or self.sid: # 删除本地数据库 + if os.path.exists('data/recycle_bin_db.pl') and not self.sid: + res = self.DeleteToRecycleBin(name) + # return self.DeleteToRecycleBin(name) + info2 = res.get("msg", "Database moved to recycle bin") + return public.return_message(0, 0, info2) + accept = find['accept'] + username = find['username'] + # 删除MYSQL + mysql_obj = public.get_mysql_obj_by_sid(self.sid) + if not mysql_obj: + # return public.return_msg_gettext(False, 'Database [{}] connection failed'.format(name)) + return public.return_message(-1, 0, 'Database [{}] connection failed'.format(name)) + result = mysql_obj.execute("drop database `" + name + "`") + isError = self.IsSqlError(result) + if isError != None: + # return isError + return public.return_message(-1, 0, isError) + users = mysql_obj.query("select Host from mysql.user where User='" + username + "' AND Host!='localhost'") + mysql_obj.execute("drop user '" + username + "'@'localhost'") + for us in users: + mysql_obj.execute("drop user '" + username + "'@'" + us[0] + "'") + mysql_obj.execute("flush privileges") + # 删除SQLITE + public.M('databases').where("id=?", (id,)).delete() + public.write_log_gettext("Database manager", 'Successfully deleted database [{}]!', (name,)) + # return public.return_msg_gettext(True, 'Successfully deleted') + return public.return_message(0, 0, 'Successfully deleted') + # except Exception as ex: + # public.write_log_gettext("Database manager",'Failed to delete database [{}]!, {}',(get.name , str(ex))) + # return public.return_msg_gettext(False,'Failed to delete') + + def db_name_to_unicode(self, name): + ''' + @name 中文数据库名转换为Unicode编码 + @author hwliang<2021-12-20> + @param name 数据库名 + @return name Unicode编码的数据库名 + ''' + name = name.replace('.', '@002e') + name = name.replace('-', '@002d') + return name.encode("unicode_escape").replace(b"\\u", b"@").decode() + + # 删除数据库到回收站 + def DeleteToRecycleBin(self, name): + import json + data = public.M('databases').where("name=?", (name,)).field( + 'id,pid,name,username,password,accept,ps,addtime').find() + username = data['username'] + panelMysql.panelMysql().execute("drop user '" + username + "'@'localhost'") + users = panelMysql.panelMysql().query( + "select Host from mysql.user where User='" + username + "' AND Host!='localhost'") + if isinstance(users, str): + return public.return_msg_gettext(False, 'Delete failed, failed to connect to database!') + try: + for us in users: + panelMysql.panelMysql().execute("drop user '" + username + "'@'" + us[0] + "'") + except Exception: + pass + panelMysql.panelMysql().execute("flush privileges") + rPath = '/www/.Recycle_bin/' + data['rmtime'] = int(time.time()) + u_name = self.db_name_to_unicode(name) + rm_path = '{}/BTDB_{}_t_{}'.format(rPath, u_name, data['rmtime']) + if os.path.exists(rm_path): rm_path += '.1' + rm_config_file = '{}/config.json'.format(rm_path) + datadir = public.get_datadir() + + db_path = '{}/{}'.format(datadir, u_name) + if not os.path.exists(db_path): + return public.return_msg_gettext(False, 'The database data does not exist!') + + public.ExecShell("mv -f {} {}".format(db_path, rm_path)) + if not os.path.exists(rm_path): + return public.return_msg_gettext(False, 'Failed to move database data to the recycle bin!') + public.writeFile(rm_config_file, json.dumps(data)) + # public.writeFile(rPath + 'BTDB_' + name +'_t_' + str(time.time()),json.dumps(data)) + public.M('databases').where("name=?", (name,)).delete() + public.write_log_gettext("Database manager", 'Successfully deleted database [{}]!', (name,)) + return public.return_msg_gettext(True, 'Database moved to recycle bin!') + + # 永久删除数据库 + def DeleteTo(self, filename): + import json + if os.path.isfile(filename): + data = json.loads(public.readFile(filename)) + if public.M('databases').where("name=?", (data['name'],)).count(): + os.remove(filename) + return public.return_msg_gettext(True, 'Successfully deleted') + else: + if os.path.exists(filename): + data = json.loads(public.readFile(filename + '/config.json')) + else: + return public.returnMsg(False, 'Recycle Bin does not exist for this database!') + + db_obj = panelMysql.panelMysql() + if self.database_exists_for_mysql(db_obj, data['name']): + u_name = self.db_name_to_unicode(data['name']) + datadir = public.get_datadir() + db_path = '{}/{}'.format(datadir, u_name) + if not os.path.exists(db_path): + os.makedirs(db_path) + public.ExecShell("chown mysql:mysql {}".format(db_path)) + result = db_obj.execute("drop database `" + data['name'] + "`") + isError = self.IsSqlError(result) + if isError != None: return isError + db_obj.execute("drop user '" + data['username'] + "'@'localhost'") + users = db_obj.query( + "select Host from mysql.user where User='" + data['username'] + "' AND Host!='localhost'") + for us in users: + db_obj.execute("drop user '" + data['username'] + "'@'" + us[0] + "'") + db_obj.execute("flush privileges") + + if os.path.isfile(filename): + os.remove(filename) + else: + import shutil + shutil.rmtree(filename) + + try: + public.write_log_gettext("Database manager", 'Successfully deleted database [{}]!', (data['name'],)) + except: + pass + return public.return_msg_gettext(True, 'Successfully deleted') + + # 恢复数据库 + def RecycleDB(self, filename): + import json + _isdir = False + if os.path.isfile(filename): + data = json.loads(public.readFile(filename)) + else: + re_config_file = filename + '/config.json' + data = json.loads(public.readFile(re_config_file)) + u_name = self.db_name_to_unicode(data['name']) + db_path = "{}/{}".format(public.get_datadir(), u_name) + if os.path.exists(db_path): + return public.return_msg_gettext(False, + 'There is a database with the same name in the current database. To ensure data security, stop recovery!') + _isdir = True + + if public.M('databases').where("name=?", (data['name'],)).count(): + if not _isdir: os.remove(filename) + return public.return_msg_gettext(True, 'Database recovered!') + + if not _isdir: + os.remove(filename) + else: + public.ExecShell('mv -f {} {}'.format(filename, db_path)) + if not os.path.exists(db_path): + return public.return_msg_gettext(False, 'Data recovery failed!') + db_config_file = "{}/config.json".format(db_path) + if os.path.exists(db_config_file): os.remove(db_config_file) + + # 设置文件权限 + public.ExecShell("chown -R mysql:mysql {}".format(db_path)) + public.ExecShell("chmod -R 660 {}".format(db_path)) + public.ExecShell("chmod 700 {}".format(db_path)) + + self.__CreateUsers(data['name'], data['username'], data['password'], data['accept']) + public.M('databases').add('id,pid,name,username,password,accept,ps,addtime', ( + data['id'], data['pid'], data['name'], data['username'], data['password'], data['accept'], data['ps'], + data['addtime'])) + return public.return_msg_gettext(True, 'Database recovered!') + + # 设置ROOT密码 + def SetupPassword(self, get): + from BTPanel import session + + # 校验参数 + try: + get.validate([ + Param('password').Require().String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + password = get['password'].strip() + try: + # if not password: return public.return_msg_gettext(False, 'Root password cannot be empty') + # rep = r"^[\w@\.\?\-\_\>\<\~\!\#\$\%\^\&\*\(\)]+$" + # if not re.match(rep, password): return public.return_msg_gettext(False, + # 'Database password cannot contain special characters!') + self.sid = get.get('sid/d', 0) + # 修改MYSQL + mysql_obj = public.get_mysql_obj_by_sid(self.sid) + if not mysql_obj: + # return public.returnMsg(False, 'Failed to connect to the specified database') + return public.return_message(-1, 0, "Failed to connect to the specified database") + + result = mysql_obj.query("show databases") + isError = self.IsSqlError(result) + is_modify = True + if isError != None and not self.sid: + # 尝试使用新密码 + public.M('config').where("id=?", (1,)).setField('mysql_root', password) + result = mysql_obj.query("show databases") + isError = self.IsSqlError(result) + if isError != None: + public.ExecShell( + "cd /www/server/panel && " + public.get_python_bin() + " tools.py root \"" + password + "\"") + is_modify = False + + if is_modify: + admin_user = 'root' + m_version = public.readFile(public.GetConfigValue('setup_path') + '/mysql/version.pl') + if self.sid: + admin_user = mysql_obj._USER + m_version = mysql_obj.query('select version();')[0][0] + + if m_version.find('5.7') == 0 or m_version.find('8.0') == 0: + accept = self.map_to_list( + mysql_obj.query("select Host from mysql.user where User='{}'".format(admin_user))) + for my_host in accept: + mysql_obj.execute( + "UPDATE mysql.user SET authentication_string='' WHERE User='{}' and Host='{}'".format( + admin_user, my_host[0])) + mysql_obj.execute( + "ALTER USER `%s`@`%s` IDENTIFIED BY '%s'" % (admin_user, my_host[0], password)) + elif m_version.find('10.5.') != -1 or m_version.find('10.4.') != -1: + accept = self.map_to_list( + mysql_obj.query("select Host from mysql.user where User='{}'".format(admin_user))) + for my_host in accept: + mysql_obj.execute( + "ALTER USER `%s`@`%s` IDENTIFIED BY '%s'" % (admin_user, my_host[0], password)) + else: + result = mysql_obj.execute( + "update mysql.user set Password=password('" + password + "') where User='{}'".format( + admin_user)) + mysql_obj.execute("flush privileges") + + msg = public.get_msg_gettext('Successfully modified root password!') + # 修改SQLITE + if self.sid: + public.M('database_servers').where('id=?', self.sid).setField('db_password', password) + public.write_log_gettext("Database manager", "Change the password of the remote MySQL server") + else: + public.M('config').where("id=?", (1,)).setField('mysql_root', password) + public.write_log_gettext("Database manager", 'Successfully modified root password!') + session['config']['mysql_root'] = password + # return public.return_msg_gettext(True, msg) + return public.return_message(0, 0, msg) + except Exception as ex: + # return public.return_msg_gettext(False, 'Failed to modify ' + str(ex)) + return public.return_message(-1, 0, 'Failed to modify : {}'.format(ex)) + + # 修改用户密码 + def ResDatabasePassword(self, get): + from BTPanel import session + + # 校验参数 + try: + get.validate([ + Param('name').Require().String(), + Param('id').Require().Integer(), + Param('password').Require().String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # try: + newpassword = get['password'] + username = get['name'] + id = get['id'] + if not newpassword: + # return public.return_msg_gettext(False, 'Database [{}] password cannot be empty',(username,)) + return public.return_message(-1, 0, 'Database [{}] password cannot be empty'.format(username)) + db_find = public.M('databases').where('id=?', (id,)).find() + name = db_find['name'] + + rep = r"^[\w@\.\?\-\_\>\<\~\!\#\$\%\^\&\*\(\)]+$" + if not re.match(rep, newpassword): + # return public.return_msg_gettext(False, 'Database password cannot contain special characters!') + return public.return_message(-1, 0, 'Database password cannot contain special characters!') + # 修改MYSQL + self.sid = db_find['sid'] + if self.sid and username == 'root': + # return public.returnMsg(False, 'Cannot change the root password of the remote database') + return public.return_message(-1, 0, 'Cannot change the root password of the remote database') + mysql_obj = public.get_mysql_obj_by_sid(self.sid) + if not mysql_obj: + # return public.returnMsg(False, 'Failed to connect to the specified database') + return public.return_message(-1, 0, 'Failed to connect to the specified database') + m_version = public.readFile(public.GetConfigValue('setup_path') + '/mysql/version.pl') + if self.sid: + m_version = mysql_obj.query('select version();')[0][0] + + if m_version.find('5.7') == 0 or m_version.find('8.0') == 0: + accept = self.map_to_list( + mysql_obj.query("select Host from mysql.user where User='" + name + "' AND Host!='localhost'")) + mysql_obj.execute("update mysql.user set authentication_string='' where User='" + username + "'") + result = mysql_obj.execute("ALTER USER `%s`@`localhost` IDENTIFIED BY '%s'" % (username, newpassword)) + for my_host in accept: + mysql_obj.execute("ALTER USER `%s`@`%s` IDENTIFIED BY '%s'" % (username, my_host[0], newpassword)) + elif m_version.find('10.5.') != -1 or m_version.find('10.4.') != -1: + accept = self.map_to_list( + mysql_obj.query("select Host from mysql.user where User='" + name + "' AND Host!='localhost'")) + result = mysql_obj.execute("ALTER USER `%s`@`localhost` IDENTIFIED BY '%s'" % (username, newpassword)) + for my_host in accept: + mysql_obj.execute("ALTER USER `%s`@`%s` IDENTIFIED BY '%s'" % (username, my_host[0], newpassword)) + else: + result = mysql_obj.execute( + "update mysql.user set Password=password('" + newpassword + "') where User='" + username + "'") + + isError = self.IsSqlError(result) + if isError != None: + # return isError + return public.return_message(-1, 0, isError) + + mysql_obj.execute("flush privileges") + # if result==False: return public.return_msg_gettext(False,'Failed to modify, database user does not exist!') + # 修改SQLITE + if int(id) > 0: + public.M('databases').where("id=?", (id,)).setField('password', newpassword) + else: + public.M('config').where("id=?", (id,)).setField('mysql_root', newpassword) + session['config']['mysql_root'] = newpassword + + public.write_log_gettext("Database manager", 'Successfully modifyied password for database [{}]!', (name,)) + # return public.return_msg_gettext(True, 'Successfully modifyied password for database [{}]!', (name,)) + return public.return_message(0, 0, 'Successfully modifyied password for database [{}]!'.format(name)) + # except Exception as ex: + # import traceback + # public.write_log_gettext("Database manager", 'Failed to modify password for database [{}]!',(username,traceback.format_exc(limit=True).replace('\n','
                                    '))) + # return public.return_msg_gettext(False,'Failed to modify password for database [{}]!',(name,)) + + # 备份 + def ToBackup(self, get): + from BTPanel import session + + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # try: + import shlex + id = get['id'] + db_find = public.M('databases').where("id=?", (id,)).find() + name = db_find['name'] + fileName = name + '_' + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.sql.gz' + backupName = session['config']['backup_path'] + '/database/' + fileName + mysqldump_bin = public.get_mysqldump_bin() + if db_find['db_type'] in ['0', 0]: + # 本地数据库 + result = panelMysql.panelMysql().execute("show databases") + isError = self.IsSqlError(result) + if isError: + # return isError + return public.return_message(-1, 0, isError) + + root = public.M('config').where('id=?', (1,)).getField('mysql_root') + if not os.path.exists(session['config']['backup_path'] + '/database'): public.ExecShell( + 'mkdir -p ' + session['config']['backup_path'] + '/database') + if not self.mypass(True, root): + # return public.return_msg_gettext(False,'Database configuration file failed to get checked, please + # check if MySQL configuration file exists [/etc/my.cnf]') + return public.return_message(-1, 0, "Database configuration file failed to get checked, please check " + "if MySQL configuration file exists [/etc/my.cnf]") + try: + password = public.M('config').where('id=?', (1,)).getField('mysql_root') + if not password: + # return public.returnMsg(False, 'Database password cannot be empty') + return public.return_message(-1, 0, "Database password cannot be empty") + password = shlex.quote(str(password)) + os.environ["MYSQL_PWD"] = password + public.ExecShell( + mysqldump_bin + " -R -E --triggers=false --default-character-set=" + public.get_database_character( + name) + " --force --opt \"" + name + "\" -u root -p" + password + " | gzip > " + backupName) + except Exception as e: + raise + finally: + os.environ["MYSQL_PWD"] = "" + self.mypass(False, root) + elif db_find['db_type'] in ['1', 1]: + # 远程数据库 + try: + conn_config = json.loads(db_find['conn_config']) + res = self.CheckCloudDatabase(conn_config) + if isinstance(res, dict): + # return res + return public.return_message(0, 0, res) + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = password + public.ExecShell(mysqldump_bin + " -h " + conn_config['db_host'] + " -P " + str(int(conn_config[ + 'db_port'])) + " -R -E --triggers=false --default-character-set=" + public.get_database_character( + name) + " --force --opt \"" + str(db_find['name']) + "\" -u " + str( + conn_config['db_user']) + " -p" + password + " | gzip > " + backupName) + except Exception as e: + raise + finally: + os.environ["MYSQL_PWD"] = "" + elif db_find['db_type'] in ['2', 2]: + try: + conn_config = public.M('database_servers').where('id=?', db_find['sid']).find() + res = self.CheckCloudDatabase(conn_config) + if isinstance(res, dict): + # return res + return public.return_message(0, 0, res) + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = password + public.ExecShell(mysqldump_bin + " -h " + conn_config['db_host'] + " -P " + str(int(conn_config[ + 'db_port'])) + " -R -E --triggers=false --default-character-set=" + public.get_database_character( + name) + " --force --opt \"" + str(db_find['name']) + "\" -u " + str( + conn_config['db_user']) + " -p" + str(conn_config['db_password']) + " | gzip > " + backupName) + except Exception as e: + raise + finally: + os.environ["MYSQL_PWD"] = "" + else: + # return public.return_msg_gettext(False, 'Unsupported database type') + return public.return_message(-1, 0, "Unsupported database type") + + if not os.path.exists(backupName): + # return public.return_msg_gettext(False, 'Backup error!') + return public.return_message(-1, 0, "Backup error") + sql = public.M('backup') + addTime = time.strftime('%Y-%m-%d %X', time.localtime()) + sql.add('type,name,pid,filename,size,addtime', (1, fileName, id, backupName, 0, addTime)) + public.write_log_gettext("Database manager", "Backup database [{}] succeed!", (name,)) + # return public.return_msg_gettext(True, 'Backup Succeeded!') + return public.return_message(0, 0, "Backup Succeeded") + # except Exception as ex: + # public.write_log_gettext("数据库管理", "备份数据库[" + name + "]失败 => " + str(ex)) + # return public.return_msg_gettext(False,'备份失败!') + + # 删除备份文件 + def DelBackup(self, get): + + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + id = get.id + where = "id=?" + backup_info = public.M('backup').where(where, (id,)).find() + filename = backup_info['filename'] + if os.path.exists(filename): os.remove(filename) + db_name = '' + if filename == 'qiniu': + name = backup_info['name'] + public.ExecShell(public.get_python_bin() + " " + public.GetConfigValue( + 'setup_path') + '/panel/script/backup_qiniu.py delete_file ' + name) + public.M('backup').where(where, (id,)).delete() + # 取实际 + pid = backup_info['pid'] + db_name = public.M('databases').where('id=?', (pid,)).getField('name') + public.write_log_gettext("Database manager", 'Successfully deleted backup [{}] for database [{}]!', + (db_name, filename)) + # return public.return_msg_gettext(True, 'Successfully deleted') + return public.return_message(0, 0, "Successfully deleted") + except Exception as ex: + public.write_log_gettext("Database manager", 'Failed to delete backup [{}] for database [{}]! => {}', + (db_name, filename, str(ex))) + # return public.return_msg_gettext(False, 'Failed to delete') + return public.return_message(-1, 0, "Failed to delete") + + # 导入 + def InputSql(self, get): + from BTPanel import session + + # 校验参数 + try: + get.validate([ + Param('file').SafePath(), # 文件路径 + Param('name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + # try: + import shlex + name = get['name'] + file = get['file'] + if "|" in file: + file = file.split('|')[-1] + root = public.M('config').where('id=?', (1,)).getField('mysql_root') + tmp = file.split('.') + exts = ['sql', 'gz', 'zip'] + ext = tmp[len(tmp) - 1] + + if ext not in exts: + # return public.return_msg_gettext(False, 'Select sql/gz/zip file!') + return public.return_message(-1, 0, "Select sql/gz/zip file") + db_find = public.M('databases').where('name=?', name).find() + mysql_obj = public.get_mysql_obj_by_sid(db_find['sid']) + + if not mysql_obj: + # return public.returnMsg(False, 'Failed to connect to the specified database') + return public.return_message(-1, 0, "Failed to connect to the specified database") + result = mysql_obj.execute("show databases") + isError = self.IsSqlError(result) + if isError: + # return isError + return public.return_message(0, 0, isError) + isgzip = False + + mysql_bin = public.get_mysql_bin() + if ext != 'sql': + import panel_restore + tmp = file.split('/') + tmpFile = tmp[len(tmp) - 1] + tmpFile = tmpFile.replace('.sql.' + ext, '.sql') + tmpFile = tmpFile.replace('.' + ext, '.sql') + tmpFile = tmpFile.replace('tar.', '') + # return tmpFile + backupPath = session['config']['backup_path'] + '/database' + if "|" in file: + panel_restore.panel_restore().restore_db_backup(get) + download_msg = panel_restore.panel_restore().restore_db_backup(get) + if not download_msg['status']: + # return download_msg + return public.return_message(0, 0, download_msg) + input_path = os.path.join(backupPath, tmpFile) + # 备份文件的路径 + input_path2 = os.path.join(os.path.dirname(file), tmpFile) + if ext == 'zip': + public.ExecShell("cd " + backupPath + " && unzip " + '"' + file + '"') + else: + public.ExecShell("cd " + backupPath + " && tar zxf " + '"' + file + '"') + if not os.path.exists(input_path): + # 兼容从备份文件所在目录恢复 + if not os.path.exists(input_path2): + public.ExecShell("cd " + backupPath + " && gunzip -q " + '"' + file + '"') + isgzip = True + else: + input_path = input_path2 + if not os.path.exists(input_path) or tmpFile == '': + if tmpFile and os.path.isfile(input_path2): + input_path = input_path2 + else: + # return public.return_msg_gettext(False, 'Configuration file not exist', (tmpFile,)) + return public.return_message(-1, 0, 'Configuration file not exist {}'.format(tmpFile)) + + try: + if db_find['db_type'] in ['0', 0]: + password = public.M('config').where('id=?', (1,)).getField('mysql_root') + password = shlex.quote(str(password)) + os.environ["MYSQL_PWD"] = str(password) + public.ExecShell(mysql_bin + " -uroot -p" + str( + password) + " --force \"" + name + "\" < " + '"' + input_path + '"') + elif db_find['db_type'] in ['1', 1]: + conn_config = json.loads(db_find['conn_config']) + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = str(password) + public.ExecShell(mysql_bin + " -h " + conn_config['db_host'] + " -P " + str( + int(conn_config['db_port'])) + " -u" + str(conn_config['db_user']) + " -p" + str( + password) + " --force \"" + name + "\" < " + '"' + input_path + '"') + elif db_find['db_type'] in ['2', 2]: + conn_config = public.M('database_servers').where('id=?', db_find['sid']).find() + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = str(password) + public.ExecShell(mysql_bin + " -h " + conn_config['db_host'] + " -P " + str( + int(conn_config['db_port'])) + " -u" + str(conn_config['db_user']) + " -p" + str( + password) + " --force \"" + name + "\" < " + '"' + input_path + '"') + except Exception as e: + raise + finally: + os.environ["MYSQL_PWD"] = "" + + if isgzip: + public.ExecShell('cd ' + os.path.dirname(input_path) + ' && gzip ' + file.split('/')[-1][:-3]) + else: + public.ExecShell("rm -f " + input_path) + else: + try: + if db_find['db_type'] in ['0', 0]: + password = public.M('config').where('id=?', (1,)).getField('mysql_root') + password = shlex.quote(str(password)) + os.environ["MYSQL_PWD"] = password + public.ExecShell( + mysql_bin + " -uroot -p" + password + " --force \"" + name + "\" < " + '"' + file + '"') + elif db_find['db_type'] in ['1', 1]: + conn_config = json.loads(db_find['conn_config']) + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = password + public.ExecShell(mysql_bin + " -h " + conn_config['db_host'] + " -P " + str( + int(conn_config['db_port'])) + " -u" + str(conn_config['db_user']) + " -p" + str( + password) + " --force \"" + name + "\" < " + '"' + file + '"') + elif db_find['db_type'] in ['2', 2]: + conn_config = public.M('database_servers').where('id=?', db_find['sid']).find() + password = shlex.quote(str(conn_config['db_password'])) + os.environ["MYSQL_PWD"] = password + public.ExecShell(mysql_bin + " -h " + conn_config['db_host'] + " -P " + str( + int(conn_config['db_port'])) + " -u" + str(conn_config['db_user']) + " -p" + str( + password) + " --force \"" + name + "\" < " + '"' + file + '"') + except Exception as e: + raise + finally: + os.environ["MYSQL_PWD"] = "" + + public.write_log_gettext("Database manager", 'Successfully imported database [{}]', (name,)) + # return public.return_msg_gettext(True, 'Successfully imported database!') + return public.return_message(0, 0, 'Successfully imported database!') + + + # 同步数据库到服务器 + def SyncToDatabases(self, get): + + # 校验参数 + try: + get.validate([ + Param('ids').Require(), # [67,66] + Param('type').Require().Integer(), # query传参 不确定.. + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # result = panelMysql.panelMysql().execute("show databases") + # isError=self.IsSqlError(result) + # if isError: return isError + type = int(get['type']) + n = 0 + sql = public.M('databases') + if type == 0: + data = sql.field('id,sid,name,username,password,accept,db_type').where("type='MySQL'", ()).select() + for value in data: + if value['db_type'] in ['1', 1]: + continue # 跳过远程数据库 + result = self.ToDataBase(value) + if result == 1: n += 1 + else: + import json + data = json.loads(get.ids) + for value in data: + find = sql.where("id=?", (value,)).field('id,sid,name,username,password,accept').find() + result = self.ToDataBase(find) + if result == 1: n += 1 + # 当只同步1个数据库时,不返回成功数量 + if n == 1: + # return public.returnMsg(True, 'Synchronization succeeded') + return public.return_message(0, 0, 'Synchronization succeeded') + elif n == 0: + # 失败 + # return public.returnMsg(False, 'Sync failed') + return public.return_message(-1, 0, 'Sync failed') + else: + # return public.return_msg_gettext(True, 'Sync {} database(s) from server!', (str(n),)) + return public.return_message(0, 0, 'Sync {} database(s) from server!'.format(str(n))) + + # 配置 + def mypass(self, act, password=None): + conf_file = '/etc/my.cnf' + conf_file_bak = '/etc/my.cnf.bak' + if os.path.getsize(conf_file) > 2: + public.writeFile(conf_file_bak, public.readFile(conf_file)) + public.set_mode(conf_file_bak, 600) + public.set_own(conf_file_bak, 'mysql') + elif os.path.getsize(conf_file_bak) > 2: + public.writeFile(conf_file, public.readFile(conf_file_bak)) + public.set_mode(conf_file, 600) + public.set_own(conf_file, 'mysql') + + public.ExecShell("sed -i '/user=root/d' {}".format(conf_file)) + public.ExecShell("sed -i '/password=/d' {}".format(conf_file)) + if act: + password = public.M('config').where('id=?', (1,)).getField('mysql_root') + mycnf = public.readFile(conf_file) + if not mycnf: return False + src_dump_re = r"\[mysqldump\][^.]" + sub_dump = "[mysqldump]\nuser=root\npassword=\"{}\"\n".format(password) + mycnf = re.sub(src_dump_re, sub_dump, mycnf) + if len(mycnf) > 100: public.writeFile(conf_file, mycnf) + return True + return True + + # 添加到服务器 + def ToDataBase(self, find): + # if find['username'] == 'bt_default': return 0 + if len(find['password']) < 3: + find['username'] = find['name'] + find['password'] = public.md5(str(time.time()) + find['name'])[0:10] + public.M('databases').where("id=?", (find['id'],)).save('password,username', + (find['password'], find['username'])) + self.sid = find['sid'] + mysql_obj = public.get_mysql_obj_by_sid(find['sid']) + if not mysql_obj: return public.returnMsg(False, 'Failed to connect to the specified database') + result = mysql_obj.execute("create database `" + find['name'] + "`") + if "using password:" in str(result): return -1 + if "Connection refused" in str(result): return -1 + + password = find['password'] + # if find['password']!="" and len(find['password']) > 20: + # password = find['password'] + + self.__CreateUsers(find['name'], find['username'], password, find['accept']) + return 1 + + # 从服务器获取数据库 + def SyncGetDatabases(self, get): + + # 校验参数 + try: + get.validate([ + Param('sid').Require().Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + self.sid = get.get('sid/d', 0) + db_type = 0 + if self.sid: db_type = 2 + mysql_obj = public.get_mysql_obj_by_sid(self.sid) + if not mysql_obj: + # return public.returnMsg(False, 'Failed to connect to the specified database') + return public.return_message(-1, 0, 'Failed to connect to the specified database') + data = mysql_obj.query("show databases") + isError = self.IsSqlError(data) + if isError != None: + # return isError + return public.return_message(-1, 0, isError) + users = mysql_obj.query( + "select User,Host from mysql.user where User!='root' AND Host!='localhost' AND Host!=''") + + if type(users) == str: + # return public.returnMsg(False, users) + return public.return_message(-1, 0, users) + if type(users) != list: + # return public.returnMsg(False, public.GetMySQLError(users)) + return public.return_message(-1, 0, public.GetMySQLError(users)) + + sql = public.M('databases') + nameArr = ['information_schema', 'performance_schema', 'mysql', 'sys'] + n = 0 + for value in data: + b = False + for key in nameArr: + if value[0] == key: + b = True + break + if b: continue + if sql.where("name=?", (value[0],)).count(): continue + host = '127.0.0.1' + for user in users: + if value[0] == user[0]: + host = user[1] + break + + ps = public.get_msg_gettext('Edit notes') + if value[0] == 'test': + ps = public.get_msg_gettext('Test Database') + + # XSS filter + if not re.match(r"^[\w+\.-]+$", value[0]): continue + + addTime = time.strftime('%Y-%m-%d %X', time.localtime()) + + if sql.table('databases').add('name,sid,db_type,username,password,accept,ps,addtime', + (value[0], self.sid, db_type, value[0], '', host, ps, addTime)): n += 1 + + # return public.return_msg_gettext(True, 'Obtain {} database(s) from server!', (str(n),)) + return public.return_message(0, 0, 'Obtain {} database(s) from server!'.format(n)) + + # 获取数据库权限 + def GetDatabaseAccess(self, get): + + # 校验参数 + try: + get.validate([ + Param('name').Require().String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + name = get['name'] + db_name = public.M('databases').where('username=?', name).getField('name') + mysql_obj = public.get_mysql_obj(db_name) + users = mysql_obj.query("select Host from mysql.user where User='" + name + "' AND Host!='localhost'") + ssl_type = panelMysql.panelMysql().query("select ssl_type from mysql.user where User='%s'" % name) + isError = self.IsSqlError(users) + if isError != None: + # return isError + return public.return_message(-1, 0, isError) + users = self.map_to_list(users) + try: + if ssl_type: + ssl_type = ssl_type[0][0] + except: + ssl_type = "" + if len(users) < 1: + # return public.return_msg_gettext(True, {"permission": "127.0.0.1", "ssl": ssl_type}) + return public.return_message(0, 0, {"permission": "127.0.0.1", "ssl": ssl_type}) + + accs = [] + for c in users: + accs.append(c[0]) + userStr = ','.join(accs) + # return public.return_msg_gettext(True, {"permission": userStr, "ssl": ssl_type}) + return public.return_message(0, 0, {"permission": userStr, "ssl": ssl_type}) + + # 设置数据库权限 + def SetDatabaseAccess(self, get): + + # 校验参数 + try: + get.validate([ + Param('name').Require().String(), + # Param('dataAccess').String().Xss(), + # Param('address').String().Xss(), + Param('access').Require().String(), + Param('ssl').String().Xss(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + ssl = "" + if hasattr(get, 'ssl'): + ssl = get.ssl + if ssl == "REQUIRE SSL" and not self.check_mysql_ssl_status(get): + # return public.return_msg_gettext(False,'SSL is not enabled in the database, please open it in the Mysql manager first') + return public.return_message(-1, 0, 'SSL is not enabled in the database, please open it in the Mysql manager first') + name = get['name'] + db_find = public.M('databases').where('username=?', (name,)).find() + db_name = db_find['name'] + self.sid = db_find['sid'] + mysql_obj = public.get_mysql_obj(db_name) + access = get['access'].strip() + # if access in ['']: return public.return_msg_gettext(False, 'The IP address cannot be empty!') + password = public.M('databases').where("username=?", (name,)).getField('password') + result = mysql_obj.query("show databases") + isError = self.IsSqlError(result) + if isError != None: + # return isError + return public.return_message(-1, 0, isError) + users = mysql_obj.query("select Host from mysql.user where User='" + name + "' AND Host!='localhost'") + for us in users: + mysql_obj.execute("drop user '" + name + "'@'" + us[0] + "'") + self.__CreateUsers(db_name, name, password, access, ssl) + # return public.return_msg_gettext(True, 'Setup successfully!') + return public.return_message(0, 0, 'Setup successfully!') + + # 获取数据库配置信息 + def GetMySQLInfo(self, get): + data = {} + try: + public.CheckMyCnf() + myfile = '/etc/my.cnf' + mycnf = public.readFile(myfile) + rep = "datadir\\s*=\\s*(.+)\n" + data['datadir'] = re.search(rep, mycnf).groups()[0] + rep = "port\\s*=\\s*([0-9]+)\\s*\n" + data['port'] = re.search(rep, mycnf).groups()[0] + except: + data['datadir'] = '/www/server/data' + data['port'] = '3306' + # return data + return public.return_message(0, 0, data) + + # 修改数据库目录 + def SetDataDir(self, get): + if get.datadir[-1] == '/': get.datadir = get.datadir[0:-1] + if len(get.datadir) > 32: return public.return_msg_gettext(False, + 'The data directory length cannot exceed 32 bits') + # if not re.search(r"^[0-9A-Za-z_/\\]$+",get.datadir): return public.return_msg_gettext(False,'Special symbols cannot be included in the database path') + if not os.path.exists(get.datadir): public.ExecShell('mkdir -p ' + get.datadir) + mysqlInfo = self.GetMySQLInfo(get) + if mysqlInfo['datadir'] == get.datadir: return public.return_msg_gettext(False, + 'The same as the current storage directory, file cannot be moved!') + + public.ExecShell('/etc/init.d/mysqld stop') + public.ExecShell(r'\cp -arf ' + mysqlInfo['datadir'] + '/* ' + get.datadir + '/') + public.ExecShell('chown -R mysql.mysql ' + get.datadir) + public.ExecShell('chmod -R 755 ' + get.datadir) + public.ExecShell('rm -f ' + get.datadir + '/*.pid') + public.ExecShell('rm -f ' + get.datadir + '/*.err') + + public.CheckMyCnf() + myfile = '/etc/my.cnf' + mycnf = public.readFile(myfile) + public.writeFile('/etc/my_backup.cnf', mycnf) + mycnf = mycnf.replace(mysqlInfo['datadir'], get.datadir) + public.writeFile(myfile, mycnf) + public.ExecShell('/etc/init.d/mysqld start') + result = public.ExecShell('ps aux|grep mysqld|grep -v grep') + if len(result[0]) > 10: + public.writeFile('data/datadir.pl', get.datadir) + return public.return_msg_gettext(True, 'File moved!') + else: + public.ExecShell('pkill -9 mysqld') + public.writeFile(myfile, public.readFile('/etc/my_backup.cnf')) + public.ExecShell('/etc/init.d/mysqld start') + return public.return_msg_gettext(False, 'Failed to move file!') + + # 修改数据库端口 + def SetMySQLPort(self, get): + myfile = '/etc/my.cnf' + mycnf = public.readFile(myfile) + rep = "port\\s*=\\s*([0-9]+)\\s*\n" + mycnf = re.sub(rep, 'port = ' + get.port + '\n', mycnf) + public.writeFile(myfile, mycnf) + public.ExecShell('/etc/init.d/mysqld restart') + return public.return_msg_gettext(True, 'Setup successfully!') + + # 获取错误日志 + def GetErrorLog(self, get): + path = self.GetMySQLInfo(get)['datadir'] + filename = '' + for n in os.listdir(path): + if len(n) < 5: continue + if n[-3:] == 'err': + filename = path + '/' + n + break + if not os.path.exists(filename): return public.return_msg_gettext(False, 'Configuration file not exist') + if hasattr(get, 'close'): + public.writeFile(filename, '') + return public.return_msg_gettext(True, 'log is empty') + return public.GetNumLines(filename, 1000) + + # 二进制日志开关 + def BinLog(self, get): + status = getattr(get, "status", None) + mysql_cnf = public.readFile(self._MYSQL_CNF) + + if mysql_cnf.find('#log-bin=mysql-bin') != -1: + if hasattr(get, 'status'): return public.return_msg_gettext(False, '0') + + log_bin_status = re.search("\nlog-bin", mysql_cnf) + + is_off_bin_log = re.search("\nskip-log-bin", mysql_cnf) + bin_log_status = False + if log_bin_status and not is_off_bin_log: + bin_log_status = True + + mysql_data_dir = self.GetMySQLInfo(get)['datadir'] + if status is not None: + bin_log_total_size = 0 + mysql_bin_index = os.path.join(mysql_data_dir, "mysql-bin.index") + mysql_bin_index_content = public.readFile(mysql_bin_index) + if mysql_bin_index_content is not False: + for name in str(mysql_bin_index_content).strip().split("\n"): + bin_log_path = os.path.join(mysql_data_dir, os.path.basename(name)) + if os.path.isfile(bin_log_path): + bin_log_total_size += os.path.getsize(bin_log_path) + # return {"status": True, "msg": "ok", "data": {"binlog_status": bin_log_status, "size": bin_log_total_size}} + return public.return_msg_gettext(True, bin_log_total_size) + + if bin_log_status is True: # 关闭 binlog 日志 + master_slave_conf_1 = "/www/server/panel/plugin/masterslave/data.json" + master_slave_conf_2 = "/www/server/panel/plugin/mysql_replicate/config.json" + if os.path.exists(master_slave_conf_1): + # return {"status": False, "msg": "请先卸载【Mysql主从复制】插件后再关闭二进制日志!!", "data": {"binlog_status": bin_log_status}} + return public.return_msg_gettext(False, + 'Please uninstall the Mysql master-slave replication plugin before closing the binary log! !') + if os.path.exists(master_slave_conf_2): + # return {"status": False, "msg": "请先卸载【Mysql主从复制(重构版)】插件后再关闭二进制日志!!", "data": {"binlog_status": bin_log_status}} + return public.return_msg_gettext(False, + 'Please uninstall the Mysql master-slave replication plugin before closing the binary log! !') + if log_bin_status: + mysql_cnf = re.sub(r"\nlog-bin", "\n#log-bin", mysql_cnf) + mysql_cnf = re.sub(r"\nbinlog_format", "\n#binlog_format", mysql_cnf) + if not is_off_bin_log: + if re.search("\n#\\s*skip-log-bin", mysql_cnf): + mysql_cnf = re.sub("\n#\\s*skip-log-bin", "\nskip-log-bin", mysql_cnf) + else: + mysql_cnf = re.sub("\n#\\s*log-bin", "\nskip-log-bin\n#log-bin", mysql_cnf) + # public.ExecShell("rm -f {}/mysql-bin.*".format(mysql_data_dir)) + else: # 开启 binlog 日志 + if re.search("\n#\\s*log-bin", mysql_cnf): + mysql_cnf = re.sub("\n#\\s*log-bin", "\nlog-bin", mysql_cnf) + else: + mysql_cnf = re.sub("[mysqld]", "[mysqld]\nlog-bin=mysql-bin", mysql_cnf) + + if re.search("\n#\\s*binlog_format", mysql_cnf): + mysql_cnf = re.sub("\n#\\s*binlog_format", "\nbinlog_format", mysql_cnf) + else: + mysql_cnf = re.sub("[mysqld]", "[mysqld]\nbinlog_format=mixed", mysql_cnf) + + if is_off_bin_log: + mysql_cnf = re.sub("\nskip-log-bin", "\n#skip-log-bin", mysql_cnf) + + public.writeFile(self._MYSQL_CNF, mysql_cnf) + public.ExecShell('sync') + public.ExecShell('/etc/init.d/mysqld restart') + return {"status": True, "msg": "{} Binary log successful, Please refresh manually".format("Enable" if not bin_log_status else "Disable"), "data": {"binlog_status": not bin_log_status}} + + # 获取MySQL配置状态 + def GetDbStatus(self, get): + result = {} + data = self.map_to_list(panelMysql.panelMysql().query('show variables')) + gets = ['table_open_cache', 'thread_cache_size', 'query_cache_type', 'key_buffer_size', 'query_cache_size', + 'tmp_table_size', 'max_heap_table_size', 'innodb_buffer_pool_size', 'innodb_additional_mem_pool_size', + 'innodb_log_buffer_size', 'max_connections', 'sort_buffer_size', 'read_buffer_size', + 'read_rnd_buffer_size', 'join_buffer_size', 'thread_stack', 'binlog_cache_size'] + result['mem'] = {} + for d in data: + try: + for g in gets: + if d[0] == g: result['mem'][g] = d[1] + except: + continue + + if 'query_cache_type' in result['mem']: + if result['mem']['query_cache_type'] != 'ON': result['mem']['query_cache_size'] = '0' + return result + + # 设置MySQL配置参数 + def SetDbConf(self, get): + gets = ['key_buffer_size', 'query_cache_size', 'tmp_table_size', 'max_heap_table_size', + 'innodb_buffer_pool_size', 'innodb_log_buffer_size', 'max_connections', 'query_cache_type', + 'table_open_cache', 'thread_cache_size', 'sort_buffer_size', 'read_buffer_size', 'read_rnd_buffer_size', + 'join_buffer_size', 'thread_stack', 'binlog_cache_size'] + emptys = ['max_connections', 'query_cache_type', 'thread_cache_size', 'table_open_cache'] + mycnf = public.readFile('/etc/my.cnf') + n = 0 + m_version = public.readFile('/www/server/mysql/version.pl') + if not m_version: m_version = '' + for g in gets: + if m_version.find('8.') == 0 and g in ['query_cache_type', 'query_cache_size']: + n += 1 + continue + s = 'M' + if n > 5 and not g in ['key_buffer_size', 'query_cache_size', 'tmp_table_size', 'max_heap_table_size', + 'innodb_buffer_pool_size', 'innodb_log_buffer_size']: s = 'K' + if g in emptys: s = '' + if g in ['innodb_log_buffer_size']: + s = 'M' + if int(get[g]) < 8: + return public.return_msg_gettext(False, 'innodb_log_buffer_size cannot be less than 8MB') + + rep = r'\s*' + g + r'\s*=\s*\d+(M|K|k|m|G)?\n' + + c = g + ' = ' + get[g] + s + '\n' + if mycnf.find(g) != -1: + mycnf = re.sub(rep, '\n' + c, mycnf, 1) + else: + mycnf = mycnf.replace('[mysqld]\n', '[mysqld]\n' + c) + n += 1 + public.writeFile('/etc/my.cnf', mycnf) + return public.return_msg_gettext(True, 'Setup successfully!') + + # 获取MySQL运行状态 + def GetRunStatus(self, get): + import time + result = {} + data = panelMysql.panelMysql().query('show global status') + gets = ['Max_used_connections', 'Com_commit', 'Com_rollback', 'Questions', 'Innodb_buffer_pool_reads', + 'Innodb_buffer_pool_read_requests', 'Key_reads', 'Key_read_requests', 'Key_writes', + 'Key_write_requests', 'Qcache_hits', 'Qcache_inserts', 'Bytes_received', 'Bytes_sent', + 'Aborted_clients', 'Aborted_connects', 'Created_tmp_disk_tables', 'Created_tmp_tables', + 'Innodb_buffer_pool_pages_dirty', 'Opened_files', 'Open_tables', 'Opened_tables', 'Select_full_join', + 'Select_range_check', 'Sort_merge_passes', 'Table_locks_waited', 'Threads_cached', 'Threads_connected', + 'Threads_created', 'Threads_running', 'Connections', 'Uptime'] + try: + if data[0] == 1045: + return public.return_msg_gettext(False, 'MySQL password ERROR!') + for d in data: + for g in gets: + try: + if d[0] == g: result[g] = d[1] + except: + pass + except: + return public.return_msg_gettext(False, str(data)) + + if not 'Run' in result and result: + result['Run'] = int(time.time()) - int(result['Uptime']) + tmp = panelMysql.panelMysql().query('show master status') + try: + + result['File'] = tmp[0][0] + result['Position'] = tmp[0][1] + except: + result['File'] = 'OFF' + result['Position'] = 'OFF' + return result + + # 取慢日志 + def GetSlowLogs(self, get): + path = self.GetMySQLInfo(get)['datadir'] + '/mysql-slow.log' + if not os.path.exists(path): return public.return_msg_gettext(False, 'Log file does NOT exist!') + return public.return_msg_gettext(True, public.GetNumLines(path, 100)) + + # 获取binlog文件列表 + def GetMySQLBinlogs(self, get): + data_dir = self.GetMySQLInfo(get)["datadir"] + index_file = os.path.join(data_dir, "mysql-bin.index") + if not os.path.exists(index_file): return public.return_msg_gettext(False, 'Binlog is not enabled or binlog file does not exist!') + + text = public.readFile(index_file) + + rows = panelMysql.panelMysql().query("show master status") + + current_log = "" + if not isinstance(rows, list): + return public.return_msg_gettext(False, "Mysql status is abnormal!") + if len(rows) != 0: + current_log = rows[0][0] + + bin_log = [] + for item in text.split('\n'): + log_file = item.strip() + log_name = log_file.lstrip("./") + if not log_file: continue # 空行 + bin_log_path = os.path.join(data_dir, log_name) + if not os.path.isfile(bin_log_path): continue + st = os.stat(bin_log_path) + bin_log.append({ + "name": log_name, + "path": bin_log_path, + "size": st.st_size, + "last_modified": int(st.st_mtime), + "last_access": int(st.st_atime), + "current": current_log == log_name + }) + return {"status": True, "msg": "ok", "data": bin_log} + + def ClearMySQLBinlog(self, get): + if not hasattr(get, "days"): + return public.returnMsg(False, "Parameters are missing! days") + if not str(get.days).isdigit(): + return public.returnMsg(False, "Parameters are missing! days") + days = int(get.days) + if days < 7: return public.return_msg_gettext(False, 'To ensure data security, recent binlogs cannot be deleted!') + + rows = panelMysql.panelMysql().query("PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL {days} DAY)".format(days=days)) + # public.print_log(rows) + # if rows: public.print_log(rows[0]) + + return public.return_msg_gettext(True, "Cleanup complete!") + + # 获取当前数据库信息 + def GetInfo(self, get): + # 校验参数 + try: + get.validate([ + Param('db_name').Require().String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + info = self.GetdataInfo(get) + # return info + return public.return_message(0, 0, info) + # if info: + # return info + # else: + # return public.return_msg_gettext(False, 'Failed to get databases') + + # 修复表信息 + def ReTable(self, get): + info = self.RepairTable(get) + if info: + # return public.return_msg_gettext(True, 'Successfully repaired!') + return public.return_message(0, 0, 'Successfully repaired!') + else: + # return public.return_msg_gettext(False, 'Failed to repair!') + return public.return_message(-1, 0, 'Failed to repair!') + + # 优化表 + def OpTable(self, get): + + + info = self.OptimizeTable(get) + if info: + # return public.return_msg_gettext(True, 'Successfully optimized!') + return public.return_message(0, 0, 'Successfully optimized!') + else: + # return public.return_msg_gettext(False, 'Failed to optimize or already optimized') + return public.return_message(-1, 0, 'Failed to optimize or already optimized') + + # 更改表引擎 + def AlTable(self, get): + + # 校验参数 + try: + get.validate([ + Param('db_name').Require().String(), + Param('tables').Require().String(), # ["wp_commentmeta"] + Param('table_type').Require().String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + info = self.AlterTable(get) + if info: + # return public.return_msg_gettext(True, 'Successfully changed') + return public.return_message(0, 0, 'Successfully changed') + else: + # return public.return_msg_gettext(False, 'Failed to change') + return public.return_message(-1, 0, 'Failed to change') + + def get_average_num(self, slist): + """ + @获取平均值 + """ + count = len(slist) + limit_size = 1 * 1024 * 1024 + if count <= 0: return limit_size + + if len(slist) > 1: + slist = sorted(slist) + limit_size = int((slist[0] + slist[-1]) / 2 * 0.85) + return limit_size + + def get_database_size(self, ids, is_pid=False): + """ + 获取数据库大小 + """ + result = {} + for id in ids: + if not is_pid: + x = public.M('databases').where('id=?', id).field('id,sid,pid,name,type,ps,addtime').find() + else: + x = public.M('databases').where('pid=?', id).field('id,sid,pid,name,ps,type,addtime').find() + if not x: continue + x['backup_count'] = public.M('backup').where("pid=? AND type=?", (x['id'], '1')).count() + if x['type'] == 'MySQL': + x['total'] = int(public.get_database_size_by_id(x['id'])) + else: + try: + from panelDatabaseController import DatabaseController + project_obj = DatabaseController() + + get = public.dict_obj() + get['data'] = {'db_id': x['id']} + get['mod_name'] = x['type'].lower() + get['def_name'] = 'get_database_size_by_id' + + x['total'] = project_obj.model(get) + except: + x['total'] = int(public.get_database_size_by_id(x['id'])) + result[x['name']] = x + return result + + def check_del_data(self, get): + """ + @删除数据库前置检测 + """ + + # 校验参数 + try: + get.validate([ + Param('ids').Require(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + ids = json.loads(get.ids) + slist = {} + result = [] + db_list_size = [] + db_data = self.get_database_size(ids) + for key in db_data: + data = db_data[key] + if not data['id'] in ids: continue + + db_addtime = public.to_date(times=data['addtime']) + data['score'] = int(time.time() - db_addtime) + data['total'] + data['st_time'] = db_addtime + + if data['total'] > 0: db_list_size.append(data['total']) + result.append(data) + + slist['data'] = sorted(result, key=lambda x: x['score'], reverse=True) + slist['db_size'] = self.get_average_num(db_list_size) + # return slist + return public.return_message(0, 0, slist) + + + # 获取备份文件 + def GetBackup(self, get): + + # 分页校验参数 + try: + get.validate([ + Param('limit').Integer(), + Param('p').Integer(), + Param('search').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + p = getattr(get, "p", 1) + limit = getattr(get, "limit", 10) + return_js = getattr(get, "return_js", "") + search = getattr(get, "search", None) + + # if not str(p).isdigit(): + # return public.returnMsg(False, "参数错误!p") + # if not str(limit).isdigit(): + # return public.returnMsg(False, "参数错误!limit") + + p = int(p) + limit = int(limit) + + ext_list = ["sql", "tar.gz", "gz", "zip"] + + backup_list = [] + + # 递归获取备份文件 + def get_dir_backup(backup_dir: str, backup_list: list, is_recursion: bool): + for name in os.listdir(backup_dir): + path = os.path.join(backup_dir, name) + if os.path.isdir(path) and name == "all_backup": continue # 跳过全部备份目录 + if os.path.isfile(path): + ext = name.split(".")[-1] + if ext.lower() not in ext_list: continue + if search is not None and search not in name: continue + + stat_file = os.stat(path) + path_data = { + "name": name, + "path": path, + "size": stat_file.st_size, + "mtime": int(stat_file.st_mtime), + "ctime": int(stat_file.st_ctime), + } + backup_list.append(path_data) + elif os.path.isdir(path) and is_recursion is True: + get_dir_backup(path, backup_list, is_recursion) + + get_dir_backup(self._MYSQL_BACKUP_DIR, backup_list, True) + get_dir_backup(self._DB_BACKUP_DIR, backup_list, False) + + try: + from flask import request + uri = public.url_encode(request.full_path) + except: + uri = '' + # 包含分页类 + import page + # 实例化分页类 + page = page.Page() + info = { + "p": p, + "count": len(backup_list), + "row": limit, + "return_js": return_js, + "uri": uri, + } + page_info = page.GetPage(info) + + start_idx = (int(p) - 1) * limit + end_idx = p * limit if p * limit < len(backup_list) else len(backup_list) + backup_list.sort(key=lambda data: data["mtime"], reverse=True) + backup_list = backup_list[start_idx:end_idx] + # return {"status": True, "msg": "OK", "data": backup_list, "page": page_info} + return public.return_message(0, 0, {"status": True, "msg": "OK", "data": backup_list, "page": page_info}) diff --git a/class_v2/datatool_v2.py b/class_v2/datatool_v2.py new file mode 100644 index 00000000..3f99f81b --- /dev/null +++ b/class_v2/datatool_v2.py @@ -0,0 +1,168 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: 1249648969@qq.com +# ------------------------------------------------------------------- + +# ------------------------------ +# 数据库工具类 +# ------------------------------ +import sys, os +os.chdir("/www/server/panel") +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import panelMysql +import re,json,public + +class datatools: + DB_MySQL = None + # 字节单位转换 + def ToSize(self, size): + ds = ['b', 'KB', 'MB', 'GB', 'TB'] + for d in ds: + if size < 1024: return ('%.2f' % size) + d + size = size / 1024 + return '0b' + + # 获取当前数据库信息 + def GetdataInfo(self,get): + ''' + 传递一个数据库名称即可 get.databases + ''' + + db_name=get.db_name + if not db_name:return False + if not self.DB_MySQL:self.DB_MySQL = public.get_mysql_obj(db_name) + if not self.DB_MySQL: return self.DB_MySQL + ret = {} + tables = self.map_to_list(self.DB_MySQL.query('show tables from `%s`' % db_name)) + if type(tables) == list: + try: + data = self.map_to_list(self.DB_MySQL.query("select sum(DATA_LENGTH)+sum(INDEX_LENGTH) from information_schema.tables where table_schema='%s'" % db_name))[0][0] + except: + data=0 + + if not data: data = 0 + ret['data_size'] = self.ToSize(data) + ret['database'] = db_name + + ret3 = [] + for i in tables: + if i == 1049: return public.return_msg_gettext(False,'Database does NOT exist!') + if type(i) == int: continue + table = self.map_to_list(self.DB_MySQL.query("show table status from `%s` where name = '%s'" % (db_name, i[0]))) + if not table: continue + try: + ret2 = {} + ret2['type']=table[0][1] + data_size = table[0][6] + ret2['rows_count'] = self.DB_MySQL.query("select count(*) from `{}`.`{}`".format(db_name,i[0]))[0][0] #table[0][4] 实时获取行数 @authow hwliang<2021-08-05> 修改 + ret2['collation'] = table[0][14] + ret2['data_size'] = self.ToSize(int(data_size)) + ret2['table_name'] = i[0] + ret3.append(ret2) + except: continue + ret['tables'] = (ret3) + return ret + + + + #修复表信息 + def RepairTable(self,get): + + ''' + POST: + db_name=web + tables=['web1','web2'] + ''' + db_name = get.db_name + tables = json.loads(get.tables) + if not db_name or not tables: return False + if not self.DB_MySQL:self.DB_MySQL = public.get_mysql_obj(db_name) + m_version = self.DB_MySQL.query('select version();')[0][0] + if m_version.find('5.1.')!=-1:return public.return_msg_gettext(False,"Nonsupport mysql5.1!") + mysql_table = self.map_to_list(self.DB_MySQL.query('show tables from `%s`' % db_name)) + ret=[] + if type(mysql_table)==list: + if len(mysql_table)>0: + for i in mysql_table: + for i2 in tables: + if i2==i[0]: + ret.append(i2) + if len(ret)>0: + for i in ret: + self.DB_MySQL.execute('REPAIR TABLE `%s`.`%s`'%(db_name,i)) + return True + return False + + + + #map to list + def map_to_list(self,map_obj): + try: + if type(map_obj) != list and type(map_obj) != str: map_obj = list(map_obj) + return map_obj + except: return [] + + + # 优化表 + def OptimizeTable(self,get): + ''' + POST: + db_name=web + tables=['web1','web2'] + ''' + + db_name = get.db_name + tables = json.loads(get.tables) + if not db_name or not tables: return False + if not self.DB_MySQL:self.DB_MySQL = public.get_mysql_obj(db_name) + mysql_table = self.map_to_list(self.DB_MySQL.query('show tables from `%s`' % db_name)) + ret=[] + if type(mysql_table) == list: + if len(mysql_table) > 0: + for i in mysql_table: + for i2 in tables: + if i2 == i[0]: + ret.append(i2) + if len(ret)>0: + for i in ret: + self.DB_MySQL.execute('OPTIMIZE table `%s`.`%s` ENGINE=MyISAM' % (db_name,i)) + return True + return False + + # 更改表引擎 + def AlterTable(self,get): + ''' + POST: + db_name=web + table_type=innodb + tables=['web1','web2'] + ''' + db_name = get.db_name + table_type = get.table_type + tables = json.loads(get.tables) + + if not db_name or not tables: return False + if not self.DB_MySQL:self.DB_MySQL = public.get_mysql_obj(db_name) + mysql_table = self.map_to_list(self.DB_MySQL.query('show tables from `%s`' % db_name)) + ret=[] + if type(mysql_table)==list: + if len(mysql_table)>0: + for i in mysql_table: + for i2 in tables: + if i2==i[0]: + ret.append(i2) + if len(ret)>0: + for i in ret: + self.DB_MySQL.execute('alter table `%s`.`%s` ENGINE=`%s`' % (db_name,i,table_type)) + return True + return False + + + #检查表 + def CheckTable(self,database,tables,*args,**kwargs): + pass diff --git a/class_v2/db_mysql_v2.py b/class_v2/db_mysql_v2.py new file mode 100644 index 00000000..91522d13 --- /dev/null +++ b/class_v2/db_mysql_v2.py @@ -0,0 +1,374 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +import re,os,sys,public,json +import pymysql + +class panelMysql: + __DB_PASS = None + __DB_USER = 'root' + __DB_NAME = None + __DB_PORT = 3306 + __DB_HOST = 'localhost' + __DB_PREFIX = '' + __DB_CONN = None + __DB_CUR = None + __DB_ERR = None + __DB_TABLE = "" # 被操作的表名称 + __OPT_WHERE = "" # where条件 + __OPT_LIMIT = "" # limit条件 + __OPT_ORDER = "" # order条件 + __OPT_FIELD = "*" # field条件 + __OPT_PARAM = () # where值 + _USER = None + _ex = None + + def __init__(self): + pass + + def set_name(self,name): + self.__DB_NAME = str(name) + return self + + def set_prefix(self,prefix): + self.__DB_PREFIX = prefix + return self + + def set_host(self,host,port,name,username,password,prefix = ''): + self.__DB_HOST = host + self.__DB_PORT = int(port) + self.__DB_NAME = name + if self.__DB_NAME: self.__DB_NAME = str(self.__DB_NAME) + self.__DB_USER = str(username) + self._USER = str(username) + self.__DB_PASS = str(password) + self.__DB_PREFIX = prefix + if not self.__GetConn(): return False + return self + + #连接MYSQL数据库 + def __GetConn(self): + try: + # print(self.__DB_HOST,self.__DB_PORT,self.__DB_NAME,self.__DB_USER,self.__DB_PASS) + self.__DB_CONN = pymysql.connect(host=self.__DB_HOST,user=self.__DB_USER,passwd=str(self.__DB_PASS),db=self.__DB_NAME,port=self.__DB_PORT,connect_timeout=15,read_timeout=60,write_timeout=60) + except Exception as ex: + self.__DB_ERR = "error: " + str(ex) + self._ex = ex + print(ex) + if self.__DB_ERR.find("timed out") != -1 or self.__DB_ERR.find("is not allowed to connect") != -1: return False + try: + self.__DB_CONN = pymysql.connect(host=self.__DB_HOST,user=self.__DB_USER,passwd=str(self.__DB_PASS),db=self.__DB_NAME,port=self.__DB_PORT) + except Exception as ex: + self.__DB_ERR = "error: " + str(ex) + self._ex = ex + print(ex) + return False + self.__DB_CUR = self.__DB_CONN.cursor() + return True + + def table(self,table): + #设置表名 + self.__DB_TABLE = self.__DB_PREFIX + 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条件 + limit = str(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() + if not self.__DB_CUR: return self.__DB_ERR + try: + self.__get_columns() + sql = "SELECT " + self.__OPT_FIELD + " FROM " + self.__DB_TABLE + self.__OPT_WHERE + self.__OPT_ORDER + self.__OPT_LIMIT + self.__DB_CUR.execute(sql,self.__OPT_PARAM) + data = self.__DB_CUR.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: + self._ex = 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( + "select COLUMN_NAME from information_schema.COLUMNS where table_name = '{}' and table_schema = '{}';" + .format(self.__DB_TABLE, self.__DB_NAME), False) + cols = [] + for col in tmp_cols: + cols.append('`' + col[0] + '`') + 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.__GetConn() + self.__DB_CONN.text_factory = str + try: + values = "" + for key in keys.split(','): + values += "%s," + values = values[0:len(values) - 1] + sql = "INSERT INTO " + self.__DB_TABLE + "(" + keys + ") " + "VALUES(" + values + ")" + self.__DB_CUR.execute(sql, self.__to_tuple(param)) + id = self.__DB_CUR.lastrowid + self.__close() + self.__DB_CONN.commit() + return id + except Exception as ex: + self._ex = 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_tmp = [] + for k in keys: + keys_tmp.append("`{}`".format(k)) + keys_str = ','.join(keys_tmp) + + param = [] + for k in keys: + #if pdata[k] == None: pdata[k] = '' + param.append(pdata[k]) + return keys_str, tuple(param) + + def addAll(self, keys, param): + #插入数据 + self.__GetConn() + self.__DB_CONN.text_factory = str + try: + values = "" + for key in keys.split(','): + values += "%s," + values = values[0:len(values) - 1] + sql = "INSERT INTO " + self.__DB_TABLE + "(" + keys + ") " + "VALUES(" + values + ")" + result = self.__DB_CUR.execute(sql, self.__to_tuple(param)) + return True + except Exception as ex: + self._ex = ex + return "error: " + str(ex) + + def commit(self): + self.__close() + self.__DB_CONN.commit() + + def save(self, keys, param): + #更新数据 + self.__GetConn() + self.__DB_CONN.text_factory = str + try: + opt = "" + for key in keys.split(','): + opt += key + "=%s," + opt = opt[0:len(opt) - 1] + sql = "UPDATE " + self.__DB_TABLE + " SET " + opt + self.__OPT_WHERE + + #处理拼接WHERE与UPDATE参数 + if param: + tmp = list(self.__to_tuple(param)) + for arg in self.__OPT_PARAM: + tmp.append(arg) + self.__OPT_PARAM = tuple(tmp) + self.__DB_CUR.execute(sql,self.__OPT_PARAM) + else: + self.__DB_CUR.execute(sql) + self.__close() + self.__DB_CONN.commit() + return self.__DB_CUR.rowcount + except Exception as ex: + self._ex = ex + return "error: " + str(ex) + + def delete(self, id=None): + #删除数据 + self.__GetConn() + try: + if id: + self.__OPT_WHERE = " WHERE id=%s" + self.__OPT_PARAM = (id, ) + sql = "DELETE FROM " + self.__DB_TABLE + self.__OPT_WHERE + self.__DB_CUR.execute(sql, self.__OPT_PARAM) + self.__close() + self.__DB_CONN.commit() + return self.__DB_CUR.rowcount + except Exception as ex: + return "error: " + str(ex) + + def execute(self,sql,param = ()): + #执行SQL语句返回受影响行 + if not self.__GetConn(): return self.__DB_ERR + try: + if param: + self.__OPT_PARAM = list(self.__to_tuple(param)) + result = self.__DB_CUR.execute(sql,self.__OPT_PARAM) + else: + result = self.__DB_CUR.execute(sql) + self.__DB_CONN.commit() + self.__close() + return result + except Exception as ex: + self._ex = ex + return ex + + + def query(self,sql,is_close=True,param=()): + #执行SQL语句返回数据集 + if not self.__GetConn(): return self.__DB_ERR + try: + if param: + self.__OPT_PARAM = list(self.__to_tuple(param)) + self.__DB_CUR.execute(sql,self.__OPT_PARAM) + else: + self.__DB_CUR.execute(sql) + result = self.__DB_CUR.fetchall() + #将元组转换成列表 + data = list(map(list,result)) + if is_close: self.__Close() + return data + except Exception as ex: + self._ex = ex + return ex + + + #关闭连接 + def __Close(self): + self.__DB_CUR.close() + self.__DB_CONN.close() + + def __close(self): + #清理条件属性 + self.__OPT_WHERE = "" + self.__OPT_FIELD = "*" + self.__OPT_ORDER = "" + self.__OPT_LIMIT = "" + self.__OPT_PARAM = () + + def close(self): + #释放资源 + try: + self.__DB_CUR.close() + self.__DB_CUR.close() + except: + pass diff --git a/class_v2/db_v2.py b/class_v2/db_v2.py new file mode 100644 index 00000000..2cf281ce --- /dev/null +++ b/class_v2/db_v2.py @@ -0,0 +1,392 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +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值 + __LOCK = '/dev/shm/sqlite_lock.pl' + + def __init__(self): + self.__DB_FILE = 'data/default.db' + + 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 connect(self): + #连接数据库 + self.__GetConn() + return self + + def dbfile(self,name): + #设置数据库文件 + if name[0] == '/': + self.__DB_FILE = name + else: + self.__DB_FILE = 'data/' + 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,offset = 0): + #LIMIT条件 + + if limit and not offset: + self.__OPT_LIMIT = " LIMIT {}".format(limit) + elif limit and offset: + self.__OPT_LIMIT = " LIMIT {},{}".format(offset,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): + return + # 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): + return + # self.is_lock() + # with open(self.__LOCK,'wb+') as f: + # f.close() + + #解锁 + def rm_lock(self): + return + # if os.path.exists(self.__LOCK): + # os.remove(self.__LOCK) + + 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 is_connect(self): + #检查是否连接数据库 + if not self.__DB_CONN: + return False + return True + + + def close(self): + #释放资源 + try: + self.__DB_CONN.close() + self.__DB_CONN = None + except: + pass + diff --git a/class_v2/dk_db.py b/class_v2/dk_db.py new file mode 100644 index 00000000..9d6a1cea --- /dev/null +++ b/class_v2/dk_db.py @@ -0,0 +1,574 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import json +import os +import sys +import sqlite3 +import re + +os.chdir("/www/server/panel") +if not "class/" in sys.path: + sys.path.insert(0,"class/") + +import public + +class Sql(): + #------------------------------ + # 数据库操作类 For sqlite3 + #------------------------------ + __LOCK = "/dev/shm/sqlite_lock.pl" + __LAST_KEY = "BT-0x:" + __ENCRYPT_KEYS = ["password","salt","email","mysql_root","db_password"] + __DEFAULT_DB_PATH = os.path.join(public.get_panel_path(), "data/db/default.db") + + __DATABASES_PATH = os.path.join(public.get_panel_path(), "config/databases.json") + + + def __init__(self): + self.__DB_FILE = None # 数据库文件 + self.__DB_CONN = None # 数据库连接对象 + self.__DB_TABLE = "" # 被操作的表名称 + self.__OPT_WHERE = "" # where条件 + self.__OPT_LIMIT = "" # limit条件 + self.__OPT_ORDER = "" # order条件 + self.__OPT_FIELD = "*" # field条件 + self.__OPT_PARAM = () # where值 + self.__DB_NAME = "" # 数据库名称 + + self.__TB_INFO = {} # 表信息 + + def __enter__(self): + return self + + def __exit__(self,exc_type,exc_value,exc_trackback): + self.close() + + def __get_db_path(self): + if self.__DB_FILE is not None: + return + + if self.__DB_NAME: + # 有指定数据库名称的情况 + databases_dict = json.loads(public.readFile(self.__DATABASES_PATH)) + for db_name, tb_dict in databases_dict.items(): + if self.__DB_NAME in tb_dict: + self.__DB_FILE = os.path.join(public.get_panel_path(), "data/db", db_name) + self.__TB_INFO = tb_dict[self.__DB_NAME] + return + else: + # 未指定数据库名称的情况 + if not self.__DB_TABLE: + self.__DB_FILE = self.__DEFAULT_DB_PATH + return + try: + databases_dict = json.loads(public.readFile(self.__DATABASES_PATH)) + for db_name, tb_dict in databases_dict.items(): + if self.__DB_TABLE in tb_dict: + self.__DB_FILE = os.path.join(public.get_panel_path(), "data/db", db_name) + self.__TB_INFO = tb_dict[self.__DB_TABLE] + return + except Exception as err: + pass + + # 使用默认数据库 + self.__DB_FILE = self.__DEFAULT_DB_PATH + return + + def __GetConn(self): + #取数据库对象 + self.__get_db_path() + try: + if self.__DB_CONN == None: + if os.path.exists(self.__DB_FILE) and os.path.getsize(self.__DB_FILE) == 0: + os.remove(self.__DB_FILE) + self.__DB_CONN = sqlite3.connect(self.__DB_FILE) + self.__DB_CONN.text_factory = str + + if "sql" in self.__TB_INFO and self.__TB_INFO.get("sql"): + result = self.__DB_CONN.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?;", (self.__DB_TABLE,)) + if list(map(list,result))[0][0] == 0: + self.__DB_CONN.execute(self.__TB_INFO["sql"]) + except Exception as ex: + return "error: " + str(ex) + + def set_dbfile(self,path): + self.__DB_FILE = path + return self + + def connect(self): + #连接数据库 + self.__GetConn() + return self + + def dbfile(self,name: str): + if not name.endswith(".db"): # 兼容老的 + name += ".db" + #设置数据库文件 + self.__DB_FILE = os.path.join(public.get_panel_path(), "data", name) + return self + + def table(self,table): + #设置表名 + self.__DB_TABLE = table + return self + + def db(self,name): + # 设置数据库名称,用于判断数据库文件 + self.__DB_NAME = name + 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,offset = 0): + #LIMIT条件 + + if limit and not offset: + self.__OPT_LIMIT = " LIMIT {}".format(limit) + elif limit and offset: + self.__OPT_LIMIT = " LIMIT {},{}".format(offset,limit) + return self + + + def field(self,field): + #FIELD条件 + if len(field): + self.__OPT_FIELD = field + return self + + def query(self, sql, param=(), transfer_list=True): + # 执行SQL语句返回数据集 + self.__GetConn() + try: + result = self.__DB_CONN.execute(sql, self.__to_tuple(param)) + # self.log("result:" + str(result)) + if transfer_list: + # 将元组转换成列表 + data = list(map(list, result)) + else: + data = result + return data + except Exception as ex: + return "error: " + str(ex) + + def select(self, num=0): + # 查询数据集 + 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() + data = self.de_crypt(data) + return data + except Exception as ex: + ex_str = str(ex) + re_column = re.compile(r"no such column:\s+(?P\S+)") + res = re_column.search(ex_str) + if res: + column = res.group("column") + if self.__TB_INFO: + for i in self.__TB_INFO.get("fields", []): + if isinstance(i, list) and len(i) == 6 and i[1] == column: + # 处理字符串类型默认值 + if i[2].lower() in ['text', 'varchar'] and i[4] == "": + i[4] = "''" + res = self.execute( + "ALTER TABLE {} ADD {} {} DEFAULT {}".format(self.__DB_TABLE, column, i[2], i[4])) + # 如果添加字段成功,且重试次数<3次,则重新查询 + if isinstance(res, int) and num < 3: + num += 1 + return self.select(num) + + if "disk I/O error" in ex_str: + return [] + + # 处理firewall_new表的异常 + if ("malformed database schema" in ex_str or "file is encrypted or is not a database" in ex_str) and self.__DB_TABLE in ['firewall_new']: + bak_file = "{}.{}".format(self.__DB_FILE, public.format_date("%Y%m%d_%H%M%S")) + public.ExecShell("mv -f {} {}".format(self.__DB_FILE, bak_file)) + return [] + + return [] + + 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.limit("1").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 + param = self.en_crypt(keys,param) + 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: + raise public.PanelError("数据库插入出错:" + "error: " + str(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 + param = self.en_crypt(keys,param) + 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 _encrypt(self,data): + import PluginLoader + # 加密数据 + if not isinstance(data,str): return data + if not data: return data + if data.startswith(self.__LAST_KEY): return data + + result = PluginLoader.db_encrypt(data) + if result['status'] == True: + return self.__LAST_KEY + result['msg'] + return data + + def _decrypt(self,data): + import PluginLoader + # 解密数据 + if not isinstance(data,str): return data + if not data: return data + if data.startswith(self.__LAST_KEY): + res = PluginLoader.db_decrypt(data[6:])['msg'] + return res + return data + + + def en_crypt(self,keys,param): + # 加密指定字段 + try: + if not param or not keys: return param + if not str(self.__DB_FILE).startswith(os.path.join(public.get_panel_path(), "data")): return param + + new_param = [] + keys_list = keys.split(',') + if isinstance(param,str): param = [param] + for i in range(len(keys_list)): + key = keys_list[i] + value = param[i] + if key in self.__ENCRYPT_KEYS: + value = self._encrypt(value) + new_param.append(value) + return tuple(new_param) + except: + return param + + def de_crypt(self,data): + # 解密字段 + try: + if not data: return data + if not str(self.__DB_FILE).startswith(os.path.join(public.get_panel_path(), "data")): return data + if isinstance(data,dict): + for key in data.keys(): + if not isinstance(data[key],str): continue + if data[key].startswith(self.__LAST_KEY): + data[key] = self._decrypt(data[key]) + elif isinstance(data,list): + for i in range(len(data)): + for key in data[i].keys(): + if not isinstance(data[i][key],str): continue + if data[i][key].startswith(self.__LAST_KEY): + data[i][key] = self._decrypt(data[i][key]) + elif isinstance(data,str): + if data.startswith(self.__LAST_KEY): + data = self._decrypt(data) + return data + except: + return data + + def save(self,keys,param): + #更新数据 + self.write_lock() + self.__GetConn() + self.__DB_CONN.text_factory = str + param = self.en_crypt(keys,param) + 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: + raise public.PanelError("数据库保存出错:" + "error: " + str(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 executemany(self, sql, param: list): + # 执行SQL语句返回受影响行 + self.write_lock() + self.__GetConn() + try: + result = self.__DB_CONN.executemany(sql, param) + self.__DB_CONN.commit() + self.rm_lock() + return result.rowcount + except Exception as ex: + return "error: " + str(ex) + + #是否有锁 + def is_lock(self): + return + # 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): + return + # self.is_lock() + # with open(self.__LOCK,'wb+') as f: + # f.close() + + #解锁 + def rm_lock(self): + return + # if os.path.exists(self.__LOCK): + # os.remove(self.__LOCK) + + 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 is_connect(self): + #检查是否连接数据库 + if not self.__DB_CONN: + return False + return True + + + def close(self): + #释放资源 + try: + self.__DB_CONN.close() + self.__DB_CONN = None + except: + pass + diff --git a/class_v2/download_file_v2.py b/class_v2/download_file_v2.py new file mode 100644 index 00000000..b8e5da7c --- /dev/null +++ b/class_v2/download_file_v2.py @@ -0,0 +1,64 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import os,sys,public,json,time +class downloadFile: + logPath = 'data/speed.json' + timeoutCount = 0; + oldTime = 0; + writeTime = 0; + down_count = 0; + #下载文件 + def DownloadFile(self,url,filename): + try: + path = os.path.dirname(filename) + if not os.path.exists(path): os.makedirs(path) + import urllib,socket,ssl + try: + ssl._create_default_https_context = ssl._create_unverified_context + except:pass + socket.setdefaulttimeout(30) + self.pre = 0; + self.oldTime = time.time(); + if sys.version_info[0] == 2: + urllib.urlretrieve(url,filename=filename,reporthook= self.DownloadHook) + else: + urllib.request.urlretrieve(url,filename=filename,reporthook= self.DownloadHook) + speed = self.GetSpeed() + speed['pre'] = 100; + speed['used'] = speed['total'] + self.WriteLogs(json.dumps(speed)); + except: + if self.timeoutCount > 5: return; + self.timeoutCount += 1 + time.sleep(5) + self.DownloadFile(url,filename) + + #下载文件进度回调 + def DownloadHook(self,count, blockSize, totalSize): + used = count * blockSize + pre1 = int((100.0 * used / totalSize)) + my_time = time.time() + if self.pre != pre1 or (my_time - self.writeTime) > 1: + dspeed = ((count -self.down_count) * blockSize) / (my_time - self.oldTime) + speed = {'name':public.GetMsg("DOWNLOAD_FILE"),'total':totalSize,'used':used,'pre':self.pre,'speed':dspeed} + self.WriteLogs(json.dumps(speed)) + self.pre = pre1 + self.writeTime = my_time + self.down_count = count + self.oldTime = my_time + + #取下载进度 + def GetSpeed(self): + speedLog = public.ReadFile(self.logPath) + if not speedLog: return {'name':public.GetMsg("DOWNLOAD_FILE"),'total':0,'used':0,'pre':0,'speed':0} + return json.loads(speedLog) + + #写输出日志 + def WriteLogs(self,logMsg): + public.WriteFile(self.logPath,logMsg) diff --git a/class_v2/fastcgi_client_two_v2.py b/class_v2/fastcgi_client_two_v2.py new file mode 100644 index 00000000..2322ea54 --- /dev/null +++ b/class_v2/fastcgi_client_two_v2.py @@ -0,0 +1,399 @@ +# Copyright (c) 2006 Allan Saddi +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $Id$ +# +# Copyright (c) 2011 Vladimir Rusinov + +__author__ = 'Allan Saddi ' +__version__ = '$Revision$' +import sys +import select +import struct +import socket +import errno +import types + + + +__all__ = ['FCGIApp'] + +# Constants from the spec. +FCGI_LISTENSOCK_FILENO = 0 + +FCGI_HEADER_LEN = 8 + +FCGI_VERSION_1 = 1 + +FCGI_BEGIN_REQUEST = 1 +FCGI_ABORT_REQUEST = 2 +FCGI_END_REQUEST = 3 +FCGI_PARAMS = 4 +FCGI_STDIN = 5 +FCGI_STDOUT = 6 +FCGI_STDERR = 7 +FCGI_DATA = 8 +FCGI_GET_VALUES = 9 +FCGI_GET_VALUES_RESULT = 10 +FCGI_UNKNOWN_TYPE = 11 +FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE + +FCGI_NULL_REQUEST_ID = 0 + +FCGI_KEEP_CONN = 1 + +FCGI_RESPONDER = 1 +FCGI_AUTHORIZER = 2 +FCGI_FILTER = 3 + +FCGI_REQUEST_COMPLETE = 0 +FCGI_CANT_MPX_CONN = 1 +FCGI_OVERLOADED = 2 +FCGI_UNKNOWN_ROLE = 3 + +FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' +FCGI_MAX_REQS = 'FCGI_MAX_REQS' +FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' + +FCGI_Header = '!BBHHBx' +FCGI_BeginRequestBody = '!HB5x' +FCGI_EndRequestBody = '!LB3x' +FCGI_UnknownTypeBody = '!B7x' + +FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody) +FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) +FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) + +if __debug__: + import time + + # Set non-zero to write debug output to a file. + DEBUG = 0 + DEBUGLOG = '/www/server/panel/logs/fastcgi.log' + + def _debug(level, msg): + if DEBUG < level: + return + + try: + f = open(DEBUGLOG, 'a') + f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg)) + f.close() + except: + pass + +def decode_pair(s, pos=0): + """ + Decodes a name/value pair. + + The number of bytes decoded as well as the name/value pair + are returned. + """ + nameLength = ord(s[pos]) + if nameLength & 128: + nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff + pos += 4 + else: + pos += 1 + + valueLength = ord(s[pos]) + if valueLength & 128: + valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff + pos += 4 + else: + pos += 1 + + name = s[pos:pos+nameLength] + pos += nameLength + value = s[pos:pos+valueLength] + pos += valueLength + + return (pos, (name, value)) + +def encode_pair(name, value): + """ + Encodes a name/value pair. + + The encoded string is returned. + """ + nameLength = len(name) + if nameLength < 128: + s = chr(nameLength).encode() + else: + s = struct.pack('!L', nameLength | 0x80000000) + + valueLength = len(value) + if valueLength < 128: + s += chr(valueLength).encode() + else: + s += struct.pack('!L', valueLength | 0x80000000) + + return s + name + value + +class Record(object): + """ + A FastCGI Record. + + Used for encoding/decoding records. + """ + def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): + self.version = FCGI_VERSION_1 + self.type = type + self.requestId = requestId + self.contentLength = 0 + self.paddingLength = 0 + self.contentData = '' + + def _recvall(sock, length): + """ + Attempts to receive length bytes from a socket, blocking if necessary. + (Socket may be blocking or non-blocking.) + """ + dataList = [] + recvLen = 0 + while length: + try: + data = sock.recv(length) + except socket.error as e: + if e[0] == errno.EAGAIN: + select.select([sock], [], []) + continue + else: + raise + if not data: # EOF + break + dataList.append(data) + dataLen = len(data) + recvLen += dataLen + length -= dataLen + return b''.join(dataList), recvLen + _recvall = staticmethod(_recvall) + + def read(self, sock): + """Read and decode a Record from a socket.""" + try: + header, length = self._recvall(sock, FCGI_HEADER_LEN) + except: + raise EOFError + + if length < FCGI_HEADER_LEN: + raise EOFError + + self.version, self.type, self.requestId, self.contentLength, \ + self.paddingLength = struct.unpack(FCGI_Header, header) + + if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, ' + 'contentLength = %d' % + (sock.fileno(), self.type, self.requestId, + self.contentLength)) + + if self.contentLength: + try: + self.contentData, length = self._recvall(sock, + self.contentLength) + except: + raise EOFError + + if length < self.contentLength: + raise EOFError + + if self.paddingLength: + try: + self._recvall(sock, self.paddingLength) + except: + raise EOFError + + def _sendall(sock, data): + """ + Writes data to a socket and does not return until all the data is sent. + """ + length = len(data) + while length: + try: + sent = sock.send(data) + except socket.error as e: + if e[0] == errno.EAGAIN: + select.select([], [sock], []) + continue + else: + raise + data = data[sent:] + length -= sent + _sendall = staticmethod(_sendall) + + def write(self, sock): + """Encode and write a Record to a socket.""" + self.paddingLength = -self.contentLength & 7 + + if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, ' + 'contentLength = %d' % + (sock.fileno(), self.type, self.requestId, + self.contentLength)) + + header = struct.pack(FCGI_Header, self.version, self.type, + self.requestId, self.contentLength, + self.paddingLength) + self._sendall(sock, header) + if self.contentLength: + self._sendall(sock, self.contentData) + if self.paddingLength: + self._sendall(sock, b'\x00'*self.paddingLength) + +class FCGIApp(object): + + def __init__(self, connect=None, host=None, port=None, filterEnviron=True): + if host is not None: + assert port is not None + connect=(host, port) + + self._connect = connect + self._filterEnviron = filterEnviron + + def __call__(self, environ, io, start_response=None): + # For sanity's sake, we don't care about FCGI_MPXS_CONN + # (connection multiplexing). For every request, we obtain a new + # transport socket, perform the request, then discard the socket. + # This is, I believe, how mod_fastcgi does things... + + sock = self._getConnection() + + # Since this is going to be the only request on this connection, + # set the request ID to 1. + requestId = 1 + + # Begin the request + rec = Record(FCGI_BEGIN_REQUEST, requestId) + rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0) + rec.contentLength = FCGI_BeginRequestBody_LEN + rec.write(sock) + + # Filter WSGI environ and send it as FCGI_PARAMS + if self._filterEnviron: + params = self._defaultFilterEnviron(environ) + else: + params = self._lightFilterEnviron(environ) + # TODO: Anything not from environ that needs to be sent also? + #return '200 OK',[],str(params),'' + self._fcgiParams(sock, requestId, params) + self._fcgiParams(sock, requestId, {}) + + # Transfer wsgi.input to FCGI_STDIN + content_length = int(environ.get('CONTENT_LENGTH') or 0) + s = '' + #io = StringIO(stdin) + while True: + if not io: break + chunk_size = min(content_length, 4096) + s = io.read(chunk_size) + content_length -= len(s) + rec = Record(FCGI_STDIN, requestId) + rec.contentData = s + rec.contentLength = len(s) + rec.write(sock) + if not s: break + # Empty FCGI_DATA stream + rec = Record(FCGI_DATA, requestId) + rec.write(sock) + return sock + + + def _getConnection(self): + if self._connect is not None: + # The simple case. Create a socket and connect to the + # application. + if isinstance(self._connect, str): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self._connect) + elif hasattr(socket, 'create_connection'): + sock = socket.create_connection(self._connect) + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(self._connect) + return sock + + # To be done when I have more time... + raise NotImplementedError #, 'Launching and managing FastCGI programs not yet implemented' + + def _fcgiGetValues(self, sock, vars): + # Construct FCGI_GET_VALUES record + outrec = Record(FCGI_GET_VALUES) + data = [] + for name in vars: + data.append(encode_pair(name, '')) + data = ''.join(data) + outrec.contentData = data + outrec.contentLength = len(data) + outrec.write(sock) + + # Await response + inrec = Record() + inrec.read(sock) + result = {} + if inrec.type == FCGI_GET_VALUES_RESULT: + pos = 0 + while pos < inrec.contentLength: + pos, (name, value) = decode_pair(inrec.contentData, pos) + result[name] = value + return result + + def _fcgiParams(self, sock, requestId, params): + rec = Record(FCGI_PARAMS, requestId) + data = [] + for name,value in params.items(): + data.append(encode_pair(name.encode('latin-1'), value.encode('latin-1'))) + data = b''.join(data) + rec.contentData = data + rec.contentLength = len(data) + rec.write(sock) + + _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_', + 'CONTENT_', 'DOCUMENT_', 'SCRIPT_'] + _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE'] + _environRenames = [] + + def _defaultFilterEnviron(self, environ): + result = {} + for n in environ.keys(): + iv = False + for p in self._environPrefixes: + if n.startswith(p): + result[n] = environ[n] + iv = True + if n in self._environCopies: + result[n] = environ[n] + iv = True + if n in self._environRenames: + result[self._environRenames[n]] = environ[n] + iv = True + if not iv: + result[n] = environ[n] + + return result + + def _lightFilterEnviron(self, environ): + result = {} + for n in environ.keys(): + if n.upper() == n: + result[n] = environ[n] + return result diff --git a/class_v2/fastcgi_client_v2.py b/class_v2/fastcgi_client_v2.py new file mode 100644 index 00000000..4940573c --- /dev/null +++ b/class_v2/fastcgi_client_v2.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# coding:utf-8 + +import socket +import random +import re +import sys + +class fastcgi_client: + __FCGI_VERSION = 1 + + __FCGI_ROLE_RESPONDER = 1 + __FCGI_ROLE_AUTHORIZER = 2 + __FCGI_ROLE_FILTER = 3 + + __FCGI_TYPE_BEGIN = 1 + __FCGI_TYPE_ABORT = 2 + __FCGI_TYPE_END = 3 + __FCGI_TYPE_PARAMS = 4 + __FCGI_TYPE_STDIN = 5 + __FCGI_TYPE_STDOUT = 6 + __FCGI_TYPE_STDERR = 7 + __FCGI_TYPE_DATA = 8 + __FCGI_TYPE_GETVALUES = 9 + __FCGI_TYPE_GETVALUES_RESULT = 10 + __FCGI_TYPE_UNKOWNTYPE = 11 + + __FCGI_HEADER_SIZE = 8 + + FCGI_STATE_SEND = 1 + FCGI_STATE_ERROR = 2 + FCGI_STATE_SUCCESS = 3 + + def __init__(self, host, port, timeout, keepalive): + self.host = host + self.port = port + self.timeout = timeout + if keepalive: + self.keepalive = 1 + else: + self.keepalive = 0 + self.sock = None + self.requests = dict() + + def __connect(self): + if self.port == None: + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + else: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(self.timeout) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + if self.port != None: + self.sock.connect((self.host, int(self.port))) + else: + self.sock.connect(self.host) + except socket.error as msg: + self.sock.close() + self.sock = None + print(repr(msg)) + return False + return True + + def _chr(self,num): + if sys.version_info[0] == 3: + return chr(num).encode('latin1') + return chr(num) + + def _ord(self,sbody): + if sys.version_info[0] == 3: + return sbody + return ord(sbody) + + def __encodeFastCGIRecord(self, fcgi_type, content, requestid): + if type(content) == str: content = content.encode() + length = len(content) + + return self._chr(self.__FCGI_VERSION) \ + + self._chr(fcgi_type) \ + + self._chr((requestid >> 8) & 0xFF) \ + + self._chr(requestid & 0xFF) \ + + self._chr((length >> 8) & 0xFF) \ + + self._chr(length & 0xFF) \ + + self._chr(0) \ + + self._chr(0) \ + + content + + def __encodeNameValueParams(self, name, value): + nLen = len(str(name)) + vLen = len(str(value)) + record = b'' + if nLen < 128: + record += self._chr(nLen) + else: + record += self._chr((nLen >> 24) | 0x80) \ + + self._chr((nLen >> 16) & 0xFF) \ + + self._chr((nLen >> 8) & 0xFF) \ + + self._chr(nLen & 0xFF) + if vLen < 128: + record += self._chr(vLen) + else: + record += self._chr((vLen >> 24) | 0x80) \ + + self._chr((vLen >> 16) & 0xFF) \ + + self._chr((vLen >> 8) & 0xFF) \ + + self._chr(vLen & 0xFF) + return record + str(name).encode() + str(value).encode() + + def __decodeFastCGIHeader(self, stream): + header = dict() + header['version'] = self._ord(stream[0]) + header['type'] = self._ord(stream[1]) + header['requestId'] = (self._ord(stream[2]) << 8) + self._ord(stream[3]) + header['contentLength'] = (self._ord(stream[4]) << 8) + self._ord(stream[5]) + header['paddingLength'] = self._ord(stream[6]) + header['reserved'] = self._ord(stream[7]) + return header + + def __decodeFastCGIRecord(self): + header = self.sock.recv(int(self.__FCGI_HEADER_SIZE)) + if not header: + return False + else: + record = self.__decodeFastCGIHeader(header) + record['content'] = b'' + if 'contentLength' in record.keys(): + contentLength = int(record['contentLength']) + buffer = self.sock.recv(contentLength) + + while contentLength and buffer: + contentLength -= len(buffer) + record['content'] += buffer + if 'paddingLength' in record.keys(): + skiped = self.sock.recv(int(record['paddingLength'])) + return record + + def request(self, nameValuePairs={}, post=''): + if not self.__connect(): + raise Exception('Connection service failed, please check if the specified service is started!') + return + + requestId = random.randint(1, (1 << 16) - 1) + self.requests[requestId] = dict() + request = b"" + beginFCGIRecordContent = self._chr(0) \ + + self._chr(self.__FCGI_ROLE_RESPONDER) \ + + self._chr(self.keepalive) \ + + self._chr(0) * 5 + request += self.__encodeFastCGIRecord(self.__FCGI_TYPE_BEGIN, + beginFCGIRecordContent, requestId) + + paramsRecord = b'' + + if nameValuePairs: + v_items = sorted(nameValuePairs.items()) + for (name, value) in v_items: + paramsRecord += self.__encodeNameValueParams(name, value) + + + if paramsRecord: + request += self.__encodeFastCGIRecord(self.__FCGI_TYPE_PARAMS, paramsRecord, requestId) + + request += self.__encodeFastCGIRecord(self.__FCGI_TYPE_PARAMS, b'', requestId) + + if post: + request += self.__encodeFastCGIRecord(self.__FCGI_TYPE_STDIN, post, requestId) + request += self.__encodeFastCGIRecord(self.__FCGI_TYPE_STDIN, b'', requestId) + + self.sock.send(request) + self.requests[requestId]['state'] = self.FCGI_STATE_SEND + self.requests[requestId]['response'] = b'' + return self.__waitForResponse(requestId) + + def __waitForResponse(self, requestId): + while True: + response = self.__decodeFastCGIRecord() + if not response: + break + if response['type'] == self.__FCGI_TYPE_STDOUT \ + or response['type'] == self.__FCGI_TYPE_STDERR: + if response['type'] == self.__FCGI_TYPE_STDERR: + self.requests['state'] = self.FCGI_STATE_ERROR + if requestId == int(response['requestId']): + self.requests[requestId]['response'] += response['content'] + if response['type'] == self.FCGI_STATE_SUCCESS: + self.requests[requestId] + if self.requests[requestId]['response'].find(b'\r\n\r\n') != -1: + tmp = b"" + tmp2 = self.requests[requestId]['response'].split(b'\r\n\r\n') + for i in range(len(tmp2)): + if i == 0: continue + tmp += tmp2[i] + b'\r\n\r\n' + self.requests[requestId]['response'] = tmp.strip() + return self.requests[requestId]['response'] + + def __repr__(self): + return "fastcgi connect host:{} port:{}".format(self.host, self.port) diff --git a/class_v2/file_execute_deny_v2.py b/class_v2/file_execute_deny_v2.py new file mode 100644 index 00000000..4eab1dc3 --- /dev/null +++ b/class_v2/file_execute_deny_v2.py @@ -0,0 +1,279 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2020 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: zhwen +#------------------------------------------------------------------- + +#------------------------------ +# 禁止某个目录运行PHP +#------------------------------ +import public,re,os,json,shutil +from public.validate import Param + +class FileExecuteDeny: + + def _init_conf(self,website): + self.ng_website_conf = '/www/server/panel/vhost/nginx/{}.conf'.format(website) + self.ap_website_conf = '/www/server/panel/vhost/apache/{}.conf'.format(website) + self.ols_website_conf = '/www/server/panel/vhost/openlitespeed/detail/{}.conf'.format(website) + self.webserver = public.get_webserver() + + # 获取某个网站禁止运行的目录规则 + def get_file_deny(self,args): + ''' + # 添加某个网站禁止运行PHP + author: zhwen + :param args: website 网站名 str + :return: + ''' + # 校验参数 + try: + get=args + get.validate([ + Param('website').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._init_conf(args.website) + if self.webserver == 'nginx': + data=self._get_nginx_file_deny() + elif self.webserver == 'apache': + data = self._get_apache_file_deny() + else: + data = self._get_ols_file_deny() + return public.return_message(0,0,data) + + def _get_nginx_file_deny(self): + conf = public.readFile(self.ng_website_conf) + if not conf: + return False + data = re.findall('BEGIN_DENY_.*',conf) + deny_name = [] + for i in data: + tmp = i.split('_') + if len(tmp) > 2: + deny_name.append('_'.join(tmp[2:])) + else: + deny_name.append(tmp[-1]) + result = [] + for i in deny_name: + reg = '#BEGIN_DENY_{}\n\\s*location\\s*\\~\\*\\s*\\^(.*)\\.\\*.*\\((.*)\\)\\$'.format(i.replace("|",r"\|")) + re_tmp = re.search(reg,conf) + if re_tmp: + deny_directory = re_tmp.groups()[0] + deny_suffix = re_tmp.groups()[1] + result.append({'name':i,'dir':deny_directory,'suffix':deny_suffix}) + return result + + def _get_apache_file_deny(self): + conf = public.readFile(self.ap_website_conf) + if not conf: + return False + data = re.findall('BEGIN_DENY_.*',conf) + deny_name = [] + for i in data: + tmp = i.split('_') + if len(tmp) > 2: + deny_name.append('_'.join(tmp[2:])) + else: + deny_name.append(tmp[-1]) + result = [] + for i in deny_name: + reg = '#BEGIN_DENY_{}\n\\s* 2: + deny_name.append('_'.join(tmp[2:])) + else: + deny_name.append(tmp[-1]) + result = [] + for i in deny_name: + reg = '#BEGIN_DENY_{}\n\\s*rules\\s*RewriteRule\\s*\\^(.*)\\.\\*.*\\((.*)\\)\\$'.format(i.replace("|",r"\|")) + deny_directory = re.search(reg, conf).groups()[0] + deny_suffix = re.search(reg,conf).groups()[1] + result.append({'name':i,'dir':deny_directory,'suffix':deny_suffix}) + return result + + def set_file_deny(self,args): + ''' + # 添加某个网站禁止运行PHP + author: zhwen + :param args: website 网站名 str + :param args: deny_name 规则名称 str + :param args: suffix 禁止访问的后续名 str + :param args: dir 禁止访问的目录 str + :param args: deny_name 规则名称 + :param args: act 操作方法 + :return: + ''' + # 校验参数 + try: + get=args + get.validate([ + Param('deny_name').String(), + Param('suffix').String(), + Param('dir').String(), + Param('act').String(), + Param('website').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + tmp = self._check_args(args) + if tmp: + return tmp + deny_name = args.deny_name + if not re.match(r"^\w+$",deny_name): return public.return_message(-1,0,'The rule name can only be composed of letters, numbers, and underscores!') + dir = args.dir + suffix = args.suffix + website = args.website + if suffix[-1] == "|": + suffix = suffix[:-1] + self._init_conf(website) + conf = public.readFile(self.ng_website_conf) + if not conf: + return public.return_message(-1,0,False) + data = re.findall('BEGIN_DENY_.*',conf) + exist_deny_name = [i.split('_')[-1] for i in data] + if args.act == 'edit': + if deny_name not in exist_deny_name: + return public.return_message(-1,0, 'The specify rule name is not exists! [ {} ]'.format(deny_name)) + self.del_file_deny(args) + else: + if deny_name in exist_deny_name: + return public.return_message(-1,0,'The specify rule name is already exists! [ {} ]'.format(deny_name)) + self._set_nginx_file_deny(deny_name,dir,suffix) + self._set_apache_file_deny(deny_name,dir,suffix) + self._set_ols_file_deny(deny_name,dir,suffix) + public.serviceReload() + return public.return_message(0,0,'Setup successfully!') + + def _set_nginx_file_deny(self,name,dir=None,suffix=None): + conf = public.readFile(self.ng_website_conf) + if not conf: + return False + if not dir and not suffix: + reg = '\\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\n'.format(n=name) + conf = re.sub(reg,'',conf) + else: + if dir[0] != '/':dir = '/'+dir + if dir[-1] != '/':dir = dir+'/' + new = ''' + #BEGIN_DENY_%s + location ~* ^%s.*.(%s)$ { + deny all; + } + #END_DENY_%s +''' % (name,dir,suffix,name) + if '#BEGIN_DENY_{}\n'.format(name) in conf: + return True + conf = re.sub('#ERROR-PAGE-END','#ERROR-PAGE-END'+new,conf) + public.writeFile(self.ng_website_conf,conf) + return True + + def _set_apache_file_deny(self,name,dir=None,suffix=None): + conf = public.readFile(self.ap_website_conf) + if not conf: + return False + if not dir and not suffix: + reg = '\\s*#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}'.format(n=name) + conf = re.sub(reg,'',conf) + else: + if dir[0] != '/':dir = '/'+dir + if dir[-1] != '/':dir = dir+'/' + new = r''' + #BEGIN_DENY_{n} + + Order allow,deny + Deny from all + + #END_DENY_{n} +'''.format(n=name,d=dir,s=suffix) + if '#BEGIN_DENY_{}'.format(name) in conf: + return True + conf = re.sub(r'#DENY\s*FILES',new+'\n #DENY FILES',conf) + public.writeFile(self.ap_website_conf,conf) + return True + + def _set_ols_file_deny(self,name,dir=None,suffix=None): + conf = public.readFile(self.ols_website_conf) + if not conf: + return False + if not dir and not suffix: + reg = '#BEGIN_DENY_{n}\n(.|\n)*#END_DENY_{n}\\s*'.format(n=name) + conf = re.sub(reg,'',conf) + else: + new = r''' + #BEGIN_DENY_{n} + rules RewriteRule ^{d}.*\.({s})$ - [F,L] + #END_DENY_{n} +'''.format(n=name,d=dir,s=suffix) + if '#BEGIN_DENY_{}'.format(name) in conf: + return True + conf = re.sub(r'autoLoadHtaccess\s*1','autoLoadHtaccess 1'+new,conf) + public.writeFile(self.ols_website_conf,conf) + return True + + # 删除某个网站禁止运行PHP + def del_file_deny(self,args): + ''' + # 添加某个网站禁止运行PHP + author: zhwen + :param args: website 网站名 str + :param args: deny_name 规则名称 str + :return: + ''' + # 校验参数 + try: + get=args + get.validate([ + Param('deny_name').String(), + Param('website').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._init_conf(args.website) + deny_name = args.deny_name + self._set_nginx_file_deny(deny_name) + self._set_apache_file_deny(deny_name) + self._set_ols_file_deny(deny_name) + public.serviceReload() + return public.return_message(0,0,'Successfully deleted!') + + # 检查传入参数 + def _check_args(self,args): + if hasattr(args,'deny_name'): + if len(args.deny_name) < 3: + return public.return_message(-1,0, 'Rule name needs to be greater than 3 bytes') + if hasattr(args,'suffix'): + if not args.suffix: + return public.return_message(-1,0, 'File suffix cannot be empty') + if hasattr(args,'dir'): + if not args.dir: + return public.return_message(-1,0, 'Directory cannot be empty') diff --git a/class_v2/files_v2.py b/class_v2/files_v2.py new file mode 100644 index 00000000..4e1c9174 --- /dev/null +++ b/class_v2/files_v2.py @@ -0,0 +1,3333 @@ +#!/usr/bin/env python +#coding:utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +from base64 import b64encode +import sys +import os +import public +import time +import json +import pwd +import cgi +import shutil +import re +import sqlite3 +from BTPanel import session, request +from public.validate import Param + + +class files: + run_path = None + path_permission_list = list() + path_permission_exclude_list = list() + file_permission_list = list() + sqlite_connection = None + download_list = None + download_is_rm = None + recycle_list = [] + download_token_list = None + # 检查敏感目录 + + def CheckDir(self, path): + path = path.replace('//', '/') + if path[-1:] == '/': + path = path[:-1] + + nDirs = ('', + '/', + '/*', + '/www', + '/root', + '/boot', + '/bin', + '/etc', + '/home', + '/dev', + '/sbin', + '/var', + '/usr', + '/tmp', + '/sys', + '/proc', + '/media', + '/mnt', + '/opt', + '/lib', + '/srv', + '/selinux', + '/www/server', + '/www/server/data', + '/www/.Recycle_bin', + public.GetConfigValue('logs_path'), + public.GetConfigValue('setup_path')) + + return not path in nDirs + + # 网站文件操作前置检测 + def site_path_check(self, get): + try: + if not 'site_id' in get: + return True + if not self.run_path: + self.run_path, self.path, self.site_name = self.GetSiteRunPath( + get.site_id) + if 'path' in get: + if get.path.find(self.path) != 0: + return False + if 'sfile' in get: + if get.sfile.find(self.path) != 0: + return False + if 'dfile' in get: + if get.dfile.find(self.path) != 0: + return False + return True + except: + return True + + # 网站目录后续安全处理 + def site_path_safe(self, get): + try: + if not 'site_id' in get: + return True + run_path, path, site_name = self.GetSiteRunPath(get.site_id) + if not os.path.exists(run_path): + os.makedirs(run_path) + ini_path = run_path + '/.user.ini' + if os.path.exists(ini_path): + return True + sess_path = '/www/php_session/%s' % site_name + if not os.path.exists(sess_path): + os.makedirs(sess_path) + ini_conf = '''open_basedir={}/:/tmp/:/proc/:{}/ +session.save_path={}/ +session.save_handler = files'''.format(path, sess_path, sess_path) + public.writeFile(ini_path, ini_conf) + public.ExecShell("chmod 644 %s" % ini_path) + public.ExecShell("chdir +i %s" % ini_path) + return True + except: + return False + + # 取当站点前运行目录 + def GetSiteRunPath(self, site_id): + try: + find = public.M('sites').where( + 'id=?', (site_id,)).field('path,name').find() + siteName = find['name'] + sitePath = find['path'] + if public.get_webserver() == 'nginx': + filename = public.get_vhost_path() + '/nginx/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r'\s*root\s+(.+);' + tmp1 = re.search(rep, conf) + if tmp1: + path = tmp1.groups()[0] + else: + filename = public.get_vhost_path() + '/apache/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = '\\s*DocumentRoot\\s*"(.+)"\\s*\n' + tmp1 = re.search(rep, conf) + if tmp1: + path = tmp1.groups()[0] + return path, sitePath, siteName + except: + return sitePath, sitePath, siteName + + # 检测文件名 + def CheckFileName(self, filename): + nots = ['\\', '&', '*', '|', ';', '"', "'", '<', '>'] + if filename.find('/') != -1: + filename = filename.split('/')[-1] + for n in nots: + if n in filename: + return False + return True + + # 名称输出过滤 + def xssencode(self, text): + list = ['<', '>'] + ret = [] + for i in text: + if i in list: + i = '' + ret.append(i) + str_convert = ''.join(ret) + if sys.version_info[0] == 3: + import html + text2 = html.escape(str_convert, quote=True) + else: + text2 = cgi.escape(str_convert, quote=True) + + reps = {'&':'&'} + for rep in reps.keys(): + if text2.find(rep) != -1: text2 = text2.replace(rep,reps[rep]) + return text2 + + # 名称输入系列化 + def xssdecode(self,text): + try: + cs = {""":'"',"'":"'"} + for c in cs.keys(): + text = text.replace(c,cs[c]) + + str_convert = text + if sys.version_info[0] == 3: + import html + text2 = html.unescape(str_convert) + else: + text2 = cgi.unescape(str_convert) + return text2 + except: + return text + + # 上传文件 + def UploadFile(self, get): + from werkzeug.utils import secure_filename + from BTPanel import request + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + if not os.path.exists(get.path): + os.makedirs(get.path) + f = request.files['zunfile'] + filename = os.path.join(get.path, f.filename) + if sys.version_info[0] == 2: + filename = filename.encode('utf-8') + s_path = get.path + if os.path.exists(filename): + s_path = filename + p_stat = os.stat(s_path) + f.save(filename) + os.chown(filename, p_stat.st_uid, p_stat.st_gid) + os.chmod(filename, p_stat.st_mode) + public.WriteLog('TYPE_FILE', 'FILE_UPLOAD_SUCCESS', + (filename, get['path'])) + return public.returnMsg(True, 'FILE_UPLOAD_SUCCESS') + + def f_name_check(self,filename): + ''' + @name 文件名检测2 + @author hwliang<2021-03-16> + @param filename 文件名 + @return bool + ''' + f_strs = [';','&','<','>'] + for fs in f_strs: + if filename.find(fs) != -1: + return False + return True + + # 上传前检查文件是否存在 + def upload_file_exists(self,args): + ''' + @name 上传前检查文件是否存在 + @author hwliang<2021-11-3> + @param filename 文件名 + @return dict + ''' + filename = args.filename.strip() + if not os.path.exists(filename): + return public.returnMsg(False,'File does not exist!') + file_info = {} + _stat = os.stat(filename) + file_info['size'] = _stat.st_size + file_info['mtime'] = int(_stat.st_mtime) + file_info['isfile'] = os.path.isfile(filename) + return public.returnMsg(True,file_info) + + + def get_real_len(self,string): + ''' + @name 获取含中文的字符串字精确长度 + @author hwliang<2021-11-3> + @param string + @return int + ''' + real_len = len(string) + for s in string: + if '\u2E80' <= s <= '\uFE4F': + real_len += 1 + return real_len + + # 上传文件2 + def upload(self, args): + if not 'f_name' in args: + args.f_name = request.form.get('f_name') + args.f_path = request.form.get('f_path') + args.f_size = request.form.get('f_size') + args.f_start = request.form.get('f_start') + + if sys.version_info[0] == 2: + args.f_name = args.f_name.encode('utf-8') + args.f_path = args.f_path.encode('utf-8') + + try: + if self.get_real_len(args.f_name) > 256: return public.return_msg_gettext(False,'The file name contains more than 256 bytes') + except: + pass + + if not self.f_name_check(args.f_name): return public.return_msg_gettext(False,'No special characters can be included in the file name!') + + if args.f_path == '/': + return public.return_msg_gettext(False,'Cannot upload files to the system document root!') + + if args.f_name.find('./') != -1 or args.f_path.find('./') != -1: + return public.return_msg_gettext(False, 'Wrong parameter') + + if not os.path.exists(args.f_path): + os.makedirs(args.f_path, 493) + if not 'dir_mode' in args or not 'file_mode' in args: + self.set_mode(args.f_path) + + save_path = os.path.join( + args.f_path, args.f_name + '.' + str(int(args.f_size)) + '.upload.tmp') + d_size = 0 + if os.path.exists(save_path): + d_size = os.path.getsize(save_path) + + if d_size != int(args.f_start): + return d_size + + try: + f = open(save_path, 'ab') + if 'b64_data' in args: + import base64 + b64_data = base64.b64decode(args.b64_data) + f.write(b64_data) + else: + upload_files = request.files.getlist("blob") + for tmp_f in upload_files: + f.write(tmp_f.read()) + f.close() + except Exception as ex: + ex = str(ex) + if ex.find('No space left on device') != -1: + return public.fail_v2('Not enough disk space') + + f_size = os.path.getsize(save_path) + if f_size != int(args.f_size): + return f_size + + new_name = os.path.join(args.f_path, args.f_name) + if os.path.exists(new_name): + if new_name.find('.user.ini') != -1: + public.ExecShell("chattr -i " + new_name) + try: + os.remove(new_name) + except: + public.ExecShell("rm -f %s" % new_name) + + os.renames(save_path, new_name) + if 'dir_mode' in args and 'file_mode' in args: + mode_tmp1 = args.dir_mode.split(',') + public.set_mode(args.f_path, mode_tmp1[0]) + public.set_own(args.f_path, mode_tmp1[1]) + mode_tmp2 = args.file_mode.split(',') + public.set_mode(new_name, mode_tmp2[0]) + public.set_own(new_name, mode_tmp2[1]) + + else: + self.set_mode(new_name) + + if new_name.find('.user.ini') != -1: + public.ExecShell("chattr +i " + new_name) + + public.write_log_gettext('File manager', 'Successfully uploaded [ {} ] !',(new_name,), + (args.f_name, args.f_path)) + + return public.success_v2('Successfully uploaded!') + + # 设置文件和目录权限 + def set_mode(self, path): + if path[-1] == '/': path = path[:-1] + s_path = os.path.dirname(path) + p_stat = os.stat(s_path) + os.chown(path,p_stat.st_uid,p_stat.st_gid) + if os.path.isfile(path): + os.chmod(path, 0o644) + else: + os.chmod(path,p_stat.st_mode) + + # 是否包含composer.json + def is_composer_json(self,path): + if os.path.exists(path + '/composer.json'): + return '1' + return '0' + + def __check_favorite(self,filepath,favorites_info): + for favorite in favorites_info: + if filepath == favorite['path']: + return '1' + return '0' + + def __get_topping_data(self): + """ + @获取置顶配置 + """ + data = {} + conf_file = '{}/data/toping.json'.format(public.get_panel_path()) + try : + if os.path.exists(conf_file): + data = json.loads(public.readFile(conf_file)) + except:pass + return data + + def __check_topping(self,filepath,top_info): + """ + @name 检测文件或者目录是否置顶 + @param filepath: 文件路径 + """ + if filepath in top_info: + return '1' + import html + filepath = html.unescape(filepath) + if filepath in top_info: + return '1' + return '0' + + + def __check_share(self,filename): + if self.download_token_list == None: + self.download_token_list = {} + my_table = 'download_token' + download_list = public.M(my_table).field('id,filename').select() + for k in download_list: + self.download_token_list[k['filename']] = k['id'] + + return str(self.download_token_list.get(filename,'0')) + + + def __filename_flater(self,filename): + ms = {";":""} + for m in ms.keys(): + filename = filename.replace(m,ms[m]) + return filename + + # 取文件/目录列表 + def GetDir(self, get): + if not hasattr(get, 'path'): + # return public.returnMsg(False,'错误的参数!') + get.path = public.get_site_path() #'/www/wwwroot' + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + if get.path == '': + get.path = '/www' + + # 转换包含~的路径 + if get.path.find('~') != -1: + get.path = os.path.expanduser(get.path) + + get.path = self.xssdecode(get.path) + if not os.path.exists(get.path): + get.path = public.get_site_path() + #return public.ReturnMsg(False, '指定目录不存在!') + if os.path.basename(get.path) == '.Recycle_bin': + return public.return_message(-1,0,'Recovery failed!') + if not os.path.isdir(get.path): + get.path = os.path.dirname(get.path) + + if not os.path.isdir(get.path): + return public.return_message(-1,0,'This is not a directory') + + import pwd + dirnames = [] + filenames = [] + + search = None + if hasattr(get, 'search'): + search = get.search.strip().lower() + public.set_search_history('files','get_list',search) + if hasattr(get, 'all'): + return public.return_message(0,0,self.SearchFiles(get)) + + # 包含分页类 + import page + # 实例化分页类 + page = page.Page() + info = {} + info['count'] = self.GetFilesCount(get.path, search) + info['row'] = 500 + if 'disk' in get: + if get.disk == 'true': info['row'] = 2000 + if 'share' in get and get.share: + info['row'] = 5000 + info['p'] = 1 + if hasattr(get, 'p'): + try: + info['p'] = int(get['p']) + except: + info['p'] = 1 + + info['uri'] = {} + info['return_js'] = '' + if hasattr(get, 'tojs'): + info['return_js'] = get.tojs + if hasattr(get, 'showRow'): + info['row'] = int(get.showRow) + + # 获取分页数据 + data = {} + data['PAGE'] = page.GetPage(info, '1,2,3,4,5,6,7,8') + + i = 0 + n = 0 + + top_data = self.__get_topping_data() + data['STORE'] = self.get_files_store(None) + data['FILE_RECYCLE'] = os.path.exists('data/recycle_bin.pl') + + if not hasattr(get, 'reverse'): get.reverse = 'False' + if not hasattr(get, 'sort'): get.sort = 'name' + reverse = bool(get.reverse) + if get.reverse == 'False': + reverse = False + for file_info in self.__list_dir(get.path, get.sort, reverse): + filename = os.path.join(get.path, file_info[0]) + if search: + if file_info[0].lower().find(search) == -1: + continue + i += 1 + if n >= page.ROW: + break + if i < page.SHIFT: + continue + if not os.path.exists(filename) and not os.path.islink(filename): continue + file_info = self.__format_stat(filename, get.path) + if not file_info: continue + favorite = self.__check_favorite(filename, data['STORE']) + r_file = self.__filename_flater(file_info['name']) + ';' + str(file_info['size']) + ';' + str(file_info['mtime']) + ';' + str( + file_info['accept']) + ';' + file_info['user'] + ';' + file_info['link']+';'\ + + self.get_download_id(filename) + ';' + self.is_composer_json(filename)+';'\ + + favorite+';'+self.__check_share(filename) + if os.path.isdir(filename): + dirnames.append(r_file) + else: + filenames.append(r_file) + n += 1 + + data['DIR'] = dirnames + data['FILES'] = filenames + data['PATH'] = str(get.path) + + #2022-07-29,增加置顶排序 + tmp_dirs = [] + for i in range(len(data['DIR'])): + filepath = os.path.join(data['PATH'] , data['DIR'][i].split(';')[0]) + toping = self.__check_topping(filepath,top_data) + info = data['DIR'][i] + ';' + self.get_file_ps(filepath)+';'+toping + if toping == '1': + tmp_dirs.insert(0, info) + else: + tmp_dirs.append(info) + + tmp_files = [] + for i in range(len(data['FILES'])): + filepath = os.path.join(data['PATH'] , data['FILES'][i].split(';')[0]) + toping = self.__check_topping(filepath,top_data) + info = data['FILES'][i] + ';' + self.get_file_ps(filepath)+';'+toping + if toping == '1': + tmp_files.insert(0, info) + else: + tmp_files.append(info) + data['DIR'] = tmp_dirs + data['FILES'] = tmp_files + + if hasattr(get, 'disk'): + import system + data['DISK'] = system.system().GetDiskInfo() + + data['dir_history'] = public.get_dir_history('files','GetDirList') + data['search_history'] = public.get_search_history('files','get_list') + public.set_dir_history('files','GetDirList',data['PATH']) + + return public.return_message(0,0,data) + + + def get_file_ps(self,filename): + ''' + @name 获取文件或目录备注 + @author hwliang<2020-10-22> + @param filename 文件或目录全路径 + @return string + ''' + + ps_path = public.get_panel_path() + '/data/files_ps' + f_key1 = '/'.join((ps_path,public.md5(filename))) + if os.path.exists(f_key1): + return public.readFile(f_key1) + + f_key2 = '/'.join((ps_path,public.md5(os.path.basename(filename)))) + if os.path.exists(f_key2): + return public.readFile(f_key2) + + pss = { + '/www/server/data':'MySQL data storage directory!', + '/www/server/mysql':'MySQL program directory', + '/www/server/redis':'Redis program directory', + '/www/server/mongodb':'MongoDB program directory', + '/www/server/nvm':'PM2/NVM/NPM program directory', + '/www/server/pass':'Website Basic Auth authentication password storage directory', + '/www/server/speed':'Website speed plugin directory', + '/www/server/docker':'Docker and data directory', + '/www/server/total':'Website Statistics Directory', + '/www/server/btwaf':'WAF directory', + '/www/server/pure-ftpd':'ftp program directory', + '/www/server/phpmyadmin':'phpMyAdmin program directory', + '/www/server/rar':'rar extension library directory, after deleting, it will lose support for RAR compressed files', + '/www/server/stop':'Website disabled page directory, please do not delete!', + '/www/server/nginx':'Nginx program directory', + '/www/server/apache':'Apache program directory', + '/www/server/cron':'Cron script and log directory', + '/www/server/php':'All interpreters of PHP versions are in this directory', + '/www/server/tomcat':'Tomcat program directory', + '/www/php_session':'PHP-SESSION Quarantine directory', + '/proc': 'System process directory', + '/dev': 'System Device Catalog', + '/sys': 'System call directory', + '/tmp': 'System temporary file directory', + '/var/log': 'System log directory', + '/var/run': 'System operation log directory', + '/var/spool': 'System queue directory', + '/var/lock': 'System lock directory', + '/var/mail': 'System mail directory', + '/mnt': 'System mount directory', + '/media': 'Multimedia catalog', + '/dev/shm': 'Shared memory directory', + '/lib': 'System dynamic library directory', + '/lib64': 'System dynamic library directory', + '/lib32': 'System dynamic library directory', + '/usr/lib': 'System dynamic library directory', + '/usr/lib64': 'System dynamic library directory', + '/usr/local/lib': 'System dynamic library directory', + '/usr/local/lib64': 'System dynamic library directory', + '/usr/local/libexec': 'System dynamic library directory', + '/usr/local/sbin': 'System script directory', + '/usr/local/bin': 'System script directory' + } + if filename in pss: return "PS:" + pss[filename] + + if not self.recycle_list: self.recycle_list = public.get_recycle_bin_list() + if filename + '/' in self.recycle_list: return 'PS: Recycle Bin Directory' + if filename in self.recycle_list: return 'PS: Recycle Bin Directory' + return '' + + + def set_file_ps(self,args): + ''' + @name 设置文件或目录备注 + @author hwliang<2020-10-22> + @param filename 文件或目录全路径 + @param ps_type 备注类型 0.完整路径 1.文件名称 + @param ps_body 备注内容 + @return dict + ''' + filename = args.filename.strip() + ps_type = int(args.ps_type) + ps_body = public.xssencode2(args.ps_body) + ps_path = public.get_panel_path() + '/data/files_ps' + if not os.path.exists(ps_path): + os.makedirs(ps_path,384) + if ps_type == 1: + f_name = os.path.basename(filename) + else: + f_name = filename + ps_key = public.md5(f_name) + + f_key = '/'.join((ps_path,ps_key)) + if ps_body: + public.writeFile(f_key,ps_body) + public.write_log_gettext('File manager','Set the file name [{}], notes: {}',(f_name,ps_body)) + else: + if os.path.exists(f_key): + os.remove(f_key) + public.write_log_gettext('File manager','Clear file notes [{}]',(f_name)) + return public.return_msg_gettext(True,'Setup successfully!') + + + + def check_file_sort(self,sort): + """ + @校验排序字段 + """ + slist = ['name','size','mtime','accept','user'] + if sort in slist: return sort + return 'name' + + def __list_dir(self, path, my_sort='name', reverse=False): + ''' + @name 获取文件列表,并排序 + @author hwliang<2020-08-01> + @param path 路径 + @param my_sort 排序字段 + @param reverse 是否降序 + @param list + ''' + if not os.path.exists(path): + return [] + py_v = sys.version_info[0] + tmp_files = [] + + for f_name in os.listdir(path): + try: + if py_v == 2: + f_name = f_name.encode('utf-8') + else: + f_name.encode('utf-8') + + #使用.join拼接效率更高 + filename = "/".join((path,f_name)) + sort_key = 1 + sort_val = None + if not os.path.islink(filename): + #此处直接做异常处理比先判断文件是否存在更高效 + if my_sort == 'name': + sort_key = 0 + elif my_sort == 'size': + sort_val = os.stat(filename).st_size + elif my_sort == 'mtime': + sort_val = os.stat(filename).st_mtime + elif my_sort == 'accept': + sort_val = os.stat(filename).st_mode + elif my_sort == 'user': + sort_val = os.stat(filename).st_uid + except Exception as err: + continue + #使用list[tuple]排序效率更高 + tmp_files.append((f_name,sort_val)) + try: + tmp_files = sorted(tmp_files, key=lambda x: x[sort_key], reverse=reverse) + except:pass + return tmp_files + + def __format_stat(self, filename, path): + try: + stat = self.__get_stat(filename, path) + if not stat: + return None + tmp_stat = stat.split(';') + file_info = {'name': self.xssencode(tmp_stat[0].replace('/', '')), 'size': int(tmp_stat[1]), 'mtime': int( + tmp_stat[2]), 'accept': int(tmp_stat[3]), 'user': tmp_stat[4], 'link': tmp_stat[5]} + return file_info + except: + return None + + def SearchFiles(self, get): + if not hasattr(get, 'path'): + get.path = public.get_site_path() + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + if not os.path.exists(get.path): + get.path = '/www' + search = get.search.strip().lower() + my_dirs = [] + my_files = [] + count = 0 + max = 3000 + for d_list in os.walk(get.path): + if count >= max: + break + for d in d_list[1]: + if count >= max: + break + d = self.xssencode(d) + if d.lower().find(search) != -1: + filename = d_list[0] + '/' + d + if not os.path.exists(filename): + continue + my_dirs.append(self.__get_stat(filename, get.path)) + count += 1 + + for f in d_list[2]: + if count >= max: + break + f = self.xssencode(f) + if f.lower().find(search) != -1: + filename = d_list[0] + '/' + f + if not os.path.exists(filename): + continue + my_files.append(self.__get_stat(filename, get.path)) + count += 1 + data = {} + data['DIR'] = sorted(my_dirs) + data['FILES'] = sorted(my_files) + data['PATH'] = str(get.path) + data['PAGE'] = public.get_page( + len(my_dirs) + len(my_files), 1, max, 'GetFiles')['page'] + data['STORE'] = self.get_files_store(None) + return data + + def __get_stat(self, filename, path=None): + if os.path.islink(filename) and not os.path.exists(filename): + accept = "0" + mtime = "0" + user = "0" + size = "0" + else: + stat = os.stat(filename) + accept = str(oct(stat.st_mode)[-3:]) + mtime = str(int(stat.st_mtime)) + user = '' + try: + user = pwd.getpwuid(stat.st_uid).pw_name + except: + user = str(stat.st_uid) + size = str(stat.st_size) + link = '' + down_url = self.get_download_id(filename) + if os.path.islink(filename): + link = ' -> ' + os.readlink(filename) + tmp_path = (path + '/').replace('//', '/') + if path and tmp_path != '/': + filename = filename.replace(tmp_path, '',1) + favorite = self.__check_favorite(filename, self.get_files_store(None)) + return filename + ';' + size + ';' + mtime + ';' + accept + ';' + user + ';' + link+';'+ down_url+';'+ \ + self.is_composer_json(filename)+';'+favorite+';'+self.__check_share(filename) + + #获取指定目录下的所有视频或音频文件 + def get_videos(self,args): + path = args.path.strip() + v_data = [] + if not os.path.exists(path): return v_data + import mimetypes + for fname in os.listdir(path): + try: + filename = os.path.join(path,fname) + if not os.path.exists(filename): continue + if not os.path.isfile(filename): continue + v_tmp = {} + v_tmp['name'] = fname + v_tmp['type'] = mimetypes.guess_type(filename)[0] + v_tmp['size'] = os.path.getsize(filename) + if not v_tmp['type'].split('/')[0] in ['video']: + continue + v_data.append(v_tmp) + except:continue + return sorted(v_data,key=lambda x:x['name']) + + # 计算文件数量 + def GetFilesCount(self, path, search): + if os.path.isfile(path): + return 1 + if not os.path.exists(path): + return 0 + i = 0 + for name in os.listdir(path): + if search: + if name.lower().find(search) == -1: + continue + i += 1 + return i + + # 创建文件 + def CreateFile(self, get): + # 校验磁盘大小 + df_data = public.ExecShell("df -T | grep '/'")[0] + for data in str(df_data).split("\n"): + data_list = data.split() + if not data_list: continue + use_size = data_list[4] + size = data_list[5] + disk_path = data_list[6] + if int(use_size) < 1024 and str(size).rstrip("%") == "100" and disk_path in ["/","/www"]: + return public.return_msg_gettext(False, f"File creation failed! The disk is full! please clear the space first!") + + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8').strip() + try: + fname = os.path.basename(get.path).strip() + fpath = os.path.dirname(get.path).strip() + get.path = os.path.join(fpath,fname) + if get.path[-1] == '.': + return public.return_msg_gettext(False, 'It is not recommended to use [ . ] at the end of the file because there may be security risks') + if not self.CheckFileName(get.path): + return public.return_msg_gettext(False, 'File names can NOT contain special characters!') + if os.path.exists(get.path): + return public.return_msg_gettext(False, 'Requested file exists!') + path = os.path.dirname(get.path) + if not os.path.exists(path): + os.makedirs(path) + open(get.path, 'w+').close() + self.SetFileAccept(get.path) + public.write_log_gettext('File manager', 'Successfully created file [{}]!', (get.path,)) + return public.return_msg_gettext(True, 'Successfully created file!') + except: + return public.return_msg_gettext(False, 'Failed to create file!') + + #创建软链 + def CreateLink(self,get): + ''' + @name 创建软链接 + @author hwliang<2021-03-23> + @param get 源文件 + dfile 软链文件名 + }> + @return dict + ''' + if not get.dfile or get.dfile[-1] == "/": + return public.return_msg_gettext(False, + 'The specified soft link file name must contain the full path (full path)') + if not 'sfile' in get: return public.return_msg_gettext(False,'Parameter ERROR!') + if not os.path.exists(get.sfile): return public.return_msg_gettext(False,'Configuration file not exist') + if os.path.exists(get.dfile): return public.return_msg_gettext(False,'The specified soft link file name already exists') + l_name = os.path.basename(get.dfile) + if re.match(r"^[\w\-\.]+$", l_name) == None: return public.returnMsg(False, 'Link file name is illegal!') + if get.dfile[0] != '/': return public.return_msg_gettext(False,'The specified soft link file name must contain the full path (full path)') + public.ExecShell("ln -sf {} {}".format(get.sfile,get.dfile)) + if not os.path.exists(get.dfile): return public.return_msg_gettext(False,'Softlink file creation failed') + public.write_log_gettext('Firewall manager','Create softlink: {} -> {}',(get.dfile,get.sfile)) + return public.return_msg_gettext(True,'The softlink file was created successfully') + + + + # 创建目录 + def CreateDir(self, get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8').strip() + try: + if get.path[-1] == '.': + return public.return_msg_gettext(False, 'It is not recommended to use [ . ] at the end of the directory, because there may be safety risks') + if not self.CheckFileName(get.path): + return public.return_msg_gettext(False, 'Directory names cannot contain special characters!') + if os.path.exists(get.path): + return public.return_msg_gettext(False, 'Requested directory exists!') + os.makedirs(get.path) + self.SetFileAccept(get.path) + public.write_log_gettext('File manager', 'Successfully created directory [ {} ]!', (get.path,)) + return public.return_msg_gettext(True, 'Successfully created directory!') + except: + return public.return_msg_gettext(False,'Failed to create directory!') + + #删除目录 + def DeleteDir(self,get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + if os.path.basename(get.path) in ['Recycle_bin','.Recycle_bin']: + return public.return_msg_gettext(False,'Recovery failed!') + if not os.path.exists(get.path): + return public.return_msg_gettext(False, 'Requested directory does not exist') + + # 检查是否敏感目录 + if not self.CheckDir(get.path): + return public.return_msg_gettext(False, 'Editing this directory may cause service exceptions!') + + try: + # 检查是否存在.user.ini + # if os.path.exists(get.path+'/.user.ini'): + # public.ExecShell("chattr -i '"+get.path+"/.user.ini'") + public.ExecShell("chattr -R -i " + get.path) + if hasattr(get, 'empty'): + if not self.delete_empty(get.path): + return public.return_msg_gettext(False, 'Cannot delete non-empty directory!') + + if os.path.exists('data/recycle_bin.pl') and session.get('debug') != 1: + if self.Mv_Recycle_bin(get): + self.site_path_safe(get) + self.remove_file_ps(get) + public.add_security_logs("Del dir","Delete directory: "+get.path) + return public.return_msg_gettext(True, 'Directory moved to recycle bin!') + + import shutil + shutil.rmtree(get.path) + self.site_path_safe(get) + public.add_security_logs("Del dir", "Delete directory: " + get.path) + public.WriteLog('TYPE_FILE', 'Successfully deleted directory [{}]!', (get.path,)) + self.remove_file_ps(get) + return public.return_msg_gettext(True, ' Successfully deleted directory!') + except: + return public.return_msg_gettext(False, 'Failed to delete directory!') + + # 删除 空目录 + def delete_empty(self, path): + if sys.version_info[0] == 2: + path = path.encode('utf-8') + if len(os.listdir(path)) > 0: + return False + return True + + # 删除文件 + def DeleteFile(self, get): + # 校验参数 + try: + get.validate([ + Param('path').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + if not os.path.exists(get.path)and not os.path.islink(get.path): + # return public.return_msg_gettext(False, 'Configuration file not exist') + return public.return_message(-1, 0,'Configuration file not exist') + + # 检查是否为.user.ini + if get.path.find('.user.ini') != -1: + public.ExecShell("chattr -i '"+get.path+"'") + try: + if os.path.exists('data/recycle_bin.pl') and session.get('debug') != 1: + if self.Mv_Recycle_bin(get): + self.site_path_safe(get) + self.remove_file_ps(get) + public.add_security_logs("Del file", "Delete file: " + get.path) + return public.return_message(0, 0, 'File moved to recycle bin') + # return public.return_msg_gettext(True, 'File moved to recycle bin!') + os.remove(get.path) + self.site_path_safe(get) + public.write_log_gettext('File manager', 'Successfully permanent deleted file: [{}]!', (get.path,)) + public.add_security_logs("Del file", "Delete file: " + get.path) + self.remove_file_ps(get) + # return public.return_msg_gettext(True, 'Successfully deleted file!') + return public.return_message(0, 0, 'Successfully deleted file') + except: + # return public.return_msg_gettext(False, 'Failed to delete file!') + return public.return_message(-1, 0, 'Failed to delete file!') + + + def remove_file_ps(self,get): + ''' + @name 删除文件或目录的备注信息 + ''' + get.filename = get.path + get.ps_body = '' + get.ps_type = '0' + self.set_file_ps(get) + + # 移动到回收站 + def Mv_Recycle_bin(self, get): + rPath = public.get_recycle_bin_path(get.path) + rFile = os.path.join(rPath , get.path.replace('/', '_bt_') + '_t_' + str(time.time())) + try: + import shutil + shutil.move(get.path, rFile) + public.write_log_gettext('File manager', 'Successfully moved file [{}] to recycle bin!', (get.path,)) + return True + except: + public.write_log_gettext( + 'File manager', 'Failed to move file [{}] to recycle bin!', (get.path,)) + return False + + # 从回收站恢复 + def Re_Recycle_bin(self, get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + get.path = public.html_decode(get.path).replace(';','') + dFile = get.path.replace('_bt_', '/').split('_t_')[0] + + # 检查所在回收站目录 + recycle_bin_list = public.get_recycle_bin_list() + _ok = False + for r_path in recycle_bin_list: + for r_file in os.listdir(r_path): + if get.path == r_file: + _ok = True + rPath = r_path + get.path = os.path.join(rPath , get.path) + break + if _ok: break + + if dFile.find('BTDB_') != -1: + import database + return database.database().RecycleDB(get.path) + try: + import shutil + if os.path.isdir(get.path) and os.path.exists(dFile): + shutil.move(dFile,dFile + "_{}.bak".format(public.format_date("%Y%m%d%H%M%S"))) + shutil.move(get.path, dFile) + public.write_log_gettext('File manager', 'Successfully recovered [{}] from recycle bin!', (dFile,)) + return public.return_msg_gettext(True, 'Recovery succeeded!') + except: + public.write_log_gettext('File manager', 'Failed to recover [{}] from recycle bin!', (dFile,)) + return public.return_msg_gettext(False, 'Recovery failed!') + + # 获取回收站信息 + def Get_Recycle_bin(self, get): + data = {} + data['dirs'] = [] + data['files'] = [] + data['status'] = os.path.exists('data/recycle_bin.pl') + data['status_db'] = os.path.exists('data/recycle_bin_db.pl') + recycle_bin_list = public.get_recycle_bin_list() + for rPath in recycle_bin_list: + if not os.path.exists(rPath): continue + for file in os.listdir(rPath): + try: + tmp = {} + fname = os.path.join(rPath , file) + if sys.version_info[0] == 2: + fname = fname.encode('utf-8') + else: + fname.encode('utf-8') + tmp1 = file.split('_bt_') + tmp2 = tmp1[len(tmp1)-1].split('_t_') + file = self.xssencode(file) + tmp['rname'] = file + tmp['dname'] = file.replace('_bt_', '/').split('_t_')[0] + if tmp['dname'].find('@') != -1: + tmp['dname'] = "BTDB_" + tmp['dname'][5:].replace('@',"\\u").encode().decode("unicode_escape") + tmp['name'] = tmp2[0] + tmp['time'] = int(float(tmp2[1])) + if os.path.islink(fname): + filePath = os.readlink(fname) + if os.path.exists(filePath): + tmp['size'] = os.path.getsize(filePath) + else: + tmp['size'] = 0 + else: + tmp['size'] = os.path.getsize(fname) + if os.path.isdir(fname): + if file[:5] == 'BTDB_': + tmp['size'] = public.get_path_size(fname) + data['dirs'].append(tmp) + else: + data['files'].append(tmp) + except: + continue + + data['dirs'] = sorted(data['dirs'],key = lambda x: x['time'],reverse=True) + data['files'] = sorted(data['files'],key = lambda x: x['time'],reverse=True) + # return data + return public.return_message(0, 0, data) + + # 彻底删除 + def Del_Recycle_bin(self, get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + + get.path = public.html_decode(get.path).replace(';','') + + dFile = get.path.split('_t_')[0] + # 检查所在回收站目录 + recycle_bin_list = public.get_recycle_bin_list() + _ok = False + for r_path in recycle_bin_list: + for r_file in os.listdir(r_path): + if get.path == r_file: + _ok = True + rPath = r_path + filename = os.path.join(rPath , get.path) + break + if _ok: break + + + tfile = get.path.replace('_bt_', '/').split('_t_')[0] + if not _ok: return public.returnMsg(False, 'Error deleting file : {}', (tfile,)) + + if dFile.find('BTDB_') != -1: + import database + return database.database().DeleteTo(filename) + if not self.CheckDir(filename): + return public.return_msg_gettext(False, 'Never trouble troubles till troubles trouble you!') + + public.ExecShell('chattr -R -i ' + filename) + if os.path.isdir(filename): + import shutil + try: + shutil.rmtree(filename) + except: + public.ExecShell('chattr -R -a ' + filename) + public.ExecShell("rm -rf " + filename) + else: + try: + os.remove(filename) + except: + public.ExecShell("rm -f " + filename) + public.write_log_gettext('File manager', 'Parmanently deleted {} from recycle bin!', (tfile,)) + return public.return_msg_gettext(True, 'Parmanently deleted {} from recycle bin!', (tfile,)) + + # 清空回收站 + def Close_Recycle_bin(self, get): + + import database + import shutil + + recycle_bin_list = public.get_recycle_bin_list() + for rPath in recycle_bin_list: + public.ExecShell('chattr -R -i ' + rPath) + rlist = os.listdir(rPath) + i = 0 + l = len(rlist) + for name in rlist: + i += 1 + path = os.path.join(rPath , name) + public.writeSpeed(name, i, l) + if name.find('BTDB_') != -1: + database.database().DeleteTo(path) + continue + if os.path.isdir(path): + try: + shutil.rmtree(path) + except: + public.ExecShell('chattr -R -a ' + path) + public.ExecShell('rm -rf ' + path) + else: + try: + os.remove(path) + except: + public.ExecShell('rm -f ' + path) + + public.writeSpeed(None, 0, 0) + public.write_log_gettext('File manager', 'Recycle bin emptied!') + return public.return_msg_gettext(True, 'Recycle bin emptied!') + + # 回收站开关 + def Recycle_bin(self, get): + c = 'data/recycle_bin.pl' + if hasattr(get, 'db'): + c = 'data/recycle_bin_db.pl' + if os.path.exists(c): + os.remove(c) + public.write_log_gettext('File manager', 'Recycle bin feature turned off!') + return public.return_msg_gettext(True, 'Recycle bin feature turned off!') + else: + public.writeFile(c, 'True') + public.write_log_gettext('File manager', 'Recycle bin feature turned on!') + return public.return_msg_gettext(True, 'Recycle bin feature turned on!') + + # 复制文件 + def CopyFile(self, get): + if sys.version_info[0] == 2: + get.sfile = get.sfile.encode('utf-8') + get.dfile = get.dfile.encode('utf-8') + if get.dfile[-1] == '.': + return public.return_msg_gettext(False, 'It is not recommended to use [.] at the end of the file because there may be security risks') + if not os.path.exists(get.sfile): + return public.return_msg_gettext(False, 'Configuration file not exist') + + # if os.path.exists(get.dfile): + # return public.return_msg_gettext(False,'Requested file exists!') + + if os.path.isdir(get.sfile): + return self.CopyDir(get) + + import shutil + try: + shutil.copyfile(get.sfile, get.dfile) + public.write_log_gettext('File manager', 'Successfully copied file [{}] to [{}]!', + (get.sfile, get.dfile)) + stat = os.stat(get.sfile) + os.chmod(get.dfile,stat.st_mode) + os.chown(get.dfile, stat.st_uid, stat.st_gid) + return public.return_msg_gettext(True, 'Successfully copied file!') + except: + return public.return_msg_gettext(False, 'Failed to copy file!') + + # 复制文件夹 + def CopyDir(self, get): + if sys.version_info[0] == 2: + get.sfile = get.sfile.encode('utf-8') + get.dfile = get.dfile.encode('utf-8') + if get.dfile[-1] == '.': + return public.return_msg_gettext(False, 'It is not recommended to use [.] at the end of the directory, because there may be safety risks') + if not os.path.exists(get.sfile): + return public.return_msg_gettext(False, 'Requested directory does not exist') + + # if os.path.exists(get.dfile): + # return public.return_msg_gettext(False,'Requested directory exists!') + + # if not self.CheckDir(get.dfile): + # return public.return_msg_gettext(False,'Never trouble troubles till troubles trouble you!') + + try: + self.copytree(get.sfile, get.dfile) + stat = os.stat(get.sfile) + os.chmod(get.dfile,stat.st_mode) + os.chown(get.dfile, stat.st_uid, stat.st_gid) + public.write_log_gettext('File manager', 'Successfully copied directory!', + (get.sfile, get.dfile)) + return public.return_msg_gettext(True, 'Successfully copied directory!') + except: + return public.return_msg_gettext(False, 'Failed to copy directory!') + + # 移动文件或目录 + def MvFile(self, get): + if sys.version_info[0] == 2: + get.sfile = get.sfile.encode('utf-8') + get.dfile = get.dfile.encode('utf-8') + if get.dfile[-1] == '.': + return public.return_msg_gettext(False, 'It is not recommended to use [.] at the end of the file because there may be security risks') + if not self.CheckFileName(get.dfile): + return public.return_msg_gettext(False,'File names can NOT contain special characters!') + if os.path.basename(get.sfile) == '.Recycle_bin': + return public.return_msg_gettext(False,'Recovery failed!') + if not os.path.exists(get.sfile): + return public.return_msg_gettext(False, 'Configuration file not exist') + + if hasattr(get, 'rename'): + if os.path.exists(get.dfile): + return public.return_msg_gettext(False,'The target file name already exists!') + + if get.dfile[-1] == '/': + get.dfile = get.dfile[:-1] + + if get.dfile == get.sfile: + return public.return_msg_gettext(False,'Meaningless operation') + + if not self.CheckDir(get.sfile): + return public.return_msg_gettext(False,'Never trouble troubles till troubles trouble you!') + try: + self.move(get.sfile,get.dfile) + self.site_path_safe(get) + if hasattr(get,'rename'): + public.write_log_gettext('File manager','[{}] renamed to [{}]',(get.sfile,get.dfile)) + return public.return_msg_gettext(True,'Successfully renamed!') + else: + public.write_log_gettext('File manager', 'File moved!', + (get.sfile, get.dfile)) + return public.return_msg_gettext(True, 'File moved!') + except: + return public.return_msg_gettext(False, 'Failed to move file!') + + # 检查文件是否存在 + def CheckExistsFiles(self, get): + if sys.version_info[0] == 2: + get.dfile = get.dfile.encode('utf-8') + data = [] + filesx = [] + if not hasattr(get, 'filename'): + if not 'selected' in session: + return [] + filesx = json.loads(session['selected']['data']) + else: + filesx.append(get.filename) + + for fn in filesx: + if fn == '.': + continue + filename = get.dfile + '/' + fn + if os.path.exists(filename): + tmp = {} + stat = os.stat(filename) + tmp['filename'] = fn + tmp['size'] = os.path.getsize(filename) + tmp['mtime'] = str(int(stat.st_mtime)) + data.append(tmp) + return data + + # 取文件扩展名 + def __get_ext(self, filename): + tmp = filename.split('.') + return tmp[-1] + + # 获取文件内容 + def GetFileBody(self, get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + + get.path = self.xssdecode(get.path) + + if get.path.find('/rewrite/null/') != -1: + webserver = public.get_webserver() + get.path = get.path.replace("/rewrite/null/", "/rewrite/{}/".format(webserver)) + if get.path.find('/vhost/null/') != -1: + webserver = public.get_webserver() + get.path = get.path.replace("/vhost/null/", "/vhost/{}/".format(webserver)) + + if not os.path.exists(get.path): + if get.path.find('rewrite') == -1: + return public.return_message(-1,0,'Configuration file not exist') + public.writeFile(get.path,'') + if self.__get_ext(get.path) in ['gz','zip','rar','exe','db','pdf','doc','xls','docx','xlsx','ppt','pptx','7z','bz2','png','gif','jpg','jpeg','bmp','icon','ico','pyc','class','so','pyd']: + return public.return_message(-1,0,'The file format does not support online editing!') + # if os.path.getsize(get.path) > 3145928: + # return public.return_msg_gettext(False,'Cannot edit files larger than 2MB online!') + if os.path.isdir(get.path): + return public.return_message(-1,0,'Writing verification file failed: {}') + + # 处理my.cnf为空的情况 + myconf_file = '/etc/my.cnf' + if get.path == myconf_file: + if os.path.getsize(myconf_file) < 10: + mycnf_file_bak = '/etc/my.cnf.bak' + if os.path.exists(mycnf_file_bak): + public.writeFile(myconf_file, public.readFile(mycnf_file_bak)) + + data = {} + data['status'] = True + data["only_read"] = False + data["size"] = os.path.getsize(get.path) + if data["size"] > 3145928: + try: + info_data=self.last_lines(get.path, 10000) + if info_data=="":return public.return_message(-1,0, u'The file encoding is not compatible, the file cannot be read correctly!') + data["data"]=info_data + data["only_read"]=True + except:return public.return_message(-1,0, u'The file encoding is not compatible, the file cannot be read correctly!') + else: + fp = open(get.path, 'rb') + if fp: + srcBody = fp.read() + fp.close() + try: + data['encoding'] = 'utf-8' + data['data'] = srcBody.decode(data['encoding']) + except: + try: + data['encoding'] = 'GBK' + data['data'] = srcBody.decode(data['encoding']) + except: + try: + data['encoding'] = 'BIG5' + data['data'] = srcBody.decode(data['encoding']) + except: + return public.return_message(-1,0, 'File encoding is not compatible and cannot be read correctly!') + else: + return public.return_message(-1,0,'Failed to open file, file may be occupied by other processes!') + if hasattr(get,'filename'): + get.path = get.filename + data['historys'] = self.get_history(get.path) + data['auto_save'] = self.get_auto_save(get.path) + data['st_mtime'] = str(int(os.stat(get.path).st_mtime)) + return_status = -1 + if data['status']: + return_status=0 + del data['status'] + return public.return_message(return_status,0,data) + + def last_lines(self,filename, lines=1): + block_size = 3145928 + block = '' + nl_count = 0 + start = 0 + fsock = open(filename, 'rU') + try: + fsock.seek(0, 2) + curpos = fsock.tell() + while (curpos > 0): + curpos -= (block_size + len(block)) + if curpos < 0: curpos = 0 + fsock.seek(curpos) + try: + block = fsock.read() + except: + continue + nl_count = block.count('\n') + if nl_count >= lines: break + for n in range(nl_count - lines + 1): + start = block.find('\n', start) + 1 + finally: + fsock.close() + return block[start:] + + + # 保存文件 + def SaveFileBody(self, get): + if not 'path' in get: + return public.return_message(-1,0,'[path] parameter cannot be empty!') + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + + if get.path.find('/rewrite/null/') != -1: + webserver = public.get_webserver() + get.path = get.path.replace("/rewrite/null/", "/rewrite/{}/".format(webserver)) + if get.path.find('/vhost/null/') != -1: + webserver = public.get_webserver() + get.path = get.path.replace("/vhost/null/", "/vhost/{}/".format(webserver)) + + if not os.path.exists(get.path): + if get.path.find('.htaccess') == -1: + return public.return_message(-1,0, 'Configuration file not exist') + elif os.path.getsize(get.path) > 3145928: + return public.return_message(-1,0, 'Files larger than 3MB cannot be edited online!') + nginx_conf_path = public.get_vhost_path() + '/nginx/' + if get.path.find(nginx_conf_path) != -1: + if get.data.find('#SSL-START') != -1 and get.data.find('#SSL-END') != -1: + if get.data.find('#error_page 404/404.html;') == -1: + str1 = public.get_msg_gettext('Failed to save the configuration file') + str2 = public.get_msg_gettext('Do not modify the 404 rule commented in the SSL config') + str3 = public.get_msg_gettext('To modify the 404 config, find the following config location') + + # return public.returnMsg(False,'Failed to save the configuration file:

                                    Do not modify the 404 rule commented in the SSL config

                                    To modify the 404 config, find the following config location:

                                    #ERROR-PAGE-START  Error page configuration, allowed to be commented
                                    ') + return public.return_message(-1,0,'{}:

                                    {}

                                    :

                                    #ERROR-PAGE-START  Error page configuration, allowed to be commented
                                    '.format(str1,str2,str3)) + + if 'st_mtime' in get: + st_mtime = str(int(os.stat(get.path).st_mtime)) + if st_mtime != get['st_mtime']: return public.return_message(-1,0,'Failed to save, {} file has been changed, please refresh the content and modify it again.'.format(get.path)) + + his_path = '/www/backup/file_history/' + if get.path.find(his_path) != -1: + return public.return_msg_gettext(False,'Cannot modify history copy directly!') + try: + if 'base64' in get: + import base64 + get.data = base64.b64decode(get.data) + isConf = -1 + if os.path.exists('/etc/init.d/nginx') or os.path.exists('/etc/init.d/httpd'): + isConf = get.path.find('nginx') + if isConf == -1: + isConf = get.path.find('apache') + if isConf == -1: + isConf = get.path.find('rewrite') + if isConf != -1: + public.ExecShell('\\cp -a '+get.path+' /tmp/backup.conf') + + data = get.data + if data == 'undefined': return public.return_message(-1,0,'Wrong file content, please save again!') + userini = False + if get.path.find('.user.ini') != -1: + userini = True + public.ExecShell('chattr -i ' + get.path) + + if get.path.find('/www/server/cron') != -1: + try: + import crontab + data = crontab.crontab().CheckScript(data) + except: + pass + + if get.encoding == 'ascii': + get.encoding = 'utf-8' + self.save_history(get.path) + try: + if sys.version_info[0] == 2: + data = data.encode(get.encoding, errors='ignore') + fp = open(get.path, 'w+') + else: + + data = data.encode(get.encoding , errors='ignore').decode(get.encoding) + fp = open(get.path, 'w+', encoding=get.encoding) + except: + fp = open(get.path, 'w+') + data = self.crlf_to_lf(data, get.path) + fp.write(data) + fp.close() + + if isConf != -1: + isError = public.checkWebConfig() + if isError != True: + public.ExecShell('\\cp -a /tmp/backup.conf '+get.path) + return public.return_message(-1,0, 'ERROR:
                                    '+isError.replace("\n", '
                                    ')+'
                                    ') + public.serviceReload() + + if userini: + public.ExecShell('chattr +i ' + get.path) + + public.write_log_gettext('File manager', 'Successfully saved file [{}]!', (get.path,)) + tmp_data = public.return_msg_gettext(True, 'Saved!') + data ={'msg': tmp_data['msg']} + data['historys'] = self.get_history(get.path) # 获取历史记录 + data['st_mtime'] = str(int(os.stat(get.path).st_mtime)) + return public.return_message(0,0,data) + except Exception as ex: + return public.return_message(-1,0, 'Save ERROR! {}' + str(ex)) + + def crlf_to_lf(self,data,filename): + ''' + @name 将CRLF转换为LF + @author hwliang + @param data 要转换的数据 + @param filename 文件名 + @return string + ''' + file_ext_name = os.path.splitext(filename)[-1] + if not file_ext_name: + if data.find('#!/bin/bash') == 0 or data.find('#!/bin/sh') == 0: + file_ext_name = '.sh' + elif data.find('#!/usr/bin/python') == 0 or data.find('import ') != -1: + file_ext_name = '.py' + elif data.find('#!/usr/bin/env node') == 0: + file_ext_name = '.js' + elif data.find('#!/usr/bin/env php') == 0 or data.find('= num: # 删除多余的副本 + if os.path.exists(rm_file): + os.remove(rm_file) + continue + # 写入新的副本 + if is_write: + public.writeFile( + save_path + '/' + str(int(time.time())), public.readFile(filename, 'rb'), 'wb') + except: + pass + + # 取历史副本 + def get_history(self, filename): + try: + save_path = ('/www/backup/file_history/' + + filename).replace('//', '/') + if not os.path.exists(save_path): + return [] + return sorted(os.listdir(save_path),reverse=True) + except: + return [] + + # 读取指定历史副本 + def read_history(self, args): + save_path = ('/www/backup/file_history/' + + args.filename).replace('//', '/') + args.path = save_path + '/' + args.history + return self.GetFileBody(args) + + # 恢复指定历史副本 + def re_history(self, args): + save_path = ('/www/backup/file_history/' + + args.filename).replace('//', '/') + args.path = save_path + '/' + args.history + if not os.path.exists(args.path): + return public.return_msg_gettext(False,'The specified historical copy does not exist!') + import shutil + shutil.copyfile(args.path, args.filename) + return self.GetFileBody(args) + + # 自动保存配置 + def auto_save_temp(self, args): + save_path = '/www/backup/file_auto_save/' + if not os.path.exists(save_path): + os.makedirs(save_path, 384) + filename = save_path + args.filename + if os.path.exists(filename): + f_md5 = public.FileMd5(filename) + s_md5 = public.md5(args.body) + if f_md5 == s_md5: + return public.return_msg_gettext(True,'Not Edit') + public.writeFile(filename,args.body) + return public.return_msg_gettext(True,'Automatically saved successfully!') + + # 取上一次自动保存的结果 + def get_auto_save_body(self, args): + save_path = '/www/backup/file_auto_save/' + args.path = save_path + args.filename + return self.GetFileBody(args) + + # 取自动保存结果 + def get_auto_save(self, filename): + try: + save_path = ('/www/backup/file_auto_save/' + + filename).replace('//', '/') + if not os.path.exists(save_path): + return None + return os.stat(save_path).st_mtime + except: + return None + + + def is_max_size(self,path,max_size,max_num=10000,total_size=0,total_num=0): + ''' + @name 是否超过最大大小 + @path 文件路径 + @max_size 最大大小 + @max_num 最大文件数量 + @return bool + ''' + if not os.path.exists(path) or not max_size: + return False,total_size,total_num + + # 是否为文件? + if os.path.isfile(path): + total_size = os.path.getsize(path) + total_num = 1 + if total_size > max_size: + return True,total_size,total_num + return False,total_size,total_num + + # 是否为目录? + for root, dirs, files in os.walk(path, topdown=True): + total_num += len(files) + total_num += len(dirs) + # 判断是否超过最大文件数量 + if total_num > max_num: + return True,total_size,total_num + + for f in files: + filename = os.path.normcase(root+os.path.sep+f) + if not os.path.exists(filename): continue + if os.path.islink(filename): continue + total_size += os.path.getsize(filename) + + # 判断是否超过最大大小 + if total_size > max_size: + return True,total_size,total_num + + return False,total_size,total_num + + + # 文件压缩 + def Zip(self, get): + if not 'z_type' in get: + get.z_type = 'rar' + + if get.z_type == 'rar': + if os.uname().machine != 'x86_64': + return public.return_msg_gettext(False,'RAR component does not support aarch 64 platform') + + import panelTask + task_obj = panelTask.bt_task() + max_size = 1024*1024*100 + max_num = 10000 + total_size = 0 + total_num = 0 + status = True + if not os.path.exists(os.path.dirname(get.dfile)): + os.makedirs(os.path.dirname(get.dfile)) + for file_name in get.sfile.split(','): + path = os.path.join(get.path,file_name) + status,total_size,total_num = self.is_max_size(path,max_size,max_num,total_size,total_num) + if not status: break + + # 如果被压缩目标小于100MB或文件数量少于1W个,则直接在主线程压缩 + if not status: + return task_obj._zip(get.path,get.sfile,get.dfile,'/tmp/zip.log',get.z_type) + + # 否则在后台线程压缩 + task_obj.create_task('压缩文件', 3, get.path, json.dumps( + {"sfile": get.sfile, "dfile": get.dfile, "z_type": get.z_type})) + public.WriteLog("TYPE_FILE", 'ZIP_SUCCESS', (get.sfile, get.dfile)) + return public.returnMsg(True, '已将压缩任务添加到消息队列!') + + # 文件解压 + def UnZip(self, get): + if get.sfile[-4:] == '.rar': + if os.uname().machine != 'x86_64': + return public.return_msg_gettext(False,'RAR component does not support aarch 64 platform') + import panelTask + if not 'password' in get: + get.password = '' + if not os.path.exists(get.sfile): + return public.returnMsg(False, 'The specified archive does not exist!') + if not os.path.exists(get.dfile): + os.makedirs(get.dfile) + zip_size = os.path.getsize(get.sfile) + task_obj = panelTask.bt_task() + if zip_size < 1024 * 1024 * 50: + return task_obj._unzip(get.sfile, get.dfile, get.password,"/tmp/unzip.log") + + task_obj.create_task(public.get_msg_gettext('Decompress the file'), 2, get.sfile, + json.dumps({"dfile": get.dfile, "password": get.password})) + public.write_log_gettext("File manager", 'Successfully uncompressed file from [{}] to [{}]!',(get.sfile,get.dfile)) + return public.return_msg_gettext(True,'Decompression task added to the message queue!') + + # 获取文件/目录 权限信息 + def GetFileAccess(self, get): + if sys.version_info[0] == 2: + get.filename = get.filename.encode('utf-8') + data = {} + try: + import pwd + stat = os.stat(get.filename) + data['chmod'] = str(oct(stat.st_mode)[-3:]) + data['chown'] = pwd.getpwuid(stat.st_uid).pw_name + except: + data['chmod'] = 644 + data['chown'] = 'www' + return data + + # 设置文件权限和所有者 + def SetFileAccess(self, get, all='-R'): + if sys.version_info[0] == 2: + get.filename = get.filename.encode('utf-8') + if 'all' in get: + if get.all == 'False': + all = '' + try: + if not self.CheckDir(get.filename): + return public.return_msg_gettext(False, 'Never trouble troubles till troubles trouble you!') + if not os.path.exists(get.filename): + return public.return_msg_gettext(False, 'Configuration file not exist') + public.ExecShell('chmod '+all+' '+get.access+" '"+get.filename+"'") + public.ExecShell('chown '+all+' '+get.user+':' + + get.user+" '"+get.filename+"'") + public.write_log_gettext('File manager', "Set [{}]'s permission to [{}] and authorized user to [{}]", + (get.filename, get.access, get.user)) + return public.return_msg_gettext(True, 'Setup successfully!') + except: + return public.return_msg_gettext(False, 'Failed to set') + + def SetFileAccept(self, filename): + public.ExecShell('chown -R www:www ' + filename) + if os.path.isfile(filename): + public.ExecShell('chmod -R 644 ' + filename) + else: + public.ExecShell('chmod -R 755 ' + filename) + + # 取目录大小 + + def GetDirSize(self, get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + return public.to_size(public.get_path_size(get.path)) + + # 取目录大小2 + def get_path_size(self, get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + data = {} + data['path'] = get.path + data['size'] = public.get_path_size(get.path) + return data + + def CloseLogs(self, get): + get.path = public.GetConfigValue('root_path') + public.ExecShell('rm -f '+public.GetConfigValue('logs_path')+'/*') + public.ExecShell('rm -rf '+public.GetConfigValue('logs_path')+'/history_backups/*') + public.ExecShell('rm -f '+public.GetConfigValue('logs_path')+'/pm2/*.log') + if public.get_webserver() == 'nginx': + public.ExecShell( + 'kill -USR1 `cat '+public.GetConfigValue('setup_path')+'/nginx/logs/nginx.pid`') + else: + public.ExecShell('/etc/init.d/httpd reload') + + public.write_log_gettext('File manager', 'Site Logs emptied!') + get.path = public.GetConfigValue('logs_path') + return self.GetDirSize(get) + + # 批量操作 + def SetBatchData(self, get): + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + if get.type == '1' or get.type == '2': + session['selected'] = get + return public.return_msg_gettext(True, 'Successfully marked, please click Paste All button in the target directory!') + elif get.type == '3': + for key in json.loads(get.data): + try: + if sys.version_info[0] == 2: + key = key.encode('utf-8') + filename = get.path+'/'+key + if not self.CheckDir(filename): + return public.return_msg_gettext(False, 'Never trouble troubles till troubles trouble you!') + ret = ' -R ' + if 'all' in get: + if get.all == 'False': + ret = '' + public.ExecShell('chmod '+ret+get.access+" '"+filename+"'") + public.ExecShell('chown '+ret+get.user + + ':'+get.user+" '"+filename+"'") + except: + continue + public.write_log_gettext('File manager', 'Batch setting permission successful!') + return public.return_msg_gettext(True, 'Batch setting permission successful!') + else: + isRecyle = os.path.exists('data/recycle_bin.pl') and session.get('debug') != 1 + path = get.path + get.data = json.loads(get.data) + l = len(get.data) + i = 0 + args = public.dict_obj() + for key in get.data: + try: + if sys.version_info[0] == 2: + key = key.encode('utf-8') + filename = path + '/'+key + get.path = filename + if not os.path.exists(filename): + continue + i += 1 + public.writeSpeed(key, i, l) + if os.path.isdir(filename): + if not self.CheckDir(filename): + return public.return_msg_gettext(False, 'Never trouble troubles till troubles trouble you!') + public.ExecShell("chattr -R -i " + filename) + if isRecyle: + self.Mv_Recycle_bin(get) + else: + shutil.rmtree(filename) + else: + if key == '.user.ini': + if l > 1: + continue + public.ExecShell('chattr -i ' + filename) + if isRecyle: + + self.Mv_Recycle_bin(get) + else: + os.remove(filename) + args.path = filename + self.remove_file_ps(args) + except: + continue + public.writeSpeed(None, 0, 0) + self.site_path_safe(get) + if not isRecyle: + public.write_log_gettext('File manager', 'Batch deleting successful!') + return public.return_msg_gettext(True, 'Batch deleting successful!') + else: + public.write_log_gettext('File manager', '{} files or directories have been moved to the recycle bin in batches'.format(i)) + return public.return_msg_gettext(True, '{} files or directories have been moved to the recycle bin in batches'.format(i)) + + # 批量粘贴 + def BatchPaste(self, get): + import shutil + if sys.version_info[0] == 2: + get.path = get.path.encode('utf-8') + if not self.CheckDir(get.path): + return public.return_msg_gettext(False,'Never trouble troubles till troubles trouble you!') + if not 'selected' in session: + return public.return_msg_gettext(False,'TCOPY_PRESS_ERR') + i = 0 + if not 'selected' in session: + return public.return_msg_gettext(False,'The operation failed, please re-operate') + myfiles = json.loads(session['selected']['data']) + l = len(myfiles) + if get.type == '1': + + for key in myfiles: + if sys.version_info[0] == 2: + sfile = session['selected']['path'] + \ + '/' + key.encode('utf-8') + dfile = get.path + '/' + key.encode('utf-8') + else: + sfile = session['selected']['path'] + '/' + key + dfile = get.path + '/' + key + + if os.path.commonpath([dfile, sfile]) == sfile: + return public.return_msg_gettext(False,'Wrong copy logic, from {} copy to {} has an inclusive relationship, there is an infinite loop copy risk!'.format(sfile,dfile)) + + for key in myfiles: + i += 1 + public.writeSpeed(key, i, l) + try: + if sys.version_info[0] == 2: + sfile = session['selected']['path'] + \ + '/' + key.encode('utf-8') + dfile = get.path + '/' + key.encode('utf-8') + else: + sfile = session['selected']['path'] + '/' + key + dfile = get.path + '/' + key + + if os.path.isdir(sfile): + self.copytree(sfile, dfile) + else: + shutil.copyfile(sfile, dfile) + stat = os.stat(sfile) + os.chown(dfile, stat.st_uid, stat.st_gid) + except: + continue + public.write_log_gettext('File manager','Batch copied from [{}] to [{}]',(session['selected']['path'],get.path)) + else: + for key in myfiles: + try: + i += 1 + public.writeSpeed(key, i, l) + if sys.version_info[0] == 2: + sfile = session['selected']['path'] + '/' + key.encode('utf-8') + dfile = get.path + '/' + key.encode('utf-8') + else: + sfile = session['selected']['path'] + '/' + key + dfile = get.path + '/' + key + self.move(sfile, dfile) + except: + continue + self.site_path_safe(get) + public.write_log_gettext('File manager','Batch moved from [{}] to [{}]',(session['selected']['path'],get.path)) + public.writeSpeed(None,0,0); + errorCount = len(myfiles) - i + del(session['selected']) + return public.return_msg_gettext(True,'Batch operating succeeded [{}], failed [{}]',(str(i),str(errorCount))) + + # 移动和重命名 + def move(self, sfile, dfile): + sfile = sfile.replace('//', '/') + dfile = dfile.replace('//', '/') + if sfile == dfile: + return False + if not os.path.exists(sfile): + return False + is_dir = os.path.isdir(sfile) + if not os.path.exists(dfile) or not is_dir: + if os.path.exists(dfile): + os.remove(dfile) + shutil.move(sfile, dfile) + else: + self.copytree(sfile, dfile) + if os.path.exists(sfile) and os.path.exists(dfile): + if is_dir: + shutil.rmtree(sfile) + else: + os.remove(sfile) + return True + + # 复制目录 + def copytree(self, sfile, dfile): + if sfile == dfile: + return False + if not os.path.exists(dfile): + os.makedirs(dfile) + for f_name in os.listdir(sfile): + if not f_name.strip(): continue + if f_name.find('./') != -1: continue + src_filename = (sfile + '/' + f_name).replace('//', '/') + dst_filename = (dfile + '/' + f_name).replace('//', '/') + mode_info = public.get_mode_and_user(src_filename) + if os.path.isdir(src_filename): + if not os.path.exists(dst_filename): + os.makedirs(dst_filename) + public.set_mode(dst_filename, mode_info['mode']) + public.set_own(dst_filename, mode_info['user']) + self.copytree(src_filename, dst_filename) + else: + try: + shutil.copy2(src_filename, dst_filename) + public.set_mode(dst_filename, mode_info['mode']) + public.set_own(dst_filename, mode_info['user']) + except: + pass + return True + + # 下载文件 + + def DownloadFile(self, get): + import panelTask + task_obj = panelTask.bt_task() + task_obj.create_task(public.get_msg_gettext('Download file'),1,get.url,get.path + '/' + get.filename) + #if sys.version_info[0] == 2: get.path = get.path.encode('utf-8'); + #import db,time + #isTask = '/tmp/panelTask.pl' + #execstr = get.url +'|bt|'+get.path+'/'+get.filename + #sql = db.Sql() + #sql.table('tasks').add('name,type,status,addtime,execstr',('下载文件['+get.filename+']','download','0',time.strftime('%Y-%m-%d %H:%M:%S'),execstr)) + # public.writeFile(isTask,'True') + # self.SetFileAccept(get.path+'/'+get.filename) + public.write_log_gettext('File manager', 'Downloaded file [{}] to [{}]', (get.url, get.path)) + return public.return_msg_gettext(True, 'Download task added into the queue!') + + # 添加安装任务 + def InstallSoft(self, get): + import db + import time + path = public.GetConfigValue('setup_path') + '/php' + if not os.path.exists(path): + public.ExecShell("mkdir -p " + path) + if session['server_os']['x'] != 'RHEL': + get.type = '3' + apacheVersion = 'false' + if public.get_webserver() == 'apache': + apacheVersion = public.readFile( + public.GetConfigValue('setup_path')+'/apache/version.pl') + public.writeFile('/var/bt_apacheVersion.pl', apacheVersion) + public.writeFile('/var/bt_setupPath.conf', + public.GetConfigValue('root_path')) + isTask = '/tmp/panelTask.pl' + execstr = "cd " + public.GetConfigValue('setup_path') + "/panel/install && /bin/bash install_soft.sh " + \ + get.type + " install " + get.name + " " + get.version + if public.get_webserver() == "openlitespeed": + execstr = "cd " + public.GetConfigValue('setup_path') + "/panel/install && /bin/bash install_soft.sh " + \ + get.type + " install " + get.name + "-ols " + get.version + sql = db.Sql() + if hasattr(get, 'id'): + id = get.id + else: + id = None + sql.table('tasks').add('id,name,type,status,addtime,execstr', (None, + 'Install ['+get.name+'-'+get.version+']', 'execshell', '0', time.strftime('%Y-%m-%d %H:%M:%S'), execstr)) + public.writeFile(isTask, 'True') + public.write_log_gettext('Installer', 'Download task added to queue!', (get.name, get.version)) + time.sleep(0.1) + return public.return_msg_gettext(True, 'Download task added to queue!') + + # 删除任务队列 + def RemoveTask(self, get): + try: + name = public.M('tasks').where('id=?', (get.id,)).getField('name') + status = public.M('tasks').where( + 'id=?', (get.id,)).getField('status') + public.M('tasks').delete(get.id) + if status == '-1': + public.ExecShell( + "kill `ps -ef |grep 'python panelSafe.pyc'|grep -v grep|grep -v panelExec|awk '{print $2}'`") + public.ExecShell( + "kill `ps -ef |grep 'install_soft.sh'|grep -v grep|grep -v panelExec|awk '{print $2}'`") + public.ExecShell( + "kill `ps aux | grep 'python task.pyc$'|awk '{print $2}'`") + public.ExecShell(''' +pids=`ps aux | grep 'sh'|grep -v grep|grep install|awk '{print $2}'` +arr=($pids) + +for p in ${arr[@]} +do + kill -9 $p +done + ''') + + public.ExecShell( + 'rm -f ' + name.replace('Scan dir [', '').replace(']', '') + '/scan.pl') + isTask = '/tmp/panelTask.pl' + public.writeFile(isTask, 'True') + public.ExecShell('/etc/init.d/bt start') + except: + public.ExecShell('/etc/init.d/bt start') + return public.return_msg_gettext(True, 'Task deleted') + + # 重新激活任务 + def ActionTask(self, get): + isTask = '/tmp/panelTask.pl' + public.writeFile(isTask, 'True') + return public.return_msg_gettext(True, 'Task queue activated') + + # 卸载软件 + def UninstallSoft(self, get): + public.writeFile('/var/bt_setupPath.conf', + public.GetConfigValue('root_path')) + get.type = '0' + if session['server_os']['x'] != 'RHEL': + get.type = '3' + if public.get_webserver() == "openlitespeed": + default_ext = ["bz2","calendar","sysvmsg","exif","imap","readline","sysvshm","xsl"] + if get.version == "73": + default_ext.append("opcache") + if not os.path.exists("/etc/redhat-release"): + default_ext.append("gmp") + default_ext.append("opcache") + if get.name.lower() in default_ext: + return public.return_msg_gettext(False, 'This extension is the default extension of OLS and cannot be uninstalled') + execstr = "cd " + public.GetConfigValue('setup_path') + "/panel/install && /bin/bash install_soft.sh " + \ + get.type+" uninstall " + get.name.lower() + " " + get.version.replace('.', '') + if public.get_webserver() == "openlitespeed": + execstr = "cd " + public.GetConfigValue('setup_path') + "/panel/install && /bin/bash install_soft.sh " + \ + get.type + " uninstall " + get.name.lower() + "-ols " + get.version.replace('.', '') + public.ExecShell(execstr) + public.write_log_gettext('Installer', 'Uninstallaton succeeded', + (get.name, get.version)) + return public.return_msg_gettext(True, 'Uninstallaton succeeded') + + # 取任务队列进度 + def GetTaskSpeed(self, get): + tempFile = '/tmp/panelExec.log' + #freshFile = '/tmp/panelFresh' + import db + find = db.Sql().table('tasks').where('status=? OR status=?',('-1','0')).field('id,type,name,execstr').find() + if(type(find) == str): + return public.return_msg_gettext(False,'Query error, {}',(find,)) + if not len(find): + return public.return_msg_gettext(False,'NO_TASK_AT_LINEUP',("-2",)) + isTask = '/tmp/panelTask.pl' + public.writeFile(isTask, 'True') + echoMsg = {} + echoMsg['name'] = find['name'] + echoMsg['execstr'] = find['execstr'] + if find['type'] == 'download': + try: + tmp = public.readFile(tempFile) + if len(tmp) < 10: + return public.return_msg_gettext(False,'NO_TASK_AT_LINEUP',("-3",)) + echoMsg['msg'] = json.loads(tmp) + echoMsg['isDownload'] = True + except: + db.Sql().table('tasks').where("id=?",(find['id'],)).save('status',('0',)) + return public.return_msg_gettext(False,'NO_TASK_AT_LINEUP',("-4",)) + else: + echoMsg['msg'] = self.GetLastLine(tempFile, 20) + echoMsg['isDownload'] = False + + echoMsg['task'] = public.M('tasks').where("status!=?", ('1',)).field( + 'id,status,name,type').order("id asc").select() + return echoMsg + + # 取执行日志 + def GetExecLog(self, get): + return self.GetLastLine('/tmp/panelExec.log', 100) + + # 读文件指定倒数行数 + def GetLastLine(self, inputfile, lineNum): + result = public.GetNumLines(inputfile, lineNum) + if len(result) < 1: + return public.get_msg_gettext('Loading...') + return result + + # 执行SHELL命令 + def ExecShell(self, get): + disabled = ['vi', 'vim', 'top', 'passwd', 'su'] + get.shell = get.shell.strip() + tmp = get.shell.split(' ') + if tmp[0] in disabled: + return public.return_msg_gettext(False, 'Sorry, [{}] command is NOT supported!', (tmp[0],)) + shellStr = '''#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH +cd %s +%s +''' % (get.path, get.shell) + public.writeFile('/tmp/panelShell.sh', shellStr) + public.ExecShell( + 'nohup bash /tmp/panelShell.sh > /tmp/panelShell.pl 2>&1 &') + return public.return_msg_gettext(True, 'Command sent') + + # 取SHELL执行结果 + def GetExecShellMsg(self, get): + fileName = '/tmp/panelShell.pl' + if not os.path.exists(fileName): + return 'FILE_SHELL_EMPTY' + status = not public.process_exists('bash', None, '/tmp/panelShell.sh') + return public.return_msg_gettext(status, public.GetNumLines(fileName, 200)) + + # 文件搜索 + def GetSearch(self, get): + if not os.path.exists(get.path): + return public.return_msg_gettext(False, 'Requested directory does not exist') + return public.ExecShell("find "+get.path+" -name '*"+get.search+"*'") + + # 保存草稿 + def SaveTmpFile(self, get): + save_path = public.get_panel_path() + '/temp' + if not os.path.exists(save_path): + os.makedirs(save_path) + get.path = os.path.join(save_path,public.Md5(get.path) + '.tmp') + public.writeFile(get.path,get.body) + return public.return_msg_gettext(True,'Saved') + + # 获取草稿 + def GetTmpFile(self, get): + self.CleanOldTmpFile() + save_path = public.get_panel_path() + '/temp' + if not os.path.exists(save_path): + os.makedirs(save_path) + src_path = get.path + get.path = os.path.join(save_path,public.Md5(get.path) + '.tmp') + if not os.path.exists(get.path): + return public.return_msg_gettext(False,'No drafts available!') + data = self.GetFileInfo(get.path) + data['file'] = src_path + if 'rebody' in get: + data['body'] = public.readFile(get.path) + return data + + # 清除过期草稿 + def CleanOldTmpFile(self): + if 'clean_tmp_file' in session: + return True + save_path = public.get_panel_path() + '/temp' + max_time = 86400 * 30 + now_time = time.time() + for tmpFile in os.listdir(save_path): + filename = os.path.join(save_path, tmpFile) + fileInfo = self.GetFileInfo(filename) + if now_time - fileInfo['modify_time'] > max_time: + os.remove(filename) + session['clean_tmp_file'] = True + return True + + # 取指定文件信息 + def GetFileInfo(self, path): + if not os.path.exists(path): + return False + stat = os.stat(path) + fileInfo = {} + fileInfo['modify_time'] = int(stat.st_mtime) + fileInfo['size'] = os.path.getsize(path) + return fileInfo + + # 安装rar组件 + def install_rar(self, get): + unrar_file = public.get_setup_path() + '/rar/unrar' + rar_file = public.get_setup_path() + '/rar/rar' + bin_unrar = '/usr/local/bin/unrar' + bin_rar = '/usr/local/bin/rar' + if os.path.exists(unrar_file) and os.path.exists(bin_unrar): + try: + import rarfile + except: + public.ExecShell("pip install rarfile") + return True + + import platform + os_bit = '' + if platform.machine() == 'x86_64': + os_bit = '-x64' + download_url = public.get_url() + '/src/rarlinux'+os_bit+'-5.6.1.tar.gz' + + tmp_file = '/tmp/bt_rar.tar.gz' + public.ExecShell('wget -O ' + tmp_file + ' ' + download_url) + if os.path.exists(unrar_file): + public.ExecShell("rm -rf {}".format(rar_file)) + public.ExecShell("tar xvf " + tmp_file + ' -C {}'.format(public.get_setup_path())) + if os.path.exists(tmp_file): + os.remove(tmp_file) + if not os.path.exists(unrar_file): + return False + + if os.path.exists(bin_unrar): + os.remove(bin_unrar) + if os.path.exists(bin_rar): + os.remove(bin_rar) + + public.ExecShell('ln -sf ' + unrar_file + ' ' + bin_unrar) + public.ExecShell('ln -sf ' + rar_file + ' ' + bin_rar) + public.ExecShell("pip install rarfile") + # public.writeFile('data/restart.pl','True') + return True + + def get_store_data(self): + data = [] + path = 'data/file_store.json' + try: + if os.path.exists(path): + data = json.loads(public.readFile(path)) + except: + data = [] + if type(data) == dict: + result = [] + for key in data: + for path in data[key]: + result.append(path) + self.set_store_data(result) + return result + return data + + def set_store_data(self, data): + public.writeFile('data/file_store.json', json.dumps(data)) + return True + + # 获取收藏夹 + def get_files_store(self, get): + data = self.get_store_data() + result = [] + for path in data: + if type(path) == dict: + path = path['path'] + info = {'path': path, 'name': os.path.basename(path)} + if os.path.isdir(path): + info['type'] = 'dir' + else: + info['type'] = 'file' + result.append(info) + return result + + # 添加收藏夹 + def add_files_store(self, get): + path = get.path + if not os.path.exists(path): + return public.return_msg_gettext(False,'File or directory does not exist!') + data = self.get_store_data() + if path in data: + return public.return_msg_gettext(False,'Do not add it repeatedly!') + data.append(path) + self.set_store_data(data) + return public.return_msg_gettext(True,'Successfully added') + + #删除收藏夹 + def del_files_store(self,get): + path = get.path + data = self.get_store_data() + if not path in data: + is_go = False + for info in data: + if type(info) == dict: + if info['path'] == path: + path = info + is_go = True + break + if not is_go: + return public.return_msg_gettext(False,'This favorite object could not be found!') + data.remove(path) + if len(data) <= 0: + data = [] + self.set_store_data(data) + return public.return_msg_gettext(True,'Successfully deleted') + + # #单文件木马扫描 + # def file_webshell_check(self,get): + # if not 'filename' in get: return public.returnMsg(True, 'file does not exist!') + # import webshell_check + # if webshell_check.webshell_check().upload_file_url(get.filename.strip()): + # return public.returnMsg(False,'This file is webshell [ %s ]'%get.filename.strip().split('/')[-1]) + # else: + # return public.returnMsg(True, 'no risk') + # + # #目录扫描木马 + # def dir_webshell_check(self,get): + # if not 'path' in get: return public.returnMsg(False, 'Please enter a valid directory!') + # path=get.path.strip() + # if os.path.exists(path): + # #启动消息队列 + # exec_shell = public.get_python_bin() + ' /www/server/panel/class/webshell_check.py dir %s mail'%path + # task_name = "Scan Trojan files for directory %s"%path + # import panelTask + # task_obj = panelTask.bt_task() + # task_obj.create_task(task_name, 0, exec_shell) + # return public.returnMsg(True, 'Starting Trojan killing process. Details will be in the panel security log') + + # 获取下载地址列表 + def get_download_url_list(self, get): + my_table = 'download_token' + count = public.M(my_table).count() + + if not 'p' in get: + get.p = 1 + if not 'collback' in get: + get.collback = '' + data = public.get_page(count, int(get.p), 12, get.collback) + data['data'] = public.M(my_table).order('id desc').field( + 'id,filename,token,expire,ps,total,password,addtime').limit(data['shift'] + ',' + data['row']).select() + return data + + + #获取短列表 + def get_download_list(self): + if self.download_list != None: return self.download_list + my_table = 'download_token' + self.download_list = public.M(my_table).field('id,filename,expire').select() + if self.download_token_list == None: self.download_token_list = {} + m_time = time.time() + for d in self.download_list: + #清理过期和无效 + if self.download_is_rm: continue + if not os.path.exists(d['filename']) or m_time > d['expire']: + public.M(my_table).where('id=?',(d['id'],)).delete() + continue + self.download_token_list[d['filename']] = d['id'] + + #标记清理 + if not self.download_is_rm: + self.download_is_rm = True + + #获取id + def get_download_id(self,filename): + self.get_download_list() + return str(self.download_token_list.get(filename,'0')) + + # 获取指定下载地址 + def get_download_url_find(self, get): + if not 'id' in get: return public.return_msg_gettext(False, 'Wrong parameter') + id = int(get.id) + my_table = 'download_token' + data = public.M(my_table).where('id=?', (id,)).find() + if not data: return public.return_msg_gettext(False, 'The specified address does not exist!') + return data + + # 删除下载地址 + def remove_download_url(self, get): + if not 'id' in get: return public.return_msg_gettext(False, 'Wrong parameter') + id = int(get.id) + my_table = 'download_token' + public.M(my_table).where('id=?', (id,)).delete() + return public.return_msg_gettext(True, 'Successfully deleted!') + + # 修改下载地址 + def modify_download_url(self, get): + if not 'id' in get: return public.return_msg_gettext(False, 'Wrong parameter') + id = int(get.id) + my_table = 'download_token' + if not public.M(my_table).where('id=?', (id,)).count(): + return public.return_msg_gettext(False, 'The specified address does not exist!') + pdata = {} + if 'expire' in get: pdata['expire'] = get.expire + if 'password' in get: + pdata['password'] = get.password + if len(pdata['password']) < 4 and len(pdata['password']) > 0: + return public.return_msg_gettext(False,'The length of the extracted password cannot be less than 4 digits') + if not re.match(r'^\w+$',pdata['password']): + return public.return_msg_gettext(False,'The password only supports a combination of uppercase and lowercase letters and numbers') + + if 'ps' in get: pdata['ps'] = get.ps + public.M(my_table).where('id=?', (id,)).update(pdata) + return public.return_msg_gettext(True, 'Successfully modified') + + # 生成下载地址 + def create_download_url(self, get): + if not os.path.exists(get.filename): + return public.return_msg_gettext(False,'File or directory does not exist!') + my_table = 'download_token' + mtime = int(time.time()) + pdata = { + "filename": get.filename, #文件名 + "token": public.GetRandomString(12), #12位随机密钥,用于URL + "expire": mtime + (int(get.expire) * 3600), #过期时间 + "ps":get.ps, #备注 + "total":0, #下载计数 + "password":str(get.password), #提取密码 + "addtime": mtime #添加时间 + } + exts = os.path.basename(get.filename).split('.') + if len(exts) > 1: + pdata['token'] += "." + exts[-1] + if len(pdata['password']) < 4 and len(pdata['password']) > 0: + return public.return_msg_gettext(False,' Please do not enter the following special characters [ ~ ` / = ]') + if not re.match(r'^\w+$',pdata['password']) and pdata['password']: + return public.return_msg_gettext(False,'The password only supports a combination of uppercase and lowercase letters and numbers') + #更新 or 插入 + token = public.M(my_table).where('filename=?',(get.filename,)).getField('token') + if token: + return public.return_msg_gettext(False, 'Already shared!') + # pdata['token'] = token + # del(pdata['total']) + # public.M(my_table).where('token=?',(token,)).update(pdata) + else: + id = public.M(my_table).insert(pdata) + pdata['id'] = id + + return public.return_msg_gettext(True, pdata) + + + #取PHP-CLI执行命令 + def __get_php_bin(self,php_version=None): + php_vs = public.get_php_versions(True) + if php_version: + if php_version != 'auto': + if not php_version in php_vs: return '' + else: + php_version = None + + #判段兼容的PHP版本是否安装 + php_path = "/www/server/php/" + php_v = None + for pv in php_vs: + if php_version: + if php_version != pv: continue + php_bin = php_path + pv + "/bin/php" + if os.path.exists(php_bin): + php_v = pv + break + # 如果没安装直接返回False + if not php_v: return '' + #处理PHP-CLI-INI配置文件 + php_ini = '/www/server/panel/tmp/composer_php_cli_'+php_v+'.ini' + if not os.path.exists(php_ini): + # 如果不存在,则从PHP安装目录下复制一份 + src_php_ini = php_path + php_v + '/etc/php.ini' + import shutil + shutil.copy(src_php_ini, php_ini) + # 解除所有禁用函数 + php_ini_body = public.readFile(php_ini) + php_ini_body = re.sub(r"disable_functions\s*=.*", "disable_functions = ", php_ini_body) + public.writeFile(php_ini, php_ini_body) + return php_path + php_v + '/bin/php -c ' + php_ini + + # 执行git + def exec_git(self,get): + if get.git_action == 'option': + public.ExecShell("nohup {} &> /tmp/panelExec.pl &".format(get.giturl)) + else: + public.ExecShell("nohup git clone {} &> /tmp/panelExec.pl &".format(get.giturl)) + return public.return_msg_gettext(True,'Command has been sent!') + + # 安装composer + def get_composer_bin(self): + composer_bin = '/usr/bin/composer' + download_addr = 'wget -O {} {}/install/src/composer.phper -T 5'.format(composer_bin,public.get_url()) + if not os.path.exists(composer_bin): + public.ExecShell(download_addr) + elif os.path.getsize(composer_bin) < 100: + public.ExecShell(download_addr) + + public.ExecShell('chmod +x {}'.format(composer_bin)) + if not os.path.exists(composer_bin): + return False + return composer_bin + + # 执行composer + def exec_composer(self,get): + # 校验参数 + try: + get.validate([ + Param('php_version').String(), + Param('composer_args').String(), + Param('composer_cmd').String(), + Param('repo').String(), + Param('path').String(), + Param('user').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + #准备执行环境 + composer_bin = self.get_composer_bin() + if not composer_bin: + return public.return_message(-1,0,'No composer available!') + + #取执行PHP版本 + php_version = None + if 'php_version' in get: + php_version = get.php_version + php_bin = self.__get_php_bin(php_version) + if not php_bin: + return public.return_message(-1,0,'No available PHP version was found, or the specified PHP version was not installed!') + get.composer_cmd = get.composer_cmd.strip() + if get.composer_cmd == '': + if not os.path.exists(get.path + '/composer.json'): + return public.return_message(-1,0,'The composer.json configuration file was not found in the specified directory!') + log_file = '/tmp/composer.log' + user = '' + # del_cache = self._composer_user_home() + if 'user' in get: + user = 'sudo -u {} '.format(get.user) + if not os.path.exists('/usr/bin/sudo'): + if os.path.exists('/usr/bin/apt'): + public.ExecShell("apt install sudo -y > {}".format(log_file)) + else: + public.ExecShell("yum install sudo -y > {}".format(log_file)) + public.ExecShell("mkdir -p /home/www && chown -R www:www /home/www") + # del_cache = self._composer_user_home() + + #设置指定源 + if 'repo' in get: + if get.repo != 'repos.packagist': + public.ExecShell('export COMPOSER_HOME=/tmp && {}{} {} config -g repo.packagist composer {}'.format(user,php_bin,composer_bin,get.repo)) + else: + public.ExecShell('export COMPOSER_HOME=/tmp && {}{} {} config -g --unset repos.packagist'.format(user,php_bin,composer_bin)) + #执行composer命令 + if not get.composer_cmd: + composer_exec_str = '{} {} {} -vvv'.format(php_bin,composer_bin,get.composer_args) + else: + if get.composer_cmd.find('composer ') == 0 or get.composer_cmd.find('/usr/bin/composer ') == 0: + composer_cmd = get.composer_cmd.replace('composer ','').replace('/usr/bin/composer ','') + composer_exec_str = '{} {} {} -vvv'.format(php_bin,composer_bin,composer_cmd) + else: + composer_exec_str = '{} {} {} {} -vvv'.format(php_bin,composer_bin,get.composer_args,get.composer_cmd) + + if os.path.exists(log_file): os.remove(log_file) + public.ExecShell("cd {} && export COMPOSER_HOME=/tmp && {} nohup {} &> {} && echo 'BT-Exec-Completed' >> {} && rm -rf /home/www &".format(get.path,user,composer_exec_str,log_file,log_file)) + public.write_log_gettext('Composer',"Execute composer [{}] in the directory: [{}]",(get.path,get.composer_args)) + # del_cache() + return public.return_message(0,0,'Command has been sent!') + + # 取composer版本 + def get_composer_version(self,get): + composer_bin = self.get_composer_bin() + if not composer_bin: + return public.return_message(-1,0,'No composer available!') + + try: + bs = str(public.readFile(composer_bin,'rb')) + result = re.findall(r"const VERSION\s*=\s*.{0,2}'([\d\.]+)",bs)[0] + if not result: raise Exception('empty!') + except: + php_bin = self.__get_php_bin() + if not php_bin: return public.return_msg_gettext(False,'No available PHP version found!') + composer_exec_str = 'export COMPOSER_HOME=/tmp && ' + php_bin + ' ' + composer_bin +' --version 2>/dev/null|grep \'Composer version\'|awk \'{print $3}\'' + result = public.ExecShell(composer_exec_str)[0].strip() + data = public.return_message(0,0,result) + if 'path' in get: + import panel_site_v2 as panelSite + data['message']['php_versions'] = panelSite.panelSite().GetPHPVersion(get,False) + data['message']['comp_json'] = True + data['message']['comp_lock'] = False + if not os.path.exists(get.path + '/composer.json'): + data['message']['comp_json'] = public.get_msg_gettext('[Composer.json] configuration file is not found in the specified directory!') + if os.path.exists(get.path + '/composer.lock'): + data['message']['comp_lock'] = public.get_msg_gettext('[Composer.lock] file exists in the specified directory, please delete it before executing') + return data + + # 升级composer版本 + def update_composer(self,get): + # 校验参数 + try: + get.validate([ + Param('repo').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + composer_bin = self.get_composer_bin() + if not composer_bin: + return public.return_message(-1,0,'No composer available!') + php_bin = self.__get_php_bin() + if not php_bin: return public.return_message(-1,0,'No available PHP version found!') + #设置指定源 + # if 'repo' in get: + # if get.repo: + # public.ExecShell('{} {} config -g repo.packagist composer {}'.format(php_bin,composer_bin,get.repo)) + + version1 = self.get_composer_version(get)['message'] + composer_exec_str = 'export COMPOSER_HOME=/tmp && {} {} self-update -vvv'.format(php_bin,composer_bin) + public.ExecShell(composer_exec_str) + version2 = self.get_composer_version(get)['message'] + if version1 == version2: + msg = 'Currently the latest version, no upgrade required!' + else: + msg = 'Upgrade composer from {} to {}'.format(version1,version2) + public.write_log_gettext('Composer',msg) + return public.return_message(0,0,msg) + + # 计算文件HASH + def get_file_hash(self,args=None,filename=None): + if not filename: filename = args.filename + import hashlib + md5_obj = hashlib.md5() + sha1_obj = hashlib.sha1() + f = open(filename,'rb') + while True: + b = f.read(8096) + if not b : + break + md5_obj.update(b) + sha1_obj.update(b) + f.close() + return {'md5':md5_obj.hexdigest(),'sha1':sha1_obj.hexdigest()} + + + # 取历史副本 + def get_history_info(self, filename): + try: + save_path = ('/www/backup/file_history/' + + filename).replace('//', '/') + if not os.path.exists(save_path): + return [] + result = [] + for f in sorted(os.listdir(save_path)): + f_name = (save_path + '/' + f).replace('//', '/') + pdata = {} + pdata['md5'] = public.FileMd5(f_name) + f_stat = os.stat(f_name) + pdata['st_mtime'] = int(f) + pdata['st_size'] = f_stat.st_size + pdata['history_file'] = f_name + result.insert(0,pdata) + return sorted(result,key=lambda x:x['st_mtime'],reverse=True) + except: + return [] + + #获取文件扩展名 + def get_file_ext(self,filename): + ss_exts = ['tar.gz','tar.bz2','tar.bz'] + for s in ss_exts: + e_len = len(s) + f_len = len(filename) + if f_len < e_len: continue + if filename[-e_len:] == s: + return s + if filename.find('.') == -1: return '' + return filename.split('.')[-1] + + + # 取所属用户或组 + def get_mode_user(self,uid): + import pwd + try: + return pwd.getpwuid(uid).pw_name + except: + return uid + + # 取lsattr + def get_lsattr(self,filename): + if os.path.isfile(filename): + return public.ExecShell('lsattr {}'.format(filename))[0].split(' ')[0] + else: + s_name = os.path.basename(filename) + s_path = os.path.dirname(filename) + + try: + res = public.ExecShell('lsattr {}'.format(s_path))[0].strip() + for s in res.split('\n'): + if not s: continue + lsattr_info = s.split() + if not lsattr_info: continue + if filename == lsattr_info[1]: + return lsattr_info[0] + except: + raise public.PanelError(lsattr_info) + + return '--------------e----' + + + # 取指定文件属性 + def get_file_attribute(self,args): + filename = args.filename.strip() + if not os.path.exists(filename): + return public.return_msg_gettext(False,'File does not exist!') + attribute = {} + attribute['name'] = os.path.basename(filename) + attribute['path'] = os.path.dirname(filename) + f_stat = os.stat(filename) + attribute['st_atime'] = int(f_stat.st_atime) # 最后访问时间 + attribute['st_mtime'] = int(f_stat.st_mtime) # 最后修改时间 + attribute['st_ctime'] = int(f_stat.st_ctime) # 元数据修改时间/权限或数据者变更时间 + attribute['st_size'] = f_stat.st_size # 文件大小(bytes) + attribute['st_gid'] = f_stat.st_gid # 用户组id + attribute['st_uid'] = f_stat.st_uid # 用户id + attribute['st_nlink'] = f_stat.st_nlink # inode 的链接数 + attribute['st_ino'] = f_stat.st_ino # inode 的节点号 + attribute['st_mode'] = f_stat.st_mode # inode 保护模式 + attribute['st_dev'] = f_stat.st_dev # inode 驻留设备 + attribute['user'] = self.get_mode_user(f_stat.st_uid) # 所属用户 + attribute['group'] = self.get_mode_user(f_stat.st_gid) # 所属组 + attribute['mode'] = str(oct(f_stat.st_mode)[-3:]) # 文件权限号 + attribute['md5'] = public.get_msg_gettext('Do not count files or directories larger than 100MB') # 文件MD5 + attribute['sha1'] = public.get_msg_gettext('Do not count files or directories larger than 100MB') # 文件sha1 + attribute['lsattr'] = self.get_lsattr(filename) + attribute['is_dir'] = os.path.isdir(filename) # 是否为目录 + attribute['is_link'] = os.path.islink(filename) # 是否为链接文件 + if attribute['is_link']: + attribute['st_type'] = 'Link file' + elif attribute['is_dir']: + attribute['st_type'] = 'Dir' + else: + attribute['st_type'] = self.get_file_ext(filename) + attribute['history'] = [] + if f_stat.st_size < 104857600 and not attribute['is_dir']: + hash_info = self.get_file_hash(filename=filename) + attribute['md5'] = hash_info['md5'] + attribute['sha1'] = hash_info['sha1'] + attribute['history'] = self.get_history_info(filename) # 历史文件 + return attribute + + def files_search(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_search(args) + + + def files_replace(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_replace(args) + + def get_replace_logs(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_replace_logs(args) + + def get_path_images(self, path): + ''' + @name 获取目录的图片列表 + @param path 目录路径 + @return 图片列表 + ''' + image_list = [] + for fname in os.listdir(path): + if fname.split('.')[-1] in ['png', 'jpeg', 'gif', 'jpg', 'bmp', 'ico']: + image_list.append(fname) + return ','.join(image_list) + + def clear_thumbnail(self): + ''' + @name 清除过期的缩略图缓存 + @author hwliang + @return void + ''' + try: + from BTPanel import cache + except: + return + ikey = 'thumbnail_cache' + if cache.get(ikey): return + + cache_path = '{}/cache/thumbnail'.format(public.get_panel_path()) + if not os.path.exists(cache_path): return + expire_time = time.time() - (30 * 86400) # 30天前的文件 + for fname in os.listdir(cache_path): + filename =os.path.join(cache_path,fname) + if os.path.getctime(filename) < expire_time: + os.remove(filename) + + # 标记,每天清理一次 + cache.set(ikey,1,86400) + + + + + def get_images_resize(self,args): + ''' + @name 获取指定图片的缩略图 + @author hwliang<2022-03-02> + @param args{ + "path": "", 图片路径 + "files": xx.png,aaa.jpg, 文件名称(不包含目录路径),如果files=*,则返回该目录下的所有图片 + "width": 50, 宽 + "heigth:50, 高 + "return_type": "base64" // base64,file + } + @return base64编码的图片 or file + ''' + from PIL import Image + from base64 import b64encode + from io import BytesIO + if args.files == '*': + args.files = self.get_path_images(args.path) + + file_list = args.files.split(',') + + width = int(args.width) + height = int(args.height) + + cache_path = '{}/cache/thumbnail'.format(public.get_panel_path()) + if not os.path.exists(cache_path): os.makedirs(cache_path,384) + data = {} + _max_time = 3 # 最大处理时间 + _stime = time.time() + + # 清理过期的缩略图缓存 + self.clear_thumbnail() + + for fname in file_list: + try: + filename = os.path.join(args.path,fname) + f_size = os.path.getsize(filename) + cache_file = os.path.join(cache_path,public.md5("{}_{}_{}_{}".format(filename,width,height,f_size))) + if not os.path.exists(filename): + # 移除缓存文件 + if os.path.exists(cache_file): os.remove(cache_file) + continue + + # 有缩略图缓存的使用缓存 + if os.path.exists(cache_file): + data[fname] = public.readFile(cache_file) + continue + + # 超出最大处理时间直接跳过后续图片的处理,以免影响前端用户体验 + if time.time() - _stime > _max_time: + data[fname] = '' + continue + + im = Image.open(filename) + im.thumbnail((width,height)) + out = BytesIO() + im.save(out, im.format) + out.seek(0) + image_type = im.format.lower() + mimetype = 'image/{}'.format(image_type) + if args.return_type == 'base64': + b64_data = "data:{};base64,".format(mimetype) + b64encode(out.read()).decode('utf-8') + data[fname] = b64_data + out.close() + # 写缩略图缓存 + public.writeFile(cache_file,b64_data) + else: + from flask import send_file + return send_file(out, mimetype=mimetype, cache_timeout=0) + except: + data[fname] = '' + + return public.return_data(True,data) + + def set_rsync_data(self,data): + ''' + @name 写入rsync配置数据 + @author cjx + @param data 配置数据 + @return bool + ''' + public.writeFile('{}/data/file_rsync.json'.format(public.get_panel_path()),json.dumps(data)) + return True + + def get_rsync_data(self): + ''' + @name 获取文件同步配置 + @author cjx + @return dict + ''' + data = {} + path = '{}/data/file_rsync.json'.format(public.get_panel_path()) + try: + if os.path.exists(path): + data = json.loads(public.readFile(path)) + except : + data = {} + return data + + def add_files_rsync(self,get): + ''' + @name 添加数据同步标记 + @author cjx + ''' + path = get.path + s_type = get.s_type + + data = self.get_rsync_data() + if not path in data: data[path] = {} + + data[path][s_type] = 1 + + self.set_rsync_data(data) + return public.return_msg_gettext(True,'Added successfully!') + # 数据库对象 + def _get_sqlite_connect(self): + try: + if not self.sqlite_connection: + self.sqlite_connection = sqlite3.connect('data/file_permissions.db') + except Exception as ex: + return "error: " + str(ex) + + # 操作数据库 + def _operate_db(self,q_sql,permissions_tb=None): + try: + self._get_sqlite_connect() + c = self.sqlite_connection.cursor() + table = "index_tb" + if permissions_tb: + table = permissions_tb + sql_data = q_sql.replace("TB_NAME",table) + return c.execute(sql_data) + except: + self._create_index_tb() + self._operate_db(q_sql,permissions_tb) + + # 判断文件个数 + def _get_file_total(self,path,num,date): + n = 0 + for p in os.listdir(path): + full_path = path + "/" + p + if os.path.isfile(full_path): + if n == 0: + first_file = full_path + n+=1 + if n >= num: + self.path_permission_exclude_list.append(path) + f_p = public.get_mode_and_user(path) + data = {'path':first_file,'owner':f_p['user'],'mode':f_p['mode'],'type':'first_file','date':date} + self.path_permission_list.append(data) + return n + + # 创建权限表 + def _create_permissions_tb(self,tb_name): + self._get_sqlite_connect() + sql=""" +CREATE TABLE {}( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path CHAR , + owner CHAR, + mode CHAR, + date CHAR, + type CHAR +);""".format(tb_name) + self.sqlite_connection.execute(sql) + + def _create_index_tb(self): + self._get_sqlite_connect() + sql = """ +CREATE TABLE index_tb( + id INTEGER PRIMARY KEY AUTOINCREMENT, + permissions_tb CHAR , + date CHAR, + remark CHAR, + first_path CHAR +);""" + self.sqlite_connection.execute(sql) + + # 获取权限表名 + def _get_permissions_tb_name(self,get_all_tb=None): + sql = 'select permissions_tb from TB_NAME' + data = self._operate_db(sql).fetchall() + exist_tb = [i[0] for i in data] + if get_all_tb: + return exist_tb + tb_names = ['p_tb'+str(x) for x in range(100)] + tb_name = [] + if exist_tb: + for n_tb in tb_names: + if n_tb not in exist_tb: + tb_name.append(n_tb) + break + if not tb_name: + tb_name.append(tb_names[0]) + self._create_permissions_tb(tb_name[0]) + return tb_name[0] + + # 写入索引表 + def _write_index_tb(self,remark,date,tb_name,path): + ins_sql = "INSERT INTO TB_NAME (remark,date,permissions_tb,first_path) VALUES ('{}', '{}', '{}','{}')".format(remark,date, tb_name,path) + self._operate_db(ins_sql) + self.sqlite_connection.commit() + + # 写入权限表 + def _write_permisssions_tb(self,tb_name): + p_p_l = self.path_permission_list + n = 0 + for p_p in p_p_l: + ins_sql = "INSERT INTO TB_NAME (path,owner,mode,date,type) VALUES ('{}', '{}', '{}','{}','{}')".format(p_p['path'], p_p['owner'], p_p['mode'],p_p['date'],p_p['type']) + self._operate_db(ins_sql,permissions_tb=tb_name) + n += 1 + if n >= 1000: + self.sqlite_connection.commit() + n = 0 + self.sqlite_connection.commit() + + # 备份路径权限 + def _back_path_permissions (self,path,date): + for p in os.listdir(path): + full_p = path + "/" + p + if os.path.isdir(full_p): + # 如果文件夹下数量大于500添加文件夹到排除列表,权限只记录第一个文件的权限 + if self._get_file_total(full_p,500,date): + permission_type = "exclude_dir" + else: + permission_type = "dir" + f_p = public.get_mode_and_user(full_p) + self.path_permission_list.append({'path':full_p,'owner':f_p['user'],'mode':f_p['mode'],'type':permission_type,'date':date}) + self._back_path_permissions(full_p,date) + continue + if path in self.path_permission_exclude_list: + continue + f_p = public.get_mode_and_user(full_p) + data = {'path':full_p,'owner':f_p['user'], 'mode':f_p['mode'], 'type':'file','date':date} + self.path_permission_list.append(data) + + # 备份目录权限 + def back_dir_perm(self,path,back_sub_dir,date,remark,tb_name): + print("开始备份目录权限 {}".format(path)) + self._write_index_tb(remark,date,tb_name,path) + f_p = public.get_mode_and_user(path) + data = {'path':path,'owner':f_p["user"],'mode':f_p['mode'],'date':date,'type':'dir'} + self.path_permission_list.append(data) + if back_sub_dir == "0": + self._write_permisssions_tb(tb_name) + return True + self._back_path_permissions(path, date) + self._write_permisssions_tb(tb_name) + + # 备份单个文件权限 + def back_single_file_perm(self,path,date,remark,tb_name): + print("开始备份文件权限 {}".format(path)) + self._write_index_tb(remark, date, tb_name,path) + f_p = public.get_mode_and_user(path) + data = {'path': path, 'owner': f_p["user"], 'mode': f_p['mode'], 'date': date, 'type': 'file'} + self.path_permission_list.append(data) + self._write_permisssions_tb(tb_name) + + # 备份权限 + def back_path_permissions(self,get): + back_limit = 100 + if self._get_total_back() >= back_limit: + return public.return_msg_gettext(False,"The number of backup versions has exceeded {} ,Please go to the upper right corner [Backup Permissions] to clean up the old backup before operating".format(back_limit)) + if not os.path.exists(get.path): + return public.return_msg_gettext(False,"Path is incorrect {}".format(get.path)) + path = get.path + back_sub_dir = get.back_sub_dir + remark = get.remark + self.path_permission_list = list() + # self.file_permission_list = list() + self.path_permission_exclude_list = list() + date = int(time.time()) + tb_name = self._get_permissions_tb_name() + try: + if os.path.isdir(path): + self.back_dir_perm(path,back_sub_dir,date,remark,tb_name) + else: + self.back_single_file_perm(path,date,remark,tb_name) + except Exception as e: + return public.return_msg_gettext(False,"Backup error {} ".format(e)) + finally: + self.sqlite_connection.commit() + self.sqlite_connection.close() + self.sqlite_connection=None + return public.return_msg_gettext(True,"Backup succeeded!") + + # 获取所有需要还原文件和文件夹 + def _get_restore_file(self,path): + for p in os.listdir(path): + full_p = path + "/" + p + if os.path.isdir(full_p): + self.file_permission_list.append(full_p) + self._get_restore_file(full_p) + continue + self.file_permission_list.append(full_p) + + # 直接递归还原目录下的文件权限 + def _recursive_restore_file_perm(self,path,p_i): + file_permissions = self._operate_db( + "SELECT owner,mode from TB_NAME where pid='{}' and type='{}'".format(p_i[0], 'first_file'), + 'file').fetchall() + f_p = file_permissions[0] + for i in os.listdir(path): + i = "{}/{}".format(path, i) + if os.path.isfile(i): + public.set_mode(i, f_p[1]) + public.set_own(i, f_p[0]) + + # 还原子目录权限 + def _restore_subdir_perm(self,path,date): + path_info = self._operate_db("SELECT id,owner,mode,type from TB_NAME where path='{}' and date='{}'".format(path,date), + 'path').fetchall() + p_i = path_info[0] + public.set_mode(path, p_i[2]) + public.set_own(path, p_i[1]) + if p_i[3] == "exclude_dir": + self._recursive_restore_file_perm(path,p_i) + file_permissions = self._operate_db("SELECT path,owner,mode from TB_NAME where pid='{}' and date='{}'".format(p_i[0],date), + 'file').fetchall() + if file_permissions: + for f in file_permissions: + if f[0] == ".user.ini": + continue + file_path = "{}/{}".format(path, f[0]) + public.set_mode(file_path, f[2]) + public.set_own(file_path, f[1]) + + # 还原目录权限 + def _restore_dir_perm(self,path_full,restore_sub_dir,date): + tb_name = self._operate_db("select permissions_tb from TB_NAME where date='{}'".format(date)).fetchall() + main_dir_data = self._operate_db("select path,owner,mode from TB_NAME where path='{}'".format(path_full),permissions_tb=tb_name[0][0]).fetchall() + if main_dir_data: + public.set_mode(main_dir_data[0][0], main_dir_data[0][2]) + public.set_own(main_dir_data[0][0], main_dir_data[0][1]) + if restore_sub_dir == "0": + public.return_msg_gettext(True, "Permission restored successfully") + self._get_restore_file(path_full) + if tb_name: + data = self._operate_db("select path,owner,mode from TB_NAME",permissions_tb=tb_name[0][0]).fetchall() + for d in data: + if '.user.ini' in d[0]: + continue + if d[0] in self.file_permission_list: + public.set_mode(d[0], d[2]) + public.set_own(d[0], d[1]) + return public.return_msg_gettext(True, "Permission restored successfully") + + # 还原单个文件权限 + def restore_single_file_perm(self,path_full,date): + tb_name = self._operate_db("select permissions_tb from TB_NAME where date='{}'".format(date)).fetchall() + main_dir_data = self._operate_db("select path,owner,mode from TB_NAME where path='{}'".format(path_full),permissions_tb=tb_name[0][0]).fetchall() + if main_dir_data: + public.set_mode(main_dir_data[0][0], main_dir_data[0][2]) + public.set_own(main_dir_data[0][0], main_dir_data[0][1]) + return public.return_msg_gettext(True, "Permission restored successfully") + return public.return_msg_gettext(False, "The file does not have backup permissions") + + + # 还原权限 + def restore_path_permissions(self,get): + self.file_permission_list = list() + path_full = get.path + restore_sub_dir = get.restore_sub_dir + date = get.date + try: + if os.path.isdir(path_full): + result = self._restore_dir_perm(path_full,restore_sub_dir,date) + else: + result = self.restore_single_file_perm(path_full,date) + return result + finally: + self.sqlite_connection.close() + self.sqlite_connection = None + + + def get_path_premissions(self,get): + path_full = get.path + result = [] + exist_tbs = self._get_permissions_tb_name(get_all_tb=True) + for tb_name in exist_tbs: + data = self._operate_db("select path,owner,mode,date from TB_NAME where path='{}'".format(path_full), + permissions_tb=tb_name).fetchall() + if not data and os.path.isdir(path_full) and path_full[-1] != "/": + path_full += "/" + data = self._operate_db( + "select path,owner,mode,date from TB_NAME where path='{}'".format(path_full), + permissions_tb=tb_name).fetchall() + if data: + index_data = self._operate_db("select id,remark from index_tb where permissions_tb='{}'".format(tb_name)).fetchall() + d_l = [] + for i in data[0]: + d_l.append(i) + if index_data: + d_l.append(index_data[0][1]) + d_l.append(index_data[0][0]) + result.append(d_l) + return sorted(result,key=lambda x:x[3],reverse=True) + + def del_path_premissions(self,get): + p_tb = self._operate_db("select permissions_tb from index_tb where id='{}'".format(get.id)).fetchall() + # 删除引导行 + self._operate_db("delete from index_tb where id='{}'".format(get.id)).fetchall() + if p_tb: + self._operate_db("drop table '{}'".format(p_tb[0][0])) + self.sqlite_connection.commit() + self.sqlite_connection.close() + return public.return_msg_gettext(True, "Successfully deleted!") + + # 获取所有备份 + def get_all_back(self,get): + data = self._operate_db('select id,remark,date,first_path from index_tb').fetchall() + return sorted(data,key=lambda x: x[2],reverse=True) + + def _get_total_back(self): + data = self._operate_db('select id from index_tb').fetchall() + return len(data) + + # 一键恢复默认权限 + def fix_permissions(self,get): + if not hasattr(get,"uid"): + import pwd + get.uid = pwd.getpwnam('www').pw_uid + get.gid = pwd.getpwnam('www').pw_gid + path = get.path + if os.path.isfile(path): + os.chown(path, get.uid, get.gid) + os.chmod(path, 0o644) + return public.return_msg_gettext(True, "Permission repair succeeded") + os.chown(path, get.uid, get.gid) + os.chmod(path, 0o755) + for file in os.listdir(path): + try: + filename = os.path.join(path,file) + os.chown(filename, get.uid, get.gid) + if os.path.isdir(filename): + os.chmod(filename, 0o755) + get.path = filename + self.fix_permissions(get) + continue + os.chmod(filename,0o644) + except: + print(public.get_error_info()) + return public.return_msg_gettext(True,"Permission repair succeeded") + + def restore_website(self,args): + """ + @name 恢复站点文件 + @author zhwen + @parma file_name 备份得文件名 + @parma site_id 网站id + """ + import panel_restore_v2 as panel_restore + pr=panel_restore.panel_restore() + return pr.restore_website_backup(args) + + def get_progress(self,args): + """ + @name 获取进度日志 + @author zhwen + """ + import panel_restore_v2 as panel_restore + pr=panel_restore.panel_restore() + return pr.get_progress(args) diff --git a/class_v2/firewallModelV2/app/appBase.py b/class_v2/firewallModelV2/app/appBase.py new file mode 100644 index 00000000..3a991af8 --- /dev/null +++ b/class_v2/firewallModelV2/app/appBase.py @@ -0,0 +1,78 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - 底层基类 +# ------------------------------ + +import sys +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public + + +class Base(object): + + def __init__(self): + pass + + # 2024/3/22 下午 3:18 通用返回 + def _result(self, status: bool, msg: str) -> dict: + ''' + @name 通用返回 + @author wzz <2024/3/22 下午 3:19> + @param status: True/False + msg: 提示信息 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return {"status": status, "msg": msg} + + # 2024/3/22 下午 4:55 检查是否设置了net.ipv4.ip_forward = 1,没有则设置 + def check_ip_forward(self) -> dict: + ''' + @name 检查是否设置了net.ipv4.ip_forward = 1,没有则设置 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("sysctl net.ipv4.ip_forward") + if "net.ipv4.ip_forward = 1" not in stdout: + # 2024/3/22 下午 4:56 永久设置 + stdout, stderr = public.ExecShell("echo net.ipv4.ip_forward=1 >> /etc/sysctl.conf") + if stderr: + return self._result(False, "设置net.ipv4.ip_forward失败, err: {}".format(stderr)) + + stdout, stderr = public.ExecShell("sysctl -p") + if stderr: + return self._result(False, "设置net.ipv4.ip_forward失败, err: {}".format(stderr)) + return self._result(True, "设置net.ipv4.ip_forward成功") + return self._result(True, "net.ipv4.ip_forward已经设置") + + # 2024/3/18 上午 11:35 处理192.168.1.100-192.168.1.200这种ip范围 + # 返回192.168.1.100,192.168.1.101,192.168.1...,192.168.1.200列表 + def handle_ip_range(self, ip): + ''' + @name 处理192.168.1.100-192.168.1.200这种ip范围的ip列表 + @author wzz <2024/3/19 下午 4:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + ip_range = ip.split("-") + ip_start = ip_range[0] + ip_end = ip_range[1] + ip_start = ip_start.split(".") + ip_end = ip_end.split(".") + ip_start = [int(i) for i in ip_start] + ip_end = [int(i) for i in ip_end] + ip_list = [] + for i in range(ip_start[0], ip_end[0] + 1): + for j in range(ip_start[1], ip_end[1] + 1): + for k in range(ip_start[2], ip_end[2] + 1): + for l in range(ip_start[3], ip_end[3] + 1): + ip_list.append("{}.{}.{}.{}".format(i, j, k, l)) + return ip_list diff --git a/class_v2/firewallModelV2/app/firewalld.py b/class_v2/firewallModelV2/app/firewalld.py new file mode 100644 index 00000000..1e70c44a --- /dev/null +++ b/class_v2/firewallModelV2/app/firewalld.py @@ -0,0 +1,750 @@ +#!/www/server/panel/pyenv/bin/python3.7 +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - firewalld封装库 +# ------------------------------ + +import os +import sys + + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public +from firewallModelV2.app.appBase import Base + + +class Firewalld(Base): + def __init__(self): + super().__init__() + self.cmd_str = self._set_cmd_str() + + def _set_cmd_str(self) -> str: + return "firewall-cmd" + + # 2024/3/20 下午 12:00 获取防火墙状态 + def status(self) -> bool: + ''' + @name 获取防火墙状态 + @author wzz <2024/3/20 下午 12:01> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + stdout, stderr = public.ExecShell("systemctl is-active firewalld") + if "not running" in stdout: + return False + return True + except Exception as e: + return False + + # 2024/3/20 下午 12:00 获取防火墙版本号 + def version(self) -> str: + ''' + @name 获取防火墙版本号 + @author wzz <2024/3/20 下午 12:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("firewall-cmd --version") + if "FirewallD is not running" in stdout: + return "Firewalld 没有启动,请先启动再试" + if stderr: + return "获取firewalld版本失败, err: {}".format(stderr) + return stdout.strip() + + # 2024/3/20 下午 12:08 启动防火墙 + def start(self) -> dict: + ''' + @name 启动防火墙 + @author wzz <2024/3/20 下午 12:08> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("systemctl start firewalld") + if stderr: + return self._result(False, "启动防火墙失败, err: {}".format(stderr)) + return self._result(True, "启动防火墙成功") + + # 2024/3/20 下午 12:10 停止防火墙 + def stop(self) -> dict: + ''' + @name 停止防火墙 + @author wzz <2024/3/20 下午 12:10> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("systemctl stop firewalld") + if stderr: + return self._result(False, "停止防火墙失败, err: {}".format(stderr)) + return self._result(True, "停止防火墙成功") + + # 2024/3/20 下午 12:11 重启防火墙 + def restart(self) -> dict: + ''' + @name 重启防火墙 + @author wzz <2024/3/20 下午 12:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("systemctl restart firewalld") + if stderr: + return self._result(False, "重启防火墙失败, err: {}".format(stderr)) + return self._result(True, "重启防火墙成功") + + # 2024/3/20 下午 12:11 重载防火墙 + def reload(self) -> dict: + ''' + @name 重载防火墙 + @author wzz <2024/3/20 下午 12:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("firewall-cmd --reload") + if stderr: + return self._result(False, "重载防火墙失败, err: {}".format(stderr)) + return self._result(True, "重载防火墙成功") + + # 2024/3/20 下午 12:12 获取所有防火墙端口列表 + def list_port(self) -> list: + ''' + @name 获取所有防火墙端口列表 + @author wzz <2024/3/20 下午 12:12> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["ports"] + self.list_output_port() + + # 2024/3/20 下午 12:12 获取防火墙端口INPUT列表 + def list_input_port(self) -> list: + ''' + @name 获取防火墙端口列表 + @author wzz <2024/3/20 下午 12:12> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["ports"] + + # 2024/3/22 上午 11:28 获取所有OUTPUT的direct 端口规则 + def list_output_port(self) -> list: + ''' + @name 获取所有OUTPUT的direct 端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + list_direct_rules = self.parse_direct_xml()["ports"] + datas = [] + for rule in list_direct_rules: + if rule.get("Chain") == "OUTPUT": + datas.append(rule) + return datas + + # 2024/3/20 下午 12:21 获取防火墙的rule的ip规则列表 + def list_address(self) -> list: + ''' + @name 获取防火墙的rule的ip规则列表 + @author wzz <2024/3/20 下午 2:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["rules"] + self.list_output_address() + + # 2024/3/20 下午 12:21 获取防火墙的rule input的ip规则列表 + def list_input_address(self) -> list: + ''' + @name 获取防火墙的rule input的ip规则列表 + @author wzz <2024/3/20 下午 2:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.parse_public_zone()["rules"] + + # 2024/3/22 下午 4:07 获取所有OUTPUT的direct ip规则 + def list_output_address(self) -> list: + ''' + @name 获取所有OUTPUT的direct ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + list_direct_rules = self.parse_direct_xml()["rules"] + datas = [] + for rule in list_direct_rules: + if rule.get("Chain") == "OUTPUT": + datas.append(rule) + return datas + + # 2024/3/20 下午 5:34 添加或删除防火墙端口 + def input_port(self, info: dict, operation: str) -> dict: + ''' + @name 添加或删除防火墙端口 + @author wzz <2024/3/20 下午 5:34> + @param info:{"Port": args[2], "Protocol": args[3]} + operation: add/remove + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + # 2024/3/25 下午 6:00 处理tcp/udp双协议的端口 + if info['Protocol'].find("/") != -1: + stdout, stderr = public.ExecShell( + "{cmd_str} --zone=public --{operation}-port={port}/{prot} --permanent" + .format( + cmd_str=self.cmd_str, + operation=operation, + port=info['Port'], + prot="tcp" + ) + ) + if stderr: + return self._result(False, "设置端口失败, err: {}".format(stderr)) + stdout, stderr = public.ExecShell( + "{cmd_str} --zone=public --{operation}-port={port}/{prot} --permanent" + .format( + cmd_str=self.cmd_str, + operation=operation, + port=info['Port'], + prot="udp" + ) + ) + if stderr: + return self._result(False, "设置端口失败, err: {}".format(stderr)) + else: + # 2024/3/25 下午 6:00 处理单协议的端口 + stdout, stderr = public.ExecShell( + "{cmd_str} --zone=public --{operation}-port={port}/{prot} --permanent" + .format( + cmd_str=self.cmd_str, + operation=operation, + port=info['Port'], + prot=info['Protocol'] + ) + ) + if stderr: + return self._result(False, "设置端口失败, err: {}".format(stderr)) + return self._result(True, "设置入站端口成功") + + # 2024/3/20 下午 6:02 设置output的防火墙端口规则 + def output_port(self, info: dict, operation: str) -> dict: + ''' + @name 设置output的防火墙端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + if info['Strategy'] == "accept": + info['Strategy'] = "ACCEPT" + elif info['Strategy'] == "drop": + info['Strategy'] = "DROP" + elif info['Strategy'] == "reject": + info['Strategy'] = "REJECT" + else: + return self._result(False, "不支持的策略: {}".format(info['Strategy'])) + + info['Port'] = info['Port'].replace("-", ":") + + if "/" in info['Protocol']: + info['Protocol'] = info['Protocol'].split("/") + for pp in info['Protocol']: + if not pp in ["tcp", "udp"]: + return self._result(False, "设置出站端口失败, err: 协议不支持 {}".format(pp)) + + stdout, stderr = public.ExecShell( + "{cmd_str} --permanent --direct --{operation}-rule ipv4 filter OUTPUT {priority} -p {prot} --dport {port} -j {strategy}" + .format( + cmd_str=self.cmd_str, + operation=operation, + priority=info['Priority'], + prot=pp, + port=info['Port'], + strategy=info['Strategy'] + ) + ) + if stderr: + return self._result(False, "设置出站端口失败, err: {}".format(stderr)) + else: + stdout, stderr = public.ExecShell( + "{cmd_str} --permanent --direct --{operation}-rule ipv4 filter OUTPUT {priority} -p {prot} --dport {port} -j {strategy}" + .format( + cmd_str=self.cmd_str, + operation=operation, + priority=info['Priority'], + prot=info['Protocol'], + port=info['Port'], + strategy=info['Strategy'] + ) + ) + + if stderr: + return self._result(False, "设置出站端口失败, err: {}".format(stderr)) + + return self._result(True, "设置出站端口成功") + + def set_rich_rule(self, info: dict, operation: str) -> dict: + ''' + @name 添加或删除复杂规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + rule_str = "rule family={}".format(info['Family'].lower()) + if "Address" in info and info["Address"] != "all": + rule_str += " source address={}".format(info['Address']) + if info.get("Port"): + rule_str += " port port={}".format(info['Port']) + if info.get("Protocol"): + rule_str += " protocol={}".format(info['Protocol']) + rule_str += " {}".format(info['Strategy']) + + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-rich-rule='{}' --permanent" + .format(self.cmd_str, operation, rule_str)) + + if stderr: + return self._result(False, "设置规则:{} 失败, err: {}".format(operation, rule_str, stderr)) + return self._result(True, "设置规则成功".format(operation)) + + # 2024/3/22 上午 11:35 添加或删除复杂规则 + def rich_rules(self, info: dict, operation: str) -> dict: + ''' + @name 添加或删除复杂规则 + @author wzz <2024/3/22 上午 11:35> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的规则操作: {}".format(operation)) + + if "Protocol" in info and info["Protocol"] == "all": + info["Protocol"] = "tcp/udp" + + if "Protocol" in info and info['Protocol'].find("/") != -1: + result_list = [] + for protocol in info['Protocol'].split("/"): + info['Protocol'] = protocol + result_list.append(self.set_rich_rule(info, operation)) + + return {"status": True, "msg": result_list} + else: + return self.set_rich_rule(info, operation) + + # 2024/3/24 下午 10:43 设置output的防火墙ip规则 + def output_rich_rules(self, info: dict, operation: str) -> dict: + ''' + @name 设置output的防火墙ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + if info['Strategy'] == "accept": + info['Strategy'] = "ACCEPT" + elif info['Strategy'] == "drop": + info['Strategy'] = "DROP" + elif info['Strategy'] == "reject": + info['Strategy'] = "REJECT" + else: + return self._result(False, "不支持的策略: {}".format(info['Strategy'])) + + rich_rules = self.cmd_str + " --permanent --direct --{0}-rule ipv4 filter OUTPUT".format(operation) + if "Priority" in info: + rich_rules += " {}".format(info["Priority"]) + if "Address" in info: + rich_rules += " -d {}".format(info["Address"]) + if "Protocol" in info: + rich_rules += " -p {}".format(info["Protocol"]) + if "Port" in info: + info["Port"] = info["Port"].replace("-", ":") + rich_rules += " --dport {}".format(info["Port"]) + if "Strategy" in info: + rich_rules += " -j {}".format(info["Strategy"]) + + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + return self._result(False, "设置出站地址失败, err: {}".format(stderr)) + if "NOT_ENABLED" in stderr: + return self._result(False, "规则不存在") + return self._result(True, "设置出站地址成功") + + # 2024/3/22 下午 12:22 解析public区域的防火墙规则 + def parse_public_zone(self) -> dict: + ''' + @name 解析public区域的防火墙规则 + @author wzz <2024/3/22 下午 12:22> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"services": services, "ports": ports, "rules": rules} rules是ip规则 + ''' + try: + import xml.etree.ElementTree as ET + file_path = "/etc/firewalld/zones/public.xml" + if not os.path.exists(file_path): + return {"services": [], "ports": [], "rules": [], "forward_ports": []} + + services = [] + ports = [] + rules = [] + forward_ports = [] + + tree = ET.parse(file_path) + root = tree.getroot() + + for elem in root: + # 2024/3/22 下午 3:01 服务规则 + if elem.tag == "service": + services.append(elem.attrib['name']) + # 2024/3/22 下午 3:01 端口规则 + elif elem.tag == "port": + port = { + "Protocol": elem.attrib["protocol"], + "Port": elem.attrib["port"], + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "INPUT", + } + ports.append(port) + # 2024/3/22 下午 3:01 复杂的规则配置 + elif elem.tag == "rule": + rule = {"Family": elem.attrib["family"] if "family" in elem.attrib else "ipv4"} + for subelem in elem: + rule["Strategy"] = "accept" + if subelem.tag == "source": + if "address" in subelem.attrib: + rule["Address"] = subelem.attrib["address"] + else: + continue + rule["Address"] = "all" if rule["Address"] == "Anywhere" else rule["Address"] + elif subelem.tag == "port": + rule["port"] = {"protocol": subelem.attrib["protocol"], "port": subelem.attrib["port"]} + elif subelem.tag == "drop": + rule["Strategy"] = "drop" + elif subelem.tag == "accept": + rule["Strategy"] = "accept" + elif subelem.tag == "forward-port": + rule["forward-port"] = { + "protocol": subelem.attrib["protocol"], + "S_Port": subelem.attrib["port"], + "T_Address": subelem.attrib["to-addr"], + "T_Port": subelem.attrib["to-port"], + } + + # 2024/3/22 下午 3:02 如果端口在里面,就放到端口规则列表中,否则就是ip规则 + if "port" in rule: + ports.append({ + "Protocol": rule["port"]["protocol"] if "protocol" in rule else "tcp", + "Port": rule["port"]["port"], + "Strategy": rule["Strategy"] if "Strategy" in rule else "accept", + "Family": rule["Family"] if "Family" in rule else "ipv4", + "Address": rule["Address"] if "Address" in rule else "all", + "Chain": "INPUT", + }) + # 2024/3/25 下午 5:01 处理带源ip的端口转发规则 + elif "forward-port" in rule: + forward_ports.append({ + "type": "port_forward", + "number": len(forward_ports) + 1, + "Protocol": rule["forward-port"]["protocol"], + "S_Address": rule["Address"], + "S_Port": rule["forward-port"]["S_Port"], + "T_Address": rule["forward-port"]["T_Address"], + "T_Port": rule["forward-port"]["T_Port"], + }) + else: + if "Address" not in rule: + continue + + rule["Chain"] = "INPUT" + rules.append(rule) + # 2024/3/25 下午 2:57 端口转发规则 + elif elem.tag == "forward-port": + port = { + "type": "port_forward", + "number": len(forward_ports) + 1, + "Protocol": elem.attrib["protocol"] if "protocol" in elem.attrib else "tcp", + "S_Address": "", + "S_Port": elem.attrib["port"] if "port" in elem.attrib else "", + "T_Address": elem.attrib["to-addr"] if "to-addr" in elem.attrib else "", + "T_Port": elem.attrib["to-port"] if "to-port" in elem.attrib else "", + } + forward_ports.append(port) + + return {"services": services, "ports": ports, "rules": rules, "forward_ports": forward_ports} + except Exception as e: + return {"services": [], "ports": [], "rules": [], "forward_ports": []} + + # 2024/3/22 下午 2:32 解析direct.xml的防火墙规则 + def parse_direct_xml(self) -> dict: + ''' + @name 解析direct.xml的防火墙规则 + @author wzz <2024/3/22 下午 2:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + try: + import xml.etree.ElementTree as ET + file_path = "/etc/firewalld/direct.xml" + if not os.path.exists(file_path): + return {"ports": [], "rules": []} + + ports = [] + rules = [] + + tree = ET.parse(file_path) + root = tree.getroot() + + for elem in root: + if elem.tag == "rule": + protocol = "tcp" + port = "" + strategy = "" + address = "" + + elem_t = elem.text.split(" ") + # 2024/3/22 下午 4:14 解析 Options 得到端口,策略,地址,协议 + for i in elem_t: + if i == "-p": + protocol = elem_t[elem_t.index(i) + 1] # 如果找到匹配项,结果为索引+1的值,-p tcp,值为tcp + elif i == "--dport": + port = elem_t[elem_t.index(i) + 1] + elif i == "-j": + strategy = elem_t[elem_t.index(i) + 1] + elif i == "-d": + address = elem_t[elem_t.index(i) + 1] + + rule = { + "Family": elem.attrib["ipv"], + "Chain": elem.attrib["chain"], + "Strategy": strategy.lower(), + "Address": address if address != "" else "all", + # "Options": elem.text + } + + # 2024/3/22 下午 4:13 如果端口不为空,就是端口规则 + if port != "": + rule["Port"] = port + rule["Protocol"] = protocol + + ports.append(rule) + # 2024/3/22 下午 4:14 如果端口为空,就是ip规则 + else: + rules.append(rule) + + return {"ports": ports, "rules": rules} + except Exception as e: + return {"ports": [], "rules": []} + + # 2024/3/22 下午 4:54 检查是否开启了masquerade,没有则开启 + def check_masquerade(self) -> dict: + ''' + @name 检查是否开启了masquerade,没有则开启 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + stdout, stderr = public.ExecShell("firewall-cmd --query-masquerade") + if "no" in stdout: + stdout, stderr = public.ExecShell("firewall-cmd --add-masquerade") + if stderr: + return self._result(False, "开启masquerade失败, err: {}".format(stderr)) + return self._result(True, "开启masquerade成功") + return self._result(True, "masquerade已经开启") + + # 2024/3/22 下午 4:57 设置端口转发 + def port_forward(self, info: dict, operation: str) -> dict: + ''' + @name 设置端口转发 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if operation not in ["add", "remove"]: + return self._result(False, "不支持的操作: {}".format(operation)) + + if operation == "add": + check_masquerade = self.check_masquerade() + if not check_masquerade["status"]: + return check_masquerade + + # 2024/3/25 下午 6:07 处理有源地址的情况 + if "S_Address" in info and info["S_Address"] != "": + # 2024/3/25 下午 6:05 处理tcp/udp双协议的情况 + if info['Protocol'].find("/") != -1: + rich_rules = self.cmd_str + " --zone=public" + rich_rules += " --{0}-rich-rule='rule family=\"{1}\" source address=\"{2}\" forward-port port=\"{3}\" protocol=\"tcp\" to-port=\"{4}\" to-addr=\"{5}\"' --permanent".format( + operation, + info['Family'], + info['S_Address'], + info['S_Port'], + info['T_Port'], + info['T_Address'], + ) + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + rich_rules = self.cmd_str + " --zone=public" + rich_rules += " --{0}-rich-rule='rule family=\"{1}\" source address=\"{2}\" forward-port port=\"{3}\" protocol=\"udp\" to-port=\"{4}\" to-addr=\"{5}\"' --permanent".format( + operation, + info['Family'], + info['S_Address'], + info['S_Port'], + info['T_Port'], + info['T_Address'], + ) + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + # 2024/3/25 下午 6:05 处理单协议的情况 + else: + rich_rules = self.cmd_str + " --zone=public" + rich_rules += " --{0}-rich-rule='rule family=\"{1}\" source address=\"{2}\" forward-port port=\"{3}\" protocol=\"{4}\" to-port=\"{5}\" to-addr=\"{6}\"'".format( + operation, + info['Family'], + info['S_Address'], + info['S_Port'], + info['Protocol'], + info['T_Port'], + info['T_Address'], + ) + rich_rules += " --permanent" + stdout, stderr = public.ExecShell(rich_rules) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + # 2024/3/25 下午 6:08 处理没有源地址的情况 + else: + # 2024/3/25 下午 6:05 处理tcp/udp双协议的情况 + if info['Protocol'].find("/") != -1: + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-forward-port='port={}:proto={}:toport={}:toaddr={}' --permanent" + .format(self.cmd_str, operation, info['S_Port'], "udp", info['T_Port'], info['T_Address']) + ) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-forward-port='port={}:proto={}:toport={}:toaddr={}' --permanent" + .format(self.cmd_str, operation, info['S_Port'], "tcp", info['T_Port'], info['T_Address']) + ) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + # 2024/3/25 下午 6:09 处理单协议的情况 + else: + stdout, stderr = public.ExecShell( + "{} --zone=public --{}-forward-port='port={}:proto={}:toport={}:toaddr={}' --permanent" + .format(self.cmd_str, operation, info['S_Port'], info['Protocol'], info['T_Port'], info['T_Address']) + ) + if "success" not in stdout and stderr: + if "ALREADY_ENABLED" in stderr: + return self._result(True, "端口转发规则已经存在") + return self._result(False, "设置端口转发失败, err: {}".format(stderr)) + + return self._result(True, "设置端口转发成功") + + # 2024/3/25 下午 2:37 获取所有端口转发规则 + def list_port_forward(self) -> list: + ''' + @name 获取所有端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + return self.parse_public_zone()["forward_ports"] + + +if __name__ == '__main__': + args = sys.argv + firewall = Firewalld() + Firewalld_status = firewall.status() + if len(args) < 2: + print("Welcome to the Firewalld command-line interface!") + print("Firewall status is :", Firewalld_status) + print("Firewall version: ", firewall.version()) + if Firewalld_status == "not running": + print("Firewalld未启动,请启动Firewalld后再执行命令!") + print("启动命令: start") + print() + sys.exit(1) + print("Usage: ") + print("status: Get the status of the firewall.") + print("version: Get the version of the firewall.") + print("start: Start the firewall.") + print("stop: Stop the firewall.") + print("restart: Restart the firewall.") + print("reload: Reload the firewall.") + print("list_input_port: List all input ports.") + print("list_input_address: List all input address rules.") + print("input_port: Add or remove input port.") + print("output_port: Add or remove output port.") + print("list_output_port: List all output ports.") + print("list_output_address: List all output address rules.") + print("rich_rules: Add or remove rich rules.") + sys.exit(1) + + if args[1] == "status": + print("Firewall status is :", Firewalld_status) + elif args[1] == "version": + print("Firewall version: ", firewall.version()) + elif args[1] == "start": + print(firewall.start()) + elif args[1] == "stop": + print(firewall.stop()) + elif args[1] == "restart": + print(firewall.restart()) + elif args[1] == "reload": + print(firewall.reload()) + elif args[1] == "list_input_port": + print(firewall.list_input_port()) + elif args[1] == "list_input_address": + print(firewall.list_input_address()) + elif args[1] == "input_port": + if len(args) < 4: + print("Usage: input_port Port Protocol") + sys.exit(1) + print(firewall.input_port({"Port": args[2], "Protocol": args[3]}, args[4])) + elif args[1] == "output_port": + if len(args) < 6: + print("Usage: output_port Port Protocol Strategy Priority") + sys.exit(1) + print(firewall.output_port({"Port": args[2], "Protocol": args[3], "Strategy": args[4], "Priority": args[5]}, args[6])) + elif args[1] == "list_output_port": + print(firewall.list_output_port()) + elif args[1] == "list_output_address": + print(firewall.list_output_address()) + elif args[1] == "rich_rules": + if len(args) < 4: + print("Usage: rich_rules Family Address Port Protocol Strategy") + sys.exit(1) + print(firewall.rich_rules({"Family": args[2], "Address": args[3], "Port": args[4], "Protocol": args[5], "Strategy": args[6]}, args[7])) + elif args[1] == "output_rich_rules": + if len(args) < 6: + print("Usage: output_rich_rules Family Address Port Protocol Strategy Priority") + sys.exit(1) + print(firewall.output_rich_rules({"Family": args[2], "Address": args[3], "Port": args[4], "Protocol": args[5], "Strategy": args[6], "Priority": args[7]}, args[8])) + elif args[1] == "port_forward": + if len(args) < 7: + print("Usage: port_forward Port Protocol ToPort ToAddr") + sys.exit(1) + print(firewall.port_forward({"Port": args[2], "Protocol": args[3], "ToPort": args[4], "ToAddr": args[5]}, args[6])) + else: + print("Command not found!") + sys.exit(1) + diff --git a/class_v2/firewallModelV2/app/iptables.py b/class_v2/firewallModelV2/app/iptables.py new file mode 100644 index 00000000..7ae89edc --- /dev/null +++ b/class_v2/firewallModelV2/app/iptables.py @@ -0,0 +1,939 @@ +#!/www/server/panel/pyenv/bin/python3.7 +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - iptables封装库 +# ------------------------------ + +import re +import subprocess +import os +import sys + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public +from firewallModelV2.app.appBase import Base + + +# import re + +class Iptables(Base): + def __init__(self): + self.cmd_str = self._set_cmd_str() + self.protocol = { + "6": "tcp", + "17": "udp", + "0": "all" + } + + def _set_cmd_str(self): + return "iptables" + + # 2024/3/19 下午 5:00 获取系统防火墙的运行状态 + def status(self): + ''' + @name 获取系统防火墙的运行状态 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return "running" + + # 2024/3/19 下午 5:00 获取系统防火墙的版本号 + def version(self): + ''' + @name 获取系统防火墙的版本号 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = public.ExecShell("iptables -v 2>&1|awk '{print $2}'|head -1")[0].replace("\n", "") + if result == "": + return "未知的iptables版本" + return result + except Exception as e: + return "未知版本" + + # 2024/3/19 下午 5:00 启动防火墙 + def start(self): + ''' + @name 启动防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持设置状态") + + # 2024/3/19 下午 5:00 停止防火墙 + def stop(self): + ''' + @name 停止防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持停止") + + # 2024/3/19 下午 4:59 重启防火墙 + def restart(self): + ''' + @name 重启防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持重启") + + # 2024/3/19 下午 4:59 重载防火墙 + def reload(self): + ''' + @name 重载防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self._result(True, "当前系统防火墙为iptables,不支持重载") + + # 2024/3/19 下午 3:36 检查表名是否合法 + def check_table_name(self, table_name): + ''' + @name 检查表名是否合法 + @param "table_name": "filter/nat/mangle/raw/security" + @return dict{"status":True/False,"msg":"提示信息"} + ''' + table_names = ['filter', 'nat', 'mangle', 'raw', 'security'] + if table_name not in table_names: + return False + return True + + # 2024/3/19 下午 3:55 解析规则列表输出,返回规则列表字典 + def parse_rules(self, stdout): + ''' + @name 解析规则列表输出,返回规则列表字典 + @author wzz <2024/3/19 下午 3:53> + 字段含义: + "number": 规则编号,对应规则在链中的顺序。 + "chain": 规则所属的链的名称。 + "pkts": 规则匹配的数据包数量。 + "bytes": 规则匹配的数据包字节数。 + "target": 规则的目标动作,表示数据包匹配到该规则后应该执行的操作。 + "prot": 规则适用的协议类型。 + "opt": 规则的选项,包括规则中使用的匹配条件或特定选项。 + "in": 规则匹配的数据包的输入接口。 + "out": 规则匹配的数据包的输出接口。 + "source": 规则匹配的数据包的源地址。 + "destination": 规则匹配的数据包的目标地址。 + "options": 规则的其他选项或说明,通常是规则中的注释或附加信息。 + + protocol(port协议头中数字对应的协议类型): + 0: 表示所有协议 + 1: ICMP(Internet 控制消息协议) + 6: TCP(传输控制协议) + 17: UDP(用户数据报协议) + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + lines = stdout.strip().split('\n') + rules = [] + current_chain = None + for line in lines: + if line.startswith("Chain"): + current_chain = line.split()[1] + elif (line.startswith("target") or line.strip() == "" or "source" in line or + "Warning: iptables-legacy tables present" in line): + # 过滤表头,空行,警告 + continue + else: + rule_info = line.split() + rule = { + "number": rule_info[0], + "chain": current_chain, + "pkts": rule_info[1], + "bytes": rule_info[2], + "target": rule_info[3], + "prot": rule_info[4], + "opt": rule_info[5], + "in": rule_info[6], + "out": rule_info[7], + "source": rule_info[8], + "destination": rule_info[9], + "options": " ".join(rule_info[10:]).strip() + } + rules.append(rule) + return rules + + # 2024/3/19 下午 3:02 列出指定表的指定链的规则 + def list_rules(self, parm): + ''' + @name 列出指定表的指定链的规则 + @author wzz <2024/3/19 下午 3:02> + @param + @return + ''' + try: + if not self.check_table_name(parm['table']): + return "错误: 不支持的表名." + stdout = subprocess.check_output( + [self.cmd_str, '-t', parm['table'], '-L', parm['chain_name'], '-nv', '--line-numbers'], + stderr=subprocess.STDOUT, universal_newlines=True + ) + return self.parse_rules(stdout) + except Exception as e: + return [] + + # 2024/4/29 下午12:16 列出iptables中所有INPUT和OUTPUT的端口规则 + def list_port(self): + ''' + @name 列出iptables中所有INPUT和OUTPUT的端口规则 + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "INPUT" + }] + ''' + try: + list_port = self.list_input_port() + self.list_output_port() + for i in list_port: + i["Strategy"] = i["Strategy"].lower() + return list_port + except Exception as e: + return [] + + # 2024/4/29 下午2:39 列出防火墙中所有的INPUT端口规则 + def list_input_port(self): + ''' + @name 列出防火墙中所有的INPUT端口规则 + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "INPUT" + }] + ''' + try: + list_port = self.get_chain_port("INPUT") + for i in list_port: + i["Strategy"] = i["Strategy"].lower() + return list_port + except Exception as e: + return [] + + # 2024/4/29 下午2:39 列出防火墙中所有的OUTPUT端口规则 + def list_output_port(self): + ''' + @name 列出防火墙中所有的OUTPUT端口规则 + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "OUTPUT" + }] + ''' + try: + list_port = self.get_chain_port("OUTPUT") + for i in list_port: + i["Strategy"] = i["Strategy"].lower() + return list_port + except Exception as e: + return [] + + # 2024/4/29 下午3:28 根据链来获取端口规则,暂时只支持INPUT/OUTPUT链 + def get_chain_port(self, chain): + ''' + @name 根据链来获取端口规则 + @author wzz <2024/4/29 下午3:29> + @param chain = INPUT/OUTPUT + @return [{ + "Protocol": "tcp", + "Port": "8888", + "Strategy": "accept", + "Family": "ipv4", + "Address": "all", + "Chain": "OUTPUT" + }] + ''' + if chain not in ["INPUT", "OUTPUT"]: + return [] + + try: + stdout = self.get_chain_data(chain) + if stdout == "": + return [] + + lines = stdout.strip().split('\n') + rules = [] + for line in lines: + if line.startswith("Chain"): + continue + if not "dpt:" in line and not "multiport sports" in line: + continue + rule_info = line.split() + if rule_info[0] == "num": + continue + if not rule_info[3] in ["ACCEPT", "DROP", "REJECT"]: + continue + if not rule_info[4] in self.protocol: + continue + if not "dpt" in rule_info[-1] and not "-" in rule_info[-1] and not ":" in rule_info[-1]: + continue + + if ":" in rule_info[-1] and not "dpt" in rule_info[-1]: + Port = rule_info[-1] + elif "-" in rule_info[-1]: + Port = rule_info[-5].split(":")[1] + else: + Port = rule_info[-1].split(":")[1] + + if "source IP range" in line and "multiport sports" in line: + Address = rule_info[-4] + elif not "0.0.0.0/0" in rule_info[8]: + Address = rule_info[8] + elif "-" in rule_info[-1]: + Address = rule_info[-1] + else: + Address = "all" + + rule = { + "Protocol": self.protocol[rule_info[4]], + "Port": Port, + "Strategy": rule_info[3], + "Family": "ipv4", + "Address": Address, + "Chain": chain, + } + rules.append(rule) + + return rules + except Exception as e: + return [] + + # 2024/4/29 下午3:28 根据链来获取IP规则,暂时只支持INPUT/OUTPUT链 + def get_chain_ip(self, chain): + ''' + @name 根据链来获取端口规则 + @author wzz <2024/4/29 下午3:29> + @param chain = INPUT/OUTPUT + @return [ + { + "Family": "ipv4", + "Address": "192.168.1.190", + "Strategy": "accept", + "Chain": "INPUT" + } + ] + ''' + if chain not in ["INPUT", "OUTPUT"]: + return [] + + try: + stdout = self.get_chain_data(chain) + if stdout == "": + return [] + + lines = stdout.strip().split('\n') + rules = [] + for line in lines: + if line.startswith("Chain"): + continue + if "dpt:" in line or "multiport sports" in line: + continue + rule_info = line.split() + if rule_info[0] == "num": + continue + if not rule_info[3] in ["ACCEPT", "DROP", "REJECT"]: + continue + if not rule_info[4] in self.protocol: + continue + + Address = "" + if not "0.0.0.0/0" in rule_info[8]: + Address = rule_info[8] + elif "0.0.0.0/0" in rule_info[8] and "-" in rule_info[-1]: + Address = rule_info[-1] + + if Address == "": + continue + + rule = { + "Family": "ipv4", + "Address": Address, + "Strategy": rule_info[3], + "Chain": chain, + } + rules.append(rule) + + return rules + except Exception as e: + return [] + + # 2024/4/29 下午4:01 获取指定链的数据 + def get_chain_data(self, chain): + ''' + @name 获取指定链的数据 + @author wzz <2024/4/29 下午4:01> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + cmd = "{} -t filter -L {} -nv --line-numbers".format(self.cmd_str, chain) + stdout, stderr = public.ExecShell(cmd) + return stdout + except Exception as e: + return "" + + # 2024/4/29 下午2:46 列出防火墙中所有的INPUT和OUTPUT的ip规则 + def list_address(self): + ''' + @name 列出防火墙中所有的ip规则 + @author wzz <2024/4/29 下午2:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return + ''' + try: + list_address = self.get_chain_ip("INPUT") + self.get_chain_ip("OUTPUT") + for i in list_address: + i["Strategy"] = i["Strategy"].lower() + return list_address + except Exception as e: + return [] + + # 2024/4/29 下午2:48 列出防火墙中所有input的ip规则 + def list_input_address(self): + ''' + @name 列出防火墙中所有input的ip规则 + @author wzz <2024/4/29 下午2:48> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + list_address = self.get_chain_ip("INPUT") + for i in list_address: + i["Strategy"] = i["Strategy"].lower() + return list_address + except Exception as e: + return [] + + # 2024/4/29 下午2:49 列出防火墙中所有output的ip规则 + def list_output_address(self): + ''' + @name 列出防火墙中所有output的ip规则 + @author wzz <2024/4/29 下午2:49> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + list_address = self.get_chain_ip("OUTPUT") + for i in list_address: + i["Strategy"] = i["Strategy"].lower() + return list_address + except Exception as e: + return [] + + # 2024/4/29 下午2:49 添加INPUT端口规则 + def input_port(self, info, operation): + ''' + @name 添加INPUT端口规则 + @author wzz <2024/4/29 下午2:50> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return self.set_chain_port(info, operation, "INPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午2:50 设置output端口策略 + def output_port(self, info, operation): + ''' + @name 设置output端口策略 + @author wzz <2024/4/29 下午2:50> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + return self.set_chain_port(info, operation, "OUTPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午4:49 添加/删除指定链的端口规则 + def set_chain_port(self, info, operation, chain): + ''' + @name 添加/删除指定链的端口规则 + @author wzz <2024/4/29 下午4:49> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not chain in ["INPUT", "OUTPUT"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的链类型")) + + if info['Protocol'] not in ["tcp", "udp"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的协议类型")) + if info["Strategy"] == "accept": + info["Strategy"] = "ACCEPT" + elif info["Strategy"] == "drop": + info["Strategy"] = "DROP" + elif info["Strategy"] == "reject": + info["Strategy"] = "REJECT" + else: + return self._result(False, "设置端口规则失败:{}".format("不支持的策略类型")) + + if operation == "add": + operation = "-I" + elif operation == "remove": + operation = "-D" + + rule = "{} -t filter {} {} -p {} --dport {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Strategy'] + ) + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "设置端口规则失败:{}".format(stderr)) + return self._result(True, "设置端口规则成功") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午5:01 添加/删除指定链的复杂端口规则 + def set_chain_rich_port(self, info, operation, chain): + ''' + @name 添加/删除指定链的复杂端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not chain in ["INPUT", "OUTPUT"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的链类型")) + + if "Address" in info and info["Address"] == "": + info["Address"] = "all" + if "Address" in info and public.is_ipv6(info['Address']): + return self._result(False, "设置端口规则失败:{}".format("不支持的IPV6地址")) + + if info['Protocol'] not in ["tcp", "udp"]: + return self._result(False, "设置端口规则失败:{}".format("不支持的协议类型")) + if info["Strategy"] == "accept": + info["Strategy"] = "ACCEPT" + elif info["Strategy"] == "drop": + info["Strategy"] = "DROP" + elif info["Strategy"] == "reject": + info["Strategy"] = "REJECT" + else: + return self._result(False, "设置端口规则失败:{}".format("不支持的策略类型")) + + if operation == "add": + operation = "-I" + elif operation == "remove": + operation = "-D" + + info['Port'] = info['Port'].replace("-", ":") + info["Address"] = info["Address"].replace(":", "-") + if ":" in info['Port'] or "-" in info['Port']: + if ":" in info["Address"] or "-" in info["Address"]: + # iptables -t filter -I INPUT -m iprange --src-range 192.168.1.100-192.168.1.200 -p tcp -m multiport --sports 8000:9000 -j ACCEPT + rule = "{} -t filter {} {} -m iprange --src-range {} -p {} -m multiport --sports {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Address'], + info['Protocol'], + info['Port'], + info['Strategy'] + ) + else: + # iptables -t filter -I INPUT -p tcp -m multiport --sports 8000:9000 -s 192.168.1.100 -j ACCEPT + rule = "{} -t filter {} {} -p {} -m multiport --sports {} -s {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Address'], + info['Strategy'] + ) + else: + if ":" in info["Address"] or "-" in info["Address"]: + # iptables -t filter -I OUTPUT -p tcp --dport 22333 -m iprange --src-range 192.168.1.100-192.168.1.200 -j ACCEPT + rule = "{} -t filter {} {} -p {} --dport {} -m iprange --src-range {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Address'], + info['Strategy'] + ) + else: + # iptables -t filter -I OUTPUT -p tcp --dport 22333 -s 192.168.1.0/24 -j ACCEPT + rule = "{} -t filter {} {} -p {} --dport {} -s {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Protocol'], + info['Port'], + info['Address'], + info['Strategy'] + ) + + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "设置端口规则失败:{}".format(stderr)) + return self._result(True, "设置端口规则成功") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午5:01 添加/删除指定链的复杂ip规则 + def set_chain_rich_ip(self, info, operation, chain): + ''' + @name 添加/删除指定链的复杂ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not chain in ["INPUT", "OUTPUT"]: + return self._result(False, "设置规则失败:{}".format("不支持的链类型")) + + if "Address" in info and info["Address"] == "": + info["Address"] = "all" + if "Address" in info and public.is_ipv6(info['Address']): + return self._result(False, "设置规则失败:{}".format("不支持的IPV6地址")) + + if info["Strategy"] == "accept": + info["Strategy"] = "ACCEPT" + elif info["Strategy"] == "drop": + info["Strategy"] = "DROP" + elif info["Strategy"] == "reject": + info["Strategy"] = "REJECT" + else: + return self._result(False, "设置规则失败:{}".format("不支持的策略类型")) + + if operation == "add": + operation = "-I" + elif operation == "remove": + operation = "-D" + + if ":" in info["Address"] or "-" in info["Address"]: + # iptables -t filter -I INPUT -m iprange --src-range 192.168.1.100-192.168.1.200 -j ACCEPT + rule = "{} -t filter {} {} -m iprange --src-range {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Address'], + info['Strategy'] + ) + else: + # iptables -t filter -I INPUT -s 192.168.1.100 -j ACCEPT + rule = "{} -t filter {} {} -s {} -j {}".format( + self.cmd_str, + operation, + chain, + info['Address'], + info['Strategy'] + ) + + + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "设置规则失败:{}".format(stderr)) + return self._result(True, "设置规则成功") + except Exception as e: + return self._result(False, "设置规则失败:{}".format(str(e))) + + # 2024/4/29 下午2:51 INPUT复杂一些的规则管理 + def rich_rules(self, info, operation): + ''' + @name + @author wzz <2024/4/29 下午2:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if "Priority" in info and not "Port" in info: + return self.set_chain_rich_ip(info, operation, "INPUT") + else: + return self.set_chain_rich_port(info, operation, "INPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/4/29 下午2:52 OUTPUT复杂一些的规则管理 + def output_rich_rules(self, info, operation): + ''' + @name OUTPUT复杂一些的规则管理 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if "Priority" in info and not "Port" in info: + return self.set_chain_rich_ip(info, operation, "OUTPUT") + else: + return self.set_chain_rich_port(info, operation, "OUTPUT") + except Exception as e: + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/3/19 下午 3:03 清空指定链中的所有规则 + def flush_chain(self, chain_name): + ''' + @name 清空指定链中的所有规则 + @author wzz <2024/3/19 下午 3:03> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + subprocess.check_output( + [self.cmd_str, '-F', chain_name], stderr=subprocess.STDOUT, universal_newlines=True + ) + return chain_name + " chain flushed successfully." + except Exception as e: + return "Failed to flush " + chain_name + " chain." + + # 2024/3/19 下午 3:03 获取当前系统中可用的链的名称列表 + def get_chain_names(self, parm): + ''' + @name 获取当前系统中可用的链的名称列表 + @author wzz <2024/3/19 下午 3:03> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not self.check_table_name(parm['table']): + return "错误: 不支持的表名." + stdout = subprocess.check_output( + [self.cmd_str, '-t', parm['table'], '-L'], stderr=subprocess.STDOUT, universal_newlines=True + ) + chain_names = re.findall(r"Chain\s([A-Z]+)", stdout) + return chain_names + except Exception as e: + return [] + + # 2024/3/19 下午 3:17 构造端口转发规则,然后调用insert_rule方法插入规则 + def port_forward(self, info, operation): + ''' + @name 构造端口转发规则,然后调用insert_rule方法插入规则 + @param "info": { + "Protocol": "tcp/udp", + "S_Port": "80", + "T_Address": "0.0.0.0/0", + "T_Port": "8080" + } + @param "operation": "add" or "remove" + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + rule = " -p {}".format(info['Protocol']) + + if "S_Address" in info and info['S_Address'] != "": + rule += " -s {}".format(info['S_Address']) + + rule += " --dport {0} -j DNAT --to-destination {1}:{2}".format( + info['S_Port'], + info['T_Address'], + info['T_Port'], + ) + + parm = { + "table": "nat", + "chain_name": "PREROUTING", + "rule": rule + } + if operation not in ["add", "remove"]: + return "请输入正确的操作类型. (add/remove)" + + if operation == "add": + parm['type'] = "-I" + elif operation == "remove": + parm['type'] = "-D" + return self.rule_manage(parm) + except Exception as e: + return self._result(False, "设置端口转发规则失败:{}".format(str(e))) + + # 2024/3/19 下午 3:03 在指定链中管理规则 + def rule_manage(self, parm): + ''' + @name 在指定链中管理规则 + @author wzz <2024/3/19 下午 3:03> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if not self.check_table_name(parm['table']): + return self._result(False, "不支持的表名{}".format(parm['table'])) + + rule = "{} -t {} {} {} {}".format( + self.cmd_str, parm['table'], parm['type'], parm['chain_name'], parm['rule'] + ) + stdout, stderr = public.ExecShell(rule) + if stderr: + return self._result(False, "规则设置失败:{}".format(stderr)) + + return self._result(True, "规则设置成功") + except Exception as e: + return self._result(False, "规则设置失败: {}".format(str(e))) + + # 2024/4/29 下午5:55 获取所有端口转发列表 + def list_port_forward(self): + ''' + @name 获取所有端口转发列表 + @author wzz <2024/4/29 下午5:55> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.get_nat_prerouting_rules() + + # 2024/3/19 下午 4:00 调用list_rules获取所有nat表中的PREROUTING链的规则(端口转发规则),并分析成字典返回 + def get_nat_prerouting_rules(self): + ''' + @name 调用list_rules获取所有nat表中的PREROUTING链的规则(端口转发规则),并分析成字典返回 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + port_forward_rules = self.list_rules({"table": "nat", "chain_name": "PREROUTING"}) + rules = [] + for rule in port_forward_rules: + if rule["target"] != "DNAT": continue + options = rule["options"].split(' ') + + protocol = "TCP" + if rule["prot"] == "6" or rule["prot"] == "tcp": + protocol = "TCP" + elif rule["prot"] == "17" or rule["prot"] == "udp": + protocol = "UDP" + elif rule["prot"] == "0" or rule["prot"] == "all": + protocol = "TCP/UDP" + rules.append({ + "type": "port_forward", + "number": rule["number"], + "S_Address": rule["source"], + "S_Port": options[1].split("dpt:")[1], + "T_Address": options[2].split("to:")[1].split(":")[0], + "T_Port": options[2].split("to:")[1].split(":")[1], + "Protocol": protocol.lower() + }) + return rules + except Exception as e: + return [] + + # 2024/4/29 下午2:43 格式化输出json + def format_json(self, data): + ''' + @name 格式化输出json + @param "data": json数据 + @return json字符串 + ''' + import json + from pygments import highlight, lexers, formatters + + formatted_json = json.dumps(data, indent=3) + colorful_json = highlight(formatted_json.encode('utf-8'), lexers.JsonLexer(), + formatters.TerminalFormatter()) + return colorful_json + + +if __name__ == '__main__': + args = sys.argv + firewall = Iptables() + if len(args) < 2: + print("Welcome to the iptables command-line interface!") + print() + print("Available options:") + print("list_rules: list_rules
                                    ") + print("flush_chain: flush_chain
                                    ") + print("get_chain_names: get_chain_names
                                    ") + print("port_forward: port_forward ") + print("get_nat_prerouting_rules: get_nat_prerouting_rules") + print() + sys.exit(1) + if args[1] == "list_rules": + # firewall.list_rules({"table": args[2], "chain_name": args[3]}) + print(firewall.list_rules({"table": args[2], "chain_name": args[3]})) + elif args[1] == "flush_chain": + print(firewall.flush_chain(args[2])) + elif args[1] == "get_chain_names": + table = args[2] if len(args) > 2 else "filter" + print(firewall.get_chain_names({"table": table})) + elif args[1] == "port_forward": + if len(args) < 8: + print("传参使用方法: port_forward ") + sys.exit(1) + + info = { + "S_Address": args[2], + "S_Port": args[3], + "T_Address": args[4], + "T_Port": args[5], + "Protocol": args[6] + } + print(firewall.port_forward(info, args[7])) + elif args[1] == "get_nat_prerouting_rules": + import json + from pygments import highlight, lexers, formatters + + formatted_json = json.dumps(firewall.get_nat_prerouting_rules(), indent=3) + colorful_json = highlight(formatted_json.encode('utf-8'), lexers.JsonLexer(), + formatters.TerminalFormatter()) + print(colorful_json) + # print(firewall.get_nat_prerouting_rules()) + elif args[1] == "list_port": + print(firewall.format_json(firewall.list_port())) + elif args[1] == "list_input_port": + print(firewall.format_json(firewall.list_input_port())) + elif args[1] == "list_output_port": + print(firewall.format_json(firewall.list_output_port())) + elif args[1] == "list_address": + print(firewall.format_json(firewall.list_address())) + elif args[1] == "list_input_address": + print(firewall.format_json(firewall.list_input_address())) + elif args[1] == "list_output_address": + print(firewall.format_json(firewall.list_output_address())) + elif args[1] == "input_port": + info = { + "Protocol": args[2], + "Port": args[3], + "Strategy": args[4] + } + print(firewall.input_port(info, args[5])) + elif args[1] == "output_port": + info = { + "Protocol": args[2], + "Port": args[3], + "Strategy": args[4] + } + print(firewall.output_port(info, args[5])) + elif args[1] == "rich_rules": + info = { + "Protocol": args[2], + "Port": args[3], + "Address": args[4], + "Strategy": args[5] + } + print(firewall.rich_rules(info, args[6])) + elif args[1] == "output_rich_rules": + info = { + "Protocol": args[2], + "Port": args[3], + "Address": args[4], + "Strategy": args[5] + } + print(firewall.output_rich_rules(info, args[6])) + else: + print("不支持的传参: " + args[1]) + sys.exit(1) diff --git a/class_v2/firewallModelV2/app/ufw.py b/class_v2/firewallModelV2/app/ufw.py new file mode 100644 index 00000000..7d5b4f8f --- /dev/null +++ b/class_v2/firewallModelV2/app/ufw.py @@ -0,0 +1,729 @@ +#!/www/server/panel/pyenv/bin/python3.7 +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - ufw封装库 +# ------------------------------ + +import subprocess +import os +import sys +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public +# import re +from firewallModelV2.app.appBase import Base + +class Ufw(Base): + def __init__(self): + self.cmd_str = self._set_cmd_str() + + def _set_cmd_str(self): + return "ufw" + + # 2024/3/19 下午 5:00 获取系统防火墙的运行状态 + def status(self): + ''' + @name 获取系统防火墙的运行状态 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run([self.cmd_str, "status"], capture_output=True, text=True, check=True) + if "Status: active" in result.stdout: + return "running" + elif "状态: 激活" in result.stdout: + return "running" + else: + return "not running" + except subprocess.CalledProcessError: + return "not running" + except Exception as e: + return "not running" + + # 2024/3/19 下午 5:00 获取系统防火墙的版本号 + def version(self): + ''' + @name 获取系统防火墙的版本号 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run([self.cmd_str, "version"], capture_output=True, text=True, check=True) + info = result.stdout.replace("\n", "") + return info.replace("ufw ", "") + except Exception as e: + return "未知版本" + + # 2024/3/19 下午 5:00 启动防火墙 + def start(self): + ''' + @name 启动防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + stdout, stderr = public.ExecShell("echo y | {} enable".format(self.cmd_str)) + if stderr: + return self._result(False, "启动防火墙失败:{}".format(stderr)) + return self._result(True, "启动防火墙成功") + except Exception as e: + return self._result(False, "启动防火墙失败:{}".format(str(e))) + + # 2024/3/19 下午 5:00 停止防火墙 + def stop(self): + ''' + @name 停止防火墙 + @author wzz <2024/3/19 下午 5:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + stdout, stderr = public.ExecShell("{} disable".format(self.cmd_str)) + if stderr: + return self._result(False, "停止防火墙失败:{}".format(stderr)) + return self._result(True, "停止防火墙成功") + except Exception as e: + return self._result(False, "停止防火墙失败:{}".format(str(e))) + + # 2024/3/19 下午 4:59 重启防火墙 + def restart(self): + ''' + @name 重启防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + self.stop() + self.start() + except Exception as e: + return self._result(False, "重启防火墙失败:{}".format(str(e))) + + # 2024/3/19 下午 4:59 重载防火墙 + def reload(self): + ''' + @name 重载防火墙 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + subprocess.run([self.cmd_str, "reload"], check=True, stdout=subprocess.PIPE) + except Exception as e: + return self._result(False, "重载防火墙失败:{}".format(str(e))) + + # 2024/3/19 上午 10:39 列出防火墙中所有端口规则 + def list_port(self): + ''' + @name 列出防火墙中所有端口规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "port") + if item_fire.get("Port") and item_fire["Port"] != "Anywhere" and "." not in item_fire["Port"]: + item_fire["Port"] = item_fire["Port"].replace(":", "-") + item_fire["Address"] = "all" if item_fire["Address"] == "Anywhere" else item_fire["Address"] + + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有input端口规则 + def list_input_port(self): + ''' + @name 列出防火墙中所有input端口规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "port") + if item_fire.get("Port") and item_fire["Port"] != "Anywhere" and "." not in item_fire["Port"]: + item_fire["Port"] = item_fire["Port"].replace(":", "-") + + if item_fire["Chain"] == "INPUT": + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有output端口规则 + def list_output_port(self): + ''' + @name 列出防火墙中所有output端口规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "port") + if item_fire.get("Port") and item_fire["Port"] != "Anywhere" and "." not in item_fire["Port"]: + item_fire["Port"] = item_fire["Port"].replace(":", "-") + + if item_fire["Chain"] == "OUTPUT": + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有的ip规则 + def list_address(self): + ''' + @name 列出防火墙中所有的ip规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + item_fire = self._load_info(line, "address") + if "Port" in item_fire: continue + if item_fire.get("Address"): + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有input的ip规则 + def list_input_address(self): + ''' + @name 列出防火墙中所有input的ip规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + if " IN" not in line: + continue + item_fire = self._load_info(line, "address") + if "Port" in item_fire: continue + if item_fire.get("Address"): + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 上午 10:39 列出防火墙中所有output的ip规则 + def list_output_address(self): + ''' + @name 列出防火墙中所有output的ip规则 + @author wzz <2024/3/19 上午 10:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + result = subprocess.run( + [self.cmd_str, "status", "verbose"], capture_output=True, text=True, check=True + ) + port_infos = result.stdout.split("\n") + datas = [] + is_start = False + for line in port_infos: + if line.startswith("-"): + is_start = True + continue + if not is_start: + continue + if " OUT" not in line: + continue + item_fire = self._load_info(line, "address") + if "Port" in item_fire: continue + if item_fire.get("Address"): + datas.append(item_fire) + return datas + except Exception as e: + return [] + + # 2024/3/19 下午 4:59 添加端口规则 + def input_port(self, info, operation): + ''' + @name 添加端口规则 + @author wzz <2024/3/19 下午 4:59> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + + if info["Port"].find('-') != -1: + info["Port"] = info["Port"].replace('-', ':') + + if operation == "add": + if info['Protocol'].find("/") != -1: + rich_rule = self.cmd_str + " insert 1 {} {}".format(info['Strategy'], info['Port']) + stdout, stderr = public.ExecShell(rich_rule) + elif info['Protocol'] == "tcp/udp": + stdout, stderr = public.ExecShell(self.cmd_str + " allow " + info['Port']) + else: + stdout, stderr = public.ExecShell(self.cmd_str + " allow " + info['Port'] + "/" + info['Protocol']) + else: + if info['Protocol'].find("/") != -1: + rich_rule = "{} delete {} {}".format(self.cmd_str, info['Strategy'], info['Port']) + stdout, stderr = public.ExecShell(rich_rule) + elif info['Protocol'] == "tcp/udp": + stdout, stderr = public.ExecShell(self.cmd_str + " delete allow " + info['Port']) + else: + stdout, stderr = public.ExecShell(self.cmd_str + " delete allow " + info['Port'] + "/" + info['Protocol']) + + if stderr: + if "setlocale" in stderr: + return self._result(True, "设置端口规则成功") + return self._result(False, "设置端口规则失败:{}".format(stderr)) + + return self._result(True, "设置端口规则成功") + + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置端口规则成功") + return self._result(False, "设置端口规则失败:{}".format(str(e))) + + # 2024/3/24 下午 11:28 设置output端口策略 + def output_port(self, info, operation): + ''' + @name 设置output端口策略 + @param info: 端口号 + @param operation: 操作 + @return None + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + + if operation == "add": + if info['Protocol'].find('/') != -1: + cmd = "{} {} out {}".format(self.cmd_str, info['Strategy'], info['Port']) + else: + cmd = "{} {} out {}/{}".format(self.cmd_str, info['Strategy'], info['Port'], info['Protocol']) + else: + if info['Protocol'].find('/') != -1: + cmd = "{} delete {} out {}".format(self.cmd_str, info['Strategy'], info['Port']) + else: + cmd = "{} delete {} out {}/{}".format(self.cmd_str, info['Strategy'], info['Port'], info['Protocol']) + stdout, stderr = public.ExecShell(cmd) + if stderr: + if "setlocale" in stderr: + return self._result(True, "设置端口规则成功") + return self._result(False, "设置output端口规则失败:{}".format(stderr)) + return self._result(True, "设置output端口规则成功") + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置output端口规则成功") + return self._result(False, "设置output端口规则失败:{}".format(str(e))) + + # 2024/3/19 下午 4:58 复杂一些的规则管理 + def rich_rules(self, info, operation): + ''' + @name 复杂一些的规则管理 + @author wzz <2024/3/19 下午 4:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + else: + return self._result(False, "未知的策略参数:{}".format(info["Strategy"])) + + rule_str = "{} insert 1 {} ".format(self.cmd_str, info["Strategy"]) + if "Address" in info and public.is_ipv6(info['Address']): + rule_str = "{} {} ".format(self.cmd_str, info["Strategy"]) + if operation == "remove": + rule_str = "{} delete {} ".format(self.cmd_str, info["Strategy"]) + + if "Address" in info and info['Address'] != "all": + rule_str += "from {} ".format(info['Address']) + if len(info.get("Protocol", "")) != 0 and "/" not in info['Protocol']: + rule_str += "proto {} ".format(info['Protocol']) + if len(info.get("Port", "")) != 0: + rule_str += "to any port {} ".format(info['Port']) + stdout, stderr = public.ExecShell(rule_str) + if stderr: + if "Rule added" in stdout or "Rule deleted" in stdout or "Rule updated" in stdout or "Rule inserted" in stdout or "Skipping adding existing rule" in stdout: + return self._result(True, "设置规则成功") + if "setlocale" in stderr: + return self._result(True, "设置规则成功") + return self._result(False, "规则设置失败:{}".format(stderr)) + return self._result(True, "设置规则成功") + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置规则成功") + return self._result(False, "规则设置失败:{}".format(e)) + + # 2024/3/24 下午 11:29 设置output rich_rules + def output_rich_rules(self, info, operation): + ''' + @name 设置output rich_rules + @param info: 规则 + @param operation: 操作 + @return None + ''' + try: + if info["Strategy"] == "accept": + info["Strategy"] = "allow" + elif info["Strategy"] == "drop": + info["Strategy"] = "deny" + else: + return self._result(False, "未知的策略: {}".format(info["Strategy"])) + + rule_str = "{} insert 1 {} ".format(self.cmd_str, info["Strategy"]) + if "Address" in info and public.is_ipv6(info['Address']): + rule_str = "{} {} ".format(self.cmd_str, info["Strategy"]) + if operation == "remove": + rule_str = "{} delete {} ".format(self.cmd_str, info["Strategy"]) + + if len(info.get("Address", "")) != 0: + rule_str += "out from {} ".format(info['Address']) + if len(info.get("Protocol", "")) != 0: + rule_str += "proto {} ".format(info['Protocol']) + if len(info.get("Port", "")) != 0: + rule_str += "to any port {} ".format(info['Port']) + stdout, stderr = public.ExecShell(rule_str) + if stderr: + if "Rule added" in stdout or "Rule deleted" in stdout or "Rule updated" in stdout or "Rule inserted" in stdout or "Skipping adding existing rule" in stdout: + return self._result(True, "设置output规则成功") + if "setlocale" in stderr: + return self._result(True, "设置规则成功") + return self._result(False, "outpu规则设置失败:{}".format(stderr)) + return self._result(True, "设置output规则成功") + except Exception as e: + if "setlocale" in str(e): + return self._result(True, "设置output规则成功") + return self._result(False, "outpu规则设置失败:{}".format(e)) + + # 2024/3/19 下午 5:01 解析防火墙规则信息,返回字典格式数据,用于添加或删除防火墙规则 + def _load_info(self, line, fire_type): + ''' + @name 解析防火墙规则信息,返回字典格式数据,用于添加或删除防火墙规则 + @author wzz <2024/3/19 上午 10:38> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + fields = line.split() + item_info = {} + if "LIMIT" in line or "ALLOW FWD" in line: + return item_info + if len(fields) < 4: + return item_info + if fields[0] == "Anywhere" and fire_type != "port": + item_info["Strategy"] = "drop" + + if fields[1] != "(v6)": + if fields[1] == "ALLOW": + item_info["Strategy"] = "accept" + if fields[2] == "IN": + item_info["Chain"] = "INPUT" + elif fields[2] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[3] + item_info["Family"] = "ipv4" + else: + if fields[2] == "ALLOW": + item_info["Strategy"] = "accept" + if fields[3] == "IN": + item_info["Chain"] = "INPUT" + elif fields[3] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[4] + item_info["Family"] = "ipv6" + + return item_info + + if "/" in fields[0]: + item_info["Port"] = fields[0].split("/")[0] + item_info["Protocol"] = fields[0].split("/")[1] + else: + item_info["Port"] = fields[0] + item_info["Protocol"] = "tcp/udp" + + if "v6" in fields[1]: + item_info["Family"] = "ipv6" + if fields[2] == "ALLOW": + item_info["Strategy"] = "accept" + else: + item_info["Strategy"] = "drop" + + if fields[3] == "IN": + item_info["Chain"] = "INPUT" + elif fields[3] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[4] + + else: + item_info["Family"] = "ipv4" if ":" not in fields[3] else "ipv6" + + if fields[1] == "ALLOW": + item_info["Strategy"] = "accept" + else: + item_info["Strategy"] = "drop" + + if fields[2] == "IN": + item_info["Chain"] = "INPUT" + elif fields[2] == "OUT": + item_info["Chain"] = "OUTPUT" + item_info["Address"] = fields[3] + + return item_info + + # 2024/3/25 下午 2:29 设置端口转发 + def port_forward(self, info, operation): + ''' + @name 设置端口转发 + @param port: 端口号 + @param ip: ip地址 + @param operation: 操作 + @return None + ''' + from firewallModel.app.iptables import Iptables + self.firewall = Iptables() + return self.firewall.port_forward(info, operation) + + # 2024/3/25 下午 2:34 获取所有端口转发列表 + def list_port_forward(self): + ''' + @name 获取所有端口转发列表 + @return None + ''' + from firewallModel.app.iptables import Iptables + self.firewall = Iptables() + return self.firewall.get_nat_prerouting_rules() + + +if __name__ == '__main__': + args = sys.argv + firewall = Ufw() + ufw_status = firewall.status() + if len(args) < 2: + print("Welcome to the UFW (Uncomplicated Firewall) command-line interface!") + print("Firewall status is :", ufw_status) + print("Firewall version: ", firewall.version()) + if ufw_status == "not running": + print("ufw未启动,请启动ufw后再执行命令!") + print("启动命令: start") + print() + sys.exit(1) + print() + print("Available options:") + print("1. Check Firewall Status: status") + print("2. Check Firewall Version: version") + print("3. Start Firewall: start") + print("4. Stop Firewall: stop") + print("5. Restart Firewall: restart") + print("6. Reload Firewall: reload") + print("7. List All Ports: list_port") + print("8. List All IP Addresses: list_address") + print("9. Add Port: add_port ") + print("10. Remove Port: remove_port ") + print("11. Add Port Rule: add_port_rule
                                    ") + print("12. Remove Port Rule: remove_port_rule
                                    ") + print("13. Add IP Rule: add_ip_rule
                                    ") + print("14. Remove IP Rule: remove_ip_rule
                                    ") + print() + sys.exit(1) + if args[1] == "status": + print(firewall.status()) + elif args[1] == "version": + print(firewall.version()) + elif args[1] == "start": + error = firewall.start() + if error: + print(f"Error: {error}") + else: + print("Firewall started successfully.") + elif args[1] == "stop": + error = firewall.stop() + if error: + print(f"Error: {error}") + else: + print("Firewall stopped successfully.") + elif args[1] == "restart": + error = firewall.restart() + if error: + print(f"Error: {error}") + else: + print("Firewall restarted successfully.") + elif args[1] == "reload": + error = firewall.reload() + if error: + print(f"Error: {error}") + else: + print("Firewall reloaded successfully.") + elif args[1] == "list_input_port": + ports = firewall.list_input_port() + for p in ports: + print(p) + elif args[1] == "list_output_port": + ports = firewall.list_output_port() + for p in ports: + print(p) + elif args[1] == "list_input_address": + addresses = firewall.list_input_address() + for a in addresses: + print(a) + elif args[1] == "list_output_address": + addresses = firewall.list_output_address() + for a in addresses: + print(a) + elif args[1] == "add_port": + port = args[2] + protocol = args[3] + error = firewall.input_port(f"{port}/{protocol}", "allow") + if error: + print(f"Error: {error}") + else: + print(f"Port {port}/{protocol} added successfully.") + elif args[1] == "remove_port": + port = args[2] + protocol = args[3] + error = firewall.input_port(f"{port}/{protocol}", "remove") + if error: + print(f"Error: {error}") + else: + print(f"Port {port}/{protocol} removed successfully.") + elif args[1] == "add_port_rule": + address = args[2] + port = args[3] + protocol = args[4] + strategy = args[5] + operation = args[6] + error = firewall.rich_rules( + {"Address": address, "Port": port, "Protocol": protocol, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule added successfully.") + elif args[1] == "remove_port_rule": + address = args[2] + port = args[3] + protocol = args[4] + strategy = args[5] + operation = args[6] + error = firewall.rich_rules( + {"Address": address, "Port": port, "Protocol": protocol, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule removed successfully.") + elif args[1] == "add_ip_rule": + address = args[2] + strategy = args[3] + operation = args[4] + error = firewall.rich_rules( + {"Address": address, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule added successfully.") + elif args[1] == "remove_ip_rule": + address = args[2] + strategy = args[3] + operation = args[4] + error = firewall.rich_rules( + {"Address": address, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Rich rule removed successfully.") + elif args[1] == "output_port": + port = args[2] + operation = args[3] + error = firewall.output_port(port, operation) + if error: + print(f"Error: {error}") + else: + print(f"Output port {port} {operation} successfully.") + elif args[1] == "output_rich_rules": + address = args[2] + port = args[3] + protocol = args[4] + strategy = args[5] + operation = args[6] + error = firewall.output_rich_rules( + {"Address": address, "Port": port, "Protocol": protocol, "Strategy": strategy}, operation) + if error: + print(f"Error: {error}") + else: + print("Output rich rule added successfully.") + else: + print("Invalid args") + sys.exit(1) + diff --git a/class_v2/firewallModelV2/comModel.py b/class_v2/firewallModelV2/comModel.py new file mode 100644 index 00000000..465833f8 --- /dev/null +++ b/class_v2/firewallModelV2/comModel.py @@ -0,0 +1,1718 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import json +import re +import time +import os + +# ------------------------------ +# 系统防火墙模型 - 业务接口类 +# ------------------------------ +import public +from firewallModelV2.firewallBase import Base + + +class main(Base): + + def __init__(self): + super().__init__() + + # 2024/3/14 下午 12:01 获取防火墙状态信息 + def get_firewall_info(self, get): + """ + @name 获取防火墙统计 + """ + data = {} + data['port'] = len(self.firewall.list_port()) + data['ip'] = len(self.firewall.list_address()) + data['trans'] = len(self.firewall.list_port_forward()) + data['country'] = public.M('firewall_country').count() + + isPing = True + try: + file = '/etc/sysctl.conf' + conf = public.readFile(file) + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)\n" + tmp = re.search(rep, conf).groups(0)[0] + if tmp == '1': isPing = False + except: + isPing = True + + data['ping'] = isPing + return data + + # 2024/3/26 下午 3:40 获取防火墙状态 + def get_status(self, get): + ''' + @name 获取防火墙状态 + @author wzz <2024/3/26 下午 3:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return self.get_firewall_status() + + # 2024/3/26 下午 3:42 设置防火墙状态 + def set_status(self, get): + ''' + @name 设置防火墙状态 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.status = get.get('status/s', '1') + if get.status not in ['0', '1']: + return public.returnMsg(False, '参数错误') + + if get.status == '1': + return self.firewall.start() + else: + return self.firewall.stop() + + # 2024/5/13 下午3:50 检查指定端口是否已经存在,如果存在则返回False,否则返回True + def check_port_exist(self, get): + ''' + @name 检查指定端口是否已经存在,如果存在则返回False,否则返回True + @author wzz <2024/5/13 下午3:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + port_rules_list = self.port_rules_list(get) + for item in port_rules_list: + if item["Port"] == get.port and item["Address"] == get.address and item["Protocol"] == get.protocol and \ + item["Strategy"] == get.strategy and item["Chain"] == get.chain: + return False + + return True + + # 2024/5/13 下午4:13 检查指定ip规则是否已经存在,如果存在则返回False,否则返回True + def check_ip_exist(self, get): + ''' + @name 检查指定ip规则是否已经存在,如果存在则返回False,否则返回True + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + ip_rules_list = self.ip_rules_list(get) + for item in ip_rules_list: + if item["Address"] == get.address and item["Strategy"] == get.strategy and item["Chain"] == get.chain: + return False + + return True + + # 2024/5/13 下午4:17 检查指定端口转发规则是否已经存在,如果存在则返回False,否则返回True + def check_forward_exist(self, get): + ''' + @name 检查指定端口转发规则是否已经存在,如果存在则返回False,否则返回True + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + forward_rules_list = self.port_forward_list(get) + for item in forward_rules_list: + if item["S_Address"] == get.S_Address and item["S_Port"] == get.S_Port and item["T_Address"] == get.T_Address and \ + item["T_Port"] == get.T_Port: + return False + + return True + + # 2024/3/26 下午 6:09 从数据库中获取端口规则列表 + def get_port_db(self, get): + ''' + @name 从数据库中获取端口规则列表 + @author wzz <2024/3/26 下午 6:13> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + where = '1=1' + sql = public.M('firewall_new') + data = sql.where(where, ()).select() + + domain_sql = public.M('firewall_domain') + domain_data = domain_sql.where(where, ()).select() + for i in range(len(data)): + if not "ports" in data[i]: + data[i]['status'] = -1 + continue + + if "brief" in data[i]: + data[i]['brief'] = public.xssdecode(data[i]['brief']) + + if not "chain" in data[i]: + data[i]['chain'] = "INPUT" + if "chain" in data[i] and data[i]['chain'] == "": + data[i]['chain'] = "INPUT" + + for j in range(len(domain_data)): + if "domain" in domain_data[j] and data[i]['address'] in domain_data[j]['domain']: + data[i]['domain'] = domain_data[j]['domain'] + break + + return data + except Exception as e: + return [] + + # 2024/3/26 下午 11:53 从数据库中获取ip规则列表 + def get_ip_db(self, get): + ''' + @name 从数据库中获取ip规则列表 + @author wzz <2024/3/26 下午 11:53> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + where = '1=1' + sql = public.M('firewall_ip') + + ip_data = sql.where(where, ()).select() + + for i_data in ip_data: + if "brief" in i_data: + i_data['brief'] = public.xssdecode(i_data['brief']) + if not "chain" in i_data: + i_data['chain'] = "INPUT" + if "chain" in i_data and i_data['chain'] == "": + i_data['chain'] = "INPUT" + + return ip_data + except Exception as e: + return [] + + # 2024/3/26 下午 11:58 从数据库中获取端口转发规则列表 + def get_forward_db(self, get): + ''' + @name 从数据库中获取端口转发规则列表 + @author wzz <2024/3/26 下午 11:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + where = '1=1' + sql = public.M('firewall_forward') + + if hasattr(get, 'query'): + where = " S_Address like '%{search}%' or S_Port like '%{search}%' or T_Address like '%{search}%' or T_Port like '%{search}%'".format( + search=get.query + ) + res = sql.where(where, ()).select() + if type(res) != list: + return [] + return res + except Exception as e: + return [] + + # 2024/3/26 下午 10:46 构造端口规则返回数据 + def structure_port_return_data(self, list_port, rule_db, query): + ''' + @name 构造返回数据 + @author wzz <2024/3/26 下午 10:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + new_list = [] + for j in range(len(list_port)): + list_port[j]['id'] = 0 + list_port[j]['sid'] = 0 + list_port[j]['brief'] = "" + list_port[j]['domain'] = "" + if (list_port[j]['Port'].find(":") != -1 or list_port[j]['Port'].find("-") != -1 or + list_port[j]['Port'].find("/") != -1 or list_port[j]['Port'].find(".") != -1): + list_port[j]['status'] = 1 + else: + try: + if not ":" in list_port[j]['Port'] or not "-" in list_port[j]['Port']: + list_port[j]['status'] = self.CheckPort(int(list_port[j]['Port']), list_port[j]['Protocol']) + else: + list_port[j]['status'] = -1 + except: + list_port[j]['status'] = -1 + + if "Chain" in list_port[j] and list_port[j]['Chain'] == "OUTPUT": + list_port[j]['status'] = -1 + + list_port[j]['addtime'] = "0000-00-00 00:00:00" + + list_port[j]['Port'] = list_port[j]['Port'].replace(":", "-") + for i in range(len(rule_db)): + if (rule_db[i]['ports'] == list_port[j]['Port'] and + rule_db[i]['protocol'] == list_port[j]['Protocol'] and + rule_db[i]['address'].lower() == list_port[j]['Address'].lower() and + rule_db[i]['types'] == list_port[j]['Strategy'] and + rule_db[i]['chain'] == list_port[j]['Chain']): + list_port[j]['id'] = rule_db[i]['id'] + list_port[j]['sid'] = rule_db[i]['sid'] + list_port[j]['brief'] = rule_db[i]['brief'] + list_port[j]['addtime'] = rule_db[i]['addtime'] + + if "domain" in rule_db[i]: + list_port[j]['domain'] = rule_db[i]['domain'] + + break + + if query != "": + if query in list_port[j]['Port'] or query in list_port[j]['brief'] or query in list_port[j]['Address']: + new_list.append(list_port[j]) + + if len(new_list) > 0 or query != "": + return sorted(new_list, key=lambda x: x['addtime'], reverse=True) + + return sorted(list_port, key=lambda x: x['addtime'], reverse=True) + + # 2024/3/27 上午 12:01 构造ip规则返回数据 + def structure_ip_return_data(self, list_ip, rule_db, query): + ''' + @name 构造ip规则返回数据 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + new_list = [] + for j in range(len(list_ip)): + list_ip[j]['id'] = 0 + list_ip[j]['sid'] = 0 + list_ip[j]['brief'] = "" + list_ip[j]['domain'] = "" + list_ip[j]['addtime'] = "0000-00-00 00:00:00" + + for i in range(len(rule_db)): + if (rule_db[i]['address'] == list_ip[j]['Address'] and + rule_db[i]['types'] == list_ip[j]['Strategy'] and + rule_db[i]['chain'] == list_ip[j]['Chain']): + list_ip[j]['id'] = rule_db[i]['id'] + list_ip[j]['sid'] = rule_db[i]['sid'] + list_ip[j]['brief'] = rule_db[i]['brief'] + list_ip[j]['addtime'] = rule_db[i]['addtime'] + + if "domain" in rule_db[i]: + list_ip[j]['domain'] = rule_db[i]['domain'] + + break + if query != "": + if query in list_ip[j]['brief'] or query in list_ip[j]['Address']: + new_list.append(list_ip[j]) + + if len(new_list) > 0 or query != "": + return public.return_area(sorted(new_list, key=lambda x: x['addtime'], reverse=True), "Address") + + return public.return_area(sorted(list_ip, key=lambda x: x['addtime'], reverse=True), "Address") + + # 2024/3/27 上午 12:11 构造端口转发规则返回数据 + def structure_forward_return_data(self, list_forward, rule_db, query): + ''' + @name 构造端口转发规则返回数据 + @author wzz <2024/3/27 上午 12:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + new_list = [] + for j in range(len(list_forward)): + list_forward[j]['id'] = 0 + list_forward[j]['brief'] = "" + list_forward[j]['addtime'] = "0000-00-00 00:00:00" + + for i in range(len(rule_db)): + if (rule_db[i]['T_Address'] == list_forward[j]['T_Address'] and + rule_db[i]['S_Port'] == list_forward[j]['S_Port'] and + rule_db[i]['T_Port'] == list_forward[j]['T_Port']): + list_forward[j]['id'] = rule_db[i]['id'] + list_forward[j]['brief'] = rule_db[i]['brief'] + list_forward[j]['addtime'] = rule_db[i]['addtime'] + break + + if query != "": + if (query in list_forward[j]['brief'] or query in list_forward[j]['S_Address'] or query in + list_forward[j]['S_Port'] or + query in list_forward[j]['T_Address'] or query in list_forward[j]['T_Port']): + new_list.append(list_forward[j]) + + if len(new_list) > 0 or query != "": + return sorted(new_list, key=lambda x: x['addtime'], reverse=True) + + return sorted(list_forward, key=lambda x: x['addtime'], reverse=True) + + # 2024/3/25 上午 11:05 获取所有端口规则列表 + def port_rules_list(self, get): + ''' + @name 获取所有端口规则列表 + @author wzz <2024/3/25 上午 11:06> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + get.chain = get.get('chain/s', 'ALL') + get.query = get.get('query/s', '') + + rule_db = self.get_port_db(get) + + if get.chain == "INPUT": + list_port = self.firewall.list_input_port() + elif get.chain == "OUTPUT": + list_port = self.firewall.list_output_port() + else: + list_port = self.firewall.list_port() + + return self.structure_port_return_data(list_port, rule_db, query=get.query) + + # 2024/3/26 下午 3:17 导出规则 + def export_rules(self, get): + ''' + @name 导出规则 + @author wzz <2024/3/26 下午 3:17> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.rule = get.get('rule/s', 'port') + if get.rule == "port": + return self.export_port_rules(get) + elif get.rule == "ip": + return self.export_ip_rules(get) + else: + return self.export_port_forward(get) + + # 2024/3/26 下午 3:18 导入规则 + def import_rules(self, get): + ''' + @name 导入规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置导入规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再导入规则!') + + get.rule = get.get('rule/s', 'port') + if get.rule == "port": + return self.import_port_rules(get) + elif get.rule == "ip": + return self.import_ip_rules(get) + else: + return self.import_port_forward(get) + + # 2024/3/26 下午 2:38 导出所有端口规则 + def export_port_rules(self, get): + ''' + @name 导出所有端口规则 + @author wzz <2024/3/26 下午 2:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.chain = get.get('chain/s', 'all') + + if get.chain == "INPUT": + file_name = "input_port_rules_{}".format(int(time.time())) + elif get.chain == "OUTPUT": + file_name = "output_port_rules_{}".format(int(time.time())) + else: + file_name = "port_rules_{}".format(int(time.time())) + + data = self.port_rules_list(get) + if not data: + return public.returnMsg(False, '没有规则无法导出') + if not os.path.exists(self.config_path): + os.makedirs(self.config_path, exist_ok=True) + file_path = "{}/{}.json".format(self.config_path, file_name) + + public.writeFile(file_path, public.GetJson(data)) + public.WriteLog("系统防火墙", "导出端口规则") + return public.returnMsg(True, file_path) + + # 2024/3/26 下午 2:41 导入端口规则 + def import_port_rules(self, get): + ''' + @name 导入端口规则 + @author wzz <2024/3/26 下午 2:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.file = get.get('file/s', '') + + if not get.file: + return public.returnMsg(False, '文件不能为空') + + if not os.path.exists(get.file): + return public.returnMsg(False, '文件不存在') + + try: + data = public.readFile(get.file) + if "|" in data and not "{" in data: + get.rule_name = "port_rule" + return self.import_rules_old(get) + + data = json.loads(data) + # 2024/4/10 下午2:51 反转数据 + data.reverse() + except: + return public.returnMsg(False, '文件内容异常或格式错误') + + args = public.dict_obj() + for item in data: + args.operation = 'add' + args.protocol = item['Protocol'] + args.port = item['Port'] + args.strategy = item['Strategy'] + args.chain = item['Chain'] + args.address = item.get('Address', 'all') + args.brief = item.get('brief', '') + args.reload = "0" + + self.set_port_rule(args) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '导入成功') + + # 2024/5/14 上午10:27 调用旧的导入规则方法 + def import_rules_old(self, get): + ''' + @name 调用旧的导入规则方法 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + from safeModel.firewallModel import main as firewall + firewall_obj = firewall() + get.file_name = get.file.split("/")[-1] + firewall_obj.import_rules(get) + + return public.returnMsg(True, '导入成功') + except Exception as e: + return public.returnMsg(False, str(e)) + + # 2024/3/26 上午 9:30 处理多个ip以换行的方式添加/删除 + def set_nline_port_ip(self, get): + ''' + @name 处理多个ip以换行的方式添加/删除 + @author wzz <2024/3/26 上午 9:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split("\n") + failed_list = [] + for addr in address: + if not public.checkIp(addr): + return public.returnMsg(False, '目标地址格式错误') + + get.address = addr + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + # if self._isFirewalld: + # self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:30 处理多个ip以逗号的方式添加/删除 + def set_tline_port_ip(self, get): + ''' + @name 处理多个ip以逗号的方式添加 + @author wzz <2024/3/26 上午 9:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split(",") + failed_list = [] + for addr in address: + if not public.checkIp(addr): + return public.returnMsg(False, '目标地址格式错误') + + get.address = addr + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + # if self._isFirewalld: + # self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:34 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + def set_range_port_ip(self, get): + ''' + @name 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + @author wzz <2024/3/26 上午 9:35> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = self.firewall.handle_ip_range(get.address) + failed_list = [] + for addr in address: + get.address = addr + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + # if self._isFirewalld: + # self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/25 下午 6:27 设置端口规则 + def set_port_rule(self, get): + ''' + @name 设置端口规则 + @author wzz <2024/3/25 下午 6:28> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置端口规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.address = get.get('address/s', 'all') + get.port = get.get('port/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + get.reload = get.get('reload/s', "1") + get.brief = get.get('brief/s', '') + + if get.address == "Anywhere" or get.address == "": + get.address = "all" + + if get.protocol == "all": + get.protocol = "tcp/udp" + + if get.port == "": + return public.returnMsg(False, '目标端口不能为空') + + if get.address != "all" and "," in get.address: + import copy + args = copy.deepcopy(get) + address_list = get.address.split(",") + for address in address_list: + args.address = address + result = self.more_prot_rule(args) + if not result['status']: + return result + if get.address != "all" and "\n" in get.address: + import copy + args = copy.deepcopy(get) + address_list = get.address.split("\n") + for address in address_list: + args.address = address + result = self.more_prot_rule(args) + if not result['status']: + return result + else: + result = self.more_prot_rule(get) + if not result['status']: + return result + + if self._isFirewalld and get.reload == "1": + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/29 下午 4:04 处理多个ip的端口规则情况 + def more_prot_rule(self, get): + ''' + @name 处理多个ip的端口规则情况 + @author wzz <2024/3/29 下午 4:02> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.port.find(",") != -1: + import copy + args = copy.deepcopy(get) + port_list = get.port.split(",") + for port in port_list: + args.port = port + result = self.exec_port_rule(args) + if not result['status']: + return result + return public.returnMsg(True, '设置成功') + else: + return self.exec_port_rule(get) + + # 2024/3/28 下午 6:29 执行端口设置 + def exec_port_rule(self, get): + ''' + @name 执行端口设置 + @author wzz <2024/3/28 下午 6:29> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add" and not self.check_port_exist(get): + return public.returnMsg(False, '端口{}已存在,请勿重复添加'.format(get.port)) + + self.set_port_db(get) + + # 2024/3/25 下午 8:23 处理多个ip的情况,例如出现每行一个ip + if get.address != "all" and "\n" in get.address: + return self.set_nline_port_ip(get) + elif get.address != "all" and "-" in get.address: + return self.set_range_port_ip(get) + elif get.address != "all" and "," in get.address: + return self.set_tline_port_ip(get) + elif get.address != "all" and "/" in get.address: + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + elif get.address != "all" and not public.checkIp(get.address) and not public.is_ipv6(get.address): + return public.returnMsg(False, '指定IP地址格式错误') + else: + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + return result + + # 2024/5/14 上午10:40 前置检测ip是否合法 + def check_ips(self, get): + ''' + @name 前置检测ip是否合法 + @author wzz <2024/5/14 上午10:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.address != "all" and "\n" in get.address: + address = get.address.split("\n") + for addr in address: + if addr != "all" and not public.checkIp(addr) and not public.is_ipv6(addr): + return public.returnMsg(False, '指定IP地址格式错误') + elif get.address != "all" and "," in get.address: + address = get.address.split(",") + for addr in address: + if addr != "all" and not public.checkIp(addr) and not public.is_ipv6(addr): + return public.returnMsg(False, '指定IP地址格式错误') + else: + if get.address != "all" and not public.checkIp(get.address) and not public.is_ipv6(get.address): + return public.returnMsg(False, '指定IP地址格式错误') + + return public.returnMsg(True, 'ok') + + # 2024/3/27 上午 9:37 修改端口规则 + def modify_port_rule(self, get): + ''' + @name 修改端口规则 + @author wzz <2024/3/27 上午 9:38> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + if "address" in get.new_data: + get.address = get.new_data['address'] + if not self.check_ips(get)["status"]: + return public.returnMsg(False, '修改后的指定IP地址格式错误') + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.port = get.old_data['Port'] + args1.protocol = get.old_data['Protocol'] + args1.address = get.old_data['Address'] + args1.strategy = get.old_data['Strategy'] + args1.chain = get.old_data['Chain'] + args1.id = get.old_data['id'] + args1.sid = get.old_data['sid'] + args1.reload = "0" + self.set_port_rule(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + args2.port = get.new_data['port'] + args2.protocol = get.new_data['protocol'] + args2.address = get.new_data['address'] if "address" in get.new_data else "all" + args2.strategy = get.new_data['strategy'] + args2.chain = get.new_data['chain'] + args2.brief = get.new_data['brief'] if 'brief' in get.new_data else "" + args2.reload = "1" + self.set_port_rule(args2) + + return public.returnMsg(True, '修改成功') + + # 2024/3/27 下午 4:03 修改域名端口规则 + def modify_domain_port_rule(self, get): + ''' + @name 修改域名端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + address = self.get_a_ip(get.new_data['domain']) + + if address == "": + return public.returnMsg(False, '域名: 【{}】解析失败'.format(get.domain)) + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.port = get.old_data['Port'] + args1.protocol = get.old_data['Protocol'] + args1.address = get.old_data['Address'] + args1.strategy = get.old_data['Strategy'] + args1.chain = get.old_data['Chain'] + args1.id = get.old_data['id'] + args1.sid = get.old_data['sid'] + self.set_port_rule(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + args2.port = get.new_data['port'] + args2.protocol = get.new_data['protocol'] + args2.address = address + args2.strategy = get.new_data['strategy'] + args2.chain = get.new_data['chain'] + args2.brief = get.new_data['brief'] if "brief" in get.new_data else "" + args2.domain = get.new_data['domain'] + self.set_port_rule(args2) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '修改成功') + + # 2024/3/26 下午 5:10 设置域名端口规则 + def set_domain_port_rule(self, get): + ''' + @name 设置域名端口规则 + @author wzz <2024/3/26 下午 5:11> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.domain = get.get('domain/s', '') + get.port = get.get('port/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + + if get.domain == "": + return public.returnMsg(False, '目标域名不能为空') + + if get.port == "": + return public.returnMsg(False, '目标端口不能为空') + + if not public.is_domain(get.domain): + return public.returnMsg(False, '目标域名格式错误') + + if "|" in get.domain: + get.domain = get.domain.split("|")[0] + + address = self.get_a_ip(get.domain) + + if address == "": + return public.returnMsg(False, '域名: 【{}】解析失败'.format(get.domain)) + + get.address = address + self.set_port_db(get) + + if get.chain == "INPUT": + result = self.input_port(get) + else: + result = self.output_port(get) + + if result['status'] and self._isFirewalld: + self.firewall.reload() + + return result + + # 2024/5/13 下午4:46 添加端口规则到指定数据库 + def add_port_db(self, get, protocol, addtime, domain): + ''' + @name 添加端口规则到指定数据库 + @author wzz <2024/5/13 下午4:46> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + add_sid = public.M('firewall_new').add( + 'ports,brief,protocol,address,types,addtime,domain,sid,chain', + ( + get.port, public.xsssec(get.brief), protocol, get.address, get.strategy, addtime, domain, 0, + get.chain) + ) + + if get.domain != "": + domain_sid = public.M('firewall_domain').add( + 'types,domain,port,address,brief,addtime,sid,protocol,domain_total', + (get.strategy, domain, get.port, get.address, public.xsssec(get.brief), + addtime, add_sid, get.protocol, get.domain) + ) + public.M('firewall_new').where("id=?", (add_sid,)).save('sid', domain_sid) + self.check_resolve_crontab() + + # 2024/5/13 下午4:49 从指定数据库删除端口规则 + def remove_port_db(self, get, protocol, addtime, domain): + ''' + @name 从指定数据库删除端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + public.M('firewall_new').where("ports=? and protocol=? and address=? and types=? and chain=?", ( + get.port, protocol, get.address, get.strategy, get.chain + )).delete() + get.domain = get.get('domain/s', '') + if get.domain != "": + public.M('firewall_domain').where("domain=?", (get.domain)).delete() + + self.remove_resolve_crontab() + + # 2024/3/26 下午 5:52 添加/删除数据库的端口规则 + def set_port_db(self, get): + ''' + @name 添加/删除数据库的端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add": + # 检测端口是否已经添加过 + query_result = public.M('firewall_new').where( + 'ports=? and address=? and protocol=? and types=? and chain=?', + (get.port, get.address, get.protocol, get.strategy, get.chain) + ).find() + + if not query_result: + get.domain = get.get('domain/s', '') + domain = "{}|{}".format(get.domain, get.address) if get.domain != "" else "" + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + + if get.protocol == "tcp/udp" and self._isFirewalld: + self.add_port_db(get, "tcp", addtime, domain) + self.add_port_db(get, "udp", addtime, domain) + else: + self.add_port_db(get, get.protocol, addtime, domain) + else: + query_result = public.M('firewall_new').where( + 'ports=? and address=? and protocol=? and types=? and chain=?', + (get.port, get.address, get.protocol, get.strategy, get.chain) + ).find() + if query_result: + if get.protocol == "tcp/udp" and self._isFirewalld: + self.remove_port_db(get, "tcp", query_result['addtime'], query_result['domain']) + self.remove_port_db(get, "udp", query_result['addtime'], query_result['domain']) + else: + self.remove_port_db(get, get.protocol, query_result['addtime'], query_result['domain']) + + # 2024/3/26 下午 11:23 添加/删除数据库的ip规则 + def set_ip_db(self, get): + ''' + @name 添加/删除数据库的ip规则 + @author wzz <2024/3/26 下午 11:24> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add": + get.domain = get.get('domain/s', '') + domain = "{}|{}".format(get.domain, get.address) if get.domain != "" else "" + query_result = public.M('firewall_ip').where("address=? and types=? and domain=? and chain=?", + (get.address, get.strategy, domain, get.chain)).find() + + if not query_result: + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + self._add_sid = public.M('firewall_ip').add( + 'address,types,brief,addtime,domain,sid,chain', + (get.address, get.strategy, public.xsssec(get.brief), addtime, domain, 0, get.chain) + ) + + if get.domain != "": + domain_sid = public.M('firewall_domain').add( + 'types,domain,port,address,brief,addtime,sid,protocol,domain_total', + (get.strategy, domain, '', get.address, public.xsssec(get.brief), addtime, self._add_sid, '', + get.domain) + ) + public.M('firewall_ip').where("id=?", (self._add_sid,)).save('sid', domain_sid) + self.check_resolve_crontab() + else: + get.address = get.get("address/s", '') + get.strategy = get.get("strategy/s", '') + if get.address == "": + return public.returnMsg(False, '请传入id') + public.M('firewall_ip').where("address=? and types=? and chain=?", (get.address, get.strategy, get.chain)).delete() + get.domain = get.get('domain/s', '') + if get.domain != "": + public.M('firewall_domain').where("domain=?", (get.domain)).delete() + + self.remove_resolve_crontab() + + # 2024/3/26 下午 11:40 添加/删除数据库的端口转发规则 + def set_forward_db(self, get): + ''' + @name 添加/删除数据库的端口转发规则 + @author wzz <2024/3/26 下午 11:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add": + query_result = public.M('firewall_forward').where("S_Port=? and T_Address=? and T_Port=? and Protocol=?", ( + get.S_Port, get.T_Address, get.T_Port, get.protocol + )).find() + + if not query_result: + get.brief = get.get('brief/s', '') + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + self._add_sid = public.M('firewall_forward').add( + 'S_Port,T_Address,T_Port,Protocol,Family,addtime,brief', + (get.S_Port, get.T_Address, get.T_Port, get.protocol, "ipv4", addtime, get.brief) + ) + else: + public.M('firewall_forward').where( + "S_Port=? and T_Address=? and T_Port=? and Protocol=?", + (get.S_Port, get.T_Address, get.T_Port, get.protocol) + ).delete() + + # 2024/3/25 下午 6:55 入站端口规则 + def input_port(self, get): + ''' + @name 入站端口规则 + @param "data":{"参数名":""} <数据类型> 参数描述 dabao + @return list[dict{}...] + ''' + info = { + "Port": get.port, + "Protocol": get.protocol.lower(), + "Strategy": get.strategy.lower(), + "Family": "ipv4", + } + + if ":" in get.address: + info["Family"] = "ipv6" + + if get.address != "all": + info["Address"] = get.address + result = self.firewall.rich_rules(info=info, operation=get.operation) + elif get.address == "all" and get.strategy == "drop": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.input_port(info=info, operation=get.operation) + + return result + + # 2024/3/25 下午 6:56 出站端口规则 + def output_port(self, get): + ''' + @name 出站端口规则 + @author wzz <2024/3/25 下午 6:56> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + info = { + "Port": get.port, + "Protocol": get.protocol.lower(), + "Strategy": get.strategy.lower(), + "Priority": "0", + "Family": "ipv4", + } + + if ":" in get.address: + info["Family"] = "ipv6" + + if get.address != "all": + info["Address"] = get.address + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_port(info=info, operation=get.operation) + + return result + + # 2024/3/26 上午 9:40 处理多个ip的情况,例如出现每行一个ip + def set_nline_ip_rule(self, get): + ''' + @name 处理多个ip的情况,例如出现每行一个ip + @author wzz <2024/3/26 上午 9:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split("\n") + failed_list = [] + import copy + for addr in address: + if not public.is_ipv4(addr) and not public.is_ipv6(addr): + continue + + args = copy.deepcopy(get) + args.address = addr + args.family = "ipv4" if ":" not in addr else "ipv6" + + if self.check_is_user_ip(args): + continue + + if get.operation == "add" and not self.check_ip_exist(args): + continue + + self.set_ip_db(args) + + info = { + "Address": args.address, + "Family": args.family, + "Strategy": args.strategy, + "Priority": args.priority, + } + + if args.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=args.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=args.operation) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:40 处理多个ip的情况,例如出现逗号隔开ip + def set_tline_ip_rule(self, get): + ''' + @name 处理多个ip的情况,例如出现逗号隔开ip + @author wzz <2024/3/26 上午 9:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = get.address.split(",") + failed_list = [] + import copy + for addr in address: + if not public.checkIp(addr): + return public.returnMsg(False, '目标地址格式错误') + + args = copy.deepcopy(get) + args.address = addr + + if self.check_is_user_ip(args): + continue + + if get.operation == "add" and not self.check_ip_exist(args): + continue + + self.set_ip_db(args) + + info = { + "Address": args.address, + "Family": args.family, + "Strategy": args.strategy, + "Priority": args.priority, + } + + if args.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=args.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=args.operation) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:43 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + def set_range_ip_rule(self, get): + ''' + @name 处理192.168.1.10-192.168.1.20这种范围ip的添加/删除 + @author wzz <2024/3/26 上午 9:43> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + address = self.firewall.handle_ip_range(get.address) + failed_list = [] + import copy + for addr in address: + args = copy.deepcopy(get) + args.address = addr + + if self.check_is_user_ip(args): + continue + + if get.operation == "add" and not self.check_ip_exist(args): + continue + + self.set_ip_db(args) + + info = { + "Address": args.address, + "Family": args.family, + "Strategy": args.strategy, + "Priority": args.priority, + } + + if args.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=args.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=args.operation) + + if not result['status']: + failed_list.append({ + "address": addr, + "msg": result['msg'] + }) + if len(failed_list) > 0: + return public.returnMsg(True, '设置成功,以下规则设置失败:{}'.format(failed_list)) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '设置成功') + + # 2024/3/26 上午 9:46 设置带掩码的ip段 + def set_mask_ip_rule(self, get): + ''' + @name 设置带掩码的ip段 + @author wzz <2024/3/26 上午 9:47> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.operation == "add" and not self.check_ip_exist(get): + return public.returnMsg(False, '目标地址{}已存在,请勿重复添加'.format(get.address)) + + self.set_ip_db(get) + info = { + "Address": get.address, + "Family": get.family, + "Strategy": get.strategy, + "Priority": get.priority, + } + + if get.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld: + self.firewall.reload() + + return result + + # 2024/4/9 下午11:31 检查是否会自己的ip,如果是则返回 + def check_is_user_ip(self, get): + ''' + @name + @author wzz <2024/4/9 下午11:31> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/3/26 下午 11:27 处理用户当前的远程ip,如果添加的ip与此ip一直则返回 + try: + from flask import request + user_ip = request.remote_addr + if user_ip in get.address: + return True + return False + except: + return False + + # 2024/3/25 下午 7:16 设置ip规则 + def set_ip_rule(self, get): + ''' + @name 设置ip规则 + @author wzz <2024/3/25 下午 7:16> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.address = get.get('address/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + get.family = get.get('family/s', 'ipv4') + get.priority = get.get('priority/s', '0') + get.reload = get.get('reload/s', "1") + + if get.address == "": + return public.returnMsg(False, '目标ip不能为空') + + # 2024/3/25 下午 8:23 处理多个ip的情况,例如出现每行一个ip + if get.address != "all" and "\n" in get.address: + return self.set_nline_ip_rule(get) + elif get.address != "all" and "-" in get.address: + return self.set_range_ip_rule(get) + elif get.address != "all" and "/" in get.address: + return self.set_mask_ip_rule(get) + elif get.address != "all" and "," in get.address: + return self.set_tline_ip_rule(get) + elif get.address != "all" and not public.checkIp(get.address) and not public.is_ipv6(get.address): + return public.returnMsg(False, '目标地址格式错误') + else: + if get.operation == "add" and get.strategy == "drop": + if self.check_is_user_ip(get): + return public.returnMsg(False, '不能添加自己的ip') + + if get.operation == "add" and not self.check_ip_exist(get): + return public.returnMsg(False, '目标地址{}已存在,请勿重复添加'.format(get.address)) + + self.set_ip_db(get) + + info = { + "Address": get.address, + "Family": get.family, + "Strategy": get.strategy.lower(), + "Priority": get.priority.lower(), + } + + if get.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld and get.reload == "1": + self.firewall.reload() + + return result + + # 2024/3/27 上午 9:44 修改ip规则 + def modify_ip_rule(self, get): + ''' + @name 修改ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.address = get.old_data['Address'] + args1.strategy = get.old_data['Strategy'] + args1.family = get.old_data['Family'] + args1.chain = get.old_data['Chain'] + args1.id = get.old_data['id'] + args1.sid = get.old_data['sid'] + args1.reload = "0" + self.set_ip_rule(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + args2.address = get.new_data['address'] + args2.strategy = get.new_data['strategy'] + args2.family = get.new_data['family'] + args2.chain = get.new_data['chain'] + args2.brief = get.new_data['brief'] + args2.reload = "0" + self.set_ip_rule(args2) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '修改成功') + + # 2024/3/26 下午 5:18 设置域名ip规则 + def set_domain_ip_rule(self, get): + ''' + @name 设置域名ip规则 + @author wzz <2024/3/26 下午 5:19> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.domain = get.get('domain/s', '') + get.port = get.get('port/s', '') + get.strategy = get.get('strategy/s', 'accept') + get.chain = get.get('chain/s', 'INPUT') + + if get.domain == "": + return public.returnMsg(False, '目标域名不能为空') + + if get.port == "": + return public.returnMsg(False, '目标端口不能为空') + + if not public.is_domain(get.domain): + return public.returnMsg(False, '目标域名格式错误') + + address = self.get_a_ip(get.domain) + + if address == "": + return public.returnMsg(False, '域名: 【{}】解析失败'.format(get.domain)) + + if not public.checkIp(get.address): + return public.returnMsg(False, '目标地址格式错误') + + info = { + "Address": address, + "Family": get.family, + "Strategy": get.strategy.lower(), + "Priority": get.priority.lower(), + } + if get.chain == "INPUT": + result = self.firewall.rich_rules(info=info, operation=get.operation) + else: + result = self.firewall.output_rich_rules(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld: + self.firewall.reload() + + return result + + # 2024/3/25 上午 11:18 获取所有ip规则列表 + def ip_rules_list(self, get): + ''' + @name 获取所有ip规则列表 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + get.chain = get.get('chain/s', 'all') + get.query = get.get('query/s', '') + ip_db = self.get_ip_db(get) + + if get.chain == "INPUT": + list_address = self.firewall.list_input_address() + elif get.chain == "OUTPUT": + list_address = self.firewall.list_output_address() + else: + list_address = self.firewall.list_address() + + return self.structure_ip_return_data(list_address, ip_db, query=get.query) + + # 2024/3/26 下午 3:03 导出所有ip规则 + def export_ip_rules(self, get): + ''' + @name 导出所有ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.chain = get.get('chain/s', 'all') + + if get.chain == "INPUT": + file_name = "input_ip_rules_{}".format(int(time.time())) + elif get.chain == "OUTPUT": + file_name = "output_ip_rules_{}".format(int(time.time())) + else: + file_name = "ip_rules_{}".format(int(time.time())) + + data = self.ip_rules_list(get) + if not data: + return public.returnMsg(False, '没有规则无法导出') + + file_path = "{}/{}.json".format(self.config_path, file_name) + + public.writeFile(file_path, public.GetJson(data)) + public.WriteLog("系统防火墙", "导出ip规则") + return public.returnMsg(True, file_path) + + # 2024/3/26 下午 3:05 导入ip规则 + def import_ip_rules(self, get): + ''' + @name 导入ip规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.file = get.get('file/s', '') + + if not get.file: + return public.returnMsg(False, '文件不能为空') + + if not os.path.exists(get.file): + return public.returnMsg(False, '文件不存在') + + try: + data = public.readFile(get.file) + if "|" in data and not "{" in data: + get.rule_name = "ip_rule" + return self.import_rules_old(get) + + data = json.loads(data) + # 2024/4/10 下午2:51 反转数据 + data.reverse() + except: + return public.returnMsg(False, '文件内容异常或格式错误') + + args = public.dict_obj() + for item in data: + args.operation = 'add' + args.address = item['Address'] + args.strategy = item['Strategy'] + args.chain = item['Chain'] + args.family = item['Family'] + args.brief = item['brief'] + args.reload = "0" + + self.set_ip_rule(args) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '导入成功') + + # 2024/3/25 下午 2:34 获取端口转发列表 + def port_forward_list(self, get): + ''' + @name 获取端口转发列表 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return list[dict{}...] + ''' + get.query = get.get('query/s', '') + list_port_forward = self.firewall.list_port_forward() + forward_db = self.get_forward_db(get) + if type(list_port_forward) == list and type(forward_db) == list: + return self.structure_forward_return_data(list_port_forward, forward_db, query=get.query) + return [] + + # 2024/3/26 下午 3:08 导出所有端口转发规则 + def export_port_forward(self, get): + ''' + @name 导出所有端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + data = self.firewall.list_port_forward() + if not data: + return public.returnMsg(False, '没有规则无法导出') + file_name = "port_forward_{}".format(int(time.time())) + file_path = "{}/{}.json".format(self.config_path, file_name) + + public.writeFile(file_path, public.GetJson(data)) + public.WriteLog("系统防火墙", "导出端口转发规则") + return public.returnMsg(True, file_path) + + # 2024/3/26 下午 3:10 导入端口转发规则 + def import_port_forward(self, get): + ''' + @name 导入端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.file = get.get('file/s', '') + + if not get.file: + return public.returnMsg(False, '文件不能为空') + + if not os.path.exists(get.file): + return public.returnMsg(False, '文件不存在') + + try: + data = public.readFile(get.file) + if "|" in data and not "{" in data: + get.rule_name = "trans_rule" + return self.import_rules_old(get) + + data = json.loads(data) + # 2024/4/10 下午2:51 反转数据 + data.reverse() + except: + return public.returnMsg(False, '文件内容异常或格式错误') + + args = public.dict_obj() + for item in data: + args.operation = 'add' + args.protocol = item['Protocol'] + args.S_Address = item['S_Address'] + args.S_Port = item['S_Port'] + args.T_Address = item['T_Address'] + args.T_Port = item['T_Port'] + args.reload = "0" + + self.set_port_forward(args) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '导入成功') + + # 2024/3/25 下午 3:43 设置端口转发 + def set_port_forward(self, get): + ''' + @name 设置端口转发 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.operation = get.get('operation/s', 'add') + get.protocol = get.get('protocol/s', 'tcp') + get.S_Address = get.get('S_Address/s', '') + get.S_Port = get.get('S_Port/s', '') + get.T_Address = get.get("T_Address/s", '') + get.T_Port = get.get("T_Port/s", '') + get.reload = get.get('reload/s', "1") + + if get.S_Port == "": + return public.returnMsg(False, '源端口不能为空') + # if get.T_Address == "": + # return public.returnMsg(False, '目标地址不能为空') + if get.T_Port == "": + return public.returnMsg(False, '目标端口不能为空') + + # 2024/3/25 下午 5:49 前置检测 + if get.operation == "add": + check_ip_forward = self.firewall.check_ip_forward() + if not check_ip_forward["status"]: + return check_ip_forward + + if not self.check_forward_exist(get): + return public.returnMsg(False, '端口转发规则已存在,请勿重复添加') + + self.set_forward_db(get) + + # 2024/3/25 下午 5:50 构造传参,调用底层方法设置端口转发 + if get.protocol == "tcp/udp" or get.protocol == "all": + info = { + "Family": "ipv4", + "Protocol": "tcp", + "S_Address": get.S_Address, + "S_Port": get.S_Port, + "T_Address": get.T_Address, + "T_Port": get.T_Port, + } + result = self.firewall.port_forward(info=info, operation=get.operation) + if not result['status']: + return result + + info = { + "Family": "ipv4", + "Protocol": "udp", + "S_Address": get.S_Address, + "S_Port": get.S_Port, + "T_Address": get.T_Address, + "T_Port": get.T_Port, + } + result = self.firewall.port_forward(info=info, operation=get.operation) + + if result['status'] and self._isFirewalld and get.reload == "1": + self.firewall.reload() + else: + info = { + "Family": "ipv4", + "Protocol": get.protocol, + "S_Address": get.S_Address, + "S_Port": get.S_Port, + "T_Address": get.T_Address, + "T_Port": get.T_Port, + } + result = self.firewall.port_forward(info=info, operation=get.operation) + + # 2024/3/25 下午 5:50 如果设置成功才重载防火墙 + if result['status'] and self._isFirewalld and get.reload == "1": + self.firewall.reload() + + return result + + # 2024/3/27 上午 9:44 修改端口转发规则 + def modify_forward_rule(self, get): + ''' + @name 修改端口转发规则 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 2024/4/17 下午4:47 检查防火墙状态,如果未启动则不允许设置规则 + if not self.get_firewall_status(): + return public.returnMsg(False, '请先启动防火墙后再设置规则!') + + get.old_data = get.get('old_data/s', '') + get.new_data = get.get('new_data/s', '') + + if get.old_data == "": + return public.returnMsg(False, '请传入old_data') + + if get.new_data == "": + return public.returnMsg(False, '请传入new_data') + + get.old_data = json.loads(get.old_data) + get.new_data = json.loads(get.new_data) + + args1 = public.dict_obj() + args1.operation = 'remove' + args1.S_Address = get.old_data['S_Address'] + args1.S_Port = get.old_data['S_Port'] + args1.T_Address = get.old_data['T_Address'] + args1.T_Port = get.old_data['T_Port'] + args1.Protocol = get.old_data['Protocol'] + args1.id = get.old_data['id'] + args1.reload = "0" + self.set_port_forward(args1) + + args2 = public.dict_obj() + args2.operation = 'add' + # args2.S_Address = get.new_data['S_Address'] + args2.S_Port = get.new_data['S_Port'] + args2.T_Address = get.new_data['T_Address'] + args2.T_Port = get.new_data['T_Port'] + args2.Protocol = get.new_data['protocol'] + args2.brief = get.new_data['brief'] + args2.reload = "0" + self.set_port_forward(args2) + + if self._isFirewalld: + self.firewall.reload() + + return public.returnMsg(True, '修改成功') diff --git a/class_v2/firewallModelV2/firewallBase.py b/class_v2/firewallModelV2/firewallBase.py new file mode 100644 index 00000000..3e8d9e83 --- /dev/null +++ b/class_v2/firewallModelV2/firewallBase.py @@ -0,0 +1,220 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2014-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 系统防火墙模型 - 基类 +# ------------------------------ + +import os +import re +from typing import Dict, Union, Any +from xml.etree.ElementTree import ElementTree + +import public + + +class Base(object): + + def __init__(self): + self.config_path = "{}/class/firewallModelV2/config".format(public.get_panel_path()) + self._isUfw = False + self._isFirewalld = False + self._isIptables = False + if os.path.exists('/usr/sbin/ufw'): + self._isUfw = True + from firewallModelV2.app.ufw import Ufw + self.firewall = Ufw() + elif os.path.exists('/usr/sbin/firewalld'): + self._isFirewalld = True + from firewallModelV2.app.firewalld import Firewalld + self.firewall = Firewalld() + elif not self._isUfw and not self._isFirewalld: + self._isIptables = True + from firewallModelv2.app.iptables import Iptables + self.firewall = Iptables() + _months = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', 'Jul': '07', + 'Aug': '08', 'Sep': '09', 'Sept': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'} + + # 2024/3/14 上午 11:27 获取防火墙运行状态 + def get_firewall_status(self) -> bool: + ''' + @name 获取防火墙运行状态 + @author wzz <2024/3/14 上午 11:27> + @param + @return bool True/False + ''' + if self._isUfw: + res = public.ExecShell("systemctl is-active ufw")[0] + if res == "active": return True + res = public.ExecShell("systemctl list-units | grep ufw")[0] + if res.find('active running') != -1: return True + res = public.ExecShell('/lib/ufw/ufw-init status')[0] + if res.find("Firewall is not running") != -1: return False + res = public.ExecShell('ufw status verbose')[0] + if res.find('inactive') != -1: return False + return True + if self._isFirewalld: + res = public.ExecShell("ps -ef|grep firewalld|grep -v grep")[0] + if res: return True + res = public.ExecShell("systemctl is-active firewalld")[0] + if res == "active": return True + res = public.ExecShell("systemctl list-units | grep firewalld")[0] + if res.find('active running') != -1: return True + return False + else: + res = public.ExecShell("/etc/init.d/iptables status")[0] + if res.find('not running') != -1: return False + res = public.ExecShell("systemctl is-active iptables")[0] + if res == "active": return True + return True + + # 2024/3/14 上午 11:30 设置禁ping + def set_ping(self, get) -> dict: + ''' + @name 设置禁ping + @author wzz <2024/3/14 上午 11:31> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.status = get.get("status", "1") + get.status = str(get.status) if str(get.status) in ['0', '1'] else '1' + + filename = '/etc/sysctl.conf' + conf = public.readFile(filename) + if conf.find('net.ipv4.icmp_echo') != -1: + rep = r"net\.ipv4\.icmp_echo.*" + conf = re.sub(rep, 'net.ipv4.icmp_echo_ignore_all=' + get.status + "\n", conf) + else: + conf += "\nnet.ipv4.icmp_echo_ignore_all=" + get.status + "\n" + + if public.writeFile(filename, conf): + public.ExecShell('sysctl -p') + return public.returnMsg(True, 'SUCCESS') + else: + return public.returnMsg( + False, + '错误:设置失败,sysctl.conf不可写!
                                    ' + '1、如果安装了[宝塔系统加固],请先关闭
                                    ' + '2、如果安装了云锁,请关闭[系统加固]功能
                                    ' + '3、如果安装了安全狗,请关闭[系统防护]功能
                                    ' + '4、如果使用了其它安全软件,请先卸载
                                    ' + ) + + # 2024/3/14 上午 11:37 获取网站日志目录的大小 + def get_www_logs_size(self, get) -> Dict[str, Union[str, Any]]: + ''' + @name 获取网站日志目录的大小 + @author wzz <2024/3/14 上午 11:37> + @param + @return dict{"status":True/False,"msg":"提示信息"} + ''' + path_size = public.get_size_total("/www/wwwlogs") + if not path_size: + return {"log_path": "/www/wwwlogs", "size": "0B"} + + return {"log_path": "/www/wwwlogs", "size": public.to_size(path_size["/www/wwwlogs"])} + + # 2024/3/25 上午 10:50 获取防火墙类型,firewall或ufw + def _get_firewall_type(self) -> str: + ''' + @name 获取防火墙类型,firewall或ufw + @return str firewall/ufw + ''' + import os + if os.path.exists('/usr/sbin/ufw'): + return 'ufw' + if os.path.exists('/usr/sbin/firewalld'): + return 'firewall' + return 'iptables' + + # 2024/3/26 下午 5:01 获取指定域名的A记录 + def get_a_ip(self, domain: str) -> str: + ''' + @name 获取指定域名的A记录 + @param domain: 域名 + @return str + ''' + try: + import socket + return socket.gethostbyname(domain) + except Exception as e: + return "" + + # 2024/3/26 下午 5:40 检查是否已添加计划任务,如果没有则添加 + def check_resolve_crontab(self): + ''' + @name 检查是否已添加计划任务 + @author wzz <2024/3/26 下午 5:41> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + python_path = "{}/pyenv/bin/python".format(public.get_panel_path()) + + if not public.M('crontab').where('name=?', ('[勿删]系统防火墙域名解析检测任务',)).count(): + cmd = '{} {}'.format(python_path, '/www/server/panel/script/firewall_domain.py') + args = {"name": "[勿删]系统防火墙域名解析检测任务", "type": 'minute-n', "where1": '5', "hour": '', + "minute": '', "sName": "", + "sType": 'toShell', "notice": '', "notice_channel": '', "save": '', "save_local": '1', + "backupTo": '', "sBody": cmd, + "urladdress": ''} + import crontab + res = crontab.crontab().AddCrontab(args) + if res and "id" in res.keys(): + return True + return False + return True + + # 2024/3/26 下午 11:37 当没有域名解析时,删除域名解析的计划任务 + def remove_resolve_crontab(self): + ''' + @name 当没有域名解析时,删除域名解析的计划任务 + @author wzz <2024/3/26 下午 11:37> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + + if not public.M('firewall_domain').count(): + pdata = public.M('crontab').where('name=?', '[勿删]系统防火墙域名解析检测任务').select() + if pdata: + import crontab + for i in pdata: + args = {"id": i['id']} + crontab.crontab().DelCrontab(args) + + # 2024/3/26 下午 6:22 端口扫描 + def CheckPort(self, port, protocol): + ''' + @name 端口扫描 + @author wzz <2024/3/26 下午 6:22> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + import socket + localIP = '127.0.0.1' + temp = {} + temp['port'] = port + temp['local'] = True + + try: + if 'tcp' in protocol.lower(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + s.connect((localIP, port)) + s.close() + if 'udp' in protocol.lower(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0.01) + s.sendto(b'', (localIP, port)) + s.close() + except: + temp['local'] = False + + result = 0 + if temp['local']: result += 2 + return result diff --git a/class_v2/firewall_new_v2.py b/class_v2/firewall_new_v2.py new file mode 100644 index 00000000..3fe29ef4 --- /dev/null +++ b/class_v2/firewall_new_v2.py @@ -0,0 +1,494 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel x5 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2018 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 1249648969@qq.com +# +------------------------------------------------------------------- +import sys,os,public,re,firewalld,time + +if sys.version_info[0] == 2: + reload(sys) + sys.setdefaultencoding('utf-8') + +class firewalls: + __isFirewalld = False + __isUfw = False + __Obj = None + + + def __init__(self): + if os.path.exists('/usr/sbin/firewalld'): self.__isFirewalld = True + if os.path.exists('/usr/sbin/ufw'): self.__isUfw = True + public.M('firewall').execute("alter table firewall add ports TEXT;",()) + public.M('firewall').execute("alter table firewall add protocol TEXT;",()) + public.M('firewall').execute("alter table firewall add address_ip TEXT;",()) + public.M('firewall').execute("alter table firewall add types TEXT;",()) + #这里判断的是Centos7 的系统 + if self.__isFirewalld: + self.__Obj = firewalld.firewalld(); + # 获取列表信息 + self.GetList(); + + #获取服务端列表 + def GetList(self,get = None): + try: + data = {} + # 获取开放的端口 + data['ports'] = self.__Obj.GetAcceptPortList(); + #当前时间 + #'2018-10-11 14:36:40' + addtime = time.strftime('%Y-%m-%d %X',time.localtime()) + # + for i in range(len(data['ports'])): + # + tmp = self.CheckDbExists(data['ports'][i]['port'],data['ports'][i]['protocol']); + # | id | port | ps | addtime | ports | protocol | address_ip | types | + if not tmp: public.M('firewall').add('port,ps,addtime',(data['ports'][i]['port'],'',addtime)) + + data['iplist'] = self.__Obj.GetDropAddressList(); + + for i in range(len(data['iplist'])): + try: + tmp = self.CheckDbExists(data['iplist'][i]['address']); + if not tmp: public.M('firewall').add('port,ps,addtime',(data['iplist'][i]['address'],'',addtime)) + except: + return public.get_error_info() + + # 添加到firewalls 数据表中 + data['reject']=self.__Obj.GetrejectLIST() + + + for i in range(len(data['reject'])): + try: + tmp=self.CheckDbExists2(data['reject'][i]['protocol'], + data['reject'][i]['type'], + data['reject'][i]['port'], + data['reject'][i]['address']) + if not tmp:public.M('firewall').add('protocol,types,ports,address_ip,addtime', + (data['reject'][i]['protocol'], + data['reject'][i]['type'], + data['reject'][i]['port'], + data['reject'][i]['address'],addtime)) + except: + return public.get_error_info() + # 添加允许信息到firewalls 表中 + data['accept'] = self.__Obj.Getacceptlist() + #return data + for i in range(len(data['accept'])): + try: + tmp = self.CheckDbExists2(data['accept'][i]['protocol'], + data['accept'][i]['type'], + data['accept'][i]['port'], + data['accept'][i]['address']) + if not tmp: public.M('firewall').add('protocol,types,ports,address_ip,addtime', + (data['accept'][i]['protocol'], + data['accept'][i]['type'], + data['accept'][i]['port'], + data['accept'][i]['address'],addtime)) + except: + return public.get_error_info() + count = public.M('firewall').count(); + data = {} + data['page'] = public.get_page(count,int(get.p),12,get.collback) + data['data'] = public.M('firewall').limit(data['page']['shift'] + ',' + data['page']['row']).order('id desc').select() + for i in range(len(data['data'])): + if data['data'][i]['port'].find(':') != -1 or data['data'][i]['port'].find('.') != -1 or data['data'][i]['port'].find('-') != -1: + data['data'][i]['status'] = -1; + else: + data['data'][i]['status'] = public.check_port_stat(int(data['data'][i]['port'])); + + data['page'] = data['page']['page'] + return data + except Exception as ex: + return public.get_error_info() + + #检查数据库是否存在 + def CheckDbExists(self,port,type=None): + data = public.M('firewall').field('id,port,ps,addtime,types').select(); + return data + for dt in data: + if dt['port'] == port and dt['type'] == type: return dt; + return False; + + # 查看frewalls 数据库表中是否存在 + # | id | port | ps | addtime | ports | protocol | address_ip | types | + def CheckDbExists2(self,protocol,type,port,address): + data = public.M('firewall').field('protocol,types,ports,address_ip').select() + for dt in data: + if dt['ports'] == port and dt['protocol']==protocol and dt['types']==type and dt['address_ip']==address: return dt; + return False + + + + #重载防火墙配置 + def FirewallReload(self): + if self.__isUfw: + public.ExecShell('/usr/sbin/ufw reload') + return; + if self.__isFirewalld: + public.ExecShell('firewall-cmd --reload') + else: + public.ExecShell('/etc/init.d/iptables save') + public.ExecShell('/etc/init.d/iptables restart') + + + #添加屏蔽IP + def AddDropAddress(self,get): + import time + import re + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + if not re.search(rep,get.port): return public.return_msg_gettext(False,'IP address youve entered is illegal!'); + address = get.port + if public.M('firewall').where("port=?",(address,)).count() > 0: return public.return_msg_gettext(False,'The IP exists in block list, no need to repeat processing!') + if self.__isUfw: + public.ExecShell('ufw deny from ' + address + ' to any'); + else: + if self.__isFirewalld: + public.ExecShell('firewall-cmd --permanent --add-rich-rule=\'rule family=ipv4 source address="'+ address +'" drop\'') + ret=self.__Obj.CheckIpDrop(address) + if not ret: + self.__Obj.AddDropAddress(address) + + else: + public.ExecShell('iptables -I INPUT -s '+address+' -j DROP') + + public.write_log_gettext("Firewall manager", 'Successfully blocked IP [{}]!',(address,)) + addtime = time.strftime('%Y-%m-%d %X',time.localtime()) + public.M('firewall').add('port,ps,addtime',(address,get.ps,addtime)) + self.FirewallReload() + return public.return_msg_gettext(True,'Successfully added') + + + + + #删除IP屏蔽 + def DelDropAddress(self,get): + address = get.port + id = get.id + if self.__isUfw: + public.ExecShell('ufw delete deny from ' + address + ' to any'); + else: + if self.__isFirewalld: + public.ExecShell('firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv4 source address="'+ address +'" drop\'') + ret=self.__Obj.DelDropAddress(address) + if ret: + pass + else: + public.ExecShell('iptables -D INPUT -s '+address+' -j DROP') + + public.write_log_gettext("Firewall manager",'Unblocked IP [{}]!',(address,)) + public.M('firewall').where("id=?",(id,)).delete() + + self.FirewallReload(); + return public.return_msg_gettext(True,'Successfully deleted') + + + #添加放行端口 + def AddAcceptPort(self,get): + flag=False + import re + rep = r"^\d{1,5}(:\d{1,5})?$" + if not re.search(rep,get.port): return public.return_msg_gettext(False,'Port range is incorrect!'); + import time + port = get.port + ps = get.ps + types=get.type + type_list=['tcp','udp'] + if types not in type_list:return public.return_msg_gettext(False, 'The port exists, no need to repeat the release!') + notudps = ['80', '443', '8888', '888', '39000:40000', '21', '22'] + if port in notudps:flag=True + #return public.M('firewall').where("port=?", (port,)).count() + if types=='tcp': + if flag: + if public.M('firewall').where("port=?", (port,)).count() > 0: return public.return_msg_gettext(False, 'The port exists, no need to repeat the release!') + else: + if public.M('firewall').where("port=? and type='tcp'",(port,)).count() > 0: return public.return_msg_gettext(False,'The port exists, no need to repeat the release!') + elif types=='udp': + if flag: + if public.M('firewall').where("port=?", (port,)).count() > 0: return public.return_msg_gettext( False, 'The port exists, no need to repeat the release!') + else: + if public.M('firewall').where("port=? and type='udp'", (port,)).count() > 0: return public.return_msg_gettext(False,'The port exists, no need to repeat the release!') + else: + return public.return_msg_gettext(False, 'The port exists, no need to repeat the release!') + + if self.__isUfw: + if port in notudps: + public.ExecShell('ufw allow ' + port + '/tcp') + else: + public.ExecShell('ufw allow ' + port + '/'+type+''); + else: + if self.__isFirewalld: + port = port.replace(':','-') + if port in notudps: + public.ExecShell('firewall-cmd --permanent --zone=public --add-port=' + port + '/tcp') + else: + public.ExecShell('firewall-cmd --permanent --zone=public --add-port=' + port + '/' + types +'') + else: + if port in notudps: + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport ' + port + ' -j ACCEPT') + else: + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m ' + types +' --dport ' + port + ' -j ACCEPT' ) + + public.write_log_gettext("Firewall manager", 'Successfully accepted port [{}]!',(port,)) + addtime = time.strftime('%Y-%m-%d %X',time.localtime()) + result = public.M('firewall').add('port,ps,addtime,types',(port,ps,addtime,types)) + #return result + self.FirewallReload() + return public.return_msg_gettext(True,'Setup successfully!') + + + #删除放行端口 + def DelAcceptPort(self,get): + port = get.port + id = get.id + types=get.type + type_list = ['tcp', 'udp'] + if not types in type_list: return public.return_msg_gettext(False, 'The port exists, no need to repeat the release!') + try: + if(port == public.GetHost(True)): return public.return_msg_gettext(False,'Failed,cannot delete current port of the panel!') + if self.__isUfw: + public.ExecShell('ufw delete allow ' + port + '/' + types+ ''); + else: + if self.__isFirewalld: + public.ExecShell('firewall-cmd --permanent --zone=public --remove-port='+port+'/' + types + '') + else: + public.ExecShell('iptables -D INPUT -p tcp -m state --state NEW -m ' + types +' --dport '+port+' -j ACCEPT') + public.write_log_gettext("Firewall manager", 'Successfully deleted accepted port [{}] on firewall!',(port,)) + public.M('firewall').where("id=?",(id,)).delete() + + self.FirewallReload() + return public.return_msg_gettext(True,'Successfully deleted') + except: + return public.return_msg_gettext(False,'Failed to delete') + + + + #设置远程端口状态 + def SetSshStatus(self,get): + version = public.readFile('/etc/redhat-release') + if int(get['status'])==1: + msg = public.get_msg_gettext('SSH service turned off') + act = 'stop' + else: + msg = public.get_msg_gettext('SSH service turned on') + act = 'start' + + if not os.path.exists('/etc/redhat-release'): + public.ExecShell('service ssh ' + act); + elif version.find(' 7.') != -1: + public.ExecShell("systemctl "+act+" sshd.service") + else: + public.ExecShell("/etc/init.d/sshd "+act) + + public.write_log_gettext("Firewall manager", msg) + return public.return_msg_gettext(True,'Setup successfully!') + + + + + #设置ping + def SetPing(self,get): + if get.status == '1': + get.status = '0'; + else: + get.status = '1'; + filename = '/etc/sysctl.conf' + conf = public.readFile(filename) + if conf.find('net.ipv4.icmp_echo') != -1: + rep = r"net\.ipv4\.icmp_echo.*" + conf = re.sub(rep,'net.ipv4.icmp_echo_ignore_all='+get.status,conf) + else: + conf += "\nnet.ipv4.icmp_echo_ignore_all="+get.status + + + public.writeFile(filename,conf) + public.ExecShell('sysctl -p') + return public.return_msg_gettext(True,'Setup successfully!') + + + + #改远程端口 + def SetSshPort(self,get): + #return public.returnMsg(False,'演示服务器,禁止此操作!'); + port = get.port + if int(port) < 22 or int(port) > 65535: return public.return_msg_gettext(False,'Port range must be between 22 and 65535!'); + ports = ['21','25','80','443','8080','888','8888']; + if port in ports: return public.return_msg_gettext(False,''); + + file = '/etc/ssh/sshd_config' + conf = public.readFile(file) + + rep = "#*Port\\s+([0-9]+)\\s*\n" + conf = re.sub(rep, "Port "+port+"\n", conf) + public.writeFile(file,conf) + + if self.__isFirewalld: + self.__Obj.AddAcceptPort(port); + public.ExecShell('setenforce 0'); + public.ExecShell('sed -i "s#SELINUX=enforcing#SELINUX=disabled#" /etc/selinux/config'); + public.ExecShell("systemctl restart sshd.service") + elif self.__isUfw: + public.ExecShell('ufw allow ' + port + '/tcp'); + public.ExecShell("service ssh restart") + else: + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport '+port+' -j ACCEPT') + public.ExecShell("/etc/init.d/sshd restart") + + self.FirewallReload() + public.M('firewall').where("ps=?",(public.get_msg_gettext('SSH Server'),)).setField('port',port) + public.write_log_gettext("Firewall manager", "Successfully changed SSH port to [{}]!",(port,)) + return public.return_msg_gettext(True,'Setup successfully!') + + #取SSH信息 + def GetSshInfo(self,get): + file = '/etc/ssh/sshd_config' + conf = public.readFile(file) + rep = "#*Port\\s+([0-9]+)\\s*\n" + port = re.search(rep,conf).groups(0)[0] + import system + panelsys = system.system(); + + version = panelsys.GetSystemVersion(); + if os.path.exists('/usr/bin/apt-get'): + status = public.ExecShell("service ssh status | grep -P '(dead|stop)'") + else: + if version.find(' 7.') != -1: + status = public.ExecShell("systemctl status sshd.service | grep 'dead'") + else: + status = public.ExecShell("/etc/init.d/sshd status | grep -e 'stopped' -e '已停'") + + if len(status[0]) > 3: + status = False + else: + status = True + isPing = True + try: + file = '/etc/sysctl.conf' + conf = public.readFile(file) + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)" + tmp = re.search(rep,conf).groups(0)[0] + if tmp == '1': isPing = False + except: + isPing = True + + + + data = {} + data['port'] = port + data['status'] = status + data['ping'] = isPing + return data + + # 指定端口 放行IP + def AddSpecifiesIp(self, get): + ''' + get 里面 有 protocol type port address ps 五个参数 + protocol == ['tcp','udp'] + types==['reject','accept'] # 放行和禁止 + port = 端口 + address 地址 + :param get : + :return: + ''' + + # | ports | protocol | address_ip | types | + flag = False + import re + # 判断端口是否正确 + rep = r"^\d{1,5}(:\d{1,5})?$" + if not re.search(rep, get.port): return public.return_msg_gettext(False, 'Port range is incorrect! should be between 100-65535'); + + # 判断IP是否正确 + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + if not re.search(rep2, get.address): return public.return_msg_gettext(False, 'IP address is illegal!'); + import time + ports = get.port + ps = get.ps + types = get.type + protocol=get.protocol + address_ip=get.address + + protocol_list = ['tcp', 'udp'] + type_list=['reject','accept'] + # 判断type类型是否正确 + + if types not in type_list:return public.return_msg_gettext(False, 'The port exists, no need to repeat the release!') + # 判断protocol 类型是否正确 + + if protocol not in protocol_list: return public.return_msg_gettext(False, 'The port exists, no need to repeat the release!') + + notudps = ['80', '443', '8888', '888', '39000:40000', '21', '22'] + if ports in notudps: flag = True + + # sql 查询 + #sql="select * from firewall where ports='%s' and address_ip='%s' and protocol='%s' and types='%s';" % (str(ports), str(address_ip), str(protocol), str(types)) + query_result = public.M('firewall').where('ports=? and address_ip=? and protocol=? and types=?',(ports, address_ip, protocol, types)).count() + # 这里大于0 表示存在 + if query_result > 0 : return public.return_msg_gettext(False,'The port exists, no need to repeat the release!') + + if self.__isUfw: + if type=='accept': + public.ExecShell('ufw allow proto '+ protocol +' from '+ address_ip+' to any port '+ ports +'') + + else: + public.ExecShell('ufw deny proto ' + protocol + ' from ' + address_ip + ' to any port ' + ports + '') + + else: + if self.__isFirewalld: + port = ports.replace(':', '-') + self.__Obj.Add_Port_IP(port=ports,address=address_ip,pool=protocol,type=types) + else: + if type == 'accept': + public.ExecShell('iptables -I INPUT -s '+ address_ip +' -p '+ protocol +' --dport '+ ports +' -j ACCEPT') + else: + public.ExecShell( + 'iptables -I INPUT -s ' + address_ip + ' -p ' + protocol + ' --dport ' + ports + ' -j DROP') + + + public.write_log_gettext("Firewall manager", 'Successfully accepted port [{}]!', (ports,)) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + result = public.M('firewall').add('protocol,types,port,address_ip,ps,addtime', (protocol,types,ports,address_ip,ps,addtime)) + self.FirewallReload() + return public.return_msg_gettext(True, 'Setup successfully!') + + # 删除指定放行端口 + def DelSpecifiesIp(self, get): + ''' + get 里面 有 protocol type port address ps 五个参数 + protocol == ['tcp','udp'] + type==['reject','accept'] # 放行和禁止 + port = 端口 + address 地址 + :param get: + :return: + ''' + ports = get.port + types = get.type + protocol=get.protocol + address_ip=get.address + protocol_list = ['tcp', 'udp'] + id = get.id + if protocol not in protocol_list: return public.return_msg_gettext(False, 'Specified protocol does NOT exist!') + if self.__isUfw: + if type=='accept': + public.ExecShell('ufw delete allow proto ' + protocol + ' from ' + address_ip + ' to any port ' + ports + '') + else: + public.ExecShell('ufw delete deny proto ' + protocol + ' from ' + address_ip + ' to any port ' + ports + '') + else: + if self.__isFirewalld: + self.__Obj.Del_Port_IP(port=ports,address=address_ip,pool=protocol,type=types) + + else: + if type == 'accept': + public.ExecShell('iptables -D INPUT -s ' + address_ip + ' -p ' + protocol + ' --dport ' + ports + ' -j ACCEPT') + else: + public.ExecShell('iptables -D INPUT -s ' + address_ip + ' -p ' + protocol + ' --dport ' + ports + ' -j DROP') + public.write_log_gettext("Firewall manager", 'FIREWALL_DROP_PORT', (ports,)) + public.M('firewall').where("id=?", (id,)).delete() + + self.FirewallReload() + return public.return_msg_gettext(True, 'Successfully deleted') + + diff --git a/class_v2/firewalld_v2.py b/class_v2/firewalld_v2.py new file mode 100644 index 00000000..0c8fb111 --- /dev/null +++ b/class_v2/firewalld_v2.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python +# coding:utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 1249648969@qq.com, +# +------------------------------------------------------------------- +# Firewalld管理类 +# ------------------------------ +from xml.etree.ElementTree import ElementTree, Element +import os, public + +class firewalld: + __TREE = None + __ROOT = None + __CONF_FILE = '/etc/firewalld/zones/public.xml' + + # 初始化配置文件XML对象 + def __init__(self): + if self.__TREE: return + self.__TREE = ElementTree() + self.__TREE.parse(self.__CONF_FILE) + self.__ROOT = self.__TREE.getroot() + + + # 获取端口列表 + def GetAcceptPortList(self): + mlist = self.__ROOT.getchildren() + data = [] + for p in mlist: + if p.tag != 'port': continue + tmp = p.attrib + port = p.attrib['port'] + data.append(tmp) + return data + + # 添加端口放行 + def AddAcceptPort(self, port, pool='tcp'): + # 检查是否存在 + if self.CheckPortAccept(pool, port): return True + attr = {"protocol": pool, "port": port} + Port = Element("port", attr) + self.__ROOT.append(Port) + self.Save() + return True + + # 删除端口放行 + def DelAcceptPort(self, port, pool='tcp'): + # 检查是否存在 + if not self.CheckPortAccept(pool, port): return True + mlist = self.__ROOT.getchildren() + m = False + for p in mlist: + if p.tag != 'port': continue + if p.attrib['port'] == port: + self.__ROOT.remove(p) + m = True + if m: + self.Save() + return True + return False + + # 添加UDP端口放行 + def AddUpdPort(self, port, pool='udp'): + # 检查是否存在 + if self.CheckPortAccept(pool, port): return True + attr = {"protocol": pool, "port": port} + Port = Element("port", attr) + self.__ROOT.append(Port) + self.Save() + return True + + # 删除UDP端口放行 + def DelUdpPort(self, port, pool='udp'): + # 检查是否存在 + if not self.CheckPortAccept(pool, port): return True + mlist = self.__ROOT.getchildren() + m = False + for p in mlist: + if p.tag != 'port': continue + if p.attrib['port'] == port: + self.__ROOT.remove(p) + m = True + if m: + self.Save() + return True + return False + + # 检查端口是否已放行 + def CheckPortAccept(self, pool, port): + for p in self.GetAcceptPortList(): + if p['port'] == port and p['protocol']==pool: return True + return False + + + # 获取屏蔽IP列表 + def GetDropAddressList(self): + mlist = self.__ROOT.getchildren() + data = [] + for ip in mlist: + + if ip.tag != 'rule': continue + tmp = {} + ch = ip.getchildren() + a=None + for c in ch: + tmp['type']=None + if c.tag == 'drop': tmp['type'] = 'drop' + if c.tag == 'source': + + tmp['address']=c.attrib['address'] + if tmp['type']: + data.append(tmp) + return data + + # 获取 reject 信息 + def GetrejectLIST(self): + mlist = self.__ROOT.getchildren() + data = [] + for ip in mlist: + #print(ip) + if ip.tag != 'rule': continue + tmp = {} + ch = ip.getchildren() + a=None + flag = None + for c in ch: + tmp['type']=None + if c.tag == 'reject': tmp['type'] = 'reject' + if c.tag == 'source': + + tmp['address']=c.attrib['address'] + if c.tag =='port': + + tmp['protocol']=c.attrib['protocol'] + tmp['port']=c.attrib['port'] + if tmp['type']: + data.append(tmp) + return data + +# 获取 accept 信息 + + def Getacceptlist(self): + mlist = self.__ROOT.getchildren() + data = [] + for ip in mlist: + + if ip.tag != 'rule': continue + tmp = {} + ch = ip.getchildren() + a=None + flag = None + for c in ch: + tmp['type']=None + if c.tag == 'accept': tmp['type'] = 'accept' + if c.tag == 'source': + + tmp['address']=c.attrib['address'] + if c.tag =='port': + tmp['protocol']=c.attrib['protocol'] + tmp['port']=c.attrib['port'] + if tmp['type']: + data.append(tmp) + return data + + +# 获取所有信息 + def Get_All_Info(self): + data={} + data['drop_ip']=self.GetDropAddressList() + data['reject']=self.GetrejectLIST() + data['accept']=self.Getacceptlist() + return data + +# 判断是否存在 + def Chekc_info(self,port,address,pool,type): + data=self.Get_All_Info() + if type=='accept': + for i in data['accept']: + #print(i['address'], i['protocol'], i['port']) + if i['address']==address and i['protocol']==pool and i['port']==port: + return True + else: + return False + elif type=='reject': + for i in data['accept']: + # print(i['address'], i['protocol'], i['port']) + if i['address'] == address and i['protocol'] == pool and i['port'] == port: + return True + else: + return False + else: + return False + + def AddDropAddress(self, address): + # 检查是否存在 + if self.CheckIpDrop(address): return True + attr = {"family": 'ipv4'} + rule = Element("rule", attr) + attr = {"address": address} + source = Element("source", attr) + drop = Element("drop", {}) + rule.append(source) + rule.append(drop) + self.__ROOT.append(rule) + self.Save() + return 'OK' + + # 删除IP屏蔽 + def DelDropAddress(self, address): + # 检查是否存在 + if not self.CheckIpDrop(address): return True + mlist = self.__ROOT.getchildren() + for ip in mlist: + if ip.tag != 'rule': continue + ch = ip.getchildren() + for c in ch: + + if c.tag != 'source':continue + if c.attrib['address'] == address: + self.__ROOT.remove(ip) + self.Save() + return True + return False + + + +# 添加端口放行并且指定IP + def Add_Port_IP(self, port,address,pool,type): + if type=='accept': + # 判断是否存在 + if self.Chekc_info(port,address,pool,type): return True + attr = {"family": 'ipv4'} + rule = Element("rule", attr) + attr = {"address": address} + source = Element("source", attr) + attr={'port':str(port),'protocol':pool} + port_info=Element("port",attr) + accept = Element("accept", {}) + rule.append(source) + rule.append(port_info) + rule.append(accept) + self.__ROOT.append(rule) + self.Save() + return True + + elif type=='reject': + # 判断是否存在 + if self.Chekc_info(port,address,pool,type):return True + attr = {"family": 'ipv4'} + rule = Element("rule", attr) + attr = {"address": address} + source = Element("source", attr) + attr = {'port': str(port), 'protocol': pool} + port_info = Element("port", attr) + reject = Element("reject", {}) + rule.append(source) + rule.append(port_info) + rule.append(reject) + self.__ROOT.append(rule) + self.Save() + return True + else: + return False + + +# 删除指定端口的=。= + def Del_Port_IP(self, port,address,pool,type): + if type=='accept': + a = None + for i in self.__ROOT: + if i.tag == 'rule': + tmp = {} + for c in i.getchildren(): + tmp['type'] = None + if c.tag == 'accept': tmp['type'] = 'accept' + if c.tag == 'source': + tmp['address'] = c.attrib['address'] + if c.tag == 'port': + tmp['protocol'] = c.attrib['protocol'] + tmp['port'] = c.attrib['port'] + if tmp['type']: + if tmp['port'] == port and tmp['address'] == address and tmp['type'] == type and tmp['protocol'] == pool: + self.__ROOT.remove(i) + self.Save() + return True + + elif type=='reject': + for i in self.__ROOT: + if i.tag == 'rule': + tmp = {} + for c in i.getchildren(): + tmp['type'] = None + if c.tag == 'reject': tmp['type'] = 'reject' + if c.tag == 'source': + tmp['address'] = c.attrib['address'] + if c.tag == 'port': + tmp['protocol'] = c.attrib['protocol'] + tmp['port'] = c.attrib['port'] + if tmp['type']: + if tmp['port'] == port and tmp['address'] == address and tmp['type'] == type and tmp['protocol'] == pool: + self.__ROOT.remove(i) + self.Save() + return True + + # 检查IP是否已经屏蔽 + def CheckIpDrop(self, address): + for ip in self.GetDropAddressList(): + if ip['address'] == address: return True + return False + + # 取服务状态 + def GetServiceStatus(self): + import psutil + for pid in psutil.pids(): + if psutil.Process(pid).name() == 'firewalld': return True + return False + + # 服务控制 + def FirewalldService(self, type): + public.ExecShell('systemctl ' + type + ' firewalld.service') + return public.return_msg_gettext(True, 'Setup successfully!') + + # 保存配置 + def Save(self): + self.format(self.__ROOT) + self.__TREE.write(self.__CONF_FILE, 'utf-8') + public.ExecShell('firewall-cmd --reload') + + # 整理配置文件格式 + def format(self, em, level=0): + i = "\n" + level * " " + if len(em): + if not em.text or not em.text.strip(): + em.text = i + " " + for e in em: + self.format(e, level + 1) + if not e.tail or not e.tail.strip(): + e.tail = i + if level and (not em.tail or not em.tail.strip()): + em.tail = i + + diff --git a/class_v2/firewalls_v2.py b/class_v2/firewalls_v2.py new file mode 100644 index 00000000..5555070c --- /dev/null +++ b/class_v2/firewalls_v2.py @@ -0,0 +1,384 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel x3 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import sys, os, public, re, firewalld, time +from public.validate import Param + + +class firewalls: + __isFirewalld = False + __isUfw = False + __Obj = None + + def __init__(self): + if os.path.exists('/usr/sbin/firewalld'): self.__isFirewalld = True + self.__ufw = 'ufw' + if os.path.exists('/usr/sbin/ufw'): + self.__isUfw = True + self.__ufw = '/usr/sbin/ufw' + if self.__isFirewalld: + try: + self.__Obj = firewalld.firewalld() + self.GetList() + except: + pass + + # 获取服务端列表 + def GetList(self): + try: + data = {} + data['ports'] = self.__Obj.GetAcceptPortList() + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + for i in range(len(data['ports'])): + tmp = self.CheckDbExists(data['ports'][i]['port']) + if not tmp: public.M('firewall').add('port,ps,addtime', (data['ports'][i]['port'], '', addtime)) + + data['iplist'] = self.__Obj.GetDropAddressList() + for i in range(len(data['iplist'])): + try: + tmp = self.CheckDbExists(data['iplist'][i]['address']) + if not tmp: public.M('firewall').add('port,ps,addtime', (data['iplist'][i]['address'], '', addtime)) + except: + pass + except: + pass + + # 检查数据库是否存在 + def CheckDbExists(self, port): + data = public.M('firewall').field('id,port,ps,addtime').select() + for dt in data: + if dt['port'] == port: return dt + return False + + # 重载防火墙配置 + def FirewallReload(self): + if self.__isUfw: + public.ExecShell('/usr/sbin/ufw reload &') + return + if self.__isFirewalld: + public.ExecShell('firewall-cmd --reload &') + else: + public.ExecShell('/etc/init.d/iptables save &') + public.ExecShell('/etc/init.d/iptables restart &') + + # 取防火墙状态 + def CheckFirewallStatus(self): + # if self.__isUfw: + # res = public.ExecShell('ufw status verbose')[0] + # if res.find('inactive') != -1: return False + # return True + + # if self.__isFirewalld: + # res = public.ExecShell("systemctl status firewalld")[0] + # if res.find('active (running)') != -1: return True + # if res.find('disabled') != -1: return False + # if res.find('inactive (dead)') != -1: return False + # else: + # res = public.ExecShell("/etc/init.d/iptables status")[0] + # if res.find('not running') != -1: return False + # return True + return public.get_firewall_status() == 1 + + def SetFirewallStatus(self, get=None): + ''' + @name 设置系统防火墙状态 + @author hwliang<2022-01-13> + ''' + status = not self.CheckFirewallStatus() + status_msg = {False: 'Close', True: 'Open'} + if self.__isUfw: + if status: + public.ExecShell('echo y|{} enable'.format(self.__ufw)) + else: + public.ExecShell('echo y|{} disable'.format(self.__ufw)) + if self.__isFirewalld: + if status: + public.ExecShell('systemctl enable firewalld') + public.ExecShell('systemctl start firewalld') + else: + public.ExecShell('systemctl disable firewalld') + public.ExecShell('systemctl stop firewalld') + else: + if status: + public.ExecShell("chkconfig iptables on") + public.ExecShell('/etc/init.d/iptables start') + else: + public.ExecShell("chkconfig iptables off") + public.ExecShell('/etc/init.d/iptables stop') + public.write_log_gettext('Firewall manager', '{} system firewall!', (status_msg[status],)) + return public.return_msg_gettext(True, '{} system firewall!', (status_msg[status],)) + + # 添加屏蔽IP + def AddDropAddress(self, get): + if not self.CheckFirewallStatus(): return public.return_msg_gettext(False, 'The system firewall is not open') + import time + import re + ip_format = get.port.split('/')[0] + if not public.check_ip(ip_format): return public.return_msg_gettext(False, 'IP address you entered is illegal!') + if ip_format in ['0.0.0.0', '127.0.0.0', "::1"]: return public.return_msg_gettext(False, + 'Disabling this IP will cause your server to fail') + address = get.port + if public.M('firewall').where("port=?", (address,)).count() > 0: return public.return_msg_gettext(False, + 'The IP exists in block list, no need to repeat processing!') + if self.__isUfw: + if public.is_ipv6(ip_format): + public.ExecShell('{} deny from {} to any'.format(self.__ufw, address)) + else: + public.ExecShell('{} insert 1 deny from {} to any'.format(self.__ufw, address)) + else: + if self.__isFirewalld: + # self.__Obj.AddDropAddress(address) + if public.is_ipv6(ip_format): + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule=\'rule family=ipv6 source address="' + address + '" drop\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule=\'rule family=ipv4 source address="' + address + '" drop\'') + else: + if public.is_ipv6(ip_format): return public.return_msg_gettext(False, 'IP address is illegal!') + public.ExecShell('iptables -I INPUT -s ' + address + ' -j DROP') + + public.WriteLog("TYPE_FIREWALL", 'FIREWALL_DROP_IP', (address,)) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + public.M('firewall').add('port,ps,addtime', (address, get.ps, addtime)) + self.FirewallReload() + return public.return_msg_gettext(True, 'Setup successfully!') + + # 删除IP屏蔽 + def DelDropAddress(self, get): + if not self.CheckFirewallStatus(): return public.return_msg_gettext(False, 'The system firewall is not open') + address = get.port + id = get.id + ip_format = get.port.split('/')[0] + if self.__isUfw: + public.ExecShell('{} delete deny from {} to any'.format(self.__ufw, address)) + else: + if self.__isFirewalld: + if public.is_ipv6(ip_format): + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv6 source address="' + address + '" drop\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv4 source address="' + address + '" drop\'') + else: + public.ExecShell('iptables -D INPUT -s ' + address + ' -j DROP') + + public.WriteLog("TYPE_FIREWALL", 'FIREWALL_ACCEPT_IP', (address,)) + public.M('firewall').where("id=?", (id,)).delete() + + self.FirewallReload() + return public.return_msg_gettext(True, 'Successfully deleted') + + # 添加放行端口 + def AddAcceptPort(self, get): + if not self.CheckFirewallStatus(): return public.return_msg_gettext(False, 'The system firewall is not open') + import re + src_port = get.port + get.port = get.port.replace('-', ':') + rep = r"^\d{1,5}(:\d{1,5})?$" + if not re.search(rep, get.port): + return public.return_msg_gettext(False, 'Port range must be between 22 and 65535!') + + import time + port = get.port + ps = public.xssencode2(get.ps) + is_exists = public.M('firewall').where("port=? or port=?", (port, src_port)).count() + if is_exists: return public.return_msg_gettext(False, 'The port exists, no need to repeat the release!') + notudps = ['80', '443', '8888', '888', '39000:40000', '21', '22'] + if self.__isUfw: + a = public.ExecShell('{} allow {}/tcp'.format(self.__ufw, port)) + # public.writeFile('/tmp/2',str(a)) + if not port in notudps: public.ExecShell('{} allow {}/udp'.format(self.__ufw, port)) + else: + if self.__isFirewalld: + # self.__Obj.AddAcceptPort(port) + port = port.replace(':', '-') + public.ExecShell('firewall-cmd --permanent --zone=public --add-port=' + port + '/tcp') + if not port in notudps: public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-port=' + port + '/udp') + else: + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport ' + port + ' -j ACCEPT') + if not port in notudps: public.ExecShell( + 'iptables -I INPUT -p tcp -m state --state NEW -m udp --dport ' + port + ' -j ACCEPT') + public.WriteLog("TYPE_FIREWALL", 'FIREWALL_ACCEPT_PORT', (port,)) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + if not is_exists: public.M('firewall').add('port,ps,addtime', (port, ps, addtime)) + self.FirewallReload() + return public.return_msg_gettext(True, 'Setup successfully!') + + # 添加放行端口 + def AddAcceptPortAll(self, port, ps): + if not self.CheckFirewallStatus(): return public.return_msg_gettext(False, 'The system firewall is not open') + import re + port = port.replace('-', ':') + rep = r"^\d{1,5}(:\d{1,5})?$" + if not re.search(rep, port): + return False + if self.__isUfw: + public.ExecShell('{} allow {}/tcp'.format(self.__ufw, port)) + public.ExecShell('{} allow {}/udp'.format(self.__ufw, port)) + else: + if self.__isFirewalld: + port = port.replace(':', '-') + public.ExecShell('firewall-cmd --permanent --zone=public --add-port=' + port + '/tcp') + public.ExecShell('firewall-cmd --permanent --zone=public --add-port=' + port + '/udp') + else: + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport ' + port + ' -j ACCEPT') + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m udp --dport ' + port + ' -j ACCEPT') + return True + + # 删除放行端口 + def DelAcceptPort(self, get): + if not self.CheckFirewallStatus(): return public.return_msg_gettext(False, 'The system firewall is not open') + port = get.port + id = get.id + + if public.is_ipv6(port): return self.DelDropAddress(get) # 如果是ipv6地址,则调用DelDropAddress + + try: + if (port == public.GetHost(True) or port == public.readFile('data/port.pl').strip()): + return public.return_msg_gettext(False, 'Failed,cannot delete current port of the panel') + if self.__isUfw: + public.ExecShell('{} delete allow {}/tcp'.format(self.__ufw, port)) + public.ExecShell('{} delete allow {}/udp'.format(self.__ufw, port)) + else: + if self.__isFirewalld: + # self.__Obj.DelAcceptPort(port) + public.ExecShell('firewall-cmd --permanent --zone=public --remove-port=' + port + '/tcp') + public.ExecShell('firewall-cmd --permanent --zone=public --remove-port=' + port + '/udp') + else: + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m tcp --dport ' + port + ' -j ACCEPT') + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m udp --dport ' + port + ' -j ACCEPT') + public.WriteLog("TYPE_FIREWALL", 'FIREWALL_DROP_PORT', (port,)) + public.M('firewall').where("id=?", (id,)).delete() + + self.FirewallReload() + return public.return_msg_gettext(True, 'Successfully deleted') + except: + return public.return_msg_gettext(False, 'Failed to delete') + + # 设置远程端口状态 + def SetSshStatus(self, get): + # version = public.readFile('/etc/redhat-release') + if int(get['status']) == 1: + msg = public.get_msg_gettext('SSH service turned off') + act = 'stop' + else: + msg = public.get_msg_gettext('SSH service turned on') + act = 'start' + + # if not os.path.exists('/etc/redhat-release'): + # public.ExecShell('service ssh ' + act) + # elif version.find(' 7.') != -1 or version.find(' 8.') != -1 or version.find('Fedora') != -1: + # public.ExecShell("systemctl "+act+" sshd") + # else: + # 全试一次? + public.ExecShell("/etc/init.d/sshd " + act) + public.ExecShell('service ssh ' + act) + public.ExecShell("systemctl " + act + " sshd") + public.ExecShell("systemctl " + act + " ssh") + if act in ['start'] and not public.get_sshd_status(): + msg = 'SSHD service failed to start' + public.WriteLog("TYPE_FIREWALL", msg) + return public.returnMsg(False, msg) + public.WriteLog("TYPE_FIREWALL", msg) + return public.return_msg_gettext(True, 'Setup successfully!') + + # 设置ping + def SetPing(self, get): + if get.status == '1': + get.status = '0' + else: + get.status = '1' + filename = '/etc/sysctl.conf' + conf = public.readFile(filename) + if conf.find('net.ipv4.icmp_echo') != -1: + rep = r"net\.ipv4\.icmp_echo.*" + conf = re.sub(rep, 'net.ipv4.icmp_echo_ignore_all=' + get.status + "\n", conf) + else: + conf += "\nnet.ipv4.icmp_echo_ignore_all=" + get.status + "\n" + + if public.writeFile(filename, conf): + public.ExecShell('sysctl -p') + return public.returnMsg(True, 'SUCCESS') + else: + return public.returnMsg(False, + 'ERROR: setup failed, [sysctl.conf] not writable!
                                    1. If [System hardening] is installed, please close it first
                                    ') + + # 改远程端口 + def SetSshPort(self, get): + + # 校验参数 + try: + get.validate([ + Param('port').Require().Number(">=", 22).Number("<=", 65535).Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + port = get.port + + ports = ['21', '25', '80', '443', '8080', '888', '8888', '7800'] + if port in ports: + # return public.return_msg_gettext(False,'Do NOT use common default port!') + return public.return_message(-1, 0, 'Do NOT use common default port!') + file = '/etc/ssh/sshd_config' + conf = public.readFile(file) + + rep = r"#*Port\s+([0-9]+)\s*\n" + conf = re.sub(rep, "Port " + port + "\n", conf) + public.writeFile(file, conf) + + if self.__isFirewalld: + public.ExecShell('firewall-cmd --permanent --zone=public --add-port=' + port + '/tcp') + public.ExecShell('setenforce 0') + public.ExecShell('sed -i "s#SELINUX=enforcing#SELINUX=disabled#" /etc/selinux/config') + public.ExecShell("systemctl restart sshd.service") + elif self.__isUfw: + public.ExecShell('{} allow {}/tcp'.format(self.__ufw, port)) + public.ExecShell("service ssh restart") + else: + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport ' + port + ' -j ACCEPT') + public.ExecShell("/etc/init.d/sshd restart") + + self.FirewallReload() + public.M('firewall').where("ps=? or ps=? or port=?", + ('SSH remote management service', 'SSH remote service', port)).delete() + public.M('firewall').add('port,ps,addtime', + (port, 'SSH remote service', time.strftime('%Y-%m-%d %X', time.localtime()))) + public.WriteLog("TYPE_FIREWALL", "FIREWALL_SSH_PORT", (port,)) + # return public.return_msg_gettext(True,'Setup successfully!') + return public.return_message(0, 0, 'Setup successfully!') + + # 取SSH信息 + def GetSshInfo(self, get): + port = public.get_sshd_port() + status = public.get_sshd_status() + isPing = True + try: + file = '/etc/sysctl.conf' + conf = public.readFile(file) + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)" + tmp = re.search(rep, conf).groups(0)[0] + if tmp == '1': isPing = False + except: + isPing = True + + data = {} + data['port'] = port + data['status'] = status + data['ping'] = isPing + data['firewall_status'] = self.CheckFirewallStatus() + # return data + return public.return_message(0, 0, data) + diff --git a/class_v2/flask_compress_v2.py b/class_v2/flask_compress_v2.py new file mode 100644 index 00000000..698224c7 --- /dev/null +++ b/class_v2/flask_compress_v2.py @@ -0,0 +1,146 @@ +import sys,os +from gzip import GzipFile +from io import BytesIO + +from flask import request, current_app,session,Response,g,abort + + +if sys.version_info[:2] == (2, 6): + class GzipFile(GzipFile): + """ Backport of context manager support for python 2.6""" + def __enter__(self): + if self.fileobj is None: + raise ValueError("I/O operation on closed GzipFile object") + return self + + def __exit__(self, *args): + self.close() + + +class DictCache(object): + + def __init__(self): + self.data = {} + + def get(self, key): + return self.data.get(key) + + def set(self, key, value): + self.data[key] = value + + +class Compress(object): + """ + The Compress object allows your application to use Flask-Compress. + + When initialising a Compress object you may optionally provide your + :class:`flask.Flask` application object if it is ready. Otherwise, + you may provide it later by using the :meth:`init_app` method. + + :param app: optional :class:`flask.Flask` application object + :type app: :class:`flask.Flask` or None + """ + def __init__(self, app=None): + """ + An alternative way to pass your :class:`flask.Flask` application + object to Flask-Compress. :meth:`init_app` also takes care of some + default `settings`_. + + :param app: the :class:`flask.Flask` application object. + """ + self.app = app + if app is not None: + self.init_app(app) + + def init_app(self, app): + defaults = [ + ('COMPRESS_MIMETYPES', ['text/html', 'text/css', 'text/xml', + 'application/json', + 'application/javascript']), + ('COMPRESS_LEVEL', 9), + ('COMPRESS_MIN_SIZE', 500), + ('COMPRESS_CACHE_KEY', None), + ('COMPRESS_CACHE_BACKEND', None), + ('COMPRESS_REGISTER', True), + ] + + for k, v in defaults: + app.config.setdefault(k, v) + + backend = app.config['COMPRESS_CACHE_BACKEND'] + self.cache = backend() if backend else None + self.cache_key = app.config['COMPRESS_CACHE_KEY'] + + if (app.config['COMPRESS_REGISTER'] and + app.config['COMPRESS_MIMETYPES']): + app.after_request(self.after_request) + + def after_request(self, response): + app = self.app or current_app + accept_encoding = request.headers.get('Accept-Encoding', '') + response.headers['Server'] = 'nginx' + response.headers['Connection'] = 'keep-alive' + if not 'tmp_login' in session: + response.headers["X-Frame-Options"] = "SAMEORIGIN" + if 'dologin' in g and app.config['SSL']: + try: + for k,v in request.cookies.items(): + response.set_cookie(k,'',expires='Thu, 01-Jan-1970 00:00:00 GMT',path='/') + except: + pass + + if 'rm_ssl' in g: + import public + try: + for k,v in request.cookies.items(): + response.set_cookie(k,'',expires='Thu, 01-Jan-1970 00:00:00 GMT',path='/') + except: + pass + session_name = app.config['SESSION_COOKIE_NAME'] + session_id = public.get_session_id() + response.set_cookie(session_name,'',expires='Thu, 01-Jan-1970 00:00:00 GMT',path='/') + response.set_cookie(session_name, session_id, path='/', max_age=86400 * 30,httponly=True) + + request_token = request.cookies.get('request_token','') + if request_token: + response.set_cookie('request_token',request_token,path='/',max_age=86400 * 30) + + if (response.mimetype not in app.config['COMPRESS_MIMETYPES'] or + 'gzip' not in accept_encoding.lower() or + not 200 <= response.status_code < 300 or + (response.content_length is not None and + response.content_length < app.config['COMPRESS_MIN_SIZE']) or + 'Content-Encoding' in response.headers): + g.response = response + return response + + response.direct_passthrough = False + if self.cache: + key = self.cache_key(response) + gzip_content = self.cache.get(key) or self.compress(app, response) + self.cache.set(key, gzip_content) + else: + gzip_content = self.compress(app, response) + + response.set_data(gzip_content) + + response.headers['Content-Encoding'] = 'gzip' + response.headers['Content-Length'] = response.content_length + + vary = response.headers.get('Vary') + if vary: + if 'accept-encoding' not in vary.lower(): + response.headers['Vary'] = '{}, Accept-Encoding'.format(vary) + else: + response.headers['Vary'] = 'Accept-Encoding' + + g.response = response + return response + + def compress(self, app, response): + gzip_buffer = BytesIO() + with GzipFile(mode='wb', + compresslevel=app.config['COMPRESS_LEVEL'], + fileobj=gzip_buffer) as gzip_file: + gzip_file.write(response.get_data()) + return gzip_buffer.getvalue() diff --git a/class_v2/flask_sockets_v2.py b/class_v2/flask_sockets_v2.py new file mode 100644 index 00000000..ce6ac897 --- /dev/null +++ b/class_v2/flask_sockets_v2.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +from werkzeug.routing import Map, Rule +from werkzeug.exceptions import NotFound +from werkzeug.http import parse_cookie +from flask import request + + +# Monkeys are made for freedom. +try: + from geventwebsocket.gunicorn.workers import GeventWebSocketWorker as Worker + from geventwebsocket.handler import WebSocketHandler + from gunicorn.workers.ggevent import PyWSGIHandler + + import gevent +except ImportError: + pass + + +class SocketMiddleware(object): + + def __init__(self, wsgi_app, app, socket): + self.ws = socket + self.app = app + self.wsgi_app = wsgi_app + + def __call__(self, environ, start_response): + adapter = self.ws.url_map.bind_to_environ(environ) + try: + handler, values = adapter.match() + environment = environ['wsgi.websocket'] + cookie = None + if 'HTTP_COOKIE' in environ: + cookie = parse_cookie(environ['HTTP_COOKIE']) + + with self.app.app_context(): + with self.app.request_context(environ): + # add cookie to the request to have correct session handling + request.cookie = cookie + + handler(environment, **values) + return [] + except (NotFound, KeyError): + return self.wsgi_app(environ, start_response) + + +class Sockets(object): + + def __init__(self, app=None): + #: Compatibility with 'Flask' application. + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. + self.url_map = Map() + + #: Compatibility with 'Flask' application. + #: All the attached blueprints in a dictionary by name. Blueprints + #: can be attached multiple times so this dictionary does not tell + #: you how often they got attached. + self.blueprints = {} + self._blueprint_order = [] + + if app: + self.init_app(app) + + def init_app(self, app): + app.wsgi_app = SocketMiddleware(app.wsgi_app, app, self) + + def route(self, rule, **options): + + def decorator(f): + endpoint = options.pop('endpoint', None) + self.add_url_rule(rule, endpoint, f, **options) + return f + return decorator + + def add_url_rule(self, rule, _, f, **options): + self.url_map.add(Rule(rule, endpoint=f)) + + def register_blueprint(self, blueprint, **options): + """ + Registers a blueprint for web sockets like for 'Flask' application. + + Decorator :meth:`~flask.app.setupmethod` is not applied, because it + requires ``debug`` and ``_got_first_request`` attributes to be defined. + """ + first_registration = False + + if blueprint.name in self.blueprints: + assert self.blueprints[blueprint.name] is blueprint, ( + 'A blueprint\'s name collision occurred between %r and ' + '%r. Both share the same name "%s". Blueprints that ' + 'are created on the fly need unique names.' + % (blueprint, self.blueprints[blueprint.name], blueprint.name)) + else: + self.blueprints[blueprint.name] = blueprint + self._blueprint_order.append(blueprint) + first_registration = True + + blueprint.register(self, options, first_registration) + + +# CLI sugar. +if ('Worker' in locals() and 'PyWSGIHandler' in locals() and + 'gevent' in locals()): + + class GunicornWebSocketHandler(PyWSGIHandler, WebSocketHandler): + def log_request(self): + if '101' not in self.status: + super(GunicornWebSocketHandler, self).log_request() + + Worker.wsgi_handler = GunicornWebSocketHandler + worker = Worker diff --git a/class_v2/ftp_log_v2.py b/class_v2/ftp_log_v2.py new file mode 100644 index 00000000..8d2ea31e --- /dev/null +++ b/class_v2/ftp_log_v2.py @@ -0,0 +1,563 @@ +#coding: utf-8 +# + ------------------------------------------------------------------- +# | aaPanel +# + ------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# + ------------------------------------------------------------------- +# | Author: hezhihong <272267659@@qq.cn> +# + ------------------------------------------------------------------- +import public, os, time +try: + from BTPanel import session +except: + pass +#英文转月份缩写 +month_list = { + "Jan": "1", + "Feb": "2", + "Mar": "3", + "Apr": "4", + "May": "5", + "Jun": "6", + "Jul": "7", + "Aug": "8", + "Sept": "9", + "Sep": "9", + "Oct": "10", + "Nov": "11", + "Dec": "12" +} + + +class ftplog: + + + def __init__(self): + self.__messages_file = "/var/log/" + self.__ftp_backup_path = public.get_backup_path() + '/pure-ftpd/' + if not os.path.isdir(self.__ftp_backup_path): + public.ExecShell('mkdir -p {}'.format(self.__ftp_backup_path)) + self.__script_py = public.get_panel_path() + '/script/ftplogs_cut.py' + self.__AUTH_MSG =public .to_string ([84 ,104 ,105 ,115 ,32 ,102 ,101 ,97 ,116 ,117 ,114 ,101 ,32 ,105 ,115 ,32 ,101 ,120 ,99 ,108 ,117 ,115 ,105 ,118 ,101 ,32 ,116 ,111 ,32 ,116 ,104 ,101 ,32 ,112 ,114 ,111 ,32 ,101 ,100 ,105 ,116 ,105 ,111 ,110 ,44 ,32 ,112 ,108 ,101 ,97 ,115 ,101 ,32 ,97 ,99 ,116 ,105 ,118 ,97 ,116 ,101 ,32 ,105 ,116 ,32 ,102 ,105 ,114 ,115 ,116 ]) + + + def __check_auth(cls): + from plugin_auth_v2 import Plugin as Plugin + plugin_obj = Plugin(False) + plugin_list = plugin_obj.get_plugin_list() + import PluginLoader + self.__IS_PRO_MEMBER = PluginLoader.get_auth_state() > 0 + return int(plugin_list["pro"]) > time.time() or self.__IS_PRO_MEMBER + + def get_file_list(self, path, is_bakcup=False): + """ + @name 取所有messages日志文件 + @param path: 日志文件路径 + @return: 返回日志文件列表 + """ + files = os.listdir(path) + if is_bakcup: + file_name_list = [{ + "file": "/var/log/pure-ftpd.log", + "time": int(time.time()) + }] + else: + file_name_list = [] + for i in files: + tmp_dict = {} + if not i: continue + file_path = path + i + tmp_dict['file'] = file_path + if is_bakcup: + if os.path.isfile(file_path) and i.find('pure-ftpd.log') != -1: + tmp_dict['time'] = int( + public.to_date( + times=os.path.basename(file_path).split('_')[0] + + ' 00:00:00')) + file_name_list.append(tmp_dict) + else: + if os.path.isfile(file_path) and i.find('messages') != -1: + tmp_dict['time'] = int( + public.to_date( + times=os.path.basename(file_path).split('-')[1] + + ' 00:00:00')) + file_name_list.append(tmp_dict) + file_name_list = sorted(file_name_list, + key=lambda x: x['time'], + reverse=False) + return file_name_list + + def set_ftp_log(self, get): + """ + @name 开启、关闭、获取日志状态 + @author hezhihong + @param get.exec_name 执行的动作 + """ + if not self.__check_auth(): + return public.return_message(-1,0, self.__AUTH_MSG) + if not hasattr(get, 'exec_name'): + return public.return_message(-1,0, 'The parameter is incorrect!') + conf_path = '/etc/rsyslog.conf' + conf = public.readFile(conf_path) + if not os.path.exists(conf_path): + return public.return_message(-1,0, 'The rsyslog configuration file does not exist!\nPlease check if rsyslog is installed or if /ect/rsyslog.cn exists!\nIf the debain system is not installed, please execute:apt-get install rsyslog\nPlease execute the Centos system:yum install rsyslog') + conf = public.readFile(conf_path) + import re + search_str = r"ftp\.\*.*\t*.*\t*.*-/var/log/pure-ftpd.log" + search_str_two = "ftp.none" + rep_str = '\nftp.*\t\t-/var/log/pure-ftpd.log\n' + result = re.search(search_str, conf) + #获取日志状态 + if get.exec_name == 'getlog': + if result: + return_result = 'start' + else: + return_result = 'stop' + return public.return_message(0,0, return_result) + #开启日志审计 + elif get.exec_name == 'start': + # 兼容之前开启,会将配置文件搞坏 + if conf.count('ftp.nonenftp') > 5: + conf = ''' +# /etc/rsyslog.conf configuration file for rsyslog +# +# For more information install rsyslog-doc and see +# /usr/share/doc/rsyslog-doc/html/configuration/index.html +# +# Default logging rules can be found in /etc/rsyslog.d/50-default.conf + + +################# +#### MODULES #### +################# + +module(load="imuxsock") # provides support for local system logging +#module(load="immark") # provides --MARK-- message capability + +# provides UDP syslog reception +#module(load="imudp") +#input(type="imudp" port="514") + +# provides TCP syslog reception +#module(load="imtcp") +#input(type="imtcp" port="514") + +# provides kernel logging support and enable non-kernel klog messages +module(load="imklog" permitnonkernelfacility="on") + +########################### +#### GLOBAL DIRECTIVES #### +########################### + +# +# Use traditional timestamp format. +# To enable high precision timestamps, comment out the following line. +# +$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat + +# Filter duplicated messages +$RepeatedMsgReduction on + +# +# Set the default permissions for all log files. +# +$FileOwner syslog +$FileGroup adm +$FileCreateMode 0640 +$DirCreateMode 0755 +$Umask 0022 +$PrivDropToUser syslog +$PrivDropToGroup syslog + + + +# +# Where to place spool and state files +# +$WorkDirectory /var/spool/rsyslog + +# +# Include all config files in /etc/rsyslog.d/ +# +$IncludeConfig /etc/rsyslog.d/*.conf + + + + ''' + public.writeFile(conf_path, conf) + return self.set_ftp_log(get) + if '*.info;mail.none;authpriv.none;' not in conf: + conf += '\n*.info;mail.none;authpriv.none;cron.none /var/log/messages\n' + if result: + conf = conf.replace(search_str, rep_str) + else: + conf += rep_str + #禁止ftp日志写入/var/log/messages + + d_conf = conf[conf.rfind('info;'):] + d_conf = d_conf[:d_conf.find('/')] + s_conf = d_conf.replace(',', ';') + if s_conf.find(search_str_two) == -1: + str_index = s_conf.rfind(';') + s_conf = s_conf[:str_index + + 1] + search_str_two + s_conf[str_index + 1:] + conf = conf.replace(d_conf, s_conf) + self.add_crontab() + #关闭日志审计 + elif get.exec_name == 'stop': + if result: + conf = re.sub(search_str, '', conf) + #取消禁止ftp日志写入/var/log/messages + if conf.find(search_str_two) != -1: + conf = conf.replace(search_str_two, '') + for i in [';;', ',,', ';,', ',;']: + if conf.find(i) != -1: conf = conf.replace(i, '') + self.del_crontab() + public.writeFile(conf_path, conf) + public.ExecShell('systemctl restart rsyslog') + return public.return_message(0,0, 'successfully set') + + def get_format_time(self, englist_time): + """ + @name 时间英文转换 + """ + chinanese_time = '' + try: + for i in month_list.keys(): + if i in englist_time: + tmp_time = englist_time.replace(i, month_list[i]) + tmp_time = tmp_time.split() + chinanese_time = '{}-{} {}'.format(tmp_time[0], tmp_time[1], + tmp_time[2]) + break + return chinanese_time + except: + return chinanese_time + + def get_login_log(self, get): + """ + @name 取登录日志 + @author hezhihong + @param get.user_name ftp用户名 + return + """ + if not self.__check_auth(): + return public.return_message(-1,0, self.__AUTH_MSG) + search_str = 'pure-ftpd:' + search_str2 = 'pure-ftpd[' + if not hasattr(get, 'user_name'): + return_message=public.returnMsg(False, 'The parameter is incorrect!') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + args = public.dict_obj() + args.exec_name = 'getlog' + file_name = self.__ftp_backup_path + is_backup = True + if self.set_ftp_log(get) == 'stop': + file_name = self.__messages_file + is_backup = False + file_list = self.get_file_list(file_name, is_backup) + data = [] + sortid = 0 + tmp_dict = {} + login_all = [] + for file in file_list: + + if not os.path.isfile(file['file']): continue + conf = public.readFile(file['file']) + lines = conf.split('\n') + for line in lines: + if not line: continue + login_info = {} + if search_str not in line and search_str2 not in line: + continue + tmp_value = ' is now logged in' + info = line[:line.find(search_str)].strip() + if not info: + info = line[:line.find(search_str2)].strip() + hostname = info.split()[-1] + exec_time = info.split(hostname)[0].strip() + exec_time = self.get_format_time(exec_time) + ip = line[line.find('(') + 1:line.find(')')].split('@')[1] + + #取登录成功日志 + if tmp_value in line: + user = line.split(tmp_value)[0].strip().split()[-1] + if user == '?' or user != get.user_name: continue + dict_index = '{}__{}'.format(user, ip) + if dict_index not in tmp_dict: + tmp_dict[dict_index] = [] + tmp_dict[dict_index].append(exec_time) + + #取登出日志 + tmp_value = '[INFO] Logout.' + tmp_value_two = 'Timeout - try typing a little faster next time' + if tmp_value in line or tmp_value_two in line: + user = line[line.find('(') + + 1:line.find(')')].split('@')[0] + if user == '?' or user != get.user_name: continue + dict_index = '{}__{}'.format(user, ip) + try: + login_info['out_time'] = exec_time + login_info['in_time'] = tmp_dict[dict_index][0] + login_info['user'] = user + login_info['ip'] = ip + login_info['status'] = 'Success' #0为登录失败,1为登录成功 + login_info['sortid'] = sortid + login_all.append(login_info) + tmp_dict[dict_index] = [] + sortid += 1 + except: + pass + #取登录失败日志 + tmp_value = 'Authentication failed for user' + if tmp_value in line: + user = line.split(tmp_value)[-1].replace('[', '').replace( + ']', '').strip() + if user == '?' or user != get.user_name: continue + login_info['user'] = user + login_info['ip'] = ip + login_info['status'] = 'Failure' #0为登录失败,1为登录成功 + login_info['in_time'] = exec_time + login_info['out_time'] = exec_time + login_info['sortid'] = sortid + login_all.append(login_info) + sortid += 1 + + if tmp_dict: + for item in tmp_dict.keys(): + if not tmp_dict[item]: continue + info = { + "status": "login successful", + "in_time": tmp_dict[item][0], + "out_time": "connecting", + "user": item.split('__')[0], + "ip": item.split('__')[1], + "sortid": sortid + } + sortid += 1 + login_all.append(info) + #搜索过滤 + if login_all and 'search' in get and get.search and get.search.strip(): + for info in login_all: + try: + search_str = str(get.search).strip().lower() + # public.writeFile('/tmp/aa.aa', get.search) + if info['ip'].find(search_str) != -1 or info['user'].lower( + ).find(search_str) != -1 or info['status'].find( + search_str) != -1 or info['in_time'].find( + search_str) != -1: + data.append(info) + elif info['out_time'] and info['out_time'].find( + search_str) != -1: + data.append(info) + except: + pass + else: + for info2 in login_all: + data.append(info2) + + data = sorted(data, key=lambda x: x['sortid'], reverse=True) + return self.get_page(data, get) + + def get_page(self, data, get): + """ + @name 取分页 + @author hezhihong + @param data 需要分页的数据 list + @param get.p 第几页 + @return 指定分页数据 + """ + # 包含分页类 + import page + # 实例化分页类 + page = page.Page() + + info = {} + info['count'] = len(data) + info['row'] = 10 + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + info['uri'] = {} + info['return_js'] = '' + # 获取分页数据 + result = {} + result['page'] = page.GetPage(info, limit='1,2,3,4,5,8') + n = 0 + result['data'] = [] + for i in range(info['count']): + if n >= page.ROW: break + if i < page.SHIFT: continue + n += 1 + result['data'].append(data[i]) + return public.return_message(0,0,result) + + def get_action_log(self, get): + """ + @name 取操作日志 + @author hezhihong + @param get.user_name ftp用户名 + return {"upload":[],"download":[],"rename":[],"delete":[]} + """ + if not self.__check_auth(): + return public.return_message(-1,0, self.__AUTH_MSG) + search_str = 'pure-ftpd:' + args = public.dict_obj() + args.exec_name = 'getlog' + file_name = self.__ftp_backup_path + is_backup = True + if self.set_ftp_log(get) == 'stop': + file_name = self.__messages_file + is_backup = False + file_list = self.get_file_list(file_name, is_backup) + if not hasattr(get, 'user_name'): + return_message=public.returnMsg(False, 'The parameter is incorrect!') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + data = [] + tmp_data = [] + sortid = 0 + for file in file_list: + if not os.path.isfile(file['file']): continue + conf = public.readFile(file['file']) + lines = conf.split('\n') + for line in lines: + if not line: continue + action_info = {} + if search_str not in line: continue + + tmp_v = line.split(search_str) + hostname = tmp_v[0].strip().split()[3].strip() + action_time = tmp_v[0].replace(hostname, '').strip() + action_info['time'] = self.get_format_time(action_time) + + upload_value = ' uploaded ' + download_value = ' downloaded ' + rename_value = 'successfully renamed or moved:' + delete_value = ' Deleted ' + ip = line[line.find('(') + 1:line.find(')')].split('@')[1] + action_info['ip'] = ip + action_info['type'] = '' + #取操作用户 + user = '' + if upload_value in line or download_value in line or rename_value in line or delete_value in line: + user = line[line.find('(') + + 1:line.find(')')].split('@')[0] + action_info['sortid'] = sortid + sortid = sortid + 1 + if not user or user != get.user_name: continue + #取上传日志 + if (get.type == 'all' + or get.type == 'upload') and upload_value in line: + line_list = line.split() + upload_index = line_list.index('uploaded') + # action_info['file'] = line_list[upload_index - 1].replace( + # '//', '/') + action_info['file'] = line[line.find(']') + + 1:line.rfind('(')].replace( + 'uploaded', + '').replace('//', + '/').strip() + action_info['type'] = 'upload' + tmp_data.append(action_info) + #取下载日志 + if (get.type == 'all' + or get.type == 'download') and download_value in line: + line_list = line.split() + upload_index = line_list.index('downloaded') + action_info['file'] = line_list[upload_index - 1].replace( + '//', '/') + action_info['type'] = 'download' + tmp_data.append(action_info) + #取重命名日志 + if (get.type == 'all' + or get.type == 'rename') and rename_value in line: + action_info['file'] = line.split(rename_value)[1].replace( + '->', 'Renamed to').strip().replace('//', '/') + action_info['type'] = 'rename' + tmp_data.append(action_info) + #取删除日志 + if (get.type == 'all' + or get.type == 'delete') and delete_value in line: + action_info['file'] = line.split()[-1].strip().replace( + '//', '/') + action_info['type'] = 'delete' + tmp_data.append(action_info) + # f.close + #搜索过滤 + if tmp_data and 'search' in get and get.search and get.search.strip(): + for info in tmp_data: + search_str = str(get.search).strip().lower() + if info['ip'].find(search_str) != -1 or info['file'].lower( + ).find(search_str) != -1 or info['type'].find( + search_str) != -1 or info['time'].find( + search_str) != -1 or get.user_name.lower().find( + search_str) != -1: + data.append(info) + else: + for info2 in tmp_data: + data.append(info2) + data = sorted(data, key=lambda x: x['sortid'], reverse=True) + return self.get_page(data, get) + + def del_crontab(self): + """ + @name 删除项目定时清理任务 + @auther hezhihong<2022-10-31> + @return + """ + cron_name = '[Do not delete] FTP audit log cutting task' + cron_path = public.GetConfigValue('setup_path') + '/cron/' + cron_list = public.M('crontab').where("name=?", (cron_name, )).select() + if cron_list: + for i in cron_list: + if not i: continue + cron_echo = public.M('crontab').where( + "id=?", (i['id'], )).getField('echo') + args = {"id": i['id']} + import crontab + crontab.crontab().DelCrontab(args) + del_cron_file = cron_path + cron_echo + public.ExecShell( + "crontab -u root -l| grep -v '{}'|crontab -u root -". + format(del_cron_file)) + + def add_crontab(self): + """ + @name 构造日志切割任务 + """ + python_path = '' + try: + python_path = public.ExecShell('which btpython')[0].strip("\n") + except: + try: + python_path = public.ExecShell('which python')[0].strip("\n") + except: + pass + if not python_path: return False + if not public.M('crontab').where('name=?', + ('[Do not delete] FTP audit log cutting task', )).count(): + cmd = '{} {}'.format(python_path, self.__script_py) + args = { + "name": "[Do not delete] FTP audit log cutting task", + "type": 'day', + "where1": '', + "hour": '0', + "minute": '1', + "sName": "", + "sType": 'toShell', + "notice": '0', + "notice_channel": '', + "save": '', + "save_local": '1', + "backupTo": '', + "sBody": cmd, + "urladdress": '' + } + import crontab + res = crontab.crontab().AddCrontab(args) + if res and "id" in res.keys(): + return True + return False + return True diff --git a/class_v2/ftp_v2.py b/class_v2/ftp_v2.py new file mode 100644 index 00000000..8a40e120 --- /dev/null +++ b/class_v2/ftp_v2.py @@ -0,0 +1,331 @@ +#coding: utf-8 +# + ------------------------------------------------------------------- +# | aaPanel +# + ------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# + ------------------------------------------------------------------- +# | Author: hwliang +# + ------------------------------------------------------------------- +import public,db,re,os,firewalls +import firewalls_v2 as firewalls +from public.validate import Param +try: + from BTPanel import session +except: pass +class ftp: + __runPath = None + + def __init__(self): + self.__runPath = '/www/server/pure-ftpd/bin' + + + #添加FTP + def AddUser(self,get): + # 校验参数 + try: + get.validate([ + Param('ftp_username').String(), + Param('ftp_password').String(), + Param('path').String(), + Param('ps').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + if not os.path.exists('/www/server/pure-ftpd/sbin/pure-ftpd'): + return_message=public.return_msg_gettext(False,'Please install the Pure-FTPd service in the software store first.') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + import files_v2,time + fileObj=files_v2.files() + if get['ftp_username'].strip().find(' ') != -1: + return_message=public.returnMsg(False,'Username cannot contain spaces') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + if re.search(r"\W+",get['ftp_username']): + return_message={'code':501,'msg':public.get_msg_gettext('Username is illegal, special characters are NOT allowed!')} + return public.return_message(-1,0, return_message) + if len(get['ftp_username']) < 3: + return_message={'code':501,'msg':public.get_msg_gettext('Username is illegal, cannot be less than 3 characters!')} + return public.return_message(-1,0, return_message) + if not fileObj.CheckDir(get['path']): + return_message={'code':501,'msg':public.get_msg_gettext('System critical directory cannot be used as FTP directory!')} + return public.return_message(-1,0, return_message) + if public.M('ftps').where('name=?',(get.ftp_username.strip(),)).count(): + return public.return_message(-1,0,'User [{}] exists!'.format(get.ftp_username)) + username = get['ftp_username'].strip() + if re.search("[\\/\\\\:\\*\\?\"\'\\<\\>\\|]+",username): + return_message=public.return_msg_gettext(False,"Name cannot contain /\\:*?\"<>| symbol") + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + password = get['ftp_password'].strip() + if len(password) < 6: + return_message=public.return_msg_gettext(False, 'Password must be at least [{}] characters',("6",)) + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + get.path = get['path'].replace(' ','') + get.path = get.path.replace("\\", "/") + fileObj.CreateDir(get) + public.ExecShell('chown www.www ' + get.path) + public.ExecShell(self.__runPath + '/pure-pw useradd "' + username + '" -u www -d ' + get.path + '< {}',(username,str(ex))) + return_message=public.return_msg_gettext(False,'Failed to add') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + #删除用户 + def DeleteUser(self,get): + # 校验参数 + try: + get.validate([ + Param('username').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + username = get['username'] + id = get['id'] + if public.M('ftps').where("id=? and name=?", (id,username, )).count()==0: + return_message=public.return_msg_gettext(False, 'DEL_ERROR') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + public.ExecShell(self.__runPath + '/pure-pw userdel "' + username + '"') + self.FtpReload() + public.M('ftps').where("id=?",(id,)).delete() + public.write_log_gettext('FTP manager', 'Successfully deleted FTP user[{}]!',(username,)) + return_message=public.return_msg_gettext(True, 'Successfully deleted') + del return_message['status'] + return public.return_message(0,0, return_message['msg']) + except Exception as ex: + public.write_log_gettext('FTP manager', 'Faided to delete FTP user[{}]! => {}',(username,str(ex))) + return_message=public.return_msg_gettext(False,'Failed to delete') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + + #修改用户密码 + def SetUserPassword(self,get): + # 校验参数 + try: + get.validate([ + Param('ftp_username').String(), + Param('new_password').String(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + id = get['id'] + username = get['ftp_username'].strip() + password = get['new_password'].strip() + if public.M('ftps').where("id=? and name=?", (id,username, )).count()==0: + return_message=public.return_msg_gettext(False, 'DEL_ERROR') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + if len(password) < 6: + return_message=public.return_msg_gettext(False,'Password must be at least [{}] characters',("6",)) + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + public.ExecShell(self.__runPath + '/pure-pw passwd "' + username + '"< {}',(username,str(ex))) + return_message=public.return_msg_gettext(False,'Failed to modify') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + + #设置用户状态 + def SetStatus(self,get): + msg = public.get_msg_gettext('Turn off'); + if get.status != '0': msg = public.get_msg_gettext('Turn on'); + try: + id = get['id'] + username = get['username'] + status = get['status'] + if public.M('ftps').where("id=? and name=?", (id,username, )).count()==0: + return_message=public.return_msg_gettext(False, 'DEL_ERROR') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + if int(status)==0: + public.ExecShell(self.__runPath + '/pure-pw usermod "' + username + '" -r 1') + else: + public.ExecShell(self.__runPath + '/pure-pw usermod "' + username + "\" -r ''") + self.FtpReload() + public.M('ftps').where("id=?",(id,)).setField('status',status) + public.write_log_gettext('FTP manager','Successfully {} FTP user [{}]!', (msg,username)) + return_message=public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0,0, return_message['msg']) + except Exception as ex: + public.write_log_gettext('FTP manager','Failed to {} FTP user [{}]! => {}', (msg,username,str(ex))) + return_message=public.return_msg_gettext(False,'{} FTP user failed!',(msg,)) + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + ''' + * 设置FTP端口 + * @param Int _GET['port'] 端口号 + * @return bool + ''' + def setPort(self,get): + # 校验参数 + try: + get.validate([ + Param('port').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + port = get['port'].strip() + if not port: + return public.return_message(-1,0, 'Please enter an integer for the port') + if int(port) < 1 or int(port) > 65535: + return public.return_message(-1,0, 'Port range is incorrect!') + check_used = public.check_port_stat(int(port),public.GetLocalIp()) + if check_used == 2: + return public.return_message(-1,0, 'Port[{}] is used!'.format(str(port))) + data = public.ExecShell('lsof -i:' + str(port))[0] + if len(data) !=0:return public.return_message(-1,0, 'Port[{}] is used!'.format(str(port))) + file = '/www/server/pure-ftpd/etc/pure-ftpd.conf' + conf = public.readFile(file) + rep = u"\n#?\\s*Bind\\s+[0-9]+\\.[0-9]+\\.[0-9]+\\.+[0-9]+,([0-9]+)" + #preg_match(rep,conf,tmp) + conf = re.sub(rep,"\nBind 0.0.0.0," + port,conf) + public.writeFile(file,conf) + public.ExecShell('/etc/init.d/pure-ftpd restart') + public.write_log_gettext('FTP manager', "Successfully modified FTP port to [{}]!",(port,)) + #添加防火墙 + #data = ftpinfo(port=port,ps = 'FTP端口') + get.port=port + get.ps = public.get_msg_gettext('FTP port'); + firewalls.firewalls().AddAcceptPort(get) + session['port']=port + return public.return_message(0,0, 'Setup successfully!') + except Exception as ex: + public.write_log_gettext('FTP manager', 'Failed to modify FTP port! => {}',(str(ex),)) + return public.return_message(-1,0, 'Failed to modify') + + #重载配置 + def FtpReload(self): + public.ExecShell(self.__runPath + '/pure-pw mkdb /www/server/pure-ftpd/etc/pureftpd.pdb') + + def get_login_logs(self, get): + import ftp_log_v2 as ftplog + ftpobj = ftplog.ftplog() + return ftpobj.get_login_log(get) + def get_action_logs(self, get): + import ftp_log_v2 as ftplog + ftpobj = ftplog.ftplog() + return ftpobj.get_action_log(get) + + def set_ftp_logs(self, get): + import ftp_log_v2 as ftplog + ftpobj = ftplog.ftplog() + result = ftpobj.set_ftp_log(get) + return result + + #修改用户密码 + def set_user_home(self,get): + """ + change user home + id: ftp id + path: the new ftp user home + ftp_username: ftp username + migrate: migrate ftp user data to the new home + + """ + # 校验参数 + try: + get.validate([ + Param('ftp_username').String(), + Param('path').String(), + Param('id').Integer(), + Param('migrate').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + try: + id = get['id'] + path = get['path'] + username = get['ftp_username'] + # get the old path in the panel sqlite db + old_path = public.M("ftps").where("id=?",(id,)).getField('path') + # check the auth ftp user if exists + auth_conf_file = '/www/server/pure-ftpd/etc/pureftpd.passwd' + auth_conf = public.readFile(auth_conf_file) + if not auth_conf: + return_message=public.returnMsg(False,'FTP account has not been set up') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + # get the user specified conf + auth_conf_list = [i for i in auth_conf.split('\n')] + rep = '^{}:.*'.format(username) + macth_conf = [i for i in auth_conf_list if re.search(rep,i)] + if not macth_conf: + return_message=public.returnMsg(False, 'FTP account has not been set up1') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + if len(macth_conf) > 1: + return_message=public.returnMsg(False, 'Matching multiple configurations, this operation has been stopped!') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + if not os.path.exists(path): + os.makedirs(path) + public.ExecShell('chown www.www ' + path) + # replace the old path + result = macth_conf[0] + specified_user_conf = result.replace(old_path,path) + auth_conf = auth_conf.replace(result,specified_user_conf) + public.writeFile(auth_conf_file,auth_conf) + if get.migrate == '1': + public.ExecShell('cp -rp {}/* {}'.format(old_path,path)) + self.FtpReload() + public.M('ftps').where("id=?",(id,)).setField('path',path) + public.write_log_gettext('FTP manager', 'Successfully changed password for FTP user[{}]!',(path,)) + return_message=public.return_msg_gettext(True,'Setup successfully!') + del return_message['status'] + return public.return_message(0,0, return_message['msg']) + except Exception as ex: + return public.get_error_info() + public.write_log_gettext('FTP manager', 'FTP_PASS_ERR',(path,str(ex))) + return_message=public.returnMsg(False,'EDIT_ERROR') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) diff --git a/class_v2/http_requests_v2.py b/class_v2/http_requests_v2.py new file mode 100644 index 00000000..f5ff530a --- /dev/null +++ b/class_v2/http_requests_v2.py @@ -0,0 +1,612 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +# +------------------------------------------------------------------- +# | 宝塔HTTP通信库 +# +------------------------------------------------------------------- +import os,sys,re +import ssl +import public +import json +import socket +import requests +import config +import requests.packages.urllib3.util.connection as urllib3_conn +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +class http: + _ip_type = None + def __init__(self): + self._ip_type = config.config().get_request_iptype() + + def get(self,url,timeout = 60,headers = {},verify = False,type = 'python'): + url = self.quote(url) + if type in ['python','src','php']: + old_family = urllib3_conn.allowed_gai_family + try: + # 默认使用IPv4 + if self._ip_type == 'ipv4': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + elif self._ip_type == 'ipv6': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + + result = requests.get(url,timeout=timeout,headers=get_headers(headers),verify=verify) + except Exception as ex: + # 可能使用了错误的family,尝试清除相关配置 + if(str(ex).find('Cannot assign requested address') != -1): + v_file = '{}/data/v4.pl'.format(public.get_panel_path()) + public.writeFile(v_file,'') + self._ip_type = 'auto' + + try: + # IPV6? + if self._ip_type != 'ipv6': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + result = requests.get(url,timeout=timeout,headers=get_headers(headers),verify=verify) + else: + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + result = requests.get(url,timeout=timeout,headers=get_headers(headers),verify=verify) + except: + # 使用CURL + result = self._get_curl(url,timeout,headers,verify) + finally: + urllib3_conn.allowed_gai_family = old_family + + elif type == 'curl': + result = self._get_curl(url,timeout,headers,verify) + if result.status_code == 0: + if self._ip_type == 'ipv4': + self._ip_type = 'ipv6' + elif self._ip_type == 'ipv6': + self._ip_type = 'ipv4' + else: + return result + result = self._get_curl(url,timeout,headers,verify) + if result.status_code != 0: + self.save_ip_type() + elif type == 'php': + result = self._get_php(url,timeout,headers,verify) + if result.status_code == 0: + if self._ip_type == 'ipv4': + self._ip_type = 'ipv6' + elif self._ip_type == 'ipv6': + self._ip_type = 'ipv4' + else: + return result + result = self._get_php(url,timeout,headers,verify) + if result.status_code != 0: + self.save_ip_type() + elif type == 'src': + if sys.version_info[0] == 2: + result = self._get_py2(url,timeout,headers,verify) + else: + result = self._get_py3(url,timeout,headers,verify) + return result + + def post(self,url,data,timeout = 60,headers = {},verify = False,type = 'python'): + url = self.quote(url) + if type in ['python','src','php']: + old_family = urllib3_conn.allowed_gai_family + try: + if self._ip_type == 'ipv4': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + elif self._ip_type == 'ipv6': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + + result = requests.post(url,data,timeout=timeout,headers=headers,verify=verify) + except: + public.print_log(public.get_error_info()) + try: + # IPV6? + if self._ip_type != 'ipv6': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + result = requests.post(url,data,timeout=timeout,headers=headers,verify=verify) + else: + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + result = requests.post(url,data,timeout=timeout,headers=headers,verify=verify) + except: + # 使用CURL + result = self._post_curl(url,data,timeout,headers,verify) + urllib3_conn.allowed_gai_family = old_family + + elif type == 'curl': + result = self._post_curl(url,data,timeout,headers,verify) + if result.status_code == 0: + if self._ip_type == 'ipv4': + self._ip_type = 'ipv6' + elif self._ip_type == 'ipv6': + self._ip_type = 'ipv4' + else: + return result + result = self._post_curl(url,data,timeout,headers,verify) + + # 保存有效的请求IP类型 + if result.status_code != 0: + self.save_ip_type() + elif type == 'php': + result = self._post_php(url,data,timeout,headers,verify) + if result.status_code == 0: + if self._ip_type == 'ipv4': + self._ip_type = 'ipv6' + elif self._ip_type == 'ipv6': + self._ip_type = 'ipv4' + else: + return result + result = self._post_php(url,data,timeout,headers,verify) + if result.status_code != 0: + self.save_ip_type() + elif type == 'src': + if sys.version_info[0] == 2: + result = self._post_py2(url,data,timeout,headers,verify) + else: + result = self._post_py3(url,data,timeout,headers,verify) + return result + + + def save_ip_type(self): + v_file = '{}/data/v4.pl'.format(public.get_panel_path()) + v_body = 'auto' + if self._ip_type == 'ipv4': + v_body = '-4' + elif self._ip_type == 'ipv6': + v_body = '-6' + public.writeFile(v_file,v_body) + + + def download_file(self,url,filename,data = None,timeout = 1800,speed_file='/dev/shm/download_speed.pl'): + ''' + @name 下载文件 + @author hwliang<2021-07-08> + @param url 下载地址 + @param filename 保存路径 + @param data POST参数,不传则使用GET方法,否则使用POST方法 + @param timeout 超时时间,默认1800秒 + @param speed_file + ''' + import requests + from requests.packages.urllib3.exceptions import InsecureRequestWarning + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + headers = public.get_requests_headers() + if data is None: + res = requests.get(url,headers=headers,timeout=timeout,stream=True) + else: + res = requests.post(url,data,headers=headers,timeout=timeout,stream=True) + with open(filename,"wb") as f: + for _chunk in res.iter_content(chunk_size=8192): + f.write(_chunk) + + + #POST请求 Python2 + def _post_py2(self,url,data,timeout,headers,verify): + import urllib2 + req = urllib2.Request(url, self._str_py_post(data,headers),headers = headers) + try: + if not verify: + context = ssl._create_unverified_context() + r_response = urllib2.urlopen(req,timeout = timeout,context = context) + else: + r_response = urllib2.urlopen(req,timeout = timeout) + except urllib2.HTTPError as err: + return response(str(err),err.code,[]) + except urllib2.URLError as err: + return response(str(err),0,[]) + return response(r_response.read(),r_response.getcode(),r_response.info().headers) + + #POST请求 Python3 + def _post_py3(self,url,data,timeout,headers,verify): + import urllib.request + req = urllib.request.Request(url, self._str_py_post(data,headers),headers = headers) + try: + if not verify: + context = ssl._create_unverified_context() + r_response = urllib.request.urlopen(req,timeout = timeout,context = context) + else: + r_response = urllib.request.urlopen(req,timeout = timeout) + except urllib.error.HTTPError as err: + return response(str(err),err.code,[]) + except urllib.error.URLError as err: + return response(str(err),0,[]) + r_body = r_response.read() + if type(r_body) == bytes: r_body = r_body.decode('utf-8') + return response(r_body,r_response.getcode(),r_response.getheaders()) + + #POST请求,通过CURL + def _post_curl(self,url,data,timeout,headers,verify): + headers_str = self._str_headers(headers) + pdata = self._str_post(data,headers_str) + _ssl_verify = '' + if not verify: _ssl_verify = ' -k' + result = public.ExecShell("{} -X POST -sS -i --connect-timeout {} {} {} '{}' 2>&1".format(self._curl_bin() + _ssl_verify,timeout,headers_str,pdata,url))[0] + r_body,r_headers,r_status_code = self._curl_format(result) + return response(r_body,r_status_code,r_headers) + + #POST请求,通过PHP + def _post_php(self,url,data,timeout,headers,verify): + php_version = self._get_php_version() + if not php_version: + raise Exception('No PHP version available!') + ip_type = '' + if self._ip_type == 'ipv6': + ip_type = 'curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);' + elif self._ip_type == 'ipv4': + ip_type = 'curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);' + tmp_file = '/dev/shm/http.php' + http_php = ''''''.format(ip_type = ip_type) + public.writeFile(tmp_file,http_php) + #if 'Content-Type' in headers: + # if headers['Content-Type'].find('application/json') != -1: + # data = json.dumps(pdata) + + data = json.dumps({"url":url,"timeout":timeout,"verify":verify,"headers":self._php_headers(headers),"data":data}) + if php_version in ['53']: + php_version = '/www/server/php/' + php_version + '/bin/php' + if php_version.find('/www/server/php') != -1: + result = public.ExecShell(php_version + ' ' + tmp_file + " --post='" + data + "'" )[0] + else: + result = public.request_php(php_version,'/http.php','/dev/shm','POST',{"data":data}) + if isinstance(result,bytes): result = result.decode('utf-8') + + if os.path.exists(tmp_file): os.remove(tmp_file) + r_body,r_headers,r_status_code = self._curl_format(result) + return response(json.loads(r_body),r_status_code,r_headers) + + + #GET请求 Python2 + def _get_py2(self,url,timeout,headers,verify): + import urllib2 + req = urllib2.Request(url, headers = headers) + try: + if not verify: + context = ssl._create_unverified_context() + r_response = urllib2.urlopen(req,timeout = timeout,context = context) + else: + r_response = urllib2.urlopen(req,timeout = timeout) + except urllib2.HTTPError as err: + return response(str(err),err.code,[]) + except urllib2.URLError as err: + return response(str(err),0,[]) + return response(r_response.read(),r_response.getcode(),r_response.info().headers) + + #URL转码 + def quote(self,url): + if url.find('[') == -1: return url + url_tmp = url.split('?') + if len(url_tmp) == 1: return url + url_last = url_tmp[0] + url_args = '?'.join(url_tmp[1:]) + if sys.version_info[0] == 2: + import urllib2 + url_args = urllib2.quote(url_args) + else: + import urllib.parse + url_args = urllib.parse.quote(url_args) + return url_last + '?' + url_args + + #GET请求 Python3 + def _get_py3(self,url,timeout,headers,verify): + import urllib.request + req = urllib.request.Request(url,headers = headers) + try: + if not verify: + context = ssl._create_unverified_context() + r_response = urllib.request.urlopen(req,timeout = timeout,context = context) + else: + r_response = urllib.request.urlopen(req,timeout = timeout) + except urllib.error.HTTPError as err: + return response(str(err),err.code,[]) + except urllib.error.URLError as err: + return response(str(err),0,[]) + r_body = r_response.read() + if type(r_body) == bytes: r_body = r_body.decode('utf-8') + return response(r_body,r_response.getcode(),r_response.getheaders()) + + #GET请求,通过CURL + def _get_curl(self,url,timeout,headers,verify): + headers_str = self._str_headers(headers) + _ssl_verify = '' + if not verify: _ssl_verify = ' -k' + result = public.ExecShell("{} -sS -i --connect-timeout {} {} {} 2>&1".format(self._curl_bin() + ' ' + str(_ssl_verify),timeout,headers_str,url))[0] + r_body,r_headers,r_status_code = self._curl_format(result) + return response(r_body,r_status_code,r_headers) + + #GET请求,通过PHP + def _get_php(self,url,timeout,headers,verify): + php_version = self._get_php_version() + if not php_version: + raise Exception('No PHP version available!') + ip_type = '' + if self._ip_type == 'ipv6': + ip_type = 'curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);' + elif self._ip_type == 'ipv4': + ip_type = 'curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);' + tmp_file = '/dev/shm/http.php' + http_php = ''''''.format(ip_type=ip_type) + + public.writeFile(tmp_file,http_php) + data = json.dumps({"url":url,"timeout":timeout,"verify":verify,"headers":self._php_headers(headers)}) + if php_version in ['53']: + php_version = '/www/server/php/' + php_version + '/bin/php' + if php_version.find('/www/server/php') != -1: + result = public.ExecShell(php_version + ' ' + tmp_file + " --post='" + data + "'" )[0] + else: + result = public.request_php(php_version,'/http.php','/dev/shm','POST',{"data":data}) + if isinstance(result,bytes): result = result.decode('utf-8') + + if os.path.exists(tmp_file): os.remove(tmp_file) + r_body,r_headers,r_status_code = self._curl_format(result) + return response(json.loads(r_body).strip(),r_status_code,r_headers) + + + + #取可用的PHP版本 + def _get_php_version(self): + php_versions = public.get_php_versions() + php_versions = sorted(php_versions,reverse=True) + php_path = '/www/server/php/{}/sbin/php-fpm' + php_sock = '/tmp/php-cgi-{}.sock' + for pv in php_versions: + if not os.path.exists(php_path.format(pv)): continue + if not os.path.exists(php_sock.format(pv)): continue + return pv + php_bin = '/www/server/php/{}/bin/php' + for pv in php_versions: + pb = php_bin.format(pv) + if not os.path.exists(pb): continue + return pb + return None + + #取CURL路径 + def _curl_bin(self): + c_bin = ['/usr/local/curl2/bin/curl','/usr/local/curl/bin/curl','/usr/local/bin/curl','/usr/bin/curl'] + curl_bin = 'curl' + for cb in c_bin: + if os.path.exists(cb): curl_bin = cb + if self._ip_type != 'auto': + v4_file = '{}/data/v4.pl'.format(public.get_panel_path()) + v4_body = public.readFile(v4_file).strip() + if not self._ip_type in v4_body: + if self._ip_type == 'ipv4': + v4_body = '-4' + else: + v4_body = '-6' + curl_bin += ' {}'.format(v4_body) + return curl_bin + + #格式化CURL响应头 + def _curl_format(self,req): + match = re.search("(.|\n)+\r\n\r\n",req) + if not match: return req,{},0 + tmp = match.group().split("\r\n") + i = 0 + if tmp[i].find('Continue') != -1: i+=1 + if not tmp[i]: i+=1 + try: + status_code = int(tmp[i].split()[1]) + except: + status_code = 0 + body = req.replace(match.group(),'') + return body,tmp,status_code + + #构造适用于PHP的headers + def _php_headers(self,headers): + php_headers = [] + for h in headers.keys(): + php_headers.append('{}: {}'.format(h,headers[h])) + return php_headers + + #构造适用于CURL的headers + def _str_headers(self,headers): + str_headers = '' + for key in headers.keys(): + str_headers += " -H '{}: {}'".format(key,headers[key]) + return str_headers + + #构造适用于CURL的post参数 + def _str_post(self,pdata,headers): + str_pdata = '' + if headers.find('application/jose') != -1 \ + or headers.find('application/josn') != -1: + if type(pdata) == dict: + pdata = json.dumps(pdata) + if type(pdata) == bytes: + pdata = pdata.decode('utf-8') + str_pdata += " -d '{}'".format(pdata) + return str_pdata + + for key in pdata.keys(): + str_pdata += " -F '{}={}'".format(key ,pdata[key]) + return str_pdata + + #构造适用于python的post参数 + def _str_py_post(self,pdata,headers): + if 'Content-Type' in headers: + if headers['Content-Type'].find('application/jose') != -1 \ + or headers['Content-Type'].find('application/josn') != -1: + if type(pdata) == dict: + pdata = json.dumps(pdata) + if type(pdata) == str: + pdata = pdata.encode('utf-8') + return pdata + return public.url_encode(pdata) + +#响应头对象 +class http_headers: + def __contains__(self, key): + return getattr(self,key.lower(),None) + def __setitem__(self, key, value): setattr(self,key.lower(),value) + def __getitem__(self, key): return getattr(self,key.lower(),None) + def __delitem__(self,key): delattr(self,key.lower()) + def __delattr__(self, key): delattr(self,key.lower()) + def get(self,key): return getattr(self,key.lower(),None) + def get_items(self): return self + +#响应对象 +class response: + status_code = None + status = None + code = None + headers = {} + text = None + content = None + def __init__(self,body,status_code,headers): + self.text = body + self.content = body + self.status_code = status_code + self.status = status_code + self.code = status_code + self.headers = http_headers() + self.format_headers(headers) + + def format_headers(self,raw_headers): + raw = [] + for h in raw_headers: + if not h: continue + if type(h) == tuple: + raw.append(h[0] + ': ' + h[1]) + if len(h) < 2: continue + self.headers[h[0]] = h[1].strip() + else: + raw.append(h.strip()) + tmp = h.split(': ') + if len(tmp) < 2: continue + self.headers[tmp[0]] = tmp[1].strip() + self.headers.raw = '\r\n'.join(raw) + + def close(self): + self.text = None + self.content = None + self.status_code = None + self.status = None + self.code = None + self.headers = None + + #取格式化JSON响应 + def json(self): + try: + return json.loads(self.text) + except: + return self.text + +DEFAULT_HEADERS = {"Content-type":"application/x-www-form-urlencoded","User-Agent":"BT-Panel"} +s_types = ['python','php','curl','src'] +DEFAULT_TYPE = 'python' +__version__ = 1.0 + +#请请求方法 +def get_stype(s_type): + if not s_type: + s_type_file = '/www/server/panel/data/http_type.pl' + if os.path.exists(s_type_file): + tmp_type = public.readFile(s_type_file) + if tmp_type: + tmp_type = tmp_type.strip().lower() + if tmp_type in s_types: s_type = tmp_type + else: + s_type = s_type.lower() + if not s_type in s_types: s_type = DEFAULT_TYPE + if not s_type: s_type = DEFAULT_TYPE + return s_type + +#获取请求头 +def get_headers(headers): + if type(headers) != dict: headers = {} + #if not 'Content-type' in headers: + # headers['Content-type'] = DEFAULT_HEADERS['Content-type'] + if not 'User-Agent' in headers: + headers['User-Agent'] = DEFAULT_HEADERS['User-Agent'] + return headers + +def post(url,data = {},timeout = 60,headers = {},verify = False,s_type = None): + ''' + POST请求 + @param [url] string URL地址 + @parma [data] dict POST参数 + @param [timeout] int 超时时间 默认60秒 + @param [headers] dict 请求头 默认{"Content-type":"application/x-www-form-urlencoded","User-Agent":"BT-Panel"} + @param [verify] bool 是否验证ssl证书 默认False + @param [s_type] string 请求方法 默认python 可选:curl或php + ''' + p = http() + try: + return p.post(url,data,timeout,get_headers(headers),verify,get_stype(s_type)) + except: + raise Exception(public.get_error_info()) + +def get(url,timeout = 60,headers = {},verify = False,s_type = None): + ''' + GET请求 + @param [url] string URL地址 + @param [timeout] int 超时时间 默认60秒 + @param [headers] dict 请求头 默认{"Content-type":"application/x-www-form-urlencoded","User-Agent":"BT-Panel"} + @param [verify] bool 是否验证ssl证书 默认False + @param [s_type] string 请求方法 默认python 可选:curl或php + ''' + p = http() + try: + return p.get(url,timeout,get_headers(headers),verify,get_stype(s_type)) + except: + raise Exception(public.get_error_info()) + diff --git a/class_v2/jobs_v2.py b/class_v2/jobs_v2.py new file mode 100644 index 00000000..e92b40f3 --- /dev/null +++ b/class_v2/jobs_v2.py @@ -0,0 +1,927 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import time,public,db,os,sys,json,re,shutil +os.chdir('/www/server/panel') + +def control_init(): + public.chdck_salt() + clear_other_files() + sql_pacth() + #disable_putenv('putenv') + #clean_session() + #set_crond() + clean_max_log('/www/server/panel/plugin/rsync/lsyncd.log') + clean_max_log('/var/log/rsyncd.log',1024*1024*10) + clean_max_log('/root/.pm2/pm2.log',1024*1024*20) + remove_tty1() + clean_hook_log() + run_new() + clean_max_log('/www/server/cron',1024*1024*5,20) + clean_max_log("/www/server/panel/plugin/webhook/script",1024*1024*1) + #check_firewall() + check_dnsapi() + clean_php_log() + files_set_mode() + set_pma_access() + # public.set_open_basedir() + clear_fastcgi_safe() + update_py37() + run_script() + set_php_cli_env() + check_enable_php() + #sync_node_list() + check_default_curl_file() + null_html() + remove_other() + deb_bashrc() + upgrade_gevent() + upgrade_polkit() + #hide_docker() + rep_pyenv_link() + rm_apache_cgi_test() + +def rm_apache_cgi_test(): + ''' + @name 删除apache测试cgi文件 + @author hwliang + @return void + ''' + test_cgi_file = '/www/server/apache/cgi-bin/test-cgi' + if os.path.exists(test_cgi_file): + os.remove(test_cgi_file) + +def rep_pyenv_link(): + ''' + @name 修复pyenv环境软链 + @author hwliang + @return void + ''' + + pyenv_bin = '/www/server/panel/pyenv/bin/python3' + btpython_bin = '/usr/bin/btpython' + pip_bin = '/www/server/panel/pyenv/bin/pip3' + btpip_bin = '/usr/bin/btpip' + + # 检查btpython软链接 + if not os.path.exists(pyenv_bin): return + if not os.path.exists(btpython_bin): + public.ExecShell("ln -sf {} {}".format(pyenv_bin,btpython_bin)) + + # 检查btpip软链接 + if not os.path.exists(pip_bin): return + if not os.path.exists(btpip_bin): + public.ExecShell("ln -sf {} {}".format(pip_bin,btpip_bin)) + +def hide_docker(): + ''' + @name 隐藏docker菜单 + @author hwliang + @return void + ''' + tip_file = '{}/data/hide_docker.pl'.format(public.get_panel_path()) + if os.path.exists(tip_file): return + + # 正在使用docker-compose的用户不隐藏 + docker_compose = "/usr/bin/docker-compose" + if os.path.exists(docker_compose): return + + # 获取隐藏菜单配置 + menu_key = 'memuDocker' + hide_menu_json = public.read_config('hide_menu') + if not isinstance(hide_menu_json,list): + hide_menu_json = [] + if menu_key in hide_menu_json: return + + # 保存隐藏菜单配置 + hide_menu_json.append(menu_key) + public.save_config('hide_menu',hide_menu_json) + public.writeFile(tip_file,'True') + + + + + +def upgrade_polkit(): + ''' + @name 修复polkit提权漏洞(CVE-2021-4034) + @author hwliang + @return void + ''' + upgrade_log_file = '{}/logs/upgrade_polkit.log'.format(public.get_panel_path()) + tip_file = '{}/data/upgrade_polkit.pl'.format(public.get_panel_path()) + if os.path.exists(tip_file): return + os.system("nohup {} {}/script/polkit_upgrade.py &> {}".format(public.get_python_bin(),public.get_panel_path(),upgrade_log_file)) + +def clear_other_files(): + dirPath = '/www/server/phpmyadmin/pma' + if os.path.exists(dirPath): + public.ExecShell("rm -rf {}".format(dirPath)) + dirPath = '/www/server/nginx/waf' + if os.path.exists(dirPath): + public.ExecShell("rm -rf {}".format(dirPath)) + public.ExecShell("/etc/init.d/nginx reload") + public.ExecShell("/etc/init.d/nginx start") + + dirPath = '/www/server/adminer' + if os.path.exists(dirPath): + public.ExecShell("rm -rf {}".format(dirPath)) + + dirPath = '/www/server/panel/adminer' + if os.path.exists(dirPath): + public.ExecShell("rm -rf {}".format(dirPath)) + + filename = '/www/server/nginx/off' + if os.path.exists(filename): os.remove(filename) + filename = "{}/vhost/nginx/waf.conf".format(public.get_panel_path()) + if os.path.exists(filename): + os.remove(filename) + public.ExecShell("/etc/init.d/nginx reload") + public.ExecShell("/etc/init.d/nginx start") + c = public.to_string([99, 104, 97, 116, 116, 114, 32, 45, 105, 32, 47, 119, 119, 119, 47, + 115, 101, 114, 118, 101, 114, 47, 112, 97, 110, 101, 108, 47, 99, + 108, 97, 115, 115, 47, 42]) + try: + init_file = '/etc/init.d/bt' + src_file = '/www/server/panel/init.sh' + md51 = public.md5(init_file) + md52 = public.md5(src_file) + if md51 != md52: + import shutil + shutil.copyfile(src_file,init_file) + if os.path.getsize(init_file) < 10: + public.ExecShell("chattr -i " + init_file) + public.ExecShell(r"\cp -arf %s %s" % (src_file,init_file)) + public.ExecShell("chmod +x %s" % init_file) + except:pass + public.writeFile('/var/bt_setupPath.conf','/www') + public.ExecShell(c) + p_file = 'class/plugin2.so' + if os.path.exists(p_file): public.ExecShell("rm -f class/*.so") + public.ExecShell("chmod -R 600 /www/server/panel/data;chmod -R 600 /www/server/panel/config;chmod -R 700 /www/server/cron;chmod -R 600 /www/server/cron/*.log;chown -R root:root /www/server/panel/data;chown -R root:root /www/server/panel/config;chown -R root:root /www/server/phpmyadmin;chmod -R 755 /www/server/phpmyadmin") + if os.path.exists("/www/server/mysql"): + public.ExecShell("chown mysql:mysql /etc/my.cnf;chmod 600 /etc/my.cnf") + public.ExecShell("rm -rf /www/server/panel/temp/*") + stop_path = '/www/server/stop' + if not os.path.exists(stop_path): + os.makedirs(stop_path) + public.ExecShell("chown -R root:root {path};chmod -R 755 {path}".format(path=stop_path)) + public.ExecShell('chmod 755 /www;chmod 755 /www/server') + if os.path.exists('/www/server/phpmyadmin/pma'): + public.ExecShell("rm -rf /www/server/phpmyadmin/pma") + if os.path.exists("/www/server/adminer"): + public.ExecShell("rm -rf /www/server/adminer") + if os.path.exists("/www/server/panel/adminer"): + public.ExecShell("rm -rf /www/server/panel/adminer") + if os.path.exists('/dev/shm/session.db'): + os.remove('/dev/shm/session.db') + + node_service_bin = '/usr/bin/nodejs-service' + node_service_src = '/www/server/panel/script/nodejs-service.py' + if os.path.exists(node_service_src): public.ExecShell("chmod 700 " + node_service_src) + if not os.path.exists(node_service_bin): + if os.path.exists(node_service_src): + public.ExecShell("ln -sf {} {}".format(node_service_src,node_service_bin)) + + +def sql_pacth(): + sql = db.Sql().dbfile('system') + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'load_average')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `load_average` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`pro` REAL, +`one` REAL, +`five` REAL, +`fifteen` REAL, +`addtime` INTEGER +)''' + sql.execute(csql,()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%type_id%')).count(): + public.M('sites').execute("alter TABLE sites add type_id integer DEFAULT 0",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'database_servers','%db_type%')).count(): + public.M('databases').execute("alter TABLE database_servers add db_type REAL DEFAULT 'mysql'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%edate%')).count(): + public.M('sites').execute("alter TABLE sites add edate integer DEFAULT '0000-00-00'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%project_type%')).count(): + public.M('sites').execute("alter TABLE sites add project_type STRING DEFAULT 'PHP'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%project_config%')).count(): + public.M('sites').execute("alter TABLE sites add project_config STRING DEFAULT '{}'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'backup','%ps%')).count(): + public.M('backup').execute("alter TABLE backup add ps STRING DEFAULT 'No'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'databases','%db_type%')).count(): + public.M('databases').execute("alter TABLE databases add db_type integer DEFAULT '0'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'databases','%conn_config%')).count(): + public.M('databases').execute("alter TABLE databases add conn_config STRING DEFAULT '{}'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'databases','%sid%')).count(): + public.M('databases').execute("alter TABLE databases add sid integer DEFAULT 0",()) + + ndb = public.M('databases').order("id desc").field('id,pid,name,username,password,accept,ps,addtime,type').select() + if type(ndb) == str: public.M('databases').execute("alter TABLE databases add type TEXT DEFAULT MySQL",()) + + # 计划任务表处理 + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%status%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'status' INTEGER DEFAULT 1",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%save%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'save' INTEGER DEFAULT 3",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%backupTo%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'backupTo' TEXT DEFAULT off",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%sName%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sName' TEXT",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%sBody%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sBody' TEXT",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%sType%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sType' TEXT",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%urladdress%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'urladdress' TEXT",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%save_local%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'save_local' INTEGER DEFAULT 0",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%notice%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'notice' INTEGER DEFAULT 0",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%notice_channel%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'notice_channel' TEXT DEFAULT ''",()) + + sql = db.Sql() + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'site_types')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `site_types` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`name` REAL, +`ps` REAL +)''' + + sql.execute(csql,()) + + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'download_token')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `download_token` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`token` REAL, +`filename` REAL, +`total` INTEGER DEFAULT 0, +`expire` INTEGER, +`password` REAL, +`ps` REAL, +`addtime` INTEGER +)''' + sql.execute(csql,()) + + + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'messages')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `messages` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`level` TEXT, +`msg` TEXT, +`state` INTEGER DEFAULT 0, +`expire` INTEGER, +`addtime` INTEGER +)''' + sql.execute(csql,()) + + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'temp_login')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `temp_login` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`token` REAL, +`salt` REAL, +`state` INTEGER, +`login_time` INTEGER, +`login_addr` REAL, +`logout_time` INTEGER, +`expire` INTEGER, +`addtime` INTEGER +)''' + sql.execute(csql,()) + + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'database_servers')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `database_servers` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`db_host` REAL, +`db_port` REAL, +`db_user` INTEGER, +`db_password` INTEGER, +`ps` REAL, +`addtime` INTEGER +)''' + sql.execute(csql,()) + + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'security')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `security` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `type` TEXT, + `log` TEXT, + `addtime` INTEGER DEFAULT 0 + )''' + sql.execute(csql, ()) + + + test_ping() + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'logs','%username%')).count(): + public.M('logs').execute("alter TABLE logs add uid integer DEFAULT '1'",()) + public.M('logs').execute("alter TABLE logs add username TEXT DEFAULT 'system'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'crontab','%status%')).count(): + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'status' INTEGER DEFAULT 1",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'save' INTEGER DEFAULT 3",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'backupTo' TEXT DEFAULT off",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sName' TEXT",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sBody' TEXT",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sType' TEXT",()) + public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'urladdress' TEXT",()) + + public.M('users').where('email=? or email=?',('287962566@qq.com','amw_287962566@qq.com')).setField('email','test@message.com') + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'users','%salt%')).count(): + public.M('users').execute("ALTER TABLE 'users' ADD 'salt' TEXT",()) + + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'messages','%retry_num%')).count(): + public.M('messages').execute("alter TABLE messages add send integer DEFAULT 0",()) + public.M('messages').execute("alter TABLE messages add retry_num integer DEFAULT 0",()) + + +def upgrade_gevent(): + ''' + @name 升级gevent + @author hwliang + @return void + ''' + tip_file = '{}/data/upgrade_gevent.lock'.format(public.get_panel_path()) + upgrade_script_file = '{}/script/upgrade_gevent.sh'.format(public.get_panel_path()) + if os.path.exists(upgrade_script_file) and not os.path.exists(tip_file): + public.writeFile(tip_file,'1') + os.system("bash {}".format(upgrade_script_file)) + if os.path.exists(tip_file): os.remove(tip_file) + + +def deb_bashrc(): + ''' + @name 针对debian/ubuntu未调用bashrc导致的问题 + @author hwliang + @return void + ''' + bashrc = '/root/.bashrc' + bash_profile = '/root/.bash_profile' + apt_get = '/usr/bin/apt-get' + if not os.path.exists(apt_get): return + if not os.path.exists(bashrc): return + if not os.path.exists(bash_profile): return + + profile_body = public.readFile(bash_profile) + if not isinstance(profile_body,str): return + if profile_body.find('.bashrc') == -1: + public.writeFile(bash_profile,'source ~/.bashrc\n' + profile_body.strip() + "\n") + + + +def remove_other(): + rm_files = [ + "class/pluginAuth.so", + "class/pluginAuth.cpython-310-x86_64-linux-gnu.so", + "class/pluginAuth.cpython-310-aarch64-linux-gnu.so", + "class/pluginAuth.cpython-37m-i386-linux-gnu.so", + "class/pluginAuth.cpython-37m-loongarch64-linux-gnu.so", + "class/pluginAuth.cpython-37m-aarch64-linux-gnu.so", + "class/pluginAuth.cpython-37m-x86_64-linux-gnu.so", + "class/pluginAuth.cpython-37m.so", + "class/libAuth.loongarch64.so", + "class/libAuth.x86.so", + "class/libAuth.x86-64.so", + "class/libAuth.glibc-2.14.x86_64.so", + "class/libAuth.aarch64.so", + "script/check_files.py" + ] + + for f in rm_files: + if os.path.exists(f): + os.remove(f) + + + +def null_html(): + null_files = ['/www/server/nginx/html/index.html','/www/server/apache/htdocs/index.html','/www/server/panel/data/404.html'] + null_new_body=''' +404 Not Found + +

                                    404 Not Found

                                    +
                                    nginx
                                    + +''' + for null_file in null_files: + if not os.path.exists(null_file): continue + + null_body = public.readFile(null_file) + if not null_body: continue + if null_body.find('没有找到站点') != -1 or null_body.find('您请求的文件不存在') != -1: + public.writeFile(null_file,null_new_body) + + +def check_default_curl_file(): + default_file = '{}/data/default_curl.pl'.format(public.get_panel_path()) + if os.path.exists(default_file): + default_curl_body = public.readFile(default_file) + if default_curl_body: + public.WriteFile(default_file,default_curl_body.strip()) + +def set_wp_cache_dir(): + import one_key_wp + one_key_wp.fast_cgi().set_nginx_conf() + public.ExecShell("/etc/init.d/nginx restart") + +def set_php_cli_env(): + ''' + @name 设置php-cli环境变量 + @author hwliang<2021-09-07> + @return void + ''' + php_path = '/www/server/php' + bashrc = '/root/.bashrc' + if not os.path.exists(php_path): return + if not os.path.exists(bashrc): return + # 清理所有别名 + public.ExecShell('sed -i "/alias php/d" {}'.format(bashrc)) + bashrc_body = public.readFile(bashrc) + if not bashrc_body: return + + # 设置默认环境变量版本别名 + env_php_bin = '/usr/bin/php' + if os.path.exists(env_php_bin): + if os.path.islink(env_php_bin): + php_cli_ini = "/etc/php-cli.ini" + if os.path.exists(php_cli_ini): + bashrc_body += "alias php='php -c {}'\n".format(php_cli_ini) + + + # 设置所有已安装的PHP版本环境变量和别名 + php_versions_list = public.get_php_versions() + for php_version in php_versions_list: + php_ini = "{}/{}/etc/php.ini".format(php_path,php_version) + php_cli_ini = "{}/{}/etc/php-cli.ini".format(php_path,php_version) + env_php_bin = "/usr/bin/php{}".format(php_version) + php_bin = "{}/{}/bin/php".format(php_path,php_version) + php_ize = '/usr/bin/php{}-phpize'.format(php_version) + php_ize_src = "{}/{}/bin/phpize".format(php_path,php_version) + php_fpm = '/usr/bin/php{}-php-fpm'.format(php_version) + php_fpm_src = "{}/{}/sbin/php-fpm".format(php_path,php_version) + php_pecl = '/usr/bin/php{}-pecl'.format(php_version) + php_pecl_src = "{}/{}/bin/pecl".format(php_path,php_version) + php_pear = '/usr/bin/php{}-pear'.format(php_version) + php_pear_src = "{}/{}/bin/pear".format(php_path,php_version) + + if os.path.exists(php_bin): + # 设置每个版本的环境变量 + if not os.path.exists(env_php_bin): os.symlink(php_bin,env_php_bin) + if not os.path.exists(php_ize) and os.path.exists(php_ize_src): os.symlink(php_ize_src,php_ize) + if not os.path.exists(php_fpm) and os.path.exists(php_fpm_src): os.symlink(php_fpm_src,php_fpm) + if not os.path.exists(php_pecl) and os.path.exists(php_pecl_src): os.symlink(php_pecl_src,php_pecl) + if not os.path.exists(php_pear) and os.path.exists(php_pear_src): os.symlink(php_pear_src,php_pear) + public.ExecShell(r"\cp -f {} {}".format(php_ini,php_cli_ini)) # 每次复制新的php.ini到php-cli.ini + public.ExecShell('sed -i "/disable_functions/d" {}'.format(php_cli_ini)) # 清理禁用函数 + bashrc_body += "alias php{}='php{} -c {}'\n".format(php_version,php_version,php_cli_ini) # 设置别名 + else: + # 清理已卸载的环境变量 + if os.path.exists(env_php_bin): os.remove(env_php_bin) + if os.path.exists(php_ize): os.remove(php_ize) + if os.path.exists(php_fpm): os.remove(php_fpm) + if os.path.exists(php_pecl): os.remove(php_pecl) + if os.path.exists(php_pear): os.remove(php_pear) + public.writeFile(bashrc,bashrc_body) + + +def check_enable_php(): + ''' + @name 检查nginx下的php配置文件 + ''' + php_versions = public.get_php_versions() + ngx_php_conf = public.get_setup_path() + '/nginx/conf/enable-php-00.conf' + public.writeFile(ngx_php_conf,'') + for php_v in php_versions: + ngx_php_conf = public.get_setup_path() + '/nginx/conf/enable-php-{}.conf'.format(php_v) + if os.path.exists(ngx_php_conf): continue + enable_conf = r''' + location ~ [^/]\.php(/|$) + {{ + try_files $uri =404; + fastcgi_pass unix:/tmp/php-cgi-{}.sock; + fastcgi_index index.php; + include fastcgi.conf; + include pathinfo.conf; + }} + '''.format(php_v) + public.writeFile(ngx_php_conf,enable_conf) + + + +def write_run_script_log(_log,rn='\n'): + _log_file = '/www/server/panel/logs/run_script.log' + public.writeFile(_log_file,_log + rn,'a+') + + +def run_script(): + try: + os.system("{} {}/script/run_script.py".format(public.get_python_bin(),public.get_panel_path())) + run_tip = '/dev/shm/bt.pl' + if os.path.exists(run_tip): return + public.writeFile(run_tip,str(time.time())) + uptime = float(public.readFile('/proc/uptime').split()[0]) + if uptime > 1800: return + run_config ='/www/server/panel/data/run_config' + script_logs = '/www/server/panel/logs/script_logs' + if not os.path.exists(run_config): + os.makedirs(run_config,384) + if not os.path.exists(script_logs): + os.makedirs(script_logs,384) + + for sname in os.listdir(run_config): + script_conf_file = '{}/{}'.format(run_config,sname) + if not os.path.exists(script_conf_file): continue + script_info = json.loads(public.readFile(script_conf_file)) + exec_log_file = '{}/{}'.format(script_logs,sname) + + if not os.path.exists(script_info['script_file']) \ + or script_info['script_file'].find('/www/server/panel/plugin/') != 0 \ + or not re.match(r'^\w+$',script_info['script_file']): + os.remove(script_conf_file) + if os.path.exists(exec_log_file): os.remove(exec_log_file) + continue + + + if script_info['script_type'] == 'python': + _bin = public.get_python_bin() + elif script_info['script_type'] == 'bash': + _bin = '/usr/bin/bash' + if not os.path.exists(_bin): _bin = 'bash' + + exec_script = 'nohup {} {} &> {} &'.format(_bin,script_info['script_file'],exec_log_file) + public.ExecShell(exec_script) + script_info['last_time'] = time.time() + public.writeFile(script_conf_file,json.dumps(script_info)) + except: + pass + + +def clear_fastcgi_safe(): + try: + fastcgifile = '/www/server/nginx/conf/fastcgi.conf' + if os.path.exists(fastcgifile): + conf = public.readFile(fastcgifile) + if conf.find('bt_safe_open') != -1: + public.ExecShell('sed -i "/bt_safe_open/d" {}'.format(fastcgifile)) + public.ExecShell('/etc/init.d/nginx reload') + except: + pass + +#设置文件权限 +def files_set_mode(): + rr = {True:'-R',False:''} + m_paths = [ + ["/www/server/total","/*.lua","root",755,False], + ["/www/server/total","/*.json","root",755,False], + ["/www/server/total/logs","","www",755,True], + ["/www/server/total/total","","www",755,True], + ["/www/server/speed","/*.lua","root",755,False], + ["/www/server/speed/total","","www",755,True], + ["/www/server/btwaf","/*.lua","root",755,False], + ["/www/backup","","root",600,True], + ["/www/wwwlogs","","www",700,True], + ["/www/enterprise_backup","","root",600,True], + ["/www/server/cron","","root",700,True], + ["/www/server/cron","/*.log","root",600,True], + ["/www/server/stop","","root",755,True], + ["/www/server/redis","","redis",700,True], + ["/www/server/redis/redis.conf","","redis",600,False], + ["/www/server/panel/class","","root",600,True], + ["/www/server/panel/data","","root",600,True], + ["/www/server/panel/plugin","","root",600,False], + ["/www/server/panel/BTPanel","","root",600,True], + ["/www/server/panel/vhost","","root",600,True], + ["/www/server/panel/rewrite","","root",600,True], + ["/www/server/panel/config","","root",600,True], + ["/www/server/panel/backup","","root",600,True], + ["/www/server/panel/package","","root",600,True], + ["/www/server/panel/script","","root",700,True], + ["/www/server/panel/temp","","root",600,True], + ["/www/server/panel/tmp","","root",600,True], + ["/www/server/panel/ssl","","root",600,True], + ["/www/server/panel/install","","root",600,True], + ["/www/server/panel/logs","","root",600,True], + ["/www/server/panel/BT-Panel","","root",700,False], + ["/www/server/panel/BT-Task","","root",700,False], + ["/www/server/panel","/*.py","root",600,False], + ["/dev/shm/session.db","","root",600,False], + ["/dev/shm/session_py3","","root",600,True], + ["/dev/shm/session_py2","","root",600,True], + ["/www/server/phpmyadmin","","root",755,True], + ["/www/server/coll","","root",700,True], + ["/www/server/panel/init.sh","","root",600,False], + ["/www/server/panel/license.txt","","root",600,False], + ["/www/server/panel/requirements.txt","","root",600,False], + ["/www/server/panel/update.sh","","root",600,False], + ["/www/server/panel/default.pl","","root",600,False], + ["/www/server/panel/hooks","","root",600,True], + ["/www/server/panel/cache","","root",600,True], + ["/root","","root",550,False], + ["/root/.ssh","","root",700,False], + ["/root/.ssh/authorized_keys","","root",600,False], + ["/root/.ssh/id_rsa.pub","","root",644,False], + ["/root/.ssh/id_rsa","","root",600,False], + ["/root/.ssh/known_hosts","","root",644,False] + ] + + recycle_list = public.get_recycle_bin_list() + for recycle_path in recycle_list: + m_paths.append([recycle_path,'','root',600,True]) + + for m in m_paths: + if not os.path.exists(m[0]): continue + path = m[0] + m[1] + public.ExecShell("chown {R} {U}:{U} {P}".format(P=path,U=m[2],R=rr[m[4]])) + public.ExecShell("chmod {R} {M} {P}".format(P=path,M=m[3],R=rr[m[4]])) + if m[1]: + public.ExecShell("chown {U}:{U} {P}".format(P=m[0],U=m[2],R=rr[m[4]])) + public.ExecShell("chmod {M} {P}".format(P=m[0],M=m[3],R=rr[m[4]])) + + # 移除面板目录下所有文件的所属组、其它用户的写权限 + public.ExecShell("chmod -R go-w /www/server/panel") + +#获取PMA目录 +def get_pma_path(): + pma_path = '/www/server/phpmyadmin' + if not os.path.exists(pma_path): return False + for filename in os.listdir(pma_path): + filepath = pma_path + '/' + filename + if os.path.isdir(filepath): + if filename[0:10] == 'phpmyadmin': + return str(filepath) + return False + + +#处理phpmyadmin访问权限 +def set_pma_access(): + try: + pma_path = get_pma_path() + if not pma_path: return False + if not os.path.exists(pma_path): return False + pma_tmp = pma_path + '/tmp' + if not os.path.exists(pma_tmp): + os.makedirs(pma_tmp) + + nginx_file = '/www/server/nginx/conf/nginx.conf' + if os.path.exists(nginx_file): + nginx_conf = public.readFile(nginx_file) + if nginx_conf.find('/tmp/') == -1: + r_conf = '''/www/server/phpmyadmin; + location ~ /tmp/ { + return 403; + }''' + + nginx_conf = nginx_conf.replace('/www/server/phpmyadmin;',r_conf) + public.writeFile(nginx_file,nginx_conf) + public.serviceReload() + + apa_pma_tmp = pma_tmp + '/.htaccess' + if not os.path.exists(apa_pma_tmp): + r_conf = '''order allow,deny + deny from all''' + public.writeFile(apa_pma_tmp,r_conf) + public.set_mode(apa_pma_tmp,755) + public.set_own(apa_pma_tmp,'root') + + public.ExecShell("chmod -R 700 {}".format(pma_tmp)) + public.ExecShell("chown -R www:www {}".format(pma_tmp)) + return True + except: + return False + + + + + +#尝试升级到独立环境 +def update_py37(): + pyenv='/www/server/panel/pyenv/bin/python3' + pyenv_exists='/www/server/panel/data/pyenv_exists.pl' + if os.path.exists(pyenv) or os.path.exists(pyenv_exists): return False + download_url = public.get_url() + public.ExecShell("nohup curl {}/install/update_panel_en.sh|bash &>/tmp/panelUpdate.pl &".format(download_url)) + public.writeFile(pyenv_exists,'True') + return True + +def test_ping(): + _f = '/www/server/panel/data/ping_token.pl' + if os.path.exists(_f): os.remove(_f) + try: + import panelPing + panelPing.Test().create_token() + except: + pass + +#检查dnsapi +def check_dnsapi(): + dnsapi_file = 'config/dns_api.json' + tmp = public.readFile(dnsapi_file) + if not tmp: return False + dnsapi = json.loads(tmp) + if tmp.find('CloudFlare') == -1: + cloudflare = { + "ps": "Use CloudFlare's API interface to automatically parse and apply for SSL", + "title": "CloudFlare", + "data": [{ + "value": "", + "key": "SAVED_CF_MAIL", + "name": "E-Mail" + }, { + "value": "", + "key": "SAVED_CF_KEY", + "name": "API Key" + }], + "help": "CloudFlare Get in the background Global API Key", + "name": "CloudFlareDns" + } + dnsapi.insert(0,cloudflare) + check_names = {"dns_bt":"Dns_com","dns_dp":"DNSPodDns","dns_ali":"AliyunDns","dns_cx":"CloudxnsDns"} + for i in range(len(dnsapi)): + if dnsapi[i]['name'] in check_names: + dnsapi[i]['name'] = check_names[dnsapi[i]['name']] + + public.writeFile(dnsapi_file,json.dumps(dnsapi)) + return True + + + +#检测端口放行是否同步(仅firewalld) +def check_firewall(): + try: + if not os.path.exists('/usr/sbin/firewalld'): return False + data = public.M('firewall').field('port,ps').select() + import firewalld,firewalls + fs = firewalls.firewalls() + accept_ports = firewalld.firewalld().GetAcceptPortList() + + port_list = [] + for port_info in accept_ports: + if port_info['port'] in port_list: + continue + port_list.append(port_info['port']) + + n = 0 + for p in data: + if p['port'].find('.') != -1: + continue + if p['port'] in port_list: + continue + fs.AddAcceptPortAll(p['port'],p['ps']) + n+=1 + #重载 + if n: fs.FirewallReload() + except: + pass + + +#尝试启动新架构 +def run_new(): + try: + new_file = '/www/server/panel/data/new.pl' + port_file = '/www/server/panel/data/port.pl' + if os.path.exists(new_file): return False + if not os.path.exists(port_file): return False + port = public.readFile(port_file) + if not port: return False + cmd_line = public.ExecShell('lsof -P -i:{}|grep LISTEN|grep -v grep'.format(int(port)))[0] + if len(cmd_line) < 20: return False + if cmd_line.find('BT-Panel') != -1: return False + public.writeFile('/www/server/panel/data/restart.pl','True') + public.writeFile(new_file,'True') + return True + except: + return False + +#清理webhook日志 +def clean_hook_log(): + path = '/www/server/panel/plugin/webhook/script' + if not os.path.exists(path): return False + for name in os.listdir(path): + if name[-4:] != ".log": continue + clean_max_log(path+'/' + name,524288) + +#清理PHP日志 +def clean_php_log(): + path = '/www/server/php' + if not os.path.exists(path): return False + php_list=public.get_php_versions() + for name in os.listdir(path): + if name not in php_list:continue + filename = path +'/'+name + '/var/log/php-fpm.log' + if os.path.exists(filename): clean_max_log(filename) + filename = path +'/'+name + '/var/log/php-fpm-test.log' + if os.path.exists(filename): clean_max_log(filename) + filename = path +'/'+name + '/var/log/slow.log' + if os.path.exists(filename): clean_max_log(filename) + +#清理大日志 +def clean_max_log(log_file,max_size = 104857600,old_line = 100): + if not os.path.exists(log_file): return False + if os.path.getsize(log_file) > max_size: + try: + old_body = public.GetNumLines(log_file,old_line) + public.writeFile(log_file,old_body) + except: + print(public.get_error_info()) + +#删除tty1 +def remove_tty1(): + file_path = '/etc/systemd/system/getty@tty1.service' + if not os.path.exists(file_path): return False + if not os.path.islink(file_path): return False + if os.readlink(file_path) != '/dev/null': return False + try: + os.remove(file_path) + except:pass + + +#默认禁用指定PHP函数 +def disable_putenv(fun_name): + try: + is_set_disable = '/www/server/panel/data/disable_%s' % fun_name + if os.path.exists(is_set_disable): return True + php_vs = public.get_php_versions() + php_ini = "/www/server/php/{0}/etc/php.ini" + rep = r"disable_functions\s*=\s*.*" + for pv in php_vs: + php_ini_path = php_ini.format(pv) + if not os.path.exists(php_ini_path): continue + php_ini_body = public.readFile(php_ini_path) + tmp = re.search(rep,php_ini_body) + if not tmp: continue + disable_functions = tmp.group() + if disable_functions.find(fun_name) != -1: continue + print(disable_functions) + php_ini_body = php_ini_body.replace(disable_functions,disable_functions+',%s' % fun_name) + php_ini_body.find(fun_name) + public.writeFile(php_ini_path,php_ini_body) + public.phpReload(pv) + public.writeFile(is_set_disable,'True') + return True + except: return False + + +#创建计划任务 +def set_crond(): + try: + echo = public.md5(public.md5('renew_lets_ssl_bt')) + cron_id = public.M('crontab').where('echo=?',(echo,)).getField('id') + + import crontab + args_obj = public.dict_obj() + if not cron_id: + cronPath = public.GetConfigValue('setup_path') + '/cron/' + echo + shell = public.get_python_bin() + ' /www/server/panel/class/panelLets.py renew_lets_ssl' + public.writeFile(cronPath,shell) + args_obj.id = public.M('crontab').add('name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress',("Renew the Let's Encrypt certificate",'day','','0','10',echo,time.strftime('%Y-%m-%d %X',time.localtime()),0,'','localhost','toShell','',shell,'')) + crontab.crontab().set_cron_status(args_obj) + else: + cron_path = public.get_cron_path() + if os.path.exists(cron_path): + cron_s = public.readFile(cron_path) + if cron_s.find(echo) == -1: + public.M('crontab').where('echo=?',(echo,)).setField('status',0) + args_obj.id = cron_id + crontab.crontab().set_cron_status(args_obj) + except: + print(public.get_error_info()) + + +#清理多余的session文件 +def clean_session(): + try: + session_path = r'/dev/shm/session_py' + str(sys.version_info[0]) + if not os.path.exists(session_path): return False + now_time = time.time() + p_time = 86400 + old_state = False + for fname in os.listdir(session_path): + filename = os.path.join(session_path,fname) + if not os.path.exists(filename): continue + modify_time = os.path.getmtime(filename) + if (now_time - modify_time) > p_time: + old_state = True + break + if old_state: public.ExecShell("rm -f " + session_path + '/*') + return True + except:return False + + + +if __name__ == '__main__': + control_init() + + diff --git a/class_v2/letsencrypt_v2.py b/class_v2/letsencrypt_v2.py new file mode 100644 index 00000000..10b304ae --- /dev/null +++ b/class_v2/letsencrypt_v2.py @@ -0,0 +1,252 @@ +# coding: utf-8 +# The MIT License (MIT) +# +# Copyright (c) 2015 Daniel Roesler +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# Copyright Daniel Roesler, under MIT license, see LICENSE at github.com/diafygi/acme-tiny +import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging, requests + +try: + from urllib.request import urlopen, Request # 3 +except ImportError: + from urllib2 import urlopen, Request # 2 + +DEFAULT_CA = "https://acme-v02.api.letsencrypt.org" +# DEFAULT_CA = "https://acme-staging-v02.api.letsencrypt.org" +DEFAULT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory" # 正式 +# DEFAULT_DIRECTORY_URL = "https://acme-staging-v02.api.letsencrypt.org/directory " # 测试 + +LOGGER = logging.getLogger(__name__) +LOGGER.addHandler(logging.StreamHandler()) +LOGGER.setLevel(logging.INFO) + + +def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None): + directory, acct_headers, alg, jwk = None, None, None, None # global variables + + def _b64(b): + return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") + + def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"): + proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate(cmd_input) + if proc.returncode != 0: + raise IOError("{0}\n{1}".format(err_msg, err)) + return out + + def _do_request(url, data=None, err_msg="Error", depth=0): + try: + resp = urlopen(Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-tiny"})) + resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers + except IOError as e: + resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e) + code, headers = getattr(e, "code", None), {} + try: + resp_data = json.loads(resp_data) + except ValueError: + pass + if depth < 100 and code == 400 and resp_data['type'] == "urn:ietf:params:acme:error:badNonce": + raise IndexError(resp_data) + if code not in [200, 201, 204]: + # print("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}".format(err_msg, url, data, code)) + sys.exit(json.dumps(resp_data)) + return resp_data, code, headers + + def _send_signed_request(url, payload, err_msg, depth=0): + payload64 = _b64(json.dumps(payload).encode('utf8')) + new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce'] + protected = {"url": url, "alg": alg, "nonce": new_nonce} + protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']}) + protected64 = _b64(json.dumps(protected).encode('utf8')) + protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8') + out = _cmd(["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error") + data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)}) + try: + return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth) + except IndexError: # retry bad nonces (they raise IndexError) + return _send_signed_request(url, payload, err_msg, depth=(depth + 1)) + + def _poll_until_not(url, pending_statuses, err_msg): + while True: + result, _, _ = _do_request(url, err_msg=err_msg) + if result['status'] in pending_statuses: + time.sleep(2) + continue + return result + + log.info("Parsing account key...") + out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error") + pub_pattern = r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)" + pub_hex, pub_exp = re.search(pub_pattern, out.decode('utf8'), re.MULTILINE | re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp + alg = "RS256" + jwk = { + "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "kty": "RSA", + "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + } + accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':')) + thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + + # find domains + log.info("Parsing CSR...") + out = _cmd(["openssl", "req", "-in", csr, "-noout", "-text"], err_msg="Error loading {0}".format(csr)) + domains = set([]) + common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8')) + if common_name is not None: + domains.add(common_name.group(1)) + subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE | re.DOTALL) + if subject_alt_names is not None: + for san in subject_alt_names.group(1).split(", "): + if san.startswith("DNS:"): + domains.add(san[4:]) + log.info("Found domains: {0}".format(", ".join(domains))) + + # get the ACME directory of urls + log.info("Getting directory...") + directory_url = CA + "/directory" if CA != DEFAULT_CA else directory_url # backwards compatibility with deprecated CA kwarg + directory, _, _ = _do_request(directory_url, err_msg="Error getting directory") + log.info("Directory found!") + + # create account, update contact details (if any), and set the global key identifier + log.info("Registering account...") + reg_payload = {"termsOfServiceAgreed": True} + account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering") + log.info("Registered!" if code == 201 else "Already registered!") + if contact is not None: + account, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details") + log.info("Updated contact details:\n{0}".format("\n".join(account['contact']))) + + # create a new order + log.info("Creating new order...") + order_payload = {"identifiers": [{"type": "dns", "value": d} for d in domains]} + order, _, order_headers = _send_signed_request(directory['newOrder'], order_payload, "Error creating new order") + log.info("Order created!") + + # get the authorizations that need to be completed + for auth_url in order['authorizations']: + authorization, _, _ = _do_request(auth_url, err_msg="Error getting challenges") + domain = authorization['identifier']['value'] + log.info("Verifying {0}...".format(domain)) + + # find the http-01 challenge and write the challenge file + challenge = [c for c in authorization['challenges'] if c['type'] == "http-01"][0] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = "{0}.{1}".format(token, thumbprint) + wellknown_path = os.path.join(acme_dir, token) + with open(wellknown_path, "w") as wellknown_file: + wellknown_file.write(keyauthorization) + + # check that the file is in place + # try: + # wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) + # assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization) + # except (AssertionError, ValueError) as e: + # os.remove(wellknown_path) + # raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e)) + + # say the challenge is done + _send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(domain)) + authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain)) + if authorization['status'] != "valid": + public.WriteFile(os.path.join(path, "check_authorization_status_response"), json.dumps(authorization), mode="w") + print("Challenge did not pass for {0}".format(domain, )) + sys.exit(json.dumps(authorization)) + log.info("{0} verified!".format(domain)) + + # finalize the order with the csr + log.info("Signing certificate...") + csr_der = _cmd(["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error") + _send_signed_request(order['finalize'], {"csr": _b64(csr_der)}, "Error finalizing order") + + # poll the order to monitor when it's done + order = _poll_until_not(order_headers['Location'], ["pending", "processing"], "Error checking order status") + if order['status'] != "valid": + raise ValueError("Order failed: {0}".format(order)) + + # download the certificate + certificate_pem, _, _ = _do_request(order['certificate'], err_msg="Certificate download failed") + log.info("Certificate signed!") + return certificate_pem + + +if __name__ == "__main__": # 文件验证调用脚本 + + os.chdir("/www/server/panel") + if not 'class/' in sys.path: + sys.path.insert(0,'class/') + import public + + data = json.loads(sys.argv[1]) + print (data) + sitedomain = data['siteName'] + path = data['path'] + public.ExecShell("mkdir -p {}".format(path)) + KEY_PREFIX = os.path.join(path, "privkey") + ACCOUNT_KEY = os.path.join(path, "letsencrypt-account.key") + DOMAIN_KEY = os.path.join(path, "privkey.pem") + DOMAIN_DIR = data['sitePath'] + DOMAINS = data['DOMAINS'] + DOMAIN_PEM = KEY_PREFIX + ".pem" + DOMAIN_CSR = KEY_PREFIX + ".csr" + DOMAIN_CRT = KEY_PREFIX + ".crt" + DOMAIN_CHAINED_CRT = os.path.join(path, "fullchain.pem") + if not os.path.isfile(ACCOUNT_KEY): + public.ExecShell('''openssl genrsa 4096 > "{}" '''.format(ACCOUNT_KEY)) + print ("Generate account key...") + if not os.path.isfile(DOMAIN_KEY): + public.ExecShell('''openssl genrsa 2048 > "{}" '''.format(DOMAIN_KEY)) + print ("Generate domain key...") + OPENSSL_CONF = "/etc/ssl/openssl.cnf" + if not os.path.isfile(OPENSSL_CONF): + OPENSSL_CONF = "/etc/pki/tls/openssl.cnf" + if not os.path.isfile(OPENSSL_CONF): + sys.exit(public.GetMsg("ACCEPT_SSL_ERR6")) + + DOMAIN_CSR_shell = '''openssl req -new -sha256 -key "{}" -subj "/" -reqexts SAN -config <(cat {} <(printf "[SAN]\\nsubjectAltName=%s" "{}")) > "{}" '''.format(DOMAIN_KEY, OPENSSL_CONF, DOMAINS, DOMAIN_CSR) + public.WriteFile(os.path.join(path, "DOMAIN_CSR_shell"), DOMAIN_CSR_shell, mode="w") + result = public.ExecShell('''cd {} && chmod +x DOMAIN_CSR_shell && bash DOMAIN_CSR_shell'''.format(path, )) + print ("Generate CSR...{}".format(DOMAIN_CSR)) + if result[1]: + sys.exit(result[1]) + if os.path.isfile(DOMAIN_CRT): + public.ExecShell('''mv "{}" "{}-OLD-$(date +%y%m%d-%H%M%S)" '''.format(DOMAIN_CRT, DOMAIN_CRT)) + + DOMAIN_DIR = os.path.join(DOMAIN_DIR, ".well-known/acme-challenge/") + public.ExecShell('''mkdir -p "{}" '''.format(DOMAIN_DIR)) + LOGGER.setLevel(LOGGER.level) + signed_crt = get_crt(ACCOUNT_KEY, DOMAIN_CSR, DOMAIN_DIR, ) ########## + public.WriteFile(DOMAIN_CRT, signed_crt, mode="w") + signed_pem_path = os.path.join(path, "lets-encrypt-x3-cross-signed.pem") + if not os.path.isfile(signed_pem_path): + req = requests.get(url="https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem", verify=False) + public.WriteFile(signed_pem_path, req.content, mode="w") + public.ExecShell(''' cd {} && cat "{}" lets-encrypt-x3-cross-signed.pem > "{}" '''.format(path, DOMAIN_CRT, DOMAIN_CHAINED_CRT)) + print ("New cert: {} has been generated".format(DOMAIN_CHAINED_CRT)) + # time.sleep(5) + # 重载Web服务配置 + if os.path.exists('/www/server/nginx/sbin/nginx'): + result = public.ExecShell('/etc/init.d/nginx reload') + if result[1].find('nginx.pid') != -1: + public.ExecShell('pkill -9 nginx && sleep 1'); + public.ExecShell('/etc/init.d/nginx start'); + else: + result = public.ExecShell('/etc/init.d/httpd reload') diff --git a/class_v2/log_analysis_v2.py b/class_v2/log_analysis_v2.py new file mode 100644 index 00000000..ceeead1e --- /dev/null +++ b/class_v2/log_analysis_v2.py @@ -0,0 +1,300 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: lkq +# | +# | 日志分析工具 +# +------------------------------------------------------------------- +import os +import time + +import public +from public.validate import Param + + +class log_analysis: + path = '/www/server/panel/script/' + log_analysis_path = '/www/server/panel/script/log_analysis.sh' + + def __init__(self): + if not os.path.exists(self.path + '/log/'): os.makedirs(self.path + '/log/') + if not os.path.exists(self.log_analysis_path): + log_analysis_data = r'''help(){ + echo "Usage: ./action.sh [options] [FILE] [OUTFILE] " + echo "Options:" + echo "xxx.sh san_log [FILE] Get the log list with the keywords xss|sql|mingsense information|php code execution in the successful request [OUTFILE] 11" + echo "xxx.sh san [FILE] Get list of logs with sql keyword in successful request [OUTFILE] 11 " +} + +if [ $# == 0 ] +then + help + exit +fi + +if [ ! -e $2 ] +then + echo -e "$2: log file does not exist" + exit +fi + +if [ ! -d "log" ] +then + mkdir log +fi + +echo "[*] Starting ..." + +if [ $1 == "san_log" ] +then + echo "1">./log/$3 + echo "Start getting xss cross-site scripting attack logs..." + + grep -E ' (200|302|301|500|444|403|304) ' $2 | grep -i -E "(javascript|data:|alert\(|onerror=|%3Cimg%20src=x%20on.+=|%3Cscript|%3Csvg/|%3Ciframe/|%3Cscript%3E).*?HTTP/1.1" >./log/$3xss.log + + echo "Analysis logs have been saved to./log/$3xss.log" + echo "Scan to attack count: "`cat ./log/$3xss.log |wc -l` + echo "20">./log/$3 + + + echo "Start getting sql injection attack logs..." + echo "Analysis logs have been saved to./log/$3sql.log" +grep -E ' (200|302|301|500|444|403) ' $2 | grep -i -E "(from.+?information_schema.+|select.+(from|limit)|union(.*?)select|extractvalue\(|case when|extractvalue\(|updatexml\(|sleep\().*?HTTP/1.1" > ./log/$3sql.log + echo "Scan to attack count: "`cat ./log/$3sql.log |wc -l` + echo "40">./log/$3 + + echo -e "Start getting related logs such as file traversal/code execution/scanner information/configuration files" + grep -E ' (200|302|301|500|444|403) ' $2 | grep -i -E "(\.\.|WEB-INF|/etc|\w\{1,6\}\.jsp |\w\{1,6\}\.php|\w+\.xml |\w+\.log |\w+\.swp |\w*\.git |\w*\.svn |\w+\.json |\w+\.ini |\w+\.inc |\w+\.rar |\w+\.gz |\w+\.tgz|\w+\.bak |/resin-doc).*?HTTP/1.1" >./log/$3san.log + echo "Analysis logs have been saved to./log/$3san.log" + echo "Scan to attack count: "`cat ./log/$3san.log |wc -l` + echo "50">./log/$3 + + + echo -e "Start getting the php code execution scan log" + grep -E ' (200|302|301|500|444|403) ' $2 | grep -i -E "(gopher://|php://|file://|phar://|dict://data://|eval\(|file_get_contents\(|phpinfo\(|require_once\(|copy\(|\_POST\[|file_put_contents\(|system\(|base64_decode\(|passthru\(|\/invokefunction\&|=call_user_func_array).*?HTTP/1.1" >./log/$3php.log + echo "Analysis logs have been saved to./log/$3php.log" + echo "Scan to attack count: "`cat ./log/$3php.log |wc -l` + echo "60">./log/$3 + + + echo -e "The number and value of the most visited ip is being counted" +# cat $2|awk -F" " '{print $1}'|sort|uniq -c|sort -nrk 1 -t' '|head -100 + awk '{print $1}' $2 |sort|uniq -c |sort -nr |head -100 >./log/$3ip.log + echo "80">./log/$3 + + + echo -e "The number and value of the url of the most visited request interface is being counted" + awk '{print $7}' $2 |sort|uniq -c |sort -nr |head -100 >./log/$3url.log + echo "100">./log/$3 + + +elif [ $1 == "san" ] +then + echo "1">./log/$3 + echo "Start getting xss cross-site scripting attack logs..." + grep -E ' (200|302|301|500|444|403|304) ' $2 | grep -i -E "(javascript|data:|alert\(|onerror=|%3Cimg%20src=x%20on.+=|%3Cscript|%3Csvg/|%3Ciframe/|%3Cscript%3E).*?HTTP/1.1" >./log/$3xss.log + echo "Analysis logs have been saved to./log/$3xss.log" + echo "Scan to attack count: "`cat ./log/$3xss.log |wc -l` + echo "20">./log/$3 + + echo "Start getting sql injection attack logs..." + echo "Analysis logs have been saved to./log/$3sql.log" +grep -E ' (200|302|301|500|444|403) ' $2 | grep -i -E "(from.+?information_schema.+|select.+(from|limit)|union(.*?)select|extractvalue\(|case when|extractvalue\(|updatexml\(|sleep\().*?HTTP/1.1" > ./log/$3sql.log + echo "Scan to attack count: "`cat ./log/$3sql.log |wc -l` + echo "40">./log/$3 + + echo -e "Start getting related logs such as file traversal/code execution/scanner information/configuration files" + grep -E ' (200|302|301|500|444|403) ' $2 | grep -i -E "(\.\.|WEB-INF|/etc|\w\{1,6\}\.jsp |\w\{1,6\}\.php|\w+\.xml |\w+\.log |\w+\.swp |\w*\.git |\w*\.svn |\w+\.json |\w+\.ini |\w+\.inc |\w+\.rar |\w+\.gz |\w+\.tgz|\w+\.bak |/resin-doc).*?HTTP/1.1" >./log/$3san.log + + echo "Analysis logs have been saved to./log/$3san.log" + echo "Scan to attack count: "`cat ./log/$3san.log |wc -l` + echo "60">./log/$3 + + echo -e "Start getting the php code execution scan log" + grep -E ' (200|302|301|500|444|403) ' $2 | grep -i -E "(gopher://|php://|file://|phar://|dict://data://|eval\(|file_get_contents\(|phpinfo\(|require_once\(|copy\(|\_POST\[|file_put_contents\(|system\(|base64_decode\(|passthru\(|\/invokefunction\&|=call_user_func_array).*?HTTP/1.1" >./log/$3php.log + echo "Analysis logs have been saved to./log/$3php.log" + echo "Scan to attack count: "`cat ./log/$3php.log |wc -l` + echo "100">./log/$3 + +else + help +fi + +echo "[*] shut down" +''' + public.WriteFile(self.log_analysis_path, log_analysis_data) + + def get_log_format(self, path): + ''' + @获取日志格式 + ''' + f = open(path, 'r') + data = None + for i in f: + data = i.split() + break + f.close() + if not data: return False + if not public.check_ip(data[0]): return False + if len(data) < 6: return False + return True + + def log_analysis(self, get): + ''' + 分析日志 + @param path:需要分析的日志 + @return 返回具体的分析结果 + @ 需要使用异步的方式进行扫描 + ''' + # 校验参数 + try: + get.validate([ + Param('action').String(), + Param('path').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + path = get.path + log_path = public.Md5(path) + serverType = public.get_webserver() + if serverType == "nginx": + pass + elif serverType == 'apache': + #path = path.strip("-access_log") + '-access_log' + pass + elif serverType == 'openlitespeed': + # path = path.strip("_ols.access_log") + '_ols.access_log' + # return public.ReturnMsg(False, 'openlitespeed is not supported yet') + return public.fail_v2('openlitespeed is not supported yet') + + # public.print_log("path1:{}".format(path)) + # public.print_log("serverType:{}".format(serverType)) + + if not os.path.exists(path): return public.return_message(-1,0, 'No log file') + if os.path.getsize(path) > 9433107294: return public.return_message(-1,0, 'The log file is too large!') + if os.path.getsize(path) < 10: return public.return_message(-1,0, 'log is empty') + # public.print_log("log_path{}".format(log_path)) + # public.print_log("self.log_analysis_path{}".format(self.log_analysis_path)) + # public.print_log("path{}".format(path)) + if self.get_log_format(path): + public.ExecShell( + "cd %s && bash %s san_log %s %s &" % (self.path, self.log_analysis_path, path, log_path)) + else: + public.ExecShell("cd %s && bash %s san %s %s &" % (self.path, self.log_analysis_path, path, log_path)) + speed = self.path + '/log/' + log_path+".time" + public.WriteFile(speed,str(time.time())+"[]"+time.strftime('%Y-%m-%d %X',time.localtime())+"[]"+"0") + return public.return_message(0,0, 'Start scan successful') + + def speed_log(self, get): + ''' + 扫描进度 + @param path:扫描的日志文件 + @return 返回进度 + ''' + # 校验参数 + try: + get.validate([ + Param('path').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + path = get.path.strip() + log_path = public.Md5(path) + speed = self.path + '/log/' + log_path + if os.path.getsize(speed) < 1: return public.return_message(-1,0, 'log is empty') + if not os.path.exists(speed): return public.return_message(-1,0, 'The directory was not scanned') + try: + data = public.ReadFile(speed) + data = int(data) + if data==100: + time_data,start_time,status=public.ReadFile(self.path + '/log/' + log_path+".time").split("[]") + public.WriteFile(speed+".time",str(time.time()-float(time_data)) + "[]" + start_time + "[]" + "1") + return public.return_message(0,0, data) + except: + return public.return_message(0,0, 0) + + def get_log_count(self, path, is_body=False): + count = 0 + if is_body: + if not os.path.exists(path): return '' + data = '' + with open(path, 'r') as f: + for i in f: + count += 1 + data = data.replace('<', '<').replace('>', '>') + i.replace('<', '<').replace('>', '>') + if count >= 300: break + return data + else: + if not os.path.exists(path): return count + with open(path, 'rb') as f: + for i in f: + count += 1 + return count + + def get_result(self, get): + ''' + 扫描结果 + @param path:扫描的日志文件 + @return 返回结果 + ''' + path = get.path.strip() + log_path = public.Md5(path) + speed = self.path + '/log/' + log_path + result = {} + if os.path.exists(speed): + result['is_status'] = True + else: + result['is_status'] = False + if os.path.exists(speed+".time"): + time_data, start_time, status = public.ReadFile(self.path + '/log/' + log_path + ".time").split("[]") + if status == '1' or start_time==1: + result['time']=time_data + result['start_time']=start_time + else: + result['time'] = "0" + result['start_time'] = "2022/2/22 22:22:22" + if 'time' not in result: + result['time'] = "0" + result['start_time'] = "2022/2/22 22:22:22" + result['xss'] = self.get_log_count(speed + 'xss.log') + result['sql'] = self.get_log_count(speed + 'sql.log') + result['san'] = self.get_log_count(speed + 'san.log') + result['php'] = self.get_log_count(speed + 'php.log') + result['ip'] = self.get_log_count(speed + 'ip.log') + result['url'] = self.get_log_count(speed + 'url.log') + return public.return_message(0,0,result) + + def get_detailed(self, get): + + # 校验参数 + try: + get.validate([ + Param('path').String(), + Param('type').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + path = get.path.strip() + log_path = public.Md5(path) + speed = self.path + '/log/' + log_path + type_list = ['xss', 'sql', 'san', 'php', 'ip', 'url'] + if get.type not in type_list: return public.return_message(-1,0, 'Type mismatch') + if not os.path.exists(speed + get.type + '.log'): return public.return_message(-1,0, 'Record does not exist') + return public.return_message(0,0,self.get_log_count(speed + get.type + '.log', is_body=True)) diff --git a/class_v2/logsModelV2/base.py b/class_v2/logsModelV2/base.py new file mode 100644 index 00000000..89ad5b5c --- /dev/null +++ b/class_v2/logsModelV2/base.py @@ -0,0 +1,141 @@ +#coding: utf-8 +import os,sys,time,json +panelPath = os.getenv('BT_PANEL') +if not panelPath: + panelPath = "/www/server/panel" +os.chdir(panelPath) +if not panelPath + "/class/" in sys.path: + sys.path.insert(0, panelPath + "/class/") +import public,re + +class logsBase: + + def __init__(self): + pass + + + def find_line_str(self,_line,search): + """ + @name 查找字符串 + """ + if search: + if _line.lower().find(search.lower()) != -1: + return True + else: + return True + return False + + + def return_line_area(self,logs_list,ip_list): + """ + @name 日志行返回归属地 + """ + if len(logs_list) <= 0: return logs_list + n_data = '\r\n'.join(logs_list) + res = public.get_ips_area(ip_list) + for ip in ip_list: + area = 'Unknown' + if 'status' in res: + area = '**** (Professional version exclusive)' + elif ip in res: + area = res[ip]['info'] + n_data = n_data.replace(ip,'{}({})'.format(ip,area)) + log_list = n_data.split('\r\n') + return log_list + + def GetNumLines(self,path, num, p=1,search = None): + """ + @name 取文件指定尾行数 + @param path 文件路径 + @param num 取尾行数 + @param p 当前页 + @param search 搜索关键字 + @return list + """ + pyVersion = sys.version_info[0] + max_len = 1024 * 128 * 1024 + try: + from html import escape + if not os.path.exists(path): return "" + start_line = (p - 1) * num + count = start_line + num + fp = open(path, 'rb') + + buf = "" + fp.seek(-1, 2) + if fp.read(1) == "\n": fp.seek(-1, 2) + data = [] + total_len = 0 + b = True + n = 0 + + for i in range(count): + while True: + newline_pos = str.rfind(str(buf), "\n") + + pos = fp.tell() + if newline_pos != -1: + if n >= start_line: + line = buf[newline_pos + 1:] + + is_res = True + if search: + is_res = False + if line.find(search) >= 0 or re.search(search,line): + is_res = True + + if is_res: + line_len = len(line) + total_len += line_len + sp_len = total_len - max_len + if sp_len > 0: + line = line[sp_len:] + try: + data.insert(0, escape(line)) + except: + pass + buf = buf[:newline_pos] + n += 1 + break + else: + if pos == 0: + b = False + break + to_read = min(4096, pos) + fp.seek(-to_read, 1) + t_buf = fp.read(to_read) + if pyVersion == 3: + try: + if type(t_buf) == bytes: t_buf = t_buf.decode('utf-8',errors='ignore') + except: + try: + if type(t_buf) == bytes: t_buf = t_buf.decode('gbk',errors='ignore') + except: + t_buf = str(t_buf) + buf = t_buf + buf + fp.seek(-to_read, 1) + if pos - to_read == 0: + buf = "\n" + buf + if total_len >= max_len: break + if not b: break + fp.close() + result = "\n".join(data) + + if not result: raise Exception('null') + except: + result = '' + if len(result) > max_len: + result = result[-max_len:] + + try: + try: + result = json.dumps(result) + return json.loads(result).strip() + except: + if pyVersion == 2: + result = result.decode('utf8', errors='ignore') + else: + result = result.encode('utf-8', errors='ignore').decode("utf-8", errors="ignore") + return result.strip() + except: + return "" \ No newline at end of file diff --git a/class_v2/logsModelV2/ftpModel.py b/class_v2/logsModelV2/ftpModel.py new file mode 100644 index 00000000..1b3a187b --- /dev/null +++ b/class_v2/logsModelV2/ftpModel.py @@ -0,0 +1,944 @@ +QRASP55VO/1DQ98p1csw9A== +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +5G1X0WJyak7IcriKROwRfg== +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +ZuLVraRZUj2RhJbyALJsxNMuhZe1AKXjIZKX1s/bahYi6PnNCfy1Qv0MKdv1oxSXYGJY2PXP7NLNatPW4VSB/zJjWGw5HqJLpeYo/MseDrI= +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +mlbcpvT0kUVOtns2yTGydDt4+07xDvuvRqS/JgkUon3SrcxUlm2Z4rG4Pa9PwOq+ +PsDvd5tup7sspheXmdBjJ9/VPPpQm3Z93wJWBqKcwhPstZtP/mNaMKeE886qnOocqTecg7HmhY5phLEK5HEF6dZHSv0Wlj6b6xPrN6rZ/hs= +piG6BsMF31u4R4iA6R4SRA== +aB1r/Hl41eeDYHjvKNsDN+fwXnHZ5cKnjNQNkvt811Q= +bDrBrYFBEwTqHDQlSf5Wqw== +1u+XjG/2+GSQRv6EzCaWRQ== +3uNPAHzfs+GjC65Wj2Rt7Yr1FmDO/WvtwI9ACTTXaaw2ZZ+bL2BzJX0KRbZ2iXJ+ +Te0vaV2Be4O1NKtbTHTSEPuSBcLm0OszybVF5j5WyuMd3w1YS1N5di1Jdb7cS5FvWbPkfy+FymquFYYQfQohUw== +iOUsBGgOHEqzdAdQgfAp4trfzBztc5RmfOjqgmGickU= +dHeACZYMIKy/ZtmfGaPh9p/FhkuRMVl3Jcf2sj3yqruCd9kzPl26Pj9O4O5lNFVR +9/AEQy6sYem8wkQNCggJi4i9kWw0XQ381/dYG6HKfZo= +WDL4n6jeJGq0nSa2RyFPYQ== +rpy88ff5JnVuJqXSNlYf+CQ4VKHExseR2LosMAdYEeHGNm80vDZH+2U6FEAnuapk +1u+XjG/2+GSQRv6EzCaWRQ== +f3AsbhS4PaN1B4cUkY9T+A== +vlNl0s/hLJ4MLdvF5a1XATBpmRe09mwg70GHdHdh7mTmKAaerhorUT+w+dAajOtb +MMZtIZZuuuWy0CLBzKekTg== +i8F7pUrlHPBNsIux2MgfYA== +iCCluDm5uh0+pvdt+aoUoztPTvOMK08MeHVxMWJklzY= +Fzf1WPeHT+phfW8FPaBhNw== +iwW5Y10vLm5bol1PMh2K5PcgF/7PR7VaxhpNPLVQXiQ= +iiFBxU1BEtsymTSqJJqljqN1RY1RLiUspBdLrvd0CW0= +ldCxpJy7GYU9Z3wc/nhJ/te4XvT+sJUYSl50ZnBKy3w= +CrJuPSINqYUMS/s5z4adcU1LJqJ2Sojj3C2wjmLuRsM= +Psg6WqdUeMsPYvW+jnYn0evEHl+1XoE/rnCfo+c6WdA= +1z8iprMiHgI6LYZftXGjKDYPI7FVqZWCQ09oWcM3Pag= +o1ftBjL5WX5IAh9xWVdqd9wdMBvoYRdNfl+IAvPWFp8= +QUUBWS4HdgaVs9J11lpeBKBH5NkT6WbMMMnumMF/7Aw= +tfRWYnUvHUwnwQYB+ywLpslVnsH3tMXHAPD/cYoTpT4= +AtAHlguqAHnQsUP0meqLH3VU1D/0sLjQMR/6J/fyRbM= +7MgotmZFCo6+Kg3s1/uhRtwRAi7VKRh0cRfONXMb3yI= +CvGG8CijcRlYyvz5dGomfhXk6DzjYPOBtu63/Veri24= +/PTfuW9bPdAz/6cUQf+E55cP43jnvaceBCRBneohYbA= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Tz7Nk5H535VRrRyTrsKtQL8WpBb8+0Czjnl0ON67RWc= +mRiVQSow7SpouYK3AvqUX4InYpFE5p5We9mVkjhzafw5I8C/twUlpi8ApStCkWUBZkKyKJec53qHlBIhRtCHfMjSjGgd605iQCFzcbbvXtI= +OoCG74q49Zorh8P1XQMewTNdwuwJd3tr6im9hHqDZZMRAiA/OeCQww8qlXt4FxqlcaQ0SkBsgfpoB2Sw/8/fMJB9bSAlRc4jBK5o2FKH6qY= +gSinJxQ2Dl02IUzuK5alezKMVZMklWE7tWiUg2329L/n+lRYCQ9UCFbErLWlw54SDdzf2aW7xcFo9f6yWsFr+Gmxv7QW7ILh8TsYOZ9znNp1PFfZW6D80dgQ9cC19CuCsN/nORHFj+ZzOKgvaEAfFiLoGt0ZpN5vpGxumpW0gpcWn6x7Ldft+bCe8OO/4vq4eE9RKaqjTgoUu0FLyAKLMsUrm9spdqinKdfS+0njcmvjNfpj5WOsg4REfjqMdCjT+eYrQYJpSEhzTFpPXaGYrdCg7TnFabrpVoJ3F5uhjQdkKGWqXHHHV/nbuQtIHnCtT8dwjmznuv5sSlpRCVqsgj9kw0VHrZYCN0PnqZ18QBTTb6aQzq2vcHCtyHbfnTgZYpXapiGZtR0S/U7DzfJCjnAyqRZxt4/wKHbAGYy8PQ8OUSXjFZJkxujMHXXUoxC2tVM7rRO1UyMTyERrP/FjuBzgj8YxTjyaxV4GnCT02jD45RbtdAK6txhyTaDZXcGS +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +NRG8WiBknxy8LZ7uM7Rtwjpor9jB6CRSN8s/tRh0bCGCT3O3yIYveV/kC3whqCOj +g+9Bvcv6QScOWqD1KWI83lDWKDEvFH24UumsH9uh9SSEzyoQuAQyJOm+k2nYRQYDLlXqKegz+5H8/A6yDuU4hTssV9YA2l/qH0zdlTP5IOI= +wgR07xfoapmx6eEnFHXXYsrtutdMRxYffYBJpExAFLL3LFn2eCspav2L+PBokDEhe88eat5Ab0fx9+MVZjZtHQ== +xWoGNWjKGPfI4gq8aHoTfOgPIu3QGZOJ5qEujzh3pzi1Iz2/AHJ8GvmS3bUx5254nOirctK/k6BFZA2v8ayK/EDVY6Wbr2d7jYxq8UAugp4= +uRzTWU2MnopGqqEdWeT6ZU1hlM1Dw8+qKY/ieJj+NsUqvH0CTNguPTeX+Laan8WHKtl7c/+1YA4rF+Fa3pfogwkGJNEhZb49H+rX+WZ7S1s= +/9gfAufUZPDDG57T/QYpxdWPD8TwNFW420xTjTTQBkORDI+ZeKEPsfM5K898UU8g5dR87tixaipTGCcauRrVQA== +QKClQ4XOaAjfVBfstd1swg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +09UuO0tw3kJ5J618hVggcQ== +1G/K7RrekfrCzSxrR6/wTYNSQbc2uz66/PV17HDK7lM= +eLcpLeV/+9uVlQTwkhqIj38TutNj0RQi67eU2eN84NTyZDpBLoXYuINl8COZ7WDtjl8ei1UK7BD0ndqK+UnR8g== +blJaGg6qKrjd1Qn4XklpZzOOb0jMgt63VR2D5mQpOlZ27FWAgwjPVohLfSnV8Fu7 +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +SWfRwL0ogDREfMfCsQquWGscpySWDKBJMwbrv47jhc4UOdewFKdXnycq45J0VXW5 +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +09UuO0tw3kJ5J618hVggcQ== +6d8NLnHX3WuS3g79bJvyhHaf1Y2F1xBjvynu5mqktdM= +cfPwrhJYFivHRkEDhYTVNtiFIOmMRDgvlDsiWwA+DRgsj3+ceY+2hTcyXuBIIfOfx3eHGtbcHMXpTWbDr2J+FA== +ET0y6SUMPE2Ml4dS8viRTJixJpYvSUNaJEQ3wgnwmDR/Dl0Tv2W4MPgVxUJMaUl1 +/VraJuu2EmN1fJjV8ELbStoy+NvUd5C4U3l5SFxwBYqHlGghTAj+KvzQIxH+57GjdeVm46KwnUg95Vg08AS+OA== +n+vWyxFDQEoPBlVkcoK8wY9pKSF5GTVfw5dP9KWL9Mc= +CN5G72nnBtsOlOwm3ZnpgqNZTuTEeStLQSFZw0wzb8LzJ/PEqjYVJPHaVhayM0CWWr8fPDm29tfR2xcYUoXx1xDftTYxgmI84hfdErz0bYQ= +jceVTICIbdeQFZqTJKGutcwURrK8EHYrvAd7eDtpxsLSaZ1ywpAsxVBXsDtUMDU+I79b8EIcEmtjZIH+yBRJYWqbLUSaX4DmhfUdUw8L2eI= +1u+XjG/2+GSQRv6EzCaWRQ== +J24jXQQti6wQv5pCoF2ZrQbQF1j1bF4/QmmX1CwZUVEjmug7a9JAroEXBBTfhYssEYHy8XwASHc+rw3shghfWA== +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIlhC5ovOIKOhQYI+YFVzTxDyBspL/Zz/EA3ekwqZLC22t +eeFiPlXUI0Fos82HYztSmTsAYhvcpYqGtZuapEgzpCBypVgTjJ6W0KJC4ipcw897 +WuRvIBaPNRsVGEMoM4NSTioH6DPOvYqQxMaOqX/7OKFY0pBl49Sar+3IeBmADhmj +Z8UsPk1Q7HtwjRd4g01ryw== +N5xldEoitd9eWUThCvdOTB6LOXhNE9TOsjnkpc/9SMN2L8WJXh22t578ODk/HhdL +oxiSpIWwD4wuBub3KU1/cF5fchu1Vqpk+nU9bn11yzo= +PRG/YRzXVD8iVR3bzEcUnjHro+U+4TucaVNI9ygPWuVU3EqLuxmqZkTk2CyNK7O/ +VmVrGQo2zRokW/ZuO9bN68uCgzKuG5LF3dEm8osmV0HPcWbbdr6DBOymTEvfp0P/ftdl4LLYfLwuPpwsYazhEA== +VmVrGQo2zRokW/ZuO9bN68sblSJDKz0OqxMt0bHs/NUCob2DYRF35HVY0+LpxHBb +iDFAoKi0IJZ7t2pJuBivrw== +l2FJPs4YkAmmok1ulDRuSA== +PRG/YRzXVD8iVR3bzEcUnt6EfhtcL4GrcBoio2oUko9nPAoPP0N4kWpuhhh+oYdV +9zgnmC3+5N9XK5/NRaR4SMVNup0N5fm76yGD/GNlNnU= +sDlCNtGYD6nPzg4cZ+6YAIVaGRKK1htDJsqNdoebWm8= +1V2v8QerKOmubvSxgB4eTCXRIGj4wwBQxqmi+7HBYgg= +PRG/YRzXVD8iVR3bzEcUntHGmZ5qTlWv7ywh0UaUxJzXifYQX4pUcW7z5PPhLeh5 +sDlCNtGYD6nPzg4cZ+6YAOK/4Cv++bqYgS9TyEF32cuA5AzQdBq8lyKWQabFTagT +1D18KWr2hdVsBcdZx1OaPWkr8ytK7qVWaaza8e1vU6k= +VmVrGQo2zRokW/ZuO9bN66Vh5+Ci920JkrzQsTHn083JJPLf45hGeTPWU0PgjupO+yaDkxvgzGWeJhaeVem9Wq+8oeyKDNsYcPKXT94yeZ0UymKFveKN91eF38ApXJp2 +VmVrGQo2zRokW/ZuO9bN625bd4BNT0j+VcP1nTIY6XaDTRy5+5AWHUZPakkiglNT +VmVrGQo2zRokW/ZuO9bN69TcL00if5hvJ8+IR0VhUuvoRwQz2rzPWjBJm1Mk3z5a +VmVrGQo2zRokW/ZuO9bN62AVcq3plEcj4rxrsvQ9pPDU08M8azrqNxOOot3ghWHctbl7KH0YZqLimlQWQDerlojzSftx5vWNNRCgaO4RyF8= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkniyOkKSi5II/qGABbjSbB5nq48GQoMvYorQ05/VR43Qg== +VmVrGQo2zRokW/ZuO9bN6/wMRobeZASkZ7UgaRv/zbQEquXghr8pcxoQVAx9J5G/e9b3CtH5KoqK0sb3R99OsA== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66Vh5+Ci920JkrzQsTHn083JJPLf45hGeTPWU0PgjupOJvco2FZSOBWgiEqCcFM7Q/vW4PqCZLGYaAAy6qf2tko= +VmVrGQo2zRokW/ZuO9bN625bd4BNT0j+VcP1nTIY6XaDTRy5+5AWHUZPakkiglNT +VmVrGQo2zRokW/ZuO9bN69TcL00if5hvJ8+IR0VhUuvoRwQz2rzPWjBJm1Mk3z5a +VmVrGQo2zRokW/ZuO9bN62AVcq3plEcj4rxrsvQ9pPDU08M8azrqNxOOot3ghWHctbl7KH0YZqLimlQWQDerlpd+8N/8ymIPDrStsyiyEo4= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkniyOkKSi5II/qGABbjSbB5nq48GQoMvYorQ05/VR43Qg== +VmVrGQo2zRokW/ZuO9bN6/wMRobeZASkZ7UgaRv/zbQEquXghr8pcxoQVAx9J5G/e9b3CtH5KoqK0sb3R99OsA== +ojv9MyHa70Aio7SDrWcjwSYYqgfeHLgPvpjtVJfUmX0s6OSNU+WrIf9mNVPQ5TfqPavTidvffxVkcshQNOCsRA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklNjZdFug/ngOpMcvCKxEBrAN68NaDKAtp2GLglVerAbQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkllTuznFui81R4CASaEKJI9 +5wsVwKLrIjIWqeTNpSVp9YtfZPXXv5krNmxeXAjq+NQ= +1u+XjG/2+GSQRv6EzCaWRQ== +JKgfL+COM6qpIwe0Wm0zY0qrBwJPhJivgZdbq2ek/MNyEskG/o3RxQuD1murI70P +Z8UsPk1Q7HtwjRd4g01ryw== +9JiWXiRNWDTg+XstQ0v3tAQjzhKVhg9yH4FpL7cmNHMby1dZvf+KLKXDFxiL/GyHwt+mo7J9Eh26MMHXk7dZ7A== +vyYQukiyWticSXOwUY8MiznZqoLnnCC/RUpXDOGxEsg= +ho/Q+jrWDtBeg9J9ZTnKizg5CYSqIH+8noUWvD9u7YuJKKfD4Zw1m+s2/8nfOg5c +Z8UsPk1Q7HtwjRd4g01ryw== +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +jEPV0YXDCqWCap/VHplvwQ== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN63+n53I33eehKLN5KvUF/njST1L3SsbP+AH0sXCoBqB8 +1u+XjG/2+GSQRv6EzCaWRQ== +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +QKClQ4XOaAjfVBfstd1swg== +c0jePRxtTVZYop75Q5JCFonnV+X8W6zX0oXQ+sQuZtHIAMgLt2jbmRWY4GrPgmiW +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5hzukpfx2w2sPIWvGOQMWA5Yh7rnfJ/ffdahL7iA8laA/3JDQRVCFmm1vTiYAMYeY9 +mDRkPFp/j8KULnXRVcN0LwEwcSJVnbuC2hmp56pZJfTUXblgm3kKERqiCPQ5WxXb +Mmz9tB5RaVIWQerkiDs2BePvXl4dNWSjl+vJFVGrvpac5kzICL/kBoClsURQxOPYTWXamhuvoo8Pwhw/Asx8vw== +wgR07xfoapmx6eEnFHXXYtX+pH+0NWr1LYbDUaw1poloo/SQreGe/i/geKkMYIe0 +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h66+MS1fyMgg4QbFqgaS5zM/xrhgXnbF7R0CfQQ1+NiPMSpG+gKOD6M2tp/y5za8ffjQSppN05X0LDqFQT9a5wOIgQJ/Yjmvbxvy5ZgSQkXThA61IOiX3O5DDhxAVwkrvF8psbn0BsLpymYmwMFV2qSiF5R3vp1Sxyl3hZW76qa52u/AlZqi2oGeEktYAA7i8pAzq77pBfMPsuV9ZAavvwmLmdr5nMrH4MCrkdZr1OBQqEOTFZy4Rtp8jRZoc9O3Ee1hevDInlv+qb3Eovojc9EXGFueUnVktB49vv6OTSgCbfCkr3GbC5cEOllSeNviaxJK8u8hN2MIBF/7ShdNspSVs88cblgg0AlY7k/1tkPo= +R14ZYm6mELJXNFBxHSBttdYfQPT7lYz3sNsgm+2cAVEVZqqsWoLADHy1tzgaDLwVuLUQU00i5jhUkarevZBgbw== +ojcUvW+Z2ehEJ6yMJpmY+2Z20VIYbVHcvJGptU0TEL4= +SSb4svay1O2En913Q8HWTI5XiKI6KpCz9PP4OKkiVgIZ9PAiv190X/Q6cvCmIvEn +O3CUgrw2GJfB+mDjH5+NdgM4cG182DjbaaqgGRQTDbbHFEBxIQHWpD3HPFUKruz8 +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTGW+VwmHBYIXijdBPs10gzVT12YFp5JZnl163bbcgFbA== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTGW+VwmHBYIXijdBPs10gzN7zADUA2hNwQQDVNbVbGVg== +13vYlKuO6A3n1KjBvCLS2cZOnSAYizlRW6NVt55Nvpg= +Rc/EJuEm/I9INM8fD5RDm8V6NM+R609UAWBHMDxIGH1KB0feE4DfP+R5GNLxg7AM +xWoGNWjKGPfI4gq8aHoTfBMUl5q05yLZWa9BqWZ+hHXNzfrvrNJv7lUhLv58UG50furKaKDp9/7/vwDMn7xgPA== +32pdC9DD05OE2l0oXazDFBLSw6QGkeD2EftJ95N/ZFU= +9Ky7E5Jxg7bFh45xr+PtLAQzRENCwucwjn+oXpXkeQE= +Rc/EJuEm/I9INM8fD5RDm8V6NM+R609UAWBHMDxIGH3FuQzm6WS6LLN0bq9R/qzA +O3CUgrw2GJfB+mDjH5+NdgM4cG182DjbaaqgGRQTDbbHFEBxIQHWpD3HPFUKruz8 +VmVrGQo2zRokW/ZuO9bN6zRl0iz3kGEFiV+wGJc1Rx+/HiOt/vX0mu63k3Gx19QQ +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJmq0nitHFfMxYtPtCzV22AE +VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUj6vOCoGShzUiscvkZRGEAKA7uR2yfJ7pXV25tjsB1b0iTAkZkAH5X+kEWvVNPVuCdYOnp7yO4pDLE8Ou2gjvWF +32pdC9DD05OE2l0oXazDFFXAy1MI/uPhakkzCdBdnMA= +4+wKB+Tel1iow8au3cOIJaTLjj3pysITCcJJ7ec43Sg8lqI/J7o8CtRAAtYI0AOu3JqoT8kI2t89ug8sZ1eCiQ== +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz7ZP5iIPcKimqUgUTHrSwkPOkVRNyKSBWUmtjLo6QNHEw== +1u+XjG/2+GSQRv6EzCaWRQ== +f+rqXRH5RpetwpbV8cB3LCQvH1cZGFXTjIrpGpfmiKB6I3CNNQHjXn+gKCasbU3WpehGERRGCkPUVijQbiz2hw== +42x4h97guh1rdm+jSDLx6WG71j+mb/kcr/EympinqGaL52MoXiK9EGabcAvWXPj/ +baycSy0dQ1Tu5e0z9Bp/lBxrTYjvAm+ccP8cFcEA8kK8O9TSDmsclixdCnVohLz9 +i9KLGXWhztWqpUW2TKI0t6Wh1fnUn6aXVqQKMbQ4Wwq24SjFcYWkCZoDKbV6MU6m +02RO8UnD41NXqUboGzyMNHX0JyR3Zt1dder6kkQ/0fvZhRxdM9NaaFhg8R9lJRcz +Q6ELX42Y0YS1qAvk/d/Oxw+XB1Oh4+oUsugYkWWAiYJVAc/gs1Z7+2AYi+OjKljk +UrD3vTXhRT2EnV+D/UAcyj4KlLaoTR0ZzVfFP7zcDh8qBFLqQVHnaVRpUcy3VNxf +fqNjaa/44JvPoTCcxz6RwKlIK34sd6QvNpIK085yp6fonLWQWHduUTCWz3l21/d0 +lNSpMXSSdsztjsXj5hxacANS7WSJ9fZBItR9FLQQCeRQ4sSSTGWKMGLiiXMpS2R/S9MpuAbK0NcchyUYHrjK0g== +1u+XjG/2+GSQRv6EzCaWRQ== +f+rqXRH5RpetwpbV8cB3LNb4H/TF90wkaKs48d1M7L+avSymddn7mKYz0NItjsSS +Z8UsPk1Q7HtwjRd4g01ryw== +6Jpks4lpI/jb7zQY9ZwZK1zkFQHFLO1SftfqDQPgq1EBsFoAqyniZa+4+xJKzZfT +Z8UsPk1Q7HtwjRd4g01ryw== +IOQ7R1QbtHtN/omAE4BWfAjZgPJASxuTDruVD3whaNc= +b4OJVZe8QyIpjuTpKXDL9A== +wlLHv6kT3Q/RmtMBN4nDAQ8YSs8Zt0gDBRyWHhM8ed+GGdWzPmL+jZ6S+F5YV+22 +VmVrGQo2zRokW/ZuO9bN6xjRZ/jgghl2/5XRKMIuEuFfgv1283QcodM/1ZOCBV+E +VmVrGQo2zRokW/ZuO9bN6x+Y6lBOlB/5zr7yqielcHbAt1j/vhhHLiR0XLFVJ4MYIB7qEVFJCIX6M64zazFiQeEMWR6HA8xoXyo6nAZJhlk= +VmVrGQo2zRokW/ZuO9bN6zKW75ke8QGqFd99GlEdhOmQAXdMhXbfkO0SVCSKXZhAgrRq5E4FlbZVLyjCi8XIzw== +VmVrGQo2zRokW/ZuO9bN6/VA0tcNP0B0yWPLZItZlkbpytNLRqAe/ddM0lgyhD/+AVdhq9Y31ZAG+aChEtUpGjK+qk8XXC1AHcYIovJNZ+PRowAmFi0HJrFUfIQN6E7Q +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpXsF9xMLoel3FaVnpw+fNuSwrGC7RSUXTIrW2j2yH61A= +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +ltIZHHUgft++nMUXBsBki41iWrVRqmYYsBZXvu1RVWtX5QNGu3kC2spvuS5BMSeI +VmVrGQo2zRokW/ZuO9bN61gUJX+sLdD5E9ziOqrlc35UesKXTRF076vwlWXX2xbP40+TL4fP4+HMWJmoIKgJCIlxfTkDq2jRuIwjBU2zhng= +L0eUthVnpkGsmKFAX6d+uK/AWodUm3mcxHH2qymBAgItW7Ziboyd2OX6uMRxQUgs +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +ltIZHHUgft++nMUXBsBki41iWrVRqmYYsBZXvu1RVWtX5QNGu3kC2spvuS5BMSeI +VmVrGQo2zRokW/ZuO9bN61gUJX+sLdD5E9ziOqrlc35UesKXTRF076vwlWXX2xbP40+TL4fP4+HMWJmoIKgJCIlxfTkDq2jRuIwjBU2zhng= +L0eUthVnpkGsmKFAX6d+uK/AWodUm3mcxHH2qymBAgItW7Ziboyd2OX6uMRxQUgs +1u+XjG/2+GSQRv6EzCaWRQ== +69Uik37T733MFTHEnugne9sIK0q/BrAEinbmIsH25IyzJWj8GWaHG6Tu8rx0i8YI +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIluXzbJqLPwB2hMZiB6idlsw= +vyYQukiyWticSXOwUY8MiznZqoLnnCC/RUpXDOGxEsg= +ho/Q+jrWDtBeg9J9ZTnKi7Z8oMuCLRtnjC35Cw+4aVDyKydByU+COt6lumOoVZyj +1xJ0clp+39cOWW64WGs9Tw== +Z8UsPk1Q7HtwjRd4g01ryw== +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +jEPV0YXDCqWCap/VHplvwQ== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN6wYljL95v5S+KLH0XvYeRAu7jaBdeUshyw1Me0QcftpN +VmVrGQo2zRokW/ZuO9bN6+eQW79nEpaaEVxkQ5mhMLDYFz3gFM695f1GA6K12DxM +VmVrGQo2zRokW/ZuO9bN67F/Q5IDafbWxan46AKyqXhJkHTWPWMJlgqQQU22Q4EH +VmVrGQo2zRokW/ZuO9bN6/0cM2nc/QcV1pNvNaFvxCS9guiudKGmsn/LBMq+3sAL +1u+XjG/2+GSQRv6EzCaWRQ== +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +avTFp0LCGrKbHzgXWCq/pYahaBNfx164yVsSAVTJDN4lWK0zoj6Eh3CajVgk0hPL +c0jePRxtTVZYop75Q5JCFsMjGu62TZDSljeQjMtCShNsz0Y9SFIv08dPVdTjI95b +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5hzukpfx2w2sPIWvGOQMWA5Yh7rnfJ/ffdahL7iA8laA/3JDQRVCFmm1vTiYAMYeY9 +TzinVBSdupkFZuXDgmwYfiSs5GTHKq13dTr2kdVzFPy0Jh42rAak6FUbANl5whZO +W7OoNBP/IWbYzUOvpg/slY2NVhp515LKHRWJIoHB+V62OipmiB60YwRsLUoQ2Yhe +ojv9MyHa70Aio7SDrWcjwQ0VSHrTkMcwddE36Wz7lCz16K6ytw4GpaCto2OYzTO/ +IfOrW6dR51K0HWsQz64KwMZLSub5xEzT/cQoJpZiUus= +MzOj4ZiBOJDNVP8vi/lFQIcd3YW2BaDb4kSPJzNVOdlx1Fa3HoeBVOYpiRWE7XX1 +PRG/YRzXVD8iVR3bzEcUno4Z8XHjrzDgKJNn9oJou7lkrRw3h7on47id8HDtKTRy +j7S8zldDFIkYlZuEh7uh1S+jR/V5xbpfKOdCgELD/V8= +yqOVJCkAGOhqcQYBOo7yqReCj9ixiaIB5tdlZ21DAUB9HlVyjl18y9Mfwf2BKrxO1mdZmBSOas0Z9YsNQ75XTg== +jg/rCd1ltkosbKN5ZG2fjyfZDh9bj6LoXpZWnO+Jm3I= +kTToPYSGc065HLDqbjV0HFp+qk2oCg3rcMPzGznvj4s= +UsNFlHPXrsQOqjn4RzHaA2d+Dsggsz8fegt1C0el50M= +m5EI3+BXxkQEYKYaxORD+qnAwk1i08GBypJtuz97N8s= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTFLJVKokCn/n7cBzRCyjPmemCDNpavHplLKoYC5ZrVB11XPhYbqw8m0ksUK1fSNNJw== +k1ktu/l6HFULD7Vyr8Cv0HMlLvOl3L1DvBU4YpKX0wAfHx0c0egKY2YtT74Sqa68k3g5TvkiAzu1NRNlkcoBZw== +rtgmVFvoY2d3wTFk50ruC6zmYmoALMFLQOc+xc+vXmJzN60uRcu12WAXC28NrZ2x +wlLHv6kT3Q/RmtMBN4nDAesM/ZGOfx7RLjx/t+u4GQE= +VmVrGQo2zRokW/ZuO9bN692Dxe6/pJb49NcEGwdYFHp1ykWA3uXz8Dyrp2BHgNzY +VmVrGQo2zRokW/ZuO9bN65W0SAM+Lz+ixIA1yfBIlyIfbjLiw8kUEWgRJErCtT/B +VmVrGQo2zRokW/ZuO9bN63sbeay11c+Azj5VEIm5hpFeadOrAd8WGDCLEKR35dm2edAnPxHas9j394knOwJ8NA== +VmVrGQo2zRokW/ZuO9bN68NGdAuCULKcdnQ9QIPhYF2d+4WoU/gSRVS6rllapIH/JcXSlRMgq/Lahda1FjioQA== +VmVrGQo2zRokW/ZuO9bN6/D3pD6pMXGNmGG8sGEgUx6XUzf02GbZTCaXui9nRWWO8tkj2O9evUoVTjlZbcp3bw== +VmVrGQo2zRokW/ZuO9bN682I1WfoM5O31Eu4y7eErmS1UbchPX+RbJH8okRw6uaf +VmVrGQo2zRokW/ZuO9bN6wGhkWn/nbtYgnRrfABp3Wx1AshDUYpYVsgVW1JY2KGZFsqdt6fYlLewntOhfc3+yQ== +VmVrGQo2zRokW/ZuO9bN61QRx6pmjScEC6Mg8X6jMiYZxM1o8NnnHqaeAW/oXr0H2CGH5XEtUznATSrKZ2lZEQ== +VmVrGQo2zRokW/ZuO9bN69ytvi1kzpknGdq50rt6Uk+ubFehg+T5L/Wn9jlHITZARQHXl2+Us7ChTWXRkBLidjmBIhghC83F2q0QovPWPAU= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64BzcfYorygF567wBnzhYgdXJpuZabV1BmxgnAUx5+Do +VmVrGQo2zRokW/ZuO9bN6+lw59pqaZzeIqhj6WMFLKB/zu4YnIm31wuOcKLBoPXS +VmVrGQo2zRokW/ZuO9bN63uBz94NTvSB4ZlhYbDfvddgnVY5f5JKOORH7Tw2EYEWgIaiQrzxYa4sTWMTgESGdzp52DXDe+CM2icfFsZP8BQ= +VmVrGQo2zRokW/ZuO9bN6yiRU09M/IrGNAMi1AT8LbUYEEDidCw2lDVT2RUEf/k9qsU1Wf5Nkpaqk5UUK1L+GYF2qfhogkOZK7QKKEBb1pw= +VmVrGQo2zRokW/ZuO9bN61tUoNETw2Gi1D/Pq4qmNf5zhx/EQVRufI8LJkPSSi0Ih/xPpxJTJaW6aikR8po+Gw== +VmVrGQo2zRokW/ZuO9bN60TADvNmgaySXS1m1WJNuPeBE1HygRWbriOlU7U2Ssu3klRVzuCKqRzIyX8uoMGm2Q== +VmVrGQo2zRokW/ZuO9bN6+NxpNK9XYUauw2abZQW3LIA6UmYzalir4YjKFLEIVPk8tEmxjCF+UBrUqEASEZ7LQ== +VmVrGQo2zRokW/ZuO9bN69V5vRMZphCh9ZgEve1d0/wJCCaWNPSysYTVRsjVkHV1kaWCUu2teK5jcDOFuBzPQg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN65ql16YTxrqF/JGVZ1MkynHsDzpubnWpMd5UwEoUiIS0 +VmVrGQo2zRokW/ZuO9bN69qq0Yi+mSeaf9labGycI4pPobf7Rk2EV5UV2RidC6jX +VmVrGQo2zRokW/ZuO9bN63BfTcQ4tGBocvZdkD6/kO+b2BWi71GtoR82tq7ZzboaxwEKuBjLI8hR7w2Vz8MwvmfTeIGeWGGODE9Xm7plcChd9CESFtAhCbsVbUZfRATe +VmVrGQo2zRokW/ZuO9bN6+lw59pqaZzeIqhj6WMFLKD5/dQ70v3ovug8BVS13b+a04fDuz4GqTVSudpfL0PZ3Q== +VmVrGQo2zRokW/ZuO9bN6/JsORMmbxegVkakcQYSuOtPUInnHZyjGgz46mtOv7qSxkpfnDglI1Zm24CHtbp9uw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknACUIF0SHq3Wk7UPJPdvLnBytT0bs3KMa3b9ztxfxoAOuFAPEC6llaTPvEl5nkxFo= +VmVrGQo2zRokW/ZuO9bN6yiRU09M/IrGNAMi1AT8LbUYEEDidCw2lDVT2RUEf/k9qsU1Wf5Nkpaqk5UUK1L+GYF2qfhogkOZK7QKKEBb1pw= +VmVrGQo2zRokW/ZuO9bN61tUoNETw2Gi1D/Pq4qmNf5zhx/EQVRufI8LJkPSSi0Ih/xPpxJTJaW6aikR8po+Gw== +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH6/zCWmOuloAo6ldSTiaAbum79T3ZyCN2uTGYeWaRk8ZDw== +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH6996q3aNX+Q/j+AZP8Qvo1SsqECC6pGPNuHt7GBg6OuU7Z1kO5CyxL0yZW1h1ERJGY= +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH69ya1Zc1M+KUYAJsz6AJ8F9YFPnDyZrQbgW0mxsmgLccg== +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH69tjxVx99Q6mXAvsEky2j6E +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH6+u5zuOiB74Y40ABQeHY7iFHo1pdDHleL7Y/5SOV2+67LgwV08qwlhzwYIvKN6ew2QPQs3KxxYBuYIuf6mUFE4LQcgTOHCghD9G2I43o+9JDw== +VmVrGQo2zRokW/ZuO9bN651w9MrkrO5sEgMcUVTfH693/pJPvxX08JK6/fU1NpPhvWQO/SYGDQsMBTlZKO7NUA== +VmVrGQo2zRokW/ZuO9bN65UApnVIJ5/vPXwprQ1J61CB61rainG+jx/I7BmKhaXNbQAK6argkLKn8aYOSqMyZg== +VmVrGQo2zRokW/ZuO9bN6+NxpNK9XYUauw2abZQW3LIA6UmYzalir4YjKFLEIVPk8tEmxjCF+UBrUqEASEZ7LQ== +VmVrGQo2zRokW/ZuO9bN64RgViN5TsYlk5QzRuGR7Q/OP4DOltXpxjAehZXCVn5t +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +VmVrGQo2zRokW/ZuO9bN6/PX3CxbCMUWnFIJe+bNqSPAMyzrThLalxkC2Swhbmfz +VmVrGQo2zRokW/ZuO9bN6xfwiyo+r5sBtLLY/yC2JQuHAqyNIUWdS8mL+/TK0tDkDYK6sWUkPNuSjWFfywjVYg== +VmVrGQo2zRokW/ZuO9bN6+lw59pqaZzeIqhj6WMFLKB/zu4YnIm31wuOcKLBoPXS +VmVrGQo2zRokW/ZuO9bN63uBz94NTvSB4ZlhYbDfvddgnVY5f5JKOORH7Tw2EYEWfecK1ZYcY5aRkTVMuXTf4H0ornquPXZt6sNbcV9pvdE= +VmVrGQo2zRokW/ZuO9bN69fMKIOt3Fz+VdjDCrbU0LCl+bFI5Faw9xdIi1QHkSiy +VmVrGQo2zRokW/ZuO9bN6yiRU09M/IrGNAMi1AT8LbUYEEDidCw2lDVT2RUEf/k9qsU1Wf5Nkpaqk5UUK1L+GYF2qfhogkOZK7QKKEBb1pw= +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLwPXg3jnnr3ZgO5MqTOchYl +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLymwotzxdwFuDwNOf7s1SnK +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLxqHw0Wm+3Oz89atXNvpO+3VpP8E3wH3+WyCZ1pBSAjAXfw0dWdNxBgtOHo9aHXAAp1ozGIrF4gPkJtrclfqSQW +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLyUNjqRPrStY0n7cNwmxO8c2/xYLBixqHfcg+3JdMiYfw== +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLx3KKJ5e9ptEgMTxJyuDzDUvQprgIXqnEOJyY8Wjzx/7g== +VmVrGQo2zRokW/ZuO9bN69q3A6NsFro7W8cTziR/HLzg8EOHacHSOV2zn2Ehx3MZFJJGKychYjDMMiIIfaMNUg== +VmVrGQo2zRokW/ZuO9bN6zSaI2hF+nruhyzPFkleUq8kEqJLrAmChIOBKZkxcqwkGxR20IThZlGA1Dw2fFbmvg== +VmVrGQo2zRokW/ZuO9bN61yX1Dmgg5/3/nXVEduWvGRjAxUjUGE7S9oN2liQJclq +1u+XjG/2+GSQRv6EzCaWRQ== +M8WJLh8/G3HKYOcF0o/j+ZydjE/y4s9kCBsZH9Ydiys= +wlLHv6kT3Q/RmtMBN4nDAR8Bo0ERF5FTL0peq/6tsQxyyboaug1HqBM4U72iapnH +VmVrGQo2zRokW/ZuO9bN68F9oo0dUBAgf3zfH+UjZ2vBnQt9FOIIe/Mpc+VcY0hPyb7Y4hQhLdZIwPaG+7+qSw== +VmVrGQo2zRokW/ZuO9bN6wO+YLo8joUjMSrMMYsU2gc= +VmVrGQo2zRokW/ZuO9bN6/pQKJ5vd2iaXyIKOVeQFX4bda0D69woaHuXDdZ+7XSF +VmVrGQo2zRokW/ZuO9bN66+oTs/ylSo2IwYwivOcQ5QBeGO3/qhUXmuWRmyY22oicnIskjhngFIDfDK5fq2/pg== +VmVrGQo2zRokW/ZuO9bN6wPyViMJrLgq6QE06AJQ/mAxMxhT6z5+FEPokBQMNnem +VmVrGQo2zRokW/ZuO9bN61Ayy1rssK205MlM60bu6RKTG0Yfp2Lha8/dhaOOY7W18jbXMCbohsZ+i3OCAo0UDg== +VmVrGQo2zRokW/ZuO9bN63oDITMAygWGYYDyry55YDO/Rs3iFoOYCQPRB0Vo6QHG +VmVrGQo2zRokW/ZuO9bN65wPRQBNUUIF+K895b5fOrPxf0bXsfEkNdHCrolD4R0g +VmVrGQo2zRokW/ZuO9bN654Enj8yhWFV20SJ83piZWE= +VmVrGQo2zRokW/ZuO9bN63D/Hc2z6pQlInT4BP7h+Iw= +VmVrGQo2zRokW/ZuO9bN6yUmZOZSHWWMW4cj+zuxv+kqaCT5BWcHH/D3f8p49LGM +1u+XjG/2+GSQRv6EzCaWRQ== +NYuttixIfaRvF0QtYZhNf933obSmTd0Sfv1RLv5CWew= +lcmg3fAsktA/DTr8lB5mRWYUQaXOV6Q27CwrOP6ime0= +/jVx2Sfk8Nv4tQDKNVrh5YQDDOyw39n94XpeN+Fc18Kb/ocx6XA91E4DLjnGHUVOp/hG38KiBHnsJF7rC+FrGh3/Y7Yjh+e73kBNop061ts4ALDF136iqb3NjiK6eB/P +wlLHv6kT3Q/RmtMBN4nDARhCtgHyXaIoGDNPdzxanK1C9qqxbuBKdK1MCBW9e3yd +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN65v9BdTfHDX5h5JoIA/XwnnexMAistxqcEkO+FfI33OD8/CZEhqElXeJdGaAFQ+8dlBsrteF3JVNER2SAEya4ro= +VmVrGQo2zRokW/ZuO9bN60wy8tZH9NZmPUZU3ymx7Rp230YC0uhO/IEmXkv95OC+kYR59ne0kzYbTAnbjiDQU/tfbdzA12veXVBzv74WMwI= +VmVrGQo2zRokW/ZuO9bN6w1QwjDxqY23oBLfy+yRgv/pNEUynCImsZYJyFdRKiY3f9n/tdlMbWaxhorqdeT5IvbcJCzD8QfNSTYnlePbg2dOcteS5jjxFSQ3VLx5bqO0 +VmVrGQo2zRokW/ZuO9bN6xDmLuIA9/MQHFSh5NeXQIQ+yxAa1KwUsFahRjuiZ8Mht4CYtcVKfJe+264KUvItRwPdoTCmQExpQVyY7LXmw64= +VmVrGQo2zRokW/ZuO9bN641hlBSX1Vyx3lcgSR68G0pIVXm8+RXV4rlYHPfD1CE2SXElOHuT06eUM55UWGQlyfgvRR7Tf1LTGZlpUWmDA0U= +VmVrGQo2zRokW/ZuO9bN641hlBSX1Vyx3lcgSR68G0q82MvnkjWxlMLct/yHsboi +VmVrGQo2zRokW/ZuO9bN6yC3ZNj/jGmx0c5FxYA8mMYFovl3/pDr9xKbkD9+awkq +VmVrGQo2zRokW/ZuO9bN6y282QljYt9u/aACctfJxlfOZSOWj+ka4WVOhE7+gYPHIAVL1wqdRYXpc242KdGWJ/V8BbFe5Mk6POcVdzCcc/E= +VmVrGQo2zRokW/ZuO9bN6zj/WJ/j3Gpxi/RO5HrCvbUT/1fI7DpxplEbSq3o7wy5 +VmVrGQo2zRokW/ZuO9bN6yC3ZNj/jGmx0c5FxYA8mMYFovl3/pDr9xKbkD9+awkq +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN6/VW4n84N+xmKyiHoJANXzc= +l2FJPs4YkAmmok1ulDRuSA== +NhnIR3Ilo4H2su9/cTNo/OxBZFmIrDzrnRKQ7FDvUwU= +1u+XjG/2+GSQRv6EzCaWRQ== +YabJCT93a3/IohXaIkr3oSIBZqGhFAEcGH7Ca/SaLLaG+fOp90QxSi85tpj3adUdyNOydRy2vjmM402XZpsX3+nkFnHf2XtWx5xSFzCScjM= +CAhn8dRUItEbEErp4w+lX6BuaW4lR2EIOU/Z+PBe2bh+we0izGB82w9NP/FQvC5X +1u+XjG/2+GSQRv6EzCaWRQ== +oSOaxL22S+/P+2LLtATJSAaArdQ7fQZ1yguM5QAidIUD9pZGaNnuSKZ/6l+iYdvR +Z8UsPk1Q7HtwjRd4g01ryw== +hpr9H7QXrORqNT7P49jOY2MKXv3knmxSuBVQsYMFcyo= +YThduOXrtrzATLnwayQWnd8n5YD62KOD6YsiYJw+RAk= +7JQQDTi0T2idhFjao4jEpWRxFagB+y/O50vW7a0bZYhQWe9QbLzaMInaqQiRp7EGliGMBr4A0vhy+GCOuLQR2g== +7JQQDTi0T2idhFjao4jEpVM5kDnYAXzeJH6AUYrQZAHIQ4Zi/CV9839Nkf/Sc8cE +h2Amc56dmaRo3inGZxN9N4t4UB6ZIYdgw3I8UrLsiK9g6KbQ85/a9qF4mH08k4ge +rErGIG0jHuEUhPzM/Cvn7eDAiXr3299blfSBm1Bfub0= +B3iNup9pdvLGx6CAo6KC6KE/v+ibyUyH8Yud04Lz810= +q4nc/jwATOMUyfSjLibfEenY5RFzfDAlBFkea5rX8Ic= +nhEO6jGYTLTxNd7tfL8haGb6dIE98SqR0mAOJokWbaA= +2gqFvhn8R1IfPGeV2rMswbHUHFXVUzGBLmhsu02wzqE= +1u+XjG/2+GSQRv6EzCaWRQ== +C3JSV38/w2nvM3I7TZ5+4epkOMkaWYvoOErk6ygROHs= +to+v3MAswOY2NqesdSm5E+gSyJU0y7Xs7zqrUefMUuCWwwmO2rJrPN7egFiRQVKU +vJtKpUSg5Ukl4nJbF6QjQKmVnEERhZkbvQk1nyA3oiKLlcyO+IWzbyEkNMowLxjNh4A4GzAgdksU4IKrFkilFw== +m64JmVQ8ZxnJzJGTbbSLFgYYjwLvqe5k1J852jWCJ20= +GDxnwLueKeYNT0/dIp4TaMZIrCOXt9Fadk7QEvmi3LY= +iZaIsa48RXfE+uf/yF/rQBFFBTbLDCVgmOpzzd9fNSoU5yqcis6eMa2AhGtSBk42 +8+kEPw2kbWRhVZANSTiFn91S4x0IfqnYB21Go0MrYHk= +ed2GXKkXmwU2EQk+6MUOb5wVZ0JLOm9I7uChKGZVytg= +ojcUvW+Z2ehEJ6yMJpmY+4qFMPDKWboTeo1Y4LE86Gs= +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +YSlit6erKXylrjSZnvyW8KJQ5WBrDhjuDH3szUt3rF4TtLL3u+2vhmfGxql7dElVlZ+FltHCrJLtURHR876Z+5QJwoIiDk96EuxXOkKMTdk= +DaK4moDemtv24P20mOaUzQ== +YSlit6erKXylrjSZnvyW8F3ElaLeR/Rlppv/IAE6pDs= +9zgnmC3+5N9XK5/NRaR4SOWz2y/YEwWVWSZltrwvNrSfE3PSPwQMDu0BXDhBm14C +1V2v8QerKOmubvSxgB4eTCxJBqBlZI1ptPijcUHCDz5sYoASyVMieGEeAAQXWrck +1D18KWr2hdVsBcdZx1OaPfHgcwFUa7nBQO7bfH5tm74XTDXVMzBwLoLCrGgXhe0X +fqq/Y0gPbKQeEXzqLGS0q/J+017f+uH388qCfSeGlh8= +o6QhOIN2Sc4SHELnst17uYigovIGLGw9Sk8MSoNStgLGPEqp/JNtpdjWEUUK4pQG +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz5EhzAUCM9U/skPlFPtVQrio/gP457Hr1SiCbunQ3L5/g== +1u+XjG/2+GSQRv6EzCaWRQ== +lkFGPJ6keZ1Jd10HSdN0i5ZJfFnQV/nGEGCTaGt06s58zYz056g6djTo00sC9scb +Z8UsPk1Q7HtwjRd4g01ryw== +a1WCYQuoRJeK3mOCGHQIlhrCEedAYfpQnUktuEYoFlw= +vyYQukiyWticSXOwUY8MiznZqoLnnCC/RUpXDOGxEsg= +ho/Q+jrWDtBeg9J9ZTnKi7Z8oMuCLRtnjC35Cw+4aVDyKydByU+COt6lumOoVZyj +AWid6G8ypXqQe9XxPPYED6EG/d+wAZpLmHn10ofRnJm2zuVMuvU9qhZPIJsbg007g9BmQxGLRAPjJyKkcrVzW+hTwBZcS8FDXPlLD0f/q/4= +Z8UsPk1Q7HtwjRd4g01ryw== +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +jEPV0YXDCqWCap/VHplvwQ== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN6wYljL95v5S+KLH0XvYeRAu7jaBdeUshyw1Me0QcftpN +VmVrGQo2zRokW/ZuO9bN6+eQW79nEpaaEVxkQ5mhMLDYFz3gFM695f1GA6K12DxM +VmVrGQo2zRokW/ZuO9bN67F/Q5IDafbWxan46AKyqXhJkHTWPWMJlgqQQU22Q4EH +VmVrGQo2zRokW/ZuO9bN6/0cM2nc/QcV1pNvNaFvxCS9guiudKGmsn/LBMq+3sAL +VmVrGQo2zRokW/ZuO9bN62GGKQt2S9DKmxGER0w1v26K7kSynx75p86xpYrJ5cQA +1u+XjG/2+GSQRv6EzCaWRQ== +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +1u+XjG/2+GSQRv6EzCaWRQ== +QKClQ4XOaAjfVBfstd1swg== +avTFp0LCGrKbHzgXWCq/pYahaBNfx164yVsSAVTJDN4lWK0zoj6Eh3CajVgk0hPL +TzinVBSdupkFZuXDgmwYfiSs5GTHKq13dTr2kdVzFPy0Jh42rAak6FUbANl5whZO +W7OoNBP/IWbYzUOvpg/slY2NVhp515LKHRWJIoHB+V62OipmiB60YwRsLUoQ2Yhe +ojv9MyHa70Aio7SDrWcjwQ0VSHrTkMcwddE36Wz7lCz16K6ytw4GpaCto2OYzTO/ +IfOrW6dR51K0HWsQz64KwMZLSub5xEzT/cQoJpZiUus= +MzOj4ZiBOJDNVP8vi/lFQIcd3YW2BaDb4kSPJzNVOdlx1Fa3HoeBVOYpiRWE7XX1 +PRG/YRzXVD8iVR3bzEcUno4Z8XHjrzDgKJNn9oJou7lkrRw3h7on47id8HDtKTRy +j7S8zldDFIkYlZuEh7uh1S+jR/V5xbpfKOdCgELD/V8= +yqOVJCkAGOhqcQYBOo7yqReCj9ixiaIB5tdlZ21DAUB9HlVyjl18y9Mfwf2BKrxO1mdZmBSOas0Z9YsNQ75XTg== +c0jePRxtTVZYop75Q5JCFsMjGu62TZDSljeQjMtCShNsz0Y9SFIv08dPVdTjI95b +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5hzukpfx2w2sPIWvGOQMWA5Yh7rnfJ/ffdahL7iA8laA/3JDQRVCFmm1vTiYAMYeY9 +NYuttixIfaRvF0QtYZhNf933obSmTd0Sfv1RLv5CWew= +z7RR81hY6MAA2voH8Z2gcSMl4BzicmRRwwcx6alhVes= +jg/rCd1ltkosbKN5ZG2fjyfZDh9bj6LoXpZWnO+Jm3I= +m5EI3+BXxkQEYKYaxORD+qnAwk1i08GBypJtuz97N8s= +1V2v8QerKOmubvSxgB4eTFLJVKokCn/n7cBzRCyjPmemCDNpavHplLKoYC5ZrVB11XPhYbqw8m0ksUK1fSNNJw== +k1ktu/l6HFULD7Vyr8Cv0HMlLvOl3L1DvBU4YpKX0wAfHx0c0egKY2YtT74Sqa68k3g5TvkiAzu1NRNlkcoBZw== +rtgmVFvoY2d3wTFk50ruC6zmYmoALMFLQOc+xc+vXmJzN60uRcu12WAXC28NrZ2x +wlLHv6kT3Q/RmtMBN4nDAesM/ZGOfx7RLjx/t+u4GQE= +VmVrGQo2zRokW/ZuO9bN692Dxe6/pJb49NcEGwdYFHp1ykWA3uXz8Dyrp2BHgNzY +VmVrGQo2zRokW/ZuO9bN69YWkPGheqxTevWr4koVetNO2nXtSSYd+VohbeMtpTJM +VmVrGQo2zRokW/ZuO9bN63sbeay11c+Azj5VEIm5hpFeadOrAd8WGDCLEKR35dm2edAnPxHas9j394knOwJ8NA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69nNx2wsmuj5rj8dMZU/ic5BLawNF/f6lthvaeVh/Ls+ +VmVrGQo2zRokW/ZuO9bN6/VAHEhJNgGHVrhdK3YgRoi+tWQYazsG4HRXEstT1K7Lt512mtzbhlWYuZWMlSdePQ== +VmVrGQo2zRokW/ZuO9bN69Fm+oKgY4oZ96N2lUN0xCYIR65p/6S+rx79lvSqg2I47knqkaZTvrsnLR+tEwCTnA== +VmVrGQo2zRokW/ZuO9bN6+qkEKPDxNwYFnkXN8TuQuYkotp65nYQ0fa3zEFof2zYnFi/LEybvPyjh48Z1idmyjl0NtdcsjLBkBJ13a8Xf98= +VmVrGQo2zRokW/ZuO9bN6wm11lyKsRHgTY7hsLdIWagcAA4Fqj10A0nlHnCL31hRaLimdtsSr1rF3vfwAkIE9UNe1Oq6AC93jtsLEBybVC4= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69fMYEokQlwk/9w9fdzOny6b4sFM5OW1KAT1oeECeG7J +VmVrGQo2zRokW/ZuO9bN6x9kSHAfsYQbQ95eUk47zCSU++ESIivk4zGTm21DHKkHEqyRv+yZpHFD0LXHT4m58A== +VmVrGQo2zRokW/ZuO9bN68s9bgaf6E2UUkkykIzwZHpz3P+5wRaqeeTuq1ofqFvFKnCBTa5TbqmgBM/RmZJPNAUu69T5cyQoQbj9yc2u8n0= +VmVrGQo2zRokW/ZuO9bN65llTn0pe1t030hd6gBN+zBd3IDBl9SSuthuY26B8/Pu +VmVrGQo2zRokW/ZuO9bN69ytvi1kzpknGdq50rt6Uk+ubFehg+T5L/Wn9jlHITZARQHXl2+Us7ChTWXRkBLidjmBIhghC83F2q0QovPWPAU= +VmVrGQo2zRokW/ZuO9bN62HF7Hu1aUGvgMj3NxvOaP7lJ1t3oULkhfzDpf5obr4i +VmVrGQo2zRokW/ZuO9bN6yfJ0tQlmRWb/s+hHO4yegbY9OZJu7Pz1YpWO35g7fWo +VmVrGQo2zRokW/ZuO9bN63C5DuArfNVx+wMW+93hp1VACOhAd7D/DOq/RNVLMAyF +VmVrGQo2zRokW/ZuO9bN633IXjOdT/W54PqLLNSn1Vg= +VmVrGQo2zRokW/ZuO9bN60VxqaV005HoUzQg7og2uQzzkGvTFfQImHgCDcOTreTvBj0gdrkAAkZ1tFN5kmALjU7kVG7jii3XUUaGJzXU1Dm0brYcNqsnFgqAbRd+hLig734vVILUrNX+DvQbOCpPeCtyQ8YEDj2QHDc0gf1laMU= +VmVrGQo2zRokW/ZuO9bN6/JsORMmbxegVkakcQYSuOtPUInnHZyjGgz46mtOv7qSxkpfnDglI1Zm24CHtbp9uw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknACUIF0SHq3Wk7UPJPdvLnBytT0bs3KMa3b9ztxfxoAOuFAPEC6llaTPvEl5nkxFo= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH13yvcZl75di1EfS2yW7gx+JTVNJQF0HOMQVAegRfGv1eA== +VmVrGQo2zRokW/ZuO9bN64OaX2n92Ly7KbozWq7a7t04nFfOkcL8NsC84xQi78iQ +VmVrGQo2zRokW/ZuO9bN6+a8J14HUH3L5V5KblyMysRYdesKR+fGd24F6C9j1l7wwo/p5lCfK41p1tvTNJzD/Q== +VmVrGQo2zRokW/ZuO9bN665+22jFAKm6V1WzyfKiYmwgxIk3QP+n9ubhUrmwYA2KnMJwdt6DVyM18JjeA81Yfw== +VmVrGQo2zRokW/ZuO9bN65NCQtgmAYdDgDxZd9uDZM6S75/AmzU9dqV8tHszmbb1 +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN64eTzIW7j12tevyQMnZuf2zxhIzkuqiugSL/r1buE4TR9WwGt7Z0s13TO+PuATcNEKCIeOxuOdltONG4HDVneoo= +VmVrGQo2zRokW/ZuO9bN61EVRyU6dsJr2EqyMVPbztJnTc0M4smwvC4e37Qh7Ms8 +VmVrGQo2zRokW/ZuO9bN67E2Rg2DTyty5YPAV4Joqwwqw9NcbRGp5GK4uyK1s8dkEGH9RxcsHHlq/24VKap3dQ== +VmVrGQo2zRokW/ZuO9bN6xRDOkMGch8OU8UdKKuA98T78S/RtIEO+vPGquNUPYP8j7Hqi5x7cOT1rq7lCPTbFRrq+ZOiuhVZC6d2uE6b5Y4RMkjy5kdxgYDR8lyh08y6 +VmVrGQo2zRokW/ZuO9bN6x+CQDkpZHTSzVJB0vr6/owqo2rDPdhhBctC47HwoQJI +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH12i/zuDq/sxwawfAUxLzsmyvkFQXYvSK+lgmw3Dfruf8oUMKRDLCW0YoRSFNGJnVRA= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmJ6/ykoozm7YfC70113tbsRK8vQxRXP6m1Lk5Pq4lJ71yrM2OyBQ2a20YT6ATnjGs= +VmVrGQo2zRokW/ZuO9bN6xC+pGEHNG3EGFN5iqJU139eI38FJ9/1MLDi82Qu88tq +VmVrGQo2zRokW/ZuO9bN6wJQGv5Qosk97LJRISI0TKDxn43J6bCNtR1cs8SqXBFf +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm/dQBBTn3j2CUDmofmN/zblP1j8uAa/rToM+BZYv+mQQ== +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH12q9ECvLOWcUe9cfTPHyuxucjjrMsKMpYA6OLWCOLB2Qw== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +VmVrGQo2zRokW/ZuO9bN6xyEP5JNRRYJBSeITWT+YxVGp+KyALUy+hocs1rcLGph +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN64eTzIW7j12tevyQMnZuf2zts3XldX2gYW8pNbC1L6OsQ4ZtJ9ajE19kuRaiWyVAoryEYwu4R+F3k0WPtnoCKwM= +VmVrGQo2zRokW/ZuO9bN61EVRyU6dsJr2EqyMVPbztJnTc0M4smwvC4e37Qh7Ms8 +VmVrGQo2zRokW/ZuO9bN67E2Rg2DTyty5YPAV4Joqwwqw9NcbRGp5GK4uyK1s8dkfYflZHE0nkkYN92Iz4DvtrB7pbXI8nJ1B+v8kBcTL9k= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH10h4UbnbgTDNlKt0H8bnNKHH19OWWvR1gRk3zXeKMJ+R8jqCu7/rIb/rNBOG+zFbIM= +VmVrGQo2zRokW/ZuO9bN6wRsqCOcYvLI1Bp/40+s8jeuGD+dib11yOYzha+J15yw +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH11DboSZV8b8syS3y20nGfXmJMrA3OSI5dV3PIVmDHiETg== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +VmVrGQo2zRokW/ZuO9bN6xwsAbGNyT9xD+H83ZahXtoSQ21b2IcHlJ/tPpLLTF4v +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN64eTzIW7j12tevyQMnZuf2zLKCX4fndgv2+Eq27hqdJSfk45Deik8ix2YNh+TocfCoJRVsdPtE1WD0Mo4Zvkv9M= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH112Cwgr1gup7jex1yHReVWxiyvsKVsK3znbe1ecdgFhbIZqxwirWCHagb7ADC5VmGM= +VmVrGQo2zRokW/ZuO9bN6yc8qBfgAsioQiqSI3s7WIFM5rHpAQSYfkhRNfXBk2JBPvIvjRzS10i3GMc0J2bxTrSvtMX11tg5EOzI+VjAy9E= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH12W5piSXwXcBTxZsgj+fR5PRj+yKlsuMRhvQaQszRXFiA== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +VmVrGQo2zRokW/ZuO9bN66sauifJfWwug1b3TCSgxtryBhfJZF25UQuNkKl6tfv2 +VmVrGQo2zRokW/ZuO9bN6+3TErdL9P5BVvv77uxFNG6LDoAfDNrQcomm1VDCoez1 +VmVrGQo2zRokW/ZuO9bN64eTzIW7j12tevyQMnZuf2zNXuQbP0F84n0/jl6+SrzC+pRpm/QL0mHxDS5+IJAnFFA7CzSbYbo7CjkNhnp0xac= +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH112Cwgr1gup7jex1yHReVWxP38Ud5r+2JgPLAafH7N4i6SpRZsaYMAGNiTp9HaQsd4= +VmVrGQo2zRokW/ZuO9bN6wRsqCOcYvLI1Bp/40+s8jeuGD+dib11yOYzha+J15yw +VmVrGQo2zRokW/ZuO9bN6/HUq2I+boGDdrr74hWiH13ETFhTDjpNg4UjDJ/ZX/5m5v/P88Ser/GIltB+CyiPFA== +VmVrGQo2zRokW/ZuO9bN64rXDFbHdbWNMaZnXznjAwk28c0+eGFiwoRx33gWzY3PbndTr3mFKpviH1b0k1s0KA== +SS3eUI6O4Am57+zsCenaFkHmh+pwM+FJ/cut0UtoxJ0= +lcmg3fAsktA/DTr8lB5mRWYUQaXOV6Q27CwrOP6ime0= +M8WJLh8/G3HKYOcF0o/j+UKQix7E5QZpJV0Tfc/XRq/AKN5kkYE+IMwFcAyVsz2M5wHJAN89+nJ1b7GSY68O6925BBXUxFwqdflokenOUZU= +wlLHv6kT3Q/RmtMBN4nDAZf8v50ttctcXvJzAQ4D3y4VerbRFUVUK+NjcpNFPsa+ +VmVrGQo2zRokW/ZuO9bN60s8cEnb0+qXY0vQh1/7elp8TJKn/o/Cr8ab/L4KpzVyx+zZfHPJlblcJVM2evPA8Q== +VmVrGQo2zRokW/ZuO9bN62wFNZfsNF2gO4y5vOS6dGtOhTIohM0/9iaTTdt4VAkwr3U4JR6xB2mGWrp5KaqxgOX+iIeHDUU+B09yhkuBbeQ= +VmVrGQo2zRokW/ZuO9bN6zJHGvDJmOJJYHHdXzByxKsyeKpya/o0dns6NEjvZ9FhBqfG56Ge8FxUmH1UPM8WYg== +VmVrGQo2zRokW/ZuO9bN635qCNYj675bllz0Ek4r4iv4EKIq35DdzNNGBjl++xHimfkScnNymMQSXs5jgXMSbQ== +VmVrGQo2zRokW/ZuO9bN635qCNYj675bllz0Ek4r4iukMkEeqt6J9KEjusjtsVrPFiXBkj3IzHvRGwRNUQeLY/b/AFWdigY4P5oM+yVsvmc= +VmVrGQo2zRokW/ZuO9bN635qCNYj675bllz0Ek4r4iuMx7lDsumcY7b34j2hhF0B +VmVrGQo2zRokW/ZuO9bN67WLIC8ccIPl/iWyXLvwqc6z89/fim1Zup64yhd9bw8m +l2FJPs4YkAmmok1ulDRuSA== +wlLHv6kT3Q/RmtMBN4nDATXcKKq3YicYjT+ilqwnePASEk6UJ4kb2FJ4sCzHQMkb +VmVrGQo2zRokW/ZuO9bN68rwZzz6qT+BRwAM0w/VgNam6voCMhL/LyfmhkE+4WOq +YabJCT93a3/IohXaIkr3oSIBZqGhFAEcGH7Ca/SaLLaG+fOp90QxSi85tpj3adUdyNOydRy2vjmM402XZpsX3+nkFnHf2XtWx5xSFzCScjM= +CAhn8dRUItEbEErp4w+lX6BuaW4lR2EIOU/Z+PBe2bh+we0izGB82w9NP/FQvC5X +1u+XjG/2+GSQRv6EzCaWRQ== +VEyEXJTNTVvV1TGW5V2yEMzjBDxvV9j1MdPjo5gCe0Q= +Z8UsPk1Q7HtwjRd4g01ryw== +dar6N0tUAb9rU8/aX1b3tI5lWSbcXQgKxiMzJeLasCmdJgT+u3YcH7fkhDzLb+1X +9BuKX7c+cBUFGH/xEQqKwDJwv6pCDrQbGrc/aEaAxEizXN1tDiWRXfYhvFOYGiCD +3AcPLYoMMn4rxWOUMA1wJdOmdYprhThTGxGyDnWq840= +Z8UsPk1Q7HtwjRd4g01ryw== +90/F6C3nHF3UrQ/Er80CwwMY0Oi3JHbGAYQNb/w2V49c1F+wL9hoGKNjAjwN6pWeeHbxvhz6JqsdWpYj5f3cjKeHXg+8Lul7hoesFngMXfE= +g9lRvj200ApJT2I/NDdmaNS2bs03/Yy1N4LL8JCPrSEGtYUIbFGqz+9Nvsx/WW8DAPlJEAOAh1mN1Q2H5srR9aN9BcyfgbXU677udPpvmJs= +AC/CEOsuoGTyXbh39EUlAxckdwNHSoPmK9UbpHaeRy5+9wXscQv0OinnRdatf4o5vPdsr8lUrlC4beVwlerQ3y4hg9sa18InYNYeL8SziTc= +HBkACpLXsHV9FQoDE2PMYwSntxSga34Rul8QLEjV96M= +wlLHv6kT3Q/RmtMBN4nDAeWjc9eUr6obACCnQsg7vueRPByWE2DayjQ0xFllv1tW +VmVrGQo2zRokW/ZuO9bN65VlXQyBOMicxNyTkfSbDVVHhvBq22DM+S7/byvVxhpE +VmVrGQo2zRokW/ZuO9bN6yititPhxMgpb/nuYcU2bQxlVrgi32/YGL7BfDcYBS6ZrhF8DKeSq4/xyWh1l402NQ== +VmVrGQo2zRokW/ZuO9bN60VWeNWzP5MtK6lHtmFWfiNac7RqAQJc2+YM8lNzCLr14sOTSSb89TTi6tiF4b5Mog== +VmVrGQo2zRokW/ZuO9bN61P2OVHnBm22rHl5VcwespdSVFvlh6jWmhNQGuWaiPCn +VmVrGQo2zRokW/ZuO9bN6/C3rj8+QHPPw24V/a4Ay7A= +VmVrGQo2zRokW/ZuO9bN61MqA9WsfKOg/tExmVwPpl7QnDnpNlS+CkeEkfTa9kINLQXgJYeYvwm13Q5goIzeLg== +VmVrGQo2zRokW/ZuO9bN632LGHZY9uic2w89Yj9RNWLrcjnK57CYRX2bXewhAHx7FVtzwI0ZOgY/T/V/6qaQlw== +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjakEuF8eNiysMFwArESQEwY +VmVrGQo2zRokW/ZuO9bN6yg8DfkEZba0oHLbAQabDs+4/JxGHZrKdgs6bMyBPLRMxwPtWLarWOkPgDMMkW2/RUdxOLVZk3+IQpAGkMdAX/s= +VmVrGQo2zRokW/ZuO9bN62dh+FzI+VGLu7ulc+ItFAbFXemabBEE/LbP+46UYm60 +1u+XjG/2+GSQRv6EzCaWRQ== +JEnsSsP7ogTJg1OXQ2YdoeqaOYYcnxFN3j3ktV+Kpps= +Z8UsPk1Q7HtwjRd4g01ryw== +8xWkPvEiREBWC3F3wdDkfHfKi35MXWHSVRhGdhf5R5T9XB9HNeEomkbAWdDKXpBP +Z8UsPk1Q7HtwjRd4g01ryw== +tsXNkoeF46BtVmFZ50qzKkC9C9rJjI8DYYqtZ5sTRNk= +b4OJVZe8QyIpjuTpKXDL9A== +cjZRE5RYY9TGg8THaNbKeimi7CKPJrP0YrozyGJJ6po8VG8Og36dzm29TqPtykmq3ShDIRxTK5G7NnirQ35JIwA7rlx3Yk1mP0YkpIDY3Ec= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN60gMmPI+qllGkwxOA9qbMmMB1m4JoBAOFYQ6YXkMo1VXquwSJ3IOKilGLS5oWPYO5OYeeloJXPkrrYqwK73A4D8= +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN64/n5EL8V7CIqhp9FPzHEuQ= +W3A5Dt+8AxWSKsTYeXZoL1vLKDueV7P0Ut7HIASfZlp4D0IrBt2bwp8YHjJXXxXp +W3A5Dt+8AxWSKsTYeXZoLzB4TUiScS3D8yuLKBmm3AwWtYcxV6f46K0T0Egvi5WKkEtlHJveHdb2egjl/+1LDg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkknASfUzt1VKHSynjO/3GCHs8NYZ0lBfA3YkEhN9KGBNrviqTxIwFkI8b8tfQOhuu9jUeb7lULfjfPkmMnUyufuDGrOr7IT6W005LdXSdC+eg== +3ihEq+EcGY3EkKhgSscgbxPe6xF3X8bYDWyX0qt6BtkDpzyqs5cxV+fqGAxgw4n0F0ocWo63+pTEB2B02sSu997O+t/1VUp8dx53F4oaaq8= +1S1xjYS0s+Ph1PyvECx3QMdMFDZ5B3+BEK8kyCfI1RM= +VmVrGQo2zRokW/ZuO9bN67pnuem2Cn+Z/63FRYUcFkLoOrSR0T8WP6DifYG5JOXMPR4AFoHBPiKvg8hnpKw+PCDgxml+g92wfGPN8zZ2WJk= +VmVrGQo2zRokW/ZuO9bN64Ga+02XCL+A28NNybmlSnI= +VmVrGQo2zRokW/ZuO9bN65a8NYx2zqK2mFD4wjj1HmQ= +VmVrGQo2zRokW/ZuO9bN61jw9zMkfm0nvQi9tveiWV0= +VmVrGQo2zRokW/ZuO9bN6zYzNJFTIYiz/a08bSUNc5M= +VmVrGQo2zRokW/ZuO9bN6ztX+cJqy4yqv63JnfA/mqI= +VmVrGQo2zRokW/ZuO9bN60jLKcJEMj36X6NZvvwxF/XvqrmJF3W3RArdDHrkgZZ6 +VmVrGQo2zRokW/ZuO9bN6zskyREk1B9SpIIVpfE36zU= +VmVrGQo2zRokW/ZuO9bN63leNhHVk6qBYEqAElCrxOXVPHIpJ8U4k7mou4tbIR/7 +VmVrGQo2zRokW/ZuO9bN68tloVdndHDn6gJZRayJG+k= +VmVrGQo2zRokW/ZuO9bN617eR9WvYqaE7y/ppCzNL1570CCRJc668VdVufs+tRvC +VmVrGQo2zRokW/ZuO9bN6x59qEj9owkguOGj8QrHQSLi1Q/1C75rARTXu4TGt8DP +VmVrGQo2zRokW/ZuO9bN60Bq4PDCei1vxH18OtSGKCg= +VmVrGQo2zRokW/ZuO9bN67biU+BUR7/d7BkQj4RgpT8E7BfCa3N4KcxGo7ivT2Hg +YH231WTDnzQG3bFilBiqIg== +p9oVsBgcyiBMjDb8CcPOqP2sLQ/zn5Dzo5lCtrgxqTQ= +lOh2GtzHjjMM8E9J40AuOcPN2eZCahfvScF49QHmlNXYE5PQWq+WYPigt3dG9DzvOjcs8WVNkHQ6P1pZps2Ehg== +UbJuac0dxLiN+5ocS3w2vTpUyzdtCQ6EQJg8V3275Z+ztU2/bGujfta5s3IG3yXD +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +PgCX5XeVQYjD7/hbTYlz4IaRp1HtU7nR0FQvcIMZh8A= +7HpfjuxaQDsQDQSHtCtrNK827fUfCR50hIlsuaTJjFjc8y25hOxPGEu/jrhlV8lb +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +jEPV0YXDCqWCap/VHplvwQ== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN67F/Q5IDafbWxan46AKyqXhJkHTWPWMJlgqQQU22Q4EH +VmVrGQo2zRokW/ZuO9bN63ZoACJPJn3hKH5qBGe9VjFfy9acDnoD4s5pyWTz4aPz +VmVrGQo2zRokW/ZuO9bN6zC77Yy5IdKUwUmbFPpcxkKmTXclfBLfED5Xnxr6+jid +1u+XjG/2+GSQRv6EzCaWRQ== +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +jEPV0YXDCqWCap/VHplvwQ== +b4OJVZe8QyIpjuTpKXDL9A== +xWoGNWjKGPfI4gq8aHoTfEof00xZubKgl7GPVFGSDVesQ26Fk2cgdMVMyPXm96MnC15F03LmfmFV2XHuofNY+j5SUiwtk+iHsWhJ28gRAZY= +32pdC9DD05OE2l0oXazDFMcaSf/OvtPF5ddCbxkc2xQaF56RJsO2Xz5jcPdtIB1/eXGzpX+pojBjz/0LE9V2xthE0OS29+QUIDSCMj3fNAQ= +32pdC9DD05OE2l0oXazDFMFVya1MEFTy0BLA66iSpsVqQm6Ex0aKp6sk/uFMjOFRLXiyo8rgZB6BCUp1rgQxw9a9JiLERnLcmc0XUhcwFis= +1V2v8QerKOmubvSxgB4eTB+P6Y06+m1KdyBFjy0y0qtWT8ILPF26Krk+5/LzoulD +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrSuY4P/AlnqUWfNJ3Nv+yymjs8C+eB+Hn8dWS7DzR5wCEVi+pzzp4DxubDifplcmTz8xv5V7wP5rejGKOKJFZz4 +uhuQgaMXnN5eYKoRWtj1+a5fnluADmFejrHKaJhU6U4= +IZ4I+7EmYS7/1XOeZ6CWDePsgkaWBxAo595vQq8IcgAmT8y6BMgbXaQOwT7ajJOyZ67cq/xz8LBemQ1aX2Z0Kw== +VmVrGQo2zRokW/ZuO9bN663zFPH6x6REddIs/KSm3NexAA0duB8e2h+avE01iaYd +VmVrGQo2zRokW/ZuO9bN62PWz3K051UrvlylqX+UdsKu5SkTaoIluiyKgMiWKP5VvUfaBzPRT+4LOTVj1DoKDMOGhOlRnJybfG0/JB3xdn8= +VmVrGQo2zRokW/ZuO9bN64iYMX+A77ebuxgWS4MZGWMQSPvTqssc411J6ON3u2s58fEiOjQ02BDwWoxRB1dSXD2N6clFtvfGUuHx+6QdDR8= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpIwympmloGxIdm3lnYC1zFlt82vhd3dIG++dF0cHPJrs= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpaXbDjWLbvZ1lZ0Zi+IId7fybt87VGRejtwKKf/BWJGU= +VmVrGQo2zRokW/ZuO9bN66AqdmrSo1oyJGbJyEFKlavIS98JVLZCvBKw35ReG7IZ +zxz/Z9yjFalbpeH0OEk069PytlF3G6u9Qh5uIBCLydc= +IZ4I+7EmYS7/1XOeZ6CWDdX50YjqkZaY4v6SVK3YYxL85Xnq8vaKhIBU5+04Vbwq1KwBOc3SUWrebxmVjCIdLg== +VmVrGQo2zRokW/ZuO9bN6zuBW3gGFHmzC3pxFkVqaOZGdVRDEqj5rzX7nMjOksm95bFAdKLGOw10hZTWVNauPw== +s2s/1ED0ebJ2NmFlE8kk041WeEOs0NQTR+NEuE/p1wycslBra4ABYHujZdQc0uDG +PRG/YRzXVD8iVR3bzEcUnhS+dVNLAvNDmBhimxHvlNA2HT82QC/02iXhdE3LqIS5 +j7S8zldDFIkYlZuEh7uh1VxkBYQtAkbaStfBbWx7fJQ= +6NhFwX8gQzuQfkiLZq050pZ+2q9zTxL4R2Rx3pYCY0lS6XAa7HxFkLPeWNSm74d/grgzrGZ4NTnGcsA2gwNf1g== +PRG/YRzXVD8iVR3bzEcUnoNc93WwzSDGZTijqo4XQYP1zB7OiFQUJ2sKTBOKiXHBegpq09XlVduN/zMZI9/IPoQkkQlIVRgE66uv5utA96s= +32pdC9DD05OE2l0oXazDFIv7Js51eMSeYiKPDoffdWw= +k1ktu/l6HFULD7Vyr8Cv0Dp/BLIuC7jc8lO034+ASSh12djXtIbpBUNxeWBNIvYRsNqqiKBD3YtyfeT5KRCKiA== +R65WfMhum3uW9cFR5COFLL9ubQ0tcpZY5jkkHBnHdce7XfcNd+pvaghvtup1k9r/CrjKQwHyNJqhVBp1y58t1lQ0AEceTHxF+sTa65mC4csmqqmEXH4lv6bNKaJSvJ14qOFHfzSjVyDM9A5KNHf2No7JEow0mX0clBmf2ktpRlk= +32pdC9DD05OE2l0oXazDFLmFDXu13Ku/x36VAfPGPP9FkvpnezPYZrhPB8PIy/uL +wlLHv6kT3Q/RmtMBN4nDAafzR3IJaSQy1F8fEUDT0KURDliq5QrdEI8/0cInAzfT +VmVrGQo2zRokW/ZuO9bN695D2NbK/khMekHONWycWzoTL3UetXnbY5ZBbnCaZGEmk8vkbkjI1J6NGFVtF3qVFg== +VmVrGQo2zRokW/ZuO9bN6wLuYcyNxaKhNZH2m+8aDPkLZsJ5l6OV6nPIyZ7rFMD3fDsuaY7RO6VkCnCbVznvMw== +VmVrGQo2zRokW/ZuO9bN69s+3Eo/hZMrKkYAXB3JkA2bQbWagpg3k5RV7ihsj0gHlQy5AeQPCIj7FGBMUa+4Gw== +VmVrGQo2zRokW/ZuO9bN6+qTSfToQVBLEY6zusk0ygqo9bxqE4YdXRCZUEfzNiWt +VmVrGQo2zRokW/ZuO9bN65N3QENvuGnjF5EmxEeDMzE= +VmVrGQo2zRokW/ZuO9bN6xmmokKCVOSzrVXzObj/KfAtUgdJPHRUFVWeGHuin00P +VmVrGQo2zRokW/ZuO9bN68Dhg+6et5k+hzf+4hfigxPO1gqzqETG/sLsufA1aXnk +VmVrGQo2zRokW/ZuO9bN6wWzNcvV4pRpB7aeiH46ZhuR7lLT0sXl4MncnelGql7ypmYCkufsO9/UWUOa5gKFFQAt0RFwBWsN3XLCnm+TI9k= +VmVrGQo2zRokW/ZuO9bN651a2HPSF4amB6pelKKGbxD/O3WChczakx9UCQPe9L4zYkn7rlilYYw4UiLgYLLHyQ== +VmVrGQo2zRokW/ZuO9bN6yMdhcWuddd6xv9F64cBYywreWSVvaR5B3yfWI7OYuMP +VmVrGQo2zRokW/ZuO9bN6/b5sfZq7QFdwQMtnNHleFw= +VmVrGQo2zRokW/ZuO9bN61gHm53bbaH+I9qyjWNPAxQviXRiEfcQJ9UaS/gMM+QlSDtja9XeaXcxbmOF5qba1Q== +VmVrGQo2zRokW/ZuO9bN6x88C1oSfUBmfAHykM+KeSlhAm0oCEAnYAW+tjlMsUZ6dHLLXaNm3S3cFG9r8INPpOyDrD1/0cYcdmgHJu+5hQc= +VmVrGQo2zRokW/ZuO9bN6x88C1oSfUBmfAHykM+KeSmsYnFTlaGSi3hItve9udXvXbK8kLkxk2JTeN5/083dIwusJTyrOp6WwhxmuVadK9w= +VmVrGQo2zRokW/ZuO9bN68QfdIZA1e8ZLlf8ttXmLYCnyNJDls9LvrZpXkKHipYZ +VmVrGQo2zRokW/ZuO9bN6x/CYIieetlN90tjay10F5n5/v1iJkArxYoRof65Hf/bWUKgONh6wM43I8Jpg8FgsHIngfteD7UBDsQdRIyf8yC4fMeAOMpse28Tp9jblFPR +VmVrGQo2zRokW/ZuO9bN60p/qfi9NG+nP+luzpqucTyAQc7Yj1OJYmKJzkVZar3zXH5sqe2agq9ywn0O/zrtYRUQXA47gxoRmDhJ3H/ABCQ= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6xzedyD4hQTvOzNo8fw2FktCfIxZrB+ETZoJ/tb/OJ7i0JBBscPeHSmlMj1p3zH2kfzbA/hQSeT2zLO4guX6IJJgLJbg9nfzCvKwiJDpAx551vjLfPPT0dZezyB/sdZE2kmDhMKoqccpXkwE5uLmZ4sE04VcafV5qPD9LrfVTiy8 +VmVrGQo2zRokW/ZuO9bN65vXYqLcZgYh9RMZsMEc7swrBAM5P9dGK8NyKUQE8BGKAhHZEhZcOq8cWZX7jTyNTKobvqySbP0rFUEeAOBEQvAlfT0krQnboyw44hgEZFoq +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN63izF5cXEigq4rWf9EMGZs5lLUFVXHaMEfeD3NCR3MQrLCj9wxqx+wsboPDs62l97A== +VmVrGQo2zRokW/ZuO9bN6/OgSB8gHUPVPZkuldfhCBiEPWNxi725HjxzxtCaRkyPAgzszKiIBdm8EejILUwhg9QwAaRCNS9pDnB2qTnv/oY= +VmVrGQo2zRokW/ZuO9bN61LgRwreI9DApMQDnZReQ7qMEmgNW02YMDHoHy0bIU+UjUji4Y86/fXHb+PlFDf+jnE3sKazkvpT5ej343oVEAXLu50VQoO8PyS+uwRDWTQSlwN12A+ouOrf2+6c9fJbyJNho+jEgVaCrQjiuTM7+GpfZkrTHb9GDgyXeQWdvr5CO8mcOsfwgX1wX7iT+NBLGA== +VmVrGQo2zRokW/ZuO9bN65pQd/WGJO8ZjtfpO3veaj4G6ZwvVtL9gz0WgP48V+4qSrYN/FDj7+EvqNhODifyiQ== +VmVrGQo2zRokW/ZuO9bN68xSb0F38voLTC1ddFF97JS0RRXjJlMhVds+i1p+pz2HZN8oiYZHWB9YNjBJp2xjb4CwY39xev88q+d5fzQky9Q= +VmVrGQo2zRokW/ZuO9bN68frBhJmhmLxBkcwHpKMHIo1Fv+32NAA+36Mtj3NWFew +VmVrGQo2zRokW/ZuO9bN606yvVyUswKh4K9nEzByhRbgQyJvF+T8ZL/ywFKLuux0 +VmVrGQo2zRokW/ZuO9bN66E5j6Fo/6VCuayBfdOIhI4Wmi0iqcFoY31iRzq2/9PpOlmVnYLDJSLa5eJrIJHOdA== +VmVrGQo2zRokW/ZuO9bN64DqcAVCT1beyEqgB+oPWssh0gta/iPjagLMcIOG0ZM4 +VmVrGQo2zRokW/ZuO9bN66E5j6Fo/6VCuayBfdOIhI6/NAlvHtD8pmc2QQMFS1c0sVcxoDVQ1zW0Yt0PbfOw/w== +VmVrGQo2zRokW/ZuO9bN6ypica2CILJsrlhqe3QxnA5/+lrILTYwkPcVkl8kP/7Y +VmVrGQo2zRokW/ZuO9bN66E5j6Fo/6VCuayBfdOIhI7SNChuca/w+Phx/6pTsj9jxs00/c+KZkfeNaMXVitrqw== +VmVrGQo2zRokW/ZuO9bN65fHmo3CpQdkgu8QZ7/F879DDf6j0KBElFUfLCOPUFfLxXQ5L/C8yo/NSNn+6+ovkQ== +VmVrGQo2zRokW/ZuO9bN69TbvhtoIodcEOVjaAiMprVI0Jq4pPMYBUjl1/yLR2kpy0NEMJqN4n37N8mHjl/M9Q== +84Vymj90Wzn5yYuvz1pUuK0n98niayd9hUG4km8pAgba1Y6eGIcXGRzbcecfmD0p +VmVrGQo2zRokW/ZuO9bN6xbcZbx7XTfd0EiNQCQCRcGDvelsKcSn5NmNaZgXDjglenv6j2NQZQfINKTQZGqdHwp+S79x2vpNGCjrJsOmtN4= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXr1SI9n6q2YW3T0H5q5k8fTEg08mVqsagYJCw9AbddXw== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX+qgw1zzEimKnHnwQ99BpoMHjRxO1mk4jjepqQS4J6VtIpyoQeFSYTDCn6S0gG0vY= +1u+XjG/2+GSQRv6EzCaWRQ== +oi31VvltPlkmgAj8qrGsXUBJ5rMdCS0QXWQhphOGafI= +ssyiL0QCGR/Kitp0jM35AvSubxcgyb02wCxY63LeQ+FdXLXZELnkpYp0+wLaWOXX +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +WHkOzVx7seuLmxs5Hu+l/tP9Id4aXVd1mst3jTveQ+TSwCdYkYYi0xlII3ad+5Mx0bTivYaMa5qHmoef8q9aEg== +NhnIR3Ilo4H2su9/cTNo/Ps3vYSOiOY6esn1xeoWmAlmLwmaOoQ9T7lnCb6TRVZ6G8N2MyoNi7SMYnVlx7nRFc+zLLFF1NONcDlGDGB1zDU= +l2FJPs4YkAmmok1ulDRuSA== +NhnIR3Ilo4H2su9/cTNo/ASGhAbuY5KDc0IEMj7gTgg= +VmVrGQo2zRokW/ZuO9bN66hILXR9DK+oj0n6h42Gdmp68GKVo7yFyC0zdiW18fCa+eCJ494xGENCOPZtT5QGzg== +VmVrGQo2zRokW/ZuO9bN61UcHI3J5ut0LQ+Fq89sq2CxmCAw00K/80Gf5NGMfLiUPQzvxBOYvZuEZ+91m9rjMA== +VmVrGQo2zRokW/ZuO9bN65WJwaAJmfHPqle/KssJiahg597TU14l8xoyJhcweL0cLgHZ1ful0i9biMG05j4pK1ULcUOqxBNL65J6SG3EPEMuMtAbMDKEnGF4eeMF6OKFVOhWbtzYZ5l2KlKRIhBq9ivs220dKxL9knDJzmuKpunFe7voKpqtNUwZ8r/eh1hLQdNeWViD2kvL9h6NyBQcP6nPW7psPsoBVwnN1/LEO+E= +VmVrGQo2zRokW/ZuO9bN6/cVRWVNFm/JqWhVgEQHano9Nl8Qe1INoxj34Eq6ji7s +VmVrGQo2zRokW/ZuO9bN6xf87BYcEtEJusNj6B9nDB0fckAQ7JvTpEflmeNXOgGh +YH231WTDnzQG3bFilBiqIg== +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+mIkOb+TCfJvH/w7nenmNQi1zWK57JebtAOzfMq0wpoFDrQFeJwj7VybHSlxyiPcW0= +W3A5Dt+8AxWSKsTYeXZoLzB4TUiScS3D8yuLKBmm3AwWtYcxV6f46K0T0Egvi5WKC9b6LRk9h/rY0UvA4He6KcMMvs/jsWH0pUrPiH4ey5uAYIK2BkaGLpMljqgMIz+zO2OKSsR6gLqnpPrjUkLyAw== +NhnIR3Ilo4H2su9/cTNo/FjiH64JsDVcEniosnxmabur41H+EjLIMfw9X2mUqowY +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6oNJYJ3oKHQnTz+Hw7wWSQ +1u+XjG/2+GSQRv6EzCaWRQ== +GfUk7vHSvwxwwg1xGBF6csBhgNmta9ilREnJyj+FktE= +c1VtHOwUx3M8GzIh4FUOTb2GTYtBDI9RX12eWasBOyJaoVTrkag2GDqWWvjqaK3O +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +QKClQ4XOaAjfVBfstd1swg== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN623k/6o1+OQ+J2DsFBOlua2+5NYedbrk8T/8p8yP8t4ZOrDHOKTqndspNT7oxPTm5g== +VmVrGQo2zRokW/ZuO9bN6xobVLd08EJekv6ZAw5NA5B/Diz1jg7kDas/QJtXmbfK +VmVrGQo2zRokW/ZuO9bN6+h+LXnp38+SIcNeILtkDJK+LUWUXA9CApQpM6oXVdrL +VmVrGQo2zRokW/ZuO9bN68PyIKRWhjdxEk2m/qVBWymy9ZNeaIw0W1O67DTRPyNnoR+nnqXQv+HYTmswh6DgsA== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +09UuO0tw3kJ5J618hVggcQ== +QKClQ4XOaAjfVBfstd1swg== +b4OJVZe8QyIpjuTpKXDL9A== +k1ktu/l6HFULD7Vyr8Cv0NlPupiosgMrWrig5wXf07sHWsWP2ZG+eSRXSHwIyzscxCduO1qC9ihgl/nNN90yJ0k32I+kdYNgmVL7KZay7LA= +IZ4I+7EmYS7/1XOeZ6CWDWWl7RxKu6InaEBG7vU84eJ6NSVD41CUFN8EiDm838Z1 +VmVrGQo2zRokW/ZuO9bN62dCBfssvm/TGBXvEVjpXqinGtoatFJMGqyPDxGf4yCjghAxfayewdj6Sq2OfRf+QQ== +IZ4I+7EmYS7/1XOeZ6CWDeSjVgBoC/80Cp9mo7qHwRL0mH0LBS3N7X+ur5jGn2XW +VmVrGQo2zRokW/ZuO9bN66Yu9sKLLNl2nwapG4f4GMyi6ZnT5AwNHkF3+HYF+veHnYTI2lFnDPGtwmEvBiYNJQ== +IZ4I+7EmYS7/1XOeZ6CWDS3EsxYnGQoZ2XFiH/62GWDwC7JpmDhNC10oZSCsZH98 +VmVrGQo2zRokW/ZuO9bN68+EFwL05pc9iCB4ZPDwL90AL87PFeHdUCmDTwBU35QzUV8sUuZeBD9hQt6GEuKkXXOYUbDtAhti8agQ0HdlNPw= +IZ4I+7EmYS7/1XOeZ6CWDVYZUPAz6JpPin6tPCQAVitIBSokU7215gEpIqEeMgz1 +VmVrGQo2zRokW/ZuO9bN63JCOljOY+Ch2K7ShGCJuvItWI8TcexGXS34lSsKvEvQG4H4kN+uwTbL3RkCFA5Z3g== +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+mIkOb+TCfJvH/w7nenmNQi1zWK57JebtAOzfMq0wpoFGdgaOOSewO4tjZybH6SMo4= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXrx0RH74rzP+DMEHwpOwZ6d7bNaVWJEBrv6ZnZvyvHTAUE3baFmml5mUO5DXOnAC0= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5hqfprx+1n+iB1xwtsTjMm8w== +1u+XjG/2+GSQRv6EzCaWRQ== +XDQrbggyGtdxKf5Qd7CNDLXV5Zo0HKhXIYGnyxT8jhA= +mkMqvMwtS246VTZkcVlxvVf3KU+EyyXEdiPyP6rXAJeHK92ZpHtFB6ZvZBEzxQWX +D1+XxMqmQ9YZoTtzHI1TVzyBnQfcGjplIcZsNUXpw7Viqec4vJXheAV+I72d2S7Uu5RlufSa/1FqPxQw04YRZw== +n2Du0DA7YjNSpLacHFycz3CP7WVEm3aXjYa3hyjX/vUnN+KoeVTuPL1fYxpOnihl +Mo+0bvALEsKrelbErcxWpfi/5wYNk7Cl3tNmt+uBON43WNIz60wjKPX54mZYH3Hg0H2o3yZh/ula0ThdIMJ/ZQ== +Mo+0bvALEsKrelbErcxWpdx8fHv4ofXPZuoNIhLdJuneSTS752QV95WJYOSMPumWztWyMZs7Nk6qd1eygS1UOQ== +juJdqsykhOSvrjoodiEp93Hsa/iwzL5LF/9dyqXpyX2xlpLk1Gj9JMQOc2/aEr2512B5LUufYJ1XyixXjBNUVg== +fNA+s8KEa0Gy0us8PkktW1uakXpccvuyNacjHqe9a1de0PLA3Lfx/4DGb3djRrvuTxv67A3Rnwb5YHI2AqyV9Q== +PA17vimSRbG7tn861DNLBtjB0ul27mGcfjS4S+qBjlb5atkTuhrMpexda4gU00KeLPy+BMdCBGGYXVrJSLm5Uv2zSWtGjbvqwpb4Nr802Fo= +1u+XjG/2+GSQRv6EzCaWRQ== +3LUcH+0zR7ubnty5cezL7HZ9GeyvS5PSfD+Q6aJjeu4= +mkMqvMwtS246VTZkcVlxveIcYyurln9c0dwl21vdjnjzHM3c7FB74O4bP43aSx0W +r+NaLoMoC1qELSrv0h1ouJTsypANy3V82LmUeRvjKQQ2HDWmhdnv++dF6nRdMCPL +423QZ5M5u8A1qyMDnjgTGHUpzkEm8yvfqG5Gm+yIhST1aIqQ6OiHVHt6S0DWqHfg +n2Du0DA7YjNSpLacHFycz3CP7WVEm3aXjYa3hyjX/vUnN+KoeVTuPL1fYxpOnihl +Mo+0bvALEsKrelbErcxWpfi/5wYNk7Cl3tNmt+uBON43WNIz60wjKPX54mZYH3Hg0H2o3yZh/ula0ThdIMJ/ZQ== +Mo+0bvALEsKrelbErcxWpdx8fHv4ofXPZuoNIhLdJuneSTS752QV95WJYOSMPumWztWyMZs7Nk6qd1eygS1UOQ== +c5IxkYKUSbSDwct6vRlq5byNjttqWKrl6pQD7obLoFH1n4hQQN4L6J4qGdvXZrZuSIolg/PvyrrNN3cAwDRMsQ== +2NXrAbLln4pcE1BnFEF3EqFgB5lC7hgbEdSrJgVH2Q6M0nL8ocKtkoRKxCZrDP99 +L0eUthVnpkGsmKFAX6d+uBWSR38I0ooDiDs76zI3rHk= +juJdqsykhOSvrjoodiEp93Hsa/iwzL5LF/9dyqXpyX2xlpLk1Gj9JMQOc2/aEr2512B5LUufYJ1XyixXjBNUVg== +fNA+s8KEa0Gy0us8PkktW1uakXpccvuyNacjHqe9a1de0PLA3Lfx/4DGb3djRrvuTxv67A3Rnwb5YHI2AqyV9Q== +PA17vimSRbG7tn861DNLBtjB0ul27mGcfjS4S+qBjlb5atkTuhrMpexda4gU00Ke5HBwV6PAc7oy61d8lCcXTDPCfj0jMO2dj03BONtTXjE= +1u+XjG/2+GSQRv6EzCaWRQ== +vqpebObTA425GjZtEgRzeKg1TcRBMIqhtwNxfjCXb3I= +mkMqvMwtS246VTZkcVlxvakn2CWC5fgOXjfig9t8ZOOKmCYcPWyiJrM2q1YTbzaY +n2Du0DA7YjNSpLacHFycz3CP7WVEm3aXjYa3hyjX/vUnN+KoeVTuPL1fYxpOnihl +Mo+0bvALEsKrelbErcxWpfi/5wYNk7Cl3tNmt+uBON43WNIz60wjKPX54mZYH3Hg0H2o3yZh/ula0ThdIMJ/ZQ== +Mo+0bvALEsKrelbErcxWpdx8fHv4ofXPZuoNIhLdJuneSTS752QV95WJYOSMPumWztWyMZs7Nk6qd1eygS1UOQ== +juJdqsykhOSvrjoodiEp93Hsa/iwzL5LF/9dyqXpyX2xlpLk1Gj9JMQOc2/aEr2512B5LUufYJ1XyixXjBNUVg== +fNA+s8KEa0Gy0us8PkktW1uakXpccvuyNacjHqe9a1de0PLA3Lfx/4DGb3djRrvuTxv67A3Rnwb5YHI2AqyV9Q== +b4OJVZe8QyIpjuTpKXDL9A== +sv7/Gn/EIUd1qd100SlAADmdMfLVzff/ds6lE3DYs60G0IRkQmqzuKCsitneaOUh42BtWRgy51WKl3wVIkqWTQ== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uBWSR38I0ooDiDs76zI3rHk= +cbvASHjpysrsjdY5RctXmFfq49X2GjJVQNVD9teGxwE= +cbvASHjpysrsjdY5RctXmN8xPh59X6dfQkAGafjIwQ/JO0RC5eZ+d8CYwUqZsrzYcHxd6s1DfQxzGCiFS5l7LQ== +oFRTJn+vQIW52ldmmXvX7SFq/R2PvVLOSjCgrER/+1ezS6CHmL/AryMKkk2VZX9TerI3Ejx/trvS3Q9Atgf/GLDC8QW1uxdpqhozEm0HSZo= +xt5C7mdEDz2/JDjO+ejBsP4W8f4f2ZIZVN5/SpPOOuQcRVeczva30Vo/fzex6J2uu0rGD3FHPjXzKGEoA+wfaK/lI3mo4/iO+G8+oG6gcVWMbZDoy6LXIywTQ/WT3A1tTlfYetI/Wa9mg9rSMNxyuRLZbOk+XDL18/+yxiYRVEY= +oFRTJn+vQIW52ldmmXvX7SFq/R2PvVLOSjCgrER/+1d/JzVXV9rI2vsjv+weSUCcmTye8Y8TItbmxBdGz92+BnDQx/jOcBYYAKZL5kL2oGY= +L0eUthVnpkGsmKFAX6d+uBWSR38I0ooDiDs76zI3rHk= +PA17vimSRbG7tn861DNLBtjB0ul27mGcfjS4S+qBjlb5atkTuhrMpexda4gU00Ke3GjW1XqseD/MxG/WOJwzU76Gizg5wATyLx1EeZvwrQw= +1u+XjG/2+GSQRv6EzCaWRQ== +EZhRblE+jJIBo34uejixWP0r0RlXrUFCVz0+1fo2ZAE= +csoFqOxWYy7ouPTWoQaLdnaFuHtfElQV/4goJW5WhfA6dTubqpn/6XqsRAsueV2O +5ybqwXbYZHutgsWYoSLQvoL5cXDCA6u5W3fIXRKORs2FHNWAnv2UwAoj7p9pJP75 +n2Du0DA7YjNSpLacHFycz3CP7WVEm3aXjYa3hyjX/vUnN+KoeVTuPL1fYxpOnihl +Mo+0bvALEsKrelbErcxWpfi/5wYNk7Cl3tNmt+uBON43WNIz60wjKPX54mZYH3Hg0H2o3yZh/ula0ThdIMJ/ZQ== +Mo+0bvALEsKrelbErcxWpdx8fHv4ofXPZuoNIhLdJuneSTS752QV95WJYOSMPumWztWyMZs7Nk6qd1eygS1UOQ== +juJdqsykhOSvrjoodiEp93Hsa/iwzL5LF/9dyqXpyX2xlpLk1Gj9JMQOc2/aEr2512B5LUufYJ1XyixXjBNUVg== +fNA+s8KEa0Gy0us8PkktW1uakXpccvuyNacjHqe9a1de0PLA3Lfx/4DGb3djRrvuTxv67A3Rnwb5YHI2AqyV9Q== +GdtTSDJSToysp+tWyVstgx0+jVfPZGcCO1zjidU7WlfAOHspewxs3TY5YVdNSxzO784QsspYaBz30M0j4itOW6U4gZU1XhTPWC+X2w8FgS35mZOuXKy0tkCVdaqov93rTFynrx2cJtWxH/7+tsq2gA== +UFqj1DupeWmtd5xz8xEzn5FkHP+WNMugEY0ws9dpcPsEwXcOg3lrZPZWMRTcctBOu/99HuBLcHszBTcdNJStaQ== +PA17vimSRbG7tn861DNLBtjB0ul27mGcfjS4S+qBjlb5atkTuhrMpexda4gU00KetJ7YxbT2Ke2Ahg20sWE0zNiHKPKp1M85sMvsK0i1HOU= +1u+XjG/2+GSQRv6EzCaWRQ== +YJ4JGstLBrjsLhFvWMqqOs7ljD8z4xTG8gc9ZDuXoV0= +tRsGtRpyd3NaPukECjTZg3hNDScnsgtniY3jTn/FQkgb9AciCC6AWK90AROTouMfNfwogCllNtrvLYv/ohfuww== +b4OJVZe8QyIpjuTpKXDL9A== +p9oVsBgcyiBMjDb8CcPOqOqpzx/tm9sFWyAV0717NUAcEeYqb0APxYfdrJL2B82m +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +xWoGNWjKGPfI4gq8aHoTfGQ/0WcRe3RGVCTVpOXBGFfK9oq2wJG0ManURHPRgoR35wHRO73LgxbQYbAUvXjJbQ== +p9oVsBgcyiBMjDb8CcPOqOqpzx/tm9sFWyAV0717NUAcEeYqb0APxYfdrJL2B82m +d0CXrv/LF0oZZvxaIzp0YNe8V4naHoVuY5ZOhZwZAi+CU9TDUBed7auu54C+Ks5hv03XlcehOyt1wCWwHvl6xQ== +YabJCT93a3/IohXaIkr3oVnUkde6iwefyaZQxdFclFpk/CBhvdo2LD14OJ5a/aKYGqwtj5bEYBqakHdcLBRXFg== +vxZRPtvgnx9GXrXB4G/UMyf+Kd1+OunvObkPcMks4F8= +kTxSrTsHul564SUUCXqFGqnCBJ/pZnZuuEzwKq5lWTMnr/pICNQxZbpETQLymx28 +BPpXO4yg5jB/geTB0jxHP9wwlicJFHphvMFQKddQ4Rsq5BW04q+dIcl7qk/g+aEK +37cKTYNVSLcMbZNWkFyw/bkqfeV4AxxhISHXKAliyoVwea2SzWHXTJiiTNKCXTxF75HulVP5xL1nq+Qz2vJfOJeGEYdoF/AIaU93PAzvRalzzks/fjs5/0XEEDQviG2o +Ba8y5vDkcldg6GNO9PPGOWYojGh05hg3eE372iUV7B0= +g9QUrDNfkepVhzTckf1czKDis40wYHzT7NkITn2PT4puZ8csXgDB93OSOQMohJ8MU2FDNN0jsT2SjEy57u4rhlYCNIjjyvyfW+JsLiYK2SgrUj+w/yOD/IHdttHNynAiG7sUzTztXEF7SmiHDVvmS4DQE+Ug1u2/z0KozHT5mXrDVSAaNrU0YK8ICVH6HCzw +53pmrPesT9qAhqxIHDNU40O01bUmTAKPQq4gLvky8PY+lrS+4rOHZ7nXjUbuxI+Aogqr6+RdxQWgZTFOOSEycg== +/0ULpLqgTvInFD0r5hHANguwJe0DtrG/oJwRJkc/Bd4qZd8TpWmTyCFdjYXANp/k +txLhhK3LqdC7OTooW0Mlz4GFqz6LzbvDy99pHBQEQ40a2cY8nrJ6La4VhXvL3Boe4Awu7qH72hlAUcQbzktaOOAAlo5l6Rl2k4yw3ndEz2A= +2n6OKffnpctHA9iLBdLftCjTYTB0+MA2s3UeGpHkjmmd18Pz2OnNoeI3qHoxFA24 +32pdC9DD05OE2l0oXazDFAy/+2z+GSJU4Tvn6nr5ymNE8T4+MwqJlcj+UtdgrwnhPZFbP3Ie/lhKQDVRXFcQkNowU/FOvKAjvpveSX0UVePp39/hXQw80/Y+fNs5+291UZGnduOcOTprruxrXY+KlQTlTCwdMg9zWdhAmYdbeTbd+fddlpQOBFG4rlhvhDnXtkAQHgXgoduqV6NO+Zkv3g== +1u+XjG/2+GSQRv6EzCaWRQ== +i3q0rc3DNSm9lJpIhv//MavLcKoA5wmJ945KuJzmuHo= +ZJ2ZYiWQdJUCWkE//4r0NCfNnqY9G3Q73P2f4Aro4jqCGj/7LLVvGr332+QukBSaBYdWKH+pjGC/xyJBWz99kw== +hBWY30cr8RshUe3F5HK1ccg4rT1kIv0AJhkTC98pA0vrCtuVPwiWWBgWi7dSFi3S +DOZzACM3rMJz/5HoVkWhTAJRYjTgu8bbUR8ehtOm5U/d4TyAg0mg3WpauSeRow8Y +32pdC9DD05OE2l0oXazDFJZeI0y+qWsI0YMRMpHa7RTnKgfplydp2n5wgWdV/pIF0Ftd3ReUh5GWOp07BgnNu80faUNzKCfXokt9yQivK+mvyfjwfw7Ch9kMg6kj61x915ce/BnFvY5nAh2PhmFazdnDfH3RSEA0VEFEFACKroM= +l2FJPs4YkAmmok1ulDRuSA== +32pdC9DD05OE2l0oXazDFJU3nCYoYJj+nlJTs9Z5eqc8W0KCpCnIGzSYVI8k24tkdtksbXboLx73K9kAfePh8w== +pVMCrZ7PAAHymZ70WROm/0U0yYoo7XCh9SJpAqZIAe7JWLjXcOW5OehWzz8mGPHHLHmDnXP/8yOBcDuWqjRxug== +VmVrGQo2zRokW/ZuO9bN68p3CvferpZDfUPfIejFq4dgtDRbn9+EC+U13hcVN4xfDuCE2jMhnt6Ks7bjLO652A== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67qZ2KUV1f4tx00lgYmOjmGe5kSqLAQ88sGlFafMPtA6 +1u+XjG/2+GSQRv6EzCaWRQ== +aGle5statgVMNf7rj3OselC6TIMMyoWDroQ0LnUnNBIeqLqsE+/ACbWNOgr3XHbZ +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +jEPV0YXDCqWCap/VHplvwQ== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN6+ugdaK72k5FDIN67ApiHeWC27mh8vSSvqLTkawZzG41 +VmVrGQo2zRokW/ZuO9bN62GGKQt2S9DKmxGER0w1v26K7kSynx75p86xpYrJ5cQA +1u+XjG/2+GSQRv6EzCaWRQ== +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +1u+XjG/2+GSQRv6EzCaWRQ== +jEPV0YXDCqWCap/VHplvwQ== +b4OJVZe8QyIpjuTpKXDL9A== +1V2v8QerKOmubvSxgB4eTB+P6Y06+m1KdyBFjy0y0qtDlVzyAzDndoVazuBea8ILSnWO8RuK5nvd0GXel7i7OVWCgkT7TP2NsLLVZ4mNqCA= +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrSo1ZAQ1FHnoPT0QBpT+y//EDQqEyU2+LLb0Qw50wZ849Ngf7YNt+KN1kLhyxEe4jPeWz9lyneG7AF4gK7wF4ng +89eRayX3G8tCX3FDU5IcjW6dZBKA/+IhDK3LhrTo6ym5cKSsa13rNj/ij10+KPkA +VmVrGQo2zRokW/ZuO9bN65BnQ8umChPy/sCxoyCmwA6LqaszzxrGnLukmnEqUv/u7z/U4QL/EzWxOm9svW+RH7G3eS2YkqUDqEJMrocMpMjyNUyyau2nVUKm9ERSLnjZisO5lnjNt+iGn0k1+yVZuw== +VmVrGQo2zRokW/ZuO9bN63A3xkU1lR5ZG1fBlpSh1c39T/3k+G4riKOjuqtHOnHMvvZvWkuOS51CC6qk4z5jnw== +89eRayX3G8tCX3FDU5IcjZio0hoGCtUO5KLqNdaB3UvURpBSOFsH1Ry2hhlpu2a3 +VmVrGQo2zRokW/ZuO9bN67wF+ldYt2W5ABEV4Jd5XnXJ5RbZPS+QKHdMwhNEDj31W6sIK4FsnIkLRJ3T5iAlNTtJ+RADG0QxCaWthy51S4e3LMPtYqvYmAIJIceoifGgj3qzjx5Mineh49VklXh8fw== +VmVrGQo2zRokW/ZuO9bN63A3xkU1lR5ZG1fBlpSh1c2HA0D8g9S9SMlaYHmeizpvWd1tnv8EXrkDAlHSfl2K/A== +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+mxGr/S7GRYgYWJrDIVeTUEC7H9MrmNwE6ENdkNQNBMlesKu21GOlwV6/lxOt+J9huukyIEOSNsWSANtpaf6kTA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXrx0RH74rzP+DMEHwpOwZ6d7bNaVWJEBrv6ZnZvyvHTAUE3baFmml5mUO5DXOnAC0= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5hqfprx+1n+iB1xwtsTjMm8w== +1u+XjG/2+GSQRv6EzCaWRQ== +vu8LS8RURN7sv+M4P2dSXuO5tOb5QA7mvTF0Q9xyHHoLcawyWgXdBSleKQcT19Gp +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +WHkOzVx7seuLmxs5Hu+l/gGhdEQDXfh3xxE5uPrZ9HkTaj8ei4z/I4Hs0fkWoW71FZtVCvpoLhrkg0DQhl0+zw== +NJQ+lum/oS33aJfKUum6ZKxq4v9IIG63s8wgAJpxyKKM3caX3PQBfK4hpVfelFefX0ha1REi5kkFRq4UVngkqz5Lga8eCOtZdzGW7Jx1e30= +PozHY517ojvTteRYsQkDRQ== +l2FJPs4YkAmmok1ulDRuSA== +NJQ+lum/oS33aJfKUum6ZGgG7m/Vgf6+MaLfXHjBYbG/boj/5Yf8rziyiMSNVNgO +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6t++CgSX8cAeNQyz47sn+GYtv0wtQ5PkRbw7visX9vww== +1u+XjG/2+GSQRv6EzCaWRQ== +fa6pp9hhUaHk9Koiu9s2ccIwXt1dcoJZB0wflEr0rwQ= +5GHtWZO1ZyuzhP6BpZ6HURY9BsEKvY5DDw7ChNy1rP2CD3rFiV2pVl9EEquVbu+K70cLSEgxArw1fmU6IVnzMw== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +m5EI3+BXxkQEYKYaxORD+qnAwk1i08GBypJtuz97N8s= +gZ7BoIAABc+I8g/rYzq+fz57S21B9Wyve1/Agtg7QmQj09si6ROVLmG2qVyyY9lL +1V2v8QerKOmubvSxgB4eTFLJVKokCn/n7cBzRCyjPmemCDNpavHplLKoYC5ZrVB11XPhYbqw8m0ksUK1fSNNJw== +1DFND+jtCuxjwXKqEjfnlD8JflvVZY16BAKNx0Ys8oh960PCawXn1DhrDIgKML2IvSaCy2FdK4vEawkKvbv+BQ== +1DFND+jtCuxjwXKqEjfnlFHSx+D7aFIV7RDIckCpWqiZCZOA+pgT/6kaWqauDZPr +wlLHv6kT3Q/RmtMBN4nDAe+nVsDSSjET98LLrywMOlg= +VmVrGQo2zRokW/ZuO9bN68umjQbqBp13Y8gxV9ie09H26ZhdYobd0e/IELC7PrX5Z4UEiAF2Lx0Kz2qNM9xtPg9hRCHPWmuRACx1FUjOq44= +VmVrGQo2zRokW/ZuO9bN61VUR3BEZJhw9HmBnyL9cM0= +VmVrGQo2zRokW/ZuO9bN68B6yBwdveCrEnL3eIPPl54HCMNcd7dZQ6V1Asu/4VOz +VmVrGQo2zRokW/ZuO9bN61m5Mxpu5OrMp8lEKzTstyW5t/Bxrf5FCuK09Q+Qt3Xb6zAMFKTKqMZ2bc8mvxMoYA== +VmVrGQo2zRokW/ZuO9bN635+naSBCfSoIFNU/mkWfEWvjx4qi995kng/6+ZHbkng+TzkNmb0iPG4cwuUVQI4ZKE+pWqvwN56kWvRb+8GUHM= +VmVrGQo2zRokW/ZuO9bN635+naSBCfSoIFNU/mkWfEXU6i2AICYZTQUe3CuP+KX8Hz39JbYJuMJN0Ey4/Kag6hZfrxZVopZx5Vy2glJNcGhvSsoSefu97SdpMTMYIKLD +VmVrGQo2zRokW/ZuO9bN6x3CEv6jZSVeTORx9EyCuuzE3c5Og66hLkfYnT65i/W/OHaxtXp/DcinMIa68QkDOAXzT1tpGn/G/1g004E8vTAvX10EUKwrhYAHwUOqjpUn7y/swzdApkXHDcJCO4M/ug== +VmVrGQo2zRokW/ZuO9bN6+qJ7ZwQdyiVmEJTiPQFIpKxvidM6dYKv4J64fba+Rv7 +VmVrGQo2zRokW/ZuO9bN6+GUsXdTMrUNGRoRF00pmbi60tgVb+d/2FD5rl281dqBNBRi5pQgGepTSMp/mK0IQb6K/t8paD6dYTpkLyU/ixxO4Yjcrsxdty7NT2h70NXG +VmVrGQo2zRokW/ZuO9bN6xEF47qKkxcGSQA8ZWEPDm4VsVyapvAq9bi5NYznw6hBoE8A28CROx2B7L7NPcqnMKPQSW9Q2meFjEGt590eZ8ikIT161qE/eWvIkfSbbh17 +VmVrGQo2zRokW/ZuO9bN6wdLYDV40oCO3TihSbGXJrZpMrfTemTStPcnvjmgnZdZNGjQQU3YFlc0N5lgKjM+sVfOqXPeo8ffNXtAXn8U6aw= +VmVrGQo2zRokW/ZuO9bN6zAPD3Xtk2YvfiqYLDohteTNRe52aljpAXhFzxSTpy34 +VmVrGQo2zRokW/ZuO9bN62u0ieXkBjpVkLjCpl9rmj10e3NBgGxfj0GYAQ/k87UY +VmVrGQo2zRokW/ZuO9bN67NTMXohKhLE0wOxn9dpXIG/8QEXIQaD4WsCbsRYT70j +VmVrGQo2zRokW/ZuO9bN61MEyHBjyzCNpNUpxJmXAqGns7E0FYe0ozkLUuME1Deh +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +8539W5My73ns3Q/u5rchhsyrOAomtnoexAKSg0NQa7s= +JNBOjio0wSMJSstABnh/zOiQQTikw5fWysUhLsbb18DmnkrHXpVy4xzmOQf8bph7 +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +QKClQ4XOaAjfVBfstd1swg== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN62PrQ58ZmOS+taM0eglCETGQ+8yO6ZBXlg+K406izWf3 +VmVrGQo2zRokW/ZuO9bN666LtvcJ+EKliohMh9tjJiFYAV0/gF3Crr6YBKyInU1P +VmVrGQo2zRokW/ZuO9bN6yLfMPEhyISD+REiXhi+kj5tXhWAiEhsZ3LNE2aBEOBX +VmVrGQo2zRokW/ZuO9bN6w0ttCFjDKrgfwVo/waQ6MI4G7C0z84Mo3p/n+iP2Czx +1u+XjG/2+GSQRv6EzCaWRQ== +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +09UuO0tw3kJ5J618hVggcQ== +QKClQ4XOaAjfVBfstd1swg== +b4OJVZe8QyIpjuTpKXDL9A== +1V2v8QerKOmubvSxgB4eTB+P6Y06+m1KdyBFjy0y0qt35NiD23nMDnboayOgm+qxRX16P6c61gdhdoE8cyqp434kBVVyEegJqMSq/9+j7qRYBzbpDTJclNP9AbLFWP/aYoHnZdjB3tUKsiTOYaWMRDDiKUJnEa0qsXpZMb2GdgjMPYZ2akb991jFm+56P1iO +VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrSuY4P/AlnqUWfNJ3Nv+yymjs8C+eB+Hn8dWS7DzR5wCEVi+pzzp4DxubDifplcmTz8xv5V7wP5rejGKOKJFZz4 +k1ktu/l6HFULD7Vyr8Cv0Dp/BLIuC7jc8lO034+ASSh12djXtIbpBUNxeWBNIvYRsNqqiKBD3YtyfeT5KRCKiA== +1D18KWr2hdVsBcdZx1OaPdYttuuMd+Wlneqh+fYf8tRS06YfvvjVykA0Ly3MBJZu +VmVrGQo2zRokW/ZuO9bN61b/OIj1n5I/egQV/irCI9ZF51EgBOVnJEOvqc/A4gWV +VmVrGQo2zRokW/ZuO9bN63OlqVFuSrE4LBwNUw3bkoDOavUodkFgOuITmFHLjrHA +VmVrGQo2zRokW/ZuO9bN61b/OIj1n5I/egQV/irCI9ZNKsuaBZPcCMLzgnnpuw9keZNTNp6iIMOAdW/dgsNpMefz9HvyioBehfZZagurPe9ROWnsKmrd366nsKzD2VyqHbZr56qX+EZbTpIgBMo8bfcma1+HcAM+UztJ8J2dcrZEO3hwMwyAZbNd+XHq/TJZ +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN60q1SD2rBZYBmqTssi+cBVTFolfijy7G1qUgw5+55fOlizRwtfrPbLo+QD4LQz/aXQ== +VmVrGQo2zRokW/ZuO9bN65ab2Motzh1/u4tD7gBJGx8gUZXzrg55jEQ36akEJ1ey +VmVrGQo2zRokW/ZuO9bN61b/OIj1n5I/egQV/irCI9YXQ8qfav5RO2NqR8STwJts +VmVrGQo2zRokW/ZuO9bN694CKy7hg/jAQggMQ3cflJfUCrGGd9dIzrTC0FxwfEf4 +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+mIkOb+TCfJvH/w7nenmNQi1zWK57JebtAOzfMq0wpoFGdgaOOSewO4tjZybH6SMo4= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXrx0RH74rzP+DMEHwpOwZ6d7bNaVWJEBrv6ZnZvyvHTAUE3baFmml5mUO5DXOnAC0= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5hqfprx+1n+iB1xwtsTjMm8w== +1u+XjG/2+GSQRv6EzCaWRQ== +VJqiSZUHAadJPm6IbO3XLPzVLLft+A1arsi3RybmnvA= +JEnsSsP7ogTJg1OXQ2YdoQg+5yBMz0QMX9uowke0S1ZHptfxrPs60KFAe5xogoyb +s0qLHwvmLw5nYPphiA6IN9NAVh47aVFuXfLEn+2tA/+IR0o4fvmeiO6qsseNVLAaEtREzItWxPogUFQTRVlGyQ== +W3A5Dt+8AxWSKsTYeXZoLzB4TUiScS3D8yuLKBmm3AwWtYcxV6f46K0T0Egvi5WKZ+IFtEVQSkI1R2/103kQoAd9RN+l2jYL5AuJdiBc8U4= +1S1xjYS0s+Ph1PyvECx3QMdMFDZ5B3+BEK8kyCfI1RM= +VmVrGQo2zRokW/ZuO9bN63KvuXi+Rau/UofskUTLsKQ= +VmVrGQo2zRokW/ZuO9bN63pkeY7E1wCFlSgT4To6rBxvA90+4fs1yFGrKqWxjhoI +VmVrGQo2zRokW/ZuO9bN69mxdR6fpagkC1QpuDN633U= +VmVrGQo2zRokW/ZuO9bN61jw9zMkfm0nvQi9tveiWV0= +VmVrGQo2zRokW/ZuO9bN67PbAfk7/vgNjya2BB9mgLo= +VmVrGQo2zRokW/ZuO9bN6ztX+cJqy4yqv63JnfA/mqI= +VmVrGQo2zRokW/ZuO9bN60jLKcJEMj36X6NZvvwxF/XvqrmJF3W3RArdDHrkgZZ6 +VmVrGQo2zRokW/ZuO9bN6zskyREk1B9SpIIVpfE36zU= +VmVrGQo2zRokW/ZuO9bN63leNhHVk6qBYEqAElCrxOXVPHIpJ8U4k7mou4tbIR/7 +VmVrGQo2zRokW/ZuO9bN68tloVdndHDn6gJZRayJG+k= +VmVrGQo2zRokW/ZuO9bN617eR9WvYqaE7y/ppCzNL1570CCRJc668VdVufs+tRvC +VmVrGQo2zRokW/ZuO9bN6x59qEj9owkguOGj8QrHQSLi1Q/1C75rARTXu4TGt8DP +VmVrGQo2zRokW/ZuO9bN6+3Qehls6zipcbnAYG52vS64qsWF7n58MJKONK4HivTE6Z0+CcayS8zXD9YppbFZ+/J2GrNDCfGEkmX6/AnIs/dP1CmXS8H+LlZmD+d0IrG3 +VmVrGQo2zRokW/ZuO9bN67biU+BUR7/d7BkQj4RgpT8E7BfCa3N4KcxGo7ivT2Hg +YH231WTDnzQG3bFilBiqIg== +lOh2GtzHjjMM8E9J40AuOcPN2eZCahfvScF49QHmlNXYE5PQWq+WYPigt3dG9DzvOjcs8WVNkHQ6P1pZps2Ehg== +UbJuac0dxLiN+5ocS3w2vTpUyzdtCQ6EQJg8V3275Z+ztU2/bGujfta5s3IG3yXD +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +aaMaQb4hNG5aY797TkrqCWfiH67nNDOngFCD9eKQBnU= +VEyEXJTNTVvV1TGW5V2yEJROi80UtMZ4Bfk9N1lPrUw= +s0qLHwvmLw5nYPphiA6IN9NAVh47aVFuXfLEn+2tA/+IR0o4fvmeiO6qsseNVLAaEtREzItWxPogUFQTRVlGyQ== +asVRHT1bqj8y+BH3ysf47YxzfxL+NexJMVBUNP6vrnufWCZJGD7MwYVGA8oeYsgFpKnSmDtvp3y/fiLSpk98IcZD1DZf0DjJ3SyzH+qyYjM= +P5FSiL1+Pqt0jNjlHNtw3PUsQ1HQjY3Zc9yP2+2FW8Y= +SHIgz36q1FYBLUgTHpJzWwvMCDfalwhI+vhlVkKru41zqEPlk9JS5loV/C9k/6Io +1u+XjG/2+GSQRv6EzCaWRQ== +hwrvMIGc4R8Rz+DcReSgt3VO4XbBq4/Dc26Csib6yT0= +uTEK8Ng11d3ix2pA+DD/aYq6iiRaSOVPA0xTeOGnO8KEq+qETa/PY3c3TUpiGOuA +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX4HVccALfPGDxT2Bl+uK54CjgC4dHORIP0UG1n3cGiVq2uzn3cFb+KWWE+PC2KoT4= +MzOj4ZiBOJDNVP8vi/lFQNI6cdfRYnHbX7jIkalGpxc= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmU8ccdG/y4bwpPH8ZZkAG5h0Y5XPh3OlhnUXvGEUQ2KWe5qKrCw4xrfZpU/bXhFhZBlnMW+ZvqnU4LLYDO0/H6M +3alpplMHMPiWRaABg96vtFa0DPujF6W95o8WfJVesvcYr94rSBkhGXG+3RqspvvAasMhJkqh0lRb4WSYwFcQ/w== +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz7Jkrj3Ngyu2cYVDtwlmqFHAh1dGKymlbb0LFY4063fRnZ236zioiutx2EgqM7OqP4= diff --git a/class_v2/logsModelV2/panelModel.py b/class_v2/logsModelV2/panelModel.py new file mode 100644 index 00000000..4f9c9986 --- /dev/null +++ b/class_v2/logsModelV2/panelModel.py @@ -0,0 +1,321 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: cjxin +#------------------------------------------------------------------- + +#------------------------------ +# 面板日志类 +#------------------------------ + +import os,re,json,time +from logsModel.base import logsBase +import public,db +from html import unescape,escape + +class main(logsBase): + + def __init__(self): + pass + + + def get_logs_info(self,args): + ''' + @name 获取分类日志信息 + ''' + data = public.M('logs').query(''' + select type,count(id) as 'count' from logs + group by type + order by count(id) desc + ''') + result = [] + for arrs in data: + item = {} + if not arrs: continue + + item['count'] = arrs[1] + item['type'] = arrs[0] + result.append(item) + public.set_module_logs('get_logs_info','get_logs_info') + return result + + def get_logs_bytype(self,args): + """ + @name 根据类型获取日志 + @param args.type 日志类型 + """ + p,limit = 1,20 + if 'p' in args: p = int(args.p) + if 'limit' in args: limit = int(args.limit) + + stype = args.stype + search = '[' + str(args.search) + ']' + + where = "type=? and log like ? " + + count = public.M('logs').where(where,(stype,'%'+search+'%')).count() + data = public.get_page(count,p,limit) + data['data'] = public.M('logs').where(where,(stype,'%'+search+'%')).limit('{},{}'.format(data['shift'], data['row'])).order('id desc').select() + + return data + + + def __get_panel_dirs(self): + ''' + @name 获取面板日志目录 + ''' + dirs = [] + for filename in os.listdir('{}/logs/request'.format(public.get_panel_path())): + if filename.find('.json') != -1: + dirs.append(filename) + + dirs = sorted(dirs,reverse=True) + return dirs + + + + def get_panel_log(self,get): + """ + @name 获取面板日志 + """ + p,limit,search = 1,20,'' + if 'p' in get: p = int(get.p) + if 'limit' in get: limit = int(get.limit) + if 'search' in get: search = get.search + + find_idx = 0 + log_list = [] + dirs = self.__get_panel_dirs() + for filename in dirs: + log_path = '{}/logs/request/{}'.format(public.get_panel_path(),filename) + if not os.path.exists(log_path): #文件不存在 + continue + + if len(log_list) >= limit: + break + + p_num = 0 #分页计数器 + next_file = False + while not next_file: + if len(log_list) >= limit: + break + p_num += 1 + result = self.GetNumLines(log_path,10001,p_num).split('\r\n') + if len(result) < 10000: + next_file = True + result.reverse() + for _line in result: + if not _line: continue + if len(log_list) >= limit: + break + + try: + if self.find_line_str(_line,search): + find_idx += 1 + + if find_idx > (p-1) * limit: + + info = json.loads(unescape(_line)) + for key in info: + if isinstance(info[key],str): + info[key] = escape(info[key]) + + info['address'] = info['ip'].split(':')[0] + log_list.append(info) + except:pass + + return public.return_area(log_list,'address') + + def get_panel_error_logs(self,get): + ''' + @name 获取面板错误日志 + ''' + search = '' + if 'search' in get: + search = get.search + filename = '{}/logs/error.log'.format(public.get_panel_path()) + if not os.path.exists(filename): + return public.returnMsg(False,'No error log') + + res = {} + res['data'] = public.xssdecode(self.GetNumLines(filename,2000,1,search)) + res['data'].reverse() + return res + + + def __get_ftp_log_files(self,path): + """ + @name 获取FTP日志文件列表 + @param path 日志文件路径 + @return list + """ + file_list = [] + if os.path.exists(path): + for filename in os.listdir(path): + if filename.find('.log') == -1: continue + file_list.append('{}/{}'.format(path,filename)) + + file_list = sorted(file_list,reverse=True) + return file_list + + def get_ftp_logs(self,get): + """ + @name 获取ftp日志 + """ + + p,limit,search,username = 1,500,'','' + if 'p' in get: p = int(get.p) + if 'limit' in get: limit = int(get.limit) + if 'search' in get: search = get.search + if 'username' in get: username = get.username + + find_idx = 0 + ip_list = [] + log_list = [] + dirs = self.__get_ftp_log_files('{}/ftpServer/Logs'.format(public.get_soft_path())) + for log_path in dirs: + + if not os.path.exists(log_path): continue + if len(log_list) >= limit: break + + p_num = 0 #分页计数器 + next_file = False + while not next_file: + if len(log_list) >= limit: + break + p_num += 1 + result = self.GetNumLines(log_path,10001,p_num).split('\r\n') + if len(result) < 10000: + next_file = True + result.reverse() + for _line in result: + if not _line.strip(): continue + if len(log_list) >= limit: + break + try: + if self.find_line_str(_line,search): + #根据用户名查找 + if username and not re.search(r'-\s+({})\s+\('.format(username),_line): + continue + + find_idx += 1 + if find_idx > (p-1) * limit: + #获取ip归属地 + for _ip in public.get_line_ips(_line): + if not _ip in ip_list: ip_list.append(_ip) + + info = escape(_line) + log_list.append(info) + except:pass + + return self.return_line_area(log_list,ip_list) + + + #取慢日志 + def get_slow_logs(self,get): + ''' + @name 获取慢日志 + @get.search 搜索关键字 + ''' + search,p,limit = '',1,1000 + if 'search' in get: search = get.search + if 'limit' in get: limit = get.limit + + my_info = public.get_mysql_info() + if not my_info['datadir']: + return public.returnMsg(False,'MySQL is not installed!') + + path = my_info['datadir'] + '/mysql-slow.log' + if not os.path.exists(path): + return public.returnMsg(False,'Log file does not exist!') + # mysql慢日志有顺序问题,倒序显示不利于排查问题 + return public.returnMsg(True, public.xsssec(public.GetNumLines(path, limit))) + + # find_idx = 0 + # p_num = 0 #分页计数器 + # next_file = False + # log_list = [] + # while not next_file: + # if len(log_list) >= limit: + # break + # p_num += 1 + # result = self.GetNumLines(path,10001,p_num).replace('\r\n','\n').split('\n') + # if len(result) < 10000: + # next_file = True + # result.reverse() + + # for _line in result: + # if not _line: continue + # if len(log_list) >= limit: + # break + + # try: + # if self.find_line_str(_line,search): + # find_idx += 1 + # if find_idx > (p-1) * limit: + # info = escape(_line) + # log_list.append(info) + # except:pass + # return log_list + + def IP_geolocation(self, get): + ''' + @name 列出所有IP及其归属地 + @return list {ip: {ip: ip_address, operation_num: 12 ,info: 归属地}, ...] + ''' + + result = dict() + + data = public.M('logs').query(''' + select * from logs + ''') + for arrs in data: + if not arrs: continue + end = 0 + # 获得IP的尾后索引 + for ch in arrs[2]: + if ch.isnumeric() or ch == '.': + end += 1 + else: + break + + ip_addr = arrs[2][0:end] + + if ip_addr: + if result.get(ip_addr) != None: + result[ip_addr]["operation_num"] = result[ip_addr]["operation_num"] + 1 + else: + result[ip_addr] = {"ip":ip_addr,"operation_num":1, "info":None} + + return_list = [] + + for k in result: + info = public.get_free_ip_info(k) + result[k]["info"] = info["info"] + return_list.append(result[k]) + + return return_list + + def get_error_logs_by_search(self, args): + ''' + @name 根据搜索内容, 获取运行日志中的内容 + @args.search 匹配内容 + @return 匹配该内容的所有日志 + ''' + log_file_path = "{}/logs/error.log".format(public.get_panel_path()) + #return log_file_path + data = public.readFile(log_file_path) + if not data: + return None + data = data.split('\n') + result = [] + for line in data: + if args.search == None: + result.append(line) + elif args.search in line: + result.append(line) + + return result \ No newline at end of file diff --git a/class_v2/logsModelV2/siteModel.py b/class_v2/logsModelV2/siteModel.py new file mode 100644 index 00000000..7013e17f --- /dev/null +++ b/class_v2/logsModelV2/siteModel.py @@ -0,0 +1,110 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: cjxin +#------------------------------------------------------------------- + +#------------------------------ +# 面板日志类 +#------------------------------ + +import os,re,json,time +from logsModel.base import logsBase +import public,db +from html import unescape,escape + +class main(logsBase): + + def __init__(self): + self.serverType = public.get_webserver() + + + def __get_iis_log_files(self,path): + """ + @name 获取IIS日志文件列表 + @param path 日志文件路径 + @return list + """ + file_list = [] + if os.path.exists(path): + for filename in os.listdir(path): + if filename.find('.log') == -1: continue + file_list.append('{}/{}'.format(path,filename)) + + file_list = sorted(file_list,reverse=False) + return file_list + + def get_iis_logs(self,get): + """ + @name 获取IIS网站日志 + """ + + p,limit,search = 1,2000,'' + if 'p' in get: limit = int(get.p) + if 'limit' in get: limit = int(get.limit) + if 'search' in get: search = get.search + + import panelSite + site_obj = panelSite.panelSite() + data = site_obj.get_site_info(get.siteName) + if not data: + return public.returnMsg(False,'【{}】网站路径获取失败,请检查IIS是否存在此站点,如IIS不存在请通过面板删除此网站后重新创建.'.format(get.siteName)) + + log_path = '{}/wwwlogs/W3SVC{}'.format(public.get_soft_path(), data['id']) + file_list = self.__get_iis_log_files(log_path) + + find_idx = 0 + log_list = [] + for log_path in file_list: + if not os.path.exists(log_path): continue + if len(log_list) >= limit: break + + p_num = 0 #分页计数器 + next_file = False + while not next_file: + if len(log_list) >= limit: + break + p_num += 1 + result = self.GetNumLines(log_path,10001,p_num).split('\r\n') + if len(result) < 10000: + next_file = True + + for _line in result: + if not _line: continue + if len(log_list) >= limit: + break + + try: + if self.find_line_str(_line,search): + find_idx += 1 + if find_idx > (p-1) * limit: + info = escape(_line) + log_list.append(info) + except:pass + return log_list + + # 取网站日志 + def get_site_logs(self, get): + logPath = '' + if self.serverType == 'iis': + return self.get_iis_logs(get) + + elif self.serverType == 'apache': + logPath = self.setupPath + '/wwwlogs/' + get.siteName + '-access.log' + else: + logPath = self.setupPath + '/wwwlogs/' + get.siteName + '.log' + + data = {} + data['path'] = '' + data['path'] = os.path.dirname(logPath) + if os.path.exists(logPath): + data['status'] = True + data['msg'] = public.GetNumLines(logPath, 1000) + + return data + data['status'] = False + data['msg'] = 'log is empty' + return data diff --git a/class_v2/monitorModelV2/base.py b/class_v2/monitorModelV2/base.py new file mode 100644 index 00000000..b80b6a24 --- /dev/null +++ b/class_v2/monitorModelV2/base.py @@ -0,0 +1,16 @@ +# coding: utf-8 +import os, sys, time, json + +panelPath = '/www/server/panel' +os.chdir(panelPath) +if not panelPath + "/class/" in sys.path: + sys.path.insert(0, panelPath + "/class/") +if not panelPath + "/class_v2/" in sys.path: + sys.path.insert(0, panelPath + "/class_v2/") +import public, re + + +class monitorBase: + + def __init__(self): + pass diff --git a/class_v2/monitorModelV2/process_managementModel.py b/class_v2/monitorModelV2/process_managementModel.py new file mode 100644 index 00000000..31a4375f --- /dev/null +++ b/class_v2/monitorModelV2/process_managementModel.py @@ -0,0 +1,560 @@ +piG6BsMF31u4R4iA6R4SRA== +dTLQ8Wr3wPn7w6pte1B1YA== +SbQQ5SqO9QrBwZ9ObpMYLw== +aB1r/Hl41eeDYHjvKNsDN+fwXnHZ5cKnjNQNkvt811Q= +wiYVtfO/yzajW1Tv5z7hyA== +bbgfjwWjzPNOxnsxTb5hzQ== +1u+XjG/2+GSQRv6EzCaWRQ== +XIfdJ79nObMM+vyAmKbTmw== +DjrnV2ugcXpCQCRtr1AhjOqKcFaCI68w9rOW7RONBIZ5ibaGH1F5fN1ozytPQ6he +ic+a3PsSrb0KPm4AsF9vXtHZSKiYneN9Rh+9RnSNB9WK7HTAVn7HvWa3LZoeRP2P +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +GC18aI/weGPndT9J1gQkOXLINKNA+KXP+Dhe1VxZyWA= +HuLSKhYFyga2gCsAbUvtZHLP0BAfxDGimJZciagBXbE= +i5Pjb3wFWjgWhO/Oy7348x0KCcyi6IVF8qqP7v32njhzG+8BdTBQyqlCg3CRdkP+hkP1ySPJM/rBG9E6T++rIg== +rdSLxsl+Jo+9wGF1wR0xFfkiBqqHhxwN5kl0Lj1eqZJZBJ0Un+czbpqSSOCnL00rgrs3FzTKbr1MIAbXu6UVGg== +0SQnak5isFUZ7qBrHG5nONf0/iIYxo+ossW+sIeJPVqc06D+NOpjaE6Uqq4RDZ6D+X1V5oSWDCNufz1Z5NsruQ== +QoXjlU/yrq3LjBi536E1lAtUbyzoaPGqmdMscHtc82Eqnx3xaK7ND//Aee6pjueUoSkSd7Kn/GTgusZOZkazWr735TNoBy8E2i+TeT5QpBw= +EnTcTKmPyFkaGN9DqDzaSceq8uDBrg212ADWAEQ4IZ7RrbWtCNgCxsKxhOwBdNTIiDKGNrYM1guBNGIg+GP4oH9ldbTHxPoem41Xx3gcVH4= +VdrjM3tESbDpF9uC89FIBP9Gd1akXGqOvVf2yxPgOSm6+6n45/Opv1Gr2Z6wLcSiweXQCxy9SU/Ps8OhjacAOibd0MvWgOEF/Qj8Hg1OpD4= +23L+yCPrnnJuqxnn47seC6mWE/7wPUK89o8BQ4gkIOBjwQKKbOoe6bUJw6g30fgZLJKIywqZgDEqKLNb6Wk4ZA== +ls1D8gXPZvKpYcbhjFq/angELE3042eHnGYIIdOc3EqVbEv8X3JJ8d4kjYJRVOzknDfBdJPlavOFcLuc1C3RlQ== +Ucs0nOwLNKLYeguD5Ho6We236+pzwCXfjIf8zCbNBPUj8Trs/3tdQqbzpyL2y/A3nGNz3pdXfnPIeWgvZPJdJCQ3oPUcIKLTYx+hNA8fP3A= +1u+XjG/2+GSQRv6EzCaWRQ== +PdlH+lu7jNu2OvYS8I8SIGv8UV7r+RCWsVIOF9/nLLA= +EnTcTKmPyFkaGN9DqDzaSeIxT+jPdlkzsjUgVGeo67M= +lfXDVmh5N4vkcCpP16rRNU+nS75ACFRPief37RRqVEQ= +QoXjlU/yrq3LjBi536E1lNTH28NopdOB61QQlHyGFcA= +UrqM+fZEnAnh676kBTiBqQ2F5qpH1BZ72+KLbvQGDMU= +zKCrIlbP/DDhVHiZ3fIxklV8gb38KK+GbBXLALoFwPQ= +VoVkMP9tW2A1crYS1JPcez/T5g9qr04wEneJMF3RRlA= +40d0Kjq9eFveZcVdlpFWHqsMLb6Cbgq596PvxkzyPPs= +KIZi5iGAZ1ERVsYyf9tRFc+B9Yz08eywa9GtzLEWwb0= +CCWp04qZQAG0xvBg/GxA1ac8M6aDoxCupSpEbINrd14SIsQMCX2yg35fZKfSZ45bUlwsnEKO6CphPTsCe870BQ== +ELPZs21xKKoOnovAKJosLe8y3drsV3LCJ9rBZ59KMXaSIKv2MS1cInuGvowyQR6/da+MfD6aJgPi0OIyjKl/TA== +0aJdVaqIprJqIF2ACVD434ZUgkgWJjA1UrDXUzIX7hLHfRqO7xfTfRdmPwLbMjnBsyI29qYoF8SnX188Ei+AEA== +cEDtw6uMb2/A6mWG+mGficxASjA1PsBLQqmyk6avgrA5NU4ovgWemvxpF4tbZuYyAvelvkrb1my6fx7rBEPUgA== +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +oKVaGzjnS0ibnrCRJ6VhOXjlPFbbLUbXwFxCuM1lBik= +x4aYGEXHUiyydN0TvGTj6vQYATZwfiFdhjHTFnA8Gao= +C/AADmj7KKNzSBTrmgjwHtkMmAZdvtiecPcfgdxsdp4= +Oonoanjo57cFaJw3N8v3+Re7+iV0uPfFwq4o6iaSCBM= +sTBwU4/6vbNIKaonWCsmPxBuizocf0lQLoMzCcsQdOE= +GQ8bP5FSB8U92LYKcbOqVPtrzArbXQw92sj1bGuzJ1Lzc0ykynAZHg7W7lIh9VCyvuy1xx0xiuBt1I7P54NmCcm3dxPo0HOwwo7tFDhoPu+yzldqqdrWTSIRrCoxLw+5YpASeQRxYjEULaeTwkqePveTZpraNCJDzhOxUhZMIOjZFUfSB2xXKuI/iazrrLYM +6XekAdRuN93speBaqeGdS7mZ+YDSAF/1X41AUJfffwgDxVBNvGTIh/BqnzHblDVEvD5QZ86VvPZQOOmgg2lVbc/yUOnfJgQ4ct4qN8vJ9S61s4UZ77FCLYvI5zgjpBa3 +Ct7YEe25Op6jhkRit6HXSSZJ3/Z8e08T/asIJPAS+JBiQpX5IzKziQ+RX8qVdG0kcWgO+eRhWTxs+8ABKecvwkYRx2jE2A3stRu9WnJ1usM= +hTUqYvcYvtmpKOewm481/HtdVHS5tCgP3k907Eb+/iNqJkMKXLfNx7388XmgCPT4y5b8ijy8mCMYYFBgxS+Y0PsJa+5ddHmDK4MoaD7zkxy4lAUla3fx83H4XA/p6emPuqNg6mWcazm2Tg7vwmSRYY4Gs+oh7Bef0ZSfwvpMdC3pgSvu6bTZx+abnOM0VmGnPvQNxptUCE9WZugB2G2CUQ== +QP9mdDvW8SzUpgVgx/NNy+PQKhOQCI9rUmKBAXP10dQohDuTfGSFbX2wVW1lE8+KWgSYsWVlDUNEoE7fuKKjg9eahEWTIGX3v6Ox+oMhYA7IdJYsJfG+u28Xblk0g3t/GQsYgE32MZxhDOynNbR5eA== +mtgwn5Yb9q2LR0oKaA1GDK35p3socJhZP/ty80mesNBoCZh8/LZIyqglfgIVeQWfDoJMlSeA89++zDh+b/cl4PZWkjjhkhxGMA1+/4R9GeucHxc0N5RGTUp9GUl8NFzhnJBAbItJO/Ooj8Adr/iqems+7nd0AhmOz0XdBpC8EkQ5O9mFl+bYDPxKMWVcK7hX0T6spCqOX8IvONthb6z56zjLZcgoYBYY5IceJEA+013xOI22RXtRL8aaOIxsWDdST2Zffi+0qqgsXA9PZTH2RwdC+LofeuboPsUEQks5gH0SpcMO8vMKy3X4TwFGqAwJ +/P08j/NQ3V/6NH/K2OPqHb4UIWT5SuwzNgp0moMGPo7NaIPUXB2YYugHQeg5J3YZ41rI3+EMFYTXhZ48WK+JFY+L648GbWWN+n4J6sblnUk/Y3BWP1/or3NVwvGtDRpaVcJcTZuCsGIovzGUJeOpujRmFEqVHUnsTRCEyPhJ9jY6fPj16MVMwHSYMHq4tgBtWySlYT2STtRxbg7aJix8yg== +DFIcnybg9Q4e8lvNk2V2upGcuZPvzrdvtUWnQY2SrrrB76YW+dcib4zuKwU0cgd7PijQCOSs1xBFA63r8yKE+BZMpIJBPcQB0nAeBcdL8AnXfEZOX/dPIJkH+2T669dro6gre3MO8GntEgTBUobb04CGQo6Xbdhdjm2W2WlfXBBivBepOgNVICEyrBEWxJ6F3qViMPn/yj3zpF1putTvCtxD3OSaQQKlTQjwwDk87N5pAANpnAgmsTpnsGc5gwEWKsGT8CEY2q30YHoO9WZdvQ== +fBUMMACbLfIUiv22DBztC9D4tN7oztrdPKUY/nUgEKs8lLVTmCNXtzImPf+SyMdcxAqdceLgLMswKsNRoTwjEOVxK0+6kH3NoMKL8JR3Axw= +KpJ00HDVp573NWedwG3qjM4nIxeXBH1Xcj8oXm1JJju9zofOnBQAbjDel5Vf4V5+CCtWZ4rZ7L7oaIW7uZznMm5bEdo4WIfLcntIP0I0Rj6y+ldQhj+n3LowsUT4JgF2Re8UK7HV5d7IVq6F3Pok4g== +NYEZAf0CPJOvudyjBgyMzE2jwr20AEiuxvmw/forLgZERQ9CF6rwwXghLg/05yOWH7DsxQtiRV0jbRcghSMcamekmL3rjNcnlxlzHIz03L9Afg2mZCw2wpE1ng2uSMu2 ++wBlhY7T5u3USVjT9x1efC8TgTZeldB/Gm8A6jOghoftvOsZOwZkZ2waSc2ajCEu8/W+3Ne1P33gQgamZtXq0KRjdAlySMJ1LwafiTxCX/wcCGguMUTgIMT8pgJWKlP3 +vX3aw/VpCn2kdUdQ401ThnzVz9KSbiqiT6L++1PUtLCWNoHXu3hXQN1cFaNNoZLtxxPINpim31IYDqEJoBDph8kNEHythJKRmMo5Z0+V4K7hgquEbbLh6QkAZoWpM+de +Rwr5WRRWJc49eH5/ngaIBiWYeyNcMjokcXjDWWMdBpR9adsWuzQNUXNAKZOXfKil/IcpYdPvTbxEDZ3s0NmGyw1GIqb/xlAka2sQwmeAeAXxauBb3FooztVgs2YWUorEk7ZkyIFFLL9lDDIkexMk9Zip8Ezq0UfS4SDL/2+UqILPJNyweBJ2lb418UIVi8ei +ZUOeDkg+GlcyKcw8Uhn9Gkzo/f8t5Thp7tyqlTwTNGzDRXaXsNYqq58i15gSj/SXZmHhMPM8acC24U2Iz388I1TabZfzR3emEMt8tor2R5qDMGDRDt3uq51gLvSbi/o7pySbUdqo94mg6zOkQZZ4qQ== +ogK1joGXApEMwUMZDKuBPQ+K7io6hyKKF6eB/S2JjwxAs9ZmLoyXyu20uQSyV7LVv0TLo45EVujcWKH0qwbMzz7uVhKFhOvhBxY3r6NN3AQZeC4mZJpMD7Ita9zCqRrE2xRqPICGsrqOQCpgPf2mYLasUXusCU7X3XaczNkzeXgEPQEfDIGDzjdgoBDve0lD +kHKnlmRHLSoRZ3FlW7VxUDBfKAp9xAMQoasPRHDCJMzjQCpKGX5xlWdj28e19mbOuK412zaSn4fr8EiZ6CQkVPJV0JhCC6oXJ1VP5dzwVK6sJP2xnnrc1maP+2bSVCSJJW6XTQBnXN67lZ9csTc/55kThXgt8/WgNWfGnv7cNft7qMGh/uqDpiTxkTvtWFyfBLHpDodAuaAm3HZF81Kb9g== +Rwr5WRRWJc49eH5/ngaIBuv5m2Ve4B1fcLet+/1wo7DH0d23Qhw6EO8INtX4jGWqGtX5+lRj8NTlwImbeCDIR7whFKL4fiSpDcIWEbjfEvA3nU/uai0b+GjyPufitj7z +UymC8Fo8j5Vfm3c3mvSqVFlgOxkgngkb4zutUybie1eYRMKNM2R0Apu4INJTA+KsZzhEsS0Dg47oP2HXN636LPwOVEk3KNf6rD5qgihriEUcABpi+i4p6MgzO0aadCLO +Cqu/R4uxTK+Rjxynhwj8CfHKWpq32DKWeMBQre76RnzbGVSYpcv/9UNs/yZShnWykkMGuk7OIuRPKCi9fXAn91OtupWnmBE/tPrM6UCmBhOzozY6zSf4z7sHfK15NfQsadC4mwWFokseEn0i8rJBXv5XXYpBjiKMnnLxRTbzuKs= +GLJti+lpRHd7Oc2b5e+nlFAlPs/zP7tUhNYN+4uCpfDwFAjlXxr7u15rd1sMBPzQd4cew+cfFWW0cLAdoMsQO8scE0139HOrzP+lc1LQt+GdbK/albCj1Cwzxm6RjnEE +yoNfTnPxkT2ArtuDpRb26am0DA2k2Xon9SZObD9Px9u4mwT57Nln0TTGVdqJQcq+0cTeXnb3giLG+gqXnBuVYdQUPl01Vu+xBAOrY2dmrWA2ETuL8gZ9oDkUBfRCWk08pjDdAnBxSYBi/tFWtStR/Q== +PEQnpcHCCkNy3Q7rsdi/TK3Xxav7oi9sGAog4t8hQeSbJg1i8dcEVVLrHD/2FpX+Y3Y8NVyvTEw3MFgOjXg5Z3inmwUbEV9YzGRFNZiNldg= +XCHGYowgcWiXCPE3uFJ0ZSKOiHX8d0J3vy29XAMA1SeblILvIlSLkA1MisVOiIdR8trHQ7lcjcjeEYK312AhuA== +ijJqnkItaijg8Xxs/K8JAtEg/1CRLWdefN4APtezsqDTzlCt54L8MhtthOhfZBPCMGcLXf4L0yj4bf2nNSwz1U1/71jG7FqtWYPcN7DRP70= +U31Q5qsC0ZXwyq8Zvtv3463ts4GVBg3RkiE4S/DzaA6Kp8GeQBp7km2wFZXuGu4Qc1rF6tnaxDAWpyk6yZ9Z1V66e0iOMfXFOGVg9Z8gVVw= +Uma9eTOXwECuwd3yDyjkcVNVDqfGb+Xyc0AfsH/Lxdjqxri6EcuUN5gbD8qkevZ49ORG2Fny0UYggt129TzU3qVjI/LW9sdY3Tv95jg0aPg= +4mJjQ2Zr7TaCMSHEDH1cmNC9B6otCDNHWFtyq1OIjer6HF3uv5P3Ah3xQAtzeb0757eFH5uqqJ7+WetJ8IfyzFGIHdCnNSuAABkwXyZDQuc= +i2sw6+ZuogTNbw/oNKxMgeHb+1h6dsKyBoq229eIP+ootCEFRhDcvdvzh+nsZyWcKhEir+qfaM3KgbcBMkWYBfBY7N4WBJ7QD0HkqP4nK4I= +F2DWu3kKh2SE9esMcSzIGfJ+0XMuBwVtVj8iDPswkZPUxgGTPxq30Ixn/I28m1U3b9kMRMkkWS+JVyFDH5VnZiHOp4OhqAF14AOvwkU2gEw= +oDtnF93s1JkfTVQ9UzfoivqdVspjUgLvGtzFnOwO5TtqaeFScbiQcr9H8kA7Pghsvi5p67GLwNQUFk+jJObctRecC9oeWPUG1GQgK8oJXFA= +JPxDl53ivISpEUZ+1kQXQXMkwdTaHzfGE1gmcvUGrI1628Vukw8+Tr2vtKNIeQLhv6uPRQQcMjoLbM6J4+lctcZSACtSN4TMxHu8vTP8sq5cFtZfX6Hp1MA1/J/Ok0Tm +RrtdXHpDf8ftCQ4plQxw0HSkwvLurQVAdFqJ2vvEDrjKg9uE6YmWw40PaMMTfomE4srMPDhufk4eoHyPuBrvY4yoRW/aaI/Mic+SApmuiok= +c33sbT/GTUpquVFIngMO87dfZjzWcp4w+CxsZIom8hnCV4LQCVINvpXQKpQwscS46W+bs/X9FlMad7t7mrWHazRBqMMn1vYjX4+/MlIw5mp5jgZaKdUE6zxw3uBSIoS6Qfeod5AnUo1RuBAmOxzLYA== +D7Xe+CnrKuTWLm74s5CdULixGR1tSsjFREsLXI3w1fOCT9R1iAfD71fBMTYs8RQDd+578zqvHQbhLzp9wNKHV1QGVpIuu0XVQ3ZOfGrW331O57iqgWVKwNy0+TA8s4yn4uKBj/GHfqoW+P8hf2Y/iw== +t607yNsl+Xt4IUZUKCdg120PGcOsg1C5ytM/H738uXTHLSoefTvEFhchJxU6jCLjxj8wt8jNH4wUy8VleGCDcQa4Cqg3WtkSsjTogkIG8ss= +v1YvZLHufhLKFxIHRhqo121CaphAKXTzi+2OQ/Jt+2Ra8IbIbxhjYLnTbprhUVFVEdfSGSka+7HARRFgMLOuRNhwyux3UaQBPkoTNTFyYzs8tx7Lwn1ov8idcvuZqjoq +O0mtTdlcYfDttKJPtVyrjT7Vr7M+sSOE09EuesaodxvBQ0f1f82UIsSTbWj1oec7fJbrrisZPiNv85eApWnhpgY+dmjE1upeUmHSjs/TBhc= +Zs0AOqbSh/pxVla9Y6PkDHix7FEQa/lG9Xe9odgRvWBg/fLfX6CaHcjTCDy3Udru5iRaLK7p7tpRdesj8fy8uWL5VP9BHPjb2yo1d8NGp1w= +eP9y1UpA8RKgKDUIdVctvyfy3tDu4z1rJ5eEoNhYGLPUnbbTOnOP52gaVwcexczf78FiWjR/hUM/wR1ShNcBCLbP2anqHbSSEhBnTsDwBE4+v/swe5ginLtQblP8dcOiOJLgNPcLB7OsAiKkh0up1Xi4SR0Z+6UcqNd1oEffI2c= +eP9y1UpA8RKgKDUIdVctv5D2EaOJzqmMQgPuQUW0viADntFpL+OR+jyI4sc/yfJaa8WDEM9qhqyjn9UWgm3wq/x5eOVXdd4Zi0GnGHErWUxLRFycodvusOgBeFy1Rf6XkcY+hGJzZyc4wlLPAC30ZIzK2tsYRCgZw6AEQaZJ4OI= +V9s5Eo3Xcr3HGbb2kxKWGBy3TVUwgHQ0HlRWypwzK0rSJKWkUaHeJV60likc2x6BCRpPWJ866FM3o7ViuvFhsqjMOkmOE/VZUAuEZQ9ARsU= +ECQx4hhql9YPrq7JsTDQaKPFGEO85KpRVw7iAwFx+sHmIx2FBJIY08G4dmyeVsdrGj7nqklvYYxhdd8i3Q/8v/EFGpXLSkcBcocB9tuVIq0= +yUEvl2lmwCONcgsqq7zSb+bsBrBDVDADE4qAggOZKqRYnwiwh+tCkoX3N8tQQj94YtF9XFrFiR9XpmeMmcUpk3QRYUNp12NQuMrOacdrhR+hHHln1Vbai+2SZwxiSRg7 +u+YxRTYN8EJVeuURXtdcZmDUmLfeALtrsOJn6iGUQPr/JDY0TtUU3bhX3QGqIcnSPR6NQViOuU54/qL93hDwWxjZkkE2OTGd/2UbeTHyolfDsW4NEaMTKexhguTwoE0K +c2G+D5skO/+d+thjXmC1YUfVX+l7iWgX9T0c9PXin3gutxdDzXVeMcl6Xfle6alDePLg9NsXQLkUJC0M6bvsYEiWIwaq3GTGPdXDs7FE4G6jCNJxO1pLDWSmDyKgRSOS +o4QTqehA3AlJXkkcJZDUJiu8aX5WBwJ8DPaJ+OLwuZqAeqwI4DMcOEIwurUrckqZp8Koyjfn3RIHisWySq1U0fxdkjrKYgs2L5fMv8WV9mSvaPZnEqYOvHG6wsKTcKnQ +UyvkdV5qs24BbOkkeKQI33u6ArkTJBgz0Ss2onvlDERuqdFwTRUb4f3Q/WiESZUnLa4y03LnZMp/RftQIlV2m91QOExNYFrX0esZ7sDdO4o4i+eSqNEaLEvPr3qYBerNBCI2h9bIcHZWiANsdpfDdQ== +yL3V7JujJ3KF9f8XtukZsR1fBJIf7GJQ4YkaACLOGsVDiOxskKXNQYQxnTbWLPRsZrBuS1Fe3DcgHTUuWLfLd4Fw559n67DfeG4HwABfM3FUmI1EYGMPp7sZWL7qtyCWJR9N1mltg/8WrBdfNAnH0B54iJ7F8X1ViYcXicK0pWimFELauqZhgLeOdpcPNRwe +Optb+ZZvcgzfoopw/H9ohY+Pa2BmRt02gEQCo9AqlcpFc7GOIZhbYQBOxIyaube5WJCVuEAsrx3/gYzb9ShcHJcgPpkHCMuXnQHO1UVitunUWrMvslPVdSSUFmpC5DfP4X3WEmStNbYEXvjzYDHGLg== +1Gt/p2zFJnwB8mRUNG4E22OhcfaaQ89D89ZyKhAoJpPCFyqgu7edsDMIM1I2ucM19GOj4qe3GWawcR2f+t4Gfr65NyakK2HZYUs45NEU7Cs1wZjDferOyImlO/o257lZ +OTCrWmGpA4EdwTS+E16lD4VaAh7pDL09qwQdgri1dA3vQiC4xwH64veDLn1yvmMrl3yCRLP3RKYK4cmqbiNVYnshkRpd2zWGN1jp1vuDAIqJkJg/C4CLFI3XuEWKg+kg +VmUH3ezP1MwgHNKPAXaOFx0SCA2BhsoQPfuhCiEf9E12HPTU7Yj+DFZsX+NiywvPSXS+BcXWFmbMx+0GgNkdhnE+SLwvjVOBS0BKyT8PmjSEOzr6RajHGqnfKYHapBkOXCFmeWY5LSkmw5OLk1Tjzw== +7Cumgo3JknMVMx9d4IyXNkWblnKDeDeoxr61OsoOCUNKX+2st58f1PBkaCQ9Ia1jr5GDlka3sJiXlAFFGh9lFCdWTvGsamc67ufjc9RWDozPNbCPRW+/lI3g1nDP5hFX8SwW1CZKxWtn0YA7Oc9axQ== +XjBVJ2fDvhqj13zHSPhNq4a/VLCQNkDASpY02Xga/Ae3sqYAzA2RkYvcT0raXxa92r9qkRiMel4/QItFFGOOMnjeDSrcpJ8Q0n5tBEEiY4R5zixk2HZVh1msPMqP+9mXDLzDJNOp9CQlFUEkE5iBA12Z1Wv7CBU5FPs4OYFqeUs= +mb5ArhJqY8U7Nhj3n51LRgVpmRFD4NHWHZSW6JTOzw/7u4vN0xNGZ5J8d33/n4wpH8JeKbYZru9COcNPB1RTqNxypEzIetud0GaIspNozv1ypxW7aW0tK7Ag19G7wLSClxcTmV49pIapmSepFbCvPhS+KVgNY/2c1L9UrlmzyI0= +eIAbDBBMnbQNURiQ/SQSj0Yr6NsZ1YoYvEDZf2EiSRgF3TWp1Um5nhuZqvLgw4qhIJWe5FN/haGPgtqHNVGSbH/TsVHYtELdz+atH0h/Z1tt+jucbNVYhhmbWshgfFdaDXu1mHOdIShKRvFpFxldew== +WMryAG7s9FKjtjrG4x+ATNmu2wszZmtzysm6i3wgpZadOLO9/ZUSF6y6dLDxOuPnAVpKa6yN4xld2dDyKKgTmqoBJhqRirzqJbKQ+0gSuRTjfleFb4FsAJ6zCoJk3PeRvXAXbEOUrSCmz2KkGYUYRQ== +WMryAG7s9FKjtjrG4x+ATOQjM6HjHtlGa8M/Jtg/uZLKDhwMkQV0s9GUOWRJ1VWbYgF4Vr6cH8SXZJNnrKRk83C67UEKd1nA5OVrKD6ocgiWbmRdmzqpVefTgxsAfxMmBy8l2vVMAhYCOSCS7vRaCQ== +WMryAG7s9FKjtjrG4x+ATNrh7dTugRp+04uBk/GSVw5LASCW74N6Ga3pvnS/VXYDQsivFtV+i1iXncagRAYIvE5SuELzodmnMfYMx1kr7zk/VwVwQaxPVYYIB6xoP6bAA4pZv9tFlx44bFfZ909CoA== +WMryAG7s9FKjtjrG4x+ATBQQi98Oq52QOwHM1kpjILnLhFORzJ68hVlRhyTTS2dRrq+0ZNWIwi+JyFTeVO8jQ9lQVWJZZYW8qCZDuCe/Yt2oEln9gcCzKr+H/APNHS3P7buJdlKeDCPPxUGiq4u2jw== +WMryAG7s9FKjtjrG4x+ATHHxs27PI3yMHf+eKI5O6HCRJmN9NoPXQGGhg8es7ScHiOCC0XWzwWPhMklgPTG4GvnWwaTcKlVCZupsc6iVC5eqBfwJ24u6uwjs9i3Df8PU0ziVf0deWwiov3Ek3fmc6A== +WMryAG7s9FKjtjrG4x+ATC+CQvkTOYaW0xCvOF9tMTvIMNmF5CsiGspToLXf+IeN+nYOwz7p0lRnDzrwC2wP1zTmeLHQaHCR8qqKluHY/KcAskVdJx0nFTfbDZMUmTASMwwHdZH/fUO/ZaHzwiqrnA== +WMryAG7s9FKjtjrG4x+ATDHYw/qAF9L/WYO4Wh8/K/gzN0ezfEYg0FxSf87Z+bMoTc8J7Gvt0onDf+pP4CuWzo8G8DDB+ZLbmQFMCQv4Gut7bWkGzoXJQXfaohg17ya3ZofORP0LtpSMlo/BxzU7jg== +WMryAG7s9FKjtjrG4x+ATBVZ1OkNxU4zBg7teiYs5c/CaFnQC3a1I6m7faKgS8ZVA4yLRjDyYsAZpa7245PyergBBVwFFH6HmKuKQ7A1gGo/6DwUzhXPA8TxhxC1q88qpBAyzBBYSdYrgGfvuTqsXw== +WMryAG7s9FKjtjrG4x+ATM/EbaHuaIT2sybliDIfD7j6r35UTo7NYFZRlrVvQiHw1cQgcLXGhBK7zIArm1uZg3WRIVGiKaEsIyEwcMJeJq/RJRQV3VcpMObXSD5dZo3SvboG3XVZThQdLGhNFP/G0Q== +WMryAG7s9FKjtjrG4x+ATAdl4avdl+hip0KZ+75VAnrQV/Pk0SyMPFwSHkA5XpNjDbTgiwCWiqaKg83iNhOHBYJp2TE8YRnHnTbnVSy8t9gIA4dNzjFn4JXuClxUWXXkc/A0XSBNs/xDGhpWF+ndUQ== +WMryAG7s9FKjtjrG4x+ATKR++QNc0SJWmG2pcTkqP+Yym7W6h6lBj2/RCffuI3zv9foH1AtBZPly2+kYRUbEDAww2N8GU6gx1qAAPikD8k0z7RsRAqWIa+KkVRGLFrVVwgnB/MeND+82Pe0wXeOJUg== +WMryAG7s9FKjtjrG4x+ATCLs9tk9Jltu2ibqzlObVwNB4Guq8ibqEdeKnK2TyySOAO/Ine8V3xPDbhbNVEyzUe8PPbAbg8gAnSNTwNvK89I2AVbo6HZPOy6Jcl09MwHYSCaxASYteUjDGkjMsWrrXw== +WMryAG7s9FKjtjrG4x+ATB2qq2ymrxcxLsPkD+m3XrSI2N2+ZRJ3eD+s9eQDVamg+7njOyPJrh65dDsZQCCUNjjFu7OyKGRhcT6kCI+FJZMYtkEGHHz9+PMPfogU1f2BrYyjUOxBNbF/TUHeyw8Aog== +WMryAG7s9FKjtjrG4x+ATOEy7/q9poeP/FqVlweawPPrKCAMVQZ5h06PAZVXWMSiQkzeYW3YpSPAOjb3GOxHTgch5CvH0Xekog2zoMhn1y0pPvzbZYWxJ7MaD6edE4EKmtEKiA7Bjj04vrSAQRkw3g== +WMryAG7s9FKjtjrG4x+ATGhR+unI2h74qdTLP7c+xNjUIK5he6zXxFVALfstE1v2JJzL6k53U2c6Q80wk4pLrYKXWZ1LOF2CkF2zKg23Jhr3uuSLfIIjCKHSP1RQpfxP9XUoIAcx/o0T8ZbwM7BWlw== +WMryAG7s9FKjtjrG4x+ATDNt289XB4z5X6cf+rpuIYu/y1ZRboRfEAM8dlSkdMYEQi0k2LPMRZG49i+OikNXME9tAy3ZYFz/S6oMQGFxcdckiJT0s5pvDNiH/XmOKGU06+Es4PF80hhgFFBTw7qfQw== +WMryAG7s9FKjtjrG4x+ATMgv+MtjO7FtemUMEZb9FojV1JkUKbj9Krklg5Df0I/59ocr61fClFZvuselgezf35+GsHIg0XjP/D4Mn2m1v4TEgdWHtzjMoTCzy7uibFfDOFJZwoN4aDjnvjFQ1HS9BuRz9rAAbV4Us6lVHO1NCYQ= +cW6VfR5aZO4LK69BZsjV9crSLG9PIz+TGmuuhzZzbJsZziHU7ZnuAY6OK306VBJIfHpKpYF5XUgk6wzhq8A9yktWjNRT8Zwbl4ZCAtKQsASJ8ZeX2XJAc+MG5CLEMF46nleScxctrO6OMnDmQ4FKdQ== +qXz/P6tVzdUn0oeNwT5P5GUTOj15ONhltrAZDg3uz8+cz7lLpSBr4qzzaARhAhY8v/EQncx5cMJOfxhR2mUP+HPH38SryLW9z4LaaSx+R4s1oiOCUdTwDFOrqVxFSDvYM9X0PPXKvaAqxW+kvxtNoQ== +u9mvWPj2GQyeh4Qfg/3Jk0F+enxwpkcd4cVgUbKEjKPDyTaFz8NJ0DuyeWx8oGlVIXJdnlkiZWOv77XDEtAX0V2FHBnVWBnPEGmRbUQCTqEco3UPCD3t37w2dEgvYm2ZOKTlRmp46AQKp74G7mJ87g== +u9mvWPj2GQyeh4Qfg/3Jk4Kkm8Da//W0pSXkBnB3Z0ZucXeNfRaqEdskySzi8GxJe5iBfUt2r2xaJMX12K513CiekJa5Lqgyv7hpn80olxEtaElTf+pBKVS06dStqXtIdUv8sggqznqUh+SfMuIorNxfv8+vZZt/F6OhKfAM9uo= +u9mvWPj2GQyeh4Qfg/3Jk2Y7CLucDXrFQMDP/vNqUcFBBoU/SJ1Q6vFBo/3rdeq9SSz627vkZ2UQDP3TYS/hn32SmFoWHbs1ypYucLNWZeqEgrSdzjn+tExUhBbfKvLlhHruu7OPU8Y2ySWAqhUDP4SIiqRZbMaYRztquwx98F8= +u9mvWPj2GQyeh4Qfg/3Jk6ZzJZSqscDZOHsDwfXZl2numAbrtOPyz8uWbrx8vgIfflEMgWNlUL/XcUXMLvZBiTmcC4ihstUjUhoGvxKSxxYnTtRbCI+pN7SLgzFFGW84g4LuGQiqOqy/ds+Q7NxncZKnHsTyBV2TXa0gyzvNrYY= +u9mvWPj2GQyeh4Qfg/3Jk85Wg6xc4QdVkVFkKn7phwRwSBemcn7v/zYOEFY7ZR0rZrrhKYyCCjyPBxlMd52CxwixdtfGrltan6Q5N5JSTBGZVfYA89Hizmj9ixHpw6E1rEFmJQcujwZQF1qxZDtGN7AAzGh+UtVO6T9EaarsjI0= +u9mvWPj2GQyeh4Qfg/3Jk+XSamLEKdP1wc/owroCbvOt43VRnBmyr6pXnX1Wp4ugN8yY3TrOkzTqGUL1lMVEtc14uu5iIyHEwoL5fVQTA10uSoKAtFx4tXYUO9TGWbktWRWf9u+FibbSIiUxh4r4zQ== +u9mvWPj2GQyeh4Qfg/3Jk4p4JccEzwE701o1Hvtv8RLgf7s63UDhY3CdacNFIx+ma8rSIZd8+HOcxHvTi5JZh0RR94JdU2o9vHg2+Anw0noFsoo0B2TLPnzk/U58/tI7tL5X2cCSsrkiif7HYk+FDA== +u9mvWPj2GQyeh4Qfg/3JkyunHx5F8pN12FI8w8bGHc8PT0RJ1ik9rOvWz0nxCgMB95VK5Polaw18USNfn0UHraSRdJnNHBkkF5SbN2X9q+afOKw15+MFNFUel/uuowT8EtX09y1P8gBRS+vTMRblU3v4j7QT7RX7r8bqNa/+GnQ= +u9mvWPj2GQyeh4Qfg/3JkwFHURXL0F3lVUG4MYO4Z+CFpWMKpr9K6TnuTtqKlGcqNGGAtLqIlfLSUN0H9pE7u+Vs+zVGKT5y73ZBqYL2xRGhpyMxSg0E3VGxeHfL735hFitq9ZfsxG/GW5gwpLIycW2EehjIhuyMHCi1GQBZEYg= +u9mvWPj2GQyeh4Qfg/3Jk+/CDh9J0IZnC8hReRXw+Umz/8JuHSL0Q/BS68JLWdBapxPtKCsxS+pwpxh6YaW73Hys2MDHBM56ftFysTHtG+ueEqmyXYhqtgY3NBRrvV847Ep0rcZsXyRrxoqPOrsbwQZTOCpOoDuzwVx7DsCsvk8= +u9mvWPj2GQyeh4Qfg/3Jk7tH0NbWKnoRUzCSYmczsVc+1NMPu4Mp1UOV4QV9L9HJCQlvpe/JCSgaF9feNutG8SNjToJO79MFOIcr48Lgj9Ay9+tKedmp+Z+Uxq2cP4J0/Imu/PLctuO6THYMj/s8Nw== +u9mvWPj2GQyeh4Qfg/3Jk+l0QL47Qrb3z56wQfMVF6nZ4uT4S4ss9Bps56roEWQIR7rGMvhrpwlc60RJTjBbsmBFIDCVkhBzKJIZ64xjL7W/7WZNLXY4MdBoZZwp4LmigdpGnRJ9+KL7zizsydYEUNtkdsrtNuhlSRYYJFHxBz4= +u9mvWPj2GQyeh4Qfg/3Jk/4Xxo+6RU8qO11KZpuw8Bhw+58+RO/ozcnjeTnqhhAtpNc1cGEYiPseIBFUR1Nxil28Ixn0ELPDop0Y2KWOoVv/6gt4XTlPt/W1JZqZRA/vx8++Jcd6gdbe5vW5Oz8YU4dFv8CTdpX4bGyks6eB/pY= +k8mQATL2QGzdHVb/ZqU0sdhr/fHf9LEKT2RVauO7PV5k3kgwhTztgLNbkH+BmV4/Pk6g+EEKg987GEI4YDWznGUpHNPklJ5S4tL5CUpGGm07t2PS1TdV7LnXXLKs8IhmBDwb4J7iBFAadgcdI5mT6A== +Optb+ZZvcgzfoopw/H9ohV7P1ww7HNP/i6UvRFAyH4f1dGmnb4FXU9hOqrHZ2iWHVN0Vrk7rU3JK9Wt0VyVrf7HJJff9eRLYW8+yUQBzKhPANw8Wod2rxu8BgxCDiU9MeWTyk9ygFVXd1535Vk8f3g== +O0H12djxOPpYv3DI+nktXcfY8mj1r9TI5/bRMWfRk/UNsmywSrOU4V/gP4TFJYSVwwK6YHTB221bjhhmUyw0Q5oEDpemVFwY0Rh2E9m5GP/CJMw5yvKxbhBMTjbjx6KGMU1JjHwTVGyleGYqdzS2tQ== +L2qllRaufT5w7PPs814S6rwJrryp795HfIlSSWRZKcdHXSHyiLRqzXNxgEe7GX2Xjm7wdcKYfEI3CK97H+Sra2yYG7DkOyngECk6zNjiZpDdnAfoZ7/+fQw6pQw2ehfnXM9RtWtoXOH7ApaB5UErfQ== +GCa+LjTuIcQFdoFSHacKrr0BJRQ64ySoR/C/RpwFxIEaesCSIkPH3WLrkTMW8Eb9pq2iZ0SJ6bourSD1TtgY1DQKJTYq6/APe8XPoClmHkEt/UWlPbAMJUfcPP6UHlMlTE49dUn2DxKTK6zlAP8OLQ== +tp5g+osoOV/pmK+tBcXKV22d9HxcLMH+xTwms4BYy9dPBjrzfxOROpQlyteKmKpmFRUppTYemg73OD86JGUMTE4wMlQ29zo95Znb8JExYm7n0ZoEj1LmiY4jrSVYBndY1G0FuGYM2MS4sU9+AnS2EA== +tp5g+osoOV/pmK+tBcXKV8uUTAiPocxsNwpKfFrj78UoZext6dgqhXvLVuzQPgAHPnQrtRgunxQqXxaxV8NLmir+TM6FrDSKCl5e9m+VrCWQU68bjLgcX421o2gyTnPEllHMrG8UKEcrD5KlRX0+ow== +tp5g+osoOV/pmK+tBcXKV6neYTsy2CBToPppeuR+1qhIFV5ufM3BpqAdx9NcFGzk0tMll/JW77Kha5GiDhtwEy9i1DqVy69M5tGkQexGCFbPFupVmqqjur1qFTbudWD6Gj8SQx7IUKlsKtBsYl3Ecw== +tp5g+osoOV/pmK+tBcXKVyHhjlHhc+8OyciAEIPtMKdJ7km+qxLwPmF6uR/Il0KsOmvIw1AoPgUwPRfbhbRJuyp9zT144++FKrH5GhqFDifI53iHtun//ddkHe/XPCkhKPzRK4XiwWEiSTBo/O9fwA== +tp5g+osoOV/pmK+tBcXKVyncfbXigmOSKsGNbE35zxQBzcHPGmO7xa+kVKyGcGw/nhOnl9JFy3J990rE2dKhI7+7XZJm1qXa5hwMD/B5/hkz+UX8th/FK8rSKs60MzN6CRpd0S0Hn09u1oByLcNq+g== +tp5g+osoOV/pmK+tBcXKV+0kNyfVosEvu1R5eIdu4mqJK/C2Xiw3YPLIY9haorUwimJsC8qf7GwnP7/DHxkQ2lI/nEYqLq6DgTX2Q/RqiC7xdf1GnikUfqpyUgIOxtXD+VTiU+m72vsm/s2IYZw+xw== +tp5g+osoOV/pmK+tBcXKV7FjVUrcsdJ3A3YM+WnARMI6e/9yYrUwUm6z6teb4DrgH0C92Pw53k2cvXbizYZQIfKvQBC81hrDi5ccUcRtDX1LwpMM/AFM3k6uaPJtqxFX1wPn0/iN9aAnuWYtNmOKTw== +tp5g+osoOV/pmK+tBcXKV23o/6FCCKq253U3hcS90I0BKd7a+rj9vrM0erCmF5fgQ7zLdhTC66dGYxORDhOCqMIhyI1LiOSK0ZHLlQJODncmBSSJMs7dXXLYExcPtxigHv18uHuc+oBY+bDjt7yPOpBPbXJcxAYfmc4FlYcZ8hY= +tp5g+osoOV/pmK+tBcXKV5NkfIfu6lyMg9b8khIrQtYI6ae2yWEudVczO9o55hLgdwio62a0SsIZax5MvGJylvOVb7Nc4dbpebjU3WKZFfzgAoH9x7fLDIKfWb2Z0HDvRnf2X7ym2qSFdjFmOrgYu+VID5J9vsD4VpS0Qe+vsws= +tp5g+osoOV/pmK+tBcXKVzTZueI/mphDci1n7i4FvjEhHVFae1ypLGLSSkID9FE8Ao4KINj7K3pSaDsJGG7nun/o/67TR0+7U1SYfpjCUxmEt1wrRoq6Tirt/vPYpSjToZkzNLeq+IZPrRPaLa5OIepHlMWd+7xIJ5wr1UNNs/k= +tp5g+osoOV/pmK+tBcXKV/R1y1G15U2L/+mO+1q9xsmI6a3P2IWGD5pa0Carq/A/xwWpE2Md6t2Wv+rgc6EMwM9lR26uoTXBpPliXXqV76qlH0YV0vwRSueJUcbSuAPYLS8Am+AYoHoW3mCnXs656HIOH0Kum0/yjHIog8jdXj4= +tp5g+osoOV/pmK+tBcXKV1nrdSo4U5sImCiOzub/YfcLeDqpPXs4az+MWQ4dntNOBN/19PUoRylHR6T0T6sB3EtA75DvoY22xQvQQkXPOfCLAP5Mbp/zJcqEufJuAemYoOmRpMMYhwPMTByMXB2d8we1RyORED1IbXOLqOmAU+E= +tp5g+osoOV/pmK+tBcXKV2XIfCmcFG05EPSUV1i+BCzS5BPRJ6Esec+PamL+FyPsmGfClXKYu6XyF9pI/ZpSBfCwT7/XKlkUaqu33LTflkbevzZ0RKBlexQLCEvL+sL4cORr8od3FfAb5j5Ff+POj7CwkpttuJ/vhHuyxnNCwek= +iaAodRlbbDbdxCarNwkcQdoTVutsf6DDR9dYt9gT2Dt9YKBt9ktQmVkZryk55qSRBIQEKM2Zw4AProG46/+su20DlJ4Okv9gogPBQQ865pvc7aQ3BokSiJOuNGzMeDVW +phX6+xCiuQFZuok+RnQpZDtuXW6l0Z/fULj48AYMV8G65jxvIjADDr898dvf+QZUI9cVeQChYvXHZDFmVbJT7bZjk78T5FVOXnbjVL3lTWYUQd1JWLfy5Zvp+gy93rdh +6dJZtsb8CUPLf6XAX0fHyzOINiwDqD+a0wyfh6+clwJcKemZZWtrS24SqtRArpwsKByB5lq2v+zcUQh9chResg9HlDBoZeV4THSiKnI+jdw= +lpMeOmzCLPmsgqJkG7PE0uIgltFtjNk9joqoi/Sx5S7Z3Hx9I+2EOHepDdzOp0bXwM65kSbvZETscKym3dV9d+06XVZxbHvbKbxpli6UKzI= +lpMeOmzCLPmsgqJkG7PE0jWng/ZqcMX1cl5Z5Pm7O4/ZytOomuD8iSxW/o7XtMO5+i1+0HbeVVdrK3zTwVpP/JyiW+ZqL3FvwsktMy1ynd7a417nF56s5xndYxt4f9Yv +dDQ2U1xB29TjkFiRc3OY9/7W0ws1tGSg2m7m2Tqp9dg5UeSyYqzwRruFgovxcuV9aKft4FBBlInJ5drQZjAZAGcIPichSJAeo7NoE+61puY= +phX6+xCiuQFZuok+RnQpZBTHYsxYcds8EwQje3SixNq4j769utpwHMU+QxYxIPWMISACJ7key8eERNbKSxulVflk7w+0RYqm5PaHBvdgHnI= +6dJZtsb8CUPLf6XAX0fHyzOINiwDqD+a0wyfh6+clwJcKemZZWtrS24SqtRArpwsKByB5lq2v+zcUQh9chResg9HlDBoZeV4THSiKnI+jdw= +glsVHMdFb/IeJsxHuz5fO9rID0zbKTCYiMUut/6OYeDxaLt+nJl6yiZbzgy1GJ2W3MrTwFTfwF4MkwrPvP5EbtBwqbSZRh/yDZePmhERZag= +LllQdNg6CwTfXdYMyq0nDg4z5E0nl6cQ+JwOyb8yTWm/nkPMbxdYEXHyeqyqde00+c58w6eOh1dK5d8qz+QcUQ== +u9mvWPj2GQyeh4Qfg/3Jk2ASxmE3NWLfHbdsvZfzKIqbD9pcZOaSu8iu3+2su1dg/0qIvoQUPMDaWJ8UD801pfdPGOyh2Q8BAi8Rh4XZ3H6wElXcHWlU+mGMJ4V8Sp8T +u9mvWPj2GQyeh4Qfg/3Jk743lvh+RBZaS40NPQX0FlF4yKVTVZ+6x94ikzwHvd0gZXRB3p7eRD9lp7TGholSJMjz7hMFEEe/HbqLcRrRhq2l3kZgXCsxBUoLw5d5Go5L+5HBJqtKmTMS216uhbkZxw== +u9mvWPj2GQyeh4Qfg/3Jk5Bfw3Qg1JNky6Qzkq0rIuCg3Iyvai0TbwAmlCFeQxBEFUXT1LEkCCObL+IrndH+gRpzmUbK0B1+EQlDa9x7R1KOP0xEJT+eHg+lUpEbycTKIPaGT8owbEHpZWJegtvgnA== +u9mvWPj2GQyeh4Qfg/3Jk0o90/9mCvvXcHLwVxWpPy2TG4JAkDeHQ5tODo9lC/OrQTj2jD6Jj/TH5u8dwrbQWOntAXUqKDvKaZ/oD1yWRM2Jv8VgPwbGbABPMC9XRyKKmr3Pu4HV55Y0yc/ceXP2Ag== +u9mvWPj2GQyeh4Qfg/3Jk0n0O4lJoUim6Eqj3aXcB9+VClCq0cYi3SXapmP+GulURUxxmjT0HWW10/QL2IzHQprUFH17R13Ncnk04N7evinySbTyxkgvNH7TGXXg26EBUSQyNHCaYyvJGbThNuUMqg== +u9mvWPj2GQyeh4Qfg/3JkyunHx5F8pN12FI8w8bGHc8PT0RJ1ik9rOvWz0nxCgMB95VK5Polaw18USNfn0UHraSRdJnNHBkkF5SbN2X9q+afOKw15+MFNFUel/uuowT8EtX09y1P8gBRS+vTMRblU3v4j7QT7RX7r8bqNa/+GnQ= +nrGCUtZXO8lhMUzpsw7flZ91P8LuwTyQc7ctIpjtlcUqrrpkghMmRmtgIz0nrzEEfUfHH35Nsi0wHH9irMek/0ac4bUEQRanjgBZNDBP3rYTH8w7Ajd/zLhkn5Lrf3fEix0w9W0HBV/gva/a5Ouvwd0HYrV03O+i8wuB2Jd9v0Y= +OlA3Ms276mZTcqwKAwXvu5EFP6wUp6Tc8bHeRGfU0rVqmQmrCtwmip5nduVo4ES3udiIu0L7M/8b9MUtJmlbHDqsOzv5vyY0QIXUaQUTLZU= +8p4c/a5VsUSqHSeB2usqZycD37psYBNkNnPj1fiVTuiZUqK/y6FMbycxnb+zen50lRoX/YhV6BJMwFCovbBDPg7DKOmmpDGP9t0ikPTNI/auAuJJoqD86QvppAsnl4iY +KE24wbSLDn1spIcZwLIuO+B0IizzYj4xKt5JChRY72ns+cwTPeG6J6a3IwwqXFceMo+CrRvcoX8bXFoznahLhw== +8lApj2B7Plee0WvmT7TeQp+gJVvFAsIP/JC2C8RasjmndPKNXehKIrKd8oQFJw0AICRsi1vVOziDdgnInw7ZbLHXjmgxEaSanOmRWKVaW+k= +2Poxxdy9o8AvAQ8T2zP50IWtw/1ESBKlEtSbdOmYYsS6yr8YAomiJQwVP+k7Expq6Lu80jKOSPyRvX4XH91qbNTajCBICm9G6EG1rU4tidv1Ji5nLHskR1kxeeyV5JIc +6I/H1Zpit1TSo4efV6bxh+qs8BV2y9sBv9FEAqnH3/ZqxYg+NDJCpT8u2pIoCmXytfZhPXXeD48qT3QbAU0Q4SCBhinazuvJgDCcBAZiIOQEyeYbiY/bJzKU9DoAVQqI8Cdze4vBmc+XF+dN58qLLg== +ztzuq6nAH2ogL/nP4kxvNxCxLpdYh57+FTjjNfzNPkBbBxri/pKFEgGvP6KCQ5Vs4QwY2otmjZ4K4dauoTYR97j+ZR7tXAi9zwKJ35wbDbdQqiV+XNbMqJu8PXLXYQCH +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +wgR07xfoapmx6eEnFHXXYsrtutdMRxYffYBJpExAFLKvOOsdc0KV0E0yQAAQBZkqO2dMmc/KvncCFdasMp/nIQ== +NqhMbY8+EQI8zhTG6Zh2I9n9ZL/a0DhWUREDi0vuSmk/BBGIzgfE/zwz/+PpUEnm +ET0y6SUMPE2Ml4dS8viRTJixJpYvSUNaJEQ3wgnwmDR/Dl0Tv2W4MPgVxUJMaUl1 +/VraJuu2EmN1fJjV8ELbStoy+NvUd5C4U3l5SFxwBYqHlGghTAj+KvzQIxH+57GjdeVm46KwnUg95Vg08AS+OA== +2FZsmfvrIlaya7vy2+sJW88yP1VAikw28x/+NdM1QmN20rxHH2Otz0YftAcEhqUY +xqNkWRXtX9e6RRiV9vfsXA9iVWM3mvaHn9rgIJn1BG49SLTPQQBTTKok5G15ksiOdIjKd8r9S3mNXYXBl6TZmw== +Lzax8ZmAeSERlgYaqH59pNEOXo6zmxKJb0ycj+VARAM= +eV2S4UjiHsDr23JvNVNK3qnbFgIz/SjyIlu0VPUX/wyZB/X0ex78snhE4vu8fZjO +1u+XjG/2+GSQRv6EzCaWRQ== +flULj/LdXe4ShFGOST+HfgiEQqJL0kKKn6iUffraNcUfW9gxdwW9wmntTAcmF7KVOzRDVCHXtZHOS4D78Zob1Q== +Z8UsPk1Q7HtwjRd4g01ryw== +v4wZTNeqSt7hKjjs5rimNMLgdpIKpbxvujNwNW9pnkHVQ8lv2VVqQHVVc0GPFdfP +6/YdOldN7ILWufE3TpQIbCIbF/kDgCBlZUKNWM/prTU= +Ys4bozvJBgtB7q16qcvtBxfJUhKmEI4H3T9j7f29CdE7rNQ1+w5tPWDUNgUfBW/n +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +8eMNwOFVbk4AtLbMoBZ3WTJylC8gGuKXwsoa33uaVOilo+eTav4x4B/CBPvAjTgn +8eMNwOFVbk4AtLbMoBZ3WXGvCdkUE1lCv740F/AYpZTVXZWXz/0Lz1ev0SYqBf0gjG5iXFTmyoAlt7NDxvLnEA== +PV+X4fvCTN/6758fJ9HMv29tbB7O8rIYWMezR27hNavFPJ2Ycj6w0s2fyIPezxVu7xjb7HbbP069Lbz42fGYbg== +ajwXO+19f5mE2k+yWvJMuiZyUpxKU04yI0OuG4s8zNpqZTpRMGfgHQ5hHcdTx6rh8peLn6TF5/UaQ669kk5gaA== +jEPV0YXDCqWCap/VHplvwQ== +bjpI32qzKze0nfrRYYQXm47rkeYYZ9X+K8gA3uq13fQ= +VmVrGQo2zRokW/ZuO9bN67zLZiQVbo3RcQYl6c571ziqQyhgroqSx3gLauH91v+fFu+QAiRnaYq9E1VremwyiHQNgxowiLZvqJOjmX8oSehrQHybiy7vwg1hHHpsU74QN+VQ7upOre9cAdJI0sm7fCv6o+OcHXiE0M+D9Q/OO1C68jXt+dgqcW/XIVM+Oq4SNMnJ6bQVlZuo4m2jVD2I7KYC889jP/b9Muozz73cb6q6JEFuRmQO3t5Us55xVPsD6anKKmnION+bvukAj0IUOfxzKWFoRGzpBr3dOo7uPWTKMSe6PYqHDOo8SsaXIjK/moVXYmrU9qAsAIWUrXUUx6MqK5J2jzuOxNwAtvQjw0irPNvO74o2wLVALenBCx6Gx1zrJQNND8iW8Z3EF3A17kjBoqVWWP8CBft4xk5G5Pa9fh1PgjoTxIEtiPflF81bxIkXX92YUpPvO+NJ+BSoTTwn2UWrfgxFNkX395bF+Xtu8gGJhb8PcG368JiIVlPa +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN65d6SIfZo75FloW0FIxk6EHEMEM5zu3rfeKGq68emUxdc1vtMX03Al6g4yk1YI4aHnCDd0S0QTT/RMg/s2I1Kvw= +iZaIsa48RXfE+uf/yF/rQHaH39lOhItuA9XlMsZGwHM= +PxQyEFNgEUrjep/ov+AWf+oM8xywLq7k8twOsAgmDnhwxbYPjvM1HR7ZmztlWsVP +iZaIsa48RXfE+uf/yF/rQFDamMuGYoHcHvig6IR7L5g= +iZaIsa48RXfE+uf/yF/rQBHzBiyCImLvDpqO+6hTyP3m9lHPXPQIIotfEF+lox/PQnwFku2M550Yae3EjKfdwSo2zaLVsBrXdy2HzGwErWc= +iZaIsa48RXfE+uf/yF/rQH2dIzSy3gkyzLeqVCbm3eTH9zmgrK/v9VFjQHkrschdvsu31TnW9AJ98XajAcXV/xJabWM9DvIsHVJ0epK6pbw= +vlw44yX3fCVt6eRmFa/S8+zcDVCNlbkZPwywv2/TkEdjzIX1a7+Qv5GdodWIWjhtmXWPTEcqpjmiAgwl6lc01A== +e1fY1YJpbz170V5bcvidMMj8zZEMpvaxNEwOhd5zodzg9VC9MnufwNdaYU25e3Ap0TjTYIXJp3aftiAH9jPWNw== +S1U+ozOPAZj3pbrAwiivwV+OvqAz1d1sZB0EPsOnolf+IN6OHYpFNOp6+2SjXYmV +iZaIsa48RXfE+uf/yF/rQDuGVEzsmF3CtJPekR383wxEc8OumBnbu+FZpS1sbexmU4IRN0hBGAisopPlzdzamXnnzur1Wji8UNl30wxAtMET/A4MwljXqYiRMkHmVxJs +iZaIsa48RXfE+uf/yF/rQBHEMyc95ROne+WTG39WCVaS+cXX9BOUlzXxE0w6ot+Oc3VNAQ+DhKgGIvQZHJ5c6g== +iZaIsa48RXfE+uf/yF/rQO/ErLeTILJ36v76UKS54Jw2UX3OGfKAG7yS6qfIxugHyp2S/OL5tJKEWjUVuW4IUuzQc20IvoIJtKzwy4FpzeobJXfdFFF6CmRLQ8jKXRiVYQeTwTiaZHk9oJGEGYazRQ== +iZaIsa48RXfE+uf/yF/rQFvnSgMMK7IUZnw+ay1bgOmW6qSuUZQTz2zXyezTI5+d7Fsrlel7B7kVccTcSm9Pfg== +VmVrGQo2zRokW/ZuO9bN698MHz+PRq+TRlhzLxtWGMygpukHf5d24j778t/zhHGidh9OMwub9c9vsi9i+zZN4Dri9QnMhFeT7rUsrCUJo5TwdvrO6yF6zrAu1/LNEjX/ +iZaIsa48RXfE+uf/yF/rQBogmq+jEWWC54y/H6MlNh4rp0+JMOgYT2mZY+TYp75FCm5W06pUVqWJPDAukRPl+ZRIueWws/l752pV//pYffU= +cdjMZGd+oj+E1aBgYuadXWtaC7aSClO5q/k5YLsaTcSQLEvmsyDwCAB+ds3GneYkbwwp8J4mMiR1G2+0Ax7KAw== +cdjMZGd+oj+E1aBgYuadXaukekpOTL5WA5BiDC9vCxPuawlZz6j7AAZlEbdZdLQ48OZE5ekQYbVCOAhL/BJpig== +2YIp4yADnjMGtY1lwBdIGuJOMZTFEHseAqpUotVOIPw0VLHXqqJAqDo1N2tbNar0q8n9HgTNNBagFvdjmshZDw== +iZaIsa48RXfE+uf/yF/rQANiUB6WFTzoHZ3OwjBwtM2/j5761QTS3AJCOB+qIm5mr/5/eVreUywu46shO608Fxk3JiSLg1QkYCinrxbBW8I= +iZaIsa48RXfE+uf/yF/rQKIqqcSbvyJ6nKs1nu85+fMRjwAg8Y6Nh993Kne9OE8bYXDQ+LuRcreEaUiZDN8aOyIbEKMTvERwYtJMKUES04k8EwtY/LwkeYK3mxxvwexvEDmRXbKbEGBLl52UPJVJfw== +iZaIsa48RXfE+uf/yF/rQJ2YbiBFucc3UQ/ZX8J50oEXgugXrduKe8SAiE/mA2k+T3N5ZO8BVpH8YAKvC4ZR0Q== +wlLHv6kT3Q/RmtMBN4nDAe8+imtRBuiJubS60XFrMJg= +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN6yl77/0MnObCK5XlSs9DWF8eqyYhe1FWHc8xaEa0H0urV7ygkURnv6BiALfKwfgNY7qBO4kuhmbf8nJ1oKsf+gyy3EG6rLIgfnG4sQZKYsDRT9AFAoDMtdIUBl7ovtqbuQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw5bx4JTsUXc9GQD7jUveTF+h1GxK2r9hBJbi4mkX/JP2 +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw3p30izicGP0/3QQynjVV8QfCMdvBT/iicXrGxILnxT7Flbhb0PBJRFV0JDAAWvxHQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw+sgN+NoknzWuDmsXkDIpaqQj2Lw0lrCOo1L/C3blJaAgKXMvFjXCS584QOcKTtHOfudZ11Mj38X+gn040NpusM= +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN6/VW4n84N+xmKyiHoJANXzc= +i/p1vQ7ugXGBvwbjECApz6tQ3fYSsRgetdpGSwwJdY0= +wlLHv6kT3Q/RmtMBN4nDAfKuWKTPnQzOUo8b39dYynKTIm9yduqnJKjcYCIOYNl2Ve+9X84vGlhWQ4dv1oTdSWR22oAktUUFZmgLi+55bm4= +VmVrGQo2zRokW/ZuO9bN6+zshBZO9d61HkhcnvN87E/esTkTggViecgSKvZl2/jilsnJj15aKiHc7zivB4GiRjN+hiLn89qackKeknKgOAkBsBvnq+NL8RWD/NHkUD0E +VmVrGQo2zRokW/ZuO9bN67EO2w8TNG1LHiP/tGFb6V2Gt2yQRtjzazeNr74m0atzaPdvLFX7XyVVRswklh1k9b4jQHovg+TYl7Ff9JiJYPcw7bsPs4ffMxTkD62b1LTR +VmVrGQo2zRokW/ZuO9bN6x1lQigDbibIoTlVRYg/sjBbtNt2NXe9ly6pYDchoxSFm8eCigCwEbYkrmTqSpR66Px+l9G0RWse6wD1iaMwlGH17ZZODY6LHJwXbnnoehfY3dgu5gHZ1QEdRO3HBSEC/BmkcWwlcqCj5HLhmnA7jVY= +VmVrGQo2zRokW/ZuO9bN65ZXaaTN9OdZLZFqE1+KS6RnzprQpjuF686uW9l1a1Xa7mf8nAsb5dvwf2nXb6MyyeGLjC6K1kWHmAxqB0N5Lhqbqh8cjSw9EakEYrt1pQmzU6bWNZ792Zty9yar13+Cvp95ctSpW2nDI1vJq91wABs= +VmVrGQo2zRokW/ZuO9bN67sdU0BGc9v1NKjQ+sFJyyAVqCbBH9iZ1H+7Xb4dU4gwDiJcnbDr43Qq212mGJoiC2DMD5ackhh4eB3W7YaQThFl4no6PKCk63hukqrWGYjX +VmVrGQo2zRokW/ZuO9bN6yl77/0MnObCK5XlSs9DWF8eqyYhe1FWHc8xaEa0H0urV7ygkURnv6BiALfKwfgNYyI+kia9//G28k57ZB6qNEQ= +VmVrGQo2zRokW/ZuO9bN68m4dRJxwc9vmoYO9XfNu8JoAL0ANoHBA6wRLssRJG9d +iZaIsa48RXfE+uf/yF/rQJ2YbiBFucc3UQ/ZX8J50oEXgugXrduKe8SAiE/mA2k+ZzCTJlHCGGr+o1zyX0J7dQ== +iZaIsa48RXfE+uf/yF/rQEW/HnjzbDWg5QiOMKFgUs166OxnL4KP5EwHDMXQoCQJRRha1BVSLbU7v0IGpgEo3Ow9rETT+gtPN+XqbTD9O4VSY9LLGQrxzpdHx4ypB9/+ +PMWbmPRKnSEEQmuoINHGWsR4WdhOdaCB8ldWDbzQF20= +wlLHv6kT3Q/RmtMBN4nDAVAwwjXMi7bsf2Urn5Fa7BXRsDUkBXTBYPEpaEfSPA3CViaW+eHPpTOr1sqaxfsUEJXr0r8RinHLoMwf+yDuYSs= +VmVrGQo2zRokW/ZuO9bN64Jqehn36bioUDDk5ORDx1UfKJPWwYobkwrB7mW4yC0E/yspyDpCiSv62gKc5GAi9T+y34+JwM/Ix71XqqYfqk6wuSlZIytENGwwf/qssogjxCLORgiVriYpcnIi5PgXEg== +VmVrGQo2zRokW/ZuO9bN69lOsOGGAluHtoQsF4JTTD91qLyShUeY2zrHp66doa7mrqRGy6XrHMPUUlJQ9jQMJN0k5EJO95P+i0RrsO6IfE9KsdjGbXe06Ke4++h4/3UVjycijEWFMnGV6uvGaxuhCw== +VmVrGQo2zRokW/ZuO9bN66Bx+QcaotFFc25usOdeBnbTRU45vVMOgan2bt8crvVKWdMPA2l5zQM2ZMjHmie4Ostb2ZlRvEpt7614r2AV4HLy37BZMk+x2Nk/Kbj+WVTFsFKkwVRGLBsFJ6yqXmiY71otfHLUQGgM8V8B/4spf6Y= +VmVrGQo2zRokW/ZuO9bN67sdU0BGc9v1NKjQ+sFJyyCMLjq+xWwbZ8c+ddHAwB7BKoukMsMxYs2sswO22RXml2KSsLww/KZ/8G2d/WiAx/rgyHeWDCW9UJufFHa7xinD +VmVrGQo2zRokW/ZuO9bN62cg3MFjF3Xh1e0EF0hJpnU2g0qslcYflC7df6LH5ycv96oJb3iOdLAMnhFiSSXOjfbj/n1Y70ip284H5cNfF+k= +VmVrGQo2zRokW/ZuO9bN66RJ3wohQFXs+QBlSfWZpaf0OWtwRJ6BrDTWIqv5NVGm +iZaIsa48RXfE+uf/yF/rQEW/HnjzbDWg5QiOMKFgUs166OxnL4KP5EwHDMXQoCQJDHEHPrnpFeN1JnNSnouOcA== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmUTEOYWDoCep0UQjwSxVF0w9eBLN2NONNvbpF4CcDGkQA== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +xWoGNWjKGPfI4gq8aHoTfBVO0rtzJGwPNNCAKcvoQ7YGnBVlRRzBkPuuYdnd1drISaN/5vpFnf0ao1Hkw6ztew== +1u+XjG/2+GSQRv6EzCaWRQ== +piplqnZD2qKjxqq5BxcYpBpGRwKsiHJxwLqnhEe2D48YtmIu0TblS7JCQluznId9 +uCeQtkgRBtN0HiqPTvhZHeALjPy6nS+/Py9QQjy3Ju9z9JZP7KSWkptweIa2bYE2 +cgFNMoW/KsgkKsovM6n6KO+C6KjDPWoUE/hR0PgyXA7qIltJH6LvNkqkyka0kjkn +v4Cu02hZRDsW+85CLRBC581P6PmAWmaUk3n76dbnkVw= +NmkgFF4ZsyEHu9KVT53La4WGMwIVFvT8dzZ/QJmAvrfSZPlY+eRgJ/IGmWYM3DiSmAIvy2CFU0T3w56mhC3RAurFjyBbAOsoZluPEh0M/sY= +YoCN/YJ3FGpKuVn0NXtXTv3XCu2B9Ep5OJG8o8tyakhU5BRvAk6zZXlPRk9etr/0bAiCaTT4Xnp28zI72QYmocOZkM9EMjn4wi9DBmAb0g6ipSHN3dmHucQQZtxoUbEu +YoCN/YJ3FGpKuVn0NXtXTsWIqvly5o9YazCVgAEKZW0PTnKV1DWqLu0lRYHDz/YcsBnA7SOwZuYWWRON2HScNg== +1u+XjG/2+GSQRv6EzCaWRQ== +PyAFsma1q0hLhwE/7yEwvlLDizBd59bTOK0yaWmk5wKN2EpZqyKHG2wdBEZW3kAlb+ZRqbiaA2kRmKB/ClkTYA== +DB6Dm0fkcUc76J51zO5BsTwaw9bnJ1hMDPyvzSAiR99N1sM5KJNB3yakje8btu8/QBHP8RxgWzfFi8SXTQxdwc2MFwLBmPC8Kwdv5Qni9ns= +C3JSV38/w2nvM3I7TZ5+4epkOMkaWYvoOErk6ygROHs= +UaGDE9swb13AVV1HxfNyqi1KJFeHDs773v7JZyf+ME4= +mbg4a7OqVLuFyb4lBv/8ihC29C2cveBqDqu2E6PhaRE= +meZhlBQhcKhD0kZGoC7z20E7608oJ67KyHB8wGHG8NC8h5uepI5cSAMadUxFvf4DOtyfgKSQxnHoEYtzKQUcloe3K8rw3Gke2CQzL8DcGYs= +p6i8iK7HDbvmLpoFrhNyvxdkpuCzYcAgEgoD8gO9QRA= +rYd5Q6Wra4KSueZqSRRqN4gDfkYJFjFLoq8Y/uFOPjk= +0aOe1PRcA3iTQeyd7fu/UT3OH8TgnUZIetc0kIThlMY= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN63igchzsu5zTaq9IXvBjrNVT0Ypem/6Ctem4lX8VHAu6 +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1hs1FfIPISXmd7TJ/r+Vl/cQigT5kdrpKyW4rHdGgtQ= +VmVrGQo2zRokW/ZuO9bN60XiiQBl5FEkQ6DHRSd+Tw111DT04Hh8GF8qhcK0KSh/ +VmVrGQo2zRokW/ZuO9bN65hNsUFyX6poB60L31EhX7HkJSxkQfY10wTx/PiHvRMW +VmVrGQo2zRokW/ZuO9bN65s61AIgyJ8ql62IoneEj2fAfjFSAde2JX230qlgjnH2vV81My82sV2kTGC+8zV2TQ== +VmVrGQo2zRokW/ZuO9bN60EOQAX3/UH5DkusEZxHTXPrNTCzuXya4iloKJBnudc0 +VmVrGQo2zRokW/ZuO9bN689+A3i/MUSda6G7q0qItIjO89bRKTthTU14pzjp1wZx/IYyafQwuUcVBEAoyeksHA== +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN67Ta1/lbp6yqDJ2hGru87rzFFPN0VimnlC4upIFz9Ysl +VmVrGQo2zRokW/ZuO9bN69twiVqU/YruzaZ31Ozd8kyG92yD5lEmoQmdiZOEP8KmMgIvjkFJw9Dw8H1UfQ7mow== +VmVrGQo2zRokW/ZuO9bN6wMQRwD6rXhLUXMdURcL5Ag= +VmVrGQo2zRokW/ZuO9bN640QMRIb+NfIAnqkUpUM0UyM1UW5nFbYvKPvqsQY7qs494aS/4n+dnRqG6/WBFmxDv9kb9uX+yopcF1JYP/VDIE= +VmVrGQo2zRokW/ZuO9bN62xFfqul5F7aMWtovWOJ5tZP1bJgyqz4vpQ1vcu9ZeS7nO6J17Baoc3zlb7Qe8qElLM00WI2XDnSBgzQHmlHrIgvHTezk3aB1hm9hvb+Gv4T +VmVrGQo2zRokW/ZuO9bN68PIFqUsDNaIp++rUmuGFKn4o7M3pUhaRcA1ks5Adan7jnrB7ZtcIC8KAwdazqXsJQnRsz/VyPAD6sC3mSzpwPRfi2ghVLotdjoha+woBHHlkbOiZCUrVfc7uglqYXgMF9aodbyDf26K3EEkt1cx5+Q= +VmVrGQo2zRokW/ZuO9bN6wNy3dMSKzw1+znxXLOAZLh5FEFcOrAX3cntmkTItZyKKVGgHnm92b7h7ork2RuLWUmLbdVgqwxu7Wl8pXJk83I= +VmVrGQo2zRokW/ZuO9bN6wRTb++fBMmvLx2k6/MEJBys2MeDsfiTOU27PQGTtFwGzoxQQOeDpqoXCiSV9odxKw== +VmVrGQo2zRokW/ZuO9bN65OUJfJvgNPZipZ9GCOTw7gT/VBpBNqR6eHMzDCD+qxc +VmVrGQo2zRokW/ZuO9bN6/6DN1Rj8W+FylDHNZarY8GmZYJIJsOXkIFPArsJz88a +VmVrGQo2zRokW/ZuO9bN60b5Dlj8qSCLiYdFYQye1vxhQfzXLk5iO7kCBpmwj0d3 +VmVrGQo2zRokW/ZuO9bN66Vj2MTf6PwKatUHlMDNJnAflwnI/tv9py6bv+AQb/3Mh1ASIGKNXpINw5P13Fnriw== +VmVrGQo2zRokW/ZuO9bN6zJK7VoTu9IkzGKsOyFHUih3nKOsBegITZ98OzfLK1Ug +VmVrGQo2zRokW/ZuO9bN6wragKme4d+RqlA7wSuKiun6Xqgs7hKkILO2biA4qzho +VmVrGQo2zRokW/ZuO9bN6924SEXfGwykaPMoxSMlsEcQAwBCGiNnpl4AiLwthDY0ogr81kYcskyrJDT5/Q87vD/ybRw7C5VoTHeL2G9HfhQinJgPrUBEI1svbLGHdxDN1m3m+AH9xwXwqMSg3chqEg== +VmVrGQo2zRokW/ZuO9bN67yPtu0N927f4HIlE8Q/3KEjZKgRfvyBEFqTjZArZjS6wWjCk37Cjc7Be61nCtFfog== +VmVrGQo2zRokW/ZuO9bN67oV8rbNeQxyRr5iXWNRT7jCGot63P1U0aZqnfo9iER3g0jsNJ4bdPmlVkmSovfUKEE8j1q098bPnAUmTwYv2U4= +VmVrGQo2zRokW/ZuO9bN61XvIee7O+pzg0zO1do+jmn7+YnxEi9X2xYlEBv1SXfIIfgDFycnDEX3zYsNHvW2uWPjibLCY7dmVLtuDnx9lOI= +VmVrGQo2zRokW/ZuO9bN6y82k0/7L8OC4IRsil2RtO1tWsTIDg48NsS7LlukbCaWEQq1DKn4luZ24BCSmDqhgg== +kJxoTVctk+bGjMYgKxsjjUpPRSfC+WsPwpMn5FGcCWkNpbjYvwc8oT1ZalJ4jAtf +zxNKIz/Dwj9Harw3dUNEQET0V05gcgtC1p+K2Ihkd0Q= +ACEEdgL/kNbx7RaZcSRxZGgMaBrP/U1Wsj+kqixOX3U0XMejpRw/rYT5i4ihGDYP +pZu1Giln7AYpCOrLbI27dxkWuQIzVUpxHeSHCtkSzeY= +pZu1Giln7AYpCOrLbI27d9FNbDQGKqlmV3/n549EBNY= +IhuisKim47k91RVt8z8qtCsumyIVSOp8xbUu5duXman+sOE6qwlPIuSaUbeysSjj1/coIZqlKQRM13ITsP3JTZKUmxD+QBryE7TKwJbeYvU= +DMKsax7BkTxm29PYxP4a3tCEAD0O5XmbLDroN8+cKNkbya4muwQehRDf9xiCNtXi5mN/bz7SmZm6fjy6PG4iu68u9ENCgu/bVtrtkv80Tvw= +v4Cu02hZRDsW+85CLRBC52OF7Q2oFtJnu5QwKIMu0BP97gowulUFTCsZQKMleZfSYIjeGZBAO1CNNX4e5jvdjNKUV0vrbVg3URKLSmZQu9pDsE/+LHEfAr1t4b9ExGJ+ +lfQMhYa2PjTGY/59jLRb898KNkZ/P9VvrG4wbt0MN94SnvQg6MxkeddSbK4PThoOhH+CDexHx9A6J1shl1FHCjlWu2q0GlNlu+H1BQlcdUI= +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +aEZ3meBRg3VU8t9WienCaIcMPcHcv2YGqbGshoEf2+BjZfFHOMPLysvgDh5kRgilD8P3YgAMi/iJ67yNmlvr2w== +mbg4a7OqVLuFyb4lBv/8ijnDf2TRJk/x5VtAuD8Lei+9UFA+RJ6F6QToHUji16bw +XisXz5DCD82wMRFAyU2YnBIQChWrgJ4+9q/ztStU2Rc= +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6oNJYJ3oKHQnTz+Hw7wWSQ +1u+XjG/2+GSQRv6EzCaWRQ== +2STnKtQfadtZuOOXKAyG97fXc39ov+eVBLi4v0OMsFg= +RGF+I9YWwlm/IovqReh8KzDsktfMik+IEctOJHKJWNfLhJfd7lO0N+PqYXq68hXW +wa6Uqxieufh+16mIo2wUWZ1mtKb/vzs433ud/yv/qeNzIjpgHyPB2b9qHji9uppV6FpACIOY4mogpGhOh+bSYg== +kFmg/dBFMjlvuWIcUrXktJM3uaPpOUszNWRf97JB6UU= +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +c0BvLCajy/2XMKFpBZTl+Iu/SkO0dvuX1US9n740cfboKQmRZYgu55JFFv3bWRzs +xM38J/T7qHoyh2Dm7AS1PeofVv7nUvjVw8J08A7tPoHKI8AfdUEMMT36J2TuAkXa +DjJQKue4V6m4p8kNUg01bEfz/+OabzCmVKUlV+DraSJuEtoAE53HRREuE0nz/mi5 +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6oNJYJ3oKHQnTz+Hw7wWSQ +1u+XjG/2+GSQRv6EzCaWRQ== +8asfyBO1JetUFwe4WyYO6Y7zXLxXmxgBzDu5xRXkg3A= +JkzYefpr/Q8/FMT+PiabUnsjVaeV4SXIGL0ZfwaksKSkUz13n6Mkp8jdbSsukdJu +MzOj4ZiBOJDNVP8vi/lFQAuCz8rMEK3CmFN5hj1hief/YV0qN2FHlXxmdMK9Ec5W1rvUke3pinD34K770i83m5kkbUBBkOOiDPjIkJrEKow= +k92oDIqD3u/zM2W83z4PpVdM7SBGKtVUm1Jd80kknUc= +gyj0tuy4SMPFYgH4w+8wBY3IXPM0uWG/GrgGCA34VZw= +k92oDIqD3u/zM2W83z4Ppf0NYHDpAym7jFb0+rpC0vUFZlWzp5yZj+qpxxcuNbrk6Vcu1GyjgOQWSHm+7DcKEQ== +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz4GANtbDXFyZG+JtrIqxTR5VPNmuw63EUywvzLoQm4kQQ== +1u+XjG/2+GSQRv6EzCaWRQ== +dq8ytOoyRUv/BeomAnpfcgN82WObFNw9OWpp+ashMco= +JkzYefpr/Q8/FMT+PiabUsLDCIfcYA9Gq0gv0IiQYerPKih0OKW4CHu8oewW1bF89ddkSdnMNn9sQNfx6cbWWA== +pTLYmNVSpbV89ewuJgN8s5DQJklspf+KlgaHmhcv0nA= +e1rLLbcLg7HKd8syQ0gecaIghAO5DHIkRtrdHrEdJec= +v4Cu02hZRDsW+85CLRBC56CB7hEAmjGZ+HomKkZKk+ti0sMqpAvIK7hwFc1+6mkIIxY3pr099j+UqOBi9ZahczUxI7FqRqgGWZSM6Xxqdbg= +uTEK8Ng11d3ix2pA+DD/aVQ101h9KjleSwZ0SYcRXd3eEzJzX3PloA8wBw8sN35krPKr/MyJpFx412GjefpG0Q== +uIsEpyb/BmTr4pwkQVVUf0RwjhiJQJf/hK04tdATmnh19x2HCfTqNvTq2rqHc5Gq +32pdC9DD05OE2l0oXazDFAdV2J9n5ffNkz5f36qCNytDp0iqq6ihW0KAr5+/dls1 +32pdC9DD05OE2l0oXazDFAdV2J9n5ffNkz5f36qCNyvubBPOn8LTVbiMDS+ibP+zvdPiwG7MZt2aBqQCUoiqByNRY4+XgGEEEtqLhpLRFzk= +L0eUthVnpkGsmKFAX6d+uOiK47GeV9U0IUhNuINx/fs= +e1rLLbcLg7HKd8syQ0gecfbZMbyjfRdQUOPnf5zIbTPr+V4LqLNiJCjGESBeFgYCEsvn7CupaoEzKXkwj2CZJsK7E0mq6ZE14nq0QU0PXcR3In/aUlH4AeU8sl7MkPKn +VmVrGQo2zRokW/ZuO9bN6/6UgeiotxsmEebrhxlDDLc8nC6Lwpn76wNAK3afDRnhMLV2RBSQaGynOLl28c/t+A== +YoCN/YJ3FGpKuVn0NXtXTgdEcSPVapklApkwW0i70PxTxKlglILzchhWiOPKZgOd +YoCN/YJ3FGpKuVn0NXtXToix6MqFL/QoVtdwEBkml6COQw8TpndvWpwBpvKNQMmTnxpUY/oF7+S3o40O72xFIA== +qxpZEz7yGh2snp/cK+0Ipk2Qv4TuHjOzatUb8F4u/nzLmmMsGUaOag81kxmq7Mlj ++jcV4UADuSsKDRH/jIi95dA2FW0TSUBn0rCfhGruLSE= +1u+XjG/2+GSQRv6EzCaWRQ== +glzTAHn8hffaBHtijzYL5UK18RwV0frcOEbqdQEdG+Pt92+be5zhscNFhL/eAiR9Tc711rcl7gLGuXJ+jzgMU+aJxXXBy1+OdZ5rcxvu5Fekj6TIakCimT7wMqEJ0BifU67RUtePcHtqliChztpO0A== +JkzYefpr/Q8/FMT+PiabUtwVJbCg1UHVw6o0ZG8VQbs= +MzOj4ZiBOJDNVP8vi/lFQAcd/8FSbCN3RDG7aQCgrl3NnBpIPZP1CuuiXlKwBhUw +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9gIulSKHdQPn1rt3eTIMEsl38gs8lF57/xlhqgd9ITkeg== +HNr5/wGY+7coHgowTe94LlheFV7l4nvszHiASjC/wO5DiwIYi0sK4qdLmDmUfgwDgE7PElyE7dR0LMY6vNGFCg== +vxZRPtvgnx9GXrXB4G/UM0PDJAHnJfAH1EU1B/RGx3flsrprR4JXQdk2AZEllj6S +gEJ/mwYAYmzZtF+bOEtqdkC8SHP+P7fOYSe7UxEq8rmitn+SghIfUQayzs5YBbRY +vxZRPtvgnx9GXrXB4G/UM0PDJAHnJfAH1EU1B/RGx3flsrprR4JXQdk2AZEllj6S +YoCN/YJ3FGpKuVn0NXtXTnef+ICKXNxnrkPoLNoM6bkjgMdyZRztWKGvv67a690e +vUSdI+D7GGPa55xHnexsw+oNL/ikozXaKZGm51a6QXc= +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +dq8ytOoyRUv/BeomAnpfcvejXPvE7NiytOq8XOUMnvml0KjFGpiByzoIln/KufdW +uCeQtkgRBtN0HiqPTvhZHcPnx9uQcC8BFwZstE/shHLyfSjSJNhoIYesa6Py6Tvp +7V1Z5ICwJM9j1fYg0cpccfMkJlFKvknmjzfobvmEo2Q= +AJ6YSai03eOKVxBS+s+A+/x4xEbw8mjQ70I4HsvrlCiUwZbEJ9HrEH9cZywfcLSS +auZi0vV8Th1M5xUz1rxru4RteY+AtM3Jk2k0FxXVoO8= +1u+XjG/2+GSQRv6EzCaWRQ== +uCeQtkgRBtN0HiqPTvhZHRHF1iwllZSPFSMptoCh8TWuJckkTFe3/T7PShD60DdF +mOVDVEmzgX6HnqY4rlOl8Zf58jGyWJmgY+rfwoP7Nv05pvO42TLHLIh+VtAUs/DRVclc15+ZQbtHBFMvjOH4+w== +1u+XjG/2+GSQRv6EzCaWRQ== +bxxa5unxulFhVIdgYf2zPoytXjzesmqgb/je+IsUbJ6JAnsRAoeAvcznZO+AYYoOKvJU0MVF4EHkDTvwO3xckQ== +0TsdrJvJroIDiAiNTYLCm/OphXNS2tzCddUWOSuFG0kG0hMQku2PUAi1ctdkxSUc +99nPav/MjT1hGMzUpnNFmmALRQAqua4s0cHovmRI5xqw0Ofo7PqaOiLu3iG2GYZ1 +KbLSYqE/CR9TL3ZELtBVgxmWrwAXvlf+ZAt0WDpGZ0g= +TQRmC0vOvJ1P+lfyS3Os4RfKsX8MHWdsIncvcLq3grDmjzVmBg58wHqSjU0RBeX1+yZNUjYibbjDYJ/2Zs4yH0t3SG/MIh4xPFPDALQI6FtMsmZN2Yf7RHU7vBbfL59WZll21jpCE6/0Kt6DwAwufw/X34CHO+/JlrRfRgCU4B8= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmV5FCFffpkRLBKsesx+UXBa9a/zzj7MC/CeJG+1iB07DF6gupL7r7q1OU6svPh0mYo= +1u+XjG/2+GSQRv6EzCaWRQ== +ZPttXNTCGDK18VzoR8DzYKyka11GnDaWIOOQV28/TIXqzJRwd+oVPa9D5jLMJ3az8mUKvRlRSJMZkBfK19w0h+r4ZtAuH86gZfkH0Tx/+KydTvaOG7bIgsn7F2KqkC3cCZfprRednK2p0WuLxxKOyg== +HJa3BH4TkRX/a5B9uAn8vA== +zfLEQOz1g23nqtChgBMTbhyiGXGhWhAY2+qwZtKU2zs= +ypyfLFF6HqkDeg1sXel/og== +w3rdXVV9x6u3KSOstnEsm4PLlBhDTI+8FdH5E2Z7OE0= +IwoYJLA2ek7vWcssU2p6YbPP0gHTLA4HmzobpfTCWYhvHXK7lgr2+CDhRu7uMpje93SAESWlp6DbIzT7VQNW/w== +jKpcG7quCcerLli2mJ39vBolbg2notAcBVKYoG/EO8o= +2SlvqX/WQojOSKqcazOiBA== +cBMX/M+ODyL+hH/po9GjAA== +EL5E2qD00QeSF0FNMBo12pQWeIyq9cNwgIZqd4VEy2c= +ZVauRl8efRxWkkXsUWoKPg== +HfHZ0x6IhQ8gr8sh9V0YAAATEqIM95DuxsRArsnn8ys= +1u+XjG/2+GSQRv6EzCaWRQ== +ydM+flUAneoazE7ph9Hof3rbIj+sluwHYaxCWsNBFhE= +P5FSiL1+Pqt0jNjlHNtw3BfOokQdNQF4so/qFjceHA8= +YnAdY30Lg/9fjxdn+Sc57PQiaDoqlnlto13gLyGNa80GKHELy59vY3a8LF3ptTbE8YxP/ayjze84gz3UubVSlu+mhbshLGLgU0vN/KTSqU0= +h4lKXT0d0p8lHVvwNQMn+LKoo6D0qtuqywubpeGhgjP1wmiYwY0D/HtlehtN3aCW +4xkKxP0XIvHb1D4sJsHh2gZjH1Vo4xijw1ZC+9Rm+a0= +NzUVwIScy4hrAjo551BWpxLBVrqGQSD9yBLKdRgxYx0= +ft4O+SUgqJFurYX9lapEio+F4WkORg7Yc5dRgf0GAKI= +CDLUY0WWS1opA7rPYYGAHqCEWLmN7ESaQ3cddP6FtEs= +D3tDGThV5uK73D09MDbkHD3JqWj3V3TF8FWWecx67z6FQM8aWFJD78O2J9r7oGW+ +fFIb+ccq5lrZDdMOZKeSYxPj8RxEZloL9mf43eMdLFg= +DV35hS1I55lzc8Rx3j2gKq3qpS4DGEY60WsG/QAoTXw= +tbVOTg+NfLlTyWxlgN7BTrKZG0+dC3Pjrp6KzKvtS4A= +u/w7lYvOA4sXyLYt/Ceg+dy7vbWqHKAUinXlMI+xkz0= +8NeoLWUN/HbOvEj4zYku64g6MsBHB3pIxUY9k/g46pm3Nw8eblpWcMTKpfvJRflp +661hZf7vhUQ+50okfwfTXw== +o+/LpOe0noTt0q9uY7caTYSaVB9I1gwyK4s4NWQ7NUA= +96orka/uERLyRst14azQwhCOqhTfcgFhXNAQS0hmuOFuEANGB6S6+TlaG45P8xM6AOLGkW5sS44kMoI+hpOgpg== +1u+XjG/2+GSQRv6EzCaWRQ== +sDEbfywocoesN89hhGJfbn+Up/JgL1Qg2VHfdBaVSh8= +bdMrmpuFQ7yM1zhu+h/eLpcnNtalyd8b0N7ueBW8qxJW2cs9cWz74j8D2CGsBGSf +7ruGyOsuKMUu2VJjD7qOMeaAkd8lbFxmFgrVLxFs0oPIjIIPvkx0ZFyMjf6oRSyt +w4I7mVFZGDKiBbcb41eQkxM4nnk+e7mIaguT0WGkkVtz+XhNeocu4Dwlis2SGs+uEVlFlJ1TTROvWNVda6V51XiWTQVNeeq+mwE7orSeGPlP88A2APkdQ7WXcpGsRZSeQTuqKraUSJATMjQccjEKUw== +VmVrGQo2zRokW/ZuO9bN646KfasAt/rPZ4Mzei/x9X4PK6kawVkqvHaQdE9ItYIrN0R1SNQyDXs9q/cle8yTU7duHnPCfZk7g+UKQZaDYdoQ1egSYJLdZGNCNlpNm7lOjvo7TlphOadcZr0WUZLKDg== +a3jbUXPmyR9Vb7Xvsw0w8VcvNLwUIPucqyBxSewCjvpNGf4qVC9bC/Kq8Ub7+IjB541pebM9XViq+TAyzXBMtnoEC9wqaxeFrEhoP73Ucb8zxTbkd8fs/i4yUk10fuvtF4yZG4ou3qTgQ9i1iaCFJRjDi7GvIiufx6zM8N7OwL4= +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz40OtKZG76yI8EPfLXQ/ptOrhgtHi/2LckZEebSz/acKg== +1u+XjG/2+GSQRv6EzCaWRQ== +dPf7g+b79gcCgal9DX4pOJHyAfVR/z7s6ET0GpBby7E= +0OsCC+YpIPphU/YpLoF2Kzv8rZ15USoyP9EDEDNoe4E= +lNoJsK2hWEGOaXNpZFMEKYAMKxXzTQglIlFqVZ/qv90ift2NxK3uK30zwQMR/g2E +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN68kG9x3f+w5aFN8e4/3SB7hYxvzV8Iokc8k375ZmlVH5 +VmVrGQo2zRokW/ZuO9bN69n2IH9Vunz7hXYmWIVxfwPXrXJCVKsHyYzEby8GZS9u0PX7VZIVelGtG52pT513uo3a3aGreyDkMtDppIaQp1w= +VmVrGQo2zRokW/ZuO9bN64wauWr3TvqFolPCIg7VtlOeTNs/abgaVrpCuUHCHrpv +VmVrGQo2zRokW/ZuO9bN6xoSQ3etHchjGzx9GgAoZUY1EPK486h/g4GcxqyTNCwQ +M0ZySqkmhuHCw6olbCKv97KlG7pHnkVH5PzxCv3SOm8hBQ62OlY+Ty/yX79KJJrdNu+CHQd3CPIkR67gsrZxGblpwz5ac1JVqyFJQsp3MvnS9K1ZYGSHoLMHlhFGV3zU +VmVrGQo2zRokW/ZuO9bN64/n5EL8V7CIqhp9FPzHEuQ= +0OsCC+YpIPphU/YpLoF2K6FQb9eutkS0W+twNd64F+O1GZYjvNnJ0aTvS0PBP4BFzU9cR9pgLcItJ3/8oa4lp9NS50ZiYATfufhv/OY4HvuGJ1DtfPl2w5Pl47TRv+1Q +nOflcriAnpFPxVtzU4aISA== +xUiJWUyMvL3eKTICfxM7FHlF8+ynsJSeQ8zvbn8P6ldzdd+1cSzmI5LPDCIBQ0j1BMY3iQTiHHXKXbMlTNzbIg== +4lm0ybc3aVxLaNQODc9aBanfiyaNlwa5Y83cl2wzTJM= +GJNoiDRZuv2t3D6WjWIl+qITVcmlxn4NEeTza92NOQJaBcd0iFalRE3MrJWhGMGx +VmVrGQo2zRokW/ZuO9bN63BJBJEXm+rc5N8L1OHvM55cAO+Ku+qj4muqvW20+pVMp1wYb7OOSX3teOhakn3wUJeQSJL3mZUL2AZMqu0/OdPAAjhra4OMDXmn6v6Poq55Oql860T4eKYpMhnUuGUa3A== +ACEEdgL/kNbx7RaZcSRxZL+LzF2dVZa5hwngNS0Drfw= +VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM= +lGY+HGJ/DlLufZHWWlj7U7tQubWSU1u4v7fNn+PU7V0= +1u+XjG/2+GSQRv6EzCaWRQ== +N2ewPkaF4vBztOl5yvdzXb12XWHEkfuTMQQ/S/IatrU= +DyDHCcahxG8x9eot9BhGGoaSx73c+e9U0Tp+L9OcbF/XbjTh88pSzp7kgiuYM1B2 +7ui7GKVu+hA1zn3iOmo+va7LW68AgDWItaWEVH6a7oq2MogBYQqB9a0ERfgAfeqp ++4NbF4lLQ35m5UiB1/0oM4hG33zvxiKsPHbFeRE//WI= +ErYWeNPhZeZLxfLfOGSlIroR342Y4yDE+5gd4kGeGi+Cxk34mAtGJIhUD+XL5o6JTvUKzpV2ggBzciaY2dSBkA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkk0jB5v9uia9s/LvId4LpDPxheq4KAuxy30SMBaP0VoMyqtdF/x/wrhCqsqsW1LYHw= +xljVybfujf1U6lUBrfLztokiw0p3LjxCtcQVbdFJCh55nLdKbOwDf5U9mlEssvrw9UuksdylMWv6nUbn2eAV3902tdVMAzh0p972kCdgMT9YwNZR7uu+4w77+HGAahFEj9ce+vlHwr+dBIbvyCIAnQ== +p9oVsBgcyiBMjDb8CcPOqN8UTrGCDDQu68V59h0pkaI= +L5I5tdTeebwyRuKjqW19YSTNGjiTv77wwjwChdXruX5PCPlB01g/8mzPNbcCV4Ej +JXk5JLKM/1JqEbD+ffdNPhq/wJlzDLI+uOyyhRBAqdkc8glvKXYuquXWB3Up9JjK +rybirQdlAeHusp/N7GM0sKNwBki4q4kuOQGfi5/04eredJsBktFstkBd+UVmjZ93 +/VHZ8wcOZ8J40IbgQT8a2I6Oa7H1i8uh0gQGQdXA45vcac/EKnJbhl/dTCny6O14DZIAcyGMJrA3ITncE4RtoQ== +XUWmzg0XlHIPZ8hF18QkYN0Aztefgf1QQvjhvSQd5GE= +XUWmzg0XlHIPZ8hF18QkYAPejS/nuYQn78KM68umIdFJnVBP9H5IDJmlSsR5EqteYipZ+YiJplji/ek9bSPUaw== +XUWmzg0XlHIPZ8hF18QkYObeL8P52xvmTgwJHyUm/mfLNMDbo8CGOnJsVUQBIWQlfvvbM+AWxxqMYzuuII860l/8q20ZMBekthIoTIQYrDy1B8N0QDPw2xPUp8eNDncZ +XUWmzg0XlHIPZ8hF18QkYAIVEnTrHWvEYlMdWS6Jr8+JxDnush415VXf5xXFH5MfPlMYBwh5EK6N7tNxMd+x9rx7QHcf0/k2of4zBCX6BgG6dLOO3vmQ54XKDk9bQHsNX8oqmp8RafMghNfHp5m+hQ== +jU/UoCJH//JN+eJXlaFsvzU8G2xBEArehtnC0GEU3kba88wtG61nlrgiA1vZDQqQbOZZmS5jYHUfwpgNaCOdal4h0gBZdfVDpEG4TnwcSco= +LjE79g4isZOsOnkgBUQGVvXGCw/Wz+rnAp975tiAu3U= +1WJNrGwGz37apsG1QXCNVX6sT/6pveI0wT7pr1N/pmU= +0kmS0E/2gTphgwOZpwAE2yHpQG847xl/ajDfa5h5ju3vA+PsuXXfOQZPpYlHfc4h +4uuueBhid9aJeYpW9TknMTKFhi7c1ZuCDkAynCPWLEE4f5zCoL+aW6sgf4jEjnJZkRv4BgsIwv5h+Nn8Ll5k4uJ3HJUccyzNzNMSKu4HZ3E= +1u+XjG/2+GSQRv6EzCaWRQ== +1zukBHMguVb5WvA8m8HQjPjAhqCqUt1LhX5PoNgDmCwZWDEDFPUr61tKw2fBGv8i +R2NB8DN3pOWXcW6vM0ClmEMoF71ke+0fKsaH+rPi+7Q= +Gbp0us/o/RYxNCpypte8wkDs+L9ibC0lB6ODeIGeBUNxo67ovf1PiljiTsXxss/EimE7fTfi4tp69JeeIZ1JucqFL7uKZjRgXzm1wTNci/8roI+2d1T+pyUp5Kd86NfJBDaru4YN7mCFiZHuAPEnwQ== +uIsEpyb/BmTr4pwkQVVUf4sU8Bq17UUXFXa/GD5UzvsLsnfQOtQoQPl3At3HFxd9QPRV9DEKp1t8SX0Bk3fmnuQ1MvO6xYN3j3krrbtx9Ri7ySAONSAUfWtbyjvLcqIKqJ33t4d6RxsAm5zFneySw9JQvlUNGDV29LpQsznKjgQ= +9AUqsxjSX9s1jg3GUS+NB5ufD2BIObuhk4izDv68g49BKR0/lv0Y5YYfBQ98x9ip +MzOj4ZiBOJDNVP8vi/lFQHphDQLizMa0vztkd1r5mDy3IP/1XcaniiKAbPj57C+Gp3AnBwYczoXDbvTbbHXTgK3WupMUtAfvAVyFkTcwA8YkZlk8xc2iiHoJ7UnppvyxfNOSQa2GgQVTpBJnRQPsBI2y6/oyrjRnCa5IW1kASp0= +4QQ/zTZZdnopElj3JQHNa9PygZa4ZCO9taMSrns8R7Q= +CAhn8dRUItEbEErp4w+lX55YSYfu77vGi60UCUzYYfp8S9MgKw/toqSe6FMs2M1v +1u+XjG/2+GSQRv6EzCaWRQ== +1XSH7jd9+l3kxs8yJf4VMU+0Br69HiFgxoemVlEviZQ= +1zukBHMguVb5WvA8m8HQjOPEJCJ9+AzZ9DJkuqA/jaCsGXgslAqdvnFUngarGbeG +Gbp0us/o/RYxNCpypte8wkDs+L9ibC0lB6ODeIGeBUNxo67ovf1PiljiTsXxss/EuN2TsKprffsLq+piRlk9US4xVMZW13fxkFIp+7Nn7ogIvtGKczBlxvsKVf4Lmurq +MzOj4ZiBOJDNVP8vi/lFQHphDQLizMa0vztkd1r5mDy3IP/1XcaniiKAbPj57C+Gp3AnBwYczoXDbvTbbHXTgK3WupMUtAfvAVyFkTcwA8YkZlk8xc2iiHoJ7UnppvyxfNOSQa2GgQVTpBJnRQPsBI2y6/oyrjRnCa5IW1kASp0= +b4OJVZe8QyIpjuTpKXDL9A== +n/Vg/an64od1JqfYD8zjtsZlIrtOVreSEto4JRdo972fXSPXC9glbleBdvuvJyBcLXUEa9mxHCFwIk/EcNAsxka2/PVgSn4sRDhJcwPf9ZuU+pcTYk7yL9iStuLBH6WcOxYcU535Ts8JW22TXuRUZg== +iUEDlyYF71b358pIL4FKL4uQouNP3knkritXeMCFqD6RcAocvu7FrPVBCb4SQVlH +JomK3KjQVhm7axi7Z5yMpzSzedclpQ0EPWf4a5slcp0= +ynmt6S+38NUiCOOHWGlzRziAza4a5udNX5tgg2hzQ1k= +MIOAnnldxAFaiMyLbIEH0zOpkRUjLKhsZU8SfkygKU8= +xWoGNWjKGPfI4gq8aHoTfCbeqO6O7OYgG1AC8Y9kBQW0BOAdclPvoPuB3UjdhN0TRKx8JizDm/NQEXrzbcwHmA== +1V2v8QerKOmubvSxgB4eTDu/hxOotI87lxS6veME3Crs2k0u4BS95hFjA9PZArPg +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjaVq63HF67baKhTwF5Wt9FBPiGnoli/Hkn+AIFnauccJA== +F9Sftf1PoN5P+lgvc+r10OJ80Vg6iPphyrOoh/5x4bwMegQKx1lxJt2/4i1MN1Vk +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjY9DrK1RwfWAFBMl0E5L7BGp6Et4s9ptYJscetnOWBhjg== +F9Sftf1PoN5P+lgvc+r10GxubuveiTDZyIVny6m+75S9YktzS7qqSq3X2ySJI1VX +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjYzzneH/VPa53Tze+qkheMyIZg41dkiAdfgOiX7zd+woA== +32pdC9DD05OE2l0oXazDFNZg85sdZ06B0olaVOjcsr2UCUkKahTATxT0kBv7/uMo +n/Vg/an64od1JqfYD8zjtvTGvwYcoCTsdR9KI4oJEXjHrKKo/lK6NN3u1IyRnVmJxAC02FuWxa549NUc0lWSZQ== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6dHp7fYDGkVGjkCRmJH6NHpLEmoBoKZCqxn3HaNJebFni7TM0UOTkq3uitwYQRUL4= +1u+XjG/2+GSQRv6EzCaWRQ== +1zukBHMguVb5WvA8m8HQjDdAZWNCBhb/LRFKU6AoQAPnDsBbWhpceRgHN0OMdoTg +XBwzd37QS0Dep4YK0TJlr1JCCGVIs7DD9eBryOC6u50= +mX7fgLhy/TJmbe7Pm0t6D4rgTdc0/jMdS8+hY9YrX9k= +ACEEdgL/kNbx7RaZcSRxZC+bHLMedAa4IlPYPErxwbQbb2bCimUzVAoWiGPSZjhG +J1UGlXkKhKXD36OrXXhN8DRNLBzSnpnR9zem4Hl46aYT4DaL8TwLah64w6PefGG35pZu+O411OAUqUyJw60qew== +iUEDlyYF71b358pIL4FKL042CE27KtREMjdeW1oXex7KFuFnXXodSBFBSaoKtMYZ +JomK3KjQVhm7axi7Z5yMpzSzedclpQ0EPWf4a5slcp0= +n/Vg/an64od1JqfYD8zjtgR84pQoEprp4i1Z7jZ8hf4= +VmVrGQo2zRokW/ZuO9bN6xBSBKdKa/mCsbRch2EchZY= +VmVrGQo2zRokW/ZuO9bN64/3v10sr+z8K0riZpxmj3dhtt9I77P5EN2qjRUN9+HZNXT5BzWZZUZQ1+4tzdGNlQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +7CWMhMuDAW1zoceP42HFI7JkOskde8GnIX1Kh6Fk5FtQiq0ZdpGNGcR5RDi7Pioy +ORph4c0b9qayU8qN/Z+ZPvufkaK4ZP6Z5pDSWcXufAEkqWlcZmYJvpUATBsncavX +uTEK8Ng11d3ix2pA+DD/aZwn7g4EisNaVgVRLiZl0yo= +32pdC9DD05OE2l0oXazDFJkxspwkL8t0IItwxjfnC8nUjRvvtzFDV48AlYBtpE1m +4gO44pHRPczUcmpB4bg0pnQGVtS7ogyMz4MAeBHcMtAxe2eF9/DiCFsLtpuKiL7B +uTEK8Ng11d3ix2pA+DD/aQrOlBP0FfiwhlnNZOyjgoo= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6+Iz01H3FqtnAgb7mhzu25zCCKQUPgasOEYO93p0OIwe +VmVrGQo2zRokW/ZuO9bN65rD4nI0MfwzlsBII0cdPaa77aMSTcf8piJKRZe3l5hL4FCHh2Pq7Gs/Wmj2RyJLsNkLZy9ia0AMqcf6dGGGEbVe0xvfm155GJTo9663VjJzFiKsJi7SJETzvnTYIwcltfuO8Uiv6IhgN87wNdwKZck= +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6+Iz01H3FqtnAgb7mhzu25zjxGKQtsXlvqkagztbW1AG +4gO44pHRPczUcmpB4bg0pklZgQJXm0A2Am+4pJfHwxIxdfNIgu3xCJb1c+nvhf0I +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +SIVhMVja35cNhEoyyFHyU5ueFbyMySbyi9mnmmhpxVU8hpsEV8BTtrKQwQni3PGF +q4nc/jwATOMUyfSjLibfEW5DCAn6VBbRQRnaOjh7eA0= +WA9gm/QP+j5PVCbJRTSxYwp3kGb4cMqf+qwJUWVEEPnmqCBjvCtbmGcOopjB+jck +UBbQHMxDYmM54VXbqfW20S22f4jK6BUOrIYt632vyLE= +0OsCC+YpIPphU/YpLoF2K65mO2fOEgg9BDORJsJnsukfPkOuC1C9HL5KjkD4ozjN +v4Cu02hZRDsW+85CLRBC5+QhixIrX1sjRbc+k4FIGXQqvaBnxkvwl7N6zF1jmyIx+QaQW6LMymcXOKxyLELGjoDaY5dHZslh+zvOJ3nOF4o= +v4Cu02hZRDsW+85CLRBC55x7KgPFiTIBJuK7Bj8UUn6kT88eAAMKmh4eP+KGfruG +v4Cu02hZRDsW+85CLRBC52M44iq5ZlKYvAT50T1VVUxbP+lYYf2HLRnTkcnOv/7RLTiXmmYbKFrtds9nTvmC78/4+GyXpy//qMRxrz1Dp81yXvKLN7kSquN/lGm0CbyIuiiLCxeHe7ZblRuSW5w/ew== +tWSLqD1lvEP9Rpp6I27PpvXrraKAQUvuzpkWxLOkdYVAxT6x12Fs+vuA/HccQbq+SMpYqdMtX58R+H1Pdla69g== +kJxoTVctk+bGjMYgKxsjjSmeY05Z0U8C6JFZmjUMMCSxIzLqQNB1ftjf5Kn92nPV0SE/2kEHSEkvBISrHEOc8olUZRsKyiScIOXAVmL8+AnZI2OJatRCbEYuUHSOXcH0UrWCb9M0odIJbfpL0b/Lw0Y3ebqDZzALGgeESF0E1IJoiAc76H5FHNtYFugXFh9i +VmVrGQo2zRokW/ZuO9bN679dY2ryxqp4ouOaU1fOIumFLB70f/WoAZf/2w7Cl1Z8ZnqXwm8+/EGNKMaSd44zNA== +f58SBztD4UCgJ/6kPztVo29CIZDUF3sUhAF1SPuV/14= +lGY+HGJ/DlLufZHWWlj7U9tymVCSNLDh3vOFZaRUBuc= +1u+XjG/2+GSQRv6EzCaWRQ== +nfkJ3QMSmeMN6/r/kF1fGVfrMpks76Z9i1HsARq3TTrMiMLoOh4ZCWV7E4Vz9LdZ +b4OJVZe8QyIpjuTpKXDL9A== +HUxaWbdFNk9pZ+7x3i/kLN34rJMFESnZNqgFnOTtmncZ9MojyT4ik/ZLHKgOBvG5jYiwtWKmCbv5/tg2EykmXA== +NhnIR3Ilo4H2su9/cTNo/Ps3vYSOiOY6esn1xeoWmAlrdRFFnm+r4q+F9O7KjdciOkRcYwkz4orBueV5Pha+Cse7WK7UEu4tjn7yjq09/pc+cqG1kgwWKp3q5L/lI5vjJjeYoh0VOhmY1zWisiB2GA== +NhnIR3Ilo4H2su9/cTNo/IbXc6jmjXpdg3/b5Q0M//5RfYowE2A9IKSdANsXKONc +xWoGNWjKGPfI4gq8aHoTfDMe4+Xx9fgEfvyQdRt49XptGFFda7+W0fuqWp8j+ScVv1QIofgDM/c9givgAZheB/DaAF8Pow88H+SBfdPZZ4jcj49xI9n48vcxq/0T0GE740KKNSWuK4oLY4A7yJGVoQ== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmV3FI++vVJXDLBceCG9F0SC+wvWb5HwFmxE4KbtErnpcH6kh83d0XIEyQijeUN9AQk= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXHkkiJnu/WJRPwK+C9Jo/kuW5qQduKUoG6ECAUaT3mOaQEo6tdlrSdJq8+xmfNVcM= +1u+XjG/2+GSQRv6EzCaWRQ== +KiWYwpJCvVh7pD2s6nbEY7wXqZOCFGVUVZew5Gnpwt8= +1FfqRJIaTv+Hy0UdnrEpXkay2ZR4P6JEneOylPoQjT4= +atQYUl7VDB6q1/U7wutSWDfOdKHc0LO3GBehPGzz84kB4HnSnaX9ecgsgds2p55j4iHYP6Wpw9G6UAP/36tXv7T/EbaSkearWbzt3B93hng= +UV6WV80R3POE+S2K3cNp7Be03g32NHcbHKk24qNdLtrAp0A+usTj+bCpNbB7iygz/7dklagArUizAxR8bSBENvVBo4ahdgX/vOCyrqfDp/su2Uuyp1mpLI0Vy7snmGERDLUpK2Zqa7AQksPVyiEWlCd+hBNkl2UKZKLhWirHqgeBi8iPaxCuOgmpX2Atifwd/P3t0vG3A9T2cS5OavVrTz03m7lAjGsSahK+zwnBj/ARP56LJ+x3wH4vD2XpZUmfigVWgJ0qHCOXBPqWJRUgQYjzxcILnrPL49EQ8j6sQR8nwgDL6+8eI80pIsF5ekz/E4h5peaKypVJdL3SarYHxLBkhz5E2Ve5eFOOoR5rPUfbKEqFWSj9zKXBEYDO1lrG +Z6/7wkt3BMqq5Y+jFo+XDlF3kOaR2IeYDNGUsl9T0uz/qKT0IazJpYRH+vtnDhQSFLfiJJ4mFRQHBu9lRaGLjjTzA37xPnZl2145pk0J6Ek7HdHe8JPU0ARBXIMrnfUMvb0t4UnusA7rXHOaFDWYu6KxxuAp8XO/o/n9p9fNFIM4oMXIVbBiDXhVKT8DWLz8 +OlFvTKqdeKF9w1tvvZyIDNVzahkzwo5OtwSBOHqpZq7rQrYcRl0GU4rDuB4C3t0SlpJc98Bf9owLulltlNj7xX+vOoRXJdkhyX94sA4Hc/qCP2bvIeB88DI58M/2k6W0Kb918bA1ebaRMTqIUaA5Kqb6WEP2JiD1O/E04FaExsDUW1TzKGwXHdYljEUHNCqr6OVoNDDqX2VRvXQN6Cw/4Bow8o+getBg3mMkopvqKac= +gIITPXcfsbW14W9koRVg64ubdAIQuwogVyN30riQP9qCEt14hC9y+w/0OL0gIB+uQpDB3L2npfDghCuu1ZHAnw3mh7q/syBZmcJyI8px/6s+C+gH9RIJN1dxRFC1OvA/87C4igNS3p6LA5Drq+eKLdx1kZpfIkEqFnzgcD72TyyqxJifu08CF019veuodsMQS4PaUKFU5lzWiAjKqTOusaJXA2XRxOT3j0VDDNr8sezyKQHdB97nFUoxcZXHJh1TbctlinXCIQIZtO5rF+MOpxv71Janh98MN/WB7ZYOSrsmBA6Wb3RKODvrTnlotkCvSikbwFecoLl/Nc0gRpJEw4jlSUQ4Lzf+Q9MC1BEWnEc= +AE2D69YCySZnqC/KuUyd6N45vDT3A9gQy23Jj38jy1qskqQxliSU/A8Brj6iQDXSs75PS8HPuR2HhguaRMk0i1m3IXtHktc0SKO/yoH9h2jLLKDmqoax0rksDE4PRuk5/QAaMXTwN884qBDxCUPYrqynQjdkcB1vQBAm+Rrglu89a3x/SP1IJ/AmLibCQ+1wOAEZu6E/GlBCpc6eKAEkrViYqG9iB6/K3QWNIVoPCEJDYlWO7YLhMAxGapLU5yhyyqW4/7gqaT+RUob7ggp2J2TQbMTjGy73viTlPPVoNz1pOo+mVPNweiMJlvtnpo/XTRxDP3pJRpVbdtXReiSrdQ== +q86vJ1Gk5zoQJkWiDC3EDCm6rrr04P0h5y/ifTvinR0mOl402S5t1jFDo8COuFsq8JhZQOn1a3UklBocfPKb43Gp1Wc8I9Wxp5gqNSmae+T2Np2j3Sgc/LVl/pJqt7AM/4quAs1e5xZTJANRTsYN7TpzCYuRvVXDbF3cbKYXcEpxiBkEDLdI3Ua3ffXh9scdAsiV8scPZpYR1YR4gjAwqfDYhwyJywPF4GvmpNBGMzxkCzMBHH0PEcbydn10mQpRkZBclHGE76Wp1zk54dcMVLCSoZCZZlNVOo2YKwqgwtD4kP6OQ8u5MxZP7jRZxY4e5nsY9Z8BZhDm+OMw3NrFCrfwkNrYJ/VmFauynaKJzFDuXgkMOxr/QUC7KzBqUK8bPjzfCEB5UxjZcnotUcQz0PffMJ9s/HiWprEfRrbnOXzY6lL9OY1Ici9rV7dsGNJR3JdmAA5nM52Yar4LjGhSxA== +atQYUl7VDB6q1/U7wutSWE+oQjmLkleKASuxcnC1RPu7SFl6slp5hJ+4H+5FMOCozi4z0ggrYDuirySFenOLoibqLBJHF35/C49Aaibv86euKItNzlPqR8c1tXjxlBu6Y9sgjFk84IgHZHBm4L3gy3a2AQiIZsw2w6GmtJeYkBIx3yt6ZBg6tviWnbqjyuGwH68UjvIexYJygtqYL8jdf0D8ZhK5oKe69wuZeDxCBWY= +VwBeeHn0i7Rn77yuYYaoYKOFCoYs6/U+KIfpnNtzvLgx4Esbh1ywIem8O9r9ZlZizISxhKnXrLS8Ln9AU9J97KkOb4piBBtDAUgLs1IrB4ju4FZ6/AD0DRQ5MqyUF+zHDSoGG3f5reENpJTP9fuhP3Ojx3Euwks3RYCQaLMkmyqhMa3omJwwPmLrZZD/gSD1rAl2im7Gaw8LxGXJhG/UTHFdjAqDasbmkUPee2norYYGvi0Ad/id5IEVSqbHAUq4PTK5EQTaKgT7X5QIwioHRlrE74UM5Q/Fqupd6Oo+G5KYvt/0qtDFbppT/fAe7NSPZZoXmt0BTk07LD+AQoNC2x0Jn1FKKGvar8S/cKcMpkx+3y+iH4HJyJd1cDOLPahVh12PUyF+3Iwg65LPvtZ/RUvtVlGxV1tOu5UqU3XVFLL1fsn+JzGSj+bJDWMFcxeHS4n6dacum3fMb14C3h/49SLDcz41r9h/fy8xHuIY0kY= +LDoWyYEVft11g2fMaUoWVvUoZyA7OvubcIVI08nXIPMuIiBls50NAqG6ooq9SM7y8WAkmw5NkFF/bWQNH8t5AT0MvZj8HLRIh9wKv82Yu3COzjzWJuFwlk4CcxuzUL68acfQfRNnfgabfFHsQ/koI1ZKHagjHsOARygeOaXNyi+ERd3BH1NwDld5D1zcssiRzMXVbsQWIINAtahNJCgMTLccxUwiENDaqFgK/L/pDFSdoYLmaK29/4IeWTwnCHXs6l7KUG4gjxXCZuTc1EI0Z1+H+SYp8ms5UoXE8vG1hPoeVsDg3JdTVH+2fCgUL88C +qzAWBJSCd8usbTDm9R1x/F+psG2bjKcJon/AVlQDaOcXjSg9GDAG/OUhpAnjjbv26MN0BWj0wOLQppGlmXv+hjlsQt8A2GrOZSowavDp7ZK4k4/C9x+D988/jEIjVmVwD6u+Ro+/yW68aaAxkA7FTv5Sif2Jxs6QkEjlZQjO+QR5YwxPeI8jbx7WvJK4h0hQV39xODIGnTfhoVcMBjOu3evTME084xGBqigLtOuWr2gSlbzsHXANuPSDE5Pw5kJoNiR82/ymHIcwe0SN//bdQlJuzzGc4KKB1nCOvbhZHCPcNR4vqSwzp4GCzS8yuC4MnZp88QgDIw+ZfUaOBzGpQo/Pu687irqNzBYlvR3eSgU/WaRHHPBb6ACgdZJ4uI2EHp3MpemhtqOykaHa2zR7gQ== +gDAZChPAe4zQXRH7y194sgFDrEiujt1haRqmInWTNIXXz8jwNwQY/Hf5uV579oNJmJYiEhEsByXbCyB3mulejdkdEqqS8tZo7U8d5tp8qdNxTvDR7N5PD08c9fn2ZyacZwggx6EB07vANwVmI1RlNr3To2op/iD3NI/Al5jJP9Dwe0dpqXxmpb6Rtl1rcU/2XkJnLIBG8DgrGwaGrxnX9c8Fa6aE1u3r+JUpy/oTOfOdLRm2rx3N/qzU/atlpJ41omtUqJmssT+9PE8Naks8An+SQGiyTIyyb3fgKQ9vNyag0aCP/FuEbx6GvZJUxIz0m8gj+Lx2dA8sZRPV7ktDgyt0R6jLNx5mEDd4QxkggMkPpt3hUnU9E2kcyErysl40pKKayz2U+7+Fg3m0qTmt1A== +661hZf7vhUQ+50okfwfTXw== +FEZSdYhAV26PfOeirhfda8qGB+2DE0ehPq4imkXwDHE= diff --git a/class_v2/monitor_v2.py b/class_v2/monitor_v2.py new file mode 100644 index 00000000..fb7e1117 --- /dev/null +++ b/class_v2/monitor_v2.py @@ -0,0 +1,380 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: wzjie +# +------------------------------------------------------------------- + +import os +import json +import time +import datetime +import re +import sqlite3 + +import public + + +class Monitor: + def __init__(self): + pass + + def __get_file_json(self, filename): + try: + if not os.path.exists(filename): return {} + return json.loads(public.readFile(filename)) + except: + return {} + + def __get_file_nums(self, filepath): + if not os.path.exists(filepath): return 0 + + count = 0 + for index, line in enumerate(open(filepath, 'r')): + count += 1 + return count + + def _get_site_list(self): + sites = public.M('sites').where('status=?', (1,)).field('name').get() + return sites + + def _statuscode_distribute_site(self, site_name): + + try: + day_401 = 0 + day_500 = 0 + day_502 = 0 + day_503 = 0 + conn = None + ts = None + start_date, end_date = self.get_time_interval(time.localtime()) + select_sql = "select time/100 as time1, sum(status_401), sum(status_500), sum(status_502), sum(status_503) from request_stat where time between {} and {}"\ + .format(start_date, end_date) + + db_path = os.path.join("/www/server/total/", "logs/{}/logs.db".format(site_name)) + if os.path.isfile(db_path): + conn = sqlite3.connect(db_path) + ts = conn.cursor() + ts.execute(select_sql) + results = ts.fetchall() + + if type(results) == list: + for result in results: + time_key = str(result[0]) + day_401 = result[1] + day_500 = result[2] + day_502 = result[3] + day_503 = result[4] + except: + pass + finally: + if ts: + ts.close() + if conn: + conn.close() + + return day_401, day_500, day_502, day_503 + + def _statuscode_distribute_site_old(self, site_name): + today = time.strftime('%Y-%m-%d', time.localtime()) + path = '/www/server/total/total/' + site_name + '/request/' + today + '.json' + + day_401 = 0 + day_500 = 0 + day_502 = 0 + day_503 = 0 + if os.path.exists(path): + spdata = self.__get_file_json(path) + + for c in spdata.values(): + for d in c: + if '401' == d: day_401 += c['401'] or 0 + if '500' == d: day_500 += c['500'] or 0 + if '502' == d: day_502 += c['502'] or 0 + if '503' == d: day_503 += c['503'] or 0 + + return day_401, day_500, day_502, day_503 + + def _statuscode_distribute(self, args): + sites = self._get_site_list() + + count_401, count_500, count_502, count_503 = 0, 0, 0, 0 + for site in sites: + site_name = site['name'] + day_401, day_500, day_502, day_503 = self._statuscode_distribute_site(site_name) + day_401 = day_401 or 0 + day_500 = day_500 or 0 + day_502 = day_502 or 0 + day_503 = day_503 or 0 + count_401 += day_401 + count_500 += day_500 + count_502 += day_502 + count_503 += day_503 + return {'401': count_401, '500': count_500, '502': count_502, '503': count_503} + + # 获取mysql当天的慢查询数量 + def _get_slow_log_nums(self, args): + if not os.path.exists('/etc/my.cnf'): + return 0 + + ret = re.findall(r'datadir\s*=\s*(.+)', public.ReadFile('/etc/my.cnf')) + if not ret: + return 0 + filename = ret[0] + '/mysql-slow.log' + + if not os.path.exists(filename): + return 0 + + count = 0 + zero_point = int(time.time()) - int(time.time() - time.timezone) % 86400 + with open(filename) as f: + for line in f: + line = line.strip().lower() + if line.startswith('set timestamp='): + timestamp = int(line.split('=')[-1].strip(';')) + if timestamp >= zero_point: + count += 1 + return count + + # 判断字符串格式的时间是不是今天 + def _is_today(self, time_str): + try: + time_date = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S").date() + except: + try: + time_date = datetime.datetime.strptime(time_str, "%y%m%d %H:%M:%S").date() + except: + time_date = datetime.datetime.strptime(time_str, "%d-%b-%Y %H:%M:%S").date() + today = datetime.date.today() + if time_date == today: + return True + return False + + # PHP慢日志 + def _php_count(self, args): + result = 0 + if not os.path.exists('/www/server/php'): + return result + + for i in os.listdir('/www/server/php'): + if os.path.isdir('/www/server/php/' + i): + php_slow = '/www/server/php/' + i + '/var/log/slow.log' + if os.path.exists(php_slow): + php_info = open(php_slow, 'r') + for j in php_info.readlines(): + if re.search(r'\[\d+-\w+-\d+.+', j): + time_str = re.findall(r'\[\d+-\w+-\d+\s+\d+:\d+:\d+\]', j) + time_str = time_str[0].replace('[', '').replace(']', '') + if self._is_today(time_str): + result += 1 + else: + break + + return result + + # 获取当天cc攻击数 + def _get_cc_attack_num(self, args): + zero_point = int(time.time()) - int(time.time() - time.timezone) % 86400 + log_path = '/www/server/btwaf/drop_ip.log' + if not os.path.exists(log_path): return 0 + + num = 100 + log_body = public.GetNumLines(log_path, num).split('\n') + while True: + if len(log_body) < num: + break + if json.loads(log_body[0])[0] < zero_point: + break + else: + num += 100 + log_body = public.GetNumLines(log_path, num).split('\n') + + num = 0 + for line in log_body: + try: + item = json.loads(line) + if item[0] > zero_point and item[-1] == 'cc': + num += 1 + except: continue + + return num + + # 获取当天攻击总数 + def _get_attack_num(self, args): + today = time.strftime('%Y-%m-%d', time.localtime()) + sites = self._get_site_list() + + count = 0 + for site in sites: + file_path = '/www/wwwlogs/btwaf/{0}_{1}.log'.format(site['name'], today) + count += self.__get_file_nums(file_path) + return count + + def get_exception(self, args): + data = {'mysql_slow': self._get_slow_log_nums(args), 'php_slow': self._php_count(args), + 'attack_num': self._get_attack_num(args), 'cc_attack_num': self._get_cc_attack_num(args)} + statuscode_distribute = self._statuscode_distribute(args) + data.update(statuscode_distribute) + return data + + def get_spider(self, args): + request_data = {} + sites = public.M('sites').field('name').order("addtime").select(); + for site_info in sites: + ts = None + conn = None + try: + site_name = site_info["name"] + start_date, end_date = self.get_time_interval(time.localtime()) + select_sql = "select time, spider from request_stat where time between {} and {}"\ + .format(start_date, end_date) + + db_path = os.path.join("/www/server/total/", "logs/{}/logs.db".format(site_name)) + if not os.path.isfile(db_path): continue + conn = sqlite3.connect(db_path) + ts = conn.cursor() + ts.execute(select_sql) + results = ts.fetchall() + + if type(results) == list: + for result in results: + time_key = str(result[0]) + hour = time_key[len(time_key)-2:] + value = result[1] + if hour not in request_data: + request_data[hour] = value + else: + request_data[hour] += value + except: + pass + finally: + if ts: + ts.close() + if conn: + conn.close() + return request_data + + # 获取蜘蛛数量分布 + def get_spider_old(self, args): + today = time.strftime('%Y-%m-%d', time.localtime()) + sites = self._get_site_list() + + data = {} + for site in sites: + site_name = site['name'] + file_name = '/www/server/total/total/' + site_name + '/spider/' + today + '.json' + if not os.path.exists(file_name): continue + day_data = self.__get_file_json(file_name) + for s_data in day_data.values(): + for s_key in s_data.keys(): + if s_key not in data: + data[s_key] = s_data[s_key] + else: + data[s_key] += s_data[s_key] + return data + + # 获取负载和上行流量 + def load_and_up_flow(self, args): + import psutil + + load_five = float(os.getloadavg()[1]) + cpu_count = psutil.cpu_count() + + up_flow = 0 + data = public.M('network').dbfile('system').field('up').order('id desc').limit("5").get() + if len(data) == 5: + up_flow = round(sum([item['up'] for item in data]) / 5, 2) + + return {'load_five': load_five, 'cpu_count': cpu_count, 'up_flow': up_flow} + + def get_time_interval(self, local_time): + start = None + end = None + time_key_format = "%Y%m%d00" + start = int(time.strftime(time_key_format, local_time)) + time_key_format = "%Y%m%d23" + end = int(time.strftime(time_key_format, local_time)) + return start, end + + def get_request_count_by_hour(self, args): + # 获取站点每小时的请求数据 + request_data = {} + import sqlite3 + sites = public.M('sites').field('name').order("addtime").select(); + for site_info in sites: + ts = None + conn = None + try: + site_name = site_info["name"] + start_date, end_date = self.get_time_interval(time.localtime()) + select_sql = "select time, req from request_stat where time between {} and {}"\ + .format(start_date, end_date) + db_path = os.path.join("/www/server/total/", "logs/{}/logs.db".format(site_name)) + if not os.path.isfile(db_path): continue + conn = sqlite3.connect(db_path) + ts = conn.cursor() + ts.execute(select_sql) + results = ts.fetchall() + if type(results) == list: + for result in results: + time_key = str(result[0]) + hour = time_key[len(time_key)-2:] + value = result[1] + if hour not in request_data: + request_data[hour] = value + else: + request_data[hour] += value + except: pass + finally: + if ts: + ts.close() + if conn: + conn.close() + return request_data + + # 取每小时的请求数 + def get_request_count_by_hour_old(self, args): + today = time.strftime('%Y-%m-%d', time.localtime()) + + request_data = {} + sites = self._get_site_list() + for site in sites: + path = '/www/server/total/total/' + site['name'] + '/request/' + today + '.json' + if os.path.exists(path): + spdata = self.__get_file_json(path) + for hour, value in spdata.items(): + count = value.get('GET', 0) + value.get('POST', 0) + if hour not in request_data: + request_data[hour] = count + else: + request_data[hour] = request_data[hour] + count + + return request_data + + # 取服务器的请求数 + def _get_request_count(self, args): + request_data = self.get_request_count_by_hour(args) + return sum(request_data.values()) + + # 获取瞬时请求数和qps + def get_request_count_qps(self, args): + from BTPanel import cache + + cache_timeout = 86400 + + old_total_request = cache.get('old_total_request') + otime = cache.get("old_get_time") + if not old_total_request or not otime: + otime = time.time() + old_total_request = self._get_request_count(args) + time.sleep(2) + ntime = time.time() + new_total_request = self._get_request_count(args) + + qps = float(new_total_request - old_total_request) / (ntime - otime) + + cache.set('old_total_request', new_total_request, cache_timeout) + cache.set('old_get_time', ntime, cache_timeout) + return {'qps': qps, 'request_count': new_total_request} diff --git a/class_v2/one_key_wp_v2.py b/class_v2/one_key_wp_v2.py new file mode 100644 index 00000000..43882be2 --- /dev/null +++ b/class_v2/one_key_wp_v2.py @@ -0,0 +1,1842 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: zhwen +# +------------------------------------------------------------------- +import os +import time +import json +import sys +import public +import re +import requests +from bs4 import BeautifulSoup +import panel_mysql_v2 as panelMysql +from public.validate import Param + +# import wp-toolkit core +# from wp_toolkit import wp_version, wpmgr, wpfastcgi_cache + + +def get_mem(): + import psutil + mem = psutil.virtual_memory() + memInfo = {'memTotal': int(mem.total / 1024 / 1024), 'memFree': int(mem.free / 1024 / 1024), + 'memBuffers': int(mem.buffers / 1024 / 1024), 'memCached': int(mem.cached / 1024 / 1024)} + return memInfo['memTotal'] + + +class one_key_wp: + wp_package_url = 'https://wordpress.org/latest.zip' + # wp_package_url = 'http://download.bt.cn/install/package/wordpress-5.9.1.zip' + base_path = "/www/server/panel/" + wp_session_path = '{}/data/wp_session'.format(base_path) + package_zip = '{}/package/wp.zip'.format(base_path) + md5_file = '{}/package/md5'.format(base_path) + log_path = '/tmp/schedule.log' + __php_tmp_file = '/tmp/wp_tmp'.format(base_path) + panel_db = "/www/server/panel/data/default.db" + timeout_count = 0 + old_time = 0 + __wp_session = None + __session_resp = None + __ajax_nonce = None + __domain = None + __plugin_page_content = None + wp_user = None + wp_passwd = None + + def __init__(self): + self.create_wp_table() + if not self.__wp_session: + self.__wp_session = requests.Session() + + import PluginLoader + self.__IS_PRO_MEMBER = PluginLoader.get_auth_state() > 0 + + def get_headers(self): + return { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36', + 'Host': self.__domain + } + + def get_plugin_page(self): + _get_ajax_nonce_url = "http://{}/wp-admin/plugins.php".format(self.__domain) + self.__plugin_page_content = self.__wp_session.get(_get_ajax_nonce_url, headers=self.get_headers(), + verify=False).text + + def get_wp_nonce(self, content, rep): + rex = re.search(rep, content) + if rex: + return rex.group(1) + + def get_nonce(self): + resp = self.__session_resp + if resp != '1': + resp = resp.text + else: + self.get_plugin_page() + resp = self.__plugin_page_content + _ajax_nonce_rep = '"ajax_nonce\\\"\\:\\\"(\\w+)' + rex = re.search(_ajax_nonce_rep, resp) + if rex: + self.__ajax_nonce = rex.group(1) + + def __load_cookies(self, s_id): + self.__domain = self.get_wp_auth(s_id) + f = '{}/{}'.format(self.wp_session_path, self.__domain) + c = public.readFile(f) + if c: + mtime = os.path.getmtime(f) + expried = 86400 + if time.time() - mtime > expried: + print('cookies expired, re-login wordpress') + self.__login_wp() + else: + print('use local session') + self.__wp_session.cookies.update(json.loads(c)) + self.__session_resp = '1' + else: + print('No local cookies, login wordpress') + self.__login_wp() + # self.__login_wp() + # print('login success!') + try: + self.get_nonce() + except: + pass + if not self.__ajax_nonce: + self.__login_wp() + self.get_nonce() + self.get_plugin_page() + + def __save_cookies(self): + if not os.path.exists(self.wp_session_path): + os.mkdir(self.wp_session_path) + f = '{}/{}'.format(self.wp_session_path, self.__domain) + return public.writeFile(f, json.dumps(self.__wp_session.cookies.get_dict())) + + def __login_wp(self): + + wp_login = 'http://{}/wp-login.php'.format(self.__domain) + headers1 = {'Cookie': 'wordpress_test_cookie=WP Cookie check', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36', + # 'referer':"https://{}/wp-login.php?loggedout=true&wp_lang=en_US".format(self.__domain), + 'Host': self.__domain + } + datas = { + 'log': self.wp_user, + 'pwd': self.wp_passwd, + 'wp-submit': 'Log In', + # 'redirect_to': 'https://{}/wp-admin'.format(self.__domain), + 'testcookie': '1' + } + # hosts = public.readFile('/etc/hosts') + # if not hosts: + # return False + # if not re.search(r'127.0.0.1\s+{}'.format(self.__domain),hosts): + # public.writeFile('/etc/hosts','\n127.0.0.1 {}'.format(self.__domain),'a+') + + self.__session_resp = retry( + lambda: self.__wp_session.post(wp_login, headers=headers1, data=datas, verify=False)) + + # cookies持久化 + self.__save_cookies() + + # 写输出日志 + def write_logs(self, log_msg, clean=None): + if clean: + fp = open(self.log_path, 'w') + fp.write(log_msg) + fp.close() + fp = open(self.log_path, 'a+') + fp.write(log_msg + '\n') + fp.close() + + # 下载文件 + def download_file(self): + try: + path = os.path.dirname(self.package_zip) + if not os.path.exists(path): os.makedirs(path) + import urllib, socket, ssl + ssl._create_default_https_context = ssl._create_unverified_context + socket.setdefaulttimeout(10) + self.pre = 0 + self.old_time = time.time() + print("Download the installation package: {} --> {}".format(self.wp_package_url, self.package_zip)) + self.write_logs( + "|-Download the installation package: {} --> {}".format(self.wp_package_url, self.package_zip)) + if sys.version_info[0] == 2: + urllib.urlretrieve(self.wp_package_url, filename=self.package_zip, reporthook=self.download_hook) + else: + urllib.request.urlretrieve(self.wp_package_url, filename=self.package_zip, + reporthook=self.download_hook) + md5str = public.FileMd5(self.package_zip) + public.writeFile(self.md5_file, str(md5str)) + except: + print("Download error: {}".format(public.get_error_info())) + if self.timeout_count > 5: return; + self.timeout_count += 1 + time.sleep(5) + self.download_file() + + # 下载文件进度回调 + def download_hook(self, count, blockSize, totalSize): + used = count * blockSize + pre1 = int((100.0 * used / totalSize)) + if self.pre != pre1: + dspeed = used / (time.time() - self.old_time) + self.pre = pre1 + + def check_package(self): + # 检查本地包 + download = False + md5str = None + if os.path.exists(self.package_zip): + md5str = public.FileMd5(self.package_zip) + if os.path.exists(self.md5_file) and md5str: + if md5str != public.readFile(self.md5_file): + download = True + else: + download = True + return download + + def download_latest_package(self): + print("Start downloading the installation package...") + self.write_logs("|-Start downloading the installation package...") + + if os.path.exists(self.package_zip): + # 获取文件的最后修改时间 + modified_time = os.path.getmtime(self.package_zip) + # 计算当前时间与最后修改时间的差值 + time_diff = time.time() - modified_time + time2 = 30 * 24 * 60 * 60 + + if time_diff > time2: # 如果超过30天,则删除文件 + os.remove(self.package_zip) + os.remove(self.md5_file) + self.write_logs("|-Del package...") + + if not self.check_package(): + print("|-MD5 consistent, no need to download...") + return public.return_msg_gettext(True, 'MD5 consistent!') + self.download_file() + if not os.path.exists(self.package_zip): + self.write_logs("|-Download failed...") + print("Download failed...") + return public.return_msg_gettext(False, 'File download failed!') + return public.return_msg_gettext(True, "Download successfully") + + def unzip_package(self, site_path): + print("Start unzipping the installation package...") + self.write_logs("|-Start unzipping the installation package...") + public.ExecShell('unzip -o {} -d {}/'.format(self.package_zip, site_path)) + public.ExecShell('mv {}/wordpress/* {}'.format(site_path, site_path)) + os.removedirs("{}/wordpress/".format(site_path)) + print("Start setting up site permissions...") + self.write_logs("|-Start setting up site permissions...") + self.set_permission(site_path) + + def set_permission(self, site_path): + os.system('chmod -R 755 ' + site_path) + os.system('chown -R www.www ' + site_path) + + def set_urlrewrite(self, site_name, site_path): + webserver = public.get_webserver() + + if webserver == 'openlitespeed': + webserver = 'apache' + + swfile = '/www/server/panel/rewrite/{}/wordpress.conf'.format(webserver) + if os.path.exists(swfile): + rewriteConf = public.readFile(swfile) + + if webserver == 'nginx': + dwfile = '{}/vhost/rewrite/{}.conf'.format(self.base_path, site_name) + + else: + dwfile = '{}/.htaccess'.format(site_path) + + public.writeFile(dwfile, rewriteConf) + + + def __write_db(self, s_id, d_id, prefix, user_name, admin_password): + print("Inserting data...") + pdata = {"s_id": s_id, "d_id": d_id, "prefix": prefix, "user": user_name, "pass": admin_password} + public.M('wordpress_onekey').where('s_id=?', (s_id,)).field('s_id').find() + if public.M('wordpress_onekey').where('s_id=?', (s_id,)).field('s_id').find(): + print("Data already exists, update data...") + public.M('wordpress_onekey').where('s_id=?', (s_id,)).update(pdata) + return + print("Insert data...") + print("Insert data:{}".format(pdata)) + print("Result:{}".format(public.M('wordpress_onekey').insert(pdata))) + + def create_wp_table(self): + if not public.M('sqlite_master').where('type=? AND name=?', ('table', 'wordpress_onekey')).count(): + public.M('').execute('''CREATE TABLE "wordpress_onekey" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "s_id" INTEGER DEFAULT '', + "d_id" INTEGER DEFAULT '', + "prefix" TEXT DEFAULT '', + "user" TEXT DEFAULT '', + "pass" TEXT DEFAULT '');''') + + def init_wp(self, values): + hosts = public.readFile('/etc/hosts') + if not hosts: + return False + if not re.search(r'127.0.0.1\s+{}'.format(values['site_name']), hosts): + public.writeFile('/etc/hosts', '\n127.0.0.1 {}'.format(values['site_name']), 'a+') + + self.write_logs("|-Start initializing Wordpress...") + + if self.__IS_PRO_MEMBER: + from wp_toolkit import wpmgr + + # 初始化WP管理类 + wpmgr_obj = wpmgr(values['s_id']) + + # 初始化WP网站配置 + self.write_logs("|-Start setup configurations...") + ok, msg = wpmgr_obj.setup_config(values['dbname'], values['db_user'], values['db_pwd'], 'localhost', values['prefix']) + self.write_logs('|-Setup config >>> {} {}'.format('OK' if ok else 'FAIL', msg)) + + if not ok: + raise Exception(msg) + + # 初始化WP网站信息 + self.write_logs("|-Start installations...") + ok, msg = wpmgr_obj.wp_install(values['weblog_title'], values['user_name'], values['admin_email'], values['admin_password'], values['language']) + self.write_logs('|-Installation {} {}'.format('OK' if ok else 'FAIL', msg)) + + if not ok: + raise Exception(msg) + else: + self.request_setup_0(values) + time.sleep(1) + if not self.request_setup_2(values): + return public.return_msg_gettext(False, + "The database connection is abnormal. Please check whether the root user authority or database configuration parameters are correct.") + time.sleep(1) + self.request_setup_3(values) + time.sleep(1) + self.request_setup_4(values) + time.sleep(1) + + # 配置伪静态规则 + self.set_urlrewrite(values['site_name'], values['site_path']) + + def request_setup_0(self, values): + self.write_logs("|-Start initializing Wordpress language...") + url = "http://{}/wp-admin/setup-config.php?step=0".format(values['site_name']) + param = { + "url": url, + "data": {"language": values['language']}, + "headers": {"Host": values['domain']} + } + # result = self.request_wp_api(param) + response = retry(lambda: self.request_wp_api(param)) + # public.print_log("开始检查--request_setup_0_result:{}".format(response)) + + def request_setup_2(self, values): + self.write_logs("|-Start initializing Wordpress config...") + url = "http://{}/wp-admin/setup-config.php?step=2".format(values['site_name']) + param = { + "url": url, + "headers": {"Host": values['domain']}, + "data": { + "dbname": values['dbname'], + "uname": values['db_user'], + "pwd": values['db_pwd'], + "dbhost": "localhost", + "prefix": values['prefix'], + "language": values['language'], + "submit": "Submit", + } + } + # result = self.request_wp_api(param) + response = retry(lambda: self.request_wp_api(param)) + if "install.php?language=ja": + return True + + def request_setup_3(self, values): + self.write_logs("|-Start initializing Wordpress install language...") + url = "http://{}/wp-admin/install.php?language={}".format(values['site_name'], values['language']) + param = { + "url": url, + "headers": {"Host": values['domain']}, + "data": { + "language": values['language'] + } + } + # self.request_wp_api(param) + response = retry(lambda: self.request_wp_api(param)) + + def request_setup_4(self, values): + self.write_logs("|-Start installing the wordpress program...") + url = "http://{}/wp-admin/install.php?step=2".format(values['site_name']) + param = { + "url": url, + "headers": {"Host": values['domain']}, + "data": { + "weblog_title": values['weblog_title'], + "user_name": values['user_name'], + "admin_password": values['admin_password'], + "admin_password2": values['admin_password'], + # "pw_weak": values['pw_weak'], # on/off + "admin_email": values['admin_email'], + # "Submit": "Install WordPress", + "language": values['language'] + } + } + # self.request_wp_api(param) + response = retry(lambda: self.request_wp_api(param)) + + def request_wp_api(self, param): + """需要指定域名得host为本机IP""" + resp = requests.post(param['url'], data=param['data'], headers=param['headers']) + # public.print_log("开始检查--request_wp_api:{}".format(resp)) + # public.print_log("开始检查--request_wp_api_resp.text:{}".format(resp.text)) + return resp.text + + def get_update_wp_nonce(self): + url = "http://{}/wp-admin/update-core.php".format(self.__domain) + # res = self.action_plugin_get(url) + _wp_nonce_rep = '"_wpnonce\\\"\\svalue=\\\"(\\w+)' + # rex = re.search(_wp_nonce_rep,res) + # if rex: + # self.__wp_nonce = rex.group(1) + + # return self.get_wp_nonce(self.action_plugin_get(url),_wp_nonce_rep) + response = retry(lambda: self.get_wp_nonce(self.action_plugin_get(url), _wp_nonce_rep)) + return response + + def action_plugin_post(self, param): + headers = self.get_headers() + if 'upgrade' in param['data']: + param['data']['_wpnonce'] = self.get_update_wp_nonce() + headers['referer'] = 'http://{}/wp-admin/update-core.php'.format(param['domain']) + + response = retry( + lambda: self.__wp_session.post(param['url'], data=param['data'], headers=headers, verify=False).text) + return response + # return self.__wp_session.post(param['url'],data=param['data'], headers=headers, verify=False).text + + def action_plugin_get(self, url, headers=None): + if not headers: + headers = self.get_headers() + # headers['Referer'] = "http://{}/wp-admin".format(self.__domain) + + response = retry(lambda: self.__wp_session.get(url, headers=headers, verify=False).text) + return response + # return self.__wp_session.get(url, headers=headers, verify=False).text + + def install_plugin(self, values): + self.write_logs("|-Start installing the [nginx-helper] plugin...") + self.__load_cookies(values['s_id']) + param = { + "url": "http://{}/wp-admin/admin-ajax.php".format(self.__domain), + "domain": values['domain'], + "data": { + "slug": values['slug'], # 插件名称 nginx-helper + "action": values['wp_action'], # action已经被面板占用 install-plugin + "_ajax_nonce": self.__ajax_nonce, + "_fs_nonce": "", + "username": "", + "password": "", + "connection_type": "", + "public_key": "", + "private_key": "" + } + } + res = self.action_plugin_post(param) + return res + + def get_smart_http_expire_form_nonce(self, url): + public.writeFile('/tmp/2', str(self.action_plugin_get(url))) + smart_http_expire_form_nonce = '"smart_http_expire_form_nonce\\\"\\svalue=\\\"(\\w+)' + return self.get_wp_nonce(self.action_plugin_get(url), smart_http_expire_form_nonce) + + def set_nginx_helper(self, values): + self.write_logs("|-Setting up [nginx-helper] plugin cache rules...") + self.__load_cookies(values['s_id']) + url = "http://{}/wp-admin/options-general.php?page=nginx".format(self.__domain) + param = { + "url": url, + "domain": values['domain'], + "data": { + "enable_purge": "1", + "is_submit": "1", + "cache_method": "enable_fastcgi", + "purge_method": "unlink_files", + "redis_hostname": "127.0.0.1", + "redis_port": "6379", + "redis_prefix": "nginx-cache", + "purge_homepage_on_edit": "1", + "purge_homepage_on_del": "1", + "purge_page_on_mod": "1", + "purge_page_on_new_comment": "1", + "purge_page_on_deleted_comment": "1", + "purge_archive_on_edit": "1", + "purge_archive_on_del": "1", + "purge_archive_on_new_comment": "1", + "purge_archive_on_deleted_comment": "1", + "purge_url": "", + "log_level": "INFO", + "log_filesize": "5", + "smart_http_expire_form_nonce": self.get_smart_http_expire_form_nonce(url), + "smart_http_expire_save": "Save All Changes" + } + } + return self.action_plugin_post(param) + + def act_nginx_helper_active(self, values): + self.write_logs("|-activating [nginx-helper] plugin...") + self.__load_cookies(values['s_id']) + values['active_id'] = "activate-nginx-helper" + self.get_plugin_page() + res = self.get_plugin_url(values['active_id'], self.__plugin_page_content) + url = 'http://{}/wp-admin/{data}'.format(self.__domain, + data=str(str(res[0]).split('"')[5])) + url = url.replace('amp;', '') + return self.action_plugin_get(url) + + def get_plugin_url(self, active_id, content): + # soup = BeautifulSoup(content) + soup = BeautifulSoup(content, features="html.parser") + res = soup.find_all(id=active_id) + return res + + def generate_wp_passwd(self, site_path, new_pass): + hash_password_code = public.readFile("{}/wp-includes/class-phpass.php".format(site_path)) + extra_code = """ + $passwordValue = "%s"; + $wp_hasher = new PasswordHash(8, TRUE); + $sigPassword = $wp_hasher->HashPassword($passwordValue); + $data = $wp_hasher->CheckPassword($passwordValue,$sigPassword); + if($data){ + echo 'True|'.$sigPassword;; + }else{ + echo 'False|'.$sigPassword;; + } + """ % new_pass + php_code = hash_password_code + extra_code + public.writeFile(self.__php_tmp_file, php_code) + a, e = public.ExecShell("php -f {}".format(self.__php_tmp_file)) + os.remove(self.__php_tmp_file) + res = a.split('|') + if res[0] == "True": + return public.return_message(0,0, res[1]) + return public.return_message(-1,0, 'Generated password detection failed!') + + def get_cache_status(self, s_id): + """ + s_id 网站id + """ + import data + site_info = public.M('sites').where('id=?', (s_id,)).field('name').find() + + if not isinstance(site_info, dict): + return False + + site_name = site_info['name'] + + # 获取WP站点绑定的PHP可执行文件 + from public import websitemgr + + php_v = websitemgr.get_site_php_version(site_name).replace('.', '') + + return fast_cgi().get_fast_cgi_status(site_name, php_v) + + ##############################对外接口—BEGIN############################## + def get_wp_username(self, get): + """ + s_id 网站ID + """ + # 校验参数 + try: + get.validate([ + Param('s_id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + values = self.check_param(get) + if values['status']==-1: + return values + values=values['message'] + db_info = public.M('wordpress_onekey').where('s_id=?', (values['s_id'],)).find() + db_name = public.M('databases').where('id=?', (db_info['d_id'],)).field('name').find() + if "name" not in db_name: + return public.return_message(-1,0, "The database of this wordpress was not found, this may be caused by the fact that you have manually deleted the database") + db_name = db_name['name'] + mysql_obj = panelMysql.panelMysql() + res = mysql_obj.query('select * from {}.{}users'.format( + db_name, db_info['prefix'])) + if hasattr(res, '__iter__'): + return public.return_message(0,0, [i[1] for i in res]) + else: + return public.return_message(-1,0, "Site database [{}] failed to query users, try setting the database for this site. Error: {}".format( + db_name, res)) + + def reset_wp_password(self, get): + """ + 重置wordpress用户密码 + s_id 网站ID + user 要重置的用户名 + new_pass + """ + # 校验参数 + try: + get.validate([ + Param('user').String(), + Param('new_pass').String(), + Param('s_id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + values = self.check_param(get) + if values['status']==-1: + return values + values=values['message'] + s_id = values['s_id'] + db_info = public.M('wordpress_onekey').where('s_id=?', (s_id,)).find() + db_name = public.M('databases').where('id=?', (db_info['d_id'],)).field('name').find()['name'] + path = public.M('sites').where('id=?', (s_id,)).field('path').find()['path'] + new_pass = values['new_pass'] + passwd = self.generate_wp_passwd(path, new_pass) + if passwd['status']==-1: + return passwd + + passwd = passwd['message'] + + if isinstance(passwd, dict): + passwd = passwd.get('result', None) + + if passwd is None: + return public.fail_v2('Reset password failed') + + mysql_obj = panelMysql.panelMysql() + sql = 'update {}.{}users set user_pass = "{}" where user_login = "{}"'.format( + db_name, db_info['prefix'], passwd, values['user']) + mysql_obj.execute(sql) + return public.return_message(0,0, "Password reset successful") + + # 获取WP可用版本列表 + def get_wp_available_versions(self, args: public.dict_obj): + # 校验参数 + try: + args.validate([ + Param('php_version_short').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + from wp_toolkit import wp_version + + versions = list(map(lambda x: { + 'locale': x['locale'], + 'version': x['version'], + 'php_version': x['php_version'], + 'mysql_version': x['mysql_version'], + }, wp_version().latest_versions())) + + if 'php_version_short' in args: + versions = list(filter(lambda x: int(args.php_version_short) >= int(''.join(str(x['php_version']).split('.')[:2])), versions)) + + return public.success_v2(versions) + + # 获取WP网站本地版本号 + def get_wp_version(self, s_id): + """获取wordpress本地版本 + s_id 网站id + """ + path = public.M('sites').where('id=?', (s_id,)).field('path').find()['path'] + conf_file = "{}/wp-includes/version.php".format(path) + conf = public.readFile(conf_file) + try: + version = re.search('\\$wp_version\\s*=\\s*[\'\"]{1}([\\d\\.]*)[\'\"]{1}', conf).groups(1)[0] + except: + version = "00" + return version + + # 获取WP已发布的最新版本号 + def get_wp_version_online(self): + """获取wordpress线上版本""" + url = "http://wordpress.org/download/" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36' + } + result = requests.get(url, headers=headers) + result = re.search(r'Download\s+WordPress\s+([\d\.]+)', result.text) + if result: + return result.group(1) + return "00" + + # 检查WP是否有新版本可以更新 + def is_update(self, get): + """ + s_id 网站ID + """ + values = self.check_param(get) + if values['status']==-1: + return values + values=values['message'] + online_v = self.get_wp_version_online() + local_v = self.get_wp_version(values['s_id']) + update = False + if str(online_v) != str(local_v): + update = True + data = { + "online_v": online_v, + "local_v": local_v, + "update": update + } + return_message=public.return_msg_gettext(True, data) + del return_message['status'] + return public.return_message(0,0, return_message['msg']) + + def purge_all_cache(self, get): + """ + 清理所有缓存 + """ + # 校验参数 + try: + get.validate([ + Param('s_id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if public.get_webserver() != "nginx": + return public.return_message(-1,0, "This feature currently only supports Nginx") + + if self.__IS_PRO_MEMBER: + from wp_toolkit import wpmgr + wpmgr(get.s_id).purge_cache_with_nginx_helper() + else: + cache_dir = "/dev/shm/nginx-cache/wp" + public.ExecShell("rm -rf {}/*".format(cache_dir)) + + return public.return_message(0,0,"Cleaned up successfully!") + + def set_fastcgi_cache(self, get): + """ + 设置缓存 + version php版本 + sitename 完整名 + act disable/enable 开启关闭缓存 + """ + # 校验参数 + try: + get.validate([ + Param('version').String(), + Param('sitename').String(), + Param('act').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + if public.get_webserver() != "nginx": + return public.return_message(-1,0, "This feature currently only supports Nginx") + values = self.check_param(get) + if values['status']==-1: + return values + values=values['message'] + if '.' in values['version']: + values['version'] = values['version'].replace('.', '') + + if self.__IS_PRO_MEMBER: + from wp_toolkit import wpfastcgi_cache + + ok, msg = wpfastcgi_cache().set_website_conf(values['version'], values['sitename'], values['act'], immediate=True) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + else: + return fast_cgi().set_website_conf(values['version'], values['sitename'], values['act']) + + # 使用网站ID查询WP站点域名 + def get_wp_auth(self, s_id): + domain = public.M('domain').where("pid=?", (s_id,)).field('name').find()['name'] + info = public.M('wordpress_onekey').where("s_id=?", (s_id,)).find() + self.wp_user = info['user'] + self.wp_passwd = info['pass'] + return domain + + # 更新WP版本 + def update_wp(self, args): + """更新wordpress版本 + s_id 网站ID + version 需要更新的版本 + """ + if self.__IS_PRO_MEMBER: + from wp_toolkit import wpmgr + + # 校验参数 + try: + args.validate([ + Param('s_id').Require().Integer('>', 0), + Param('version').Require().Regexp(r'^\d+(?:\.\d+)+$'), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + ok, msg = wpmgr(args.s_id).update_version(args.version) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2('Upgrade to {}'.format(msg)) + else: + self.__load_cookies(args.s_id) + param = { + "url": "http://{}/wp-admin/update-core.php?action=do-core-upgrade".format(self.__domain), + "domain": self.__domain, + "data": { + # "_wpnonce":get._wpnonce, + "_wp_http_referer": '/wp-admin/update-core.php', + "version": args.version, + "locale": "en_US", + "upgrade": "Update to version {}".format(args.version) + } + } + self.action_plugin_post(param) + return_message=public.return_msg_gettext(True, 'Updated successfully!') + del return_message['status'] + return public.return_message(0,0, return_message['msg']) + + # 获取可用的语言列表 + def get_language(self, get=None): + language = { + "en": "English (United States)", + "af": "Afrikaans", + "am": "አማርኛ", + "ar": "العربية", + "ary": "العربية المغربية", + "as": "অসমীয়া", + "az": "Azərbaycan dili", + "azb": "گؤنئی آذربایجان", + "bel": "Беларуская мова", + "bg_BG": "Български", + "bn_BD": "বাংলা", + "bo": "བོད་ཡིག", + "bs_BA": "Bosanski", + "ca": "Català", + "ceb": "Cebuano", + "cs_CZ": "Čeština", + "cy": "Cymraeg", + "da_DK": "Dansk", + "de_CH_informal": "Deutsch (Schweiz, Du)", + "de_DE": "Deutsch", + "de_DE_formal": "Deutsch (Sie)", + "de_CH": "Deutsch (Schweiz)", + "de_AT": "Deutsch (Österreich)", + "dsb": "Dolnoserbšćina", + "dzo": "རྫོང་ཁ", + "el": "Ελληνικά", + "en_US": "English (United States)", + "en_NZ": "English (New Zealand)", + "en_AU": "English (Australia)", + "en_CA": "English (Canada)", + "en_GB": "English (UK)", + "en_ZA": "English (South Africa)", + "eo": "Esperanto", + "es_ES": "Español", + "es_EC": "Español de Ecuador", + "es_CO": "Español de Colombia", + "es_AR": "Español de Argentina", + "es_DO": "Español de República Dominicana", + "es_PE": "Español de Perú", + "es_CR": "Español de Costa Rica", + "es_UY": "Español de Uruguay", + "es_CL": "Español de Chile", + "es_PR": "Español de Puerto Rico", + "es_VE": "Español de Venezuela", + "es_GT": "Español de Guatemala", + "es_MX": "Español de México", + "et": "Eesti", + "eu": "Euskara", + "fa_IR": "فارسی", + "fa_AF": "(فارسی (افغانستان", + "fi": "Suomi", + "fr_FR": "Français", + "fr_BE": "Français de Belgique", + "fr_CA": "Français du Canada", + "fur": "Friulian", + "gd": "Gàidhlig", + "gl_ES": "Galego", + "gu": "ગુજરાતી", + "haz": "هزاره گی", + "he_IL": "עִבְרִית", + "hi_IN": "हिन्दी", + "hr": "Hrvatski", + "hsb": "Hornjoserbšćina", + "hu_HU": "Magyar", + "hy": "Հայերեն", + "id_ID": "Bahasa Indonesia", + "is_IS": "Íslenska", + "it_IT": "Italiano", + "ja": "日本語", + "jv_ID": "Basa Jawa", + "ka_GE": "ქართული", + "kab": "Taqbaylit", + "kk": "Қазақ тілі", + "km": "ភាសាខ្មែរ", + "kn": "ಕನ್ನಡ", + "ko_KR": "한국어", + "ckb": "كوردی‎", + "lo": "ພາສາລາວ", + "lt_LT": "Lietuvių kalba", + "lv": "Latviešu valoda", + "mk_MK": "Македонски јазик", + "ml_IN": "മലയാളം", + "mn": "Монгол", + "mr": "मराठी", + "ms_MY": "Bahasa Melayu", + "my_MM": "ဗမာစာ", + "nb_NO": "Norsk bokmål", + "ne_NP": "नेपाली", + "nl_NL_formal": "Nederlands (Formeel)", + "nl_NL": "Nederlands", + "nl_BE": "Nederlands (België)", + "nn_NO": "Norsk nynorsk", + "oci": "Occitan", + "pa_IN": "ਪੰਜਾਬੀ", + "pl_PL": "Polski", + "ps": "پښتو", + "pt_BR": "Português do Brasil", + "pt_AO": "Português de Angola", + "pt_PT_ao90": "Português (AO90)", + "pt_PT": "Português", + "rhg": "Ruáinga", + "ro_RO": "Română", + "ru_RU": "Русский", + "sah": "Сахалыы", + "snd": "سنڌي", + "si_LK": "සිංහල", + "sk_SK": "Slovenčina", + "skr": "سرائیکی", + "sl_SI": "Slovenščina", + "sq": "Shqip", + "sr_RS": "Српски језик", + "sv_SE": "Svenska", + "sw": "Kiswahili", + "szl": "Ślōnskŏ gŏdka", + "ta_IN": "தமிழ்", + "ta_LK": "தமிழ்", + "te": "తెలుగు", + "th": "ไทย", + "tl": "Tagalog", + "tr_TR": "Türkçe", + "tt_RU": "Татар теле", + "tah": "Reo Tahiti", + "ug_CN": "ئۇيغۇرچە", + "uk": "Українська", + "ur": "اردو", + "uz_UZ": "O‘zbekcha", + "vi": "Tiếng Việt", + "zh_TW": "繁體中文", + "zh_HK": "香港中文版 ", + "zh_CN": "简体中文", + } + return_message=public.return_msg_gettext(True, language) + del return_message['status'] + return public.return_message(0,0, return_message['msg']) + + # 请求参数校验 + def check_param(self, args): + """ + @name 检测传入参数 + @author zhwen<2022-03-10> + """ + # 检查email格式 + rep_email = r"[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?" + # 检查域名格式 + rep_domain = r"^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" + values = {} + if hasattr(args, 'd_id'): + if re.search(r'\d+', args.d_id): + values["d_id"] = args.d_id + else: + return public.return_message(-1,0, "Please check if the [{}] format is correct For example: {}".format("d_id", "99")) + if hasattr(args, 's_id'): + if re.search(r'\d+', args.s_id): + values["s_id"] = args.s_id + else: + return public.return_message(-1,0, "Please check if the [{}] format is correct For example: {}".format("s_id", "99")) + if hasattr(args, 'language'): + if args.language in self.get_language()['message']: + values['language'] = args.language + else: + return public.return_message(-1,0, "Please check if the [{}] format is correct For example: {}".format("language", "en")) + if hasattr(args, 'domain'): + if re.search(rep_domain, args.domain): + values['domain'] = public.xssencode2(args.domain) + else: + return public.return_message(-1,0, "Please check if the [{}] format is correct For example: {}".format("domain", "aapanel.com")) + if hasattr(args, 'weblog_title'): + values['weblog_title'] = public.xssencode2(args.weblog_title) + if hasattr(args, 'user_name'): + values['user_name'] = public.xssencode2(args.user_name) + if hasattr(args, 'admin_password'): + values['admin_password'] = public.xssencode2(args.admin_password) + if hasattr(args, 'pw_weak'): + if args.pw_weak in ['on', 'off']: + values['pw_weak'] = args.pw_weak + else: + return public.return_message(-1,0, "Please check if the [{}] format is correct For example: {}".format("pw_weak", "on/off")) + if hasattr(args, 'admin_email'): + if re.search(rep_email, args.admin_email): + values['admin_email'] = public.xssencode2(args.admin_email) + else: + return public.return_message(-1,0, "Please check if the [{}] format is correct For example: {}".format("admin_email", "adimn@aapanel.com")) + if hasattr(args, 'prefix'): + values['prefix'] = public.xssencode2(args.prefix) + if hasattr(args, 'php_version'): + values['php_version'] = public.xssencode2(args.php_version) + if hasattr(args, 'enable_cache'): + values['enable_cache'] = public.xssencode2(args.enable_cache) + if hasattr(args, 'wp_action'): + values['slug'] = public.xssencode2(args.slug) + if hasattr(args, 'slug'): + values['wp_action'] = public.xssencode2(args.wp_action) + if hasattr(args, 'active_id'): + values['active_id'] = public.xssencode2(args.active_id) + if hasattr(args, 'new_pass'): + values['new_pass'] = public.xssencode2(args.new_pass) + if hasattr(args, 'user'): + values['user'] = public.xssencode2(args.user) + if hasattr(args, 'sitename'): + values['sitename'] = public.xssencode2(args.sitename) + if hasattr(args, 'act'): + values['act'] = public.xssencode2(args.act) + if hasattr(args, 'version'): + values['version'] = public.xssencode2(args.version) + return public.return_message(0,0, values) + + # 删除网站 + def del_site(self, get): + import panelSite + p = panelSite.panelSite() + site_info = public.M('sites').where('id=?', (get.s_id,)).find() + get.id = get.s_id + get.webname = site_info['name'] + get.ftp = "1" + get.database = "1" + get.path = "1" + p.DeleteSite(get) + + # 安装WP + def deploy_wp(self, get): + """ + d_id 数据据库ID + s_id 网站ID + language 部署wordpress后的语言 + weblog_title wordpress博客名 + user_name wordpress后台用户名 + admin_password 管理员密码 + admin_password2 + pw_weak 允许弱密码 + admin_email 管理员邮箱 + prefix wordpress数据表前缀 + php_version php版本 + enable_cache 开启缓存 + package_version Wordpress版本 + @name 部署wordpress + @author zhwen<2022-03-10> + """ + # 校验参数 + try: + get.validate([ + Param('domain').String().Host(), + Param('weblog_title').String(), + Param('language').String(), + Param('php_version').String(), + Param('user_name').String(), + Param('admin_email').String().Email(), + Param('prefix').String(), + Param('pw_weak').String(), + Param('admin_password').String(), + Param('enable_cache').Integer(), + Param('d_id').Integer(), + Param('s_id').Integer(), + Param('package_version').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + self.write_logs('', clean=True) + values = self.check_param(get) + if values['status']==-1: + # 删除自动创建的空白网站 + self.del_site(get) + return values + values=values['message'] + s_id = values['s_id'] # 网站ID + d_id = values['d_id'] # 数据库ID + prefix = values['prefix'] # 前缀 + site_info = public.M('sites').where('id=?', (s_id,)).find() + print("Get site info:\n ID: {}\npath: {}\n".format(s_id, site_info['path'])) + self.write_logs(""" + |-Get website information: + ID: {} + Path: {} + + """.format(s_id, site_info['path'])) + db_info = public.M('databases').where('id=?', (d_id,)).find() + + print("Get database information: \nID: {}\nDBName: {}\nUser: {}\nPassWD: {}".format( + d_id, db_info["name"], db_info["username"], db_info["password"])) + self.write_logs(""" + |-Get database information: + ID: {} + DBName: {} + User: {} + PassWD: {} + + """.format(d_id, db_info["name"], db_info["username"], db_info["password"])) + values['dbname'] = db_info["name"] + self.wp_user = values['user_name'] + self.wp_passwd = values['admin_password'] + values['db_user'] = db_info["username"] + values['db_pwd'] = db_info["password"] + values['site_path'] = site_info['path'] + values['site_name'] = site_info['name'] + + # 开始下载安装包 + if self.__IS_PRO_MEMBER: + from wp_toolkit import wp_version + + wp_version_obj = wp_version() + + if 'package_version' not in get or get.package_version is None: + get.package_version = wp_version_obj.latest_version()['version'] + + self.write_logs('|-Package version: {}'.format(get.package_version)) + + # 下载特定版本的安装包 + self.package_zip = wp_version_obj.download_package(get.package_version) + else: + # 下载最新版本的安装包 + self.download_latest_package() + + self.unzip_package(site_info['path']) + + # 优化PHP + res = optimize_php().optimize_php(get) + if not res['status']: + return public.return_message(-1,0,res) + + # 初始化wp + self.init_wp(values) + self.__write_db(s_id, d_id, prefix, get.user_name, get.admin_password) + + # 优化mysql + optimize_db().self_db_cache(get) + + # 设置fastcgi缓存 + if int(get.enable_cache) == 1 and public.get_webserver() == 'nginx': + if self.__IS_PRO_MEMBER: + from wp_toolkit import wpmgr, wpfastcgi_cache + + # 配置Nginx-fastcgi-cache + wpfastcgi_cache().set_fastcgi(values['site_path'], values['site_name'], values['php_version']) + + self.write_logs('|-WP Plugin nginx-helper installing...') + wpmgr(s_id).init_plugin_nginx_helper() + self.write_logs('|-WP Plugin nginx-helper installation succeeded') + else: + # 安装nginxHelper + # slug nginx-helper + values['slug'] = "nginx-helper" + values['wp_action'] = "install-plugin" + self.install_plugin(values) + self.act_nginx_helper_active(values) + + # 安装并启用nginx-helper插件 + self.set_nginx_helper(get) + + # 设置登录入口保护 + if int(get.get('enable_whl', 0)) == 1: + if self.__IS_PRO_MEMBER: + from wp_toolkit import wpmgr + + # 安装并启用wps-hide-login插件 + self.write_logs('|-WP Plugin wps-hide-login installing...') + wpmgr(s_id).init_plugin_wps_hide_login(get.get('whl_page', 'login'), get.get('whl_redirect_admin', '404')) + self.write_logs('|-WP Plugin wps-hide-login installation succeeded') + + public.ServiceReload() + + self.write_logs("\n\n\n|-Deployment was successful!") + + return public.return_message(0,0, "Deployment was successful!") + except: + self.del_site(get) + from traceback import format_exc + public.print_log(format_exc()) + return public.return_message(-1,0, "Deployment failed!") + + # 重新关联WP网站数据库 + def reset_wp_db(self, args): + """ + :param args db_name 数据库名 + :param args site_id 网站ID + """ + db_name = public.xssencode2(args.db_name) + try: + site_id = int(args.site_id) + except: + return public.return_message(-1,0, "Site ID must be numeric") + db_info = public.M("databases").where("name=?", (db_name,)).field('id').find() + if 'id' not in db_info: + return public.return_message(-1,0, "This database was not found!") + pdata = { + "d_id": db_info['id'] + } + public.M('wordpress_onekey').where("s_id=?", (site_id,)).update(pdata) + return public.return_message(0,0, "Setup successfully!") + + # 获取WP Toolkit配置信息 + def get_wp_configurations(self, args: public.dict_obj): + # 校验参数 + try: + args.validate([ + Param('s_id').Require().Integer('>', 0), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + from wp_toolkit import wpmgr + + wpmgr_obj = wpmgr(args.s_id) + + wp_local_version = wpmgr_obj.get_local_version() + wp_latest_version = wpmgr_obj.get_latest_version(available=True)['version'] + can_upgrade = wp_latest_version > wp_local_version + + wp_toolkit_config_data = wpmgr_obj.get_wp_toolkit_config_data() + + return public.success_v2({ + 'local_version': wp_local_version, + 'latest_version': wp_latest_version, + 'can_upgrade': can_upgrade, + 'language': wp_toolkit_config_data['locale'], + 'login_url': wp_toolkit_config_data['login_url'], + 'home_url': wp_toolkit_config_data['site_url'], + 'cache_enabled': self.get_cache_status(args.s_id), + 'admin_user': wp_toolkit_config_data['admin_info']['user_login'], + 'admin_email': wp_toolkit_config_data['admin_info']['user_email'], + 'whl_enabled': wp_toolkit_config_data['whl_config'].get('activated', False), + 'whl_page': wp_toolkit_config_data['whl_config'].get('whl_page', 'login'), + 'whl_redirect_admin': wp_toolkit_config_data['whl_config'].get('whl_redirect_admin', '404'), + }) + + # 保存WP Toolkit配置 + def save_wp_configurations(self, args: public.dict_obj): + # 校验参数 + try: + args.validate([ + Param('s_id').Require().Integer('>', 0), + Param('language').String('in', list(self.get_language(args)['message'].keys())), + Param('admin_password').String('>=', 8), + Param('admin_email').Email(), + Param('whl_enabled').Integer('in', [0, 1]), + Param('whl_page').SafePath(), + Param('whl_redirect_admin').SafePath(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + from wp_toolkit import wpmgr + + wpmgr_obj = wpmgr(args.s_id) + + # 更新语言 + if 'language' in args: + locale = wpmgr_obj.get_local_language() + if args.language != locale: + wpmgr_obj.update_language(args.language) + + # 写操作日志 + wpmgr.log_opt('Change language from [{}] to [{}] successfully', (locale, args.language)) + + # 更新管理员密码 + if 'admin_password' in args: + wpmgr_obj.set_admin_password(args.admin_password) + + # 写操作日志 + wpmgr.log_opt('Reset admin password successfully') + + # 更新管理员邮箱 + if 'admin_email' in args: + wpmgr_obj.set_admin_email(args.admin_email) + + # 写操作日志 + wpmgr.log_opt('Reset admin email to [{}] successfully', (args.admin_email,)) + + # 更新WPS-Hide-Login插件配置 + if 'whl_enabled' in args: + # 停用插件 + if int(args.whl_enabled) == 0: + wpmgr_obj.deactivate_plugins('wps-hide-login/wps-hide-login.php') + + # 写操作日志 + wpmgr.log_opt('Deactivate plugin [{}] successfully', ('WPS Hide Login',)) + + # 启用插件 + else: + wpmgr_obj.config_plugin_wps_hide_login(args.get('whl_page', 'login'), args.get('whl_redirect_admin', '404')) + + # 写操作日志 + wpmgr.log_opt('Activate plugin [{}] successfully', ('WPS Hide Login',)) + + return public.success_v2('Update successfully') + + +##############################对外接口-END############################## + +class optimize_php: + + def optimize_php(self, get): + get.version = get.php_version + # 安装缓存插件 + # self.install_ext(get) + # 根据主机性能调整参数 + return self.php_fpm(get) + + def install_ext(self, get): + exts = ['opcache'] + import files + mfile = files.files() + for ext in exts: + # print("开始安装php扩展 [{}]...".format(ext)) + + if ext == 'pathinfo': + import config + con = config.config() + get.version = get.php_version + get.type = 'on' + con.setPathInfo(get) + else: + get.name = ext + get.version = get.php_version + get.type = '1' + mfile.InstallSoft(get) + + # 优化phpfpm + def php_fpm(self, get): + one_key_wp().write_logs("|-Start tuning PHP FPM parameters...") + mem_total = int(get_mem()) + if mem_total <= 1024: + get.max_children = '30' + get.start_servers = '5' + get.min_spare_servers = '5' + get.max_spare_servers = '20' + elif 1024 < mem_total <= 2048: + get.max_children = '50' + get.start_servers = '5' + get.min_spare_servers = '5' + get.max_spare_servers = '30' + elif 2048 < mem_total <= 4098: + get.max_children = '80' + get.start_servers = '10' + get.min_spare_servers = '10' + get.max_spare_servers = '30' + elif 4098 < mem_total <= 8096: + get.max_children = '120' + get.start_servers = '10' + get.min_spare_servers = '10' + get.max_spare_servers = '30' + elif 8096 < mem_total <= 16192: + get.max_children = '200' + get.start_servers = '15' + get.min_spare_servers = '15' + get.max_spare_servers = '50' + elif 16192 < mem_total <= 32384: + get.max_children = '300' + get.start_servers = '20' + get.min_spare_servers = '20' + get.max_spare_servers = '50' + elif 32384 < mem_total: + get.max_children = '500' + get.start_servers = '20' + get.min_spare_servers = '20' + get.max_spare_servers = '50' + # get.version = self.version + import config + current_conf = config.config().getFpmConfig(get) + get.pm = current_conf['pm'] + get.listen = current_conf['unix'] + one_key_wp().write_logs(""" + ===================PHP FPM parameters======================= + + max_children: {} + start_servers: {} + min_spare_servers: {} + max_spare_servers: {} + Running mode: {} + Connection: {} + + ===================PHP FPM parameters======================= + + + """.format(get.max_children, get.start_servers, get.min_spare_servers, get.max_spare_servers, get.pm, get.listen)) + # self.backup_conf('/www/server/php/{}/etc/php-fpm.conf'.format(self.version)) + result = config.config().setFpmConfig(get) + if not result['status']: + one_key_wp().write_logs("|-PHP FPM Optimization failed: {}".format(result)) + return public.return_msg_gettext(False, "PHP FPM Optimization failed: {}", (result,)) + one_key_wp().write_logs("|-PHP FPM optimization succeeded") + return public.return_msg_gettext(True, "PHP FPM optimization succeeded") + + +class optimize_db: + + def self_db_cache(self, get): + one_key_wp().write_logs("|-Start optimizing Mysql") + mem_total = int(get_mem()) + if mem_total <= 2048: + get.key_buffer_size = '128' + get.query_cache_size = '64' + get.tmp_table_size = '64' + get.innodb_buffer_pool_size = '256' + get.innodb_log_buffer_size = '16' + get.sort_buffer_size = '768' + get.read_buffer_size = '768' + get.read_rnd_buffer_size = '512' + get.join_buffer_size = '1024' + get.thread_stack = '256' + get.binlog_cache_size = '64' + get.thread_cache_size = '64' + get.table_open_cache = '128' + get.max_connections = '100' + get.query_cache_type = '1' + get.max_heap_table_size = '64' + elif 2048 < mem_total <= 4096: + get.key_buffer_size = '256' + get.query_cache_size = '128' + get.tmp_table_size = '384' + get.innodb_buffer_pool_size = '384' + get.innodb_log_buffer_size = '16' + get.sort_buffer_size = '768' + get.read_buffer_size = '768' + get.read_rnd_buffer_size = '512' + get.join_buffer_size = '2048' + get.thread_stack = '256' + get.binlog_cache_size = '64' + get.thread_cache_size = '96' + get.table_open_cache = '192' + get.max_connections = '200' + get.query_cache_type = '1' + get.max_heap_table_size = '384' + elif 4096 < mem_total <= 8192: + get.key_buffer_size = '384' + get.query_cache_size = '192' + get.tmp_table_size = '512' + get.innodb_buffer_pool_size = '512' + get.innodb_log_buffer_size = '16' + get.sort_buffer_size = '1024' + get.read_buffer_size = '1024' + get.read_rnd_buffer_size = '768' + get.join_buffer_size = '2048' + get.thread_stack = '256' + get.binlog_cache_size = '128' + get.thread_cache_size = '128' + get.table_open_cache = '384' + get.max_connections = '300' + get.query_cache_type = '1' + get.max_heap_table_size = '512' + elif 8192 < mem_total <= 16384: + get.key_buffer_size = '512' + get.query_cache_size = '256' + get.tmp_table_size = '1024' + get.innodb_buffer_pool_size = '1024' + get.innodb_log_buffer_size = '16' + get.sort_buffer_size = '2048' + get.read_buffer_size = '2048' + get.read_rnd_buffer_size = '1024' + get.join_buffer_size = '4096' + get.thread_stack = '384' + get.binlog_cache_size = '192' + get.thread_cache_size = '192' + get.table_open_cache = '1024' + get.max_connections = '400' + get.query_cache_type = '1' + get.max_heap_table_size = '1024' + elif 16384 < mem_total <= 32768: + get.key_buffer_size = '1024' + get.query_cache_size = '384' + get.tmp_table_size = '2048' + get.innodb_buffer_pool_size = '4096' + get.innodb_log_buffer_size = '16' + get.sort_buffer_size = '4096' + get.read_buffer_size = '4096' + get.read_rnd_buffer_size = '2048' + get.join_buffer_size = '8192' + get.thread_stack = '512' + get.binlog_cache_size = '256' + get.thread_cache_size = '256' + get.table_open_cache = '2048' + get.max_connections = '500' + get.query_cache_type = '1' + get.max_heap_table_size = '2048' + elif 32768 < mem_total: + get.key_buffer_size = '2048' + get.query_cache_size = '500' + get.tmp_table_size = '4096' + get.innodb_buffer_pool_size = '8192' + get.innodb_log_buffer_size = '16' + get.sort_buffer_size = '8192' + get.read_buffer_size = '8192' + get.read_rnd_buffer_size = '4096' + get.join_buffer_size = '16384' + get.thread_stack = '1024' + get.binlog_cache_size = '512' + get.thread_cache_size = '512' + get.table_open_cache = '2048' + get.max_connections = '1000' + get.query_cache_type = '1' + get.max_heap_table_size = '4096' + one_key_wp().write_logs(""" + =====================Mysql parameters======================= + + key_buffer_size: {} + query_cache_size: {} + tmp_table_size: {} + innodb_buffer_pool_size: {} + innodb_log_buffer_size: {} + sort_buffer_size: {} + read_buffer_size: {} + read_rnd_buffer_size: {} + join_buffer_size: {} + thread_stack: {} + binlog_cache_size: {} + thread_cache_size: {} + table_open_cache: {} + max_connections: {} + query_cache_type: {} + max_heap_table_size: {} + + =====================Mysql parameters======================= + + """.format(get.key_buffer_size, get.query_cache_size, get.tmp_table_size, get.innodb_buffer_pool_size, + get.innodb_log_buffer_size, get.sort_buffer_size, get.read_buffer_size, get.read_rnd_buffer_size, + get.join_buffer_size, get.thread_stack, get.binlog_cache_size, get.thread_cache_size, get.table_open_cache, + get.max_connections, get.query_cache_type, get.max_heap_table_size)) + import database + result = database.database().SetDbConf(get) + if not result['status']: + one_key_wp().write_logs("|-Mysql optimization failed {}".format(result)) + return public.return_msg_gettext(False, "Mysql optimization failed {}", (result,)) + public.ExecShell("/etc/init.d/mysqld restart") + one_key_wp().write_logs("|-Mysql optimization succeeded") + return public.return_msg_gettext(True, "Mysql optimization succeeded") + + +# Nginx缓存加速WP站点 +class fast_cgi: + + def get_fastcgi_conf(self, version): + conf = r""" +set $skip_cache 0; + +if ($request_method = POST) { + set $skip_cache 1; +} + +if ($query_string != "") { + set $skip_cache 1; +} + +if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") { + set $skip_cache 1; +} + +if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") { + set $skip_cache 1; +} + +location ~ (/[^/]+\.php)(/|$) { + if ( !-f $document_root$1 ) { + return 404; + } + + # try_files $uri =404; + fastcgi_pass unix:/tmp/php-cgi-%s.sock; + fastcgi_index index.php; + include fastcgi.conf; + add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; + fastcgi_cache_bypass $skip_cache; + fastcgi_no_cache $skip_cache; + add_header X-Cache "$upstream_cache_status From $host"; + fastcgi_cache WORDPRESS; + add_header Cache-Control max-age=0; + add_header Nginx-Cache "$upstream_cache_status"; + add_header Last-Modified $date_gmt; + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + etag on; + fastcgi_cache_valid 200 301 302 1d; +} + +location ~ /purge(/.*) { + allow 127.0.0.1; + deny all; + fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1"; +} +""" % version + return conf + + def get_fast_cgi_status(self, sitename, php_v): + if public.get_webserver() != "nginx": + return False + conf_path = "/www/server/panel/vhost/nginx/{}.conf".format(sitename) + content = public.readFile(conf_path) + fastcgi_conf = "include enable-php-{}-wpfastcgi.conf;".format(php_v) + # return conf_path,fastcgi_conf + if fastcgi_conf in content: + return True + return False + + def set_nginx_conf(self): + if not os.path.exists("/dev/shm/nginx-cache/wp"): + os.makedirs("/dev/shm/nginx-cache/wp") + one_key_wp().set_permission("/dev/shm/nginx-cache") + conf = """ + #AAPANEL_FASTCGI_CONF_BEGIN + fastcgi_cache_key "$scheme$request_method$host$request_uri"; + fastcgi_cache_path /dev/shm/nginx-cache/wp levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g; + fastcgi_cache_use_stale error timeout invalid_header http_500; + fastcgi_ignore_headers Cache-Control Expires Set-Cookie; + #AAPANEL_FASTCGI_CONF_END + """ + conf_path = "/www/server/nginx/conf/nginx.conf" + public.back_file(conf_path) + content = public.readFile(conf_path) + if not content: + return + if "#AAPANEL_FASTCGI_CONF_BEGIN" in content: + one_key_wp().write_logs("|-Nginx FastCgi cache configuration already exists") + print("Nginx FastCgi cache configuration already exists") + return public.return_msg_gettext(True, "Nginx FastCgi cache configuration already exists") + rep = "http\\s*\n\\s*{" + content = re.sub(rep, "http\n\t{" + conf, content) + public.writeFile(conf_path, content) + + # 如果配置出错恢复 + conf_pass = public.checkWebConfig() + if conf_pass != True: + public.restore_file(conf_path) + one_key_wp().write_logs("|-Nginx FastCgi configuration error! {}".format(conf_pass)) + print("Nginx FastCgi configuration error! {}".format(conf_pass)) + return public.return_msg_gettext(False, "Nginx FastCgi configuration error!") + one_key_wp().write_logs("|-Nginx FastCgi cache configuration complete...") + print("Nginx FastCgi cache configuration complete") + return public.return_msg_gettext(True, "Nginx FastCgi cache configuration complete") + + def set_nginx_init(self): + # if not os.path.exists("/dev/shm/nginx-cache/wp"): + # os.makedirs("/dev/shm/nginx-cache/wp") + # one_key_wp().set_permission("/dev/shm/nginx-cache") + conf2 = """ + #AAPANEL_FASTCGI_CONF_BEGIN + mkdir -p /dev/shm/nginx-cache/wp + #AAPANEL_FASTCGI_CONF_END + """ + init_path = "/etc/init.d/nginx" + public.back_file(init_path) + content_init = public.readFile(init_path) + if not content_init: + return + if "#AAPANEL_FASTCGI_CONF_BEGIN" in content_init: + one_key_wp().write_logs("|-Nginx init FastCgi cache configuration already exists") + print("Nginx init FastCgi cache configuration already exists") + return public.return_msg_gettext(True, "Nginx init FastCgi cache configuration already exists") + # content_init = re.sub(r"\$NGINX_BIN -c \$CONFIGFILE", + conf2, content_init) + rep2 = r"\$NGINX_BIN -c \$CONFIGFILE" + content_init = re.sub(rep2, conf2 + " $NGINX_BIN -c $CONFIGFILE", content_init) + public.writeFile(init_path, content_init) + + # 如果配置出错恢复 + public.ExecShell("/etc/init.d/nginx restart") + conf_pass = public.is_nginx_process_exists() + if conf_pass == False: + public.restore_file(init_path) + one_key_wp().write_logs("|-Nginx init FastCgi configuration error! {}".format(conf_pass)) + print("Nginx init FastCgi configuration error! {}".format(conf_pass)) + return public.return_msg_gettext(False, "Nginx init FastCgi configuration error!") + one_key_wp().write_logs("|-Nginx init FastCgi cache configuration complete...") + print("Nginx init FastCgi cache configuration complete") + return public.return_msg_gettext(True, "Nginx init FastCgi cache configuration complete") + + def set_fastcgi_php_conf(self, version): + conf_path = "/www/server/nginx/conf/enable-php-{}-wpfastcgi.conf".format(version) + if os.path.exists(conf_path): + one_key_wp().write_logs("|-Nginx FastCgi PHP configuration already exists...") + print("Nginx FastCgi PHP configuration already exists") + return True + public.writeFile(conf_path, self.get_fastcgi_conf(version)) + + def set_website_conf(self, version, sitename, act=None): + conf_path = "/www/server/panel/vhost/nginx/{}.conf".format(sitename) + public.back_file(conf_path) + conf = public.readFile(conf_path) + if not conf: + print("Website configuration file does not exist {}".format(conf_path)) + one_key_wp().write_logs("|-Website configuration file does not exist: {}".format(conf_path)) + return public.return_message(-1,0,False) + if act == 'disable': + fastcgi_conf = "include enable-php-{}-wpfastcgi.conf;".format(version) + if fastcgi_conf not in conf: + print("FastCgi configuration does not exist in website configuration") + one_key_wp().write_logs("|-FastCgi configuration does not exist in website configuration, skip") + return public.return_message(-1,0, "FastCgi configuration does not exist in website configuration") + rep = r"include\s+enable-php-{}-wpfastcgi.conf;".format(version) + conf = re.sub(rep, "include enable-php-{}.conf;".format(version), conf) + else: + fastcgi_conf = "include enable-php-{}-wpfastcgi.conf;".format(version) + if fastcgi_conf in conf: + one_key_wp().write_logs( + "|-The FastCgi configuration already exists in the website configuration, skip it") + return public.return_message(0,0, + "The FastCgi configuration already exists in the website configuration") + rep = r"include\s+enable-php-{}.conf;".format(version) + + one_key_wp().write_logs("|-Current configuration: {}".format(conf)) + one_key_wp().write_logs("|-Regular expression: {}".format(rep)) + + conf = re.sub(rep, fastcgi_conf, conf) + + one_key_wp().write_logs("|-Modified configuration: {}".format(conf)) + public.writeFile(conf_path, conf) + conf_pass = public.checkWebConfig() + if conf_pass != True: + public.restore_file(conf_path) + print("Website FastCgi configuration error {}".format(conf_pass)) + one_key_wp().write_logs("|-Website FastCgi configuration error: {}", (conf_pass,)) + return public.return_message(-1,0, "Website FastCgi configuration error!") + print("Website FastCgi configuration complete") + one_key_wp().write_logs("|-Website FastCgi configuration complete...") + return public.return_message(0,0, "Website FastCgi configuration complete") + + def set_fastcgi(self, values): + """ + get.version + get.name + """ + # 设置nginx启动文件 + self.set_nginx_init() + # 设置nginx全局配置 + self.set_nginx_conf() + # 设置fastcgi location + self.set_fastcgi_php_conf(values['php_version']) + # 设置网站配置文件 + self.set_website_conf(values['php_version'], values['site_name']) + # 设置wp的变量用于nginxhelper插件清理缓存 + self.set_wp_nginx_helper(values['site_path']) + # 设置userini允许访问 /dev/shm/nginx-cache/wp 目录 + self.set_userini(values['site_path']) + + def set_wp_nginx_helper(self, site_path): + cache_conf = """ + #AAPANEL_FASTCGICACHE_BEGIN + define('RT_WP_NGINX_HELPER_CACHE_PATH','/dev/shm/nginx-cache/wp'); + #AAPANEL_FASTCGICACHE_END + """ + conf_file = "{}/wp-config.php".format(site_path) + conf = public.readFile(conf_file) + if not conf: + print("Wordpress configuration file does not exist: {}".format(conf_file)) + one_key_wp().write_logs("|-Wordpress configuration file does not exist: {}".format(conf_file)) + return public.return_msg_gettext(False, "Wordpress configuration file does not exist: {}", (conf_file,)) + if re.search(r'''define\(\s*'RT_WP_NGINX_HELPER_CACHE_PATH'\s*,''', conf): + # if "RT_WP_NGINX_HELPER_CACHE_PATH" in conf: + one_key_wp().write_logs("|-Cache cleaning configuration already exists, skip") + print("Cache cleaning configuration already exists") + return + conf = conf.replace(" +#------------------------------------------------------------------- + +#------------------------------ +# 系统安全管理控制器 +#------------------------------ +import os,sys,public,json,re + +class Controller: + + + def __init__(self): + pass + + def model(self,args): + ''' + @name 调用指定项目模型 + @author hezhihong<2024-04-15> + @param args { + mod_name: string<模型名称> + def_name: string<方法名称> + data: JSON + } + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: + return_message=public.return_status_code(1000,'Bad call!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.exists_args('def_name,mod_name',args) + if args['def_name'].find('__') != -1: + return_message=public.return_status_code(1000,'The called method name cannot contain the "__" character') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['mod_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['def_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + except: + return public.return_message(-1,0, public.get_error_object()) + # 参数处理 + mod_name = "{}Model".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() + model_index = None + if 'model_index' in args: + model_index = args['model_index'] + + if not hasattr(args,'data'): args.data = {} + if args.data: + if isinstance(args.data,str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.return_message(-1,0, public.get_error_object()) + else: + pdata = args.data + else: + pdata = args + + if isinstance(pdata,dict): + pdata = public.to_dict_obj(pdata) + + if not isinstance(pdata,public.dict_obj): + return public.return_message(-1,0, "The passed parameter is not a universal internal object") + + # 告诉加载器,要加载什么模块 + if model_index: pdata.model_index = model_index + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(),def_name.upper()) + hook_result = public.exec_hook(hook_index,pdata) + if isinstance(hook_result,public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result,dict): + return public.return_message(-1,0, hook_result) # 响应具体错误信息 + elif isinstance(hook_result,bool): + if not hook_result: # 直接中断操作 + return_message=public.return_data(False,{},error_msg='Pre-HOOK interrupt operation') + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 调用处理方法 + # result = run_object(pdata) + import public.PluginLoader as plugin_loader + mod_file = '{}/class_v2/{}ModelV2/{}.py'.format(public.get_panel_path(),model_index,mod_name) + plugin_class = plugin_loader.get_module(mod_file) + class_string='main' + # if mod_name=='ftpModel': + # class_string='ftplog' + plugin_object = getattr(plugin_class,class_string)() + result = getattr(plugin_object,def_name)(pdata) + if isinstance(result,dict): + if 'status' in result and result['status'] == False and 'msg' in result: + if isinstance(result['msg'],str): + if result['msg'].find('Traceback ') != -1: + raise public.return_message(-1,0,public.PanelError(result['msg'])) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(),def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index,hook_data) + if isinstance(hook_result,dict): + result = hook_result['result'] + return result + + diff --git a/class_v2/panelDatabaseControllerV2.py b/class_v2/panelDatabaseControllerV2.py new file mode 100644 index 00000000..240dec59 --- /dev/null +++ b/class_v2/panelDatabaseControllerV2.py @@ -0,0 +1,128 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hezhihong<2024-04-15> +#------------------------------------------------------------------- + +#------------------------------ +# 数据库管理控制器 +#------------------------------ +import os,sys,public,json,re + +class DatabaseController: + + + def __init__(self): + pass + + def model(self,args): + ''' + @name 调用指定项目模型 + @author hezhihong<2024-04-15> + @param args { + mod_name: string<模型名称> + def_name: string<方法名称> + data: JSON + } + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: + return_message=public.return_status_code(1000,'Bad call!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.exists_args('def_name,mod_name',args) + if args['def_name'].find('__') != -1: + return_message=public.return_status_code(1000,'The called method name cannot contain the "__" character') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['mod_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['def_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + except: + return public.return_message(-1,0, public.get_error_object()) + # 参数处理 + module_name = args['mod_name'].strip() + mod_name = "{}Model".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() + + # # 指定模型是否存在 + # mod_file = "{}/databaseModel/{}.py".format(public.get_class_path(),mod_name) + # if not os.path.exists(mod_file): + # return public.return_status_code(1003,mod_name) + # # 实例化 + # def_object = public.get_script_object(mod_file) + # if not def_object: return public.return_status_code(1000,'没有找到{}模型'.format(mod_name)) + # run_object = getattr(def_object.main(),def_name,None) + + # if not run_object: return public.return_status_code(1000,'没有在{}模型中找到{}方法'.format(mod_name,def_name)) + if not hasattr(args,'data'): args.data = {} + if args.data: + if isinstance(args.data,str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.return_message(-1,0,public.get_error_object()) + elif isinstance(args.data,dict): + pdata = public.to_dict_obj(args.data) + else: + pdata = args.data + else: + pdata = public.dict_obj() + + if isinstance(pdata,dict): pdata = public.to_dict_obj(pdata) + pdata.model_index = 'database_v2' + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(),def_name.upper()) + hook_result = public.exec_hook(hook_index,pdata) + if isinstance(hook_result,public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result,dict): + # return hook_result # 响应具体错误信息 + return public.return_message(-1,0, hook_result) + elif isinstance(hook_result,bool): + if not hook_result: # 直接中断操作 + return_message=public.return_data(False,{},error_msg='Pre-HOOK interrupt operation') + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 调用处理方法 + # result = run_object(pdata) + import public.PluginLoader as plugin_loader + mod_file = '{}/class_v2/databaseModelV2/{}.py'.format(public.get_panel_path(),mod_name) + plugin_class = plugin_loader.get_module(mod_file) + plugin_object = getattr(plugin_class,"main")() + result = getattr(plugin_object,def_name)(pdata) + + # public.print_log("-------------------控制器mode : {}".format(result)) + # if isinstance(result,dict): + # if 'status' in result and result['status'] == False and 'msg' in result: + # if isinstance(result['msg'],str): + # if result['msg'].find('Traceback ') != -1: + # raise public.PanelError(result['msg']) + if isinstance(result,dict): + if 'status' in result and result['status'] == -1 and 'message' in result and 'result' in result['message']: + if isinstance(result['message']['result'],str): + if result['message']['result'].find('Traceback ') != -1: + raise public.return_message(-1,0,public.PanelError(result['message']['result'])) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(),def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index,hook_data) + if isinstance(hook_result,dict): + result = hook_result['result'] + return result + + diff --git a/class_v2/panelDockerControllerV2.py b/class_v2/panelDockerControllerV2.py new file mode 100644 index 00000000..e8b1073c --- /dev/null +++ b/class_v2/panelDockerControllerV2.py @@ -0,0 +1,133 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hezhihong<2024-04-15> +#------------------------------------------------------------------- + +#------------------------------ +# docker模块控制器 +#------------------------------ +import os,sys,public,json,re + +class DockerController: + + + def __init__(self): + pass + + def model(self,args): + ''' + @name 调用指定项目模型 + @author hezhihong<2024-04-15> + @param args { + mod_name: string<模型名称> + def_name: string<方法名称> + data: JSON + } + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: + return_message=public.return_status_code(1000,'wrong call!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.exists_args('def_name,mod_name',args) + if args['def_name'].find('__') != -1: + return_message=public.return_status_code(1000,'Called method name cannot contain [ __ ] characters') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['mod_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['def_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + except: + return public.return_message(-1,0, public.get_error_object()) + + #静态html调用 + if 'stype' in args and args['stype'] == 'html': + from BTPanel import render_template_string + t_path_root = public.get_panel_path()+'/class/projectModel/templates/' + t_path = t_path_root + args['mod_name']+"_"+args['def_name'] + '.html' + if not os.path.exists(t_path): + return_message=public.return_status_code(1000,'The called template does not exist!'+t_path) + del return_message['status'] + return public.return_message(-1,0, return_message) + t_body = public.readFile(t_path) + public.return_message(0,0, "") + return render_template_string(t_body, data={}) + + # 参数处理 + module_name = args['mod_name'].strip() + mod_name = "{}Model".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() + + # # 指定模型是否存在 + # mod_file = "{}/projectModel/{}.py".format(public.get_class_path(),mod_name) + # if not os.path.exists(mod_file): + # return public.return_status_code(1003,mod_name) + # # 实例化 + # def_object = public.get_script_object(mod_file) + # if not def_object: return public.return_status_code(1000,'没有找到{}模型'.format(mod_name)) + # run_object = getattr(def_object.main(),def_name,None) + # if not run_object: return public.return_status_code(1000,'没有在{}模型中找到{}方法'.format(mod_name,def_name)) + if not hasattr(args,'data'): args.data = {} + if args.data: + if isinstance(args.data,str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.return_message(-1,0,public.get_error_object()) + else: + pdata = args.data + else: + pdata = args + + if isinstance(pdata,dict): pdata = public.to_dict_obj(pdata) + + pdata.model_index = 'docker_v2' + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(),def_name.upper()) + hook_result = public.exec_hook(hook_index,pdata) + if isinstance(hook_result,public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result,dict): + return public.return_message(-1,0,hook_result) # 响应具体错误信息 + elif isinstance(hook_result,bool): + if not hook_result: # 直接中断操作 + return_message=public.return_data(False,{},error_msg='Pre-HOOK interrupt operation') + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 调用处理方法 + # result = run_object(pdata) + import public.PluginLoader as plugin_loader + mod_file = '{}/class_v2/btdockerModelV2/{}.py'.format(public.get_panel_path(),mod_name) + plugin_class = plugin_loader.get_module(mod_file) + plugin_object = getattr(plugin_class, "main")() + result = getattr(plugin_object,def_name)(pdata) + if isinstance(result,dict): + if 'status' in result and result['status'] == False and 'msg' in result: + if isinstance(result['msg'],str): + if result['msg'].find('Traceback ') != -1: + raise public.return_message(-1,0,public.PanelError(result['msg'])) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(),def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index,hook_data) + if isinstance(hook_result,dict): + result = hook_result['result'] + return result + + + diff --git a/class_v2/panelModControllerV2.py b/class_v2/panelModControllerV2.py new file mode 100644 index 00000000..05a682d7 --- /dev/null +++ b/class_v2/panelModControllerV2.py @@ -0,0 +1,117 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aapanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hezhihong +# ------------------------------------------------------------------- + +# ------------------------------ +# 网站模型管理控制器 +# ------------------------------ +import json +import public +import re + + +class Controller: + + def __init__(self): + pass + + def model(self, args): + ''' + @name 调用指定项目模型 + @author hezhihong + @param {"mod_name":"string<模型名称>","def_name":"string<方法名称>","data":JSON,} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: return public.return_status_code(1000, '错误的调用!') + public.exists_args('def_name,mod_name', args) + if args['def_name'].find('__') != -1: return public.return_status_code(1000, + '调用的方法名称中不能包含“__”字符') + if not re.match(r"^\w+$", args['mod_name']): return public.return_status_code(1000, + r'调用的模块名称中不能包含\w以外的字符') + if not re.match(r"^\w+$", args['def_name']): return public.return_status_code(1000, + r'调用的方法名称中不能包含\w以外的字符') + except: + return public.get_error_object() + # 参数处理 + module_name = args['mod_name'].strip() + sub_mod_name = args['sub_mod_name'].strip() + mod_name = "{}Mod".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() + model_index = None + if 'model_index' in args: model_index = args['model_index'] + + if not hasattr(args, 'data'): args.data = {} + if args.data: + if isinstance(args.data, str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.get_error_object() + else: + pdata = args.data + else: + pdata = args + + if isinstance(pdata, dict): + pdata = public.to_dict_obj(pdata) + + if not isinstance(pdata, public.dict_obj): + return public.return_error("传递的参数不是通用的内部对象") + + # 告诉加载器,要加载什么模块 + if model_index: pdata.model_index = model_index + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(), def_name.upper()) + hook_result = public.exec_hook(hook_index, pdata) + if isinstance(hook_result, public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result, dict): + return hook_result # 响应具体错误信息 + elif isinstance(hook_result, bool): + if not hook_result: # 直接中断操作 + return public.return_data(False, {}, error_msg='前置HOOK中断操作') + + # 调用处理方法 + # result = run_object(pdata) + # import PluginLoader + + # public.print_log('mol--v2---module_fuile:{}'.format("{}/{}".format(module_name, sub_mod_name))) + # result = PluginLoader.module_run("{}/{}".format(module_name, sub_mod_name), def_name, pdata) + # public.print_log('mol--v2---result:{}'.format(result) + + import public.PluginLoader as plugin_loader + mod_file = '{}/class_v2/modModelV2/project/{}/comMod.py'.format(public.get_panel_path(),module_name) + public.print_log('mol--v2---mod_file:{}'.format(mod_file)) + plugin_class = plugin_loader.get_module(mod_file) + public.print_log('mol--v2---plugin_class:{}'.format(plugin_class)) + class_string='main' + # if mod_name=='ftpModel': + # class_string='ftplog' + plugin_object = getattr(plugin_class,class_string)() + public.print_log('mol--v2---plugin_object:{}'.format(plugin_object)) + result = getattr(plugin_object,def_name)(pdata) + public.print_log('mol--v2---result:{}'.format(result)) + + if isinstance(result, dict): + if 'status' in result and result['status'] == False and 'msg' in result: + if isinstance(result['msg'], str): + if result['msg'].find('Traceback ') != -1: + raise public.PanelError(result['msg']) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(), def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index, hook_data) + if isinstance(hook_result, dict): + result = hook_result['result'] + return result diff --git a/class_v2/panelModelV2/backupModel.py b/class_v2/panelModelV2/backupModel.py new file mode 100644 index 00000000..4731b9eb --- /dev/null +++ b/class_v2/panelModelV2/backupModel.py @@ -0,0 +1,83 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: cjxin +#------------------------------------------------------------------- + +# 备份 +#------------------------------ +import os,sys,re,json,shutil,psutil,time +from panelModelV2.base import panelBase +import public + +class main(panelBase): + + + def __init__(self): + pass + + + def get_site_backup_info(self,get): + """ + @获取网站是否开启计划任务备份 + @param get['site_id'] 网站id + @return + all : 开启全部网站备份 + info:计划任务详情 + """ + + id = get.id + find = public.M('sites').where("id=?",(id,)).find() + if not find: + return public.returnMsg(False,'找不到指定网站.') + + result = {} + result['all'] = 0 + result['info'] = False + result['status'] = True + data = public.M('crontab').where('sName=? and sType =?',(find['name'],'site')).order('id desc').select() + if len(data) > 0: + result['info'] = data[0] + + data = public.M('crontab').where('sName=? and sType =?',('ALL','site')).order('id desc').select() + if len(data) > 0: + result['info'] = data[0] + result['all'] = 1 + return result + + + def get_database_backup_info(self,get): + """ + @获取数据库是否开启计划任务备份 + @param get['site_id'] 数据库id + @return + all : 开启全部数据库备份 + info:计划任务详情 + """ + + id = get.id + find = public.M('databases').where("id=?",(id,)).find() + if not find: + return public.returnMsg(False,'找不到指定数据库.') + + result = {} + result['all'] = 0 + result['info'] = False + result['status'] = True + data = public.M('crontab').where('sName=? and sType =?',(find['name'],'database')).order('id desc').select() + if len(data) > 0: + result['info'] = data[0] + + data = public.M('crontab').where('sName=? and sType =?',('ALL','database')).order('id desc').select() + if len(data) > 0: + result['info'] = data[0] + result['all'] = 1 + return result + + + + + diff --git a/class_v2/panelModelV2/base.py b/class_v2/panelModelV2/base.py new file mode 100644 index 00000000..ccbceed5 --- /dev/null +++ b/class_v2/panelModelV2/base.py @@ -0,0 +1,21 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: cjxin +#------------------------------------------------------------------- + +# 面板其他模型新增功能 +#------------------------------ +import public,re,time,sys,os,json +from datetime import datetime + + + +class panelBase: + + + def __init__(self): + pass diff --git a/class_v2/panelModelV2/publicModel.py b/class_v2/panelModelV2/publicModel.py new file mode 100644 index 00000000..8e2b85b9 --- /dev/null +++ b/class_v2/panelModelV2/publicModel.py @@ -0,0 +1,280 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: cjxin +# ------------------------------------------------------------------- + +# 备份 +# ------------------------------ +import os, sys, re, json, shutil, psutil, time +from panelModelV2.base import panelBase +import public, panelTask +import config_v2 as config + +try: + from BTPanel import cache +except:pass + +class main(panelBase): + __table = 'task_list' + # public.check_database_field("ssl_data.db","ssl_info") + task_obj = panelTask.bt_task() + + def __init__(self): + pass + + + """ + @name 获取面板日志 + """ + def get_update_logs(self,get): + try: + + skey = 'panel_update_logs' + res = cache.get(skey) + if res: return res + + res = public.httpPost('https://www.bt.cn/Api/getUpdateLogs?type=Linux',{}) + + start_index = res.find('(') + 1 + end_index = res.rfind(')') + json_data = res[start_index:end_index] + + res = json.loads(json_data) + cache.set(skey,res,60) + except: + res = [] + + return res + + def get_public_config(self, args): + """ + @name 获取公共配置 + """ + # public.print_log("error 3366666: {}".format()) + _config_obj = config.config() + data = _config_obj.get_config(args)['message'] + + data['task_list'] = self.task_obj.get_task_lists(args) + data['task_count'] = public.M('tasks').where("status!=?", ('1',)).count() + data['get_pd'] = self.get_pd(args) + data['ipv6'] = '' + if _config_obj.get_ipv6_listen(None): data['ipv6'] = 'checked' + data['is_local'] = '' + if public.is_local(): data['is_local'] = 'checked' + + if data['get_pd'] and data['get_pd'][2] != -1: + time_diff = (data['get_pd'][2]-int(time.time())) % (365*86400) + data['active_pro_time'] = int(time.time()) - (365*86400 - time_diff) + else: + data['active_pro_time'] = 0 + data['status_code'] = _config_obj.get_not_auth_status() + if os.path.exists('/www/server/panel/config/api.json'): + try: + res = json.loads(public.readFile('/www/server/panel/config/api.json')) + data['api'] = 'checked' if res['open'] else '' + except: + public.ExecShell('rm -f /www/server/panel/config/api.json') + data['api'] = '' + else: + data['api'] = '' + return public.return_message(0,0,data) + + # 获取公共配置(精简版) + def get_public_config_simple(self, args): + """ + @name 获取公共配置(精简版) + @param args 请求参数 + @return dict + """ + data = {} + data['task_count'] = public.M('tasks').where("status!=?", ('1',)).count() + data['get_pd'] = self.get_pd(args) + + import panelSSL + data['user_info'] = panelSSL.panelSSL().GetUserInfo(None) + + import system_v2 as system + data['panel'] = system.system().GetPanelInfo() + + data["webname"] = public.GetConfigValue("title") + + data['menu_list'] = config.config().get_menu_list(args) + + data['install_finished'] = os.path.exists('{}/data/install_finished.mark'.format(public.get_panel_path())) + + return public.return_message(0, 0, data) + + # 获取常用软件安装状态 + def get_public_config_setup(self, args): + from BTPanel import session + if 'config' not in session: + session['config'] = public.M('config').where("id=?", ('1',)).field( + 'webserver,sites_path,backup_path,status,mysql_root').find() + data = session['config'] + + import system_v2 as system + data.update(system.system().GetConcifInfo()) + + if 'ftpPort' not in data: + # 获取FTP端口 + if 'port' not in session: + import re + try: + file = public.GetConfigValue('setup_path') + '/pure-ftpd/etc/pure-ftpd.conf' + conf = public.readFile(file) + rep = r"\n#?\s*Bind\s+[0-9]+\.[0-9]+\.[0-9]+\.+[0-9]+,([0-9]+)" + port = re.search(rep, conf).groups()[0] + except: + port = '21' + session['port'] = port + + data['ftpPort'] = session['port'] + + return public.return_message(0, 0, data) + + # 获取授权信息 + def get_pd(self, get): + return public.get_pd(get) + + @staticmethod + def set_backup_path(get): + try: + backup_path = get.backup_path.strip().rstrip("/") + except AttributeError: + return public.returnMsg(False, "参数错误") + + if not os.path.exists(backup_path): + return public.returnMsg(False, "指定目录不存在") + + if backup_path[-1] == "/": + backup_path = backup_path[:-1] + + import files + try: + from BTPanel import session + except: + session = None + fs = files.files() + + if not fs.CheckDir(get.backup_path): + return public.returnMsg(False, '不能使用系统关键目录作为默认备份目录') + if session is not None: + session['config']['backup_path'] = os.path.join('/', backup_path) + db_backup = backup_path + '/database' + site_backup = backup_path + '/site' + + if not os.path.exists(db_backup): + try: + os.makedirs(db_backup, 384) + except: + public.ExecShell('mkdir -p ' + db_backup) + + if not os.path.exists(site_backup): + try: + os.makedirs(site_backup, 384) + except: + public.ExecShell('mkdir -p ' + site_backup) + + public.M('config').where("id=?", ('1',)).save('backup_path', (get.backup_path,)) + public.WriteLog('TYPE_PANEL', 'PANEL_SET_SUCCESS', (get.backup_path,)) + + public.restart_panel() + return public.returnMsg(True, "设置成功") + + + def get_soft_status(self, get): + if not hasattr(get, 'name'): return public.return_message(-1, 0,'Parameter error') + s_status = False + status = False + setup = False + name = get.name.strip() + if name == 'web': name = public.get_webserver() + version = '' + if name == 'sqlite': + status = True + if name in ['mysql', 'pgsql', 'sqlserver', 'mongodb', 'redis']: + count = public.M('database_servers').where("LOWER(db_type)=LOWER(?)", (name,)).count() + if count > 0: status = True + if os.path.exists('/www/server/{}'.format(name)) and len(os.listdir('/www/server/{}'.format(name))) > 2: + if not public.M('tasks').where("name like ? and status == -1", + ('安装%{}%'.format(name.replace('-', '')),)).count() > 0: + status = True + setup = True + if name == 'openlitespeed': + status = os.path.exists('/usr/local/lsws/bin/lswsctrl') + setup = status + if status: + path_data = { + "nginx": "/www/server/nginx/logs/nginx.pid", + "mysql": "/www/server/data/localhost.localdomain.pid", + "apache": "/www/server/apache/logs/httpd.pid", + "pure-ftpd": "/var/run/pure-ftpd.pid", + "redis": "/www/server/redis/redis.pid", + "pgsql": "/www/server/pgsql/data_directory/postmaster.pid", + "openlitespeed": "/tmp/lshttpd/lshttpd.pid" + } + if name == 'mysql': + datadir = public.get_datadir() + if datadir: + path_data["mysql"] = "{}/{}.pid".format(datadir, public.get_hostname()) + + if name in path_data.keys(): + if os.path.exists(path_data[name]): + pid = public.readFile(path_data[name]) + if pid: + try: + psutil.Process(int(pid)) + s_status = True + except: + pass + else: + # 可能会存在用户修改主机名后 找不到pid文件的情况 + if name == 'mysql' and not s_status: + for proc in psutil.process_iter(): + if proc.name() == 'mysqld': + s_status = True + public.writeFile(path_data['mysql'], str(proc.pid)) + break + version_data = { + "nginx": '/www/server/nginx/version.pl', + "mysql": "/www/server/mysql/version.pl", + "pgsql": "/www/server/pgsql/data/PG_VERSION", + "apache": "/www/server/apache/version.pl", + "pure-ftpd": "/www/server/pure-ftpd/version.pl", + "openlitespeed": "/usr/local/lsws/VERSION" + } + if name in version_data.keys(): + if os.path.exists(version_data[name]): + version = public.readFile(version_data[name]).strip() + title_data = { + "nagix": "Nginx", + "mysql": "MySQL", + "pgsql": "PostgreSQL", + "mongodb": "MongoDB", + "redis": "Redis", + "apache": "Apache", + "openlitespeed": "OpenLiteSpeed", + "pure-ftpd": "Pure-FTPd", + } + s_version_data = { + 'mysql': 'mysqld', + 'apache': 'httpd', + } + + data = { + "status": status, + "s_status": s_status, + "msg": '', + "version": version, + "name": name.replace('-', ''), + "title": title_data.get(name, name), + "admin": os.path.exists('/www/server/panel/plugin/' + name), + "s_version": s_version_data.get(name, name), + "setup": setup + } + return public.return_message(0, 0,data) + diff --git a/class_v2/panelProjectControllerV2.py b/class_v2/panelProjectControllerV2.py new file mode 100644 index 00000000..a8d36141 --- /dev/null +++ b/class_v2/panelProjectControllerV2.py @@ -0,0 +1,136 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hezhihong<2024-04-15> +#------------------------------------------------------------------- + +#------------------------------ +# 项目管理控制器 +#------------------------------ +import os,sys,public,json,re + +class ProjectController: + + + def __init__(self): + pass + + def model(self,args): + ''' + @name 调用指定项目模型 + @author hezhihong<2024-04-15> + @param args { + mod_name: string<模型名称> + def_name: string<方法名称> + data: JSON + } + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: + return_message=public.return_status_code(1000,'wrong call!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.exists_args('def_name,mod_name',args) + if args['def_name'].find('__') != -1: + return_message=public.return_status_code(1000,'Called method name cannot contain [ __ ] characters') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['mod_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['def_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + except: + return public.return_message(-1,0,public.get_error_object()) + + #静态html调用 + if 'stype' in args and args['stype'] == 'html': + from BTPanel import render_template_string + t_path_root = public.get_panel_path()+'/class_v2/projectModelV2/templates/' + t_path = t_path_root + args['mod_name']+"_"+args['def_name'] + '.html' + if not os.path.exists(t_path): + return_message=public.return_status_code(1000,'The called template does not exist!'+t_path) + del return_message['status'] + return public.return_message(-1,0, return_message) + t_body = public.readFile(t_path) + public.return_message(0,0, "") + return render_template_string(t_body, data={}) + + # 参数处理 + module_name = args.mod_name.strip() + mod_name = "{}Model".format(args.mod_name.strip()) + def_name = args.def_name.strip() + # # 指定模型是否存在 + # mod_file = "{}/projectModel/{}.py".format(public.get_class_path(),mod_name) + # if not os.path.exists(mod_file): + # return public.return_status_code(1003,mod_name) + # # 实例化 + # def_object = public.get_script_object(mod_file) + # if not def_object: return public.return_status_code(1000,'没有找到{}模型'.format(mod_name)) + # run_object = getattr(def_object.main(),def_name,None) + # if not run_object: return public.return_status_code(1000,'没有在{}模型中找到{}方法'.format(mod_name,def_name)) + if not hasattr(args,'data'): args.data = {} + if args.data: + if isinstance(args.data,str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.return_message(-1,0,public.get_error_object()) + else: + pdata = args.data + else: + pdata = args + + if isinstance(pdata,dict): pdata = public.to_dict_obj(pdata) + pdata.model_index = 'project_v2' + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(),def_name.upper()) + hook_result = public.exec_hook(hook_index,pdata) + if isinstance(hook_result,public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result,dict): + return public.return_message(-1,0, hook_result) # 响应具体错误信息 + elif isinstance(hook_result,bool): + if not hook_result: # 直接中断操作 + return_message=public.return_data(False,{},error_msg='Pre-HOOK interrupt operation') + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 调用处理方法 + # result = run_object(pdata) + class_string='main' + import public.PluginLoader as plugin_loader + if module_name=='proxy': + class_string='Redirect' + mod_file = '{}/class_v2/projectModelV2/common/redirect.py'.format(public.get_panel_path()) + else: + mod_file = '{}/class_v2/projectModelV2/{}.py'.format(public.get_panel_path(),mod_name) + plugin_class = plugin_loader.get_module(mod_file) + plugin_object = getattr(plugin_class,class_string)() + result = getattr(plugin_object,def_name)(pdata) + if isinstance(result,dict): + if 'status' in result and result['status'] == False and 'msg' in result: + if isinstance(result['msg'],str): + if result['msg'].find('Traceback ') != -1: + raise public.return_message(-1,0,public.PanelError(result['msg'])) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(),def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index,hook_data) + if isinstance(hook_result,dict): + result = hook_result['result'] + return result + + + diff --git a/class_v2/panelSafeControllerV2.py b/class_v2/panelSafeControllerV2.py new file mode 100644 index 00000000..70a0d77f --- /dev/null +++ b/class_v2/panelSafeControllerV2.py @@ -0,0 +1,110 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hezhihong +#------------------------------------------------------------------- + +#------------------------------ +# 系统安全管理控制器 +#------------------------------ +import os,sys,public,json,re + +class SafeController: + + + def __init__(self): + pass + + def model(self,args): + ''' + @name 调用指定项目模型 + @author hezhihong<2024-04-15> + @param args { + mod_name: string<模型名称> + def_name: string<方法名称> + data: JSON + } + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: + return_message=public.return_status_code(1000,'wrong call!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.exists_args('def_name,mod_name',args) + if args['def_name'].find('__') != -1: + return_message=public.return_status_code(1000,'The called method name cannot contain the "__" character') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['mod_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not re.match(r"^\w+$",args['def_name']): + return_message=public.return_status_code(1000,r'The called module name cannot contain characters other than \w') + del return_message['status'] + return public.return_message(-1,0, return_message) + except: + return public.return_message(-1,0,public.get_error_object()) + # 参数处理 + module_name = args['mod_name'].strip() + mod_name = "{}Model".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() + + if not hasattr(args,'data'): args.data = {} + if args.data: + if isinstance(args.data,str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.return_message(-1,0,public.get_error_object()) + elif isinstance(args.data,dict): + pdata = public.to_dict_obj(args.data) + else: + pdata = args.data + else: + pdata = public.dict_obj() + + if isinstance(pdata,dict): pdata = public.to_dict_obj(pdata) + pdata.model_index = 'safe_v2' + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(),def_name.upper()) + hook_result = public.exec_hook(hook_index,pdata) + if isinstance(hook_result,public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result,dict): + return public.return_message(-1,0,hook_result) # 响应具体错误信息 + elif isinstance(hook_result,bool): + if not hook_result: # 直接中断操作 + return_message=public.return_data(False,{},error_msg='Pre-HOOK interrupt operation') + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 调用处理方法 + # result = run_object(pdata) + import public.PluginLoader as plugin_loader + mod_file = '{}/class_v2/safeModelV2/{}.py'.format(public.get_panel_path(),mod_name) + plugin_class = plugin_loader.get_module(mod_file) + plugin_object = getattr(plugin_class,"main")() + result = getattr(plugin_object,def_name)(pdata) + if isinstance(result,dict): + if 'status' in result and result['status'] == False and 'msg' in result: + if isinstance(result['msg'],str): + if result['msg'].find('Traceback ') != -1: + raise public.return_message(-1,0, public.PanelError(result['msg'])) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(),def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index,hook_data) + if isinstance(hook_result,dict): + result = hook_result['result'] + return result + + diff --git a/class_v2/panel_api_v2.py b/class_v2/panel_api_v2.py new file mode 100644 index 00000000..f7b417ba --- /dev/null +++ b/class_v2/panel_api_v2.py @@ -0,0 +1,252 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import public,os,json,time +class panelApi: + save_path = '/www/server/panel/config/api.json' + timeout = 600 + max_bind = 5 + def get_token(self,get): + data = self.get_api_config() + if not 'key' in data: + data['key'] = public.GetRandomString(16) + public.writeFile(self.save_path,json.dumps(data)) + + if 'token_crypt' in data: + data['token'] = public.de_crypt(data['token'],data['token_crypt']) + else: + data['token'] = "***********************************" + + data['limit_addr'] = '\n'.join(data['limit_addr']) + data['bind'] = self.get_bind_token() + qrcode = (public.getPanelAddr() + "|" + data['token'] + "|" + data['key'] + '|' + data['bind']['token']+'|aapanel').encode('utf-8') + data['qrcode'] = public.base64.b64encode(qrcode).decode('utf-8') + data['apps'] = sorted(data['apps'],key=lambda x: x['time'],reverse=True) + del(data['key']) + return data + + + def login_for_app(self,get): + from BTPanel import cache + import uuid + tid = get.tid + if(len(tid) != 32): return public.return_msg_gettext(False,'Invalid login key1') + session_id = cache.get(tid) + if not session_id: return public.return_msg_gettext(False,'The specified key does not exist or has expired1') + if(len(session_id) != 64): return public.return_msg_gettext(False,'Invalid login key2') + try: + if not os.path.exists('/www/server/panel/data/app_login_check.pl'):return public.returnMsg(False,'Invalid login key3') + key, init_time, tid2, status = public.readFile('/www/server/panel/data/app_login_check.pl').split(':') + if session_id!=key:return public.returnMsg(False,'Invalid login key4') + if tid != tid2: return public.returnMsg(False, 'The specified key does not exist or has expired5') + if time.time() - float(init_time) > 60: + return public.returnMsg(False, 'QR code validity time expired6') + cache.set(session_id,public.md5(uuid.UUID(int=uuid.getnode()).hex),120) + import uuid + data = key + ':' + init_time + ':' + tid2 + ':' + uuid.UUID(int=uuid.getnode()).hex[-12:] + public.writeFile("/www/server/panel/data/app_login_check.pl", data) + return public.return_msg_gettext(True,'Scan code successfully, log in!') + except: + os.remove("/www/server/panel/data/app_login_check.pl") + return public.return_msg_gettext(False, 'Invalid login key') + + def get_api_config(self): + tmp = public.ReadFile(self.save_path) + if not tmp or not os.path.exists(self.save_path): + data = { "open":False, "token":"", "limit_addr":[] } + public.WriteFile(self.save_path,json.dumps(data)) + public.ExecShell("chmod 600 " + self.save_path) + tmp = public.ReadFile(self.save_path) + data = json.loads(tmp) + + is_save = False + if not 'binds' in data: + data['binds'] = [] + is_save = True + + if not 'apps' in data: + data['apps'] = [] + is_save = True + + data['binds'] = sorted(data['binds'],key=lambda x: x['time'],reverse=True) + if len(data['binds']) > 5: + data['binds'] = data['binds'][:5] + is_save = True + + if is_save: + self.save_api_config(data) + return data + + def save_api_config(self,data): + public.WriteFile(self.save_path,json.dumps(data)) + public.set_mode(self.save_path,'600') + return True + + def check_bind(self,args): + if not 'bind_token' in args or not 'client_brand' in args or not 'client_model' in args: + return 0 + if not args.client_brand or not args.client_model: + return 'Invalid device' + + bind = self.get_bind_token(args.bind_token) + if bind['token'] != args.bind_token: + return public.get_msg_gettext('The current QR code has expired, please refresh the page and rescan the code!') + + apps = self.get_apps() + if len(apps) >= self.max_bind: + return public.get_msg_gettext('This server is bound to a maximum of {} devices, which has reached the limit!',(self.max_bind,)) + + bind['status'] = 1 + bind['brand'] = args.client_brand + bind['model'] = args.client_model + self.set_bind_token(bind) + return 1 + + def get_bind_status(self,args): + if not public.cache_get(public.Md5(os.uname().version)): + public.cache_set(public.Md5(os.uname().version),1,60) + bind = self.get_bind_token(args.bind_token) + return bind + + def get_app_bind_status(self,args): + if not 'bind_token' in args: + return 0 + if self.get_app_find(args.bind_token): + return 1 + return 0 + + def set_bind_token(self,bind): + data = self.get_api_config() + is_save = False + for i in range(len(data['binds'])): + if data['binds'][i]['token'] == bind['token']: + data['binds'][i] = bind + is_save = True + break + if is_save: + self.save_api_config(data) + return True + + + def get_apps(self,args = None): + data = self.get_api_config() + return data['apps'] + + def get_app_find(self,bind_token): + apps = self.get_apps() + for s_app in apps: + if s_app['token'] == bind_token: + return s_app + return None + + def add_bind_app(self,args): + bind = self.get_bind_token(args.bind_token) + if bind['status'] == 0: + return public.return_msg_gettext(False,'Failed verification!') + apps = self.get_apps() + if len(apps) >= self.max_bind: + return public.return_msg_gettext(False,'A server allows up to {} device bindings!'.format(self.max_bind)) + + args.bind_app = args.bind_token + self.remove_bind_app(args) + data = self.get_api_config() + data['apps'].append(bind) + self.save_api_config(data) + self.remove_bind_token(args.bind_token) + return public.return_msg_gettext(True,'Bind successfully!') + + def remove_bind_token(self,bind_token): + data = self.get_api_config() + tmp_binds = [] + for s_bind in data['binds']: + if bind_token == s_bind['token']: + continue + tmp_binds.append(s_bind) + data['binds'] = tmp_binds + self.save_api_config(data) + + def remove_bind_app(self,args): + data = self.get_api_config() + tmp_apps = [] + for s_app in data['apps']: + if args.bind_app == s_app['token']: + continue + tmp_apps.append(s_app) + data['apps'] = tmp_apps + self.save_api_config(data) + s_file = '/dev/shm/{}'.format(args.bind_app) + if os.path.exists(s_file): + os.remove(s_file) + return public.return_msg_gettext(True,'Successfully deleted!') + + def get_bind_token(self,token = None): + data = self.get_api_config() + s_time = time.time() + binds = [] + bind = None + is_write = False + for i in range(len(data['binds'])): + if s_time - data['binds'][i]['time'] > self.timeout: + is_write = True + continue + binds.append(data['binds'][i]) + if token: + if token == data['binds'][i]['token']: + bind = data['binds'][i] + else: + if not bind: + bind = data['binds'][i] + if not bind: + if len(binds) > 0: + binds = sorted(binds,key=lambda x: x['time'],reverse=True) + bind = binds[0] + else: + bind = {"time":s_time,"token":public.GetRandomString(18),'status':0} + binds.append(bind) + is_write = True + + if is_write: + data['binds'] = binds + self.save_api_config(data) + return bind + + + def set_token(self,get): + if 'request_token' in get: return public.return_msg_gettext(False,'Cannot configure API through API interface') + data = self.get_api_config() + if get.t_type == '1': + token = public.GetRandomString(32) + data['token'] = public.md5(token) + data['token_crypt'] = public.en_crypt(data['token'],token).decode('utf-8') + public.write_log_gettext('API configuration','Regenerate API-Token') + public.add_security_logs('API configuration','Regenerate API-Token') + elif get.t_type == '2': + data['open'] = not data['open'] + stats = {True:'Open',False:'Close'} + if not 'token_crypt' in data: + token = public.GetRandomString(32) + data['token'] = public.md5(token) + data['token_crypt'] = public.en_crypt(data['token'],token).decode('utf-8') + public.write_log_gettext('API configuration','{} API interface',(stats[data['open']],)) + public.add_security_logs('API configuration', '{} API interface', (stats[data['open']],)) + token = stats[data['open']] + ' success!' + elif get.t_type == '3': + data['limit_addr'] = get.limit_addr.split('\n') + public.write_log_gettext('API configuration','Change IP limit to [{}]',(get.limit_addr,)) + public.add_security_logs('API configuration', 'Change IP limit to [{}]', (get.limit_addr,)) + token ='Saved successfully!' + self.save_api_config(data) + return public.return_msg_gettext(True,token) + + def get_tmp_token(self,get): + if not 'request_token' in get: return public.return_msg_gettext(False,'Temporary keys can only be obtained through the API interface') + data = self.get_api_config() + data['tmp_token'] = public.GetRandomString(64) + data['tmp_time'] = time.time() + self.save_api_config(data) + return public.return_msg_gettext(True,data['tmp_token']) diff --git a/class_v2/panel_auth_v2.py b/class_v2/panel_auth_v2.py new file mode 100644 index 00000000..07bb202d --- /dev/null +++ b/class_v2/panel_auth_v2.py @@ -0,0 +1,781 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2019 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------ +# AUTH验证接口 +# ------------------------------ + +import public, time, json, os, requests +from BTPanel import session, cache +from public.validate import Param + + +class panelAuth: + __request_url = None + __product_list_path = 'data/product_list.pl' + __product_bay_path = 'data/product_bay.pl' + __product_id = '100000011' + __official_url = 'https://www.aapanel.com' + # __official_url = 'http://dev.aapanel.com' + __failed_connect_server='Failed to connect to the server!' + + def create_serverid(self, get): + try: + userPath = 'data/userInfo.json' + if not os.path.exists(userPath): + return public.return_message(-1, 0, 'Please login with account first') + tmp = public.readFile(userPath) + if len(tmp) < 2: tmp = '{}' + data = json.loads(tmp) + data['uid'] = data['id'] + if not data: + return public.return_message(-1, 0, 'Please login with account first') + if not 'server_id' in data: + s1 = public.get_mac_address() + public.get_hostname() + s2 = self.get_cpuname() + serverid = public.md5(s1) + public.md5(s2) + data['server_id'] = serverid + public.writeFile(userPath, json.dumps(data)) + return data + except: + return public.return_message(-1, 0,'Please login with account first') + + + def create_plugin_other_order(self, get): + pdata = self.create_serverid(get) + pdata['pid'] = get.pid + pdata['cycle'] = get.cycle + p_url = public.GetConfigValue('home') + '/api/Pluginother/create_order' + if get.type == '1': + pdata['renew'] = 1 + p_url = public.GetConfigValue('home') + '/api/Pluginother/renew_order' + return public.return_message(0, 0, json.loads(public.httpPost(p_url,pdata))) + + def get_order_stat(self, get): + pdata = self.create_serverid(get) + pdata['order_id'] = get.oid + p_url = public.GetConfigValue('home') + '/api/Pluginother/order_stat' + if get.type == '1': p_url = public.GetConfigValue('home') + '/api/Pluginother/re_order_stat' + return public.return_message(0, 0, json.loads(public.httpPost(p_url,pdata))) + + def check_serverid(self, get): + if get.serverid != self.create_serverid(get): return public.return_message(-1,0,False) + return public.return_message(0,0,True) + + # 获取价格列表 新增多机购买 + def get_plugin_price(self, get): + # 校验参数 + try: + get.validate([ + Param('product_id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + userPath = 'data/userInfo.json' + if not 'pluginName' in get and not 'product_id' in get: return public.return_message(-1, 0,'Parameter ERROR!') + if not os.path.exists(userPath): return public.return_message(-1, 0,'Please login with account first') + params = {} + if not hasattr(get, 'product_id'): + params['product_id'] = self.get_plugin_info(get.pluginName)['id'] + else: + params['product_id'] = get.product_id + data = self.send_cloud('{}/api/product/pricesV3'.format(self.__official_url), params) + if data['status']==-1: + return data + data=data['message'] + if not data: + return public.return_message(-1, 0, 'Please log in to your aaPanel account on the panel first!') + if not data['success']: + return public.return_message(-1, 0,data['msg']) + # if len(data['res']) == 6: + # return data['res'][3:] + return public.return_message(0, 0,data['res']) + except: + del(session['get_product_list']) + return public.return_message(-1, 0,'Syncing information, please try again!\n {}',(public.get_error_info(),)) + + def get_plugin_info(self, pluginName): + data = self.get_business_plugin(None) + if data['status']==-1: return None + for d in data['message']: + if d['name'] == pluginName: return d + return None + + def get_plugin_list(self, get): + try: + if not session.get('get_product_bay') or not os.path.exists(self.__product_bay_path): + data = self.send_cloud('get_order_list_byuser', {}) + if data['status']==-1:return data + if data['message']: public.writeFile(self.__product_bay_path, json.dumps(data['message'])) + session['get_product_bay'] = True + data = json.loads(public.readFile(self.__product_bay_path)) + return public.return_message(0,0,data) + except: + return public.return_message(-1,0,None) + + def get_buy_code(self, get): + + # 校验参数 + try: + get.validate([ + Param('pid').Integer(), + Param('cycle').Integer(), + Param('source').Integer(), + Param('num').Integer(), + Param('charge_type').Integer(), + Param('src').Integer(), + Param('is_ipv6').Integer(), + Param('coupon_id').Integer(), + Param('cycle_unit').String(), + Param('pay_channel').String(), + Param('ip').String(), + Param('os').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + cycle = getattr(get, 'cycle', 1) + params = {} + params['cycle'] = cycle + + params['product_id'] = get.pid + params['src'] = 2 + params['trigger_entry'] = get.source + params['pay_channel'] = 2 + # 0.管理后台生成 1.Ping++ 2.Stripe 3.Paypal 10.抵扣券 + if hasattr(get, 'coupon_id'): + params['coupon_id'] = get.coupon_id + if hasattr(get, 'pay_channel'): + params['pay_channel'] = get.pay_channel + #优惠券检测 + if get.pay_channel==10 and not hasattr(get, 'coupon_id'): + return public.return_message(-1, 0, 'parameter error: coupon_id') + if hasattr(get, 'charge_type'): + params['charge_type'] = get.charge_type + if int(get.charge_type) == 1: + if not hasattr(get, 'cycle_unit'):return public.return_message(-1, 0, 'parameter error: cycle_unit') + params['cycle_unit'] = get.cycle_unit + + + env_info = public.fetch_env_info() + params['environment_info'] = json.dumps(env_info) + params['server_id'] = env_info['install_code'] + # 多机购买 数量 + if not hasattr(get, 'num'): + return public.return_message(-1, 0, 'parameter error: num') + params['num'] = get.num + + # 添加购买来源 + # params['source'] = get.source + + data = self.send_cloud('{}/api/order/product/create'.format(self.__official_url), params) + if data['status']==-1: + return data + return public.return_message(0, 0, data['message']['res']) + + def get_stripe_session_id(self, get): + + params = {} + if hasattr(get, 'order_no'): + params['order_no'] = get.order_no + if hasattr(get, 'order_id'): + params['order_id'] = get.order_id + + if hasattr(get, 'subscribe'): + params['subscribe'] = get.subscribe + + if not params.get('order_no', None) and not params.get('order_id', None): + return public.return_message(-1, 0,'parameter error') + + data = self.send_cloud('{}/api/order/product/pay'.format(self.__official_url), params) + session['focre_cloud'] = True + return public.return_message(0, 0, data['message']['res']) + + # paypal支付 + def get_paypal_session_id(self, get): + + params = {} + if hasattr(get, 'oid'): + params['oid'] = get.oid + + if not params.get('oid', None): + return public.return_message(-1, 0, 'parameter error') + + data = self.send_cloud('{}/api/paypal/create_order'.format(self.__official_url), params) + if data['status']==-1: + return data + session['focre_cloud'] = True + data=data['message'] + data2 = { + "status": data.get("success", False), + "res": data.get("res", ""), + "nonce": data.get("nonce", 0), + } + + return public.return_message(0, 0, data2) + + # paypal 支付确认 + def check_paypal_status(self, get): + + params = {} + if hasattr(get, 'paypal_order_id'): + params['paypal_order_id'] = get.paypal_order_id + + if not params.get('paypal_order_id', None): + return public.return_message(-1, 0,'parameter error') + + data = self.send_cloud('{}/api/paypal/capture_order'.format(self.__official_url), params) + data=data['message'] + status_code=-1 + status=data.get("success", False) + if status: + status_code=0 + # 刷新授权状态 + public.load_soft_list() + public.refresh_pd() + data2 = { + "res": data.get("res", ""), + "nonce": data.get("nonce", 0), + } + + return public.return_message(status_code, 0, data2) + + def check_pay_status(self, get): + params = {} + params['id'] = get.id + data = self.send_cloud('check_product_pays', params) + if data['status']==-1:return data + data=data['message'] + if not data: return public.return_message(-1, 0,'Fail to connect to the server!') + if data['status'] == True: + self.flush_pay_status(get) + if 'get_product_bay' in session: del (session['get_product_bay']) + return public.return_message(0, 0, data) + + def flush_pay_status(self, get): + if 'get_product_bay' in session: del (session['get_product_bay']) + data = self.get_plugin_list(get) + if data['status']==-1: return public.return_message(-1, 0,'Fail to connect to the server!') + return public.return_message(0, 0, 'Flush status success') + + def get_renew_code(self): + pass + + def check_renew_code(self): + pass + + def get_business_plugin(self, get): + try: + if not session.get('get_product_list') or not os.path.exists(self.__product_list_path): + data = self.send_cloud('{}/api/product/chargeProducts'.format(self.__official_url), {}) + if data['message']['success']: public.writeFile(self.__product_list_path, json.dumps(data['message']['res'])) + session['get_product_list'] = True + data = json.loads(public.readFile(self.__product_list_path)) + return public.return_message(0,0,data) + except: + return public.return_message(-1,0,None) + + def get_ad_list(self): + pass + + def check_plugin_end(self): + pass + + def get_re_order_status_plugin(self, get): + params = {} + params['pid'] = getattr(get, 'pid', 0) + data = self.send_cloud('get_re_order_status', params) + if data['status']==-1:return data + data=data['message'] + if not data: return public.return_message(-1, 0,'Fail to connect to the server!') + if data['status'] == True: + self.flush_pay_status(get) + if 'get_product_bay' in session: del (session['get_product_bay']) + return public.return_message(0, 0, data) + + def get_voucher_plugin(self, get): + # 校验参数 + try: + get.validate([ + Param('pid').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + params = {} + params['product_id'] = getattr(get, 'pid', 0) + params['status'] = '0' + data = self.send_cloud('{}/api/user/productVouchers'.format(self.__official_url), params) + if data['status']==-1:return data + if not data['message']: + return public.return_message(0, 0,[]) + return public.return_message(0, 0,data['message']['res']) + + def create_order_voucher_plugin(self, get): + # 校验参数 + try: + get.validate([ + Param('cycle_unit').String(), + Param('pid').Integer(), + Param('coupon_id').Integer(), + Param('cycle').Integer(), + Param('charge_type').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + cycle = getattr(get, 'cycle', '1') + params = {} + params['cycle'] = cycle + params['cycle_unit'] = get.cycle_unit + params['coupon_id'] = get.coupon_id + params['src'] = 2 + params['pay_channel'] = 10 + params['charge_type'] = get.charge_type + env_info = public.fetch_env_info() + params['environment_info'] = json.dumps(env_info) + params['server_id'] = env_info['install_code'] + data = self.send_cloud('{}/api/order/product/create'.format(self.__official_url), params) + session['focre_cloud'] = True + if data['message']['success']: + # 刷新授权状态 + public.load_soft_list() + public.refresh_pd() + return public.return_message(0, 0,'Activate successfully') + + return public.return_message(-1, 0, 'Activate failed') + + def send_cloud(self, cloudURL, params): + try: + userInfo = self.create_serverid(None) + if 'token' not in userInfo: + return public.return_message(-1,0,None) + url_headers = {"Content-Type": "application/json", + "authorization": "bt {}".format(userInfo['token']) + } + resp = requests.post(cloudURL, params=params, headers=url_headers) + resp_json = resp.json() + if resp.status_code != 200 or not resp_json.get('success', False): + # 当接口错误信息存在时,返回接口错误信息 + if isinstance(resp_json.get('res', None), str): + raise public.HintException(str(resp_json['res'])) + # return public.return_message(-1, 0, str(resp_json['res'])) + + # 否则统一返回连接服务器失败 + return public.return_message(-1, 0, self.__failed_connect_server) + # if not resp['res']: + # public.print_log('not res:') + # return public.return_message(-1,0,self.__failed_connect_server) + return public.return_message(0, 0, resp_json) + except public.HintException: + raise + except Exception: + return public.return_message(-1,0,self.__failed_connect_server) + + def send_cloud_v2(self, cloudURL, params): + try: + userInfo = self.create_serverid(None) + if 'token' not in userInfo: + return public.return_message(-1,0,None) + url_headers = {"Content-Type": "application/json", + "authorization": "bt {}".format(userInfo['token']) + } + resp = requests.post(cloudURL, params=params, headers=url_headers) + resp = resp.json() + if not resp or 'res' not in resp: + return public.return_message(-1,0,self.__failed_connect_server) + if not resp['res']: + return public.return_message(-1,0,None) + if resp['success'] == False: + return public.return_message(-1,0,self.__failed_connect_server) + return public.return_message(0,0,resp['res']) + except: + return public.return_message(-1,0,self.__failed_connect_server) + + + def send_cloud_v3(self, module, params): + userInfo = self.create_serverid(None); + if 'status' in userInfo: + params['uid'] = 0 + params['serverid'] = '' + else: + params['uid'] = userInfo['uid'] + params['serverid'] = userInfo['serverid'] + params['access_key'] = userInfo['access_key'] + params['os'] = 'Windows' + data = self.send_cloud_pro_2('obtain_coupons', params) + return data + + + def send_cloud_get(self, cloudURL, params): + try: + userInfo = self.create_serverid(None) + if 'token' not in userInfo: + return public.return_message(-1,0,None) + url_headers = {"Content-Type": "application/json", + "authorization": "bt {}".format(userInfo['token']) + } + resp = requests.get(cloudURL,headers=url_headers, stream=True).json() + if not resp or 'res' not in resp: return public.return_message(-1,0,self.__failed_connect_server) + if resp['success'] == False: + return public.return_message(-1,0,self.__failed_connect_server) + return public.return_message(0,0,resp['res']) + except: + return public.return_message(-1,0,self.__failed_connect_server) + + def send_cloud_pro(self, module, params): + try: + cloudURL = '{}/api/order/product/'.format(self.__official_url) + userInfo = self.create_serverid(None) + params['os'] = 'Linux' + if 'status' in userInfo: + params['server_id'] = '' + else: + params['server_id'] = userInfo['server_id'] + url_headers = {"authorization": "bt {}".format(userInfo['token'])} + resp = requests.post(cloudURL, params=params, headers=url_headers) + resp = resp.json()['res'] + if not resp: return None + return resp + except: + return None + + + def send_cloud_pro_2(self, module, params): + try: + cloudURL = '{}/api/user/{}'.format(self.__official_url,module) + userInfo = self.create_serverid(None) + params['os'] = 'Linux' + if 'status' in userInfo: + params['server_id'] = '' + else: + params['server_id'] = userInfo['server_id'] + url_headers = {"authorization": "bt {}".format(userInfo['token'])} + resp = requests.post(cloudURL, params=params, headers=url_headers) + resp = resp.json() + if not resp or 'res' not in resp: + return public.return_message(-1,0,self.__failed_connect_server) + if not resp['res']: return public.return_message(-1,0,self.__failed_connect_server) + if resp['success'] == False: + return public.return_message(-1,0,self.__failed_connect_server) + return public.return_message(0,0,resp['res']) + except: + return public.return_message(-1,0,self.__failed_connect_server) + + def get_voucher(self, get): + params = {} + params['product_id'] = self.__product_id + params['status'] = '0' + data = self.send_cloud_pro('get_voucher', params) + return data + + def get_order_status(self, get): + params = {} + data = self.send_cloud_pro('get_order_status', params) + return data + + def get_product_discount_by(self, get): + params = {} + data = self.send_cloud_pro('get_product_discount_by', params) + return data + + def get_re_order_status(self, get): + params = {} + data = self.send_cloud_pro('get_re_order_status', params) + return data + + def create_order_voucher(self, get): + code = getattr(get, 'code', '1') + params = {} + params['code'] = code + data = self.send_cloud_pro('create_order_voucher', params) + public.return_message(0, 0, data) + + def create_order(self, get): + cycle = getattr(get, 'cycle', '1') + params = {} + params['cycle'] = cycle + params['cycle'] = 'month' + params['product_id'] = 100000012 + params['src'] = 2 + params['pay_channel'] = 2 + params['charge_type'] = 1 + params['environment_info'] = json.dumps(public.fetch_env_info()) + data = self.send_cloud_pro('create', params) + return data + + def get_cpuname(self): + return public.ExecShell("cat /proc/cpuinfo|grep 'model name'|cut -d : -f2")[0].strip() + + def get_product_auth(self, get): + # 校验参数 + try: + get.validate([ + Param('pid').Integer(), + Param('page').Integer(), + Param('pageSize').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + params = {} + params['page'] = get.page if 'page' in get else 1 + params['pageSize'] = get.pageSize if 'pageSize' in get else 15 + data = self.send_cloud('{}/api/user/productAuthorizes'.format(self.__official_url), params) + if not data: + return public.return_message(0, 0,[]) + if 'success' not in data['message']: return public.return_message(0, 0,[]) + data = data['message']['res'] + # return [i for i in data['list'] if i['status'] != 'activated' and get.pid == i['product_id']] + res = list() + for i in data['list']: + if i['status'] != 'activated' and str(get.pid) == str(i['product_id']): + res.append(i) + return public.return_message(0, 0,res) + + def auth_activate(self, get): + params = {} + params['serial_no'] = get.serial_no + params['environment_info'] = json.dumps(public.fetch_env_info()) + data = self.send_cloud('{}/api/authorize/product/activate'.format(self.__official_url), params) + if 'success' not in data['message'] or not data['message']['success']: + return public.return_message(-1, 0,'Activate Failed') + session['focre_cloud'] = True + # 刷新授权状态 + public.load_soft_list() + public.refresh_pd() + return public.return_message(0, 0,'Activate successfully') + + def renew_product_auth(self, get): + params = {} + params['serial_no'] = get.serial_no + params['pay_channel'] = get.pay_channel + params['cycle'] = get.cycle + params['cycle_unit'] = get.cycle_unit + params['src'] = 2 + params['trigger_entry'] = get.source + params['environment_info'] = json.dumps(public.fetch_env_info()) + if hasattr(get, 'coupon_id') and get.pay_channel == '10': + params['coupon_id'] = get.coupon_id + data = self.send_cloud('{}/api/authorize/product/renew'.format(self.__official_url), params) + + if not data['message']['success']: + data['message']['res'] = 'Invalid authorize OR authorize not found!' + return public.return_message(-1, 0, data['message']['res']) + session['focre_cloud'] = True + # 使用抵扣券续费直接返回续费结果 + if get.pay_channel == '10': + if not data['message']['success']: + return public.return_message(-1, 0, 'Renew Failed') + # 刷新授权状态 + public.load_soft_list() + public.refresh_pd() + return public.return_message(0, 0, 'Renew successfully') + # 使用支付续费返回stripe的请求数据 + return public.return_message(0, 0,data['message']['res']) + + def free_trial(self, get): + """ + 每个账号有一次免费试用专业版15天的机会 + :return: + """ + params = {} + params['environment_info'] = json.dumps(public.fetch_env_info()) + data = self.send_cloud('{}/api/product/obtainProfessionalMemberFree'.format(self.__official_url), params) + session['focre_cloud'] = True + # 使用抵扣券续费直接返回续费结果 + if not data['message']['success']: + return public.return_message(-1, 0, 'Apply Failed') + return public.return_message(0, 0, 'Apply successfully') + + # 获取专业版特权信息 或插件信息? + def get_plugin_remarks(self, get): + # 校验参数 + try: + get.validate([ + Param('product_id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + if not hasattr(get, 'product_id'): + return public.return_message(-1, 0, 'product_id Parameter ERROR!') + product_id = get.product_id + + # ikey = 'plugin_remarks' + product_id + # if ikey in session: + # return session.get(ikey) + try: + url = '{}/api/panel/get_advantages/{}'.format(self.__official_url, product_id) + data = requests.get(url).json() + except: + return public.return_message(-1, 0, 'Failed to connect to the server!') + if not data: return public.return_message(-1, 0, 'Failed to connect to the server!') + # session[ikey] = data + return public.return_message(0,0,data) + + def res_request_error(self): + return public.return_message(-1,0, 'Interface request failed ({})!'.format(self.__request_url)) + + + def get_apply_copon(self, get): + """ + 领取优惠券 + @get.coupon 优惠券 + """ + # 校验参数 + try: + get.validate([ + Param('obtain_id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + params = {} + params['obtain_id']= get.obtain_id + cloudUrl=self.__official_url+'/api/user/obtain_coupons' + # data = self.send_cloud_v2('obtain_coupons', params) + data = self.send_cloud_v3(cloudUrl, params) + return data + + def get_ignore_time(self, get): + """ + 获取忽略时间 + @get.coupon 优惠券 + """ + try: + limit = int(public.readFile('data/ignore_coupon_time.pl')) + except: + limit = public.return_message(0,0,0) + + if limit == -100 or limit > time.time(): + return public.return_message(0,0,1) + return public.return_message(0,0,0) + + def get_coupon_list(self, get): + """ + 获取可用的优惠券 + @get.coupon 优惠券 + """ + params = {} + cloudUrl=self.__official_url+'/api/user/coupons' + data = self.send_cloud_get(cloudUrl, params) + return data + # if not data: + # if type(data) == list: + # return public.return_message(0,0,[]) + # return public.return_message(-1,0,self.res_request_error()) + # return public.return_message(0,0,data) + + def ignore_coupon_time(self, get): + """ + 获取优惠券 + @get.coupon 优惠券 + @get.limit_time 限制时间 永久 -100 + """ + if not hasattr(get, 'limit_time')or not get.limit_time: return public.return_message(-1,0, 'Missing parameter limit_time or parameter cannot be empty!') + limit_time = int(get.limit_time) + if limit_time < time.time() and limit_time > 0: + return public.return_message(-1,0, 'Time cannot be less than the current time!') + + public.writeFile('data/ignore_coupon_time.pl', str(limit_time)) + + msg = 'Ignoring success, coupon information will not be displayed in the future' + if limit_time > 0: msg = 'Ignoring success, coupon information will no longer be displayed to you before {}'.format(public.format_date(times=limit_time)) + return public.return_message(0,0, msg) + + def get_coupons(self, get): + """ + @name 获取可领取的优惠券列表 + @param uid 用户id + """ + # 用户是否忽略 + if self.get_ignore_time(get)['message']['result']: + return public.return_message(0,0,[]) + params = {} + cloudURL = self.__official_url+'/api/user/obtainable_coupons' + data = self.send_cloud_v2(cloudURL, params) + if not data: + return public.return_message(-1,0,None) + if data['status']==-1: + return data + return data + + + def get_all_coupons(self,get): + """ + @name 获取所有优惠券 + """ + #获取可领取的优惠券列表 + params = {} + cloudURL = self.__official_url+'/api/user/obtainable_coupons' + data = self.send_cloud_v2(cloudURL, params) + if not data['message']: + return public.return_message(-1,0,None) + if data['status']==0: + if data['message']['status'] ==1: + data['message']['interface_type']=1 + return data + else: + data['message']['interface_type']=2 + # 获取可用的优惠券列表 + params = {} + cloudUrl=self.__official_url+'/api/user/coupons' + tmp_data = self.send_cloud_get(cloudUrl, params) + data['message']['total']=0 + if isinstance(tmp_data['message'],list) and len(tmp_data['message'])>0: + data['message']['total']=len(tmp_data['message']) + data['message']['end_time']=tmp_data['message'][0]['end_time'] + return data + else: + # 接口请求失败,返回默认值 + return public.success_v2({ + 'status': 0, + 'obtain_id': 0, + 'end_time': 0, + 'type': 0, + 'coupons': [], + 'usable_coupon_num': 0, + }) + return data + + """ + @name 统一请求接口 + @param url 返回URL不是www.bt.cn,修改config/config.json的home字段 + """ + + def request_post(self, url, params): + params = {} + data = self.send_cloud_pro_2('obtainable_coupons', params) + return data diff --git a/class_v2/panel_dns_api_v2.py b/class_v2/panel_dns_api_v2.py new file mode 100644 index 00000000..06e28701 --- /dev/null +++ b/class_v2/panel_dns_api_v2.py @@ -0,0 +1,565 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 沐落 +# +------------------------------------------------------------------- +import public,os,sys,json,time,random +import requests +from OpenSSL import crypto +import sys, os +import time +import copy +import json +import base64 +import hashlib +import binascii +import urllib + +if sys.version_info[0] == 2: # python2 + import urlparse + from urlparse import urljoin + import urllib2 + import cryptography.hazmat + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization +else: # python3 + from urllib.parse import urlparse + from urllib.parse import urljoin + import cryptography +import platform +import hmac +try: + import requests +except: + public.ExecShell('btpip install requests') + import requests +try: + import OpenSSL +except: + public.ExecShell('btpip install pyOpenSSL') + import OpenSSL +import random +import datetime +import logging +from hashlib import sha1 + +os.chdir("/www/server/panel") +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import public +caa_value = '0 issue "letsencrypt.org"' + + +def extract_zone(domain_name): + domain_name = domain_name.lstrip("*.") + top_domain_list = ['.ac.cn', '.ah.cn', '.bj.cn', '.com.cn', '.cq.cn', '.fj.cn', '.gd.cn', + '.gov.cn', '.gs.cn', '.gx.cn', '.gz.cn', '.ha.cn', '.hb.cn', '.he.cn', + '.hi.cn', '.hk.cn', '.hl.cn', '.hn.cn', '.jl.cn', '.js.cn', '.jx.cn', + '.ln.cn', '.mo.cn', '.net.cn', '.nm.cn', '.nx.cn', '.org.cn','.my.id'] + old_domain_name = domain_name + top_domain = "."+".".join(domain_name.rsplit('.')[-2:]) + new_top_domain = "." + top_domain.replace(".","") + is_tow_top = False + if top_domain in top_domain_list: + is_tow_top = True + domain_name = domain_name[:-len(top_domain)] + new_top_domain + + if domain_name.count(".") > 1: + zone, middle, last = domain_name.rsplit(".", 2) + acme_txt = "_acme-challenge.%s" % zone + if is_tow_top: last = top_domain[1:] + root = ".".join([middle, last]) + else: + zone = "" + root = old_domain_name + acme_txt = "_acme-challenge" + return root, zone, acme_txt + +class BaseDns(object): + def __init__(self): + self.dns_provider_name = self.__class__.__name__ + + def log_response(self, response): + try: + log_body = response.json() + except ValueError: + log_body = response.content + return log_body + + def create_dns_record(self, domain_name, domain_dns_value): + raise NotImplementedError("create_dns_record method must be implemented.") + + def delete_dns_record(self, domain_name, domain_dns_value): + raise NotImplementedError("delete_dns_record method must be implemented.") + +class DNSPodDns(BaseDns): + dns_provider_name = "dnspod" + _type = 0 # 0:lest 1:锐成 + def __init__(self, DNSPOD_ID, DNSPOD_API_KEY, DNSPOD_API_BASE_URL="https://dnsapi.cn/"): + self.DNSPOD_ID = DNSPOD_ID + self.DNSPOD_API_KEY = DNSPOD_API_KEY + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + self.HTTP_TIMEOUT = 65 # seconds + self.DNSPOD_LOGIN = "{0},{1}".format(self.DNSPOD_ID, self.DNSPOD_API_KEY) + + if DNSPOD_API_BASE_URL[-1] != "/": + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + "/" + else: + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + super(DNSPodDns, self).__init__() + + def create_dns_record(self, domain_name, domain_dns_value): + domain_name,_,subd = extract_zone(domain_name) + if self._type == 1: + self.add_record(domain_name,subd.replace('_acme-challenge.',''),domain_dns_value,'CNAME') + else: + self.add_record(domain_name,subd,domain_dns_value,'TXT') + + + + def add_record(self,domain_name,subd,domain_dns_value,s_type): + url = urljoin(self.DNSPOD_API_BASE_URL, "Record.Create") + body = { + "record_type": s_type, + "domain": domain_name, + "sub_domain": subd, + "value": domain_dns_value, + "record_line_id": "0", + "format": "json", + "login_token": self.DNSPOD_LOGIN, + } + create_dnspod_dns_record_response = requests.post( + url, data=body, timeout=self.HTTP_TIMEOUT + ).json() + if create_dnspod_dns_record_response["status"]["code"] != "1": + raise ValueError( + "Error creating dnspod dns record: status_code={status_code} response={response}".format( + status_code=create_dnspod_dns_record_response["status"]["code"], + response=create_dnspod_dns_record_response["status"]["message"], + ) + ) + + + def remove_record(self,domain_name,subd,s_type): + url = urljoin(self.DNSPOD_API_BASE_URL, "Record.List") + rootdomain = domain_name + body = { + "login_token": self.DNSPOD_LOGIN, + "format": "json", + "domain": rootdomain, + "subdomain": subd, + "record_type": s_type, + } + list_dns_response = requests.post(url, data=body, timeout=self.HTTP_TIMEOUT).json() + for i in range(0, len(list_dns_response["records"])): + if list_dns_response["records"][i]['name'] != subd: + continue + rid = list_dns_response["records"][i]["id"] + urlr = urljoin(self.DNSPOD_API_BASE_URL, "Record.Remove") + bodyr = { + "login_token": self.DNSPOD_LOGIN, + "format": "json", + "domain": rootdomain, + "record_id": rid, + } + requests.post( + urlr, data=bodyr, timeout=self.HTTP_TIMEOUT + ).json() + + def delete_dns_record(self, domain_name, domain_dns_value): + try: + domain_name,_,subd = extract_zone(domain_name) + self.remove_record(domain_name,subd,'TXT') + self.remove_record(domain_name,'_acme-challenge','CNAME') + except: + pass + + + +class CloudFlareDns(BaseDns): + dns_provider_name = "cloudflare" + _type = 0 # 0:lest 1:锐成 + def __init__( + self, + CLOUDFLARE_EMAIL, + CLOUDFLARE_API_KEY, + CLOUDFLARE_API_BASE_URL="https://api.cloudflare.com/client/v4/", + ): + self.CLOUDFLARE_DNS_ZONE_ID = None + self.CLOUDFLARE_EMAIL = CLOUDFLARE_EMAIL + self.CLOUDFLARE_API_KEY = CLOUDFLARE_API_KEY + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + self.HTTP_TIMEOUT = 65 # seconds + + try: + import urllib.parse as urlparse + except: + import urlparse + + if CLOUDFLARE_API_BASE_URL[-1] != "/": + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + "/" + else: + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + super(CloudFlareDns, self).__init__() + + def get_headers(self): + if os.path.exists('/www/server/panel/data/cf_limit_api.pl'): + headers = {"Authorization": "Bearer "+self.CLOUDFLARE_API_KEY} + else: + headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + return headers + + def find_dns_zone(self, domain_name): + url = urljoin(self.CLOUDFLARE_API_BASE_URL, "zones?status=active&name={0}".format(domain_name)) + headers = self.get_headers() + find_dns_zone_response = requests.get(url, headers=headers, timeout=self.HTTP_TIMEOUT) + if find_dns_zone_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=find_dns_zone_response.status_code, + response=self.log_response(find_dns_zone_response), + ) + ) + + result = find_dns_zone_response.json()["result"] + for i in result: + if i["name"] in domain_name: + setattr(self, "CLOUDFLARE_DNS_ZONE_ID", i["id"]) + if isinstance(self.CLOUDFLARE_DNS_ZONE_ID, type(None)): + raise ValueError( + "Error unable to get DNS zone for domain_name={domain_name}: status_code={status_code} response={response}".format( + domain_name=domain_name, + status_code=find_dns_zone_response.status_code, + response=self.log_response(find_dns_zone_response), + ) + ) + + def add_record(self,domain_name,value,s_type): + url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID), + ) + # if '_' in self.CLOUDFLARE_API_KEY or '-' in self.CLOUDFLARE_API_KEY: + # headers = {"Authorization": "Bearer "+self.CLOUDFLARE_API_KEY} + # else: + # headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + headers = self.get_headers() + body = { + "type": s_type, + "name": domain_name, + "content": "{0}".format(value), + } + + create_cloudflare_dns_record_response = requests.post( + url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT + ) + if create_cloudflare_dns_record_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=create_cloudflare_dns_record_response.status_code, + response=self.log_response(create_cloudflare_dns_record_response), + ) + ) + + def create_dns_record(self, domain_name, domain_dns_value): + domain_name = domain_name.lstrip("*.") + self.find_dns_zone(domain_name) + + url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID), + ) + # if '_' in self.CLOUDFLARE_API_KEY or '-' in self.CLOUDFLARE_API_KEY: + # headers = {"Authorization": "Bearer "+self.CLOUDFLARE_API_KEY} + # else: + # headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + headers = self.get_headers() + body = { + "type": "TXT", + "name": "_acme-challenge" + "." + domain_name + ".", + "content": "{0}".format(domain_dns_value), + } + + if self._type == 1: + body['type'] = 'CNAME' + root, _, acme_txt = extract_zone(domain_name) + body['name'] = acme_txt.replace('_acme-challenge.','') + + create_cloudflare_dns_record_response = requests.post( + url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT + ) + if create_cloudflare_dns_record_response.status_code != 200: + # raise error so that we do not continue to make calls to ACME + # server + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=create_cloudflare_dns_record_response.status_code, + response=self.log_response(create_cloudflare_dns_record_response), + ) + ) + + + def remove_record(self,domain_name,dns_name,s_type): + # if '_' in self.CLOUDFLARE_API_KEY or '-' in self.CLOUDFLARE_API_KEY: + # headers = {"Authorization": "Bearer "+self.CLOUDFLARE_API_KEY} + # else: + # headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + headers = self.get_headers() + list_dns_payload = {"type": s_type, "name": dns_name} + list_dns_url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID), + ) + + list_dns_response = requests.get( + list_dns_url, params=list_dns_payload, headers=headers, timeout=self.HTTP_TIMEOUT + ) + + for i in range(0, len(list_dns_response.json()["result"])): + dns_record_id = list_dns_response.json()["result"][i]["id"] + url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records/{1}".format(self.CLOUDFLARE_DNS_ZONE_ID, dns_record_id), + ) + headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + requests.delete( + url, headers=headers, timeout=self.HTTP_TIMEOUT + ) + + def delete_dns_record(self, domain_name, domain_dns_value): + domain_name = domain_name.lstrip("*.") + dns_name = "_acme-challenge" + "." + domain_name + self.remove_record(domain_name,dns_name,'TXT') + + + +class AliyunDns(object): + _type = 0 # 0:lest 1:锐成 + def __init__(self, key, secret, ): + self.key = str(key).strip() + self.secret = str(secret).strip() + self.url = "http://alidns.aliyuncs.com" + + def sign(self, accessKeySecret, parameters): # '''签名方法 + def percent_encode(encodeStr): + encodeStr = str(encodeStr) + if sys.version_info[0] == 3: + import urllib.request + res = urllib.request.quote(encodeStr, '') + else: + res = urllib2.quote(encodeStr, '') + res = res.replace('+', '%20') + res = res.replace('*', '%2A') + res = res.replace('%7E', '~') + return res + + sortedParameters = sorted(parameters.items(), key=lambda parameters: parameters[0]) + canonicalizedQueryString = '' + for (k, v) in sortedParameters: + canonicalizedQueryString += '&' + percent_encode(k) + '=' + percent_encode(v) + stringToSign = 'GET&%2F&' + percent_encode(canonicalizedQueryString[1:]) + if sys.version_info[0] == 2: + h = hmac.new(accessKeySecret + "&", stringToSign, sha1) + else: + h = hmac.new(bytes(accessKeySecret + "&", encoding="utf8"), stringToSign.encode('utf8'), sha1) + signature = base64.encodestring(h.digest()).strip() + return signature + + + def create_dns_record(self, domain_name, domain_dns_value): + root, _, acme_txt = extract_zone(domain_name) + self.delete_dns_record(domain_name, domain_dns_value) + if self._type == 1: + acme_txt = acme_txt.replace('_acme-challenge.','') + self.add_record(root,'CNAME',acme_txt,domain_dns_value) + else: + try: + self.add_record(root,'CAA','@',caa_value) + except: pass + self.add_record(root,'TXT',acme_txt,domain_dns_value) + + def add_record(self,domain,s_type,host,value): + randomint = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + paramsdata = { + "Action": "AddDomainRecord", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key, + "DomainName": domain, + "RR": host, + "Type": s_type, + "Value": value, + } + + Signature = self.sign(self.secret, paramsdata) + paramsdata['Signature'] = Signature + req = requests.get(url=self.url, params=paramsdata) + if req.status_code != 200: + if req.json()['Code'] == 'IncorrectDomainUser' or req.json()['Code'] == 'InvalidDomainName.NoExist': + raise ValueError("This domain name does not exist under this Ali cloud account. Adding parsing failed.") + elif req.json()['Code'] == 'InvalidAccessKeyId.NotFound' or req.json()['Code'] == 'SignatureDoesNotMatch': + raise ValueError("API key error, add parsing failed") + else: + raise ValueError(req.json()['Message']) + + def query_recored_items(self, host, zone=None, tipe=None, page=1, psize=200): + randomint = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + paramsdata = { + "Action": "DescribeDomainRecords", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key, + "DomainName": host, + } + if zone: + paramsdata['RRKeyWord'] = zone + if tipe: + paramsdata['TypeKeyWord'] = tipe + Signature = self.sign(self.secret, paramsdata) + paramsdata['Signature'] = Signature + req = requests.get(url=self.url, params=paramsdata) + return req.json() + + def query_recored_id(self, root, zone, tipe="TXT"): + record_id = None + recoreds = self.query_recored_items(root, zone, tipe=tipe) + recored_list = recoreds.get("DomainRecords", {}).get("Record", []) + recored_item_list = [i for i in recored_list if i["RR"] == zone] + if len(recored_item_list): + record_id = recored_item_list[0]["RecordId"] + return record_id + + def remove_record(self,domain,host,s_type = 'TXT'): + record_id = self.query_recored_id(domain,host,s_type) + if not record_id: + msg = "Cannot find record_id for domain name: ", domain + print(msg) + return + randomint = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + paramsdata = { + "Action": "DeleteDomainRecord", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key, + "RecordId": record_id, + } + Signature = self.sign(self.secret, paramsdata) + paramsdata['Signature'] = Signature + req = requests.get(url=self.url, params=paramsdata) + if req.status_code != 200: + raise ValueError("Deleting a parse record failed") + + def delete_dns_record(self, domain_name, domain_dns_value): + root, _, acme_txt = extract_zone(domain_name) + self.remove_record(root,acme_txt,'TXT') + self.remove_record(root,'@','CAA') + self.remove_record(root,'_acme-challenge','CNAME') + +class CloudxnsDns(object): + def __init__(self, key, secret, ): + self.key = key + self.secret = secret + self.APIREQUESTDATE = time.ctime() + + def get_headers(self, url, parameter=''): + APIREQUESTDATE = self.APIREQUESTDATE + APIHMAC = public.Md5(self.key + url + parameter + APIREQUESTDATE + self.secret) + headers = { + "API-KEY": self.key, + "API-REQUEST-DATE": APIREQUESTDATE, + "API-HMAC": APIHMAC, + "API-FORMAT": "json" + } + return headers + + def get_domain_list(self): + url = "https://www.cloudxns.net/api2/domain" + headers = self.get_headers(url) + req = requests.get(url=url, headers=headers,verify=False) + req = req.json() + + return req + + def get_domain_id(self, domain_name): + req = self.get_domain_list() + for i in req["data"]: + if domain_name.strip() == i['domain'][:-1]: + return i['id'] + return False + + def create_dns_record(self, domain_name, domain_dns_value): + root, _, acme_txt = extract_zone(domain_name) + domain = self.get_domain_id(root) + if not domain: + raise ValueError('The domain name does not exist under this cloudxns user, adding parsing failed.') + + url = "https://www.cloudxns.net/api2/record" + data = { + "domain_id": int(domain), + "host": acme_txt, + "value": domain_dns_value, + "type": "TXT", + "line_id": 1, + } + parameter = json.dumps(data) + headers = self.get_headers(url, parameter) + req = requests.post(url=url, headers=headers, data=parameter,verify=False) + req = req.json() + + return req + + def delete_dns_record(self, domain_name, domain_dns_value): + root, _, acme_txt = extract_zone(domain_name) + print("delete_dns_record start: ", acme_txt, domain_dns_value) + url = "https://www.cloudxns.net/api2/record/{}/{}".format(self.get_record_id(root,'TXT'), self.get_domain_id(root)) + headers = self.get_headers(url, ) + req = requests.delete(url=url, headers=headers, verify=False) + req = req.json() + return req + + def get_record_id(self, domain_name,s_type = 'TXT'): + url = "http://www.cloudxns.net/api2/record/{}?host_id=0&offset=0&row_num=2000".format(self.get_domain_id(domain_name)) + headers = self.get_headers(url, ) + req = requests.get(url=url, headers=headers,verify=False) + req = req.json() + for i in req['data']: + if i['type'] == s_type: + return i['record_id'] + return False + +class Dns_com(object): + _type = 0 # 0:lest 1:锐成 + def __init__(self, key, secret, ): + pass + + def get_dns_obj(self): + p_path = '/www/server/panel/plugin/dns' + if not os.path.exists(p_path +'/dns_main.py'): return None + sys.path.insert(0,p_path) + import dns_main + public.mod_reload(dns_main) + return dns_main.dns_main() + + def create_dns_record(self, domain_name, domain_dns_value): + root, _, acme_txt = extract_zone(domain_name) + + if self._type == 1: + acme_txt = acme_txt.replace('_acme-challenge.','') + result = self.add_record(acme_txt + '.' + root,domain_dns_value) + else: + result = self.get_dns_obj().add_txt(acme_txt + '.' + root,domain_dns_value) + + if result == "False": + raise ValueError('[DNS] This domain name does not exist in the currently bound Pagoda DNS cloud resolution account. Adding parsing failed!') + time.sleep(5) + + def delete_dns_record(self, domain_name, domain_dns_value): + root, _, acme_txt = extract_zone(domain_name) + self.get_dns_obj().remove_txt(acme_txt + '.' + root) + + + + diff --git a/class_v2/panel_http_proxy_v2.py b/class_v2/panel_http_proxy_v2.py new file mode 100644 index 00000000..2e8f8ec5 --- /dev/null +++ b/class_v2/panel_http_proxy_v2.py @@ -0,0 +1,277 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# HTTP代理模块 +#------------------------------ + +import requests,os,re,time +from BTPanel import request,Response,public,app,get_phpmyadmin_dir,session +from http.cookies import SimpleCookie +import requests.packages.urllib3.util.connection as urllib3_conn +import socket + + +class HttpProxy: + _pma_path = None + def get_res_headers(self,p_res): + ''' + @name 获取响应头 + @author hwliang<2022-01-19> + @param p_res requests响应对像 + @return dict + ''' + headers = {} + for h in p_res.headers.keys(): + if h in ['Content-Encoding','Transfer-Encoding']: continue + headers[h] = p_res.headers[h] + if h in ['Location']: + + if headers[h].find('phpmyadmin_') != -1: + if not self._pma_path: + self._pma_path = get_phpmyadmin_dir() + if self._pma_path: + self._pma_path = self._pma_path[0] + else: + self._pma_path = '' + headers[h] = headers[h].replace(self._pma_path,'phpmyadmin') + + if headers[h].find('127.0.0.1') != -1: + headers[h] = re.sub(r"https?://127.0.0.1(:\d+)?/",request.url_root,headers[h]) + if request.url_root.find('https://') == 0: + headers[h] = headers[h].replace('http://','https://') + return headers + + def set_res_headers(self,res,p_res): + ''' + @name 设置响应头 + @author hwliang<2022-01-19> + @param res flask响应对像 + @param p_res requests响应对像 + @return res + ''' + # from datetime import datetime + # cookie_dict = p_res.cookies.get_dict() + # expires = datetime.utcnow() + app.permanent_session_lifetime + # for k in cookie_dict.keys(): + # httponly = True + # if k in ['phpMyAdmin']: httponly = True + # res.set_cookie(k, cookie_dict[k], + # expires=expires, httponly=httponly, + # path='/') + + return res + + def get_pma_phpversion(self): + ''' + @name 获取phpmyadmin的php版本 + @author hwliang<2022-01-19> + @return str + ''' + from panelPlugin import panelPlugin + pma_status = panelPlugin().getPHPMyAdminStatus() + if 'phpversion' in pma_status: + return pma_status['phpversion'] + return None + + def get_pma_version(self): + ''' + @name 获取phpmyadmin的版本 + @author hwliang<2022-01-19> + @return str + ''' + pma_vfile = public.get_setup_path() + '/phpmyadmin/version.pl' + if not os.path.exists(pma_vfile): return '' + pma_version = public.readFile(pma_vfile).strip() + if not pma_version: return '' + return pma_version + + def set_pma_phpversion(self): + ''' + @name 设置phpmyadmin兼容的php版本 + @author hwliang<2022-01-19> + @return str + ''' + + pma_version = self.get_pma_version() + if not pma_version: return False + + old_phpversion = self.get_pma_phpversion() + if not old_phpversion: return False + if pma_version == '4.0': + php_versions = ['52','53','54'] + elif pma_version == '4.4': + php_versions = ['54','55','56'] + elif pma_version == '4.9': + php_versions = ['55','56','70','71','72','73','74'] + elif pma_version == '5.0': + php_versions = ['70','71','72','73','74'] + elif pma_version == '5.1': + php_versions = ['71','72','73','74','80'] + elif pma_version == '5.2': + php_versions = ['72','73','74','80','81'] + elif pma_version == '5.3': + php_versions = ['72','73','74','80','81'] + else: + return False + + if old_phpversion in php_versions: return True + + installed_php_versions = [] + php_install_path = '/www/server/php' + for version in php_versions: + php_bin = php_install_path + '/' + version + '/bin/php' + if os.path.exists(php_bin): + installed_php_versions.append(version) + + if not installed_php_versions: return False + + php_version = installed_php_versions[-1] + + import ajax + args = public.dict_obj() + args.phpversion = php_version + ajax.ajax().setPHPMyAdmin(args) + public.WriteLog('数据库','检测到phpMyAdmin使用的PHP版本不兼容,已自动修改为最佳兼容版本: PHP-' + php_version) + time.sleep(0.5) + + + def get_request_headers(self): + ''' + @name 获取请求头 + @author hwliang<2022-01-19> + @return dict + ''' + headers = {} + rm_cookies = [app.config['SESSION_COOKIE_NAME'],'bt_user_info','file_recycle_status','ltd_end', + 'memSize','page_number','pro_end','request_token','serverType','site_model', + 'sites_path','soft_remarks','load_page','Path','distribution','order'] + for k in request.headers.keys(): + headers[k] = request.headers.get(k) + if k == 'Cookie': + cookie_dict = SimpleCookie(headers[k]) + for rm_cookie in rm_cookies: + if rm_cookie in cookie_dict: + del(cookie_dict[rm_cookie]) + headers[k] = cookie_dict.output(header='',sep=';').strip() + return headers + + def form_to_dict(self,form): + ''' + @name 将表单转为字典 + @author hwliang<2022-02-18> + @param form 表单数据 + @return dict + ''' + + data = {} + for k in form.keys(): + data[k] = form.getlist(k) + if len(data[k]) == 1: data[k] = data[k][0] + return data + + def proxy(self,proxy_url): + ''' + @name 代理指定URL地址 + @author hwliang<2022-01-19> + @param proxy_url 被代理的URL地址 + @return Response + ''' + try: + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + s_key = 'proxy_{}_{}'.format(app.secret_key,self.get_pma_version()) + + if not s_key in session: + session[s_key] = requests.Session() + session[s_key].keep_alive = False + session[s_key].headers = { + 'User-Agent':'BT-Panel', + 'Connection':'close' + } + + if proxy_url.find('phpmyadmin') != -1: + if proxy_url.find('https://') == 0: + session[s_key].cookies.update({'pma_lang_https':'zh_CN'}) + else: + session[s_key].cookies.update({'pma_lang':'zh_CN'}) + self.set_pma_phpversion() + + if 'Authorization' in request.headers: + session[s_key].headers['Authorization'] = request.headers['Authorization'] + + try: + session[s_key].headers['Host'] = public.en_punycode(request.url_root).replace('http://','').replace('https://','').split('/')[0] + except:pass + # headers = self.get_request_headers() + headers = None + if request.method == 'GET': + # 转发GET请求 + p_res = session[s_key].get(proxy_url,headers=headers,verify=False,allow_redirects=False) + elif request.method == 'POST': + # 转发POST请求 + if request.files: # 如果上传文件 + tmp_path = '{}/tmp'.format(public.get_panel_path()) + if not os.path.exists(tmp_path): os.makedirs(tmp_path,384) + + # 处理请求头 + if headers: + if 'Content-Type' in headers: del(headers['Content-Type']) + if 'Content-Length' in headers: del(headers['Content-Length']) + + # 遍历form表单中的所有文件 + files = {} + f_list = {} + for key in request.files: + upload_files = request.files.getlist(key) + filename = upload_files[0].filename + if not filename: filename = public.GetRandomString(12) + tmp_file = '{}/{}'.format(tmp_path,filename) + + + # 保存上传文件到临时目录 + with open(tmp_file,'wb') as f: + for tmp_f in upload_files: + f.write(tmp_f.read()) + f.close() + + # 构造文件上传对象 + f_list[key] = open(tmp_file,'rb') + files[key] = (filename, f_list[key]) + + # 删除临时文件 + if os.path.exists(tmp_file): os.remove(tmp_file) + + # 转发上传请求 + + p_res = session[s_key].post(proxy_url,self.form_to_dict(request.form),headers=headers,files=files,verify=False,allow_redirects=False) + + # 释放文件对象 + for fkey in f_list.keys(): + f_list[fkey].close() + else: + p_res = session[s_key].post(proxy_url,self.form_to_dict(request.form),headers=headers,verify=False,allow_redirects=False) + else: + return Response('不支持的请求类型',500) + + # PHP版本自动切换处理 + if proxy_url.find('phpmyadmin') != -1 and proxy_url.find('/index.php') != -1: + if len(p_res.content) < 1024: + if p_res.content.find(b'syntax error, unexpected') != -1 or p_res.content.find(b'offset access syntax with') != -1 or p_res.content.find(b'+ is required') != -1: + self.set_pma_phpversion() + return 'Incompatible PHP version, an attempt has been made to automatically switch to a compatible PHP version, please refresh the page and try again!' + elif p_res.content.find(b'Deprecation Notice') != -1 and not session.get('set_pma_phpversion'): + self.set_pma_phpversion() + session['set_pma_phpversion'] = True + return 'Incompatible PHP version, an attempt has been made to automatically switch to a compatible PHP version, please refresh the page and try again!' + + res = Response(p_res.content,headers=self.get_res_headers(p_res),content_type=p_res.headers.get('content-type',None),status=p_res.status_code) + res = self.set_res_headers(res,p_res) + return res + except Exception as ex: + return Response(str(ex),500) diff --git a/class_v2/panel_lets_v2.py b/class_v2/panel_lets_v2.py new file mode 100644 index 00000000..61abfa4f --- /dev/null +++ b/class_v2/panel_lets_v2.py @@ -0,0 +1,796 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 沐落 +# +------------------------------------------------------------------- +import os,sys,json,time,re +setup_path = '/www/server/panel' +os.chdir(setup_path) +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import http_requests as requests +import sewer,public +from OpenSSL import crypto +try: + requests.packages.urllib3.disable_warnings() +except:pass +if __name__ != '__main__': + import BTPanel +try: + import dns.resolver +except: + public.ExecShell("pip install dnspython") + try: + import dns.resolver + except: + pass + +class panelLets: + let_url = "https://acme-v02.api.letsencrypt.org/directory" + #let_url = "https://acme-staging-v02.api.letsencrypt.org/directory" + + setupPath = None #安装路径 + server_type = None + log_file = '/www/server/panel/logs/letsencrypt.log' + + #构造方法 + def __init__(self): + self.setupPath = public.GetConfigValue('setup_path') + self.server_type = public.get_webserver() + + def write_log(self,log_str): + + f = open(self.log_file,'ab+') + log_str += "\n" + f.write(log_str.encode('utf-8')) + f.close() + return True + + #拆分根证书 + def split_ca_data(self,cert): + datas = cert.split('-----END CERTIFICATE-----') + return {"cert":datas[0] + "-----END CERTIFICATE-----\n","ca_data":datas[1] + '-----END CERTIFICATE-----\n' } + + #证书转为pkcs12 + def dump_pkcs12(self,key_pem=None,cert_pem = None, ca_pem=None, friendly_name=None): + p12 = crypto.PKCS12() + if cert_pem: + ret = p12.set_certificate(crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem.encode())) + assert ret is None + if key_pem: + ret = p12.set_privatekey(crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem.encode())) + assert ret is None + if ca_pem: + ret = p12.set_ca_certificates((crypto.load_certificate(crypto.FILETYPE_PEM, ca_pem.encode()),) ) + if friendly_name: + ret = p12.set_friendlyname(friendly_name.encode()) + return p12 + + def extract_zone(self,domain_name): + domain_name = domain_name.lstrip("*.") + top_domain_list = ['.ac.cn', '.ah.cn', '.bj.cn', '.com.cn', '.cq.cn', '.fj.cn', '.gd.cn','.gov.cn', '.gs.cn', + '.gx.cn', '.gz.cn', '.ha.cn', '.hb.cn', '.he.cn','.hi.cn', '.hk.cn', '.hl.cn', '.hn.cn', + '.jl.cn', '.js.cn', '.jx.cn','.ln.cn', '.mo.cn', '.net.cn', '.nm.cn', '.nx.cn', '.org.cn', + '.my.id','.com.ac','.com.ad','.com.ae','.com.af','.com.ag','.com.ai','.com.al','.com.am', + '.com.an','.com.ao','.com.aq','.com.ar','.com.as','.com.as','.com.at','.com.au','.com.aw', + '.com.az','.com.ba','.com.bb','.com.bd','.com.be','.com.bf','.com.bg','.com.bh','.com.bi', + '.com.bj','.com.bm','.com.bn','.com.bo','.com.br','.com.bs','.com.bt','.com.bv','.com.bw', + '.com.by','.com.bz','.com.ca','.com.ca','.com.cc','.com.cd','.com.cf','.com.cg','.com.ch', + '.com.ci','.com.ck','.com.cl','.com.cm','.com.cn','.com.co','.com.cq','.com.cr','.com.cu', + '.com.cv','.com.cx','.com.cy','.com.cz','.com.de','.com.dj','.com.dk','.com.dm','.com.do', + '.com.dz','.com.ec','.com.ee','.com.eg','.com.eh','.com.es','.com.et','.com.eu','.com.ev', + '.com.fi','.com.fj','.com.fk','.com.fm','.com.fo','.com.fr','.com.ga','.com.gb','.com.gd', + '.com.ge','.com.gf','.com.gh','.com.gi','.com.gl','.com.gm','.com.gn','.com.gp','.com.gr', + '.com.gt','.com.gu','.com.gw','.com.gy','.com.hm','.com.hn','.com.hr','.com.ht','.com.hu', + '.com.id','.com.id','.com.ie','.com.il','.com.il','.com.in','.com.io','.com.iq','.com.ir', + '.com.is','.com.it','.com.jm','.com.jo','.com.jp','.com.ke','.com.kg','.com.kh','.com.ki', + '.com.km','.com.kn','.com.kp','.com.kr','.com.kw','.com.ky','.com.kz','.com.la','.com.lb', + '.com.lc','.com.li','.com.lk','.com.lr','.com.ls','.com.lt','.com.lu','.com.lv','.com.ly', + '.com.ma','.com.mc','.com.md','.com.me','.com.mg','.com.mh','.com.ml','.com.mm','.com.mn', + '.com.mo','.com.mp','.com.mq','.com.mr','.com.ms','.com.mt','.com.mv','.com.mw','.com.mx', + '.com.my','.com.mz','.com.na','.com.nc','.com.ne','.com.nf','.com.ng','.com.ni','.com.nl', + '.com.no','.com.np','.com.nr','.com.nr','.com.nt','.com.nu','.com.nz','.com.om','.com.pa', + '.com.pe','.com.pf','.com.pg','.com.ph','.com.pk','.com.pl','.com.pm','.com.pn','.com.pr', + '.com.pt','.com.pw','.com.py','.com.qa','.com.re','.com.ro','.com.rs','.com.ru','.com.rw', + '.com.sa','.com.sb','.com.sc','.com.sd','.com.se','.com.sg','.com.sh','.com.si','.com.sj', + '.com.sk','.com.sl','.com.sm','.com.sn','.com.so','.com.sr','.com.st','.com.su','.com.sy', + '.com.sz','.com.tc','.com.td','.com.tf','.com.tg','.com.th','.com.tj','.com.tk','.com.tl', + '.com.tm','.com.tn','.com.to','.com.tp','.com.tr','.com.tt','.com.tv','.com.tw','.com.tz', + '.com.ua','.com.ug','.com.uk','.com.uk','.com.us','.com.uy','.com.uz','.com.va','.com.vc', + '.com.ve','.com.vg','.com.vn','.com.vu','.com.wf','.com.ws','.com.ye','.com.za','.com.zm', + '.com.zw'] + old_domain_name = domain_name + m_count = domain_name.count(".") + top_domain = "."+".".join(domain_name.rsplit('.')[-2:]) + new_top_domain = "." + top_domain.replace(".","") + is_tow_top = False + if top_domain in top_domain_list: + is_tow_top = True + domain_name = domain_name[:-len(top_domain)] + new_top_domain + + if domain_name.count(".") > 1: + zone, middle, last = domain_name.rsplit(".", 2) + acme_txt = "_acme-challenge.%s" % zone + if is_tow_top: last = top_domain[1:] + root = ".".join([middle, last]) + else: + zone = "" + root = old_domain_name + acme_txt = "_acme-challenge" + return root, zone, acme_txt + + #获取根域名 + def get_root_domain(self,domain_name): + d_root,tow_name,acme_txt = self.extract_zone(domain_name) + return d_root + + #获取acmename + def get_acme_name(self,domain_name): + d_root,tow_name,acme_txt = self.extract_zone(domain_name) + return acme_txt + '.' + d_root + + #格式化错误输出 + def get_error(self,error): + if error.find("Max checks allowed") >= 0 : + return public.get_msg_gettext("CA can't verify your domain name, please check if the domain name resolution is correct, or wait 5-10 minutes and try again.") + elif error.find("Max retries exceeded with") >= 0 or error.find('status_code=0 ') != -1: + return public.get_msg_gettext("The CA server connection timed out, please try again later.") + elif error.find("The domain name belongs") >= 0: + return public.get_msg_gettext("The domain name does not belong to this DNS service provider. Please ensure that the domain name is filled in correctly.") + elif error.find('login token ID is invalid') >=0: + return public.get_msg_gettext('The DNS server connection failed. Please check if the key is correct.') + elif "too many certificates already issued for exact set of domains" in error: + return public.get_msg_gettext('The signing failed, the domain name exact set of domains: (.+): {} exceeded the weekly number of repeated issuances!',(error,)) + elif "Error creating new account :: too many registrations for this IP" in error: + return public.get_msg_gettext('The signing failed, the current server IP has reached the limit of creating up to 10 accounts every 3 hours..') + elif "DNS problem: NXDOMAIN looking up A for" in error: + return public.get_msg_gettext('The verification failed, the domain name was not resolved, or the resolution did not take effect.!') + elif "Invalid response from" in error: + return public.get_msg_gettext('Authentication failed, domain name resolution error or verification URL could not be accessed!') + elif error.find('TLS Web Server Authentication') != -1: + public.restart_panel() + return public.get_msg_gettext("Failed to connect to CA server, please try again later.") + elif error.find('Name does not end in a public suffix') != -1: + return public.get_msg_gettext("Unsupported domain name {}, please check if the domain name is correct!",(re.findall("Cannot issue for \"(.+)\":", error),)) + elif error.find('No valid IP addresses found for') != -1: + return public.get_msg_gettext("The domain name {} did not find a resolution record. Please check if the domain name is resolved.!",(re.findall("No valid IP addresses found for (.+)", error),)) + elif error.find('No TXT record found at') != -1: + return public.get_msg_gettext("If a valid TXT resolution record is not found in the domain name {}, please check if the TXT record is correctly parsed. If it is applied by DNSAPI, please try again in 10 minutes.!",(re.findall( + "No TXT record found at (.+)", error),)) + elif error.find('Incorrect TXT record') != -1: + return public.get_msg_gettext("Found the wrong TXT record on {}: {}, please check if the TXT resolution is correct. If it is applied by DNSAPI, please try again in 10 minutes.!",( + re.findall("found at (.+)", error), re.findall("Incorrect TXT record \"(.+)\"", error))) + elif error.find('Domain not under you or your user') != -1: + return public.get_msg_gettext("This domain name does not exist under this dnspod account. Adding parsing failed.!") + elif error.find('SERVFAIL looking up TXT for') != -1: + return public.get_msg_gettext("If a valid TXT resolution record is not found in the domain name {}, please check if the TXT record is correctly parsed. If it is applied by DNSAPI, please try again in 10 minutes.!",(re.findall( + "looking up TXT for (.+)", error),)) + elif error.find('Timeout during connect') != -1: + return public.get_msg_gettext("Connection timed out, CA server could not access your website!") + elif error.find("DNS problem: SERVFAIL looking up CAA for") != -1: + return public.get_msg_gettext("The domain name {} is currently required to verify the CAA record. Please manually resolve the CAA record, or try again after 1 hour.!" , (re.findall("looking up CAA for (.+)", error),)) + elif error.find("Read timed out.") != -1: + return public.get_msg_gettext("Verification timeout, please check whether the domain name is correctly resolved. If dns is resolved, the connection between the server and Let'sEncrypt may be abnormal. Please try again later!") + elif error.find("Error creating new order") != -1: + return public.get_msg_gettext("Order creation failed, please try again later!") + elif error.find("Too Many Requests") != -1: + return public.get_msg_gettext("More than 5 verification failures in 1 hour, application is temporarily banned, please try again later!") + elif error.find('HTTP Error 400: Bad Request') != -1: + return public.get_msg_gettext("CA server denied access, please try again later!") + else: + return error; + + #获取DNS服务器 + def get_dns_class(self,data): + if data['dnsapi'] == 'dns_ali': + import panelDnsapi + public.mod_reload(panelDnsapi) + dns_class = panelDnsapi.AliyunDns(key = data['dns_param'][0], secret = data['dns_param'][1]) + return dns_class + elif data['dnsapi'] == 'dns_dp': + dns_class = sewer.DNSPodDns(DNSPOD_ID = data['dns_param'][0] ,DNSPOD_API_KEY = data['dns_param'][1]) + return dns_class + elif data['dnsapi'] == 'dns_cx': + import panelDnsapi + public.mod_reload(panelDnsapi) + dns_class = panelDnsapi.CloudxnsDns(key = data['dns_param'][0] ,secret =data['dns_param'][1]) + result = dns_class.get_domain_list() + if result['code'] == 1: + return dns_class + elif data['dnsapi'] == 'dns_bt': + import panelDnsapi + public.mod_reload(panelDnsapi) + dns_class = panelDnsapi.Dns_com() + return dns_class + return False + + #续签证书 + def renew_lest_cert(self,data): + #续签网站 + path = self.setupPath + '/panel/vhost/cert/'+ data['siteName'] + if not os.path.exists(path): return public.return_msg_gettext(False, 'The renewal failed and the certificate directory does not exist.') + + account_path = path + "/account_key.key" + if not os.path.exists(account_path): return public.return_msg_gettext(False, 'Renewal failed, missing account_key.') + + #续签 + data['account_key'] = public.readFile(account_path) + + if not 'first_domain' in data: data['first_domain'] = data['domains'][0] + + if 'dnsapi' in data: + certificate = self.crate_let_by_dns(data) + else: + certificate = self.crate_let_by_file(data) + + if not certificate['status']: return public.return_msg_gettext(False, certificate['msg']) + + #存储证书 + public.writeFile(path + "/privkey.pem",certificate['key']) + public.writeFile(path + "/fullchain.pem",certificate['cert'] + certificate['ca_data']) + public.writeFile(path + "/account_key.key", certificate['account_key']) #续签KEY + + #转为IIS证书 + p12 = self.dump_pkcs12(certificate['key'], certificate['cert'] + certificate['ca_data'],certificate['ca_data'],data['first_domain']) + pfx_buffer = p12.export() + public.writeFile(path + "/fullchain.pfx",pfx_buffer,'wb+') + + return public.return_msg_gettext(True, '[ {} ] The certificate renewal was successful.',(data['siteName'],)) + + + + #申请证书 + def apple_lest_cert(self,get): + data = {} + data['siteName'] = get.siteName + data['domains'] = json.loads(get.domains) + data['email'] = get.email + data['dnssleep'] = get.dnssleep + self.write_log(public.get_msg_gettext('Ready to apply for SSL, domain name {}',(data['domains'],))) + self.write_log("="*50) + if len(data['domains']) <=0 : return public.return_msg_gettext(False, 'The list of applied domain names cannot be empty.') + + data['first_domain'] = data['domains'][0] + + path = self.setupPath + '/panel/vhost/cert/'+ data['siteName'] + if not os.path.exists(path): os.makedirs(path) + + # 检查是否自定义证书 + partnerOrderId = path + '/partnerOrderId' + if os.path.exists(partnerOrderId): os.remove(partnerOrderId) + #清理续签key + re_key = path + '/account_key.key' + if os.path.exists(re_key): os.remove(re_key) + + re_password = path + '/password' + if os.path.exists(re_password): os.remove(re_password) + + data['account_key'] = None + if hasattr(get, 'dnsapi'): + if not 'app_root' in get: get.app_root = '0' + data['app_root'] = get.app_root + domain_list = data['domains'] + if data['app_root'] == '1': + public.writeFile(self.log_file,''); + domain_list = [] + data['first_domain'] = self.get_root_domain(data['first_domain']) + for domain in data['domains']: + rootDoamin = self.get_root_domain(domain) + if not rootDoamin in domain_list: domain_list.append(rootDoamin) + if not "*." + rootDoamin in domain_list: domain_list.append("*." + rootDoamin) + data['domains'] = domain_list + if get.dnsapi == 'dns': + domain_path = path + '/domain_txt_dns_value.json' + if hasattr(get, 'renew'): #验证 + data['renew'] = True + dns = json.loads(public.readFile(domain_path)) + data['dns'] = dns + certificate = self.crate_let_by_oper(data) + else: + public.writeFile(self.log_file,''); + #手动解析提前返回 + result = self.crate_let_by_oper(data) + if 'status' in result and not result['status']: return result + result['status'] = True + public.writeFile(domain_path, json.dumps(result)) + result['msg'] = public.get_msg_gettext('Get successful, please manually resolve the domain name') + result['code'] = 2 + return result + elif get.dnsapi == 'dns_bt': + public.writeFile(self.log_file,'') + data['dnsapi'] = get.dnsapi + certificate = self.crate_let_by_dns(data) + else: + public.writeFile(self.log_file,'') + data['dnsapi'] = get.dnsapi + data['dns_param'] = get.dns_param.split('|') + certificate = self.crate_let_by_dns(data) + else: + #文件验证 + public.writeFile(self.log_file,'') + data['site_dir'] = get.site_dir + certificate = self.crate_let_by_file(data) + + if not certificate['status']: return public.return_msg_gettext(False, certificate['msg']) + + #保存续签 + self.write_log(public.get_msg_gettext('|-Saving certificate..')) + cpath = self.setupPath + '/panel/vhost/cert/crontab.json' + config = {} + if os.path.exists(cpath): + try: + config = json.loads(public.readFile(cpath)) + except:pass + + config[data['siteName']] = data + public.writeFile(cpath,json.dumps(config)) + public.set_mode(cpath,600) + + #存储证书 + public.writeFile(path + "/privkey.pem",certificate['key']) + public.writeFile(path + "/fullchain.pem",certificate['cert'] + certificate['ca_data']) + public.writeFile(path + "/account_key.key",certificate['account_key']) #续签KEY + + #转为IIS证书 + p12 = self.dump_pkcs12(certificate['key'], certificate['cert'] + certificate['ca_data'],certificate['ca_data'],data['first_domain']) + pfx_buffer = p12.export() + public.writeFile(path + "/fullchain.pfx",pfx_buffer,'wb+') + public.writeFile(path + "/README","let") + + #计划任务续签 + self.write_log(public.get_msg_gettext('|-Setting up auto-renewal configuration..')) + self.set_crond() + self.write_log(public.get_msg_gettext('|-The application is successful and it is being automatically deployed to the website!')) + self.write_log("="*50) + return public.return_msg_gettext(True, 'Application successful.') + + #创建计划任务 + def set_crond(self): + try: + echo = public.md5(public.md5('renew_lets_ssl_bt')) + cron_id = public.M('crontab').where('echo=?',(echo,)).getField('id') + + import crontab + args_obj = public.dict_obj() + if not cron_id: + cronPath = public.GetConfigValue('setup_path') + '/cron/' + echo + shell = public.get_python_bin() + ' %s/panel/class/panelLets.py renew_lets_ssl ' % (self.setupPath) + public.writeFile(cronPath,shell) + args_obj.id = public.M('crontab').add('name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress',("Renew the Letter's Encrypt certificate",'day','','0','10',echo,time.strftime('%Y-%m-%d %X',time.localtime()),0,'','localhost','toShell','',shell,'')) + crontab.crontab().set_cron_status(args_obj) + else: + cron_path = public.get_cron_path() + if os.path.exists(cron_path): + cron_s = public.readFile(cron_path) + if cron_s.find(echo) == -1: + public.M('crontab').where('echo=?',(echo,)).setField('status',0) + args_obj.id = cron_id + crontab.crontab().set_cron_status(args_obj) + except:pass + + #手动解析 + def crate_let_by_oper(self,data): + result = {} + result['status'] = False + try: + if not data['email']: data['email'] = public.M('users').getField('email') + + #手动解析记录值 + if not 'renew' in data: + self.write_log(public.get_msg_gettext('|-Initializing ACME protocol...')) + BTPanel.dns_client = sewer.Client(domain_name = data['first_domain'],dns_class = None,account_key = data['account_key'],domain_alt_names = data['domains'],contact_email = str(data['email']) ,ACME_AUTH_STATUS_WAIT_PERIOD = 15,ACME_AUTH_STATUS_MAX_CHECKS = 5,ACME_REQUEST_TIMEOUT = 20,ACME_DIRECTORY_URL = self.let_url) + domain_dns_value = "placeholder" + dns_names_to_delete = [] + self.write_log(public.get_msg_gettext('|-Registering account...')) + BTPanel.dns_client.acme_register() + authorizations, finalize_url = BTPanel.dns_client.apply_for_cert_issuance() + responders = [] + self.write_log(public.get_msg_gettext('|-Getting verification information...')) + for url in authorizations: + identifier_auth = BTPanel.dns_client.get_identifier_authorization(url) + authorization_url = identifier_auth["url"] + dns_name = identifier_auth["domain"] + dns_token = identifier_auth["dns_token"] + dns_challenge_url = identifier_auth["dns_challenge_url"] + + acme_keyauthorization, domain_dns_value = BTPanel.dns_client.get_keyauthorization(dns_token) + + acme_name = self.get_acme_name(dns_name) + dns_names_to_delete.append({"dns_name": public.de_punycode(dns_name),"acme_name":acme_name, "domain_dns_value": domain_dns_value}) + responders.append( + { + "dns_name":dns_name, + "authorization_url": authorization_url, + "acme_keyauthorization": acme_keyauthorization, + "dns_challenge_url": dns_challenge_url, + } + ) + + dns = {} + dns['dns_names'] = dns_names_to_delete + dns['responders'] = responders + dns['finalize_url'] = finalize_url + self.write_log(public.get_msg_gettext('|-Return the verification information to the front end, wait for the user to manually resolve the domain name and complete the verification...')) + return dns + else: + self.write_log(public.get_msg_gettext('|-User submits verification request...')) + responders = data['dns']['responders'] + dns_names_to_delete = data['dns']['dns_names'] + finalize_url = data['dns']['finalize_url'] + for i in responders: + self.write_log(public.get_msg_gettext('|-Requesting CA to verify domain name [{}]...',(i['dns_name'],))) + auth_status_response = BTPanel.dns_client.check_authorization_status(i["authorization_url"]) + if auth_status_response.json()["status"] == "pending": + BTPanel.dns_client.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"]) + + for i in responders: + self.write_log(public.get_msg_gettext('|-Get CA verification results [{}]...',(i['dns_name'],))) + BTPanel.dns_client.check_authorization_status(i["authorization_url"], ["valid","invalid"]) + self.write_log(public.get_msg_gettext('|-All domain names are verified and CSR is being sent...')) + certificate_url = BTPanel.dns_client.send_csr(finalize_url) + self.write_log(public.get_msg_gettext('|-Getting certificate content...')) + certificate = BTPanel.dns_client.download_certificate(certificate_url) + + if certificate: + certificate = self.split_ca_data(certificate) + result['cert'] = certificate['cert'] + result['ca_data'] = certificate['ca_data'] + result['key'] = BTPanel.dns_client.certificate_key + result['account_key'] = BTPanel.dns_client.account_key + result['status'] = True + BTPanel.dns_client = None + else: + result['msg'] = public.get_msg_gettext('Certificate acquisition failed, please try again later.') + + except Exception as e: + self.write_log(public.get_msg_gettext('|-Error: {}, exited the application process.',(e,))) + self.write_log("=" * 50) + res = str(e).split('>>>>') + err = False + try: + err = json.loads(res[1]) + except: err = False + result['msg'] = [self.get_error(res[0]),err] + + return result + + #dns验证 + def crate_let_by_dns(self,data): + dns_class = self.get_dns_class(data) + if not dns_class: + self.write_log(public.get_msg_gettext('|-Error: {}, exit the application process.')) + self.write_log(public.get_msg_gettext('|-Exited the application process!')) + self.write_log("="*50) + return public.return_msg_gettext(False, 'An error occurred while requesting a certificate using dns') + + result = {} + result['status'] = False + try: + log_level = "INFO" + if data['account_key']: log_level = 'ERROR' + if not data['email']: data['email'] = public.M('users').getField('email') + self.write_log(public.get_msg_gettext('|-Initializing ACME protocol...')) + client = sewer.Client(domain_name = data['first_domain'],domain_alt_names = data['domains'],account_key = data['account_key'],contact_email = str(data['email']),LOG_LEVEL = log_level,ACME_AUTH_STATUS_WAIT_PERIOD = 15,ACME_AUTH_STATUS_MAX_CHECKS = 5,ACME_REQUEST_TIMEOUT = 20, dns_class = dns_class,ACME_DIRECTORY_URL = self.let_url) + domain_dns_value = "placeholder" + dns_names_to_delete = [] + try: + self.write_log(public.get_msg_gettext('|-Registering account...')) + client.acme_register() + authorizations, finalize_url = client.apply_for_cert_issuance() + responders = [] + self.write_log(public.get_msg_gettext('|-Getting verification information...')) + for url in authorizations: + identifier_auth = client.get_identifier_authorization(url) + authorization_url = identifier_auth["url"] + dns_name = identifier_auth["domain"] + dns_token = identifier_auth["dns_token"] + dns_challenge_url = identifier_auth["dns_challenge_url"] + acme_keyauthorization, domain_dns_value = client.get_keyauthorization(dns_token) + self.write_log(public.get_msg_gettext('|-Adding resolution record, domain name [{}], record value [{}]...',(dns_name,domain_dns_value))) + dns_class.create_dns_record(public.de_punycode(dns_name), domain_dns_value) + dns_names_to_delete.append({"dns_name": public.de_punycode(dns_name), "domain_dns_value": domain_dns_value}) + responders.append({"dns_name":dns_name,"domain_dns_value":domain_dns_value,"authorization_url": authorization_url, "acme_keyauthorization": acme_keyauthorization,"dns_challenge_url": dns_challenge_url} ) + + + + try: + for i in responders: + self.write_log(public.get_msg_gettext('|-Attempt to verify the resolution result, domain name [{}], record value [{}]...',(i['dns_name'],i['domain_dns_value']))) + self.check_dns(self.get_acme_name(i['dns_name']),i['domain_dns_value']) + self.write_log(public.get_msg_gettext('|-Request CA to verify domain name [{}]...',(i['dns_name']))) + auth_status_response = client.check_authorization_status(i["authorization_url"]) + r_data = auth_status_response.json() + if r_data["status"] == "pending": + client.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"]) + + for i in responders: + self.write_log(public.get_msg_gettext('|-Check CA verification results [{}]...',(i['dns_name'],))) + client.check_authorization_status(i["authorization_url"], ["valid","invalid"]) + except Exception as ex: + self.write_log(public.get_msg_gettext('|-An error occurred, try again [{}]',(str(ex),))) + for i in responders: + self.write_log(public.get_msg_gettext('|-Attempt to verify the resolution result, domain name [{}], record value [{}]...',(i['dns_name'],i['domain_dns_value']))) + self.check_dns(self.get_acme_name(i['dns_name']),i['domain_dns_value']) + self.write_log(public.get_msg_gettext('|-Request CA to verify domain name [{}]...',(i['dns_name']))) + auth_status_response = client.check_authorization_status(i["authorization_url"]) + r_data = auth_status_response.json() + if r_data["status"] == "pending": + client.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"]) + for i in responders: + self.write_log(public.get_msg_gettext('|-Check CA verification results [{}]...',(i['dns_name'],))) + client.check_authorization_status(i["authorization_url"], ["valid","invalid"]) + self.write_log(public.get_msg_gettext('|-All domain names are verified and CSR is being sent...')) + certificate_url = client.send_csr(finalize_url) + self.write_log(public.get_msg_gettext('|-Fetching certificate content...')) + certificate = client.download_certificate(certificate_url) + if certificate: + certificate = self.split_ca_data(certificate) + result['cert'] = certificate['cert'] + result['ca_data'] = certificate['ca_data'] + result['key'] = client.certificate_key + result['account_key'] = client.account_key + result['status'] = True + + except Exception as e: + raise e + finally: + try: + for i in dns_names_to_delete: + self.write_log(public.get_msg_gettext('|-Clearing resolve history [{}]',(i["dns_name"]))) + dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"]) + except : + pass + + except Exception as e: + try: + for i in dns_names_to_delete: + self.write_log(public.get_msg_gettext('|-Clearing resolve history [{}]',(i["dns_name"]))) + dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"]) + except:pass + self.write_log(public.get_msg_gettext('|-Error: {}, exit the application process.',(str(public.get_error_info()),))) + self.write_log("=" * 50) + res = str(e).split('>>>>') + err = False + try: + err = json.loads(res[1]) + except: err = False + result['msg'] = [self.get_error(res[0]),err] + return result + + #文件验证 + def crate_let_by_file(self,data): + result = {} + result['status'] = False + result['clecks'] = [] + try: + self.write_log(public.get_msg_gettext('|-Initializing ACME protocol...')) + log_level = "INFO" + if data['account_key']: log_level = 'ERROR' + if not data['email']: data['email'] = public.M('users').getField('email') + client = sewer.Client(domain_name = data['first_domain'],dns_class = None,account_key = data['account_key'],domain_alt_names = data['domains'],contact_email = str(data['email']),LOG_LEVEL = log_level,ACME_AUTH_STATUS_WAIT_PERIOD = 15,ACME_AUTH_STATUS_MAX_CHECKS = 5,ACME_REQUEST_TIMEOUT = 20,ACME_DIRECTORY_URL = self.let_url) + self.write_log(public.get_msg_gettext('|-Registering account...')) + client.acme_register() + authorizations, finalize_url = client.apply_for_cert_issuance() + responders = [] + sucess_domains = [] + self.write_log(public.get_msg_gettext('|-Getting verification information...')) + for url in authorizations: + identifier_auth = self.get_identifier_authorization(client,url) + + authorization_url = identifier_auth["url"] + http_name = identifier_auth["domain"] + http_token = identifier_auth["http_token"] + http_challenge_url = identifier_auth["http_challenge_url"] + + acme_keyauthorization, domain_http_value = client.get_keyauthorization(http_token) + acme_dir = '%s/.well-known/acme-challenge' % (data['site_dir']) + if not os.path.exists(acme_dir): os.makedirs(acme_dir) + + #写入token + wellknown_path = acme_dir + '/' + http_token + self.write_log(public.get_msg_gettext('|-Writing verification file [{}]...',(wellknown_path,))) + public.writeFile(wellknown_path,acme_keyauthorization) + wellknown_url = "http://{}/.well-known/acme-challenge/{}".format(http_name, http_token) + + result['clecks'].append({'wellknown_url':wellknown_url,'http_token':http_token}) + is_check = False + n = 0 + self.write_log(public.get_msg_gettext('|-Attempt to verify file contents via HTTP [{}]...',(wellknown_url))) + while n < 5: + print("wait_check_authorization_status") + try: + retkey = public.httpGet(wellknown_url,20) + if retkey == acme_keyauthorization: + is_check = True + self.write_log(public.get_msg_gettext('|-Verified, content [{}]...',(retkey,))) + break + except : + pass + n += 1 + time.sleep(1) + sucess_domains.append(http_name) + responders.append({"http_name":http_name,"authorization_url": authorization_url, "acme_keyauthorization": acme_keyauthorization,"http_challenge_url": http_challenge_url}) + + if len(sucess_domains) > 0: + #验证 + for i in responders: + self.write_log(public.get_msg_gettext('|-Request CA to verify domain name [{}]...',(i['http_name'],))) + auth_status_response = client.check_authorization_status(i["authorization_url"]) + if auth_status_response.json()["status"] == "pending": + client.respond_to_challenge(i["acme_keyauthorization"], i["http_challenge_url"]).json() + + for i in responders: + self.write_log(public.get_msg_gettext('|-Check CA verification results [{}]...',(i['http_name'],))) + client.check_authorization_status(i["authorization_url"], ["valid","invalid"]) + + self.write_log(public.get_msg_gettext('|-All domain names are verified and CSR is being sent...')) + certificate_url = client.send_csr(finalize_url) + self.write_log(public.get_msg_gettext('|-Getting certificate content...')) + certificate = client.download_certificate(certificate_url) + + if certificate: + certificate = self.split_ca_data(certificate) + result['cert'] = certificate['cert'] + result['ca_data'] = certificate['ca_data'] + result['key'] = client.certificate_key + result['account_key'] = client.account_key + result['status'] = True + + else: + result['msg'] = public.get_msg_gettext('Certificate acquisition failed, please try again later.') + else: + result['msg'] = public.get_msg_gettext('The signing failed, we were unable to verify your domain name:

                                    1. Check if the domain name is bound to the corresponding site.

                                    2. Check if the domain name is correctly resolved to the server, or the resolution is not fully effective.

                                    3. If your site has a reverse proxy set up, or if you are using a CDN, please turn it off first.

                                    4. If your site has a 301 redirect, please turn it off first

                                    5. If the above checks confirm that there is no problem, please try to change the DNS service provider.

                                    ') + except Exception as e: + self.write_log(public.get_msg_gettext('|-Error: {}, exit the application process.',(str(public.get_error_info()),))) + self.write_log("=" * 50) + res = str(e).split('>>>>') + err = False + try: + err = json.loads(res[1]) + except: err = False + result['msg'] = [self.get_error(res[0]),err] + return result + + + def get_identifier_authorization(self,client, url): + + headers = {"User-Agent": client.User_Agent} + get_identifier_authorization_response = requests.get(url, timeout = client.ACME_REQUEST_TIMEOUT, headers=headers,verify=False) + if get_identifier_authorization_response.status_code not in [200, 201]: + raise ValueError("Error getting identifier authorization: status_code={status_code}".format(status_code=get_identifier_authorization_response.status_code ) ) + res = get_identifier_authorization_response.json() + domain = res["identifier"]["value"] + wildcard = res.get("wildcard") + if wildcard: + domain = "*." + domain + + for i in res["challenges"]: + if i["type"] == "http-01": + http_challenge = i + http_token = http_challenge["token"] + http_challenge_url = http_challenge["url"] + identifier_auth = { + "domain": domain, + "url": url, + "wildcard": wildcard, + "http_token": http_token, + "http_challenge_url": http_challenge_url, + } + return identifier_auth + + #检查DNS记录 + def check_dns(self,domain,value,type='TXT'): + time.sleep(5) + n = 0 + while n < 10: + try: + import dns.resolver + ns = dns.resolver.query(domain,type) + for j in ns.response.answer: + for i in j.items: + txt_value = i.to_text().replace('"','').strip() + if txt_value == value: + self.write_log(public.get_msg_gettext('|-Successful verification, domain name [{}], record type [{}], record value [{}]!',(domain,type,txt_value))) + print("Verification succeeded: %s" % txt_value) + return True + except: + try: + import dns.resolver + except: + return False + n+=1 + time.sleep(5) + return True + + #获取证书哈希 + def get_cert_data(self,path): + try: + if path[-4:] == '.pfx': + f = open(path,'rb') + pfx_buffer = f.read() + p12 = crypto.load_pkcs12(pfx_buffer,'') + x509 = p12.get_certificate() + else: + cret_data = public.readFile(path) + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cret_data) + + buffs = x509.digest('sha1') + hash = bytes.decode(buffs).replace(':','') + data = {} + data['hash'] = hash + data['timeout'] = bytes.decode(x509.get_notAfter())[:-1] + return data + except : + return False + + + #获取快过期的证书 + def get_renew_lets_bytimeout(self,cron_list): + tday = 30 + path = self.setupPath + '/panel/vhost/cert' + nlist = {} + new_list = {} + for siteName in cron_list: + spath = path + '/' + siteName + #验证是否存在续签KEY + if os.path.exists(spath + '/account_key.key'): + if public.M('sites').where("name=?",(siteName,)).count(): + new_list[siteName] = cron_list[siteName] + data = self.get_cert_data(self.setupPath + '/panel/vhost/cert/' + siteName + '/fullchain.pem') + timeout = int(time.mktime(time.strptime(data['timeout'],'%Y%m%d%H%M%S'))) + eday = (timeout - int(time.time())) / 86400 + if eday < 30: + nlist[siteName] = cron_list[siteName] + #清理过期配置 + public.writeFile(self.setupPath + '/panel/vhost/cert/crontab.json',json.dumps(new_list)) + return nlist + + #===================================== 计划任务续订证书 =====================================# + #续订 + def renew_lets_ssl(self): + cpath = self.setupPath + '/panel/vhost/cert/crontab.json' + if not os.path.exists(cpath): + print(public.get_msg_gettext('|-There are currently no certificates to renew.') ) + else: + old_list = json.loads(public.ReadFile(cpath)) + print('=======================================================================') + print(public.get_msg_gettext('|-{} Total [{}] renewal of visa tasks',(time.strftime('%Y-%m-%d %X',time.localtime()),str(len(old_list))))) + cron_list = self.get_renew_lets_bytimeout(old_list) + + tlist = [] + for siteName in old_list: + if not siteName in cron_list: tlist.append(siteName) + print(public.get_msg_gettext(r'|-[{}] Not expired or the site does not use the Let\s Encrypt certificate.',(','.join(tlist),))) + print(public.get_msg_gettext('|-{} Waiting for renewal [{}].',(time.strftime('%Y-%m-%d %X',time.localtime()),str(len(cron_list))))) + + sucess_list = [] + err_list = [] + for siteName in cron_list: + data = cron_list[siteName] + ret = self.renew_lest_cert(data) + if ret['status']: + sucess_list.append(siteName) + else: + err_list.append({"siteName":siteName,"msg":ret['msg']}) + print(public.get_msg_gettext('|-After the task is completed, a total of renewals are required.[{}], renewal success [%s], renewal failed [{}]. ',(str(len(cron_list)),str(len(sucess_list)),str(len(err_list))))) + if len(sucess_list) > 0: + print(public.get_msg_gettext('|-Renewal success:{}',(','.join(sucess_list),))) + if len(err_list) > 0: + print(public.get_msg_gettext('|-Renewal failed:')) + for x in err_list: + print(" %s ->> %s" % (x['siteName'],x['msg'])) + + print('=======================================================================') + print(" ") + +if __name__ == "__main__": + if len(sys.argv) > 1: + type = sys.argv[1] + if type == 'renew_lets_ssl': + try: + panelLets().renew_lets_ssl() + except: pass + os.system(public.get_python_bin() + " /www/server/panel/class/acme_v2.py --renew=1") diff --git a/class_v2/panel_mssql_v2.py b/class_v2/panel_mssql_v2.py new file mode 100644 index 00000000..c5dd2c84 --- /dev/null +++ b/class_v2/panel_mssql_v2.py @@ -0,0 +1,121 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Windows面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 沐落 +# +------------------------------------------------------------------- + +import re,os,sys,public + +class panelMssql: + __DB_PASS = None + __DB_USER = 'sa' + __DB_PORT = 1433 + __DB_HOST = '127.0.0.1' + __DB_CONN = None + __DB_CUR = None + __DB_ERR = None + __DB_SERVER = 'MSSQLSERVER' + + __DB_CLOUD = 0 #远程数据库 + def __init__(self): + self.__DB_CLOUD = 0 + + + def set_host(self,host,port,name,username,password,prefix = ''): + self.__DB_HOST = host + self.__DB_PORT = int(port) + self.__DB_NAME = name + if self.__DB_NAME: self.__DB_NAME = str(self.__DB_NAME) + self.__DB_USER = str(username) + self._USER = str(username) + self.__DB_PASS = str(password) + self.__DB_PREFIX = prefix + self.__DB_CLOUD = 1 + return self + + def __Conn(self): + """ + 连接MSSQL数据库 + """ + try: + import pymssql + except : + os.system("btpip install pymssql==2.3.0") + # os.system("btpip install pymssql==2.1.4") + import pymssql + + + if not self.__DB_CLOUD: + sa_path = 'data/sa.pl' + if os.path.exists(sa_path): self.__DB_PASS = public.readFile(sa_path) + self.__DB_PORT = self.get_port() + + try: + + if self.__DB_CLOUD: + try: + self.__DB_CONN = pymssql.connect(server=self.__DB_HOST, port=str(self.__DB_PORT), + user=self.__DB_USER, + password=self.__DB_PASS, database=None, login_timeout=30, + timeout=0, autocommit=True, charset="CP936", tds_version='7.0') + except: + self.__DB_ERR = 'Failed to connect to database! Check that the remote database information is correct' + return False + # self.__DB_CONN = pymssql.connect(server=self.__DB_HOST, port=str(self.__DB_PORT), + # user=self.__DB_USER, + # password=self.__DB_PASS, database=None, login_timeout=30, + # timeout=0, autocommit=True, charset="CP936", tds_version='7.0') + + else: + self.__DB_CONN = pymssql.connect(server=self.__DB_HOST, port=str(self.__DB_PORT), login_timeout=30, + timeout=0, autocommit=True, charset="CP936", tds_version='7.0') + self.__DB_CUR = self.__DB_CONN.cursor() #将数据库连接信息,赋值给cur。 + self.__DB_CUR = self.__DB_CONN.cursor() #将数据库连接信息,赋值给cur。 + if self.__DB_CUR: + return True + else: + self.__DB_ERR = 'Failed to connect to the database, please check whether SQL Server is installed' + return False + except Exception as ex: + self.__DB_ERR = public.get_error_info() + + return False + + def execute(self,sql): + + #执行SQL语句返回受影响行 + if not self.__Conn(): return self.__DB_ERR + try: + result = self.__DB_CUR.execute(sql) + + self.__Close() + return result; + except Exception as ex: + self.__DB_ERR = public.get_error_info() + return self.__DB_ERR + + def query(self,sql): + #执行SQL语句返回数据集 + if not self.__Conn(): return self.__DB_ERR + try: + self.__DB_CUR.execute(sql) + result = self.__DB_CUR.fetchall() + + #print(result) + #将元组转换成列表 + data = list(map(list,result)) + self.__Close() + return data + except Exception as ex: + self.__DB_ERR = public.get_error_info() + #public.WriteLog('SQL Server查询异常', self.__DB_ERR); + return str(ex) + + + #关闭连接 + def __Close(self): + self.__DB_CUR.close() + self.__DB_CONN.close() \ No newline at end of file diff --git a/class_v2/panel_mysql_v2.py b/class_v2/panel_mysql_v2.py new file mode 100644 index 00000000..d3f4e40c --- /dev/null +++ b/class_v2/panel_mysql_v2.py @@ -0,0 +1,231 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +import re,os,sys,public + +class panelMysql: + __DB_PASS = None + __DB_USER = 'root' + __DB_PORT = 3306 + __DB_HOST = 'localhost' + __DB_CONN = None + __DB_CUR = None + __DB_ERR = None + __DB_NET = None + #连接MYSQL数据库 + def __Conn(self): + if self.__DB_NET: return True + try: + myconf = public.readFile('/etc/my.cnf') + socket_re = re.search(r"socket\s*=\s*(.+)",myconf) + if socket_re: + socket = socket_re.groups()[0] + else: + socket = '/tmp/mysql.sock' + + try: + if sys.version_info[0] != 2: + try: + import pymysql + except: + public.ExecShell("pip install pymysql") + import pymysql + pymysql.install_as_MySQLdb() + import MySQLdb + if sys.version_info[0] == 2: + reload(MySQLdb) + except: + try: + import pymysql + pymysql.install_as_MySQLdb() + import MySQLdb + except Exception as e: + self.__DB_ERR = e + return False + try: + + rep = r"port\s*=\s*([0-9]+)" + self.__DB_PORT = int(re.search(rep,myconf).groups()[0]) + except: + self.__DB_PORT = 3306 + self.__DB_PASS = public.M('config').where('id=?',(1,)).getField('mysql_root') + + try: + self.__DB_CONN = MySQLdb.connect(host = self.__DB_HOST,user = self.__DB_USER,passwd = self.__DB_PASS,port = self.__DB_PORT,charset="utf8",connect_timeout=1,unix_socket=socket) + except MySQLdb.Error as e: + self.__DB_HOST = '127.0.0.1' + self.__DB_CONN = MySQLdb.connect(host = self.__DB_HOST,user = self.__DB_USER,passwd = self.__DB_PASS,port = self.__DB_PORT,charset="utf8",connect_timeout=1,unix_socket=socket) + self.__DB_CUR = self.__DB_CONN.cursor() + return True + except MySQLdb.Error as e: + self.__DB_ERR = e + return False + + #连接远程数据库 + def connect_network(self,host,port,username,password): + self.__DB_NET = True + try: + try: + if sys.version_info[0] != 2: + try: + import pymysql + except: + public.ExecShell("pip install pymysql") + import pymysql + pymysql.install_as_MySQLdb() + import MySQLdb + if sys.version_info[0] == 2: + reload(MySQLdb) + except: + try: + import pymysql + pymysql.install_as_MySQLdb() + import MySQLdb + except Exception as e: + self.__DB_ERR = e + return False + self.__DB_CONN = MySQLdb.connect(host = host,user = username,passwd = password,port = port,charset="utf8",connect_timeout=10) + self.__DB_CUR = self.__DB_CONN.cursor() + return True + except MySQLdb.Error as e: + self.__DB_ERR = e + return False + + + + def execute(self,sql): + #执行SQL语句返回受影响行 + if not self.__Conn(): return self.__DB_ERR + try: + result = self.__DB_CUR.execute(sql) + self.__DB_CONN.commit() + self.__Close() + return result + except Exception as ex: + return ex + + + def query(self,sql): + #执行SQL语句返回数据集 + if not self.__Conn(): return self.__DB_ERR + try: + self.__DB_CUR.execute(sql) + result = self.__DB_CUR.fetchall() + #将元组转换成列表 + if sys.version_info[0] == 2: + data = map(list,result) + else: + data = list(map(list,result)) + self.__Close() + return data + except Exception as ex: + return ex + + + #关闭连接 + def __Close(self): + self.__DB_CUR.close() + self.__DB_CONN.close() + + +# Mysql数据库连接类 支持Context +class PanelMysqlWithContext: + def __init__(self, db_name=None, db_user: str = 'root', db_pwd=None, db_host: str = 'localhost'): + self.__CONN = None + self.__DB_NAME = db_name + self.__HOST = db_host + self.__PORT = 3306 + self.__USERNAME = db_user + self.__PASSWORD = db_pwd + self.__CHARSET = 'utf8mb4' + self.__CONNECT_TIMEOUT = 10 + self.__UNIX_SOCK = None + + def __enter__(self): + if self.__CONN: + return self + + if self.__HOST in ('localhost', '127.0.0.1'): + self.__UNIX_SOCK = '/tmp/mysql.sock' + self.__CONNECT_TIMEOUT = 1 + + myconf = public.readFile('/etc/my.cnf') + m = re.search(r"socket\s*=\s*(.+)", myconf) + if m: + self.__UNIX_SOCK = m.group(1) + + m = re.search(r"port\s*=\s*([0-9]+)", myconf) + if m: + self.__PORT = int(m.group(1)) + + if self.__USERNAME == 'root': + self.__PASSWORD = public.M('config').where('id=?', (1,)).getField('mysql_root') + + import pymysql + + try: + self.__CONN = pymysql.connect(host=self.__HOST, user=self.__USERNAME, passwd=self.__PASSWORD, + port=self.__PORT, charset=self.__CHARSET, database=self.__DB_NAME, + connect_timeout=self.__CONNECT_TIMEOUT, + cursorclass=pymysql.cursors.DictCursor, unix_socket=self.__UNIX_SOCK) + except pymysql.Error: + if self.__HOST == 'localhost': + self.__HOST = '127.0.0.1' + self.__CONN = pymysql.connect(host=self.__HOST, user=self.__USERNAME, passwd=self.__PASSWORD, + port=self.__PORT, charset=self.__CHARSET, database=self.__DB_NAME, + connect_timeout=self.__CONNECT_TIMEOUT, + cursorclass=pymysql.cursors.DictCursor, unix_socket=self.__UNIX_SOCK) + raise + + return self + + def __del__(self): + if self.__CONN: + self.__CONN.close() + self.__CONN = None + + def __exit__(self, exc_type, exc_val, exc_tb): + self.__CONN.close() + self.__CONN = None + + # 执行SQL + def execute(self, sql): + cur = self.__CONN.cursor() + + try: + row_count = cur.execute(sql) + + self.__CONN.commit() + + return row_count + finally: + cur.close() + + # 查询多条 + def query(self, sql): + cur = self.__CONN.cursor() + + try: + row_count = cur.execute(sql) + + if row_count == 0: + return [] + + return cur.fetchall() + finally: + cur.close() + + # 查询单条 + def find(self, sql): + ret = self.query(sql) + + if len(ret) == 0: + return None + + return ret[0] diff --git a/class_v2/panel_php_v2.py b/class_v2/panel_php_v2.py new file mode 100644 index 00000000..e3fe0652 --- /dev/null +++ b/class_v2/panel_php_v2.py @@ -0,0 +1,622 @@ +#coding:utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +# +------------------------------------------------------------------- +# | PHP插件兼容模块 +# +------------------------------------------------------------------- + +import json,os,public,time,re,sys +import time +import fastcgiClient as fcgi_client +import struct +FCGI_Header = '!BBHHBx' + +if sys.version_info[0] == 2: + try: + from cStringIO import StringIO + except: + from StringIO import StringIO +else: + from io import BytesIO as StringIO + +class panelPHP: + re_io = None + def __init__(self,plugin_name = None): + if plugin_name: + self.__plugin_name = plugin_name + self.__plugin_path = "/www/server/panel/plugin/%s" % plugin_name + self.__args_dir = self.__plugin_path + '/args' + self.__args_tmp = self.__args_dir + '/' + public.GetRandomString(32) + if not os.path.exists(self.__args_dir): os.makedirs(self.__args_dir, 384) + + #调用PHP插件 + def exec_php_script(self,args): + #取PHP执行文件和CLI配置参数 + php_bin = self.__get_php_bin() + if not php_bin: return public.returnMsg(False,'PHP_NOT_FOUND') + #是否将参数写到文件 + self.__write_args(args) + result = os.popen("cd " + self.__plugin_path + " && %s /www/server/panel/class/panel_php_run.php --args_tmp=\"%s\" --plugin_name=\"%s\" --fun=\"%s\"" % + (php_bin,self.__args_tmp,self.__plugin_name,args.s)).read() + try: + #解析执行结果 + result = json.loads(result) + except: pass + #删除参数文件 + if os.path.exists(self.__args_tmp): + os.remove(self.__args_tmp) + return result + + #将参数写到文件 + def __write_args(self,args): + from BTPanel import request + if os.path.exists(self.__args_tmp): os.remove(self.__args_tmp) + self.__clean_args_file() + data = {} + data['GET'] = request.args.to_dict() + data['POST'] = {} + for key in request.form.keys(): + data['POST'][key] = str(request.form.get(key,'')) + data['POST']['client_ip'] = public.GetClientIp() + data = json.dumps(data) + public.writeFile(self.__args_tmp,data) + + #清理参数文件 + def __clean_args_file(self): + args_dir = self.__plugin_path + '/args' + if not os.path.exists(args_dir): return False + now_time = time.time() + for f_name in os.listdir(args_dir): + filename = args_dir + '/' + f_name + if not os.path.exists(filename): continue + #清理创建时间超过60秒的参数文件 + if now_time - os.path.getctime(filename) > 60: os.remove(filename) + + #取PHP-CLI执行命令 + def __get_php_bin(self): + #如果有指定兼容的PHP版本 + php_v_file = self.__plugin_path + '/php_version.json' + if os.path.exists(php_v_file): + php_vs = json.loads(public.readFile(php_v_file).replace('.','')) + else: + #否则兼容所有版本 + php_vs = public.get_php_versions(True) + #判段兼容的PHP版本是否安装 + php_path = "/www/server/php/" + php_v = None + for pv in php_vs: + php_bin = php_path + pv + "/bin/php" + if os.path.exists(php_bin): + php_v = pv + break + #如果没安装直接返回False + if not php_v: return False + #处理PHP-CLI-INI配置文件 + php_ini = self.__plugin_path + '/php_cli_'+php_v+'.ini' + if not os.path.exists(php_ini): + #如果不存在,则从PHP安装目录下复制一份 + src_php_ini = php_path + php_v + '/etc/php.ini' + import shutil + shutil.copy(src_php_ini,php_ini) + #解除所有禁用函数 + php_ini_body = public.readFile(php_ini) + php_ini_body = re.sub(r"disable_functions\s*=.*","disable_functions = ",php_ini_body) + php_ini_body = re.sub(r".*bt_filter.+","",php_ini_body) + public.writeFile(php_ini,php_ini_body) + return php_path + php_v + '/bin/php -c ' + php_ini + + def get_php_version(self,php_version): + if php_version: + if not isinstance(php_version,list): + php_vs = [php_version] + else: + php_vs = sorted(php_version,reverse=True) + else: + php_vs = public.get_php_versions(True) + php_path = "/www/server/php/" + php_v = None + for pv in php_vs: + php_bin = php_path + pv + "/bin/php" + if os.path.exists(php_bin) and os.path.exists("/tmp/php-cgi-{}.sock".format(pv)): + php_v = pv + break + return php_v + +# def get_phpmyadmin_phpversion(self): +# ''' +# @name 获取当前phpmyadmin设置的PHP版本 +# @author hwliang<2020-07-13> +# @return string +# ''' +# from BTPanel import cache +# ikey = 'pma_phpv' +# phpv = cache.get(ikey) +# if phpv: return phpv +# webserver = public.get_webserver() +# if webserver == 'nginx': +# filename = public.GetConfigValue('setup_path') + '/nginx/conf/enable-php.conf' +# conf = public.readFile(filename) +# if not conf: return None +# rep = r"php-cgi-(\d+)\.sock" +# phpv = re.findall(rep,conf) +# elif webserver == 'openlitespeed': +# filename = public.GetConfigValue('setup_path') + "/panel/vhost/openlitespeed/detail/phpmyadmin.conf" +# conf = public.readFile(filename) +# if not conf: return None +# rep = r"/usr/local/lsws/lsphp(\d+)/bin/lsphp" +# phpv = re.findall(rep,conf) +# else: +# filename = public.GetConfigValue('setup_path') + '/apache/conf/extra/httpd-vhosts.conf' +# conf = public.readFile(filename) +# if not conf: return None +# rep = r"php-cgi-(\d+)\.sock" +# phpv = re.findall(rep,conf) + +# if not phpv: return None +# cache.set(ikey,phpv[0],3) +# return phpv[0] + +# def get_pma_root(self): +# ''' +# @name 获取phpmyadmin根目录 +# @author hwliang<2020-07-13> +# @return string +# ''' +# pma_path = '/www/server/phpmyadmin/' +# if not os.path.exists(pma_path): +# os.makedirs(pma_path) +# for dname in os.listdir(pma_path): +# if dname.find('phpmyadmin_') != -1: +# return os.path.join(pma_path,dname) +# return None + +# def check_phpmyadmin_phpversion(self): +# ''' +# @name 检查当前phpmyadmin版本可用的php版本列表 +# @author hwliang<2020-07-13> +# @return list +# ''' +# return +# pma_path = '/www/server/phpmyadmin/' +# pma_version_f1 = os.path.join(pma_path,'version_check.pl') +# pma_root = os.path.join(pma_path,'pma') +# pma_version_f2 = os.path.join(pma_root,'version_check.pl') +# if not os.path.exists(pma_version_f1): +# src_vfile = os.path.join(pma_path,'version.pl') +# if os.path.exists(src_vfile): +# public.writeFile(pma_version_f1,public.readFile(src_vfile)) +# v_sync = public.readFile(pma_version_f1) == public.readFile(pma_version_f2) +# +# if not os.path.exists(pma_root + '/index.php') or not v_sync: +# o_pma_root = self.get_pma_root() +# +# if o_pma_root: +# if not os.path.exists(pma_root): +# os.makedirs(pma_root) +# public.ExecShell(r"\cp -arf {}/* {}/".format(o_pma_root,pma_root)) +# public.ExecShell("chown -R www:www {}".format(pma_root)) +# public.ExecShell("chmod -R 700 {}".format(pma_root)) +# public.ExecShell(r"\cp -arf {} {}".format(pma_version_f1,pma_version_f2)) +# index = public.readFile(pma_root + '/index.php') +# if index: +# if index.find("use PhpMyAdmin\\Util") != -1: +# resp = "use PhpMyAdmin\\Util;\nif(function_exists('opcache_invalidate')) opcache_invalidate('/www/server/phpmyadmin/pma/config.inc.php');" +# index = index.replace("use PhpMyAdmin\\Util;",resp) +# elif index.find(r"use PMA\libraries\LanguageManager;") != -1: +# resp = "use PMA\\libraries\\LanguageManager;\nif(function_exists('opcache_invalidate')) opcache_invalidate('/www/server/phpmyadmin/pma/config.inc.php');" +# index = index.replace(r"use PMA\libraries\LanguageManager;",resp) +# elif index.find("require_once 'libraries/common.inc.php';") != -1: +# resp = "if(function_exists('opcache_invalidate')) opcache_invalidate('/www/server/phpmyadmin/pma/config.inc.php');\nrequire_once 'libraries/common.inc.php';" +# index = index.replace("require_once 'libraries/common.inc.php';",resp) +# +# +# public.writeFile(pma_root + '/index.php',index) +# +# if not os.path.exists(pma_version_f2): +# return False +# +# pma_version = public.readFile(pma_version_f2) +# self.pma_version = pma_version +# if pma_version: +# pma_version = pma_version[:3] +# +# if pma_version == '4.4': +# return ['53','54','55','56'] +# elif pma_version == '4.0': +# return ['52','53'] +# elif pma_version == '4.6': +# return None +# elif pma_version == '4.7': +# return ['55','56','70','71','72'] +# elif pma_version in ['4.8','4.9','5.0']: +# return ['70','71','72','73','74'] +# else: +# return ['55','56','70','71','72'] +# +# def get_mysql_port(self): +# ''' +# @name 获取mysql当前端口号 +# @author hwliang<2020-07-13> +# @return int +# ''' +# try: +# myconf = public.readFile('/etc/my.cnf') +# rep = r"port\s*=\s*([0-9]+)" +# port = int(re.search(rep,myconf).groups()[0]) +# if not port: port = 3306 +# return port +# except: +# return 3306 +# +# def write_pma_passwd(self,username,password): +# ''' +# @name 写入mysql帐号密码到配置文件 +# @author hwliang<2020-07-13> +# @param username string(用户名) +# @param password string(密码) +# @return bool +# ''' +# +# self.check_phpmyadmin_phpversion() +# pconfig = 'cookie' +# if username: +# pconfig = 'config' +# pma_path = '/www/server/phpmyadmin/' +# pma_config_file = os.path.join(pma_path,'pma/config.inc.php') +# conf = public.readFile(pma_config_file) +# if not conf: return False +# rep = r"/\* Authentication type \*/(.|\n)+/\* Server parameters \*/" +# rstr = '''/* Authentication type */ +# $cfg['Servers'][$i]['auth_type'] = '{}'; +# $cfg['Servers'][$i]['host'] = 'localhost'; +# $cfg['Servers'][$i]['port'] = '{}'; +# $cfg['Servers'][$i]['user'] = '{}'; +# $cfg['Servers'][$i]['password'] = '{}'; +# /* Server parameters */'''.format(pconfig,self.get_mysql_port(),username,password) +# conf = re.sub(rep,rstr,conf) +# public.writeFile(pma_config_file,conf) +# return True +# +# def request_php(self,uri): +# ''' +# @name 发起fastcgi请求到PHP-FPM +# @author hwliang<2020-07-11> +# @param puri string(URI地址) +# @return socket +# ''' +# php_unix_socket = '/tmp/php-cgi-{}.sock'.format(self.php_version) +# f = FPM(php_unix_socket,self.document_root,self.last_path) +# +# if request.full_path.find('?') != -1: +# uri = request.full_path[request.full_path.find(uri):] +# if self.re_io: +# sock = f.load_url(uri,content=self.re_io) +# else: +# sock = f.load_url(uri,content=request.stream) +# return sock +# +# def start(self,puri,document_root,last_path = ''): +# ''' +# @name 开始处理PHP请求 +# @author hwliang<2020-07-11> +# @param puri string(URI地址) +# @return socket or Response +# ''' +# if puri in ['/','',None]: puri = 'index.php' +# if puri[0] == '/': puri = puri[1:] +# self.document_root = document_root +# self.last_path = last_path +# filename = document_root + puri +# +# +# #如果是PHP文件 +# if puri[-4:] == '.php': +# if request.path.find('/phpmyadmin/') != -1: +# ikey = 'pma_php_version' +# self.php_version = cache.get(ikey) +# if not self.php_version: +# php_version = self.get_phpmyadmin_phpversion() +# php_versions = self.check_phpmyadmin_phpversion() +# if not php_versions: +# if php_versions == False: +# return Resp( +# 'Phpmyadmin is not installed, or support for phpMyAdmin4.6 has been discontinued due to security issues, uninstall and install other secure versions in the software store!') +# else: +# return Resp('phpmyadmin is not installed') +# if not php_version or not php_version in php_versions: +# php_version = php_versions +# self.php_version = self.get_php_version(php_version) +# if not self.php_version: +# php_version = self.check_phpmyadmin_phpversion() +# self.php_version = self.get_php_version(php_version) +# if not php_version: +# return Resp('No supported PHP version found: {}'.format(php_versions)) +# +# if not self.php_version in php_versions: +# self.php_version = self.get_php_version(php_versions) +# +# if not self.php_version: +# return Resp('No supported PHP version found: {}'.format(php_versions)) +# cache.set(ikey,self.php_version,1) +# if request.method == 'POST': +# #登录phpmyadmin +# if puri in ['index.php','/index.php']: +# content = public.url_encode(request.form.to_dict()) +# if not isinstance(content,bytes): +# content = content.encode() +# self.re_io = StringIO(content) +# username = request.form.get('pma_username') +# if username: +# password = request.form.get('pma_password') +# if not self.write_pma_passwd(username,password): +# return Resp('Phpmyadmin is not installed') +# +# if puri in ['logout.php', '/logout.php']: +# self.write_pma_passwd(None, None) +# else: +# src_path = '/www/server/panel/adminer' +# dst_path = '/www/server/adminer' +# if os.path.exists(src_path): +# if not os.path.exists(dst_path): os.makedirs(dst_path) +# public.ExecShell(r"\cp -arf {}/* {}/".format(src_path, dst_path)) +# public.ExecShell("chown -R www:www {}".format(dst_path)) +# public.ExecShell("chmod -R 700 {}".format(dst_path)) +# public.ExecShell("rm -rf {}".format(src_path)) +# +# if not os.path.exists(dst_path + '/index.php'): +# return Resp("The AdMiner file is missing. Please try again after the [Fix] panel on the first page!") +# +# ikey = 'aer_php_version' +# self.php_version = cache.get(ikey) +# if not self.php_version: +# self.php_version = self.get_php_version(None) +# cache.set(ikey, self.php_version, 10) +# if not self.php_version: +# return Resp('没有找到可用的PHP版本') +# +# #文件是否存在? +# if not os.path.exists(filename): +# return abort(404) +# +# #发送到FPM +# try: +# return self.request_php(puri) +# except Exception as ex: +# if str(ex).find('No such file or directory') != -1: +# return Resp('Specify PHP version: {}, not started, or unable to connect!'.format(self.php_version)) +# return Resp(str(ex)) +# +# if not os.path.exists(filename): +# return abort(404) +# +# #如果是静态文件 +# return send_file(filename) + + + + #获取头部128KB数据 + def get_header_data(self,sock): + ''' + @name 获取头部32KB数据 + @author hwliang<2020-07-11> + @param sock socketobject(fastcgi套接字对象) + @return bytes + ''' + headers_data = b'' + total_len = 0 + header_len = 1024 * 128 + while True: + fastcgi_header = sock.recv(8) + if not fastcgi_header: break + if len(fastcgi_header) != 8: + headers_data += fastcgi_header + break + fast_pack = struct.unpack(FCGI_Header, fastcgi_header) + if fast_pack[1] == 3: break + + tlen = fast_pack[3] + while tlen > 0: + sd = sock.recv(tlen) + if not sd: break + headers_data += sd + tlen -= len(sd) + + total_len += fast_pack[3] + if fast_pack[4]: + sock.recv(fast_pack[4]) + if total_len > header_len: break + return headers_data + + #格式化响应头 + def format_header_data(self,headers_data): + ''' + @name 格式化响应头 + @author hwliang<2020-07-11> + @param headers_data bytes(fastcgi头部32KB数据) + @return status int(响应状态), headers dict(响应头), bdata bytes(格式化响应头后的多余数据) + ''' + status = '200 OK' + headers = {} + pos = 0 + while True: + eolpos = headers_data.find(b'\n', pos) + if eolpos < 0: break + line = headers_data[pos:eolpos-1] + pos = eolpos + 1 + line = line.strip() + if len(line) < 2: break + if line.find(b':') == -1: continue + header, value = line.split(b':', 1) + header = header.strip() + value = value.strip() + if isinstance(header,bytes): + header = header.decode() + value = value.decode() + if header == 'Status': + status = value + if status.find(' ') < 0: + status += ' BTPanel' + else: + headers[header] = value + bdata = headers_data[pos:] + status = int(status.split(' ')[0]) + return status,headers,bdata + + #以流的方式发送剩余数据 + def resp_sock(self,sock,bdata): + ''' + @name 以流的方式发送剩余数据 + @author hwliang<2020-07-11> + @param sock socketobject(fastcgi套接字对象) + @param bdata bytes(格式化响应头后的多余数据) + @return yield bytes + ''' + #发送除响应头以外的多余头部数据 + yield bdata + while True: + fastcgi_header = sock.recv(8) + if not fastcgi_header: break + if len(fastcgi_header) != 8: + yield fastcgi_header + break + fast_pack = struct.unpack(FCGI_Header, fastcgi_header) + if fast_pack[1] == 3: break + tlen = fast_pack[3] + while tlen > 0: + sd = sock.recv(tlen) + if not sd: break + tlen -= len(sd) + if sd: + yield sd + + if fast_pack[4]: + sock.recv(fast_pack[4]) + sock.close() + + + + +class FPM(object): + def __init__(self,sock=None, document_root='',last_path = ''): + ''' + @name 实例化FPM对象 + @author hwliang<2020-07-11> + @param sock string(unixsocket路径) + @param document_root string(PHP文档根目录) + @return FPM + ''' + if sock: + self.fcgi_sock = sock + if document_root[-1:] != '/': + document_root += '/' + self.document_root = document_root + self.last_path = last_path + + def load_url(self, url, content=b''): + ''' + @name 转发URL到PHP-FPM + @author hwliang<2020-07-11> + @param url string(URI地址) + @param content stream(POST数据io对象) + @return fastcgi-socket + ''' + fcgi = fcgi_client.FCGIApp(connect=self.fcgi_sock) + try: + script_name, query_string = url.split('?') + except ValueError: + script_name = url + query_string = '' + from BTPanel import request + env = { + 'SCRIPT_FILENAME': '%s%s' % (self.document_root, script_name), + 'QUERY_STRING': query_string, + 'REQUEST_METHOD': request.method, + 'SCRIPT_NAME': self.last_path + script_name, + 'REQUEST_URI': self.last_path + url, + 'GATEWAY_INTERFACE': 'CGI/1.1', + 'SERVER_SOFTWARE': 'BT-Panel', + 'REDIRECT_STATUS': '200', + 'CONTENT_TYPE': request.headers.get('Content-Type','application/x-www-form-urlencoded'), + 'CONTENT_LENGTH': str(request.headers.get('Content-Length','0')), + 'DOCUMENT_URI': request.path, + 'DOCUMENT_ROOT': self.document_root, + 'SERVER_PROTOCOL' : 'HTTP/1.1', + 'REMOTE_ADDR': request.remote_addr.replace('::ffff:',''), + 'REMOTE_PORT': str(request.environ.get('REMOTE_PORT')), + 'SERVER_ADDR': request.headers.get('host'), + 'SERVER_PORT': '80', + 'SERVER_NAME': 'BT-Panel', + } + + for k in request.headers.keys(): + key = 'HTTP_' + k.replace('-','_').upper() + env[key] = request.headers[k] + fpm_sock = fcgi(env, content) + return fpm_sock + + def load_url_public(self,url,content = b'',method='GET',content_type='application/x-www-form-urlencoded'): + ''' + @name 转发URL到PHP-FPM 公共 + @author hwliang<2020-07-11> + @param url string(URI地址) + @param content stream(POST数据io对象) + @return fastcgi-socket + ''' + fcgi = fcgi_client.FCGIApp(connect=self.fcgi_sock) + try: + script_name, query_string = url.split('?') + except ValueError: + script_name = url + query_string = '' + + content_length = len(content) + if content: + content = StringIO(content) + + env = { + 'SCRIPT_FILENAME': '%s%s' % (self.document_root, script_name), + 'QUERY_STRING': query_string, + 'REQUEST_METHOD': method, + 'SCRIPT_NAME': self.last_path + script_name, + 'REQUEST_URI': url, + 'GATEWAY_INTERFACE': 'CGI/1.1', + 'SERVER_SOFTWARE': 'BT-Panel', + 'REDIRECT_STATUS': '200', + 'CONTENT_TYPE': content_type, + 'CONTENT_LENGTH': str(content_length), + 'DOCUMENT_URI': script_name, + 'DOCUMENT_ROOT': self.document_root, + 'SERVER_PROTOCOL' : 'HTTP/1.1', + 'REMOTE_ADDR': '127.0.0.1', + 'REMOTE_PORT': '8888', + 'SERVER_ADDR': '127.0.0.1', + 'SERVER_PORT': '80', + 'SERVER_NAME': 'BT-Panel' + } + + fpm_sock = fcgi(env, content) + _data = b'' + while True: + fastcgi_header = fpm_sock.recv(8) + if not fastcgi_header: break + if len(fastcgi_header) != 8: + _data += fastcgi_header + break + fast_pack = struct.unpack(FCGI_Header, fastcgi_header) + if fast_pack[1] == 3: break + tlen = fast_pack[3] + while tlen > 0: + sd = fpm_sock.recv(tlen) + if not sd: break + tlen -= len(sd) + _data += sd + if fast_pack[4]: + fpm_sock.recv(fast_pack[4]) + status,headers,data = panelPHP().format_header_data(_data) + return data diff --git a/class_v2/panel_ping_v2.py b/class_v2/panel_ping_v2.py new file mode 100644 index 00000000..dee5f39f --- /dev/null +++ b/class_v2/panel_ping_v2.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +#coding:utf-8 +import os, sys, socket, struct, select, time + +def checksum(source_string): + sum = 0 + countTo = (len(source_string)/2)*2 + count = 0 + while count> 16) + (sum & 0xffff) + sum = sum + (sum >> 16) + answer = ~sum + answer = answer & 0xffff + answer = answer >> 8 | (answer << 8 & 0xff00) + return answer + +def receive_one_ping(my_socket, ID, timeout): + timeLeft = timeout + while True: + startedSelect = time.time() + whatReady = select.select([my_socket], [], [], timeLeft) + howLongInSelect = (time.time() - startedSelect) + if whatReady[0] == []: return + timeReceived = time.time() + recPacket, addr = my_socket.recvfrom(1024) + icmpHeader = recPacket[20:28] + type, code, checksum, packetID, sequence = struct.unpack("bbHHh", icmpHeader) + if packetID == ID: + bytesInDouble = struct.calcsize("d") + timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0] + return timeReceived - timeSent + timeLeft = timeLeft - howLongInSelect + if timeLeft <= 0: return + +def send_one_ping(my_socket, dest_addr, ID): + dest_addr = socket.gethostbyname(dest_addr) + my_checksum = 0 + ICMP_ECHO_REQUEST = 8 + header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1) #压包 + bytesInDouble = struct.calcsize("d") + data = (192 - bytesInDouble) * "Q" + data = struct.pack("d", time.time()) + data + my_checksum = checksum(header + data) + header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1) + packet = header + data + my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1 + +def do_one(dest_addr, timeout): + icmp = socket.getprotobyname("icmp") + try: + my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) + except socket.error, (errno, msg): + if errno == 1: + msg = msg + ( + " - Note that ICMP messages can only be sent from processes" + " running as root." + ) + raise socket.error(msg) + raise # raise the original error + + my_ID = os.getpid() & 0xFFFF + send_one_ping(my_socket, dest_addr, my_ID) + delay = receive_one_ping(my_socket, my_ID, timeout) + my_socket.close() + return delay + +def get_ping(timeout = 0.5): + try: + delay = do_one(dest_addr, timeout) + except socket.gaierror, e: + return -2 + + if delay == None: + return -1 + else: + delay = delay * 1000 + return delay + +if __name__ == '__main__': + print verbose_ping(sys.argv[1],2) diff --git a/class_v2/panel_plugin_v2.py b/class_v2/panel_plugin_v2.py new file mode 100644 index 00000000..52a0d4a6 --- /dev/null +++ b/class_v2/panel_plugin_v2.py @@ -0,0 +1,2849 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- +import public +import os +import sys +import json +import time +import psutil +import re +import shutil +import requests +from BTPanel import session, cache, send_file + + +if sys.version_info[0] == 3: from importlib import reload + + +class mget: pass + + +class panelPlugin: + __list = 'data/list.json' + __type = 'data/type.json' + __index = 'config/index.json' + __link = 'config/link.json' + + __official_url = 'https://www.aapanel.com' + # __official_url = 'http://dev.aapanel.com' + + def __init__(self): + self.__isTable = None + self.__tasks = None + self.__product_list = None + self.__plugin_list = None + self.__exists_names = {} + self.__plugin_s_list = [] + self.__plugin_info = None + self.__plugin_name = None + self.__plugin_object = None + self.__plugin_list = None + self.__panel_path = '/www/server/panel' + self.__plugin_path = self.__panel_path + '/plugin/' + self.__plugin_save_file = self.__panel_path + '/data/plugin_bin.pl' + self.__api_root_url = self.__official_url + '/api' + self.__api_url = self.__api_root_url + '/panel/get_plugin_list' + self.__download_url = self.__api_root_url + '/panel/download_plugin' + self.__download_d_main_url = self.__api_root_url + '/panel/download_plugin_main' + self._check_url = self.__api_root_url + '/panel/get_soft_list_status' + self._unbinding_url = self.__api_root_url + '/panel/get_unbinding' + self.__tmp_path = self.__panel_path + '/temp/' + self.__plugin_timeout = 3600 + self.__is_php = False + self.__install_opt = 'i' + self.__pid = 0 + self.__path_error = self.__panel_path + '/data/error_pl.pl' + self.__error_html = '/www/server/panel/BTPanel/templates/default/block_error.html' + self.__sub_rules = [] + self.__replace_rule = [] + + self.pids = None + self.ROWS = 15 + + self.__install_path = '/www/server/panel/plugin' + + if not self.__tasks: + try: + self.__tasks = public.M('tasks').where("status!=?", ('1',)).field('status,name').select() + except: + self.__tasks = [] + + if not os.path.exists(self.__tmp_path): + os.makedirs(self.__tmp_path, 0o755) + + # 检查依赖 + def check_deps(self,get): + cacheKey = 'plugin_lib_list' + if not 'force' in get: + libList = cache.get(cacheKey) + if libList: return libList + libList = json.loads(public.readFile('config/lib.json')) + centos = os.path.exists('/bin/yum') + for key in libList.keys(): + for i in range(len(libList[key])): + checks = libList[key][i]['check'].split(',') + libList[key][i]['status'] = False + for check in checks: + if os.path.exists(check): + libList[key][i]['status'] = True + break + libList[key][i]['version'] = "-" + if libList[key][i]['status']: + shellTmp = libList[key][i]['getv'].split(':D') + shellEx = shellTmp[0] + if len(shellTmp) > 1 and not centos: shellEx = shellTmp[1] + libList[key][i]['version'] = public.ExecShell(shellEx)[0].strip() + cache.set(cacheKey,libList,86400) + return libList + + #检测关键目录是否可以被写入文件 + def check_sys_write(self): + test_file = '/etc/init.d/bt_10000100.pl' + public.writeFile(test_file,'True') + if os.path.exists(test_file): + if public.readFile(test_file) == 'True': + os.remove(test_file) + return True + os.remove(test_file) + return False + + #检查互斥 + def check_mutex(self,mutex): + if mutex == -1: return True + mutexs = mutex.split(',') + for name in mutexs: + pluginInfo = self.get_soft_find(name) + if not pluginInfo: continue + if pluginInfo['setup'] == True: + self.mutex_title = pluginInfo['title'] + return False + return True + + #检查依赖 + def check_dependent(self,dependent): + if not dependent: return True + dependents = dependent.split(',') + status = True + for dep in dependents: + if not dep: continue + if dep.find('|') != -1: + names = dep.split('|') + for name in names: + pluginInfo = self.get_soft_find(name) + if not pluginInfo: return True + if pluginInfo['setup'] == True: + status = True + break + else: + status = False + else: + pluginInfo = self.get_soft_find(dep) + if pluginInfo['setup'] != True: + status = False + break + return status + + #检查CPU限制 + def check_cpu_limit(self,cpuLimit): + if psutil.cpu_count() < cpuLimit: return False + return True + + #检查内存限制 + def check_mem_limit(self,memLimit): + if psutil.virtual_memory().total/1024/1024 < memLimit: return False + return True + + #检查操作系统限制 + def check_os_limit(self,osLimit): + if osLimit == 0: return True + if osLimit == 1: + centos = os.path.exists('/usr/bin/yum') + return centos + elif osLimit == 2: + debian = os.path.exists('/usr/bin/apt-get') + return debian + return True + + # 检查安装限制 + def check_install_limit(self,get): + if not hasattr(get,'pluginInfo'): + pluginInfo = self.get_soft_find(get.sName) + else: + pluginInfo = get.pluginInfo + p_node = '/www/server/panel/install/public.sh' + if os.path.exists(p_node): + if len(public.readFile(p_node)) < 100: os.remove(p_node) + if not pluginInfo: return public.return_msg_gettext(False, 'The specified plugin does not exist!') + self.mutex_title = pluginInfo['mutex'] + if not self.check_mutex(pluginInfo['mutex']): return public.return_msg_gettext(False, 'Please uninstall [{}] first', + (self.mutex_title,)) + if not hasattr(get, 'id'): + if not self.check_dependent(pluginInfo['dependent']): return public.return_msg_gettext(False, 'Depends on the following software, please install [{}] first', + (pluginInfo['dependent'],)) + if 'version' in get: + for versionInfo in pluginInfo['versions']: + if versionInfo['m_version'] != get.version: continue + if not 'type' in get: get.type = '0' + if int(get.type) > 4: get.type = '0' + if get.type == '0': + if not self.check_cpu_limit(versionInfo['cpu_limit']): + return public.return_msg_gettext(False,'At least [{0}] CPU cores are required to install'.format(versionInfo['cpu_limit'])) + if not self.check_mem_limit(versionInfo['mem_limit']): + return public.return_msg_gettext(False,'At least [{0} MB] memory is required to install'.format(versionInfo['mem_limit'])) + if not self.check_os_limit(versionInfo['os_limit']): + m_ps = {0: "All", 1: "Centos", 2: "Ubuntu/Debian"} + return public.return_msg_gettext(False, 'Only supports [{}] system',(m_ps[int(versionInfo['os_limit'])],)) + if not hasattr(get, 'id'): + if not self.check_dependent(versionInfo['dependent']): return public.return_msg_gettext(False,'Depend on the following software, please install first [{}]',(versionInfo['dependent'],)) + + # 获取插件安装包下载进度 + def get_download_speed(self, get): + ''' + @name 获取插件下载进度 + @author hwliang<2021-06-25> + @param plugin_name 插件名称 + @return dict + ''' + result = self.__get_download_speed(get.plugin_name) + return result + + # 取消下载 + def close_install(self, get): + ''' + @name 取消指定插件安装过程 + @author hwliang<2021-07-07> + @param plugin_name 插件名称 + @return void + ''' + plugin_name = get.plugin_name.strip() + tmp_path = '{}/{}'.format(self.__tmp_path, plugin_name) + if os.path.exists(tmp_path): shutil.rmtree(tmp_path) + return public.returnMsg(False, '安装过程已取消!') + + #安装插件 + def install_plugin(self,get): + str1 = public.get_msg_gettext("System critical directory is not writable!") + str2 = public.get_msg_gettext("1. If [System Hardening] is installed, please turn it off") + str3 = public.get_msg_gettext("2. If Yunsuo is installed, please turn off [System Hardening] feature") + str4 = public.get_msg_gettext("3. If Safedog is installed, please turn off [System Protection] feature") + str5 = public.get_msg_gettext("4. If other security software is used, please uninstall it") + if not self.check_sys_write(): return public.return_msg_gettext(False,'ERROR:{}
                                    {}

                                    {}
                                    {}

                                    '.format(str1,str2,str3,str4)) + if not 'sName' in get: return public.return_msg_gettext(False,'Please specify the software name!') + #处理ols还不支持php81的情况 + # if get.sName == "php-8.1" and public.get_webserver() == 'openlitespeed': + # return public.return_msg_gettext(False, 'Sorry, currently OLS official does not support php8.1') + pluginInfo = self.get_soft_find(get.sName) + get.pluginInfo = pluginInfo + check_result = self.check_install_limit(get) + if check_result: + return check_result + if pluginInfo['name'] in ['dns_manager','mail_sys']: + pluginInfo['type'] = 5 + + if pluginInfo['type'] != 5: + result = self.install_sync(pluginInfo,get) + else: + result = self.install_async(pluginInfo,get) + try: + if 'status' in result: + if result['status']: + public.arequests('post','{}/api/setupCount/setupPlugin'.format(self.__official_url),data={"pid":pluginInfo['id'],'p_name':pluginInfo['name']},timeout=3) + # get.force = 1 + # self.get_cloud_list(get) + except:pass + return result + + #同步安装 + def install_sync(self,pluginInfo,get): + import panelAuth + try: + token = panelAuth.panelAuth().create_serverid(None)['token'] + except: + # return public.returnMsg(False,'Please log in as aaPanel account first') + token = None + if 'download' in pluginInfo['versions'][0]: + tmp_path = '/www/server/panel/temp' + if not os.path.exists(tmp_path): os.makedirs(tmp_path,mode=384) + public.ExecShell("rm -rf " + tmp_path + '/*') + toFile = tmp_path + '/' + pluginInfo['name'] + '.zip' + public.downloadFile('{}/api/plugin/download?filename={}&token={}'.format( + self.__official_url, + pluginInfo['versions'][0]['download'], + token + ),toFile) + if public.FileMd5(toFile) != pluginInfo['versions'][0]['md5']: return public.return_msg_gettext(False,'File hash verification failed, stop installation!') + update = False + if os.path.exists(pluginInfo['install_checks']): update =pluginInfo['versions'][0]['version_msg'] + return self.update_zip(None,toFile,update) + else: + # download_url = public.get_url() + '/install/plugin/' + pluginInfo['name'] + '_en/install.sh' + # toFile = '/tmp/%s.sh' % pluginInfo['name'] + # public.downloadFile(download_url,toFile) + # self.set_pyenv(toFile) + # public.ExecShell('/bin/bash ' + toFile + ' install &> /tmp/panelShell.pl') + # if os.path.exists(pluginInfo['install_checks']): + # public.write_log_gettext('Installer','Successfully installed plugin [{}]',(pluginInfo['title'],)) + # if os.path.exists(toFile): os.remove(toFile) + # return public.return_msg_gettext(True,'Installation succeeded!') + # return public.return_msg_gettext(False,'Installation failed') + + if hasattr(get, 'min_version'): + get.version += '.' + get.min_version + + return self.__install_plugin(pluginInfo['name'], get.version) + + # 设置Python环境变量 + def set_pyenv(self, filename): + if not os.path.exists(filename): return False + env_py = '/www/server/panel/pyenv/bin' + if not os.path.exists(env_py): return False + temp_file = public.readFile(filename) + env_path = ['PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin'] + rep_path = ['PATH={}/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin'.format(env_py + ":")] + for i in range(len(env_path)): + temp_file = temp_file.replace(env_path[i], rep_path[i]) + public.writeFile(filename, temp_file) + return True + + #异步安装 + def install_async(self,pluginInfo,get): + # # 只取主版本号与子版本号,忽略修订号 + # if 'version' in get: + # get.version = '.'.join(str(get.version).split('.')[:2]) + + mtype = 'install'; + mmsg = public.get_msg_gettext('Install') + if hasattr(get, 'upgrade'): + mtype = 'update' + mmsg = 'upgrade' + if not 'type' in get: get.type = '0' + if int(get.type) > 4: get.type = '0' + if get.sName == 'nginx': + if get.version == '1.8': return public.return_msg_gettext(False,'Nginx 1.8.1 is too old, no longer available, please choose another version!') + if get.sName.find('php-') != -1:get.sName = get.sName.split('-')[0] + ols_execstr = "" + if "php" == get.sName and os.path.exists('/usr/local/lsws/bin/lswsctrl'): + ols_sName = 'php-ols' + ols_version = get.version.replace('.','') + ols_execstr = " &> /tmp/panelExec.log && /bin/bash install_soft.sh {} {} " + ols_sName + " " + ols_version + php_path = '/www/server/php' + if not os.path.exists(php_path): os.makedirs(php_path) + apacheVersion='false' + if public.get_webserver() == 'apache': + apacheVersion = public.xss_version(public.readFile('/www/server/apache/version.pl')) + public.writeFile('/var/bt_apacheVersion.pl',apacheVersion) + public.writeFile('/var/bt_setupPath.conf','/www') + if os.path.exists('/usr/bin/apt-get'): + if get.type == '0': + get.type = '3' + else: + get.type = '4' + if ols_execstr: + ols_execstr = ols_execstr.format(get.type,mtype) + execstr = "cd /www/server/panel/install && /bin/bash install_soft.sh {} {} {} {} {}".format( + get.type, mtype, get.sName, get.version, ols_execstr) + if get.sName == "phpmyadmin": + execstr += "&> /tmp/panelExec.log" + if public.get_webserver() == 'openlitespeed': + execstr += " && sleep 1 && /usr/local/lsws/bin/lswsctrl restart" + # 清理日志文件 + if os.path.exists("/tmp/panelExec.log"): + public.writeFile("/tmp/panelExec.log","") + public.M('tasks').add('id,name,type,status,addtime,execstr',(None, mmsg + '['+get.sName+'-'+get.version+']','execshell','0',time.strftime('%Y-%m-%d %H:%M:%S'),execstr)) + cache.delete('install_task') + public.writeFile('/tmp/panelTask.pl','True') + public.write_log_gettext('Installer','Successfully added intallation task [{}-{}]',(get.sName,get.version)) + return public.return_msg_gettext(True,'Installation task added to queue') + + #卸载插件 + def uninstall_plugin(self,get): + pluginInfo = self.get_soft_find(get.sName) + if not pluginInfo: return public.return_msg_gettext(False,'The specified plugin does not exist!') + if pluginInfo['type'] != 5: + pluginPath = self.__install_path + '/' + pluginInfo['name'] + if pluginInfo['type'] != 6: + download_url = session['download_url'] + '/install/plugin/' + pluginInfo['name'] + '_en/install.sh' + toFile = '/tmp/%s.sh' % pluginInfo['name'] + public.downloadFile(download_url,toFile) + self.set_pyenv(toFile) + if os.path.exists(toFile): + if os.path.getsize(toFile) > 100: + public.ExecShell('/bin/bash ' + toFile + ' uninstall') + + if os.path.exists(pluginPath + '/install.sh'): + self.set_pyenv(pluginPath + '/install.sh') + public.ExecShell('/bin/bash ' + pluginPath + '/install.sh uninstall') + + if os.path.exists(pluginPath): public.ExecShell('rm -rf ' + pluginPath) + public.write_log_gettext('Installer','Successfully uninstalled software [{}]',(pluginInfo['title'],)) + return public.return_msg_gettext(True,'Uninstallaton succeeded') + else: + + if pluginInfo['name'] == 'mysql': + if public.M('databases').where('db_type=?',0).count() > 0: return public.return_msg_gettext(False,'The database list is not empty. For your data security, please backup and delete the existing database.
                                    Forced uninstall command: rm -rf /www/server/mysql') + if pluginInfo['name'] == 'nginx': + import nginx + nginx.nginx().del_all_log_format(get) + if pluginInfo['name'] == 'apache': + import apache + apache.apache().del_all_log_format(get) + get.type = '0' + if session['server_os']['x'] != 'RHEL': get.type = '3' + get.sName = get.sName.lower() + if get.sName.find('php-') != -1: + get.sName = get.sName.split('-')[0] + execstr = "cd /www/server/panel/install && /bin/bash install_soft.sh "+get.type+" uninstall " + get.sName.lower() + " "+ get.version.replace('.','') + public.ExecShell(execstr) + public.write_log_gettext('Installer','Successfully unintalled [{}-{}]',(get.sName,get.version)) + return public.return_msg_gettext(True,"Uninstallation succeeded") + + #从云端取列表 + def get_cloud_list(self, get=None): + force = 0 + if get and hasattr(get, 'force'): + force = int(get.force) + + if 'focre_cloud' in session: + if session['focre_cloud']: + force = 1 + session['focre_cloud'] = False + + if 'init_cloud' not in session: + force = 1 + session['init_cloud'] = True + + softList = public.load_soft_list(True if force == 1 else False) + + if get and 'init' in get: + if softList: + if 'success' not in softList: + return softList + + if force > 0: + public.ExecShell('rm -f /tmp/bmac_*') + public.run_thread(self.getCloudPHPExt) + # 专业版和企业版到期提醒,aaPanel目前没有先注释 + # self.expire_msg(softList) + + try: + p_token = cache.get('p_token') + + if p_token is None: + p_token = 'bmac_' + public.Md5(public.get_mac_address()) + cache.set('p_token', p_token) + + public.writeFile("/tmp/" + p_token, str(softList['pro'])) + public.writeFile('/tmp/{}.time'.format(p_token), str(int(time.time()))) + except: + pass + + sType = 0 + try: + if hasattr(get,'type'): sType = int(get['type']) + if hasattr(get,'query'): + if get.query: + # 关键词统计 参数keyword + import panelAuth + import requests + countUrl = '{}/api/panel/submit_keyword'.format(self.__official_url) + pdata = panelAuth.panelAuth().create_serverid(None) + url_headers = {} + if 'token' in pdata: + url_headers = {"authorization": "bt {}".format(pdata['token'])} + pdata['environment_info'] = json.dumps(public.fetch_env_info()) + keyword = { + "keyword": get.query + } + try: + requests.post(countUrl, params=keyword, headers=url_headers, verify=False, timeout=3) + except: + pass + sType = 0 + except:pass + + # 扫描本地插件并追加到软件列表中 + softList['list'] = self.get_local_plugin(softList['list']) + + # 软件列表分类处理 + softList['list'] = self.get_types(softList['list'], sType) + + if hasattr(get, 'query'): + if get.query: + get.query = get.query.lower() + tmpList = [] + for softInfo in softList['list']: + if softInfo['name'].lower().find(get.query) != -1 or \ + softInfo['title'].lower().find(get.query) != -1 or \ + softInfo['ps'].lower().find(get.query) != -1: + tmpList.append(softInfo) + softList['list'] = tmpList + + for softInfo in softList['list']: + if 'uninsatll_checks' not in softInfo: + softInfo['uninsatll_checks'] = softInfo['uninstall_checks'] + + return softList + + #取提醒标记 + def get_level_msg(self,level,s_time,endtime): + ''' + level 提醒标记 + s_time 当前时间戳 + endtime 到期时间戳 + ''' + expire_day = (endtime - s_time) / 86400 + if expire_day < 15 and expire_day > 7: + level = level + '15' + elif expire_day < 7 and expire_day > 3: + level = level + '7' + elif expire_day < 3 and expire_day > 0: + level = level + '3' + return level,expire_day + + #添加到期提醒 + def add_expire_msg(self,title,level,name,expire_day,pid,endtime): + ''' + title 软件标题 + level 提醒标记 + name 软件名称 + expire_day 剩余天数 + ''' + import panelMessage #引用消息提醒模块 + pm = panelMessage.panelMessage() + pm.remove_message_level(level) #删除旧的提醒 + if expire_day > 15: return False + if pm.is_level(level): #是否忽略 + if level != name: #到期还是即将到期 + msg_last = '您的【{}】授权还有{}天到期'.format(title,int(expire_day) + 1) + else: + msg_last = '您的【{}】授权已到期'.format(title) + pl_msg = 'true' + if name in ['pro','ltd']: + pl_msg = 'false' + renew_msg = '立即续费' % (title,pid,name,pl_msg,endtime) + pm.create_message(level=level,expire=7,msg="{},为了不影响您正常使用【{}】功能,请及时续费,{}".format(msg_last,title,renew_msg)) + return True + return False + + #到期提醒 + def expire_msg(self,data): + ''' + data 插件列表 + ''' + s_time = time.time() + is_plugin = True + import panelMessage #引用消息提醒模块 + pm = panelMessage.panelMessage() + #企业版到期提醒 + if not data['ltd'] in [-1] : + + if data['pro'] < 0 or (data['pro'] - s_time) / 86400 < 15 : + level,expire_day = self.get_level_msg('ltd',s_time,data['ltd']) + print(level,expire_day) + self.add_expire_msg('企业版',level,'ltd',expire_day,100000046,data['ltd']) + pm.remove_message_level('pro') + return True + + #专业版到期提醒 + if not data['pro'] in [-1,0]: + level,expire_day = self.get_level_msg('pro',s_time,data['pro']) + self.add_expire_msg('专业版',level,'pro',expire_day,100000030,data['pro']) + pm.remove_message_level('ltd') + is_plugin = False + + return True + + + #提交用户评分 + def set_score(self,args): + try: + import panelAuth + pdata = panelAuth.panelAuth().create_serverid(None) + pdata['ps'] = args.ps + pdata['num'] = int(args.num) + pdata['pid'] = int(args.pid) + if 1< pdata['num'] >5: return public.return_msg_gettext(False,'Scoring range [1-5]') + if not pdata['pid']: return public.return_msg_gettext(False,'The specified plugin does not exist!') + + result = public.httpPost(public.GetConfigValue('home') + '/api/panel/plugin_score',pdata,10) + result = json.loads(result) + return result + except: + return public.return_msg_gettext(False,'Connection failure!') + + #获取指定插件评分 + def get_score(self,args): + try: + import panelAuth + pdata = panelAuth.panelAuth().create_serverid(None) + pdata['pid'] = int(args.pid) + if not pdata['pid']: return [] + u_args = "" + sp_tip = '?' + if 'p' in args: + u_args += sp_tip + 'p=' + args.p + sp_tip = '&' + if 'tojs' in args: + u_args += sp_tip + 'tojs='+ args.tojs + sp_tip = '&' + if 'limit_num' in args: + pdata['limit_num'] = int(args.limit_num) + result = public.httpPost(public.GetConfigValue('home') + '/api/panel/get_plugin_socre' + u_args,pdata,10) + result = json.loads(result) + return result + except: + return public.return_msg_gettext(False,'Connection failure!') + + #清除多余面板日志 + def clean_panel_log(self): + try: + log_path = 'logs/request' + if not os.path.exists(log_path): return False + limit_num = 180 + p_logs = sorted(os.listdir(log_path)) + num = len(p_logs) - limit_num + if num > 0: + for i in range(num): + filename = log_path + '/' + p_logs[i] + if not os.path.exists(filename): continue + os.remove(filename) + + today = public.getDate(format='%Y-%m-%d') + for fname in os.listdir(log_path): + fsplit = fname.split('.') + if fsplit[-1] != 'json': + continue + if fsplit[0] == today: + continue + public.ExecShell("cd {} && gzip {}".format(log_path,fname)) + + #清理错误日志 + public.clean_max_log('/www/server/panel/logs/error.log',10,20) + public.clean_max_log('/www/server/panel/logs/socks5.log',10,20) + public.clean_max_log('/www/server/panel/logs/oos.log',10,20) + return True + except:return False + + #取本地插件 + def get_local_plugin(self,sList): + for name in os.listdir('plugin/'): + isExists = False + for softInfo in sList: + if name == softInfo['name']: + isExists = True + break + if isExists: continue + filename = 'plugin/' + name + '/info.json' + if not os.path.exists(filename): continue + tmpInfo = public.ReadFile(filename).strip() + if not tmpInfo: continue + try: + info = json.loads(tmpInfo) + except: continue + pluginInfo = self.get_local_plugin_info(info) + if not pluginInfo: continue + sList.append(pluginInfo) + return sList + + #检查是否正在安装 + def check_setup_task(self,sName): + if not self.__tasks: + self.__tasks = public.M('tasks').where("status!=?",('1',)).field('status,name').select() + if sName.find('php-') != -1: + tmp = sName.split('-') + sName = tmp[0] + version = tmp[1] + isTask = '1' + for task in self.__tasks: + tmpt = public.getStrBetween('[',']',task['name']) + if not tmpt:continue + tmp1 = tmpt.split('-') + name1 = tmp1[0].lower() + if sName == 'php': + if name1 != sName or tmp1[1] != version: continue + isTask = task['status'] + else: + if name1 == 'pure': name1 = 'pure-ftpd' + if name1 != sName: continue + isTask = task['status'] + + if isTask == '-1' or isTask == '0': + if task['name'].find('upgrade') != -1: isTask = '-2' + break + return isTask + + + #构造本地插件信息 + def get_local_plugin_info(self,info): + m_version = info['versions'].split(".") + if len(m_version) < 2: return None + if len(m_version) > 2: + tmp = m_version[:] + del(tmp[0]) + m_version[1] = '.'.join(tmp) + + try: + if not 'author' in info: info['author'] = '未知' + if not 'home' in info: info['home'] = '#' + pluginInfo = { + "id": 10000, + "pid": 0, + "type": 10, + "price": 0, + "author":info['author'], + "home":info['home'], + "name": info['name'], + "title": info['title'], + "panel_pro": 1, + "panel_free": 1, + "panel_test": 1, + "ps": info['ps'], + "version": info['versions'], + "s_version": "0", + "manager_version": "1", + "c_manager_version": "1", + "dependent": "", + "mutex": "", + "install_checks": "/www/server/panel/plugin/" + info['name'], + "uninsatll_checks": "/www/server/panel/plugin/" + info['name'], + "compile_args": 0, + "version_coexist": 0, + "versions": [ + { + "m_version": m_version[0], + "version": m_version[1], + "dependent": "", + "mem_limit": 32, + "cpu_limit": 1, + "os_limit": 0, + "setup": True + } + ], + "setup": True, + "status": True + } + except: pluginInfo = None + return pluginInfo + + #处理分类 + def get_types(self,sList,sType): + if sType <= 0: return sList + sType = [sType] + # if sType != 12: + # sType = [sType] + # else: + # sType = [sType,8] + newList = [] + for sInfo in sList: + if int(sInfo['type']) in sType: newList.append(sInfo) + return newList + + #检查权限 + def check_accept(self,get): + args = public.dict_obj() + args.type = '8' + p_list = self.get_cloud_list(args) + for p in p_list['list']: + if p['name'] == get.name: + if int(p_list['pro']) < 0 and int(p['endtime']) < 0: return False + break + + args.type = '10' + p_list = self.get_cloud_list(args) + for p in p_list['list']: + if p['name'] == get.name: + if not 'endtime' in p: continue + if int(p['endtime']) < 0: return False + break + + args.type = '12' + p_list = self.get_cloud_list(args) + for p in p_list['list']: + if not p['type'] in [12,'12']: continue + if p['name'] == get.name: + if not 'endtime' in p: continue + if int(p_list['ltd']) < 1 and int(p['endtime']) < 1: return False + break + return True + + #取软件列表 + def get_soft_list(self,get = None): + softList = self.get_cloud_list(get) + if not softList: + get.force = 1 + softList = self.get_cloud_list(get) + if not softList: return public.return_msg_gettext(False,'Failed to get software list ({})',"401") + softList['list'] = self.set_coexist(softList['list']) + if not 'type' in get: get.type = '0' + if get.type == '-1': + soft_list_tmp = [] + softList['list'] = self.check_isinstall(softList['list']) + for val in softList['list']: + if 'setup' in val: + if val['setup']: soft_list_tmp.append(val) + softList['list'] = soft_list_tmp + softList['list'] = self.get_page(softList['list'],get) + else: + softList['list'] = self.get_page(softList['list'],get) + softList['list']['data'] = self.check_isinstall(softList['list']['data']) + softList['apache22'] = False + softList['apache24'] = False + check_version_path = '/www/server/apache/version_check.pl' + if os.path.exists(check_version_path): + softList['apache24'] = True + if public.readFile(check_version_path).find('2.2') == 0: + softList['apache22'] = True + softList['apache24'] = False + if os.path.exists('/www/server/nginx/conf/nginx.conf'): + import one_key_wp + one_key_wp.fast_cgi().set_nginx_conf() + one_key_wp.fast_cgi().set_nginx_init() + public.ExecShell("/etc/init.d/nginx start") + return softList + + #取首页软件列表 + def get_index_list(self, get=None): + softList = self.get_cloud_list(get)['list'] + if not softList: + get.force = 1 + softList = self.get_cloud_list(get)['list'] + if not softList: return public.return_msg_gettext(False,'Failed to get software list ({})',"401") + softList = self.set_coexist(softList) + if not os.path.exists(self.__index): public.writeFile(self.__index,'[]') + try: + indexList = json.loads(public.ReadFile(self.__index)) + except Exception: + os.remove(self.__index) + public.writeFile(self.__index, '[]') + indexList = [] + dataList = [] + for index in indexList: + for softInfo in softList: + if softInfo['name'] == index: dataList.append(softInfo) + dataList = self.check_isinstall(dataList) + + title_has_version_reg = re.compile(r'-\d+(?:\.\d+)+$') + + # 过滤软件列表 + ret = [] + for item in dataList: + if not item.get('setup', False): + continue + + item['title'] = title_has_version_reg.sub('', item['title']) + + ret.append(item) + + return ret + + #添加到首页 + def add_index(self,get): + sName = get.sName + if not os.path.exists(self.__index): public.writeFile(self.__index,'[]') + indexList = json.loads(public.ReadFile(self.__index)) + if sName in indexList: return public.return_msg_gettext(False,'Please do NOT repeat adding') + if len(indexList) >= 12: + softList = self.get_cloud_list(get)['list'] + softList = self.set_coexist(softList) + for softInfo in softList: + # return softList + if softInfo['name'] == 'php': + for i in softInfo['versions']: + php_v = 'php-'+ i['m_version'] + if not os.path.exists('/www/server/php/{}'.format(i['m_version']))\ + and php_v in indexList: + indexList.remove(php_v) + if softInfo['name'] in indexList: + new_softInfo = self.check_status(softInfo) + if not new_softInfo['setup']: indexList.remove(softInfo['name']) + public.writeFile(self.__index,json.dumps(indexList)) + if len(indexList) >= 12: return public.return_msg_gettext(False,'Dashboard only display up to 12 software!') + indexList.append(sName) + public.writeFile(self.__index,json.dumps(indexList)) + return public.return_msg_gettext(True,'Setup successfully!') + + #删除首页 + def remove_index(self,get): + sName = get.sName + indexList = [] + if not os.path.exists(self.__index): public.writeFile(self.__index,'[]') + indexList = json.loads(public.ReadFile(self.__index)) + if not sName in indexList: return public.return_msg_gettext(True,'Successfully deleted!') + indexList.remove(sName) + public.writeFile(self.__index,json.dumps(indexList)) + return public.return_msg_gettext(True,'Successfully deleted!') + + #设置排序 + def sort_index(self,get): + indexList = get.ssort.split('|') + public.writeFile(self.__index,json.dumps(indexList)) + return public.return_msg_gettext(True,'Setup successfully!') + + #取快捷软件列表 + def get_link_list(self,get=None): + softList = self.get_cloud_list(get)['list'] + softList = self.set_coexist(softList) + indexList = json.loads(public.ReadFile(self.__link)) + dataList = [] + for index in indexList: + for softInfo in softList: + if softInfo['name'] == index: dataList.append(softInfo) + dataList = self.check_isinstall(dataList) + return dataList + + #添加到快捷栏 + def add_link(self,get): + sName = get.sName + indexList = json.loads(public.ReadFile(self.__link)) + if sName in indexList: return public.return_msg_gettext(False,'Please do NOT repeat adding') + if len(indexList) >= 5: return public.return_msg_gettext(False,'Shortcut Bar only display up to 5 software!') + indexList.append(sName) + public.writeFile(self.__link,json.dumps(indexList)) + return public.return_msg_gettext(True,'Setup successfully!') + + #删除快捷栏 + def remove_link(self,get): + sName = get.sName + indexList = [] + indexList = json.loads(public.ReadFile(self.__link)) + if sName in indexList: return public.return_msg_gettext(True,'Successfully deleted!') + indexList.remove(sName) + public.writeFile(self.__link,json.dumps(indexList)) + return public.return_msg_gettext(True,'Successfully deleted!') + + #设置快捷栏排序 + def sort_link(self,get): + indexList = get.ssort.split('|') + public.writeFile(self.__link,json.dumps(indexList)) + return public.return_msg_gettext(True,'Setup successfully!') + + + + #处理共存软件 + def set_coexist(self,sList): + softList = [] + for sInfo in sList: + try: + if sInfo['version_coexist'] == 1 and 'versions' in sInfo: + for versionA in sInfo['versions']: + try: + sTmp = sInfo.copy() + v = versionA['m_version'].replace('.','') + sTmp['title'] = sTmp['title']+'-'+versionA['m_version'] + sTmp['name'] = sTmp['name']+'-'+versionA['m_version'] + sTmp['version'] = sTmp['version'].replace('{VERSION}',v) + sTmp['manager_version'] = sTmp['manager_version'].replace('{VERSION}',v) + sTmp['install_checks'] = sTmp['install_checks'].replace('{VERSION}',v) + if 'uninsatll_checks' not in sTmp: + sTmp['uninsatll_checks'] = sTmp['uninstall_checks'].replace('{VERSION}',v) + else: + sTmp['uninsatll_checks'] = sTmp['uninsatll_checks'].replace('{VERSION}',v) + sTmp['s_version'] = sTmp['s_version'].replace('{VERSION}',v) + sTmp['versions'] = [] + sTmp['versions'].append(versionA) + softList.append(sTmp) + except: continue + else: + softList.append(sInfo) + except: continue + return softList + + #检测是否安装 + def check_isinstall(self,sList): + if not os.path.exists(self.__index): public.writeFile(self.__index,'[]') + indexList = json.loads(public.ReadFile(self.__index)) + for i in range(len(sList)): + sList[i]['index_display'] = sList[i]['name'] in indexList + sList[i] = self.check_status(sList[i]) + return sList + + + #检查软件状态 + def check_status(self,softInfo): + softInfo['setup'] = os.path.exists(softInfo['install_checks']) + softInfo['status'] = False + softInfo['task'] = self.check_setup_task(softInfo['name']) + if softInfo['name'].find('php-') != -1: softInfo['fpm'] = False + if softInfo['setup']: + softInfo['shell'] = softInfo['version'] + softInfo['version'] = self.get_version_info(softInfo) + softInfo['status'] = True + softInfo['versions'] = self.tips_version(softInfo['versions'],softInfo['version']) + softInfo['admin'] = os.path.exists('/www/server/panel/plugin/' + softInfo['name']) + + if 's_version' in softInfo and len(softInfo['s_version']) > 3: + pNames = softInfo['s_version'].split(',') + for pName in pNames: + if len(softInfo['manager_version']) > 5: + softInfo['status'] = self.process_exists(pName,softInfo['manager_version']) + else: + softInfo['status'] = self.process_exists(pName) + if softInfo['status']: break + else: + softInfo['version'] = "" + if softInfo['version_coexist'] == 1: + if softInfo['id'] != 10000: + self.get_icon(softInfo['name'].split('-')[0]) + else: + if 'min_image' in softInfo: + if softInfo['id'] != 10000: + self.get_icon(softInfo['name'],softInfo['min_image']) + else: + # if softInfo['id'] != 10000: + self.get_icon(softInfo['name']) + + if softInfo['name'].find('php-') != -1: + v2= softInfo['versions'][0]['m_version'].replace('.','') + softInfo['fpm'] = os.path.exists('/www/server/php/' + v2 + '/sbin/php-fpm') + softInfo['status'] = self.get_php_status(v2) + pid_file = '/www/server/php/' + v2 + '/var/run/php-fpm.pid' + if not softInfo['fpm']: + softInfo['status'] = True + elif softInfo['status'] and os.path.exists(pid_file): + try: + softInfo['status'] = public.pid_exists(int(public.readFile(pid_file))) + except: + if os.path.exists(pid_file): + os.remove(pid_file) + + if softInfo['name'] == 'mysql': + softInfo['status'] = self.process_exists('mysqld') + if not softInfo['status']: softInfo['status'] = self.process_exists('mariadbd') + if softInfo['name'] == 'phpmyadmin': softInfo['status'] = self.get_phpmyadmin_stat() + if softInfo['name'] == 'openlitespeed': + pid_file = '/run/openlitespeed.pid' + if os.path.exists(pid_file): + pid = int(public.readFile(pid_file)) + softInfo['status'] = public.pid_exists(pid) + return softInfo + + def get_php_status(self,phpversion): + ''' + @name 获取指定PHP版本的服务状态 + @author hwliang<2020-10-23> + @param phpversion string PHP版本 + @return bool + ''' + try: + php_status = os.path.exists('/tmp/php-cgi-'+phpversion+'.sock') + if php_status: return php_status + pid_file = '/www/server/php/{}/var/run/php-fpm.pid'.format(phpversion) + if not os.path.exists(pid_file): return False + pid = int(public.readFile(pid_file)) + return os.path.exists('/proc/{}/comm'.format(pid)) + except: + return False + + + + + #取phpmyadmin状态 + def get_phpmyadmin_stat(self): + webserver = public.get_webserver() + if webserver == 'nginx': + filename = public.GetConfigValue('setup_path') + '/nginx/conf/nginx.conf' + elif webserver == 'apache': + filename = public.GetConfigValue('setup_path') + '/apache/conf/extra/httpd-vhosts.conf' + else: + filename = "/www/server/panel/vhost/openlitespeed/detail/phpmyadmin.conf" + if not os.path.exists(filename): return False + conf = public.readFile(filename) + if not conf: return False + is_start = conf.find('/www/server/stop') == -1 + if is_start: + if webserver == 'nginx': + is_start = conf.find('allow 127.0.0.1;') == -1 + elif webserver == 'apache': + is_start = conf.find('Allow from 127.0.0.1 ::1 localhost') == -1 + return is_start + + + #获取指定软件信息 + def get_soft_find(self,get = None): + if not self.__plugin_s_list: + softList = self.get_cloud_list(get)['list'] + self.__plugin_s_list = self.set_coexist(softList) + + try: + sName = get['sName'] + except: + sName = get + + for softInfo in self.__plugin_s_list: + + if softInfo['name'] == sName: + if sName == 'phpmyadmin': + # 检查是否需要开启SSL + if os.path.exists('{}/data/phpmyadmin_ssl.mark'.format(public.get_panel_path())) and os.path.exists('{}/phpmyadmin'.format(public.get_setup_path())): + os.remove('{}/data/phpmyadmin_ssl.mark'.format(public.get_panel_path())) + from ajax_v2 import ajax + ajax().set_phpmyadmin_ssl(public.to_dict_obj({'v': '1'})) + + from BTPanel import get_phpmyadmin_dir + pmd = get_phpmyadmin_dir() + softInfo['ext'] = self.getPHPMyAdminStatus() + if softInfo['ext'] and pmd: + port = softInfo['ext']['ssl_port'] if softInfo['ext'].get('ssl_enabled', False) else pmd[1] + softInfo['ext']['url'] = 'http' + ('s' if softInfo['ext'].get('ssl_enabled', + False) else '') + '://' + public.GetHost() + ':' + port + '/' + \ + pmd[0] + if "php-" in sName: + v = softInfo["versions"][0]["m_version"] + v1 = v.replace(".", "") + if public.get_webserver() == "openlitespeed": + softInfo["php_ini"] = "/usr/local/lsws/lsphp{}/etc/php/{}/litespeed/php.ini".format(v1, v) + if os.path.exists("/etc/redhat-release"): + softInfo["php_ini"] = "/usr/local/lsws/lsphp{}/etc/php.ini".format(v1) + else: + softInfo["php_ini"] = "/www/server/php/{}/etc/php.ini".format(v1) + + return public.success_v2(self.check_status(softInfo)) + + return public.fail_v2('failed to get soft info') + + + #获取版本信息 + def get_version_info(self,sInfo): + version = '' + vFile1 = sInfo['uninsatll_checks'] + '/version_check.pl' + vFile2 = sInfo['uninsatll_checks'] + '/info.json' + if os.path.exists(vFile1): + version = public.xss_version(public.ReadFile(vFile1).strip()) + if not version: os.remove(vFile1) + elif os.path.exists(vFile2): + v_tmp = public.ReadFile(vFile2).strip() + if v_tmp: + try: + version = json.loads(v_tmp)['versions'] + except: public.ExecShell('rm -f ' + vFile2) + else: + version = "1.0" + else: + exec_args = { + 'nginx':"/www/server/nginx/sbin/nginx -v 2>&1|grep version|awk '{print $3}'|cut -f2 -d'/'", + 'apache':"/www/server/apache/bin/httpd -v|grep version|awk '{print $3}'|cut -f2 -d'/'", + 'mysql':"/www/server/mysql/bin/mysql -V|grep Ver|awk '{print $5}'|cut -f1 -d','", + 'php':"/www/server/php/{VERSION}/bin/php -v|grep cli|awk '{print $2}'", + 'pureftpd':"cat /www/server/pure-ftpd/version.pl", + 'phpmyadmin':"cat /www/server/phpmyadmin/version.pl", + 'tomcat':"/www/server/tomcat/bin/version.sh|grep version|awk '{print $4}'|cut -f2 -d'/'", + 'memcached':"/usr/local/memcached/bin/memcached -V|awk '{print $2}'", + 'redis':"/www/server/redis/src/redis-server -v|awk '{print $3}'|cut -f2 -d'='", + 'openlitespeed': "cat /usr/local/lsws/VERSION", + 'gitlab':'echo "8.8.5"' + } + + exec_str = '' + if sInfo['name'] in exec_args: exec_str = exec_args[sInfo['name']] + if sInfo['version_coexist'] == 1: + v_tmp = sInfo['name'].split('-') + exec_str = exec_args[v_tmp[0]].replace('{VERSION}',v_tmp[1].replace('.','')) + version = public.ExecShell(exec_str)[0].strip() + if version: + public.writeFile(vFile1,version) + else: + vFile4 = sInfo['uninsatll_checks'] + '/version.pl' + if os.path.exists(vFile4): + version = public.xss_version(public.readFile(vFile4).strip()) + + if sInfo['name'] == 'mysql': + vFile3 = sInfo['uninsatll_checks'] + '/version.pl' + version_str = None + if os.path.exists(vFile3): + version_str = public.xss_version(public.readFile(vFile3)) + if version_str.find('AliSQL') != -1: version = 'AliSQL' + if version == 'Linux' and version_str: + version = version_str + public.writeFile(vFile1,version) + + if sInfo['name'] == 'nginx': + if version.find('2.2.') != -1: version = '-Tengine' + version + return version.replace('p1','') + + + #标记当前安装的版本 + def tips_version(self,versions,version): + if len(versions) == 1: + versions[0]['setup'] = True + return versions + + for i in range(len(versions)): + if version == (versions[i]['m_version'] + '.' + versions[i]['version']): + versions[i]['setup'] = True + continue + vTmp = versions[i]['m_version'].split('_') + if len(vTmp) > 1: + vTmp = vTmp[1] + else: + vTmp = vTmp[0] + vLen = len(vTmp) + versions[i]['setup'] = (version[:vLen] == vTmp) + return versions + + #取pids + def get_pids(self): + pids = [] + for pid in os.listdir('/proc'): + if re.match(r"^\d+$",pid): pids.append(pid) + return pids + + + #进程是否存在 + def process_exists(self,pname,exe = None): + if pname in ['mysqld','mariadbd']: + datadir = public.get_datadir() + if datadir: + pid_file = "{}/{}.pid".format(datadir,public.get_hostname()) + if os.path.exists(pid_file): + try: + pid = int(public.readFile(pid_file)) + status = public.pid_exists(pid) + if status: return status + except: + return False + + if pname in ['php-fpm'] and exe: + pid_file = exe.replace('sbin/php-fpm','/var/run/php-fpm.pid') + if os.path.exists(pid_file): + try: + pid = int(public.readFile(pid_file)) + return public.pid_exists(pid) + except: + return False + + if not self.pids: self.pids = psutil.pids() + for pid in self.pids: + try: + l = '/proc/%s/exe' % pid + f = '/proc/%s/comm' % pid + p_exe = '' + p_name = '' + if os.path.exists(l): + p_exe = os.readlink(l) + if not p_name: p_name = p_exe.split('/')[-1] + + if not p_name and os.path.exists(f): + fp = open(f,'r') + p_name = fp.read().strip() + fp.close() + + if not p_name: continue + if p_name == pname: + if not exe: + return True + else: + if p_exe == exe: return True + except: continue + return False + + #取分页 + def get_page(self,data,get): + #包含分页类 + import page + #实例化分页类 + page = page.Page() + info = {} + info['count'] = len(data) + info['row'] = self.ROWS + info['p'] = 1 + if hasattr(get,'p'): + try: + info['p'] = int(get['p']) + except: + info['p'] = 1 + info['uri'] = {} + info['return_js'] = '' + if hasattr(get,'tojs'): + info['return_js'] = get.tojs + + #获取分页数据 + result = {} + result['page'] = page.GetPage(info) + n = 0 + result['data'] = [] + for i in range(info['count']): + if n >= page.ROW: break + if i < page.SHIFT: continue + n += 1 + result['data'].append(data[i]) + return result + + + #取列表 + def GetList(self,get = None): + try: + if not os.path.exists(self.__list): return [] + data = json.loads(public.readFile(self.__list)) + + #排序 + data = sorted(data, key= lambda b:b['sort'],reverse=False) + + #获取非划分列表 + n = 0 + for dirinfo in os.listdir(self.__install_path): + isTrue = True + for tm in data: + if tm['name'] == dirinfo: isTrue = False + if not isTrue: continue + + path = self.__install_path + '/' + dirinfo + if os.path.isdir(path): + jsonFile = path + '/info.json' + if os.path.exists(jsonFile): + try: + tmp = json.loads(public.readFile(jsonFile)) + if not hasattr(get,'type'): + get.type = 0 + else: + get.type = int(get.type) + + if get.type > 0: + try: + if get.type != tmp['id']: continue + except: + continue + + tmp['pid'] = len(data) + 1000 + n + tmp['status'] = tmp['display'] + tmp['display'] = 0 + data.append(tmp) + except: + pass + #索引列表 + if get: + display = None + if hasattr(get,'display'): display = True + if not hasattr(get,'type'): + get.type = 0 + else: + get.type = int(get.type) + if not hasattr(get,'search'): + search = None + m = 0 + else: + search = get.search.encode('utf-8').lower() + m = 1 + + tmp = [] + for d in data: + if d['id'] != 10000: + self.get_icon(d['name']) + if display: + if d['display'] == 0: continue + i=0 + if get.type > 0: + if get.type == d['id']: i+=1 + else: + i+=1 + if search: + if d['name'].lower().find(search) != -1: i+=1 + if d['name'].find(search) != -1: i+=1 + if d['title'].lower().find(search) != -1: i+=1 + if d['title'].find(search) != -1: i+=1 + if get.type > 0 and get.type != d['type']: i -= 1 + if i>m:tmp.append(d) + data = tmp + return data + except Exception as ex: + return str(ex) + + + #获取图标 + def get_icon(self,name,downFile = None): + iconFile = 'BTPanel/static/img/soft_ico/ico-' + name + '.png' + if not os.path.exists(iconFile): + public.run_thread(self.download_icon,(name,iconFile,downFile)) + else: + size = os.path.getsize(iconFile) + if size == 0: + public.run_thread(self.download_icon,(name,iconFile,downFile)) + # self.download_icon(name,iconFile,downFile) + + #下载图标 + def download_icon(self,name,iconFile,downFile): + srcIcon = 'plugin/' + name + '/icon.png' + skey = name+'_icon' + if cache.get(skey): return None + if os.path.exists(srcIcon): + public.ExecShell(r"\cp -a -r " + srcIcon + " " + iconFile) + else: + if downFile: + public.ExecShell('wget -O ' + iconFile + ' ' + public.GetConfigValue('home') + downFile + " &") + else: + public.ExecShell('wget -O ' + iconFile + ' ' + public.get_url() + '/install/plugin/' + name + '/icon.png' + " &") + cache.set(skey,1,86400) + + + #取分页 + def GetPage(self,data,get): + #包含分页类 + import page + #实例化分页类 + page = page.Page() + info = {} + info['count'] = len(data) + info['row'] = self.ROWS + info['p'] = 1 + if hasattr(get,'p'): + info['p'] = int(get['p']) + info['uri'] = {} + info['return_js'] = '' + if hasattr(get,'tojs'): + info['return_js'] = get.tojs + + #获取分页数据 + result = {} + result['page'] = page.GetPage(info) + n = 0 + result['data'] = [] + for i in range(info['count']): + if n > page.ROW: break + if i < page.SHIFT: continue + n += 1 + result['data'].append(data[i]) + return result + + #取分类 + def GetType(self,get = None): + try: + if not os.path.exists(self.__type): return False + data = json.loads(public.readFile(self.__type)) + return data + except: + return False + + #取单个 + def GetFind(self,name): + try: + data = self.GetList(None) + for d in data: + if d['name'] == name: return d + return None + except: + return None + + #设置 + def SetField(self,name,key,value): + data = self.GetList(None) + for i in range(len(data)): + if data[i]['name'] != name: continue + data[i][key] = value + + public.writeFile(self.__list,json.dumps(data)) + return True + + + + #安装插件 + def install(self,get): + pluginInfo = self.GetFind(get.name) + if not pluginInfo: + import json + pluginInfo = json.loads(public.readFile(self.__install_path + '/' + get.name + '/info.json')) + + if pluginInfo['tip'] == 'lib': + if not os.path.exists(self.__install_path + '/' + pluginInfo['name']): public.ExecShell('mkdir -p ' + self.__install_path + '/' + pluginInfo['name']) + if not 'download_url' in session: session['download_url'] = public.get_url() + download_url = session['download_url'] + '/install/plugin/' + pluginInfo['name'] + '/install.sh' + toFile = self.__install_path + '/' + pluginInfo['name'] + '/install.sh' + public.downloadFile(download_url,toFile) + self.set_pyenv(toFile) + public.ExecShell('/bin/bash ' + toFile + ' install') + if self.checksSetup(pluginInfo['name'],pluginInfo['checks'],pluginInfo['versions'])[0]['status'] or os.path.exists(self.__install_path + '/' + get.name): + public.write_log_gettext('Installer','Successfully installed plugin [{}]',(pluginInfo['title'],)) + #public.ExecShell('rm -f ' + toFile); + return public.return_msg_gettext(True,'Installation succeeded!') + return public.return_msg_gettext(False,'Installation failed!') + else: + import db,time + path = '/www/server/php' + if not os.path.exists(path): public.ExecShell("mkdir -p " + path) + issue = public.readFile('/etc/issue') + if session['server_os']['x'] != 'RHEL': get.type = '3' + + apacheVersion='false' + if public.get_webserver() == 'apache': + apacheVersion = public.xss_version(public.readFile('/www/server/apache/version.pl')) + public.writeFile('/var/bt_apacheVersion.pl',apacheVersion) + public.writeFile('/var/bt_setupPath.conf',public.GetConfigValue('root_path')) + isTask = '/tmp/panelTask.pl' + + mtype = 'install' + mmsg = 'install' + if hasattr(get, 'upgrade'): + if get.upgrade: + mtype = 'update' + mmsg = 'upgrade' + execstr = "cd /www/server/panel/install && /bin/bash install_soft.sh " + get.type + " "+mtype+" " + get.name + " "+ get.version; + sql = db.Sql() + if hasattr(get,'id'): + id = get.id + else: + id = None + sql.table('tasks').add('id,name,type,status,addtime,execstr',(None, mmsg + '['+get.name+'-'+get.version+']','execshell','0',time.strftime('%Y-%m-%d %H:%M:%S'),execstr)) + public.writeFile(isTask,'True') + public.write_log_gettext('Installer','Successfully added intallation task [{}-{}]',(get.name,get.version)) + return public.return_msg_gettext(True,'Installation task added to queue') + + + #卸载插件 + def unInstall(self,get): + pluginInfo = self.GetFind(get.name) + if not pluginInfo: + import json + pluginInfo = json.loads(public.readFile(self.__install_path + '/' + get.name + '/info.json')) + if pluginInfo['tip'] == 'lib': + if not os.path.exists(self.__install_path+ '/' + pluginInfo['name']): public.ExecShell('mkdir -p ' + self.__install_path + '/' + pluginInfo['name']) + download_url = session['download_url'] + '/install/plugin/' + pluginInfo['name'] + '/install.sh' + toFile = self.__install_path + '/' + pluginInfo['name'] + '/uninstall.sh' + install_sh = self.__install_path + '/' + pluginInfo['name'] + '/install.sh' + if not os.path.exists(toFile) and not os.path.exists(install_sh): + public.downloadFile(download_url,toFile) + self.set_pyenv(toFile) + + pluginPath = self.__install_path + '/' + pluginInfo['name'] + + if os.path.exists(toFile): + public.ExecShell('/bin/bash {} uninstall'.format(toFile)) + elif os.path.exists(pluginPath + '/install.sh'): + public.ExecShell('/bin/bash ' + pluginPath + '/install.sh uninstall') + + if os.path.exists(pluginPath): + public.ExecShell('rm -rf ' + pluginPath) + + public.write_log_gettext('Installer','Successfully uninstalled software [{}]',(pluginInfo['title'],)) + return public.return_msg_gettext(True,"Uninstallation succeeded") + else: + get.type = '0' + issue = public.readFile('/etc/issue') + if session['server_os']['x'] != 'RHEL': get.type = '3' + public.writeFile('/var/bt_setupPath.conf',public.GetConfigValue('root_path')) + execstr = "cd /www/server/panel/install && /bin/bash install_soft.sh "+get.type+" uninstall " + get.name.lower() + " "+ get.version.replace('.','') + public.ExecShell(execstr) + public.WriteLog('TYPE_SETUP','Successfully uninstalled [{}-{}]',(get.name,get.version)) + return public.returnMsg(True,"Uninstallation succeeded") + + #取产品信息 + def getProductInfo(self,productName): + if not self.__product_list: + import panelAuth + Auth = panelAuth.panelAuth() + self.__product_list = Auth.get_business_plugin(None) + for product in self.__product_list: + if product['name'] == productName: return product + return None + + #取到期时间 + def getEndDate(self,pluginName): + if not self.__plugin_list: + import panelAuth + Auth = panelAuth.panelAuth() + tmp = Auth.get_plugin_list(None) + if not tmp: return public.get_msg_gettext('NOT opened') + if not 'data' in tmp: return public.get_msg_gettext('NOT opened') + self.__plugin_list = tmp['data'] + for pluinfo in self.__plugin_list: + if pluinfo['product'] == pluginName: + if not pluinfo['endtime'] or not pluinfo['state']: return public.get_msg_gettext('To be paid') + if pluinfo['endtime'] < time.time(): return public.get_msg_gettext('Expired') + return time.strftime("%Y-%m-%d",time.localtime(pluinfo['endtime'])); + return public.get_msg_gettext('NOT opened') + + #取插件列表 + def getPluginList(self,get): + import json + arr = self.GetList(get) + result = {} + if not arr: + result['data'] = arr + result['type'] = self.GetType(None) + return result + apacheVersion = "" + try: + apavFile = '/www/server/apache/version.pl' + if os.path.exists(apavFile): + apacheVersion = public.xss_version(public.readFile(apavFile).strip()) + except: + pass + + result = self.GetPage(arr,get) + arr = result['data'] + for i in range(len(arr)): + arr[i]['end'] = '--' + #if 'price' in arr[i]: + # if arr[i]['price'] > 0: + # arr[i]['end'] = self.getEndDate(arr[i]['title']); + # if os.path.exists('plugin/beta/config.conf'): + # if os.path.exists('plugin/' + arr[i]['name'] + '/' + arr[i]['name'] + '_main.py') and arr[i]['end'] == '未开通': arr[i]['end'] = '--'; + + + if arr[i]['name'] == 'php': + if apacheVersion == '2.2': + arr[i]['versions'] = '5.2,5.3,5.4' + arr[i]['update'] = self.GetPv(arr[i]['versions'], arr[i]['update']) + elif apacheVersion == '2.4': + arr[i]['versions'] = '5.3,5.4,5.5,5.6,7.0,7.1,7.2,7.3,7.4' + arr[i]['update'] = self.GetPv(arr[i]['versions'], arr[i]['update']) + arr[i]['apache'] = apacheVersion + + arr[i]['versions'] = self.checksSetup(arr[i]['name'].replace('_soft',''),arr[i]['checks'],arr[i]['versions']) + + try: + arr[i]['update'] = arr[i]['update'].split(',') + except: + arr[i]['update'] = [] + + #是否强制使用插件模板 LIB_TEMPLATE + if os.path.exists(self.__install_path+'/'+arr[i]['name']): arr[i]['tip'] = 'lib' + + if arr[i]['tip'] == 'lib': + arr[i]['path'] = self.__install_path + '/' + arr[i]['name'].replace('_soft','') + arr[i]['config'] = os.path.exists(arr[i]['path'] + '/index.html') + else: + arr[i]['path'] = '/www/server/' + arr[i]['name'].replace('_soft','') + arr.append(public.M('tasks').where("status!=?",('1',)).count()) + + + result['data'] = arr + result['type'] = self.GetType(None) + return result + + #GetPHPV + def GetPv(self,versions,update): + versions = versions.split(',') + update = update.split(',') + updates = [] + for up in update: + if up[:3] in versions: updates.append(up) + return ','.join(updates) + + #保存插件排序 + def savePluginSort(self,get): + ssort = get.ssort.split('|') + data = self.GetList(None) + l = len(data) + for i in range(len(ssort)): + if int(ssort[i]) > 1000: continue + for n in range(l): + if data[n]['pid'] == int(ssort[i]): data[n]['sort'] = i + public.writeFile(self.__list,json.dumps(data)) + return public.return_msg_gettext(True,'Sort saved') + + #检查是否安装 + def checksSetup(self,name,checks,vers = ''): + tmp = checks.split(',') + versions = [] + path = '/www/server/' + name + '/version.pl' + v1 = '' + if os.path.exists(path): v1 = public.xss_version(public.readFile(path).strip()) + if name == 'nginx': v1 = v1.replace('1.10', '1.12') + if not self.__tasks: + self.__tasks = public.M('tasks').where("status!=?",('1',)).field('status,name').select() + isStatus = 0 + versArr = vers.split(',') + for v in versArr: + version = {} + + v2 = v + if name == 'php': v2 = v2.replace('.','') + status = False + for tm in tmp: + if name == 'php': + path = '/www/server/php/' + v2 + if os.path.exists(path + '/bin/php') and not os.path.exists(path + '/version.pl'): + public.ExecShell("echo `"+path+"/bin/php 2>/dev/null -v|grep cli|awk '{print $2}'` > " + path + '/version.pl') + try: + v1 = public.xss_version(public.readFile(path+'/version.pl').strip()) + if not v1: public.ExecShell('rm -f ' + path + '/version.pl') + except: + v1 = "" + if os.path.exists(tm.replace('VERSION',v2)): status = True + else: + if os.path.exists(tm) and isStatus == 0: + if len(versArr) > 1: + im = v1.find(v) + if im != -1 and im < 3: + status = True + isStatus += 1 + else: + status = True + isStatus += 1 + #处理任务标记 + if not self.__tasks: + self.__tasks = public.M('tasks').where("status!=?",('1',)).field('status,name').select() + isTask = '1' + for task in self.__tasks: + tmpt = public.getStrBetween('[',']',task['name']) + if not tmpt:continue + tmp1 = tmpt.split('-') + name1 = tmp1[0].lower() + if name == 'php': + if name1 == name and tmp1[1] == v: isTask = task['status'] + else: + if name1 == 'pure': name1 = 'pure-ftpd' + if name1 == name: isTask = task['status'] + + infoFile = 'plugin/' + name + '/info.json' + if os.path.exists(infoFile): + try: + tmps = json.loads(public.readFile(infoFile)) + if tmps: v1 = tmps['versions'] + except:pass + + if name == 'memcached': + if os.path.exists('/etc/init.d/memcached'): + v1 = session.get('memcachedv') + if not v1: + v1 = public.ExecShell("memcached -V|awk '{print $2}'")[0].strip() + session['memcachedv'] = v1 + if name == 'apache': + if os.path.exists('/www/server/apache/bin/httpd'): + v1 = session.get('httpdv') + if not v1: + v1 = public.ExecShell(r"/www/server/apache/bin/httpd -v|grep Apache|awk '{print $3}'|sed 's/Apache\///'")[0].strip(); + session['httpdv'] = v1 + #if name == 'mysql': + # if os.path.exists('/www/server/mysql/bin/mysql'): v1 = public.ExecShell("mysql -V|awk '{print $5}'|sed 's/,//'")[0].strip(); + + version['status'] = status + version['version'] = v + version['task'] = isTask + version['no'] = v1 + versions.append(version) + return self.checkRun(name,versions) + + #检查是否启动 + def checkRun(self,name,versions): + if name == 'php': + path = '/www/server/php' + pids = psutil.pids() + for i in range(len(versions)): + if versions[i]['status']: + v4 = versions[i]['version'].replace('.','') + versions[i]['run'] = os.path.exists('/tmp/php-cgi-' + v4 + '.sock') + pid_file = path + '/' + v4 + '/var/run/php-fpm.pid' + versions[i]['process_id'] = public.readFile(pid_file) + if versions[i]['run'] and os.path.exists(pid_file): + if not int(public.readFile(pid_file)) in pids: + versions[i]['run'] = False + + versions[i]['fpm'] = os.path.exists('/etc/init.d/php-fpm-'+v4) + phpConfig = self.GetPHPConfig(v4) + versions[i]['max'] = phpConfig['max'] + versions[i]['maxTime'] = phpConfig['maxTime'] + versions[i]['pathinfo'] = phpConfig['pathinfo'] + versions[i]['display'] = os.path.exists(path + '/' + v4 + '/display.pl') + if len(versions) < 5: versions[i]['run'] = True + + elif name == 'nginx': + status = False + if os.path.exists('/etc/init.d/nginx'): + pidf = '/www/server/nginx/logs/nginx.pid' + if os.path.exists(pidf): + try: + pid = public.readFile(pidf) + pname = self.checkProcess(pid) + if pname: status = True + except: + status = False + for i in range(len(versions)): + versions[i]['run'] = False + if versions[i]['status']: versions[i]['run'] = status + elif name == 'apache': + status = False + if os.path.exists('/etc/init.d/httpd'): + pidf = '/www/server/apache/logs/httpd.pid' + if os.path.exists(pidf): + pid = public.readFile(pidf) + status = self.checkProcess(pid) + for i in range(len(versions)): + versions[i]['run'] = False + if versions[i]['status']: versions[i]['run'] = status + elif name == 'mysql': + status = os.path.exists('/tmp/mysql.sock') + for i in range(len(versions)): + versions[i]['run'] = False + if versions[i]['status']: versions[i]['run'] = status + elif name == 'tomcat': + status = False + if os.path.exists('/www/server/tomcat/logs/catalina-daemon.pid'): + if self.getPid('jsvc'): status = True + if not status: + if self.getPid('java'): status = True + for i in range(len(versions)): + versions[i]['run'] = False + if versions[i]['status']: versions[i]['run'] = status + elif name == 'pure-ftpd': + for i in range(len(versions)): + pidf = '/var/run/pure-ftpd.pid' + if os.path.exists(pidf): + pid = public.readFile(pidf) + versions[i]['run'] = self.checkProcess(pid) + if not versions[i]['run']: public.ExecShell('rm -f ' + pidf) + elif name == 'phpmyadmin': + for i in range(len(versions)): + if versions[i]['status']: versions[i] = self.getPHPMyAdminStatus() + elif name == 'redis': + for i in range(len(versions)): + pidf = '/var/run/redis_6379.pid' + if os.path.exists(pidf): + pid = public.readFile(pidf) + versions[i]['run'] = self.checkProcess(pid) + if not versions[i]['run']: public.ExecShell('rm -f ' + pidf) + elif name == 'memcached': + for i in range(len(versions)): + pidf = '/var/run/memcached.pid' + if os.path.exists(pidf): + pid = public.readFile(pidf) + versions[i]['run'] = self.checkProcess(pid) + if not versions[i]['run']: public.ExecShell('rm -f ' + pidf) + else: + for i in range(len(versions)): + if versions[i]['status']: versions[i]['run'] = True + return versions + + # 取PHPMyAdmin状态 + def getPHPMyAdminStatus(self): + import re + tmp = {} + setupPath = '/www/server' + configFile = setupPath + '/nginx/conf/nginx.conf' + pauth = False + pstatus = False + phpversion = "54" + phpport = '888' + ssl_port = '887' + ssl_enabled = False + if os.path.exists(configFile): + conf = public.readFile(configFile) + rep = r"listen\s+([0-9]+)\s*;" + rtmp = re.search(rep, conf) + if rtmp: + phpport = rtmp.groups()[0] + + # SSL配置文件查看 + ssl_config_file = '{}/vhost/nginx/phpmyadmin.conf'.format(public.get_panel_path()) + if os.path.exists(ssl_config_file) and os.path.getsize(ssl_config_file) > 10: + tmps = public.readFile(ssl_config_file) + m = re.search(r"listen\s*(\d+)", tmps) + if m is not None: + ssl_enabled = True + ssl_port = m.group(1) + + if conf.find('AUTH_START') != -1: pauth = True + if conf.find(setupPath + '/stop') == -1: pstatus = True + configFile = setupPath + '/nginx/conf/enable-php.conf' + if not os.path.exists(configFile): public.writeFile(configFile, public.readFile( + setupPath + '/nginx/conf/enable-php-54.conf')) + conf = public.readFile(configFile) + rep = r"php-cgi-([0-9]+)\.sock" + rtmp = re.search(rep, conf) + if rtmp: + phpversion = rtmp.groups()[0] + else: + rep = r'127.0.0.1:10(\d{2,2})1' + rtmp = re.findall(rep, conf) + if rtmp: + phpversion = rtmp[0] + else: + rep = r"php-cgi.*\.sock" + public.writeFile(configFile, conf) + phpversion = '54' + + configFile = setupPath + '/apache/conf/extra/httpd-vhosts.conf' + if os.path.exists(configFile): + conf = public.readFile(configFile) + rep = r"php-cgi-([0-9]+)\.sock" + rtmp = re.search(rep, conf) + if rtmp: + phpversion = rtmp.groups()[0] + rep = r"Listen\s+([0-9]+)\s*\n" + rtmp = re.search(rep, conf) + if rtmp: + phpport = rtmp.groups()[0] + + # SSL配置文件查看 + ssl_config_file = '{}/vhost/apache/phpmyadmin.conf'.format(public.get_panel_path()) + if os.path.exists(ssl_config_file) and os.path.getsize(ssl_config_file) > 10: + tmps = public.readFile(ssl_config_file) + m = re.search(r"Listen\s*(\d+)", tmps) + if m is not None: + ssl_enabled = True + ssl_port = m.group(1) + + if conf.find('AUTH_START') != -1: pauth = True + if conf.find('/www/server/stop') == -1: pstatus = True + + if os.path.exists('/usr/local/lsws/bin/lswsctrl'): + result = self._get_ols_myphpadmin_info() + if result: + phpversion = result['php_version'] + phpport = result['php_port'] + pauth = result['pauth'] + pstatus = result['pstatus'] + try: + vfile = setupPath + '/phpmyadmin/version.pl' + if os.path.exists(vfile): + tmp['version'] = public.xss_version(public.readFile(vfile).strip()) + tmp['status'] = True + tmp['no'] = tmp['version'] + else: + tmp['version'] = "" + tmp['status'] = False + tmp['no'] = "" + + tmp['run'] = pstatus + tmp['phpversion'] = phpversion + tmp['ssl_enabled'] = ssl_enabled + tmp['port'] = phpport + tmp['ssl_port'] = ssl_port + tmp['auth'] = pauth + except Exception as ex: + tmp['status'] = False + tmp['error'] = str(ex) + return tmp + + def _get_ols_myphpadmin_info(self): + filename = "/www/server/panel/vhost/openlitespeed/detail/phpmyadmin.conf" + conf = public.readFile(filename) + if not conf:return False + reg = r'/usr/local/lsws/lsphp(\d+)/bin/lsphp' + php_v = re.search(reg,conf) + phpversion = '73' + phpport = '888' + if php_v: + phpversion = php_v.group(1) + filename = '/www/server/panel/vhost/openlitespeed/listen/888.conf' + conf = public.readFile(filename) + reg = r'address\s+\*\:(\d+)' + php_port = re.search(reg,conf) + if php_port: + phpport = php_port.group(1) + pauth = False + pstatus = False + if conf.find('/www/server/stop') == -1: pstatus = True + return {'php_version':phpversion,'php_port':phpport,'pauth':pauth,'pstatus':pstatus} + + #取PHP配置 + def GetPHPConfig(self,version): + import re + setupPath = '/www/server' + file = setupPath + "/php/"+version+"/etc/php.ini" + phpini = public.readFile(file) + file = setupPath + "/php/"+version+"/etc/php-fpm.conf" + phpfpm = public.readFile(file) + data = {} + try: + rep = r"upload_max_filesize\s*=\s*([0-9]+)M" + tmp = re.search(rep,phpini).groups() + data['max'] = tmp[0] + except: + data['max'] = '50' + try: + rep = r"request_terminate_timeout\s*=\s*([0-9]+)\n" + tmp = re.search(rep,phpfpm).groups() + data['maxTime'] = tmp[0] + except: + data['maxTime'] = 0 + + try: + rep = r"\n;*\s*cgi\.fix_pathinfo\s*=\s*([0-9]+)\s*\n" + tmp = re.search(rep,phpini).groups() + + if tmp[0] == '1': + data['pathinfo'] = True + else: + data['pathinfo'] = False + except: + data['pathinfo'] = False + + return data + + #名取PID + def getPid(self,pname): + try: + if not self.pids: self.pids = psutil.pids() + for pid in self.pids: + if psutil.Process(pid).name() == pname: return True + return False + except: return True + + #检测指定进程是否存活 + def checkProcess(self,pid): + try: + if not self.pids: self.pids = psutil.pids() + if int(pid) in self.pids: return True + return False + except: return False + + #获取配置模板 + def getConfigHtml(self,get): + filename = self.__install_path + '/' + get.name + '/index.html' + if not os.path.exists(filename): return public.return_msg_gettext(False,'This plugin does NOT have template!') + mimetype = 'text/html' + cache_time = 0 if public.is_debug() else 86400 + self.plugin_open_total(get.name) + import flask + if flask.__version__ < "2.1.0": + return send_file(filename, + mimetype = mimetype, + as_attachment = True, + add_etags = True, + conditional = True, + cache_timeout = cache_time) + else: + return send_file(filename, + mimetype = mimetype, + as_attachment = True, + etag = True, + conditional = True, + max_age = cache_time) + + + def creatab_open_total_table(self,sql): + ''' + @name 创建插件打开统计表 + @author hwliang<2021-06-26> + @param sql 数据库对像 + @return void + ''' + if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'open_total')).count(): + csql = '''CREATE TABLE IF NOT EXISTS `open_total` ( +`id` INTEGER PRIMARY KEY AUTOINCREMENT, +`plugin_name` REAL, +`num` INTEGER +)''' + sql.execute(csql,()) + + + def plugin_open_total(self,plugin_name): + ''' + @name 插件打开统计 + @author hwliang<2021-06-26> + @param plugin_name 插件名称 + @return void + ''' + import db + sql = db.Sql().dbfile('plugin_total') + self.creatab_open_total_table(sql) + pdata = { + "plugin_name":plugin_name, + "num":1 + } + + num = sql.table('open_total').where('plugin_name=?',plugin_name).getField('num') + if not num: + sql.table('open_total').insert(pdata) + else: + sql.table('open_total').where('plugin_name=?',plugin_name).setField('num',num+1) + + def get_usually_plugin(self,get): + ''' + @name 获取常用插件 + @author hwliang<2021-06-26> + @param get + @return list + ''' + import db + sql = db.Sql().dbfile('plugin_total') + self.creatab_open_total_table(sql) + plugin_list = sql.table('open_total').order('num desc').limit(10).select() + usually_list = [] + for p in plugin_list: + plugin_info = self.get_soft_find(p['plugin_name']) + if plugin_info: + if plugin_info['setup']: + usually_list.append(plugin_info) + if len(usually_list) >= 5: break + return usually_list + + + def get_plugin_upgrades(self,get): + ''' + @name 获取指定插件的近期更新历史 + @author hwliang<2021-06-30> + @param get{ + plugin_name: string 插件名称 + } + @return list + ''' + plugin_name = get.plugin_name + if getattr(get,'show',0): + plugin_info = self.__get_plugin_find(plugin_name) + if plugin_info and 'versions' in plugin_info: + return plugin_info['versions'] + return [] + else: + return self.__get_plugin_upgrades(plugin_name) + + #取插件信息 + def getPluginInfo(self,get): + try: + pluginInfo = self.GetFind(get.name) + apacheVersion = "" + try: + apavFile = '/www/server/apache/version.pl' + if os.path.exists(apavFile): + apacheVersion = public.xss_version(public.readFile(apavFile).strip()) + except: + pass + if pluginInfo['name'] == 'php': + if apacheVersion == '2.2': + pluginInfo['versions'] = '5.2,5.3,5.4' + elif apacheVersion == '2.4': + pluginInfo['versions'] = '5.3,5.4,5.5,5.6,7.0,7.1,7.2,7.3,7.4' + + pluginInfo['versions'] = self.checksSetup(pluginInfo['name'],pluginInfo['checks'],pluginInfo['versions']) + if get.name == 'php': + pluginInfo['phpSort'] = public.readFile('/www/server/php/sort.pl') + return pluginInfo + except: + return False + + #取插件状态 + def getPluginStatus(self,get): + find = self.GetFind(get.name) + versions = [] + path = '/www/server/php' + for version in find['versions'].split(','): + tmp = {} + tmp['version'] = version + if get.name == 'php': + tmp['status'] = os.path.exists(path + '/' + version.replace(',','') + '/display.pl') + else: + tmp['status'] = find['status'] + versions.append(tmp) + return versions + + #设置插件状态 + def setPluginStatus(self,get): + if get.name == 'php': + isRemove = True + path = '/www/server/php' + if get.status == '0': + versions = self.GetFind(get.name)['versions'] + public.ExecShell('rm -f ' + path + '/' + get.version.replace('.','') + '/display.pl') + for version in versions.split(','): + if os.path.exists(path + '/' + version.replace('.','') + '/display.pl'): + isRemove = False + break + else: + public.writeFile(path + '/' + get.version.replace('.','') + '/display.pl','True') + + if isRemove: + self.SetField(get.name, 'display', int(get.status)) + else: + self.SetField(get.name, 'display', int(get.status)) + return public.return_msg_gettext(True,'Setup successfully!') + + #从云端获取插件列表 + def getCloudPlugin(self,get): + if session.get('getCloudPlugin') and get != None: return public.return_msg_gettext(True,'Your plugin list is already the latest version {}!',("-1",)) + import json + if not session.get('download_url'): session['download_url'] = 'http://node.aapanel.com' + + #获取列表 + try: + newUrl = public.get_url() + if os.path.exists('plugin/beta/config.conf'): + download_url = newUrl + '/install/list.json' + else: + download_url = newUrl + '/install/list_pro.json' + data = json.loads(public.httpGet(download_url)) + session['download_url'] = newUrl + except: + download_url = session['download_url'] + '/install/list_pro.json' + data = json.loads(public.httpGet(download_url)) + + n = i = j = 0 + + lists = self.GetList(None) + + for i in range(len(data)): + for pinfo in lists: + if data[i]['name'] != pinfo['name']: continue + data[i]['display'] = pinfo['display'] + if data[i]['default']: + get.name = data[i]['name'] + self.install(get) + + public.writeFile(self.__list,json.dumps(data)) + + #获取分类 + try: + download_url = session['download_url'] + '/install/type.json' + types = json.loads(public.httpGet(download_url)) + public.writeFile(self.__type,json.dumps(types)) + except: + pass + + self.getCloudPHPExt(get) + self.GetCloudWarning(get) + session['getCloudPlugin'] = True + return public.return_msg_gettext(True,'Software list updated!') + + #刷新缓存 + def flush_cache(self,get): + self.getCloudPlugin(None) + return public.return_msg_gettext(True,'Software list updated!') + + #获取PHP扩展 + def getCloudPHPExt(self,get=None): + import json + try: + key = 'php_ext_cache' + if cache.get(key): return 1 + surl = public.get_url() + download_url = surl + '/install/lib/phplib.json' + tstr = public.httpGet(download_url) + data = json.loads(tstr) + if not data: return 2 + public.writeFile('data/phplib.conf',json.dumps(data)) + + # download_url = surl + '/license/md5.txt' + # li_md5 = public.httpGet(download_url) + # if not li_md5: return 3 + # li_md5 = li_md5.strip() + # if len(li_md5) != 32: return 4 + # l_file = 'BTPanel/templates/default/license.html' + # lfile2 = 'data/license.md5' + # old_md5 = '' + # if os.path.exists(lfile2): + # old_md5 = public.readFile(lfile2) + + # if li_md5 != old_md5: + # download_url = surl + '/license/license.html' + # public.downloadFile(download_url,l_file) + # old_md5 = public.FileMd5(l_file) + # if li_md5 == old_md5: + # public.writeFile(lfile2,old_md5) + # s_file = 'data/licenes.pl' + # if os.path.exists(s_file): + # os.remove(s_file) + cache.set(key,86400) + return True + except: + return public.get_error_info() + + + + #获取警告列表 + def GetCloudWarning(self,get): + import json + if not session.get('download_url'): session['download_url'] = public.get_url() + download_url = session['download_url'] + '/install/warning.json' + tstr = public.httpGet(download_url) + data = json.loads(tstr) + if not data: return False + wfile = 'data/warning.json' + wlist = json.loads(public.readFile(wfile)) + for i in range(len(data['data'])): + for w in wlist['data']: + if data['data'][i]['name'] != w['name']: continue + data['data'][i]['ignore_count'] = w['ignore_count'] + data['data'][i]['ignore_time'] = w['ignore_time'] + public.writeFile(wfile,json.dumps(data)) + return data + + #名取标题 + def get_title_byname(self,get): + get.sName = get.name + find = self.get_soft_find(get) + return find['title'] + + + def a(self, get): + try: + return public.run_plugin_v2(get.name, get.s, get) + except: + return public.get_error_object(None, plugin_name=get.name) + + #上传插件包 + def update_zip(self,get = None,tmp_file = None, update = False): + tmp_path = '/www/server/panel/temp' + if not os.path.exists(tmp_path): + os.makedirs(tmp_path,mode=384) + + if tmp_file: + if not os.path.exists(tmp_file): return public.return_msg_gettext(False,'File download failed!') + + + if get: + public.ExecShell("rm -rf " + tmp_path + '/*') + tmp_file = tmp_path + '/plugin_tmp.zip' + from werkzeug.utils import secure_filename + from flask import request + f = request.files['plugin_zip'] + if f.filename[-4:] != '.zip': tmp_file = tmp_path + '/plugin_tmp.tar.gz' + + f.save(tmp_file) + + + import panelTask + panelTask.bt_task()._unzip(tmp_file,tmp_path,'','/dev/null') + os.remove(tmp_file) + p_info = tmp_path + '/info.json' + if not os.path.exists(p_info): + d_path = None + for df in os.walk(tmp_path): + if len(df[2]) < 3: continue + if not 'info.json' in df[2]: continue + if not 'install.sh' in df[2]: continue + if not os.path.exists(df[0] + '/info.json'): continue + d_path = df[0] + if d_path: + tmp_path = d_path + p_info = tmp_path + '/info.json' + try: + try: + data = json.loads(public.ReadFile(p_info)) + except: + data = json.loads(public.ReadFile(p_info).decode('utf-8-sig')) + data['size'] = public.get_path_size(tmp_path) + if not 'author' in data: data['author'] = public.get_msg_gettext('Unknown') + if not 'home' in data: data['home'] = 'https://www.bt.cn/bbs/forum-40-1.html' + + plugin_path = '/www/server/panel/plugin/' + data['name'] + '/info.json' + data['old_version'] = '0' + data['tmp_path'] = tmp_path + if os.path.exists(plugin_path): + try: + old_info = json.loads(public.ReadFile(plugin_path)) + data['old_version'] = old_info['versions'] + except:pass + except: + public.ExecShell("rm -rf " + tmp_path + '/*') + return public.return_msg_gettext(False,'No plugin info found in the archive, please check the plugin package!') + + data['update'] = update + return data + + #导入插件包 + def input_zip(self,get): + if not os.path.exists(get.tmp_path): return public.return_msg_gettext(False,'TEM_FILE_NOT_EXIST!') + plugin_path = '/www/server/panel/plugin/' + get.plugin_name + if not os.path.exists(plugin_path): os.makedirs(plugin_path) + public.ExecShell(r"\cp -a -r " + get.tmp_path + '/* ' + plugin_path + '/') + public.ExecShell('chmod -R 600 ' + plugin_path) + self.set_pyenv(plugin_path + '/install.sh') + public.ExecShell('cd ' + plugin_path + ' && bash install.sh install &> /tmp/panelShell.pl') + p_info = public.ReadFile(plugin_path + '/info.json') + public.ExecShell("rm -rf /www/server/panel/temp/*") + if p_info: + #----- 增加图标复制 hwliang<2021-03-23> -----# + icon_sfile = plugin_path + '/icon.png' + icon_dfile = '/www/server/panel/BTPanel/static/img/soft_ico/ico-{}.png'.format(get.plugin_name) + if os.path.exists(plugin_path + '/icon.png'): + import shutil + shutil.copyfile(icon_sfile,icon_dfile) + #----- 增加图标复制 END -----# + public.write_log_gettext('Software manager','Installed third-party plugin [{}]' ,(json.loads(p_info)['title'],)) + return public.return_msg_gettext(True,'Installation succeeded!') + public.ExecShell("rm -rf " + plugin_path) + return public.return_msg_gettext(False,'Installation failed') + + + #导出插件包 + def export_zip(self,get): + plugin_path = '/www/server/panel/plugin/' + get.plugin_name + if not os.path.exists(plugin_path): return public.return_msg_gettext(False,'The specified plugin does not exist!') + + get.sfile = plugin_path + '/' + get.dfile = '/www/server/panel/temp/bt_plugin_' + get.plugin_name + '.zip' + get.type = 'zip' + import files + files.files().Zip(get) + if not os.path.exists(get.dfile): return public.return_msg_gettext(False,'Export failed, please check permissions!') + return public.return_msg_gettext(True,get.dfile) + + + #获取编译参数 + def get_make_args(self,get): + config_path = 'install/' + get.name + if not os.path.exists(config_path): + os.makedirs(config_path) + #读支持的编译参数列表 + make_args = [] + for p_name in os.listdir(config_path): + path = os.path.join(config_path,p_name) + if not os.path.isdir(path): continue + make_info = {"name":p_name,"init":"","args":"","ps":""} + init_file = os.path.join(path,'init.sh') + args_file = os.path.join(path,'args.pl') + ps_file = os.path.join(path,'ps.pl') + if not os.path.exists(args_file): + continue + if os.path.exists(init_file): + make_info['init'] = public.readFile(init_file) + if os.path.exists(ps_file): + make_info['ps'] = public.readFile(ps_file) + make_info['args'] = public.readFile(args_file) + make_args.append(make_info) + #读当前配置 + data = {'args':make_args,'config':''} + config_file = config_path + '/config.pl' + if os.path.exists(config_file): + data['config'] = public.readFile(config_file) + return data + + #添加编译参数 + def add_make_args(self,get): + get.args_name = get.args_name.strip() + get.name = get.name.strip() + get.ps = get.ps.strip() + if not re.match(r'^\w+$',get.args_name): + return public.return_msg_gettext(False,'Non-compliant names can only be numbers, letters, underscores') + + config_path = os.path.join('install' , get.name , get.args_name) + if not os.path.exists(config_path): + os.makedirs(config_path,384) + + init_file = os.path.join(config_path,'init.sh') + args_file = os.path.join(config_path,'args.pl') + ps_file = os.path.join(config_path,'ps.pl') + public.writeFile(init_file,get.init.replace("\r\n","\n")) + public.writeFile(args_file,get.args) + public.writeFile(ps_file,get.ps) + + public.write_log_gettext('Software manager','Add custom compilation parameters: {}:{}',(get.name,get.args_name)) + return public.return_msg_gettext(True,'Setup successfully!') + + #删除编译参数 + def del_make_args(self,get): + get.args_name = get.args_name.strip() + get.name = get.name.strip() + if not re.match(r'^\w+$',get.args_name): + return public.return_msg_gettext(False,'Non-compliant names can only be numbers, letters, underscores') + config_path = os.path.join('install' , get.name , get.args_name) + if not os.path.exists(config_path): + return public.return_msg_gettext(False,'The specified custom compilation parameters do not exist!') + public.ExecShell("rm -rf {}".format(config_path)) + config_file = 'install/' + get.name + '/config.pl' + if os.path.exists(config_file): + config_data = public.readFile(config_file).split("\n") + if get.args_name in config_data: + config_data.remove(get.args_name) + public.writeFile(config_file,"\n".join(config_data)) + public.write_log_gettext('Software manager','Remove custom compilation parameters: {}:{}',(get.name,get.args_name)) + return public.return_msg_gettext(True,'Successfully deleted') + + + #设置当前编译参数 + def set_make_args(self,get): + get.args_names = get.args_names.strip().split("\n") + get.name = get.name.strip() + config_file = 'install/' + get.name + '/config.pl' + config_data = [] + for args_name in get.args_names: + path = 'install/' + get.name + '/' + args_name + if not os.path.exists(path): continue + if args_name in config_data: continue + config_data.append(args_name) + public.writeFile(config_file,"\n".join(config_data)) + public.write_log_gettext('Software manager','Setup software: Custom compilation parameters for {} are configured as: {}'.format(get.name,config_data)) + return public.return_msg_gettext(True,'Setup successfully!') + + # 安装插件 + def __install_plugin(self, upgrade_plugin_name, upgrade_version=None): + ''' + @name 安装指定插件 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 版本号.指定版本号 / tls.最新正式版 / beta.最新测试版 + @return dict + ''' + self.__plugin_name = upgrade_plugin_name + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: + raise public.PanelError('指定插件不存在,无法安装!') + if not plugin_info['versions']: + raise public.PanelError('指定插件当前未发布版本信息,请稍候再安装!') + if not upgrade_version: + upgrade_version = '{}.{}'.format( + plugin_info['versions'][0]['m_version'], + plugin_info['versions'][0]['version']) + filename = self.__download_plugin(upgrade_plugin_name, upgrade_version) + # 如果下载失败 + if isinstance(filename, dict): + return filename + return self.__unpackup_plugin(filename) + + # 修复插件 + def __repair_plugin(self, upgrade_plugin_name, upgrade_version=None): + ''' + @name 修复指定插件 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 版本号.指定版本号 / tls.最新正式版 / beta.最新测试版 + @return dict + ''' + self.__install_opt = 'r' + return self.__install_plugin(upgrade_plugin_name, upgrade_version) + + # 升级插件版本 + def __upgrade_plugin(self, upgrade_plugin_name, upgrade_version=None): + ''' + @name 升级到指定版本 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 版本号.指定版本号 / tls.最新正式版 / beta.最新测试版 + @return dict + ''' + self.__install_opt = 'u' + return self.__install_plugin(upgrade_plugin_name, upgrade_version) + + # 获取插件信息 + def __get_plugin_info(self, upgrade_plugin_name): + ''' + @name 获取插件信息 + @author hwliang<2021-06-15> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + plugin_info_file = '{}/{}/info.json'.format(self.__plugin_path, + upgrade_plugin_name) + if not os.path.exists(plugin_info_file): return {} + info_body = self.__read_file(plugin_info_file) + if not info_body: return {} + plugin_info = json.loads(info_body) + return plugin_info + + # 获取插件最近1条更新日志 + def __get_update_msg(self, upgrade_plugin_name, upgrade_version): + ''' + @name 检查指定插件版本更新日志 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 + @return string + ''' + plugin_update_msg = '' + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: return plugin_update_msg + for _version_info in plugin_info['versions']: + l_version = '{}.{}'.format(_version_info['m_version'], + _version_info['version']) + if l_version == upgrade_version: + plugin_update_msg = _version_info['update_msg'] + break + return plugin_update_msg + + # 获取插件最近10条更新日志 + def __get_plugin_upgrades(self, upgrade_plugin_name): + ''' + @name 检查指定插件最近10条更新日志 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @return list + ''' + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: return [] + + try: + upgrade_list = public.httpPost( + self.__api_root_url + '/down/get_update_msg', + {'soft_id': plugin_info['id']}) + return json.loads(upgrade_list) + except: + return [] + + # 获取指定软件信息 + def __get_plugin_find(self, upgrade_plugin_name=None): + ''' + @name 获取指定软件信息 + @author hwliang<2021-06-15> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + self.__ensure_plugin_list_obtained(True) + + for p_data_info in self.__plugin_list['list']: + if p_data_info['name'] == upgrade_plugin_name: + upgrade_plugin_name = p_data_info['name'] + return p_data_info + + # 如果不在插件列表中 + return self.__get_plugin_info(upgrade_plugin_name) + + # 检查插件依赖 + def __check_dependent(self, upgrade_plugin_name): + ''' + @name 检查指定插件的依赖安装情况 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + plugin_info = self.__get_plugin_find(upgrade_plugin_name) + if not plugin_info: return {} + if not plugin_info['dependent']: return {} + deployment_list = {} + for dependent_plu_name in plugin_info['dependent'].split(','): + p_info = self.__get_plugin_find(dependent_plu_name) + if not p_info: continue + deployment_list[dependent_plu_name] = os.path.exists( + p_info['install_checks']) + return deployment_list + + # 读取指定文件 + def __read_file(self, filename, open_mode='r'): + ''' + @name 读取指定文件 + @author hwliang<2021-06-16> + @param filename 文件名 + @param mode 打开模式, 默认: r + @return bytes or string + ''' + f_object = open(filename, mode=open_mode) + file_body = f_object.read() + f_object.close() + return file_body + + # 解包插件压缩包 + def __unpackup_plugin(self, tmp_file): + ''' + @name 解包插件包 + @author hwliang<2021-06-21> + @param tmp_file 下载好的保存路径,从self.download_plugin方法中获取 + @return dict + ''' + if type(tmp_file) == dict: + return tmp_file + + if "false" in tmp_file or "错误" in tmp_file: + return json.loads(tmp_file) + + s_tmp_path = self.__tmp_path + if not os.path.exists(s_tmp_path): + os.makedirs(s_tmp_path, mode=384) + + if tmp_file: + if not os.path.exists(tmp_file): + return public.returnMsg(False, '文件下载失败!') + import panelTask as plu_panelTask + plu_panelTask.bt_task()._unzip(tmp_file, s_tmp_path, '', + '/dev/null') + if os.path.exists(tmp_file): + os.remove(tmp_file) + + s_tmp_path = os.path.join(s_tmp_path, self.__plugin_name) + + p_info = os.path.join(s_tmp_path, 'info.json') + if not os.path.exists(p_info): + d_path = None + for plugin_df in os.walk(s_tmp_path): + if len(plugin_df[2]) < 3: continue + if not 'info.json' in plugin_df[2]: continue + if not 'install.sh' in plugin_df[2]: continue + if not os.path.exists(plugin_df[0] + '/info.json'): continue + d_path = plugin_df[0] + if d_path: + s_tmp_path = d_path + p_info = s_tmp_path + '/info.json' + try: + try: + plugin_data_info = json.loads(public.ReadFile(p_info)) + except: + plugin_data_info = json.loads(self.__read_file(p_info)) + + plugin_data_info['size'] = public.get_path_size(s_tmp_path) + if not 'author' in plugin_data_info: + plugin_data_info['author'] = 'aapanel' + if not 'home' in plugin_data_info: + plugin_data_info['home'] = 'https://www.aapanel.com' + + p_info_file = self.__plugin_path + plugin_data_info[ + 'name'] + '/info.json' + plugin_data_info['old_version'] = '0' + plugin_data_info['tmp_path'] = s_tmp_path + if os.path.exists(p_info_file): + try: + old_info = json.loads(public.ReadFile(p_info_file)) + plugin_data_info['old_version'] = old_info['versions'] + except: + pass + except: + public.ExecShell("rm -rf " + s_tmp_path + '/*') + return public.get_error_object(plugin_name=self.__plugin_name) + plugin_data_info['install_opt'] = self.__install_opt + plugin_data_info['dependent'] = self.__check_dependent(plugin_data_info['name']) + plugin_data_info['update_msg'] = self.__get_update_msg( + plugin_data_info['name'], plugin_data_info['versions']) + not_check = self.not_cpu_or_bit(plugin_data_info) + if not_check: + if os.path.exists(s_tmp_path): shutil.rmtree(s_tmp_path) + return not_check + + return plugin_data_info + + # 检测是否为不支持的平台和系统位数 + def not_cpu_or_bit(self, plugin_data_info): + ''' + @name 检测是否为不支持的平台和系统位数 + @author hwliang<2021-07-07> + @param plugin_data_info 插件信息数据 + @return dict or None + ''' + if 'not_os_bit' in plugin_data_info: + if public.get_sysbit() == int(plugin_data_info['not_os_bit']): + return public.returnMsg( + False, + '该应用不支持{}位系统'.format(plugin_data_info['not_os_bit'])) + if 'not_cpu_type' in plugin_data_info: + if not plugin_data_info['not_cpu_type']: return None + machine = os.uname().machine + for c_type in plugin_data_info['not_cpu_type']: + c_type = c_type.lower() + result = public.returnMsg( + False, '该应用不支持{}平台,{}'.format(c_type, machine)) + if c_type in ['arm', 'aarch64', 'aarch']: + if machine in ['aarch64', 'aarch']: + return result + elif c_type in ['mips', 'mips64', 'mips64el']: + if machine.find('mips') != -1: + return result + elif c_type in ['x86', 'x86-64']: + if machine in ['x86', 'x86-64']: + return result + return None + + # 下载插件安装包 + def __download_plugin(self, upgrade_plugin_name, upgrade_version): + ''' + @name 下载插件包 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @param upgrade_version 插件版本 + @return string 保存路径 + ''' + + pkey = '{}_pre'.format(upgrade_plugin_name) + pdata = public.get_user_info() + pdata['name'] = upgrade_plugin_name + pdata['version'] = upgrade_version + pdata['os'] = 'Linux' + pdata['environment_info'] = json.dumps(public.fetch_env_info(), ensure_ascii=False) + filename = '{}/{}.zip'.format(self.__tmp_path, upgrade_plugin_name) + if not os.path.exists(self.__tmp_path): + os.makedirs(self.__tmp_path, 384) + + if not cache.get(pkey): + import config, socket + import requests.packages.urllib3.util.connection as urllib3_conn + _ip_type = config.config().get_request_iptype() + old_family = urllib3_conn.allowed_gai_family + if _ip_type == 'ipv4': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + elif _ip_type == 'ipv6': + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + try: + download_res = requests.post( + self.__download_url, + pdata, + headers=public.get_requests_headers(), + timeout=(60, 1800), + stream=True) + except Exception as ex: + str_ex = str(ex) + if 'Name or service not known' in str_ex: + return public.returnMsg(False, + '下载软件包时DNS解析失败,请检查服务器网络配置是否正常:

                                    Error: Name or service not known

                                    ') + elif 'Failed to establish a new connection' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Failed to establish a new connection

                                    ') + elif 'Read timed out' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Read timed out

                                    ') + elif 'Connection refused' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Connection refused

                                    ') + elif 'Remote end closed connection without response' in str_ex: + return public.returnMsg(False, + '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: Remote end closed connection without response

                                    ') + else: + return public.returnMsg(False, '连接下载节点失败,请尝试到【面板设置】页面切换通讯节点,或检查服务器网络是否正常:

                                    Error: {}

                                    '.format(str_ex)) + finally: + urllib3_conn.allowed_gai_family = old_family + + try: + headers_total_size = int(download_res.headers['File-size']) + except: + try: + return json.loads(download_res.text).json() + except: + if download_res.text.find('') != -1: + raise public.PanelError( + public.error_conn_cloud(download_res.text)) + raise public.PanelError(download_res.text) + + res_down_size = 0 + res_chunk_size = 8192 + last_time = time.time() + with open(filename, 'wb+') as with_res_f: + try: + for download_chunk in download_res.iter_content( + chunk_size=res_chunk_size): + if download_chunk: + with_res_f.write(download_chunk) + speed_last_size = len(download_chunk) + res_down_size += speed_last_size + res_start_time = time.time() + res_timeout = (res_start_time - last_time) + res_sec_speed = int(res_down_size / res_timeout) + pre_text = '{}/{}/{}'.format(res_down_size, + headers_total_size, + res_sec_speed) + cache.set(pkey, pre_text, 3600) + + except Exception as ex: + ex_str = str(ex) + if "Read timed out" in ex_str: + return public.returnMsg(False, '软件包下载超时,请重试: {}'.format(ex_str)) + if "No space left on device" in ex_str: + return public.returnMsg(False, "磁盘空间不足,请先清理后再试!") + finally: + with_res_f.close() + if cache.get(pkey): cache.delete(pkey) + + if public.FileMd5(filename) != download_res.headers['Content-md5']: + return public.returnMsg(False, '软件包校验失败,请更新软件列表,并重试') + else: + while True: + time.sleep(1) + if not cache.get(pkey): break + return '' + return filename + + # 获取插件安装包下载进度 + def __get_download_speed(self, upgrade_plugin_name): + ''' + @name 取插件下载进度 + @author hwliang<2021-06-21> + @param upgrade_plugin_name 插件名称 + @return dict + ''' + pkey = '{}_pre'.format(upgrade_plugin_name) + pre_text = cache.get(pkey) + if not pre_text: + return public.returnMsg(False, '指定进度信息不存在!') + result = {"status": True} + pre_tmp = pre_text.split('/') + result['down_size'], result['total_size'] = (int(pre_tmp[0]), + int(pre_tmp[1])) + result['down_pre'] = round( + result['down_size'] / result['total_size'] * 100, 1) + result['sec_speed'] = int(float(pre_tmp[2])) + result['need_time'] = int( + (result['total_size'] - result['down_size']) / result['sec_speed']) + return result + + + # 获取插件与授权信息 + def __ensure_plugin_list_obtained(self, force: bool = False): + if force or not self.__plugin_list: + self.__plugin_list = public.load_soft_list(force) diff --git a/class_v2/panel_push_v2.py b/class_v2/panel_push_v2.py new file mode 100644 index 00000000..01199ae7 --- /dev/null +++ b/class_v2/panel_push_v2.py @@ -0,0 +1,703 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 沐落 +# | Author: lx +# | 消息推送管理 +# | 对外方法 get_modules_list、install_module、uninstall_module、get_module_template、set_push_config、get_push_config、del_push_config +# +------------------------------------------------------------------- + +import os, sys + +panelPath = "/www/server/panel" +os.chdir(panelPath) +sys.path.insert(0,panelPath + "/class/") +import public,re,json,time +try: + from BTPanel import session +except : + pass +class panelPush: + + __conf_path = "{}/class/push/push.json".format(panelPath) + def __init__(self): + spath = '{}/class/push'.format(panelPath) + if not os.path.exists(spath): os.makedirs(spath) + + """ + @获取推送模块列表 + """ + def get_modules_list(self,get): + cpath = '{}/class/push/push_list.json'.format(panelPath) + try: + spath = os.path.dirname(cpath) + if not os.path.exists(spath): os.makedirs(spath) + + if 'force' in get or not os.path.exists(cpath): + if not 'download_url' in session: session['download_url'] = public.get_url() + public.downloadFile('{}/linux/panel/push/push_list.json'.format(session['download_url']),cpath) + except : pass + + if not os.path.exists(cpath): + return {} + + data = {} + push_list = self._get_conf() + module_list = public.get_modules('class/push') + + configs = json.loads(public.readFile(cpath)) + for p_info in configs: + p_info['data'] = {} + p_info['setup'] = False + p_info['info'] = False + key = p_info['name'] + try: + if hasattr(module_list, key): + p_info['setup'] = True + # if key in module_list: + # print(dir(module_list)) + # print(dir(module_list[key])) + # print(dir(getattr(module_list[key], key))) + push_module = getattr(module_list[key], key)() + p_info['info'] = push_module.get_version_info(None); + #格式化消息通道 + if key in push_list: + p_info['data'] = self.__get_push_list(push_list[key]) + #格式化返回执行周期 + if hasattr(push_module,'get_push_cycle'): + p_info['data'] = push_module.get_push_cycle(p_info['data']) + except : + return public.get_error_object(None) + data[key] = p_info + return data + + """ + 安装/更新消息通道模块 + @name 需要安装的模块名称 + """ + def install_module(self,get): + module_name = get.name + down_url = public.get_url() + + local_path = '{}/class/push'.format(panelPath) + if not os.path.exists(local_path): os.makedirs(local_path) + + sfile = '{}/{}.py'.format(local_path,module_name) + public.downloadFile('{}/linux/panel/push/{}.py'.format(down_url,module_name),sfile) + if not os.path.exists(sfile): return public.returnMsg(False, '[{}] Module installation failed'.format(module_name)) + if os.path.getsize(sfile) < 1024: return public.returnMsg(False, '[{}] Module installation failed'.format(module_name)) + + sfile = '{}/class/push/{}.html'.format(panelPath,module_name) + public.downloadFile('{}/linux/panel/push/{}.html'.format(down_url,module_name),sfile) + + return public.returnMsg(True, '[{}] Module installed successfully.'.format(module_name)) + + """ + 卸载消息通道模块 + @name 需要卸载的模块名称 + """ + def uninstall_module(self,get): + module_name = get.name + sfile = '{}/class/push/{}.py'.format(panelPath,module_name) + if os.path.exists(sfile): os.remove(sfile) + + return public.returnMsg(True, '[{}] Module uninstalled successfully'.format(module_name)) + + + """ + @获取模块执行日志 + """ + def get_module_logs(self,get): + module_name = get.name + id = get.id + return [] + + """ + 获取模块模板 + """ + def get_module_template(self,get): + sfile = '{}/class/push/{}.html'.format(panelPath,get.module_name) + + if not os.path.exists(sfile): + return public.returnMsg(False, 'template file does not exist!') + + shtml = public.readFile(sfile) + return public.returnMsg(True, shtml) + + + """ + @获取模块推送参数,如:panel_push ssl到期,服务停止 + """ + def get_module_config(self,get): + module = get.name + p_list = public.get_modules('class/push') + push_module = getattr(p_list[module], module)() + + if not module in p_list: + return public.returnMsg(False, 'The specified module [{}] is not installed!'.format(module)) + + if not hasattr(push_module,'get_module_config'): + return public.returnMsg(False, 'No get_module_config method exists for the specified module [{}].'.format(module)) + return push_module.get_module_config(get) + + + + """ + @获取模块配置项 + @优先调用模块内的get_push_config + """ + def get_push_config(self,get): + module = get.name + id = get.id + p_list = public.get_modules('class/push') + if not module in p_list: + return public.returnMsg(False, 'The specified module [{}] is not installed.'.format(module)) + + result = None + push_module = getattr(p_list[module], module)() + if not hasattr(push_module,'get_push_config'): + push_list = self._get_conf() + + res_data = public.returnMsg(False, 'The specified configuration was not found!') + res_data['code'] = 100 + if not module in push_list: + return res_data + if not id in push_list[module]: + return res_data + + result = push_list[module][id] + else: + result = push_module.get_push_config(get) + return self.get_push_user(result) + + def get_push_user(self,result): + + #获取发送给谁 + if not 'to_user' in result: + result['to_user'] = {} + if 'module' in result: + for s_module in result['module'].split(','): + result['to_user'][s_module] = 'default' + else: + return False + + info = {} + for s_module in result['module'].split(','): + msg_obj = public.init_msg(s_module) + if not msg_obj: continue + + info[s_module] = {} + data = msg_obj.get_config(None) + + if 'list' in data: + for key in result['to_user'][s_module].split(','): + if not key in data['list']: + continue + info[s_module][key] = data['list'][key] + result['user_info'] = info + return result + + """ + @设置推送配置 + @优先调用模块内的set_push_config + """ + def set_push_config(self,get): + module = get.name + id = get.id + p_list = public.get_modules('class/push') + + if not module in p_list: + return public.returnMsg(False, 'The specified module [{}] is not installed.'.format(module)) + + pdata = json.loads(get.data) + if not 'module' in pdata or not pdata['module']: + return public.returnMsg(False, 'The specified alarm method is not set, please select again.') + if module == "load_balance_push": + pdata = self.__get_args(pdata,'cycle', "500|502|503|504") + else: + pdata = self.__get_args(pdata, 'cycle', 1) + pdata = self.__get_args(pdata,'count',1) + pdata = self.__get_args(pdata,'interval',600) + pdata = self.__get_args(pdata,'key','') + pdata = self.__get_args(pdata,'push_count',0) + + nData = {} + for skey in ['key','type','cycle','count','interval','module','title','project','status','index','push_count']: + if skey in pdata: + nData[skey] = pdata[skey] + + public.set_module_logs('set_push_config',nData['type']) + class_obj = getattr(p_list[module], module)() + if hasattr(class_obj,'set_push_config'): + get['data'] = json.dumps(nData) + result = class_obj.set_push_config(get) + if 'status' in result: return result + + data = result + else: + data = self._get_conf() + if not module in data:data[module] = {} + data[module][id] = nData + + + public.writeFile(self.__conf_path,json.dumps(data)) + return public.returnMsg(True, 'Saved successfully') + + """ + @设置推送状态 + """ + def set_push_status(self,get): + id = get.id + module = get.name + + data = self._get_conf() + if not module in data: return public.returnMsg(True, 'module name does not exist!') + if not id in data[module]: return public.returnMsg(True, 'The specified push task does not exist!') + + status = int(get.status) + if status: + data[module][id]['status'] = True + else: + data[module][id]['status'] = False + public.writeFile(self.__conf_path,json.dumps(data)) + return public.returnMsg(True, 'Successful operation.') + """ + @删除指定配置 + """ + def del_push_config(self,get): + id = get.id + module = get.name + + p_list = public.get_modules('class/push') + if not module in p_list: + return public.returnMsg(False, 'The specified module {} is not installed.'.format(module)) + push_module = getattr(p_list[module], module)() + if not hasattr(push_module,'del_push_config'): + data = self._get_conf() + del data[module][id] + public.writeFile(self.__conf_path,json.dumps(data)) + return public.returnMsg(True, 'successfully deleted.') + + return push_module.del_push_config(get) + + """ + 获取消息通道配置列表 + """ + def get_push_msg_list(self,get): + data = {} + msgs = self.__get_msg_list() + from panelMessage import panelMessage + pm = panelMessage() + for x in msgs: + x['setup'] = False + key = x['name'] + try: + obj = pm.init_msg_module(key) + if obj: + x['setup'] = True + if key == 'sms':x['title'] = '{}?'.format(x['title']) + except : + pass + data[key] = x + return data + + """ + @ 获取消息推送配置 + """ + def _get_conf(self): + data = {} + try: + if os.path.exists(self.__conf_path): + data = json.loads(public.readFile(self.__conf_path)) + self.update_config(data) + except:pass + return data + + """ + @ 获取插件版本信息 + """ + def get_version_info(self): + """ + 获取版本信息 + """ + data = {} + data['ps'] = '' + data['version'] = '1.0' + data['date'] = '2020-07-14' + data['author'] = '宝塔' + data['help'] = 'http://www.bt.cn' + return data + + """ + @格式化推送对象 + """ + def format_push_data(self,push = ['dingding','weixin','feishu'], project = '', type = ''): + item = { + 'title':'', + 'project':project, + 'type':type, + 'cycle':1, + 'count':1, + 'keys':[], + 'helps':[], + 'push':push + } + return item + + + + def push_message_immediately(self, channel_data): + """推送消息到指定的消息通道,即时 + + Args: + channel_data(dict): + key: msg_channel, 消息通道名称,多个用逗号相连 + value: msg obj, 每种消息通道的消息内容格式,可能包含标题 + + Returns: + { + status: True/False, + msg: { + "email": {"status": msg}, + ... + } + } + """ + if type(channel_data) != dict: + return public.returnMsg(False, "The parameter is wrong") + + from panelMessage import panelMessage + pm = panelMessage() + channel_res = {} + res = { + "status": False, + "msg": channel_res + } + + for module, msg in channel_data.items(): + modules = [] + if module.find(",") != -1: + modules = module.split(",") + else: + modules.append(module) + for m_module in modules: + msg_obj = pm.init_msg_module(m_module) + if not msg_obj:continue + ret = msg_obj.push_data(msg) + if ret and "status" in ret and ret['status']: + res["status"] = True + channel_res[m_module] = ret + else: + msg = "Message push failed." + if "msg" in ret: + msg = ret["msg"] + channel_res[m_module] = public.returnMsg(False, msg) + return res + + """ + @格式为消息通道格式 + """ + def format_msg_data(self): + data = { + 'title':'', + 'to_email':'', + 'sms_type':'', + 'sms_argv':{}, + 'msg':'' + } + return data + + def __get_msg_list(self): + """ + 获取消息通道列表 + """ + data = [] + cpath = '{}/data/msg.json'.format(panelPath) + if not os.path.exists(cpath): + return data + try: + conf = public.readFile(cpath) + data = json.loads(conf) + except : + try: + time.sleep(0.5) + conf = public.readFile(cpath) + data = json.loads(conf) + except:pass + + return data + + def __get_args(self,data,key,val = ''): + """ + @获取默认参数 + """ + if not key in data: data[key] = val + if type(data[key]) != type(val): + data[key] = val + return data + + + def __get_push_list(self,data): + """ + @格式化列表数据 + """ + m_data = {} + result = {} + for x in self.__get_msg_list(): m_data[x['name']] = x + + for skey in data: + result[skey] = data[skey] + + m_list = [] + for x in data[skey]['module'].split(','): + if x in m_data: m_list.append(m_data[x]['title']) + result[skey]['m_title'] = '、'.join(m_list) + + m_cycle =[] + if data[skey]['cycle'] > 1: + m_cycle.append('every {} seconds'.format(data[skey]['cycle'])) + m_cycle.append('{} times, with an interval of {} seconds'.format(data[skey]['count'],data[skey]['interval'])) + result[skey]['m_cycle'] = ''.join(m_cycle) + + # 兼容旧版本没有返回project项,导致前端无法编辑问题 + if "project" not in result[skey] and "type" in result[skey]: + if result[skey]["type"] == "services": + services = ['nginx','apache',"pure-ftpd",'mysql','php-fpm','memcached','redis'] + _title = result[skey]['title'] + for s in services: + if _title.find(s)!=-1: + result[skey]["project"] = s + else: + result[skey]["project"] = result[skey]["type"] + if "project" in result[skey]: + if result[skey]["project"] == "FTP server": + result[skey]["project"] ="pure-ftpd" + return result + + + #************************************************推送 + """ + @推送data/push目录的所有文件 + """ + def push_messages_from_file(self): + + path = "{}/data/push".format(panelPath) + if not os.path.exists(path): os.makedirs(path) + + from panelMessage import panelMessage + pm = panelMessage() + + for x in os.listdir(path): + try: + spath = '{}/{}'.format(path,x) + if os.path.isdir(spath): continue + data = json.loads(public.readFile(spath)) + + msg_obj = pm.init_msg_module(data['module']) + if not msg_obj:continue + + ret = msg_obj.push_data(data) + if ret['status']: pass + + os.remove(spath) + except : + print(public.get_error_info()) + + """ + @消息推送线程 + """ + def start(self): + + total = 0 + interval = 5 + + tips = '{}/data/push/tips'.format(public.get_panel_path()) + if not os.path.exists(tips): os.makedirs(tips) + + try: + if True: + # 推送文件 + self.push_messages_from_file() + + # 调用推送子模块 + data = {} + is_write = False + path = "{}/class/push/push.json".format(panelPath) + + if os.path.exists(path): + data = public.readFile(path) + data = json.loads(data) + + p = public.get_modules('class/push') + for skey in data: + if len(data[skey]) <= 0: continue + if skey in ['panelLogin_push','panel_login']: continue #面板登录主动触发 + + total = None + obj = getattr(p[skey], skey)() + + for x in data[skey]: + try: + + item = data[skey][x] + item['id'] = x + if not item['status']: continue + if not item['module']: continue + if not 'index' in item: item['index'] = 0 + + if time.time() - item['index'] < item['interval']: + print('{} Interval not reached, skip.'.format(item['title'])) + continue + + #验证推送次数 + push_record = {} + tips_path = '{}/{}'.format(tips,x) + if 'push_count' in item and item['push_count'] > 0: + item['tips_list'] = [] + try: + push_record = json.loads(public.readFile(tips_path)) + except:pass + for k in push_record: + if push_record[k] < item['push_count']: + continue + item['tips_list'].append(k) + + #获取推送数据 + if not total: total = obj.get_total() + rdata = obj.get_push_data(item,total) + if not rdata: + continue + push_status = False + for m_module in item['module'].split(','): + if not m_module in rdata: + continue + + msg_obj = public.init_msg(m_module) + if not msg_obj:continue + + if 'to_user' in item and m_module in item['to_user']: + rdata[m_module]['to_user'] = item['to_user'][m_module] + + ret = msg_obj.push_data(rdata[m_module]) + data[skey][x]['index'] = rdata['index'] + is_write = True + push_status = True + + #获取是否推送成功. + if push_status: + if 'push_keys' in rdata: + for k in rdata['push_keys']: + if not k in push_record: push_record[k] = 0 + push_record[k] += 1 + public.writeFile(tips_path,json.dumps(push_record)) + except : + print(public.get_error_info()) + + if is_write: + public.writeFile(path,json.dumps(data)) + #time.sleep(interval) + except : + + print(public.get_error_info()) + + + def __get_login_panel_info(self): + """ + @name 获取面板登录列表 + @auther cjxin + @date 2022-09-29 + """ + import config + c_obj = config.config() + send_type = c_obj.get_login_send(None)['msg'] + if not send_type: + return False + return {"type":"panel_login","module":send_type,"interval":600,"status":True,"title":"Panel Login Alert","cycle":1,"count":1,"key":"","module_type":'site_push'} + + + def __get_ssh_login_info(self): + """ + @name 获取SSH登录列表 + @auther cjxin + @date 2022-09-29 + """ + import ssh_security + c_obj = ssh_security.ssh_security() + send_type = c_obj.get_login_send(None)['msg'] + if not send_type or send_type in ['error']: + return False + + return {"type":"ssh_login","module":send_type,"interval":600,"status":True,"title":"SSH login warning","cycle":1,"count":1,"key":"","module_type":'site_push'} + + + + def get_push_list(self,get): + """ + @获取所有推送列表 + """ + conf = self._get_conf() + for key in conf.keys(): + for x in conf[key]: + data = conf[key][x] + data['module_type'] = key + + conf[key][x] = self.get_push_user(data) + + if not 'site_push' in conf: conf['site_push'] = {} + + data = conf['site_push'] + for skey in ['panel_login','ssh_login']: + info = None + if skey in data: + del data[skey] + if skey in ['panel_login']: + info = self.__get_login_panel_info() + elif skey in ['ssh_login']: + info = self.__get_ssh_login_info() + + if info: + data[skey] = info + conf['site_push'] = data + return conf + + def get_push_logs(self,get): + """ + @name 获取推送日志 + """ + + p = 1 + limit = 15 + if 'p' in get: p = get.p + if 'limit' in get: limit = get.limit + + where = "type = 'Alarm notification'" + sql = public.M('logs') + + if hasattr(get, 'search'): + where = " and logs like '%{search}%' ".format(search=get.search) + + count = sql.where(where,()).count() + data = public.get_page(count,int(p),int(limit)) + data['data'] = public.M('logs').where(where,()).limit('{},{}'.format(data['shift'], data['row'])).order('id desc').select() + + return data + + # 兼容旧版本的告警 + def update_config(self, config): + if "site_push" not in config: + config["site_push"] = {} + if "panel_push" in config: + for k, v in config["panel_push"].items(): + if v["type"] != "endtime": + config["site_push"][k] = v + if "push_count" not in v: + v["push_count"] = 1 if v["type"] == "ssl" else 0 + del config["panel_push"] + public.writeFile(self.__conf_path, json.dumps(config)) + + +if __name__ == '__main__': + panelPush().start() diff --git a/class_v2/panel_redirect_v2.py b/class_v2/panel_redirect_v2.py new file mode 100644 index 00000000..a1bb4b85 --- /dev/null +++ b/class_v2/panel_redirect_v2.py @@ -0,0 +1,754 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2018 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# URL重写类 +#------------------------------ +import os,public,json,re,sys,socket,shutil +from public.validate import Param +os.chdir("/www/server/panel") +class panelRedirect: + + setupPath = '/www/server' + __redirectfile = "/www/server/panel/data/redirect.conf" + __firsturl="" + + #匹配目标URL的域名并返回 + def GetToDomain(self,tourl): + if tourl: + rep = r"https?://([\w\-\.]+)" + tu = re.search(rep, tourl) + return tu.group(1) + + #取某个站点下所有域名 + def GetAllDomain(self,sitename): + domains = [] + id = public.M('sites').where("name=?",(sitename,)).getField('id') + tmp = public.M('domain').where("pid=?",(id,)).field('name').select() + for key in tmp: + domains.append(key["name"]) + return domains + + #检测被重定向域名是否有已经存在配置文件里面 + def __CheckRepeatDomain(self,get,action): + conf_data = self.__read_config(self.__redirectfile) + repeat = [] + # for conf in conf_data: + # if conf["sitename"] == get.sitename and conf["redirectname"] != get.redirectname: + # repeat += list(set(conf["redirectdomain"]).intersection(set(get.redirectdomain))) + + + for conf in conf_data: + if conf["sitename"] == get.sitename: + if action == "create": + if conf["redirectname"] == get.redirectname: + repeat += list(set(conf["redirectdomain"]).intersection(set(get.redirectdomain))) + else: + if conf["redirectname"] != get.redirectname: + repeat += list(set(conf["redirectdomain"]).intersection(set(get.redirectdomain))) + if list(set(repeat)): + return list(set(repeat)) + + #检测被重定向路径是否重复 + def __CheckRepeatPath(self, get): + conf_data = self.__read_config(self.__redirectfile) + repeat = [] + for conf in conf_data: + if conf["sitename"] == get.sitename and get.redirectpath != "": + if conf["redirectname"] != get.redirectname and conf["redirectpath"] == get.redirectpath: + repeat.append(get.redirectpath) + if repeat: + return repeat + # 检测URL是否可以访问 + def __CheckRedirectUrl(self, domainlist): + """ + @name 检测URL是否可以访问 + @author: hezhihong + @param domainlist: 域名列表 + """ + http_list=[] + import requests + for i in domainlist: + i = i.replace("*.", "") + https_url = "https://" + i + http_url = "http://" + i + try: + response=requests.get(https_url,timeout=20) + if response.status_code==200:return https_url + except:pass + try: + response=requests.get(http_url,timeout=20) + if response.status_code==200:http_list.append(http_url) + except:pass + if http_list:return http_list[0] + else:return [] + + # 计算proxyname md5 + def __calc_md5(self,redirectname): + import hashlib + md5 = hashlib.md5() + md5.update(redirectname.encode('utf-8')) + return md5.hexdigest() + + # 设置Nginx配置 + def SetRedirectNginx(self,get): + ng_redirectfile = "%s/panel/vhost/nginx/redirect/%s/*.conf" % (self.setupPath,get.sitename) + ng_file = self.setupPath + "/panel/vhost/nginx/" + get.sitename + ".conf" + p_conf = self.__read_config(self.__redirectfile) + if public.get_webserver() == 'nginx': + shutil.copyfile(ng_file, '/tmp/ng_file_bk.conf') + if os.path.exists(ng_file): + ng_conf = public.readFile(ng_file) + if not p_conf: + rep = "#SSL-END(\n|.)*\\/redirect\\/.*\\*.conf;" + ng_conf = re.sub(rep, '#SSL-END', ng_conf) + public.writeFile(ng_file, ng_conf) + return + sitenamelist = [] + for i in p_conf: + sitenamelist.append(i["sitename"]) + + if get.sitename in sitenamelist: + rep = r"include.*\/redirect\/.*\*.conf;" + if not re.search(rep,ng_conf): + ng_conf = ng_conf.replace("#SSL-END","#SSL-END\n\t%s\n\t" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') + "include " + ng_redirectfile + ";") + public.writeFile(ng_file,ng_conf) + + else: + rep = "#SSL-END(\n|.)*\\/redirect\\/.*\\*.conf;" + ng_conf = re.sub(rep,'#SSL-END',ng_conf) + public.writeFile(ng_file, ng_conf) + + # 设置apache配置 + def SetRedirectApache(self,sitename): + ap_redirectfile = "%s/panel/vhost/apache/redirect/%s/*.conf" % (self.setupPath,sitename) + ap_file = self.setupPath + "/panel/vhost/apache/" + sitename + ".conf" + p_conf = public.readFile(self.__redirectfile) + if public.get_webserver() == 'apache': + shutil.copyfile(ap_file, '/tmp/ap_file_bk.conf') + if os.path.exists(ap_file): + ap_conf = public.readFile(ap_file) + if p_conf == "[]": + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') + ap_conf = re.sub(rep, '', ap_conf) + public.writeFile(ap_file, ap_conf) + return + if sitename in p_conf: + rep = "%s(\n|.)+IncludeOptional.*\\/redirect\\/.*conf" % public.get_msg_gettext('#referenced redirect rule') + rep1 = "combined" + if not re.search(rep,ap_conf): + ap_conf = ap_conf.replace(rep1, rep1 + "\n\t%s" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') +"\n\tIncludeOptional " + ap_redirectfile) + public.writeFile(ap_file,ap_conf) + else: + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext('#referenced redirect rule, if commented, the configured redirect rule will be invalid') + ap_conf = re.sub(rep,'', ap_conf) + public.writeFile(ap_file, ap_conf) + + # 创建修改配置检测 + def __CheckRedirectStart(self,get,action=""): + isError = public.checkWebConfig() + if (isError != True): + return public.return_message(-1,0, 'An error was detected in the configuration file. Please solve it before proceeding') + if action == "create": + #检测名称是否重复 + if sys.version_info.major < 3: + if len(get.redirectname) < 3 or len(get.redirectname) > 15: + return public.return_message(-1,0, 'Database name cannot be more than 16 characters!') + else: + if len(get.redirectname.encode("utf-8")) < 3 or len(get.redirectname.encode("utf-8")) > 15: + return public.return_message(-1,0, 'Database name cannot be more than 16 characters!') + if 'errorpage' in get:is_error_page = True + else:is_error_page = False + if self.__CheckRedirect(get.sitename,get.redirectname,is_error_page): + return public.return_message(-1,0, 'Specified redirect name already exists') + #检测目标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 'tourl' in get and not re.match(rep, get.tourl): + return public.return_message(-1,0, 'Target URL format is wrong %s' + get.tourl) + + #非404页面重定向检测项 + if 'errorpage' not in get: + #检测是否选择域名 + if get.domainorpath == "domain": + if not json.loads(get.redirectdomain): + return public.return_message(-1,0, 'Please select redirected domain') + else: + if not get.redirectpath: + return public.return_message(-1,0, 'Please enter redirected path') + #repte = "[\\?\\=\\[\\]\\)\\(\\*\\&\\^\\%\\$\\#\\@\\!\\~\\`{\\}\\>\\<\\,\',\"]+" + # 检测路径格式 + if "/" not in get.redirectpath: + return public.return_message(-1,0, 'Path format is incorrect, the format is /xxx') + #if re.search(repte, get.redirectpath): + # return public.return_msg_gettext(False, "代理目录不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]") + #检测域名是否已经存在配置文件 + repeatdomain = self.__CheckRepeatDomain(get,action) + if repeatdomain: + return public.return_message(-1,0, 'Redirected domain already exists {}' , (repeatdomain,)) + #检测路径是否有存在配置文件 + repeatpath = self.__CheckRepeatPath(get) + if repeatpath: + return public.return_message(-1,0, 'Redirected domain already exists {}' , (repeatpath,)) + #检测目标URL是否可用 + #if self.__CheckRedirectUrl(get): + # return public.return_msg_gettext(False, '目标URL无法访问') + + #检查目标URL的域名和被重定向的域名是否一样 + if get.domainorpath == "domain": + for d in json.loads(get.redirectdomain): + tu = self.GetToDomain(get.tourl) + if d == tu: + return public.return_message(-1,0,public.get_msg_gettext('Domain name {} is the same as the target domain name, please deselect it',(d,))) + + if get.domainorpath == "path": + domains = self.GetAllDomain(get.sitename) + rep = "https?://(.*)" + tu = re.search(rep,get.tourl).group(1) + for d in domains: + ad = "%s%s" % (d,get.redirectpath) #站点域名+重定向路径 + if tu == ad: + return public.return_message(0,0,'{}, the target URL is the same as the redirected path',(tu,)) + + #404页面重定向检测项 + else: + if 'tourl' not in get and 'topath' not in get: + return public.return_message(-1,0, 'Please select where you need to redirect to') + #网站首页访问检测 + if 'topath' in get and get.topath == "/": + domainlist=self.GetAllDomain(get.sitename) + self.__firsturl=self.__CheckRedirectUrl(domainlist) + if not self.__firsturl:return public.return_message(-1,0, 'The website cannot be accessed, please check whether the website is working properly') + + #创建重定向 + def CreateRedirect(self,get): + # 校验参数 + try: + get.validate([ + Param('sitename').String(), + Param('redirectname').String(), + Param('domainorpath').String(), + Param('redirectpath').String(), + Param('redirectdomain').String(), + Param('tourl').String(), + Param('type').Integer(), + Param('holdpath').Integer(), + Param('redirecttype').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if self.__CheckRedirectStart(get,"create"): + return self.__CheckRedirectStart(get,"create") + redirectconf = self.__read_config(self.__redirectfile) + redirectconf.append({ + "sitename":get.sitename, + "redirectname":get.redirectname, + "tourl":get.tourl, + "redirectdomain":json.loads(get.redirectdomain), + "redirectpath":get.redirectpath, + "redirecttype":get.redirecttype, + "type":int(get.type), + "domainorpath":get.domainorpath, + "holdpath":int(get.holdpath) + }) + self.__write_config(self.__redirectfile,redirectconf) + self.SetRedirectNginx(get) + self.SetRedirectApache(get.sitename) + self.SetRedirect(get) + public.serviceReload() + return public.return_message(0,0, 'Successfully created file!') + + + def ModifyRedirect(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 + """ + + # 校验参数 + try: + get.validate([ + Param('sitename').String(), + Param('redirectname').String(), + Param('domainorpath').String(), + Param('redirectpath').String(), + Param('redirectdomain').String(), + Param('tourl').String(), + Param('type').Integer(), + Param('holdpath').Integer(), + Param('redirecttype').Integer(), + Param('errorpage').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # 基本信息检查 + if self.__CheckRedirectStart(get): + return self.__CheckRedirectStart(get) + redirectconf = self.__read_config(self.__redirectfile) + for i in range(len(redirectconf)): + domainorpath='' + if 'domainorpath' not in get or not get.domainorpath:domainorpath='domain' if get.tourl else 'path' + if not domainorpath:domainorpath=get.domainorpath + if redirectconf[i]["redirectname"] == get.redirectname and redirectconf[i]["sitename"] == get.sitename: + redirectconf[i]["tourl"] =get.tourl if 'tourl' in get and get.tourl else "" + redirectconf[i]["redirectdomain"] = "" if 'redirectdomain' not in get else json.loads(get.redirectdomain) + redirectconf[i]["redirectpath"] ="" if 'redirectpath' not in get else get.redirectpath + redirectconf[i]["redirecttype"] ='' if 'redirecttype' not in get else get.redirecttype + redirectconf[i]["type"] = int(get.type) + redirectconf[i]["domainorpath"] = domainorpath + redirectconf[i]["topath"] = "" if 'topath' not in get else get.topath + redirectconf[i]["holdpath"] =999 if 'holdpath' not in get else int(get.holdpath) + redirectconf[i]["errorpage"]=1 if 'errorpage' in get and get.errorpage in [1,'1'] else 0 + self.__write_config(self.__redirectfile, redirectconf) + redirect_path=get.tourl.strip() if 'tourl' in get and get.tourl else get.topath.strip() + #404页面重定向 + is_del= True if int(get.type) == 0 else False + if 'errorpage' in get and get.errorpage in [1,'1']: + web_type=public.get_webserver() + if web_type == 'nginx': + self.SetRedirectNginx(get) + self.unset_nginx_conf(get.sitename) + self.get_nginx_conf(redirect_path,get.redirecttype,get.sitename,get.redirectname,is_del) + elif web_type == 'apache' or web_type == 'openlitespeed': + self.get_apache_conf(redirect_path,get.sitename,get.redirectname,str(get.redirecttype),is_del) + else: + return public.return_message(-1,0,'web server not installed or unknown web server') + #非404页面重定向 + else: + self.SetRedirect(get) + self.SetRedirectNginx(get) + self.SetRedirectApache(get.sitename) + public.serviceReload() + return public.return_message(0,0, 'Successfully modified') + + + def set_error_redirect(self,get): + """ + @name 设置404重定向 + @author hezhihong + @param get.sitename 站点名称 + @param get.redirectname 重定向名称(唯一key标志) + @param get.tourl 重定向到的url + @param get.topath 重定向到的路径 + @param get.redirecttype 重定向类型 + @param get.type 重定向状态 0禁用 1启用 + @param get.domainorpath 重定向类型 domain 域名重定向 path 路径重定向 + @param get.holdpath 是否保留原路径 0不保留 1保留 + @param get.errorpage 是否为404重定向 1是 0否 + @return json + """ + public.set_module_logs('panelRedirect','set_error_redirect') + check_result = self.__CheckRedirectStart(get,"create") + if check_result:return check_result + redirectconf = self.__read_config(self.__redirectfile) + site_name= get.sitename.strip() + redirect_path=get.tourl if 'tourl' in get and get.tourl and get.tourl.strip() else get.topath.strip() + redirectconf.append({ + "sitename":site_name, + "redirectname":get.redirectname, + "tourl":get.tourl if 'tourl' in get else '', + "redirectdomain":"", + "redirectpath":"", + "topath": get.topath.strip() if 'topath' in get and get.topath.strip() else "", + "redirecttype":get.redirecttype, + "type":int(get.type), + "domainorpath":'domain' if 'tourl' in get else 'path', + "holdpath":999, + "errorpage":1 + }) + self.__write_config(self.__redirectfile,redirectconf) + web_type=public.get_webserver() + if web_type == 'nginx': + self.SetRedirectNginx(get) + self.unset_nginx_conf(site_name) + self.get_nginx_conf(redirect_path,get.redirecttype,site_name,get.redirectname) + elif web_type == 'apache' or web_type == 'openlitespeed': + self.SetRedirectApache(get.sitename) + self.get_apache_conf(redirect_path,site_name,get.redirectname,str(get.redirecttype)) + else: + return public.returnMsg(False,'web server not installed or unknown web server') + public.serviceReload() + return public.returnMsg(True, '404 redirect set successfully') + + + def get_nginx_conf(self,redirect_path,redirecttype,site_name,redirectname,is_del=False): + """ + @name 设置nginx 404重定向 + @author hezhihong + @param redirect_path 重定向到(路径或地址) + @param redirecttype 重定向方式(301/302) + @param site_name 站点名称 + @param redirectname 重定向名称(唯一key标志) + @param is_del 是否删除 + """ + redirectname_md5 = self.__calc_md5(redirectname) + file_path= "%s/panel/vhost/nginx/redirect/%s" % (self.setupPath,site_name) + public.ExecShell("mkdir -p %s" % file_path) + file_path+= '/%s_%s.conf' % (redirectname_md5, site_name) + add_str='#REWRITE-START\nerror_page 404 = @notfound;\nlocation @notfound {\n return '+str(redirecttype)+' '+ redirect_path+'; \n}\n#REWRITE-END' + if os.path.isfile(file_path):public.ExecShell("rm -f %s" % file_path) + if not is_del:public.writeFile(file_path,add_str) + + def get_apache_conf(self,redirect_path,site_name,redirectname='',r_type='301',is_del=False): + """ + @name 设置apache 404重定向 + @author hezhihong + @param redirect_path 重定向到(路径或地址) + @param site_name 站点名称 + @param redirectname 重定向名称(唯一key标志) + @param is_del 是否删除 + @param r_type 重定向方式 + """ + if self.__firsturl:redirect_path=self.__firsturl + add_type=',R={}]'.format(str(r_type)) + add_str='#REWRITE-START\n\n RewriteEngine on\n RewriteCond %\\{REQUEST_FILENAME\\} !-f\n RewriteCond %{REQUEST_FILENAME} !-d\n RewriteRule . '+redirect_path+' [L'+add_type+'\n\n#REWRITE-END' + redirectname_md5 = self.__calc_md5(redirectname) + file_path= "%s/panel/vhost/apache/redirect/%s" % (self.setupPath,site_name) + public.ExecShell("mkdir -p %s" % file_path) + file_path+= '/%s_%s.conf' % (redirectname_md5, site_name) + if os.path.isfile(file_path):public.ExecShell("rm -f %s" % file_path) + if not is_del:public.writeFile(file_path,add_str) + + + def unset_nginx_conf(self,site_name): + """ + @name 取消设置nginx 404重定向 + @author hezhihong + @param site_name 站点名称 + """ + file_path='/www/server/panel/vhost/nginx/{}.conf'.format(site_name) + hta_path='/www/server/panel/vhost/rewrite/{}.conf'.format(site_name) + rep_str_one='error_page 404 /404.html' + rep_str_two='location = /404.html' + #清理nginx伪静态404配置 + hta_conf=public.readFile(hta_path) + if hta_conf: + hta_conf=self.replace_str_to_srt(hta_conf,rep_str_one,'','\n') + hta_conf=self.replace_str_to_srt(hta_conf,rep_str_two,'','}') + public.writeFile(hta_path,hta_conf) + #清理nginx网站配置文件非include方式404配置 + conf=public.readFile(file_path) + conf=self.replace_str_to_srt(conf,rep_str_one,'','\n') + conf=self.replace_str_to_srt(conf,rep_str_two,'','}') + public.writeFile(file_path,conf) + + + def replace_str_to_srt(self,conf,str_src,str_d,end_str,is_replace=False): + """ + @name 替换字符串 + @author hezhihong + @param conf 配置文件内容 + @param str_src 要替换的字符串 + @param str_d 替换成的字符串 + @param end_str 结束字符串 + @param is_replace 是否替换 + """ + if conf.strip(): + start_num=conf.find(str_src) + if start_num !=-1: + d_conf=conf[start_num:] + end_num = d_conf.find(end_str) + if end_num ==-1:end_num=len(conf) + d_conf=d_conf[:end_num+1] + if is_replace:conf=conf.replace(d_conf,str_d) + else:conf=conf.replace(d_conf,'') + return conf + + + + # 设置重定向 + def SetRedirect(self,get): + ng_file = self.setupPath + "/panel/vhost/nginx/" + get.sitename + ".conf" + ap_file = self.setupPath + "/panel/vhost/apache/" + get.sitename + ".conf" + p_conf = self.__read_config(self.__redirectfile) + # nginx + # 构建重定向配置 + if int(get.type) == 1: + domainstr = """ + if ($host ~ '^%s'){ + return %s %s%s; + } +""" + pathstr = """ + rewrite ^%s(.*) %s%s %s; +""" + rconf = "#REWRITE-START" + tourl = get.tourl + # if tourl[-1] == "/": + # tourl = tourl[:-1] + if get.domainorpath == "domain": + domains = json.loads(get.redirectdomain) + holdpath = int(get.holdpath) + if holdpath == 1: + for sd in domains: + rconf += domainstr % (sd,get.redirecttype,tourl,"$request_uri") + else: + for sd in domains: + rconf += domainstr % (sd,get.redirecttype,tourl,"") + if get.domainorpath == "path": + redirectpath = get.redirectpath + if get.redirecttype == "301": + redirecttype = "permanent" + else: + redirecttype = "redirect" + if int(get.holdpath) == 1 and redirecttype == "permanent": + rconf += pathstr % (redirectpath,tourl,"$1",redirecttype) + elif int(get.holdpath) == 0 and redirecttype == "permanent": + rconf += pathstr % (redirectpath, tourl,"",redirecttype) + elif int(get.holdpath) == 1 and redirecttype == "redirect": + rconf += pathstr % (redirectpath,tourl,"$1",redirecttype) + elif int(get.holdpath) == 0 and redirecttype == "redirect": + rconf += pathstr % (redirectpath, tourl,"",redirecttype) + rconf += "#REWRITE-END" + nginxrconf = rconf + + + + # 设置apache重定向 + + domainstr = """ + + RewriteEngine on + RewriteCond %s{HTTP_HOST} ^%s [NC] + RewriteRule ^(.*) %s%s [L,R=%s] + +""" + pathstr = """ + + RewriteEngine on + RewriteRule ^%s(.*) %s%s [L,R=%s] + +""" + rconf = "#REWRITE-START" + if get.domainorpath == "domain": + domains = json.loads(get.redirectdomain) + holdpath = int(get.holdpath) + if holdpath == 1: + for sd in domains: + rconf += domainstr % ("%",sd,tourl,"$1",get.redirecttype) + else: + for sd in domains: + rconf += domainstr % ("%",sd,tourl,"",get.redirecttype) + + if get.domainorpath == "path": + holdpath = int(get.holdpath) + if holdpath == 1: + rconf += pathstr % (get.redirectpath,tourl,"$1",get.redirecttype) + else: + rconf += pathstr % (get.redirectpath,tourl,"",get.redirecttype) + rconf += "#REWRITE-END" + apacherconf = rconf + + redirectname_md5 = self.__calc_md5(get.redirectname) + for w in ["nginx","apache"]: + redirectfile = "%s/panel/vhost/%s/redirect/%s/%s_%s.conf" % (self.setupPath,w,get.sitename,redirectname_md5, get.sitename) + redirectdir = "%s/panel/vhost/%s/redirect/%s" % (self.setupPath,w,get.sitename) + + if not os.path.exists(redirectdir): + public.ExecShell("mkdir -p %s" % redirectdir) + if w == "nginx": + public.writeFile(redirectfile,nginxrconf) + else: + public.writeFile(redirectfile, apacherconf) + isError = public.checkWebConfig() + if (isError != True): + if public.get_webserver() == "nginx": + shutil.copyfile('/tmp/ng_file_bk.conf', ng_file) + else: + shutil.copyfile('/tmp/ap_file_bk.conf', ap_file) + for i in range(len(p_conf) - 1, -1, -1): + if get.sitename == p_conf[i]["sitename"] and p_conf[i]["redirectname"]: + del(p_conf[i]) + return public.return_msg_gettext(False, '%s
                                    ' % public.get_msg_gettext('Sorry, something went wrong') + isError.replace("\n",'
                                    ') + '
                                    ') + + else: + redirectname_md5 = self.__calc_md5(get.redirectname) + redirectfile = "%s/panel/vhost/%s/redirect/%s/%s_%s.conf" + for w in ["apache","nginx"]: + rf = redirectfile % (self.setupPath,w ,get.sitename, redirectname_md5, get.sitename) + if os.path.exists(rf): + os.remove(rf) + + def del_redirect_multiple(self,get): + ''' + @name 批量删除重定向 + @author zhwen<2020-11-21> + @param site_id 1 + @param redirectnames test,baohu + ''' + redirectnames = get.redirectnames.split(',') + del_successfully = [] + del_failed = {} + get.sitename = public.M('sites').where("id=?", (get.site_id,)).getField('name') + for redirectname in redirectnames: + get.redirectname = redirectname + try: + get.multiple = 1 + result = self.DeleteRedirect(get,multiple=1) + if result['status'] !=0: + del_failed[redirectname] = result['msg'] + continue + del_successfully.append(redirectname) + except: + del_failed[redirectname]=public.get_msg_gettext('There was an error deleting, please try again.') + public.serviceReload() + return public.return_message(0,0,{'msg': public.get_msg_gettext('Delete redirects [{}] successfully',(','.join(del_successfully),)), 'error': del_failed, + 'success': del_successfully}) + + def DeleteRedirect(self,get,multiple=None): + # 校验参数 + try: + get.validate([ + Param('sitename').String(), + Param('redirectname').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + redirectconf = self.__read_config(self.__redirectfile) + sitename = get.sitename + redirectname = get.redirectname + for i in range(len(redirectconf)): + if redirectconf[i]["sitename"] == sitename and redirectconf[i]["redirectname"] == redirectname: + proxyname_md5 = self.__calc_md5(redirectconf[i]["redirectname"]) + public.ExecShell("rm -f %s/panel/vhost/nginx/redirect/%s/%s_%s.conf" % (self.setupPath,redirectconf[i]["sitename"],proxyname_md5,redirectconf[i]["sitename"])) + public.ExecShell("rm -f %s/panel/vhost/apache/redirect/%s/%s_%s.conf" % (self.setupPath,redirectconf[i]["sitename"],proxyname_md5, redirectconf[i]["sitename"])) + del redirectconf[i] + self.__write_config(self.__redirectfile,redirectconf) + self.SetRedirectNginx(get) + self.SetRedirectApache(get.sitename) + if not multiple: + public.serviceReload() + return public.return_message(0,0, 'Successfully deleted') + + def GetRedirectList(self,get): + """ + @name 获取重定向列表 + @author hezhihong + @param get.sitename 站点名 + @param get.errorpage 1:404页面重定向 0:非404页面重定向 + @return 重定向列表 + """ + # 校验参数 + 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)) + + redirectconf = self.__read_config(self.__redirectfile) + sitename = get.sitename + redirectlist = [] + for i in redirectconf: + if i["sitename"] == sitename: + if 'errorpage' in get and 'errorpage' in i and int(get.errorpage)!=int(i['errorpage']):continue + if 'errorpage' in i and i['errorpage'] in [1,'1']:i['redirectdomain']=['404 page'] + redirectlist.append(i) + print(redirectlist) + return public.return_message(0,0,redirectlist) + + def ClearOldRedirect(self,get): + for i in ["apache","nginx"]: + conf_path = "%s/panel/vhost/%s/%s.conf" % (self.setupPath,i,get.sitename) + old_conf = public.readFile(conf_path) + rep ="" + if i == "nginx": + rep += "#301-START\n+[\\s\\w\\:\\/\\.\\;\\$]+#301-END" + if i == "apache": + rep += "#301-START[\n\\<\\>\\w\\.\\s\\^\\*\\$\\/\\[\\]\\(\\)\\:\\,\\=]+#301-END" + conf = re.sub(rep, "", old_conf) + public.writeFile(conf_path, conf) + public.serviceReload() + return public.return_msg_gettext(False, 'Old redirection cleaned') + + # 取重定向配置文件 + def GetRedirectFile(self,get): + # 校验参数 + try: + get.validate([ + Param('sitename').String(), + Param('redirectname').String(), + Param('webserver').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + import files_v2 as files + conf = self.__read_config(self.__redirectfile) + sitename = get.sitename + redirectname = get.redirectname + proxyname_md5 = self.__calc_md5(redirectname) + if get.webserver == 'openlitespeed': + get.webserver = 'apache' + get.path = "%s/panel/vhost/%s/redirect/%s/%s_%s.conf" % (self.setupPath, get.webserver, sitename,proxyname_md5,sitename) + for i in conf: + if redirectname == i["redirectname"] and sitename == i["sitename"] and i["type"] != 1: + return public.return_message(-1,0, 'Redirection suspended') + f = files.files() + return_message =f.GetFileBody(get) + return_message['message']['file']=get.path + return return_message + + # 保存重定向配置文件 + def SaveRedirectFile(self,get): + import files_v2 as files + f = files.files() + return f.SaveFileBody(get) + # return public.return_msg_gettext(True, '保存成功') + + def __CheckRedirect(self,sitename,redirectname,is_error=False): + conf_data = self.__read_config(self.__redirectfile) + for i in conf_data: + if i["sitename"] == sitename: + if is_error and "errorpage" in i and i["errorpage"] in [1,'1']: + return i + if i["redirectname"] == redirectname: + return i + + + # 读配置 + def __read_config(self, path): + if not os.path.exists(path): + public.writeFile(path, '[]') + upBody = public.readFile(path) + if not upBody: upBody = '[]' + return json.loads(upBody) + + # 写配置 + def __write_config(self ,path, data): + return public.writeFile(path, json.dumps(data)) + + diff --git a/class_v2/panel_restore_v2.py b/class_v2/panel_restore_v2.py new file mode 100644 index 00000000..93924381 --- /dev/null +++ b/class_v2/panel_restore_v2.py @@ -0,0 +1,226 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: zhwwen +# ------------------------------------------------------------------- +# +# ------------------------------ +# 网站恢复 +# ------------------------------ +import public,os,files,sys +from time import sleep +from public.validate import Param +class panel_restore: + + _local_file = '/tmp/{}' + _progress_file = '/tmp/restore_site.log' + + # def __init__(self): + # # 清空日志文件 + + def _progress_rewrite(self,content,mothed='a+'): + sleep(2) + public.writeFile(self._progress_file,content+'\n',mothed) + + def _get_local_backup_path(self): + local_backdir = public.M('config').field('backup_path').find()['backup_path'] + return local_backdir + + def _build_aws_backup_path(self,btype,file_name,domain): + config_file = "/www/server/panel/plugin/aws_s3/config.conf" + conf = public.readFile(config_file) + backup_path = conf.split('|')[-1].strip()+btype+'/'+ domain + '/' + file_name + return backup_path + + def _build_google_backup_path(self,btype,file_name,domain): + object_name = 'bt_backup/{}/{}/{}'.format(btype,domain,file_name) + return object_name + + def _get_backfile_method(self,filename): + backup_info = public.M('backup').where("name=?", (filename,)).getField('filename') + backup_info = backup_info.split('|') + if len(backup_info) >= 3: + method = backup_info[1] + else: + method = 'local' + return method + + def _remove_old_website_file_to_trush(self,args): + # 将原来目录移至回收站 + files.files().DeleteDir(args) + + def _get_website_info(self,site_id): + site_name = public.M('sites').where("id=?",(site_id,)).getField('name') + site_path = public.M('sites').where("id=?",(site_id,)).getField('path') + return {'site_name':site_name,'site_path':site_path} + + def _restore_backup(self,local_backup_file_path,site_info,args): + + # 判断备份文件是否存在,如果不存在继续检查是否远程备份 + if not os.path.exists(local_backup_file_path): + self._progress_rewrite('No backup file found: {}'.format(str(local_backup_file_path))) + return public.return_message(-1,0, 'Panel does not find the backup file: {}'.format(local_backup_file_path)) + # 将网站目录移至回收站 + self._progress_rewrite('Move the current website directory to the recycle bin: {}'.format(str(args.path))) + self._remove_old_website_file_to_trush(args) + if not os.path.exists(args.path): + self._progress_rewrite('Create an empty directory for the site: {}'.format(str(args.path))) + os.makedirs(site_info['site_path']) + if 'zip' in args.file_name: + uncompress_comand = 'unzip' + else: + uncompress_comand = 'tar -zxvf' + self._progress_rewrite('The decompression command is: {}'.format(str(uncompress_comand))) + self._progress_rewrite('Start to restore data......') + public.ExecShell('cd {} && {} {} >> /tmp/restore_site.log'.format(site_info['site_path'], uncompress_comand, local_backup_file_path)) + if len(os.listdir(site_info['site_path'])) == 2: + public.ExecShell('cd {s} && mv {s}/{d}/* .'.format(s=site_info['site_path'],d=site_info['site_name'])) + public.ExecShell('cd {s} && rmdir {d}'.format(s=site_info['site_path'],d=site_info['site_name'])) + # 将文件全新设置为644,文件夹设置为755 + self._progress_rewrite('Setting site permissions......') + files.files().fix_permissions(args) + + def _download_aws_file(self,args,btype='site'): + sys.path.append('/www/server/panel/plugin/aws_s3') + import aws_s3_main + aws3 = aws_s3_main.aws_s3_main() + self._progress_rewrite('Building S3 download path...') + download_file = self._build_aws_backup_path(btype,args.file_name,args.obj_name) + self._progress_rewrite('The download path is:{}'.format(download_file)) + self._local_file = self._local_file.format(args.file_name) + self._progress_rewrite('Backup file will be downloaded to:{}'.format(self._local_file)) + self._progress_rewrite('Starting to download file:{}'.format(self._local_file)) + args.object_name = download_file + args.local_file = self._local_file + aws3.download_file(args) + self._progress_rewrite('Download completed:{}'.format(self._local_file)) + return self._local_file + + def _download_google_cloud_file(self,args,btype='site'): + sys.path.append('/www/server/panel/plugin/gcloud_storage') + import gcloud_storage_main + gs = gcloud_storage_main.gcloud_storage_main() + self._progress_rewrite('Building Google Store download path...') + download_file = self._build_google_backup_path(btype,args.file_name,args.obj_name) + self._progress_rewrite('The download path is:{}'.format(download_file)) + self._local_file = self._local_file.format(args.file_name) + self._progress_rewrite('Backup file will be downloaded to:{}'.format(self._local_file)) + self._progress_rewrite('Starting to download file:{}'.format(self._local_file)) + args.source_blob_name = download_file + args.destination_file_name = self._local_file + gs.download_blob(args) + self._progress_rewrite('Download completed:{}'.format(self._local_file)) + return self._local_file + + def _download_google_drive_file(self,args): + sys.path.append('/www/server/panel/plugin/gdrive') + import gdrive_main + gd = gdrive_main.gdrive_main() + self._local_file = self._local_file.format(args.file_name) + self._progress_rewrite('Backup file will be downloaded to:{}'.format(self._local_file)) + self._progress_rewrite('Starting to download file:{}'.format(self._local_file)) + gd.download_file(args.file_name) + self._progress_rewrite('Download completed:{}'.format(self._local_file)) + return self._local_file + + def restore_website_backup(self,args): + """ + @name 恢复站点文件 + @author zhwen + @parma file_name 备份得文件名 + @parma site_id 网站id + """ + # 校验参数 + try: + args.validate([ + Param('file_name').String(), + Param('site_id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + self._progress_rewrite('','w') + site_info = self._get_website_info(args.site_id) + self._progress_rewrite('Get site information:{}'.format(str(site_info))) + args.path = site_info['site_path'] + args.obj_name = site_info['site_name'] + self._progress_rewrite('Get the site path:{}'.format(str(site_info['site_path']))) + local_backup_path = self._get_local_backup_path() + local_backup_file_path = local_backup_path +'/site/'+ args.file_name + self._progress_rewrite('Get the local backup file path: {}'.format(str(local_backup_path))) + backup_method = self._get_backfile_method(args.file_name) + self._progress_rewrite('Get the backup method: {}'.format(str(backup_method))) + if backup_method == 'local': + self._progress_rewrite('Start to restore local backup files: {}'.format(str(local_backup_file_path))) + result = self._restore_backup(local_backup_file_path,site_info,args) + if result: + self._progress_rewrite('Recovery failed: {}'.format(str(site_info['site_path']))) + return result + elif backup_method == 'aws_s3': + self._download_aws_file(args) + result = self._restore_backup(self._local_file, site_info, args) + elif backup_method == 'Google Cloud': + self._download_google_cloud_file(args) + result = self._restore_backup(self._local_file, site_info, args) + elif backup_method == 'Google Drive': + self._download_google_drive_file(args) + result = self._restore_backup(self._local_file, site_info, args) + else: + return public.return_msg_gettext(False,'Currently only supports restoring local, Google storage and AWS S3 backups') + if os.path.exists(self._local_file): + os.remove(self._local_file) + if result: + self._progress_rewrite('Recovery failed: {}'.format(str(site_info['site_path']))) + return result + self._progress_rewrite('Successful recovery: {}'.format(str(site_info['site_path']))) + return public.return_message(0,0,'Restore Successful') + + # 取任务进度 + def get_progress(self, get): + """ + @name 获取进度日志 + @author zhwen + """ + # result = public.GetNumLines(self._progress_file, 20) + result = public.ExecShell('tail -n 20 {}'.format(self._progress_file))[0] + if len(result) < 1: + return public.return_message(0,0,"Wait for the restore to start") + return public.return_message(0,0,result) + + # 恢复数据库 + def restore_db_backup(self,args): + """ + @name 恢复站点文件 + @author zhwen + @parma file_name 备份得文件名 /www/backup/database/db_test_com_20200817_112722.sql.gz|Google Drive|db_test_com_20200817_112722.sql.gz + @parma obj_name 数据库名 + """ + if "|" not in args.file: + return public.returnMsg(True,'success') + try: + backup_info = args.file.split('|') + args.file_name = backup_info[-1] + args.obj_name = args.name + backup_method = backup_info[1] + self._progress_rewrite('','w') + self._progress_rewrite('Restoring database...') + self._progress_rewrite('Get the backup method: {}'.format(str(backup_method))) + if backup_method == 'aws_s3': + self._download_aws_file(args,'database') + elif backup_method == 'Google Cloud': + self._download_google_cloud_file(args,'database') + elif backup_method == 'Google Drive': + self._download_google_drive_file(args) + else: + return public.returnMsg(False,'Currently only supports restoring local, Google storage and AWS S3 backups') + public.ExecShell('mv {} {}/database'.format(self._local_file, self._get_local_backup_path())) + return public.returnMsg(True,'success') + except: + return public.returnMsg(False,"Download error!") diff --git a/class_v2/panel_site_v2.py b/class_v2/panel_site_v2.py new file mode 100644 index 00000000..da7794c5 --- /dev/null +++ b/class_v2/panel_site_v2.py @@ -0,0 +1,7633 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------ +# 网站管理类 +# ------------------------------ +import io, re, public, os, sys, shutil, json, hashlib, socket, time + +try: + import OpenSSL +except: + os.system("btpip install pyOpenSSL -I") + import OpenSSL +import base64 + +try: + from BTPanel import session +except: + pass +from panel_redirect_v2 import panelRedirect +import site_dir_auth_v2 as site_dir_auth +from public.validate import Param + + +class panelSite(panelRedirect): + siteName = None # 网站名称 + sitePath = None # 根目录 + sitePort = None # 端口 + phpVersion = None # PHP版本 + setupPath = None # 安装路径 + isWriteLogs = None # 是否写日志 + nginx_conf_bak = '/tmp/backup_nginx.conf' + apache_conf_bak = '/tmp/backup_apache.conf' + is_ipv6 = False + conf_dir = '{}/vhost/config'.format(public.get_panel_path()) # 防盗链配置 + + def __init__(self): + self.setupPath = public.get_setup_path() + path = self.setupPath + '/panel/vhost/nginx' + if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) + path = self.setupPath + '/panel/vhost/apache' + if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) + path = self.setupPath + '/panel/vhost/rewrite' + if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) + path = self.setupPath + '/stop' + if not os.path.exists(path + '/index.html'): + public.ExecShell('mkdir -p ' + path) + public.ExecShell('wget -O ' + path + '/index.html ' + public.get_url() + '/stop_en.html &') + self.__proxyfile = '{}/data/proxyfile.json'.format(public.get_panel_path()) + self.OldConfigFile() + if os.path.exists(self.nginx_conf_bak): os.remove(self.nginx_conf_bak) + if os.path.exists(self.apache_conf_bak): os.remove(self.apache_conf_bak) + self.is_ipv6 = os.path.exists(self.setupPath + '/panel/data/ipv6.pl') + sys.setrecursionlimit(1000000) + try: + if not os.path.isdir(self.conf_dir): + os.makedirs(self.conf_dir, 0o755) + except PermissionError as e: + public.WriteLog('信息获取', "{}失败: {}".format(self.conf_dir, str(e))) + + # 默认配置文件 + def check_default(self): + nginx = self.setupPath + '/panel/vhost/nginx' + httpd = self.setupPath + '/panel/vhost/apache' + httpd_default = ''' + ServerAdmin webmaster@example.com + DocumentRoot "/www/server/apache/htdocs" + ServerName bt.default.com + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + Order allow,deny + Allow from all + DirectoryIndex index.html + +''' + + listen_ipv6 = '' + if self.is_ipv6: listen_ipv6 = "\n listen [::]:80;" + nginx_default = '''server +{ + listen 80;%s + server_name _; + index index.html; + root /www/server/nginx/html; +}''' % listen_ipv6 + if not os.path.exists(httpd + '/0.default.conf') and not os.path.exists( + httpd + '/default.conf'): public.writeFile(httpd + '/0.default.conf', httpd_default) + if not os.path.exists(nginx + '/0.default.conf') and not os.path.exists( + nginx + '/default.conf'): public.writeFile(nginx + '/0.default.conf', nginx_default) + + # 添加apache端口 + def apacheAddPort(self, port): + port = str(port) + filename = self.setupPath + '/apache/conf/extra/httpd-ssl.conf' + if os.path.exists(filename): + ssl_conf = public.readFile(filename) + if ssl_conf: + if ssl_conf.find('Listen 443') != -1: + ssl_conf = ssl_conf.replace('Listen 443', '') + public.writeFile(filename, ssl_conf) + + filename = self.setupPath + '/apache/conf/httpd.conf' + if not os.path.exists(filename): return + allConf = public.readFile(filename) + rep = r"Listen\s+([0-9]+)\n" + tmp = re.findall(rep, allConf) + if not tmp: return False + for key in tmp: + if key == port: return False + + listen = "\nListen " + tmp[0] + "\n" + listen_ipv6 = '' + # if self.is_ipv6: listen_ipv6 = "\nListen [::]:" + port + allConf = allConf.replace(listen, listen + "Listen " + port + listen_ipv6 + "\n") + public.writeFile(filename, allConf) + return True + + # 添加到apache + def apacheAdd(self): + import time + listen = '' + if self.sitePort != '80': self.apacheAddPort(self.sitePort) + acc = public.md5(str(time.time()))[0:8] + try: + httpdVersion = public.readFile(self.setupPath + '/apache/version.pl').strip() + except: + httpdVersion = "" + if httpdVersion == '2.2': + vName = '' + if self.sitePort != '80' and self.sitePort != '443': + vName = "NameVirtualHost *:" + self.sitePort + "\n" + phpConfig = "" + apaOpt = r"Order allow,deny\n\t\tAllow from all" + else: + vName = "" + phpConfig = ''' + #PHP + + SetHandler "proxy:%s" + + ''' % (public.get_php_proxy(self.phpVersion, 'apache'),) + apaOpt = 'Require all granted' + + conf = r'''%s + ServerAdmin webmaster@example.com + DocumentRoot "%s" + ServerName %s.%s + ServerAlias %s + #errorDocument 404 /404.html + ErrorLog "%s-error_log" + CustomLog "%s-access_log" combined + + #DENY FILES + + Order allow,deny + Deny from all + + %s + #PATH + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + %s + DirectoryIndex index.php index.html index.htm default.php default.html default.htm + +''' % (vName, self.sitePort, self.sitePath, acc, self.siteName, self.siteName, + public.GetConfigValue('logs_path') + '/' + self.siteName, + public.GetConfigValue('logs_path') + '/' + self.siteName, phpConfig, self.sitePath, apaOpt) + + htaccess = self.sitePath + '/.htaccess' + if not os.path.exists(htaccess): public.writeFile(htaccess, ' ') + public.ExecShell('chmod -R 644 ' + htaccess) + public.ExecShell('chown -R www:www ' + htaccess) + + filename = self.setupPath + '/panel/vhost/apache/' + self.siteName + '.conf' + public.writeFile(filename, conf) + return True + + # 添加到nginx + def nginxAdd(self): + listen_ipv6 = '' + if self.is_ipv6: listen_ipv6 = "\n listen [::]:%s;" % self.sitePort + + conf = r'''server +{{ + listen {listen_port};{listen_ipv6} + server_name {site_name}; + index index.php index.html index.htm default.php default.htm default.html; + root {site_path}; + + #SSL-START {ssl_start_msg} + #error_page 404/404.html; + #SSL-END + + #ERROR-PAGE-START {err_page_msg} + error_page 404 /404.html; + error_page 502 /502.html; + #ERROR-PAGE-END + + #PHP-INFO-START {php_info_start} + include enable-php-{php_version}.conf; + #PHP-INFO-END + + #REWRITE-START {rewrite_start_msg} + include {setup_path}/panel/vhost/rewrite/{site_name}.conf; + #REWRITE-END + + {description} + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + {{ + return 404; + }} + + {description1} + location ~ \.well-known{{ + allow all; + }} + + #Prohibit putting sensitive files in certificate verification directory + if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {{ + return 403; + }} + + location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ + {{ + expires 30d; + error_log /dev/null; + access_log /dev/null; + }} + + location ~ .*\.(js|css)?$ + {{ + expires 12h; + error_log /dev/null; + access_log /dev/null; + }} + access_log {log_path}/{site_name}.log; + error_log {log_path}/{site_name}.error.log; +}}'''.format( + listen_port=self.sitePort, + listen_ipv6=listen_ipv6, + site_path=self.sitePath, + ssl_start_msg=public.get_msg_gettext( + 'SSL related configuration, do NOT delete or modify the next line of commented-out 404 rules'), + err_page_msg=public.get_msg_gettext( + 'Error page configuration, allowed to be commented, deleted or modified'), + php_info_start=public.get_msg_gettext( + 'PHP reference configuration, allowed to be commented, deleted or modified'), + php_version=self.phpVersion, + setup_path=self.setupPath, + rewrite_start_msg=public.get_msg_gettext( + 'URL rewrite rule reference, any modification will invalidate the rewrite rules set by the panel'), + description=public.get_msg_gettext('# Forbidden files or directories'), + description1=public.get_msg_gettext( + '# Directory verification related settings for one-click application for SSL certificate'), + log_path=public.GetConfigValue('logs_path'), + site_name=self.siteName + ) + + # 写配置文件 + filename = self.setupPath + '/panel/vhost/nginx/' + self.siteName + '.conf' + public.writeFile(filename, conf) + + # 生成伪静态文件 + urlrewritePath = self.setupPath + '/panel/vhost/rewrite' + urlrewriteFile = urlrewritePath + '/' + self.siteName + '.conf' + if not os.path.exists(urlrewritePath): os.makedirs(urlrewritePath) + open(urlrewriteFile, 'w+').close() + if not os.path.exists(urlrewritePath): + public.writeFile(urlrewritePath, '') + return True + + # 重新生成nginx配置文件 + def rep_site_config(self, get): + self.siteName = get.siteName + siteInfo = public.M('sites').where('name=?', (self.siteName,)).field('id,path,port').find() + siteInfo['domains'] = public.M('domains').where('pid=?', (siteInfo['id'],)).field('name,port').select() + siteInfo['binding'] = public.M('binding').where('pid=?', (siteInfo['id'],)).field('domain,path').select() + + # openlitespeed + def openlitespeed_add_site(self, get, init_args=None): + # 写主配置httpd_config.conf + # 操作默认监听配置 + if not self.sitePath: + return_message = public.return_msg_gettext(False, "Not specify parameter [sitePath]") + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if init_args: + self.siteName = init_args['sitename'] + self.phpVersion = init_args['phpv'] + self.sitePath = init_args['rundir'] + conf_dir = self.setupPath + '/panel/vhost/openlitespeed/' + if not os.path.exists(conf_dir): + os.makedirs(conf_dir) + file = conf_dir + self.siteName + '.conf' + + v_h = """ +#VHOST_TYPE BT_SITENAME START +virtualhost BT_SITENAME { +vhRoot BT_RUN_PATH +configFile /www/server/panel/vhost/openlitespeed/detail/BT_SITENAME.conf +allowSymbolLink 1 +enableScript 1 +restrained 1 +setUIDMode 0 +} +#VHOST_TYPE BT_SITENAME END +""" + self.old_name = self.siteName + if hasattr(get, "dirName"): + self.siteName = self.siteName + "_" + get.dirName + # sub_dir = self.sitePath + "/" + get.dirName + v_h = v_h.replace("VHOST_TYPE", "SUBDIR") + v_h = v_h.replace("BT_SITENAME", self.siteName) + v_h = v_h.replace("BT_RUN_PATH", self.sitePath) + # extp_name = self.siteName + "_" + get.dirName + else: + self.openlitespeed_domain(get) + v_h = v_h.replace("VHOST_TYPE", "VHOST") + v_h = v_h.replace("BT_SITENAME", self.siteName) + v_h = v_h.replace("BT_RUN_PATH", self.sitePath) + # extp_name = self.siteName + public.writeFile(file, v_h, "a+") + # 写vhost + conf = '''docRoot $VH_ROOT +vhDomain $VH_NAME +adminEmails example@example.com +enableGzip 1 +enableIpGeo 1 + +index { + useServer 0 + indexFiles index.php,index.html +} + +errorlog /www/wwwlogs/$VH_NAME_ols.error_log { + useServer 0 + logLevel ERROR + rollingSize 10M +} + +accesslog /www/wwwlogs/$VH_NAME_ols.access_log { + useServer 0 + logFormat '%{X-Forwarded-For}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"' + logHeaders 5 + rollingSize 10M + keepDays 10 compressArchive 1 +} + +scripthandler { + add lsapi:BT_EXTP_NAME php +} + +extprocessor BTSITENAME { + type lsapi + address UDS://tmp/lshttpd/BT_EXTP_NAME.sock + maxConns 20 + env LSAPI_CHILDREN=20 + initTimeout 600 + retryTimeout 0 + persistConn 1 + pcKeepAliveTimeout 1 + respBuffer 0 + autoStart 1 + path /usr/local/lsws/lsphpBTPHPV/bin/lsphp + extUser www + extGroup www + memSoftLimit 2047M + memHardLimit 2047M + procSoftLimit 400 + procHardLimit 500 +} + +phpIniOverride { +php_admin_value open_basedir "/tmp/:BT_RUN_PATH" +} + +expires { + enableExpires 1 + expiresByType image/*=A43200,text/css=A43200,application/x-javascript=A43200,application/javascript=A43200,font/*=A43200,application/x-font-ttf=A43200 +} + +rewrite { + enable 1 + autoLoadHtaccess 1 + include /www/server/panel/vhost/openlitespeed/proxy/BTSITENAME/urlrewrite/*.conf + include /www/server/panel/vhost/apache/redirect/BTSITENAME/*.conf + include /www/server/panel/vhost/openlitespeed/redirect/BTSITENAME/*.conf +} +include /www/server/panel/vhost/openlitespeed/proxy/BTSITENAME/*.conf +''' + open_base_path = self.sitePath + if self.sitePath[-1] != '/': + open_base_path = self.sitePath + '/' + conf = conf.replace('BT_RUN_PATH', open_base_path) + conf = conf.replace('BT_EXTP_NAME', self.siteName) + conf = conf.replace('BTPHPV', self.phpVersion) + conf = conf.replace('BTSITENAME', self.siteName) + + # 写配置文件 + conf_dir = self.setupPath + '/panel/vhost/openlitespeed/detail/' + if not os.path.exists(conf_dir): + os.makedirs(conf_dir) + file = conf_dir + self.siteName + '.conf' + # if hasattr(get,"dirName"): + # file = conf_dir + self.siteName +'_'+get.dirName+ '.conf' + public.writeFile(file, conf) + + # 生成伪静态文件 + # urlrewritePath = self.setupPath + '/panel/vhost/rewrite' + # urlrewriteFile = urlrewritePath + '/' + self.siteName + '.conf' + # if not os.path.exists(urlrewritePath): os.makedirs(urlrewritePath) + # open(urlrewriteFile, 'w+').close() + return public.return_message(0, 0, True) + + # 上传CSV文件 + # def upload_csv(self, get): + # import files + # f = files.files() + # get.f_path = '/tmp/multiple_website.csv' + # result = f.upload(get) + # return result + + # 处理CSV内容 + def __process_cvs(self, key): + import csv + with open('/tmp/multiple_website.csv') as f: + f_csv = csv.reader(f) + # result = [i for i in f_csv] + return [dict(zip(key, i)) for i in [i for i in f_csv if "FTP" not in i]] + + # 批量创建网站 + def __create_website_mulitiple(self, websites_info, site_path, get): + create_successfully = {} + create_failed = {} + for data in websites_info: + if not data: + continue + try: + domains = data['website'].split(',') + website_name = domains[0].split(':')[0] + data['port'] = '80' if len(domains[0].split(':')) < 2 else domains[0].split(':')[1] + get.webname = json.dumps({"domain": website_name, "domainlist": domains[1:], "count": 0}) + get.path = data['path'] if 'path' in data and data['path'] != '0' and data[ + 'path'] != '1' else site_path + '/' + website_name + get.version = data['version'] if 'version' in data and data['version'] != '0' else '00' + get.ftp = 'true' if 'ftp' in data and data['ftp'] == '1' else 'false' + get.sql = 'true' if 'sql' in data and data['sql'] == '1' else 'false' + get.port = data['port'] if 'port' in data else '80' + get.codeing = 'utf8' + get.type = 'PHP' + get.type_id = '0' + get.ps = '' + create_other = {} + create_other['db_status'] = False + create_other['ftp_status'] = False + if get.sql == 'true': + create_other['db_pass'] = get.datapassword = public.gen_password(16) + create_other['db_user'] = get.datauser = website_name.replace('.', '_') + create_other['db_status'] = True + if get.ftp == 'true': + create_other['ftp_pass'] = get.ftp_password = public.gen_password(16) + create_other['ftp_user'] = get.ftp_username = website_name.replace('.', '_') + create_other['ftp_status'] = True + result = self.AddSite(get, multiple=1) + if result['status'] == -1: + create_failed[domains[0]] = result['message'] + continue + create_successfully[domains[0]] = create_other + except: + create_failed[domains[0]] = public.get_msg_gettext('There was an error creating, please try again.') + return_message = { + 'msg': public.get_msg_gettext('Create the website [ {} ] successfully', (','.join(create_successfully),)), + 'error': create_failed, + 'success': create_successfully} + return public.return_message(0, 0, return_message) + + # 批量创建网站 + def create_website_multiple(self, get): + ''' + @name 批量创建网站 + @author zhwen<2020-11-26> + @param create_type txt/csv txt格式为 “网站名|网站路径|是否创建FTP|是否创建数据库|PHP版本” 每个网站一行 + "aaa.com:88,bbb.com|/www/wwwserver/aaa.com/或1|1/0|1/0|0/73" + csv格式为 “网站名|网站端口|网站路径|PHP版本|是否创建数据库|是否创建FTP” + @param websites_content "[[aaa.com|80|/www/wwwserver/aaa.com/|1|1|73]...." + ''' + # 校验参数 + try: + get.validate([ + Param('create_type').String(), + Param('websites_content').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + key = ['website', 'path', 'ftp', 'sql', 'version'] + site_path = public.M('config').getField('sites_path') + if get.create_type == 'txt': + websites_info = [dict(zip(key, i)) for i in + [i.strip().split('|') for i in json.loads(get.websites_content)]] + else: + websites_info = self.__process_cvs(key) + res = self.__create_website_mulitiple(websites_info, site_path, get) + public.serviceReload() + return res + + # 检测enable-php-00.conf + def check_php_conf(self): + try: + file = '/www/server/nginx/conf/enable-php.conf' + if public.get_webserver() != "nginx": + return + if os.path.exists(file): + return + php_v = os.listdir('/www/server/php') + if not php_v: + return + conf = public.readFile('/www/server/nginx/conf/enable-php-{}.conf'.format(php_v[0])) + public.writeFile(file, conf) + except: + pass + + # 添加站点 + def AddSite(self, get, multiple=None): + # 校验参数 + try: + get.validate([ + Param('webname').String(), + Param('type').String(), + Param('ps').String(), + Param('path').String(), + Param('version').String(), + Param('sql').String(), + Param('datapassword').String(), + Param('codeing').String(), + Param('port').Integer(), + Param('type_id').Integer(), + Param('set_ssl').Integer(), + Param('force_ssl').Integer(), + Param('ftp').Bool(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if get.get('ftp', False): + # 校验参数 + try: + get.validate([ + Param('ftp_username').String(), + Param('ftp_password').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not get.path: + return_message = public.return_msg_gettext(False, "Please fill in the website path") + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if get.path == "/": + return_message = public.return_msg_gettext(False, "The website path cannot be the root directory [/]") + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + rep_email = r"[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?" + if hasattr(get, 'email'): + if not re.search(rep_email, get.email): + return_message = public.return_msg_gettext(False, "Please check if the [Email] format correct") + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if hasattr(get, 'password') and hasattr(get, 'pw_weak'): + l = public.check_password(get.password) + if l == 0 and get.pw_weak == 'off': + return_message = public.return_msg_gettext(False, + 'Password very weak, if you are sure to use it, please tick [ Allow weak passwords ]') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + # 判断Mysql PHP 没有安装不能继续 + if not os.path.exists("/www/server/mysql") or not os.path.exists("/www/server/php"): + return_message = public.return_msg_gettext(False, + 'Please install Mysql and PHP first!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + self.check_default() + + self.check_php_conf() + + isError = public.checkWebConfig() + if isError != True: + return_message = public.return_msg_gettext(False, + 'ERROR: %s

                                    ' % public.get_msg_gettext( + 'An error was detected in the configuration file. Please solve it before proceeding') + isError.replace( + "\n", '
                                    ') + '
                                    ') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + import json, files + + get.path = self.__get_site_format_path(get.path) + if not public.check_site_path(get.path): + a, c = public.get_sys_path() + return_message = public.return_msg_gettext(False, + 'Please do not set the website root directory to the system main directory:
                                    {}'.format( + "
                                    ".join(a + c))) + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + try: + siteMenu = json.loads(get.webname) + except: + return_message = public.return_msg_gettext(False, + 'The format of the webname parameter is incorrect, it should be a parseable JSON string') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + self.siteName = self.ToPunycode(siteMenu['domain'].strip().split(':')[0]).strip().lower() + self.sitePath = self.ToPunycodePath(self.GetPath(get.path.replace(' ', ''))).strip() + self.sitePort = get.port.strip().replace(' ', '') + + if self.sitePort == "": get.port = "80" + if not public.checkPort(self.sitePort): + return_message = public.return_msg_gettext(False, 'Port range is incorrect! should be between 100-65535') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + for domain in siteMenu['domainlist']: + if not len(domain.split(':')) == 2: + continue + if not public.checkPort(domain.split(':')[1]): + return_message = public.return_msg_gettext(False, + 'Port range is incorrect! should be between 100-65535') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + if hasattr(get, 'version'): + self.phpVersion = get.version.replace(' ', '') + else: + self.phpVersion = '00' + + if not self.phpVersion: self.phpVersion = '00' + + php_version = self.GetPHPVersion(get, False) + is_phpv = False + for php_v in php_version: + if self.phpVersion == php_v['version']: + is_phpv = True + break + if not is_phpv: + return_message = public.return_msg_gettext(False, 'Requested PHP version does NOT exist!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + domain = None + # if siteMenu['count']: + # domain = get.domain.replace(' ','') + # 表单验证 + if not self.__check_site_path(self.sitePath): + return_message = public.return_msg_gettext(False, + 'System critical directory cannot be used as site directory') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if len(self.phpVersion) < 2: + return_message = public.return_msg_gettext(False, 'PHP version cannot be empty') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + reg = r"^([\w\-\*]{1,100}\.){1,24}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$" + if not re.match(reg, self.siteName): + return_message = public.return_msg_gettext(False, 'Format of primary domain is incorrect') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if self.siteName.find('*') != -1: + return_message = public.return_msg_gettext(False, 'Primary domain cannot be wildcard DNS record') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if self.sitePath[-1] == '.': + return_message = public.return_msg_gettext(False, 'DIR_END_WITH', ("'.'",)) + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + if not domain: domain = self.siteName + + # 是否重复 + sql = public.M('sites') + if sql.where("name=?", (self.siteName,)).count(): + return_message = public.return_msg_gettext(False, 'The site you tried to add already exists!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + opid = public.M('domain').where("name=?", (self.siteName,)).getField('pid') + + if opid: + if public.M('sites').where('id=?', (opid,)).count(): + return_message = public.return_msg_gettext(False, 'The domain you tried to add already exists!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + public.M('domain').where('pid=?', (opid,)).delete() + + if public.M('binding').where('domain=?', (self.siteName,)).count(): + return_message = public.return_msg_gettext(False, 'The domain you tried to add already exists!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + # 创建根目录 + if not os.path.exists(self.sitePath): + try: + os.makedirs(self.sitePath) + except Exception as ex: + return_message = public.return_msg_gettext(False, 'Failed to create site document root, {}', (ex,)) + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + public.ExecShell('chmod -R 755 ' + self.sitePath) + public.ExecShell('chown -R www:www ' + self.sitePath) + + # 创建basedir + self.DelUserInI(self.sitePath) + userIni = self.sitePath + '/.user.ini' + if not os.path.exists(userIni): + public.writeFile(userIni, 'open_basedir=' + self.sitePath + '/:/tmp/') + public.ExecShell('chmod 644 ' + userIni) + public.ExecShell('chown root:root ' + userIni) + public.ExecShell('chattr +i ' + userIni) + + ngx_open_basedir_path = self.setupPath + '/panel/vhost/open_basedir/nginx' + if not os.path.exists(ngx_open_basedir_path): + os.makedirs(ngx_open_basedir_path, 384) + ngx_open_basedir_file = ngx_open_basedir_path + '/{}.conf'.format(self.siteName) + ngx_open_basedir_body = '''set $bt_safe_dir "open_basedir"; +set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) + public.writeFile(ngx_open_basedir_file, ngx_open_basedir_body) + + # 创建默认文档 + index = self.sitePath + '/index.html' + if not os.path.exists(index): + public.writeFile(index, public.readFile('data/defaultDoc.html')) + public.ExecShell('chmod -R 644 ' + index) + public.ExecShell('chown -R www:www ' + index) + + # 创建自定义404页 + doc404 = self.sitePath + '/404.html' + if not os.path.exists(doc404): + public.writeFile(doc404, public.readFile('data/404.html')) + public.ExecShell('chmod -R 644 ' + doc404) + public.ExecShell('chown -R www:www ' + doc404) + # 创建自定义502页面 + doc502 = self.sitePath + '/502.html' + if not os.path.exists(doc502) and os.path.exists('data/502.html'): + public.writeFile(doc502, public.readFile('data/502.html')) + public.ExecShell('chmod -R 644 ' + doc502) + public.ExecShell('chown -R www:www ' + doc502) + + # 写入配置 + result = self.nginxAdd() + result = self.apacheAdd() + result = self.openlitespeed_add_site(get) + + # 检查处理结果 + if not result: + return_message = public.return_msg_gettext(False, 'Failed to add, write configuraton ERROR!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + ps = public.xssencode2(get.ps) + # 添加放行端口 + if self.sitePort != '80': + import firewalls + get.port = self.sitePort + get.ps = self.siteName + firewalls.firewalls().AddAcceptPort(get) + + if not hasattr(get, 'type_id'): get.type_id = 0 + if not hasattr(get, 'project_type'): get.project_type = "PHP" + public.check_domain_cloud(self.siteName) + # 统计wordpress安装次数 + if get.project_type == 'WP': + public.count_wp() + # 写入数据库 + get.pid = sql.table('sites').add('name,path,status,ps,type_id,addtime,project_type', ( + self.siteName, self.sitePath, '1', ps, get.type_id, public.getDate(), get.project_type)) + + # 添加更多域名 + for domain in siteMenu['domainlist']: + get.domain = domain + get.webname = self.siteName + get.id = str(get.pid) + self.AddDomain(get, multiple) + + sql.table('domain').add('pid,name,port,addtime', (get.pid, self.siteName, self.sitePort, public.getDate())) + + data = {} + data['siteStatus'] = True + data['siteId'] = get.pid + + # 添加FTP + data['ftpStatus'] = False + if 'ftp' not in get: + get.ftp = False + if get.ftp == 'true': + import ftp + get.ps = self.siteName + result = ftp.ftp().AddUser(get) + if result['status']: + data['ftpStatus'] = True + data['ftpUser'] = get.ftp_username + data['ftpPass'] = get.ftp_password + + # 添加数据库 + data['databaseStatus'] = False + if 'sql' not in get: + get.sql = 'false' + if get.sql == 'true' or get.sql == 'MySQL': + import database + if len(get.datauser) > 16: get.datauser = get.datauser[:16] + get.name = get.datauser + get.db_user = get.datauser + get.password = get.datapassword + get.address = '127.0.0.1' + get.ps = self.siteName + result = database.database().AddDatabase(get) + + public.print_log(result) + + if result['status']: + data['databaseStatus'] = True + data['databaseUser'] = get.datauser + data['databasePass'] = get.datapassword + data['d_id'] = str(public.M('databases').where('pid=?', (get.pid,)).field('id').find()['id']) + if not multiple: + public.serviceReload() + data = self._set_ssl(get, data, siteMenu) + data = self._set_redirect(get, data['message']) + public.write_log_gettext('Site manager', 'Successfully added site [{}]!', (self.siteName,)) + return data + + # 添加WP站点 + def AddWPSite(self, args: public.dict_obj): + # 参数验证 + try: + args.validate([ + Param('webname').String(), + Param('type').String(), + Param('ps').String(), + Param('path').String(), + Param('version').String(), + Param('sql').String(), + Param('datauser').String(), + Param('datapassword').String(), + Param('codeing').String(), + Param('port').Integer(), + Param('type_id').Integer(), + Param('set_ssl').Integer(), + Param('force_ssl').Integer(), + Param('ftp').Bool(), + Param('weblog_title').Require().Xss(), + Param('language').Require(), + Param('user_name').Require().Xss(), + Param('email').Require().Email(), + Param('pw_weak').Require().String('in', ['on', 'off']), + Param('password').Require(), + Param('prefix').Require().Xss(), + Param('enable_cache').Require().Integer(), + Param('enable_whl').Integer(), + Param('whl_page').SafePath(), + Param('whl_redirect_admin').SafePath(), + Param('package_version').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + from copy import deepcopy + args_dup = public.to_dict_obj(deepcopy(args.get_items())) + + data = self.AddSite(args_dup) + + if int(data.get('status', 0)) != 0: + return data + + data = data.get('message', {}) + + if int(data.get('databaseStatus', 0)) != 1: + raise public.HintException(public.get_msg_gettext('Database creation failed. Please check mysql running status and try again.')) + + return self.deploy_wp(public.to_dict_obj({ + 'domain': json.loads(args.webname).get('domain', ''), + 'weblog_title': args.weblog_title, + 'language': args.get('language', ''), + 'php_version': args.version, + 'user_name': args.user_name, + 'admin_password': args.password, + 'pw_weak': args.pw_weak, + 'admin_email': args.email, + 'prefix': args.prefix, + 'enable_cache': args.enable_cache, + 'd_id': data.get('d_id', 0), + 's_id': data.get('siteId', 0), + 'enable_whl': args.get('enable_whl', 0), + 'whl_page': args.get('whl_page', 'login'), + 'whl_redirect_admin': args.get('whl_redirect_admin', '404'), + 'package_version': args.get('package_version', None), + })) + + def _set_redirect(self, get, data): + try: + if not hasattr(get, 'redirect') and not get.redirect: + data['redirect'] = False + return public.return_message(0, 0, data) + import panel_redirect_v2 as panelRedirect + get.redirectdomain = json.dumps([get.redirect]) + get.sitename = get.webname + get.redirectname = 'Default' + get.redirecttype = '301' + get.holdpath = '1' + get.type = '1' + get.domainorpath = 'domain' + get.redirectpath = '' + if data['ssl']: + get.tourl = 'https://{}'.format(get.tourl) + else: + get.tourl = 'http://{}'.format(get.tourl) + panelRedirect.panelRedirect().CreateRedirect(get) + data['redirect'] = True + except: + data['redirect'] = str(public.get_error_info()) + data['redirect'] = True + return public.return_message(0, 0, data) + + def _set_ssl(self, get, data, siteMenu): + try: + if get.set_ssl != '1': + data['ssl'] = False + return public.return_message(0, 0, data) + import acme_v2 + ssl_domain = siteMenu['domainlist'] + ssl_domain.append(self.siteName) + get.id = str(get.pid) + get.auth_to = str(get.pid) + get.auth_type = 'http' + get.auto_wildcard = '' + get.domains = json.dumps(ssl_domain) + result = acme_v2.acme_v2().apply_cert_api(get) + get.type = '1' + get.siteName = self.siteName + get.key = result['private_key'] + get.csr = result['cert'] + result['root'] + self.SetSSL(get) + data['ssl'] = True + if hasattr(get, 'force_ssl') and get.force_ssl == '1': + get.siteName = self.siteName + self.HttpToHttps(get) + except: + data['ssl'] = str(public.get_error_info()) + return public.return_message(0, 0, data) + + def __get_site_format_path(self, path): + path = path.replace('//', '/') + if path[-1:] == '/': + path = path[:-1] + return path + + def __check_site_path(self, path): + path = self.__get_site_format_path(path) + other_path = public.M('config').where("id=?", ('1',)).field('sites_path,backup_path').find() + if path == other_path['sites_path'] or path == other_path['backup_path']: return False + return True + + def delete_website_multiple(self, get): + ''' + @name 批量删除网站 + @author zhwen<2020-11-17> + @param sites_id "1,2" + @param ftp 0/1 + @param database 0/1 + @param path 0/1 + ''' + sites_id = get.sites_id.split(',') + del_successfully = [] + del_failed = {} + for site_id in sites_id: + get.id = site_id + get.webname = public.M('sites').where("id=?", (site_id,)).getField('name') + if not get.webname: + continue + try: + self.DeleteSite(get, multiple=1) + del_successfully.append(get.webname) + except: + del_failed[get.webname] = public.get_msg_gettext('There was an error deleting, please try again.') + pass + public.serviceReload() + return_message = { + 'msg': public.get_msg_gettext('Delete website [{}] successfully', (','.join(del_successfully),)), + 'error': del_failed, + 'success': del_successfully} + + return public.return_message(0, 0, return_message) + + # 删除站点 + def DeleteSite(self, get, multiple=None): + # 校验参数 + try: + get.validate([ + Param('webname').String(), + Param('id').Integer(), + Param('ftp').Integer(), + Param('database').Integer(), + Param('path').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + proxyconf = self.__read_config(self.__proxyfile) + id = get.id + if public.M('sites').where('id=?', (id,)).count() < 1: + return_message = public.return_msg_gettext(False, 'Specified site does NOT exist') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + siteName = get.webname + get.siteName = siteName + self.CloseTomcat(get) + # 删除反向代理 + for i in range(len(proxyconf) - 1, -1, -1): + if proxyconf[i]["sitename"] == siteName: + del proxyconf[i] + self.__write_config(self.__proxyfile, proxyconf) + + m_path = self.setupPath + '/panel/vhost/nginx/proxy/' + siteName + if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) + + m_path = self.setupPath + '/panel/vhost/apache/proxy/' + siteName + if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) + + # 删除目录保护 + _dir_aith_file = "%s/panel/data/site_dir_auth.json" % self.setupPath + _dir_aith_conf = public.readFile(_dir_aith_file) + if _dir_aith_conf: + try: + _dir_aith_conf = json.loads(_dir_aith_conf) + if siteName in _dir_aith_conf: + del (_dir_aith_conf[siteName]) + except: + pass + self.__write_config(_dir_aith_file, _dir_aith_conf) + + dir_aith_path = self.setupPath + '/panel/vhost/nginx/dir_auth/' + siteName + if os.path.exists(dir_aith_path): public.ExecShell("rm -rf %s" % dir_aith_path) + + dir_aith_path = self.setupPath + '/panel/vhost/apache/dir_auth/' + siteName + if os.path.exists(dir_aith_path): public.ExecShell("rm -rf %s" % dir_aith_path) + + # 删除重定向 + __redirectfile = "%s/panel/data/redirect.conf" % self.setupPath + redirectconf = self.__read_config(__redirectfile) + for i in range(len(redirectconf) - 1, -1, -1): + if redirectconf[i]["sitename"] == siteName: + del redirectconf[i] + self.__write_config(__redirectfile, redirectconf) + m_path = self.setupPath + '/panel/vhost/nginx/redirect/' + siteName + if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) + m_path = self.setupPath + '/panel/vhost/apache/redirect/' + siteName + if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) + + # 删除配置文件 + confPath = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(confPath): os.remove(confPath) + + confPath = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if os.path.exists(confPath): os.remove(confPath) + open_basedir_file = self.setupPath + '/panel/vhost/open_basedir/nginx/' + siteName + '.conf' + if os.path.exists(open_basedir_file): os.remove(open_basedir_file) + + # 删除openlitespeed配置 + vhost_file = "/www/server/panel/vhost/openlitespeed/{}.conf".format(siteName) + if os.path.exists(vhost_file): + public.ExecShell('rm -f {}*'.format(vhost_file)) + vhost_detail_file = "/www/server/panel/vhost/openlitespeed/detail/{}.conf".format(siteName) + if os.path.exists(vhost_detail_file): + public.ExecShell('rm -f {}*'.format(vhost_detail_file)) + vhost_ssl_file = "/www/server/panel/vhost/openlitespeed/detail/ssl/{}.conf".format(siteName) + if os.path.exists(vhost_ssl_file): + public.ExecShell('rm -f {}*'.format(vhost_ssl_file)) + vhost_sub_file = "/www/server/panel/vhost/openlitespeed/detail/{}_sub.conf".format(siteName) + if os.path.exists(vhost_sub_file): + public.ExecShell('rm -f {}*'.format(vhost_sub_file)) + vhost_redirect_file = "/www/server/panel/vhost/openlitespeed/redirect/{}".format(siteName) + if os.path.exists(vhost_redirect_file): + public.ExecShell('rm -rf {}*'.format(vhost_redirect_file)) + vhost_proxy_file = "/www/server/panel/vhost/openlitespeed/proxy/{}".format(siteName) + if os.path.exists(vhost_proxy_file): + public.ExecShell('rm -rf {}*'.format(vhost_proxy_file)) + + # 删除openlitespeed监听配置 + self._del_ols_listen_conf(siteName) + + # 删除伪静态文件 + # filename = confPath+'/rewrite/'+siteName+'.conf' + filename = '/www/server/panel/vhost/rewrite/' + siteName + '.conf' + if os.path.exists(filename): + os.remove(filename) + public.ExecShell("rm -f " + confPath + '/rewrite/' + siteName + "_*") + + # 删除日志文件 + filename = public.GetConfigValue('logs_path') + '/' + siteName + '*' + public.ExecShell("rm -f " + filename) + + # 删除证书 + # crtPath = '/etc/letsencrypt/live/'+siteName + # if os.path.exists(crtPath): + # import shutil + # shutil.rmtree(crtPath) + + # 删除日志 + public.ExecShell("rm -f " + public.GetConfigValue('logs_path') + '/' + siteName + "-*") + + # 删除备份 + # public.ExecShell("rm -f "+session['config']['backup_path']+'/site/'+siteName+'_*') + + # 删除根目录 + if 'path' in get: + if get.path == '1': + import files_v2 as files + get.path = self.__get_site_format_path(public.M('sites').where("id=?", (id,)).getField('path')) + if self.__check_site_path(get.path): + if public.M('sites').where("path=?", (get.path,)).count() < 2: + files.files().DeleteDir(get) + get.path = '1' + + # 重载配置 + if not multiple: + public.serviceReload() + + # 从数据库删除 + public.M('sites').where("id=?", (id,)).delete() + public.M('binding').where("pid=?", (id,)).delete() + public.M('domain').where("pid=?", (id,)).delete() + public.M('wordpress_onekey').where("s_id=?", (id,)).delete() + public.write_log_gettext('Site manager', 'Successfully deleted site!', (siteName,)) + + # 是否删除关联数据库 + if hasattr(get, 'database'): + if get.database == '1': + find = public.M('databases').where("pid=?", (id,)).field('id,name').find() + if find: + import database_v2 as database + get.name = find['name'] + get.id = find['id'] + database.database().DeleteDatabase(get) + + # 是否删除关联FTP + if hasattr(get, 'ftp'): + if get.ftp == '1': + find = public.M('ftps').where("pid=?", (id,)).field('id,name').find() + if find: + import ftp_v2 as ftp + get.username = find['name'] + get.id = find['id'] + ftp.ftp().DeleteUser(get) + return_message = public.return_msg_gettext(True, 'Successfully deleted site!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + def _del_ols_listen_conf(self, sitename): + conf_dir = '/www/server/panel/vhost/openlitespeed/listen/' + if not os.path.exists(conf_dir): + return False + for i in os.listdir(conf_dir): + file_name = conf_dir + i + if os.path.isdir(file_name): + continue + conf = public.readFile(file_name) + if not conf: + continue + map_rep = r'map\s+{}.*'.format(sitename) + conf = re.sub(map_rep, '', conf) + if "map" not in conf: + public.ExecShell('rm -f {}*'.format(file_name)) + continue + public.writeFile(file_name, conf) + + # 域名编码转换 + def ToPunycode(self, domain): + import re + if sys.version_info[0] == 2: domain = domain.encode('utf8') + tmp = domain.split('.') + newdomain = '' + for dkey in tmp: + if dkey == '*': continue + # 匹配非ascii字符 + match = re.search(u"[\x80-\xff]+", dkey) + if not match: match = re.search(u"[\u4e00-\u9fa5]+", dkey) + if not match: + newdomain += dkey + '.' + else: + if sys.version_info[0] == 2: + newdomain += 'xn--' + dkey.decode('utf-8').encode('punycode') + '.' + else: + newdomain += 'xn--' + dkey.encode('punycode').decode('utf-8') + '.' + if tmp[0] == '*': newdomain = "*." + newdomain + return newdomain[0:-1] + + # 中文路径处理 + def ToPunycodePath(self, path): + if sys.version_info[0] == 2: path = path.encode('utf-8') + if os.path.exists(path): return path + import re + match = re.search(u"[\x80-\xff]+", path) + if not match: match = re.search(u"[\u4e00-\u9fa5]+", path) + if not match: return path + npath = '' + for ph in path.split('/'): + npath += '/' + self.ToPunycode(ph) + return npath.replace('//', '/') + + def export_domains(self, args): + ''' + @name 导出域名列表 + @author hwliang<2020-10-27> + @param args{ + siteName: string<网站名称> + } + @return string + ''' + + pid = public.M('sites').where('name=?', args.siteName).getField('id') + domains = public.M('domain').where('pid=?', pid).field('name,port').select() + text_data = [] + for domain in domains: + text_data.append("{}:{}".format(domain['name'], domain['port'])) + data = "\n".join(text_data) + return public.send_file(data, '{}_domains'.format(args.siteName)) + + def import_domains(self, args): + ''' + @name 导入域名 + @author hwliang<2020-10-27> + @param args{ + siteName: string<网站名称> + domains: string<域名列表> 每行一个 格式: 域名:端口 + } + @return string + ''' + + domains_tmp = args.domains.split("\n") + get = public.dict_obj() + get.webname = args.siteName + get.id = public.M('sites').where('name=?', args.siteName).getField('id') + domains = [] + for domain in domains_tmp: + if public.M('domain').where('name=?', domain.split(':')[0]).count(): + continue + domains.append(domain) + + get.domain = ','.join(domains) + return self.AddDomain(get) + + # 添加域名 + + def AddDomain(self, get, multiple=None): + # 校验参数 + try: + get.validate([ + Param('webname').String(), + Param('domain').String(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # 检查配置文件 + isError = public.checkWebConfig() + if isError != True: + return public.return_message(-1, 0, 'ERROR: %s

                                    ' % public.get_msg_gettext( + 'An error was detected in the configuration file. Please solve it before proceeding') + isError.replace( + "\n", '
                                    ') + '
                                    ') + + if not 'domain' in get: return public.return_message(-1, 0, 'Please enter the host domain name') + if len(get.domain) < 3: return public.return_message(-1, 0, 'Domain cannot be empty!') + domains = get.domain.replace(' ', '').split(',') + + for domain in domains: + if domain == "": continue + domain = domain.strip().split(':') + get.domain = self.ToPunycode(domain[0]).lower() + get.port = '80' + # 判断通配符域名格式 + if get.domain.find('*') != -1 and get.domain.find('*.') == -1: + return public.return_message(-1, 0, 'Domain name format is incorrect!') + + # 判断域名格式 + reg = r"^([\w\-\*]{1,100}\.){1,24}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$" + if not re.match(reg, get.domain): return public.return_message(-1, 0, 'Format of domain is invalid!') + + # 获取自定义端口 + if len(domain) == 2: + get.port = domain[1] + if get.port == "": get.port = "80" + + # 判断端口是否合法 + if not public.checkPort(get.port): return public.return_message(-1, 0, + 'Port range is incorrect! should be between 100-65535') + # 检查域名是否存在 + sql = public.M('domain') + opid = sql.where("name=? AND (port=? OR pid=?)", (get.domain, get.port, get.id)).getField('pid') + if opid: + siteName = public.M('sites').where('id=?', (opid,)).getField('name') + if siteName: + return public.return_message(-1, 0, + 'The specified domain name has been bound by the website [{}]'.format( + siteName)) + sql.where('pid=?', (opid,)).delete() + opid = public.M('binding').where('domain=?', (get.domain,)).getField('pid') + if opid: + siteName = public.M('sites').where('id=?', (opid,)).getField('name') + return public.return_message(-1, 0, + 'The specified domain name has been bound by a subdirectory of the website [{}]!'.format( + siteName)) + + # 写配置文件 + self.NginxDomain(get) + try: + self.ApacheDomain(get) + self.openlitespeed_domain(get) + if self._check_ols_ssl(get.webname): + get.port = '443' + self.openlitespeed_domain(get) + get.port = '80' + except: + pass + + # 检查实际端口 + if len(domain) == 2: get.port = domain[1] + + # 添加放行端口 + if get.port != '80': + import firewalls + get.ps = get.domain + firewalls.firewalls().AddAcceptPort(get) + + # 重载webserver服务 + if not multiple: + public.serviceReload() + full_domain = get.domain + if not get.port in ['80', '443']: full_domain += ':' + get.port + public.check_domain_cloud(full_domain) + public.write_log_gettext('Site manager', 'Site [{}] added domain [{}] successfully!', + (get.webname, get.domain)) + sql.table('domain').add('pid,name,port,addtime', (get.id, get.domain, get.port, public.getDate())) + + return public.return_message(0, 0, 'Successfully added site!') + + # 判断ols_ssl是否已经设置 + def _check_ols_ssl(self, webname): + conf = public.readFile('/www/server/panel/vhost/openlitespeed/listen/443.conf') + if conf and webname in conf: + return True + return False + + # 添加openlitespeed 80端口监听 + def openlitespeed_set_80_domain(self, get, conf): + rep = r'map\s+{}.*'.format(get.webname) + domains = get.webname.strip().split(',') + if conf: + map_tmp = re.search(rep, conf) + if map_tmp: + map_tmp = map_tmp.group() + domains = map_tmp.strip().split(',') + if not public.inArray(domains, get.domain): + new_map = '{},{}'.format(conf, get.domain) + conf = re.sub(rep, new_map, conf) + else: + map_tmp = '\tmap\t{d} {d}\n'.format(d=domains[0]) + listen_rep = r"secure\s*0" + conf = re.sub(listen_rep, "secure 0\n" + map_tmp, conf) + return public.return_message(0, 0, conf) + + else: + rep_default = 'listener\\s+Default\\{(\n|[\\s\\w\\*\\:\\#\\.\\,])*' + tmp = re.search(rep_default, conf) + # domains = get.webname.strip().split(',') + if tmp: + tmp = tmp.group() + new_map = '\tmap\t{d} {d}\n'.format(d=domains[0]) + tmp += new_map + conf = re.sub(rep_default, tmp, conf) + return public.return_message(0, 0, conf) + + # openlitespeed写域名配置 + def openlitespeed_domain(self, get): + listen_dir = '/www/server/panel/vhost/openlitespeed/listen/' + if not os.path.exists(listen_dir): + os.makedirs(listen_dir) + listen_file = listen_dir + get.port + ".conf" + listen_conf = public.readFile(listen_file) + try: + get.webname = json.loads(get.webname) + get.domain = get.webname['domain'].replace('\r', '') + get.webname = get.domain + "," + ",".join(get.webname["domainlist"]) + if get.webname[-1] == ',': + get.webname = get.webname[:-1] + except: + pass + if listen_conf: + # 添加域名 + rep = r'map\s+{}.*'.format(get.webname) + map_tmp = re.search(rep, listen_conf) + if map_tmp: + map_tmp = map_tmp.group() + domains = map_tmp.strip().split(',') + if not public.inArray(domains, get.domain): + new_map = '{},{}'.format(map_tmp, get.domain) + listen_conf = re.sub(rep, new_map, listen_conf) + else: + domains = get.webname.strip().split(',') + map_tmp = '\tmap\t{d} {d}'.format(d=domains[0]) + listen_rep = r"secure\s*0" + listen_conf = re.sub(listen_rep, "secure 0\n" + map_tmp, listen_conf) + else: + listen_conf = """ +listener Default%s{ + address *:%s + secure 0 + map %s %s +} +""" % (get.port, get.port, get.webname, get.domain) + # 保存配置文件 + public.writeFile(listen_file, listen_conf) + return public.return_message(0, 0, True) + + # Nginx写域名配置 + def NginxDomain(self, get): + file = self.setupPath + '/panel/vhost/nginx/' + get.webname + '.conf' + conf = public.readFile(file) + if not conf: return public.return_message(-1, 0, 'domains file not exists:' + file) + # 添加域名 + rep = r"server_name\s*(.*);" + tmp = re.search(rep, conf).group() + domains = tmp.replace(';', '').strip().split(' ') + if not public.inArray(domains, get.domain): + newServerName = tmp.replace(';', ' ' + get.domain + ';') + conf = conf.replace(tmp, newServerName) + + # 添加端口 + rep = r"listen\s+[\[\]\:]*([0-9]+).*;" + tmp = re.findall(rep, conf) + if not public.inArray(tmp, get.port): + listen = re.search(rep, conf).group() + listen_ipv6 = '' + if self.is_ipv6: listen_ipv6 = "\n\t\tlisten [::]:" + get.port + ';' + conf = conf.replace(listen, listen + "\n\t\tlisten " + get.port + ';' + listen_ipv6) + # 保存配置文件 + public.writeFile(file, conf) + return public.return_message(0, 0, True) + + # Apache写域名配置 + def ApacheDomain(self, get): + file = self.setupPath + '/panel/vhost/apache/' + get.webname + '.conf' + conf = public.readFile(file) + if not conf: return public.return_message(-1, 0, 'domains file not exists:' + file) + + port = get.port + siteName = get.webname + newDomain = get.domain + find = public.M('sites').where("id=?", (get.id,)).field('id,name,path').find() + sitePath = find['path'] + siteIndex = 'index.php index.html index.htm default.php default.html default.htm' + + # 添加域名 + if conf.find('') != -1: + repV = r"(.|\n)*" + domainV = re.search(repV, conf).group() + rep = r"ServerAlias\s*(.*)\n" + tmp = re.search(rep, domainV).group(0) + domains = tmp.strip().split(' ') + if not public.inArray(domains, newDomain): + rs = tmp.replace("\n", "") + newServerName = rs + ' ' + newDomain + "\n" + myconf = domainV.replace(tmp, newServerName) + conf = re.sub(repV, myconf, conf) + if conf.find('') != -1: + repV = r"(.|\n)*" + domainV = re.search(repV, conf).group() + rep = r"ServerAlias\s*(.*)\n" + tmp = re.search(rep, domainV).group(0) + domains = tmp.strip().split(' ') + if not public.inArray(domains, newDomain): + rs = tmp.replace("\n", "") + newServerName = rs + ' ' + newDomain + "\n" + myconf = domainV.replace(tmp, newServerName) + conf = re.sub(repV, myconf, conf) + else: + try: + httpdVersion = public.readFile(self.setupPath + '/apache/version.pl').strip() + except: + httpdVersion = "" + if httpdVersion == '2.2': + vName = '' + if self.sitePort != '80' and self.sitePort != '443': + vName = "NameVirtualHost *:" + port + "\n" + phpConfig = "" + apaOpt = "Order allow,deny\n\t\tAllow from all" + else: + vName = "" + # rep = r"php-cgi-([0-9]{2,3})\.sock" + # version = re.search(rep,conf).groups()[0] + version = public.get_php_version_conf(conf) + if len(version) < 2: + return_message = public.return_msg_gettext(False, 'Failed to get PHP version!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + phpConfig = ''' + #PHP + + SetHandler "proxy:%s" + + ''' % (public.get_php_proxy(version, 'apache'),) + apaOpt = 'Require all granted' + + newconf = r''' + ServerAdmin webmaster@example.com + DocumentRoot "%s" + ServerName %s.%s + ServerAlias %s + #errorDocument 404 /404.html + ErrorLog "%s-error_log" + CustomLog "%s-access_log" combined + %s + + #DENY FILES + + Order allow,deny + Deny from all + + + #PATH + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + %s + DirectoryIndex %s + +''' % (port, sitePath, siteName, port, newDomain, public.GetConfigValue('logs_path') + '/' + siteName, + public.GetConfigValue('logs_path') + '/' + siteName, phpConfig, sitePath, apaOpt, siteIndex) + conf += "\n\n" + newconf + + # 添加端口 + if port != '80' and port != '888': self.apacheAddPort(port) + + # 保存配置文件 + public.writeFile(file, conf) + return public.return_message(0, 0, True) + + def delete_domain_multiple(self, get): + ''' + @name 批量删除网站 + @author zhwen<2020-11-17> + @param id "1" + @param domains_id 1,2,3 + ''' + domains_id = get.domains_id.split(',') + get.webname = public.M('sites').where("id=?", (get.id,)).getField('name') + del_successfully = [] + del_failed = {} + for domain_id in domains_id: + get.domain = public.M('domain').where("id=? and pid=?", (domain_id, get.id)).getField('name') + get.port = str(public.M('domain').where("id=? and pid=?", (domain_id, get.id)).getField('port')) + if not get.webname: + continue + try: + result = self.DelDomain(get, multiple=1) + tmp = get.domain + ':' + get.port + if result['status'] == -1: + del_failed[tmp] = result['msg'] + continue + del_successfully.append(tmp) + except: + tmp = get.domain + ':' + get.port + del_failed[tmp] = public.get_msg_gettext('There was an error deleting, please try again.') + pass + public.serviceReload() + return_message = { + 'msg': public.get_msg_gettext('Delete domain [{}] successfully', (','.join(del_successfully),)), + 'error': del_failed, + 'success': del_successfully} + return public.return_message(0, 0, return_message) + + # 删除域名 + def DelDomain(self, get, multiple=None): + # 校验参数 + try: + get.validate([ + Param('webname').String(), + Param('domain').String(), + Param('id').Integer(), + Param('port').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not 'id' in get: return public.return_message(-1, 0, 'Please choose a domain name') + if not 'port' in get: return public.return_message(-1, 0, 'Please choose a port') + sql = public.M('domain') + id = get['id'] + port = get.port + find = sql.where("pid=? AND name=?", (get.id, get.domain)).field('id,name').find() + domain_count = sql.table('domain').where("pid=?", (id,)).count() + if domain_count == 1: return public.return_message(-1, 0, 'Last domain cannot be deleted!') + + # nginx + file = self.setupPath + '/panel/vhost/nginx/' + get['webname'] + '.conf' + conf = public.readFile(file) + if conf: + # 删除域名 + rep = r"server_name\s+(.+);" + tmp = re.search(rep, conf).group() + newServerName = tmp.replace(' ' + get['domain'] + ';', ';') + newServerName = newServerName.replace(' ' + get['domain'] + ' ', ' ') + conf = conf.replace(tmp, newServerName) + + # 删除端口 + rep = r"listen.*[\s:]+(\d+).*;" + tmp = re.findall(rep, conf) + port_count = sql.table('domain').where('pid=? AND port=?', (get.id, get.port)).count() + if public.inArray(tmp, port) == True and port_count < 2: + rep = r"\n*\s+listen.*[\s:]+" + port + r"\s*;" + conf = re.sub(rep, '', conf) + # 保存配置 + public.writeFile(file, conf) + + # apache + file = self.setupPath + '/panel/vhost/apache/' + get['webname'] + '.conf' + conf = public.readFile(file) + if conf: + # 删除域名 + try: + rep = r"\n*(.|\n)*" + tmp = re.search(rep, conf).group() + + rep1 = "ServerAlias\\s+(.+)\n" + tmp1 = re.findall(rep1, tmp) + tmp2 = tmp1[0].split(' ') + if len(tmp2) < 2: + conf = re.sub(rep, '', conf) + rep = r"NameVirtualHost.+\:" + port + "\n" + conf = re.sub(rep, '', conf) + else: + newServerName = tmp.replace(' ' + get['domain'] + "\n", "\n") + newServerName = newServerName.replace(' ' + get['domain'] + ' ', ' ') + conf = conf.replace(tmp, newServerName) + # 保存配置 + public.writeFile(file, conf.strip()) + except: + pass + + # openlitespeed + self._del_ols_domain(get) + + sql.table('domain').where("id=?", (find['id'],)).delete() + public.write_log_gettext('Site manager', 'Site [{}] deleted domain [{}] successfully!', + (get.webname, get.domain)) + if not multiple: + public.serviceReload() + return public.return_message(0, 0, 'Successfully deleted') + + # openlitespeed删除域名 + def _del_ols_domain(self, get): + conf_dir = '/www/server/panel/vhost/openlitespeed/listen/' + if not os.path.exists(conf_dir): + return return_message(-1, 0, 'directory not exists:' + conf_dir) + for i in os.listdir(conf_dir): + file_name = conf_dir + i + if os.path.isdir(file_name): + continue + conf = public.readFile(file_name) + map_rep = r'map\s+{}\s+(.*)'.format(get.webname) + domains = re.search(map_rep, conf) + if domains: + domains = domains.group(1).split(',') + if get.domain in domains: + domains.remove(get.domain) + if len(domains) == 0: + os.remove(file_name) + continue + else: + domains = ",".join(domains) + map_c = "map\t{} ".format(get.webname) + domains + conf = re.sub(map_rep, map_c, conf) + public.writeFile(file_name, conf) + return public.return_message(0, 0, 'Setup successfully!') + + # 检查域名是否解析 + def CheckDomainPing(self, get): + try: + epass = public.GetRandomString(32) + spath = get.path + '/.well-known/pki-validation' + if not os.path.exists(spath): public.ExecShell("mkdir -p '" + spath + "'") + public.writeFile(spath + '/fileauth.txt', epass) + result = public.httpGet( + 'http://' + get.domain.replace('*.', '') + '/.well-known/pki-validation/fileauth.txt') + if result == epass: return public.return_message(0, 0, "") + return public.return_message(-1, 0, "") + except: + return public.return_message(-1, 0, "") + + # 保存第三方证书 + def SetSSL(self, get): + # 校验参数 + try: + get.validate([ + Param('siteName').String(), + Param('key').String(), + Param('csr').String(), + Param('type').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + siteName = get.siteName + path = '/www/server/panel/vhost/cert/' + siteName + csrpath = path + "/fullchain.pem" + keypath = path + "/privkey.pem" + + if (get.key.find('KEY') == -1): + return_message = public.return_msg_gettext(False, 'Private Key ERROR, please check!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if (get.csr.find('CERTIFICATE') == -1): + return_message = public.return_msg_gettext(False, 'Certificate ERROR, please check!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + public.writeFile('/tmp/cert.pl', get.csr) + if not public.CheckCert('/tmp/cert.pl'): + return_message = public.return_msg_gettext(False, 'Error getting certificate') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + backup_cert = '/tmp/backup_cert_' + siteName + + import shutil + if os.path.exists(backup_cert): shutil.rmtree(backup_cert) + if os.path.exists(path): shutil.move(path, backup_cert) + if os.path.exists(path): shutil.rmtree(path) + + public.ExecShell('mkdir -p ' + path) + public.writeFile(keypath, get.key) + public.writeFile(csrpath, get.csr) + + # 写入配置文件 + result = self.SetSSLConf(get) + if result['status'] == -1: return result + isError = public.checkWebConfig() + + if (type(isError) == str): + if os.path.exists(path): shutil.rmtree(backup_cert) + shutil.move(backup_cert, path) + return_message = public.return_msg_gettext(False, + 'ERROR:
                                    ' + isError.replace("\n", + '
                                    ') + '
                                    ') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + public.serviceReload() + + if os.path.exists(path + '/partnerOrderId'): os.remove(path + '/partnerOrderId') + if os.path.exists(path + '/certOrderId'): os.remove(path + '/certOrderId') + p_file = '/etc/letsencrypt/live/' + get.siteName + if os.path.exists(p_file): shutil.rmtree(p_file) + public.write_log_gettext('Site manager', 'Certificate saved!') + + # 清理备份证书 + if os.path.exists(backup_cert): shutil.rmtree(backup_cert) + return_message = public.return_msg_gettext(True, 'Certificate saved!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 获取运行目录 + def GetRunPath(self, get): + if not hasattr(get, 'id'): + if hasattr(get, 'siteName'): + get.id = public.M('sites').where('name=?', (get.siteName,)).getField('id') + else: + get.id = public.M('sites').where('path=?', (get.path,)).getField('id') + if not get.id: return public.return_message(-1, 0, "") + if type(get.id) == list: get.id = get.id[0]['id'] + result = self.GetSiteRunPath(get)['message'] + if 'runPath' in result: + return public.return_message(0, 0, result['runPath']) + return public.return_message(-1, 0, "") + + # 创建Let's Encrypt免费证书 + def CreateLet(self, get): + + domains = json.loads(get.domains) + if not len(domains): + return_message = public.return_msg_gettext(False, 'Please choose a domain name') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + file_auth = True + if hasattr(get, 'dnsapi'): + file_auth = False + + if not hasattr(get, 'dnssleep'): + get.dnssleep = 10 + + email = public.M('users').getField('email') + if hasattr(get, 'email'): + if get.email.find('@') == -1: + get.email = email + else: + get.email = get.email.strip() + public.M('users').where('id=?', (1,)).setField('email', get.email) + else: + get.email = email + + for domain in domains: + if public.checkIp(domain): continue + if domain.find('*.') >= 0 and file_auth: + return_message = public.return_msg_gettext(False, + 'A generic domain name cannot be used to apply for a certificate using [File Validation]!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + if file_auth: + get.sitename = get.siteName + if self.GetRedirectList(get): + return_message = public.return_msg_gettext(False, + 'Your site has 301 Redirect on,Please turn it off first!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if self.GetProxyList(get): + return_message = public.return_msg_gettext(False, + 'Sites that have reverse proxy turned on cannot request SSL!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + data = self.get_site_info(get.siteName) + get.id = data['id'] + runPath = self.GetRunPath(get)['message']['result'] + if runPath != '/': + if runPath[:1] != '/': runPath = '/' + runPath + else: + runPath = '' + get.site_dir = data['path'] + runPath + + else: + dns_api_list = self.GetDnsApi(get) + get.dns_param = None + for dns in dns_api_list: + if dns['name'] == get.dnsapi: + param = [] + if not dns['data']: continue + for val in dns['data']: + param.append(val['value']) + get.dns_param = '|'.join(param) + n_list = ['dns', 'dns_bt'] + if not get.dnsapi in n_list: + if len(get.dns_param) < 16: + return_message = public.return_msg_gettext(False, 'No valid DNSAPI key information found', + (get.dnsapi,)) + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if get.dnsapi == 'dns_bt': + if not os.path.exists('plugin/dns/dns_main.py'): + return_message = public.return_msg_gettext(False, + 'Please go to the software store to install [Cloud Resolution] and complete the domain name NS binding.') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + self.check_ssl_pack() + try: + import panel_lets_v2 as panelLets + public.mod_reload(panelLets) + except Exception as ex: + if str(ex).find('No module named requests') != -1: + public.ExecShell("pip install requests &") + return_message = public.return_msg_gettext(False, + 'Missing requests component, please try to repair the panel!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + return_message = public.return_msg_gettext(False, str(ex)) + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + lets = panelLets.panelLets() + result = lets.apple_lest_cert(get) + if result['status'] and not 'code' in result: + get.onkey = 1 + path = '/www/server/panel/cert/' + get.siteName + if os.path.exists(path + '/certOrderId'): os.remove(path + '/certOrderId') + result = self.SetSSLConf(get) + return result + + def get_site_info(self, siteName): + data = public.M("sites").where('name=?', siteName).field('id,path,name').find() + return data + + # 检测依赖库 + def check_ssl_pack(self): + try: + import requests + except: + public.ExecShell('btpip install requests') + try: + import OpenSSL + except: + public.ExecShell('btpip install pyOpenSSL') + + # 判断DNS-API是否设置 + def Check_DnsApi(self, dnsapi): + dnsapis = self.GetDnsApi(None) + for dapi in dnsapis: + if dapi['name'] == dnsapi: + if not dapi['data']: return True + for d in dapi['data']: + if d['key'] == '': return False + return True + + # 获取DNS-API列表 + def GetDnsApi(self, get): + api_path = './config/dns_api.json' + api_init = './config/dns_api_init.json' + if not os.path.exists(api_path): + if os.path.exists(api_init): + import shutil + shutil.copyfile(api_init, api_path) + apis = json.loads(public.ReadFile(api_path)) + + path = '/root/.acme.sh' + if not os.path.exists(path + '/account.conf'): path = "/.acme.sh" + account = public.readFile(path + '/account.conf') + if not account: account = '' + is_write = False + for i in range(len(apis)): + if not apis[i]['data']: continue + for j in range(len(apis[i]['data'])): + if apis[i]['data'][j]['value']: continue + search_str = apis[i]['data'][j]['key'] + r"\s*=\s*'(.+)'" + match = re.search("" + apis[i]['data'][j]['key'] + r"\s*=\s*'(.+)'", account) + if match: apis[i]['data'][j]['value'] = match.groups()[0] + if apis[i]['data'][j]['value']: is_write = True + if is_write: public.writeFile('./config/dns_api.json', json.dumps(apis)) + result = [] + for i in apis: + if i['title'] == 'CloudFlare': + if os.path.exists('/www/server/panel/data/cf_limit_api.pl'): + i['API_Limit'] = True + else: + i['API_Limit'] = False + result.insert(0, i) + return public.return_message(0, 0, result) + + # 设置DNS-API + def SetDnsApi(self, get): + pdata = json.loads(get.pdata) + cf_limit_api = "/www/server/panel/data/cf_limit_api.pl" + if 'API_Limit' in pdata and pdata['API_Limit'] == True and not os.path.exists(cf_limit_api): + os.mknod(cf_limit_api) + if 'API_Limit' in pdata and pdata['API_Limit'] == False: + if os.path.exists(cf_limit_api): os.remove(cf_limit_api) + apis = json.loads(public.ReadFile('./config/dns_api.json')) + is_write = False + for key in pdata.keys(): + for i in range(len(apis)): + if not apis[i]['data']: continue + for j in range(len(apis[i]['data'])): + if apis[i]['data'][j]['key'] != key: continue + apis[i]['data'][j]['value'] = pdata[key] + is_write = True + + if is_write: public.writeFile('./config/dns_api.json', json.dumps(apis)) + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 获取站点所有域名 + def GetSiteDomains(self, get): + data = {} + domains = public.M('domain').where('pid=?', (get.id,)).field('name,id').select() + binding = public.M('binding').where('pid=?', (get.id,)).field('domain,id').select() + if type(binding) == str: return public.return_message(0, 0, binding) + for b in binding: + tmp = {} + tmp['name'] = b['domain'] + tmp['id'] = b['id'] + tmp['binding'] = True + domains.append(tmp) + data['domains'] = domains + data['email'] = public.M('users').where('id=?', (1,)).getField('email') + if data['email'] == '287962566@qq.com': data['email'] = '' + return public.return_message(0, 0, data) + + def GetFormatSSLResult(self, result): + try: + import re + rep = "\\s*Domain:.+\n\\s+Type:.+\n\\s+Detail:.+" + tmps = re.findall(rep, result) + + statusList = [] + for tmp in tmps: + arr = tmp.strip().split('\n') + status = {} + for ar in arr: + tmp1 = ar.strip().split(':') + status[tmp1[0].strip()] = tmp1[1].strip() + if len(tmp1) > 2: + status[tmp1[0].strip()] = tmp1[1].strip() + ':' + tmp1[2] + statusList.append(status) + return statusList + except: + return None + + # 获取TLS1.3标记 + def get_tls13(self): + nginx_bin = '/www/server/nginx/sbin/nginx' + nginx_v = public.ExecShell(nginx_bin + ' -V 2>&1')[0] + nginx_v_re = re.findall(r"nginx/(\d\.\d+).+OpenSSL\s+(\d\.\d+)", nginx_v, re.DOTALL) + if nginx_v_re: + if nginx_v_re[0][0] in ['1.8', '1.9', '1.7', '1.6', '1.5', '1.4']: + return '' + if float(nginx_v_re[0][0]) >= 1.15 and float(nginx_v_re[0][-1]) >= 1.1: + return ' TLSv1.3' + else: + _v = re.search(r'nginx/1\.1(5|6|7|8|9).\d', nginx_v) + if not _v: + _v = re.search(r'nginx/1\.2\d\.\d', nginx_v) + openssl_v = public.ExecShell(nginx_bin + ' -V 2>&1|grep OpenSSL')[0].find('OpenSSL 1.1.') != -1 + if _v and openssl_v: + return ' TLSv1.3' + return '' + + # 获取apache反向代理 + def get_apache_proxy(self, conf): + rep = "\n*#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid\n+\\s+IncludeOptiona.*" + proxy = re.search(rep, conf) + if proxy: + return proxy.group() + return "" + + def _get_site_domains(self, sitename): + site_id = public.M('sites').where('name=?', (sitename,)).field('id').find() + domains = public.M('domain').where('pid=?', (site_id['id'],)).field('name').select() + domains = [d['name'] for d in domains] + return domains + + # 设置OLS ssl + def set_ols_ssl(self, get, siteName): + listen_conf = self.setupPath + '/panel/vhost/openlitespeed/listen/443.conf' + conf = public.readFile(listen_conf) + ssl_conf = """ + vhssl { + keyFile /www/server/panel/vhost/cert/BTDOMAIN/privkey.pem + certFile /www/server/panel/vhost/cert/BTDOMAIN/fullchain.pem + certChain 1 + sslProtocol 24 + ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4 + enableECDHE 1 + renegProtection 1 + sslSessionCache 1 + enableSpdy 15 + enableStapling 1 + ocspRespMaxAge 86400 + } + """ + ssl_dir = self.setupPath + '/panel/vhost/openlitespeed/detail/ssl/' + if not os.path.exists(ssl_dir): + os.makedirs(ssl_dir) + ssl_file = ssl_dir + '{}.conf'.format(siteName) + if not os.path.exists(ssl_file): + ssl_conf = ssl_conf.replace('BTDOMAIN', siteName) + public.writeFile(ssl_file, ssl_conf, "a+") + include_ssl = '\ninclude {}'.format(ssl_file) + detail_file = self.setupPath + '/panel/vhost/openlitespeed/detail/{}.conf'.format(siteName) + public.writeFile(detail_file, include_ssl, 'a+') + if not conf: + conf = """ +listener SSL443 { + map BTSITENAME BTDOMAIN + address *:443 + secure 1 + keyFile /www/server/panel/vhost/cert/BTSITENAME/privkey.pem + certFile /www/server/panel/vhost/cert/BTSITENAME/fullchain.pem + certChain 1 + sslProtocol 24 + ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4 + enableECDHE 1 + renegProtection 1 + sslSessionCache 1 + enableSpdy 15 + enableStapling 1 + ocspRespMaxAge 86400 +} +""" + + else: + rep = r'listener\s*SSL443\s*{' + map = '\n map {s} {s}'.format(s=siteName) + conf = re.sub(rep, 'listener SSL443 {' + map, conf) + domain = ",".join(self._get_site_domains(siteName)) + conf = conf.replace('BTSITENAME', siteName).replace('BTDOMAIN', domain) + public.writeFile(listen_conf, conf) + + def _get_ap_static_security(self, ap_conf): + if not ap_conf: return public.return_message(0, 0, '') + ap_static_security = re.search('#SECURITY-START(.|\n)*#SECURITY-END', ap_conf) + if ap_static_security: + return public.return_message(0, 0, ap_static_security.group()) + return public.return_message(0, 0, '') + + # 添加SSL配置 + def SetSSLConf(self, get): + """ + @name 兼容批量设置 + @auther hezhihong + """ + siteName = get.siteName + if not 'first_domain' in get: get.first_domain = siteName + if 'isBatch' in get and siteName != get.first_domain: get.first_domain = siteName + + # Nginx配置 + file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + + # Node项目 + if not os.path.exists(file): file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' + + ng_file = file + conf = public.readFile(file) + + # 是否为子目录设置SSL + # if hasattr(get,'binding'): + # allconf = conf; + # conf = re.search("#BINDING-"+get.binding+"-START(.|\n)*#BINDING-"+get.binding+"-END",conf).group() + + if conf: + if conf.find('ssl_certificate') == -1: + sslStr = """#error_page 404/404.html; + ssl_certificate /www/server/panel/vhost/cert/%s/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/%s/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2%s; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000"; + error_page 497 https://$host$request_uri; +""" % (get.first_domain, get.first_domain, self.get_tls13()) + if (conf.find('ssl_certificate') != -1): + if 'isBatch' not in get: + public.serviceReload() + return_message = public.return_msg_gettext(True, 'SSL turned on!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + else: + return public.return_message(0, 0, "") + + conf = conf.replace('#error_page 404/404.html;', sslStr) + conf = re.sub(r"\s+\#SSL\-END", "\n\t\t#SSL-END", conf) + + # 添加端口 + rep = r"listen.*[\s:]+(\d+).*;" + tmp = re.findall(rep, conf) + if not public.inArray(tmp, '443'): + listen_re = re.search(rep, conf) + if not listen_re: + conf = re.sub(r"server\s*{\s*", "server\n{\n\t\tlisten 80;\n\t\t", conf) + listen_re = re.search(rep, conf) + listen = listen_re.group() + versionStr = public.readFile('/www/server/nginx/version.pl') + http2 = '' + if versionStr: + if versionStr.find('1.8.1') == -1: http2 = ' http2' + default_site = '' + if conf.find('default_server') != -1: default_site = ' default_server' + + listen_ipv6 = ';' + if self.is_ipv6: listen_ipv6 = ";\n\t\tlisten [::]:443 ssl" + http2 + default_site + ";" + conf = conf.replace(listen, listen + "\n\t\tlisten 443 ssl" + http2 + default_site + listen_ipv6) + shutil.copyfile(file, self.nginx_conf_bak) + + public.writeFile(file, conf) + + # Apache配置 + file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + # if not os.path.exists(file): file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' + is_node_apache = False + if not os.path.exists(file): + is_node_apache = True + file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' + conf = public.readFile(file) + ap_static_security = self._get_ap_static_security(conf) + if conf: + ap_proxy = self.get_apache_proxy(conf) + if conf.find('SSLCertificateFile') == -1 and conf.find('VirtualHost') != -1: + find = public.M('sites').where("name=?", (siteName,)).field('id,path').find() + tmp = public.M('domain').where('pid=?', (find['id'],)).field('name').select() + domains = '' + for key in tmp: + domains += key['name'] + ' ' + path = (find['path'] + '/' + self.GetRunPath(get))['message']['result'].replace('//', '/') + index = 'index.php index.html index.htm default.php default.html default.htm' + + try: + httpdVersion = public.readFile(self.setupPath + '/apache/version.pl').strip() + except: + httpdVersion = "" + if httpdVersion == '2.2': + vName = "" + phpConfig = "" + apaOpt = "Order allow,deny\n\t\tAllow from all" + else: + vName = "" + # rep = r"php-cgi-([0-9]{2,3})\.sock" + # version = re.search(rep, conf).groups()[0] + version = public.get_php_version_conf(conf) + if len(version) < 2: + if 'isBatch' not in get: + return_message = public.return_msg_gettext(False, 'Failed to get PHP version!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + else: + return public.return_message(-1, 0, "") + phpConfig = ''' + #PHP + + SetHandler "proxy:%s" + + ''' % (public.get_php_proxy(version, 'apache'),) + apaOpt = 'Require all granted' + + sslStr = r'''%s + ServerAdmin webmaster@example.com + DocumentRoot "%s" + ServerName SSL.%s + ServerAlias %s + #errorDocument 404 /404.html + ErrorLog "%s-error_log" + CustomLog "%s-access_log" combined + %s + #SSL + SSLEngine On + SSLCertificateFile /www/server/panel/vhost/cert/%s/fullchain.pem + SSLCertificateKeyFile /www/server/panel/vhost/cert/%s/privkey.pem + SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 + SSLProtocol All -SSLv2 -SSLv3 -TLSv1 + SSLHonorCipherOrder On + %s + %s + + #DENY FILES + + Order allow,deny + Deny from all + + + #PATH + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + %s + DirectoryIndex %s + +''' % (vName, path, siteName, domains, public.GetConfigValue('logs_path') + '/' + siteName, + public.GetConfigValue('logs_path') + '/' + siteName, ap_proxy, get.first_domain, get.first_domain, + ap_static_security, phpConfig, path, apaOpt, index) + conf = conf + "\n" + sslStr + self.apacheAddPort('443') + shutil.copyfile(file, self.apache_conf_bak) + public.writeFile(file, conf) + if is_node_apache: # 兼容Nodejs项目 + from projectModel.nodejsModel import main + m = main() + project_find = m.get_project_find(siteName) + m.set_apache_config(project_find) + # OLS + self.set_ols_ssl(get, siteName) + isError = public.checkWebConfig() + if (isError != True): + if os.path.exists(self.nginx_conf_bak): shutil.copyfile(self.nginx_conf_bak, ng_file) + if os.path.exists(self.apache_conf_bak): shutil.copyfile(self.apache_conf_bak, file) + public.ExecShell("rm -f /tmp/backup_*.conf") + if 'isBatch' not in get: + return_message = public.return_msg_gettext(False, + public.get_msg_gettext( + 'Certificate ERROR, please check!') + ':
                                    ' + isError.replace( + "\n", '
                                    ') + '
                                    ') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + else: + return public.return_message(-1, 0, "") + + sql = public.M('firewall') + import firewalls + get.port = '443' + get.ps = 'HTTPS' + if not public.M('firewall').where('port=?', ('443',)).count(): + firewalls.firewalls().AddAcceptPort(get) + public.serviceReload() + if 'isBatch' not in get: firewalls.firewalls().AddAcceptPort(get) + if 'isBatch' not in get: public.serviceReload() + self.save_cert(get) + public.write_log_gettext('Site manager', 'Site [{}] turned on SSL successfully!', (siteName,)) + result = public.return_msg_gettext(True, 'SSL turned on!') + result['csr'] = public.readFile('/www/server/panel/vhost/cert/' + get.siteName + '/fullchain.pem') + result['key'] = public.readFile('/www/server/panel/vhost/cert/' + get.siteName + '/privkey.pem') + del result['status'] + if 'isBatch' not in get: + return public.return_message(0, 0, result) + else: + return public.return_message(0, 0, "") + + def save_cert(self, get): + # try: + import panel_ssl_v2 as panelSSL + ss = panelSSL.panelSSL() + get.keyPath = '/www/server/panel/vhost/cert/' + get.siteName + '/privkey.pem' + get.certPath = '/www/server/panel/vhost/cert/' + get.siteName + '/fullchain.pem' + return ss.SaveCert(get) + return public.return_message(0, 0, "") + # except: + # return False; + + # HttpToHttps + def HttpToHttps(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)) + + siteName = get.siteName + # Nginx配置 + file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' + conf = public.readFile(file) + if conf: + if conf.find('ssl_certificate') == -1: + return_message = public.return_msg_gettext(False, 'SSL is NOT currently enabled') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + to = """#error_page 404/404.html; + #HTTP_TO_HTTPS_START + if ($server_port !~ 443){ + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END""" + conf = conf.replace('#error_page 404/404.html;', to) + public.writeFile(file, conf) + + file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' + conf = public.readFile(file) + if conf: + httpTohttos = '''combined + #HTTP_TO_HTTPS_START + + RewriteEngine on + RewriteCond %{SERVER_PORT} !^443$ + RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] + + #HTTP_TO_HTTPS_END''' + conf = re.sub('combined', httpTohttos, conf, 1) + public.writeFile(file, conf) + # OLS + conf_dir = '{}/panel/vhost/openlitespeed/redirect/{}/'.format(self.setupPath, siteName) + if not os.path.exists(conf_dir): + os.makedirs(conf_dir) + file = conf_dir + 'force_https.conf' + ols_force_https = ''' +#HTTP_TO_HTTPS_START + + RewriteEngine on + RewriteCond %{SERVER_PORT} !^443$ + RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] + +#HTTP_TO_HTTPS_END''' + public.writeFile(file, ols_force_https) + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # CloseToHttps + def CloseToHttps(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)) + + siteName = get.siteName + file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' + conf = public.readFile(file) + if conf: + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" + conf = re.sub(rep, '', conf) + rep = "\\s+if.+server_port.+\n.+\n\\s+\\s*}" + conf = re.sub(rep, '', conf) + public.writeFile(file, conf) + + file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + conf = public.readFile(file) + if conf: + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" + conf = re.sub(rep, '', conf) + public.writeFile(file, conf) + # OLS + file = '{}/panel/vhost/openlitespeed/redirect/{}/force_https.conf'.format(self.setupPath, siteName) + public.ExecShell('rm -f {}*'.format(file)) + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 是否跳转到https + def IsToHttps(self, siteName): + file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' + if not os.path.exists(file): return False + conf = public.readFile(file) + if conf: + if conf.find('HTTP_TO_HTTPS_START') != -1: return True + if conf.find('$server_port !~ 443') != -1: return True + return False + + # 清理SSL配置 + def CloseSSLConf(self, get): + # 校验参数 + try: + get.validate([ + Param('siteName').String(), + Param('updateOf').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + siteName = get.siteName + + file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' + conf = public.readFile(file) + if conf: + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" + conf = re.sub(rep, '', conf) + rep = r"\s+ssl_certificate\s+.+;\s+ssl_certificate_key\s+.+;" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_protocols\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_ciphers\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_prefer_server_ciphers\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_session_cache\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_session_timeout\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_ecdh_curve\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_session_tickets\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_stapling\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+ssl_stapling_verify\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+add_header\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = "\\s+add_header\\s+.+;\n" + conf = re.sub(rep, '', conf) + rep = r"\s+ssl\s+on;" + conf = re.sub(rep, '', conf) + rep = r"\s+error_page\s497.+;" + conf = re.sub(rep, '', conf) + rep = "\\s+if.+server_port.+\n.+\n\\s+\\s*}" + conf = re.sub(rep, '', conf) + rep = r"\s+listen\s+443.*;" + conf = re.sub(rep, '', conf) + rep = r"\s+listen\s+\[::\]:443.*;" + conf = re.sub(rep, '', conf) + public.writeFile(file, conf) + + file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' + conf = public.readFile(file) + if conf: + rep = "\n(.|\n)*<\\/VirtualHost>" + conf = re.sub(rep, '', conf) + rep = "\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,250}#HTTP_TO_HTTPS_END" + conf = re.sub(rep, '', conf) + rep = "NameVirtualHost *:443\n" + conf = conf.replace(rep, '') + public.writeFile(file, conf) + + # OLS + ssl_file = self.setupPath + '/panel/vhost/openlitespeed/detail/ssl/{}.conf'.format(siteName) + detail_file = self.setupPath + '/panel/vhost/openlitespeed/detail/' + siteName + '.conf' + force_https = self.setupPath + '/panel/vhost/openlitespeed/redirect/' + siteName + string = 'rm -f {}/force_https.conf*'.format(force_https) + public.ExecShell(string) + detail_conf = public.readFile(detail_file) + if detail_conf: + detail_conf = detail_conf.replace('\ninclude ' + ssl_file, '') + public.writeFile(detail_file, detail_conf) + public.ExecShell('rm -f {}*'.format(ssl_file)) + + self._del_ols_443_domain(siteName) + partnerOrderId = '/www/server/panel/vhost/cert/' + siteName + '/partnerOrderId' + if os.path.exists(partnerOrderId): public.ExecShell('rm -f ' + partnerOrderId) + p_file = '/etc/letsencrypt/live/' + siteName + '/partnerOrderId' + if os.path.exists(p_file): public.ExecShell('rm -f ' + p_file) + + public.write_log_gettext('Site manager', 'Site [{}] turned off SSL successfully!', (siteName,)) + public.serviceReload() + return public.return_message(0, 0, 'SSL turned off!') + + def _del_ols_443_domain(self, sitename): + file = "/www/server/panel/vhost/openlitespeed/listen/443.conf" + conf = public.readFile(file) + if conf: + rep = '\n\\s*map\\s*{}.*'.format(sitename) + conf = re.sub(rep, '', conf) + if not "map " in conf: + public.ExecShell('rm -f {}*'.format(file)) + return + public.writeFile(file, conf) + + # 取SSL状态 + def GetSSL(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)) + + siteName = get.siteName + path = os.path.join('/www/server/panel/vhost/cert/', siteName) + if not os.path.isfile(os.path.join(path, "fullchain.pem")) and not os.path.isfile( + os.path.join(path, "privkey.pem")): + path = os.path.join('/etc/letsencrypt/live/', siteName) + type = 0 + if os.path.exists(path + '/README'): type = 1 + if os.path.exists(path + '/partnerOrderId'): type = 2 + if os.path.exists(path + '/certOrderId'): type = 3 + csrpath = path + "/fullchain.pem" # 生成证书路径 + keypath = path + "/privkey.pem" # 密钥文件路径 + key = public.readFile(keypath) + csr = public.readFile(csrpath) + file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + siteName + '.conf' + + # 是否为node项目 + if not os.path.exists( + file): file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/node_' + siteName + '.conf' + if public.get_webserver() == "openlitespeed": + file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/detail/' + siteName + '.conf' + conf = public.readFile(file) + if not conf: return public.return_message(-1, 0, 'The specified website profile does not exist') + + if public.get_webserver() == 'nginx': + keyText = 'ssl_certificate' + elif public.get_webserver() == 'apache': + keyText = 'SSLCertificateFile' + else: + keyText = 'openlitespeed/detail/ssl' + + status = True + if (conf.find(keyText) == -1): + status = False + type = -1 + + toHttps = self.IsToHttps(siteName) + id = public.M('sites').where("name=?", (siteName,)).getField('id') + domains = public.M('domain').where("pid=?", (id,)).field('name').select() + cert_data = {} + if csr: + get.certPath = csrpath + import panel_ssl_v2 + cert_data = panel_ssl_v2.panelSSL().GetCertName(get) + if not cert_data: + cert_data = { + 'certificate': 0 + } + + email = public.M('users').where('id=?', (1,)).getField('email') + if email == '287962566@qq.com': email = '' + index = '' + auth_type = 'http' + if status == True: + if type != 1: + import acme_v3 + acme = acme_v3.acme_v2() + index = acme.check_order_exists(csrpath) + if index: + if index.find('/') == -1: + auth_type = acme._config['orders'][index]['auth_type'] + type = 1 + else: + crontab_file = 'vhost/cert/crontab.json' + tmp = public.readFile(crontab_file) + if tmp: + crontab_config = json.loads(tmp) + if siteName in crontab_config: + if 'dnsapi' in crontab_config[siteName]: + auth_type = 'dns' + + if os.path.exists(path + '/certOrderId'): type = 3 + oid = -1 + if type == 3: + oid = int(public.readFile(path + '/certOrderId')) + # 作者 muluo + # return {'status': status,'oid':oid, 'domain': domains, 'key': key, 'csr': csr, 'type': type, 'httpTohttps': toHttps,'cert_data':cert_data,'email':email,"index":index,'auth_type':auth_type} + res = {'status': status, 'oid': oid, 'domain': domains, 'key': key, 'csr': csr, 'type': type, + 'httpTohttps': toHttps, 'cert_data': cert_data, 'email': email, "index": index, 'auth_type': auth_type} + res['push'] = self.get_site_push_status(None, siteName, 'ssl') + return public.return_message(0, 0, res) + + def get_site_push_status(self, get, siteName=None, stype=None): + """ + @获取网站ssl告警通知状态 + @param get: + @param siteName 网站名称 + @param stype 类型 ssl + """ + import panel_push_v2 as panelPush + if get: + siteName = get.siteName + stype = get.stype + + result = {} + result['status'] = False + try: + data = {} + try: + data = json.loads(public.readFile('{}/class/push/push.json'.format(public.get_panel_path()))) + except: + pass + + if not 'site_push' in data: + return result + + ssl_data = data['site_push'] + for key in ssl_data.keys(): + if ssl_data[key]['type'] != stype: + continue + + project = ssl_data[key]['project'] + if project in [siteName, 'all']: + ssl_data[key]['id'] = key + ssl_data[key]['s_module'] = 'site_push' + + if project == siteName: + result = ssl_data[key] + break + + if project == 'all': + result = ssl_data[key] + except: + pass + + p_obj = panelPush.panelPush() + return public.return_message(0, 0, p_obj.get_push_user(result)) + + def set_site_status_multiple(self, get): + ''' + @name 批量设置网站状态 + @author zhwen<2020-11-17> + @param sites_id "1,2" + @param status 0/1 + ''' + sites_id = get.sites_id.split(',') + sites_name = [] + errors = {} + day_time = time.time() + for site_id in sites_id: + get.id = site_id + find = public.M('sites').where("id=?", (site_id,)).find() + get.name = find['name'] + + if get.status == '1': + if find['edate'] != '0000-00-00' and public.to_date("%Y-%m-%d", find['edate']) < day_time: + errors[get.name] = "failed, site has expired" + continue + sites_name.append(get.name) + if get.status == '1': + self.SiteStart(get, multiple=1) + else: + self.SiteStop(get, multiple=1) + public.serviceReload() + if get.status == '1': + return_message = { + 'msg': public.get_msg_gettext('Enable website [{}] successfully', (','.join(sites_name),)), + 'error': {}, 'success': sites_name} + return public.return_message(0, 0, return_message) + else: + return_message = { + 'msg': public.get_msg_gettext('Disable website [{}] successfully', (','.join(sites_name),)), + 'error': {}, 'success': sites_name} + return public.return_message(0, 0, return_message) + + # 启动站点 + def SiteStart(self, get, multiple=None): + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + Path = self.setupPath + '/stop' + sitePath = public.M('sites').where("id=?", (id,)).getField('path') + + # nginx + file = self.setupPath + '/panel/vhost/nginx/' + get.name + '.conf' + conf = public.readFile(file) + if conf: + conf = conf.replace(Path, sitePath) + conf = conf.replace("#include", "include") + public.writeFile(file, conf) + # apache + file = self.setupPath + '/panel/vhost/apache/' + get.name + '.conf' + conf = public.readFile(file) + if conf: + conf = conf.replace(Path, sitePath) + conf = conf.replace("#IncludeOptional", "IncludeOptional") + public.writeFile(file, conf) + + # OLS + file = self.setupPath + '/panel/vhost/openlitespeed/' + get.name + '.conf' + conf = public.readFile(file) + if conf: + rep = r'vhRoot\s*{}'.format(Path) + new_content = 'vhRoot {}'.format(sitePath) + conf = re.sub(rep, new_content, conf) + public.writeFile(file, conf) + + public.M('sites').where("id=?", (id,)).setField('status', '1') + if not multiple: + public.serviceReload() + public.write_log_gettext('Site manager', 'Site [{}] started!', (get.name,)) + return_message = public.return_msg_gettext(True, 'Site started') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + def _process_has_run_dir(self, website_name, website_path, stop_path): + ''' + @name 当网站存在允许目录时停止网站需要做处理 + @author zhwen<2020-11-17> + @param site_id 1 + @param names test,baohu + ''' + conf = public.readFile(self.setupPath + '/panel/vhost/nginx/' + website_name + '.conf') + if not conf: + return False + try: + really_path = re.search(r'root\s+(.*);', conf).group(1) + tmp = stop_path + '/' + really_path.replace(website_path + '/', '') + public.ExecShell('mkdir {t} && ln -s {s}/index.html {t}/index.html'.format(t=tmp, s=stop_path)) + except: + pass + + # 停止站点 + def SiteStop(self, get, multiple=None): + path = self.setupPath + '/stop' + id = get.id + site_status = public.M('sites').where("id=?", (id,)).getField('status') + if str(site_status) != '1': + return public.return_message(0, 0, 'SITE_STOP_SUCCESS') + if not os.path.exists(path): + os.makedirs(path) + public.downloadFile('https://node.aapanel.com/stop_en.html', path + '/index.html') + + # if 'This site has been closed by administrator' not in public.readFile(path + '/index.html'): + # public.downloadFile('http://download.bt.cn/stop_en.html', path + '/index.html') + + binding = public.M('binding').where('pid=?', (id,)).field('id,pid,domain,path,port,addtime').select() + for b in binding: + bpath = path + '/' + b['path'] + if not os.path.exists(bpath): + public.ExecShell('mkdir -p ' + bpath) + public.ExecShell('ln -sf ' + path + '/index.html ' + bpath + '/index.html') + + sitePath = public.M('sites').where("id=?", (id,)).getField('path') + self._process_has_run_dir(get.name, sitePath, path) + # nginx + file = self.setupPath + '/panel/vhost/nginx/' + get.name + '.conf' + conf = public.readFile(file) + if conf: + src_path = 'root ' + sitePath + dst_path = 'root ' + path + if conf.find(src_path) != -1: + conf = conf.replace(src_path, dst_path) + else: + conf = conf.replace(sitePath, path) + conf = conf.replace("include", "#include") + public.writeFile(file, conf) + + # apache + file = self.setupPath + '/panel/vhost/apache/' + get.name + '.conf' + conf = public.readFile(file) + if conf: + conf = conf.replace(sitePath, path) + conf = conf.replace("IncludeOptional", "#IncludeOptional") + public.writeFile(file, conf) + # OLS + file = self.setupPath + '/panel/vhost/openlitespeed/' + get.name + '.conf' + conf = public.readFile(file) + if conf: + rep = r'vhRoot\s*{}'.format(sitePath) + new_content = 'vhRoot {}'.format(path) + conf = re.sub(rep, new_content, conf) + public.writeFile(file, conf) + + public.M('sites').where("id=?", (id,)).setField('status', '0') + if not multiple: + public.serviceReload() + public.write_log_gettext('Site manager', 'Site [{}] stopped!', (get.name,)) + return_message = public.return_msg_gettext(True, 'Site stopped') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 取流量限制值 + def GetLimitNet(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + + # 取回配置文件 + siteName = public.M('sites').where("id=?", (id,)).getField('name') + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + + # 站点总并发 + data = {} + conf = public.readFile(filename) + try: + rep = r"\s+limit_conn\s+perserver\s+([0-9]+);" + tmp = re.search(rep, conf).groups() + data['perserver'] = int(tmp[0]) + + # IP并发限制 + rep = r"\s+limit_conn\s+perip\s+([0-9]+);" + tmp = re.search(rep, conf).groups() + data['perip'] = int(tmp[0]) + + # 请求并发限制 + rep = r"\s+limit_rate\s+([0-9]+)\w+;" + tmp = re.search(rep, conf).groups() + data['limit_rate'] = int(tmp[0]) + except: + data['perserver'] = 0 + data['perip'] = 0 + data['limit_rate'] = 0 + + return public.return_message(0, 0, data) + + # 设置流量限制 + def SetLimitNet(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + Param('perserver').Integer(), + Param('perip').Integer(), + Param('limit_rate').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if (public.get_webserver() != 'nginx'): return public.return_message(-1, 0, + 'Site Traffic Control only supports Nginx Web Server!') + + id = get.id + if int(get.perserver) < 1 or int(get.perip) < 1 or int(get.perip) < 1: + return public.return_message(-1, 0, + 'Concurrency restrictions, IP restrictions, traffic restrictions must be greater than 0') + perserver = 'limit_conn perserver ' + get.perserver + ';' + perip = 'limit_conn perip ' + get.perip + ';' + limit_rate = 'limit_rate ' + get.limit_rate + 'k;' + + # 取回配置文件 + siteName = public.M('sites').where("id=?", (id,)).getField('name') + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + conf = public.readFile(filename) + + # 设置共享内存 + oldLimit = self.setupPath + '/panel/vhost/nginx/limit.conf' + if (os.path.exists(oldLimit)): os.remove(oldLimit) + limit = self.setupPath + '/nginx/conf/nginx.conf' + nginxConf = public.readFile(limit) + limitConf = "limit_conn_zone $binary_remote_addr zone=perip:10m;\n\t\tlimit_conn_zone $server_name zone=perserver:10m;" + nginxConf = nginxConf.replace("#limit_conn_zone $binary_remote_addr zone=perip:10m;", limitConf) + public.writeFile(limit, nginxConf) + + if (conf.find('limit_conn perserver') != -1): + # 替换总并发 + rep = r"limit_conn\s+perserver\s+([0-9]+);" + conf = re.sub(rep, perserver, conf) + + # 替换IP并发限制 + rep = r"limit_conn\s+perip\s+([0-9]+);" + conf = re.sub(rep, perip, conf) + + # 替换请求流量限制 + rep = r"limit_rate\s+([0-9]+)\w+;" + conf = re.sub(rep, limit_rate, conf) + else: + conf = conf.replace('#error_page 404/404.html;', + "#error_page 404/404.html;\n " + perserver + "\n " + perip + "\n " + limit_rate) + + import shutil + shutil.copyfile(filename, self.nginx_conf_bak) + public.writeFile(filename, conf) + isError = public.checkWebConfig() + if (isError != True): + if os.path.exists(self.nginx_conf_bak): shutil.copyfile(self.nginx_conf_bak, filename) + return public.return_message(-1, 0, + 'ERROR:
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ') + + public.serviceReload() + public.write_log_gettext('Site manager', 'Site [{}] traffic control turned on!', (siteName,)) + return public.return_message(0, 0, 'Setup successfully!') + + # 关闭流量限制 + def CloseLimitNet(self, get): + id = get.id + # 取回配置文件 + siteName = public.M('sites').where("id=?", (id,)).getField('name') + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + conf = public.readFile(filename) + # 清理总并发 + rep = r"\s+limit_conn\s+perserver\s+([0-9]+);" + conf = re.sub(rep, '', conf) + + # 清理IP并发限制 + rep = r"\s+limit_conn\s+perip\s+([0-9]+);" + conf = re.sub(rep, '', conf) + + # 清理请求流量限制 + rep = r"\s+limit_rate\s+([0-9]+)\w+;" + conf = re.sub(rep, '', conf) + public.writeFile(filename, conf) + public.serviceReload() + public.write_log_gettext('Site manager', 'Site Traffic Control has been turned off!', (siteName,)) + return_message = public.return_msg_gettext(True, 'Site Traffic Control has been turned off!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 取301配置状态 + def Get301Status(self, get): + siteName = get.siteName + result = {} + domains = '' + id = public.M('sites').where("name=?", (siteName,)).getField('id') + tmp = public.M('domain').where("pid=?", (id,)).field('name').select() + node = public.M('sites').where('id=? and project_type=?', (id, 'Node')).count() + if node: + node = 'node_' + else: + node = '' + for key in tmp: + domains += key['name'] + ',' + try: + if (public.get_webserver() == 'nginx'): + conf = public.readFile(self.setupPath + '/panel/vhost/nginx/' + node + siteName + '.conf') + if conf.find('301-START') == -1: + result['domain'] = domains[:-1] + result['src'] = "" + result['status'] = False + result['url'] = "http://" + return public.return_message(0, 0, result) + rep = r"return\s+301\s+((http|https)\://.+);" + arr = re.search(rep, conf).groups()[0] + rep = r"'\^(([\w-]+\.)+[\w-]+)'" + tmp = re.search(rep, conf) + src = '' + if tmp: src = tmp.groups()[0] + elif public.get_webserver() == 'apache': + conf = public.readFile(self.setupPath + '/panel/vhost/apache/' + node + siteName + '.conf') + if conf.find('301-START') == -1: + result['domain'] = domains[:-1] + result['src'] = "" + result['status'] = False + result['url'] = "http://" + return public.return_message(0, 0, result) + rep = r"RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" + arr = re.search(rep, conf).groups()[0] + rep = r"\^((\w+\.)+\w+)\s+\[NC" + tmp = re.search(rep, conf) + src = '' + if tmp: src = tmp.groups()[0] + else: + conf = public.readFile( + self.setupPath + '/panel/vhost/openlitespeed/redirect/{s}/{s}.conf'.format(s=siteName)) + if not conf: + result['domain'] = domains[:-1] + result['src'] = "" + result['status'] = False + result['url'] = "http://" + return result + rep = r"RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" + arr = re.search(rep, conf).groups()[0] + rep = r"\^((\w+\.)+\w+)\s+\[NC" + tmp = re.search(rep, conf) + src = '' + if tmp: src = tmp.groups()[0] + except: + src = '' + arr = 'http://' + + result['domain'] = domains[:-1] + result['src'] = src.replace("'", '') + result['status'] = True + if (len(arr) < 3): result['status'] = False + result['url'] = arr + + return public.return_message(0, 0, result) + + # 设置301配置 + def Set301Status(self, get): + siteName = get.siteName + srcDomain = get.srcDomain + toDomain = get.toDomain + type = get.type + rep = r"(http|https)\://.+" + if not re.match(rep, toDomain): + return_message = public.return_msg_gettext(False, 'URL address is invalid!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + # nginx + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + mconf = public.readFile(filename) + if mconf == False: + return_message = public.return_msg_gettext(False, 'Configuration file not exist') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if (srcDomain == 'all'): + conf301 = "\t#301-START\n\t\treturn 301 " + toDomain + "$request_uri;\n\t#301-END" + else: + conf301 = "\t#301-START\n\t\tif ($host ~ '^" + srcDomain + "'){\n\t\t\treturn 301 " + toDomain + "$request_uri;\n\t\t}\n\t#301-END" + if type == '1': + mconf = mconf.replace("#error_page 404/404.html;", "#error_page 404/404.html;\n" + conf301) + else: + rep = "\\s+#301-START(.|\n){1,300}#301-END" + mconf = re.sub(rep, '', mconf) + public.writeFile(filename, mconf) + + # apache + filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + mconf = public.readFile(filename) + if mconf == False: + return_message = public.return_msg_gettext(False, 'Configuration file not exist') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if type == '1': + if (srcDomain == 'all'): + conf301 = "\n\t#301-START\n\t\n\t\tRewriteEngine on\n\t\tRewriteRule ^(.*)$ " + toDomain + "$1 [L,R=301]\n\t\n\t#301-END\n" + else: + conf301 = "\n\t#301-START\n\t\n\t\tRewriteEngine on\n\t\tRewriteCond %{HTTP_HOST} ^" + srcDomain + " [NC]\n\t\tRewriteRule ^(.*) " + toDomain + "$1 [L,R=301]\n\t\n\t#301-END\n" + rep = "combined" + mconf = mconf.replace(rep, rep + "\n\t" + conf301) + else: + rep = "\n\\s+#301-START(.|\n){1,300}#301-END\n*" + mconf = re.sub(rep, '\n\n', mconf, 1) + mconf = re.sub(rep, '\n\n', mconf, 1) + + public.writeFile(filename, mconf) + + # OLS + conf_dir = self.setupPath + '/panel/vhost/openlitespeed/redirect/{}/'.format(siteName) + if not os.path.exists(conf_dir): + os.makedirs(conf_dir) + file = conf_dir + siteName + '.conf' + if type == '1': + if (srcDomain == 'all'): + conf301 = "#301-START\nRewriteEngine on\nRewriteRule ^(.*)$ " + toDomain + "$1 [L,R=301]#301-END\n" + else: + conf301 = "#301-START\nRewriteEngine on\nRewriteCond %{HTTP_HOST} ^" + srcDomain + " [NC]\nRewriteRule ^(.*) " + toDomain + "$1 [L,R=301]\n#301-END\n" + public.writeFile(file, conf301) + else: + public.ExecShell('rm -f {}*'.format(file)) + + isError = public.checkWebConfig() + if (isError != True): + return_message = public.return_msg_gettext(False, + 'ERROR:
                                    ' + isError.replace("\n", + '
                                    ') + '
                                    ') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 取子目录绑定 + def GetDirBinding(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + path = public.M('sites').where('id=?', (get.id,)).getField('path') + if not os.path.exists(path): + checks = ['/', '/usr', '/etc'] + if path in checks: + data = {} + data['dirs'] = [] + data['binding'] = [] + return data + public.ExecShell('mkdir -p ' + path) + public.ExecShell('chmod 755 ' + path) + public.ExecShell('chown www:www ' + path) + get.path = path + self.SetDirUserINI(get) + siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + public.write_log_gettext('Site manager', "Site [{}], document root [{}] does NOT exist, recreated!", + (siteName, path)) + dirnames = [] + # 取运行目录 + run_path = self.GetRunPath(get)['message']['result'] + if run_path: path += run_path + # 遍历目录 + if os.path.exists(path): + for filename in os.listdir(path): + try: + json.dumps(filename) + if sys.version_info[0] == 2: + filename = filename.encode('utf-8') + else: + filename.encode('utf-8') + filePath = path + '/' + filename + if os.path.islink(filePath): continue + if os.path.isdir(filePath): + dirnames.append(filename) + except: + pass + data = {} + data['run_path'] = run_path # 运行目录 + data['dirs'] = dirnames + data['binding'] = public.M('binding').where('pid=?', (get.id,)).field( + 'id,pid,domain,path,port,addtime').select() + # 标记子目录是否存在 + for dname in data['binding']: + _path = os.path.join(path, dname['path']) + if not os.path.exists(_path): + _path = _path.replace(run_path, '') + if not os.path.exists(_path): + dname['path'] += ' >> error: directory does not exist' + else: + dname['path'] = '../' + dname['path'] + + return public.return_message(0, 0, data) + + # 添加子目录绑定 + def AddDirBinding(self, get): + import shutil + id = get.id + tmp = get.domain.split(':') + domain = tmp[0].lower() + # 中文域名转码 + domain = public.en_punycode(domain) + port = '80' + version = '' + if len(tmp) > 1: port = tmp[1] + if not hasattr(get, 'dirName'): + return_message = public.return_msg_gettext(False, 'Directory cannot be empty!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + dirName = get.dirName + reg = r"^([\w\-\*]{1,100}\.){1,4}([\w\-]{1,100}|[\w\-]{1,100}\.[\w\-]{1,100})$" + if not re.match(reg, domain): + return_message = public.return_msg_gettext(False, 'Format of primary domain is incorrect') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + siteInfo = public.M('sites').where("id=?", (id,)).field('id,path,name').find() + # 实际运行目录 + root_path = siteInfo['path'] + run_path = self.GetRunPath(get)['message']['result'] + if run_path: root_path += run_path + + webdir = root_path + '/' + dirName + webdir = webdir.replace('//', '/').strip() + if not os.path.exists(webdir): # 如果在运行目录找不到指定子目录,尝试到根目录查找 + root_path = siteInfo['path'] + webdir = root_path + '/' + dirName + webdir = webdir.replace('//', '/').strip() + + sql = public.M('binding') + if sql.where("domain=?", (domain,)).count() > 0: + return_message = public.return_msg_gettext(False, 'The domain you tried to add already exists!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if public.M('domain').where("name=?", (domain,)).count() > 0: + return_message = public.return_msg_gettext(False, + 'The domain you tried to add already exists!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + filename = self.setupPath + '/panel/vhost/nginx/' + siteInfo['name'] + '.conf' + nginx_conf_file = filename + conf = public.readFile(filename) + if conf: + listen_ipv6 = '' + if self.is_ipv6: listen_ipv6 = "\n listen [::]:%s;" % port + try: + rep = r"enable-php-(\w{2,5})\.conf" + tmp = re.search(rep, conf) + if not tmp: + rep = r"enable-php-(\d+-wpfastcgi).conf" + tmp = re.search(rep, conf) + except: + return public.return_message(-1, 0, "Get enable php config failed!") + tmp = tmp.groups() + version = tmp[0] + bindingConf = r''' +#BINDING-%s-START +server +{ + listen %s;%s + server_name %s; + index index.php index.html index.htm default.php default.htm default.html; + root %s; + + include enable-php-%s.conf; + include %s/panel/vhost/rewrite/%s.conf; + %s + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + { + return 404; + } + + %s + location ~ \.well-known{ + allow all; + } + + location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + error_log /dev/null; + access_log /dev/null; + } + location ~ .*\\.(js|css)?$ + { + expires 12h; + error_log /dev/null; + access_log /dev/null; + } + access_log %s.log; + error_log %s.error.log; +} +#BINDING-%s-END''' % (domain, port, listen_ipv6, domain, webdir, version, self.setupPath, siteInfo['name'], + public.get_msg_gettext('# Forbidden files or directories'), public.get_msg_gettext( + '# Directory verification related settings for one-click application for SSL certificate'), + public.GetConfigValue('logs_path') + '/' + siteInfo['name'], + public.GetConfigValue('logs_path') + '/' + siteInfo['name'], domain) + + conf += bindingConf + shutil.copyfile(filename, self.nginx_conf_bak) + public.writeFile(filename, conf) + + filename = self.setupPath + '/panel/vhost/apache/' + siteInfo['name'] + '.conf' + conf = public.readFile(filename) + if conf: + try: + try: + httpdVersion = public.readFile(self.setupPath + '/apache/version.pl').strip() + except: + httpdVersion = "" + if httpdVersion == '2.2': + phpConfig = "" + apaOpt = "Order allow,deny\n\t\tAllow from all" + else: + # rep = r"php-cgi-([0-9]{2,3})\.sock" + # tmp = re.search(rep,conf).groups() + # version = tmp[0] + version = public.get_php_version_conf(conf) + phpConfig = ''' + #PHP + + SetHandler "proxy:%s" + + ''' % (public.get_php_proxy(version, 'apache'),) + apaOpt = 'Require all granted' + + bindingConf = r''' + +#BINDING-%s-START + + ServerAdmin webmaster@example.com + DocumentRoot "%s" + ServerAlias %s + #errorDocument 404 /404.html + ErrorLog "%s-error_log" + CustomLog "%s-access_log" combined + %s + + #DENY FILES + + Order allow,deny + Deny from all + + + #PATH + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + %s + DirectoryIndex index.php index.html index.htm default.php default.html default.htm + + +#BINDING-%s-END''' % (domain, port, webdir, domain, public.GetConfigValue('logs_path') + '/' + siteInfo['name'], + public.GetConfigValue('logs_path') + '/' + siteInfo['name'], phpConfig, webdir, apaOpt, domain) + + conf += bindingConf + shutil.copyfile(filename, self.apache_conf_bak) + public.writeFile(filename, conf) + except: + pass + get.webname = siteInfo['name'] + get.port = port + self.phpVersion = version + self.siteName = siteInfo['name'] + self.sitePath = webdir + listen_file = self.setupPath + "/panel/vhost/openlitespeed/listen/80.conf" + listen_conf = public.readFile(listen_file) + if listen_conf: + rep = r'secure\s*0' + map = '\tmap {}_{} {}'.format(siteInfo['name'], dirName, domain) + listen_conf = re.sub(rep, 'secure 0\n' + map, listen_conf) + public.writeFile(listen_file, listen_conf) + self.openlitespeed_add_site(get) + + # 检查配置是否有误 + isError = public.checkWebConfig() + if isError != True: + if os.path.exists(self.nginx_conf_bak): shutil.copyfile(self.nginx_conf_bak, nginx_conf_file) + if os.path.exists(self.apache_conf_bak): shutil.copyfile(self.apache_conf_bak, filename) + return_message = public.return_msg_gettext(False, + 'ERROR:
                                    ' + isError.replace("\n", + '
                                    ') + '
                                    ') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + public.M('binding').add('pid,domain,port,path,addtime', (id, domain, port, dirName, public.getDate())) + public.serviceReload() + public.write_log_gettext('Site manager', 'Site [{}] subdirectory [{}] bound to [{}]', + (siteInfo['name'], dirName, domain)) + return_message = public.return_msg_gettext(True, 'Successfully added') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + def delete_dir_bind_multiple(self, get): + ''' + @name 批量删除网站 + @author zhwen<2020-11-17> + @param bind_ids 1,2,3 + ''' + bind_ids = get.bind_ids.split(',') + del_successfully = [] + del_failed = {} + for bind_id in bind_ids: + get.id = bind_id + domain = public.M('binding').where("id=?", (get.id,)).getField('domain') + if not domain: + continue + try: + self.DelDirBinding(get, multiple=1) + del_successfully.append(domain) + except: + del_failed[domain] = public.get_msg_gettext('There was an error deleting, please try again.') + pass + public.serviceReload() + return_message = {'msg': public.get_msg_gettext('Delete [{}] subdirectory binding successfully', + (','.join(del_successfully),)), + 'error': del_failed, + 'success': del_successfully} + return public.return_message(0, 0, return_message) + + # 删除子目录绑定 + def DelDirBinding(self, get, multiple=None): + id = get.id + binding = public.M('binding').where("id=?", (id,)).field('id,pid,domain,path').find() + siteName = public.M('sites').where("id=?", (binding['pid'],)).getField('name') + + # nginx + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + conf = public.readFile(filename) + if conf: + rep = r"\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" + conf = re.sub(rep, '', conf) + public.writeFile(filename, conf) + + # apache + filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + conf = public.readFile(filename) + if conf: + rep = r"\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" + conf = re.sub(rep, '', conf) + public.writeFile(filename, conf) + + # openlitespeed + filename = self.setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' + conf = public.readFile(filename) + rep = "#SUBDIR\\s*{s}_{d}\\s*START(\n|.)+#SUBDIR\\s*{s}_{d}\\s*END".format(s=siteName, d=binding['path']) + if conf: + conf = re.sub(rep, '', conf) + public.writeFile(filename, conf) + # 删除域名,前端需要传域名 + get.webname = siteName + get.domain = binding['domain'] + self._del_ols_domain(get) + + # 清理子域名监听文件 + listen_file = self.setupPath + "/panel/vhost/openlitespeed/listen/80.conf" + listen_conf = public.readFile(listen_file) + if listen_conf: + map_reg = r'\s*map\s*{}_{}.*'.format(siteName, binding['path']) + listen_conf = re.sub(map_reg, '', listen_conf) + public.writeFile(listen_file, listen_conf) + # 清理detail文件 + detail_file = "{}/panel/vhost/openlitespeed/detail/{}_{}.conf".format(self.setupPath, siteName, binding['path']) + public.ExecShell("rm -f {}*".format(detail_file)) + + # 从数据库删除绑定 + public.M('binding').where("id=?", (id,)).delete() + + # 如果没有其它域名绑定同一子目录,则删除该子目录的伪静态规则 + if not public.M('binding').where("path=? AND pid=?", (binding['path'], binding['pid'])).count(): + filename = self.setupPath + '/panel/vhost/rewrite/' + siteName + '_' + binding['path'] + '.conf' + if os.path.exists(filename): public.ExecShell('rm -rf %s' % filename) + # 是否需要重载服务 + if not multiple: + public.serviceReload() + public.write_log_gettext('Site manager', 'Deleted site [{}] subdirectory [{}] binding', + (siteName, binding['path'])) + return_message = public.return_msg_gettext(True, 'Successfully deleted') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 取子目录Rewrite + def GetDirRewrite(self, get): + id = get.id + find = public.M('binding').where("id=?", (id,)).field('id,pid,domain,path').find() + site = public.M('sites').where("id=?", (find['pid'],)).field('id,name,path').find() + + if (public.get_webserver() != 'nginx'): + filename = site['path'] + '/' + find['path'] + '/.htaccess' + else: + filename = self.setupPath + '/panel/vhost/rewrite/' + site['name'] + '_' + find['path'] + '.conf' + + if hasattr(get, 'add'): + public.writeFile(filename, '') + if public.get_webserver() == 'nginx': + file = self.setupPath + '/panel/vhost/nginx/' + site['name'] + '.conf' + conf = public.readFile(file) + domain = find['domain'] + rep = "\n#BINDING-" + domain + "-START(.|\n)+BINDING-" + domain + "-END" + tmp = re.search(rep, conf).group() + dirConf = tmp.replace('rewrite/' + site['name'] + '.conf;', + 'rewrite/' + site['name'] + '_' + find['path'] + '.conf;') + conf = conf.replace(tmp, dirConf) + public.writeFile(file, conf) + data = {} + return_status = -1 + if os.path.exists(filename): + return_status = 0 + data['data'] = public.readFile(filename) + data['rlist'] = ['0.default'] + webserver = public.get_webserver() + if webserver == "openlitespeed": + webserver = "apache" + for ds in os.listdir('rewrite/' + webserver): + if ds == 'list.txt': continue + data['rlist'].append(ds[0:len(ds) - 5]) + data['filename'] = filename + return public.return_message(return_status, 0, data) + + # 取默认文档 + def GetIndex(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + Name = public.M('sites').where("id=?", (id,)).getField('name') + file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + Name + '.conf' + if public.get_webserver() == 'openlitespeed': + file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/detail/' + Name + '.conf' + conf = public.readFile(file) + if conf == False: return public.return_message(-1, 0, 'Configuration file not exist') + if public.get_webserver() == 'nginx': + rep = r"\s+index\s+(.+);" + elif public.get_webserver() == 'apache': + rep = "DirectoryIndex\\s+(.+)\n" + else: + rep = "indexFiles\\s+(.+)\n" + if re.search(rep, conf): + tmp = re.search(rep, conf).groups() + if public.get_webserver() == 'openlitespeed': + return public.return_message(0, 0, tmp[0]) + return public.return_message(0, 0, tmp[0].replace(' ', ',')) + return public.return_message(-1, 0, 'Failed to get, there is no default document in the configuration file') + + # 设置默认文档 + def SetIndex(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + Param('Index').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + + Index = get.Index.replace(' ', '') + Index = Index.replace(',,', ',').strip() + if not Index: return public.return_message(-1, 0, "Default index file cannot be empty") + if get.Index.find('.') == -1: return public.return_message(-1, 0, + 'Default Document Format is invalid, e.g., index.html') + + if len(Index) < 3: return public.return_message(-1, 0, 'Default Document cannot be empty!') + + Name = public.M('sites').where("id=?", (id,)).getField('name') + # 准备指令 + Index_L = Index.replace(",", " ") + + # nginx + file = self.setupPath + '/panel/vhost/nginx/' + Name + '.conf' + conf = public.readFile(file) + if conf: + rep = r"\s+index\s+.+;" + conf = re.sub(rep, "\n\tindex " + Index_L + ";", conf) + public.writeFile(file, conf) + + # apache + file = self.setupPath + '/panel/vhost/apache/' + Name + '.conf' + conf = public.readFile(file) + if conf: + rep = "DirectoryIndex\\s+.+\n" + conf = re.sub(rep, 'DirectoryIndex ' + Index_L + "\n", conf) + public.writeFile(file, conf) + + # openlitespeed + file = self.setupPath + '/panel/vhost/openlitespeed/detail/' + Name + '.conf' + conf = public.readFile(file) + if conf: + rep = "indexFiles\\s+.+\n" + Index = Index.split(',') + Index = [i for i in Index if i] + Index = ",".join(Index) + conf = re.sub(rep, 'indexFiles ' + Index + "\n", conf) + public.writeFile(file, conf) + + public.serviceReload() + public.write_log_gettext('Site manager', 'Defualt document of site [{}] is [{}]', (Name, Index_L)) + return public.return_message(0, 0, 'Setup successfully!') + + # 修改物理路径 + def SetPath(self, get): + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('id').Integer(), + Param('path').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + Path = self.GetPath(get.path) + if Path == "" or id == '0': return public.return_message(-1, 0, 'Directory cannot be empty!') + + if not self.__check_site_path(Path): return public.return_message(-1, 0, + 'System critical directory cannot be used as site directory') + if not public.check_site_path(Path): + a, c = public.get_sys_path() + return public.return_message(-1, 0, + 'Please do not set the website root directory to the system main directory:
                                    {}'.format( + "
                                    ".join(a + c))) + + SiteFind = public.M("sites").where("id=?", (id,)).field('path,name').find() + if SiteFind["path"] == Path: return public.return_msg_gettext(False, + 'Same as original path, no need to change!') + Name = SiteFind['name'] + file = self.setupPath + '/panel/vhost/nginx/' + Name + '.conf' + conf = public.readFile(file) + if conf: + conf = conf.replace(SiteFind['path'], Path) + public.writeFile(file, conf) + + file = self.setupPath + '/panel/vhost/apache/' + Name + '.conf' + conf = public.readFile(file) + if conf: + rep = "DocumentRoot\\s+.+\n" + conf = re.sub(rep, 'DocumentRoot "' + Path + '"\n', conf) + rep = "\n", conf) + public.writeFile(file, conf) + + # OLS + file = self.setupPath + '/panel/vhost/openlitespeed/' + Name + '.conf' + conf = public.readFile(file) + if conf: + reg = 'vhRoot.*' + conf = re.sub(reg, 'vhRoot ' + Path, conf) + public.writeFile(file, conf) + + # 创建basedir + userIni = Path + '/.user.ini' + if os.path.exists(userIni): public.ExecShell("chattr -i " + userIni) + public.writeFile(userIni, 'open_basedir=' + Path + '/:/tmp/') + public.ExecShell('chmod 644 ' + userIni) + public.ExecShell('chown root:root ' + userIni) + public.ExecShell('chattr +i ' + userIni) + public.set_site_open_basedir_nginx(Name) + + public.serviceReload() + public.M("sites").where("id=?", (id,)).setField('path', Path) + public.write_log_gettext('Site manager', 'Successfully changed directory of site [{}]!', (Name,)) + self.CheckRunPathExists(id) + return public.return_message(0, 0, 'Successfully set') + + def CheckRunPathExists(self, site_id): + ''' + @name 检查站点运行目录是否存在 + @author hwliang + @param site_id int 站点ID + @return bool + ''' + + site_info = public.M('sites').where('id=?', (site_id,)).field('name,path').find() + if not site_info: return False + args = public.dict_obj() + args.id = site_id + run_path = self.GetRunPath(args)['message']['result'] + site_run_path = site_info['path'] + '/' + run_path + if os.path.exists(site_run_path): return True + args.runPath = '/' + self.SetSiteRunPath(args) + public.WriteLog('TYPE_SITE', + 'Due to modifying the root directory of the website [{}], the original running directory [.{}] does not exist, and the directory has been automatically switched to [./]'.format( + site_info['name'], run_path)) + return False + + # 取当前可用PHP版本 + def GetPHPVersion(self, get, is_http=True): + # 校验参数--无参数,暂不需要添加校验 + + phpVersions = public.get_php_versions() + phpVersions.insert(0, 'other') + phpVersions.insert(0, '00') + httpdVersion = "" + filename = self.setupPath + '/apache/version.pl' + if os.path.exists(filename): httpdVersion = public.readFile(filename).strip() + + if httpdVersion == '2.2': phpVersions = ('00', '52', '53', '54') + if httpdVersion == '2.4': + if '52' in phpVersions: phpVersions.remove('52') + if os.path.exists('/www/server/nginx/sbin/nginx'): + cfile = '/www/server/nginx/conf/enable-php-00.conf' + if not os.path.exists(cfile): public.writeFile(cfile, '') + + s_type = getattr(get, 's_type', 0) + data = [] + for val in phpVersions: + tmp = {} + checkPath = self.setupPath + '/php/' + val + '/bin/php' + if val in ['00', 'other']: checkPath = '/etc/init.d/bt' + if httpdVersion == '2.2': checkPath = self.setupPath + '/php/' + val + '/libphp5.so' + if os.path.exists(checkPath): + tmp['version'] = val + tmp['name'] = 'PHP-' + val + if val == '00': + tmp['name'] = public.get_msg_gettext('Static') + + if val == 'other': + if s_type: + tmp['name'] = 'Customize' + else: + continue + data.append(tmp) + if is_http: + return public.return_message(0, 0, data) + return data + + # 取指定站点的PHP版本 + def GetSitePHPVersion(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)) + + try: + siteName = get.siteName + data = {} + data['phpversion'] = public.get_site_php_version(siteName) + conf = public.readFile(self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + siteName + '.conf') + data['tomcat'] = conf.find('#TOMCAT-START') + data['tomcatversion'] = public.readFile(self.setupPath + '/tomcat/version.pl') + data['nodejsversion'] = public.readFile(self.setupPath + '/node.js/version.pl') + data['php_other'] = '' + if data['phpversion'] == 'other': + other_file = '/www/server/panel/vhost/other_php/{}/enable-php-other.conf'.format(siteName) + if os.path.exists(other_file): + conf = public.readFile(other_file) + data['php_other'] = re.findall(r"fastcgi_pass\s+(.+);", conf)[0] + return public.return_message(0, 0, data) + except: + return public.return_message(-1, 0, + 'Apache2.2 does NOT support MultiPHP!,{}'.format(public.get_error_info())) + + def set_site_php_version_multiple(self, get): + ''' + @name 批量设置PHP版本 + @author zhwen<2020-11-17> + @param sites_id "1,2" + @param version 52...74 + ''' + # 校验参数 + try: + get.validate([ + Param('version').String(), + Param('sites_id').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + sites_id = get.sites_id.split(',') + set_phpv_successfully = [] + set_phpv_failed = {} + for site_id in sites_id: + get.id = site_id + get.siteName = public.M('sites').where("id=?", (site_id,)).getField('name') + if not get.siteName: + continue + try: + result = self.SetPHPVersion(get, multiple=1) + if result['status'] == -1: + set_phpv_failed[get.siteName] = result['message'] + continue + set_phpv_successfully.append(get.siteName) + except: + set_phpv_failed[get.siteName] = public.get_msg_gettext('There was an error setting, please try again.') + pass + public.serviceReload() + return_message = {'msg': public.get_msg_gettext( + 'Set up website [{}] PHP version successfully'.format(','.join(set_phpv_successfully), )), + 'error': set_phpv_failed, + 'success': set_phpv_successfully} + return public.return_message(0, 0, return_message) + + # 设置指定站点的PHP版本 + def SetPHPVersion(self, get, multiple=None): + # 校验参数 + try: + get.validate([ + Param('siteName').String(), + Param('version').String(), + Param('other').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + siteName = get.siteName + version = get.version + if version == 'other' and not public.get_webserver() in ['nginx', 'tengine']: + return public.return_message(-1, 0, 'Custom PHP configuration only supports Nginx') + try: + # nginx + file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + conf = public.readFile(file) + if conf: + wp00 = "/www/server/nginx/conf/enable-php-00-wpfastcgi.conf" + if not os.path.exists(wp00): + public.writeFile(wp00, '') + other_path = '/www/server/panel/vhost/other_php/{}'.format(siteName) + if not os.path.exists(other_path): os.makedirs(other_path) + other_rep = "{}/enable-php-other.conf".format(other_path) + + if version == 'other': + dst = other_rep + get.other = get.other.strip() + + if not get.other: + return public.return_message(-1, 0, + 'The PHP connection configuration cannot be empty when customizing the version!') + + if not re.match(r"^(\d+\.\d+\.\d+\.\d+:\d+|unix:[\w/\.-]+)$", get.other): + return public.return_message(-1, 0, + 'The PHP connection configuration format is incorrect, please refer to the example!') + + other_tmp = get.other.split(':') + if other_tmp[0] == 'unix': + if not os.path.exists(other_tmp[1]): + return public.return_message(-1, 0, 'The specified unix socket [{}] does not exist!'.format( + other_tmp[1])) + else: + if not public.check_tcp(other_tmp[0], int(other_tmp[1])): + return public.return_message(-1, 0, + 'Unable to connect to [{}], please check whether the machine can connect to the target server'.format( + get.other)) + + other_conf = r'''location ~ [^/]\.php(/|$) +{{ + try_files $uri =404; + fastcgi_pass {}; + fastcgi_index index.php; + include fastcgi.conf; + include pathinfo.conf; +}}'''.format(get.other) + public.writeFile(other_rep, other_conf) + conf = conf.replace(other_rep, dst) + rep = r"include\s+enable-php-(\w{2,5})\.conf" + tmp = re.search(rep, conf) + if tmp: conf = conf.replace(tmp.group(), 'include ' + dst) + elif re.search(r"enable-php-\d+-wpfastcgi.conf", conf): + dst = 'enable-php-{}-wpfastcgi.conf'.format(version) + conf = conf.replace(other_rep, dst) + rep = r"enable-php-\d+-wpfastcgi.conf" + tmp = re.search(rep, conf) + if tmp: conf = conf.replace(tmp.group(), dst) + else: + dst = 'enable-php-' + version + '.conf' + conf = conf.replace(other_rep, dst) + rep = r"enable-php-(\w{2,5})\.conf" + tmp = re.search(rep, conf) + if tmp: conf = conf.replace(tmp.group(), dst) + public.writeFile(file, conf) + try: + import site_dir_auth_v2 as site_dir_auth + site_dir_auth_module = site_dir_auth.SiteDirAuth() + auth_list = site_dir_auth_module.get_dir_auth(get) + if auth_list: + for i in auth_list[siteName]: + auth_name = i['name'] + auth_file = "{setup_path}/panel/vhost/nginx/dir_auth/{site_name}/{auth_name}.conf".format( + setup_path=self.setupPath, site_name=siteName, auth_name=auth_name) + if os.path.exists(auth_file): + site_dir_auth_module.change_dir_auth_file_nginx_phpver(siteName, version, auth_name) + except: + pass + + # apache + file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + conf = public.readFile(file) + if conf and version != 'other': + rep = r"(unix:/tmp/php-cgi-(\w{2,5})\.sock\|fcgi://localhost|fcgi://127.0.0.1:\d+)" + tmp = re.search(rep, conf).group() + conf = conf.replace(tmp, public.get_php_proxy(version, 'apache')) + public.writeFile(file, conf) + # OLS + if version != 'other': + file = self.setupPath + '/panel/vhost/openlitespeed/detail/' + siteName + '.conf' + conf = public.readFile(file) + if conf: + rep = r'lsphp\d+' + tmp = re.search(rep, conf) + if tmp: + conf = conf.replace(tmp.group(), 'lsphp' + version) + public.writeFile(file, conf) + if not multiple: + public.serviceReload() + public.write_log_gettext("Site manager", + 'Successfully changed PHP Version of site [{}] to PHP-{}'.format(siteName, + version)) + return public.return_message(0, 0, + 'Successfully changed PHP Version of site [{}] to PHP-{}'.format(siteName, + version)) + except: + return public.get_error_info() + return public.return_message(-1, 0, + 'Setup failed, no enable-php-xx related configuration items were found in the website configuration file!') + + # 是否开启目录防御 + def GetDirUserINI(self, get): + # 校验参数 + try: + get.validate([ + Param('path').String(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + path = get.path + self.GetRunPath(get)['message']['result'] + if not path: return public.return_message(-1, 0, 'Requested directory does not exist') + id = get.id + get.name = public.M('sites').where("id=?", (id,)).getField('name') + data = {} + data['logs'] = self.GetLogsStatus(get)['message'] + data['userini'] = False + user_ini_file = path + '/.user.ini' + user_ini_conf = public.readFile(user_ini_file) + if user_ini_conf and "open_basedir" in user_ini_conf: + data['userini'] = True + tmp_run_path = self.GetSiteRunPath(get) + if "result" in tmp_run_path: + data['runPath'] = tmp_run_path['message']['result'] + else: + data['runPath'] = tmp_run_path['message'] + data['pass'] = self.GetHasPwd(get)['message'] + return public.return_message(0, 0, data) + + # 清除多余user.ini + def DelUserInI(self, path, up=0): + useriniPath = path + '/.user.ini' + if os.path.exists(useriniPath): + public.ExecShell('chattr -i ' + useriniPath) + try: + os.remove(useriniPath) + except: + pass + + for p1 in os.listdir(path): + try: + npath = path + '/' + p1 + if not os.path.isdir(npath): continue + useriniPath = npath + '/.user.ini' + if os.path.exists(useriniPath): + public.ExecShell('chattr -i ' + useriniPath) + os.remove(useriniPath) + if up < 3: self.DelUserInI(npath, up + 1) + except: + continue + return True + + # 设置目录防御 + def SetDirUserINI(self, get): + # 校验参数 + try: + get.validate([ + Param('path').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + path = get.path + runPath = self.GetRunPath(get)['message']['result'] + filename = path + runPath + '/.user.ini' + siteName = public.M('sites').where('path=?', (get.path,)).getField('name') + conf = public.readFile(filename) + try: + self._set_ols_open_basedir(get) + public.ExecShell("chattr -i " + filename) + if conf and "open_basedir" in conf: + rep = "\n*open_basedir.*" + conf = re.sub(rep, "", conf) + if not conf: + os.remove(filename) + else: + public.writeFile(filename, conf) + public.ExecShell("chattr +i " + filename) + public.set_site_open_basedir_nginx(siteName) + return public.return_message(0, 0, 'Base directory turned off!') + + if conf and "session.save_path" in conf: + rep = r"session.save_path\s*=\s*(.*)" + s_path = re.search(rep, conf).groups(1)[0] + public.writeFile(filename, conf + '\nopen_basedir={}/:/tmp/:{}'.format(path, s_path)) + else: + public.writeFile(filename, 'open_basedir={}/:/tmp/'.format(path)) + public.ExecShell("chattr +i " + filename) + public.set_site_open_basedir_nginx(siteName) + public.serviceReload() + return public.return_message(0, 0, 'Base directory turned on!') + except Exception as e: + public.ExecShell("chattr +i " + filename) + return public.return_message(-1, 0, str(e)) + + def _set_ols_open_basedir(self, get): + # 设置ols + try: + sitename = public.M('sites').where("id=?", (get.id,)).getField('name') + # sitename = path.split('/')[-1] + f = "/www/server/panel/vhost/openlitespeed/detail/{}.conf".format(sitename) + c = public.readFile(f) + if not c: return public.return_message(-1, 0, "") + if f: + rep = '\nphp_admin_value\\s*open_basedir.*' + result = re.search(rep, c) + s = 'on' + if not result: + s = 'off' + rep = '\n#php_admin_value\\s*open_basedir.*' + result = re.search(rep, c) + result = result.group() + if s == 'on': + c = re.sub(rep, '\n#' + result[1:], c) + else: + result = result.replace('#', '') + c = re.sub(rep, result, c) + public.writeFile(f, c) + except: + pass + return public.return_message(0, 0, "") + + # 读配置 + def __read_config(self, path): + if not os.path.exists(path): + public.writeFile(path, '[]') + upBody = public.readFile(path) + if not upBody: upBody = '[]' + return json.loads(upBody) + + # 写配置 + + def __write_config(self, path, data): + return public.writeFile(path, json.dumps(data)) + + # 取某个站点某条反向代理详情 + + def GetProxyDetals(self, get): + proxyUrl = self.__read_config(self.__proxyfile) + sitename = get.sitename + proxyname = get.proxyname + for i in proxyUrl: + if i["proxyname"] == proxyname and i["sitename"] == sitename: + return public.return_message(0, 0, i) + + # 取某个站点反向代理列表 + def GetProxyList(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)) + + n = 0 + for w in ["nginx", "apache"]: + conf_path = "%s/panel/vhost/%s/%s.conf" % (self.setupPath, w, get.sitename) + old_conf = "" + if os.path.exists(conf_path): + old_conf = public.readFile(conf_path) + rep = "(#PROXY-START(\n|.)+#PROXY-END)" + url_rep = r"proxy_pass (.*);|ProxyPass\s/\s(.*)|Host\s(.*);" + host_rep = r"Host\s(.*);" + if re.search(rep, old_conf): + # 构造代理配置 + if w == "nginx": + get.todomain = str(re.search(host_rep, old_conf).group(1)) + get.proxysite = str(re.search(url_rep, old_conf).group(1)) + else: + get.todomain = "" + get.proxysite = str(re.search(url_rep, old_conf).group(2)) + get.proxyname = public.get_msg_gettext('Old proxy') + get.type = 1 + get.proxydir = "/" + get.advanced = 0 + get.cachetime = 1 + get.cache = 0 + get.subfilter = "[{\"sub1\":\"\",\"sub2\":\"\"},{\"sub1\":\"\",\"sub2\":\"\"},{\"sub1\":\"\",\"sub2\":\"\"}]" + + # proxyname_md5 = self.__calc_md5(get.proxyname) + # 备份并替换老虚拟主机配置文件 + public.ExecShell("cp %s %s_bak" % (conf_path, conf_path)) + conf = re.sub(rep, "", old_conf) + public.writeFile(conf_path, conf) + if n == 0: + self.CreateProxy(get) + n += 1 + # 写入代理配置 + # proxypath = "%s/panel/vhost/%s/proxy/%s/%s_%s.conf" % ( + # self.setupPath, w, get.sitename, proxyname_md5, get.sitename) + # proxycontent = str(re.search(rep, old_conf).group(1)) + # public.writeFile(proxypath, proxycontent) + if n == "1": + public.serviceReload() + proxyUrl = self.__read_config(self.__proxyfile) + sitename = get.sitename + proxylist = [] + for i in proxyUrl: + if i["sitename"] == sitename: + proxylist.append(i) + return public.return_message(0, 0, proxylist) + + def del_proxy_multiple(self, get): + ''' + @name 批量网站到期时间 + @author zhwen<2020-11-20> + @param site_id 1 + @param proxynames ces,aaa + ''' + proxynames = get.proxynames.split(',') + del_successfully = [] + del_failed = {} + get.sitename = public.M('sites').where("id=?", (get.site_id,)).getField('name') + for proxyname in proxynames: + if not proxyname: + continue + get.proxyname = proxyname + try: + resule = self.RemoveProxy(get, multiple=1) + if resule['status'] == -1: + del_failed[proxyname] = resule['msg'] + del_successfully.append(proxyname) + except: + del_failed[proxyname] = public.get_msg_gettext('There was an error deleting, please try again.') + pass + return_message = {'msg': public.get_msg_gettext('Delete [ {} ] proxy successfully', (','.join(del_failed),)), + 'error': del_failed, + 'success': del_successfully} + return public.return_message(0, 0, return_message) + + # 删除反向代理 + def RemoveProxy(self, get, multiple=None): + # 校验参数 + try: + get.validate([ + Param('proxyname').String(), + 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)) + + conf = self.__read_config(self.__proxyfile) + sitename = get.sitename + proxyname = get.proxyname + for i in range(len(conf)): + c_sitename = conf[i]["sitename"] + c_proxyname = conf[i]["proxyname"] + if c_sitename == sitename and c_proxyname == proxyname: + proxyname_md5 = self.__calc_md5(c_proxyname) + for w in ["apache", "nginx", "openlitespeed"]: + p = "{sp}/panel/vhost/{w}/proxy/{s}/{m}_{s}.conf*".format(sp=self.setupPath, w=w, s=c_sitename, + m=proxyname_md5) + + public.ExecShell('rm -f {}'.format(p)) + p = "{sp}/panel/vhost/openlitespeed/proxy/{s}/urlrewrite/{m}_{s}.conf*".format(sp=self.setupPath, + m=proxyname_md5, + s=get.sitename) + public.ExecShell('rm -f {}'.format(p)) + del conf[i] + self.__write_config(self.__proxyfile, conf) + self.SetNginx(get) + self.SetApache(get.sitename) + if not multiple: + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Successfully deleted') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 检查代理是否存在 + def __check_even(self, get, action=""): + conf_data = self.__read_config(self.__proxyfile) + for i in conf_data: + if i["sitename"] == get.sitename: + if action == "create": + if i["proxydir"] == get.proxydir or i["proxyname"] == get.proxyname: + return public.return_message(0, 0, i) + else: + if i["proxyname"] != get.proxyname and i["proxydir"] == get.proxydir: + return public.return_message(0, 0, i) + + # 检测全局代理和目录代理是否同时存在 + def __check_proxy_even(self, get, action=""): + conf_data = self.__read_config(self.__proxyfile) + n = 0 + if action == "": + for i in conf_data: + if i["sitename"] == get.sitename: + n += 1 + if n == 1: + return public.return_message(0, 0, "") + for i in conf_data: + if i["sitename"] == get.sitename: + if i["advanced"] != int(get.advanced): + return public.return_message(-1, 0, i) + return public.return_message(0, 0, "") + + # 计算proxyname md5 + def __calc_md5(self, proxyname): + md5 = hashlib.md5() + md5.update(proxyname.encode('utf-8')) + return md5.hexdigest() + + # 检测URL是否可以访问 + def __CheckUrl(self, get): + sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sk.settimeout(5) + rep = r"(https?)://([\w\.\-]+):?([\d]+)?" + h = re.search(rep, get.proxysite).group(1) + d = re.search(rep, get.proxysite).group(2) + try: + p = re.search(rep, get.proxysite).group(3) + except: + p = "" + try: + if p: + sk.connect((d, int(p))) + else: + if h == "http": + sk.connect((d, 80)) + else: + sk.connect((d, 443)) + except: + return_message = public.return_msg_gettext(False, 'Can NOT get target URL') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + # 基本设置检查 + def __CheckStart(self, get, action=""): + isError = public.checkWebConfig() + if isinstance(isError, str): + if isError.find('/proxy/') == -1: # 如果是反向代理配置文件本身的错误,跳过 + return public.return_message(-1, 0, + 'An error was detected in the configuration file. Please solve it before proceeding') + if action == "create": + if sys.version_info.major < 3: + if len(get.proxyname) < 3 or len(get.proxyname) > 40: + return public.return_message(-1, 0, 'Database name cannot be more than 40 characters!') + else: + if len(get.proxyname.encode("utf-8")) < 3 or len(get.proxyname.encode("utf-8")) > 40: + return public.return_message(-1, 0, 'Database name cannot be more than 40 characters!') + if self.__check_even(get, action): + return public.return_message(-1, 0, 'Specified reverse proxy name or proxy folder already exists') + # 判断代理,只能有全局代理或目录代理 + check_proxy_even = self.__check_proxy_even(get, action) + if check_proxy_even['status'] == -1: + return public.return_message(-1, 0, 'Cannot set both directory and global proxies') + # 判断cachetime类型 + if get.cachetime: + try: + int(get.cachetime) + except: + return public.return_message(-1, 0, 'Please enter number') + + rep = r"http(s)?\:\/\/" + # repd = r"http(s)?\:\/\/([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+([a-zA-Z0-9][a-zA-Z0-9]{0,62})+.?" + tod = "[a-zA-Z]+$" + repte = "[\\?\\=\\[\\]\\)\\(\\*\\&\\^\\%\\$\\#\\@\\!\\~\\`{\\}\\>\\<\\,\',\"]+" + # 检测代理目录格式 + if re.search(repte, get.proxydir): + return public.return_message(-1, 0, "PROXY_DIR_ERR", ("?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]",)) + # 检测发送域名格式 + if get.todomain: + if re.search("[\\}\\{\\#\\;\"\']+", get.todomain): + return public.return_message(-1, 0, + 'Sent Domain format error :' + get.todomain + '
                                    The following special characters cannot exist [ } { # ; \" \' ] ') + if public.get_webserver() != 'openlitespeed' and not get.todomain: + get.todomain = "$host" + + # 检测目标URL格式 + if not re.match(rep, get.proxysite): + return public.return_message(-1, 0, 'Sent domain format ERROR {}', (get.proxysite,)) + if re.search(repte, get.proxysite): + return public.return_message(-1, 0, "PROXY_URL_ERR", ("?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]",)) + # 检测目标url是否可用 + # if re.match(repd, get.proxysite): + # if self.__CheckUrl(get): + # return public.returnMsg(False, "目标URL无法访问") + subfilter = json.loads(get.subfilter) + # 检测替换内容 + if subfilter: + for s in subfilter: + if not s["sub1"]: + if s["sub2"]: + return public.return_message(-1, 0, 'Please enter the content to be replaced') + elif s["sub1"] == s["sub2"]: + return public.return_message(-1, 0, + 'The content to replace cannot be the same as the content to be replaced') + + # 设置Nginx配置 + def SetNginx(self, get): + ng_proxyfile = "%s/panel/vhost/nginx/proxy/%s/*.conf" % (self.setupPath, get.sitename) + ng_file = self.setupPath + "/panel/vhost/nginx/" + get.sitename + ".conf" + p_conf = self.__read_config(self.__proxyfile) + cureCache = '' + + if public.get_webserver() == 'nginx': + shutil.copyfile(ng_file, '/tmp/ng_file_bk.conf') + + # if os.path.exists('/www/server/nginx/src/ngx_cache_purge'): + cureCache += ''' + location ~ /purge(/.*) { + proxy_cache_purge cache_one $host$1$is_args$args; + #access_log /www/wwwlogs/%s_purge_cache.log; + }''' % (get.sitename) + if os.path.exists(ng_file): + self.CheckProxy(get) + ng_conf = public.readFile(ng_file) + if not p_conf: + # rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.{1,66}[\\s\\w\\/\\*\\.\\;]+include enable-php-" % public.GetMsg( + # "CLEAR_CACHE") + rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.*\n.*" % public.get_msg_gettext("#Clear cache") + # ng_conf = re.sub(rep, 'include enable-php-', ng_conf) + ng_conf = re.sub(rep, '', ng_conf) + oldconf = '''location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + error_log /dev/null; + access_log /dev/null; + } + location ~ .*\\.(js|css)?$ + { + expires 12h; + error_log /dev/null; + access_log /dev/null; + }''' + if "(gif|jpg|jpeg|png|bmp|swf)$" not in ng_conf: + ng_conf = re.sub(r'access_log\s*/www', oldconf + "\n\taccess_log /www", ng_conf) + public.writeFile(ng_file, ng_conf) + return public.return_message(0, 0, "") + sitenamelist = [] + for i in p_conf: + sitenamelist.append(i["sitename"]) + + if get.sitename in sitenamelist: + rep = r"include.*\/proxy\/.*\*.conf;" + if not re.search(rep, ng_conf): + rep = "location.+\\(gif[\\w\\|\\$\\(\\)\n\\{\\}\\s\\;\\/\\~\\.\\*\\\\\\?]+access_log\\s+/" + ng_conf = re.sub(rep, 'access_log /', ng_conf) + ng_conf = ng_conf.replace("include enable-php-", "%s\n" % public.get_msg_gettext( + "#Clear cache") + cureCache + "\n\t%s\n\t" % public.get_msg_gettext( + "#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid") + "include " + ng_proxyfile + ";\n\n\tinclude enable-php-") + public.writeFile(ng_file, ng_conf) + + else: + # rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.{1,66}[\\s\\w\\/\\*\\.\\;]+include enable-php-" % public.GetMsg( + # "CLEAR_CACHE") + rep = "%s[\\w\\s\\~\\/\\(\\)\\.\\*\\{\\}\\;\\$\n\\#]+.*\n.*" % public.get_msg_gettext("#Clear cache") + # ng_conf = re.sub(rep, 'include enable-php-', ng_conf) + ng_conf = re.sub(rep, '', ng_conf) + oldconf = '''location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + error_log /dev/null; + access_log /dev/null; + } + location ~ .*\\.(js|css)?$ + { + expires 12h; + error_log /dev/null; + access_log /dev/null; + }''' + if "(gif|jpg|jpeg|png|bmp|swf)$" not in ng_conf: + ng_conf = re.sub(r'access_log\s*/www', oldconf + "\n\taccess_log /www", ng_conf) + public.writeFile(ng_file, ng_conf) + return public.return_message(0, 0, "") + + # 设置apache配置 + + def SetApache(self, sitename): + ap_proxyfile = "%s/panel/vhost/apache/proxy/%s/*.conf" % (self.setupPath, sitename) + ap_file = self.setupPath + "/panel/vhost/apache/" + sitename + ".conf" + p_conf = public.readFile(self.__proxyfile) + + if public.get_webserver() == 'apache': + shutil.copyfile(ap_file, '/tmp/ap_file_bk.conf') + + if os.path.exists(ap_file): + ap_conf = public.readFile(ap_file) + if p_conf == "[]": + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext( + '#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') + ap_conf = re.sub(rep, '', ap_conf) + public.writeFile(ap_file, ap_conf) + return + if sitename in p_conf: + rep = "combined(\n|.)+IncludeOptional.*\\/proxy\\/.*conf" + rep1 = "combined" + if not re.search(rep, ap_conf): + ap_conf = ap_conf.replace(rep1, rep1 + "\n\t%s\n\t" % public.get_msg_gettext( + '#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') + "\n\tIncludeOptional " + ap_proxyfile) + public.writeFile(ap_file, ap_conf) + else: + # rep = "\n*#引用反向代理(\n|.)+IncludeOptional.*\\/proxy\\/.*conf" + rep = "\n*%s\n+\\s+IncludeOptiona[\\s\\w\\/\\.\\*]+" % public.get_msg_gettext( + '#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid') + ap_conf = re.sub(rep, '', ap_conf) + public.writeFile(ap_file, ap_conf) + + # 设置OLS + def _set_ols_proxy(self, get): + # 添加反代配置 + proxyname_md5 = self.__calc_md5(get.proxyname) + dir_path = "%s/panel/vhost/openlitespeed/proxy/%s/" % (self.setupPath, get.sitename) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + file_path = "{}{}_{}.conf".format(dir_path, proxyname_md5, get.sitename) + reverse_proxy_conf = """ +extprocessor %s { + type proxy + address %s + maxConns 1000 + pcKeepAliveTimeout 600 + initTimeout 600 + retryTimeout 0 + respBuffer 0 +} +""" % (get.proxyname, get.proxysite) + public.writeFile(file_path, reverse_proxy_conf) + # 添加urlrewrite + dir_path = "%s/panel/vhost/openlitespeed/proxy/%s/urlrewrite/" % (self.setupPath, get.sitename) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + file_path = "{}{}_{}.conf".format(dir_path, proxyname_md5, get.sitename) + reverse_urlrewrite_conf = """ +RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] +""" % (get.proxydir, get.proxyname, get.todomain) + public.writeFile(file_path, reverse_urlrewrite_conf) + return public.return_message(0, 0, "") + + # 检查伪静态、主配置文件是否有location冲突 + def CheckLocation(self, get): + # 伪静态文件路径 + rewriteconfpath = "%s/panel/vhost/rewrite/%s.conf" % (self.setupPath, get.sitename) + # 主配置文件路径 + nginxconfpath = "%s/nginx/conf/nginx.conf" % (self.setupPath) + # vhost文件 + vhostpath = "%s/panel/vhost/nginx/%s.conf" % (self.setupPath, get.sitename) + + rep = "location\\s+/[\n\\s]+{" + + for i in [rewriteconfpath, nginxconfpath, vhostpath]: + conf = public.readFile(i) + if re.findall(rep, conf): + return public.return_message(-1, 0, + 'A global reverse proxy already exists in the rewrite/nginx master configuration/vhost file') + return public.return_message(0, 0, "") + + # 创建反向代理 + def CreateProxy(self, get): + # 校验参数 + try: + get.validate([ + Param('proxyname').String(), + Param('proxydir').String(), + Param('proxysite').String(), + Param('todomain').String(), + Param('sitename').String(), + Param('subfilter').String(), + Param('type').Integer(), + Param('cache').Integer(), + Param('advanced').Integer(), + Param('cachetime').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + nocheck = get.nocheck + except: + nocheck = "" + if not get.get('proxysite', None): + return public.return_message(-1, 0, 'Destination URL cannot be empty') + if not nocheck: + if self.__CheckStart(get, "create"): + return self.__CheckStart(get, "create") + if public.get_webserver() == 'nginx': + if self.CheckLocation(get)['message']['result']: + return self.CheckLocation(get) + if not get.proxysite.split('//')[-1]: + return public.return_message(-1, 0, + 'The target URL cannot be [http:// or https://], please fill in the full URL, such as: https://aapanel.com') + # project_type = public.M('sites').where('name=?', (get.sitename,)).field('project_type').find()['project_type'] + # if project_type == 'WP': + # return public.return_msg_gettext(False,'Reverse proxies are not currently available for Wordpress sites that use one-click deployment') + proxyUrl = self.__read_config(self.__proxyfile) + proxyUrl.append({ + "proxyname": get.proxyname, + "sitename": get.sitename, + "proxydir": get.proxydir, + "proxysite": get.proxysite, + "todomain": get.todomain, + "type": int(get.type), + "cache": int(get.cache), + "subfilter": json.loads(get.subfilter), + "advanced": int(get.advanced), + "cachetime": int(get.cachetime) + }) + self.__write_config(self.__proxyfile, proxyUrl) + self.SetNginx(get) + self.SetApache(get.sitename) + self._set_ols_proxy(get) + status = self.SetProxy(get) + if status["status"] == -1: + return status + if get.proxydir == '/': + get.version = '00' + get.siteName = get.sitename + self.SetPHPVersion(get) + public.serviceReload() + return public.return_message(0, 0, 'Setup successfully!') + + # 取代理配置文件 + def GetProxyFile(self, get): + # 校验参数 + try: + get.validate([ + Param('proxyname').String(), + Param('sitename').String(), + Param('webserver').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import files_v2 as files + conf = self.__read_config(self.__proxyfile) + sitename = get.sitename + proxyname = get.proxyname + proxyname_md5 = self.__calc_md5(proxyname) + get.path = "%s/panel/vhost/%s/proxy/%s/%s_%s.conf" % ( + self.setupPath, get.webserver, sitename, proxyname_md5, sitename) + for i in conf: + if proxyname == i["proxyname"] and sitename == i["sitename"] and i["type"] != 1: + return public.return_message(-1, 0, 'Proxy suspended') + f = files.files() + return_message = f.GetFileBody(get) + return_message['message']['file'] = get.path + return return_message + + # 保存代理配置文件 + def SaveProxyFile(self, get): + # 校验参数 + try: + get.validate([ + Param('path').String(), + Param('data').String(), + Param('encoding').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import files_v2 as files + f = files.files() + return f.SaveFileBody(get) + # return public.returnMsg(True, '保存成功') + + # 检查是否存在#Set Nginx Cache + def check_annotate(self, data): + rep = "\n\\s*#Set\\s*Nginx\\s*Cache" + if re.search(rep, data): + return True + + def old_proxy_conf(self, conf, ng_conf_file, get): + rep = r'location\s*\~\*.*gif\|png\|jpg\|css\|js\|woff\|woff2\)\$' + if not re.search(rep, conf): + return public.return_message(0, 0, conf) + + self.RemoveProxy(get) + self.CreateProxy(get) + return public.return_message(0, 0, public.readFile(ng_conf_file)) + + # 修改反向代理 + def ModifyProxy(self, get): + # 校验参数 + try: + get.validate([ + Param('proxyname').String(), + Param('proxydir').String(), + Param('proxysite').String(), + Param('todomain').String(), + Param('sitename').String(), + Param('subfilter').String(), + Param('type').Integer(), + Param('cache').Integer(), + Param('advanced').Integer(), + Param('cachetime').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + if not get.get('proxysite', None): + return public.return_message(-1, 0, 'Destination URL cannot be empty') + proxyname_md5 = self.__calc_md5(get.proxyname) + ap_conf_file = "{p}/panel/vhost/apache/proxy/{s}/{n}_{s}.conf".format( + p=self.setupPath, s=get.sitename, n=proxyname_md5) + ng_conf_file = "{p}/panel/vhost/nginx/proxy/{s}/{n}_{s}.conf".format( + p=self.setupPath, s=get.sitename, n=proxyname_md5) + ols_conf_file = "{p}/panel/vhost/openlitespeed/proxy/{s}/urlrewrite/{n}_{s}.conf".format( + p=self.setupPath, s=get.sitename, n=proxyname_md5) + if self.__CheckStart(get): + return self.__CheckStart(get) + conf = self.__read_config(self.__proxyfile) + random_string = public.GetRandomString(8) + for i in range(len(conf)): + if conf[i]["proxyname"] == get.proxyname and conf[i]["sitename"] == get.sitename: + if int(get.type) != 1: + if not os.path.exists(ng_conf_file): + return public.return_message(-1, 0, "Please enable the reverse proxy before editing!") + public.ExecShell("mv {f} {f}_bak".format(f=ap_conf_file)) + public.ExecShell("mv {f} {f}_bak".format(f=ng_conf_file)) + public.ExecShell("mv {f} {f}_bak".format(f=ols_conf_file)) + conf[i]["type"] = int(get.type) + self.__write_config(self.__proxyfile, conf) + public.serviceReload() + return public.return_message(0, 0, 'Setup successfully!') + else: + if os.path.exists(ap_conf_file + "_bak"): + public.ExecShell("mv {f}_bak {f}".format(f=ap_conf_file)) + public.ExecShell("mv {f}_bak {f}".format(f=ng_conf_file)) + public.ExecShell("mv {f}_bak {f}".format(f=ols_conf_file)) + ng_conf = public.readFile(ng_conf_file) + ng_conf = self.old_proxy_conf(ng_conf, ng_conf_file, get)['message']['result'] + # 修改nginx配置 + # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri + php_pass_proxy = get.proxysite + if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: + php_pass_proxy = re.search(r'(https?\:\/\/[\w\.]+)', get.proxysite).group(0) + ng_conf = re.sub(r"location\s+[\^\~]*\s?%s" % conf[i]["proxydir"], "location ^~ " + get.proxydir, + ng_conf) + ng_conf = re.sub(r"proxy_pass\s+%s" % conf[i]["proxysite"], "proxy_pass " + get.proxysite, ng_conf) + ng_conf = re.sub("location\\s+\\~\\*\\s+\\\\.\\(php.*\n\\{\\s*proxy_pass\\s+%s.*" % (php_pass_proxy), + "location ~* \\.(php|jsp|cgi|asp|aspx)$\n{\n\tproxy_pass %s;" % php_pass_proxy, + ng_conf) + ng_conf = re.sub("location\\s+\\~\\*\\s+\\\\.\\(gif.*\n\\{\\s*proxy_pass\\s+%s.*" % (php_pass_proxy), + "location ~* \\.(gif|png|jpg|css|js|woff|woff2)$\n{\n\tproxy_pass %s;" % php_pass_proxy, + ng_conf) + + backslash = "" + if "Host $host" in ng_conf: + backslash = "\\" + ng_conf = re.sub(r"\sHost\s+%s" % backslash + conf[i]["todomain"], " Host " + get.todomain, ng_conf) + cache_rep = r"proxy_cache_valid\s+200\s+304\s+301\s+302\s+\d+m;((\n|.)+expires\s+\d+m;)*" + if int(get.cache) == 1: + if re.search(cache_rep, ng_conf): + expires_rep = "\\{\n\\s+expires\\s+12h;" + ng_conf = re.sub(expires_rep, "{", ng_conf) + ng_conf = re.sub(cache_rep, "proxy_cache_valid 200 304 301 302 {0}m;".format(get.cachetime), + ng_conf) + else: + # ng_cache = """ + # proxy_ignore_headers Set-Cookie Cache-Control expires; + # proxy_cache cache_one; + # proxy_cache_key $host$uri$is_args$args; + # proxy_cache_valid 200 304 301 302 %sm;""" % (get.cachetime) + ng_cache = r""" + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + expires 1m; + } + proxy_ignore_headers Set-Cookie Cache-Control expires; + proxy_cache cache_one; + proxy_cache_key $host$uri$is_args$args; + proxy_cache_valid 200 304 301 302 %sm;""" % (get.cachetime) + if self.check_annotate(ng_conf): + cache_rep = '\n\\s*#Set\\s*Nginx\\s*Cache(.|\n)*no-cache;\\s*\n*\\s*\\}' + ng_conf = re.sub(cache_rep, '\n\t#Set Nginx Cache\n' + ng_cache, ng_conf) + else: + # cache_rep = r'#proxy_set_header\s+Connection\s+"upgrade";' + cache_rep = r"proxy_set_header\s+REMOTE-HOST\s+\$remote_addr;" + ng_conf = re.sub(cache_rep, + r"\n\tproxy_set_header\s+REMOTE-HOST\s+\$remote_addr;\n\t#Set Nginx Cache" + ng_cache, + ng_conf) + else: + no_cache = r""" + #Set Nginx Cache + set $static_file%s 0; + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + set $static_file%s 1; + expires 1m; + } + if ( $static_file%s = 0 ) + { + add_header Cache-Control no-cache; + } +} +#PROXY-END/""" % (random_string, random_string, random_string) + if self.check_annotate(ng_conf): + rep = r'\n\s*#Set\s*Nginx\s*Cache(.|\n)*' + # ng_conf = re.sub(rep, + # "\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", + # ng_conf) + ng_conf = re.sub(rep, no_cache, ng_conf) + else: + rep = r"\s+proxy_cache\s+cache_one.*[\n\s\w\_\";\$]+m;" + # ng_conf = re.sub(rep, + # r"\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", + # ng_conf) + ng_conf = re.sub(rep, no_cache, ng_conf) + + sub_rep = "sub_filter" + subfilter = json.loads(get.subfilter) + if str(conf[i]["subfilter"]) != str(subfilter) or ng_conf.find('sub_filter_once') == -1: + if re.search(sub_rep, ng_conf): + sub_rep = "\\s+proxy_set_header\\s+Accept-Encoding(.|\n)+off;" + ng_conf = re.sub(sub_rep, "", ng_conf) + + # 构造替换字符串 + ng_subdata = '' + ng_sub_filter = ''' + proxy_set_header Accept-Encoding "";%s + sub_filter_once off;''' + if subfilter: + for s in subfilter: + if not s["sub1"]: + continue + if '"' in s["sub1"]: + s["sub1"] = s["sub1"].replace('"', '\\"') + if '"' in s["sub2"]: + s["sub2"] = s["sub2"].replace('"', '\\"') + ng_subdata += '\n\tsub_filter "%s" "%s";' % (s["sub1"], s["sub2"]) + if ng_subdata: + ng_sub_filter = ng_sub_filter % (ng_subdata) + else: + ng_sub_filter = '' + sub_rep = r'#Set\s+Nginx\s+Cache' + ng_conf = re.sub(sub_rep, '#Set Nginx Cache\n' + ng_sub_filter, ng_conf) + + # 修改apache配置 + ap_conf = public.readFile(ap_conf_file) + ap_conf = re.sub(r"ProxyPass\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]), + "ProxyPass %s %s" % (get.proxydir, get.proxysite), ap_conf) + ap_conf = re.sub(r"ProxyPassReverse\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]), + "ProxyPassReverse %s %s" % (get.proxydir, get.proxysite), ap_conf) + # 修改OLS配置 + p = "{p}/panel/vhost/openlitespeed/proxy/{s}/{n}_{s}.conf".format(p=self.setupPath, n=proxyname_md5, + s=get.sitename) + c = public.readFile(p) + if c: + rep = r'address\s+(.*)' + new_proxysite = 'address\t{}'.format(get.proxysite) + c = re.sub(rep, new_proxysite, c) + public.writeFile(p, c) + + # p = "{p}/panel/vhost/openlitespeed/proxy/{s}/urlrewrite/{n}_{s}.conf".format(p=self.setupPath,n=proxyname_md5,s=get.sitename) + c = public.readFile(ols_conf_file) + if c: + rep = r'RewriteRule\s*\^{}\(\.\*\)\$\s+http://{}/\$1\s*\[P,E=Proxy-Host:{}\]'.format( + conf[i]["proxydir"], get.proxyname, conf[i]["todomain"]) + new_content = 'RewriteRule ^{}(.*)$ http://{}/$1 [P,E=Proxy-Host:{}]'.format(get.proxydir, + get.proxyname, + get.todomain) + c = re.sub(rep, new_content, c) + public.writeFile(ols_conf_file, c) + + conf[i]["proxydir"] = get.proxydir + conf[i]["proxysite"] = get.proxysite + conf[i]["todomain"] = get.todomain + conf[i]["type"] = int(get.type) + conf[i]["cache"] = int(get.cache) + conf[i]["subfilter"] = json.loads(get.subfilter) + conf[i]["advanced"] = int(get.advanced) + conf[i]["cachetime"] = int(get.cachetime) + + public.writeFile(ng_conf_file, ng_conf) + public.writeFile(ap_conf_file, ap_conf) + self.__write_config(self.__proxyfile, conf) + self.SetNginx(get) + self.SetApache(get.sitename) + # self.SetProxy(get) + + # if int(get.type) != 1: + # os.system("mv %s %s_bak" % (ap_conf_file, ap_conf_file)) + # os.system("mv %s %s_bak" % (ng_conf_file, ng_conf_file)) + if not hasattr(get, 'notreload'): + public.serviceReload() + return public.return_message(0, 0, 'Setup successfully!') + + # 设置反向代理 + + def SetProxy(self, get): + sitename = get.sitename # 站点名称 + advanced = int(get.advanced) + type = int(get.type) + cache = int(get.cache) + cachetime = int(get.cachetime) + proxysite = get.proxysite + proxydir = get.proxydir + ng_file = self.setupPath + "/panel/vhost/nginx/" + sitename + ".conf" + ap_file = self.setupPath + "/panel/vhost/apache/" + sitename + ".conf" + p_conf = self.__read_config(self.__proxyfile) + random_string = public.GetRandomString(8) + + # websocket前置map + map_file = self.setupPath + "/panel/vhost/nginx/0.websocket.conf" + if not os.path.exists(map_file): + map_body = '''map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} +''' + public.writeFile(map_file, map_body) + + # 配置Nginx + # 构造清理缓存连接 + + # 构造缓存配置 + ng_cache = r""" + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + expires 1m; + } + proxy_ignore_headers Set-Cookie Cache-Control expires; + proxy_cache cache_one; + proxy_cache_key $host$uri$is_args$args; + proxy_cache_valid 200 304 301 302 %sm;""" % (cachetime) + no_cache = r""" + set $static_file%s 0; + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + set $static_file%s 1; + expires 1m; + } + if ( $static_file%s = 0 ) + { + add_header Cache-Control no-cache; + }""" % (random_string, random_string, random_string) + # rep = r"(https?://[\w\.]+)" + # proxysite1 = re.search(rep,get.proxysite).group(1) + ng_proxy = ''' +#PROXY-START%s + +location %s +{ + proxy_pass %s; + proxy_set_header Host %s; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_http_version 1.1; + # proxy_hide_header Upgrade; + %s + + add_header X-Cache $upstream_cache_status; + + #Set Nginx Cache + %s + %s +} + +#PROXY-END%s''' + ng_proxy_cache = '' + proxyname_md5 = self.__calc_md5(get.proxyname) + ng_proxyfile = "%s/panel/vhost/nginx/proxy/%s/%s_%s.conf" % (self.setupPath, sitename, proxyname_md5, sitename) + ng_proxydir = "%s/panel/vhost/nginx/proxy/%s" % (self.setupPath, sitename) + if not os.path.exists(ng_proxydir): + public.ExecShell("mkdir -p %s" % ng_proxydir) + + # 构造替换字符串 + ng_subdata = '' + ng_sub_filter = ''' + proxy_set_header Accept-Encoding "";%s + sub_filter_once off;''' + if get.subfilter: + for s in json.loads(get.subfilter): + if not s["sub1"]: + continue + if '"' in s["sub1"]: + s["sub1"] = s["sub1"].replace('"', '\\"') + if '"' in s["sub2"]: + s["sub2"] = s["sub2"].replace('"', '\\"') + ng_subdata += '\n\tsub_filter "%s" "%s";' % (s["sub1"], s["sub2"]) + if ng_subdata: + ng_sub_filter = ng_sub_filter % (ng_subdata) + else: + ng_sub_filter = '' + # 构造反向代理 + # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri + # php_pass_proxy = get.proxysite + # if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: + # php_pass_proxy = re.search(r'(https?\:\/\/[\w\.]+)', get.proxysite).group(0) + if advanced == 1: + if proxydir[-1] != '/': + proxydir = '{}/'.format(proxydir) + if proxysite[-1] != '/': + proxysite = '{}/'.format(proxysite) + if type == 1 and cache == 1: + ng_proxy_cache += ng_proxy % ( + proxydir, proxydir, proxysite, get.todomain, + public.get_msg_gettext('#Persistent connection related configuration'), ng_sub_filter, ng_cache, + get.proxydir) + if type == 1 and cache == 0: + ng_proxy_cache += ng_proxy % ( + get.proxydir, get.proxydir, proxysite, get.todomain, + public.get_msg_gettext('#Persistent connection related configuration'), ng_sub_filter, no_cache, + get.proxydir) + else: + if type == 1 and cache == 1: + ng_proxy_cache += ng_proxy % ( + get.proxydir, get.proxydir, get.proxysite, get.todomain, + public.get_msg_gettext('#Persistent connection related configuration'), ng_sub_filter, ng_cache, + get.proxydir) + if type == 1 and cache == 0: + ng_proxy_cache += ng_proxy % ( + get.proxydir, get.proxydir, get.proxysite, get.todomain, + public.get_msg_gettext('#Persistent connection related configuration'), ng_sub_filter, no_cache, + get.proxydir) + public.writeFile(ng_proxyfile, ng_proxy_cache) + + # APACHE + # 反向代理文件 + ap_proxyfile = "%s/panel/vhost/apache/proxy/%s/%s_%s.conf" % ( + self.setupPath, get.sitename, proxyname_md5, get.sitename) + ap_proxydir = "%s/panel/vhost/apache/proxy/%s" % (self.setupPath, get.sitename) + if not os.path.exists(ap_proxydir): + public.ExecShell("mkdir -p %s" % ap_proxydir) + ap_proxy = '' + if type == 1: + ap_proxy += '''#PROXY-START%s + + ProxyRequests Off + SSLProxyEngine on + ProxyPass %s %s/ + ProxyPassReverse %s %s/ + +#PROXY-END%s''' % (get.proxydir, get.proxydir, get.proxysite, get.proxydir, + get.proxysite, get.proxydir) + public.writeFile(ap_proxyfile, ap_proxy) + isError = public.checkWebConfig() + if (isError != True): + if public.get_webserver() == "nginx": + shutil.copyfile('/tmp/ng_file_bk.conf', ng_file) + else: + shutil.copyfile('/tmp/ap_file_bk.conf', ap_file) + for i in range(len(p_conf) - 1, -1, -1): + if get.sitename == p_conf[i]["sitename"] and p_conf[i]["proxyname"]: + del p_conf[i] + self.RemoveProxy(get) + return public.return_message(-1, 0, 'ERROR: %s
                                    ' % public.get_msg_gettext( + 'Configuration ERROR') + isError.replace("\n", + '
                                    ') + '
                                    ') + return public.return_message(0, 0, 'Setup successfully!') + + # 开启缓存 + def ProxyCache(self, get): + if public.get_webserver() != 'nginx': + return_message = public.return_msg_gettext(False, 'Currently only support Nginx') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + file = self.setupPath + "/panel/vhost/nginx/" + get.siteName + ".conf" + conf = public.readFile(file) + if conf.find('proxy_pass') == -1: + return_message = public.return_msg_gettext(False, 'Failed to set') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if conf.find('#proxy_cache') != -1: + conf = conf.replace('#proxy_cache', 'proxy_cache') + conf = conf.replace('#expires 12h', 'expires 12h') + else: + conf = conf.replace('proxy_cache', '#proxy_cache') + conf = conf.replace('expires 12h', '#expires 12h') + + public.writeFile(file, conf) + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 检查反向代理配置 + def CheckProxy(self, get): + if public.get_webserver() != 'nginx': return public.return_message(0, 0, "") + file = self.setupPath + "/nginx/conf/proxy.conf" + if not os.path.exists(file): + conf = '''proxy_temp_path %s/nginx/proxy_temp_dir; + proxy_cache_path %s/nginx/proxy_cache_dir levels=1:2 keys_zone=cache_one:10m inactive=1d max_size=5g; + client_body_buffer_size 512k; + proxy_connect_timeout 60; + proxy_read_timeout 60; + proxy_send_timeout 60; + proxy_buffer_size 32k; + proxy_buffers 4 64k; + proxy_busy_buffers_size 128k; + proxy_temp_file_write_size 128k; + proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; + proxy_cache cache_one;''' % (self.setupPath, self.setupPath) + public.writeFile(file, conf) + + file = self.setupPath + "/nginx/conf/nginx.conf" + conf = public.readFile(file) + if (conf.find('include proxy.conf;') == -1): + rep = r"include\s+mime.types;" + conf = re.sub(rep, "include mime.types;\n\tinclude proxy.conf;", conf) + public.writeFile(file, conf) + return public.return_message(0, 0, "") + + def get_project_find(self, project_name): + ''' + @name 获取指定项目配置 + @author hwliang<2021-08-09> + @param project_name 项目名称 + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?', ('Java', project_name)).find() + if not project_info: False + project_info['project_config'] = json.loads(project_info['project_config']) + return project_info + + # 取伪静态规则应用列表 + def GetRewriteList(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)) + + if get.siteName.find('node_') == 0: + get.siteName = get.siteName.replace('node_', '') + rewriteList = {} + ws = public.get_webserver() + if ws == "openlitespeed": + ws = "apache" + if ws == 'apache': + get.id = public.M('sites').where("name=?", (get.siteName,)).getField('id') + runPath = self.GetSiteRunPath(get).get('message', {}) + if runPath.get('runPath', '').find('/www/server/stop') != -1: + runPath['runPath'] = runPath['runPath'].replace('/www/server/stop', '') + rewriteList['sitePath'] = public.M('sites').where("name=?", (get.siteName,)).getField('path') + runPath.get( + 'runPath', '') + + rewriteList['rewrite'] = [] + rewriteList['rewrite'].append('0.' + public.get_msg_gettext('Current')) + for ds in os.listdir('rewrite/' + ws): + if ds == 'list.txt': continue + rewriteList['rewrite'].append(ds[0:len(ds) - 5]) + rewriteList['rewrite'] = sorted(rewriteList['rewrite']) + return public.return_message(0, 0, rewriteList) + + # 保存伪静态模板 + def SetRewriteTel(self, get): + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('data').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + ws = public.get_webserver() + if not get.name: + return_message = public.return_msg_gettext(True, 'Please enter a template name') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + if ws == "openlitespeed": + ws = "apache" + if sys.version_info[0] == 2: get.name = get.name.encode('utf-8') + filename = 'rewrite/' + ws + '/' + get.name + '.conf' + public.writeFile(filename, get.data) + return_message = public.return_msg_gettext(True, 'New URL rewrite rule has been saved!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 打包 + def ToBackup(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + find = public.M('sites').where("id=?", (id,)).field('name,path,id').find() + import time + fileName = find['name'] + '_' + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.zip' + backupPath = session['config']['backup_path'] + '/site' + zipName = backupPath + '/' + fileName + if not (os.path.exists(backupPath)): os.makedirs(backupPath) + tmps = '/tmp/panelExec.log' + execStr = "cd '" + find['path'] + "' && zip '" + zipName + "' -x .user.ini -r ./ > " + tmps + " 2>&1" + public.ExecShell(execStr) + sql = public.M('backup').add('type,name,pid,filename,size,addtime', + (0, fileName, find['id'], zipName, 0, public.getDate())) + public.write_log_gettext('Site manager', 'Backup site [{}] succeed!', (find['name'],)) + return_message = public.return_msg_gettext(True, 'Backup Succeeded!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 删除备份文件 + def DelBackup(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + where = "id=?" + backup_info = public.M('backup').where(where, (id,)).find() + filename = backup_info['filename'] + if os.path.exists(filename): os.remove(filename) + name = '' + if filename == 'qiniu': + name = backup_info['name'] + public.ExecShell( + public.get_python_bin() + " " + self.setupPath + '/panel/script/backup_qiniu.py delete_file ' + name) + + pid = backup_info['pid'] + site_name = public.M('sites').where('id=?', (pid,)).getField('name') + public.write_log_gettext('Site manager', 'Successfully deleted backup [{}] of site [{}]!', + (site_name, filename)) + public.M('backup').where(where, (id,)).delete() + return_message = public.return_msg_gettext(True, 'Successfully deleted') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 旧版本配置文件处理 + def OldConfigFile(self): + # 检查是否需要处理 + moveTo = 'data/moveTo.pl' + if os.path.exists(moveTo): return + + # 处理Nginx配置文件 + filename = self.setupPath + "/nginx/conf/nginx.conf" + if os.path.exists(filename): + conf = public.readFile(filename) + if conf.find('include vhost/*.conf;') != -1: + conf = conf.replace('include vhost/*.conf;', 'include ' + self.setupPath + '/panel/vhost/nginx/*.conf;') + public.writeFile(filename, conf) + + self.moveConf(self.setupPath + "/nginx/conf/vhost", self.setupPath + '/panel/vhost/nginx', 'rewrite', + self.setupPath + '/panel/vhost/rewrite') + self.moveConf(self.setupPath + "/nginx/conf/rewrite", self.setupPath + '/panel/vhost/rewrite') + + # 处理Apache配置文件 + filename = self.setupPath + "/apache/conf/httpd.conf" + if os.path.exists(filename): + conf = public.readFile(filename) + if conf.find('IncludeOptional conf/vhost/*.conf') != -1: + conf = conf.replace('IncludeOptional conf/vhost/*.conf', + 'IncludeOptional ' + self.setupPath + '/panel/vhost/apache/*.conf') + public.writeFile(filename, conf) + + self.moveConf(self.setupPath + "/apache/conf/vhost", self.setupPath + '/panel/vhost/apache') + + # 标记处理记录 + public.writeFile(moveTo, 'True') + public.serviceReload() + + # 移动旧版本配置文件 + def moveConf(self, Path, toPath, Replace=None, ReplaceTo=None): + if not os.path.exists(Path): return + import shutil + + letPath = '/etc/letsencrypt/live' + nginxPath = self.setupPath + '/nginx/conf/key' + apachePath = self.setupPath + '/apache/conf/key' + for filename in os.listdir(Path): + # 准备配置文件 + name = filename[0:len(filename) - 5] + filename = Path + '/' + filename + conf = public.readFile(filename) + + # 替换关键词 + if Replace: conf = conf.replace(Replace, ReplaceTo) + ReplaceTo = letPath + name + Replace = 'conf/key/' + name + if conf.find(Replace) != -1: conf = conf.replace(Replace, ReplaceTo) + Replace = 'key/' + name + if conf.find(Replace) != -1: conf = conf.replace(Replace, ReplaceTo) + public.writeFile(filename, conf) + + # 提取配置信息 + if conf.find('server_name') != -1: + self.formatNginxConf(filename) + elif conf.find(' 0: return + public.M('sites').add('name,path,status,ps,addtime', + (name, path, '1', public.get_msg_gettext('Please enter a note'), public.getDate())) + pid = public.M('sites').where("name=?", (name,)).getField('id') + for domain in domains: + public.M('domain').add('pid,name,port,addtime', (pid, domain, '80', public.getDate())) + + # 移动旧版本证书 + def moveKey(self, srcPath, dstPath): + if not os.path.exists(srcPath): return + import shutil + os.makedirs(dstPath) + srcKey = srcPath + '/key.key' + srcCsr = srcPath + '/csr.key' + if os.path.exists(srcKey): shutil.move(srcKey, dstPath + '/privkey.pem') + if os.path.exists(srcCsr): shutil.move(srcCsr, dstPath + '/fullchain.pem') + + # 路径处理 + def GetPath(self, path): + if path[-1] == '/': + return path[0:-1] + return path + + # 日志开关 + def logsOpen(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.name = public.M('sites').where("id=?", (get.id,)).getField('name') + # APACHE + filename = public.GetConfigValue('setup_path') + '/panel/vhost/apache/' + get.name + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + if conf.find('#ErrorLog') != -1: + conf = conf.replace("#ErrorLog", "ErrorLog").replace('#CustomLog', 'CustomLog') + else: + conf = conf.replace("ErrorLog", "#ErrorLog").replace('CustomLog', '#CustomLog') + public.writeFile(filename, conf) + + # NGINX + filename = public.GetConfigValue('setup_path') + '/panel/vhost/nginx/' + get.name + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = public.GetConfigValue('logs_path') + "/" + get.name + ".log" + if conf.find(rep) != -1: + conf = conf.replace(rep, "/dev/null") + else: + # conf = re.sub('}\n\\s+access_log\\s+off', '}\n\taccess_log ' + rep, conf) + conf = conf.replace('access_log /dev/null', 'access_log ' + rep) + public.writeFile(filename, conf) + + # OLS + filename = public.GetConfigValue('setup_path') + '/panel/vhost/openlitespeed/detail/' + get.name + '.conf' + conf = public.readFile(filename) + if conf: + rep = "\nerrorlog(.|\n)*compressArchive\\s*1\\s*\n}" + tmp = re.search(rep, conf) + s = 'on' + if not tmp: + s = 'off' + rep = "\n#errorlog(.|\n)*compressArchive\\s*1\\s*\n#}" + tmp = re.search(rep, conf) + tmp = tmp.group() + if tmp: + result = '' + if s == 'on': + for l in tmp.strip().splitlines(): + result += "\n#" + l + else: + for l in tmp.splitlines(): + result += "\n" + l[1:] + conf = re.sub(rep, "\n" + result.strip(), conf) + public.writeFile(filename, conf) + + public.serviceReload() + return public.return_message(0, 0, 'Setup successfully!') + + # 取日志状态 + def GetLogsStatus(self, get): + filename = public.GetConfigValue( + 'setup_path') + '/panel/vhost/' + public.get_webserver() + '/' + get.name + '.conf' + if public.get_webserver() == 'openlitespeed': + filename = public.GetConfigValue( + 'setup_path') + '/panel/vhost/' + public.get_webserver() + '/detail/' + get.name + '.conf' + conf = public.readFile(filename) + if not conf: return public.return_message(0, 0, True) + if conf.find('#ErrorLog') != -1: return public.return_message(0, 0, False) + # if re.search("}\n*\\s*access_log\\s+off", conf): + if conf.find("access_log /dev/null") != -1: return public.return_message(0, 0, False) + if re.search('\n#accesslog', conf): + return public.return_message(0, 0, False) + return public.return_message(0, 0, True) + + # 取目录加密状态 + def GetHasPwd(self, get): + if not hasattr(get, 'siteName'): + get.siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + get.configFile = self.setupPath + '/panel/vhost/nginx/' + get.siteName + '.conf' + conf = public.readFile(get.configFile) + if type(conf) == bool: return public.return_message(0, 0, False) + if conf.find('#AUTH_START') != -1: return public.return_message(0, 0, True) + return public.return_message(0, 0, False) + + # 设置目录加密 + def SetHasPwd(self, get): + # 校验参数 + try: + get.validate([ + Param('username').String(), + Param('password').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if public.get_webserver() == 'openlitespeed': + return public.return_message(-1, 0, + 'The current web server is openlitespeed. This function is not supported yet.') + if len(get.username.strip()) < 3 or len(get.password.strip()) < 3: return public.return_message(-1, 0, + 'Username or password cannot be less than 3 digits!') + + if not hasattr(get, 'siteName'): + get.siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + + self.CloseHasPwd(get) + filename = public.GetConfigValue('setup_path') + '/pass/' + get.siteName + '.pass' + try: + passconf = get.username + ':' + public.hasPwd(get.password) + except: + return public.return_message(-1, 0, + "The password fomart is wrong, please do not use special symbols for the first two digits!") + + if get.siteName == 'phpmyadmin': + get.configFile = self.setupPath + '/nginx/conf/nginx.conf' + if os.path.exists(self.setupPath + '/panel/vhost/nginx/phpmyadmin.conf'): + get.configFile = self.setupPath + '/panel/vhost/nginx/phpmyadmin.conf' + else: + get.configFile = self.setupPath + '/panel/vhost/nginx/' + get.siteName + '.conf' + + # 处理Nginx配置 + conf = public.readFile(get.configFile) + if conf: + rep = '#error_page 404 /404.html;' + if conf.find(rep) == -1: rep = '#error_page 404/404.html;' + data = ''' + #AUTH_START + auth_basic "Authorization"; + auth_basic_user_file %s; + #AUTH_END''' % (filename,) + conf = conf.replace(rep, rep + data) + public.writeFile(get.configFile, conf) + + if get.siteName == 'phpmyadmin': + get.configFile = self.setupPath + '/apache/conf/extra/httpd-vhosts.conf' + if os.path.exists(self.setupPath + '/panel/vhost/apache/phpmyadmin.conf'): + get.configFile = self.setupPath + '/panel/vhost/apache/phpmyadmin.conf' + else: + get.configFile = self.setupPath + '/panel/vhost/apache/' + get.siteName + '.conf' + + conf = public.readFile(get.configFile) + if conf: + # 处理Apache配置 + rep = 'SetOutputFilter' + if conf.find(rep) != -1: + data = '''#AUTH_START + AuthType basic + AuthName "Authorization " + AuthUserFile %s + Require user %s + #AUTH_END + ''' % (filename, get.username) + conf = conf.replace(rep, data + rep) + conf = conf.replace(' Require all granted', " #Require all granted") + public.writeFile(get.configFile, conf) + + # 写密码配置 + passDir = public.GetConfigValue('setup_path') + '/pass' + if not os.path.exists(passDir): public.ExecShell('mkdir -p ' + passDir) + public.writeFile(filename, passconf) + public.serviceReload() + public.write_log_gettext("Site manager", "Set site [{}] to password authentication required!", (get.siteName,)) + return public.return_message(0, 0, 'Setup successfully!') + + # 取消目录加密 + def CloseHasPwd(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not hasattr(get, 'siteName'): + get.siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + + if get.siteName == 'phpmyadmin': + get.configFile = self.setupPath + '/nginx/conf/nginx.conf' + else: + get.configFile = self.setupPath + '/panel/vhost/nginx/' + get.siteName + '.conf' + + if os.path.exists(get.configFile): + conf = public.readFile(get.configFile) + rep = "\n\\s*#AUTH_START(.|\n){1,200}#AUTH_END" + conf = re.sub(rep, '', conf) + public.writeFile(get.configFile, conf) + + if get.siteName == 'phpmyadmin': + get.configFile = self.setupPath + '/apache/conf/extra/httpd-vhosts.conf' + else: + get.configFile = self.setupPath + '/panel/vhost/apache/' + get.siteName + '.conf' + + if os.path.exists(get.configFile): + conf = public.readFile(get.configFile) + rep = "\n\\s*#AUTH_START(.|\n){1,200}#AUTH_END" + conf = re.sub(rep, '', conf) + conf = conf.replace(' #Require all granted', " Require all granted") + public.writeFile(get.configFile, conf) + public.serviceReload() + public.write_log_gettext("Site manager", "Cleared password authentication for site [{}]!", (get.siteName,)) + return public.return_message(0, 0, 'Setup successfully!') + + # 启用tomcat支持 + def SetTomcat(self, get): + siteName = get.siteName + name = siteName.replace('.', '_') + + rep = r"^(\d{1,3}\.){3,3}\d{1,3}$" + if re.match(rep, siteName): + return_message = public.return_msg_gettext(False, 'ERROR, primary domain cannot be IP address!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + # nginx + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + if conf.find('#TOMCAT-START') != -1: return self.CloseTomcat(get) + tomcatConf = r'''#TOMCAT-START + location / + { + proxy_pass "http://%s:8080"; + proxy_set_header Host %s; + proxy_set_header X-Forwarded-For $remote_addr; + } + location ~ .*\.(gif|jpg|jpeg|bmp|png|ico|txt|js|css)$ + { + expires 12h; + } + + location ~ .*\.war$ + { + return 404; + } + #TOMCAT-END + ''' % (siteName, siteName) + rep = 'include enable-php' + conf = conf.replace(rep, tomcatConf + rep) + public.writeFile(filename, conf) + + # apache + filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + if conf.find('#TOMCAT-START') != -1: return self.CloseTomcat(get) + tomcatConf = '''#TOMCAT-START + + ProxyRequests Off + SSLProxyEngine on + ProxyPass / http://%s:8080/ + ProxyPassReverse / http://%s:8080/ + RequestHeader unset Accept-Encoding + ExtFilterDefine fixtext mode=output intype=text/html cmd="/bin/sed 's,:8080,,g'" + SetOutputFilter fixtext + + #TOMCAT-END + ''' % (siteName, siteName) + + rep = '#PATH' + conf = conf.replace(rep, tomcatConf + rep) + public.writeFile(filename, conf) + path = public.M('sites').where("name=?", (siteName,)).getField('path') + import tomcat + tomcat.tomcat().AddVhost(path, siteName) + public.serviceReload() + public.ExecShell('/etc/init.d/tomcat stop') + public.ExecShell('/etc/init.d/tomcat start') + public.ExecShell('echo "127.0.0.1 ' + siteName + '" >> /etc/hosts') + public.write_log_gettext('TYPE_SITE', 'Turned on Tomcat supporting for site [{}]!', (siteName,)) + return public.return_msg_gettext(True, 'Succeeded, please test JSP program!') + + # 关闭tomcat支持 + def CloseTomcat(self, get): + if not os.path.exists('/etc/init.d/tomcat'): return public.return_message(-1, 0, "") + siteName = get.siteName + name = siteName.replace('.', '_') + + # nginx + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = "\\s*#TOMCAT-START(.|\n)+#TOMCAT-END" + conf = re.sub(rep, '', conf) + public.writeFile(filename, conf) + + # apache + filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = "\\s*#TOMCAT-START(.|\n)+#TOMCAT-END" + conf = re.sub(rep, '', conf) + public.writeFile(filename, conf) + public.ExecShell('rm -rf ' + self.setupPath + '/panel/vhost/tomcat/' + name) + try: + import tomcat + tomcat.tomcat().DelVhost(siteName) + except: + pass + public.serviceReload() + public.ExecShell('/etc/init.d/tomcat restart') + public.ExecShell("sed -i '/" + siteName + "/d' /etc/hosts") + public.write_log_gettext('Site manager', 'Turned off Tomcat supporting for site [{}]!', (siteName,)) + return_message = public.return_msg_gettext(True, 'Tomcat mapping closed!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 取当站点前运行目录 + def GetSiteRunPath(self, get): + siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + sitePath = public.M('sites').where('id=?', (get.id,)).getField('path') + if not siteName or os.path.isfile(sitePath): return public.return_message(0, 0, {"runPath": "/", 'dirs': []}) + path = sitePath + if public.get_webserver() == 'nginx': + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r'\s*root\s+(.+);' + path = re.search(rep, conf) + if not path: + return_message = public.return_msg_gettext(False, 'Get Site run path false') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + path = path.groups()[0] + elif public.get_webserver() == 'apache': + filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = '\\s*DocumentRoot\\s*"(.+)"\\s*\n' + path = re.search(rep, conf) + if not path: + return_message = public.return_msg_gettext(False, 'Get Site run path false') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + path = path.groups()[0] + else: + filename = self.setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r"vhRoot\s*(.*)" + path = re.search(rep, conf) + if not path: + return_message = public.return_msg_gettext(False, 'Get Site run path false') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + path = path.groups()[0] + data = {} + if sitePath == path: + data['runPath'] = '/' + else: + data['runPath'] = path.replace(sitePath, '') + + if data['runPath'] == path: + data['runPath'] = '/' + + dirnames = [] + dirnames.append('/') + if not os.path.exists(sitePath): os.makedirs(sitePath) + for filename in os.listdir(sitePath): + try: + json.dumps(filename) + if sys.version_info[0] == 2: + filename = filename.encode('utf-8') + else: + filename.encode('utf-8') + filePath = sitePath + '/' + filename + if not os.path.exists(filePath): continue + if os.path.islink(filePath): continue + if os.path.isdir(filePath): + dirnames.append('/' + filename) + except: + pass + + data['dirs'] = dirnames + return public.return_message(0, 0, data) + + # 设置当前站点运行目录 + def SetSiteRunPath(self, get): + # 校验参数 + try: + get.validate([ + Param('runPath').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + sitePath = public.M('sites').where('id=?', (get.id,)).getField('path') + old_run_path = self.GetRunPath(get)['message']['result'] + # 处理Nginx + filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + if conf: + rep = r'\s*root\s+(.+);' + tmp = re.search(rep, conf) + if tmp: + path = tmp.groups()[0] + conf = conf.replace(path, sitePath + get.runPath) + public.writeFile(filename, conf) + + # 处理Apache + filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + if conf: + rep = '\\s*DocumentRoot\\s*"(.+)"\\s*\n' + tmp = re.search(rep, conf) + if tmp: + path = tmp.groups()[0] + conf = conf.replace(path, sitePath + get.runPath) + public.writeFile(filename, conf) + # 处理OLS + self._set_ols_run_path(sitePath, get.runPath, siteName) + # self.DelUserInI(sitePath) + # get.path = sitePath; + # self.SetDirUserINI(get); + s_path = sitePath + old_run_path + "/.user.ini" + d_path = sitePath + get.runPath + "/.user.ini" + if s_path != d_path: + public.ExecShell("chattr -i {}".format(s_path)) + public.ExecShell("mv {} {}".format(s_path, d_path)) + public.ExecShell("chattr +i {}".format(d_path)) + + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + def _set_ols_run_path(self, site_path, run_path, sitename): + ols_conf_file = "{}/panel/vhost/openlitespeed/{}.conf".format(self.setupPath, sitename) + ols_conf = public.readFile(ols_conf_file) + if not ols_conf: + return + reg = '#VHOST\\s*{s}\\s*START(.|\n)+#VHOST\\s*{s}\\s*END'.format(s=sitename) + tmp = re.search(reg, ols_conf) + if not tmp: + return + reg = r"vhRoot\s*(.*)" + # tmp = re.search(reg,tmp.group()) + # if not tmp: + # return + tmp = "vhRoot " + site_path + run_path + ols_conf = re.sub(reg, tmp, ols_conf) + public.writeFile(ols_conf_file, ols_conf) + + # 设置默认站点 + def SetDefaultSite(self, get): + # 校验参数 + try: + get.validate([ + Param('name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import time + if public.GetWebServer() in ['openlitespeed']: + return public.return_message(-1, 0, 'OpenLiteSpeed does not support setting the default site') + default_site_save = 'data/defaultSite.pl' + # 清理旧的 + defaultSite = public.readFile(default_site_save) + http2 = '' + versionStr = public.readFile('/www/server/nginx/version.pl') + if versionStr: + if versionStr.find('1.8.1') == -1: http2 = ' http2' + if defaultSite: + path = self.setupPath + '/panel/vhost/nginx/' + defaultSite + '.conf' + if os.path.exists(path): + conf = public.readFile(path) + rep = r"listen\s+80.+;" + conf = re.sub(rep, 'listen 80;', conf, 1) + rep = r"listen\s+\[::\]:80.+;" + conf = re.sub(rep, 'listen [::]:80;', conf, 1) + rep = r"listen\s+443.+;" + conf = re.sub(rep, 'listen 443 ssl' + http2 + ';', conf, 1) + rep = r"listen\s+\[::\]:443.+;" + conf = re.sub(rep, 'listen [::]:443 ssl' + http2 + ';', conf, 1) + public.writeFile(path, conf) + + path = self.setupPath + '/apache/htdocs/.htaccess' + if os.path.exists(path): os.remove(path) + + if get.name == '0': + if os.path.exists(default_site_save): os.remove(default_site_save) + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 处理新的 + path = self.setupPath + '/apache/htdocs' + if os.path.exists(path): + conf = ''' + RewriteEngine on + RewriteCond %{HTTP_HOST} !^127.0.0.1 [NC] + RewriteRule (.*) http://%s/$1 [L] +''' + conf = conf.replace("%s", get.name) + if get.name == 'off': conf = '' + public.writeFile(path + '/.htaccess', conf) + + path = self.setupPath + '/panel/vhost/nginx/' + get.name + '.conf' + if os.path.exists(path): + conf = public.readFile(path) + rep = r"listen\s+80\s*;" + conf = re.sub(rep, 'listen 80 default_server;', conf, 1) + rep = r"listen\s+\[::\]:80\s*;" + conf = re.sub(rep, 'listen [::]:80 default_server;', conf, 1) + rep = r"listen\s+443\s*ssl\s*\w*\s*;" + conf = re.sub(rep, 'listen 443 ssl' + http2 + ' default_server;', conf, 1) + rep = r"listen\s+\[::\]:443\s*ssl\s*\w*\s*;" + conf = re.sub(rep, 'listen [::]:443 ssl' + http2 + ' default_server;', conf, 1) + public.writeFile(path, conf) + + path = self.setupPath + '/panel/vhost/nginx/default.conf' + if os.path.exists(path): public.ExecShell('rm -f ' + path) + public.writeFile(default_site_save, get.name) + public.serviceReload() + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 取默认站点 + def GetDefaultSite(self, get): + data = {} + data['sites'] = public.M('sites').where('project_type=? OR project_type=?', ('PHP', 'WP')).field('name').order( + 'id desc').select() + data['defaultSite'] = public.readFile('data/defaultSite.pl') + return public.return_message(0, 0, data) + + # 扫描站点 + def CheckSafe(self, get): + import db, time + isTask = '/tmp/panelTask.pl' + if os.path.exists(self.setupPath + '/panel/class/panelSafe.py'): + import py_compile + py_compile.compile(self.setupPath + '/panel/class/panelSafe.py') + get.path = public.M('sites').where('id=?', (get.id,)).getField('path') + execstr = "cd " + public.GetConfigValue( + 'setup_path') + "/panel/class && " + public.get_python_bin() + " panelSafe.pyc " + get.path + sql = db.Sql() + sql.table('tasks').add('id,name,type,status,addtime,execstr', ( + None, '%s [' % public.get_msg_gettext('Scan directory') + get.path + ']', 'execshell', '0', + time.strftime('%Y-%m-%d %H:%M:%S'), + execstr)) + public.writeFile(isTask, 'True') + public.write_log_gettext('Installer', 'Added trojan scan task for directory [{}]!', (get.path,)) + return_message = public.return_msg_gettext(True, 'Scan Task has in the queue!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 获取结果信息 + def GetCheckSafe(self, get): + get.path = public.M('sites').where('id=?', (get.id,)).getField('path') + path = get.path + '/scan.pl' + result = {} + result['data'] = [] + result['phpini'] = [] + result['userini'] = result['sshd'] = True + result['scan'] = False + result['outime'] = result['count'] = result['error'] = 0 + if not os.path.exists(path): return result + import json + return public.return_message(0, 0, json.loads(public.readFile(path))) + + # 更新病毒库 + def UpdateRulelist(self, get): + try: + conf = public.httpGet(public.getUrl() + '/install/ruleList.conf') + if conf: + public.writeFile(self.setupPath + '/panel/data/ruleList.conf', conf) + return_message = public.return_msg_gettext(True, 'Update Succeeded!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + return_message = public.return_msg_gettext(False, 'Failed to connect server!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + except: + return_message = public.return_msg_gettext(False, 'Failed to connect server!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + def set_site_etime_multiple(self, get): + ''' + @name 批量网站到期时间 + @author zhwen<2020-11-17> + @param sites_id "1,2" + @param edate 2020-11-18 + ''' + sites_id = get.sites_id.split(',') + set_edate_successfully = [] + set_edate_failed = {} + for site_id in sites_id: + get.id = site_id + site_name = public.M('sites').where("id=?", (site_id,)).getField('name') + if not site_name: + continue + try: + self.SetEdate(get) + set_edate_successfully.append(site_name) + except: + set_edate_failed[site_name] = 'There was an error setting, please try again.' + pass + return_message = {'msg': public.get_msg_gettext('Set the website [{}] expiration time successfully', + (','.join(set_edate_successfully),)), + 'error': set_edate_failed, + 'success': set_edate_successfully} + return public.return_message(0, 0, return_message) + + # 设置到期时间 + def SetEdate(self, get): + # 校验参数 + try: + get.validate([ + Param('edate').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + result = public.M('sites').where('id=?', (get.id,)).setField('edate', get.edate) + siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + public.write_log_gettext('Site manager', 'Set expired date to [{}] for site[{}]!', (get.edate, siteName)) + return_message = public.return_msg_gettext(True, + 'Successfully set, the site will stop automatically when expires!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 获取防盗链状态 + def GetSecurity(self, get): + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + file = '/www/server/panel/vhost/nginx/' + get.name + '.conf' + conf = public.readFile(file) + data = {} + if type(conf) == bool: return public.return_message(-1, 0, 'Reading configuration file failed!') + if conf.find('SECURITY-START') != -1: + rep = "#SECURITY-START(\n|.)+#SECURITY-END" + tmp = re.search(rep, conf).group() + content = re.search(r"\(.+\)\$", tmp) + if content: + data['fix'] = content.group().replace('(', '').replace(')$', '').replace('|', ',') + else: + data['fix'] = '' + try: + data['domains'] = ','.join( + list(set(re.search("valid_referers\\s+none\\s+blocked\\s+(.+);\n", tmp).groups()[0].split()))) + except: + data['domains'] = ','.join(list(set(re.search("valid_referers\\s+(.+);\n", tmp).groups()[0].split()))) + data['status'] = True + data['http_status'] = tmp.find('none blocked') != -1 + try: + data['return_rule'] = re.findall(r'(return|rewrite)\s+.*(\d{3}|(/.+)\s+(break|last));', conf)[0][ + 1].replace('break', '').strip() + except: + data['return_rule'] = '404' + else: + conf_file = self.conf_dir + '/{}_door_chain.json'.format(get.name) + try: + data = json.loads(public.readFile(conf_file)) + data['status'] = data['status'] == "true" + except: + data['fix'] = 'jpg,jpeg,gif,png,js,css' + domains = public.M('domain').where('pid=?', (get.id,)).field('name').select() + tmp = [] + for domain in domains: + tmp.append(domain['name']) + data['domains'] = ','.join(tmp) + data['return_rule'] = '404' + data['status'] = False + data['http_status'] = False + return public.return_message(0, 0, data) + + # 设置防盗链 + def SetSecurity(self, get): + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('fix').String(), + Param('domains').String(), + Param('return_rule').String(), + Param('id').Integer(), + Param('status').Bool(), + Param('http_status').Bool(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if len(get.fix) < 2: return public.return_message(-1, 0, 'URL suffix cannot be empty!') + if len(get.domains) < 3: return public.return_message(-1, 0, 'Anti-theft chain domain name cannot be empty!') + try: + conf_file = self.conf_dir + '/{}_door_chain.json'.format(get.name) + except Exception as ee: + public.print_log('ee:{}'.format(ee)) + data = { + "name": get.name, + "fix": get.fix, + "domains": get.domains, + "status": get.status, + "http_status": get.http_status, + "return_rule": get.return_rule, + } + public.writeFile(conf_file, json.dumps(data)) + # nginx + file = '/www/server/panel/vhost/nginx/' + get.name + '.conf' + if os.path.exists(file): + conf = public.readFile(file) + if conf.find('SECURITY-START') != -1: + # 先替换域名部分,防止域名过多导致替换失败 + rep = r"\s+valid_referers.+" + conf = re.sub(rep, '', conf) + # 再替换配置部分 + rep = "\\s+#SECURITY-START(\n|.){1,500}#SECURITY-END\n?" + conf = re.sub(rep, '\n', conf) + if get.status == 'false': + public.write_log_gettext('Site manager', "Hotlink Protection for site [{}] disabled!", (get.name,)) + public.writeFile(file, conf) + elif get.status == 'true': + if conf.find('SECURITY-START') == -1: + return_rule = 'return 404' + if 'return_rule' in get: + get.return_rule = get.return_rule.strip() + if get.return_rule in ['404', '403', '200', '301', '302', '401', '201']: + return_rule = 'return {}'.format(get.return_rule) + else: + if get.return_rule[0] != '/': + return public.return_message(-1, 0, + 'Response resources should use URI path or HTTP status code, such as: /test.png or 404') + return_rule = 'rewrite /.* {} break'.format(get.return_rule) + rconf = r'''#SECURITY-START Hotlink protection configuration + location ~ .*\.(%s)$ + { + expires 30d; + access_log /dev/null; + valid_referers %s; + if ($invalid_referer){ + %s; + } + } + #SECURITY-END + include enable-php-''' % ( + get.fix.strip().replace(',', '|'), get.domains.strip().replace(',', ' '), return_rule) + conf = re.sub(r"include\s+enable-php-", rconf, conf) + public.write_log_gettext('Site manager', "Hotlink Protection for site [{}] enabled!", (get.name,)) + + r_key = 'valid_referers none blocked' + d_key = 'valid_referers' + if get.http_status == 'true' and conf.find(r_key) == -1: + conf = conf.replace(d_key, r_key) + elif get.http_status == 'false' and conf.find(r_key) != -1: + conf = conf.replace(r_key, d_key) + public.writeFile(file, conf) + + # apache + file = '/www/server/panel/vhost/apache/' + get.name + '.conf' + if os.path.exists(file): + conf = public.readFile(file) + if conf.find('SECURITY-START') != -1: + rep = "#SECURITY-START(\n|.){1,500}#SECURITY-END\n" + conf = re.sub(rep, '', conf) + if get.status == "false": + public.writeFile(file, conf) + elif get.status == 'true': + if conf.find('SECURITY-START') == -1: + return_rule = '/404.html [R=404,NC,L]' + if 'return_rule' in get: + get.return_rule = get.return_rule.strip() + if get.return_rule in ['404', '403', '200', '301', '302', '401', '201']: + return_rule = '/{s}.html [R={s},NC,L]'.format(s=get.return_rule) + else: + if get.return_rule[0] != '/': + return public.return_message(-1, 0, + 'Response resources should use URI path or HTTP status code, such as: /test.png or 404') + return_rule = '{}'.format(get.return_rule) + + tmp = " RewriteCond %{HTTP_REFERER} !{DOMAIN} [NC]" + tmps = [] + for d in get.domains.split(','): + tmps.append(tmp.replace('{DOMAIN}', d)) + domains = "\n".join(tmps) + rconf = "combined\n #SECURITY-START Hotlink protection configuration\n RewriteEngine on\n" + domains + "\n RewriteRule .(" + get.fix.strip().replace( + ',', '|') + ") " + return_rule + "\n #SECURITY-END" + conf = conf.replace('combined', rconf) + + r_key = '#SECURITY-START Hotlink protection configuration\n RewriteEngine on\n RewriteCond %{HTTP_REFERER} !^$ [NC]\n' + d_key = '#SECURITY-START Hotlink protection configuration\n RewriteEngine on\n' + if get.http_status == 'true' and conf.find(r_key) == -1: + conf = conf.replace(d_key, r_key) + elif get.http_status == 'false' and conf.find(r_key) != -1: + if conf.find('SECURITY-START') == -1: public.return_message(-1, 0, + 'Please activate the anti-theft chain first!') + conf = conf.replace(r_key, d_key) + public.writeFile(file, conf) + # OLS + cond_dir = '/www/server/panel/vhost/openlitespeed/prevent_hotlink/' + if not os.path.exists(cond_dir): + os.makedirs(cond_dir) + file = cond_dir + get.name + '.conf' + if get.http_status == 'true': + conf = r""" +RewriteCond %{HTTP_REFERER} !^$ +RewriteCond %{HTTP_REFERER} !BTDOMAIN_NAME [NC] +RewriteRule \.(BTPFILE)$ /404.html [R,NC] +""" + conf = conf.replace('BTDOMAIN_NAME', get.domains.replace(',', ' ')).replace('BTPFILE', + get.fix.replace(',', '|')) + else: + conf = r""" +RewriteCond %{HTTP_REFERER} !BTDOMAIN_NAME [NC] +RewriteRule \.(BTPFILE)$ /404.html [R,NC] +""" + conf = conf.replace('BTDOMAIN_NAME', get.domains.replace(',', ' ')).replace('BTPFILE', + get.fix.replace(',', '|')) + public.writeFile(file, conf) + if get.status == "false": + if os.path.exists(file): os.remove(file) + public.serviceReload() + return public.return_message(0, 0, 'Setup successfully!') + + # xss 防御 + def xsssec(self, text): + replace_list = { + "<": "<", + ">": ">", + "'": "'", + '"': """, + } + for k, v in replace_list.items(): + text = text.replace(k, v) + return public.xssencode2(text) + + # 取网站日志 + def GetSiteLogs(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)) + + serverType = public.get_webserver() + if serverType == "nginx": + logPath = '/www/wwwlogs/' + get.siteName + '.log' + elif serverType == 'apache': + logPath = '/www/wwwlogs/' + get.siteName + '-access_log' + else: + logPath = '/www/wwwlogs/' + get.siteName + '_ols.access_log' + if not os.path.exists(logPath): + return_message = public.return_msg_gettext(False, 'Log is empty') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + # return public.return_msg_gettext(True, self.xsssec(public.GetNumLines(logPath, 1000))) + return_message = public.return_msg_gettext(True, self.xsssec(public.GetNumLines(logPath, 1000))) + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + # if not os.path.exists(logPath): return public.return_message(-1,0, 'Log is empty') + # return public.return_message(0,0, self.xsssec(public.GetNumLines(logPath, 1000))) + + # 取网站日志 + def get_site_err_log(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)) + + return_status = 0 + serverType = public.get_webserver() + if serverType == "nginx": + logPath = '/www/wwwlogs/' + get.siteName + '.error.log' + elif serverType == 'apache': + logPath = '/www/wwwlogs/' + get.siteName + '-error_log' + else: + logPath = '/www/wwwlogs/' + get.siteName + '_ols.error_log' + if not os.path.exists(logPath): + return_message = public.return_msg_gettext(False, 'Log is empty') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + return_message = public.return_msg_gettext(True, self.xsssec(public.GetNumLines(logPath, 1000))) + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 取网站分类 + def get_site_types(self, get): + data = public.M("site_types").field("id,name").order("id asc").select() + data.insert(0, {"id": 0, "name": public.get_msg_gettext('Default category')}) + for i in data: + i['name'] = public.xss_version(i['name']) + return public.return_message(0, 0, data) + + # 添加网站分类 + def add_site_type(self, get): + # 校验参数 + try: + get.validate([ + Param('name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.name = get.name.strip() + if not get.name: + return_message = public.return_msg_gettext(False, 'Category name cannot be empty') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if len(get.name) > 16: + return_message = public.return_msg_gettext(False, 'Category name cannot exceed 16 letters') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + type_sql = public.M('site_types') + if type_sql.count() >= 10: + return_message = public.return_msg_gettext(False, 'Add up to 10 categories!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if type_sql.where('name=?', (get.name,)).count() > 0: + return_message = public.return_msg_gettext(False, 'Specified category name already exists!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + type_sql.add("name", (public.xssencode2(get.name),)) + return_message = public.return_msg_gettext(True, 'Setup successfully!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 删除网站分类 + def remove_site_type(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + type_sql = public.M('site_types') + if type_sql.where('id=?', (get.id,)).count() == 0: + return_message = public.return_msg_gettext(False, 'Specified category does NOT exist!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + type_sql.where('id=?', (get.id,)).delete() + public.M("sites").where("type_id=?", (get.id,)).save("type_id", (0,)) + return_message = public.return_msg_gettext(True, 'Category deleted!') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 修改网站分类名称 + def modify_site_type_name(self, get): + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.name = get.name.strip() + if not get.name: + return_message = public.return_msg_gettext(False, 'Category name cannot be empty') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + if len(get.name) > 16: + return_message = public.return_msg_gettext(False, 'Category name cannot exceed 16 letters') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + type_sql = public.M('site_types') + if type_sql.where('id=?', (get.id,)).count() == 0: + return_message = public.return_msg_gettext(False, 'Specified category does NOT exist!') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + type_sql.where('id=?', (get.id,)).setField('name', get.name) + return_message = public.return_msg_gettext(True, 'Successfully modified') + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + # 设置指定站点的分类 + def set_site_type(self, get): + site_ids = json.loads(get.site_ids) + site_sql = public.M("sites") + for s_id in site_ids: + site_sql.where("id=?", (s_id,)).setField("type_id", get.id) + return public.return_message(0, 0, "Setup successfully!") + + # 设置目录保护 + def set_dir_auth(self, get): + sd = site_dir_auth.SiteDirAuth() + return sd.set_dir_auth(get) + + def delete_dir_auth_multiple(self, get): + ''' + @name 批量目录保护 + @author zhwen<2020-11-17> + @param site_id 1 + @param names test,baohu + ''' + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + names = get.names.split(',') + del_successfully = [] + del_failed = {} + for name in names: + get.name = name + get.id = get.site_id + try: + get.multiple = 1 + result = self.delete_dir_auth(get) + if not result['status']: + del_failed[name] = result['msg'] + continue + del_successfully.append(name) + except: + del_failed[name] = public.get_msg_gettext('There was an error deleting, please try again.') + public.serviceReload() + return_message = { + 'msg': public.get_msg_gettext('Delete [ {} ] dir auth successfully', (','.join(del_successfully),)), + 'error': del_failed, + 'success': del_successfully} + return public.return_message(0, 0, return_message) + + # 删除目录保护 + def delete_dir_auth(self, get): + sd = site_dir_auth.SiteDirAuth() + return sd.delete_dir_auth(get) + + # 获取目录保护列表 + def get_dir_auth(self, get): + sd = site_dir_auth.SiteDirAuth() + return sd.get_dir_auth(get) + + # 修改目录保护密码 + def modify_dir_auth_pass(self, get): + sd = site_dir_auth.SiteDirAuth() + return sd.modify_dir_auth_pass(get) + + def _check_path_total(self, path, limit): + """ + 根据路径获取文件/目录大小 + @path 文件或者目录路径 + return int + """ + + if not os.path.exists(path): return 0; + if not os.path.isdir(path): return os.path.getsize(path) + size_total = 0 + for nf in os.walk(path): + for f in nf[2]: + filename = nf[0] + '/' + f + if not os.path.exists(filename): continue; + if os.path.islink(filename): continue; + size_total += os.path.getsize(filename) + if size_total >= limit: return limit + return size_total + + def get_average_num(self, slist): + """ + @获取平均值 + """ + count = len(slist) + limit_size = 1 * 1024 * 1024 + if count <= 0: return limit_size + print(slist) + if len(slist) > 1: + slist = sorted(slist) + limit_size = int((slist[0] + slist[-1]) / 2 * 0.85) + return limit_size + + def check_del_data(self, get): + """ + @删除前置检测 + @ids = [1,2,3] + """ + # 校验参数 + try: + get.validate([ + Param('ids').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + ids = json.loads(get['ids']) + slist = {} + result = [] + + import database_v2 as database + db_data = database.database().get_database_size(ids, True) + limit_size = 50 * 1024 * 1024 + f_list_size = []; + db_list_size = [] + for id in ids: + data = public.M('sites').where("id=?", (id,)).field('id,name,path,addtime').find(); + if not data: continue + + addtime = public.to_date(times=data['addtime']) + + data['st_time'] = addtime + data['limit'] = False + data['backup_count'] = public.M('backup').where("pid=? AND type=?", (data['id'], '0')).count() + f_size = self._check_path_total(data['path'], limit_size) + data['total'] = f_size; + data['score'] = 0 + + # 目录太小不计分 + if f_size > 0: + f_list_size.append(f_size) + + # 10k 目录不参与排序 + if f_size > 10 * 1024: data['score'] = int(time.time() - addtime) + f_size + + if data['total'] >= limit_size: data['limit'] = True + data['database'] = False + + find = public.M('databases').field('id,pid,name,ps,addtime').where('pid=?', (data['id'],)).find() + if find: + db_addtime = public.to_date(times=find['addtime']) + + data['database'] = db_data[find['name']] + data['database']['st_time'] = db_addtime + + db_score = 0 + db_size = data['database']['total'] + + if db_size > 0: + db_list_size.append(db_size) + if db_size > 50 * 1024: db_score += int(time.time() - db_addtime) + db_size + + data['score'] += db_score + result.append(data) + + slist['data'] = sorted(result, key=lambda x: x['score'], reverse=True) + slist['file_size'] = self.get_average_num(f_list_size) + slist['db_size'] = self.get_average_num(db_list_size) + return public.return_message(0, 0, slist) + + def get_https_mode(self, get=None): + ''' + @name 获取https模式 + @author hwliang<2022-01-14> + @return bool False.宽松模式 True.严格模式 + ''' + web_server = public.get_webserver() + if web_server not in ['nginx', 'apache']: + return public.return_message(0, 0, False) + + if web_server == 'nginx': + default_conf_file = "{}/nginx/0.default.conf".format(public.get_vhost_path()) + else: + default_conf_file = "{}/apache/0.default.conf".format(public.get_vhost_path()) + + if not os.path.exists(default_conf_file): return public.return_message(0, 0, False) + default_conf = public.readFile(default_conf_file) + if not default_conf: return False + + if default_conf.find('DEFAULT SSL CONFI') != -1: return public.return_message(0, 0, True) + return public.return_message(0, 0, False) + + def write_ngx_default_conf_by_ssl(self): + ''' + @name 写nginx默认配置文件(含SSL配置) + @author hwliang<2022-01-14> + @return bool + ''' + default_conf_body = '''server +{ + listen 80; + listen 443 ssl; + server_name _; + index index.html; + root /www/server/nginx/html; + + # DEFAULT SSL CONFIG + ssl_certificate /www/server/panel/vhost/cert/0.default/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/0.default/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000"; +}''' + ngx_default_conf_file = "{}/nginx/0.default.conf".format(public.get_vhost_path()) + self.create_default_cert() + return public.writeFile(ngx_default_conf_file, default_conf_body) + + def write_ngx_default_conf(self): + ''' + @name 写nginx默认配置文件 + @author hwliang<2022-01-14> + @return bool + ''' + default_conf_body = '''server +{ + listen 80; + server_name _; + index index.html; + root /www/server/nginx/html; +}''' + ngx_default_conf_file = "{}/nginx/0.default.conf".format(public.get_vhost_path()) + return public.writeFile(ngx_default_conf_file, default_conf_body) + + def write_apa_default_conf_by_ssl(self): + ''' + @name 写nginx默认配置文件(含SSL配置) + @author hwliang<2022-01-14> + @return bool + ''' + default_conf_body = ''' + ServerAdmin webmaster@example.com + DocumentRoot "/www/server/apache/htdocs" + ServerName bt.default.com + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + Order allow,deny + Allow from all + DirectoryIndex index.html + + + + ServerAdmin webmaster@example.com + DocumentRoot "/www/server/apache/htdocs" + ServerName ssl.default.com + + # DEFAULT SSL CONFIG + SSLEngine On + SSLCertificateFile /www/server/panel/vhost/cert/0.default/fullchain.pem + SSLCertificateKeyFile /www/server/panel/vhost/cert/0.default/privkey.pem + SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 + SSLProtocol All -SSLv2 -SSLv3 -TLSv1 + SSLHonorCipherOrder On + + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + Order allow,deny + Allow from all + DirectoryIndex index.html + +''' + apa_default_conf_file = "{}/apache/0.default.conf".format(public.get_vhost_path()) + self.create_default_cert() + return public.writeFile(apa_default_conf_file, default_conf_body) + + def write_apa_default_conf(self): + ''' + @name 写apache默认配置文件 + @author hwliang<2022-01-14> + @return bool + ''' + default_conf_body = ''' + ServerAdmin webmaster@example.com + DocumentRoot "/www/server/apache/htdocs" + ServerName bt.default.com + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + Order allow,deny + Allow from all + DirectoryIndex index.html + +''' + apa_default_conf_file = "{}/apache/0.default.conf".format(public.get_vhost_path()) + return public.writeFile(apa_default_conf_file, default_conf_body) + + def set_https_mode(self, get=None): + ''' + @name 设置https模式 + @author hwliang<2022-01-14> + @return dict + ''' + web_server = public.get_webserver() + if web_server not in ['nginx', 'apache']: + return_message = public.return_msg_gettext(False, 'This function only supports Nginx/Apache') + del return_message['status'] + return public.return_message(-1, 0, return_message['msg']) + + ngx_default_conf_file = "{}/nginx/0.default.conf".format(public.get_vhost_path()) + apa_default_conf_file = "{}/apache/0.default.conf".format(public.get_vhost_path()) + ngx_default_conf = public.readFile(ngx_default_conf_file) + apa_default_conf = public.readFile(apa_default_conf_file) + status = False + if ngx_default_conf: + if ngx_default_conf.find('DEFAULT SSL CONFIG') != -1: + status = False + self.write_ngx_default_conf() + self.write_apa_default_conf() + else: + status = True + self.write_ngx_default_conf_by_ssl() + self.write_apa_default_conf_by_ssl() + else: + status = True + self.write_ngx_default_conf_by_ssl() + self.write_apa_default_conf_by_ssl() + + public.serviceReload() + status_msg = {True: 'Open', False: 'Close'} + msg = public.gettext_msg('Has {} HTTPS strict mode', (status_msg[status],)) + public.write_log_gettext('WebSite manager', msg) + return_message = public.return_msg_gettext(True, msg) + del return_message['status'] + return public.return_message(0, 0, return_message['msg']) + + def create_default_cert(self): + ''' + @name 创建默认SSL证书 + @author hwliang<2022-01-14> + @return bool + ''' + cert_pem = '/www/server/panel/vhost/cert/0.default/fullchain.pem' + cert_key = '/www/server/panel/vhost/cert/0.default/privkey.pem' + if os.path.exists(cert_pem) and os.path.exists(cert_key): return True + cert_path = os.path.dirname(cert_pem) + if not os.path.exists(cert_path): os.makedirs(cert_path) + import OpenSSL + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) + cert = OpenSSL.crypto.X509() + cert.set_serial_number(0) + # cert.get_subject().CN = '' + cert.set_issuer(cert.get_subject()) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(86400 * 3650) + cert.set_pubkey(key) + cert.sign(key, 'md5') + cert_ca = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) + private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + if len(cert_ca) > 100 and len(private_key) > 100: + public.writeFile(cert_pem, cert_ca, 'wb+') + public.writeFile(cert_key, private_key, 'wb+') + return True + return False + + def get_upload_ssl_list(self, get): + """ + @获取上传证书列表 + @siteName string 网站名称 + """ + siteName = get['siteName'] + path = '{}/vhost/upload_ssl/{}'.format(public.get_panel_path(), siteName) + if not os.path.exists(path): os.makedirs(path) + + res = [] + for filename in os.listdir(path): + try: + filename = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(filename))) + res.append(filename) + except: + pass + return public.return_message(0, 0, res) + + # 获取指定证书基本信息 + def get_cert_init(self, cert_data, ssl_info=None): + """ + @获取指定证书基本信息 + @cert_data string 证书数据 + @ssl_info dict 证书信息 + """ + try: + result = {} + if ssl_info and ssl_info['ssl_type'] == 'pfx': + x509 = self.__check_pfx_pwd(cert_data, ssl_info['pwd'])[0] + else: + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_data) + # 取产品名称 + issuer = x509.get_issuer() + result['issuer'] = '' + if hasattr(issuer, 'CN'): + result['issuer'] = issuer.CN + if not result['issuer']: + is_key = [b'0', '0'] + issue_comp = issuer.get_components() + if len(issue_comp) == 1: + is_key = [b'CN', 'CN'] + for iss in issue_comp: + if iss[0] in is_key: + result['issuer'] = iss[1].decode() + break + # 取到期时间 + result['notAfter'] = self.strf_date( + bytes.decode(x509.get_notAfter())[:-1]) + # 取申请时间 + result['notBefore'] = self.strf_date( + bytes.decode(x509.get_notBefore())[:-1]) + # 取可选名称 + result['dns'] = [] + for i in range(x509.get_extension_count()): + s_name = x509.get_extension(i) + if s_name.get_short_name() in [b'subjectAltName', 'subjectAltName']: + s_dns = str(s_name).split(',') + for d in s_dns: + result['dns'].append(d.split(':')[1]) + subject = x509.get_subject().get_components() + + # 取主要认证名称 + if len(subject) == 1: + result['subject'] = subject[0][1].decode() + else: + if len(result['dns']) > 0: + result['subject'] = result['dns'][0] + else: + result['subject'] = ''; + return result + except: + return False + + def strf_date(self, sdate): + """ + @转换证书时间 + """ + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + + def check_ssl_endtime(self, data, ssl_info=None): + """ + @检查证书是否有效(证书最高有效期不超过1年) + @data string 证书数据 + @ssl_info dict 证书信息 + """ + info = self.get_cert_init(data, ssl_info) + if info: + end_time = time.mktime(time.strptime(info['notAfter'], "%Y-%m-%d")) + start_time = time.mktime(time.strptime(info['notBefore'], "%Y-%m-%d")) + + days = int((end_time - start_time) / 86400) + if days < 400: # 1年有效期+1个月续签时间 + return data + return False + + # 证书转为pkcs12 + def dump_pkcs12(self, key_pem=None, cert_pem=None, ca_pem=None, friendly_name=None): + """ + @证书转为pkcs12 + @key_pem string 私钥数据 + @cert_pem string 证书数据 + @ca_pem string 可选的CA证书数据 + @friendly_name string 可选的证书名称 + """ + p12 = OpenSSL.crypto.PKCS12() + if cert_pem: + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem.encode()) + p12.set_certificate(x509) + if key_pem: + p12.set_privatekey(OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, key_pem.encode())) + if ca_pem: + p12.set_ca_certificates((OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, ca_pem.encode()),)) + if friendly_name: + p12.set_friendlyname(friendly_name.encode()) + return p12 + + def download_cert(self, get): + """ + @下载证书 + @get dict 请求参数 + siteName string 网站名称 + ssl_type string 证书类型 + key string 密钥 + pem string 证书数据 + pwd string 证书密码 + """ + pem = get['pem'] + siteName = get['siteName'] + ssl_type = get['ssl_type'] + + rpath = '{}/temp/ssl/'.format(public.get_panel_path()) + if os.path.exists(rpath): shutil.rmtree(rpath) + + ca_list = [] + path = '{}/{}_{}'.format(rpath, siteName, int(time.time())) + if ssl_type == 'pfx': + res = self.__check_pfx_pwd(base64.b64decode(pem), get['pwd']) + p12 = res[1]; + x509 = res[0]; + get['pwd'] = res[2] + print(get['pwd']) + ca_list = [] + for x in p12.get_ca_certificates(): + ca_list.insert(0, OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, x).decode().strip()) + ca_cert = '\n'.join(ca_list) + key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, p12.get_privatekey()).decode().strip() + domain_cert = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, x509).decode().strip() + else: + key = get['key'] + domain_cert = pem.split('-----END CERTIFICATE-----')[0] + "-----END CERTIFICATE-----\n" + ca_cert = pem.replace(domain_cert, '') + + p12 = self.dump_pkcs12(key, '{}\n{}'.format(domain_cert.strip(), ca_cert), ca_cert) + + for x in ['IIS', 'Apache', 'Nginx', 'Other']: + d_file = '{}/{}'.format(path, x) + if not os.path.exists(d_file): os.makedirs(d_file) + + if x == 'IIS': + public.writeFile2(d_file + '/fullchain.pfx', p12.export(), 'wb+') + public.writeFile(d_file + '/password.txt', get['pwd']) + elif x == 'Apache': + public.writeFile(d_file + '/privkey.key', key) + public.writeFile(d_file + '/root_bundle.crt', ca_cert) + public.writeFile(d_file + '/domain.crt', domain_cert) + else: + public.writeFile(d_file + '/privkey.key', key) + public.writeFile(d_file + '/fullchain.pem', '{}\n{}'.format(domain_cert.strip(), ca_cert)) + + flist = [] + public.get_file_list(path, flist) + + zfile = '{}/{}.zip'.format(rpath, os.path.basename(path)) + import zipfile + f = zipfile.ZipFile(zfile, 'w', zipfile.ZIP_DEFLATED) + for item in flist: + s_path = item.replace(path, '') + if s_path: f.write(item, s_path) + f.close() + + return public.return_message(0, 0, zfile); + + def check_ssl_info(self, get): + """ + @解析证书信息 + @get dict 请求参数 + path string 上传文件路径 + """ + path = get['path'] + if not os.path.exists(path): + return public.return_message(-1, 0, '查询失败,不存在的地址') + + info = {'root': '', 'cert': '', 'pem': '', 'key': ''} + ssl_info = {'pwd': None, 'ssl_type': None} + for filename in os.listdir(path): + filepath = '{}/{}'.format(path, filename) + ext = filename[-4:] + if ext == '.pfx': + ssl_info['ssl_type'] = 'pfx' + + f = open(filepath, 'rb') # pfx为二进制文件 + info['pem'] = f.read() + + else: + data = public.readFile(filepath) + if filename.find('password') >= 0: # 取pfx密码 + ssl_info['pwd'] = re.search('([a-zA-Z0-9]+)', data).groups()[0] + continue + + if len(data) < 1024: + continue + + if data.find('PRIVATE KEY') >= 0: + info['key'] = data # 取key + + if ext == '.pem': + if self.check_ssl_endtime(data): + info['pem'] = data + else: + if data.find('BEGIN CERTIFICATE') >= 0: + if not info['root']: + info['root'] = data + else: + info['cert'] = data + + if ssl_info['ssl_type'] == 'pfx': + info['pem'] = self.check_ssl_endtime(info['pem'], ssl_info) + if info['pem']: + info['pem'] = base64.b64encode(info['pem']) + info['key'] = True + else: + if not info['pem']: + # 确认ca证书和域名证书顺序 + info['pem'] = self.check_ssl_endtime(info['root'] + "\n" + info['cert'], ssl_info) + if not info['pem']: + info['pem'] = self.check_ssl_endtime(info['cert'] + "\n" + info['root'], ssl_info) + + if info['key'] and info['pem']: + return_message = {'key': info['key'], 'pem': info['pem'], 'ssl_type': ssl_info['ssl_type'], + 'pwd': ssl_info['pwd']} + return public.return_message(0, 0, return_message) + return public.return_message(0, 0, False) + + def __check_pfx_pwd(self, data, pwd): + """ + @检测pfx证书密码 + @data string pfx证书内容 + @pwd string 密码 + """ + try: + p12 = OpenSSL.crypto.load_pkcs12(data, pwd) + x509 = p12.get_certificate() + except: + pwd = re.search('([a-zA-Z0-9]+)', pwd).groups()[0] + p12 = OpenSSL.crypto.load_pkcs12(data, pwd) + x509 = p12.get_certificate() + return [x509, p12, pwd] + + def auto_restart_rph(self, get): + # 设置申请或续签SSL时自动停止反向代理、重定向、http to https,申请完成后自动开启 + conf_file = '{}/data/stop_rp_when_renew_ssl.pl'.format(public.get_panel_path()) + conf = public.readFile(conf_file) + if not conf: + public.writeFile(conf_file, json.dumps([get.sitename])) + try: + conf = json.loads(conf) + if get.sitename not in conf: + conf.append(get.sitename) + public.writeFile(conf_file, json.dumps(conf)) + except: + return public.return_message(0, 0, 'Error parsing configuration file') + return public.return_message(0, 0, 'Setup successfully') + + def remove_auto_restart_rph(self, get): + # 设置申请或续签SSL时自动停止反向代理、重定向、http to https,申请完成后自动开启 + conf_file = '{}/data/stop_rp_when_renew_ssl.pl'.format(public.get_panel_path()) + conf = public.readFile(conf_file) + if not conf: + return public.return_message(-1, 0, + 'Website [proxy,redirect,http to https] are not set to restart automatically') + try: + conf = json.loads(conf) + conf.remove(get.sitename) + public.writeFile(conf_file, json.dumps(conf)) + except: + return public.return_message(-1, 0, 'Configuration file parsing error') + return public.return_message(0, 0, 'Setup successfully') + + def get_auto_restart_rph(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)) + + # 设置申请或续签SSL时自动停止反向代理、重定向、http to https,申请完成后自动开启 + conf_file = '{}/data/stop_rp_when_renew_ssl.pl'.format(public.get_panel_path()) + conf = public.readFile(conf_file) + if not conf: + return public.return_message(-1, 0, + 'Website [proxy,redirect,http to https] are not set to restart automatically') + try: + conf = json.loads(conf) + if get.sitename in conf: + return public.return_message(0, 0, + 'This website has turn on [proxy,redirect,http to https] auto restart') + return public.return_message(-1, 0, 'Website has turn off auto restart') + except: + return public.return_message(-1, 0, 'Configuration file parsing error') + + # *********** WP Toolkit --Begin-- ************* + + # 重置Wordpress管理员账号密码 + def reset_wp_password(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().reset_wp_password(get) + + # 检查Wordpress版本更新 + def is_update(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().is_update(get) + + # 清除Wordpress Nginx fastcgi cache + def purge_all_cache(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().purge_all_cache(get) + + # 设置Wordpress Nginx fastcgi cache + def set_fastcgi_cache(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().set_fastcgi_cache(get) + + # 更新Wordpress到最新版本 + def update_wp(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().update_wp(get) + + # 获取可用的语言列表 + def get_language(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().get_language(get) + + # 获取可用的WP安装版本 + def get_wp_versions(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().get_wp_available_versions(get) + + # 获取WP Toolkit配置信息 + def get_wp_configurations(self, args): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().get_wp_configurations(args) + + # 保存WP Toolkit配置 + def save_wp_configurations(self, args): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().save_wp_configurations(args) + + # 获取wp 安全模块配置 + def get_wp_security_info(self, get): + from wp_toolkit import wp_security + result = wp_security().get_security_info(get) + get.name = get.site_name + # 取防盗链配置 + hotlink = self.GetSecurity(get) + try: + result['message']['hotlink_status'] = 1 if hotlink['message']['status'] else 0 + except: + pass + return result + + # 开启WP 文件防护 + def open_wp_file_protection(self, get): + from wp_toolkit import wp_security + return wp_security().open_file_protection(get) + + # 关闭WP 文件防护 + def close_wp_file_protection(self, get): + from wp_toolkit import wp_security + return wp_security().close_file_protection(get) + + # 获取WP 文件防护 + def get_wp_file_info(self, get): + from wp_toolkit import wp_security + return wp_security().get_file_info(get) + + # 开启WP 防火墙防护 + def open_wp_firewall_protection(self, get): + from wp_toolkit import wp_security + return wp_security().open_firewall_protection(get) + + # 关闭WP 防火墙防护 + def close_wp_firewall_protection(self, get): + from wp_toolkit import wp_security + return wp_security().close_firewall_protection(get) + + def deploy_wp(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().deploy_wp(get) + + def get_wp_username(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().get_wp_username(get) + + def reset_wp_db(self, get): + import one_key_wp_v2 as one_key_wp + return one_key_wp.one_key_wp().reset_wp_db(get) + + # 备份WP站点 + def wp_backup(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('bak_type').Require().Integer('>', 0), + ]) + + from wp_toolkit import wpbackup + + bak_obj = wpbackup(args.s_id) + bak_type = int(args.bak_type) + + if bak_type == 1: + ok, msg = bak_obj.backup_files() + elif bak_type == 2: + ok, msg = bak_obj.backup_database() + elif bak_type == 3: + ok, msg = bak_obj.backup_full() + else: + return public.fail_v2('Invalid backup type {}', (bak_type,)) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 还原WP站点 + def wp_restore(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('bak_id').Require().Integer('>', 0), + ]) + + from wp_toolkit import wpbackup + + bak_id = int(args.bak_id) + bak_obj = wpbackup(wpbackup.retrieve_site_id_with_bak_id(bak_id)) + ok, msg = bak_obj.restore_with_backup(bak_id) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # WP备份列表 + def wp_backup_list(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('p').Integer('>', 0), + public.Param('limit').Integer('>', 0), + public.Param('tojs').Regexp(r"^[\w\.\-]+$"), + public.Param('result').Regexp(r"^[\d\,]+$"), + ]) + + from wp_toolkit import wpbackup + + bak_obj = wpbackup(args.s_id) + + return public.success_v2(bak_obj.backup_list(args)) + + # 删除WP站点备份 + def wp_remove_backup(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('bak_id').Require().Integer('>', 0), + ]) + + from wp_toolkit import wpbackup + + bak_id = int(args.bak_id) + bak_obj = wpbackup(wpbackup.retrieve_site_id_with_bak_id(bak_id)) + ok, msg = bak_obj.remove_backup(bak_id) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 从 [网站管理] 迁移到 [WP Toolkit] + def wp_migrate_from_website_to_wptoolkit(self, args: public.dict_obj): + from wp_toolkit import wpmigration + ok, msg = wpmigration.migrate_aap_from_website_to_wptoolkit() + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 查询可从 [网站管理] 迁移到 [WP Toolkit] 的网站列表 + def wp_can_migrate_from_website_to_wptoolkit(self, args: public.dict_obj): + from wp_toolkit import wpmigration + return public.success_v2(wpmigration.can_migrations_of_aap_website()) + + # 从aapanel WP备份中创建WP站点 + def wp_create_with_aap_bak(self, args: public.dict_obj): + from wp_toolkit import wpbackup + ok, msg = wpbackup.wp_deploy_with_aap_bak(args) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 从plesk/cpanel WP备份中创建WP站点 + def wp_create_with_plesk_or_cpanel_bak(self, args: public.dict_obj): + from wp_toolkit import wpbackup + ok, msg = wpbackup.wp_deploy_with_plesk_or_cpanel_bak(args) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 克隆WP站点 + def wp_clone(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + ]) + + from wp_toolkit import wpbackup + + ok, msg = wpbackup(args.s_id).clone(args) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # Wordpress完整性校验 + def wp_integrity_check(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).integrity_check() + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 重新下载并安装Wordpress(仅限框架文件,不会删除新文件) + def wp_reinstall_files(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).reinstall_package() + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # TODO 获取可安装的插件列表 + def wp_plugin_list(self, args: public.dict_obj): + pass + + # 安装插件 + def wp_install_plugin(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('slug').Require().SafePath(), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).install_plugin(args.slug) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 已安装插件列表 + def wp_installed_plugins(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('force_check_updates').Integer('in', [0, 1]), + ]) + + from wp_toolkit import wpmgr + + return public.success_v2(wpmgr(args.s_id).installed_plugins(bool(int(args.get('force_check_updates', 0))))) + + # 更新插件 + def wp_update_plugin(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('plugin_file').Require().SafePath(), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).update_plugin(args.plugin_file) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2('Success') + + # 开启/关闭插件自动更新 + def wp_set_plugin_auto_update(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('plugin_file').Require().SafePath(), + public.Param('enable').Require().Integer('in', [0, 1]), + ]) + + from wp_toolkit import wpmgr + + wpmgr_obj = wpmgr(args.s_id) + + if int(args.enable) == 1: + fn = wpmgr_obj.enable_plugin_auto_update + else: + fn = wpmgr_obj.disable_plugin_auto_update + + ok, msg = fn(args.plugin_file) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2('Success') + + # 激活/禁用插件 + def wp_set_plugin_status(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('plugin_file').Require().SafePath(), + public.Param('activate').Require().Integer('in', [0, 1]), + ]) + + from wp_toolkit import wpmgr + + wpmgr_obj = wpmgr(args.s_id) + + if int(args.activate) == 1: + fn = wpmgr_obj.activate_plugins + errmsg = public.get_msg_gettext('Activate plugin failed, please try again later.') + else: + fn = wpmgr_obj.deactivate_plugins + errmsg = public.get_msg_gettext('Deactivate plugin failed, please try again later.') + + if not fn(args.plugin_file): + return public.fail_v2(errmsg) + + return public.success_v2('Success') + + # 卸载插件 + def wp_uninstall_plugin(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('plugin_file').Require().SafePath(), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).uninstall_plugin(args.plugin_file) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2('Success') + + # TODO 获取可安装的主题列表 + def wp_theme_list(self, args: public.dict_obj): + pass + + # 安装主题 + def wp_install_theme(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('slug').Require(), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).install_theme(args.slug) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2(msg) + + # 已安装主题列表 + def wp_installed_themes(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('force_check_updates').Integer('in', [0, 1]), + ]) + + from wp_toolkit import wpmgr + + return public.success_v2(wpmgr(args.s_id).installed_themes(bool(int(args.get('force_check_updates', 0))))) + + # 更新主题 + def wp_update_theme(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('stylesheet').Require(), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).update_theme(args.stylesheet) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2('Success') + + # 开启/关闭主题自动更新 + def wp_set_theme_auto_update(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('stylesheet').Require(), + public.Param('enable').Require().Integer('in', [0, 1]), + ]) + + from wp_toolkit import wpmgr + + wpmgr_obj = wpmgr(args.s_id) + + if int(args.enable) == 1: + fn = wpmgr_obj.enable_theme_auto_update + else: + fn = wpmgr_obj.disable_theme_auto_update + + ok, msg = fn(args.stylesheet) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2('Success') + + # 切换主题 + def wp_switch_theme(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('stylesheet').Require(), + ]) + + from wp_toolkit import wpmgr + + if not wpmgr(args.s_id).switch_theme(args.stylesheet): + return public.fail_v2('Switch theme failed, please try again later.') + + return public.success_v2('Success') + + # 卸载主题 + def wp_uninstall_theme(self, args: public.dict_obj): + # 参数校验 + args.validate([ + public.Param('s_id').Require().Integer('>', 0), + public.Param('stylesheet').Require(), + ]) + + from wp_toolkit import wpmgr + + ok, msg = wpmgr(args.s_id).uninstall_theme(args.stylesheet) + + if not ok: + return public.fail_v2(msg) + + return public.success_v2('Success') + + # *********** WP Toolkit --End-- ************* + + @staticmethod + def test_domains_api(get): + try: + domains = json.loads(get.domains.strip()) + except (json.JSONDecodeError, AttributeError, KeyError): + return public.return_message(-1, 0, "参数错误") + try: + from panel_dns_api_v2 import DnsMager + public.print_log("开始测试域名解析---- {}") + # public.print_log("开始测试域名解析---- {}".format(domains[0])) + + return DnsMager().test_domains_api(domains) + except: + pass + public.return_message(0, 0, "") + + def site_rname(self, get): + try: + if not (hasattr(get, "id") and hasattr(get, "rname")): + return public.return_message(-1, 0, "parameter error") + id = get.id + rname = get.rname + data = public.M('sites').where("id=?", (id,)).select() + if not data: + return public.return_message(-1, 0, "The site does not exist!") + data = data[0] + name = data['rname'] if 'rname' in data.keys() and data.get('rname', '') else data['name'] + if 'rname' not in data.keys(): + public.M('sites').execute("ALTER TABLE 'sites' ADD 'rname' text DEFAULT ''", ()) + public.M('sites').where('id=?', data['id']).update({'rname': rname}) + # public.write_log_gettext('Site manager', 'Website [{}] renamed: [{}]'.format(name, rname)) + return public.return_message(0, 0, 'Website [{}] renamed: [{}]'.format(name, rname)) + except: + return public.return_message(-1, 0, traceback.format_exc()) diff --git a/class_v2/panel_ssl_v2.py b/class_v2/panel_ssl_v2.py new file mode 100644 index 00000000..3e80a87c --- /dev/null +++ b/class_v2/panel_ssl_v2.py @@ -0,0 +1,1759 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------ +# SSL接口 +# ------------------------------ +from panel_auth_v2 import panelAuth as panelAuth +import public, os, sys, binascii, urllib, json, time, datetime, re +from ssl_manage import SSLManger # 新的ssl管理 + +from Crypto import Random +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher +import base64 +from public.validate import Param + +try: + from BTPanel import cache, session +except: + pass + + +class panelSSL: + # __APIURL = public.GetConfigValue('home') + '/api/Auth' + # __APIURL2 = public.GetConfigValue('home') + '/api/Cert' + # __BINDURL = 'https://api.bt.cn/Auth/GetAuthToken' # 获取token 获取官网token + + + + + __BINDURL = 'https://www.aapanel.com/api/user' # 获取token 获取官网token + # __BINDURL = 'http://dev.aapanel.com/api/user' # 获取token 获取官网token + + __CODEURL = 'https://api.bt.cn/Auth/GetBindCode' # 获取绑定验证码 + __UPATH = 'data/userInfo.json' + + # __APIURL = 'http://dev.aapanel.com/api' + __APIURL = 'https://www.aapanel.com/api' + + __PUBKEY = 'data/public.key' + + # 证书购买 + # __APIURL_CERT = 'https://www.aapanel.com/api/cert' + + __userInfo = None # 用户信息 从文件中读取的 + __PDATA = None + _check_url = None + + # 构造方法 + def __init__(self): + pdata = {} + data = {} # 存放调用接口的参数 + # 记录了用户信息 + if os.path.exists(self.__UPATH): + my_tmp = public.readFile(self.__UPATH) + if my_tmp: + try: + self.__userInfo = json.loads(my_tmp) + except: + self.__userInfo = {} + else: + self.__userInfo = {} + + # public.print_log('初始化 !!!!!!!!!!!!!!!!!!!用户信息: {}'.format(self.__userInfo)) + try: + if self.__userInfo: + # 记录里没有这两个key + pdata['access_key'] = self.__userInfo['access_key'] + data['secret_key'] = self.__userInfo['secret_key'] + # pdata['access_key'] = 'test' + # data['secret_key'] = '123456' + except: + # self.__userInfo = {} + pdata['access_key'] = 'test' + data['secret_key'] = '123456' + else: + pdata['access_key'] = 'test' + data['secret_key'] = '123456' + pdata['data'] = data + self.__PDATA = pdata + + # public.print_log('初始化------->最后 !!!!!!!!!!!!!!!!!!!用户信息: {}'.format(self.__userInfo)) + + def en_code_rsa(self, data): + pk = public.readFile(self.__PUBKEY) + if not pk: + return False + pub_k = RSA.importKey(pk) + cipher = PKCS1_cipher.new(pub_k) + rsa_text = base64.b64encode(cipher.encrypt(bytes(data.encode("utf8")))) + return str(rsa_text, encoding='utf-8') + + # 获取Token 最新 + # def GetToken(self, get): + # rtmp = "" + # data = {} + # data['username'] = get.username + # data['password'] = public.md5(get.password) + # data['serverid'] = panelAuth().get_serverid() + # pdata = {} + # pdata['data'] = self.De_Code(data) + # try: + # rtmp = public.httpPost(self.__BINDURL, pdata) + # result = json.loads(rtmp) + # result['data'] = self.En_Code(result['data']) + # if result['data']: + # result['data']['serverid'] = data['serverid'] + # public.writeFile(self.__UPATH, json.dumps(result['data'])) + # public.flush_plugin_list() + # del (result['data']) + # session['focre_cloud'] = True + # return result + # except Exception as ex: + # # bind = 'data/bind.pl' + # # if os.path.exists(bind): os.remove(bind) + # # return public.returnMsg(False,'连接服务器失败!
                                    ' + str(ex)) + # raise public.error_conn_cloud(str(ex)) + # + # # 删除Token 最新 + # def DelToken(self, get): + # if os.path.exists(self.__UPATH): os.remove(self.__UPATH) + # session['focre_cloud'] = True + # return public.returnMsg(True, "SSL_BTUSER_UN") + + # 获取Token todo 在用 + def GetToken(self, get): + rtmp = "" + data = {} + data['identification'] = self.en_code_rsa(get.username) + # data['username'] = self.en_code_rsa(get.username) + data['password'] = self.en_code_rsa(get.password) + data['from_panel'] = self.en_code_rsa('1') # 1 代表从面板登录 + try: + rtmp = public.httpPost(self.__APIURL + '/user/login', data) + + # public.print_log("写入用户信息 @@@@222 {}".format(self.__APIURL + '/user/login')) + result = json.loads(rtmp) + # public.print_log("写入用户信息 @@@@ {}".format(rtmp)) + # public.print_log("写入用户信息 @@@@ {}".format(result)) + if result['success']: + bind = 'data/bind.pl' + if os.path.exists(bind): os.remove(bind) + userinfo = result['res']['user_data'] + userinfo['token'] = result['res']['access_token'] + # 用户信息写入文件 + public.writeFile(self.__UPATH, json.dumps(userinfo)) + # if bool: + # # public.print_log("写入用户信息 成功 {}".format(userinfo)) + # else: + # public.print_log("写入用户信息 失败") + + session['focre_cloud'] = True + return public.return_message(0,0, 'Bind successfully') + # return result + else: + return public.return_message(-1,0, + 'Invalid username or email or password! please check and try again!') + except Exception as ex: + bind = 'data/bind.pl' + if os.path.exists(bind): os.remove(bind) + return public.return_message(-1,0, '%s
                                    %s' % ( + public.get_msg_gettext('Failed to connect server!'), str(rtmp))) + + # 删除Token todo + def DelToken(self, get): + uinfo = public.readFile(self.__UPATH) + try: + uinfo = json.loads(uinfo) + public.writeFile(self.__UPATH, json.dumps({'server_id': uinfo['server_id']})) + except: + public.ExecShell("rm -f " + self.__UPATH) + session['focre_cloud'] = True + + return public.return_msg_gettext(True, 'Unbound!') + + # 获取用户信息 todo + # def GetUserInfo(self, get): + # result = {} + # + # # public.print_log("@@@@@@@@@@获取用户信息 开始----- {}".format(self.__userInfo)) + # + # if self.__userInfo: + # userTmp = {} + # userTmp['username'] = self.__userInfo['username'][0:3] + '****' + self.__userInfo['username'][-4:] + # result['status'] = True + # result['msg'] = public.get_msg_gettext('SSL_GET_SUCCESS') + # result['data'] = userTmp + # else: + # userTmp = {} + # userTmp['username'] = public.get_msg_gettext('SSL_NOT_BTUSER') + # result['status'] = False + # result['msg'] = public.get_msg_gettext('SSL_NOT_BTUSER') + # result['data'] = userTmp + # return result + + def GetUserInfo(self, get): + # public.print_log("获取用户信息222") + return_status = -1 + result = {} + try: + if self.__userInfo: + userTmp = {} + userTmp['username'] = self.__userInfo['email'][0:3] + '****' + self.__userInfo['email'][-4:] + return_status= 0 + result['msg'] = public.get_msg_gettext('Got successfully!') + result['data'] = userTmp + else: + userTmp = {} + userTmp['username'] = public.get_msg_gettext('Please bind your account!') + result['msg'] = public.get_msg_gettext('Please bind your account!') + result['data'] = userTmp + except: + userTmp = {} + userTmp['username'] = public.get_msg_gettext('Please bind your account!') + result['msg'] = public.get_msg_gettext('Please bind your account!') + result['data'] = userTmp + return public.return_message(return_status,0,result) + + # 获取产品列表 todo + # def get_product_list(self, get): + # p_type = 'dv' + # if 'p_type' in get: p_type = get.p_type + # result = self.request('get_product_list?p_type={}'.format(p_type)) + # return result + + # # 获取产品列表2 todo + # def get_product_list_v2(self, get): + # p_type = 'dv' + # if 'p_type' in get: p_type = get.p_type + # + # result = self.request('get_product_list_v2?p_type={}'.format(p_type)) + # return result + + # 获取产品列表2 todo 产品列表 + def get_product_list_v2(self, get): + return_status = 0 + result = self.request('cert/product/list') + # if "success" in result: + # print(f"键 '{key_to_check}' 存在于字典中。") + # else: + # print(f"键 '{key_to_check}' 不存在于字典中。") + return public.return_message(0,0,result) + + # 获取商业证书订单列表 todo 用户订单列表 + def get_order_list(self, get): + result = self.request('cert/user/list') # 获取当前登录用户的SSL证书列表 + return public.return_message(0,0,result) + + + # 下载证书 todo + def download_cert(self, get): + self.__PDATA['uc_id'] = get.uc_id + result = self.request('cert/user/download') + return result + + # 获指定商业证书订单 + def get_order_find(self, get): + self.__PDATA['uc_id'] = get.uc_id + result = self.request('cert/user/info') + return result + + # 获取证书管理员信息 todo + def get_cert_admin(self, get): + result = self.request('cert/user/administrator') + return result + + # 完善资料CA(先支付接口) todo 可能是支付后的完善信息接口 + def apply_order_ca(self, args): + pdata = json.loads(args.pdata) + + result = self.check_ssl_caa(pdata['domains']) + if result: + return result + + self.__PDATA['data'] = pdata + result = self.request('cert/user/update_profile') + + return result + + + # 部署指定商业证书 todo 部署证书 + def set_cert(self, get): + siteName = get.siteName + certInfoall = self.get_order_find(get) + + if certInfoall["success"] is False: + return public.return_msg_gettext(False, certInfoall["res"]) + certInfo = certInfoall["res"] + path = '/www/server/panel/vhost/cert/' + siteName + if not os.path.exists(path): + public.ExecShell('mkdir -p ' + path) + csrpath = path + "/fullchain.pem" + keypath = path + "/privkey.pem" + pidpath = path + "/certOrderId" + + other_file = path + '/partnerOrderId' + if os.path.exists(other_file): + os.remove(other_file) + other_file = path + '/README' + if os.path.exists(other_file): + os.remove(other_file) + + public.writeFile(keypath, certInfo['private_key']) + public.writeFile(csrpath, certInfo['certificate'] + "\n" + certInfo['ca_certificate']) + # 改记录 uc_id + public.writeFile(pidpath, get.uc_id) + import panel_site_v2 as panelSite + panelSite.panelSite().SetSSLConf(get) + public.serviceReload() + return public.return_msg_gettext(True, 'Setup successfully!') + + # # 生成商业证书支付订单 暂无 + # def apply_order_pay(self, args): + # self.__PDATA['data'] = json.loads(args.pdata) + # result = self.check_ssl_caa(self.__PDATA['data']['domains']) + # if result: return result + # result = self.request('apply_cert_order') + # return result + + # 检查CAA记录是否正确 + def check_ssl_caa(self, domains, clist=['sectigo.com', 'digicert.com', 'comodoca.com']): + ''' + @name 检查CAA记录是否正确 + @param domains 域名列表 + @param clist 正确的记录值关键词 + @return bool + ''' + try: + data = {} + for domain in domains: + root, zone = public.get_root_domain(domain) + for d in [domain, root, '_acme-challenge.{}'.format(root), '_acme-challenge.{}'.format(domain)]: + ret = public.query_dns(d, 'CAA') + if not ret: continue + slist = [] + for val in ret: + if val['value'] in clist: + return False + slist.append(val) + + if len(slist) > 0: + data[d] = slist + if data: + result = {} + result['status'] = False + result[ + 'msg'] = 'error: There is a CAA record in the DNS resolution of the domain name. Please delete it and apply again ' + result['data'] = json.dumps(data) + result['caa_list'] = data + return result + except: + pass + return False + + + # 提交商业证书订单到CA + # def apply_order(self, args): + # self.__PDATA['data']['oid'] = args.oid + # result = self.request('apply_cert') + # if result['status'] == True: + # self.__PDATA['data'] = {} + # result['verify_info'] = self.get_verify_info(args) + # return result + + # 获取证书域名验证结果 todo 暂未使用 + # 用到: 处理验证信息 set_verify_info 完善资料 apply_order_ca 续签证书 renew_cert_order + def get_verify_info(self, args): + self.__PDATA['uc_id'] = args.uc_id + verify_info = self.request('cert/user/validate_domains') + if verify_info['success']: + return "success" + return "error" + + # is_file_verify = 'fileName' in verify_info + # verify_info['paths'] = [] + # verify_info['hosts'] = [] + # for domain in verify_info['domains']: + # if is_file_verify: + # siteRunPath = self.get_domain_run_path(domain) + # if not siteRunPath: + # # if domain[:4] == 'www.': domain = domain[:4] + # verify_info['paths'].append(verify_info['path'].replace('example.com', domain)) + # continue + # verify_path = siteRunPath + '/.well-known/pki-validation' + # if not os.path.exists(verify_path): + # os.makedirs(verify_path) + # verify_file = verify_path + '/' + verify_info['fileName'] + # if os.path.exists(verify_file): continue + # public.writeFile(verify_file, verify_info['content']) + # else: + # original_domain = domain + # # if domain[:4] == 'www.': domain = domain[:4] + # verify_info['hosts'].append(verify_info['host'] + '.' + domain) + # if 'auth_to' in args: + # root, zone = public.get_root_domain(domain) + # res = self.create_dns_record(args['auth_to'], verify_info['host'] + '.' + root, + # verify_info['value'], original_domain) + # print(res) + # return verify_info + + # 处理验证信息 todo 如果传参 要传uc_id + def set_verify_info(self, args): + # self.__PDATA['uc_id'] = args.uc_id # 新增 + verify_info = self.get_verify_info(args) + is_file_verify = 'fileName' in verify_info + verify_info['paths'] = [] + verify_info['hosts'] = [] + for domain in verify_info['domains']: + if domain[:2] == '*.': domain = domain[2:] + if is_file_verify: + siteRunPath = self.get_domain_run_path(domain) + if not siteRunPath: + # if domain[:4] == 'www.': domain = domain[4:] + verify_info['paths'].append(verify_info['path'].replace('example.com', domain)) + continue + verify_path = siteRunPath + '/.well-known/pki-validation' + if not os.path.exists(verify_path): + os.makedirs(verify_path) + verify_file = verify_path + '/' + verify_info['fileName'] + if os.path.exists(verify_file): continue + public.writeFile(verify_file, verify_info['content']) + else: + original_domain = domain + # if domain[:4] == 'www.': domain = domain[4:] + verify_info['hosts'].append(verify_info['host'] + '.' + domain) + + if 'auth_to' in args: + root, zone = public.get_root_domain(domain) + self.create_dns_record(args['auth_to'], verify_info['host'] + '.' + root, + verify_info['value'], original_domain) + return verify_info + + # 获取指定域名的PATH + def get_domain_run_path(self, domain): + pid = public.M('domain').where('name=?', (domain,)).getField('pid') + if not pid: return False + return self.get_site_run_path(pid) + + # 获取网站运行目录 + def get_site_run_path(self, pid): + ''' + @name 获取网站运行目录 + @author hwliang<2020-08-05> + @param pid(int) 网站标识 + @return string + ''' + siteInfo = public.M('sites').where('id=?', (pid,)).find() + siteName = siteInfo['name'] + sitePath = siteInfo['path'] + webserver_type = public.get_webserver() + setupPath = '/www/server' + path = None + if webserver_type == 'nginx': + filename = setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r'\s*root\s+(.+);' + tmp1 = re.search(rep, conf) + if tmp1: path = tmp1.groups()[0] + + elif webserver_type == 'apache': + filename = setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r'\s*DocumentRoot\s*"(.+)"\s*\n' + tmp1 = re.search(rep, conf) + if tmp1: path = tmp1.groups()[0] + else: + filename = setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r"vhRoot\s*(.*)" + path = re.search(rep, conf) + if not path: + path = None + else: + path = path.groups()[0] + + if not path: + path = sitePath + return path + + # 验证URL是否匹配 + def check_url_txt(self, args, timeout=5): + url = args.url + content = args.content + + import http_requests + res = http_requests.get(url, s_type='curl', timeout=timeout) + result = res.text + if not result: return 0 + + if result.find('11001') != -1 or result.find('curl: (6)') != -1: return -1 + if result.find('curl: (7)') != -1 or res.status_code in [403, 401]: return -5 + if result.find('Not Found') != -1 or result.find('not found') != -1 or res.status_code in [404]: return -2 + if result.find('timed out') != -1: return -3 + if result.find('301') != -1 or result.find('302') != -1 or result.find( + 'Redirecting...') != -1 or res.status_code in [301, 302]: return -4 + if result == content: return 1 + return 0 + + # 更换验证方式 # todo? ['data'] + def again_verify(self, args): + self.__PDATA['uc_id'] = args.uc_id + self.__PDATA['dcv_method'] = args.dcv_method + result = self.request('cert/user/update_dcv') + return result + + # 获取商业证书验证结果 + def get_verify_result(self, args): + self.__PDATA['uc_id'] = args.uc_id + res = self.request('cert/user/validate') + if res['success'] is False: + return res + verify_info = res['res'] + + if verify_info['status'] in ['COMPLETE', False]: + return verify_info + + is_file_verify = 'CNAME_CSR_HASH' != verify_info['data']['dcvList'][0]['dcvMethod'] + verify_info['paths'] = [] + verify_info['hosts'] = [] + if verify_info['data']['application']['status'] == 'ongoing': + return public.return_msg_gettext(False, 'In verification, please contact aaPanel if the audit still fails after 24 hours') + + for dinfo in verify_info['data']['dcvList']: + is_https = dinfo['dcvMethod'] == 'HTTPS_CSR_HASH' + if is_https: + is_https = 's' + else: + is_https = '' + domain = dinfo['domainName'] + if domain[:2] == '*.': + domain = domain[2:] + dinfo['domainName'] = domain + + if is_file_verify: + # 判断是否是Springboot 项目 + if public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (dinfo['domainName'])).getField('pid'),)).getField( + 'project_type') == 'Java' or public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (dinfo['domainName'])).getField('pid'),)).getField( + 'project_type') == 'Go' or public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (dinfo['domainName'])).getField('pid'),)).getField( + 'project_type') == 'Other': + siteRunPath = '/www/wwwroot/java_node_ssl' + else: + siteRunPath = self.get_domain_run_path(domain) + # if domain[:4] == 'www.': domain = domain[4:] + status = 0 + url = 'http' + is_https + '://' + domain + '/.well-known/pki-validation/' + verify_info['data'][ + 'DCVfileName'] + get = public.dict_obj() + get.url = url + get.content = verify_info['data']['DCVfileContent'] + status = self.check_url_txt(get) + + verify_info['paths'].append({'url': url, 'status': status}) + if not siteRunPath: + continue + + verify_path = siteRunPath + '/.well-known/pki-validation' + if not os.path.exists(verify_path): + os.makedirs(verify_path) + verify_file = verify_path + '/' + verify_info['data']['DCVfileName'] + if os.path.exists(verify_file): + continue + public.writeFile(verify_file, verify_info['data']['DCVfileContent']) + else: + # if domain[:4] == 'www.': domain = domain[4:] + domain, subb = public.get_root_domain(domain) + dinfo['domainName'] = domain + verify_info['hosts'].append(verify_info['data']['DCVdnsHost'] + '.' + domain) + + return verify_info + + # 取消订单 暂无 + def cancel_cert_order(self, args): + self.__PDATA['data']['oid'] = args.oid + result = self.request('cancel_cert_order') + return result + + # 单独购买人工安装服务 + def apply_cert_install_pay(self, args): + ''' + @name 单独购买人工安装服务 + @param args{ + 'uc_id' 订单ID + } + ''' + self.__PDATA['uc_id'] = args.uc_id + result = self.request('cert/order/deployment_assistance') + return result + + # 生成商业证书支付订单 todo 生成支付订单 下单支付 + def apply_cert_order_pay(self, args): + pdata = json.loads(args.pdata) + self.__PDATA['data'] = pdata + result = self.request('cert/order/create') + return public.return_message(0,0,result) + + # 模拟支付 + # def pay_test(self, args): + # out_trade_no = args.out_trade_no + # # /api/common/stripe/{out_trade_no} + # # result = self.request_test('order/pay') + # result = public.return_msg_gettext(False, '测试用 模拟支付!') + # url = "https://dev.aapanel.com/api/common/stripe/" + out_trade_no + # response_data = public.httpGet(url) + # + # # public.print_log("******************** url: {}".format(url)) + # + # try: + # result = json.loads(response_data) + # except: + # pass + # return result + + + + # 申请证书 ??? + def ApplyDVSSL(self, get): + + """ + 申请证书 + """ + if not 'orgName' in get: return public.returnMsg(False, 'missing parameter: orgName') + if not 'orgPhone' in get: return public.returnMsg(False, 'missing parameter: orgPhone') + if not 'orgPostalCode' in get: return public.returnMsg(False, 'missing parameter: orgPostalCode') + if not 'orgRegion' in get: return public.returnMsg(False, 'missing parameter: orgRegion') + if not 'orgCity' in get: return public.returnMsg(False, 'missing parameter: orgCity') + if not 'orgAddress' in get: return public.returnMsg(False, 'missing parameter: orgAddress') + if not 'orgDivision' in get: return public.returnMsg(False, 'missing parameter: orgDivision') + + get.id = public.M('domain').where('name=?', (get.domain,)).getField('pid') + if hasattr(get, 'siteName'): + get.path = public.M('sites').where('id=?', (get.id,)).getField('path') + else: + get.siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + + # 当申请二级域名为www时,检测主域名是否绑定到同一网站 + if get.domain[:4] == 'www.': + if not public.M('domain').where('name=? AND pid=?', (get.domain[4:], get.id)).count(): + return public.returnMsg(False, + "Request for [%s] certificate requires verification [%s] Please bind and resolve [%s] to the site!" % ( + get.domain, get.domain[4:], get.domain[4:])) + # 判断是否是Java项目 + if public.M('sites').where('id=?', (get.id,)).getField('project_type') == 'Java' or public.M('sites').where( + 'id=?', (get.id,)).getField('project_type') == 'Go' or public.M('sites').where('id=?', + (get.id,)).getField( + 'project_type') == 'Other': + get.path = '/www/wwwroot/java_node_ssl/' + runPath = '' + # 判断是否是Node项目 + elif public.M('sites').where('id=?', (get.id,)).getField('project_type') == 'Node': + get.path = public.M('sites').where('id=?', (get.id,)).getField('path') + runPath = '' + # 判断是否是python项目 + elif public.M('sites').where( + 'id=?', (get.id,)).getField('project_type') == 'Python': + get.path = public.M('sites').where('id=?', + (get.id,)).getField('path') + runPath = '' + else: + runPath = self.GetRunPath(get) + if runPath != False and runPath != '/': get.path += runPath + authfile = get.path + '/.well-known/pki-validation/fileauth.txt' + if not self.CheckDomain(get): + if not os.path.exists(authfile): + return public.returnMsg(False, 'Unable to write validation file: {}'.format(authfile)) + else: + msg = '''can't correct access validation file
                                    {c_url}

                                    +

                                    Possible cause:

                                    + 1、the resolution is not correct, or the resolution does not work [please resolve the domain correctly, or wait for the resolution to work and try again]
                                    + 2、 check whether the 301/302 redirection is set [please temporarily turn off the redirection related configuration]
                                    + 3、 Check whether the site has HTTPS deployed and set mandatory HTTPS [Please temporarily turn off mandatory HTTPS feature]
                                    '''.format( + c_url=self._check_url) + return public.returnMsg(False, msg) + + action = 'ApplyDVSSL' + if hasattr(get, 'partnerOrderId'): + self.__PDATA['data']['partnerOrderId'] = get.partnerOrderId + action = 'ReDVSSL' + + self.__PDATA['data']['domain'] = get.domain + self.__PDATA['data']['orgPhone'] = get.orgPhone + self.__PDATA['data']['orgPostalCode'] = get.orgPostalCode + self.__PDATA['data']['orgRegion'] = get.orgRegion + self.__PDATA['data']['orgCity'] = get.orgCity + self.__PDATA['data']['orgAddress'] = get.orgAddress + self.__PDATA['data']['orgDivision'] = get.orgDivision + self.__PDATA['data']['orgName'] = get.orgName + self.__PDATA['data'] = self.De_Code(self.__PDATA['data']) + try: + result = public.httpPost(self.__APIURL + 'user/' + action, self.__PDATA) + except Exception as ex: + raise public.error_conn_cloud(str(ex)) + try: + result = json.loads(result) + except: + return result + if 'status' in result: + if not result['status']: return result + result['data'] = self.En_Code(result['data']) + try: + if not 'authPath' in result['data']: result['data']['authPath'] = '/.well-known/pki-validation/' + authfile = get.path + result['data']['authPath'] + result['data']['authKey'] + except: + if 'authKey' in result['data']: + authfile = get.path + '/.well-known/pki-validation/' + result['data']['authKey'] + else: + return public.returnMsg(False, ' Failed to get the validation file!') + + if 'authValue' in result['data']: + public.writeFile(authfile, result['data']['authValue']) + return result + + + # 发送请求 todo + def request(self, dname): + self.__PDATA['data'] = json.dumps(self.__PDATA['data']) + url_headers = { + "authorization": "bt {}".format(self.__userInfo['token']) + } + + result = public.return_msg_gettext(False, 'The request failed, please try again later!') + try: + # response_data = public.httpPost(self.__APIURL + '/' + dname, self.__PDATA) + response_data = public.httpPost(self.__APIURL + '/' + dname, data=self.__PDATA, headers=url_headers) + except Exception as ex: + raise public.error_conn_cloud(str(ex)) + try: + result = json.loads(response_data) + except: + pass + return result + + # # 发送请求 todo 测试购买证书 + # def request_test(self, dname): + # self.__PDATA['data'] = json.dumps(self.__PDATA['data']) + # # "Content-Type": "application/json", + # url_headers = { + # "authorization": "bt {}".format(self.__userInfo['token']) + # } + # + # result = public.return_msg_gettext(False, '测试用 The request failed, please try again later!') + # try: + # response_data = public.httpPost(self.__APIURLtest + '/' + dname, data=self.__PDATA, headers=url_headers) + # + # # public.print_log("******************** url: {}".format(self.__APIURLtest + '/' + dname)) + # + # except Exception as ex: + # raise public.error_conn_cloud(str(ex)) + # + # + # try: + # result = json.loads(response_data) + # except: + # pass + # return result + + # 获取订单列表 ??? + def GetOrderList(self, get): + if hasattr(get, 'siteName'): + path = '/etc/letsencrypt/live/' + get.siteName + '/partnerOrderId' + if os.path.exists(path): + self.__PDATA['data']['partnerOrderId'] = public.readFile(path) + else: + path = '/www/server/panel/vhost/cert/' + get.siteName + '/partnerOrderId' + if os.path.exists(path): + self.__PDATA['data']['partnerOrderId'] = public.readFile(path) + + self.__PDATA['data'] = self.De_Code(self.__PDATA['data']) + try: + rs = public.httpPost(self.__APIURL + 'user/GetSSLList', self.__PDATA) + except Exception as ex: + raise public.error_conn_cloud(str(ex)) + try: + result = json.loads(rs) + except: + return public.return_msg_gettext(False, 'Failed to get, please try again later!') + + result['data'] = self.En_Code(result['data']) + for i in range(len(result['data'])): + result['data'][i]['endtime'] = self.add_months(result['data'][i]['createTime'], + result['data'][i]['validityPeriod']) + return result + + # 计算日期增加(月) + def add_months(self, dt, months): + import calendar + dt = datetime.datetime.fromtimestamp(dt / 1000) + month = dt.month - 1 + months + year = dt.year + month // 12 + month = month % 12 + 1 + + day = min(dt.day, calendar.monthrange(year, month)[1]) + return (time.mktime(dt.replace(year=year, month=month, day=day).timetuple()) + 86400) * 1000 + + # 申请证书 + def GetDVSSL(self, get): + get.id = public.M('domain').where('name=?', (get.domain,)).getField('pid') + if hasattr(get, 'siteName'): + get.path = public.M('sites').where('id=?', (get.id,)).getField('path') + else: + get.siteName = public.M('sites').where('id=?', (get.id,)).getField('name') + + # 当申请二级域名为www时,检测主域名是否绑定到同一网站 + if get.domain[:4] == 'www.': + if not public.M('domain').where('name=? AND pid=?', (get.domain[4:], get.id)).count(): + return public.return_msg_gettext(False, + "Apply for [{}] certificate to verify [{}] Please bind [{}] and resolve to the site!".format( + get.domain, get.domain[4:], get.domain[4:])) + + # 检测是否开启强制HTTPS + if not self.CheckForceHTTPS(get.siteName): + return public.return_msg_gettext(False, + '[Force HTTPS] is enabled on the current website, please turn off this function before applying for an SSL certificate!') + + # 获取真实网站运行目录 + runPath = self.GetRunPath(get) + if runPath != False and runPath != '/': get.path += runPath + + # 提前模拟测试验证文件值是否正确 + authfile = get.path + '/.well-known/pki-validation/fileauth.txt' + if not self.CheckDomain(get): + if not os.path.exists(authfile): + return public.return_msg_gettext(False, 'Cannot create [{}]', (authfile,)) + else: + msg = ''''Unable to access the verification file
                                    {c_url}

                                    +

                                    Possible reasons:

                                    + 1. Incorrect or ineffective DNS resolution [Please ensure correct domain name resolution or wait for the resolution to take effect and try again]
                                    + 2. Check if there are any 301/302 redirects set [Temporarily disable redirect-related configurations]
                                    + 3. Check if the website has enforced HTTPS [Temporarily disable the enforced HTTPS feature]
                                    '''.format( + c_url=self._check_url) + return public.return_msg_gettext(False, msg) + + action = 'GetDVSSL' + if hasattr(get, 'partnerOrderId'): + self.__PDATA['data']['partnerOrderId'] = get.partnerOrderId + action = 'ReDVSSL' + + self.__PDATA['data']['domain'] = get.domain + self.__PDATA['data'] = self.De_Code(self.__PDATA['data']) + result = public.httpPost(self.__APIURL + 'user/' + action, self.__PDATA) + try: + result = json.loads(result) + except: + return result + result['data'] = self.En_Code(result['data']) + + try: + if 'authValue' in result['data'].keys(): + public.writeFile(authfile, result['data']['authValue']) + except: + try: + public.writeFile(authfile, result['data']['authValue']) + except: + return result + + return result + + # 检测是否强制HTTPS + def CheckForceHTTPS(self, siteName): + conf_file = '/www/server/panel/vhost/nginx/{}.conf'.format(siteName) + if not os.path.exists(conf_file): + return True + + conf_body = public.readFile(conf_file) + if not conf_body: return True + if conf_body.find('HTTP_TO_HTTPS_START') != -1: + return False + return True + + # 获取运行目录 + def GetRunPath(self, get): + if hasattr(get, 'siteName'): + get.id = public.M('sites').where('name=?', (get.siteName,)).getField('id') + else: + get.id = public.M('sites').where('path=?', (get.path,)).getField('id') + if not get.id: return False + import panelSite + result = panelSite.panelSite().GetSiteRunPath(get) + return result['runPath'] + + # 检查域名是否解析 + def CheckDomain(self, get): + try: + # 创建目录 + spath = get.path + '/.well-known/pki-validation' + if not os.path.exists(spath): + os.makedirs(spath, 0o755, True) + # public.ExecShell("mkdir -p '" + spath + "'") + + # 生成并写入检测内容 + epass = public.GetRandomString(32) + public.writeFile(spath + '/fileauth.txt', epass) + + # 检测目标域名访问结果 + if get.domain[:4] == 'www.': # 申请二级域名为www时检测主域名 + get.domain = get.domain[4:] + + import http_requests + self._check_url = 'http://127.0.0.1/.well-known/pki-validation/fileauth.txt' + result = http_requests.get(self._check_url, s_type='curl', timeout=6, headers={"host": get.domain}).text + self.__test = result + if result == epass: return True + self._check_url = self._check_url.replace('127.0.0.1', get.domain) + return False + except: + self._check_url = self._check_url.replace('127.0.0.1', get.domain) + return False + + # 确认域名 + def Completed(self, get): + self.__PDATA['data']['partnerOrderId'] = get.partnerOrderId + self.__PDATA['data'] = self.De_Code(self.__PDATA['data']) + if hasattr(get, 'siteName'): + get.path = public.M('sites').where('name=?', (get.siteName,)).getField('path') + if public.M('sites').where('id=?', + (public.M('domain').where('name=?', (get.siteName)).getField('pid'),)).getField( + 'project_type') == 'Java' or public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (get.siteName)).getField('pid'),)).getField( + 'project_type') == 'Go' or public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (get.siteName)).getField('pid'),)).getField( + 'project_type') == 'Other': + runPath = '/www/wwwroot/java_node_ssl' + else: + runPath = self.GetRunPath(get) + if runPath != False and runPath != '/': get.path += runPath + tmp = public.httpPost(self.__APIURL + 'user/SyncOrder', self.__PDATA) + try: + sslInfo = json.loads(tmp) + except: + return public.return_msg_gettext(False, tmp) + + sslInfo['data'] = self.En_Code(sslInfo['data']) + try: + + if public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (get.siteName)).getField('pid'),)).getField( + 'project_type') == 'Java' or public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (get.siteName)).getField('pid'),)).getField( + 'project_type') == 'Go' or public.M('sites').where('id=?', ( + public.M('domain').where('name=?', (get.siteName)).getField('pid'),)).getField( + 'project_type') == 'Other': + spath = '/www/wwwroot/java_node_ssl/.well-known/pki-validation' + else: + spath = get.path + '/.well-known/pki-validation' + if not os.path.exists(spath): public.ExecShell("mkdir -p '" + spath + "'") + public.writeFile(spath + '/' + sslInfo['data']['authKey'], sslInfo['data']['authValue']) + except: + return public.return_msg_gettext(False, 'Verification error!') + try: + result = json.loads(public.httpPost(self.__APIURL + 'user/Completed', self.__PDATA)) + if 'data' in result: + result['data'] = self.En_Code(result['data']) + except: + result = public.return_msg_gettext(True, 'Checking...') + n = 0; + my_ok = False + while True: + if n > 5: break + time.sleep(5) + rRet = json.loads(public.httpPost(self.__APIURL + 'user/SyncOrder', self.__PDATA)) + n += 1 + rRet['data'] = self.En_Code(rRet['data']) + try: + if rRet['data']['stateCode'] == 'COMPLETED': + my_ok = True + break + except: + return public.get_error_info() + if not my_ok: return result + return rRet + + # 同步指定订单 + def SyncOrder(self, get): + self.__PDATA['data']['partnerOrderId'] = get.partnerOrderId + self.__PDATA['data'] = self.De_Code(self.__PDATA['data']) + result = json.loads(public.httpPost(self.__APIURL + 'user/SyncOrder', self.__PDATA)) + result['data'] = self.En_Code(result['data']) + return result + + # 获取证书 + def GetSSLInfo(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.__PDATA['data']['partnerOrderId'] = get.partnerOrderId + self.__PDATA['data'] = self.De_Code(self.__PDATA['data']) + time.sleep(3) + result = json.loads(public.httpPost(self.__APIURL + 'user/GetSSLInfo', self.__PDATA)) + result['data'] = self.En_Code(result['data']) + if not 'privateKey' in result['data']: return result + + # 写配置到站点 + if hasattr(get, 'siteName'): + try: + siteName = get.siteName + path = '/www/server/panel/vhost/cert/' + siteName + if not os.path.exists(path): + public.ExecShell('mkdir -p ' + path) + csrpath = path + "/fullchain.pem" + keypath = path + "/privkey.pem" + pidpath = path + "/partnerOrderId" + # 清理旧的证书链 + public.ExecShell('rm -f ' + keypath) + public.ExecShell('rm -f ' + csrpath) + public.ExecShell('rm -rf ' + path + '-00*') + public.ExecShell('rm -rf /etc/letsencrypt/archive/' + get.siteName) + public.ExecShell('rm -rf /etc/letsencrypt/archive/' + get.siteName + '-00*') + public.ExecShell('rm -f /etc/letsencrypt/renewal/' + get.siteName + '.conf') + public.ExecShell('rm -f /etc/letsencrypt/renewal/' + get.siteName + '-00*.conf') + public.ExecShell('rm -f ' + path + '/README') + public.ExecShell('rm -f ' + path + '/certOrderId') + + public.writeFile(keypath, result['data']['privateKey']) + public.writeFile(csrpath, result['data']['cert'] + result['data']['certCa']) + public.writeFile(pidpath, get.partnerOrderId) + import panel_site_v2 as panelSite + panelSite.panelSite().SetSSLConf(get) + public.serviceReload() + return public.return_msg_gettext(True, 'Setup successfully!') + except: + return public.return_msg_gettext(False, 'Failed to set') + result['data'] = self.En_Code(result['data']) + return result + + def GetSiteDomain(self, get): + """ + @name 获取网站域名对应的站点名 + @param cert_list 证书域名列表 + @auther hezhihong + return 证书域名对应的站点名字典,如证书域名未绑定则为空 + """ + all_site = [] # 所有站点名列表 + cert_list = [] # 证书域名列表 + site_list = [] # 证书域名列表对应的站点名列表 + all_domain = [] # 所有域名列表 + try: + cert_list = json.loads(get.cert_list) + except: + pass + result = {} + # 取所有站点名和所有站点的绑定域名 + all_sites = public.M('sites').field('name').select() + for site in all_sites: + all_site.append(site['name']) + if not cert_list: continue + tmp_dict = {} + tmp_dict['name'] = site['name'] + pid = public.M('sites').where("name=?", (site['name'],)).getField('id') + domain_list = public.M('domain').where("pid=?", (pid,)).field('name').select() + for domain in domain_list: + all_domain.append(domain['name']) + # 取证书域名所在的所有域名列表 + site_domain = [] # 证书域名对应的站点名列表 + if cert_list and all_domain: + for cert in cert_list: + d_cert = '' + if re.match(r"^\*\..*", cert): + d_cert = cert.replace('*.', '') + for domain in all_domain: + if cert == domain: + site_domain.append(domain) + else: + replace_str = domain.split('.')[0] + '.' + if d_cert and d_cert == domain.replace(replace_str, ''): + site_domain.append(domain) + # 取证书域名对应的站点名 + for site in site_domain: + site_id = public.M('domain').where("name=?", (site,)).getField('pid') + site_name = public.M('sites').where("id=?", (site_id,)).getField('name') + site_list.append(site_name) + site_list = sorted(set(site_list), key=site_list.index) + result['all'] = all_site + result['site'] = site_list + return result + + def SetBatchCertToSite(self, get): + """ + @name 批量部署证书 + @auther hezhihong + """ + ssl_list = [] + if not hasattr(get, 'BatchInfo') or not get.BatchInfo: + return public.returnMsg(False, 'parameter error') + else: + ssl_list = json.loads(get.BatchInfo) + if isinstance(ssl_list, list): + total_num = len(ssl_list) + resultinfo = {"total": total_num, "success": 0, "faild": 0, "successList": [], "faildList": []} + successList = [] + faildList = [] + successnum = 0 + failnum = 0 + for Info in ssl_list: + set_result = {} + set_result['status'] = True + get.certName = set_result['certName'] = Info['certName'] + get.siteName = set_result['siteName'] = str(Info['siteName']) # 站点名称必定为字符串 + get.isBatch = True + if "ssl_hash" in Info: + get.ssl_hash = Info['ssl_hash'] + result = self.SetCertToSite(get) + if not result: + set_result['status'] = False + failnum += 1 + faildList.append(set_result) + else: + successnum += 1 + successList.append(set_result) + public.writeSpeed('setssl', successnum + failnum, total_num) + import firewalls + get.port = '443' + get.ps = 'HTTPS' + firewalls.firewalls().AddAcceptPort(get) + public.serviceReload() + resultinfo['success'] = successnum + resultinfo['faild'] = failnum + resultinfo['successList'] = successList + resultinfo['faildList'] = faildList + + if hasattr(get, "set_https_mode") and get.set_https_mode.strip() in (True, 1, "1", "true"): + import panelSite + sites_obj = panelSite.panelSite() + if not sites_obj.get_https_mode(): + sites_obj.set_https_mode() + + else: + return public.returnMsg(False, 'Parameter type error') + return resultinfo + + # 部署证书夹证书 + def SetCertToSite(self, get): + """ + @name 兼容批量部署 + @auther hezhihong + """ + + # 校验参数 + try: + get.validate([ + Param('siteName').String(), + Param('certName').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + try: + result = self.GetCert(get) + if not 'privkey' in result: return result + siteName = get.siteName + path = '/www/server/panel/vhost/cert/' + siteName + if not os.path.exists(path): + public.ExecShell('mkdir -p ' + path) + csrpath = path + "/fullchain.pem" + keypath = path + "/privkey.pem" + + # 清理旧的证书链 + public.ExecShell('rm -f ' + keypath) + public.ExecShell('rm -f ' + csrpath) + public.ExecShell('rm -rf ' + path + '-00*') + public.ExecShell('rm -rf /etc/letsencrypt/archive/' + get.siteName) + public.ExecShell('rm -rf /etc/letsencrypt/archive/' + get.siteName + '-00*') + public.ExecShell('rm -f /etc/letsencrypt/renewal/' + get.siteName + '.conf') + public.ExecShell('rm -f /etc/letsencrypt/renewal/' + get.siteName + '-00*.conf') + public.ExecShell('rm -f ' + path + '/README') + if os.path.exists(path + '/certOrderId'): os.remove(path + '/certOrderId') + + public.writeFile(keypath, result['privkey']) + public.writeFile(csrpath, result['fullchain']) + import panel_site_v2 as panelSite + panelSite.panelSite().SetSSLConf(get) + public.serviceReload() + return public.return_message(0,0, 'Setup successfully!') + except Exception as ex: + if 'isBatch' in get: return public.return_message(-1,0,"") + return public.return_message(-1,0, 'SET_ERROR,' + public.get_error_info()) + + # 获取证书列表 + def GetCertList(self, get): + try: + vpath = '/www/server/panel/vhost/ssl' + if not os.path.exists(vpath): public.ExecShell("mkdir -p " + vpath) + data = [] + for d in os.listdir(vpath): + mpath = vpath + '/' + d + '/info.json' + if not os.path.exists(mpath): continue + tmp = public.readFile(mpath) + if not tmp: continue + tmp1 = json.loads(tmp) + data.append(tmp1) + return data + except: + return [] + + # 删除证书 + def RemoveCert(self, get): + # 校验参数 + try: + get.validate([ + Param('certName').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + vpath = '/www/server/panel/vhost/ssl/' + get.certName.replace("*.", '') + if not os.path.exists(vpath): return public.return_msg_gettext(False, 'Certificate does NOT exist!') + public.ExecShell("rm -rf " + vpath) + return public.return_msg_gettext(True, 'Certificate deleted!') + except: + return public.return_msg_gettext(False, 'Failed to delete!') + + # 保存证书 + def SaveCert(self, get): + try: + certInfo = self.GetCertName(get) + if not certInfo: + return_message=public.return_msg_gettext(False, 'Certificate parsing failed') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + SSLManger().save_by_file(get.certPath, get.keyPath) + vpath = '/www/server/panel/vhost/ssl/' + certInfo['subject'] + vpath = vpath.replace("*.", '') + if not os.path.exists(vpath): + public.ExecShell("mkdir -p " + vpath) + public.writeFile(vpath + '/privkey.pem', public.readFile(get.keyPath)) + public.writeFile(vpath + '/fullchain.pem', public.readFile(get.certPath)) + public.writeFile(vpath + '/info.json', json.dumps(certInfo)) + return_message=public.return_msg_gettext(True, 'Successfully saved certificate!') + del return_message['status'] + return public.return_message(0,0, return_message['msg']) + except: + return_message=public.return_msg_gettext(False, 'Failed to save certificate!') + del return_message['status'] + return public.return_message(-1,0, return_message['msg']) + + # 读取证书 + def GetCert(self, get): + vpath = os.path.join('/www/server/panel/vhost/ssl', get.certName.replace("*.", '')) + if not os.path.exists(vpath): return public.return_msg_gettext(False, 'Certificate does NOT exist!') + data = {} + data['privkey'] = public.readFile(vpath + '/privkey.pem') + data['fullchain'] = public.readFile(vpath + '/fullchain.pem') + return data + + # 获取证书名称 + def GetCertName(self, get): + return self.get_cert_init(get.certPath) + # try: + # openssl = '/usr/local/openssl/bin/openssl' + # if not os.path.exists(openssl): openssl = 'openssl' + # result = public.ExecShell(openssl + " x509 -in "+get.certPath+" -noout -subject -enddate -startdate -issuer") + # tmp = result[0].split("\n") + # data = {} + # data['subject'] = tmp[0].split('=')[-1] + # data['notAfter'] = self.strfToTime(tmp[1].split('=')[1]) + # data['notBefore'] = self.strfToTime(tmp[2].split('=')[1]) + # if tmp[3].find('O=') == -1: + # data['issuer'] = tmp[3].split('CN=')[-1] + # else: + # data['issuer'] = tmp[3].split('O=')[-1].split(',')[0] + # if data['issuer'].find('/') != -1: data['issuer'] = data['issuer'].split('/')[0] + # result = public.ExecShell(openssl + " x509 -in "+get.certPath+" -noout -text|grep DNS") + # data['dns'] = result[0].replace('DNS:','').replace(' ','').strip().split(',') + # return data + # except: + # print(public.get_error_info()) + # return None + + def get_unixtime(self, data, format="%Y-%m-%d %H:%M:%S"): + import time + timeArray = time.strptime(data, format) + timeStamp = int(time.mktime(timeArray)) + return timeStamp + + # 获取指定证书基本信息 + def get_cert_init(self, pem_file): + if not os.path.exists(pem_file): + return None + try: + import OpenSSL + result = {} + x509 = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, public.readFile(pem_file)) + # 取产品名称 + issuer = x509.get_issuer() + result['issuer'] = '' + if hasattr(issuer, 'CN'): + result['issuer'] = issuer.CN + if not result['issuer']: + is_key = [b'0', '0'] + issue_comp = issuer.get_components() + if len(issue_comp) == 1: + is_key = [b'CN', 'CN'] + for iss in issue_comp: + if iss[0] in is_key: + result['issuer'] = iss[1].decode() + break + if not result['issuer']: + if hasattr(issuer, 'O'): + result['issuer'] = issuer.O + # 取到期时间 + result['notAfter'] = self.strf_date( + bytes.decode(x509.get_notAfter())[:-1]) + # 取申请时间 + result['notBefore'] = self.strf_date( + bytes.decode(x509.get_notBefore())[:-1]) + # 取可选名称 + result['dns'] = [] + for i in range(x509.get_extension_count()): + s_name = x509.get_extension(i) + if s_name.get_short_name() in [b'subjectAltName', 'subjectAltName']: + s_dns = str(s_name).split(',') + for d in s_dns: + result['dns'].append(d.split(':')[1]) + subject = x509.get_subject().get_components() + # 取主要认证名称 + if len(subject) == 1: + result['subject'] = subject[0][1].decode() + else: + if not result['dns']: + for sub in subject: + if sub[0] == b'CN': + result['subject'] = sub[1].decode() + break + # result['dns'].append(result['subject']) + if 'subject' in result: + result['dns'].append(result['subject']) + + else: + result['subject'] = result['dns'][0] + result['endtime'] = int( + int(time.mktime(time.strptime(result['notAfter'], "%Y-%m-%d")) - time.time()) / 86400) + return result + except: + return None + + # 转换时间 + def strf_date(self, sdate): + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + + # 转换时间 + def strfToTime(self, sdate): + import time + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%b %d %H:%M:%S %Y %Z')) + + # 获取产品列表 + def GetSSLProduct(self, get): + self.__PDATA['data'] = self.De_Code(self.__PDATA['data']) + result = json.loads(public.httpPost(self.__APIURL + 'user/GetSSLProduct', self.__PDATA)) + result['data'] = self.En_Code(result['data']) + return result + + # 加密数据 + def De_Code(self, data): + if sys.version_info[0] == 2: + import urllib + pdata = urllib.urlencode(data) + return binascii.hexlify(pdata) + else: + import urllib.parse + pdata = urllib.parse.urlencode(data) + if type(pdata) == str: pdata = pdata.encode('utf-8') + return binascii.hexlify(pdata).decode() + + # 解密数据 + def En_Code(self, data): + if sys.version_info[0] == 2: + import urllib + result = urllib.unquote(binascii.unhexlify(data)) + else: + import urllib.parse + if type(data) == str: data = data.encode('utf-8') + tmp = binascii.unhexlify(data) + if type(tmp) != str: tmp = tmp.decode('utf-8') + result = urllib.parse.unquote(tmp) + + if type(result) != str: result = result.decode('utf-8') + return json.loads(result) + + # 手动一键续签 + def renew_lets_ssl(self, get): + if not os.path.exists('vhost/cert/crontab.json'): + return public.return_msg_gettext(False, 'There are currently no certificates to renew!') + + old_list = json.loads(public.ReadFile("vhost/cert/crontab.json")) + cron_list = old_list + if hasattr(get, 'siteName'): + if not get.siteName in old_list: + return public.return_msg_gettext(False, + 'There is no certificate that can be renewed on the current website..') + cron_list = {} + cron_list[get.siteName] = old_list[get.siteName] + + import panelLets + lets = panelLets.panelLets() + + result = {} + result['status'] = True + result['sucess_list'] = [] + result['err_list'] = [] + for siteName in cron_list: + data = cron_list[siteName] + ret = lets.renew_lest_cert(data) + if ret['status']: + result['sucess_list'].append(siteName) + else: + result['err_list'].append({"siteName": siteName, "msg": ret['msg']}) + return result + + # todo? + def renew_cert_order(self, args): + ''' + @name 续签商用证书 + @author cjx + @version 1.0 + ''' + if not 'pdata' in args: + return public.returnMsg(False, 'The pdata parameter cannot be empty!') + pdata = json.loads(args.pdata) + self.__PDATA['data'] = pdata + + result = self.request('renew_cert_order') + if result['status'] == True: + self.__PDATA['data'] = {} + args['oid'] = result['oid'] + result['verify_info'] = self.get_verify_info(args) + return result + + def GetAuthToken(self, get): + """ + 登录官网获取Token + @get.username 官网手机号 + @get.password 官网账号密码 + """ + rtmp = "" + data = {} + data['username'] = public.rsa_decrypt(get.username) + data['password'] = public.md5(public.rsa_decrypt(get.password)) + data['serverid'] = panelAuth().get_serverid() + + if 'code' in get: data['code'] = get.code + if 'token' in get: data['token'] = get.token + + pdata = {} + pdata['data'] = self.De_Code(data) + try: + rtmp = public.httpPost(self.__BINDURL, pdata) + result = json.loads(rtmp) + result['data'] = self.En_Code(result['data']) + if not result['status']: return result + + if result['data']: + if result['data']['serverid'] != data['serverid']: # 保存新的serverid + public.writeFile('data/sid.pl', result['data']['serverid']) + public.writeFile(self.__UPATH, json.dumps(result['data'])) + if os.path.exists('data/bind_path.pl'): os.remove('data/bind_path.pl') + public.flush_plugin_list() + del (result['data']) + session['focre_cloud'] = True + return result + except Exception as ex: + error = str(ex) + if error.lower().find('json') >= 0: + error = '
                                    错误:连接宝塔官网异常,请按照以下方法排除问题后重试:
                                    解决方法:https://www.bt.cn/bbs/thread-87257-1-1.html
                                    ' + # raise public.PanelError(error) + return public.returnMsg(False, 6) + else: + return public.returnMsg(False, 6) + # raise public.error_conn_cloud(error) + # return public.returnMsg(False,'连接服务器失败!
                                    {}'.format(rtmp)) + + def GetBindCode(self, get): + """ + 获取验证码 + """ + rtmp = "" + data = {} + data['username'] = get.username + data['token'] = get.token + pdata = {} + pdata['data'] = self.De_Code(data) + try: + rtmp = public.httpPost(self.__CODEURL, pdata) + result = json.loads(rtmp) + return result + except Exception as ex: + raise public.error_conn_cloud(str(ex)) + # return public.returnMsg(False,'连接服务器失败!
                                    ' + rtmp) + + # 解析DNSAPI信息 + def get_dnsapi(self, auth_to): + tmp = auth_to.split('|') + dns_name = tmp[0] + key = "None" + secret = "None" + if len(tmp) < 3: + dnsapi_config = json.loads(public.readFile('{}/config/dns_api.json'.format(public.get_panel_path()))) + for dc in dnsapi_config: + if dc['name'] != dns_name: + continue + if not dc['data']: + continue + key = dc['data'][0]['value'] + secret = dc['data'][1]['value'] + else: + key = tmp[1] + secret = tmp[2] + return dns_name, key, secret + + # 获取dnsapi对象 + def get_dns_class(self, auth_to): + try: + import panelDnsapi + dns_name, key, secret = self.get_dnsapi(auth_to) + dns_class = getattr(panelDnsapi, dns_name)(key, secret) + dns_class._type = 1 + return dns_class + except: + return None + + # 解析域名 + def create_dns_record(self, auth_to, domain, dns_value, original_domain=None): + # 如果为手动解析 + if auth_to == 'dns': + return None + from panelDnsapi import DnsMager + dns_class = DnsMager().get_dns_obj_by_domain(original_domain) + dns_class._type = 1 + if not dns_class: + return public.returnMsg(False, + "The operation failed. Please check that the key is correct") + + # 申请前删除caa记录 + root, zone = public.get_root_domain(domain) + try: + dns_class.remove_record(public.de_punycode(root), '@', 'CAA') + except: + pass + try: + dns_class.create_dns_record(public.de_punycode(domain), dns_value) + return public.returnMsg(True, 'Added successfully') + except: + return public.returnMsg(False, public.get_error_info()) + + # 检测ssl验证方式 + def check_ssl_method(self, get): + """ + @name 检测ssl验证方式 + @domain string 域名 + """ + + domain = get.domain + if public.M('sites').where('id=?', (public.M('domain').where('name=?', (domain)).getField('pid'),)).getField( + 'project_type') == 'Java': + siteRunPath = '{}/java_node_ssl'.format(public.M("config").getField("sites_path")) + else: + siteRunPath = self.get_domain_run_path(domain) + + if not siteRunPath: + return public.returnMsg(False, + 'Failed to get the website path. Please check if the website exists') + + verify_path = siteRunPath + '/.well-known/pki-validation' + if not os.path.exists(verify_path): os.makedirs(verify_path) + + # 生成临时文件 + check_val = public.GetRandomString(16) + verify_file = '{}/{}.txt'.format(verify_path, check_val) + public.writeFile(verify_file, check_val) + if not os.path.exists(verify_file): + return public.returnMsg(False, + 'Failed to create the validation file. Check if the write was blocked') + + res = {} + msg = [' domain name [{}] validation file cannot be accessed correctly'.format(domain), + 'Probable cause', + '1、the resolution was not correct, or the resolution did not work [Please resolve the domain correctly, or wait for the resolution to work and try again]', + '2、check whether 301/302 redirects are set [please temporarily turn off redirects related configuration]', + '3、check whether the site has enabled reverse proxy [please temporarily turn off reverse proxy configuration]' + ] + + res['HTTP_CSR_HASH'] = msg + res['HTTPS_CSR_HASH'] = msg + + # 检测HTTP/https访问 + args = public.dict_obj() + for stype in ['http', 'https']: + args.url = '{}://{}/.well-known/pki-validation/{}.txt'.format(stype, domain, check_val) + args.content = check_val + if self.check_url_txt(args, 2) == 1: + res['{}_CSR_HASH'.format(stype).upper()] = 1 + + # 检测caa记录 + result = self.check_ssl_caa([domain]) + if not result: + res['CNAME_CSR_HASH'] = 1 + else: + res['CNAME_CSR_HASH'] = json.loads(result['data']) + + if os.path.exists(verify_file): + os.remove(verify_file) + return res + + @staticmethod + def upload_cert_to_cloud(get): + ssl_id = None + ssl_hash = None + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "parameter error") + from ssl_manage import SSLManger + try: + return SSLManger().upload_cert(ssl_id, ssl_hash) + except ValueError as e: + return public.returnMsg(False, str(e)) + except Exception as e: + return public.returnMsg(False, "operation mistake:" + str(e)) + + @staticmethod + def remove_cloud_cert(get): + ssl_id = None + ssl_hash = None + local = False + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + + if "local" in get and get.local.strip() in ("1", 1, True, "true"): + local = True + + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "parameter error") + from ssl_manage import SSLManger + try: + return SSLManger().remove_cert(ssl_id, ssl_hash, local=local) + except ValueError as e: + return public.returnMsg(False, str(e)) + except Exception as e: + return public.returnMsg(False, "operation mistake:" + str(e)) + + # 未使用 + @staticmethod + def refresh_cert_list(get=None): + from ssl_manage import SSLManger + try: + return SSLManger().get_cert_list(force_refresh=True) + except ValueError as e: + return public.returnMsg(False, str(e)) + except Exception as e: + return public.returnMsg(False, "operation mistake:" + str(e)) + + @staticmethod + def get_cert_info(get): + ssl_id = None + ssl_hash = None + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "parameter error") + from ssl_manage import SSLManger + try: + ssl_mager = SSLManger() + target = ssl_mager.find_ssl_info(ssl_id, ssl_hash) + if target is None: + return public.returnMsg(False, "No certificate information was obtained") + target.update(ssl_mager.get_cert_for_deploy(target["hash"])) + return target + except ValueError as e: + return public.returnMsg(False, str(e)) + except Exception as e: + return public.returnMsg(False, "operation mistake:" + str(e)) + + @staticmethod + def get_cert_list(get): + """ + search_limit 0 -> 所有证书 + search_limit 1 -> 没有过期的证书 + search_limit 2 -> 有效期小于等于15天的证书 但未过期 + search_limit 3 -> 过期的证书 + search_limit 4 -> 过期时间1年以上的证书 + """ + search_name = None + search_limit = 0 + force_refresh = False + + try: + if "search_name" in get: + search_name = get.search_name.strip() + if "search_limit" in get: + search_limit = int(get.search_limit.strip()) + if "force_refresh" in get and get.force_refresh.strip() in ("1", 1, "True", True): + force_refresh = True + + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "parameter error") + + param = None + if search_name is not None: + param = ['subject LIKE ?', ["%{}%".format(search_name)]] + + now = datetime.datetime.now() + filter_func = lambda x: True + if search_limit == 1: + date = now.strftime("%Y-%m-%d") + filter_func = lambda x: x["not_after"] >= date + elif search_limit == 2: + date1 = now.strftime("%Y-%m-%d") + date2 = (now + datetime.timedelta(days=15)).strftime("%Y-%m-%d") + filter_func = lambda x: date1 <= x["not_after"] <= date2 + elif search_limit == 3: + date = now.strftime("%Y-%m-%d") + filter_func = lambda x: x["not_after"] < date + elif search_limit == 4: + date = (now + datetime.timedelta(days=366)).strftime("%Y-%m-%d") + filter_func = lambda x: x["not_after"] > date + + from ssl_manage import SSLManger + try: + res_list = SSLManger().get_cert_list(param=param, force_refresh=force_refresh) + return list(filter(filter_func, res_list)) + except ValueError as e: + return public.returnMsg(False, str(e)) + except Exception as e: + return public.returnMsg(False, "operation mistake:" + str(e)) diff --git a/class_v2/panel_task_v2.py b/class_v2/panel_task_v2.py new file mode 100644 index 00000000..236f9e0b --- /dev/null +++ b/class_v2/panel_task_v2.py @@ -0,0 +1,686 @@ +#coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2019-2099 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------ +# 消息队列 +# ------------------------------ +import json +import time +import public +import sys +import os +import re +from public.validate import Param +os.chdir('/www/server/panel') +if not 'class/' in sys.path: + sys.path.insert(0,'class/') + +class bt_task: + __table = 'task_list' + __task_tips = '/dev/shm/bt_task_now.pl' + __task_path = '/www/server/panel/tmp/' + down_log_total_file = '/tmp/download_total.pl' + not_web = False + def __init__(self): + + # 创建数据表 + sql = '''CREATE TABLE IF NOT EXISTS `task_list` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `name` TEXT, + `type` TEXT, + `status` INTEGER, + `shell` TEXT, + `other` TEXT, + `exectime` INTEGER, + `endtime` INTEGER, + `addtime` INTEGER +);''' + public.M(None).execute(sql, ()) + + # 创建临时目录 + if not os.path.exists(self.__task_path): + os.makedirs(self.__task_path, 384) + + # 取任务列表 + def get_task_list(self, status=-3): + sql = public.M(self.__table) + if status != -3: + sql = sql.where('status=?', (status,)) + data = sql.field( + 'id,name,type,shell,other,status,exectime,endtime,addtime').select() + return data + + # 取任务列表前端 + def get_task_lists(self, get): + # 校验参数 + try: + get.validate([ + Param('status').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + sql = public.M(self.__table) + if 'status' in get: + if get.status == '-3': + sql = sql.where('status=? OR status=?', (-1, 0)) + else: + sql = sql.where('status=?', (get.status,)) + data = sql.field('id,name,type,shell,other,status,exectime,endtime,addtime').order( + 'id asc').limit('10').select() + if type(data) == str: + public.WriteLog('TASK_QUEUE', data,not_web = self.not_web) + return public.return_message(0,0,[]) + if not 'num' in get: + get.num = 15 + num = int(get.num) + for i in range(len(data)): + data[i]['log'] = '' + if data[i]['status'] == -1: + data[i]['log'] = self.get_task_log( + data[i]['id'], data[i]['type'], num) + elif data[i]['status'] == 1: + data[i]['log'] = self.get_task_log( + data[i]['id'], data[i]['type'], 10) + if data[i]['type'] == '3': + data[i]['other'] = json.loads(data[i]['other']) + return public.return_message(0,0,data) + + # 创建任务 + def create_task(self, task_name, task_type, task_shell, other=''): + self.clean_log() + task_id = public.M(self.__table).add('name,type,shell,other,addtime,status', + (task_name, task_type, task_shell, other, int(time.time()), 0)) + public.WriteFile(self.__task_tips, 'True') + public.ExecShell("/etc/init.d/bt start") + if not public.M(self.__table).where('status=?', ('-1',)).count(): + tip_file = "/dev/shm/.start_task.pl" + tip_time = public.readFile(tip_file) + if not tip_time or time.time() - int(tip_time) > 600: + public.ExecShell("/www/server/panel/BT-Task") + public.print_log("Background task restarted") + return task_id + + # 修改任务 + def modify_task(self, id, key, value): + public.M(self.__table).where('id=?', (id,)).setField(key, value) + return True + + # 删除任务 + def remove_task(self, get): + task_info = self.get_task_find(get.id) + public.M(self.__table).where('id=?', (get.id,)).delete() + if str(task_info['status']) == '-1': + public.ExecShell( + "kill -9 $(ps aux|grep 'task.py'|grep -v grep|awk '{print $2}')") + if task_info['type'] == '1': + public.ExecShell( + "kill -9 $(ps aux|grep '{}')".format(task_info['other'])) + time.sleep(1) + if os.path.exists(task_info['other']): + os.remove(task_info['other']) + elif task_info['type'] == '3': + z_info = json.loads(task_info['other']) + if z_info['z_type'] == 'tar.gz': + public.ExecShell( + "kill -9 $(ps aux|grep 'tar -zcvf'|grep -v grep|awk '{print $2}')") + elif z_info['z_type'] == 'rar': + public.ExecShell( + "kill -9 $(ps aux|grep /www/server/rar/rar|grep -v grep|awk '{print $2}')") + elif z_info['z_type'] == 'zip': + public.ExecShell( + "kill -9 $(ps aux|grep '.zip -r'|grep -v grep|awk '{print $2}')") + public.ExecShell( + "kill -9 $(ps aux|grep '.zip\' -r'|grep -v grep|awk '{print $2}')") + if os.path.exists(z_info['dfile']): + os.remove(z_info['dfile']) + elif task_info['type'] == '2': + public.ExecShell( + "kill -9 $(ps aux|grep 'tar -zxvf'|grep -v grep|awk '{print $2}')") + public.ExecShell( + "kill -9 $(ps aux|grep '/www/server/rar/unrar'|grep -v grep|awk '{print $2}')") + public.ExecShell( + "kill -9 $(ps aux|grep 'unzip -P'|grep -v grep|awk '{print $2}')") + public.ExecShell( + "kill -9 $(ps aux|grep 'gunzip -c'|grep -v grep|awk '{print $2}')") + elif task_info['type'] == '0': + public.ExecShell( + "kill -9 $(ps aux|grep '"+task_info['shell']+"'|grep -v grep|awk '{print $2}')") + + public.ExecShell("/etc/init.d/bt start") + return public.return_msg_gettext(True, 'Task cancelled!') + + # 取一条任务 + def get_task_find(self, id): + data = public.M(self.__table).where('id=?', (id,)).field( + 'id,name,type,shell,other,status,exectime,endtime,addtime').find() + return data + + # 执行任务 + # task_type 0.执行shell 1.下载文件 2.解压文件 3.压缩文件 + def execute_task(self, id, task_type, task_shell, other=''): + if not os.path.exists(self.__task_path): + os.makedirs(self.__task_path, 384) + log_file = self.__task_path + str(id) + '.log' + + # 标记状态执行时间 + self.modify_task(id, 'status', -1) + self.modify_task(id, 'exectime', int(time.time())) + task_type = int(task_type) + # 开始执行 + if task_type == 0: # 执行命令 + public.ExecShell(task_shell + ' &> ' + log_file) + elif task_type == 1: # 下载文件 + if os.path.exists(self.down_log_total_file): + os.remove(self.down_log_total_file) + public.ExecShell( + "wget -O '{}' '{}' --no-check-certificate -T 30 -t 5 -d &> {}".format(other, task_shell, log_file)) + if os.path.exists(log_file): + os.remove(log_file) + elif task_type == 2: # 解压文件 + zip_info = json.loads(other) + self._unzip(task_shell, zip_info['dfile'], + zip_info['password'], log_file) + elif task_type == 3: # 压缩文件 + zip_info = json.loads(other) + if not 'z_type' in zip_info: + zip_info['z_type'] = 'tar.gz' + print(self._zip( + task_shell, zip_info['sfile'], zip_info['dfile'], log_file, zip_info['z_type'])) + elif task_type == 4: # 备份数据库 + self.backup_database(task_shell, log_file) + elif task_type == 5: # 导入数据库 + self.input_database(task_shell, other, log_file) + elif task_type == 6: # 备份网站 + self.backup_site(task_shell, log_file) + elif task_type == 7: # 恢复网站 + pass + # 标记状态与结束时间 + self.modify_task(id, 'status', 1) + self.modify_task(id, 'endtime', int(time.time())) + + # 开始检测任务 + def start_task(self): + noe = False + n = 0 + tip_file = '/dev/shm/.start_task.pl' + while True: + try: + time.sleep(1) + public.writeFile(tip_file, str(int(time.time()))) + n += 1 + if not os.path.exists(self.__task_tips) and noe and n < 60: + continue + if os.path.exists(self.__task_tips): + os.remove(self.__task_tips) + n = 0 + public.M(self.__table).where( + 'status=?', ('-1',)).setField('status', 0) + task_list = self.get_task_list(0) + for task_info in task_list: + self.execute_task( + task_info['id'], task_info['type'], task_info['shell'], task_info['other']) + noe = True + except: + print(public.get_error_info()) + + # 前端通过任务ID取某一个任务的日志 + def get_task_log_by_id(self, get): + task_id = get.id + task_type = get.task_type + log_data = {} + if "num" in get: + num = int(get.num) + log_data = self.get_task_log(task_id, task_type, num) + else: + log_data = self.get_task_log(task_id, task_type) + task_obj = self.get_task_find(task_id) + log_data["status"] = task_obj["status"] + return log_data + + # 取任务执行日志 + def get_task_log(self, id, task_type, num=5): + log_file = self.__task_path + str(id) + '.log' + if not os.path.exists(log_file): + data = '' + if(task_type == '1'): + data = {'name': public.get_msg_gettext('Download file'), 'total': 0, 'used': 0, + 'pre': 0, 'speed': 0, 'time': 0} + return data + + if(task_type == '1'): + total = 0 + if not os.path.exists(self.down_log_total_file): + f = open(log_file, 'r') + head = f.read(4096) + content_length = re.findall(r"Length:\s+(\d+)", head) + if content_length: + total = int(content_length[0]) + public.writeFile(self.down_log_total_file, + content_length[0]) + else: + total = public.readFile(self.down_log_total_file) + if not total: + total = 0 + total = int(total) + + filename = public.M(self.__table).where( + 'id=?', (id,)).getField('shell') + + speed_tmp = public.ExecShell("tail -n 2 {}".format(log_file))[0] + speed_total = re.findall( + r"([\d\.]+[BbKkMmGg]).+\s+(\d+)%\s+([\d\.]+[KMBGkmbg])\s+(\w+[sS])", speed_tmp) + if not speed_total: + data = {'name':public.get_msg_gettext('Download file {}',(filename,)),'total':0,'used':0,'pre':0,'speed':0,'time':0} + else: + speed_total = speed_total[0] + used = speed_total[0] + if speed_total[0].lower().find('k') != -1: + used = public.to_size( + float(speed_total[0].lower().replace('k', '')) * 1024) + u_time = speed_total[3].replace( + 'h', 'Hour').replace('m', 'Minute').replace('s', 'Second') + data = {'name': public.get_msg_gettext('Download file {}',(filename,)),'total': total, 'used': used, 'pre': speed_total[1], 'speed': speed_total[2], 'time': u_time} + else: + data = public.ExecShell("tail -n {} {}".format(num, log_file))[0] + if type(data) == list: + return '' + if isinstance(data,bytes): + data = data.decode('utf-8') + data = data.replace('\x08', '').replace('\n', '
                                    ') + return data + + # 清理任务日志 + def clean_log(self): + import shutil + s_time = int(time.time()) + timeout = 86400 + for f in os.listdir(self.__task_path): + filename = self.__task_path + f + c_time = os.stat(filename).st_ctime + if s_time - c_time > timeout: + if os.path.isdir(filename): + shutil.rmtree(filename) + else: + os.remove(filename) + return True + + # 文件压缩 + def _zip(self, path, sfile, dfile, log_file, z_type='tar.gz'): + if sys.version_info[0] == 2: + sfile = sfile.encode('utf-8') + dfile = dfile.encode('utf-8') + if sys.version_info[0] == 2: + path = path.encode('utf-8') + if sfile.find(',') == -1: + if not os.path.exists(path+'/'+sfile): + return public.return_msg_gettext(False, 'Configuration file not exist') + # 处理多文件压缩 + sfiles = '' + for sfile in sfile.split(','): + if not sfile: + continue + sfiles += " '" + sfile + "'" + + # 判断压缩格式 + if z_type == 'zip': + public.ExecShell("cd '"+path+"' && zip '"+dfile + + "' -r "+sfiles+" &> "+log_file) + elif z_type == 'tar.gz': + public.ExecShell("cd '" + path + "' && tar -zcvf '" + + dfile + "' " + sfiles + " &> " + log_file) + elif z_type == 'rar': + rar_file = '/www/server/rar/rar' + if not os.path.exists(rar_file): + self.install_rar() + public.ExecShell("cd '" + path + "' && "+rar_file + + " a -r '" + dfile + "' " + sfiles + " &> " + log_file) + elif z_type == '7z': + _7z_bin = self.get_7z_bin() + if not _7z_bin: + self.install_7zip() + err_msg = 'The p7zip component is not installed, an automatic installation has been attempted, please wait a few minutes and try again!' + public.WriteLog("File manager","Failed to compress file, reason: {}, file: {}".format(err_msg,sfile)) + return public.returnMsg(False, err_msg) + public.ExecShell("cd {} && {} a -t7z {} {} -y &> {}".format(path, _7z_bin, dfile, sfiles, log_file)) + else: + return public.return_msg_gettext(False,'Specified compression format is not supported!') + + self.set_file_accept(dfile) + #public.WriteLog("TYPE_FILE", 'Compression succeeded!', (sfiles, dfile),not_web = self.not_web) + public.write_log_gettext("File manager", 'Compressed file [ {} ] to [ {} ] success', (sfiles, dfile)) + return public.return_msg_gettext(True, 'Compression succeeded!') + + # 文件解压 + def _unzip(self, sfile, dfile, password, log_file): + if sys.version_info[0] == 2: + sfile = sfile.encode('utf-8') + dfile = dfile.encode('utf-8') + if not os.path.exists(sfile): + return public.return_msg_gettext(False, 'Configuration file not exist') + + # 判断压缩包格式 + if sfile[-4:] == '.zip': + public.ExecShell("unzip -P '"+password+"' -o '" + + sfile + "' -d '" + dfile + "' &> " + log_file) + elif sfile[-7:] == '.tar.gz' or sfile[-4:] == '.tgz': + public.ExecShell("tar zxvf '" + sfile + + "' -C '" + dfile + "' &> " + log_file) + elif sfile[-4:] == '.rar': + rar_file = '/www/server/rar/unrar' + if not os.path.exists(rar_file): + self.install_rar() + pass_opt = '-p-' + if password: + password = password.replace("&",r"\&").replace('"','\"') + pass_opt = '-p"{}"'.format(password) + + public.ExecShell(rar_file + ' x '+ pass_opt +' -u -y "' + sfile + '" "' + dfile + '" &> ' + log_file) + + elif sfile[-4:] == '.war': + public.ExecShell("unzip -P '"+password+"' -o '" + + sfile + "' -d '" + dfile + "' &> " + log_file) + elif sfile[-4:] == '.bz2': + public.ExecShell("tar jxvf '" + sfile + + "' -C '" + dfile + "' &> " + log_file) + elif sfile[-3:] == '.7z': + _7zbin = self.get_7z_bin() + if not _7zbin: + self.install_7zip() + err_msg = 'The p7zip component is not installed, an automatic installation has been attempted, please wait a few minutes and try again!' + public.WriteLog("File manager","Failed to compress file, reason: {}, file: {}".format(err_msg,sfile)) + return public.returnMsg(False, err_msg) + pass_opt = "" + if password: + pass_opt = '-p"{}"'.format(password) + public.ExecShell('{} x "{}" -o"{}" -y {} &> {}'.format(_7zbin,sfile,dfile,pass_opt,log_file)) + else: + public.ExecShell("gunzip -c " + sfile + " > " + sfile[:-3]) + + # 异常处理 + log_msg = public.readFile(log_file) + err_msg = None + if log_msg: + if log_msg.find("incorrect password") != -1 \ + or log_msg.find("The specified password is incorrect.") != -1 \ + or log_msg.find("Data Error in encrypted file. Wrong password") != -1: + err_msg = 'Decompression password error!' + public.WriteLog("File manager","Unzip file failed, reason: {}, file: {}".format(err_msg,sfile)) + elif log_msg.find("unsupported compression method 99") != -1: + err_msg = 'Unsupported Zip encryption and compression, only ZIP traditional encryption is supported for ZIP archives!' + public.WriteLog("File manager","Unzip file failed, reason: {}, file: {}".format(err_msg,sfile)) + elif log_msg.find("is not RAR archive") != -1: + err_msg = "It is not a rar archive, check whether to modify the file with the extension rar for other compression formats!" + public.WriteLog("File manager","Unzip file failed, reason: {}, file: {}".format(err_msg,sfile)) + elif log_msg.find("gzip: stdin") != -1: + public.ExecShell("tar xvf '" + sfile + "' -C '" + dfile + "' &> " + log_file) + + if err_msg: return public.returnMsg(False, err_msg) + + # 检查是否设置权限 + if self.check_dir(dfile): + sites_path = public.M('config').where( + 'id=?', (1,)).getField('sites_path') + if dfile.find('/www/wwwroot') != -1 or dfile.find(sites_path) != -1: + self.set_file_accept(dfile) + else: + import pwd + user = pwd.getpwuid(os.stat(dfile).st_uid).pw_name + public.ExecShell("chown %s:%s %s" % (user, user, dfile)) + + #public.WriteLog("TYPE_FILE", 'Uncompression succeeded!', (sfile, dfile),not_web = self.not_web) + public.write_log_gettext("File manager", 'unzip file [ {} ] -> [ {} ] success', (sfile, dfile)) + return public.return_msg_gettext(True, 'Uncompression succeeded!') + + def get_7z_bin(self): + ''' + @name 获取7z命令路径 + @author hwliang + @return {string} 7z命令路径 + ''' + _7z_bins = ["/usr/bin/7z","/usr/bin/7za","/usr/bin/7zr"] + for _7z_bin in _7z_bins: + if os.path.exists(_7z_bin): + return _7z_bin + return None + + def install_7zip(self): + ''' + @name 安装7zip + @author hwliang + @return {bool} True/False + ''' + _7z_bin = self.get_7z_bin() + if _7z_bin: + return True + + # 是否已经尝试安装过 + install_tip = '{}/data/7z_install.pl'.format(public.get_panel_path()) + if os.path.exists(install_tip): + return False + + if os.path.exists("/usr/bin/apt-get"): + public.ExecShell("nohup apt-get -y install p7zip-full &> /dev/null &") + elif os.path.exists("/usr/bin/yum"): + public.ExecShell("nohup yum -y install p7zip &> /dev/null &") + elif os.path.exists("/usr/bin/dnf"): + public.ExecShell("nohup dnf -y install p7zip &> /dev/null &") + else: + return False + return True + + # 备份网站 + def backup_site(self, id, log_file): + find = public.M('sites').where( + "id=?", (id,)).field('name,path,id').find() + fileName = find['name']+'_' + \ + time.strftime('%Y%m%d_%H%M%S', time.localtime())+'.zip' + backupPath = public.M('config').where( + 'id=?', (1,)).getField('backup_path') + '/site' + + zipName = backupPath + '/'+fileName + if not (os.path.exists(backupPath)): + os.makedirs(backupPath) + + execStr = "cd '" + find['path'] + "' && zip '" + \ + zipName + "' -x .user.ini -r ./ &> " + log_file + public.ExecShell(execStr) + + sql = public.M('backup').add('type,name,pid,filename,size,addtime', + (0, fileName, find['id'], zipName, 0, public.getDate())) + public.WriteLog('TYPE_SITE', 'SITE_BACKUP_SUCCESS', (find['name'],),not_web = self.not_web) + return public.return_msg_gettext(True, 'Backup Succeeded!') + + # 备份数据库 + def backup_database(self, id, log_file): + name = public.M('databases').where("id=?", (id,)).getField('name') + find = public.M('config').where('id=?', (1,)).field( + 'mysql_root,backup_path').find() + + if not os.path.exists(find['backup_path'] + '/database'): + public.ExecShell('mkdir -p ' + find['backup_path'] + '/database') + self.mypass(True, find['mysql_root']) + + fileName = name + '_' + \ + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.sql.gz' + backupName = find['backup_path'] + '/database/' + fileName + public.ExecShell("/www/server/mysql/bin/mysqldump --force --opt \"" + + name + "\" | gzip > " + backupName) + if not os.path.exists(backupName): + return public.return_msg_gettext(False, 'Backup error!') + + self.mypass(False, find['mysql_root']) + + sql = public.M('backup') + addTime = time.strftime('%Y-%m-%d %X', time.localtime()) + sql.add('type,name,pid,filename,size,addtime', + (1, fileName, id, backupName, 0, addTime)) + public.WriteLog("TYPE_DATABASE", "DATABASE_BACKUP_SUCCESS", (name,),not_web = self.not_web) + return public.return_msg_gettext(True, 'Backup Succeeded!') + + # 导入数据库 + def input_database(self, id, file, log_file): + name = public.M('databases').where("id=?", (id,)).getField('name') + root = public.M('config').where('id=?', (1,)).getField('mysql_root') + tmp = file.split('.') + exts = ['sql', 'gz', 'zip'] + ext = tmp[len(tmp) - 1] + if ext not in exts: + return public.return_msg_gettext(False, 'Select sql/gz/zip file!') + + isgzip = False + if ext != 'sql': + tmp = file.split('/') + tmpFile = tmp[len(tmp)-1] + tmpFile = tmpFile.replace('.sql.' + ext, '.sql') + tmpFile = tmpFile.replace('.' + ext, '.sql') + tmpFile = tmpFile.replace('tar.', '') + backupPath = public.M('config').where( + 'id=?', (1,)).getField('backup_path') + '/database' + + if ext == 'zip': + public.ExecShell("cd " + backupPath + " && unzip " + file) + else: + public.ExecShell("cd " + backupPath + " && tar zxf " + file) + if not os.path.exists(backupPath + "/" + tmpFile): + public.ExecShell("cd " + backupPath + + " && gunzip -q " + file) + isgzip = True + + if not os.path.exists(backupPath + '/' + tmpFile) or tmpFile == '': + return public.return_msg_gettext(False, 'Configuration file not exist', (tmpFile,)) + self.mypass(True, root) + public.ExecShell(public.GetConfigValue('setup_path') + "/mysql/bin/mysql -uroot -p" + + root + " --force \"" + name + "\" < " + backupPath + '/' + tmpFile) + self.mypass(False, root) + if isgzip: + public.ExecShell('cd ' + backupPath + + ' && gzip ' + file.split('/')[-1][:-3]) + else: + public.ExecShell("rm -f " + backupPath + '/' + tmpFile) + else: + self.mypass(True, root) + public.ExecShell(public.GetConfigValue( + 'setup_path') + "/mysql/bin/mysql -uroot -p" + root + " --force \"" + name + "\" < " + file) + self.mypass(False, root) + + public.WriteLog("TYPE_DATABASE", 'Successfully imported database [{}]', (name,),not_web = self.not_web) + return public.return_msg_gettext(True, 'Successfully imported database!') + + # 配置 + def mypass(self, act, root): + my_cnf = '/etc/my.cnf' + public.ExecShell("sed -i '/user=root/d' " + my_cnf) + public.ExecShell("sed -i '/password=/d' " + my_cnf) + if act: + mycnf = public.readFile(my_cnf) + rep = "\\[mysqldump\\]\nuser=root" + sea = "[mysqldump]\n" + subStr = sea + "user=root\npassword=\"" + root + "\"\n" + mycnf = mycnf.replace(sea, subStr) + if len(mycnf) > 100: + public.writeFile(my_cnf, mycnf) + + # 设置权限 + def set_file_accept(self, filename): + # public.ExecShell('chown -R www:www ' + filename) + # public.ExecShell('chmod -R 755 ' + filename) + import files + from collections import namedtuple + get = namedtuple('get',['path']) + get.path = filename + files.files().fix_permissions(get) + + # 检查敏感目录 + def check_dir(self, path): + path = path.replace('//', '/') + if path[-1:] == '/': + path = path[:-1] + + nDirs = ('', + '/', + '/*', + '/www', + '/root', + '/boot', + '/bin', + '/etc', + '/home', + '/dev', + '/sbin', + '/var', + '/usr', + '/tmp', + '/sys', + '/proc', + '/media', + '/mnt', + '/opt', + '/lib', + '/srv', + '/selinux', + '/www/server', + '/www/server/data', + public.GetConfigValue('logs_path'), + public.GetConfigValue('setup_path')) + + return not path in nDirs + + # 安装rar组件 + def install_rar(self): + unrar_file = '/www/server/rar/unrar' + rar_file = '/www/server/rar/rar' + bin_unrar = '/usr/local/bin/unrar' + bin_rar = '/usr/local/bin/rar' + if os.path.exists(unrar_file) and os.path.exists(bin_unrar): + try: + import rarfile + except: + public.ExecShell("pip install rarfile") + return True + + import platform + os_bit = '' + if platform.machine() == 'x86_64': + os_bit = '-x64' + download_url = public.get_url() + '/src/rarlinux'+os_bit+'-5.6.1.tar.gz' + + tmp_file = '/tmp/bt_rar.tar.gz' + public.ExecShell('wget -O ' + tmp_file + ' ' + download_url) + if os.path.exists(unrar_file): + public.ExecShell("rm -rf /www/server/rar") + public.ExecShell("tar xvf " + tmp_file + ' -C /www/server/') + if os.path.exists(tmp_file): + os.remove(tmp_file) + if not os.path.exists(unrar_file): + return False + + if os.path.exists(bin_unrar): + os.remove(bin_unrar) + if os.path.exists(bin_rar): + os.remove(bin_rar) + + public.ExecShell('ln -sf ' + unrar_file + ' ' + bin_unrar) + public.ExecShell('ln -sf ' + rar_file + ' ' + bin_rar) + #public.ExecShell("pip install rarfile") + return True + + +if __name__ == '__main__': + p = bt_task() + #p.create_task('测试执行SHELL',0,'yum install wget -y','') + # print(p.get_task_list()) + # p.modify_task(3,'status',0) + #p.modify_task(3,'shell','bash /www/server/panel/install/install_soft.sh 0 update php 5.6') + # p.modify_task(1,'other','{"sfile":"BTPanel","dfile":"/www/test.rar","z_type":"rar"}') + p.start_task() + # p._zip(sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5]) diff --git a/class_v2/panel_video_V2.py b/class_v2/panel_video_V2.py new file mode 100644 index 00000000..3298d85c --- /dev/null +++ b/class_v2/panel_video_V2.py @@ -0,0 +1,62 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang <2020-05-18> +# +------------------------------------------------------------------- + +# +------------------------------------------------------------------- +# | 流媒体模块 +# +------------------------------------------------------------------- + +import os,sys,re +import mimetypes +import public +from BTPanel import Response + +def get_buff_size(file_size): + buff_size = 2097152 + if file_size > 1073741824: + buff_size = 4194304 + return buff_size + +def partial_response(path, start, end=None): + if not os.path.exists(path): + return Response("file not fount!",404) + file_size = os.path.getsize(path) + buff_size = get_buff_size(file_size) + + if end is None: + end = start + buff_size - 1 + end = min(end, file_size - 1) + end = min(end, start + buff_size - 1) + length = end - start + 1 + + with open(path, 'rb') as fd: + fd.seek(start) + bytes = fd.read(length) + assert len(bytes) == length + + response = Response(bytes,206,mimetype=mimetypes.guess_type(path)[0],direct_passthrough=True,) + response.headers.add('Content-Range', 'bytes {0}-{1}/{2}'.format(start, end, file_size,),) + response.headers.add('Accept-Ranges', 'bytes') + return response + +def get_range(request): + range = request.headers.get('Range') + m = None + if range: + m = re.match(r'bytes=(?P\d+)-(?P\d+)?', range) + if m: + start = m.group('start') + end = m.group('end') + start = int(start) + if end is not None: + end = int(end) + return start, end + else: + return 0, None + + diff --git a/class_v2/panel_warning_v2.py b/class_v2/panel_warning_v2.py new file mode 100644 index 00000000..84348249 --- /dev/null +++ b/class_v2/panel_warning_v2.py @@ -0,0 +1,1668 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang <2020-08-04> +# +------------------------------------------------------------------- + +import os, sys, json, time, datetime +os.chdir("/www/server/panel") +sys.path.append("class/") +import public +from public.validate import Param + +class panelWarning: + __path = '/www/server/panel/data/warning' + __ignore = __path + '/ignore' + __result = __path + '/result' + __risk = __path + '/risk' + _vuln_ignore = __path + '/ignore.json' + _vuln_result = __path + '/result.json' + __repair_count = __path + '/repair_count.json' + __vul_list = __path + '/high_risk_vul-9.json' # 空i那么;皮的泡沫隔离 + __report = '/www/server/panel/data/warning_report' + vul_num = 0 + discov_count = 0 # 扫描中发现的漏洞数 + score = 100 # 扫描中动态分数 + yum_time = __path + '/yum_time.pl' + new_vul_list = __path + '/vul_centos7.json' + product_version = __path + '/product_version.json' + + def __init__(self): + if not os.path.exists(self.__ignore): + os.makedirs(self.__ignore, 384) + if not os.path.exists(self.__result): + os.makedirs(self.__result, 384) + if not os.path.exists(self.__risk): + os.makedirs(self.__risk, 384) + if not os.path.exists(self.__path): + os.makedirs(self.__path, 384) + if not os.path.exists(self.__report): + os.makedirs(self.__report, 384) + + if not os.path.exists(self._vuln_ignore): + result = [] + public.WriteFile(self._vuln_ignore, json.dumps(result)) + if not os.path.exists(self._vuln_result): + result = [] + public.WriteFile(self._vuln_result, json.dumps(result)) + self.new_system_result = [] + # self.sys_version = self.get_sys_version() + # self.sys_product = self.new_get_sys_product() + + def _get_list(self): + # 最终输出结果 + self.data = { + 'security': [], + 'risk': [], + 'ignore': [], + "is_autofix": [], + } + # 获取支持一键修复的列表 + try: + is_autofix = public.read_config("safe_autofix") + except: + is_autofix = [] + # 临时扫描结果,中断的时候返回 + self.tmp_data = { + 'security': [], + 'risk': [], + 'ignore': [], + 'is_autofix': is_autofix, + 'check_time': datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") + } + context = {"status": "Ready to repair", "percentage": 0, "count": 0, "score": 100} + public.WriteFile(self.__path + '/bar.txt', json.dumps(context)) # 扫描进度条归零 + bar_num = 0 # 进度条初始化 + bar_limit = 0 # 进度条限制 + self.discov_count = 0 # 扫描中发现漏洞数量初始化 + self.score = 100 # 扫描中分数变化 + sys_version = self.get_sys_version() # 获取系统版本 + # self.compare_md5() # 比较漏洞库版本 + # 下载新版漏洞库文件 + if not self.download_new_vulns(): + sys_version = None + # centos_7走新版漏洞扫描 + if sys_version == 'centos_7' or sys_version == 'centos_8' or sys_version == 'centos_8_stream': + self.new_system_scan() + # ubuntu走新版2接口 + elif sys_version == 'ubuntu_20.04' or sys_version == 'ubuntu_22.04' or sys_version == 'ubuntu_18.04' or sys_version == 'debian_12' or sys_version == 'debian_11' or sys_version == 'debian_10': + self.new_system_scan2() + # 旧版本漏洞检测 + # else: + # self.system_scan() # 旧版本系统漏洞扫描 + + # 加载安全风险模块 + p = public.get_modules('class_v2/safe_warning_v2') + for m_name in p.__dict__.keys(): + ignore_file = self.__ignore + '/' + m_name + '.pl' + # 忽略的检查项 + if p[m_name]._level == 0: continue + + m_info = { + 'title': p[m_name]._title, + 'm_name': m_name, + 'ps': p[m_name]._ps, + 'version': p[m_name]._version, + 'level': p[m_name]._level, + 'ignore': p[m_name]._ignore, + 'date': p[m_name]._date, + 'tips': p[m_name]._tips, + 'help': p[m_name]._help + } + try: + m_info['remind'] = p[m_name]._remind + except: + pass + result_file = self.__result + '/' + m_name + '.pl' + + try: + s_time = time.time() + m_info['status'], m_info['msg'] = p[m_name].check_run() + m_info['taking'] = round(time.time() - s_time, 6) + m_info['check_time'] = int(time.time()) + public.writeFile(result_file, json.dumps( + [m_info['status'], m_info['msg'], m_info['check_time'], m_info['taking']], )) + except: + continue + + m_info['ignore'] = os.path.exists(ignore_file) + if m_info['ignore']: + self.data['ignore'].append(m_info) + self.tmp_data['ignore'].append(m_info) # 临时扫描结果 + else: + if m_info['status']: + self.data['security'].append(m_info) + self.tmp_data['security'].append(m_info) # 临时扫描结果 + else: + risk_file = self.__risk + '/' + m_name + '.pl' + public.writeFile(risk_file, json.dumps(m_info)) + self.data['risk'].append(m_info) + self.tmp_data['risk'].append(m_info) # 临时扫描结果 + self.discov_count += 1 # 扫描中发现风险数 + self.score -= m_info['level'] + if self.score < 0: + self.score = 0 + + bar = ("%.2f" % (float(bar_num) / float(len(p.__dict__.keys())) * 50 + 50)) + # 通过进度条限制,防止写文件频繁占用高 + if int(float(bar)) >= bar_limit: + context = {"status": "{}".format(m_info['title']), "percentage": int(float(bar)), "count": self.discov_count, "score": self.score} + public.WriteFile(self.__path + '/bar.txt', json.dumps(context)) + self.dump_tmp_result() # 发现漏洞,先保存一份临时的 + bar_limit += 10 + bar_num += 1 + + # 新版漏洞检测无需读文件 + # is_autofix被包含进tmp_data{}字典里,会动态增加 + self.data['is_autofix'] = is_autofix + # self.data['is_autofix'] += is_autofix + + # if sys_version == 'centos_7' or sys_version == 'ubuntu_20.04' or sys_version == 'ubuntu_22.04' or sys_version == 'ubuntu_18.04' or sys_version == 'debian_12' or sys_version == 'debian_11' or sys_version == 'debian_10': + # self.data['is_autofix'] += is_autofix + # 旧版本漏洞检测 + # else: + # vuln_result = self.get_vuln_result() + # self.data['risk'] = self.data['risk'] + vuln_result['risk'] + # self.data['ignore'] = self.data['ignore'] + vuln_result['ignore'] + # vuln_is_autofix = [] + # for vr in vuln_result['risk']: + # if not vr["reboot"]: + # vuln_is_autofix.append(vr["cve_id"]) + # self.data['is_autofix'] = is_autofix + vuln_is_autofix + + score = 100 + for d in self.data['risk']: + score = score - d['level'] + if score < 0: + self.data['score'] = 0 + else: + self.data['score'] = score + self.data['risk'] = sorted(self.data['risk'], key=lambda x: x['level'], reverse=True) + self.data['security'] = sorted(self.data['security'], key=lambda x: x['level'], reverse=True) + self.data['ignore'] = sorted(self.data['ignore'], key=lambda x: x['level'], reverse=True) + self.data['check_time'] = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") + # 将结果输出一份到报告目录下 + with open("/www/server/panel/data/warning_report/data.json", "w") as f: + json.dump(self.data, f) + self.record_times() + context = {"status": "Detection completed", "percentage": 100, "count": self.discov_count, "score": self.score} + public.WriteFile(self.__path + '/bar.txt', json.dumps(context)) + # public.WriteFile(self.__path + '/bar.txt', "100") # 扫描进度条归零 + return self.data + + def download_new_vulns(self): + ''' + 根据系统版本确定漏洞库名 + ''' + sys_version = self.get_sys_version() + zip_file = "" + if sys_version == "centos_7": + self.new_vul_list = self.__path + '/vul_centos7.json' + zip_file = "vul_centos7.zip" + elif sys_version == "centos_8": + self.new_vul_list = self.__path + '/vul_centos8.json' + zip_file = "vul_centos8.zip" + elif sys_version == "centos_8_stream": + self.new_vul_list = self.__path + '/vul_centos8stream.json' + zip_file = "vul_centos8stream.zip" + elif sys_version == "ubuntu_20.04": + self.new_vul_list = self.__path + '/vul_ubuntu2004.json' + zip_file = "vul_ubuntu2004.zip" + elif sys_version == "ubuntu_22.04": + self.new_vul_list = self.__path + '/vul_ubuntu2204.json' + zip_file = "vul_ubuntu2204.zip" + elif sys_version == "ubuntu_18.04": + self.new_vul_list = self.__path + '/vul_ubuntu1804.json' + zip_file = "vul_ubuntu1804.zip" + elif sys_version == "debian_12": + self.new_vul_list = self.__path + '/vul_debian12.json' + zip_file = "vul_debian12.zip" + elif sys_version == "debian_11": + self.new_vul_list = self.__path + '/vul_debian11.json' + zip_file = "vul_debian11.zip" + elif sys_version == "debian_10": + self.new_vul_list = self.__path + '/vul_debian10.json' + zip_file = "vul_debian10.zip" + # 检查漏洞库文件是否存在 + if not os.path.exists(self.new_vul_list): + if zip_file != '': + downfile = self.__path+'/'+zip_file + public.downloadFile("/safe_warning/{}".format(public.get_url(),zip_file), downfile) + o, e = public.ExecShell("unzip -o {} -d {}".format(downfile, self.__path)) + # 解压报错 + if e != "": + return False + else: + return False + return True + # cve_id + def get_list(self, args): + ''' + @name 开始扫描并返回结果 + @param args: + @return: + ''' + if 'force' in args: + # 校验参数 + try: + args.validate([ + Param('force').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + import subprocess + public.set_module_logs("panel_warning_v2", "get_list") + public.WriteFile(self.__path + '/kill.pl', "False") # 用来判断这次的扫描是否被中断,默认没中断 + command = "btpython /www/server/panel/class_v2/panel_warning_v2.py" + process = subprocess.Popen(command, shell=True) + # 获取进程ID + pid = process.pid + public.WriteFile(self.__path + "/pid.txt", str(pid)) + process.wait() + result_file = self.__path + '/resultresult.json' + output = { + "score": 100, + "check_time": public.format_date(), + "interrupt": False, + "security": [], + "risk": [], + "ignore": [], + "is_autofix": [] + } + if not os.path.exists(result_file): + return output + result_body = public.ReadFile(result_file) + if not result_body: + return output + try: + output = json.loads(result_body) + except: + return output + # 给前端判断这次的扫描结果是否中断 + if public.ReadFile(self.__path + '/kill.pl') == "True": + output["interrupt"] = True + else: + output["interrupt"] = False + return public.return_message(0,0,output) + + def sync_rule(self): + ''' + @name 从云端同步规则 + @author hwliang<2020-08-05> + @return void + ''' + # try: + # dep_path = '/www/server/panel/class/safe_warning' + # local_version_file = self.__path + '/version.pl' + # last_sync_time = local_version_file = self.__path + '/last_sync.pl' + # if os.path.exists(dep_path): + # if os.path.exists(last_sync_time): + # if int(public.readFile(last_sync_time)) > time.time(): + # return + # else: + # if os.path.exists(local_version_file): os.remove(local_version_file) + + # download_url = public.get_url() + # version_url = download_url + '/install/warning/version.txt' + # cloud_version = public.httpGet(version_url) + # if cloud_version: cloud_version = cloud_version.strip() + + # local_version = public.readFile(local_version_file) + # if local_version: + # if cloud_version == local_version: + # return + + # tmp_file = '/tmp/bt_safe_warning.zip' + # public.ExecShell('wget -O {} {} -T 5'.format(tmp_file,download_url + '/install/warning/safe_warning.zip')) + # if not os.path.exists(tmp_file): + # return + + # if os.path.getsize(tmp_file) < 2129: + # os.remove(tmp_file) + # return + + # if not os.path.exists(dep_path): + # os.makedirs(dep_path,384) + # public.ExecShell("unzip -o {} -d {}/ >/dev/null".format(tmp_file,dep_path)) + # public.writeFile(local_version_file,cloud_version) + # public.writeFile(last_sync_time,str(int(time.time() + 7200))) + # if os.path.exists(tmp_file): os.remove(tmp_file) + # public.ExecShell("chmod -R 600 {}".format(dep_path)) + # except: + # pass + + def set_ignore(self, args): + ''' + @name 设置指定项忽略状态 + @author hwliang<2020-08-04> + @param dict_obj { + m_name 模块名称 + } + @return dict + ''' + m_name = args.m_name.strip() + ignore_file = self.__ignore + '/' + m_name + '.pl' + if os.path.exists(ignore_file): + os.remove(ignore_file) + else: + public.writeFile(ignore_file, '1') + return public.return_message(0,0, 'successfully set!') + + def check_find(self, args): + ''' + @name 检测指定项 + @author hwliang<2020-08-04> + @param dict_obj { + m_name 模块名称 + } + @return dict + ''' + # 校验参数 + try: + args.validate([ + Param('m_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + try: + m_name = args.m_name.strip() + p = public.get_modules('class_v2/safe_warning_v2') + m_info = { + 'title': p[m_name]._title, + 'm_name': m_name, + 'ps': p[m_name]._ps, + 'version': p[m_name]._version, + 'level': p[m_name]._level, + 'ignore': p[m_name]._ignore, + 'date': p[m_name]._date, + 'tips': p[m_name]._tips, + 'help': p[m_name]._help + } + + # 解决已经在忽略列表中,但是如果仍然需要检查的话可以检查 + ignore_file = self.__ignore + '/' + m_name + '.pl' + if os.path.exists(ignore_file): + from cachelib import SimpleCache + cache = SimpleCache(5000) + ikey = 'warning_list' + cache.delete(ikey) + os.remove(ignore_file) + + result_file = self.__result + '/' + m_name + '.pl' + s_time = time.time() + m_info['status'], m_info['msg'] = p[m_name].check_run() + m_info['taking'] = round(time.time() - s_time, 4) + m_info['check_time'] = int(time.time()) + public.writeFile(result_file, json.dumps( + [m_info['status'], m_info['msg'], m_info['check_time'], m_info['taking']])) + return public.return_message(0,0, 'It has been retested.') + except: + return public.return_message(-1,0, 'Detection failed') + + + def system_scan(self): + ''' + 一键扫描系统 + :param get: + :return: dict + ''' + self.compare_md5() + sys_version = self.get_sys_version() + + # if sys_version == 'None': + # return public.returnMsg(False, '当前系统暂不支持') + sys_product = self.get_sys_product() + # if not os.path.exists(self.__vul_list): + # return public.returnMsg(False, "扫描失败") + vul_list = self.get_vul_list() + + new_risk_list = [] + new_ignore_list = [] + # error_list = [] + result_dict = {} + # cp_list = [] + vul_count = 0 + ignore_list = self.get_ignore_list() + reboot_count = 0 + self.vul_num = len(vul_list) + bar_num = 0 # 进度条初始化 + bar_limit = 0 # 进度条限制 + for vul in vul_list: + bar = ("%.2f" % (float(bar_num)/float(self.vul_num)*50)) + # 限制进度条,限制写文件频率 + if int(float(bar)) >= bar_limit: + context = {"status": "{}".format(vul['cve_id']), "percentage": int(float(bar)), "count": self.discov_count, "score": self.score} + public.WriteFile(self.__path+'/bar.txt', json.dumps(context)) + self.dump_tmp_result() # 发现漏洞,先保存一份临时的 + bar_limit += 10 + bar_num += 1 + vul_count += 1 + for v in vul["affected_list"]: + if v["manufacturer"] == sys_version: + tmp = 1 # 默认命中 + if not 'affected' in v: continue + arr = v['affected'].split("Up to (excluding)\n ") + if len(arr) < 2: continue + vul_version = arr[1] + try: + for soft in v["softname"]: + compare_result = self.version_compare(sys_product[soft], vul_version) + if compare_result >= 0: + tmp = 0 # 当有一个软件包版本不在漏洞范围内,则不命中 + break + if tmp == 1: + # softname_list = [soft+'-'+sys_product[soft] for soft in v["softname"]] + # softname_list = [{soft: sys_product[soft]} for soft in v["softname"]] + self.discov_count += 1 # 扫描中发现漏洞数 + softname_dict = {} + for soft in v["softname"]: + softname_dict[soft] = sys_product[soft] + level = self.get_score_risk(vul["score"]) + vul_dict = {key: vul[key] for key in ["cve_id", "vuln_name", "vuln_time", "vuln_solution"]} + vul_dict["level"] = level + self.score -= level # 扫描中的动态分数 + if self.score < 0: + self.score = 0 + vul_dict["soft_name"] = softname_dict + vul_dict["vuln_version"] = vul_version + vul_dict["check_time"] = int(time.time()) + vul_dict["reboot"] = "" + if "kernel" in [k for k in v["softname"]]: + vul_dict["reboot"] = "This vulnerability is a kernel vulnerability, and you need to upgrade the kernel version yourself. It is recommended to make snapshots and backups before upgrading" + reboot_count += 1 + if vul["cve_id"] in ignore_list: + new_ignore_list.append(vul_dict) + self.tmp_data['ignore'].append(vul_dict) # 添加到临时字典 + break + new_risk_list.append(vul_dict) + self.tmp_data['risk'].append(vul_dict) # 添加到临时字典 + if vul_dict["reboot"]: + self.tmp_data['is_autofix'].append(vul_dict["cve_id"]) + break + # cp_list.append(vul["cve_id"]+': '+str([soft+'-'+sys_product[soft] for soft in v["softname"]])+' >= '+vul_version) + except Exception as e: + # error_list.append(vul["cve_id"]+': '+str(e)) + break + result_dict["vul_count"] = vul_count + result_dict["risk"] = new_risk_list + result_dict["ignore"] = new_ignore_list + # result_dict["reboot"] = self.__need_reboot + # result_dict["error"] = error_list + # result_dict["compare"] = cp_list + public.WriteFile(self.__path + '/system_scan_time', str(int(time.time()))) + public.WriteFile(self._vuln_result, json.dumps(result_dict)) + # try: + # public.WriteFile(self._vuln_result, json.dumps(result_dict)) + # return public.returnMsg(True, "扫描完成") + # except: + # return public.returnMsg(False, "扫描失败") + + # 版本比较 + def version_compare(self, ver_a, ver_b): + ''' + 比较版本大小 + :param ver_a: 软件版本 + :param ver_b: 漏洞版本 + :return: int 大于等于返回1或0,小于返回-1 + ''' + sys_version = self.get_sys_version() + if "ubuntu" in sys_version or "debian" in sys_version: + if ver_b.startswith("1:"): + ver_b = ver_b[2:] + # if ver_a.startswith("1:"): + # ver_a = ver_a[2:] + result = public.ExecShell("dpkg --compare-versions " + ver_a + " ge " + ver_b + " && echo true") + if 'warning' in result[1].strip(): return None + if 'true' in result[0].strip(): + return 1 + else: + return -1 + return self.vercmp(ver_a, ver_b) + + def vercmp(self, first, second): + import re + R_NONALNUMTILDE = re.compile(br"^([^a-zA-Z0-9~]*)(.*)$") + R_NUM = re.compile(br"^([\d]+)(.*)$") + R_ALPHA = re.compile(br"^([a-zA-Z]+)(.*)$") + first = first.encode("ascii", "ignore") + second = second.encode("ascii", "ignore") + while first or second: + m1 = R_NONALNUMTILDE.match(first) + m2 = R_NONALNUMTILDE.match(second) + m1_head, first = m1.group(1), m1.group(2) + m2_head, second = m2.group(1), m2.group(2) + if m1_head or m2_head: + continue + + if first.startswith(b'~'): + if not second.startswith(b'~'): + return -1 + first, second = first[1:], second[1:] + continue + if second.startswith(b'~'): + return 1 + + if not first or not second: + break + + m1 = R_NUM.match(first) + if m1: + m2 = R_NUM.match(second) + if not m2: + return 1 + isnum = True + else: + m1 = R_ALPHA.match(first) + m2 = R_ALPHA.match(second) + isnum = False + + if not m1: + return -1 + if not m2: + return 1 if isnum else -1 + + m1_head, first = m1.group(1), m1.group(2) + m2_head, second = m2.group(1), m2.group(2) + + if isnum: + m1_head = m1_head.lstrip(b'0') + m2_head = m2_head.lstrip(b'0') + + m1hlen = len(m1_head) + m2hlen = len(m2_head) + if m1hlen < m2hlen: + return -1 + if m1hlen > m2hlen: + return 1 + if m1_head < m2_head: + return -1 + if m1_head > m2_head: + return 1 + continue + + m1len = len(first) + m2len = len(second) + if m1len == m2len == 0: + return 0 + if m1len != 0: + return 1 + return -1 + + # 取系统版本 + def get_sys_version(self): + ''' + 获取当前系统版本 + :return: string + ''' + sys_version = "None" + if os.path.exists("/etc/redhat-release"): + result = public.ReadFile("/etc/redhat-release") + if "CentOS Linux release 7" in result: + sys_version = "centos_7" + elif "CentOS Linux release 8" in result: + sys_version = "centos_8" + elif "CentOS Stream release 8" in result: + sys_version = "centos_8_stream" + elif os.path.exists("/etc/lsb-release"): + if "Ubuntu 20.04" in public.ReadFile("/etc/lsb-release"): + sys_version = "ubuntu_20.04" + elif "Ubuntu 22.04" in public.ReadFile("/etc/lsb-release"): + sys_version = "ubuntu_22.04" + elif "Ubuntu 18.04" in public.ReadFile("/etc/lsb-release"): + sys_version = "ubuntu_18.04" + elif os.path.exists("/etc/debian_version"): + result = public.ReadFile("/etc/debian_version") + if "10." in result: + sys_version = "debian_10" + elif "11." in result: + sys_version = "debian_11" + elif "12." in result: + sys_version = "debian_12" + return sys_version + + def new_get_sys_product(self, flag=False): + ''' + 新版获取系统软件包及版本{"name":"version"} + @param flag bool 为True时直接扫一次 + ''' + # 修复完成需要重新获取一次软件包版本 + if flag: + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) + + sys_version = self.get_sys_version() + if sys_version == "centos_7": + # 根据yum日志判断是否用软件包更新 + yumlog = "/var/log/yum.log" + elif sys_version == "centos_8" or sys_version == "centos_8_stream": + yumlog = "/var/log/dnf.log" + else: + yumlog = "/var/log/apt/history.log" + try: + # 先判断有没有yum.log + if os.path.exists(yumlog): + # 第一次将yum.log修改时间记录在文件里 + if not os.path.exists(self.yum_time): + new_modi_time = str(int(os.path.getmtime(yumlog))) + public.WriteFile(self.yum_time, new_modi_time) + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) + else: + old_modi_time = public.ReadFile(self.yum_time) + new_modi_time = str(int(os.path.getmtime(yumlog))) + # 比较上一次记录的时间和这次获取的修改时间,相等则根据rpm文件的修改时间 + if old_modi_time == new_modi_time: + if os.path.exists(self.product_version): + # 若rpm文件修改日期大于七天,则还是执行rpm检测 + if self.is_file_too_old(self.product_version, 7): + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) + else: + sys_product = json.loads(public.ReadFile(self.product_version)) + else: + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) + # 不相等证明最近有用yum安装过新软件,更新文件并检测 + else: + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) # 将软件包版本写入文件 + public.WriteFile(self.yum_time, new_modi_time) + # 没有yum.log,则直接根据rpm文件修改日期 + else: + if os.path.exists(self.product_version): + if self.is_file_too_old(self.product_version, 7): + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) + else: + sys_product = json.loads(public.ReadFile(self.product_version)) + else: + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) + except Exception as e: + sys_product = self.get_sys_product() + public.WriteFile(self.product_version, json.dumps(sys_product)) + # 其他系统版本 + return sys_product + + # 取软件包版本 + def get_sys_product(self): + """ + 获取系统软件包及版本 + {"name":"version"} + :return dict 如果系统不支持则返回str None + """ + product_version = {} + sys_version = self.get_sys_version() + + # if sys_version == 'None':return public.returnMsg(False,'当前系统暂不支持') + if "centos" in sys_version: + result = public.ExecShell('rpm -qa --qf \'%{NAME};%{VERSION}-%{RELEASE}\\n\'')[0].strip().split('\n') + elif "ubuntu" in sys_version: + # result1 = subprocess.check_output(['dpkg-query', '-W', '-f=${Package};${Version}\n']).decode('utf-8').strip().split('\n') + result = public.ExecShell('dpkg-query -W -f=\'${Package};${Version}\n\'')[0].strip().split('\n') + elif "debian" in sys_version: + result = public.ExecShell('dpkg-query -W -f=\'${Package};${Version}\n\'')[0].strip().split('\n') + elif sys_version == "None": + return None + else: + return None + for pkg in result: + try: + product_version[pkg.split(";")[0]] = pkg.split(";")[1] + except: + return None + # product_version["kernel"] = subprocess.check_output(['uname', '-r']).decode('utf-8').strip().replace(".x86_64", "") + product_version["kernel"] = public.ExecShell('uname -r')[0].strip() + return product_version + + def get_vuln_result(self): + ''' + 获取上一次扫描结果 + :param get: + :return: dict + ''' + d_risk = 0 + h_risk = 0 + m_risk = 0 + vul_list = [] + if not os.path.exists(self.__vul_list): + self.vul_num = 0 + else: + self.vul_num = len(self.get_vul_list()) + if not os.path.exists(self._vuln_result): + tmp_dict = {"vul_count": self.vul_num, "risk": [], "ignore": [], + "count": {"serious": 0, "high_risk": 0, "moderate_risk": 0}, "msg": "", + "repair_count": {"all_count": 0, "today_count": 0}, "all_check_time": "", "ignore_count": 0} + if os.path.exists("/etc/redhat-release"): + result = public.ReadFile("/etc/redhat-release") + if "CentOS Linux release 8" in result: + # tmp_dict["msg"] = "当前系统【centos_8】官方已停止维护,为了安全起见,建议升级至centos 8 stream\n详情参考教程:https://www.bt.cn/bbs/thread-82931-1-1.html" + tmp_dict["msg"] = "The current system [centos_8] has been officially stopped maintenance, for security purposes, it is recommended to upgrade to centos 8 stream" + return tmp_dict + if public.ReadFile(self._vuln_result) == '[]': + tmp_dict = {"vul_count": self.vul_num, "risk": [], "ignore": [], + "count": {"serious": 0, "high_risk": 0, "moderate_risk": 0}, "msg": "", + "repair_count": {"all_count": 0, "today_count": 0}, "all_check_time": "", "ignore_count": 0} + if os.path.exists("/etc/redhat-release"): + result = public.ReadFile("/etc/redhat-release") + if "CentOS Linux release 8" in result: + # tmp_dict["msg"] = "当前系统【centos_8】官方已停止维护,为了安全起见,建议升级至centos 8 stream\n详情参考教程:https://www.bt.cn/bbs/thread-82931-1-1.html" + tmp_dict["msg"] = "The current system [centos_8] has been officially stopped maintenance, for security purposes, it is recommended to upgrade to centos 8 stream" + return tmp_dict + result_dict = json.loads(public.ReadFile(self._vuln_result)) + old_risk_list = result_dict["risk"] + old_ignore_list = result_dict["ignore"] + new_risk_list = old_risk_list.copy() + new_ignore_list = old_ignore_list.copy() + tmp_ignore_list = self.get_ignore_list() + for cve in old_risk_list: + if cve["cve_id"] in tmp_ignore_list: + new_ignore_list.append(cve) + new_risk_list.remove(cve) + for cve_ig in old_ignore_list: + if cve_ig["cve_id"] not in tmp_ignore_list: + new_risk_list.append(cve_ig) + new_ignore_list.remove(cve_ig) + for vul in new_ignore_list + new_risk_list: + vul_list.append(vul["cve_id"]) + if vul["cve_id"] in tmp_ignore_list: + continue + if vul["level"] == 3: + d_risk += 1 + elif vul["level"] == 2: + h_risk += 1 + elif vul["level"] == 1: + m_risk += 1 + list_sort = [3, 2, 1] # 排序列表 + # result_dict["risk"] = old_risk_list + result_dict["risk"] = sorted(new_risk_list, key=lambda x: list_sort.index(x.get("level"))) + # result_dict["ignore"] = old_ignore_list + result_dict["ignore"] = sorted(new_ignore_list, key=lambda x: list_sort.index(x.get("level"))) + # result_dict["reboot"] = self.__need_reboot + result_dict["count"] = {"serious": d_risk, "high_risk": h_risk, "moderate_risk": m_risk} + result_dict["msg"] = "" + result_dict["repair_count"] = self.count_repair(vul_list) + result_dict["all_check_time"] = public.ReadFile(self.__path + '/system_scan_time') + result_dict["ignore_count"] = len(tmp_ignore_list) + if os.path.exists("/etc/redhat-release"): + result = public.ReadFile("/etc/redhat-release") + if "CentOS Linux release 8" in result: + result_dict[ + "msg"] = "The current system [centos_8] has been officially stopped maintenance, for security purposes, it is recommended to upgrade to centos 8 stream" + public.WriteFile(self._vuln_result, json.dumps(result_dict)) + return result_dict + + # 按分数评等级 + def get_score_risk(self, score): + ''' + 拿到分数,返回危险等级 + :param score: + :return: int 若没有符合的分数就报错,需要捕获异常 + ''' + if float(score) >= 9.0: + risk = 3 + elif float(score) >= 7.0: + risk = 2 + elif float(score) >= 6.0: + risk = 1 + return risk + + def get_vul_list(self): + return json.loads(public.ReadFile(self.__vul_list)) + + def get_ignore_list(self): + return json.loads(public.ReadFile(self._vuln_ignore)) + + def set_vuln_ignore(self, args): + ''' + 设置忽略指定cve,若已在列表里,则删除,不在列表里则添加 + :param args: + :return: dict {status:true,msg:'设置成功/失败'} + ''' + cve_list = json.loads(args.cve_list.strip()) + ignore_list = self.get_ignore_list() + for cl in cve_list: + if cl in ignore_list: + ignore_list.remove(cl) + else: + ignore_list.append(cl) + + public.WriteFile(self._vuln_ignore, json.dumps(ignore_list)) + # public.WriteFile(self.__result, json.dumps(result_dict)) + return public.return_message(0,0, 'successfully set!') + # except: + # return public.returnMsg(False, '{}设置失败!'.format(cve_list)) + + def count_repair(self, now_list): + ''' + 获取总共修复漏洞的数量以及今日修复漏洞数量 + :param now_list: + :return: dict + ''' + cve_dict = {} + if not os.path.exists(self.__repair_count): + cve_dict["all_cve"] = now_list + cve_dict["today_cve"] = now_list + cve_dict["time"] = int(time.time()) + public.WriteFile(self.__repair_count, json.dumps(cve_dict)) + cve_dict = json.loads(public.ReadFile(self.__repair_count)) + cve_dict["all_cve"].extend(set(now_list) - set(cve_dict["all_cve"])) + all_count = len(cve_dict["all_cve"]) - len(now_list) + cve_dict["today_cve"].extend(set(now_list) - set(cve_dict["today_cve"])) + today_count = len(cve_dict["today_cve"]) - len(now_list) + # if cve_dict["time"].split(" ")[0] != self.get_time().split(" ")[0]: + # cve_dict["today_cve"] = now_list + cve_dict["time"] = int(time.time()) + public.WriteFile(self.__repair_count, json.dumps(cve_dict)) + return {"all_count": all_count, "today_count": today_count} + + def get_time(self): + return public.format_date() + + def check_cve(self, args): + ''' + 检测单个漏洞 + :param args: + :return: dict + ''' + sys_product = self.get_sys_product() + if not sys_product: + return public.returnMsg(True, 'Detection failed') + cve_id = args.cve_id.strip() + result_dict = json.loads(public.ReadFile(self._vuln_result)) + risk_list = result_dict["risk"] + ignore_list = result_dict["ignore"] + tmptmp = 1 + for cve in risk_list: + if cve["cve_id"] == cve_id: + tmp = 1 # 默认命中漏洞 + cve["check_time"] = int(time.time()) + for soft in list(cve["soft_name"].keys()): + if self.version_compare(sys_product[soft], cve["vuln_version"]) >= 0: + tmp = 0 # 当有一个软件包不命中,则为已修复 + tmptmp = 0 + break + if tmp == 0: + risk_list.remove(cve) + for cve in ignore_list: + if cve["cve_id"] == cve_id: + tmp = 1 # 默认命中漏洞 + cve["check_time"] = int(time.time()) + for soft in list(cve["soft_name"].keys()): + if self.version_compare(sys_product[soft], cve["vuln_version"]) >= 0: + tmp = 0 # 当有一个软件包不命中,则为已修复 + tmptmp = 0 + break + if tmp == 0: + ignore_list.remove(cve) + result_dict["risk"] = risk_list + result_dict["ignore"] = ignore_list + public.WriteFile(self._vuln_result, json.dumps(result_dict)) + if tmptmp == 0: + return public.returnMsg(True, 'It has been retested.') + else: + return public.returnMsg(True, 'It has been retested.') + + def compare_md5(self): + ''' + 对比md5,更新漏洞库 + :return: + ''' + import requests + # try: + # new_md5 = requests.get("https://www.bt.cn/vulscan_d11ad1fe99a5f078548b0ea355db42dc.txt").text + # except: + # return 0 + # old_md5 = public.FileMd5(self.__vul_list) + # if old_md5 != new_md5 or not os.path.exists(self.__vul_list): + if not os.path.exists(self.__vul_list): + try: + public.downloadFile("{}/install/src/high_risk_vul.zip".format(public.get_url()), + self.__path + "/high_risk_vul.zip") + public.ExecShell("unzip -o {}/high_risk_vul.zip -d {}/".format(self.__path, self.__path)) + except: + return 0 + return 1 + + def get_logs(self, get): + ''' + 获取升级日志 + :param get: + :return: dict + ''' + import files + return public.returnMsg(True, files.files().GetLastLine(self.__path + '/log.txt', 20)) + + def record_times(self): + ''' + 记录近七日扫描次数 + ''' + date_obj = datetime.datetime.now() + weekday = datetime.datetime.now().weekday() + if not os.path.exists("/www/server/panel/data/warning_report/record.json"): + tmp = {"scan": [], "repair": []} + for i in range(6, -1, -1): + last_date = (date_obj - datetime.timedelta(days=i)).strftime("%Y/%m/%d") + tmp["scan"].append({"date": last_date, "times": 0}) + tmp["repair"].append({"date": last_date, "times": 0}) + public.WriteFile("/www/server/panel/data/warning_report/record.json", json.dumps(tmp)) + with open("/www/server/panel/data/warning_report/record.json", "r") as f: + record = json.load(f) + if record["scan"][weekday]["date"] == datetime.datetime.now().strftime("%Y/%m/%d"): + record["scan"][weekday]["times"] += 1 + else: + record["scan"][weekday]["date"] = datetime.datetime.now().strftime("%Y/%m/%d") + record["scan"][weekday]["times"] = 1 + public.WriteFile("/www/server/panel/data/warning_report/record.json", json.dumps(record)) + + def get_scan_bar(self, args): + ''' + 获取扫描进度条 + @param args: + @return: int + ''' + if not os.path.exists(self.__path + '/bar.txt'):return 0 + data = json.loads(public.ReadFile(self.__path + '/bar.txt')) + return public.return_message(0,0,data) + + def kill_get_list(self, args): + ''' + 杀掉扫描进程 + @param args: + @return: + ''' + if not os.path.exists(self.__path + '/pid.txt'): + return {"status": False, "msg": "Interrupt failure"} + pid = public.ReadFile(self.__path + '/pid.txt') + err = public.ExecShell("kill -9 {}".format(str(pid)))[1].strip() + if err: + return {"status": False, "msg": "Interrupt failure"} + else: + public.WriteFile(self.__path + '/kill.pl', "True") + return {"status": True, "msg": "Interrupt successfully"} + + def dump_tmp_result(self): + ''' + 动态保存结果 + @param args: + @return: + ''' + public.WriteFile(self.__path + '/tmp_result.json', json.dumps(self.tmp_data)) + + def get_tmp_result(self, args): + ''' + 获取中途中断结果 + @param args: + @return: + ''' + if not os.path.exists(self.__path + '/tmp_result.json'): + return "err" + return json.loads(public.ReadFile(self.__path + '/tmp_result.json')) + + def new_system_scan2(self): + ''' + 新版系统dpkg软件包漏洞检测 + ''' + if not os.path.exists(self.new_vul_list): + return + # 加载漏洞库文件 + try: + vul_json = json.loads(public.ReadFile(self.new_vul_list)) + packages_rule = vul_json['Packages'] + detail = vul_json['Detail'] + except Exception as e: + return + sys_product = self.new_get_sys_product() + if sys_product is None: + return + + dpkg = Dpkg # 获取DPKG对象 + # 符合 + systemscan_result = {} + # 开始检测 + # 第一层遍历系统软件包 + for pk, ver in sys_product.items(): + # 跳过内核漏洞检测 + # if pk.startswith("kernel"): + # continue + # 是否有历史漏洞 + if pk in packages_rule: + # 第二层遍历比较软件包涉及的漏洞版本 + for rule in packages_rule[pk]: + # 判断软件包版本是否存在主版本号,有则漏洞版本一起保留主版本号,否则去掉漏洞版本的主版本号 + pk_ver, vul_ver = self.adjust_ver(ver, rule[0]) + try: + cp_result = dpkg.compare_versions(pk_ver, vul_ver) + except: + continue + # 任意一个命中 + if cp_result == -1: + if rule[1] not in systemscan_result: + systemscan_result[rule[1]] = detail[str(rule[1])] + systemscan_result[rule[1]]["impact"] = [{"package": pk, "version": pk_ver, "vul_ver": vul_ver}] + else: + systemscan_result[rule[1]]["impact"].append({"package": pk, "version": pk_ver, "vul_ver": vul_ver}) + + # 为了兼容旧版本再次做处理 + for sr in systemscan_result.values(): + one_risk = {} + one_risk["title"] = "【{}】Linux系统安全漏洞编号".format(sr["ref_id"]) + one_risk["data"] = "2023-12-08" + one_risk["help"] = "" + one_risk["ignore"] = False + level = self.new_severity_to_num(sr["severity"]) + one_risk["level"] = level + one_risk["m_name"] = sr["ref_id"] + pk_list = [] + # 判断是否有内核漏洞在里面 + is_kernel = False + soft_list = [] + for impact in sr["impact"]: + if impact["package"].startswith("kernel"): + is_kernel = True + continue + soft_list.append("{} Versions below {}".format(impact["package"]+"-"+impact["version"], impact["vul_ver"])) + pk_list.append(impact["package"]) + if is_kernel: + continue + one_risk["msg"] = "Security vulnerabilities are found in the following system software:
                                    {}
                                    Vulnerabilities involved:{}
                                    Please refer to the official announcement for details:{}".format('
                                    '.join(soft_list), '、'.join(sr["cve"]),sr["ref_url"]) + one_risk["ps"] = "【{}】Linux system vulnerability security vulnerability number".format(sr["ref_id"]) + one_risk["remind"] = "Fixing vulnerabilities has certain risks, so it is recommended to take a good system snapshot to prevent system operation from being affected." + one_risk["status"] = False + one_risk["taking"] = 0.000001 + one_risk["tips"] = ["Update the software to a safe version according to the risk description", "Or click [One-click Repair] to solve all security issues"] + one_risk["version"] = 1 + one_risk["type"] = "vulnerability" + one_risk["package"] = pk_list + + # 存储结果 + self.data["risk"].append(one_risk) + self.data["is_autofix"].append(one_risk["m_name"]) + + # 扫描中发现的漏洞数 + self.discov_count += 1 + # 扫描中的动态分数 + self.score -= level + if self.score < 0: + self.score = 0 + # 扫描中的动态风险 + self.tmp_data['risk'].append(one_risk) + # 可修复项 + self.tmp_data['is_autofix'].append(one_risk["m_name"]) + + def new_system_scan(self): + ''' + 新版系统rpm软件包漏洞检测 + 提升扫描速度 + :param get: + :return: dict + ''' + + # 判断新漏洞库存不存在 + if not os.path.exists(self.new_vul_list): + return + + context = {"status": "Checking system software", "percentage": 0, "count": 0, "score": 100} + public.WriteFile(self.__path + '/bar.txt', json.dumps(context)) + sys_product = self.new_get_sys_product() + # 加载漏洞库文件 + try: + vul_json = json.loads(public.ReadFile(self.new_vul_list)) + packages_rule = vul_json['Packages'] + detail = vul_json['Detail'] + except Exception as e: + return + if sys_product is None: + return + + # 符合 + systemscan_result = {} + # 开始检测 + # 第一层遍历系统软件包 + for pk, ver in sys_product.items(): + # 跳过内核漏洞检测 + # if pk.startswith("kernel"): + # continue + # 是否有历史漏洞 + if pk in packages_rule: + # 第二层遍历比较软件包涉及的漏洞版本 + for rule in packages_rule[pk]: + # 判断软件包版本是否存在主版本号,有则漏洞版本一起保留主版本号,否则去掉漏洞版本的主版本号 + pk_ver, vul_ver = self.adjust_ver(ver, rule[0]) + cp_result = self.vercmp(pk_ver, vul_ver) + if cp_result == -1: + if rule[1] not in systemscan_result: + systemscan_result[rule[1]] = detail[str(rule[1])] + systemscan_result[rule[1]]["impact"] = [{"package": pk, "version": pk_ver, "vul_ver": vul_ver}] + else: + systemscan_result[rule[1]]["impact"].append({"package": pk, "version": pk_ver, "vul_ver": vul_ver}) + # public.WriteFile("/tmp/centos7_result.json", json.dumps(systemscan_result, indent=4)) + + # 为了兼容旧版本再次做处理 + for sr in systemscan_result.values(): + one_risk = {} + one_risk["title"] = "【{}】Linux system vulnerability security notice".format(sr["ref_id"]) + one_risk["data"] = "2023-12-08" + one_risk["help"] = "" + one_risk["ignore"] = False + level = self.new_severity_to_num(sr["severity"]) + one_risk["level"] = level + one_risk["m_name"] = sr["ref_id"] + pk_list = [] + # 判断是否有内核漏洞在里面 + is_kernel = False + soft_list = [] + for impact in sr["impact"]: + if impact["package"].startswith("kernel"): + is_kernel = True + continue + soft_list.append("{} Versions below {}".format(impact["package"]+"-"+impact["version"], impact["vul_ver"])) + pk_list.append(impact["package"]) + if is_kernel: + continue + one_risk["msg"] = "The following system software was found to have security vulnerabilities:
                                    {}
                                    Vulnerabilities involved:{}
                                    Refer to the official announcement for details:{}".format('
                                    '.join(soft_list), '、'.join(sr["cve"]),sr["ref_url"]) + one_risk["ps"] = "【{}】Linux system vulnerability security notice".format(sr["ref_id"]) + one_risk["remind"] = "Fixing vulnerabilities has certain risks, so it is recommended to take a good system snapshot to prevent system operation from being affected." + one_risk["status"] = False + one_risk["taking"] = 0.000001 + one_risk["tips"] = ["Update the software to a safe version according to the risk description", "Or click [One-click Repair] to solve all security issues"] + one_risk["version"] = 1 + one_risk["type"] = "vulnerability" + one_risk["package"] = pk_list + + # 存储结果 + self.data["risk"].append(one_risk) + self.data["is_autofix"].append(one_risk["m_name"]) + + # 扫描中发现的漏洞数 + self.discov_count += 1 + # 扫描中的动态分数 + self.score -= level + if self.score < 0: + self.score = 0 + # 扫描中的动态风险 + self.tmp_data['risk'].append(one_risk) + # 可修复项 + self.tmp_data['is_autofix'].append(one_risk["m_name"]) + + # result_json = { + # "vul_count": len(detail), + # "risk": [], + # "ignore": [], + # "all_check_time": "", + # "ignore_count": 0, + # "msg": "", + # "repair_count": {"all_count": 0, "today_vount": 0}, + # } + # for sr in systemscan_result.values(): + # one_risk = { + # "cve_id": "", + # "vuln_name": "", + # "vuln_time": "2021-12-16", + # "vuln_solution": "", + # "level": 1, + # "soft_name": {}, + # "vuln_version": "", + # "check_time": 1701910962, + # "reboot": "" + # } + # one_risk["cve_id"] = sr["ref_id"] + # one_risk["vuln_name"] = "【{}】Linux软件安全公告".format(sr["ref_id"]) + # one_risk["vuln_solution"] = "更新涉及软件补丁,具体信息参考官方公告{}".format(sr["ref_url"]) + # level = self.new_severity_to_num(sr["severity"]) + # one_risk["level"] = level + # # 处理受影响的软件包(为了兼容旧版本暂时这样) + # soft_name = {} + # vuln_version = "" + # for impact in sr["impact"]: + # soft_name[impact["package"]] = impact["version"] + # vuln_version = impact["vul_ver"] + # one_risk["soft_name"] = soft_name + # one_risk["vuln_version"] = vuln_version + # risk_list.append(one_risk) + # + # # 扫描中发现的漏洞数 + # self.discov_count += 1 + # # 扫描中的动态分数 + # self.score -= level + # if self.score < 0: + # self.score = 0 + # # 扫描中的动态风险 + # self.tmp_data['risk'].append(one_risk) + # # 可修复项 + # self.tmp_data['is_autofix'].append(one_risk["cve_id"]) + # + # result_json["risk"] = risk_list + # public.WriteFile(self.__path + '/system_scan_time', int(time.time())) + # public.WriteFile(self._vuln_result, json.dumps(result_json)) + + def is_file_too_old(self, file_path, days): + """ + 判断文件是否过于陈旧 + :param file_path: 文件路径 + :param days: 超过的天数 + :return: bool + """ + mtime = os.path.getmtime(file_path) + # 不存在直接返回True + if mtime is None: + return True + mod_time = datetime.datetime.fromtimestamp(mtime) + days_old = datetime.datetime.now() - mod_time + return days_old.days > days + + def adjust_ver(self, ver_a, ver_b): + ''' + 确保两个版本主版本号统一,一方存在另一方不存在则删除主版本,要么都有,要么都没有 + ''' + if ":" in ver_a: + if ":" in ver_b: + ver_1 = ver_a + else: + ver_1 = ver_a.split(":")[1] + ver_2 = ver_b + else: + if ":" in ver_b: + ver_2 = ver_b.split(":")[1] + else: + ver_2 = ver_b + ver_1 = ver_a + return ver_1, ver_2 + + def new_severity_to_num(self, severity): + ''' + 将漏洞级别转变成数字 + ''' + if severity == "Critical": + return 3 + elif severity == "Important": + return 2 + elif severity == "Moderate": + return 1 + elif severity == "Low": + return 1 + elif severity == "High": + return 3 + elif severity == "Medium": + return 2 + else: + return 2 + + # def new_rpmvercmp(self, sys_ver, vul_ver): + # output, err = public.ExecShell("rpmdev-vercmp {} {}".format(sys_ver, vul_ver)) + # if err != '': + # return 1 + # output = output.strip() + # if output == "{} > {}".format(sys_ver, vul_ver): + # return 1 + # elif output == "{} == {}".format(sys_ver, vul_ver): + # return 0 + # elif output == "{} < {}".format(sys_ver, vul_ver): + # return -1 + # else: + # return 1 + + +class Dpkg: + def __init__(self): + self._fileinfo = None + self._control_str = None + self._headers = None + self._message = None + self._upstream_version = None + self._debian_revision = None + self._epoch = None + + @staticmethod + def get_epoch(version_str): + try: + e_index = version_str.index(":") + except ValueError: + return 0, version_str + + try: + epoch = int(version_str[0:e_index]) + except ValueError as ex: + print(f"Corrupt dpkg version '{version_str}': epochs can only be ints, and " + "epochless versions cannot use the colon character.") + return epoch, version_str[e_index + 1:] + + @staticmethod + def get_upstream(version_str): + try: + d_index = version_str.rindex("-") + except ValueError: + return version_str, "0" + + return version_str[0:d_index], version_str[d_index + 1:] + + @staticmethod + def split_full_version(version_str): + epoch, full_ver = Dpkg.get_epoch(version_str) + upstream_rev, debian_rev = Dpkg.get_upstream(full_ver) + return epoch, upstream_rev, debian_rev + + @staticmethod + def get_alphas(revision_str): + for i, char in enumerate(revision_str): + if char.isdigit(): + if i == 0: + return "", revision_str + return revision_str[0:i], revision_str[i:] + return revision_str, "" + + @staticmethod + def get_digits(revision_str): + if not revision_str: + return 0, "" + for i, char in enumerate(revision_str): + if not char.isdigit(): + if i == 0: + return 0, revision_str + return int(revision_str[0:i]), revision_str[i:] + return int(revision_str), "" + + @staticmethod + def listify(revision_str): + result = [] + while revision_str: + rev_1, remains = Dpkg.get_alphas(revision_str) + rev_2, remains = Dpkg.get_digits(remains) + result.extend([rev_1, rev_2]) + revision_str = remains + return result + + @staticmethod + def dstringcmp(a, b): + if a == b: + return 0 + try: + for i, char in enumerate(a): + if char == b[i]: + continue + if char == "~": + return -1 + if b[i] == "~": + return 1 + if char.isalpha() and not b[i].isalpha(): + return -1 + if not char.isalpha() and b[i].isalpha(): + return 1 + if ord(char) > ord(b[i]): + return 1 + if ord(char) < ord(b[i]): + return -1 + except IndexError: + if char == "~": + return -1 + return 1 + if b[len(a)] == "~": + return 1 + return -1 + + @staticmethod + def compare_revision_strings(rev1, rev2): + if rev1 == rev2: + return 0 + list1 = Dpkg.listify(rev1) + list2 = Dpkg.listify(rev2) + if list1 == list2: + return 0 + try: + for i, item in enumerate(list1): + if i >= len(list2): + raise IndexError + if not isinstance(item, list2[i].__class__): + print(f"Cannot compare '{item}' to {list2[i]}, something has gone horribly awry.") + if item == list2[i]: + continue + if isinstance(item, int): + if item > list2[i]: + return 1 + if item < list2[i]: + return -1 + else: + return Dpkg.dstringcmp(item, list2[i]) + except IndexError: + if list1[len(list2)][0][0] == "~": + return -1 + return 1 + if list2[len(list1)][0][0] == "~": + return 1 + return -1 + + @staticmethod + def compare_versions(ver1, ver2): + if ver1 == ver2: + return 0 + epoch1, upstream1, debian1 = Dpkg.split_full_version(str(ver1)) + epoch2, upstream2, debian2 = Dpkg.split_full_version(str(ver2)) + + if epoch1 < epoch2: + return -1 + if epoch1 > epoch2: + return 1 + + upstr_res = Dpkg.compare_revision_strings(upstream1, upstream2) + if upstr_res != 0: + return upstr_res + + debian_res = Dpkg.compare_revision_strings(debian1, debian2) + if debian_res != 0: + return debian_res + + return 0 + +if __name__ == "__main__": + # st = time.time() + panel = panelWarning() + # panel.new_system_scan() + public.WriteFile('/www/server/panel/data/warning/resultresult.json', json.dumps(panel._get_list())) + # et = time.time() + + + + + + +# #coding: utf-8 +# # +------------------------------------------------------------------- +# # | aaPanel +# # +------------------------------------------------------------------- +# # | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# # +------------------------------------------------------------------- +# # | Author: hwliang <2020-08-04> +# # +------------------------------------------------------------------- +# +# import os,sys,json,time,public +# +# class panelWarning: +# __path = '/www/server/panel/data/warning' +# __ignore = __path + '/ignore' +# __result = __path + '/result' +# __risk = __path + '/risk' +# def __init__(self): +# if not os.path.exists(self.__ignore): +# os.makedirs(self.__ignore,384) +# if not os.path.exists(self.__result): +# os.makedirs(self.__result,384) +# if not os.path.exists(self.__risk): +# os.makedirs(self.__risk, 384) +# +# def get_list(self,args): +# p = public.get_modules('class/safe_warning') +# data = { +# 'security':[], +# 'risk':[], +# 'ignore':[] +# } +# +# for m_name in p.__dict__.keys(): +# ignore_file = self.__ignore + '/' + m_name + '.pl' +# # 忽略的检查项 +# if p[m_name]._level == 0: continue +# +# m_info = { +# 'title': p[m_name]._title, +# 'm_name': m_name, +# 'ps': p[m_name]._ps, +# 'version': p[m_name]._version, +# 'level': p[m_name]._level, +# 'ignore': p[m_name]._ignore, +# 'date': p[m_name]._date, +# 'tips': p[m_name]._tips, +# 'help': p[m_name]._help +# } +# result_file = self.__result + '/' + m_name + '.pl' +# +# try: +# s_time = time.time() +# m_info['status'],m_info['msg'] = p[m_name].check_run() +# m_info['taking'] = round(time.time() - s_time,6) +# m_info['check_time'] = int(time.time()) +# public.writeFile(result_file,json.dumps([m_info['status'],m_info['msg'],m_info['check_time'],m_info['taking']],)) +# except: +# continue +# +# m_info['ignore'] = os.path.exists(ignore_file) +# if m_info['ignore']: +# data['ignore'].append(m_info) +# else: +# if m_info['status']: +# data['security'].append(m_info) +# else: +# risk_file = self.__risk + '/' + m_name + '.pl' +# public.writeFile(risk_file, json.dumps(m_info)) +# data['risk'].append(m_info) +# +# data['risk'] = sorted(data['risk'],key=lambda x: x['level'],reverse=True) +# data['security'] = sorted(data['security'],key=lambda x: x['level'],reverse=True) +# data['ignore'] = sorted(data['ignore'],key=lambda x: x['level'],reverse=True) +# # 获取支持一键修复的列表 +# try: +# is_autofix = public.read_config("safe_autofix") +# except: +# is_autofix = [] +# data['is_autofix'] = is_autofix +# return data +# +# +# def sync_rule(self): +# ''' +# @name 从云端同步规则 +# @author hwliang<2020-08-05> +# @return void +# ''' +# # try: +# # dep_path = '/www/server/panel/class/safe_warning' +# # local_version_file = self.__path + '/version.pl' +# # last_sync_time = local_version_file = self.__path + '/last_sync.pl' +# # if os.path.exists(dep_path): +# # if os.path.exists(last_sync_time): +# # if int(public.readFile(last_sync_time)) > time.time(): +# # return +# # else: +# # if os.path.exists(local_version_file): os.remove(local_version_file) +# +# # download_url = public.get_url() +# # version_url = download_url + '/install/warning/version.txt' +# # cloud_version = public.httpGet(version_url) +# # if cloud_version: cloud_version = cloud_version.strip() +# +# # local_version = public.readFile(local_version_file) +# # if local_version: +# # if cloud_version == local_version: +# # return +# +# # tmp_file = '/tmp/bt_safe_warning.zip' +# # public.ExecShell('wget -O {} {} -T 5'.format(tmp_file,download_url + '/install/warning/safe_warning.zip')) +# # if not os.path.exists(tmp_file): +# # return +# +# # if os.path.getsize(tmp_file) < 2129: +# # os.remove(tmp_file) +# # return +# +# # if not os.path.exists(dep_path): +# # os.makedirs(dep_path,384) +# # public.ExecShell("unzip -o {} -d {}/ >/dev/null".format(tmp_file,dep_path)) +# # public.writeFile(local_version_file,cloud_version) +# # public.writeFile(last_sync_time,str(int(time.time() + 7200))) +# # if os.path.exists(tmp_file): os.remove(tmp_file) +# # public.ExecShell("chmod -R 600 {}".format(dep_path)) +# # except: +# # pass +# +# +# +# def set_ignore(self,args): +# ''' +# @name 设置指定项忽略状态 +# @author hwliang<2020-08-04> +# @param dict_obj { +# m_name 模块名称 +# } +# @return dict +# ''' +# m_name = args.m_name.strip() +# ignore_file = self.__ignore + '/' + m_name + '.pl' +# if os.path.exists(ignore_file): +# os.remove(ignore_file) +# else: +# public.writeFile(ignore_file,'1') +# return public.returnMsg(True,'Successfully set!') +# +# def check_find(self, args): +# ''' +# @name 检测指定项 +# @author hwliang<2020-08-04> +# @param dict_obj { +# m_name 模块名称 +# } +# @return dict +# ''' +# try: +# m_name = args.m_name.strip() +# p = public.get_modules('class/safe_warning') +# m_info = { +# 'title': p[m_name]._title, +# 'm_name': m_name, +# 'ps': p[m_name]._ps, +# 'version': p[m_name]._version, +# 'level': p[m_name]._level, +# 'ignore': p[m_name]._ignore, +# 'date': p[m_name]._date, +# 'tips': p[m_name]._tips, +# 'help': p[m_name]._help +# } +# +# # 解决已经在忽略列表中,但是如果仍然需要检查的话可以检查 +# ignore_file = self.__ignore + '/' + m_name + '.pl' +# if os.path.exists(ignore_file): +# from cachelib import SimpleCache +# cache = SimpleCache(5000) +# ikey = 'warning_list' +# cache.delete(ikey) +# os.remove(ignore_file) +# +# result_file = self.__result + '/' + m_name + '.pl' +# s_time = time.time() +# m_info['status'], m_info['msg'] = p[m_name].check_run() +# m_info['taking'] = round(time.time() - s_time, 4) +# m_info['check_time'] = int(time.time()) +# public.writeFile(result_file, json.dumps( +# [m_info['status'], m_info['msg'], m_info['check_time'], m_info['taking']])) +# return public.returnMsg(True, 'Retested') +# except: +# return public.returnMsg(False, 'Detection failed') diff --git a/class_v2/password_v2.py b/class_v2/password_v2.py new file mode 100644 index 00000000..68b43a6d --- /dev/null +++ b/class_v2/password_v2.py @@ -0,0 +1,218 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: lkqiang +# +------------------------------------------------------------------- +# +-------------------------------------------------------------------- +# | 密码管理 +# +-------------------------------------------------------------------- +import sys, os +if sys.version_info[0] == 2: + reload(sys) + sys.setdefaultencoding('utf-8') +os.chdir('/www/server/panel') +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import re +import public,data,database,config + +class password: + def __init__(self): + self.__data=data.data() + self.__database=database.database() + self.__config=config.config() + + #设置面板密码 + def set_panel_password(self,get): + get.password1=get.password + get.password2 = get.password + data=self.__config.setPassword(get) + return data + + #查看面板用户名 + def get_panel_username(self,get): + data=public.M('users').where("id=?", (1,)).getField('username') + if data: + return data + else: + return False + + # 设置root 密码 + def set_root_password(self,get): + public.ExecShell("echo"+get.user+":"+get.password+"|chpasswd") + return True + + #查看mysql_root密码 + def get_mysql_root(self,get): + password = public.M('config').where("id=?",(1,)).getField('mysql_root') + return public.returnMsg(True, password) + + #设置mysql_root 密码 + def set_mysql_password(self,get): + if 'password' in get: + resutl=self.__database.SetupPassword(get) + return resutl + else: + return public.returnMsg(False, 'password参数不能为空') + + + # MySQL 的其他账户设置 + #获取其他mysql的信息 + def get_databses(self,get): + data=public.M('databases').select() + return public.returnMsg(True, data) + + # 修改MySQL 其他账户的密码 + def rem_mysql_pass(self,get): + ''' + 参数 三个 + id 数据库ID, name:数据库名称, password:数据库密码 + ''' + data=self.__database.ResDatabasePassword(get) + return data + + # 修改其他Mysql 账户的权限 + def set_mysql_access(self,get): + ''' + 参数 三个 + name:数据库名称, dataAccess: 权限 access 权限 + ''' + data=self.__database.SetDatabaseAccess(get) + return data + + + #################### SSH 的基础设置#################### + + # 开启密码登陆 + def SetPassword(self, get): + ssh_password = '\n#?PasswordAuthentication\\s\\w+' + file = public.readFile('/etc/ssh/sshd_config') + if len(re.findall(ssh_password, file)) == 0: + file_result = file + '\nPasswordAuthentication yes' + else: + file_result = re.sub(ssh_password, '\nPasswordAuthentication yes', file) + self.Wirte('/etc/ssh/sshd_config', file_result) + self.RestartSsh() + return public.returnMsg(True, '开启成功') + + # 设置ssh_key + def SetSshKey(self, get): + '''''' + type_list = ['rsa', 'dsa'] + ssh_type = ['yes', 'no'] + ssh = get.ssh + if not ssh in ssh_type: return public.returnMsg(False, 'ssh选项失败') + type = get.type + if not type in type_list: return public.returnMsg(False, '加密方式错误') + file = ['/root/.ssh/id_rsa.pub', '/root/.ssh/id_rsa', '/root/.ssh/authorized_keys'] + for i in file: + if os.path.exists(i): + os.remove(i) + public.ExecShell("ssh-keygen -t %s -P '' -f ~/.ssh/id_rsa |echo y" % type) + if os.path.exists(file[0]): + public.ExecShell('cat %s >%s && chmod 600 %s' % (file[0], file[-1], file[-1])) + rec = '\n#?RSAAuthentication\\s\\w+' + rec2 = '\n#?PubkeyAuthentication\\s\\w+' + file = public.readFile('/etc/ssh/sshd_config') + if len(re.findall(rec, file)) == 0: file = file + '\nRSAAuthentication yes' + if len(re.findall(rec2, file)) == 0: file = file + '\nPubkeyAuthentication yes' + file_ssh = re.sub(rec, '\nRSAAuthentication yes', file) + file_result = re.sub(rec2, '\nPubkeyAuthentication yes', file_ssh) + if ssh == 'no': + ssh_password = '\n#?PasswordAuthentication\\s\\w+' + if len(re.findall(ssh_password, file_result)) == 0: + file_result = file_result + '\nPasswordAuthentication no' + else: + file_result = re.sub(ssh_password, '\nPasswordAuthentication no', file_result) + self.Wirte('/etc/ssh/sshd_config', file_result) + self.RestartSsh() + return public.returnMsg(True, '开启成功') + else: + return public.returnMsg(False, '开启失败') + + + # 关闭sshkey + def StopKey(self, get): + file = ['/root/.ssh/id_rsa.pub', '/root/.ssh/id_rsa', '/root/.ssh/authorized_keys'] + rec = '\n#?RSAAuthentication\\s\\w+' + rec2 = '\n#?PubkeyAuthentication\\s\\w+' + file = public.readFile('/etc/ssh/sshd_config') + file_ssh = re.sub(rec, '\n#RSAAuthentication no', file) + file_result = re.sub(rec2, '\n#PubkeyAuthentication no', file_ssh) + self.Wirte('/etc/ssh/sshd_config', file_result) + self.SetPassword(get) + self.RestartSsh() + return public.returnMsg(True, '关闭成功') + # 读取配置文件 获取当前状态 + + def GetConfig(self, get): + result = {} + file = public.readFile('/etc/ssh/sshd_config') + rec = '\n#?RSAAuthentication\\s\\w+' + pubkey = '\n#?PubkeyAuthentication\\s\\w+' + ssh_password = '\nPasswordAuthentication\\s\\w+' + ret = re.findall(ssh_password, file) + if not ret: + result['password'] = 'no' + else: + if ret[-1].split()[-1] == 'yes': + result['password'] = 'yes' + else: + result['password'] = 'no' + pubkey = re.findall(pubkey, file) + if not pubkey: + result['pubkey'] = 'no' + else: + if pubkey[-1].split()[-1] == 'no': + result['pubkey'] = 'no' + else: + result['pubkey'] = 'yes' + rsa_auth = re.findall(rec, file) + if not rsa_auth: + result['rsa_auth'] = 'no' + else: + if rsa_auth[-1].split()[-1] == 'no': + result['rsa_auth'] = 'no' + else: + result['rsa_auth'] = 'yes' + return result + + # 关闭密码方式 + def StopPassword(self, get): + file = public.readFile('/etc/ssh/sshd_config') + ssh_password = '\n#?PasswordAuthentication\\s\\w+' + file_result = re.sub(ssh_password, '\nPasswordAuthentication no', file) + self.Wirte('/etc/ssh/sshd_config', file_result) + self.RestartSsh() + return public.returnMsg(True, '关闭成功') + + #显示key文件 + def GetKey(self, get): + file = '/root/.ssh/id_rsa' + if not os.path.exists(file): return public.returnMsg(True, '') + ret = public.readFile(file) + return public.returnMsg(True, ret) + + # 下载 + def Download(self, get): + if os.path.exists('/root/.ssh/id_rsa'): + ret = '/download?filename=/root/.ssh/id_rsa' + return public.returnMsg(True, ret) + + # 写入配置文件 + def Wirte(self, file, ret): + result = public.writeFile(file, ret) + return result + + def RestartSsh(self): + version = public.readFile('/etc/redhat-release') + act = 'restart' + if not os.path.exists('/etc/redhat-release'): + public.ExecShell('service ssh ' + act) + elif version.find(' 7.') != -1: + public.ExecShell("systemctl " + act + " sshd.service") + else: + public.ExecShell("/etc/init.d/sshd " + act) diff --git a/class_v2/plugin_auth_v2.py b/class_v2/plugin_auth_v2.py new file mode 100644 index 00000000..557e13d2 --- /dev/null +++ b/class_v2/plugin_auth_v2.py @@ -0,0 +1,97 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +#+-------------------------------------------------------------------- +#| 插件认证模块 +#+-------------------------------------------------------------------- + + +import public +import PluginLoader + +class Plugin: + __plugin_name = None + __is_php = False + __dict__ = None + __obj_dict = {} + + + def __init__(self,init_plugin_name = None): + ''' + @name 实例化插件对像 + @author hwliang<2021-06-15> + @param init_plugin_name 插件名称 + @return Plguin + ''' + if not init_plugin_name is False: + if not init_plugin_name: + raise ValueError('参数错误,plugin_name少需要一个有效参数') + self.__plugin_name = init_plugin_name + + def get_plugin_list(self,upgrade_force = False): + ''' + @name 获取插件列表 + @author hwliang<2021-06-15> + @param upgrade_force 是否强制重新获取列表 + @return dict + ''' + force = 1 if upgrade_force else 0 + return PluginLoader.get_plugin_list(force) + + + def exec_fun(self,get_args,def_name = None): + ''' + @name 执行指定方法 + @author hwliang<2021-06-16> + @param def_name 方法名称 + @param get_args POST/GET参数对像 + @return mixed + ''' + if not def_name: + def_name = get_args.get("s","") + else: + if not 's' in get_args: + get_args.s = def_name + + res = PluginLoader.plugin_run(self.__plugin_name,def_name,get_args) + if isinstance(res,dict): + if 'status' in res and res['status'] == False and 'msg' in res: + if isinstance(res['msg'],str): + if res['msg'].find('Traceback ') != -1: + raise public.PanelError(res['msg']) + return res + + def get_fun(self,def_name): + ''' + @name 获取函对像 + @author hwliang<2021-06-28> + @param def_name 函数名称 + @return func_object + ''' + if def_name in self.__obj_dict.keys(): + return self.__obj_dict[def_name] + get_args = public.dict_obj() + get_args.plugin_get_object = 1 + return PluginLoader.plugin_run(self.__plugin_name,def_name,get_args) + + + def isdef(self,def_name): + ''' + @name 指定方法是否存在 + @author hwliang<2021-06-16> + @param def_name 方法名称 + @return bool + ''' + if self.__is_php: return True + self.__obj_dict[def_name] = self.get_fun(def_name) + return True if self.__obj_dict[def_name] else False + + def __dir__(self): + return '' + diff --git a/class_v2/plugin_deployment_v2.py b/class_v2/plugin_deployment_v2.py new file mode 100644 index 00000000..1eb40cde --- /dev/null +++ b/class_v2/plugin_deployment_v2.py @@ -0,0 +1,519 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +#+-------------------------------------------------------------------- +#| 自动部署网站 +#+-------------------------------------------------------------------- + +import public,json,os,time,sys,re +from BTPanel import session,cache +class obj: id=0 +class plugin_deployment: + __setupPath = 'data' + __panelPath = '/www/server/panel' + logPath = 'data/deployment_speed.json' + __tmp = '/www/server/panel/temp/' + timeoutCount = 0 + oldTime = 0 + _speed_key = 'dep_download_speed' + + #获取列表 + def GetList(self,get): + self.GetCloudList(get) + jsonFile = self.__panelPath + '/data/deployment_list.json' + if not os.path.exists(jsonFile): return public.returnMsg(False,'Profile does not exist!') + data = {} + data = self.get_input_list(json.loads(public.readFile(jsonFile))) + + if not hasattr(get,'type'): + get.type = 0 + else: + get.type = int(get.type) + if not hasattr(get,'search'): + search = None + m = 0 + else: + if sys.version_info[0] == 2: + search = get.search.encode('utf-8').lower() + else: + search = get.search.lower() + m = 1 + + tmp = [] + for d in data['list']: + i=0 + if get.type > 0: + if get.type == d['type']: i+=1 + else: + i+=1 + if search: + if d['name'].lower().find(search) != -1: i+=1 + if d['title'].lower().find(search) != -1: i+=1 + if d['ps'].lower().find(search) != -1: i+=1 + if get.type > 0 and get.type != d['type']: i -= 1 + + if i>m: + del(d['versions'][0]['download']) + del(d['versions'][0]['md5']) + d = self.get_icon(d) + tmp.append(d) + + data['list'] = tmp + return data + + #获取图标 + def get_icon(self,pinfo): + path = '/www/server/panel/BTPanel/static/img/dep_ico' + if not os.path.exists(path): os.makedirs(path,384) + filename = "%s/%s.png" % (path, pinfo['name']) + m_uri = pinfo['min_image'] + pinfo['min_image'] = '/static/img/dep_ico/%s.png' % pinfo['name'] + if sys.version_info[0] == 2: filename = filename.encode('utf-8') + if os.path.exists(filename): + if os.path.getsize(filename) > 100: return pinfo + public.ExecShell("wget -O " + filename + ' https://www.bt.cn' + m_uri + " &") + return pinfo + + #获取插件列表 + def GetDepList(self,get): + jsonFile = self.__setupPath + '/deployment_list.json' + if not os.path.exists(jsonFile): return public.returnMsg(False,'Profile does not exist!') + data = {} + data = json.loads(public.readFile(jsonFile)) + return self.get_input_list(data) + + #获取本地导入的插件 + def get_input_list(self,data): + try: + jsonFile = self.__setupPath + '/deployment_list_other.json' + if not os.path.exists(jsonFile): return data + i_data = json.loads(public.readFile(jsonFile)) + for d in i_data: + data['list'].append(d) + return data + except:return data + + #从云端获取列表 + def GetCloudList(self,get): + try: + jsonFile = self.__setupPath + '/deployment_list.json' + if not 'package' in session or not os.path.exists(jsonFile) or hasattr(get,'force'): + downloadUrl = 'http://www.bt.cn/api/panel/get_deplist' + pdata = public.get_pdata() + tmp = json.loads(public.httpPost(downloadUrl,pdata,3)) + if not tmp: return public.returnMsg(False,'Failed to get from the cloud!') + public.writeFile(jsonFile,json.dumps(tmp)) + session['package'] = True + return public.returnMsg(True,'Update completed!') + return public.returnMsg(True,'No need to update!') + except: + return public.returnMsg(False,'Failed to get from the cloud!') + + + + #导入程序包 + def AddPackage(self,get): + jsonFile = self.__setupPath + '/deployment_list_other.json' + if not os.path.exists(jsonFile): + public.writeFile(jsonFile,'[]') + pinfo = {} + pinfo['name'] = get.name + pinfo['title'] = get.title + pinfo['version'] = get.version + pinfo['php'] = get.php + pinfo['ps'] = get.ps + pinfo['official'] = '#' + pinfo['sort'] = 1000 + pinfo['min_image'] = '' + pinfo['id'] = 0 + pinfo['type'] = 100 + pinfo['enable_functions'] = get.enable_functions + pinfo['author'] = 'Import from local' + from werkzeug.utils import secure_filename + from flask import request + f = request.files['dep_zip'] + s_path = self.__panelPath + '/package' + if not os.path.exists(s_path): os.makedirs(s_path,384) + s_file = s_path + '/' + pinfo['name'] + '.zip' + if os.path.exists(s_file): os.remove(s_file) + f.save(s_file) + os.chmod(s_file,384) + pinfo['versions'] = [] + version = {"cpu_limit": 1, + "dependnet": "", + "m_version":pinfo['version'], + "mem_limit": 32, + "os_limit": 0, + "size": os.path.getsize(s_file), + "version": "0", + "download":"", + "version_msg": "test2"} + version['md5'] = self.GetFileMd5(s_file) + pinfo['versions'].append(version) + data = json.loads(public.readFile(jsonFile)) + is_exists = False + for i in range(len(data)): + if data[i]['name'] == pinfo['name']: + data[i] = pinfo + is_exists = True + + if not is_exists: data.append(pinfo) + + public.writeFile(jsonFile,json.dumps(data)) + return public.returnMsg(True,'Import Success!') + + #取本地包信息 + def GetPackageOther(self,get): + p_name = get.p_name + jsonFile = self.__setupPath + '/deployment_list_other.json' + if not os.path.exists(jsonFile): public.returnMsg(False,'could not find [%s]' % p_name) + data = json.loads(public.readFile(jsonFile)) + + for i in range(len(data)): + if data[i]['name'] == p_name: return data[i] + return public.returnMsg(False,'could not find [%s]' % p_name) + + + #删除程序包 + def DelPackage(self,get): + jsonFile = self.__setupPath + '/deployment_list_other.json' + if not os.path.exists(jsonFile): return public.returnMsg(False,'Profile does not exist!') + + data = {} + data = json.loads(public.readFile(jsonFile)) + + tmp = [] + for d in data: + if d['name'] == get.dname: + s_file = self.__panelPath + '/package/' + d['name'] + '.zip' + if os.path.exists(s_file): os.remove(s_file) + continue + tmp.append(d) + + data = tmp + public.writeFile(jsonFile,json.dumps(data)) + return public.returnMsg(True,'Successfully deleted!') + + #下载文件 + def DownloadFile(self,url,filename): + try: + path = os.path.dirname(filename) + if not os.path.exists(path): os.makedirs(path) + import urllib,socket + socket.setdefaulttimeout(10) + self.pre = 0 + self.oldTime = time.time() + if sys.version_info[0] == 2: + urllib.urlretrieve(url,filename=filename,reporthook= self.DownloadHook) + else: + urllib.request.urlretrieve(url,filename=filename,reporthook= self.DownloadHook) + self.WriteLogs(json.dumps({'name':'Download File','total':0,'used':0,'pre':0,'speed':0})) + except: + if self.timeoutCount > 5: return + self.timeoutCount += 1 + time.sleep(5) + self.DownloadFile(url,filename) + + #下载文件进度回调 + def DownloadHook(self,count, blockSize, totalSize): + used = count * blockSize + pre1 = int((100.0 * used / totalSize)) + if self.pre != pre1: + dspeed = used / (time.time() - self.oldTime) + speed = {'name':'Download File','total':totalSize,'used':used,'pre':self.pre,'speed':dspeed} + self.WriteLogs(json.dumps(speed)) + self.pre = pre1 + + #写输出日志 + def WriteLogs(self,logMsg): + fp = open(self.logPath,'w+') + fp.write(logMsg) + fp.close() + + #一键安装网站程序 + #param string name 程序名称 + #param string site_name 网站名称 + #param string php_version PHP版本 + def SetupPackage(self,get): + name = get.dname + site_name = get.site_name + php_version = get.php_version + #取基础信息 + find = public.M('sites').where('name=?',(site_name,)).field('id,path,name').find() + if not 'path' in find: + return public.returnMsg(False, 'Site not exist!') + path = find['path'] + if path.replace('//','/') == '/': return public.returnMsg(False,'Dangerous website root directory!') + #获取包信息 + pinfo = self.GetPackageInfo(name) + id = pinfo['id'] + if not pinfo: return public.returnMsg(False,'The specified package does not exist.!') + + #检查本地包 + self.WriteLogs(json.dumps({'name':'Verifying package...','total':0,'used':0,'pre':0,'speed':0})) + pack_path = self.__panelPath + '/package' + if not os.path.exists(pack_path): os.makedirs(pack_path,384) + packageZip = pack_path + '/'+ name + '.zip' + isDownload = False + if os.path.exists(packageZip): + md5str = self.GetFileMd5(packageZip) + if md5str != pinfo['versions'][0]['md5']: isDownload = True + else: + isDownload = True + + #下载文件 + if isDownload: + self.WriteLogs(json.dumps({'name':'Downloading file ...','total':0,'used':0,'pre':0,'speed':0})) + if pinfo['versions'][0]['download']: self.DownloadFile('http://www.bt.cn/api/Pluginother/get_file?fname=' + pinfo['versions'][0]['download'], packageZip) + + if not os.path.exists(packageZip): return public.returnMsg(False,'File download failed!' + packageZip) + + pinfo = self.set_temp_file(packageZip,path) + if not pinfo: return public.returnMsg(False,'Cannot find [aaPanel Auto Deployment Configuration File] in the installation package') + + #设置权限 + self.WriteLogs(json.dumps({'name':'Setting permissions','total':0,'used':0,'pre':0,'speed':0})) + public.ExecShell('chmod -R 755 ' + path) + public.ExecShell('chown -R www.www ' + path) + if pinfo['chmod']: + for chm in pinfo['chmod']: + public.ExecShell('chmod -R ' + str(chm['mode']) + ' ' + (path + '/' + chm['path']).replace('//','/')) + + #安装PHP扩展 + self.WriteLogs(json.dumps({'name':'Install the necessary PHP extensions','total':0,'used':0,'pre':0,'speed':0})) + import files + mfile = files.files(); + if type(pinfo['php_ext']) != list : pinfo['php_ext'] = pinfo['php_ext'].strip().split(',') + for ext in pinfo['php_ext']: + if ext == 'pathinfo': + import config + con = config.config() + get.version = php_version + get.type = 'on' + con.setPathInfo(get) + else: + get.name = ext + get.version = php_version + get.type = '1' + mfile.InstallSoft(get) + + #解禁PHP函数 + if 'enable_functions' in pinfo: + try: + if type(pinfo['enable_functions']) == str : pinfo['enable_functions'] = pinfo['enable_functions'].strip().split(',') + php_f = public.GetConfigValue('setup_path') + '/php/' + php_version + '/etc/php.ini' + php_c = public.readFile(php_f) + rep = "disable_functions\\s*=\\s{0,1}(.*)\n" + tmp = re.search(rep,php_c).groups() + disable_functions = tmp[0].split(',') + for fun in pinfo['enable_functions']: + fun = fun.strip() + if fun in disable_functions: disable_functions.remove(fun) + disable_functions = ','.join(disable_functions) + php_c = re.sub(rep, 'disable_functions = ' + disable_functions + "\n", php_c) + public.writeFile(php_f,php_c) + public.phpReload(php_version) + except:pass + + + #执行额外shell进行依赖安装 + self.WriteLogs(json.dumps({'name':'Execute extra SHELL','total':0,'used':0,'pre':0,'speed':0})) + if os.path.exists(path+'/install.sh'): + public.ExecShell('cd '+path+' && bash ' + 'install.sh ' + find['name'] + " &> install.log") + public.ExecShell('rm -f ' + path+'/install.sh') + + #是否执行Composer + if os.path.exists(path + '/composer.json'): + self.WriteLogs(json.dumps({'name':'Execute Composer','total':0,'used':0,'pre':0,'speed':0})) + if not os.path.exists(path + '/composer.lock'): + execPHP = '/www/server/php/' + php_version +'/bin/php' + if execPHP: + if public.get_url().find('125.88'): + public.ExecShell('cd ' +path+' && '+execPHP+' /usr/bin/composer config repo.packagist composer https://packagist.phpcomposer.com') + import panelSite + phpini = '/www/server/php/' + php_version + '/etc/php.ini' + phpiniConf = public.readFile(phpini) + phpiniConf = phpiniConf.replace('proc_open,proc_get_status,','') + public.writeFile(phpini,phpiniConf) + public.ExecShell('nohup cd '+path+' && '+execPHP+' /usr/bin/composer install -vvv > /tmp/composer.log 2>&1 &') + + #写伪静态 + self.WriteLogs(json.dumps({'name':'Set URL rewrite','total':0,'used':0,'pre':0,'speed':0})) + swfile = path + '/nginx.rewrite' + if os.path.exists(swfile): + rewriteConf = public.readFile(swfile) + dwfile = self.__panelPath + '/vhost/rewrite/' + site_name + '.conf' + public.writeFile(dwfile,rewriteConf) + + swfile = path + '/.htaccess' + if os.path.exists(swfile): + swpath = (path + '/'+ pinfo['run_path'] + '/.htaccess').replace('//','/') + if pinfo['run_path'] != '/' and not os.path.exists(swpath): + public.writeFile(swpath, public.readFile(swfile)) + + + #删除伪静态文件 + public.ExecShell("rm -f " + path + '/*.rewrite') + + #删除多余文件 + rm_file = path + '/index.html' + if os.path.exists(rm_file): + rm_file_body = public.readFile(rm_file) + if rm_file_body.find('panel-heading') != -1: os.remove(rm_file) + + #设置运行目录 + self.WriteLogs(json.dumps({'name':'Set the run directory','total':0,'used':0,'pre':0,'speed':0})) + if pinfo['run_path'] != '/': + import panelSite + siteObj = panelSite.panelSite() + mobj = obj() + mobj.id = find['id'] + mobj.runPath = pinfo['run_path'] + siteObj.SetSiteRunPath(mobj) + + #导入数据 + self.WriteLogs(json.dumps({'name':'Import database','total':0,'used':0,'pre':0,'speed':0})) + if os.path.exists(path+'/import.sql'): + databaseInfo = public.M('databases').where('pid=?',(find['id'],)).field('username,password').find() + if databaseInfo: + public.ExecShell('/www/server/mysql/bin/mysql -u' + databaseInfo['username'] + ' -p' + databaseInfo['password'] + ' ' + databaseInfo['username'] + ' < ' + path + '/import.sql') + public.ExecShell('rm -f ' + path + '/import.sql') + siteConfigFile = (path + '/' + pinfo['db_config']).replace('//','/') + if os.path.exists(siteConfigFile): + siteConfig = public.readFile(siteConfigFile) + siteConfig = siteConfig.replace('BT_DB_USERNAME',databaseInfo['username']) + siteConfig = siteConfig.replace('BT_DB_PASSWORD',databaseInfo['password']) + siteConfig = siteConfig.replace('BT_DB_NAME',databaseInfo['username']) + public.writeFile(siteConfigFile,siteConfig) + + #清理文件和目录 + self.WriteLogs(json.dumps({'name':'清理多余的文件','total':0,'used':0,'pre':0,'speed':0})) + if type(pinfo['remove_file']) == str : pinfo['remove_file'] = pinfo['remove_file'].strip().split(',') + print(pinfo['remove_file']) + for f_path in pinfo['remove_file']: + if not f_path: continue + filename = (path + '/' + f_path).replace('//','/') + if os.path.exists(filename): + if not os.path.isdir(filename): + if f_path.find('.user.ini') != -1: + public.ExecShell("chattr -i " + filename) + os.remove(filename) + else: + public.ExecShell("rm -rf " + filename) + + public.serviceReload() + if id: self.depTotal(id) + self.WriteLogs(json.dumps({'name':'Ready to deploy','total':0,'used':0,'pre':0,'speed':0})) + return public.returnMsg(True,pinfo) + + + #处理临时文件 + def set_temp_file(self,filename,path): + public.ExecShell("rm -rf " + self.__tmp + '/*') + self.WriteLogs(json.dumps({'name':'Unpacking the package...','total':0,'used':0,'pre':0,'speed':0})) + public.ExecShell('unzip -o '+filename+' -d ' + self.__tmp) + auto_config = 'auto_install.json' + p_info = self.__tmp + '/' + auto_config + p_tmp = self.__tmp + p_config = None + if not os.path.exists(p_info): + d_path = None + for df in os.walk(self.__tmp): + if len(df[2]) < 3: continue + if not auto_config in df[2]: continue + if not os.path.exists(df[0] + '/' + auto_config): continue + d_path = df[0] + if d_path: + tmp_path = d_path + auto_file = tmp_path + '/' + auto_config + if os.path.exists(auto_file): + p_info = auto_file + p_tmp = tmp_path + if os.path.exists(p_info): + try: + p_config = json.loads(public.readFile(p_info)) + os.remove(p_info) + i_ndex_html = path + '/index.html' + if os.path.exists(i_ndex_html): os.remove(i_ndex_html) + if not self.copy_to(p_tmp,path): public.ExecShell((r"\cp -arf " + p_tmp + '/. ' + path + '/').replace('//','/')) + except: pass + public.ExecShell("rm -rf " + self.__tmp + '/*') + return p_config + + + def copy_to(self,src,dst): + try: + if src[-1] == '/': src = src[:-1] + if dst[-1] == '/': dst = dst[:-1] + if not os.path.exists(src): return False + if not os.path.exists(dst): os.makedirs(dst) + import shutil + for p_name in os.listdir(src): + f_src = src + '/' + p_name + f_dst = dst + '/' + p_name + if os.path.isdir(f_src): + print(shutil.copytree(f_src,f_dst)) + else: + print(shutil.copyfile(f_src,f_dst)) + return True + except: return False + + + #提交安装统计 + def depTotal(self,id): + import panelAuth + p = panelAuth.panelAuth() + pdata = p.create_serverid(None); + pdata['pid'] = id; + p_url = 'http://www.bt.cn/api/pluginother/create_order_okey' + public.httpPost(p_url,pdata) + + #获取进度 + def GetSpeed(self,get): + try: + if not os.path.exists(self.logPath):return public.returnMsg(False,'There are currently no deployment tasks!') + return json.loads(public.readFile(self.logPath)) + except: + return {'name':'Ready to deploy','total':0,'used':0,'pre':0,'speed':0} + + #获取包信息 + def GetPackageInfo(self,name): + data = self.GetDepList(None) + if not data: return False + for info in data['list']: + if info['name'] == name: + return info + return False + + #检查指定包是否存在 + def CheckPackageExists(self,name): + data = self.GetDepList(None) + if not data: return False + for info in data['list']: + if info['name'] == name: return True + + return False + + #文件的MD5值 + def GetFileMd5(self,filename): + if not os.path.isfile(filename): return False + import hashlib + myhash = hashlib.md5() + f = open(filename,'rb') + while True: + b = f.read(8096) + if not b : + break + myhash.update(b) + f.close() + return myhash.hexdigest() + + #获取站点标识 + def GetSiteId(self,get): + return public.M('sites').where('name=?',(get.webname,)).getField('id') diff --git a/class_v2/projectModelV2/base.py b/class_v2/projectModelV2/base.py new file mode 100644 index 00000000..c47f27cb --- /dev/null +++ b/class_v2/projectModelV2/base.py @@ -0,0 +1,63 @@ +#coding: utf-8 +import public,re + +class projectBase: + + 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,}(? 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) diff --git a/class_v2/projectModelV2/common/limit_net.py b/class_v2/projectModelV2/common/limit_net.py new file mode 100644 index 00000000..f044654d --- /dev/null +++ b/class_v2/projectModelV2/common/limit_net.py @@ -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, "参数错误") + + if self.config_prefix is None: + return public.returnMsg(False, "不支持的网站类型") + + # 取配置文件 + 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, "配置文件读取错误") + + # 站点总并发 + data = { + 'perserver': 0, + 'perip': 0, + 'limit_rate': 0, + } + + rep_per_server = re.compile(r"(?P.*)limit_conn +perserver +(?P\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.*)limit_conn +perip +(?P\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.*)limit_rate +(?P\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, "全局配置缓存配置失败" + 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, "全局配置缓存配置失败" + 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, "参数错误") + + if per_server < 1 or perip < 1 or limit_rate < 1: + return public.returnMsg(False, '并发限制,IP限制,流量限制必需大于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:
                                    ' + is_error.replace("\n", '
                                    ') + '
                                    ') + + public.serviceReload() + public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_OPEN_SUCCESS', (site_name,)) + return public.returnMsg(True, 'SET_SUCCESS') + + # 关闭流量限制 + 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:
                                    ' + is_error.replace("\n", '
                                    ') + '
                                    ') + public.serviceReload() + public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_CLOSE_SUCCESS', (site_name,)) + return public.returnMsg(True, 'SITE_NETLIMIT_CLOSE_SUCCESS') diff --git a/class_v2/projectModelV2/common/redirect.py b/class_v2/projectModelV2/common/redirect.py new file mode 100644 index 00000000..c224f8f5 --- /dev/null +++ b/class_v2/projectModelV2/common/redirect.py @@ -0,0 +1,806 @@ +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 = """ + + RewriteEngine on + RewriteCond %%{HTTP_HOST} ^%s [NC] + RewriteRule ^(.*) %s%s [L,R=%s] + +""" + _ap_path_format = """ + + RewriteEngine on + RewriteRule ^%s(.*) %s%s [L,R=%s] + +""" + + 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 '配置文件出错请先排查配置' + + 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 '参数错误' + + if not is_modify: + if not redirect_name: + return "参数错误,配置名称不能为空" + # 检测名称是否重复 + if not (3 < len(redirect_name) < 15): + return '名称必须大于3小于15个字符串' + + if self._check_redirect(site_name, redirect_name, error_page == 1): + return '指定重定向名称已存在' + + site_info = public.M('sites').where("name=?", (site_name,)).find() + if not isinstance(site_info, dict): + return "站点信息查询错误" + 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 '目标URL格式不对【%s】' % to_url + + # 非404页面de重定向检测项 + if error_page != 1: + # 检测是否选择域名 + if domain_or_path == "domain": + if not redirect_domain: + return '请选择重定向域名' + # 检测域名是否已经存在配置文件 + repeat_domain = self._check_redirect_domain_exist(site_name, redirect_domain, redirect_name, is_modify) + if repeat_domain: + return '重定向域名重复 %s' % repeat_domain + + # 检查目标URL的域名和被重定向的域名是否一样 + tu = self._parse_url_domain(to_url) + for d in redirect_domain: + if d == tu: + return '域名 "%s" 和目标域名一致请取消选择' % d + else: + if not redirect_path: + return '请输入重定向路径' + if redirect_path[0] != "/": + return "路径格式不正确,格式为/xxx" + # 检测路径是否有存在配置文件 + if self._check_redirect_path_exist(site_name, redirect_path, redirect_name): + return '重定向路径重复 %s' % redirect_path + + to_url_path = self._parse_url_path(to_url) + if to_url_path.startswith(redirect_path): + return '目标URL[%s]以被重定向的路径[%s]开头,会导致循环匹配' % (to_url_path, redirect_path) + # 404页面重定向检测项 + else: + if not to_url and not to_path: + return '首页或自定义页面必须二选一' + 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, '创建成功') + + 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, '修改成功') + + 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, "nginx配置文件读取失败" + + rep_include = re.compile(r"\sinclude +.*/redirect/.*\*\.conf;", re.M) + if rep_include.search(ng_conf): + return True, "" + redirect_include = ( + "#SSL-END\n" + " #引用重定向规则,注释后配置的重定向代理将无效\n" + " include {}/*.conf;" + ).format(ng_redirect_dir) + + if "#SSL-END" not in ng_conf: + return False, "添加配置失败,无法定位SSL相关配置的位置" + + 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, "添加配置失败" + + 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, "nginx配置文件读取失败" + + 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, "移除配置失败" + + 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, "apache配置文件读取失败" + + 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("") == 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 # 引用重定向规则,注释后配置的重定向代理将无效\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, "设置失败" + + 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, "apache配置文件读取失败" + + 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, "移除配置失败" + + 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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + 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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + + 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.*)error_page +404 +/404\.html[^\n]*\n', re.M) + rep_location_404 = re.compile(r'(?P.*)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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + + 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 + + RewriteEngine on + RewriteCond %{{REQUEST_FILENAME}} !-f + RewriteCond %{{REQUEST_FILENAME}} !-d + RewriteRule . {} [L,R={}] + +#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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + + def remove_redirect(self, get, multiple=None): + try: + site_name = get.sitename.strip() + redirect_name = get.redirectname.strip() + except AttributeError: + return public.returnMsg(False, "参数错误") + target_idx = None + have_other_redirect = False + target_conf = None + for i, conf in enumerate(self.config): + 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 not target_idx: + return public.returnMsg(False, '没有指定的配置') + + 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, '删除成功') + + 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, "参数错误") + 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.serviceReload() + return { + 'status': True, + 'msg': '删除重定向 [ {} ] 成功'.format(','.join(del_successfully)), + 'error': del_failed, + 'success': del_successfully + } + + 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 (AttributeError, ValueError, TypeError): + 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.return_message(0,0,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): + # 匹配目标URL的域名并返回 + + 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) + + 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) + + def modify_project_redirect(self, get): + if not isinstance(self.config_prefix, str): + return public.returnMsg(False, "Unsupported website type") + + return _RealRedirect(self.config_prefix).modify_redirect(get) + + 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) + + 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) + + 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) + + + diff --git a/class_v2/projectModelV2/dockerModel.py b/class_v2/projectModelV2/dockerModel.py new file mode 100644 index 00000000..6a79438b --- /dev/null +++ b/class_v2/projectModelV2/dockerModel.py @@ -0,0 +1,58 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# aaPanel +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +# ------------------------------------------------------------------- +# Author: zouhw +# ------------------------------------------------------------------- + +# ------------------------------ +# 项目管理控制器 +# ------------------------------ +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 diff --git a/class_v2/projectModelV2/nodejsModel.py b/class_v2/projectModelV2/nodejsModel.py new file mode 100644 index 00000000..832e9517 --- /dev/null +++ b/class_v2/projectModelV2/nodejsModel.py @@ -0,0 +1,2240 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# node.js模型 +#------------------------------ +import os,sys,re,json,shutil,psutil,time +from projectModelV2.base import projectBase +import public +from public.validate import Param +try: + from BTPanel import cache +except: + pass + +class main(projectBase): + _panel_path = public.get_panel_path() + _nodejs_plugin_path = public.get_plugin_path('nodejs') + _nodejs_path = '{}/nodejs'.format(public.get_setup_path()) + _log_name = 'Project management' + _npm_exec_log = '{}/logs/npm-exec.log'.format(_panel_path) + _node_pid_path = '{}/vhost/pids'.format(_nodejs_path) + _node_logs_path = '{}/vhost/logs'.format(_nodejs_path) + _node_run_scripts = '{}/vhost/scripts'.format(_nodejs_path) + _pids = None + _vhost_path = '{}/vhost'.format(_panel_path) + _www_home = '/home/www' + + + + def __init__(self): + if not os.path.exists(self._node_run_scripts): + os.makedirs(self._node_run_scripts,493) + + if not os.path.exists(self._node_pid_path): + os.makedirs(self._node_pid_path,493) + + if not os.path.exists(self._node_logs_path): + os.makedirs(self._node_logs_path,493) + + if not os.path.exists(self._www_home): + os.makedirs(self._www_home,493) + public.set_own(self._www_home,'www') + + + def get_exec_logs(self,get): + ''' + @name 获取执行日志 + @author hwliang<2021-08-09> + @param get + @return string + ''' + if not os.path.exists(self._npm_exec_log): return public.returnMsg(False,'NODE_NOT_EXISTS') + return public.return_message(0,0,public.GetNumLines(self._npm_exec_log,20)) + + + def get_project_list(self,get): + ''' + @name 获取项目列表 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('search').String(), + Param('limit').Integer(), + Param('p').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + if not 'p' in get: get.p = 1 + if not 'limit' in get: get.limit = 20 + if not 'callback' in get: get.callback = '' + if not 'order' in get: get.order = 'id desc' + + if 'search' in get: + get.project_name = get.search.strip() + search = "%{}%".format(get.project_name) + count = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)',('Node',search,search)).count() + data = public.get_page(count,int(get.p),int(get.limit),get.callback) + data['data'] = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)',('Node',search,search)).limit(data['shift'] + ',' + data['row']).order(get.order).select() + else: + count = public.M('sites').where('project_type=?','Node').count() + data = public.get_page(count,int(get.p),int(get.limit),get.callback) + data['data'] = public.M('sites').where('project_type=?','Node').limit(data['shift'] + ',' + data['row']).order(get.order).select() + + for i in range(len(data['data'])): + data['data'][i] = self.get_project_stat(data['data'][i]) + return public.return_message(0,0,data) + + + def get_ssl_end_date(self,project_name): + ''' + @name 获取SSL信息 + @author hwliang<2021-08-09> + @param project_name 项目名称 + @return dict + ''' + import data + return data.data().get_site_ssl_info('node_{}'.format(project_name)) + + + + def is_install_nodejs(self,get): + ''' + @name 是否安装nodejs版本管理器 + @author hwliang<2021-08-09> + @param get 请求数据 + @return bool + ''' + return_message=os.path.exists(self._nodejs_plugin_path) + return public.return_message(0,0,return_message) + + + def get_nodejs_version(self,get): + ''' + @name 获取已安装的nodejs版本 + @author hwliang<2021-08-09> + @param get 请求数据 + @return list + ''' + nodejs_list = [] + if not os.path.exists(self._nodejs_path): return public.return_message(0,0,nodejs_list) + for v in os.listdir(self._nodejs_path): + if v[0] != 'v' or v.find('.') == -1: continue + node_path = os.path.join(self._nodejs_path,v) + node_bin = '{}/bin/node'.format(node_path) + if not os.path.exists(node_bin): + if os.path.exists(node_path + '/bin'): + public.ExecShell('rm -rf {}'.format(node_path)) + continue + nodejs_list.append(v) + return public.return_message(0,0,nodejs_list) + + + + def get_run_list(self,get): + ''' + @name 获取node项目启动列表 + @author hwliang<2021-08-10> + @param get{ + project_cwd: string<项目目录> + } + '''# 校验参数 + try: + get.validate([ + Param('project_cwd').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_cwd = get.project_cwd.strip() + if not os.path.exists(project_cwd): + return_message=public.return_error('The project directory does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + package_file = '{}/package.json'.format(project_cwd) + if not os.path.exists(package_file): return public.return_message(0,0,{}) #public.return_error('没有在项目目录中找到package.json配置文件!') + package_info = json.loads(public.readFile(package_file)) + if not 'scripts' in package_info: return public.return_message(0,0,{})# public.return_error('没有在项目配置文件package.json中找到scripts配置项!') + if not package_info['scripts']: return public.return_message(0,0,{})# public.return_error('没有找到可用的启动选项!') + return public.return_message(0,0,package_info['scripts']) + + + def get_npm_bin(self,nodejs_version): + ''' + @name 获取指定node版本的npm路径 + @author hwliang<2021-08-10> + @param nodejs_version nodejs版本 + @return string + ''' + npm_path = '{}/{}/bin/npm'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(npm_path): return False + return npm_path + + def get_yarn_bin(self,nodejs_version): + ''' + @name 获取指定node版本的yarn路径 + @author hwliang<2021-08-28> + @param nodejs_version nodejs版本 + @return string + ''' + yarn_path = '{}/{}/bin/yarn'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(yarn_path): return False + return yarn_path + + + def get_node_bin(self,nodejs_version): + ''' + @name 获取指定node版本的node路径 + @author hwliang<2021-08-10> + @param nodejs_version nodejs版本 + @return string + ''' + node_path = '{}/{}/bin/node'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(node_path): return False + return node_path + + + def get_last_env(self,nodejs_version,project_cwd = None): + ''' + @name 获取前置环境变量 + @author hwliang<2021-08-25> + @param nodejs_version Node版本 + @return string + ''' + nodejs_bin_path = '{}/{}/bin'.format(self._nodejs_path,nodejs_version) + if project_cwd: + _bin = '{}/node_modules/.bin'.format(project_cwd) + if os.path.exists(_bin): + nodejs_bin_path = _bin + ':' + nodejs_bin_path + + last_env = '''PATH={nodejs_bin_path}:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH +'''.format(nodejs_bin_path = nodejs_bin_path) + return last_env + + + def install_packages(self,get): + ''' + @name 安装指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not os.path.exists(project_find['path']): + return_message=public.return_error('The project directory does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): + return_message=public.return_error('The package.json configuration file was not found in the project directory!') + del return_message['status'] + return public.return_message(-1,0, return_message) + nodejs_version = project_find['project_config']['nodejs_version'] + + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + node_modules_path = '{}/node_modules'.format(project_find['path']) + + # 已经安装过的依赖包的情况下,可能存在不同node版本导致的问题,可能需要重新构建依赖包 + rebuild = False + if os.path.exists(package_lock_file) and os.path.exists(node_modules_path): + rebuild = True + + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + if not npm_bin and not yarn_bin: + return_message=public.return_error('The specified nodejs version does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.writeFile(self._npm_exec_log,"Installing dependencies...\n") + public.writeFile(self._npm_exec_log,"Downloading dependency package, please wait...\n") + if yarn_bin: + if os.path.exists(package_lock_file): + os.remove(package_lock_file) + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install 2>&1 >> {}".format(project_find['path'],yarn_bin,self._npm_exec_log)) + else: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install 2>&1 >> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + public.writeFile(self._npm_exec_log,"|-Successify --- Command executed! ---",'a+') + public.WriteLog(self._log_name, 'Node project: {}, the installation of the dependency package is complete!'.format(project_find['name'])) + if rebuild: # 重新构建已安装模块? + self.rebuild_project(get.project_name) + return_message=public.return_data(True,'The dependency package is installed successfully!') + del return_message['status'] + return public.return_message(0,0, return_message) + + + + def update_packages(self,get): + ''' + @name 更新指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not os.path.exists(project_find['path']): + return_message=public.return_error('The project directory does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): + return_message=public.return_error('The package.json configuration file was not found in the project directory!') + del return_message['status'] + return public.return_message(-1,0, return_message) + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + if not os.path.exists(package_lock_file): + return_message=public.return_error('Please install the dependency package first!') + del return_message['status'] + return public.return_message(-1,0, return_message) + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + if not npm_bin: + return_message=public.return_error('The specified nodejs version does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} update &> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + public.WriteLog(self._log_name, 'Project [{}] update all dependent packages'.format(get.project_name)) + return_message=public.return_data(True,'Dependent package updated successfully!') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def reinstall_packages(self,get): + ''' + @name 重新安装指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not os.path.exists(project_find['path']): + return_message=public.return_error('The project directory does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): + return_message=public.return_error('The package.json configuration file was not found in the project directory!') + del return_message['status'] + return public.return_message(-1,0, return_message) + + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + if os.path.exists(package_lock_file): os.remove(package_lock_file) + package_path = '{}/node_modules' + if os.path.exists(package_path): shutil.rmtree(package_path) + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + if not npm_bin: + return_message=public.return_error('The specified nodejs version does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.WriteLog(self._log_name,'Node project: {}, all dependent packages have been reinstalled') + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install &> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + return_message=public.return_data(True,'Dependent package reinstalled successfully!') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def get_project_modules(self,get): + ''' + @name 获取指定项目的依赖包列表 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> 可选 + } + return list + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + Param('project_cwd').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not 'project_cwd' in get: + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_cwd = project_find['path'] + else: + project_cwd = get.project_cwd + mod_path = os.path.join(project_cwd,'node_modules') + modules = [] + if not os.path.exists(mod_path): return public.return_message(0,0,modules) + for mod_name in os.listdir(mod_path): + try: + mod_pack_file = os.path.join(mod_path,mod_name,'package.json') + if not os.path.exists(mod_pack_file): continue + mod_pack_info = json.loads(public.readFile(mod_pack_file)) + pack_info = { + "name": mod_name, + "version": mod_pack_info['version'], + "description":mod_pack_info['description'], + "license": mod_pack_info['license'] if 'license' in mod_pack_info else 'NULL', + "homepage": mod_pack_info['homepage'] + } + modules.append(pack_info) + except: + continue + return public.return_message(0,0,modules) + + def install_module(self,get): + ''' + @name 安装指定模块 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_cwd = project_find['path'] + + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if os.path.exists(filename): + return_message=public.return_error('The specified module has been installed!') + del return_message['status'] + return public.return_message(-1,0, return_message) + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + + if not npm_bin and not yarn_bin: + return_message=public.return_error('The specified nodejs version does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + if yarn_bin: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} add {} &> {}".format(project_find['path'],yarn_bin,mod_name,self._npm_exec_log)) + else: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install {} &> {}".format(project_find['path'],npm_bin,mod_name,self._npm_exec_log)) + if not os.path.exists(filename): + return_message=public.return_error('Failed to install the specified module!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.WriteLog(self._log_name,'Node project {}, {} module installation is complete!'.format(get.project_name,mod_name)) + return_message=public.return_data(True,'Successful installation!') + del return_message['status'] + return public.return_message(0,0, return_message) + + def uninstall_module(self,get): + ''' + @name 卸载指定模块 + @author hwliang<2021-04-08> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + Param('mod_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_cwd = project_find['path'] + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if not os.path.exists(filename): + return_message=public.return_error('The specified module is not installed!') + del return_message['status'] + return public.return_message(-1,0, return_message) + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + if not npm_bin and not yarn_bin: + return_message=public.return_error('The specified nodejs version does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + if yarn_bin: + result = public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} remove {}".format(project_find['path'],yarn_bin,mod_name)) + else: + result = public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} uninstall {}".format(project_find['path'],npm_bin,mod_name)) + if os.path.exists(filename): + result = "\n".join(result) + if result.find('looking for funding') != -1: + return_message=public.return_error("This module is dependent on other installed modules and cannot be uninstalled!") + del return_message['status'] + return public.return_message(-1,0, return_message) + return_message=public.return_error("Unable to uninstall this module!") + del return_message['status'] + return public.return_message(-1,0, return_message) + + public.WriteLog(self._log_name,'Node project {}, {} module uninstallation completed!'.format(get.project_name,mod_name)) + return_message=public.return_data(True,'Module unloaded successfully!') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def upgrade_module(self,get): + ''' + @name 更新指定模块 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + Param('mod_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_cwd = project_find['path'] + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if not os.path.exists(filename): + return_message=public.return_error('The specified module is not installed!') + del return_message['status'] + return public.return_message(-1,0, return_message) + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + + if not npm_bin: + return_message=public.return_error('The specified nodejs version does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} update {} &> {}".format(project_find['path'],npm_bin,mod_name,self._npm_exec_log)) + public.WriteLog(self._log_name,'Node project {}, {} module update completed!'.format(get.project_name,mod_name)) + return_message=public.return_data(True,'Module updated successfully!') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def create_project(self,get): + ''' + @name 创建新的项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> + project_script: string<项目脚本> + project_ps: string<项目备注信息> + bind_extranet: int<是否绑定外网> 1:是 0:否 + domains: list<域名列表> ["domain1:80","domain2:80"] // 在bind_extranet=1时,需要填写 + is_power_on: int<是否开机启动> 1:是 0:否 + run_user: string<运行用户> + max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 + nodejs_version: string + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_cwd').String(), + Param('project_name').String(), + Param('project_script').String(), + Param('port').String(), + Param('run_user').String(), + Param('nodejs_version').String(), + Param('project_ps').String(), + Param('domains').List(), + Param('project_env').String(), + Param('bind_extranet').Integer(), + Param('is_power_on').Integer(), + Param('max_memory_limit').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not isinstance(get,public.dict_obj): + return_message=public.return_error('The parameter type is wrong, need dict obj object') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not self.is_install_nodejs(get): + return_message=public.return_error('Please install nodejs version manager first') + del return_message['status'] + return public.return_message(-1,0, return_message) + + project_name = get.project_name.strip() + if not re.match(r"^\w+$",project_name): + return_message=public.return_error('The project name format is incorrect and supports letters, numbers, underscores, and expressions: ^[0-9A-Za-z_]$') + del return_message['status'] + return public.return_message(-1,0, return_message) + + if public.M('sites').where('name=?',(get.project_name,)).count(): + return_message=public.return_error('The specified project name already exists: {}'.format(get.project_name)) + del return_message['status'] + return public.return_message(-1,0, return_message) + get.project_cwd = get.project_cwd.strip() + if not os.path.exists(get.project_cwd): + return_message=public.return_error('The project directory does not exist: {}'.format(get.project_cwd)) + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 端口占用检测 + if self.check_port_is_used(get.get('port/port')): + return_message=public.return_error('This port is already occupied, please modify your project port, port: {}'.format(get.port)) + del return_message['status'] + return public.return_message(-1,0, return_message) + + domains = [] + if get.bind_extranet == 1: + domains = get.domains + if not public.is_apache_nginx(): + return_message=public.return_error('Please install Nginx or Apache first') + del return_message['status'] + return public.return_message(-1,0, return_message) + for domain in domains: + domain_arr = domain.split(':') + if public.M('domain').where('name=?',domain_arr[0]).count(): + return_message=public.return_error('Domain name already exists: {}'.format(domain)) + del return_message['status'] + return public.return_message(-1,0, return_message) + pdata = { + 'name': get.project_name, + 'path': get.project_cwd, + 'ps': get.project_ps, + 'status':1, + 'type_id':0, + 'project_type': 'Node', + 'project_config': json.dumps( + { + 'project_name': get.project_name, + 'project_cwd': get.project_cwd, + 'project_script': get.project_script, + 'bind_extranet': get.bind_extranet, + 'domains': [], + 'is_power_on': get.is_power_on, + 'run_user': get.run_user, + 'max_memory_limit': get.max_memory_limit, + 'nodejs_version': get.nodejs_version, + 'port': int(get.port) + } + ), + 'addtime': public.getDate() + } + + project_id = public.M('sites').insert(pdata) + if get.bind_extranet == 1: + format_domains = [] + for domain in domains: + if domain.find(':') == -1: domain += ':80' + format_domains.append(domain) + get.domains = format_domains + self.project_add_domain(get) + self.set_config(get.project_name) + public.WriteLog(self._log_name,'Add Node.js project {}'.format(get.project_name)) + self.install_packages(get) + self.start_project(get) + return_message=public.return_data(True,'Added project successfully',project_id) + del return_message['status'] + return public.return_message(0,0, return_message) + + def modify_project(self,get): + ''' + @name 修改指定项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> + project_script: string<项目脚本> + project_ps: string<项目备注信息> + is_power_on: int<是否开机启动> 1:是 0:否 + run_user: string<运行用户> + max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 + nodejs_version: string + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_cwd').String(), + Param('project_name').String(), + Param('project_script').String(), + Param('port').String(), + Param('run_user').String(), + Param('nodejs_version').String(), + Param('project_ps').String(), + Param('domains').String(), + Param('bind_extranet').Integer(), + Param('is_power_on').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not isinstance(get,public.dict_obj): + return_message=public.return_error('The parameter type is wrong, need dict obj') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not self.is_install_nodejs(get): + return_message=public.return_error('Please install nodejs version manager before installing at least one nodejs') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('Item does not exist: {}'.format(get.project_name)) + del return_message['status'] + return public.return_message(-1,0, return_message) + + if not os.path.exists(get.project_cwd): + return_message=public.return_error('The project directory does not exist: {}'.format(get.project_cwd)) + del return_message['status'] + return public.return_message(-1,0, return_message) + rebuild = False + if hasattr(get,'port'): + if int(project_find['project_config']['port']) != int(get.port): + if self.check_port_is_used(get.get('port/port'),True): + return_message=public.return_error('The port is already occupied, please modify your port, port: {}'.format(get.port)) + del return_message['status'] + return public.return_message(-1,0, return_message) + project_find['project_config']['port'] = int(get.port) + if hasattr(get,'project_cwd'): project_find['project_config']['project_cwd'] = get.project_cwd + if hasattr(get,'project_script'): + if not get.project_script.strip(): + return_message=public.return_error('Start command cannot be empty') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_find['project_config']['project_script'] = get.project_script.strip() + if hasattr(get,'is_power_on'): project_find['project_config']['is_power_on'] = get.is_power_on + if hasattr(get,'run_user'): project_find['project_config']['run_user'] = get.run_user + if hasattr(get,'max_memory_limit'): project_find['project_config']['max_memory_limit'] = get.max_memory_limit + if hasattr(get,'nodejs_version'): + if project_find['project_config']['nodejs_version'] != get.nodejs_version: + rebuild = True + project_find['project_config']['nodejs_version'] = get.nodejs_version + pdata = { + 'path': get.project_cwd, + 'ps': get.project_ps, + 'project_config': json.dumps(project_find['project_config']) + } + + public.M('sites').where('name=?',(get.project_name,)).update(pdata) + self.set_config(get.project_name) + public.WriteLog(self._log_name,'Modify Node.js project {}'.format(get.project_name)) + if rebuild: + self.rebuild_project(get.project_name) + return_message=public.return_data(True,'Modify the project successfully') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def rebuild_project(self,project_name): + ''' + @name 重新构建指定项目 + @author hwliang<2021-08-26> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} rebuild 2>&1 >> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + return True + + + def remove_project(self,get): + ''' + @name 删除指定项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist: {}'.format(get.project_name)) + del return_message['status'] + return public.return_message(-1,0, return_message) + + self.stop_project(get) + self.clear_config(get.project_name) + public.M('domain').where('pid=?',(project_find['id'],)).delete() + public.M('sites').where('name=?',(get.project_name,)).delete() + + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if os.path.exists(pid_file): os.remove(pid_file) + script_file = '{}/{}.sh'.format(self._node_run_scripts,get.project_name) + if os.path.exists(script_file): os.remove(script_file) + log_file = '{}/{}.log'.format(self._node_logs_path,get.project_name) + if os.path.exists(log_file): os.remove(log_file) + public.WriteLog(self._log_name,'Delete Node.js project {}'.format(get.project_name)) + return_message=public.return_data(True,'Successfully deleted item') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def project_get_domain(self,get): + ''' + @name 获取指定项目的域名列表 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_id = public.M('sites').where('name=?',(get.project_name,)).getField('id') + domains = public.M('domain').where('pid=?',(project_id,)).order('id desc').select() + project_find = self.get_project_find(get.project_name) + if len(domains) != len(project_find['project_config']['domains']): + public.M('domain').where('pid=?',(project_id,)).delete() + if not project_find: return public.return_message(0,0,[]) + for d in project_find['project_config']['domains']: + domain = {} + arr = d.split(':') + if len(arr) < 2: arr.append(80) + domain['name'] = arr[0] + domain['port'] = int(arr[1]) + domain['pid'] = project_id + domain['addtime'] = public.getDate() + public.M('domain').insert(domain) + if project_find['project_config']['domains']: + domains = public.M('domain').where('pid=?',(project_id,)).select() + return public.return_message(0,0,domains) + + + def project_add_domain(self,get): + ''' + @name 为指定项目添加域名 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + domains: list<域名列表> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + Param('domains').List(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist',data='') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_id = project_find['id'] + + domains = get.domains + success_list = [] + error_list = [] + for domain in domains: + domain = domain.strip() + if not domain: + return_message=public.return_error('Domain name cannot be empty',data='') + del return_message['status'] + return public.return_message(-1,0, return_message) + domain_arr = domain.split(':') + if len(domain_arr) == 1: + domain_arr.append(80) + domain += ':80' + if not public.M('domain').where('name=?',(domain_arr[0],)).count(): + public.M('domain').add('name,pid,port,addtime',(domain_arr[0],project_id,domain_arr[1],public.getDate())) + if not domain in project_find['project_config']['domains']: + project_find['project_config']['domains'].append(domain) + public.WriteLog(self._log_name,'Successfully added the domain [{}] to the project [{}]'.format(domain,get.project_name)) + success_list.append(domain) + else: + public.WriteLog(self._log_name,'Domain [{}] already exists'.format(domain)) + error_list.append(domain) + + if success_list: + public.M('sites').where('id=?',(project_id,)).save('project_config',json.dumps(project_find['project_config'])) + self.set_config(get.project_name) + return_message=public.return_data(True,"[{}] domain names added successfully, [{}] failed!".format(len(success_list),len(error_list)),error_msg=error_list) + del return_message['status'] + return public.return_message(0,0, return_message) + return_message=public.return_data(False,"[{}] domain names added successfully, [{}] failed!".format(len(success_list),len(error_list)),error_msg=error_list) + del return_message['status'] + return public.return_message(-1,0, return_message) + + + def project_remove_domain(self,get): + ''' + @name 为指定项目删除域名 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + domain: string<域名> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + Param('domain').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('The specified item does not exist') + del return_message['status'] + return public.return_message(-1,0, return_message) + last_domain = get.domain + domain_arr = get.domain.split(':') + if len(domain_arr) == 1: + domain_arr.append(80) + + project_id = public.M('sites').where('name=?',(get.project_name,)).getField('id') + if project_find['project_config']['bind_extranet']: + if len(project_find['project_config']['domains']) == 1: + return_message=public.return_error('At least one domain name is required for the mapped project') + del return_message['status'] + return public.return_message(-1,0, return_message) + domain_id = public.M('domain').where('name=? AND pid=?',(domain_arr[0],project_id)).getField('id') + if not domain_id: + return_message=public.return_error('The specified domain name does not exist') + del return_message['status'] + return public.return_message(-1,0, return_message) + public.M('domain').where('id=?',(domain_id,)).delete() + + if get.domain in project_find['project_config']['domains']: + project_find['project_config']['domains'].remove(get.domain) + if get.domain+":80" in project_find['project_config']['domains']: + project_find['project_config']['domains'].remove(get.domain + ":80") + + public.M('sites').where('id=?',(project_id,)).save('project_config',json.dumps(project_find['project_config'])) + public.WriteLog(self._log_name,'From project: [{}], delete domain name [{}]'.format(get.project_name,get.domain)) + self.set_config(get.project_name) + return_message=public.return_data(True,'Domain name deleted successfully') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def bind_extranet(self,get): + ''' + @name 绑定外网 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + Param('domains').List(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not public.is_apache_nginx(): + return_message=public.return_error('Please install Nginx or Apache first') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_name = get.project_name.strip() + project_find = self.get_project_find(project_name) + if not project_find: + return_message=public.return_error('Item does not exist') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not project_find['project_config']['domains']: + return_message=public.return_error('Please add at least one domain name in the [Domain Management] option') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_find['project_config']['bind_extranet'] = 1 + public.M('sites').where("id=?",(project_find['id'],)).setField('project_config',json.dumps(project_find['project_config'])) + self.set_config(project_name) + public.WriteLog(self._log_name,'Node project{}, enable mapping'.format(project_name)) + return_message=public.return_data(True,'Enable the mapping successfully') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def set_config(self,project_name): + ''' + @name 设置项目配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + if not project_find['project_config']: return False + if not project_find['project_config']['bind_extranet']: return False + if not project_find['project_config']['domains']: return False + self.set_nginx_config(project_find) + self.set_apache_config(project_find) + public.serviceReload() + return True + + def clear_config(self,project_name): + ''' + @name 清除项目配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + self.clear_nginx_config(project_find) + self.clear_apache_config(project_find) + public.serviceReload() + return True + + def clear_apache_config(self,project_find): + ''' + @name 清除apache配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + config_file = "{}/apache/node_{}.conf".format(self._vhost_path,project_name) + if os.path.exists(config_file): + os.remove(config_file) + return True + + + def clear_nginx_config(self,project_find): + ''' + @name 清除nginx配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + config_file = "{}/nginx/node_{}.conf".format(self._vhost_path,project_name) + if os.path.exists(config_file): + os.remove(config_file) + rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path = self._panel_path,project_name = project_name) + if os.path.exists(rewrite_file): + os.remove(rewrite_file) + return True + + + def set_nginx_config(self,project_find): + ''' + @name 设置Nginx配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + ports = [] + domains = [] + + for d in project_find['project_config']['domains']: + domain_tmp = d.split(':') + if len(domain_tmp) == 1: domain_tmp.append(80) + if not int(domain_tmp[1]) in ports: + ports.append(int(domain_tmp[1])) + if not domain_tmp[0] in domains: + domains.append(domain_tmp[0]) + listen_ipv6 = public.listen_ipv6() + listen_ports = '' + for p in ports: + listen_ports += " listen {};\n".format(p) + if listen_ipv6: + listen_ports += " listen [::]:{};\n".format(p) + listen_ports = listen_ports.strip() + + is_ssl,is_force_ssl = self.exists_nginx_ssl(project_name) + ssl_config = '' + if is_ssl: + listen_ports += "\n listen 443 ssl http2;" + if listen_ipv6: listen_ports += "\n listen [::]:443 ssl http2;" + + ssl_config = '''ssl_certificate {vhost_path}/cert/{priject_name}/fullchain.pem; + ssl_certificate_key {vhost_path}/cert/{priject_name}/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000"; + error_page 497 https://$host$request_uri;'''.format(vhost_path = self._vhost_path,priject_name = project_name) + + if is_force_ssl: + ssl_config += ''' + #HTTP_TO_HTTPS_START + if ($server_port !~ 443){ + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END''' + + config_file = "{}/nginx/node_{}.conf".format(self._vhost_path,project_name) + template_file = "{}/template/nginx/node_http.conf".format(self._vhost_path) + + config_body = public.readFile(template_file) + config_body = config_body.format( + site_path = project_find['path'], + domains = ' '.join(domains), + project_name = project_name, + panel_path = self._panel_path, + log_path = public.get_logs_path(), + url = 'http://127.0.0.1:{}'.format(project_find['project_config']['port']), + host = '$host', + listen_ports = listen_ports, + ssl_config = ssl_config + ) + + # # 恢复旧的SSL配置 + # ssl_config = self.get_nginx_ssl_config(project_name) + # if ssl_config: + # config_body.replace('#error_page 404/404.html;',ssl_config) + + + rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path = self._panel_path,project_name = project_name) + if not os.path.exists(rewrite_file): public.writeFile(rewrite_file,'# Please fill in the URLrewrite or custom NGINX config here\n') + public.writeFile(config_file,config_body) + return True + + def get_nginx_ssl_config(self,project_name): + ''' + @name 获取项目Nginx SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return string + ''' + result = '' + config_file = "{}/nginx/node_{}".format(self._vhost_path,project_name) + if not os.path.exists(config_file): + return result + + config_body = public.readFile(config_file) + if not config_body: + return result + if config_body.find('ssl_certificate') == -1: + return result + + ssl_body = re.search("#SSL-START(.|\n)+#SSL-END",config_body) + if not ssl_body: return result + result = ssl_body.group() + return result + + def exists_nginx_ssl(self,project_name): + ''' + @name 判断项目是否配置Nginx SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return tuple + ''' + config_file = "{}/nginx/node_{}.conf".format(public.get_vhost_path(),project_name) + if not os.path.exists(config_file): + return False,False + + config_body = public.readFile(config_file) + if not config_body: + return False,False + + is_ssl,is_force_ssl = False,False + if config_body.find('ssl_certificate') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl,is_force_ssl + + def exists_apache_ssl(self,project_name): + ''' + @name 判断项目是否配置Apache SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + config_file = "{}/apache/node_{}.conf".format(public.get_vhost_path(),project_name) + if not os.path.exists(config_file): + return False,False + + config_body = public.readFile(config_file) + if not config_body: + return False,False + + is_ssl,is_force_ssl = False,False + if config_body.find('SSLCertificateFile') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl,is_force_ssl + + def set_apache_config(self,project_find): + ''' + @name 设置Apache配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + + # 处理域名和端口 + ports = [] + domains = [] + for d in project_find['project_config']['domains']: + domain_tmp = d.split(':') + if len(domain_tmp) == 1: domain_tmp.append(80) + if not int(domain_tmp[1]) in ports: + ports.append(int(domain_tmp[1])) + if not domain_tmp[0] in domains: + domains.append(domain_tmp[0]) + + + config_file = "{}/apache/node_{}.conf".format(self._vhost_path,project_name) + template_file = "{}/template/apache/node_http.conf".format(self._vhost_path) + config_body = public.readFile(template_file) + apache_config_body = '' + + # 旧的配置文件是否配置SSL + is_ssl,is_force_ssl = self.exists_apache_ssl(project_name) + if is_ssl: + if not 443 in ports: ports.append(443) + + from panelSite import panelSite + s = panelSite() + + # 根据端口列表生成配置 + for p in ports: + # 生成SSL配置 + ssl_config = '' + if p == 443 and is_ssl: + ssl_key_file = "{vhost_path}/cert/{project_name}/privkey.pem".format(project_name = project_name,vhost_path = public.get_vhost_path()) + if not os.path.exists(ssl_key_file): continue # 不存在证书文件则跳过 + ssl_config = '''#SSL + SSLEngine On + SSLCertificateFile {vhost_path}/cert/{project_name}/fullchain.pem + SSLCertificateKeyFile {vhost_path}/cert/{project_name}/privkey.pem + SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 + SSLProtocol All -SSLv2 -SSLv3 -TLSv1 + SSLHonorCipherOrder On'''.format(project_name = project_name,vhost_path = public.get_vhost_path()) + else: + if is_force_ssl: + ssl_config = '''#HTTP_TO_HTTPS_START + + RewriteEngine on + RewriteCond %{SERVER_PORT} !^443$ + RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] + + #HTTP_TO_HTTPS_END''' + + # 生成vhost主体配置 + apache_config_body += config_body.format( + site_path = project_find['path'], + server_name = '{}.{}'.format(p,project_name), + domains = ' '.join(domains), + log_path = public.get_logs_path(), + server_admin = 'admin@{}'.format(project_name), + url = 'http://127.0.0.1:{}'.format(project_find['project_config']['port']), + port = p, + ssl_config = ssl_config, + project_name = project_name + ) + apache_config_body += "\n" + + # 添加端口到主配置文件 + if not p in [80]: + s.apacheAddPort(p) + + # 写.htaccess + rewrite_file = "{}/.htaccess".format(project_find['path']) + if not os.path.exists(rewrite_file): public.writeFile(rewrite_file,'# Please fill in the URLrewrite rules or custom Apache config here\n') + + # 写配置文件 + public.writeFile(config_file,apache_config_body) + return True + + + def unbind_extranet(self,get): + ''' + @name 解绑外网 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + project_name = get.project_name.strip() + self.clear_config(project_name) + public.serviceReload() + project_find = self.get_project_find(project_name) + project_find['project_config']['bind_extranet'] = 0 + public.M('sites').where("id=?",(project_find['id'],)).setField('project_config',json.dumps(project_find['project_config'])) + public.WriteLog(self._log_name,'Node project {}, disable the mapping'.format(project_name)) + return_message=public.return_data(True,'Disabled successfully') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def get_project_pids(self,get = None,pid = None): + ''' + @name 获取项目进程pid列表 + @author hwliang<2021-08-10> + @param pid: string<项目pid> + @return list + ''' + if get: pid = int(get.pid) + if not self._pids: self._pids = psutil.pids() + project_pids = [] + + for i in self._pids: + try: + p = psutil.Process(i) + except: continue + if p.ppid() == pid: + if i in project_pids: continue + if p.name() in ['bash']: continue + project_pids.append(i) + + other_pids = [] + for i in project_pids: + other_pids += self.get_project_pids(pid=i) + if os.path.exists('/proc/{}'.format(pid)): + project_pids.append(pid) + + all_pids = list(set(project_pids + other_pids)) + if not all_pids: + all_pids = self.get_other_pids(pid) + return public.return_message(0,0,sorted(all_pids)) + + def get_other_pids(self,pid): + ''' + @name 获取其他进程pid列表 + @author hwliang<2021-08-10> + @param pid: string<项目pid> + @return list + ''' + project_name = None + for pid_name in os.listdir(self._node_pid_path): + pid_file = '{}/{}'.format(self._node_pid_path,pid_name) + #s_pid = int(public.readFile(pid_file)) + data = public.readFile(pid_file) + if isinstance(data,str) and data: + s_pid = int(data) + else: + return public.return_message(0,0,[]) + if pid == s_pid: + project_name = pid_name[:-4] + break + project_find = self.get_project_find(project_name) + if not project_find: return public.return_message(0,0,[]) + if not self._pids: self._pids = psutil.pids() + all_pids = [] + for i in self._pids: + try: + p = psutil.Process(i) + if p.cwd() == project_find['path']: + pname = p.name() + if pname in ['node','npm','pm2','yarn'] or pname.find('node ') == 0: + cmdline = ','.join(p.cmdline()) + if cmdline.find('God Daemon') != -1:continue + env_list = p.environ() + if 'name' in env_list: + if not env_list['name'] == project_name: continue + if 'NODE_PROJECT_NAME' in env_list: + if not env_list['NODE_PROJECT_NAME'] == project_name: continue + all_pids.append(i) + except: continue + return public.return_message(0,0,all_pids) + + def get_project_state_by_cwd(self,project_name): + ''' + @name 通过cwd获取项目状态 + @author hwliang<2022-01-17> + @param project_name 项目名称 + @return bool or list + ''' + project_find = self.get_project_find(project_name) + self._pids = psutil.pids() + if not project_find: return [] + all_pids = [] + for i in self._pids: + try: + p = psutil.Process(i) + if p.cwd() == project_find['path']: + pname = p.name() + if pname in ['node','npm','pm2','yarn'] or pname.find('node ') == 0: + cmdline = ','.join(p.cmdline()) + if cmdline.find('God Daemon') != -1:continue + env_list = p.environ() + if 'name' in env_list: + if not env_list['name'] == project_name: continue + if 'NODE_PROJECT_NAME' in env_list: + if not env_list['NODE_PROJECT_NAME'] == project_name: continue + all_pids.append(i) + except: continue + if all_pids: + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + public.writeFile(pid_file,str(all_pids[0])) + return all_pids + return False + + def kill_pids(self,get=None,pids = None): + ''' + @name 结束进程列表 + @author hwliang<2021-08-10> + @param pids: string<进程pid列表> + @return dict + ''' + if get: pids = get.pids + if not pids: + return_message=public.return_data(True, 'No process') + del return_message['status'] + return public.return_message(0,0, return_message) + pids = sorted(pids,reverse=True) + for i in pids: + try: + p = psutil.Process(i) + p.kill() + except: + pass + return_message=public.return_data(True, 'The process has all ended') + del return_message['status'] + return public.return_message(0,0, return_message) + + + + + def start_project(self,get): + ''' + @name 启动项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if os.path.exists(pid_file): + self.stop_project(get) + + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('Item does not exist') + del return_message['status'] + return public.return_message(-1,0, return_message) + + if not os.path.exists(project_find['path']): + error_msg = 'Startup failed, Nodejs project {}, running directory {} does not exist!'.format(get.project_name,project_find['path']) + public.WriteLog(self._log_name,error_msg) + return_message=public.return_error(error_msg) + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 是否安装依赖模块? + package_file = "{}/package.json".format(project_find['path']) + package_info = {} + if os.path.exists(package_file): + node_modules_path = "{}/node_modules".format(project_find['path']) + if not os.path.exists(node_modules_path): + return_message=public.return_error('Please go to the [Module] and click [One-key install] to install the module dependencies!') + del return_message['status'] + return public.return_message(-1,0, return_message) + package_info = json.loads(public.readFile(package_file)) + if not package_info: package_info['scripts'] = {} + if 'scripts' not in package_info: package_info['scripts'] = {} + try: + scripts_keys = package_info['scripts'].keys() + except: + scripts_keys = [] + + + # 前置准备 + nodejs_version = project_find['project_config']['nodejs_version'] + node_bin = self.get_node_bin(nodejs_version) + npm_bin = self.get_npm_bin(nodejs_version) + project_script = project_find['project_config']['project_script'].strip().replace(' ',' ') + if project_script[:3] == 'pm2': # PM2启动方式处理 + project_script = project_script.replace('pm2 ','pm2 -u {} -n {} '.format(project_find['project_config']['run_user'],get.project_name)) + project_find['project_config']['run_user'] = 'root' + log_file = "{}/{}.log".format(self._node_logs_path,get.project_name) + if not project_script: + return_message=public.return_error('No startup script configured') + del return_message['status'] + return public.return_message(-1,0, return_message) + + last_env = self.get_last_env(nodejs_version,project_find['path']) + + # 生成启动脚本 + if os.path.exists(project_script): + start_cmd = '''{last_env} +export NODE_PROJECT_NAME="{project_name}" +cd {project_cwd} +nohup {node_bin} {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + node_bin = node_bin, + project_script = project_script, + log_file = log_file, + pid_file = pid_file, + last_env = last_env, + project_name = get.project_name +) + elif project_script in scripts_keys: + start_cmd = '''{last_env} +export NODE_PROJECT_NAME="{project_name}" +cd {project_cwd} +nohup {npm_bin} run {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + npm_bin = npm_bin, + project_script = project_script, + pid_file = pid_file, + log_file = log_file, + last_env = last_env, + project_name = get.project_name +) + else: + start_cmd = '''{last_env} +export NODE_PROJECT_NAME="{project_name}" +cd {project_cwd} +nohup {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + project_script = project_script, + pid_file = pid_file, + log_file = log_file, + last_env = last_env, + project_name = get.project_name +) + script_file = "{}/{}.sh".format(self._node_run_scripts,get.project_name) + + # 写入启动脚本 + public.writeFile(script_file,start_cmd) + if os.path.exists(pid_file): os.remove(pid_file) + + # 处理前置权限 + public.ExecShell("chown -R {user}:{user} {project_cwd}".format(user=project_find['project_config']['run_user'],project_cwd=project_find['path'])) + public.ExecShell("chown -R www:www {}/vhost".format(self._nodejs_path)) + public.ExecShell("chmod 755 {} {} {}".format(self._nodejs_path,public.get_setup_path(),'/www')) + public.set_own(script_file,project_find['project_config']['run_user'],project_find['project_config']['run_user']) + public.set_mode(script_file,755) + + # 执行脚本文件 + p = public.ExecShell("bash {}".format(script_file),user=project_find['project_config']['run_user']) + time.sleep(1) + n = 0 + while n < 5: + if self.get_project_state_by_cwd(get.project_name): break + n+=1 + if not os.path.exists(pid_file): + p = '\n'.join(p) + public.writeFile(log_file,p,"a+") + if p.find('[Errno 0]') != -1: + if os.path.exists('{}/bt_security'.format(public.get_plugin_path())): + return_message=public.return_error('The start command was intercepted by [Fort Tower Defense Privilege], please turn off {} user protection'.format(project_find['project_config']['run_user'])) + del return_message['status'] + return public.return_message(-1,0, return_message) + return_message=public.return_error('The startup command was intercepted by unknown security software, please check the installation software log') + del return_message['status'] + return public.return_message(-1,0, return_message) + return_message=public.return_error('failed to activate
                                    {}
                                    '.format(p)) + del return_message['status'] + return public.return_message(-1,0, return_message) + + # 获取PID + try: + pid = int(public.readFile(pid_file)) + except: + return public.return_error('Startup failed
                                    {}'.format(public.GetNumLines(log_file,20))) + pids = self.get_project_pids(pid=pid) + if not pids: + if os.path.exists(pid_file): os.remove(pid_file) + return_message=public.return_error('failed to activate
                                    {}'.format(public.GetNumLines(log_file,20))) + del return_message['status'] + return public.return_message(-1,0, return_message) + return_message=public.return_data(True, 'Successfully started', pids) + del return_message['status'] + return public.return_message(0,0, return_message) + + + def stop_project(self,get): + ''' + @name 停止项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return_message=public.return_error('Project does not exist') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_script = project_find['project_config']['project_script'].strip().replace(' ',' ') + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if project_script.find('pm2 start') != -1: # 处理PM2启动的项目 + nodejs_version = project_find['project_config']['nodejs_version'] + last_env = self.get_last_env(nodejs_version,project_find['path']) + project_script = project_script.replace('pm2 start','pm2 stop') + public.ExecShell('''{} +cd {} +{}'''.format(last_env,project_find['path'],project_script)) + else: + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if not os.path.exists(pid_file): + return_message=public.return_error('Project did not start') + del return_message['status'] + return public.return_message(-1,0, return_message) + data = public.readFile(pid_file) + if isinstance(data,str) and data: + pid = int(data) + pids = self.get_project_pids(pid=pid) + else: + return_message=public.return_error('Project did not start') + del return_message['status'] + return public.return_message(-1,0, return_message) + if not pids: + return_message=public.return_error('Project did not start') + del return_message['status'] + return public.return_message(-1,0, return_message) + self.kill_pids(pids=pids) + if os.path.exists(pid_file): os.remove(pid_file) + time.sleep(0.5) + pids = self.get_project_state_by_cwd(get.project_name) + if pids: self.kill_pids(pids=pids) + return_message=public.return_data(True, 'Stopped successfully') + del return_message['status'] + return public.return_message(0,0, return_message) + + def restart_project(self,get): + ''' + @name 重启项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + res = self.stop_project(get) + if res['status']==-1: return res + res = self.start_project(get) + if res['status']==-1: return res + return_message=public.return_data(True, 'Successful restart') + del return_message['status'] + return public.return_message(0,0, return_message) + + # xss 防御 + def xsssec(self,text): + return text.replace('<', '<').replace('>', '>') + + + def get_project_log(self,get): + ''' + @name 获取项目日志 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + log_file = "{}/{}.log".format(self._node_logs_path,get.project_name) + if not os.path.exists(log_file): + return_message=public.return_error('Log file does not exist') + del return_message['status'] + return public.return_message(-1,0, return_message) + return public.return_message(0,0,self.xsssec(public.GetNumLines(log_file,200))) + + + def get_project_load_info(self,get = None,project_name = None): + ''' + @name 获取项目负载信息 + @author hwliang<2021-08-12> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + if get: project_name = get.project_name.strip() + load_info = {} + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return public.return_message(0,0,load_info) + data = public.readFile(pid_file) + if isinstance(data,str) and data: + pid = int(data) + pids = self.get_project_pids(pid=pid) + else: + return public.return_message(0,0,load_info) + if not pids: return public.return_message(0,0,load_info) + for i in pids: + process_info = self.get_process_info_by_pid(i) + if process_info: load_info[i] = process_info + return public.return_message(0,0,load_info) + + + def object_to_dict(self,obj): + ''' + @name 将对象转换为字典 + @author hwliang<2021-08-09> + @param obj + @return dict + ''' + result = {} + for name in dir(obj): + value = getattr(obj, name) + if not name.startswith('__') and not callable(value) and not name.startswith('_'): result[name] = value + return result + + + def list_to_dict(self,data): + ''' + @name 将列表转换为字典 + @author hwliang<2021-08-09> + @param data + @return dict + ''' + result = [] + for s in data: + result.append(self.object_to_dict(s)) + return result + + + def get_connects(self,pid): + ''' + @name 获取进程连接信息 + @author hwliang<2021-08-09> + @param pid + @return dict + ''' + connects = 0 + try: + if pid == 1: return connects + tp = '/proc/' + str(pid) + '/fd/' + if not os.path.exists(tp): return connects + for d in os.listdir(tp): + fname = tp + d + if os.path.islink(fname): + l = os.readlink(fname) + if l.find('socket:') != -1: connects += 1 + except:pass + return connects + + + def format_connections(self,connects): + ''' + @name 获取进程网络连接信息 + @author hwliang<2021-08-09> + @param connects + @return list + ''' + result = [] + for i in connects: + raddr = i.raddr + if not i.raddr: + raddr = ('',0) + laddr = i.laddr + if not i.laddr: + laddr = ('',0) + result.append({ + "fd": i.fd, + "family": i.family, + "local_addr": laddr[0], + "local_port": laddr[1], + "client_addr": raddr[0], + "client_rport": raddr[1], + "status": i.status + }) + return result + + + def get_process_info_by_pid(self,pid): + ''' + @name 获取进程信息 + @author hwliang<2021-08-12> + @param pid: int<进程id> + @return dict + ''' + process_info = {} + try: + if not os.path.exists('/proc/{}'.format(pid)): return process_info + p = psutil.Process(pid) + status_ps = {'sleeping':'Sleeping','running':'Running'} + with p.oneshot(): + p_mem = p.memory_full_info() + if p_mem.uss + p_mem.rss + p_mem.pss + p_mem.data == 0: return process_info + p_state = p.status() + if p_state in status_ps: p_state = status_ps[p_state] + # process_info['exe'] = p.exe() + process_info['name'] = p.name() + process_info['pid'] = pid + process_info['ppid'] = p.ppid() + process_info['create_time'] = int(p.create_time()) + process_info['status'] = p_state + process_info['user'] = p.username() + process_info['memory_used'] = p_mem.uss + process_info['cpu_percent'] = self.get_cpu_precent(p) + process_info['io_write_bytes'],process_info['io_read_bytes'] = self.get_io_speed(p) + process_info['connections'] = self.format_connections(p.connections()) + process_info['connects'] = self.get_connects(pid) + process_info['open_files'] = self.list_to_dict(p.open_files()) + process_info['threads'] = p.num_threads() + process_info['exe'] = ' '.join(p.cmdline()) + return process_info + except: + return process_info + + + def get_io_speed(self,p): + ''' + @name 获取磁盘IO速度 + @author hwliang<2021-08-12> + @param p: Process<进程对像> + @return list + ''' + + skey = "io_speed_{}".format(p.pid) + old_pio = cache.get(skey) + if not hasattr(p,'io_counters'): return 0,0 + pio = p.io_counters() + if not old_pio: + cache.set(skey,[pio,time.time()],3600) + # time.sleep(0.1) + old_pio = cache.get(skey) + pio = p.io_counters() + + old_write_bytes = old_pio[0].write_bytes + old_read_bytes = old_pio[0].read_bytes + old_time = old_pio[1] + + new_time = time.time() + write_bytes = pio.write_bytes + read_bytes = pio.read_bytes + + cache.set(skey,[pio,new_time],3600) + + write_speed = int((write_bytes - old_write_bytes) / (new_time - old_time)) + read_speed = int((read_bytes - old_read_bytes) / (new_time - old_time)) + + return write_speed,read_speed + + + + + + def get_cpu_precent(self,p): + ''' + @name 获取进程cpu使用率 + @author hwliang<2021-08-09> + @param p: Process<进程对像> + @return dict + ''' + skey = "cpu_pre_{}".format(p.pid) + old_cpu_times = cache.get(skey) + + process_cpu_time = self.get_process_cpu_time(p.cpu_times()) + if not old_cpu_times: + cache.set(skey,[process_cpu_time,time.time()],3600) + # time.sleep(0.1) + old_cpu_times = cache.get(skey) + process_cpu_time = self.get_process_cpu_time(p.cpu_times()) + + old_process_cpu_time = old_cpu_times[0] + old_time = old_cpu_times[1] + new_time = time.time() + cache.set(skey,[process_cpu_time,new_time],3600) + percent = round(100.00 * (process_cpu_time - old_process_cpu_time) / (new_time - old_time) / psutil.cpu_count(),2) + return percent + + + def get_process_cpu_time(self,cpu_times): + cpu_time = 0.00 + for s in cpu_times: cpu_time += s + return cpu_time + + + def get_project_run_state(self,get = None,project_name = None): + ''' + @name 获取项目运行状态 + @author hwliang<2021-08-12> + @param get{ + project_name: string<项目名称> + } + @param project_name 项目名称 + @return bool + ''' + if get: project_name = get.project_name.strip() + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return public.return_message(0,0,False) + data=public.readFile(pid_file) + if isinstance(data,str) and data: + pid = int(data) + pids = self.get_project_pids(pid=pid) + else: + return public.return_message(0,0,self.get_project_state_by_cwd(project_name)) + if not pids: return self.get_project_state_by_cwd(project_name) + return public.return_message(0,0,True) + + def get_project_find(self,project_name): + ''' + @name 获取指定项目配置 + @author hwliang<2021-08-09> + @param project_name 项目名称 + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',project_name)).find() + if not project_info: return False + project_info['project_config'] = json.loads(project_info['project_config']) + return project_info + + + def get_project_info(self,get): + ''' + @name 获取指定项目信息 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + # 校验参数 + try: + get.validate([ + Param('project_name').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + project_info = public.M('sites').where('project_type=? AND name=?',('Node',get.project_name)).find() + if not project_info: + return_message=public.return_error('The specified item does not exist!') + del return_message['status'] + return public.return_message(-1,0, return_message) + project_info = self.get_project_stat(project_info) + return public.return_message(0,0, project_info) + + + def get_project_stat(self,project_info): + ''' + @name 获取项目状态信息 + @author hwliang<2021-08-09> + @param project_info 项目信息 + @return list + ''' + project_info['project_config'] = json.loads(project_info['project_config']) + project_info['run'] = self.get_project_run_state(project_name = project_info['name']) + project_info['load_info'] = {} + if project_info['run']: + project_info['load_info'] = self.get_project_load_info(project_name = project_info['name'])['message'] + project_info['ssl'] = self.get_ssl_end_date(project_name = project_info['name']) + project_info['listen'] = [] + project_info['listen_ok'] = True + if project_info['load_info']: + for pid in project_info['load_info'].keys(): + if not pid:continue + if not 'connections' in project_info['load_info'][pid]: + project_info['load_info'][pid]['connections'] = [] + for conn in project_info['load_info'][pid]['connections']: + if not conn['status'] == 'LISTEN': continue + if not conn['local_port'] in project_info['listen']: + project_info['listen'].append(conn['local_port']) + if project_info['listen']: + project_info['listen_ok'] = project_info['project_config']['port'] in project_info['listen'] + return project_info + + + + def get_project_state(self,project_name): + ''' + @name 获取项目状态 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',project_name)).find() + if not project_info: return False + return project_info['status'] + + def get_project_listen(self,project_name): + ''' + @name 获取项目监听端口 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + project_config = json.loads(public.M('sites').where('name=?',project_name).getField('project_config')) + if 'listen_port' in project_config: return project_config['listen_port'] + return False + + + def set_project_listen(self,get): + ''' + @name 设置项目监听端口(请设置与实际端口相符的,仅在自动获取不正确时使用) + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + port: int<端口> + } + @return dict + ''' + project_config = json.loads(public.M('sites').where('name=?',get.project_name).getField('project_config')) + project_config['listen_port'] = get.port + public.M('sites').where('name=?',get.project_name).save('project_config',json.dumps(project_config)) + public.WriteLog(self._log_name, 'Modify the port of the project ['+get.project_name+'] to ['+get.port+']') + return_message=public.return_data(True,'Set successfully') + del return_message['status'] + return public.return_message(0,0, return_message) + + + def set_project_nodejs_version(self,get): + ''' + @name 设置nodejs版本 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + nodejs_version: string + } + @return dict + ''' + + project_config = json.loads(public.M('sites').where('name=?',get.project_name).getField('project_config')) + project_config['nodejs_version'] = get.nodejs_version + public.M('sites').where('name=?',get.project_name).save('project_config',json.dumps(project_config)) + public.WriteLog(self._log_name, 'Modify the nodejs version of the project ['+get.project_name+'] to ['+get.nodejs_version+']') + return_message=public.return_data(True,'Set successfully') + del return_message['status'] + return public.return_message(0,0, return_message) + + def get_project_nodejs_version(self,project_name): + ''' + @name 获取nodejs版本 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return string + ''' + + project_config = json.loads(public.M('sites').where('name=?',project_name).getField('project_config')) + if 'nodejs_version' in project_config: return project_config['nodejs_version'] + return False + + + def check_port_is_used(self,port,sock=False): + ''' + @name 检查端口是否被占用 + @author hwliang<2021-08-09> + @param port: int<端口> + @return bool + ''' + if not isinstance(port,int): port = int(port) + if port == 0: return False + project_list = public.M('sites').where('status=? AND project_type=?',(1,'Node')).field('name,path,project_config').select() + for project_find in project_list: + project_config = json.loads(project_find['project_config']) + if not 'port' in project_config: continue + if int(project_config['port']) == port: return True + if sock: return False + return public.check_tcp('127.0.0.1',port) + + def get_project_run_state_byaotu(self,project_name): + ''' + @name 获取项目运行状态 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return False + pid = public.readFile(pid_file) + pids = self.get_project_pids(pid=pid) + if not pids: return False + return True + + def auto_run(self): + ''' + @name 自动启动所有项目 + @author hwliang<2021-08-09> + @return bool + ''' + project_list = public.M('sites').where('project_type=?',('Node',)).field('name,path,project_config').select() + get= public.dict_obj() + success_count = 0 + error_count = 0 + for project_find in project_list: + try: + project_config = json.loads(project_find['project_config']) + if project_config['is_power_on'] in [0,False,'0',None]: continue + project_name = project_find['name'] + project_state = self.get_project_run_state(project_name=project_name) + if not project_state: + get.project_name = project_name + result = self.start_project(get)['message'] + if result['status']==-1: + error_count += 1 + error_msg = 'Automatically start Nodej project ['+project_name+'] failed!' + public.WriteLog(self._log_name, error_msg) + public.print_log(error_msg + ", " + result['error_msg'],'ERROR') + else: + success_count += 1 + success_msg = 'Automatically start the Nodej project ['+project_name+'] successfully!' + public.WriteLog(self._log_name, success_msg) + public.print_log(success_msg,'INFO') + except: + error_count += 1 + public.print_log(public.get_error_info(),'ERROR') + if (success_count + error_count) < 1: return False + dene_msg = 'A total of {} Nodejs projects need to be started, {} successfully and {} failed'.format(success_count + error_count,success_count,error_count) + public.WriteLog(self._log_name, dene_msg) + public.print_log(dene_msg,'INFO') + return True diff --git a/class_v2/projectModelV2/quotaModel.py b/class_v2/projectModelV2/quotaModel.py new file mode 100644 index 00000000..a8d14116 --- /dev/null +++ b/class_v2/projectModelV2/quotaModel.py @@ -0,0 +1,695 @@ +4wZIF38E5nxrLYkW2uUR7d3iaGQLSTjR4DuJynwT7k0= +XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q= +NF4oGUk70PXEJoCfkU1k+A== +XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q= +n+0ptngHIPIjFuMNQ53bftpaK0KYKjbY/JzxOkjVDFkqjO9WVvBZllMi2G3YerYfc6HYWOzoeF74xGLl1Znalv8dm0hrQlyXIHlBtsUjirU= +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== +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== +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 diff --git a/class_v2/safeModelV2/base.py b/class_v2/safeModelV2/base.py new file mode 100644 index 00000000..d9966666 --- /dev/null +++ b/class_v2/safeModelV2/base.py @@ -0,0 +1,92 @@ +#coding: utf-8 +import public,re,time,sys,os +from datetime import datetime + + +class safeBase: + + __isUfw = False + __isFirewalld = False + _months = {'Jan':'01','Feb':'02','Mar':'03','Apr':'04','May':'05','Jun':'06','Jul':'07','Aug':'08','Sep':'09','Sept':'09','Oct':'10','Nov':'11','Dec':'12'} + + def __init__(self): + if os.path.exists('/usr/sbin/firewalld'): self.__isFirewalld = True + if os.path.exists('/usr/sbin/ufw'): self.__isUfw = True + + #转换时间格式 + def to_date(self,date_str): + tmp = re.split(r'\s+',date_str) + if len(tmp) < 3: return date_str + s_date = str(datetime.now().year) + '-' + self._months.get(tmp[0]) + '-' + tmp[1] + ' ' + tmp[2] + time_array = time.strptime(s_date, "%Y-%m-%d %H:%M:%S") + time_stamp = int(time.mktime(time_array)) + return time_stamp + + + def to_date2(self,date_str): + tmp = date_str.split() + if len(tmp) < 4: return date_str + s_date = str(tmp[-1]) + '-' + self._months.get(tmp[1],tmp[1]) + '-' + tmp[2] + ' ' + tmp[3] + return s_date + + def to_date3(self,date_str): + tmp = date_str.split() + if len(tmp) < 4: return date_str + s_date = str(datetime.now().year) + '-' + self._months.get(tmp[1],tmp[1]) + '-' + tmp[2] + ' ' + tmp[3] + return s_date + + def to_date4(self,date_str): + tmp = date_str.split() + if len(tmp) < 3: return date_str + s_date = str(datetime.now().year) + '-' + self._months.get(tmp[0],tmp[0]) + '-' + tmp[1] + ' ' + tmp[2] + return s_date + + + #取防火墙状态 + def CheckFirewallStatus(self): + if self.__isUfw: + res = public.ExecShell('ufw status verbose')[0] + if res.find('inactive') != -1: return False + return True + + if self.__isFirewalld: + res = public.ExecShell("systemctl status firewalld")[0] + if res.find('active (running)') != -1: return True + if res.find('disabled') != -1: return False + if res.find('inactive (dead)') != -1: return False + else: + res = public.ExecShell("/etc/init.d/iptables status")[0] + if res.find('not running') != -1: return False + return True + return False + + + def get_ssh_log_files(self,get): + """ + 获取ssh日志文件 + """ + s_key = 'secure' + if not os.path.exists('/var/log/secure'): + s_key = 'auth.log' + if os.path.exists('/var/log/secure') and os.path.getsize('/var/log/secure') == 0: + s_key = 'auth.log' + + res = [] + spath = '/var/log/' + for fname in os.listdir(spath): + fpath = '{}{}'.format(spath,fname) + if fname.find(s_key) == -1 or fname == s_key: + continue + + #debian解压日志 + if fname[-3:] in ['.gz','.xz']: + if os.path.exists(fpath[:-3]): + continue + public.ExecShell("gunzip -c " + fpath + " > " + fpath[:-3]) + res.append(fpath[:-3]) + else: + res.append(fpath) + + res = sorted(res,reverse=True) + res.insert(0,spath + s_key) + return res diff --git a/class_v2/safeModelV2/firewallModel.py b/class_v2/safeModelV2/firewallModel.py new file mode 100644 index 00000000..99686d39 --- /dev/null +++ b/class_v2/safeModelV2/firewallModel.py @@ -0,0 +1,4300 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +# 系统防火墙 +#------------------------------ +import sys, os, json, re, time, sqlite3 +import contextlib +import traceback +from types import coroutine +from xml.etree.ElementTree import ElementTree, Element +from safeModelV2.base import safeBase +from flask import send_file, abort + +os.chdir("/www/server/panel") +sys.path.append("class/") +import public + + +from public import dict_obj +from public.validate import Param + +class main(safeBase): + + __isFirewalld = False + __isUfw = False + __firewall_obj = None + _add_sid = 0 + _ip_list = [] + _port_list = [] + _ufw_default = '/etc/default/ufw' + _ufw_sysctl = '/etc/ufw/sysctl.conf' + _ufw_before = '/etc/ufw/before.rules' + + _trans_status = "/www/server/panel/plugin/firewall/status.json" + _rule_path = "/www/server/panel/plugin/firewall/" + _ips_path = "/www/server/panel/plugin/firewall/ips.txt" + _country_path = "/www/server/panel/plugin/firewall/country.txt" + _white_list_file = "/www/server/panel/plugin/firewall/whitelist.txt" # 证书验证IP + + _white_list = [] + _firewall_create_tip = '{}/data/firewall_sqlite.pl'.format( + public.get_panel_path()) + + def __init__(self): + self.__firewall_obj = firewalld() + if os.path.exists('/usr/sbin/firewalld') and os.path.exists('/usr/bin/yum'): + self.__isFirewalld = True + if os.path.exists('/usr/sbin/ufw') and os.path.exists('/usr/bin/apt-get'): + self.__isUfw = True + # self.__ufw = '/usr/sbin/ufw' + self.get_old_rule() + + if not os.path.exists(self._trans_status): + ret = {"status": "close"} + public.writeFile(self._trans_status, json.dumps(ret)) + + if not os.path.exists(self._firewall_create_tip): + Sqlite() + public.writeFile(self._firewall_create_tip, '') + + def get_old_rule(self): + """ + @兼容防火墙插件规则 + """ + + self._rule_path = self._rule_path.replace('plugin', 'data') + if not os.path.exists(self._rule_path): os.makedirs(self._rule_path) + + if os.path.exists(self._trans_status): + n_path = self._trans_status.replace('plugin', 'data') + if not os.path.exists(n_path): + public.writeFile(n_path, public.readFile(self._trans_status)) + + if os.path.exists(self._ips_path): + n_path = self._ips_path.replace('plugin', 'data') + if not os.path.exists(n_path): + public.writeFile(n_path, public.readFile(self._ips_path)) + + if os.path.exists(self._white_list_file): + n_path = self._white_list_file.replace('plugin', 'data') + if not os.path.exists(n_path): + public.writeFile(n_path, + public.readFile(self._white_list_file)) + + if os.path.exists(self._country_path): + n_path = self._country_path.replace('plugin', 'data') + if not os.path.exists(n_path): + public.writeFile(n_path, public.readFile(self._country_path)) + + self._white_list_file = self._white_list_file.replace('plugin', 'data') + self._country_path = self._country_path.replace('plugin', 'data') + self._ips_path = self._ips_path.replace('plugin', 'data') + self._trans_status = self._trans_status.replace('plugin', 'data') + + def install_sys_firewall(self, get): + """ + @安装系统防火墙 + """ + + res = public.install_sys_firewall() + if res: + # return public.returnMsg(True, 'Successful installation') + return public.return_message(0, 0, 'Successful installation') + # return public.returnMsg(False, 'installation failed') + return public.return_message(-1, 0, 'installation failed') + + def get_firewall_info(self, get): + """ + @name 获取防火墙统计 + """ + data = {} + data['port'] = public.M('firewall_new').count() + data['ip'] = public.M('firewall_ip').count() + data['trans'] = public.M('firewall_trans').count() + data['country'] = public.M('firewall_country').count() + + isPing = True + try: + file = '/etc/sysctl.conf' + conf = public.readFile(file) + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)\n" + tmp = re.search(rep, conf).groups(0)[0] + if tmp == '1': isPing = False + except: + isPing = True + + data['ping'] = isPing + data['status'] = self.get_firewall_status() + # return data + return public.return_message(0, 0, data) + + # 服务状态获取 + # def get_firewall_status(self): + # if self.__isUfw: + # res = public.ExecShell("systemctl is-active ufw")[0] + # if res == "active": + # return True + # + # res = public.ExecShell("systemctl list-units | grep ufw")[0] + # if res.find('active running') != -1: + # return True + # + # res = public.ExecShell('/lib/ufw/ufw-init status')[0] + # if res.find("Firewall is not running") != -1: + # return False + # + # res = public.ExecShell('ufw status verbose')[0] + # if res.find('inactive') != -1: + # return False + # + # return True + # + # + # if self.__isFirewalld: + # res = public.ExecShell("ps -ef|grep firewalld|grep -v grep")[0] + # if res: + # return True + # + # res = public.ExecShell("systemctl is-active firewalld")[0] + # if res == "active": + # return True + # + # res = public.ExecShell("systemctl list-units | grep firewalld")[0] + # if res.find('active running') != -1: + # return True + # return False + # + # else: + # res = public.ExecShell("/etc/init.d/iptables status")[0] + # if res.find('not running') != -1: + # return False + # + # res = public.ExecShell("systemctl is-active iptables")[0] + # if res == "active": + # return True + # + # return True + def get_firewall_status(self): + if self.__isUfw: + res = public.ExecShell("systemctl is-active ufw")[0] + if res == "active": + # return True + return public.return_message(0, 0, True) + res = public.ExecShell("systemctl list-units | grep ufw")[0] + if res.find('active running') != -1: + # return True + return public.return_message(0, 0, True) + res = public.ExecShell('/lib/ufw/ufw-init status')[0] + if res.find("Firewall is not running") != -1: + # return False + return public.return_message(-1, 0, True) + res = public.ExecShell('ufw status verbose')[0] + if res.find('inactive') != -1: + # return False + return public.return_message(-1, 0, True) + # return True + return public.return_message(0, 0, True) + + if self.__isFirewalld: + res = public.ExecShell("ps -ef|grep firewalld|grep -v grep")[0] + if res: + # return True + return public.return_message(0, 0, True) + res = public.ExecShell("systemctl is-active firewalld")[0] + if res == "active": + # return True + return public.return_message(0, 0, True) + res = public.ExecShell("systemctl list-units | grep firewalld")[0] + if res.find('active running') != -1: + # return True + return public.return_message(0, 0, True) + # return False + return public.return_message(-1, 0, True) + else: + res = public.ExecShell("/etc/init.d/iptables status")[0] + if res.find('not running') != -1: + # return False + return public.return_message(-1, 0, True) + res = public.ExecShell("systemctl is-active iptables")[0] + if res == "active": + # return True + return public.return_message(0, 0, True) + # return True + return public.return_message(0, 0, True) + + def SetPing(self, get): + + if get.status == '1': + get.status = '0' + else: + get.status = '1' + filename = '/etc/sysctl.conf' + conf = public.readFile(filename) + if conf.find('net.ipv4.icmp_echo') != -1: + rep = r"net\.ipv4\.icmp_echo.*" + conf = re.sub(rep, 'net.ipv4.icmp_echo_ignore_all=' + get.status + "\n", conf) + else: + conf += "\nnet.ipv4.icmp_echo_ignore_all=" + get.status + "\n" + + if public.writeFile(filename, conf): + public.ExecShell('sysctl -p') + return public.returnMsg(True, 'SUCCESS') + else: + return public.returnMsg( + False, + # '错误:设置失败,sysctl.conf不可写!
                                    ' + # '1、如果安装了[宝塔系统加固],请先关闭
                                    ' + # '2、如果安装了云锁,请关闭[系统加固]功能
                                    ' + # '3、如果安装了安全狗,请关闭[系统防护]功能
                                    ' + # '4、如果使用了其它安全软件,请先卸载
                                    ' + 'Error: Setting failed, sysctl.conf is not writable!
                                    ' + '1. If [System Hardening] is installed, please close it first
                                    ' + '2. If Cloud Lock is installed, please turn off the [System Hardening] function
                                    ' + '3. If a security dog is installed, please turn off the [System Protection] function
                                    ' + '4. If you use other security software, please uninstall it first
                                    ' + ) + + + + # 服务状态控制 dict_obj. + def firewall_admin(self, get: dict_obj): + + try: + get.validate([ + Param('status').Require().String('in', ['start', 'stop']).Xss(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + order = ['reload', 'restart', 'stop', 'start'] + if not get.status in order: + return public.returnMsg(False, 'unknown control command!') + names = ["reload", "restart", "stop", "start"] + result = dict(zip(order, names)) + if self.__isUfw: + if get.status == "stop": + public.ExecShell('/usr/sbin/ufw disable') + elif get.status == "start": + public.ExecShell('echo y|/usr/sbin/ufw enable') + elif get.status == "reload": + public.ExecShell('/usr/sbin/ufw reload') + elif get.status == "restart": + public.ExecShell('/usr/sbin/ufw disable && /usr/sbin/ufw enable') + + # ufw防火墙启动时重载一次sysctl + # 解决deian系统启动ufw后禁ping失1效 by wzz/2023-06-14 + filename = '/etc/sysctl.conf' + conf = public.readFile(filename) + if conf.find('net.ipv4.icmp_echo') != -1: + public.ExecShell("sysctl -p") + public.WriteLog("system firewall", "firewall {}".format(result[get.status])) + # return public.returnMsg(True, 'firewall has {}'.format(result[get.status])) + return public.return_message(0, 0, "firewall has {}".format(result[get.status])) + if self.__isFirewalld: + public.ExecShell('systemctl {} firewalld'.format(get.status)) + public.WriteLog("system firewall", "firewall {}".format(result[get.status])) + # return public.returnMsg(True, 'firewall has {}'.format(result[get.status])) + return public.return_message(0, 0, "firewall has {}".format(result[get.status])) + else: + public.ExecShell('service iptables {}'.format(get.status)) + public.WriteLog("system firewall", "firewall {}".format(result[get.status])) + # return public.returnMsg(True, 'firewall has {}'.format(result[get.status])) + return public.return_message(0, 0, "fire/wall has {}".format(result[get.status])) + + # 重载防火墙配置 + def FirewallReload(self): + if self.__isUfw: + public.ExecShell('/usr/sbin/ufw reload') # 兼容安装了多个防火墙的情况 hezhihong # return + # 解决deian系统启动ufw后禁ping失1效 by wzz/2023-06-14 + filename = '/etc/sysctl.conf' + conf = public.readFile(filename) + if conf.find('net.ipv4.icmp_echo') != -1: + public.ExecShell("sysctl -p") + if self.__isFirewalld: + public.ExecShell('firewall-cmd --reload') + else: + public.ExecShell('/etc/init.d/iptables save') + public.ExecShell('/etc/init.d/iptables restart') + + #端口扫描 + def CheckPort(self, port, protocol): + import socket + localIP = '127.0.0.1' + temp = {} + temp['port'] = port + temp['local'] = True + try: + if 'tcp' in protocol.lower(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + s.connect((localIP, port)) + s.close() + if 'udp' in protocol.lower(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0.01) + s.sendto(b'', (localIP, port)) + s.close() + except: + temp['local'] = False + + result = 0 + if temp['local']: result += 2 + return result + + # 查询入栈规则 + def get_rules_list(self, args): + # 分页校验参数 + try: + args.validate([ + Param('limit').Integer(), + Param('p').Integer(), + Param('query').String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if self.__isFirewalld: + self.__firewall_obj = firewalld() + self.GetList() + else: + self.get_ufw_list() + try: + p = 1 + limit = 15 + if 'p' in args: p = args.p + if 'limit' in args: limit = args.limit + + where = '1=1' + sql = public.M('firewall_new') + + if hasattr(args, 'query'): + where = " ports like '%{search}%' or brief like '%{search}%' or address like '%{search}%'".format( + search=args.query) + + count = sql.where(where, ()).count() + data = public.get_page(count, int(p), int(limit)) + data['data'] = sql.where(where, ()).limit('{},{}'.format( + data['shift'], data['row'])).order('addtime desc').select() + res_data = data['data'] + for i in range(len(res_data)): + if not 'ports' in res_data[i]: + res_data[i]['status'] = -1 + continue + d = res_data[i] + _port = d['ports'] + _protocol = d['protocol'] + if _port.find(':') != -1 or _port.find('.') != -1 or _port.find( + '-') != -1: + d['status'] = -1 + else: + d['status'] = self.CheckPort(int(_port), _protocol) + for i in res_data: + if 'brief' in i: + i['brief'] = public.xsssec(i['brief']) + + return public.return_message(0, 0, res_data) + # return res_data + except Exception as e: + # return [] + return public.return_message(-1, 0, []) + + def check_firewall_rule(self, args): + """ + @检测防火墙规则 + """ + port = args['port'] + find = public.M('firewall_new').where('ports=?', (str(port), )).find() + if find: + return True + return False + + # 端口检查 + def check_port(self, port_list): + rep1 = r"^\d{1,5}(:\d{1,5})?$" + # rep1 = r'^[0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]$' + for port in port_list: + if port.find('-') != -1: + ports = port.split('-') + if not re.search(rep1, ports[0]): + return public.returnMsg(False, 'PORT_CHECK_RANGE') + if not re.search(rep1, ports[1]): + return public.returnMsg(False, 'PORT_CHECK_RANGE') + elif port.find(':') != -1: + ports = port.split(':') + if not re.search(rep1, ports[0]): + return public.returnMsg(False, 'PORT_CHECK_RANGE') + if not re.search(rep1, ports[1]): + return public.returnMsg(False, 'PORT_CHECK_RANGE') + else: + if not re.search(rep1, port): + return public.returnMsg(False, 'PORT_CHECK_RANGE') + + def parse_ip_interval(self, ip_str): + """解析区间IP + + author: lx + date: 2022/10/25 + + Returns: + list : IP列表 + """ + ips = [] + try: + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + searchor = re.compile(rep2) + if ip_str.find("-") != -1: + pre_ip, end_ip = ip_str.split("-") + if searchor.search(pre_ip) and searchor.search(end_ip): + ips.append(pre_ip) + pinx = pre_ip.rfind(".") + 1 + einx = end_ip.rfind(".") + 1 + pre = pre_ip[0:pinx] + end = end_ip[0:einx] + if pre == end: + start_num = int(pre_ip[pinx:]) + end_num = min(int(end_ip[einx:]), 255) + for i in range(start_num + 1, end_num): + new_ip = pre + str(i) + if searchor.search(new_ip): + ips.append(new_ip) + end_ip = end + str(end_num) + ips.append(end_ip) + except: + pass + return ips + + # 判断是否为ipv6网段 + @staticmethod + def is_ipv6_network_segment_or_ipv6_address(ip_datas: str) -> bool: + from ipaddress import IPv6Network, IPv6Address + try: + tmp_data = IPv6Network(ip_datas) + except: + try: + tmp_data = IPv6Address(ip_datas) + except: + return False + return True + + # 添加入栈规则 + def create_rules2(self, get): + ''' + get 里面 有 protocol port type address brief 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types == [accept、drop] # 放行和禁止 + address 地址,允许放行的ip,如果全部就是:0.0.0.0/0;另外可以包含“,"或者"-" + 表示区间IP + brief 备注说明 + ''' + protocol = get.protocol + ports = get.ports.strip() + types = get.types + address = get.source.strip() + port_list = ports.split(',') + result = self.check_port(port_list) # 检测端口 + if result: return result + + allow_ips = [] + if address: + sources = [ + sip.strip() for sip in address.split(",") if sip.strip() + ] + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + _ips = [] + for source_ip in sources: + if source_ip.find("-") != -1: + _ips += self.parse_ip_interval(source_ip) + else: + _ips.append(source_ip) + + for source_ip in _ips: + if not re.search(rep2, source_ip) and not self.is_ipv6_network_segment_or_ipv6_address(source_ip): + return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + allow_ips.append(source_ip) + if not allow_ips: + allow_ips.append("") + for source_ip in allow_ips: + if self.__isUfw: + for port in port_list: + if port.find('-') != -1: + port = port.replace('-', ':') + self.add_ufw_rule(source_ip, protocol, port, types) + else: + if self.__isFirewalld: + for port in port_list: + if port.find(':') != -1: + port = port.replace(':', '-') + self.add_firewall_rule(source_ip, protocol, port, + types) + else: + for port in port_list: + self.add_iptables_rule(source_ip, protocol, port, types) + + # 添加入栈规则 + def create_rules(self, get): + ''' + get 里面 有 protocol ports types address brief 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types == [accept、drop] # 放行和禁止 + address 地址,允许放行的ip,如果全部就是:0.0.0.0/0;另外可以包含“,"或者"-" + 表示区间IP + brief 备注说明 + ''' + + # 校验参数 + try: + get.validate([ + Param('ports').Require().Number(">=", 1).Number("<=", 65535), + Param('address').Require().Ip(), + Param('types').Require().String('in', ['accept', 'drop']), + Param('protocol').Require().String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + protocol = get.protocol + ports = get.ports.strip() + types = get.types + address = get.source.strip() + brief = get.brief.strip() + port_list = ports.split(',') + is_add = 2 if 'add' not in get else get.add + domain_total = '' if ("domain" not in get or not get.domain) else get.domain.strip() + domain = '' if ("domain" not in get or not get.domain) else get.domain.strip() + '|' + address + result = self.check_port(port_list) # 检测端口 + if result: + # return result + return public.return_message(0, 0, result) + + allow_ips = [] + if address: + sources = [sip.strip() for sip in address.split(",") if sip.strip()] + rep2 = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + _ips = [] + for source_ip in sources: + if source_ip.find("-") != -1: + _ips += self.parse_ip_interval(source_ip) + else: + _ips.append(source_ip) + + for source_ip in _ips: + if not re.search(rep2, source_ip) and not self.is_ipv6_network_segment_or_ipv6_address(source_ip): + # return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + return public.return_message(-1, 0, "FIREWALL_IP_FORMAT") + query_result = public.M('firewall_new').where( + 'ports=? and address=? and protocol=? and types=?', + (ports, source_ip, protocol, types,) + ).count() + if query_result > 0: + continue + allow_ips.append(source_ip) + if not allow_ips: + allow_ips.append("") + # 忽略的列表 + ignore_list = [] + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + for source_ip in allow_ips: + for port in port_list: + if is_add == 1: continue + # 检测端口是否已经添加过 + query_result = public.M('firewall_new').where( + 'ports=? and address=? and protocol=? and types=?', + (port, source_ip, protocol, types,) + ).find() + firewall_rules = self.get_sys_firewall_rules() + if query_result: + new_query_result = {"ports": query_result["ports"], "address": query_result["address"], + "protocol": query_result["protocol"], "types": query_result["types"]} + for firewall_rule in firewall_rules: + if new_query_result == firewall_rule: + ignore_list.append(port) + break + + self._add_firewall_rules(source_ip, protocol, port, types) + + if not query_result: + self._add_sid = public.M('firewall_new').add( + 'ports,brief,protocol,address,types,addtime,domain,sid', + (port, public.xsssec(brief), protocol, source_ip, types, addtime, domain, 0) + ) + + if domain: + domain_sid = public.M('firewall_domain').add( + 'types,domain,port,address,brief,addtime,sid,protocol,domain_total', + (types, domain, ports, address, public.xsssec(brief), addtime, self._add_sid, protocol, + domain_total) + ) + public.M('firewall_new').where("id=?", (self._add_sid,)).save('sid', domain_sid) + if len(allow_ips) > 0: + self.FirewallReload() + if not get.source.strip(): + log_ip = "All IPs" + else: + log_ip = get.source.strip() + strategy = '' + if types == 'accept': + strategy = "accept" + elif types == 'drop': + strategy = "drop" + public.WriteLog("system firewall", "Add port rules: Protocol:{}, Port:{}, Policy:{}, IP:{}".format(protocol, ports, strategy, log_ip)) + # 如果有忽略的端口,返回忽略的端口 + if ignore_list: + # return public.returnMsg(True, 'Added successfully, {} The same rule exists for the port and has been skipped'.format(', '.join(ignore_list))) + return public.return_message(0, 0, 'Added successfully, {} The same rule exists for the port and has been skipped'.format(', '.join(ignore_list))) + # return public.returnMsg(True, 'ADD_SUCCESS') + return public.return_message(0, 0, 'ADD_SUCCESS') + + # 删除入栈规则 + def remove_rules(self, get): + ''' + get 里面有 id protocol port type address 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types == [accept、drop] # 放行和禁止 + address 地址,允许放行的ip + ''' + # 检测是否开启防火墙 hezhihong + if not self.get_firewall_status(): + # return public.returnMsg(False, 'Please enable the firewall before proceeding.') + return public.return_message(-1, 0, 'Please enable the firewall before proceeding.') + + # { + # "id": 13, + # "protocol": "tcp", + # "ports": "3309", + # "address": "192.168.69.148", + # "types": "accept", + # "source": "192.168.69.148" + # } + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + Param('ports').Require().Number(">=", 1).Number("<=", 65535), + Param('address').Require().Ip(), + Param('source').Require().Ip(), + Param('types').Require().String('in', ['accept', 'drop']), + Param('protocol').Require().String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + id = get.id + address = get.address + protocol = get.protocol + ports = get.ports + types = get.types + self._del_firewall_rules(address, protocol, ports, types) + public.M('firewall_new').where("id=?", (id, )).delete() + self.FirewallReload() + if not get.address: + log_ip = "All IPs" + else: + log_ip = get.address + if types == 'accept': + strategy = "accept" + elif types == 'drop': + strategy = "drop" + public.WriteLog("system firewall", "Delete port rules: Protocol:{}, Port:{}, Policy:{}, IP:{}".format(get.protocol, get.ports, types, log_ip)) + # return public.returnMsg(True, 'DEL_SUCCESS') + return public.return_message(-1, 0, 'DEL_SUCCESS') + + + # 修改入栈规则 + def modify_rules(self, get, addtime=None): + ''' + get 里面有 id protocol port type address 五个参数 + protocol == ['tcp','udp'] + port = 端口 + types==['reject','accept'] # 放行和禁止 + address 地址,允许放行的ip,如果全部就是:0.0.0.0/0 + ''' + # 检测是否开启防火墙 hezhihong + if not self.get_firewall_status(): + return public.return_message(-1, 0, 'Please enable the firewall before proceeding.') + + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + Param('ports').Require().Number(">=", 1).Number("<=", 65535), + Param('address').Require().Ip(), + Param('source').Require().Ip(), + Param('types').Require().String('in', ['accept', 'drop']), + Param('protocol').Require().String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + protocol = get.protocol + ports = get.ports.strip() + types = get.types + address = get.source.strip() + brief = get.brief.strip() + domain = '' if 'domain' not in get else get.domain + domain_total = domain.split('|')[0] + sid = 0 if 'sid' not in get else get.sid + if address: + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + if not re.search(rep, get.source) and self.is_ipv6_network_segment_or_ipv6_address(get.source): + return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + data = public.M('firewall_new').where('id=?', (id, )).field( + 'id,address,protocol,ports,types,brief,addtime,domain' + ).find() + if data: + _address = data.get("address", "") + _protocol = data.get("protocol", "") + _port = data.get("ports", "") + _type = data.get("types", "") + else: + _address = _protocol = _port = _type = "" + self._modify_firewall_rules(_address, _protocol, _port, _type, address, protocol, ports, types) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + public.M('firewall_new').where('id=?', id).update({ + 'address': address, + 'protocol': protocol, + 'ports': ports, + 'types': types, + 'brief': brief, + 'addtime': addtime, + 'sid': sid, + 'domain': domain + } + ) + if domain: + public.M('firewall_domain').where("id=?", (sid,)).save( + 'sid,types,brief,protocol,domain_total', + (id, types, brief, protocol, domain_total) + ) + + with contextlib.suppress(Exception): + if int(ports) == 22: self.delete_service() + self.FirewallReload() + if not address: + log_ip = "All IPs" + else: + log_ip = address + if get.types == 'accept': + strategy = "accept" + elif get.types == 'drop': + strategy = "drop" + public.WriteLog("system firewall", "Modify port rules: Protocol:{}, Port:{}, Strategy:{}, IP:{}".format(get.protocol, get.ports.strip(), get.types, log_ip)) + # return public.returnMsg(True, '操作成功') + return public.return_message(-1, 0, 'operate successfully') + + # firewall端口规则添加 + def add_firewall_rule(self, address, protocol, ports, types): + if not address: + if protocol.find('/') != -1: + if types == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-port=' + + ports + '/tcp') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-port=' + + ports + '/udp') + else: + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule="rule family=ipv4 port protocol="tcp" port="%s" drop"' + % ports + ) + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule="rule family=ipv4 port protocol="udp" port="%s" drop"' + % ports + ) + else: + if types == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-port=' + + ports + '/' + protocol + '') + else: + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule="rule family=ipv4 port protocol="%s" port="%s" drop"' + % (protocol, ports)) + return True + if self.is_ipv6_network_segment_or_ipv6_address(address): + if protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv6 source address="%s" port protocol="tcp" port="%s" %s"' + % (address, ports, types)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv6 source address="%s" port protocol="udp" port="%s" %s"' + % (address, ports, types)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv6 source address="%s" port protocol="%s" port="%s" %s"' + % (address, protocol, ports, types)) + else: + if protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 source address="%s" port protocol="tcp" port="%s" %s"' + % (address, ports, types)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 source address="%s" port protocol="udp" port="%s" %s"' + % (address, ports, types)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 source address="%s" port protocol="%s" port="%s" %s"' + % (address, protocol, ports, types)) + return True + + # firewall端口规则删除 + def del_firewall_rule(self, address, protocol, ports, types): + if not address: + if protocol.find('/') != -1: + if types == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-port=' + + ports + '/tcp') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-port=' + + ports + '/udp') + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family=ipv4 port protocol="tcp" port="%s" drop"' + % ports + ) + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family=ipv4 port protocol="udp" port="%s" drop"' + % ports + ) + else: + if types == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-port=' + + ports + '/' + protocol + '') + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family=ipv4 port protocol="%s" port="%s" drop"' + % (protocol, ports)) + self.update_panel_data(ports) + return True + if self.is_ipv6_network_segment_or_ipv6_address(address): + if protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv6" source address="%s" port protocol="tcp" port="%s" %s"' + % (address, ports, types)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv6" source address="%s" port protocol="udp" port="%s" %s"' + % (address, ports, types)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv6" source address="%s" port protocol="%s" port="%s" %s"' + % (address, protocol, ports, types)) + else: + if protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv4" source address="%s" port protocol="tcp" port="%s" %s"' + % (address, ports, types)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv4" source address="%s" port protocol="udp" port="%s" %s"' + % (address, ports, types)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv4" source address="%s" port protocol="%s" port="%s" %s"' + % (address, protocol, ports, types)) + return True + + # firewall端口规则编辑 + def edit_firewall_rule(self, _address, _protocol, _port, _type, address, + protocol, ports, types): + if not _address: + if _protocol.find('/') != -1: + if _type == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-port=' + + _port + '/tcp') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-port=' + + _port + '/udp') + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family=ipv4 port protocol="tcp" port="%s" drop"' + % ports + ) + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family=ipv4 port protocol="udp" port="%s" drop"' + % ports + ) + else: + if _type == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-port=' + + _port + '/' + _protocol + '') + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family=ipv4 port protocol="%s" port="%s" drop"' + % (protocol, ports)) + else: + if self.is_ipv6_network_segment_or_ipv6_address(address): + if _protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv6" source address="%s" port protocol="tcp" port="%s" %s"' + % (_address, _port, _type)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv6" source address="%s" port protocol="udp" port="%s" %s"' + % (_address, _port, _type)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv6" source address="%s" port protocol="%s" port="%s" %s"' + % (_address, _protocol, _port, _type)) + else: + if _protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv4" source address="%s" port protocol="tcp" port="%s" %s"' + % (_address, _port, _type)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv4" source address="%s" port protocol="udp" port="%s" %s"' + % (_address, _port, _type)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--remove-rich-rule="rule family="ipv4" source address="%s" port protocol="%s" port="%s" %s"' + % (_address, _protocol, _port, _type)) + if not address: + if protocol.find('/') != -1: + if types == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-port=' + + ports + '/tcp') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-port=' + + ports + '/udp') + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 port protocol="tcp" port="%s" drop"' + % ports + ) + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 port protocol="udp" port="%s" drop"' + % ports + ) + else: + if types == "accept": + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-port=' + + ports + '/' + protocol + '') + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 port protocol="%s" port="%s" drop"' + % (protocol, ports)) + else: + if self.is_ipv6_network_segment_or_ipv6_address(address): + if protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv6 source address="%s" port protocol="tcp" port="%s" %s"' + % (address, ports, types)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv6 source address="%s" port protocol="udp" port="%s" %s"' + % (address, ports, types)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv6 source address="%s" port protocol="%s" port="%s" %s"' + % (address, protocol, ports, types)) + else: + if protocol.find('/') != -1: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 source address="%s" port protocol="tcp" port="%s" %s"' + % (address, ports, types)) + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 source address="%s" port protocol="udp" port="%s" %s"' + % (address, ports, types)) + else: + public.ExecShell( + 'firewall-cmd --permanent ' + '--add-rich-rule="rule family=ipv4 source address="%s" port protocol="%s" port="%s" %s"' + % (address, protocol, ports, types)) + return True + + # ufw 端口规则添加 + def add_ufw_rule(self, address, protocol, ports, types): + rule = "allow" if types == "accept" else "deny" + if address == "": + if protocol.find('/') != -1: + # public.ExecShell('ufw ' + rule + ' ' + ports + '/tcp') + # public.ExecShell('ufw ' + rule + ' ' + ports + '/udp') + public.ExecShell('ufw ' + rule + ' ' + ports) + else: + public.ExecShell('ufw ' + rule + ' ' + ports + '/' + protocol + '') + else: + if protocol.find('/') != -1: + # public.ExecShell('ufw ' + rule + ' proto tcp from ' + address + ' to any port ' + ports + '') + # public.ExecShell('ufw ' + rule + ' proto udp from ' + address + ' to any port ' + ports + '') + public.ExecShell('ufw ' + rule + ' from ' + address + ' to any port ' + ports + '') + else: + public.ExecShell( + 'ufw ' + rule + ' proto ' + protocol + ' from ' + address + ' to any port ' + ports + '') + + # ufw 端口规则删除 + def del_ufw_rule(self, address, protocol, ports, types): + rule = "allow" if types == "accept" else "deny" + if address == "": + if protocol.find('/') != -1: + public.ExecShell('ufw delete ' + rule + ' ' + ports + '/tcp') + public.ExecShell('ufw delete ' + rule + ' ' + ports + '/udp') + public.ExecShell('ufw delete ' + rule + ' ' + ports) + else: + public.ExecShell('ufw delete ' + rule + ' ' + ports + '/' + protocol + '') + else: + if protocol.find('/') != -1: + public.ExecShell('ufw delete ' + rule + ' proto tcp from ' + address + ' to any port ' + ports + '') + public.ExecShell('ufw delete ' + rule + ' proto udp from ' + address + ' to any port ' + ports + '') + public.ExecShell('ufw delete ' + rule + ' from ' + address + ' to any port ' + ports) + else: + public.ExecShell( + 'ufw delete ' + rule + ' proto ' + protocol + ' from ' + address + ' to any port ' + ports + '' + ) + self.update_panel_data(ports) + + # ufw 端口规则修改 + def edit_ufw_rule(self, _address, _protocol, _port, _type, address, + protocol, ports, types): + _rule = "allow" if _type == "accept" else "deny" + rules = "allow" if types == "accept" else "deny" + if _address == "": + if _protocol.find('/') != -1: + public.ExecShell('ufw delete ' + _rule + ' ' + _port + '/tcp') + public.ExecShell('ufw delete ' + _rule + ' ' + _port + '/udp') + public.ExecShell('ufw delete ' + _rule + ' ' + _port) + else: + public.ExecShell('ufw delete ' + _rule + ' ' + _port + '/' + _protocol + '') + else: + if _protocol.find('/') != -1: + public.ExecShell('ufw delete ' + _rule + ' proto tcp from ' + _address + ' to any port ' + _port + '') + public.ExecShell('ufw delete ' + _rule + ' proto udp from ' + _address + ' to any port ' + _port + '') + public.ExecShell('ufw delete ' + _rule + ' from ' + _address + ' to any port ' + _port) + else: + public.ExecShell( + 'ufw delete ' + _rule + ' proto ' + _protocol + ' from ' + _address + ' to any port ' + _port + '' + ) + if address == "": + if protocol.find('/') != -1: + # public.ExecShell('ufw ' + rules + ' ' + ports + '/tcp') + # public.ExecShell('ufw ' + rules + ' ' + ports + '/udp') + public.ExecShell('ufw ' + rules + ' ' + ports) + else: + public.ExecShell('ufw ' + rules + ' ' + ports + '/' + protocol + '') + else: + if protocol.find('/') != -1: + # public.ExecShell('ufw ' + rules + ' proto tcp from ' + address + ' to any port ' + ports + '') + # public.ExecShell('ufw ' + rules + ' proto udp from ' + address + ' to any port ' + ports + '') + public.ExecShell('ufw ' + rules + ' from ' + address + ' to any port ' + ports) + else: + public.ExecShell( + 'ufw ' + rules + ' proto ' + protocol + ' from ' + address + ' to any port ' + ports + '' + ) + + # iptables端口规则添加 + def add_iptables_rule(self, address, protocol, ports, types): + rule = "ACCEPT" if types == "accept" else "DROP" + if not address: + if protocol.find('/') != -1: + public.ExecShell( + 'iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport ' + + ports + ' -j ' + rule + '') + public.ExecShell( + 'iptables -I INPUT -p tcp -m state --state NEW -m udp --dport ' + + ports + ' -j ' + rule + '') + else: + public.ExecShell( + 'iptables -I INPUT -p tcp -m state --state NEW -m ' + + protocol + ' --dport ' + ports + ' -j ' + rule + '') + else: + if protocol.find('/') != -1: + public.ExecShell('iptables -I INPUT -s ' + address + + ' -p tcp --dport ' + ports + ' -j ' + rule + + '') + public.ExecShell('iptables -I INPUT -s ' + address + + ' -p udp --dport ' + ports + ' -j ' + rule + + '') + else: + public.ExecShell('iptables -I INPUT -s ' + address + ' -p ' + + protocol + ' --dport ' + ports + ' -j ' + + rule + '') + return True + + # iptables端口规则删除 + def del_iptables_rule(self, address, protocol, ports, types): + rule = "ACCEPT" if types == "accept" else "DROP" + if not address: + if protocol.find('/') != -1: + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m tcp --dport ' + + ports + ' -j ' + rule + '') + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m udp --dport ' + + ports + ' -j ' + rule + '') + else: + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m ' + + protocol + ' --dport ' + ports + ' -j ' + rule + '') + else: + if protocol.find('/') != -1: + public.ExecShell('iptables -D INPUT -s ' + address + + ' -p tcp --dport ' + ports + ' -j ' + rule + + '') + public.ExecShell('iptables -D INPUT -s ' + address + + ' -p udp --dport ' + ports + ' -j ' + rule + + '') + else: + public.ExecShell('iptables -D INPUT -s ' + address + ' -p ' + + protocol + ' --dport ' + ports + ' -j ' + + rule + '') + return True + + # iptables端口规则编辑 + def edit_iptables_rule(self, _address, _protocol, _port, _type, address, + protocol, ports, types): + rule1 = "ACCEPT" if _type == "accept" else "DROP" + rule2 = "ACCEPT" if types == "accept" else "DROP" + if not _address: + if _protocol.find('/') != -1: + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m tcp --dport ' + + _port + ' -j ' + rule1 + '') + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m udp --dport ' + + _port + ' -j ' + rule1 + '') + else: + public.ExecShell( + 'iptables -D INPUT -p tcp -m state --state NEW -m ' + + _protocol + ' --dport ' + _port + ' -j ' + rule1 + '') + else: + if _protocol.find('/') != -1: + public.ExecShell('iptables -D INPUT -s ' + _address + + ' -p tcp --dport ' + _port + ' -j ' + rule1 + + '') + public.ExecShell('iptables -D INPUT -s ' + _address + + ' -p udp --dport ' + _port + ' -j ' + rule1 + + '') + else: + public.ExecShell('iptables -D INPUT -s ' + _address + ' -p ' + + _protocol + ' --dport ' + _port + ' -j ' + + rule1 + '') + if not address: + if protocol.find('/') != -1: + public.ExecShell( + 'iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport ' + + ports + ' -j ' + rule2 + '') + public.ExecShell( + 'iptables -I INPUT -p tcp -m state --state NEW -m udp --dport ' + + ports + ' -j ' + rule2 + '') + else: + public.ExecShell( + 'iptables -I INPUT -p tcp -m state --state NEW -m ' + + protocol + ' --dport ' + ports + ' -j ' + rule2 + '') + else: + if protocol.find('/') != -1: + public.ExecShell('iptables -I INPUT -s ' + address + + ' -p tcp --dport ' + ports + ' -j ' + rule2 + + '') + public.ExecShell('iptables -I INPUT -s ' + address + + ' -p udp --dport ' + ports + ' -j ' + rule2 + + '') + else: + public.ExecShell('iptables -I INPUT -s ' + address + ' -p ' + + protocol + ' --dport ' + ports + ' -j ' + + rule2 + '') + return True + + # 修改面板数据 + def update_panel_data(self, ports): + res = public.M('firewall').where("port=?", (ports, )).delete() + + # 查询IP规则 + def get_ip_rules_list(self, args): + # 分页校验参数 + try: + args.validate([ + Param('limit').Integer(), + Param('p').Integer(), + Param('query').String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + p = 1 + limit = 15 + if 'p' in args: p = args.p + if 'limit' in args: limit = args.limit + + where = '1=1' + sql = public.M('firewall_ip') + + if hasattr(args, 'query'): + where = " address like '%{search}%' or brief like '%{search}%' ".format( + search=args.query) + + count = sql.where(where, ()).count() + data = public.get_page(count, int(p), int(limit)) + data['data'] = sql.where(where, ()).limit('{},{}'.format( + data['shift'], data['row'])).order('addtime desc').select() + + data['data'] = public.return_area(data['data'], 'address') + # return data + return public.return_message(0, 0, data) + + def check_a_ip(self, address): + """ + @name 检测A记录是否为域名 + @author hezhihong + """ + if address: + if public.is_ipv4(address) or public.is_ipv6(address): + return address + if address[-1] == '.': + address = address[:-1] + if public.is_domain(address): return self.get_a_ip(address) + return address + + def get_a_ip(self, hostname): + ''' + @name 检测主机名是否有A记录 + @author hezhihong + :param hostname: + :return: + ''' + if not self.install_dnspython(): + return public.returnMsg(False, '请先安装dnspython模块') + import dns.resolver + # 尝试3次 + a_ip = [] + for i in range(3): + try: + resolver = dns.resolver.Resolver() + resolver.timeout = 1 + try: + result = resolver.query(hostname, 'A') + except: + result = resolver.resolve(hostname, 'A') + for i in result.response.answer: + for j in i.items: + try: + A_ip = str(j).strip() + if A_ip[-1] == '.': + A_ip = A_ip[:-1] + except: + pass + + if A_ip not in a_ip: + a_ip.append(A_ip) + + except: + pass + + # 去除域名 + if len(a_ip) > 1: + for i2 in a_ip: + if public.is_ipv4(i2) or public.is_ipv6(i2): + continue + if public.is_domain(i2): + a_ip.remove(i2) + return a_ip + + def install_dnspython(self): + """ + @name 安装dnspython模块 + @author hezhihong + """ + # 检测dns解析 + try: + import dns.resolver + return True + except: + if os.path.exists('/www/server/panel/pyenv'): + public.ExecShell('/www/server/panel/pyenv/bin/pip install dnspython') + else: + public.ExecShell('pip3 install dnspython') + try: + import dns.resolver + return True + except: + return False + + def del_domain_ip(self, args): + """ + @name 删除域名设置 + @author hezhihong + """ + + if 'id' not in args or not args.id or 'sid' not in args: + return public.returnMsg(False, 'Parameter error') + domain_id = int(args.sid) + + # 删除IP规则 + if domain_id > 0: + # 删除域名解析 + public.M('firewall_domain').where('id=?', (str(domain_id),)).delete() + # 删除端口规则 + if 'ports' in args: + self.remove_rules(args) + # 删除IP规则 + else: + self.remove_ip_rules(args) + + # 当没有域名解析时,删除计划任务 + if not public.M('firewall_domain').count(): + pdata = public.M('crontab').where('name=?', '[Do not delete] System firewall domain name resolution detection task').select() + if pdata: + for i in pdata: + args = {"id": i['id']} + import crontab + crontab.crontab().DelCrontab(args) + + return public.returnMsg(True, 'successfully deleted') + + def add_crontab(self): + """ + @name 构造日志切割任务 + @author hezhihong + """ + python_path = '' + try: + python_path = public.ExecShell('which btpython')[0].strip("\n") + except: + try: + python_path = public.ExecShell('which python')[0].strip("\n") + except: + pass + if not python_path: return False + if not public.M('crontab').where('name=?', ('[Do not delete] System firewall domain name resolution detection task',)).count(): + cmd = '{} {}'.format(python_path, '/www/server/panel/script/firewall_domain.py') + args = {"name": "[Do not delete] System firewall domain name resolution detection task", "type": 'minute-n', "where1": '5', "hour": '', + "minute": '', "sName": "", + "sType": 'toShell', "notice": '', "notice_channel": '', "save": '', "save_local": '1', + "backupTo": '', "sBody": cmd, + "urladdress": ''} + import crontab + res = crontab.crontab().AddCrontab(args) + if res and "id" in res.keys(): + return True + return False + return True + + def __check_auth(self): + try: + from pluginAuth import Plugin + plugin_obj = Plugin(False) + plugin_list = plugin_obj.get_plugin_list() + if int(plugin_list['ltd']) > time.time(): + return True + return False + except: + return False + + def set_domain_ip2(self, args): + """ + @name 设置域名规则 + @author hezhihong + """ + pay = self.__check_auth() + if not pay: return public.returnMsg(False, 'Current features are exclusive to the professional version') + if not args.domain: return public.returnMsg(False, 'Please enter domain name') + ports = '' + if 'ports' in args and args.ports: ports = args.ports + ip = args.source + + # 添加计划任务 + self.add_crontab() + # 添加端口规则 + # {"protocol":"tcp","ports":"819","choose":"point","address":"125.93.252.236","types":"accept","brief":"","source":"125.93.252.236"} + args.address = ip + args.source = ip + if ports: + if public.is_ipv6(ip): + return public.returnMsg(False, 'The domain name is resolved to an IPv6 address and port rules are not supported.') + self.create_rules2(args) + # 添加IP规则 + else: + # return 333 + self.create_ip_rules(args) + + return public.returnMsg(True, 'Domain name {} resolution added successfully'.format(args.domain)) + + def set_domain_ip(self, args): + """ + @name 设置域名规则 + @author hezhihong + """ + pay = self.__check_auth() + if not pay: return public.returnMsg(False, 'Current features are exclusive to the professional version') + if not args.domain: return public.returnMsg(False, 'Please enter domain name') + ports = '' + if 'ports' in args and args.ports: ports = args.ports + protocol = '' if 'protocol' not in args else args.protocol + a_ip = self.get_a_ip(args.domain) + # return a_ip + if a_ip and len(a_ip) < 2 and public.is_domain(a_ip[0]): + # return 111 + a_ip = [self.check_a_ip(a_ip[0])] + # return a_ip + if not a_ip: + return public.returnMsg(False, 'The domain name resolution has not been resolved or the resolution has not taken effect. If it has been resolved, please try again after 10 minutes.') + if public.M('firewall_domain').where("domain=? and types=? and port=? and protocol=?", + (args.domain, args.types, ports, protocol,)).count(): + return public.returnMsg(False, 'Domain name {} already exists'.format(args.domain)) + + # 添加计划任务 + self.add_crontab() + # 添加端口规则 + # {"protocol":"tcp","ports":"819","choose":"point","address":"125.93.252.236","types":"accept","brief":"","source":"125.93.252.236"} + for ip in a_ip: + args.address = ip + args.source = ip + if ports: + if public.is_ipv6(ip): + return public.returnMsg(False, 'The domain name is resolved to an IPv6 address and port rules are not supported.') + self.create_rules(args) + # 添加IP规则 + else: + # return 333 + self.create_ip_rules(args) + + return public.returnMsg(True, 'Domain name {} resolution added successfully'.format(args.domain)) + + def modify_domain_ip(self, args): + """ + @name 修改域名规则(当修改为指定域名或从指定域名修改为其他时,需要调用此方法) + @name hezhihong + """ + pay = self.__check_auth() + if not pay: return public.returnMsg(False, 'Current features are exclusive to the professional version') + + # 检测是否开启防火墙 hezhihong + if not self.get_firewall_status(): + return public.returnMsg(False, 'Please enable the firewall before proceeding.') + + modify_args = public.dict_obj() + modify_args.id = args.id + modify_args.types = args.types + modify_args.brief = args.brief + modify_args.address = args.address + modify_args.sid = 0 if 'sid' not in args else args.sid + ports = '' if 'ports' not in args else args.ports + domain = '' if 'domain' not in args else args.domain + if ports: modify_args.ports = ports + choose = '' if 'choose' not in args else args.choose + pdata = {} + if int(args.sid) > 0: + pdata = public.M('firewall_domain').where('id=?', (args.sid,)).find() + # 修改端口规则 + if ports: + modify_args.protocol = args.protocol + # 已经指定域名 + if int(args.sid) > 0: + # 当修改为指定域名时 + if choose == 'domain': + # 当修改为不同域名时 + if domain != pdata['domain']: + self.del_domain_ip(args) + self.set_domain_ip(args) + # 当修改为相同域名时 + else: + pdata['protocol'] = args.protocol + pdata['types'] = args.types + pdata['brief'] = public.xsssec(args.brief) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + pdata['addtime'] = addtime + public.M('firewall_domain').where('id=?', pdata['id']).update(pdata) + self.modify_rules(args) + return public.returnMsg(True, 'Successfully modified') + else: + args.domain = '' + self.del_domain_ip(args) + self.create_rules(args) + return public.returnMsg(True, 'Successfully modified') + # 当未指定域名时 + else: + # 修改为指定域名 + if domain: + self.remove_rules(args) + self.set_domain_ip(args) + return public.returnMsg(True, 'Successfully modified') + # 修改IP规则 + else: + if int(args.sid) > 0: + modify_args.address = pdata['address'] + modify_args.domain = pdata['domain'] + return self.modify_ip_rules(modify_args) + + # IP地址检测 + def check_ip(self, address_list): + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + for address in address_list: + address = address.split('/')[0] + if address.find('-') != -1: + addresses = address.split('-') + if addresses[0] >= addresses[1]: + return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + s_ips = addresses[0].split(".") + e_ips = addresses[1].split(".") + head_s_ip = s_ips[0] + "." + s_ips[1] + "." + s_ips[2] + "." + head_e_ip = e_ips[0] + "." + e_ips[1] + "." + e_ips[2] + "." + if head_s_ip != head_e_ip: + return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + if not re.search(rep, addresses[0]): + return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + if not re.search(rep, addresses[1]): + return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + else: + if not re.search(rep, address) and not public.is_ipv6(address): + return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + + # 获取IP范围 + def get_ip(self, address): + result = [] + arrys = address.split("-") + s_ips = arrys[0].split(".") + e_ips = arrys[1].split(".") + head_s_ip = s_ips[0] + "." + s_ips[1] + "." + s_ips[2] + "." + region = int(e_ips[-1]) - int(s_ips[-1]) + for num in range(0, region + 1): + result.append(head_s_ip + str(num + int(s_ips[-1]))) + return result + + def handle_firewall_ip(self, address, types): + ip_list = self.get_ip(address) + if isinstance(ip_list, dict): + return + public.ExecShell( + 'firewall-cmd --permanent --zone=public --new-ipset=' + address + + ' --type=hash:net') + xml_path = "/etc/firewalld/ipsets/%s.xml" % address + tree = ElementTree() + tree.parse(xml_path) + root = tree.getroot() + for ip in ip_list: + entry = Element("entry") + entry.text = ip + root.append(entry) + self.format(root) + tree.write(xml_path, 'utf-8', xml_declaration=True) + # public.ExecShell('firewall-cmd --permanent --zone=public --add-rich-rule=\'rule source ipset="'+ address +'" accept\'') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-rich-rule=\'rule source ipset="' + + address + '" ' + types + '\'') + + def handle_ufw_ip(self, address, types): + ip_list = self.get_ip(address) + if isinstance(ip_list, dict): + return + public.ExecShell('ipset create ' + address + ' hash:net') + for ip in ip_list: + public.ExecShell('ipset add ' + address + ' ' + ip) + public.ExecShell('iptables -I INPUT -m set --match-set ' + address + + ' src -j ' + types.upper()) + + # 检查IP地址是否在范围内 + def ip_in_range(self, ip, ip_range): + import ipaddress + # 2024/1/3 下午 7:59 兼容192.168.0.0/24这种形式 + if ip_range.find('/') != -1: + return ipaddress.ip_address(ip) in ipaddress.ip_network(ip_range) + + ip_range = ip_range.split('-') + if len(ip_range) == 1: # 如果只有一个IP地址 + return ipaddress.ip_address(ip) == ipaddress.ip_address(ip_range[0]) + else: # 如果是一个IP范围 + start_ip, end_ip = ip_range + ip_networks = ipaddress.summarize_address_range(ipaddress.ip_address(start_ip), ipaddress.ip_address(end_ip)) + return any(ipaddress.ip_address(ip) in net for net in ip_networks) + + + # 添加IP规则 + def create_ip_rules(self, get): + # { + # "protocol": "tcp", + # "ports": "3309", + # "choose": "point", + # "address": "192.168.69.148", + # "types": "accept", + # "brief": "", + # "source": "192.168.69.148" + # } + + + # 校验参数 + try: + get.validate([ + Param('ports').Number(">=", 1).Number("<=", 65535), + Param('source').Ip(), + Param('address').Ip(), + Param('types').String('in', ['accept', 'drop']), + Param('protocol').String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + from flask import request + user_ip = request.remote_addr + _address = get.address.strip() + original_types = get.types + brief = get.brief + domain_total = '' if ('domain' not in get or not get.domain) else get.domain.strip() + domain = '' if ('domain' not in get or not get.domain) else get.domain.strip() + '|' + _address + address_list = _address.split(',') + # public.print_log('############ ip接口 {}'.format(address_list)) + result = self.check_ip(address_list) + + if result: + # return result + return public.return_message(0, 0, result) + + # 先处理用户的IP地址 + old_login_ip = public.M('firewall_ip').where("brief=?", ("IP that allows users to log in",)).field('id, address').select() + # public.print_log('############ ip接口user_ip {}'.format(user_ip)) + for ip_range in address_list: + + if self.ip_in_range(user_ip, ip_range): + + if old_login_ip and old_login_ip[0][' address'] != user_ip: + address = old_login_ip[0][' address'] + public.M('firewall_ip').where("address=?", (address,)).delete() + + self.update_panel_data(address) # 删除面板自带防火墙的表数据 + + self.add_rule(user_ip, "accept", "IP that allows users to log in", domain, domain_total) + + break + + # 然后处理其他的IP地址 + for address in address_list: + + self.add_rule(address, original_types, brief, domain, domain_total) + + self.FirewallReload() + + + public.WriteLog("system firewall", "Add IP rules: IP: {}, policy: {}".format(_address, original_types)) + + # return public.returnMsg(True, 'ADD_SUCCESS') + return public.return_message(0, 0, 'ADD_SUCCESS') + + # 添加单个IP规则 + def add_rule(self, address, types, brief, domain, domain_total): + if public.M('firewall_ip').where("address=? and types=? and domain=?", (address, types, domain)).count() > 0: + + return + if self.__isUfw: + # public.print_log('############ ip接口 1') + _rule = "allow" if types == "accept" else "deny" + if address.find('-') != -1: + # public.print_log('############!!! ip接口 2') + self.handle_ufw_ip(address, types) + else: + # public.print_log('############!!! ip接口 3') + is_debian = True if public.get_os_version().lower().find("debian") != -1 else False + if not is_debian: + if _rule == "allow": + if public.is_ipv6(address): + public.ExecShell('ufw ' + _rule + ' from ' + address + ' to any') + else: + public.ExecShell('ufw insert 1 ' + _rule + ' from ' + address + ' to any') + else: + public.ExecShell('ufw ' + _rule + ' from ' + address + ' to any') + else: + public.ExecShell('iptables -I INPUT -s ' + address + ' -j ' + types.upper()) + else: + # public.print_log('############!!! ip接口 4') + if self.__isFirewalld: + # public.print_log('############ ip接口 6') + if address.find('-') != -1: + self.handle_firewall_ip(address, types) + else: + if types == "accept": + public.ExecShell('firewall-cmd --permanent --add-source=' + address + ' --zone=trusted') + else: + if public.is_ipv6(address): + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule=\'rule family=ipv6 source address="' + address + '" ' + types + '\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule=\'rule family=ipv4 source address="' + address + '" ' + types + '\'') + else: + # public.print_log('############ ip接口 7') + if address.find('-') != -1: + self.handle_ufw_ip(address, types) + else: + public.ExecShell('iptables -I INPUT -s ' + address + ' -j ' + types.upper()) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + self._add_sid = public.M('firewall_ip').add('address,types,brief,addtime,domain,sid', + (address, types, public.xsssec(brief), addtime, domain, 0,)) + # public.print_log('############ ip接口 8{}'.format(self._add_sid)) + if domain: + # public.print_log('############ ip接口9{}'.format(domain)) + domain_sid = public.M('firewall_domain').add( + 'types,domain,port,address,brief,addtime,sid,protocol,domain_total', ( + types, domain, '', address, public.xsssec(brief), addtime, self._add_sid, '', domain_total)) + + public.M('firewall_ip').where("id=?", (self._add_sid,)).save('sid', domain_sid) + + + # 删除All IPs规则 + def remove_all_ip_rules(self, get): + ip_list = public.M('firewall_ip').select() + for ip in ip_list: + id = ip["id"] + address = ip["address"] + types = ip["types"] + if self.__isUfw: + _rule = "allow" if types == "accept" else "deny" + if address.find('-') != -1: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + address + ' src -j ' + types.upper()) + public.ExecShell('ipset destroy ' + address) + else: + is_debian = True if public.get_os_version().lower().find( + "debian") != -1 else False + if not is_debian: + public.ExecShell('ufw delete ' + _rule + ' from ' + address + ' to any') + else: + public.ExecShell("iptables -D INPUT -s " + address + + " -j " + types.upper()) + else: + if self.__isFirewalld: + if address.find('-') != -1: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-rich-rule=\'rule source ipset="' + + address + '" ' + types + '\'') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --delete-ipset=' + + address) + else: + public.ExecShell( + 'firewall-cmd --permanent --remove-source=' + + address + ' --zone=trusted') + if public.is_ipv6(address): + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv6 source address="' + + address + '" ' + types + '\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv4 source address="' + + address + '" ' + types + '\'') + else: + if address.find('-') != -1: + public.ExecShell( + 'iptables -D INPUT -m set --match-set ' + address + + ' src -j ' + types.upper()) + public.ExecShell('ipset destroy ' + address) + else: + public.ExecShell('iptables -D INPUT -s ' + address + + ' -j ' + types.upper()) + public.M('firewall_ip').where("id=?", (id, )).delete() + self.update_panel_data(address) # 删除面板自带防火墙的表数据 + self.FirewallReload() + return public.returnMsg(True, 'All IP rules have been removed.') + + # 删除IP规则 + def remove_ip_rules(self, get): + + # {"id": 4, "types": "accept", "address": "192.168.168.162", "brief": "123", "addtime": "2024-04-12 17:19:04", + # "sid": 0, "domain": "", "area": {"info": "Intranet"}} + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + # Param('ports').Require(), + Param('address').Require().Ip(), + Param('types').Require().String('in', ['accept', 'drop']), + # Param('area').Require().Dict().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + id = get.id + address = get.address + types = get.types + if self.__isUfw: + _rule = "allow" if types == "accept" else "deny" + if address.find('-') != -1: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + address + ' src -j ' + types.upper()) + public.ExecShell('ipset destroy ' + address) + else: + is_debian = True if public.get_os_version().lower().find( + "debian") != -1 else False + if not is_debian: + public.ExecShell('ufw delete ' + _rule + ' from ' + address + ' to any') + else: + public.ExecShell('ufw delete ' + _rule + ' from ' + address + ' to any') + public.ExecShell("iptables -D INPUT -s " + address + + " -j " + types.upper()) + else: + if self.__isFirewalld: + if address.find('-') != -1: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-rich-rule=\'rule source ipset="' + + address + '" ' + types + '\'') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --delete-ipset=' + + address) + else: + public.ExecShell( + 'firewall-cmd --permanent --remove-source=' + address + + ' --zone=trusted') + if public.is_ipv6(address): + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv6 source address="' + + address + '" ' + types + '\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv4 source address="' + + address + '" ' + types + '\'') + else: + if address.find('-') != -1: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + address + ' src -j ' + types.upper()) + public.ExecShell('ipset destroy ' + address) + else: + public.ExecShell('iptables -D INPUT -s ' + address + + ' -j ' + types.upper()) + public.M('firewall_ip').where("id=?", (id, )).delete() + self.update_panel_data(address) # 删除面板自带防火墙的表数据 + self.FirewallReload() + strategy= '' + if get.types == 'accept': + strategy = "accept" + elif get.types == 'drop': + strategy = "drop" + public.WriteLog("system firewall", "Delete IP rules: IP:{}, policy:{}".format(get.address, strategy)) + return public.returnMsg(True, 'DEL_SUCCESS') + + # 修改IP规则 + def modify_ip_rules(self, get): + + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + # Param('ports').Require(), + # Param('source').Require().Ip(), + Param('address').Require().Ip(), + Param('types').Require().String('in', ['accept', 'drop']), + # Param('protocol').Require().String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + id = get.id + address = get.address.strip() + types = get.types + brief = get.brief + result = self.check_ip([address]) + sid = 0 if 'sid' not in get else get.sid + domain = '' if 'domain' not in get else get.domain + domain_total = domain.split('|')[0] + # return 22 + if result: + # return result + return public.return_message(0, 0, result) + data = public.M('firewall_ip').where( + 'id=?', (id, )).field('id,address,types,brief,addtime').find() + _address = data.get("address", "") + _type = data.get("types", "") + if self.__isUfw: + rule1 = "allow" if _type == "accept" else "deny" + if _address.find('-') != -1: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + _address + ' src -j ' + _type.upper()) + public.ExecShell('ipset destroy ' + _address) + else: + is_debian = True if public.get_os_version().lower().find( + "debian") != -1 else False + if not is_debian: + public.ExecShell('ufw delete ' + rule1 + ' from ' + address + ' to any') + else: + cmd = "iptables -D INPUT -s " + address + " -j " + _type.upper( + ) + public.ExecShell(cmd) + # public.ExecShell('ufw delete ' + rule1 + ' from ' + _address + ' to any') + rule2 = "allow" if types == "accept" else "deny" + if address.find('-') != -1: + self.handle_ufw_ip(address, types) + else: + is_debian = True if public.get_os_version().lower().find( + "debian") != -1 else False + if not is_debian: + if rule2 == "allow": + if public.is_ipv6(address): + public.ExecShell('ufw ' + rule2 + ' from ' + address + ' to any') + else: + public.ExecShell('ufw insert 1 ' + rule2 + ' from ' + address + ' to any') + else: + public.ExecShell('ufw ' + rule2 + ' from ' + address + ' to any') + else: + public.ExecShell('iptables -I INPUT -s ' + address + + ' -j ' + types.upper()) + else: + if self.__isFirewalld: + if _address.find('-') != -1: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-rich-rule=\'rule source ipset="' + + _address + '" ' + _type + '\'') + public.ExecShell( + 'firewall-cmd --permanent --zone=public --delete-ipset=' + + _address) + else: + public.ExecShell( + 'firewall-cmd --permanent --remove-source=' + + _address + ' --zone=trusted') + if public.is_ipv6(address): + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv6 source address="' + + _address + '" ' + _type + '\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv4 source address="' + + _address + '" ' + _type + '\'') + if address.find('-') != -1: + brief = address + self.handle_firewall_ip(address, types) + else: + if types == "accept": + public.ExecShell( + 'firewall-cmd --permanent --add-source=' + + address + ' --zone=trusted') + else: + if public.is_ipv6(address): + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule=\'rule family=ipv6 source address="' + + address + '" ' + types + '\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --add-rich-rule=\'rule family=ipv4 source address="' + + address + '" ' + types + '\'') + else: + if _address.find('-') != -1: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + _address + ' src -j ' + types.upper()) + public.ExecShell('ipset destroy ' + _address) + else: + public.ExecShell('iptables -D INPUT -s ' + _address + + ' -j ' + _type.upper()) + if address.find('-') != -1: + self.handle_ufw_ip(address, types) + else: + public.ExecShell('iptables -I INPUT -s ' + address + + ' -j ' + types.upper()) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + public.M('firewall_ip').where('id=?', id).update( + {'address': address, 'types': types, 'brief': brief, 'addtime': addtime, 'sid': sid, 'domain': domain}) + if domain: + public.M('firewall_domain').where('id=?', (sid,)).save('sid,types,brief,domain_total', + (id, types, brief, domain_total)) + self.FirewallReload() + old_strategy = '' + if types == 'accept': + old_strategy = "accept" + elif types == 'drop': + old_strategy = "drop" + if get.types == 'accept': + strategy = "accept" + elif get.types == 'drop': + strategy = "drop" + public.WriteLog("system firewall", "Modify rules, IP:{}, Strategy:{} -> IP:{}, Strategy:{}".format(_address, old_strategy, get.address.strip(), get.types)) + # return public.returnMsg(True, 'Successful operation') + return public.return_message(0, 0, 'Successful operation') + + # 查看端口转发状态 + def trans_status(self): + content = dict() + with open(self._trans_status, 'r') as fr: + content = json.loads(fr.read()) + if content["status"] == "open": + return True + self.open_forward() + content["status"] = "open" + with open(self._trans_status, 'w') as fw: + fw.write(json.dumps(content)) + return True + + # 查询端口转发 + def get_forward_list(self, args): + + # 分页校验参数 + try: + args.validate([ + Param('limit').Integer(), + Param('p').Integer(), + Param('query').String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + result = self.trans_status() + + p = 1 + limit = 15 + if 'p' in args: p = args.p + if 'limit' in args: limit = args.limit + + where = '1=1' + sql = public.M('firewall_trans') + + if hasattr(args, 'query'): + where = " start_port like '%{search}%'".format(search=args.query) + + count = sql.where(where, ()).count() + data = public.get_page(count, int(p), int(limit)) + data['data'] = sql.where(where, ()).limit('{},{}'.format( + data['shift'], data['row'])).order('addtime desc').select() + return public.return_message(0, 0, data) + + # 添加端口转发 + def create_forward(self, get): + # {"protocol":"tcp","s_ports":"1234","d_address":"192.168.198.199","d_ports":"2345"} + # 校验参数 + try: + get.validate([ + # Param('id').Require().Integer().Xss(), + Param('s_ports').Require().Number(">=", 1).Number("<=", 65535), + Param('d_ports').Require().Number(">=", 1).Number("<=", 65535), + Param('d_address').Require().Ip(), + Param('protocol').Require().String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + s_port = get.s_ports.strip() # 起始端口 + d_port = get.d_ports.strip() # 目的端口 + d_ip = get.d_address.strip() # 目的ip + protocol = get.protocol + rep1 = r"^\d{1,5}(:\d{1,5})?$" + if not re.search(rep1, s_port): + # return public.returnMsg(False, 'PORT_CHECK_RANGE') + return public.return_message(-1, 0,'PORT_CHECK_RANGE') + if not re.search(rep1, d_port): + # return public.returnMsg(False, 'PORT_CHECK_RANGE') + return public.return_message(-1, 0, 'PORT_CHECK_RANGE') + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + if d_ip: + if not re.search(rep, get.d_address) and not public.is_ipv6( + get.d_address): + # return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + return public.return_message(-1, 0, 'FIREWALL_IP_FORMAT') + if d_ip in ["127.0.0.1", "localhost"]: + d_ip = "" + if public.M('firewall_trans').where("start_port=?", + (s_port, )).count() > 0: + # return public.returnMsg(False, 'This port already exists, please do not add it again!') + return public.return_message(-1, 0, 'This port already exists, please do not add it again!') + + if self.__isUfw: + content = self.ufw_handle_add(s_port, d_port, d_ip, protocol) + self.save_profile(self._ufw_before, content) + else: + if self.__isFirewalld: + self.firewall_handle_add(s_port, d_port, d_ip, protocol) + else: + self.iptables_handle_add(s_port, d_port, d_ip, protocol) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + public.M('firewall_trans').add( + 'start_port, ended_ip, ended_port, protocol, addtime', + (s_port, d_ip, d_port, protocol, addtime)) + self.FirewallReload() + public.WriteLog("system firewall", "Add port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {}".format(s_port, d_port, d_ip)) + # return public.returnMsg(True, 'ADD_SUCCESS') + return public.return_message(0, 0, 'ADD_SUCCESS') + + + # 删除端口转发 + def remove_forward(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + Param('s_ports').Require().Number(">=", 1).Number("<=", 65535), + Param('d_ports').Require().Number(">=", 1).Number("<=", 65535), + # Param('d_address').Require().Ip(), + Param('protocol').Require().String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + s_port = get.s_port + d_port = get.d_port + d_ip = get.d_ip + protocol = get.protocol + if self.__isUfw: + content = self.ufw_handle_del(s_port, d_port, d_ip, protocol) + self.save_profile(self._ufw_before, content) + else: + if self.__isFirewalld: + self.firewall_handle_del(s_port, d_port, d_ip, protocol) + else: + self.iptables_handle_del(s_port, d_port, d_ip, protocol) + public.M('firewall_trans').where("id=?", (id, )).delete() + self.FirewallReload() + public.WriteLog("system firewall", "Delete port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {}".format(s_port, d_port, d_ip)) + # return public.returnMsg(True, 'DEL_SUCCESS') + return public.return_message(0, 0, 'DEL_SUCCESS') + + # 修改端口转发 + def modify_forward(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + Param('s_ports').Require().Number(">=", 1).Number("<=", 65535), + Param('d_ports').Require().Number(">=", 1).Number("<=", 65535), + Param('d_address').Require().Ip(), + Param('protocol').Require().String('in', ['tcp', 'udp']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + id = get.id + s_port = get.s_ports.strip() + d_port = get.d_ports.strip() + d_ip = get.d_address.strip() + pool = get.protocol + rep1 = r"^\d{1,5}(:\d{1,5})?$" + if not re.search(rep1, s_port): + # return public.returnMsg(False, 'PORT_CHECK_RANGE') + return public.return_message(-1, 0, 'PORT_CHECK_RANGE') + if not re.search(rep1, d_port): + # return public.returnMsg(False, 'PORT_CHECK_RANGE') + return public.return_message(-1, 0, 'PORT_CHECK_RANGE') + data = public.M('firewall_trans').where('id=?', (id, )).field( + 'id,start_port,ended_ip,ended_port,protocol,addtime').find() + start_port = data.get("start_port", "") + ended_ip = data.get("ended_ip", "") + ended_port = data.get("ended_port", "") + protocol = data.get("protocol", "") + if d_ip: + rep = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$" + if not re.search(rep, get.d_address) and not public.is_ipv6( + get.d_address): + # return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + return public.return_message(-1, 0, 'FIREWALL_IP_FORMAT') + if d_ip in ["127.0.0.1", "localhost"]: + d_ip = "" + if self.__isUfw: + content = self.ufw_handle_update(start_port, ended_ip, ended_port, + protocol, s_port, d_ip, d_port, + pool) + self.save_profile(self._ufw_before, content) + else: + if self.__isFirewalld: + self.firewall_handle_update(start_port, ended_ip, ended_port, + protocol, s_port, d_ip, d_port, + pool) + else: + self.iptables_handle_update(start_port, ended_ip, ended_port, + protocol, s_port, d_ip, d_port, + pool) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + public.M('firewall_trans').where('id=?', id).update( + {'start_port': s_port, "ended_ip": d_ip, "ended_port": d_port, "protocol": pool}) + self.FirewallReload() + public.WriteLog("system firewall", "Modify port forwarding rules: Start port: {}, Destination port: {}, Destination IP: {} -> Start port: {}, Destination port: {}, Destination IP: {}".format(start_port, ended_port, ended_ip, s_port, d_port, d_ip)) + # return public.returnMsg(True, 'Successful operation.') + return public.return_message(0, 0, 'Successful operation') + + # 处理ufw的端口转发添加 + def ufw_handle_add(self, s_port, d_port, d_ip, protocol): + content = self.get_profile(self._ufw_before) + if content.find('*nat') == -1: + content = "*nat\n" + ":PREROUTING ACCEPT [0:0]\n" + ":POSTROUTING ACCEPT [0:0]\n" + "COMMIT\n" + content + array = content.split('\n') + result = array.index(":POSTROUTING ACCEPT [0:0]") + if d_ip == "": + if protocol.find('/') != -1: + _string = "-A PREROUTING -p tcp --dport {1} -j REDIRECT --to-port {2}\n".format( + s_port, d_port) + _string = _string + "-A PREROUTING -p udp --dport {1} -j REDIRECT --to-port {2}".format( + s_port, d_port) + else: + _string = "-A PREROUTING -p {0} --dport {1} -j REDIRECT --to-port {2}".format( + protocol, s_port, d_port) + else: + _string = "-A PREROUTING -p {0} --dport {1} -j DNAT --to-destination {2}:{3}\n".format( + protocol, s_port, d_ip, + d_port) + "-A POSTROUTING -d {0} -j MASQUERADE".format(d_ip) + array.insert(result + 1, _string) + return '\n'.join(array) + + # 处理ufw的端口转发删除 + def ufw_handle_del(self, s_port, d_port, d_ip, protocol): + content = self.get_profile(self._ufw_before) + if d_ip == "": + _string = "-A PREROUTING -p {0} --dport {1} -j REDIRECT --to-port {2}\n".format( + protocol, s_port, d_port) + else: + _string = "-A PREROUTING -p {0} --dport {1} -j DNAT --to-destination {2}:{3}\n".format( + protocol, s_port, d_ip, + d_port) + "-A POSTROUTING -d {0} -j MASQUERADE\n".format(d_ip) + content = content.replace(_string, "") + return content + + # 处理ufw的端口转发修改 + def ufw_handle_update(self, start_port, ended_ip, ended_port, protocol, + s_port, d_ip, d_port, pool): + content = self.get_profile(self._ufw_before) + if ended_ip == "": + s_string = "-A PREROUTING -p {0} --dport {1} -j REDIRECT --to-port {2}\n".format( + protocol, start_port, ended_port) + else: + s_string = "-A PREROUTING -p {0} --dport {1} -j DNAT --to-destination {2}:{3}\n".format( + protocol, start_port, ended_ip, ended_port + ) + "-A POSTROUTING -d {0} -j MASQUERADE\n".format(ended_ip) + if d_ip == "": + d_string = "-A PREROUTING -p {0} --dport {1} -j REDIRECT --to-port {2}\n".format( + pool, s_port, d_port) + else: + d_string = "-A PREROUTING -p {0} --dport {1} -j DNAT --to-destination {2}:{3}\n".format( + pool, s_port, d_ip, + d_port) + "-A POSTROUTING -d {0} -j MASQUERADE\n".format(d_ip) + content = content.replace(s_string, d_string) + return content + + # 处理firewall的端口转发添加 + def firewall_handle_add(self, s_port, d_port, d_ip, protocol): + if protocol.find('/') != -1: + public.ExecShell( + "firewall-cmd --permanent --zone=public --add-forward-port=port=" + + s_port + ":proto=tcp:toaddr=" + d_ip + ":toport=" + d_port + + "") + public.ExecShell( + "firewall-cmd --permanent --zone=public --add-forward-port=port=" + + s_port + ":proto=udp:toaddr=" + d_ip + ":toport=" + d_port + + "") + else: + cmd = "firewall-cmd --permanent --zone=public --add-forward-port=port=" + s_port + ":proto=" + protocol + ":toaddr=" + d_ip + ":toport=" + d_port + "" + public.ExecShell(cmd) + + # 处理firewall的端口转发删除 + def firewall_handle_del(self, s_port, d_port, d_ip, protocol): + if protocol.find('/') != -1: + public.ExecShell( + "firewall-cmd --permanent --zone=public --remove-forward-port=port=" + + s_port + ":proto=tcp:toaddr=" + d_ip + ":toport=" + d_port + + "") + public.ExecShell( + "firewall-cmd --permanent --zone=public --remove-forward-port=port=" + + s_port + ":proto=udp:toaddr=" + d_ip + ":toport=" + d_port + + "") + else: + public.ExecShell( + "firewall-cmd --permanent --zone=public --remove-forward-port=port=" + + s_port + ":proto=" + protocol + ":toaddr=" + d_ip + + ":toport=" + d_port + "") + + # 处理firewall的端口转发修改 + def firewall_handle_update(self, start_port, ended_ip, ended_port, + protocol, s_port, d_ip, d_port, pool): + if protocol.find('/') != -1: + public.ExecShell( + "firewall-cmd --permanent --zone=public --remove-forward-port=port=" + + start_port + ":proto=tcp:toaddr=" + ended_ip + ":toport=" + + ended_port + "") + public.ExecShell( + "firewall-cmd --permanent --zone=public --remove-forward-port=port=" + + start_port + ":proto=udp:toaddr=" + ended_ip + ":toport=" + + ended_port + "") + else: + public.ExecShell( + "firewall-cmd --permanent --zone=public --remove-forward-port=port=" + + start_port + ":proto=" + protocol + ":toaddr=" + ended_ip + + ":toport=" + ended_port + "") + if pool.find('/') != -1: + public.ExecShell( + "firewall-cmd --permanent --zone=public --add-forward-port=port=" + + s_port + ":proto=tcp:toaddr=" + d_ip + ":toport=" + d_port + + "") + public.ExecShell( + "firewall-cmd --permanent --zone=public --add-forward-port=port=" + + s_port + ":proto=udp:toaddr=" + d_ip + ":toport=" + d_port + + "") + else: + public.ExecShell( + "firewall-cmd --permanent --zone=public --add-forward-port=port=" + + s_port + ":proto=" + pool + ":toaddr=" + d_ip + ":toport=" + + d_port + "") + + # 处理iptables的端口转发添加 + def iptables_handle_add(self, s_port, d_port, d_ip, protocol): + if d_ip == "": + if protocol.find('/') != -1: + public.ExecShell( + "iptables -t nat -A PREROUTING -p tcp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + public.ExecShell( + "iptables -t nat -A PREROUTING -p udp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + public.ExecShell("iptables -t nat -A PREROUTING -p " + + protocol + " --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + if protocol.find('/') != -1: + public.ExecShell( + "iptables -t nat -A PREROUTING -p tcp --dport " + s_port + + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell( + "iptables -t nat -A PREROUTING -p udp --dport " + s_port + + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell( + "iptables -t nat -A POSTROUTING -j MASQUERADE") + else: + public.ExecShell( + "iptables -t nat -A PREROUTING -p " + protocol + " --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell("iptables -t nat -A POSTROUTING -j MASQUERADE") + return True + + # 处理iptables的端口转发删除 + def iptables_handle_del(self, s_port, d_port, d_ip, protocol): + if d_ip == "": + if protocol.find('/') != -1: + public.ExecShell( + "iptables -t nat -D PREROUTING -p tcp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + public.ExecShell( + "iptables -t nat -D PREROUTING -p udp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + public.ExecShell("iptables -t nat -D PREROUTING -p " + + protocol + " --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + if protocol.find('/') != -1: + public.ExecShell( + "iptables -t nat -D PREROUTING -p tcp --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell( + "iptables -t nat -D PREROUTING -p udp --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell("iptables -t nat -D POSTROUTING -j MASQUERADE") + else: + public.ExecShell( + "iptables -t nat -D PREROUTING -p " + protocol + " --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell("iptables -t nat -D POSTROUTING -j MASQUERADE") + return True + + # 处理iptables的端口转发删除 + def iptables_handle_update(self, start_port, ended_ip, ended_port, + protocol, s_port, d_ip, d_port, pool): + if ended_ip == "": + if protocol.find('/') != -1: + public.ExecShell( + "iptables -t nat -D PREROUTING -p tcp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + public.ExecShell( + "iptables -t nat -D PREROUTING -p udp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + public.ExecShell("iptables -t nat -D PREROUTING -p " + + protocol + " --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + if protocol.find('/') != -1: + public.ExecShell( + "iptables -t nat -D PREROUTING -p tcp --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell( + "iptables -t nat -D PREROUTING -p udp --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell("iptables -t nat -D POSTROUTING -j MASQUERADE") + else: + public.ExecShell( + "iptables -t nat -D PREROUTING -p " + protocol + " --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell("iptables -t nat -D POSTROUTING -j MASQUERADE") + if d_ip == "": + if pool.find('/') != -1: + public.ExecShell( + "iptables -t nat -A PREROUTING -p tcp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + public.ExecShell( + "iptables -t nat -A PREROUTING -p udp --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + public.ExecShell("iptables -t nat -A PREROUTING -p " + + protocol + " --dport " + s_port + + " -j REDIRECT --to-port " + d_port + '') + else: + if pool.find('/') != -1: + public.ExecShell( + "iptables -t nat -A PREROUTING -p tcp --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell( + "iptables -t nat -A PREROUTING -p udp --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell("iptables -t nat -A POSTROUTING -j MASQUERADE") + else: + public.ExecShell( + "iptables -t nat -A PREROUTING -p " + protocol + " --dport " + s_port + " -j DNAT --to-destination " + d_ip + ":" + d_port + '') + public.ExecShell("iptables -t nat -A POSTROUTING -j MASQUERADE") + return True + + # 开启端口转发 + def open_forward(self): + if self.__isUfw: + content1 = self.get_profile(self._ufw_default) + content2 = self.get_profile(self._ufw_sysctl) + content1 = content1.replace('DEFAULT_FORWARD_POLICY="DROP"', + 'DEFAULT_FORWARD_POLICY="ACCEPT"') + content2 = content2.replace('#net/ipv4/ip_forward=1', + 'net/ipv4/ip_forward=1') + self.save_profile(self._ufw_default, content1) + self.save_profile(self._ufw_sysctl, content2) + self.FirewallReload() + return True + if self.__isFirewalld: + public.ExecShell( + 'echo "\nnet.ipv4.ip_forward=1" >> /etc/sysctl.conf') + public.ExecShell('firewall-cmd --add-masquerade --permanent') + self.FirewallReload() + else: + public.ExecShell( + 'echo "\nnet.ipv4.ip_forward=1" >> /etc/sysctl.conf') + public.ExecShell('sysctl -p /etc/sysctl.conf') + self.FirewallReload() + return True + + # 开启或关闭端口转发 + def open_close_forward(self, get): + if not get.status in ["open", "close"]: + return public.returnMsg(False, 'Unknown control command!') + if self.__isUfw: + content1 = self.get_profile(self._ufw_default) + content2 = self.get_profile(self._ufw_sysctl) + if get.status == 'open': + content1 = content1.replace('DEFAULT_FORWARD_POLICY="DROP"', + 'DEFAULT_FORWARD_POLICY="ACCEPT"') + content2 = content2.replace('#net/ipv4/ip_forward=1', + 'net/ipv4/ip_forward=1') + else: + content1 = content1.replace('DEFAULT_FORWARD_POLICY="ACCEPT"', + 'DEFAULT_FORWARD_POLICY="DROP"') + content2 = content2.replace('net/ipv4/ip_forward=1', + '#net/ipv4/ip_forward=1') + self.save_profile(self._ufw_default, content1) + self.save_profile(self._ufw_sysctl, content2) + self.FirewallReload() + return public.returnMsg(True, + 'Enable' if get.status == "open" else "Disable") + if self.__isFirewalld: + if get.status == 'open': + public.ExecShell('firewall-cmd --add-masquerade --permanent') + else: + public.ExecShell( + 'firewall-cmd --remove-masquerade --permanent') + self.FirewallReload() + else: + public.ExecShell( + 'echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf') + public.ExecShell('sysctl -p /etc/sysctl.conf') + return public.returnMsg(True, "Turn off port forwarding") + + def get_host_ip(self): + """ + 查询本机ip地址 + :return: + """ + try: + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + finally: + s.close() + + return ip + + def load_white_list(self): + try: + if not self._white_list: + ip_data = self.get_profile(self._white_list_file) + white_list_ips = json.loads(ip_data) + white_list = [] + for ip_obj in white_list_ips: + white_list += ip_obj["ips"] + self._white_list = white_list + # public.WriteLog("firewall_debug", str(white_list)) + return self._white_list + except Exception as e: + public.WriteLog("firewall", "Failed to load whitelist!") + return [] + + def verify_ip(self, ip_entry): + """检查规则IP是否和内网IP重叠""" + try: + try: + import IPy + except: + ipy_tips = '/tmp/bt_ipy.pl' + if not os.path.exists(ipy_tips): + os.system("nohup btpip install IPy &>/dev/null &") + public.WriteFile(ipy_tips, 'True') + + release_ips = [ + IPy.IP("127.0.0.1"), + IPy.IP("172.16.1.1"), + IPy.IP("10.0.0.1"), + IPy.IP("192.168.0.0"), + IPy.IP(self.get_host_ip()) + ] + + white_list = self.load_white_list() + + release_ips += white_list + + ip = IPy.IP(ip_entry, make_net=True) + for rip_obj in release_ips: + overlap = ip.overlaps(rip_obj) + if overlap > 0: + return False + return True + except: + return False + + def handle_firewall_country(self, brief, ip_list, types, port_list): + try: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --new-ipset=' + brief + + ' --type=hash:net') + xml_path = "/etc/firewalld/ipsets/%s.xml" % brief + tree = ElementTree() + tree.parse(xml_path) + root = tree.getroot() + for ip in ip_list: + if self.verify_ip(ip): + entry = Element("entry") + entry.text = ip + root.append(entry) + self.format(root) + tree.write(xml_path, 'utf-8', xml_declaration=True) + if port_list: + for port in port_list: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-rich-rule=\'rule source ipset="' + + brief + '" port port="' + port + '" protocol=tcp ' + + types + '\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --add-rich-rule=\'rule source ipset="' + + brief + '" ' + types + '\'') + except Exception as e: + return {"status": "error", "msg": e} + + def handle_ufw_country(self, brief, ip_list, types, port_list): + tmp_path = '/tmp/firewall_tmp.sh' + tmp_file = open(tmp_path, 'w') + _string = "#!/bin/bash\n" + for ip in ip_list: + if self.verify_ip(ip): + _string = _string + 'ipset add ' + brief + ' ' + ip + '\n' + tmp_file.write(_string) + tmp_file.close() + public.ExecShell('ipset create ' + brief + + ' hash:net; /bin/bash /tmp/firewall_tmp.sh') + if port_list: + for port in port_list: + public.ExecShell('iptables -I INPUT -m set --match-set ' + + brief + ' src -p tcp --destination-port ' + + port + ' -j ' + types.upper()) + else: + public.ExecShell('iptables -I INPUT -m set --match-set ' + brief + + ' src -j ' + types.upper()) + + # 查询区域规则 + def get_country_list(self, args): + + # 分页校验参数 + try: + args.validate([ + Param('limit').Integer(), + Param('p').Integer(), + Param('query').String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + p = 1 + limit = 15 + if 'p' in args: p = args.p + if 'limit' in args: limit = args.limit + + where = '1=1' + sql = public.M('firewall_country') + + if hasattr(args, 'query'): + where = " country like '%{search}%' or brief like '%{search}%'".format( + search=args.query) + + count = sql.where(where, ()).count() + data = public.get_page(count, int(p), int(limit)) + data['data'] = sql.where(where, ()).limit('{},{}'.format( + data['shift'], data['row'])).order('addtime desc').select() + # return data + return public.return_message(0, 0, data) + + def create_countrys(self, get): + try: + if not hasattr(get, 'country'): + return public.returnMsg(False, 'Please enter the country name!') + input_country = get.country + countrys = self.get_countrys(None) + countrys = countrys[1:] + + # 2024/1/6 下午 5:00 获取防火墙状态,如果没有启动则启动防火墙 + if not self.get_firewall_status(): + get.status = "start" + self.firewall_admin(get) + + if "Except China" in input_country: + input_country = [i['CH'] for i in countrys if not "China" in i['CH']] + countrys_dict = {i['CH']: i['brief'] for i in countrys} + content = self.get_profile(self._ips_path) + + for i in input_country: + get.brief = countrys_dict.get(i, None) + get.country = i + self.create_country(get, True, content) + else: + countrys_dict = {i['CH']: i['brief'] for i in countrys} + if isinstance(input_country, str): + input_country = [input_country] + for i in input_country: + get.brief = countrys_dict.get(i, None) + get.country = i + self.create_country(get, True) + get.status = "restart" + self.firewall_admin(get) + return public.returnMsg(True, 'Added successfully') + except: + print(traceback.format_exc()) + return public.returnMsg(False, 'Add failed') + + # 添加区域规则 + def create_country(self, get, is_mutil=False, _ips_paths=None): + # {"country": "US Virgin Islands", "types": "drop", "choose": "all", "ports": "", "brief": "VI"} 暂时只有封锁和地区可选 + # 校验参数 + try: + get.validate([ + Param('country').Require().Xss(), + Param('types').Require().String('in', ['accept', 'drop']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + brief = get.brief + types = get.types # types in [accept, drop] + ports = get.ports + country = get.country + rep = r"^\d{1,5}(:\d{1,5})?$" + port_list = [] + + # 检测该区域是否已添加过全部端口规则 hezhihong + add_list = public.M('firewall_country').where("country=?", (country,)).field('ports').select() + + for add in add_list: + if not add['ports']: + # return public.returnMsg(False, 'This area has already been added, please do not add it again!') + return public.return_message(-1, 0, 'This area has already been added, please do not add it again!') + if ports: + port_list = ports.split(',') + for port in port_list: + if not re.search(rep, port): + # return public.returnMsg(False, 'PORT_CHECK_RANGE') + return public.return_message(-1, 0, 'PORT_CHECK_RANGE') + if public.M('firewall_country').where( + "country=? and ports=?", (country, port)).count() > 0: + public.print_log('############ 地区接口8{}'.format(port)) + # return public.returnMsg(False, 'This area has already been added, please do not add it again!') + return public.return_message(-1, 0, 'This area has already been added, please do not add it again!') + self.get_os_info() + if _ips_paths is None: + content = self.get_profile(self._ips_path) + else: + content = _ips_paths + result = json.loads(content) + ip_list = [] + for r in result: + if brief == r["brief"]: + ip_list = r["ips"] + break + if not ip_list: + public.print_log('############ 地区接口7'.format()) + # return public.returnMsg(True, "Please enter the correct area name!") + return public.return_message(0, 0, 'Please enter the correct area name!') + if self.__isUfw: + self.handle_ufw_country(brief, ip_list, types, port_list) + else: + if self.__isFirewalld: + result = self.handle_firewall_country(brief, ip_list, types, + port_list) + if result: + public.print_log('############ 地区接口6{}'.format(result)) + # return result + return public.return_message(0, 0, result) + else: + self.handle_ufw_country(brief, ip_list, types, port_list) + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + if port_list: + for port in port_list: + public.M('firewall_country').add( + 'country,types,brief,ports,addtime', + (country, types, brief, port, addtime)) + else: + public.M('firewall_country').add( + 'country,types,brief,ports,addtime', + (country, types, brief, '', addtime)) + if is_mutil is False: + # self.FirewallReload() + get.status = "restart" + self.firewall_admin(get) + if not get.ports: + log_port = "All ports" + else: + log_port = get.ports + # strategy = '' + # if get.types == 'accept': + # strategy = "accept" + # elif get.types == 'drop': + # strategy = "drop" + # public.print_log('############ 地区接口5'.format()) + public.WriteLog("system firewall", "Add regional rules: Region:{}, Policy:{}, Port:{}".format(get.country, get.types, log_port)) + # return public.returnMsg(True, 'ADD_SUCCESS') + return public.return_message(0, 0, 'ADD_SUCCESS') + + # 删除区域规则 + def remove_country(self, get): + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer(), + Param('country').Require().Xss(), + Param('types').Require().String('in', ['accept', 'drop']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + id = get.id + types = get.types + brief = get.brief + ports = get.ports + country = get.country + reload = True + if "not_reload" in get: + reload = get.not_reload.lower() == "true" + public.M('firewall_country').where("id=?", (id, )).delete() + if self.__isUfw: + if not ports: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + brief + ' src -j ' + types.upper()) + else: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + brief + ' src -p tcp --destination-port ' + + ports + ' -j ' + types.upper()) + if not public.M('firewall_country').where("country=?", + (country, )).count() > 0: + public.ExecShell('ipset destroy ' + brief) + else: + if self.__isFirewalld: + if not ports: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-rich-rule=\'rule source ipset="' + + brief + '" ' + types + '\'') + else: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --remove-rich-rule=\'rule source ipset="' + + brief + '" port port="' + ports + '" protocol=tcp ' + + types + '\'') + if not public.M('firewall_country').where( + "country=?", (country, )).count() > 0: + public.ExecShell( + 'firewall-cmd --permanent --zone=public --delete-ipset=' + + brief) + else: + if not ports: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + brief + ' src -j ' + types.upper()) + else: + public.ExecShell('iptables -D INPUT -m set --match-set ' + + brief + + ' src -p tcp --destination-port ' + + ports + ' -j ' + types.upper()) + if not public.M('firewall_country').where( + "country=?", (country, )).count() > 0: + public.ExecShell('ipset destroy ' + brief) + if reload: + get.status = "restart" + self.firewall_admin(get) + if not get.ports: + log_port = "All ports" + else: + log_port = get.ports + strategy = '' + if get.types == 'accept': + strategy = "accept" + elif get.types == 'drop': + strategy = 'drop' + public.WriteLog("system firewall", "Delete zone rules: Region:{}, Policy:{}, Port:{}".format(get.country, strategy, log_port)) + # return public.returnMsg(True, 'DEL_SUCCESS') + return public.return_message(0, 0, 'DEL_SUCCESS') + + # 编辑区域规则 + def modify_country(self, get): + # {"id":17,"country":"Vanuatu","types":"drop","choose":"all","ports":"","brief":"VU "} + # 校验参数 + try: + get.validate([ + Param('id').Require().Integer().Xss(), + Param('country').Require().Xss(), + Param('types').Require().String('in', ['accept', 'drop']), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + # 2022/11/24 修复编辑地区端口规则问题 lx + id = get.id + # types = get.types + # brief = get.brief + # country = get.country + data = public.M('firewall_country').where( + 'id=?', + (id, )).field('id,country,types,brief,ports,addtime').find() + ori_get = public.dict_obj() + ori_get.id = id + ori_get.types = data.get("types", "") + ori_get.brief = data.get("brief", "") + ori_get.country = data.get("country", "") + ori_get.ports = data.get("ports", "") + ori_get.not_reload = "true" + rm_res = self.remove_country(ori_get) + if rm_res["status"]: + create_res = self.create_country(get) + if create_res["status"]: + # return public.returnMsg(True, "Successful operation") + return public.return_message(0, 0, "Successful operation") + # return public.returnMsg(False, "operation failed") + return public.return_message(0, 0, "operation failed") + + # 获取服务端列表:centos + def GetList(self): + try: + result, arry = self.__Obj.GetAcceptPortList() + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + for i in range(len(result)): + if "address" not in result[i].keys(): continue + tmp = self.check_db_exists(result[i]['ports'], + result[i]['address'], + result[i]['types']) + protocol = result[i]['protocol'] + ports = result[i]['ports'] + types = result[i]['types'] + address = result[i]['address'] + if not tmp: + if ports: + public.M('firewall_new').add( + 'ports,protocol,address,types,brief,addtime', + (ports, protocol, address, types, '', addtime)) + else: + public.M('firewall_ip').add( + 'address,types,brief,addtime', + (address, types, '', addtime)) + for i in range(len(arry)): + if arry[i]['port']: + tmp = self.check_trans_data(arry[i]['port']) + protocol = arry[i]['protocol'] + s_port = arry[i]['port'] + d_port = arry[i]['to-port'] + address = arry[i]['address'] + if not tmp: + public.M('firewall_trans').add( + 'start_port,ended_ip,ended_port,protocol,addtime', + (s_port, address, d_port, protocol, addtime)) + except Exception as e: + file = open('error.txt', 'w') + return public.returnMsg(False, e) + + # 获取服务端列表:ufw + def get_ufw_list(self): + data = public.M('firewall').field('id,port,ps,addtime').select() + if type(data) != list: return + try: + for dt in data: + port = dt['port'] + brief = dt['ps'] + addtime = dt['addtime'] + if port.find('.') != -1: + tmp = self.check_db_exists('', port, 'drop') + if not tmp: + public.M('firewall_ip').add('address,types,brief,addtime', + (port, 'drop', '', addtime)) + else: + tmp = self.check_db_exists(port, '', 'accept') + if not tmp: + public.M('firewall_new').add( + 'ports,brief,protocol,address,types,addtime', + (port, brief, 'tcp/udp', '', 'accept', addtime)) + except: + pass + + # 检查数据库是否存在 + def check_db_exists(self, ports, address, types): + if ports: + data = public.M('firewall_new').field( + 'id,ports,protocol,address,types,brief,addtime').select() + for dt in data: + if dt['ports'] == ports: return dt + return False + else: + data = public.M('firewall_ip').field( + 'id,address,types,brief,addtime').select() + for dt in data: + if dt["address"] == address and dt["types"] == types: return dt + return False + + def check_trans_data(self, ports): + data = public.M('firewall_trans').field( + 'id,start_port,ended_ip,ended_port,protocol,addtime').select() + for dt in data: + if dt['start_port'] == ports: return dt + return False + + # 规则导出:服务器 todo + def export_rules(self, get): + rule_name = get.rule_name + arry = [] + data_list = None + filename = '' + if rule_name == "port_rule": + filename = self._rule_path + "port.json" + data_list = public.M('firewall_new').order("id desc").select() + elif rule_name == "ip_rule": + filename = self._rule_path + "ip.json" + data_list = public.M('firewall_ip').order("id desc").select() + elif rule_name == "trans_rule": + filename = self._rule_path + "forward.json" + data_list = public.M('firewall_trans').order("id desc").select() + elif rule_name == "country_rule": + filename = self._rule_path + "country.json" + data_list = public.M('firewall_country').order("id desc").select() + if not data_list: + data_list = [] + # 将数据格式换成以|分割的字符串 hezhihong + write_string = "" + if data_list: + for i in data_list: + for v in i.keys(): + if v == 'domain': i[v] = i[v].replace('|', '#') + write_string += str(i[v]) + "|" + write_string += '\n' + public.writeFile(filename, write_string) + public.WriteLog("system firewall", "导出端口规则") + return public.returnMsg(True, filename) + + # 规则导出:本地 todo + def get_file(self, args): + filename = args.filename + mimetype = "application/octet-stream" + if not os.path.exists(filename): + return abort(404) + return send_file(filename, + mimetype=mimetype, + as_attachment=True, + attachment_filename=os.path.basename(filename), + cache_timeout=0) + + # 规则导入:json todo + def import_rules(self, get): + try: + rule_name = get.rule_name # 规则名:[port_rule, ip_rule, trans_rule, country_rule] + file_name = get.file_name # 文件命:[port.json, ip.json, trans.json, country.json] + file_path = "{0}{1}".format(self._rule_path, file_name) + data_list = self.get_profile(file_path) + pay = self.__check_auth() + not_pay_list = [] + tmp_data = [] + # |分隔符格式文件导入 hezhihong + if data_list and isinstance(data_list, str): + + if data_list.find('|') != -1: + data_list = data_list.split('\n') + for data in data_list: + if not data: continue + split_data = data.split('|') + data_dict = {} + data_dict['id'] = split_data[0] + if rule_name == 'port_rule': + if not pay and split_data[7].find('#') != -1: + not_pay_list.append(data) + continue + data_dict['protocol'] = split_data[1] + data_dict['ports'] = split_data[2] + data_dict['types'] = split_data[3] + data_dict['address'] = split_data[4] + data_dict['brief'] = split_data[5] + data_dict['addtime'] = split_data[6] + data_dict['domain'] = split_data[7] + elif rule_name == 'ip_rule': + if not pay and split_data[6].find('#') != -1: + not_pay_list.append(data) + continue + data_dict['types'] = split_data[1] + data_dict['address'] = split_data[2] + data_dict['brief'] = split_data[3] + data_dict['addtime'] = split_data[4] + data_dict['domain'] = split_data[6] + elif rule_name == 'trans_rule': + data_dict['start_port'] = split_data[1] + data_dict['ended_ip'] = split_data[2] + data_dict['ended_port'] = split_data[3] + data_dict['protocol'] = split_data[4] + data_dict['addtime'] = split_data[5] + elif rule_name == 'country_rule': + data_dict['types'] = split_data[1] + data_dict['country'] = split_data[2] + data_dict['brief'] = split_data[3] + data_dict['addtime'] = split_data[4] + data_dict['ports'] = split_data[5] + tmp_data.append(data_dict) + data_list = tmp_data + # 一行一条规则格式文件导入 hezhihong + if data_list and isinstance(data_list, str): + data_list = data_list.strip() + if isinstance(data_list, str) and data_list.find('\n') != -1: + data_list = data_list.split('\n') + try: + data_list.remove('') + except: + pass + if isinstance(data_list, str): + try: + data_list = json.loads(data_list) + except: + if os.path.exists(file_path): + os.remove(file_path) + return public.ReturnMsg(False, "The file content is incorrect!!") + if data_list: + if isinstance(data_list, dict): + data_list = [data_list] + if not isinstance(data_list, list): + if os.path.exists(file_path): + os.remove(file_path) + return public.ReturnMsg(False, "The file content is incorrect!!") + if len(data_list) == 0: + return public.ReturnMsg(False, "The file is empty!") + result = self.hand_import_rules(rule_name, data_list) + os.remove(file_path) + if not_pay_list: + not_pay_list = ("
                                    " + "-" * 20 + "
                                    ").join(not_pay_list) + return public.ReturnMsg( + result["status"], + "{}
                                    The designated domain name function is exclusive to the Enterprise Edition, and the following rules are not imported:
                                    {}".format(result["msg"], not_pay_list) + ) + public.WriteLog("system firewall", "Import port rules") + return public.ReturnMsg(result["status"], result["msg"]) + except Exception: + return public.ReturnMsg(False, "The import failed. The format of the rules is wrong. Please try again according to the format of the export rules!") + + # 处理规则导入,读取json文件内容 + def hand_import_rules(self, rule_name, data_list): + table_head = [] + try: + if rule_name == "port_rule": + table_head = ["id", "protocol", "ports", "types", "address", "brief", "addtime", "domain", ] + for data in data_list: + #兼容一行一条规则格式文件导入 hezhihong + try: + data = json.loads(data) + except: + pass + + res = all([field in data.keys() for field in table_head]) + if not res or len(table_head) != len(data.keys()): + return {"status": False, "msg": "The data format is incorrect!"} + get = public.dict_obj() + get.protocol = data["protocol"] + get.ports = data["ports"] + get.types = data["types"] + get.source = data["address"] + get.brief = data["brief"] + # 兼容域名导入 hezhihong + if 'domain' in data.keys() and data['domain']: + get.domain = data['domain'].split('#')[0] + get.source = data['domain'].split('#')[1] + result = self.create_rules(get) + if not result["status"]: + continue + elif rule_name == "ip_rule": + table_head = ["id", "types", "address", "brief", "addtime"] + for data in data_list: + # 兼容一行一条规则格式文件导入 hezhihong + try: + data = json.loads(data) + except: + pass + res = all([field in data.keys() for field in table_head]) + if not res: + return {"status": False, "msg": "The data format is incorrect!"} + get = public.dict_obj() + get.types = data["types"] + get.address = data["address"] + get.brief = data["brief"] + # 兼容域名导入 hezhihong + if 'domain' in data.keys() and data['domain']: + get.domain = data['domain'].split('#')[0] + get.source = data['domain'].split('#')[1] + result = self.create_ip_rules(get) + if not result["status"]: + continue + elif rule_name == "trans_rule": + table_head = [ + "id", "start_port", "ended_ip", "ended_port", "protocol", + "addtime" + ] + for data in data_list: + # 兼容一行一条规则格式文件导入 hezhihong + try: + data = json.loads(data) + except: + pass + res = all([field in data.keys() for field in table_head]) + if not res: + return {"status": False, "msg": "The data format is incorrect!"} + get = public.dict_obj() + get.s_ports = data["start_port"] + get.d_address = data["ended_ip"] + get.d_ports = data["ended_port"] + get.protocol = data["protocol"] + result = self.create_forward(get) + if not result["status"]: + continue + elif rule_name == "country_rule": + table_head = ["id", "types", "country", "brief", "addtime", "ports"] + for data in data_list: + # 兼容一行一条规则格式文件导入 hezhihong + try: + data = json.loads(data) + except: + pass + res = all([field in data.keys() for field in table_head]) + if not res: + return {"status": False, "msg": "The data format is incorrect!"} + get = public.dict_obj() + get.types = data["types"] + get.ports = data["ports"] + get.brief = data["brief"] + get.country = data["country"] + result = self.create_country(get) + if not result["status"]: + continue + except: + return {"status": False, "msg": "Import failed!"} + return {"status": True, "msg": "Imported successfully!"} + + def get_countrys(self, get): + result = [] + content = self.get_profile(self._country_path) + result = json.loads(content) + result = sorted(result, key=lambda x : x['CH'], reverse=True); + + if isinstance(result, list): + result.insert(0, {"CH": "Except China", "brief": "OTHER"}) + return result + + # 读取配置文件 + def get_profile(self, path): + + if not os.path.exists(path): + b_path = os.path.dirname(path) + if not os.path.exists(b_path): os.makedirs(b_path) + + if path in [ + self._ips_path, self._country_path, self._white_list_file + ]: + public.downloadFile( + 'https://download.bt.cn/install/lib/{}'.format( + os.path.basename(path)), path) + + content = "" + with open(path, "r") as fr: + content = fr.read() + return content + + # 保存配置文件 + def save_profile(self, path, data): + with open(path, "w") as fw: + fw.write(data) + + # 读取配置文件 + def update_profile(self, path): + import files + f = files.files() + return f.GetFileBody(path) + + # 获取端口规则列表 + def get_port_rules(self, get): + rule_list = public.M('firewall_new').order("id desc").select() + # return public.returnMsg(True, rule_list) + return public.return_message(0, 0, rule_list) + + # 整理配置文件格式 + def format(self, em, level=0): + i = "\n" + level * " " + if len(em): + if not em.text or not em.text.strip(): + em.text = i + " " + for e in em: + self.format(e, level + 1) + if not e.tail or not e.tail.strip(): + e.tail = i + if level and (not em.tail or not em.tail.strip()): + em.tail = i + + def check_table(self): + if public.M('sqlite_master').where('type=? AND name=?', + ('table', 'firewall_new')).count(): + if public.M('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_ip')).count(): + if public.M('sqlite_master').where( + 'type=? AND name=?', + ('table', 'firewall_trans')).count(): + if public.M('sqlite_master').where( + 'type=? AND name=?', + ('table', 'firewall_country')).count(): + return True + return Sqlite() + + def delete_service(self): + if self.__isUfw: + public.ExecShell('ufw delete allow ssh') + else: + if self.__isFirewalld: + public.ExecShell( + 'firewall-cmd --zone=public --remove-service=ssh --permanent' + ) + else: + pass + return True + + # 获取系统类型(具体到哪个版本) + def get_os_info(self): + tmp = {"osname": "", "version": ""} + if os.path.exists('/etc/redhat-release'): + sys_info = public.ReadFile('/etc/redhat-release') + elif os.path.exists('/usr/bin/yum'): + sys_info = public.ReadFile('/etc/issue') + elif os.path.exists('/etc/issue'): + sys_info = public.ReadFile('/etc/issue') + try: + tmp['osname'] = sys_info.split()[0] + tmp['version'] = re.search(r'\d+(\.\d*)*', sys_info).group() + except: + os_result = public.ExecShell(". /etc/os-release && echo $ID")[0] + if "amzn" == os_result: + tmp['osname'] = 'CentOS' + tmp['version'] = '8' + if tmp["osname"] == "CentOS": + if tmp["version"].startswith("8"): + content = self.get_profile("/etc/firewalld/firewalld.conf") + content = content.replace("FirewallBackend=nftables", "FirewallBackend=iptables") + self.save_profile("/etc/firewalld/firewalld.conf", content) + public.ExecShell("systemctl restart firewalld") + return True + + # 新加代码----- start + + def sync_must_ports(self, get): + ''' + 同步必须放行的端口 + @param get: + @return: + ''' + # 检查必传参数 + try: + public.exists_args('ports', get) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(0, 0, str(ex)) + + protocol = "tcp" + ports = get.ports.strip() + print(ports) + if not ports: return public.returnMsg(False, 'Port cannot be empty!') + port_list = ports.split(",") if ports.find(",") != -1 else [ports] + types = "accept" + check_result = self.check_port(port_list) + if check_result: return check_result + + firewall_type = 'iptables' + if self.__isFirewalld: firewall_type = 'firewalld' + if self.__isUfw: firewall_type = 'ufw' + + try: + # 2024/1/6 下午 5:00 获取防火墙状态,如果没有启动则启动防火墙 + if not self.get_firewall_status(): + get = public.dict_obj() + get.status = 1 + self.firewall_admin(get) + + for port in port_list: + if firewall_type == 'firewalld': + if port.find(':') != -1: port = port.replace(':', '-') + self.add_firewall_rule("", protocol, port, types) + elif firewall_type == 'ufw': + if port.find('-') != -1: port = port.replace('-', ':') + self.add_ufw_rule("", protocol, port, types) + else: + self.add_iptables_rule("", protocol, port, types) + + query_result = public.M('firewall_new').where( + 'ports=? and address=? and protocol=? and types=?', + (port, "", protocol, types) + ).find() + print(query_result) + if query_result: continue + + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + self._add_sid = public.M('firewall_new').add( + 'ports,brief,protocol,address,types,addtime,domain,sid', + (port, "", protocol, "", types, addtime, "", 0) + ) + # return public.returnMsg(True, 'ADD_SUCCESS') + return public.return_message(0, 0, "ADD_SUCCESS") + except Exception: + print(traceback.format_exc()) + # return public.returnMsg(False, 'ADD_ERROR') + return public.return_message(0, 0, "ADD_ERROR") + + def _get_webserver(self): + ''' + 获取web服务器类型 + @return: + ''' + webserver = '' + if os.path.exists('/www/server/nginx/sbin/nginx'): + webserver = 'nginx' + elif os.path.exists('/www/server/apache/bin/httpd'): + webserver = 'apache' + elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): + webserver = 'lswsctrl' + return webserver + + def get_port_info(self, get): + ''' + 获取面板防火墙关键服务端口放行状态信息 + 判断服务是否存在,能读取文件就读取文件,配置文件不大不会影响性能,这种方式能最大缩短接口响应时间,公网测试80ms + @param get: + @return: + ''' + ports_list = [] + result_list = [{"name": "FTP passive port", "status": 0, "port": "39000-40000"}] + + webserver = self._get_webserver() + if webserver in ['nginx', 'apache', 'lswsctrl']: + result_list.append({"name": "website port", "status": 0, "port": "80"}) + ports_list.append("80") + + port_443, _ = public.ExecShell("fuser -n tcp 443") + if port_443: + result_list.append({"name": "HTTPS port", "status": 0, "port": "443"}) + ports_list.append("443") + + _panel_port_file = '/www/server/panel/data/port.pl' + panel_port = public.readFile(_panel_port_file).strip(" ").strip("\n") + + cmd = "cat /www/server/pure-ftpd/etc/pure-ftpd.conf |grep Bind|awk -F ',' '{print $2}'" + ftp_port = public.ExecShell(cmd)[0].strip(" ").strip("\n").strip("\r") + + cmd = "cat /etc/ssh/sshd_config |grep -E '^Port'|awk '{print $2}'|awk 'NR == 1'" + ssh_port = public.ExecShell(cmd)[0].strip(" ").strip("\n") + + if ssh_port == "": ssh_port = "22" + ports_list.append(panel_port) + result_list.append({"name": "panel", "status": 0, "port": panel_port}) + ports_list.append(ftp_port) + result_list.append({"name": "FTP active port", "status": 0, "port": ftp_port}) + ports_list.append(ssh_port) + result_list.append({"name": "SSH", "status": 0, "port": ssh_port}) + ports_list.append("39000-40000") + + if self.__isUfw: + # return self._get_ufw_port_status(ports_list, result_list) + list1 = self._get_ufw_port_status(ports_list, result_list) + return public.return_message(0, 0, list1) + if self.__isFirewalld: + # return self._get_firewall_port_status(ports_list, result_list) + list1 = self._get_firewall_port_status(ports_list, result_list) + return public.return_message(0, 0, list1) + # return {} + return public.return_message(0, 0, {}) + + def _get_firewall_port_status(self, ports_list, result_list): + ''' + 获取firewalld防火墙端口状态 + @param ports_list: + @param result_list: + @return: + ''' + with contextlib.suppress(Exception): + _firewalld_ports, _ = self.__firewall_obj.GetAcceptPortList() + # print("_firewalld_ports: ", _firewalld_ports) + for firewalld_port in _firewalld_ports: + if firewalld_port['ports'] in ports_list: + for result in result_list: + if result['port'] == firewalld_port['ports']: + result['status'] = 1 + break + return result_list + + def _get_ufw_port_status(self, ports_list, result_list): + ''' + 获取ufw防火墙端口状态 + @param ports_list: + @param result_list: + @return: + ''' + with contextlib.suppress(Exception): + rules_result = self._get_ufw_port_info() + # print("rules_result: ", rules_result) + ports_set = set(ports_list) # 将要查找的端口列表转换成集合,以便进行高效查找 + + for rule in rules_result: + if 'tcp' in rule['protocol'] and rule['ports'] in ports_set: + for result in result_list: + if result['port'] == rule['ports']: + result['status'] = 1 + ports_set.remove(rule['ports']) + break + + if not ports_set: break + return result_list + + def _get_ufw_port_info(self): + ''' + 获取ufw防火墙端口信息 + @return: + ''' + with open('/etc/ufw/user.rules', 'r') as f: + content = f.read() + start_index = content.find('### RULES ###') + end_index = content.find('### END RULES ###') + result = content[start_index + 15:end_index] + sys_rules = [rule for rule in result.split('\n') if rule != '' and '###' in rule] + # 将sys_rules列表中的每个元素拆分出来,并且去掉空格,元素为字符串,例如:'### tuple ### allow tcp 20 0.0.0.0/0 any 0.0.0.0/0 in' + # 拆分后的列表元素为:{'protocol': 'tcp', 'ports': '20', 'types': 'allow', 'address': '0.0.0.0'} + rules = [] + for rule in sys_rules: + rule = rule.split(' ') + rule = [i for i in rule if i != ''] + rules.append({ + 'protocol': rule[4] if rule[4] != 'any' else 'tcp/udp', + 'ports': rule[5] if rule[5].find(':') == -1 else rule[5].replace(':', '-'), + 'types': 'accept' if rule[3] == 'allow' else 'drop', + 'address': rule[8] if rule[8] != '0.0.0.0/0' else '' + }) + + unique_set = set(tuple(sorted(item.items())) for item in rules) + rules = [dict(item) for item in unique_set] + return rules + + @staticmethod + def get_listening_processes(get): + ''' + 获取指定端口的进程信息 + @param get: + @return: + ''' + + # 校验参数 + try: + get.validate([ + Param('port').Require().Number(">=", 1).Number("<=", 65535).Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + print("get.get_items().keys(): ", get.get_items().keys()) + + if get.port.find('-') != -1 or get.port.find(':') != -1: + # return public.returnMsg(False, 'Range ports not supported') + return public.return_message(-1, 0, 'Range ports not supported') + + + process_name = '' + process_pid = '' + process_cmd = '' + + cmd = "lsof -i:{}|grep LISTEN|grep -v COMMAND".format(get.port) + "|awk '{print $1,$2}'" + info_list = public.ExecShell(cmd)[0].split("\n")[0].split(" ") + if len(info_list) == 2: + process_name = info_list[0] + process_pid = info_list[1] + cmd_ps = "ps aux|grep {}|grep -v grep".format(process_pid) + cmd_awk = "|awk '{print $11,$12,$13,$14}'" + process_cmd = public.ExecShell(cmd_ps + cmd_awk)[0].split("\n")[0].strip(" ") + + # return { + # "process_name": process_name, + # "process_pid": process_pid, + # "process_cmd": process_cmd + # } + + return public.return_message(0, 0, { + "process_name": process_name, + "process_pid": process_pid, + "process_cmd": process_cmd + }) + + def get_diff_panel_firewall_rules(self, get): + ''' + 对比面板防火墙规则数据库和防火墙配置文件,取出差异的规则 + @return: + ''' + # 获取面板防火墙规则数据库 + panel_firewall_rules = self.get_panel_firewall_rules() + # 获取防火墙配置文件 + firewall_rules = self.get_sys_firewall_rules() + # 取出差异的规则 + diff_rules = self._get_diff_rules(panel_firewall_rules, firewall_rules) + # return diff_rules + return public.return_message(0, 0, diff_rules) + + def get_panel_firewall_rules(self): + ''' + 获取面板防火墙规则数据库 + @return: + ''' + all_ports = public.M('firewall_new').field('protocol,ports,types,address').order('addtime desc').select() + unique_set = set(tuple(sorted(item.items())) for item in all_ports) + new_ports = [dict(item) for item in unique_set] + return new_ports + + def get_sys_firewall_rules(self): + ''' + 获取防火墙配置文件 + @return: + ''' + if self.__isUfw: return self._get_ufw_port_info() + if self.__isFirewalld: return self.__firewall_obj.recombine_rules() + return [] + + def _diff_dict_list(self, list1, list2): + ''' + 比较两个dict类型的list,返回list1中有,而list2中没有的元素 + @param list1: + @param list2: + @return: + ''' + list1_not_in_list2 = [] + for item1 in list1: + found = False + try: + for item2 in list2: + # 对比'protocol,ports,types,address'是否相同 + if all(item1[key] == item2[key] for key in ('protocol', 'ports', 'types', 'address')): + found = True + break + except KeyError: + continue + if not found: list1_not_in_list2.append(item1) + return list1_not_in_list2 + + def _get_diff_rules(self, panel_firewall_rules, firewall_rules): + ''' + 取出差异的规则 + @param panel_firewall_rules: + @param firewall_rules: + @return: + ''' + firewall_diff_rules_name = 'firewall_diff_rules' + firewall_diff_rules = {} + if os.path.isfile("config/{}.json".format(firewall_diff_rules_name)): + firewall_diff_rules = public.read_config(firewall_diff_rules_name) + if not firewall_diff_rules: + data = { + "panel_not_in_sys_fw_diff_list": self._diff_dict_list(panel_firewall_rules, firewall_rules), + "sys_not_in_panel_fw_diff_list": self._diff_dict_list(firewall_rules, panel_firewall_rules), + "panel_exclude": [], + "sys_exclude": [] + } + public.save_config(firewall_diff_rules_name, data) + return data + + panel_not_in_sys_fw_diff_list = self._diff_dict_list(panel_firewall_rules, firewall_rules) + sys_not_in_panel_fw_diff_list = self._diff_dict_list(firewall_rules, panel_firewall_rules) + + # 如果firewall_diff_rules的panel_exclude和sys_exclude中有值,则将其从panel_not_in_sys_fw_diff_list和sys_not_in_panel_fw_diff_list中去掉 + if firewall_diff_rules['panel_exclude']: + for key in firewall_diff_rules['panel_exclude']: + if key in panel_not_in_sys_fw_diff_list: + panel_not_in_sys_fw_diff_list.remove(key) + if firewall_diff_rules['sys_exclude']: + for key in firewall_diff_rules['sys_exclude']: + # panel_not_in_sys_fw_diff_list是list,如果key在panel_not_in_sys_fw_diff_list中,则将其从panel_not_in_sys_fw_diff_list中去掉 + if key in sys_not_in_panel_fw_diff_list: + sys_not_in_panel_fw_diff_list.remove(key) + + # 将差异规则写入文件并返回 + firewall_diff_rules['panel_not_in_sys_fw_diff_list'] = panel_not_in_sys_fw_diff_list + firewall_diff_rules['sys_not_in_panel_fw_diff_list'] = sys_not_in_panel_fw_diff_list + public.save_config(firewall_diff_rules_name, firewall_diff_rules) + return firewall_diff_rules + + def exclude_diff_rules(self, get): + ''' + 排除firewall_diff_rules的规则,并写入配置文件 + @param get: + @return: + ''' + try: + panel_excludes = get.panel_exclude if "panel_exclude" in get.get_items().keys() else {} + sys_excludes = get.sys_exclude if "sys_exclude" in get.get_items().keys() else {} + status = get.status if "status" in get.get_items().keys() else {} + # print("panel_excludes: ", panel_excludes) + + if status == 'add': + return self._add_exclude(panel_excludes, sys_excludes) + elif status == 'del': + return self._del_exclude(panel_excludes, sys_excludes) + except Exception as e: + # print(e) + # return public.returnMsg(False, 'Ignore rule failed,{}!'.format(e)) + return public.return_message(0, 0, 'Ignore rule failed,{}!'.format(e)) + + def _add_exclude(self, panel_excludes, sys_excludes): + ''' + 添加排除规则 + @param panel_exclude: + @param sys_exclude: + @return: + ''' + firewall_diff_rules_name = 'firewall_diff_rules' + firewall_diff_rules = {} + if os.path.isfile("config/{}.json".format(firewall_diff_rules_name)): + firewall_diff_rules = public.read_config(firewall_diff_rules_name) + + if not firewall_diff_rules['panel_exclude']: + firewall_diff_rules['panel_exclude'] = panel_excludes + else: + for exclude in panel_excludes: + if exclude not in firewall_diff_rules['panel_exclude']: + firewall_diff_rules['panel_exclude'].append(exclude) + + if not firewall_diff_rules['sys_exclude']: + firewall_diff_rules['sys_exclude'] = sys_excludes + else: + for exclude in sys_excludes: + if exclude not in firewall_diff_rules['sys_exclude']: + firewall_diff_rules['sys_exclude'].append(exclude) + public.save_config(firewall_diff_rules_name, firewall_diff_rules) + return public.returnMsg(True, 'Ignore rules successfully!') + + def _del_exclude(self, panel_excludes, sys_excludes): + ''' + 删除排除规则 + @param panel_excludes: + @param sys_excludes: + @return: + ''' + firewall_diff_rules_name = 'firewall_diff_rules' + firewall_diff_rules = {} + if os.path.isfile("config/{}.json".format(firewall_diff_rules_name)): + firewall_diff_rules = public.read_config(firewall_diff_rules_name) + + new_panel_exclude = [] + new_sys_exclude = [] + + for exclude in firewall_diff_rules['panel_exclude']: + if exclude not in panel_excludes: + new_panel_exclude.append(exclude) + for exclude in firewall_diff_rules['sys_exclude']: + if exclude not in sys_excludes: + new_sys_exclude.append(exclude) + + firewall_diff_rules['panel_exclude'] = new_panel_exclude + firewall_diff_rules['sys_exclude'] = new_sys_exclude + public.save_config(firewall_diff_rules_name, firewall_diff_rules) + # return public.returnMsg(True, 'Cancel ignore rule successfully') + return public.return_message(0, 0, 'Cancel ignore rule successfully') + + def _add_firewall_rules(self, source_ip, protocol, port, types): + ''' + 添加防火墙规则 + @param source_ip: + @param protocol: + @param port: + @param types: + @return: + ''' + if self.__isUfw: + if port.find('-') != -1: + port = port.replace('-', ':') + self.add_ufw_rule(source_ip, protocol, port, types) + elif self.__isFirewalld: + if port.find(':') != -1: + port = port.replace(':', '-') + self.add_firewall_rule(source_ip, protocol, port, types) + else: + self.add_iptables_rule(source_ip, protocol, port, types) + + def _del_firewall_rules(self, source_ip, protocol, port, types): + ''' + 删除防火墙规则 + @param source_ip: + @param protocol: + @param port: + @param types: + @return: + ''' + if self.__isUfw: + self.del_ufw_rule(source_ip, protocol, port, types) + elif self.__isFirewalld: + self.del_firewall_rule(source_ip, protocol, port, types) + else: + self.del_iptables_rule(source_ip, protocol, port, types) + + def _modify_firewall_rules(self, address, protocol, port, type, source_ip, source_protocol, ports, types): + ''' + 修改防火墙规则1 + @param address: + @param protocol: + @param port: + @param type: + @param source_ip: + @param source_protocol: + @param ports: + @param types: + @return: + ''' + if self.__isUfw: + self.edit_ufw_rule(address, protocol, port, type, source_ip, source_protocol, ports, types) + elif self.__isFirewalld: + self.edit_firewall_rule(address, protocol, port, type, source_ip, source_protocol, ports, types) + else: + self.edit_iptables_rule(address, protocol, port, type, source_ip, source_protocol, ports, types) + + # 新加代码----- end + + # 端口防扫描 --- start + def _get_server_lists_scan(self): + """ + @name 获取服务器常用端口 + @return: + """ + return { + "sshd": "{}".format(public.get_sshd_port()), + "mysql": "{}".format(public.get_mysql_info()["port"]), + "ftpd": "21", + "dovecot": "110,143", + "postfix": "25,465,587", + } + + def get_anti_scan_logs(self, get): + """ + @name 获取防扫描日志 + @param get: + @return: + """ + get = public.dict_obj() + server_lists = self._get_server_lists_scan() + result_dict = { + "currently_failed": 0, + "total_failed": 0, + "currently_banned": 0, + "total_banned": 0, + "banned_ip_list": [] + } + + import PluginLoader + for key in server_lists: + get.mode = key + + logs_result = PluginLoader.plugin_run('fail2ban', 'get_status', get) + if type(logs_result['msg']) is dict: + result_dict["currently_failed"] += int(logs_result["msg"]["currently_failed"]) + result_dict["total_failed"] += int(logs_result["msg"]["total_failed"]) + result_dict["currently_banned"] += int(logs_result["msg"]["currently_banned"]) + result_dict["total_banned"] += int(logs_result["msg"]["total_banned"]) + result_dict["banned_ip_list"] += logs_result["msg"]["banned_ip_list"] + + # return result_dict + return public.return_message(0, 0, result_dict) + + def get_anti_scan_status(self, get): + """ + @name 获取端口防扫描 + @return: + """ + plugin_path = "/www/server/panel/plugin/fail2ban" + result_data = {"status": 0, "installed": 1} + if not os.path.exists("{}".format(plugin_path)): + result_data['installed'] = 0 + # return result_data + return public.return_message(0, 0, result_data) + sock = "{}/fail2ban.sock".format(plugin_path) + if not os.path.exists(sock): + # return result_data + return public.return_message(0, 0, result_data) + + + server_lists = self._get_server_lists_scan() + s_file = '{}/plugin/fail2ban/config.json'.format(public.get_panel_path()) + if os.path.exists(s_file): + try: + data = json.loads(public.readFile(s_file)) + if len(data) == 0: + # return result_data + return public.return_message(0, 0, result_data) + + for key in server_lists: + if key in data: + if data[key]['act'] != 'true': + result_data['status'] = 0 + # return result_data + return public.return_message(0, 0, result_data) + + result_data['status'] = 1 + # return result_data + return public.return_message(0, 0, result_data) + except: + pass + + # return result_data + return public.return_message(0, 0, result_data) + def set_anti_scan_status(self, get): + """ + @name 设置常用端口防扫描 + @param get: + @return: + """ + scan_status = get.status if "status" in get else 0 + param_dict = { + 'type': 'edit', + 'act': 'true' if scan_status == 1 else 'false', + 'maxretry': '30', + 'findtime': '300', + 'bantime': '600', + 'port': '', + 'mode': '' + } + server_lists = self._get_server_lists_scan() + _set_up_path = "/www/server/panel/plugin/fail2ban" + _config = _set_up_path + "/config.json" + if not os.path.exists(_set_up_path + "/fail2ban_main.py"): + # return public.returnMsg(False, "fail2ban plugin is not installed") + return public.return_message(0, 0, "fail2ban plugin is not installed") + + if os.path.exists(_config): + try: + _conf_data = json.loads(public.ReadFile(_config)) + except: + _conf_data = {} + else: + _conf_data = {} + + import PluginLoader + + # if scan_status == "1" and PluginLoader.plugin_run('fail2ban', 'get_fail2ban_status', get) is False: + # get.type = "start" + # PluginLoader.plugin_run('fail2ban', 'set_fail2ban_status', get) + + for key in server_lists: + tmp = param_dict.copy() + tmp["port"] = server_lists[key] + tmp["mode"] = key + + if key not in _conf_data: + tmp["type"] = "add" + else: + tmp["maxretry"] = _conf_data[key]["maxretry"] + tmp["findtime"] = _conf_data[key]["findtime"] + tmp["bantime"] = _conf_data[key]["bantime"] + + tmp = public.to_dict_obj(tmp) + + PluginLoader.plugin_run('fail2ban', 'set_anti', tmp) + del tmp + + # if scan_status == "0": + # get.type = "stop" + # PluginLoader.plugin_run('fail2ban', 'set_fail2ban_status', get) + + public.WriteLog("Port Scanning Prevention", "[Security]-[System Firewall]-[Set Port Scanning Prevention]") + # return public.returnMsg(True, "Setup successful!") + return public.return_message(0, 0, "Setup successful!") + + def del_ban_ip(self, get): + """ + 删除封锁IP + @param get: + @return: + """ + get.ip = get.ip + import PluginLoader + server_lists = self._get_server_lists_scan() + for key in server_lists: + get.mode = key + PluginLoader.plugin_run('fail2ban', 'ban_ip_release', get) + + # return public.returnMsg(True, "Unlocked successfully") + return public.return_message(0, 0, "Unlocked successful!") + + +# 端口防扫描 --- end111 + + +class firewalld: + __TREE = None + __ROOT = None + __CONF_FILE = '/etc/firewalld/zones/public.xml' + + # 初始化配置文件XML对象 + def __init__(self): + if self.__TREE: return + if not os.path.exists(self.__CONF_FILE): return + self.__TREE = ElementTree() + self.__TREE.parse(self.__CONF_FILE) + self.__ROOT = self.__TREE.getroot() + + # 获取规则列表 + def GetAcceptPortList(self): + try: + mlist = self.__ROOT.getchildren() + except: + mlist = [] + data, arry = [], [] + + if len(mlist) < 1: + return data, arry + + data, arry = [], [] + for p in mlist: + tmp = {} + if p.tag == 'port': + tmp["protocol"] = p.attrib['protocol'] + tmp['ports'] = p.attrib['port'] + tmp['types'] = 'accept' + tmp['address'] = '' + elif p.tag == 'forward-port': + tmp["protocol"] = p.attrib['protocol'] + tmp["port"] = p.attrib['port'] + tmp["address"] = p.attrib.get('to-addr', '') + tmp["to-port"] = p.attrib['to-port'] + arry.append(tmp) + continue + elif p.tag == 'rule': + tmp["types"] = 'accept' + tmp['ports'] = '' + tmp['protocol'] = '' + ch = p.getchildren() + for c in ch: + if c.tag == 'port': + tmp['protocol'] = c.attrib['protocol'] + tmp['ports'] = c.attrib['port'] + elif c.tag == 'drop': + tmp['types'] = 'drop' + elif c.tag == 'reject': + tmp['types'] = 'reject' + elif c.tag == 'source': + if "address" in c.attrib.keys(): + tmp['address'] = c.attrib['address'] + if "address" not in tmp: + tmp['address'] = '' + else: + continue + if tmp: + data.append(tmp) + return data, arry + + def recombine_rules(self): + ''' + 重组防火墙规则,将tcp和udp端口相同的规则合并111111 + @return: + ''' + firewalld_rules = self.GetAcceptPortList()[0] + tcp_rules = [] + udp_rules = [] + for rule in firewalld_rules: + if rule['protocol'] == 'tcp': + tcp_rules.append(rule) + elif rule['protocol'] == 'udp': + udp_rules.append(rule) + + result_rules = [] + + for tcp_rule in tcp_rules: + for udp_rule in udp_rules: + if tcp_rule['ports'] == udp_rule['ports']: + if tcp_rule['types'] == udp_rule['types']: + if tcp_rule['address'] == udp_rule['address']: + if tcp_rule['protocol'] != udp_rule['protocol']: + tcp_rule['protocol'] = 'tcp/udp' + udp_rules.remove(udp_rule) + break + result_rules.append(tcp_rule) + result_rules.extend(udp_rules) + return result_rules + + +class Sqlite(): + db_file = None # 数据库文件 + connection = None # 数据库连接对象 + + def __init__(self): + self.db_file = "/www/server/panel/data/default.db" + self.create_table() + + # 获取数据库对象 + def GetConn(self): + try: + if self.connection == None: + self.connection = sqlite3.connect(self.db_file) + self.connection.text_factory = str + except Exception as ex: + import traceback + traceback.print_exc() + return "error: " + str(ex) + + def create_table(self): + # 创建firewall_new表记录端口规则 + if not public.M('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_new')).count(): + public.M('').execute('''CREATE TABLE "firewall_new" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "protocol" TEXT DEFAULT '', + "ports" TEXT, + "types" TEXT, + "address" TEXT DEFAULT '', + "brief" TEXT DEFAULT '', + "addtime" TEXT DEFAULT '');''') + public.M('').execute( + 'CREATE INDEX firewall_new_port ON firewall_new (ports);') + + if public.M('firewall_new').count() < 1: + # 写入默认数据 + if not public.M('firewall_new').where('ports=?', ('80',)).count(): + public.M('firewall_new').add( + 'ports,brief,addtime,protocol,types', + ('80', 'Website default port', '0000-00-00 00:00:00', 'tcp', 'accept') + ) + if not public.M('firewall_new').where('ports=?', ('21',)).count(): + public.M('firewall_new').add( + 'ports,brief,addtime,protocol,types', + ('21', 'FTP service', '0000-00-00 00:00:00', 'tcp', 'accept') + ) + if not public.M('firewall_new').where('ports=?', ('22',)).count(): + public.M('firewall_new').add( + 'ports,brief,addtime,protocol,types', + ('22', 'SSH remote service', '0000-00-00 00:00:00', 'tcp', 'accept') + ) + try: + _panel_port_file = '/www/server/panel/data/port.pl' + panel_port = public.readFile(_panel_port_file).strip(" ").strip("\n") + except Exception: + panel_port = '8888' + + if not public.M('firewall_new').where('ports=?', (panel_port,)).count(): + public.M('firewall_new').add( + 'ports,brief,addtime,protocol,types', + (panel_port, 'panel', '0000-00-00 00:00:00', 'tcp', 'accept') + ) + + # 创建firewall_ip表记录IP规则(屏蔽或放行) + if not public.M('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_ip')).count(): + public.M('').execute('''CREATE TABLE "firewall_ip" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "types" TEXT, + "address" TEXT DEFAULT '', + "brief" TEXT DEFAULT '', + "addtime" TEXT DEFAULT '');''') + public.M('').execute( + 'CREATE INDEX firewall_ip_addr ON firewall_ip (address);') + + # 创建firewall_trans表记录端口转发记录 + if not public.M('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_trans')).count(): + public.M('').execute('''CREATE TABLE firewall_trans ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "start_port" TEXT, + "ended_ip" TEXT, + "ended_port" TEXT, + "protocol" TEXT DEFAULT '', + "addtime" TEXT DEFAULT '');''') + public.M('').execute( + 'CREATE INDEX firewall_trans_port ON firewall_trans (start_port);' + ) + + # 创建firewall_country表记录IP规则(屏蔽或放行) + if not public.M('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_country')).count(): + public.M('').execute('''CREATE TABLE "firewall_country" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "types" TEXT, + "country" TEXT DEFAULT '', + "brief" TEXT DEFAULT '', + "addtime" TEXT DEFAULT '');''') + public.M('').execute('CREATE INDEX firewall_country_name ON firewall_country (country);') + + # 创建firewall_domain表记录域名规则(屏蔽或放行) + if not public.M('sqlite_master').where('type=? AND name=?', ('table', 'firewall_domain')).count(): + public.M('').execute('''CREATE TABLE "firewall_domain" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "types" TEXT, + "domain" TEXT, + "domain_total" TEXT, + "port" TEXT, + "sid" int DEFAULT 0, + "address" TEXT DEFAULT '', + "brief" TEXT DEFAULT '', + "protocol" TEXT DEFAULT '', + "addtime" TEXT DEFAULT '');''') + public.M('').execute('CREATE INDEX firewall_domain_addr ON firewall_domain (domain);') + + + # 修复之前已经创建的 firewall_domain 表无 domain_total 字段的问题 + create_table_str = public.M('firewall_new').table('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_new')).getField('sql') + if 'domain_total' not in create_table_str: + public.M('firewall_new').execute('ALTER TABLE "firewall_domain" ADD "domain_total" TEXT DEFAULT ""') + # 修复之前已经创建的 firewall_new 表无 domain 字段的问题 + create_table_str = public.M('firewall_new').table('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_new')).getField('sql') + if 'domain' not in create_table_str: + public.M('firewall_new').execute('ALTER TABLE "firewall_new" ADD "domain" TEXT DEFAULT ""') + if 'sid' not in create_table_str: + public.M('firewall_new').execute('ALTER TABLE "firewall_new" ADD "sid" int DEFAULT 0') + # 修复之前已经创建的 firewall_ip 表无 domain 字段的问题 + create_table_str = public.M('firewall_ip').table('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_ip')).getField('sql') + if 'sid' not in create_table_str: + public.M('firewall_ip').execute('ALTER TABLE "firewall_ip" ADD "sid" int DEFAULT 0') + if 'domain' not in create_table_str: + public.M('firewall_ip').execute('ALTER TABLE "firewall_ip" ADD "domain" TEXT DEFAULT ""') + + # 修复之前已经创建的 firewall_country 表无 ports 字段的问题 + create_table_str = public.M('firewall_country').table('sqlite_master').where( + 'type=? AND name=?', ('table', 'firewall_country')).getField('sql') + if 'ports' not in create_table_str: + public.M('firewall_country').execute('ALTER TABLE "firewall_country" ADD "ports" TEXT DEFAULT ""') + + def create_trigger(self, sql): + self.GetConn() + self.connection.text_factory = str + try: + result = self.connection.execute(sql) + id = result.lastrowid + self.connection.commit() + self.rm_lock() + return id + except Exception as ex: + return "error: " + str(ex) + + +sql = """ + CREATE TRIGGER update_port AFTER DELETE ON firewall + when old.port!='' + BEGIN + delete from firewall_new where ports = old.port; + delete from firewall_ip where address = old.port; + END; + """ +s = Sqlite() +s.create_trigger(sql) diff --git a/class_v2/safeModelV2/freeipModel.py b/class_v2/safeModelV2/freeipModel.py new file mode 100644 index 00000000..a7762a98 --- /dev/null +++ b/class_v2/safeModelV2/freeipModel.py @@ -0,0 +1,97 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: cjxin +#------------------------------------------------------------------- + +# 免费IP库 +#------------------------------ +import os,re,json,time +from safeModelV2.base import safeBase +import public + + +class main(safeBase): + _sfile = '{}/data/free_ip_area.json'.format(public.get_panel_path()) + + def __init__(self): + try: + self.user_info = public.get_user_info() + except: + self.user_info = None + + def get_ip_area(self,get): + """ + @获取IP地址所在地 + @param get: dict/array + """ + ips = get['ips'] + arrs,result = [],{} + for ip in ips:arrs.append(ip) + if len(arrs) > 0: + data = self.__get_cloud_ip_info(arrs) + for ip in data: + result[ip] = data[ip] + return result + + + def __get_cloud_ip_info(self,ips): + """ + @获取IP地址所在地 + @得判断是否是我们的用户 + @param ips: + """ + result = {} + try: + ''' + @从云端获取IP地址所在地 + @param data 是否是宝塔用户,如果不是则不返回 + @param ips: IP地址 + ''' + data = {} + data['ip'] = ','.join(ips) + data['uid'] = self.user_info['uid'] + data["serverid"]=self.user_info["serverid"] + #如果不是我们的用户,那么不返回数据 + res = public.httpPost('https://www.bt.cn/api/ip/info',data) + res = json.loads(res) + data = self.get_ip_area_cache() + for key in res: + info = res[key] + if public.is_local_ip(key): + res[key]['city']="Intranet" + if not res[key]['city']: continue + if not res[key]['city'].strip() and not res[key]['continent'].strip(): + info = {'info':'Unknown IP'} + else: + info['info'] = '{} {} {} {}'.format(info['carrier'],info['country'],info['province'],info['city']).strip() + data[key] = info + result[key] = info + self.set_ip_area_cache(data) + except: + pass + return result + + + def get_ip_area_cache(self): + """ + @获取IP地址所在地 + @param get: + """ + data = {} + try: + data = json.loads(public.readFile(self._sfile)) + except: + public.writeFile(self._sfile,json.dumps({})) + return data + + def set_ip_area_cache(self,data): + """ + @设置IP地址所在地 + @param data: + """ + public.writeFile(self._sfile,json.dumps(data)) + return True diff --git a/class_v2/safeModelV2/ipsModel.py b/class_v2/safeModelV2/ipsModel.py new file mode 100644 index 00000000..1fc95238 --- /dev/null +++ b/class_v2/safeModelV2/ipsModel.py @@ -0,0 +1,118 @@ +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5 +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +rUv3hasppUA6pOugYcQZyA== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +iEprTHe70MI/8Rhzd8EK+QnVQn6aZyZ8dBODiF5pySg= +P1nWGQOfbECkszATyvUnYMoMB+zOtupYIF89n5X+cOkvqsM/qxPIR+8JRAch8USo +XIfdJ79nObMM+vyAmKbTmw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +hbDorme8BqzUEmeXCAWmYa52umm3PBCcfSvATzTG02w= +1u+XjG/2+GSQRv6EzCaWRQ== +giysigZ4bhExZB4emZkX3a9fe6PGGMNE9ZZnk7xPqW8QW2Ij1yNFf5XEj5pPPlMwdhA6dIcO2ZX23qJszvnq1fcl6NckihyZ+Uf83rbYjeQ= +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +b4OJVZe8QyIpjuTpKXDL9A== +32pdC9DD05OE2l0oXazDFI+hlwIzzkFTo7tZH3axSGoKNgh7EevAzaB+FO2ZLVS/maKOtFAjIeKErPBj7dsfmg== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +32pdC9DD05OE2l0oXazDFOQO8mg+tgs322u5NSxm1hnE+dYVd/hkfCsat3ixeRAk +1u+XjG/2+GSQRv6EzCaWRQ== +Fq/4dKD9ssEKrNEHrSuygxu04P7BPF8PboiRMtr4FMM= +Z8UsPk1Q7HtwjRd4g01ryw== +KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD +ho/Q+jrWDtBeg9J9ZTnKi7MMSdcDWkuDSRbkOa0slC0= +Z8UsPk1Q7HtwjRd4g01ryw== +1aJ5h9ef6qScYRSEXHxMz/JV+hrqnP7g6CgzmGbTA34= +CH5utP+NORdjI2nqATw4gJ30bQaw4oV4TkWtZlCiO9A= +lbIj6ug3LX3xS019kmbRSTcfm4XASPCYnVO8MD2z14s= +iZaIsa48RXfE+uf/yF/rQD9SK8CJ49+yPAMIKmMZPD4= +lOh2GtzHjjMM8E9J40AuOc+/vc1yhUL+xJx/Mivlb25pg5HBJ1HJ91Rfq30bJq9S +UbJuac0dxLiN+5ocS3w2vRp92UK1M7Voei4ApHZyTgY= +VmVrGQo2zRokW/ZuO9bN69BDpCzHoJtTjTgNlAOe5sUQUaSnZ1Z1hl5M9Ym+7tCG +VmVrGQo2zRokW/ZuO9bN61V3/TpT/zrd2QvdUtMgGKRq8f0CD0RNnZ4rTqSInsSj +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN657tYrblt+z3q06zgiQZcYA= +VmVrGQo2zRokW/ZuO9bN66aFKP1IdEjWKaMooH0pyYReSxgrF6gfc+R+CrZAggrY +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN69KBqHrgU1XKz6C1sxuKNo2/9c/EcmFt/gfo+iaOu2+K +1u+XjG/2+GSQRv6EzCaWRQ== +n+FHKmWLbioNpC38yMj4WmiXmXqyLuzKUmIAM1Ft5eM= +NhnIR3Ilo4H2su9/cTNo/MME7jTfPzQcknP67wyItNzcCacoNjdvCuiW0x8udsSO/edftRARPJ4xwm1wQrVT2A== +wlLHv6kT3Q/RmtMBN4nDATwL/9Oa0sOvSxwVwQfq99U= +VmVrGQo2zRokW/ZuO9bN68Jg91voF9Ce3nf2o3e+jkofqu9SSuRXcnpG7smrmLLo +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhMRKs67DB9ZOIiBDrB02YSQctSNS1aqQlPvqVprQ2WDG +Z8UsPk1Q7HtwjRd4g01ryw== +fXZfnC6cxxgiN+onu+xJ1wUhirDdf4kByrL5vhQHFnCYKlzy/eVgdjzy55WiEFTz +LikBEoRjt8wwWzUZNw+jJgsad0LKWFNZK+YO+SfR6UU= +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +W3A5Dt+8AxWSKsTYeXZoL+tHA8UsZgQuAUkDUOPJ9p9irMcLKjcsuwS2q6lHXAKR +L0eUthVnpkGsmKFAX6d+uOlJx8vHVMiotg8sk086vHQ= +TQRmC0vOvJ1P+lfyS3Os4X1xcTKiQ06lXIJEwRwWbq4ieECEmB4BmDhbitv1CsLg +L0eUthVnpkGsmKFAX6d+uLqKlmsh+FpVhvWy0B6mjhM= +1u+XjG/2+GSQRv6EzCaWRQ== +YabJCT93a3/IohXaIkr3oS6/BpCE446GJgDFFvo/yjjYAG9E6229ssUIWmf519v1 +hBWY30cr8RshUe3F5HK1cRZ9KmhsMz0lNR6pLxg9eoM= +L0eUthVnpkGsmKFAX6d+uFd45MXPp4WlUdlXMx7fhE8= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +avrdBvQ7h/91pEC4PcvqMSR2TJ0t+8EMPSsnARb+XVHHkBrQO1HsCg6QcbTZvZ2W +Z8UsPk1Q7HtwjRd4g01ryw== +KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD +LikBEoRjt8wwWzUZNw+jJpxJaQXGUN8g/Hr0khQ9A1Q= +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +b4OJVZe8QyIpjuTpKXDL9A== +1u+XjG/2+GSQRv6EzCaWRQ== +NhnIR3Ilo4H2su9/cTNo/F9IByyW2CHxX2+3PSbgZmE= +NhnIR3Ilo4H2su9/cTNo/JEWIdEQUX/MlMcOaFZ8tGgjCfBcJNHhmedart7PDRhF +NhnIR3Ilo4H2su9/cTNo/E4e/PRk3DEtgyjsUDeOpSOwGpZehbHYaLN6kP4SrhN+PFbXCI6E8Z+8865ULB7IOw== +NhnIR3Ilo4H2su9/cTNo/C/mkLay+Rx5WJjwGcYV7pSktyrOzy78W2NcadJ49lO1rNrUNnnzcDMri5NWEH6lGQ== +lOh2GtzHjjMM8E9J40AuOVVeyoh6rSy98QMdDBotkjU+CpzMJCQMfJrCkE4uEcQAgmW0jhOnZHI5mtglpnoAuAijuP+2lAOsmb6+b+801pIjLK0hXXo2u4rmG7jkFs3R +lOh2GtzHjjMM8E9J40AuOeMCiZGQsphriaBgvIdFiyOx9f7gw874ih7YoSVMDwrD +1u+XjG/2+GSQRv6EzCaWRQ== +NhnIR3Ilo4H2su9/cTNo/A5SzFv4xHkReni2nLCW9wf/+pP3k795kKoMZAQXwXD6 +wlLHv6kT3Q/RmtMBN4nDASCRBoPIX3WHiVt75UTjznI= +VmVrGQo2zRokW/ZuO9bN63WVsNPEwwprYYkAUqx5H2WQM74qCCEzrB3qJy/Mpn1CTpqeZGmRvqbYKmI/uSdTZw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61LJQniFugoi6x2ujjnJM7tldXKimSA8e3cVKdvY80W2 +VmVrGQo2zRokW/ZuO9bN64CPlXy2HmOeksJNoNOmCTRN2iWrExIMUdvTGpj/NNWq+ALT/bwKyDsjcfkGB9eQcrQ3H8UC+ywFIkXx32JcLCKuolLfVZKLqPTsQHTpYF9P +VmVrGQo2zRokW/ZuO9bN6yDw6frMtVHHQaPzsZSKK0aES6D4Kud9zFd68X/jO6wJ +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN61V3/TpT/zrd2QvdUtMgGKQFi9VDtwL4Vc83iegeMAft36zpJ+t/eeWtmA4Eamfb77jKlCL/2MWCO4n+tmFfKcs5bo8I5tFr5Qu31v4FEN0K0c0Au7xRuJmPMknxTzJ1AQ4L8Z8QbLuQ3EJTDAgG6zw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wKjnvaTwlMeHWSuJ/EAxZ8Z1KtkmFIaU/d9KNNMifcN +VmVrGQo2zRokW/ZuO9bN62ExR0OHxzNY3oC4Mfi694VYJyilbRPMd6JDlCBDdbCe +32pdC9DD05OE2l0oXazDFLdmyVEUr33LQ7qI5CZQN57igfVF4gan9s5C0Jnc3Ki1 +6ZPJI/HSoc4xA2zncU65FpfH+eGtPlOYNU1YCnnAis8= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Fq/4dKD9ssEKrNEHrSuyg9mm2ubmmEvtKER/ZGhfGKuM0YKPX8D/je6PexvVvgfs +Z8UsPk1Q7HtwjRd4g01ryw== +KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD +ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE= +Z8UsPk1Q7HtwjRd4g01ryw== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +b4OJVZe8QyIpjuTpKXDL9A== +NhnIR3Ilo4H2su9/cTNo/Ps3vYSOiOY6esn1xeoWmAlmLwmaOoQ9T7lnCb6TRVZ67prVtxc96eRcwG0EieQAHA== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+nhmNgO2Yqkfbla+bgPzSp/ulULnjCxA+skRNBtWTg0BA== ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +0jp9NuG0oWLkMfJ/MsYOmfu7WkSnZjHksXr6BBYLZSUOzHDXl9XjsxiUKNVbrHEG +Z8UsPk1Q7HtwjRd4g01ryw== ++kAMkUztiJJUlfW5H0qbp+x6TgEEQKPe4CNld5ATADH0jjvxLnFDBqMEQxxyT+xg +vPgTNKnHCM+ykPDJGfC5NIdfn4GuPGcMgxOHmcFE4sA= +Z8UsPk1Q7HtwjRd4g01ryw== +IhuisKim47k91RVt8z8qtK4Hi8QoBKGCIdLYuWbuvVULY/yxOc14YsJd+ymvu1M2ua6mSguBgb6gPVMjpVflaQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= diff --git a/class_v2/safeModelV2/securityModel.py b/class_v2/safeModelV2/securityModel.py new file mode 100644 index 00000000..b99a503f --- /dev/null +++ b/class_v2/safeModelV2/securityModel.py @@ -0,0 +1,1691 @@ +4wZIF38E5nxrLYkW2uUR7d3iaGQLSTjR4DuJynwT7k0= +XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q= +dBZyCsfrbwqvA0sbdGrIGg== +XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q= +n+0ptngHIPIjFuMNQ53bfryjWeDQdwMlvn4xPuJO5u+Z2ORjG7RQlZx91meRqdmMVOBP9Rmyv3YsNRyjdE2DJuPBBv+kSws1+u5OFq4s9Vo= +XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q= +i5dtmtvJAakTQ4h3th/Bg/k+ogi7UBuwCMMdkJp2H/ABwRFxrb4nYZGwElANdaIy +XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q= +1u+XjG/2+GSQRv6EzCaWRQ== +QEXWR1+ijYfZcVT7Jxgn9b/1qODi2lleSOUlnjby8yCki/rkQucI6WIYSwMiN5/Z +XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAbFx1CPChpgx6xlsqPUrTBV +/hGh1tb/B6hRHKE+/g3LdcoNHYeGiE8itwmNgTlyaS8= +SbQQ5SqO9QrBwZ9ObpMYLw== +1u+XjG/2+GSQRv6EzCaWRQ== +D0dsg8rStGc6knFsZy6SyBGy0nmeaHYzoYKgGu7QIjA= +sRPdbbdTaQFxLkgXulRHBHxNJMNNOY69Hqrbr+c7JKc= +WVZHNW0sF72nCINduLgk2oCijQvdyFvHnftiwPgbI1o= +P1nWGQOfbECkszATyvUnYMoMB+zOtupYIF89n5X+cOkvqsM/qxPIR+8JRAch8USo +rpy88ff5JnVuJqXSNlYf+CQ4VKHExseR2LosMAdYEeHGNm80vDZH+2U6FEAnuapk +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +hbDorme8BqzUEmeXCAWmYa52umm3PBCcfSvATzTG02w= +1m0JCmsSm1bWc5jyHCVv6Cnrr9E+yNmZlb1WibISmGbr+KMdKOT/9t43CBeYx5Mc +xJmvc/WjX5ZL7/2nnJq3MuPp0h21tahzXjft6G3i4wA= +OjvdpCJPmEE/XXQgxX6Q24fmhYnAOnqgciYGQvGkzouXZiBeHrHfoFUV4jYH3VEvsLAIpicQnJ6upUuxH++tCQ== +Pk6vNT/ApTXrbpdwgZfTzevfF7XV6fOgcuiG9Z1yvQU= +2R9NYztgl9lLDOzubkvxEV6cK6WrhYbB1T56PdZLuuHhbNjHFVl740pepm61uO3JkeqD0S6IXZYSllKQKR0sbza6F7w0qyZA/ZvlpU8Xpd5OrZURZQl8icGs4TiJvjd+ +aefSBGgeVI8UGNoyrQSBRfmVCVIDHP8GpI8mqMUyykdnPz4dml7AVkuruY4OmLvPIpB69geoHjAHaUF0XjdTm7meTUUu9x/6OX7szGQ6fX0PRtKB+8ysX5M3W7iEm572MkNBJ65MMNxDypeJiTv2kFa/BVQmUgZuooWqwii6XQKOVGUIv+98RZxlm6zjVnAD +Z4pQntO7b1+TL5Qz65Fz5zriYgn2ylWcQkZg4u1EWj4jHgcNFqKUv7EgQUHg42sI +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +W+Nfmvg8yIH+Ez2ByJPwEuRsNvYcoPFkZ3RWm/lu7+xcxc+Ys2eYbEO1gFL92OPb +8Aj3fakGh9c+1bfJQURlxSYFVwepqRq9sYBTYz2BfDs= +58w29u4j3WzehOmWB7rm9YG5zqbZp63232wfcqK8b/HRchMGw7R3evjgobd7vgm0JeHu+mrOAZSJQH8cfxJ2lofJu0lYkVVJPeACgd1Qn78= +zgKkJRNMCn8GCiKimPAwiS6Eh5QvNeiufZlJW0pVFOCDTi8Zq0UjIqdrcCYoKZBKrXH/0J+OG6Q9qMcVo+7QExcods3k0ZKFFS84IvfuuLl8iHj5uJJlzgbhhyFVu9/sCz1h/jxQpRl5r8nx7xfBpalTSL6FvFvJsUdubtLHW24= +8CjvdUzUhFR4VHs3tQRSSwYiHBlxTz6PjSFhA3FUgYqT1zzri2hRFYa/xrlfgQBo1FYnA4pxFvpNU3l8P3TBOehLOqu9HRpz2/ZdUhRLmyM= +IwNSi3B49UePWkec9h2aViQNaJUtCr9Fg/zONZJ7BAHhAsPlhlhiTSxbbZt7qBzHuRXA8ssbgkncTWfewCFCSg== +IwNSi3B49UePWkec9h2aVgOML7jzMqS//KVIa6H25JlhxzwLYUXHhh9ZWyM9VxIU +IwNSi3B49UePWkec9h2aVu9+rAVrv2IeJy5ZA+9BD24UnRRiP78d4Ay8RJEgtlD2 +IwNSi3B49UePWkec9h2aVlW8jH99OssjKYRGL6FWV964jAUW5pxd8jCuWDX8dtdA +IwNSi3B49UePWkec9h2aVvJSXqZVYn+ZKcZ+eqKPth235+6nSxcTSEEjMq/cEdOq +IwNSi3B49UePWkec9h2aVuswOY+QUwCBRWJFqAEl89arDwaEbUyDQXH1w3CtPXfb +IwNSi3B49UePWkec9h2aVrKxz33p+NgrbHjI2f68/l0= +xZ9u1heNQTI58qBqs7KOylcOEoGYAm9wm2Pc3fOQ0cJLZYrlXYJw2ARn9LsMw+7x +1u+XjG/2+GSQRv6EzCaWRQ== +mlGJWwQAi41HKr7j70v3zhszuDsZD3HZNEjtkStFCvaGWj5rbui1QRSx2O28AJAD +KZTmaJLp+FU9X93j5Tpqtw== +onBbf8l/M7QFwfZbFXfWhYgF0MmzmYL20H6FmpRmhbVRUdjnkUi/v3dYWgjVOc60 +ho/Q+jrWDtBeg9J9ZTnKixi1K9oP2iYTOwPxyGNJHuD9qiXINrmWXThQlfvTzpE9ciXrbN9G/qMSY3MMkCKL/5/nFme7bdfFltbItJiFd8RYpPELl34HLaIyxxkMZIKJ +WuRvIBaPNRsVGEMoM4NSTj9okWpIPDBwXYlx0ptZRL/htKAkfBzMha+fobGCSqWT +KZTmaJLp+FU9X93j5Tpqtw== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN6+4zVF4aBgZVa7yqS9yu7Z6BodAkPc4fSu07VpQm9LQX +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +1u+XjG/2+GSQRv6EzCaWRQ== +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs9hz49lIM9QDOgdKn6cpsbvJLdc46vLcM1bYfBB2uH1ZN96pfFNt1UywexVY6Eg3nQ= +nz85yNtnEmjM5Lt96/cqExqhhOV1bjZ7LXcnfSnc2mQxUr3BlqzDO/LhVuFObkBAuW5KUUj8ijP08AWgaDHjtQ== +JDFeI9M1ZJLkjtjDVE9IMPiP1kqYwJdmJMKGfC47oA7Kp+dmwuvb9PEetQRbd6lRwlRirecZpfXrJocTJxgu+WHDfEg7JRIGK0obV3ES5iUNkXsHfXND5u2JNIYiNCu3 +2+RceSNjD2iq0sRzepMysVUxXE5N/Z4aOUZt1dAV3sWaDWXqIfjhxQTvYnq4ah3QZj0WoRcONsWRupa+3ejbEBRYVH5nwvT9JMx7WrOcBonEe8X2OOlNs5jKQwIqSNFYdXIWehQc0oVw/jQL1cNoycRLhmLrMIFuEjNpmpdc+KxN55kgYP7/iCNcqdgh+UMEKF4Ua10x33s2VaYGF8BIsQ== +NjzfvS0PyiH/YLJl8s484uWnGRrJKl44YUVPdHIyTzs7zCpdD7dKmGCibl9hUZ4X +XgacqF8d3I7m81jvrWCZxEbPrRTJ4dccIpuqFLLKM3SrDnEjcEI5CZs2Wxa6jAbiAtaZXoIMixtLIkqYZtB1Xby8dmoJWlCtJymRxQbLhFqNfEzgbaQXN4NcNeCSDglY +D5IA5Wx3J15yXUzPYlpr3mf5+WwRzHJeA8ARYLZ4caU= +w9DemAtCZ0Ko6Anoc5h4VW7y7dUeT8/wcW1sU8fqHKOgYZ9pl2XvvKLCvcl6CyW8 +zI8euyep3wFTTSveCidutgleCpP4XPHma8hywAp0dMtSW6RT3fEFTSUppl2kz6zL +n4ws3AetOdjkVQ7W9sbFyIb1VMlkK8TIfGjugWfYcosSyIq6C5dXbLvQ9I1T7BHT +1WrybT4InUxTVVQ5qhmrwoPkhYSV3eIyimLs+wiMJF5L1U7FlpulsmbfN84+/4mu +PNZcZ67j5O9c3iqbWgpxu5nKlQozrHwjVzIdJAvGVYEoNvdnI3rSIqCHs17t7HS9 +dV9xz3yyeXnMinI4Y9Bh+g8oAw4fkxA8V1c13xBw/CUGBZNtriVcNDYJYGckmm8W +VmVrGQo2zRokW/ZuO9bN64DKxIwe3TA0ZLk66JmNQXsifOH5RrSbRDqQwbzEts/g +F9Sftf1PoN5P+lgvc+r10FBMPK3gvX66Cu6ezHPjSU1/y3nolrnayo5Gf/xX5Vj2sKxQdIO0cNrptiMHaAQ1BjubdHD6cPNhAtbjhwvNK+s= +VmVrGQo2zRokW/ZuO9bN6/hr1K0eBkvC9NUF3b0MUUJ3i6RzmoS9/63Bct21USwa +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6xi6Ahf4/3nDVsR0SZow43cnpBa1vPhNrgBa4z4semwK +5STPzn/BifKzw7c0kQHkfSIlGB7jcmAM5JZZKFm0TG02Opdmx8Z2gVax2S6s4Ylu2ZVwcsrSjAEy9iImRgcXMAwMPli6wqYNTMxXqBNprJ6xy/E1th9i37Nha8qdi87G +NjzfvS0PyiH/YLJl8s484nsOAbYHJry53idXgVQiuUWbrWeQqcaxytCuRIeFNu+I+LKqM8tQ6Loq3MAFvKviQRV+Ve9xKo+uITTOYC+AueeCEfOoBXHtmleK5nbGAQVC97OUZ41r4GQIxoDM38npc5PvoA0zaHU9sw8g4M3CgKM= +Du0z5l9SDrO/Iv1mHDRkV+2SoQzopuUElLrQ6UgQIpflAYRmn6+pjfyt1mlRpjXZocfLDB7jtns7FyPuEIErvw== +m5eWh32xFyyO8LCP2Vawm0LvIWVhsBJCQVLCzszazG0= +9tYtzqEz04IhMzB5yZnN6kcwmurGecELg77mlM5HHsI= +bJM2RTsGFKAbaqkE1Sj6R0Z6GTygMGuI5TmGACQgShE/Bl3RfPqENt2hnEeKv4pq +JbnXIK/axHVmA2plDNFCpEofHtK6A0JApZ48oEIOmf0= +yisP9bnsS89PFbBRJbtYnF8Fz028aCPQdjOlgUye+4A= +nuQO1if3lMKUHCYi1j17dS3Q/eaY/q+iWSoWyU0wzdY= +QzW09Oyj0g7smRjIbo1KUooEoyib3fJA7tjfk3GopNpDQnzVj7Gt04kf4xUrvLtk +661hZf7vhUQ+50okfwfTXw== +5VCCe6IJEmur5OrxSKQjRy3ztaMlZW5jXIYv4pxo+nOUSKQJT98C64IJZq+/tw86f97cK0yNmn7K8efC/fb64mhtL1iDKqOmaUGYe8G49BQupwMPZd7D/p0mG+CNrF69 +KAi6Ky2zICxpxofM/wpAf9uCC26kg1b5ddk95IooqzvsSbZHJRlqmcos6vwQ69s2 +e4wK/DYuOItHm+Ja22fo+265Gx45nnvVUa4C2H5Mgvo= +3W5ILeM0JZ9iXzlHvguO5B+mxh/pX62YGmjEidibBfUqEY0J6XYt+HvYkO9ai7VO +IwNSi3B49UePWkec9h2aVk4UXoXVaJ9qrpAMx0aJvDTMxPcHU3yDfwGWAyMNYBu6JE/+j+YKjUxEiCzOMioN3g+uvH10bqobqmys5DvAsXE= +IwNSi3B49UePWkec9h2aVv/ob4vTPqn27fqWQT45EsrgRZ6BzpV9qJ4b/c/ewvMcL03aVfS5RHI8WmnxVIzG4QI7WZvgVszoVPLcveGEZM3rZoVn3YHJamE+mmtY97n+776O0JaOcMZ27p5gSfUFk4c7VUAaTa5+w8aQlmYW1Oo= +IwNSi3B49UePWkec9h2aVjsT0WYGT/LbilqaR+VLAy+xTERHvM8GkYpvyw8eWpG4c3jJUw3KEIwjIxHeJEymIr96ZH6TCjLttgJpjl/KTeBViJAxQZ+SAtMNDVjxBgCY +IwNSi3B49UePWkec9h2aVmLpQ9q5D1+jKkSrtSZBN4Q= +IwNSi3B49UePWkec9h2aVi7anccYLXBKtuipT0ML+0U= +IwNSi3B49UePWkec9h2aVgawMbI8ZWX/Xthp5FWNpTm02Lkxd9v8ubMNX9x8JKDF +IwNSi3B49UePWkec9h2aVlUEpUR6OtqBi/qhu7NMaFTzGaGrows7DWabczrBMLMqx7UYjFxGeO5j5sISF6Wl8g== +IwNSi3B49UePWkec9h2aVuEWYmD9zZq7SqfctvdTuegHWnU7GtxmXsO6Ex+TYRsR +IwNSi3B49UePWkec9h2aVpDSHoWHblWlctFbb3T0wn7kT9dbfRWCMBBcKXvptGx6Iyj9VinRWXcAD7bzHBWaIzj2u3iflHj6jYLIc1iOd3Q= +IwNSi3B49UePWkec9h2aVqCItR/0Skx2rxI0FNlR3eTU8Mcy9auyJZRPKogRnHiUeXETFnehNdLAfDEraSiSPg== +IwNSi3B49UePWkec9h2aVrAdvpZovuYoEMvM76ppquahUOn+ZXzu/YvYlyw84LWp +IwNSi3B49UePWkec9h2aVs1nT9H8GPxL9ewV2R/PADe5mMoK+Rt8Fa4Rm5dp5IVC +IwNSi3B49UePWkec9h2aVkWqi+tO4rK1OQBy5RtqrDmx85Ll9vM58wEcCSvi/NNd +IwNSi3B49UePWkec9h2aVrsmuP8cJlGgy5uDNkRzP1oH1/0PpcrLleuVhRXqfWPiVvswjp3rvGyNSY1AT33Kr6cKPXwQg2UReoHOwoobyC9sGk0KxMTWqTwptw2R52m8+wr1UCePfU/w34PFNxYfiu6GT11WU2tu5RBT50NRK6I= +IwNSi3B49UePWkec9h2aVg/ONH6pQSdKZSA1IVlvWS/ar/yzE1xDkU2l0Ue3HYVETw3EV0tplvmq/j1TpJWUhQ== +IwNSi3B49UePWkec9h2aVtSXV3is472UB+EIIPIVVRDvrA5LpLJ7cy1YL59hpv7b +IwNSi3B49UePWkec9h2aVmmt5aJVr2D6g6kdfI46jiDateIEZYAW4ZJcvMPCZoFd +IwNSi3B49UePWkec9h2aViQu4nXoXYfJBDkVJG10Xjy3xnK5+ASnxqwekT8TTxQ+NIzIPuGf3QNp7DHSF+XGHALN6MLwF9pNj4IRhsIfWJHOwmsMpUYHWiplMeJBynyXlJO41+MQRg3WoUoWPdlTmzvY3Qn1KhI8NVlVo1DGyDI+cWuxKx7A3usGE1fX9Y/w +IwNSi3B49UePWkec9h2aVrAdvpZovuYoEMvM76ppquZ2XWBdd/QTtjqDVwiuUScLlEQKBmSwBjaesbSMBLuX4HmovPkJ4154JvQTeq8xWzQ= +IwNSi3B49UePWkec9h2aVtSXV3is472UB+EIIPIVVRDvrA5LpLJ7cy1YL59hpv7b +XAJ9ytHVbHM1Btk2P2iYeI0x8NHMILHjHS2x8djNkDxbw/0Fw47Iz1lwrR5xvvn7 +Tk9xlR7qdfcItfb2ZCawoa3gDrkaHoK+GdTFuR0DhNiseeLZWRRwq8iA//kQVb4I +mGjRXdtl0PckyTSYK5yVPc6XcHPw/lAYYguBn17flAo= +DPgKxKUksPJmBI8KfD7MnG19wQLsGqEChmw0QoF0cBEGNXse2EH2RoQjVnGDcYmdB0GUkNwww4YhQ8aieso8Hw== +DPgKxKUksPJmBI8KfD7MnHMEClnTrZOgWuhoya+UtfRGOBC1uS5ILl2CMZwpFEHoFGX1Hq6KjlDVLLyHJa4A22z9KqUeYzo7kdD3IRmApficIMZX9EgibFlELgr1Nuve2jGxqJBcYG5NM3h2kdRegg== +Nm/97dX7QPdHIqApKEes3DDGlNYTObtQW4TndMqXq36d2O/XMJDcOXXw1xcOIV/CXht4Yme2sDhmwflt9XyXOV3//eCre3z5J8TLTuCe6MeGV4CDvsiwh3JeJvhS+PHh +IW3EqlnnkSmY1/JPGNw5+3vURuvsnXAvPp4bO9F0tPsoflnTjqJSRd8ZzMGHdglHHfxLeAi74N4KRzlqNSRQ2ptalZyi8UHTJ1zH0cYyr406TJ06FY/tbiwgCjZHi4eVd4h3DKYyqPMBoM2LyQdFGg== +wlLHv6kT3Q/RmtMBN4nDAZOq/KkXwjmJXU6lIwaeSQux0oN4tIbhF8ZR5VYXdOa8 +VmVrGQo2zRokW/ZuO9bN66tKItbXQ2S07vvESMb0Xbx3fmLgrcSLbgPkYQ0cjDs4n4nwgxuwWO5ZRu/l3LP7KA== +VmVrGQo2zRokW/ZuO9bN65KPamIARv99cFn9NbPlKuvW0pRhETj4/81+ZPDZcAKs +VmVrGQo2zRokW/ZuO9bN67vH5ZnhExKao21k1gaT2G0FW1Fan/3LmBluWT+lEhtBkl8E6G66TjuT3POYVcwWj+zIW2FtUL84muoV4/VPvDE= +wlLHv6kT3Q/RmtMBN4nDAa0nhnlZDCiGXf0V3odfM5gdveMKPlnoe1cvlrbeeQf9 +VmVrGQo2zRokW/ZuO9bN692I9BHTUUNtPoqLNpaHMVuAxs6D7xvFG9y4+GFpJG4QYj/otOtWLL3aoqR3r2jKZw== +VmVrGQo2zRokW/ZuO9bN63oQXh9DZkh7TCvG9sp3gJi/ZoKhTvTBKm2jHLQI+hVS +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZcKSoZD8wPjLjehW2rIbxpWCeB5SDWc7JzgrKKoXCb5EA== +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZfPeLJTt9nZb7Y5PSF7XoobGM/xM4dWAILbiNCrNHLtUw== +VmVrGQo2zRokW/ZuO9bN64aCc/sRPofqjT6zGI12oEVmj13/GVwVAJqjfidsNNMY0k2NMsYUOupTPggVIvGz73NWNMN+yri319t6Pflfq9Q= +VmVrGQo2zRokW/ZuO9bN684ct1InVNowfFSmyust26FWDCXHcd0UemTNTXUth2VboC0EOmXg6vzaA4Ro8MFNXNdxXSqKlzNKA4j6jPhO++I= +VmVrGQo2zRokW/ZuO9bN62eFz5hnP30y3wtyekB6DRbzyysSkha/M93Z9tUVsPioFD8M/Myvsauv7CKZIsUF8bN+2irFUFxyhWCGqDkE96/DoEDPhby03UyOqQUOnNhFQH7+91qtwIUZYcHjf+Pj0/TeagyZaNvNNVB5JJTm8vk= +VmVrGQo2zRokW/ZuO9bN6+NM+EEAOXoul34lVekTk9XdEpwm5N6VOdmYCRu0UZNDeYYo3JSnU7nGyfWq5IWysj2+Cbclo86tR/GmgN8a0L4jIMMwhPZ25sfNuOCBhCQ/ +VmVrGQo2zRokW/ZuO9bN68pj4OAM9ceLEL0V8G9Pm0M= +VmVrGQo2zRokW/ZuO9bN6w63gxf8yZDAhmO6QX8vKO99kRo8Tw6ZtkZ8S0o86rGGlhSeH6EcFyzEAEBZLglLkUsgn8ynefX876R1E6+Q838qPxsIQ+nTjMcmjFwk6NfA +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN6yzRIO0vwutUgOnYoAZiENfKGuEEla9CfQfa6gTxunqEsZiyxjnfoaBugkUvvXr2Rw== +VmVrGQo2zRokW/ZuO9bN69nE2sfjmDVttSaOgdZVlWJ63QYeqvyoAYe2eLCe1n94fhdbFrmlBBGKELpX6vh0Zw== +VmVrGQo2zRokW/ZuO9bN67OLNVXM5ci5HUbiKSxFONHiC6Xt4U1aS+8X88zD3fZq +VmVrGQo2zRokW/ZuO9bN67JgsGJtX6EgeNIetmCvIZTDKouBEO/dMjDCbSOu57B3ED/vMQt5sqDDht16EZufT3i2I7twNbn5Ks4qihBXVwY= +VmVrGQo2zRokW/ZuO9bN6yaCiWSg4HG+i5+Du7fqu+RvYiZHs1OKN/b+c06Yjb7eN/WozKC4OfXfeFilLlqgmQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklhYQuSnpbSGQ82T8TKI2z6 +VmVrGQo2zRokW/ZuO9bN6yMdhcWuddd6xv9F64cBYywreWSVvaR5B3yfWI7OYuMP +VmVrGQo2zRokW/ZuO9bN6/vn/3cOWDwADLDcgHwMLeI3JY5bka5LlkKpq0MYXcsoB7UmUgxFfYsvrjsD8DNpiw== +VmVrGQo2zRokW/ZuO9bN6wKd3hA771N/TeGmptQceIYiS5uPTHliZojP2k7ZtxzSHJ/A7emyoPtxPldUadDQ1z4YB2e03b3LCuUSznXaCdcatAod67kNj0SyKlsbUFaD +VmVrGQo2zRokW/ZuO9bN6yFoyEK37tDej6I4O17EJK459bjz0b2aVOFysAY69AAI +VmVrGQo2zRokW/ZuO9bN67rthG39/u+hUAk/TFHDa1VGBOFJwoQOghMtw0t9SgUwXuyF/hRrB+Gf3XckWgDxww3QLz/Gu/peTY0Vd6Gg1xb+KAX00Wk3vr1B5OMxTI7aQ/Ifoh11zpKu/NDW2I0hGmizjni0htGkt74vDHRxoVh3MSUDH5SZpu+51ji4sZAP +VmVrGQo2zRokW/ZuO9bN65K4OzhqN62rdPrqAylIxiSfc/0Q13hiTjBH1bGNcEz4aPPkPX5svr4sN1Ftu9vIYA== +VmVrGQo2zRokW/ZuO9bN6+hkvN/q3FP5RxK/Rg+svANXm3H1HT2pqctOKa00PJej +VmVrGQo2zRokW/ZuO9bN6yju6Ue5iIbZkqhzYfgvkrySRMheiu5V+Ta8hxwUEYtyV5yMYCJkOp17i9yxEqnSoAsVha/5IlfEIG466h6aA9ABaVS8/6yuKdQuxOEv3DKPUhMOrfnEfM8aC4GqWgHrnhfMnM+XlC7Uxi87ysjRKWDOrWQU6YBXRwjupMCbe0SEXZK9A8ozplzLIcf5aN/JXdGELghOktU+LBGwgiBuIfji4VuamEV2GC4h1ZfwrRQb5QJjIufhNn+QKkNZF7kvARNPPt4X2NsrnHvMVkl22BBGRWXXuvnBlCRdn1kGgUiCECwtjF8r8qnQy09o78UxQQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkldwb8jwt9DxZpgrJeOzJ5Ij8OQIUCa0CerXo1Qo3bw3IPKZ5V8SctaW+5DIK7QWhk= +VmVrGQo2zRokW/ZuO9bN6+hkvN/q3FP5RxK/Rg+svAOJYrDIO7s7OB0iiKzDZl59 +VmVrGQo2zRokW/ZuO9bN67rthG39/u+hUAk/TFHDa1VGBOFJwoQOghMtw0t9SgUwXuyF/hRrB+Gf3XckWgDxw7DLMf5H1eEmfPk6cdOXzW47In+/SEGy9c/a4ZkM6IZjDOboo6FQzRVZNiW7ZTA8hw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehptp4khfwBylvSeT1suy0diqoTvxNS9EMUDiNnHRZaYX2N+xEgg1O4D3bbS/Ud4Du8qxQtV3GSlMBzkn2zpP7PCfLt+zFrmQaQ8dqjYJASJzEJjI0p44nybI7rSOX1Ur8pzKOOL8SG6FUY8ynPnHXdBRwBLnSnIn3C69Oytt+NtUsAj6NlXA4snQKXDdq3pyeyEhcXvMkv5THWk/K9RxmMqA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklZleNLu+NU2fHIDIK+5y42Se4QjVlk3EpMo+ZcELlSW7vLZXtQKUZ1BEsv4QUd1oM= +VmVrGQo2zRokW/ZuO9bN6/xDysL+wsJSb13AswERz4dbP32z4nnYwOYj0CDjnhOh +VmVrGQo2zRokW/ZuO9bN65TLhtjykfNX8WJm3Pk8YW+WVkYXvKuXVAWeiZcTVClQY0GxZVLKZco4wIWMwgiDQXjteoRkeHHNPD3UcQj8FGg= +VmVrGQo2zRokW/ZuO9bN67CsaWaIPoNPPv9rrlUCpWPQJYFKEdBkMv0SxvLDOQ817rdfMwkgWxpkGk9a8pCge7/S+HnN4lZX28CNzWerH014+BGQD0z4R7x01aJw6EXHRYVXV9MynwKzDH6wO3RcXbSmldK4BIfvVkkZ9kP17fX+3TrVTnh7UmvSBwgDROjAAm8UpQj33+2wvfSvu9SMyFi15HP6t19gWxOVeFZtx2N+CTOnrHuZ91CltWAghlaAVlGlx6T42OXYkt9M/UJCc3tfRGJNF1KaelYVCIS9HfY= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknzyrk+dVp6PmqNpyPq6vZZnihxgqkTAYKhUWLqhmAkYwLePWapfwKt03bgcDUCeOE= +VmVrGQo2zRokW/ZuO9bN68IVqk5miXE2mnKv9puBimRl2KBuaeKnnVtZiU+rRfK4sSkVydSM0QE6ee/tsEEwaM6zcOj8rcIicUVSnuqtteY= +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZcIBa2uNfov+/XeRNdZe66MEk1poQGPzb9rX/JgATA/bMUXBS/CbKPdTay5uZcZ3sYUgk9ezt/r8bq3kLsYMmsQesgDWI6MwVt3ta42RiBxgoIMVS6ETH4RrMqxHchWD/8l2ne5CpfrnuXoy4Qf1BKH +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZfSUix7t/J02EKzLYN3W7lneCAT73Pg77ICBvCCeR7hahskumb4s5Qhf5/YpDeUBLJNhvZfKlRbiypJO9SGwHZTr3sFcR9vQT6gpuX3ULVcemq7uN1K4SJeWJzF2xZfztqyzJLVMc3GuAIpeupNq1UTK5yfbeWF+zRppUr+865LfO9sEUKaS0qPDfbiKbxRPGV/qsky4VKryRpka6kLCFDMdueo+Bz2tGJOoGEN1klX/QTZmvX7EEqQBAfK2/7AMecNmvc/63C18FanCYKATtCKUJkCFpKQmiLJFg/MhGd3Dw== +VmVrGQo2zRokW/ZuO9bN6+/4QbgLnH6ddT96QVU2BWIrsd3eEvEMj5S3ByOD7Cmm3QMSJN2q4KX4YNvBGAWi4w== +rWUQK5drq2FAykvoqvdEFxoGIhNxRjfJiH+/FZVNPLeg8Jfg4FKs2Fr94MGjSAK4 +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN65PZoHgqHeg2tPblo1krTlxzlv5EOqp5opCmAyCAeKT45K6wg8ejAyXR1JcE8YF1SA== +VmVrGQo2zRokW/ZuO9bN6woHMy6hb62x2fhlLnsl631XUsxwnZL7Y/DKLi3T2mQs66jvP/0U+MMKUttSSGlCpcRD5qxdcOcpSgZdBtkwlus= +VmVrGQo2zRokW/ZuO9bN6yzeHL+uw3HF0QgQWqkuWpLXViw8Q08RxhcsgNDUoPUdEWAcNJd85Weog2y46VgzAdqS1DwDPxix7MAz3WaXam0= +M0ZySqkmhuHCw6olbCKv95w0wnb5tXdVpvk+nm5slh1G8xJLU1xuHApKfUAMvlbG +VmVrGQo2zRokW/ZuO9bN61Jo4bcSJ4kcuwCfHogcpLvrNPD9zJMAQccIRdfKWJAC31+hQ9ZXGOd8d+LhnFiWN8BpaSyxK14CiuXsSZrbI3hL/0ulrVAR9WWoHJRHnxHbOm8ok+YUHaagbP8z96+R7Q== +K4yn9TN/uzMe9BBgrE1owVggOblz5V+u7pKTqFtSQHL3iIEokAVpreCfX7C6eBtN +F+/dN6XwseDUiXkUekEXT6mO4mb6/XgfmHVOqlLPeww= +DPgKxKUksPJmBI8KfD7MnG19wQLsGqEChmw0QoF0cBEGNXse2EH2RoQjVnGDcYmdB0GUkNwww4YhQ8aieso8Hw== +DPgKxKUksPJmBI8KfD7MnHMEClnTrZOgWuhoya+UtfRGOBC1uS5ILl2CMZwpFEHo6fFfTMIb89Rz87f1qgOy7zYaiIaklAdfGPQoGliv0z/2Y2unPwdN86OTnYU524u8 +Nm/97dX7QPdHIqApKEes3DDGlNYTObtQW4TndMqXq36d2O/XMJDcOXXw1xcOIV/CXht4Yme2sDhmwflt9XyXObZHIkfqN7uTVhRogjoM7kx9b2SsH0KN1XeM3xwm1QuR +IW3EqlnnkSmY1/JPGNw5+3vURuvsnXAvPp4bO9F0tPsoflnTjqJSRd8ZzMGHdglHHfxLeAi74N4KRzlqNSRQ2ptalZyi8UHTJ1zH0cYyr406TJ06FY/tbiwgCjZHi4eVd4h3DKYyqPMBoM2LyQdFGg== +wlLHv6kT3Q/RmtMBN4nDAZOq/KkXwjmJXU6lIwaeSQux0oN4tIbhF8ZR5VYXdOa8 +VmVrGQo2zRokW/ZuO9bN66tKItbXQ2S07vvESMb0Xbx3fmLgrcSLbgPkYQ0cjDs4n4nwgxuwWO5ZRu/l3LP7KA== +VmVrGQo2zRokW/ZuO9bN65KPamIARv99cFn9NbPlKuvW0pRhETj4/81+ZPDZcAKs +VmVrGQo2zRokW/ZuO9bN67vH5ZnhExKao21k1gaT2G0FW1Fan/3LmBluWT+lEhtBkl8E6G66TjuT3POYVcwWj+zIW2FtUL84muoV4/VPvDE= +1u+XjG/2+GSQRv6EzCaWRQ== +wlLHv6kT3Q/RmtMBN4nDATC997xtl3+dwIM3Vjo1fNfYpvl7rJe+1k7mDowOrD7Q +VmVrGQo2zRokW/ZuO9bN692I9BHTUUNtPoqLNpaHMVuAxs6D7xvFG9y4+GFpJG4QYj/otOtWLL3aoqR3r2jKZw== +VmVrGQo2zRokW/ZuO9bN63oQXh9DZkh7TCvG9sp3gJhiav0QeisBSComydq5Jid9 +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZcKSoZD8wPjLjehW2rIbxpWCeB5SDWc7JzgrKKoXCb5EA== +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZfPeLJTt9nZb7Y5PSF7XoobBD8togGt22mVzBahmkvECA== +VmVrGQo2zRokW/ZuO9bN64aCc/sRPofqjT6zGI12oEVVHCITULKhIq+Lgy4/mXUYAgwHGxd8E4XOx9XL+uCCFOUHXOvKRFjQS2RPn1Qrhn8= +VmVrGQo2zRokW/ZuO9bN684ct1InVNowfFSmyust26FWDCXHcd0UemTNTXUth2VboC0EOmXg6vzaA4Ro8MFNXNdxXSqKlzNKA4j6jPhO++I= +VmVrGQo2zRokW/ZuO9bN62eFz5hnP30y3wtyekB6DRbzyysSkha/M93Z9tUVsPioFD8M/Myvsauv7CKZIsUF8ZlcrcMflLDlILvfcomKvahHyi+BSgElsOhxz5LvpeJjUuRght49ekCUWF3gL+z6saGhSF3KvtEuIE+XtgluEL8= +VmVrGQo2zRokW/ZuO9bN6+NM+EEAOXoul34lVekTk9XdEpwm5N6VOdmYCRu0UZNDeYYo3JSnU7nGyfWq5IWysj2+Cbclo86tR/GmgN8a0L4jIMMwhPZ25sfNuOCBhCQ/ +VmVrGQo2zRokW/ZuO9bN68pj4OAM9ceLEL0V8G9Pm0M= +VmVrGQo2zRokW/ZuO9bN6zB63Ivt2xCMJfE7SCLw43s= +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN6yzRIO0vwutUgOnYoAZiENeHqGqZbMOarS/0U42fodjXqH7Cei9mZkN2WxnVngmsrg== +VmVrGQo2zRokW/ZuO9bN69nE2sfjmDVttSaOgdZVlWJ63QYeqvyoAYe2eLCe1n94fhdbFrmlBBGKELpX6vh0Zw== +VmVrGQo2zRokW/ZuO9bN67OLNVXM5ci5HUbiKSxFONHiC6Xt4U1aS+8X88zD3fZq +VmVrGQo2zRokW/ZuO9bN67JgsGJtX6EgeNIetmCvIZTDKouBEO/dMjDCbSOu57B3ED/vMQt5sqDDht16EZufT3i2I7twNbn5Ks4qihBXVwY= +VmVrGQo2zRokW/ZuO9bN6yaCiWSg4HG+i5+Du7fqu+RvYiZHs1OKN/b+c06Yjb7eN/WozKC4OfXfeFilLlqgmQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklhYQuSnpbSGQ82T8TKI2z6 +VmVrGQo2zRokW/ZuO9bN6yMdhcWuddd6xv9F64cBYywreWSVvaR5B3yfWI7OYuMP +VmVrGQo2zRokW/ZuO9bN6/vn/3cOWDwADLDcgHwMLeI3JY5bka5LlkKpq0MYXcsoB7UmUgxFfYsvrjsD8DNpiw== +VmVrGQo2zRokW/ZuO9bN6wKd3hA771N/TeGmptQceIYiS5uPTHliZojP2k7ZtxzSHJ/A7emyoPtxPldUadDQ15OwaiosCVmwCzZO8uObu34= +VmVrGQo2zRokW/ZuO9bN6yFoyEK37tDej6I4O17EJK459bjz0b2aVOFysAY69AAI +VmVrGQo2zRokW/ZuO9bN67rthG39/u+hUAk/TFHDa1VGBOFJwoQOghMtw0t9SgUwXuyF/hRrB+Gf3XckWgDxw7DLMf5H1eEmfPk6cdOXzW47In+/SEGy9c/a4ZkM6IZjwFbCBfLcjuN1ph7dXL69NbEYqSHAUYrdgovEd+6R44A= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkldwb8jwt9DxZpgrJeOzJ5I120dhk8M+FtTOjxWb/xzhxVLKAVuFPJZ0WZ/dGerni0= +VmVrGQo2zRokW/ZuO9bN6+hkvN/q3FP5RxK/Rg+svANXm3H1HT2pqctOKa00PJej +VmVrGQo2zRokW/ZuO9bN6yju6Ue5iIbZkqhzYfgvkrySRMheiu5V+Ta8hxwUEYtyV5yMYCJkOp17i9yxEqnSoGaswzu9+McwwHad5v1bjq0= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpWAZDWQYZM9u1cxtCvNhbPooL0ajh9MpCi4Hg6mHWFepIrUaELGkT8BlfyIopeU47PGFEELMdT7TSYIQ0qNiPmJGqasOXFDTjF2zWDf0dAS0l3lnIwfdrfU0GAHbuHcr8 +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpAQPQTfFcASsalkbD4pFXaseNfXO3/kTNf61EMrDi+Je/7g8ehzlbUlTX4aZ1hVLch+r1EfurKSDSPnqHD8HY+hSS15SwBbExPjTnF1IBBX4= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklZleNLu+NU2fHIDIK+5y42fBSjXSW3t6amo3LLHZ6byf2e2/PI7Tj1IoF9SPmTQlo= +VmVrGQo2zRokW/ZuO9bN6+hkvN/q3FP5RxK/Rg+svAOJYrDIO7s7OB0iiKzDZl59 +VmVrGQo2zRokW/ZuO9bN67rthG39/u+hUAk/TFHDa1VGBOFJwoQOghMtw0t9SgUwXuyF/hRrB+Gf3XckWgDxw7vyIGIrTvsqSwVabH94NPs= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpdgMJEzyO03tJBQm2adJkUrRWBuS6Ey7sxkpJGNCZ/ath4Am5fEEZFWjHHvvOeERK +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehptp4khfwBylvSeT1suy0diqoTvxNS9EMUDiNnHRZaYX2N+xEgg1O4D3bbS/Ud4Du8qxQtV3GSlMBzkn2zpP7PCfLt+zFrmQaQ8dqjYJASJzEJjI0p44nybI7rSOX1Ur8pzKOOL8SG6FUY8ynPnHXdBTDnIRva7UUPKtL6AHaQF08vtcQm7NpSUDmKTfcHj9wcFSUkOspwTbuxcTbJ8bbnpA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklZleNLu+NU2fHIDIK+5y42fBSjXSW3t6amo3LLHZ6byf2e2/PI7Tj1IoF9SPmTQlo= +VmVrGQo2zRokW/ZuO9bN6/xDysL+wsJSb13AswERz4dbP32z4nnYwOYj0CDjnhOh +VmVrGQo2zRokW/ZuO9bN65TLhtjykfNX8WJm3Pk8YW+WVkYXvKuXVAWeiZcTVClQY0GxZVLKZco4wIWMwgiDQUmeYSvaHyOxGnQt5qzprsw= +VmVrGQo2zRokW/ZuO9bN67CsaWaIPoNPPv9rrlUCpWPQJYFKEdBkMv0SxvLDOQ817rdfMwkgWxpkGk9a8pCge/VeLlYftLVcRLaZqX5ex29GZk48TuAk8VjtoY2bRUdZZWLkBdYDgVaIsMZT7g5QjcvFH2Luho2y2TCfmiZp1EUDPOnkiH61//ooBkHid+aYP/rRJzr1darh4Mq9VWhFlUiifB5crQ7DrfsgqlvjgkvCtLMuCxVVuJY5aa8QABaw/3Q0mHKYcCm7hQZ8mkcwGw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknzyrk+dVp6PmqNpyPq6vZZOfE2049o5YQczTseRZhSU48lvYICEhHh8MGvMwzXDTg= +VmVrGQo2zRokW/ZuO9bN68IVqk5miXE2mnKv9puBimRq9Q/v1MO8qgdK5uBK+6EHJyHxhfkOmVNW09AfJgsvhSwjQuPDOrwSPWH/kVPU7Co= +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZcIBa2uNfov+/XeRNdZe66MEk1poQGPzb9rX/JgATA/bMUXBS/CbKPdTay5uZcZ3sYUgk9ezt/r8bq3kLsYMmsQesgDWI6MwVt3ta42RiBxgoIMVS6ETH4RrMqxHchWD/8l2ne5CpfrnuXoy4Qf1BKH +VmVrGQo2zRokW/ZuO9bN64EUjFb66du5csfmqn4guZfSUix7t/J02EKzLYN3W7lneCAT73Pg77ICBvCCeR7hav/zdd9VH7r/m6sUb1HxqmhsvpSJhPoVpKk4z9Vjw6MAQ7kvU6PJlyy+3C1oiCIuW6ebkWvm/6qoMHlI53uC2HoxE8lugv8/DgfWrz3cBv7cNjT33H9uv2A9d55LgMqGEgzS0+MRIa7Q7COyOBgv/l/M/OIYLIWYbtZ25RMtTNjRmWhWsVkz1UV0KR1oCJcpuGNegIFtMVTWQTmQL6pnNCpb9j7EvbZeoRKZ0wobGD3wuS+RsWmC/cLLmfEDdhIQIg== +VmVrGQo2zRokW/ZuO9bN6+/4QbgLnH6ddT96QVU2BWIrsd3eEvEMj5S3ByOD7Cmm3QMSJN2q4KX4YNvBGAWi4w== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN65PZoHgqHeg2tPblo1krTlxzlv5EOqp5opCmAyCAeKT45K6wg8ejAyXR1JcE8YF1SA== +VmVrGQo2zRokW/ZuO9bN6yzeHL+uw3HF0QgQWqkuWpLXViw8Q08RxhcsgNDUoPUdEWAcNJd85Weog2y46VgzAdqS1DwDPxix7MAz3WaXam0= +M0ZySqkmhuHCw6olbCKv95w0wnb5tXdVpvk+nm5slh1G8xJLU1xuHApKfUAMvlbG +VmVrGQo2zRokW/ZuO9bN61Jo4bcSJ4kcuwCfHogcpLvrNPD9zJMAQccIRdfKWJAC31+hQ9ZXGOd8d+LhnFiWN8BpaSyxK14CiuXsSZrbI3hL/0ulrVAR9WWoHJRHnxHbOm8ok+YUHaagbP8z96+R7Q== +1u+XjG/2+GSQRv6EzCaWRQ== +rVN2gQsrcQDlbnpSi8Z8Q6rgn/0T7hQWvJY4GQLMIs9AIys1inq5DkYPMgls2o+mJW3SfNTEx/GhhDY0TQd0UB/KkXGzZbqKcvH8j2fDwSmuGfxlLBtk3AbGZSvDtpoSGwuzLY43M5ISj8W0y0/imQ== +9cDZy0qlZW3l1x5OBfxnvO0ZOfno5ld/uuXXhKKyg3uIcXrrtMO6pFpU0DI5e3Yx +GCn4vkscBQCyFsJqT8NwR2LqhGP3NYb9FEma1BX3QpWYg74ASXZiriukTidAkbQI +NAimcGcce9/2qY2fTDUfwj3/O6QRkR+LZhxD40hWYL4TxYQ3miCx5/h9nbCzTANPM8KZYeDE5eSEQPxFH78dvw== +NAimcGcce9/2qY2fTDUfwhcqLyAvzPw4wNTLao2yokYlX1rKhl/FIVm4Lgve1IVc76RE0Jqa7hKlMEraqRFBxw== +PNZcZ67j5O9c3iqbWgpxu2EHvTQ7URoTi+/pefkS6d6fbDFw7ZBZnx6O9h7ZvR60 +xWoGNWjKGPfI4gq8aHoTfEof00xZubKgl7GPVFGSDVetH85R9nTnrsUt0fCijUwRlw/sWM3BY5Pjj/8zJYtqDA== +6K6zXmJJNC0xAXfJ6TCmfWKrZRJUke7mWTEHsPav/5bJPLpyykZ7KCd7fBI5s0BC +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj2I06OqcNmpw9TSQDmoIjgsN8SILLl14ekx/mYMdKWdhA== +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj2hTItQ2Ij+1snSKd1v7XUNy/e7NOVcHIv3beElF9/qQw== +RJ0Jr3va304T+IIxEmBMaDZh5F9oISUovbDJxgb1Fci7kfpoP2KFguUQ2iicYcsiJw9j/EJ8UbGkVKJAnZcOSaYZsaFa8g9piUwzwxW1VnQ= ++Clzt950thRTm4LwIgUtPhyWBqEsgJp1+aWVuN4uzhj1mySJAgcqukdWlyarfnnKaL4r8VoOp/K2eGKuJ9zO9KtrBdNySU/kcg1SUQqE5Xj4I/Gqu97OR6xgDaFUpIeq +xWoGNWjKGPfI4gq8aHoTfEqKjy/gKwJlHr8gW57qOoBqcb0urvlg7lYsKGdM0qdHasQGA3eLdYAyBwzMOTeIzSnoVbpqljeEGULKc9z/qVerE6OJNKH9bfgPDxyaWhBt ++Clzt950thRTm4LwIgUtPgVr3Pc2smn6BhW76Xa4ivk= +o6QhOIN2Sc4SHELnst17uccQsGoG6l+gSpQG5tq8A0nmmKRHpnDq+7te8JNh1HhV +IW3EqlnnkSmY1/JPGNw5+4LhZm/frK6qv93QOVkJnqJWHyDzktjmb22a4qC7XhwTvmUv976mKYyq0xbMDgaj3w== +0Cj55jDWeNf+rIE9r3fPh6JSGqHoFqAU4HV6us+IVRVh29MQUSpF+EBdHBvHUV9G +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033Zi5ItsJ3y41J6ZURymCdQyD+cPGgw2n+a8rLGuLKEUNQ== +dV9xz3yyeXnMinI4Y9Bh+tDzHweRiR3OY04CwsDSrEzJ8NdJW9o0KROEfFeWty1m +VmVrGQo2zRokW/ZuO9bN6+H/V3XeapzirFfkI6FkhjlZtbue/6GBNBtcFaRV3lEw2dULRqauw8C8XrGfYteMzw== +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +wlLHv6kT3Q/RmtMBN4nDASsdDC1+/U09X7W1/sF+PBgDYxQHTs6TeZfbpjCRfxsR7Ri0v1pNpacNyjvn+ZAWcQ== +VmVrGQo2zRokW/ZuO9bN6xIPRe73WG3trsUg7zsddsUGML8FxwJqzkawilmA/yG4 +VmVrGQo2zRokW/ZuO9bN60b1xUHVzhvqjGgJEFa7z2PStb70jOyvc8lS3Di3xXvE3QFnlqvAtgdrxSwXHNUW4Q== +VmVrGQo2zRokW/ZuO9bN60qVoAbkZpaJD4xo3DSlycVxelswpyldpCEVTkQNyWer +VmVrGQo2zRokW/ZuO9bN6/A/JH2g7iLFpPjkuo1x7nSreL7iMQZNdvu11f1uZaBYeeVwK0zeqqcKrot+B7LvGw== +VmVrGQo2zRokW/ZuO9bN6wf3F3T/p04Y+AmmtjbatEIGoQwyQhVz/a/AzqStaf9rhOzlP5uUixaG1KHUTIK8ow== +VmVrGQo2zRokW/ZuO9bN60c9qsbDvegp0EbVxa/vRG3fOUmkm9bGganfHODTzHqXy0UDvXB0g4VA0t9QGBGbuw== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6+rB+IyBYXcXUy46qig+BtwdYdqD+TxxLhWkYSkJmiO4RFsna/dwqHaJ4pO+KsmBBdYrBgm6RWet76WBcw59cVY= +VmVrGQo2zRokW/ZuO9bN648jccnJRw8a2i6QrfCqObE= +VmVrGQo2zRokW/ZuO9bN6zEqFF4QC6LTSGFDZhIGz7GcR+ygVS9QnYBnqBZxUAT9 +VmVrGQo2zRokW/ZuO9bN6/Vwcya7gDAvJTKuMpYpyFlafJ/BPYjtHMwkK9de1BoC +VmVrGQo2zRokW/ZuO9bN654Enj8yhWFV20SJ83piZWE= +VmVrGQo2zRokW/ZuO9bN63So1HGlXm9PaEG2pZJhSdcK7+Mc8DRjd5XdH1Tqy15w +VmVrGQo2zRokW/ZuO9bN62RPHcQXi3ZrUKSXDdSHHUIE+TC5rWLejWhcUHBiNr87 +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN67CsaWaIPoNPPv9rrlUCpWMcwkU6teswneWijOIA4pB0 +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +M0ZySqkmhuHCw6olbCKv95w0wnb5tXdVpvk+nm5slh1G8xJLU1xuHApKfUAMvlbG +VmVrGQo2zRokW/ZuO9bN67RqmNj5b9Yl7yAom+TwpFzVrmvT/HrQJWe28Zbtkp15 +1u+XjG/2+GSQRv6EzCaWRQ== +BP5df94zrjqSm8bAbTOrL5GvmURzSLlleO9d/CPt3EdSw9Np3rh+GFXgZddZgINx +MOmjZUF9MED9E8RkMZXTKuAZ6lHL+/KvEyaqYJYD8unbnnXi6XeQdy2cbayVl0zR +24o3icEhYfd+ebd6821V1vk+Ob66PUIOpthX78JkZSOWniOIUe8Z2y/ETnuz7vmR/ZJhFufm/OjJnLSwzQAgzg== +1u+XjG/2+GSQRv6EzCaWRQ== +2+RceSNjD2iq0sRzepMysVUxXE5N/Z4aOUZt1dAV3sWaDWXqIfjhxQTvYnq4ah3QQ/ApXfydcjDZjJErj9wzNQ== +VmVrGQo2zRokW/ZuO9bN6wOXRPNxR5cZD1m/7kptrG2nCI6NBuWJvllrDx6o+52L7gJNIS2eWfBcZwnn+hz7K/wu0xeSDGe/V0C2AaBo49gM9vt9cgZtIdxSk6AReeoDEynV2qmrTB+ZwTT93iZJMw== +8hsf0L9bk/4d+fdV9pZPDw== +382wTcvh4neRg+mxPFc2rCefgHeTPQCz7FdbLHHgNfvV0PAxIqkvHsu8vBFlt1WHkYHwpigjrRPDkt0HxLed5A== +gxkHD5Myi9QB+wCuYFL+wWc3s44uTroMHNL09BK3V9Y= +mKLWhD6ySe40lXWfdhAlw/EiKhvOuh5IjHI66mvpGPOkWDpO8c3ZlbHXSZlyKiJyW787HAHiImrxsDCjXxrqfQ== +382wTcvh4neRg+mxPFc2rG2pU6jpYp9eTnamxOu1i1A= +o+u2uhgAD+6IzOb94R7i3PsKHQ4xrrpf81AY40SXV/ldpAm6Hc2r0ESFFDSIsUib +WHkOzVx7seuLmxs5Hu+l/rPH5+mQcujSxbSmaq50dGAumaYdp++Urceo/sFK7JDvNePZE19AENBMudTBCBu/+w== +3zDbxVuaFCI1nhEYDQoyM8I3uk74D7lNZO/e2PnOR4BzKyhfYbGMLSvwwAa2ovdbrVyoeiBr7HltiyLXLgbQu3J0cWKS/0hxGK7UrO+IPmY= +xWoGNWjKGPfI4gq8aHoTfEqKjy/gKwJlHr8gW57qOoCwycshGI0AFaixups5pnycze/ENiP7AP2bo0m8VIkIQtZ8Z6Ib8E1lrQyfpi4aEqk0CpBVwAeefhCrk3Kq1B4N +l2FJPs4YkAmmok1ulDRuSA== +xWoGNWjKGPfI4gq8aHoTfEqKjy/gKwJlHr8gW57qOoCwycshGI0AFaixups5pnycze/ENiP7AP2bo0m8VIkIQnZoroz4r0AAFpXTko/h6PBTUzIwhFO49lcotmCqudMD +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6oNJYJ3oKHQnTz+Hw7wWSQ +1u+XjG/2+GSQRv6EzCaWRQ== +iVuFz3/PiBaH1Tva0mvaRKzuSK6RTdeIraH0vCGAPH8= +Z8UsPk1Q7HtwjRd4g01ryw== +N/c3VKyijhiHyyJkJRVAXzxvXhx9eCkM3b+f7TKuxvcDiazOSxHmadhOm9IqzB9i +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNUZltf51gbntc2j7XAPs8+MGpwQUGSXnr1GRlhLGDckpqtG/8Yila5edTgq85180DA4s0FUMiYZIMEHXHAnu700tADqDXaTPJos5uKYE+e+d26TSIYJIRFyM2NK9/wxBqEEA5Fb2NfuB+8HINWgkQzO +b4OJVZe8QyIpjuTpKXDL9A== +Tf4gSxOqrVf/zV4SjgKLZwbhuXlNuItVIwTZNlpCQLJ0Q/RDC11dDCuy56JlBZpAXSExNRBKtSmzN8+NglHLL9DhtI/OznA1iN64ZpslzaoNU3TiNZtXYT8Rh/DavDSZFjVkiLBLDjQrkb5s0+lgBAmfPxb7nPrkzMBdWsgKoS3s7FRd1MXukyuE9orAplI3PQ8N8NaBde9596Wqnw5U9A== +VmVrGQo2zRokW/ZuO9bN66obcEt8FvVttW4yYWEH6uIA7s/HoyUpTNCCqNXuG0ea58Ui4rsjqKur6hAMNcrcMx/at0bQyXzWyEII9AQ7KjNK2AeIF0eWTCrs4Snj1qaEKkv0qPpMC0x9MTz4WEf6Mg== +VmVrGQo2zRokW/ZuO9bN65lxcp/TK9w0q9jz7GHWjKIlBr8LHu2cXmcQ2DsHkT1/8rPPDJdw5+/SjWCMRz1HD6RKMiShwU9H1dKRDQMgwsVDUkSArWZRzEgDv/PWeBFb +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj0H4xphI80JaVwdOBqT4vpFtnVDTN0hWgNIS/91N0m4FCPqgIhArHY8jwdYWxzDKrBk59lfM+i8sqN8QCKbMO7OP11K4RR8s9DIo07Xvipy3NSIj+dcgSDYNjADQTqLHTrO7javH+mxN+lydeM4yDOI +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj0Go3ZdM40X+nb01il25Cd+AljdpR/WSOL7piwzm9D4WcaH6KL8uJyr/VORP5gWAcRZ1zHGtdYKybNNqyW10mUeSUktNLRhZDDQn3EjSdQr5g== +A50UO/kAI17YP7MCbTvBkFQ/jf5eMs11PzZj1m3IBsQ= +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj2OXw8C3o6E1wLjd5wbvbsNzy5l5ApUuk5qBPz1WdMnlw== +1u+XjG/2+GSQRv6EzCaWRQ== +Vtg8xVOtjpKd+p8vaXfWJRm3BenTCuwYX+Bxe4j96OlTkLr1UKAMKSzEVyMv3B9m +FOaMsCyYl1K5BM/R7S86ICr1MTzEduCDbdR5dv7vfTdB/FZw6sr77rwVLL+uV9uw +E8W4VRiGsaFlC63shQT6iGQ3NT+hVNaOOiy1bY8KRLHgpbbZDfGGu0nZ5d1Wf6Cu308i6+gPlIv8CspUlVbYDX50Q5QfOTZIouZfjH/opqd98cmLn+YpH2Nbl2Z7XUGT9T8gA9FQUFuOhIGTZcsfnQ== +lQJjRn1YGiZ9pmOVzoRZH/LzOv9nkxZA24G8DNkRdAhOWCagTJeCmUveyd/p/vmUlVOMXXoQY+Q5YnuX9s5XRh19ZxQQcUHRd97rhfiUx5M1ahkZdWtUEoalumzyvb4S +lQJjRn1YGiZ9pmOVzoRZHxukC7GWttKaP+XUscJXWEggaM7c22lTg2UY2Nb/ws+Mb7D7WFU1XcMVxdR7sTThHE4TD9Ee0zQVbxVFEAr4ucg= +Xsx+eKeQ2UpKLnofos5x2caURkwi5/bjNLjA2duBbgRiOKtrTQAenMlL2o3N7YpEDwJKG6ztGICevAUUH+3QcpW0T0uAA4EXdsa9UuY7Fl0= +o3kxTO4RwuXO7GZ9wpXG+fb8QBpjTrjWHeDnUy/AgYU= +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uXTeuYyqadnM9vezgj1BtYH9u8offJe1HA3KV1PTtnzL33A0OfyNVLZeRc+C0JHwAdXW16ZDl/Sm31MqyWR8jotHjt8vvFRc9Z4dGbarb923 +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +fxKEovgMcVETu4isdJSo/jhZxMQfQuYY1Vy5f4FcYio= +KZTmaJLp+FU9X93j5Tpqtw== +bXpQuJfgvk/X2lgXTBSBmmPIeohiQPFvf9agMVKrPCpiJXvay09w777OBeP4X78N +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +yqOVJCkAGOhqcQYBOo7yqW1oXCc6F5Djl9me/lICq8g= +Z29bUbmuVBrM98CRqOY4amLs9Omys8D+fr/4IELH5v6WD8rdrrF0X1tdK2GsKzgwLrVhiaJc/nfUg0MItfuMmg== +Z29bUbmuVBrM98CRqOY4amLs9Omys8D+fr/4IELH5v7ZyI1LbiANbBjGOIBhYM4BZAyiTObbg9dRbqPDN6coXw== +YfTgIRhahLXe0ss6mxVGGcSQ4eqMOTJWPxuhr+fHH4s= +744+BgzK3LVsSzMxY5sSrw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5g0grokREAXB9h6KEOQ7SQ3rmh2uBNavUr8TaYBLS9og2llVxjctC8yMwUXWvSYY0lyYX+9/18jVIFLI76aWlxWMeabF029wDZJDPqQV+blE= +KgZhnCYglllfsrazbVxNli3dO57PXUycfNR797BU9y4= +e1qLykdSSG57G+I4DSZ3lPbnd9MDQtXYM1cRPgH2NZY= +b4OJVZe8QyIpjuTpKXDL9A== +p9oVsBgcyiBMjDb8CcPOqFV5wpmixLB1nyXJiJ2HPNs= +wlLHv6kT3Q/RmtMBN4nDAafzR3IJaSQy1F8fEUDT0KURDliq5QrdEI8/0cInAzfT +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvMpc2DCYsr/Y+VR15aQe/co +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN61QY7FRZP3nLjLSHZpK1fChnZ3O3NtYzOWr34yLfe2iFvPA6Ez/VuxipQPOhQ7naEudlyVefOAk7fmuoeCfZQzU= +VmVrGQo2zRokW/ZuO9bN6zhkfconuDiQ7Hi+HHfb092tsLnoojdy07SctBem0iwa +VmVrGQo2zRokW/ZuO9bN67aDIttUw3ewuD5Ela+0fVjSsNlHIVXDPlU35BbI9ASiwsZCpEAG3FaOgv1pdnMAbg== +VmVrGQo2zRokW/ZuO9bN64VbAs0AMhVgj9Vdo6de1Hm9k6jRFiP6pd3OuTpILNmsO4T96zohpHhsxIIJTvpHJQ== +VmVrGQo2zRokW/ZuO9bN69AxjbwgN5/r4H729rZYw+itpXa7Sfd3UqvtnySLc7pbHlYlkwrDJkN4lKVteM6PSv8iDLaQAfsFSZW78PCIWj+ncV0kvU5QvUJdv7cFQD3N +VmVrGQo2zRokW/ZuO9bN6y1qjxtkMU9nGvhd3REzjQlflUcZo8MWVjp13B9YfVOFvwSEF/c3vjwH4gvmctbPKA== +VmVrGQo2zRokW/ZuO9bN65a1lvAsgN8t7bU4ymW4veAsILdDJ60KfokEuwETIFZk +VmVrGQo2zRokW/ZuO9bN65IMmCegrj1iToe3m+KEt2lYR2GShaC1HZt0ENbtcWDT +VmVrGQo2zRokW/ZuO9bN66zBSwHJm6ElN+7ZdiAVZBTvBew8YxafVWuo2yjvJVU7XQLDpZScPIbuHyakPvsAt7g//bPIKP5wqL0K9MrvB0NLkTgDLvhL/Msehj5zQDqP +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN65a1lvAsgN8t7bU4ymW4veAumH9BDyXcgjZGd5H0sUIW +VmVrGQo2zRokW/ZuO9bN65IMmCegrj1iToe3m+KEt2lYR2GShaC1HZt0ENbtcWDT +VmVrGQo2zRokW/ZuO9bN66zBSwHJm6ElN+7ZdiAVZBTvBew8YxafVWuo2yjvJVU7XQLDpZScPIbuHyakPvsAt7IEViCt2T4QtzN9Qy9djaZTr3A6biDXued2Flv/NxGi +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uXr5UNbgUmXTutS1OHze4KAWfW3eA+x3RJPXRQ93lOYqGcInwu3ERZkI0NS5vrNL+sP+PoD3MJwWoJIn1h1ruB2mlWtaP9ATlsd+gksrWj49 +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj0H4xphI80JaVwdOBqT4vpFoYlr2YEOLcHWVeU8XvA6DcJG/8/11CmMgn+dU2p+MiteDOVRuiE+vkRJyCKGHE41wHxivV6nzAK9IrCg/kgKo1r3TbCAho4S/unYWBGxwmrqp+8ZOvtXQB5XA8bjBzXdDymC/ZUqQHO8ss4ZVeIjTA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw58tqRCidnUtuhiV/SQek1uLeqFhZN4Qp7W1PBlJxy9Uij4W4Ylrlk/HX5S/Q3N9gbstqjFhfBAcmwuzubPT0II= +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj0Go3ZdM40X+nb01il25Cd+Ij2ks0QNQ8ckELJbWPuSoOaKJkHC6l3WHCulMTp8j+LeAWCb5DBJpRVpiRip8pDe/KIfBFKsAy1uPllcESEV07XCLR6vETN2Y//6YxPUCufIc5q0ZxCiI0dqCJvZuBn0 +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +wwsYDn3qQ9eA0sEqk73MVuPXfo7SRy5xLbvd1X4NKq8= +KZTmaJLp+FU9X93j5Tpqtw== +v6hV3loTEdQDUP9Ry99+0RAKjxfNw2Dz+nW6kFLJdPapLbTXMY2KJjmTphId8NeM +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKMlEIqjWm8+kOtLiMZRGDRK2CASDX1cIWeRfBMLHYT8i9caoCs1tnMoi/mLDYHLHsrG5pYDOjL5lbQQFrLwKmqEf4eUWu36maCOXwDY2JSwmxjItoWpsMmOWjeU8IEe5UsZSP3vWVanEc8+qzu9RF +KgZhnCYglllfsrazbVxNlhZvo/OKoJzdIEoxUuSu8luaoRrzRCusP9S+W8XyCKbv7XFXz65iKXwYhcIBP+oK2DxE0i5qVgufTG67CahKc1wsqgjEyrxHBX32oHmncRC/beuNoupsN0Dg0UScKVJFUg== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DHoOe8nxF4SPJvnopjp+aXxQW+2hMxHCJlMDYgHaIwV9/agBpo/OaGkqJKy/l1jf8tlaOj9owrGI3tR4SCZF9/fDvrdqc4ZWW7Q4NpEMrhzh39he3y8EB8embKGdQ9gt39z72m8Bgcgl+Bgm5ytGw1nf+fDKO00hvhLOu3hJ64SPpPXdeljrdMa6mNSQ2eoQc= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27guByVel75+n6aNpw/ji8t66pMWEvvfB0qjiOgKN2OK2JnDa1DI8vVGfbIvfmOW81ScJ4skmbFuY92sIqmFZn6hMag5GUZpPnAEdGK9yQwCm6dMYt1piNKwtRFu7LC/VcK2ulORGmq7ygse1vRFE6DdA +td0Hee4NJPblA28GiIcOmVz/iEnSh7ccFLM88eucCwhDxWpOIeDaytxmPiP2pwkpNyGbS3x73U5/Z+SQSDEikA== +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +JgoYwGgHB5s2Z4NsL2JUA5RS8BCOiLnJjVzPXD6IXDZAIwZ6LljCrK4YzU8NX+ta +NcZ6sQ1mLPT8+JyzvATZXEFVG1KyQwPWlwFE3CIvEPhQby6LJAZAkWdC/cDzvIH2FxKh8I3W2yWeMr9En0u9IwE9OI+ifUFV3c8+IYfti3+3SIhgnDLoC0gR+kL4AB7y +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM6xtVlQGubW43oelk8eYxRIH8zFsXQbG1pTf3qhdrUJMf371FEbN3vCj8HCBSwaDRcg== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uQb44yiF3et2MoRPcBFK+GMeDEfpsZJKqxSJcEGTH6Rz+9X1BKI8qQupMymJcWNG5+XC2z1XIpw9nq74i1WHl36cklVUtWYHm8jM5UCo2Kz2zRxAWh3xmytAGDv6LQv1nw== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +NZydr5h7sqaRpfl7TbGhKd19ljGVD0oUWF8Qdy6kYbMjwXI28kAw6q5cS8e7La2e +KZTmaJLp+FU9X93j5Tpqtw== +bqizElyCHvmotMcL7tLUXISKYaW23+snAEAzwAXO0AJdIQ0sKphjO0wpP7Oa2+dR +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +IZj4TYq2HlScfSdRkI+QP2MphWNMq1cYD9ikLH7arN4012AQ4XEbHR7fbs2p2xZI +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+BrcUIMudTtdKnBbOiZIsdm+76h3MsHger1Q6VGZyZqpv1mxz/XRJktIzRZ7/ikiX7QXPA7qgSnErFged9pU+fhNJZ4ZyZU5ZDbDTyTQQkEeA== +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gtyrN3fmhNqvxN36kot5RF2fOWlkbJiEZHzIAHNv7pMzkmswcUK9woTu9LfE/4jOcCk+Ff2NWiDHRjrYsOXg9XYD4MTMx3MoSP5s/abZGj+zg== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5bMAh4sKbNzkcjzof9wZD14bso4do6g8oMHxd8Gzsibd0PLp5P+1G5ALMI5bhosmbepYaf3c6LZvIATzFuMxyU7EZYN6aCuVLFSuSSPc1Y5k= +8hsf0L9bk/4d+fdV9pZPDw== +m5EI3+BXxkQEYKYaxORD+jMrAWQO06+dOKA0AMSR1lo= +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033YYZQbvhYc0zusW/weg/CqL +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +CW/G9plDL3SfzmBxnqr6q87wTQCNFJP1gF2kN0sG2a4Ay72WUFWA27WL0DdCE7Rmca525jyZpBIhzsH+Eea2b2BA90XJYyeqnQeFEhxJ8CI= +ltIZHHUgft++nMUXBsBkizMqTqlEdfzGFVQe64Ee6MnnA8YVQnL0OWdmHGxjivUF +vLjkUA7bT696TSSdDLcBOwHGyjTF6RjwKcPbS8/3KEz8IDyLGkB044FzrMJdsSGT +vLjkUA7bT696TSSdDLcBOxxQj41fdn/gNOhiWy4qS+r+dwSOL/cS6zSr65GTqjeAOQzAr29RUiT6j6J9tKLwPycKER71qUKPBR6sHcdh1/P73QzrPupswIlih/8T/HWy +gxkHD5Myi9QB+wCuYFL+wWc3s44uTroMHNL09BK3V9Y= +382wTcvh4neRg+mxPFc2rG2pU6jpYp9eTnamxOu1i1A= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +Sv2zQAAwtxKbD/XdM/+WUM7lG6QiYp1yBcKQ64zS9vXrBe1i9UUCioqbSWCX3EPc +KZTmaJLp+FU9X93j5Tpqtw== +DXnCT40IH6WTABJ8YSFXroivLD+L5XjDnseVaJ3GRDsvr+GwN+xEtXVl274WIJxN +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5Yjd0zuj3dh2k1soSq8z7BD366CML8egIr9+RWKS9tu7hy5ThiQW9xrT1yOgfZpmFnbdA1C/Mq2xStnh5Qt3XdA== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DMp35ZO6aoXY/WoLH+K64ifsvsF0vJsKao+rZhSUts0KS+GfybfE/6SJOvKSzRkCPmZ6bx3oIdSgzFoKVqjZhe05uY3gWbpjDWLae4DFBZgg== +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gtyrN3fmhNqvxN36kot5RF2zItGfyfEiFuBhMm2rOiJ7OkZP6a7cOrneDOCN2184l+ZwZJF5qeIzal2Dop22X5hW26mYIKv2A9GjfQi08Q3BHNaSn5fukm5t+d/lDz+OrBMKxFq2jS013FuZef967uC +4+wKB+Tel1iow8au3cOIJaTLjj3pysITCcJJ7ec43SjbGuLLwmdIpxnALkilclujXdCRkF1pWzr9l8hRm545yA== +Vtg8xVOtjpKd+p8vaXfWJRm3BenTCuwYX+Bxe4j96OnxMxH9X//3nMcOtd6Y6/TgOHH8oPBGfVeprOiaoeXRDl7vU4I8BRD+VocM/McwMuo= +o3kxTO4RwuXO7GZ9wpXG+fb8QBpjTrjWHeDnUy/AgYU= +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uXr5UNbgUmXTutS1OHze4KCsFAtrypiPgQFgZLgamax9XqIv2DluNeQrjzBIITfz2UDLd1PBFsxuyK0PgWT6E2U= +l2FJPs4YkAmmok1ulDRuSA== +o6QhOIN2Sc4SHELnst17uTGbfoG65qJYuOo7HVAUfwrrWa4B36uJ5REt040T2SNmGAV0LMPrERjzC/oRk1GEuX7AfuSpWeNyK561rxlWmqKq8esttm3BDwzTTA5Wp/ZKFGU72IJK2pjrHuSoqkzLVujx2qaqM/Km1yCxuLXdC2UZk358XQ+ddlTAL22cH156434R092KKggual1jvxpED9VmPJ1PNoUP6cx4WbWWYCyTyPc7+BhF/kMm12S2r8/z +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +q8NAywUgfWU6ZD/x59lWQG/RfUCU4y+olc/SbIg8B/0xCDpFWcjdNZBflnfCoehl +KZTmaJLp+FU9X93j5Tpqtw== +k4Oy3nZBEabTf/R3M0qud1RXTMs/SVAz7a00xWJGU6ZDDOl3CXrcw25TDnbSo0az +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNVRlZAMt29egPfWa3xSy0wC+hMCHaHnKnUtceII0RupXd8IOQSC4RUB0NbAR30foCsKXj+XCTmbMk/YqyH7uqUi6NJk/EPAbnzegcwhi9vBLw== +oSHYYVMaAzYJr1tP/DcgRffJWEj903F5HYDoV4XPcGHYkVn3IHc6Ap3uBQjWJsvBhk3bbvqYj3Zbc0k0mzj8qg== +xBNXAnUYpksOoaUztSBEcDCA3geSIlub48bUfXp9xmXPUKk/lJKMEh7cy/BygLUlHNyI47PyoopKMBq3MKayWePpwk8J5h++jGIkejavX4k= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+A+mcHfJAcdSEyMx/0+b22bsoYMl4HNKrPY0rcwU/I2hR+egxv39o+j6gCI9ao+XHx5IZEEj0CJYjzIuFWDW//j +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvukJqMTKpVsftOd4s//BOvjT/PcuWcqYjTPhGPjAJjS9eD/6U2Ktl8yDSB7KwR46Y3aEj92Nse6GQg7Z1sPKog +q4nc/jwATOMUyfSjLibfEW5DCAn6VBbRQRnaOjh7eA0= +v4Cu02hZRDsW+85CLRBC5yJTAnmRyE/J47U8yoV7+OG1o5+ROeIFbBg7WFiHmbuo +YKnHO5bC7P3fGpw5spE9ySFVNxaPfjLTGzTVXG8c3HHtkOS5QmjnMETu5VCO88YN +lNoJsK2hWEGOaXNpZFMEKYMgvifZKE+W+E/+8t9UT9fUy+Mm60dxPXPiyrPXtjO0 +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN62bqq+dVd1n67gwziL9Mi81vExgcI5sYg/aWnUHWD1weRj6bWPlcMwz8F38kqrVw9A== +VmVrGQo2zRokW/ZuO9bN61VTXWM6E/WTa3hYSohE63iz4D2nDm1NDjVxR/N+aqwD +VmVrGQo2zRokW/ZuO9bN6/X63Q8flAlw7IgGSkzO/eJ45k1Wq+ti01sEdZA9zkoT +VmVrGQo2zRokW/ZuO9bN63NDPumt9xAJMEP12SPXSnVwOUffjfek/z3/ToWdpbmGJIlq9ap8wrv8M+T271jK9mxGqM/8s3N+90r7p+FOhUg= +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN64/n5EL8V7CIqhp9FPzHEuQ= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +7oQeD4Rmi1Y5D/LIXmHxSfqLHPqYMZQsaxNs+fmYClg= +yXqkSqtRGSEMRZqQHZ1+mk5ETQkjHG8RX3fksiZJH6g= +KZTmaJLp+FU9X93j5Tpqtw== +ICNW6HFddBNmnBhH77VOJpT9s18JQsgSzvTZHyZ0w/4BTSBkCJlMBEOpTsvIP5F+ +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXtJmfln4rMhDKBEG62wPdFimDT8DrWkwoNehtLLH8mTfjv4GTcLa5F6IswN1F3EXm5eevHvy7XTk9dd5xSeqaq3iWTfgmsLs05fIcCd2nLmSfim33RMfQcMyMyH+bNZCQ= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+ApG5Mqf2WNrCxn3Gs2J4gOKJOfwShy25ZiJUHypApjzbExoGzP9ZnIzmZeb871vjf1OWvvHB0xalVmr1yKPjHm +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3IH5JkziqYhKBZseo0PQgzjjffFH7hOgJvxXJ916evBfNF+Czpo6v6J9zA5Straj6eLiZTyh0X+Lij4+07yiJyg3L+/nKZjO/37S8aq1NXQqY= +b4OJVZe8QyIpjuTpKXDL9A== +hSTJYpfOKqYLHJXvUGdsqJ27r1F8m8FTyXTwnXVWi/gwMflgvTRpPmpjhbuVtEc/D4oFAlC0tXqwyXGJrWKXr8XJhcHJ+BIOKSJ2bNuQQ2IiG+zkAmA072UbR6+1amZQ +k1ktu/l6HFULD7Vyr8Cv0HMlLvOl3L1DvBU4YpKX0wBIjUg6dQrNHRkXanVRfhKXrNAnH5kYjXIKcVHFQPJZnQ== +k1ktu/l6HFULD7Vyr8Cv0JUVYI748cB6mvkJiRhx4Gzmd0xowSn+pxKufrg3tRQEiN+qR26hQm6+FP8SWt1NvTBFRMGZKZLENueY0QAf2XEwTtcCoTzjnubqEJqekjtm +xWoGNWjKGPfI4gq8aHoTfIVkMgneahEXssm3KpTrqpr5HFLdQc+EjdkYRpshTbPj/FzqroxUZeB57FAHeJpOBw== +NUvaU3NpUZfa22aObUMgTYksNTb1aGXRl3st8KW92Rsv2ee7hf5/t42axhCc2LaZMCVUBuj9c7r1sNAlPrjZWEmcWdRR6B528+MZD+zeF8g= +PV+X4fvCTN/6758fJ9HMvyJNBHisoA22qKMtx3QNPL9j5Nbn1tfk//YG7JNXM2E3LE54k6F6pCjzabbI8uezxw== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uZqjPvbxWGrU5F/9Mfe3I3cQaISW5zLheEHDcMmSfSBTxrJ4RTwERVV5xP1tPbQDoa3QJdzK+tQWy8CxB4ANhIwZ0D/AypHVjTXbbIoOkq6L +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +5bq1N2nQOsMJOSjumB5HbtNgV5IhQTWyu3kx0nLJVhI= +KZTmaJLp+FU9X93j5Tpqtw== +F5PB/Ezvy87BYEWhACXaHg+G2+JUNqttQ2BGipoe/HpCQCzCmv72B8lUwrzg28+k +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNVE7/nTAY/4DTMPMCxe0BFtLYToSYLqyUhVBf/EV63jXhcNZIHDDe6HQPwUa0kyWFT32N1vLAiAOvAT7fU5rdXIepRDtCEJjI9PR4Y4xHvHRw== +UGqQTbo+FQ89dDBUZtFrg5DxdIZZTfO4XMCcertveHM= +B9q3HJxjdhik+xeIzU4Jb+seILuHUGw5SYrPoE3Rqnw= +WHkOzVx7seuLmxs5Hu+l/qqTd4Qb9qnLQgtLGl5Gi+JbwOQ4t8GjAIXgCmAhab1k +EiP/K054tbB05pkitbcV30bEwW+W/C7CxFeH+V8oCtQd48ujpIzk3Na8clHX1dQ6 +wlLHv6kT3Q/RmtMBN4nDAVvgi32EgeeClpdnP2N/vcs= +VmVrGQo2zRokW/ZuO9bN66M15Npik7O655FPzJgq2Wqqn8DYR5Ke//SMXY15WGpT +VmVrGQo2zRokW/ZuO9bN6xZT7d8s6PBBzk+QUE5OFqPg0uFK6TvMCZaaARpX7A7LUoi4zMHgUUUEwXqc3agjjw== +VmVrGQo2zRokW/ZuO9bN611xUZIvytu5Gk61D+9F8MsSkVQh32Q03nPjSVxte+Z4nBieQndu1Aj5lvKbavuiSA== +VmVrGQo2zRokW/ZuO9bN6zi5tiZ+l+/RutFjjKVqzPJkMdXh6CpSlXrI0U6mysdW +VmVrGQo2zRokW/ZuO9bN61TuBb9tXEBoRJN/gin7Z4MhGb/wu2L0XkxMGyLo8nS48xqakc5UMvRQYiOJKhGpTFF9kL5a7zBMKM7IJHY2FiU= +VmVrGQo2zRokW/ZuO9bN65TLhtjykfNX8WJm3Pk8YW9wqUJNdhnquGvjaQe9nAEAcWw78IXj0KBEhDvjixJVT7z6aeJsKaoK/nPDQpRZceE= +VmVrGQo2zRokW/ZuO9bN64htQvpIoTo9uIrNUaqC7ysnNeOSpoTevFJ4BT/hGFM8 +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5GoIUl64lV4c6SM6jvfq6ZHRjPDd8079G2HEigi/ZfKC4Q== +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5GplD/uP1150ghFS+BYu/BamG/T4xQD6Xbs8SqOlow0H2KXfbDHQXLNog8EBLWEUhHE= +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5Gq0d2ZE6cnfazkFCFePQ+aqXYHgHLHiD4y+zDPj0czCQUlieFMRFTuiURI05jIC8LO4owCk6/6Vbwv+eWNKllLGSBLtQ+B5DOSD/NKOY2/yoiAS2Hybi5KXRKsHdHO50DH6shFKOeIEeUNisDiCVipLY3mfH+CDhejxvwQZNsp//YYuvRJo14m/n4uj8RfcLGS4Ld+8xYRWQhdZjYOaJiiR +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+D2021K6m4bs2o1hVfLElNe60aysgdIwheeBdFbreKFe7rJFqa1bYA6PILIzkpa31MAdU9tyqa9Vg8ArQG4G0A+2B4T3UKCyJQwlEpXGbjiUijyAnYRD+fvROHSP22zdbc= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gtDMVu+iNCTaW92ygiohkArsnYj8DZEk2XNcYZHd7kRFH404wvz3DwFVfB6ZIJyX9k= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +yJPjNZwRskKR56nld/uGbduJY1jzekNuDeo3AqUOCi8= +KZTmaJLp+FU9X93j5Tpqtw== +3py9oHXgbkm3kj5vZO424kY9EbV+2w060lPtk1RV/So= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DMp35ZO6aoXY/WoLH+K64iO862fVKEvhHjtcapW+VHgbaTOI7XkhvepSbVjtoaHSI= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3ICsdE6DJkIqo6NqgVbROB8ltob0F22NhfxLMnSWsptnQV/2EqJqMrHV05DP1dh2QH ++m2Ig0EQEJ4Gc+2v8s5xrnZRBGAfDHuVg5r9VE/U7Dqcsp5mSPEG0DHHYcBZkfWo +/0ULpLqgTvInFD0r5hHANokPEeujoIOEEMQg2gP5hZ7UUQoqIjD1Xd/oZDwlfcsy +r4lXdc7E0Sbufy5J9lgVEw+k/OsdnZeoR4umjEfazo4so584LJL1JvLbSnSv0RnlMbdUX30yqQUO6nKGPhmNRqRcO6RFHU4qH5PVHX7n7Ng= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +IiaIcsewT3XJ3flbzG9czZETXGNwH3y/VWrivm8ME4s= +KZTmaJLp+FU9X93j5Tpqtw== +znSKQADK6yp9O52W6r7arDGBJVANQwVc4yDg/LI4eMo8u5U5a/7AWstzKi372NPeGEf4x/fSQCRmwq+F45NO2w== +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DHoOe8nxF4SPJvnopjp+aXp/KQoD44qBgpZViiCaDvjdfmvXT6xn+iCCBIQzsirNbuT+eoAcUlA09mHzRIOqGZ/EYpBc0376dZodYLjZbw9opLwbcYBtvpXXHK+yTTtuo10WBsbgpHX/YN+DeVbP49SbGnqhpe6tIFPytCCaMomQ== +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3IJhj7qdo0pX8MRXotsDWzMgf80RDCpPxKyjEFk9lQUERbV4chjxQhuGP6lP5uqlh7smfTguuKNL7+GaMfGKdg+w== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKMlEIqjWm8+kOtLiMZRGDiLR7B/+aZPftSeL16BO251zD/ekYvXghGe/B4OR2pKhCi4e0FcxpqxYknLcvp7fM5/uW2MIwxhXJjRGmh83ppP6qmYI6KMRl9hsl8LYKb/k= +BIa0ibHnqUboaK9kYHYO4i2dMxeM+vZKXAzDd8j8jd/yn/ENK7wJenIypTdgVnal +q289llODyfpvIou3XwdsGimFXHjDd6hIxEHPXbgygNntXvmhJR+Jqa1CbILKAv5drJNbn0IVP138z0q5XdUOiWINp96cMzaVBk+RM0VKb0uqMuIqlhIW3bAsgUMNZvUV +D9nFoWE/zqXaNxfAecYXR4Fh5DHDUtWIpaiFYfWvnDw= +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uT4RR1LPOL6E9tmqLd6Y6X4vLBk1OvZe/5L9ln+zX6CIit1RotOsuNgr+2hJUY/X/BSsL2A2K0C+Q9QJhBFSULwymfk1FVjKxlkVtAVbGek2 +kFG+HHGCYC9YcDUqd/b7oS1ZUpCvemM/AZwyk439i3N8ljE9lCYl7mEZugjCSGeT8rE+l49BssP170i7a5XEjctDsYoR5zwoa+zzuqQ8603rvR3tMX3pd2Yr/rfDLpwy +o6QhOIN2Sc4SHELnst17uV6uru3KtN7sc12yr5NsDmNgF6lzBfLQ5gSkgIMKaKncHwM7j+KEq5dXilr0gom31GjRPKOwT7x5i8iT/CJU8vsT0+LsGde9gh1zQXKdQHJ2rMyf/jXKkITuuyk1Yop55dl46UGYURcETbX3huzp9J0/LYbZxHuV7qKqJHeEM1zGUQKX6q3nIZFaQIT07DIgQf2V1ZFSqlKSpxJzKVTh2Jk= +o6QhOIN2Sc4SHELnst17uTGbfoG65qJYuOo7HVAUfwrrWa4B36uJ5REt040T2SNmGAV0LMPrERjzC/oRk1GEuX7AfuSpWeNyK561rxlWmqKq8esttm3BDwzTTA5Wp/ZKFGU72IJK2pjrHuSoqkzLVujx2qaqM/Km1yCxuLXdC2UZk358XQ+ddlTAL22cH156434R092KKggual1jvxpED9VmPJ1PNoUP6cx4WbWWYCyTyPc7+BhF/kMm12S2r8/z +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +NZydr5h7sqaRpfl7TbGhKe216Fai60U6XhMGhAUjgS8= +KZTmaJLp+FU9X93j5Tpqtw== +OviH5bvijIV+DBBPGXEvCeQxYzYrA2Fv4OsiuQeQbUkmL2HMsYDDAMjOhkCr2Tnp +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANnKkcJSE/H3VghkioZ1tnjyunYYq8XfVeT+J18IgTe/zFYLqRUYiaB1wowQIrBkYKw5d33go/f72T4D/yyYBCiG4Q1a2QpXXpdXNM2eyLBKd +yqOVJCkAGOhqcQYBOo7yqXSmQrhxny/twquABNpyYQrRxdmuEjhqv0I7ly8qcq9M0YOpjkkiv64zNUhRH18xB5+07iPi/pvu9+xDvtZyVBFGBqdzgHOGVcUQ1iTa9g/Z +VmVrGQo2zRokW/ZuO9bN6xR0lUkNHI1Unbm3W80skQ4Fc8+zISiEmEeRAyY2VWkD3Vn41T9b3eYLbyrqcfI2Kw== +VmVrGQo2zRokW/ZuO9bN61CLj991rBI9py3Mg2t0s6qFlO9r4CArF2KyduCrowCUtQmQHS+E3guomUvU9r9WWk5BCrM3zWpMIxUdEP53b1SKKCiLqXwN3v+Gqd3LFJWz +VmVrGQo2zRokW/ZuO9bN61qoeAEMHMJQFtGb5H5iFFSLl++WsYL8tJ/EWHrHSPF/UpymNQjoFMe4e5Air7Wb7A== +ChQf0+VWU2P1GW8Y1diCReOM1CzElHSu6IohDJwDQKQ= +lqgpKSCmWXEPVsTfAK5gKNLM+j7V3wcFdbeGqSPza2A= +TBZ8vu6Ktk1d60dO/QgtuFjcDR2A6F9M+r4Nr+2kgGFTuQ6TqiiMd0CoMviLaqVR/HASMw71dnfQOt20QHCyfLwl5vCwrQMAH2kG4aMJHOVycZAk7NgRpvkYE9YxwzukntI0cZ25BrrOM4mZg90pJhTYUvd8nGsixHMv4qLLZMM= +6TNZQyb72eciPdabT8jRGfmuaRvwk/Mze1bnDI+wkJ9pu7p4D8bccTL3dM+QXYUR/W+Fqi8avhGjWlgxFaXhVd9m0d+D8yk8VsnB/rwN/QcmQTSfWr8tEdjJPInELiWDl2gvFPziker2PRSkevW8S4IupBgdJzCWboThvA465n0= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DY554v+n5AiLrboeHZMaNQuH5thKl7X1yjAIM4IcU20xPRP6qFXv4bSQVe49cNneL7k17W0S5V/B9GSCvAbisSnhCOGMFNXd45AK1U26BUCDGK0K8Nu6yOQF/lK+9KkSj8N1U6UsDosAlbOSk8Z2zojHM06HkJuGC/EO2ZrAXACC5apDIyUEMCMOLfxRR7Rlc= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gtyrN3fmhNqvxN36kot5RF2oafj5DnCTDYuiKfAO0vfxHXDV1Se5E88TFWe4ZWoVcY4oI3KLyRWKLDNgW4dsbT3mdksYnoqnqqy6L0m/74mcb2fh3zC9jkL+4KDa6fIPJEJ9YHKPrMiItYf4z/dX0Vk +Cq6vhHOCmT4DWgfjwQdcYyiy1M1IDdVXJwLjV4udcbI= +Lp+10g3H3f14x0jksZoIZ/GLIPoRjf+Nw9N/DO0k+YU= +keqgOa/ELntiDC6RBRh+WL3vBVGsi3+jLKV3Y5rnZbE= +xh2vymxYeeZomkH262+n7plVUTkskt6kICx3mGCen7E= +b4OJVZe8QyIpjuTpKXDL9A== +wlLHv6kT3Q/RmtMBN4nDAafzR3IJaSQy1F8fEUDT0KURDliq5QrdEI8/0cInAzfT +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvMpc2DCYsr/Y+VR15aQe/co +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN64JUrjunpKQspTKm5wPUxf6/8fn9iycJNxQEzrRgKGCtNOqMeXzSymH1TgsVYcmN2UVZm7EhcksllhYNLkg6mD8= +VmVrGQo2zRokW/ZuO9bN62tDGDWvKCYUebsejX4RaJAFq9louk8D3mueq5NpdQByFjpctkUKLYKwcbguVOwJKXr2njOXfo1Js6EGYJSYwGw= +VmVrGQo2zRokW/ZuO9bN62fDdtc0mkszRhY1uV11YW4hXfV+ivpQSVthbEPY7zMTbcPEWtnBUa2cpauAzQSZB9CvXLuvdzmuAmv6Dgj0s6HotusH5aCxgCaT5MVjFPJlTwRQSHFT7rVUuNFtt0zYAg== +VmVrGQo2zRokW/ZuO9bN6w4io/b2JfrrmF5Ha/WszofXR/BET/NWvwsve+jrPERFZuO5aqMCRqJtW2w1QEntkRDN8kZmDAQOrNWvXorHXstYfUTJP0rYpggc/JT4Vigt/AU4+WYOVn1ZA0tKYQv7IA== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +1u+XjG/2+GSQRv6EzCaWRQ== +NYuttixIfaRvF0QtYZhNf9Sr9Ezmh/DjG0auW/4hLi4= +yisP9bnsS89PFbBRJbtYnF/l6kdsBz3Rim9oMpP6KMQ= +VmVrGQo2zRokW/ZuO9bN6ynLlFbGdWgwF9oAzMWAsfq3SP9mz9i1GoYlYGPxKaOg7yvwd6pL2IHH2Pn++jQzOA== +VmVrGQo2zRokW/ZuO9bN68MToCp8Q3LqgKNwHk9HXpVWQpUvTYbzaPq6og2ymqH55WRDGbQdfaEUR9DX7Jx86w== +sQfTf8f6jxyRSx9SNPNEtQ== +LUyc0dkTAKzugKnygsDP/38QmLeSAJyAB/Ppb3nQQELYQDgknMMUXYjkzF/EL+pL +LUyc0dkTAKzugKnygsDP/5Z4sHWYFfh1kDOdFaqFILQ8vsbTDmaHqPa3BecYk48U ++kIRCoW/7ri3diXr3wWp1w== +nuQO1if3lMKUHCYi1j17dU2TVxcjNRiFeijXT8u08XI= +VmVrGQo2zRokW/ZuO9bN60ZBYPiu5K44Q81Y3HdJjz75baowlOpwAvMkyYnEhssZkO2CI06Zd/H57Fc7UDQyZQ== +VmVrGQo2zRokW/ZuO9bN687Bhmo9x1q4Sv+T3D3k61cVoxASpAc7ExocFqj5Uf9WIWfFdWMJcFXS47nqSlBKtw== +sQfTf8f6jxyRSx9SNPNEtQ== +LUyc0dkTAKzugKnygsDP/zoq6bTmOgvD3qTc1/nBOA7xH4brF+K37o7urVklYQvN +LUyc0dkTAKzugKnygsDP/8vbQfLRKD1F6cRboe96aOaYtpywuVHYyNBuk5TJlq3i +L4gdNj7prH5DbeXua1XCbw== +0zuirfbdySeBs+yooVipit3YVEWuFLtLLm3mSba015N2OChGckw+zhnhhTa+i2Le +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +HeO84Ys53EcJxPZee59V7+QahpyULu2MesUbbnsHLV8= +KZTmaJLp+FU9X93j5Tpqtw== +9LbWYShHWRb8kqF6HqdNj6ezf1RzgSeG9JidnRXnGdX/IDyjSP6FOKgxBDMRBccZ +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5cDGSaMte+M461CXbcrSNIkerQ4ssZDfTqopcwoVN51k4ZoSD57ED4izO3AHkb/Y1h9emANNKxzlKU2rzEatXtnMMAXVGbjTY9r1q7LoAFlE= +td0Hee4NJPblA28GiIcOmVz/iEnSh7ccFLM88eucCwhDxWpOIeDaytxmPiP2pwkpNyGbS3x73U5/Z+SQSDEikA== +0ow4r3+bBC/sw9JQ+2+N6bQ79V5uMTUjRNGc4T4cAh2FOu2MjnIIMRWqvnMnFGnbv1x4EnQ1TdZXDdS+rZQFbiHH5FW4N8dc1NDPckXp5oqoA3pk6qKPzyG9VkIif6AEf60pT+ibksesb2c/NsuCPg== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+AOtWH9LO9ZBEToAbmupyOjq4r2xS6X3ooTK59khXwyxMphKsyzdV2T4Hnszg9ewV4f/dsIou9h3HQbCNTQbOqUUlvQ+ORbUWrUhrs858PljmTQ4m3bPKPNC/8xZrsvK1RhMA9bQvcU4SvOcBc+VC6r/Jy2XtcAdGkQ1dj/iVDkK7B8GqblBnZOBMc3y/hNrS+Lda5EYfUX7c8XRyzH2cyASXyprblojVnmJrnkPYRNRsYwGz+hcJ3OZTPm9qD1Xsc= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3IfkuRDQnQwoXQsyTGB5dwnnwLgkEfh4pR18v6bDOnh3B353j8TPunwxYTg1rZro30xivHOhHgMr2UMnoD9xg30YM1g57qkcLLdVNAFh58ZjMLTTcNaotcuDQ4ROv1X0rJ +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +E7HVj5u79lsEJBByAcI6+Y+iKetyWZpxWwXpNnY61L9K9E5sTBYilct1gvcFQJXX +NcZ6sQ1mLPT8+JyzvATZXNzUyynsS0mSwmHNHb0WEkKhIEOCarOBj/VNewvvS2r6y/Nzg2lxm+39+tlylXcMGzNRdESr6Nwk+sO8AiKTri5lczg0Joh6jwYSOdWZ/xzs +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM6ydIkN3ng0PuKyALE9oNtyobOGvrGiji6Fktt/Q/jdyF +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uafQ2FFgng7Owlb2ojgdsbmj25Egdnnu7s1LrnHeckFsmr/dhQ070oBFuJsDLG1N2+O22yM3TQqnLHaCcXMPL5aaMr3+PJKPzxB29pbOHwL9 +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +2ard15XnfQb3lp+vey3nABFSCrBCIZnKXwgggL5wjCo= +KZTmaJLp+FU9X93j5Tpqtw== +U1uWmisr/Bzw00RzQojfMqW2zprEuuyc+F8DxOqhiC5mNNbPyRjvX4X0n94q3zbo +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5Y5IiAYey2dZpBSLvBmCmiTdM6LndDn8aYxZpLkGl58QmsrFNt/Xhzti1amoabI3SDkQv5V3yFTOwPCfZIQxPCfECh2PhXI082wqRdcnmlnw= +td0Hee4NJPblA28GiIcOmTuwGfcKcaMQUQAHvx5+SrqJRTOnfpxLSMOragNj5LzS +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+BfByZDBmKK7K3HCn0xbzDN/p1PsgAQJWE3qzjMeMU1gCmeV57i/W0t1+UyJzE+KaZmHfcCRIqlb81oSG/kpEejc7iBJnN4FWS3kw5GtvDOJA6er0KzkFPZyw7Zqq8Snsc= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3I1HQBLdpHf/UEi3kk35v3/r9YpR/3yiYo+wEA0D66txcqEjQV/544BZpZ8QEwYr9Cl5Xu0TtbFHmzTQHYZArivllZFNvaxVOwvVEpgBPTF6A= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +b4OJVZe8QyIpjuTpKXDL9A== +gZ7BoIAABc+I8g/rYzq+fxN650FDIkzSAlL6+Jr9FEI/Q3wY42Db+gtvNHy5SguA +VmVrGQo2zRokW/ZuO9bN673lu7cHUGvKUqK/AdiZP5r/K7bXVjEqb5SYh7vvuzRX +VmVrGQo2zRokW/ZuO9bN63QrZyqUb5BKoT4N9cyeKvyfSYVxVAmHuRZgE98udr/TTkHnNOUUUGZ2+0jty8b2cSsROo8qzEMZ/XSVHRgbkhA= +VmVrGQo2zRokW/ZuO9bN63CK8jinFx30LvrC5BheN9+Vzu9dKNjPscy3v1elx2imYJ9CITDn0Nb2t/sy83JQAQ== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjao/TArw7d9PyMMKcD/gCyakAqAOInO/kyLo0xwBhDw4HFB6R4lqJg7BgJThUr7tdk= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM676D02CK8sgxlqu813CFwIReF8PG0JQQtpVrQcpOA1a0 +CW/G9plDL3SfzmBxnqr6q87wTQCNFJP1gF2kN0sG2a7bqaDYcLhdeA9QmQTdIHLVcJ0jJwg47dIkFyZzmF2U02GNPNgixcpO69jEa3mc2Mg= +1V2v8QerKOmubvSxgB4eTM++l0T/KuLt76uWSU9JjRixsDS2R16+WnBtvujyRShs +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqGyyDPCcCSOlRV5etylc9M5 +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VWSi8LKj0objBUIL73B+ABRovi/1tQfrdsCJTFxnHsgX5Nm+Gz285S/IPocayx2mJMK8RAWx+2TsPclXjuZG1Cj +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +oLkjJHDOdcPq1MvWDVGWmS9Ci5tZeDQS30Leq2hud0RyBR33jbClETck9j3BDjgb +KZTmaJLp+FU9X93j5Tpqtw== +T64f/w1LnBT4KwoMv72GxqryOhcsfNMoJd8yAgS0pbE= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +KgZhnCYglllfsrazbVxNli3dO57PXUycfNR797BU9y4= +e1qLykdSSG57G+I4DSZ3lPbnd9MDQtXYM1cRPgH2NZY= +N5xldEoitd9eWUThCvdOTOnQKBuAThYmYC8u+4DcAu5Tc/F6a1hQxBvmeWgTOpVI/wA+jhFG50+kJF42ji29zmrDzE1c92kPIwz6/8Mjv0E= +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5dq1EJSpcqAYEPXy6WaSHxs9fAXX/i7MYu+4BBwECfWZNM+SgGf9wWDjprmWthwO3+OXnvBwSuKB6jb5d3MBnTQ== +b4OJVZe8QyIpjuTpKXDL9A== +p9oVsBgcyiBMjDb8CcPOqFV5wpmixLB1nyXJiJ2HPNs= +wlLHv6kT3Q/RmtMBN4nDAXDoAk0pYvGIaqdbi2BBOVs= +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJnCX4DFeOIdkDtEirmUW5Ek +VmVrGQo2zRokW/ZuO9bN6/lQLfGZFAL2XD1b3BIrGimFqqXjMICw7zHJXBpX9GU+N7Igcy5nBNFAN5iTqyRUqpp7FAYH52O9YRVJDSmTxBhJ1FbjehHcE7Xxv8M8gnD8 +VmVrGQo2zRokW/ZuO9bN6654aX7czo4xbfX5k3Xe/kWgm50rbPsjEZDMcSxuTnM1 +VmVrGQo2zRokW/ZuO9bN6wFRMguAm+TeJk1+fnt1XFUKXJnn5v8/CZK5fl5mMLFYBNRgOK3irni8g1fo4OxwSz60lYH+vPSgypNUpdk2hSo= +VmVrGQo2zRokW/ZuO9bN6/A97cjuU4K9eGnKJMbx3jem/I4X01j8TXo9M2kXhK5vPAbexcO0WsbiraLHoYAxuH/s00M7q68EnsJkSQu+EdY= +VmVrGQo2zRokW/ZuO9bN612MpsaDdEzPe6hUSc/u/3d/wHgKOWbxgXCW+3+76tnnqUpZpa7KtkEvjfjCd4auFj1jVZzIo4Tq1x2v3NP6TP5mpfH2V3IprP3UaIKvPpuQ +VmVrGQo2zRokW/ZuO9bN66zBSwHJm6ElN+7ZdiAVZBTvBew8YxafVWuo2yjvJVU7XQLDpZScPIbuHyakPvsAt6MbXk5duFoiXazpWjBOs+dS3qJ+rWeGKS3yWHGtPNmE +VmVrGQo2zRokW/ZuO9bN65a1lvAsgN8t7bU4ymW4veC29rX5B8jKlrdWpQ78Mp7t +VmVrGQo2zRokW/ZuO9bN65IMmCegrj1iToe3m+KEt2lYR2GShaC1HZt0ENbtcWDT +VmVrGQo2zRokW/ZuO9bN6/X63Q8flAlw7IgGSkzO/eJ45k1Wq+ti01sEdZA9zkoT +VmVrGQo2zRokW/ZuO9bN63NDPumt9xAJMEP12SPXSnVwOUffjfek/z3/ToWdpbmGkgQ/+uK86U8g1oipbDnW9PymzEkvRKIRFdQUbVx5NK0= +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj0H4xphI80JaVwdOBqT4vpFoYlr2YEOLcHWVeU8XvA6DcJG/8/11CmMgn+dU2p+MiteDOVRuiE+vkRJyCKGHE41wHxivV6nzAK9IrCg/kgKo1r3TbCAho4S/unYWBGxwmrqp+8ZOvtXQB5XA8bjBzXdDymC/ZUqQHO8ss4ZVeIjTA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw58tqRCidnUtuhiV/SQek1uLeqFhZN4Qp7W1PBlJxy9Uij4W4Ylrlk/HX5S/Q3N9gbstqjFhfBAcmwuzubPT0II= +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj0Go3ZdM40X+nb01il25Cd+Ij2ks0QNQ8ckELJbWPuSoOaKJkHC6l3WHCulMTp8j+LeAWCb5DBJpRVpiRip8pDe/KIfBFKsAy1uPllcESEV07XCLR6vETN2Y//6YxPUCufIc5q0ZxCiI0dqCJvZuBn0 +A50UO/kAI17YP7MCbTvBkFQ/jf5eMs11PzZj1m3IBsQ= +s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +2JIP/YvC/EYiPSt6qDLzOQhm3De1OK9Fj2Y8TJvQnSE= +KZTmaJLp+FU9X93j5Tpqtw== +n4iAFdIZCZOiKRB3MX5Ggpkc+IH/i74d6PZkAfctWjw= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKMlEIqjWm8+kOtLiMZRGDUax5h2UcZ8Ya1rrZl2aGrIJbzHdHHnX+T/+biFUpKbdHonbFld4dXvY+6LOVTF9Nv3yUp5wratDvKbY8p3n8eg== +td0Hee4NJPblA28GiIcOmbsckmQsAXhpobaiY9HWzrc= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+A5lwM2h5lzBxHhpyk75OffzP5T6ccpWZGCDSLAwHQAOqdLucRP8vYEN1wPagfqO9A= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3I6VuQRbRuF5/N81TAoJjY/0z7udH8+YFAivsDd7eWtdtCqEwXmCnT6vpxY7FYdo/A/o1CeWSjqxfBy7nqNuAO4ypjYC7dTdgCEv48YYiV4mY/NyGUA2VOjNuy6IyI12wI +kzM691k2SeYKeNMbUF0yqmSymIPsSgGyT9n/RVKtUNJJbYbqp5uYucQrLfQAkK40GofBHd/0/2BBbBXjbCtyxjMGVWZtqZoQvgak39kHPuTE8e+zClutQI0HPSrrwnOz +hQvBDyxoZ883xMeg+G+dguIRaVdeXNIpB22dELbA1zShjsM2m9SHvl2Jf0Gm2g69hp/QoBxrjqsFHdLlWcqnMJQReKNHOuNET9qNrMQDIuNPm0dXTZGq4nNCZqdwv07V +ncUn2TP8ZQ4fZMBYk+CUrbbEKRHejS+40NUHzR6Awm8= +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uXr5UNbgUmXTutS1OHze4KAGr7se+yI7S4wrgZcXm/EOOFcoL0lqR7Jo9kpGphoKPg== +kFG+HHGCYC9YcDUqd/b7oS1ZUpCvemM/AZwyk439i3MszfOTyV8WWbttn3e2LEnKaUTIOxww65MvXZ3IghDToyT4lC6GeuPdeiITUZesqYNzMbxL9aEgohkmBpjvYNb9 +o6QhOIN2Sc4SHELnst17uV6uru3KtN7sc12yr5NsDmNgF6lzBfLQ5gSkgIMKaKncHwM7j+KEq5dXilr0gom31GjRPKOwT7x5i8iT/CJU8vsT0+LsGde9gh1zQXKdQHJ2rMyf/jXKkITuuyk1Yop55dl46UGYURcETbX3huzp9J0/LYbZxHuV7qKqJHeEM1zGUQKX6q3nIZFaQIT07DIgQf2V1ZFSqlKSpxJzKVTh2Jk= +o6QhOIN2Sc4SHELnst17uTGbfoG65qJYuOo7HVAUfwrrWa4B36uJ5REt040T2SNmGAV0LMPrERjzC/oRk1GEuX7AfuSpWeNyK561rxlWmqKq8esttm3BDwzTTA5Wp/ZKFGU72IJK2pjrHuSoqkzLVujx2qaqM/Km1yCxuLXdC2UZk358XQ+ddlTAL22cH156434R092KKggual1jvxpED9VmPJ1PNoUP6cx4WbWWYCyTyPc7+BhF/kMm12S2r8/z +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +Rz4VePnQFedRnn2vEuyuMMWmrmrCy2te1mN0yZZPeFf/tPi4zDP/Sz8hLtaLzNpv +KZTmaJLp+FU9X93j5Tpqtw== +6mbfqKQBA+uDZRQFe2c1GjvCRYwRRIW84uHjS5VFY8riJ/whk63HCANV+SfATQqk +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5bVBRiuiys3C14OcWDZeDhsb7M6ThnoThQZMir8c/N4fN4C/QfrpDjbsTSrY49RV8Ds2C+piCS+iPG8dLc0wgfQ== +td0Hee4NJPblA28GiIcOmTn4RzFMEvXiU0UZLJrdfkCrgL97jIVINknjWk0x+1O4 +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+BfByZDBmKK7K3HCn0xbzDNUpzYPikvK6gsqSpFFB/HgzVgjoip/a8H78JMmT/0PXDSGDzhZIgh9eXeP4s6EaxotE1e+SBHya9ymoCIszuucrnHyOJrpOoPfKv3h+TSd1Q= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3I1HQBLdpHf/UEi3kk35v3/tvXJWw4f6CagBPpoTfZCj+Lmc0U1XGVkEFGtq5U4bGibDNW8X8NONEYSmiHAx6wv6vqnJSoZCL4gYLNaDY3MTBdVk1PvJ7DJAZqmf8BDNXXBCb3ywi6cRUXaomQCEJa0P/0RzrpdU1u7zAsUxjCgdk= +4+wKB+Tel1iow8au3cOIJW20IY6a/C0B44B/KjPC0z0PBCwjA2BpF8P7X8qas5oE4QSzW2DfcB4JA+JKqVwe2bK9oPMOFT4IiqCjFQ5nOrBV5TVuUnwRK5fUL/jPXykk +NCNZbNM/44ZjjoozPyWc0+tM/Ko39cLvmZ6rDu3bqd56+rv+caEBclISWvT9u3a+TK2k3OkQpEHdSwPR66zFBpcPlkoPpLCn1AeE9BFlqr488UcWR3G6Z6tlekXYyIda +4+wKB+Tel1iow8au3cOIJQ0cJN9p5Wagnh0MqbqOZlO9xYInEhd3gMa/cM0J9Goc +Z//5BJjWi0XgA176xj120AVcrX6xxmymlS4iQjuS12A= +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uXr5UNbgUmXTutS1OHze4KARyxr9E96roa5Gpi912InxCeT7C+HJljZhbQ1Txb1yYBlO/ZfFkaSTm2ohx5UydoA= +kFG+HHGCYC9YcDUqd/b7oS1ZUpCvemM/AZwyk439i3OEAGHnGg7APcs+Q7Jc+pmL1zHxbcw19Bs7JnJUIIsYm8/Iz6SRn8WolmmMLo9qtlVcb1r2LlcggL4UnFnE0Ma8 +o6QhOIN2Sc4SHELnst17uV6uru3KtN7sc12yr5NsDmNgF6lzBfLQ5gSkgIMKaKncHwM7j+KEq5dXilr0gom31GjRPKOwT7x5i8iT/CJU8vsT0+LsGde9gh1zQXKdQHJ2rMyf/jXKkITuuyk1Yop55dl46UGYURcETbX3huzp9J0/LYbZxHuV7qKqJHeEM1zGUQKX6q3nIZFaQIT07DIgQf2V1ZFSqlKSpxJzKVTh2Jk= +o6QhOIN2Sc4SHELnst17uTGbfoG65qJYuOo7HVAUfwrrWa4B36uJ5REt040T2SNmGAV0LMPrERjzC/oRk1GEuX7AfuSpWeNyK561rxlWmqKq8esttm3BDwzTTA5Wp/ZKFGU72IJK2pjrHuSoqkzLVujx2qaqM/Km1yCxuLXdC2UZk358XQ+ddlTAL22cH156434R092KKggual1jvxpED9VmPJ1PNoUP6cx4WbWWYCyTyPc7+BhF/kMm12S2r8/z +1u+XjG/2+GSQRv6EzCaWRQ== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +y62YGJfxIw6ef8JbuviiBWSxpyb44wiHodqhKrmL6B0= +KZTmaJLp+FU9X93j5Tpqtw== +RVwcA1dEXc+xOwN+lDdOfay7l0LhXFDOOUxgQf/xIAk= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +CGMG5LRrLyjmr66XNRnXQNcf/FN2KrQbe28ibKHRFdY= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+ApG5Mqf2WNrCxn3Gs2J4gOVvw02w7il4t0HnisbvGHY4YduN5akAghrek50kSlw/0= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3IhDx8iWxquXZunJU5mVL7/VqsZSKV/+K3GrhX3KVQZghpRq0S+Y5TwF7MsnjJ/WxfCZso5gfYjIOEP1TwvA0cvg== +cOxD5sJ0lN579xb/M4GPhw3kVmt2grK1diTnTZzA0WhBFWh0NiJqiHTHuZjgDQtw ++m2Ig0EQEJ4Gc+2v8s5xrnZRBGAfDHuVg5r9VE/U7Dqcsp5mSPEG0DHHYcBZkfWo +15GT7DI/XLgGdHKj30fzw8q7e+nW8+9C3TkmYHTBvMc= +/0ULpLqgTvInFD0r5hHANiJDT3VB4wGOmEljuAQxauIwEB9fxQTLUxSj5siVt0NQ +r4lXdc7E0Sbufy5J9lgVEw+k/OsdnZeoR4umjEfazo7kva6TIJ+8qApX4pM8br8il/BdL8JkriTmAZSYpuq/9tAqSe+ugBhf+4W0shpum5E= +l2FJPs4YkAmmok1ulDRuSA== +o6QhOIN2Sc4SHELnst17uV6uru3KtN7sc12yr5NsDmNgF6lzBfLQ5gSkgIMKaKncHwM7j+KEq5dXilr0gom31GjRPKOwT7x5i8iT/CJU8vsT0+LsGde9gh1zQXKdQHJ2rMyf/jXKkITuuyk1Yop55dl46UGYURcETbX3huzp9J0/LYbZxHuV7qKqJHeEM1zGUQKX6q3nIZFaQIT07DIgQf2V1ZFSqlKSpxJzKVTh2Jk= +o6QhOIN2Sc4SHELnst17uTGbfoG65qJYuOo7HVAUfwrrWa4B36uJ5REt040T2SNmGAV0LMPrERjzC/oRk1GEuX7AfuSpWeNyK561rxlWmqKq8esttm3BDwzTTA5Wp/ZKFGU72IJK2pjrHuSoqkzLVujx2qaqM/Km1yCxuLXdC2UZk358XQ+ddlTAL22cH156434R092KKggual1jvxpED9VmPJ1PNoUP6cx4WbWWYCyTyPc7+BhF/kMm12S2r8/z +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +gEP05OadtLdkLYzVLnPWJtuEd0CjPV4sWpcuA8Bc8lY= +KZTmaJLp+FU9X93j5Tpqtw== +0gvMBZ4kovgEhxHWNAchbKTO/JlgzQRZj63SucHKPWw= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +gpFa656cOAA2jnAKeODU3AAgd95/CC8ayLarQGb5lNyubLQWTspFThZdlTMj0rfCwqOkvu/JHRe2+4sfRPXO9BEe+aKv8eIE6Ui+ph/DQmiO2BKts4OZwzwl/kbrmheD +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+BfByZDBmKK7K3HCn0xbzDNr3SGPGW1QVBFNvitxp4NtueJYE+UZBZmiy0WI8jJuAI= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3IOzliM2uqy6AvVrnIbYzBb6iXCJHdiyjd+X+/w+QOeI/Mjam5uOMoe8Re6LfHO/Jzmnc4a3ZSaRhMJ4QG0k58rpV7WD0G4FI/AGg4MxTdtFQ= ++m2Ig0EQEJ4Gc+2v8s5xrnZRBGAfDHuVg5r9VE/U7Dqcsp5mSPEG0DHHYcBZkfWo +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +FBy3vCcAP87b/+M8ydgUJfBFLx67cYyGkiMKbGJXSvk= +YRZX6HCWMiKTW01+ivlbhAIWu/PNVBQgfWo9+hdhs9kxvZC3ZudmNefMrUxrLPI5 +o6QhOIN2Sc4SHELnst17ueoeSgiOQdahK7r1p0YoOKG+GFm7FBIFuvdjXiD4Jty/S0l6Ls4msakVN/hcKfNT2g== +1V2v8QerKOmubvSxgB4eTN1T1U8qetoMKVWXAhsfWddPSgYrLGFPjONflBvn6GGY +o6QhOIN2Sc4SHELnst17uUa6ao9WPlAvu6qJ9Oaltu+N9yoda7jqAbjw8dgbdvmN +3xAddec2urtvyH+TFd6l1MpXQTsst1/yT8DXfXPGrEk= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +lwkeHL0mQDDDLPNH1hfdfpmM6lMgNPGwcK2tYqIdJrw= +KZTmaJLp+FU9X93j5Tpqtw== +mW+EgwYdyL65jNEPCrA0ZwWKQbTBzhVydoIGmj/tuShzlyIlRPiIHzo8mFnkmp9qHnhBREmgooQgxhEx6+g8dg== +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +KgZhnCYglllfsrazbVxNlhZvo/OKoJzdIEoxUuSu8lsXUJ953DROtt9/W8ov0/2Uy3wnno06iDJ/U99VKO/wJ6hnihhWj9KvFR0gCHiVXANKPHeKBkWwHMbB3tRL18NSfH63uhuZkCBsbHYFiQY54A== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+Ckp19baRdobJWMhNbvSH4AO5DkozbXxMbeGpE/feH7BvuGSRBMHz/7PQ4wOt3URRejc+wF57CZzZ9EfIQjh/OIo5gheS7i0OW3Oeu12l3Qa7cZZdmALg/LcGympci9F2F82RQaoVLFCShpVGHU/GlWl58NwsR2iB97Qxs5+ig2F2e8DUiV/axaUWm1u3YrJ2GX9rLSRWcXFP/1zHThELnt +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gu/u/ZWm9DYyJF1WaRWt8m4szLYRnpzJhkglnvq95ps7XeOSDK1kvlY62CKviuMif7n2Mmbj2lybmTmobHcleWUc2EGPCh9WGRsXg5Dr6Lf+VVp6chVEJryPq4SV5TIO2CP9nBy8Pb7VSc0MyqKHEdYrq5l9lzSf+35SA/FEvWXE0qs8P2XxmOMGYB79WsuAri8FJ0kGBMMBqwpDy4X2UjfKHhcRMntbynLU+sO/97imYQkaPgxYpo2DifZURRPKOc= +td0Hee4NJPblA28GiIcOmclQqHHAArGnEN+3TpG3G/6oIQzAMSnmClJuq4p8RfBh +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXoz0tnByKjXrpn0PixC53x4Xpx0Ix1Z2lwFszcOsVRwdpekP4+NVMuYq0zb5Za/NtCyCB7VEiNKTN/tLrKLhTkSIIGLQlDqmIqZN6+PysaCyYqhylEOsE0UmJ944jPtek= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +FbS8mn3AvBwAFaIg5VB7LOzT9U0kJ5mxsQx8KNUG+5LCI0buRob1rf5RKOq9EDAvD204ODuaxW5pUhthVQx83A== +NcZ6sQ1mLPT8+JyzvATZXLyE31SvRRysQ1/hjQengeSM+y8n10m3af8e9oG8ETAQCO0q9SGk2Ohqx6GNl/3rh86tSpFetzdEltlMTixrnEM= +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +50qVUF0NwqssMqlRNPRPPpqMj6LbxX8baefoYpKbhOVpTChUCOK9IHPvQbc/mS1LHqoYfTg+KvGPXxnX7JArf7qx/5PC9hhM7cUAL0aFIzM= +UjGO21cC7sdtapmgeLbjSQFA3swQz8WfVfZ1OKckSOkQAvRjEFagtcWFL9TBY8IY +o6QhOIN2Sc4SHELnst17uQJAWihAAHNbOeUoWaFGGFB9Wh/G28ZIDmYw8Fa+iVby +L0eUthVnpkGsmKFAX6d+uGkPqUsUWC4LzxhOoLQ9PX8= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM6zvFOB89rC01/aufevhgpB+OHXClzxML5RkaNLVMRSyndrWXRAxADnCSvgpLctf23g== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uX3TRUWCciAkYHgiV/48FRFbL+GGan2NKhot8hPXtlt++f0g2KGc+U61KFJMmqRQNXsCKI4ttSMCoSWvCUqzyPYn6UV3HuiPo0PnAWM1c9nbuJ+zH48uovRGxvn/ipU5qA== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +lwkeHL0mQDDDLPNH1hfdfiUvzPpE2+Al7c4iRuJXSXs= +KZTmaJLp+FU9X93j5Tpqtw== +mW+EgwYdyL65jNEPCrA0Z/HbZf7YqLhq4ozQYKWbG37D0lH/Pu/ByyjqrUuy+3sl +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +td0Hee4NJPblA28GiIcOmclQqHHAArGnEN+3TpG3G/6oIQzAMSnmClJuq4p8RfBh +KgZhnCYglllfsrazbVxNlhZvo/OKoJzdIEoxUuSu8lsXUJ953DROtt9/W8ov0/2Uy3wnno06iDJ/U99VKO/wJy4MCRJUSGE+Cbr4gBuLOYtyJL4v+ANAq01XFRdIzIW9IP8V9bUqZpDUCww8rpTE/w== +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/hwfY+PcmHUBHGDqmg8nupFflJCXYm3kUTl++1ggJJQQ8JHS/TmguEAQrFARTn8O8sQnMFQCmypot0yJldoctZ0/s/5aM3/Wz4EZuhhxz14irWVVXH3qxdf71j0CpNXXKzWFbDx3eK1nlPrJrd/L48kUmRVF2BQNFomvLaBWHRY0NK5dSH6zapA8DRfUnmz42Z8BszLTKE+xNW1YpI9OPxQ= +3zDbxVuaFCI1nhEYDQoyMwlgMc9uqhh0gl/QmFcvmxA= +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRjex9aSwVHDL31oGD3Nb9mdsHgcdoHbrke3lOU1OjnP4fBvI2tq6+API/aYD8YVtWEVgTS73G5g/W6+ZAlpx4X/4RkIpBReNnGXcSmW3syV2MMZ/V+Bemzfq3e6tmc73qklfNmbkNxtdp8FqHpfKzoxlsus2UXk8esOZoo5ddrO+EBbIrFdyDSVuvBUkWY2jUtf3uny1f+8G5lWLo6qsX/Z0/J2Nuk4GeukbqDMxIfR/23KurY+yaue8uvWdm8RtYU9JXaWAsc/UYdHavNL5Koo= +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXoz0tnByKjXrpn0PixC53xWdKQvml7T8eOgThNFvdQGnBcayAu8Me7h5s2hlYcFGEHeQLuiMrummvBHmV5RJyJSK6DWzuBeKrTegS10yGfqxHRJjPZNhRIfic198uyDyc= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +FbS8mn3AvBwAFaIg5VB7LDGfk3PQI2b+STPhWtnGc8D3iulNRzeDuTfaMY/U+yzu52qH2fy10KLHYMa6coqlSw== +NcZ6sQ1mLPT8+JyzvATZXLyE31SvRRysQ1/hjQengeRCKPXtbhTAJUlGW621z0XKkgSRfauHtZ9aomyb7WCUwI4oK5AcjCtFxg8T0Pcu8K4= +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +50qVUF0NwqssMqlRNPRPPpqMj6LbxX8baefoYpKbhOVpTChUCOK9IHPvQbc/mS1Laq1B3OTDkQPLCmgfNigbW37+SZsoTGtAvaDWgH7sZqY= +UjGO21cC7sdtapmgeLbjSQFA3swQz8WfVfZ1OKckSOkQAvRjEFagtcWFL9TBY8IY +o6QhOIN2Sc4SHELnst17uQJAWihAAHNbOeUoWaFGGFB9Wh/G28ZIDmYw8Fa+iVby +L0eUthVnpkGsmKFAX6d+uGkPqUsUWC4LzxhOoLQ9PX8= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM6xcX3Pk89cGteJAfYh6IoNGakYTDDlTPcUzPja4BiQ6YkcwLdAKaF9O4p34Vm0ChxA== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uW0V0SNu6gMkNVnvQaya81p4dbSBuufxcX8hYuE+m+2pjG3aE7tNeDeTL4XXFoTLLtqYdCA64SICmGn+zTcsfvVou/lm7Y33q16raIq50Q7m +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +lwkeHL0mQDDDLPNH1hfdfsShqwPc+CUlLp3NmcMPEhU= +KZTmaJLp+FU9X93j5Tpqtw== +ZE/mIUUH6qV6H+P0DGv+nt4Y2tE1Cu+fFmYIchcr6x3dQ3imPAjJgyFCKQfqibPk +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +td0Hee4NJPblA28GiIcOmclQqHHAArGnEN+3TpG3G/6oIQzAMSnmClJuq4p8RfBh +KgZhnCYglllfsrazbVxNlhZvo/OKoJzdIEoxUuSu8lsXUJ953DROtt9/W8ov0/2Uy3wnno06iDJ/U99VKO/wJ/rbeDYVTRn12IFOo2gbCeFXXhFBQ3YjA/zkr6p6N9LcaIAoH5Q5/E87/U/1SgduZw== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+BfByZDBmKK7K3HCn0xbzDNFqqXFzNmD6bGgOB9/PE1TmzCg8PBCY9+RK/3mbcJkupjXVE++SIInpFExFpl4aBvseXMcSLuHWRNVZDzAjSxmOany7aipQEQcTY/pdEAzxgzKzDWT+o19NIR1eoL8wCJWix95p//OPjJIS1DizRkRizVe13hCCrb2F0lFkdJQgBz1ibxdpNsfT/YqMrGtew8 +3zDbxVuaFCI1nhEYDQoyMwlgMc9uqhh0gl/QmFcvmxA= +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRq0SE3ke1DVICYcXA2LLsKIbdLH7j92zGlDWBkSn1r8jjU7z6f7zl/bCsa/nN9UdQPt7jUFU3466JQex2z6lVWzL6cmBtWDz5pRXKAmOCGMTE8E8JWsS9knqIHDELxtXe0FfYNWpg2aG3ShN9Tmxszf9Ix5Z2yaPIIySqU2pkq2S +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXoz0tnByKjXrpn0PixC53xWdKQvml7T8eOgThNFvdQGu9KW0EKUp9/AOqFnMyl7N0p6kgywp4Iycx09eTDi9coo+2QD6y+GkDvFtQctJv1GWaJ5m+hnPs4ty1VkZaTu20= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +FbS8mn3AvBwAFaIg5VB7LF5D+W5DVBr7vJDfpCs+o7UfcFlxIT9U/KT0IiGdMlcpT41Jf3vQM+3R+hEWLGg2HQ== +NcZ6sQ1mLPT8+JyzvATZXLyE31SvRRysQ1/hjQengeQkNcIkG8myBjLQODy4wUw6b6IgxvpG3kDss4bAp4Mkg7+j3iMBcRlmzV08ot9P0ho= +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +50qVUF0NwqssMqlRNPRPPpqMj6LbxX8baefoYpKbhOVCc9uuNYEopENSH1IAYrkcJIAGJfEGaukafekVV6Q4TZrMJvKnrl4Ety0JsUyWxLo= +UjGO21cC7sdtapmgeLbjSQFA3swQz8WfVfZ1OKckSOkQAvRjEFagtcWFL9TBY8IY +o6QhOIN2Sc4SHELnst17uQJAWihAAHNbOeUoWaFGGFB9Wh/G28ZIDmYw8Fa+iVby +L0eUthVnpkGsmKFAX6d+uGkPqUsUWC4LzxhOoLQ9PX8= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM6xRep9h4JeTcmDXtoHfuJJhLeMp/96lFFHPE3DsqJf5dPQVeKcYvlq5i5hNVg/oNQA== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uW0V0SNu6gMkNVnvQaya81p4dbSBuufxcX8hYuE+m+2pz+G/9AHxWfOf3ccqQKB6hww2KBeXJf/QOdql70Jnc2DCyqXTwxBzlShb/nrkfeWjxsflBXGxhI3vDXsYvMobXQ== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== ++crFcMETgRZk2iHejzHCbekxzGHP2OCYmVF6HPBoESU= +KZTmaJLp+FU9X93j5Tpqtw== +ZE/mIUUH6qV6H+P0DGv+nr2CTZLs1/rJcRO1XwCVW/9XAw45hBgxaY0BFfGvHswU +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +td0Hee4NJPblA28GiIcOmc/Ym6Bu3ogIAYm5c/oogwk0XBSzLRar0Ih7afhnqigD +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/j9YDYTBW7wzXOYwIgWpVKa4bLLtq8FvlTrhYJpES3jZ27y8d3fqhIUr+i3S+RDaElKHFSFGyD1FTDeDliZEytNZNtgA9D8ezFQj/kBFAN9E/P7It4Etpn5TBosumpDnXVft66fsfb6ncNZmrFxs3jk= +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRrzaJ3fStGEkJ1TrpFS03bbdUIiVBbfc+ifRnD8hoj5i/vGHJwbslvMaI07I6UZqlSe4f3Af9BMCsD66aZlfXP6JZgVfGWadkqsrXGCRsfIV +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXoz0tnByKjXrpn0PixC53xxW424JtFQiPFy3Sr4iAaZyHbxdg7T+nBoNbXsIKW0qzqpTFzWnwebvdbxflyIaYDFF7xsOPdqQ/uMxgOiuHsDxnkGhQh2qQOqFgAuZecsfQ= +wgR07xfoapmx6eEnFHXXYsXheOqb+ac7ivrKomECGJjgpziToag3V5fNXXvZF2VqJzdP2yN9TApu5LJMNhW/Bphlhm7/zg8uo3tW5D2Oe/deZXzoOQx/WMoQDK1USmSn +WHkOzVx7seuLmxs5Hu+l/vewF5INruHu21pbeP57J1zziU9fiig3+az234q1W7m2 +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +kgF2/6843W+vdtmAWZoAPUUQVr7H1bIkInRSHfSIegJxbEkLNKdQmNKtEDjEYvE7TAdXnlvIsQZVLB8wq0YhbA== +PRG/YRzXVD8iVR3bzEcUnpaQzJLIqAWGPz21mQzKfgigD0h0w3pUrWBlzWfZSsNIAA8aq2BTcbI8zUNJoPHL8I/D3OGrbtm9uWjtHKbcm1c= +xWoGNWjKGPfI4gq8aHoTfOuj52m++iET96O/j9y6xVlGm4+LbAACgyp9oG8Hv7OC2+YhWhhRxM7XkLUnZ8CjRA== +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +gZ7BoIAABc+I8g/rYzq+f9+JYoehhU6jEk+BbcHC0G4+oVooZaXV7wQeZ+nU74Bk7rzgbj24HoseqPAY5HVLBg== +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqGyyDPCcCSOlRV5etylc9M5 +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VV8gPVA+bYqqpFFCw1YseDEzzB2XhvwgeARRSR2QlM7ZSrGEPeKj2PlbIY2wtLsEq61MP+8rJkGGqrAgI/6o6I2 +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +c19batRfRungBChht8vUMh1zy11wl0LMS69ItNS4EhzPiaGMcmOapctmdbTQO/LV +KZTmaJLp+FU9X93j5Tpqtw== +mW+EgwYdyL65jNEPCrA0ZyAiluXKvg/5eAb7XSOfiNawxURG9t1EBba6VRmOBkJq +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +yXqkSqtRGSEMRZqQHZ1+mt+fn6ZoE4w4GNs8EC4uyuo= +EXpmYt6ARP1igJ4wg7aM1bxZSfm/CYsM2UZa57yY2kwV1UU3EwzSn6LkGJw8sL7Y4278DX0WDnRvgif0UVQ0RQ== +td0Hee4NJPblA28GiIcOmTuwGfcKcaMQUQAHvx5+SrqJRTOnfpxLSMOragNj5LzS +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/vYqxGK254IkSnFz5Gbp+4D+189i758u5SMyioBNY12TNuKolISMgIdF+ywmCAE+gka4NGyLSjBg4/EcgJgqQdsBg7Pt9zrLwkffmO+9XgeDDbKDnsDT2xAjXIapIMddUSL36aiHe3sWwxqbPgWPNsEDr95n1XMy2Qabo0H99FGN +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRkp8YQy11zttrnYylwpxv3NT21EGBXtuBB6FRyIL3b/coiheIu5ek4X7YoV1Evy83rI3w8z6fy1IGgpLjNGAd4qf9g8Ugjb6DVjrN/HmIchP7rkjrv8/uIekNKtMI2/f6hpIJSyeP/bOXKVDe2uMBks= +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNUKpj9eBHJWRU8H36UXDyZdte2RnR4/TzUtpmwI7Mb1yeurMacT25OjD/5z3Jjpbra3hHXD+LiqfMD2gFdaHlb4VLjRqQW884HCyYph4x+cSA== +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +w3tITPcVqHzL879MydOuJWxTXRB2aaFKrq0+P1wwYr+Tn/VQhZrVtE1sOh/Shl8tzkg/xw0YCXeaoCjUhruUE4HePjZG55uasXJhtmcbhEE= +NcZ6sQ1mLPT8+JyzvATZXLyE31SvRRysQ1/hjQengeTwNZn320jW8CLJ6hhiOvminGdO9sL1lYxrbAiYZ3/ZSyLPfa4mpmClQ25ZDtSIL8xQOLRuc/8OLNGi5EGzIJQQ +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +EXpmYt6ARP1igJ4wg7aM1eAL38y2o9wJmdEMbt2Sx8MVFzLtkihZLt87HyV7GGKe +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM68aMl/bRRI6Tco5z/OD4QuuJOPxL9bPneSyufUxuGKiyG8VZgNRnFN758snADqdGzA== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uW0V0SNu6gMkNVnvQaya81rLC1/0Fvm7UsmCiOH3f1ao04XjlFRFbSsOJfb5NzMdtgTbqjkn3P+poxpfB2JcbFY= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +j+SwqpR+F2lYRZXNJryLScYai1d96SkGSYuWvaU/bq8= +KZTmaJLp+FU9X93j5Tpqtw== +mW+EgwYdyL65jNEPCrA0Z2ByoKEXE9FxVo/BFhk+1r9rNF5yEpIfXSbwvjCXhJcc +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +yXqkSqtRGSEMRZqQHZ1+mt+fn6ZoE4w4GNs8EC4uyuo= +EXpmYt6ARP1igJ4wg7aM1bxZSfm/CYsM2UZa57yY2kwV1UU3EwzSn6LkGJw8sL7Y4278DX0WDnRvgif0UVQ0RQ== +td0Hee4NJPblA28GiIcOmTuwGfcKcaMQUQAHvx5+SrqJRTOnfpxLSMOragNj5LzS +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/j9YDYTBW7wzXOYwIgWpVKZT2HFNTZ/dnELZpQw8sFx2NM6OSEYzJeSbY+hg0pY2YR3YlUvPs8waHihkMT3/w5gEqC9SDEfFBLCggA+6vVOvCmKlXfSId3RedaMql/OQGI2KIRH9oZZr0FLJwJ/rfiHLvHF5emM0UM6Pvk5A4rYV +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRkp8YQy11zttrnYylwpxv3NT21EGBXtuBB6FRyIL3b/coiheIu5ek4X7YoV1Evy83rI3w8z6fy1IGgpLjNGAd4rnSQmRRxr8fplixGB+zZpnozX8CXOVBq7e4QokRD6H0A== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKMlEIqjWm8+kOtLiMZRGDCdhGoNPppiiLZ6wPeQonyx8QVAsGxKGp5aqYd41djmd7mKT222yACYHIgsJosqizpwpc3VQxt+nUwmZflirICXuEVd78LA+xjQBmoYusnc0= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +B6GcXnDqGMwz94rY1vzl3lcoIveB2pMDzZOwAWkcbnDoSW9+kEaPJIiGT80+9uuaMaFtSlE3linX5NcYT8M6tg== +NcZ6sQ1mLPT8+JyzvATZXLyE31SvRRysQ1/hjQengeQgHhAHwpP51q6ZVvlSjrFoRWgULlt6+pnCGuxC90wI5LMIgyeRUwkutTf5mzchqYk= +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +EXpmYt6ARP1igJ4wg7aM1eAL38y2o9wJmdEMbt2Sx8MVFzLtkihZLt87HyV7GGKe +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM67AZHUosgLUiIdVjNoh6KcHkOZpX0/gAyUoZNZVkt8TMqg1owN4+6WgtvW5QxxJ3XA== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uX3TRUWCciAkYHgiV/48FRG5mrFv0q5F8ynfWe8wmV1Tlzb9GJX3yEyrdbRkbKBig4oFpaaO8y3nH+CJvUnnEyqNGWpMs3VNzicgdIgpKjgl +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +QUCbQ9a0sqSffASbUUf6Zlw29gjlpj1Jf/NheMzsBZs= +KZTmaJLp+FU9X93j5Tpqtw== +vnyeEw84C3zNqYtEr8IrMVYfdroxRyDoD6fgBCWOC1zxCh9TdDz61H66jS9ZZgyW +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +yXqkSqtRGSEMRZqQHZ1+mhBaPbfHnkFsN1qDfA9aL2jK43JmePxKyQ83j7jD0949 +EXpmYt6ARP1igJ4wg7aM1bxZSfm/CYsM2UZa57yY2kwV1UU3EwzSn6LkGJw8sL7Y4278DX0WDnRvgif0UVQ0RQ== +td0Hee4NJPblA28GiIcOmTuwGfcKcaMQUQAHvx5+SrqJRTOnfpxLSMOragNj5LzS +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/j9YDYTBW7wzXOYwIgWpVKaRwM8m4W7nRnbHQEcrh5EBYmJZlGIqfYwQxFmlVxM49wxm6Cnv5y3/UTXc/s5sEe3C/UKbwp+Egw42yyoblh1S+6ifPQgYTWCNgqq6llQ9VEIcgO5wEXZMNvV21T+07emp/0tTHW6U5xh1na8pXVLZ +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRkp8YQy11zttrnYylwpxv3PAoF9abnkTLvBuxKwqngRf4jL/n7LWjAvSrAYdFb+9BaNinGPi5QRyUCX9iUPvtUB38XcGR52uCIxFIGPX5y8P1MrqTIQ+eSStzPUhzlz7XPpl1fxR/UZQ8U9/TTFSoHY= +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXoz0tnByKjXrpn0PixC53xu/Hz275RJL4kiRWBr+mu2ooqlwYGOWxj1p59WTwtqYxYP4BapWeh+MjlYkUmAK3o6NE0FnbzwUeJ+/leN7aNVRh1wvtsFyrqW27xcgc34Ec= +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +1bgO57ntu/GNYZX2caSlwYZUyyyxdGMxQKqFhp5LtTSjxRUhVjR4S9CAeRudMXK38fJvAV6auHGbfNnBiiE8baXvHIT1dDjV6qtkJtEtHDs= +NcZ6sQ1mLPT8+JyzvATZXLyE31SvRRysQ1/hjQengeTrNPpSvOL6d79ECgjYIgQaZ+/wTT4FPcAS6JC05hJ7fTG7gykXosXQBVFP6bEQdlz0GEkRvFECHWX7l/owsLJP +IhuisKim47k91RVt8z8qtAEkNTtsA6zH8QG+PK5FdYvfkYixZzQRMXKE3CLvPLBh +EXpmYt6ARP1igJ4wg7aM1eAL38y2o9wJmdEMbt2Sx8MVFzLtkihZLt87HyV7GGKe +Nd4NGMamJX/VaDzIQ/eFpJMlRO3YexRW3lEtVUlCMgx/FZPEtgNZe5Yx355KpsFi +nktihtQ3XBvUv8xRFezM61BR4+uEHudN/AadFltFcKTYemmqpnexZSV8Nh+Cr0OVLstCu4a/JtyIJRGOEOnY7Q== +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uW0V0SNu6gMkNVnvQaya81qwIzWYl9wpCfcvIrYp5Poa3hLEkz8EUxDfrf/Ywk7Zp166IwQYGb5Hif9xtrMf5JXDxDjMQ2w896YpYhT8CjcA +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +gEP05OadtLdkLYzVLnPWJgAmUJ75B38mNHU6Ew0rSyiwMGR/TReCNztLh0QT5fOC +KZTmaJLp+FU9X93j5Tpqtw== +dEamQ2u1OznjljWi73pYBuaIah6zJ4rofIkRjr/0s/U= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== ++m2Ig0EQEJ4Gc+2v8s5xrnZRBGAfDHuVg5r9VE/U7Dqcsp5mSPEG0DHHYcBZkfWo +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DMp35ZO6aoXY/WoLH+K64ilYPlqw9L4ZaTKohSCA0j+Cgq1ydAwCcdZTbRoiCAjSs= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gu/u/ZWm9DYyJF1WaRWt8m4jRqhzizNU1gK/xrmGr0hXVY3UUV08M8l9gUQoeu5PPREImMreYrCZJ+E8reG4IsJzSRP8CrJT+peGh5SAc3lShXPFKo4Bik5dGrbBucJyiY= +KlUaknvMM0XG1KrqU1G4aqEq7xOHjfhNa6dXTCBym0E= +1jxLOCmFwAyjjgguzOAQujJ0C1Mc9f/enNUulmX/aPQ= +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5MWjC6OS8dz++HLDh9rgwCg8eJTHO8GLedgAPe2+4wRq1YT7E6kAyCN9VhCDEMLkdT9tL4CDW9/n5tF12+RhFhg== +HLX4lLKY+SXlZdPzp8fBulkdKWMqwxNxIAJ+qchj7AXrCXVeQe8/sC1B3D88QrNc +UP8nNoA9lY66xsBlnwz1GFTpFEvmG+sRugBw9rVzyuU= +o6QhOIN2Sc4SHELnst17ud6xJhYBLL1CI0oeXeqG7U7HOupQi5nahh0bhahrL3Vq +o6QhOIN2Sc4SHELnst17uR7klP9o7zRX3OHkgqJMrpdIzGaTlIXDBx4pw1TNtIL3zv6hzXyxdWcDB+hqAhJRlSCApqCXSRusVAmzB+biouM= +o6QhOIN2Sc4SHELnst17uTTVGGxmpRyQZPTY6PQEKa5SY8WEKWbH6/gFdfBFWrxu +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +i+CTuLsvZvzsrs4dmj1KKRYZ90Ua9c+VB1lKC01O6+1xYa3w6cjuyODS7zol9LuQ +KZTmaJLp+FU9X93j5Tpqtw== +03VatSOa2OwbTaGxCCsDkfRNwuehin4sgRTEQbZqq7kVYhs1CEWFqJtNU++WDwFatgr039Pfm2BjL4R3Wy2RgQ== +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +KgZhnCYglllfsrazbVxNlpRsHzYPmB5M2ILaQpT/0noosYD48ccQjgkGrnOLuFIlUfWt0Jxc2AEcng9bpMY6YA== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNVnsl1+vJ2tBE0OwPxs7prnT5qapGvONu8Ngbc+l7WyuuVfJW+FtR/74vS+pISAmo6KWIZQc2VlGsAsSf3tYsJM25uB3sclTZycx0fC6R4YjifNDos6GxaOCSoI1UCWY34= +hv9JMLvs53QQh1DP5+6ndZgln+eW0nElKMm1KbM2W2PskqOPB99iZL61ZSnG7rJ2eDdknIv/XgX6lVBgD8IQWg== +XdtEyBM3Mm5sMRdUDFrxBy4laVZx2zxt3h85A7IU2cFKM0GDhpJXtqHS8spMHIJsvz0OEaBwNFrn+KMa6btmO8oo/wXvMMdi+NMtM1ihg0g= +WHkOzVx7seuLmxs5Hu+l/iDmKuVKi/pTW+c8evqV+TMVEqGxxzIStmDwxFwASXBI +CW/G9plDL3SfzmBxnqr6q87wTQCNFJP1gF2kN0sG2a6iDm31tOkXDjbm8C6mn5Aa0SWfua5kDd00kMFazsK26LlL62O+1eQeI35+d9zGbnk= +ltIZHHUgft++nMUXBsBki69X2QPZHt4EmCiu3d4tkyvgWs+bXIvDihJYJrCebLpX +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqGyyDPCcCSOlRV5etylc9M5 +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VUBj8icwO++avtf1sayWKFhVOSpue1yY9bDJYOTTzSX+hmpAzNJxKSaCs2lwXXKg6s4n1M++HfQ5CBXXS/2BpJ6QBoC6+CwoyyT3S8vHxCKiA== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN60wnCWEGAjttVF/M5NHLkMAqyk4umCw9L2ii1LX6tY23 +e1qLykdSSG57G+I4DSZ3lDDNWWotaAXYRIzVKa1N2epQ6l683rHvc1gkqjl/iCnr2qtmRYNKoBqDAWQAoAmgqw== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DJq9FsIZwgRA0seTz5ZLwf45ZaY/43JatXtyv1sn0l0W6pN14o95ePpIaeWhFSX11VADyLJ8CqNt/eJ+ry+p7nphc7oPJdOUSR1dHg6UxeDeaelTU+cnumX2i4XH+SR6VfJl0sCuoZtf5wyHE/bXM3AIC96j7iuo68O1F/NLd6Py8iiKh+hKfIqZUuJdkdQhE= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gtyrN3fmhNqvxN36kot5RF2oiiBq19d9SVBzZYiebNEINSmskRQEht+1EFBWi9GHHz1bQLYHdD3FkyAvTyWsMdyPzrxR3h8BEtAcNzMAqTIecygi91vhM2xBUA701EBPeI+8qxtM5zgqvJBtXwFAez+ +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +MgPAHo2kx/v09x73PLfPz4Y9t0RHilXsEXvbfeZF4mc= +KZTmaJLp+FU9X93j5Tpqtw== +YSMUXXbx4tCGvAMJYzluZMmToF6l7FIiZwFQRe3i0aW86r8VPTzleeun6DfljKPh +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +fdeYOeeuoWy6J7ezwxYzw4ZIY9pPIraCczrpQYJiR9mb7+DAhEV3ebytkVxaIQ23 +Mb1TsV/7Ki446XO5w7AS5Tnke4Mw0UNWSe5LSnNguUo= +1WBG7bZf/s5gJ+qIaMFAbF6S8FDZoLzAhhqJ2MNTCVdX0rFK8L37rJXe6iLQ+VIT +/0ULpLqgTvInFD0r5hHANnKkcJSE/H3VghkioZ1tnjyunYYq8XfVeT+J18IgTe/z8NUrzTSoyNxgWbkOYZIRQgQdFDdl6ZyMvSRHhimVOo+6+IPoXd7e8XcJmZN+sF1bywAr0k3SZVauL4vVCrZUmA== +7KFwZuSJAC7mbgtMUMJu2zeBbC3XtaoOE8JL/ymxSKo= +Dlyx6ZDFdkn72YIxkC3bo24wiNcBTdhojKIyzDox6X6Fn/6SHA+LMut2OBSeK24aqcciho46v81csISe+FPjxh696U3t5f8i6aTPRj3iFBD2ad2pyEBsB010WMOs1tGGILKoL0q3TjlbS/5SqbuicA== +VmVrGQo2zRokW/ZuO9bN60EXpD8GKK2fiY1Xg0XFbMyYwhONhKS+p6paMXgT3qX0nX6YjEUDLqRgt9iLSNuHcQ== +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJmXbFWE3Z8RNiviG8V4hH16 +VmVrGQo2zRokW/ZuO9bN67li7ywWLYu9Y0gRIjplCnGtfVJYQf3jsgZWBUOgAMgb +VmVrGQo2zRokW/ZuO9bN632Bl5M8VcuruOzTbQLrYlXe5tWig3n9A4hJ+A+5Alc4L9uOOt7pn/CI01swnNUUvg== +VmVrGQo2zRokW/ZuO9bN69oalWQ/Y/wPLgrpr0WDj4hWOHZqXDwO01uW8tMCe6+T+IcR7L5rURVZI+OAbd1kSA== +VmVrGQo2zRokW/ZuO9bN65rHFZTGDKBLHDMo5/MqrG0zTM5Wypw2eZ4UHeJSniwCC68QyXMnn1OcmKYARfxFLjR065H6tvg75tpwdq0aMv8= +VmVrGQo2zRokW/ZuO9bN6+Aghyufrhh2DrE8EjGxU9B9P0zZZOVvSNoiMbJ8u2TGCQfRBmDeQ561PT7skPTptg== +VmVrGQo2zRokW/ZuO9bN6w6imMnMMPRq3v/Vj215HswQhNFa+u7N7x5+h75+em5GT8jPtaH8gFgTeTBSxpfSDA== +VmVrGQo2zRokW/ZuO9bN66IgW9hzgMYjzzLREnulmKvAw/D9tfQZ8fSSgnT1zRpdJ6OEyrEsY9aSVoO92LEzJA== +VmVrGQo2zRokW/ZuO9bN66z2sdLiszhqoZcMSNt8QNUub75cDq0NNFT+IReTx+rjc4S7Mq7fPH6kcdZUXNFv+g== +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5Gr5CHxLtPtSieSDXE0Cb/KjcFqA1XA/MXMoi2HpbdsqDw== +gApoKqs53F0hB+0nTxFBUPbKBjYJlSBd3lBIeTTeEd+DPe1cmLese42GyyMquowC +o6QhOIN2Sc4SHELnst17uVVY+9qVJ1W0msopxPqPktiralI+o9zT0ki0vmr3WpBr +o6QhOIN2Sc4SHELnst17ucR5tMgam53vmM/6MsjArHiEQsl9RJ+nyTnBs5oVagK9XukIqUhbZVd8sXKP5yHIiNKDz8B/KmT9KN7HJwI2BcPSX02PuccB3Z/IFU27EqWKPCpbdnA3DsU/4bepg0I4i5dBl4+hmyKk0e7qg4I3G5wcHgbY54DVLW18NqkNdkb0 +o6QhOIN2Sc4SHELnst17uTGbfoG65qJYuOo7HVAUfwrrWa4B36uJ5REt040T2SNmGAV0LMPrERjzC/oRk1GEuX7AfuSpWeNyK561rxlWmqKq8esttm3BDwzTTA5Wp/ZKFGU72IJK2pjrHuSoqkzLVujx2qaqM/Km1yCxuLXdC2UZk358XQ+ddlTAL22cH156434R092KKggual1jvxpED9VmPJ1PNoUP6cx4WbWWYCyTyPc7+BhF/kMm12S2r8/z +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+AI78hRtJVdZsXVPm3xzZWyq2UU/Mc2lFq9aD/4Cho4iS44X5riwbq/fq6uPnqzbTcKYuqgsiWplT21S8LZ4Fm2zty/dHSsU/avOJz6rLWj7Je4qi3fKZwvPFeX7kuPiMB4Yq6CAzssmnUrr7OiNswW +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gu/u/ZWm9DYyJF1WaRWt8m4c6VFLbfAfHnQc9YRPwfcloEvHTXGvN6Pl7C/SMpwgHElRXOpJnHTV1iiSJ8iz3dqt83iucDEN0bGwOeUejFRB3HPF/p4p0surHRPXiGRcTNk4/L7ZKrCYSp8ociPUGApdade1OrQqF2OyGzrfVrUDo8JaTMR4VipxZ4gZP4qTHKj5LN5ozU9PmCuQxpl3wHvIT1lbRsfuac9vaCl6BAdPXY4r31LU1E4tPw1tvt6ooM= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +rOsFgabYAzAaGnc11wPwfr9SKNg/vOxFZuHKBFHseoil0bLX+vnM+JDUWI+3VHCK ++m2Ig0EQEJ4Gc+2v8s5xrnZRBGAfDHuVg5r9VE/U7Dqcsp5mSPEG0DHHYcBZkfWo +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+ApG5Mqf2WNrCxn3Gs2J4gOCuJpjPTZCrljpDKhczQ7rzIKfTvOZGrf0s+/mJDgPbU= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3I3oWHCNytmq/khPZ8T5vop9d8mBTS0Q0tTFiGtAE0nP5h/ZBt1B4WKQrqqTDIqvR+PZeU2ufL5ZkReQeSSo8K/w== +CGMG5LRrLyjmr66XNRnXQM947ggRCBWkdZ3U+YWgedy2DMtmq9woOHurkWQQKXj1 +N5xldEoitd9eWUThCvdOTIiFGgkhHo+5taj7z225wOk= +mx/N1vLTSXxv3fzHVQc+Ld+BhrsjDb/SE7F6msNJ+3P32RMgFq8NWcxgNjec9esvqcQUK3a1Ff8Q7ZaBHVBGZpPDopiwFhi8WouoZKAu8Hw= +wgR07xfoapmx6eEnFHXXYsOrmBevoT4x/SdtelHhA34TH6958NaiARR18MH1W5Zyw+ONWn77sAOwiF7dLtKmtnm5vZ/jUNl7TgUev4NvSj8= +L0eUthVnpkGsmKFAX6d+uPOqTLQy9mN/LSujvNUBfJILNcqX+tIER2PaAivxKdD5 +1u+XjG/2+GSQRv6EzCaWRQ== +8W/JzearhnJZ/f1fuuFws1bd87udt2BzqP1QM+dEHmQ= +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNU+8Y6i6Nj3L9nyiNA2a/7B/3wHduPj4RjT8eK5AyaCKUxLExONYh+9hvT9FTHh9fD6G+9Ge/whDoUufbCKcn/p004yMZ8iVPclymeY+PFJa/RUPTY+X2OhDtINflLUn4g= +KgZhnCYglllfsrazbVxNluM0wwLR9C9fkh4Pz9PkxWumtUfGfHTWVnBtNg7DAsoHZkbWSD0tcMd4XAK42WFKJ72tG6nWENMgudeMeVoZNF/g6695TZv+m7OkQ6qGTkk1 +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+ApG5Mqf2WNrCxn3Gs2J4gOyeUOMMa0j3ynXabSclI520O67KtE5ptoz++MMmbDiXrVgINl6S4qZyg5YZQtf0fuDkfYvYTaSyVxl/SmDQiGsl/bspMGVS44bf2lQChSrR86ZH9rF5pt76PJLSL7rkhrPgpE6vVEEBx5dYFTA0cODkh+wHPg3f9QypkuQ+aI9g8= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gsaEBHtsTX5vLF1A0mWW99ykODxJ3k+2T+yMQKxUoogBbz3OIO46VTOBz5+V6BXF3C0auyYhKrkqaLjQ+muwZlyRlF897Aq6r4BzK7piTkJ+hv96K4iWQKsWsYAZQAMc/R+Nhte+F5inwutMUSMOXJFutn297TrNfsagkLU43GTnbcNFNx8E7pjKiaa48ZrhNLxxk60X7XP7AQJzmLT+JAz+hbZuYqdOZVZSxDT0eHMV4tYTLwla+erBXlkxziCFGJ6OahhrazzD7zbNeQ2KzEEcIxOesuonYUs5ktdGWuspQ== +td0Hee4NJPblA28GiIcOmUAQS8rUDYsqzBGv1wXjMizXxtTyVHniddoIJFWktq56tn2BgJd7xITgOUFxE9+rWg== +WHkOzVx7seuLmxs5Hu+l/vewF5INruHu21pbeP57J1zziU9fiig3+az234q1W7m2 +CW/G9plDL3SfzmBxnqr6q87wTQCNFJP1gF2kN0sG2a4BRmZHGr+3zw2g1xf5vtfgLwfVm8mGYIny9ZcTFjXWVKK6iLDSSj/rpfEb6rp4iEk= +ltIZHHUgft++nMUXBsBki69X2QPZHt4EmCiu3d4tkyvgWs+bXIvDihJYJrCebLpX +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqGyyDPCcCSOlRV5etylc9M5 +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VU06FxUJvqkHXdK+DAvhPcso+ah+qu24mF+1RXcb6qM5EmSmg1x0qIsCtR+09SHqPw= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN60wnCWEGAjttVF/M5NHLkMDrXkykOUsjbKojY14TcXm0 +4+wKB+Tel1iow8au3cOIJQ0cJN9p5Wagnh0MqbqOZlNSf/KBhrLWbADm0lYhyxRZSTv1g0SpHIeHGOo6DJBP1Pxj0XFYKXqcn18zEdiU1ng= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +NNi1hKZwwGwLUUv7+aXfFZA+Iw3un2r6P72Ea5HC0dqLITVSg45e6aTL62Bu0dbw +KZTmaJLp+FU9X93j5Tpqtw== +s6MM2ymq3uTQAnahg9ZXG5jOA0jMV+0VYOIZp5JMlIE= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKzFnp5L8v7gjpRd9cQpcaHB+Wayp+7VzPYdk0sF4AoTWeyVn1M609OmPqDhov5yljlIPUExR6T6+xi1AMd2nmE/TST+Kij0fvHcVXKrM7+Q== +JmsPmuBt43Be+lPMfSus704KoO026ezXn6mEblx2QTe8QfYhTuwHXsXH0R4EwA7WweYFxxjsfZCrQQJKRELANQ== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DMp35ZO6aoXY/WoLH+K64i1f+1W/w8dngvqNZEKnyPaydYFpnV/gNAPkvDbqDtt8Axr0OD5HFD7pDSWgjWT+kj +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gu/u/ZWm9DYyJF1WaRWt8m4WICqCsMbx1EgPW0h8DWlhcT3CCFTaJ5AOdc1RFp6LNCnvBeeqXIuaO3pMcyZ2LIthcqYu7tRXZ1ElqTv9riSxCHD+L+16e6wlm0Bxs55V5btAJZ3dMw1FkL0S3BJZvSVOsM0xpkKtPQOj98RoVc9Cp8PYveJV8ES07Vz6f+DtWAaMZGrwkv9mNisPngiTETORUjmJdrr/g8tUbl2Uagcx0hlu8hwtnY+5ho15Xw+4CU= +WHkOzVx7seuLmxs5Hu+l/nDthAkam1k6D4nYqoGh8cipRxIezIDmlk5DZPBQiAaC +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN60lFRhtJKRQkH4TNEtZxNneE3WeZsRDha5/iKw47AfE0emSsmQ7PWPQXPg2kLPkYGA== +VmVrGQo2zRokW/ZuO9bN60fEHr48OZi82IVUC6YL3H0= +VmVrGQo2zRokW/ZuO9bN6/79/0H/+4xrFviTRFBeiNXjvYVaeWNuMzUV59P1gfmlEg6Omuf71CFpA90HyUXUzRCpsNjekwSQDS8UWjK2eal9Rvahps0GRwBh8VR+JcvK +VmVrGQo2zRokW/ZuO9bN66D2kHqyaaaDXvKupYqk/8786fCDGP+SR9HR380T9MZRh9mNZmdOqHh5iPNTdQGDxQ== +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5GoIUl64lV4c6SM6jvfq6ZHRjPDd8079G2HEigi/ZfKC4Q== +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5GpgKez6lH4gXHygcarAPRBnkgOyt/F4Ace8Vj3cK4++8eimHerH6cVUupLPWGCcqVM= +VmVrGQo2zRokW/ZuO9bN63Bu8EQDuwqHh06ZdDTIvqAiFMjwA7VbfCT2Ne9JHAZv +VmVrGQo2zRokW/ZuO9bN6/po07eiFpLnQezWsZS4nGPPig3Mk2QOpRNzGs2hSrN2yBAx0P10m5qVtjlTeATq4hYMkYUmz9Habg0RlyYw/iQ= +VmVrGQo2zRokW/ZuO9bN6xwvWX36N5sH14is1gdaKn5yp9luAKwrLh+Qt1r7O2jENYPr6Cli2gmaPDxTG+1787BDzfYrwHiwMfjoqFlpXSk= +VmVrGQo2zRokW/ZuO9bN631IQ4qTkXU6zgbp3sCrd1NlYB8hewvpcJNKvjMrbUEzL2M7tq/dbcSz/GolEssynfajWfrV45qeOrdjq4k5Rep+mmAhTL6rGIL/vMYaF6/S +VmVrGQo2zRokW/ZuO9bN6wJ6Cg+0P6lZUZNdwnbFiXDIRqFpSEIlfB5NK2xsspXXxdR6zIPWUVgA8Jvm6DNRFZQ3FdczlOxHbTzI2zZhWG9zKJudPh3RFXkz+qxI/SI0 +VmVrGQo2zRokW/ZuO9bN6+Aghyufrhh2DrE8EjGxU9Dlq2r8b/D5xW5WgaM5MFXpz5KcJG9DakmG9eznFCr2mw== +VmVrGQo2zRokW/ZuO9bN66vfklQEmoLc3ecqJIXu87MePSNwYUYJqyLugLRSgxrOVoqSBxoLcq7Ceaq9HlkGdgeeAdYQBlUxf9velRkNBNNkOkxPTnk2O9G00hCADkZQ +VmVrGQo2zRokW/ZuO9bN66D2kHqyaaaDXvKupYqk/84CKVCkSmNV0w9PNkR0+m87Pw4VsDgnnvQ9qy2bouykwg== +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5GoIUl64lV4c6SM6jvfq6ZHRjPDd8079G2HEigi/ZfKC4Q== +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5GpgKez6lH4gXHygcarAPRBnkgOyt/F4Ace8Vj3cK4++8eimHerH6cVUupLPWGCcqVM= +VmVrGQo2zRokW/ZuO9bN63Bu8EQDuwqHh06ZdDTIvqAiFMjwA7VbfCT2Ne9JHAZv +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN62wASuZkFvG/cu/SkmwF5Gq0d2ZE6cnfazkFCFePQ+aqXYHgHLHiD4y+zDPj0czCQUlieFMRFTuiURI05jIC8LO4owCk6/6Vbwv+eWNKllLGSBLtQ+B5DOSD/NKOY2/yoiAS2Hybi5KXRKsHdHO50DH6shFKOeIEeUNisDiCVipLY3mfH+CDhejxvwQZNsp//YYuvRJo14m/n4uj8RfcLGS4Ld+8xYRWQhdZjYOaJiiR +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN66Px6Idtf+WhzwY1S1dxdnQ= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +eno9WEg2wTTzJ+gLpzzw/HTtAiB8aKTYv7hWsOroVkI= +KZTmaJLp+FU9X93j5Tpqtw== +VoUrgU2B8KdsRpJ7efZ+EA5jANK2VE4Q9yJqIsWf0kDunXA6T/uxqyFKNXOtG4S0 +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +td0Hee4NJPblA28GiIcOmQvZP3B6R2ZujAdkKnUx0Q+I4+Sv4HSGtNjiXZnpen2szaYMKvGoBXyB7qIf+7BawA== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O55N3rbhAq7klcRcckAjfgMUIW9a1jx1oEi1X++CwmLErG8HB6OZL7x4Hr5sQznpZ6W4/Ts68IEMmYnin3AQei0w== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DMp35ZO6aoXY/WoLH+K64io59bs+NGpD+pgxT5vCczMLbfrJTSxqqt/Hw8Op4348+BrAUdwrp3VpXTh7mH1WSM +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gu/u/ZWm9DYyJF1WaRWt8m4WICqCsMbx1EgPW0h8DWlhcT3CCFTaJ5AOdc1RFp6LNCr00FtbkTnWsCwfrXrTsbLMLqIIdyowhKIEVZ1uMxl0aBbWG7UNTfincWWCEaudDtMd47vWWXiO6D/JoTPSrtYC0G8p1Bm5nHc/HdBY+8rTfS8lOCEPpP59kCqsis2/VJHupMxAupSe0IWI6wYkxX2 +WHkOzVx7seuLmxs5Hu+l/vewF5INruHu21pbeP57J1zziU9fiig3+az234q1W7m2 +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +QUS70OYdt9peiASDI5xIXhmuP5qGNd7l29FUn0MVUOKUoVKh81g3CNlyheex2BJCXnT0ZA4lUnRVs2EFBjO3NAYSDO5CZ+iNOggPK3uST5o= +PRG/YRzXVD8iVR3bzEcUnpaQzJLIqAWGPz21mQzKfgjXbgf1iITB9OijnCvCJdY/3JhJQM98k5ft0O7J677c1xgw4zfoiZ14vhhJp0udnmOkskg+IOrFJIjKK0kLYunI +xWoGNWjKGPfI4gq8aHoTfOuj52m++iET96O/j9y6xVlGm4+LbAACgyp9oG8Hv7OC2+YhWhhRxM7XkLUnZ8CjRA== +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +xWoGNWjKGPfI4gq8aHoTfAu9Vs6/gfwfIM7OdnTK36Kfyn1EWsGnxFcC//aU1qhR +gZ7BoIAABc+I8g/rYzq+f7YphugF2lPRkwa5nJWjTMdXHzOu3xxHriH9vVu1fRerJsjp1v++nvVN0LNnEFp/NxA3klFsj65vbAPPxX3ES58= +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqGyyDPCcCSOlRV5etylc9M5 +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VUVV9LQ44dzTLse/kwqOl9yxmLv8W4mg0ZKrhszhJym8x2yHwRsRyGJqB9LxBe/DF8= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN60wnCWEGAjttVF/M5NHLkMCCbzD9pGO2cReuqjFybSp4kz4yQBLIUnA+3PCye0fdAn/WRK9dgI7mgpXuHvqzysrNqHoOJf0LnrV0M3c+lzju3l0nHt+m2gvMHoModDcBDjPxUKv+v27VRiOh9jUhbrppuC+UGQw2aw9YgBOPhWf32MKM/vjAEsYElsi9V29gOQ6f9EIZ/UK1WQooXYQeAnyt6UGuX/jmPTKUkOoByT4L +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +8XTxPQIfYULtvjCBvkZx1YhoUTFClr16CT915q1viIE= +KZTmaJLp+FU9X93j5Tpqtw== +0+26ahAP5xvCkxbzFI4FXcr4ixe9Woj9tMUwqqejvuqkOa65iAEbFYfwb0oO5kxV +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +VaCAe3UOqxyutBS1/stGgipTu1LCBB6LfEItp6T8ogw= +o6QhOIN2Sc4SHELnst17uQEjK8c+2Lhy2hqf8rBcSosctX4Bqp2L483M87OE9eXCGS0gqgBqPSCFoCG71wRbJu3JX8WfDjtLEd1sjE6RVCkHyG5at/ORgRx5QkamXNtMHbIKKxfSINigd4EufkdHhLXViRuJvlEjZRSF0fNCHg1u+zI6kuMck2hgx3CY3lhR +p9oVsBgcyiBMjDb8CcPOqM81PXqdethGefe3rfhxupg= +R+prWFCOPrLT5joahWmk52xc+RdsPT+vH/fKn9PeKDeMUXUKiIbyxa8tqQID7SBN +h2UtuTBnsrr4HCgqqYYJnWrnByIjZpML/uoCeGo7MsLJGsfnYIvKrtVcwss1Oh+u +FLMbt5iAjw12yzW85KcCsjoifO62vxUFIWez1JgdTFHE88gXvEhSlegwuVdY3pAG8tOY307gOTK7Ozx8DjifMPuGQsnxoFa6TuTaP1CVs0XMNRPKG/qsLUd84L/gEMxHIIv6vDPJUPC78RE6arUx2w== +VmVrGQo2zRokW/ZuO9bN69CajjgKg5fUQ/+FvW133FWUCCqqTzYArO5dPHlx+pkF +wlLHv6kT3Q/RmtMBN4nDAd97h19+OtRfxNGzgi50jGOssMw8atTBQYoxchzMqbEw +VmVrGQo2zRokW/ZuO9bN62Cwe0ClUSmyaEfpupp3IhPxOnz12DoA1ftqTu4+jHbSL3aXsFJVGUTTTp3E3l+QKw== +VmVrGQo2zRokW/ZuO9bN69KnuFUVVuap5VPherGCzxD67VRKanNxWNtD/wYRqFgm +VmVrGQo2zRokW/ZuO9bN63h2RLSpmgNQ9reaQqWoBOdLPQqe1RaG92OesbHjIRNW +VmVrGQo2zRokW/ZuO9bN638Nc0xIUBtDeFSRLYCJUAKsTYVvdvblY0PmimcsMh4i +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJla/4dM8s7JsXnXWwFHUnQekOTfql1IW25401hZC7tHuw== +VmVrGQo2zRokW/ZuO9bN6+llKPV15WQhW7xuzoBZz1guvOzmtgE/eKSB+h4nHTiiCmN9xjvaac480xJ2OvxKzQ== +VmVrGQo2zRokW/ZuO9bN64m8VszAejd6d4FfAhX9Cv8KptT/EOR6e94FpQNclqzm +VmVrGQo2zRokW/ZuO9bN6/X63Q8flAlw7IgGSkzO/eJ69mkVltYvmfkDQ6tS4dWE +VmVrGQo2zRokW/ZuO9bN63NDPumt9xAJMEP12SPXSnWEdWqH10pkLGs8/lFhJOGSkDnXFusKkW00fzymwglmQVXPu8fni+fBi16Rhny/PSL25mZby/s2O6nh2lQqvIQ+//fwyJrqHV3j5/8fV+Vx7mQYf9ldV61iQS9Ao77tCafWvgOB0v8NGmGJZsM0QMcA +VmVrGQo2zRokW/ZuO9bN69Fq9tQArdLM3BMqzPbmvmpjhNhlnB+ZcEKXrBeiVz47 +VmVrGQo2zRokW/ZuO9bN60PTt5T70hT6FO7/DB/tfTkN6NfI5stb993dqOlw89AB +VmVrGQo2zRokW/ZuO9bN6y8gyB2r/UIU6IQwHyLn4Lpsf8X1wDPfRvI+wVriqk2tvjZlU3nAx/f7gMQC2jP0tQ== +ACEEdgL/kNbx7RaZcSRxZKNHHeC8ShmrOBm/ecY32lz8qYq3yY7+wb6+3Ah3KqAo +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqEmK4YtKPxBWv6u+f5Z+JoC +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VW9kPh4MRgv9O9KvycXV4e/NNm+VgR/nnCxAKUgoDvpCnR3WNh7E34jXwn8UXub1u1f93LnSmwqSexksosXixf4OyJTE6cuXVhhCuVhPYaRtWoCUl7WW59vUKyH2bpVomHH3bZshbw1WLTi7ueyFafT +L0eUthVnpkGsmKFAX6d+uGkPqUsUWC4LzxhOoLQ9PX8= +1u+XjG/2+GSQRv6EzCaWRQ== +Vb5WV9jio932WKGNTjSAezegCS9tOGWq8jZ9rekzK/MmnXXK7zq32D8EyCcjQxduXK1IX3EyULpgCr2okmKSrg== +Ge4cVWkLTUQ/z9Yna5V2THkcuZPJNSf+2pCrKnAlUqE= +VmVrGQo2zRokW/ZuO9bN61KUtgGUCHuZv3UzXTqlm8eMiRgXug9QtUOYSuzu0Wdu +VmVrGQo2zRokW/ZuO9bN6ycnC3aGaIgah7TcR12oSsCezj2/uODAc95kytori8b9 +VmVrGQo2zRokW/ZuO9bN65fb+fYMg9c9vIPVuWlMQmxyfUxscDhXWrOR4VbrPTGL3OAYIVNcolbRwqMS7vsqig== +VmVrGQo2zRokW/ZuO9bN6y8QP3PQuUkUEDKUNOPDK+Gwcrhc53lkiYcTB9/FmicNXUcV6kT/lwU/7VbDc/oY+w== +VmVrGQo2zRokW/ZuO9bN6+F3OQjQ4pLjv/kdVYoaQPc= +Ge4cVWkLTUQ/z9Yna5V2THkcuZPJNSf+2pCrKnAlUqE= +MehrqbDHPhwXNY3FGnyrJ8ifFwtO/xOi2TmviP1CfElAFk+Nn4CGca7bvdq2Utki +dbeoYmi2AyUYPPKRkPUUOREQ0To7SbiMINlYYXBzWA1QRyUMOUGLfVlY0tE2lhMV7nu1mgYGcpvK3qcwkr3w1w== +9uZN8r0CKVAbfzGtFDuvL4PHTg7r1boDDHPeFGuViZA= +W15L7Vl4niMZUGqXxD6c3ZcFuCu5BaPAmzxhA6Q2NaeMcYqB9mn3t51p4nrx4o8W +VmVrGQo2zRokW/ZuO9bN65FWZiWNfabLLUKv/j/pIruOHKtdVs6BolvMynLb0NYkXKXbAWt4ZPE2ia8CqxhBEndi0L5bYkV+It0WSusoO4RmgN9xWt34rRAeWeExd3EU +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJm9y8nqv5Py/zRJVpi7pE0u +VmVrGQo2zRokW/ZuO9bN61S44hR6AKUobovVufqsNmgQEZ5BONP/3l6yR6Aq4ATaFdy5Ec/+YbAdwsytHjsjQg== +VmVrGQo2zRokW/ZuO9bN68DuozpuQTlmqRl34f2eOHseerzXblhEKsNhb2vs5Ff0 +VmVrGQo2zRokW/ZuO9bN68MXPHGMPcxrDl8VU8nc78icU4F1zYizwy2HV0DPHYJTway1sSDQd8lzZio+Ktlx/g== +VmVrGQo2zRokW/ZuO9bN69dRIuBXxqstSMyqkjuz4hmc+/u0Pn8R0vur1PkwWxJxkdPkyDRHx/DzxwNK3aPtvw== +1u+XjG/2+GSQRv6EzCaWRQ== +F9Sftf1PoN5P+lgvc+r10FT6GwAWf3OH1Mcvp8jOdb1pTtMZQczVgCKJGOvEAzY0 +VmVrGQo2zRokW/ZuO9bN65FWZiWNfabLLUKv/j/pIruOHKtdVs6BolvMynLb0NYkINJbqGdGHKh3pwzU96n3H16eWZo5MOHVBpM75uHBdJ3ipz6Pg0iRPdE6+7+SnwM2 +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJm9y8nqv5Py/zRJVpi7pE0u +VmVrGQo2zRokW/ZuO9bN61S44hR6AKUobovVufqsNmgQEZ5BONP/3l6yR6Aq4ATaFdy5Ec/+YbAdwsytHjsjQg== +VmVrGQo2zRokW/ZuO9bN66XiAxJifzrdqMJr0Vi6f6Er1za0cjqXwfvOvug+gGN+UI8G5pjIIlZZhjnEsTtIaQ== +VmVrGQo2zRokW/ZuO9bN68MXPHGMPcxrDl8VU8nc78icU4F1zYizwy2HV0DPHYJTway1sSDQd8lzZio+Ktlx/g== +VmVrGQo2zRokW/ZuO9bN69dRIuBXxqstSMyqkjuz4hmc+/u0Pn8R0vur1PkwWxJxkdPkyDRHx/DzxwNK3aPtvw== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN65FWZiWNfabLLUKv/j/pIruOHKtdVs6BolvMynLb0NYkdIquYr81OqSexjH1hTfuKf0JPSSa1cXoTFlZCQcTvRAl3r4tOoST1NcYL/bQCxTD +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJm9y8nqv5Py/zRJVpi7pE0u +VmVrGQo2zRokW/ZuO9bN61S44hR6AKUobovVufqsNmgQEZ5BONP/3l6yR6Aq4ATaFdy5Ec/+YbAdwsytHjsjQg== +VmVrGQo2zRokW/ZuO9bN66jeJFeEUBfpMFw08/CF0hEGc1OR/ngG1FuEwe27Qxa5 +VmVrGQo2zRokW/ZuO9bN6/2tEmuR5A8x+f/v64Yc57rXiZXo/lwRfR2FWn71IN/Gwt9ZxCfxEXo2EnFJk8l4lw== +VmVrGQo2zRokW/ZuO9bN6zWtJzhMO8kW4zfjc6tVOvSTLJsrBTA+DYgjo0v95rM6 +VmVrGQo2zRokW/ZuO9bN66R2klGQ2C2715cC3kBnsTYQd1JYjglbXRxBRgwJuMoB +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN69msIuj8j7GAyYqVMi7jZzwj7ThRkDWXP/o2D0Fzd7aLnh8YvmsE2A2FmEZPcXQNJQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTB3pcp1z08CiB5hzzXu9vyM= +VmVrGQo2zRokW/ZuO9bN6+nKvAE8klUAS7rTKwqs9qpnkti3YXsvNZ6cQz30INAW +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uFpDF6Gyk2UbO0kYdOu3SN4= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+ApG5Mqf2WNrCxn3Gs2J4gO9QtfVyhZMMNOC56RS2QJkE1F3jgXr8Uz++T95hbdOqu5X6sTdgwv+5kZMylMPTuF +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gsTsx7xA9KhGk+vbrG1DfC1PhrBVd7Gpna1pDUNfyFbTqCY5hB/aQUfHzrVs571qDedA6chJ2qePaIOlsvgkWLOno5+qSBUGAvbjDACK8QbvMTFHi1x85WX6nbG5M1bs0LrLcyjlsK7yPv89RDHlAChqJslIXj3ftpKqWjjulqGiVfL3E4OGOAcswtWVnHufAk= +VFTOjet/jHfq61eirHfZIdgCtpI7t4oMY/WZBhwrqJo= +1u+XjG/2+GSQRv6EzCaWRQ== +sDtJSpHge52JmNZhrZMCGYpIcONHyTFzOT2w3fomHV7sC9WfKTKMgYDd/fLvLr/T +KZTmaJLp+FU9X93j5Tpqtw== +OesRJKYu44O4NH30ZL8V4BEPEJFmy//dPbNw4FRhznmbbpLLpb8IbIC+B/Kkz6jv +vyYQukiyWticSXOwUY8Mi6Si8vrAGYSN6Jhlp0g6zD0vA03deIc7rjRB+nBYUZwN +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +0R+h+w6Hf9rHroWFUcGug0Bg6EHLqpJlj9PtariFudE= +fdeYOeeuoWy6J7ezwxYzw4ZIY9pPIraCczrpQYJiR9mb7+DAhEV3ebytkVxaIQ23 +ojcUvW+Z2ehEJ6yMJpmY+1qbBrZXJsPhXyLqkhY6MM2eeV6AyNZSnMoRBLFCNVnT +1WBG7bZf/s5gJ+qIaMFAbF6S8FDZoLzAhhqJ2MNTCVdX0rFK8L37rJXe6iLQ+VIT +ABbY04lsTxNhdVh21a9JsdTVstQlKOYc4GlR2ph+k3k= +7KFwZuSJAC7mbgtMUMJu2zeBbC3XtaoOE8JL/ymxSKo= +Dlyx6ZDFdkn72YIxkC3bo7GMaBnPIeNZdMzUVdRqGKdC+WYea6xFLhhU85UufGTUhNvOWD2JEFPBIUptqGgTCBTTbJ2EaowPispOLjbj6Pf5uWt+rvwnHL9C4/+D95rklyWC2NrA2USx2CODo7WzXA== +VmVrGQo2zRokW/ZuO9bN60EXpD8GKK2fiY1Xg0XFbMyYwhONhKS+p6paMXgT3qX0nX6YjEUDLqRgt9iLSNuHcQ== +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJmXbFWE3Z8RNiviG8V4hH16 +VmVrGQo2zRokW/ZuO9bN656pTK3mAcJtKx4qITAmjAubpEiO6CYsc0NUVzgKF6yg +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN6/53VW+ZlBDVzVmTlHlsnW9qegPY0HL3zn+yEThRxHYemjjfRlraqfjjigIl5ztY0Q== +VmVrGQo2zRokW/ZuO9bN65wi8pbg35KHVf5hLCcaNjP6Y52g0tULksGaE6M5gmLRKi9lbuBWKL5pUOw11lRIyQ== +VmVrGQo2zRokW/ZuO9bN61w45ohnrfIHuVWBaR1GWNC4IGPG59UBAfvbyrGXbovuROgbv8sXB3LYfCsdgynLaA== +VmVrGQo2zRokW/ZuO9bN65+n3J7Q157xwH480GlAa6Glg9kTevDy3qUm8fgZXDfVy+/ld4KKcEetaksCcicCWQ== +VmVrGQo2zRokW/ZuO9bN67JeIeiYGs5UBd0XMQ9WocCkzsvFIGNYiDZf9hvLeuOZ1sVSi0InB3+1FfCCHIYgy8JxxOTZL8/cjogbKkfWXTc= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn3/nWqVEkTaZU6+sH0KaiKxOOrUCk0nTXWtEvL0pqNFg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl05UTLpRkWz5XW+5+6lTE6JjhToXs7eBWuf2pul2IyEbF5rG4luplCJxK4K+cXwO96KZBYpxSWFkz+HCmNy6zGYBI9VOVCx/WRp+TOZuLP2WXF1PUWt6BBmW7tlOiLCxg= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkmi60cDp2TxrMvVedfpglvacyvjQdToUD7W/8SB0KfBknabyryJl+w2o9ArKH35SLM= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkHCzH2qHhO/qDkepEmUpj7lb5oI/8ZPQEQscOzPkJeMhAtxd+T2VZKP8A3UfotdqI= +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6+iELpcntY7rjeay17ceU1DAjm/bhNxoMtXxcae3Elda +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DJq9FsIZwgRA0seTz5ZLwfjooyZ9lmW+fh8LaNxhSyH3dIDn7uBoRZsSyNQarnt4JOWZuFvvm+G9tPw79kxhBpeIwBnMSZVMS/10wQa5Nz3PZwf4o6EgYxFAIDuDYMlQH1MM3mHcjlSCBBzRAO3mUc +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gsTsx7xA9KhGk+vbrG1DfC1Em+Vi7/vI69YuDPCPXI7UZWkEwMfdSnw3H9JUPEP6s039CTBUnsztM0S5z+QZrRK0X7DBp/id2lXgQ4Sckkc0TOFwKo7d1VFsY6NlJd6k29m2LSKkRKj5Dw9gUql+lElj3ab5H9KmY2RsWndac3zthWAMmFnOwibXGbiSVr0PngLIFyLtohxrYRMSGU1cxvztdWDAp88mQU2xhUKoFWfgw== +O2/afRhLjcpEpE7n3oiRK+e/jZdM44aBvevq/p7Lcog= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvIvdA/BPxMMswj2KwAj0DzPJpEuJa49EPzEJPcZn930wbH3mCRcBIkwcbSSbJ2Buf/ckKrCBwetl0stqUIpxv/PgScjmiLT0rQ63qva/FBQuc7EtzX0sRoHtiTBV5bAi4A8tGF6iTh9bv/CV7GFXUaJf5r87LwMnuxG+Hf0pn7tbmJ1j60p18iOZWgJa5y3p9iaazpAtqHxJ9cyUksjilQs8eJkKaY9y63M/u4ftUReOLHQekR26YMv4TWRCVXokA69ZjHFGpxWB4G1US+urNzEUvoyK60K5wdUPbNGThdHmK/5yu734i4rtJi90FFL6WF6rXApZSljp+Me6oECUHd+5naIminskFkGbEqzsxuCqw== +l2FJPs4YkAmmok1ulDRuSA== +L0eUthVnpkGsmKFAX6d+uINvW468tRk1gDFX3gUWs5BHEstgx78O51lyIwt6GKBBESpRrUckWKsGqB23pJTF8uQCSM7KG8/5kLoTI00Fxb/XU2EYNcz9bMBO6gxbwsfE +1u+XjG/2+GSQRv6EzCaWRQ== +RbYBt1qPOjc7CqnjQqSariWidMbAf/9f2bpNNAc6jss= +KZTmaJLp+FU9X93j5Tpqtw== +n7Qm0xjL76LGt3LsjFPTfN+RjEF8lK4wZw2W6CGgGPI= +vyYQukiyWticSXOwUY8Mi6Si8vrAGYSN6Jhlp0g6zD0vA03deIc7rjRB+nBYUZwN +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +fdeYOeeuoWy6J7ezwxYzw4ZIY9pPIraCczrpQYJiR9mb7+DAhEV3ebytkVxaIQ23 +ojcUvW+Z2ehEJ6yMJpmY+1qbBrZXJsPhXyLqkhY6MM2eeV6AyNZSnMoRBLFCNVnT +1WBG7bZf/s5gJ+qIaMFAbF6S8FDZoLzAhhqJ2MNTCVdX0rFK8L37rJXe6iLQ+VIT +ABbY04lsTxNhdVh21a9Jsa5MM1huVcW3K4ep78S0YvzIBzl0Htj8tDy2gZLA89S+ +/OGQuMZnBzFhUjEyuxVI3QyDvHtQGcefzTwcHXPdSDcqeG/4oUO/xM1j30dN/366 +7KFwZuSJAC7mbgtMUMJu2zeBbC3XtaoOE8JL/ymxSKo= +Dlyx6ZDFdkn72YIxkC3bo7GMaBnPIeNZdMzUVdRqGKdC+WYea6xFLhhU85UufGTUhNvOWD2JEFPBIUptqGgTCBTTbJ2EaowPispOLjbj6Pf5uWt+rvwnHL9C4/+D95rklyWC2NrA2USx2CODo7WzXA== +VmVrGQo2zRokW/ZuO9bN60EXpD8GKK2fiY1Xg0XFbMyYwhONhKS+p6paMXgT3qX0nX6YjEUDLqRgt9iLSNuHcQ== +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJmXbFWE3Z8RNiviG8V4hH16 +VmVrGQo2zRokW/ZuO9bN656pTK3mAcJtKx4qITAmjAubpEiO6CYsc0NUVzgKF6yg +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN6yiPTEYWFyiii1z6fYyTRwSQ5ZL6P09z2N6tIvkiKIJtlGP7QJM9/vC0Mgb2xTKyzA== +VmVrGQo2zRokW/ZuO9bN6/53VW+ZlBDVzVmTlHlsnW9qegPY0HL3zn+yEThRxHYemjjfRlraqfjjigIl5ztY0Q== +VmVrGQo2zRokW/ZuO9bN6/iKpolEV5R8cPB4C0cpY6IbSgCdIcDA+feAwumGbO/xhqZzuW5iwvjZW43lLpJcRpGsf5x3gc/LoEyYAff1Uhw= +VmVrGQo2zRokW/ZuO9bN61w45ohnrfIHuVWBaR1GWNAbiZG8jVcxusqzImOwPDwesXh9MAcy+SfJqT0TOvN43A== +VmVrGQo2zRokW/ZuO9bN63QaiT3TZL1ld2XIlhhp8mFsNZtZPH34RFJK+kA1vyvF6PaxDUXc8/VIU6ejv2dGt7ZvsXzis/SA9ZJ1c0PEd5A= +VmVrGQo2zRokW/ZuO9bN63a0AyG+0Yd1AVfwXv5GgxEXbo4ulcLt1fkQsIDZCYWX3Z9Vg86swx4NgmYxAbcSaw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmeDBC+wwB7L00dSItUTzpGoOpMae46Kkr4wRAOQamej3gtJ6pC/34dwPDdn2JtxuWTliUU5G0oYJC1JjkBSyvX +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknib4F/ULGtFP9wWoFfp45lKxxdqNkeevIWoGcgcUXpRvcWv2iRU6EqmJxLnVVR0eA= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklTEsfbjnR8UzKnIvKxQFo/ +VmVrGQo2zRokW/ZuO9bN64ABOq3ND4zxH495RpxNO32j60kGFmn9giYlDubEKLPyAv6w72okw/zZdhRnx3YDTsaPP0AyDncoglEF7HYknOw= +VmVrGQo2zRokW/ZuO9bN61w45ohnrfIHuVWBaR1GWNCB/5rkvOQ1JUxCLOsb7iACAoxoWOv1PvyamMkuHECtiA== +VmVrGQo2zRokW/ZuO9bN6+ITNwUqwqpPF4QwbxHLM1Bea/LGRIU8PGwRgIxuIboEoZ1UOShIaoAEy5qqmZ4FuA== +VmVrGQo2zRokW/ZuO9bN63a0AyG+0Yd1AVfwXv5GgxEXbo4ulcLt1fkQsIDZCYWXzOEaaOaXx7rdqQJyyyVRzg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmM/9Z7SK54zPvY/da926qA8bChbyodfv/rEIOy2PE/ByXjCo8xy89KE1f+7F0p3mwv+tNgofGqpLqp0m8m2YcP64OmtoxsBfOQLx09zV6jAQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl+7Z916maMXI5sNa5j8I1rOztvvDuMDLeRh5/7FfDQKi2q0Wz6n2Mk8AgB1+W5p2wU4KRs6OXsttNzkCU/O6ly +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknib4F/ULGtFP9wWoFfp45ldJzgqemiYb1uWHn5Z4CyZAz/mxlNoz4KAOXoElHPKO8= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklTEsfbjnR8UzKnIvKxQFo/ +VmVrGQo2zRokW/ZuO9bN6ywyqAt0WUYk0wz2RaTbkeMb61E3V3fbNF4IaFu+N7qh +VmVrGQo2zRokW/ZuO9bN69mYlXMpkeP/tI+BiRsKgP/nDrnSbwXecg0sLxbrNySXN48BrsK05HP3ZcpiOF2B0w== +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6+iELpcntY7rjeay17ceU1DAjm/bhNxoMtXxcae3Elda +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+Bi8Z1FE+FAse72JdcAwLyz7N7AdgeMilAl4jFyEUL9y/9danSEfjfFUTf/EaLw6ihA9xgPI2q6p8tZwr0SCG9th6hRuGRTl5HCHN8CCp37mvvAOYzVOE6RqFtDkj2E1keSZ3DDp8w8VfhovKHylky2y0VvfSxIim82AMPNYVnf/Y3ecf8pgjXf4nTfee0sp6U= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3IcUXlek89393nCblo8Isrs4O2g78nhDNiOaiqZ4JTWhg2uFQtFA7cs1sgChJQO16mi/8h3lvnbKT6S/5yL3VpSujVeV7oQ1BVAlP+yJCxr0Bpqa5GjhYFYHLFBa1o2HO1Ux4Goq9jKl9+1e12Hz8DkJumijhNOnCgZxde/xHUVevT1IajgJWnlM7SOB0LsASMbcA0ZMJJDbDYGW4lhyPb3RYXf+MZJMtoemC7yqRWR2s= +O2/afRhLjcpEpE7n3oiRK+e/jZdM44aBvevq/p7Lcog= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvI0ChiHvIXECwh13b1ou5tGyAcZ989PvBblU5ej+BDvnI3VEWIlXkiZuznaMMdmT5p8K47ecvwSrlykEZn5WeFny2c0ti4qrLfqupJMrEqSfZQdm7BkeYEiBJ0FCE41WVNsw/WAHqnAFU9UIMcV/HWmYewZVUQsIKROXUH5Qr2+Wjq2fS1jDWPIWjs0ru1Uf6ZinM78UnM/dcwjlvf6BmEfYaaEFUP/yV980z/SZz+mB90Rv72yOFcHjxqnQMKNs2SuV7wkWIEDKjgYhRQE3wISoVBN4KKL8UObGYP6Xc2giXSgcVv4N/Sav6SIWi+DTP5yk+0/OJc3BhybDAUFDstSEeZ3g6Ne0HRmCz1Gj+UxWQ== +l2FJPs4YkAmmok1ulDRuSA== +L0eUthVnpkGsmKFAX6d+uINvW468tRk1gDFX3gUWs5BHEstgx78O51lyIwt6GKBBPCwh9svUVTinVDauyCUCzpvXvQeGO/iAQvQInO+ZySF9go1JNC0U7B5THCUdtyaeE0k59LpmHWJ8fXKRCbiBKg== +1u+XjG/2+GSQRv6EzCaWRQ== +NNi1hKZwwGwLUUv7+aXfFX0WlAgJjMF1mn0cr0V+eLJZgPm6ioMdh3wF5NBd97bZ +KZTmaJLp+FU9X93j5Tpqtw== +bbq4lairxF5x9Y/2FznYXzYyAEdIhqXtt33hBXZ7ZUI= +WuRvIBaPNRsVGEMoM4NSTre3AWcVm9DI4cyTv48g9mE= +KZTmaJLp+FU9X93j5Tpqtw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKzFnp5L8v7gjpRd9cQpcaGsEL+3gMZ3+Rt/32jPeRKibN+zEdpsIi8yNaEoNqLoj53hjcigwD4wJB5VWlIvJMPU2/HcstUaxhHgX47JfoOyrmDcVnouQB3nlqyUUZYv0= +FdiCS85SRBQfnSjMA8tkZV6oRvzqSRJD1LpQbDuFDxeH0O81t1s2G1n0XMXVJbAl55s6qHrz67OVRBVai6CnmQ== +WHkOzVx7seuLmxs5Hu+l/oKDOLwxIevEYDQQSkSHLXAyjmecsvnAyvZgm4ierwqH +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN64iCNmu0AaHyXrBNPVPWAp1ThxeVb/KD0vtV/xz0AgMQCRJTK6q5yiadoQl/y78/NZagoV8IvrhjMB4eEHoMngc= +VmVrGQo2zRokW/ZuO9bN6/buxDVz6te1BcmgUscw+/QJM0DywotxCnRiR/pXIylL +VmVrGQo2zRokW/ZuO9bN65Duud/K4k4MvHXmn2qEajZGrx7fedXEig/ku4JxVgi4 +VmVrGQo2zRokW/ZuO9bN60bFno3MqHd3T/rG2S3ig0CBVt+KXXSqwzc4kcuoC/vf0yf6wiIJozIMOf5i8CNXoA== +VmVrGQo2zRokW/ZuO9bN6+y0ZOZctjGr3gvXBvJjV+rNELb6u1MfHULHNG8PWPaW +VmVrGQo2zRokW/ZuO9bN6yjHGE0JXsLlrAxP5SVVNcL0NMqchZOgDKhkrULXUXh28+y615jF6QPTvHtvK/hzXnrcYC0dElsKGyMbLigQiVI= +VmVrGQo2zRokW/ZuO9bN6/Ykiia9pTHWG10nmQ2azo226d7dP/GWOCZv7AWa3+C7tBnYRIz4NKi885GjKIz/9w== +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN6+iHurRnEBNTF3klJjMOc+gO0ZMGRV1MNe1GkDEzOG7EbokHDAj4CUk5czh9q13yyQ== +VmVrGQo2zRokW/ZuO9bN695kdCbBp3rs6kAkTjsyuSsZoEWA5E6Gt2wvV074dODC6bbCoQ7owj8e4lpPmNCB4A== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN66wmtriv6QXeuDc39Fjfymhh1V6oDvIzFea05q1WLQ48dxmVpbbVfW5hVjLhRerYwTelQ7K+1ebii3T4OIcEnkkAOE4av29HoAr2p57LJb8SR8sBvEnH+n/pjbeSWzcfN5B5cORWnJjsqZ3Oq2En0tlZjCjlRDROXlpsOTbv/0Yx4UBLzcVjMZVmMRpnu+qwrFKUDB6hnkaLeRD0rn8jVNUZBhYnFEOWAOQBzIbOmRgXeSLQJfhUqiWvgS7Fby4liBiBCVAUu2eJus9dOc7DkS3IRXbWeuTSGY5OdPdpQDMMzfr6ZX1yPidjE5J2PUetDQ== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DMp35ZO6aoXY/WoLH+K64iyrZNRFu6p0NBUfnEATxgiNqQd82aNTyTpr9w4nTw1e+MI7JW+nfXnMQ24Ue3P7Ar +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gsTsx7xA9KhGk+vbrG1DfC1Em+Vi7/vI69YuDPCPXI7UYtEkmFhepnZKbL1TW+7aIkE2Fa6jeCR6vjETvLOLnprnY7fIGTOFdybiFsR/vAhH30D34Mg9qevkD5LCjmm49X5v0sbzzD8OAzttjX+w8ae6YeRRcmoU4SohKnYA+kB4Q== +AWid6G8ypXqQe9XxPPYEDxbTVrr/afH0tgxxR3vp12kZjXdV2hhayXt4Z3ANZRuYGTykjT9/QbnIaFTnHbK234Xm8RKJ1q0gBzf6kKc3zbvXY0O/dlJbKajVKf4Se08l +1u+XjG/2+GSQRv6EzCaWRQ== +eno9WEg2wTTzJ+gLpzzw/Dt6MMn2JHOrHDTXxQU5NQzKXNPSaPCkxJpWXmITHMkF +KZTmaJLp+FU9X93j5Tpqtw== +UkZvM94VD+N5rc+bpUuSyj930VFc/PZjT/8WTlyk7uUibm82rLjnHJ7wAPKl7iCT +vyYQukiyWticSXOwUY8Miw4iIHb6MzwycaSxDkiiwaiAUvfAc/a7+RBBFPbc7tve +KZTmaJLp+FU9X93j5Tpqtw== +FdiCS85SRBQfnSjMA8tkZbHZZT2xZv+S1BUUnmnW7OQypFh1+BoWo8VnVA+4JdUw5lxTKt/Yq3/o1vI7ddK9Kw== +gAw6jEA1xpCl2x7CdbH5N0fhpimFkvWV/09+ofK3lEE= +2rR7FCuURs31owV4VKiJMGBmLmcYRap+vwGtJtbKN/qJPNLFgtBP5E7GmPy4CTWCFZsWXTpiYPMCFSrxHp7gUQ== +CoEn0XmcJvBhYKpgK7QRAsUOx/pm360OzXz3G91DO7c= +VYbNNoCl2L/0xN1rxzqDfNaNMljNeaskgOGXkCD7BmpIHlyPxUitV/bU34NYNXJp +m5EI3+BXxkQEYKYaxORD+gqL2sR27598EwZ5hS5OCirQ3GfmLOOwsZHXlapY4KmEI4Nz1+babOgSBI3i/+6TmGjdDaoKX+8ilzuJAOBclJY= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN68zGYCiKFErhHvD5ZZAc/LfZgze3bLwYCdQ+hqSMbOSJ +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN6/buxDVz6te1BcmgUscw+/SurFNddjQ0UZMkynBUYcYRlWOccxGXy4wiecMi2gRrCQ== +VmVrGQo2zRokW/ZuO9bN65Duud/K4k4MvHXmn2qEajZGrx7fedXEig/ku4JxVgi4 +VmVrGQo2zRokW/ZuO9bN60bFno3MqHd3T/rG2S3ig0CBVt+KXXSqwzc4kcuoC/vf0yf6wiIJozIMOf5i8CNXoA== +VmVrGQo2zRokW/ZuO9bN64m//3Jz0ZiAR0S9mCkiLdNkG7nfU6RCaa9+TDQYlAOS +VmVrGQo2zRokW/ZuO9bN6yu5K/jGWYCtAGCexwcaRcOK8Hxixptm3cYErBC4rrj97iBCLVsEw9fsQ99+QRUS03KrgkC1dSobB0UXSTnqzthbpV5nqQxQ98XAc0/fW/H4OycAZ258d6jaM7iz6X+UJQ== +VmVrGQo2zRokW/ZuO9bN67YR/1PYhFzsDnaYstxq2K+czAMB7do8wzjOL1nCKk1rspQT9RsBLfw6hrfNff2FpQ== +VmVrGQo2zRokW/ZuO9bN62QpyNaGZlZhv4IbQG7908XnDM+dQtlsizeF8XBhx/7k9f+WeHjo7ZCi84RwMM3TBA== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6/MOkLqGwS8VnfX7E3oaijPEFJmGqIQCWkaO7MDul/7J +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +mrVdYSdrlMUt/i3dk4wvYPt7L71DMwMIf5lCFz5wmcU= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvI2ZIJtXma82ILlINzNlhZetsXiQ3gRQKKQIbpQDk1s6LdW1Hi++CXreQV6VXdV0s/TETX+UVVrO4bUWKetk00vP/gJ0ZdiGAjhw8vLqBLrrKC3XjIOM+Kg2l1GyWAkwVuuNT3o+LPuuVTzGwqQvj2u2EFlKX4ynpxHRCFuHbSe1keAxrLIN+gpx7gXOPWFk2BeL+DV7fjd38MLf749v/pMupa4kRdjh55XM6bDOrUxHdLNcytaKRBWyMCdV6+tF9GQ6XsH2GUfjUQPgxGHN3zFNlpeddmsp3Httq+o4Bp+vQ== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+Bi8Z1FE+FAse72JdcAwLyz7N7AdgeMilAl4jFyEUL9y8T8uhJuOqsPISjy+vfkHb5K1qNJgW5Hsmh1mSpGt/1F40AtzDdYVHJB03cN5Qrn6/H6/8VvEXu3/i8UmZi6lIExC/uUi/UnRa0lruTyFvle +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3Ir5ImnwbZflgaEXs04WUTtwWYt4EXu+bQOpWaHFMmFWXCu5yGjn0udwWDB0DQccukiu8C/U6kAE1V9ka5ZvVJye4Rfi6OHYAsCdqf3txPdW0= +AWid6G8ypXqQe9XxPPYEDxbTVrr/afH0tgxxR3vp12kZjXdV2hhayXt4Z3ANZRuYm1LgMK/Ec4Kzzh4X/6KIhhr7M5HNACrgGIQBlrPavApjR0Z25h56CbtFcJjA/x6v +1u+XjG/2+GSQRv6EzCaWRQ== +OkR9+o3X4leB2g7h1B3lJkYA4nWPGhkQYZcLBLYBzWBhuUETlC6XCwMuUKo/Qm2a +KZTmaJLp+FU9X93j5Tpqtw== +t987AUZxQR72IPRcAqvQ/tibhHz8PUmHWkn4njG5giZS80R/TFcN2/nOV1r8J8nvYZ8RCxMEMkjRnAHsR12zbA== +vyYQukiyWticSXOwUY8Miw4iIHb6MzwycaSxDkiiwaiAUvfAc/a7+RBBFPbc7tve +KZTmaJLp+FU9X93j5Tpqtw== +KgZhnCYglllfsrazbVxNluM0wwLR9C9fkh4Pz9PkxWumtUfGfHTWVnBtNg7DAsoHYQOgXz6ncqBs8DnqbgiyyLdhIxLWHNsTf9TX85io1+t0CP0KVo/0GvYMUnoLzpLU +b4OJVZe8QyIpjuTpKXDL9A== +O3CUgrw2GJfB+mDjH5+NdnceC2jE7ElwqGvUxIcW3ioHfSctkOVZd2w7nq+FgQrLyf6Tgrcdtbn8ez0vOK6b8m4yew6GoCDzKNGlRD2A0GM= +VmVrGQo2zRokW/ZuO9bN6/cAwD7FtgMVHkis4Ui84zAFkE61SWNJhtiefH7/hOOPjBzO5I2WYVxQGNiLItvNgEmqLLMm/ELgKduXss+aI3zOABy9AFY2xOClqptdlMv9 +VmVrGQo2zRokW/ZuO9bN67z7xKi7t0sis4QRWFCs8hCiS/uJ/Rl+Ues2MIWik2Yhg6JPuE/+oEOAiLw5IqEmDw== +VmVrGQo2zRokW/ZuO9bN6/yFKioIAkiiom978C458vKlCkkSuaTcDpWTlkmoI+njhiij6OvVizC2XzPIbWC1Eewk/G77IoNCtrWstqFbHqWTw6rID9GtmRuIIaZK1eFJ +VmVrGQo2zRokW/ZuO9bN6wGoHfF5haCaGx7RecrNSSqUFt3rf2NR06hTa+m4t650 +VmVrGQo2zRokW/ZuO9bN6/qFPBjAcCRKO+Q71Jok/yHRvedPc1qQTGBXupsmVZ9s14eEIq0P74SyLdV71YdqGVsocuDW/C5ZUF5UTemUsGKEPZhl/D5s+F6CY+v5tPTTR2bWJd6vm5YQ7mwK5FWGyg== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvIvdA/BPxMMswj2KwAj0DzPEJjkGl5wONj2n7h0C7z0XGS+5+7HRt1O+hmCRFHoKHpsjAkmMAECdaLGsWFcfHLmNZDybUkxxON27SiRjircb8Ptt90NZT4PSLZV6DdZerV+hVL4WQdAAAImnUlf92pQEVUPf1RdeMI4p9k09by9ukjCzPgaMuPGscSgFXJAN5l4QxftLoy+J0JbddBrKQJk +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DJq9FsIZwgRA0seTz5ZLwf3RdIiHDnlgHNwLKLC+ZJ5HCtb7i6Wj9Uhnc4GJoKW5Hw6Un3PAKmVUNrhtXbGu6D4lfXzvdo5Pc3SRuejzw/NbyjSacrlbht9vQMVkU63oYGCzO7S4JLQVfttCN6gGxcPCASuq7U/xQJcvVJt7UurajFfxNzFG9hlSFcHj9wPrs= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gv+I2RFyT79Y1QmzUWqMcCDL/GNHeFUDcNCmJXnMAHPVyFWZrjYwGSdBpX4MYsdiSPFEycEk6zFFEiJEFHbI2jY6FB6n69s8D1lfH5FHL6HY+XNLVP5dpKcKA6Poow5VvNy1PO1SAei41UmoZXro/ftL35R5lS4vC0Qz/C3eh8wKhTyB3X94e6wF6TCF+7NbfNtkevu7HmPUe2fq8qEN7hy +AWid6G8ypXqQe9XxPPYEDwu+TqeLjRNiCyKIeDxmJ+4n/guLg4KTbgvVWBA2UxP+F3daAs7BWSBRZ/SEQhqEHzBKP6qD2YfRVpYLkJCVkNvNWwUrJ5qCi4bonEMuSfaVPUxVhgvHjBo9pDaX7Ni8v2f9auriDVW+rIbbzM6NQgWSsvQ1/V17Qwi6hW4n9eSR5kgTGefBQXNC7YipJTZahBfcWUzOoqOGvOuzfKWESNQ= +1u+XjG/2+GSQRv6EzCaWRQ== +OkR9+o3X4leB2g7h1B3lJt7bvetLNL6EgiHL51ksw1bqEGmo228ga5yo3iobCwJN +KZTmaJLp+FU9X93j5Tpqtw== +t987AUZxQR72IPRcAqvQ/sY0M7/tWSmKJ6lEKTULAB8E+VMcnZguJpvOKm7smI4pP8Ot9fGmR9fg+Q/ldI1yQg== +vyYQukiyWticSXOwUY8Miw4iIHb6MzwycaSxDkiiwaiAUvfAc/a7+RBBFPbc7tve +KZTmaJLp+FU9X93j5Tpqtw== +KgZhnCYglllfsrazbVxNluM0wwLR9C9fkh4Pz9PkxWumtUfGfHTWVnBtNg7DAsoHiQXjI/rHvzhqEIyknkG/Ifr8bO3bbT1UMq8Jk4B6AwPIfmej/ppzKIXLRKYI/PIP +b4OJVZe8QyIpjuTpKXDL9A== +O3CUgrw2GJfB+mDjH5+NdnceC2jE7ElwqGvUxIcW3ioHfSctkOVZd2w7nq+FgQrL9VSD27J3Dcu41ygRndBX1xhgoRZ55s1JBrOA0mZyTtA= +VmVrGQo2zRokW/ZuO9bN6/cAwD7FtgMVHkis4Ui84zAFkE61SWNJhtiefH7/hOOPjBzO5I2WYVxQGNiLItvNgA72wsm8ydc4Us2l2g4nMBAaoMrml2MMcVNkOn7dX8CK +VmVrGQo2zRokW/ZuO9bN67z7xKi7t0sis4QRWFCs8hCiS/uJ/Rl+Ues2MIWik2Yhg6JPuE/+oEOAiLw5IqEmDw== +VmVrGQo2zRokW/ZuO9bN6/yFKioIAkiiom978C458vKlCkkSuaTcDpWTlkmoI+njhiij6OvVizC2XzPIbWC1EV4lsITGATrevYEXkFb8uxs4pARhuCnMRDW6Rraw23Ls +VmVrGQo2zRokW/ZuO9bN6wGoHfF5haCaGx7RecrNSSqUFt3rf2NR06hTa+m4t650 +VmVrGQo2zRokW/ZuO9bN6/qFPBjAcCRKO+Q71Jok/yHRvedPc1qQTGBXupsmVZ9s14eEIq0P74SyLdV71YdqGZ34+YV+eKd8BTPwwqngzU2tqaOM0mIi29h5CvJe7pNXrSKW9l44xnz+tEZsS6ec3g== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvIvdA/BPxMMswj2KwAj0DzPmk+d6hDUeK/kx243PE+f8sqNntVT5eLoUyIjVrPRtztat+3tF5b1T8ZIAQqw2jKbd6lihN8mZw1V7gcaYKPkKN5wbGX/V9n+veN5oEZ0D6xFVL3onHQKF/dH92EdkENRePgiLdfOWCUmFzMUFlZ87zr6lk+yGPTuMDg+P8LdkOx5CNhBtzjG2+o2/p9DiqJH +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/gsvOkuNyKQEB1x1K7yfn1TNiYOv6GG4D+U48JzhvRUS0bxD/zJ2fkUDse8r/E35BwQPVtqyvrjP1+QF4FN9iloGi6v7PCTzziQB5AsRUDe4UpC84dAUU4j8KO2r8tMYStidDeSbGD3EzXncoHUoPZmaMODoKUdjbdvXONXAzg8K2u/6PCPfrWatd/WgYGkEfg== +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRq+pLnKGBonJOCon5A9Iu8obfo4Ssynv14o8JKpRSe6NPCsRcl5AsiUByvWIr4wdCRPSK7QEEVzs5Fd0chvgRHU6ZcQimIsGnFqKwigy3DnwuDapzHnc6X04JApWIwQJUc2Bf08/ZcLcCdkrPPISg90/+NLvLhZKBaqmKVC/bpCADv4G/fpfkhZguwFnZ0COmw== +3zDbxVuaFCI1nhEYDQoyM1f9xGpqJQ6I+xPuSfmlBjqc+8R8rBVsAmdAcWdq4mue +AWid6G8ypXqQe9XxPPYEDwu+TqeLjRNiCyKIeDxmJ+4n/guLg4KTbgvVWBA2UxP+fBwe0nvyQ79KXDrPUBCbLAqlyVwXhlUNFFpEb0i9zx5v2cb4Cve9UVABZYxsp0yQ+qIxds/h9TXRRbu101xW8ucqsnWSnEly8Yrbg5eDRCvc1OIH0An2bLDPwXq6eY9AR+7pLowhltH9HLY/LOZ2LfcuVtjs0GOWPjgscxphDhI= +1u+XjG/2+GSQRv6EzCaWRQ== +nVECa+x0Yqtq1QnLZbnSF2l2tGr3XJm8Xdf1dyDLfWP5o0OdC0QXVDDKsyyZR7iC +KZTmaJLp+FU9X93j5Tpqtw== +MhuKaQ3W5CTX2qAwHlXPZ9AMl+XCZrfAVFW9msOF5ycLZctKA2pRtHm66BBrcqoK +vyYQukiyWticSXOwUY8Miw4iIHb6MzwycaSxDkiiwaiAUvfAc/a7+RBBFPbc7tve +KZTmaJLp+FU9X93j5Tpqtw== +FdiCS85SRBQfnSjMA8tkZaMBJpdFWavd+guuEDi4XTAEoWvJW639SgiAZFJNrBYP +WHkOzVx7seuLmxs5Hu+l/oKDOLwxIevEYDQQSkSHLXAyjmecsvnAyvZgm4ierwqH +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6/buxDVz6te1BcmgUscw+/QJM0DywotxCnRiR/pXIylL +VmVrGQo2zRokW/ZuO9bN67SBoWlR2uADcUbbqmSDS0p5wI4DxzvHXXmzRioh9YVd8vZIG89Ur675dvik11NyvA== +VmVrGQo2zRokW/ZuO9bN6/ECUQ1pEZydIMH/HOQ6ULrP4XlNr+6Dc22l4vbPzglY +VmVrGQo2zRokW/ZuO9bN6+iHurRnEBNTF3klJjMOc+gO0ZMGRV1MNe1GkDEzOG7EbokHDAj4CUk5czh9q13yyQ== +VmVrGQo2zRokW/ZuO9bN695kdCbBp3rs6kAkTjsyuSvRn4PKR7um+ls3qO0eKiW4dCIXgCkz+R5d7vbkpjl3xA== +VmVrGQo2zRokW/ZuO9bN62q9XghE2OCF4PKntiTTdFeYBDavV+DrGKV98RjWw7enVFUfL1MfqZmVc9AvSrMU6a0Nf++LAJdgStRWeVEBl68= +VmVrGQo2zRokW/ZuO9bN64+UkdN0Lokn3FF1p3mXL8nNUlW4zXUenXSS+taauSH/ +VmVrGQo2zRokW/ZuO9bN6/qFPBjAcCRKO+Q71Jok/yF9QXjEMLxZU0vpc1LZyobx4spAJCp7PacjQidkDYnNyRuyWgHy5yGVVlhTMoIrV+qTLPGUel8eu62qzDEhi8UtqQa3TYESyoC+tFaOH4HKR4toh7OXTYd7hzOe25wDtSjKbIX6formVoRnimOC1XjsKJccugDqhIbtN4yEOsB6tUm2/BuTW0Ieoq1A52unTPi45QdudDUDFM+sojsDecJVKETfM0mWkMwxGjNY73UZmQ== +VmVrGQo2zRokW/ZuO9bN6147x1dkhbDeqs2uAuxfhFmxq2/uca9NaDYGJ2BAX8sLScPslONplwtqoLEYQiXt435lYdccpEtUz0QUHTdsE4wdgNU1Z3gU/KjMGv1PSqrsTRh2GodoAkU5qGNLv9MU+Q== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN66wmtriv6QXeuDc39Fjfymhh1V6oDvIzFea05q1WLQ48sDs5DKnirpJPxnGDyxIvNUcTbGulP7M6tYTS1Bq3ia+fp6vhaSXc8lmTaDZT/r3bct6iukzQWNHyMQ79YE1J0I5uPs1xVGBqkHPkG3YVIzm8RUt0VTyugnlhhSHRXLSJvb6Kbw6GJuRxI2lrLaY1pkX/JZ7Sy2D99MBeynX5EyAgNE2NFKpBpx7Elp8jU4b1 +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+BrcUIMudTtdKnBbOiZIsdmYYc3BnIuHAW6aQ1+iO+Ya6rMo2YOjrP9G31wNRAG90uOoQjvFvgHPFxoX1Zgzty1 +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3I1HQBLdpHf/UEi3kk35v3/oRCA3fCp5MAsrwfLTMfNVpDjudu5qwB5RKwL0YOAgE4w9JM78FC966GnO2Ae/vIC0Xj0wqT12IaOoKtxwkRk/9PtUex0DAX/MMt6wf86pVOYVI83W7GiJSivRwmzmF0PomdSNFQQZD+v6I5mfo5Stcr1yhPA1XE02kY8nyaY8lh +AWid6G8ypXqQe9XxPPYEDwu+TqeLjRNiCyKIeDxmJ+4n/guLg4KTbgvVWBA2UxP+v8XGqRSyNn1ZxoEQrkOk9Blse6VyVcHv0uZie2PeTg1ZCw/FrVhBKWLVp5Vak/muNp9qI98pCAkH9KTAtXMI7wru23+EuiZX8ZVAHfNTIOZ55fE8AefcGh00bkrZR4bKZ7Ffb4KN7C5REs8msjM4ry5CLFaxF9U0TtgOen2AHZcx2HpRPyQDB1xykk3sKOJP +1u+XjG/2+GSQRv6EzCaWRQ== +xRssbjvdeiRK/kTrbUih2Uwt8ZvAZwKl0lbBn5ClukAIGp00rpYSraa6CBi28C5F +KZTmaJLp+FU9X93j5Tpqtw== +X8vlhlWnGAA1nrnZ5ZveWZdJtvjS5hFrl/a/QftKPrJ8ZBElpfR9S0lTyghWy45c +vyYQukiyWticSXOwUY8Miw4iIHb6MzwycaSxDkiiwaiAUvfAc/a7+RBBFPbc7tve +KZTmaJLp+FU9X93j5Tpqtw== +CoEn0XmcJvBhYKpgK7QRAsUOx/pm360OzXz3G91DO7c= +gAw6jEA1xpCl2x7CdbH5N0fhpimFkvWV/09+ofK3lEE= +JJZbyyrz/lRPhW4FMh1hcGuyLEPLRHRXVqorNJ2XOjwnDmS7Vz++8Bj5AU5/VIUs +DR5nxYJ6zQKKFtuQybUoKkmLYmPagy2c3k8dauTnSv5uufhzA4rdZXVfFHdo/iph +ST/USnRm3XSWOYqaR3MkqSuaJAfILc3umNmiE5Yr9mc= +nkN2edJiPuYCVVYG3gy17NM3foSxSLfpW7iDEGJfg5POb5mDQhK13/akeyzZ7/2+ +WHkOzVx7seuLmxs5Hu+l/hyHoX60ZpE0NaiFK74zP+frkLDZ94LjTFf/aXlDaMjs +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6y8Eoo36yyDXTxzlT/au5oVrbOQpyEqqlHDNx+x2s76vDrgJG0ZhN8rteYV6W9PeGQ== +VmVrGQo2zRokW/ZuO9bN65Duud/K4k4MvHXmn2qEajaT3SMsDDJFD+yYrbVmOMwi +VmVrGQo2zRokW/ZuO9bN60bFno3MqHd3T/rG2S3ig0DEBnYH1Fvoj1weGsf6mT+CGD+z4HPX0o2/0zdDBqDOnw== +VmVrGQo2zRokW/ZuO9bN6/JRZibcuPBU8/CiGNnTBLd1gAIkc167sax4FZiX/DqL +VmVrGQo2zRokW/ZuO9bN66TNhkklohQU8yXWkX1/8ve6ltv0dyEaRRtNS0DqtCgfiuY0sj0e46QmJTbTGgSYVAKGhjPIHt0/3Mii2a8wop19SJ9wtoOuz5VD7sYzYc0QCVEn1Vu6aJ4PTTuRlFepUw== +VmVrGQo2zRokW/ZuO9bN62FEf+z25aVPh3mnr1/MzeWjVU+Y8vf15zZdQF6nDzcWQUJPYD5kQKQCDhlZCoY2tWLd0Ju8WroQQJKgEsvcSI4= +VmVrGQo2zRokW/ZuO9bN6/Ykiia9pTHWG10nmQ2azo1VcMPhswdrFHN1jpqZ5/0/p/4F35gxzjwDww4PDWG8iw/Ua1JnKY3kbAPR9vfEE9s= +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6/MOkLqGwS8VnfX7E3oaijOqD6at5Fcyjj46Bdo8bAj+ +m5EI3+BXxkQEYKYaxORD+gqL2sR27598EwZ5hS5OCionjw/VUCtHeAjFt4Miidb4AZkU8kRNmZULy7bwWzTgQ4I9ZrJxfqVy7DqMaTV4k2M= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN6/buxDVz6te1BcmgUscw+/SurFNddjQ0UZMkynBUYcYRlWOccxGXy4wiecMi2gRrCQ== +VmVrGQo2zRokW/ZuO9bN65Duud/K4k4MvHXmn2qEajaM2D4s3SjB5Hr4TTgvZLCH +VmVrGQo2zRokW/ZuO9bN60bFno3MqHd3T/rG2S3ig0DEBnYH1Fvoj1weGsf6mT+C9rASYnUPL7355LK9b8fFgQ== +VmVrGQo2zRokW/ZuO9bN67wGGKFHu25hl6gpHC8tYCtH7b/7LbkWqLaeWk0nWtuG +VmVrGQo2zRokW/ZuO9bN66TNhkklohQU8yXWkX1/8ve6ltv0dyEaRRtNS0DqtCgfiuY0sj0e46QmJTbTGgSYVCZHXJfPAcVRyw/D3CdZY6R9Zx2hogX1nhG53zZnpqpGQTFZYaPP/H247ipcsajy9w== +VmVrGQo2zRokW/ZuO9bN62FEf+z25aVPh3mnr1/MzeWjVU+Y8vf15zZdQF6nDzcWPgV1DSs36UkVti8EGHpqEKawT5xR19GETdKgZj0kciM= +VmVrGQo2zRokW/ZuO9bN6/Ykiia9pTHWG10nmQ2azo0Z5RerD2mnZSPH22OB1sPWUzceVXCUx3ftu18I7o2Z4j/UN1l2oPX2/HkrGZlz3eY= +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6/MOkLqGwS8VnfX7E3oaijPEFJmGqIQCWkaO7MDul/7J +mrVdYSdrlMUt/i3dk4wvYPt7L71DMwMIf5lCFz5wmcU= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvI2ZIJtXma82ILlINzNlhZetsXiQ3gRQKKQIbpQDk1s6MK15d6K2fN7nXRH9JpS+6LJ9jT3Dws2zQ3hIhHzvSTHzyZuYoQs1/m2bhspkn9k+3hy2VIIZ0OEJnp9PdN00KqqLQMEXIlCluYD7E1iDGPsToQHgj3EdAbMxiUIDf0j3ROe0Of0WY5QoEindqrjJkSlw8nTiYOj9EhaDzKXz0HP61dBLR80YMKz00imFucXgYHgggxiFzjTTKtUNTxVXLkn05KXjzpN/1qOmZRsQ9UZxpROfVQ3nObQ9YoqdnVanA== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+Bi8Z1FE+FAse72JdcAwLyz7N7AdgeMilAl4jFyEUL9yzoKALVFbH3Pk63mBNjTaPLQkCBTlVz+Jd0gQwCgitq9nGLGuUuCLbGcWuBS773pDpQA/1DlgQH8h8qHYffcB023mRs2DxSNcclBtdNtJZLuMqUsUl9GUCBOikTLBjh8fg== +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3Ipt4cRxWBWkLTuaRegERI0v8yYOIOrlEbGVUktufL5Jb7YIFuObXcD9nhWh1kKnrlNHHa0ZZOt+8grgzMCe2lSg== +AWid6G8ypXqQe9XxPPYEDxbTVrr/afH0tgxxR3vp12kZjXdV2hhayXt4Z3ANZRuYoqtTnI43CmuVBBSyJx/ylrocToxV5Kdg1MqpFGv+3ZEZS6pKFeCKCxEPgplfwKbG0nc018FXRtPKKRSI5nQyLA== +1u+XjG/2+GSQRv6EzCaWRQ== +cJmB06UmqDKjxva/Os4rBK7oCenU2s+jLAZWOIZU63f2BVRBpk0n9UUXSDOvnWyj +KZTmaJLp+FU9X93j5Tpqtw== +t987AUZxQR72IPRcAqvQ/rreeag3Ad8GhEoJTkRKansYIGwJWg68JbCEtEi3Vp8i +vyYQukiyWticSXOwUY8Miw4iIHb6MzwycaSxDkiiwaiAUvfAc/a7+RBBFPbc7tve +KZTmaJLp+FU9X93j5Tpqtw== +b4OJVZe8QyIpjuTpKXDL9A== +O3CUgrw2GJfB+mDjH5+NdnceC2jE7ElwqGvUxIcW3io9lXrx1058xtn9b8Zy8PwI2q5hrccgRDSF+rrBtZxvEA== +VmVrGQo2zRokW/ZuO9bN62kQ47H3AOQW/v2ah7ChRq/hUBhc1YCYvdXATP3+1l3lI94+ObeQmysbvWBuFqyEAy5wirBdC4ub7rWVyjoZf1o= +VmVrGQo2zRokW/ZuO9bN67C3EO0AODwHr2XhnVyil+hJ8qzhwLSszejCosoVe9WL +VmVrGQo2zRokW/ZuO9bN6/yFKioIAkiiom978C458vKlCkkSuaTcDpWTlkmoI+njhiij6OvVizC2XzPIbWC1Edjp/BY2kye43PvmFOvtC6hq+tOcTQc/ClTZ3efZ4QB+ +VmVrGQo2zRokW/ZuO9bN6wGoHfF5haCaGx7RecrNSSqUFt3rf2NR06hTa+m4t650 +VmVrGQo2zRokW/ZuO9bN6/qFPBjAcCRKO+Q71Jok/yHRvedPc1qQTGBXupsmVZ9s118rmnOZS2ZklXHHvgkYh6hbIYuNoCrmEb1433eONkZJaAblhFS+4r6uTwWgUscH +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvJYnepdBaFtfrpFU+gSppNNbRDT0bDLvNMku8ssdOm/T6cMT14iN7gTUqTMhEjP8z1lIadC+Z+r311p/nPWwrt51a0ncxV5Y8pYIX/JI3nc5PxemeYXR0otHWJ3twto+bGawb7QD6KzsdQgYwT1a+J9VjUNqmcsYN+A/2DfJa3qaKMIJDgKLogizg+DiL2oXvk= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DMp35ZO6aoXY/WoLH+K64iJgSodJNk5FY0TIG0OqDrOKFMlV3bDTEdk6Sk0l1OyeIEdCHnlqhNfhN2c+SnE4e4+PUWnKiEGupg/xmBEI38fVKaCGg5ShHrFnzIGsqqcz0= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gtyrN3fmhNqvxN36kot5RF26vQ+BLZ61btprKN3uvvY01GiAwZhD81cs48Qtr3XbV/XxpEddkZVGzHLT/inP3jh0zcBNk4j+ZScrRGlh4dNVwT8K23F1oHM4PfVRLPiESV5tSlzohg2AmsgfcjsCD4H +AWid6G8ypXqQe9XxPPYEDwu+TqeLjRNiCyKIeDxmJ+4yRZNkxlzn9JWKePvvN7ODib/dl6KchyLFVQd1OfygWGYUmC18ELkGE1OewZAGkH7jUN171KEq4zCeovaS7znP0lwiIpHq43tBGjTO+/Kft63PBOUbkhlzXpXbcHOgvP0tUnsb7DHz1c1UayZQ9t9hZ+eNLc5Z7uHHOviT4QVekFBsWGO6dXyvtAPvyJT/g2M= +1u+XjG/2+GSQRv6EzCaWRQ== +n/F9tQKFCOBL8SSKtdjmoF0Tv0yL0K1sVJs5HcqZwpU= +KZTmaJLp+FU9X93j5Tpqtw== +CPTcjU2rzLyHvO5pcE/jijlYQy8NUAqCiCYgA9driBWF5b4QtriVvqUcWCsH9HZ4 +vyYQukiyWticSXOwUY8Miw4iIHb6MzwycaSxDkiiwaiAUvfAc/a7+RBBFPbc7tve +KZTmaJLp+FU9X93j5Tpqtw== +jn4Jp5w8pGBZmH/mrk7JdXgXtIJkc9O8w1VIYQSgAjw= +ABbY04lsTxNhdVh21a9JsdTVstQlKOYc4GlR2ph+k3k= +hikyW30JAmj5eBRHdEOqhFUIfnOQvVxMUeF19AfupMu9TCv5ouwrbo3LTSFK42kePhFyzPjYc4J1hJfTumUu++kQPyE2Vf5PLEDMFVOmPoo3r6t8lTjfk1H8TNdoQaWLIQE2P3S2W27OgArr3+YTig== +VmVrGQo2zRokW/ZuO9bN61vH5sjWffQJ/Z6LhUukRqhFUVsgnKIN1U2qntxDo0ah8KrzveVikvczGRpykv6JvQrAKL11QBS9uuhDOccBU8w= +VmVrGQo2zRokW/ZuO9bN6+GURGd6DS1JyFV64xtzrEC6cPlHGdJqfoBpNgB/sSTgcgT4U4PH2nIVtKJ6QMQ5oAd7rQ1fOctb3DNvtmXOVWcbr1qFUu2REriFoUvh1j8RTUIO+LjOPzeEPXH34iWnCA== +VmVrGQo2zRokW/ZuO9bN60DiZAwvOpVeHgZDS6zB0aBvYLXw5nHb5KOvVaZNTx5JTS6hj9OkNVdCtBdF0ioAGA== +m5EI3+BXxkQEYKYaxORD+sjWZkM336Qy6KuMov9/njc= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvOvKbZ3tEv+5Q+mTyYmHi9v32uKyu6CDih1wbEzENWW8Q== +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN68zGYCiKFErhHvD5ZZAc/LfZgze3bLwYCdQ+hqSMbOSJ +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +VmVrGQo2zRokW/ZuO9bN66q6FC3QEoB+YRZ1QHy5pxVGlfmACFQi/OJ+PP4I9cNoXXSZJmfFuEcJTQLq/ZCtU19yTz4vtrPnXVX762d9h+4= +VmVrGQo2zRokW/ZuO9bN61PJA7yxDL1KVTl01QCuQGtJW3vzQJOOgYhl5Y7o7eST +VmVrGQo2zRokW/ZuO9bN6w2ObTNAGN/v2fqKVfeJS6c= +VmVrGQo2zRokW/ZuO9bN6xf//wKDPUVWK2L+sTurgG8pByAOHY9FmNge5ivZzg0F +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6+aAtE6B7Ua6ua44h+Blx7sxVJQb5CW9peZjxWzhjxqj +mrVdYSdrlMUt/i3dk4wvYFDCIEEJMR2D5CYOFcP0t1Y= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvIvdA/BPxMMswj2KwAj0DzPteynFqBUhl8Kq1bDuRSgDa51fMeRQlv6aZPWLzFbUT8/e6KYdMeonvqmmXj1kYGh +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+D2021K6m4bs2o1hVfLElNe60aysgdIwheeBdFbreKFewz8Yrj6oafuN+wI69l1aFSUwWIV1h9WjnZOAYWfOxAU525+9Hn3fQP/7T13b2eVM1900+fb7XzdSr4D+nwHPVA= +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gsm54ihysnJ59T3r1w58GlkE0AWSJA+NwfowV84ygSuF8SxIMNMu4ntji5etztp4X0vBHWWwFWNCMhf5buob1Ps +AWid6G8ypXqQe9XxPPYEDxbTVrr/afH0tgxxR3vp12kZjXdV2hhayXt4Z3ANZRuYfhbRbVINB7LqQRnXe8IESyUoanNJmlA2D+4vzUjFZgYhphe6/FpCfcrSuNP8lVrg +1u+XjG/2+GSQRv6EzCaWRQ== +BHwLTkTRuTKcPwdIjAcfVkOik2Rc/K5Nexf+DT+LukSaHYw9Dj4sDCbc58/s1Fiq +Z8UsPk1Q7HtwjRd4g01ryw== +9JiWXiRNWDTg+XstQ0v3tPeqUlPmg6wBEj7lFSkjZd+alW6ktC8WQV0ss0QeBhrB +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNWBffLwUmKuHtFpMsDDA2O5v4M3zia2qUITwhdFgY4yggQokKnC4zVVqaK8NKI64dN4SBpt9ju2hqjDIAZj/ujfRUfaFZYSRCOULuYNpZhbqFOW9sFxf5P1jMQUbHGS/ymWMT0ei8bT5/Qdo6qRAv3KNQSGgGGvSmQltaJYyK6sLyvR5rNR+SnSLtwiKXLwqPo= +ThFx2Cr48x/QQ85XdFiDbJ0MpfhAacDjCUEeAv6461qpGtfgUo3h7kg7zIYRtLC8 ++m2Ig0EQEJ4Gc+2v8s5xrnZRBGAfDHuVg5r9VE/U7Dqcsp5mSPEG0DHHYcBZkfWo ++BDeh+Jl+ij831Dpk0PB+Z4HFq6+q1a4rNdeKuppydU= +LZmMG6wvL/RwTngCfmYObZFZwcfNS11r9ToFIpEHI5qSZA2RNbC5oYcATq4hxL+4 +gLN6phBd7ANIyGGdAIDQMbyCTw00UfS1R/3TnxM0GcHBqU9YRCImNzP8M7/QLNMx +2L0QJ15u7QVqb2rh2+Jj9OjvspJ74+mv9yVwvkZheBQ= +o6QhOIN2Sc4SHELnst17uQEjK8c+2Lhy2hqf8rBcSosctX4Bqp2L483M87OE9eXCkj/+ppL669Mf7q4I1AC6IZB6xcQgkLQzvCONBhXLvAADDiTIf4CA8j+v20CZ3Bia +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj0H4xphI80JaVwdOBqT4vpF3VmvR6RTq2YTGtk8EvQYIjitR3pAJjIGNX8AedGosB4= +32pdC9DD05OE2l0oXazDFEMJXq0ne9PGT66MpxxXKj3tninr4hwd2T2yAOK1JXtQKoyiP9cx8TICWduIpGMbbmX/j9MK2tkD+qwr7zwgCre7WU0Rtm3bCVTiRNVsJ6evOuWrKI+elR1nYHXO5o/br6SiMhiUUajUSMv4bE1I88M= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +sDtJSpHge52JmNZhrZMCGU8smhngYGnIx0FtIWFXIK9UOc0pvoHveaPk3U/EAgVK +Z8UsPk1Q7HtwjRd4g01ryw== +TxNYSU1H3DuE77lI6VVbczU2no0FFBSymt4xdgX/hdJWA2DmdWCYK6kxKEv2A+M1 +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANnKkcJSE/H3VghkioZ1tnjyunYYq8XfVeT+J18IgTe/zHEZGG5QWrYq0zwWxfhCVZWEWl8rO045J1o8hf2K/ZnB87MjLagLhDLLXO9zmkwzP +fdeYOeeuoWy6J7ezwxYzw4ZIY9pPIraCczrpQYJiR9mb7+DAhEV3ebytkVxaIQ23 +/OGQuMZnBzFhUjEyuxVI3ZnOu9toaGbEBmEpZpK4MC2T4SSUWnvQ4S1vyipoqwRb +ojcUvW+Z2ehEJ6yMJpmY+1qbBrZXJsPhXyLqkhY6MM2eeV6AyNZSnMoRBLFCNVnT +1WBG7bZf/s5gJ+qIaMFAbF6S8FDZoLzAhhqJ2MNTCVdX0rFK8L37rJXe6iLQ+VIT +ABbY04lsTxNhdVh21a9JsdTVstQlKOYc4GlR2ph+k3k= +lzSgbK+CTpY6T87LqdbU/clHi3bu6SNcJfqLXtam1ikWDppG2m46c0kV81wr46VR9gD170FUCMnwcOAwRf7brsv3F0ZWF8CxpJqJ31n3ShkANwE580geiRO02zBhyMPBr5dy1w0DLVL600RnWY38Ew== +7KFwZuSJAC7mbgtMUMJu2zeBbC3XtaoOE8JL/ymxSKo= +Dlyx6ZDFdkn72YIxkC3bo7GMaBnPIeNZdMzUVdRqGKdC+WYea6xFLhhU85UufGTUhNvOWD2JEFPBIUptqGgTCBTTbJ2EaowPispOLjbj6Pf5uWt+rvwnHL9C4/+D95rklyWC2NrA2USx2CODo7WzXA== +VmVrGQo2zRokW/ZuO9bN60EXpD8GKK2fiY1Xg0XFbMyYwhONhKS+p6paMXgT3qX0nX6YjEUDLqRgt9iLSNuHcQ== +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJmXbFWE3Z8RNiviG8V4hH16 +VmVrGQo2zRokW/ZuO9bN656pTK3mAcJtKx4qITAmjAubpEiO6CYsc0NUVzgKF6yg +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN6/53VW+ZlBDVzVmTlHlsnW9qegPY0HL3zn+yEThRxHYemjjfRlraqfjjigIl5ztY0Q== +VmVrGQo2zRokW/ZuO9bN61w45ohnrfIHuVWBaR1GWNCnJgiclV34O09qlernj8Ngf6EPDG6ycXjoJqX/O/rUAnXGfm/Ym0q6fn3eJ8nDZ9Tkbz08C0hO8cdYYeNZR2RT +VmVrGQo2zRokW/ZuO9bN6x0Gk75J1/BaQz5SHiFE7LwusXV3DXYl/kmqwGfdbXxhyjblUNvdDWBZ7Y3+15CcnPKmWcZhqIO1IDh0Hx5VgLVb0OsMYQOPFoPFpckxPmp4IWB7WGAgQzl0dyS2Ed7jEw== +VmVrGQo2zRokW/ZuO9bN66gdKC4XWAPTc0u6UqkBUwXMTb+xl65yl85BJEDDPLGV42bLC/xBHO//T/VE02D0IA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hklp363Bu8rdFozhQMZEcyJuoa6y0cF6ek/6sCDR56ZgU0ov7DD10tY1dsdlxz2uV8EdRT998sybm5qC0dXNJab9 +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkfFqOUpBuFDDedffQiweXDdQB51lYJTB6cDjIMCMAIMqWOvERgha3FSBeCQfNsH64= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknqHMtA0p81h7WgPWuZvjLxGWgU49/Fzn0oKjOapfQvQg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl/MJBWJAVmoJGxsDL68ycvUqeI+YX6IGWXpVF22fMLHYMJRaQNEb53dzwEXO7VDCA= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklMyQpx4RA3yWj2gHs3GC4vIyqjDRL3KM2k0B1E0sZxGg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkP1XBTxxQoteTxOSKZlOU1xCch6VJoD/wVOttkUfGtZZrzrOJ2CTHlETW6IXIYi0eYqYCOgLXPKQc1q+IprgTZAtukeGTMV7QE+GJ1mi7ItQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkna8O36iQ0f6+VQ9jUD+CypNUm59qefVh1Q5vRIL6VHzaitUehF2TwSMFU2620My6XgR2Hh+KLxbKXFnTvM9XTw9YIaoMyazG0SmZ8aHIP3xnKpMRfHm1HDvKI9k9aY780= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkkc2m8aZr4thivH/Ab3oA/a +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkmdt5dOdCvjZNhOajiM06zyI8F1z6cBCD/13fp5iaIUjwl9xxsQL1Hat8hK9yzEQhRSssNZvkdLVkAj/oVNnPPegZ3G4SG3u3mt+NnA0QdjeA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknqHmN/WlP3X2MHW/MQN0MSBCwwPsPQ4SSwOYFC6Zl0pBilRqFplQP1lFInZHBmYbY= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkmnus2k6YYE0gU7vts7In+q4D69qS4CoEAQsjosGcUieZmtjpnG2kPzCSXy9SQyOK4= +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6+iELpcntY7rjeay17ceU1B98gClccl8gUonh8ZDKaHe1HtcfxRP9urvpol+KcbQCg== +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +mrVdYSdrlMUt/i3dk4wvYFDCIEEJMR2D5CYOFcP0t1Y= +L0eUthVnpkGsmKFAX6d+uE/CYsJNqOMjxonFB46aPvI2ZIJtXma82ILlINzNlhZe1ST9fmewhy9KXv5tOsXOUjPk1VN2suqdjbPMMI98LX65RJWDLHLQj5mVoBttiC7YkZt5W1l5TdVyq3TDsza2yA8w1NwsbJ+Ot9VMl0ovN/yp15X3s2uB27IPzOdgEXSaveH5lr0wrLVJXdtbVKBYbzvqsf9xKXQ9rw3D7ofi22PVkI/9qREszUVghc9nlpQOA6mTwq9ydiK9PjudV0xISCbRjxb+BgOwIM5ZLe8E1OZZvC4KeHShrMgqENlPBMWLLN4EFcMzjWmf+pEefYkveA== +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DV0qk9bj7sgF/XbcR6HNF0PiIwxkkZrBn4EvPXRpixrjusCFcxWk4YLbs9fkPbJCfWm39gvlyvUtZawoxhllMj9Xarua5W2M4YPFqxLtjnIeqaxLEE3DmkcMomNhXopwVO97i1xtDtCOGOcGBfFFrf +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gsTsx7xA9KhGk+vbrG1DfC1PhrBVd7Gpna1pDUNfyFbTurtAWA/h1CW6lcZlBLfBYaVVq+1swaK7TIue8EHKDcf3mUiQwLL1G2q6tpxj/J/y+bv0oGFnFKjesSbmlm7kcQBkeytFhNsup8HGn15QhEVsre2FZALBJjQS+tiMIyTHtOdlA3VrkKkskPv51l5F5IgRBAZfaEgYTrGHM2OSW6e +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +j+SwqpR+F2lYRZXNJryLSfytyH8xp6DD2Ek1E//Dlq4= +Z8UsPk1Q7HtwjRd4g01ryw== +FJoH8ANT+vaNanqqjLNUASlBlTfMmvJKMlUIANe/xUS2YR7YnTt49y+tUZRjmDTy +vyYQukiyWticSXOwUY8Mi9HOMnnB1n92sYcB0OnLxm1bO4npQEN0XyKOb+Ol/RKB +Z8UsPk1Q7HtwjRd4g01ryw== +td0Hee4NJPblA28GiIcOmc/Ym6Bu3ogIAYm5c/oogwk0XBSzLRar0Ih7afhnqigD +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/j9YDYTBW7wzXOYwIgWpVKbM/qZEO9IUBt+yeznHTI43/fXnwevohkxSPhjjCK6wHqaB//FH6qUR6SJKMWA35iXTIp4jTxg7HPrg+nGOAj04yFsP5MvH3XSBm7FLwf7MvS8ROXzpuIA+qX0XLVxcKrNAUf0xbbW+xr8o6lS0wIjc +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRrzaJ3fStGEkJ1TrpFS03bbdUIiVBbfc+ifRnD8hoj5i/vGHJwbslvMaI07I6UZqlSe4f3Af9BMCsD66aZlfXP6z0uy27fEfDlLHFmfF/xMg +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXoz0tnByKjXrpn0PixC53xxW424JtFQiPFy3Sr4iAaZyHbxdg7T+nBoNbXsIKW0qy5z7tFq9bNw3QQLZMfinM5lOIiROL/n6xbmdNH/l+s9yhWf3NmfcHw1Rs7YD44yQUlCuWIay2lcvI1qON9rks/ +wgR07xfoapmx6eEnFHXXYsXheOqb+ac7ivrKomECGJjgpziToag3V5fNXXvZF2VqJzdP2yN9TApu5LJMNhW/Bphlhm7/zg8uo3tW5D2Oe/deZXzoOQx/WMoQDK1USmSn +WHkOzVx7seuLmxs5Hu+l/vewF5INruHu21pbeP57J1zziU9fiig3+az234q1W7m2 +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +UbJuac0dxLiN+5ocS3w2vY6ev/aUUjOLImcKs1TVtH7uEx9dvbj/NPEHyUCaV7obCsYW06pB01JKt2QDWmY/u+Vi8KfUkoDI96zKFszF2vc= +VmVrGQo2zRokW/ZuO9bN63QrZyqUb5BKoT4N9cyeKvwVDGzKQdsnMEcG985fFOqcgHsu+uLtgt7Vq8Wk3JHDXDpm1QlW8uY/EhfiY2ZD55vGyoD6HUoMDMPNAuGx2EeN +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6ywsF/deIZTFh2C8FEj6CP6YghSMMH++16olpAkUr8aH61SNeNj92Dha8MaRKdYrjw== +xWoGNWjKGPfI4gq8aHoTfOuj52m++iET96O/j9y6xVlGm4+LbAACgyp9oG8Hv7OC2+YhWhhRxM7XkLUnZ8CjRA== +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +gZ7BoIAABc+I8g/rYzq+fxNPIG9Bz2s8T7kg5H+enXflQyLGwsFGAoEO/fc2Ik1Dmi+JF5ZO1umLWtK9kK01SA== +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqGyyDPCcCSOlRV5etylc9M5 +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VX19S4Vbf3FE8SyCSP2reHhg7sd0ER/UUOD9fjemeCn7d8eagdZzgkRi3pUJOoynXbu6ZEhByY0iftuMMH2Ze8I +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +eWkbU128GVQ8HNvAeSSgRCkOh+s4OU/QYOa8I/Ismus= +Z8UsPk1Q7HtwjRd4g01ryw== +pWDI3suimaW/fzzd89s5Xbvno7XAD4tQJ3S6aVXwVoYuocfGBi0UxPXNL7vswEjX +vyYQukiyWticSXOwUY8Mi9HOMnnB1n92sYcB0OnLxm1bO4npQEN0XyKOb+Ol/RKB +Z8UsPk1Q7HtwjRd4g01ryw== +td0Hee4NJPblA28GiIcOmVz/iEnSh7ccFLM88eucCwhDxWpOIeDaytxmPiP2pwkpNyGbS3x73U5/Z+SQSDEikA== +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +vdBxA0rg+ZMSoZiCzvqi/gqIiVZTQcFAiLmPoNai8xutYA0dS83vWxSWdRLODeDp4q4QeK7kwtUtEti2vAkZRSQA4dw/W0RTHFPO851VdCTY0ktN4EQtfhMkQ+a30AGvrl1Vaagw/r25jo3ENPWu+eXus/Jo0iW8we/7jbdwBQjB9zp3h7LesrbSjhjlsmwanUy6Xyo7srjQHGgZvJG7pQ== +898mfgNS2d01I2C0OohgeqJkYuBJuZv3XRwykfNYg/I= +oS2TjyjVloijdu/67u+fRjo8xBq+dALUaqMR32a7+KRtMnzAWvGG9TrwG/jaGJP4P7qHeX27kwpQafD9eT9biE/pqVeLldo5PWIL2at0SiCLXdDB0neLpLXz6VzBcXwznkGLqkUWTDloI16oOqAXLA== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKMlEIqjWm8+kOtLiMZRGDzJmH9PGjMR4p8yi4oAXfNcgEl2vZxxgv8NVYMmLnl+v42UgdEFHJPZcltMDkGeY+xBlp0cPQhJMkm+IxLo4M+2i+Am1Vz6IcVdBmY4Os14rFxvOJaPGWGxV8MBT6BzDyjHm/l0LUbD6rUFkipEhoTA== +WHkOzVx7seuLmxs5Hu+l/vewF5INruHu21pbeP57J1zziU9fiig3+az234q1W7m2 +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +UbJuac0dxLiN+5ocS3w2vY6ev/aUUjOLImcKs1TVtH5T76Zz5yb1C9U5rakgdm9RW5nHdQvRPYO143hdvo+HIA== +VmVrGQo2zRokW/ZuO9bN63QrZyqUb5BKoT4N9cyeKvzykZRK7pHPkeDFgAlLwcFk4kSum59DnV9kJdKHpC/A2ISh5PNPzp/qpyaJdATbMlTIoYou1YK2/ZSLGwPFlAXWkM6aQwZSO0FxOI6JoW9q+Q== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6ywsF/deIZTFh2C8FEj6CP7DuKwnsBG8hBHPbBdcBfSCHxeDBAbFdt+XiARKA1MomU6qtvykBPVi6KHdqBDCPxE= +xWoGNWjKGPfI4gq8aHoTfOuj52m++iET96O/j9y6xVlGm4+LbAACgyp9oG8Hv7OC2+YhWhhRxM7XkLUnZ8CjRA== +NZ8anjNE8y4KXkXCdAa3W8kH6DmFwpSFX5BZjT1uwtrxI0WPyIknAqss64fcQgDb +gZ7BoIAABc+I8g/rYzq+fxRpaih0LRniUlkMHsVtxMEnYoFJPfaZbR5kMXorUXCehBDH0psjaJSqMceG+oUtwOBbbRcs3Z7Su59BVe/h8ic= +VmVrGQo2zRokW/ZuO9bN67ce+2hN0tsNBlpK2BZSXqGyyDPCcCSOlRV5etylc9M5 +VmVrGQo2zRokW/ZuO9bN61Ms/KjQeK69A0jSbIx+0VX5fR/aAYlzEz6OTgMCNWMwdkboe2dfTAhdyyWmB2y67cYtcOlXL8H85aKqsoB2DoTGQLb8N1LojshwROENSJaoEBPipFiXAHubq9mik6A0Pw== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +0U7VJZXCdgOpQNU62wPJQE/UePejZ9QzjtvZYbqon2oN3mK8me36GGGZ2OPkTBxp +Z8UsPk1Q7HtwjRd4g01ryw== +DzneBA92dU5V88ETMYFQ58NxLyhmUEIVlWZDWxrt0Mn/UQsW65SPLeHjo6VlpkgVOX+07UHpql5UeKozmcS33Q== +Z8UsPk1Q7HtwjRd4g01ryw== +/0ULpLqgTvInFD0r5hHANlHeJWQw9fSF1K6yNPR7TNXKMlEIqjWm8+kOtLiMZRGDexhZBoIAf0WAPIsI2Bi69+daBHJk0QBFqm9azTOEwXyl/TjY+SrKdfHV4yX7UqnL+bHOT+A2D8kciW14Jx2V75S7+9oeo2B0w2fsEhD7pcWuZSYtzH87yNjLnwXZWshYz3lL++0DW4SqFI6j5RIwA2h6+bUUPK3Wl9iZ5cwx72Q= +UGqQTbo+FQ89dDBUZtFrgwmFMzzlmsFtvZg4+pH6Qs79Gq+C4Ck/Usx9f8JkpPA2 +WHkOzVx7seuLmxs5Hu+l/qqTd4Qb9qnLQgtLGl5Gi+JbwOQ4t8GjAIXgCmAhab1k +k1ktu/l6HFULD7Vyr8Cv0AK1TDWGg/XdXjBE31fdyPB2B5OmiMxgO7bZeua7BoWU +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN68GhBqRTSx7b66q1h0SPjamiShttYZYtB/bL+x1+9gBL6a/8TtD3hX3wysZxq0l0VDucqm9mgSrBadqMkbFS5y0= +VmVrGQo2zRokW/ZuO9bN6510v1jY/Sjqk6PHjy+iz+IM5pMt0wEdPwZx+4gi9VAt +VmVrGQo2zRokW/ZuO9bN6yVOCMsCJ4iLcnswVEG9lew= +VmVrGQo2zRokW/ZuO9bN6936UGmgfBI5SpeqACm5kwysJAm/xNZ7cKcs6XkJehlaiV6VnhN/O98kVrhggzYeM4knX4jr6QJIksrEqAaEzXE= +VmVrGQo2zRokW/ZuO9bN6+6OSQjJyDHPNRB3fSq3EOchq4P2ddcxWXhO1zetOnKmyIOdqGQsUFDo6VZggORDnA== +VmVrGQo2zRokW/ZuO9bN6ylkoE1zhjzQNj2TzKNcV51RpDvRchETaM43HO/RmjbjRZuV9JQk5rUXcO1NwMJ/nXK+hbrNfPDcYhgprqsmpJGW0Vbu3wJ5obGDwzEggmv8HvHB80kIfzBSaJ671gcVAA== +VmVrGQo2zRokW/ZuO9bN68hgSH6aExFBRtC3BQoB9fGNY/KuuyVJ8rT89YRZBLSE +VmVrGQo2zRokW/ZuO9bN696q5lrt6GPZtHqhTnRu4kfipTGQ62kylytalQ/LzQMAXLd1F7jC0FKKozy1USi+UHzqBRctxqLBWMTHN3AYBvCDMmil2VRD0DGA6U9C/VZRzgC0mZHM8LzZSb0MCGnWHSzc166EOAF0sFLBk9SxELv4KyBcqOdZ+TF9e/YGdYNgRD9EQypEmsGh7hCgnzWXdzLcMul9JSmpLmPz2lAOT9g= +VmVrGQo2zRokW/ZuO9bN68hgSH6aExFBRtC3BQoB9fHf+4oDrdgnlf7iqtuRvlzDqBGJEACuxZTqr7OlaB4j2z5xgdoCjEbtO3uP9fbhkhp+pxzUj3h6Askvq1HF23eVt2uhxVReLZq0b/FUl76NxuGMeVJCEsnyKrRBwDGQqruB8nxdjlPJuw0iW1MFRhGGNG7XB0GhT720aJEPKMBwrQ== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN66Px6Idtf+WhzwY1S1dxdnQ= +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +Nhb1Brnfw2g37hqPO7sHSRwdew3jTXEfOCUuPk1m52o= +Z8UsPk1Q7HtwjRd4g01ryw== +TxNYSU1H3DuE77lI6VVbc5W9rK3Rhspm2qS129+sVQzX88IbkgKkv+nhn3s+6WweAcBpFTFLR1nYXwRqzCFAOw== +Z8UsPk1Q7HtwjRd4g01ryw== +UGqQTbo+FQ89dDBUZtFrg2BLWJrWwmrMQcEReM1++ABiGJdvEdJy4p2e72NjUhX6 +jQ3ffgdk5F3qci4zFBKoIcXBKAyBSxkzXHyAK98DGV+aih2DPKuqnX9CsRZR0w3W +MYV814tIGmmxARDMkm8xwje3+pWdIcAMtkpxX1J7i4iRTzdcgMEmmoGsIJGDeOu6FV/JBjJ8wBzBgw5aOXgVvA== +tv/17GGxFXcthQ3Mn9JkloLRwsg6BziAmBUzA5lKZPilZkThOpdX7kCVsjuEEe0C +BFN6J8z7zeK+NmgEVVl/O5WC+l21AQTyKJG7WJUGT4k= +Xntf5tKQoxjFTkKgcujQGT4lt8J/gJMuatMbMqnL6Wj4txU7aSJrOs92uZUakKQrktGDQUuHhVDx043KfLncaA== +JhRCGz84Brc1np71ddZ7rk1JYormFFESzmIiY1mrggzRpIGqz2ZSvMC+XRbvcpF4 +1V2v8QerKOmubvSxgB4eTPPa7eui7zDRItA19Ag3PR0= +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN65rD4nI0MfwzlsBII0cdPaYdlqqTXeiar/5F20QT5k8d6zPM46scMv8VX2QdB/T36tWZNkSDIucLfsd2OjaWjXA/k5p9yZTjMJaZfyIOeArHL47/iXF3gwhcjm5Rlkzrtw== +VmVrGQo2zRokW/ZuO9bN68hgSH6aExFBRtC3BQoB9fGNY/KuuyVJ8rT89YRZBLSE +VmVrGQo2zRokW/ZuO9bN696q5lrt6GPZtHqhTnRu4kfipTGQ62kylytalQ/LzQMAXLd1F7jC0FKKozy1USi+UHzqBRctxqLBWMTHN3AYBvCDMmil2VRD0DGA6U9C/VZRzgC0mZHM8LzZSb0MCGnWHSzc166EOAF0sFLBk9SxELv4KyBcqOdZ+TF9e/YGdYNgRD9EQypEmsGh7hCgnzWXdzLcMul9JSmpLmPz2lAOT9g= +VmVrGQo2zRokW/ZuO9bN68hgSH6aExFBRtC3BQoB9fHf+4oDrdgnlf7iqtuRvlzDqBGJEACuxZTqr7OlaB4j2z5xgdoCjEbtO3uP9fbhkhp+pxzUj3h6Askvq1HF23eVt2uhxVReLZq0b/FUl76NxuGMeVJCEsnyKrRBwDGQqruL4teiu1UyHCzigYMQ7Wxh +VmVrGQo2zRokW/ZuO9bN6/xDysL+wsJSb13AswERz4dbP32z4nnYwOYj0CDjnhOh +VmVrGQo2zRokW/ZuO9bN6147x1dkhbDeqs2uAuxfhFmqV1MUQe7ntuqK6FUrhRdWrhb6PFzNeMbW752CHp/xOcV4lOx/+l7F8C+hsS/7fGJa4xAb+A4A56JkrCbngLOIkjPfh6O86JCi1rPBFnmHVDEhWfPEMFUWdqib4+0YMKx3uV35+1cRpK5JKIXpL0HGwCZ4eBIzv1fSWDVgotUWepFA6NwKW0m+RKNVHjVf4B/Z6ThMx9gWCeYkK3qOPCvw +AWid6G8ypXqQe9XxPPYEDxbTVrr/afH0tgxxR3vp12kZjXdV2hhayXt4Z3ANZRuYyQUpOWBMl0q+ODdH4cmNOX5wCNLlqGkH68vGj3DMWZh7ID3mLi0fFs/xEt/mD6eM +1u+XjG/2+GSQRv6EzCaWRQ== +mT9/9GDeyDquZiN6X8gEAWVfQh1dKbyQd9AnYmwsV+qKs48j1ia2S84MCdV0XhVm +Z8UsPk1Q7HtwjRd4g01ryw== +btFyzMGgdk+J9spCzbDmLrPmdjo2LkuGg5/zyBJtrrkS96BNBVO4uQFj93pZtz62 +vyYQukiyWticSXOwUY8Mixyz/Y61a4ulbPhcSy36UUHNE5uCHFyd8WxpyS6PjoRT +Z8UsPk1Q7HtwjRd4g01ryw== +fdeYOeeuoWy6J7ezwxYzw4ZIY9pPIraCczrpQYJiR9mb7+DAhEV3ebytkVxaIQ23 +ojcUvW+Z2ehEJ6yMJpmY+1qbBrZXJsPhXyLqkhY6MM2eeV6AyNZSnMoRBLFCNVnT +1WBG7bZf/s5gJ+qIaMFAbF6S8FDZoLzAhhqJ2MNTCVdX0rFK8L37rJXe6iLQ+VIT +BCHJZOAnQzSQuLH4cAWqAD40vaEYChOYgPF83uywbL8= +7KFwZuSJAC7mbgtMUMJu2zeBbC3XtaoOE8JL/ymxSKo= +Dlyx6ZDFdkn72YIxkC3bo7GMaBnPIeNZdMzUVdRqGKdC+WYea6xFLhhU85UufGTUhNvOWD2JEFPBIUptqGgTCBTTbJ2EaowPispOLjbj6Pf5uWt+rvwnHL9C4/+D95rklyWC2NrA2USx2CODo7WzXA== +VmVrGQo2zRokW/ZuO9bN60EXpD8GKK2fiY1Xg0XFbMyYwhONhKS+p6paMXgT3qX0nX6YjEUDLqRgt9iLSNuHcQ== +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJmXbFWE3Z8RNiviG8V4hH16 +VmVrGQo2zRokW/ZuO9bN656pTK3mAcJtKx4qITAmjAubpEiO6CYsc0NUVzgKF6yg +VmVrGQo2zRokW/ZuO9bN62HWtxZ2I/oCwtyWP0WSTIg= +VmVrGQo2zRokW/ZuO9bN6/53VW+ZlBDVzVmTlHlsnW9qegPY0HL3zn+yEThRxHYemjjfRlraqfjjigIl5ztY0Q== +VmVrGQo2zRokW/ZuO9bN6wJWVgTmJ2EgLSyZyUD8GJvuHeaf0spXYmylV4qXy9bv +VmVrGQo2zRokW/ZuO9bN61w45ohnrfIHuVWBaR1GWNAm4hIL0YdCOkxxghBXsrDNVZLtGLIXEmOsSIG7qSONcIKf9yowJaUr7bX1jpVx/9dYB5vu5CZT7elU293Se6+e +VmVrGQo2zRokW/ZuO9bN65Ce/QOFjiSIqWrjA3Er0R7cq13Ywzo6BVLzfKd5i4T9KWnqpI+rdk5rD52UxDTMe5ESZtIhT80HF0t9dvklhFYRivipKbc4izxkqOF7nD1aq86RpWIgdXHLInyT/G9TdKHEmByuJozqgV7i0liSu2E= +VmVrGQo2zRokW/ZuO9bN67K98zu3r3M0RkF92kvh12XnD9tV/+RgwuvbKkE4b5FdDl218u7Gd5XpjU37yihT/++A/5ESJcYgfipI+1y2djg= +VmVrGQo2zRokW/ZuO9bN67T1YHfouoH37HxIGfwtIV53hsmDtjo2IqQwFmkRMIWW7EyF0LvAQ0ClcioTFbLI0Q== +VmVrGQo2zRokW/ZuO9bN63LTkM1jyAeOymc1jas3GbfWbaFV6asNTxE8gO4sa1C2 +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmU2PP8T4rE0Ki2DpDYGQQTMBQPeP+VDqiMKBG8WE9YhabFJOyPwvzPtBeNAqp+F6w3uhUvQ27nyMB9ItJOAwHO4cp38z/RclIqa42VDd+bqQTkuxT/++1Z+7ibbkn/8IFPAPtKXXiaaBLLv1Vt4Sk8CbDkTCDEWKsNtB93KN1PLyZOF/f3opKHyi9/Bvrf8gLoVOW5yEYJLLYRI4BMc1viPO2lPYBlyCPWmCyItaqP3w== +VmVrGQo2zRokW/ZuO9bN6zTwEIRXNDisRd1QRhA8x1E= +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +898mfgNS2d01I2C0OohgeruAe/UCLgGD6NNdg7XFY+DJq9FsIZwgRA0seTz5ZLwfjooyZ9lmW+fh8LaNxhSyH3yppJ68a4s3H54tsSOpWoZ2m6PWB40Ky92OraEDselU2Qk3ji/D/iY4v+bIOMW+2A7SpeMS4h3h8qlYXo0JWbtgm9ezxrehbVsJ0liFX1gh +898mfgNS2d01I2C0OohgemSehcduuEOb/A/jSuv27gvK+dzc/IUFREK2vrqEjL3IcUXlek89393nCblo8Isrsx0C7qa9fJ7b2Xxsl2RtDAJplaSUPejh9zSGeZWWQWbiHBhEOP8J9idbfwP5l9UbdsbJ6a7CtrHEFTvVZUENLl9FoGcin9oi7/ZxDTQWSN4mf8qodCt5Lng/4uh8oghzsZ2rWteQPiz0XJDtWCCEmaw= +AWid6G8ypXqQe9XxPPYEDxbTVrr/afH0tgxxR3vp12kZjXdV2hhayXt4Z3ANZRuYgbX4QKMnToJ3T90Hv966oI9IH50UIaYKlBaOTM+olPbmcW/SjwYVBBC4/OQfpx1W +1u+XjG/2+GSQRv6EzCaWRQ== +nXT3H2N0DWm/wen4hKeMsWmii+E32LKTZLTl/eBH0rc= +QdTjTxbShl+8mrARISGoAe20TT841Yg39a2OpGi6Sb8= +KZTmaJLp+FU9X93j5Tpqtw== +6cOGAVw2kriw5n/nELD3EzDieqNF+nFwRUrV3liflgFtretISs87Q7zY7vAjAZsi +Ys4bozvJBgtB7q16qcvtB02lyBPPkFWxHdwFABPScSc= +KZTmaJLp+FU9X93j5Tpqtw== +cUWcz3lcQ6qo3eAgjfy6hcBWsrIOiJOuPw9tmudt9no= +WHkOzVx7seuLmxs5Hu+l/jlzY3WAQ5RZScd8PnXAehWqEELjLynZVT3LobYIZ/w/30Q6418opECWCKddX40xNg== +o6QhOIN2Sc4SHELnst17uWIkkNEHVSV9/Mf3IwuQT5oWHGgnzfKSw5gD7uL0PrYiWJWeHrDWof1TpozUKVXCXw== +PWKGbNEPxxxpGpe8THO249G7n1OzHa4qu7TqsAFL1ussgdlPfAKMdupfQKMuwnR8s7m3gXBwkyoEl6H2pcQF4Q== +VmVrGQo2zRokW/ZuO9bN69CLEnLN/3TzL7iVitOx0qOanWOL3ccZQGpL5b45AlgM +F9Sftf1PoN5P+lgvc+r10Cd2oRkiWUAJQpp2kvYSUOKRyoMCdwcp9b65maauRWu7QcAuEovJwCqZKKX6ehkIZxvzRsa+MKUYj2szuzk8Kz90R7acxowMdh+wDztnyA5L +VmVrGQo2zRokW/ZuO9bN69CLEnLN/3TzL7iVitOx0qOnBkFJIyNLmtrTlZAVUyNY +F9Sftf1PoN5P+lgvc+r10GMXgUldLBRR33Dadi8u6OA8bUTVO2N7S+6ANZzHThORqV1/TdlcekIu+FMlbuYcsf6saBggftYMn1qrCHI00ss= +VmVrGQo2zRokW/ZuO9bN65Z/+3Ip5etct37v7j19XWoJfRDvWc0/MW3gn6OwUnX+ +F9Sftf1PoN5P+lgvc+r10GMXgUldLBRR33Dadi8u6OA8bUTVO2N7S+6ANZzHThORQOa5dwCApnFuLSiohDrK1/EpVeb/GgZDYWIB0Uicqyu2cjFmNxQa8wpUMgPEMjlC +VmVrGQo2zRokW/ZuO9bN65Z/+3Ip5etct37v7j19XWpv0JGsd66pHsu8hfkT/w13 +SAfJlmeeNJ+zov8ENFGT09M8rmWLHAFAE9hX9AZnIaeRvIdH4lb4Dv/EphznOIG0p+BtyTEt52gBeN0XDtnnYA== +PWKGbNEPxxxpGpe8THO249UOX20uJsE/HUiON5To0V2M5OkhyqQZnLyfsaPnuR+ElxVkbZWLHeByQrSysFw3qHnC7iVp9kxao5KnEJm2Cbo= +VmVrGQo2zRokW/ZuO9bN6739lKjFbXcMNJNlnotcdtZVgaenRLW4xOhLXGNCESYw +F9Sftf1PoN5P+lgvc+r10KLYM0ScSe6shiqZtH8DkCVUWXTUNLs5bvA0vNjn5jSZF+EsetsUvn8ldRytIuidaaNtYIo2VbuEkFWUiW4qe5o= +VmVrGQo2zRokW/ZuO9bN6739lKjFbXcMNJNlnotcdtY75pP4x2cTIQwbiiOwVG8e +F9Sftf1PoN5P+lgvc+r10MDkY9Zzcv6r6mBg6yHiWa0jq7Lr02xbnTUCpb4C3i3GRv5DwdGIkGtj9V9QfyK3JIbq4WDzOvoRNvgRKJuxyZc= +VmVrGQo2zRokW/ZuO9bN6739lKjFbXcMNJNlnotcdtYFW2yb3/MDorhH/+SF0I6I +SAfJlmeeNJ+zov8ENFGT09M8rmWLHAFAE9hX9AZnIaexDpx/z5hFlldfLGzHTsJ+MjIJjhGKMzeGqtLLfveLAA== +o6QhOIN2Sc4SHELnst17uWIkkNEHVSV9/Mf3IwuQT5q8c7RLrXh5V+xjdjr/pAz+JiONfDRu5Ynrv/K9N+ikig== +PWKGbNEPxxxpGpe8THO246Lf+8uDPmcVXUEAqHlmGZAV60/XYRPLoj1gpM92g2f7 +VmVrGQo2zRokW/ZuO9bN6/eIyTQVswpdOu9Nq1sM3lQBu05pzXl0GtKAgGyGfqsD +F9Sftf1PoN5P+lgvc+r10JJRjwlrykEl+0UkBy+8s+wl6iJLRTrEjruWhbanwxCI +VmVrGQo2zRokW/ZuO9bN6/eIyTQVswpdOu9Nq1sM3lQkxBfOuMPt3otkFc4Q500J +F9Sftf1PoN5P+lgvc+r10P5rsxboKPJdW+WQ92oAYaPBjhzguqiGDlmBhbYnpPXA +VmVrGQo2zRokW/ZuO9bN6/eIyTQVswpdOu9Nq1sM3lQZSfEQkoKmCvlWl3+15Yox +CAhn8dRUItEbEErp4w+lXyV0loCDlCywENzaYBwlkWk= +1u+XjG/2+GSQRv6EzCaWRQ== +0Ve/zXHSGNiOUBo4yQnSN8h4af+HeQDngC0H65SCobeLf7kMLGavxVwShh5imclm +KZTmaJLp+FU9X93j5Tpqtw== +mi1BvZebKjLnvrPqK6jwja3E3QfTJvJWnVYsxIEAKzI= +WLkT6KP2hJRlDfsZ6zo34K8RZH9Q1hb1hNi9wmThLMB12O7KPk+5j0/GRJoss+VE +Ys4bozvJBgtB7q16qcvtB6gj7zK8RzQFnHpNWJR4p4UaRcqxVOHLSW7Jj6fuzno7AdapSE7H3a1qkP/O08Wpbgc1HMk/XRoJspdxk6iDbug= +KZTmaJLp+FU9X93j5Tpqtw== +P9I0Yo5Y0r5MzXHfQxIQ4VRw6cg3Tvyquq6BFuWWzahipyJoyglt4PwdBHXadxz1FJ32cb4J2MTOw4Ifoq2QXw== +fg51De/+rw3N9wbc1MuySUIHo3PVXjOsQFdU3u96SKLKMqHC1U9t4ZcQP/kzZvTdsIpmeX9rZrgXvyotzgfBKnScZaI4Te8WBFbkZHaz1bNRc7JqV7RfIL5HFJ1TU8HY05shYcGZaaZ4g/6fFmdknA== +CgqYRLrQUkd1LIPh8RCRgASyeniOqnpA8Um4ooJeuPfl6TOwvTdPe7mY7KreIB+P +/0ULpLqgTvInFD0r5hHANsVMQyYMTxOCtjrehT+unwA= +j6FlrHlwfUXyl86hihVEkw///qeVs7YU/Opojpb+AmM= +4NFRQhGbYiwbXMyUtItQUY+Gh83Woc67fsFC1u/FOwaKoBHeKz4nEVgnTpMsVFNs +PV+X4fvCTN/6758fJ9HMvyJNBHisoA22qKMtx3QNPL+ICERBAD22cVhzRCEq6qsHMy1foH0BG3pz06U/2cWylm0RzH3tg1ZoI9WotVFooiu5a2T8MVwdO1z5AMemNHq1 +PV+X4fvCTN/6758fJ9HMvyJNBHisoA22qKMtx3QNPL/LD40K0qPB9Bm1U2/qklY5gUG8NCl5v3rrKcRvqW/0ZMmOAowv3rGD+1TnJMcobi923YBqBmPj9+f+fZpv2sgWsBex+D8k4prheh4K9rUh1Q== +o6QhOIN2Sc4SHELnst17uYPG2bSrBTq+8xVqwH4sIfiS9YmVUjMQLk6dgCU2pCGWV7cf/q0OTAr//rcGY7mah6es65Pbro3DCx0E/Ug7nL4= +rdDP4+hyPVJxG5CN3xYWHf113zx3tYHA6xKLMCTBJj2Rx4jTkLfp9CuMTi8GUSbSHYn2wabDmvJPq9k+FZvmrJGwBbQ95lt/e9PDFbOjo38= +2XzP+t8SYt+LAJpCSTOHHRM4YGWtBn9wyjrl0Z7w9n6H7vIvHcmV3axtCpvSaWdf +o6QhOIN2Sc4SHELnst17uYPG2bSrBTq+8xVqwH4sIfiS9YmVUjMQLk6dgCU2pCGWV7cf/q0OTAr//rcGY7mah6es65Pbro3DCx0E/Ug7nL4= +NU9SH7jhJIwKb5zjqWXqk65fNeGhk77lzYivS19ZJBF6ITWFKYey4oiKohXFwI+p +PV+X4fvCTN/6758fJ9HMvyJNBHisoA22qKMtx3QNPL/iIJraL6kdY5lY6KL7/ip6Hwc1nBwX70nUzCehNIv8ZBAFuC7y06txh0/BrVlycXwh5TzFT5xImNR/uqQcv9BV +PV+X4fvCTN/6758fJ9HMvyJNBHisoA22qKMtx3QNPL/BDOVGwZZxv9gP6y6NzEIAaULdSERVvUeGpt40DMTAJazDZ2pV+w93CRpMNi4QLCPQQykW9deLj4tPkezHrIzmv5s5VmsVo6AY9pqgH5C9hQ== +o6QhOIN2Sc4SHELnst17uYPG2bSrBTq+8xVqwH4sIfgR8iv3tcx+sjdMESJ1elfUCfSSeNXkxnnNboKgKtyl3oTlPkGr4KqB1+/RZZu0Td8= +BqvuIz6bL1BUHqmqyqHsX1XrX2F1wazlPq7N3wCpRPTpG4/rkBe7vHqfjJ7P2T4M +PV+X4fvCTN/6758fJ9HMvyJNBHisoA22qKMtx3QNPL/BDOVGwZZxv9gP6y6NzEIAaULdSERVvUeGpt40DMTAJazDZ2pV+w93CRpMNi4QLCPQQykW9deLj4tPkezHrIzmv5s5VmsVo6AY9pqgH5C9hQ== +o6QhOIN2Sc4SHELnst17uYPG2bSrBTq+8xVqwH4sIfgR8iv3tcx+sjdMESJ1elfUCfSSeNXkxnnNboKgKtyl3oTlPkGr4KqB1+/RZZu0Td8= +0zuirfbdySeBs+yooVipiiz0Cxx+yREQNDtO9afFJ0E0auuJuItw++uRZsyCs/0mwsD9m0mtlp/PKEQbrYj9pw== +gabdfZBybRrEth0k+KwuxZyyEqlDV6bZCuwzFLSo2WORLdRQsU80BfqPYfKlTg/W +UbJuac0dxLiN+5ocS3w2vb1KFn10TLldzslPwtaSogaw6uXdYc4F3/TAd3bUxElmTpKU0xCUDJnKuyWkxkudk/W7JqKwNXL6VxCha5g8CSM= +VmVrGQo2zRokW/ZuO9bN6xpbx1qLI3EtiMid/m2TtjY= +kTR15emObMQ9ZEgK/gbh5d35BqzmDFEwThoStH8fe0R8w+jBvBjLky3/oggxoKM9 +UbJuac0dxLiN+5ocS3w2vQi6x7BNbtUV3Q64NadvISTMKztUb0hq+3ThgAzVoB7pugZ8qxnxmGvVa1DL8F9QXw== +VmVrGQo2zRokW/ZuO9bN6wF6MF6VaPt6TgWwEopVPSc= +YL8fmwlqGqA5UEMeQuiL3j/sOMhe5GgzqH42CajYxfY= +1u+XjG/2+GSQRv6EzCaWRQ== +mXfGGds+NfPoeaDAMT3F3De3/vlx6XXvO4H7C/y//YrT01i3Gq0AAfY8U3eN59ss +KZTmaJLp+FU9X93j5Tpqtw== +sI0jOmEN85Ro1te7GgeteXy94cioshWIePmeDD6Uxd4= +ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE= +WuRvIBaPNRsVGEMoM4NSTuFzX4eqJNnk/iFRNUV/bCc= +KZTmaJLp+FU9X93j5Tpqtw== +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9gF5hYkCkXc4JDP5o6wj7zgNnj43zzTJobXabJOujNI/3d5gYLfgopXsQ9TvgmajM5w7X1OF3hSKAH2q3itrMyifNr4tcqv5PdLsKtyJ/LZZg== +GedaiPN5dqYTkpDAFlSse/fC+o3CaXrtPxdRW8m742Kdn3Cepr1sPUG1iwk6gWB3KfxJJylEJ31Ej++TN1MLwNhNXqxXqu31jPQhtmiOeX0= +oacxXEoPih9NKsd5qduwcF9ATIokCvDNdMWbbQ+1okLBLD+YzR0umulBCe4LKyBV +NhnIR3Ilo4H2su9/cTNo/JKKP+tBN/b/Js0u4fdYxT9Ou4UdOoVlTebwsMHqtpY8 +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN63t2MnnwkYHotYsrWH9vRICyWhn4bg+fudldZUhNSnla +VmVrGQo2zRokW/ZuO9bN698zdDf3TANViroaMFHNADkZuAZgDosHI43LqmWsgNONNNe48/1mFtI+asjmDrtKdA== +M0ZySqkmhuHCw6olbCKv95w0wnb5tXdVpvk+nm5slh1G8xJLU1xuHApKfUAMvlbG +VmVrGQo2zRokW/ZuO9bN69x5pV7nqDSWcTIi/Vxvw0q6MyrM+QjxLfKuXvnjwTTx +VmVrGQo2zRokW/ZuO9bN62pzVXz4KUejuXAqVdbEaEA= +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmUEMlB6WmrP7+t/B8UnJ5dehldGx1WpDO2ExqKyCtyLHA== +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz4jzvAaM97jaS2W0DRLyZ0k +1u+XjG/2+GSQRv6EzCaWRQ== +MB1q+N8CKxdHGjQk/9deWZnhOVPe2MNBJZhO1Yv83Kikb5nCCv4dYz93XNUeuoRM +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/PwBxF6bViFfZCK2od+BAyePkH05jVBu/cqOQR75KzenE +eeFiPlXUI0Fos82HYztSmek1Hai4VfOTZKn5AL9OI2s= +akHibliG6j9uvBEg96vReoCSW/2cQPbVxMmo1WnRDSTKckRA339gBDtR6W7CFwfso7I+qtvW4vUOFQbswXXT2w== +Z8UsPk1Q7HtwjRd4g01ryw== +SpI0zYSub7pmTdpBbKZzxqqObf5aYw5azkv7zdXxcMAHXq7l4H31BeCIFw7vemzpAatEUGoPtgFcgAdwExxlChv9FP5972erQefCcZ9QD9hgsNikShhS8ERWyNZAF2tiug7tqKnhHl0bMX21kDW3NA== +b8Z66UKHFNKfnviyrBdGNF7ubN/CHfspitZnXjg/lpCt22nhOjl8D94DRsY99XX2cZe5+WrAvq3IRMqKjiGzbQ== +/0ULpLqgTvInFD0r5hHANn/h2hIINSKoXkLr0tVjm7A+IDYkO5eeoSYY8vf6hSf3 +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +dH7AMVfUJYrKAb48JP4/cg61oLtT2qbIX591S6dFu+nmhL+9JNt4vu7EAVziuI9l +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P7e7/XNxPy5zWcJOvTd9fDBvrYakNMRuqp9fvHhaooUF +eeFiPlXUI0Fos82HYztSmek1Hai4VfOTZKn5AL9OI2s= +akHibliG6j9uvBEg96vReoCSW/2cQPbVxMmo1WnRDSTKckRA339gBDtR6W7CFwfso7I+qtvW4vUOFQbswXXT2w== +Z8UsPk1Q7HtwjRd4g01ryw== +SpI0zYSub7pmTdpBbKZzxqqObf5aYw5azkv7zdXxcMAP8PrCcWZBSrIVB3sZgZlURbCaOz0RX72rrSkrsnQlhJPjlbXFYTjZgbsrw8P20Qhi9NXAs6Vrjcxm16J5GJr3o+1+GgVvLzU/1fkVo52Myw== +b8Z66UKHFNKfnviyrBdGNF7ubN/CHfspitZnXjg/lpCt22nhOjl8D94DRsY99XX2cZe5+WrAvq3IRMqKjiGzbQ== +/0ULpLqgTvInFD0r5hHANn/h2hIINSKoXkLr0tVjm7A+IDYkO5eeoSYY8vf6hSf3 +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= diff --git a/class_v2/safeModelV2/sshModel.py b/class_v2/safeModelV2/sshModel.py new file mode 100644 index 00000000..9fc643eb --- /dev/null +++ b/class_v2/safeModelV2/sshModel.py @@ -0,0 +1,189 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +# ssh信息 +#------------------------------ +import json +import os +import re +import time + +import public +from safeModelV2.base import safeBase + + +class main(safeBase): + + def __init__(self): + pass + + + def get_ssh_intrusion(self,get): + """ + @获取SSH爆破次数 + @param get: + """ + result = {'error':0,'success':0} + if os.path.exists("/etc/debian_version"): + version = public.readFile('/etc/debian_version').strip() + if 'bookworm' in version or 'jammy' in version or 'impish' in version: + version = 12 + else: + try: + version = float(version) + except: + version = 11 + if version >= 12: + result['error'] = int(public.ExecShell("journalctl -u ssh --no-pager |grep -a 'Failed password for' |grep -v 'invalid' |wc -l")[0]) + int(public.ExecShell("journalctl -u ssh --no-pager|grep -a 'Connection closed by authenticating user' |grep -a 'preauth' |wc -l")[0]) + result['success'] = int(public.ExecShell("journalctl -u ssh --no-pager|grep -a 'Accepted' |wc -l")[0]) + return result + # return public.return_message(0, 0, result) + data = self.get_ssh_cache() + for sfile in self.get_ssh_log_files(None): + for stype in result.keys(): + count = 0 + if sfile in data[stype] and not sfile in ['/var/log/auth.log','/var/log/secure']: + count += data[stype][sfile] + else: + try: + if stype == 'error': + num1,num2 = 0,0 + try: + num1 = int(public.ExecShell("cat %s|grep -a 'Failed password for' |grep -v 'invalid' |wc -l" % (sfile))[0].strip()) + except:pass + try: + num2 += int(public.ExecShell("cat %s|grep -a 'Connection closed by authenticating user' |grep -a 'preauth' |wc -l" % (sfile))[0].strip()) + except:pass + + count = num1 + num2 + else: + count = int(public.ExecShell("cat %s|grep -a 'Accepted' |wc -l" % (sfile))[0].strip()) + except: pass + data[stype][sfile] = count + + result[stype] += count + self.set_ssh_cache(data) + return result + # return public.return_message(0, 0, result) + + def get_ssh_cache(self): + """ + @获取缓存ssh记录 + """ + file = '{}/data/ssh_cache.json'.format(public.get_panel_path()) + if not os.path.exists(file): + public.writeFile(file,json.dumps({'success':{},'error':{}})) + data = json.loads(public.readFile(file)) + + return data + + def set_ssh_cache(self,data): + """ + @设置ssh缓存 + """ + file = '{}/data/ssh_cache.json'.format(public.get_panel_path()) + public.writeFile(file,json.dumps(data)) + return True + + + def GetSshInfo(self,get): + """ + @获取SSH登录信息 + + """ + port = public.get_sshd_port() + status = public.get_sshd_status() + isPing = True + try: + file = '/etc/sysctl.conf' + conf = public.readFile(file) + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)" + tmp = re.search(rep,conf).groups(0)[0] + if tmp == '1': isPing = False + except: + isPing = True + + data = {} + data['port'] = port + data['status'] = status + data['ping'] = isPing + data['firewall_status'] = self.CheckFirewallStatus() + data['error'] = self.get_ssh_intrusion(get) + data['fail2ban'] = self.get_ssh_fail2ban(get) + return data + + + def get_ssh_fail2ban(self,get): + """ + @防爆破开关 + """ + data = {} + s_file = '{}/plugin/fail2ban/config.json'.format(public.get_panel_path()) + if os.path.exists(s_file): + try: + data = json.loads(public.readFile(s_file)) + except: pass + if 'sshd' in data: + if data['sshd']['act'] == 'true': + return 1 + return 0 + + #改远程端口 + def SetSshPort(self,get): + port = get.port + if int(port) < 22 or int(port) > 65535: return public.returnMsg(False,'FIREWALL_SSH_PORT_ERR') + ports = ['21','25','80','443','8080','888','8888'] + if port in ports: return public.returnMsg(False,'Please dont use default ports for common programs!') + file = '/etc/ssh/sshd_config' + conf = public.readFile(file) + + rep = r"#*Port\s+([0-9]+)\s*\n" + conf = re.sub(rep, "Port "+port+"\n", conf) + public.writeFile(file,conf) + + if self.__isFirewalld: + public.ExecShell('firewall-cmd --permanent --zone=public --add-port='+port+'/tcp') + public.ExecShell('setenforce 0') + public.ExecShell('sed -i "s#SELINUX=enforcing#SELINUX=disabled#" /etc/selinux/config') + public.ExecShell("systemctl restart sshd.service") + elif self.__isUfw: + public.ExecShell('ufw allow ' + port + '/tcp') + public.ExecShell("service ssh restart") + else: + public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport '+port+' -j ACCEPT') + public.ExecShell("/etc/init.d/sshd restart") + + self.FirewallReload() + public.M('firewall').where("ps=? or ps=? or port=?",('SSH remote management service','SSH remote service',port)).delete() + public.M('firewall').add('port,ps,addtime',(port,'SSH remote service',time.strftime('%Y-%m-%d %X',time.localtime()))) + public.WriteLog("TYPE_FIREWALL", "FIREWALL_SSH_PORT",(port,)) + return public.returnMsg(True,'EDIT_SUCCESS') + + + + def SetSshStatus(self,get): + """ + @设置SSH状态 + """ + if int(get['status'])==1: + msg = public.getMsg('FIREWALL_SSH_STOP') + act = 'stop' + else: + msg = public.getMsg('FIREWALL_SSH_START') + act = 'start' + + public.ExecShell("/etc/init.d/sshd "+act) + public.ExecShell('service ssh ' + act) + public.ExecShell("systemctl "+act+" sshd") + public.ExecShell("systemctl "+act+" ssh") + + public.WriteLog("TYPE_FIREWALL", msg) + return public.returnMsg(True,'SUCCESS') + + diff --git a/class_v2/safeModelV2/syslogModel.py b/class_v2/safeModelV2/syslogModel.py new file mode 100644 index 00000000..31e7450b --- /dev/null +++ b/class_v2/safeModelV2/syslogModel.py @@ -0,0 +1,983 @@ +QRASP55VO/1DQ98p1csw9A== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +dBZyCsfrbwqvA0sbdGrIGg== +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI= +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +PEKPgJeDDCLnL9UcS39EYQ0oxS0YHncLtyklh46m5EBkNzGCoSotSCFeO4A9wJZj +I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M= +1u+XjG/2+GSQRv6EzCaWRQ== +izOKcEjLnmyxxrod80R5Ow== +I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0 +9/AEQy6sYem8wkQNCggJi4i9kWw0XQ381/dYG6HKfZo= +piG6BsMF31u4R4iA6R4SRA== +dTLQ8Wr3wPn7w6pte1B1YA== +wiYVtfO/yzajW1Tv5z7hyA== +bDrBrYFBEwTqHDQlSf5Wqw== +SbQQ5SqO9QrBwZ9ObpMYLw== +1u+XjG/2+GSQRv6EzCaWRQ== +XIfdJ79nObMM+vyAmKbTmw== +P1nWGQOfbECkszATyvUnYMoMB+zOtupYIF89n5X+cOkvqsM/qxPIR+8JRAch8USo +rpy88ff5JnVuJqXSNlYf+CQ4VKHExseR2LosMAdYEeHGNm80vDZH+2U6FEAnuapk +1u+XjG/2+GSQRv6EzCaWRQ== +hbDorme8BqzUEmeXCAWmYa52umm3PBCcfSvATzTG02w= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +hxPPiT2HRNyGCUbYWjiGemMsho9zzoERNvAKutOOCEWVhC1sbjDeebh+SGn5u8GmkL2g41VpzeVyv9m/J//MuDXVnCmww1qdZncqaHpwyg8= +wgR07xfoapmx6eEnFHXXYlFDtUVt+yJ+tU8VHk3FJTvnJUFpUNld/t84QFPEYKTpJU156tin3Vc241jPdNebVm6HK/Ydm5jv2Mb3PtLSJC4= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +0feE7ECax+/ZCUSs3gQ/3AaKzCTzDvnrH+EOl140p3Ejvkj/qRKNVgZY6MNTd6YR+5IenuNCFRCqo568dB1SxUvcHBcdz25PdNciWuU0T5T0+kclUUtQxfNacEShYTanibNFhUnMe4HHLWOUhn13GDCqcyIADTbNu8ZEuFklZzsT8l2mK3j3yk9iI88+Tz8U +1u+XjG/2+GSQRv6EzCaWRQ== +TC/LaX35Eb0V/GmW3Zr2UpJqD2VXFNDkddRfcdBjn5Xo11cfHVg4MHTU1ECnQkBR +Z8UsPk1Q7HtwjRd4g01ryw== +wa8O1mcGGxpEvPPRX3ikmvykeeKhIH9HZybe0SWYg6M= +ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE= +wI4EEWzdSnynhqFxMCzFmo1bZxLdYghu7tPjNNa83Yw= +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +/p0PqyMitwnpAkMVBMAV0MOxh8vna0k0svW7xqoHRyo= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN6+eQW79nEpaaEVxkQ5mhMLDYFz3gFM695f1GA6K12DxM +VmVrGQo2zRokW/ZuO9bN6wYljL95v5S+KLH0XvYeRAu7jaBdeUshyw1Me0QcftpN +VmVrGQo2zRokW/ZuO9bN67F/Q5IDafbWxan46AKyqXhJkHTWPWMJlgqQQU22Q4EH +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +SvL+08tQNi0qJOHjInKZY6RndgMQo6zUlLYIZu8iQNU261HNxXIEnmCJLHd/cAn682cyzkJMzokqlyn5ZLB0wQ== +GDxnwLueKeYNT0/dIp4TaA2EgKkJaFhgdpXqI89CP9JxudfN4YO5Bip/xZCOAyUl +89eRayX3G8tCX3FDU5IcjV0+/dsDUitdv1ScoFSstoWJGpFlcQ4x5TphzsFb2IQj +VmVrGQo2zRokW/ZuO9bN66izANLHguDX6h0ZMylPTZjSNwJjUmKceY0nGArlnnjq +F9Sftf1PoN5P+lgvc+r10NBQ12btoHUSbewJsWZIkLA4htiDy2/7V46D3DsQdHGR +VmVrGQo2zRokW/ZuO9bN61Go4AfJcv9+G+6Yze5rakTqOEVt8xxc04JEb0WvhXiiyOuFu/FvRpO928WIX0wTxw== +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +1u+XjG/2+GSQRv6EzCaWRQ== +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P3lRQWSpZzKNexozsWlQSmt +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidZcC3xryH5q1zGGaoBvvKTdC1AgWcdepqNrVhCT6tqt7k+JxPMwVGPnSqxhvLgv1DQhFmLlBc8XKdIY0AICqQQB +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmzHeTpF9D/4KEYPN1ABlSXLB+bxGaCCDvXrlgokvNEu+QTyZ0OkNbOjawGeCOhOAY= +esnxVrQTJckUcXi8Vt8zdyY4i7Esn3+rv1ghcUNo4q0= +NhnIR3Ilo4H2su9/cTNo/L0HJQSj7xDirJmInN4ogA7N4utqCGavJw/zWl3BqVBOcu0yh4310yAJbcS5M5y7KLT8b6U9cXXn+DOi/El+juU= +1V2v8QerKOmubvSxgB4eTCJqrBg4vZgNAdzlsRAbXfGCflRUO5Z4sXw8H0VTfzWe +1u+XjG/2+GSQRv6EzCaWRQ== +o6QhOIN2Sc4SHELnst17ubkalL7sSCEY8nh1Aau3kAESZfNThdx/u6aA+dfb26kB +1u+XjG/2+GSQRv6EzCaWRQ== ++kSw/JERkIrPRLyCm/D7AdGS0jaXYQYSNkJ6PDaS5kKRDw+QnaOx8ss81acdnppHhvRGi5fhyW54Z5gPLzuFGbO8BuY6dL1EqHs8DpgsB0c= +DGL4HmYq7EifnYNbqVjovJ9rKPDbCo2TWohz7qULva5wehaU26sfTyFVmBnq3LLIjH6lvyfPNa4W+j1TNuSYYA== +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz7wO3aq9w4Rrk6lz64ig0wO +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +TC/LaX35Eb0V/GmW3Zr2UpCUU06JrEEfNymvLzRwUVgF/R4iaOcaI2S3O3N7OAwl +Z8UsPk1Q7HtwjRd4g01ryw== +wa8O1mcGGxpEvPPRX3ikmjl4u4bTOurjxSyetFOSLpQ= +5Ch5Cy/OpWh+1WaF8Su/eDwl93O5klNCtuxXaiypyoA= +wI4EEWzdSnynhqFxMCzFmo1bZxLdYghu7tPjNNa83Yw= +Z8UsPk1Q7HtwjRd4g01ryw== +/p0PqyMitwnpAkMVBMAV0MOxh8vna0k0svW7xqoHRyo= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN6+eQW79nEpaaEVxkQ5mhMLDYFz3gFM695f1GA6K12DxM +VmVrGQo2zRokW/ZuO9bN6wYljL95v5S+KLH0XvYeRAu7jaBdeUshyw1Me0QcftpN +VmVrGQo2zRokW/ZuO9bN67F/Q5IDafbWxan46AKyqXhJkHTWPWMJlgqQQU22Q4EH +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P1DHXJ/TJdbmPH2ckSraWgG +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidZcC3xryH5q1zGGaoBvvKTdC1AgWcdepqNrVhCT6tqt7pCgU8fcpxsvRgulBE1NZu9SSmus2oz4HzvvEcXgYZbN/5tlDq8RdAY5WQkwLWPTq/dVzZBv6z2B8FhigMKfcMnFomnMj1iyhyb10pjIliz/ +1u+XjG/2+GSQRv6EzCaWRQ== +esnxVrQTJckUcXi8Vt8zdyY4i7Esn3+rv1ghcUNo4q0= +NhnIR3Ilo4H2su9/cTNo/L0HJQSj7xDirJmInN4ogA7N4utqCGavJw/zWl3BqVBOcu0yh4310yAJbcS5M5y7KLT8b6U9cXXn+DOi/El+juU= +1V2v8QerKOmubvSxgB4eTBzsiAfEZawGIv/PJDVZk8DLw/veVTQK3qL0m1VX1XdQ +o6QhOIN2Sc4SHELnst17ubkalL7sSCEY8nh1Aau3kAESZfNThdx/u6aA+dfb26kB +1u+XjG/2+GSQRv6EzCaWRQ== ++kSw/JERkIrPRLyCm/D7AdGS0jaXYQYSNkJ6PDaS5kKRDw+QnaOx8ss81acdnppHhvRGi5fhyW54Z5gPLzuFGbO8BuY6dL1EqHs8DpgsB0c= +3iLgbOjdwb2NJRX0BUpwUIGMMUcYdts4/byp4bTeA76nRH83w4WFLV5l6fI4JsOauRI+ykY2xjrMOgZrZSyGfw== +DGL4HmYq7EifnYNbqVjovJ9rKPDbCo2TWohz7qULva5wehaU26sfTyFVmBnq3LLIjH6lvyfPNa4W+j1TNuSYYA== +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz7wO3aq9w4Rrk6lz64ig0wO +TC/LaX35Eb0V/GmW3Zr2UtYsmXWasld7EcVPoh9dGjGt+2Xjb/ezeoPiUvh51Scc +Z8UsPk1Q7HtwjRd4g01ryw== +wa8O1mcGGxpEvPPRX3ikmj1BsUFLbf8jUkWlQ6+15/GeyFemD4bVxaphwo/ntI75 +ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE= +wI4EEWzdSnynhqFxMCzFmo1bZxLdYghu7tPjNNa83Yw= +Z8UsPk1Q7HtwjRd4g01ryw== +/p0PqyMitwnpAkMVBMAV0MOxh8vna0k0svW7xqoHRyo= +b4OJVZe8QyIpjuTpKXDL9A== +OO/dMlOTxBikYSnUTxYxGoo1e4d9a2dJZTGtQemHKj8= +VmVrGQo2zRokW/ZuO9bN6+eQW79nEpaaEVxkQ5mhMLDYFz3gFM695f1GA6K12DxM +VmVrGQo2zRokW/ZuO9bN6wYljL95v5S+KLH0XvYeRAu7jaBdeUshyw1Me0QcftpN +VmVrGQo2zRokW/ZuO9bN67F/Q5IDafbWxan46AKyqXhJkHTWPWMJlgqQQU22Q4EH +YdSR6pyH4bNMAFEsS23H+Q0wLdDp3qS2XQEA+Pt5gvQ= +VmVrGQo2zRokW/ZuO9bN61NtX+W4Z81vyIgNc7bfj8ScGuujW22yH0T8YtIQEhHl +CVRTg4gcJQueLD3Ni7+7eA== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +xWoGNWjKGPfI4gq8aHoTfCvmCdiXBxNOXqLgrzMPQ6YpjxbqlA6+KifkyBYEhUnLM4EqaRP0uxbk2Gj0m0Rw7w== +L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLqzcS5swPR9eX3Xw0rx13vEw== +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcOuWMW7q96BiVbhOlHj8k0LS+GLTgLVoUNjFkoOOd6Q/ +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidZcC3xryH5q1zGGaoBvvKTdC1AgWcdepqNrVhCT6tqt7pCgU8fcpxsvRgulBE1NZu9SSmus2oz4HzvvEcXgYZbNsAyiUdVbe7Hjmg9ET03bHboNn3w+ITWBFzpEUndWW90= +1u+XjG/2+GSQRv6EzCaWRQ== +esnxVrQTJckUcXi8Vt8zdyY4i7Esn3+rv1ghcUNo4q0= +NhnIR3Ilo4H2su9/cTNo/L0HJQSj7xDirJmInN4ogA7N4utqCGavJw/zWl3BqVBOcu0yh4310yAJbcS5M5y7KLT8b6U9cXXn+DOi/El+juU= +1V2v8QerKOmubvSxgB4eTCJqrBg4vZgNAdzlsRAbXfGCflRUO5Z4sXw8H0VTfzWe +1u+XjG/2+GSQRv6EzCaWRQ== +o6QhOIN2Sc4SHELnst17ubkalL7sSCEY8nh1Aau3kAESZfNThdx/u6aA+dfb26kB ++kSw/JERkIrPRLyCm/D7AdGS0jaXYQYSNkJ6PDaS5kKRDw+QnaOx8ss81acdnppHhvRGi5fhyW54Z5gPLzuFGbO8BuY6dL1EqHs8DpgsB0c= +DGL4HmYq7EifnYNbqVjovJ9rKPDbCo2TWohz7qULva5wehaU26sfTyFVmBnq3LLIjH6lvyfPNa4W+j1TNuSYYA== +96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz7wO3aq9w4Rrk6lz64ig0wO +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +gn9R9ypc7EQr3UZGxM7R2AzpBmjpt//gk3VbIVrmsWPUgmmJziYMAJ3SBavXHHpg +Z8UsPk1Q7HtwjRd4g01ryw== +IafCwgGxn6pY+tyyf7k+k1uPwgoyKLoGy1T2IHFPUrs= +lGY+HGJ/DlLufZHWWlj7U8JUjESN387GevZrf0/dwpoTGJutrsojcsEElOQwuKhR +ZCo7BKJgBzBoR5QyWRMODvuiXuF1YyjhIRa499f9oeDFOwCSNimfzEv6xwOiYENc +Z8UsPk1Q7HtwjRd4g01ryw== +NYUPEpAMYcAgMU1lywXh+APRTYQ7bT0FxrwWNK9r+kE= +X/Lp4PsbbXulceSXUvGUpAixZi3IzpQX5iLz2HxFdWwS5A14Kie1TLFHz4QUExEMrjBXCKSQ2mS07fFxKBoIRQ== +s2s/1ED0ebJ2NmFlE8kk05gdRKkjvH+bphLoBZkO6vMkQmee56NmkDMz3ewSmOSg +wlLHv6kT3Q/RmtMBN4nDASYMmJRC8P+40dq2mnWeVhc= +VmVrGQo2zRokW/ZuO9bN6+vO0T5LxpdIZ6hmr5+BXMwvx1h4H3tu9h3Lw+gtPHVT +ACEEdgL/kNbx7RaZcSRxZKy0zW5JN3zc9G0K673Kt8E= +VmVrGQo2zRokW/ZuO9bN6+CU0Uzm+BLGwlKn+xqu8O0= +L0eUthVnpkGsmKFAX6d+uOW5BVuM+7tcrCiEwvU64kY= +CAhn8dRUItEbEErp4w+lX3cOtVXg8rs8OR96pWoJZvY= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +TC/LaX35Eb0V/GmW3Zr2Uq12VTZnVAgLNUPRVpboscSkdq/rWesDEOFnnwfIdOM2 +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY8qDyry3eeYEnSUnFHqFOEdqLpNRCL3M5LaBhSQtDro7 +7JQQDTi0T2idhFjao4jEpWyyzmLHEngKhI6rkPYC+OEQ3v3507uiPdl6O4aO+4ER +7JQQDTi0T2idhFjao4jEpV/go6iO1qzsLRmLYXsC2awnSWcIsvPYVwnYvfMjZaDtow5+WSfBeFo+pvL7hARzXDxRko1EzJaJ+fi1oy8D1/8= +KZTmaJLp+FU9X93j5Tpqtw== +CRgPeWAvMGMS2vfLmB+XqAPNkWJygDEUlCyJRk0QVvtGXcHrIivyKjsAViAs3OCtr/wdXA9sspltDH7P0695hw== +/R3t0fIiNxFa1+EM4KdcC/Tq9YNQxX2rnc682C9+fe8= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY= +gmymfXJ+E4e4NAtQtupPJnNetLAHixU3GutHyzvm5FjbdeuSmaUynxh9ANB6mVzZ +/2yBdrTJDE3TFtcSLCcx/9sJSd4kIdV7hBv4DW/k5kQjLCSiOqb/ooMNKbVZJjgekfdghInCCdI6qQz/jIgZgA== +NhnIR3Ilo4H2su9/cTNo/FhhB6MZ5HlL8ilxek9dnzUbA0zrRzxd/m81iyHyj8Jn +NhnIR3Ilo4H2su9/cTNo/KlYN/bsxuWzFF+nWhu3hBR2BWGqsiqwtBlTpURXFcpL +NhnIR3Ilo4H2su9/cTNo/G1hu91GucoyUk5CAa7Se62ja+Iodh3ArUEpNw9/IIap +l2FJPs4YkAmmok1ulDRuSA== +NhnIR3Ilo4H2su9/cTNo/FhhB6MZ5HlL8ilxek9dnzWFsroGkpAbYhhbP16I8llP +NhnIR3Ilo4H2su9/cTNo/KlYN/bsxuWzFF+nWhu3hBQYlkX5gl9hxYgaKXgpyNbH +NhnIR3Ilo4H2su9/cTNo/G1hu91GucoyUk5CAa7Se62jqdhNgRiCY2VhbppcAkgq +1u+XjG/2+GSQRv6EzCaWRQ== +eDDKQ1PxEOXD40WdPkjAt+4gIw5LFJ2JffPLp1i0RM0= +/2yBdrTJDE3TFtcSLCcx/7i0c5WFgpYA4Y1T5vIUHVO5oUJ3yHw6al3zQOD6XGLm +NhnIR3Ilo4H2su9/cTNo/B0KkXyoVKsrbyorgJnjAt4= ++plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg= +1u+XjG/2+GSQRv6EzCaWRQ== +0feE7ECax+/ZCUSs3gQ/3AaKzCTzDvnrH+EOl140p3Ejvkj/qRKNVgZY6MNTd6YRirlilOXAKg9O2fXCfHGfBtMDPeLErFs86v0ZeSLWbiUlxSp8Q6JZIdSDtsOukEmI9CkgD9cr1YKi5zNr/8OAB6nq4n5Nx0RnnUUavr/JMa8eGXsXCu3G6OAO8lGrXUZm +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +ISoywpaLHuQBxWIUflU2WE6goDl6H+SMTlvAvhjEOunmRWoDI71YFjPGTq2PREVfgI/0kDXgAM1iC59/IdE0zUqUlwg/visqR/ji4CUCNls= +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P2neUS+qyt4fxBbg7aS5U8ZVSEGgWJcksomV0iOXfr3S +wOl7lRII4ZgqRTj9M//0b1qhFBAHPMvcKJHX8xF2dd3LFwNZYH3pF0t/6DGxSkRF +yqM179SzW9HnBHozkU+IlZLJm7366WyNnx7ZMsu9exUEQG5/9gzkoWdK3lm2KNSi +NjU4zQ5DRyOHitK6CHLOgNNxRKGkyV3kGqu2T3oe3wk= +NjU4zQ5DRyOHitK6CHLOgMJ4vze7G/BcNjyu30dz0dI= +bIts5UhOLbX+5vrkUi2fEhpPnL7BGuZkarYaOytlgoinj9jnlLh+lL3x0ZV5fo0E +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +XEKtPwhadR+kpKQN/+UTafshnKFviV+idW9zO4AP3io= +1P+dKgYYLCQ5O0NZwxSOaJcUBjFbQdzF9CNcdagyAI18a21hAHVeicWVY9WgSU2K +zrSEP+cwIY0LGAkn1FqokI6RwnSJpjctW3UvricDZLxItXuL73USA+6pRr2HvpJaC7RiR4yb1PJQkbGlH0YRWQ== +1u+XjG/2+GSQRv6EzCaWRQ== ++xLLwGkVmjlAlAN2wYY0FOnVwRrXqNlRBemygIVo5xo= +p0E//cFR8EOf13yDhOlObMD3Yuxj4bTt6EnfF43vdnLk7mp9E28enjNVoyEj00PVBw63PNtJHh7nYjCP57FGPqozYy2TzONryzbzwkoUMNwvUkxvlVRrGo2rxAnE/g0V4Cmmkw+dKL0nhwQooy8bnVmuDHamveEqMpiknfK8NBA= +p0E//cFR8EOf13yDhOlObP3FQjpN4YYsA4YJPXooDzrxOF7DYxRlXeKipAKSaevnHWB9P8oJq6upWDy/MUOvLA2G18HPNK28orPv/JxpaCU= +p0E//cFR8EOf13yDhOlObJwu0OCz0PycRy7pO+A7qycPBfJfS4wcckld5TKXEJcQ8LTGY/BgOJ8/Ey+ycvnkaPNCF8x8ePPtPFV33M0BgGR33MbsHGevwAuVMKg7zRUDIXwgrTQUFIdujw+lXXU2GmtNOEJjkhHjhR2dXDsYQl+qxS0SKUpVIYzDr1eRI12GUQFYJwR76xewQvSowM7l2Q== +p0E//cFR8EOf13yDhOlObNQNgN99cJOreDX6FzaHhxv3V4pef0BAT3QaZNKMIUivldoeOBbHNkFVO4BMgqzu4fDHYcR1BKL6eBdwt3OBu1kVdmgx3s4fTZ8LAlEPq2BRzuN4F8ZeuA1ppvWiDhACcEHyX6Pz6yKUiqxmQn0qung= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw58tqRCidnUtuhiV/SQek1vZ6sNmdinwpH5+NOEy1mNl8yusqwsRmAnJlzr3FKfnstUDfM7vtqOWitJUQd0uNl0= +tmi7c6vWl6N6sdrwujX3GReSPhJ5ssuXUZtdOTkPGXc= +4E0YNxQKZtqIoiEqkAJApA== +fE3zmgbfhO9ZwEda5GgwDzwSc+DUx0Ss08sG6egCcJP+XUG+DzlYynYbT/JVkU7P +EsliW4ECGA8kkEJ0BUSIKZ+6kvYYxaoa29sn6RE+30Q= +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUqoJYM2k3wpGIGNfe4ZVylRY2CmgGV6o3LCAp54cduWSIwMrRMTIErqM/4y21/qV2s2Bxs/+ycPzzZdeNzsz9/q4sZS2NAVCp5rDAV2WNLaY= +VmVrGQo2zRokW/ZuO9bN6xCL2jO48J3n3RHpsxsFW0A= +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUtD4Dy7F2C5KfRvcNVwIaIQRcd7Fy2ZXHLWGZJPl8MX9RBwYra8rs4CAKfeGMN3Bb +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUgC86euXJOlFyHZyqDAOLUAdeaHFM4IYtg18Uequ7wafHVvZue+VX8ISJjUr1sqwFRVRgsWgqIDu5NviNkV3pauDs1a5D7tn5bIpmJcr1KCm6cN6DLgXTjktT05vWK1srWo2mj1pylV4R46zpVCmFGA== +VmVrGQo2zRokW/ZuO9bN66Op6JyhixbDicezrxsJdmvmw/JvaaHtlFkTsLFyz3qi +VmVrGQo2zRokW/ZuO9bN67OqiCrLl/L35iT+4iw8qa11ix7vyn67SQSkxOVmBzkUv3HBNQdTfHHC76HdkzkeKhcn8l+7nqNJm8fOH+30rrfwQzm8hP4An/vuWNJxgVfdBK/7rxGOROS+FKukjRDAA7v4+O6TFSTWLNyfZEFdSHAACyn19p4G4SaBK+kNp7Y4 +VmVrGQo2zRokW/ZuO9bN63hl7oGUPd8q+ErnISkgMco= +VmVrGQo2zRokW/ZuO9bN6xCL2jO48J3n3RHpsxsFW0A= +4E0YNxQKZtqIoiEqkAJApA== +AaV0Q13NpbJz1j/27jY/ubg6CfL/9D/rk9oHG6vv7Tk= +J1UGlXkKhKXD36OrXXhN8PbGS4CCkC/QcSnejunC4s1HNXfKIR7pGHCoZHkqc2al9dG3/LRVuqxK1/lHGulhpw== +VmVrGQo2zRokW/ZuO9bN64ywnuovLuNhCiDoP2wQAWzHwT2J7ZgEwYI2DHVR0db7 +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN64ywnuovLuNhCiDoP2wQAWxQoOqVjvwf024i/wGnySdC +l2FJPs4YkAmmok1ulDRuSA== +EsliW4ECGA8kkEJ0BUSIKVKU5W04dIhCWYbq1NBglGIbd3FwxA+IXISbsRb5KU8x +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +6xO+ubEwkQTp/D6ATXPZaR9RFvPZlxoFFcMoTEBWzdY= +1u+XjG/2+GSQRv6EzCaWRQ== +lOh2GtzHjjMM8E9J40AuOWo7iSH0P5z8Y9QjLJCJBGjZZ/Q9S9r5t3lPlsYorgw68Y18oFZuov4IXxgtwIU+fyOxgNLXV9uwsqyK3UQIuC0= +T0lwjDK5GymcJe/LbHEB+5LTXLLonGWNuuTQfjCTPqg= +wlLHv6kT3Q/RmtMBN4nDAfGIYwX5xDRj7uztmlOephI= +VmVrGQo2zRokW/ZuO9bN615pZCvbchosnTxjAGDSTuu5F/3dP6AEc2sI/MY5xwN3 +1u+XjG/2+GSQRv6EzCaWRQ== +YnP/gQVEpWES+RQ7J7RJRlUivABjbdmquJjPlRdBH7s= +Wd7OXFQxn2HMHBqE448Pi6S8+/GBA34d3QZMmVea2Tk= +p6i8iK7HDbvmLpoFrhNyvxqRSTuyLOZi037vACDsfa8= +esnxVrQTJckUcXi8Vt8zdytTNOUxGpVlE+9gz0v5AKo= +topgAunkM89n84cH9D9pHIJ0dN6Gk7b/C+bsk/3fJRquhc7fUjMhZYvLbsfG2DMWzaSd6qLjQSoUeAO/UtXhP1Yel7kB7XwKvSI9CIFJqS8= +ACEEdgL/kNbx7RaZcSRxZPvmvyPIJl0tNCG2z5oQFSMF3Rtz2Zl3Uf+ip6mRNkxm +VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM= +1u+XjG/2+GSQRv6EzCaWRQ== +J1UGlXkKhKXD36OrXXhN8DolGRp6NkzBMOOoPdzZSROBDCr+gph8JmqTmJ+BwzEcAyCmQfvb6v5GI6hFw38sPQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+M80RvQb5VsoyLkSgRiqcM= +VmVrGQo2zRokW/ZuO9bN61RJ7HPvuDQyY/7jnqZlstvr14lD1Nsc1GBMciUP56h8 +VmVrGQo2zRokW/ZuO9bN67mWWDHj8CSfv92pyS9sLERjFtkVPnpdPLZSP2NPs1gXeeQZjNsawPLfcnlKBwAakNgFVuo2Yd5N4wsDtWM07aE= +VmVrGQo2zRokW/ZuO9bN68V03MBz2tWTRGBIaN8QcnWa3+54ie/uRDPpsaNfYNUhfJWkoMWEjN141ojxjPQz7iUbeWa1gJ4fwE2A4QfkFyY= +1u+XjG/2+GSQRv6EzCaWRQ== +5wsVwKLrIjIWqeTNpSVp9QDHrlhrdh8VDOVcdAVCwxo= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +QdTjTxbShl+8mrARISGoAew+M6wRwESTJ4GivNwwWTIyDaIjNgge4ZpF0WBvDJqZRgXN2RgjWkE0y98VXQbEpQ== +Z8UsPk1Q7HtwjRd4g01ryw== +EauAFvhEfQAlaCCYJmuW5eV4DJOW7GNwqk+bbKrKpSaH1MvmX7D3DvPHyH3oySvOnRXE5AlMLgwUDhfDZANse1idoOMd3rJKanSLH7Uc123RRwtjA5eu4NckUBCieqKNHkfIlUn5nFjMA1ICSzTCQsv5pY+TV/1yO9BG5IofZXg= +eeFiPlXUI0Fos82HYztSmaNe9in2SzXpHGr5hBhAinnNHyxMDiBuvbazGbrnCMky +bIts5UhOLbX+5vrkUi2fEhOK0qYcUWJPnPtVafbmigUdzXiHWAONlKw9KDOwOlPM +akHibliG6j9uvBEg96vRelg6b5pjOK0azc0J/Ev8D6EPHt5rKichbRoGlTycmehx+Th3o7fyS7lnrCT4ZXS1gA== +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +NmkgFF4ZsyEHu9KVT53La1lz7XNNET0A9zdhd9gFT9fBSLnem4K6iQOWTy9x8XwI +topgAunkM89n84cH9D9pHITAncWu+e82byWKRCcayzEuvdX6re3IAygjiK0dtvwH5pfePRD6LD7p9ALfCp41GA== +1u+XjG/2+GSQRv6EzCaWRQ== +NmkgFF4ZsyEHu9KVT53La1eD1XgGKKkTpXLEHn6AsToySvZkHOsdmdS0PUJccZzo +sVk6QS4Pf3ntiMkl4LqRRdNSXsQEDaTnzm0yHVvLcNydLD7JFUCuBLvtEtInfM9cLHKhemuIRU5S45GYI+zKfw== +1u+XjG/2+GSQRv6EzCaWRQ== +JkdoXgqzqMYpkKh82pPju9o4/urRwxg/lFTkhxBEG5hgOJ5vGO/GPYvjcLXE248o1JQGij+4m8UEtIcbHbGSRRVhEZVih+RN/xjwceaGFpM= +mgV9MrvUK0fssdmv3ApVy7k8mJdR/B+7eG8ccjV2Ab788JJLQjLAWgAvu87PYMIxrNY3ii6KXngsif22LUSmMDbdoMq8P1ubKINGjlYCA7gTuIGpBMZsNYzFMa5NWPLopAgMwMESku+TbkFwChLHaw== +K9fxAkAAgWE70Il8AJdd4rdBcU0my9+uNHCVuDkF28NZN3ATTy62g9PIqSFIMzetO7VcoZ3+2dLF7JWJz2PwmrKOhy7hLolj7vwIWSqWgW1E/24aK4BeKO/DZ6lIwCW2E0Ly26J/TZrEe4nSAsvnhCF2/Sb4A2swT7NQC9kADyg= +L0eUthVnpkGsmKFAX6d+uCiYQic7T+3704A5xRjtHHV7D1K7sA7PU5Lx63H+wuEdqWGBURjxs/fUqKDjPX8aHg== +NmkgFF4ZsyEHu9KVT53La25kspERjj3Kd1WtR5F3DjUeyufiu9OPGHQHCZOJDZd/ +L0eUthVnpkGsmKFAX6d+uCiYQic7T+3704A5xRjtHHULvPeJ8pPVUmfUVgaJGNCIp4sjsG53ijiz9EipY86UOQ== +lGY+HGJ/DlLufZHWWlj7U/wgW3T0y9TOw/q89uQqE+c= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +WjK+gflGKYbL+4lozgCawFxAQnjo2U7M7PxwwYRzZjsaB1ggdhAibLBZPcfdEwOx2KWulak8d6g87sjhm9UYeQ== +Z8UsPk1Q7HtwjRd4g01ryw== +DzneBA92dU5V88ETMYFQ57uX0+Soy2svl62wHi59Ele/p/Fr3qr6/65PhVKxF6Ph +bIts5UhOLbX+5vrkUi2fEuV05t7kiBXke4QQt4fGEIi1pyM37lJMRHRDxQmK3gOZ +Tn3rOfE+s6NRy9yVv10cExsGK3PlpvQ/21Omt6hqVOjP06IQhzjsmDO6W8MidErE +eeFiPlXUI0Fos82HYztSmXW23eSyH2bXlOYpYhyb1RKLhaGx2IoCJa24JWsHrcMAg8yR7zX1oWHc14AqlOrK0g== +9BuKX7c+cBUFGH/xEQqKwAu6MbJNm8+b4qD0aAxmgDU= +Z8UsPk1Q7HtwjRd4g01ryw== +Wd7OXFQxn2HMHBqE448Pi6S8+/GBA34d3QZMmVea2Tk= +LxCLgek/xo8kXqFx9hyUVJPL3TFbh8u2ikhOGSAV0rg85G9Y5wYNCPljvLZlShFM +topgAunkM89n84cH9D9pHIV4dJ5fXtVEdOnz8/oeQYqujphF6DcnuFFxsQW+tGqMts90IYJNxLKB0izTwoEhvQ== +MzoJ1HANqAion0JXNkOKLkUXCYWzU8bckEHdk0zTAVCxXpXc/N2/2C4s2HOzit+j8Cu9wnFj9h55D4ASfhbhaA== +topgAunkM89n84cH9D9pHIjK3O9J9W+DroJme0vc0dxLsI4ZMqq+7wxYZZro3x0X +T8ztCulMhN50eAMwonrN+mnIYb5fDs3vnEBct26rKaEcORGfRo9zgegCK5TefOmJQqk4fO89rFCV3QP2Wzahfg== +bo0XlOt+6S78LT3IsBdT1dEroBAtDg8pr1Pybq8zhQJAu/VoI80oqogQomMUUlPM +v9uxr4HcPPk5UqMUuJoEoB7nY2X+tzb9PZyPToR8I6vD2vPuybQkfI0RMQ5SpdHZ +topgAunkM89n84cH9D9pHLhIfpf4IEOslMiXh31h17WTVnRZP8H/e9ND9t9DHG3EOcgqF1/s/qPfl2fGKEz3rA== +1u+XjG/2+GSQRv6EzCaWRQ== +Wd7OXFQxn2HMHBqE448Pi+XpPmaTTyeqpJxLnY2yRin/EZIgzqJ3CViWAhoEpU6erObfPycKdvcVekGvCE4Fyw== +lGY+HGJ/DlLufZHWWlj7U/wgW3T0y9TOw/q89uQqE+c= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +xzdWmbiM4p5/aGFV8YqP4XLLyrbPzLln0rwZDoXcnKc= +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P4+OZYX1oZ6IDO9Bb8L9jOTF/Bt2J4quQTw6RN85Bumk +Ys4bozvJBgtB7q16qcvtB0RluR7RwJXaqe7bJfwI9hU= +KZTmaJLp+FU9X93j5Tpqtw== +eW+3TRHX+r8wYGmvO/rbmQKRB+qqZDGER9PE45Hh6DhKWFPeFSKD/CBlJ+rZhJXE/6NzadTbUrA2WD4xQ/NtZg== +eW+3TRHX+r8wYGmvO/rbmQA+K0RVUGWoC1b+gP2mkFaXSKIZdVNEI2dyM5Nc324r +m00MPwDBB9sb6xb6db18s1+VrxrV3/MYAhac4NSUZUk= +4lm0ybc3aVxLaNQODc9aBcT0vlFE00Wx+6lONISyLb4= +iUEDlyYF71b358pIL4FKLxKByAvC7nSDmBVkTcIWC+9RPGSBJZRzJ2JFx9aDM6YT +zxz/Z9yjFalbpeH0OEk06wb+y3G26TwFK0BePbfiFxswJa+xbCabv6iKd6mKX8sM +zrSvdYautDwgfoprAQtW7Mh3DupVdGrOOgUnOp6J1n0= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +WjK+gflGKYbL+4lozgCawHo0YqPGy4jvJqzaJIsqw8aht/a5La2JHCiBs7MPYkJNnkttfrNlz3AZpwB+QDSF06CacZvErF4UHWUUepAZDW4= +Z8UsPk1Q7HtwjRd4g01ryw== +NNh1PlSD8WUnUEbz+BU/P5s4ZWJ5nCZ67hdzagwPZfnK+vAzOOBUPTQOeTz/eFRP +yqM179SzW9HnBHozkU+IlS7E9Jat0KMy3qoE1n5uiMAJ7/ZYgCvtzu/Qi+/1mAim +NjU4zQ5DRyOHitK6CHLOgMz13P9khDg6jBdTbmByKXOddMHKhILqXmmCVMC8sS3X +NjU4zQ5DRyOHitK6CHLOgKqZU0dP/fC3Nw6diKT/uI3N4y0eHljA4CIE4JRl3a/I +yqM179SzW9HnBHozkU+IlZ+sLT0JkWbe6QF8HumAIuHDqd+ZqJq4nJMu+r1QY06/ +Z8UsPk1Q7HtwjRd4g01ryw== +XEKtPwhadR+kpKQN/+UTaW/KEaZ8xQsb0J4R4+wy+qw= +F3HmfyKnf+xR8iqi6kYc0jeDpsR33ZKtlDiexNRZYLA= +3hCm6yDF8QSQ/yEbAtwtdmUoSfAgsymzLlnjfiQPPgQUC7w3DY9Wc7Bn+2+99jPC +6P8mKfouOK1hwXy3MrrCzRBPdWeF7NFSQT8rGlPCjGB/+XWHqqTbqSpG6ZavymMALgzEpY4QOg7MZRCal+ePJw== +1D18KWr2hdVsBcdZx1OaPddn9pCR79IrJK89K1q817H5A2Tia2JPGCrrHgOkp8RV +VmVrGQo2zRokW/ZuO9bN67r54/S5K8MNluBiVrjXDNs= +VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM= +WHkOzVx7seuLmxs5Hu+l/mrLDQ6BBXRseSNCn19UXP3hoocFrE+8Ryld4yV1KZfPLP8WjHXhqhwqVYteO5GihA== +uLea96U3oZ+p5tRB14NcukOqXOc+02jM+Wu1nAP3Qp8TsFA09eNgyYSzkxUs00j0kvSyulXKCf4d4VyaNdgiOYGlI6IN2tNOigQjWnS0T6k= +84Vymj90Wzn5yYuvz1pUuN+5YqHrmeLr14uTJtaH6Rw8M//FznjjEFAKx/A6G+T4mRGWmaJJ5mrlCNpDHMiqiiab0aARiCzaUr+TVJAp1ktqd5/WqjIhEIQ22J7sfDuL +VmVrGQo2zRokW/ZuO9bN67G5CWsj7J/k/lt9VMRehZ4= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN63zFN5O/w1krXKBn8bzERXc= +VmVrGQo2zRokW/ZuO9bN6wUs+Gj8tefXPS6OI0lu6+C28ajUjdjhJZKZ78jrIltE +VmVrGQo2zRokW/ZuO9bN6xoaa3pLsxXyLdB8GbtG2YY= +VmVrGQo2zRokW/ZuO9bN64WtjIYDMA8f1aIhS71PV/BDDNtiMn/i2htt7qG6uu2/ +xCSE09sQPuao2+KRKinomiRcpRci7l420yHWQGSif24= +VmVrGQo2zRokW/ZuO9bN65aumthNqQ+IW4uZbHuZhwikHiiPwk1EfEOEo86sWjD7LcGb0RbtIKmAR4xv+N7qXZImTRX92Ml/lMtUX2VvSmDQSHW/qxC7fvXW0Egj7mQJn19nWjNYYBlGLlz5NXYlvWMRri9elAmP5NNsyphXvXk= +VmVrGQo2zRokW/ZuO9bN69P4j4YErd4z0j/lq64sehhXBs7XglFhAiuqgq6fZc7o +c0jePRxtTVZYop75Q5JCFsHjToEqjqqXfvyUun7oxD4= +L0eUthVnpkGsmKFAX6d+uHvX7jXsjikZBMo/rKrjiBw= +1u+XjG/2+GSQRv6EzCaWRQ== +RpR4QysGv9QPcAuUlmZB/ldqYJzcdfQBNZ1Th5SM6QO+POEY2DGleYbCe5l6UfNu +yqOVJCkAGOhqcQYBOo7yqQoTxFeI3v/YH5mpkidrW9HyNTAJRD02Ymur9pCntQEY +6P8mKfouOK1hwXy3MrrCzYZqDmfnLEVSCjSn5KIdRmc4dkXAA/1uvzDl3l1PUWtt +PRG/YRzXVD8iVR3bzEcUnllZ+cz9udLBY+J74ckAM+cZ2hSzyyfq/FybzwKXxMpO +1u+XjG/2+GSQRv6EzCaWRQ== +YnP/gQVEpWES+RQ7J7RJRlUivABjbdmquJjPlRdBH7s= +Wd7OXFQxn2HMHBqE448Pi6S8+/GBA34d3QZMmVea2Tk= +p6i8iK7HDbvmLpoFrhNyv9D601DxXdSOTjmgiyd3Nqr1byx7HtjSclWDwuzUoQV7 +m00MPwDBB9sb6xb6db18swVJnbf7H4WMvnIczhPQZ+0aVjp7owaSvIW84fWof7uc +1u+XjG/2+GSQRv6EzCaWRQ== +m5EI3+BXxkQEYKYaxORD+g/7lNafasyk405EhIyuvnNqv6n1h2jC986RGfvXjJkv +50HSHdrYmraajnW93vvabzwR6Glv3j7i9vFV1sPcZ7KgQtnqa8GX9KUqxMUlXQ3m +1u+XjG/2+GSQRv6EzCaWRQ== +gZ7BoIAABc+I8g/rYzq+f81Dacxy6eezCrTC1ekZwPEW9OSpvgIrYa0KGCOwwYXcTIWHKwChc+OzuGx8c8CYxqz7F329o74YYNCcaYwSM9V4eKMtsG8h6eORGPf5+1XV +VmVrGQo2zRokW/ZuO9bN66Yu9w4eCdqZ8p4k0NS0g/mr6t4fHQqW9//3beDBilCZmROWIZ1UbGT0bwXRWX+ibj7PTXWgbRivwGWChIIkZpWmZxWQE8b/GcSOusQIDbAt +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1u+XjG/2+GSQRv6EzCaWRQ== +iUEDlyYF71b358pIL4FKLzQOhNOG5YF9nS6QdrhVDB90W++YzeWFyqsFYbjeCsHl +3puL9F6TU32sbZzfbgCef8z8OvYCxvmTlQzeetx7rGA= +HiVf4YuHR4gw4rFUUeRvKw8qvbDmX+tfMbpiIaNfsno= +gZ7BoIAABc+I8g/rYzq+f+9HHgvBGOxBt37ysBPwI13uPk52FI6uvFjvWgHf7Jko6NUiSbwIYvUdo44a+xTir+OvHDqhK3py8D4bbLHhU7E= +1u+XjG/2+GSQRv6EzCaWRQ== +uWQGv+LShBH8u8S26pv5nPnM8ff4ZGji1vcSAAFUnfWxWXseAysF38MM+4hJ5dUZiFZbZax0zk7JjR3dSb08FV21UNIPkNYIE3W2R6DJCOc= +ltIZHHUgft++nMUXBsBki6T1wXBeAb/L2jS/24mdBRE= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69uRnj8/r8BaXEMGSBfrjESJouhp3OTlYGDThMnU5TB6K0SkOsqpVqbZDNGFz5szCw/AgnLXsS7/vstntyyZyJXLi5GL2bMqOI9UfzQHPSFIzK98I+cwwynqIM0yyc5o2IXjcbGmabsIRO4LkX3gb0k= +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvOMI4vw48TaUq4aRLYrmsXaJb+h7/y2WxDdspRfKEXnQw== +VmVrGQo2zRokW/ZuO9bN6/jFu99KJ+MquaMXJxls+pzVuGNN+Sl79RauAvg76B1wsGLU4dxJTOlUT9dMZ1w1ug== +VmVrGQo2zRokW/ZuO9bN60SggNWucpg/S6RxW0/YnaA1skwCA0ZpXLeYREWmU+BS +1u+XjG/2+GSQRv6EzCaWRQ== +c3KN//OVINDLfTVXAO/Lbk2Hd5TU1RVmBdDgvw8QHupIMpn754UzwYg885IkGmJJnOM8kM3rDIg7hTLxdJmlTw== +gZ7BoIAABc+I8g/rYzq+f+9HHgvBGOxBt37ysBPwI135+k2vlF9wrhYiCZmHN8IE +VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjYKmRtsBhPWippExvZOW0/LJEaf8OI42VUCkcmkYiDNBmW4Tnaa50/2shkMsiFA/ejpSEFIqAkBQ53j0BA08zWZ +VmVrGQo2zRokW/ZuO9bN69z/Qlonskd1/GLdnc2k+VvhZQyzqI/fgAtkLK8iM1Hn +1u+XjG/2+GSQRv6EzCaWRQ== +jGJZZqYXcDE83UImRHGU4lffa63XK0BuEvGrV/OrPdOAgULF7Tuf5t89bW0q2hMA +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvOvKbZ3tEv+5Q+mTyYmHi9vPAa0y2OBkMds3bGNOyphY7BSeSDajRh6Njz8SSLq+O0= +VmVrGQo2zRokW/ZuO9bN62BySiMpDH9w/XNtJREDLzDV1N7g8dL9ueDFUmh6GeUDDFVqtUWhLZ39rABIECio6GXmpVIpQAKy8dcZ2qCvhpg= +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +VmVrGQo2zRokW/ZuO9bN6+g5JNrv8rEbMLqUNj28Cdo= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69C6A2K67LgD1eJvP3N6AWwUURoCkGGIg7SORUpL6Seh8a2ykGijyqDWzo86U586lwaKy2+VvF67QGk3R/UXPiXiVzWtIisu9t+GqCz0r6aF +VmVrGQo2zRokW/ZuO9bN6xigJm8qa4WpsQUsklfpWpWMUosdOYpkkLOs84FK2jeGjVjPCXRS43orE0WYC4dCfwQQxhdDG3yLsvZlftXvV0o= +VmVrGQo2zRokW/ZuO9bN63a0R/S59cuVV4xXEauJnWRz3xaZ2HgPpWq/a72oFYZVDKhEoXsn2iHq60nHoc8QreeySx+sbJT56yXV0YT5mB8= +VmVrGQo2zRokW/ZuO9bN6z6oXkNlIbiWTEz21chkjli+NoZjhxN9ORJwLCGQEnpn +VmVrGQo2zRokW/ZuO9bN62SJgGjT/WmMIIxZ/ruMwWWisTQI+r0CB+ZPSVt0T7SS +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6/x9mVEQ39E9CmRqJ+FlnL9EqemW9t23C9OdbGoCQ8cM +VmVrGQo2zRokW/ZuO9bN64w6dpups4cXvLDqooOrnJyvr6Av+x6T4G801ox+KJHC +VmVrGQo2zRokW/ZuO9bN60tueB5yS1mXpf65ThW2BRcr02wDxH+Ue+yMjEpkVNPJk/hP4oWqVTQE7JZlCIIrmA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69Hh4k/KN5p4WHNMxzu2tx2JIw6LXMR5NzQxKIVS864yY10isQvmwGVQ2DlSTPoctMbfzw5uVWHhvpOBeLt9pKI= +VmVrGQo2zRokW/ZuO9bN6wJmwpIiDUqk/W5Xvci/WyF4XF2UWEAYoz09NC+em7kI +VmVrGQo2zRokW/ZuO9bN6+LmY8Wp+WHHBK/N/eg16Q15+pIZBGb6sScWJhvIma2k +VmVrGQo2zRokW/ZuO9bN62L/LKqacP9lunLs/sZVgAVfljLX6fxmEJSvrO4zXddcQ4NElU+FCAcEPT2EZ880Cw== +VmVrGQo2zRokW/ZuO9bN66YYIRANoQmJRNKXv2xg3pCaJdGg4CQVst48mAlOjitX +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN631Ij+DuOT66h6R4m3bVN3TwDZnJGvuPIa0DLVLfOxX/JTIYUqYRpbQ52ElbP7VAkg== +VmVrGQo2zRokW/ZuO9bN66PmE0xfRAboNx2TeC9P7J4NhzSV+m0EkNzefjR+mpgN +VmVrGQo2zRokW/ZuO9bN69qqX19RJUeMAQ5O8TuJCvc= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6y49h0FPe0mXa/MRz9rhxplV7doJhsM1E7TfB0kuuQSh +VmVrGQo2zRokW/ZuO9bN6xwhOaVQWDpfnMF6LA0XCFstMJg50MMXLnnkQUEa/wNn +VmVrGQo2zRokW/ZuO9bN68sDl/nq5E1fo4CRxhlKiT6ewilZZCn5nRtj5+srI5B2 +VmVrGQo2zRokW/ZuO9bN6/TV8HYrXfBS0+5RislOy68JgSfzi3n359kKXZzzkBVKkRF/lY4XEsYndx7+YjTP0jz72pF5WNcrFfW3cbITPNg= +VmVrGQo2zRokW/ZuO9bN619JyKoLhHxa0i2TABYxWfIzBj4xXUmyUuFPMVlHNfolzXARazZQnnKzxz8Qgx2TFd/VDzNwQ8p8VUgiV3DidQAni8mtUG7OUvdH9rqfLaPy +VmVrGQo2zRokW/ZuO9bN6x5O5549z42ZbPZdzwy2tzg5ykzCAsDBEFgAKnqr/bsWy+wofZoh6groBJA7PTdK/0btaorXdA/sR+o+7A1E5hLNkjWbWomUfcFC39spL93x +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkllVUyblmrBXx4KTxlxVkh4bcW2se2uqmrpRH3UsBE2hQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklScjVEbWucpHFf4jnfOHB52z9dx42c56L6XC4wp2g2ruIrt2feNZhOUsfn6mmpqAU= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklOzDQU2UmHOWsnNbrLjDmizGDPlPu+SoalSQx/sN2EQIT/3N3nh5LNd2fZegmgZDo= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklS6OX30XitSf1zWXDoSBdzuvFrWI1gEiyE0eWnthSNpA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkngkTGQ8tITagcno1If0T633KRDqrO43O4ckFcdiv4j57M7qt2UPUTmNG9R+ArOTMhYo2cTmli5bm6RYaKnxaZE +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkkyr3u3tmRRdTjbXFN3bPPu9wRFkGc8MqoonARkL67arQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkngkTGQ8tITagcno1If0T633KRDqrO43O4ckFcdiv4j57M7qt2UPUTmNG9R+ArOTMhYo2cTmli5bm6RYaKnxaZE +VmVrGQo2zRokW/ZuO9bN61TKHwbNGf/2Ub0vd756U9O0Gtl0vapnf7HpdYx4U2ib +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklEcvH4r4MfmtfJUGH+dpI5pxEL8+TV2kZl/s7yEkRSBkV6R2mDWz6QJPXIhWnq/4s= +1u+XjG/2+GSQRv6EzCaWRQ== +lGY+HGJ/DlLufZHWWlj7U0N/DF45+V15HT5Nu+KAqWE= +1u+XjG/2+GSQRv6EzCaWRQ== +aH8osMxZgIfRkNShGiaQ8KeostDT+ZPsWsXljTu4fBJd1fM2igANyxWLADUf8Q/PaQu53yzgmI+X5rE72QX5HA== +Z8UsPk1Q7HtwjRd4g01ryw== +8+pJybctMsfvqVW7hafi21j617S4ZJBcnVSvovNJf2fofEHOzMbs09NxooOPyRza +wOl7lRII4ZgqRTj9M//0b3Na+XUNXsGthFHJZn+iwk9DLm8ehZxPVZsuck2aDMbz +FFgrrvXvjrcz4wZcb8tZVBfFxJJtUNj66FBgYGeUBuNe1QYojsfdMMTfmUWbDp4P +Z8UsPk1Q7HtwjRd4g01ryw== +1u+XjG/2+GSQRv6EzCaWRQ== +0yVj5tN/frSXacyX2PgyeT5E+HbZCBESO3Xe8mtOrLxt4iljR9cXdbATNOKhHq7wMxj1yRwJ7OaljkHeJlnzK3LEcWydZBw++GRPav2xpkPrnDffgdhIqRhLBkni7ydMmI2eX/TuJRps99MKfJtUAA== +1u+XjG/2+GSQRv6EzCaWRQ== +PWLW/WTodw3tqQQSBBHInw4g49GUi/PQoXrJnMOKMcv3Wp6VztQYqiLzaEcDdgSs +gZ7BoIAABc+I8g/rYzq+f/E+IDZ6NsXsUDbHValdfzb/Kruq/Vjk28atdn7C+Siu +VmVrGQo2zRokW/ZuO9bN6/8yGXI8x1DvWcxmMbVGl2/d+UnadBjumrSvrXqCFvxL/dQ1IF3GPQDGm1Za9My9dqpvvZxqoUl6zH2mT03wugk= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66QEbsrD511ragqq/ntAN9ip4Cl7MQ5ucFmM7oBrE7CwtFofMzsbhBUBYHWjnO4+Gm4RaVlUCL2nZMQ4ZlUMHk4= +gwLNi9yZl3Cgb1vmhIukcDiwvybm4po7xCREzBIXUUWMhzq1uZtLMFENJQeoM6bf +gZ7BoIAABc+I8g/rYzq+f/E+IDZ6NsXsUDbHValdfzb/Kruq/Vjk28atdn7C+Siu +VmVrGQo2zRokW/ZuO9bN6/8yGXI8x1DvWcxmMbVGl2/d+UnadBjumrSvrXqCFvxL05RBhHMkSHCThcMnvcrtBL755t4Y28qLxX95H9SOw5MIMfwwm+AX7jFrLSlPFL7z/LG23Z7Qi0ZBkt5yi3J065WfKpBp7ljnujnyRI5KxS3WLULIWKJfkqhy13iwfEXTWU5aH1Cdj3ES1NC50RTweA== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66QEbsrD511ragqq/ntAN9gn6I8xS/ec2ORcznjf8NbtWmOguhtVlMEA8/tuxJYlc+EcZYOpOz/OOJi7ykbx5GuqFl+UVDS0tpV5JPDnbAvDoPyg5AP415q+Nhiu9uo2uVlZvJcu7WWtFPCoNTx+1lGiW3wClWt7AZYCeza9v3va +VmVrGQo2zRokW/ZuO9bN63oVhYTQuQYZZogmVe/TzMw= +l2FJPs4YkAmmok1ulDRuSA== +gZ7BoIAABc+I8g/rYzq+f/E+IDZ6NsXsUDbHValdfzb/Kruq/Vjk28atdn7C+Siu +VmVrGQo2zRokW/ZuO9bN6/8yGXI8x1DvWcxmMbVGl2/d+UnadBjumrSvrXqCFvxL05RBhHMkSHCThcMnvcrtBL755t4Y28qLxX95H9SOw5NBHzoK2EBm81zr5nBdZfIqY9xIQsE1T6+1rIBc9RtxWrAgPHnUSyFwCGVIYz6lZga1UtCe6E+ND5HnA71abf4+8Mx/rNnpalgInue9MBDnlQ== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN66QEbsrD511ragqq/ntAN9gn6I8xS/ec2ORcznjf8Nbtv6k9SWAza517BnGnZCkv2xN0WdZ4ZLw0aPWE+30+q6fOg28LzTGXOUhu+Ve6HvJaj4cxoQ4JYcacpAzNMdlTCWLZM+Uptv/BbJhem6UisCnkHFC8acpX/dMFcmgzOKuIaIG9LX5bT7btboBMh7V/7w== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYmirComgoMouw9fWv+9wQLbQn3IxZ0+fHgSwwrVEz19D +lOh2GtzHjjMM8E9J40AuOWo7iSH0P5z8Y9QjLJCJBGiaE95pK7CRTqvzzc1f82PP +xWoGNWjKGPfI4gq8aHoTfMo8UWPcZxcLStbRn3vuZvrvRuO8gwYogoCldXZi6VGk +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +6d8NLnHX3WuS3g79bJvyhOcA9ux5Auf73oocslB7sDeWu0fDEoiafZo2yMYtO6chT1HFJerygYO/wT5DIOkSKA== +Z8UsPk1Q7HtwjRd4g01ryw== +JLWwEPnWK0L+CzhaEIjbQU4GwWA558QpsjOq+5nz4DgxhiCXwzz0jGRGKxh8dUTODJr+s6+Ah0R9doWb6zPLQQ== +L5bpcfZyn5Pc75WQ8ineZfusv0+lxTTZjtC4b06AF3zKUPd2R/NTwAVMY8WVnZ7X +EhfxP26BarXubxBeyusn3NfNOtb3HPBGFTHMHZMXSqvtCtihhBnDLGI6VsAxQd4V +Z8UsPk1Q7HtwjRd4g01ryw== +uTEK8Ng11d3ix2pA+DD/aXy+DXgD07P/dsANZ49KBIoQ9DDyAcW8u95okz+vDaVb +1u+XjG/2+GSQRv6EzCaWRQ== +fE3zmgbfhO9ZwEda5GgwD/RsUOXfY9pzpW9xRZl2fW0PbW4wjRnTUOggbHQB//acwA0Dv9aDYg0PNWy2YecNQfNF05hJabP1q+/C4qoAENQ= +lOh2GtzHjjMM8E9J40AuOZ7zZZM0vaCjhH3uRcP/NrM= +pVMCrZ7PAAHymZ70WROm/67KCrp/ppOHMaNTwq4h7pocNe5jswE5ov4hxkNlgfBC +VmVrGQo2zRokW/ZuO9bN63ZBl+ZLQVwzJ8aZTH8bazNsMnfZ+bm+E1rvhx3AfI2H +1u+XjG/2+GSQRv6EzCaWRQ== +CmFznH7ArXzhNsFBIGOkr5Vsg/ycHXuLeeSShlENgnA= +UbJuac0dxLiN+5ocS3w2vfHOkOj7T66O79wy+D+TgtoNfPdzdywGX8tpg28oiCYnv83pW/YxuQwN1apN//aLdw== +1u+XjG/2+GSQRv6EzCaWRQ== +ACEEdgL/kNbx7RaZcSRxZENcLifoZmLdDLAaOT7aBW7RjTwy4Q7KIvPTg8m/9qBC +VmVrGQo2zRokW/ZuO9bN62YaWBnXKkSoB0qO9pLfxSSPe4WYW+NQbuQnofC6bhb9 +J1UGlXkKhKXD36OrXXhN8PbGS4CCkC/QcSnejunC4s1HNXfKIR7pGHCoZHkqc2al9dG3/LRVuqxK1/lHGulhpw== +VmVrGQo2zRokW/ZuO9bN6/d2CLxxt41IFX+A1fbH83na68t62fa3cjMWGKFnkaMR +L0eUthVnpkGsmKFAX6d+uPmf6IqlOWYOMRLeTkcrllRN4/sFJgvvL5z1UEeHE8+W +VMfVrQporTLzVAs+KagENgnYKzmIrs4+BBVOAKxaFac= +1u+XjG/2+GSQRv6EzCaWRQ== +rKj1u6Uot58r2z9vVGpkYPs6jYzSiEQNQ0Vlqh8zH1eUMBI5lpqMlLhl/4lO3nQu +Z8UsPk1Q7HtwjRd4g01ryw== +ewqdfdhwKtr+fTAdDLdXxNoKltdD5CW97QCrf+yOioE= +Sc8e9jvbHiy5hwiy0HtAoOvGcehWBaW/6Q54bzic4ooH1vds49RieexSGCVDRfXT +1B9Eo1v10Ho41P86Qsxs+Rf3h6kOJxgKbqyJXd2SQDCZaqaZocZ40EQpaNjgDGb2 +Z8UsPk1Q7HtwjRd4g01ryw== +NmkgFF4ZsyEHu9KVT53La24afMe7Hpz+AvRb0pPLT5Y+DtSbSZQ9wXQJ9PdlRpbh +ACEEdgL/kNbx7RaZcSRxZDbs+G2PAEacg2XavjihjHkGDr+yC4TGqKna/D6MU25t +VmVrGQo2zRokW/ZuO9bN66b1EQBxDDTQrHy92SlIikM= +wlLHv6kT3Q/RmtMBN4nDAcHzCoHA7fx8ge824KyD6j2RGkLqKD0SoqmJdGVHetKh +VmVrGQo2zRokW/ZuO9bN6za89G+hm2DHEo1o7nC89kWHww92mvNp+cT0E6zNNzmXwifWbhsAF7H7xfRYH1Tcsg== +VmVrGQo2zRokW/ZuO9bN69h0W3AGoL6Fh678416+Hu2KoilysLPFqxJJbiCWg0kt +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +l2FJPs4YkAmmok1ulDRuSA== +gZ7BoIAABc+I8g/rYzq+f+vaPyTBeRc/jgIGrjGoV1I= +VmVrGQo2zRokW/ZuO9bN64Vu/H9alPnu3iLUUUBoSK+kyhoBM510sPwyNCX8Yr+ofuNO9OIf8TJMlD7EUz7RELcHLIPBGPTTiECymRVomBM= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +vF0ZftWrAGfdsoFfHqmHDJuEsO4R/FX2ga77ewUw7Y11IGYw63nwjmoZNzNj628o +Z8UsPk1Q7HtwjRd4g01ryw== +GlXLieqbP3oJrHHHl1oqk5GfuHjv+M1uY5kQiwNKOz0= +cCPEDb5F9Qd4JMGAsR7UfZfXQMkZOQ9q6AipKLgN9qA= +wzhSc3VGUoAQ+07oznSZPLPT5vU/alKRgCd2xATHoIdgjG3VPCXSpthiTEqckgdX +Z8UsPk1Q7HtwjRd4g01ryw== +rLwQ5yQ9bNuG44la3oQ+3afvJa6cgkEkDaPubWhiBa8= +2/eGB4mNeTC2Lw/I1/B9jHReQG3Sf6fhE0IH64g/YfeE/Ws6fbXaJX/LLa/JASMj +9L3jIB734YKv4S9AqEY35Kh1jnwS0FJtLaDJkqWU7Zs= +J1UGlXkKhKXD36OrXXhN8A/6KQQIEyEAY+cUx85urfskVuM/oTrnEBFsI3zXMuwQtyvGFxwsnrTAlyx/0hytcw== +VmVrGQo2zRokW/ZuO9bN68dKk9G97rhJCB3VEv0S3qjWifbCFeCN1sSryrl3vtTYdLbCLZieWWRVdoajo7IOpA== +VmVrGQo2zRokW/ZuO9bN69r44+V6JJX/2NLSGlXYr1YD313HJ7qeRebm2/AdMhL11t96l/ZN3bscCjY1669x54LQrad8RoESQEFGNJ7tr8I= +VmVrGQo2zRokW/ZuO9bN62MvivPt6ucjDY6Y4UwXjRGX+MgrdU7vOEwe6NO1KSXK +VmVrGQo2zRokW/ZuO9bN60Sd7dFtgik0J339YqGp+LqOWzrYRhuwK3n/DwPxquLs+N36KFtUHhWKI9VFqVraYlnKaqKXuvwu19LED1GmG2Kkna5Ve0z1QySEyTbnYwpj3mq3A5LUCr6VTgwESp9FYrceQzV/js0BIa16dwTapHE06PsyAeRntvguLENP7jvaYu2A79wYGT81tc7XklvAEdronju9kPx+7a+fzFUwV5U= +VmVrGQo2zRokW/ZuO9bN62MvivPt6ucjDY6Y4UwXjRGX+MgrdU7vOEwe6NO1KSXK +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN69r44+V6JJX/2NLSGlXYr1Z5XOqt9InPZIx/uA7fZePS +VmVrGQo2zRokW/ZuO9bN62MvivPt6ucjDY6Y4UwXjRGX+MgrdU7vOEwe6NO1KSXK +1u+XjG/2+GSQRv6EzCaWRQ== +sVwPh1y7MInnwEDK61kL2ZgurqcOtElG452IbPDSARaEvm4o7F6Vljqg3s4Vb98N +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +WjK+gflGKYbL+4lozgCawEfi7RDXlvnIiPUmGf6XyPurYR5IRGH6Jhoi1u8dhC6C +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY3e5Z5xEtbMc4JrlUL6rftaZUmLPHCH16nEnJiDRycKT +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQG9CJ/mCl9wdy9aVT0qKaa8 +7JQQDTi0T2idhFjao4jEpdluONpwOUUVh7y/o1Lzsm4a2FL3SufObmrWF3NyQ11G7uUUREDf5s1H4nVGAyEIiQ== +h2Amc56dmaRo3inGZxN9N0Mh3da6J2tf8CU4p/n8Ca2A7uHSizxBYlbR4VDvVrH8 +KZTmaJLp+FU9X93j5Tpqtw== +W08jtqcK2gqkconJfg+WHLJlYDaZnYglU+gPmeaeJz0dXPQc4GmpJ18lTWeF8Jvc +IsslvnYVqGkrqD0qU5QGYf5J/O0JjEzsRDI+7c68kAPbhQeof0lnZloVKmPJ0/OPwERg1AgDmZ5jUeNQ6jfaldScBwiDIsOdWtI9+ChcDxI= +L0eUthVnpkGsmKFAX6d+uK14dMm1+GhG8c2gpiVyR+E1Z2/jkCwp6Os25PRP8RSk +IsslvnYVqGkrqD0qU5QGYbCFSj6VHiK8jGEp4Z7SGzCVaeuBljBtpL3ckAqxImzQ1d5MvnFkYYWoGsfPJ/WPv8swi2RH+jUjIGOKkl3usaE= +L0eUthVnpkGsmKFAX6d+uH51QgM5kWHywxiE6Ty0F6BCQoJXs5zaJJqYGuXoD81B +IsslvnYVqGkrqD0qU5QGYcjXjwdbef/ruiHWGoRx3+tWAsJVYvcBpbkV5AJPt5MOcPZrXMgXPIOVr93qjXogTtDc9vraJd/N0pYe5+Z4eNM= +L0eUthVnpkGsmKFAX6d+uM+i6uIzzgNIsrvcJ0xQsTE06WRnqMHo4Q1g/bkpbg6d +IsslvnYVqGkrqD0qU5QGYbP084sfOENgLhN/XMGReZg0zyyRimxRdtJtSm6Sqdya +L0eUthVnpkGsmKFAX6d+uDQtcaiDMT/if2pHlZtAF6w7EuP6p/BouaL69QDJbMlX +IsslvnYVqGkrqD0qU5QGYbrfmZ+lxyGmJzabndV+bycc7U0Wl5SH5mS4H3CFQFF5 +L0eUthVnpkGsmKFAX6d+uEpYxmDfNJv/38PnR3IuvUzG7TueShUPZjSOSr1mkV90 +IsslvnYVqGkrqD0qU5QGYdNGxot71haXjS99OqNqqQ074sGxXHCxg0T7gUwpV061 +L0eUthVnpkGsmKFAX6d+uBEBFHibxFuwHX5Xun9xifvXFUxIe1hqpBHRh1Lw72g9 +IsslvnYVqGkrqD0qU5QGYacx+GIk+O8p/nDetEHOBxNO8G66dVdD3XUsN1D4L+DX +L0eUthVnpkGsmKFAX6d+uDKFjFlH66scjoKkr0HS3PwzqDQpIUplT5Z0WdNifCR8 +IsslvnYVqGkrqD0qU5QGYTZbaTDAr7WzW3U05y7JjCJE+JWxxlyG4/oGHZQRzxpM +L0eUthVnpkGsmKFAX6d+uKR3GJL+5DYQD9ef2JvARGDOf7TUitZF3uJDvhf4SXd6 +IsslvnYVqGkrqD0qU5QGYT4PypAZWVuTa3yx/tngA8BLwrQemYJ/HPN/7QEqxYRG +L0eUthVnpkGsmKFAX6d+uOhvQuK51ItTe89peTAEMXOQ5smjp/DG2mf1guds/omG +IsslvnYVqGkrqD0qU5QGYRnGdaaoC1M1g7ZAadH1EZcM7x8K1ujx/VKQzwSiV3gl +L0eUthVnpkGsmKFAX6d+uJ+E0oqu/x++zQJcSBL0VOekQHmZVACJJPl8TTRzVI8m9M3y+IGr3/7RTyRMpjpcig== +IsslvnYVqGkrqD0qU5QGYZTJr0cGLieJcZu1a1iY/RUTgK39OfOzDT+/AdeI2wCL +L0eUthVnpkGsmKFAX6d+uH07G4h7QHuhkW15eHMk0eA= +IsslvnYVqGkrqD0qU5QGYQn8o/Js3mP8t+fBdcR0TgAwd00zhld6rOJZQ6+yFxbe +L0eUthVnpkGsmKFAX6d+uIXRRpClQCuJqr/OY3HUOQs= +IsslvnYVqGkrqD0qU5QGYQheScjm8xnwil+c9CUIBAIfDVr4aEwxf02axHs55dTj +L0eUthVnpkGsmKFAX6d+uAX3W7tCeHuM0iu6taGk0Ik= +IsslvnYVqGkrqD0qU5QGYUyoMIjJLmAF5ojMLgMpyaE41ZKkM8Gjl3APdZx0Hj59 +L0eUthVnpkGsmKFAX6d+uEmIBdMpCCkqkvpD1pJK/Fc= +IsslvnYVqGkrqD0qU5QGYRNYwVLAovvSjrixyYk8aT36EarzpP9AgYTB5zNgeLWC +L0eUthVnpkGsmKFAX6d+uJfb3o2MxlDhM0IFL0qo2QevpUkPYKMcpvvjGL4e+KUJ +IsslvnYVqGkrqD0qU5QGYXSqTc4qTS7hPO4iPJv++5lyNhEn9/t3xBtXLeIofwMp +L0eUthVnpkGsmKFAX6d+uLOTVM8QXQomM/ScmIi0Aurqqbx0I74fDjhRNAJFYEPJ3pYwtdrIJSDHigUgbAk2dg== +IsslvnYVqGkrqD0qU5QGYXG6NlsXd/YqSaoeZGsOge0O+VwQiRXmNu6YtlNYN82K +L0eUthVnpkGsmKFAX6d+uK/VpHdm7KMAlOrmSjRBOrc= +IsslvnYVqGkrqD0qU5QGYU5Lk4PmA/HmYwjmHWabZ6hHV55xwbkRmwNx/z168Oti +L0eUthVnpkGsmKFAX6d+uHf7H9Z69Fy6EtLuPiv9GmjWSmDXIF8makwzDG3As2Xu +IsslvnYVqGkrqD0qU5QGYZszVUnULbYAsg6SI1v40Ie3ai1tISljWyUaHEuTyTeq +L0eUthVnpkGsmKFAX6d+uOMCsU8cjJLqb/iyzhRPI/ckwr99ZMBbGMnD7MK89CzJiEYNWtqeqQk216KxbOgGvQ== +IsslvnYVqGkrqD0qU5QGYQAmKpTYece2KpTt3d1NBCNYX8Ap/8fRHVUzkhz0ko4H +L0eUthVnpkGsmKFAX6d+uG8+ZBh4qlZRJvEzt5lBDV+PtTrj7mpKSjT2XGDtk0Vh +4hdB7RJambPsQ0dtPl5R2akq0BODLVZotsDNh2wSunpksKRZQGda2QfFL3yC7r9FNUGWj4cTVasPp4knqjntXg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +21/52BX1a38vl6/d4QTVP0hoUi6PwJEXFQ6wphCzPr6fXDo3Z5Vg+a7TcMA5dHOO +KZTmaJLp+FU9X93j5Tpqtw== +NNh1PlSD8WUnUEbz+BU/P/pyukZf+uIGpurCH+0HOPz4Yr/Z3jIpQQmOXWHeoYrG +ZtsV8vaHEXutlQxVeZ819plsRMEMfUI0BG6ieJYZQ0SZ5n31ETTM1BuyD7ejcndH +KZTmaJLp+FU9X93j5Tpqtw== +W08jtqcK2gqkconJfg+WHMWCpDQbDVBXW3mpkNRn3lIXAExnTItYo1kq5iRG4GcR +1u+XjG/2+GSQRv6EzCaWRQ== +CTVkyTnZxhWMnQdl2TPfNtZypvunYlImqx0NqMHaPXRBRXlCxO+cXNezSDuvkCTx +TADZGw0D6PLYGUHKmgeZL4HNEC8FTcIShYtTIXIxyo+0tdoxkJP4HbuHSul3u35j +topgAunkM89n84cH9D9pHAfyyGx8htIfqZCXE8ucjJj/OUSO2V7ozSUs83ljlvy2 +O3CUgrw2GJfB+mDjH5+NdkkEd9P97hGK1RTeRfwhysCroE1sFZzj1cl2Wj01k4MM +VmVrGQo2zRokW/ZuO9bN65x96E+pC4TupOHurj34PBY= +topgAunkM89n84cH9D9pHKjRNdYyRqpYiqgVsH37hnqzQCcFnmPRP7x+3/JqeKZ+haxhWTIMsEjUYMkHZbb60w== +1u+XjG/2+GSQRv6EzCaWRQ== +YcIT2RppFBANuzyiUKo/XbqbC0hnpSADZ2Xd7Kwfuo91ygsOr2hFjcoPoo/9iE1B +mEOKzB0uJs35kZrdrKxIkCclaJRcU34BXh5nvgXkUmhXqzXNaToiOYCIt8Xptl1d +W/Rdcl/m9EziNRa9XlvfZH5kAFMc6PQ4eF7Rsh/gbQLJ9bIjLmL0JAuq36SAsgMj +W/Rdcl/m9EziNRa9XlvfZCdb6qvEMYvH5wnxbbWCd1+DR0Cf3qqNnJRCqdPpXwVK +L0eUthVnpkGsmKFAX6d+uMzHH+Rw5vYjEEqS9kb+ft733Qn8E+MZcbFtFKTonvDe +YcIT2RppFBANuzyiUKo/XfLqIAjoBYCaWlb87AUyhxA= +mEOKzB0uJs35kZrdrKxIkAwLXKX1c/aY0fGJeKgMKfhPtxAt6BH1o9t+1mdRbz8z +W/Rdcl/m9EziNRa9XlvfZH5kAFMc6PQ4eF7Rsh/gbQKgUj6yNAchV8t2eLfKtDtG +W/Rdcl/m9EziNRa9XlvfZCdb6qvEMYvH5wnxbbWCd1+DR0Cf3qqNnJRCqdPpXwVK +L0eUthVnpkGsmKFAX6d+uPK1shPIFD6x9kjjo2oaS3fd8Xa6knSu/TMUZumMZllv +lGY+HGJ/DlLufZHWWlj7U5YwItAjxeXXjqfky42+km8= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +QdTjTxbShl+8mrARISGoAcUhgZRi1ym8p5oZAX1ZOiFX408x5lL2opLCXVnt/vA2 +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY933z5nYV5e5Vw9OGrr8WfRmBaVCVOfV3FtWbmWGq0nhK9Hbx3/RcbihQUwUjoECJw== +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +QeR/vJltSrUa36sdgLk/IfL4q9jp7Kh5fPIA7nHg0hc= +f1uTQJeAsYOuzXSplbSkIbPpvJXETldnXXS37p1dj3s= +Ugk5+soYHCBsUKBc634TSP5vAwrOjcBahLrsDvmb5FWZu1d3ORGf9BCh0nq1o7jN +ACEEdgL/kNbx7RaZcSRxZIZ7jqIFcKvTxbeHCZUGxKDJtaY2uw5Wg70BfPJBD9aWShCUnQQRQXXWR+PHAmREhq/pitnanpldXtISZM6BIHXSJKcThqhLjhdomc7tD9FE0gFGYn2hL/fyorY87ERXwg== +1u+XjG/2+GSQRv6EzCaWRQ== +PRG/YRzXVD8iVR3bzEcUnoHre2ym25NbVgAhCcm2A1hoiCeOLVi33wtvGBo3MOPE2Cb54XyHCFdK3+BCNa1Dpg== +O3CUgrw2GJfB+mDjH5+NdiRXh8cEweT1cRWwOW9QX5fh6Yaj08E7nEo3hPidINlo +VmVrGQo2zRokW/ZuO9bN65b6RdMyfA4zumGrUHCFg65RTBrSUTABEpsq9PJ4JCel +VmVrGQo2zRokW/ZuO9bN6w9tUPRz4/J0Cgrs+FV7Tr/YcWK0KgvLa4A8Zh6i8yg+uRoNBCckO5193pFh3dc5mA== +VmVrGQo2zRokW/ZuO9bN6+YGqEY/sG/2DL6NHBGgZFPn8HU0P0VRGqmcpSfpPR9W +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61YlA8R/kneqHpa4ue21nrXaE+mh5Y+vq6iFNzk+uuvg +VmVrGQo2zRokW/ZuO9bN69Ej+WUzR36sZsG0nGLhsTjFUTA171Py2jCOZGaWjhVUzTwgwMBDdiIDiZBt0M49pQ== +VmVrGQo2zRokW/ZuO9bN62oRed6hCMosT7UP35UqsiJ+neIey4O/p83yxE/Xz8Vq7FTWnyXgWBqC2Ofg/IQMLA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6xv6VKf5v25Gv35Ngxt7+SxexyFaNW8qsenwXUokPM6b +VmVrGQo2zRokW/ZuO9bN64a61Rpqu4F50qZSKzf6C34rN8pqiwxpLduFB9njxLiv +VmVrGQo2zRokW/ZuO9bN6xO985T9pwQ7sN4pEaMuKfefQO3H0FO740MZBVb4uwOf +VmVrGQo2zRokW/ZuO9bN6xr/qvm+5ucCO1hkB5N9exATtIjyk7lQ3ExTkIcU12jOt9fONgTPPb03RIYeiIzThg== +VmVrGQo2zRokW/ZuO9bN69SQQTccEgrQxwYplaazClQ6D2QNKN0egHCbyag6gxMo/CPqg8bW9jLetQduGzVa8g== +VmVrGQo2zRokW/ZuO9bN6+dlyOY32uq88jvIbgRzKiD+c9puldgGvnigwOeCT81wJWkhhBMtEqqc8b+vZxO8tQ== +VmVrGQo2zRokW/ZuO9bN6/bbff3RPIfdJD9Tz8nKTkS8AWh7qG4PAFWqinn3GS+B +VmVrGQo2zRokW/ZuO9bN68cDhwAKTM2Cy3z7y8RSN78= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6463nYA6JOgSgcM3CVjNTJznMLYNsYHB/6aPvDLy4Q80 +VmVrGQo2zRokW/ZuO9bN6xv6VKf5v25Gv35Ngxt7+Sx0bNvv17oN8olvRoRNFEX7V2BUAqQk1w9AG+fHfA3mjg== +VmVrGQo2zRokW/ZuO9bN69dQDCddUjZfqhLWgtMTA+6lr1BUsQBWcHEfSvRE3879 +VmVrGQo2zRokW/ZuO9bN659sVHCRNKwi5EybWXGjIFHDG+wKjWhy7PLQCmhQOSV4291MUSOlSEESLCcvl7/Fng== +VmVrGQo2zRokW/ZuO9bN62+mfM1KxuYR9GMEOHSn12FMvAE6TgNv6jSr3+eIjIkftGBTCi5VuuIi6ta2B1ahrhcNQtCYZ8yLHfkFzehQCbU= +VmVrGQo2zRokW/ZuO9bN68xhA6nn3nm5MXR5GCHMXtcxC3eMgYv7+sQYRGoRMUKJ/NtYdiS6ZTF06NLLhQeZ3Q== +VmVrGQo2zRokW/ZuO9bN68M2OVEeH8lXEDh7ocUvFZ4= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN60ntial7L47tWO4rV4Icn7HpBltblYWBCieqT4UVgq9CFSyUKzN/U7U1y808EhW5Qw== +VmVrGQo2zRokW/ZuO9bN62SJgGjT/WmMIIxZ/ruMwWUGgOzwXkp7Yl61rDQTwCRFc6zTS2NxH91SNGo4fvq8c0/QMcscIcl8fl2DAYz9BEM= +VmVrGQo2zRokW/ZuO9bN62oRed6hCMosT7UP35UqsiK29Hs7QixVm5i7kdWxory/eb/Qqnm5/jUpYwUUUkLhDA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+6X3Z2WgPscDVJea7oFkkBvFOwA8ZIliXzYpwCO5w8mCjx0uV1YsGYftfAWvGpnZgdXDsovKcS9qBn/5dy9/jU= +VmVrGQo2zRokW/ZuO9bN60TFHropKQybmgi/Ewhnmc7xtekqzThyNJcyQykKtAL8 +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN60TFHropKQybmgi/Ewhnmc7PyGF61LfQUSq+4E3rbm1p +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yOPAnkhPEHUxQR8wIbPiwSTWlEsAD2rF3bnj5NhAv9XbaRnFLHata2faDtCqOdjdb1U7MCISF8/6F97G7mquUM= +VmVrGQo2zRokW/ZuO9bN6w3mLqzdYJP4YnG22gQyoLmoBk43np2N2P04jX+kASBb9gClLRXQLQzop3WjvHjIKKMsPzTJmkWildPSC3LXw1E= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6y7h5D3hwYRtgfvir5w8hacLALFT2aKPT+72WX0CphB7 +VmVrGQo2zRokW/ZuO9bN69dQDCddUjZfqhLWgtMTA+7x3DyGBidwiLe8ycxBOOYA +VmVrGQo2zRokW/ZuO9bN68xhA6nn3nm5MXR5GCHMXtfY9183Ko8rfO9qF7oaQasTgQyzsj3jWJAdaiSjnzCWMw== +VmVrGQo2zRokW/ZuO9bN659sVHCRNKwi5EybWXGjIFHDG+wKjWhy7PLQCmhQOSV4ZrV+dIrpw07fAyDbieibNw== +VmVrGQo2zRokW/ZuO9bN64E7kV0oe1KNCWtfO+2IzG2FPrJbLwlL61MXY6h19yCgwlN05/jodOhk+oUWPJqcNN6IBI7oumAfwTtn3AERBME= +VmVrGQo2zRokW/ZuO9bN62+mfM1KxuYR9GMEOHSn12FMvAE6TgNv6jSr3+eIjIkfOEwcPjcDLjN9tD0xweBslcAkKKcfqK+vUKrRm3X7Nl4= +VmVrGQo2zRokW/ZuO9bN64OojIhoKNJ1F73QGw4VuTsSq1t0rpx7OXXkl8uQ30KM +VmVrGQo2zRokW/ZuO9bN69lJPu1G3iX01kmj1/0ynPc= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN60NuI+3rwmZPRXBatW5je5ashCp3DeAz4mwJFRsD2uIz +VmVrGQo2zRokW/ZuO9bN6y7h5D3hwYRtgfvir5w8hacofEZFxYSxFBNP4nrNMDKlU4v51PkdjGnN4kullijdzQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn3k4igWZGKmftHXEMWcn7kly21b/nYMgSJlcJhkvDi9w== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkliTSPh1fvkS/ScWR2yLc4+HTPJwBf81Hy/L5fQgnRrH7bNCsflDO0k2jj/2A1Vbck= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn84mKwOupbY4jKHqhGVSsSRWvM73UP+iiVN5SxtdLKWMg4JgXS/q/83v0PECO1Oxw= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklAMkk+cPHwsBtRXYD12TtxwEYLEKXTLpcd7/0JsjoVdw== +VmVrGQo2zRokW/ZuO9bN66XK99UTYPeH5P9OwsB1SDc= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +h9U6jl4cZ4C22C1wpKu8ve1yUcQ+dIn1vMmQcAZ0Z0w= +FBy3vCcAP87b/+M8ydgUJRF52I47z/TZBzA6Bymagn0= +xOhAHwpsatV+hYHn6t6A6bUPiUFFzlk01p4yvWK50jyy3U69zJPFTOkEngdeeh4dV2ZsmN/celU41tVeKyZCPKR9o4ij9I2DDit4hQ8yk8OvD6qwHkCfsNW43rtkacx8 +topgAunkM89n84cH9D9pHBHJGKogzkmjYZitDzWAv+GuAYoOOkSChVYahktCovv+ +h9U6jl4cZ4C22C1wpKu8vZOLikErPbvUJn108FkBgu9+60+OG57l8ccneiDXTrwOoz7y2s/Cah0Aj4t1RxHPgznaDgjVKZs3ALXpKjeJ0vo= +lGY+HGJ/DlLufZHWWlj7U6go50WaXphLgme8Jt6GCTs= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +IE+9Uft2qvVuoU6zfHciG3wzRZH5DHb0iYqeBG8ShAk= +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY1lJUaDlPvjJcgqNTD10CNjS+JFlHmmzAW7C9TIFGPZZ +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +2A27FrJel7LwQVQbnj4q4hlgih3qBk24vCQh3ZEzZH1D9Te7T5q1mDbPLZp0o0v6 +wXC0PXkDt+//2q1Gfu8pw1omD2s1DPC9/eHWLru0T7M= +/0ULpLqgTvInFD0r5hHANlt/A4HN48BaWWKSY1CtQvClxZHrVHBtcJRLbba3CP1w +9kMYV6mizqMbt4N+qTZtN4zB76eoGATkjA0lU5cQ2Ak= +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +Nxw10FAZUk+m0nadC1hdy7a2+IPpKTLzQ6uOO+E1Dbs= +X/Lp4PsbbXulceSXUvGUpEwg42OTOSx1VxHM46BBNX0= +s2s/1ED0ebJ2NmFlE8kk02x6FisgSkWfiVYBaeNNhGvko7bwZ1FD5aZ3vGnu9kSm +1u+XjG/2+GSQRv6EzCaWRQ== +p+qW8e8p1zrxOyU05vOA0oTKH5zqNz+PCgf62f9tApk= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P3lRQWSpZzKNexozsWlQSmt +FFvLZRifY4ofMFjBuFasFxWXCYrK9vqCc5dq5VE0iw6DSlYwafvxu5DYwNYrMthu +1V2v8QerKOmubvSxgB4eTBZTEixtIaCeLifMkliDnl1FSzgrzmlMEyvFnWK3RGd/ +J1UGlXkKhKXD36OrXXhN8BVlTjNvt2FG2CiPqdl199JaWrFGr3c4jWvQXS12vU6gFbGt23o5BdLlHOVX3j/o5Q== +1u+XjG/2+GSQRv6EzCaWRQ== +2J/cJdNHXZ2Qsa37Fo6/Z/gM9h4l3NzxQe1VHPUoNCj50z035sIRNuNbaAptxn44 +0aOe1PRcA3iTQeyd7fu/UT3OH8TgnUZIetc0kIThlMY= +V79F1HDTm8z6yGnQm/hDJr2dkX3jv5pKaOjk74dknTfkNo8du33ifDa8c3SEgcfl +mWtqqlZvPd5u+MDOxVnfq7x81G9ia1eZdDpax7kDimPICwqRAAp4SD95uu4DP+3q +ftwgBqNbwagslv/gBU4tpEE5GDJfsuYmSwXmJa3+o1fA58g3B92Kp8T+2Ht6OVkf +K/3TjoKXtlVKQEXdtPO0uifsTNFU0eFt8+2eD6sIbn2PmRNN2I5GHAjvsDqrxu3awe7s1IGxmpgEyF5PTdi4Wg== +VmVrGQo2zRokW/ZuO9bN6xz5iGlq9bNMsveN3G8E1JikmXOYVXY7T1MoO8bJd/lt +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P0b5qliwag+wNT+s/UEtf4f +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P3vHLK4yXJh/Y+S+1XhH9rt +1u+XjG/2+GSQRv6EzCaWRQ== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6xz5iGlq9bNMsveN3G8E1JisN1fUkIMjwSqaHMkv+2OFIPr5q4vWGJVqp1MZusIqwg== +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P1Oxzn1tUamy4I7kxYIUhQU3NM1tr9mAx/yIud930NXFA== +VmVrGQo2zRokW/ZuO9bN664XPSepA7GnWfbAIyL55P2Ei14aMB20i0YI/RCMc9vC587JhNV12B308q8WzisouLfqdSseWY0+wqtG9K74mqs= +1u+XjG/2+GSQRv6EzCaWRQ== +1D18KWr2hdVsBcdZx1OaPfvwrE7g7+awLCd9GHCg8UHyIrzitpsEab7Put5dPSX9 +VmVrGQo2zRokW/ZuO9bN654Optg5TGZB1gMGxxgScexFS1RR1Ks0UsELmW81E6uK +1u+XjG/2+GSQRv6EzCaWRQ== +o1bT2zrSxs12aPyNKpukk+RBjtOXgYwyhl9BlbuoPII= +9kMYV6mizqMbt4N+qTZtN73ymQjbbWCfVaP1lIPcs1OU1AQgg6+83fISm2I1XX/N50H2FN/cmR/jUdb8rfxdgjyYdsNSEsociL+R9aPXHUMPalIxO0ZQEAVocqvn4o/N +9zgnmC3+5N9XK5/NRaR4SIX5nH4iJWB9r8DAPkHCqMUc7KHT6GbFzm1/ZZDfEDnV +ACEEdgL/kNbx7RaZcSRxZNCtRTkFUngsUPEeQy/+UyWj7cI78bO+VYj4wVqG+FraCRyNCLzTSU5fw732DiiY4Bs6NS4JZHrotgQbzgu/NHg0XC2ZDt2Tdq3I1jVs24bycirX7+k1becWKOkHHvZ60ZXv1saJ5rczadDgDE9Qj+k= +lGY+HGJ/DlLufZHWWlj7UzT71dQgCsiwDOluCIjxD+k= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +IE+9Uft2qvVuoU6zfHciG5u6HyMWvuiuseR1J2M4pfs= +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY++vWRAhgONxDuLxgRuRo4NAs9c+F3KZcSg+B4cy6yyb +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +2A27FrJel7LwQVQbnj4q4hlgih3qBk24vCQh3ZEzZH1D9Te7T5q1mDbPLZp0o0v6 +/p6t8/yEODj2SpyYrFogNPdnJ598mYDI9OQG3SKrs2s0xEMb9jmts3FpPIdSfwUlsOkZH7GoGPHgLihjl0yD5koXY2E5+OCPEh6lFcT5FWGdmJWettOxpcoMW5CbJ+ym +/0ULpLqgTvInFD0r5hHANlt/A4HN48BaWWKSY1CtQvClxZHrVHBtcJRLbba3CP1w +9kMYV6mizqMbt4N+qTZtN4zB76eoGATkjA0lU5cQ2Ak= +1u+XjG/2+GSQRv6EzCaWRQ== +Nxw10FAZUk+m0nadC1hdy7a2+IPpKTLzQ6uOO+E1Dbs= +X/Lp4PsbbXulceSXUvGUpEwg42OTOSx1VxHM46BBNX0= +s2s/1ED0ebJ2NmFlE8kk02x6FisgSkWfiVYBaeNNhGvko7bwZ1FD5aZ3vGnu9kSm +1u+XjG/2+GSQRv6EzCaWRQ== +y+v+I4rQ8oQ+Y894tAwqPQ== +blmlG6qfvRPckY4P30JkN6n0giAGqVejsbTuRPj0rAA= +NnzeuyTPUXJo3DzO+hKDlpdmpF8keGQCo3mkfAR/FtjeJLOfkOWLB3iDATKP54pf2G2pJLJiXme7qXzDE1IPMA== +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YIReLG3rs8BVbRSwONMnP/2 +1u+XjG/2+GSQRv6EzCaWRQ== +p+qW8e8p1zrxOyU05vOA0oTKH5zqNz+PCgf62f9tApk= +G/hjuaHIfOdhg/DGAsPhcPOPTRXDiI3ZJx/D3q3b4P3lRQWSpZzKNexozsWlQSmt +1u+XjG/2+GSQRv6EzCaWRQ== +FFvLZRifY4ofMFjBuFasFxWXCYrK9vqCc5dq5VE0iw6DSlYwafvxu5DYwNYrMthu +1V2v8QerKOmubvSxgB4eTBZTEixtIaCeLifMkliDnl1FSzgrzmlMEyvFnWK3RGd/ +J1UGlXkKhKXD36OrXXhN8BVlTjNvt2FG2CiPqdl199JaWrFGr3c4jWvQXS12vU6gFbGt23o5BdLlHOVX3j/o5Q== +1u+XjG/2+GSQRv6EzCaWRQ== +2J/cJdNHXZ2Qsa37Fo6/Z/gM9h4l3NzxQe1VHPUoNCj50z035sIRNuNbaAptxn44 +0aOe1PRcA3iTQeyd7fu/UT3OH8TgnUZIetc0kIThlMY= +V79F1HDTm8z6yGnQm/hDJr2dkX3jv5pKaOjk74dknTfkNo8du33ifDa8c3SEgcfl +mWtqqlZvPd5u+MDOxVnfq7x81G9ia1eZdDpax7kDimPICwqRAAp4SD95uu4DP+3q +J1UGlXkKhKXD36OrXXhN8IJbvphSi+7vstAaJ7toEgm4zZMxFe/rKS4u9y2Ssvzh +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAcBxCAXlvfzQFBJgoPBTJbj +VmVrGQo2zRokW/ZuO9bN67iVlFAU8hAYMafjpVDx4oOy1GxC/YZz9MGGC5hXHifDIzzMbooqVq803bDkm/ITJg== +VmVrGQo2zRokW/ZuO9bN66Bl6UaWMaQpzwFzTULo/tY6jlTwP4gsV1QVwm+H2RE+ygziQ7qyYUv2jZGrZghIXhaI9IfXLM4+/7ZEaTQK0+JudXe43jFBuxq9uaQVanpR +F9Sftf1PoN5P+lgvc+r10DzCXpZBs2+NQLZrUg4+WkxCye8nWiOXw8mDAYUsbMktOZPiLEpeqFuRtHXGZ2OIbA== +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAeRwlwRNQErNfyGxNziVpfT +VmVrGQo2zRokW/ZuO9bN67iVlFAU8hAYMafjpVDx4oOy1GxC/YZz9MGGC5hXHifDCkvImVt+p1wORLo6xFoArA== +VmVrGQo2zRokW/ZuO9bN6xYoLnmVOS/ahrj5uuRdV0EWsnEizWRxlOrvM8wENlN4 +VmVrGQo2zRokW/ZuO9bN65e0KdmCuDXHyRDbH0mrOLKZuncKdgINFZSmlvo5RFWwygn3vSbFEX3KrOzxXciCHQemoYWznnH0DlDnPaok5SJghk4Gvzt1lP8Nd8xypH1gdpU29AuUlNmUasQNQYvk1Q== +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN65e0KdmCuDXHyRDbH0mrOLKZuncKdgINFZSmlvo5RFWwygn3vSbFEX3KrOzxXciCHQemoYWznnH0DlDnPaok5SLNfnGfLb1wbPgdBMy9o4VF+look+gYRrvZgUaelLl4lQ== +F9Sftf1PoN5P+lgvc+r10D7o64ZLbi78S/TgPxw/f4SOC++uRT8tIfgwb8Omo7vu6chE1yw4OMlsbZQVGt7v1YdICPBImfmbQa8l34QUTWP5nPueh8DOOl+YjipCgyfiah1QuO33ZZN9u1isdHNgaw== +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAdgqSKLQvYqIyYU7AJpR7fK +VmVrGQo2zRokW/ZuO9bN65X+HRZP26yr5JJcJjbroHgY2yQZCC2iz/EentJIOM14 +VmVrGQo2zRokW/ZuO9bN66Bl6UaWMaQpzwFzTULo/tY6jlTwP4gsV1QVwm+H2RE+U2dhdllU017qeNlwV/t3qMxoN3b6ZEwtypHeRVok5QLeq7jBhGNGeUsnEKGRoGiN +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN624P9C6dpnhq+uflbVX/QAebii1mp4p3I7SDA1b7UujH +VmVrGQo2zRokW/ZuO9bN65X+HRZP26yr5JJcJjbroHgY2yQZCC2iz/EentJIOM14 +VmVrGQo2zRokW/ZuO9bN66Bl6UaWMaQpzwFzTULo/tY6jlTwP4gsV1QVwm+H2RE+0ESEOFCVWY3zpdLKkxogIjfbS+mp5BcT25dPwPqol/ewrCICKac8rraGy87WhRhdSJ6N6R7PX4AbXUWbAtjbLA== +1D18KWr2hdVsBcdZx1OaPfvwrE7g7+awLCd9GHCg8UHyIrzitpsEab7Put5dPSX9 +VmVrGQo2zRokW/ZuO9bN654Optg5TGZB1gMGxxgScexFS1RR1Ks0UsELmW81E6uK +o1bT2zrSxs12aPyNKpukk+RBjtOXgYwyhl9BlbuoPII= +axUGGc1VN8059OpAvKXjQWop4g5wEilwDML73fTR0aROShh1A/PpsX89P62Fxwd4hZKM+zVAPfmRsWcP03u9Ku3VD03o8PL+LDYwCELmIUqQ9SwDTtOHpMHkHqyUOUm1 +lGY+HGJ/DlLufZHWWlj7UzT71dQgCsiwDOluCIjxD+k= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +OtFYf3L1SCFz3eV8uxxLQDcWdIw+tWys4K3uAhWrSOIHz+Hf+pmJOLXaeG2mKqnJ +Z8UsPk1Q7HtwjRd4g01ryw== +x1Dv0cSaQmavN9ft+ex9jCSwM0+ru2NYHY8G/siatT0= +Z8UsPk1Q7HtwjRd4g01ryw== +b4OJVZe8QyIpjuTpKXDL9A== +L0eUthVnpkGsmKFAX6d+uCuT47MPwULi2lpyJh4ryFDW9qwV9g5joBbVHwA9hMBSw4eJiwzIZ2bMkcNfWxzADeutkGRiY7X7EdjbySNl+rA= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN67Z4GyvH1s+PEZuudjntZcEEAEOgkqKv/lySCowLggPbbCf4J73tOz2c1J7SATG4GzNyDiMvQ17bky1qOfBwo79dgSMSwZ1eu3+WvIgk3sqo +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN64mARHpcJLoYD+9rQ7Ln85U= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +QdTjTxbShl+8mrARISGoAbKd/0Om5LDKQaniNR51X/Q= +KZTmaJLp+FU9X93j5Tpqtw== +hpr9H7QXrORqNT7P49jOY6IrtSknVO/GcflctAZsVFrYCUa5MPG1XS1Xvxi+13Qa +YThduOXrtrzATLnwayQWnYFkaOF5vtHROqD7i+WyeQF+2C5kHzeTIKqA+x7p3Smg +7JQQDTi0T2idhFjao4jEpc6eTmK7gx2xmUXSHS2TmYl+CMUIX3n8ZLAvm1PoDU1M +h2Amc56dmaRo3inGZxN9Nz8nhuDC+/7OFA7hKd2Ve2Q= +KZTmaJLp+FU9X93j5Tpqtw== +1u+XjG/2+GSQRv6EzCaWRQ== +h9U6jl4cZ4C22C1wpKu8vSM7bTtvy9o2g7Di3K7jTo2HJl2CG2yVZIJgod88bc9l +1u+XjG/2+GSQRv6EzCaWRQ== +Mtl1kt9g1gV2KJvyBZ2K8tOa6EEkDwaXESGA9zv8nLEBgBW5UcdKSZH2enp+Ljnf +mcQwVxjUw79rwGN6NopbQ8o+o0Oqap86qulYzVaU1YK+xvdtabAjOdZa8rH/UawJ +VIHA31zdnuwVlvUv3VOlYVRNGNAMqDxg7g3P55Yig7XGJ0frgqBbhflLu9pZDsGtG7PWJFzYnbAhv0HasJD0eg== +X/Lp4PsbbXulceSXUvGUpDfqLabbqgG4MZE+Cdy4FwlQTZqFS7MLKRTLDMwaDvILIRxQybc5sD1eU9BOwIwsyw== +1u+XjG/2+GSQRv6EzCaWRQ== +HTOfkyRnnkvrq92C2ykyOPO4d7gErRgYL7aScZTpzxREn6lD4QDtxAH0B30SgN6E3F22VdO8GzELDUsICPiSeg== +P8KbMTjfuhARpOL8NoffNro45O1U8oEoZJkv8WBE9IrllSlHFr8oQ8frOkOgpbVNE8ApvoZT6kfHiImzMYVOag== +L0eUthVnpkGsmKFAX6d+uFjjhUy+MCKFNT4NrCL5yNnhcA/pEoLZALqem+hbKHoY +1u+XjG/2+GSQRv6EzCaWRQ== +P8KbMTjfuhARpOL8NoffNom7jch4ZQTU5jfIo7wG8ScR4XnHaBOnM0U+2vh4JNta +L0eUthVnpkGsmKFAX6d+uFjjhUy+MCKFNT4NrCL5yNmWWojcHbeJQ61OMhTGFdlC +1u+XjG/2+GSQRv6EzCaWRQ== +i4c/4ocK9c+0kOv/NeukhzRCRIDY4b1IcwhCyS6fwKaxL1iz5YdtgQ8ur54xnSve +89eRayX3G8tCX3FDU5IcjcECGHtpMfMxvoO/SDLARSENffTD+JaXDY9RnayEnHLnlI8b7pgVZ6VRMA8KffpRHg== +VmVrGQo2zRokW/ZuO9bN67AUBZ3/XEKHpa/wOGbEOnmtIF6uAz0tBlf7rcOxUQPuytPu9bJw1t4kl86ncxKWUr6UFGd4R8GkpK1204AYmG4bJSHkhwXQJ4SNyep2Orx8zpIZXaQwin0CO9B7uaPk1A== +1u+XjG/2+GSQRv6EzCaWRQ== +swRuTHNu5hN+oxWHfH2uTRaafzF+WZloRON/s6FBnLM= +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANgyhe0+ApeojOmIJni82PRs= +G/hjuaHIfOdhg/DGAsPhcJ0dAhlHHuuDGrGW0z9/mG1YgeQhelMeN8fM9H05Q7bFoBNC//YD8IgCAMKgkLtISr7jJb8ysmAn68sYqB3zYKY= +XEKtPwhadR+kpKQN/+UTabdcOsvQ70heGQXJNuzLidaKrkdw//NbtiPyzZIdYJRwkO1XfoVngfneyEI34hiEIjZDJsEWIi+RYy+5PJ52y4M= +1u+XjG/2+GSQRv6EzCaWRQ== +6P8mKfouOK1hwXy3MrrCzYNv1bj48gIxdX69IR058kk= +2J/cJdNHXZ2Qsa37Fo6/Z45HJGTQKJ2XarF9YWFlYi/NS7Y1/C8WI42zDRQS65sB +K/3TjoKXtlVKQEXdtPO0urKjWEFV9hqYUzZS4hx2t3vgmB4P0sEnPBB9+cxnHYt4 +VmVrGQo2zRokW/ZuO9bN6zPTlJYqiIwYIHWMLx6MVG4A/jqaX4tahdPmgshFSQG4 +VmVrGQo2zRokW/ZuO9bN60KbPALjpXkxdnMsVQY2qiQ2mn3irY/rZFqtKYoHLxRE +VmVrGQo2zRokW/ZuO9bN60Xvz4OmVOCjpkbTCNQIATsOCVbKnePG0cTDCTWdue9j +VmVrGQo2zRokW/ZuO9bN66guoSIjBQ2hp+h2iWlRadc= +VmVrGQo2zRokW/ZuO9bN6855zfg/ZnrwyfrD1ClJJctc8wQUc7t8LPNPEB5FCa4H +VmVrGQo2zRokW/ZuO9bN6yrrO27yfi+aG1SaNFn36A8CnO5yCexFXRxrEFBi6HH8 +VmVrGQo2zRokW/ZuO9bN6/m/X7phwv/rEzEofJRELW/qETCx/ewa2vv2I76GwEWB +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN6/m/X7phwv/rEzEofJRELW+ZhbjEfjOJN1QgOkdHcgfg +VmVrGQo2zRokW/ZuO9bN69t0x0NhvKGOHr2+EEBw8Jek/n491mR4LKuB/UuUgzMUy7YY4ELecU+vhof4LvKC/Bhkvz+bT+jSRSy/qFVu/F0= +VmVrGQo2zRokW/ZuO9bN6wIR+5HpCjcoa898tUp4mpJqO8Na+qweZg8vpjfBiLDS +F9Sftf1PoN5P+lgvc+r10HYBS8twYBi0U/DCh6ocj+X6RD0HNlSpwv37yho4nl4Ot2LOLOWFJbMnayi7wAK30w== +VmVrGQo2zRokW/ZuO9bN67yPVwtUvdC1tj4awelda/fkZ9dUXYCEUeCyL3Yrqyn/ +VmVrGQo2zRokW/ZuO9bN60Xvz4OmVOCjpkbTCNQIATsXOlREGrlEeF/zy2qslZ1j +VmVrGQo2zRokW/ZuO9bN67HiZr3Mu5Xx/pLDDclKRz8= +VmVrGQo2zRokW/ZuO9bN60KbPALjpXkxdnMsVQY2qiQyq2q2A/0gHrZHUNJvsm2k +VmVrGQo2zRokW/ZuO9bN69t0x0NhvKGOHr2+EEBw8JfSNIZvkvl8jIdEvRQwBwJ8qc6p773V8w3sbnJ7sNRCjGOJmoa2y21Lbs/qYLxw7ZE= +VmVrGQo2zRokW/ZuO9bN6wIR+5HpCjcoa898tUp4mpJqO8Na+qweZg8vpjfBiLDS +F9Sftf1PoN5P+lgvc+r10NgITD/y7z0I4C2wYBhOQf9kEr4e0+0VrZmNsbWDAMY531ZzaC8lUc4x5xBNRPYlTw== +VmVrGQo2zRokW/ZuO9bN6yH86FWoLic2FxcRUV4zebxf0/M2UvczoadL9jeQKWgM +VmVrGQo2zRokW/ZuO9bN69so/u5P+e4KbXGeD0Dm7to/lSyHyW7MicMJ0jDa2eXr +VmVrGQo2zRokW/ZuO9bN6xQjxHnk9JeJcV3ebvE1Z2ZFylERkWn2HmPyx+oo40NY +VmVrGQo2zRokW/ZuO9bN60KbPALjpXkxdnMsVQY2qiQQ1X8tQ+wf4ZCJUp0oMXLr +VmVrGQo2zRokW/ZuO9bN69t0x0NhvKGOHr2+EEBw8JfSNIZvkvl8jIdEvRQwBwJ8qc6p773V8w3sbnJ7sNRCjGOJmoa2y21Lbs/qYLxw7ZE= +VmVrGQo2zRokW/ZuO9bN6wIR+5HpCjcoa898tUp4mpJqO8Na+qweZg8vpjfBiLDS +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6zPiagaoQNUZ6Q8MukMCasPCfaBG6YJFzlL84vUM1kV8 +VmVrGQo2zRokW/ZuO9bN69eu9LBWEB6PDbGO+0TyWwBhVpoFqMPkT8HlHX8RMHrR+75Z9K8m2JUCWTBuR0Tk9Q== +1u+XjG/2+GSQRv6EzCaWRQ== +o6QhOIN2Sc4SHELnst17uXpnZ5EAUQuq8Epej7P3sTRveRgNRdPH82tKZu7g4Eiq +1u+XjG/2+GSQRv6EzCaWRQ== +Ic1WBp01nLyRxbqJnsu2pypx+Gb9M9+y4CZIw5enWs+glmD/7BTlR5C7Ujg9dOtQxQc0uyS/kT/ILSqYhwQGNA== +b4OJVZe8QyIpjuTpKXDL9A== +KToqm5Or/ymuVO66QU/2pu7oKyhGYWQ+0pgKsF2sKsQ= +S76gqn57AVIGnu8xVYuZaO27N6yYhZ4GNoiBGQP9DKg= +K9/ExlRvl0KpaOPuc49gtg1nR3Bc0+g9UN7uwKhXbdM= +wlLHv6kT3Q/RmtMBN4nDAVircipW/U7HQDc2UPUyQ/vmRhc3HaKPFi1NtnisK6xX +VmVrGQo2zRokW/ZuO9bN64JOsgztOsHxYHKGCeX474Qcqbriq9LDc2GKDAaXpEva +VmVrGQo2zRokW/ZuO9bN6173lsXiCfzKN2E9+9sH2KYDQWUma1tCh1mCQLO6go9pew9ww96U7ujnre6SjyCuxQ== +VmVrGQo2zRokW/ZuO9bN6/EQSa+RVcntmzafI6w4hrXeQefCDpqhGzMMDJocinCp +VmVrGQo2zRokW/ZuO9bN64ViM2XLR1EOvB6UVgIoYuF8fdMuzlzZir170SdeoO1N +VmVrGQo2zRokW/ZuO9bN6/EQSa+RVcntmzafI6w4hrVZKx2YSK2fHbBecH4ladDU +VmVrGQo2zRokW/ZuO9bN66vpbXlJpZkCo8bNa8hfdTtD0OOBen8N9PlZvb6sHluL +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +KToqm5Or/ymuVO66QU/2puiKplVgTGTPZqNeAeKoYCRnGqCo0URmUW6QuDrFvxhR +S76gqn57AVIGnu8xVYuZaLg1MWy1KmN4+LZJN+yDmqCMssHrDpVNECVIttB2y5oe +K9/ExlRvl0KpaOPuc49gtoyfPotIPHp16XgJV6eRYyrbA+h8XwwZ0jIgIeiV5WJH +K/3TjoKXtlVKQEXdtPO0uuuN7F0BMuO/Q3cMv6UmDSFakb80AgleKsv9apABSHy8BJ9UVzoo8dLM78KhLfy4Pw== +VmVrGQo2zRokW/ZuO9bN65LhaN63q3ApjnzlgAtjYqw= +F9Sftf1PoN5P+lgvc+r10C1yj1HZBpntAx/i13/oYeYaK3wfHrLLbHW7duBhTqbF+VuvZwLaBegIbS5rvj5IKg== +VmVrGQo2zRokW/ZuO9bN65ZTH8ABglrqk9nDO7nL0rs= +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN64De6ITZeavbNwRiqFkMysk= +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +L0eUthVnpkGsmKFAX6d+uAST/oojvSe5vOTCDj9ZTSci5aesa7iYDqIo84xNyUjr +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +9OQ4F/6xMvSNwNUGdXyQ9F1NqMgCz0f8DrpmEaOqG0E= +kkhBrrZfa9Lz2ARtxqQ9WI07s+twubQO6K+lkziI1UrpPBayzeeEXSMVuzem4Nzq +UxWVPzm6Eag/KO1azIHUdx+idAdS+rfPARzR88tVbgYdpuz2ULTF+2vmnBiei7OW +Lb802TYIO8cTPX7RtIYGZ+6eFx+9GqXwRvUIq98SK68Eo/uW/nIxVIKX8k1Y1OBk +b4OJVZe8QyIpjuTpKXDL9A== +I5csN8e3J/KIFkMa5t+SJCqL3g3fUtFpMCVgdY7uBLC2hDdwb73LgrQLVQ3aMtK6 +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033ZLNQ2XZF4KsFXQ0AJ186dTqy0tFe9yVaa5Nvi4GzvRQQ== +UiubnxD7y7+jD/CLXy/lIzUv+EQIUnxp2+tRaByCudDYzFPn13pFELSDNAgNf0aY +wI4EEWzdSnynhqFxMCzFmvtQa54ffiTU8OKIuM+mUtbRQW9n1JFAWLuXCST3voAI +GL/yLvMwjflJNfDPl5ASjQ2UrwSPDpUhyRkgeiXFg2U8KeI5JLJSQLyyHw7AauiQ +P5fFy0VqKNQcixZC6/gotxklJeUYBp6qUkCM0szi5po= +3HDkd2BFYLPk4K73U5D+BYdx+YPBx8edbX2mUEZUhcw= +gZ7BoIAABc+I8g/rYzq+fw0QRhyOskLowTpBMxzNkkXJfHhElpIZe5wsCdqDFCZvGpC72uDw/Cu+D1FuT4o1ew== +NhnIR3Ilo4H2su9/cTNo/AxXtCXBAtO/DWPyG38GkWo= +SXXsDW1aLF5ljPyJUoRc7t2uV1Ea/KDPsubfQRA1/g0= +i/p1vQ7ugXGBvwbjECApz/Cr9YvVAiEjIabMp4aMpkk= +zo+FVrlHkUeZ8fJX4PVhAH4W08MspSsE1ssI18UNPaA= +wlLHv6kT3Q/RmtMBN4nDAaQcqH83V4KNJSetgkXN2mpJpLhuyLAaUNt+tKkzaLSI +VmVrGQo2zRokW/ZuO9bN632VflaKbWaV9Wa8IOB78Qk= +VmVrGQo2zRokW/ZuO9bN608eo5JjqlJJ+hYuirBCFcmf4cKrfv149X3GuxINE1xDDJnAj+I17VZiW+JCiwfRwA== +VmVrGQo2zRokW/ZuO9bN67FMFupuL9NnhZUvLdV1qH1jDx/7tB6qL1SG7Pqfwsin +VmVrGQo2zRokW/ZuO9bN65qbK6/I8bncNNV3Fr84lp9CCx8VYIApMdX4h8SHWyk2 +VmVrGQo2zRokW/ZuO9bN6zEL40rbK+o6j49cNVbzKqOsExNYZ+R8lUnZEGfiuhXY +VmVrGQo2zRokW/ZuO9bN6+EP/UfvFvq8hDmkfhk2A+qnnPSHBLYJiyMbFizN9aWEoBeZFZoLhXy50+Ts9Ur1/A== +VmVrGQo2zRokW/ZuO9bN6+EP/UfvFvq8hDmkfhk2A+pmn/JN+SNJ6dRlNT3kh3N/3tDs7zMtD+kCYf9bfutOkQ== +VmVrGQo2zRokW/ZuO9bN60xwqMjDC3vPo6L4vEr/pBaJQkw/7oJUptDcMB6Fr5Cj4S+qQ9QZRL24FoS5lS3+qQ== +VmVrGQo2zRokW/ZuO9bN61G/tYB+vcuOiiaR8UbkscqXrSG+YeXqyTcHAiaZKnMP/R8AE4EFWhpiT+I3n+NNhw== +VmVrGQo2zRokW/ZuO9bN6zDu/0cPxO8iIAxo/MyAOyO3mi+GHsPcbGU9H8sA9FWz +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm5Kes26rz2vkjDRpZY9y8CG+MKLyqwcOLQaC8uFAcQhA== +VmVrGQo2zRokW/ZuO9bN64ApqCXcODvD57AL9oh19dsQTwsNrJDaw5mMC3iBkcYF +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkmssm/VPICxxxORTX5OIKSeBFVbEgJs/oO6D+R5qX3b7w== +VmVrGQo2zRokW/ZuO9bN69/FjVndCR4PL8NhQnFk2Gew+8ZLbD2plI32rzUojOkk +VmVrGQo2zRokW/ZuO9bN69IgslanOR1qBWJ7u9f8kApHl7m/3x/YUX5TmvRhoWWrhrEvRs3e6Vo03iY4FZoGJQ== +VmVrGQo2zRokW/ZuO9bN6yAU/fW49cRu9QdgvjUnDRs= +VmVrGQo2zRokW/ZuO9bN69qqX19RJUeMAQ5O8TuJCvc= +VmVrGQo2zRokW/ZuO9bN69jIxF5VISZtC2vxWRknx7k= +VmVrGQo2zRokW/ZuO9bN6w7SLxNKr+6YhBMvtqYtIS5B39SOQ9awYMdHGNN7g2HF +VmVrGQo2zRokW/ZuO9bN61ftGV3vPtB2WjF6YrN/0jNHYRNif5kQHZWzQEGp4Mrw +VmVrGQo2zRokW/ZuO9bN68i9AQUAyLa5Izsu2k/9DPtGLI71suphKNgMpp1nHjp3 +VmVrGQo2zRokW/ZuO9bN60FmyZq9YoZep5W4V0xLs2VJw4EwR4WqghfwBKZHKkzFW5QCRYRsMStsmunHScuj3A== +VmVrGQo2zRokW/ZuO9bN6zw+wLmacHPHBj00bNmIGZaQr03AAq32yLrsoy6EYs3C +VmVrGQo2zRokW/ZuO9bN67c57efC+E7a6G1EN/4VXAUA5IVAU8G/FAp8AnNhV+8pDSYVAZUG09Nqr+7c8CWyvg== +VmVrGQo2zRokW/ZuO9bN6x9SPCVoe4PZoqZ6VqYN93GZvfLrMfeSjpwDGE6Mez3z +VmVrGQo2zRokW/ZuO9bN6yf4ml6i++/GqH/wSBe2JUimuAJuO3PPz0XEqqC+kEwmq2P2CCy/D5JDk3aKmyunXg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yZnMkqRvl1W1z1JA43ooyLsysqnBW0cSn//FgR0VFUn +VmVrGQo2zRokW/ZuO9bN6zw+wLmacHPHBj00bNmIGZaQr03AAq32yLrsoy6EYs3C +VmVrGQo2zRokW/ZuO9bN66U3/6225IbLyIW0FVhZRNEyLZurMXbjR98/i0DXR7zf +VmVrGQo2zRokW/ZuO9bN6xaf2vdYGOU85+dskGN5Zti8/wz7YWu3y/jTkefW+oHw +VmVrGQo2zRokW/ZuO9bN6xkyZls2bS2c0K0h3aqZRdZrjes6+0hX3Wr6T+0mnqitWU1y3v2I2Wt4PYH0XBjT6w== +VmVrGQo2zRokW/ZuO9bN62zrDNonCkxA5BeQHJAPUOvVeRTu47AJZhJxjTJXUg6D +QPiJZBcZo3Zu5kDhrrGzbI3GQxTKN1tlSYOd4wqk4Qk= +o6QhOIN2Sc4SHELnst17uYG57hDyRaN+v5d46f3m9TtvuM1edZME3sYQm1fwKXSE +1V2v8QerKOmubvSxgB4eTM+LVbDQYpuIwwcEKVLExe6MC8BJb9x5uoyJJlFncEM6rY6CjjYEoI1lGYiKE1nBng== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +o6QhOIN2Sc4SHELnst17ucZEhQfEUee5E6C4gEbgFRMehTYHPr1sbQ4k/qB7LJ8r1OhV8ece9Zkwgx68RDZgODP4asgDCZmEmIilOSuj+JI= +ACEEdgL/kNbx7RaZcSRxZClHBggmOTwuoukB6MgtasIiEFle6fCvd15573nsLbI9 +VmVrGQo2zRokW/ZuO9bN67Pocv3BMn87TAB+S8aqZRSL3g30PRHmEkGas7a6CXo4 +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN61efkqHAT/xspGlYy7KhUnGfupbfN6kadJV1FzZSIvpa +VmVrGQo2zRokW/ZuO9bN6+b0SyPGp1VLLfFreWS32CdvJQ0lilsNKGrnKkyex2l6ZoEN1nB9tzGcgrcEFt4mWg== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN6zpJcGQDok2bzZbwZ5+2z7FMKwUs1r6SjvZQDsl9E9LF +VmVrGQo2zRokW/ZuO9bN65HmUSl6PKrPLdi3pIfWJgEYDxdXHdYYYJAWxRhWjvkMURIUYeBTgS8tgmIZ7YENDxgg1Zs4OdrlyVu+VDCE+oc= +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN65HmUSl6PKrPLdi3pIfWJgGfllFXAtMFaX/YohJyQyfhm+vA31HUxmdUtlkayzAZUKQbFld81ekbiffgRsrQqQB1xVb1v/rxv/O8tBET/TaPCdwsyclhJhXcaG/6gaDkZg== +L0eUthVnpkGsmKFAX6d+uA6wgjrZ2VgEjfvSiyQz6oysGP6R543HWJTxMrxe6FdC +6ZPJI/HSoc4xA2zncU65Fm80XRief2SLbJIcIhJc9eE= diff --git a/class_v2/safe_warning_v2/sw_alias_ls_rm.py b/class_v2/safe_warning_v2/sw_alias_ls_rm.py new file mode 100644 index 00000000..b1d3b31b --- /dev/null +++ b/class_v2/safe_warning_v2/sw_alias_ls_rm.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, re, public + + +_title = 'Check alias configuration' +_version = 1.0 # 版本 +_ps = "Check if the ls and rm commands set aliases" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_alias_ls_rm.pl") +_tips = [ + "Add or modify alias ls=\'ls -alh\' and alias rm=\'rm -i\' in the file [~/.bashrc]", + "Execute [source ~/.bashrc] to make the configuration take effect", +] +_help = '' +_remind = 'This scheme can make ls command list more detailed file information and reduce the risk of rm command deleting files by mistake, but it may affect the original operation habits.' + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + # 存放配置不当的命令,分别用正则判断是否配置别名 + result_list = [] + cfile = '/root/.bashrc' + if not os.path.exists(cfile): + return True, 'Risk-free' + conf = public.readFile(cfile) + # rep1 = 'alias(\\s*)ls(\\s*)=(\\s*)[\'\"]ls(\\s*)-.*[alh].*[alh].*[alh]' + # tmp1 = re.search(rep1, conf) + # if not tmp1: + # result_list.append('ls') + rep2 = 'alias(\\s*)rm(\\s*)=(\\s*)[\'\"]rm(\\s*)-.*[i?].*' + tmp2 = re.search(rep2, conf) + if not tmp2: + result_list.append('rm') + if len(result_list) > 0: + return False, '{} The command does not have an alias configured or is configured incorrectly'.format('、'.join(result_list)) + else: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_audit_docker.py b/class_v2/safe_warning_v2/sw_audit_docker.py new file mode 100644 index 00000000..e03285f0 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_audit_docker.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + +_title = 'Whether to enable Docker log audit check' +_version = 1.0 # 版本 +_ps = "Whether to enable Docker log audit check" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-13' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_audit_docker.pl") +_tips = [ + "Add -w /usr/bin/docker -k docker from [/etc/audit/rules.d/audit.rules] file", + "Restart auditd process: systemctl restart auditd" +] +_help = '' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + audit_path = '/etc/audit/audit.rules' + if not os.path.exists(audit_path): + return False, 'Risky, the auditd audit tool is not installed' + # auditctl -l命令列出当前auditd规则,匹配是否有对docker做审计记录 + result = public.ExecShell('auditctl -l')[0].strip() + rep = '/usr/bin/docker' + if re.search(rep, result): + return True, 'Risk-free' + else: + return False, 'Risky, the docker audit log is not enabled' + diff --git a/class_v2/safe_warning_v2/sw_audit_log_keep.py b/class_v2/safe_warning_v2/sw_audit_log_keep.py new file mode 100644 index 00000000..90d70b5f --- /dev/null +++ b/class_v2/safe_warning_v2/sw_audit_log_keep.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + + +_title = 'Audit logs are kept forever' +_version = 1.0 # 版本 +_ps = "Check whether the audit log is automatically deleted when it is full" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-15' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_audit_log_keep.pl") +_tips = [ + "In [/etc/audit/auditd.conf] max_log_file_action changes ROTATE to KEEP_LOGS", + "Restart auditd service: systemctl restart auditd" +] +_help = '' + + +def check_run(): + cfile = '/etc/audit/auditd.conf' + if not os.path.exists(cfile): + return False, 'Risky,The auditd audit tool is not installed' + result = public.ReadFile(cfile) + # 默认是rotate,日志满了后循环日志,keep_logs会保留旧日志 + rep = r'max_log_file_action\s*=\s(.*)' + tmp = re.search(rep, result) + if tmp: + if 'keep_logs'.lower() == tmp.group(1).lower(): + return True, 'Risk-free' + return False, 'The current max_log_file_action value is {}, it should be KEEP_LOGS'.format(tmp.group(1)) diff --git a/class_v2/safe_warning_v2/sw_bashrc.py b/class_v2/safe_warning_v2/sw_bashrc.py new file mode 100644 index 00000000..a6129703 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_bashrc.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 用户缺省权限检查 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = '[/etc/bashrc] User default permission check' +_version = 1.0 # 版本 +_ps = "/etc/bashrc User default permission check" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_bashrc.pl") +_tips = [ + "[/etc/bashrc] The umask set in the file is 002, and it is recommended to set it to 027", + "Solution: Modify the /etc/bashrc file permission to 027", +] +_help = '' +_remind = 'This scheme can strengthen the protection of system user rights, but it may affect the original operation habits.' + + +def check_run(): + # 判断是否存在/etc/profile文件 + if os.path.exists("/etc/bashrc"): + # 读取文件内容 + profile = public.ReadFile("/etc/bashrc") + # 判断是否存在umask设置 + if re.search("umask 0",profile): + # 判断是否设置为027 + if re.search("umask 027",profile): + return True,"Risk-free" + else: + return False,"umask is not set to 027" + else: + return False,"umask not set" \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_bootloader_mod.py b/class_v2/safe_warning_v2/sw_bootloader_mod.py new file mode 100644 index 00000000..8f8d5f9d --- /dev/null +++ b/class_v2/safe_warning_v2/sw_bootloader_mod.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# coding: utf-8 + +import sys, os, public +_title = 'bootloader Configuring permissions' +_version = 1.0 # 版本 +_ps = "bootloader Configuring permission checks" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-15' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_bootloader_mod.pl") +_tips = [ + "Configure secure permissions for grub according to the file suggested by the risk description", + "If grub2, then: chmod 600 /boot/grub2/grub.cfg、chown root /boot/grub2/grub.cfg", + "If grub, then: chmod 600 /boot/grub/grub.cfg、chown root /boot/grub/grub.cfg" +] +_help = '' +_remind = 'This scheme can strengthen the server grub interface protection, further prevent external intrusion server.' + +def check_run(): + dir_list = [ + ['/boot/grub2/grub.cfg', 600, 'root'], + ['/boot/grub/grub.cfg', 600, 'root'] + ] + # 存放没有配置权限的文件 + not_mode_list = [] + for d in dir_list: + if not os.path.exists(d[0]): + continue + u_mode = public.get_mode_and_user(d[0]) + if u_mode['user'] != d[2]: + not_mode_list.append("{} Current permissions: {} : {} Security permissions: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + if int(u_mode['mode']) != d[1]: + not_mode_list.append("{} Current permissions: {} : {} Security permissions: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + if not_mode_list: + return False, 'The following critical file or directory permissions are incorrect:{}'.format('、'.join(not_mode_list)) + else: + return True, "Risk-free" diff --git a/class_v2/safe_warning_v2/sw_chmod_sid.py b/class_v2/safe_warning_v2/sw_chmod_sid.py new file mode 100644 index 00000000..b53e7559 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_chmod_sid.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, public + + +_title = 'Check for files with suid and sgid permissions' +_version = 1.0 # 版本 +_ps = "Check important files for suid and sgid permissions" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_chmod_sid.pl") +_tips = [ + "Use the chmod u-s/g-s [filename] command to change the permissions of the file", +] +_help = '' +_remind = 'This scheme removes the special permissions of important files, which can prevent intruders from using these files for privilege escalation.' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + exist_list = [] + # 列出重要文件,先判断是否存在 + file_list = ['/usr/bin/chage', '/usr/bin/gpasswd', '/usr/bin/wall', '/usr/bin/chfn', '/usr/bin/chsh', '/usr/bin/newgrp', + '/usr/bin/write', '/usr/sbin/usernetctl', '/bin/mount', '/bin/umount', '/bin/ping', '/sbin/netreport'] + for fl in file_list: + if os.path.exists(fl): + exist_list.append(fl) + # find命令-perm 判断是否有suid或guid,有则返回该文件名 + result_str = public.ExecShell('find {} -type f -perm /04000 -o -perm /02000'.format(' '.join(exist_list)))[0].strip() + result = '、'.join(result_str.split('\n')) + if result: + return False, 'The following files have sid privilege, chmod u-s or g-s remove sid bits: \"{}\"'.format(result) + else: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_chmod_stickybit.py b/class_v2/safe_warning_v2/sw_chmod_stickybit.py new file mode 100644 index 00000000..b64a4422 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_chmod_stickybit.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, sys, public +_title = 'Check temporary directory for sticky bit' +_version = 1.0 # 版本 +_ps = "Check if the temporary directory has the sticky bit permission set" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_chmod_stickybit.pl") +_tips = [ + "Use the chmod +t [file name] command to modify the permissions of the file", +] +_help = '' +_remind = 'This solution prevents system users from accidentally deleting files on the server. ' + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + # result_list存放未配置粘滞位的目录名 + result_list = [] + tmp_path = ['/var/tmp', '/tmp'] + for t in tmp_path: + # 文件不存在则跳过,保险操作。 + if not os.path.exists(t): + continue + result_str = public.ExecShell('find {} -maxdepth 0 -perm /01000 -type d'.format(t))[0].strip() + if not result_str[1]: + result_list.append(t) + if result_list: + result = '、'.join(result_list) + return False, 'The following directories do not have sticky bit permissions set:{}'.format(result) + else: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_cshrc.py b/class_v2/safe_warning_v2/sw_cshrc.py new file mode 100644 index 00000000..24c681a0 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_cshrc.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 用户缺省权限检查 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = '[/etc/csh.cshrc] User default permission check' +_version = 1.0 # 版本 +_ps = "[/etc/csh.cshrc] User default permission check" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_cshrc.pl") +_tips = [ + "[/etc/csh.cshrc] The umask set in the file is 002, which does not meet the requirements. It is recommended to set it to 027", + "The operation is as follows: Modify umask to 027", +] +_help = '' +_remind = 'This scheme can strengthen the protection of user privileges on the system. ' + +def check_run(): + # 判断是否存在/etc/profile文件 + if os.path.exists("/etc/csh.cshrc"): + # 读取文件内容 + profile = public.ReadFile("/etc/csh.cshrc") + # 判断是否存在umask设置 + if re.search("umask 0",profile): + # 判断是否设置为027 + if re.search("umask 027",profile): + return True,"Risk-free" + else: + # return False,"umask not set to 027" + return True, "Risk-free" + else: + # return False,"umask not set" + return True, "Risk-free" \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_cve_2019_5736.py b/class_v2/safe_warning_v2/sw_cve_2019_5736.py new file mode 100644 index 00000000..f25b9599 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_cve_2019_5736.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, public, re + +_title = 'CVE-2019-5736容器逃逸漏洞检测' +_version = 1.0 # 版本 +_ps = "检测CVE-2019-5736容器逃逸漏洞" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-27' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_cve_2019_5736.pl") +_tips = [ + "docker version查看docker版本是否小于18.09.2,runc版本小于1.0-rc6", +] +_help = '' +_remind = 'An attacker can use this vulnerability to gain access to the server. ' + +# https://nvd.nist.gov/vuln/detail/CVE-2019-5736#match-7231264 +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + docker = public.ExecShell("docker version --format=\'{{ .Server.Version }}\'")[0].strip() + if 'command not found' in docker or 'Command not found' in docker: + return True, 'Risk-free,docker is not installed' + if not re.search(r'\d+.\d+.\d+', docker): + return True, 'Risk-free' + docker = docker.split('.') + if len(docker[0]) < 2: + return False, 'Risky,The current docker version has security risks and needs to be upgraded to a safe version' + elif int(docker[0]) < 18: + return False, 'Risky,The current docker version has security risks and needs to be upgraded to a safe version' + elif int(docker[0]) == 18: + if int(docker[1]) < 9: + return False, 'Risky,The current docker version has security risks and needs to be upgraded to a safe version' + elif int(docker[1]) == 9: + if int(docker[2][0]) < 2: + return False, 'Risky,The current docker version has security risks and needs to be upgraded to a safe version' + else: + return True, 'Risk-free' + else: + return True, 'Risk-free' + else: + return True, 'Risk-free' \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_cve_2021_4034.py b/class_v2/safe_warning_v2/sw_cve_2021_4034.py new file mode 100644 index 00000000..02bed4c7 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_cve_2021_4034.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# CVE-2021-4034 polkit pkexec 本地提权漏洞检测 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import public, os +_title = 'CVE-2021-4034 polkit pkexec Local Privilege Escalation Vulnerability Detection' +_version = 1.0 # 版本 +_ps = "CVE-2021-4034 polkit pkexec Local Privilege Escalation Vulnerability Detection" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_cve_2021_4034.pl") +_tips = [ + "Update polkit components" +] +_help = '' +_remind = 'There is a risk to upgrade the software version. It is strongly recommended that the server do a snapshot backup first, in case the operation fails to restore in time! ' + + +def check_run(): + ''' + @name CVE-2021-4034 polkit pkexec 本地提权漏洞检测 + @time 2022-08-12 + @author lkq@bt.cn + ''' + + st = os.stat('/usr/bin/pkexec') + setuid, setgid = bool(st.st_mode & stat.S_ISUID), bool(st.st_mode & stat.S_ISGID) + if not setuid: return True, 'Risk-free' + + redhat_file = '/etc/redhat-release' + if os.path.exists(redhat_file): + data=public.ReadFile(redhat_file) + if not data:return True, 'Risk-free' + if data.find('CentOS Linux release 7.') != -1: + polkit=public.ExecShell("rpm -q polkit-0.*") + if not polkit[0]:return True, 'Risk-free' + polkit_list = polkit[0].strip() + if polkit_list.find('polkit-0') != -1:return True, 'Risk-free' + p = polkit_list.strip().split(".") + if len(p)<2:return True, 'Risk-free' + if p[1] == '112-26':return True,'Risk-free' + p2=p[1].split("-") + if p2[1] <26: + return False, 'Please update polkit' + return True,'Risk-free' + #CentOS 8.0 + elif data.find('CentOS Linux release 8.0') == 0: + polkit = public.ExecShell("rpm -q polkit-0.*") + if not polkit[0]: return True, 'Risk-free' + polkit_list = polkit[0].strip() + if polkit_list.find('polkit-0') != -1: return True, 'Risk-free' + # Centos 7 + p = polkit_list.strip().split(".") + if len(p) < 2: return True, 'Risk-free' + if p[1] == '115-13': return True, 'Risk-free' + p2 = p[1].split("-") + if p2[1] < 13: + return False, 'Please update polkit' + return True, 'Risk-free' + elif data.find("CentOS Linux release 8.2") == 0: + polkit = public.ExecShell("rpm -q polkit-0.*") + if not polkit[0]: return True, 'Risk-free' + polkit_list = polkit[0].strip() + if polkit_list.find('polkit-0') != -1: return True, 'Risk-free' + # Centos 7 + p = polkit_list.strip().split(".") + if len(p) < 2: return True, 'Risk-free' + if p[1] == '115-11': return True, 'Risk-free' + p2 = p[1].split("-") + if p2[1] < 11: + return False, 'Please update polkit' + return True, 'Risk-free' + elif data.find("CentOS Linux release 8.5") == 0: + polkit = public.ExecShell("rpm -q polkit-0.*") + if not polkit[0]: return True, 'Risk-free' + polkit_list = polkit[0].strip() + if polkit_list.find('polkit-0.115-12') ==0: + return False, 'Please update polkit' + return True, 'Risk-free' + return True, 'Risk-free' \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_cve_2022_2068.py b/class_v2/safe_warning_v2/sw_cve_2022_2068.py new file mode 100644 index 00000000..8903bf5f --- /dev/null +++ b/class_v2/safe_warning_v2/sw_cve_2022_2068.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + +_title = 'CVE-2022-2068 OpenSSL任意命令执行漏洞检测' +_version = 1.0 # 版本 +_ps = "CVE-2022-2068 OpenSSL任意命令执行漏洞检测" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_cve_2022_2068.pl") +_tips = [ + "升级OpenSSL至最新版本或是安全版本", + "1.0.2zf、1.1.1p、3.0.4及以上版本", +] +_help = '' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + # https://nvd.nist.gov/vuln/detail/CVE-2022-2068#range-8768393 + openssl = public.ExecShell("openssl version")[0].strip() + openssl = openssl.split(' ')[1] + openssl = openssl.split('.') + if openssl[0] == '1': + if openssl[1] == '0' and openssl[2][0] == '2': + if len(openssl[2]) < 3: + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + elif not openssl[2][1].islower(): + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + elif openssl[2][1] < 'z': + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + elif openssl[2][1] == 'z' and openssl[2][2] < 'f': + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + else: + return True, 'Risk-free' + elif openssl[1] == '1' and openssl[2][0] == '1': + if len(openssl[2]) < 2: + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + elif not openssl[2][1].islower(): + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + elif openssl[2][1] < 'p': + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + else: + return True, 'Risk-free' + elif openssl[0] == '3' and openssl[1] == '0': + if openssl[2][0] < '4': + return False, 'There are security risks in the current version of openssl; you need to update to a secure version' + else: + return True, 'Risk-free' + else: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_cve_2022_25845.py b/class_v2/safe_warning_v2/sw_cve_2022_25845.py new file mode 100644 index 00000000..52b6e572 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_cve_2022_25845.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + +_title = 'CVE-2022-25845 Fastjson Arbitrary Code Execution vulnerability Detection' +_version = 1.0 # 版本 +_ps = "CVE-2022-25845 Fastjson Arbitrary Code Execution vulnerability Detection" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-13' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_cve_2022_25845.pl") +_tips = [ + "Open the pom.xml file in the website directory and check for fastjson dependencies." + "If fastjson version is less than 1.2.83, upgrade to security version 1.2.83 or higher ", +] +_help = '' + + +# https://nvd.nist.gov/vuln/detail/CVE-2022-25845 +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + web_list = [] + path = '/www/wwwroot/' + pom = public.ExecShell("find {} |grep pom.xml".format(path))[0].split('\n') + if pom[0]: + for p in pom: + if p == '': + continue + conf = public.ReadFile(p.strip()) + rep = r'fastjson(\s*)(.*)' + tmp = re.search(rep, conf) + rep1 = r'{}(.*)/'.format(path) + if tmp: + fastjson = tmp.group(2).split('.') + if len(fastjson) == 3: + if fastjson[0] == '1' and fastjson[1] == '1': + if not contrast(fastjson[2], '157'): + web_list.append(re.search(rep1, p).group(1)) + elif fastjson[0] == '1' and fastjson[1] == '2': + + if not contrast(fastjson[2], '83'): + web_list.append(re.search(rep1, p).group(1)) + if web_list: + return False, 'Website [{}] fastjson component has a security risk, need to upgrade the component to a secure version'.format('、'.join(web_list)) + return True, 'Risk-free' + + +def contrast(a, b): + if len(a) >= 3: + if a[0].isdigit() and a[1].isdigit() and a[2].isdigit(): + if int(a[0:3]) >= int(b): + return True + else: + return False + elif a[0].isdigit() and a[1].isdigit(): + if int(a[0:2]) >= int(b): + return True + else: + return False + elif a[0].isdigit(): + if int(a[0]) >= int(b): + return True + else: + return False + else: + return False + elif len(a) == 2: + if a[0].isdigit() and a[1].isdigit(): + if int(a[0:2]) >= int(b): + return True + else: + return False + elif a[0].isdigit(): + if int(a[0]) >= int(b): + return True + else: + return False + else: + return False + elif len(a) == 1: + if a[0].isdigit(): + if int(a[0]) >= int(b): + return True + else: + return False + else: + return False + else: + return False diff --git a/class_v2/safe_warning_v2/sw_cve_2023_0386.py b/class_v2/safe_warning_v2/sw_cve_2023_0386.py new file mode 100644 index 00000000..b6741a8c --- /dev/null +++ b/class_v2/safe_warning_v2/sw_cve_2023_0386.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +#coding: utf-8 +import os +import public +import re + +_title = 'CVE-2023-0386 Linux Kernel OverlayFS Vulnerability' +_version = 1.0 # 版本 +_ps = "CVE-2023-0386 Linux Kernel OverlayFS Vulnerability" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-06-06' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_cve_2023_0386.pl") +_tips = [ + "Check whether the kernel version is lower than the specified version according to the prompt [uname -r]", + "If is CentOS 8 Stream, execute [yum install kernel] command to upgrade the kernel version, and restart the server", + "If it Ubuntu 22.04, execute [apt install linux-image] to check the installable version number, select version number higher than 5.15.0-70, execute [apt install linux-image-version_number] again, and restart the server" +] +_help = '' +_remind = 'The above kernel upgrade operation has certain risks, it is strongly recommended that the server do a snapshot backup first, in case the operation fails to restore in time! ' + +# https://nvd.nist.gov/vuln/detail/CVE-2023-0386 +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + kernel = public.ExecShell('uname -r')[0] + result = re.search("^(\\d+.\\d+.\\d+-\\d+)", kernel) + # centos8 + if os.path.exists('/etc/redhat-release'): + ver = public.ReadFile('/etc/redhat-release') + if ver.startswith('CentOS Stream release 8'): + if result: + result = result.group(1).split('.') + result = result[:2] + result[2].split('-') + if len(result) == 4: + if result[0] == '4' and result[1] == '18' and result[2] == '0': + if len(result[3]) <= 3: + fin = contrast(result[3], 425) + if not fin: + return False, 'The current kernel version [{}] has security risks, please upgrade to 4.18.0-425 and above as soon as possible'.format(kernel) + if os.path.exists('/etc/issue'): + ver = public.ReadFile('/etc/issue') + if ver.startswith('Ubuntu 22.04'): + if result: + result = result.group(1).split('.') + result = result[:2] + result[2].split('-') + print(result) + if len(result) == 4: + if result[0] == '5' and result[1] == '15' and result[2] == '0': + if len(result[3]) <= 3: + fin = contrast(result[3], 70) + if not fin: + return False, 'The current kernel version [{}] has security risks, please upgrade to 5.15.0-70 and above as soon as possible'.format(kernel) + return True, 'The current kernel version [{}] is risk-free'.format(kernel) + + +def contrast(a, b): + if len(a) >= 3: + if a[0].isdigit() and a[1].isdigit() and a[2].isdigit(): + if int(a[0:3]) >= int(b): + return True + else: + return False + elif a[0].isdigit() and a[1].isdigit(): + if int(a[0:2]) >= int(b): + return True + else: + return False + elif a[0].isdigit(): + if int(a[0]) >= int(b): + return True + else: + return False + else: + return False + elif len(a) == 2: + if a[0].isdigit() and a[1].isdigit(): + if int(a[0:2]) >= int(b): + return True + else: + return False + elif a[0].isdigit(): + if int(a[0]) >= int(b): + return True + else: + return False + else: + return False + elif len(a) == 1: + if a[0].isdigit(): + if int(a[0]) >= int(b): + return True + else: + return False + else: + return False + else: + return False + diff --git a/class_v2/safe_warning_v2/sw_database_backup.py b/class_v2/safe_warning_v2/sw_database_backup.py new file mode 100644 index 00000000..45107b75 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_database_backup.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 数据库定时备份检测 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'Database backup' +_version = 1.0 # 版本 +_ps = "Checks whether all databases are set up for periodic backup" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_database_backup.pl") +_tips = [ + "On the [ Cron ] page, set the database that is not backed up, or set all databases to be backed up", + "Tip: if the database is not set up for regular backup, once the data is lost accidentally and cannot be recovered, the loss will be huge" + ] + +_help = '' +_remind = 'This solution prevents data loss in the database and keeps data safe. ' + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-03> + @return tuple (status,msg) + ''' + + if os.path.exists('/www/server/panel/plugin/enterprise_backup'): + return True,'Risk-free' + if public.M('crontab').where('sType=? AND sName=?', + ('database', 'ALL')).count(): + return True, 'Risk-free' + + db_list = public.M('databases').field('name').select() + + not_backups = [] + sql = public.M('crontab') + for db in db_list: + if sql.where('sType=? AND sName=?',('database',db['name'])).count(): + continue + not_backups.append(db['name']) + + if not_backups: + return False ,'The following databases are not set up for regular backup:
                                    ' + ('
                                    '.join(not_backups)) + + return True,'Risk-free' + + diff --git a/class_v2/safe_warning_v2/sw_database_priv.py b/class_v2/safe_warning_v2/sw_database_priv.py new file mode 100644 index 00000000..98350b92 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_database_priv.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: linxiao +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 数据库备份权限检测 +# ------------------------------------------------------------------- + +import os, re, public, panelMysql + +_title = 'Database backup permission detection' +_version = 1.0 # 版本 +_ps = "Check whether the MySQL root user has database backup privileges" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-09-19' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_database_priv.pl") +_tips = [ + "To temporarily enter the database without authorization, it is recommended to restore all permissions of the root user.", +] + +_help = '' +_remind = 'This scheme ensures that the root user has the permission to backup the database and ensures that the database backup work is carried out. ' + +def check_run(): + """检测root用户是否具备数据库备份权限 + + @author linxiao<2020-9-18> + @return (bool, msg) + """ + mycnf_file = '/etc/my.cnf' + if not os.path.exists(mycnf_file): + return True, 'Risk-free' + mycnf = public.readFile(mycnf_file) + port_tmp = re.findall(r"port\s*=\s*(\d+)", mycnf) + if not port_tmp: + return True, 'Risk-free' + if not public.ExecShell("lsof -i :{}".format(port_tmp[0]))[0]: + return True, 'Risk-free' + + base_backup_privs = ["Lock_tables_priv", "Select_priv"] + select_sql = "Select {} FROM mysql.user WHERE user='root' and " \ + "host=SUBSTRING_INDEX((select current_user()),'@', " \ + "-1);".format(",".join(base_backup_privs)) + select_result = panelMysql.panelMysql().query(select_sql) + if not select_result: + return False, "The root user has insufficient privileges to perform mysqldump backups." + select_result = select_result[0] + for priv in select_result: + if priv.lower() != "y": + return False, "The root user has insufficient privileges to perform mysqldump backups." + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_debug_mode.py b/class_v2/safe_warning_v2/sw_debug_mode.py new file mode 100644 index 00000000..2272c99d --- /dev/null +++ b/class_v2/safe_warning_v2/sw_debug_mode.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测是否开debug模式 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'Developer Mode' +_version = 1.0 # 版本 +_ps = "Checks whether panel developer mode is enabled" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_debug_mode.pl") +_tips = [ + "Turn off developer mode on the [ Settings ] page", + "Note: Developer mode is only used for panel plug-in or API development, do not use in production environment" + ] + +_help = '' +_remind = 'This solution prevents the disclosure of sensitive information and reduces the risk of your website being compromised. ' + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-05> + @return tuple (status,msg) + ''' + if os.path.exists('/www/server/panel/data/debug.pl'): + return False,'[Developer mode] has been opened, and risks such as data communication and information leakage exist' + return True,'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_dir_mode.py b/class_v2/safe_warning_v2/sw_dir_mode.py new file mode 100644 index 00000000..69e5e8b8 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_dir_mode.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测关键目录权限是否正确 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'System directory permissions' +_version = 1.0 # 版本 +_ps = "Checks if the System directory permissions are correct" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_dir_mode.pl") +_tips = [ + "On the [ File ] page, set the correct permissions and owner for the specified directory or file", + "Note 1: When setting directory permissions through the [File] page, please cancel the [Apply to subdirectories] option", + "Note 2: Incorrect file permissions not only pose a security risk, but also may cause some software on the server to fail to work properly" + ] + +_help = '' + + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-05> + @return tuple (status,msg) + ''' + dir_list = [ + ['/usr',755,'root'], + ['/usr/bin',555,'root'], + ['/usr/sbin',555,'root'], + ['/usr/lib',555,'root'], + ['/usr/lib64',555,'root'], + ['/usr/local',755,'root'], + ['/etc',755,'root'], + ['/etc/passwd',644,'root'], + ['/etc/shadow',600,'root'], + ['/etc/gshadow',600,'root'], + ['/etc/cron.deny',600,'root'], + ['/etc/anacrontab',600,'root'], + ['/var',755,'root'], + ['/var/spool',755,'root'], + ['/var/spool/cron',700,'root'], + ['/var/spool/cron/root',600,'root'], + ['/var/spool/cron/crontabs/root',600,'root'], + ['/www',755,'root'], + ['/www/server',755,'root'], + ['/www/wwwroot',755,'root'], + ['/root',550,'root'], + ['/mnt',755,'root'], + ['/home',755,'root'], + ['/dev',755,'root'], + ['/opt',755,'root'], + ['/sys',555,'root'], + ['/run',755,'root'], + ['/tmp',777,'root'] + ] + + not_mode_list = [] + # for d in dir_list: + # if not os.path.exists(d[0]): continue + # u_mode = public.get_mode_and_user(d[0]) + # if u_mode['user'] != d[2]: + # not_mode_list.append("{} 当前权限: {} : {} 安全权限: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + # if int(u_mode['mode']) != d[1]: + # not_mode_list.append("{} 当前权限: {} : {} 安全权限: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + + # if not_mode_list: + # return False,'以下关键文件或目录权限错误:
                                    ' + ("
                                    ".join(not_mode_list)) + + return True,'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_docker_api.py b/class_v2/safe_warning_v2/sw_docker_api.py new file mode 100644 index 00000000..5d2b1f15 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_docker_api.py @@ -0,0 +1,60 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# Docker API 未授权访问 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") + +import public, os,requests +_title = 'Docker API unauthorized access' +_version = 1.0 # 版本 +_ps = "Docker API unauthorized access" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_docker_api.pl") +_tips = [ + "Authentication should be turned on to authenticate or turn off the Docker Api" +] +_help = '' +_remind = "This solution fixes Docker's unauthorized access vulnerability, preventing attackers from using Docker to break into the server. We need to restrict API access to ensure that it does not affect the original website business operation. " + +# +def get_local_ip(): + '''获取内网IP''' + import socket + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + return ip + finally: + s.close() + return '127.0.0.1' + +def check_run(): + ''' + @name 面板登录告警是否开启 + @time 2022-08-12 + @author lkq@bt.cn + ''' + try: + if os.path.exists("/lib/systemd/system/docker.service"): + data=public.ReadFile("/lib/systemd/system/docker.service") + if not data:return True, 'Risk-free' + if '-H tcp://' in data: + datas=requests.get("http://{}:2375/info".format(get_local_ip()),timeout=1) + datas.json() + if 'KernelVersion' in datas.text and 'RegistryConfig' in datas.text and 'DockerRootDir' in datas.text: + return False,"Unauthorized access to the Docker API" + return True, 'Risk-free' + except:return True,"Risk-free" \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_docker_mod.py b/class_v2/safe_warning_v2/sw_docker_mod.py new file mode 100644 index 00000000..998d53a4 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_docker_mod.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# coding: utf-8 + +import sys, os, public +_title = 'Docker critical file permission checks' +_version = 1.0 # 版本 +_ps = "Docker critical file permission checks" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-14' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_docker_mod.pl") +_tips = [ + "On the File page, set the correct permissions and owners for the specified directory or file", + "docker.service and docker.socket require permission [644]", + "docker directory chmod /etc/docker 755" +] +_help = '' +_remind = 'This solution strengthens the protection of Docker files and prevents intruders from tampering with Docker files. ' + + +def check_run(): + dir_list = [ + ['/usr/lib/systemd/system/docker.service', 644, 'root'], + ['/usr/lib/systemd/system/docker.socket', 644, 'root'], + ['/etc/docker', 755, 'root'] + ] + + not_mode_list = [] + for d in dir_list: + if not os.path.exists(d[0]): + continue + u_mode = public.get_mode_and_user(d[0]) + if u_mode['user'] != d[2]: + not_mode_list.append("{} Current permissions: {} : {} Security permissions: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + if int(u_mode['mode']) != d[1]: + not_mode_list.append("{} Current permissions: {} : {} Security permissions: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + if not_mode_list: + return False, 'The following critical file or directory permission error:{}'.format('、'.join(not_mode_list)) + else: + return True, "Risk-free" diff --git a/class_v2/safe_warning_v2/sw_empty_passwd_user.py b/class_v2/safe_warning_v2/sw_empty_passwd_user.py new file mode 100644 index 00000000..729578a1 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_empty_passwd_user.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, sys, public + +_title = 'Check if an empty password user exists' +_version = 1.0 # 版本 +_ps = "Check if an empty password user exists" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-21' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_empty_passwd_user.pl") +_tips = [ + "Log in to server as root, set password for empty password user ", + "If you do not know the user's purpose, you can execute the command [passwd -l (username)] to temporarily block the user." + "Unlock user command [passwd-fu (username)]" +] +_help = '' +_remind = 'detects the existence of blank password users, may be hackers reserved backdoor users, if not business needs to suggest setting a password. ' + + +def check_run(): + ''' + @name 开始检测 + @author lwh<2023-11-21> + @return tuple (status,msg) + ''' + user_list = [] + try: + output, err = public.ExecShell('awk -F: \'($2 == "") {print}\' /etc/shadow') + if err == '' and output != '': + output_list = output.strip().split('\n') + for op in output_list: + user_list.append(op.split(':')[0]) + if len(user_list)>0: + return False, 'Found an empty password user【{}】'.format('、'.join(user_list)) + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_file_lock.py b/class_v2/safe_warning_v2/sw_file_lock.py new file mode 100644 index 00000000..7b7f5f8e --- /dev/null +++ b/class_v2/safe_warning_v2/sw_file_lock.py @@ -0,0 +1,44 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, sys, re, public + + +_title = '设置关键文件底层属性' +_version = 1.0 # 版本 +_ps = "检查关键文件的底层属性是否配置" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_file_lock.pl") +_tips = [ + "给系统日志文件【/var/log/messages】添加只可追加属性chattr +a", + "给关键文件【/etc/passwd /etc/shadow /etc/group /etc/gshadow】添加锁属性chattr +i" +] +_help = '' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + result_list = [] + result_str1 = public.ExecShell('lsattr -l /var/log/messages*')[0].strip() + tmp_list1 = result_str1.split('\n') + # 执行lsattr -l查看文件特殊属性,若存在特殊属性,则判断是否为“追加属性”,若为否,则加入到result_list,最终显示到面板中 + for tl1 in tmp_list1: + if not "Append_Only" in tl1: + log1 = re.search(r'.*?\s', tl1) + result_list.append(log1.group().strip()) + result_str2 = public.ExecShell('lsattr -l /etc/passwd /etc/shadow /etc/group /etc/gshadow')[0].strip() + tmp_list2 = result_str2.split('\n') + # immutable判断是否为锁属性 + for tl2 in tmp_list2: + if not "Immutable" in tl2: + log2 = re.search(r'.*?\s', tl2) + result_list.append(log2.group().strip()) + if result_list: + return False, '以下文件未配置适当的底层属性:{}'.format('、'.join(result_list)) + else: + return True, 'Risk-free' + diff --git a/class_v2/safe_warning_v2/sw_file_mod.py b/class_v2/safe_warning_v2/sw_file_mod.py new file mode 100644 index 00000000..a61babf0 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_file_mod.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 开启地址空间布局随机化 +# ------------------------------------------------------------------- +import sys, os +os.chdir('/www/server/panel') +sys.path.append("class/") +import os, sys, re, public + +_title = 'Critical file permission checks' +_version = 1.0 # 版本 +_ps = "Critical file permission checks" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_file_mod.pl") +_tips = [ + "On the [File] page, set the correct permissions and owner for the specified directory or file", +] +_help = '' + +def check_run(): + dir_list = [ + ['/etc/shadow', 400, 'root'], + ['/etc/security', 600, 'root'], + ['/etc/passwd', 644, 'root'], + ['/etc/services', 644, 'root'], + ['/etc/group', 644, 'root'], + ['/var/spool/cron/root',600, 'root'], + ['/etc/ssh/sshd_config',644,'root'], + ['/etc/sysctl.conf',644,'root'], + ['/etc/crontab',644,'root'], + ['/etc/hosts.deny',644,'root'], + ['/etc/hosts.allow',644,'root'], + ['/etc/gshadow',400,'root'], + ['/etc/passwd',644,'root'], + ['/etc/shadow',400,'root'], + ['/etc/group',644,'root'], + ['/etc/gshadow',400,'root'], + ] + + not_mode_list = [] + + for d in dir_list: + if not os.path.exists(d[0]): continue + u_mode = public.get_mode_and_user(d[0]) + if u_mode['user'] != d[2]: + not_mode_list.append("{} Current permissions: {} : {} Suggested changes to: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + if int(u_mode['mode']) != d[1]: + not_mode_list.append("{} Current permissions: {} : {} Suggested changes to: {} : {}".format(d[0],u_mode['mode'],u_mode['user'],d[1],d[2])) + + if not_mode_list: + return False,'The following critical files or directories have permission errors:
                                    ' + ("
                                    ".join(not_mode_list)) + else: + return True,"Risk-free" + + +# check_run() \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_files_recycle_bin.py b/class_v2/safe_warning_v2/sw_files_recycle_bin.py new file mode 100644 index 00000000..0a197ec8 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_files_recycle_bin.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测是否开启文件回收站 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'File Recycle Bin' +_version = 1.0 # 版本 +_ps = "Check whether the file recycle bin is open" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_files_recycle_bin.pl") +_tips = [ + "On the [File] page, [Recycle Bin] - opens the [File Recycle Bin] function" + ] + +_help = '' +_remind = 'This solution prevents files from being deleted by mistake and restores them through the recycle bin in time. ' + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-05> + @return tuple (status,msg) + ''' + if not os.path.exists('/www/server/panel/data/recycle_bin.pl'): + return False,'The function of [File Recycle Station] is not enabled at present. There is a risk that files cannot be retrieved in case of being deleted by mistake' + return True,'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_firewall_open.py b/class_v2/safe_warning_v2/sw_firewall_open.py new file mode 100644 index 00000000..26058da7 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_firewall_open.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 系统防火墙检测 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'System firewall' +_version = 1.0 # 版本 +_ps = "Check whether the system firewall is enable" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_firewall_open.pl") +_tips = [ + "It is recommended to enable the system firewall to prevent all server ports from being exposed to the Internet. If the server has [security group] function, please ignore this prompt", + "Note: To open the system firewall, the ports that need to be opened, especially SSH and panel ports, should be added to the release list in advance, otherwise the server may not be able to access them" + ] + +_help = '' +_remind = 'This solution can reduce the risk surface of the server exposure and enhance the protection of the website. However, you need to add the port that needs to be opened at the port rule, otherwise the website will be unreachable. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2022-08-18> + @return tuple (status,msg) + ''' + status = public.get_firewall_status() + if status == 1: + return True,'Risk-free' + elif status == -1: + return False,'System firewall is not installed, there are security risks' + else: + return False,'System firewall is not installed, there are security risks' \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_ftp_login.py b/class_v2/safe_warning_v2/sw_ftp_login.py new file mode 100644 index 00000000..b080de46 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ftp_login.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# coding: utf-8 + +import re, os, public + +_title = 'Disable anonymous FTP login' +_version = 1.0 # 版本 +_ps = "Disable Anonymous Login FTP Detection" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-3-15' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ftp_login.pl") +_tips = [ + "Modify the value of NoAnonymous to yes in the [/www/server/pure-ftpd/etc/pure-ftpd.conf] configuration file", +] +_help = '' +_remind = 'This scheme can enhance the FTP server protection, prevent illegal intrusion into the server. Unable to log in to the FTP server using Anonymous after configuration. ' + +def check_run(): + + if os.path.exists('/www/server/pure-ftpd/etc/pure-ftpd.conf'): + try: + info_data = public.ReadFile('/www/server/pure-ftpd/etc/pure-ftpd.conf') + if info_data: + if re.search(r'.*NoAnonymous\s*yes', info_data): + return True, 'Risk-free' + else: + return False, 'Currently pure-ftpd does not disable anonymous login, modify/add the value of NoAnonymous to yes in the [pure-ftpd.conf] file' + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ftp_pass.py b/class_v2/safe_warning_v2/sw_ftp_pass.py new file mode 100644 index 00000000..4c0e524d --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ftp_pass.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: linxiao +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# FTP弱口令检测 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os,public + +_title = 'Weak password detection for FTP services' +_version = 2.0 # 版本 +_ps = "Detect enabled weak passwords for FTP services" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-12' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ftp_pass.pl") +_tips = [ + "Please go to [FTP] page to change the FTP password ", + "Note: Please do not use too simple account password, so as not to cause security risks ", + "Strong passwords are recommended: numeric, upper - and lowercase, special characters, and no less than seven characters long." , + "Using [Fail2ban] plugin to protect FTP server" +] + +_help = '' +_remind = 'This scheme can strengthen the protection of the FTP server, to prevent intruders from blasting into the FTP server. ' + + +def check_run(): + """检测FTP弱口令 + @author linxiao<2020-9-19> + @return (bool, msg) + """ + + pass_info = public.ReadFile("/www/server/panel/config/weak_pass.txt") + if not pass_info: return True, 'Risk-free' + pass_list = pass_info.split('\n') + data = public.M("ftps").select() + ret = "" + for i in data: + if i['password'] in pass_list: + ret += "FTP:" + i['name'] + "weak passwords exist:" + i['password'] + "\n" + if ret: + # print(ret) + return False, ret + else: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ftp_root.py b/class_v2/safe_warning_v2/sw_ftp_root.py new file mode 100644 index 00000000..3756e67a --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ftp_root.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# coding: utf-8 + +import re, os, public + +_title = 'Forbid the root user to log in to FTP' +_version = 1.0 # 版本 +_ps = "Prohibit the root user from logging in to FTP inspection" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-3-15' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ftp_root.pl") +_tips = [ + "Modify the value of MinUID to 100 in the [/www/server/pure-ftpd/etc/pure-ftpd.conf] configuration file", +] +_help = '' +_remind = 'This solution can be used to enhance the protection of your FTP server. After configuration, the root user cannot login ftp, use with caution. ' + +def check_run(): + if os.path.exists('/www/server/pure-ftpd/etc/pure-ftpd.conf'): + try: + info_data = public.ReadFile('/www/server/pure-ftpd/etc/pure-ftpd.conf') + if info_data: + tmp = re.search('\nMinUID\\s*([0-9]{1,4})', info_data) + if tmp: + if int(tmp.group(1).strip()) < 100: + return False, 'Currently pure-ftpd is not configured with security access, modify/add the value of MinUID to 100 in the [pure-ftpd.conf] file' + else: + return True, 'Risk-free' + else: + return False, 'Currently pure-ftpd is not configured with security access, modify/add the value of MinUID to 100 in the [pure-ftpd.conf] file' + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ftp_umask.py b/class_v2/safe_warning_v2/sw_ftp_umask.py new file mode 100644 index 00000000..37cefecf --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ftp_umask.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# coding: utf-8 + +import re, os, public +_title = 'User FTP access security configuration' +_version = 1.0 # 版本 +_ps = "User FTP access security configuration checks" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-3-15' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ftp_umask.pl") +_tips = [ + "In [/www/server/pure-ftpd/etc/pure-ftpd.conf] change the value of Umask to 177:077 in the config file", +] +_help = '' +_remind = 'This scheme can enhance the protection of FTP server and reduce the risk of server intrusion.' + + +def check_run(): + + if os.path.exists('/www/server/pure-ftpd/etc/pure-ftpd.conf'): + try: + info_data = public.ReadFile('/www/server/pure-ftpd/etc/pure-ftpd.conf') + if info_data: + if re.search(r'.*Umask\s*177:077', info_data): + return True, 'Risk-free' + else: + return False, 'Currently pure-ftpd is not configured with security access. Modify/add the value of Umask to 177:077 in the [pure-ftpd.conf] file' + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_httpd_trace_enable.py b/class_v2/safe_warning_v2/sw_httpd_trace_enable.py new file mode 100644 index 00000000..4f608474 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_httpd_trace_enable.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# coding: utf-8 + + +import re, os, public +_title = 'Apache TRACE request checks' +_version = 1.0 # 版本 +_ps = "Apache TRACE request checks" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-21' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_httpd_trace_enable.pl") +_tips = [ + "Set TraceEnable off in [httpd.conf] file and restart Apache server ", + "Or handle security risks with one-click fixes." +] +_help = '' +_remind = 'TRACE request is generally used to test HTTP protocol. Attackers may use TRACE request combined with other vulnerabilities to perform cross-site scripting attacks to obtain sensitive information.' + + +def check_run(): + ''' + @name + @author lwh<2023-11-22> + @return tuple (status,msg) + ''' + + if os.path.exists('/www/server/apache/conf/httpd.conf'): + try: + info_data = public.ReadFile('/www/server/apache/conf/httpd.conf') + if info_data: + if not re.search('TraceEnable off', info_data): + return False, 'TRACE requests are not currently disabled by Apache. Set TraceEnable off in the [httpd.conf] file' + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_httpd_version_leak.py b/class_v2/safe_warning_v2/sw_httpd_version_leak.py new file mode 100644 index 00000000..09168516 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_httpd_version_leak.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# coding: utf-8 + + +import re, os, public +_title = 'Apache version leak' +_version = 1.0 # 版本 +_ps = "Apache Version leak check" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-14' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_httpd_version_leak.pl") +_tips = [ + "Add ServerSignature Off and ServerTokens Prod to the [httpd.conf] file", +] +_help = '' +_remind = 'This solution can enhance the protection of your server and reduce the risk of your website being compromised. ' + +def check_run(): + ''' + @name + @author + @return tuple (status,msg) + ''' + + if os.path.exists('/www/server/apache/conf/httpd.conf'): + try: + info_data = public.ReadFile('/www/server/apache/conf/httpd.conf') + if info_data: + if not re.search('ServerSignature', info_data) and not re.search('ServerTokens', + info_data): + return True, 'Risk-free' + if re.search('ServerSignature Off', info_data) and re.search('ServerTokens Prod', + info_data): + return True, 'Risk-free' + else: + return False, 'Currently Apache has a version leak problem, please add ServerSignature Off and ServerTokens Prod in the [httpd.conf] file' + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_kernel_space.py b/class_v2/safe_warning_v2/sw_kernel_space.py new file mode 100644 index 00000000..11507463 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_kernel_space.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 开启地址空间布局随机化 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = 'Enable kernel.randomize_va_space' +_version = 1.0 # 版本 +_ps = "Enable kernel.randomize_va_space" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_kernel_space.pl") +_tips = [ + "[/proc/sys/kernel/randomize_va_space] value is 2: ", + "How to set:sysctl -w kernel.randomize_va_space=2", +] +_help = '' +_remind = 'This scheme can reduce the risk of intrusers using buffer overflow to attack the server, and strengthen the protection of the server. ' +def check_run(): + try: + if os.path.exists("/proc/sys/kernel/randomize_va_space"): + randomize_va_space=public.ReadFile("/proc/sys/kernel/randomize_va_space") + if int(randomize_va_space)!=2: + return False, 'Enable kernel.randomize_va_space' + else: + return True,"Risk-free" + except: + return True, "Risk-free" \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_login_fail_limit.py b/class_v2/safe_warning_v2/sw_login_fail_limit.py new file mode 100644 index 00000000..e9ddc545 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_login_fail_limit.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + + +_title = 'Check account authentication failure limit' +_version = 1.0 # 版本 +_ps = "Check whether to limit the number of account authentication failures" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-20' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_login_fail_limit.pl") +_tips = [ + "Add or modify the second line of the [/etc/pam.d/sshd] file", + "auth required pam_tally2.so onerr=fail deny=5 unlock_time=300 even_deny_root root_unlock_time=300" +] +_help = '' +_remind = 'This reduces the risk of a server being blown up. Be sure to remember your login password, though, in case you get locked out of your account for five minutes because of too many failed login attempts. ' + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + cfile = '/etc/pam.d/sshd' + if not os.path.exists(cfile): + return True, 'Risk-free' + conf = public.readFile(cfile) + rep = r".*auth(\s*)required(\s*)pam_tally[2]?\.so.*deny(\s*)=.*unlock_time(\s*)=.*even_deny_root.*root_unlock_time(\s*)=" + tmp = re.search(rep, conf) + if tmp: + if tmp.group()[0] == '#': + return False, 'The limit on the number of authentication failures is not configured or is improperly configured' + return True, 'Risk-free' + else: + return False, 'The limit on the number of authentication failures is not configured or is improperly configured' + diff --git a/class_v2/safe_warning_v2/sw_login_message.py b/class_v2/safe_warning_v2/sw_login_message.py new file mode 100644 index 00000000..be981c0f --- /dev/null +++ b/class_v2/safe_warning_v2/sw_login_message.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测用户登录通知 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'SSH user login notification' +_version = 1.0 # 版本 +_ps = "Check whether SSH user login notification is enabled" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_login_message.pl") +_tips = [ + "On the [Security] page, [SSH security management] - [login alarm] enable the [monitor root login] function" + ] + +_help = '' + + +def return_bashrc(): + if os.path.exists('/root/.bashrc'):return '/root/.bashrc' + if os.path.exists('/etc/bashrc'):return '/etc/bashrc' + if os.path.exists('/etc/bash.bashrc'):return '/etc/bash.bashrc' + return '/root/.bashrc' + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-04> + @return tuple (status,msg) + ''' + + data = public.ReadFile(return_bashrc()) + if not data: return True,'Risk-free' + if re.search('ssh_security.py login', data): + return True,'Risk-free' + else: + return False,'SSH user login notification is not configured, so it is impossible to know whether the server has been illegally logged in in the first place' \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_login_user.py b/class_v2/safe_warning_v2/sw_login_user.py new file mode 100644 index 00000000..274bf5b9 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_login_user.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测风险用户 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'Risk User' +_version = 1.0 # 版本 +_ps = "Detect if there is a risk user in the system user list" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_login_user.pl") +_tips = [ + "If these users are not added by the server administrator, the system may have been compromised and should be dealt with as soon as possible." + ] + +_help = '' + + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-04> + @return tuple (status,msg) + ''' + + u_list = get_ulist() + + try_users = [] + for u_info in u_list: + if u_info['user'] == 'root': continue + if u_info['pass'] == '*': continue + if u_info['uid'] == 0: + try_users.append(u_info['user'] + ' > Unknown administrator user [high risk]') + + if u_info['login'] in ['/bin/bash','/bin/sh']: + try_users.append(u_info['user'] + ' > Logged-in user [medium risk]') + + if try_users: + return False, 'There are security risks for the following users:
                                    ' + ('
                                    '.join(try_users)) + + return True,'Risk-free' + + + +#获取用户列表 +def get_ulist(): + u_data = public.readFile('/etc/passwd') + u_list = [] + for i in u_data.split("\n"): + u_tmp = i.split(':') + if len(u_tmp) < 3: continue + u_info = {} + u_info['user'],u_info['pass'],u_info['uid'],u_info['gid'],u_info['user_msg'],u_info['home'],u_info['login'] = u_tmp + u_list.append(u_info) + return u_list \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_memcached_port.py b/class_v2/safe_warning_v2/sw_memcached_port.py new file mode 100644 index 00000000..1cefc107 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_memcached_port.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# Memcached安全检测 +# ------------------------------------------------------------------- + +import os,sys,re,public + +_title = 'Memcached security' +_version = 1.0 # 版本 +_ps = "Check whether the current Memcached is safe" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_memcached_port.pl") +_tips = [ + "Do not configure bindIP for Memcached as 0.0.0.0 unless necessary", + "If bindIP is 0.0.0.0, be sure to set IP access restrictions through the [SYS firewall] plugin or the Security group" + ] +_help = '' +_remind = 'This solution can reduce the risk exposure of the server and strengthen the protection of the website. However, it is necessary to set the accessible IP according to the business requirements. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-03> + @return tuple (status,msg) + ''' + + p_file = '/etc/init.d/memcached' + p_body = public.readFile(p_file) + if not p_body: return True,'Risk-free' + + tmp = re.findall(r"^\s*IP=(0\.0\.0\.0)",p_body,re.M) + if not tmp: return True,'Risk-free' + + tmp = re.findall(r"^\s*PORT=(\d+)",p_body,re.M) + + result = public.check_port_stat(int(tmp[0]),public.GetClientIp()) + if result == 0: + return True,'Risk-free' + + return False,'The current Memcached port: {} allows arbitrary client access, which can lead to data leakage'.format(tmp[0]) + diff --git a/class_v2/safe_warning_v2/sw_mongodb_auth.py b/class_v2/safe_warning_v2/sw_mongodb_auth.py new file mode 100644 index 00000000..2e7873f4 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_mongodb_auth.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + +_title = 'Whether to enable security authentication for MongoDB' +_version = 1.0 # 版本 +_ps = "Check whether MongoDB security authentication is enabled" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_mongodb_auth.pl") +_tips = [ + "Turn on the security authentication switch in the aaPanel Databases MongoDB", +] +_help = '' +_remind = 'This is a great way to protect your database against hackers trying to steal data from your mongo database. ' + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + if not public.process_exists("mongod"): + return True, 'Risk-free,The MongoDB service has not been started!' + cfile = '{}/mongodb/config.conf'.format(public.get_setup_path()) + conf = public.readFile(cfile) + rep = r".*authorization(\s*):(\s*)enabled" + tmp = re.search(rep, conf) + if tmp: + return True, 'Risk-free' + else: + return False, 'MongoDB security authentication is not enabled' + diff --git a/class_v2/safe_warning_v2/sw_mysql_pass.py b/class_v2/safe_warning_v2/sw_mysql_pass.py new file mode 100644 index 00000000..c57484ac --- /dev/null +++ b/class_v2/safe_warning_v2/sw_mysql_pass.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# Mysql 弱口令检测 +# ------------------------------------------------------------------- + +import public, os +_title = 'Mysql weak password detection' +_version = 1.0 # 版本 +_ps = "Mysql weak password detection" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_mysql_pass.pl") +_tips = [ + "If a weak password is detected, please change the password in time" +] +_help = '' +_remind = 'This scheme increases the strength of the database password and reduces the risk of being successfully exploded. ' + +def check_run(): + ''' + @name Mysql 弱口令检测 + @time 2022-08-12 + @author lkq@bt.cn + ''' + pass_info = public.ReadFile("/www/server/panel/config/weak_pass.txt") + if not pass_info: return True, 'Risk-free' + pass_list = pass_info.split('\n') + data=public.M("databases").select() + ret="" + for i in data: + if i['password'] in pass_list: + ret+="Database: "+i['name']+" has weak password: "+i['password']+"\n" + if ret: + return False, ret + else: + return True, 'Risk-free' + diff --git a/class_v2/safe_warning_v2/sw_mysql_port.py b/class_v2/safe_warning_v2/sw_mysql_port.py new file mode 100644 index 00000000..06ac8688 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_mysql_port.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# MySQL端口安全检测 +# ------------------------------------------------------------------- + +import os,sys,re,public,json + +_title = 'MySQL security' +_version = 1.0 # 版本 +_ps = "Checks whether the current server's MySQL port is secure" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-03' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_mysql_port.pl") +_tips = [ + "If not necessary, remove the MySQL port release from the [Security] page", + "Restrict IP access to MySQL port through the [System firewall] plug-in to enhance security", + "Use [ Fail2ban ] plug-in to protect MySQL service" + ] +_help = '' +_remind = 'This scheme strengthens the protection of the MySQL database and reduces the risk of the server being stolen data. Before the repair, open up the accessible IP according to the business requirements to ensure that the website is working properly. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-03> + @return tuple (status,msg) + + @example + status, msg = check_run() + if status: + print('OK') + else: + print('Warning: {}'.format(msg)) + + ''' + mycnf_file = '/etc/my.cnf' + if not os.path.exists(mycnf_file): + return True,'MySQL is not installed' + mycnf = public.readFile(mycnf_file) + port_tmp = re.findall(r"port\s*=\s*(\d+)",mycnf) + if not port_tmp: + return True,'MySQL is not installed' + if not public.ExecShell("lsof -i :{}".format(port_tmp[0]))[0]: + return True,'MySQL is not installed' + result = public.check_port_stat(int(port_tmp[0]),public.GetLocalIp()) + #兼容socket能连通但实际端口不通情况 + if result != 0: + res='' + if os.path.exists('/usr/sbin/firewalld'): + res=public.ExecShell('firewall-cmd --list-all') + elif os.path.exists('/usr/sbin/ufw'): + try: + res=public.ExecShell('sudo ufw status verbose') + except: + res=public.ExecShell('ufw status verbose') + else: + pass + check_str=' '+port_tmp[0]+'/' + if res[0].find(check_str) == -1: + return True,'Risk-free' + else:return True,'Risk-free' + + + fail2ban_file = '/www/server/panel/plugin/fail2ban/config.json' + if os.path.exists(fail2ban_file): + try: + fail2ban_config = json.loads(public.readFile(fail2ban_file)) + if 'mysql' in fail2ban_config.keys(): + if fail2ban_config['mysql']['act'] == 'true': + return True,'Fail2ban is enabled' + except: pass + + return False,'MySQL port: {}, can be accessed by any server, which may cause MySQL to be cracked by brute force, posing security risks'.format(port_tmp[0]) diff --git a/class_v2/safe_warning_v2/sw_mysql_priv.py b/class_v2/safe_warning_v2/sw_mysql_priv.py new file mode 100644 index 00000000..9b529c17 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_mysql_priv.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: linxiao +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 数据库备份权限检测 +# ------------------------------------------------------------------- + +import os, re, public, panelMysql + +_title = 'Database backup permission detection' +_version = 1.0 # 版本 +_ps = "Check whether the MySQL root user has database backup permissions" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-09-19' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_database_priv.pl") +_tips = [ + "To temporarily access the database without authorization, it is recommended to restore all permissions of the root user.", +] + +_help = '' +_remind = 'This scheme ensures that the root user has the permission to backup the database and ensures that the database backup work is carried out. ' + +def check_run(): + """检测root用户是否具备数据库备份权限 + + @author linxiao<2020-9-18> + @return (bool, msg) + """ + mycnf_file = '/etc/my.cnf' + if not os.path.exists(mycnf_file): + return True, 'Risk-free' + mycnf = public.readFile(mycnf_file) + port_tmp = re.findall(r"port\s*=\s*(\d+)", mycnf) + if not port_tmp: + return True, 'Risk-free' + if not public.ExecShell("lsof -i :{}".format(port_tmp[0]))[0]: + return True, 'Risk-free' + + base_backup_privs = ["Lock_tables_priv", "Select_priv"] + select_sql = "Select {} FROM mysql.user WHERE user='root' and " \ + "host=SUBSTRING_INDEX((select current_user()),'@', " \ + "-1);".format(",".join(base_backup_privs)) + select_result = panelMysql.panelMysql().query(select_sql) + if not select_result: + return False, "The root user has insufficient authority to execute mysqldump backup." + select_result = select_result[0] + for priv in select_result: + if priv.lower() != "y": + return False, "The root user has insufficient authority to execute mysqldump backup." + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_nginx_malware.py b/class_v2/safe_warning_v2/sw_nginx_malware.py new file mode 100644 index 00000000..16a052b7 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_nginx_malware.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, sys, public + +_title = 'Check if the nginx config file is compromised' +_version = 1.0 # 版本 +_ps = "Check if the nginx config file is compromised" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-21' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_nginx_malware.pl") +_tips = [ + "Follow the prompts to find the nginx config file and delete the js hang-up content ", + "Restart nginx server" +] +_help = '' +_remind = 'Before fixing it manually, it is recommended to make a backup of the configuration file in case the service fails to start. Or use one click to fix ' + + +def check_run(): + ''' + @name 开始检测 + @author lwh<2023-11-21> + @return tuple (status,msg) + ''' + import glob + path = '/www/server/panel/vhost/nginx/' + risk_file = [] + for filename in glob.glob(os.path.join(path, '*.conf')): + if os.path.isdir(filename): + continue + output = public.ReadFile(filename) + if "sub_filter" in output: + risk_file.append(filename) + if len(risk_file) > 0: + return False, 'The following nginx files contain malicious content: {}'.format('、'.join(risk_file)) + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_nginx_md5.py b/class_v2/safe_warning_v2/sw_nginx_md5.py new file mode 100644 index 00000000..e97c7dbb --- /dev/null +++ b/class_v2/safe_warning_v2/sw_nginx_md5.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, sys, public + +_title = 'Check nginx binaries for tampering' +_version = 1.0 # 版本 +_ps = "Check nginx binaries for tampering" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-21' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_nginx_md5.pl") +_tips = [ + "Reinstall Nginx in panel [Software Store] - [Run Environment] ", +] +_help = '' +_remind = '/ WWW/server/nginx/sbin/nginx executable been tampered with, risk was invaded site. ' + + +def check_run(): + ''' + @name 开始检测 + @author lwh<2023-11-21> + @return tuple (status,msg) + ''' + nginx_path = '/www/server/nginx/sbin/nginx' + nginx_md5 = '/www/server/panel/data/nginx_md5.pl' + if not os.path.exists(nginx_path): + return True, 'Risk-free' + try: + new_md5 = public.ExecShell('md5sum {}'.format(nginx_path))[0].strip().split(" ")[0] + if os.path.exists(nginx_md5): + old_md5 = public.ReadFile(nginx_md5).split(" ")[0] + if new_md5 != old_md5: + return False, "nginx file tampering has been detected(MD5:{})".format(new_md5) + return True, 'Risk-free' + except: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_nginx_server.py b/class_v2/safe_warning_v2/sw_nginx_server.py new file mode 100644 index 00000000..13ba7615 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_nginx_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# Nginx 版本泄露 +# ------------------------------------------------------------------- + +import re, public, os +_title = 'Nginx version leaked' +_version = 1.0 # 版本 +_ps = "Nginx version leaked" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_nginx_server.pl") +_tips = [ + "Set [server_tokens off;] in the [/www/server/nginx/conf/nginx.conf] file;", + "Tips:server_tokens off;" +] +_help = '' +_remind = 'This solution enhances server protection and reduces the risk of your website being compromised. ' +def check_run(): + ''' + @name 检测nginx版本泄露 + @author lkq<2020-08-10> + @return tuple (status,msg) + ''' + + if os.path.exists('/www/server/nginx/conf/nginx.conf'): + try: + info_data = public.ReadFile('/www/server/nginx/conf/nginx.conf') + if info_data: + if re.search('server_tokens off;', info_data): + return True, 'Risk-free' + else: + return False, 'The current version of Nginx is leaked, please add or modify the parameter server_tokens to off; in the Nginx configuration file, for example: server_tokens off;' + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_panel_control.py b/class_v2/safe_warning_v2/sw_panel_control.py new file mode 100644 index 00000000..8de58d04 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_panel_control.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# Mysql 弱口令检测 +# ------------------------------------------------------------------- + +import public, os +_title = 'The panel is not monitoring' +_version = 1.0 # 版本 +_ps = "The panel is not monitoring" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_panel_control.pl") +_tips = [ + "Open it in [Monitor] - [System Monitor]" +] +_help = '' +_remind = 'Enable server monitoring, you can record the recent operation of the server, to facilitate the troubleshooting of system anomalies. ' +def check_run(): + ''' + @name 面板未开启监控 + @time 2022-08-12 + @author lkq@bt.cn + ''' + global _tips + send_type = "" + if os.path.exists("/www/server/panel/data/control.conf"): + return True, 'Risk-free' + return False, _tips[0] + + + diff --git a/class_v2/safe_warning_v2/sw_panel_pass.py b/class_v2/safe_warning_v2/sw_panel_pass.py new file mode 100644 index 00000000..0fe48580 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_panel_pass.py @@ -0,0 +1,1156 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 是否修改面板默认帐号密码 +# ------------------------------------------------------------------- + +import os,sys,re,public + +_title = 'Panel password' +_version = 1.0 # 版本 +_ps = "Check whether the panel account password is safe" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_panel_pass.pl") +_tips = [ + "Please go to the [Settings] page to modify the panel account password", + "Note: Please do not use too simple account password, so as not to cause security risks" + ] +_help = '' +_remind = 'This scheme can strengthen the complexity of the panel login account password and reduce the risk of being successfully exploded. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-04> + @return tuple (status,msg) + ''' + + default_file = '/www/server/panel/default.pl' + if not os.path.exists(default_file): + return True,'Risk-free' + default_pass = public.readFile(default_file).strip() + + p1 = password_salt(public.md5(default_pass),uid=1) + find = public.M('users').where('id=?',(1,)).field('username,password').find() + if p1 == find['password']: + return False,'The default password of the panel has not been modified, and there is a security risk' + + lower_pass_txt = '''12123 +china +test +test12 +test11 +test1 +test2 +test123 +bt.cn +www.bt.cn +admin +root +12345 +123456 +123456789 +111111 +from91 +12345678 +123123 +5201314 +000000 +11111111 +a123456 +163.com +fill.com +123321 +123123123 +00000000 +1314520 +7758521 +1234567 +666666 +123456a +1234567890 +woaini +a123456789 +888888 +88888888 +147258369 +qq123456 +654321 +zxcvbnm +woaini1314 +112233 +5211314 +123456abc +520520 +aaaaaa +123654 +987654321 +123456789a +12345 +7758258 +100200 +147258 +111222 +abc123456 +111222tianya +121212 +1111111 +abc123 +110110 +admin123 +789456 +q123456 +123456aa +aa123456 +asdasd +999999 +123qwe +789456123 +1111111111 +1314521 +iloveyou +qwerty +password +qazwsx +159357 +222222 +woaini520 +woaini123 +521521 +asd123 +qqqqqq +qq1111 +1234 +qwe123 +111111111 +1qaz2wsx +qwertyuiop +5201314520 +asd123456 +159753 +31415926 +qweqwe +555555 +333333 +woaini521 +abcd1234 +ASDFGHJKL +123456qq +11223344 +456123 +123000 +123698745 +wangyut2 +201314 +zxcvbnm123 +qazwsxedc +1q2w3e4r +z123456 +123abc +a123123 +12345678910 +asdfgh +456789 +qwe123456 +321321 +123654789 +456852 +0000000000 +WOAIWOJIA +741852963 +5845201314 +aini1314 +0123456789 +a321654 +123456123 +584520 +778899 +520520520 +7777777 +q123456789 +123789 +zzzzzz +qweasdzxc +5845211314 +123456q +w123456 +12301230 +qq123456789 +wocaonima +qq123123 +a5201314 +a12345678 +asdasdasd +a1234567 +147852 +110120 +135792468 +CAONIMA +963852741 +3.1415926 +1234560 +101010 +7758520 +753951 +666888 +zxc123 +0000000 +zhang123 +987654 +a111111 +1233211234567 +789789 +25257758 +7708801314520 +zzzxxx +1111 +999999999 +1357924680 +yahoo.com.cn +123456789q +12341234 +5841314520 +zxc123456 +yangyang +168168 +123123qaz +abcd123456 +456456 +963852 +as123456 +741852 +xiaoxiao +1230123 +555666 +000000000 +369369 +211314 +102030 +aaa123456 +zxcvbn +110110110 +buzhidao +qaz123 +123456. +asdfasdf +123456789. +woainima +123456ASD +woshishui +131421 +123321123 +dearbook +1234qwer +qaz123456 +aaaaaaaa +111222333 +qq5201314 +3344520 +147852369 +1q2w3e +windows +123456987 +zz12369 +qweasd +qiulaobai +66666666 +12344321 +qwer1234 +a12345 +7894561230 +qwqwqw +777777 +110120119 +951753 +wmsxie123 +131420 +1314520520 +369258147 +321321321 +110119 +beijing2008 +321654 +a000000 +147896325 +12121212 +123456aaa +521521521 +22222222 +888999 +123456789ABC +abc123456789 +12345678900 +1q2w3e4r5t +1234554321 +www123456 +w123456789 +336699 +abcdefg +709394 +258369 +z123456789 +314159 +584521 +12345678a +7788521 +9876543210 +258258 +111111a +87654321 +123asd +5201314a +134679 +135246 +hotmail.com +123123a +11112222 +131313 +100200300 +11111 +1234567899 +520530 +251314 +qq66666 +yahoo.cn +123456qwe +worinima +sohu.com +NULL +518518 +123457 +q1w2e3r4 +721521 +123456789QQ +584131421 +qw123456 +123456.. +0123456 +135790 +3344521 +980099 +a1314520 +123456123456 +qazwsx123 +asdf1234 +444444 +123456z +120120 +wang123456 +12345600 +7758521521 +12369874 +abcd123 +a12369 +li123456 +1234567891 +wang123 +1234abcd +147369 +zhangwei +qqqqqqqq +521125 +010203 +369258 +654123 +woailaopo +QAZQAZ +121314 +1qazxsw2 +zxczxc +l123456 +111000 +jingjing +0000 +1472583690 +25251325 +langzi123 +wojiushiwo +7895123 +wangjian +123qweasd +110120130 +1123581321 +142536 +584131420 +aaa123 +aaa111 +woaiwoziji +520123 +665544 +ab123456 +a123456a +fuckyou +99999999 +5203344 +qwertyui +521314 +18881888 +584201314 +woaini@ +7654321 +20082008 +520131 +124578 +852456 +nihaoma +74108520 +232323 +55555555 +zx123456 +wwwwww +119119 +weiwei +13145200 +LOVE1314 +564335 +123456789123 +wo123456 +123520 +52013145201314 +loveyou +wolf8637 +112358 +5201314123 +yuanyuan +zhanglei +zz123456 +1234567A +a11111 +000000a +321654987 +xiaolong +5841314521 +shmily +520025 +159951 +77585210 +tiantian +134679852 +QWASZX +123456654321 +20080808 +zhangjian +123465 +9958123 +159159 +5508386 +wangwei +5205201314 +woaini5201314 +888666 +52013141314 +qweqweqwe +1122334455 +123456789z +585858 +33333333 +aa123123 +qwertyuiop123 +q111111 +9638527410 +911911 +qqq111 +5213344 +sunshine +liu123456 +abcdef +zhendeaini +007007 +555888 +qq111111 +jiushiaini +mnbvcxz +xiaoqiang +445566 +nicholas +dongdong +123456abcd +111qqq +aptx4869 +258456 +wobuzhidao +qazxsw +123789456 +zhang123456 +7215217758991 +1234567890123 +...... +huang123 +maomao +222333 +wangyang +123456789aa +1.23457E+11 +1234566 +1230456 +1a2b3c4d +13141314 +a7758521 +123456zxc +123456as +forever +s123456 +12348765 +xxxxxx +asdf123 +a1b2c3d4 +246810 +333666 +mingming +000123 +jiajia +12qwaszx +ffffff +112233445566 +77585211314 +520131400 +aa123456789 +wpc000821 +WANGJING +woaini1314520 +  +000111 +qq1314520 +1234512345 +147147 +123456qaz +q123123 +123456ab +xiaofeng +wodemima +shanshan +w2w2w2 +666999 +123456w +321456 +feifei +dragon +computer +dddddd +zhangjie +baobao +x123456 +q1w2e3 +chenchen +12345679 +131452 +caonima123 +asdf123456 +tangkai +52013140 +longlong +ssssss +www123 +1234568 +q1q1q1q1 +asdfghjkl123 +14789632 +123456711 +michael +tingting +woshishei +asd123456789 +1314258 +sunliu66 +qwert12345 +235689 +565656 +1234569 +ww123456 +1314159 +5211314521 +123456789w +123123aa +139.com@163.com +111111q +hao123456 +52tiance +19830122 +y123456 +110119120 +1231230 +sj811212 +13579246810 +123.123 +superman +789123 +12345qwert +770880 +js77777 +zhangyang +686868 +@163.com +imzzhan +xiaoyu +7758521a +abc12345 +nihao123 +wokaonima +q11111 +623623623 +989898 +122333 +13800138000 +laopowoaini +787878 +123456l +a123123123 +198611 +332211 +tom.com +212121 +woaini123456 +wanglei +yang123456 +zhangqiang +zxcvbnm,./ +zhangyan +181818 +234567 +stryker +167669123 +laopo520 +2597758 +aa5201314 +139.com +5201314. +8888888888 +74107410 +zhanghao +77777777 +zhangyu +qwerty123 +zzb19860526 +qwertyu +5201314qq +198612 +q5201314 +999888 +369852 +121121 +1122334 +123456789asd +123zxc +a123321 +QWErtyUIO +456456456 +qq000000 +m123456 +q1w2e3r4t5 +woainilaopo +123456789* +131425 +liuchang +85208520 +zhangjing +c123456 +asdfghjk +qq1234 +asdzxc +hao123 +777888 +131131 +woainia +beyond +zhang520 +556688 +123456qw +wangchao +woshiniba +168888 +7758991 +woshizhu +ainiyiwannian +LAOpo521 +abcd123456789 +qwerasdf +123456ok +woshinidie +huanhuan +1hxboqg2s +meiyoumima +456321 +QQQ123456 +1314 +898989 +123456798 +pp.com@163.com +mm123456 +123698741 +a520520 +z321321 +asasas +YANG123 +584211314 +1234561 +123456789+ +miaomiao +789789789 +7788520 +AAAAAAa +h123456 +3838438 +l123456789 +198511 +ABCDEFG123 +zhangjun +123qaz +198512 +2525775 +54545454 +789632145 +831213 +10101010 +xiaohe +19861010 +10203 +woshishen +0987654321 +yj2009 +wangqiang +198411 +1314520a +xiaowei +123456000 +123987 +love520 +caonimabi +qwe123123 +010101 +qq666666 +789987 +10161215 +liangliang +qwert123 +112112 +qianqian +1a2b3c +198410 +nuttertools +goodluck +zhangxin +18n28n24a5 +liuyang +998877 +woxiangni +7788250 +a147258369 +zhangliang +16897168 +223344 +123123456 +a1b2c3 +killer +321123 +pp.com +chen123456 +wangpeng +753159 +775852100 +1478963 +1213141516 +369369369 +1236987 +123369 +12345a +bugaosuni +13145201314520 +110112 +123456... +JIAOJIAO +100100 +1314520123 +19841010 +7758521123 +shangxin +woshiwo +12312300 +xingxing +yingying +1233210 +34416912 +qq12345 +qweasd123 +nishizhu +19861020 +qwe123456789 +808080 +1310613106 +456789123 +44444444 +123123qq +3141592653 +556677 +xx123456 +jianjian +a1111111 +0.123456 +198610 +loveme +tianshi +woxihuanni +11235813 +252525 +225588 +lovelove +mengmeng +7758258520 +xiaoming +shanghai +huyiming +6543210 +a7758258 +7788414 +123456789.. +Jordan +nishiwode +ZHUZHU +1314woaini +chenjian +131415 +xy123456 +123456520 +a00000 +jiang123 +WOAIMAMA +monkey +7418529630 +lingling +987456321 +w5201314 +qwer123456 +198412 +asdasd123 +zzzzzzzz +1q1q1q +741741 +987456 +19851010 +2587758 +456654 +Iloveyou1314 +q12345 +imissyou +daniel +aipai +2222222 +0147258369 +123456789l +q1234567 +963963 +123123123123 +125521 +womendeai +baobei520 +19861015 +667788 +000000. +zhangtao +yy123456 +chen123 +nishishui +789654 +liu123 +19861212 +1230 +19841020 +wangjun +wangliang +zhangpeng +woainimama +zhangchao +5201314q +19841025 +123567 +aaaa1111 +123456+ +134679258 +668899 +811009 +qaz123456789 +123456789qwe +111112 +130130 +19861016 +wozhiaini +198712 +123... +abcde12345 +abcd12345 +wanggang +llllll +5121314 +456258 +125125 +qq7758521 +369963 +987987 +142857 +poiuytrewq +qqq123 +323232 +baobei +g227w212 +962464 +mylove +p1a6s3m +202020 +19491001 +963258 +hhhhhh +2582587758 +wangfeng +tiancai +11111111111 +summer +wangwang +asd123123 +19841024 +xinxin +0.0.0. +19861012 +19861210 +8888888 +zhanghui +wenwen +635241 +ASDFGHJ +19861023 +1234567890. +888168 +19861120 +tianya +123aaa +111aaa +123456789aaa +8008208820 +123123q +football +dandan +www123456789 +19861026 +qingqing +315315 +1111122222 +171204jg +19861021 +5555555 +AS123456789 +qqqwww +19861024 +yahoo.com +19861225 +1qaz1qaz +19871010 +1029384756 +123258 +zxcv123 +19861123 +1314520. +aidejiushini +123qwe123 +198711 +operation +19861025 +yu123456 +19851225 +wangshuai +19841015 +520521 +wangyan +19861011 +7007 +123456zz +521000 +198311 +299792458 +112211 +****** +00000 +princess +qwer123 +51201314 +password1 +qazwsxedcrfv +LOVE5201314 +198312 +198510 +888888888 +1314521521 +internet +z123123 +a147258 +696969 +1234321 +476730751 +5201314789 +012345 +19861022 +welcome +aqwe518951 +19861121 +HUANGwei +868686 +wanghao +NIAIWOMA +xiaojian +19851120 +19851212 +100000 +19841022 +zhangbin +shadow +mmmmmm +000... +1357913579 +77585217758521 +19861216 +19841016 +az123456 +zxcv1234 +19841023 +wu123456 +163163 +2008520085 +pppppp +789654123 +EtnXtxSa65 +19851025 +woaiwolaopo +ww111111 +woaini110 +123455 +19841026 +19881010 +www163com +159357456 +fangfang +19851015 +19861013 +19861220 +12312 +19861018 +19861028 +a11111111 +19841018 +119911 +AI123456 +198211 +55555 +zhangkai +wangxin +xihuanni +19871024 +19861218 +16899168 +1010110 +nimabi +19861125 +52013143344 +131452000 +19871020 +freedom +baobao520 +winner +123456m +12312312 +''' + + lower_pass = lower_pass_txt.split("\n") + + for lp in lower_pass: + if not lp: continue + if lp == find['username']: + return False,'The user name of the current panel is: {}, which is too simple and poses security risks'.format(lp) + p1 = password_salt(public.md5(lp),uid=1) + if p1 == find['password']: + return False,'The current panel password is too simple and there is a security risk' + + lp = lp.upper() + if lp == find['username']: + return False,'The user name of the current panel is: {}, which is too simple and poses security risks'.format(lp) + p1 = password_salt(public.md5(lp),uid=1) + if p1 == find['password']: + return False,'The current panel password is too simple and there is a security risk' + + lower_rule = 'qwertyuiopasdfghjklzxcvbnm1234567890' + for s in lower_rule: + for i in range(12): + if not i: continue + lp = s * i + if lp == find['username']: + return False,'The user name of the current panel is: {}, which is too simple and poses security risks'.format(lp) + p1 = password_salt(public.md5(lp),uid=1) + if p1 == find['password']: + return False,'The current panel password is too simple and there is a security risk' + + lp = s.upper() * i + if lp == find['username']: + return False,'The user name of the current panel is: {}, which is too simple and poses security risks'.format(lp) + p1 = password_salt(public.md5(lp),uid=1) + if p1 == find['password']: + return False,'The current panel password is too simple and there is a security risk' + if not is_strong_password(find["password"]): + return False, 'The current panel password is too simple and there is a security risk' + return True,'Risk-free' + +salt = None + +def password_salt(password,username=None,uid=None): + ''' + @name 为指定密码加盐 + @author hwliang<2020-07-08> + @param password string(被md5加密一次的密码) + @param username string(用户名) 可选 + @param uid int(uid) 可选 + @return string + ''' + global salt + if not salt: + salt = public.M('users').where('id=?',(uid,)).getField('salt') + if salt: + salt = salt[0] + else: + salt = "" + return public.md5(public.md5(password+'_bt.cn')+salt) + + +def is_strong_password(password): + """判断密码复杂度是否安全 + + 非弱口令标准:长度大于等于7,分别包含数字、小写、大写、特殊字符。 + @password: 密码文本 + @return: True/False + @author: linxiao<2020-9-19> + """ + + if len(password) < 7: + return False + + import re + digit_reg = "[0-9]" # 匹配数字 +1 + lower_case_letters_reg = "[a-z]" # 匹配小写字母 +1 + upper_case_letters_reg = "[A-Z]" # 匹配大写字母 +1 + special_characters_reg = r"((?=[\x21-\x7e]+)[^A-Za-z0-9])" # 匹配特殊字符 +1 + + regs = [digit_reg, + lower_case_letters_reg, + upper_case_letters_reg, + special_characters_reg] + + grade = 0 + for reg in regs: + if re.search(reg, password): + grade += 1 + + if grade == 4 or (grade == 3 and len(password) >= 9): + return True + return False diff --git a/class_v2/safe_warning_v2/sw_panel_path.py b/class_v2/safe_warning_v2/sw_panel_path.py new file mode 100644 index 00000000..5a1b52fc --- /dev/null +++ b/class_v2/safe_warning_v2/sw_panel_path.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 面板安全入口检测 +# ------------------------------------------------------------------- + +import os,sys,re,public + +_title = 'Safe entrance' +_version = 1.0 # 版本 +_ps = "Check the security entrance security of the panel" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_panel_path.pl") +_tips = [ + "Please modify the security entrance on the [Settings] page", + "Set the binding domain name on the [Settings] page, or set authorized IP restrictions", + "Note: Please do not set up too simple safety entrance, which may cause safety hazards" + ] +_help = '' +_remind = 'This solution can strengthen the panel login entry protection, improve server security. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-03> + @return tuple (status,msg) + ''' + + p_file = '/www/server/panel/data/domain.conf' + if public.readFile(p_file): + return True,'Risk-free' + + p_file = '/www/server/panel/data/limitip.conf' + if public.readFile(p_file): + return True,'Risk-free' + + + p_file = '/www/server/panel/data/admin_path.pl' + p_body = public.readFile(p_file) + if not p_body: return False,'No security entrance is set, the panel is at risk of being scanned' + p_body = p_body.strip('/').lower() + if p_body == '': return False,'No security entrance is set, the panel is at risk of being scanned' + + lower_path = ['root','admin','123456','123','12','1234567','12345','1234','12345678','123456789','abc','bt'] + + if p_body in lower_path: + return False,'The security entrance is: {}, too simple, there are potential safety hazards'.format(p_body) + + lower_rule = 'qwertyuiopasdfghjklzxcvbnm1234567890' + for s in lower_rule: + for i in range(12): + if not i: continue + lp = s * i + if p_body == lp: + return False,'The security entrance is: {}, too simple, there are potential safety hazards'.format(p_body) + + return True,'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_panel_port.py b/class_v2/safe_warning_v2/sw_panel_port.py new file mode 100644 index 00000000..94129830 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_panel_port.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 面板端口检测 +# ------------------------------------------------------------------- + +import os,sys,re,public + +_title = 'Panel port' +_version = 1.0 # 版本 +_ps = "Check whether the current panel port is safe" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-03' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_panel_port.pl") +_tips = [ + "Please modify the default panel port on the [Settings] page", + "Note: Servers with [Security Group] should release the new port in the [Security Group] in advance to prevent the new port cannot be opened" + ] +_help = '' +_remind = 'This solution can strengthen the panel protection and reduce the risk of the panel being attacked. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-03> + @return tuple (status,msg) + ''' + + port_file = '/www/server/panel/data/port.pl' + port = public.readFile(port_file) + if not port: return True,'Rick-free' + port = int(port) + if port != 7800: + return True,'Rick-free' + return False,'The panel port is the default port ({}), which may cause unnecessary security risks'.format(port) + diff --git a/class_v2/safe_warning_v2/sw_panel_swing.py b/class_v2/safe_warning_v2/sw_panel_swing.py new file mode 100644 index 00000000..0d8ddb3f --- /dev/null +++ b/class_v2/safe_warning_v2/sw_panel_swing.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# Mysql 弱口令检测 +# ------------------------------------------------------------------- + +import public, os +_title = 'Panel login alarm' +_version = 1.0 # 版本 +_ps = "Panel login alarm" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_panel_swing.pl") +_tips = [ + "Enable it in [Settings] - [Notification]" +] +_help = '' +_remind = 'This solution can strengthen the panel protection and reduce the risk of the panel being attacked. ' +def check_run(): + ''' + @name 面板登录告警是否开启 + @time 2022-08-12 + @author lkq@bt.cn + ''' + send_type = "" + tip_files = ['panel_login_send.pl','login_send_type.pl','login_send_mail.pl','login_send_dingding.pl'] + for fname in tip_files: + filename = 'data/' + fname + if os.path.exists(filename): + return True, 'Risk-free' + return False, 'Please enable it in [Settings] - [Notification]' + + + diff --git a/class_v2/safe_warning_v2/sw_passwd_repeat.py b/class_v2/safe_warning_v2/sw_passwd_repeat.py new file mode 100644 index 00000000..e880801e --- /dev/null +++ b/class_v2/safe_warning_v2/sw_passwd_repeat.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + + +_title = 'Check password reuse limit' +_version = 1.0 # 版本 +_ps = "Detect whether to limit password reuse times" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_passwd_repeat.pl") +_tips = [ + "Configuration file backup: cp -p /etc/pam.d/system-auth /etc/pam.d/system-auth.bak", + "Add or modify remember=5 after [password sufficient] in [/etc/pam.d/system-auth] file" +] +_help = '' +_remind = 'This scheme enhances server access control protection by limiting the number of times login passwords are reused. ' + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + try: + cfile = '/etc/pam.d/system-auth' + conf = public.readFile(cfile) + rep = r"password(\s*)sufficient.*remember(\s*)=(\s*)[1-9]+" + tmp = re.search(rep, conf) + if tmp: + return True, 'Risk-free' + else: + return False, 'Unlimited password reuse' + except: + return True, 'Risk-free' + diff --git a/class_v2/safe_warning_v2/sw_php_backdoor.py b/class_v2/safe_warning_v2/sw_php_backdoor.py new file mode 100644 index 00000000..573c2722 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_php_backdoor.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lwh +# ------------------------------------------------------------------- +# Time: 2023-08-07 +# ------------------------------------------------------------------- +# PHP.ini挂马 +# ------------------------------------------------------------------- + + +import sys, os + +os.chdir('/www/server/panel') +sys.path.append("class/") + +import public, re, os + +_title = 'PHP configuration file failure detection' +_version = 1.0 # 版本 +_ps = "Check if PHP config file is suspended" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-8-7' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_php_backdoor.pl") +_tips = [ + "According to the risk description, find the corresponding version of PHP plugin in [Software Store] - [Running Environment]. ", + "On the [Configuration] page, go to auto_prepend_file or auto_append_file, delete the rest, save and restart PHP." +] + +_help = '' +_remind = 'This solution removes malicious code from php configuration files and suggests a full Trojan scan of the server to remove backdoor files and fix website vulnerabilities. ' +_type = 'web' + + +def check_run(): + path = "/www/server/php" + # 获取目录下的文件夹 + dirs = os.listdir(path) + result = {} + for dir in dirs: + if dir in ["52", "53", "54", "55", "56", "70", "71", "72", "73", "74", "80", "81"]: + file_path = path + "/" + dir + "/etc/php.ini" + if os.path.exists(file_path): + # 获取文件内容 + try: + php_ini = public.readFile(file_path) + if re.search("\nauto_prepend_file\\s?=\\s?(.+)", php_ini): + prepend = re.findall("\nauto_prepend_file\\s?=\\s?(.+)", php_ini) + if "data:;base64" in prepend[0]: + result[dir] = ["auto_prepend_file"] + if re.search("\nauto_append_file\\s?=\\s?(.+)", php_ini): + append = re.findall("\nauto_append_file\\s?=\\s?(.+)", php_ini) + if "data:;base64" in append[0]: + if dir in result: + result[dir].append("auto_append_file") + else: + result[dir] = ["auto_append_file"] + except: + pass + if result: + ret = "" + for i in result: + ret += "【PHP" + i + "】A field where malicious code is present:" + ",".join(result[i]) + "\n" + return False, ret + else: + return True, "Risk-free" diff --git a/class_v2/safe_warning_v2/sw_php_disable_functions.py b/class_v2/safe_warning_v2/sw_php_disable_functions.py new file mode 100644 index 00000000..4e404ced --- /dev/null +++ b/class_v2/safe_warning_v2/sw_php_disable_functions.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# PHP未禁用函数 +# ------------------------------------------------------------------- + + +import sys,os +os.chdir('/www/server/panel') +sys.path.append("class/") + +import public,re,os + +_title = 'PHP Dangerous Functions' +_version = 1.0 # 版本 +_ps = "PHP Dangerous Functions" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_php_php_disable_funcation.pl") +_tips = [ + "[disable_functions] is not set in the [php.ini] file to disable dangerous functions such as system, exec, passthru, shell_exec, popen, proc_open, etc.", + "Tips: [disable_functions] is not set in the [php.ini] file to disable dangerous functions such as system, exec, passthru, shell_exec, popen, proc_open, etc." + ] + +_help = '' +_remind = 'This solution can strengthen the protection of the website and reduce the risk of server intrusion. ' +def check_run(): + path ="/www/server/php" + #获取目录下的文件夹 + dirs = os.listdir(path) + result={} + for dir in dirs: + if dir in ["52","53","54","55","56","70","71","72","73","74","80","81"]: + file_path=path+"/"+dir+"/etc/php.ini" + if os.path.exists(file_path): + #获取文件内容 + try: + php_ini = public.readFile(file_path) + if re.search("\ndisable_functions\\s?=\\s?(.+)",php_ini): + disable_functions = re.findall("\ndisable_functions\\s?=\\s?(.+)",php_ini) + disa_fun=["system","exec","passthru","shell_exec","popen","proc_open","putenv"] + if len(disable_functions) > 0: + disable_functions= disable_functions[0].split(",") + for i2 in disa_fun: + if i2 not in disable_functions: + if dir in result: + result[dir].append(i2) + else: + result[dir]=[i2] + except: + pass + if result: + ret="" + for i in result: + ret+="[PHP "+i+"] Dangerous functions that are not disabled are as follows:"+",".join(result[i])+"\n" + return False,ret + else: + return True, "Risk-free" + diff --git a/class_v2/safe_warning_v2/sw_php_display_errors.py b/class_v2/safe_warning_v2/sw_php_display_errors.py new file mode 100644 index 00000000..8bc6f5dc --- /dev/null +++ b/class_v2/safe_warning_v2/sw_php_display_errors.py @@ -0,0 +1,60 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lwh +# ------------------------------------------------------------------- +# Time: 2023-08-05 +# ------------------------------------------------------------------- +# PHP未关闭错误提示 +# ------------------------------------------------------------------- + + +import sys, os + +os.chdir('/www/server/panel') +sys.path.append("class/") + +import public, re, os + +_title = 'PHP is giving an error message' +_version = 1.0 # 版本 +_ps = "Check if PHP is turned off" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-8-5' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_php_display_errors.pl") +_tips = [ + "According to the risk description, find the corresponding version of PHP plugin in [Software Store] - [Running Environment], and in [Configuration Modification] page, set display_errors to off and save." +] + +_help = '' +_remind = "PHP error prompts may reveal sensitive information about your site's applications; This solution prevents website information from leaking by turning off the [display_errors] option" +_type = 'web' + + +def check_run(): + path = "/www/server/php" + # 获取目录下的文件夹 + dirs = os.listdir(path) + result = [] + for dir in dirs: + if dir in ["52", "53", "54", "55", "56", "70", "71", "72", "73", "74", "80", "81"]: + file_path = path + "/" + dir + "/etc/php.ini" + if os.path.exists(file_path): + # 获取文件内容 + try: + php_ini = public.readFile(file_path) + if re.search("\ndisplay_errors\\s?=\\s?(.+)", php_ini): + status = re.findall("\ndisplay_errors\\s?=\\s?(.+)", php_ini) + if 'On' in status or 'on' in status: + result.append(dir[0]+'.'+dir[1]) # 中间加个. + except: + pass + if result: + ret = "The PHP versions that do not turn off error messages are: {}".format('、'.join(result)) + return False, ret + else: + return True, "Risk-free" diff --git a/class_v2/safe_warning_v2/sw_php_expose.py b/class_v2/safe_warning_v2/sw_php_expose.py new file mode 100644 index 00000000..de6d8932 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_php_expose.py @@ -0,0 +1,62 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# PHP存在版本泄露 +# ------------------------------------------------------------------- + +# import sys,os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import re,public,os + + +_title = 'PHP version leaked' +_version = 1.0 # 版本 +_ps = "PHP version leaked" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_php_expose.pl") +_tips = [ + "Set [expose_php] in the [php.ini] file and configure it to Off", + "Tips: Set [expose_php] in the [php.ini] file and configure it to Off" + ] + +_help = '' +_remind = 'This solution can prevent the disclosure of sensitive information on the website and reduce the possibility of server intrusion. ' + + +def check_run(): + path ="/www/server/php" + #获取目录下的文件夹 + dirs = os.listdir(path) + resulit=[] + for dir in dirs: + if dir in ["52","53","54","55","56","70","71","72","73","74","80","81"]: + file_path=path+"/"+dir+"/etc/php.ini" + if os.path.exists(file_path): + #获取文件内容 + try: + php_ini = public.readFile(file_path) + #查找expose_php + if re.search("\nexpose_php\\s*=\\s*(\\w+)",php_ini): + expose_php = re.search("\nexpose_php\\s*=\\s*(\\w+)",php_ini).groups()[0] + if expose_php.lower() == "off": + pass + else: + resulit.append(dir) + except: + pass + if resulit: + return False, "The affected php versions are as follows: ["+",".join(resulit)+"], please set expose_php to Off in php.ini" + else: + return True, "Risk-free" + +# check_run() \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_ping.py b/class_v2/safe_warning_v2/sw_ping.py new file mode 100644 index 00000000..dc23d239 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ping.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测是否禁ping +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'ICMP detection' +_version = 1.0 # 版本 +_ps = "Check whether ICMP access is allowed (Block ICMP)" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ping.pl") +_tips = [ + "Turn on the [Block ICMP] function in the [Security] page", + "Note: The server IP or domain name cannot be Ping after it is turned on, please set according to actual needs" + ] + +_help = '' +_remind = 'This scheme can reduce the risk of the real IP of the server being found, and enhance the security of the server. ' + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-05> + @return tuple (status,msg) + ''' + + cfile = '/etc/sysctl.conf' + conf = public.readFile(cfile) + rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)" + tmp = re.search(rep,conf) + if tmp: + if tmp.groups(0)[0] == '1': + return True,'Rick-free' + + return False,'If the [Block ICMP] function is not enabled, there is a risk that the server will be attacked or scanned by ICMP' diff --git a/class_v2/safe_warning_v2/sw_pingin.py b/class_v2/safe_warning_v2/sw_pingin.py new file mode 100644 index 00000000..5e0bb2dd --- /dev/null +++ b/class_v2/safe_warning_v2/sw_pingin.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测是否禁ping +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = '2222222' +_version = 1.0 # 版本 +_ps = "222222222(禁Ping)" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ping_in.pl") +_tips = [ + "Enable Disable Ping in Security page ", + "Note: You cannot ping the server IP or domain name after opening, please set it according to your actual needs" + ] + +_help = '' + + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-05> + @return tuple (status,msg) + ''' + try: + cfile = '/proc/sys/net/ipv4/icmp_echo_ignore_all' + conf = public.readFile(cfile) + if conf: + if int(conf)!=1: + return False,'The "Ban Ping" function is not enabled at present, there is a risk that the server is attacked by ICMP or swept' + else: + return True,"Risk-free" + except: + return True,"Risk-free" \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_pip_poison.py b/class_v2/safe_warning_v2/sw_pip_poison.py new file mode 100644 index 00000000..db9aa393 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_pip_poison.py @@ -0,0 +1,29 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + +_title = 'Pypi supply chain poisoning detection' +_version = 1.0 # 版本 +_ps = "Pypi supply chain poisoning detection" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-14' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_pip_poison.pl") +_tips = [ + "Execute the command btpip uninstall [detected malicious library name]", +] +_help = '' +_remind = 'This solution can remove vulnerable packages from the server and prevent them from being exploited by hackers. Before executing the solution command, make sure that the malicious library name is not a dependency library of normal business, otherwise it may affect the operation of the website. ' + +def check_run(): + pip = public.ExecShell("btpip freeze | grep -E \"istrib|djanga|easyinstall|junkeldat|libpeshka|mumpy|mybiubiubiu|nmap" + "-python|openvc|python-ftp|pythonkafka|python-mongo|python-mysql|python-mysqldb|python" + "-openssl|python-sqlite|virtualnv|mateplotlib|request=\"")[0].strip() + if 'command not found' in pip or 'command not found' in pip: + return True, 'Risk-free,pip is not installed' + if pip: + pip = pip.split('\n') + return False, '【{}】security risk in the python library, please deal with it as soon as possible'.format('、'.join(pip)) + else: + return True, 'Risk-free' + diff --git a/class_v2/safe_warning_v2/sw_protected_hardlinks.py b/class_v2/safe_warning_v2/sw_protected_hardlinks.py new file mode 100644 index 00000000..29b8c6e3 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_protected_hardlinks.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lwh +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 开启地址空间布局随机化 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = 'Whether hard link protection is enabled' +_version = 1.0 # 版本 +_ps = "Whether hard link protection is enabled" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-21' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_protected_hardlinks.pl") +_tips = [ + "operation is as follows: sysctl -w fs.protected_hardlinks=1", +] +_help = '' +_remind = "By enabling this kernel parameter, users can no longer create soft or hard links to files they do not own, which reduces the vulnerability of privileged programs to access insecure filesystems and enhances server security." + + +def check_run(): + try: + if os.path.exists("/proc/sys/fs/protected_hardlinks"): + protected_hardlinks = public.ReadFile("/proc/sys/fs/protected_hardlinks") + if int(protected_hardlinks) != 1: + return False, 'Hard link protection is not enabled' + else: + return True, "Risk-free" + except: + return True, "Risk-free" + return True, "Risk-free" diff --git a/class_v2/safe_warning_v2/sw_protected_symlinks.py b/class_v2/safe_warning_v2/sw_protected_symlinks.py new file mode 100644 index 00000000..c4271764 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_protected_symlinks.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lwh +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 开启软链接保护 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = 'Whether to enable soft link protection' +_version = 1.0 # 版本 +_ps = "Whether to enable soft link protection" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-21' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_protected_symlinks.pl") +_tips = [ + "operation is as follows: sysctl -w fs.protected_symlinks=1", +] +_help = '' +_remind = "By enabling this kernel parameter, symbolic links are only allowed to be tracked if they are outside the sticky global writable directory, or if the directory owner matches the owner of the symbolic link. Banning such symbolic links helps mitigate vulnerabilities in insecure file systems based on privileged program access." + + +def check_run(): + try: + if os.path.exists("/proc/sys/fs/protected_symlinks"): + protected_symlinks = public.ReadFile("/proc/sys/fs/protected_symlinks") + if int(protected_symlinks) != 1: + return False, 'Soft link protection is not enabled' + else: + return True, "Risk-free" + except: + return True, "Risk-free" + return True, "Risk-free" diff --git a/class_v2/safe_warning_v2/sw_redis_pass.py b/class_v2/safe_warning_v2/sw_redis_pass.py new file mode 100644 index 00000000..11ad179a --- /dev/null +++ b/class_v2/safe_warning_v2/sw_redis_pass.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# Redis 密码检测 +# ------------------------------------------------------------------- + +import os,sys,re,public + +_title = 'Redis weak password' +_version = 1.0 # 版本 +_ps = "Check if the current Redis password is secure" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_redis_pass.pl") +_tips = [ + "1.Redis passwords are too simple" + "2.Please change your password in time" + ] +_help = '' +_remind = 'This solution reduces the risk of server intrusion by strengthening the database login password. ' + +def check_run(): + try: + p_file = '/www/server/redis/redis.conf' + p_body = public.readFile(p_file) + if not p_body: return True, 'Risk-free' + + tmp = re.findall(r"^\s*requirepass\s+(.+)", p_body, re.M) + if not tmp: return True, 'Risk-free' + + redis_pass = tmp[0].strip() + pass_info=public.ReadFile("/www/server/panel/config/weak_pass.txt") + if not pass_info: return True, 'Risk-free' + pass_list = pass_info.split('\n') + for i in pass_list: + if i==redis_pass: + return False, 'The Redis password [%s] is a weak password, please change the password'%redis_pass + return True, 'Risk-free' + except: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_redis_port.py b/class_v2/safe_warning_v2/sw_redis_port.py new file mode 100644 index 00000000..f9372705 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_redis_port.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# Redis安全检测 +# ------------------------------------------------------------------- + +import os,sys,re,public + +_title = 'Redis security' +_version = 1.0 # 版本 +_ps = "Check whether the current Redis is safe" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_redis_port.pl") +_tips = [ + "If not necessary, please do not configure Redis bind to 0.0.0.0", + "If bind is 0.0.0.0, be sure to set an access password for Redis", + "Do not use too simple password as Redis access password", + "Strong passwords are recommended: numeric, upper - and lowercase, special characters, and no less than seven characters long.", + "If there is a security problem in Redis, it will lead to a high probability of server intrusion, please be sure to deal with it carefully." + ] +_help = '' +_remind = 'This solution can strengthen the protection of Redis database and reduce the risk of server intrusion. When fixing the risk, make sure that you have opened the IP access of the relevant website, and change the access password synchronously to prevent the server from being unable to access Redis. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-03> + @return tuple (status,msg) + ''' + + p_file = '/www/server/redis/redis.conf' + p_body = public.readFile(p_file) + if not p_body: return True,'Rick-free' + + tmp = re.findall(r"^\s*bind\s+(0\.0\.0\.0)",p_body,re.M) + if not tmp: return True,'Rick-free' + + tmp = re.findall(r"^\s*requirepass\s+(.+)",p_body,re.M) + if not tmp: return False,'Reids allows public internet connection, but no Redis password is set, which is extremely dangerous, please deal with it immediately' + + redis_pass = tmp[0].strip() + if not is_strong_password(redis_pass): + return False, 'Redis access password is too simple, and there are security risks' + + return True,'Risk-free' + + +def is_strong_password(password): + """判断密码复杂度是否安全 + + 非弱口令标准:长度大于等于7,分别包含数字、小写、大写、特殊字符。 + @password: 密码文本 + @return: True/False + """ + + if len(password) < 7: + return False + + import re + digit_reg = "[0-9]" # 匹配数字 +1 + lower_case_letters_reg = "[a-z]" # 匹配小写字母 +1 + upper_case_letters_reg = "[A-Z]" # 匹配大写字母 +1 + special_characters_reg = r"((?=[\x21-\x7e]+)[^A-Za-z0-9])" # 匹配特殊字符 +1 + + regs = [digit_reg, + lower_case_letters_reg, + upper_case_letters_reg, + special_characters_reg] + + grade = 0 + for reg in regs: + if re.search(reg, password): + grade += 1 + + if grade == 4 or (grade >= 2 and len(password) >= 9): + return True + return False diff --git a/class_v2/safe_warning_v2/sw_risk_file.py b/class_v2/safe_warning_v2/sw_risk_file.py new file mode 100644 index 00000000..4f2485ce --- /dev/null +++ b/class_v2/safe_warning_v2/sw_risk_file.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, public + +_title = 'Check for dangerous remote access files' +_version = 1.0 # 版本 +_ps = "Check for dangerous remote access files:hosts.equiv、.rhosts、.netrc" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_risk_file.pl") +_tips = [ + "Delete the .rhosts and .netrc files in the home directory and delete the hosts.equiv file in the root directory", + "Follow the prompts to find the risk file and delete it" +] +_help = '' +_remind = 'This solution removes all vulnerable files, preventing them from being exploited by hackers to gain access to the server. You can backup files before deleting them to prevent them from affecting the operation of your website. ' + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + result_list = [] + cfile = ['hosts.equiv', '.rhosts', '.netrc'] + for cf in cfile: + file = public.ExecShell('find / -maxdepth 3 -name {}'.format(cf)) + if file[0]: + result_list = result_list+file[0].split('\n') + result = '、'.join(reform_list(result_list)) + if result: + return False, 'High-risk files, delete the following files as soon as possible\"{}\"'.format(result) + else: + return True, 'Risk-free' + + +def reform_list(check_list): + """处理列表里的空字符串""" + return [i for i in check_list if (i is not None) and (str(i).strip() != '')] + diff --git a/class_v2/safe_warning_v2/sw_site_logs.py b/class_v2/safe_warning_v2/sw_site_logs.py new file mode 100644 index 00000000..cc2178ef --- /dev/null +++ b/class_v2/safe_warning_v2/sw_site_logs.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 网站日志检测 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'Web log detection' +_version = 1.0 # 版本 +_ps = "Check all site log retention cycles for compliance" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_site_logs.pl") +_tips = [ + "In the [Scheduled Task] page, set the log cutting of the specified website or all websites once a day and save more than 180 copies ", + "Tip: According to Article 21 of the Network Security Law, network logs should be retained for no less than six months." + ] + +_help = '' +_remind = 'This solution can help discover the risk of external intrusions and vulnerabilities by retaining logs to ensure the security of your website. Make sure you have enough log space. ' + + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-03> + @return tuple (status,msg) + ''' + + if public.M('crontab').where('sType=? AND sName=? AND save>=?',('logs','ALL',180)).count(): + return True,'Risk-free' + + log_list = public.M('crontab').where('sType=? AND save=?',('logs',180)).field('sName').select() + ok_logs = [] + for ml in log_list: + if ml['sName'] in ok_logs: continue + ok_logs.append(ml['sName']) + + not_logs = [] + site_list = public.M('sites').field('name').select() + for s in site_list: + if s['name'] in ok_logs: continue + if s['name'] in not_logs: continue + not_logs.append(s['name']) + + if not_logs: + return False ,'The following website log preservation cycle is not compliant:
                                    ' + ('
                                    '.join(not_logs)) + + return True,'Risk-free' + + diff --git a/class_v2/safe_warning_v2/sw_site_spath.py b/class_v2/safe_warning_v2/sw_site_spath.py new file mode 100644 index 00000000..152e74aa --- /dev/null +++ b/class_v2/safe_warning_v2/sw_site_spath.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测网站是否开启防跨站 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'Website anti-cross-site detection' +_version = 1.0 # 版本 +_ps = "Check the website to prevent cross-site" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_site_spath.pl") +_tips = [ + "On the [WebSite] page, [Settings]-[Site Directory], turn on the [Anti-cross-site attack (open_basedir)] function" + ] + +_help = '' +_remind = 'This solution can prevent hackers from stealing server information across directories, and strengthen the protection of the website. ' + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-05> + @return tuple (status,msg) + ''' + not_uini = [] + site_list = public.M('sites').where('status=? AND project_type=?',(1,'PHP')).field('name,path').select() + for s in site_list: + path = get_site_run_path(s['name'],s['path']) + user_ini = path + '/.user.ini' + if os.path.exists(user_ini): continue + not_uini.append(s['name']) + if not_uini: + return False,'The following websites are not enabled for cross-site prevention:
                                    ' + ('
                                    '.join(not_uini)) + return True,'Rick-free' + + + +webserver_type = None +setupPath = '/www/server' +def get_site_run_path(siteName,sitePath): + ''' + @name 获取网站运行目录 + @author hwliang<2020-08-05> + @param siteName(string) 网站名称 + @param sitePath(string) 网站根目录 + @return string + ''' + global webserver_type,setupPath + if not webserver_type: + webserver_type = public.get_webserver() + path = None + if webserver_type == 'nginx': + filename = setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r'\s*root\s+(.+);' + tmp1 = re.search(rep,conf) + if tmp1: path = tmp1.groups()[0] + + elif webserver_type == 'apache': + filename = setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r'\s*DocumentRoot\s*"(.+)"\s*\n' + tmp1 = re.search(rep,conf) + if tmp1: path = tmp1.groups()[0] + else: + filename = setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' + if os.path.exists(filename): + conf = public.readFile(filename) + rep = r"vhRoot\s*(.*)" + path = re.search(rep,conf) + if not path: + path = None + else: + path = path.groups()[0] + + if not path: + path = sitePath + + return path \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_site_ssl.py b/class_v2/safe_warning_v2/sw_site_ssl.py new file mode 100644 index 00000000..caf5f0fc --- /dev/null +++ b/class_v2/safe_warning_v2/sw_site_ssl.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 网站证书检测 +# ------------------------------------------------------------------- + +import os,sys,re,public + +_title = 'Website certificate (SSL)' +_version = 1.0 # 版本 +_ps = "Check whether all websites deploy SSL" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_site_ssl.pl") +_tips = [ + "Please consider deploying an SSL certificate for your website to improve its security" + ] +_help = '' +_remind = 'SSL certificates ensure that the communication on your website is secure, preventing hackers from stealing data while it is in transit. ' + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-04> + @return tuple (status,msg) + ''' + + site_list = public.M('sites').field('id,name').select() + + not_ssl_list = [] + for site_info in site_list: + ng_conf_file = '/www/server/panel/vhost/nginx/' + site_info['name'] + '.conf' + if not os.path.exists(ng_conf_file): continue + s_body = public.readFile(ng_conf_file) + if not s_body: continue + if s_body.find('ssl_certificate') == -1: + not_ssl_list.append(site_info['name']) + + if not_ssl_list: + return False ,'The following sites do not deploy SSL certificates:
                                    ' + ('
                                    '.join(not_ssl_list)) + + return True,'Rick-free' + + + diff --git a/class_v2/safe_warning_v2/sw_site_ssl_expire.py b/class_v2/safe_warning_v2/sw_site_ssl_expire.py new file mode 100644 index 00000000..34942f17 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_site_ssl_expire.py @@ -0,0 +1,77 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 网站证书过期检测 +# ------------------------------------------------------------------- + +import os,sys,re,public,OpenSSL,time + +_title = 'Website certificate expired' +_version = 1.0 # 版本 +_ps = "Check whether the websites SSL has expired" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_site_ssl_expire.pl") +_tips = [ + "Please renew or replace with a new SSL certificate for your site to avoid affecting normal website access", + "After the SSL certificate expires, the user will be prompted by the browser to access the website as insecure, and most browsers will block access, seriously affecting online business" + ] +_help = '' +_remind = 'SSL certificates ensure that the communication on your website is secure, preventing hackers from stealing data while it is in transit. ' +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-04> + @return tuple (status,msg) + ''' + + site_list = public.M('sites').field('id,name').select() + + not_ssl_list = [] + s_time = time.time() + for site_info in site_list: + ng_conf_file = '/www/server/panel/vhost/nginx/' + site_info['name'] + '.conf' + if not os.path.exists(ng_conf_file): continue + s_body = public.readFile(ng_conf_file) + if not s_body: continue + if s_body.find('ssl_certificate') == -1: continue + + cert_file = '/www/server/panel/vhost/cert/{}/fullchain.pem'.format(site_info['name']) + if not os.path.exists(cert_file): continue + + cert_timeout = get_cert_timeout(cert_file) + if s_time > cert_timeout: + not_ssl_list.append(site_info['name'] + ' Expiration: ' + public.format_date("%Y-%m-%d",cert_timeout)) + + if not_ssl_list: + return False ,'The following sites SSL certificate has expired:
                                    ' + ('
                                    '.join(not_ssl_list)) + + return True,'Rick-free' + + + +# 获取证书到期时间 +def get_cert_timeout(cert_file): + try: + cert = split_ca_data(public.readFile(cert_file)) + x509 = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, cert) + cert_timeout = bytes.decode(x509.get_notAfter())[:-1] + return int(time.mktime(time.strptime(cert_timeout, '%Y%m%d%H%M%S'))) + except: + return time.time() + 86400 + + + +# 拆分根证书 +def split_ca_data(cert): + datas = cert.split('-----END CERTIFICATE-----') + return datas[0] + "-----END CERTIFICATE-----\n" diff --git a/class_v2/safe_warning_v2/sw_ssh_clientalive.py b/class_v2/safe_warning_v2/sw_ssh_clientalive.py new file mode 100644 index 00000000..78db2c26 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_clientalive.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# SSH 空闲超时时间检测 +# ------------------------------------------------------------------- +import re,public,os + + +_title = 'SSH idle timeout detection' +_version = 1.0 # 版本 +_ps = "SSH idle timeout detection" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_clientalive.pl") +_tips = [ + "Set [ClientAliveInterval] in the [/etc/ssh/sshd_config] file to be between 600 and 900", + "Tip: The recommended SSH idle timeout time is: 600-900" + ] + +_help = '' +_remind = 'This scheme can enhance the security of SSH service, after the repair of SSH connection for a long time without operation will automatically quit, to prevent others from using. ' + + +def check_run(): + ''' + @name SSH 空闲超时检测 + @time 2022-08-10 + @author lkq<2020-08-10> + @return tuple (status,msg) + ''' + if os.path.exists('/etc/ssh/sshd_config'): + try: + info_data=public.ReadFile('/etc/ssh/sshd_config') + if info_data: + if re.search(r'ClientAliveInterval\s+\d+',info_data): + clientalive=re.findall(r'ClientAliveInterval\s+\d+',info_data)[0] + #clientalive 需要大于600 小于900 + if int(clientalive.split(' ')[1]) >= 600 and int(clientalive.split(' ')[1]) <= 900: + return True,'Rick-free' + else: + return False,'The current SSH idle timeout time is: '+clientalive.split(' ')[1]+', it is recommended to set it to 600-900' + else: + return True,'Rick-free' + except: + return True,'Rick-free' + return True,'Rick-free' + +def repaired(): + ''' + @name 修复ssh最大连接数 + @author lkq<2022-08-10> + @return tuple (status,msg) + ''' + # 暂时不处理 + pass diff --git a/class_v2/safe_warning_v2/sw_ssh_forward.py b/class_v2/safe_warning_v2/sw_ssh_forward.py new file mode 100644 index 00000000..47c913e7 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_forward.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +#coding: utf-8 + +import sys,re,os,public + +_title = 'Use the graphical interface to check after restricting SSH login' +_version = 1.0 # 版本 +_ps = "Use the graphical interface to check after restricting SSH login" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-14' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_forward.pl") +_tips = [ + "Modify X11Forwarding to no in [/etc/ssh/sshd_config]", + ] +_help = '' +_remind = 'This can be used to enforce SSH login security and speed up SSH connections. Notethe fix turns off X11 graphical forwarding, so do not configure it if you need to use it. ' + +def check_run(): + conf = '/etc/ssh/sshd_config' + if not os.path.exists(conf): + return True, 'Risk-free' + result = public.ReadFile(conf) + rep = r'.*?X11Forwarding\s*?yes' + tmp = re.search(rep, result) + if tmp: + if tmp.group()[0] == '#': + return True, 'Risk-free' + else: + return False, 'SSH graphical forwarding is not disabled' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ssh_hosts.py b/class_v2/safe_warning_v2/sw_ssh_hosts.py new file mode 100644 index 00000000..9e34de08 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_hosts.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +#coding: utf-8 + +import os,sys,re,public + + +_title = 'ssh access control list checking' +_version = 1.0 # 版本 +_ps = "Set up an ssh login whitelist" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_hosts.pl") +_tips = [ + "add ALL:ALL in 【/etc/hosts.deny】", + "add sshd:【visitor IP address】 in【/etc/hosts.allow】" +] +_help = '' +_remind = 'This scheme will block the rest of the IP except the white list to login the server, and enhance the security protection of the server. Note that this solution is risky; be sure to add an IP address to the server before you fix it.' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + cfile = '/etc/hosts.deny' + conf = public.ReadFile(cfile) + if 'all:all' in conf or 'ALL:ALL' in conf: + return True, 'Risk-free' + else: + return False, 'ssh login whitelist is not set' diff --git a/class_v2/safe_warning_v2/sw_ssh_login_grace.py b/class_v2/safe_warning_v2/sw_ssh_login_grace.py new file mode 100644 index 00000000..6270e1f0 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_login_grace.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lwh +# ------------------------------------------------------------------- +# Time: 2023-11-22 +# ------------------------------------------------------------------- +# SSH 登录超时时间 +# ------------------------------------------------------------------- + +import re, public, os + +_title = 'SSH Login timeout configuration detection' +_version = 1.0 # 版本 +_ps = "SSH Login timeout configuration detection" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_login_grace.pl") +_tips = [ + "Set [LoginGraceTime] to 60 in [/etc/ssh/sshd_config] file", +] + +_help = '' +_remind = 'Setting the LoginGraceTime parameter to a small number minimizes the risk of a successful brute force attack on the SSH server. It will also limit the number of concurrent unauthenticated connections. ' + + +def check_run(): + ''' + @name SSH 登录超时配置检测 + @author lwh<2023-11-22> + @return tuple (status,msg) + ''' + path = '/etc/ssh/sshd_config' + if os.path.exists(path): + try: + output, err = public.ExecShell(r"grep -P '^(?!#)[\s]*LoginGraceTime.*$' {}".format(path)) + if output == '' and err == '': + return False, 'The SSH login timeout configuration is not enabled' + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ssh_maxauth.py b/class_v2/safe_warning_v2/sw_ssh_maxauth.py new file mode 100644 index 00000000..a4b1ae08 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_maxauth.py @@ -0,0 +1,64 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# SSH 最大连接数检测 +# ------------------------------------------------------------------- + +import re, public, os + +_title = 'SSH connection attempts' +_version = 1.0 # 版本 +_ps = "SSH connection attempts" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_maxauth.pl") +_tips = [ + "Set [MaxAuthTries] to 3-5 in the [/etc/ssh/sshd_config] file", + "Tips: Set [MaxAuthTries] to 3-5 in the [/etc/ssh/sshd_config] file" +] + +_help = '' +_remind = 'This reduces the risk of server intrusion by reducing the maximum number of SSH connections. Note Before fixing, confirm the number of simultaneous connections that SSH needs to support to prevent affecting normal business operations. ' + + +def check_run(): + ''' + @name 检测ssh最大连接数 + @author lkq<2020-08-10> + @return tuple (status,msg) + ''' + + if os.path.exists('/etc/ssh/sshd_config'): + try: + info_data = public.ReadFile('/etc/ssh/sshd_config') + if info_data: + if re.search(r'MaxAuthTries\s+\d+', info_data): + maxauth = re.findall(r'MaxAuthTries\s+\d+', info_data)[0] + # max 需要大于3 小于6 + if int(maxauth.split(' ')[1]) >= 3 and int(maxauth.split(' ')[1]) <= 6: + return True, 'Rick-free' + else: + return False, 'The current maximum number of SSH connections is: ' + maxauth.split(' ')[1] + ', please set it to 3-5' + else: + return True, 'Rick-free' + except: + return True, 'Rick-free' + return True, 'Rick-free' + + +def repaired(): + ''' + @name 修复ssh最大连接数 + @author lkq<2020-08-10> + @return tuple (status,msg) + ''' + # 暂时不处理 + pass diff --git a/class_v2/safe_warning_v2/sw_ssh_minclass.py b/class_v2/safe_warning_v2/sw_ssh_minclass.py new file mode 100644 index 00000000..9d946f4d --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_minclass.py @@ -0,0 +1,44 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# SSH密码复杂度检查 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = 'SSH password complexity check' +_version = 1.0 # 版本 +_ps = "SSH password complexity check" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_minclass.pl") +_tips = [ + "【/etc/security/pwquality.conf】 Set password complexity to require 3 or 4 types of characters, such as lowercase letters, uppercase letters, numbers, and special characters. like:", + "minclass=3", +] +_help = '' +_remind = 'This scheme strengthens the complexity of the server login password and reduces the risk of being successfully exploded. ' + +def check_run(): + try: + p_file = '/etc/security/pwquality.conf' + p_body = public.readFile(p_file) + if not p_body: return True, 'Risk-free' + tmp = re.findall(r"\s*minclass\s+=\s+(.+)", p_body, re.M) + if not tmp: return True, 'Risk-free' + minlen = tmp[0].strip() + if int(minlen) <3: + return False, '【%s】set the minclass setting to 3 or 4 in the file' % p_file + return True, 'Risk-free' + except: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ssh_notpass.py b/class_v2/safe_warning_v2/sw_ssh_notpass.py new file mode 100644 index 00000000..512f9fc9 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_notpass.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lkq +# ------------------------------------------------------------------- +# Time: 2022-08-10 +# ------------------------------------------------------------------- +# 禁止SSH空密码登录 +# ------------------------------------------------------------------- +import re,public,os + + +_title = 'Prohibit SSH login with empty password' +_version = 1.0 # 版本 +_ps = "Prohibit SSH login with empty password" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-8-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_notpasswd.pl") +_tips = [ + "Set [PermitEmptyPasswords] in the [/etc/ssh/sshd_config] file to configure it to no", + "Tips: Set [PermitEmptyPasswords] in the [/etc/ssh/sshd_config] file to configure it to no" + ] + +_help = '' +_remind = 'This scheme prevents the server from logging in with an empty password. Note that the server cannot login with an empty password after the repair. Ensure that the login password is configured synchronously for related business access. ' + + +def check_run(): + ''' + @name 检测禁止SSH空密码登录 + @author lkq<2020-08-10> + @return tuple (status,msg) + ''' + + if os.path.exists('/etc/ssh/sshd_config'): + try: + info_data = public.ReadFile('/etc/ssh/sshd_config') + if info_data: + if re.search('\nPermitEmptyPasswords\\s*yes', info_data): + return False, 'The [PermitEmptyPasswords] value is: yes, please set it to no' + else: + return True, 'Rick-free' + except: + return True, 'Rick-free' + return True, 'Rick-free' \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_ssh_passmax.py b/class_v2/safe_warning_v2/sw_ssh_passmax.py new file mode 100644 index 00000000..d49c2c1c --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_passmax.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检查SSH密码失效时间 +# ------------------------------------------------------------------- +import sys, os +os.chdir('/www/server/panel') +sys.path.append("class/") +import os, sys, re, public + +_title = 'Check SSH password expiration time' +_version = 1.0 # 版本 +_ps = "Check SSH password expiration time" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_passmax.pl") +_tips = [ + "[/etc/login.defs] Use a non-password login key pair. Please ignore this, and set the PASS_MAX_DAYS parameter to between 90-180 in /etc/login.defs", + "PASS_MAX_DAYS 90 You need to execute the command to set the root password expiration time at the same time. The command is as follows: chage --maxdays 90 root", +] +_help = '' +_remind = 'This solution reduces the risk of a breach by setting an expiration date for the root login password. Note that the repair scheme will invalidate the root password after the expiration date, so it is necessary to modify the password before the expiration date. If the modification is not timely, it may affect the operation of some services. ' + +def check_run(): + try: + p_file = '/etc/login.defs' + p_body = public.readFile(p_file) + if not p_body: return True, 'Risk-free' + tmp = re.findall("\nPASS_MAX_DAYS\\s+(.+)", p_body, re.M) + if not tmp: return True, 'Risk-free' + maxdays = tmp[0].strip() + #60-180之间 + if int(maxdays) < 90 or int(maxdays) > 180: + return False, '【%s】Set PASS_MAX_DAYS to between 90-180 in the file' % p_file + return True, 'Risk-free' + except: + return True, 'Risk-free' + diff --git a/class_v2/safe_warning_v2/sw_ssh_passmin.py b/class_v2/safe_warning_v2/sw_ssh_passmin.py new file mode 100644 index 00000000..228cd54d --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_passmin.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检查SSH密码失效时间 +# ------------------------------------------------------------------- +import sys, os +os.chdir('/www/server/panel') +sys.path.append("class/") +import os, sys, re, public + +_title = 'Check minimum interval between SSH password changes' +_version = 1.0 # 版本 +_ps = "Check minimum interval between SSH password changes" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_passmin.pl") +_tips = [ + "[/etc/login.defs] PASS_MIN_DAYS should be set to be greater than or equal to 7", + "PASS_MIN_DAYS 7 needs to execute the command at the same time to set the expiration time of the root password. The command is as follows: chage --mindays 7 root", +] +_help = '' +_remind = 'This solution sets the number of days after the SSH login password is changed, it cannot be changed again. ' + +def check_run(): + try: + p_file = '/etc/login.defs' + p_body = public.readFile(p_file) + if not p_body: return True, 'Risk-free' + tmp = re.findall("\nPASS_MIN_DAYS\\s+(.+)", p_body, re.M) + if not tmp: return True, 'Risk-free' + maxdays = tmp[0].strip() + #7-14 + if int(maxdays) < 7: + return False, '【%s】In the file, PASS_MIN_DAYS is greater than or equal to 7' % p_file + return True, 'Risk-free' + except: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ssh_port.py b/class_v2/safe_warning_v2/sw_ssh_port.py new file mode 100644 index 00000000..b45b5a19 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_port.py @@ -0,0 +1,77 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# SSH安全检测 +# ------------------------------------------------------------------- + + +import os,sys,re,public,json + +_title = 'SSH security' +_version = 1.0 # 版本 +_ps = "Check whether the SSH port of the current server is safe" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-04' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_port.pl") +_tips = [ + "Modify the SSH port on the [Security] page, and consider turning off [SSH password login] in [SSH security management], and turning on [SSH key login]", + "If SSH connection service is not required, it is recommended to disable SSH service on the [Security] page", + "Through the [System Firewall] plug-in or in the [Security Group] modify the release behavior of the SSH port to limit the IP to enhance security", + "Use [Fail2ban] plug-in to protect SSH service" + ] + +_help = '' +_remind = "This solution reduces the risk of a breach by changing the default SSH login port. Noteafter the fix, you'll need to change the SSH port that the relevant business logs on to. " + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2022-08-18> + @return tuple (status,msg) + + @example + status, msg = check_run() + if status: + print('OK') + else: + print('Warning: {}'.format(msg)) + + ''' + port = public.get_sshd_port() + + version = public.readFile('/etc/redhat-release') + if not version: + version = public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace(r'\l','').strip() + else: + version = version.replace('release ','').replace('Linux','').replace('(Core)','').strip() + + status = public.get_sshd_status() + + fail2ban_file = '/www/server/panel/plugin/fail2ban/config.json' + if os.path.exists(fail2ban_file): + try: + fail2ban_config = json.loads(public.readFile(fail2ban_file)) + if 'sshd' in fail2ban_config.keys(): + if fail2ban_config['sshd']['act'] == 'true': + return True,'Fail2ban is enable' + except: pass + + if not status: + return True,'SSH service is not enabled' + if port != '22': + return True,'The default SSH port has been modified' + + result = public.check_port_stat(int(port),public.GetLocalIp()) + if result == 0: + return True,'Rick-free' + + return False,'The default SSH port ({}) has not been modified, and the access IP limit configuration has not been done, there is a risk of SSH breaching'.format(port) + diff --git a/class_v2/safe_warning_v2/sw_ssh_root.py b/class_v2/safe_warning_v2/sw_ssh_root.py new file mode 100644 index 00000000..56fa9091 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_root.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检查SSH root是否可以登录 +# ------------------------------------------------------------------- +import sys, os +os.chdir('/www/server/panel') +sys.path.append("class/") +import os, sys, re, public + +_title = 'Check if SSH root can log in' +_version = 1.0 # 版本 +_ps = "Check if SSH root can log in" # 描述 +_level = 0 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_root.pl") +_tips = [ + "Add [ PermitRootLogin no ] parameter in [/etc/ssh/sshd_config]", + "PermitRootLogin no", +] +_help = '' +_remind = 'SSH remote login as root is not possible after this solution is fixed' + + +def check_run(): + #ssh 检查root 登录 + if os.path.exists('/etc/ssh/sshd_config'): + try: + info_data = public.ReadFile('/etc/ssh/sshd_config') + if info_data: + if re.search(r'PermitRootLogin\s+no', info_data): + return True, 'Risk-free' + else: + return True, 'Risk-free' + return False, 'The parameter [PermitRootLogin] in /etc/ssh/sshd_config is configured as: "yes", please set it to "no"' + except: + return True, 'Risk-free' + return True, 'Risk-free' \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_ssh_security.py b/class_v2/safe_warning_v2/sw_ssh_security.py new file mode 100644 index 00000000..7774bdd6 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_security.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# SSH密码复杂度检查 +# ------------------------------------------------------------------- +import sys,os +os.chdir('/www/server/panel') +sys.path.append("class/") +import os,sys,re,public + +_title = 'SSH password length check' +_version = 1.0 # 版本 +_ps = "SSH password length check" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_security.pl") +_tips = [ + "In the [/etc/security/pwquality.conf] file, set minlen (minimum password length) to 9-32 bits", + "minlen=9", + ] +_help = '' +_remind = 'This solution enforces a minimum login password length, reducing the risk of server blow-up. ' + + +def check_run(): + try: + p_file = '/etc/security/pwquality.conf' + p_body = public.readFile(p_file) + if not p_body: return True, 'Risk-free' + tmp = re.findall(r"\s*minlen\s+=\s+(.+)", p_body, re.M) + if not tmp: return True, 'Risk-free' + minlen = tmp[0].strip() + if int(minlen) < 9: + return False, 'In the [%s] file, set minlen (minimum password length) to 9-32 characters'%p_file + + return True, 'Risk-free' + except: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_ssh_v2.py b/class_v2/safe_warning_v2/sw_ssh_v2.py new file mode 100644 index 00000000..9baa2331 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_ssh_v2.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + + +_title = 'Whether to use encrypted remote administration ssh' +_version = 1.0 # 版本 +_ps = "Detect whether secure socket layer encryption is used to transmit information to avoid eavesdropping on sensitive information" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_ssh_v2.pl") +_tips = [ + "Add or modify Protocol 2 in [/etc/ssh/sshd_config] file ", + "Then run the command systemctl restart sshd to restart the process ", +] +_help = '' +_remind = 'This scheme can enhance the protection of SSH communication and avoid sensitive data leakage.' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + cfile = '/etc/ssh/sshd_config' + conf = public.readFile(cfile) + rep = r"\nProtocol 2" + tmp = re.search(rep, conf) + if tmp: + return True, 'Risk-free' + else: + return False, 'Remote administration of ssh without secure socket encryption' diff --git a/class_v2/safe_warning_v2/sw_strace_backdoor.py b/class_v2/safe_warning_v2/sw_strace_backdoor.py new file mode 100644 index 00000000..3ed5f19e --- /dev/null +++ b/class_v2/safe_warning_v2/sw_strace_backdoor.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + +_title = 'strace obtains login credentials backdoor detection' +_version = 1.0 # 版本 +_ps = "Detect user information leakage via strace command during process" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_strace_backdoor.pl") +_tips = [ + "The ps aux command checks whether there are sshd login credentials read through strace", + "ps aux | grep strace", + "If the process is filtered out, use the kill -9 [pid] command to stop the process" +] +_help = '' +_remind = 'detects the existence of hacker intrusion on the server, and the hacker behavior can be interrupted in time through the scheme command to prevent the server from being further invaded and controlled. ' + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + sshd_pid = public.ExecShell('ps aux|grep "sshd -D"|grep -v grep|awk {\'print$2\'}')[0].strip() + result = public.ExecShell('ps aux')[0].strip() + rep = 'strace.*' + sshd_pid + '.*trace=read,write' + tmp = re.search(rep, result) + if tmp: + return False, 'Malicious process that steals sshd login information through strace' + else: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_su_root.py b/class_v2/safe_warning_v2/sw_su_root.py new file mode 100644 index 00000000..a5730110 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_su_root.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, re, public + + +_title = 'Check if the user outside the whell group su is disabled as root' +_version = 1.0 # 版本 +_ps = "Check if the PAM authentication module is used to forbid users outside the wheel group su to be root" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_su_root.pl") +_tips = [ + "In the file [/etc/pam.d/su] add auth sufficient pam_rootok.so and auth required pam_wheel.so group=wheel", + "To configure the user to switch to root, add the user to the wheel group with gpasswd -d a username wheel", +] +_help = '' +_remind = 'This scheme enhances the protection of server permissions by forbidding low-privilege users to switch to root user. Make sure that there is no need for the business to switch root before fixing, otherwise ignore this risk item.' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + cfile = '/etc/pam.d/su' + if not os.path.exists(cfile): + return True, 'Risk-free' + conf = public.readFile(cfile) + rep1 = r'[^#](\s*)auth(\s*)sufficient(\s*)pam_rootok.so' + tmp1 = re.search(rep1, conf) + if not tmp1: + return False, 'Normal user su is not prohibited as the root user' + rep2 = r'[^#](\s*)auth(\s*)required(\s*)pam_wheel.so(\s*)group(\s*)=(\s*)wheel' + tmp2 = re.search(rep2, conf) + if not tmp2: + return True, 'Risk-free, But the whell group user can su is not configured' + else: + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_sudoers_nopasswd.py b/class_v2/safe_warning_v2/sw_sudoers_nopasswd.py new file mode 100644 index 00000000..aec56fc9 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_sudoers_nopasswd.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, sys, public + +_title = 'Check if an empty password sudo is allowed' +_version = 1.0 # 版本 +_ps = "Check if an empty password sudo is allowed" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-21' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_sudoers_nopasswd.pl") +_tips = [ + "Open /etc/sudoers or /etc/sudoers.d ", + "Remove or comment the line of the [NOPASSWD] marker ", + "Or handle security risks with one-click fixes." +] +_help = '' +_remind = 'When sudo uses the [NOPASSWD] flag, it allows users to execute commands using sudo without authenticating. This insecure configuration can lead to hackers gaining advanced privileges on the server.' + + +def check_run(): + ''' + @name 开始检测 + @author lwh<2023-11-21> + @return tuple (status,msg) + ''' + risk_list = [] + sudo_file = "/etc/sudoers" + sudo_dir = "/etc/sudoers.d/" + if not os.path.exists(sudo_file): + return True, 'Risk-free' + try: + output, err = public.ExecShell('grep -P \'^(?!#).*[\\s]+NOPASSWD[\\s]*\\:.*$\' {}'.format(sudo_file)) + if err == '' and output != '': + risk_list.append(sudo_file) + if os.path.exists(sudo_dir): + import glob + for filename in glob.glob(os.path.join(sudo_dir, '*')): + output, err = public.ExecShell('grep -P \'^(?!#).*[\\s]+NOPASSWD[\\s]*\\:.*$\' {}'.format(filename)) + if err == '' and output != '': + risk_list.append(filename) + if len(risk_list)>0: + return False, 'The following sudo files contain the NOPASSWD flag:【{}】'.format('、'.join(risk_list)) + except: + return True, 'Risk-free' + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_suid_dumpable.py b/class_v2/safe_warning_v2/sw_suid_dumpable.py new file mode 100644 index 00000000..3a734470 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_suid_dumpable.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: lwh +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 开启软链接保护 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = 'Whether core dumps are restricted' +_version = 1.0 # 版本 +_ps = "Whether core dumps are restricted" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-22' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_suid_dumpable.pl") +_tips = [ + "operation is as follows: sysctl -w fs.suid_dumpable=0", +] +_help = '' +_remind = 'Core dumps of setuid programs are more likely to contain sensitive data, and limiting the ability of any setuid program to write to core files reduces the risk of sensitive data leakage.' + + +def check_run(): + try: + if os.path.exists("/proc/sys/fs/suid_dumpable"): + suid_dumpable = public.ReadFile("/proc/sys/fs/suid_dumpable") + if int(suid_dumpable) != 0: + return False, 'The core dump is not limited, and information leakage may occur.' + else: + return True, "Risk-free" + except: + return True, "Risk-free" + return True, "Risk-free" diff --git a/class_v2/safe_warning_v2/sw_system_user.py b/class_v2/safe_warning_v2/sw_system_user.py new file mode 100644 index 00000000..e9e86781 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_system_user.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# coding: utf-8 +# Date 2022/1/12 + +import sys,os + +_title = 'System backdoor user detection' +_version = 1.0 # 版本 +_ps = "System backdoor user detection" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2021-01-12' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_system_user.pl") +_tips = [ + "Delete backdoor user in command line", + "Note: If there is a backdoor user, it means that your server has been invaded" +] +_help = '' +_remind = 'This scheme will remove the backdoor users with the same privileges as the root user, and enhance the protection of the server permission control. If it is a business requirement, this risk term is ignored. ' +def check_run(): + ''' + @name 开始检测 + @author lkq<2021-01-12> + @return tuple (status,msg) + ''' + ret=[] + cfile = '/etc/passwd' + if os.path.exists(cfile): + f=open(cfile,'r') + for i in f: + i=i.strip().split(":") + if i[2]=='0' and i[3]=='0': + if i[0]=='root':continue + ret.append(i[0]) + if ret: + return False, 'There is a backdoor user: %s'%''.join(ret) + return True, 'No backdoor users are currently found' \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_tcp_syn_cookie.py b/class_v2/safe_warning_v2/sw_tcp_syn_cookie.py new file mode 100644 index 00000000..057479fa --- /dev/null +++ b/class_v2/safe_warning_v2/sw_tcp_syn_cookie.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, sys, public + + +_title = 'TCP-SYNcookie protection detection' +_version = 1.0 # 版本 +_ps = "Check whether TCP-SYNcookie protection is enabled to mitigate syn flood attacks" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-09' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_tcp_syn_cookie.pl") +_tips = [ + "Add net.ipv4.tcp_syncookies=1 in [/etc/sysctl.conf] file ", + "Then execute the command sysctl -p to effect the configuration ", +] +_help = '' +_remind = 'This scheme can alleviate network flood attacks and enhance the stability of server operation. ' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + + cfile = '/etc/sysctl.conf' + conf = public.readFile(cfile) + rep = r"\nnet.ipv4.tcp_syncookies(\s*)=(\s*)1" + tmp = re.search(rep, conf) + if tmp: + return True, 'Risk-free' + else: + return False, 'TCP-SYNcookie protection is not enabled' + diff --git a/class_v2/safe_warning_v2/sw_telnet_server.py b/class_v2/safe_warning_v2/sw_telnet_server.py new file mode 100644 index 00000000..dffd0f42 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_telnet_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# coding: utf-8 + +import sys, os, public +_title = 'Disable non-encrypted remote management telnet' +_version = 1.0 # 版本 +_ps = "Turn off non-encrypted remote management telnet checks" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-15' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_telnet_server.pl") +_tips = [ + "Use encrypted remote management sshd service as much as possible, and close unsafe telnet service", + "systemctl stop telnet.socket stop telnet service" +] +_help = '' +_remind = 'This scheme shuts down the insecure telnet service, reducing the risk of data leakage. If the business requires telnet, this risk term is ignored. ' + +def check_run(): + result = public.ExecShell('systemctl is-active telnet.socket')[0].strip() + if 'active' == result: + return False, 'telnet service is not closed' + else: + return True, 'Risk-free' + diff --git a/class_v2/safe_warning_v2/sw_time_out.py b/class_v2/safe_warning_v2/sw_time_out.py new file mode 100644 index 00000000..8726eea2 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_time_out.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, re, public + + +_title = 'Check if the command-line interface timeout is set' +_version = 1.0 # 版本 +_ps = "Check if the command-line interface timeout is set" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_time_out.pl") +_tips = [ + "Add tmout=300 in the file [/etc/profile], and the waiting time is not more than 600 seconds ", + "Execute source /etc/profile to make the configuration work ", +] +_help = '' +_remind = 'This solution will make the server command line over a certain period of time does not operate automatically shut down, can strengthen the security of the server. ' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + cfile = '/etc/profile' + conf = public.readFile(cfile) + rep = r'(tmout|TMOUT)(\s*)=(\s*)([1-9][^0-9]|[1-9][0-9][^0-9]|[1-5][0-9][0-9][^0-9]|600[^0-9])' + tmp = re.search(rep, conf) + if tmp: + return True, 'Risk-free' + else: + return False, 'No command line timeout is configured for exit' diff --git a/class_v2/safe_warning_v2/sw_tmp_malware.py b/class_v2/safe_warning_v2/sw_tmp_malware.py new file mode 100644 index 00000000..b26280e2 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_tmp_malware.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# coding: utf-8 + +import os, sys, public + +_title = 'Check the tmp directory for the existence of abnormal Trojan files' +_version = 1.0 # 版本 +_ps = "Check the tmp directory for the existence of abnormal Trojan files" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-11-22' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_tmp_malware.pl") +_tips = [ + "Delete the detected abnormal Trojan file according to the description", +] +_help = '' +_remind = 'This file conforms to the characteristics of a Trojan file. It is recommended to delete and reinstall the nginx server, and conduct a comprehensive security check on the server' + + +def check_run(): + ''' + @name 开始检测 + @author lwh<2023-11-22> + @return tuple (status,msg) + ''' + list1 = ['/var/tmp/systemd-private-56d86f7d8382402517f3b51625789161d2cb-chronyd.service-jP37av','/var/tmp/systemd-private-56d86f7d8382402517f3b5-jP37av','/tmp/systemd-private-56d86f7d8382402517f3b5-jP37av','/var/tmp/count','/var/tmp/count.txt','/var/tmp/backkk','/var/tmp/msglog.txt'] + risk_file = [] + for filename in list1: + if not os.path.exists(filename): + continue + if os.path.isdir(filename): + continue + risk_file.append(filename) + if len(risk_file) > 0: + return False, 'Abnormal Trojan file has been detected, please delete it ASAP:{}'.format('、'.join(risk_file)) + return True, 'Risk-free' diff --git a/class_v2/safe_warning_v2/sw_tomcat_pass.py b/class_v2/safe_warning_v2/sw_tomcat_pass.py new file mode 100644 index 00000000..dd7d6535 --- /dev/null +++ b/class_v2/safe_warning_v2/sw_tomcat_pass.py @@ -0,0 +1,61 @@ +#!/usr/bin/python +#coding: utf-8 + +import os, re, public + + +_title = 'tomcat Background Access Weak Password Detection' +_version = 1.0 # 版本 +_ps = "tomcat Background Access Weak Password Detection" # 描述 +_level = 3 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2023-03-13' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_tomcat_pass.pl") +_tips = [ + "Change password weak password in【/usr/local/bttomcat/tomcat/conf/tomcat-users.xml】", +] +_help = '' +_remind = 'This scheme by strengthening the tomcat background login password strength, reduce the risk of being exploded, to avoid hackers using tomcat to invade the server. ' + + +def check_run(): + ''' + @name 开始检测 + @return tuple (status,msg) + ''' + tomcat_conf = '/usr/local/bttomcat/tomcat{}/conf/tomcat-users.xml' + version = ['7','8','9'] + vul_list = [] + # 第一步先用正则找到密码的输入点 + rep = 'password(\\s*)=(\\s*)[\"\'](.*?)[\"\']' + for v in version: + annotator = 0 + if not os.path.exists(tomcat_conf.format(v)): + continue + with open(tomcat_conf.format(v)) as f: + lines = f.readlines() + # 通过逐行判断是否存在注释符闭合,以annotator作锁计数,存在左闭合则+1,存在右闭合-1,当annotator值为0时才不在闭合范围内 + for l in lines: + if '' in l: + annotator -= 1 + if '' in l: + continue + if annotator != 0: + continue + if 'manager-gui' in l and 'password' in l: + tmp = re.search(rep, l.rstrip()) + passwd = tmp.group(3).strip() + for d in get_pass_list(): + if passwd == d: + vul_list.append(v) + if vul_list: + return False, 'tomcat{} has a weak background password'.format('、'.join(vul_list)) + else: + return True, 'Risk-free' + + +# 获取弱口令字典 +def get_pass_list(): + pass_info = public.ReadFile("/www/server/panel/config/weak_pass.txt") + return pass_info.split('\n') diff --git a/class_v2/safe_warning_v2/sw_umask.py b/class_v2/safe_warning_v2/sw_umask.py new file mode 100644 index 00000000..6393083e --- /dev/null +++ b/class_v2/safe_warning_v2/sw_umask.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 用户缺省权限检查 +# ------------------------------------------------------------------- +# import sys, os +# os.chdir('/www/server/panel') +# sys.path.append("class/") +import os, sys, re, public + +_title = 'User default permission check' +_version = 1.0 # 版本 +_ps = "User Default Permission Check [/etc/profile]" # 描述 +_level = 2 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2022-08-10' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_umask.pl") +_tips = [ + "【/etc/profile] The umask set in the file is 002, which does not meet the requirements. It is recommended to set it to 027", + "Fix: modify umask to 027", +] +_help = '' +_remind = 'This scheme can strengthen the control of user permissions and avoid excessive user privileges. ' +def check_run(): + # 判断是否存在/etc/profile文件 + if os.path.exists("/etc/profile"): + # 读取文件内容 + profile = public.ReadFile("/etc/profile") + # 判断是否存在umask设置 + if re.search("umask 0",profile): + # 判断是否设置为027 + if re.search("umask 027",profile): + return True,"Risk-free" + else: + return True,"Risk-free" + # return False,"未设置umask为027" + else: + return True,"Risk-free" \ No newline at end of file diff --git a/class_v2/safe_warning_v2/sw_waf_install.py b/class_v2/safe_warning_v2/sw_waf_install.py new file mode 100644 index 00000000..d1ea4cdd --- /dev/null +++ b/class_v2/safe_warning_v2/sw_waf_install.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +#coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: hwliang +# ------------------------------------------------------------------- + +# ------------------------------------------------------------------- +# 检测风险用户 +# ------------------------------------------------------------------- + + +import os,sys,re,public + +_title = 'WAF firewall detection' +_version = 1.0 # 版本 +_ps = "Detect whether a WAF firewall is installed" # 描述 +_level = 1 # 风险级别: 1.提示(低) 2.警告(中) 3.危险(高) +_date = '2020-08-05' # 最后更新时间 +_ignore = os.path.exists("data/warning/ignore/sw_waf_install.pl") +_tips = [ + "It is recommended to install a WAF firewall, such as: Pagoda Nginx Firewall, Pagoda Apache Firewall, Nginx Free Firewall, etc.", + "Note: Only one type of WAF firewall can be installed. Installing too many WAF firewalls may cause your website to be abnormal and increase unnecessary server overhead" + ] + +_help = '' +_remind = "WAF firewall is the first line of defense to protect the server, can block external network attacks, to ensure the safe and stable operation of the website. " + +def check_run(): + ''' + @name 开始检测 + @author hwliang<2020-08-04> + @return tuple (status,msg) + ''' + + web_list = [ + '/www/server/nginx/sbin/nginx', + '/www/server/apache/bin/httpd', + '/usr/local/lsws/bin' + ] + is_install_web = False + for w in web_list: + if os.path.exists(w): + is_install_web = True + break + + if not is_install_web: + return True,'Risk-free' + + waf_list = [ + '/www/server/panel/plugin/btwaf/info.json', + '/www/server/panel/plugin/btwaf_httpd/info.json', + '/www/server/panel/plugin/free_waf/info.json', + '/usr/local/yunsuo_agent/uninstall', + '/etc/safedog', + '/usr/share/xmirror/scripts/uninstall.sh' + ] + + for waf in waf_list: + if os.path.exists(waf): + return True,'Risk-free' + + return True,'If the WAF firewall is not installed, the server website is vulnerable to attacks and there is a security risk' + diff --git a/class_v2/san_baseline_v2.py b/class_v2/san_baseline_v2.py new file mode 100644 index 00000000..9b39b786 --- /dev/null +++ b/class_v2/san_baseline_v2.py @@ -0,0 +1,1315 @@ +#!/usr/bin/python +# coding: utf-8 +# | Author: 1249648969@qq.com +# +-------------------------------------------------------------------- +# | 宝塔安全基线扫描 | +# +-------------------------------------------------------------------- +import sys, os + +if sys.version_info[0] == 2: + reload(sys) + sys.setdefaultencoding('utf-8') +os.chdir('/www/server/panel') +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import time, hashlib, sys, os, json, requests, re, public, random, string, requests + + +class san_baseline: + setupPath = '/www/server' + logPath = '/www/server/panel/data/san_baseline.log' + _Speed = None + config = '/www/server/panel/data/result.log' + repair_json='/www/server/panel/data/repair.json' + __repair=None + + def __init__(self): + + if not os.path.exists(self.logPath): + resutl={} + public.WriteFile(self.logPath,json.dumps(resutl)) + if not os.path.exists(self.config): + resutl = {} + public.WriteFile(self.config, json.dumps(resutl)) + if os.path.exists(self.repair_json): + self.__repair=json.loads(public.ReadFile(self.repair_json)) + + # SSH 安全扫描 + def ssh_security(self): + # 确保SSH MaxAuthTries 设置为3-6之间 + result = [] + ret = self.check_san_baseline(self.__repair['1']) + if not ret: result.append(self.__repair['1']) + ret = self.check_san_baseline(self.__repair['2']) + if not ret: result.append(self.__repair['2']) + ret = self.check_san_baseline(self.__repair['3']) + if not ret: result.append(self.__repair['3']) + ret = self.check_san_baseline(self.__repair['4']) + if not ret: result.append(self.__repair['4']) + ret = self.check_san_baseline(self.__repair['5']) + if not ret: result.append(self.__repair['5']) + ret = self.check_san_baseline(self.__repair['6']) + if not ret: result.append(self.__repair['6']) + return result + + ######面板安全监测########################## + + # 监测是否开启IP限制登陆 + def get_limitip(self): + if os.path.exists('/www/server/panel/data/limitip.conf'): + ret = public.ReadFile('/www/server/panel/data/limitip.conf') + if not ret: + return False + return True + else: + return False + + # 监测默认端口 + def get_port(self): + ret = public.ReadFile('/www/server/panel/data/port.pl') + ret = int(public.ReadFile('/www/server/panel/data/port.pl')) + if ret == 8888: + return False + else: + return True + + # 监测是否开启安全入口 + def get_admin_path(self): + if os.path.exists('/www/server/panel/data/admin_path.pl'): + return True + else: + return False + + # 域名绑定 + def get_domain(self): + if os.path.exists('/www/server/panel/data/domain.conf'): + return True + else: + return False + + # 查看APi是否开启 + def get_api_open(self): + if os.path.exists('/www/server/panel/config/api.json'): + ret = json.loads(public.ReadFile('/www/server/panel/config/api.json')) + if ret['open']: + return False + return True + else: + return True + + # 查看用户名是否是弱用户名 + def get_username(self): + userInfo = public.M('users').where("id=?", (1,)).field('id,username,password').find() + if userInfo['username'] == 'admin' or userInfo['username'] == 'root' or userInfo['username'] == 'password': + return False + else: + return True + + # 不安全的插件 + def get_secite(self): + if os.path.exists('/www/server/panel/plugin/ss'): + return False + else: + return True + + # 面板目录权限 + + # 面板安全扫描 + def panel_security(self): + result = [] + if not self.get_limitip(): + ret1 = { + 'id': 7, + "repaired": "0", + "harm": "警告", + "level": "1", + "type": "file", + "name": "宝塔面板登陆未开启(授权IP)限制登陆", + "Suggestions": "加固建议 :如果你的IP存在固定IP建议添加到面板的授权IP", + "repair": "首页-->面板设置->授权IP->添加IP", + } + result.append(ret1) + # 端口是否是8888端口 + get_port_default = self.get_port() + if not get_port_default: + ret1 = { + 'id': 8, + "repaired": "0", + "harm": "中", + "level": "2", + "type": "file", + "name": "宝塔面板登陆端口未修改", + "Suggestions": "加固建议 : 修改默认端口,例如8989或56641", + "repair": "首页-->面板设置->面板端口->修改端口-->保存", + } + result.append(ret1) + get_admin_path = self.get_admin_path() + if not get_admin_path: + ret1 = { + 'id': 9, + "repaired": "0", + "harm": "高", + "level": "3", + "type": "file", + "name": "宝塔面板登陆未开启安全入口", + "Suggestions": "加固建议 : 修改安全入口例如 /123456789", + "repair": "首页-->面板设置->安全入口->修改安全入口-->保存", + } + result.append(ret1) + get_username = self.get_username() + if not get_username: + ret1 = { + 'id': 11, + "harm": "高", + "repaired": "0", + "level": "3", + "type": "file", + "name": "面板用户名过于简单", + "Suggestions": "加固建议 : 修改为强用户名", + "repair": "例如:ad!@#min1750..", + } + result.append(ret1) + + get_secite = self.get_secite() + if not get_secite: + ret1 = { + 'id': 12, + "harm": "高", + "repaired": "0", + "level": "3", + "type": "file", + "name": "存在国家不允许的翻墙插件", + "Suggestions": "加固建议 : 建议删除SS插件", + "repair": "rm -rf /www/server/panel/plugin/ss", + } + result.append(ret1) + panel_chome = [ + { + 'id': 13, + "type": "chmod", + "file": "/www/server/panel/BTPanel", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 14, + "type": "chmod", + "file": "/www/server/panel/class", + "chmod": [600], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 15, + "type": "chmod", + "file": "/www/server/panel/config", + "chmod": [600], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 16, + "type": "chmod", + "file": "/www/server/panel/data", + "chmod": [600], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 17, + "type": "chmod", + "file": "/www/server/panel/install", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 18, + "type": "chmod", + "file": "/www/server/panel/logs", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 19, + "type": "chmod", + "file": "/www/server/panel/package", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 20, + "type": "chmod", + "file": "/www/server/panel/plugin", + "chmod": [644, 600], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 21, + "type": "chmod", + "file": "/www/server/panel/rewrite", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 22, + "type": "chmod", + "file": "/www/server/panel/ssl", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 23, + "type": "chmod", + "file": "/www/server/panel/temp", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 24, + "type": "chmod", + "file": "/www/server/panel/vhost", + "chmod": [600, 644], + "user": ['root'], + 'group': ['root'] + } + ] + for i in panel_chome: + if not self.check_san_baseline(i): + ret1 = { + 'id': i['id'], + "harm": "高", + "repaired": "1", + "level": "3", + "type": "file", + "name": "面板关键性文件权限错误%s" % i['file'], + "Suggestions": "加固建议 : %s 权限改为%s 所属用户为%s" % (i['file'], i['chmod'], i['user']), + "repair": "加固建议 : %s 权限改为%s 所属用户为%s" % (i['file'], i['chmod'], i['user']), + } + result.append(ret1) + + return result + + def php_id(self,php=None,php_2=None): + if php=='52':id =25;return id + if php == '53': id = 26;return id + if php == '54': id = 27;return id + if php == '55': id = 28;return id + if php == '56': id = 29;return id + if php == '70': id = 30;return id + if php == '71': id = 31;return id + if php == '72': id = 32;return id + if php == '73': id = 32.5;return id + + if php_2=='52':id =33;return id + if php_2 == '53': id = 34;return id + if php_2 == '54': id = 35;return id + if php_2 == '55': id = 36;return id + if php_2 == '56': id = 37;return id + if php_2 == '70': id = 38;return id + if php_2 == '71': id = 39;return id + if php_2 == '72': id = 40;return id + if php == '73': id = 40.5;return id + + # php版本泄露 + def php_version_info(self): + ret = [] + php_path = '/www/server/php/' + php_list = os.listdir(php_path) + if len(php_list) >= 1: + for i in php_list: + if os.path.isdir(php_path + i): + if os.path.exists(php_path + i + '/etc/php.ini'): + php_data = { + 'id': self.php_id(i), + "type": "file", + "harm": "中", + "level": "2", + "repaired": "1", + "name": "PHP 版本泄露", + "file": php_path + i + '/etc/php.ini', + "Suggestions": "加固建议, 在%s expose_php的值修改为Off中修改" % (php_path + i + '/etc/php.ini'), + "repair": "expose_php = Off", + "rule": [ + {"re": "\nexpose_php\\s*=\\s*(\\w+)", "check": {"type": "string", "value": ['Off']}}] + } + if not self.check_san_baseline(php_data): + ret.append(php_data) + return ret + + + # PHP 危险函数 + def php_error_funcation(self): + ret = [] + php_path = '/www/server/php/' + php_list = os.listdir(php_path) + if len(php_list) >= 1: + for i in php_list: + if os.path.isdir(php_path + i): + if os.path.exists(php_path + i + '/etc/php.ini'): + php_data = { + 'id': self.php_id(php='1',php_2=i), + "type": "diff", + "harm": "严重", + "level": "5", + "repaired": "1", + "name": "PHP%s 中存在危险函数未禁用" % i, + "file": php_path + i + '/etc/php.ini', + "Suggestions": "加固建议, 在%s 中 disable_functions= 修改成如下:" % (php_path + i + '/etc/php.ini'), + "repair": "disable_functions = passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv", + "rule": [ + {"re": "\ndisable_functions\\s?=\\s?(.+)", "check": {"type": "string", "value": [ + 'passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv']}}] + } + if not self.check_san_baseline(php_data): + ret.append(php_data) + return ret + + # 版本过旧 + def php_dir(self): + + php_version_dir = { + 'id':41, + "type": "dir", + "harm": "高", + "level": "3", + "repaired": "0", + "name": "PHP 5.2 版本过旧", + "file": '/www/server/php/52', + "Suggestions": "加固建议:不再使用php5.2 ", + "repair": "PHP 5.2 已经被淘汰建议升级更高的版本", + "rule": [] + } + if not self.check_san_baseline(php_version_dir): + return php_version_dir + return {} + + # php配置安全 + + + def php_security(self): + ret = [] + php_path = '/www/server/php/' + php_list = os.listdir(php_path) + if len(php_list) >= 1: + for i in php_list: + if os.path.isdir(php_path + i): + if os.path.exists(php_path + i + '/etc/php.ini'): + php_data = { + 'id': self.php_id(i), + "type": "file", + "harm": "中", + "level": "2", + "repaired": "1", + "name": "PHP%s 版本泄露" % i, + "file": php_path + i + '/etc/php.ini', + "Suggestions": "加固建议, 在%s expose_php的值修改为Off中修改" % (php_path + i + '/etc/php.ini'), + "repair": "expose_php = Off", + "rule": [ + {"re": "\nexpose_php\\s*=\\s*(\\w+)", "check": {"type": "string", "value": ['Off']}}] + } + if not self.check_san_baseline(php_data): + ret.append(php_data) + + if len(php_list) >= 1: + for i in php_list: + if os.path.isdir(php_path + i): + if os.path.exists(php_path + i + '/etc/php.ini'): + php_data = { + 'id': self.php_id(php='1', php_2=i), + "type": "diff", + "harm": "严重", + "level": "5", + "repaired": "1", + "name": "PHP%s 中存在危险函数未禁用" % i, + "file": php_path + i + '/etc/php.ini', + "Suggestions": "加固建议, 在%s 中 disable_functions= 修改成如下:" % (php_path + i + '/etc/php.ini'), + "repair": "disable_functions = passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv", + "rule": [ + {"re": "\ndisable_functions\\s?=\\s?(.+)", "check": {"type": "string", "value": [ + 'passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv']}}] + } + if not self.check_san_baseline(php_data): + ret.append(php_data) + + php_version_dir = { + 'id': 41, + "type": "dir", + "harm": "高", + "level": "3", + "repaired": "0", + "name": "PHP 5.2 版本过旧", + "file": '/www/server/php/52', + "Suggestions": "加固建议:不再使用php5.2 ", + "repair": "PHP 5.2 已经被淘汰建议升级更高的版本", + "rule": [] + } + if not self.check_san_baseline(php_version_dir): + ret.append(php_version_dir) + return ret + + # Redis 配置按 + def redis_security(self): + ret = [] + # 查看redis 是否监听的是0.0.0.0 返回True 代表高危 + redis_server_ip = { + 'id': 42, + "type": "file", + "harm": "高", + "level": "3", + "repaired": "0", + "check_file":"/www/server/redis", + "name": "Redis 监听的地址为0.0.0.0", + "file": '/www/server/redis/redis.conf', + "Suggestions": "加固建议, 在%s 中的监听IP设置为127.0.0.1 例如" % ('/www/server/redis/redis.conf'), + "repair": "bind 127.0.0.1", + "rule": [ + {"re": "\nbind\\s*(.+)", "check": {"type": "string", "value": ['0.0.0.0']}}] + } + if self.check_san_baseline(redis_server_ip): + ret.append(redis_server_ip) + + # 查看redis是否设置密码 + redis_server_not_pass = { + 'id': 43, + "type": "password", + "harm": "高", + "level": "3", + "check_file": "/www/server/redis", + "repaired": "0", + "name": "Redis 查看是否设置密码", + "file": '/www/server/redis/redis.conf', + "Suggestions": "加固建议, 在%s 中的为未设置密码 例如" % ('/www/server/redis/redis.conf'), + "repair": "requirepass requirepassQWERQQQQQQQ", + "rule": [ + {"re": "\nrequirepass\\s*(.+)", "check": {"type": "string", "value": []}}] + } + if not self.check_san_baseline(redis_server_not_pass): + ret.append(redis_server_not_pass) + + # 查看redis 是否是弱密码 + redis_server_pass = { + 'id': 44, + "type": "password", + "harm": "高", + "level": "3", + "repaired": "0", + "check_file": "/www/server/redis", + "name": "Redis 存在弱密码", + "file": '/www/server/redis/redis.conf', + "Suggestions": "加固建议, 在%s 中requirepass 设置为强密码" % ('/www/server/redis/redis.conf'), + "repair": "requirepass requirepassQWERQQQQQQQ", + "rule": [ + {"re": "\nrequirepass\\s*(.+)", "check": {"type": "string", "value": ['123456', 'admin', 'damin888']}}] + } + if not self.check_san_baseline(redis_server_pass): + ret.append(redis_server_pass) + # 查看版本是否是低于最新版本 + if os.path.exists('/www/server/redis/version.pl'): + re2t = public.ReadFile('/www/server/redis/version.pl') + if re2t != '5.0.3': + ret2 = { + 'id': 45, + "type": "password", + "harm": "高", + "check_file": "/www/server/redis", + "level": "3", + "repaired": "0", + "name": "Redis 版本低于最新版本", + "file": '/www/server/redis/redis.conf', + "Suggestions": "加固建议,升级到最新版的redis", + "repair": "最新版为5.0.3" + } + ret.append(ret2) + return ret + + # memcached 配置安全 + def memcache_security(self): + ret = [] + memcache_bind = { + 'id': 46, + "type": "file", + "harm": "高", + "level": "3", + "repaired": "0", + "name": "Memcache 监听IP为0.0.0.0", + "check_file": "/usr/local/memcached", + "file": '/etc/init.d/memcached', + "Suggestions": "加固建议, 在%s 中的监听IP设置为127.0.0.1 例如" % ('/etc/init.d/memcached'), + "repair": "IP=127.0.0.1", + "rule": [ + {"re": "\nIP\\s?=\\s?(.+)", "check": {"type": "string", "value": ['0.0.0.0']}}] + } + if self.check_san_baseline(self.__repair['46']): + ret.append(self.__repair['46']) + return ret + + # 查看是否是弱密码 + def get_root_pass(self): + # mysql 弱密码 + if not os.path.exists('/www/server/mysql'): return True + ret = public.M('config').field('mysql_root').select()[0]['mysql_root'] + if ret == '123456' or ret == 'admin': + return False + if len(ret) <= 6: + return False + return True + + # 查看mysql 是否有对外连接用户 + def chekc_mysql_user(self): + if not os.path.exists('/www/server/mysql'):return True + ret = public.M('config').field('mysql_root').select()[0]['mysql_root'] + sql = ''' mysql -uroot -p''' + ret + ''' -e "select User,Host from mysql.user where host='%'" ''' + resutl = public.ExecShell(sql) + if resutl[0] == '': + return True + else: + return False + + # mysql 配置安全 + def mysql_security(self): + result = [] + if not self.get_root_pass(): + ret = { + 'id': 47, + "type": "password", + "harm": "高", + "repaired": "0", + "level": "3", + "name": "Mysql root密码为弱密码", + "file": '/etc/init.d/memcached', + "Suggestions": "加固建议: 使用强密码", + "repair": "例如:adM1#@$544..", + } + result.append(ret) + + if public.M('firewall').where('port=?', ('3306',)).count(): + ret = { + 'id': 48, + "type": "password", + "harm": "高", + "repaired": "0", + "level": "3", + "name": "3306 端口对外开放", + "file": '/etc/init.d/memcached', + "Suggestions": "加固建议: 建议3306不对外开放,如果是特殊需求可以忽略这次记录", + "repair": "关闭3306对外访问", + } + result.append(ret) + + if not self.chekc_mysql_user(): + e = '''select User,Host from mysql.user where host='%' ''' + ret = { + 'id': 49, + "type": "password", + "harm": "高", + "repaired": "0", + "level": "3", + "name": "Mysql 存在外部连接用户", + "file": '/etc/my.local', + "Suggestions": "加固建议: 进入数据库查看mysql用户表", + "repair": e, + } + result.append(ret) + return result + + # 系统用户 安全 + def user_security(self): + result = [] + if not self.check_san_baseline(self.__repair['50']): + result.append(self.__repair['50']) + if not self.check_san_baseline(self.__repair['51']): + result.append(self.__repair['51']) + if not self.check_san_baseline(self.__repair['52']): + result.append(self.__repair['52']) + + # 存在非root 的管理员用户(危险) + get_root_0 = { + 'id': 53, + "type": "shell", + "harm": "紧急", + "repaired": "0", + "level": "5", + "name": "存在非root 的管理员用户(危险)", + "ps": "除root以为的其他的UID为0的用户的应该删除。或者为其分配新的UID", + "cmd": '''cat /etc/passwd | awk -F: '($3 == 0) { print $1 }'|grep -v '^root$' ''', + "find": {"re": r"\w+"} + } + if not self.check_san_baseline(get_root_0): + result.append(get_root_0) + if not self.check_san_baseline(self.__repair['54']): + result.append(self.__repair['54']) + if not self.check_san_baseline(self.__repair['55']): + result.append(self.__repair['55']) + + # 查看用户是否空密码的用户 + if len(self.user_not_password()) >= 1: + user_len = { + 'id': 56, + "type": "file", + "harm": "中", + "repaired": "0", + "level": "2", + "name": "系统存在空密码的用户", + "file": "/etc/login.defs ", + "Suggestions": "加固建议 为如下%s这些用户添加密码" % self.user_not_password(), + "repair": "(如果用户不用可以删除)", + } + result.append(user_len) + + return result + + # 查看用户是否有空密码的用户 + def user_not_password(self): + ret = public.ReadFile('/etc/passwd') + ret = ret.split('\n') + base_user = [] + not_pass_user = [] + for i in ret: + i = i.split(':') + if i[-1] == '/sbin/nologin': + continue + if i[0] == '': continue + base_user.append(i[0]) + check_file_resutl = public.ReadFile('/etc/shadow') + check_file_resutl = check_file_resutl.split('\n') + for i in check_file_resutl: + if not i: continue + i = i.split(':') + # print(i[0],base_user) + if i[0] in base_user: + if i[1] == '!!': + not_pass_user.append(i[0]) + return not_pass_user + + # 计划任务 安全 + def tasks_security(self): + ret = [] + if not os.path.exists(public.get_cron_path()):return ret + f = open(public.get_cron_path(), 'r') + for i in f.readlines(): + + if not i: continue; + i2 = i + i = i.strip().split() + if not i: continue + if i == None: continue + if i[5]: + if '/www/server/' not in i[5]: + if '/root/.acme.sh' not in i[5]: + if 'wget' in i or 'curl' in i or 'bash' or 'http://' in i or 'https://' in i: + task ={ + 'name': "异常计划任务", + "harm": "高", + "repaired": "0", + "level": 3, + "repair": "请排查是否是异常下载", + "Suggestions":"请排查是否是异常下载", + 'list': i2 + } + ret.append(task) + return ret + + # system 关键目录权限 + def system_dir_security(self): + # 关键性文件权限 + result = [] + user_config_chmoe = [ + { + 'id': 57, + "type": "chmod", + "file": "/etc/passwd", + "chmod": [644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 58, + "type": "chmod", + "file": "/etc/shadow", + "chmod": [400], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 59, + "type": "chmod", + "file": "/etc/group", + "chmod": [644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 60, + "type": "chmod", + "file": "/etc/gshadow", + "chmod": [400], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 61, + "type": "chmod", + "file": "/etc/hosts.allow", + "chmod": [644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 62, + "type": "chmod", + "file": "/etc/hosts.deny", + "chmod": [644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 63, + "type": "chmod", + "file": "/www", + "chmod": [755], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 64, + "type": "chmod", + "file": "/www/server", + "chmod": [755], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 65, + "type": "chmod", + "file": "/www/wwwroot", + "chmod": [755], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 66, + "type": "chmod", + "file": "/etc/rc.d", + "chmod": [755], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 67, + "type": "chmod", + "file": "/etc/rc.local", + "chmod": [644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 68, + "type": "chmod", + "file": "/etc/rc.d/rc.local", + "chmod": [644], + "user": ['root'], + 'group': ['root'] + }, { + 'id': 69, + "type": "chmod", + "file": "/var/spool/cron/root", + "chmod": [600], + "user": ['root'], + 'group': ['root'] + } + ] + for i in user_config_chmoe: + if not self.check_san_baseline(i): + ret1 = { + 'id':i['id'], + "harm": "高", + "repaired": "1", + "type": "file", + "name": "系统关键性文件权限错误%s" % i['file'], + "Suggestions": "加固建议 : %s 权限改为%s 所属用户为%s" % (i['file'], i['chmod'], i['user']), + "repair": "加固建议 : %s 权限改为%s 所属用户为%s" % (i['file'], i['chmod'], i['user']), + } + result.append(ret1) + return result + + # 查看站点是否开启SSL + # 取SSL状态 + def GetSSL(self, siteName): + path = '/etc/letsencrypt/live/' + siteName; + type = 0 + if os.path.exists(path + '/README'): type = 1; + if os.path.exists(path + '/partnerOrderId'): type = 2; + csrpath = path + "/fullchain.pem"; # 生成证书路径 + keypath = path + "/privkey.pem"; # 密钥文件路径 + key = public.readFile(keypath); + csr = public.readFile(csrpath); + file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + siteName + '.conf' + conf = public.readFile(file); + if not conf: return False + keyText = 'SSLCertificateFile' + if public.get_webserver() == 'nginx': keyText = 'ssl_certificate'; + status = True + if (conf.find(keyText) == -1): + status = False + type = -1 + return status + + # 取SSL的 SSL 协议 + def get_ssl_tls(self, siteName): + tls = [] + if os.path.exists('/www/server/panel/vhost/nginx/%s.conf' % siteName): + ret = public.ReadFile('/www/server/panel/vhost/nginx/%s.conf' % siteName) + valuse = re.findall(r'ssl_protocols\s+(.+)', ret) + print(valuse) + if not valuse: return tls + if not valuse[0]: return tls + if 'TLSv1' in valuse[0]: + tls.append('TLSv1') + if 'TLSv1.1' in valuse[0]: + tls.append('LSv1.1') + return tls + + # 是否使用宝塔防火墙 + def get_btwaf(self): + if os.path.exists('/www/server/btwaf'): + return True + else: + return False + + def site_security(self): + # 是否开启防御跨站的 + resutl = {} + site_secr = [] + site_lists = public.M('sites').field('name,path').select() + for i in site_lists: + + path = i['path'] + '/.user.ini' + ssl = self.GetSSL(i['name']) + tls = [] + if ssl: + tls = self.get_ssl_tls(i['name']) + if not os.path.exists(path): + site = { + "user_ini": False, + "level": 1, + "name": '%s该站点未启用SSL' % i['name'], + "ssl": ssl, + "tls": tls, + "harm": "警告", + } + if not ssl: + site['Suggestions'] = '加固建议使用https为访问方式' + site['repair'] = 'https 强制模式' + site['ps'] = '%s该站点未启用SSL' % i['name'] + else: + if tls: + site['Suggestions'] = '加固建议: 建议使用TLS1.2及以上的安全协议' + site['repair'] = 'TLS1.2 或者TLS1.3' + site['name'] = '%s该站点启用了不安全的SSL协议LSv1 或者LSv1.1' % i['name'] + site['ps'] = '%s该站点启用了不安全的SSL协议LSv1 或者LSv1.1' % i['name'] + site_secr.append(site) + else: + site = { + "user_ini": True, + "level": 1, + "name": '%s该站点未启用SSL' % i['name'], + "ssl": ssl, + "tls": tls, + "harm": "警告", + } + if not ssl: + site['Suggestions'] = '加固建议使用https为访问方式' + site['repair'] = 'https 强制模式' + site['ps'] = '%s该站点未启用SSL' % i['name'] + else: + if tls: + site['Suggestions'] = '加固建议: 建议使用TLS1.2及以上的安全协议' + site['repair'] = 'TLS1.2 或者TLS1.3' + site['name'] = '%s该站点启用了不安全的SSL协议LSv1 或者LSv1.1' % i['name'] + site['ps'] = '%s该站点启用了不安全的SSL协议LSv1 或者LSv1.1' % i['name'] + site_secr.append(site) + resutl['site_list'] = site_secr + resutl['btwaf'] = self.get_btwaf() + return resutl + + # 主判断函数 + def check_san_baseline(self, base_json): + if base_json['type'] == 'file': + if 'check_file' in base_json: + if not os.path.exists(base_json['check_file']): + return False + else: + if os.path.exists(base_json['file']): + ret = public.ReadFile(base_json['file']) + for i in base_json['rule']: + valuse = re.findall(i['re'], ret) + print(valuse) + if i['check']['type'] == 'number': + if not valuse: return False + if not valuse[0]: return False + valuse = int(valuse[0]) + + if valuse > i['check']['min'] and valuse < i['check']['max']: + return True + else: + return False + elif i['check']['type'] == 'string': + + if not valuse: return False + if not valuse[0]: return False + valuse = valuse[0] + print(valuse) + if valuse in i['check']['value']: + return True + else: + return False + return True + + elif base_json['type'] == 'diff': + if os.path.exists(base_json['file']): + ret = public.ReadFile(base_json['file']) + for i in base_json['rule']: + valuse = re.findall(i['re'], ret) + if not valuse: return False + if not valuse[0]: return False + if i['check']['type'] == 'string': + if valuse[0] in i['check']['value']: + return True + else: + return False + else: + return True + + elif base_json['type'] == 'password': + if os.path.exists(base_json['file']): + ret = public.ReadFile(base_json['file']) + for i in base_json['rule']: + valuse = re.findall(i['re'], ret) + print(valuse) + if not valuse: return False + if not valuse[0]: return False + if not i['check']['value']: return True + if i['check']['value']: + if valuse[0] in i['check']['value']: + return False + else: + return True + else: + return True + + elif base_json['type'] == 'dir': + if os.path.exists(base_json['file']): + return False + else: + return True + + + elif base_json['type'] == 'shell': + ret = public.ExecShell(base_json['cmd']) + if not ret: return True + if not ret[0]: return True + if re.search(base_json['find']['re'], ret[0]): + return False + else: + return True + + elif base_json['type'] == 'chmod': + #@print(base_json) + if os.path.exists(base_json['file']): + ret = self.GetFileAccess(base_json['file']) + print(base_json['chmod']) + if ret['chown'] in base_json['user'] and int(ret['chmod']) in base_json['chmod'] and ret['group'] in \ + base_json['group']: + return True + else: + return False + else: + return True + + # 获取文件/目录 权限信息 + def GetFileAccess(self, filename): + if sys.version_info[0] == 2: filename = filename.encode('utf-8'); + data = {} + try: + import pwd + stat = os.stat(filename) + data['chmod'] = str(oct(stat.st_mode)[-3:]) + data['chown'] = pwd.getpwuid(stat.st_uid).pw_name + data['group'] = pwd.getpwuid(stat.st_gid).pw_name + except: + data['chmod'] = 755 + data['chown'] = 'www' + data['group'] = 'www' + return data + + ####################################网站连通性############################## + + # 网站连通性 + def site_curl_security(self): + result = [] + site_list = public.M('sites').field('name').select() + if len(site_list) >= 1: + for i in site_list: + site = i['name'] + print(site) + try: + ret = requests.get('http://127.0.0.1', timeout=3, headers={"host": site}, verify=False) + if ret.status_code != 200: + ret_status = { + "type": "site", + "repaired": "0", + "name": "%s站点通过本机访问失败" % i['nane'], + "harm": "警告", + 'level':"1", + "file": "%s站点通过本机访问失败" % i['name'], + "Suggestions": "加固建议, 检查是否是绑定了当前服务器的IP", + "repair": "检查是否是绑定了当前服务器的IP" + } + result.append(ret_status) + except: + continue + return result + + ################################## Nginx/APACHE 安全################################ + # Nginx/Apache 配置安全 + def Nginx_Apache_security(self): + ret = [] + Nginx_Get_version = { + 'id': 70, + "type": "file", + "name": "Nginx 版本泄露", + "harm": "低", + 'level': "1", + "repaired": "0", + "file": '/www/server/nginx/conf/nginx.conf', + "Suggestions": "加固建议, 在%s expose_php的值修改为Off中修改" % ('/www/server/nginx/conf/nginx.conf'), + "repair": "expose_php = Off", + "rule": [ + {"re": r"server_tokens\s*(.+)", "check": {"type": "string", "value": ['off;']}}] + } + if not self.check_san_baseline(Nginx_Get_version): + ret.append(Nginx_Get_version) + if os.path.exists('/www/server/nginx/version.pl'): + ret2 = public.ReadFile('/www/server/nginx/version.pl') + if ret2 == '1.8': + Nginx_Get_version = { + 'id': 71, + "type": "file", + 'level': "1", + "repaired": "0", + "name": "Nginx 版本过低", + "harm": "低", + "file": '/www/server/nginx/conf/nginx.conf', + "Suggestions": "加固建议, 升级至最新版的Nginx 软件", + "repair": "例如:Nignx1.17 或者Nginx1.16", + } + ret.append(Nginx_Get_version) + + return ret + + #################################### system 关键文件版本 ###################### + # system 关键文件版本 + def system_version_security(self): + ret = [] + return ret + + # 查询日志(查看进度) + def get_api_log(self, get): + + if not os.path.exists(self.logPath): public.returnMsg(False, "无日志") + ret = json.loads(public.readFile(self.logPath)) + if int(len(ret)) == 0: + return public.returnMsg(False, "无日志") + return public.returnMsg(True, ret) + # 写输出日志 + + def WriteLogs(self, logMsg): + fp = open(self.logPath, 'w+') + fp.write(logMsg) + fp.close() + + # 查询日志(查看进度) + def get_resut(self, get): + time.sleep(0.5) + if not os.path.exists(self.config): public.returnMsg(False, "无日志") + ret = json.loads(public.readFile(self.config)) + if int(len(ret)) == 0: + return public.returnMsg(False, "无日志") + return public.returnMsg(True, ret) + # 写输出日志 + + def Write_result(self, logMsg): + fp = open(self.config, 'w+') + fp.write(logMsg) + fp.close() + + def Write(self, name, count): + Speed = {} + Speed['name'] = '正在进行检测%s' % name + Speed['total'] = 0 + Speed['Current_file'] = None # 当前发送的文件 + Speed['progress'] = "%.2f" % (float(count) / float(13) * 100) + Speed['ok'] = False + self._Speed = Speed + self.WriteLogs(json.dumps(Speed)) + + ######################################################################### + # 统计入口 + def San_Entrance(self): + if os.path.exists(self.logPath): os.remove(self.logPath) + SSH = self.ssh_security() + self.Write(name='ssh安全监测', count=1) + + time.sleep(1) + PANEL = self.panel_security() + self.Write(name='面板安全监测', count=2) + time.sleep(1) + PHP = self.php_security() + self.Write(name='PHP安全监测', count=3) + time.sleep(1) + NINGX = self.Nginx_Apache_security() + self.Write(name='Nginx/Apache安全监测', count=4) + time.sleep(1) + redis = self.redis_security() + self.Write(name='redis安全监测', count=5) + time.sleep(1) + memcache = self.memcache_security() + self.Write(name='memcache安全监测', count=6) + + mysql = self.mysql_security() + self.Write(name='Mysql安全监测', count=7) + time.sleep(1) + system_user = self.user_security() + self.Write(name='系统用户安全监测', count=8) + time.sleep(1) + task = self.tasks_security() + self.Write(name='系统计划任务安全监测', count=9) + time.sleep(1) + site_curl = self.site_curl_security() + self.Write(name='网站连接监测', count=10) + time.sleep(1) + system_dir = self.system_dir_security() + self.Write(name='系统关键目录安全监测', count=11) + time.sleep(1) + site_sec = self.site_security() + self.Write(name='网站安全监测', count=12) + time.sleep(1) + system_file = self.system_version_security() + self.Write(name='系统关键性文件监控', count=13) + + if not self._Speed == None: + self._Speed['ok'] = True + self._Speed['name'] = '所有扫描完毕' + self.WriteLogs(json.dumps(self._Speed)) + + aa = { + "SSH": SSH, + "PANEL": PANEL, + "PHP": PHP, + "NINGX/APCHE": NINGX, + "redis": redis, + "memcache": memcache, + "mysql": mysql, + "system_user": system_user, + "task": task, + "site_curl": site_curl, + "system_dir": system_dir, + "site_sec": site_sec, + "system_file": system_file, + } + + self.Write_result(json.dumps(aa)) + return aa + + def start(self, get): + os.system(public.get_python_bin() + ' /www/server/panel/class/san_baseline.py &') + return public.returnMsg(True, '1') + + # 取爆破 + def get_ssh_errorlogin(self, get): + import datetime + path = '/var/log/secure' + if not os.path.exists(path): public.writeFile(path, ''); + fp = open(path, 'r'); + l = fp.readline(); + data = {}; + data['intrusion'] = []; + # data['intrusion_total'] = 0; + + data['defense'] = []; + data['defense_total'] = 0; + + data['success'] = []; + data['success_total'] = 0; + day_count = 0 + data['intrusion_total'] = day_count + limit = 10000; + flag_limit = 1 + while l and flag_limit <= 10000: + if l.find('Failed password for root') != -1: + flag_limit += 1 + if len(data['intrusion']) > limit: del (data['intrusion'][0]); + + months = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'} + time_str11 = re.findall(r'\w+\s+\d+\s+.\d+:\d+:\d+', l) + if time_str11[0]: + time_str = re.findall(r'\w+\s+\d+', time_str11[0]) + month = int(months[time_str[0].split()[0]]) + day = int(time_str[0].split()[1]) + cur_month = datetime.datetime.now().month + cur_day = datetime.datetime.now().day + if month != cur_month: + continue + else: + if month == cur_month and day == cur_day: + day_count+=1 + else: + continue + + #data['intrusion'].append(l); + #data['intrusion_total'] += 1; + elif l.find('Accepted') != -1: + if len(data['success']) > limit: del (data['success'][0]); + data['success'].append(l); + # data['success_total'] += 1; + l = fp.readline(); + data['intrusion_total'] = day_count + months = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'} + + success = []; + for g in data['success']: + tmp = {} + tmp1 = g.split(); + tmp['date'] = months[tmp1[0]] + '/' + tmp1[1] + ' ' + tmp1[2]; + tmp['user'] = tmp1[8]; + tmp['address'] = tmp1[10]; + success.append(tmp); + data['success'] = success; + + return data; + + + + # 修复的主函数 + def repair_san_baseline(self, base_json): + if base_json['type'] == 'file': + if os.path.exists(base_json['file']): + ret = public.ReadFile(base_json['file']) + for i in base_json['repair_loophole']: + valuse = re.search(i['re'], ret) + if valuse: + data2=re.sub(i['re'],i['check'],ret) + public.WriteFile(base_json['file'],data2) + return True + else: + return False + if base_json['type'] == 'chmod': + if os.path.exists(base_json['file']): + os.system('chown %s:%s %s'%(base_json['user'],base_json['group'],base_json['file'])) + os.system('chmod %s %s'%(base_json['chmod'],base_json['file'])) + return True + + # 修复 + def repair(self,get): + id=get.id + if id in self.__repair: + return self.repair_san_baseline(self.__repair[id]) + else: + return False + + # 修复全部 + def repair_all(self,get): + for i in self.__repair: + if self.__repair[i]['repaired']=='1': + self.repair_san_baseline(self.__repair[i]) + return True + +if __name__ == '__main__': + my_api = san_baseline() + r_data = my_api.San_Entrance() diff --git a/class_v2/site_dir_auth_v2.py b/class_v2/site_dir_auth_v2.py new file mode 100644 index 00000000..f5fb6d79 --- /dev/null +++ b/class_v2/site_dir_auth_v2.py @@ -0,0 +1,420 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: zhwen +#------------------------------------------------------------------- + +#------------------------------ +# 站点目录密码保护 +#------------------------------ +import public,re,os,json,shutil +from public.validate import Param + +class SiteDirAuth: + # 取目录加密状态 + def __init__(self): + self.setup_path = public.GetConfigValue('setup_path') + self.conf_file = self.setup_path + "/panel/data/site_dir_auth.json" + # 读取配置 + def _read_conf(self): + conf = public.readFile(self.conf_file) + if not conf: + conf = {} + public.writeFile(self.conf_file,json.dumps(conf)) + return conf + try: + conf = json.loads(conf) + if not isinstance(conf,dict): + conf = {} + public.writeFile(self.conf_file, json.dumps(conf)) + except: + conf = {} + public.writeFile(self.conf_file, json.dumps(conf)) + return conf + + def _write_conf(self,conf,site_name): + c = self._read_conf() + if not c or site_name not in c: + c[site_name] = [conf] + else: + if site_name in c: + c[site_name].append(conf) + public.writeFile(self.conf_file,json.dumps(c)) + + def _check_site_authorization(self,site_name): + webserver=public.get_webserver() + conf_file = "{setup_path}/panel/vhost/{webserver}/{site_name}.conf".format( + setup_path=self.setup_path, site_name=site_name,webserver=webserver) + if "Authorization" in public.readFile(conf_file): + return True + + + # 设置目录加密 + def set_dir_auth(self,get): + ''' + get.name auth_name + get.site_dir auth_dir + get.username username + get.password password + get.id site id + :param get: + :return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_dir').String(), + Param('name').String(), + Param('username').String(), + Param('password').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + # if len(get.username) < 3 or len(get.password) < 3: + # return public.return_msg_gettext(False, 'Username or password cannot be less than 3 characters') + # name = get.name + param = self.__check_param(get) + if param['status']==-1: + return param + param = param['message'] + password = param['password'] + username = param['username'] + name = param['name'] + site_dir = get.site_dir + if public.get_webserver() == "openlitespeed": + return public.return_message(-1,0,"OpenLiteSpeed is currently not supported") + # if not hasattr(get,"password") or not get.password or not hasattr(get,"username") or not get.username: + # return public.return_msg_gettext(False, 'Please enter an account or password') + if not get.site_dir: + return public.return_message(-1,0, 'Please enter the directory to be protected') + if not get.name: + return public.return_message(-1,0, 'Please enter the Name') + passwd = public.hasPwd(password) + site_info = self.get_site_info(get.id) + site_name = site_info["site_name"] + if self._check_site_authorization(site_name): + return public.return_message(-1,0, 'Site password protection has been set, please cancel and then set. Site directory --> Password access') + if self._check_dir_auth(site_name, name,site_dir): + return public.return_message(-1,0, 'Directory has been protected') + auth = "{user}:{passwd}".format(user=username,passwd=passwd) + auth_file = '{setup_path}/pass/{site_name}'.format(setup_path=self.setup_path,site_name=site_name) + if not os.path.exists(auth_file): + os.makedirs(auth_file) + auth_file = auth_file+"/{}.pass".format(name) + public.writeFile(auth_file,auth) + # 配置独立认证文件 + self.set_dir_auth_file(site_info["site_path"],site_name,name,username,site_dir,auth_file) + # 配置站点主文件 + result = self.set_conf(site_name,"create") + if result: + return public.return_message(0,0,result) + # 检查配置 + webserver = public.get_webserver() + result=self.check_site_conf(webserver,site_name,name) + if result: + return public.return_message(0,0,result) + # 写配置 + conf = {"name":name,"site_dir":get.site_dir,"auth_file":auth_file} + self._write_conf(conf,site_name) + public.serviceReload() + return public.return_message(0,0,"Successfully created") + + # 检查配置是否存在 + def _check_dir_auth(self, site_name, name,site_dir): + conf = self._read_conf() + if not conf: + return False + if site_name in conf: + for i in conf[site_name]: + if name in i.values() or site_dir == i["site_dir"]: + return True + + # 获取当前站点php版本 + def get_site_php_version(self,siteName): + try: + conf = public.readFile(self.setup_path + '/panel/vhost/'+public.get_webserver()+'/'+siteName+'.conf'); + if public.get_webserver() == 'nginx': + rep = r"enable-php-(\w{2,5})\.conf" + tmp = re.search(rep,conf) + if not tmp: + rep = r"enable-php-(\d+-wpfastcgi).conf" + re.search(rep, conf) + else: + rep = r"php-cgi-(\w{2,5})\.sock" + tmp = re.search(rep,conf).groups() + if tmp: + return tmp[0] + else: + return "" + except: + return public.return_msg_gettext(False, 'Apache2.2 does NOT support MultiPHP!') + + # 获取站点名 + def get_site_info(self,id): + site_info = public.M('sites').where('id=?', (id,)).field('name,path').find() + return {"site_name":site_info["name"],"site_path":site_info["path"]} + + def change_dir_auth_file_nginx_phpver(self,site_name,phpv,auth_name): + file_path = "{setup_path}/panel/vhost/nginx/dir_auth/{site_name}/{auth_name}.conf".format( + setup_path=self.setup_path,site_name=site_name,auth_name=auth_name) + conf = public.readFile(file_path) + if not conf: + return False + + if phpv == 'other': + php_conf = "include /www/server/panel/vhost/other_php/{}/enable-php-other.conf;".format(site_name) + else: + php_conf = 'include enable-php-{}.conf;'.format(phpv) + + rep = r"include\s+(enable-php-\w+|/www/server/panel/vhost/other_php/{}/enable-php-other)\.conf;".format(site_name) + conf = re.sub(rep,php_conf,conf) + + public.writeFile(file_path,conf) + + # 设置独立认证文件 + def set_dir_auth_file(self,site_path,site_name,name,username,site_dir,auth_file): + php_ver = self.get_site_php_version(site_name) + php_conf = "" + if php_ver: + if php_ver == 'other': + php_conf = "include /www/server/panel/vhost/other_php/{}/enable-php-{}.conf;".format(site_name,php_ver) + else: + php_conf = "include enable-php-{}.conf;".format(php_ver) + + for i in ["nginx","apache"]: + file_path = "{setup_path}/panel/vhost/{webserver}/dir_auth/{site_name}" + if i == "nginx": + # 设置nginx + conf = '''location ~* ^%s* { + #AUTH_START + auth_basic "Authorization"; + auth_basic_user_file %s; + %s + #AUTH_END +}''' % (site_dir,auth_file,php_conf) + else: + # 设置apache + conf = ''' + #AUTH_START + AuthType basic + AuthName "Authorization " + AuthUserFile {auth_file} + Require user {username} + #AUTH_END + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + #Require all granted + DirectoryIndex index.php index.html index.htm default.php default.html default.htm +'''.format(site_path=site_path,site_dir=site_dir,auth_file=auth_file,username=username,site_name=site_name) + conf_file = file_path.format(setup_path=self.setup_path,site_name=site_name,webserver=i) + if not os.path.exists(conf_file): + os.makedirs(conf_file) + conf_file = conf_file + '/{}.conf'.format(name) + public.writeFile(conf_file,conf) + + # 设置apache配置 + def set_conf(self,site_name,act): + for i in ["nginx", "apache"]: + dir_auth_file = "%s/panel/vhost/%s/dir_auth/%s/*.conf" % (self.setup_path,i,site_name,) + file = self.setup_path + "/panel/vhost/{}/".format(i) + site_name + ".conf" + shutil.copyfile(file, '/tmp/{}_file_bk.conf'.format(i)) + + if os.path.exists(file): + conf = public.readFile(file) + if i == "apache": + if act == "create": + rep = "IncludeOptional.*\\/dir_auth\\/.*conf(\n|.)+<\\/VirtualHost>" + rep1 = "" + if not re.search(rep, conf): + conf = conf.replace(rep1, + "\n\t#Directory protection rules, do not manually delete\n\tIncludeOptional {}\n".format( + dir_auth_file)) + else: + rep = "\n*#Directory protection rules, do not manually delete\n+\\s+IncludeOptional[\\s\\w\\/\\.\\*]+" + conf = re.sub(rep, '', conf) + public.writeFile(file, conf) + else: + if act == "create": + rep = "#SSL-END(\n|.)+include.*\\/dir_auth\\/.*conf;" + rep1 = "#SSL-END" + if not re.search(rep,conf): + conf = conf.replace(rep1, rep1 + "\n\t#Directory protection rules, do not manually delete\n\tinclude {};".format(dir_auth_file)) + else: + rep = "\n*#Directory protection rules, do not manually delete\n+\\s+include[\\s\\w\\/\\.\\*]+;" + conf = re.sub(rep, '', conf) + public.writeFile(file, conf) + + # 验证站点配置 + def check_site_conf(self,webserver,site_name,name): + isError = public.checkWebConfig() + auth_file = "{setup_path}/panel/vhost/{webserver}/dir_auth/{site_name}/{name}.conf".format(setup_path=self.setup_path,webserver=webserver,site_name=site_name,name=name) + if (isError != True): + os.remove(auth_file) + # a_conf = self._read_conf() + # for i in range(len(a_conf)-1,-1,-1): + # if site_name == a_conf[i]["sitename"] and a_conf[i]["proxyname"]: + # del a_conf[i] + return public.return_msg_gettext(False, 'ERROR: %s
                                    ' % public.get_msg_gettext('Configuration ERROR') + isError.replace("\n", + '
                                    ') + '
                                    ') + + # 删除密码保护 + def delete_dir_auth(self,get): + ''' + get.id + get.name + :param get: + :return: + ''' + try: + get.validate([ + Param('name').String(), + Param('id').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + name = get.name + site_info = self.get_site_info(get.id) + site_name = site_info["site_name"] + conf = self._read_conf() + if site_name not in conf: + return public.return_message(-1,0,"The website does not exist in the configuration:{}",(site_name,)) + for i in range(len(conf[site_name])): + if name in conf[site_name][i].values(): + print(conf[site_name][i]) + del(conf[site_name][i]) + if not conf[site_name]: + del(conf[site_name]) + break + public.writeFile(self.conf_file,json.dumps(conf)) + for i in ["nginx", "apache"]: + file_path = "{setup_path}/panel/vhost/{webserver}/dir_auth/{site_name}/{name}.conf".format(webserver=i, + setup_path=self.setup_path, + site_name=site_name, + name=name) + os.remove(file_path) + if not conf: + self.set_conf(site_name,"delete") + if not hasattr(get,'multiple'): + public.serviceReload() + return public.return_message(0,0,'Successfully deleted!') + + # 修改目录保护密码 + def modify_dir_auth_pass(self,get): + ''' + get.id + get.name + get.username + get.password + :param get: + :return: + ''' + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('username').String(), + Param('password').String(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # if not hasattr(get,"password") or not get.password or not hasattr(get,"username") or not get.username: + # return public.return_msg_gettext(False, 'Username or password cannot be less than 3 characters') + param = self.__check_param(get) + if param['status']==-1: + return param + param = param['message'] + password = param['password'] + username = param['username'] + name = get.name + site_info = self.get_site_info(get.id) + site_name = site_info["site_name"] + passwd = public.hasPwd(get.password) + auth = "{user}:{passwd}".format(user=get.username,passwd=passwd) + auth_file = '{setup_path}/pass/{site_name}/{name}.pass'.format(setup_path=self.setup_path,site_name=site_name,name=name) + public.writeFile(auth_file,auth) + public.serviceReload() + return public.return_message(0,0,'Setup successfully!') + + # 获取目录保护列表 + def get_dir_auth(self,get): + ''' + get.id + get.sitename + :param get: + :return: + ''' + # 校验参数 + try: + get.validate([ + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + if not hasattr(get, 'siteName'): + site_info = self.get_site_info(get.id) + site_name = site_info["site_name"] + else: + site_name = get.siteName + conf = self._read_conf() + if site_name in conf: + return public.return_message(0,0,{site_name:conf[site_name]}) + return public.return_message(0,0,{}) + + def __check_param(self, get): + values = {} + if hasattr(get, "password"): + if not get.password: + return public.return_message(-1,0, 'Please enter password!') + password = get.password.strip() + if len(password) < 3: + return public.return_message(-1,0, 'Password cannot be less than 3 characters') + if re.search(r'\s', password): + return public.return_message(-1,0, 'Password cannot contain spaces') + values['password'] = password + + if hasattr(get, "username"): + if not get.username: + return public.return_message(-1,0, 'Please enter username!') + username = get.username.strip() + if len(username) < 3: + return public.return_message(-1,0, 'Username cannot be less than 3 characters') + if re.search(r'\s', username): + return public.return_message(-1,0, 'Username cannot contain spaces') + values['username'] = username + + if hasattr(get, "name"): + if not get.name: + return public.return_message(-1,0, 'Please enter a name!') + name = get.name.strip() + if len(name) < 3: + return public.return_message(-1,0, 'Name cannot be less than 3 characters') + if re.search(r'\s', name): + return public.return_message(-1,0, 'Name cannot contain spaces') + if re.search('[\\/\"\'\\!@#$%^&*()+={}\\[\\]\\:\\;\\?><,./\\\\]+', name): + return public.return_message(-1,0, 'Name format must be [ aaa_bbb ]') + values['name'] = name + + return public.return_message(0,0, values) diff --git a/class_v2/ssh_security_v2.py b/class_v2/ssh_security_v2.py new file mode 100644 index 00000000..23a6497e --- /dev/null +++ b/class_v2/ssh_security_v2.py @@ -0,0 +1,1018 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# aaPanel +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. +#------------------------------------------------------------------- +# Author: lkqiang +#------------------------------------------------------------------- +# SSH 安全类 +#------------------------------ +import public,os,re,send_mail,json +from datetime import datetime +from public.validate import Param + + +class ssh_security: + __type_list = ['ed25519','ecdsa','rsa', 'dsa'] + __key_type_file = '{}/data/ssh_key_type.pl'.format(public.get_panel_path()) + __key_files = ['/root/.ssh/id_ed25519','/root/.ssh/id_ecdsa','/root/.ssh/id_rsa','/root/.ssh/id_rsa_bt'] + __type_files = { + "ed25519": "/root/.ssh/id_ed25519", + "ecdsa": "/root/.ssh/id_ecdsa", + "rsa": "/root/.ssh/id_rsa", + "dsa": "/root/.ssh/id_dsa" + } + open_ssh_login = public.get_panel_path() + '/data/open_ssh_login.pl' + + __SSH_CONFIG='/etc/ssh/sshd_config' + __ip_data = None + __ClIENT_IP='/www/server/panel/data/host_login_ip.json' + __pyenv = 'python' + __REPAIR={"1":{"id":1, + "type":"file", + "harm":"High", + "repaired":"1", + "level":"3", + "name":"Make sure SSH MaxAuthTries is set between 3-6", + "file":"/etc/ssh/sshd_config", + "Suggestions":"Remove the MaxAuthTries comment symbol # in /etc/ssh/sshd_config, set the maximum number of failed password attempts 3-6 recommended 4", + "repair":"MaxAuthTries 4", + "rule":[{"re":"\nMaxAuthTries\\s*(\\d+)","check":{"type":"number","max":7,"min":3}}], + "repair_loophole":[{"re":"\n?#?MaxAuthTries\\s*(\\d+)","check":"\nMaxAuthTries 4"}]}, + "2":{"id":2, + "repaired":"1", + "type":"file", + "harm":"High", + "level":"3", + "name":"SSHD Mandatory use of V2 security protocol", + "file":"/etc/ssh/sshd_config", + "Suggestions":"Set parameters in the /etc/ssh/sshd_config file as follows", + "repair":"Protocol 2", + "rule":[{"re":"\nProtocol\\s*(\\d+)", + "check":{"type":"number","max":3,"min":1}}], + "repair_loophole":[{"re":"\n?#?Protocol\\s*(\\d+)","check":"\nProtocol 2"}]}, + "3":{"id":3, + "repaired":"1", + "type":"file", + "harm":"High", + "level":"3", + "name":"Set SSH idle exit time", + "file":"/etc/ssh/sshd_config", + "Suggestions":"Set ClientAliveInterval to 300 to 900 in /etc/ssh/sshd_config, which is 5-15 minutes, and set ClientAliveCountMax to 0-3", + "repair":"ClientAliveInterval 600 ClientAliveCountMax 2", + "rule":[{"re":"\nClientAliveInterval\\s*(\\d+)","check":{"type":"number","max":900,"min":300}}], + "repair_loophole":[{"re":"\n?#?ClientAliveInterval\\s*(\\d+)","check":"\nClientAliveInterval 600"}]}, + "4":{"id":4, + "repaired":"1", + "type":"file", + "harm":"High", + "level":"3", + "name":"Make sure SSH LogLevel is set to INFO", + "file":"/etc/ssh/sshd_config", + "Suggestions":"Set parameters in the /etc/ssh/sshd_config file as follows (uncomment)", + "repair":"LogLevel INFO", + "rule":[{"re":"\nLogLevel\\s*(\\w+)","check":{"type":"string","value":["INFO"]}}], + "repair_loophole":[{"re":"\n?#?LogLevel\\s*(\\w+)","check":"\nLogLevel INFO"}]}, + "5":{"id":5, + "repaired":"1", + "type":"file", + "harm":"High", + "level":"3", + "name":"Disable SSH users with empty passwords from logging in", + "file":"/etc/ssh/sshd_config", + "Suggestions":"Configure PermitEmptyPasswords to no in /etc/ssh/sshd_config", + "repair":"PermitEmptyPasswords no", + "rule":[{"re":"\nPermitEmptyPasswords\\s*(\\w+)","check":{"type":"string","value":["no"]}}], + "repair_loophole":[{"re":"\n?#?PermitEmptyPasswords\\s*(\\w+)","check":"\nPermitEmptyPasswords no"}]}, + "6":{"id":6, + "repaired":"1", + "type":"file", + "name":"SSH uses the default port 22", + "harm":"High", + "level":"3", + "file":"/etc/ssh/sshd_config", + "Suggestions":"Set Port to 6000 to 65535 in / etc / ssh / sshd_config", + "repair":"Port 60151", + "rule":[{"re":"Port\\s*(\\d+)","check":{"type":"number","max":65535,"min":22}}], + "repair_loophole":[{"re":"\n?#?Port\\s*(\\d+)","check":"\nPort 65531"}]}} + __root_login_types = {'yes':'yes - keys and passwords','no':'no - no login','without-password':'without-password - only key login','forced-commands-only':'forced-commands-only - can only execute commands'} + + + def __init__(self): + if not public.M('sqlite_master').where('type=? AND name=?', ('table', 'ssh_login_record')).count(): + public.M('').execute('''CREATE TABLE ssh_login_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + addr TEXT, + server_ip TEXT, + user_agent TEXT, + ssh_user TEXT, + login_time INTEGER DEFAULT 0, + close_time INTEGER DEFAULT 0, + video_addr TEXT);''') + public.M('').execute('CREATE INDEX ssh_login_record ON ssh_login_record (addr);') + + if not os.path.exists(self.__ClIENT_IP): + public.WriteFile(self.__ClIENT_IP,json.dumps([])) + self.__mail=send_mail.send_mail() + self.__mail_config=self.__mail.get_settings() + self._check_pyenv() + try: + self.__ip_data = json.loads(public.ReadFile(self.__ClIENT_IP)) + except: + self.__ip_data=[] + + def _check_pyenv(self): + if os.path.exists('/www/server/panel/pyenv'): + self.__pyenv = 'btpython' + + def get_ssh_key_type(self): + ''' + 获取ssh密钥类型 + @author hwliang + :return: + ''' + default_type = 'rsa' + if not os.path.exists(self.__key_type_file): + return default_type + new_type = public.ReadFile(self.__key_type_file) + if new_type in self.__type_list: + return new_type + return default_type + + + def return_python(self): + if os.path.exists('/www/server/panel/pyenv/bin/python'):return '/www/server/panel/pyenv/bin/python' + if os.path.exists('/usr/bin/python'):return '/usr/bin/python' + if os.path.exists('/usr/bin/python3'):return '/usr/bin/python3' + return 'python' + + + def return_profile(self): + if os.path.exists('/root/.bash_profile'): return '/root/.bash_profile' + if os.path.exists('/etc/profile'): return '/etc/profile' + fd = open('/root/.bash_profil', mode="w", encoding="utf-8") + fd.close() + return '/root/.bash_profil' + + def return_bashrc(self): + if os.path.exists('/root/.bashrc'):return '/root/.bashrc' + if os.path.exists('/etc/bashrc'):return '/etc/bashrc' + if os.path.exists('/etc/bash.bashrc'):return '/etc/bash.bashrc' + fd = open('/root/.bashrc', mode="w", encoding="utf-8") + fd.close() + return '/root/.bashrc' + + + def check_files(self): + try: + json.loads(public.ReadFile(self.__ClIENT_IP)) + except: + public.WriteFile(self.__ClIENT_IP, json.dumps([])) + + def get_ssh_port(self): + conf = public.readFile(self.__SSH_CONFIG) + if not conf: conf = '' + rep = r"#*Port\s+([0-9]+)\s*\n" + tmp1 = re.search(rep,conf) + port = '22' + if tmp1: + port = tmp1.groups(0)[0] + return port + + # 主判断函数 + def check_san_baseline(self, base_json): + if base_json['type'] == 'file': + if 'check_file' in base_json: + if not os.path.exists(base_json['check_file']): + return False + else: + if os.path.exists(base_json['file']): + ret = public.ReadFile(base_json['file']) + for i in base_json['rule']: + valuse = re.findall(i['re'], ret) + if i['check']['type'] == 'number': + if not valuse: return False + if not valuse[0]: return False + valuse = int(valuse[0]) + if valuse > i['check']['min'] and valuse < i['check']['max']: + return True + else: + return False + elif i['check']['type'] == 'string': + if not valuse: return False + if not valuse[0]: return False + valuse = valuse[0] + if valuse in i['check']['value']: + return True + else: + return False + return True + + def san_ssh_security(self,get): + data={"num":100,"result":[]} + result = [] + ret = self.check_san_baseline(self.__REPAIR['1']) + if not ret: result.append(self.__REPAIR['1']) + ret = self.check_san_baseline(self.__REPAIR['2']) + if not ret: result.append(self.__REPAIR['2']) + ret = self.check_san_baseline(self.__REPAIR['3']) + if not ret: result.append(self.__REPAIR['3']) + ret = self.check_san_baseline(self.__REPAIR['4']) + if not ret: result.append(self.__REPAIR['4']) + ret = self.check_san_baseline(self.__REPAIR['5']) + if not ret: result.append(self.__REPAIR['5']) + ret = self.check_san_baseline(self.__REPAIR['6']) + if not ret: result.append(self.__REPAIR['6']) + data["result"]=result + if len(result)>=1: + data['num']=data['num']-(len(result)*10) + return data + + ################## SSH 登陆报警设置 #################################### + def send_mail_data(self,title,body,type=None): + try: + login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl" + if not os.path.exists(login_send_type_conf): + login_type = "mail" + else: + login_type = public.readFile(login_send_type_conf).strip() + if not login_type: + login_type = "mail" + object = public.init_msg(login_type.strip()) + if not object: + return False + if login_type=="mail": + data={} + data['title'] = title + data['msg'] = body + object.push_data(data) + elif login_type=="wx_account": + object.send_msg(body) + else: + + msg = public.get_push_info("SSH login warning",['>Send Content:' + body]) + object.push_data(msg) + except: + pass + + #检测非UID为0的账户 + def check_user(self): + ret = [] + cfile = '/etc/passwd' + if os.path.exists(cfile): + f = open(cfile, 'r') + for i in f: + i = i.strip().split(":") + if i[2] == '0' and i[3] == '0': + if i[0] == 'root': continue + ret.append(i[0]) + if ret: + data=''.join(ret) + public.run_thread(self.send_mail_data,args=(public.GetLocalIp()+' There is a backdoor user in the server',public.GetLocalIp()+' There is a backdoor user in the server '+data+' please check/etc/passwd',)) + return True + else: + return False + + #记录root 的登陆日志 + + #返回登陆IP + def return_ip(self,get): + self.check_files() + # return public.returnMsg(True, self.__ip_data) + return public.return_message(0, 0, self.__ip_data) + + #添加IP白名单 + def add_return_ip(self, get): + + # 校验参数 + try: + get.validate([ + Param('ip').Require().String().Ip(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + self.check_files() + if get.ip.strip() in self.__ip_data: + # return public.returnMsg(False, "Already exists") + return public.return_message(-1, 0, "Already exists") + else: + self.__ip_data.append(get.ip.strip()) + public.writeFile(self.__ClIENT_IP, json.dumps(self.__ip_data)) + # return public.returnMsg(True, "Added successfully") + return public.return_message(0, 0, "Added successfully") + + def del_return_ip(self, get): + # 校验参数 + try: + get.validate([ + Param('ip').Require().Ip(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + self.check_files() + if get.ip.strip() in self.__ip_data: + self.__ip_data.remove(get.ip.strip()) + public.writeFile(self.__ClIENT_IP, json.dumps(self.__ip_data)) + # return public.returnMsg(True, "Successfully deleted") + return public.return_message(0, 0, "Successfully deleted") + else: + # return public.returnMsg(False, "IP does not exist") + return public.return_message(-1, 0, "IP does not exist") + + #取登陆的前50个条记录 + def login_last(self): + self.check_files() + data=public.ExecShell('last -n 50') + data=re.findall(r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",data[0]) + if data>=1: + data2=list(set(data)) + for i in data2: + if not i in self.__ip_data: + self.__ip_data.append(i) + public.writeFile(self.__ClIENT_IP, json.dumps(self.__ip_data)) + return self.__ip_data + + #获取ROOT当前登陆的IP + def get_ip(self): + data = public.ExecShell(''' who am i |awk ' {print $5 }' ''') + data = re.findall(r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",data[0]) + return data + + def get_logs(self, get): + + # 分页校验参数 + try: + get.validate([ + Param('p_size').Integer(), + Param('p').Integer(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + import page + page = page.Page() + count = public.M('logs').where('type=?', ('SSH security',)).count() + limit = 10 + info = {} + info['count'] = count + info['row'] = limit + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + info['uri'] = get + info['return_js'] = '' + if hasattr(get, 'tojs'): + info['return_js'] = get.tojs + data = {} + # 获取分页数据 + data['page'] = page.GetPage(info, '1,2,3,4,5,8') + data['data'] = public.M('logs').where('type=?', (u'SSH security',)).order('id desc').limit( + str(page.SHIFT) + ',' + str(page.ROW)).field('log,addtime').select() + # return data + return public.return_message(0, 0, data) + + def get_server_ip(self): + if os.path.exists('/www/server/panel/data/iplist.txt'): + data=public.ReadFile('/www/server/panel/data/iplist.txt') + return data.strip() + else:return '127.0.0.1' + + + #登陆的情况下 + def login(self): + self.check_files() + self.check_user() + self.__ip_data = json.loads(public.ReadFile(self.__ClIENT_IP)) + ip=self.get_ip() + if len(ip[0])==0:return False + try: + import time + mDate = time.strftime('%Y-%m-%d %X', time.localtime()) + if ip[0] in self.__ip_data: + if public.M('logs').where('type=? addtime', ('SSH security',mDate,)).count():return False + public.WriteLog('SSH security', 'The server {} login IP is {}, login user is root'.format(public.GetLocalIp(),ip[0])) + return False + else: + if public.M('logs').where('type=? addtime', ('SSH security', mDate,)).count(): return False + self.send_mail_data('Server {} login alarm'.format(public.GetLocalIp()),'There is a login alarm on the server {}, the login IP is {}, the login user is root'.format(public.GetLocalIp(),ip[0])) + public.WriteLog('SSH security','There is a login alarm on the server {}, the login IP is {}, login user is root'.format(public.GetLocalIp(),ip [0])) + return True + except: + pass + + + + #修复bashrc文件 + def repair_bashrc(self): + data = public.ReadFile(self.return_bashrc()) + if re.search(self.return_python() + ' /www/server/panel/class/ssh_security.py', data): + public.WriteFile(self.return_bashrc(),data.replace(self.return_python()+' /www/server/panel/class/ssh_security.py login','')) + #遗留的错误信息 + datassss = public.ReadFile(self.return_bashrc()) + if re.search(self.return_python(),datassss): + public.WriteFile(self.return_bashrc(),datassss.replace(self.return_python(),'')) + + + #开启监控 + def start_jian(self,get): + self.repair_bashrc() + data = public.ReadFile(self.return_profile()) + if not re.search(self.return_python() + ' /www/server/panel/class/ssh_security.py', data): + cmd = '''shell="%s /www/server/panel/class/ssh_security.py login" + nohup `${shell}` &>/dev/null & + disown $!''' % (self.return_python()) + public.WriteFile(self.return_profile(), data.strip() + '\n' + cmd) + return public.returnMsg(True, 'Open successfully') + return public.returnMsg(False, 'Open failed') + + #关闭监控 + def stop_jian(self,get): + data = public.ReadFile(self.return_profile()) + if re.search(self.return_python()+' /www/server/panel/class/ssh_security.py', data): + cmd='''shell="%s /www/server/panel/class/ssh_security.py login"'''%(self.return_python()) + data=data.replace(cmd, '') + cmd='''nohup `${shell}` &>/dev/null &''' + data=data.replace(cmd, '') + cmd='''disown $!''' + data=data.replace(cmd, '') + public.WriteFile(self.return_profile(),data) + #检查是否还存在遗留 + if re.search(self.return_python()+' /www/server/panel/class/ssh_security.py', data): + public.WriteFile(self.return_profile(),data.replace(self.return_python()+' /www/server/panel/class/ssh_security.py login','')) + #遗留的错误信息 + datassss = public.ReadFile(self.return_profile()) + if re.search(self.return_python(),datassss): + public.WriteFile(self.return_profile(),datassss.replace(self.return_python(),'')) + + return public.returnMsg(True, 'Closed successfully') + else: + return public.returnMsg(True, 'Closed successfully') + + #监控状态 + def get_jian(self,get): + data = public.ReadFile(self.return_profile()) + #if re.search(r'{}\/www\/server\/panel\/class\/ssh_security.py\s+login'.format(r".*python\s+"), data): + if re.search('/www/server/panel/class/ssh_security.py login', data): + return public.returnMsg(True, '1') + else: + return public.returnMsg(False, '1') + + def set_password(self, get): + ''' + 开启密码登陆 + get: 无需传递参数 + ''' + ssh_password = r'\n#?PasswordAuthentication\s\w+' + file = public.readFile(self.__SSH_CONFIG) + if not file: + return public.return_message(-1, 0, 'ERROR: sshd config configuration file does not exist, cannot continue!') + # return public.returnMsg(False,'ERROR: sshd config configuration file does not exist, cannot continue!') + if len(re.findall(ssh_password, file)) == 0: + file_result = file + '\nPasswordAuthentication yes' + else: + file_result = re.sub(ssh_password, '\nPasswordAuthentication yes', file) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + public.WriteLog('SSH management', 'Enable password login') + # return public.returnMsg(True, 'Open successfully') + return public.return_message(0, 0, 'Open successfully') + + def set_sshkey(self, get): + ''' + 设置ssh 的key + 参数 ssh=rsa&type=yes + ''' + # 分页校验参数 + try: + get.validate([ + Param('ssh').Require().String('in', ['yes', 'no']).Xss(), + Param('type').Require().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + # ssh_type = ['yes', 'no'] + ssh = get.ssh + # if not ssh in ssh_type: return public.returnMsg(False, 'ssh option failed') + s_type = get.type + if not s_type in self.__type_list: + # return public.returnMsg(False, 'Wrong encryption method') + return public.return_message(-1, 0, "Wrong encryption method") + + authorized_keys = '/root/.ssh/authorized_keys' + file = ['/root/.ssh/id_{}.pub'.format(s_type), '/root/.ssh/id_{}'.format(s_type)] + for i in file: + if os.path.exists(i): + public.ExecShell(r'sed -i "\~$(cat %s)~d" %s' % (file[0], authorized_keys)) + os.remove(i) + os.system("ssh-keygen -t {s_type} -P '' -f /root/.ssh/id_{s_type} |echo y".format(s_type = s_type)) + if os.path.exists(file[0]): + public.ExecShell('cat %s >> %s && chmod 600 %s' % (file[0], authorized_keys, authorized_keys)) + rec = r'\n#?RSAAuthentication\s\w+' + rec2 = r'\n#?PubkeyAuthentication\s\w+' + file = public.readFile(self.__SSH_CONFIG) + if not file: + # return public.returnMsg(False, 'ERROR: sshd config configuration file does not exist, cannot continue!') + return public.return_message(-1, 0, "ERROR: sshd config configuration file does not exist") + if len(re.findall(rec, file)) == 0: file = file + '\nRSAAuthentication yes' + if len(re.findall(rec2, file)) == 0: file = file + '\nPubkeyAuthentication yes' + file_ssh = re.sub(rec, '\nRSAAuthentication yes', file) + file_result = re.sub(rec2, '\nPubkeyAuthentication yes', file_ssh) + if ssh == 'no': + ssh_password = r'\n#?PasswordAuthentication\s\w+' + if len(re.findall(ssh_password, file_result)) == 0: + file_result = file_result + '\nPasswordAuthentication no' + else: + file_result = re.sub(ssh_password, '\nPasswordAuthentication no', file_result) + self.wirte(self.__SSH_CONFIG, file_result) + public.writeFile(self.__key_type_file, s_type) + self.restart_ssh() + public.WriteLog('SSH management', 'Set up SSH key authentication and successfully generate the key') + # return public.returnMsg(True, 'Open successfully') + return public.return_message(0, 0, 'Open successfully') + else: + public.WriteLog('SSH management', 'Failed to set SSH key authentication') + # return public.returnMsg(False, 'Open failed') + return public.return_message(-1, 0, "Open failed") + + # 取SSH信息 + + def get_msg_push_list(self,get): + """ + @name 获取消息通道配置列表 + @auther: cjxin + @date: 2022-08-16 + @ + """ + cpath = 'data/msg.json' + try: + if 'force' in get or not os.path.exists(cpath): + public.downloadFile('{}/linux/panel/msg/msg.json'.format("https://node.aapanel.com"),cpath) + except : pass + + data = {} + if os.path.exists(cpath): + msgs = json.loads(public.readFile(cpath)) + for x in msgs: + x['setup'] = False + x['info'] = False + key = x['name'] + try: + obj = public.init_msg(x['name']) + if obj: + x['setup'] = True + x['info'] = obj.get_version_info(None) + except : + print(public.get_error_info()) + pass + data[key] = x + # return data + return public.return_message(0, 0, data) + + + #取消告警 + def clear_login_send(self,get): + + # 校验参数 + try: + get.validate([ + Param('type').Require().String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl" + os.remove(login_send_type_conf) + self.stop_jian(get) + # return public.returnMsg(True, 'Successfully cancel the login alarm!') + return public.return_message(0, 0, "Successfully cancel the login alarm") + + #设置告警 + def set_login_send(self,get): + # 校验参数 + try: + get.validate([ + Param('type').Require().String().Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl" + set_type=get.type.strip() + msg_configs = self.get_msg_push_list(get) + if set_type not in msg_configs.keys(): + # return public.returnMsg(False,'This send type is not supported') + return public.return_message(-1, 0, "This send type is not supported") + + from panelMessage import panelMessage + pm = panelMessage() + obj = pm.init_msg_module(set_type) + if not obj: + # return public.returnMsg(False, "The message channel is not installed.") + return public.return_message(-1, 0, "The message channel is not installed") + + public.writeFile(login_send_type_conf, set_type) + self.start_jian(get) + # return public.returnMsg(True, 'Successfully set') + return public.return_message(0, 0, "Successfully set") + + #查看告警 + def get_login_send(self, get): + login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl" + if os.path.exists(login_send_type_conf): + send_type = public.readFile(login_send_type_conf).strip() + else: + send_type ="error" + # return public.returnMsg(True, send_type) + return public.return_message(0, 0, send_type) + + def GetSshInfo(self, get): + # port = public.get_ssh_port() + + pid_file = '/run/sshd.pid' + if os.path.exists(pid_file): + pid = int(public.readFile(pid_file)) + status = public.pid_exists(pid) + else: + import system + panelsys = system.system() + version = panelsys.GetSystemVersion() + if os.path.exists('/usr/bin/apt-get'): + if os.path.exists('/etc/init.d/sshd'): + status = public.ExecShell("service sshd status | grep -P '(dead|stop)'|grep -v grep") + else: + status = public.ExecShell("service ssh status | grep -P '(dead|stop)'|grep -v grep") + else: + if version.find(' 7.') != -1 or version.find(' 8.') != -1 or version.find('Fedora') != -1: + status = public.ExecShell("systemctl status sshd.service | grep 'dead'|grep -v grep") + else: + status = public.ExecShell("/etc/init.d/sshd status | grep -e 'stopped' -e '已停'|grep -v grep") + + # return status; + if len(status[0]) > 3: + status = False + else: + status = True + return status + + + def stop_key(self, get): + ''' + 关闭key + 无需参数传递 + ''' + is_ssh_status=self.GetSshInfo(get) + rec = r'\n\s*#?\s*RSAAuthentication\s+\w+' + rec2 = r'\n\s*#?\s*PubkeyAuthentication\s+\w+' + file = public.readFile(self.__SSH_CONFIG) + if not file: + # return public.returnMsg(False,'错误:sshd_config配置文件不存在,无法继续!') + return public.return_message(-1, 0, "Error: sshd config configuration file does not exist") + file_ssh = re.sub(rec, '\nRSAAuthentication no', file) + file_result = re.sub(rec2, '\nPubkeyAuthentication no', file_ssh) + self.wirte(self.__SSH_CONFIG, file_result) + + if is_ssh_status: + self.set_password(get) + self.restart_ssh() + public.WriteLog('SSH management','Disable SSH key login') + # return public.returnMsg(True, 'Disable successfully') + return public.return_message(0, 0, "Disable successfully") + + + + def get_config(self, get): + ''' + 获取配置文件 + 无参数传递 + ''' + result = {} + file = public.readFile(self.__SSH_CONFIG) + if not file: + # return public.returnMsg(False, 'Error: sshd config does not exist') + return public.return_message(-1, 0, "Error: sshd config does not exist") + + # ======== 以下在2022-10-12重构 ========== + # author : hwliang + # 是否开启RSA公钥认证 + # 默认开启(最新版openssh已经不支持RSA公钥认证) + # yes = 开启 + # no = 关闭 + result['rsa_auth'] = 'yes' + rec = r'^\s*RSAAuthentication\s*(yes|no)' + rsa_find = re.findall(rec, file, re.M|re.I) + if rsa_find and rsa_find[0].lower() == 'no': result['rsa_auth'] = 'no' + + # 获取是否开启公钥认证 + # 默认关闭 + # yes = 开启 + # no = 关闭 + result['pubkey'] = 'no' + if self.get_key(get)['msg']: # 先检查是否存在可用的公钥 + pubkey = r'^\s*PubkeyAuthentication\s*(yes|no)' + pubkey_find = re.findall(pubkey, file, re.M|re.I) + if pubkey_find and pubkey_find[0].lower() == 'yes': result['pubkey'] = 'yes' + + + # 是否开启密码登录 + # 默认开启 + # yes = 开启 + # no = 关闭 + result['password'] = 'yes' + ssh_password = r'^\s*PasswordAuthentication\s*([\w\-]+)' + ssh_password_find = re.findall(ssh_password, file, re.M|re.I) + if ssh_password_find and ssh_password_find[0].lower() == 'no': result['password'] = 'no' + + #是否允许root登录 + # 默认允许 + # yes = 允许 + # no = 不允许 + # without-password = 允许,但不允许使用密码登录 + # forced-commands-only = 允许,但只允许执行命令,不能使用终端 + result['root_is_login'] = 'yes' + result['root_login_type'] = 'yes' + root_is_login=r'^\s*PermitRootLogin\s*([\w\-]+)' + root_is_login_find = re.findall(root_is_login, file, re.M|re.I) + if root_is_login_find and root_is_login_find[0].lower() != 'yes': + result['root_is_login'] = 'no' + result['root_login_type'] = root_is_login_find[0].lower() + result['root_login_types'] = self.__root_login_types + # return result + return public.return_message(0, 0, result) + + + def set_root(self, get): + ''' + 开启密码登陆 + get: 无需传递参数 + ''' + # without-password yes no forced-commands-only + # 分页校验参数 + try: + get.validate([ + Param('p_type').String('in', ['yes', 'no', 'without-password', 'forced-commands-only']).Xss(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + p_type = 'yes' + if 'p_type' in get: p_type = get.p_type + if p_type not in self.__root_login_types.keys(): + # return public.returnMsg(False, 'Parameter passing error!') + return public.return_message(-1, 0, 'Parameter passing error') + ssh_password = r'^\s*#?\s*PermitRootLogin\s*([\w\-]+)' + file = public.readFile(self.__SSH_CONFIG) + src_line = re.search(ssh_password, file,re.M) + new_line = 'PermitRootLogin {}'.format(p_type) + if not src_line: + file_result = file + '\n{}'.format(new_line) + else: + file_result = file.replace(src_line.group(),new_line) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + msg = 'Set the root login method as: {}'.format(self.__root_login_types[p_type]) + public.WriteLog('SSH management',msg) + # return public.returnMsg(True, msg) + return public.return_message(0, 0, msg) + + def stop_root(self, get): + ''' + 开启密码登陆 + get: 无需传递参数 + ''' + ssh_password = r'\n\s*PermitRootLogin\s+\w+' + file = public.readFile(self.__SSH_CONFIG) + if len(re.findall(ssh_password, file)) == 0: + file_result = file + '\nPermitRootLogin no' + else: + file_result = re.sub(ssh_password, '\nPermitRootLogin no', file) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + public.WriteLog('SSH management','Set the root login method to: no') + return public.returnMsg(True, 'Disable successfully') + + def stop_password(self, get): + ''' + 关闭密码访问 + 无参数传递 + ''' + file = public.readFile(self.__SSH_CONFIG) + ssh_password = r'\n#?PasswordAuthentication\s\w+' + file_result = re.sub(ssh_password, '\nPasswordAuthentication no', file) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + public.WriteLog('SSH management','Disable password access') + return public.returnMsg(True, 'Closed successfully') + + def get_key(self, get): + ''' + 获取key 无参数传递 + ''' + key_type = self.get_ssh_key_type() + if key_type in self.__type_files.keys(): + key_file = self.__type_files[key_type] + key = public.readFile(key_file) + return public.returnMsg(True,key) + # return public.return_message(0, 0, key) + return public.returnMsg(True, '') + # return public.return_message(0, 0, '') + + def download_key(self, get): + ''' + @name 下载密钥 + ''' + download_file = '' + key_type = self.get_ssh_key_type() + if key_type in self.__type_files.keys(): + if os.path.exists(self.__type_files[key_type]): + download_file = self.__type_files[key_type] + + else: + for file in self.__key_files: + if not os.path.exists(file): continue + download_file = file + break + + if not download_file: return public.returnMsg(False, 'Key file not found!') + from flask import send_file + filename = "{}_{}".format(public.GetHost(),os.path.basename(download_file)) + return send_file(download_file,download_name=filename) + + def wirte(self, file, ret): + result = public.writeFile(file, ret) + return result + + def restart_ssh(self): + ''' + 重启ssh 无参数传递 + ''' + version = public.readFile('/etc/redhat-release') + act = 'restart' + if not os.path.exists('/etc/redhat-release'): + public.ExecShell('service ssh ' + act) + elif version.find(' 7.') != -1 or version.find(' 8.') != -1: + public.ExecShell("systemctl " + act + " sshd.service") + else: + public.ExecShell("/etc/init.d/sshd " + act) + #检查是否设置了钉钉 + def check_dingding(self, get): + ''' + 检查是否设置了钉钉 + ''' + #检查文件是否存在 + if not os.path.exists('/www/server/panel/data/dingding.json'):return False + dingding_config=public.ReadFile('/www/server/panel/data/dingding.json') + if not dingding_config:return False + #解析json + try: + dingding=json.loads(dingding_config) + if dingding['dingding_url']: + return True + except: + return False + + #开启SSH双因子认证 + def start_auth_method(self, get): + ''' + 开启SSH双因子认证 + ''' + #检查是否设置了钉钉 + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.start_ssh_authentication_two_factors() + + #关闭SSH双因子认证 + def stop_auth_method(self, get): + ''' + 关闭SSH双因子认证 + ''' + #检查是否设置了钉钉 + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.close_ssh_authentication_two_factors() + + #获取SSH双因子认证状态 + def get_auth_method(self, get): + ''' + 获取SSH双因子认证状态 + ''' + #检查是否设置了钉钉 + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.check_ssh_authentication_two_factors() + + #判断so文件是否存在 + def check_so_file(self, get): + ''' + 判断so文件是否存在 + ''' + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.is_check_so() + + #下载so文件 + def get_so_file(self, get): + ''' + 下载so文件 + ''' + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.download_so() + + #获取pin + def get_pin(self, get): + ''' + 获取pin + ''' + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return public.returnMsg(True, ssh_class.get_pin()) + + def get_login_record(self,get): + if os.path.exists(self.open_ssh_login): + + return public.returnMsg(True,'') + else: + return public.returnMsg(False,'') + def start_login_record(self,get): + if os.path.exists(self.open_ssh_login): + return public.returnMsg(True,'') + else: + public.writeFile(self.open_ssh_login,"True") + return public.returnMsg(True,'') + def stop_login_record(self,get): + if os.path.exists(self.open_ssh_login): + os.remove(self.open_ssh_login) + return public.returnMsg(True,'') + else: + return public.returnMsg(True,'') + # 获取登录记录列表 + def get_record_list(self, get): + if 'limit' in get: + limit = int(get.limit.strip()) + else: + limit = 12 + import page + page = page.Page() + count = public.M('ssh_login_record').order("id desc").count() + info = {} + info['count'] = count + info['row'] = limit + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + info['uri'] = get + info['return_js'] = '' + if hasattr(get, 'tojs'): + info['return_js'] = get.tojs + data = {} + # 获取分页数据 + data['page'] = page.GetPage(info, '1,2,3,4,5,8') + + data['data'] = public.M('ssh_login_record').order('id desc').limit( + str(page.SHIFT) + ',' + str(page.ROW)).select() + + return data + + def get_file_json(self,get): + + if os.path.exists(get.path): + ret=json.loads(public.ReadFile(get.path)) + return ret + else: + return '' + +if __name__ == '__main__': + import sys + type = sys.argv[1] + if type=='login': + try: + aa = ssh_security() + aa.login() + except:pass + else: + pass diff --git a/class_v2/ssh_terminal_v2.py b/class_v2/ssh_terminal_v2.py new file mode 100644 index 00000000..0f4edaae --- /dev/null +++ b/class_v2/ssh_terminal_v2.py @@ -0,0 +1,1259 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import json +import time +import os +import sys +import socket +import threading +import re + + +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +from io import BytesIO, StringIO + +def returnMsg(status,msg,value=None): + if value: + msg = public.get_msg_gettext(msg,value) + return {'status':status,'msg':msg} + +import public + +class ssh_terminal: + _panel_path = '/www/server/panel' + _save_path = _panel_path + '/config/ssh_info/' + _host = None + _port = 22 + _user = None + _pass = None + _pkey = None + _ws = None + _ssh = None + _last_cmd = "" + _last_cmd_tip = 0 + _log_type = public.get_msg_gettext('aaPanel terminal') + _history_len = 0 + _client = "" + _rep_ssh_config = False + _sshd_config_backup = None + _rep_ssh_service = False + _tp = None + _old_conf = None + _debug_file = 'logs/terminal.log' + _s_code = None + _last_num = 0 + _key_passwd = None + _video_addr = "" + _host_row_id = "" + + def __init__(self): + # 创建jp_login_record表记录ssh登录记录 + if not public.M('sqlite_master').where('type=? AND name=?', ('table', 'ssh_login_record')).count(): + public.M('').execute('''CREATE TABLE ssh_login_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + addr TEXT, + server_ip TEXT, + user_agent TEXT, + ssh_user TEXT, + login_time INTEGER DEFAULT 0, + close_time INTEGER DEFAULT 0, + video_addr TEXT);''') + public.M('').execute('CREATE INDEX ssh_login_record ON ssh_login_record (addr);') + self.time = time.time() + + def record(self, rtype, data): + if os.path.exists(public.get_panel_path() + "/data/open_ssh_login.pl") and self._video_addr: + path=self._video_addr + if rtype == 'header': + with open(path, 'w') as fw: + fw.write(json.dumps(data) + '\n') + return True + else: + with open(path, 'r') as fr: + content = json.loads(fr.read()) + stdout = content["stdout"] + atime = time.time() + iodata = [atime - self.time, data] + stdout.append(iodata) + content["stdout"] = stdout + with open(path, 'w') as fw: + fw.write(json.dumps(content) + '\n') + self.time = atime + return True + return False + + def connect(self): + ''' + @name 连接服务器 + @author hwliang<2020-08-07> + @return dict{ + status: bool 状态 + msg: string 详情 + } + ''' + if not self._host: return public.return_msg_gettext(False,'Wrong connection address') + + if not self._user: self._user = 'root' + if not self._port: self._port = 22 + self.is_local() + + if self._host in ['127.0.0.1','localhost']: + self._port = public.get_ssh_port() + self.set_sshd_config(True) + + num = 0 + while num < 5: + num +=1 + try: + self.debug(public.get_msg_gettext('Reconnection attempts:{}',(num,))) + if self._rep_ssh_config: time.sleep(0.1) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(2 + num) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192) + sock.connect((self._host, self._port)) + break + except Exception as e: + if num == 5: + self.set_sshd_config(True) + self.debug(public.get_msg_gettext('Retry connection failed, {}',(e,))) + if self._host in ['127.0.0.1','localhost']: + return returnMsg(False,'Connection failure: {}',("Authentication failed ," + self._user + "@" + self._host + ":" +str(self._port),)) + return returnMsg(False,'Connection failure: {}',(self._host,self._port)) + else: + time.sleep(0.2) + + + import paramiko + + self._tp = paramiko.Transport(sock) + + try: + self._tp.start_client() + if not self._pass and not self._pkey: + self.set_sshd_config(True) + return public.return_msg_gettext(False,'Password or private key cannot both be empty: {}:{}',(self._host,str(self._port))) + self._tp.banner_timeout=60 + if self._pkey: + self.debug(public.get_msg_gettext('Authenticating private key')) + if sys.version_info[0] == 2: + try: + self._pkey = self._pkey.encode('utf-8') + except: + pass + p_file = BytesIO(self._pkey) + else: + p_file = StringIO(self._pkey) + + try: + if self._key_passwd: + pkey = paramiko.RSAKey.from_private_key(p_file,password=self._key_passwd) + else: + pkey = paramiko.RSAKey.from_private_key(p_file) + self.debug("尝试使用RSA私钥认证") + except Exception as ex: + try: + p_file.seek(0) # 重置游标 + if self._key_passwd: + pkey = paramiko.Ed25519Key.from_private_key(p_file,password=self._key_passwd) + else: + pkey = paramiko.Ed25519Key.from_private_key(p_file) + self.debug("尝试使用Ed25519私钥认证") + except: + try: + p_file.seek(0) + if self._key_passwd: + pkey = paramiko.ECDSAKey.from_private_key(p_file,password=self._key_passwd) + else: + pkey = paramiko.ECDSAKey.from_private_key(p_file) + self.debug("尝试使用ECDSA私钥认证") + except: + p_file.seek(0) + if self._key_passwd: + try: + pkey = paramiko.DSSKey.from_private_key(p_file,password=self._key_passwd) + except Exception as ex: + ex = str(ex) + if ex.find('OpenSSH private key file checkints do not match') != -1: + return public.returnMsg(False,'Incorrect private key password:{}'.format(ex)) + elif ex.find('encountered RSA key, expected DSA key') != -1: + pkey = paramiko.RSAKey.from_private_key(p_file,password=self._key_passwd) + else: + return public.returnMsg(False,'Private key error: {}'.format(ex)) + else: + pkey = paramiko.DSSKey.from_private_key(p_file) + if not pkey: return public.returnMsg(False,'Private key error!') + self._tp.auth_publickey(username=self._user, key=pkey) + else: + try: + self._tp.auth_none(self._user) + except Exception as e: + e = str(e) + if e.find('keyboard-interactive') >= 0: + self._auth_interactive() + else: + self.debug('Authenticating password') + self._tp.auth_password(username=self._user, password=self._pass) + # self._tp.auth_password(username=self._user, password=self._pass) + except Exception as e: + if self._old_conf: + s_file = '/www/server/panel/config/t_info.json' + if os.path.exists(s_file): os.remove(s_file) + self.set_sshd_config(True) + self._tp.close() + e = str(e) + if e.find('websocket error!') != -1: + return public.return_msg_gettext(True,'connection succeeded') + if e.find('Authentication timeout') != -1: + self.debug("认证超时{}".format(e)) + return public.return_msg_gettext(False,'Authentication timed out, please press enter to try again!{}',(e,)) + if e.find('Authentication failed') != -1: + self.debug('认证失败{}'.format(e)) + if self._key_passwd: + sshd_config = public.readFile('/etc/ssh/sshd_config') + if sshd_config and sshd_config.find('ssh-dss') == -1: + return returnMsg(False,'The private key verification fails, the private key may be incorrect, or the ssh-dss private key authentication type may not be enabled in the /etc/ssh/sshd_config configuration file') + return returnMsg(False,'Authentication failed, please check whether the private key is correct: {}'.format(e + "," + self._user + "@" + self._host + ":" +str(self._port))) + return public.return_msg_gettext(False,'Account or Password incorrect: {}',(str(e + "," + self._user + "@" + self._host + ":" +str(self._port)),)) + if e.find('Bad authentication type; allowed types') != -1: + self.debug(public.get_msg_gettext('Authentication failed {}',(str(e),))) + if self._host in ['127.0.0.1','localhost'] and self._pass == 'none': + return public.return_msg_gettext(False,'Username or Password incorrect: {}',(str("Authentication failed ," + self._user + "@" + self._host + ":" +str(self._port)),)) + return public.return_msg_gettext(False,'Unsupported authentication type: {}',(str(e))) + if e.find('Connection reset by peer') != -1: + self.debug(public.get_msg_gettext('The target server actively refused the connection')) + return public.return_msg_gettext(False,public.get_msg_gettext('The target server actively refused the connection')) + if e.find('Error reading SSH protocol banner') != -1: + self.debug('The protocol header response timed out') + return public.return_msg_gettext(False,public.get_msg_gettext('The protocol header response timed out, and the network quality with the target server was too bad: {}',(str(e),))) + if e.find('encountered RSA key, expected DSA key') != -1: + self.debug('Private keys may require password access') + return public.return_msg_gettext(False,public.get_msg_gettext('Private keys may require password access: {}',(str(e),))) + if e.find('password and salt must not be empty') != -1: + self.debug('Private keys may require password access') + return public.return_msg_gettext(False,public.get_msg_gettext('Private keys may require password access: {}',(str(e),))) + if not e: + self.debug('The SSH protocol handshake timed out') + return public.return_msg_gettext(False,"The SSH protocol handshake timed out, and the network quality with the target server is too bad") + err = public.get_error_info() + self.debug(err) + return public.return_msg_gettext(False,public.get_msg_gettext('unknown error: {}',(str(err),))) + + self.debug(public.get_msg_gettext('The authentication is successful and the session channel is being constructed')) + self._ssh = self._tp.open_session() + self._ssh.get_pty(term='xterm', width=100, height=34) + self._ssh.invoke_shell() + self._connect_time = time.time() + self._last_send = [] + from BTPanel import request + self._client = public.GetClientIp() +':' + str(request.environ.get('REMOTE_PORT')) + public.write_log_gettext(self._log_type,'Successfully logged in to the SSH server [{}:{}]',(self._host,str(self._port))) + self.history_send(public.get_msg_gettext("Login success\n")) + self.set_sshd_config(True) + self.debug(public.get_msg_gettext('Login success')) + from BTPanel import session + self._video_addr = "/www/server/panel/plugin/jumpserver/static/video/%s.json" % str(int(self._connect_time)) + if not os.path.exists("/www/server/panel/plugin/jumpserver/static/video/"): + os.makedirs("/www/server/panel/plugin/jumpserver/static/video/") + # 如果开启了录像功能 + user_agent = str(request.headers.get('User-Agent')) + if os.path.exists(public.get_panel_path() + "/data/open_ssh_login.pl"): + self._host_row_id = public.M('ssh_login_record').add( + 'addr,server_ip,ssh_user,user_agent,login_time,video_addr', + (self._client, self._host, self._user, user_agent + , int(self._connect_time), + self._video_addr)) + + self.record('header', { + "version": 1, + "width": 100, + "height": 29, + "timestamp": int(self._connect_time), + "env": { + "TERM": "xterm", + "SHELL": "/bin/bash", + }, + "stdout": [] + }) + return public.return_msg_gettext(True,'connection succeeded') + + def _auth_interactive(self): + self.debug('Verification Code') + + self.brk = False + + def handler(title, instructions, prompt_list): + if not self._ws: raise public.PanelError('websocket error!') + if instructions: + self._ws.send(instructions) + if title: + self._ws.send(title) + resp = [] + for pr in prompt_list: + if str(pr[0]).strip() == "Password:": + resp.append(self._pass) + elif str(pr[0]).strip() == "Verification code:": + # 获取前段传入的验证码 + self._ws.send("Verification code# ") + self._s_code = True + code = "" + while True: + data = self._ws.receive() + if data.find('"resize":1') != -1: + self.resize(data) + continue + self._ws.send(data) + if data in ["\n", "\r"]: break + code += data + resp.append(code) + self._ws.send("\n") + self._s_code = None + return tuple(resp) + + self._tp.auth_interactive(self._user, handler) + + def get_login_user(self): + ''' + @name 获取本地登录用户 + @author hwliang<2020-08-07> + @return string + ''' + + if self._user != 'root': return self._user + l_user = 'root' + ssh_config_file = '/etc/ssh/sshd_config' + ssh_config = public.readFile(ssh_config_file) + if not ssh_config: return l_user + + if ssh_config.find('PermitRootLogin yes') != -1: return l_user + + + user_list = self.get_ulist() + login_user = '' + for u_info in user_list: + if u_info['user'] == 'root': continue + if u_info['login'] == '/bin/bash': + login_user = u_info['user'] + break + + if not login_user: + return l_user + + return login_user + + + def get_ulist(self): + ''' + @name 获取本地用户列表 + @author hwliang<2020-08-07> + @return list + ''' + u_data = public.readFile('/etc/passwd') + u_list = [] + for i in u_data.split("\n"): + u_tmp = i.split(':') + if len(u_tmp) < 3: continue + u_info = {} + u_info['user'],u_info['pass'],u_info['uid'],u_info['gid'],u_info['user_msg'],u_info['home'],u_info['login'] = u_tmp + u_list.append(u_info) + return u_list + + def is_local(self): + ''' + @name 处理本地连接 + @author hwliang<2020-08-07> + @ps 如果host为127.0.0.1或localhost,则尝试自动使用publicKey登录 + @return void + ''' + + if self._pass: return + if self._pkey: return + if self._host in ['127.0.0.1','localhost']: + try: + self._port = public.get_ssh_port() + self.set_sshd_config() + s_file = '/www/server/panel/config/t_info.json' + if os.path.exists(s_file): + ssh_info = json.loads(public.en_hexb(public.readFile(s_file))) + self._host = ssh_info['host'].strip() + if 'username' in ssh_info: + self._user = ssh_info['username'] + if 'pkey' in ssh_info: + self._pkey = ssh_info['pkey'] + if 'password' in ssh_info: + self._pass = ssh_info['password'] + self._old_conf = True + return + ssh_key_type_file = '{}/data/ssh_key_type.pl'.format(public.get_panel_path()) + ssh_key_type = '' + if os.path.exists(ssh_key_type_file): + ssh_key_type_new = public.readFile(ssh_key_type_file) + if ssh_key_type_new: ssh_key_type = ssh_key_type_new.strip() + login_user = self.get_login_user() + if self._user == 'root' and login_user == 'root': + id_rsa_file = ['/root/.ssh/id_ed25519','/root/.ssh/id_ecdsa','/root/.ssh/id_rsa','/root/.ssh/id_rsa_bt'] + if ssh_key_type: id_rsa_file.insert(0,'/root/.ssh/id_{}'.format(ssh_key_type)) + for ifile in id_rsa_file: + if os.path.exists(ifile): + self._pkey = public.readFile(ifile) + host_path = self._save_path + self._host + if not os.path.exists(host_path): + os.makedirs(host_path,384) + return + + + if not self._pass or not self._pkey or not self._user: + home_path = '/home/' + login_user + if login_user == 'root': + home_path = '/root' + self._user = login_user + id_rsa_file = [home_path + '/.ssh/id_ed25519',home_path + '/.ssh/id_ecdsa',home_path + '/.ssh/id_rsa',home_path + '/.ssh/id_rsa_bt'] + if ssh_key_type: id_rsa_file.insert(0,home_path + '/.ssh/id_{}'.format(ssh_key_type)) + for ifile in id_rsa_file: + if os.path.exists(ifile): + self._pkey = public.readFile(ifile) + return + + self._pass = 'none' + return + + except: + return + + def get_sys_version(self): + ''' + @name 获取操作系统版本 + @author hwliang<2020-08-13> + @return bool + ''' + version = public.readFile('/etc/redhat-release') + if not version: + version = public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace(r'\l','').strip() + else: + version = version.replace('release ','').replace('Linux','').replace('(Core)','').strip() + return version + + def get_ssh_status(self): + ''' + @name 获取SSH服务状态 + @author hwliang<2020-08-13> + @return bool + ''' + version = self.get_sys_version() + if os.path.exists('/usr/bin/apt-get'): + if os.path.exists('/etc/init.d/sshd'): + status = public.ExecShell("service sshd status | grep -P '(dead|stop|not running)'|grep -v grep") + else: + status = public.ExecShell("service ssh status | grep -P '(dead|stop|not running)'|grep -v grep") + else: + if version.find(' 7.') != -1 or version.find(' 8.') != -1 or version.find('Fedora') != -1: + status = public.ExecShell("systemctl status sshd.service | grep 'dead'|grep -v grep") + else: + status = public.ExecShell("/etc/init.d/sshd status | grep -e 'stopped' -e '已停'|grep -v grep") + if len(status[0]) > 3: + status = False + else: + status = True + return status + + def is_running(self,rep = False): + ''' + @name 处理SSH服务状态 + @author hwliang<2020-08-13> + @param rep 是否恢复原来的SSH服务状态 + @return bool + ''' + try: + if rep and self._rep_ssh_service: + self.restart_ssh('stop') + return True + + ssh_status = self.get_ssh_status() + if not ssh_status: + self.restart_ssh('start') + self._rep_ssh_service = True + return True + return False + except: + return False + + + def set_sshd_config(self,rep = False): + ''' + @name 设置本地SSH配置文件,以支持pubkey认证 + @author hwliang<2020-08-13> + @param rep 是否恢复ssh配置文件 + @return bool + ''' + self.is_running(rep) + if rep and not self._rep_ssh_config: + return False + + try: + sshd_config_file = '/etc/ssh/sshd_config' + if not os.path.exists(sshd_config_file): + return False + + sshd_config = public.readFile(sshd_config_file) + + if not sshd_config: + return False + + if rep: + if self._sshd_config_backup: + public.writeFile(sshd_config_file,self._sshd_config_backup) + self.restart_ssh() + return True + + pin = r'^\s*PubkeyAuthentication\s+(yes|no)' + pubkey_status = re.findall(pin,sshd_config,re.I) + if pubkey_status: + if pubkey_status[0] == 'yes': + pubkey_status = True + else: + pubkey_status = False + + pin = r'^\s*RSAAuthentication\s+(yes|no)' + rsa_status = re.findall(pin,sshd_config,re.I) + if rsa_status: + if rsa_status[0] == 'yes': + rsa_status = True + else: + rsa_status = False + + self._sshd_config_backup = sshd_config + is_write = False + if not pubkey_status: + sshd_config = re.sub(r'\n#?PubkeyAuthentication\s\w+','\nPubkeyAuthentication yes',sshd_config) + is_write = True + if not rsa_status: + sshd_config = re.sub(r'\n#?RSAAuthentication\s\w+','\nRSAAuthentication yes',sshd_config) + is_write = True + + if is_write: + public.writeFile(sshd_config_file,sshd_config) + self._rep_ssh_config = True + self.restart_ssh() + else: + self._sshd_config_backup = None + + return True + except: + return False + + def restart_ssh(self,act = 'reload'): + ''' + 重启ssh 无参数传递 + ''' + version = public.readFile('/etc/redhat-release') + if not os.path.exists('/etc/redhat-release'): + public.ExecShell('service ssh ' + act) + elif version.find(' 7.') != -1 or version.find(' 8.') != -1: + public.ExecShell("systemctl " + act + " sshd.service") + else: + public.ExecShell("/etc/init.d/sshd " + act) + + def resize(self, data): + ''' + @name 调整终端大小 + @author hwliang<2020-08-07> + @param data 终端尺寸数据 + { + cols: int 列 + rows: int 行 + } + @return bool + ''' + try: + data = json.loads(data) + self._ssh.resize_pty(width=data['cols'], height=data['rows']) + return True + except: + return False + + + def recv(self): + ''' + @name 读取tty缓冲区数据 + @author hwliang<2020-08-07> + @return void + ''' + n = 0 + try: + while self._ws.connected: + resp_line = self._ssh.recv(1024) + if not resp_line: + if not self._tp.is_active(): + self.debug(public.get_msg_gettext('Channel disconnected')) + self._ws.send(public.get_msg_gettext('The connection is disconnected, press enter to try to reconnect!')) + self.close() + return + + if not resp_line: + n+=1 + if n > 5: break + continue + n = 0 + if not self._ws.connected: + return + try: + result = resp_line.decode('utf-8','ignore') + except: + try: + result = resp_line.decode() + except: + result = str(resp_line) + self.record('iodata', result) + self._ws.send(result) + + # self.history_recv(result) + except Exception as e: + e = str(e) + if e.find('closed') != -1: + self.debug(public.getMsg('SSH_LOGIN_INFO')) + elif self._ws.connected: + self.debug(public.get_msg_gettext('Error reading tty buffer data, {}',(str(e),))) + + if not self._ws.connected: + self.debug(public.get_msg_gettext('The client has actively disconnected')) + self.close() + + def send(self): + ''' + @name 写入数据到缓冲区 + @author hwliang<2020-08-07> + @return void + ''' + try: + while self._ws.connected: + if self._s_code: + time.sleep(0.1) + continue + client_data = self._ws.receive() + if not client_data: continue + if client_data == '{}': continue + if len(client_data) > 10: + if client_data.find('{"host":"') != -1: + continue + if client_data.find('"resize":1') != -1: + self.resize(client_data) + continue + self._ssh.send(client_data) + # self.history_send(client_data) + except Exception as ex: + ex = str(ex) + + if ex.find('_io.BufferedReader') != -1: + self.debug(public.get_msg_gettext('An error occurred while reading data from websocket. Retrying')) + self.send() + return + elif ex.find('closed') != -1: + self.debug(public.get_msg_gettext('SSH_LOGIN_INFO')) + else: + self.debug(public.get_msg_gettext('An error occurred while writing data to the buffer: {}',(str(ex),))) + + if not self._ws.connected: + self.debug(public.get_msg_gettext('The client has actively disconnected')) + self.close() + + + def history_recv(self,recv_data): + ''' + @name 从接收实体保存命令 + @author hwliang<2020-08-12> + @param recv_data 数据实体 + @return void + ''' + #处理TAB补登 + if self._last_cmd_tip == 1: + if not recv_data.startswith('\r\n'): + self._last_cmd += recv_data.replace('\u0007','').replace("\x07","").strip() + self._last_cmd_tip = 0 + + #上下切换命令 + if self._last_cmd_tip == 2: + self._last_cmd = recv_data.strip().replace("\x08","").replace("\x07","").replace("\x1b[K","") + self._last_cmd_tip = 0 + + def history_send(self,send_data): + ''' + @name 从发送实体保存命令 + @author hwliang<2020-08-12> + @param send_data 数据实体 + @return void + ''' + if not send_data: return + his_path = self._save_path + self._host + if not os.path.exists(his_path): return + his_file = his_path + '/history.pl' + + #上下切换命令 + if send_data in ["\x1b[A","\x1b[B"]: + self._last_cmd_tip = 2 + return + + #左移光标 + if send_data in ["\x1b[C"]: + self._last_num -= 1 + return + + # 右移光标 + if send_data in ["\x1b[D"]: + self._last_num += 1 + return + + #退格 + if send_data == "\x7f": + self._last_cmd = self._last_cmd[:-1] + return + + #过滤特殊符号 + if send_data in ["\x1b[C","\x1b[D","\x1b[K","\x07","\x08","\x03","\x01","\x02","\x04","\x05","\x06","\x1bOB","\x1bOA","\x1b[8P","\x1b","\x1b[4P","\x1b[6P","\x1b[5P"]: + return + + #Tab补全处理 + if send_data == "\t": + self._last_cmd_tip = 1 + return + + if str(send_data).find("\x1b") != -1: + return + + if send_data[-1] in ['\r','\n']: + if not self._last_cmd: return + his_shell = [int(time.time()),self._client,self._user,self._last_cmd] + public.writeFile(his_file, json.dumps(his_shell) + "\n","a+") + self._last_cmd = "" + + #超过50M则保留最新的20000行 + if os.stat(his_file).st_size > 52428800: + his_tmp = public.GetNumLines(his_file,20000) + public.writeFile(his_file, his_tmp) + else: + if self._last_num >= 0: + self._last_cmd += send_data + + + def close(self): + ''' + @name 释放连接 + @author hwliang<2020-08-07> + @return void + ''' + try: + if self._host_row_id: + public.M('ssh_login_record').where('id=?', self._host_row_id).update( + {'close_time': int(time.time())}) + if self._ssh: + self._ssh.close() + if self._tp: # 关闭宿主服务 + self._tp.close() + if self._ws.connected: + self._ws.close() + except: + pass + + + def set_attr(self,ssh_info): + ''' + @name 设置对象属性,并连接服务器 + @author hwliang<2020-08-07> + @return void + ''' + self._host = ssh_info['host'].strip() + self._port = int(ssh_info['port']) + if 'username' in ssh_info: + self._user = ssh_info['username'] + if 'pkey' in ssh_info: + self._pkey = ssh_info['pkey'] + if 'password' in ssh_info: + self._pass = ssh_info['password'] + if 'pkey_passwd' in ssh_info: + self._key_passwd = ssh_info['pkey_passwd'] + try: + result = self.connect() + except Exception as ex: + if str(ex).find("NoneType") == -1: + raise public.PanelError(ex) + return result + + + def heartbeat(self): + ''' + @name 心跳包 + @author hwliang<2020-09-10> + @return void + ''' + while True: + time.sleep(30) + if self._tp.is_active(): + self._tp.send_ignore() + else: + break + if self._ws.connected: + self._ws.send("") + else: + break + + def debug(self,msg): + ''' + @name 写debug日志 + @author hwliang<2020-09-10> + @return void + ''' + msg = "{} - {}:{} => {} \n".format(public.format_date(),self._host,self._port,msg) + self.history_send(msg) + public.writeFile(self._debug_file,msg,'a+') + + def run(self,web_socket, ssh_info=None): + ''' + @name 启动SSH客户端对象 + @author hwliang<2020-08-07> + @param web_socket websocket句柄对像 + @param ssh_info SSH信息{ + host: 主机地址, + port: 端口 + username: 用户名 + password: 密码 + pkey: 密钥(如果不为空,将使用密钥连接) + } + @return void + ''' + self._ws = web_socket + if not self._ssh: + if not ssh_info: + return + result = self.set_attr(ssh_info) + else: + result = public.get_msg_gettext(True,'ALREADY_CONNECTED') + if result['status']: + sendt = threading.Thread(target=self.send) + recvt = threading.Thread(target=self.recv) + ht = threading.Thread(target=self.heartbeat) + sendt.start() + recvt.start() + ht.start() + sendt.join() + recvt.join() + ht.join() + self.close() + else: + self._ws.send(result['msg']) + self.close() + + def __del__(self): + ''' + 自动释放 + ''' + self.close() + + + +class ssh_host_admin(ssh_terminal): + _panel_path = '/www/server/panel' + _save_path = _panel_path + '/config/ssh_info/' + _pass_file = _panel_path + '/data/a_pass.pl' + _user_command_file = _save_path + '/user_command.json' + _sys_command_file = _save_path + '/sys_command.json' + _pass_str = None + + def __init__(self): + self.__create_aes_pass() + + def __create_aes_pass(self): + ''' + @name 创建AES密码 + @author + @return string + ''' + if not os.path.exists(self._save_path): + os.makedirs(self._save_path,384) + if not os.path.exists(self._pass_file): + public.writeFile(self._pass_file,public.GetRandomString(16)) + public.set_mode(self._pass_file,600) + if not self._pass_str: + self._pass_str = public.readFile(self._pass_file) + if not self._pass_str: + self._pass_str = public.GetRandomString(16) + public.writeFile(self._pass_file,self._pass_str) + public.set_mode(self._pass_file,600) + + def get_host_list(self,args = None): + ''' + @name 获取本机保存的SSH信息列表 + @author hwliang<2020-08-07> + @param args + @return list + ''' + + host_list = [] + for name in os.listdir(self._save_path): + info_file = self._save_path + name +'/info.json' + if not os.path.exists(info_file): continue + try: + info_tmp = self.get_ssh_info(name) + host_info = {} + host_info['host'] = name + host_info['port'] = info_tmp['port'] + host_info['ps'] = info_tmp['ps'] + host_info['sort'] = int(info_tmp['sort']) + except: + if os.path.exists(info_file): + os.remove(info_file) + continue + + host_list.append(host_info) + + host_list = sorted(host_list,key=lambda x: x['sort'],reverse=False) + return host_list + + def get_host_find(self,args): + ''' + @name 获取指定SSH信息 + @author hwliang<2020-08-07> + @param args{ + host: 主机地址 + } + @return dict + ''' + args.host = args.host.strip() + info_file = self._save_path + args.host +'/info.json' + if not os.path.exists(info_file): + return public.return_msg_gettext(False,'The specified SSH information does not exist!') + info_tmp = self.get_ssh_info(args.host) + host_info = {} + host_info['host'] = args.host + host_info['port'] = info_tmp['port'] + host_info['ps'] = info_tmp['ps'] + host_info['sort'] = info_tmp['sort'] + host_info['username'] = info_tmp['username'] + host_info['password'] = info_tmp['password'] + host_info['pkey'] = info_tmp['pkey'] + host_info['pkey_passwd'] = '' + if 'pkey_passwd' in info_tmp: + host_info['pkey_passwd'] = info_tmp['pkey_passwd'] + return host_info + + def modify_host(self,args): + ''' + @name 修改SSH信息 + @author hwliang<2020-08-07> + @param args{ + host: 被修改的主机地址, + new_host: 新的主机地址, + port: 端口 + ps: 备注 + sort: 排序(可选) + username: 用户名 + password: 密码 + pkey: 密钥(如果不为空,将使用密钥连接) + pkey_passwd: 密钥的密码 + } + @return dict + ''' + args.new_host = args.new_host.strip() + args.host = args.host.strip() + if args.host != args.new_host: + info_file = self._save_path + args.new_host +'/info.json' + if os.path.exists(info_file): + return public.return_msg_gettext(False,'The specified host address has been added to other SSH information!') + + info_file = self._save_path + args.host +'/info.json' + + if not os.path.exists(info_file): + return public.return_msg_gettext(False,'The specified SSH information does not exist!') + + if not 'sort' in args: + r_data = public.aes_decrypt(public.readFile(info_file),self._pass_str) + info_tmp = json.loads(r_data) + args.sort = info_tmp['sort'] + + host_info = {} + host_info['host'] = args.new_host + host_info['port'] = int(args['port']) + host_info['ps'] = args['ps'] + host_info['sort'] = args['sort'] + host_info['username'] = args['username'] + host_info['password'] = args['password'] + host_info['pkey'] = args['pkey'] + if 'pkey_passwd' in args: + host_info['pkey_passwd'] = args['pkey_passwd'] + else: + host_info['pkey_passwd'] = '' + if not host_info['pkey']: host_info['pkey'] = '' + result = self.set_attr(host_info) + if not result['status']: return result + self.save_ssh_info(args.host,host_info) + if args.host != args.new_host: + public.ExecShell('mv {} {}'.format(self._save_path + args.host,self._save_path + args.new_host)) + public.write_log_gettext(self._log_type,'Modify the SSH information of HOST: {}',(args.host,)) + return public.return_msg_gettext(True,'Setup successfully!') + + def create_host(self,args): + ''' + @name 添加SSH信息 + @author hwliang<2020-08-07> + @param args{ + host: 主机地址, + port: 端口 + ps: 备注 + sort: 排序(可选,默认0) + username: 用户名 + password: 密码 + pkey: 密钥(如果不为空,将使用密钥连接) + pkey_passwd: 密钥的密码 + } + @return dict + ''' + args.host = args.host.strip() + host_path = self._save_path + args.host + info_file = host_path +'/info.json' + if os.path.exists(info_file): + args.new_host = args.host + return self.modify_host(args) + #return public.returnMsg(False,'指定SSH信息已经添加过了!') + if not os.path.exists(host_path): + os.makedirs(host_path,384) + if not 'sort' in args: args.sort = 0 + if not 'ps' in args: args.ps = args.host + host_info = {} + host_info['host'] = args.host + host_info['port'] = int(args['port']) + host_info['ps'] = args['ps'] + host_info['sort'] = int(args['sort']) + host_info['username'] = args['username'] + host_info['password'] = args['password'] + host_info['pkey'] = args['pkey'] + host_info['pkey_passwd'] = '' + if 'pkey_passwd' in args: + host_info['pkey_passwd'] = args['pkey_passwd'] + result = self.set_attr(host_info) + if not result['status']: return result + self.save_ssh_info(args.host,host_info) + public.write_log_gettext(self._log_type,'Add the SSH information of HOST: {}',(str(args.host),)) + return public.return_msg_gettext(True,'Setup successfully!') + + + def remove_host(self,args): + ''' + @name 删除指定SSH信息 + @author hwliang<2020-08-07> + @param args{ + host: 主机地址 + } + @return dict + ''' + args.host = args.host.strip() + if not args.host: return public.return_msg_gettext(False,'Parameter ERROR!') + host_path = self._save_path + args.host + if not os.path.exists(host_path): + return public.return_msg_gettext(False,'The specified SSH information does not exist!') + public.ExecShell("rm -rf {}".format(host_path)) + public.write_log_gettext(self._log_type,'Delete the SSH information of HOST: {}',(str(args.host),)) + return public.return_msg_gettext(True,'Setup successfully!') + + + def get_ssh_info(self,host): + ''' + @name 获取并解密指定SSH信息 + @author hwliang<2020-08-07> + @param host 主机地址 + @return dict or False + ''' + info_file = self._save_path + host + '/info.json' + if not os.path.exists(info_file): return False + try: + r_data = public.aes_decrypt(public.readFile(info_file),self._pass_str) + except ValueError as ex: + if str(ex).find('Incorrect AES key length') != -1: + if os.path.exists(self._pass_file): + os.remove(self._pass_file) + self.__create_aes_pass() + r_data = public.aes_decrypt(public.readFile(info_file),self._pass_str) + + return json.loads(r_data) + + def save_ssh_info(self,host,host_info): + ''' + @name 获取并解密指定SSH信息 + @author hwliang<2020-08-07> + @param host 主机地址 + @param host_info ssh信息字典 + @return bool + ''' + host_path = self._save_path + host + if not os.path.exists(host_path): + os.makedirs(host_path,384) + info_file = host_path +'/info.json' + r_data = public.aes_encrypt(json.dumps(host_info),self._pass_str) + public.writeFile(info_file,r_data) + return True + + def set_sort(self,args): + ''' + @name 获取并解密指定SSH信息 + @author hwliang<2020-08-07> + @param args{ + sort_list{ + 主机host : 排序编号, + 主机host : 排序编号, + ... + } + } + @return bool + ''' + if not 'sort_list' in args: + return public.return_msg_gettext(False,'Please pass in the [sort_list] field') + sort_list = json.loads(args.sort_list) + for name in sort_list.keys(): + info_file = self._save_path + name + '/info.json' + if not os.path.exists(info_file): continue + + ssh_info = self.get_ssh_info(name) + ssh_info['sort'] = int(sort_list[name]) + self.save_ssh_info(name,ssh_info) + return public.return_msg_gettext(True,'Setup successfully!') + + def get_command_list(self,args = None, user_cmd = False , sys_cmd = False): + ''' + @name 获取常用命令列表 + @author hwliang<2020-08-08> + @param args + @param user_cmd 是否不获取用户配置 + @param sys_cmd 是否不获取系统配置 + @return list + ''' + + sys_command = [] + if not sys_cmd: + if os.path.exists(self._sys_command_file): + sys_command = json.loads(public.readFile(self._sys_command_file)) + + user_command = [] + if not user_cmd: + if os.path.exists(self._user_command_file): + user_command = json.loads(public.readFile(self._user_command_file)) + + command = sys_command + user_command + return command + + + def command_exists(self,command,title): + ''' + @name 判断命令是否存在 + @author hwliang<2020-08-08> + @param command 常用命令列表 + @param title 命令标题 + @return bool + ''' + for cmd in command: + if cmd['title'] == title: return True + return False + + def save_command(self,command,sys_cmd=False): + ''' + @name 保存常用命令 + @author hwliang<2020-08-08> + @param command 常用命令列表 + @param sys_cmd 是否为系统配置 + @return void + ''' + s_file = self._user_command_file + if sys_cmd: + s_file = self._sys_command_file + public.writeFile(s_file,json.dumps(command)) + + def create_command(self,args): + ''' + @name 创建常用命令 + @author hwliang<2020-08-08> + @param args{ + title 标题 + shell 命令文本 + } + @return dict + ''' + args.title = args.title.strip() + command = self.get_command_list(sys_cmd=True) + + if self.command_exists(command,args.title): + return public.return_msg_gettext(False,'The specified command name already exists') + + cmd = { + "title": args.title, + "shell": args.shell.strip() + } + + command.append(cmd) + self.save_command(command) + public.write_log_gettext(self._log_type,'Add common commands [{}]',(str(args.title),)) + return public.return_msg_gettext(True,'Setup successfully!') + + def get_command_find(self,args = None, title=None): + ''' + @name 获取指定命令信息 + @author hwliang<2020-08-08> + @param args{ + title 标题 + } 可选 + @param title 标题 可选 + @return dict + ''' + if args: title = args.title.strip() + command = self.get_command_list() + for cmd in command: + if cmd['title'] == title or cmd['title'] == args.title: + return cmd + return public.return_msg_gettext(False,'The specified command does not exist') + + def modify_command(self,args): + ''' + @name 修改常用命令 + @author hwliang<2020-08-08> + @param args{ + title 标题 + new_title 新标题 + shell 命令文本 + } + @return dict + ''' + args.title = args.title.strip() + command = self.get_command_list(sys_cmd=True) + if not self.command_exists(command,args.title): + return public.return_msg_gettext(False,'The specified command does not exist') + for i in range(len(command)): + if command[i]['title'] == args.title or command[i]['title'] == title: + command[i]['title'] = args.new_title.strip() + command[i]['shell'] = args.shell.strip() + break + self.save_command(command) + public.write_log_gettext(self._log_type,'Modify common commands [{}]',(str(args.title),)) + return public.return_msg_gettext(True,'Setup successfully!') + + def remove_command(self,args): + ''' + @name 删除指定命令 + @author hwliang<2020-08-08> + @param args{ + title 标题 + } + @return dict + ''' + args.title = args.title.strip() + command = self.get_command_list(sys_cmd=True) + if not self.command_exists(command,args.title): + return public.return_msg_gettext(False,'The specified command does not exist') + for i in range(len(command)): + if command[i]['title'] == args.title: + del(command[i]) + break + + self.save_command(command) + public.write_log_gettext(self._log_type,'Delete common commands [{}]',(str(args.title),)) + return public.return_msg_gettext(True,'Setup successfully!') diff --git a/class_v2/system_v2.py b/class_v2/system_v2.py new file mode 100644 index 00000000..7184cb1c --- /dev/null +++ b/class_v2/system_v2.py @@ -0,0 +1,1036 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aapanel x3 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import psutil,time,os,public,re,sys +from public.validate import Param +try: + from BTPanel import session,cache +except: + pass +class system: + setupPath = None + ssh = None + shell = None + + def __init__(self): + self.setupPath = public.GetConfigValue('setup_path') + + def GetConcifInfo(self,get=None): + #取环境配置信息 + if 'config' not in session: + session['config'] = public.M('config').where("id=?",('1',)).field('webserver,sites_path,backup_path,status,mysql_root').find() + if 'email' not in session['config']: + session['config']['email'] = public.M('users').where("id=?",('1',)).getField('email') + data = {} + data = session['config'] + data['webserver'] = public.get_webserver() + #PHP版本 + phpVersions = public.get_php_versions() + + data['php'] = [] + + for version in phpVersions: + tmp = {} + tmp['setup'] = os.path.exists(self.setupPath + '/php/'+version+'/bin/php') + if tmp['setup']: + phpConfig = self.GetPHPConfig(version) + tmp['version'] = version + tmp['max'] = phpConfig['max'] + tmp['maxTime'] = phpConfig['maxTime'] + tmp['pathinfo'] = phpConfig['pathinfo'] + tmp['status'] = os.path.exists('/tmp/php-cgi-' + version + '.sock') + data['php'].append(tmp) + + tmp = {} + data['webserver'] = '' + serviceName = 'nginx' + tmp['setup'] = False + phpversion = "54" + phpport = '888' + pstatus = False + pauth = False + if os.path.exists(self.setupPath+'/nginx'): + data['webserver'] = 'nginx' + serviceName = 'nginx' + tmp['setup'] = os.path.exists(self.setupPath +'/nginx/sbin/nginx') + configFile = self.setupPath + '/nginx/conf/nginx.conf' + try: + if os.path.exists(configFile): + conf = public.readFile(configFile) + rep = r"listen\s+([0-9]+)\s*;" + rtmp = re.search(rep,conf) + if rtmp: + phpport = rtmp.groups()[0] + + if conf.find('AUTH_START') != -1: pauth = True + if conf.find(self.setupPath + '/stop') == -1: pstatus = True + configFile = self.setupPath + '/nginx/conf/enable-php.conf' + conf = public.readFile(configFile) + rep = r"php-cgi-([0-9]+)\.sock" + rtmp = re.search(rep,conf) + if rtmp: + phpversion = rtmp.groups()[0] + except: + pass + + elif os.path.exists(self.setupPath+'/apache'): + data['webserver'] = 'apache' + serviceName = 'httpd' + tmp['setup'] = os.path.exists(self.setupPath +'/apache/bin/httpd') + configFile = self.setupPath + '/apache/conf/extra/httpd-vhosts.conf' + try: + if os.path.exists(configFile): + conf = public.readFile(configFile) + rep = r"php-cgi-([0-9]+)\.sock" + rtmp = re.search(rep,conf) + if rtmp: + phpversion = rtmp.groups()[0] + rep = "Listen\\s+([0-9]+)\\s*\n" + rtmp = re.search(rep,conf) + if rtmp: + phpport = rtmp.groups()[0] + if conf.find('AUTH_START') != -1: pauth = True + if conf.find(self.setupPath + '/stop') == -1: pstatus = True + except: + pass + elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): + data['webserver'] = 'openlitespeed' + serviceName = 'openlitespeed' + tmp['setup'] = os.path.exists('/usr/local/lsws/bin/lswsctrl') + configFile = '/usr/local/lsws/bin/lswsctrl' + try: + if os.path.exists(configFile): + conf = public.readFile('/www/server/panel/vhost/openlitespeed/detail/phpmyadmin.conf') + rep = r"/usr/local/lsws/lsphp(\d+)/bin/lsphp" + rtmp = re.search(rep,conf) + if rtmp: + phpversion = rtmp.groups()[0] + conf = public.readFile('/www/server/panel/vhost/openlitespeed/listen/888.conf') + rep = r"address\s+\*\:(\d+)" + rtmp = re.search(rep,conf) + if rtmp: + phpport = rtmp.groups()[0] + if conf.find('AUTH_START') != -1: pauth = True + if conf.find(self.setupPath + '/stop') == -1: pstatus = True + except: + pass + + + tmp['type'] = data['webserver'] + tmp['version'] = public.xss_version(public.readFile(self.setupPath + '/'+data['webserver']+'/version.pl')) + tmp['status'] = False + result = public.ExecShell('/etc/init.d/' + serviceName + ' status') + if result[0].find('running') != -1: tmp['status'] = True + data['web'] = tmp + + tmp = {} + vfile = self.setupPath + '/phpmyadmin/version.pl' + tmp['version'] = public.xss_version(public.readFile(vfile)) + if tmp['version']: tmp['version'] = tmp['version'].strip() + tmp['setup'] = os.path.exists(vfile) + tmp['status'] = pstatus + tmp['phpversion'] = phpversion.strip() + tmp['port'] = phpport + tmp['auth'] = pauth + data['phpmyadmin'] = tmp + + tmp = {} + tmp['setup'] = os.path.exists('/etc/init.d/tomcat') + tmp['status'] = tmp['setup'] + #if public.ExecShell('ps -aux|grep tomcat|grep -v grep')[0] == "": tmp['status'] = False + tmp['version'] = public.xss_version(public.readFile(self.setupPath + '/tomcat/version.pl')) + data['tomcat'] = tmp + + tmp = {} + tmp['setup'] = os.path.exists(self.setupPath +'/mysql/bin/mysql') + tmp['version'] = public.xss_version(public.readFile(self.setupPath + '/mysql/version.pl')) + tmp['status'] = os.path.exists('/tmp/mysql.sock') + data['mysql'] = tmp + + tmp = {} + tmp['setup'] = os.path.exists(self.setupPath +'/redis/runtest') + tmp['status'] = os.path.exists('/var/run/redis_6379.pid') + data['redis'] = tmp + + tmp = {} + tmp['setup'] = os.path.exists('/usr/local/memcached/bin/memcached') + tmp['status'] = os.path.exists('/var/run/memcached.pid') + data['memcached'] = tmp + + tmp = {} + tmp['setup'] = os.path.exists(self.setupPath +'/pure-ftpd/bin/pure-pw') + tmp['version'] = public.xss_version(public.readFile(self.setupPath + '/pure-ftpd/version.pl')) + tmp['status'] = os.path.exists('/var/run/pure-ftpd.pid') + data['pure-ftpd'] = tmp + data['panel'] = self.GetPanelInfo() + data['systemdate'] = public.ExecShell('date +"%Y-%m-%d %H:%M:%S %Z %z"')[0].strip(); + data['show_workorder'] = not os.path.exists('data/not_workorder.pl') + return data + + def GetPanelInfo(self,get=None): + #取面板配置 + address = public.GetLocalIp() + try: + port = public.GetHost(True) + except: + port = '7800'; + domain = '' + if os.path.exists('data/domain.conf'): + domain = public.readFile('data/domain.conf'); + + autoUpdate = '' + if os.path.exists('data/autoUpdate.pl'): autoUpdate = 'checked'; + limitip = '' + if os.path.exists('data/limitip.conf'): limitip = public.readFile('data/limitip.conf'); + admin_path = '/' + if os.path.exists('data/admin_path.pl'): admin_path = public.readFile('data/admin_path.pl').strip() + + templates = [] + #for template in os.listdir('BTPanel/templates/'): + # if os.path.isdir('templates/' + template): templates.append(template); + template = public.GetConfigValue('template') + + check502 = ''; + if os.path.exists('data/502Task.pl'): check502 = 'checked'; + return {'port':port,'address':address,'domain':domain,'auto':autoUpdate,'502':check502,'limitip':limitip,'templates':templates,'template':template,'admin_path':admin_path} + + def GetPHPConfig(self,version): + #取PHP配置 + file = self.setupPath + "/php/"+version+"/etc/php.ini" + phpini = public.readFile(file) + file = self.setupPath + "/php/"+version+"/etc/php-fpm.conf" + phpfpm = public.readFile(file) + data = {} + try: + rep = r"upload_max_filesize\s*=\s*([0-9]+)M" + tmp = re.search(rep,phpini).groups() + data['max'] = tmp[0] + except: + data['max'] = '50' + try: + rep = "request_terminate_timeout\\s*=\\s*([0-9]+)\n" + tmp = re.search(rep,phpfpm).groups() + data['maxTime'] = tmp[0] + except: + data['maxTime'] = 0 + + try: + rep = r"\n;*\s*cgi\.fix_pathinfo\s*=\s*([0-9]+)\s*\n" + tmp = re.search(rep,phpini).groups() + + if tmp[0] == '1': + data['pathinfo'] = True + else: + data['pathinfo'] = False + except: + data['pathinfo'] = False + + return data + + + def GetSystemTotal(self,get,interval = 1): + #取系统统计信息 + data = self.GetMemInfo()['message'] + cpu = self.GetCpuInfo(interval) + data['cpuNum'] = cpu[1] + data['cpuRealUsed'] = cpu[0] + data['time'] = self.GetBootTime() + data['system'] = self.GetSystemVersion() + data['isuser'] = public.M('users').where('username=?',('admin',)).count() + try: + data['isport'] = public.GetHost(True) == '8888' + except:data['isport'] = False + + data['version'] = session['version'] + return public.return_message(0,0,data) + + def GetLoadAverage(self,get): + try: + c = os.getloadavg() + except: + c = [0,0,0] + data = {} + data['one'] = float(c[0]) + data['five'] = float(c[1]) + data['fifteen'] = float(c[2]) + data['max'] = psutil.cpu_count() * 2 + data['limit'] = data['max'] + data['safe'] = data['max'] * 0.75 + return data + + def GetAllInfo(self,get): + data = {} + data['load_average'] = self.GetLoadAverage(get) + data['title'] = self.GetTitle() + data['network'] = self.GetNetWorkApi(get) + data['cpu'] = self.GetCpuInfo(1) + data['time'] = self.GetBootTime() + data['system'] = self.GetSystemVersion() + data['mem'] = self.GetMemInfo()['message'] + data['version'] = session['version'] + return data + + def GetTitle(self): + return public.xss_version(public.GetConfigValue('title')) + + def GetSystemVersion(self): + #取操作系统版本 + key = 'sys_version' + version = cache.get(key) + if version: return version + version = public.get_os_version() + cache.set(key,version,600) + return version + + def GetBootTime(self): + #取系统启动时间 + key = 'sys_time' + sys_time = cache.get(key) + if sys_time: return sys_time + import public,math + conf = public.readFile('/proc/uptime').split() + tStr = float(conf[0]) + min = tStr / 60 + hours = min / 60 + days = math.floor(hours / 24) + hours = math.floor(hours - (days * 24)) + min = math.floor(min - (days * 60 * 24) - (hours * 60)) + sys_time = "{} Day(s)".format(int(days)) + cache.set(key,sys_time,1800) + return sys_time + #return public.getMsg('SYS_BOOT_TIME',(str(int(days)),str(int(hours)),str(int(min)))) + + def GetCpuInfo(self,interval = 1): + #取CPU信息 + cpuCount = psutil.cpu_count() + cpuNum = psutil.cpu_count(logical=False) + c_tmp = public.readFile('/proc/cpuinfo') + d_tmp = re.findall("physical id.+",c_tmp) + cpuW = len(set(d_tmp)) + import threading + p = threading.Thread(target=self.get_cpu_percent_thead,args=(interval,)) + # p.setDaemon(True) + p.start() + + used = cache.get('cpu_used_all') + if not used: used = self.get_cpu_percent_thead(interval) + + used_all = psutil.cpu_percent(percpu=True) + cpu_name = public.getCpuType() + " * {}".format(cpuW) + + return used,cpuCount,used_all,cpu_name,cpuNum,cpuW + + def get_cpu_percent_thead(self,interval): + used = psutil.cpu_percent(interval) + cache.set('cpu_used_all',used,10) + return used + + + def get_cpu_percent(self): + percent = 0.00 + old_cpu_time = cache.get('old_cpu_time') + old_process_time = cache.get('old_process_time') + if not old_cpu_time: + old_cpu_time = self.get_cpu_time() + old_process_time = self.get_process_cpu_time() + time.sleep(1) + new_cpu_time = self.get_cpu_time() + new_process_time = self.get_process_cpu_time() + try: + percent = round(100.00 * ((new_process_time - old_process_time) / (new_cpu_time - old_cpu_time)),2) + except: percent = 0.00 + cache.set('old_cpu_time',new_cpu_time) + cache.set('old_process_time',new_process_time) + if percent > 100: percent = 100 + if percent > 0: return percent + return 0.00 + + def get_process_cpu_time(self): + pids = psutil.pids() + cpu_time = 0.00 + for pid in pids: + try: + cpu_times = psutil.Process(pid).cpu_times() + for s in cpu_times: cpu_time += s + except:continue + return cpu_time + + def get_cpu_time(self): + cpu_time = 0.00 + cpu_times = psutil.cpu_times() + for s in cpu_times: cpu_time += s + return cpu_time + + def GetMemInfo(self,get=None): + #取内存信息 + skey = 'memInfo' + memInfo = cache.get(skey) + if memInfo: return public.return_message(0,0,memInfo) + mem = psutil.virtual_memory() + memInfo = {'memTotal':int(mem.total/1024/1024),'memFree':int(mem.free/1024/1024),'memBuffers':int(mem.buffers/1024/1024),'memCached':int(mem.cached/1024/1024)} + memInfo['memRealUsed'] = memInfo['memTotal'] - memInfo['memFree'] - memInfo['memBuffers'] - memInfo['memCached'] + cache.set(skey,memInfo,60) + return public.return_message(0,0,memInfo) + + def GetDiskInfo(self,get=None): + return self.GetDiskInfo2() + #取磁盘分区信息 + diskIo = psutil.disk_partitions() + diskInfo = [] + cuts = ['/mnt/cdrom','/boot','/boot/efi','/dev','/dev/shm','/run/lock','/run','/run/shm','/run/user'] + for disk in diskIo: + if not cuts: continue + tmp = {} + tmp['path'] = disk[1] + tmp['size'] = psutil.disk_usage(disk[1]) + diskInfo.append(tmp) + return diskInfo + + def GetDiskInfo2(self, human=True): + + #取磁盘分区信息 + key = f'sys_disk_{human}' + diskInfo = cache.get(key) + if diskInfo: return diskInfo + if human: + temp = public.ExecShell("df -hT -P|grep '/'|grep -v tmpfs|grep -v 'snap/core'|grep -v udev")[0] + else: + temp = public.ExecShell("df -T -P|grep '/'|grep -v tmpfs|grep -v 'snap/core'|grep -v udev")[0] + tempInodes = public.ExecShell("df -i -P|grep '/'|grep -v tmpfs|grep -v 'snap/core'|grep -v udev")[0] + temp1 = temp.split('\n') + tempInodes1 = tempInodes.split('\n') + diskInfo = [] + n = 0 + cuts = ['/mnt/cdrom','/boot','/boot/efi','/dev','/dev/shm','/run/lock','/run','/run/shm','/run/user'] + for tmp in temp1: + n += 1 + try: + inodes = tempInodes1[n-1].split() + disk = re.findall(r"^(.+)\s+([\w\.]+)\s+([\w\.]+)\s+([\w\.]+)\s+([\w\.]+)\s+([\d%]{2,4})\s+(/.{0,100})$",tmp.strip().replace(',','.')) + if disk: disk = disk[0] + if len(disk) < 6: continue + # if disk[2].find('M') != -1: continue + if disk[2].find('K') != -1: continue + if len(disk[6].split('/')) > 10: continue + if disk[6] in cuts: continue + if str(disk[6]).startswith("/snap"): continue + if disk[6].find('docker') != -1: continue + if disk[1].strip() in ['tmpfs']: continue + arr = {} + arr['filesystem'] = disk[0].strip() + arr['type'] = disk[1].strip() + arr['path'] = disk[6].replace('/usr/local/lighthouse/softwares/btpanel','/www') + tmp1 = [disk[2],disk[3],disk[4],disk[5]] + arr['size'] = tmp1 + arr['inodes'] = [inodes[1],inodes[2],inodes[3],inodes[4]] + diskInfo.append(arr) + except Exception as ex: + public.write_log_gettext('Get Info',str(ex)) + continue + cache.set(key,diskInfo,10) + return diskInfo + + + # 获取磁盘IO开销数据 + def get_disk_iostat(self): + iokey = 'iostat' + diskio = cache.get(iokey) + mtime = int(time.time()) + if not diskio: + diskio = {} + diskio['info'] = None + diskio['time'] = mtime + diskio_1 = diskio['info'] + stime = mtime - diskio['time'] + if not stime: stime = 1 + diskInfo = {} + diskInfo['ALL'] = {} + diskInfo['ALL']['read_count'] = 0 + diskInfo['ALL']['write_count'] = 0 + diskInfo['ALL']['read_bytes'] = 0 + diskInfo['ALL']['write_bytes'] = 0 + diskInfo['ALL']['read_time'] = 0 + diskInfo['ALL']['write_time'] = 0 + diskInfo['ALL']['read_merged_count'] = 0 + diskInfo['ALL']['write_merged_count'] = 0 + try: + if os.path.exists('/proc/diskstats'): + diskio_2 = psutil.disk_io_counters(perdisk=True) + if not diskio_1: + diskio_1 = diskio_2 + for disk_name in diskio_2.keys(): + diskInfo[disk_name] = {} + diskInfo[disk_name]['read_count'] = int((diskio_2[disk_name].read_count - diskio_1[disk_name].read_count) / stime) + diskInfo[disk_name]['write_count'] = int((diskio_2[disk_name].write_count - diskio_1[disk_name].write_count) / stime) + diskInfo[disk_name]['read_bytes'] = int((diskio_2[disk_name].read_bytes - diskio_1[disk_name].read_bytes) / stime) + diskInfo[disk_name]['write_bytes'] = int((diskio_2[disk_name].write_bytes - diskio_1[disk_name].write_bytes) / stime) + diskInfo[disk_name]['read_time'] = int((diskio_2[disk_name].read_time - diskio_1[disk_name].read_time) / stime) + diskInfo[disk_name]['write_time'] = int((diskio_2[disk_name].write_time - diskio_1[disk_name].write_time) / stime) + diskInfo[disk_name]['read_merged_count'] = int((diskio_2[disk_name].read_merged_count - diskio_1[disk_name].read_merged_count) / stime) + diskInfo[disk_name]['write_merged_count'] = int((diskio_2[disk_name].write_merged_count - diskio_1[disk_name].write_merged_count) / stime) + + diskInfo['ALL']['read_count'] += diskInfo[disk_name]['read_count'] + diskInfo['ALL']['write_count'] += diskInfo[disk_name]['write_count'] + diskInfo['ALL']['read_bytes'] += diskInfo[disk_name]['read_bytes'] + diskInfo['ALL']['write_bytes'] += diskInfo[disk_name]['write_bytes'] + if diskInfo['ALL']['read_time'] < diskInfo[disk_name]['read_time']: + diskInfo['ALL']['read_time'] = diskInfo[disk_name]['read_time'] + if diskInfo['ALL']['write_time'] < diskInfo[disk_name]['write_time']: + diskInfo['ALL']['write_time'] = diskInfo[disk_name]['write_time'] + diskInfo['ALL']['read_merged_count'] += diskInfo[disk_name]['read_merged_count'] + diskInfo['ALL']['write_merged_count'] += diskInfo[disk_name]['write_merged_count'] + + cache.set(iokey,{'info':diskio_2,'time':mtime}) + except: + return diskInfo + return diskInfo + + + #清理系统垃圾 + def ClearSystem(self,get): + count = total = 0 + tmp_total,tmp_count = self.ClearMail() + count += tmp_count + total += tmp_total + tmp_total,tmp_count = self.ClearOther() + count += tmp_count + total += tmp_total + return count,total + + #清理邮件日志 + def ClearMail(self): + rpath = '/var/spool' + total = count = 0 + import shutil + con = ['cron','anacron','mail'] + for d in os.listdir(rpath): + if d in con: continue + dpath = rpath + '/' + d + time.sleep(0.2) + num = size = 0 + for n in os.listdir(dpath): + filename = dpath + '/' + n + fsize = os.path.getsize(filename) + size += fsize + if os.path.isdir(filename): + shutil.rmtree(filename) + else: + os.remove(filename) + print('\t\033[1;32m[OK]\033[0m') + num += 1 + total += size + count += num + return total,count + + #清理其它 + def ClearOther(self): + clearPath = [ + {'path':'/www/server/panel','find':'testDisk_'}, + {'path':'/www/wwwlogs','find':'log'}, + {'path':'/tmp','find':'panelBoot.pl'}, + {'path':'/www/server/panel/install','find':'.rpm'} + ] + + total = count = 0 + for c in clearPath: + for d in os.listdir(c['path']): + if d.find(c['find']) == -1: continue + filename = c['path'] + '/' + d + if os.path.isdir(filename): continue + fsize = os.path.getsize(filename) + total += fsize + os.remove(filename) + count += 1 + public.serviceReload() + filename = '/www/server/nginx/off' + if os.path.exists(filename): os.remove(filename) + public.ExecShell('echo > /tmp/panelBoot.pl') + return total,count + + def GetNetWork(self,get=None): + cache_timeout = 86400 + otime = cache.get("otime") + ntime = time.time() + networkInfo = {} + networkInfo['network'] = {} + networkInfo['upTotal'] = 0 + networkInfo['downTotal'] = 0 + networkInfo['up'] = 0 + networkInfo['down'] = 0 + networkInfo['downPackets'] = 0 + networkInfo['upPackets'] = 0 + networkIo_list = psutil.net_io_counters(pernic = True) + for net_key in networkIo_list.keys(): + networkIo = networkIo_list[net_key][:4] + up_key = "{}_up".format(net_key) + down_key = "{}_down".format(net_key) + otime_key = "otime" + + if not otime: + otime = time.time() + + cache.set(up_key,networkIo[0],cache_timeout) + cache.set(down_key,networkIo[1],cache_timeout) + cache.set(otime_key,otime ,cache_timeout) + + networkInfo['network'][net_key] = {} + up = cache.get(up_key) + down = cache.get(down_key) + if not up: + up = networkIo[0] + if not down: + down = networkIo[1] + networkInfo['network'][net_key]['upTotal'] = networkIo[0] + networkInfo['network'][net_key]['downTotal'] = networkIo[1] + networkInfo['network'][net_key]['up'] = round(float(networkIo[0] - up) / 1024 / (ntime - otime),2) + networkInfo['network'][net_key]['down'] = round(float(networkIo[1] - down) / 1024 / (ntime - otime),2) + networkInfo['network'][net_key]['downPackets'] =networkIo[3] + networkInfo['network'][net_key]['upPackets'] =networkIo[2] + + networkInfo['upTotal'] += networkInfo['network'][net_key]['upTotal'] + networkInfo['downTotal'] += networkInfo['network'][net_key]['downTotal'] + networkInfo['up'] += networkInfo['network'][net_key]['up'] + networkInfo['down'] += networkInfo['network'][net_key]['down'] + networkInfo['downPackets'] += networkInfo['network'][net_key]['downPackets'] + networkInfo['upPackets'] += networkInfo['network'][net_key]['upPackets'] + + cache.set(up_key,networkIo[0],cache_timeout) + cache.set(down_key,networkIo[1],cache_timeout) + cache.set(otime_key, time.time(),cache_timeout) + + if get != False: + networkInfo['cpu'] = self.GetCpuInfo(1) + networkInfo['cpu_times'] = self.get_cpu_times() + networkInfo['load'] = self.GetLoadAverage(get) + networkInfo['mem'] = self.GetMemInfo(get)['message'] + networkInfo['version'] = session['version'] + disk_list = [] + for disk in self.GetDiskInfo2(False): + disk['size'].append(int(disk['size'][0]) - (int(disk['size'][1]) + int(disk['size'][2]))) # 计算系统占用 + disk['size'] = list(map(lambda num: f"{round(int(num) / 1048576, 2)}G" if str(num).isdigit() and str(num).find('G') == -1 else num, disk['size'])) + disk['inodes'] = list(map(lambda num: f"{round(int(num) / 1048576, 2)}G" if str(num).isdigit() and str(num).find('G') == -1 else num, disk['inodes'])) + disk_list.append(disk) + networkInfo['disk'] = disk_list + + networkInfo['title'] = self.GetTitle() + networkInfo['time'] = self.GetBootTime() + networkInfo['site_total'] = public.M('sites').count() + networkInfo['ftp_total'] = public.M('ftps').count() + networkInfo['database_total'] = public.M('databases').count() + networkInfo['system'] = self.GetSystemVersion() + networkInfo['installed'] = self.CheckInstalled() + import panel_ssl_v2 as panelSSL + user_info = panelSSL.panelSSL().GetUserInfo(None) + networkInfo['user_info'] = user_info['message'] + networkInfo['user_info']['status'] = user_info['status'] + networkInfo['up'] = round(float(networkInfo['up']),2) + networkInfo['down'] = round(float(networkInfo['down']),2) + networkInfo['iostat'] = self.get_disk_iostat() + + return public.return_message(0,0,networkInfo) + + + def get_cpu_times(self): + skey = 'cpu_times' + data = cache.get(skey) + if data:return data + try: + data = {} + cpu_times_p = psutil.cpu_times_percent() + data['user'] = cpu_times_p.user + data['nice'] = cpu_times_p.nice + data['system'] = cpu_times_p.system + data['idle'] = cpu_times_p.idle + data['iowait'] = cpu_times_p.iowait + data['irq'] = cpu_times_p.irq + data['softirq'] = cpu_times_p.softirq + data['steal'] = cpu_times_p.steal + data['guest'] = cpu_times_p.guest + data['guest_nice'] = cpu_times_p.guest_nice + data['total_processes'] = 0 + data['active_processes'] = 0 + for pid in psutil.pids(): + try: + p = psutil.Process(pid) + if p.status() == 'running': + data['active_processes'] += 1 + except: + continue + data['total_processes'] += 1 + + cache.set(skey,data,60) + except: return None + return data + + + + + def GetNetWorkApi(self,get=None): + return self.GetNetWork() + + #检查是否安装任何 + def CheckInstalled(self): + checks = ['nginx','apache','php','pure-ftpd','mysql'] + import os + for name in checks: + filename = public.GetConfigValue('root_path') + "/server/" + name + if os.path.exists(filename): return True + return False + + def GetNetWorkOld(self): + #取网络流量信息 + import time; + pnet = public.readFile('/proc/net/dev') + rep = r'([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)' + pnetall = re.findall(rep,pnet) + networkInfo = {} + networkInfo['upTotal'] = networkInfo['downTotal'] = networkInfo['up'] = networkInfo['down'] = networkInfo['downPackets'] = networkInfo['upPackets'] = 0 + + + for pnetInfo in pnetall: + if pnetInfo[0] == 'io': continue + networkInfo['downTotal'] += int(pnetInfo[1]) + networkInfo['downPackets'] += int(pnetInfo[2]) + networkInfo['upTotal'] += int(pnetInfo[9]) + networkInfo['upPackets'] += int(pnetInfo[10]) + + cache_timeout = 86400 + otime = cache.get("otime") + if not otime: + otime = time.time() + cache.set('up',networkInfo['upTotal'],cache_timeout) + cache.set('down',networkInfo['downTotal'],cache_timeout) + cache.set('otime',otime ,cache_timeout) + + ntime = time.time() + tmpDown = networkInfo['downTotal'] - cache.get("down") + tmpUp = networkInfo['upTotal'] - cache.get("up") + networkInfo['down'] = str(round(float(tmpDown) / 1024 / (ntime - otime),2)) + networkInfo['up'] = str(round(float(tmpUp) / 1024 / (ntime - otime),2)) + if networkInfo['down'] < 0: networkInfo['down'] = 0 + if networkInfo['up'] < 0: networkInfo['up'] = 0 + + otime = time.time() + cache.set('up',networkInfo['upTotal'],cache_timeout) + cache.set('down',networkInfo['downTotal'],cache_timeout) + cache.set('otime',ntime ,cache_timeout) + + networkInfo['cpu'] = self.GetCpuInfo() + return networkInfo + + + #取IO读写信息 + def get_io_info(self,get = None): + io_disk = psutil.disk_io_counters() + ioTotal = {} + ioTotal['write'] = self.get_io_write(io_disk.write_bytes) + ioTotal['read'] = self.get_io_read(io_disk.read_bytes) + return ioTotal + + #取IO写 + def get_io_write(self,io_write): + disk_io_write = 0 + old_io_write = cache.get('io_write') + if not old_io_write: + cache.set('io_write',io_write) + return disk_io_write + + old_io_time = cache.get('io_time') + new_io_time = time.time() + if not old_io_time: old_io_time = new_io_time + io_end = (io_write - old_io_write) + time_end = (time.time() - old_io_time) + if io_end > 0: + if time_end < 1: time_end = 1 + disk_io_write = io_end / time_end + cache.set('io_write',io_write) + cache.set('io_time',new_io_time) + if disk_io_write > 0: return int(disk_io_write) + return 0 + + #取IO读 + def get_io_read(self,io_read): + disk_io_read = 0 + old_io_read = cache.get('io_read') + if not old_io_read: + cache.set('io_read',io_read) + return disk_io_read + old_io_time = cache.get('io_time') + new_io_time = time.time() + if not old_io_time: old_io_time = new_io_time + io_end = (io_read - old_io_read) + time_end = (time.time() - old_io_time) + if io_end > 0: + if time_end < 1: time_end = 1 + disk_io_read = io_end / time_end + cache.set('io_read',io_read) + if disk_io_read > 0: return int(disk_io_read) + return 0 + + #检查并修复MySQL目录权限 + def __check_mysql_path(self): + try: + #获取datadir路径 + mypath = '/etc/my.cnf' + if not os.path.exists(mypath): return False + public.set_mode(mypath,644) + mycnf = public.readFile(mypath) + tmp = re.findall(r'datadir\s*=\s*(.+)',mycnf) + if not tmp: return False + datadir = tmp[0] + + #可以被启动的权限 + accs = ['755','777'] + + #处理data目录权限 + mode_info = public.get_mode_and_user(datadir) + if not mode_info['mode'] in accs or mode_info['user'] != 'mysql': + public.ExecShell('chmod 755 ' + datadir) + public.ExecShell('chown -R mysql:mysql ' + datadir) + + #递归处理父目录权限 + datadir = os.path.dirname(datadir) + while datadir != '/': + if datadir == '/': break + mode_info = public.get_mode_and_user(datadir) + if not mode_info['mode'] in accs: + public.ExecShell('chmod 755 ' + datadir) + datadir = os.path.dirname(datadir) + except: pass + + def ServiceAdmin(self,get=None): + # 校验参数 + try: + get.validate([ + Param('name').String(), + Param('type').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + #服务管理 + if get.name == 'mysqld': + public.CheckMyCnf() + self.__check_mysql_path() + if get.name.find('webserver') != -1: + get.name = public.get_webserver() + + if get.name == 'phpmyadmin': + import ajax_v2 + get.status = 'True' + ajax_v2.ajax().setPHPMyAdmin(get) + return public.return_message(0,0,'Executed successfully!') + + if get.name == 'openlitespeed': + if get.type == 'stop': + public.ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl stop') + elif get.type == 'start': + public.ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl start') + else: + public.ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart') + return public.return_message(0,0,'Executed successfully!') + + #检查httpd配置文件 + if get.name == 'apache' or get.name == 'httpd': + get.name = 'httpd' + if not os.path.exists(self.setupPath+'/apache/bin/apachectl'): return public.return_message(-1,0,'Execution failed, check if Apache installed') + vhostPath = self.setupPath + '/panel/vhost/apache' + if not os.path.exists(vhostPath): + public.ExecShell('mkdir ' + vhostPath) + public.ExecShell('/etc/init.d/httpd start') + + if get.type == 'start': + public.ExecShell('/etc/init.d/httpd stop') + self.kill_port() + + result = public.ExecShell('ulimit -n 8192 ; ' + self.setupPath+'/apache/bin/apachectl -t') + if result[1].find('Syntax OK') == -1: + public.write_log_gettext("Software manager",'Execution failed: {}', (str(result),)) + return public.return_message(-1,0,"Apache rule configuration error:
                                    {}",(result[1].replace("\n",'
                                    '),)) + + if get.type == 'restart': + public.ExecShell('pkill -9 httpd') + public.ExecShell('/etc/init.d/httpd start') + time.sleep(0.5) + + #检查nginx配置文件 + elif get.name == 'nginx': + vhostPath = self.setupPath + '/panel/vhost/rewrite' + if not os.path.exists(vhostPath): public.ExecShell('mkdir ' + vhostPath) + if not os.path.exists("/dev/shm/nginx-cache/wp"): + public.ExecShell('mkdir -p /dev/shm/nginx-cache/wp && chown -R www.www /dev/shm/nginx-cache') + vhostPath = self.setupPath + '/panel/vhost/nginx' + if not os.path.exists(vhostPath): + public.ExecShell('mkdir ' + vhostPath) + public.ExecShell('/etc/init.d/nginx start') + + result = public.ExecShell('ulimit -n 8192 ; '+self.setupPath+'/nginx/sbin/nginx -t -c '+self.setupPath+'/nginx/conf/nginx.conf') + if result[1].find('perserver') != -1: + limit = self.setupPath + '/nginx/conf/nginx.conf' + nginxConf = public.readFile(limit) + limitConf = "limit_conn_zone $binary_remote_addr zone=perip:10m;\n\t\tlimit_conn_zone $server_name zone=perserver:10m;" + nginxConf = nginxConf.replace("#limit_conn_zone $binary_remote_addr zone=perip:10m;",limitConf) + public.writeFile(limit,nginxConf) + public.ExecShell('/etc/init.d/nginx start') + return public.return_message(0,0,'Configuration file mismatch caused by reinstalling Nginx fixed') + + if result[1].find('proxy') != -1: + import panelSite + panelSite.panelSite().CheckProxy(get) + public.ExecShell('/etc/init.d/nginx start') + return public.return_message(0,0,'Configuration file mismatch caused by reinstalling Nginx fixed') + + #return result + if result[1].find('successful') == -1: + public.write_log_gettext("Software manager",'Execution failed: {}', (str(result),)) + return public.return_message(-1,0,"Nginx rule configuration error:
                                    {}".format(result[1].replace("\n",'
                                    '),)) + + if get.type == 'start': + self.kill_port() + time.sleep(0.5) + if get.name == 'redis': + redis_init = '/etc/init.d/redis' + if os.path.exists(redis_init): + init_body = public.ReadFile(redis_init) + if init_body.find('pkill -9 redis') == -1: + public.ExecShell("wget -O " + redis_init + " " + public.get_url() + '/init/redis.init') + public.ExecShell("chmod +x " + redis_init) + + #执行 + execStr = "/etc/init.d/"+get.name+" "+get.type + if execStr == '/etc/init.d/pure-ftpd reload': execStr = self.setupPath+'/pure-ftpd/bin/pure-pw mkdb '+self.setupPath+'/pure-ftpd/etc/pureftpd.pdb' + if execStr == '/etc/init.d/pure-ftpd start': public.ExecShell('pkill -9 pure-ftpd') + if execStr == '/etc/init.d/tomcat reload': execStr = '/etc/init.d/tomcat stop && /etc/init.d/tomcat start' + if execStr == '/etc/init.d/tomcat restart': execStr = '/etc/init.d/tomcat stop && /etc/init.d/tomcat start' + + if get.name != 'mysqld': + result = public.ExecShell(execStr) + else: + public.ExecShell(execStr) + result = [] + result.append('') + result.append('') + + if result[1].find('nginx.pid') != -1: + public.ExecShell('pkill -9 nginx && sleep 1') + public.ExecShell('/etc/init.d/nginx start') + if get.type != 'test': + public.write_log_gettext("Software manager", 'Executed successfully [{}]!',(execStr,)) + + if get.type != 'stop': + n = 0 + num = 5 + while not self.check_service_status(get.name): + time.sleep(0.5) + n += 1 + if n > num: break + + if not self.check_service_status(get.name): + if len(result[1]) > 1 and get.name != 'pure-ftpd' and get.name != 'redis': + return public.return_message(-1,0, '

                                    failed to activate:

                                    ' + result[1].replace('\n','
                                    ')) + else: + return public.return_message(-1,0,'{} service failed to start'.format(get.name)) + else: + if self.check_service_status(get.name): return public.return_message(-1,0, 'Service stop failed!') + return public.return_message(0,0,'Executed successfully!') + + def check_service_status(self,name): + ''' + @name 检查服务管理状态 + @author hwliang + @param name 服务名称 + @return bool + ''' + if name in ['mysqld','mariadbd']: + return public.is_mysql_process_exists() + elif name == 'redis': + return public.is_redis_process_exists() + elif name == 'pure-ftpd': + return public.is_pure_ftpd_process_exists() + elif name.find('php-fpm') != -1: + return public.is_php_fpm_process_exists(name) + elif name == 'nginx': + return public.is_nginx_process_exists() + elif name in ['httpd','apache']: + return public.is_httpd_process_exists() + elif name == 'memcached': + return public.is_memcached_process_exists() + elif name == 'mongodb': + return public.is_mongodb_process_exists() + else: + return True + + + + + def RestartServer(self,get): + if not public.IsRestart(): return public.return_message(-1,0,'Please run the program when all install tasks finished!') + public.ExecShell("sync && init 6 &") + return public.return_message(0,0,'Command sent successfully!') + + def kill_port(self): + public.ExecShell('pkill -9 httpd') + public.ExecShell('pkill -9 nginx') + public.ExecShell("kill -9 $(lsof -i :80|grep LISTEN|awk '{print $2}')") + return True + + #释放内存 + def ReMemory(self,get): + public.ExecShell('sync') + scriptFile = 'script/rememory.sh' + if not os.path.exists(scriptFile): + public.downloadFile(public.GetConfigValue('home') + '/script/rememory.sh',scriptFile) + public.ExecShell("/bin/bash " + self.setupPath + '/panel/' + scriptFile) + return self.GetMemInfo() + + #重启面板 + def ReWeb(self,get): + # 校验参数 + try: + get.validate([ + Param('toUpdate').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + public.ExecShell("/etc/init.d/bt start") + public.writeFile('data/restart.pl','True') + # 重启面板 默认开启系统监控 + public.writeFile('data/control.conf', '30') + return public.return_message(0,0,'Panel restarted') + + + #修复面板 + def RepPanel(self,get): + # 校验参数 + try: + get.validate([ + Param('toUpdate').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + public.writeFile('data/js_random.pl','1') + public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update_7.x_en.sh && bash update.sh") + self.ReWeb(None) + return public.return_message(0,0,True) + + #升级到专业版 + def UpdatePro(self,get): + public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update_7.x_en.sh && bash update.sh") + self.ReWeb(None) + return True diff --git a/class_v2/user_login_v2.py b/class_v2/user_login_v2.py new file mode 100644 index 00000000..4b1e6b32 --- /dev/null +++ b/class_v2/user_login_v2.py @@ -0,0 +1,422 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +import public,os,sys,db,time,json,re +from BTPanel import session,cache,json_header +from flask import request,redirect,g + +class userlogin: + limit_expire_time = 0 + def request_post(self,post): + if not hasattr(post, 'username') or not hasattr(post, 'password'): + return public.returnJson(False,'User name or password cannot be empty!'),json_header + self.error_num(False) + if self.limit_address('?') < 1: return public.returnJson(False,'You have failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header + post.username = post.username.strip() + format_error = 'Parameter format error' + + # 核验用户名密码格式 + post.username = public.rsa_decrypt(post.username) + + if len(post.username) != 32: + return public.returnMsg(False,format_error+"1"),json_header + post.password = public.rsa_decrypt(post.password) + if len(post.password) != 32: + return public.returnMsg(False,format_error+"2"),json_header + + if not re.match(r"^\w+$",post.username): return public.return_msg_gettext(False,'Disk inode has been exhausted, the panel has attempted to release the inode. Please try again ...'),json_header + if not re.match(r"^\w+$",post.password): return public.return_msg_gettext(False,'Disk inode has been exhausted, the panel has attempted to release the inode. Please try again ...'),json_header + last_login_token = session.get('last_login_token',None) + if not last_login_token: + public.WriteLog('TYPE_LOGIN','LOGIN_ERR_CODE',('****','****',public.GetClientIp())) + return public.returnJson(False,"Verification failed, please refresh the page and log in again!"),json_header + + public.chdck_salt() + sql = db.Sql() + userInfo = None + user_plugin_file = '{}/users_main.py'.format(public.get_plugin_path('users')) + if os.path.exists(user_plugin_file): + user_list = sql.table('users').field('id,username,password,salt').select() + for u_info in user_list: + if public.md5(public.md5(u_info['username'] + last_login_token)) == post.username: + userInfo = u_info + else: + userInfo = sql.table('users').where('id=?',1).field('id,username,password,salt').find() + + + if 'code' in session: + if session['code'] and not 'is_verify_password' in session: + if not hasattr(post, 'code'): return public.returnJson(False,'Verification code can not be empty!'),json_header + if not re.match(r"^\w+$",post.code): return public.returnJson(False,'Verification code is incorrect, please try again!'),json_header + if not public.checkCode(post.code): + public.write_log_gettext('Login','Verification code is incorrect, Username:{}, Verification Code:{}, Login IP:{}',('****','****',public.GetClientIp())) + return public.returnJson(False,'Verification code is incorrect, please try again!'),json_header + try: + if not userInfo: + public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp())) + num = self.limit_address('+') + if not num: return public.returnJson(False,'You have failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header + return public.returnJson(False,'wrong user name or password,please refresh the page and try again,You can retry {} more times'.format(num)),json_header + + if userInfo and not userInfo['salt']: + public.chdck_salt() + userInfo = sql.table('users').where('id=?',(userInfo['id'],)).field('id,username,password,salt').find() + + password = public.md5(post.password.strip() + userInfo['salt']) + s_username = public.md5(public.md5(userInfo['username'] + last_login_token)) + if s_username != post.username or userInfo['password'] != password: + public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) + num = self.limit_address('+') + if not num: return public.returnJson(False,'You failed to log in many times, please try again in {} seconds!'.format(int(self.limit_expire_time - time.time()))),json_header + return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header + _key_file = "/www/server/panel/data/two_step_auth.txt" + + # 密码过期检测 + if sys.path[0] != 'class/': sys.path.insert(0,'class/') + if not public.password_expire_check(): + session['password_expire'] = True + + #登陆告警 + #public.run_thread(public.login_send_body,("账号密码",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) + # public.login_send_body("账号密码",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT'))) + if hasattr(post,'vcode'): + if not re.match(r"^\d+$",post.vcode): return public.returnJson(False,'Incorrect format of verification code'),json_header + if self.limit_address('?',v="vcode") < 1: return public.returnJson(False,'You have failed verification many times, forbidden for 10 minutes'),json_header + import pyotp + secret_key = public.readFile(_key_file) + if not secret_key: + return public.returnJson(False, "Did not find the key, please close Google verification on the command line and trun on again"),json_header + t = pyotp.TOTP(secret_key) + result = t.verify(post.vcode) + if not result: + if public.sync_date(): result = t.verify(post.vcode) + if not result: + num = self.limit_address('++',v="vcode") + return public.returnJson(False, 'Invalid Verification code. You have [{}] times left to try!'.format(num)), json_header + now = int(time.time()) + # public.run_thread(public.login_send_body,("account",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) + public.writeFile("/www/server/panel/data/dont_vcode_ip.txt",json.dumps({"client_ip":public.GetClientIp(),"add_time":now})) + self.limit_address('--',v="vcode") + self.set_cdn_host(post) + return self._set_login_session(userInfo) + + acc_client_ip = self.check_two_step_auth() + + if not os.path.exists(_key_file) or acc_client_ip: + public.run_thread(public.login_send_body,("account",userInfo['username'],public.GetClientIp(),str(int(request.environ.get('REMOTE_PORT'))))) + self.set_cdn_host(post) + return self._set_login_session(userInfo) + self.limit_address('-') + session['is_verify_password'] = True + return "1" + except Exception as ex: + stringEx = str(ex) + if stringEx.find('unsupported') != -1 or stringEx.find('-1') != -1: + public.ExecShell("rm -f /tmp/sess_*") + public.ExecShell("rm -f /www/wwwlogs/*log") + public.ServiceReload() + return public.returnJson(False,'USER_INODE_ERR'),json_header + public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) + num = self.limit_address('+') + if not num: return public.returnJson(False,'You have failed to log in many times, please wait {} seconds and try again!'.format(int(self.limit_expire_time - time.time()))),json_header + return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header + + def request_tmp(self,get): + try: + if not hasattr(get,'tmp_token'): return public.returnJson(False,'Parameter ERROR!'),json_header + if len(get.tmp_token) == 48: + return self.request_temp(get) + if len(get.tmp_token) != 64: return public.returnJson(False,'Parameter ERROR!'),json_header + if not re.match(r"^\w+$",get.tmp_token):return public.returnJson(False,'Parameter ERROR!'),json_header + save_path = '/www/server/panel/config/api.json' + data = json.loads(public.ReadFile(save_path)) + if not 'tmp_token' in data or not 'tmp_time' in data: return public.returnJson(False,'Verification failed'),json_header + if (time.time() - data['tmp_time']) > 120: return public.returnJson(False,'Expired Token'),json_header + if get.tmp_token != data['tmp_token']: return public.returnJson(False,'Invalid Token!'),json_header + userInfo = public.M('users').where("id=?",(1,)).field('id,username').find() + session['login'] = True + session['username'] = userInfo['username'] + session['tmp_login'] = True + session['uid'] = userInfo['id'] + ids=public.WriteLog('TYPE_LOGIN','Login success',(userInfo['username'],public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')))) + public.cache_set(public.GetClientIp() + ":" + str(request.environ.get('REMOTE_PORT')), ids) + self.limit_address('-') + cache.delete('panelNum') + cache.delete('dologin') + session['session_timeout'] = time.time() + public.get_session_timeout() + del(data['tmp_token']) + del(data['tmp_time']) + public.writeFile(save_path,json.dumps(data)) + self.set_request_token() + self.login_token() + self.set_cdn_host(get) + return redirect('/') + except: + return public.returnJson(False,'Login failed,' + public.get_error_info()),json_header + + + def request_temp(self,get): + try: + if len(get.get_items().keys()) > 2: return public.get_msg_gettext('Parameter ERROR!') + if not hasattr(get,'tmp_token'): return public.get_msg_gettext('Parameter ERROR!') + if len(get.tmp_token) != 48: return public.get_msg_gettext('Parameter ERROR!') + if not re.match(r"^\w+$",get.tmp_token):return public.get_msg_gettext('Parameter ERROR!') + skey = public.GetClientIp() + '_temp_login' + if not public.get_error_num(skey,10): return public.get_msg_gettext('10 consecutive authentication failures are prohibited for 1 hour') + s_time = int(time.time()) + if public.M('temp_login').where('state=? and expire>?',(0,s_time)).field('id,token,salt,expire').count()==0: + public.set_error_num(skey) + return public.get_msg_gettext('Verification failed') + + data = public.M('temp_login').where('state=? and expire>?',(0,s_time)).field('id,token,salt,expire').find() + if not data: + public.set_error_num(skey) + return public.get_msg_gettext('Verification failed') + if not isinstance(data,dict): + public.set_error_num(skey) + return public.get_msg_gettext('Verification failed') + r_token = public.md5(get.tmp_token + data['salt']) + if r_token != data['token']: + public.set_error_num(skey) + return public.get_msg_gettext('Verification failed') + public.set_error_num(skey,True) + userInfo = public.M('users').where("id=?",(1,)).field('id,username').find() + session['login'] = True + session['username'] = public.get_msg_gettext('TEMPORARY_ID({})',(data['id'],)) + session['tmp_login'] = True + session['tmp_login_id'] = str(data['id']) + session['tmp_login_expire'] = time.time() + 3600 + session['uid'] = data['id'] + sess_path = 'data/session' + if not os.path.exists(sess_path): + os.makedirs(sess_path,384) + public.writeFile(sess_path + '/' + str(data['id']),'') + login_addr = public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')) + ids = public.write_log_gettext('Login','Login succeed, Username: {}, Login IP: {}',(userInfo['username'],login_addr)) + public.cache_set(public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')),ids) + public.M('temp_login').where('id=?',(data['id'],)).update({"login_time":s_time,'state':1,'login_addr':login_addr}) + self.limit_address('-') + cache.delete('panelNum') + cache.delete('dologin') + session['session_timeout'] = time.time() + public.get_session_timeout() + self.set_request_token() + self.login_token() + self.set_cdn_host(get) + public.run_thread(public.login_send_body("Temporary authorization",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT')))) + return redirect('/') + except: + public.print_log(public.get_error_info(),'ERROR') + return public.get_msg_gettext('Login failed') + + + def login_token(self): + import config + config.config().reload_session() + + def request_get(self,get): + ''' + @name 验证登录页面请求权限 + @author hwliang + @return False | Response + ''' + # 获取标题 + if not 'title' in session: session['title'] = public.getMsg('NAME') + + # 验证是否使用限制的域名访问 + domain_check = public.check_domain_panel() + if domain_check: return domain_check + + # 验证是否使用限制的IP地址访问 + ip_check = public.check_ip_panel() + if ip_check: return ip_check + + # 验证是否已经登录 + if 'login' in session: + if session['login'] == True: + return redirect('/') + + # 复位验证码 + if not 'code' in session: + session['code'] = False + + # 记录错误次数 + self.error_num(False) + + #生成request_token + def set_request_token(self): + html_token_key = public.get_csrf_html_token_key() + session[html_token_key] = public.GetRandomString(48) + session[html_token_key.replace("https_","")] = public.GetRandomString(48) + #session['client_hash'] = public.get_client_hash() + + def set_cdn_host(self,get): + try: + if not 'cdn_url' in get: return True + plugin_path = 'plugin/static_cdn' + if not os.path.exists(plugin_path): return True + cdn_url = public.get_cdn_url() + if not cdn_url or cdn_url == get.cdn_url: return True + public.set_cdn_url(get.cdn_url) + except: + return False + + #防暴破 + def error_num(self,s = True): + nKey = 'panelNum' + num = cache.get(nKey) + if not num: + cache.set(nKey,1) + num = 1 + if s: cache.inc(nKey,1) + if num > 6: session['code'] = True + + #IP限制 + def limit_address(self,type,v=""): + import time + clientIp = public.GetClientIp() + numKey = 'limitIpNum_' + v + clientIp + limit = 5 + outTime = 300 + try: + #初始化 + num1 = cache.get(numKey) + if not num1: + cache.set(numKey,0,outTime) + num1 = 0 + + self.limit_expire_time = cache.get_expire_time(numKey) + + #计数 + if type == '+': + cache.inc(numKey,1) + self.error_num() + session['code'] = True + return limit - (num1+1) + + #计数验证器 + if type == '++': + cache.inc(numKey,1) + self.error_num() + session['code'] = False + return limit - (num1+1) + + #清空 + if type == '-': + cache.delete(numKey) + session['code'] = False + return 1 + + #清空验证器 + if type == '--': + cache.delete(numKey) + session['code'] = False + return 1 + return limit - num1 + except: + return limit + + # 登录成功设置session + def _set_login_session(self,userInfo): + try: + session['login'] = True + session['username'] = userInfo['username'] + session['uid'] = userInfo['id'] + session['login_user_agent'] = public.md5(request.headers.get('User-Agent','')) + ids = public.write_log_gettext('Login','Login succeed, Username: {}, Login IP: {}',(userInfo['username'],public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')))) + public.cache_set(public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')),ids) + self.limit_address('-') + cache.delete('panelNum') + cache.delete('dologin') + session['session_timeout'] = time.time() + public.get_session_timeout() + if 'last_login_token' in session: del(session['last_login_token']) + self.set_request_token() + self.login_token() + login_type = 'data/app_login.pl' + if os.path.exists(login_type): + os.remove(login_type) + try: + default_pl = "{}/default.pl".format(public.get_panel_path()) + public.writeFile(default_pl,"********") + except: + pass + + address = public.GetClientIp() + port = str(request.environ.get('REMOTE_PORT')) + + login_address = '{}(unknown)'.format(address,) + #返回增加登录地区 + res = public.returnMsg(True,'LOGIN_SUCCESS') + res['login_time'] = time.time() + res['login_time_str'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + try: + ip_info = public.get_free_ip_info(address) + if 'city' in ip_info: + res['ip_info'] = ip_info + if 'Internal network address' in ip_info['info']: + res['ip_info'] = ip_info + res['ip_info']['ip'] = address + login_address = '{}({})'.format(address,ip_info['info']) + except: + print(public.get_error_info()) + + last_login = {} + last_file = 'data/last_login.pl' + try: + last_login = json.loads(public.readFile(last_file)) + except:pass + public.writeFile(last_file,json.dumps(res)) + + res['last_login'] = last_login + session['login_address'] = public.xsssec(login_address) + session['login_time'] = res['login_time'] # 记录登录时间,验证客户端时要用,不要删除 + public.record_client_info() + return public.getJson(res),json_header + except Exception as ex: + stringEx = str(ex) + if stringEx.find('unsupported') != -1 or stringEx.find('-1') != -1: + public.ExecShell("rm -f /tmp/sess_*") + public.ExecShell("rm -f /www/wwwlogs/*log") + public.ServiceReload() + return public.returnJson(False,'Disk inode has been exhausted, the panel has attempted to release the inode. Please try again ...'),json_header + public.write_log_gettext('Login','Password is incorrect, Username:{}, Password:{}, Login IP:{}',('****','******',public.GetClientIp())) + num = self.limit_address('+') + return public.returnJson(False,'Invalid username or password. You have [{}] times left to try!',(str(num),)),json_header + + + # 检查是否需要进行二次验证 + def check_two_step_auth(self): + dont_vcode_ip_info = public.readFile("/www/server/panel/data/dont_vcode_ip.txt") + acc_client_ip = False + if dont_vcode_ip_info: + dont_vcode_ip_info = json.loads(dont_vcode_ip_info) + ip = dont_vcode_ip_info["client_ip"] == public.GetClientIp() + now = int(time.time()) + v_time = now - int(dont_vcode_ip_info["add_time"]) + if ip and v_time < 86400: + acc_client_ip = True + return acc_client_ip + + # 清理多余SESSION数据 + def clear_session(self): + try: + session_file = '/dev/shm/session.db' + if not os.path.exists(session_file): return False + s_size = os.path.getsize(session_file) + if s_size < 1024 * 512: return False + if s_size > 1024 * 1024 * 10: + from BTPanel import sdb + if os.path.exists(session_file): os.remove(session_file) + sdb.create_all() + if not os.path.exists(session_file): + public.writeFile('/www/server/panel/data/reload.pl','True') + return False + return True + except: + return False + diff --git a/class_v2/vilidate_v2.py b/class_v2/vilidate_v2.py new file mode 100644 index 00000000..c7e29e95 --- /dev/null +++ b/class_v2/vilidate_v2.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 宝塔(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- + +import random, math +from PIL import Image, ImageDraw, ImageFont, ImageFilter + +class vieCode: + __fontSize = 20 #字体大小 + __width = 120 #画布宽度 + __heigth = 45 #画布高度 + __length = 4 #验证码长度 + __draw = None #画布 + __img = None #图片资源 + __code = None #验证码字符 + __str = None #自定义验证码字符集 + __inCurve = True #是否画干扰线 + __inNoise = True #是否画干扰点 + __type = 2 #验证码类型 1、纯字母 2、数字字母混合 + __fontPatn = 'class/fonts/2.ttf' #字体 + + def GetCodeImage(self,size = 80,length = 4): + '''获取验证码图片 + @param int size 验证码大小 + @param int length 验证码长度 + ''' + #准备基础数据 + self.__length = length + self.__fontSize = size + self.__width = self.__fontSize * self.__length + self.__heigth = int(self.__fontSize * 1.5) + + #生成验证码图片 + self.__createCode() + self.__createImage() + self.__createNoise() + self.__printString() + self.__cerateFilter() + + return self.__img,self.__code + + def __cerateFilter(self): + '''模糊处理''' + self.__img = self.__img.filter(ImageFilter.BLUR) + filter = ImageFilter.ModeFilter(8) + self.__img = self.__img.filter(filter) + + def __createCode(self): + '''创建验证码字符''' + #是否自定义字符集合 + if not self.__str: + #源文本 + number = "3456789" + srcLetter = "qwertyuipasdfghjkzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM" + srcUpper = srcLetter.upper() + if self.__type == 1: + self.__str = number + else: + self.__str = srcLetter + srcUpper + number + + #构造验证码 + self.__code = random.sample(self.__str,self.__length) + + def __createImage(self): + '''创建画布''' + bgColor = (random.randint(200,255),random.randint(200,255),random.randint(200,255)) + self.__img = Image.new('RGB', (self.__width,self.__heigth), bgColor) + self.__draw = ImageDraw.Draw(self.__img) + + def __createNoise(self): + '''画干扰点''' + if not self.__inNoise: + return + font = ImageFont.truetype(self.__fontPatn, int(self.__fontSize / 1.5)) + for i in range(5): + #杂点颜色 + noiseColor = (random.randint(150,200), random.randint(150,200), random.randint(150,200)) + putStr = random.sample(self.__str,2) + for j in range(2): + #绘杂点 + size = (random.randint(-10,self.__width), random.randint(-10,self.__heigth)) + self.__draw.text(size,putStr[j], font=font,fill=noiseColor) + pass + + def __createCurve(self): + '''画干扰线''' + if not self.__inCurve: + return + x = y = 0; + + #计算曲线系数 + a = random.uniform(1, self.__heigth / 2) + b = random.uniform(-self.__width / 4, self.__heigth / 4) + f = random.uniform(-self.__heigth / 4, self.__heigth / 4) + t = random.uniform(self.__heigth, self.__width * 2) + xend = random.randint(self.__width / 2, self.__width * 2) + w = (2 * math.pi) / t + + #画曲线 + color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150)) + for x in range(xend): + if w!=0: + for k in range(int(self.__heigth / 10)): + y = a * math.sin(w * x + f)+ b + self.__heigth / 2 + i = int(self.__fontSize / 5) + while i > 0: + px = x + i + py = y + i + k + self.__draw.point((px , py), color) + i -= i + + def __printString(self): + '''打印验证码字符串''' + font = ImageFont.truetype(self.__fontPatn, self.__fontSize) + x = 0; + #打印字符到画板 + for i in range(self.__length): + #设置字体随机颜色 + color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150)) + #计算座标 + x = random.uniform(self.__fontSize*i*0.95,self.__fontSize*i*1.1); + y = self.__fontSize * random.uniform(0.3,0.5); + #打印字符 + self.__draw.text((x, y),self.__code[i], font=font, fill=color) diff --git a/class_v2/wp_toolkit/__init__.py b/class_v2/wp_toolkit/__init__.py new file mode 100644 index 00000000..46795cb3 --- /dev/null +++ b/class_v2/wp_toolkit/__init__.py @@ -0,0 +1,17 @@ +import public +import public.PluginLoader as plugin_loader + + +core_m = plugin_loader.get_module('{}/class_v2/wp_toolkit/core.py'.format(public.get_panel_path())) +wpmgr = core_m.wpmgr +wp_version = core_m.wp_version +wpfastcgi_cache = core_m.wpfastcgi_cache +wpbackup = core_m.wpbackup +wpmigration = core_m.wpmigration +wpdeployment = core_m.wpdeployment +# from .core import wpmgr, wp_version, wpfastcgi_cache, wpbackup, wpmigration, wpdeployment + + + +security_m = plugin_loader.get_module('{}/class_v2/wp_toolkit/security.py'.format(public.get_panel_path())) +wp_security = security_m.wp_security diff --git a/class_v2/wp_toolkit/core.py b/class_v2/wp_toolkit/core.py new file mode 100644 index 00000000..e4b09a86 --- /dev/null +++ b/class_v2/wp_toolkit/core.py @@ -0,0 +1,3818 @@ +3m6maKOpM/L/yP3rpT4bK2ggj7JOWTKnF9lbOKsSNQw= +hlsbBRfiGI29i3aXREgRMrgu25uNDIw/Wngp8RMy9JY= ++0eYQPDDVpFGHN21OazCCuiB1t3ZWxn4cDgvZYFE/8A= +dTLQ8Wr3wPn7w6pte1B1YA== +SbQQ5SqO9QrBwZ9ObpMYLw== +piG6BsMF31u4R4iA6R4SRA== +XIfdJ79nObMM+vyAmKbTmw== +wiYVtfO/yzajW1Tv5z7hyA== +w42r1xg9qpESU6Bd+WNVzhnKUgvI6yig6xoFOZh+Cng= +DRVOWA8p3Hg+UVFB+ijoeJiQKS/oQs+5utRgqpiaBn4= +2+FiEb7mbSCVnobUl8p5wg== +SLvA+cN7EMWEfpSbTDPlOtvm1okUJx/fvmAkl6hTius= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +B1MnpepwGABZj+h+l4KbeCnHuyUvLETYyVVJBjqH0Qk= +gpdp0iBlo1faqisT2fyaTEQwitRJuKpFcBeXqsk4rjo= +GCKcam944byMATazjtzHeVsUGqC6vWeA1DfjDIipRhxLtfv4KarwG6sc3yAKPfXzW0/4GSvEN8yz/jftCH5BLkqJhp0cFRhshphRei2x/2T/DgZXH+DlrxsSX42f866jmbz88aLHwJ+ztyswFButMg== +EAgPtM5g3sE/C9xowdVWHh5T9sPYgMk499kn7UZXGsqqddNIQpwwYZQDmFPx4l/vAvWreETGtszc1fz27yvBRGXBILoZ97zoSJZctY15wm1P+zDFeWIDXUoPKAnfvDM1 +BZJVsvwr9RklOP4oCxlRj7RoslAW4veKqRHMHaknsfsiMLPxk5j/x9wk24l57+FuwD/veQlzmDlP2ltT+fkJv6gEyhMY1rtExZrfKMKcrVxZM7OBAmOa9gBDDceMbmhPoyIh1hR/NZOA8YSULoP87w== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Rsz65WKl8ZLvUDoyoE6qaZxywIHsbaqTmpElJujTduk= +CqGpN3Lz6vgA0ts4Kp9Fzgup9BufOQ4OyxefqALb8iQ= +mA5cESknfOIsZuRV/c4Jnj2ahKMS5z0hQlLzYOzH8EIHcPRQP+M8ShrrFNXoeRVHsVmYb4gFZNJkCEB9jgK7jYTJ2Prh5cGlgjjjAqkJWf0B9SGTjc09NWzT/IUc2rbfyAkKLD5gyHdCVAbnwUl5DxohqYaXb/8pE7ZroGbKqlE= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+HUA8m6mVgnOLyW2K/n831KjDD/vs3+PYavlI9PIHAVxZitd/gf/EGgU3HSjiGPHZ9PnOduRTbHl7wqc9DjbH150xNqzn5OZ7ZUKk32Z9wcXUG6N3AytRvnrsT2MPmW1ogEWqmw2arSEV2dGU1vSOAbKpL8Xs1EbRJcSRn2l2o9XiXklQXPDp2iZDtrMZNCidA== +IqGIB4xf/Wocd4vrF30SvjrifOecHjUB/3CQ+0MqtOc= +TRyo3DV8r8YcF71x/BjKjVtNCQE12YHUh8Hewxm2qQmm/R5Zwzp25xb8A8+g7CQ8 +xL30zHgIG5h0PGFEOMTs7R3DGeKTKDH8OyCwEBkjeo639eNUHj9xIKJtp9XyvZlR +fXBVUljuohLKH8mwvXLO+SePHL7KvCNMwBdVXOouxz8= +fXBVUljuohLKH8mwvXLO+focbmHlRM72hp4YjCz9E3E= +fXBVUljuohLKH8mwvXLO+VWJDPoImeUvuijs+4Qy5hw= +fXBVUljuohLKH8mwvXLO+ZFndevbcoyfhrye9pOf6DZbMYg1ZAQu3a08jxwZC6ju +xqyScO/MOJo+rfifnO2pVJpWeznnAM0PzFdZ280Jy/34dEIKaw/7o7MVUnsgPLioaawyeQDCDAqnThzqWocCug== +1u+XjG/2+GSQRv6EzCaWRQ== +bH9iS7BmfKTTQ5e5cIHrV+NToa+Qu0Q6miLpIDPC7OQ= +/gHFu7aRGoB8NMhadfH0LSYgFv3ONQfPrqC2lhgmRwSiatfh+gngguBY7Z04lTP2 +wgR07xfoapmx6eEnFHXXYlEV6h2Qh0/E9zwcuGp+QctxMca/YFYUc4GaSAHUOjC/ +YY5wEo94l9qfo5TPxQmUgm3uLJxp9ZFr+0KFa0oXP4fvmkRFy3kB+iXcpGXlgGUvSOAewGU+5zp9LlFk0DAKdbERHqOgd3tqzlIJKxfIuc5T8Q1EqMGy6UZxtIacOK0y +1u+XjG/2+GSQRv6EzCaWRQ== +gx3w+Kr8Nv+8r/RRihui3svS1Hqc14zcq/98XoqABuY= ++pBWk5LxptzizUTsHnZAzy9uhfHenDbrnbLeEHvXb3gY87+VI2BjWXeaFF/dYIPd +1ZFe+UUdQmu/QujLaciJaqHVy1/Zv7gv7g81P1rTkAeDT02SO6T7WTRZBmkZwNSh +1u+XjG/2+GSQRv6EzCaWRQ== +ewMPic4oY8KA6fNbX11t2SULqPgooroL0OECyzsrHb8= +kgIgK7U5R0FgdlMg5Rh9so7sozUcB0zfuzq4OYYN619zxkcq69ovj0XL19qa+qDQuEiz3G0oQoHCwoMeF3rGZw== +xWoGNWjKGPfI4gq8aHoTfKj7V6/GbVtiII/Elkzy/SIxsAN6Em7FysJa4jADHlGMg/lVpPwNP8IEbwy04HBldA== +xWoGNWjKGPfI4gq8aHoTfKPuN4OuuBBNBwJwP5t+rrG0Y2u5WoVDzNeyHKGQE/w3iS/K+Zw1zFYz9h0+kWnC2g== +xWoGNWjKGPfI4gq8aHoTfIj4i28ss+29lGHGjDyT4yR9pxg/1r/Xdbf0jjEA9G5Eo0UfSlH1xtjkf2egjpprdQ== +xWoGNWjKGPfI4gq8aHoTfI65fOfKuI1D/27XO5rrJkxYDYN5AaD/U8ieE2lndMCRUHP5i2HSRS1QzIGTv9NIPQ== +xWoGNWjKGPfI4gq8aHoTfBJP/QcKNqdYzuq5movMnohyKLAZnic66Q+f6D9RHfEv4F2aRqi56VqKzsll17+P3wQU2933tacjO6MptwnWvUs= +xWoGNWjKGPfI4gq8aHoTfBmQh5TOrrwkxUFe47DZWLniKO5z5xOMDVxEQ+jICCB7I4S9CMcaer47K7CObFBgnYhS8H9/OW6bU2bSVR0IyeU= +xWoGNWjKGPfI4gq8aHoTfIn6PnMrB/8qeqFwmI91ZmH9qroXVV1zNa+Wb/R36MP0Ov2NlEydgmSe0VAxyfEZYA== +xWoGNWjKGPfI4gq8aHoTfOFwAtPLj4a53BzMR+y33t5F5Dsx8SXJwti8PdMsllMW8x40N19uyMI4YjnqPGawjqrcb8pVrh08FkRfVMPdizE= +jIYtewpcL6elNlf4btqXRw== +1u+XjG/2+GSQRv6EzCaWRQ== +3PYuJCOiZH1sjGKjxSVU6FxQzcYGr9jxKQMV6Y0aCm7Q8D0/3M6ro6FjClK10BcOVWBbfWS93OOUvuvrZj/Uvg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmezvkVwBnln66Bc2ddZ9UUpfzRP5JCN9M61YkL8kkfZGts31X0xlOn1WK79Qdtt/I= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkRTza+VuW0x8O83NImYiJZGuJJW/3v1tzOfzL5MSH+EVtJRiicUo0JkYXm8N8Rnlc= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklLUHU89Sv40FT2Rv0m4sknkQd0bS6cwOdmtwhtNePqhw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl7yTnnnorksGepT/Q4eVwpjg2BBCtQxuOPuKZY6DHa1A== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkml9enbBiSpusrvGBnE/5OLrGUo44QSfVEE9FVYnEZ39w== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm3+5dZCbMg80GQapK6hYjbDrbBo07UYr5auewiEcA57IxmIKl+Fp+HHS+0CDYltcE= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn2roHpOuDc0PI1P3pE25/MdJ6gcnQqeeLqyKYycmGD0rFHSqCjj/7fZpiImHAkXmg= +1u+XjG/2+GSQRv6EzCaWRQ== +xHcOA00ZGeV2Y1yiFUriAeqhbb5Y4Kg7BYO9ULVhShk= +xZTDA13Z2aXQCOTarFNfvvSHkKKdIEL9fr6G2/Uo5VMrTV+42Fqo8lo/M3Vsgecu +AWid6G8ypXqQe9XxPPYED21qUZ05KduG0I5aBgcmb0M= +Oc/iRPlDEtyVPsBvXbRFyzMsneQ3tkrY+OvPJlqNc4zJi/LtX9Sh6vLSPyzveTXG +K8WxDHXgwdNGovmPv4l3bBPIHS/ynmOr4VV2JPV3z1uOCkG03cwN02ZluHoUCy1B +V2fR4lY2I9TUOq7llIMViSAda5sjsuG/aghTCUzxHRWMOqTAJEcPGQV1sgnSfFgg4nKP1sEAsvPHYiKS1tL3qw== +XogJGekt1YVWNznrGGQkHMZZ++lm9bt3hejW59CNL3xJd1jGnpwyEQ9fGd1lH7b/ +XogJGekt1YVWNznrGGQkHKhAB+YaJVFLU31KreZaNlc8Z9HZUKZPjIBpTPjEmw5n +XogJGekt1YVWNznrGGQkHP8ntlWAsRSKvGhVAbTDFZ9wUhIbjcc+T25Fxq0kdi5y +XogJGekt1YVWNznrGGQkHDWgIPBFSa7I/42S9W8M7xeJxq0R0TwZDuNXQ/txpDKM +DxAshdaxPMjTqVP65pzgn1mDaN/wjMK9a/Skh3Gwtmb+beZNBxsoNe9KC9IBpxReAzx87DZlPxMWjZlx1qbUmA== +661hZf7vhUQ+50okfwfTXw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +Brinyc2FMdG3K66yYsaM/46LvfR7DZvVtJk4kqVJrnY= +kKEHZSjBs0JaG0I7BGh34QWeEiULH9CknWVRytnon9g= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +bLlixn6igwVr7WeYa5jmwDrF36ciRNNuWP8IKAPThj5ALnT3Qy0qMqUkocWUN7aNFt2VdOGgb55P2u2uNVSshI+uybRclxp2wf2lD+4rExY= +M52FSgXgnXx2Hd7dfJpFXqUxWAn//1/37n40o4U555UEM6YD+xkY0/vXUQIyze8JhCezbHWAWdjOA28+qUxzVQ== +Gft9mnUbPRlw7Lcbqmj5nG1LPrJBxU5tQ+GYPbByiRyY8OR/ds74YdTRBXCb/dwZqN/XE5ko3wEpHgez9k0PszM+auooFj2yvntzJvdDmbvhvDo3YY06I9zKyvnJ4AeK +Gft9mnUbPRlw7Lcbqmj5nFjZ5vJ7MSGmsRJBY2Y6jeqvYHf6NMbgHnKumVDiLPlnfOsFMk07Dtx4sKqykIB2lw== +Gft9mnUbPRlw7Lcbqmj5nOn0r1+dTzEFMz/TuZQujsMtys2TUz7xp4iv61K/LfIQ +1u+XjG/2+GSQRv6EzCaWRQ== +NRG8WiBknxy8LZ7uM7RtwuCiVTmnFdHKG+846d0G0jqPO1wAZOfRb+FVWlMKscSalXdEAOtk8QzGkMMpNBEFfQ== +1u+XjG/2+GSQRv6EzCaWRQ== +ab9qVNCikBOofZBq3Gme8HQcHs9Y7cbGOcd1c8d7Zbs3R+Q0JL5emJPPF2BzU/W7 +fsuS82vhGsZ1ifItIPgGCr9WKjaKw1ANZnihJNoOblBBD1DzvcRUtMY5sUO13mSsXI7hDfDDRAvY5tgIyQqIiQ== +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9i3iIoN4da+NV6zS2jUJBjOZ25VMbpcWtPa1ePPSKlOug== +NqhMbY8+EQI8zhTG6Zh2I2tVjSnXDwhlBCtnVzuUnFfryLTPmSMz+UQHehiKmZtj6p470FlnBoiFHv/pEpQaag== +1u+XjG/2+GSQRv6EzCaWRQ== +DBrQ+tT9Otxe5su49ASt/93XucGowmX/RxXV+PlJleqPcZvcm/67VQJCCWj7YBEAwt20nevK67+4EoTjXeEqQQ== +O6khUd/vkbHbRdUkJIkLvYmnN4I20A6/6B/+MBaixZYSK9w8XgrkTuu7sLDpxm9f9qZGhX3HgkU6LeaQ2Hysn9urJd4DjDoOuFiHG2CZ0eA= +91gsC2HsVrvTm3cwuG3h9nZQd4BMS+GIOQtJeYVYg42RqQFL3+RZI6cOvFHmnDcH +2yQsUep5WrV3T3htNZ1L8VpIZobQOi6VLLDMdOiUktny6MBI+2Mz1bnR2dv2ZweR24dy1pkWzmWOlmazYnw/lvsyuX828RamOjCWr05DU+I= +Sue/4scTt39iGPa9/O3QwnWJDL8HoMWb/oV3KtMVeK2pqskV7L/K1Ekh5NMfbCTqJlUBSpp4ABGyfflrxMTSzuwLIsXNuArLc/qa/JoE0qc= +1u+XjG/2+GSQRv6EzCaWRQ== +sJAqu+RKGNS3DoGZfXMrdAtUx1XnMB78PshxGhtOKXY= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9o2kvXaOAtJwzudc1BuzPhLyb33yC+z5K6zpgrKtmiPS +ghdF4G8YV9yNjU+KsNcQR/VMWHcF+CTNHIlYTVhUWmgu04/2YZIzDcfqjhf3fFrtoaPuJ1ZAgoFdTrU2n3Bp6CEuImuelVqhbjlIS3Ots6bXPpgIt6k6gOj9Lz+xpaOd +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYo4se8cEG3Kpe+KryARC+5+xlpIc9lykYcAsobdrHGRP +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9vI0FW78zR8MLP0SwlWQPTGDShhW6CG3xtqXeqzB1OG/u8NShoIZl2E0BcAQlNUE2osl5b3NQ0uBeogijU+Jk6ce8mNGCpm8lE8TQpUA5NGb +WHkOzVx7seuLmxs5Hu+l/qarLKXvM4ZssfOCb5hUzDDAsdyt2nHkmI9xC7FYW2tZ9GF+QKMxspFxWk+uvKcPgQBKWqcX/JWdl1opCZTMEtSuSoOqYyklRY/ccPZ97bSS +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +e91oxhGuXYdY7jQndbCTnkR3/JBPSsXMQKkFKHIi4XfC7qybkzu3totipce6WUuj +MzOj4ZiBOJDNVP8vi/lFQLZHDfm+szfyZrGm6N0CNxOdc6o1JD5GkGrCFoHqWVqlvhA8R9vwjwMTxPNXj2xD2P2HI0OKA5jewTS9Lgua+r5FgsWY2eGBp8WUFOdz6QAr +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +cGe8DWc/z+TXH5eIZ06sPPCnlejxDyXdQgh5XWNRPbE= +fcd26qdp+FAAJ8Un64yfsngs/9D3TyO1zoT1IRKT/a6fAKrRvA8A8L3cZg5sA9OIsb9KhrQ344PFm+9BDoubDwBfi8BZ4dDf4PQcDhpBPY8= +MzOj4ZiBOJDNVP8vi/lFQIitY6ec9KUdnqBwYPj8LW/XB6/o9WZZ8zoxcP5nWFGM +L0eUthVnpkGsmKFAX6d+uHtCZj/pUaiDieVAnXD5pY7Sv65rDuNWilNBbWAi5frQ +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9q4OT4WqICTkepUshfIn1VEuFQwhgGt/4mtj5sBxLljedFEPYuA1FNDwgs84qwYY6A== +ghdF4G8YV9yNjU+KsNcQR/VMWHcF+CTNHIlYTVhUWmgu04/2YZIzDcfqjhf3fFrtoaPuJ1ZAgoFdTrU2n3Bp6CEuImuelVqhbjlIS3Ots6bXPpgIt6k6gOj9Lz+xpaOd +SMMdC6NQONyUThzk8KcNdiEuFChkdyfrXoOuNv8F7GqSdn5UrMO3EUirdrzZ4jGHWbw/HM9MQ7L2CQBSQfvL0Q== +1u+XjG/2+GSQRv6EzCaWRQ== +2yQsUep5WrV3T3htNZ1L8VpIZobQOi6VLLDMdOiUktnA+K5G6RVZfVMSE462fDWBz3hXD5zSQkk0VJk7hWSnkg== +1u+XjG/2+GSQRv6EzCaWRQ== +25O1h/mctlgTNH6uiqc0ANi8TL5k3W089rH3RO7g6ojDONahflohA2lT39BHtPfS/e78VaqMfeHp1t6nySWXuyj5Wi+KAKTin4GN5bcN4kWykvXky2N/hrzdjjiAwDjDtwWmjCwYBVJSOw5y/zNProDMi9lDy2fHRbDB3ZXM30M= +WHkOzVx7seuLmxs5Hu+l/tf51q8WG3vFFhDtLbV80GiYN5VO2BrUQHJaggSEtkf6k+WBi+mP3olNpbaqYCkL12zjClyDut3FRePfbDYMdXclfBg9ZWQ4tAO9uaJjRghMLltswGa/In+ATyIB2lu3DI0YsFOlxsnRD06C3L0uEqAqIbnCgtaeYgXx1JDkVl9nYjTnF+dLAyNooi78VqHcVA== +1hs1FfIPISXmd7TJ/r+Vl7qG4Vk04Nqb7xlEO974+g36ebjqekhI5N1i1YoQXLyn +VmVrGQo2zRokW/ZuO9bN60oVPe45Jqv1Ihdxdz+8r+BoxG1E2W9FjbYTt0EQ+jVH +1u+XjG/2+GSQRv6EzCaWRQ== +zBfpEDEHUpKcgzvJDq0C4efibHCd1IfBfYtJcrr2rCKJrTrkHOWDEuDZ2db0VLXf +dV9xz3yyeXnMinI4Y9Bh+qW0OFHtwajp60c7Zo+aS8zNCfHvOKjfr4NL7ZPuzC6+oANC/f3C6vRb+AeYU/Kwwg== +VmVrGQo2zRokW/ZuO9bN68M1iR/q7rXPheDRGlmMwDTXXKGMyTcbL56i8fKCdpuj21TCZxNzrC3W7EwT7e0FdQ== +VmVrGQo2zRokW/ZuO9bN60pZC8KgCSt5/wO3m11t4ORj72sfxotiDHY4TlDYBOZd +1u+XjG/2+GSQRv6EzCaWRQ== +u9l1ZaXtVaYlyzKhJ32cEHtjbGYyhbGklEQ89tkB0FKhN7oEmT2WsEheTBy3Wo5D6/lal/9CZs8NYkERcwLmMg== +ghdF4G8YV9yNjU+KsNcQR37Z9yu4rJQS2qMMMkR/Z0ATZx7dENZgOS+DLQo8+AgyFOFWwS95Y5VRLCFY4BzR8HZjEccLEwAIWyx7ieM6BEIqip222ROM+Gg28PulsH/cGHsGz0ore9TvdpCv87Bd/C4Hn+lynXanwalY4AWdrZBI1QPCqIJld9eQAkbTs/cv +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9pTEmSWM9G4WzELaUcpESanbkDFxPARX0TRtOSOLCviAsAptbuukF/Ud3XzEa5tzvQ== +0Ey56M3Rq4Z87mMZBzWCkc+uSKtphLKybBPpWa3LqbDSjktYcDOLY4QyDDr0fDRZAgH25cnh7Oc0+hAdqsEWNA== +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7Z+2OVMl5+r4kRRoTlBeW+kf/aK+IIIQHVM99XKaa9eAf1cJVLTv1fR1S38khDnTdD0jiXxx8qZwRyxAhpFGVWaQAj5VgnsfoSyrPK7kJlVKCZn6wEyCL1uds860YV2fp +1u+XjG/2+GSQRv6EzCaWRQ== +YzaD6B908RU9bV8IDcajo6chDSERkl2seXU/yZlu/+7JS8dFpcbiT9RD33d9SqMY ++pBWk5LxptzizUTsHnZAz0YU69PyIiLz+EZj+GV7UHCvthR5xAqr3A75+NFTC33L +p2+bVXgc/OGpnu+3Mkh/HbF7FFV9jaNYd2Ecmtu19AY/zaj2C5YMN3GytlTkrQrW +1u+XjG/2+GSQRv6EzCaWRQ== +Gft9mnUbPRlw7Lcbqmj5nNwiPZVAZaRv5qe+FBX936kLOPRsZNspd3GpR2+3ryGZ +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwrZC6E3RApMFvCYKX/b+QNg= +1u+XjG/2+GSQRv6EzCaWRQ== +jCqMwRKTYZbqj942eGY3g6H7HrJzpJtNEeAiCxVEQsUY+94jjj4jjGMYW5tcQQVO +iVrumwMNPYZ207haO9u4MdLAwVB5xOk7iikXIMTVL/qL0+TBe1COKZNOXz9EWzaa +yXSv/K3mXXlz2KvCo7xU+as167yQDD7w3um0fZ/Zo5HXPyK5XwkS3F80/E1fUXUS7P7Urig0jQFbPt18H7X61A== +1u+XjG/2+GSQRv6EzCaWRQ== +0ZrlxPlHcPlwwdbql/Y0goAKwSbzsj5E0f+76P+ZTI0= +L0eUthVnpkGsmKFAX6d+uDdUftKQ37e2yKnmxKzZfC0= +1u+XjG/2+GSQRv6EzCaWRQ== +dcfAqzXc4801QvnwiMyF+2DhmN8XNNuQlHXbQfCV0Nn9ypOlE4j4bW7JGiceAExZ +1u+XjG/2+GSQRv6EzCaWRQ== +ncUn2TP8ZQ4fZMBYk+CUrdu53GyhDeivQVbLe/Cj8XY= +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg76SFgO0NgF6IQZ0deo2NvyZe9iARvP/9LNBMCXc/GwFKhfGg0oHXvQhP6zj+NTzm2WWDLwmt6i8LJm1s52bqPwMicoRl1tqnBMMlAk6QdUw0= +1u+XjG/2+GSQRv6EzCaWRQ== +v3wpVTLCYf7MVLSW8dNUkMNCsmhq64mdHqB4jWWj7ECOBeTJVXByucVDZMM4OgAc +1u+XjG/2+GSQRv6EzCaWRQ== +9vrQICgDEUtTq2I1sk9wVZxywwiqb4lPgg1tfjsx9ks= +L0eUthVnpkGsmKFAX6d+uLib58fjFLHLA3nq/44o/Cw= +1u+XjG/2+GSQRv6EzCaWRQ== +yXSv/K3mXXlz2KvCo7xU+TwZ8YT1S8ZVU1YkdAJMPPc= +1u+XjG/2+GSQRv6EzCaWRQ== +H1zFwfLyaEvixPsCrENwTVKYo5wX2+mZpzdnjbJxYXFr/VM0UXh2/6nKGuWYcaYRVBWbeqiy0hYB23zdr2qqzw== +1u+XjG/2+GSQRv6EzCaWRQ== +auZi0vV8Th1M5xUz1rxru/S/WPgeQ9D9vW1dYZxGC28= +1u+XjG/2+GSQRv6EzCaWRQ== +S+kfs54VNIP/n32P81eUqUVnMQh0dtT8GLygVFqA+pI= +iVrumwMNPYZ207haO9u4MbOs5zARicyvkD7ENDcmKp1YMhInE1BWOWJaZVqn9fug +HCOe5+CEuFhpcNFnn2isi7n6VeCjSB8p31KLJNkuVQcdsGx+IxWL2LnX2N0sWmqi +1u+XjG/2+GSQRv6EzCaWRQ== +5Bx9T8RabvzydjBVIGKDKF1934sgTtE9dUuydNh5v3U= +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg76SFgO0NgF6IQZ0deo2NvyZe9iARvP/9LNBMCXc/GwFKhfGg0oHXvQhP6zj+NTzm2YCL6arRmvQoIO8XVGEm50kPI4g0OrcTYHXBerclgg+k= +1u+XjG/2+GSQRv6EzCaWRQ== +8nTfjc42y9zZE/C8Mgio16q1Vv6hM/RPQZOPQVLeito= +1u+XjG/2+GSQRv6EzCaWRQ== ++GQhUHFV8V4JMfVXtWjle+jCaSZKCHeDhapa4+6ehPE= +vHI6ym+jnn06PgL99476z6Uwx4qvfe0JegnXA4qmLNl8/pYgnzU4wh0jCA58VVutfY4MrkqewDmJI+ejDGKNQQ== +DzWU5pX0U6cGCLLtlLj3YJU3Thb2TzBJE1jx5FMcRponhGm9owvR/LWtpuV1gqBq+oa72tMTaZWAJjWghMubjMlnHtotD7AxMClsn6ZQftEkzbOCLb9RhvcObU5RYT74 +1u+XjG/2+GSQRv6EzCaWRQ== +BIa0ibHnqUboaK9kYHYO4tbU8n1NMn8Xi2IBkiJce/0aOeCXvbyPEZUd4kMLMJP1mcLvJ0FK4ro+W9D/Ckf1vxbra62mI0mIQL9r4GzXc2fEiUSTo9SLgrEQSCR3x6GJ +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9jfwEyK95V8JMRysAynvXyLfTKMD36VsdSSSKTE/9HcJDu7rBiq5+LZUasIHQ8orig== +uTEK8Ng11d3ix2pA+DD/aeX4oVY4ObR2fwNjSqwh/LbiZHEYz3Vfv/nBYRIy+S7F5cTeG6y7Yv8hmDkyVTnlnnhtgtTOE51bS1FBhGbaS+Y= +nH/wawDoCvT30BetT9hlE02N6v2mxLpWzvHZPo21ucDSIFS/eqxhwPlfTCcpcx6+ +xWoGNWjKGPfI4gq8aHoTfAr5zz+AGV1f1n+brmMBXW0jGAESEjoMrZ7jItK278XKCKp5YiJWlk9daKvLt52yWPoHEl2lNUxAiDxxcNAZE9sKYPkAYH2eydLUEXTX2BrS +L0eUthVnpkGsmKFAX6d+uD2wphTRX7Sm0fBhJDeUo+w= +1u+XjG/2+GSQRv6EzCaWRQ== +ojcUvW+Z2ehEJ6yMJpmY+1BIkc6rSsZIuNzII88JWGre1k6Lx1bHPPy2u2hfHiab +ghdF4G8YV9yNjU+KsNcQR9/VTmoHLDyOw1Oy4SVcpoph4aTe2gOSesTP5XETsPoFunerNRWlPc17+NqkC+JA75UFkhCO+zPstdL+gghr7Oo= +1u+XjG/2+GSQRv6EzCaWRQ== +lMcV7pLM4V6yRWTmLUOCgaVkqzYwnwVgunmVXX73/bQ= +JxCuM+wh8jU3LxN1s2jW+ou1+YFUOpfeMZrEoruVlCea1QWpBOw33j8/t5tn5v48AJdT+ITut52C8bioDjwemj4RpfzLAAJaXatuY/UhYVqPQuhe0gCr9LHvUAET6KOjWRgtNj1V0Rco8JxZ39lG2g== +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +13mReRIe0T94ElNhUup+f1S2jbSZpPAXzAJ/8Q5RExkq8wzwjYROwYGrrxYGMM+E +VmVrGQo2zRokW/ZuO9bN60/Romck7/ZGlIQYh0r0SKY= +VmVrGQo2zRokW/ZuO9bN69y4ScxvyusaIJAAB2obAVLmwbX6ZHhBC9fe26H9hBYT6yFt09qy0i6ljp54zYEzwQ== +VmVrGQo2zRokW/ZuO9bN6+4NNVEiV/ck9Ae+8oDJrc1+c/vMbOREODpMOUcSlpwJ +VmVrGQo2zRokW/ZuO9bN6/VbEMb4SrLRnTT1zhB19ac= +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +Cj5bIZIaHsN6z1G2HZPA70cvamsJbajcxwl4EX/9FVA= +84Vymj90Wzn5yYuvz1pUuMcQc/5cIBQNdMvZ5GFX+Y8q95LOQWt+X47RrsBXSt+WgP/tuYrfYCCeoHJBWCOrdw== +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNsteJ+IsYunNCc1YkZMiZyZU= +VmVrGQo2zRokW/ZuO9bN6+F2CFsALMaXVdBA3HKUr8gd3TOT5slJjO8b2qVlVJ2nEs96wh6X4ADX3XYZ+wXX1kEwKHJWnOgeXeClZ94m9sVawkJRJwPTOldubjLm3ZP/zsEh9r0VBlko5tuNNHcV+vKPu1T/r6BjfMcQZoQJUc4= +1u+XjG/2+GSQRv6EzCaWRQ== +F9Sftf1PoN5P+lgvc+r10NBKW2R+26ei/bRKalXOAhAI236wwjVau5pNFZvrcQ86ZP5625mPyV6IMNdP77Y6wFYZfiUM0wwcV+lAZPlyjko= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNsteJ+IsYunNCc1YkZMiZyZU= +VmVrGQo2zRokW/ZuO9bN6zOXQ+FQiEpTeJqvLADK9HrT9/1LnCYltn0TCdscSKv9twISbOLYeMT1vRGyXss78s5PBFoEZzJkvAP1whVmMo0TQyOiLxWwbFs/lIBqlhqeJgRpHGCtq9RC+bxkSTYdbS9Gqw3uxCF3p/a+1M489Mk= +1u+XjG/2+GSQRv6EzCaWRQ== +F9Sftf1PoN5P+lgvc+r10A845uNSmtFh8fvuV7jkYMquSNXbHEcmCUotgU+/LvzN +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNssk9PPZCaHrfAXriSfntx6s1jAMIIPF1n/P/4sI7mev/ +1u+XjG/2+GSQRv6EzCaWRQ== +F9Sftf1PoN5P+lgvc+r10JxBhEIEKrfIEl/IdIR0YkD/vO2L5FCeUyz7ul+h1Dgt+DHkTHy1qB+TYfB86VRYrg== +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNslWI+TykiAi4ZvrI+lQnfhKNeBcmc3sS9GbiGHYF5shr +1u+XjG/2+GSQRv6EzCaWRQ== +F9Sftf1PoN5P+lgvc+r10ItKLbtXkEFBanzJA32vJBHAq7Hel/C4XvC6BvvRgLu78+wogiv5E5FwxFvvWDpkYa8TxVlShxRa/4gKNR9gs18= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNslnNlTS+MrKg1dEvZwgZC1czCRmLfvB+oTHO+ZsL8czaIHc7hfz5zVlgT13pzRh7dfqtQhu8Gxvq/YLpztx1O90= +1u+XjG/2+GSQRv6EzCaWRQ== +YY5wEo94l9qfo5TPxQmUguohX8FNa5Am+zbgHGpyzEY= +1u+XjG/2+GSQRv6EzCaWRQ== +ZYadN0lxV3av7R0IlSlFc4jHJgL9o9Bu/DN+HyKi8za2mlriK24hCtsUGlPUk0J3kW+jUn/gkPU/z4JksqmdenLF2phYW6Ldki1dFNGSDgk= +1u+XjG/2+GSQRv6EzCaWRQ== +AyzgCjEanvVjy2pbs0WTmY32p5FBb6bDD4pNT8f+HU0= +h1aN8vz6GICRzkv/5tZnhit754oWtIyBrjl7ztyIcrQ= +MEgdP2ueBOmTLeiwlOehUTJtRVa6mPXbpVD4PeJhjtN3sb0eKjvpC7usqe8+r23y +b4OJVZe8QyIpjuTpKXDL9A== +1hs1FfIPISXmd7TJ/r+Vl20P/yfp1nvjLuNOvIMEPE7FfByQiv0oXZLjstjozSI/Vh/jkaFCfsawdKLJVJxKnw== +VmVrGQo2zRokW/ZuO9bN62rM3Hdt9MDIS0VX28CReuLiGyFICWxXFD8oPeJolyenaKARJbHEbsTc4UMK1uKl40tK00PdmnkKMZ5nBWliEm6L92G+EcrAm0slVBE6AHfl +VmVrGQo2zRokW/ZuO9bN68X19+v7hiouS8aCgSgcC310W4EEQk8lcEs0wB1lqE7B +VmVrGQo2zRokW/ZuO9bN65Qt/eNdU97Xgg3MBTwMk+j3nkkrbNVglsIqBXsXS6Ea7wLjLa/uoNTumjIBCN4VMQ== +VmVrGQo2zRokW/ZuO9bN62Y1nlba5dAZkCotwLpvSRLuemdvRbjxhFIOFHlbMlr4dAHNHHRqKWSDJtopzS8RYw== +VmVrGQo2zRokW/ZuO9bN65NYJ2r3bXAWyqzEJGl9lwQYT8w2NGvuw6XdCwNgyeZo6MVXTMlJk/zeCWJ2W8AS9g== +VmVrGQo2zRokW/ZuO9bN6zy4TLHyrcZJLPS8QMGFxO5ThIzCAU9EsO75fHFtqJqZHZV/w+9QNHgul+Vn42PpWw== +VmVrGQo2zRokW/ZuO9bN67dwuvMkl+StGG2xLb7VnpMrHZuj/OmxjOHQoAlFGt985VZT+RbK/egoTQ9t2JOsZyycEHCgSslu7SheQFoVOHc= +VmVrGQo2zRokW/ZuO9bN65aeNv0pgf+lyAFM+EtM/NWcjqFdwDCPzInlXF/heOHj0OvN301aT/WNxYqGZHJX+N9kgX/AJCNgROgpqeVcABM= +VmVrGQo2zRokW/ZuO9bN6wC3E3y2lww84cJpWrUnHxcfX/3HqrtfnuqEhoZ9ufTayVRnLdAcbqSODxJznhaSXR1FGFgRpktwLVHzkeKrmBk= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpsJVPSWMHfiHsVRWFGceRv1LMg1Vu6K3a/hYcINVMcdM= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpOo9L/MB/xTLfSDfaLkEAzADgZJIsZ/PLmUe8Q9TxMHI= +VmVrGQo2zRokW/ZuO9bN66DIsOpHyMnIWw8lV1Va/qW2RVl1rTPqvlLzkCO4/7pn0I7Qr7PGQlmCrGBmQOs7XKn646ADijtbWAL6y8UXhcs= +1u+XjG/2+GSQRv6EzCaWRQ== +7bJ3Uk7aqJsghutxBeStwgvqR4bkEY5QVcOQS/PJlRdXIPN4aRNIb4mVLW6S7+Gt +n/Vg/an64od1JqfYD8zjtuhUx0dEkE8tG18Cs4NQXPxx5sy783NZOSwjs9+QhV8It1fc9kFscbYVv4ylrN2HuA== +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNstN/qHjpTO9iRJHpCbOxN+TMBfwk7N0MXaSimAMiFTOxteDedfb9T+7KvvZj49awNg== +A50UO/kAI17YP7MCbTvBkF1QxWGO4d7iFEjQyMKkG7Et7gIkhal1mvZiTsAv76s9 +mi1SrApdTCLGy389+Ojfxu/q0EBE6R6dRrIZe1CIcNI= +PWKGbNEPxxxpGpe8THO244ghyizvbQZERneppW2EwRWsPuiKc0jPoRLQHA/bWvpO +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKXrTBAX3Epgf61rKlJnxQ +VmVrGQo2zRokW/ZuO9bN64xe4hDTZPj3fRbdVOy3M0gsl+SOQ6YsPkIgqa8pzIFfrtUF7F9rbmTKcpkr49UiGgR1WiH9jlJ0oZUVuBOuyLYIkW5B7HXfTSfUGRS/JZKTzT7S3dJJQ+BFKSyBC2XBmw== +1u+XjG/2+GSQRv6EzCaWRQ== +PWKGbNEPxxxpGpe8THO244DGL576GmijVaKevMErWO119RbSnNIEkuBNIHeT6Ai2+a1pBvUn/ZOnbD69V27C2Q== +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNsteJ+IsYunNCc1YkZMiZyZU= +VmVrGQo2zRokW/ZuO9bN6zYI+LIJJd4lYJHncT3lTUMt15YZBA6AE1TLzwGllt4/isIlE6x6dAiSncL/gkfdvHbREBv3JQM4xVbHvZu89Tb8lVb2E9ns3688cxUHJQkPfBERrxPIl3tJ0LwWB6DEftjYpjjikJvLL6NGBvxA1yQ= +1u+XjG/2+GSQRv6EzCaWRQ== +YY5wEo94l9qfo5TPxQmUguohX8FNa5Am+zbgHGpyzEY= +1u+XjG/2+GSQRv6EzCaWRQ== +lMcV7pLM4V6yRWTmLUOCgZTWTB0MwKd2QFinlTk55lzWhcIOj8c6+ON1lhK0GZAnWhQuyFz0fu9WO0bbW8zmPsX79jRGERAgKC/0xJfqqTM= +9cHtK3WQzcQ0plYUXf7jo5qnXPqri9UiaWkgG1RZ5EpxPoOkZjJYFxNIQYeRlKIlewO1Ew4gZrnV0HQK55rPdfdd5g3ZS8nq2eU11rMtBfvFrVJwT45Q8gjcLpKo/O/OZfM44Ucc7wpE2DceXSzeBw== +1u+XjG/2+GSQRv6EzCaWRQ== +5wsVwKLrIjIWqeTNpSVp9SZAE79lgehXxNsrtfaN8Wk= +1u+XjG/2+GSQRv6EzCaWRQ== +cGe8DWc/z+TXH5eIZ06sPKkzzs973lpkVMCsZnaRSTMykUjo9hw9lXtwZPLAWJxA +rd+FFDwen8tJkKTf8UQJhMdUyYbtrdwamsCwQBeqM77gCC1coFEm67jW5p3AGGTFaEx3k5up907Bu9eID9IsuBdOvJyvhUZnVYJWja7Du7U= +DzWU5pX0U6cGCLLtlLj3YJU3Thb2TzBJE1jx5FMcRponhGm9owvR/LWtpuV1gqBq+oa72tMTaZWAJjWghMubjMlnHtotD7AxMClsn6ZQftEkzbOCLb9RhvcObU5RYT74 +nTtNt3MBgYNjJmpj+SJTbjmFU1ysPkgosk2DIZGJGZANd2VdAYGqOWvHYE/dQ7FE+w+H4dMtnj7ZZFpDBWu3fw== +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk= +1u+XjG/2+GSQRv6EzCaWRQ== +6wFlSwTFCI9eChOaieSFsx80SKDMht+X6uTKCTpMEqiGD6rExt4ONikcUUc/Y4vZ +YSlit6erKXylrjSZnvyW8KXMcZpc+ySBJipnAYUbDnrciP8x5aX/z37JXnXmIVnXeNHyWzgNPddmP33SYJutS1wQp6BoE3YXMy3wcBQpxeVVEPFYVgObLHXqCksjD1kH +YSlit6erKXylrjSZnvyW8BTZ20UXEQbIv8O2Jnz5Wos= +YSlit6erKXylrjSZnvyW8IK6PYdRu+CyjyKaHL3rmxC4EpZLip/M0Tp3pIiSLRDQ5ZNTqPsqEfAPwVyj4NZ3hA== +YSlit6erKXylrjSZnvyW8CA/pcANPjvtu+uuPfJXIprc2q4WoAuHzJfiQx7JYwdW +1u+XjG/2+GSQRv6EzCaWRQ== +r4lXdc7E0Sbufy5J9lgVE4NJfYY/c+gDUuK2Iz/JCT7wrjo+1KR4JjoPpqwZqCim +o6QhOIN2Sc4SHELnst17uZgjkgwXn1/M+/MspOMRe8qErqHrwASJCbkY7Zd2v7ukVyqyMVloFrPueTGSTHWs6ZHYNZAeUkN73bKXZ7/kk6L5CT68BmkH0qvMlJ+Y0UqJ +1u+XjG/2+GSQRv6EzCaWRQ== +r4lXdc7E0Sbufy5J9lgVE4pbGMag5Hpdnvj5BejBwvCAFwc1r7BOrfAB+laDU83a +o6QhOIN2Sc4SHELnst17ue7UgX3cUWMg9t/Eu2uBRoMOMUD0FIX1etHDPWEW1yOD1hvfuObOXulfzOA0+6HEoS7AmK9fWHTMUeLmI9mgjz3zn+KCwSLstzU6XXjgfy9d3NCkWIa/TP3YMIDcr9/9cA== +1u+XjG/2+GSQRv6EzCaWRQ== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +E/n3ybIG4ZImU99q/cGzcu/5uqUXXASkKd805ti+c5U= +c5CYytli+0H4ItySqYCTUXW313IrB73EGKOLQIMh9jh6rSkAc91jWwLuYUGr3xT9tsRK7xA1/niUlMsXlRBZCYbnJ9NjkwpjP+9aRb12ChiiS8NwbRQCckGdVBRz92NAXH/6NYiWKGxMl9ktE1am8g== +91gsC2HsVrvTm3cwuG3h9mG24K43yYxFv+xvDyViy4N+wu3NdWlBIFLk0b7CGRRA +ERFPcpFJ5x6x1iUKDQh3uhV6XHEL2RzE+8jUFszzqrcdB5G+ZfrbdykWEc9ZX/x1Ejas1dVOGo86IikmY7wyIYS6RbzzvRNojI2tYlyVQ2g= +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7JMh0e6jas9TdX6IgGuKJ8WEfWqw7FpYn6Vks40J9UfmyN68pAzdF2M2fRcS5j2gXOZYsvfx/4AdDv/SBK4oPL4zuaQmzHdrO/YOUGT0+j4U= +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9o2kvXaOAtJwzudc1BuzPhLyb33yC+z5K6zpgrKtmiPS +wgR07xfoapmx6eEnFHXXYo4se8cEG3Kpe+KryARC+5/s+COk9MOO9VM3V7stcJr5 +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg75lda++q/Brd6OdhuAQiMuzDqQg2rcamWh4QNAJqK9ZjVTK9OCy8SvVsEP817o9Aw02erW1GmSUt/X7K9Z4FB/Q== +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9tiBLFoSz+Q8e/mqFDjU1mzBbo/zwQDSGZ8Clmtl4uRF +wgR07xfoapmx6eEnFHXXYryMp/IOpAVMQWeh6o1xVJVY+WtE4kQ2Rf2ZdrFBqqhV +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg75lda++q/Brd6OdhuAQiMuzNINW594ZtQp61oKq9T7kSjfzt6WR7Ekku0esal+faO65KWAUAWl1VEgI4EUAQ2aA== +1u+XjG/2+GSQRv6EzCaWRQ== +UjJYObka+XFwpFaJTEtLqy29c/jy/AjNarMMaME61HK/XJRX5uPdMC4CfV5PJQpfy8h8NtjJmI2duXGfeeNjzQ== +UfmKuMUgPrxICqV/6pOXFeS+VYzMIR9DZH5eKkrr0p+eP/9ecU+z5yDwXL2SDsfj1A3cHOtWQNfH2vUCkEvVOg== +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7Z+2OVMl5+r4kRRoTlBeW+s8qjH+9UWHyNO2Kom9gBLD3Bnl+Jep2pKnim0Pw+N1RsviTmCIC0XUAq4DK4fjWLQ== +1u+XjG/2+GSQRv6EzCaWRQ== +ay0EttHaNaneTGWIcmFa++K73P9pvh2VSzg+P00CwohuGiaVM/ZapxK0+tdbyiZG +oacxXEoPih9NKsd5qduwcFVQqOK1fDx9z+cJtgCHbASOkBZ+RhKAHisYrDl0KzY8 +LIVQpNl3np6j6izGHqNuEtktf69h2ilj8nRc8mvgqL9P1n83aQsTnON2FL3CUilT +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +1u+XjG/2+GSQRv6EzCaWRQ== +P40wuHk7MslPmvVoiV0eZS9GHjGsA1BpCAEYF+DyKrg= +MfyKAQ23ShWgHgDOhnt00JYPadq1ebm9qznrho4FZBduZ2u7wkpwf0AJ8OAq0h9d2g9MARQ1tSItZxXz5keeiKcuHp+62maKGp8UGkoCv5k= +CUHO82G5MDpSEiqhNImyKyJX2KNfQKTBGoaamjo5u92wM337YigK+jqUrPMgpjyDCu1A4H5EjlyWyI7VjuJwqdmC21PoPZcLG9J36qN0EnM= +1u+XjG/2+GSQRv6EzCaWRQ== +0Cj55jDWeNf+rIE9r3fPh2gJStPMOTkFnyHb98nJDM1TVOasHYBTWXvTvlsc7cuiS6gGwRCJYYPZYXLWU3C2tg== +4nsuLyRybkZg8C3fJCsRPV1NQ1meiKgBF7vL5WvRnVWCYL/jOt5ljw6aC2uSGkFKQiztAIGFXkPKZ1r80cJ3j6yRkFhQ2AmAeo7arPXOC/s= +1u+XjG/2+GSQRv6EzCaWRQ== +I5csN8e3J/KIFkMa5t+SJENrCajRah1QU3uYF/z5CIDRw0MaiyCM2fRi6PpL0QdK +1u+XjG/2+GSQRv6EzCaWRQ== +O3CUgrw2GJfB+mDjH5+Ndpu+WAzgg1kWTsaUvXCjyy00gT44xpgRo/Z8tiUOas3LbAwwk8LiKg0Ys7278kAUyA== +VmVrGQo2zRokW/ZuO9bN6+UDkdyH1jvzci/SO7sIyeGh25hP2YNRS26DPmSNPqGYn9TvWug0oujS37FMzLac1yaEK1mqsx30h8eypGHDFxQ3zVqdRramRybhAQL4WEHq +VmVrGQo2zRokW/ZuO9bN66qOwalg1l6K9dBrmRBv69FxXJyYfs33xPcKtJ4DIqtnOOdfdzpIzeX/v8Eil+aQS9HpUwSxhuII7GhlYzNPL0k= +VmVrGQo2zRokW/ZuO9bN64gV4ZQ0s+PSF46pcysJ964ZDoTYVV0/vmjUytWdzy3HMJJ4WI0MS+Mk2qZOYT47fw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61ojevdazMjTwm0BH811Ps7aAo73Uk/kFpnGR4koCv3i +VmVrGQo2zRokW/ZuO9bN6769Xwxwzwiz/mrK8IQ3GdDReIRW0D+64vbRHuTCTfYoSATpZ/r/3FtrzvGxY2paCg== +1u+XjG/2+GSQRv6EzCaWRQ== +mG4WxLOsQ3e8HrREEjwO64P1IYQHnm+KlCB71ShayaqW/UVcvwq+j8DAec/g4/ZQjcl33YQ3GGaLUJauCOHBIg== +wlLHv6kT3Q/RmtMBN4nDAbv8zySb0rt8J6SFevmRPRiPnxbmTXG0CIobSeUW6QsVLFj6tZcRzTPaAfIuh264CQ== +VmVrGQo2zRokW/ZuO9bN6zG1efnzclkjRL8albVfDO8= +VmVrGQo2zRokW/ZuO9bN68zGYCiKFErhHvD5ZZAc/LfERjPE86j8NiF3cxijYAj0 +VmVrGQo2zRokW/ZuO9bN69jFBEQu7J6GFTIkqRwcgzHpBHAr6I+Sv94b0eCJtyiI80tUk/0PkMCZ2uHa8NQbUg== +VmVrGQo2zRokW/ZuO9bN66h1h1HHXIvvIkLe/lhJ2KGtNZQybFt634kbmBxer6uWFj64VfbtbyAxsXFf5Wn/Bgf0Nv6VQVpnzH2Z5lljXN8= +VmVrGQo2zRokW/ZuO9bN60nAH3WUKkUlT5O4jQaOZzLK+g4pNiP6/gmrX4QzzMwtpxBskRYTZgiVRHcyb9aijw== +VmVrGQo2zRokW/ZuO9bN6ykI5A2Im7PDglfBDvULTu6TRwoYUqLczuFeU4Fd0+hxu/tetbxzjAR35mOyQk5oSf63CeFF7EFJks7IVthR8pg= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wpw5gwG1/dzQpQyvd2GvdOv0cFdkQwVgF8NVJIgUMl8pk4ktf4DbH2S3bqX+8T+WA== +VmVrGQo2zRokW/ZuO9bN6x5ZLr54eP5z/wgx80I/KMw8cM7Musyjgn5+TkjEGWVOC+Qi8ffREikhJPkHHfHR0Q== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wpw5gwG1/dzQpQyvd2GvdNGvlQRjczPfMuNg4zFLM1sEIijzq1RF2KGUQbcWac9qw== +VmVrGQo2zRokW/ZuO9bN61grNev++OJbZOvcfBl4PSLrLDf60Lt2k5dA/Up9aoqxtke61MuczF5mGxNGw+rL5Q== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64C8lBav4E/bwSvzCtdtWHAolLv2+CayKpgVaX+yF1uO +VmVrGQo2zRokW/ZuO9bN60IwKZKFLs+S/ZVoYxLFr2V28ISbHZU866Ehjd8HsPu0B2ZnoRqc8o8ETwBEkWC2kxjSKTUrpP4gmiUaZVPp91g= +VmVrGQo2zRokW/ZuO9bN65qgzI0QqVLALq/Lz0RhHgW9m2ajcOiIA9uWs5v3wkUaJe47zA/PyA1JX76mEc2rzg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm8F5JEe+U2jlpN6wcqTIdB +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN63P6tOJ7fyq9rvq7kKcvasxFIFZVzWgxXq9T4zBQhhYX +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wpw5gwG1/dzQpQyvd2GvdML/mm/8WOdBeLD0Bu47qOqGUKnOr16K9DtoQn04QBDmQ== +VmVrGQo2zRokW/ZuO9bN6/yFPKJzHk83O/jODZ3f03K+2vnvt1PDTSCyctQ1wMHp+xW7ozrnPXLa/wSmrkiiQA== +VmVrGQo2zRokW/ZuO9bN62i9bsSVIvARJmHSP7/WrsjBOt4biVvSUkMpRJ+wzzCyNuc4hHDB2VhU+axCzGA9Pg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmLDaRX9/A8Mp0mJa00inWJ +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklzREPECe+UKwV56rejoQ6v +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN60LKmkQWzeSoQopkz0jYRYmnrfieabeWSr9l8AM4yxl8 +VmVrGQo2zRokW/ZuO9bN6yMdhcWuddd6xv9F64cBYywreWSVvaR5B3yfWI7OYuMP +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69L6dvVGvUfflzQI/giDj3rYEZFK+6sdVcl6KNxSjDS326f6psyHgorB4N+18EIwag== +VmVrGQo2zRokW/ZuO9bN66eoOYxnr9FHt69Wyrd/dgzp6XR6d82YdgshJrx/Py5fPIZGPxv4pJG9SDuEjnucTw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN63zC9uJg841KP8wNNYc+nngPoEpW4dX5XmdII7Ixe7IKCVHtaWfSpewZ4iVRMuWaoLWF+DfYB/guNXjHetd/U5WXSO+GjMdQOR0UDB/Gl+wZr7zyLvmtf7Tf93gY6dai6ggCM8g8z3uYxcyqMWRcD8M= +VmVrGQo2zRokW/ZuO9bN64C8lBav4E/bwSvzCtdtWHAolLv2+CayKpgVaX+yF1uO +VmVrGQo2zRokW/ZuO9bN6/qFtVlcLtzHiyT37CsGhUvQNduXSZfry/woHogT03g5GozglK/EH/TKFGumU1791b/zXvAjURRbnRCH+Civvnc= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6xL0H6enpMjxIH6Mjvd9EY84p4RmSCGGwgHSDMbD1BTjcBCdCjpVzlSZBpQCMMAH5g== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN62I+mLemZ/lLANFFnbNMu2g= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6z+i9MgIszQbZcSmYMZW1kYuYaSdWaJ3xM6cs1JOYdMrxMj2ddQ7B8UxnQg6ul3ioA== +VmVrGQo2zRokW/ZuO9bN67fwPa8UM9+lklG5XL66l1y+oEnsdYpYY3BhviSkIGec +VmVrGQo2zRokW/ZuO9bN60v8A+kLsJksGY4TM4KxnI1JTWXEnIvy288T8Vuot8uWIuQUXypVZTqU3Nofekpx5A== +VmVrGQo2zRokW/ZuO9bN64Tak+9Ph53fp8HAA81fuFDIyeivIgy79fEFVoRnAGBs +VmVrGQo2zRokW/ZuO9bN69qqX19RJUeMAQ5O8TuJCvc= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wmNFrhUNmptvf1kK8QmPZo= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67TRSqbKXcvZWR7E1jtoqMA= +VmVrGQo2zRokW/ZuO9bN6yuXF5mdisxBEft+lGUhyG8F5rYlcaT97mqb+GZ6eoQQ5ZBGGZdtVBOT4CUHnG1XMT3dkpnHpcwJp1zJ3xbTV0U= +1u+XjG/2+GSQRv6EzCaWRQ== +1drs/VADXzYV0BLytcpXyXvzCzd3QBLybXVI1a4mLzI= +9uj34YQ9oTVZUgtppiP8x+tmoscxj+/6NZmGBudbiIaCiMoySzRNQyuo/8kM3M/hCqL1p7DINH0366C4+5xMCTAiHnPPOrkfTtECCiXy4n8yPncljEYARQVoiaNzPkzc +X0OWiIeapcNJaErCRaD3DKR70UoHBRazkGuXISOiiA9VJx6mhhC4Bo5mbcp4rpHq +KE2kjumtDruFjPy8ClG6lvGC1Kx9xjtIzJwV3K+FTCs= +kCHcsIktkP5vF9f3GzzLlSczXK/ATFefbA+LM5rYQWE= +744+BgzK3LVsSzMxY5sSrw== +1u+XjG/2+GSQRv6EzCaWRQ== +W1zN2Bg82icFUtPUoqK/2RvgpuIEchVMvfneG9GmRQlw4qXPEJHuO1Hc2cvE97aVrCXYh0x2bsOPwDGhJEQ2dG4TCwC1a3mFXuX6s85eAsY= +xpUfQ2Yni23t7mfMo+WrgA3G1ylslCSvI4KnxvyhF15IAdh69hJzUoYnq58doFZmQJ5tksBHquIzY9szx7wKfXEOY26gQXhXGARuIBrUvDg/MKIpIKTq5/LA7W4EL7dz8Sx+CKKmU0xk9nfoQJ1u+u63M0qUDtqw1VI/iso5AkPtKP0DZ9OVV6E/C8YE9AwG +1u+XjG/2+GSQRv6EzCaWRQ== +OKUra5bY1GoJSomYqJxvwhNloJB8JlBZ/RbGtwTbupfBFwltjPLWQ37DtDKc8DzPDjUOlIYB8QWdbvfktuTRoqswQaXLKJJzzu9ntywIJA8= +wgR07xfoapmx6eEnFHXXYt1a1nlhtXQu0eU8+K9mU64UukT/EyLV3CdSaW72GYbSqkTsdOvPvfDaxvvR6aDbNQ== +32pdC9DD05OE2l0oXazDFBI8qzZPKE3wxp8e0fCnLEr4eLlpJTvCkNugpnFxO3SaGVmfaWnPzj8UbX73vBn/7Q== +1u+XjG/2+GSQRv6EzCaWRQ== +ojcUvW+Z2ehEJ6yMJpmY+38ZswsJGSBNc7eZcLWhwsDLUdSOubUICH63PGhwN4Ht +px9xYmGR/YR+e6NfoBzEdvxf0F7ihFpkhPP06Dk7Mn0jrenAkPhD+dFrRu8svS/N +1u+XjG/2+GSQRv6EzCaWRQ== +t0GuLt0m++J4JSv49oLSPu9CS2ozx2jEbn+Bf8wrMpk= ++pBWk5LxptzizUTsHnZAz6/A8P7Q4Hih05TEgBD36b03mDKbTfMQDzUISSEHfEIIz27Nr2qhocMuTnXAj7uQfA== +uWQGv+LShBH8u8S26pv5nDFBegwB7lws4Brqvxrrp30DC9VpmoRjeNwPIuWcYhn1 +1u+XjG/2+GSQRv6EzCaWRQ== +OKUra5bY1GoJSomYqJxvwi0hu0Gt46vEkfeQ0nYkRXamtAjDZMLJKCG0hGZJPejY +iBlEIelEq1jinkoBxXIXVhJunMbVgB5RBRAl9RjkDCw= +1u+XjG/2+GSQRv6EzCaWRQ== +e91oxhGuXYdY7jQndbCTnr8V385I1qnsEs4YLZMIhQg= +m5EI3+BXxkQEYKYaxORD+kNr1puCll3/YpxseNRKLLX8Bj2kLpq+mQmWMvl49/dv +JBG/gf6gnSFjRryYbJ1kBiSD8y+9MzUhRsXDAS14Q6HAIFV5lPml12zVWvAKyd4larKyEu5RbNsYxAIA7G9Gh+HjQn0vu4nmDmt/K1W8Y6U= +u7v11solBPjrBcBVo5sXZFFxgTGfWx7uaPShG8O+a9c= +wlLHv6kT3Q/RmtMBN4nDAck3bsisbgfoN3mG5s58Uro= +VmVrGQo2zRokW/ZuO9bN6wNOmDpce5aCei5dlVHmLPwk6N8GJ1S5J3zG4sqyFHvj +VmVrGQo2zRokW/ZuO9bN660v+GJbhUwILTBKZ8XkQOw= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+Ei9AA41PX+b7cBXziSnYY= +VmVrGQo2zRokW/ZuO9bN63P+EUZOI45PqES3tmisTPmd7QRbNS6wsDKOktQ9Qwo6 +VmVrGQo2zRokW/ZuO9bN6zZcpHqbLPCfucCocPyjGlY= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTBFSFUBK/z6XBzbXuYHoD9Y= +VmVrGQo2zRokW/ZuO9bN68bptVQRtuQKW/RUj6bM2TTaszdj81ZCGhRHduuV/AXO +1u+XjG/2+GSQRv6EzCaWRQ== +ldwxuKUlVGrrnQ/DH0TE/9ss+kcPMclos3BsGWAs3VHwskH2OyERPPvVRAgIHUb6TsiVAYYwFhIk9NkAztVB6KuFvSbmhoFLMk89ZRmcqAc= +RsEWPElNhlgl6zgKimcjfSbwxiMtoleUSzTRCkP1AIxvWQAirNRm8hcrkW9XflXT +jLPO8fH2aVlnrPDSrKetO+O96gPwjFf9jB/ILvVUxcan+U+sxJWJeGEcW6LbrA0XqYpfvy30BoKAB4GIK4CKc/BtKLyT0lpxI2afDBRQElqZpFLybRrjCwb+8KuH0RdjTyK3EhhQL4X3vkvw+maTo9bnFcGz1OGk8Wlb5b/5RUY= +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0U8Txq6Fo/snJ9Tr0v3OFIX6+VFuT5OBCASomg+3wpXPMEgx02UHFWj9NbkE0bb2tBPswtaZ77xcVwGOASrND/nZOAvUfL/N8mhgGkmWSGRidPHCmdI0h+E21CM8AFLq8yZthnUSsJmzgW/anvD3/zI= +1u+XjG/2+GSQRv6EzCaWRQ== +CP4/PJPfBY94m+CkqaLwMTJpzAeBEAaMFD3d7lqw5ek1rheRoPP/eokYfGcv/shA +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgZHHHGZUr4XV5KWn3V2+BQs= +gv64ikBFr+DizEWnHVZW9Bg09e3LSs96RnSDTeQ1bVwOHOuybfAFCD+eEmSo72Fru+D04igpr3dfhJ3806gMl3ENMPsBL9xQkV14Zr4pPRoSu7w9hWUcKiFHM9yj+wbyVxJbC53MD81QSP63IgEEFoe4PYNQmIwaTPzc+UW4ys4= +1u+XjG/2+GSQRv6EzCaWRQ== +0xDm24SjFmBL3vgymT+RygfT8TfQMNNOTMYZrjS6A9U= +ViTQHUTsV656/tTtMADrUqxCJoZGngdZtIxdupx6nyguPlsEnxGX4o+E22z4ctMI9PhYuYVCN9lUwg/g/t0PiVxnNh7bRp7ic3D+rW1Cic/4KwRrGKHSKqTGu0n7MFrvSL5uLm57DAoiQLm6SuzqrV5Bo6ELSXZQN1dAqFfzjWQ= +xpUfQ2Yni23t7mfMo+WrgFEGGpn7jH7cqeEs0VuGAUlAkVv7N8Ztw2Oa60GTRPc7PzARH75k7n91/ateYBUAYsaMPsU2/eWzuwIzumLPppcmxBG45yq3lTvIlRcVDnIemXYDZf16D/+mSrCSWyHeRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYt1a1nlhtXQu0eU8+K9mU67vJcBqbgpDHa6PsN2xocPKoSZplQYphlyDd+FL0CZimw== +NqhMbY8+EQI8zhTG6Zh2I48Ny5mHBYhSF/P635IIeMp6U8x40vHLxmBtCXUxdRRuagrLbnza8Kq8wIPvwaTHsA== +1u+XjG/2+GSQRv6EzCaWRQ== +25O1h/mctlgTNH6uiqc0AI6rCQnw5P2GRWLl7a4W2pTcvbZZtenhSHZ3vHLzp/eMT0zUIPZpeJaKJMcOnEfqmIE0for5m7KXgPphCgOelx0= +TOmaqtOsYFL5TNLmZ8scY9SHeqrBU/qQeXsmQsOrulUrGmLO1VPihVArwJhDHgZRu9RXdMbbX1kJUQkM6q6cumuKXi387px2dYvFD/4VFa+f94gsL+L/btnyGh0L4M2pDkRmuIcwaiqlcsIf4KgnIY8afBHqGYrTDxY/HYae1YY= +y12DYNXPkWUkYFm2zhC8yLLgaDKZ2YBNHmj1IC7i+PWELcnDHBN89kIEVkNjg/6HsSTbiIe+Ni8nZbHxOKP3qBrZc5/BmEwEtAEREOx3CQw= +1u+XjG/2+GSQRv6EzCaWRQ== +ZoKeJHJp0lAyGfZ4EoQS0DlrjPMXcXRhA57y9yFTzJks5qWSK+q/Wu+OFCA5Myw6CGm/EiFmJyKVspUmsur4Qw== +suEadW8BDgV1o9Z5BuKrKHLRnFgqfxX5X/vN7SnA+LjV7r1W48urFvcqh8u97Z9YgpSaUvOjgGHFP8Cl6/J5M0onL1QKwDHXNv2TcnQDq04= +1u+XjG/2+GSQRv6EzCaWRQ== +XZFXH6iM7A192nPekk4XWT4O2Vr0S0DYQUZxBm3mwEXig1jmm4RGVIBZDU+kWW8Keuztv39rUWbD/HIHWpM7J+1IJdA8TMhrsA05ofAGlVOVqx2F3jMieC8Ah+0l1nEWcwL07jfcBx71AIpuA+99DA== +XZFXH6iM7A192nPekk4XWdkmTnpMuTSimxW/K7oQopU= +1u+XjG/2+GSQRv6EzCaWRQ== +MfyKAQ23ShWgHgDOhnt00JYPadq1ebm9qznrho4FZBduZ2u7wkpwf0AJ8OAq0h9d2g9MARQ1tSItZxXz5keeiKcuHp+62maKGp8UGkoCv5k= +Wk+6iLfu38DoetOmRN2a9zm36qqahYh3GbOZBPQLAUU= +32pdC9DD05OE2l0oXazDFKN8+yCaxsULylpVUl/0HV4COaEhRjguA/YfMKVNRN63 +1u+XjG/2+GSQRv6EzCaWRQ== +I5csN8e3J/KIFkMa5t+SJENrCajRah1QU3uYF/z5CIDRw0MaiyCM2fRi6PpL0QdK +wlLHv6kT3Q/RmtMBN4nDAbv8zySb0rt8J6SFevmRPRjW4HnLs1eBZMvfLGUg2BZlUWqZNZq9fjH99w85ckrllEy7IWLdKhn/PLzoLzGjURA= +VmVrGQo2zRokW/ZuO9bN66JMcTYjLDaEhZc5QfC+HcA= +VmVrGQo2zRokW/ZuO9bN68zGYCiKFErhHvD5ZZAc/LfERjPE86j8NiF3cxijYAj0 +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64AVDBbJZFuUixKENv+dwAbqZLdwlfh8v50ImdsaNeGI +VmVrGQo2zRokW/ZuO9bN68r7FXIh9N9oGEDRCUcPBt0qACIF26/Ycmbt+3LwhVJKfm464IkLzDIIBO+eIgYOyA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6/faHgOQ1El6IVDk+PhHdmVnlAGxE8JxvWa3QmTV+bwv7NvaXvaMdSwwry/1nVJ0yw== +VmVrGQo2zRokW/ZuO9bN66OV/0ptHLZhPYaHOj8YRl5A/bXcx6fkZgocgn0vAzTUX9F2uupJTD9l3Mp4LUOIHg== +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN62XqkKGTDRfn0OWv6sJzm4qF2iqPkaG7HNnKt8BzUtyb +VmVrGQo2zRokW/ZuO9bN69NWUwEfXLkEPnQ9fFWACgH7kgttpjff46QD2i1lkAAk +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61xFjNPd/WKO0XGcnDIhrwycJU+lc9EH1KnlBMnezwjWhKQdxgqIa4H6NCwrkbuAokS+pvMEnV/He22zuiaNSawDzjChYNKzff4zLkxclW0A68pLYvoDxzvdMhPxn5DbBE7pgPXn0BZyWBQpNsk/B28= +VmVrGQo2zRokW/ZuO9bN6xwWsPypa8PYwj7rMZlyoveCzD1mjwcgMRlNoc0rLxxG2LLPlC+K+rlLmKPc06PSLs0QAW55V4Db72PowA9Sm90= +VmVrGQo2zRokW/ZuO9bN6+386QxSyLES4mWeh3pHPccCibSFdD6YtDiEelWHt3EvrfWQCUec9tVFL9nxCv8Uaw== +VmVrGQo2zRokW/ZuO9bN61ww1Wg9v6SQgGomejo/JshqGpgU8Czj3fwjtIZzbuqb +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6z/YDcEJV+9E2VV7BjdV/W0kxu7gz/Ioijd/ulFe6zJK +o2sgQHazONPoJDqmq4d8xsjERPoWH8JoaRDeqyIl36gdDkp5QJvYO+Mla5bvXpyb +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyVnGSQtPVRcl6g4dN2SK/XBs+Q2vUN/x4CB8BkgNPUp2A== +o2sgQHazONPoJDqmq4d8xn+5v2tTUpCO8U6fXW/R+CFqtIFVblZn6Oh1NOALumM+ +CFjPhNS+4D2oRoMxN/lPRA== +VmVrGQo2zRokW/ZuO9bN6+WAMYleJHArv2JdTgPddeNSNwuwagPD3vkAdGjn3ApO +VmVrGQo2zRokW/ZuO9bN64U0AMlU0Sfqpa3U8qpKk6//YVbvPdfGQek6eA3HS2lEMe1HCUztEMLPcmvgKeNUfA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61uBMsTpTUkJJoJUrQMnvvEOHdEUEFa9ZmUMrKXKiVcc +VmVrGQo2zRokW/ZuO9bN6+UAUlGnbw4fLUGyw+bVh4OKevFjAbzmsXlmSy/BSYVgJxKrV1tFpXuLT8/bMGQ81g== +1u+XjG/2+GSQRv6EzCaWRQ== +mG4WxLOsQ3e8HrREEjwO62Yeh/Lh+udVxYsIfU1ww2xnzlp1pf2VhkLSTbXZfs3fQMGX9Ic2TYevn9ztU25MGg== +1hs1FfIPISXmd7TJ/r+Vl0XS7zAjeMWVpHR+5RwPhu+EkNr3Fen8dHIwweuIX6jarYBX0Q/a7jm2WjX1GDQ3cg== +VmVrGQo2zRokW/ZuO9bN64LUNt5+YaZJNuS5WgwZGmdxtbQjBmmmaCjZWUBxtK2obCGRtIZLGrpu9ulIGnTM+w== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgWfgxG6pllgEDPQYhOTZb/cxoDs8f0laSCoj68mmXhkD7ag6tNFtPFzS7gFJMsEx1Ao7D838Ncfm01V7u8qwt4s= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +pFACfzWgrgLSGEmbkBfExw== +yANqf+EL6aAYN/o4EmjjQQ== +a+/w8Se1Yte7BhPKfcDwZNINWOdFKiSbeGWgIgEkr/g= +hIvXLCqYnsHuuByFmIsauTDkvy5fXysrJx0It3A41OvBgYEosyTjKiuSI41dZd6tsBd8tpijCKetDUannEFBKQ== +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+NKPudEm++RMQZs3h+yWJpXSp4aRpFh9wJGsBkiKsHmF +UNp81wVrUcRkYFwXtaRvZm/YdNzLjqLyw/8a8f96hEtFZaYldoJr8vIZPdZBHs0i +c0ufQ97F5najN1prFovGfmBX2ywnfIpvmeWPFVpBmk1uz5P7fyb972YjO+kozxkW +Oi82qoqybzdF6wkjTqWiMzGzXQL7iHiBqRLBkHw+BOsdRkDMJkM7UYT5cqNiuIZd +Oi82qoqybzdF6wkjTqWiM5H4nyX+z7+K42rG5S0789sDzjVbj36Gw1sRYJ87eXPj +bLlixn6igwVr7WeYa5jmwH9SOJuX3Wjo+qPT3h4cv3bHukJiNa9/seXqbhGgVIwJ +cMOsEjqfjGgKL7ZBm7gTSqFnNa4a2DH2wJl5N01Qd+ZqyYmYIer4O71V8EprhJLj +U8F9NA+lyS3OvZlsASE9k9Chr7r7iXeKLkL6J5ppGVw= +1u+XjG/2+GSQRv6EzCaWRQ== +ET7Gy9IHVEYnOggxHAkB1DZscabMsD9ayftZM6IanZw= ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +5C9Yloo3ut0BJGfyUPKOOGLitTrk2UkEwS6/wHrPeZTeajG9pp3nw/V1UtbKgm/wWq6pTnMef2sE/Zt8bfNWGQ== +2+RceSNjD2iq0sRzepMysTdyYOYPWGVIEQKLtGrplu9S3WUPrlF4F3K1VySjyz811mbTNYE2csIBAgRz5Ehh/NLrm63S6dUF9fRWg5WIGmE= +1u+XjG/2+GSQRv6EzCaWRQ== +Vew/k3VsEDJyMKn0A5V2yxQvKKRYaDpPXmabxgnb1uQ= +cCKGdtIe7qSNBJ/C7EH9q4NUvSaLVcv4fntSYZbf5PgrCZHFpjHikwuGgCQ7a8ct +CAhn8dRUItEbEErp4w+lX1dpCvDJRAzR51ESaD6r7YQ= +1u+XjG/2+GSQRv6EzCaWRQ== +dskxeNlXSU2ewA7cm6Iz1V7JqWd6vK/Qszm6LObtmBE= +lH/cdKlrko0tsugF0s+VlniaL1RiUd+5AR4RrK/htuqUbacAqR1Ym2pyjeQHc6OZd5uL41MWlXrjTf3rcH0Tbg== +Jlnomz8gC+UNHsk1SduKX6rHscSuN/Jdn9G08BtBAns= +MzOj4ZiBOJDNVP8vi/lFQGYgoB+pZfuVXVBhng8CoNCSciBPOhTxqc5X/8/ufOTq +L0eUthVnpkGsmKFAX6d+uLVEtHv2wNMbXwfSUCMWgaZUxksZvx3hzFGnkfjPDe5X +1u+XjG/2+GSQRv6EzCaWRQ== +LlqFg7Hz2WoVahwCYiFOVp2SFowQjlNc88+tU9937tw8Zjq23jXmPMthMOlW58A/ +dh7w9F2NtRbFZXQAT43UNN7/yW9jpPDxwnQLlmXSh2N40SDWq0C+2vAvknDDgCGKsXcDvi/C3MZbXoObOf+Ygg== +1u+XjG/2+GSQRv6EzCaWRQ== +U/sc+Dp+fKf1xYip5RqQssF0Fd95YioZhS60mEJ3FY6o2r7WFokXUxFd2KBNB9lT +MfyKAQ23ShWgHgDOhnt00A3E2JnCEMGROSk19XPaoa0gcXfpvHAqoqLv8SuAItLiCiTBC5iEEIXHdub9WG7PGg== +NhnIR3Ilo4H2su9/cTNo/GRiB5uAzdvXt51bXn6abPQ= +VmVrGQo2zRokW/ZuO9bN6/6Egoh7rwbwIEJPbUNotp9g6/T7AJqkCQ/JUwwo5DBpYtW2vRgHN+p1M2uayawcABvnATETsC89dTOUwleteq7CSs/+vcTas8hKNi2ti7PFfnx9Oum8Y5lu5leSLRnVP/9IO6OsuBH0sUsKnduvPS2vDCuWh7vY7zRWtXr99njZw7K32ZVRJjXecZTzwAYbgj+IN48y9DC0myUpul6GVEQ= +VmVrGQo2zRokW/ZuO9bN6847gMZFq+X1CVFOJf/pT+mo9DppVDgxkhcyIarAWXGi +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTNvoC4FCbRFpgf4tShNvOpA= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNslPKOPQq+IBahjd/hMJqCiCk+goCSiQ5N0FaLf19fW0G7oB9N5HuTh1mMYm6ZbTeHw== +1u+XjG/2+GSQRv6EzCaWRQ== +o+Mm3OTJQ7VavMzv55JJwTvkG02Ttli9fAvVMx48bZ2YJxgQadT+J2VrpkCJvGhQ +Oi82qoqybzdF6wkjTqWiM9k078F6fAbJat2dZXQQ1xJEY+8BmLFgGoVrmrIJs+DGBf+YDxBHW/HjBSLIPpwSjA== +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX9WKxjWlNEEUlzMaeHc7B8uxI4F0Afj+uGRT7LgIEni9 +1u+XjG/2+GSQRv6EzCaWRQ== +qWX2pEoFG75csIjWZUSgjwJhUAruu9izeIs1xQGcCWxWjqNcvdO8BGNx5WfO1hd0 +lH/cdKlrko0tsugF0s+VlqulEnFS6q2DYv/82WE5ZElbXOBL1kK8Ov40RcDImlSNwtBivvHWT9Gbu/VABsQTeDiQ5N2axHUvhqWBJnpn30M= +Jlnomz8gC+UNHsk1SduKX6rHscSuN/Jdn9G08BtBAns= +MzOj4ZiBOJDNVP8vi/lFQPj9ZL+Si//Xr2gb0mal0K2sWwzWsizCOg/F4gWV4o9h +L0eUthVnpkGsmKFAX6d+uHyOyOwOgs0ecjP/FgbXXBdQVhOhEgvbdWIrPXjxZNQF +1u+XjG/2+GSQRv6EzCaWRQ== +LlqFg7Hz2WoVahwCYiFOVmFZDDcvw5fBwYpBCi14Rqxfn0LfxmtnRzCC82/xhjmB ++rhAblUdSVCAIh3rTkmcASyeZck/OO96uXmWTAZ4zm/Q9T+vnYHR6XQcwVMJL/vb1LCtSEZc1lQZXonwJG9uLxcBSULgn8ihkLKtky1mYbRz8ZGN+59nvH6CZfaYuAAo +1u+XjG/2+GSQRv6EzCaWRQ== +cbvASHjpysrsjdY5RctXmK6Asu7/18oVVR++ajkLB6BcjXD46xvjCBcEqsP3yQoE +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7PqEY/t3psY8Uj1eii891uKc39XUkAfHgY3XYKmYGqPHyhQW5dM5UDcsRKuOpYsPpu0NLY26QfPUKDlXGoiLz2A== +1u+XjG/2+GSQRv6EzCaWRQ== +PWLW/WTodw3tqQQSBBHIn29ODWH9FqA5eR600Y+xo0xCdtgSloxGWdkfyaDbipIWgUjK2+W0phGPSaFhYmz847CjnIC3/8AON2RVQx54W6w= +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7PqEY/t3psY8Uj1eii891uKc39XUkAfHgY3XYKmYGqPHyhQW5dM5UDcsRKuOpYsPpUZULeRclDszb4IVEjXvOpA== +1u+XjG/2+GSQRv6EzCaWRQ== +Oi82qoqybzdF6wkjTqWiMxht3QXhh4TVamJ8+MzbwHaM6BeLsJ8AJnio/H7aSc8KP48VBVkQAlR3qefPdAOfVQ== +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX0vlU9xLPcaearZQOaPco3nVqTdZJNKgT9eVQ7sL5fAS +1u+XjG/2+GSQRv6EzCaWRQ== +qWX2pEoFG75csIjWZUSgj0O7NRHjDcE4EqF3OLWM5jqn7Qhzf6Np6D4hExMDEEd+ +lH/cdKlrko0tsugF0s+Vlh+uyVPTU2Uu1gdMx4iZDrHpuAWG6+Tsy4s4PZEAUc1y+uInpZOSO3zBzsnGLCYv/w== +MzOj4ZiBOJDNVP8vi/lFQDUGZ3T2w79vfb30gIlWgcI/U/wia2WXTY+3v4RNaupM +L0eUthVnpkGsmKFAX6d+uJJXMK7bnkWfNkkHaMuf40v0BJ979EinOdjlaZcSUibY +1u+XjG/2+GSQRv6EzCaWRQ== +LlqFg7Hz2WoVahwCYiFOVp2SFowQjlNc88+tU9937tw8Zjq23jXmPMthMOlW58A/ +gsfPduqwLLZC7wml5Sb/DXLi7x86waTqKKYtnhY5nX1T8vwbOg1yx7kX6DiwXwrfL+TzZuquclIDLq0LjQmoNu0XVwkRjmq9XntioHniSc4AOqE2iBTWcFhpHFjtOoSexqsySAVm27Qv5imvX3H8lXAijnGFLSuETftne4e/OQo= +1u+XjG/2+GSQRv6EzCaWRQ== +cbvASHjpysrsjdY5RctXmBxHhDN33U2ca26UAHq/cDOhaioL0yjMYh0suHii9extNPKdBjmaxeeqz8myc6umvg== +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7PqEY/t3psY8Uj1eii891uDs4ZxXxn1Bp3/ABNZpsCVE7qAgF1C2TYvgYMCYeV5r8+YvocCLcXd1tdEpvG0QWqA== +1u+XjG/2+GSQRv6EzCaWRQ== +LlqFg7Hz2WoVahwCYiFOVhl4zdxkeNZF7Cuse+0PcbBf17g6+8T1GnxOm6vsIgXx +gaoPpiWV9GllNK+GAvAUtpvPcfa1PhfYTPixiCNsuAqwLRMqFKiMs5r1IJtZaKvKBfR3VSXXLYmfsb/Qze7qh01mUYHjNT9tVmbOkbVQYC70NG7rIQ9xfhaLhE5RSWc00LT/X4QzFd0rW+x2/wZyeQ== +1u+XjG/2+GSQRv6EzCaWRQ== +cbvASHjpysrsjdY5RctXmHG82Mxk8G6rvXGlYzbZRl3d+/e+Yyy+/LmfBmrPwBQU +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7PqEY/t3psY8Uj1eii891uDs4ZxXxn1Bp3/ABNZpsCVFym8/219pens2+ofUn3qi39ph+3SfK02d4PPkbpdl/cg== +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9kc7XgKtI0Il8Zt9Rf6Ft60XaD0Uu4a2UrbOgQZf0Z0v +ncUn2TP8ZQ4fZMBYk+CUrQKQSyulUXTZ0VJpJnAjkq2lI6PJcmZ6seWXTmvT18Rq8TygwMefm6lKCBBQ2yTgtA== +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjui35z8/dEnrWz+BRwiynkVz +VmVrGQo2zRokW/ZuO9bN60ZeHVJUIFWfcACCS5B4b7pMow27nbKKz31iyngemwh2yodnGFPvjpb4Ud35FRSkt2Yu8aFIiWIId9spbK1sxpS0ZzZmgC2FAJ5wBl508R6I/HJUkt5iS+se33KMh/6PVA== +1u+XjG/2+GSQRv6EzCaWRQ== +U8F9NA+lyS3OvZlsASE9kzb4sW35eeYQeqJHBbSnWjmga7nQD/669+csH2pROqT9ru6xg+Sx9SeWJRtXGnrTsepUEp/5a4sZetB/Udp5RtU= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm7I187cTv4QxxxILH8LA4BZXZY+H77XcyVwH1IWlBrX+LygAR9eYXn54DoxKWoVbo= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn4epCnnjOljy7/jzvVjgWcYrLQww6VKP0VMUWD+J0DnA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklOwYH3d3YWdlE1InyBQMAURBKsDFIjP2dweE5ztulUubuUADmhyV7/VRSS/c4GFLE= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkkyFIxg24+ePD95zE6A0SC5eRCOMHRv6dtSVuImhmJjKdo4YpvIWb8yLMMif4f7Wwo= +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX0u7Y5xoMOF7x4YZ9MSjXqk= +1u+XjG/2+GSQRv6EzCaWRQ== +qWX2pEoFG75csIjWZUSgj59Dr5jY16aBxRjMyk2yTPNl5QyOGBj8kMfYw1Y5EZn9 +lH/cdKlrko0tsugF0s+Vll1ahjLcp1f0IXErQDWFgfL4xooQZlHIRJBmA9KbC3EN +DLVzBwuz4PMv4vTgkifUTJaKr4VJTYryoHRFaipUjq0= +1u+XjG/2+GSQRv6EzCaWRQ== ++pBWk5LxptzizUTsHnZAzxAJwezCSSmljUZUExfm7Ltyf0Kivt3Uq+r4F0WaGQURcGzsb7jXe4cn3NEOv+qNqGCcaGtT4Kd+raV4ru0OCqCjFS0jK7uBlUY6LjNe2DEI +E5z5ik3n347ddaQbLVWZIxOdAncbCwTxI+lXPyWbsa2mEbmC+3K4uqRRbIUbdZz5/sqa0ZLUM32ZUEi7ZObJP1JYt9Th9StNaKGc2Wq8onjp+EOGVoUwTC7dafiMNw+3 +1u+XjG/2+GSQRv6EzCaWRQ== +dV9xz3yyeXnMinI4Y9Bh+pP704aButokeFyzhY64Ypc= +VmVrGQo2zRokW/ZuO9bN6/mq00FvIOFaJg7q9q2tw7KFggB/xekQ9z79QdcEiHe2 +1u+XjG/2+GSQRv6EzCaWRQ== +3PYuJCOiZH1sjGKjxSVU6PvW4RD/wi9fWsjJqzCWS5A= +1u+XjG/2+GSQRv6EzCaWRQ== +dhTtvhm0jvbK9rVEWF62eq0wSd0tqSAkKyDFU0NloTlGdEGP7cfJRaTTNDqFAjTo +lH/cdKlrko0tsugF0s+VlmqAEmeW8mxbsgnQ+fs+BARMVIb6SsXuoKuj6IEW9bVB +2yQsUep5WrV3T3htNZ1L8TIohok8dxSGY22+WeWcmQzlauL17DyWRvW+qqAHUI9z +1u+XjG/2+GSQRv6EzCaWRQ== +w2QuAWUFVSYjyNKuLr0IKmsft563vBeXEDVw611njJAhYVSu44IunqOLx4+4z/7Ov2Y5r47WX5THcTBwc4oozBLv5hZTs4wx51qgMMdywDqB94p0Uk28e3pWmve/zOF1 +1u+XjG/2+GSQRv6EzCaWRQ== +p1hQ9hJvEa/OWih75Q+PzwZE3ySonErKe/TaKmGA9kHR+n3lh/osL8uZNWfgFiwiJe+mK4RaEeGSIFW81Fuhkw== +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7PqEY/t3psY8Uj1eii891uDs4ZxXxn1Bp3/ABNZpsCVF2I8lCbIb1oQvEpdhcx5jdbysDj6lNmlEcJ0+fbwLlAw== +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX+1wdJ9DeqzVohVbiq5d4Ak= +1u+XjG/2+GSQRv6EzCaWRQ== +dhTtvhm0jvbK9rVEWF62eorElN2oZAqdnjin8oXill+vMtu2wCfGYc8vhb2R6xoL +lH/cdKlrko0tsugF0s+VlvMo7d2fvypDmHSCTjtl9paGccSVAo8TjM0TcxWNXhMC +ojcUvW+Z2ehEJ6yMJpmY+y/kPPkMMDIwbG9jlBcEHyvRbVVvbpVbyGkx7eQsRDovsxUnDVNB96kBbUJIwS6XXg== +w2QuAWUFVSYjyNKuLr0IKkWdSvA2IEsC6IP69ni3Dt9KBDzXq3HV2ZNiJdU7uKha +1u+XjG/2+GSQRv6EzCaWRQ== +qfEqfdUCJW3nKBiyck8MpMSixdVknz+Bevwkyv5V4NhOEJ6ArEN9ueCKpL4IwkP2HvP/82lZC3ajBAgYswy+OQSpghte2vT00WwoZhjZPGizZ30T6wB/LuC1skNFfOVcMwHIgdBzwsA7U9aZh3XpkA== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYj7p4q9ubmfYsOireEOtrHzJOhIN10EiGs7jKzal6Y1T +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7PqEY/t3psY8Uj1eii891uOyslvfK3h1sRo0wYiAq3Uix7ySyfwzrlxK1gaG3GvtU69/JVF6hzcFKY2N7uNDn6eOoH28BDZNZyFZ6jWUHjBA= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwtpm12B9B4UugWr8Glp8zlE= +1u+XjG/2+GSQRv6EzCaWRQ== +7gazhtuMiN/qSCX/IGFtRE7Ee7E+rRtONmW4sNnlMhv2ia0VMYv6WmjhQ4wHKUgp +s8Fw7TvFZZTIDpwLQ1IAjjgB8auuxWg3yZEiM+5W/FSiWXgKYISQ9RWGOE+9wiUJ+V7JhJagl8hF8nJIukTRVQ== +FPr8PM9GCeXhiW+GMolpg7lknUtlxGqMTtUxkIXrE/hClDQMM2jgUA1EnRweVAOB +AKR+OhMsr76k2SU3CFoUdaj3FZP7qeQ4MaAUD0QhqThCvCZrb+nfwpLtAjEO6uFzfkwvaEOodZ+BD9j6W7IlGA== +F8dfsz4u1nt76GApBgNQ3+j+HymT0HCOV+sBsgFQ41+Rupr7aPc5/xikyNGSjMP6JrWTseZOdvg0ZVzNo/D3hw== +4hdB7RJambPsQ0dtPl5R2VvAzymSmu0haJp04GkS5OAMVZlakrsaRowJvil9BL5r/ZhYQWERVuum9UAZsSPo7waqRr92jBas7Nw4DXS+wZDNDLHb76EWCXqTHm6XcNfoyo8ahELlA2cWa7MSEzEPCxkrdlRItTWrzSEPoLeZdw40RCjTmW3gmkEPKnEs+yDHNuLbKyIkCmcOxz0Ge7Otaw== +1u+XjG/2+GSQRv6EzCaWRQ== +jb7/3LG719n78U9xnFeh4LiEHOioATtkfGRHQB81/7Z4E1+uxuEyNUu6fidQ3Vlh +lH/cdKlrko0tsugF0s+Vll8qpo1A+xIOWPsv19uguu02U/gjbpfnFn/7z+3Y44j3 +MzOj4ZiBOJDNVP8vi/lFQJzUZ6bZC6athN10lm+cgOzKJbw4R/m8Ylk/HYpP2jk4 +L0eUthVnpkGsmKFAX6d+uAW2pCR1V0qEjRR33nKvawkas/SxD3lQ+lzmDscg76W/ +1u+XjG/2+GSQRv6EzCaWRQ== +4o9XYhAkGdVgUbgn3dFO+LiWBu1bwE9HbEt1cxrZevXmSOBR5MBG6yS17lBOi+RQ0KPxIRd6O5AfpuyF+l4G+lAqp/9GYcUTN12/RaxY22+t6hCepIkb9zPuhg1fV7iFhOCjsr5LFB1jEt1WAtKEroDi5qtkwn6XTLjS68fzIEY= +1u+XjG/2+GSQRv6EzCaWRQ== +oacxXEoPih9NKsd5qduwcEnzBZI8rfgRYwZgCqWSvBGiiXF2EbB3tngSNuauhDoYsgdypq6PIYZcPcmu4EyANWpT14Y08zIC8K6sfD4vSN0= +wlLHv6kT3Q/RmtMBN4nDAW56rv0fhH2Po+f5AS4a+NMzTaXxOIC5lp0xI5qC7yDv5UOOmqDNpFSWktZ2kEGsnA== +VmVrGQo2zRokW/ZuO9bN6/nDTUc8fBZlCkrIOGMhjTWQFJ/mZhkgafwOqjQycpdrHJRvy/Lrf4S/QvwlUxWvgV2xMN5MNnW8gVCk8DA9ZX83j6lcAeeQbeP1sS4+Hwrj +VmVrGQo2zRokW/ZuO9bN6yD5x1/xKggX5H3Nn57LNcimjPWeOd1OaroKeyMqLvPJKjNl0qn2axptrpFkpe+DgNNz6XloVJDu/NeMg1/r4ww= +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN66/eWUNGeG4mAVR3lxT33mY9hv00sP6Yubjx2uWFFRrcxhXU0VrKGI8w7aahssUcPnbNDt7qcKleBLwUgMzW819hkolaKjyV7mHW6qURhPyG +1u+XjG/2+GSQRv6EzCaWRQ== +J1UGlXkKhKXD36OrXXhN8Gl0Hlqu6NDmQFs2CD4UxLqvf/AxL84caGN+U5x1Yqze72o5+/3hF9hFxJCc5yCHHA== +VmVrGQo2zRokW/ZuO9bN6yRuj5F4YDEZPrdVCrZ9+PSbvDfWooLb6mtaKOP5Nmoi +1u+XjG/2+GSQRv6EzCaWRQ== +OKUra5bY1GoJSomYqJxvwjh+I9EVEootZkQ97c/eZnuJ6XSrvy+x4OMHtZmctwl5o8YIHqt5AX6pXEdWhPQMXEQxFFB/NI2X+p4mC7ymMJww+dFAvYIfbK8Q/EBPnp+s +cMOsEjqfjGgKL7ZBm7gTSt9qlMuDQDwGqeFxYznl+wUCFpSJL/VyD4LKst69KhPPzfBMCjfAHVoyJVkUsfZPDb+bnw6euvuykGIp5tfVRYY= +CAhn8dRUItEbEErp4w+lX04zbFiGxfA+WrDRXUky97CgGgJ2JJmEKblfan1r18+Z +1u+XjG/2+GSQRv6EzCaWRQ== +jb7/3LG719n78U9xnFeh4Pj+0m6Z5MGg63BB8B4QOT6LSlvnXW90vRRgaYgRlI8d +lH/cdKlrko0tsugF0s+VltE4SqdKh2621JGNleXVhKWMz4UY8sZXz/wLt+QZWChE/IzuaRu7FkcvFrdqJGyD3g== +TDAwprI7xAg/i9h0iFymfWWcHaCFydAk8tcEdcDL73rWDr8Ka/k/spyMwBKetJkm +CAhn8dRUItEbEErp4w+lX+EzuCHnfawlbGBEWHqTk+uGXgT+1eSqYPZt+TFSCtBQ +1u+XjG/2+GSQRv6EzCaWRQ== +o2ubjfluOOAAUwr4tWKgGBjOaO8YuesUuICLqFn1uQAxpo6IXEzBQgxm6USXIdsVYboCJIsBs1PZ9ZVbEQP/Pg== +GfEkZ4NfPO3albBU65iCnI/YlKcPp89u0yJejNWUbyxYIWxBIvRXparAxFUFZ65RMwYe7jruETuwMgWPiXvTAtc87qJ1e7SjRH1PdM0vQYjM7CH4R6WtOjU+lzqONjvLWvdnRMvUaKQrNm3vlYCV0g== +VmVrGQo2zRokW/ZuO9bN663a1vcP2Wgp3erCrW2K3l+Zhq8scHLL2chDuHbH/IinjQfWslQYOiACDDq93wJsSQ== +UCx684ZLJmRpC8ptLUtEbjyLKBtfZBD9gsKCO9WFUWE= +CuALK1sA7rU5lII7Ti/IXqhlyIFYgvHVh1fw3vP/cWk= +1u+XjG/2+GSQRv6EzCaWRQ== +2h+GQ0ip6FWGdAf+0rsQ8fDvrbBa/8flTMuXE9aM8dU= +Lovnzsh+ewE4yI30ps0ccHsrcv6YVEoN4GdClwAZZGtiePQovzeF6py/nvQiyOOJ +1u+XjG/2+GSQRv6EzCaWRQ== +k2XarOmIG1CiRPdxaUQRe9O9WW6xe3pzBYyNrHH5EWA= +sGvN55RVjPs2jX2J2XGz5mLHqVQsj2GoejqMHQ5TrUyQPt2cny+eifcMA8WhnTMS +3ivNlzX0mI2/YxGM9KTPAul8g9kOqiifSuOcQP4jtqE= +guxFBhjFCahQsRVLv8Fqb8cPJYUV3OvgvPngmakxp08= +6ykQVsEUWBdxXFRX1gkpyjO1BfU0ZI+BieyV2+JvGoa6ldjiEnfi7ARnbXyQEcBn +Z/cADHTKcwZABip0KMBWYyF/jpCNjEVfN+lxRsNFffA= +6Ec1ekJgpoPVeYsFkNhj1PlAzgqMyfI/vBtMn17NFN7GibBFt+3gpTQVZw6Y5BKe/LRX6o0HPkyDHe71R5tlvA== +Eq3OHWgucsEdNSbXWskpRw== +35DMgoqiI2OAWqY91zzNew== +1u+XjG/2+GSQRv6EzCaWRQ== +pcLElPihWNGsLwTfd6tA2lE1NMYIK66wQ499lXREsjU= +kmuqTLFy1lansHX3M1CzBGnuU/Qo0jPALsptaoIk+pA= +mkTKRMvtKXs11nPwUzmgVWR5v9mUDrVm2zGohg8FRkk= +35DMgoqiI2OAWqY91zzNew== +1u+XjG/2+GSQRv6EzCaWRQ== +q9mtXtgnV9kdkurhYlX/A8wXnZxVZaVAIrHeDy0/aVo= +9L+tZdOzKy5VsulB31CUB3MAV9NW1lp0/DM/S+WFVRA= +mkTKRMvtKXs11nPwUzmgVbb3CHI9+m09CEUTqt9WyUo= +35DMgoqiI2OAWqY91zzNew== +xgQxN9GJBE0IKoADj8CxGN1lyul1PqjOv4/Bd1hgfRc= +UkUvEkND05+1eWBhIO9ayBjKVoAihFabfqBamIqjC1wgCNlliEH4ebmszCSNPEy4 +DXqo/gKgZk7T7yIsX7GGL2+jah7PFR/dLsgGcpoIdAYQZSmwlMufH4BBXIHVoH0S +1u+XjG/2+GSQRv6EzCaWRQ== +dZ3Yd/2oq6SGw68qvdZIUlXJxmXL0j9h/9iEXw+QdZIqwzRnfq/+UGAvYbDHtCgZJDlQHsqj2nzA36KkQGwgPA== +1u+XjG/2+GSQRv6EzCaWRQ== +2fs0ZaO/ujkrB5ZGtLBUfOEc2OluEn7JrUkLakDwnSM= +1u+XjG/2+GSQRv6EzCaWRQ== +lmFcCxjFI69GzGyr8PNzny26IWzn2Oyyvgy2nlDXKls3Cm9j9sZFT7Cj3lBheTPZ +wiN7N+OV8hQjTyTLFwEOPhTPSWuBVH9DTPm34Ue/SkNJvUvwN/sg1ifaEANkQFAo +1rs913dQZjyW0NF/9VHH9b7a/BBh4ncqrLWNX0mhCPO6tSSm4f4QWDsvicp9VCpb +35DMgoqiI2OAWqY91zzNew== +1u+XjG/2+GSQRv6EzCaWRQ== +lmFcCxjFI69GzGyr8PNzn2IFObeqH2EqLordlGn+uk+P+Ou6OCrmcdiOnEiNk8S0 +/xV/Pwz1pzUeeJWz00zuwA/bhcRbX7XCO351Mc4MPrhYxHR04pn+BmL3aSImrb3p +1rs913dQZjyW0NF/9VHH9b7a/BBh4ncqrLWNX0mhCPM+CyTeP02ihghLXjV+kKvu9ZHC9rMCoh+LVy46aD0Svw== +35DMgoqiI2OAWqY91zzNew== +1u+XjG/2+GSQRv6EzCaWRQ== +vrwcmHoAVQ+0ACvkFc1wL1dk9ZDmYZ0qUhXXJUnRmE4KaUg3LRNIR4ENG2Y1qjwuV1opfZGExcSjDAF3pPk7vQ== +SshXV5L5XwzNd0TbkNH+HlawOW2QILRbfjgry4oPbRFhDumTOiwk+JBJVAs9DPkA +1u+XjG/2+GSQRv6EzCaWRQ== +NdjeiTbmdvTutE4pgxeq5g== +1u+XjG/2+GSQRv6EzCaWRQ== +bDQBBLco7OXTZrlp7Oq0/w== +1u+XjG/2+GSQRv6EzCaWRQ== +LQ0KPm/tSx92rgvt7una4iJFdfRqWInfo3MzyGa2xkc= +RUOYtOIVoIEW0c5/LE3dYA== +PWPFJDMOUlKtMQsx8OcdBeCIsh7ZnFqAha4/64XVcu8bqwL/cB21S3QRcrGKoCQlVKscqCa2o05U03/uq/9bfg== +N2AxSv647SE8o7pH02PLTVwRIWX7aZS5Oc2LthIwLPI= +ovGXUor0RFoDFl2o1PzVGx5Dk1QkAGyFN3iYcLwiQvh+pEp81KlSiNFxM+EejhMRi2mqyB4DlzXpcWRTmTKlcjLpYPj3/SP02pYou0Rb/WI= +N2AxSv647SE8o7pH02PLTfcDmty3/mTJNQkhnIIAyqs= +3VHxeGDDuNFDLVMbh+qvyX6Lc0z7K7s8cS5ANjkq/CTG/pfKTi3z60MDknIBb7eU +Q7QVGPDExqjR7ynTbPAdwUaFtejNgF2A7xf/cyGEpXwsjpF5JLmqrT8qNEr8K1Y5XzpvGIP0tK1VuKgb8hm1yg== +1u+XjG/2+GSQRv6EzCaWRQ== +76vFhm0rUQUkhnOJW3uilxfKkyhEsaOiPZT/zrOkEk+TYEZA5Mz6VazGXfI9ln5c0hFwEuCuUHhB0TIAA47v8g== +7AKAp+KkzjPZuOR3b89ifrPTm9hESZmaOReycYgDDByJ8/EiaV0E2tOUyZ8rGQAhI8glxa6A6Be6X/Qa1TSRjg== +9UvoHK2VToOX7RS9aHcpAVa7hUi7kNnFTnR0gBfNtqaOcXjSXyRWFP4Va8t+uwKN +rLgcnY7EUTjJ7/2Z/RIYEpqDLaO6oJm4hr6u/q0x+5DGK1IiL5dnsGo36WVRr+ND +yoFtq8KDstGoS8N/pwvtcw== +1u+XjG/2+GSQRv6EzCaWRQ== ++jegiVkS4cuX6Qy92PUlORw0Ev69pyAjCXMCDqnP1ea34J9ppQP+5FB4HDBFrvzV +2hBg5PiJoedaDi4r6TXwo/u3V8+5GvaV4k3aC6cey6bHx5SdONOmtPgVnbzruWCF +UeNT1dmY702Loi8xix1N/Y9v1944wqSMejOdVM8uDLzVELJpCTeFZXeEJBqWcOdWD0xnDQroen53P/0k4GOLDsw5nT634zOAme/AYIR6+5z2WUi/Qnunef5PO0um3KEV +744+BgzK3LVsSzMxY5sSrw== +1u+XjG/2+GSQRv6EzCaWRQ== +VjdgpwoKK3BTMsCBbM5v1kyuF0wLAY8PmteuSynRDeU= +dd4XXun59JHdLPquVYyhuphAOrPbNRWJV3hO/mdMS6FuSRor8tToju6GeRLlXcrpyhreP1UBtd8SoNYD0n/+iZzFwvPk54imoIVD3LDxKAE= +1u+XjG/2+GSQRv6EzCaWRQ== ++pBWk5LxptzizUTsHnZAz7mzwwOakqLVEwy5kkQvI9DGr115jKDalwXcNQzHi+3frHXmEm9PZAEg1TWbhhfM2g== +p2+bVXgc/OGpnu+3Mkh/HboqxyaARCwkxmEd/dSA3Ek= +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +sQf6NLYGZxk4Y9knQLy60wKwQq7oAVYJkZBcerOK9Av+i0gwwe5coMr4cy8maEpieoh+kv5D9sebn2ggVATijQ== +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033a0p7ZTOs4HLJWM5pa8I8wXg9ZmXXDBuoUY9f/vWJOkWg== +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJn/xkDohKB2FOar9iIwA6LOdYh6U4GTMfaPBq0bPmJGJg== +VmVrGQo2zRokW/ZuO9bN65rD4nI0MfwzlsBII0cdPaY53XQcAP1K7MD7XDuIYxlJjHyPmHbsy8Iwat3/1Rw5FgEV295FW1kcTvXh38W7xCeiRxhwCFt3NWJ5IXxc/Hz/ +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN65rD4nI0MfwzlsBII0cdPaZxGq73u9PQD1+IxG4Rbi9vRoYfgsSJoffX9b6BeK31+CuD3umRfx8C7XHcYNodxbsb2PZKIH7aQIbtOnMviAES +1u+XjG/2+GSQRv6EzCaWRQ== +OpwSuFTBAL7XPZ3GOEDUSdpXkC9LFF8I2vj6o010A6I= +O3CUgrw2GJfB+mDjH5+NdmBcNzOXWsLRlv/Eo9RwDRld9W0gWDl+xk62GLY3Qv2DWGKpIX5tFl+cO8m6/hK1Mg== +VmVrGQo2zRokW/ZuO9bN67ZrXFXKDa7npzYMgDzE3vnAj/Qjbl8cXHqReM85Rn39 +VmVrGQo2zRokW/ZuO9bN674wiAEkOXZuouIpywYVpYhDLB80TDafhfVQ/XKGGGDeJpiWjm6svggX0B4Bt5PbI2YjVgoM58ElF2LWkAYx2BU99WBhKDJqjamY3AgGLeGO07lsU9N+nToSzDhHkNAgRw== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN6783Wuxhwb2sxokA50qL52ZhgGhtQ8rehhFhgBhiVRiT +VmVrGQo2zRokW/ZuO9bN674wiAEkOXZuouIpywYVpYgYQo0fVK9J34UXk88uPiaXZt/AGrZOeV9dHK2dSCKRkvDGRgg70LAFEH/SXTzKjMQLINMeDPBZNMrSaMH9JrBMvB0oL/szrCBM73uTaYAM1A== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN62IPEWb3HxwK3/8kqu9CPGWphXg606ZeDmzQjj9scBytKgmpCbGm2xyiNSddgD9gTg== +VmVrGQo2zRokW/ZuO9bN6++ongIsGLxOwnmsK56k8eo8nthS+TB8bD6bBvjw8Krw9JM3Y7bF8icw3X/1fRakVJ5Xd2n7ocRlWZIDVddFXqg= +VmVrGQo2zRokW/ZuO9bN60Noo1YoOv2brxUNEwUO+Aq6tREQAFumdXlyA6HdKVMsNjNfvz8z2upx2HVvVCisZOVTbYZ3ZkIMwucvC8Q5XH4= +1u+XjG/2+GSQRv6EzCaWRQ== +8i88MKADknh6ve+bU8Ezb2fMWyOdoYwy/nQLgcNcqQKKSdGUiteGncWnyshAKHiv +YA93D7bVXAwMe11nxv6BP0osBRZ1aDv7C092rWDZE9s= +VmVrGQo2zRokW/ZuO9bN61Jo4bcSJ4kcuwCfHogcpLuF3fcu3K2c+Ai88EDEcUqK8OYgX3butIj1TyW3wFzkIeddODjkw7ZFIoK8AK/m9Yg= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+PvOxVUEC8J2Zv1Mf1MR9f7HAHl5gxCz9Ra2KeHai2W +VmVrGQo2zRokW/ZuO9bN67ySazUSUjnUfpehBMCBA3PealA3xew8Lxgpk3bBd/icpSulRDbeDwAoSCxnkYMWx51OVStf4mIJKVDgCAMD0Gg= +VmVrGQo2zRokW/ZuO9bN638zramoHtIC9zyxgaBuC7/ZiLhR0xQx9iLnwCv5JsN3Cr51CZFf6nG+zuC5p8BkoQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN647dISbCvs+83ZjMHun8Zak= +VmVrGQo2zRokW/ZuO9bN66sK8yway8eq4yOKP0KSly1WR+xW3zjEjNELOJdLQdAJv79Hr1Wx/wGqcerWY9l+DoB0urw8GBJXDsmK9e4zBFY= +VmVrGQo2zRokW/ZuO9bN65msj2VfsvrEWaPQWhn4JLSldG8fzS8ZswPhfN3JoFhjByI1egZfGIzCFcnrbKLglgPzzaX+oygCzvcKG5rVMKy5tn4EdZZal9nnGVvAsWvVIoTVmSN9f1KUUgh2szjwT43NAa8oS7rN9qEIfw6IyNFpiVGuy3e3v1jbt5HaSSU57z6bAkkbp8c2bXEJEuNgwWGv0Z6DnuAGR9qXlwNWFyo= +1u+XjG/2+GSQRv6EzCaWRQ== +EwP7AACOVTDx5AWnAeeGJ1feDvaGNb16m3I20fR33Zthrs03y7dSVKWSc/IMwSfE +L0eUthVnpkGsmKFAX6d+uOW5BVuM+7tcrCiEwvU64kY= +1u+XjG/2+GSQRv6EzCaWRQ== +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +a0q7lWJSm6u4huPzjxOOQdfyAK962CJDhqJrKlaIlmP6OIGqOThO/9i2JrxXJL2I +gC7pmugJ3/w5trYRkDiKMfwbAVR+lU9AovUlz17O5l1MqcYhNqibDOmf4bMkzZtc +1u+XjG/2+GSQRv6EzCaWRQ== +nr7wnSngruPkaFV7FwaQkBxMMcgyUH/Aqu2obbYJTxY= +Xw7xih7AI+PZHSQQqJxLaj32V3SAelBI3FCd01OyUChaTNrQPLcS2sZvi3PUacj3HNO6dGXUqbOAvAWWuM6B/tvKcENk/1nX3JY5CkmfRgBN9leT02AGgRpOuxYhOT6tdGm7oSKlNHSnZhHfbLBDXALS/nktH06Tu1W5usWstt4= +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUCsKQJMKz57hDGzJ13+V8f6 +uB5xMqywxOr6sg2uvk1oIA== +afVXxqs8+P3b7z8Rj2RIMoYkaZYUhApCKoPSV5q4efI= +vXJ5dquhhV65cFgqiJNSzg== +QYg0P6cy0syPj2wysSE7JRFQTPUz7IuQpOIRqTWHI0WnVQT0uQCXbK0ZZ1XOIyWr +1u+XjG/2+GSQRv6EzCaWRQ== +uB5xMqywxOr6sg2uvk1oIA== +O6raHGR22ptSzcLlTA6plZtx0fpb1ELa6w/zugUV/qh0OVH+0+9QY61qCDG6PJYJ +vXJ5dquhhV65cFgqiJNSzg== +nmcFcbHyKAPaGCmiuUmblsmi72hbMIMtxRCY/4Q9jP9co8BCZQBoGkh/gznMPPOd +1u+XjG/2+GSQRv6EzCaWRQ== +q3JI+CJn8EUW6JbRAwDJwwrOBECg/0QADmFnC8JvjwJ2qg4FgMh4CAE8VyU5Ba6Z +Yi0nLXW+2v/erEDLKcVze4KgKKfsG2Fi8DLInjE0+mOp5J/Fx0rj6I25o87L5Fvl +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +Q7QVGPDExqjR7ynTbPAdwdQcxZFfXYvgsHOQLIMaqKB5CxgZn+t1/y/7rMhBEuhw +1u+XjG/2+GSQRv6EzCaWRQ== +1AoMPtvRxz5qNPcY9ZnByWyDRZ/bMTurllH5x7LLfo1VnJuTsyhzH5XlwS5fQK19hM+fHswzD+i6YAXYSTlXZQ== +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyV580u5rQuaq3+Osg23k3+voq97h4Fl38/mV5TWQrrm+g== +1u+XjG/2+GSQRv6EzCaWRQ== +1AoMPtvRxz5qNPcY9ZnByQOS2KjSpVH1eKO7bE2sQaCN/RJFyLGvTSvfRYlUWnSgQzCHaMkR5ElKaWq1r17+9g== +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyVlWHWRVMXlumVg06MNKbE3dS8MvKjLjUKe/Cc7fDtt41mohfLoJvsAOaJGG/Uisw4= +1u+XjG/2+GSQRv6EzCaWRQ== +Rt2DIhU5+Br7TTS/+aIzhpfwe2Za0BTqWj5Nns1H3Ee9rqLAcPVwhSwmJQychDO0hY04rJxRLAJvTqNuJkFn2vYTdrwas6Cgegei39li7Nk= +0BKHdp/dyazawjuP87aIMRN0k85QzpJDqlUPvsirnnsr03l1/pkd3sxDxe0tB9X5FlDbEWbvHulmEeCZS98UBQ== +iDSwwU+SCMb+zcKzWYZQw6oBz9ZY830bE9lNHotXMcWxUfrY904r4W6X3qpocd1fTOUbZEzHpuwndyzVYr/JoQ== +JNr0N0tggrxTj82vgv/JR3L8EGbD2H5kO3ZfNQvi5ua3biG+3H2jyMDJxHZOBqi4rwJ4EUsDekl70Fl/FWSBmsbzVHU9/cfxr3TlwITWvvI= +iDSwwU+SCMb+zcKzWYZQwy0W0kDMDkhisD1Z98pyS+4WMfFyHWdsB1N9GHcEiftJpTRQjQpiO9QLHvSfzl1/ydVASt1EssXgGyPnHPPHSUc= +3gHx3euGCEcAU89Mm6ORtA== +jjL6Cxo9xAtZA7Vdo0ubcQ== +B7gcgvHV8FsVJzB0bc6LYw== +Ivf7313boLfZJ1CPVYIsdUEA9aSYIhQfQWsNgcZHDRivRLeitrddNX0V3e/L0Vzp +yweGob02k0AO/YkOVMp6X+TVqTFIlOs0QRSw8j+c/uxK3JFq7i0Re3GaDOpn0tDa5EerD1d53geTnuKGOcXQQn0dYA6mPy6YflkgOaIa7nshRPhUv2BR6g1KsRmahhE6sNJrzqTw6ieiGfcKOYmlXQ== +1KTMEHdMaiUieavWhiidDJN3EBokEyCm5JMZaMHg5pY= +gPY7b/O18gvpr9jG+iVvig== +jRwywiW0qJerbVBQxbiiWA== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +3BB32YsArGvi5To0fsgr2CiId++wXq5fXwnxK1zJS1sKVRT7xb2qvIk/jQA6vpU2 +0BKHdp/dyazawjuP87aIMRN0k85QzpJDqlUPvsirnntTAp6i4Teq5wK5qX7pRYdLQVLIxY3Yw8Z2tHMiAQJWPg== +jjL6Cxo9xAtZA7Vdo0ubcQ== +B7gcgvHV8FsVJzB0bc6LYw== +Ivf7313boLfZJ1CPVYIsdfWUkxmMal1NW60s+7G4NRtPVn33XtRI6X5yjFme6lP2GbimWo45DSN5jIdUjcaZjA== +/5tg6CGVS8djohtRN2xAIPUdxmfaI+M87JUlZ/wdVLntpRUfykep3JSSHdYJwl1g7C0XEbrVv1IUwuHG8vvOZrPHlSbZupp0KEg/zwecp6hEORrcYvcZ5xRDOhUP3Exr9FwbzHJVYqOClUJJLFGB9k4MGYt3P3M33xmo261zfndjnL14LgWg1Pz4rtwLuB4qJWIf7EynzFZXslV8Oh+FgAqE+5nAOu+e6qtg0BhRP9j4q9Ga6n9d9+4L7SieV8N/ +G7QeY+zhxDXR6kMOT+CbTpc2Hg5tgLRewcArSHBwE+E= +fYHNWwfvnaqvh2J3taNb9y8ULds1NmI0kHUSIa4DZqE= +gPY7b/O18gvpr9jG+iVvig== +jRwywiW0qJerbVBQxbiiWA== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +3BB32YsArGvi5To0fsgr2KV/xrBSiQN5hPtLkKizFfupJnbhHaMsREHQ1q00Xu4v1DIsk0xlDEYIkB2R7DBqCs6ERmQMiWRNi0ILhiSbYxjUmShXEfq4sWRVr0THuB1yzCTvsMB5OmOPtO1JAdpo7g== +cCb3q+SmIZu3FGnZqV9x9uwlbmYxgiXNK+dEKMu2KrRtANfeYUP1wKppgWruOld7o5d/e63j1XMZV+YR15XgXRgOhQNTsSOcMRcubwhervKcU0lnkdimphGOIWUkGNiHcV2k6ADfGA4IQKgmtWkPlw== +jjL6Cxo9xAtZA7Vdo0ubcQ== +B7gcgvHV8FsVJzB0bc6LYw== +Ivf7313boLfZJ1CPVYIsdfWUkxmMal1NW60s+7G4NRtPVn33XtRI6X5yjFme6lP2GbimWo45DSN5jIdUjcaZjA== +/5tg6CGVS8djohtRN2xAIPUdxmfaI+M87JUlZ/wdVLmKgFIRwtU+pmA07/3/kp8auG2zBPo44ye4korlSkP4PBATP2E8VDUWgu78dJjOIvbM3seIHUJXS13bYCUMvqsEVivES14voZBhUwMvlYjpAuuV7J2AlQaZ6oVnLPO+g9BigNz11TAQ1UIi2gSgv9EAlGFCXeVspm++cRMQ7aU/+BT4AIJt3v4TaYPRRtskKjrkXh89iiNoccdtn9SHmmncca3i8PWwYQghY7FfI00taaPFbXBq5kzLL2UyTgHCVdQ= +G7QeY+zhxDXR6kMOT+CbTpc2Hg5tgLRewcArSHBwE+E= +fYHNWwfvnaqvh2J3taNb9y8ULds1NmI0kHUSIa4DZqE= +gPY7b/O18gvpr9jG+iVvig== +jRwywiW0qJerbVBQxbiiWA== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +QH7NV0DTZEFfFUF4o7B+jhE5fPmec+pPjwsuCA6SAfh12Bd/xH9KbjWtC6oPLloGq86Xv++T1m4M1iGHDRUI9A== +wMiVF+RVQuJVQj3n+9Eg/URACPI4AcGVQ1yEY4iIOmzEGJ+l9TaWpFFsf58o1sJxZkn3kVW2ucZNexURGsahwg== +zi8rzIVkr2F99KlemQoOq0K/D/TW5K5Jn96rQe4iFeytYHCH71ssy1dk4G+oWE1J +kDiPYAmVdaQjaaqZ31PehEqdxbwK28N0KhU+QxnQlxvY4qHvVK1kw+21mzagywGKATFm7dwJcsSsbofCd78LyA== +8GzSzmPyECyoWE/uo85qdR0FRPKFPtTU+3b26HuE16an4j2VLYFdgBiFDAG4037JjXQi38NmLzmVONL7ZNWXjQ== +1u+XjG/2+GSQRv6EzCaWRQ== +kgkMvQXTCwh6F+mcyMsGrJHxKHe5twNkkDmRCTdzEWc= +VzUo2AcOkR7s/IzsaL0SuA== +xkoukpxOayWRzNMpKNBZQQ== +T5Yz3hzK/gTYy8E1rqMujQ== +vXJ5dquhhV65cFgqiJNSzg== +s4QT2gUXNI7CtR+xBG9scWrxxXFh4d8c/AqK+6+HXGE= +J3cCF5F5lMJzHabBBFbIO/ygWwiMTjd+KpvovO3cLo4= +Hv0b+60gZXEtM/aOnH1MztAvUT3xkS6j/OfA1Cxl4bo= +kduFpFkLKg9qGyT9ZrvAjALNchpgJk8eWcyLhrVLGps= +Agl71OM9UQtkHo2XveoIWQ== +1u+XjG/2+GSQRv6EzCaWRQ== +FfPd0mAfqb2L4b20V5L3JrqGq3JspkF3cpDgYWe0EfF/M1b9hc9l0kGsXNU6gIVL +mvzsqGDGq7heX5STUWahtjkERaEip7AbHLibPK+TaCs= +DfnFu8zXb1HhdphG17AZ+KycT+FZ5oMtfqHqLoq8ssA= +1u+XjG/2+GSQRv6EzCaWRQ== +t6py7+n2nYrnRcTqYmVk1g== +o8bvh0VBIgvVkzBeHxXlLEPg5hbU7mw8LBVIa0zm1PJDr6qfLVLMoV/qEOA4I58208v7luEU6ZulhKE+sCrRlD2XejWHLuOKH1Vvtpu0beo= +9O5uzZQEX7OrNEGnTQTdKvY2+5FcbLbOKhp2ZN+qIMRYxVZInb13lj4zCz9xO0FtQy1hGQxs1EahTSjkLDbFygWupFGlaLt+iXaU9oe7DCs= +NTfD/2QdEOMSOY8p8LQQLA== +++Emx+f2aZSbtkRPOSPAhoTY6Aa8kbY9SwCojUSy6W4= +1u+XjG/2+GSQRv6EzCaWRQ== +fjmhS4v/8xWhzMXplqU38F6gHgLmEsWhV3IJjzQaXKOL1bWtfuPODGE/wrG0IkfA +DxJiUgt/ReBxNNtPK0WQ/gnIhO5UVevBd25STZFgp7Fp6tLIfb8lpPaxmUTxQIgBMHPXhJYLicparwLmitsLMZ23u07apTK1we81Ju2WNOE= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +4roTOrfENPXMa3wMTcZ48Gd5caiMTH1R/njuvBZLW+D9gqk+dSMmLP+udSxsX44I +wMrZm0BQsU33qqqzWGG/j0XGM9bzMTLDvUiqKcOSVg9+6cgMFTwGpN7NTLmWQRIc +4uSJTxLG3WRqfql+VEh2mKErIioHP277fpu9rvziK6EJtsxOC5pmoqrRPPODi765 +1u+XjG/2+GSQRv6EzCaWRQ== +3kdsAwWZSEEaChmQUD9QZgT6/AIYqAB8Z97VZco5/Wg= +ImBuWrJYZyJ3Z4QS3TZY/AEQdf457fdIEZVYZBcXO2G9wPu4JkLY3xPoNqAPCe5Oo/sxoEBwxfjXvMB4Rv0UAEL0xxRBQ4o/MEj7mnjW9Kf+jWYR0Hd/Xdi7bGWbj9cc +6dgC9QozWIlvE3N2y59t3B39Bl1HqTOFD6k50U11osVKiFqfmAT+qe+ChOCdqWQPTGzq8/eiaE+cyewno8SD59KCD1wUfuCEWC789tE7h4w= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +esZqCbY2U0tDkYEJ9WdOGqH7ZkpKbR9eBdR3DqWTppG2qKjWrzvAdguobZXxV5rPbZWxJbrZFWWemeEno/X0xlcGWSivODQqOPpmetD49zM/Qf2fvpvLsICJPK9yKvmMCbrugvwaXbrasqa+4y7I9DkYG3RPgseLnby09FZEK2z3Li1cpvzM7/VFTIIFUDs+ +O7f6kKV1hliq+ujUlJHLfQ== +nqT6Z0I4DEJ1fpJD0qCwW877uh0YGimzdJbZB5dqeYO5lVcy5AsASWZ9RvOU9OTDabujsLVV8CZ0qutrvF0pdbTpP5vaYDCfmmgPnyooCNbMirfd5zZMkCMhds/zUl5qDN8RfVlBeJ75MwTr6zoxoA== +s2MXk3gaIHoG6PiyucOWEiaaKhwCovxHG6+uDsssb62WLyV8d9tvOhrfsBg5fuLe +5V1NTYeA3RKnDDORgkA4dL0HB3Y0TuSJVSqnvg7bThBAoxwCQ4roE9JUNYK0DkUJ +/HRPF/ybrrEZU2N30BOMAzleN/qbLV2/0JyoBaHaG0g= +yM7LmRhDX4N6be9pOzWVCdEj5U1wvLfZ1Q8LPLd9EbGZggBsaNoAIXWkVIQyvBAt +BWnhzGvzqVPra/cBIsaYUTWwVgnYBiWHTVlANjkJPeowP+uyKjSpMTF5IjW2xUU0LeNFain+ZOa2zvJbDboLFoyyQdE201sj0+hNvhY2S/Q= +661hZf7vhUQ+50okfwfTXw== +s+CwzRPn0zEGm7n1YL21pauz6alw5B6ioQN4tBO7OL4= +4bCaCPES2AXNnKU3FRtflw== +ayV4CzMiV10qa/iRMvYWiARPQ/i7V4UcD78VrjQePkE= +PRRuJqBA1MDLG67eRb7TP0NC9c1/dqtd3DKW2T+wMqjLgK/ncoADl+aLR+GlQgM1 +1u+XjG/2+GSQRv6EzCaWRQ== +cX5hj2layJNfcD945JBsE6qXR7UkO+h5v7X8+cTSunY= +s+CwzRPn0zEGm7n1YL21pcwxb4HcpX5toin6jgGoUat6vgpdG8v/JQ1ANL2n8PR8gnF1vXTymVQt/YhdidCT9bVtfV7vohNTDj3qH2XSiDl5oNPdCUbk83pomb7qngnI +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +QwsrIBX0gHK/V9IcqmQNOcv+ytwiuCifDUSXSwvcOS4/8TfuTNy5MiRCYxeKKFwavmk5BH4nuwVO4mUVDPVuwQ== +s+CwzRPn0zEGm7n1YL21pYNB+K6hRyax7Bx09qeurXRHkg44VCIdGiiCK/QQeUEI +uANEHXj3yvYHbmB7zAZgp/PchJxBEvfjWYUu3WQ8qq2EoCVa8OQAAyDm9KfQxNxp +TF57p/nEanynaty0cxMJMe8X86IfHPI5cZCc8X1KWZVo0GRByrRV87sM/JpEMgYgm9s0T3mtLJzZDeodxn85HDmLVA+KdNMXtTqyqNnLMpc= +661hZf7vhUQ+50okfwfTXw== +bCsu6Ogu/YuUlBRl9FFbjA== +s+CwzRPn0zEGm7n1YL21pYLW+/nCyXStQglZ9J0GvETBggiHm3XFJS89JAuM2S1juY9eNpG384+ce40odcEyG68cHyug6DN1VXNzseOj1P0ivBkBTF0FDa5wIST1WZq+ +1gtsWh8TwXmfYlHhuq8EY2BzNX/G5cPq+FXJJ4QMZYLRVUdRnD9BkTj4yiAyYCjW +TF57p/nEanynaty0cxMJMc5tnLkjVkLepRQ9Dl9WVR6dsyWXghO01NWlFVnCvGH5Js7zHOr/fki2jtE+/mltUw== +661hZf7vhUQ+50okfwfTXw== +4bCaCPES2AXNnKU3FRtflw== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +6iRSx4r0kYhT+dJaF3ntCw== +B/mj7m8tERtuEgm33+3/aA58DPeaWARPCdDHL/8PMqtBZNj76eBSH7RzBLBh29bxnZnqFHqh4ysKRHS++hMtsA== +pw+kZ1ziE7hWMzi2OIcxaSUfawylbhk03ROLv9phMRFt3IVL1ku5MosMpnk9/An71A4NYgfnrUfk+0/ADTR82g== +V0TZHpooT9iGmiNRKGMOR8j/qoroYkDDBE0RA082YMM/isbFmEFbppQ13IPHIEvYllBcpMyEbd5q4hdfevakvRJr8jdpI2TVMN5zHgQLApar/DOvvLjZjn+sNW1wQEpNkhcuPY6NWodfeqdUTLO3kQ== +wp2KLov3TQOyTMSOIUz76muW83ROQ+sHAB1YN4U97aY= +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +JAFfusAXVDjXTUC0nMBtimFCgWcWJbt1pZJNLh5aVidX3mknMVIabpM1gKcf1EzYbVg42UuqVomFzt9wKe1/Hc8jK1ZCgoH9lpGcRD0ihhqpbrYIgZVCjx0g3ShzNitF +wp2KLov3TQOyTMSOIUz76muW83ROQ+sHAB1YN4U97aY= +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +EqkcabguX+DNjJSa+LEtYGutw8NbQP7QsjM2D83zYws= +56W3WYut0WdYk6EvvuhL0kN+XZ0CRW0hKEuxA8hKw64= +1u+XjG/2+GSQRv6EzCaWRQ== +sRAb6ov102JQTBX6qaRghoZQxpwIoEn6Vwa9FsN6SPs= +XHjlJ5QkyFRvw9xr26/0xPscUAEqKOLYM5G/5Um10Yk= +XHjlJ5QkyFRvw9xr26/0xAleb71i55hiyT/TFhLianA= +XHjlJ5QkyFRvw9xr26/0xPrbDD8AmcZjtERDQZm6YUU= +XHjlJ5QkyFRvw9xr26/0xGPMpvTlPIhXh1JMSiAgEn8= +X7OfeH9FO4zPna0tm5rnBd50bCjVbbJMizCxiLJUbQ1wRm2Uu3OwLK0w5iGHfLdJWyvHuwFzGmvWdQSX+HlcrqUSRoeOoVHy8r+gQgsowOUpvvw7AZDyCzbT03n8pbaPeB8n4EpFzzR6R0erB45Fn20MFMVI5Sp6VGyia7zuo4iW4DujJ0bybBgo0pceKt1MvL3tbCiBeqca2TDHutktkw== +3xAddec2urtvyH+TFd6l1ND1/W4Rs+gX8BoEyACd3/s= +XHjlJ5QkyFRvw9xr26/0xKhNG+MNLZLfV2u+MOzvoX0= +oJQSCA4LmnTse1ofrS7jT/Z+dGfPQeL58AJ9eM0RSpi6Wwk7RD68WHkNYmF15o2xp4RDH10ucwfvqO+RvVQKnEY7VU7gQFPQzavienLYhTQZ91LCFisTLzR7eB0KHG8kz+qkHMQ25scob/LXXrbzoA== +VmVrGQo2zRokW/ZuO9bN69klrP5VS1yvWj3KRK1cKibenNx52wZY3VQMgwjtLaHpueDTt6P2pW5CZqiIzx8mFpg3TzAFYjrr+7Zbricd5ekQv0ACYM97FxXgdkD8B9WHblh1Kr0/8k6CzB3TZgDahA== +YH231WTDnzQG3bFilBiqIg== +3xAddec2urtvyH+TFd6l1ND1/W4Rs+gX8BoEyACd3/s= +b4bsEu9DXPsyWIbebfjSoLZ3cTIy0TPH2pfFE2kFWTU= +lyLtdrLJs+Z73tmx1kioONtOKETMncaUX5vBp3qD46esCHncHjdgPwMZa9yh4/TQ +J4Lce4mpTPiXpoeeH1bDCr76PLmrVXGIwuatJprs5z4= +EHif0Y8cXHj0pZ6DUXyAHxGPo85eCMoK37szLhBDJlg= +b4bsEu9DXPsyWIbebfjSoPPLo564fPBg9v3dET3wtVY= +lyLtdrLJs+Z73tmx1kioOIRfS3Ws0rwPUl0b6u5j8lC/2y5P6Iuj8+GDmZDRcXz9 +J4Lce4mpTPiXpoeeH1bDCk7LFwdoNWI7epgouLthuzg= +EHif0Y8cXHj0pZ6DUXyAHzM750WhsoTesLNNLL9OA2Y= +X7OfeH9FO4zPna0tm5rnBd50bCjVbbJMizCxiLJUbQ1wRm2Uu3OwLK0w5iGHfLdJWyvHuwFzGmvWdQSX+HlcrqUSRoeOoVHy8r+gQgsowOUdZxPkywkp/8Rvx6lr9X1J5AXK9rEcHGLQVXeFOJWr5IXfRj2cpHm1MkFmFcJa494= +3xAddec2urtvyH+TFd6l1ND1/W4Rs+gX8BoEyACd3/s= +4bCaCPES2AXNnKU3FRtflw== +YHmVAP+XfXvh6Do8nF7SSw== +zs8IyhC4mp7fdo+QSsNdNVtBjg3/nyvjxRZmGkai39U= +1u+XjG/2+GSQRv6EzCaWRQ== +t6py7+n2nYrnRcTqYmVk1g== +9Cm9j2OVKaoZvBE5ROReRE0glF3xam7vLDZOTh9E+RtMgrLV1Ffv3ctNus/pFF+SbXxMM2Hpng7W68zX1t0sKfYqDqREFCnTwb5SXNv+Vp8= +OQ/PAwNecxnJvYI0crHwNZvNGQCPkCzyhAa19r9kkC7gqCy2F61R8TbjavEfZMheoBUorsxnZGx+6kd68PiVcg== +vXJ5dquhhV65cFgqiJNSzg== +0BKHdp/dyazawjuP87aIMRN0k85QzpJDqlUPvsirnnsr03l1/pkd3sxDxe0tB9X5FlDbEWbvHulmEeCZS98UBQ== +RGJaNRhUc7xniMpOCx47lJjbr9whnQatzbexv5d9E489UTiSabkn84oItSMkcRtxzWFPj5w+xnJvnUZvWuxm2A== +3gHx3euGCEcAU89Mm6ORtA== +RGJaNRhUc7xniMpOCx47lDc2J39/g8ducuD+8z9qvTMs+C/MQJwZopRiCHibzb/HdanJ0eUTAYn/aZi4AR76kOnioW2B/CLcNjsH4Tf1OMQ= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +q9hnzMssrXqcTCLiOgb3Pt4x0pbPvMnHF/Wpas9MxK8= +7VEvsiaW/e4WzKqjRhxdn/J/yts2zO4mzP17OL0mpdxK3iBNwvoOLY4yAXjGgkWQg+zKx8F6CelpwLLAicmA1Q== +t6py7+n2nYrnRcTqYmVk1g== +3XRjvbBj9M6051ma0SUeJyk7qUafmNtsLffgYQboLDhVEnf2H+YRVTNKphXOFqxYH6lpLwsVWGXqUqZPr1pPbOlWVC7MnGu8bgmkBDQFk6Eats97zPgJaAaxTyb4jcIH +JQ4X5JxszsxxqNxCHNJECvya39bVGA+18mtCCin2QPooWOqFlBS0lZRdA0bDssigdruaPmBMeo1QillfT4ilziC02+1HSsGM7U7/wAJMqwmEswodsyZ7nIORZsTFWzfg +VcN8SF/gDhRH3UFk49vbA0D7gn/naFttl+/9F5MtSfI= +vXJ5dquhhV65cFgqiJNSzg== +j4jUczuOnT8XNKeqhEWEhBGJb0aupNBHfpG79rOa1vU= +MOK+RHsT0acZqB9RaYxDHnZ8X5woyOjVf8AcVkal9r0K63Gx5h8/BO4jIIMbIXZ1 +EmQ+qnEVpsnDPjMBQYSK3COgFFzJxnrdwFqhBxfvSHcv6wuAdaZcApglVxq03xP6 +4bCaCPES2AXNnKU3FRtflw== +twlDdKYxDod7XsLjF7TCAdB5t09Kb2peV05+2+gw7xw= +3gHx3euGCEcAU89Mm6ORtA== +SQKIchox1OOH6Vto0u7oEmpXtDLRk+8KAk5trrtYRrB8kx0eK6F6Q1Cw9D/PK+/i+cX0vkt4xirF0b+DcZz6og== +IoLDBLko7iHiCa1p2GnUi5C797S5mivkr1QYG93iYoIHkdJ7WYFywQwaJb7zMpqvWMvzbzUuV5T5bv9hNX/eaNsMsgrmW/nnYnBd4MXshwOZ4c2lU8TNK693pzKapzb0 +/unGt5iSJSQDvBe72EnC7GlgaGpSycw5v/7OJDym0L5RLv/ybCsPlRA8eRAFOrW8 +0H85JoP4BLHDFlJ38WrsgFccHaSvR+EHOUV7ZjPKkTQh1+0pqVfm+ZuUsnx5Fs/JOSnRBRY5Wk1RhPZS7AvqnT7vcWHaqiO+twBztRkvfdI= +u0lLTa3LFnMjc0kFzruvMMgXrrflUH6C4OWAi+ObpmT5t23i583Pd+a6yuDhdLSDbYoxfcRiJD4Ejo2BrsmLDtaCXx3UQ0lVVa+uWPGOVd3xM8vi6tyywOls/WDowJIZIONxrrlTil6/q4S6mEiApLOFkb1szLeIXJvoLJX/kjbRQPPKtGEJUE4tiyx1c3B/VrtUVR/bu7mPAU0y/797a8PLmHdz0tfxS7oXMCgc5Gg= +UwukhuHIWIoHdyFrCKw1NPYBzg+/Au7ChwM47beA7yHm2AtAZziOZndzOSOREOcj +u0lLTa3LFnMjc0kFzruvMFUq1+CWxlY7u+WQFJb12KcO9DXB+RWOdIqEIfy7kqtHrLV/rADNkkJkPdCUofHXb4jULYhXaHOFiJ5eA98Xbjf0/z1ecT4jTdsFquC2Js25 +lkAvgw4IgCnb5kWz9IAqZg== +bCsu6Ogu/YuUlBRl9FFbjA== +/unGt5iSJSQDvBe72EnC7GlgaGpSycw5v/7OJDym0L5RLv/ybCsPlRA8eRAFOrW8 +0H85JoP4BLHDFlJ38WrsgFWCX6SS4vr/ZvqcTM4rmKQ8SgVosb2DnfRd4pB3jmIAX5Q/B7IWF85tCUVHGenYzg== +u0lLTa3LFnMjc0kFzruvMLgSdA91qlq64+C8z/lyMbW/TweccdonCj0Z4bAAlK3EihDuok4VNHOA7f0R3tAORg== +UwukhuHIWIoHdyFrCKw1NPYBzg+/Au7ChwM47beA7yFy5GbCfZpAYjtY3IQ09KDS +lkAvgw4IgCnb5kWz9IAqZg== +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +dDXZXjuwcrIxqy6es61/nyRcKfn1Y2qGovdjM3oOYXY= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +tuabV/M24KrbK72tyVMUbHHV7LOQEXDWZyGMItEqUKQz4yLbDtQMekJg4Qg0sUO9 +1u+XjG/2+GSQRv6EzCaWRQ== +IiX74ldEcGDQXKip1H05bN+167oRPVQ39pYJkFOMTpU= +Mcnw0hYRTOqKGEpOyBgpBw== +UCrNFjv1VaMMEy3B4FzV8obiwulaoWqVBA7sXzlFLro= +VmVrGQo2zRokW/ZuO9bN69pK000FWnjwYXoOGvK9SBFj+eF3B9SQga2bUT2Yydcv +VmVrGQo2zRokW/ZuO9bN64kSw7YXl4tjs9oeHSnIzSwZ0v/i6lIVBlTTpFdDIVT9 +VmVrGQo2zRokW/ZuO9bN65TPyVZdihnyMCOvbLGaxyk= +VmVrGQo2zRokW/ZuO9bN65pxmLHk9w3KGdkwWsJ28DH/fPPmX31ryqpN/GvDZn6c +VmVrGQo2zRokW/ZuO9bN64IqjyidHMp9xXl6n73N1fqCcUsKRbzSuB3kxPgNe7G8 +YH231WTDnzQG3bFilBiqIg== +9qER4YIkDZjdPoLOFvkvNlk9NR3L9JigkekReXxE3xmdtdj7JmGhsYcb7wRb4un5 +1u+XjG/2+GSQRv6EzCaWRQ== +3vDcRXnLzCU8KW6t8SEdRkU3kVEcTs9Bi2k4YRiVLI6/B0Gy5PF44mVjoBtae4hk +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z4NS+dyWIuctgwZb9g2MGkpDRTGNoVTnpm25oSGOXR+RyOR598O+VUWrXhufpZu95HYQpceZrisY7ZoCAUk8wLf +1u+XjG/2+GSQRv6EzCaWRQ== +nr7wnSngruPkaFV7FwaQkNCh2w6Uyjqv8aVxqT7swq4= +U9yRwXmCyj2Iaoh9IwqvfkVUQJ/N2p+VvI84AcFXBnAKFZHIVUgBZEIsD+wVme7U6UDabWZ0t6MdE9zUxuiomvu6z4m8hPlOUrHo1KB2TnI/atz0ZHOdp2aM4khKae6T5yGnfII9BydSqqTahAFOHg== +VmVrGQo2zRokW/ZuO9bN61rniUjnyU8v2a/upLDkgdO0L1ySdL7Ms5h8Hy8q3iWTTK2lHczABdUrvmNWVzy/Yo73PxbYi86+/JNHbqa9xPs= +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUCsKQJMKz57hDGzJ13+V8f6 +uB5xMqywxOr6sg2uvk1oIA== +afVXxqs8+P3b7z8Rj2RIMoYkaZYUhApCKoPSV5q4efI= +vXJ5dquhhV65cFgqiJNSzg== +QYg0P6cy0syPj2wysSE7JRFQTPUz7IuQpOIRqTWHI0WnVQT0uQCXbK0ZZ1XOIyWr +1u+XjG/2+GSQRv6EzCaWRQ== +uB5xMqywxOr6sg2uvk1oIA== +EvY2KCF9uy04UZiPOMpkQrfXmJpmBQQvu7xY803wjds= +NTfD/2QdEOMSOY8p8LQQLA== +Qy3HRQRwJZ3e8l9ZIXEc892h37EtSM9sBXxXVfO9jrkwVxxTpbnz7ByA3FImoth36dvKHUUTa3cTOHIczm9uGg== +1u+XjG/2+GSQRv6EzCaWRQ== +1AoMPtvRxz5qNPcY9ZnByRHH/ref6tnyRFC2B9uN9Yskqi8G/U2RNcwDyCDWIahL +Sj4v6DYFiLE4K/aEoT1l1oYuEhDwke+uGTDzUOi7LL/hYMH0YZRVXWpUvZjA/ked +1u+XjG/2+GSQRv6EzCaWRQ== +1AoMPtvRxz5qNPcY9ZnByWyDRZ/bMTurllH5x7LLfo1VnJuTsyhzH5XlwS5fQK19hM+fHswzD+i6YAXYSTlXZQ== +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyV580u5rQuaq3+Osg23k3+voq97h4Fl38/mV5TWQrrm+g== +1u+XjG/2+GSQRv6EzCaWRQ== +1AoMPtvRxz5qNPcY9ZnByQOS2KjSpVH1eKO7bE2sQaBIBtZjw1YVSGIE4QPIx5xk +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyVlWHWRVMXlumVg06MNKbE3dS8MvKjLjUKe/Cc7fDtt41mohfLoJvsAOaJGG/Uisw4= +1u+XjG/2+GSQRv6EzCaWRQ== +xbDY1Pf/5gTDcwQWw+dIWmRHUmWUyazH68q/Pn9mLlbu0cwrXcRHEh7BBiyFQUBPuhlXngQAO4XFwWijH7hAiw== +Q8Ng/8lgE0TE6lAb2ZrEmQ== +8yxJ9jVfhlWOpV1s5kNCj45HdhVOGsKeUlCwyJhqyrcb67yx6ZX5cxYwnak8iMzZjVgGBplc2r1vDFcgIrSlyw== +lXkyogSVp/PthG2AB7BW3Q== +uB5pAOhcVBu56EIfiydcJfhSuCwTSAnBk0uesto5i2Y= +lXkyogSVp/PthG2AB7BW3Q== +IIVrnJVbyailXHSj5+5tvNLQjFL2Bg6W2kd9iA50KpbFTcZxn/sC9/KvzU3Y7gn5px77DzIHff7nJ6vrNRpHCJNI7d2GcDg4Jr+e0jr+Oyg= +4SIkpRlUAC2uCZaICxRMP/EwXEhokG3OsDHrzyyXPBGcHkEMKzJ9qLyPUQwGJiBS +zGAdgOrTcRtUoT7wXPkM7w== +R9LOcCsedpYlDZdzMp8Et40/Z1VTbO5NcjEaZ8SmirkOiPAOLKBhvcfKFNHC5Yv8y9lyyCVn9feRLW6bQ3wqsA== +zRc9rqL2954JVDVGY6jmezTzEvtQ/kn1zXEsoq0cxGV8m1v6N+olP3EoIdZsVwWn +MSY3FO5VrLzKJIF/dJedrDIIuIYtQDy2Buf22l2TuuKQq/IqbnW8woMAC8X9Tv1gbSVzb2Y2Cw+PkmrDPydNfwaatG+9EZIdXyvUPJ/uviQ= +1u+XjG/2+GSQRv6EzCaWRQ== +rPclH30oEJmGG1aCsaEUow== +P+daphW1KMbcBtDFREt2+OeAZSihF8C+u4QO/Nfn+A/nJ4kHlKx2ahIJCK0lCg0Otvhr4SbskDOjIvOnNxKY2Q== +gbXg2jMpSTnF2ryZp4BBYg== +te8+BPZgyFMg6L+OAX7dgdzWP/+plQBYEGlBCIt23IQ= +gbXg2jMpSTnF2ryZp4BBYg== +6JGYWzgQ7nr2g6aL9XgoeiTf5qXC93FQ9QhL4m+eszFJHmyEnFwADIgrMCTkryhy8o6+9gYPVIue+S8TYOGJfqBoYue7YS+/SIGriaYMRes= +6JGYWzgQ7nr2g6aL9Xgoeh3Czi+TAIJAIGvkK/YMZOdqFd9rIsl3lESKpGmy9GfvegVvWlnF8NeJg7fLuC7VGQVevD2B20qVkx6mUZIePhA= +0rlRB1kTKPNKo3Ah4iFYhA== +Sgq5UjzDFxqKLmpW5b2c3Whahg2KN6BFVW5pJFLgUoiPfw34msEXseZZRYU30dBUxQgAQQ3ozWB00X9AI/kpinRWSdOLQDLgUe8whPQzU7k4SPQoUnuq4YrG0Nv/cAEH +4bCaCPES2AXNnKU3FRtflw== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +TAhxN2R+Pt700IuaaVSuHcgygtjCxWmnITgHeRMkSH0GAXoTLdWcoj6xczFEJSM3wX0sAib7kT3lipQfQicKEI1t+iSyxtvt0MNc0wQBU1Y= +Mxa6u+oWaeDYujlKlKoRg08kP1e7MSD5fPNSCm8KJXyZG9aoB2thmehjQoWrDu5Gjr3+qRDuPGOkP1Pe9a+jVWDlnVqfPGRAaVOhgDSo5cw= +6OayCSS+2n0aZPt8Zt4GPSqTWUlsjIoIO1515Ro+rT9M4Tlf+oDEYOaxUaJkYwZfm8UBL4C28r1dAqXz/33emxdgntTSaDzZJx7JTEvD4wM= +e8tZHNzJI2MUNqqsKeaZfZwb5am3sdEPjNKN9XYbuzCiOWu4nEvBDSfzc9EHTVT6mEHgoGJqo7z93A2GvTJUjPvYhmcisf5krDCG/aqgbNs= +1Z7N+R6KUqEHl4gdueaetQNIyMWeGvmvLBNJLtv9R/FI9cwMfz+G6jsPRWeZeiEN9CH9zDX1hvzd0vGPFuLygBb+hmsCxg90MJbYWI2kD/w= +NhJUaNtTG4TU1FZR22MFcMlMK+FEDdCZJDEp9pJeeGA= +1u+XjG/2+GSQRv6EzCaWRQ== +HWqNEoY5C+an3Gw944UBo+Ag5VhBqmnfaZTY5V7H3/Q= +1u+XjG/2+GSQRv6EzCaWRQ== +REsEDBcn8X7lu6D8frjYCHE9h3P031N3vUruw3A193k= +8vxRGfWjHjYgzqup9Q+m+//rouGfVDxtEDvGpfiz3lg= +JSau+28Jw2AQxXNKzdbnsNuXubL71P0LV3umaY8hsIuCwmhbLi5IdEDwE/bjuxSjjzSGroIv1AT1G5Z93Q+MVw== +8Oo5KVF3BBzc2x3nk8qPOxwpIfOHuMp+ocVy+0AXWLA= +C+Fb7zAf4RW+4DYpTQiePWBaky2T5CzTdjDT7oDx6eMFRzxqyi5UuH7h8n15b3HMdnD2Bg3Oe7b8cwWtlmkb0w== +4aC5cfvQ3l9iGnDGVJAIjEiHBUQawNJJ2JjkxYBR8XV/+yMry60EG2nYyFnVjZC/CAgYY11vf9HM50Nqo8sSSQ== +4bCaCPES2AXNnKU3FRtflw== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +PV/aqPEboJ9Pq3+JHyD1u7IzHrxXfjtA7YbTXPrjpk5/B3hadzV3ftZxWzbQCiLD/JeHfjal5Qu1OygdvRmz7KccunK71voBxAuOdN9YwXdrOrYJvJOnBkhWOQyrrjCyvpF6kP2UPOr8C12g0xJiB8lCdx3/Z5Ce+0M2nenpOb/lH045pH0iQD0HDBz8yVr1 +Mcnw0hYRTOqKGEpOyBgpBw== +K8WxDHXgwdNGovmPv4l3bCxjrGV6ObKbdJZxtQ5auwWViMVgS03Egc754OuevB67 +xIgEw49v8sDhC1CmSU2fOSx7yWEfGg1vKqqPOXad9B/rYl2dcCjk8LjOFZgMAQlM +/rf/6WKYRysX0TuKufaOmCRITfLcrYlXOrBc7MNJUHEExOfwYXzJKsfplvwL5TGg +/rf/6WKYRysX0TuKufaOmK+AKWMpjn4snWLPmIlgbsCcKX1gGDQALcMM5Z7FuyUW +/rf/6WKYRysX0TuKufaOmK9kBkU3JDrz53d2RYMb7sszLzuy/mskIp3Jg+EmLZi9 +rWUZ2EBvPnm9di5UCBWoASJuwkacwLzx7wFiKfg94vlJIWKTQ+uPLkA09UViXUa4 +9qER4YIkDZjdPoLOFvkvNlk9NR3L9JigkekReXxE3xmdtdj7JmGhsYcb7wRb4un5 +1u+XjG/2+GSQRv6EzCaWRQ== +3vDcRXnLzCU8KW6t8SEdRkU3kVEcTs9Bi2k4YRiVLI6/B0Gy5PF44mVjoBtae4hk +1u+XjG/2+GSQRv6EzCaWRQ== +6FicM4CqMxenNhTRcLHhndR2GDv/lntmO+sbtn13HIVkc9B1zI2UX5LpLSXuCPlT +32pdC9DD05OE2l0oXazDFJQn0hVp8gc+C5QQmnaSN9hUoeSYMqla2T0C4R4eeBD6DflhuTwMX8qXxZ+uCNSKyg== +1u+XjG/2+GSQRv6EzCaWRQ== +h6T+tg8vGGKXsOucEwh3nnqLxnNvVWWrXKiZuXIG6qiDLad/tmVy0dEbd+QKhuiA +LZxY+aE8R2cSJ2ihvlvUVNPZkUww7pwaEFmxE2HiqRgvUFPOvzwbam3kaM6W+bNP +2zinJNDNj3eHwjxh2vOuZEGmSIe0J+MoJGW+BU5LSvc+i6kOxPW/AFK602WFK6bmxlQrd1L7hKrseQD8hXIaD9l03R7a4pCf/O07ikZvJk8wxK6ifHj955Va5Sge0npX4uEs3SUXlvxGE6uw1ElPGUxT77d4n+7hcbFkRuHmhec364MSr/AA+eEve/0xQesC +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z4NS+dyWIuctgwZb9g2MGkpDRTGNoVTnpm25oSGOXR+RyOR598O+VUWrXhufpZu95HYQpceZrisY7ZoCAUk8wLf +1u+XjG/2+GSQRv6EzCaWRQ== +OMWgmFohSY+HhGBycmwDAPvXi4Q/Detqp8NaL/+BD8Tc13tXiek8NNFjWVNHqANT +D9TdMirjWSUZlIBcDV7TR1j/eU/Bo1+eiNZ06muakZDw2DUgSvE8J/QU577njUb3 +CAhn8dRUItEbEErp4w+lX9tO1yhDltqrpu/1lMlkX6WpztWzmtAW/PHf+d4uuEHbSZnksfwIE2PpTs1bkSwI8yS0ZD4BNKZ9M6AWf0p4ce0= +1u+XjG/2+GSQRv6EzCaWRQ== +OMWgmFohSY+HhGBycmwDAPguPHGvuWdhpr8O7kAaTabHRi1DhwdeN5s826/klvpT ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +z+dYlwdfLYFVN90Ks4B0y61Rwolq6gKCDi/AKThfddItCsZ63QlGvJYc1Czo5YjdEWV4KCAR9RxFK39nwTeWoQ== +DzWU5pX0U6cGCLLtlLj3YP3wnqb4eg2zQDwRoDEc7R5c/iv9ijuULunRt8jzWUq1YnQQIlHFIYZ/r4CC6y4YEK6NI4B3ZccourZeOw8fL3c= +1u+XjG/2+GSQRv6EzCaWRQ== +yXSv/K3mXXlz2KvCo7xU+as167yQDD7w3um0fZ/Zo5E9JIjyB2KBd0obBhq914c8 +1u+XjG/2+GSQRv6EzCaWRQ== +0ZrlxPlHcPlwwdbql/Y0goAKwSbzsj5E0f+76P+ZTI0= +L0eUthVnpkGsmKFAX6d+uDdUftKQ37e2yKnmxKzZfC0= +1u+XjG/2+GSQRv6EzCaWRQ== +yXSv/K3mXXlz2KvCo7xU+deCM883gaWEi8LmG1e47+IypQlWwO46ASs6dnJht4QKgac5hZEMhVWDaMdCouYTSPbZmJqexrkts6twefQ6p7Gm4+8m3QD27ZBgJCBqCcDc +H1zFwfLyaEvixPsCrENwTQh03kfRdd0G/qa3bdyiewG5csj1cVE56lC+O/fo/7FZU3a03qFsh82HJPq3LbpdWQ== +1u+XjG/2+GSQRv6EzCaWRQ== +auZi0vV8Th1M5xUz1rxru/S/WPgeQ9D9vW1dYZxGC28= +1u+XjG/2+GSQRv6EzCaWRQ== +OMWgmFohSY+HhGBycmwDANQZ//yxLFZ2NZt0XkyM8WIxo3CYYe1psj4hyT7J3zZH +6Tszs5q0lk7xuZNv95x3DFnGucP2++Lm8QD+AuJ6qu0Vd0ZxpkaECa5kzK/6+Q0Ye2coeOggKVMjYn7Wus+S7A== +RqVoQzk6IR4JMpR/e8KmeBBBq50KHICX2aiKgGPkxZtJ1GPEBK5yGFwxFArBEhdUBSDg5+jJKgJxzOOU/SywVg== +fP11mcmXn9udwkOIYV19lXBgr4l37SABb2pThLc8BpwqmrPa6pfPqn01jxyPm4Md +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyVlWHWRVMXlumVg06MNKbE3dS8MvKjLjUKe/Cc7fDtt41mohfLoJvsAOaJGG/Uisw4= +1u+XjG/2+GSQRv6EzCaWRQ== +uPpiLeur2btrOEQRiBLfQigEnPc+SEYY+OKbGWGE0exRhzUKakUk60feja1CydWa +wXVIuIhRYsvivxjKWnaJzZhpBbxw15smk30jzXwsUOY= +1u+XjG/2+GSQRv6EzCaWRQ== +ReYQo2pn9rmJzmBZKqwu+xVbpre4IpoWWtGUSQVnByM= +SdiasT2hKhjFSAeRn9qV76o2NewaZrMKlDzTFPgke+MHJZQmVbLT8ofsTgC27SZiSGfeuUucpHiplzLvB4l7sw== +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUCsKQJMKz57hDGzJ13+V8f6 +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyVlWHWRVMXlumVg06MNKbE3dS8MvKjLjUKe/Cc7fDtt41mohfLoJvsAOaJGG/Uisw4= +1u+XjG/2+GSQRv6EzCaWRQ== +WpY9ZtIiA9ABEnbVj5DyJmWVcpFCUNIJIYnEeSA4FHWNeby09RxyUAlrL5Y4Wf+O +1u+XjG/2+GSQRv6EzCaWRQ== +avI5joWVv4iCIn016GtmLjpKF/TDmYZ2DkiySqe/TD//Ce16V20cW357y28WVVbL +1u+XjG/2+GSQRv6EzCaWRQ== +rVtuMq2z2rZvAFHgd8U8Gi7grSuEbMXjWt3nlf8Yt6EDzprHxt7hTAe3nKttC/hh+hg79ZsiVa4gNMJAsH/vbQ== +WkD9i24WL0SHRwWpBC8kWuzCHz2N4m57yrY5+Emq+aI= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +2i6x8wBckU1qBAmwxiL9oph2Y+6CCZXuys0610hvxNk= +WkD9i24WL0SHRwWpBC8kWqdZ/bUUC3b+1CuNif8RhcnZ2x6OMDL15EXevCqgszDiJMzZgLuir5K22zDwGQKCLQ== +1u+XjG/2+GSQRv6EzCaWRQ== +xYmaecvol+c9YasGJNvV/qhhNEUjcZwFfZDtZJa5DfY= +87Ms4TywY4z4R2LZYVXhx5tNMGfPRnY5UNEFGdEmg27+AACpFoxPt8UVeo3Xy+Fc +4bCaCPES2AXNnKU3FRtflw== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +hgaVJJLhwzT4fPjiPJtX3SYFhdem66oTfVZrP7bYjY01lCfhZRgcHjp3EKhAUQ1n +1u+XjG/2+GSQRv6EzCaWRQ== +t6py7+n2nYrnRcTqYmVk1g== +lX1lTDNy/dioNNWGwt7Vn1YrrlE6Bz3LPQLoIabgSvavE39QR33gtj03mEz2ZXNYXaF6rebKbI0fVwTv0fJmPQ== +LqraSsPmAErNgEH2eD4t4JNa4RV+xYcfEX5LTv+p9xm4KjFmRq2FWm5T45J86X6MSQjyxl2iOo9KgUunV8i1Pg== +xF643vIXkxvpAtwArjcNE8WEuf+lw6UUX2VFQYnBrDXbtQcthrh3TfSJrvfJc8PO +vXJ5dquhhV65cFgqiJNSzg== +0c/SnHA15Lp8idrmXI1HInrecnrdbTTjs/InHLqYQv4= +tsB4P1bMIORXogy+7KnvWagc7KIWmt4/itYL1MGJdYw2Q/6Jqxy0BW+BiBpb82xQ +GXXSXcQc7VKFoarv46j5oI7aSXymMmoaJG7iEnmnYD/MYm6jg1qyw9mDJWe+yQvkjNWG41Pb+o/nVXsVYQJcVw== +5atW9Av1Gc1JH7IpyAg4UfrAD0UAtbWI28bsXBSnCgMRn8P0KMN/8G9u1DF+ledd1KFZwf7ss4lHi9NEEY1vdQ== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +IiX74ldEcGDQXKip1H05bN+167oRPVQ39pYJkFOMTpU= +Mcnw0hYRTOqKGEpOyBgpBw== +UCrNFjv1VaMMEy3B4FzV8obiwulaoWqVBA7sXzlFLro= +VmVrGQo2zRokW/ZuO9bN64DWW2h47Z2bn9xDV8evWEGtTLYAd70MokLPjnkeEYrC +YH231WTDnzQG3bFilBiqIg== +Q3VFEGn9TH87P5446LMfE8uD9OcqjCZCzl+dH7oa+ys= +1u+XjG/2+GSQRv6EzCaWRQ== +3vDcRXnLzCU8KW6t8SEdRkU3kVEcTs9Bi2k4YRiVLI6/B0Gy5PF44mVjoBtae4hk +1u+XjG/2+GSQRv6EzCaWRQ== +ruTTNFTdswRs4Mc+srnRk0bG4n/8owB9YurdUoxgVWEHRPulP6ItpyVe7Vr7iDkO +1u+XjG/2+GSQRv6EzCaWRQ== +3JQ3q/ROrvwRm8WfyA+npAARKnASum6++D50zIpE/R6AvQuG/3wkscYmXT1XQloO +D9TdMirjWSUZlIBcDV7TRxhh2KHeKJ1VsN3W8maNQ37B426ov12fcLO12TdqnkqQ +OocbHHgFkR258yFUzxWbR9rwh2ylTC2OU8qnmIhXBPCocYkILEh+yrCdZ+EhH7bM6wC8z6w0bDULnLKekRi+tgZIzVMMVWK104cvnr/TcWxP+vV5j5PvknTH/DuUIP2ltXPJg//FIOJf7trKB8PfGQ== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYsBNzI8ySI29Nw64a94cEffbHlSBlXbGfuFOOeuh89TeM10ryA7G2otgu37lFn/HHQ== +L0eUthVnpkGsmKFAX6d+uBrpNoWGSfoffH/xD2VynzU= +1u+XjG/2+GSQRv6EzCaWRQ== ++pBWk5LxptzizUTsHnZAz2/Ryv0b/Ufe8w2TCjmy7OcFJCyKfbUntURDtOc1cFdf +E5z5ik3n347ddaQbLVWZI3KmUxpbIbyuQ3XX+9hJJ9/hJqDjXbqp/uuiRB1b1MGgwnb1DfSuE3JTnvMrshWrp40nJ9hQ670vJj1aiXjBX1BlzyGUtrO9cVUkddVLmJh6 +1u+XjG/2+GSQRv6EzCaWRQ== +dV9xz3yyeXnMinI4Y9Bh+pP704aButokeFyzhY64Ypc= +VmVrGQo2zRokW/ZuO9bN6+9JtNWtCjOEM5vrdD8eHUecs2LoaeesFMLbkty+ImwS +1u+XjG/2+GSQRv6EzCaWRQ== +4hdB7RJambPsQ0dtPl5R2bJk5yDbOboCJXKH8r58L7c= +1u+XjG/2+GSQRv6EzCaWRQ== +S+kfs54VNIP/n32P81eUqYWl7+7t/Z5112WhM6MBUhQ4I++QYoWH8PI6U6TVLdLs +g+oKTl5ncG77t0ddYxT7iINA5lehid/0FkWDJig2hvvzJtlry5BXYxkw4C6Rb1W6bZdU0EI6JDB1+K4kgDb/MCF2TVqWazgoyprzbiO+gU4= +Z8UsPk1Q7HtwjRd4g01ryw== +8G3AhHvwQIC2dfce6V5Hi+Sz3pSulk3ZPGPOORUFimqh/+3hiDvQFR5kSo57EELg66iO3UpXh9PkPjMeuVbn9LdU2pNhzDmLu2cIoKce9Ho= +Ys4bozvJBgtB7q16qcvtB4iNMB7C9mpb+KRQ/xKMgCE= +Z8UsPk1Q7HtwjRd4g01ryw== +/6nzhTiQpSJoBsodavJSvhrnpsry7VIjV0S0SBrQKxg= +03QGGoLai4/2Dt+r4/Z7bqT22Tv5nhkPQ4c80/zB6Hk1HlcVBsxuEVamB0M9fr7kF2oLuG2KV1ofJX8Fwgqh+g== +N2AxSv647SE8o7pH02PLTTxQJZyLiFQi3ZKpaS+0d+6+cMXYt0/6YMi9vJ8FGxm2NxYyXp60JimXSdNQmcuNxg== +L0eUthVnpkGsmKFAX6d+uFnEdtTQvGxHWVdWPengwM8SrOznNDd+JSKhY5WBoxIAhZhjHlAc5QQze4ffdB4yQ4M7T5sU27auKKLvUmePwgTP1YBnZFC97G0Kn3drNW79njIrqbRsrvggaPnHBPiWZQ5OhFA+gDJ+Zm7ZnsVYyX0= +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lXyfKXxXDhOK8Y13cqRVbOCfJo810ckrr45hdYa5VfxH0n9n4OtlDZG92Ia6b0Voo0A== +1u+XjG/2+GSQRv6EzCaWRQ== +OMWgmFohSY+HhGBycmwDAKlzMy1Qrye4ySBrbLpHQh/10ohLj1C1SI23bRneYhun +iVrumwMNPYZ207haO9u4MdLAwVB5xOk7iikXIMTVL/qL0+TBe1COKZNOXz9EWzaa +CAhn8dRUItEbEErp4w+lXyfKXxXDhOK8Y13cqRVbOCfJo810ckrr45hdYa5VfxH0C8N5JEg/9LHhKHeo0RbNqg== +1u+XjG/2+GSQRv6EzCaWRQ== +Srf5AIzkpfdN0c4nOY6t4sGAUtQIfdofNy7n/g7tol4= +vHI6ym+jnn06PgL99476z6Uwx4qvfe0JegnXA4qmLNl8/pYgnzU4wh0jCA58VVutfY4MrkqewDmJI+ejDGKNQQ== +CAhn8dRUItEbEErp4w+lXyfKXxXDhOK8Y13cqRVbOCdhCWl1Qh746Xywu95/4pkITT8KgljSGAwu/5cKQ5rTniUjdofLMTwl9oAp06PR5ls= +1u+XjG/2+GSQRv6EzCaWRQ== +OMWgmFohSY+HhGBycmwDACm8KBM9McA6bMfF7bWoCcq/96vBjywwKMifRE7T7X+G +rd+FFDwen8tJkKTf8UQJhMdUyYbtrdwamsCwQBeqM77gCC1coFEm67jW5p3AGGTFaEx3k5up907Bu9eID9IsuBdOvJyvhUZnVYJWja7Du7U= +CAhn8dRUItEbEErp4w+lXyfKXxXDhOK8Y13cqRVbOCciGRpycCKiBr1kcnrqM2Ni1tSv5A/haJTQeOWgwogWK/mLhT6Lu8q34h1NwJSnvtY= +1u+XjG/2+GSQRv6EzCaWRQ== +Ou9p7fXXFVhRiwA9kUvqbQB3LRoICh5tKq/vC0zda/YXw/tQhr8ReoxBKRzjqWbY +TPDhEyc3gvdCLuSpfWPtkMo217/b+wUr0WK8q7nkfj/nu9POVb56XZDN6pu5OBmU2Eo3m8A3YSU276U1x5fvHf07fdUjNR6Oj23JkwK+sbom7XizX6t2ETi9O7cG2ntDEmNei3bJgzOyduxBN5qqOg== +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUCsKQJMKz57hDGzJ13+V8f6 +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyXW1h+kfMT68veMT5Cm+2zxz8Vmt2bA2ScEIICK1Jf8Kjys09SeWqEQXr4sZGzDGtI= +1u+XjG/2+GSQRv6EzCaWRQ== +X0HrHnnVVAGJX1LFiXPkceg5OodpPnylmQkbSgbOhdnBFW5kZYDLk5gYUZvUZCeu5XUOTVzbErklH12QSsLHvZvciMa+XkhozpxGVVTuajVPncY9lIy5bxfMgY49D3gc +/NaqBMGVF/kApFw75QMoWE7Bi8yCcG2HPuA7c8S6vUNoikVL8DsXPN6kxcUhLAMpv6NaSljIODjfhl/mDMRvEz9Zx2TBsH/cDKirDoVcRKDRTmOSnNecVHu3SALXSizJ +1u+XjG/2+GSQRv6EzCaWRQ== +6tLF2b2MbPQJ/+1LCtIRa57AcYo8PJYJ+3q+V09oOKv2nWk4K+IbPEvRI1bzSMlQHtoTXBVrsp04deCTn4biHQ== +K+etge8Zj4gb40jmKXSx29zMdWcXNAkih8N1HpwmcEU= +8k1NHlf/dJJ71i6dP4d7XM8rtRFXkuktgcatPKvMuoASR0leN0Aiyn0dzAIvBBmmdN7XzJeHHcfBA33dE1+9rg== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +ZDt4tHr/EqgJvUzyQHIFSmiu85buXZguIJEShIlI8YFKDS0DK0ZuDq5qLGHmR8M6vIb1wA2eLZ45JowB+n1pMQ== +1u+XjG/2+GSQRv6EzCaWRQ== +CTyZ7MfB5iFIHP7wDXTc1o2wRLSVdLEtFqWiz/0+BDU= +gNkHdAF675KKZeBcAgFegB/sM2X2Y6bJt298opT8yTn9AD7jeF0eTQbrz/YtEJk+ +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +TDIZYXrOARpw3pgUPeUmeRLVXw/+4xgF2d1NrS1ZoKzqvY+UDd54F49SOls+n0qz +CO7W/6aJFQTRG/xTm6ku3kw62D37m8x0vrdg4RooiDSQZs6q0s61X9iJ7qqN0pM6 +FfOMsVlmcn2d70w9TKFQhhFhdaCPRRsgc9EyfesHHcd3gHPIGQd1OrDWqSXKZh++ +1u+XjG/2+GSQRv6EzCaWRQ== +ofbnZlafmPW+1kVnxnIwQOFl55Q6lyscHPdpS1CcGuo27gOlJCnQtk6SBo2EF/PB +x4fPj1hBhtair/f3xLPwJQktZQsbYZcnxh7Rg6O43sL4QEYXlG2z4QbhMGU1/aSy +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +U7h6BIX4p+v7nxzqr+lrQD35elqwPh3VgUWs2GHxhOQ= +Mcnw0hYRTOqKGEpOyBgpBw== +x+7nl26IqLqPeUC9UGWi4sALKH8l8SN9EE0EoCaUklKHkzReRxFn0BorkBQFnyoD +HxMcStsbV913HJxaH8CyjlVi2ECFUqb7EDcn7FH4LgoBjaR1g4xrkrZAcLSRghPh +03A0XX8qTsa/Wb4yaQ9jXv/t9evdswNpDBdnjsuz+f8dWJdjlhi6TMmu5N/mCoNibo2WXpWuFWFgYk2e8485lQTeBC+eYbA1rr0ygM+oXFH3bKzycimXoSyrCxyB9l2W +Q3VFEGn9TH87P5446LMfE8uD9OcqjCZCzl+dH7oa+ys= +1u+XjG/2+GSQRv6EzCaWRQ== +3vDcRXnLzCU8KW6t8SEdRkU3kVEcTs9Bi2k4YRiVLI6/B0Gy5PF44mVjoBtae4hk +1u+XjG/2+GSQRv6EzCaWRQ== +ruTTNFTdswRs4Mc+srnRk0bG4n/8owB9YurdUoxgVWGVu4Z7ijI97A5SVbr3+BdCmF01aiyZvnqpwcnaEp0dNw== +1u+XjG/2+GSQRv6EzCaWRQ== +dskxeNlXSU2ewA7cm6Iz1fYt678fBjl85HT3mB8+LJM= +C0sKqLgrkjALklBmidqKczuu6n3ZhV3ftlrBKdkQlL6TAqQxrxb3fGL+WZpjpkis +RqVoQzk6IR4JMpR/e8KmeBBBq50KHICX2aiKgGPkxZtJ1GPEBK5yGFwxFArBEhdU33OpOKmcHAynmrOx93P56SxkUrOSmhL95kGcSEHq/1Q9bivwrjJyQYMc+22AJN81dU5MSDYUNVba0rdJ9FmkQqb9gsYLWLgWSI+qfxdKqGU= +1u+XjG/2+GSQRv6EzCaWRQ== +x76N8xgB8xjuhknjQsti/5opyRcSliwcCfQ4SsxEiAPvMf7JggL8LKCao8x4abcn +dYBxYC/SPPLDq4jXDlgeUXWL1ZZige1IGh/Ka3jXGFtW1ITTwiXwsPrRCWNYtWnV +RqVoQzk6IR4JMpR/e8KmeBBBq50KHICX2aiKgGPkxZtJ1GPEBK5yGFwxFArBEhdUBSDg5+jJKgJxzOOU/SywVg== +VTFF6c252MOvMdjaSnnOt5zC6iglsMzhnYs2G6lrDCY= +1u+XjG/2+GSQRv6EzCaWRQ== +u+tG5dK3//gTqbaBgFgQJQ== +5dgN48vgAE6fUdm8Cg+kvaNYK4/BCgONdK4dvjPbbcx1+U/3TgvUCfMVz1BbLXjQTe18iSOC75OGg0Bzyt6J5Q== +GYvLm+RxY6GAhJqRwEJV2WC1caUdnFFkgZ7yW53m1Aaq4YGSwmm2Dn59A/CgkaOOjPPf97hfUEHdVgaB5aH2yOGcD0+feSNWsm5vsfsduE4= +WASycj2Wa6aTDHB+95lzJWfKcUAm+/xexypzaHrXYC7qcfXNxKS/UqnVo9AVhhGr +flaxCjdiK/3pdKEQMZfg01rU4BTe0j7Jdm0gHzKjl9QYgxabYgNkUdYdcTzlTABNi2daxEc2Yu5VdMM4/i7CfA== +OBqjTkQhw4mQaVgdxp0kIHWqUGSF0wB7yidkUDsuGWo= +ys5lzs4DwATAOzYtPZvF1zp9axmk5Xs8tRr3D23tmzG17k0hbYoRMPXBNRXnqhX5Z+R3zKHWkMzCq0GRKueCD4eShnGrlR1umutWz/sTZ9K1t6EHZ/InYovQ64ktijdP +vqVKYDeh6Q1Iw+2qfAj2xhtY3zrwlUINQqWLzgvvZ3Y56fX2wXYiJ4HPKOzT1YevEkRkVBGI3nXPhS0L3UgFQ+ZrAtVT4Rc/LtUcCL6xcSM= +H0pYAv/06Ol8kQ7vSZ2pjeDJUGWx6uI3t/U3Oy0K+fdd8zmrvC05fpjVSuNMJCL8CcG6EolQIxI2ocMuWpERGlj7xYHen6D+2vFBow2doCYbSGBj28JRdTmUVhCQLT8+ +x0nVCxDD2zr+LjlV0wlFgg== +ym2R2BitFkCIemDFubiwLg== +wXVIuIhRYsvivxjKWnaJzZhpBbxw15smk30jzXwsUOY= +1u+XjG/2+GSQRv6EzCaWRQ== +GlaIlOOYnMM1QkhK/fd3iMZ/ohgTkVJVWE0ntP4W40jJLwkrx/ac0SDy+W0FpeStPGjiDHZ4r9w9syHUBaIXzw== +iLhV4FEnj1EmEQKF/ZATY3M/FNJUHhCT23gyUqYFd9jWjWBRzt8Fu4R/X1V+5bDanFzQhiDtuhcX6B3wTcS7ka3VsCGp5UGiO1HthFTQZbZb4Oc8CeSz7+knCm9m3OZ6xcSnuqmtB0aOCCMvkbPN3VhtyqM5BHfREIV0qEekcKKxbF8LYCfdz8LSWZ/EiehHJB3ziKvtGgMXD0UlbagogVjQAL8ZGyOFaPIC0JCSNB7yOprJAzw4EtnNyfzW7q0DRRZncf4wrWm1XcRk586glQ5+hS5bAY3V3WUbnLntTXkmnetDE+ulhtlGVy11kIoT +arZDCk94/K/J0iZu4oF+HRuqYEk8Vy/hVmDkwxFFqLlpKv/NJl33jR7nYw2cThvOTx9AmeVFT9p/BxfOqZik8Q== +1u+XjG/2+GSQRv6EzCaWRQ== +ncUn2TP8ZQ4fZMBYk+CUrWeaSmVXN5jSwIoxdmYJQMztQZ4JvkpRAxRbBaoWp0U7 +IaE0wXi3qVFO6+jRuxMC7qoy2fPw/2Em9xHbKwjSAwOPIrYaQ1d1x/FXvJrgjiZihZkBPiPRjA30kZdERp/aiw== +1u+XjG/2+GSQRv6EzCaWRQ== +YA93D7bVXAwMe11nxv6BP8N2JjEI8RKP112QbKxpuWA= +VmVrGQo2zRokW/ZuO9bN605Wj+7zpmTV/RDAnHeUp4v8j+vBMiP9wzEjlBAwLSyne822pv8KTa7xVdMPTpyQt6MH4sxXZOi+dBMrqgMrft3ZNG1CSAdMn4zoeVqqtbM0 +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X3r8XvXVNPjPJP8mrPh5ahg== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zAgKjCBCMFWcjEGKZKeBnYuHbmqnbW+28rSCnK8i3pDT9TMA+U1QMV0B5YS8m3XvUq +1u+XjG/2+GSQRv6EzCaWRQ== +uEiJaDMDAi1FoDM5lppdq9SF+6YelJsoEY3ZHuPMu/LI4rDG3pLdm5x5wjDwlj4g +GqKSCRi5O2RDIx2yEhsWngsKn1ziJo9iV8IGQryLbuxInqd0vtp5a0RohTqZaJzl2iQLfIoF3GeoxVHGSh8yPFyw+7HF0QxCRQD+b8Y7LLlT9OPQu+7ispLKoDk7W31NoCmCTtOkKjiyBIda3rfSTA== +CAhn8dRUItEbEErp4w+lX+NNLjQ3s7dTCionRz0FX53bLy7t8/38W+QbRkSn/k6fBpDHAETQi9H9KM4C/14NFP18j04UcGidK01HI8DQXS1iB91jNLybWJeWT+QOGbMh +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyW/8gUwPdqqjWj5njHrCfAtKpK8jdvX2JoJE02bBmKZZA== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY5P7pklwxOw6dxXjirT1q1bEvfiu25vBLYVWLwsqVoO/g== +1u+XjG/2+GSQRv6EzCaWRQ== +ELZnlgNdTWbG8JmZqANPTtICkopfjqD2q88Xf4yxAmE= +EmZYXAoWwLEw6AEemfKeYDL+Mm6dJt59rMxv90xwwQimFjCIiHVBZhVl1R2yvUg7iVmyjZH6b5XeDmq6hHFFHIBUGAwPUGkouwNMHgvaAb4= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpnFTGf0yf7GnaDVYS3coZ6Yh4KLB+u22zIIdWhA9g05+pUTZczg4x2J3oXBMHMP6yAeQfyZgxWhDpT2a6smi7fZmGavb8vp0xuKcZyaMbTZbHUR8i290iZ6b6nDiGf4INSi3HXPz3QMBSFmYSWZySPH6SVzcwFrcv+ns0ROBMBR8= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpnFTGf0yf7GnaDVYS3coZ6TxoHBdtsrDWqesC3XQSmVYf8i0WrlOvkFaOqUFq68PUm4P2pVA7ABE1Dfiqr07OthiFuvXWs8h9dR0+n8S3m+XlMdEj/xkwQ6szopis63QXA0BxuUtCZorFYNexwVURWtGRCc+w+XoPUzaOOJ57iR0= +1u+XjG/2+GSQRv6EzCaWRQ== +uiIF/qFEXBu0rVeM0ZiVRrsuzpVplMyIrEoh588zQNa7LgNNPihQq+OxTL9IXdo9/laR/cZLatR5sNKP6+Z2iw== +ju17ZXRoA3BJIHkFg5ZKWr4pfPiuJhh95gPAvQHudLUcfIARKX9jv+Grou8/3OKd +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT3PFDAZpFnQ8l757+75ltgEjR9hK3iRyKMAFAfyUdLSIQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +2/KyATjQOqxRBb1WFuBZrtK/MLzOCJgKrDkN6X2yG3jZLCbxczaoNIN74FewOHig +47qB4zCkPWSNQHMg32YzdUy4ZK/KQbraTTR0QOCeNW4diavPCbKBub6PI1jH9Vc0li6nX9tU6x11U+/N7xB0wpFbOArXXE0g+En1Al/orak= +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUCsKQJMKz57hDGzJ13+V8f6 +Q7QVGPDExqjR7ynTbPAdwUHuFe4OKIlOD99L5ojaFA3L2UdCkE+gebH/hMzoz22JgvfRDkI0tRQJOFMEAhM0KQ== +1u+XjG/2+GSQRv6EzCaWRQ== +pxrc1WLH/H5lt/NGAqhTitoSpdXVvJ6Kv6ABkIYPYPTiR0z/V/PSUvEe2L8snFA8 +ruVNXLPFmkyjjAB0LDE/N2Ymqg9ceD1CjYDATDeVK8Y= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +vcNCYe+BnNTo29rOge0xtys1DPV99nqlMXGfoPHaos4= +MZ1exjepopSsVT4i3zM7Rz+Ui00ZvMt4Q5SQB4StHAexXCJqO0zCYRVJtuqMihFn +lzvxzEnWSUQFSnfrWc3cNWpvNAl84J96nrLlEsRgPeI713lHwwNGRkwGjxpr+Vg0JeUJkHMF4miEeIZiYFjyCJTiVRINYvVSQ/yhA8vrUqs= +fB5YVyq+Mh1BzTmuk43D5unBtLKBpLX6HXhyeOrv9ffzt0qlo02qsq5dIaMDErODiCGD1xXhYXE5NI8oelQlbQ== +8KyOS+Ff4JzgjqlSdBaVLa8RMBHkgsKXCsKW+BjZk5hSPQprgE+Ij9fYVynPuzyl388XYTIBhLXUyX/pYgtAH3cYcoiEyW28HYNTgi02+UDvJbVxZe9Ik5uwnV7D9CJ2 +1u+XjG/2+GSQRv6EzCaWRQ== +o5JFNYRcDoTkRRL5F2vl3Q== +1u+XjG/2+GSQRv6EzCaWRQ== +bld/RgDAciWvrNce3UF9ayA0lJr9osshYNoRb1QHUhxJMDZ+kSfX48vFWKuSE+wB +R1d2q3mo0tt3i9cxkLE/AGOYQJ/iMDUe4unhX52RYj7/TLLEmkGwnB7aq8oxciOF +HGnYSz17CMUrJXyLBdcBXeL9mvyGtPW+wtb6CzqK2+5VFGSAOfwhOpBwlqWhZfxpxidwc67dGaCmgLUSxWeZBJSdNvjbQUrEKXvyYNP/koZqcMg4kO7cTCM5tXHe6PF56e1gakMoQrWOPieKtIA8CgIVjNcHBAAN1hUul5pYqS8= +NLg+36Rajr6hGkly3f37K4i3hjcUgiTv6a1u1cIla/WRgttaJQUlpbazq+WHT2KZ +eMNmCZf8V6J71T4XuVP27PvULBvVHnmruRI2Qz5hfCrVO1PZPj/Ph58vUnfdfVpP +uXoZXAVOz0sQA4gwnrLcfUUGZy7debabEfu76Egpvmg= +09UuO0tw3kJ5J618hVggcQ== +yi4USbg972Yx1t8cEq+SaxT/h0vE3+fmWsgbJM7vuAQt2AQeheXnQ+jB027jiVfE +1zUreoqlIVA+vRqhYARhcllgHIcu5oCip7CBwrzZ/d/I/9O7w1JC+cQr38BVLFXUrJnbWRG7elpiBt7V1OUSVw== +QKClQ4XOaAjfVBfstd1swg== +MFZbje12INoH5rrCsfSaIqZIf4nQgQ2DMwYznhp8MTX08M9XPRTCpgObVs4wLdiWxntq0EcLin7RQL/YElpAu5VGBxZ3qDU371dtBOTkAyjGc4oYD0bsrLozyLXTs0GhRZgxdQS7Gskg6eVW5E83F3MUL5wSXgR1wWFmiN/fbPWue365Rit5sNTW+EgNClZyboOJWgYqJPw/x0kH1F1MlQ== +SCzSXq7nmS1ZLNRPWXJY+EK3m5qXAxVb/9Vh4tVeM06eKt6I9zZisq4iD8IcJ4KP +661hZf7vhUQ+50okfwfTXw== +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +6zjp987/AMIbKnKovhAijQ== +qwRFMQtB//Sb3qPvlGYoE8E+xP+owOVyFQ0q2crm7Bw= +aenAUWzQKgBPx3+h8uG+r/5FNgb8FCyZOmvzmQcgVHnq3vhWuRD2vC5Izlcjm7rQ +l2yLikINIVyLeFLumT0qzoqlRnXCrSN4knk1KIfxws3eP5nbL6xsfEMZ3XPuRA++v3gVu88+DhE1nQxkTdI3zg== +4GguegA3qQ75gzaokIqjx9k+qXfXkvTCbb7ehNIEE+DwvbdaGkHAldp2VAB8Ujup +6HBnM1U2T6LZrk6Crmb3zSw+27WapQM7gqZvLI/3sacGf2Re1hlzoYAX2WbZbWMY +1KNPUTnB0+GSRuoO+9nUS1JIDmVKgL45RpJskaf9gFyLVy+Ui7nbPXMT6LUNJINO +6P2gJ+vDC1pRh1WT99ShL8llWAjZiunK3McnsIzPqcHzCZs1Z+nRKNy6bk9YlopocGfWFCnC+mvKZWwS6ur88g== +nrwNQBTj88dT7zfiKny867DRRc5nxwhABXy7lwigvRxW/Vzp39tFuvuK3zb1rRxK +AKbJWWdo/DU+a/JrHx1OHXK6ROJwEocJqPIcSJdJwA2hZVELZ+2Wskryw/bOqTVD +yFstOGL9WPJZAYINe9h/sTjDoynAzYDiDKhqXn2z5tdt9O82YG+IygXS+Mwnsd2aOz/hcE9XtXJq4ucGLhMZyw== +w4DgeJTyUUQbStJqN2eVkpgT9LWwau0D3sP0VXSeyp9aH71mM/5SQZkiHiwzN2NJ93hvyFLCYlG7NZ+yy2yNKA== +yTTp28LIim5+YjxAfVNM0HMBj5i6hO7cEwrn4UqGKuxdOiaOTrAqs+rSjarYssN+onSj4darz2KahQbGg2a3Eg== ++XAZx/LA8Tqt2DY5E6RCz+RMtgTQd5aWdbscNadpGT0HUonFD5NLcViOnThShnPoNBbJQ3lEF9tlqtzejL86vQ== +XIHRHCfwuCFaNecBe9SVlA== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +U7h6BIX4p+v7nxzqr+lrQLHkPVBLWwWs53akWmvnS6Y= +H3X3EGUrpS/29Wm+tIafOyUN8R5pTpM1CsRClEWPZgVpkgRyTwWuh54nHfoGymxr +b4OJVZe8QyIpjuTpKXDL9A== +L0eUthVnpkGsmKFAX6d+uIYN9x+SK7VEV0i6S3Uj+u76lOGg6RYQqX/NDsCTZwiEh4gNkI/Qv/zFNStH5jSeKA== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +YY5wEo94l9qfo5TPxQmUgiOUk9wZ4yEOytm43lMuFnOPIa+XXU9SaayNCqc6hc7h +1u+XjG/2+GSQRv6EzCaWRQ== +wWjnF9eN5P+UWvdDUKiAFegjuAMpxcMGGMHmthta34w= +yDOAduTbv6/KjNMoxqPpJ2w10n4roBaEHSpxMMKG6UTwiBX3VHChUlfx1Sav3pODM6bim3j43otXnHneUJYNvJmdl1iMLY9XgofE/AuzLzY= +CAhn8dRUItEbEErp4w+lX+NNLjQ3s7dTCionRz0FX53bLy7t8/38W+QbRkSn/k6fBpDHAETQi9H9KM4C/14NFP18j04UcGidK01HI8DQXS1iB91jNLybWJeWT+QOGbMh +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyW/8gUwPdqqjWj5njHrCfAtKpK8jdvX2JoJE02bBmKZZA== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY5P7pklwxOw6dxXjirT1q1bEvfiu25vBLYVWLwsqVoO/g== +1u+XjG/2+GSQRv6EzCaWRQ== +PzEvxs4M77eQp4726OdUdmiaFs5FrB+rN501JI5oyeQ= +EmZYXAoWwLEw6AEemfKeYE6959W5nEE/3bBTBQPwWpM1mOu+SGdpiCkmvvutBWda +1u+XjG/2+GSQRv6EzCaWRQ== +ZJDi9TUcMjkN4cH/dB5wm1El6QQzopeDtiLfApPqTxE= +UUUaCiklesr+fpQDx89I9CEGENY8eKaRi+RVs952oN9/jAJF76rgi5zarBh8nv8cjYxGRAqnnFkZQ2StmpjCmlIEjU3OSsouFY3xx/5yLGY= +CAhn8dRUItEbEErp4w+lX+NNLjQ3s7dTCionRz0FX53bLy7t8/38W+QbRkSn/k6fBpDHAETQi9H9KM4C/14NFP18j04UcGidK01HI8DQXS1iB91jNLybWJeWT+QOGbMh +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyW/8gUwPdqqjWj5njHrCfAtKpK8jdvX2JoJE02bBmKZZA== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY5P7pklwxOw6dxXjirT1q1bEvfiu25vBLYVWLwsqVoO/g== +1u+XjG/2+GSQRv6EzCaWRQ== +byLWqIOe1XbkFTQu7j5nOqKeWJfJJJ/cW+Dnb0APrGk= +EmZYXAoWwLEw6AEemfKeYGdwgSeN1PIbEbfhEAXejJWkmhL2PcOjg45phhb2skZz +1u+XjG/2+GSQRv6EzCaWRQ== +7uEPwOBJ6pip2ywoNW4IE4VbaRdQZAV1GeeyVgXoZnk= +na8APpoRB8t6c4RbUreK+jVWU1RNWhVsPHkcHM+g9Ikqdn89KNcL5kBOWHQd//53tnUTYT6RPNUzOtPRo8dt2Q== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT2RcWpkQ0BBnE0X4ertjcM2O7FXNP0Ca5yK1Wbc1hUvLc/1O8sbhADCELO0EWSW5QKQzil7HdD3Kq23yniR6EBhbgOwMyMfbd80M2UsLEZTCA== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +fHhyoqZJ+sh07SfZEu+gxL49MsVNHwlg4gQuNEkifi0= +UfrFLwox5T7Q8/E+j18mDJclg8/8iK9JYYboPg3vbv9qmGu6PuprVbXp6GQ5aVde9eCvyqnqlETt0eQgF8iF49QJfhurwrdj82K0cfFAHVk= +CAhn8dRUItEbEErp4w+lX+NNLjQ3s7dTCionRz0FX53bLy7t8/38W+QbRkSn/k6fBpDHAETQi9H9KM4C/14NFP18j04UcGidK01HI8DQXS1iB91jNLybWJeWT+QOGbMh +MZ1exjepopSsVT4i3zM7Rz+Ui00ZvMt4Q5SQB4StHAexXCJqO0zCYRVJtuqMihFn +1u+XjG/2+GSQRv6EzCaWRQ== +M+B95+gF897A4LE3SraesW9jqG6vPRMBZ45nxtjn/F4YKS14BseTSdBfBPdz0a50 +JlOeqrNAZXGZgxJpw3ujiDg8IoCONpPFeiTT3WZ/aLasSWVSHTBf49MOnKpo/v2f +pUencgOuLmNHRknRyPT9Xx3oP7Tmg5fibR26+fVERDzKtPqLarPtCnVUtVrzLFlmF9uT+NgPEUVOrc8O1CEqQqRCwXUAtaqsPvgJ+YHbrGROHSkwtgGYFxBfKGpDbGzWijmLINXAgUw150Hz5CKXeHj0V5MWdo85LRgw1AoA7Tw= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyW/8gUwPdqqjWj5njHrCfAtKpK8jdvX2JoJE02bBmKZZA== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY5P7pklwxOw6dxXjirT1q1bEvfiu25vBLYVWLwsqVoO/g== +1u+XjG/2+GSQRv6EzCaWRQ== +n8+Sm0LGPcnI3Npys6C0BV/7VdctpXK5jz8n71G2NgI= +EmZYXAoWwLEw6AEemfKeYGdwgSeN1PIbEbfhEAXejJWkmhL2PcOjg45phhb2skZz +1u+XjG/2+GSQRv6EzCaWRQ== +B9YE1vegPqFZhW7U1qNrMKSgsytgrvH/2eTz8V1hwAE= +lSCItBDj81FscebZao0b39tg7nb6Ucxna+I6FNpfYZU0wH/VgGPepuRFQQciC3fCzS9uyA3SzsxN5Q58Npc8tOT+slmDqaIupOEbXEXl6xN6WByRxc60/K+EqXBlUEvK +CAhn8dRUItEbEErp4w+lX16+n40eaV6hpDW/6jSggsErVdLIeCO7GpA5aTIWY3OqL6w1mrhk2CNVFcw+RI+FuR6xW+eHcODg8NN4pYiAdBI= +1u+XjG/2+GSQRv6EzCaWRQ== +G0dSb8cfwCOcOtjEWz8TiMXyy0yCBhNe8qZ/+hOEL3M= +QvwWeJbbvcvxBQxj3taJOtGxJXXi/nmUe3b0KpG16fZ98qbugsjzYpFQRs5T271Sl82bSyJ00KVKhD1Q19FWQiv8YRuD1wOOycCvOLu5pdQkhDzSqVE8cOGRvSrT3xs6 +CAhn8dRUItEbEErp4w+lX16+n40eaV6hpDW/6jSggsErVdLIeCO7GpA5aTIWY3OqL6w1mrhk2CNVFcw+RI+FuTE8jycmPD5eNNO7fhwVyuM= +1u+XjG/2+GSQRv6EzCaWRQ== +uiIF/qFEXBu0rVeM0ZiVRucOq113oDIxjuY5HbU69bYfbQfvTjQC5NZ48+QAKoCgsc8yeIJDuM+Cl47BzppOiw== +ju17ZXRoA3BJIHkFg5ZKWg+V7+37jlKz+K2dTb5oHVb5L+GjrH/+qvmFPO599xZARR/tPk6SENuiCyM0RitPDA== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT12X7G0fgM/G1g1mq2qs0HiBi+U45fZJ1W/OzC2qNiQPA== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +2/KyATjQOqxRBb1WFuBZrrLqCF/uEBM7vP+L6PwCMKN9p+q8X6WK2g2t5KY0gaTy +47qB4zCkPWSNQHMg32YzdfQWa/EFuV9bOpEezNrhnVTXpT7OxL/rmhcaYi/87liiyOqos8Ow1P8uaMr6rE4YjhG+XurNHIUDkma7t5bKES0= +RqVoQzk6IR4JMpR/e8KmeBBBq50KHICX2aiKgGPkxZtJ1GPEBK5yGFwxFArBEhdUBSDg5+jJKgJxzOOU/SywVg== +Q7QVGPDExqjR7ynTbPAdwUHuFe4OKIlOD99L5ojaFA2iOKYJ3ochJrOwbe3IsxOB +1u+XjG/2+GSQRv6EzCaWRQ== +pxrc1WLH/H5lt/NGAqhTitoSpdXVvJ6Kv6ABkIYPYPTiR0z/V/PSUvEe2L8snFA8 +oi9cfgCdWXEjlixQmXZZ36u4NEhmY4xEjyWTDvLrpLk= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +PfBdnBUSRvetrvXb3KKmYjrMMbFx4Hu/xMfwM7V4tT8= +lzvxzEnWSUQFSnfrWc3cNWpvNAl84J96nrLlEsRgPeI713lHwwNGRkwGjxpr+Vg0O/mIV7z3HrttkMyqTPxBesPa5HyHRTC+O3s+qJX9Cuk= +EMbPxLmnXz3C9+PY6ukDEJbIMJHUnqpEg1qX/67gXW3avaIOGUNe4YeCjCb+n/LpHV5+idq1YAU77Jsn+LGa8g== +w/pHxNu4waIaMplDWJEPc5+0zVolFaxS+yRA8k0rTsA81gzByI+ANivS4kmnj7cZKcHJZx8RmgTfKcMjrKWH8RYf3ail6nbwPDveChS9b67QD7bSlyUR5X8pCDxKhz7e +1u+XjG/2+GSQRv6EzCaWRQ== +o5JFNYRcDoTkRRL5F2vl3Q== +1u+XjG/2+GSQRv6EzCaWRQ== +q+wANJnRBfSeOt8GfRM+HHMKe8/R8tQaXHlCNxGp8DE3GGIKvah9L4d+XXu3UUGT +HGnYSz17CMUrJXyLBdcBXXicBhFS+rjbAxXSuZjBzmtnFqvSvj7ZoMVroYsSVM6rA2sUKv+jBhKxzXDt8NLAYRVp2yNNIpYF9QUiYmBSxuUS15U7Pf4Qro2gC9NPJHjM4REu+C6Fstd2Felv9Zb5Zw== +NLg+36Rajr6hGkly3f37K7uhk90incRRI1r8ofy8z/TPpcO1jBhQ8MMIXNK4XTXM +eMNmCZf8V6J71T4XuVP27PvULBvVHnmruRI2Qz5hfCrVO1PZPj/Ph58vUnfdfVpP +uXoZXAVOz0sQA4gwnrLcfUUGZy7debabEfu76Egpvmg= +09UuO0tw3kJ5J618hVggcQ== +yi4USbg972Yx1t8cEq+SaxT/h0vE3+fmWsgbJM7vuAQt2AQeheXnQ+jB027jiVfE +1zUreoqlIVA+vRqhYARhcllgHIcu5oCip7CBwrzZ/d/I/9O7w1JC+cQr38BVLFXUrJnbWRG7elpiBt7V1OUSVw== +QKClQ4XOaAjfVBfstd1swg== +MFZbje12INoH5rrCsfSaIqZIf4nQgQ2DMwYznhp8MTX08M9XPRTCpgObVs4wLdiWxntq0EcLin7RQL/YElpAu5VGBxZ3qDU371dtBOTkAyjGc4oYD0bsrLozyLXTs0GhRZgxdQS7Gskg6eVW5E83F3MUL5wSXgR1wWFmiN/fbPWue365Rit5sNTW+EgNClZyboOJWgYqJPw/x0kH1F1MlQ== +SCzSXq7nmS1ZLNRPWXJY+EK3m5qXAxVb/9Vh4tVeM06eKt6I9zZisq4iD8IcJ4KP +661hZf7vhUQ+50okfwfTXw== +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +6zjp987/AMIbKnKovhAijQ== +qwRFMQtB//Sb3qPvlGYoE2ancTXrfnV9a36LAYM7eUkSUoQXtyhACA+39DOLvWgk +aenAUWzQKgBPx3+h8uG+r/5FNgb8FCyZOmvzmQcgVHnq3vhWuRD2vC5Izlcjm7rQ +4GguegA3qQ75gzaokIqjx9k+qXfXkvTCbb7ehNIEE+DwvbdaGkHAldp2VAB8Ujup +6HBnM1U2T6LZrk6Crmb3zSw+27WapQM7gqZvLI/3sacGf2Re1hlzoYAX2WbZbWMY +1KNPUTnB0+GSRuoO+9nUS1JIDmVKgL45RpJskaf9gFyLVy+Ui7nbPXMT6LUNJINO +l2yLikINIVyLeFLumT0qznDyhSEyXtc2IpufUPJvZ7c7VXllgjDAT6BF5pRPlB38GVVG67pOJVUZcPJTDqemig== +yTTp28LIim5+YjxAfVNM0FqY2cnfoKiEfoGcP/vw6X8QIj8yCecttyUJBJLARZjW +AKbJWWdo/DU+a/JrHx1OHfiyYnDqwTLg8eqd6kxEOWOqCCqDz0NYnQLyhlVXJTvi +w4DgeJTyUUQbStJqN2eVkknuj55C7ViAisQ1E/U062F3gHtzlhLtgbQxBKYhbUkr +WCJLqPptHtaJYPlph+wWeg+sOxk3Uv6xj2ce5wJkmKCUipTEif/EVPfK4rUVBt28 +4mNcCYu6bgJf0BSQhxRcisvwAScbMuCqKWHcVXIbj7wNu6qpAFwGTeJeboRpV4r8mv+H9WiDLaRerTAnGAFJEA== +WCJLqPptHtaJYPlph+wWevd5sI5xYoG1zjsIm1ZW7JA= ++XAZx/LA8Tqt2DY5E6RCz+RMtgTQd5aWdbscNadpGT0x9Pd4PGItjkiDDgieRB7b77Sl7xeASrdD6cS5HIBf+Q== +XIHRHCfwuCFaNecBe9SVlA== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +U7h6BIX4p+v7nxzqr+lrQLHkPVBLWwWs53akWmvnS6Y= +H3X3EGUrpS/29Wm+tIafOyUN8R5pTpM1CsRClEWPZgVxxWJEx6/M8Hwi9JWiUQOvspeTeunE8GXUosngMViJjg== +1u+XjG/2+GSQRv6EzCaWRQ== +U2kUm68+PfY7l2FEHprZHQ4zaSjqboIWU0C6fmJl7mI= +YP707gDXV92HFzvrbPngtud6mQr0wqhlxyeAi0kZpiB3Ihd1qVQvkSij13D1jYfC8oUMT7Nn8G1DPFdi/15r2nMTGZPV0z3fzp94fCaM1+3f+oNiWu0sfaQY9eUn9cEs +oacxXEoPih9NKsd5qduwcJP5AF6Kl6JdSjECtt0tknbNs4pgmNiR6Yl+BqOoKyll +8eMNwOFVbk4AtLbMoBZ3WQXk6rNPQP4WCclseKXAP1JIV/JyHM54kMCWSAIjVhmJ +1u+XjG/2+GSQRv6EzCaWRQ== +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUBV16H/wFIocLdPKt+iN1Q+QPnCkfaCBWt/veMZH79zLlyiS5k1FWT+2Yml0FGnP+5VvgWsaRh4pvYQ3oU3Kj69sQnSPzOWni71Ew6+zBjjRA== +1u+XjG/2+GSQRv6EzCaWRQ== +NuQf5vOe0Oik9LWfqOnHdy/NOX+H2BGhwJI9uG0gx6tVV0LDgDKjt5zGVa6JPR2W +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +YKTlZVqhMnSRcIeAIjPbFD/jkWWEolgtSWeBe9MPXjc= +NoDeiQRzOwaAKEe1H+Wt2oljPZfQG4ZaFi0eYcsMPioBvlqYa+ow1OMLcn5Vt8JUkVqJstTJ7wh7Sk4PR7iGOl17IYCtzUj4sAXJu9I6YVWXciC0L4ttEwR7mM+Qr6En +oacxXEoPih9NKsd5qduwcJP5AF6Kl6JdSjECtt0tknbNs4pgmNiR6Yl+BqOoKyll +8eMNwOFVbk4AtLbMoBZ3WQXk6rNPQP4WCclseKXAP1JIV/JyHM54kMCWSAIjVhmJ +1u+XjG/2+GSQRv6EzCaWRQ== +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUDJtODfX51TdteaqQ3a+UcgHFQmZwOovGMrZXXAykB3xneSZS9ptT38XIhclZuW4tIUsA9oZG/PT+pLKemuFLdw6cydjnbL9fdsWUVK5NAZQA== +1u+XjG/2+GSQRv6EzCaWRQ== +NuQf5vOe0Oik9LWfqOnHd71D0GHLd+Dnie3THU9YhIw= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +2Ul3GipzCUNX7RveCe82CbBj+sKXsmwByweOM1JQOH4= +yDOAduTbv6/KjNMoxqPpJ4CUNe4+WGLXKDJ93lX0uZUoSZaLEi5gwHitgUzbD1tFUzOR/pdMYMSsY1m2If+wBGNve6WdoZMW2Lh0pAiZwpE= +CAhn8dRUItEbEErp4w+lX+NNLjQ3s7dTCionRz0FX53bLy7t8/38W+QbRkSn/k6fBpDHAETQi9H9KM4C/14NFP18j04UcGidK01HI8DQXS1iB91jNLybWJeWT+QOGbMh +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyW/8gUwPdqqjWj5njHrCfAtKpK8jdvX2JoJE02bBmKZZA== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY5P7pklwxOw6dxXjirT1q1bEvfiu25vBLYVWLwsqVoO/g== +1u+XjG/2+GSQRv6EzCaWRQ== +PzEvxs4M77eQp4726OdUdjy1F7989QHAkzQUa5m7IUo= +EmZYXAoWwLEw6AEemfKeYE6959W5nEE/3bBTBQPwWpM1mOu+SGdpiCkmvvutBWda +1u+XjG/2+GSQRv6EzCaWRQ== +BEefnVQInBLLUz5V3TvZjhb2ZCGHPFwPTtFWXf/Hahk= +UfrFLwox5T7Q8/E+j18mDM78wSrP5Sl+ShMSLTRst5kT4jdbSnGHbabDR7jbAt0ipcRtkX2IuVMOFlh8rGIlvbY6T+MVm8fHFBrIB6AULjVOUHOanq/Rb72gsyADXZOu +PRTQce9kzL0wbtVMaFHuX/dt4H8WFwH22DPdffzZqPLttVTviK6wrLoAb38Vzp0W +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX+NNLjQ3s7dTCionRz0FX53bLy7t8/38W+QbRkSn/k6fBpDHAETQi9H9KM4C/14NFP18j04UcGidK01HI8DQXS1iB91jNLybWJeWT+QOGbMh +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyW/8gUwPdqqjWj5njHrCfAtKpK8jdvX2JoJE02bBmKZZA== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY5P7pklwxOw6dxXjirT1q1bEvfiu25vBLYVWLwsqVoO/g== +1u+XjG/2+GSQRv6EzCaWRQ== +KQjAPiNciP5B8oZtUmJlKnrbmvn0317qudFG0Bt0PrY= +EmZYXAoWwLEw6AEemfKeYD7dAwuwV/Y5SW0i/EiGH54OTgiiIoJ5DKeDr2Z23b/hi7QI/t68KhCH+HxXxTotk30oGLvkUwgFdM94oSQYvk0= +1u+XjG/2+GSQRv6EzCaWRQ== +FihJYIbh7GjJE1FoiskZG0ZvWVP3/pyMdhA9rlfHbfM= +Ix9T6TIGvd+yLi6JDJzskxBFKq9fJlmSJr7Z6ignFwGm/e7819CIiVV4QJQHO/I1/7JnaRhaQBTeIM0uU3Zmpw7sjgHeoRA1ChI9we+IAk1OBkWCvapdKrjhtpDFRQWj +CAhn8dRUItEbEErp4w+lX16+n40eaV6hpDW/6jSggsH3BMR7I+h+5pwJwKlMreghBi/c+hgEI1zRZknIqU3eS1IlKuU4hEmdzpwcI2z3hAA= +1u+XjG/2+GSQRv6EzCaWRQ== +fqar2jT9xx+bGYaxqWLMVYeBlBDx9IDnUH3/AtFX2VQ= +QvwWeJbbvcvxBQxj3taJOvme5tgtQlpINZ8yhC6dvJ+r5mm9OpIu0bQuTo3/IGwVzPX/FrVInqx1HJTKYbaCM14+KDM1YY+5DBIoXrGEHHpzd7iUY0WnIl4WiBhdGoZ7 +CAhn8dRUItEbEErp4w+lX16+n40eaV6hpDW/6jSggsH3BMR7I+h+5pwJwKlMreghBi/c+hgEI1zRZknIqU3eSy44aVGzW09U+Xn5L0wfj/E= +1u+XjG/2+GSQRv6EzCaWRQ== +pJpolZmpdjkaaSzPnxZ8yElNV4hiqHY+b6WpMB50Sa/m/daco24GRJNlL8Bm/buQ +xdXOUWOTkkx6T85mcNzH6zSlu7P9BcOAh7EHvdS4zxOtS3cnyRYoWkd7wGd1aMRKxOV6GcIp3qdrQACZt1OgKtG2X8Ug5xB7PRXuXinJJkM= +PRTQce9kzL0wbtVMaFHuX/dt4H8WFwH22DPdffzZqPLttVTviK6wrLoAb38Vzp0W +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX+NNLjQ3s7dTCionRz0FX53bLy7t8/38W+QbRkSn/k6fBpDHAETQi9H9KM4C/14NFP18j04UcGidK01HI8DQXS1iB91jNLybWJeWT+QOGbMh +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyW/8gUwPdqqjWj5njHrCfAtKpK8jdvX2JoJE02bBmKZZA== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY5P7pklwxOw6dxXjirT1q1bEvfiu25vBLYVWLwsqVoO/g== +1u+XjG/2+GSQRv6EzCaWRQ== +nvdnm1OK3Jn6gqZBwAyoEPqO8Rg1yIK6wyF5T9GpZxY= +EmZYXAoWwLEw6AEemfKeYD7dAwuwV/Y5SW0i/EiGH54OTgiiIoJ5DKeDr2Z23b/hi7QI/t68KhCH+HxXxTotk30oGLvkUwgFdM94oSQYvk0= +1u+XjG/2+GSQRv6EzCaWRQ== +unIQShDuzrPdTMMq+8wt+bvogYdwlYiMkGwWf63Ga3g= +9d/IppkQ2PyRmRu5osqwoODc5CDWJohhv0xlwFqB2IhoN9c+zmwSjJwlbpoMs79EmxYOCz+VexZABh6SG2CRHQ== +9uF3R/OfNUGVQlxbTNUS57cIoFaPqhrrnXficEPkiqM= +aqZ3DiBpVY4y61L8uxIxkDYLYA59C5wmzW7qiAI6evbvFvxuTloi+oIdrvqJ623ezEJ5p1OLdZ0VCv3rFLXWt0wS5wpoHvMetdvu2ROiJaobIXWjCjqaecfqQPN0ca2hhPmXxOeP7qW0EnWHkfhwZg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw6Vm3TUw8Lv4bv6ZRU2TmU8hXNojMo0qmLtZ+hZTHjq6 +1u+XjG/2+GSQRv6EzCaWRQ== +unIQShDuzrPdTMMq+8wt+U+Wz/v0Jg4gyP+ueTxOOc4AcsQ/hNjiq5uI1N0crM4B +9d/IppkQ2PyRmRu5osqwoJLIGbq2r9f/L/Nhg78pizqK30f9mR0UWajDI6Im5XYv6Yp7ixriqrxg1x+CNjyb/Q== +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUABXrfIFy3unwvGyjMmaKXnAeSARa0sWBILuhzTWPaTrcrjKyA3m1znDhqukw6fQ6pEAn63jUEzYiJVOtXn0XWCFgy7htf7csPJEhrttL4dhw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklbmV5J1RodET3Yh1/QCAz460nMx47+LQNMiL/R8TOs2uFXXpzYMVXcunKSLN219OehsoCDhwpRSVID5Aqd256/ +1u+XjG/2+GSQRv6EzCaWRQ== +NuQf5vOe0Oik9LWfqOnHdy/NOX+H2BGhwJI9uG0gx6tVV0LDgDKjt5zGVa6JPR2W +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8= +1u+XjG/2+GSQRv6EzCaWRQ== +sxMa/4cqt2s6Ko+hmWaWEifcl6/bwjn01DNZahA1tAw= +OAEDDa+aQ7uy1PKxSeroSW/gOn353zJYbhpwKG5CTdc= +e45CXpLCksCwwl+qz0OhtwCL153WIIB7r8/dO7BoNjrEpThnr297Ejqueer+xuKM51wTjoVVoWCq2gjKnDH9dg== +4IMnPiozlLMNPPcmwmcZbPKIGYm+t01jGPwHOqqlXjw= +1SbkTSil8XzgXK/aUkDPBq8OMXws2xvAQiCzL5QcHCTkiaR3ktH2dVNxKRKmi5TpJAOxU+OghdinX7oVFAx/Lsxd3JfLqYwbYW/SyO4OVmO17W3BN/w33noVEljvSxI6 +JIiKyjS/k33RBMRccr+b+pR9EUVPccDNK8t/6wKdwQNIr4sZ2ttmiaf8q90zDBdw1AEHFu+TyZJZO/TZbRjTvlj81nTdJiv04G7vkmlvbGjfztAr2ev+Ga2mvBH4YF/sW6oWWKZnXpobhu9INrVRQg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw9WPwFTtYvtbUyDFEE2SgTg/SQczH1eMbb7YCuamz+2z +1u+XjG/2+GSQRv6EzCaWRQ== +1hs1FfIPISXmd7TJ/r+Vl9dMPEgcDvEOlu1C3C1qpMWS8rh+CAbRFm5iXmz7ilkKPqVF7sbrHwdGLf+K4KP18Q== +VmVrGQo2zRokW/ZuO9bN688igcXPE7yYZaIJiLiDRRXaN+Ef3quooLUT5xQpwAgG +1u+XjG/2+GSQRv6EzCaWRQ== +UbJuac0dxLiN+5ocS3w2vb6NR4w8OuptZ2vYLZkEwyjtjE6EQUnY8kZBjMXw91b8Za/uM0INyKfIkcTMic2le1vG1tIbsY4NAxy9c7005dXEF/sqGLFLPxXywI0QD2KV3lhYGTIFsxoftW46+TY2DoKzgyZRZDYCjCdS5jN7gic= +VmVrGQo2zRokW/ZuO9bN6+HJdArV1b33DElozCQPNnvLNKQQtHNevoVLbf1iU/JbaoTpj76VkUPBqRLHqqaJyA== +VmVrGQo2zRokW/ZuO9bN69P6LVR0oSl+6UWthAPQB3mNhu2gEk8qrTxZZ2YVj788 +o2sgQHazONPoJDqmq4d8xsjERPoWH8JoaRDeqyIl36gdDkp5QJvYO+Mla5bvXpyb +bd+9QIe29SsrbbehlnMAB09DVblHAHKEk7Pw/HBeQyVnGSQtPVRcl6g4dN2SK/XBs+Q2vUN/x4CB8BkgNPUp2A== +o2sgQHazONPoJDqmq4d8xn+5v2tTUpCO8U6fXW/R+CFqtIFVblZn6Oh1NOALumM+ +DlzIWJ6kmiDAMD0FTp4Tbw== +1u+XjG/2+GSQRv6EzCaWRQ== +s+huLxQ7UCfPSSYad9iGU7bGybh4FBpMS7xWjHgqZqhkfSVFfkrvZO19eJRM8Q9gNGPkC25aEsh+qePC/Rb5qw== +s+huLxQ7UCfPSSYad9iGU37qZUvgsWJ/BGDTQZINlS546Abk3zye2dfrSMqBWLa4vq6Obz9wgbwjzmczPuScYFVkG6KyyRkXch4NaUKNNR2UM0Kc0cEi34mntRiVfSBC1xfYM2+GrYmy0zYialYm3sp5QE0sn6Z70rSB43aieOM= +1u+XjG/2+GSQRv6EzCaWRQ== +6K6zXmJJNC0xAXfJ6TCmfXX8ZMxXxIpirco7xXSn7dPeoPtziy1mOBK1RHoKEXs6 +I5csN8e3J/KIFkMa5t+SJENrCajRah1QU3uYF/z5CIDRw0MaiyCM2fRi6PpL0QdK +wlLHv6kT3Q/RmtMBN4nDARgRHJZodbVUlxKMP98xSUWZLqHxT63+ALyRl8UjRkj9ZgPwhaXVgbAH+EnkPOgvn5H3icsDS+6qgTq4EvJ/Ph1Ayr5+O522VWNouuh+Nu25eW/TMOgvGRhLmPYRASS6EBSXwMNHVwYQI6wRv6/SjkI= +VmVrGQo2zRokW/ZuO9bN68iARkW+2qOAi4bZ/j/vhWzi0utY9vdPwPxCn7JFSs9I +1u+XjG/2+GSQRv6EzCaWRQ== +1hs1FfIPISXmd7TJ/r+Vly2zekzOC5ssUL1V8jlhnk8Df2VI1IsRdyGx1Nj4Fyjr +VmVrGQo2zRokW/ZuO9bN6wMcoqume3QgnGKKcerpjjOagfMFVUa4Yn6IjT+fhDzx +1u+XjG/2+GSQRv6EzCaWRQ== +h0+APXqIzYQEOyw+poOgAQtD5D2A2VnZ8W4aLPzIrYX7EhijWi5n047kz/O9j8mIBOcmjJg1zfzbETkddwL1hA== +h0+APXqIzYQEOyw+poOgAQraxOLbnSj8RyhHDbBa3bRROceUrlermIcRfKzfYFWs +1u+XjG/2+GSQRv6EzCaWRQ== +wviPW+SHXn5CELvbZpyCzfIWQYKipi377C6iCgSuFqTRa5HA7QMAn/6tpJp2iaHx +1u+XjG/2+GSQRv6EzCaWRQ== +Jsvyu+OJlxxgWnhhtvuaanku2sR/0iBg3aC4QIN1isAhAIT7i6BzAg0iIEuK+qds +yi4USbg972Yx1t8cEq+Sa8G6FvHc2YbLbTqw+SKRWSXkp7gSAD8yYrKs+HSinb+5FTSIg7djs8FKVMAeZLPhq+nZOABJQiJSAx9Y/oFEmHmZilYB0S8uj9UMBgQTW5qtO2zp8bm3ro2i/9OxSwcLnrIWt+rxmq6wlbNFdldcc/qxMQzkJgtj4utT67SKgiL/YNXfs1K5VtrH3rbCEqInUpNl8DkdMeminJN16YG1MGQ= +9A0z4e7cwr+yotmVsrNffU3PtcyTHkpBUTP51niLILgBdrFotqUxLb+gugfUNdPByIu9P4AnLyAyhvvtlxuKOymr66njf2Jx069hVs8Oja0= +sRgqjbkCp0JTY71i6IpaD5yr1bzxGPNwLclSeXwxzo1jPqTrJMG+Or71f1eSnQwLeB9/Rb/iLp0AUuKtKNjWEA== +sRgqjbkCp0JTY71i6IpaD1pMqeBWiwpDG+N/zqBc9+wA9DPb4tkCuDcHb9iIrnbKkcEDcos+xoLvbR836lEpYw== +sRgqjbkCp0JTY71i6IpaD9z9G5z1tgUn7HoU0UqLUs7BX0H30wt7a3b2oVOACqFbfEtEcsXgAjOCq6IrGK5wxQ== +cTJX444NgL3VDcF24XeDWA== +1u+XjG/2+GSQRv6EzCaWRQ== +9A0z4e7cwr+yotmVsrNffcGYZmeoiobQdku68vaLrufsiuxP4PgdHvXKY6a6hylV +Hijs30SKpxh3hwbgfMdgtjdqDQp5tdX/SkvgcT1R0aNriJw28ajQ2hr+/0jaxTkHBD9esO2rNbFT6yF3O03HFoIFDChiHjvrQ0UnMbYg78g9FayBJtc7b9/cevS5FUPk +cTJX444NgL3VDcF24XeDWA== +1u+XjG/2+GSQRv6EzCaWRQ== +4hBTaWEn4eqD2LZuK5M9ym4eeg4p+Byf8jyEneh1GNOlYax1xnlbrMWw8sNi/fxf +4umSJ0NiqamOepFV/j7D/zptZ6SDe3pg/46YlA4AEh+8IpV7v+HDfg4fdss9vefJ +ujmcTMuWacoI527PHGC6aQ== +yoFtq8KDstGoS8N/pwvtcw== +/tSUgLBgzBmGkFZIpM4i8XoH0jW2npLvm+9a50QJUxhQ7pbZpBZ410Qf74ZxZ9XNs8Mr7S3+B4SpZ6n+MhSDVExOLjILc0L8BSnPTwDZojY7bSxuM0bCcPtSh8/reHRZ +8Irf3aUta4tuLJtYoptydt6RKGKtjAWJW/tjMvOO19IrXYEdrmYS+f4a0AfRy8+q +1u+XjG/2+GSQRv6EzCaWRQ== +1hs1FfIPISXmd7TJ/r+Vl/T2i5E2us7QWAoF81arf0fnOndNSwVEGRHo8XJ2N/afn10yTJs3e2VgOdeoGD3ICNayduTKXNkJio3jqwCPnOC34CGK1Yfg3M0aHbnF0qbCz4jmh+hWWUmIDo2Souh4/w== +VmVrGQo2zRokW/ZuO9bN6yuEJkW7NDtWtC4lviaMGRlWvQMZZmF87qNEzwGL2tpy +1u+XjG/2+GSQRv6EzCaWRQ== +I5csN8e3J/KIFkMa5t+SJDeqDKlPRRY/FSBBPJ6csdT9pYImto1Xyd5p/dFDUxDF +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uDZCYj4Guowyuc6J+GNxC+7ExiXI+OqLR+7RlpkYma+8HdbcotJiewZc0TGawVCZMO8IP+QhfoLz2bwP1nqYzAqWVdi4KGKvCMtUKa4TnlbzGjdOZ6z9JrdUT6i9TXBVsQ== +1u+XjG/2+GSQRv6EzCaWRQ== +viJsJdQuChPEovMu+m/oTwa2nn60sePbzT8fCKchiAQ= +cCKGdtIe7qSNBJ/C7EH9q1kLv7DOS/qhmjyapqTSfedVLP9JF6AGmsrMko5+cBM0 +CAhn8dRUItEbEErp4w+lX9tO1yhDltqrpu/1lMlkX6XUv+KQeN58tkpQ3d0vZO/YZZNjI6c+F8Ktu7JIGxObVfPGh/6mBnEhxKLtIU9l1gY= +1u+XjG/2+GSQRv6EzCaWRQ== +EMb90FFcbfUzo1/WG7lt1eU5UBhN0ruXREL6QXZf3FU= +GqUqYGmRl+hr+XI1B2wabDDAyxp3UQGXW5nSgVLbop6lo3RV3aCe/w3+Z4S+Kt0xz+ZRlavpureSVZGlt7eldQ== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT1NId7eTcvWP2D8jig7SHgz +mx3k5W7Ft7693DnOXXB9KbmlSGY9oDre0yMpuoKMrh0CNKyWRuqdjcZ85mwdCfM91B4zqdT97jS/m2g3fnap/w== +SAJBrQbXj/3Yv5Z90xSuEx7a7gDVMPDVI2TmcWrMbNpZnfidDGOknuOBCv3b962Kx3Rap0poWauVI+C6ON5Fpg== +WCmWIlk33/3XPV4HpL28c69ZEzmFJQoxmqH6ZfMqWxM= +1u+XjG/2+GSQRv6EzCaWRQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +sEDutDf0WonupGrRy9fpQcF5Va5jvIN+k+U+fmYCPIowwpEJPKvtPJlRfraV/h3M +69Uik37T733MFTHEnugne1m32oomlDt4UU5Ljjc1D9D4PvLNTB7C6iFSu+gFuakO +CAhn8dRUItEbEErp4w+lX9tO1yhDltqrpu/1lMlkX6X/Q4KQ9gaJO3pTyPTMj/sGFpGsOdRM+EcVFoeqSKhM0oORu57jXW/E5b8VtVpAa74= +1u+XjG/2+GSQRv6EzCaWRQ== +2BgtnQjBgSe2j4V15cu0Dm2XMMCmCwVJSxjjoUzXSZY= +r/0wCwJk3rNLiCV218dnRUUkvIsEBWEaS8u+Mr/2l/xNMKY1cor5WKiOyAgN+YtVzmaViZIQ/dcDlmJ8GHm+yQ== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT0+rOMOcbGFclG/jCcKe3nTdMnqO6KAIfcuBZwBHnc1kKyoEOK3zcmm/23+uF4vn+3FNJBoA91UgxXiT3x46QjDmSZyDI06svXdTOi8CKkcIwfjDRF6Ujky+uantQOv9pA= +VmVrGQo2zRokW/ZuO9bN69GFvmmq/nTHbXpTajOaul9+G3CNogrAtvwj5naTtXBkLELB4fMdjAjWozSvpJZS3MmZOXACNOqTEdsQ3V6rOng= +1u+XjG/2+GSQRv6EzCaWRQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +2BgtnQjBgSe2j4V15cu0DtoAXNyfevDbms0TtLHKBto= +r/0wCwJk3rNLiCV218dnRTrkBCvcvyaRRM5tN0a41zRHnYOiogFwb3MLTY9ZYm4dMIIujTbFFLkLMfeqASkYuQ== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT1NId7eTcvWP2D8jig7SHgz +grzBy7GUsBDuu0zii8vywQ== +1u+XjG/2+GSQRv6EzCaWRQ== +KL0iUMGm7xl6MMomGt0oXw== +uPPxGK64MnKQJZKou95/VLUmdE/OK7fVqH7ENaNbOQg= +UCQ8aAKvTghblJekMqQwxg== +lDZnNCM+YynWMsABEEsKrqo1I3AIuOrMO3or/QPxaQ4AbO97cMN4zn6chclW3i151CWrfqh9HoM/UiEV5JqHng== +OziAiqoRfo7FIe75SosO3A== +k7wxYTGqdPiSOHYIW9IKk+s0n87RoUnpU8ssdlSyiY7NvvPKS76go4jJ5UG6VuJS +KateciczQIv5SSuk5VEvWA== +1u+XjG/2+GSQRv6EzCaWRQ== +D5yUdC/X/2X8xRRnHhs0OHahuod7ipGPfybdbUJBKpPWS1JTrRZWxoCi8ZAQfyjU +1u+XjG/2+GSQRv6EzCaWRQ== +GB2nISfsU+i5RmZ8DRo+4YN+XjxeUMjbvGz+t7of97thh6/kkM7sdNWKHFacZKO320cmFDmM62NXXx5ByVBjtg== +hoCqJ3JcDOOxFSmkMqcwgkNZg7YhdSOdH6GkLDSTKvo= +ZFpbrqPVIsB3KH4ufFtdKtVCai4ko6OzhZt6p2E1Bwp4TBbxL2KBHRBlLVMQ+fUk +8UWeATmC2XV4LIrvDOTfGE2loSgv6pbumOy7/eZY7Y8gYSCHak7omiomEIp7z8zh +1u+XjG/2+GSQRv6EzCaWRQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +u9kTefckBoXakXk3CTY4UIj/f0jufIRnkH3ynvmhPcFrck7rPekjBPKWoxaJghu7 +KP52Q/5db8uomsPaNzzcfIBjLFJhlSafmTXNQwcksBNM/dWURKZ7tigXEGdz28kjUyJ1QCa3FloYIl3wdJs1eCU2/90k2ni1+EZJe4e886OYOeWxS+S7ypBZeLCrlwD12KOmubvHJcBWOuk/JFqwgEV/4SUWy0ybR/5S7ie/146m/Gu0nv0W29RxEK07Jqil +3PYuJCOiZH1sjGKjxSVU6D375Z9ZgehGZdANj6AiuKHBMJNPOLB5MQmgTUWROoA4fjmQLGPZuV9g3aw1jUGp1Exhygj9B4L9l3rPJxSUH8499UkZDWWz5lMm+2N9Z/To73cn62qXoDa9GQbQQ4nDYXf+FZ7hxOQwL0xvGFnUlUY= +1u+XjG/2+GSQRv6EzCaWRQ== +xEseMjL2u1zyCrXAmoV2Na6fJqpnL+CP/HaW2+j1AKLn7xZZzglrOzWv+s36xzMn4ACa0jVf4y7LG3AUn1J2tA== +hmCKbWWrRb4+/UkzNiAoHWug+k1X8xlXC4xlCvy74XaShT7URFdytUA3T0TczOy5PFGVSC3XqRrEB1M1gxwfGA== +Gs5/4CF0+TC/AxMFhocKRfWRy1dIZPIlJLzDBICJZWg= +1u+XjG/2+GSQRv6EzCaWRQ== +MzOj4ZiBOJDNVP8vi/lFQAZAqITjRF84dp3Z2sCcn8tGMMINDi2isbVwyJqk27Ts +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +lMcV7pLM4V6yRWTmLUOCgUZ7m1lBdwCVgXdmCls9ytIkvM+5sLlzt0i3d/wBPI16 +HslBPRwNoisnbgrxs4yVRZbcWkA7FZiBP8esA/l08OK3uVqT+XSoSIftyNMSAAQu +1u+XjG/2+GSQRv6EzCaWRQ== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT1NId7eTcvWP2D8jig7SHgz +IXNngFiySrZ19SPkSCMqPNm1Qo+QXsa3tJhorZy3l54= +wZ7V04uSzznB9BGl8vsW37jyoPdP8Acl2EpSFy1TAGtXnJartRbalXySHd6eDtGDP4U5bCyjzL77Ti6XVu+gUA== +KXRtA12NIhDKUxoVgaJi30EO1QCarO2WkYf3FTqBwxqirBAyrKEpiOpGO5Rbtq0SYesFuJ8iS9sFKfikt3tK7A== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +uISNfeyYNv2Q42tw0THAnAyeWL6M8kyxs8FpWaZGBkI= +aY1ilRUfFFER7CArZPWDNNQ7upxdJWYzB2KT2gdOCbyZMVun5y92c+G7qQWcAkafOJdbc6/K6gHJ9tFBCw1DjmOxo+BPluZ7weCacHqNmNiogzvTANHdPY6bHCd+PsWD +1u+XjG/2+GSQRv6EzCaWRQ== +TCg8jQstvFHGdSGuCc1KgaFyaoNWzNonyS3v4IGn+IfwSUJCVAVYBWozbIU/t2yI8pruX4yuH0Gjj0heMNrqaG6IgoCDI2BAqynyABGHbdLeRQQcf6uqI5Rxtb73HnoxLFTuvRQsr5tmxXNXY2A2Vg== +Mcnw0hYRTOqKGEpOyBgpBw== +gxlR2PLWboEZkMexyPwRCiIOzb1aU8OaYgy7xLlrmduA18IRSkZjwCBXwE2Xp6Ja1w7Mb2bPvsCrP0uXB6ImmmuY1krORwxw0JUoq4FLcos= +UCrNFjv1VaMMEy3B4FzV8obiwulaoWqVBA7sXzlFLro= +VmVrGQo2zRokW/ZuO9bN68OzLq2MgGMAx+4zQqOWwjXCHE94r7mbPBA9XTDctoqL +VmVrGQo2zRokW/ZuO9bN6/jcfOTLNldQyZXzkxdlxzc9zg3Sf9DhTFva0LpW1q3k +VmVrGQo2zRokW/ZuO9bN66gVyRtF8qKm1PSxGVl6HNUVci9c7APbYBkpw0BKy2R3Lioao0VA+DkaB5AV/mms9A== +VmVrGQo2zRokW/ZuO9bN63BFmZ7ivlH94JUGp638GVflgqQ1nvRPFEtqmP6VjjjqHX7O2AhQKEVbwkeXBbA5jw== +VmVrGQo2zRokW/ZuO9bN68HhOPQXu4oTax1KsfZyiyFdFDnoB/tHIxtbJ5anMmeu +VmVrGQo2zRokW/ZuO9bN660fmWwEJcvP9dAG+whDSI8uhKTEdGxKFtrutTAWnl8r +VmVrGQo2zRokW/ZuO9bN6/tRxb/wN9AtJtJU22fO8GadHU9NMgeZQncWFKlhlIgD +VmVrGQo2zRokW/ZuO9bN6+UOF94flNEg0zGJRSI7fXaGFU1NJiFQR+ImvIV9qSmO +VmVrGQo2zRokW/ZuO9bN6+UOF94flNEg0zGJRSI7fXYEqTb/7Ok4zLRpW+70ceGe +VmVrGQo2zRokW/ZuO9bN64AmMQAL82p7VguSmnhEZ6hLrJ5kBIzsc5tB0oBv2+M+ +VmVrGQo2zRokW/ZuO9bN67jRPBnb6YliAbYhKWq82NwcOtZt67rzMA8UUtJ7DEp3BAvbUi/JZJpcCa/f4P+Bgw== +VmVrGQo2zRokW/ZuO9bN66OJLO5Ju+MbANF/1yEuP+gKEqCm24nW8fx9sAfrYesYJtqSMCle8eRjKv/Sl2TqrA== +VmVrGQo2zRokW/ZuO9bN61qUBcUSC7RUve4rKSjgUJQVYApbD3gE7ZiNK1YZb5Sf +VmVrGQo2zRokW/ZuO9bN61qUBcUSC7RUve4rKSjgUJRfGmElip4DBK9DatLtFULH +VmVrGQo2zRokW/ZuO9bN61qUBcUSC7RUve4rKSjgUJQqUPFbEmDCe39RhBoDIl+s7gfvqQQyUPCLOUM0ecC+bQ== +VmVrGQo2zRokW/ZuO9bN61qUBcUSC7RUve4rKSjgUJQ1HR6r8DVgg98vEI+3RQ5DZPn8k4P24lLukGwUCDtIVw== +VmVrGQo2zRokW/ZuO9bN61J3nzXUnT1eAKT9Wzo8fbIXPPOU2UJ+nRNj8533fB2X +VmVrGQo2zRokW/ZuO9bN6yWEqfht7pe/XepbCKScuC/xKzomUEBzQfvkhIpFCZXI +VmVrGQo2zRokW/ZuO9bN6w4G4e082/GXwvTFes/2c9D4qgNOBIos+8hcH+9F80qg +VmVrGQo2zRokW/ZuO9bN60g7LF9BVGGxX4UUGy13KM3l8sVAXnZXlgswLpKM1resiCZEqoG+/+Lj/PnMspoQ7w== +VmVrGQo2zRokW/ZuO9bN60g7LF9BVGGxX4UUGy13KM3XiTgjinSYUJUlaRCHAGlVg0MP6znueS283ddAamX1gg== +sQfTf8f6jxyRSx9SNPNEtQ== +cJ1Wblh8gF3p5U3Kb/RGlQ== +1u+XjG/2+GSQRv6EzCaWRQ== +V1ScATKwVuxN5IABiDyoJZjIuKObRPtYuA01KmsSWLlGGWgSFKdMsHqhXpZkyR05 +V1ScATKwVuxN5IABiDyoJYZrsVnLaxqHITnbAierM5ptIs//Y24gnsyAah/g9TJH +1u+XjG/2+GSQRv6EzCaWRQ== +neG2ZqmysBKy0DmUM70J4u9daMDHK700A8L9Yt6A9aSf5jgoZR0fhg62RnUq8JMj +J2qwAW0nzQmbo9HpSOVZlls8HVNZZa3niK9RcAZZhRGBQj1ppGSDDxPdFq8eTYo6FWEynTKLrEUBF+dufTx+oftnH3SpViDHRos5eJpOXZLAq+NZLYFIsRtFei6Kzj51 +1u+XjG/2+GSQRv6EzCaWRQ== +2KB6FUymfVlSXKS0i/5ug8d9ecb/r7dav0sN47NAJtU= +icE/1HXqa4P9k/UugIr7hiS6P9OYskJnKKex+xP/icPzqLyDFDzei5SuttsEByH6hsHBIC7+lpa3ZhxrvD6aJaDVxDXuGrpzJen87EBk3us= +1hs1FfIPISXmd7TJ/r+Vl8wmau3SjlrAOVP7jo5XBOg5UPV0iSkwPv5kgrTNpseuKiyqJjvWHxvHK2iUVJby7g== +VmVrGQo2zRokW/ZuO9bN6+EocMRiC9L9hAVGHkM+STXpdaJ9Gg9L+2/r5c6x0BYR +1u+XjG/2+GSQRv6EzCaWRQ== +kniDpSFE0vLELL83X2MGdZZGI/Bm8B56URRt92I2Sic= +ltIZHHUgft++nMUXBsBki9zUIO+KQFOzUqIhvbD3f+FNMKzrTV7bWmTdcsTgEIbzcpuJLb3OEax5pB+V5es4pQ== +VmVrGQo2zRokW/ZuO9bN675hS/LcFlcq/BjQUaJzFZ1NDnzfojKO/3lzAqNEW7axhBR41xHL8R3XjM3D9HqNuU6VypV/nQwEziWPbgpwWY+5Tyy42B8oGourZU0/E6ZL/l3UeRMxn4Td9QDcqDRuO3gHfymrThtaQJw9Q5HJcvQits+i5qWseL/YffzSpCec +VmVrGQo2zRokW/ZuO9bN6yqz1dlqtk3pOCKCyM/1IIFUdQhNYYqY7n1oRKZREMJSNyOcM/zDrS5FCVXVlVKI4A== +VmVrGQo2zRokW/ZuO9bN60lvcKqcaQTU1BtN9b6GQkKCh3a9NSIORQqd0UoHfge5 +1u+XjG/2+GSQRv6EzCaWRQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +LQf+folDfiGQMcxC7dO+lDBCbZnG5zOWwHGK9ASM5nmsGno7qe9v/ax41bQSa6l5OTlHRDTiSskx5HniW6mBsQ== +RPDz9BzqpeCKMTW9YCV91bc03VIMXGUhS4opsNtJ+NJKLcUinuKvHdpNAE0BZtochgUxCnh4A+AHED5dJMKDBA== +sOGOvvpsg0kUOYaWfW/HeNbUCFZpBQ9FqOxJ4ZRqxzIMPUHZJ94Kt8VxFQ/LkG4BrOsDhuI4ruCuzwckibBWh46gcte7aYqkHf761Pc6BWg= +RC8IWtD8oQ+ISFVqRg4bKSW04wSMM2Z9CjzWrQPAh7V0nNy4IO0ezXpVsA02kwCn +1u+XjG/2+GSQRv6EzCaWRQ== +tQDb7SQIT3JulrlZ/b7u3XdjEC7ESHbvFrvbY038JZY= +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT1NId7eTcvWP2D8jig7SHgz +YdyoD9gMz8atqH8muJOm+Ll06HqTCBCR1d3b2W+SGNY= +1u+XjG/2+GSQRv6EzCaWRQ== +2VsuHWLc2PbyPgC+gaUrxG2G41IQ6UaVHfeblBfCKn3Cf/7LjLLRf9z4FubRI4zv +DlzIWJ6kmiDAMD0FTp4Tbw== +1u+XjG/2+GSQRv6EzCaWRQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +xEseMjL2u1zyCrXAmoV2Neanxx5/YzzawG2lRv5hB+t76SC0IctgRXZvNn56+Ig1ubQawfDUQRrcmRqwOZ7V4A== +hmCKbWWrRb4+/UkzNiAoHX0e+uZWXcaoT5CbMnrUDl6CsrPdxya9C2uM4/WPuo9c6p2YJn9KP3h54Wc1c4iDuyy5C7BS1e2g5nFMgY+TDUcunx6wPw5rmpS7Nk8ook44 +Gs5/4CF0+TC/AxMFhocKRX/kqiBLyDVESab3Sruf2Bixqzq7gsfedcPnEv/p6UXB +1u+XjG/2+GSQRv6EzCaWRQ== +MzOj4ZiBOJDNVP8vi/lFQAZAqITjRF84dp3Z2sCcn8tGMMINDi2isbVwyJqk27Ts +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +lMcV7pLM4V6yRWTmLUOCgcp/K74eIJxjJHbTaVKvaXiox5BmDOqTGnjWbGvl5Fup +HslBPRwNoisnbgrxs4yVRZbcWkA7FZiBP8esA/l08OK3uVqT+XSoSIftyNMSAAQu +1u+XjG/2+GSQRv6EzCaWRQ== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT1NId7eTcvWP2D8jig7SHgz +IXNngFiySrZ19SPkSCMqPNm1Qo+QXsa3tJhorZy3l54= +wZ7V04uSzznB9BGl8vsW37jyoPdP8Acl2EpSFy1TAGtXnJartRbalXySHd6eDtGDP4U5bCyjzL77Ti6XVu+gUA== +KXRtA12NIhDKUxoVgaJi30EO1QCarO2WkYf3FTqBwxqirBAyrKEpiOpGO5Rbtq0SYesFuJ8iS9sFKfikt3tK7A== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +uISNfeyYNv2Q42tw0THAnAyeWL6M8kyxs8FpWaZGBkI= +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY7KZMO/ZVvs81Dvb/IbegIIFTH9WoJwcOQCgdxqvvMKCQ== +1u+XjG/2+GSQRv6EzCaWRQ== +RJO4PIle9WJa6nrSrDK4/acyIWMkOtAojDEaZjGUWntDFAELN+iKcRBdMcdsMLNDVe4SL6OWsMKGwLg/KAM/TEYc/HHjJGKNLscUn4D2BzQ= +Mcnw0hYRTOqKGEpOyBgpBw== +gxlR2PLWboEZkMexyPwRCiIOzb1aU8OaYgy7xLlrmduA18IRSkZjwCBXwE2Xp6Ja1w7Mb2bPvsCrP0uXB6ImmmuY1krORwxw0JUoq4FLcos= +UCrNFjv1VaMMEy3B4FzV8obiwulaoWqVBA7sXzlFLro= +VmVrGQo2zRokW/ZuO9bN6+b4NmZdyk5c5LHWgu6MLaKte6pFj7SOCW8I4rmGl0mM +VmVrGQo2zRokW/ZuO9bN63gL+NYGsT8A9ybxZZqQV63Hb/ut/ai+kM302hcAEJRwZL7ytjctI1y9OyTQHitVUw== +sQfTf8f6jxyRSx9SNPNEtQ== +cJ1Wblh8gF3p5U3Kb/RGlQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +sbAVO78DchSWkYbMhox27hgIvZs06l1RTZnD0/enKaMpWOajgHava/aY74uqJG3N +aWYZWD7hPqKlzlWWEJ1xN3QqDMQsB2DQzcpRyaGC1liL+0YHyeKfGB8onX+iDlA9cFyl2cj1wxO/Jr8n1M1wH+J0VdA6hiI3ewhK5HyofKWFXoh1Di3FHBCk0Mp5i27l +Gs5/4CF0+TC/AxMFhocKRX/kqiBLyDVESab3Sruf2Bixqzq7gsfedcPnEv/p6UXB +1u+XjG/2+GSQRv6EzCaWRQ== +6CkLE5vty/NbnywWy3iWahp+6HrIDDUTlX7Wuo25xfTL2JV/mlFCtzUtgmJlgiN5ljX/x+aDdpp/KDOl81ZxLw== +uTEK8Ng11d3ix2pA+DD/afa4+lge0mDF8jIYu0ES6knXDT7VzQAcCe5LhWXidX57 +L0eUthVnpkGsmKFAX6d+uKItPv+N8UKkEdcZ0HZF2G37oXxzr916p5G6V/GkH+ULTqGd3YcBByDnHgry95125Y25CDPwuMdsuMwQos3x5h1qNGEKs+sVNL3TUHGaF7Ht +1u+XjG/2+GSQRv6EzCaWRQ== +c+eZ9iERZLzrToMbbeDzTg2bRa2LBcfKbG8lAaFyHT1NId7eTcvWP2D8jig7SHgz +IXNngFiySrZ19SPkSCMqPNm1Qo+QXsa3tJhorZy3l54= +wZ7V04uSzznB9BGl8vsW37jyoPdP8Acl2EpSFy1TAGtXnJartRbalXySHd6eDtGDP4U5bCyjzL77Ti6XVu+gUA== +KXRtA12NIhDKUxoVgaJi30EO1QCarO2WkYf3FTqBwxqirBAyrKEpiOpGO5Rbtq0SYesFuJ8iS9sFKfikt3tK7A== +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +7cZLCIyMlhVx/ty2znkcZk88zXX0jr6WhPmfnm1FMY7KZMO/ZVvs81Dvb/IbegIIFTH9WoJwcOQCgdxqvvMKCQ== +1u+XjG/2+GSQRv6EzCaWRQ== +RJO4PIle9WJa6nrSrDK4/acyIWMkOtAojDEaZjGUWntDFAELN+iKcRBdMcdsMLNDVe4SL6OWsMKGwLg/KAM/TEYc/HHjJGKNLscUn4D2BzQ= +Mcnw0hYRTOqKGEpOyBgpBw== +gxlR2PLWboEZkMexyPwRCiIOzb1aU8OaYgy7xLlrmduA18IRSkZjwCBXwE2Xp6Ja1w7Mb2bPvsCrP0uXB6ImmmuY1krORwxw0JUoq4FLcos= +UCrNFjv1VaMMEy3B4FzV8obiwulaoWqVBA7sXzlFLro= +VmVrGQo2zRokW/ZuO9bN6+b4NmZdyk5c5LHWgu6MLaKte6pFj7SOCW8I4rmGl0mM +VmVrGQo2zRokW/ZuO9bN63gL+NYGsT8A9ybxZZqQV63Hb/ut/ai+kM302hcAEJRwZL7ytjctI1y9OyTQHitVUw== +sQfTf8f6jxyRSx9SNPNEtQ== +cJ1Wblh8gF3p5U3Kb/RGlQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== ++lzLYqoQ0EreL84jN73LzkRkzIA83nRpDjgtA2b6HsfLlk3qQ08jeHIEnv1zEEh5DtsBdqOMNFikXk3/9aQ7Cg== +w4jmCS/kxlITpTLK5ezfwaOotiyvPbwFNhVrk5TZl3UoCWi9A++xJJOJ9Aa9UNXtM37+Xmb1SBCeJyiRmCFLtQ== +Gs5/4CF0+TC/AxMFhocKRX/kqiBLyDVESab3Sruf2Bixqzq7gsfedcPnEv/p6UXB +1u+XjG/2+GSQRv6EzCaWRQ== +6CkLE5vty/NbnywWy3iWahp+6HrIDDUTlX7Wuo25xfQHKpWxhUohgmgQVhz3ftjr+6LJu7Nn0zavA+4delF7YA== +uTEK8Ng11d3ix2pA+DD/afa4+lge0mDF8jIYu0ES6knXDT7VzQAcCe5LhWXidX57 +L0eUthVnpkGsmKFAX6d+uDMxEKKefTCRPxLapN/kTgE= +VmVrGQo2zRokW/ZuO9bN66BKCjNCszk9OrpNhI5/lDKVIVWzFgCJKcjF8B2gQmH0 +VmVrGQo2zRokW/ZuO9bN6/AjGw7s+1fp5mLt2vsBgZChSTjmCbLl+IlX2G7k3f6M +VmVrGQo2zRokW/ZuO9bN63gL+NYGsT8A9ybxZZqQV63OYMZGRv5n7o3maWLfj4sg +YH231WTDnzQG3bFilBiqIg== +1u+XjG/2+GSQRv6EzCaWRQ== +5QnPArxk3YUw+SK3Ek+bmYwKlDcszoeS750KGzFKtUCsKQJMKz57hDGzJ13+V8f6 +qKiQto1Mg19+mk4MC0Qyno+n31U2xaBfScynGItdFlA= +9MjkQZlYeGQ2p2SsU3BCwUGs3x5evPwwdBgt+S90T6wVSkDc2DbhJNTQ9CNEqI0WR6mOdFZp1GZ2j0q8X9Oh5qfRxAWRjObfsM07o4aSUac= +wYXwkdeg3SHiLLFBdFd7x9FkOEIJ1IJawZdL/1VUJ91+PVQlTFD0NFdtjd47QRZsp5GYLsIAGyUiM7r0UkMqfw== +YWocLyuwJOlEJPhi4S5IIMZzheKeIep7CtuOJJl4wYmTFQalfs+7zI1ZvOxtY/7p0TzwLyXWgmjHxUuzemJIrY7CO4sYrHHuKxvsZX8tRkk= +0BzZlwrn+EZHwB+aAGLzKYG6k7W1nuepU1wt9Uv69DyAOxBXzletESCKCKaZRXA9nMkne8pMQmozTiLgDyOHOg== +PsDdTxSrRX/dITzDjfn9sSpd6qvCaBFHjXYc23OzMU12aWnTey+FEczimFLZbyOVteFrDxGJRmlFD/F88jcAeA3dhaHqSSeiGpFGE9YkwBY= +1u+XjG/2+GSQRv6EzCaWRQ== +RqVoQzk6IR4JMpR/e8KmeOh2Tqc5m3P9w6Y4h9L6IWE= +1u+XjG/2+GSQRv6EzCaWRQ== +C+35W5EnWcdohnWDdhY3U4LZza0CXooGoP2tHWIqZirSjmthbZ/1q51XWlQ/i1Fb +1PZrVi4rzkp2jtFwplJMv4xS/hxrY7O0tEh+sre4MMW/fM3b/4YMLtOYhQE49I0gKb8sXbVtSUrEjspPPJFfhw== +CAhn8dRUItEbEErp4w+lXyfKXxXDhOK8Y13cqRVbOCfhKbvdex7OjupCU/YrP/GvbdpVx2a1yoVXqFhzhAnb4PWqTd9UEvbTiyAw+DhcegCTzQ4vorEJPpM5giZjGot+1/hvmeaimXv/DR2G79t0eg== +1u+XjG/2+GSQRv6EzCaWRQ== +y0mf7vZk9Gzg5n8pC30UOVAUMGeuSJdfKl4EUiyD7nURiVUeYjQjxLElhSoR3bNV +2/XryqUzwHCgbYFyhZW9WnmAk1jY/jK0oPXrZQ55yqc0+G5lGKhFT9JYJfp8rUbVxIo8AlQWU5oUedsIA4zb5Q== +ghdF4G8YV9yNjU+KsNcQR85CT1ozzusVQI4giY22sxScoZZcUBWP2VXsKbvwAotoCo7sJ/l3iwhmdlat45lcYfeKMumOBM2rAbPy+SPVkN9fRTJHmfQCDO6strAruBpH +1u+XjG/2+GSQRv6EzCaWRQ== +ojcUvW+Z2ehEJ6yMJpmY+9UYnB/4hbTswrKo/LDRiWU= +IXE5utwGYOdlZaZ+BFyYQe1G2Tnq0CGGgT232Xu04sUWUEYIotwb3bTJroY57YnW8kCPSAVUyslXJprqpbPKug== +1u+XjG/2+GSQRv6EzCaWRQ== +IluO19DOYseHfsOaGSZp+R4V7B9GVY5oAk8TC8T6Gnc7LrdNbGMIhnCSWofIYMb9dQLD32WGVqZKgIAXAu9XxvNHlWgZHFZlmDg8w7ZfYs0= +c0ufQ97F5najN1prFovGfgk0GuVcVIrGJkW3P7fFMPUiGaR3aLplwVVbNzgbsqp+0vGQbpy2CAV+ubAdUGLAcx/dacUKuBlHZaJjoF7OmVk= +1u+XjG/2+GSQRv6EzCaWRQ== +H+FNXRb0Umc4CYTtn00ZPsezhzhZnftPrifLFVTEL610sfwvMP4rwX7H+VjgeuAIsVL0VyE7Z9UNt75beyU3tw== +V1ScATKwVuxN5IABiDyoJZjIuKObRPtYuA01KmsSWLlGGWgSFKdMsHqhXpZkyR05 +V1ScATKwVuxN5IABiDyoJYZrsVnLaxqHITnbAierM5ptIs//Y24gnsyAah/g9TJH +V1ScATKwVuxN5IABiDyoJX1bESOEMJu9Q4TMxlPdlqt9I7l3T4IqY6aVA0L38+LioKZuR66g0LUNOWt4x4gymQ== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgdBHeuqRUKH+enUeTo9ZkZgIkGXBBjkWk7rTGnBK4iYFAIC0JfbJeWcgY1tHoFo1z9NuwLfCTGj0sqfaIJKcDncwD4CqCGELOtRsNgIStfwUgi5bX2adkmr6SDjjmH7JWw== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +aZA9PYUJziqBP2H5HskmSVCfuC3EjLFxSG1cBIplggomTSKkos5JazKt1IJCco3j +h4QFtaqS3ViS+INB4rxJt9qsy/cEarJxhur1+b1Epq0= +1u+XjG/2+GSQRv6EzCaWRQ== +1Pmp5sZxqBziC3LYj2Yu9x/CfkN5l5V1iip1b6Q5io0= +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +PMsnmYnXwTnywF6bV4DtbjBmhBKkqmzzWhgWTMQKUtmEG7+uMZ1O0FzmJK/3hQ4e +M52FSgXgnXx2Hd7dfJpFXpiKlzAN4YRV/CIkbNeNVwQd62ZPvhrLx7OdKmSaMiHnbJA4qgZq5m5KLCmSyuXkug== +1u+XjG/2+GSQRv6EzCaWRQ== +1DMUmhwH/xlvvlF9Wem6fpbYbbn0p+nKYrV/0hTgwyo= +trSptuLx8nV2jiPpFv9oLQZzRewlWovNpEZlX9ZHqdNJS+w8pfaVBXwYDtyCfN1c ++pBWk5LxptzizUTsHnZAzw98qUHepCXnd8PKllvEYMVQqP5jFGDm74q9ET0oP4ac +p2+bVXgc/OGpnu+3Mkh/HSvD2ZS1eGKMWD4ICmOkJ7Yc2VZQ033ixPInbY93yISA +1u+XjG/2+GSQRv6EzCaWRQ== +m92ezhFeyf+sWceUE1djmpp4NJFeExHTa4QK+T/1NCA= +cGzT2936rNP0cRSqygcUrFh3FmD38wvnuTWk2SyzI59xWXnxNVRcrLh19A4g7HMO +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9g04MsbYTOIAGaquwEscysPIIK8VGgbwfrEcungom0j8w== +NqhMbY8+EQI8zhTG6Zh2IxVqZLUKZKeArb9KXtRVtI1zza2alI0lIMMW0lO74psM +7jSw4GB3eBRbmC1MzdtYH+CDxdUbtTlhKtB1IFfKlC11A8M2REU4c77xpWdfeCN3AHrmvn2VSqCGaY8uQ26pIklaULHxf7ArASyPUJmvV1I= +2Yi69n4qf67UzMPLWBtv/o0+nYhkxACwgdoHt5MRwUJTAjuB+cIzQ0tyb1Ai455T3Mb019U/W5B2/n0IxenWZw== +2Yi69n4qf67UzMPLWBtv/hkw3ZwgSmwqGRNu3ZH6b2OOBTGAKTRGhfEMiWpNdvKr1IDi0DdhO/3uYJ0NbKzSZQ== +1u+XjG/2+GSQRv6EzCaWRQ== +zCIGDIqtj2dlccTvTm9UXWCmwqyMGJ0YiWI74SUSA3VQsERFMQr4CRCaEeXs71Sh +6UhqZyAo5HepxMPmTLDSMd1/xMbuYZ2J2bf1UogTJQ2R2XIoxZuqn4nnSgEZPQLR+WbxECMBKJ1Nap1j+2FSVw== +p1hQ9hJvEa/OWih75Q+Pz5Q9VMnMDN5bSSFB0OHsJNbb8yXQEa6Zf0Bm3WVa5ah1 +N2AxSv647SE8o7pH02PLTRW2x6MA9cDI2cPf/O1sMfglaiJwFomfzciuBxQr9Mgy +1u+XjG/2+GSQRv6EzCaWRQ== +ruTTNFTdswRs4Mc+srnRk+bc5hfn355hOu4LLRIyPbo= ++v5jFclfrXth0YtG2QE2a8MQXOKx495Y3PXsDHZfSc0= +1u+XjG/2+GSQRv6EzCaWRQ== +/gPwyveRFK3/9KFETEewLWQOV4XftFZCVsqYRjt77pY= +8Ikvgoz7G6LjRTgHz+Nt69tOOXff2rqyevdQTuhV1hI= +5zNnqVXnV6Chbrd3pP6XYA== +1u+XjG/2+GSQRv6EzCaWRQ== +WemqAXk0niM1sSsUg6jdeTbtqaCdmhPPCoo39cSeU2Q= +8Ikvgoz7G6LjRTgHz+Nt69tOOXff2rqyevdQTuhV1hI= +9uUY3kk/WJwpmBRXbh6HKA== +1u+XjG/2+GSQRv6EzCaWRQ== +JPz+oc/K3sEZgugfA6X1X+6ySKjCnFVofSBuWB28FvwjJ0cxcTdMIP0dE1OWGdx1wckqCFuZ1A0BHtul3R2FenqJEjWFvzXd59KNk/4oNeX3qhLVk6GAumseqXOY5UlcyX8z6CyZXCY3UgJFtUE5Pg== +8Ikvgoz7G6LjRTgHz+Nt69tOOXff2rqyevdQTuhV1hI= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +6U2650ptI+oPKvxPm5zL45f5u97dfSzSVoHpNFJ+A4MFNCWlaZTVNSKl0Rk3SDuoEYYJRzfeBQx4bACUrDkAjmbEaFZ0FlJCsslKWYCjsUJlAX07oUmlnA4xReTp12bAMK6jKv4AsTbMUuUDYgqdHQ== +8Ikvgoz7G6LjRTgHz+Nt69tOOXff2rqyevdQTuhV1hI= +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +CBfgBEEA7tq4ux9Gx6WS1/M+ABiuXumx6BCqjcbiLqY= +QyES93W+LGyOBhDFuc2jKi5yToxLD4QuqNwf24V7DJXtBunEZ/LW5RXnsZ8YDCPH +i17nysO1t1ITOoDCL72Are7OX/eZStswDWqnlSCNraw= +4bCaCPES2AXNnKU3FRtflw== +1u+XjG/2+GSQRv6EzCaWRQ== +ItdSTWx/Mvr9hjd/GqTKHFUc1ht+CkWsmbY5eJzN14d1QIUHuhUkvZ2B3SbMI9Za +r81T5uZ9X/I0cCXfT4cueD/dyVMDPM+HKjMgDG2I41Q= +O47dNRnVwHIis+DPEB6/DX7lujZdEMbQ+1VvQqZcHvY= +f9ezXD1Yr6Z74a+8ZNKcY5C4T3GB+JqRqTx6kCZaunt7KU5rhnNcFLF5SLwYpRtgbxSjxU8ZRLY+jonAkEjZdDAYSTyNeUaynZmhknT7P+b0h8eEvymtMuO6Wkf11w9c +VIv8qOuEkjvP/JYQFt0oyAn0GIUqowr6bnEiQPoUOHSlhKLqbRzUBmmoz2W+WdVw +/v5heaJ9sRBLbNxUdXuMAIab2VCV0zV1gjiXajR7ubuitmu8NUoWbMv7onWRbB4I +vKHUDXVnyBYBzViFHpiy1GAIV6YVlhk1T3u+0XLNpzsaR2oe30zpnZ+OO6m2PV1HJ0LQZTBDY2T2eEAaz4i0/Q== +VIv8qOuEkjvP/JYQFt0oyAx6pI1t0s+phkJ6CHCmcco= +hcMtC74tsxWufkV6jtbL0vbEaXA7ze899vAQ+c+zk9xBtmsuNgTPflBTHSPRtjON +wQq7tqChd8xWopyCcIMbNUPGH6B2NVU6ZXgqujtUaHVpn7dliEK8pKYqylm3P6QhCks/8WDXDe2tm3P8mJ5YVw== +QKNI/Y0oXWCAKvQ5eNPPzEaieKXZOQFim9enUTo051oFla/MESYtuaCc9BGHc6O/ +vKHUDXVnyBYBzViFHpiy1KhbsCJXt7qYAWGl4UJdBiL6IquFPYcBNZEQBkN/2ArH +vKHUDXVnyBYBzViFHpiy1Pzy6O43SQLOqf4JTpfWxFlMX5ikhfn+7n32L3JBmeuw +vKHUDXVnyBYBzViFHpiy1Pn10/PuJ7Ev/Y4m4TSUWCA/WH0WSNJq9h1q6Vb9twDbMSJJKOq1fIHLUekAJ5KQvQ== +33PnBUJTi+OsoyuQ5/WaYg== +VIv8qOuEkjvP/JYQFt0oyMkX0llDCaD1oU4fhPFRY0uaWBpHlbVx4RG9xvyZW1q2 +YHmVAP+XfXvh6Do8nF7SSw== +1u+XjG/2+GSQRv6EzCaWRQ== +fVpK7pI0vuTDeI9U+N/SkXV9wQX47UqiuaMQk8+i41Q= +GvmW0nHk58bq7fXWqIlW/DcSR+3AY/zEF2yfl3+bhiM= +ME8Kpf7OhCa4sOzsqiPQhw== +VIv8qOuEkjvP/JYQFt0oyNAlKIvgmFkQIyA9XLDmipPTgnEMlECyRwMVHf2qFFQ07eAxfE+I3ejc6oIN3Gf/Jvaber20F//2Eb0FzBiaYfg= +YHmVAP+XfXvh6Do8nF7SSw== +0lLaiu4mZY7mSMLrB3JPhg== +1u+XjG/2+GSQRv6EzCaWRQ== +CaYPcoo3hrIrfdb7ag2X67CrplDHZqI5hNTxKQEQ5NgnUTyt6Ciav9i1BLiiJLMI +6UhqZyAo5HepxMPmTLDSMcee5/Z7ylNjOrtvxkZQaEmC093m6D+eWbq0pikBWpRTYEZm4FeNSV7+thWRruqnEDx0JlyJGR77prg9yuRJKg0= +4TatgresHQvhPdvLpNRT9/tOXOKiQa1gutZyOO6FlyM= +TQRmC0vOvJ1P+lfyS3Os4WprL4M30aPh5ZZ8aCvQohlxZYtSKbBmnILGnqXGWfhZ +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +p1hQ9hJvEa/OWih75Q+Pz5Q9VMnMDN5bSSFB0OHsJNbb8yXQEa6Zf0Bm3WVa5ah1 +N2AxSv647SE8o7pH02PLTRW2x6MA9cDI2cPf/O1sMfglaiJwFomfzciuBxQr9Mgy +1u+XjG/2+GSQRv6EzCaWRQ== +mDRkPFp/j8KULnXRVcN0L0yqOW8umfrqR7aShWloZvEk+TnIwHULtqQ2QK5aDlL55LLOXmMtOQ4V2JBVGBsfzyMkX88uEXwXDkS3QdUC9O35X5js/wsq9KXlJbpgSnV6 +QjEYNJp9YKIp2aaVIb+6f6gVzO2t3XqwygKKaudSGLZwsx7jEUvqLi1wMpsVMRKD +1u+XjG/2+GSQRv6EzCaWRQ== +o3kxTO4RwuXO7GZ9wpXG+a0z0P2h4pccG1GCZJnRu2U= +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7PqEY/t3psY8Uj1eii891uGnTMMS0HKp1hOus8shMqxU5v5gbao4nwbk601ukDtNI +1u+XjG/2+GSQRv6EzCaWRQ== +Nd5rVySRefuK+cWgFJnVfBAdTcp/n+9a7PVBTnI8UfdxG9tmHzlXLaqopg0Rax4Km2+opphtA9Kg0XgHI2K3oMQw4/+YV/khxJvcs2tvWFo= +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX8s6Egsn/Ecj0xE23wGfbU1REI0ww2/VyZZCPSRphVZGFPbP95GY0G0wmFIK2h4Smw== +1u+XjG/2+GSQRv6EzCaWRQ== +Ab5B1FuMu6QDBtV+HE9XYL88VNgWcI0oAPnVxRFQLNrnxx+Es3CNBiwMMggj8m99 +oAptEs9PzyZpwE9U096RuO2jQBGmre3FmrEJfIo+zUJgeZmvmn/jzcPJg1L8TYl+ +S9tzajN7j7km7OLOHRXxfStxcqmdbj5DFbF7yR/TmmftBkgkgsHLHa3sGJ10CRau +1u+XjG/2+GSQRv6EzCaWRQ== +ZVyfYzP/n5Z7xgLr646p2cso7EYQKXARTQgN3etlAJM= +1vTE71QDj8abeS2qCOg+Mq3xL/mJAEC4AvMwgLb1Q1Y= +sY+h3BA+xMy9GeW34XQAabd7Wi8q9JFFbepPpqBRnRoWfMsuD4jfN1SjzHQFmO6M3cknt9Ks3mpymXImeKJ5uQ== +AD6SJc/iTPwH54/xssz1WVBWcH/ZEzyHa5mjHMSYrB5GeiQ+f2SWwPqnKqcSi+h6gEdIGtWRwKGZVM0HGnq6tXrVwmabpNx5JH1YRhQPOvhwMfOOGFTNMWxkT9xpTxXB3SUNK/pW+bPnaZtAe/+MgQ== +2JT5nIzi3HZAVmpbu86iZtf9bpFoQsFTXEZ1DpvglCR/Fi5CZYhRntJ2TkC214SfE25FA6XvLpJdRMGHpwKJ9g== +2sUL8q4SzZWwUPFU8GqGKwlSX8gJLLZDiTnldkGow68t+MACNiQJkyqEihk5i0t9i6Djy9cGBAaz7N8s/DiUlQ== +1vTE71QDj8abeS2qCOg+MgByo7zmSIRb7zHL7r506EE= +3NwppGL6CXZoJeTRAeohZw== +mDRkPFp/j8KULnXRVcN0Lyp5WnNz/kgxfVb5zO+CjZsN5QDfY+27nx7D2xQHkVoArXItOma5gJUyRFa/R2Rjy93sk25s3yS0BYLm/AOkERg= +bzZrWU71Kl/tzM0dltE5S0vrk0As/eofsNFV2X57dSUVHi1smijIpQNOIDEUkx+k +QjEYNJp9YKIp2aaVIb+6f6gVzO2t3XqwygKKaudSGLZwsx7jEUvqLi1wMpsVMRKD +1u+XjG/2+GSQRv6EzCaWRQ== +o3kxTO4RwuXO7GZ9wpXG+a0z0P2h4pccG1GCZJnRu2U= +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg75lda++q/Brd6OdhuAQiMu/X5dNEKabxgfMfiDhU0jZkrRy8PediFwsv5MLvLU0aT +1u+XjG/2+GSQRv6EzCaWRQ== +UmS1yzxZhsQI5gKyRk5d5lqliffLO2gbVqdb+WFM95KiRFbdF4zc7m+cuRBaFuVkNC0svUnyr2bmcUat93KT0w== +32pdC9DD05OE2l0oXazDFCi1BbdwJQkVAtSX5ha9T5+RIQUJvLGGrI7U0YrpeV0YxfTiLNDcg/vWmq3qe2hRiv8axmzPTZOR7VXC3NnMaac= +8hgDxirDXnnAyiJ4NE3XAh1L75lxsU5g8Q2vpUpKCnLQRvSoo6un8mbBwbulC3Y+Z34YLvKJaXqproeMMrsz6RJn4sdi7TxTtPRxHDrs+Fg= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +lWZgRwbdKIHiAzPxX6jE+9LxDLPZ9yRU9XV+SZ6Di92P2h9FyC/TkmJYGOwOwbDp +nkN2edJiPuYCVVYG3gy17C3t6eYJXmuD7prKh+1YrOA= +QjEYNJp9YKIp2aaVIb+6fyu2743KZ2UaISTafvxfNjROIoGdfCsptyBRj9161HvW7YR2/ZmaeMMuidD+IuK2gQ== +IhuisKim47k91RVt8z8qtLwjSwIip6cQtpdYaeIQehPNjDveraAeyqcFD3YskoBA +1u+XjG/2+GSQRv6EzCaWRQ== +ujLyuF4aD5b3aVgwNdn4LEZp2uPP77K8ayqbjETZ7NEqvDbrF/U24Zz6A3sBJtq8 +7fawZPkPf9I8tocNh8gW1M+aD2PnNv9ZBwayYOFUEKJtB+DUakmieFm3USDuw0Q/ +eZELc9H0KX5T8+zDFk/NnDXLZHl/0S1UA7Xr0ZYFSeU= +xWoGNWjKGPfI4gq8aHoTfCcui7R5ftvzgjCoNLWDE5qRBMSouXk3l6sAEA4jM24h +32pdC9DD05OE2l0oXazDFCi1BbdwJQkVAtSX5ha9T5/UbYjI80Zbuzx5oNnPU84gSPa/bH/kT/5aRUMT0WjBle5ZusJuzt4vjBzbYDj2qsZ1Rkk65+ZIhiS9XX3Rzlgd +8hgDxirDXnnAyiJ4NE3XAh1L75lxsU5g8Q2vpUpKCnLyrQbYQL/EUUZ+kQAxn1InllG8Bxq90nNyBew2Xwz5YG2dHCGc2iZhIVXpW6AQrTM= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBGO8XXm4X8uGfSGWh0Q9Dh4OeJdWimialzhHIAzr1KphDMQGqBgOocRe5ZY0OMuR5bi2Gwcas0w7Oib78mnfc98= +6RYaxNf0Q7ur8o6H0KcmAne5oDvYfvSK2ahZ9xoAioWCh5olS1e7HKg3qZL3C+AsaeqFUBzSkYfZEFRZVjFOiQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +AfYhxE1RMT4+vSRagSjmsLsUzfcE70zc6dM6L1ej1do= +oAptEs9PzyZpwE9U096RuCus3APIDPDy2uyJitbODeClUvUg2OU9IInsdr2NC1Go +zgKkJRNMCn8GCiKimPAwidR1NhrGRrF7Q2XXJLlPplfTFnChzzCion3R9SEDQ+foZ4w9liYQTrWrPKaRsWMToA== +QBw8o37CfRNQqICuEYC4D6GsO8P7Yz2X05moJafy1kH1rIRjiHEP5co6htBQurmlpFOToI4GUIfaIN35X6oXPA== +0/E9i/F0M+CrJ1F9soXnsKDDiXSMfOHlr0TCbPx+W8W50Day5ctjGvq6hG4Ur2LAbzqpEjF9reqGEb/vAKZ7eYPBSeYmG8I4uRN0PujYjvo= +VH9v8S09LC3phvMB+CmLUbe1vtFQrSs4ByQ5tsm1f0Q= +N/Su6K1KPpvRUC24rdHSo9Pcn1Mipkl6Mes428lawMmFcdAeP47i33W4xy9ZCoZE +ONpBmHTGg834+jRUzvf6DbXxy33wSuzhXUugCxbtoo4y77u2Ecq5viDjcWO0RVde +N/Su6K1KPpvRUC24rdHSo24O3FYDDs8pqzz2dfcCHQQ= +qDOj0w76F0mFhV/v8uZ+CQ== +1u+XjG/2+GSQRv6EzCaWRQ== +3aDESarhfDmQ/r/rgqARObXv8Xn9M6rcH3agWA8YLjU8n7FM9GYIVg2WFLmy0Xte +bzZrWU71Kl/tzM0dltE5S+oh1fstnjeyZPeRfGr+w6Ua/NAlKKh3qbAqIacZqIa7 +WoK/RbwXPZh2ejRVQ59oGOCkdAAhuCQq2//ME+i1YBkacV8oAGKt7vMrnxVl4w2PO/h720Ujo+p6duTJNMAv2Q== +1u+XjG/2+GSQRv6EzCaWRQ== +o3kxTO4RwuXO7GZ9wpXG+c9alTV6xYS1q5pSZZwCAH0= +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +UmS1yzxZhsQI5gKyRk5d5lqliffLO2gbVqdb+WFM95KiRFbdF4zc7m+cuRBaFuVkGlR9NfCfU7HBUtUKDLlUiQ== +32pdC9DD05OE2l0oXazDFCi1BbdwJQkVAtSX5ha9T58SGl5i8Kq4zTLHG7S1jDTpmiJD3xJ9+XHDUyrGE6HTiI4Wf75O+CVfsByElPvBD4AZ2THJhcazJ8ftU30V7aAQ +8hgDxirDXnnAyiJ4NE3XAoXAEwwNQ2QyoD90wpcU2wp1yA50Bj2ES07+7laOdBbsoI+WSy7OcwdR51e8rhfugr7PgaD1qKUU2G/4cNqJCK8= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +Uh05WP1QqV5PcVEL2PdGDQI98xtXE9gqUJ5FPEQvuGWuhObfgWoNrWy1VrBfPl2I +WoK/RbwXPZh2ejRVQ59oGLmF69fFU+GWrWX8eZSJVghccOKpnc2nZYxL7SirH4yKnqD3furGYyLPFsQVGdh3PZbUQjjTuHwJNgloU6EzzBjoK85GI8L4woqkEn0CnS/D +IhuisKim47k91RVt8z8qtK/TXQTSbdLKYe19YsHte9SRs25QHUzAZYgQ0fxof5lduwz1AxllhIIeHNsNIOKVXQ== +1u+XjG/2+GSQRv6EzCaWRQ== +ujLyuF4aD5b3aVgwNdn4LEZp2uPP77K8ayqbjETZ7NEqvDbrF/U24Zz6A3sBJtq8 +4+wKB+Tel1iow8au3cOIJTXxOTIBsTZ98UgT9I4N6WDO5I0NFKX7FPDWH0wxN+MEM8CFh2PbZT41YEIRrKHAJQ== +7fawZPkPf9I8tocNh8gW1GggbMuSSfyuxQNC1dbuQC0pUCKBoiH86+FkIe29b/fXXLRLVa8hmP9rcqE7IJtrsQ== +eZELc9H0KX5T8+zDFk/NnNXv04aSZ1VDBqaryxS12TI= +xWoGNWjKGPfI4gq8aHoTfCcui7R5ftvzgjCoNLWDE5o6ZSJO/O0DThhI7iNgUIpz +32pdC9DD05OE2l0oXazDFCi1BbdwJQkVAtSX5ha9T5+qjQFtb0cwYmYWRLm8TYX9QW4AeuR/8A6qsKVhz1H7ABGEtDVZkWStUmTzusexzTO7EhdAvql5TCdqLwxhciif +8hgDxirDXnnAyiJ4NE3XAoXAEwwNQ2QyoD90wpcU2wqXLKnagnLmsuUbRo7hX7JsEXIFHebP89bmeSpfvtUYCu94jmLdf/7S65TERZwPCZpxb05unDrz0gPysbpR+Z8V +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBLSvHCv5lbF+UpVH3FEr+ldO11eH3FrDkT1OuSJMVBdHcCzMHldhxmOlQo5GOhymmucHzO7G/DhLRdjL7VWfbfk= +6RYaxNf0Q7ur8o6H0KcmAntx2MI8lZnuMCivKrzMt9HT62AqyzkT0HnCS8YUYIeQCAzBSsFqtRKOzSDNND7xG+wZdOvdxeUgjbQ766a6iMg= +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +gWFiPJSntgxSRYbzsKluMWOCieKV9/Pke2ng5S1HSW5y/DCVQhfrQzCEQWmJARhh +ic9m9nLege344z3astc4PdiEs4ZI/9xLnqY4aKi0iz6vx0zP0ZvJv4TD0vQdHmoDj2KKzv9Qt31+ZPeETqBCKw== +p1hQ9hJvEa/OWih75Q+Pz5Q9VMnMDN5bSSFB0OHsJNbb8yXQEa6Zf0Bm3WVa5ah1 +N2AxSv647SE8o7pH02PLTRW2x6MA9cDI2cPf/O1sMfglaiJwFomfzciuBxQr9Mgy +1u+XjG/2+GSQRv6EzCaWRQ== +mDRkPFp/j8KULnXRVcN0Lyp5WnNz/kgxfVb5zO+CjZvIU/la8LVZWcuZ6FJgZRKDsSk1JKbBUDivtbb0qo+ZrhLl6d1rN+PoIN64Izfk87p63611k4RmUD4ChgaTE3MbfzpL82b9YPMPHvZIQmzTvA== +Nd5rVySRefuK+cWgFJnVfKuTBzFVbSzNcdb5qU+Fk00bADr0EhUmDWWE8VJXMmBZKQZ2/BejLGCtQGnnTZLjXA== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYtX+pH+0NWr1LYbDUaw1poloo/SQreGe/i/geKkMYIe0 +32pdC9DD05OE2l0oXazDFCi1BbdwJQkVAtSX5ha9T5+x3Qx+NKBxMKPHco8yAWL/OJtoiyx4uWw5VI38M8cH0L2vxDDrIW6p/FtPXUFj4lKwHyoXkfbiyS6H7Je5XddC +8hgDxirDXnnAyiJ4NE3XAnpb4a/ZQsxvMQPhJZvfVvuIb7XYB4QeeT1fsju5A6udlz3sItqQXhMatlhFCJGAPmUFnm5Zh8XBHK9c+LW0UxP2JLVNxTSAzbOqfkvJ4hou +xWoGNWjKGPfI4gq8aHoTfAVzAXop9hMYnJknq6h/JgEQTT6r137FN2QVFE8/GuHp6sdq04v1tOimEcXA2YpwTg== +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +TQRmC0vOvJ1P+lfyS3Os4RuPQ8hF+YFFMqpaB/VTTDlP3bmpX2gF1zcf/IiqtvFIE7KUBJDowOTDY9SiLkWmw+k+glDsreRvMO6jUE4rEQg= +32pdC9DD05OE2l0oXazDFCi1BbdwJQkVAtSX5ha9T5+x3Qx+NKBxMKPHco8yAWL/OJtoiyx4uWw5VI38M8cH0GpcTJanjod967P5zHA6shNET4rGfyBkGPXk6v/FPBKf +8hgDxirDXnnAyiJ4NE3XAnpb4a/ZQsxvMQPhJZvfVvuIb7XYB4QeeT1fsju5A6ud2Sy1NlHAxGMQxMd3OYQTymR//g6QvP5Tmr3cNXjbmX6SHW/28QhlLh6s99lo5xm5 +xWoGNWjKGPfI4gq8aHoTfAVzAXop9hMYnJknq6h/JgGeK11hs8QPXgVGiTUQEBtpxhBCp0qLZfulH9cPU6bVxCR5Yy8UXMNPDqmgcV97Ysw= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBGO8XXm4X8uGfSGWh0Q9Dh4sGrEdgDWSuxarfQzodCIV61pnaF6n92qnPOQd6Gfitcli+smspn/tZCQIdPS5OrY= +0xm/B4AFa71L5BjV6JltokEncoxnqsOzNuF86FshPuxzuZU9fdYxwmyWnAb90taaKm98BwzhPaxsaKRBmaiQHBmUzTICnOHOmD5xQMFRwA8= +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +O4aek/DKwaWYXPpxyDmn8Dwj7Hp8+wI9C4JTzDyBSyOSEty+9uyFk+eQxt0EiOEEwGpA4F4k3hyik4xa8MlNtQ== +8ELD6Q/osNELEQY4NJz5zFGnBmiX3BbURb82nib66lYhSiY6ieDaLK0TJpNQApZwajzwfANmVtz0UeFlkCvYZQ+Pk3pmYHmAxH+qE2OF73T54i3GgbkbLU8wu+1yYxHspLBh6ROBRzHx4dcd5KnEc1JsBpxoqzZMnyQaeTsD2+lHu/4gid8Wug7JcpAcxxzN +mDRkPFp/j8KULnXRVcN0L0yqOW8umfrqR7aShWloZvEk+TnIwHULtqQ2QK5aDlL55LLOXmMtOQ4V2JBVGBsfzyMkX88uEXwXDkS3QdUC9O35X5js/wsq9KXlJbpgSnV6 +bzZrWU71Kl/tzM0dltE5S0vrk0As/eofsNFV2X57dSUVHi1smijIpQNOIDEUkx+k +jQ3ffgdk5F3qci4zFBKoIcXBKAyBSxkzXHyAK98DGV8JWHXnrpg5uso8bDmzMUMO +1u+XjG/2+GSQRv6EzCaWRQ== +o3kxTO4RwuXO7GZ9wpXG+ccsEfCpqX+ouikkRqgtBPo= +8hgDxirDXnnAyiJ4NE3XApEVlKRLAace6M0X89R2ldVUxvOmPTWqbuHGoPJPtuhsaLz0uAA/IMgQAGnglNB63mbwrur8pT2nPfMsISnuRkmohWt9As0UOPxlqrGxAWmm +32pdC9DD05OE2l0oXazDFEGpY065jdJgUgEBkm0CwDbQI3tBc7SMMY7mZve74irmOD750Rq1VALn6p2Qz0se1boLrABazyJSOtqu7kpuDTRzjLO8581bXDkO3+alVeh/ +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0XNfGTRUIWBl4JET8wgpUrGZT/gYIwgTJT1MOvb4bK/P8EHyRZbqYNYrjJvOno1qZKI8hxm6JU9Y39tFX1C+j28= +1u+XjG/2+GSQRv6EzCaWRQ== +p1hQ9hJvEa/OWih75Q+Pz5Q9VMnMDN5bSSFB0OHsJNbb8yXQEa6Zf0Bm3WVa5ah1 +N2AxSv647SE8o7pH02PLTRW2x6MA9cDI2cPf/O1sMfglaiJwFomfzciuBxQr9Mgy +1u+XjG/2+GSQRv6EzCaWRQ== +sMKoPjDBuqzbbqMokHb7qBK6eCzOVQDquwxr518batA= +ljjmkU1Q/8Bu6zm/9Kyjlz1n+vbphPN17dG062Zh6qnzLsomCI+z2cu6YkCz0FnVxi+3sxvaO+VQBjwm2SZDJPZvwi76SFMRpEExqS6CdkDY2ReTa6XWNRaOmH12+cEt +1u+XjG/2+GSQRv6EzCaWRQ== +gZ7BoIAABc+I8g/rYzq+f3zw1YdgnCLySWu6UKK8Troc6wJZ1uW8+TyN9EcXK0Hq +VmVrGQo2zRokW/ZuO9bN68yhfDu0q2U53395uu7cTD6Jj4SiUIk+DA5DvLQtYJFaLpLf6oC+Zj98Rb+W3mKcQAyhtF3mfDYaDm+mBFnCQykivFDc53z6Gm83/gIK7SpU +VmVrGQo2zRokW/ZuO9bN66NCc6SIFwft2suCXJ0OllShZ+C4+jJGGTg7tiXgNGtkz1v9KX0ErEO6aZ6Er2g8nfmEe3HbhELEJAf0RlRT5IUvCJjHqns494BqLCiVJEmlgE8jqWgwl6ZgP4xb0TYJZg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpT73tXJGPh9xzuFnKDgTWtsdbWbNX/tXxLXIlkjl64xBuS4C5J2xhg4JBnw7ir5qk9qpgihJz26D6Zi3e0CbqbQCEpaB56JvfVxWJwrEwJbUV9ia0+BWmD4uBYD98d9tzBzYe6FMX2KWyi8lxIrzqTQ== +1u+XjG/2+GSQRv6EzCaWRQ== +zWJ7/ALVhEfwzAYgn+dRKswddbaeR79XwNONdh/sTNCO+BK1uX/Gse8oNTPpGw1GN2b67iK33WYtn6In7TJDWWf2ePMTQrYnhCXrBbzjqT4= +k1ktu/l6HFULD7Vyr8Cv0KP7PNmReoQOWf8Y2ED4xF4ZQLx+BEhzgvnG9vMmTQmSTVdERCO0Xf3/cfMoR8iJ6lz3IOX1aAi7TbKADKG9oL2bGeQ7W2qw7I6VC2LwgGiQ +1u+XjG/2+GSQRv6EzCaWRQ== +7l9d04qcMHKdUu9FRiKktau3Sg6akhe9l3PRtks7t64= +a0q7lWJSm6u4huPzjxOOQep1TYTsFv9gGElhAisuDgPJ4eCy2ZTynze2GPMkYk3Kbl+YOR4qSyD3tByKTFyt9Q== +32pdC9DD05OE2l0oXazDFOIHV3NTG5lRlSLTG9FZ9TMtekMLJ+bJ8AwiqyaW+nVL +1u+XjG/2+GSQRv6EzCaWRQ== +ljjmkU1Q/8Bu6zm/9Kyjlz1n+vbphPN17dG062Zh6qnzLsomCI+z2cu6YkCz0FnVxi+3sxvaO+VQBjwm2SZDJPZvwi76SFMRpEExqS6CdkDY2ReTa6XWNRaOmH12+cEt +1u+XjG/2+GSQRv6EzCaWRQ== +gZ7BoIAABc+I8g/rYzq+f9wRS/yLEkjmZguTwxPynAm4YdQdrLr0p0joitrQW3tD +VmVrGQo2zRokW/ZuO9bN63KhtIGhClV8lKEP7LJ9bvu1/9SW47zTc8mAeAK9yfZ1a7qhgjXk8vBf/dKtcTNZ0ax37QChG0Hlrd1/HcVV3XF+tE2UvN2FFw9ObveudrTG3WyLB2mtsFCGwzQ0D/69jw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpnUPZP5I1ml+E1aCJdhd7CkYbD2NI/Cwh8L2i13eZBlVb/ntGp27aQ1PsrXBsu2nLzOnxQhLKTxIYhGqh4xZv675atHosYofA+/Jx5mWz6NRG/D+eUeQAqJzcslcgVAYakyUBMKfEEsdpuaf5UOmNOg== +1u+XjG/2+GSQRv6EzCaWRQ== +zWJ7/ALVhEfwzAYgn+dRKswddbaeR79XwNONdh/sTNA5nRL3au1RHYDH1/5J96qSOhLA/MnL4a642Mb+xCXYJTlhWnE3vpOzTyEuNXeV69c= +k1ktu/l6HFULD7Vyr8Cv0L2tearPPLAF1k2Yxwn7SM3YZBNYu8v0r6sPjPJDQ/UaYJ9y3fO9DQ7aUBscbefFJQ== +1u+XjG/2+GSQRv6EzCaWRQ== +l2FJPs4YkAmmok1ulDRuSA== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0dYBGHqHJ+ZHR0bgDyInIJZZzQQvijBrBN8jA6zW8mkSX2gS6R+Ozsz8OpJPh4jBTg== +1u+XjG/2+GSQRv6EzCaWRQ== +IhuisKim47k91RVt8z8qtLwjSwIip6cQtpdYaeIQehPMxoSsCV7gSNK2n8mfVb2q +7fawZPkPf9I8tocNh8gW1M+aD2PnNv9ZBwayYOFUEKJtB+DUakmieFm3USDuw0Q/ +1u+XjG/2+GSQRv6EzCaWRQ== +o3kxTO4RwuXO7GZ9wpXG+ctGHEJ1gwkP3dOavw7bIO0= +xWoGNWjKGPfI4gq8aHoTfCcui7R5ftvzgjCoNLWDE5qRBMSouXk3l6sAEA4jM24h +1u+XjG/2+GSQRv6EzCaWRQ== +8hgDxirDXnnAyiJ4NE3XAhYEjurUtKtsotqEFRxrvovmdycwwos3+Qkh2MGIv1TaULI5J+NveYWrdBc0VBR5qo1zvMIlUYwftJpGe8YmjPI= +32pdC9DD05OE2l0oXazDFEGpY065jdJgUgEBkm0CwDZ5zMyx2aSiQQeWrQjmxny0kXV4y/ibZmLeL1QrgP6Xzq5LiTnjopGM9cZhD6WbeT0/3vRqzYcfQwCzpSkzURcI +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Wh08/fy52mF/swYriPujPxjJZVljR+KdhHigD0XKfnhFwQ5YrW2aWXvdVzhxNaXguEIPZFaNpBVpVxJd0pzan4= +1u+XjG/2+GSQRv6EzCaWRQ== +xvhr5kRQI+GM++iQkYI5iRzYgEK4/VTYe0WhCZrOYLbR3msUiSBD7xvy8p9JDsSW +bPvoUv/oYIf95kcOJg0X+NNoAYMJQAMLTjT9pmuQRiw= +xWoGNWjKGPfI4gq8aHoTfAu9Vs6/gfwfIM7OdnTK36Kfyn1EWsGnxFcC//aU1qhR +1u+XjG/2+GSQRv6EzCaWRQ== +nHTktSVwzL+LTRoOM6vU1JkIBUJIzDEqGKhmLOtagMOf8iOvrGRM/dOQOyQ6kgK1Xo1q9yiZcZD/0GK/4H1xYA== +a6zIn83aERAna8E909JSBHm/vydPUR78j2AfKIzU3catrtZQ6G1SNU8GZ2ojNYDY3rFyY+AFxCQMH1W2RPfsWDBG+bp2fe+339xcyDUQauc= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgUvrGiMY/BUEAaAwYq6mHKHgxj2b4jhhvkbC9TjHed3dQ5kxfbJH8h7wBPvR/HoyjuzeY2MeoHl6yqWSlejIj4M= +1u+XjG/2+GSQRv6EzCaWRQ== +KswAwn8yMtQiBZzxbzyvrzMxWc9NjHna1XdPoh75eOQ= +ic9m9nLege344z3astc4PcEJamOecLZiB1PkVZ9oWvtNZ6EXAqDfAUwfAKoKj3RA+Vh/eWJyUCGYV8lIfCXi959iRbY+alv86eYmHedEmSA= +Z8UsPk1Q7HtwjRd4g01ryw== +gvnPhBIKcIKOwmiSOZA5DBVFpY4mO1aB6cV+QQXA8Cg= +omlT268+oBkimNFOsNx6YunY1ufTnG1xLJhuUGAdlNw= +Z8UsPk1Q7HtwjRd4g01ryw== +neG2ZqmysBKy0DmUM70J4iiPfPCyqwlAQFJ1UVW9tyx0lB7dlCj1KImwGNIWCJAF +UAktoVS4akIiKTcgVUIl8dzIBWAK47O24mfsy0MquWM= +1u+XjG/2+GSQRv6EzCaWRQ== +neG2ZqmysBKy0DmUM70J4vbukQaHE0gUMnrX8E6T0tYJ4lUJCGMpVsNwhUR3EOS0 +UAktoVS4akIiKTcgVUIl8c6Q1/CFtjCX8ZjdkDvQNfc= +1u+XjG/2+GSQRv6EzCaWRQ== +neG2ZqmysBKy0DmUM70J4nzfAB2BnukWfJon1idMsXgB85kwTgteyodQAzMCaRUU +UAktoVS4akIiKTcgVUIl8fo8xE5+HFnXsZ/VG8SNg9QbX2Zn3tTD9EXgcaUENTvj +1u+XjG/2+GSQRv6EzCaWRQ== +neG2ZqmysBKy0DmUM70J4r5Snk0+WRT229e/KLul9UuKwnJGy9SABbGMfSxNrRNk +UAktoVS4akIiKTcgVUIl8Za85FwNfaFtdwG8k3mc9w1hljQ6Hyhk92zvziTGBH8E +1u+XjG/2+GSQRv6EzCaWRQ== +neG2ZqmysBKy0DmUM70J4pR+5fGjmcFY04mKxcb+NkfTmUGk0Pp235eGnRoMPh2Z4XlMFFFdHQqIRKMgyi7w3r1VSqF2lwjPHVCPMbdjeEo= +UAktoVS4akIiKTcgVUIl8f6vZz7OYn3aRsDA3l3DqywrV32HZcUE4Ouhh0b3Q46V +1u+XjG/2+GSQRv6EzCaWRQ== +neG2ZqmysBKy0DmUM70J4ufDq5bIgyvNFX+xNgoXw5gLoaqTaQckHsD2jMsSN1s4O8ogda4ppaYW3YSI1oVaegOoWnofD0c+/fGVwh0SV7g= +UAktoVS4akIiKTcgVUIl8XCh4UNWQHYgn23kwh7Zgy257V+Md+P0Lsk9oD4hmiyo +1u+XjG/2+GSQRv6EzCaWRQ== +nfCoFuGbfqaeHgp9xDhU2sqztj1hNoztcl3jZgyzBnzWvu3vhX79IUdg71nlyYqM +GlJUIzKyUWmd9WndFdsMBAaxs0QsjrkL+f6oe4h4Eld9SvTwHsAR+0pL6LnTsfKUOqO/hFyvDqmuFDcCM7gnHA== +V1ScATKwVuxN5IABiDyoJdTV42dHALLvsDdEXU4uHVtVZ0XDRo6QfIbKpbVyUh9x4ZaoTT1nwK8OuBGVlBRBCxqVeH1+3vhmpZbvtSc0WOSWgQtQKNz0rGhqfMCI+BYHn0VDEbohoq5AjNji7Ce5DSdNEqDTKitL/25KGt07xiUR3xmQn5AcakmTSBIEqx0e +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +csLthA53ojJM7MvqrKXR/IVSbZvZqW9/ti8E3LU+0sY= +DQh9OqLqPHUvWKrDS01q945GWzrJkPBrnXQZFgjWbJd+EwN2g6b4lzhZ4K249a7NOSh4gftd4pzX9/8f9t8V/Q== +96gahBYKYgdoVkNtss7VEIqReftla6L2G/wQSaYc+JtZ2mltrJT0ayycamLkAMymad6CkOw/Lrrfl69C2zBWSQ== +jQ3ffgdk5F3qci4zFBKoIcXBKAyBSxkzXHyAK98DGV8EReHjma6MPG5BkPVxltjf +o3kxTO4RwuXO7GZ9wpXG+ccsEfCpqX+ouikkRqgtBPo= +8hgDxirDXnnAyiJ4NE3XAruj382Df9L5WU9Xc7bZhJKTvZJmssE+ZNd2zehiIi0zySWjj3T8z9p/U9GoORYd7sOCSIhN7IFhmkwC6ha9qM3AaffFnyjh+eKzhwgU1WAK +32pdC9DD05OE2l0oXazDFH9aIUzYqykzFyV91nuGAgz5o36+rbZbPvhhAhhso57ADPyFMWlOHZBYuMIfmsc+GvISWliXxkO2+ze32aSI8A6Cn0ON6Aez0d33h5586GxdO/bOb2Gkd7O7Et8OQhulPw== +L0eUthVnpkGsmKFAX6d+uKdLEGlEXm+o7O9zcmHr26U= +1u+XjG/2+GSQRv6EzCaWRQ== +MzOj4ZiBOJDNVP8vi/lFQBnf+vH0KPJCtQI0kfLkdxzE3zrEXe5Q2trbA9Ips9q5 +8hgDxirDXnnAyiJ4NE3XAruj382Df9L5WU9Xc7bZhJKTvZJmssE+ZNd2zehiIi0zhvhEP+f0BqCGut5sQ77wA99os+thTFXzLh3CbgbSNZI= +32pdC9DD05OE2l0oXazDFH9aIUzYqykzFyV91nuGAgz5o36+rbZbPvhhAhhso57AaCY9jqliNSQZ2LVkroT3AfToqjQkHSdaEaMU7S/UcTM= +L0eUthVnpkGsmKFAX6d+uMfCKxbeZE5XF/T3qfhxdVg= +1u+XjG/2+GSQRv6EzCaWRQ== +4+wKB+Tel1iow8au3cOIJeDFU2Q2B3JtrpibuQEbYES8SabktJN7fqgQK1x4ewV5RO8D2I8AhLEtCMss0y778g== +6wrc0niBYuog/c7MnH4VtcoT/Tfu4sUPPe/LrZuHbJGoNO5gU5vQ5iphy4qPmUZYq6AmhpIlOw8aD51ubOGXnw== +IhuisKim47k91RVt8z8qtJyWLVz+KjhPuZWupljEG6xC9dMadE3qrj97YQsLRp7E +4+wKB+Tel1iow8au3cOIJeDFU2Q2B3JtrpibuQEbYERAap2FyD37a6qdU/GC8K3eRu/4Wvd25srzAJzT6RAcSQ== +a6zIn83aERAna8E909JSBP8SJBr70N46YgudK+Sv1Qop/gxH/Mi8DST7vZVoEJfEHetyd55JLA5s4ROPds+DqCW8G1uV7VrAQjhA35rrd+I= +1u+XjG/2+GSQRv6EzCaWRQ== +guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE= +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +GLGtUp+TFOQm+GBvlIABmg== +yumxBYTWMrcppQtunXLfmyEyOAz+K4NmRL+cFQgmtVI= +A9UA1yJHjpCuekrdSHwJzwuDsOzAva/TKyY4hRWkzUItvkH9O2yMtLzK1BqycTAR +5q5ONz8Yaa8v7vRvRPTOzakXAoRyKg9pON5usuPrXorHSii72CArhD5EZ22ZviPj +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+NKPudEm++RMQZs3h+yWJpXSp4aRpFh9wJGsBkiKsHmF +9WE49JNcaw71DcH3jZRpmVwnPH/UaQKqus8UOE6/l52OAN3uW1xlorJ8f2t6En8j +S9tzajN7j7km7OLOHRXxfSHfnw+TtpuP+NLP0Gzk/e5WV7MWASPZuEhp5Xsam7rT +S9tzajN7j7km7OLOHRXxfSHfnw+TtpuP+NLP0Gzk/e4DFqSiIEs8XTS7puvw52m1 +1u+XjG/2+GSQRv6EzCaWRQ== +oVio2TufACN3lw0gU/jnBOjY0lEe/kxafoD+w+zPtFignUcYDzgftDO/QdXWk+Rx +cGzT2936rNP0cRSqygcUrG2qeeQUduHW4QB07SyID2pn3NgaN6zRgOwLY6ByjyC4 +W3A5Dt+8AxWSKsTYeXZoL03E+GmF6G6ZqKMiSSB2XE+S2J0lAHvztfjEq7cK++/6RAqMfNQ2O/HkSgB6w6nRHhdnFMRajeJIomuRGUCboZe+BWjJqWc7ADlhoqz9wDqAmarDK7NshyxA30lz2Aug+A== +xWoGNWjKGPfI4gq8aHoTfMILR2ZzS4c+Hu2XOwXpf0zVeJvwZXetFtWYi+vkCyUVIbLFP303SoVxsMoy3AE211EkR4yh6N5rWi0xHDl3FBM= +JYdPWj5v53ypTmGSOiRIZZjWI/qJIrSui12uoATtWVc0Z+qBy9Y+0p3qHMEVtIeV +y8KOrd2JMFCujyKjz49scwnPCdv1TY713CDyrrQ//Gn9d3P0H8rbCLW8XOZU30SO +DlxUi3JAc/vyg4ay0A/IItFIl6i3fu/PnJqNXd7VVeTWZG+mT8lSHuVt1ZhP7RjO +cckoT/9AvNsG/TDCsQNU+tAHxhnQmFupeyfgMD4iCxhCQmuPMTsP075ogjdgbabg +v+DDyXDjf61g9n4W6mcmtthl1GYvBAZPVxvw3FJtXWvVJnZdmdLyZ83L0Ig9TgZg +ZScVkvS+1nFlxR0kJgx8r4przx4jubQnKFwJYHop8YCpqt1NN+lfBsal4NV4yUd3D1f98lRwh2bQuY8ln4Ydvvn+8YstTNwUhv4kalesCroVoOmCWAx5udr125AdQssQcV5VT6+9CLaosZGd5wa5Jg== +1u+XjG/2+GSQRv6EzCaWRQ== +52/Lmy4Q4wKraJQg2hRIeyuHFHLh11m1ZeCnt+Ni5ug= +cGzT2936rNP0cRSqygcUrJOxUjnCHa9sEX5hR04nyiLdvhetCk/PoI82Y5lcc00/ +wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9jKh/8KBfZ0+G+MHb1MJ10o287KiHh5wxJj6tj0uIAaHw== +NqhMbY8+EQI8zhTG6Zh2I7OuFm0LEyDZo5ubxQ6qlyuG3LeSLODqszufAD8BP74/6Zg2lF8CrLjuMDYEmGv7Ng== +1u+XjG/2+GSQRv6EzCaWRQ== +vTNP81yZLjoLdNpbBobM74/BQxPMwxMsJu8vs2PJnsg= ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +qQRjwP5jr3EkHpYOFL9JKpejYz3YkCA6D/FaPfRpuAg= +96orka/uERLyRst14azQwhglCKRM38C+C4aUvBEVMMIDcSTKbaOo7uyX1IVdLPpT +1u+XjG/2+GSQRv6EzCaWRQ== +hKAY1MN4tjDTCH4mzcSt5x3bIyz256V7Ktk4aXetbXTjkvzjKHD15uKc8LJ7EOHj ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +lH/cdKlrko0tsugF0s+Vlmch8L7TiwqJVSGo0y300TN4LLLRYbuM32nlrjpXlpOcCg8dZybbMXU0l+U4PCfBvw== +Zlk2SG0MJaZt6EfRBI0kp9vUxHcN1+bEkJyK2UlKPfv0QxeK1+j8vMCfDiNB4kcag2mAtxXyro2MWSfm1cctgs+2MUSDMO1LN0D4WDqUFaI= +1u+XjG/2+GSQRv6EzCaWRQ== +cbvASHjpysrsjdY5RctXmNuRWaVCTV3IfXxq5/B4M/l3pXW1iHZZkPg8E2gTy8to +YY5wEo94l9qfo5TPxQmUgsjASH85p5GWa2GCKbiYjuhHNdPmXacx4jJgyIx3crg7JMh0e6jas9TdX6IgGuKJ8e+zKb8DDG6oIcqgcRHkJJ9Y1gHeomwDIkT8MoVShXIb +1u+XjG/2+GSQRv6EzCaWRQ== +jceVTICIbdeQFZqTJKGutUQB7Hbi81qWzj0AOly7qXovSR3Lf1HM0DNmAwq/l6om +1u+XjG/2+GSQRv6EzCaWRQ== +thVfqBgAco1nB4F6IluvgSqMP63nNItipWQE1gNjliLD2cXvuUd9muNJSLOfozaG +MMrMMpc9v7ckr66/VK7Ec1hIOdGGwmvX9E5gNWPp+MkCPVdLAZb9AxNz/Z/G9FvN +CAhn8dRUItEbEErp4w+lX7vbmr/bNP07jusNGdRn6UMqnttWuRB8zdyXUvih53uhyR9L/RgXlaLctW4AE9qHdvpWtKn6Vd66tj2LDakdZj8= +1u+XjG/2+GSQRv6EzCaWRQ== +jn78igeUmfFOceOz2+Dq0t2oRr5O3ng9C3dd7IUVhRY= +iFyq/frsCN+T+ob9HZgV6RBC4AasXij0tA2Q5r6p0sqZ8S5iGJ8EQfPmoQg/gwZVwCG1mGwMchcPX0D7Z47ezAfOVrnMuRy/wH1nj69wXNA= +nB06I2Hxz6OOZ/h3WgJqXO93iUNYGfwGKgOYnEP6PME= +kE0OGUDieDpLXO/pX4APBArHp97I07iQR92UhHwLo6k= +xWoGNWjKGPfI4gq8aHoTfGhDoL0FWhHLxOhWanDWjydm9xWWyNnAoNHcNpQ0g102 +xWoGNWjKGPfI4gq8aHoTfBbHBrew4OORUYkhM6YqKjf6SDQWC2/RaegGQGu8zZGJKrE4SiBPQK2X49lKnyMSlQ== +xWoGNWjKGPfI4gq8aHoTfDKxSKKNgns4/0qynXnFwUEifYc25s9jEppiSHlmNgJIbbiAZqBzjbUlKMMWWAWZFA== +xWoGNWjKGPfI4gq8aHoTfJtqtdlbDzIlSSQeU93WLnLhiTD4ARtudXQBl82ZNrDxq7aGHKp4d0YyU0yv5Akxog== +jIYtewpcL6elNlf4btqXRw== +1u+XjG/2+GSQRv6EzCaWRQ== +blmlG6qfvRPckY4P30JkN335dk8Qhiv9xtSTJvy74nXt4KKGUQeMxv3LGqC4EiKlrrrM9waZ/IX8yREn7fzx6lHyHwLbwl89Ha8qWWuN2sE= +1u+XjG/2+GSQRv6EzCaWRQ== +q4nc/jwATOMUyfSjLibfEenY5RFzfDAlBFkea5rX8Ic= +1u+XjG/2+GSQRv6EzCaWRQ== +nhEO6jGYTLTxNd7tfL8haGb6dIE98SqR0mAOJokWbaA= +2gqFvhn8R1IfPGeV2rMswbHUHFXVUzGBLmhsu02wzqE= +1u+XjG/2+GSQRv6EzCaWRQ== +C3JSV38/w2nvM3I7TZ5+4epkOMkaWYvoOErk6ygROHs= +to+v3MAswOY2NqesdSm5Exj6qBaLBkXUylBlDyDbIhw= +vJtKpUSg5Ukl4nJbF6QjQLuZX3zqzNFC23N7KFHJ2pb9VBWPaZgOK4GnOINBgv0Qe7eUBHDPvgSE5jB9PRi0bA== +m64JmVQ8ZxnJzJGTbbSLFmPZmjvCILB/bV9nRo8GL6NKsGOJsqMWcGGUi++CT+7m +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +I5csN8e3J/KIFkMa5t+SJIq0Ov5w/UnVo4TFeszykl1vfhlws6PesLWNE3+wXgp2 +iZaIsa48RXfE+uf/yF/rQFPJLjyJKEpYeouGzVmJly57Pnx1mBkjOCPGwtJiM69as+U2C/swKlzzbyYBv31C7Q== +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +iZaIsa48RXfE+uf/yF/rQDmVf328qBPv3bR+Py+WNSE= +1u+XjG/2+GSQRv6EzCaWRQ== +ed2GXKkXmwU2EQk+6MUOb6X0dykjON5F9loCj9NSFIJ1utl5wcg82br2RuE7nqGidwvIZth0k8yrt/tKMlvveQ== +1u+XjG/2+GSQRv6EzCaWRQ== +UdRrmeNuA6iQO1dKro9kRwF3OYmiy5ZlRqyd3A4uZyN+pGoX0K+1Onfj/vzDTyNDuILAyXnOlgmr282vwaZMqQxD6vNEJivqCpZ9XFuzAiQ= +YabJCT93a3/IohXaIkr3oWlupiwiF+JLx3IX7SB5elhN4NhpGYJQTmff6iRPs2JdtOAZBzYcm1TdmxaznlUYkN9sOVTwRJNPkv415eed3cyu1xFcEqng44Ouauyp/fl5/Xn8FQ3+NGfujRnSxomrATZM9so5PfWEeXWn2zk/O+AsF7DyGrsU9Gz/S1xpPl0MTJYbqSABe6w5QSXv5ivAawAMfWT2nJd8IVyiL+nuVeNGTerw1NJTlRxAf6/0DibQ +1u+XjG/2+GSQRv6EzCaWRQ== +ei/1A+2pKZSasjYF/t4qonJTYWTgtQ6/8yY2jxeVp1Q= +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033YUrJ6QMqkWO5Cc0jIsEGw+6iP8H3Dk0UjjYsqtFnyUkQ== +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +Z+ggJg2m6KwS0nwm7QET8pV5hELSna1KrJ3dbsxMCZvtvEutwFmvxFwwvWdWg0JOapnYKCpfmfjmyKil9taONgs4/en9BS/usV/XEf+JUTs= +Z+ggJg2m6KwS0nwm7QET8rn13r7w/TkLuMZO7IcPMTSmMxfZpJJJMtkuMAvCM5kcaEei7wIzhDqADk7lQX8WZw== +1u+XjG/2+GSQRv6EzCaWRQ== +AWid6G8ypXqQe9XxPPYED21qUZ05KduG0I5aBgcmb0M= +4aPKyS2x5nu37fgMhLLhDhMCDK1kNFoG8MumDTVGGyxB4RtDSDWoU/vMteU2gspy +pNBzRwWPbUPagxYCBV0D9504jwGzgP6k404+8oPMDSY= +661hZf7vhUQ+50okfwfTXw== +1u+XjG/2+GSQRv6EzCaWRQ== +cXtpAJUiuedkrLUQ1sudz2FjlsFQU1P3cvwuWZEnYro= +GGi2YAbG0JZpdujBWPDe8q6r0BXXG/ADJ0/vIyhuvF24eqDiBL1a4pgQF9vmODmY2EanODYfYSmVY9McoVPpNQ== +2yQsUep5WrV3T3htNZ1L8TIohok8dxSGY22+WeWcmQzlauL17DyWRvW+qqAHUI9z +s8oZlk3kc0FacCBDAb+rTuU1p7WSuwEaBvHSlQxhPF1A/pzDaZHAC0iGCpRNjvRQ9gHLPvTdcN94+q732TJAjg== +phCI6T9adFO3Wc/DVsBsqQ== +32pdC9DD05OE2l0oXazDFDA9Lzw/0WeBxrdioFHnxzVwvcVrLrxArU4IIOJOa11S +VmVrGQo2zRokW/ZuO9bN6zCkMpfQe4UnnBkVgICzbEb6tolP+BPLLou1a+/RbH1M +VmVrGQo2zRokW/ZuO9bN6xNU5PME4RrEC5PBTWK1bEU= +VmVrGQo2zRokW/ZuO9bN6965xIByjT49kMuhmr9spXrl6X5/thGLtEfrS6s7Jz3X +VmVrGQo2zRokW/ZuO9bN6w02CrW9obh4JLgJqDECxKzH4NhaqQzS+0lZtQC6hevX +krWGxagzYm1msYy2LL9fQw== +1u+XjG/2+GSQRv6EzCaWRQ== +nNkEkiLAGyoMlIeE163bvCjozNxoG5VPQyS9oAyg2RtYQyGDfNbsHZUXkQuqIg9X13MameKB055zyFkjS7YoOyUuPt/6wl8Hkip3Sy2v26o= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z5HnZoMWfDfSE6uG5YIN4bem1GfJCtbwzGPfNujBHcvww== +1u+XjG/2+GSQRv6EzCaWRQ== +UN4dh6a4b0J/bagwf+RdMs7USLMnorVY2XpiQ4UceK8= +YDTEV1k7tbLtvEoTqGEv2OXhvBkm9KUpW7wsfNngLXivFnQ2VcwH/UZImexftwrtfaVwMoU15vw7D9tN6E1f5Q== +2yQsUep5WrV3T3htNZ1L8ZgufAH/JYncX/gwsMyHRywlSW/d2XbPF8kVwpa/08ZU +dh7w9F2NtRbFZXQAT43UNN7/yW9jpPDxwnQLlmXSh2N40SDWq0C+2vAvknDDgCGKsXcDvi/C3MZbXoObOf+Ygg== +s8oZlk3kc0FacCBDAb+rTn/xz1jUP6sCeC6dcjjAsaIRXjy7gZRdXrHmgn6OL5Nb +phCI6T9adFO3Wc/DVsBsqQ== +32pdC9DD05OE2l0oXazDFDA9Lzw/0WeBxrdioFHnxzVwvcVrLrxArU4IIOJOa11S +VmVrGQo2zRokW/ZuO9bN6zCkMpfQe4UnnBkVgICzbEb6tolP+BPLLou1a+/RbH1M +VmVrGQo2zRokW/ZuO9bN62+L6cQRTk8P6sDJrn8EAMI= +VmVrGQo2zRokW/ZuO9bN6965xIByjT49kMuhmr9spXrl6X5/thGLtEfrS6s7Jz3X +VmVrGQo2zRokW/ZuO9bN6w02CrW9obh4JLgJqDECxKzH4NhaqQzS+0lZtQC6hevX +krWGxagzYm1msYy2LL9fQw== +1u+XjG/2+GSQRv6EzCaWRQ== +nNkEkiLAGyoMlIeE163bvCjozNxoG5VPQyS9oAyg2RtYQyGDfNbsHZUXkQuqIg9X13MameKB055zyFkjS7YoOyUuPt/6wl8Hkip3Sy2v26o= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z5HnZoMWfDfSE6uG5YIN4bem1GfJCtbwzGPfNujBHcvww== +1u+XjG/2+GSQRv6EzCaWRQ== +BvSmZ/BI0uZGHBYtB585N/U3iN59wZfemYloAVPaSaUVTis7uAFH5Yydmw5Pyx8Sgknnv4FE7h8Acc5wglEqjw== +GGi2YAbG0JZpdujBWPDe8uvKC1MrIqDB0nBS13CWxf+38Go+PNVPp+EA4h4sSPIf8Ag2tSnFd7XlwYFKl+kJIA== +7Ve/CPYxbRIq/L2vO/1jNQ6UJGckq6ZieBVpLhPhDlw= +Ouu0Z4BmmXiNrI/mUpX7JrmB73n8FqZOUNkVpBZGbsp4b/jr849u9oLh5uxbLy1s +1u+XjG/2+GSQRv6EzCaWRQ== +HTdnfikDSTQ3eoPgrOd1XSIVSJ0FqgT0ujqJT4TT0N0= +khWCUyuDMwE3p+YqYO+JiuSzaDWchHxEBdmaNldbu4/8n2h1UDnGoFL3l6U+zr7yjbpDtI+lpXv0bGiBl+Gm+DUBoSG2xaU4LxcCgaLsFHAafulP/uWgQxkZFPfsN2NX +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgcKB9TzbQjRX3Joo6+0Mt87UTjldZEiqXA+V8/7jkju+ +1u+XjG/2+GSQRv6EzCaWRQ== +BvSmZ/BI0uZGHBYtB585N/U3iN59wZfemYloAVPaSaUVTis7uAFH5Yydmw5Pyx8S8WQtcPE+1GY07MDllZTjgPbJHSd079dwtH1u6JaM+BM7SczuvnoHVIn0Cxrss13V +GGi2YAbG0JZpdujBWPDe8qgDvsVRQGKs6M5wE1KkLqk212AUjZ2JKyRIckpfxrDYrol4/LZB4EFrwaW2L/zKSA== +ojcUvW+Z2ehEJ6yMJpmY+zpSWeXvgsw3nc4Fnh9nwZmEVHOyzIHhUN0xTP6ilzSt +Y1nYwlj45GWxs+Tt8B+LmX+sNN7H8KkIIw4ZqJqDU/F+DrT5pwbOp1aHf5p9sZAxXKDIRbagPOS8t1fJKz3HZg== +1u+XjG/2+GSQRv6EzCaWRQ== +P40wuHk7MslPmvVoiV0eZS9GHjGsA1BpCAEYF+DyKrg= +IZj4TYq2HlScfSdRkI+QP7ds2Lg0nLguFAxiA9uKmwWNy6Iyrh6ke395/+UQsoR+T3bjNrP1/7gKiifvP7wGuw== +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +nXHLJs+jc9UGwRbqbBpdlu0AA4913T2Ow3f4dJt3NQs= +I5csN8e3J/KIFkMa5t+SJP5ieXqyhKdpsUu8T3AoWE7POsp2/AbDJJ+EacQGfSh3 +Fabvv1g95jlx3ERwtS0pokMTpJX6SfPcIoCkhbe+ucBsMqfvfsjyGkOgq6g10Owag05vKfEyZIsm+RHQvpNIvA== +0snEhJ9RKaTb75xc3VQkt0AnFMygVKUR82TOQgO0LlTgiK0cDl8t3IfMVUhY4uZp1+Aq3ZrJoKh7aCp4JjiW2k8tNk1oUh4/x+UyMI2WQXI= +1u+XjG/2+GSQRv6EzCaWRQ== +AtqIw4BuTOuXet8gp4hSxVytPu35+6Fh6JqtAycgjXL2DjziJ6bnWfSIklmkKg1j +CUHO82G5MDpSEiqhNImyK8rF+px8D9Apk2tILux+kOFfRgA0QwD3TgdEqdiFSXwWvizkWItTF3jVcGqSzaPvEr+/L5WYdvI386+jaLSFwqE4Rv07J6yXA01s+6h0sQ+U +1u+XjG/2+GSQRv6EzCaWRQ== +nXHLJs+jc9UGwRbqbBpdlnwxDWxHB/SirxtmpFRuBJMZYg7A14xxDPiNwLI5RaAq +CUHO82G5MDpSEiqhNImyK9/OM4rF5zR/mlqmGFjGt4m+iNy3vI4bD6mEhNundnbuuxnt8UhKqS397s7FpGEVYUuR4yJt5YkUp2S0b4r1B21BoHJSK2mVnzr/rWHx+ihB +1u+XjG/2+GSQRv6EzCaWRQ== +1ZFe+UUdQmu/QujLaciJajx26a+2STfwMi+zieErgi3CBgaNFGZigZi8pTmSdXTQmGqTiUVKLPzrQkD8KSrdFg== +1u+XjG/2+GSQRv6EzCaWRQ== +lTLlgCoqofnya1u8s4t+TDawwrR4oUXemb8SF/aUeJZWG0A/XJNhZqwgus1CUe2E +1hs1FfIPISXmd7TJ/r+Vl48ped6CO4DUHuCgWusI3kallCcZLBpmRjFrGPj00Ur8taAQU/SisSdBkNC8m3lz9w== +VmVrGQo2zRokW/ZuO9bN64LUNt5+YaZJNuS5WgwZGme6UYMSga5cgiF24rpHaJsx +VmVrGQo2zRokW/ZuO9bN61kIfIQ8DIhwn6LdOOFqHc/lF3MRnE4cwPcgtSvez1VRJ9groAMVhUy6oYbGUHVpYg== +VmVrGQo2zRokW/ZuO9bN61bvrNj0Ph72scyutykwz/koB4bDmWokTi1uq1Abi1Km3tmvlDF3S6tz5+YtKcQ9J2jkI9ZdrCb4DKXDk6FNpOw= +VmVrGQo2zRokW/ZuO9bN66YryIEGaR0UOYU9rBMMwfmUm9CxVX4jsMTvX8adUwuO7mQspac8jr1VIV/pJ0L/OQ== +VmVrGQo2zRokW/ZuO9bN6xq7wQbNlpaf9ykrQ4xyoDR7YkSnhkMlXCgAAfTr1fEWEaCwUEK1oX5Jfr/TJpMY4Q2FWBcbR8MsJWUcXitbNWfVGg7AQ5z4a76mwhR937vY +VmVrGQo2zRokW/ZuO9bN65Cwfp0UoeyzmXbNAj08Gj2seJKExdcpkF+MxU7N8+3j +VmVrGQo2zRokW/ZuO9bN69nmqVlwEfkeInTfzNPBlpt3FCBDtT/vHHqiuGu29vny +VmVrGQo2zRokW/ZuO9bN6wv4AKQhKyRNmuTyLRo1pGizt7/tAWfGxRsLrowheuSs9sxqpN3/sDXEuy580V+msg== +VmVrGQo2zRokW/ZuO9bN62QuF9RgIvWYAvtkBOklWahJuS5Iwrpco5flniSnXTMnKmF6EanvevZl7ro/UcwauQ== +VmVrGQo2zRokW/ZuO9bN61GGTyOeuxbBELjSBi9UjiuHUrSs0Qw4w4RQ0FpK9Uhw +1u+XjG/2+GSQRv6EzCaWRQ== +p9oVsBgcyiBMjDb8CcPOqISoXcP31sEy6yeGqrdTHTE= +9roQnpt+Z2/MRGQwTV70NmvcIxxAFj+jWWxJm0JLQ+8toR3dEMzh+bvbY3wCbu1mXhNOFaMf293ZJdD8nAR2vRWuDQ0xFK1XOYgRT9saoCZAoMAlxYAeMwEYZ5i1EUeO +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmXE0DHXGFB6aJiNsHWco656Um0K+v9MxXclNSH3Y2Y94IBt2VU17vowICuf1l4x5+3ys5NwwgsWQGB/O8+kRvW +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw1rzSdHaz4/toHahnquvybLe56ob+rTcpUnQNLoF8FdDkYoXHE7rb7P+VtPnklwyk8leqsnKjpX8iLFIbauaCBo= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpnqrVS87Jazk1COXxY6MKoGdqTUD/rrae2XraDTP/sxg= +1u+XjG/2+GSQRv6EzCaWRQ== +/F49ykgjedWbzpXYCpMOghhz4+aAm4g1cL7md+v3VTg= +CUHO82G5MDpSEiqhNImyK9/OM4rF5zR/mlqmGFjGt4ntdgFK1WwSZcRWwmCxvBIE3s6rp4znbtySNAs0shfQr1XP3oYG+rZj6Tg0JrXTaaI= +1u+XjG/2+GSQRv6EzCaWRQ== +Z8vww4cYScz1fdJzPlqVZ+6A5TA48aA/WM1C/4K6iLj65MrumO53WZnax3RZ5aMI +1u+XjG/2+GSQRv6EzCaWRQ== +Z8vww4cYScz1fdJzPlqVZ1Fr5D7yweiZGvqFkjWBesigBcoLEhPnVl/yBmOg8mCu +VmVrGQo2zRokW/ZuO9bN6zCkMpfQe4UnnBkVgICzbEb6tolP+BPLLou1a+/RbH1M +VmVrGQo2zRokW/ZuO9bN6+H21AmEV6PHgIhYl3OzxYc= +VmVrGQo2zRokW/ZuO9bN6/ao2MdYJvICHKJi0iswn+ipqpiAi+O/r+bOKGxPFUWk +VmVrGQo2zRokW/ZuO9bN6zBS33FyqU5nOu7l2DKtAMI+wopEbCffT7KX3GUxYv8P +krWGxagzYm1msYy2LL9fQw== +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uKuogcIjV98/chvhl6NjkqTkfbhJEHcVdgJyQRXlPbM5 +VmVrGQo2zRokW/ZuO9bN62MS9nmzeLlDUQSVLN+VybIHMMP4nyOG7ekrDcQ67mz0jIHnNSKxpFxniyoeiWpU5w== +VmVrGQo2zRokW/ZuO9bN62MS9nmzeLlDUQSVLN+VybIbkojqK5eksnefGgNlzw1V +VmVrGQo2zRokW/ZuO9bN63nfiB1fb9BC3tUwYcETBbhJUCbL+zNAoeECaUn0yNhmnmBze1ME6WnXiUtSzOtL5w== +VmVrGQo2zRokW/ZuO9bN62MS9nmzeLlDUQSVLN+VybJVhEftsZNCd4velHlcgGxldZDD+5m9cDufAPjwj6Y9SQ== +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +6K6zXmJJNC0xAXfJ6TCmfaBFl7DuG7+1nNvbLJkeawSnTK85ofp4jusSyh9wK19J +CUHO82G5MDpSEiqhNImyK3tJdLE8+4hhF+l+X4RDWwZAafVU8dGDkz+ecUDZLlFH +1u+XjG/2+GSQRv6EzCaWRQ== +hKAY1MN4tjDTCH4mzcSt5683GyUfTVF4A+b07SeQ8/U= +idAyQSjf8l5iNhCK9+gaITQB4kgmdDtbyl/z5F9YPRD0ykLyuvEjiFmNreaIjKyTehPHBBk24XGkKG/+P0FZ7m8PjTi/DCgHxZIDt8B/rZI= +bIkY0AstamhoAJWQHS2qfNmqdFJjl53QH6aOEHrfyT4WfmpECEcW5OVLsa5rLQ1h2KspJxu32LsHEodsEkKYAUslzaOOc0wG2G+6Co7ob1WL+31rpusFfjNEojxjBgXY +cbvASHjpysrsjdY5RctXmP6D+Vy4glrmEJZK4yi1rZ5rwerVlH4IKMknSrmKJNSc +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0aVB8BypfrBhxU22tb3SHdH7cLG+LHPTKEXXLctsVUbMkrF/lUbZghZ3wz2/0/SazA== +1u+XjG/2+GSQRv6EzCaWRQ== +kS9bS0X8uoJgO+kRy6AKEYJI24CYqFGct9VL5n2L2/FNn06N27RdCd4EwjGRePON +OIAm83eO9UgPVVx31CBN5xreZ2M+ksUUTrtU1zMxpFFXtFphqoCUmkaNp3CiNkgE +1u+XjG/2+GSQRv6EzCaWRQ== +pYLSDpJvxx66yCfbYL/7EbfQxG/toh+Cmgxys2Df/pI= +pAfxmP+viWgKmtuFOCzDu1hEZQQp+wqTknfp6z2Tm4U= +I5csN8e3J/KIFkMa5t+SJKcvYE4NNVj3kiL8YMvvrz8H6inVAPWunV1GucokSU+W +L0eUthVnpkGsmKFAX6d+uF9QuZRvAVhOdp7bYvmE4v2pGZRCaEHh0pj3OUCYNeLAaPji9u190jf+uxjEzWhHYBk4nt7aLuBKJRPOGrG8SEo= +1u+XjG/2+GSQRv6EzCaWRQ== +pYLSDpJvxx66yCfbYL/7EdgkfwHG3aEGi17Yw6v2tyQ= +pAfxmP+viWgKmtuFOCzDuyQ1vioe7dK9S8u8CBHIFJI= +I5csN8e3J/KIFkMa5t+SJP5ieXqyhKdpsUu8T3AoWE7POsp2/AbDJJ+EacQGfSh3 +Fabvv1g95jlx3ERwtS0pokMTpJX6SfPcIoCkhbe+ucBsMqfvfsjyGkOgq6g10Owag05vKfEyZIsm+RHQvpNIvA== +L0eUthVnpkGsmKFAX6d+uCYHancDBEBlUnfd05YgW3SDMIHSXx22lljPFS1K4roEZ51GSak5pZlbDiXrhxL5+A== +1u+XjG/2+GSQRv6EzCaWRQ== +pYLSDpJvxx66yCfbYL/7EZH0qryfIX31tmUAbL+gWkEhWkWvBqU+QJ+hc6hbVqe6 +pAfxmP+viWgKmtuFOCzDu2JmOf9GQXytwsvaIgIhszk= +1V2v8QerKOmubvSxgB4eTJhFEA1z5XTTrkAvv9L033ZAG7k0tx3iiFdZDczcHAoX +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpT73tXJGPh9xzuFnKDgTWtsdbWbNX/tXxLXIlkjl64xCRxa5L//4pcJZwyko8vq8Ddn3VzIupuHrhyDNYxUSe9A== +1u+XjG/2+GSQRv6EzCaWRQ== +O3CUgrw2GJfB+mDjH5+NdrFhpXZtHMkHCqEznptoGf0lBJpze3PNPJcRe3oa85S4MDyhGh3U0fz16HxaImXUQXarHy4q9LbhIUifu0i2eHYtIl9DWdSgEfv3paanVjCv +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpT73tXJGPh9xzuFnKDgTWtsdbWbNX/tXxLXIlkjl64xBGobRRP6kDKta2YP5emQ03k2IAFnD3PzGu4kdTyLtxtg== +1u+XjG/2+GSQRv6EzCaWRQ== +FLMbt5iAjw12yzW85KcCsne9YBxTXZnNpbhDsA7Fr51zRS4AaCpZE8V+2GCKGB99jqRKEEjE9tTqqNOgz/FIZg== +1u+XjG/2+GSQRv6EzCaWRQ== +p9oVsBgcyiBMjDb8CcPOqN0/eKL3KRSmTvNKShWWzrw= +sDlCNtGYD6nPzg4cZ+6YAByKWn4AGh8Mzj5t7m4RHwvubLRA8HJ9/h6upYn0rD+dEoRGvgO5paBrB4hcTihbcg== +1u+XjG/2+GSQRv6EzCaWRQ== +mG4WxLOsQ3e8HrREEjwO6zSQnaDODPqx7hMDbjtUismkv2I9zu4roUGjTYTyaaNr/0Ak8Jd+GSIXo6o32+4njSSKq8d06R7FmG9pCBgEDGI= +CUHO82G5MDpSEiqhNImyK7iEFplWCvgelfrRiFWadE1r4Xk7E8F6zZPA1k86kuFB4F6PpavPmhcMnLVaGFIITQ== +1u+XjG/2+GSQRv6EzCaWRQ== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN65wVnPTSBJsAF25KglBK1zY8WoWiscTmnWzWDp3SoJoFUlAu0H6rthhk6T+8/hs4Xg== +VmVrGQo2zRokW/ZuO9bN60ZPyni6U+mAJSWcuTR0dUAEJYkMv1AFk5xdJ21gf5OqWalMa4OOzbm7YMPcloHCbAuFRFjmesnBI6NjeSXTavc= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64tbY8l/LJ1xG0ple3sqNLiZ8LChK6DqTZF6bZWus+xJ6uopxYut/oeNC1OHGnkiMg== +VmVrGQo2zRokW/ZuO9bN60ZPyni6U+mAJSWcuTR0dUBkfKfptHml8+4mSQ9K+w9jUZ3Xxe4ZSu0dX8asqBn+VMcv/ibBiatwY+ZGR2EeOyHh2xDKMs7iHPDLdpYVVXZ5 +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wIUNMPxkYSX/0GfNzJ3xUfYXcXzgHFUgcIQPyiinkOW +VmVrGQo2zRokW/ZuO9bN646OXWelyIYU2xRPUxRlGkJcIaIJLy1ed19RDL6uKh8/ +VmVrGQo2zRokW/ZuO9bN63SGeQVwDZMIg/q7Rk0N58YkDmBJRbv3hogMcIoRjjwsqMOOvazJs9boqaajLTdjbA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN683UE/A93CRhdY1t6X40J92nGgImx/KyCa54eyFhMmSe +VmVrGQo2zRokW/ZuO9bN646OXWelyIYU2xRPUxRlGkIoRVshwk5gzLGe1zgdOkhR +VmVrGQo2zRokW/ZuO9bN62PWu0VDstvxytifZKTp7nHtf3/ncqMW+HuGM+N2K+nVQTS+PV2r0SfVVXdiNQ+/WQ== +VmVrGQo2zRokW/ZuO9bN6+uD3+pvByB5DglVdzot2VsZTsK9H+SNLyasTa76LKnruZk2yvNkUno9d2Y3OQpRxphyCRA60H1+NrJhw5/8LLSaQXcCp1dZGt07bbIc3teV9+jYEkn3YjPItybzEN/lbw== +VmVrGQo2zRokW/ZuO9bN67Nw2FfqHjSLPJc/1V3wls4= +VmVrGQo2zRokW/ZuO9bN65msj2VfsvrEWaPQWhn4JLQOnMC5giqyFGmolHfkuSbGxIJcp2aIMZjU92LK95LucA== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN69jxBxUR3JCDU2eiVWpDzR5AR8IAtxhsgnlMTA7SgWob +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJk2Xs964T5TXoRM4N/jz1qgA9IkrN606LnjGizXZqo2bw== +VmVrGQo2zRokW/ZuO9bN67AUlCoGGGFeT+yIe3Vo1T1wlumgJWQNCCq0gfXaTzaMEE7BEh3eAD+I76ehEb9DMY8RHUnzt9m6hSDiNem6wx4= +VmVrGQo2zRokW/ZuO9bN63jU+XEf5ff8P7z9TBBfViG/kpIf7jXs93Y1tmNGW9mRQ8KmlMVem/ESe+BpyIlFTw== +VmVrGQo2zRokW/ZuO9bN67C8R2BqtiOVDd4j9LKAl9gBYjN5mZzYwBrm0MhBhKu1MhcCj1ODpye5zh6S3bgKzWa+v/yOWx0/4gEhIanyN18= +VmVrGQo2zRokW/ZuO9bN68jYngvlcpgCPZczKlSbEura1ZOdwl1Td8C0KFwIfN+HbOLHnG5adJWe8zWZBbepJA== +VmVrGQo2zRokW/ZuO9bN660x1E2z80RG1M2zA4mBLClrRqBFqfukmmRdeofCWtYPJbM5+NgcUnf5E5x3UHFXQBM90iAi6iCW94Q8SGPtMd14h/qikOGv+kfWBPWHQhR973pwJHhs0giXhGwEjakgkkmxt0itQTLjc42KcVb/ejE= +VmVrGQo2zRokW/ZuO9bN6xuBEXhOP3O4ciLUH53QFC0= +YJhe5dA54PK+sRzGNXyfpxJNuz0JDGq+jOlsqpxouD0= +VmVrGQo2zRokW/ZuO9bN6zOdvfncHnE4ZOiekMEGN90v9Keolx6PWBYkQCqgcucU +VmVrGQo2zRokW/ZuO9bN675TerfJPX0w8BHJqZqh1glUh+/xY+yOI2Vv/D8E/AFH/5EpbteBA4NqRFuF0xb/k0PV9UKofxoT/3oWAF8SvAMgGNnZauhwjMPGiidtXLh1aro46xf8n7Ib0Nno39o43A== +VmVrGQo2zRokW/ZuO9bN6xF1SJaAm3VttartJvhccJnyW/ti+4dj1F8iJbyP9yoDrLn3/I8TUd+n58isdoRtjw== +VmVrGQo2zRokW/ZuO9bN65rD4nI0MfwzlsBII0cdPaaZpIV+fzDC6hg78Cjxx/8NSrACu6//htKGSL52rvk7cwPl1C+Sbu5k/0wyDW0xiUc= +VmVrGQo2zRokW/ZuO9bN67XKiXTToYD4Xj8EJf9tU1+G0DDYHo9+c8BkhKhpt52Q +1u+XjG/2+GSQRv6EzCaWRQ== +lTLlgCoqofnya1u8s4t+TKUxw0oaCBKvUBBgu/9ntrs= +iEbw/85IA9dHrKUZiqzvJXZy1BSITBE6scdeXmXhVokjaUw6U3cWyAc1c5cQJJ97MfMgs0Rf6wBnBJMwNTsrstHS23vkCN89ogbpejUXPY89L9yw/sp4qCPrFlSwIwo1F7O+MPmPe0HtpYdS/upm4w== +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkvCs01Ef75uP/SBKZUAi7QcIuWcW1HaUBcTUGockc85s1agOkpQZfCzrtnUqIugFXnSjQrieNzx60LxT3MMOZ5RGdLjH3Ih0pcSyMNTnl1Ptg== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z7Vc7221CfIz8ZtOcaiNHvZkZy6HmpWI+rolHwFXNUBFFiZwVxZXFUVrWyCHCIWcGx/WaCGsO10+7MAHOvcnd0RLFCqhKBqnlrNLHn1aola0g== +1u+XjG/2+GSQRv6EzCaWRQ== +an12ef3MEYZRkLo9u1pLIxgtduPO1eU2evs0LB4g6To= +ZIINHHQqnB+3o7SPJLU6718MxCs8rC3NXHx0+pOLLOs4F8HgaFgB2G0uEixGsaYIuOIkktee4ok6HSonoGjeLbG1FTWjAj7SbFjGT+6PnbA= +bIkY0AstamhoAJWQHS2qfNmqdFJjl53QH6aOEHrfyT4WfmpECEcW5OVLsa5rLQ1h2KspJxu32LsHEodsEkKYAUslzaOOc0wG2G+6Co7ob1WL+31rpusFfjNEojxjBgXY +cbvASHjpysrsjdY5RctXmP6D+Vy4glrmEJZK4yi1rZ5rwerVlH4IKMknSrmKJNSc +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0aVB8BypfrBhxU22tb3SHdH7cLG+LHPTKEXXLctsVUbMkrF/lUbZghZ3wz2/0/SazA== +1u+XjG/2+GSQRv6EzCaWRQ== +kS9bS0X8uoJgO+kRy6AKEYJI24CYqFGct9VL5n2L2/FNn06N27RdCd4EwjGRePON +OIAm83eO9UgPVVx31CBN5xreZ2M+ksUUTrtU1zMxpFFXtFphqoCUmkaNp3CiNkgE +1u+XjG/2+GSQRv6EzCaWRQ== +JO+88//yCrSdCZ+EAMDm0yFbSIopqNx3oIgThJXexUg= +3evf4kKYx93swnLRU7HD6o3c0ZSE4DrGhUeq5hD0Tk6sCeZ8grDcjGgmV9ekLXlXLrjiKhGxLN/RGc9CfEUS2hZK4IFrEcH9P3r7zs0yv9Y= +1u+XjG/2+GSQRv6EzCaWRQ== +Ok7/pyXurdp+IyZCH/4JsjwYGkFmVamoKkgQ/Sxb3qURbC16yjmYW7NsBU4DwPV1 +pAfxmP+viWgKmtuFOCzDu1hEZQQp+wqTknfp6z2Tm4U= +I5csN8e3J/KIFkMa5t+SJKcvYE4NNVj3kiL8YMvvrz8H6inVAPWunV1GucokSU+W +FsRTDT5nlYHpT6JC9UdH4T/Ks1cr44pCsms/6UrSZVup0zIWRkrkO5J74QApXSGyC9gRdNAEpidKLLWeHj+Wpg== +1u+XjG/2+GSQRv6EzCaWRQ== +Ok7/pyXurdp+IyZCH/4JsrX/xmC2hqUpuA5tYuXgXM/zu/KZHZNQGd8q3r453WRu +z4ZB+KgsiuNQr27ZYy3/iLBr6gY3JFidQY4HnHOG1WE= +I5csN8e3J/KIFkMa5t+SJP5ieXqyhKdpsUu8T3AoWE7POsp2/AbDJJ+EacQGfSh3 +FsRTDT5nlYHpT6JC9UdH4e46H3OUVWY2nDDUZuv7bYX/aBzdhhcivJQ5ftagC0vBI6oRuNmT4qbsTFi6+BY0bg== +1u+XjG/2+GSQRv6EzCaWRQ== +Ok7/pyXurdp+IyZCH/4Jsir7IfJ+8RckVBFzCfSCTIfUG3sHk63L30a4Cc8bunFe +z4ZB+KgsiuNQr27ZYy3/iC8E0qBENptDhINYHhQB2rw= +O3CUgrw2GJfB+mDjH5+NdnSaXRqSe5/vglum8pPqhP1kN5ssZXebzL4qrP3wm1YS +VmVrGQo2zRokW/ZuO9bN6xOcE74GXukFgoP7mn89osg8SsOUQXFdzvPn+qkat/4/ +1u+XjG/2+GSQRv6EzCaWRQ== +EFBFgJDcvthaVbYohI7UmsUkC6amB/doNkBisGj5LmY= +nNkEkiLAGyoMlIeE163bvCjozNxoG5VPQyS9oAyg2RvorzxOhrFffjzL1Y4lPgxXPigscZpJNW4Xksn/TqWrxXexkrtmcCPVaNBXIZ8xAh4= +1u+XjG/2+GSQRv6EzCaWRQ== +phCI6T9adFO3Wc/DVsBsqQ== +32pdC9DD05OE2l0oXazDFHGwLxQjI8TRgs4IRu7iv8BCvdjOJiV7f4nfgJ+TK7+mP+ORhQXW55an/xk0xcPrzQ== +1u+XjG/2+GSQRv6EzCaWRQ== +lTLlgCoqofnya1u8s4t+TKUxw0oaCBKvUBBgu/9ntrs= +iEbw/85IA9dHrKUZiqzvJfBJjW1/xsIsR8A2CXbUbTDF856L6rSWQ+dTSrJL9C43oAkW44qMM6W91BRZ6TbD8HJs0JZRxyJR85GlHfR4G7E= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z5HnZoMWfDfSE6uG5YIN4bem1GfJCtbwzGPfNujBHcvww== +1u+XjG/2+GSQRv6EzCaWRQ== +5xPAMLVvOxwzYIoGEl+KvlDMXEKZkD6wifs2Gz721lQ= ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +AjHNSdyORItsH7GfVM9IMkEQfAG8nvpJ5PlBXkkA4c6uVb4nV8pDwKyCuOnroe6myPTXQuthSQdyVS+yTRqpatFK+UJ3mgiIlT0oL/T0j5g= +91gsC2HsVrvTm3cwuG3h9ku+SnEuh1gfPlaT0BYQ3ws= +ewXF36/n6Kf3NzNwV/iVfvwkfIk6841e0Ue7kEYDxQ4= +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0YxSyi5rjFDyLtdtV6fGTbqCUrVbW9jXMKIMhkWcPFOF8Dui5BREfhlXCbWX4Wq02oFgzzgkuOtvoX6E1LDgkC4= +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9sd97ybfxMksfg+EU7vkw/c= +DeJGSUuvGZdpIIECWorxwVf8BOBEktMLPmP9cIr++leKhxxVkTq7YW2L3GnsowyD +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0YxSyi5rjFDyLtdtV6fGTbqCUrVbW9jXMKIMhkWcPFOFIzuN7mmSLPtngA2wCqTx+2J/IShLth0EDROW6Lo7Ros= +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9i1NEZiWQbUWUf1I+2XxcOPyVXXChizlVctse2YyhkT7 +wgR07xfoapmx6eEnFHXXYmVJ5v0pwDT3iCzPtUJe5eY3RjX2ra7K+8jmS/1AkuXm +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0YxSyi5rjFDyLtdtV6fGTbqCUrVbW9jXMKIMhkWcPFOFBRAEpGizsECN2YlJX8ywlhVlKo6NdWc5iBwanJVStXM= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgRy3xtF/BIZqC2qdzL6cmg6LMRGLe6FQeczA1WOg2/oY +1u+XjG/2+GSQRv6EzCaWRQ== +FkDKDwNuIXbbD9xZMbZzXKACTQpPNB+EXUHoS4rgiCpdwfK+kMCs0TPIsFl2PUtc ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +L2O3BGGkFVpSEfRRE02F9q5srPOdWcGGM5brUKy/ynpX8Q1shh2zIsxyBAeb6vYTRFvQAsLy/pOfnHTRucKxz8PgOzhMH805I+D2oQJIJCzYmoCDVaXibcgTo6PV82Yr +wgR07xfoapmx6eEnFHXXYm7IOJ/6J1NThWNK2EdPRzYwWPvLP+jlqDp61mwE1mzG +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0VgYJcDXtyKb/JKLOY2KZO4UB+NPR50aKckf8eAL2HnqEcojt9ZBnrI4j1z2fJqPuQ== +1u+XjG/2+GSQRv6EzCaWRQ== +cALTOh7OQEr4o5fCc9/A03qUKf+Tn4nSnE7MBDCNym8sW+P3qPtD52WwMx6XflDoef7qvmEIEoplahR/D7P1jg== +Il5YcNzsAW1ppTTOdwCOzSr5dk99Ps4e1hQaV/md3oyuR0zkTwtNhfpKQ17x4GtqcBU9F80v1byDZSyaMuObzg== +jFWp7iGXcNFAo7+mU0KY9ahm9fm2OLt//Id5Opfj3a06nttS5zFGBG0bD/0dE2HzQEOVppFdSfzEePm7Ka355Q== +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9ie0DHX18L8LGfHVKI63Ke/pNWx8fOQwIScGdpToAWtI +wgR07xfoapmx6eEnFHXXYlEV6h2Qh0/E9zwcuGp+QctxMca/YFYUc4GaSAHUOjC/ +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Z4Oar5wqYQDTCRptdPgFxPSYxJ9SeZK+6AUohBzjAqKpGaY/Pc5FRIPzVpxGjpYMLzNG3i6E7y5GvOCz0EtKxmgQ9WBxh93nG76H/azI8fKZwYTplbkyl6TOnVDhwQ/jg== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYp8QPW/Rny7GxLHpvcjeLGxgJSlfxADrCxF/0Fw8Dr1L +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Z4Oar5wqYQDTCRptdPgFxPSYxJ9SeZK+6AUohBzjAqKpGaY/Pc5FRIPzVpxGjpYMLzNG3i6E7y5GvOCz0EtKxm+ShYMtCfjIyflJQ5tzWuWXQox0G6KPLXWwOeoXn1KRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYhx8u27be6FTqpRMEKSdDxFFRQGrLDDOpz/lgdLZ14Au +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Z4Oar5wqYQDTCRptdPgFxPSYxJ9SeZK+6AUohBzjAqKpGaY/Pc5FRIPzVpxGjpYMLzNG3i6E7y5GvOCz0EtKxlQqvnXgrs5S+WY8mcBeEx9Rtwo0s+xbAWYW/hc65t15ZV/iLOS5mrgOAn4xWFVlMs= +1u+XjG/2+GSQRv6EzCaWRQ== +ewMPic4oY8KA6fNbX11t2et6gSY3TY22d+P69UwmEhMFCd+Kvsrq+ING6uFRo9XR +XFFJBAEuV3u3dxqzRUxYy6I54hjChMSzUkAPwfcU37M/WvG0nD8Ukv536FNzU6xe +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgRy3xtF/BIZqC2qdzL6cmg72RAKeTh1iY9zoq4fK2PdLVCZC4a1j1X0DgFlBQiM9xg== +1u+XjG/2+GSQRv6EzCaWRQ== +a6iZYbokeyFY3ARiw5T+6DQlFHi2Qvw66wFDifoWB24TnOelgbiQ9gGMHj2IrS3uiASYZej7fIYhwhI82Nhq1A== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +Ws6i1ZM/c9zrSAbxeLx1LWu3wUH7xCI4ahh5PhMYd67NejEZazvcf+jkP2YLfr6knfXxb1hvXg91B7lZ1H/XnwFZqnDt3Wcdh2Kw3C/0vWHG3yfR0y75HhiXSsYZkPqH +91gsC2HsVrvTm3cwuG3h9ku+SnEuh1gfPlaT0BYQ3ws= +s8oZlk3kc0FacCBDAb+rTqHvR/rpCiMtf9O5/oRpCMDKQKA1V+fg392NLu/6HnRe +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYrQR/7nBKlFU7UIR7Tp7Ask= +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/XRRyd9HDiBpo0pCt8PUySBg== +1u+XjG/2+GSQRv6EzCaWRQ== +P40wuHk7MslPmvVoiV0eZS9GHjGsA1BpCAEYF+DyKrg= +IZj4TYq2HlScfSdRkI+QP7ds2Lg0nLguFAxiA9uKmwWNy6Iyrh6ke395/+UQsoR+T3bjNrP1/7gKiifvP7wGuw== +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +Wk+6iLfu38DoetOmRN2a98ccri5id3E91jM5eSptqngq5wL3OhenY4CPUO5T8L0u6OwkphGnac6ebW9W/b+NfQ== +CUHO82G5MDpSEiqhNImyKyJX2KNfQKTBGoaamjo5u91mIsUT/9ESdK1Yvlj/kDEPdW5TWq4h64U4gwbyure4PQ== +1u+XjG/2+GSQRv6EzCaWRQ== +0Cj55jDWeNf+rIE9r3fPh69q5/xKR7d/fTlhH/eiNy2HOp9v+xDToENMafrI2uH+ +FsRTDT5nlYHpT6JC9UdH4XvVWOdC38vK3DRrBxV85ZmavtyCegaO9LHz5BgOeHOtGNMY3uKNB5/c2qM203CTHA== +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpKm/Hz2+WKHac+36/qoVAtg== +1u+XjG/2+GSQRv6EzCaWRQ== +Nm/97dX7QPdHIqApKEes3MdGfvBCDkBlgzrReFmLJtM= +1ZFe+UUdQmu/QujLaciJakTompq9jdeKbyO+InnG7jf+rN+ro8NqSu4sq3o0fjhL/Xl1OWOxH32TuBUBpXhGOb0l+MfmSWcAi8hOuIFR/5I= +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkvCs01Ef75uP/SBKZUAi7QcnZn8b7/Sy8ao1iKp+B8eNa2x9mngQD5DySa3JctmO+E= +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +6K6zXmJJNC0xAXfJ6TCmfaBFl7DuG7+1nNvbLJkeawSnTK85ofp4jusSyh9wK19J +CUHO82G5MDpSEiqhNImyK3tJdLE8+4hhF+l+X4RDWwZAafVU8dGDkz+ecUDZLlFH +1u+XjG/2+GSQRv6EzCaWRQ== +evtbGaKLtemdDHjWgKV7NPj/mVfU/GjMxho1uU6G59IXPsNE8ecIelvjLKue6j3hS2NNnUxYMJuf58RqfRfnhQ== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +ZKXiNVFMygAA5wm63MFYL6obkTLenBSf5k8VJAPp5ngGs0PMZUcSj6GjMkDoXKcetkZtYlbP9iN4n8/XkbLp5hSnhSLePQghMUqrvv8+QM4UQzU/8e8Y6TcovrPiv7Nk +2yQsUep5WrV3T3htNZ1L8TIohok8dxSGY22+WeWcmQzlauL17DyWRvW+qqAHUI9z +1u+XjG/2+GSQRv6EzCaWRQ== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +kE0OGUDieDpLXO/pX4APBArHp97I07iQR92UhHwLo6k= +xWoGNWjKGPfI4gq8aHoTfOHCHk0sElv9s8frQ1Es3TZlMYL9ooet3sY2uLB3HVTi3e57gINhClfifZldkEGkgg== +xWoGNWjKGPfI4gq8aHoTfKj7V6/GbVtiII/Elkzy/SIxsAN6Em7FysJa4jADHlGMg/lVpPwNP8IEbwy04HBldA== +xWoGNWjKGPfI4gq8aHoTfPfBJKXQ9w97Eln03DT906wnKaz6rRkQSCBVFLl+HC23ufNrzhC5bijcSs87ACWLNg== +xWoGNWjKGPfI4gq8aHoTfI65fOfKuI1D/27XO5rrJkxarQ3VgRs1nfQOOlihDLiD+LO2IfhZysqSiHq1mjfExfKeT5oNAUBXJTvvHENcYk770mveqykgpfgJ9/IEcV3cKm7aaYU1d9T5J29bDv573OBqG10PZi8y457Tnusw4Va2gexlwGVlDGG5TengHzQH +xWoGNWjKGPfI4gq8aHoTfBJP/QcKNqdYzuq5movMnogxB5R2dU5gt0Zk7St83JWg8FD4u9UEVIw/00jn1jMKcA== +xWoGNWjKGPfI4gq8aHoTfIn6PnMrB/8qeqFwmI91ZmH0gsNqllNJ0dN1JhivPS8/w0GsFtLkbsrsy19h/7c+0g== +xWoGNWjKGPfI4gq8aHoTfOoPpV8RnCEp0qDDG75IWv/DIdiq3sMANSD/7jkYq96F/ZclnJTAxRILux6aALMcJg== +jIYtewpcL6elNlf4btqXRw== +1u+XjG/2+GSQRv6EzCaWRQ== +3sfJhDjqeEN3VkPRXnOTXHQyM5tO/8e8FVlTeqKqv46WsUzAvHp6vNSziJaI721C +A69EZujp4WFkVkJizqN6C/fqBP4sRbQVabCFtjuIU2ZOJ2TWCbEt4t9BMwacHaGI +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0QA9S0cmE5ZptD/tu+M6uzCl/jog6QV0tj6yWgEiZQ4q6uuSDo3ayDOPPYnkUgMtX16FWlrLW+uZ5n5D5c/0/Tc= +1u+XjG/2+GSQRv6EzCaWRQ== +OIAm83eO9UgPVVx31CBN5xrFh1fu4lJbxv0hxvFbJ9/Lzwlg3G1oy4PAivfxK5DN +1u+XjG/2+GSQRv6EzCaWRQ== +ptbr9LWGGT7tn7j3hgPjfbrssREvkhB1Av6GQHuY5C8= +91gsC2HsVrvTm3cwuG3h9ku+SnEuh1gfPlaT0BYQ3ws= +s8oZlk3kc0FacCBDAb+rTqHvR/rpCiMtf9O5/oRpCMDKQKA1V+fg392NLu/6HnRe +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYrQR/7nBKlFU7UIR7Tp7Ask= +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/XRRyd9HDiBpo0pCt8PUySBg== +1u+XjG/2+GSQRv6EzCaWRQ== +P40wuHk7MslPmvVoiV0eZS9GHjGsA1BpCAEYF+DyKrg= +IZj4TYq2HlScfSdRkI+QP7ds2Lg0nLguFAxiA9uKmwWNy6Iyrh6ke395/+UQsoR+T3bjNrP1/7gKiifvP7wGuw== +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +1u+XjG/2+GSQRv6EzCaWRQ== +YnT9M79JgM+dY7nG7ivAFRDN5w3P6LhHZG0naG4H2pMqSOnLuEF3TrnObbybq/T3 +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +Wk+6iLfu38DoetOmRN2a98ccri5id3E91jM5eSptqngq5wL3OhenY4CPUO5T8L0u6OwkphGnac6ebW9W/b+NfQ== +CUHO82G5MDpSEiqhNImyKyJX2KNfQKTBGoaamjo5u91mIsUT/9ESdK1Yvlj/kDEPdW5TWq4h64U4gwbyure4PQ== +1u+XjG/2+GSQRv6EzCaWRQ== +0Cj55jDWeNf+rIE9r3fPhwm4Yk27M5YlBXxNpkGWTJ+votWQvP1T5Ojr7y0INGHkHfvpZbOucM52tKCtp/K5SQ== +FsRTDT5nlYHpT6JC9UdH4XvVWOdC38vK3DRrBxV85ZmavtyCegaO9LHz5BgOeHOtGNMY3uKNB5/c2qM203CTHA== +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpKm/Hz2+WKHac+36/qoVAtg== +1u+XjG/2+GSQRv6EzCaWRQ== +1ZFe+UUdQmu/QujLaciJajx26a+2STfwMi+zieErgi3CBgaNFGZigZi8pTmSdXTQmGqTiUVKLPzrQkD8KSrdFg== +FLMbt5iAjw12yzW85KcCsnQmJLJhCftAGa03uwPfXWNUQ2Jfp4woEQ+JLN+7bg9Oy3HrWj/QgzOMHfF3mMIFtg== +YROiDf0HfA7clzlpwWrPnWCbzQCFmxCtQkXbusOY3gjdQhuS38kc2bwUPY2GagZ/m/n1Cy54BuGtIYCO0lz2+Xnwiapa99iOCszKltCMtTw= +1u+XjG/2+GSQRv6EzCaWRQ== +Wk+6iLfu38DoetOmRN2a93iqiWLghhd+n8qz7n4B9D0= +1ZFe+UUdQmu/QujLaciJakTompq9jdeKbyO+InnG7jdyCf15aEY6LOsV+bdTpipbwl3ZH5jxRlMPO8+qalE9xw== +1u+XjG/2+GSQRv6EzCaWRQ== +8i88MKADknh6ve+bU8Ezb+WJ6niZtJuVbQw4qpRaLig/H0N9vJfeujRYJbh3YTtH +84Vymj90Wzn5yYuvz1pUuDvv2eJTuL0TJq9mU3xLbg30wcVaLL+kSX0M9FeEUdaO +VmVrGQo2zRokW/ZuO9bN67C2ez7Z7xiKZG19sVmgmMg/szOSu0TUxszZWDrXwid9LxFkwHgd1UBJa6AfIpQ+qA== +1u+XjG/2+GSQRv6EzCaWRQ== +84Vymj90Wzn5yYuvz1pUuCWRuZGkhlJnLSClaFLOOGb6poAqIb2TXQcYiRPjpWOS +VmVrGQo2zRokW/ZuO9bN675vxEk+ZDMj1IxUahsCYOb8sGgoVXMqknYjdiwdUQrN1uOQv6+nqJfxxhc+fmvEydbSMV0wyDhXbD2QcmxYuFbeQv6ltaFTzgkV/Gjsana4Z6uPBK3w93KR+W5cIfoStg== +1u+XjG/2+GSQRv6EzCaWRQ== +84Vymj90Wzn5yYuvz1pUuDeimrBZjjT24kCGUmMMVrxvZ2a3W/syVkEg92NrBZWw +VmVrGQo2zRokW/ZuO9bN6y8aQcwZ/hSna7uqNWY2EKiaIYyFt7YecCLXxJctzYQmq2zUc1xRjRe2sZt4Yq8Ovw== +1u+XjG/2+GSQRv6EzCaWRQ== +FLMbt5iAjw12yzW85KcCsmy0dtqL77758KtFtx4Wv3amJjJGHf130+oBwGsdvcbN+qQC09Ap2i1s7iy0TrXHib90sT1B/mL+D7Tlnm4/Kho= +1u+XjG/2+GSQRv6EzCaWRQ== +7bJ3Uk7aqJsghutxBeStwitQyrNq3vvsQtPQ7nQ8Roo= +I5csN8e3J/KIFkMa5t+SJKcvYE4NNVj3kiL8YMvvrz8H6inVAPWunV1GucokSU+W +UCjjP9uamQhFjq6n90cX334w5N6kM7POhL+ZChb6xIMkwXdXYjEQ5NlTf7yiiE9gvWTb6WOc+v/DaO8XKvCo0DfwvXhwZmBLMdEWuY0QChlXwvqsJ3uRRSP2BgNVeIh3y8nv99zO6pX6H+vKgdKxbPBCRVDJV3qv+rKTCbhMJSTb3EZvFShchBKabWneBbufQFurmxe8euQhTZnVG+1OKutH0YhtkgjuKkdMoUYOnHo= +1u+XjG/2+GSQRv6EzCaWRQ== +zBfpEDEHUpKcgzvJDq0C4YC6UMbAUfqAJ0xI0cKnOaESr1M8ldgYE+7A7EPPEZ5g +84Vymj90Wzn5yYuvz1pUuBf9UiMoSzUCfQBk2T/lZO1ZenPtkuAwJZ/DjYGk7EF3U1UkgN8sUTg2e8HZLYpjgw== +VmVrGQo2zRokW/ZuO9bN61K4MXaMsg5/Kxe0Z+g/3qNgb6okRPQ5AYXwGskH9ejOnwi4YAc+D7PnIV1ewTrjYg== +VmVrGQo2zRokW/ZuO9bN6wZri2Z3CBt2YpUf7zFZ0+KNguNuVrtBMhHtFgJ7WYYf3JHfXVX1T0GtSc/XTfHPmg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wXyEF6XhF/PmYb/RCvDnIg= +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvNCObquF8KcqaCn9joiTHShp4vhVSeHci2o+TX7lXV5bg== +VmVrGQo2zRokW/ZuO9bN684ehqpwxtlh5PoulYJQlzwAsh2I93oB3djuYOuh9qQN7UA7ptBZ9kbWq4QL87fNrg== +VmVrGQo2zRokW/ZuO9bN632U9Ut74PgKeN/Vd++qe96QUfvqhKcAMSKVyTsZ5tjiis5cEaQeER/nY9riZ04zHw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64IOlCXEAuw7+JQuoGU1tA7y+jY6s/JvEWCe2pdd+pjEK7nJUBmqZJQTwm3h72ag5FdSSEPIKbcMVZEYOsof8a+Ny0UHjA4vHFUkNX8Y9NDS +VmVrGQo2zRokW/ZuO9bN6+0ruBcpcWh1tg//57328LXWh98WBeFYX/reLiggO4A8 +VmVrGQo2zRokW/ZuO9bN65YLSeu6SlsROZcKj6GOXcg= +1u+XjG/2+GSQRv6EzCaWRQ== +P1bBdERejTBi3ynjwlj7j9BN16EKwuXwZL0J6G3zRwc= +I5csN8e3J/KIFkMa5t+SJP5ieXqyhKdpsUu8T3AoWE7POsp2/AbDJJ+EacQGfSh3 +FsRTDT5nlYHpT6JC9UdH4bKLDUGeT2OmWeP4uXVXctcW9UIqr4TBGbKKt1WvG/efazn8+MYwktdhj56RVE/jzB6VLeItbFDX6J9e2phdbn4= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +4Ky5RNfad94RDxFAfm3/hD1S6TvHVGkU5RCZUxk7mPUS55Jd4Ae2dEHUhzTAxZvn +FsRTDT5nlYHpT6JC9UdH4XY/Mijkqlb0CXZ+gLX4s8Lo/U2S2nexcGC+4OIyPFh5WgAPq9Gi1jmpFN9HHqK8ckgWpJQHRdLlpUv0ZQ66iK+mMCKlcQxGqoBtWZH9V+uyb9EdarEJRD9Gwzv3czT0CHCq3JfIYwJXLBpxNcw57pU= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +KZsPH0cmEM2QRSeJCcSyucww7vjWuHBitH1P49Sqh2Kwbdrie0k2+bXVIdFrP4Hp +iEbw/85IA9dHrKUZiqzvJYFw7QmcLtkqxGPIfRytlW97cg8SvyFzcKnhDF/yOCIqBpgg2zvuF6H5ZBpWx9EJEA== +1u+XjG/2+GSQRv6EzCaWRQ== +Oq4JxO2byG1NpOr3BWRu3IQ7sxcnLbXUljntZmFfE3nh6wlJfMDKKO9ubZppLWjY +Oq4JxO2byG1NpOr3BWRu3Oj+k38Jm2FrVzxggusCskbYvEUoOq/NQoGJhFujv6sPpGYXVLDzIbfY26IswYzgjA== +1u+XjG/2+GSQRv6EzCaWRQ== +/F49ykgjedWbzpXYCpMOgrI9YLZA9ExR/eC6+PgN+EBaWlpc2Mqf7k4KIlVymqOX +Oq4JxO2byG1NpOr3BWRu3DAGQWIxEWJFwo5ywit0Hm8vjztDN9DFBJzSbRYvk9E8aVOOFQPA0eKwd04IoEKP0Q== +1u+XjG/2+GSQRv6EzCaWRQ== +Wk+6iLfu38DoetOmRN2a9+VXMx6SjWK1apPtY6dMfYMoHCpgsiaPyMgPmEtMZnTp +CUHO82G5MDpSEiqhNImyKyJX2KNfQKTBGoaamjo5u93IOV5gsvlF+rJFV1zpx+Xg4cy6nKs4KOvEdUG+jqVRMcHps5klF6H7/4DUJCho/6Q= +1u+XjG/2+GSQRv6EzCaWRQ== +tm8SW4k/FXA1T5YrQDZ7wWnoVqAkjsZ5VE6E38yZjMw= +Oq4JxO2byG1NpOr3BWRu3MthcyzqNuNs+1GYOnU5w+jsgU7gzHR2JfE2rBUCpEkb +1u+XjG/2+GSQRv6EzCaWRQ== +kniDpSFE0vLELL83X2MGdatB7FelKh/g6Qxw8LyyT8HGxMvOPbla6t91vkFW97vk +Oq4JxO2byG1NpOr3BWRu3EQ6fq4nu+wy0ERSqtq/yw8M0VQBkl4VxvsO8pLJ0tXjHk57blGJcDTSf5IZnT6A33vklf/1JB4f15dsurs4TIQGV33FHDGQiycTjLI89DIU +VmVrGQo2zRokW/ZuO9bN68PLvFAyPYCFptnUBkrwhM+fcDkoqf6krf9e3/VUGhLb +VmVrGQo2zRokW/ZuO9bN6wj+eM5w23B0fsVf/h/HrFSTzO98rc5IeoE6011BQZVK +VmVrGQo2zRokW/ZuO9bN69QoqAP8+J9sRJqfCnnzeuAXZF+1S2UZnPaDpalUlo3b +VmVrGQo2zRokW/ZuO9bN68ULkZoiR2t+tvBrm+xgZHsLbfA78IfbZM7Dajj7ThPX +VmVrGQo2zRokW/ZuO9bN6yLf0ryvhEUQNBfas0QHrzpoCyWrDfpVrSO9zlFCkvcL +VmVrGQo2zRokW/ZuO9bN649yP7OWYGC7nlAREd+ogQMAhw3vr7ETL50mj9Txb3p4QXNDSpgyxEDuOxx/H+bCgw== +VmVrGQo2zRokW/ZuO9bN6yXvSimWjBkSPa0oACTE6ZLwBjvix6RVUl1pckZX2MOfzsYgQ/oO/5Wg0pi5sa44/Q== +VmVrGQo2zRokW/ZuO9bN68Th/U76hkAmXqrzmdE/ZI2hWSlM1jO4tNZ9jOuBXyUu4UXCtxGmpfpurmSBZ7nMz3CfJpY98NDnzDpftzNKbQ0= +VmVrGQo2zRokW/ZuO9bN60dcEpCcNfgwHuX11nm6/kpaOKlAgQkhD4jWIN9kUu++6vBRzZCJihAhZezy7MiEDg== +VmVrGQo2zRokW/ZuO9bN68woQ38PWuIkn2KGEnMhf18vsoH73KIsVQPZZeg0H6i/uUTwlcqjqaOOcGE2yARxSA== +VmVrGQo2zRokW/ZuO9bN6xc3KvQQ3gxQg4pqItQtHXv4FkR9WdU3ksM799ggpOqo2DDqalu/U1q8We9X/tjBew== +VmVrGQo2zRokW/ZuO9bN6zKmc9qq/eDXqFNWaoFb0zhPVy2ABZhvEveRj7QcX4YCewTr6ys+LhFoqIchJ6zMaMOLXJXIH3Brcdeb5Ue6260= +VmVrGQo2zRokW/ZuO9bN64l1qBkNbm6bRJujDvKFqz8cikEfZdrnEcpounC4jt3+LsHKisT4qqKZ9+YZq//PUHsLDe4jo0qqLnSPkq2P1BU= +VmVrGQo2zRokW/ZuO9bN6+PaCbxoEB1NEX+VMo3rreipvAaQyLbViNAr3J6XiR8BJigxp9Grj+VE6re2Su5SXw== +krWGxagzYm1msYy2LL9fQw== +1u+XjG/2+GSQRv6EzCaWRQ== +kniDpSFE0vLELL83X2MGdQI2Yuhuvo0Ui21i4BCi/nOhdFEM8veprF56BF8YOMRn +xWoGNWjKGPfI4gq8aHoTfNm7h2/3rk5d26JyRNKl45IM1E5n5Mf0G579JPEy0AiTBQKCeUZgjg8y5TnHK2UMyh8aGD16tXjy7by0AqnrLgJYAe3ICK+4mv1iRt4grae/ +VmVrGQo2zRokW/ZuO9bN622mJBXMeZSsngb+R2rsXcQ/aLBiSrwL1n1nxMo0NOr+ +krWGxagzYm1msYy2LL9fQw== +1u+XjG/2+GSQRv6EzCaWRQ== +CU51y3DKNJot7qy7c7KKwO8WYvpMDLkO/fSRGZVdWOribLR0Z36bszb9TfMNFtaC +FsRTDT5nlYHpT6JC9UdH4XY/Mijkqlb0CXZ+gLX4s8Lo7IDK7Ba57kevbJOEF5g5iN9/CDdZUu2p5umGxZgi8mGXi2A6aBVsitBddxv3fWSEKNYQ+BpNe6ioS+VoiF7ZIdU5qPOWwJ3oLZqIke8l1aP0euqLUVfelvnlr4cIIFIj5ihOF/r3Za1FMkLpsZeZTi+bY+yHHE5B+Oyn2aPT4A== +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +CU51y3DKNJot7qy7c7KKwEfIM1RpEMC8MSDuKuFpo9c0WuShpb1Gljya7chVcXrt +FsRTDT5nlYHpT6JC9UdH4XY/Mijkqlb0CXZ+gLX4s8J+IQGDwbrhMIxooL0HHyeMwJROaopw2r49Np4gDG/ZPg== +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +OpwSuFTBAL7XPZ3GOEDUSXmEvKt60PMcxH4xv5faETw= +Oq4JxO2byG1NpOr3BWRu3HsPBkHg7cg6rT4zB1qEB0iT4ntPsedRnYHV5l2bFN/rGoeO8y0fYbDnXhc++lmpPqD/7rT/EUbMaZkgXSxKZhc= +1u+XjG/2+GSQRv6EzCaWRQ== +OpwSuFTBAL7XPZ3GOEDUSYYkSmNb531LWXBThqJnA2Q= +Oq4JxO2byG1NpOr3BWRu3HsPBkHg7cg6rT4zB1qEB0gxNz22V2Dc9j8EyqMVksnN +1u+XjG/2+GSQRv6EzCaWRQ== +8i88MKADknh6ve+bU8Ezb/5J/KOGrNrp/hd+fOHffgs= +Oq4JxO2byG1NpOr3BWRu3Oj+k38Jm2FrVzxggusCskZ4mzSuMzk+00wLYsSzYvJQZjz2fhF1T8o51Q/mKcz39g== +1u+XjG/2+GSQRv6EzCaWRQ== +kniDpSFE0vLELL83X2MGdV6+T1+oCOldFzv131aPdi4= +Oq4JxO2byG1NpOr3BWRu3MthcyzqNuNs+1GYOnU5w+j+CaEwIuq9rJHkO8fHjOtidD5V4PbuwUZmtFwGxxAulOrqkCwdTQ9F4tN/9xCEPFk= +1u+XjG/2+GSQRv6EzCaWRQ== +0Cj55jDWeNf+rIE9r3fPh8pA+0jaLcKE0K2LySjNBG5CU9NDINKpk6NXnWZNXYHv +84Vymj90Wzn5yYuvz1pUuOO0+fywpFJdc39xXTBu9YirwjBLohUzBWtPEoQZ/0wTdvMyNkBBMME3Rb8uomVU2c1LaHGQYxq0DYsuUSVKELM= +VmVrGQo2zRokW/ZuO9bN69JCeXP8VKIhEnUaQVSzY2BypuWREM5sjaruxBFKD2EvkbTZZ0Pw6RDYZMEKcXxRO3BkbFDyeCCX4BTkwrqvx9cQXOTLphOa12SDXvFCXlDo +1u+XjG/2+GSQRv6EzCaWRQ== +Hl/tmaE5nbbDLFZ4/b5RqLZNnSDcxCnQkebcIFUyCThHdTJZ1+0H2i0w2oG8wc2kUhrQlZ5vz/e+PVt8T4Avsg== +Oq4JxO2byG1NpOr3BWRu3MthcyzqNuNs+1GYOnU5w+iR55CovI+mLkNUdXQ9Cjms +1u+XjG/2+GSQRv6EzCaWRQ== +4yfrVt9mcL2kjZOi6OFl+/gCpFE8/3I3bb9RGL9+SD5GqtpUVKxQmo2nLkA+/LSc +xWoGNWjKGPfI4gq8aHoTfPWfjS6vSpTufw6V8YWjiHIbW78881nysbHK/4+YnV4b +1u+XjG/2+GSQRv6EzCaWRQ== +nXHLJs+jc9UGwRbqbBpdlnoY9wZYkQQX09YG8sUW0k9w37YahVa+gJNOrD7ggdpa +6K6zXmJJNC0xAXfJ6TCmfcRxgBy4y4bgsEJ6wp9YKM0ivN098EI8rMPmitQhDg0Y +1V2v8QerKOmubvSxgB4eTNOWXfUINjsWZ02BDEUwQGr1EDSYeDhTrvRMzg0k85YK+Tdos98WcA+iIgGaBKpiKT9+UZijWczfGNGyP/pcPfc= +VmVrGQo2zRokW/ZuO9bN6xOcE74GXukFgoP7mn89osg8SsOUQXFdzvPn+qkat/4/ +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +6K6zXmJJNC0xAXfJ6TCmfaVK7fRvRq1+Vnadm3/saDE= +J1UGlXkKhKXD36OrXXhN8NQKeNxEQEtSrCTi40QJlhZc8+TaSALhLYCLySZ2J9Ro +VmVrGQo2zRokW/ZuO9bN646OXWelyIYU2xRPUxRlGkJcIaIJLy1ed19RDL6uKh8/ +VmVrGQo2zRokW/ZuO9bN63w47pW0ZChbYuF1zj78dlkjTE1Ux1w+sowcZh1RTEJIJotfy7tFcWOcc+h/FsMt6JMpiz5HQflNCEI8Eq16698= +1u+XjG/2+GSQRv6EzCaWRQ== +YY5wEo94l9qfo5TPxQmUgoNFl2Fp8+FsNDKaanjjEPI= +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +6K6zXmJJNC0xAXfJ6TCmfaBFl7DuG7+1nNvbLJkeawSnTK85ofp4jusSyh9wK19J +CUHO82G5MDpSEiqhNImyK3tJdLE8+4hhF+l+X4RDWwZAafVU8dGDkz+ecUDZLlFH +1u+XjG/2+GSQRv6EzCaWRQ== +HTdnfikDSTQ3eoPgrOd1XSIVSJ0FqgT0ujqJT4TT0N0= +p/2Atf50M8NSYtiojzMPSPDDLBr62CZS2VKFEpxbOYlh2aRzggeKZtOgJa4QNRRjDmlZSoUEDWHTnLBsPR5L1g== +khWCUyuDMwE3p+YqYO+JihDH6VWAOeZ4oABpau/+19tmJy+J3rvX4fpN/8te7LPxB1H3pwVBig7ITL7uPGmgS1trp4e84U+WKBrLkYlgk+9ksLUpnsMXlluLn47Pq+RSKPiYH3r5Z+ysz5QznyNytQ== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgULgM9yjXNLL1pbqni6GCYV7COvUA6EuHsC6DPEvWiBT +1u+XjG/2+GSQRv6EzCaWRQ== +ImN2fuKsoGGvkOmi8kzyJurqrN+AxWpO5c/NRxwjHm3eBS+7qYgiJIt4WV5eHPYs96HzZh1O4ZAmtTRU1UWauA== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +LwzzX0OXdM0O35JPf5nR+zpuH+Oz32pLKy+0Xt6eMbm0Y5ZLRkVsXFZlYPOoDLADcVXsKSLYUG0oej+lTH/6n2bE+KW3uYrALghxrY4nD6JiEM+VpsPSaFVP665QVb9u +wgR07xfoapmx6eEnFHXXYm7IOJ/6J1NThWNK2EdPRzYwWPvLP+jlqDp61mwE1mzG +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0VgYJcDXtyKb/JKLOY2KZO4UB+NPR50aKckf8eAL2HnqEcojt9ZBnrI4j1z2fJqPuQ== +1u+XjG/2+GSQRv6EzCaWRQ== +cALTOh7OQEr4o5fCc9/A03qUKf+Tn4nSnE7MBDCNym8sW+P3qPtD52WwMx6XflDoef7qvmEIEoplahR/D7P1jg== +Il5YcNzsAW1ppTTOdwCOzSr5dk99Ps4e1hQaV/md3owjglrqco9N56a1CqAlb1gpQZpEginNw3bJlFt+dUpTdg== +jFWp7iGXcNFAo7+mU0KY9db0cG5f3MFhw9ssDGknJ+lQMNh1E1cVt/CHqWM/XVAFpscGO9Y+3QE7bod6bSZD2Q== +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9ie0DHX18L8LGfHVKI63Ke/pNWx8fOQwIScGdpToAWtI +wgR07xfoapmx6eEnFHXXYlEV6h2Qh0/E9zwcuGp+QctxMca/YFYUc4GaSAHUOjC/ +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Z4Oar5wqYQDTCRptdPgFxPSYxJ9SeZK+6AUohBzjAqKVFZzCleRVbF2iRpitP5hQgQDYykjqMPTrGdNOh5USUgVOc60cmy5X8OkSRPq2LH7uKtidfwECLQ8kdPEcFfoIUj9s6fZ546ljWRMYy7X1Cc= +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYp8QPW/Rny7GxLHpvcjeLGxgJSlfxADrCxF/0Fw8Dr1L +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Z4Oar5wqYQDTCRptdPgFxPSYxJ9SeZK+6AUohBzjAqKVFZzCleRVbF2iRpitP5hQgQDYykjqMPTrGdNOh5USUihJ7M+2HTHaNfcLtMv1MUxT4d54PujWuc7tNecdnY3A+t68VMhYvkK7T9sEEKUv60= +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYhx8u27be6FTqpRMEKSdDxFFRQGrLDDOpz/lgdLZ14Au +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Z4Oar5wqYQDTCRptdPgFxPSYxJ9SeZK+6AUohBzjAqKVFZzCleRVbF2iRpitP5hQgQDYykjqMPTrGdNOh5USUgVOc60cmy5X8OkSRPq2LH7g5LjOgHDdJQF3u3PU0J3OO4wjU8AHSNNeufnzOhIS/M= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgRy3xtF/BIZqC2qdzL6cmg72RAKeTh1iY9zoq4fK2PdLVCZC4a1j1X0DgFlBQiM9xg== +1u+XjG/2+GSQRv6EzCaWRQ== +6XgPd5VIjYSXPl1j1b9rwkngQLI6fjrUdgeS2Mo5IRMUECnehgLEabBcaYc6RpEJiVJ/Alx91bDLqTwMAq/6drU1zGNaE9D9DHePb11qcsw= ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +Ws6i1ZM/c9zrSAbxeLx1LfJruhTdKESdPJU7A5Fw9/6rLNuF8MiZFkrRY4q1bxW00FETxRds2YLATonbAPmJ10qFQPtJm7UyXPmuZtqu5D9H7EXWmVryyQnef8CYOZ5Fdv0Ygg+pX9uuCBY5taEYaA== +91gsC2HsVrvTm3cwuG3h9ku+SnEuh1gfPlaT0BYQ3ws= +s8oZlk3kc0FacCBDAb+rTqHvR/rpCiMtf9O5/oRpCMDKQKA1V+fg392NLu/6HnRe +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYrQR/7nBKlFU7UIR7Tp7Ask= +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/XRRyd9HDiBpo0pCt8PUySBg== +1u+XjG/2+GSQRv6EzCaWRQ== +P40wuHk7MslPmvVoiV0eZS9GHjGsA1BpCAEYF+DyKrg= +IZj4TYq2HlScfSdRkI+QP7ds2Lg0nLguFAxiA9uKmwWNy6Iyrh6ke395/+UQsoR+T3bjNrP1/7gKiifvP7wGuw== +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +Wk+6iLfu38DoetOmRN2a98ccri5id3E91jM5eSptqngq5wL3OhenY4CPUO5T8L0u6OwkphGnac6ebW9W/b+NfQ== +CUHO82G5MDpSEiqhNImyKyJX2KNfQKTBGoaamjo5u91mIsUT/9ESdK1Yvlj/kDEPdW5TWq4h64U4gwbyure4PQ== +1u+XjG/2+GSQRv6EzCaWRQ== +0Cj55jDWeNf+rIE9r3fPh69q5/xKR7d/fTlhH/eiNy2HOp9v+xDToENMafrI2uH+ +FsRTDT5nlYHpT6JC9UdH4XrDOnfzNeguIPWOtTAjsddxGDd9fUcR8/xS//eaWJr4NParBtsOHs0TnJGQL6RQHX0pdc/CP7V/Z+HvKowX1qI= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpKm/Hz2+WKHac+36/qoVAtg== +1u+XjG/2+GSQRv6EzCaWRQ== +vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo= +VmVrGQo2zRokW/ZuO9bN63M3N/QI6jfN21aCOjn5qoIH65oh3lPdyA0LInTMBTnC +VmVrGQo2zRokW/ZuO9bN6y1nBG3iYKMCH+hzUkF1M8ZnhEqE9tZS72zfT2evK1dQ2LMvOK1lgrtQuS5Vtw6hT5MeP0utiFLoDnrrf+qjvwU= +VmVrGQo2zRokW/ZuO9bN676cRzFmy9/zo55zwgaMh+JG53w7pjIKf/vbD16cejyz49FB2vYDXVhkdLeF4Rdnrw== +M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25cy4PX077/sCXB8dqPHOKWHwGfhj+HPrIcuJ8jQUUnUNsgOaiEdOQeCfdlLOd3khzQkvQkPfY59MGCnqpJ+W4jmx1K9o6ogXNBZ7YBZsaluozw== +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkvCs01Ef75uP/SBKZUAi7QcdQu4K6cOeOFIx/oZ5wHn8A== +VmVrGQo2zRokW/ZuO9bN63QpQYSLmPtP/Hy48PObUTAIbyGMAEXMcC/u59GCOJz2SDcCydB78ik1XpbXw9Vz0Q== +krWGxagzYm1msYy2LL9fQw== +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +6K6zXmJJNC0xAXfJ6TCmfaBFl7DuG7+1nNvbLJkeawSnTK85ofp4jusSyh9wK19J +CUHO82G5MDpSEiqhNImyK3tJdLE8+4hhF+l+X4RDWwZAafVU8dGDkz+ecUDZLlFH +1u+XjG/2+GSQRv6EzCaWRQ== +hxOlvQqGzvBGNobqltO1S2DP9hqsRonfsYFJZcqe/d42lsMgi7PCiKBUwTriYSCM+Ol5iVFA0QvMhVJ85kmwsg== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +ZKXiNVFMygAA5wm63MFYLzANoH3WugY0nYaKERSEhzNXjmqU0LhVN+s6xZNku1ol2uRURizImcI6Q9qNqfY4Mry4RpzUtuYD2GTDJo86DjSmwlPpqhYKKGZL/m7DI37Lng5fq7lCIcNZovKBj6FBCQ== +2yQsUep5WrV3T3htNZ1L8TIohok8dxSGY22+WeWcmQzlauL17DyWRvW+qqAHUI9z +1u+XjG/2+GSQRv6EzCaWRQ== +ewMPic4oY8KA6fNbX11t2ac6pvQBdZ/brZ6zxY+WDEY= +kE0OGUDieDpLXO/pX4APBArHp97I07iQR92UhHwLo6k= +xWoGNWjKGPfI4gq8aHoTfOHCHk0sElv9s8frQ1Es3TZlMYL9ooet3sY2uLB3HVTi3e57gINhClfifZldkEGkgg== +xWoGNWjKGPfI4gq8aHoTfKj7V6/GbVtiII/Elkzy/SIxsAN6Em7FysJa4jADHlGMg/lVpPwNP8IEbwy04HBldA== +xWoGNWjKGPfI4gq8aHoTfPfBJKXQ9w97Eln03DT906wnKaz6rRkQSCBVFLl+HC23ufNrzhC5bijcSs87ACWLNg== +xWoGNWjKGPfI4gq8aHoTfI65fOfKuI1D/27XO5rrJkxYDYN5AaD/U8ieE2lndMCRssuSq+OPqH5+o3oPnZhR2fDSmXENHaMrM/plpW2LB2tYrPOpDeIQ/1hajVeQlo8yggq+xVJXt+IYapVi/hVh3yV/lFwctoQT7KaBsjEaTru6xmXSpD+4XvjxxDywCjgs +xWoGNWjKGPfI4gq8aHoTfBJP/QcKNqdYzuq5movMnogxB5R2dU5gt0Zk7St83JWg8FD4u9UEVIw/00jn1jMKcA== +xWoGNWjKGPfI4gq8aHoTfIn6PnMrB/8qeqFwmI91ZmH0gsNqllNJ0dN1JhivPS8/w0GsFtLkbsrsy19h/7c+0g== +xWoGNWjKGPfI4gq8aHoTfOoPpV8RnCEp0qDDG75IWv/DIdiq3sMANSD/7jkYq96F/ZclnJTAxRILux6aALMcJg== +jIYtewpcL6elNlf4btqXRw== +1u+XjG/2+GSQRv6EzCaWRQ== +3sfJhDjqeEN3VkPRXnOTXHQyM5tO/8e8FVlTeqKqv46WsUzAvHp6vNSziJaI721C +A69EZujp4WFkVkJizqN6C/fqBP4sRbQVabCFtjuIU2ZOJ2TWCbEt4t9BMwacHaGI +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0QA9S0cmE5ZptD/tu+M6uzCl/jog6QV0tj6yWgEiZQ4q6uuSDo3ayDOPPYnkUgMtX16FWlrLW+uZ5n5D5c/0/Tc= +1u+XjG/2+GSQRv6EzCaWRQ== +OIAm83eO9UgPVVx31CBN5xrFh1fu4lJbxv0hxvFbJ9/Lzwlg3G1oy4PAivfxK5DN +1u+XjG/2+GSQRv6EzCaWRQ== +ptbr9LWGGT7tn7j3hgPjfbrssREvkhB1Av6GQHuY5C8= +91gsC2HsVrvTm3cwuG3h9ku+SnEuh1gfPlaT0BYQ3ws= +s8oZlk3kc0FacCBDAb+rTqHvR/rpCiMtf9O5/oRpCMDKQKA1V+fg392NLu/6HnRe +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYrQR/7nBKlFU7UIR7Tp7Ask= +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/XRRyd9HDiBpo0pCt8PUySBg== +1u+XjG/2+GSQRv6EzCaWRQ== +P40wuHk7MslPmvVoiV0eZS9GHjGsA1BpCAEYF+DyKrg= +IZj4TYq2HlScfSdRkI+QP7ds2Lg0nLguFAxiA9uKmwWNy6Iyrh6ke395/+UQsoR+T3bjNrP1/7gKiifvP7wGuw== +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +1u+XjG/2+GSQRv6EzCaWRQ== +YnT9M79JgM+dY7nG7ivAFRDN5w3P6LhHZG0naG4H2pMqSOnLuEF3TrnObbybq/T3 +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +Wk+6iLfu38DoetOmRN2a98ccri5id3E91jM5eSptqngq5wL3OhenY4CPUO5T8L0u6OwkphGnac6ebW9W/b+NfQ== +CUHO82G5MDpSEiqhNImyKyJX2KNfQKTBGoaamjo5u91mIsUT/9ESdK1Yvlj/kDEPdW5TWq4h64U4gwbyure4PQ== +1u+XjG/2+GSQRv6EzCaWRQ== +0Cj55jDWeNf+rIE9r3fPh8hYVyhGzrLc1d5gspB1nJ6MkdD973lU5E6ZY+/i87tn +FsRTDT5nlYHpT6JC9UdH4XrDOnfzNeguIPWOtTAjsddxGDd9fUcR8/xS//eaWJr4NParBtsOHs0TnJGQL6RQHX0pdc/CP7V/Z+HvKowX1qI= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpKm/Hz2+WKHac+36/qoVAtg== +1u+XjG/2+GSQRv6EzCaWRQ== +1ZFe+UUdQmu/QujLaciJajx26a+2STfwMi+zieErgi3CBgaNFGZigZi8pTmSdXTQmGqTiUVKLPzrQkD8KSrdFg== +FLMbt5iAjw12yzW85KcCsnQmJLJhCftAGa03uwPfXWOyGjMGMSeZIIZ4JQD4SoKE8cwGXJJKnfKd3VO5NrxPjA== +YROiDf0HfA7clzlpwWrPnZTelRd7BRvkhZN4nFqogkcleIQbXGcCHAtpjDXpXs8esQomgYP0+wCvwRWlLSwEmw== +1u+XjG/2+GSQRv6EzCaWRQ== +Wk+6iLfu38DoetOmRN2a93iqiWLghhd+n8qz7n4B9D0= +1hs1FfIPISXmd7TJ/r+Vl48ped6CO4DUHuCgWusI3kaQo9WDqYldGt+iTzvFGu+sJOuc4kzIb/fNjSI7+VIovA== +VmVrGQo2zRokW/ZuO9bN6yrtj1Y8IOc0iyqbQeKQWnIEwvxCta/kYWjRV+ISgn1lBunAHsxOxxCiik7fS/QPJA== +1u+XjG/2+GSQRv6EzCaWRQ== +8i88MKADknh6ve+bU8Ezb+WJ6niZtJuVbQw4qpRaLig/H0N9vJfeujRYJbh3YTtH +84Vymj90Wzn5yYuvz1pUuCWRuZGkhlJnLSClaFLOOGb6poAqIb2TXQcYiRPjpWOS +VmVrGQo2zRokW/ZuO9bN675vxEk+ZDMj1IxUahsCYOb8sGgoVXMqknYjdiwdUQrN1uOQv6+nqJfxxhc+fmvEydbSMV0wyDhXbD2QcmxYuFbeQv6ltaFTzgkV/Gjsana4Z6uPBK3w93KR+W5cIfoStg== +1u+XjG/2+GSQRv6EzCaWRQ== +84Vymj90Wzn5yYuvz1pUuDeimrBZjjT24kCGUmMMVrxvZ2a3W/syVkEg92NrBZWw +VmVrGQo2zRokW/ZuO9bN6y8aQcwZ/hSna7uqNWY2EKiaIYyFt7YecCLXxJctzYQmq2zUc1xRjRe2sZt4Yq8Ovw== +1u+XjG/2+GSQRv6EzCaWRQ== +FLMbt5iAjw12yzW85KcCsmy0dtqL77758KtFtx4Wv3amJjJGHf130+oBwGsdvcbN+qQC09Ap2i1s7iy0TrXHib90sT1B/mL+D7Tlnm4/Kho= +1u+XjG/2+GSQRv6EzCaWRQ== +7bJ3Uk7aqJsghutxBeStwitQyrNq3vvsQtPQ7nQ8Roo= +I5csN8e3J/KIFkMa5t+SJKcvYE4NNVj3kiL8YMvvrz8H6inVAPWunV1GucokSU+W +UCjjP9uamQhFjq6n90cX334w5N6kM7POhL+ZChb6xIMkwXdXYjEQ5NlTf7yiiE9gvWTb6WOc+v/DaO8XKvCo0DfwvXhwZmBLMdEWuY0QChlXwvqsJ3uRRSP2BgNVeIh3y8nv99zO6pX6H+vKgdKxbNRuD8SQHzWYnwtq/tDTamc= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw1yG/npscG9yRca9RdwsykUzZ2fB1ISUhruNnNyh6AU0h80pp4mCZIr3r6vzPWn4QA== +1u+XjG/2+GSQRv6EzCaWRQ== +zBfpEDEHUpKcgzvJDq0C4YC6UMbAUfqAJ0xI0cKnOaESr1M8ldgYE+7A7EPPEZ5g +84Vymj90Wzn5yYuvz1pUuBf9UiMoSzUCfQBk2T/lZO1ZenPtkuAwJZ/DjYGk7EF3U1UkgN8sUTg2e8HZLYpjgw== +VmVrGQo2zRokW/ZuO9bN61K4MXaMsg5/Kxe0Z+g/3qNgb6okRPQ5AYXwGskH9ejOnwi4YAc+D7PnIV1ewTrjYg== +VmVrGQo2zRokW/ZuO9bN6wZri2Z3CBt2YpUf7zFZ0+KNguNuVrtBMhHtFgJ7WYYf3JHfXVX1T0GtSc/XTfHPmg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wXyEF6XhF/PmYb/RCvDnIg= +VmVrGQo2zRokW/ZuO9bN66V9gsLif7RpodOVxdJVsvNCObquF8KcqaCn9joiTHShp4vhVSeHci2o+TX7lXV5bg== +VmVrGQo2zRokW/ZuO9bN684ehqpwxtlh5PoulYJQlzwAsh2I93oB3djuYOuh9qQN7UA7ptBZ9kbWq4QL87fNrg== +VmVrGQo2zRokW/ZuO9bN632U9Ut74PgKeN/Vd++qe96QUfvqhKcAMSKVyTsZ5tjiis5cEaQeER/nY9riZ04zHw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64IOlCXEAuw7+JQuoGU1tA7y+jY6s/JvEWCe2pdd+pjEK7nJUBmqZJQTwm3h72ag5FdSSEPIKbcMVZEYOsof8a+Ny0UHjA4vHFUkNX8Y9NDS +VmVrGQo2zRokW/ZuO9bN6+0ruBcpcWh1tg//57328LXWh98WBeFYX/reLiggO4A8 +VmVrGQo2zRokW/ZuO9bN65YLSeu6SlsROZcKj6GOXcg= +1u+XjG/2+GSQRv6EzCaWRQ== +P1bBdERejTBi3ynjwlj7j9BN16EKwuXwZL0J6G3zRwc= +I5csN8e3J/KIFkMa5t+SJP5ieXqyhKdpsUu8T3AoWE7POsp2/AbDJJ+EacQGfSh3 +FsRTDT5nlYHpT6JC9UdH4bKLDUGeT2OmWeP4uXVXctcW9UIqr4TBGbKKt1WvG/efazn8+MYwktdhj56RVE/jzB6VLeItbFDX6J9e2phdbn4= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +4Ky5RNfad94RDxFAfm3/hD1S6TvHVGkU5RCZUxk7mPUS55Jd4Ae2dEHUhzTAxZvn +FsRTDT5nlYHpT6JC9UdH4XY/Mijkqlb0CXZ+gLX4s8Lo/U2S2nexcGC+4OIyPFh5WgAPq9Gi1jmpFN9HHqK8ckgWpJQHRdLlpUv0ZQ66iK+mMCKlcQxGqoBtWZH9V+uyqy/HzwNXYzZkmwHooOMLk3U5rG/7RvTMa+ny/hOD4o4= +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +KZsPH0cmEM2QRSeJCcSyucww7vjWuHBitH1P49Sqh2Kwbdrie0k2+bXVIdFrP4Hp +iEbw/85IA9dHrKUZiqzvJYFw7QmcLtkqxGPIfRytlW97cg8SvyFzcKnhDF/yOCIqBpgg2zvuF6H5ZBpWx9EJEA== +1u+XjG/2+GSQRv6EzCaWRQ== +Oq4JxO2byG1NpOr3BWRu3IQ7sxcnLbXUljntZmFfE3nh6wlJfMDKKO9ubZppLWjY +Oq4JxO2byG1NpOr3BWRu3Oj+k38Jm2FrVzxggusCskbYvEUoOq/NQoGJhFujv6sPpGYXVLDzIbfY26IswYzgjA== +1u+XjG/2+GSQRv6EzCaWRQ== +/F49ykgjedWbzpXYCpMOgrI9YLZA9ExR/eC6+PgN+EBaWlpc2Mqf7k4KIlVymqOX +Oq4JxO2byG1NpOr3BWRu3DAGQWIxEWJFwo5ywit0Hm8vjztDN9DFBJzSbRYvk9E8aVOOFQPA0eKwd04IoEKP0Q== +1u+XjG/2+GSQRv6EzCaWRQ== +mG4WxLOsQ3e8HrREEjwO673DdHmO16hnGJ4F+Ej5WMn87jXFSwAlFwhj2veKWfNoDM+uXyfiitL62YeJKZOR3Q== +I5csN8e3J/KIFkMa5t+SJENrCajRah1QU3uYF/z5CIDRw0MaiyCM2fRi6PpL0QdK +wlLHv6kT3Q/RmtMBN4nDAbv8zySb0rt8J6SFevmRPRgKzDMeoocwhCXrYYhV8NxX6MkHxyBVKtZUBa2yXK5ttQ== +VmVrGQo2zRokW/ZuO9bN6yuXF5mdisxBEft+lGUhyG8veZzPJ1y7VdReq0jTr8QY +1u+XjG/2+GSQRv6EzCaWRQ== +tm8SW4k/FXA1T5YrQDZ7wWnoVqAkjsZ5VE6E38yZjMw= +Oq4JxO2byG1NpOr3BWRu3MthcyzqNuNs+1GYOnU5w+jsgU7gzHR2JfE2rBUCpEkb +1u+XjG/2+GSQRv6EzCaWRQ== +kniDpSFE0vLELL83X2MGdatB7FelKh/g6Qxw8LyyT8HGxMvOPbla6t91vkFW97vk +Oq4JxO2byG1NpOr3BWRu3EQ6fq4nu+wy0ERSqtq/yw8M0VQBkl4VxvsO8pLJ0tXjHk57blGJcDTSf5IZnT6A33vklf/1JB4f15dsurs4TIQGV33FHDGQiycTjLI89DIU +VmVrGQo2zRokW/ZuO9bN68PLvFAyPYCFptnUBkrwhM+fcDkoqf6krf9e3/VUGhLb +VmVrGQo2zRokW/ZuO9bN6wj+eM5w23B0fsVf/h/HrFSTzO98rc5IeoE6011BQZVK +VmVrGQo2zRokW/ZuO9bN69QoqAP8+J9sRJqfCnnzeuAXZF+1S2UZnPaDpalUlo3b +VmVrGQo2zRokW/ZuO9bN68ULkZoiR2t+tvBrm+xgZHsLbfA78IfbZM7Dajj7ThPX +VmVrGQo2zRokW/ZuO9bN6yLf0ryvhEUQNBfas0QHrzpoCyWrDfpVrSO9zlFCkvcL +VmVrGQo2zRokW/ZuO9bN649yP7OWYGC7nlAREd+ogQPwefQidhBHbKOs43VE+Bj4wleoG4CsMdHTj27yDJrlMA== +VmVrGQo2zRokW/ZuO9bN6yXvSimWjBkSPa0oACTE6ZLwBjvix6RVUl1pckZX2MOfzsYgQ/oO/5Wg0pi5sa44/Q== +VmVrGQo2zRokW/ZuO9bN68Th/U76hkAmXqrzmdE/ZI2hWSlM1jO4tNZ9jOuBXyUu4UXCtxGmpfpurmSBZ7nMz3CfJpY98NDnzDpftzNKbQ0= +VmVrGQo2zRokW/ZuO9bN60dcEpCcNfgwHuX11nm6/kpaOKlAgQkhD4jWIN9kUu++6vBRzZCJihAhZezy7MiEDg== +VmVrGQo2zRokW/ZuO9bN68woQ38PWuIkn2KGEnMhf18vsoH73KIsVQPZZeg0H6i/uUTwlcqjqaOOcGE2yARxSA== +VmVrGQo2zRokW/ZuO9bN6xc3KvQQ3gxQg4pqItQtHXv4FkR9WdU3ksM799ggpOqo2DDqalu/U1q8We9X/tjBew== +VmVrGQo2zRokW/ZuO9bN6zKmc9qq/eDXqFNWaoFb0zhPVy2ABZhvEveRj7QcX4YCewTr6ys+LhFoqIchJ6zMaMOLXJXIH3Brcdeb5Ue6260= +VmVrGQo2zRokW/ZuO9bN64l1qBkNbm6bRJujDvKFqz8cikEfZdrnEcpounC4jt3+LsHKisT4qqKZ9+YZq//PUHsLDe4jo0qqLnSPkq2P1BU= +VmVrGQo2zRokW/ZuO9bN6+PaCbxoEB1NEX+VMo3rreipvAaQyLbViNAr3J6XiR8BJigxp9Grj+VE6re2Su5SXw== +krWGxagzYm1msYy2LL9fQw== +1u+XjG/2+GSQRv6EzCaWRQ== +kniDpSFE0vLELL83X2MGdQI2Yuhuvo0Ui21i4BCi/nOhdFEM8veprF56BF8YOMRn +xWoGNWjKGPfI4gq8aHoTfNm7h2/3rk5d26JyRNKl45IM1E5n5Mf0G579JPEy0AiTBQKCeUZgjg8y5TnHK2UMyh8aGD16tXjy7by0AqnrLgJYAe3ICK+4mv1iRt4grae/ +VmVrGQo2zRokW/ZuO9bN622mJBXMeZSsngb+R2rsXcQ/aLBiSrwL1n1nxMo0NOr+ +krWGxagzYm1msYy2LL9fQw== +1u+XjG/2+GSQRv6EzCaWRQ== +CU51y3DKNJot7qy7c7KKwO8WYvpMDLkO/fSRGZVdWOribLR0Z36bszb9TfMNFtaC +FsRTDT5nlYHpT6JC9UdH4XY/Mijkqlb0CXZ+gLX4s8Lo7IDK7Ba57kevbJOEF5g5iN9/CDdZUu2p5umGxZgi8mGXi2A6aBVsitBddxv3fWSEKNYQ+BpNe6ioS+VoiF7Zzw3HAtWZ78yWlxOYh+qZ7g== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw0YOhGBfjG2Hra1hWU5/NCZ/kMm8XEy455Qk9gUpHzzUb19ZnbEg357UEEe8f0jDkw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw8p43/TPHAazuzA5FuFRGKEfnXqhECXWFVnsFjkiUzh6SuNCWVHYS2nQXqvJpPbJeg== +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +CU51y3DKNJot7qy7c7KKwEfIM1RpEMC8MSDuKuFpo9c0WuShpb1Gljya7chVcXrt +FsRTDT5nlYHpT6JC9UdH4XY/Mijkqlb0CXZ+gLX4s8J+IQGDwbrhMIxooL0HHyeMwJROaopw2r49Np4gDG/ZPg== +1u+XjG/2+GSQRv6EzCaWRQ== +1V2v8QerKOmubvSxgB4eTDkpvxy+zQh0OKrDfGYCy1o= +VmVrGQo2zRokW/ZuO9bN61kbD8u+dbPaT3TvyJy25czKezzdOCDSA9H1RwvJ5lkG4q/cqRp8Wx3Ph/wk2iqr5A== +1u+XjG/2+GSQRv6EzCaWRQ== +OpwSuFTBAL7XPZ3GOEDUSXmEvKt60PMcxH4xv5faETw= +Oq4JxO2byG1NpOr3BWRu3HsPBkHg7cg6rT4zB1qEB0iT4ntPsedRnYHV5l2bFN/rGoeO8y0fYbDnXhc++lmpPqD/7rT/EUbMaZkgXSxKZhc= +1u+XjG/2+GSQRv6EzCaWRQ== +OpwSuFTBAL7XPZ3GOEDUSYYkSmNb531LWXBThqJnA2Q= +Oq4JxO2byG1NpOr3BWRu3HsPBkHg7cg6rT4zB1qEB0gxNz22V2Dc9j8EyqMVksnN +1u+XjG/2+GSQRv6EzCaWRQ== +8i88MKADknh6ve+bU8Ezb/5J/KOGrNrp/hd+fOHffgs= +Oq4JxO2byG1NpOr3BWRu3Oj+k38Jm2FrVzxggusCskZ4mzSuMzk+00wLYsSzYvJQZjz2fhF1T8o51Q/mKcz39g== +1u+XjG/2+GSQRv6EzCaWRQ== +kniDpSFE0vLELL83X2MGdV6+T1+oCOldFzv131aPdi4= +Oq4JxO2byG1NpOr3BWRu3MthcyzqNuNs+1GYOnU5w+j+CaEwIuq9rJHkO8fHjOtidD5V4PbuwUZmtFwGxxAulOrqkCwdTQ9F4tN/9xCEPFk= +1u+XjG/2+GSQRv6EzCaWRQ== +0Cj55jDWeNf+rIE9r3fPh8pA+0jaLcKE0K2LySjNBG5CU9NDINKpk6NXnWZNXYHv +84Vymj90Wzn5yYuvz1pUuOO0+fywpFJdc39xXTBu9YirwjBLohUzBWtPEoQZ/0wTdvMyNkBBMME3Rb8uomVU2c1LaHGQYxq0DYsuUSVKELM= +VmVrGQo2zRokW/ZuO9bN69JCeXP8VKIhEnUaQVSzY2BypuWREM5sjaruxBFKD2EvkbTZZ0Pw6RDYZMEKcXxRO3BkbFDyeCCX4BTkwrqvx9cQXOTLphOa12SDXvFCXlDo +1u+XjG/2+GSQRv6EzCaWRQ== +Hl/tmaE5nbbDLFZ4/b5RqLZNnSDcxCnQkebcIFUyCThHdTJZ1+0H2i0w2oG8wc2kUhrQlZ5vz/e+PVt8T4Avsg== +Oq4JxO2byG1NpOr3BWRu3MthcyzqNuNs+1GYOnU5w+iR55CovI+mLkNUdXQ9Cjms +1u+XjG/2+GSQRv6EzCaWRQ== +4yfrVt9mcL2kjZOi6OFl+/gCpFE8/3I3bb9RGL9+SD5GqtpUVKxQmo2nLkA+/LSc +xWoGNWjKGPfI4gq8aHoTfPWfjS6vSpTufw6V8YWjiHIbW78881nysbHK/4+YnV4b +1u+XjG/2+GSQRv6EzCaWRQ== +nXHLJs+jc9UGwRbqbBpdlnoY9wZYkQQX09YG8sUW0k9w37YahVa+gJNOrD7ggdpa +6K6zXmJJNC0xAXfJ6TCmfcRxgBy4y4bgsEJ6wp9YKM0ivN098EI8rMPmitQhDg0Y +1V2v8QerKOmubvSxgB4eTNOWXfUINjsWZ02BDEUwQGr1EDSYeDhTrvRMzg0k85YK+Tdos98WcA+iIgGaBKpiKT9+UZijWczfGNGyP/pcPfc= +VmVrGQo2zRokW/ZuO9bN6xOcE74GXukFgoP7mn89osg8SsOUQXFdzvPn+qkat/4/ +mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU= +6K6zXmJJNC0xAXfJ6TCmfaVK7fRvRq1+Vnadm3/saDE= +J1UGlXkKhKXD36OrXXhN8NQKeNxEQEtSrCTi40QJlhZc8+TaSALhLYCLySZ2J9Ro +VmVrGQo2zRokW/ZuO9bN646OXWelyIYU2xRPUxRlGkJcIaIJLy1ed19RDL6uKh8/ +VmVrGQo2zRokW/ZuO9bN63w47pW0ZChbYuF1zj78dlkjTE1Ux1w+sowcZh1RTEJIJotfy7tFcWOcc+h/FsMt6JMpiz5HQflNCEI8Eq16698= +1u+XjG/2+GSQRv6EzCaWRQ== +YY5wEo94l9qfo5TPxQmUgoNFl2Fp8+FsNDKaanjjEPI= +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +6K6zXmJJNC0xAXfJ6TCmfaBFl7DuG7+1nNvbLJkeawSnTK85ofp4jusSyh9wK19J +CUHO82G5MDpSEiqhNImyK3tJdLE8+4hhF+l+X4RDWwZAafVU8dGDkz+ecUDZLlFH +1u+XjG/2+GSQRv6EzCaWRQ== +HTdnfikDSTQ3eoPgrOd1XSIVSJ0FqgT0ujqJT4TT0N0= +p/2Atf50M8NSYtiojzMPSPDDLBr62CZS2VKFEpxbOYlh2aRzggeKZtOgJa4QNRRjDmlZSoUEDWHTnLBsPR5L1g== +khWCUyuDMwE3p+YqYO+JihDH6VWAOeZ4oABpau/+19tmJy+J3rvX4fpN/8te7LPxB1H3pwVBig7ITL7uPGmgS1trp4e84U+WKBrLkYlgk+9ksLUpnsMXlluLn47Pq+RSKPiYH3r5Z+ysz5QznyNytQ== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgULgM9yjXNLL1pbqni6GCYV7COvUA6EuHsC6DPEvWiBT +1u+XjG/2+GSQRv6EzCaWRQ== +x4FKpKjDTgzvmsthM9ed85mLitLpZHpV+2azaMAwDvA= +fy7xOJXQBoDzcC4fp2Es8b4oK73/j4/qQT4PiJNAKpYgQpTKqYT+u/Wt6mMI8by8/GiIcIkDsL8FfJgpPklA9ORnDbZQ31dWakN+BYNoQ9c= +UWFnivDUNpc4sbUlvIH35sb1fP3ifqnvtwVEW7gDPMgzaTOToWZ+dtm0gJs6iOgW +D4h5kSTIuI12S4B28Nji1MQZ6Vc6GT9t5sEWSn9oPc+6yHMu1JHyMvb1duE1CGOy +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +1S1xjYS0s+Ph1PyvECx3QEpO6VoTDgpnpbfwQwThNxKM/15BQhqE/rSxGMGgDBro +1u+XjG/2+GSQRv6EzCaWRQ== +Hl/tmaE5nbbDLFZ4/b5RqJ6A6NK6PDhvbfLkcmuyEhj5oVkDGTTQyM++uB2zy5uw +JSomnfT9VTS5sDZJMF/Aaelww2MKwbZp/7eVVD8kr6OXesFepu35fbSIQV1yLVid +FsRTDT5nlYHpT6JC9UdH4aLgoBMH9iI/gDvK9Otd3oohooirPb2qWTIh8DVE0WcJGabF1RhdQ7m+M4FZrMwTig== +1u+XjG/2+GSQRv6EzCaWRQ== +O3CUgrw2GJfB+mDjH5+Ndrd4WB3m61bDboPhSQC1vd0= +VmVrGQo2zRokW/ZuO9bN66z9WxSrMZJFS7jTHpVswVz5UK0TwzzVbyNLsvrwIKMT +VmVrGQo2zRokW/ZuO9bN6/H50zYoQCvE/xsaNA2dJ0smLeVKwyq8MoZNwgB82urOHV+e+XqjX8rx2PoseXYk0Nx/Ao/FZzLApoRzXwE3RbodsGHAw0KVf6rai1cXQVq+JBHQTExmenCnQdeYk/0nNw== +1u+XjG/2+GSQRv6EzCaWRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOktxTjajw9xsLSKA56jqUoJZmUS8grJOzdDA9zc5o9VsbA== +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +6K6zXmJJNC0xAXfJ6TCmfV4ZV8gD9Up1YDhGvRtUFUU= +32pdC9DD05OE2l0oXazDFHqxhgDkaP1Qwpz/PDvbqSJEPj5vobYs0/kRpbeR3LCO +1u+XjG/2+GSQRv6EzCaWRQ== +JLRZ+53GxwzadsJQ+cNcEvILo3rrnPDBNygnfFALFu+Mh1D4ajcYKZqLxpxVE+W/ +gzS5oEArlO5TxOrOKotmI/9kCRGAvCQb4oKKj4l+uig= +6c3cWGGlsEf4q4E+EjPFyg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +pO1FJuvEndUwxGBqdeSIRA== +wyfCe6oL2aEYwEDDv8Scf1z8OmPYpCQARbBm8WGAJQE= +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+NKPudEm++RMQZs3h+yWJpXSp4aRpFh9wJGsBkiKsHmF +9WE49JNcaw71DcH3jZRpmVwnPH/UaQKqus8UOE6/l52OAN3uW1xlorJ8f2t6En8j +1u+XjG/2+GSQRv6EzCaWRQ== +EtuU2kTXk31Go8LkHgiwYE1zCM6asOCACOKJw6mfvhDS6qwg3RORxK+jxMszbXZ6CynXCUpaL1kYqgYUZ/tD8w== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +TZemUyEKHHK61yV1V/TL3c3aAr/plbfFTj5mrv0ZHu1mWLy3iXWg6zOKwPN9jPx42Kk2EL2cFJJubM2P5tt5Ch9LvVIGm+tXfMWqGMkJgdoiyrdvD+hTbjXfbXjMqer+ +ia8+eC8+w4kAKqFUfldoNFpsh1NWef58Ny6sIyvqtw6qH9CxYpcyf6YG9EGHgDajB/4NeuN4mjZrHxd1A1rlGLVe5pVBRyL5CNF1ue/s1Nw= +bT1+2FdOMy7aHyHoEhqX31H35PmRS18TqFVVYUSHorhJIaeU5AOidVN7OfUSEexj +cJ1Wblh8gF3p5U3Kb/RGlQ== +1u+XjG/2+GSQRv6EzCaWRQ== +HTdnfikDSTQ3eoPgrOd1XSIVSJ0FqgT0ujqJT4TT0N0= +khWCUyuDMwE3p+YqYO+JiqLTIUygvZ5uMSKfTR+fx1XKwwwUntKKHZyTjiz9ncDw3ezEL7rreBIGqeATfWQvY60Y8R0oiKJZboV4IbR8UB6XyjBiPH6/vUGqWSORfz+5 +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgba26bgHYZ1ZBeY1/SjzhAsPvlAejrJC4fhBL2mDvxzMJtEw8xuxPJkjj+xihfDqZw== +1u+XjG/2+GSQRv6EzCaWRQ== +5t2lVsKh3JYt9KTvLOEawsxLSsX41phqO0MU8uSWo6r4XDrDQwudtJWSeEzqg3NotAx507xB+RVyfqJg2igAxQ== ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +fspf8pU+r9SJrkYWWMygT6wKhlTIxgusmWmD2dnHgu7HDbL3Gkq2md95yvz+bnSuPmaPD5+aC+NXXr8/6KAvxAghl6aEaxfJwZ2dmiqJQZs= +HNr5/wGY+7coHgowTe94Lr/tpcWCBkrQdCN5TmR/MAme0Jr5QQWmAR1uVKIjAvAzn+s7yX5xJQZR5lwQ+YXaCBx4yeoACHcsCmEXecYQ2du8mE4EUU3yUKDsd9098flSOInGOo2qJpGVdwv/jFTOGQ== +1u+XjG/2+GSQRv6EzCaWRQ== +cbvASHjpysrsjdY5RctXmNuRWaVCTV3IfXxq5/B4M/l5hIxMJvyrVBtT7iVWvCds +L0eUthVnpkGsmKFAX6d+uLib58fjFLHLA3nq/44o/Cw= +1u+XjG/2+GSQRv6EzCaWRQ== +lGY+HGJ/DlLufZHWWlj7U9r38R8Fxa6wY+rYHsrxZNlSONyk6R3MNMnRoFhbQcysKRZ1RW/46c7fe3pKcKgneg== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +YC1UY33E0KOAQUeqmpcQgQ== +oISfKnZmdvuToULQZ+FFyr2i2Brz9ljXaHOKyGYb25c= +A9UA1yJHjpCuekrdSHwJzwo1EuST4zAJ3LNkG5YnB4PPfjBHPyZxUhXtg2keHQ22 +1u+XjG/2+GSQRv6EzCaWRQ== +9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U= +PMsnmYnXwTnywF6bV4DtbjBmhBKkqmzzWhgWTMQKUtmEG7+uMZ1O0FzmJK/3hQ4e +nISGhBW0tbL/D0lQ2yNPTT7FVkdEno/QyBB3p9pKFKC6TdxJPov/HfIHAHYw7yRYeNPnXUiRtuBRXDHMjr0X3w== +Oi82qoqybzdF6wkjTqWiM5joMzAjQWTwbxNSIEXhMgiab78qj/lZLvmdt1PPZ2bnGI46aVJ2+zh0tp64hc8PgQ== +S9tzajN7j7km7OLOHRXxfebLQ823piED5q0tpH8z/1YPaWNiYvsAENBtlVGf97n8uTj8hVrVf6ihHZvjygrbZg== +1u+XjG/2+GSQRv6EzCaWRQ== +CO4zLQS+7sxKNzjwb5Qdv7eEsH7r2P2dLPAV1to97gol96aFPusyoJUNwYFFNYbo +cGzT2936rNP0cRSqygcUrE7A42L7FIG/hGqW+QvEGkYcjK//YXubOaR/fkQS1+cw9Dyjvvz4uuakJwfgKFL0Ug== +W3A5Dt+8AxWSKsTYeXZoL03E+GmF6G6ZqKMiSSB2XE+S2J0lAHvztfjEq7cK++/6RAqMfNQ2O/HkSgB6w6nRHhdnFMRajeJIomuRGUCboZe+BWjJqWc7ADlhoqz9wDqAmarDK7NshyxA30lz2Aug+A== +xWoGNWjKGPfI4gq8aHoTfMILR2ZzS4c+Hu2XOwXpf0zVeJvwZXetFtWYi+vkCyUVIbLFP303SoVxsMoy3AE211EkR4yh6N5rWi0xHDl3FBM= +Yaigb1wG5vfgU7Q9M8eNHG4/xVhRXpcLlEI3ZAQ1KH7Fb+RCLrOE7B5W481+LpjQ +2VEsH0pOk9hnXSSgTnnijKIYL450dGfbk9HM/wtm/o4urNDTfRtdqkVM+pQb6jmH +oJ9IMj4ANseQkyyPRqcQFFOmpjYou3Smk4qEnAgkl5KSr+zFhFa88zZ1Ug4ba+PI +x1dgkCsAmAsGlXluxUqAjWLjzp9psxIa8kZebpYD08O/k9dqMliiryELooAt4Xn5 +MQzeGnqHGo4wZ4zu4kGdwqlUbK0ZzFZG41yK61wvKsGmqFjk3xQlSBk3ljyhiIJC +34r0w5Whxk2ASQ6HLGWfjYft42ErrWXEFVAKelxYobY8vy4Um42KbVMsAh3Cek0SelH0oyKFyaWGPcQvfG3i1vsM63kYV+g/egDCSHvBP2E= +1u+XjG/2+GSQRv6EzCaWRQ== +1DMUmhwH/xlvvlF9Wem6fpbYbbn0p+nKYrV/0hTgwyo= +trSptuLx8nV2jiPpFv9oLQZzRewlWovNpEZlX9ZHqdNJS+w8pfaVBXwYDtyCfN1c ++pBWk5LxptzizUTsHnZAzw98qUHepCXnd8PKllvEYMVQqP5jFGDm74q9ET0oP4ac +p2+bVXgc/OGpnu+3Mkh/HSvD2ZS1eGKMWD4ICmOkJ7Yc2VZQ033ixPInbY93yISA +1u+XjG/2+GSQRv6EzCaWRQ== +vTNP81yZLjoLdNpbBobM74/BQxPMwxMsJu8vs2PJnsg= ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +qQRjwP5jr3EkHpYOFL9JKpejYz3YkCA6D/FaPfRpuAg= +96orka/uERLyRst14azQwhglCKRM38C+C4aUvBEVMMIDcSTKbaOo7uyX1IVdLPpT +1u+XjG/2+GSQRv6EzCaWRQ== +G6YJbDdwBAkXl7+77P1VXsOTVb80rJImj1GUv8HzzrnTsvRqoxVq97cqSh5TqqXNZFj72XSR1FalQoEg6zIUfirlaJswaMTfYLUxATtNUkw= +2jj1sPN6onDq9H0brUOW8QaXy9+XjjcDu0XJ1gL/R9WfNVmPzRgHqCGjZTiIH+ozcL1S5ZpYEL1CVlX5aLXcw/ob647jqxaJBHXBhg53iEtZkWxuk6wUDb3ZeyxhYUjg+hVSeXHh2jNM6Hp9wnVCScSVo5u4gAXSl04VDSb4oshlO8RMnh8DAM2Xnq1PlQ6qCrWJMBa355tpCgjgxveZFQ== +Z8UsPk1Q7HtwjRd4g01ryw== +Mqbt0PMmZi4h/xKUypXQQacVcRFEO1s+fuomosiA4i4vhKbObY/72ISr111UDQrL7/AulKpsowQr7iFl2gPLZ/me9uc7gFsfVeCvi3kQksM= +Mqbt0PMmZi4h/xKUypXQQcR0BoWthVBkcYEvEFJoVQt1QSpsLzyCCHLMqCeGVJYtKB+h5wXanOmJNDUy7CAPmRDmr2WVJqKuolo8HBr9dWI= +dY4KBvoy6P950RQXZL6GYqLIqrTEq4Y5oeZN3IYCOfc9zK38SIvjtAvasPc+ddRdntcypyB7CxJFKWE8IPyVvg== +dY4KBvoy6P950RQXZL6GYnJWa9907U6wpOurCx9j7Z5VOxp5kLTqY/2BD3q//8uhlGspDDcFpc1zd780k3Lkfw== +Ys4bozvJBgtB7q16qcvtBwNMiYZSYHZr5hA+OJTMPZqqqm6alXyu2POp+5gODZhP +Z8UsPk1Q7HtwjRd4g01ryw== +qK4BqV+m2mfNquWU05Vg/0NMTolkH/PzyzM1mbdgq4g= +yBHE1iDakfhoGXyKaj1gSGfHrpXcMw6rnEfH9XHrgr607aQXMfaN1Hj/8IfgUIUBdG/klgyZT5Vm01HWYQlfjg== +2ysp4amLVCES1Pxcsvl/lHS0fM5YKC4M6iZSbJILQAyfyq3F2+f1mUQU/7yehOqV +EV8iUGS7sT+PUUyXub9OjRjJTBTvfEZOSua8POeq29FOkVJNwiVdoDDoyr7td/h2 +/rf/6WKYRysX0TuKufaOmJOoPJlyPU2hMSnbVKPQaHm/1V+wiuV8GfC9QadVbBjP +815Ee+iZtyjtHRMIJUIZZt36Fw/kbeWkQhsrhAK1mPVqRN0QJFZp5rEae/AMxWIl +661hZf7vhUQ+50okfwfTXw== +1u+XjG/2+GSQRv6EzCaWRQ== +YabJCT93a3/IohXaIkr3oWlupiwiF+JLx3IX7SB5elgYui0suzuRoXjqQr6C50M1d2QOXZCiCtbcrQ2cnuAdpsoM6Dy/0tYW04qaANfH51WFUE96cDd6YClRjyA02R5Q8iU5D8qUNXwHVUnDft9fYA== +1u+XjG/2+GSQRv6EzCaWRQ== +EfRe9mf74UfK+9Ag3xivJHhph+dverhm4EdMcw3Xx0gmgVSQnFzq7kwn/hVChgPd +oacxXEoPih9NKsd5qduwcEDhOLgYVO7fKFngVnbMb6E7IvS/sqSZFXzo3XUaLunP +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Sj35ZJ4tRqncv3eTewvWCYTVi//owYnWUb3EQdeX95EDDg6adquVddsWeGr0Ty9h/m037knzcOQNL6388HNwCY= +1u+XjG/2+GSQRv6EzCaWRQ== +zACox7+Ziz8wU0czVTxOUy+ks7x6cRtUos5ttlQSAFcy/PMmajBThYyk6c7kIdkm +oacxXEoPih9NKsd5qduwcDiYSJyxXWrMlIwKVA+jt11Nl4igHLNM+eecuFa+MI2V +32pdC9DD05OE2l0oXazDFHGwLxQjI8TRgs4IRu7iv8AJg9X0Ms5wLpGMWQIg4x52n6cXO1mISZ9rNPx1WG2EsbOTjO6/U0JvvsRkuZxOpxoRGlG5Dq7TVsoTOyYlFTox +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkvCs01Ef75uP/SBKZUAi7QcIuWcW1HaUBcTUGockc85s2tAMP1FH3uC0yUPmPkoVGKAhoRfRSNXjDN7uOhecIT+uWgmQyflNPIMcVN2xiHvNw== +1u+XjG/2+GSQRv6EzCaWRQ== +XgKqf76l8RNsRorJUycsTjsBLg+53P3wxj6t48F4LaUz1IW9XZkwmjHdKLketDxx +/o1IY9/qcv/0KFiM9egBx/gKHJAl67agkj8npfqcJQnuaRN1a1TxspwvD+Tj/5Pp +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgWO6e0QX9n6cbuR3oEEP2/AjHeZCSnyTT42Ga28tQxqRyUhdaNOXsY5oYaNn/hJa6g== +1u+XjG/2+GSQRv6EzCaWRQ== +LsvGMcKEG3AW3jqdlISpabFo0vNBFt6+XKN0xCaGVpo= +MggzX5igTzCWk6jnex4yNhZaYkbGyk+2bWWUgJKFK8lZcet6pQ65vxbLg3Yw0gPH +Oi82qoqybzdF6wkjTqWiM3Ej/6cJYZT99PxuXrJGKsklj8CWLBtXfXEVRo4dS2WV +CAhn8dRUItEbEErp4w+lX1hGKjitJdXTa8UuB+McQN0= +1u+XjG/2+GSQRv6EzCaWRQ== +sEDutDf0WonupGrRy9fpQWSlBBi1IQpmFiKRDVcSFf0= +74b5ByHflXvpHDAbFdMOR1KbRyH5iYw91R9i/OMBXOw= +MzOj4ZiBOJDNVP8vi/lFQDKjTKywLp3ERfUCqj4x8U1yWrEbKh5LG0cEQH9nD1tM +YY5wEo94l9qfo5TPxQmUgm3uLJxp9ZFr+0KFa0oXP4fvmkRFy3kB+iXcpGXlgGUv3IV80OjzGlcQlKBZhlYIqyJv1jiDIv6mIuk5wUcpf6+QBYYu8DqIAW10Dm8ErIR9 +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lX6DMJVgu4pHprAR4mk9MP9Y= +1u+XjG/2+GSQRv6EzCaWRQ== +PtnzyMg0rQKQyXWwmsDmjLu8Hf+EvRsKp4XgGkQ3PSQ= +5rjHMLATIBIsMC+DPjqwgJ0PTbeD55FI9SR6yWcTxomOepeWBoqzGc0fNM7pbR0i +MzOj4ZiBOJDNVP8vi/lFQKuN6FqSR/X1y5Ym9ludFS2siHQAn+kOqZoKDv3PDMJh +L0eUthVnpkGsmKFAX6d+uAqd6gumt8xhrtuQ4n/xAmGddE6Jp4uyfuELyFXtm7wb +1u+XjG/2+GSQRv6EzCaWRQ== +q4nc/jwATOMUyfSjLibfEW5DCAn6VBbRQRnaOjh7eA0= +7ruGyOsuKMUu2VJjD7qOMeaAkd8lbFxmFgrVLxFs0oPIjIIPvkx0ZFyMjf6oRSyt +nISGhBW0tbL/D0lQ2yNPTcTXXQ0x+6saiqpEkbK6NAeuJX6XzqONacN+7aveOlAT +SXXsDW1aLF5ljPyJUoRc7gTkpwyyuZiQadFWUK6ykeVcUQREHclZPKPeiKYvZapm +k5tq0z46pbBEHjNLy3B210WNtogyJ1pKl5SDWGobuN7lIugvbz0/UjtbpBjJtfep +vutPSFQmcQS0oPfw5a/SpHhmS47guKe061165kwNCHVBa9zeUiK5VwM/CeGN0AnG +7jSw4GB3eBRbmC1MzdtYH4Mvcc/Am0zt+u3UuaEBahPSlLobj01cABzDY1FoGFCl +1u+XjG/2+GSQRv6EzCaWRQ== +CAhn8dRUItEbEErp4w+lXz7yoJNCHk+ySttYgyPNvHViu9Hpq9uXa+rVadpMBL7U +1u+XjG/2+GSQRv6EzCaWRQ== +ppSV4h/JuTO765ee8P60djFRTcv7RbQXr3ejg/5Stro= +enjROCFkVaOWy0LTK7q34hv13RHzNPw8gX1Hu4pdzpwkBazrf1LBazavB+eTYwPl1Z/6ldcy91ldsw6oqOOIP+hnq5pzxcIt4EaeT6dCf0kGmxKRT0iopahN+XchpQK4 +TzinVBSdupkFZuXDgmwYft0hyWG9gyvqIM5z2+BRSDSvDLhxrZ/Hg0Og/OncZLBF +HxMcStsbV913HJxaH8Cyjg0iaGCAJnQeoQ3h2LRHpbqQu6HIltuuNboh1V+GpS7B +cJ1Wblh8gF3p5U3Kb/RGlQ== +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBOiUkMe6n0pbo14wGSTsDQOk8liAs4JeLrT3NGczOns2jBJYnsItK2zQ0a5S5xadMw== +QKClQ4XOaAjfVBfstd1swg== +xUiJWUyMvL3eKTICfxM7FCMSNzMufGxklC0IU8/JVYPq8PoBNmJ1SH1+L8SDAvuH +t7mxej6jakCNqddDE36Df3zkTsb653qGbV/F34tZdIE= +1S1xjYS0s+Ph1PyvECx3QJDm3XNFfIYZRHnGYmSIwCIcHDeqrSzjaegoh8rWKdF/ +1S1xjYS0s+Ph1PyvECx3QBWsBJUQeCyOY5IXni3FxPmv9MRNAWQc9UzRezGwFGgc +1S1xjYS0s+Ph1PyvECx3QGODptMk7+KbeXTQrnrxNF0zQ4LLpXSBvYjEKIyuOp90 +1S1xjYS0s+Ph1PyvECx3QKWGwAO0eUErM9kYc7KGNOMY1i0mZksqhWM86rD9DfZP +u0hUnNmN45uOBDWPIUe7InS6qNJJc5/f+rwcsLLih3s0/VimE65P3ZG4RlH9wNXV +1S1xjYS0s+Ph1PyvECx3QJDm3XNFfIYZRHnGYmSIwCIVMA3n4yBi/xezVsKMK0S/ +1S1xjYS0s+Ph1PyvECx3QBWsBJUQeCyOY5IXni3FxPmv9MRNAWQc9UzRezGwFGgc +1S1xjYS0s+Ph1PyvECx3QGODptMk7+KbeXTQrnrxNF0zQ4LLpXSBvYjEKIyuOp90 +1S1xjYS0s+Ph1PyvECx3QKWGwAO0eUErM9kYc7KGNOO8ILrbj4/es454sXpzd3A9 +dbNteFumm5vpTsr+4i62X5p/8+u+M5D6CfZ4tEhPZr5vyPbl4o59YpdPyYwE1ESV +1S1xjYS0s+Ph1PyvECx3QJDm3XNFfIYZRHnGYmSIwCKf+3KUw3BlXg72hpbuo+TP +1S1xjYS0s+Ph1PyvECx3QBWsBJUQeCyOY5IXni3FxPlGL26wgkY6+dJ7PJ+fCFV1 +1S1xjYS0s+Ph1PyvECx3QGODptMk7+KbeXTQrnrxNF3n6+vY553WSIle7J4J59E/ +1S1xjYS0s+Ph1PyvECx3QKWGwAO0eUErM9kYc7KGNOO8ILrbj4/es454sXpzd3A9 +6qYim6zQvFxsPlX6lxLuh9OpLzwsGkuv+0cGbg7wnw0K+TLbV453rCxqOMOQZKGr +1S1xjYS0s+Ph1PyvECx3QJDm3XNFfIYZRHnGYmSIwCKymbQ+0RtDOQFQ22DIHD5m +1S1xjYS0s+Ph1PyvECx3QBWsBJUQeCyOY5IXni3FxPlGL26wgkY6+dJ7PJ+fCFV1 +1S1xjYS0s+Ph1PyvECx3QGODptMk7+KbeXTQrnrxNF3n6+vY553WSIle7J4J59E/ +1S1xjYS0s+Ph1PyvECx3QKWGwAO0eUErM9kYc7KGNOO8ILrbj4/es454sXpzd3A9 +XjfbTFaj7XoPc2eYZ+gMJo6eWbUCqqDAvsjMnfSodX/PCCXBu8WCakJqKFr1aDCV +1S1xjYS0s+Ph1PyvECx3QJDm3XNFfIYZRHnGYmSIwCKmOeuQopJx735jIb3Tnyyt +1S1xjYS0s+Ph1PyvECx3QBWsBJUQeCyOY5IXni3FxPnDBLbFAEuz8EZ7CzrIrLsg +1S1xjYS0s+Ph1PyvECx3QGODptMk7+KbeXTQrnrxNF1CdXIJ/Sp1dgfFIonc942S +1S1xjYS0s+Ph1PyvECx3QKWGwAO0eUErM9kYc7KGNOOuIeWVnk4wWvUyekrFHmY8 +rGKhzlj3D9UcbU0euMjFPh1e6IZ4vFWaK9PIKtHBKllXWCWQkATITqofLPRkJmPK +1S1xjYS0s+Ph1PyvECx3QJDm3XNFfIYZRHnGYmSIwCKbEUxCBqpJbuXsXfSXYx+4 +1S1xjYS0s+Ph1PyvECx3QBWsBJUQeCyOY5IXni3FxPnIytPW7kzFAmoRGY9h43/9 +1S1xjYS0s+Ph1PyvECx3QGODptMk7+KbeXTQrnrxNF0V10jqIB/iyunigxEm2gVB +1S1xjYS0s+Ph1PyvECx3QKWGwAO0eUErM9kYc7KGNOOuIeWVnk4wWvUyekrFHmY8 +NCQ2T4UV6OxwlwSAOAZoi/fyvUH6F2cQj0U8NBdNAwhJ8TL/krMO8NKKOKr89mTh +1S1xjYS0s+Ph1PyvECx3QJDm3XNFfIYZRHnGYmSIwCJszMpvxPshNnany0KKKy6z +1S1xjYS0s+Ph1PyvECx3QBWsBJUQeCyOY5IXni3FxPnIytPW7kzFAmoRGY9h43/9 +1S1xjYS0s+Ph1PyvECx3QGODptMk7+KbeXTQrnrxNF0V10jqIB/iyunigxEm2gVB +1S1xjYS0s+Ph1PyvECx3QKWGwAO0eUErM9kYc7KGNOOuIeWVnk4wWvUyekrFHmY8 +QKClQ4XOaAjfVBfstd1swg== +qxwHEnmbNjMSRPgeUHc97/jUb4NHog5a2OXXyrE5u2Of3XiO+4extGGaGn3tNa7e +7PAOTihuiyx8HjohYQQI8loW0wLM6/rTzDgnVLHLrvIdLPn96K23u+K9vXl0dME7O/JRL3Q4S0gC5IANpqaGnA== +QKClQ4XOaAjfVBfstd1swg== +c5XAUFkwrmDqd0lBTuA47ZNDz8svkxc+khqA0H7J1k8HFv06pegjE2fNysr82zF9 +xqM9kL6IfC1Rf9JegDmC+ccMma6Uewr2Y0GD7LeUqg8+lOT2/clkrbZ0C/8g9o/e +QKClQ4XOaAjfVBfstd1swg== +a6zIn83aERAna8E909JSBDqt1cQLqKrsvQYIMf651TQ= +IFG43LCpIjO82IA6vnlrf5L6k5l2Z7kaFblYYwre7b3rrYQHDUIsH2J6z/J9gxVt1LeHuTFxwKMm6VP5kJdY8w== +1u+XjG/2+GSQRv6EzCaWRQ== +FVoz2gTAtboGsizqSnJ3jww9cBxg3GA/8DdZ8K/YOQM= +okO46V5/+Iw5Cn7pxha+1Cjh2KQ02cL1Ml9F1rWAsUw= +8MfNZ2qNU5c4gNo9k4SQSprgPJF4as/VuJWfMh+Lfik= +sa8TD1DC5UMKDDvtk4naVSMHAkbu51tWOn2Ae+n43d8= +fpyOtVj+Hi3KT0znpAq9h2b4mXK4/sYA8Hn8jy/kPgY= +10p5ngvfyrUYoKtSdW4psO65l2NSS/xzodovSXPEglY= +1u+XjG/2+GSQRv6EzCaWRQ== +IFG43LCpIjO82IA6vnlrf5L6k5l2Z7kaFblYYwre7b3rrYQHDUIsH2J6z/J9gxVt1LeHuTFxwKMm6VP5kJdY8w== +1u+XjG/2+GSQRv6EzCaWRQ== +1u+XjG/2+GSQRv6EzCaWRQ== +vklqNFXBTCz+emXAvQbqCmTyGNezXWtr4nhxd7vprN6spAuLmFPhm0znocdIYVOcFuEuT41R4eIF4LyG+xHauP43mNiv9RKb7tGmGt0Vp1V7wfmSJqxr9n5OV4pcp7RL3ISRND2qxky4qyrya7eCTcOMKVkC+CA5lLq6CsSiFeQ= +QKClQ4XOaAjfVBfstd1swg== +/0ULpLqgTvInFD0r5hHANn0d+C4pinWmo+ezxdjIRItfT6OYsNXkv0SpD+iyt2Vp +ncUn2TP8ZQ4fZMBYk+CUrRwKSWxmuB8Lw8CqqP/PPoRn6EV0WYYsVVC1R0wBcsmp +32pdC9DD05OE2l0oXazDFFaYrzujKRzF6iM6m7f/iWZ5Kn5ob6aBarlGlMBKy9YxEiSqThn9qgj8Sq520UiTGttGM5ZOU8F6A7t/xYjvSSI= +YY5wEo94l9qfo5TPxQmUgnlAHpO3fFZOqoui4H9UIxi30xHoX4p3D192RUI+AmwAxMJoUuZQP5ZQzEFFlVQZK1QrzVmdrWBoreVkoGjLqeKc7a3qd2Vp2J3qMPwzKm1HSn0WC4BLoooyUPPrr9vsQ/RjtIWK0rdDnN48LmxmbH4= +QKClQ4XOaAjfVBfstd1swg== +a6zIn83aERAna8E909JSBHhJtq8ngOEr28mE8+KAnfy+dCG3Se8doeiKaUlR2Sa+LeFJjhBDsYYk5AYgmm0wYw== +QKClQ4XOaAjfVBfstd1swg== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgWw9lTNZDbjWm86zjFsfk7fJXF5DJDnyD5Xvclrc1YbXWazwvYez0tTHnkuj5IZG/A== +09UuO0tw3kJ5J618hVggcQ== +SaZ4oe/LxFZ0EyU/TY70RbOqRL58hj9gO2Z24GnCulw= +enjROCFkVaOWy0LTK7q34sNdGLv3RPN7LaAk3QYUfrVY5IpXrwwhIbUuUN0u2kxza1BYg+uVoUFH5W/7zjNIcw== +TzinVBSdupkFZuXDgmwYft0hyWG9gyvqIM5z2+BRSDSFEMzGdIs0FhDrn3wW2meV +QKClQ4XOaAjfVBfstd1swg== +a6zIn83aERAna8E909JSBP20sdUGQnUau01b8cCNryaXRaP5Bs06FUEZPwhTsZHJ +1u+XjG/2+GSQRv6EzCaWRQ== +xUiJWUyMvL3eKTICfxM7FCMSNzMufGxklC0IU8/JVYPq8PoBNmJ1SH1+L8SDAvuH +t7mxej6jakCNqddDE36DfwXpwVzHftJTjcyprhlLPhQ= +1S1xjYS0s+Ph1PyvECx3QBqKE+KgSKnKVV7UA4OK1QDENdMybOcvSzV+IEQomIVH +1S1xjYS0s+Ph1PyvECx3QG327AjRjrCCDGPwqLhv/F0HVp4Wxd+NyR68DZMooJ+o +1S1xjYS0s+Ph1PyvECx3QKd7bySDyCSTW77tgemoglrJkCJ0g6tTvKHXi0aVjXlL +1S1xjYS0s+Ph1PyvECx3QGpIwPsVPwwGsOGVa+rf7iQ4HrTRft/bE9PmThE/9UJE2uLLvFyx/vy1DGPEO5x9Xg== +1S1xjYS0s+Ph1PyvECx3QG2QNugDn2QjPlRxEjs/XZx+B0Vi+uhnPkhY/oVFJfMz +1S1xjYS0s+Ph1PyvECx3QG9F1Y5ZUI3n+pcZqmBadIIr6VsfT/Hcs+83FUqbL4Nb +1S1xjYS0s+Ph1PyvECx3QMe6/E+gLCykds0Xu4IXv5i7IMsugGATrLQb0AB9+Dt+ +1S1xjYS0s+Ph1PyvECx3QGC6nCIPRhi+tE/QlVJPMEyELX7uH1DnRiZQPJFt1TS5 +1S1xjYS0s+Ph1PyvECx3QLSoLCvMVmMj0HBq/ILswPXUs2L0x2mGleOfYlj8trIW +1S1xjYS0s+Ph1PyvECx3QLfjvJgGgMJh5MrgNPIpplch9MVstRguT5YUR1lfoQ3m +1S1xjYS0s+Ph1PyvECx3QOBtTdDTg1VFFDmn/oGf3XCl2dDKwsetF2cAVajwQFvm +1S1xjYS0s+Ph1PyvECx3QM3onnNj20wwhiQvIcLd6XhWMehJOJtO3ne8ufQCQJLr +1S1xjYS0s+Ph1PyvECx3QAEVp198XLYW/lpzScc2UyHwxcDxH2P0BHC64793M4bY +1S1xjYS0s+Ph1PyvECx3QA4SoJn7AOxtpfjqgNZKusdcH438nFsdMDXH8oNbzI/w +1S1xjYS0s+Ph1PyvECx3QM7KWeunR6gW0J8nfZnvMDuWS0Mvgefou0oHZeGySfEF +1S1xjYS0s+Ph1PyvECx3QD5Wzcw7Wi+7zlfkmMhc82uY1PNuQ21W35y5wRxjlu+i +dbNteFumm5vpTsr+4i62X5p/8+u+M5D6CfZ4tEhPZr5gi91XqU6X4bzKuLH55aP2 +1S1xjYS0s+Ph1PyvECx3QBqKE+KgSKnKVV7UA4OK1QASucgJj0v3uTMozsfirNsl +1S1xjYS0s+Ph1PyvECx3QG327AjRjrCCDGPwqLhv/F19sMLWo34TwxexpczPlJGX +1S1xjYS0s+Ph1PyvECx3QKd7bySDyCSTW77tgemoglq/K9CzMgySOCxalkuHQ6+X +1S1xjYS0s+Ph1PyvECx3QGpIwPsVPwwGsOGVa+rf7iQP78T8eMQ6TEsXgqYuRYvu9pAVizYt19r04fboCmMdOA== +1S1xjYS0s+Ph1PyvECx3QG2QNugDn2QjPlRxEjs/XZx+B0Vi+uhnPkhY/oVFJfMz +1S1xjYS0s+Ph1PyvECx3QG9F1Y5ZUI3n+pcZqmBadIIr6VsfT/Hcs+83FUqbL4Nb +1S1xjYS0s+Ph1PyvECx3QMe6/E+gLCykds0Xu4IXv5i7IMsugGATrLQb0AB9+Dt+ +1S1xjYS0s+Ph1PyvECx3QGC6nCIPRhi+tE/QlVJPMEyELX7uH1DnRiZQPJFt1TS5 +1S1xjYS0s+Ph1PyvECx3QLSoLCvMVmMj0HBq/ILswPXE5XUwmQ4t8W6NCGpTBl4l +1S1xjYS0s+Ph1PyvECx3QLfjvJgGgMJh5MrgNPIpplch9MVstRguT5YUR1lfoQ3m +1S1xjYS0s+Ph1PyvECx3QOBtTdDTg1VFFDmn/oGf3XCl2dDKwsetF2cAVajwQFvm +1S1xjYS0s+Ph1PyvECx3QM3onnNj20wwhiQvIcLd6XhPCSOJQjRr+jsWGG11TSPG +1S1xjYS0s+Ph1PyvECx3QAEVp198XLYW/lpzScc2UyE00oYYE7oZdtlPQtsRyoPj +1S1xjYS0s+Ph1PyvECx3QA4SoJn7AOxtpfjqgNZKusfVugRVa86k6lWghvJqy86U +1S1xjYS0s+Ph1PyvECx3QM7KWeunR6gW0J8nfZnvMDuWS0Mvgefou0oHZeGySfEF +1S1xjYS0s+Ph1PyvECx3QD5Wzcw7Wi+7zlfkmMhc82sRV5Gq2tNcx+GG9qrHh9D+ +6qYim6zQvFxsPlX6lxLuh7HbWeeWtsLkCwJUqFKrIKKGe0iHM5F7wVV279LZo5Um +1S1xjYS0s+Ph1PyvECx3QBqKE+KgSKnKVV7UA4OK1QDXSgzMJ591Syi0yYA1i3N9 +1S1xjYS0s+Ph1PyvECx3QG327AjRjrCCDGPwqLhv/F22JGtLRrwf7z9VN9MMYQmD +1S1xjYS0s+Ph1PyvECx3QKd7bySDyCSTW77tgemoglozBusaLbagLq96pNAM7dzp +1S1xjYS0s+Ph1PyvECx3QGpIwPsVPwwGsOGVa+rf7iR1ypv9WO3jamfVkAJ742vvhAJcPkzs74CDN8ynpSrShg== +1S1xjYS0s+Ph1PyvECx3QG2QNugDn2QjPlRxEjs/XZx+B0Vi+uhnPkhY/oVFJfMz +1S1xjYS0s+Ph1PyvECx3QG9F1Y5ZUI3n+pcZqmBadILQLRetMSIfTB0UJHT09nOB +1S1xjYS0s+Ph1PyvECx3QMe6/E+gLCykds0Xu4IXv5iHNDKwTwMrA2BvZuEJQdvO +1S1xjYS0s+Ph1PyvECx3QGC6nCIPRhi+tE/QlVJPMEwd++SC9I5wklQQpUNXxd7v +1S1xjYS0s+Ph1PyvECx3QLSoLCvMVmMj0HBq/ILswPXE5XUwmQ4t8W6NCGpTBl4l +1S1xjYS0s+Ph1PyvECx3QLfjvJgGgMJh5MrgNPIpplch9MVstRguT5YUR1lfoQ3m +1S1xjYS0s+Ph1PyvECx3QOBtTdDTg1VFFDmn/oGf3XDDKD+8bU6i6bDS07xqARqI +1S1xjYS0s+Ph1PyvECx3QM3onnNj20wwhiQvIcLd6Xj5pVJijW7oFhCG8YIbeuNL +1S1xjYS0s+Ph1PyvECx3QAEVp198XLYW/lpzScc2UyEl33dYYyDmY7WYFeJEFfqB +1S1xjYS0s+Ph1PyvECx3QA4SoJn7AOxtpfjqgNZKuse1f6hs9YDPNAZYibBrIa8j +1S1xjYS0s+Ph1PyvECx3QM7KWeunR6gW0J8nfZnvMDuWS0Mvgefou0oHZeGySfEF +1S1xjYS0s+Ph1PyvECx3QD5Wzcw7Wi+7zlfkmMhc82uLHFw4ntNZpdN/trE0VOVD +pqOZpxr43erzGhsD276zcP2CN4AcWDfUaoK6HgMB7lgHGOS3pRATdKfBKkW8qtUz +1S1xjYS0s+Ph1PyvECx3QBqKE+KgSKnKVV7UA4OK1QBgeJeihVskhX9TEZhogxvF +1S1xjYS0s+Ph1PyvECx3QG327AjRjrCCDGPwqLhv/F3nSVyIqgWDO5FXeZ4MyXHa +1S1xjYS0s+Ph1PyvECx3QKd7bySDyCSTW77tgemoglo0CbodyK45xUwBPOaZ06I7 +1S1xjYS0s+Ph1PyvECx3QGpIwPsVPwwGsOGVa+rf7iRHUsXvrm89QolnvcriXE0YYS1HDjp+9pMBKHYELFyZjA== +1S1xjYS0s+Ph1PyvECx3QG2QNugDn2QjPlRxEjs/XZx+B0Vi+uhnPkhY/oVFJfMz +1S1xjYS0s+Ph1PyvECx3QG9F1Y5ZUI3n+pcZqmBadIL4M0Zi06cyHuoOABl8lnDY +1S1xjYS0s+Ph1PyvECx3QMe6/E+gLCykds0Xu4IXv5hTQM3ebGFs3rYGkwemSf6+ +1S1xjYS0s+Ph1PyvECx3QGC6nCIPRhi+tE/QlVJPMEyyNKGVn9/g5T9PubLVs2FA +1S1xjYS0s+Ph1PyvECx3QLSoLCvMVmMj0HBq/ILswPXaaDI/EXUucz7cd8lP2Cbh +1S1xjYS0s+Ph1PyvECx3QLfjvJgGgMJh5MrgNPIpplcKtIaTB6tyeYxHlH23tpmz +1S1xjYS0s+Ph1PyvECx3QOBtTdDTg1VFFDmn/oGf3XDh5ieDOWkopzMoE2QeL2k4 +1S1xjYS0s+Ph1PyvECx3QM3onnNj20wwhiQvIcLd6XgyePZQKFRLkDVeCcn4Kmio +1S1xjYS0s+Ph1PyvECx3QAEVp198XLYW/lpzScc2UyGbyoY8NU3UwOieCtXVMo68 +1S1xjYS0s+Ph1PyvECx3QA4SoJn7AOxtpfjqgNZKusfnEc1S/PVTPBILTzOTmaOq +1S1xjYS0s+Ph1PyvECx3QM7KWeunR6gW0J8nfZnvMDuWS0Mvgefou0oHZeGySfEF +1S1xjYS0s+Ph1PyvECx3QD5Wzcw7Wi+7zlfkmMhc82t/YEJwhYkbDGIU7dWs5FjJ +udULpF7ryNYaShAeqif0OcOkhTplELhVJ+d7CksZpXe8CPKYMz/a67N/PPKXVXBq +1S1xjYS0s+Ph1PyvECx3QBqKE+KgSKnKVV7UA4OK1QAAM7hHfpR5grasic5aVZL5 +1S1xjYS0s+Ph1PyvECx3QG327AjRjrCCDGPwqLhv/F0GfbihwfxeZqqoNleh/4PU +1S1xjYS0s+Ph1PyvECx3QKd7bySDyCSTW77tgemoglosLQNn6DUpnn+uR//Ujsty +1S1xjYS0s+Ph1PyvECx3QGpIwPsVPwwGsOGVa+rf7iSrqr7vTEbYdLS/rkx22vCSySK+eIqtcFnARk/Ex4Tbyg== +1S1xjYS0s+Ph1PyvECx3QG2QNugDn2QjPlRxEjs/XZx+B0Vi+uhnPkhY/oVFJfMz +1S1xjYS0s+Ph1PyvECx3QG9F1Y5ZUI3n+pcZqmBadILU2nOGFvQ2zsfkD0teTxYx +1S1xjYS0s+Ph1PyvECx3QMe6/E+gLCykds0Xu4IXv5jhm6GMFj7aukCHqxK5c1w9 +1S1xjYS0s+Ph1PyvECx3QGC6nCIPRhi+tE/QlVJPMEw4M8qQxPQuXT56/bsSrgeA +1S1xjYS0s+Ph1PyvECx3QLSoLCvMVmMj0HBq/ILswPXHva5In99h07zGEhVkoIjv +1S1xjYS0s+Ph1PyvECx3QLfjvJgGgMJh5MrgNPIpplflKcKLSjWkyefJX2R64Dux +1S1xjYS0s+Ph1PyvECx3QOBtTdDTg1VFFDmn/oGf3XAIbXOHJGxlk1XBXM0UOHC9 +1S1xjYS0s+Ph1PyvECx3QM3onnNj20wwhiQvIcLd6XjrdvOt2SDcgCK1YXVwn4J9 +1S1xjYS0s+Ph1PyvECx3QAEVp198XLYW/lpzScc2UyFjWGwzcW3QaEWmgeDoKzMW +1S1xjYS0s+Ph1PyvECx3QA4SoJn7AOxtpfjqgNZKuscHWbXZnAFcJwODPUN4xLSr +1S1xjYS0s+Ph1PyvECx3QM7KWeunR6gW0J8nfZnvMDuWS0Mvgefou0oHZeGySfEF +1S1xjYS0s+Ph1PyvECx3QD5Wzcw7Wi+7zlfkmMhc82tRE88A79jIzI3pO7xQGogf +uVL60onZgepybsBGUcM1bEXzN9+n44mWAaWEboxr0c+9UfQYgLy9PXHi6Vs/Cqs4 +1S1xjYS0s+Ph1PyvECx3QBqKE+KgSKnKVV7UA4OK1QBWv3Za4nrH5U4sal5lcCQz +1S1xjYS0s+Ph1PyvECx3QG327AjRjrCCDGPwqLhv/F3YqRKoF+F2hI7q7kTgc0QU +1S1xjYS0s+Ph1PyvECx3QKd7bySDyCSTW77tgemoglrybGB+bctx6705qkqdu/EI +1S1xjYS0s+Ph1PyvECx3QGpIwPsVPwwGsOGVa+rf7iTTu1YD6ABDbk3ueQr5ZX6KWzzDX/kmABUKAcTgG8cQVQ== +1S1xjYS0s+Ph1PyvECx3QG2QNugDn2QjPlRxEjs/XZx+B0Vi+uhnPkhY/oVFJfMz +1S1xjYS0s+Ph1PyvECx3QG9F1Y5ZUI3n+pcZqmBadIJ+2GYPgoGRbQTuUVGm863U +1S1xjYS0s+Ph1PyvECx3QMe6/E+gLCykds0Xu4IXv5gefwMMxbXDYICJe0L0kGmc +1S1xjYS0s+Ph1PyvECx3QGC6nCIPRhi+tE/QlVJPMExtwGmKtdyCA2xV9is/a2XX +1S1xjYS0s+Ph1PyvECx3QLSoLCvMVmMj0HBq/ILswPUg/QA+cdnlZVOkrwre0eve +1S1xjYS0s+Ph1PyvECx3QLfjvJgGgMJh5MrgNPIppld+yNSmmPJKnu7aA2xjOjct +1S1xjYS0s+Ph1PyvECx3QOBtTdDTg1VFFDmn/oGf3XAdLLSNje3O+1BkZC176R8O +1S1xjYS0s+Ph1PyvECx3QM3onnNj20wwhiQvIcLd6Xhl+0P8le1nr0Foa96vdbN0 +1S1xjYS0s+Ph1PyvECx3QAEVp198XLYW/lpzScc2UyFjWGwzcW3QaEWmgeDoKzMW +1S1xjYS0s+Ph1PyvECx3QA4SoJn7AOxtpfjqgNZKusfHlQYhu6ZIoYIrG4uI5vqw +1S1xjYS0s+Ph1PyvECx3QM7KWeunR6gW0J8nfZnvMDuWS0Mvgefou0oHZeGySfEF +1S1xjYS0s+Ph1PyvECx3QD5Wzcw7Wi+7zlfkmMhc82ttzc31hJ6wgHK5zQ9aeio8 +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBDqt1cQLqKrsvQYIMf651TQ= +IFG43LCpIjO82IA6vnlrfzMReHRbZWQ8dI5L4Jx4xFX82YkE6Hp/QlzSN60BmOl/DjzujgtD7AazYeGTU+ef9Q== +1u+XjG/2+GSQRv6EzCaWRQ== +OI7XXcJXnrIII2GiXN7VOonZx2KmFXV8HHUfygdVqIE= +q0BvNrS1iCae7wPxzcti3nPuymljOYKxSS+j/H2t3+A= +xBiDY2gzO50UCzaSTm05eL8zCGZlJTOzZ31IHnUR2Yg= +WZ0p9nwXkFA1muq3Fk3fAFere6UHZKo/Is+2TI1VyYU= +GkPrcJMZt4MurQQIgQeoOTRN+uesEkSLIkg38Lg4H7A= +WgLyQBjil3em1amUHBXYkr78Vvu8sbcZUY8u2slWL08= +4a3Ec2l4WLyfo1D1AU8OWZekK+O1EJ/ZWb9E+LHCc/w= +QXwQ5GTwiZ22rTbFVwfzNsveilFnj/BZfW7SIVgW/xo= +O09S0GM8fpSDiFbyYHciM4Iv1lqmryVDqhHUavtFdS0= +1BcovXVZrWFlgzWmVAN9LymlH++i2PjeHe3Gm1MJevY= +yPXQlut/XebodovHtXmIWJNwUiITQb8IuDkVAosmX3o= +EsijZB03SsL8d5BUzE5XOVumuQPWNBWG+ZsvMWCHGdQ= +NZ+dhEPsNw4VvBoRDlz2vEzmhmwxfuhe+7PLpGyxRqc= +45NcT2PdBV9V+6rOpgsfVhQ3S1HrhnlFcF85dwAH+bE= +NfdjIB2SoHREnA/OxPPhyDk07RXCe6xz9yAr2saEOBU= +iFbeB1kOlkZ9ppCgIEEjq6mXkNvFQ3gWnxOkscPDPLc= +1u+XjG/2+GSQRv6EzCaWRQ== +IFG43LCpIjO82IA6vnlrfzMReHRbZWQ8dI5L4Jx4xFX82YkE6Hp/QlzSN60BmOl/DjzujgtD7AazYeGTU+ef9Q== +1u+XjG/2+GSQRv6EzCaWRQ== +vklqNFXBTCz+emXAvQbqCjdTrbXsYerJAcQSTOmZEiuQQO16qJ96/L6MEXk69iIs/s/z62TG1LiTTUmSf7NxBjIwppBMR395oNKnc3w4/RDU/Xutgd5YjaT/rzkqqI6Y64VJrcd4s4L5FJfxk6kp/g== +MErXDftQ+xr/u1OkdBBq1BIa4JKCHyvCbz3I9Kxh8Enua63tr8weBf6Z2HCAoqc0dPeq+VpmLSBXxxDnZYPtYxH+wWaP3Av3dYWi8XeKKQRif5BkAhl/RPCZVKRfyOXvexIKvuVgGCJPusaLGpibmw== +AVUNgqiRCLFrtq3+4Kn7wwfbLaZ8P6z8nlnRlIb9aITJ2gLbVZz6FHc4SbHHUpF8IDIdONoBiIdSXsNGI8BdbQCTxoGdB+96fGfrpJd85W9VjXs5ttw/iTqTnsD0SKy+xHoJG7jGenhUhEi0b/FJcA== +yiPmCGIpg0oISpLv9VnQ/W5FzPf609jJGm1QRe9nZu0= +/SNeT522SVGIBtw6IVssOI9bSU9tOMBOjmZhbynDLzWSBlTXwgWGjTORytYt+zp7HlVNY8P8t6CVbjVrnUes+uax9LdanugfBIqd6y8FUZCEBSrVzL1zsJPWC0wtoj1o +1u+XjG/2+GSQRv6EzCaWRQ== +FPr8PM9GCeXhiW+GMolpg+w+AXb9/K7D1HbxkEjhLAWugcs3/N0Qvl9Gz3mYbBRv +/0ULpLqgTvInFD0r5hHANuNzaA1vC2kIeLZ34++4hCWSsKig58IDJlQdSGXBrWIl +1u+XjG/2+GSQRv6EzCaWRQ== +4zBt+EL5WSFbHsGmdOu5qsGueq5ernEwjO8s0U4z/5oY7CEMjK3XuEb1rJ28SktS +32pdC9DD05OE2l0oXazDFGl7O4fHxPWEbSR4z08KD1UXw9CakW5mzLN7EbtRHIJ9VB6IMXkiXYFQtXLBHtdlLFj7RvW/7LrlSchK11cTRn8= +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0aC4uUZdlLB2twSbs9qonY7D5PAxRlyelpbvDMX+ES1fHsPCm0XUzzs04SnkU8Fe5zdJRkWIxVX9rHx4DfwayrQ= +1u+XjG/2+GSQRv6EzCaWRQ== +4+wKB+Tel1iow8au3cOIJTXxOTIBsTZ98UgT9I4N6WAjwpeOWnbO4P4GElShXJSE9GddxjXrR8uwTYH7CCuPPQ== +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBP1yHXNm1SKt6FEfia7PkLl+YIrFxWTKar/SWOyud2SDyEGxxoa27ylYfHmG2oiQMA== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgb77l0Z6frIZyDRbHki5n3bxHQrtmQQiRJ33iBCyc9yzXFd0Tp8ZWE5qw1HmC9GduA== +1u+XjG/2+GSQRv6EzCaWRQ== +MIoXGNzn9yHKdgBbrjqXHMxDbCEWd78y6VoFbKknTJM= +IhwiQ6c7kIg/TFydBot2SO+Hb+O3YgDGtHs2mT+94gO0C89VvB/zyIVkI/MWCMSdpcIjZf312e/4Fwst+C8AjQ== +ojcUvW+Z2ehEJ6yMJpmY+365uxDMHhvYP1+9nY+XIpRmNjEB4/EnbEzWomGDfrkK +AKR+OhMsr76k2SU3CFoUdQCld9mYBwHKY4bTjN1twhsOpMQeg7+RtGd77Ky7gtODMsDAYRe8/pDPPxQP8zwS8g== +1u+XjG/2+GSQRv6EzCaWRQ== +ojcUvW+Z2ehEJ6yMJpmY+9rwchSyxh2jERaT/W2pGwE= +Y1nYwlj45GWxs+Tt8B+LmYxBMp2B2z+cYxVQuDl2gZaAP+0FdPNHv9CT4GDs3QuY5KBczdRuIrVwzLeJP96o+Q== +1u+XjG/2+GSQRv6EzCaWRQ== +ojcUvW+Z2ehEJ6yMJpmY+9MoFwyHPJNw4Ww50lDl5A1mBJqkFjopAZGXuWRoOiZRHk7VIG1mXh/DNm4dhl1Wdw== +geMiRxLD+RL5sRJPNAhMYJfabWpMBq5Uma/RAbghuSyFZesyYu9qZWtCJqDqODnv +1u+XjG/2+GSQRv6EzCaWRQ== +luZ90593aV7WhjMgeAQMJ6VREYXnHz2s9POji2NJwFKKdiLYHo385XTo97dmhGA8 +dbeoYmi2AyUYPPKRkPUUOW9rg3JuFtKNeXRQIhTW7PmpiDQD7ZSXvMqAgENABgSO +1u+XjG/2+GSQRv6EzCaWRQ== +4TatgresHQvhPdvLpNRT9/7uxiwpA1D44/S/bAXIZlFCwEZQNwN+YyJHFkulyk0o +luZ90593aV7WhjMgeAQMJ3NoZiLqUCw2/VNZl5QI0tqsZL6yqK8C3YiPXN2aAcd+GlOs9jK0b5Kp82ds//z2Fw== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0a3HoxgAwEO6mZx0EkNLX8SJgZi4w4QwsQaSaGMJ4oODSbJg7pAxBQOQeZbTJwApVO2IbyIkaA9CkK8VB5zXBfn2G5LFz0UA0D4K4IL+GztU +1u+XjG/2+GSQRv6EzCaWRQ== +ojcUvW+Z2ehEJ6yMJpmY+7GcfzGZWBHKqwbNDjqpt4K3JzVNp9HV1PSLhb+aFmttSqpuCyO0fsTgY4mv93/3hg== +NMi8UsFw86NN/FccVttHUaORN1u2j1crkMWlROFkJaA+EpMIaWrdpW9z/JUhEoQVF4a9gS9C2IcrEguNYs7/RLbU62wYCbZjWjT3hWZKGfNQ9pJQvjJYBBh8UenijobZ +wgR07xfoapmx6eEnFHXXYk5FrJ2FLX4JhQ064+WlI5TqgJcmaXxaJcDhp0DNqQ+0 +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0ePjvkTlFnwXtvFn7E2pyDz480edL6ye4sMejwvcjVa/OjS42529tdC96Fz/mBJuNbC/f3fRiewSGJzL+zR2mkc= +1u+XjG/2+GSQRv6EzCaWRQ== +Zz7V+a7CTH+8kewJI1afPyTaVYrCHeEVhqALbi8MB4PdT9Okdpgq/m33yXST5aNS +1u+XjG/2+GSQRv6EzCaWRQ== +luZ90593aV7WhjMgeAQMJ3I1QogDsz/uwbSX3gm6LcirQCC0YJtYrhAspLOlJWh6 +SPX4u9u/0KH75hz32JgtQHXTh69bfcvrs+87KM/53MO/s0BOvorhq82N2mtr78IhPw6f+zKNqrlAEK3bOSa58sFkzCdoIv0i7pw90AxEt8zWd1d+DdZBt8A0NlxMOyTk +l2FJPs4YkAmmok1ulDRuSA== +SPX4u9u/0KH75hz32JgtQNgRHAD1u3yTwQTCzUH08EXcDkWzTO2EYQdrGmWi5vd1LuKLjM6EZFJgf4zR2Q+Ebg== +1u+XjG/2+GSQRv6EzCaWRQ== +IhuisKim47k91RVt8z8qtD7kriuPGqp4ckZ30RGBSw+AuTXuDldOdIaOIUn2Ii3W +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgVmdjPMlUk6mZmWovLLUEYZ+ZqXNh+YvK0HwFE+YrA0DIE8fPEodMfNYCsaU55nRvrunxPFh+DTCk4lxM9NY1uc= +1u+XjG/2+GSQRv6EzCaWRQ== +onAPAt0fHSY2gF8A/EW/pXkZ8u0x++PYItwapADNfJ0= +Fymfc54sieZHZmkEhOshrYIKO69Se9tgbZ1kRKOJz8iN71getnpQBtz7GMQTYSkO93NjK6fZaOh3U0Ty//zX3OO4uGMnZwcAAJ0mq9h6Tgx/D5PGSULJ1QEDvjy6CIeJvKyAyfgi2GVgVkRFBV4n4Q== +uLdHavPlpyoTtfi79NzuD4irZaJjSvR77u3FTdek4+gf/r3Ust+vUo/FyRwrMbzTw40YptpFVZofYcQRjLKvBZIjaqdKp3NyvnhCoRQrdXc= +r9s+exzBpvjvlZprTESsD842BwePoNeX7dpITypqUq9x37NRdrY4VhG8e090xeTu +vF6cvI2zz3yfLrz/L6DzRhQ6a2ytaNSIU/GDWts2+oElRlMqO5XQIuma6td47Y4usUpSeChf1aq0cHCcS3BzUQ== +1u+XjG/2+GSQRv6EzCaWRQ== +2yQsUep5WrV3T3htNZ1L8TIohok8dxSGY22+WeWcmQzlauL17DyWRvW+qqAHUI9z +3PYuJCOiZH1sjGKjxSVU6JzQUvtdJ+VVWARI44QMBGlh/gUtgUeUNGH3r4EubhzNR+5hibzjxp2yDGUd527b+g== +1u+XjG/2+GSQRv6EzCaWRQ== +mDXWO5b8TJxjEYL777wpQTYZo2Ga1uh5pSM07bzJNv22/DAgU3WGlSs/NOE6bPUlef2NRRZvcHbXa3zq+wJ9+RrXqccmcIN5DO+ZXKKEI5g= +PgWbOuBwavQIqRuZzZE8k7Uine4Ykoj/jwONh9/Ai5+iwjYA+vZPiIwBOqr0rZLsr3qVtFh+JfYh8BhAZyyKWb5vVWBX0yid4ICiQD1Adfg= +a6zIn83aERAna8E909JSBDOCWX/Dz1m2VNy56hiOHudLJ7i2tbYRZ0/VNjFjTl1B7WMOshHHxt8a++/G8NazvmTFJKk1ySb2t1L9XIohmKo= +1u+XjG/2+GSQRv6EzCaWRQ== +ERFPcpFJ5x6x1iUKDQh3uhV6XHEL2RzE+8jUFszzqrcEqnovXY8cqnrdhcaKMKYN +32pdC9DD05OE2l0oXazDFLDC8Y3XJ6Q+kvlM4fAob6pFoskCLn1bRmvXqvEFDrxfBcWK+T3LN+Nx7bPw4vXdahQzHmvKsZDhr+NQmTBO7TDMZVc9ES+1nTTgE5Qm2XLQLwDcAnmG5rdXqi52ZAInPw== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0WC8kW3bE+2X+BQZ2qWFR2dblcp0jQihBxk5XETTmJmUhJqxiw+mAczruhVhUDly7w== +1u+XjG/2+GSQRv6EzCaWRQ== +TOmaqtOsYFL5TNLmZ8scYxpit/p0HllL53yegHPETk+yqAho1/9kMv4GjYiI4pQ9 +32pdC9DD05OE2l0oXazDFLDC8Y3XJ6Q+kvlM4fAob6pFoskCLn1bRmvXqvEFDrxf96ZdQNyJo6/ZF8ohSNGFDk2JcwLqk7DuaW/UuAC6BAgJY8mYx+uvQ5eyuHinF3rIACeW0CH+RnQElp6pWLdLEw== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0WC8kW3bE+2X+BQZ2qWFR2eA2eQ3Z+GaqEIqQNMwYIPX/GEQJ9XdGLtsD4r5G2llaA== +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYo4se8cEG3Kpe+KryARC+5/s+COk9MOO9VM3V7stcJr5 +32pdC9DD05OE2l0oXazDFLDC8Y3XJ6Q+kvlM4fAob6pFoskCLn1bRmvXqvEFDrxfRNrLULHAYnnf2hHerDGSYZLJJjKH94XJw6LAvIuCId6YAdyiiU8q0W1PZ7naMtjoPAwls8myIpRZdqBmZpFiqQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0WC8kW3bE+2X+BQZ2qWFR2fdfGk66PDxQDgTN0Urx1EjJNyfDnMCtPFIgDzeFRiGAQ== +1u+XjG/2+GSQRv6EzCaWRQ== +yXqkSqtRGSEMRZqQHZ1+mrgtQzra/P1V601MdoqkoAU= +1u+XjG/2+GSQRv6EzCaWRQ== +IluO19DOYseHfsOaGSZp+fJ8N5g+OaC8b7k3ympQvaGMGyhiyfwWR2pzsuqqwUZA +tMFpb/D+AxluSZnCdwhYWtSYJY9GbZxtoaNwiPkdykyV3LNmni+sCGKEkOL/T5rOJyanyEzLH2GSOCw+5lJ7yn0RzOXTLrxGgNC/4vn3PQnbN6Yo2gZqiHC2D23cy+mm +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBOxfDPv1caf3MVW0/bzicu2a0aeg/x6WnWH2wJJTytV56y8+0TcVJEarc5ZmcTxqTUswmEV7vETpOqfgqSDkC1g= +1u+XjG/2+GSQRv6EzCaWRQ== +neG2ZqmysBKy0DmUM70J4lIP33D1GfhPBL+Wg7KQ2BkDE77E/2K5WGxQnOu8m/A0uBqEBzSQfFFEF296r40wYQ== +s8oZlk3kc0FacCBDAb+rTqYD2zBXMxp249h0ThifeozwWhcQz5oX2ByAR/Ez25cu +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYrQR/7nBKlFU7UIR7Tp7Ask= +32pdC9DD05OE2l0oXazDFLlxeme0qQsW9zSWINEVqdI9GKmjReB/qO89pKloFO9GyhaqMKHcux0qBd5mqGBMhC1FPc4z+4iPVLBHDNXiVjZFzCCdElNlSXNFSFN2liCeH05FIQwSmtBo8JWqN1VpRQ== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/XRRyd9HDiBpo0pCt8PUySBg== +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBORQpeyOGH5rhd8/0ewNMCSTxsv0lluP3y2Z5wRzuZT8WIMCWlFvQ7XQFC2SdLlTjj/QLLHfImFlZdb1mlvLeQaCw+q2t86+41nAQ+wyhHkx +1u+XjG/2+GSQRv6EzCaWRQ== +a6zIn83aERAna8E909JSBP8pJQPtlYRRn6plgZBpZpB2oPOV5cTcii5VWlLmobX4Q0zFUDXFwHxhoo5Nh2SRFNSWjqRtdyWKFPTKK+QMkco= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgQG1+/Qgep0fT2He09pY+Ml5uk0cFWikdszqoM55EKQl/fYItdN7Zxh71psNj3mAYQ== +1u+XjG/2+GSQRv6EzCaWRQ== +H41vcsdfxPVk/6oDUlZ/BP3M0lFxno+/zrBHSB0fPms= +fJv4WNxy/gQMuHmvLnp8VvQ8fmE2jG3iJrlz1TKSIBpR4GPKJEGtMqpg8U37TStq49Gh9wRLaRotwvq4t+lIJw== +CDPfAm/EwLePrkOZgdeVYKOrmGQELqLsNAmExyF30rk3tOkMH/Bx1x83Ad3+GZ+s +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYgDVslGW6Tjip0lox1J5sqlBCynPg4r4cj8czqxp4Bct +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0d+RcKVoSGK0peBx39cFmtm116OHDpWP/LXfzehL0ORTHy0Jc+S5tSD5KxfmCBWM22Yc7rvXvWIYHJmEZmQZiCgivw/eVt2HwE4LuwoyqBOR +1u+XjG/2+GSQRv6EzCaWRQ== +mEOKzB0uJs35kZrdrKxIkFIbyleq3BbZR6kwmxMNH4lw6SxiWS+B8nQJYJz8JPZtth3mCpnpihp5L0FHci20M9iHXhCRv6yMxGIVQuJTbKdJMyaTwFnhX56XpimqN+q676kxoCGLvrNU0WbKrccIt34Ybx7DVbGxo6fX57CsE1RPwn27uZioxigyFxtlJDU2 +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkvCs01Ef75uP/SBKZUAi7QcIuWcW1HaUBcTUGockc85sxrSAaaq/5LrID+23fcnDrEBsJnoL2CVi/OL9fwEGw6veGthGn92N+wSctGOfX5IOg== +1u+XjG/2+GSQRv6EzCaWRQ== +IhuisKim47k91RVt8z8qtDZn3vSHi6GdokkJW4MxFlElWlnCrzThXPwU4IrYTTIdSSBgyo5OOR4P9Q95RqcUi5B1sADy9cu8D8QbTfdvkpb9lSElhxX4hqutFbyXQMH59ADrKCoreTdIq97TZ3dizw== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgcbEldxW1qTzCmNrnlq1LxOgyzyEButiX8Ugd2H56CDJjUIyDKGL0d6LGLmS8vaEQg== +1u+XjG/2+GSQRv6EzCaWRQ== +EeZmdYErF7AflKJCqxtKMg6b0pGdD0QOT6/Z1g3jHeWtqM/on8V8danCl00U/Chf +YuZBUDBm4rTV8T4XSN+9AasKJpYeJR+KM6McfNGy0UHUK0cFzIeHUrrnUOZ9peToPNnIkhDU+1ZM/SF78o/qi+OD4p7IkMFASQdI8m37bx++GD4f2zw+KFa8ieYBQwugdZpuOWst0VsDWlazVBSkaZxQRWZcxHnxOdKThxJQHyE= +kxpI+i2rKAyojfy7HFEIC0EQy1ABCZ0vy7+ga7nuf9dfVcMIkye3AkL4fVw8LXP4o3xcxn1l8Vsme4OXHaJ5r6kP0dTfux62uf8C3og4ewB0eze/S7A+r69X7DtiOmnT +kxpI+i2rKAyojfy7HFEICw/FXPQUMGkENEmBxT5Hi+ny5yboBcFJUVRsJQdwRZoedAZRQ4eajCDLNVOioICfup+IRbn9sNPir8Uy/gxK3PZ3XWRbCnBEKGXlkEQRBWr15aCLve20NB5cuonuSFfsvA== +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9lPOP0lY7q4hvohNcUtsqtEL932oLK/jp/Tjh617JoAz +wgR07xfoapmx6eEnFHXXYsBNzI8ySI29Nw64a94cEffIMV9wqgCfiQ9HYQJJ8aonAXh5PVq6JW8YOuwuAORGtbyWXhvDh9hSJzYZ7nCJ/C4TQVxgMX6LVTbXtz2TmFAaDlhTXv6NlfSjiAOfW2yV5Q== +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0YjBKrjsytEwU6dzdcOfBC4t6wbBVvK6nrDz9aCSeQG9LGyvyFv7Lorrh6Y5jVjXMNUF83wusFlVPk1GiDYR0u7ytascDktrk/gNDKEAdb/oAExojz2xd3kV3kSkyJE+ammVUEIgpQniFH8yQb6xZefmpRjy5gxraclTMQd9uAIOF+YqOb2J9YbU4xy/3GsBTQ== +1u+XjG/2+GSQRv6EzCaWRQ== +91gsC2HsVrvTm3cwuG3h9hkj/X8VjpYDw5TnTmbutMcrO6FCN8mCfAgz+L96d169 +WHkOzVx7seuLmxs5Hu+l/vuirCks0EHZBBXVgwpgabkiMw9aNr1FMS9nozRa2SKSmJqPScuD/h7i0EBMBfgcJ5IGDIHEO/Y6IFk4Jrxsj75Gt0YslCm6kw4ZrbQnEuOz +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0Z4Oar5wqYQDTCRptdPgFxPsPsjN2AeFbeXlACBjPzgSKLxtrs9M9BQMLKBxvBMoHsNi4yxx3hO61hwwv//nWiOKzcWp1v/e6qREqP/7sC1l4TcrylyxgBZtdtYBg+WAPJg5vMTeQ2fFzVYbxkYEG8AFNnLGaE9AuqXpVc7Mfd7Ys+D4OdP3srGBvrYpz7TPR72XnrwBk3bLGtD1WPigJEg= +1u+XjG/2+GSQRv6EzCaWRQ== ++pBWk5LxptzizUTsHnZAz99i/5vNWptrZ9p5u7vIcfi5qEc39vhVrczSNIQsTzd1 +1hs1FfIPISXmd7TJ/r+Vl0ubuLMfXeCOli9jAjGpnrrwPm63h+EdkpPu7CLcQhV5pYZQDPB+VG5U+OMuKfxrhQ== +VmVrGQo2zRokW/ZuO9bN68/aqwLZuBDDNiLmObYqCwz/LzioFoJlCT3GV6dGDP1i +1u+XjG/2+GSQRv6EzCaWRQ== +/R3RsIyyuJzgF1BbtmZTiH7qhWhAIqUT5M07bHjDIBE= +CAhn8dRUItEbEErp4w+lXzHj0KEav7R82AXYdQaL+QYlq4rbQkgbntuj6ycJvwzBWjhu0u6EUDd0C8TmJFMu9Q== +zp9fjHH16jzBIgtF28Da2YOvmdawqMDTu2Bh9h68R4Q= +zp9fjHH16jzBIgtF28Da2c+aU+2QxcgoX54n6QYh+lU= +zp9fjHH16jzBIgtF28Da2fr07paxMrC1d5oLLJXG+5JKMR1JpjhnDFe8g5XNxTq4 +zp9fjHH16jzBIgtF28Da2WsyLCM8SyPHcLf1wAy4XRw= +7CO/lzuS2S8MDvGd4S4S/IalBviO8MTV90p8lL14VHHdMO0ff1TwO/CAycdFE4ty +KCyhk8I+KaKyBa22YL+N2gjVwOTdvm1sKS8fP2tGIbtMS1Cl8j41UTUDANPSh5st +4PrWX+3jeUMWuxDrEA3D6EpOcgYGLQWMBkB4xqBeMa60vwyyu83vzV2wBlCR8C/wXXPpBxTDiEFM0eLimSCGJg== +/j0OZS3RkHNaeh863pRRt+dpqitBk3unrtRVwXgYOpQyAj3sdeq1SKPCtvLlK8Gfqy/ydRlZXM6vaSmiVAItHw== +8y1tZpQcUJ+ePSQFq/eV+sEM3xfHLTKbvzwwOnsLzCca2azkEbA/Utw1HsXImScL +KCyhk8I+KaKyBa22YL+N2o6VCfA74hipUsC+qIzvz6soMQoC61Txij2ap8GOzEvT +4PrWX+3jeUMWuxDrEA3D6IFlj30nx90db/NfTrCW68Mlw20waiTKadYnGV3D51zcrlUnEs3mRL7px4YA1VmSdw== +/j0OZS3RkHNaeh863pRRt08PgfIvDTeMRf1H8Ka+mVrBtz249BhURkPXWaEQp9/qGp9dLpdlFp7YOptdFLQzFg== +8y1tZpQcUJ+ePSQFq/eV+jLexRGdMoIwl174I7ydZ3azFShSloNvfN3zE3cBaRIpSq/zN1ACcvNiVs6X9y+iRQ== +cJ1Wblh8gF3p5U3Kb/RGlQ== +1u+XjG/2+GSQRv6EzCaWRQ== +NTRv89rZA7DPzi76uqJdbzXOdgBgNzT6QDUOzrlbuz1gJAX81O/dl+LZ367aJ2hBsaHQ7TXAtxkV10yvImGVGxEjk1f755R9btlslEeQTI4= ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +KP52Q/5db8uomsPaNzzcfBn0Wjgc5Vn7k816BhiOEa0yQxZGpiMFcg2VAxUiuXWPWQwoEFWgWLe1IyDs5XNGiS2hWBSj4zXhvrDP3L+5Bv5rP1o+9rur4BaQfMunkW4lta28xCCqzPexjJzZTKAm9K5fp0zDeB0U+HOA0qZg+W7U3UyNf0h+XI4pESiSFYHc1awlgF9IR1Ew9Tsmn1nxIw== +91gsC2HsVrvTm3cwuG3h9i1NEZiWQbUWUf1I+2XxcOPyVXXChizlVctse2YyhkT7 +wgR07xfoapmx6eEnFHXXYsBNzI8ySI29Nw64a94cEffXW8F+CU/im35npnABgsjX +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkuoFr7qnY1fGt33XKYP4P/X/VyoP20fBYr9NZA/eWwI0RPGbYy2dR0G/Doe39YMmgG+mU3AETWDI+tP+t0yh6/IpeakQOiKDz2cqZHFypPSAk+qITC9qWXZh1H86dCZl0s= +1u+XjG/2+GSQRv6EzCaWRQ== +i/zCPf9Qs/+kk7v5QupM9ktzU/pxIPa+5LXi+xSzGQU= +1u+XjG/2+GSQRv6EzCaWRQ== +DTYhmGovF1cVlw5xWr0CoXOCjpxAh7QjCRjwCK4S6+4gt/e+HT30mXhkurFOI4xR +CdQuMPSL2hJUsDHDyZQSpR71yvB0E0H6vD8WVTIil6g= +7CO/lzuS2S8MDvGd4S4S/IvwnfFqwr7s1TxB+WyG8fw= +zp9fjHH16jzBIgtF28Da2eyKqanv7EW9dVHpDUtfa5E= +zp9fjHH16jzBIgtF28Da2R2Utw2iwv0Zb6OWswkn4zM= +zp9fjHH16jzBIgtF28Da2d8O3Pnb/er/sFLCDaflPFk= +zp9fjHH16jzBIgtF28Da2eGSS7cf64kFJT4RQ07PuAA= +zp9fjHH16jzBIgtF28Da2X0uyMOo9W7KQx+3luRcZaA= +zp9fjHH16jzBIgtF28Da2aGciHU2TRzJEvNcgmYe1Yw= +KCyhk8I+KaKyBa22YL+N2pQHbunP9syXmbPopwYYfuc= +4PrWX+3jeUMWuxDrEA3D6NHHMBm8HBRXBpqA0M+ngPg= +/j0OZS3RkHNaeh863pRRt28YLyhV6ospqk1aNo78Pn0= +8y1tZpQcUJ+ePSQFq/eV+vn97szSzdj2V+8ZMiOcsvs= +KCyhk8I+KaKyBa22YL+N2sHzTUvM7KwB0xiWYAMrC7M= +4PrWX+3jeUMWuxDrEA3D6PwUG9XzlT5dHAqiTmtYb9Se1LCHQVlYujfLkVqOUKTQ +/j0OZS3RkHNaeh863pRRt2ywOO/Y4uWpogYIf3a9cgE= +8y1tZpQcUJ+ePSQFq/eV+kVi8e+9+Thk3cM/Z/bzq1Y= +uumnoU2LrNuffNNOaLzz8BKARJb9Kn15vhyiYohdTx4= +axxUqcdVGr1JJnerFS5k2Wtj33XEqOslTB7ROcUmkyY= +744+BgzK3LVsSzMxY5sSrw== +1u+XjG/2+GSQRv6EzCaWRQ== +gMD/odU6bw4q71U7vnM8p43WvHcvPK4XqMf03RJyBFw= +kxpI+i2rKAyojfy7HFEICxc1rwUXvGMiicmWBwCBm50= +1u+XjG/2+GSQRv6EzCaWRQ== +6IBX/NbikFfeTGpcH6P73cx5TNBfxE8Rflj/2ckzt6NTYpxGjCBXImPVgszH7a/V +RuNblYXcNMm2CK4ud0d7WsIPxNHzHI2me6wjkWQM8rx/mupZmcoMoTNGwZMxUZ3j +4uNFnuZO7ryRU8xLndWWZ2ESRPOipJWPGA7A97qnJZEOvhxml7IRLVJqED5v3VpD +1u+XjG/2+GSQRv6EzCaWRQ== +kMiP2L0bhoHJnBv2fGeU0aETkJ5D8MfNd0uuMJ629cthy7dL2tCVGiMmkYQHZ+g5 +hUBmVW1yXNp5HDi8AdqadAriG38dOuVpam0yMcQYhHg46E/bIt2iIkSDAfuSuUbkueEOaCQwYHsu+Uwr52/pyhN3YvzF18C/oIQKg3gquP0= +hUBmVW1yXNp5HDi8AdqadAmxe96DWEWEjMtViLMzPGEYmE9eFmnZZnKWIhfIX+St +1u+XjG/2+GSQRv6EzCaWRQ== +fZFC9ntdS82BL3LhVXodWOW+PK36r48XyLFmqg7XRuJTamyT4wsP/4JjobBfTKfs +OerrFZmSJ9J50EZUqnQsfaK4B4BPUg0/kOi4Pzd+MXnnO/Qs0dKGSyktcDxL6uKTIh2N8zQ/gAY9DCc8vpFPEw== +1u+XjG/2+GSQRv6EzCaWRQ== +b93MO6Z2suiz1Fjm60rjIwvt6Pnl+Bt6G/961J/ZSfLQogWE8pEQj6ItEIcK740p ++pBWk5LxptzizUTsHnZAz99i/5vNWptrZ9p5u7vIcfjWNgTSYjncObhYz2uB45Mx +zXhoKyuNaw2yhNT+M+EnM9hSZ+9RY1V7jjGxZ3LEJGE= +wlLHv6kT3Q/RmtMBN4nDAXW1lDvH/Dt9OaSyalc22mM= +VmVrGQo2zRokW/ZuO9bN6+le2yBw+Q1tvVBKJnyyii1uo7OVGnioZ6XV3GUfagGq +VmVrGQo2zRokW/ZuO9bN6zxIfAqaAztckaecNx3iwpgczmxDHOlL9zGuSohYhWyNXJOPY0ihVIw636OyelHv+A== +VmVrGQo2zRokW/ZuO9bN63NQDSoOf1fHuK5xTuU+C1YFeUqeWEpg5FeoE44mF0vhtUAT5SX/7FB8qPCddnWg9w== +VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY= +VmVrGQo2zRokW/ZuO9bN6yG1lGH/32At5mNyjtgpBJsy3atwNxad12gy4FY0QyLt +VmVrGQo2zRokW/ZuO9bN6/2uJitF9IbwWElkXfhIj90rIpRTgCV5/2wWbbt/RNwMLCl8UcVXq3ZlnX2vgWA4ag== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN60VMFs6YNEgtSc/wRLv43YqqU7U8RVHzwVeXYJ1DZZmo +VmVrGQo2zRokW/ZuO9bN69W/qVVVHiwit5fvkuVU6IT9YdjS+s/PaXgJ0VAS6WK6WUyqjBPDAtCEehVPSiGMfw== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN63Z9zmLIE92FM7w5Lo6V45EShP4cuLfp9c8qZhUzqrCfOheFFriTIVriVXTOpXJUZg== +VmVrGQo2zRokW/ZuO9bN6wxZiVPiC/XiYv1gwQ0iYp2OSTMIXua53iaLx02LpOwefirT/l0AlzQM0Psoj/SPLg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64auVdecQEHLb+XuKkuiiVSm/WO0WaJO5XuXtDtQwUSR +VmVrGQo2zRokW/ZuO9bN6//e8iZcVVdZNKq5EcFKJJU= +1u+XjG/2+GSQRv6EzCaWRQ== +o1msixA+MDtPVXFYKZeSXCMnz2H3hhFkmVSwecnXMkIKcjH13RpDhYsEv3B9ZR07 +McSfMGbGDseDL9zZOM/2UnQRv5JGc1OPARlS31Mv16vw3qCKjvaLABYDLBpmy276 +1u+XjG/2+GSQRv6EzCaWRQ== +xBwuQlkMmm42MK+w1NUK8vJefzT9BJUeJCAIu7z3mg0= +aM6iHfRmEebzKIflcWS+OhHFOnACt62ZxssTET7+24o= +1u+XjG/2+GSQRv6EzCaWRQ== +bjZj6OKTweM5OZURBrTjsObnLWBUl+YceEw1D23vbJc= +VWk88LPTBSz9q4QFIgbDNhRMO2uwT2FY3us1iuTgEsY= +1u+XjG/2+GSQRv6EzCaWRQ== +7Ve/CPYxbRIq/L2vO/1jNW8apRCWnAzslCYX9W5e0jETZHCNQ8Fk6nI+ybOE3Cht +C8mxUpsh4XMtIN+H0SJ4glrglri4/JGM6lWSY9opT90NHbyMdCWvIf4WWqzzrmTT +jEH+E0FSKBqwvo8QKx5Bbc+r84Ib0C+KyPsQ5+s8rnDbzZhuoz9OWGrPWOVuz7JyAhNWEXviUWMymZHycoFrUQ== +nYils6ppgxoVJaY4oukM0W+bHFNl+YUudTDuVTnqvyM= +jEPV0YXDCqWCap/VHplvwQ== +xCSE09sQPuao2+KRKinomkSNGTwhnKwbHmZxiX6Wca8= +VmVrGQo2zRokW/ZuO9bN61QC6t8vqLH+1I4/Oj5z54VmAFtJ7oTCvKbL0Zs9g24O +VmVrGQo2zRokW/ZuO9bN6xrOzovpr9LhhtFOlE/zdBN4iaEhYXMJEpnUzDcxhxes +F9Sftf1PoN5P+lgvc+r10BfxjF9TrM4nZooF1uf3jHRyKk8q88ZSc3l8lSS20qa9 +VmVrGQo2zRokW/ZuO9bN61QC6t8vqLH+1I4/Oj5z54VmAFtJ7oTCvKbL0Zs9g24O +F9Sftf1PoN5P+lgvc+r10OPUzmdzCAkOuP8E6cZUG6Go6KuYU9OF//rNP4Q51eM6 +VmVrGQo2zRokW/ZuO9bN64XuLHX8TC3s3b1uGPe+ujw= +F9Sftf1PoN5P+lgvc+r10DjWtjPZiHJbHB3vxWiN0sBmlHwxOQcu8hDX8WHkhpbz1wQMdmEM+WGDbT3dyclbDA== +VmVrGQo2zRokW/ZuO9bN6639zDKuATNNI1Q9z+5tQonkvQo6FgRE5j7xdzZ42nJ/lORyMCWm/wmnAOaqz92/GQ== +sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q= +VmVrGQo2zRokW/ZuO9bN659qpiza6mT3x4YMO+6u+7sN6/jgn1zAWO8Fa+uepsZ12GMbwXphSbiRomrqpihvqJtjJXmbUczu8tcAfkU1VRGmOTk1tRp7A3GYuk/UIEJpWyvivkPpiXNND8vshaZd5g== +1u+XjG/2+GSQRv6EzCaWRQ== +rWUQK5drq2FAykvoqvdEF6wWd7LjE4kHgkD92yYOWOs= +hAMG91uEKNfkOfEEFCO+FK0iS+0JGy5+jARrADnDEa32Uwwylxze//eg1DVXCY5UZ3UEnExsn2/G7vh8hVbTiiy7fbfiULVIcxktbE4qc2I= +VmVrGQo2zRokW/ZuO9bN67R1DwALFuRG0kM6YcDyBBen891nAny/jFMWmWnUs5Xk +VmVrGQo2zRokW/ZuO9bN64vCg92/s/g+NlYhCZamCQQ= +VmVrGQo2zRokW/ZuO9bN6+fwD8q80qE6VL1CccMoSNmddfkxATao7BBHZ23dmm+dVGm8NaGh/D9nozgZwu6iSYbwk0n9GtiCOjFHgzFtCpbH3+d0wZKnx4RoydMBeyBbiC4r6YELRejWrnlHc+Ep/G/AZY3P4OKdprYZDVlosTA= +VmVrGQo2zRokW/ZuO9bN64tf15HGxAcSa9z84GJV9y8= +VmVrGQo2zRokW/ZuO9bN6xkIM7I1Zmovmt3Y/W47TxySobbDNMTMXRX8veFk2OcdMhbLUcmcmpgjhM8Na8h2efDuZ9bZTQpu/Rtis2UquulFC7QgLTgzcv1Fhnmv1p7L1oI6cjPZK+PrntXES6YK4A== +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1u+XjG/2+GSQRv6EzCaWRQ== +qWJGUrAouxOI5V0qjzZzNbr8eYFMpkg05uX84QkJkgmKptzv9ua2VKBjG4gtNuCP +pL3S7PtZieqNcOPWW1Wy+Mm7q9BQIc0sQpM7UAUeNTM= +1u+XjG/2+GSQRv6EzCaWRQ== +EwP7AACOVTDx5AWnAeeGJ8tasYoLLkoLzEywVuXVV8ABaGlOEOidBIf2tuEyZkNO +1D18KWr2hdVsBcdZx1OaPcbLFHQ4KhN0pa1s00kniNA= +VmVrGQo2zRokW/ZuO9bN68Qt9FjfzL4A+CvxKkSuuDddZb9mZOEJg2J+h2XvUCxJ3pX6wIF06uaCgRYRBLRJ0Q== +VmVrGQo2zRokW/ZuO9bN65ijqnEyrvIyAVuIqxq4rQePVAVfCKXoOne7xnzZp5/m +VmVrGQo2zRokW/ZuO9bN6+fwD8q80qE6VL1CccMoSNmddfkxATao7BBHZ23dmm+dVGm8NaGh/D9nozgZwu6iSYbwk0n9GtiCOjFHgzFtCpaI3Kw98MklI5pjdt5Q6CIRfgojQbDvoa/J2SE1A6cSVQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69ezyNmZ3pvZTpJb9DT/bjOuH1V3vD5arhSOiaIgRUpcXNAuyHS2zmxk0TSStp2Z3VIy2oVdAkgS3Sqr97fIvEM= +VmVrGQo2zRokW/ZuO9bN63bmR2XEvZ4L8bBhKPo3OQLTqbFd6Ae79tpkaZaBPMho +VmVrGQo2zRokW/ZuO9bN69N1o2p5L/ExX/nsz6f7VFFh4KJh6WnruPKbBhGPrAZ4LF3bWZTGf7ODPGMQS2L2CA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN69eui6a1YbX7OrkBUaCzmLfYn73897Y2o89d9qvD2Buc +VmVrGQo2zRokW/ZuO9bN69r29wuoFWHhjYiqT94OnmuE8uq5TDMbg+TXvfMmjAuymzswJCGcu31956jb3gLsgg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN650Weibqrj1zpZBud8Hh3QBuuR7J0JDAtVE1TX/2/0aP +VmVrGQo2zRokW/ZuO9bN6/gw6K9Dj/fRRsfPo2yr1sO6xMQpow3e6uvawqtUyTvc +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64bUSgVrpXojaBUFhIlsUuUH0OEUdsr2gEPd2WsWDpG7 +VmVrGQo2zRokW/ZuO9bN6+HkCu44TJXLDmAXqHXxhisd/D/DeG+UFYK+iifh9lpclsrbXug8dtoRGGnrw8dWZw4dT7SB8CtduTBEdzchxWSBRZ1pPQ/xlUFTaJYgh4UzxwqXXJ8rB/G8x3gCls3eu0lHYX7/sLGoZK8ZMnwjo2niOIahEUotY9xD/+yc0Etp +VmVrGQo2zRokW/ZuO9bN6/6TnYYUnVY4bZC18wJU6T0= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6113zUljeX9o2ixJP/gnH7LaL606P1ZBF0ApW3FjcwLj +VmVrGQo2zRokW/ZuO9bN6w/uKfdWuN3PSSKvYL7HvZvEZtmIGDE8QXjplmQREyZd5Jt2NWUJ4p1Y7zQL2Rzj5bHnAt5nVS0dmeiNa8rgGSE= +VmVrGQo2zRokW/ZuO9bN6y7t+gxoP0rr0hobxqEQxMV6sMQGQaapwFJ+dMIXisWN +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN68Z27ko+jrFTO2jEAZG6Hd0IKAo0cF5PrRwGYZwZusNh +VmVrGQo2zRokW/ZuO9bN61aNVJHNHezpHwuTzqAZxa3EWynVWi45cMZ4zVY4MdTvgAmiSYU8T2nhMDTBDGpi1xugbM/s23975mmW9xs6yLaXlILHKcdjWYb/LdBQYTllE1868gW2t6qrJoMETwQfLw== +VmVrGQo2zRokW/ZuO9bN65tUAmzXVbhX4oRxj91LK4yLExrPpaMCGO47d/g8kXWh +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN68GJ79NnzQlhumBucX2yu0w7h/u7IIzNGkfI8BE4SJPayPdwml+f7L3rq2aoaAmU2TFvl6W63C4k7u7Wx7Zh2Wc= +VmVrGQo2zRokW/ZuO9bN6xrw6sIykZVlpj5qj21yDJxBELIrBcRTMwtecqgfrvXahlRsktoFJrTTujTkBIAoiw== +VmVrGQo2zRokW/ZuO9bN63tULs45GHnXM3NqslS6LEBz3nyDgu52+Tsitysf0QZf +VmVrGQo2zRokW/ZuO9bN66VrfdQtjHzqiJ8BALUrs1gQLJX1wN9UiEeNoZc8eynMS4OApEC7DwmyjDyZ6LENYQ== +VmVrGQo2zRokW/ZuO9bN66M5iubTg6dStDldTTJLmzOabe5lXsXe8id+7YhUbGvC +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6/bQaA9LJJZW8FYc3p9IPrclO+QBQrl1iB/ZfHDCnfMNK8+HzIy/06dNIxpovmSwSL9ek3VWTeTPeewVJQ0ajis= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN625yrsrHJv0V0Hwm7FOoKjMr8kVSVbN0F1GQPCkOFsLt +VmVrGQo2zRokW/ZuO9bN6283QTUhEash6C7foDlF++oHmO3XQ0iVIG6KVSmKS93o +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmGVADbU4booSzUdXZ4GUEguvg8lJAwadAJSkwxJTBUFA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklTMk1Q7TBsNExASi5wYm4q2NZUngXyA/qnkTRODkcT2rXFwwSzP3pTnp1TCeViSUk= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl1VW212KeLe3VRnhjOZq3zwnR0k+VkoaUIGp7kBt/7yA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm8F5JEe+U2jlpN6wcqTIdB +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN62knPdY6gJAD0N4C9oFG5jjFE5e0Mg9EHaZI0bGTZiTvia+sZP3mgeE7PSkqsMhBCg== +VmVrGQo2zRokW/ZuO9bN63LTkM1jyAeOymc1jas3GbfGMxfBlK4yuq8zmyDwO7H8kgPWvIbT8OxQflnzbQpyWTyNliWGe70Ol2nLxBj/8o614j0YiI1OP6I7dEmJ+cXgCV1F6ZRZ8wjAJGdjkCHNpw== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklzREPECe+UKwV56rejoQ6v +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1u+XjG/2+GSQRv6EzCaWRQ== +rWUQK5drq2FAykvoqvdEF6Nj2zy9yrHRoRHq+fx1eQ3fg+Gjp8wUR8okTW5YANCa +hAMG91uEKNfkOfEEFCO+FHZFLrW1eiEP9aPK/bPVELZugQqC+Ayx5wGrmA7ySAfR +VmVrGQo2zRokW/ZuO9bN6xkIM7I1Zmovmt3Y/W47Txw7vYnaH2F+/diM3U7W9Ag+VB2tiqTKg+iwzMiTXdBhvB27uDbEGVA/s3PoXQKaimqNGqngEdX/9SuOA/QMMdh6cbECLpAf6I2luY/hUBPVnA== +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1u+XjG/2+GSQRv6EzCaWRQ== +7bJ3Uk7aqJsghutxBeStwoenI08pUWSoM4N7sSAouEtjh+QS1U5nuyJhxZiIhpSI +yFgeqfMAgEJgI96oT6TzlCEDLfDNroEDqZFIIp32DMJIxdgcrgg3B9rdn+GxNmdnpPIoMTbNuc1y4s868VOQPfREj2g33+OAiKCGorTI7np4tkgOC4hVOIeCd9zxT2hS +1u+XjG/2+GSQRv6EzCaWRQ== +LGmSsHqujAfmhwNEd6RJ+GmsyXVlQMo4npjoyzQ9KVg= +qBwwXitZ6fOSGblD0bzEzzRm6ns8kU1RkVj81hEHjRE= +YZ5nkj7LXEY8vgx+ejNN3F5X/iIwOnVUJm0i5Gg4NMHLl3e2vTbzlZ8Gb1qqwmF4 +1u+XjG/2+GSQRv6EzCaWRQ== +7Ve/CPYxbRIq/L2vO/1jNQeS0Fo0VpZMr4nUR1PVEumsw9imXKwrNof2G9CctIO2 +9zgnmC3+5N9XK5/NRaR4SMCZ85P2RzwaGK5SA0vBOslCjy9ZmcAs3KQ2NtaSwfQA +EwP7AACOVTDx5AWnAeeGJ2Zsz8LDAHir+G/F6YdEjBz9n0F9mAAuiGt8X314+TfU +1D18KWr2hdVsBcdZx1OaPTohP4zASL7n5IxKaRzFUvefWqcLeMNWReV686Q0FGEM +VmVrGQo2zRokW/ZuO9bN6/vHEEiMOBtCIlg1b8xVuig= +1u+XjG/2+GSQRv6EzCaWRQ== +fsjjo9JMj+4JWdxlwkj1H6sXoRH1lwp8SiB+9r4aFoDyAJj9l69OTq45rHb3H17R +1u+XjG/2+GSQRv6EzCaWRQ== +P1bBdERejTBi3ynjwlj7jzyBMGnK/wy6ECH6atxCqHX+s9CqS5SvzZZ7qXGoPDFb +1V2v8QerKOmubvSxgB4eTJ7mxqKaHgZDk0TCKmDxab/i7UiTimIyWpTvPeWGcxSx +VmVrGQo2zRokW/ZuO9bN6w6Z/TqX6Wl2dt8y89u0Y8A5fvocduAP8ymKlzd7yd2c3l78L1+lgP1PDHuclUTZRg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6+h/Yx5zTOXfG2zCKV0c8HY= +VmVrGQo2zRokW/ZuO9bN65JUC//yK7sSrpv1nCoiFRgR55ff6DKxoPHhk5usJx4ABjdpJH6whWXq6v6x9SZDFt0eN1VSfrHgvSmlgkspxH8nUpgMwAlTKBaQQH6rQTe3dUR5To5zoruJAG/AXkwC1AezUsfJsrbjt6YyCq87Kbo= +VmVrGQo2zRokW/ZuO9bN60Hl0aNCqYkJum1dslT8fcgI2FgNEP77JyWsgMf7Ze0N +VmVrGQo2zRokW/ZuO9bN66rIgoW0pjLA4ZrmefxAT7FEmwWt4kUPVWjixEuCfAhs +VmVrGQo2zRokW/ZuO9bN6zWChYLPlHamJFV5kS2vr8eccn0RUBVPyzIUacZ8cLtx09HLs05RuP3IUPIzFhMi3g== +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN67tavF5vvboi8AuT9uuOo4jN1a0eFrjxRsSgiiE8G9nd +VmVrGQo2zRokW/ZuO9bN65JUC//yK7sSrpv1nCoiFRikDiuqro1UpUyozvyAERApVod95iuaZ3EToM9uBv33irNb/rmenKeTRyzJgCIW4//NdBYd6UYHgv1j8KTQrljmggpfyFTWVa2XukBRipHQhA== +VmVrGQo2zRokW/ZuO9bN60Hl0aNCqYkJum1dslT8fcgI2FgNEP77JyWsgMf7Ze0N +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN65QMABcAGQGqWS2bDDTVP0U= +VmVrGQo2zRokW/ZuO9bN67F9GiVmC3C5V+1yVe9fw/qyhwJ+i0/o+6252tPxa0UZFnD4giSgSuQ0EHrgxn/Bag== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN61aIVCtFWfxi0nFWfro5cfGZmqCuA0Qw8ghbwvTZCMFEQvwlOEHTc0ivdB4oX25nh/wlIxKoovR4X8cI7Nn6w10UTTfw5KeazVcPa6A2plXA +VmVrGQo2zRokW/ZuO9bN68nP/EVKSwLU0SzNmqvOcnjaHw8sK/A3thzVqcqSXyMe +VmVrGQo2zRokW/ZuO9bN6+7S5/1zNa0L7V/u81W4gn/YEoqcJwrT4OSZThBu4rjJfbXGfDcXeKnLQ6pGanw0Ug== +VmVrGQo2zRokW/ZuO9bN6wesJclK6hOBZiYJ2iVGe8bQj9VqOl7yZLKvFfO5uA0ordgKjxk+YGna1voajuWtOQ== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6y7t+gxoP0rr0hobxqEQxMWmZMJeuUJOrzc0ZIHIhLoI +VmVrGQo2zRokW/ZuO9bN65tUAmzXVbhX4oRxj91LK4xkgsmYb7WSC0w0hE0rMDNW +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6yyFuucItNMqkjHU/pLTjeNNW1EGDBzHTHdjw/btxqOf +VmVrGQo2zRokW/ZuO9bN69WmSOt0p25HK+6oDGX0onDmIbjHZglYciw7pSM/Ax0F9PTT7ldUlOW3pMN+8lAPAFV3roUCy/UVmTKzy20yWbs= +VmVrGQo2zRokW/ZuO9bN6yxribns4fsS10IzsnXprQ8cQ3YChjMu1EDJkvreQ8Gtjs7S5CMc+Dwtj1HGJt1TwA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6w+74ARNR31sZhmaCuAMwsnKN/MnD1BKy565H23K8AoA +VmVrGQo2zRokW/ZuO9bN6x0LKap7xve1BUCCCZ/ZM9+B1+2mcJDoFZ1qAcdfRoQmlzIZofQopeEqOQmkl4DirbA0kyqPAD67+3O4Nym6xl+Xw52AiB5HdP+FzZ/gbsLUuyL4YDRunQQ1Li4gL1h2Ww== +VmVrGQo2zRokW/ZuO9bN669R7mdHpygJNR1k/erSr8ba1V+vnWiGgk4PWIWLXymYT0CQ2pPCYGGxLlvYfwaDNg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6wRWLqdJyjJ0e37jNW4UHPtkNfA+fwtgEj54x943BOGimir/JnsLnQzXetoOQlv/Vw== +VmVrGQo2zRokW/ZuO9bN69WmSOt0p25HK+6oDGX0onCGMOSiXbzJb3PVdPrcevf1eWbq9rjIwUO2VrcHrAZC/A== +VmVrGQo2zRokW/ZuO9bN6zatYZyKtIQNclCsWFRbkxqcYHdny41DrJbTs//IH0QNN9Z+Pab/Si+tGBqs1nrMNg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl0loyRl3Y+2lMvxKZrsSP4mX0eu+fncq0R/Gj2sMxNtg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkm7B1iycNQxsaqQDzmbZTu3A/QICa/qVeUrppmsxrFqmg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklGFrs4gwGXvmp4BPscyldS0+7KEZ325wQEwfOSBU6xppAdoh79sA9sJGXSncT3qcs= +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn7riDEdBtVgGPKUzJw06A1 +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknxuDY0yMUd6CtugRn68diS7IRPBwto8KUITtCHjzsVgg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmyOHs1nXMxTF8IZWi4vGdIu/dwsvSGUeMRnZRjGJJ/rA== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklOv3JCeX9TwkZ2qOV8fdCeOA5GFMg7l9EKy86Bo3BHEtBW5iRaGbAAysxJvejkA1M= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl44hEndxWfoKun5ZaEbUMH7GPrgZQODWEVCSDVK4quyg== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkk1A7YjKzApThJwBqVzLaWq +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkn6VEPmWYcJhg8lgmV5LgMEWpJARBLApS9tW/DU9OLvFg== +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkk/Xr/2ghELRNGM0Tnj00JYzXj6NQFPFGdK04zU180w8or2N1YqsnGnkPmBi3iiKOz3Hb5VN5RzZz88iahP5EhZIiY/IxHFclTRwrKa6NEYy4aCInm+rtS6Kh1UcX7Akxk= +VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknHU9sbiSXnkJH2Ine9gQMa +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN6zWChYLPlHamJFV5kS2vr8eccn0RUBVPyzIUacZ8cLtx09HLs05RuP3IUPIzFhMi3g== +VmVrGQo2zRokW/ZuO9bN66rIgoW0pjLA4ZrmefxAT7FEmwWt4kUPVWjixEuCfAhs +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN62hT4Us+sf3STuKUPNaNAmwVCNrYqUsg2YHsybMZByDD +VmVrGQo2zRokW/ZuO9bN69CHCfVBuJ8M9my8n/THYQAAVVSzed4flp6c/dEcJ5QmmO3PuHa6h6i0NGmizE0IkA== +1u+XjG/2+GSQRv6EzCaWRQ== +VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw= +1u+XjG/2+GSQRv6EzCaWRQ== +/R3RsIyyuJzgF1BbtmZTiGOqRYPC1wSxCIUPgAw0AXw= ++pBWk5LxptzizUTsHnZAz99i/5vNWptrZ9p5u7vIcfi5qEc39vhVrczSNIQsTzd1 +p2+bVXgc/OGpnu+3Mkh/HWgCi8l5R+8ciwkTACh8s8pCY45MbTxrLbNgCiSPXvng +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgZS60kNIgA2GGL7NP/H70aZ/rEh3QTPcRuOhpA401DEz +1u+XjG/2+GSQRv6EzCaWRQ== +SPGIcLQvfIdvbEm4bSwFQee10PGHfW4lbbTEmggKXRk= ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +XIS7WB0EDgFbcOJd3ow5ksYQUhL1QPtlxmX0lZI5T6UeSzkMcT9kwZxfgvYXCNqU +Fk6c/WQWq+cjUfytKivXf3LJLCykmIxL10jYThUpGn4IB8gURfF+sNfw7Gr2p7xE7RtBFpXdgfnQ+F9kQLtr0vSs7TodrIZHgl++fEDZeP7W17KqWSg3tsEvOjGupuCyE6D3X2uU7KapE9X26cI0XA== +tyk4qsf5nvIeNK9AIRjxVKtCCQCkuvSRzv5fqZrIqlg= +1u+XjG/2+GSQRv6EzCaWRQ== +ASbBxj96B4nLdItQB6jdDzHn62WA0u8GeY1oMMA4+/Js+QppsP6IdKCODXPagf4J +1u+XjG/2+GSQRv6EzCaWRQ== +/0ULpLqgTvInFD0r5hHANsVMQyYMTxOCtjrehT+unwA= +1u+XjG/2+GSQRv6EzCaWRQ== +9zgnmC3+5N9XK5/NRaR4SAkovFAqchP05HrrVafSP3Q= +o6QhOIN2Sc4SHELnst17uS1HqGOIMdnzZRPat3BllvVCeJ1Z6uEV9DN2tsvk3F0e +1u+XjG/2+GSQRv6EzCaWRQ== +ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8= +1u+XjG/2+GSQRv6EzCaWRQ== +YqxtfS4Jl1CuIMzPO0zZvzYpKKy/M6UA7pvbZGxgUhn3deCKXI/5yLc9AmKyIV2O ++qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk= +YcpegqjyUjgt7VJT3/cANyAV3hppZQCGgDoI7Ovcr55fV+vIFvz1cOJe/qWkGyWXcWfvKt2XxZY24ZrY4Oajp3DLokK44fyb03eU51IiZzvI7MJ5Ov6cV/A8Dt/6Q9C6yT5eVkPA5RK6ZcGCzRj0QLNg+gMJ+XS3AWD2knLUrccZwfl/VpSqZFMYZiCoigD+SYKdAAe/vxlwVnS4JEJpdg== +q4nc/jwATOMUyfSjLibfEZ4XjohyYx40xFfjkATboNc= +1u+XjG/2+GSQRv6EzCaWRQ== +b4OJVZe8QyIpjuTpKXDL9A== +1hs1FfIPISXmd7TJ/r+VlwAvPWZGdqAvmnIAjKFyuOELw291PAhj1V4Xxj5g/nqdLmx/rPGpdKS4yvHkefKp8i0IaPXFt4dxP3dbflnJPps= +VmVrGQo2zRokW/ZuO9bN60Gth2ckbJATM/BfrvUVPmue2p+WofJm/WxwT7mqVqTvgLK+8t4OFQLjOf5z9aRHWA== +g+Jm/FxeTBTKtmon+biYfZROYL1BhogZsAe/p0Xuf+OUExdOCR/cA+jbvzjhaUeV +AtqIw4BuTOuXet8gp4hSxawGcJ3LMOV8osinYrIlYbs2j0TVHQsHVjjAeewMAVb69ISskpQyRuvAetXmOzd/TA== +J1UGlXkKhKXD36OrXXhN8GGOTZOUFHzOMTHrRyVHVYZ5816/SOJMs9TyaFp9Axdr +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpQTcVH02FaIC+n3BD2EZiZ7GdVRqNxGvbDvHwy4TqkEQ= +1u+XjG/2+GSQRv6EzCaWRQ== +AtqIw4BuTOuXet8gp4hSxawGcJ3LMOV8osinYrIlYbs+l4h2IfleAKUNo0IMzhsNR678evF5KCIVXdg2jUlBQg== +J1UGlXkKhKXD36OrXXhN8GGOTZOUFHzOMTHrRyVHVYbwXDCKseAmFmmaGUxzaTCb +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpQTcVH02FaIC+n3BD2EZiZ7GdVRqNxGvbDvHwy4TqkEQ= +1u+XjG/2+GSQRv6EzCaWRQ== +zYuCrpMrZGCN996UA1dFfCqrlOoIQdYuHJjs/EiwTAjnR0eLJq4Yq0OFR/Vezo7BVoy78uRiTBjRiW0lltTvSU5jvePE2Js5VQGPu57LdAeeczfocTqtzjmnRXzsdiexBe2Lj5Gkzq2Ivs9H2fzp1w== +J1UGlXkKhKXD36OrXXhN8GGOTZOUFHzOMTHrRyVHVYYdK9Onw9+O40QawqeKhZFY +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpnUPZP5I1ml+E1aCJdhd7Cr+eL7kl8Hn422yZZO07Uf3sg9l9R9yP9YHxLqn5Fczm+zM17b5MeGui2pJw7Da9eQ== +1u+XjG/2+GSQRv6EzCaWRQ== +YY5wEo94l9qfo5TPxQmUgoNFl2Fp8+FsNDKaanjjEPI= +g+Jm/FxeTBTKtmon+biYffaq+RoNyUuVQpnhzMR9ZI9ada3yog46BhaKJEyY9rrm +zYuCrpMrZGCN996UA1dFfCqrlOoIQdYuHJjs/EiwTAjnR0eLJq4Yq0OFR/Vezo7BVoy78uRiTBjRiW0lltTvSU5jvePE2Js5VQGPu57LdAeeczfocTqtzjmnRXzsdiexBe2Lj5Gkzq2Ivs9H2fzp1w== +J1UGlXkKhKXD36OrXXhN8GGOTZOUFHzOMTHrRyVHVYYdK9Onw9+O40QawqeKhZFY +VmVrGQo2zRokW/ZuO9bN6xCz3gmXMpXHlLbeFcnZX8BjeW9DGnq0/peKsi5Sk+PpnUPZP5I1ml+E1aCJdhd7Cr+eL7kl8Hn422yZZO07Uf3sg9l9R9yP9YHxLqn5Fczm+zM17b5MeGui2pJw7Da9eQ== +1u+XjG/2+GSQRv6EzCaWRQ== +YY5wEo94l9qfo5TPxQmUgoNFl2Fp8+FsNDKaanjjEPI= +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z7Vc7221CfIz8ZtOcaiNHvZkZy6HmpWI+rolHwFXNUBFEpU9CY15Oz/Aa0LvqSOT/n2OtBHgVt7X+g0nKeI9QJQcntqrGdqnHB792/SuaMskNbTw645/oWoVaxxDSribyqfiqnQa23vYCz1caGrcuT7 +1u+XjG/2+GSQRv6EzCaWRQ== +WxLCrccfrU/GbYaQM0CttymCEoSSL4XQ2dUp46xr/R4= +B5OpQk9aNCq2cqtWnot+0ektsSR04ZQDuLGC6CtaDUd1iC1qQLD2uvrnEKJf7KyiVEbSBuQTlVXOMFeYjT7NSvHdAvjLAnwJW+s1N/peQUo= +NX/64OOR80jDJXFsjw7Fd2YEHAapydu6aFaFkA/faxiitqfJRm/RafF4reJcLKcr0FB7azaNeAGrQBvvmtKR0g== +1u+XjG/2+GSQRv6EzCaWRQ== +lSUcoY7WGh25lL+WkZ2yhFKeopk8TTSGyYLCojtKOrbHs0ZeC70IpO+XHxenHz3VBvV/qqAXWKrRXX1CpTy5Yg== +1u+XjG/2+GSQRv6EzCaWRQ== +YupK9RnBtGWSJ/bJnNE323UsRUYz7fAl7U+FnhNXx5NJzsbMCgA5QSLHBAut1/xN +32pdC9DD05OE2l0oXazDFKrh78d0BRqSJX0q2CkcDE93BpFpOsUT0ysr/XrsMnFYOEkaY3FIqaqavCLBkH2U+g== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgTxF21Ps2kmxgSF7PHkiu0MGSqzva9k+nwV7qCXvJa+3aB1BQ7DR77CAXbylNAQwjg== +1u+XjG/2+GSQRv6EzCaWRQ== +CtrEhNe2VFAiMmP1D651CXARpKnz5L5gnl8P0Cw9Nf71zlpMlCHzn5h1+ygqMJA5 +NQncuz/16VbFlhzZ0M9aVNMjHZ01MIK51TBvwohq/aRI5YtMYBD9ZDDuLopvZpD7enL+ip6O02WnELmwZzpDi07sEEiaMnbwiJ5JT4pHO8U= +pHO5j+XfjoQBHQFq+CDhtnG6ZeQfJihks/j/OzQePRsiP9KrD2nqkoBypFrGLzsI8uu9YPme2V2eyTJAYhMGv5+ah5IuWHC6xlz/w3lz3/74aAiQRAfMoH7oj+Kms0CK +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYidWX2gC3fVamYQXJ9ey9xQWt939cRs/gIoasbB0JAK0 +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkvCs01Ef75uP/SBKZUAi7QcIuWcW1HaUBcTUGockc85s1CH1aR+7H9LuAtylnJJMAMSo6nahEmsxkN44zkjC9dXVkzfjHBfBrf5QGp646Bz8Q== +1u+XjG/2+GSQRv6EzCaWRQ== +4+wKB+Tel1iow8au3cOIJeDFU2Q2B3JtrpibuQEbYEQsv3h+Pn1Sph6+lKk2YYEZoZ5M40Os/ejo6Vasw0SzwA== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgV5MR2wpcZ2T3AJHGErCLIxvBJzMyE+aTyZ5n3RXBFa7h3v1HmAxsqWfbDVied+W3MVE+u34oT59Ro+iVsfSQpX6SK3qe3xEyraF9Uiio+M0 +1u+XjG/2+GSQRv6EzCaWRQ== +csLthA53ojJM7MvqrKXR/JMLDLfhogJnpxuetF5T7vY= +6Pn8WKJkUFkKq6rlGCHZR+o5YGDgTV8miYZLyJP0GtH3JleoBl+kgxNFB49Y+n6XiUg7bE1b429A6uNDgX9thw== +pHO5j+XfjoQBHQFq+CDhtnG6ZeQfJihks/j/OzQePRsiP9KrD2nqkoBypFrGLzsI8uu9YPme2V2eyTJAYhMGv5+ah5IuWHC6xlz/w3lz3/74aAiQRAfMoH7oj+Kms0CK +1u+XjG/2+GSQRv6EzCaWRQ== +wgR07xfoapmx6eEnFHXXYidWX2gC3fVamYQXJ9ey9xQWt939cRs/gIoasbB0JAK0 +L0eUthVnpkGsmKFAX6d+uJFrCS/2hF2imwgPWwEHOkvCs01Ef75uP/SBKZUAi7QcIuWcW1HaUBcTUGockc85s1CH1aR+7H9LuAtylnJJMAMSo6nahEmsxkN44zkjC9dXVkzfjHBfBrf5QGp646Bz8Q== +1u+XjG/2+GSQRv6EzCaWRQ== ++pBWk5LxptzizUTsHnZAz+rqReKZ+A+992491ivMMGOTakmdHhRY+LzmYCN+4Pu0 +0976Vy/lfkoO1AbuRG+nV0apXetJKv3OodW/dljN/INIGDb/zuAzFLXnaYu62PSZ +1u+XjG/2+GSQRv6EzCaWRQ== +QjEYNJp9YKIp2aaVIb+6fxAxGWjlbup6X2xFhqc4V37TtVEsRNLZDsrqifdWwuU9Hl0/Mb7eu8BQqjySX2BjJOWz0WPOpM6/fQ+oRDRutdyw5Qdets8ldoS+RFYb17+Ny0tjX4ue1UBrJYIl6jAMKWdXCqYaw6N1+Ja9rDImwuQJPqXXFIK8i6Sqs96664B8 +1u+XjG/2+GSQRv6EzCaWRQ== +4+wKB+Tel1iow8au3cOIJeDFU2Q2B3JtrpibuQEbYEQsv3h+Pn1Sph6+lKk2YYEZoZ5M40Os/ejo6Vasw0SzwA== +1u+XjG/2+GSQRv6EzCaWRQ== ++pBWk5LxptzizUTsHnZAz+rqReKZ+A+992491ivMMGOw4uN0RmY6FHaPfgSPvDAH +p2+bVXgc/OGpnu+3Mkh/HX0nh9R726Gt/6kJ6Z8jC4I= +1u+XjG/2+GSQRv6EzCaWRQ== +4+wKB+Tel1iow8au3cOIJeDFU2Q2B3JtrpibuQEbYERdXH1NACiaD4lhdcNmPcy4tzPE6sUzhYykOaufrGsnmA== +1u+XjG/2+GSQRv6EzCaWRQ== +96orka/uERLyRst14azQwomqJXk08MT8ElCYPFEm1z46GfCHZKttxdYHc80Wh2zA9fcGihKrecOIC7ra8FlSgRYzwHLo9JtKhRtTS3Fjr3IJMH/yCG7ebNQo8ouASR8TymckgcJhWCYtdsr21ivlHoH4JoegN0OzbyP2ILL+c9I= +1u+XjG/2+GSQRv6EzCaWRQ== +X2wedBXc8JweJQhy5Mkny6kgVWWDAcJUfZ0tRXKZ1QcENA/FiBSXZ1NGld+Z5g6F +JBUvLRho2v4T0sgReac7edzubT6+00LY16NgjS7SFCM= +MVNqiWIB6fAQku1mTlf1Jxq+pMUfgeOXSedGI3zuVG6fEtfmYQ5CWzTFA+jTu2bi +b4OJVZe8QyIpjuTpKXDL9A== +zxz/Z9yjFalbpeH0OEk066ab1RF3DcbItEg0mv/cL8Yx9+E07lhARaUu01cucUxRGrRzIlk14o2iChsrkpCS+/4eAYj4epWkLH+4PRjsCayFvqF5Z/CSwgbfszGPnPB5 +32pdC9DD05OE2l0oXazDFHjt/ErrLQ9zO8Hn8/+OjTf+ZYkoqbck5z09Fea0uwJv +/9/zB6N+j20R9WpzrjSFz7AHCoy3iHwP7TIpB9/jIKfVWViFPp3zrtzJ2mRSV4Aj +jYPHQAqSoL3QG9u85m6Jg0oiDIwa2nYjYliF1PoBA2U= +32pdC9DD05OE2l0oXazDFHMTHwhviEdVnkMORuxEOpOD1JJJ1BRdiehEFmiWTs9b diff --git a/class_v2/wp_toolkit/security.py b/class_v2/wp_toolkit/security.py new file mode 100644 index 00000000..2a985192 --- /dev/null +++ b/class_v2/wp_toolkit/security.py @@ -0,0 +1,223 @@ +# WP Toolkit security +# author: hzh<2024-07-02> +import public +from public.validate import Param + + +# WP安全模块类 +class wp_security: + + # 开启文件防护 + def open_file_protection(self, get): + # 校验参数 + try: + get.validate([ + Param('paths').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + tamper_core_status = public.run_plugin('tamper_core', 'get_service_status', public.to_dict_obj({})) + if not (tamper_core_status["kernel_module_status"] and tamper_core_status["controller_status"]): + return public.return_message(-1,0,'Sorry, opening failed, please go to the App Store --Tamper-proof for Enterprise to view details') + + result= public.run_plugin('tamper_core', 'multi_create', public.to_dict_obj({ + 'paths': get.paths, + })) + if 'status' in result: + return public.return_message(-1,0,result[0]['msg']) + if type(result)==list: + # for i in result: + + public.run_plugin('tamper_core', 'assign_rule_to_directory', public.to_dict_obj({ + 'rule_group_name': 'WordPress Normal', + 'path_id': result[0]['pid'], + })) + return public.return_message(0,0,'Successfully added file protection') + + #取安全防护配置 + def get_security_info(self, get): + + # 校验参数 + try: + get.validate([ + Param('path').Require().String(), + Param('site_name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + return_result={ + 'hotlink_status':0, + 'file_status':0, + 'firewall_status':0, + 'file_count':0, + 'firewall_count':0, + } + if get.path[-1] !='/': + get.path+='/' + #取文件防护配置 + tamper_core_status = public.run_plugin('tamper_core', 'get_service_status', public.to_dict_obj({})) + if tamper_core_status["kernel_module_status"] and tamper_core_status["controller_status"]: + result= public.run_plugin('tamper_core', 'get_tamper_paths', public.to_dict_obj({})) + if type(result)==list: + for i in result: + if i['path']==get.path and i['status']==1: + return_result['file_status']=1 + today=i['total']['today'] + return_result['file_count']=int(today['create'])+int(today['modify'])+int(today['unlink'])+int(today['rename'])+int(today['mkdir'])+int(today['rmdir'])+int(today['chmod'])+int(today['chown'])+int(today['link']) + break + + #取防火墙配置 + result= public.run_plugin('btwaf', 'get_site_config_byname', public.to_dict_obj({'siteName':get.site_name})) + try: + if result['open']: + return_result['firewall_status']=1 + except: + pass + result= public.run_plugin('btwaf', 'get_site_config3', public.to_dict_obj({'siteName':get.site_name})) + try: + if type(result['data'])==list: + for i in result['data']: + if i['siteName']!=get.site_name: + continue + if type(i['total'])==list: + for total in i['total']: + return_result['firewall_count']+=int(total['value']) + except: + pass + + return public.return_message(0,0,return_result) + + #关闭文件保护 + def close_file_protection(self, get): + # 校验参数 + try: + get.validate([ + Param('path_id').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + tamper_core_status = public.run_plugin('tamper_core', 'get_service_status', public.to_dict_obj({})) + if not (tamper_core_status["kernel_module_status"] and tamper_core_status["controller_status"]): + return public.return_message(-1,0,'Sorry, opening failed, please go to the App Store --Tamper-proof for Enterprise to view details') + + result= public.run_plugin('tamper_core', 'remove_path_config', public.to_dict_obj({ + 'path_id': get.path_id, + })) + status=0 + if not result['status']: + status=-1 + return public.return_message(status,0,result['msg']) + + + + # 开启防火墙防护 + def open_firewall_protection(self, get): + # 校验参数 + try: + get.validate([ + Param('site_name').Require().String(), + Param('obj').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + result= public.run_plugin('btwaf', 'get_total_all', public.to_dict_obj({ + })) + if not result['open']: + result= public.run_plugin('btwaf', 'set_open', public.to_dict_obj({ + })) + if not result['status']: + return public.return_message(-1,0,'Failed to open firewall') + + result= public.run_plugin('btwaf', 'get_site_config_byname', public.to_dict_obj({ + 'siteName': get.site_name, + })) + if not result['open']: + result= public.run_plugin('btwaf', 'set_site_obj_open', public.to_dict_obj({ + 'siteName': get.site_name, + 'obj': get.obj, + })) + if not result['status']: + return public.return_message(-1,0,'Failed to open firewall') + return public.return_message(0,0,'Successfully opened firewall') + + + # 关闭防火墙防护 + def close_firewall_protection(self, get): + # 校验参数 + try: + get.validate([ + Param('site_name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + result= public.run_plugin('btwaf', 'set_site_obj_open', public.to_dict_obj({ + 'siteName': get.site_name, + 'obj': 'open', + })) + if not result['status']: + return public.return_message(-1,0,'Failed to close firewall') + return public.return_message(0,0,'Successfully closeed firewall') + + # 获取防火墙防护配置 + def get_firewall_info(self, get): + # 校验参数 + try: + get.validate([ + Param('site_name').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + result= public.run_plugin('btwaf', 'get_site_config_byname', public.to_dict_obj({ + 'siteName':get.site_name, + })) + return public.return_message(0,0,result) + + + # 获取文件防护配置 + def get_file_info(self, get): + # 校验参数 + try: + get.validate([ + Param('path').Require().String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + if get.path[-1] !='/': + get.path+='/' + result= public.run_plugin('tamper_core', 'get_tamper_paths', public.to_dict_obj({ + 'path':get.path, + })) + for i in result: + if i['path']==get.path: + return public.return_message(0,0,i) + return public.return_message(0,0,{}) + + + + \ No newline at end of file diff --git a/class_v2/wxapp_v2.py b/class_v2/wxapp_v2.py new file mode 100644 index 00000000..890d62a1 --- /dev/null +++ b/class_v2/wxapp_v2.py @@ -0,0 +1,110 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | aaPanel +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2019 aaPanel(www.aapanel.com) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import os +import sys +if not 'class/' in sys.path: + sys.path.insert(0,'class/') +import public +import json +import time +import uuid +from BTPanel import session,cache,request + +class wxapp(): + + def __init__(self): + self.app_path = '/www/server/panel/data/' + self.app_path_p = '/www/server/panel/plugin/app/' + + def _check(self, get): + if get['fun'] in ['set_login', 'is_scan_ok', 'login_qrcode']: + return True + return public.returnMsg(False, 'Unauthorized') + + # 验证是否扫码成功 + def is_scan_ok(self, get): + if os.path.exists(self.app_path+"app_login_check.pl"): + try: + key, init_time, tid, status = public.readFile(self.app_path+'app_login_check.pl').split(':') + if time.time() - float(init_time) > 60: + return public.returnMsg(False, 'QR code expired') + session_id = public.get_session_id() + if cache.get(session_id) == public.md5(uuid.UUID(int=uuid.getnode()).hex): + return public.returnMsg(True, 'Scan QRCORE successfully') + except: + os.remove(self.app_path + "app_login_check.pl") + return public.returnMsg(False, '') + return public.returnMsg(False, '') + + # 返回二维码地址 + def login_qrcode(self, get): + tid = public.GetRandomString(32) + qrcode_str = 'https://app.bt.cn/app.html?&panel_url='+public.getPanelAddr()+'&v=' + public.GetRandomString(3)+'?login&tid=' + tid + data = public.get_session_id() + ':' + str(time.time()) + ':' + tid + ':' + tid + public.writeFile(self.app_path + "app_login_check.pl", data) + cache.set(tid,public.get_session_id(),360) + cache.set(public.get_session_id(),tid,360) + return public.returnMsg(True, qrcode_str) + + # 设置登录状态 + def set_login(self, get): + session_id = public.get_session_id() + if cache.get(session_id): + if cache.get(session_id) == public.md5(uuid.UUID(int=uuid.getnode()).hex): + return self.check_app_login(get) + else: + cache.delete(cache.get(session_id)) + cache.delete(session_id) + return public.returnMsg(False, 'Login failed 2') + return public.returnMsg(False, 'Login failed 1') + + #验证APP是否登录成功 + def check_app_login(self,get): + #判断是否存在绑定 + btapp_info = json.loads(public.readFile('/www/server/panel/config/api.json')) + if not btapp_info:return public.returnMsg(False,'Unbound!') + if not btapp_info['open']:return public.returnMsg(False,'API is not turned on') + if not 'apps' in btapp_info:return public.returnMsg(False,'Unbound phone') + if not btapp_info['apps']:return public.returnMsg(False,'Unbound phone') + try: + session_id=public.get_session_id() + if not os.path.exists(self.app_path+'app_login_check.pl'):return public.returnMsg(False,'Waiting for APP scan code login 1') + data = public.readFile(self.app_path+'app_login_check.pl') + public.ExecShell('rm ' + self.app_path+"app_login_check.pl") + secret_key, init_time, tid, status = data.split(':') + if len(session_id)!=64:return public.returnMsg(False,'Waiting for APP scan code login 2') + if len(secret_key)!=64:return public.returnMsg(False,'Waiting for APP scan code login 2') + if session_id != secret_key: + return public.returnMsg(False,'QR code expired') + if time.time() - float(init_time) > 60: + return public.returnMsg(False,'Waiting for APP scan code login') + import uuid + if status != uuid.UUID(int=uuid.getnode()).hex[-12:]: return public.returnMsg(False, '当前二维码失效222') + cache.delete(session_id) + cache.delete(tid) + userInfo = public.M('users').where("id=?",(1,)).field('id,username').find() + session['login'] = True + session['username'] = userInfo['username'] + session['tmp_login'] = True + public.WriteLog('Login','APP scan code login, account: {}, login IP: {}'.format(userInfo['username'],public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')))) + cache.delete('panelNum') + cache.delete('dologin') + session['session_timeout'] = time.time() + public.get_session_timeout() + login_type = 'data/app_login.pl' + self.set_request_token() + import config + config.config().reload_session() + public.writeFile(login_type,'True') + public.login_send_body("aaPanel Mobile",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT'))) + return public.returnMsg(True,'login successful!') + except: + return public.returnMsg(False, 'Login failed 2') + #生成request_token + def set_request_token(self): + session['request_token_head'] = public.GetRandomString(48) diff --git a/config/GeoLite2-City.mmdb b/config/GeoLite2-City.mmdb new file mode 100644 index 00000000..5a0a3125 Binary files /dev/null and b/config/GeoLite2-City.mmdb differ diff --git a/config/databases.json b/config/databases.json new file mode 100644 index 00000000..680a13c6 --- /dev/null +++ b/config/databases.json @@ -0,0 +1,698 @@ +{ + "ssl_data.db":{ + "ssl_info":{ + "sql":"CREATE TABLE IF NOT EXISTS 'ssl_info' ('id' INTEGER PRIMARY KEY AUTOINCREMENT,'hash' TEXT NOT NULL UNIQUE,'path' TEXT NOT NULL,'dns' TEXT NOT NULL,'subject' TEXT NOT NULL,'info' TEXT NOT NULL DEFAULT '','cloud_id' INTEGER NOT NULL DEFAULT -1,'not_after' TEXT NOT NULL,'use_for_panel' INTEGER NOT NULL DEFAULT 0,'use_for_site' TEXT NOT NULL DEFAULT '[]','auth_info' TEXT NOT NULL DEFAULT '{}','create_time' INTEGER NOT NULL DEFAULT (strftime('%s')));", + "fields":[ + [0, "id", "INTEGER", 0, null, 1], + [1, "hash", "TEXT", 0, null, 0], + [2, "path", "TEXT", 0, null, 0], + [3, "dns", "TEXT", 0, null, 0], + [4, "subject", "TEXT", 0, null, 0], + [5, "info", "TEXT", 0, "", 0], + [6, "cloud_id", "INTEGER", 0, "-1", 0], + [7, "not_after", "TEXT", 0, null, 0], + [8, "use_for_panel", "INTEGER", 0, "0", 0], + [9, "use_for_site", "TEXT", 0, "'[]'", 0], + [10, "auth_info", "TEXT", 0, "'{}'", 0], + [11, "create_time", "TEXT", 0, "strftime('%s')", 0] + ] + } + }, + "panel.db": { + "config": { + "sql": "CREATE TABLE `config` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `webserver` TEXT,\n `backup_path` TEXT,\n `sites_path` TEXT,\n `status` INTEGER,\n `mysql_root` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "webserver", "TEXT", 0, null, 0], + [2, "backup_path", "TEXT", 0, null, 0], + [3, "sites_path", "TEXT", 0, null, 0], + [4, "status", "INTEGER", 0, null, 0], + [5, "mysql_root", "TEXT", 0, null, 0] + ] + }, + "users": { + "sql": "CREATE TABLE `users` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `username` TEXT,\n `password` TEXT,\n `login_ip` TEXT,\n `login_time` TEXT,\n `phone` TEXT,\n `email` TEXT,\n `salt` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "username", "TEXT", 0, null, 0], + [2, "password", "TEXT", 0, null, 0], + [3, "login_ip", "TEXT", 0, null, 0], + [4, "login_time", "TEXT", 0, null, 0], + [5, "phone", "TEXT", 0, null, 0], + [6, "email", "TEXT", 0, null, 0], + [7, "salt", "TEXT", 0, null, 0] + ] + } + }, + "site.db": { + "sites": { + "sql": "CREATE TABLE `sites` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` TEXT,\n `path` TEXT,\n `status` TEXT,\n `index` TEXT,\n `ps` TEXT,\n `addtime` TEXT,\n `type_id` integer DEFAULT 0,\n `edate` integer DEFAULT '0000-00-00',\n `project_type` STRING DEFAULT 'PHP',\n `project_config` STRING DEFAULT '{}',\n `rname` text DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "TEXT", 0, null, 0], + [2, "path", "TEXT", 0, null, 0], + [3, "status", "TEXT", 0, null, 0], + [4, "index", "TEXT", 0, null, 0], + [5, "ps", "TEXT", 0, null, 0], + [6, "addtime", "TEXT", 0, null, 0], + [7, "type_id", "integer", 0, "0", 0], + [8, "edate", "integer", 0, "'0000-00-00'", 0], + [9, "project_type", "STRING", 0, "'PHP'", 0], + [10, "project_config", "STRING", 0, "'{}'", 0], + [11, "rname", "text", 0, "", 0] + ] + }, + "site_types": { + "sql": "CREATE TABLE `site_types` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` REAL,\n `ps` REAL\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "REAL", 0, null, 0], + [2, "ps", "REAL", 0, null, 0] + ] + }, + "domain": { + "sql": "CREATE TABLE `domain` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `pid` INTEGER,\n `name` TEXT,\n `port` INTEGER,\n `addtime` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "pid", "INTEGER", 0, null, 0], + [2, "name", "TEXT", 0, null, 0], + [3, "port", "INTEGER", 0, null, 0], + [4, "addtime", "TEXT", 0, null, 0] + ] + }, + "binding": { + "sql": "CREATE TABLE `binding` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `pid` INTEGER,\n `domain` TEXT,\n `path` TEXT,\n `port` INTEGER,\n `addtime` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "pid", "INTEGER", 0, null, 0], + [2, "domain", "TEXT", 0, null, 0], + [3, "path", "TEXT", 0, null, 0], + [4, "port", "INTEGER", 0, null, 0], + [5, "addtime", "TEXT", 0, null, 0] + ] + } + }, + "ftp.db": { + "ftps": { + "sql": "CREATE TABLE `ftps` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `pid` INTEGER,\n `name` TEXT,\n `password` TEXT,\n `path` TEXT,\n `status` TEXT,\n `ps` TEXT,\n `addtime` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "pid", "INTEGER", 0, null, 0], + [2, "name", "TEXT", 0, null, 0], + [3, "password", "TEXT", 0, null, 0], + [4, "path", "TEXT", 0, null, 0], + [5, "status", "TEXT", 0, null, 0], + [6, "ps", "TEXT", 0, null, 0], + [7, "addtime", "TEXT", 0, null, 0] + ] + } + }, + "database.db": { + "database_servers": { + "sql": "CREATE TABLE `database_servers` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `db_host` REAL,\n `db_port` REAL,\n `db_user` INTEGER,\n `db_password` INTEGER,\n `ps` REAL,\n `addtime` INTEGER,\n `db_type` REAL DEFAULT 'mysql',\n `type` STRING DEFAULT 'MySQL'\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "db_host", "REAL", 0, null, 0], + [2, "db_port", "REAL", 0, null, 0], + [3, "db_user", "INTEGER", 0, null, 0], + [4, "db_password", "INTEGER", 0, null, 0], + [5, "ps", "REAL", 0, null, 0], + [6, "addtime", "INTEGER", 0, null, 0], + [7, "db_type", "REAL", 0, "'mysql'", 0], + [8,"type","STRING",0,"'MySQL'",0] + ] + }, + "databases": { + "sql": "CREATE TABLE `databases` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `pid` INTEGER,\n `name` TEXT,\n `username` TEXT,\n `password` TEXT,\n `accept` TEXT,\n `ps` TEXT,\n `addtime` TEXT,\n `db_type` integer DEFAULT '0',\n `conn_config` STRING DEFAULT '{}',\n `sid` integer DEFAULT 0,\n `type` TEXT DEFAULT 'MySQL'\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "pid", "INTEGER", 0, null, 0], + [2, "name", "TEXT", 0, null, 0], + [3, "username", "TEXT", 0, null, 0], + [4, "password", "TEXT", 0, null, 0], + [5, "accept", "TEXT", 0, null, 0], + [6, "ps", "TEXT", 0, null, 0], + [7, "addtime", "TEXT", 0, null, 0], + [8, "db_type", "integer", 0, "'0'", 0], + [9, "conn_config", "STRING", 0, "'{}'", 0], + [10, "sid", "integer", 0, "0", 0], + [11, "type", "TEXT", 0, "'MySQL'", 0] + ] + }, + "mysql_increment_settings": { + "sql": "CREATE TABLE `mysql_increment_settings` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `cron_id` INTEGER,\n `db_name` TEXT DEFAULT '',\n `tb_name` TEXT DEFAULT '',\n `zip_password` TEXT DEFAULT '',\n `last_backup_time` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "cron_id", "INTEGER", 0, null, 0], + [2, "db_name", "TEXT", 0, "''", 0], + [3, "tb_name", "TEXT", 0, "''", 0], + [4, "zip_password", "TEXT", 0, "''", 0], + [5, "last_backup_time", "TEXT", 0, "''", 0] + ] + }, + "mysql_increment_backup": { + "sql": "CREATE TABLE `mysql_increment_backup` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `cron_id` INTEGER,\n `size` INTEGER DEFAULT 0,\n `type` INTEGER DEFAULT 0,\n `addtime` TEXT DEFAULT '',\n `name` TEXT DEFAULT '',\n `localhost` TEXT DEFAULT '',\n `ftp` TEXT DEFAULT '',\n `alioss` TEXT DEFAULT '',\n `txcos` TEXT DEFAULT '',\n `qiniu` TEXT DEFAULT '',\n `aws_s3` TEXT DEFAULT '',\n `upyun` TEXT DEFAULT '',\n `obs` TEXT DEFAULT '',\n `bos` TEXT DEFAULT '',\n `gcloud_storage` TEXT DEFAULT '',\n `gdrive` TEXT DEFAULT '',\n `msonedrive` TEXT DEFAULT '',\n `jdcloud` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "cron_id", "INTEGER", 0, null, 0], + [2, "size", "INTEGER", 0, "0", 0], + [3, "type", "INTEGER", 0, "0", 0], + [4, "addtime", "TEXT", 0, "''", 0], + [5, "name", "TEXT", 0, "''", 0], + [6, "localhost", "TEXT", 0, "''", 0], + [7, "ftp", "TEXT", 0, "''", 0], + [8, "alioss", "TEXT", 0, "''", 0], + [9, "txcos", "TEXT", 0, "''", 0], + [10, "qiniu", "TEXT", 0, "''", 0], + [11, "aws_s3", "TEXT", 0, "''", 0], + [12, "upyun", "TEXT", 0, "''", 0], + [13, "obs", "TEXT", 0, "''", 0], + [14, "bos", "TEXT", 0, "''", 0], + [15, "gcloud_storage", "TEXT", 0, "''", 0], + [16, "gdrive", "TEXT", 0, "''", 0], + [17, "msonedrive", "TEXT", 0, "''", 0], + [18, "jdcloud", "TEXT", 0, "''", 0] + ] + } + }, + "docker.db": { + "dk_sites": { + "sql": "CREATE TABLE `dk_sites` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` TEXT,\n `path` TEXT,\n `status` TEXT DEFAULT 1,\n `ps` TEXT,\n `addtime` TEXT,\n `type_id` integer DEFAULT 111,\n `edate` integer DEFAULT '0000-00-00',\n `project_type` STRING DEFAULT 'dk_proxy',\n `container_id` TEXT DEFAULT '',\n `container_name` TEXT DEFAULT '',\n `container_port` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "TEXT", 0, null, 0], + [2, "path", "TEXT", 0, null, 0], + [3, "status", "TEXT", 0, "1", 0], + [4, "ps", "TEXT", 0, null, 0], + [5, "addtime", "TEXT", 0, null, 0], + [6, "type_id", "integer", 0, "111", 0], + [7, "edate", "integer", 0, "'0000-00-00'", 0], + [8, "project_type", "STRING", 0, "'dk_proxy'", 0], + [9, "container_id", "TEXT", 0, null, 0], + [10, "container_name", "TEXT", 0, null, 0], + [11, "container_port", "TEXT", 0, null, 0] + ] + }, + "dk_domain": { + "sql": "CREATE TABLE `dk_domain` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `pid` INTEGER,\n `name` TEXT,\n `addtime` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "pid", "INTEGER", 0, null, 0], + [2, "name", "TEXT", 0, null, 0], + [3, "addtime", "TEXT", 0, null, 0] + ] + }, + "dk_backup": { + "sql": "CREATE TABLE `dk_backup` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `type` INTEGER,\n `name` TEXT,\n `container_id` TEXT,\n `container_name` TEXT,\n `filename` TEXT,\n `size` INTEGER,\n `addtime` TEXT,\n`ps` STRING DEFAULT '\u65e0',\n`cron_id` INTEGER DEFAULT 0\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "type", "INTEGER", 0, null, 0], + [2, "name", "TEXT", 0, null, 0], + [3, "container_id", "TEXT", 0, null, 0], + [4, "container_name", "TEXT", 0, null, 0], + [5, "filename", "TEXT", 0, null, 0], + [6, "size", "INTEGER", 0, null, 0], + [7, "addtime", "TEXT", 0, null, 0], + [8, "ps", "STRING", 0, "'\u65e0'", 0], + [9, "cron_id", "INTEGER", 0, "0", 0] + ], + "comment": { + "table": "容器备份表", + "type": { + "ps": "备份类型", + "0": "容器备份", + "1": "镜像备份", + "2": "容器日志备份", + "3": "容器目录或文件备份" + }, + "name": "备份名称", + "container_id": "容器ID", + "container_name": "容器名", + "filename": "备份文件名", + "size": "备份文件大小", + "addtime": "备份时间", + "ps": "备注", + "cron_id": "计划任务ID" + } + }, + "container": { + "sql": "CREATE TABLE `container` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `cpu_limit` VARCHAR DEFAULT (1),\n `container_name` VARCHAR DEFAULT ''\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "cpu_limit", "VARCHAR", 0, "1", 0], + [2, "container_name", "VARCHAR", 0, "''", 0] + ] + }, + "container_count": { + "sql": "CREATE TABLE `container_count` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `container_count` INTEGER,\n `time` INTEGER\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "container_count", "INTEGER", 0, null, 0], + [2, "time", "INTEGER", 0, null, 0] + ] + }, + "cpu_stats": { + "sql": "CREATE TABLE `cpu_stats` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `time` INTEGER, \n `cpu_usage` VARCHAR,\n `online_cpus` INT,\n `container_id` VARCHAR\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "time", "INTEGER", 0, null, 0], + [2, "cpu_usage", "VARCHAR", 0, null, 0], + [3, "online_cpus", "INT", 0, null, 0], + [4, "container_id", "VARCHAR", 0, null, 0] + ] + }, + "hosts": { + "sql": "CREATE TABLE `hosts` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `url` VARCHAR,\n `remark` VARCHAR DEFAULT (''),\n `time` INTEGER\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "url", "VARCHAR", 0, null, 0], + [2, "remark", "VARCHAR", 0, "''", 0], + [3, "time", "INTEGER", 0, null, 0] + ] + }, + "image_infos": { + "sql": "CREATE TABLE `image_infos` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `num` INTEGER,\n `size` INTEGER,\n `time` INTEGER\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "num", "INTEGER", 0, null, 0], + [2, "size", "INTEGER", 0, null, 0], + [3, "time", "INTEGER", 0, null, 0] + ] + }, + "io_stats": { + "sql": "CREATE TABLE `io_stats` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `container_id` VARCHAR,\n `time` VARCHAR,\n `read_total` INTEGER,\n `write_total` INTEGER\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "container_id", "VARCHAR", 0, null, 0], + [2, "time", "VARCHAR", 0, null, 0], + [3, "read_total", "INTEGER", 0, null, 0], + [4, "write_total", "INTEGER", 0, null, 0] + ] + }, + "mem_stats": { + "sql": "CREATE TABLE `mem_stats` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `mem_limit` VARCHAR,\n `cache` VARCHAR,\n `usage` VARCHAR,\n `usage_total` VARCHAR,\n `container_id` VARCHAR,\n `time` INTEGER\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "mem_limit", "VARCHAR", 0, null, 0], + [2, "cache", "VARCHAR", 0, null, 0], + [3, "usage", "VARCHAR", 0, null, 0], + [4, "usage_total", "VARCHAR", 0, null, 0], + [5, "container_id", "VARCHAR", 0, null, 0], + [6, "time", "INTEGER", 0, null, 0] + ] + }, + "net_stats": { + "sql": "CREATE TABLE `net_stats` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `container_id` INTEGER,\n `rx_total` INTEGER,\n `tx_total` INTEGER,\n `rx` INTEGER,\n `tx` INTEGER,\n `time` VARCHAR\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "container_id", "INTEGER", 0, null, 0], + [2, "rx_total", "INTEGER", 0, null, 0], + [3, "tx_total", "INTEGER", 0, null, 0], + [4, "rx", "INTEGER", 0, null, 0], + [5, "tx", "INTEGER", 0, null, 0], + [6, "time", "VARCHAR", 0, null, 0] + ] + }, + "registry": { + "sql": "CREATE TABLE `registry` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `url` VARCHAR,\n `username` VARCHAR DEFAULT (''),\n `password` VARCHAR DEFAULT (''),\n `name` VARCHAR UNIQUE,\n `namespace` VARCHAR,\n `remark` VARCHAR DEFAULT ('')\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "url", "VARCHAR", 0, null, 0], + [2, "username", "VARCHAR", 0, "''", 0], + [3, "password", "VARCHAR", 0, "''", 0], + [4, "name", "VARCHAR", 0, null, 0], + [5, "namespace", "VARCHAR", 0, null, 0], + [6, "remark", "VARCHAR", 0, "''", 0] + ] + }, + "stacks": { + "sql": "CREATE TABLE `stacks` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` VARCHAR UNIQUE,\n `status` VARCHAR,\n `path` VARCHAR,\n `template_id` INTEGER,\n `time` INTEGER,\n `remark` VARCHAR DEFAULT ('')\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "VARCHAR", 0, null, 0], + [2, "status", "VARCHAR", 0, null, 0], + [3, "path", "VARCHAR", 0, null, 0], + [4, "template_id", "INTEGER", 0, null, 0], + [5, "time", "INTEGER", 0, null, 0], + [6, "remark", "VARCHAR", 0, "''", 0] + ] + }, + "templates": { + "sql": "CREATE TABLE `templates` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `path` VARCHAR,\n `name` VARCHAR UNIQUE,\n `remark` VARCHAR DEFAULT (''),\n `add_in_path` INTEGER\n);", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "path", "VARCHAR", 0, null, 0], + [2, "name", "VARCHAR", 0, null, 0], + [3, "remark", "VARCHAR", 0, "''", 0], + [4, "add_in_path", "INTEGER", 0, null, 0] + ] + } + }, + "firewall.db": { + "firewall": { + "sql": "CREATE TABLE `firewall` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `port` TEXT,\n `ps` TEXT,\n `addtime` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "port", "TEXT", 0, null, 0], + [2, "ps", "TEXT", 0, null, 0], + [3, "addtime", "TEXT", 0, null, 0] + ] + }, + "firewall_country": { + "sql": "CREATE TABLE `firewall_country` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `types` TEXT,\n `country` TEXT DEFAULT '',\n `brief` TEXT DEFAULT '',\n `addtime` TEXT DEFAULT '',\n `ports` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "types", "TEXT", 0, null, 0], + [2, "country", "TEXT", 0, "''", 0], + [3, "brief", "TEXT", 0, "''", 0], + [4, "addtime", "TEXT", 0, "''", 0], + [5, "ports", "TEXT", 0, "''", 0] + ] + }, + "firewall_domain": { + "sql": "CREATE TABLE `firewall_domain` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `types` TEXT,\n `domain` TEXT,\n `domain_total` TEXT,\n `port` TEXT,\n `sid` int DEFAULT 0,\n `address` TEXT DEFAULT '',\n `brief` TEXT DEFAULT '',\n `protocol` TEXT DEFAULT '',\n `addtime` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "types", "TEXT", 0, null, 0], + [2, "domain", "TEXT", 0, null, 0], + [3, "domain_total", "TEXT", 0, null, 0], + [4, "port", "TEXT", 0, null, 0], + [5, "sid", "int", 0, "0", 0], + [6, "address", "TEXT", 0, "''", 0], + [7, "brief", "TEXT", 0, "''", 0], + [8, "protocol", "TEXT", 0, "''", 0], + [9, "addtime", "TEXT", 0, "''", 0] + ] + }, + "firewall_ip": { + "sql": "CREATE TABLE `firewall_ip` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `types` TEXT,\n `address` TEXT DEFAULT '',\n `brief` TEXT DEFAULT '',\n `addtime` TEXT DEFAULT '',\n `sid` int DEFAULT 0,\n `domain` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "types", "TEXT", 0, null, 0], + [2, "address", "TEXT", 0, "''", 0], + [3, "brief", "TEXT", 0, "''", 0], + [4, "addtime", "TEXT", 0, "''", 0], + [5, "sid", "INTEGER", 0, 0, 0], + [6, "domain", "TEXT", 0, "", 0] + ] + }, + "firewall_new": { + "sql": "CREATE TABLE `firewall_new` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `protocol` TEXT DEFAULT '',\n `ports` TEXT,\n `types` TEXT,\n `address` TEXT DEFAULT '',\n `brief` TEXT DEFAULT '',\n `addtime` TEXT DEFAULT '',\n `domain` TEXT DEFAULT '',\n `sid` int DEFAULT 0\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "protocol", "TEXT", 0, "''", 0], + [2, "ports", "TEXT", 0, null, 0], + [3, "types", "TEXT", 0, null, 0], + [4, "address", "TEXT", 0, "''", 0], + [5, "brief", "TEXT", 0, "''", 0], + [6, "addtime", "TEXT", 0, "''", 0], + [7, "domain", "TEXT", 0, "", 0], + [8, "sid", "INTEGER", 0, 0, 0] + ] + }, + "firewall_trans": { + "sql": "CREATE TABLE `firewall_trans` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `start_port` TEXT,\n `ended_ip` TEXT,\n `ended_port` TEXT,\n `protocol` TEXT DEFAULT '',\n `addtime` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "start_port", "TEXT", 0, null, 0], + [2, "ended_ip", "TEXT", 0, null, 0], + [3, "ended_port", "TEXT", 0, null, 0], + [4, "protocol", "TEXT", 0, "''", 0], + [5, "addtime", "TEXT", 0, "''", 0] + ] + } + }, + "log.db": { + "logs": { + "sql": "CREATE TABLE `logs` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `type` TEXT,\n `log` TEXT,\n `addtime` TEXT,\n `uid` integer DEFAULT '1',\n `username` TEXT DEFAULT 'system'\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "type", "TEXT", 0, null, 0], + [2, "log", "TEXT", 0, null, 0], + [3, "addtime", "TEXT", 0, null, 0], + [4, "uid", "integer", 0, "'1'", 0], + [5, "username", "TEXT", 0, "'system'", 0] + ] + } + }, + "crontab.db": { + "crontab": { + "sql": "CREATE TABLE \"crontab\" (\n \"id\" INTEGER PRIMARY KEY AUTOINCREMENT,\n \"name\" TEXT,\n \"type\" TEXT,\n \"where1\" TEXT,\n \"where_hour\" INTEGER,\n \"where_minute\" INTEGER,\n \"echo\" TEXT,\n \"addtime\" TEXT,\n \"status\" INTEGER DEFAULT 1,\n \"save\" INTEGER DEFAULT 3,\n \"backupTo\" TEXT DEFAULT off,\n \"sName\" TEXT,\n \"sBody\" TEXT,\n \"sType\" TEXT,\n \"urladdress\" TEXT,\n \"save_local\" INTEGER DEFAULT 0,\n \"notice\" INTEGER DEFAULT 0,\n \"notice_channel\" TEXT DEFAULT '',\n `db_type` TEXT DEFAULT '',\n `split_type` TEXT DEFAULT '',\n `split_value` INTEGER DEFAULT 0,\n `rname` TEXT DEFAULT '',\n `type_id` INTEGER\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "TEXT", 0, null, 0], + [2, "type", "TEXT", 0, null, 0], + [3, "where1", "TEXT", 0, null, 0], + [4, "where_hour", "INTEGER", 0, null, 0], + [5, "where_minute", "INTEGER", 0, null, 0], + [6, "echo", "TEXT", 0, null, 0], + [7, "addtime", "TEXT", 0, null, 0], + [8, "status", "INTEGER", 0, "1", 0], + [9, "save", "INTEGER", 0, "3", 0], + [10, "backupTo", "TEXT", 0, "off", 0], + [11, "sName", "TEXT", 0, null, 0], + [12, "sBody", "TEXT", 0, null, 0], + [13, "sType", "TEXT", 0, null, 0], + [14, "urladdress", "TEXT", 0, null, 0], + [15, "save_local", "INTEGER", 0, "0", 0], + [16, "notice", "INTEGER", 0, "0", 0], + [17, "notice_channel", "TEXT", 0, "''", 0], + [18, "db_type", "TEXT", 0, "''", 0], + [19, "split_type", "TEXT", 0, "''", 0], + [20, "split_value", "INTEGER", 0, "0", 0], + [21, "rname", "TEXT", 0, "''", 0], + [22, "type_id", "INTEGER", 0, null, 0], + [23, "backup_mode", "TEXT", 0, "''", 0], + [24, "db_backup_path", "TEXT", 0, "''", 0], + [25, "time_type", "TEXT", 0, "''", 0], + [26, "special_time", "TEXT", 0, "''", 0], + [27, "flock","INTEGER", 0, "0", 0], + [28, "post_param", "TEXT", 0, "''", 0], + [29, "log_cut_path", "TEXT", 0, "''", 0], + [30, "user_agent", "TEXT", 0, "''", 0], + [31, "version", "TEXT", 0, "''", 0], + [32, "table_list", "TEXT", 0, "''", 0], + [33, "time_set", "TEXT", 0, "''", 0], + [34, "keyword", "TEXT", 0, "''", 0] + ] + } + }, + "backup.db": { + "backup": { + "sql": "CREATE TABLE `backup` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `type` INTEGER,\n `name` TEXT,\n `pid` INTEGER,\n `filename` TEXT,\n `size` INTEGER,\n `addtime` TEXT,\n\t`ps` STRING DEFAULT '\u65e0',\n\t`cron_id` INTEGER DEFAULT 0\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "type", "INTEGER", 0, null, 0], + [2, "name", "TEXT", 0, null, 0], + [3, "pid", "INTEGER", 0, null, 0], + [4, "filename", "TEXT", 0, null, 0], + [5, "size", "INTEGER", 0, null, 0], + [6, "addtime", "TEXT", 0, null, 0], + [7, "ps", "STRING", 0, "'\u65e0'", 0], + [8, "cron_id", "INTEGER", 0, "0", 0] + ] + } + }, + "task.db": { + "boce_task": { + "sql": "CREATE TABLE `boce_task` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` STRING (64),\n `url` STRING (128),\n `status` INTEGER,\n `channel` STRING (128),\n `cycle` INTEGER default 10,\n `addtime` INTEGER default 0,\n `address` STRING (128) default 'localhost',\n `keyword` STRING (128) default '',\n `status_code` BOOLEAN default 0,\n `delay` INTEGER default 0,\n `similarity` TEXT default '',\n `size` INTEGER default 0,\n `sensitive` BOOLEAN default 0,\n `alarm_count` integer default -1\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "STRING (64)", 0, null, 0], + [2, "url", "STRING (128)", 0, null, 0], + [3, "status", "INTEGER", 0, null, 0], + [4, "channel", "STRING (128)", 0, null, 0], + [5, "cycle", "INTEGER", 0, "10", 0], + [6, "addtime", "INTEGER", 0, "0", 0], + [7, "address", "STRING (128)", 0, "'localhost'", 0], + [8, "keyword", "STRING (128)", 0, "''", 0], + [9, "status_code", "BOOLEAN", 0, "0", 0], + [10, "delay", "INTEGER", 0, "0", 0], + [11, "similarity", "TEXT", 0, "''", 0], + [12, "size", "INTEGER", 0, "0", 0], + [13, "sensitive", "BOOLEAN", 0, "0", 0], + [14, "alarm_count", "integer", 0, "-1", 0] + ] + }, + "boce_list": { + "sql": "CREATE TABLE `boce_list` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `pid` INTEGER,\n `data` TEXT,\n `status` INTEGER,\n `addtime` INTEGER\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "pid", "INTEGER", 0, null, 0], + [2, "data", "TEXT", 0, null, 0], + [3, "status", "INTEGER", 0, null, 0], + [4, "addtime", "INTEGER", 0, null, 0] + ] + }, + "task_list": { + "sql": "CREATE TABLE `task_list` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` \t\t\tTEXT,\n `type`\t\t\tTEXT,\n `status` \t\t\tINTEGER,\n `shell` \t\t\tTEXT,\n `other` TEXT,\n `exectime` \t \tINTEGER,\n `endtime` \t \tINTEGER,\n `addtime`\t\t\tINTEGER\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "TEXT", 0, null, 0], + [2, "type", "TEXT", 0, null, 0], + [3, "status", "INTEGER", 0, null, 0], + [4, "shell", "TEXT", 0, null, 0], + [5, "other", "TEXT", 0, null, 0], + [6, "exectime", "INTEGER", 0, null, 0], + [7, "endtime", "INTEGER", 0, null, 0], + [8, "addtime", "INTEGER", 0, null, 0] + ] + }, + "tasks": { + "sql": "CREATE TABLE `tasks` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` \t\t\tTEXT,\n `type`\t\t\tTEXT,\n `status` \t\tTEXT,\n `addtime` \tTEXT,\n `start` \t INTEGER,\n `end` \t INTEGER,\n `execstr` \tTEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "TEXT", 0, null, 0], + [2, "type", "TEXT", 0, null, 0], + [3, "status", "TEXT", 0, null, 0], + [4, "addtime", "TEXT", 0, null, 0], + [5, "start", "INTEGER", 0, null, 0], + [6, "end", "INTEGER", 0, null, 0], + [7, "execstr", "TEXT", 0, null, 0] + ] + } + }, + "default.db": { + "div_list": { + "sql": "CREATE TABLE `div_list` (\n`id` INTEGER PRIMARY KEY AUTOINCREMENT,\n`div` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "div", "TEXT", 0, null, 0] + ] + }, + "download_token": { + "sql": "CREATE TABLE `download_token` (\n`id` INTEGER PRIMARY KEY AUTOINCREMENT,\n`token` REAL,\n`filename` REAL,\n`total` INTEGER DEFAULT 0,\n`expire` INTEGER,\n`password` REAL,\n`ps` REAL,\n`addtime` INTEGER\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "token", "REAL", 0, null, 0], + [2, "filename", "REAL", 0, null, 0], + [3, "total", "INTEGER", 0, "0", 0], + [4, "expire", "INTEGER", 0, null, 0], + [5, "password", "REAL", 0, null, 0], + [6, "ps", "REAL", 0, null, 0], + [7, "addtime", "INTEGER", 0, null, 0] + ] + }, + "messages": { + "sql": "CREATE TABLE `messages` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `level` TEXT,\n `msg` TEXT,\n `state` INTEGER DEFAULT 0,\n `expire` INTEGER,\n `addtime` INTEGER,\n `send` integer DEFAULT 0,\n `retry_num` integer DEFAULT 0\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "level", "TEXT", 0, null, 0], + [2, "msg", "TEXT", 0, null, 0], + [3, "state", "INTEGER", 0, "0", 0], + [4, "expire", "INTEGER", 0, null, 0], + [5, "addtime", "INTEGER", 0, null, 0], + [6, "send", "integer", 0, "0", 0], + [7, "retry_num", "integer", 0, "0", 0] + ] + }, + "temp_login": { + "sql": "CREATE TABLE `temp_login` (\n`id` INTEGER PRIMARY KEY AUTOINCREMENT,\n`token` REAL,\n`salt` REAL,\n`state` INTEGER,\n`login_time` INTEGER,\n`login_addr` REAL,\n`logout_time` INTEGER,\n`expire` INTEGER,\n`addtime` INTEGER\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "token", "REAL", 0, null, 0], + [2, "salt", "REAL", 0, null, 0], + [3, "state", "INTEGER", 0, null, 0], + [4, "login_time", "INTEGER", 0, null, 0], + [5, "login_addr", "REAL", 0, null, 0], + [6, "logout_time", "INTEGER", 0, null, 0], + [7, "expire", "INTEGER", 0, null, 0], + [8, "addtime", "INTEGER", 0, null, 0] + ] + }, + "security": { + "sql": "CREATE TABLE `security` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `type` TEXT,\n `log` TEXT,\n `addtime` INTEGER DEFAULT 0\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "type", "TEXT", 0, null, 0], + [2, "log", "TEXT", 0, null, 0], + [3, "addtime", "INTEGER", 0, "0", 0] + ] + }, + "send_msg": { + "sql": "CREATE TABLE `send_msg` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` TEXT,\n `send_type` TEXT,\n `msg` TEXT,\n `is_send` TEXT,\n `type` TEXT,\n `inser_time` TEXT DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "TEXT", 0, null, 0], + [2, "send_type", "TEXT", 0, null, 0], + [3, "msg", "TEXT", 0, null, 0], + [4, "is_send", "TEXT", 0, null, 0], + [5, "type", "TEXT", 0, null, 0], + [6, "inser_time", "TEXT", 0, "''", 0] + ] + }, + "send_settings": { + "sql": "CREATE TABLE `send_settings` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `name` TEXT,\n `type` TEXT,\n `path` TEXT,\n `send_type` TEXT,\n `last_time` TEXT,\n `time_frame` TEXT,\n `inser_time` TEXT DEFAULT''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "name", "TEXT", 0, null, 0], + [2, "type", "TEXT", 0, null, 0], + [3, "path", "TEXT", 0, null, 0], + [4, "send_type", "TEXT", 0, null, 0], + [5, "last_time", "TEXT", 0, null, 0], + [6, "time_frame", "TEXT", 0, null, 0], + [7, "inser_time", "TEXT", 0, "''", 0] + ] + }, + "ssh_login_record": { + "sql": "CREATE TABLE `ssh_login_record` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `addr` TEXT,\n `server_ip` TEXT,\n `user_agent` TEXT,\n `ssh_user` TEXT,\n `login_time` INTEGER DEFAULT 0,\n `close_time` INTEGER DEFAULT 0,\n `video_addr` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "addr", "TEXT", 0, null, 0], + [2, "server_ip", "TEXT", 0, null, 0], + [3, "user_agent", "TEXT", 0, null, 0], + [4, "ssh_user", "TEXT", 0, null, 0], + [5, "login_time", "INTEGER", 0, "0", 0], + [6, "close_time", "INTEGER", 0, "0", 0], + [7, "video_addr", "TEXT", 0, null, 0] + ] + }, + "rsync_oss": { + "sql": "CREATE TABLE `rsync_oss` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `rsync_path` DEFAULT '',\n `rsync_name` DEFAULT '',\n `all_num` INTEGER DEFAULT 0,\n `rsync_num` INTEGER DEFAULT 0,\n `recycle_bin` DEFAULT '',\n `where_1` DEFAULT '',\n `where_2` DEFAULT '',\n `webdav` DEFAULT '',\n `ftp` DEFAULT '',\n `alioss` DEFAULT '',\n `txcos` DEFAULT '',\n `qiniu` DEFAULT '',\n `aws` DEFAULT '',\n `upyun` DEFAULT '',\n `obs` DEFAULT '',\n `bos` DEFAULT '',\n `gcloud_storage` DEFAULT '', \n `gdrive` DEFAULT '',\n `notice` DEFAULT '',\n `notice_channel` DEFAULT '',\n `rsync_status` DEFAULT '',\n `rsync_cycle` DEFAULT '',\n `last_time` TEXT DEFAULT '',\n `rsync_mode` TEXT DEFAULT '',\n `is_del` TEXT DEFAULT '',\n `add_time` DEFAULT ''\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "rsync_path", "", 0, "''", 0], + [2, "rsync_name", "", 0, "''", 0], + [3, "all_num", "INTEGER", 0, "0", 0], + [4, "rsync_num", "INTEGER", 0, "0", 0], + [5, "recycle_bin", "", 0, "''", 0], + [6, "where_1", "", 0, "''", 0], + [7, "where_2", "", 0, "''", 0], + [8, "webdav", "", 0, "''", 0], + [9, "ftp", "", 0, "''", 0], + [10, "alioss", "", 0, "''", 0], + [11, "txcos", "", 0, "''", 0], + [12, "qiniu", "", 0, "''", 0], + [13, "aws", "", 0, "''", 0], + [14, "upyun", "", 0, "''", 0], + [15, "obs", "", 0, "''", 0], + [16, "bos", "", 0, "''", 0], + [17, "gcloud_storage", "", 0, "''", 0], + [18, "gdrive", "", 0, "''", 0], + [19, "notice", "", 0, "''", 0], + [20, "notice_channel", "", 0, "''", 0], + [21, "rsync_status", "", 0, "''", 0], + [22, "rsync_cycle", "", 0, "''", 0], + [23, "last_time", "TEXT", 0, "''", 0], + [24, "rsync_mode", "TEXT", 0, "''", 0], + [25, "is_del", "TEXT", 0, "''", 0], + [26, "add_time", "", 0, "''", 0] + ] + }, + "panel_search_log": { + "sql": "CREATE TABLE `panel_search_log` (\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `rtext` TEXT,\n `exts` TEXT,\n `path` TEXT,\n `mode` TEXT,\n `isword` TEXT,\n `iscase` TEXT,\n `noword` TEXT,\n `backup_path` TEXT,\n `time` TEXT\n)", + "fields": [ + [0, "id", "INTEGER", 0, null, 1], + [1, "rtext", "TEXT", 0, null, 0], + [2, "exts", "TEXT", 0, null, 0], + [3, "path", "TEXT", 0, null, 0], + [4, "mode", "TEXT", 0, null, 0], + [5, "isword", "TEXT", 0, null, 0], + [6, "iscase", "TEXT", 0, null, 0], + [7, "noword", "TEXT", 0, null, 0], + [8, "backup_path", "TEXT", 0, null, 0], + [9, "time", "TEXT", 0, null, 0] + ] + } + } +} diff --git a/config/docker_project_info.json b/config/docker_project_info.json new file mode 100644 index 00000000..7e24b0f5 --- /dev/null +++ b/config/docker_project_info.json @@ -0,0 +1,338 @@ +[ + { + "id": 1, + "sort": 1, + "Top-notch": 1, + "template_id": 1, + "server_name": "nextcloud", + "title": "Open source private cloud disk", + "version": "1.0.0", + "home": "https://nextcloud.com/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "20230406", + "icon": "", + "ps": "Nextcloud is an open source private cloud project, which can quickly deploy your own or your team's private cloud disk" + }, + { + "id": 2, + "sort": 2, + "Top-notch": 1, + "template_id": 2, + "server_name": "redis", + "title": "Open source key-value database", + "version": "1.0.0", + "home": "https://redis.io/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304012", + "icon": "", + "ps": "Millions of developers use it as an open source in-memory data store for databases, caches, streaming engines, and message brokers" + }, + { + "id": 3, + "sort": 3, + "Top-notch": 1, + "template_id": 3, + "server_name": "jenkins", + "title": "Continuous integration and delivery servers", + "version": "1.0.0", + "home": "http://www.jenkins.org.cn/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304012", + "icon": "", + "ps": "Jenkins, the leading open source automation server, offers hundreds of plugins to support building, deploying, and automating any project." + }, + { + "id": 4, + "sort": 4, + "Top-notch": 1, + "template_id": 4, + "server_name": "gitlab-ce", + "title": "Open source project for a repository management system", + "version": "1.0.0", + "home": "https://about.gitlab.com/", + "type": 1, + "bt_web": 1, + "cpu": 2, + "mem": 2, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "", + "ps": "GitLab is an open source project for repository management system, using Git as a code management tool, and on this basis to build a web service." + }, + { + "id": 5, + "sort": 5, + "Top-notch": 1, + "template_id": 5, + "server_name": "kodbox", + "title": "Web file manager", + "version": "1.0.0", + "home": "https://kodcloud.com/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "\n\n \n\n", + "ps": "kodbox Cloud (formerly Mango Cloud KodExplorer) is the industry's leading government/enterprise private cloud and online document management system." + }, + { + "id": 6, + "sort": 6, + "Top-notch": 1, + "template_id": 6, + "server_name": "mongodb", + "title": "Database based on distributed file storage", + "version": "1.0.0", + "home": "https://www.mongodb.com/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "", + "ps": "It provides a scalable and high-performance data storage solution for WEB applications." + }, + { + "id": 7, + "sort": 7, + "Top-notch": 1, + "template_id": 7, + "server_name": "nexus3", + "title": "Very popular warehouse management software", + "version": "1.0.0", + "home": "https://www.sonatype.com/products/nexus-repository", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "\n\n \n\n", + "ps": "SonatypeNexus3 software repository server, which is both a private software repository server and can serve as a proxy and cache server to fetch content from public software repositories." + }, + { + "id": 8, + "sort": 8, + "Top-notch": 1, + "template_id": 8, + "server_name": "awvs", + "title": "Web Vulnerability Scanner", + "version": "1.0.0", + "home": "https://www.acunetix.com/vulnerability-scanner/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "", + "ps": "It is a complete Web application security testing solution that can be used either on its own or as part of a complex environment." + }, + { + "id": 9, + "sort": 9, + "Top-notch": 1, + "template_id": 9, + "server_name": "kafka", + "title": "Open source distributed event streaming platform", + "version": "1.0.0", + "home": "https://kafka.apache.org/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "", + "ps": "Provides high-performance data pipelines, streaming analytics, data integration, and mission-critical applications." + }, + { + "id": 11, + "sort": 11, + "Top-notch": 1, + "template_id": 11, + "server_name": "solr", + "title": "A standalone enterprise search application server", + "version": "1.0.0", + "home": "https://solr.apache.org/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "\n\n \n\n", + "ps": "Solr is a popular, blazing fast, open-source enterprise search platform." + }, + { + "id": 12, + "sort": 12, + "Top-notch": 1, + "template_id": 12, + "server_name": "gogs", + "title": "A self-service Git service that's easy to set up", + "version": "1.0.0", + "home": "https://gogs.io/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "\n\n \n", + "ps": "The easiest, fastest, and easiest way to set up a self-service Git service." + }, + { + "id": 14, + "sort": 14, + "Top-notch": 1, + "template_id": 14, + "server_name": "teleport", + "title": "An easy to use open source bastion host system", + "version": "1.0.0", + "home": "https://tp4a.com/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "\n\n \n", + "ps": "Teleport is a simple and easy to use open source fortress machine system. It supports RDP/SSH/SFTP/Telnet protocol remote connection and audit management." + }, + { + "id": 15, + "sort": 15, + "Top-notch": 1, + "template_id": 15, + "server_name": "prometheus", + "title": "Leading open source monitoring solution.", + "version": "1.0.0", + "home": "https://prometheus.io/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "", + "ps": "The Prometheus monitoring system and time series database." + }, + { + "id": 16, + "sort": 16, + "Top-notch": 1, + "template_id": 16, + "server_name": "metabase", + "title": "Open source business intelligence tools", + "version": "1.0.0", + "home": "https://www.metabase.com/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "\n\n \n", + "ps": "Let your company explore the data on its own with a friendly user experience and integrated tools for fast analysis." + }, + { + "id": 17, + "sort": 17, + "Top-notch": 1, + "template_id": 17, + "server_name": "grafana", + "title": "Open source data visualization tool", + "version": "1.0.0", + "home": "https://grafana.com/grafana/", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "", + "ps": "With Grafana, you can create, explore, and share all your data through beautiful, flexible dashboards." + }, + { + "id": 18, + "sort": 18, + "Top-notch": 1, + "template_id": 18, + "server_name": "nacos", + "title": "Configuration management and service platform", + "version": "1.0.0", + "home": "https://nacos.io/zh-cn/index.html", + "type": 1, + "bt_web": 1, + "cpu": 1, + "mem": 1, + "disk": 10, + "tip": "lib", + "help": "", + "author": "", + "date": "202304013", + "icon": "\n\n \n\n", + "ps": "A dynamic service discovery, configuration management, and service management platform that makes it easier to build cloud-native applications." + } +] diff --git a/config/menu.json b/config/menu.json index 3d4cc54b..0ad6cdd2 100644 --- a/config/menu.json +++ b/config/menu.json @@ -13,6 +13,13 @@ "id": "memuAsite", "sort": 2 }, + { + "title": "WP Toolkit", + "href": "/wp/toolkit", + "class": "menu_wp_toolkit", + "id": "memuAwptoolkit", + "sort": 2 + }, { "title": "FTP", "href": "/ftp", @@ -105,4 +112,4 @@ "id": "dologin", "sort": 14 } -] \ No newline at end of file +] diff --git a/config/php_versions.json b/config/php_versions.json new file mode 100644 index 00000000..266979ec --- /dev/null +++ b/config/php_versions.json @@ -0,0 +1 @@ +["52","53","54","55","56","70","71","72","73","74","80","81","82","83","84","90","91"] \ No newline at end of file diff --git a/config/safe_autofix.json b/config/safe_autofix.json new file mode 100644 index 00000000..56de2e00 --- /dev/null +++ b/config/safe_autofix.json @@ -0,0 +1,45 @@ +[ + "sw_ping", + "sw_panel_swing", + "sw_ssh_passmin", + "sw_ssh_passmax", + "sw_ssh_clientalive", + "sw_panel_control", + "sw_ssh_notpass", + "sw_cve_2021_4034", + "sw_php_expose", + "sw_ssh_maxauth", + "sw_ssh_security", + "sw_files_recycle_bin", + "sw_kernel_space", + "sw_nginx_server", + "sw_ssh_passwarn", + "sw_site_spath", + "sw_tcp_syn_cookie", + "sw_alias_ls_rm", + "sw_bootloader_mod", + "sw_ssh_v2", + "sw_ftp_umask", + "sw_chmod_sid", + "sw_time_out", + "sw_debug_mode", + "sw_system_user", + "sw_pip_poison", + "sw_docker_mod", + "sw_ftp_root", + "sw_chmod_stickybit", + "sw_telnet_server", + "sw_strace_backdoor", + "sw_mongodb_auth", + "sw_httpd_version_leak", + "sw_php_display_errors", + "sw_php_backdoor", + "sw_httpd_trace_enable", + "sw_nginx_malware", + "sw_ssh_login_grace", + "sw_sudoers_nopasswd", + "sw_suid_dumpable", + "sw_tmp_malware", + "sw_protected_hardlinks", + "sw_protected_symlinks" +] diff --git a/data/pay_type.json b/data/pay_type.json index ab37d8ff..b76928de 100644 --- a/data/pay_type.json +++ b/data/pay_type.json @@ -8,11 +8,10 @@ "price": "188.3", "ps": [ "All paid plugins", - "Low then $0.52/day", - "2 free 1-year DV SSL (for year)", - "Replaceable authorized IP", - "Priority response service", "15 days no reason to refund", + "Replaceable authorized IP", + "2 free 1-year DV SSL (for year)", + "Priority response service", "Paid service group (for year)" ] @@ -201,4 +200,4 @@ "preview": "" }] } -] \ No newline at end of file +] diff --git a/data/softList.conf b/data/softList.conf index d63c585e..c1b8bf1f 100644 --- a/data/softList.conf +++ b/data/softList.conf @@ -6,4 +6,4 @@ {"name":"Tomcat","versions":[{"status":false,"version":"7"},{"status":false,"version":"8"},{"status":false,"version":"9"}],"type":"语言解释器","msg":"java-ee解释器","shell":"tomcat.sh","check":"server/tomcat/bin/catalina.sh"}, {"name":"phpMyAdmin","versions":[{"status":false,"version":"4.0"},{"status":false,"version":"4.4"},{"status":false,"version":"4.7"},{"status":false,"version":"4.8"},{"status":false,"version":"4.9"},{"status":false,"version":"5.0"}],"type":"数据库工具","msg":"Web端MySQL管理工具","shell":"phpmyadmin.sh","check":"server/phpmyadmin/version.pl"}, {"name":"DNS-Server","versions":[{"status":false,"version":"3"}],"type":"DNS server","msg":"DNS Manager","shell":"dns.sh","check":"server/panel/plugin/dns_manager/dns_manager_main.py"}, -{"name":"Mail-Server","versions":[{"status":false,"version":"4"}],"type":"Mail Server","msg":"Mail Server","shell":"mail.sh","check":"server/panel/plugin/mail_sys/mail_sys_main.py"}] \ No newline at end of file +{"name":"Mail-Server","versions":[{"status":false,"version":"3"}],"type":"Mail Server","msg":"Mail Server","shell":"mail.sh","check":"server/panel/plugin/mail_sys/mail_sys_main.py"}] diff --git a/init.sh b/init.sh index 23ab81df..2fe873b7 100644 --- a/init.sh +++ b/init.sh @@ -11,14 +11,25 @@ # Short-Description: starts bt # Description: starts the bt ### END INIT INFO + panel_init(){ + + if [ -f "/etc/redhat-release" ]; then + os_version=$(cat /etc/redhat-release | grep -E "Red Hat|CentOS" | grep -Eo '([0-9]+\.)+[0-9]+' | grep -Eo '^[0-9]') + fi + OPENSSL_VER=$(openssl version|grep -oE '1.0|1.1.0') + if [ "$os_version" == "7" ] || [ "${OPENSSL_VER}" ]; then + if [ -d /usr/local/openssl111 ]; then + export LD_LIBRARY_PATH=/usr/local/openssl111/lib:$LD_LIBRARY_PATH + fi + fi + panel_path=/www/server/panel pidfile=$panel_path/logs/panel.pid cd $panel_path - env_path=$panel_path/pyenv/bin/activate + env_path=$panel_path/pyenv/bin/python3 if [ -f $env_path ];then - source $env_path - pythonV=$panel_path/pyenv/bin/python + pythonV=$panel_path/pyenv/bin/python3 chmod -R 700 $panel_path/pyenv/bin else pythonV=/usr/bin/python @@ -36,9 +47,9 @@ panel_init(){ chmod 700 $panel_path/BT-Task log_file=$panel_path/logs/error.log task_log_file=$panel_path/logs/task.log - if [ -f $panel_path/data/ssl.pl ];then - log_file=/dev/null - fi +# if [ -f $panel_path/data/ssl.pl ];then +# log_file=/dev/null +# fi port=$(cat $panel_path/data/port.pl) } @@ -65,7 +76,7 @@ panel_start() get_panel_pids if [ "$isStart" == '' ];then rm -f $pidfile - panel_port_check + echo -e "Starting Bt-Panel...\c" nohup $panel_path/BT-Panel >> $log_file 2>&1 & isStart="" @@ -81,6 +92,7 @@ panel_start() fi done if [ "$isStart" == '' ];then + panel_port_check echo -e "\033[31mfailed\033[0m" echo '------------------------------------------------------' tail -n 20 $log_file @@ -91,7 +103,7 @@ panel_start() else echo "Starting Bt-Panel... Bt-Panel (pid $(echo $isStart)) already running" fi - + get_task_pids if [ "$isStart" == '' ];then echo -e "Starting Bt-Tasks... \c" @@ -114,7 +126,7 @@ panel_start() panel_port_check() { - is_process=$(lsof -n -P -i:$port|grep LISTEN|grep -v grep|awk '{print $1}'|sort|uniq|xargs) + is_process=$(lsof -n -P -i:$port -sTCP:LISTEN|grep LISTEN|grep -v grep|awk '{print $1}'|sort|uniq|xargs) for pn in ${is_process[@]} do if [ "$pn" = "nginx" ];then @@ -172,6 +184,14 @@ panel_port_check() fi } +stop_webserver() +{ + webserver_ctl=$panel_path/script/webserver-ctl.sh + if [ -f $webserver_ctl ];then + bash $webserver_ctl stop &> /dev/null + fi +} + panel_stop() { echo -e "Stopping Bt-Tasks...\c"; @@ -194,6 +214,9 @@ panel_stop() if [ -f $pidfile ];then rm -f $pidfile fi + + stop_webserver + echo -e " \033[32mdone\033[0m" } @@ -206,7 +229,7 @@ panel_status() else echo -e "\033[31mBt-Panel not running\033[0m" fi - + get_task_pids if [ "$isStart" != '' ];then echo -e "\033[32mBt-Task (pid $isStart) already running\033[0m" @@ -218,60 +241,77 @@ panel_status() panel_reload() { isStart=$(ps aux|grep 'runserver:app'|grep -v grep|awk '{print $2}') - if [ "$isStart" != '' ];then + if [ "$isStart" != '' ];then kill -9 $isStart sleep 0.5 fi get_panel_pids + stop_webserver if [ "$isStart" != '' ];then + get_panel_pids + for p in ${arr[@]} + do + kill -9 $p + done + rm -f $pidfile + echo -e "Reload Bt-Panel.\c"; + nohup $panel_path/BT-Panel >> $log_file 2>&1 & + isStart="" + n=0 + while [[ "$isStart" == "" ]]; + do + echo -e ".\c" + sleep 0.5 + get_panel_pids + let n+=1 + if [ $n -gt 8 ];then + break; + fi + done + if [ "$isStart" == '' ];then + panel_port_check + echo -e "\033[31mfailed\033[0m" + echo '------------------------------------------------------' + tail -n 20 $log_file + echo '------------------------------------------------------' + echo -e "\033[31mError: BT-Panel service startup failed.\033[0m" + return; + fi - get_panel_pids - for p in ${arr[@]} - do - kill -9 $p - done - rm -f $pidfile - panel_port_check - echo -e "Reload Bt-Panel.\c"; - nohup $panel_path/BT-Panel >> $log_file 2>&1 & - isStart="" - n=0 - while [[ "$isStart" == "" ]]; - do - echo -e ".\c" - sleep 0.5 - get_panel_pids - let n+=1 - if [ $n -gt 8 ];then - break; - fi - done - if [ "$isStart" == '' ];then - echo -e "\033[31mfailed\033[0m" - echo '------------------------------------------------------' - tail -n 20 $log_file - echo '------------------------------------------------------' - echo -e "\033[31mError: BT-Panel service startup failed.\033[0m" - return; + echo -e " \033[32mdone\033[0m" + else + echo -e "\033[31mBt-Panel not running\033[0m" + panel_start fi - echo -e " \033[32mdone\033[0m" - else - echo -e "\033[31mBt-Panel not running\033[0m" - panel_start - fi } install_used() { - if [ ! -f $panel_path/aliyun.pl ];then - return; + if [ -f $panel_path/aliyun.pl ];then + password=$(cat /dev/urandom | head -n 16 | md5sum | head -c 12) + username=$($pythonV $panel_path/tools.py panel $password) + echo "$password" > $panel_path/default.pl + rm -f $panel_path/aliyun.pl + chattr +i $panel_path/default.pl fi - password=$(cat /dev/urandom | head -n 16 | md5sum | head -c 12) - username=$($pythonV $panel_path/tools.py panel $password) - safe_path=$(cat /dev/urandom | head -n 16 | md5sum | head -c 8) - echo "/$safe_path" > $panel_path/data/admin_path.pl - echo "$password" > $panel_path/default.pl - rm -f $panel_path/aliyun.pl + + if [ -f $panel_path/php_mysql_auto.pl ];then + bash $panel_path/script/mysql_auto.sh &> /dev/null + bash $panel_path/script/php_auto.sh &> /dev/null + rm -f $panel_path/php_mysql_auto.pl + fi + + pip_file=/www/server/panel/pyenv/bin/pip3 + python_file=/www/server/panel/pyenv/bin/python3 + if [ -f $pip_file ];then + is_rep=$(ls -l /usr/bin/btpip|grep pip3.) + if [ "${is_rep}" != "" ];then + rm -f /usr/bin/btpip /usr/bin/btpython + ln -sf $pip_file /usr/bin/btpip + ln -sf $python_file /usr/bin/btpython + fi + fi + } error_logs() @@ -290,7 +330,7 @@ case "$1" in ;; 'restart') panel_stop - sleep 1 + sleep 1 panel_start ;; 'reload') @@ -306,22 +346,31 @@ case "$1" in $pythonV $panel_path/tools.py cli $2 ;; 'default') - LOCAL_IP=$(ip addr | grep -E -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | grep -E -v "^127\.|^255\.|^0\." | head -n 1) port=$(cat $panel_path/data/port.pl) password=$(cat $panel_path/default.pl) if [ -f $panel_path/data/domain.conf ];then address=$(cat $panel_path/data/domain.conf) fi + auth_path=/login if [ -f $panel_path/data/admin_path.pl ];then auth_path=$(cat $panel_path/data/admin_path.pl) fi if [ "$address" = "" ];then - address=$(curl -sS --connect-timeout 10 -m 60 https://www.aapanel.com/api/common/getClientIP) + address=$(curl -sS --connect-timeout 10 -m 20 https://www.aapanel.com/api/common/getClientIP) + # IPV6_REGEX="^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$" + IPV6_REGEX="^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$" + if [[ $address =~ $IPV6_REGEX ]]; then + address=$(echo "[$address]") + fi fi - pool=http - if [ -f $panel_path/data/ssl.pl ];then - pool=https - fi + pool=http + if [ -f $panel_path/data/ssl.pl ];then + pool=https + fi + if [ "$auth_path" == "/" ];then + auth_path=/login + fi + LOCAL_IP=$(ip addr | grep -E -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | grep -E -v "^127\.|^255\.|^0\." | head -n 1) echo -e "==================================================================" echo -e "\033[32maaPanel default info!\033[0m" echo -e "==================================================================" @@ -331,7 +380,7 @@ case "$1" in echo -e "password: $password" echo -e "\033[33mWarning:\033[0m" echo -e "\033[33mIf you cannot access the panel, \033[0m" - echo -e "\033[33mrelease the following port (7800|888|80|443|20|21) in the security group\033[0m" + echo -e "\033[33mrelease the following port ($port|888|80|443|20|21) in the security group\033[0m" echo -e "==================================================================" ;; *) diff --git a/install/public.sh b/install/public.sh index 11659009..09104268 100644 --- a/install/public.sh +++ b/install/public.sh @@ -141,7 +141,7 @@ if [ -d "/www/server/phpmyadmin/pma" ];then rm -rf /www/server/phpmyadmin/pma EN_CHECK=$(cat /www/server/panel/config/config.json |grep English) if [ "${EN_CHECK}" ];then - curl http://download.bt.cn/install/update6_en.sh|bash + curl http://download.bt.cn/install/update_7.x_en.sh|bash else curl http://download.bt.cn/install/update6.sh|bash fi diff --git a/mod/__init__.py b/mod/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/base/__init__.py b/mod/base/__init__.py new file mode 100644 index 00000000..0bba13cd --- /dev/null +++ b/mod/base/__init__.py @@ -0,0 +1,31 @@ +import time +from typing import Dict, List, Tuple, Union + +from .process import RealProcess, Process +from .process import RealUser, User +from .process import RealServer, Server + + +def json_response( + status: bool, + msg: str = None, + data: Union[Dict, List, Tuple, bool, str, int, float] = None, + code: int = 0, + args: Union[List[str], Tuple[str]] = None, +): + if isinstance(msg, str) and args is not None: + for i in range(len(args)): + rep = '{' + str(i + 1) + '}' + msg = msg.replace(rep, args[i]) + + if msg is None: + msg = "ok" + + return { + "status": status, + "msg": msg, + "data": data, + "code": code, + "timestamp": int(time.time()) + } + diff --git a/mod/base/backup_tool/__init__.py b/mod/base/backup_tool/__init__.py new file mode 100644 index 00000000..94552888 --- /dev/null +++ b/mod/base/backup_tool/__init__.py @@ -0,0 +1,135 @@ +import os +import time +from hashlib import md5 +from typing import Optional, List, Union, Dict, Any + +from .util import DB, ExecShell, write_file, write_log +from .versions_tool import VersionTool + + +class BackupTool: + + def __init__(self): + self._backup_path: Optional[str] = None + self._sub_dir_name: str = "" + self.exec_log_file = "/tmp/mod_backup_exec.log" + + @staticmethod + def _hash_src_name(name: Union[str, bytes]) -> str: + if isinstance(name, str): + name = name.encode('utf-8') + md5_obj = md5() + md5_obj.update(name) + return md5_obj.hexdigest() + + @property + def backup_path(self) -> str: + if self._backup_path is None: + config_data = DB("config").where("id=?", (1,)).select() + if isinstance(config_data, dict): + path = config_data["backup_path"] + else: # 查询出错 + path = "/www/backup" + self._backup_path = path + return self._backup_path + + # sub_dir 可以设置为多级子目录, 如 "site/aaa", 或使用列表传递如:["site", "aaa"] + def set_sub_dir(self, sub_dir: Union[str, List[str]]) -> Optional[str]: + if isinstance(sub_dir, str): + self._sub_dir_name = sub_dir.strip("./") + elif isinstance(sub_dir, list): + self._sub_dir_name = "/".join(filter(None, [i.strip("./") for i in sub_dir])) + else: + return "不支持的类型设置" + + def backup(self, + src: str, # 源文件位置 + backup_path: Optional[str] = None, # 备份位置 + sub_dir: Union[str, List[str]] = None, # 备份目录的子目录 + site_info: Dict[str, Any] = None, # 关联的站点信息, 必须包含 id 和 name + sync=False # 是否同步执行, 默认异步由单独的线程放入后台执行 + ) -> Optional[str]: # 返回执行错误的信息 + + if not os.path.exists(src): + return "源路径不存在" + if backup_path is None: + backup_path = self.backup_path + + if not os.path.exists(backup_path): + return "备份目录不存在" + if sub_dir is not None: + set_res = self.set_sub_dir(sub_dir) + if set_res is not None: + return set_res + + target_path = os.path.join(backup_path, self._sub_dir_name) + if not os.path.isdir(target_path): + os.makedirs(target_path) + zip_name = "{}_{}.tar.gz".format(os.path.basename(src), time.strftime('%Y%m%d_%H%M%S', time.localtime())) + if sync: + return self._sync_backup(src, target_path, zip_name, site_info) + else: + return self._async_backup(src, target_path, zip_name, site_info) + + def _sync_backup(self, src: str, target_path: str, zip_name: str, site_info: dict): + try: + write_file(self.exec_log_file, "") + execStr = ("cd {} && " + "tar -zcvf '{}' --exclude=.user.ini ./ 2>&1 > {} \n" + "echo '---备份执行完成---' >> {}" + ).format(src, os.path.join(target_path, zip_name), self.exec_log_file, self.exec_log_file) + ExecShell(execStr) + if site_info is not None and "id" in site_info and "name" in site_info: + DB('backup').add( + 'type,name,pid,filename,size,addtime', + (0, zip_name, site_info["id"], os.path.join(target_path, zip_name), 0, self.get_date()) + ) + write_log('TYPE_SITE', 'SITE_BACKUP_SUCCESS', (site_info["name"],)) + except: + return "备份执行失败" + + def _async_backup(self, src: str, target_path: str, zip_name: str, site_info: dict): + import threading + + hash_name = self._hash_src_name(src) + backup_tip_path = "/tmp/mod_backup_tip" + if os.path.exists(backup_tip_path): + os.makedirs(backup_tip_path) + + tip_file = os.path.join(backup_tip_path, hash_name) + if os.path.isfile(tip_file): + mtime = os.stat(tip_file).st_mtime + if time.time() - mtime > 60 * 20: # 20 分钟未执行,认为出现在不可抗力,导致备份失败,允许再次备份 + os.remove(tip_file) + else: + return "备份进行中,请勿继续操作" + + write_file(tip_file, "") + + def _back_p(): + try: + write_file(self.exec_log_file, "") + execStr = ("cd {} && " + "tar -zcvf '{}' --exclude=.user.ini ./ 2>&1 > {} \n" + "echo '---备份执行完成---' >> {}" + ).format(src, os.path.join(target_path, zip_name), self.exec_log_file, self.exec_log_file) + ExecShell(execStr) + if site_info is not None and "id" in site_info and "name" in site_info: + DB('backup').add( + 'type,name,pid,filename,size,addtime', + (0, zip_name, site_info["id"], os.path.join(target_path, zip_name), 0, self.get_date()) + ) + write_log('TYPE_SITE', 'SITE_BACKUP_SUCCESS', (site_info["name"],)) + except: + pass + finally: + if os.path.exists(tip_file): + os.remove(tip_file) + + t = threading.Thread(target=_back_p) + t.start() + + @staticmethod + def get_date(): + # 取格式时间 + return time.strftime('%Y-%m-%d %X', time.localtime()) diff --git a/mod/base/backup_tool/util.py b/mod/base/backup_tool/util.py new file mode 100644 index 00000000..d8f96f6f --- /dev/null +++ b/mod/base/backup_tool/util.py @@ -0,0 +1,65 @@ +import sys +from typing import Optional, Callable, Tuple, Union + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public + +ExecShell: Callable = public.ExecShell +write_log: Callable[[str, str, Tuple], Union[int, str, type(None)]] = public.WriteLog + + +def write_file(filename: str, s_body: str, mode='w+') -> bool: + """ + 写入文件内容 + @filename 文件名 + @s_body 欲写入的内容 + return bool 若文件不存在则尝试自动创建 + """ + try: + fp = open(filename, mode=mode) + fp.write(s_body) + fp.close() + return True + except: + try: + fp = open(filename, mode=mode, encoding="utf-8") + fp.write(s_body) + fp.close() + return True + except: + return False + + +def read_file(filename, mode='r') -> Optional[str]: + """ + 读取文件内容 + @filename 文件名 + return string(bin) 若文件不存在,则返回None + """ + import os + if not os.path.exists(filename): + return None + fp = None + try: + fp = open(filename, mode=mode) + f_body = fp.read() + except: + return None + finally: + if fp and not fp.closed: + fp.close() + return f_body + + +class _DB: + + def __call__(self, table: str): + import db + with db.Sql() as t: + t.table(table) + return t + + +DB = _DB() diff --git a/mod/base/backup_tool/versions_tool.py b/mod/base/backup_tool/versions_tool.py new file mode 100644 index 00000000..1e9796b5 --- /dev/null +++ b/mod/base/backup_tool/versions_tool.py @@ -0,0 +1,291 @@ +import json +import os +import shutil +import tarfile +import time +from hashlib import md5 +from typing import Optional, List, Union, Dict, Any + +from .util import DB, ExecShell, write_file, write_log, read_file + + +class VersionTool: + _config_file = "/www/server/panel/data/version_config.json" + + def __init__(self): + self._config: Optional[Dict[str, List[Dict[str, Any]]]] = None + self._pack_class = BasePack + self.pack_path = "/www/backup/versions" + + @property + def config(self) -> Dict[str, List[Dict[str, Any]]]: + if self._config is not None: + return self._config + + data = {} + try: + res = read_file(self._config_file) + if isinstance(res, str): + data = json.loads(res) + except (json.JSONDecoder, TypeError, ValueError): + pass + self._config = data + return self._config + + def save_config(self): + if self._config is not None: + write_file(self._config_file, json.dumps(self._config)) + + def add_to_config(self, data: dict): + project_name = data.get("project_name") + self._config = None + if project_name not in self.config: + self.config[project_name] = [] + self.config[project_name].append(data) + self.save_config() + + def set_pack_class(self, pack_cls): + self._pack_class = pack_cls + + def version_list(self, project_name: str): + if project_name in self.config: + return self.config[project_name] + return [] + + def get_version_info(self, project_name: str, version: str) -> Optional[dict]: + if project_name in self.config: + for i in self.config[project_name]: + if i.get("version") == version: + return i + return None + + # 把某个路径下的文件打包并发布为一个版本 + def publish_by_src_path(self, + project_name: str, # 名称 + src_path: str, # 源路径 + version: str, # 版本号 + ps: Optional[str] = None, # 备注 + other: Optional[dict] = None, # 其他信息 + sync: bool = False, # 是否同步执行 + ): + + if project_name in self.config: + for i in self.config[project_name]: + if i["version"] == version: + return "当前版本已存在" + if not os.path.isdir(src_path): + return "源路径不存在" + + if ps is None: + ps = '' + + if other is None: + other = {} + + zip_name = "{}_{}.tar.gz".format( + os.path.basename(src_path), time.strftime('%Y%m%d_%H%M%S', time.localtime()) + ) + + data = { + "project_name": project_name, + "version": version, + "ps": ps, + "other": other, + "zip_name": zip_name, + "backup_time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + } + return self._pack_class(src_path, self.pack_path, zip_name, sync=sync, vt=self, data=data)(**other) + + def recover(self, + project_name: str, # 名称 + version: str, # 版本 + target_path: str, # 目标路径 + run_path=None + ): + if not run_path: + run_path = target_path + if project_name not in self.config: + return '项目不存在' + + target = None + for i in self.config[project_name]: + if i["version"] == version: + target = i + break + + if target is None: + return '版本不存在' + + file = os.path.join(self.pack_path, target["zip_name"]) + if not os.path.exists(file): + return '版本文件丢失' + + tmp_path = '/tmp/version_{}'.format(int(time.time())) + tar = tarfile.open(file, mode='r') + tar.extractall(tmp_path) + user_data = None + if os.path.exists(target_path): + ExecShell("chattr -i -R {}/".format(target_path)) + user_data = read_file(run_path + "/.user.ini") + ExecShell("rm -rf {}".format(target_path)) + os.makedirs(target_path) + if not os.path.exists(target_path): + os.makedirs(target_path) + ExecShell(r"\cp -rf {}/* {}".format(tmp_path, target_path)) + if user_data: + write_file(target_path + "/.user.ini", run_path) + ExecShell("chattr +i {}/.user.ini".format(run_path)) + ExecShell("rm -rf {}".format(tmp_path)) + return True + + def publish_by_file(self, + project_name: str, # 名称 + src_file: str, # 源路径 + version: str, # 版本号 + ps: Optional[str] = None, # 备注 + other: Optional[dict] = None, # 其他信息 + ): + + if project_name in self.config: + for i in self.config[project_name]: + if i["version"] == version: + return "当前版本已存在" + + if not os.path.isfile(src_file): + return "源路径不存在" + + if ps is None: + ps = '' + + if other is None: + other = {} + + zip_name = os.path.basename(src_file) + + data = { + "project_name": project_name, + "version": version, + "ps": ps, + "other": other, + "zip_name": zip_name, + "backup_time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + } + try: + shutil.copy(src_file, self.pack_path + "/" + zip_name) + except: + return "文件保存失败" + self.add_to_config(data) + return None + + def remove(self, + project_name: str, # 名称 + version: str, # 版本 + ) -> Optional[str]: + + if project_name not in self.config: + return '项目不存在' + + target = None + for i in self.config[project_name]: + if i["version"] == version: + target = i + break + + if target is None: + return '版本不存在' + + file = os.path.join(self.pack_path, target["zip_name"]) + if os.path.isfile(file): + os.remove(file) + + self.config[project_name].remove(target) + + self.save_config() + return None + + def set_ps(self, name: str, version: str, ps: str): + [i.update({'ps': ps}) for i in self.config[name] if i["version"] == version] + self.save_config() + return True + + +class BasePack: + exec_log_file = "/tmp/project_pack.log" + + def __init__(self, src_path, target_path, zip_name, sync=False, vt: VersionTool = None, data: dict = None): + self.src_path = src_path + self.target_path = target_path + self.zip_name = zip_name + self.sync = sync + self.v = vt + self._add_data = data + + def save_config(self): + self.v.add_to_config(self._add_data) + + def __call__(self, *args, **kwargs) -> Optional[str]: + if not os.path.exists(self.src_path): + return "源路径不存在" + target_path = "/www/backup/versions" + + if not os.path.isdir(target_path): + os.makedirs(target_path) + if self.sync: + return self._sync_backup(self.src_path, target_path, self.zip_name) + else: + return self._async_backup(self.src_path, target_path, self.zip_name) + + def _sync_backup(self, src: str, target_path: str, zip_name: str) -> Optional[str]: + try: + write_file(self.exec_log_file, "") + execStr = ("cd {} && " + "tar -zcvf '{}' --exclude=.user.ini ./ 2>&1 > {} \n" + "echo '---打包执行完成---' >> {}" + ).format(src, os.path.join(target_path, zip_name), self.exec_log_file, self.exec_log_file) + ExecShell(execStr) + self.save_config() + except: + return "打包执行失败" + + def _async_backup(self, src: str, target_path: str, zip_name: str): + import threading + hash_name = self._hash_src_name(src) + backup_tip_path = "/tmp/mod_version_tip" + if os.path.exists(backup_tip_path): + os.makedirs(backup_tip_path) + + tip_file = os.path.join(backup_tip_path, hash_name) + if os.path.isfile(tip_file): + mtime = os.stat(tip_file).st_mtime + if time.time() - mtime > 60 * 20: # 20 分钟未执行,认为出现在不可抗力,导致备份失败,允许再次备份 + os.remove(tip_file) + else: + return "打包进行中,请勿继续操作" + + write_file(tip_file, "") + + def _back_p(): + try: + write_file(self.exec_log_file, "") + execStr = ("cd {} && " + "tar -zcvf '{}' --exclude=.user.ini ./ 2>&1 > {} \n" + "echo '---备份执行完成---' >> {}" + ).format(src, os.path.join(target_path, zip_name), self.exec_log_file, self.exec_log_file) + ExecShell(execStr) + self.save_config() + except: + pass + finally: + if os.path.exists(tip_file): + os.remove(tip_file) + + t = threading.Thread(target=_back_p) + t.start() + + @staticmethod + def _hash_src_name(name: Union[str, bytes]) -> str: + if isinstance(name, str): + name = name.encode('utf-8') + md5_obj = md5() + md5_obj.update(name) + return md5_obj.hexdigest() diff --git a/mod/base/database_tool/__init__.py b/mod/base/database_tool/__init__.py new file mode 100644 index 00000000..3dc4ed48 --- /dev/null +++ b/mod/base/database_tool/__init__.py @@ -0,0 +1,53 @@ +from .pgsql import PgsqlTool +from .mongodb import MongodbTool +from .mysql import MysqlTool +from .sql_server import SQLServerTool + +from typing import Optional + +DB_TYPE = ( + "pgsql", + "mongodb", + "mysql", + "sqlserver" +) + + +def add_database(db_type: str, data: dict) -> Optional[str]: + """ + data: 中包含的有效参数为 + database_name:数据库名称 + server_id:数据库 id + db_user:数据库用户名 + password:数据库用户的密码 + dataAccess :链接限制方式 如:ip + address:可允许使用的ip, 配合上一个参数使用 + codeing: 编码 + ps:备注 + listen_ip: pgsql 有效,可设置访问地址 + """ + if db_type not in DB_TYPE: + return "错误的数据库类型" + + if db_type == "pgsql": + tool = PgsqlTool() + elif db_type == "mongodb": + tool = MongodbTool() + elif db_type == "mysql": + tool = MysqlTool() + else: + tool = SQLServerTool() + + f, msg = tool.add_database(data.pop("server_id"), data.pop("database_name"), **data) + if not f: + return msg + return None + + +__all__ = [ + "PgsqlTool", + "MongodbTool", + "MysqlTool", + "SQLServerTool", + "add_database", +] diff --git a/mod/base/database_tool/base.py b/mod/base/database_tool/base.py new file mode 100644 index 00000000..2540c29b --- /dev/null +++ b/mod/base/database_tool/base.py @@ -0,0 +1,34 @@ +import sys +from typing import List, Dict, Optional +from .util import DB + + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +from db_mysql import panelMysql +from database import database +from databaseModel.mongodbModel import main as mongodb +from databaseModel.pgsqlModel import main as pgsql +from databaseModel.sqlserverModel import main as sqlserver + + +class BaseDatabaseTool: + _type_name = "" + + def local_server_info(self) -> Optional[Dict]: + raise NotImplementedError() + + # 获取所有可以管理的服务器的信息 + def server_list(self) -> List[Dict]: + data = DB('database_servers').where("LOWER(db_type)=LOWER('?')", (self._type_name, )).select() + if not isinstance(data, list): + data = [] + local_server = self.local_server_info() + if local_server is not None: + data.insert(0, local_server) + return data + + # 添加一个数据库 + def add_database(self, server_id: int, database_name: str, **kwargs) -> List[Dict]: + raise NotImplementedError() diff --git a/mod/base/database_tool/mongodb.py b/mod/base/database_tool/mongodb.py new file mode 100644 index 00000000..016f8ee4 --- /dev/null +++ b/mod/base/database_tool/mongodb.py @@ -0,0 +1,51 @@ +import os +import re + +from typing import Optional, Dict, List, Union, Tuple + +from .base import BaseDatabaseTool, mongodb +from .util import read_file, GET_CLASS + + +class MongodbTool(BaseDatabaseTool): + _type_name = "mongodb" + + def local_server_info(self) -> Optional[Dict]: + bin_path = "/www/server/mongodb/bin/mongod" + if not os.path.isfile(bin_path): + return None + + conf_file = '/www/server/mongodb/config.conf' + conf = read_file(conf_file) + default_port = 27017 + if not isinstance(conf, str): + port = default_port + else: + rep_port = re.compile(r"\s*port\s*:\s*(?P\d+)", re.M) + port_res = rep_port.search(conf) + if not port_res: + port = default_port + else: + port = int(port_res.group("port")) + + return { + 'id': 0, + 'db_host': '127.0.0.1', + 'db_port': port, + 'db_user': 'root', + 'db_password': '', + 'ps': '本地服务器', + 'addtime': 0 + } + + # 添加一个数据库 + def add_database(self, server_id: int, database_name: str, **kwargs) -> Tuple[bool, str]: + get_obj = GET_CLASS() + get_obj.name = database_name + get_obj.sid = server_id + get_obj.ps = kwargs.get("ps", "") + res = mongodb().AddDatabase(get_obj) + if res["status"] is True: + return True, "添加成功" + else: + return False, res['msg'] diff --git a/mod/base/database_tool/mysql.py b/mod/base/database_tool/mysql.py new file mode 100644 index 00000000..cbe21583 --- /dev/null +++ b/mod/base/database_tool/mysql.py @@ -0,0 +1,98 @@ +import os +import re + +from typing import Optional, Dict, List, Union, Tuple + +from .base import BaseDatabaseTool, panelMysql, database +from .util import read_file, write_file, DB, GET_CLASS + + +class MysqlTool(BaseDatabaseTool): + _type_name = "mysql" + + def local_server_info(self) -> Optional[Dict]: + bin_path = "/www/server/mysql/bin/mysql" + if not os.path.isfile(bin_path): + return None + + conf_file = '/etc/my.cnf' + conf = read_file(conf_file) + default_port = 3306 + if not isinstance(conf, str): + port = default_port + else: + rep_port = re.compile(r"\s*port\s*=\s*(?P\d+)", re.M) + port_res = rep_port.search(conf) + if not port_res: + port = default_port + else: + port = int(port_res.group("port")) + + return { + 'id': 0, + 'db_host': '127.0.0.1', + 'db_port': port, + 'db_user': 'root', + 'db_password': '', + 'ps': '本地服务器', + 'addtime': 0 + } + + # 检测服务是否可以链接 + # def server_status(self, server_id: int) -> Union[Dict, str]: + # """ + # 数据库状态检测 + # """ + # db_name = None + # if server_id != 0: + # conn_config = DB("database_servers").where("id=? AND LOWER(db_type)=LOWER('mysql')", (server_id,)).find() + # if not conn_config: + # return "远程数据库信息不存在!" + # conn_config["db_name"] = None + # db_user = conn_config["db_user"] + # root_password = conn_config["db_password"] + # db_host = conn_config["db_host"] + # db_port = conn_config["db_port"] + # else: + # db_user = "root" + # root_password = DB("config").where("id=?", (1,)).getField("mysql_root") + # db_host = "localhost" + # try: + # db_port = int(panelMysql().query("show global variables like 'port'")[0][1]) + # except: + # db_port = 3306 + # mysql_obj = panelMysql() + # flag = mysql_obj.set_host(db_host, db_port, db_name, db_user, root_password) + # + # error = '' + # db_status = True + # if flag is False: + # db_status = False + # error = mysql_obj._ex + # + # return { + # "status": True, + # 'error': str(error), + # "msg": "正常" if db_status is True else "异常", + # "db_status": db_status + # } + + # 添加一个数据库 + def add_database(self, server_id: int, database_name: str, **kwargs) -> Tuple[bool, str]: + get_obj = GET_CLASS() + get_obj.name = database_name + get_obj.sid = server_id + get_obj.db_user = kwargs.get("db_user", "") + get_obj.password = kwargs.get("password", "") + get_obj.dataAccess = kwargs.get("dataAccess", "") + get_obj.address = kwargs.get("address", "") + get_obj.codeing = kwargs.get("codeing", "") + get_obj.dtype = "MySQL" + get_obj.ps = kwargs.get("ps", "") + get_obj.host = kwargs.get("host", "") + get_obj.pid = str(kwargs.get("pid", '0')) + res = database().AddDatabase(get_obj) + if res["status"] is True: + return True, "添加成功" + else: + return False, res['msg'] diff --git a/mod/base/database_tool/pgsql.py b/mod/base/database_tool/pgsql.py new file mode 100644 index 00000000..dad3decb --- /dev/null +++ b/mod/base/database_tool/pgsql.py @@ -0,0 +1,54 @@ +import os +import re + +from typing import Optional, Dict, List, Union, Tuple + +from .base import BaseDatabaseTool, pgsql +from .util import read_file, GET_CLASS + + +class PgsqlTool(BaseDatabaseTool): + _type_name = "pgsql" + + def local_server_info(self) -> Optional[Dict]: + bin_path = "/www/server/pgsql/bin/postgres" + if not os.path.isfile(bin_path): + return None + + conf_file = '/www/server/pgsql/data/postgresql.conf' + conf = read_file(conf_file) + default_port = 5432 + if not isinstance(conf, str): + port = default_port + else: + rep_port = re.compile(r"\s*port\s*=\s*(?P\d+)", re.M) + port_res = rep_port.search(conf) + if not port_res: + port = default_port + else: + port = int(port_res.group("port")) + + return { + 'id': 0, + 'db_host': '127.0.0.1', + 'db_port': port, + 'db_user': 'root', + 'db_password': '', + 'ps': '本地服务器', + 'addtime': 0 + } + + # 添加一个数据库 + def add_database(self, server_id: int, database_name: str, **kwargs) -> Tuple[bool, str]: + get_obj = GET_CLASS() + get_obj.name = database_name + get_obj.sid = server_id + get_obj.ps = kwargs.get("ps", "") + get_obj.db_user = kwargs.get("db_user", "") + get_obj.password = kwargs.get("password", "") + get_obj.listen_ip = kwargs.get("listen_ip", "") + res = pgsql().AddDatabase(get_obj) + if res["status"] is True: + return True, "添加成功" + else: + return False, res['msg'] diff --git a/mod/base/database_tool/sql_server.py b/mod/base/database_tool/sql_server.py new file mode 100644 index 00000000..278f99f9 --- /dev/null +++ b/mod/base/database_tool/sql_server.py @@ -0,0 +1,28 @@ +import os +import re + +from typing import Optional, Dict, List, Union, Tuple + +from .base import BaseDatabaseTool, sqlserver +from .util import read_file, GET_CLASS + + +class SQLServerTool(BaseDatabaseTool): + _type_name = "sqlserver" + + def local_server_info(self) -> Optional[Dict]: + return None + + # 添加一个数据库 + def add_database(self, server_id: int, database_name: str, **kwargs) -> Tuple[bool, str]: + get_obj = GET_CLASS() + get_obj.name = database_name + get_obj.sid = server_id + get_obj.ps = kwargs.get("ps", "") + get_obj.db_user = kwargs.get("db_user", "") + get_obj.password = kwargs.get("password", "") + res = sqlserver().AddDatabase(get_obj) + if res["status"] is True: + return True, "添加成功" + else: + return False, res['msg'] diff --git a/mod/base/database_tool/util.py b/mod/base/database_tool/util.py new file mode 100644 index 00000000..b21df69c --- /dev/null +++ b/mod/base/database_tool/util.py @@ -0,0 +1,68 @@ +import os +import sys +from typing import Optional, Tuple, Callable + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + +import public + + +def write_file(filename: str, s_body: str, mode='w+') -> bool: + """ + 写入文件内容 + @filename 文件名 + @s_body 欲写入的内容 + return bool 若文件不存在则尝试自动创建 + """ + try: + fp = open(filename, mode=mode) + fp.write(s_body) + fp.close() + return True + except: + try: + fp = open(filename, mode=mode, encoding="utf-8") + fp.write(s_body) + fp.close() + return True + except: + return False + + +def read_file(filename, mode='r') -> Optional[str]: + """ + 读取文件内容 + @filename 文件名 + return string(bin) 若文件不存在,则返回None + """ + import os + if not os.path.exists(filename): + return None + fp = None + try: + fp = open(filename, mode=mode) + f_body = fp.read() + except: + return None + finally: + if fp and not fp.closed: + fp.close() + return f_body + + +class _DB: + + def __call__(self, table: str): + import db + with db.Sql() as t: + t.table(table) + return t + + +DB = _DB() + +GET_CLASS = public.dict_obj + +ExecShell: Callable = public.ExecShell diff --git a/mod/base/git_tool/__init__.py b/mod/base/git_tool/__init__.py new file mode 100644 index 00000000..0bf72f8c --- /dev/null +++ b/mod/base/git_tool/__init__.py @@ -0,0 +1 @@ +from .tool import GitTool, GitMager, RealGitMager diff --git a/mod/base/git_tool/install.py b/mod/base/git_tool/install.py new file mode 100644 index 00000000..afa9b4f7 --- /dev/null +++ b/mod/base/git_tool/install.py @@ -0,0 +1,40 @@ +import re +from .util import ExecShell + + +def installed(): + sh_str = "git --version" + out, error = ExecShell(sh_str) + if re.search(r"git\s+version\s+(\d+\.){1,4}\d+", out): + return True + return False + + +def version_1_5_3() -> bool: + sh_str = "git --version" + out, error = ExecShell(sh_str) + res = re.search(r"git\s+version\s+(?P(\d+\.){1,4}\d+)", out) + if not res: + return False + ver = [int(i) for i in res.group('v').split(".")] + if len(ver) < 3: + ver.extend([0] * (3 - len(ver))) + if ver[0] > 1: + return True + elif ver[0] == 1 and ver[1] > 5: + return True + elif ver[0] == 1 and ver[1] == 5 and ver[3] >= 3: + return True + return False + + +def install_git(): + check_str = "Git installed successfully" + if not installed(): + script_path = "/www/server/panel/mod/base/git_tool/install.sh" + out, error = ExecShell("bash {}".format(script_path)) + if out.find(check_str) != -1: + return True + else: + return False + return True diff --git a/mod/base/git_tool/install.sh b/mod/base/git_tool/install.sh new file mode 100644 index 00000000..697fef0e --- /dev/null +++ b/mod/base/git_tool/install.sh @@ -0,0 +1,26 @@ + +if command -v apt-get &> /dev/null; then + package_manager="apt-get" +elif command -v yum &> /dev/null; then + package_manager="yum" +else + echo "Git installation failed." + exit 1 +fi + +# 安装Git +if [ "$package_manager" = "apt-get" ]; then + apt-get update + apt-get install git -y +elif [ "$package_manager" = "yum" ]; then + yum install git -y +fi + +# 验证Git安装 +git_version=$(git --version) +# shellcheck disable=SC2181 +if [ $? -eq 0 ]; then + echo "Git installed successfully. Version: $git_version" +else + echo "Git installation failed." +fi \ No newline at end of file diff --git a/mod/base/git_tool/tool.py b/mod/base/git_tool/tool.py new file mode 100644 index 00000000..ad761d34 --- /dev/null +++ b/mod/base/git_tool/tool.py @@ -0,0 +1,543 @@ +import json +import os +import re +import shutil +import time + +from .install import installed, install_git, version_1_5_3 +from .util import read_file, write_file, ExecShell, set_ownership +from typing import Optional, Dict, Union, List +from urllib3.util import parse_url, Url +from mod.base import json_response + +GIT_TMP_PATH = "/www/server/git_tmp" + + +class GitTool: + + def __init__(self, project_path: str, git_url: str, user_config: Optional[dict] = None, git_id: str = ""): + self.git_url = git_url + self.get_id = git_id + self.project_path = project_path + if self.project_path[-1] == '/': + self.project_path = self.project_path[:-1] + if not os.path.isdir(GIT_TMP_PATH): + os.makedirs(GIT_TMP_PATH) + + self._tmp_path: Optional[str] = None + self._askpass_path: Optional[str] = None + + self.user_config = user_config + _conf = self.get_user_config_by_project_path() + if _conf and not self.user_config: + self.user_config = _conf + + self._init_tmp_path = False + + @property + def tmp_path(self) -> Optional[str]: + if self._tmp_path is not None: + return self._tmp_path + if not os.path.isdir(self.project_path): + return None + ino = os.stat(self.project_path).st_ino + self._tmp_path = "{}/{}".format(GIT_TMP_PATH, str(ino)) + self._askpass_path = "{}/help_{}.sh".format(GIT_TMP_PATH, str(ino)) + return self._tmp_path + + def get_user_config_by_project_path(self) -> Optional[dict]: + if os.path.isfile(self.project_path + "/.git/config"): + git_config = read_file(self.project_path + "/.git/config") + if isinstance(git_config, str): + return self._read_user_conf_by_config(git_config) + return None + + @property + def askpass_path(self) -> Optional[str]: + if self._askpass_path is not None: + return self._askpass_path + if not os.path.isdir(self.project_path): + return None + ino = os.stat(self.project_path).st_ino + self._tmp_path = "{}/{}".format(GIT_TMP_PATH, str(ino)) + self._askpass_path = "{}/help_{}.sh".format(GIT_TMP_PATH, str(ino)) + return self._askpass_path + + def _setup_tmp_path(self) -> Optional[str]: + if not os.path.isdir(self.project_path): + return "目标目录:【{}】不存在,无法进行git操作".format(self.project_path) + + if not os.path.exists(self.tmp_path): + os.makedirs(self.tmp_path) + self._init_tmp_path = True + else: + if not self._init_tmp_path: + shutil.rmtree(self.tmp_path) + os.makedirs(self.tmp_path) + write_file(self.askpass_path, "") + ExecShell("chmod +x " + self.askpass_path) + + if os.path.isdir(self.tmp_path + "/.git"): + return None + sh_str = """cd {tmp_path} +{git_bin} init . +{git_bin} remote add origin {git_url} +""".format(tmp_path=self.tmp_path, git_bin=self.git_bin(), git_url=self.git_url) + + ExecShell(sh_str) + if not os.path.isfile(self.tmp_path + "/.git/config"): + return "git 初始化失败" + + git_conf = read_file(self.tmp_path + "/.git/config") + if not (isinstance(git_conf, str) and git_conf.find("origin") != -1 and git_conf.find(self.git_url) != -1): + return "git 设置远程路由失败" + + if self.git_url.find("ssh://") != -1: # ssh的情况下不做处理 + return + + if isinstance(self.user_config, dict): + sh_str_list = ["cd {}".format(self.tmp_path)] + config_sh = self.git_bin() + " config user.{} {}" + for k, v in self.user_config.items(): + if isinstance(k, str) and isinstance(v, str) and k.strip() and v.strip(): + sh_str_list.append(config_sh.format(k, v)) + ExecShell("\n".join(sh_str_list)) + askpass_str = """#!/bin/sh +case "$1" in + Username*) exec echo "{}" ;; + Password*) exec echo "{}" ;; +esac +""".format(self.user_config.get('name', "--"), self.user_config.get('password', "--")) + write_file(self.askpass_path, askpass_str) + + def remote_branch(self) -> Union[str, List[str]]: + error = self._setup_tmp_path() + if error: + return error + out, err = ExecShell("cd {} && export GIT_ASKPASS='{}' && git ls-remote origin".format( + self.tmp_path, self.askpass_path)) + rep_branch = re.compile(r"refs/heads/(?P[^\n]*)\n") + branch_list = [] + for tmp_res in rep_branch.finditer(out): + branch_list.append(tmp_res.group("b")) + + if not branch_list: + return err + return branch_list + + @staticmethod + def _read_user_conf_by_config(git_config_data: str) -> Optional[dict]: + rep_user = re.compile(r"\[user][^\n]*\n(?P(\s*\w+\s*=\s*[^\n]*\n)*(\s*\w+\s*=\s*[^\n]*)?)(\s*\[)?") + res = rep_user.search(git_config_data) + if not res: + return None + res_data = dict() + k_v_str = res.group("target") + for line in k_v_str.split("\n"): + if not line.strip(): + continue + k, v = line.split("=", 1) + res_data[k.strip()] = v.strip() + return res_data + + @classmethod + def global_user_conf(cls) -> dict: + res_dict = { + "name": None, + "password": None, + "email": None, + } + global_file = "/root/.gitconfig" + if not os.path.isfile(global_file): + return res_dict + data = read_file(global_file) + if not isinstance(data, str): + return res_dict + res_data = cls._read_user_conf_by_config(data) + res_dict.update(res_data) + return res_dict + + @classmethod + def set_global_user_conf(cls, data) -> None: + sh_str = cls.git_bin() + " config --global user.{} {}" + for k, v in data.items(): + if isinstance(k, str) and isinstance(v, str) and k.strip() and v.strip(): + ExecShell(sh_str.format(k, v)) + + @classmethod + def ssh_pub_key(cls): + key_files = ('id_ed25519', 'id_rsa', 'id_ecdsa', 'id_rsa_bt') + for key_file in key_files: + key_file = "/root/.ssh/{}".format(key_file) + pub_file = "/root/.ssh/{}.pub".format(key_file) + if os.path.isfile(pub_file) and os.path.isfile(key_file): + data = read_file(pub_file) + if isinstance(data, str): + return data + return cls._create_ssh_key() + + @staticmethod + def _create_ssh_key() -> str: + key_type = "ed25519" + ExecShell("ssh-keygen -t {s_type} -P '' -f /root/.ssh/id_{s_type} |echo y".format(s_type=key_type)) + authorized_keys = '/root/.ssh/authorized_keys' + pub_file = "/root/.ssh/id_{s_type}.pub".format(s_type=key_type) + ExecShell('cat %s >> %s && chmod 600 %s' % (pub_file, authorized_keys, authorized_keys)) + key_type_file = '/www/server/panel/data/ssh_key_type.pl' + write_file(key_type_file, key_type) + return read_file(pub_file) + + @staticmethod + def git_bin() -> str: + if not installed(): + if not install_git(): + raise ValueError("没有git工具,且安装失败,无法使用此功能") + default = "/usr/bin/git" + git_path = shutil.which("git") + if git_path is None: + return default + return git_path + + def pull(self, branch, set_own: Optional[str] = None) -> Optional[str]: + if self.git_url.startswith("https://") or self.git_url.startswith("http://"): + res = parse_url(self.git_url) + if isinstance(res, Url) and not res.auth: + if self.user_config and "name" in self.user_config and "password" in self.user_config: + res.auth = "{}:{}".format(self.user_config["name"], self.user_config["password"]) + self.git_url = res.url + git_name = self.git_name() + if git_name is None: + git_name = 'None' + if os.path.isdir(self.tmp_path + "/" + git_name): + shutil.rmtree(self.tmp_path + "/" + git_name) + + log_file = "/tmp/git_{}_log.log".format(self.get_id) + + shell_command_str = "cd {0} && {1} clone --progress -b {2} {3} &>> {4}".format( + self.tmp_path, self.git_bin(), branch, self.git_url, log_file + ) + + ExecShell(shell_command_str) + if not os.path.isdir(self.tmp_path + "/" + git_name): + return "拉取错误" + + ExecShell(r"\cp -rf {}/{}/* {}/".format(self.tmp_path, git_name, self.project_path)) + if isinstance(set_own, str): + set_ownership(self.project_path, set_own) + + if os.path.isdir(self.tmp_path + "/" + git_name): + shutil.rmtree(self.tmp_path + "/" + git_name) + + def git_name(self) -> Optional[str]: + if isinstance(self.git_url, str): + name = self.git_url.rsplit("/", 1)[1] + if name.endswith(".git"): + name = name[:-4] + return name + return None + + @classmethod + def new_id(cls) -> str: + from uuid import uuid4 + return uuid4().hex[::2] + + +class RealGitMager: + _git_config_file = "/www/server/panel/data/site_git_config.json" + + def __init__(self): + self._config: Optional[Dict[str, List[Dict[str, Union[int, str, dict]]]]] = None + # c = { + # "site_name": [{ + # "id": "", + # "site_name": "aaaa", + # "url": "http://git.bt.cn/cjxin/panel-plugin.git", # ssh://git@git.bt.cn/cjxin/panel-plugin.git + # "path_ino": 4564524, + # "git_path": "/www/wwwroot/site", + # "config": { + # "name": "", + # "password": "", + # "email": "", + # }, + # } + # ] + # } + + @property + def configure(self) -> Dict[str, List[Dict[str, Union[int, str, dict]]]]: + if self._config is None: + try: + res = read_file(self._git_config_file) + if res is None: + data = {} + else: + data = json.loads(res) + except (json.JSONDecoder, TypeError, ValueError): + data = {} + + self._config = data + return self._config + + def save_configure(self): + if self._config: + write_file(self._git_config_file, json.dumps(self._config)) + + def add_git(self, git_url: str, site_name: str, git_path: str, user_config: Optional[dict]) -> Union[str, list]: + url = parse_url(git_url) + if not (isinstance(url, Url) and url.scheme and url.host and url.path): + return "url格式错误" + + if user_config and not (isinstance(user_config, dict) and "name" in user_config and "password" in user_config): + return "用户信息输入错误" + + if not os.path.exists(git_path): + return "git目标目录不存在" + else: + path_ino = os.stat(git_path).st_ino + + if site_name not in self.configure: + self.configure[site_name] = [] + + for c in self.configure[site_name]: + if c["path_ino"] == path_ino or git_path == c["git_path"]: + return "该路径已存在,请不要重复添加" + + try: + GitTool.git_bin() + except ValueError as e: + return str(e) + + git_id = GitTool.new_id() + git = GitTool(project_path=git_path, git_url=git_url, user_config=user_config, git_id=git_id) + res = git.remote_branch() + if isinstance(res, str): + return res + + self.configure[site_name].append( + { + "id": git_id, + "site_name": site_name, + "url": git_url, + "path_ino": path_ino, + "git_path": git_path, + "remote_branch": res, + "remote_branch_time": int(time.time()), + "config": user_config, + } + ) + self.save_configure() + return res + + def modify_git( + self, + git_id: str, + site_name: str, + git_url: Optional[str], + git_path: Optional[str], + user_config: Optional[dict] + ) -> Optional[str]: + target = None + for i in self.configure.get(site_name, []): + if i["id"] == git_id: + target = i + break + + if target is None: + return '指定的git配置不存在' + if git_url: + url = parse_url(git_url) + if not (isinstance(url, Url) and url.scheme and url.host and url.path): + return "url格式错误" + target["url"] = git_url + + if git_path: + if not os.path.exists(git_path): + return "git目标目录不存在" + else: + path_ino = os.stat(git_path).st_ino + target["path_ino"] = path_ino + target["git_path"] = git_path + + if user_config: + if not (isinstance(user_config, dict) and "name" in user_config and "password" in user_config): + return "用户信息输入错误" + target["config"] = user_config + + git = GitTool(project_path=target['git_path'], + git_url=target['url'], + user_config=target['config'], + git_id=target['id']) + res = git.remote_branch() + if isinstance(res, str): + return res + + self.save_configure() + return None + + def remove_git(self, git_id: str, site_name: str) -> Optional[str]: + target = None + for i in self.configure.get(site_name, []): + if i["id"] == git_id: + target = i + break + + if target is None: + return '指定的git配置不存在' + + self.configure[site_name].remove(target) + self.save_configure() + + def site_git_configure(self, site_name, refresh: bool = False) -> List[dict]: + if site_name not in self.configure: + return [] + res_list = [] + for i in self.configure[site_name]: + if time.time() - i.get("remote_branch_time", 0) > 60 * 60 or refresh: + g = GitTool(project_path=i['git_path'], git_url=i['url'], user_config=i['config'], git_id=i['id']) + res = g.remote_branch() + if isinstance(res, str): + i.update(remote_branch_error=res, remote_branch=[], remote_branch_time=int(time.time())) + else: + i.update(remote_branch=res, remote_branch_time=int(time.time())) + res_list.append(i) + + return res_list + + @staticmethod + def set_global_user(name: Optional[str], password: Optional[str], email: Optional[str] = None) -> None: + data = {} + if name: + data['name'] = name + if password: + data['password'] = password + if email: + data['email'] = email + GitTool.set_global_user_conf(data) + + def git_pull(self, git_id: str, site_name: str, branch: str) -> Optional[str]: + target = None + for i in self.configure.get(site_name, []): + if i["id"] == git_id: + target = i + break + + if target is None: + return '指定的git配置不存在' + + g = GitTool( + project_path=target['git_path'], + git_url=target['url'], + user_config=target['config'], + git_id=target["id"]) + return g.pull(branch) + + +class GitMager: + # 添加git信息 + @staticmethod + def add_git(get): + user_config = None + try: + git_url = get.url.strip() + site_name = get.site_name.strip() + git_path = get.git_path.strip() + if hasattr(get, "config") and get.config.strip(): + user_config = json.loads(get.config.strip()) + except (json.JSONDecoder, AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + res = RealGitMager().add_git(git_url, site_name, git_path, user_config) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, data=res) + + # 修改git信息 + @staticmethod + def modify_git(get): + git_url = None + git_path = None + user_config = None + try: + git_id = get.git_id.strip() + site_name = get.site_name.strip() + if "url" in get: + git_url = get.url.strip() + if 'git_path' in get: + git_path = get.git_path.strip() + if hasattr(get, "user_config") and get.user_config.strip(): + user_config = json.loads(get.user_config.strip()) + except (json.JSONDecoder, AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + res = RealGitMager().modify_git(git_id, site_name, git_url, git_path, user_config) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, data=res) + + # 移除git信息 + @staticmethod + def remove_git(get): + try: + git_id = get.git_id.strip() + site_name = get.site_name.strip() + except (json.JSONDecoder, AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + res = RealGitMager().remove_git(git_id, site_name) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, data=res) + + @staticmethod + def site_git_configure(get): + if not version_1_5_3(): + return json_response(status=False, msg="git 版本低于1.5.3无法使用") + refresh = '' + try: + site_name = get.site_name.strip() + if "refresh" in get: + refresh = get.refresh.strip() + except (AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + if refresh in ("true", "1"): + refresh = True + else: + refresh = False + res = RealGitMager().site_git_configure(site_name, refresh=refresh) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, data=res) + + @staticmethod + def set_global_user(get): + name = password = email = None + try: + if "name" in get: + name = get.name.strip() + if "password" in get: + password = get.password.strip() + if "email" in get: + email = get.email.strip() + except (AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + + RealGitMager().set_global_user(name, password, email) + return json_response(status=True, msg="设置成功") + + @staticmethod + def git_pull(get): + try: + site_name = get.site_name.strip() + git_id = get.git_id.strip() + branch = get.branch.strip() + except (AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + res = RealGitMager().git_pull(git_id, site_name, branch) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, data=res) + + @staticmethod + def git_global_user_conf(get=None): + return GitTool.global_user_conf() + + @staticmethod + def git_ssh_pub_key(get=None): + return GitTool.ssh_pub_key() diff --git a/mod/base/git_tool/util.py b/mod/base/git_tool/util.py new file mode 100644 index 00000000..f4cd2474 --- /dev/null +++ b/mod/base/git_tool/util.py @@ -0,0 +1,56 @@ +import os +import sys +from typing import Optional, Tuple, Callable + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + +import public + + +def write_file(filename: str, s_body: str, mode='w+') -> bool: + """ + 写入文件内容 + @filename 文件名 + @s_body 欲写入的内容 + return bool 若文件不存在则尝试自动创建 + """ + try: + fp = open(filename, mode=mode) + fp.write(s_body) + fp.close() + return True + except: + try: + fp = open(filename, mode=mode, encoding="utf-8") + fp.write(s_body) + fp.close() + return True + except: + return False + + +def read_file(filename, mode='r') -> Optional[str]: + """ + 读取文件内容 + @filename 文件名 + return string(bin) 若文件不存在,则返回None + """ + import os + if not os.path.exists(filename): + return None + fp = None + try: + fp = open(filename, mode=mode) + f_body = fp.read() + except: + return None + finally: + if fp and not fp.closed: + fp.close() + return f_body + + +ExecShell: Callable = public.ExecShell +set_ownership: Callable = public.set_ownership diff --git a/mod/base/msg/__init__.py b/mod/base/msg/__init__.py new file mode 100644 index 00000000..8342523c --- /dev/null +++ b/mod/base/msg/__init__.py @@ -0,0 +1,145 @@ +import json +import os.path + +from .weixin_msg import WeiXinMsg +from .mail_msg import MailMsg +from .web_hook_msg import WebHookMsg +from .feishu_msg import FeiShuMsg +from .dingding_msg import DingDingMsg +from .sms_msg import SMSMsg +from .wx_account_msg import WeChatAccountMsg +from .manager import SenderManager +from .util import read_file + +from mod.base.push_mod import SenderConfig, PUSH_DATA_PATH + + +# 把旧地告警系统的信息通道更新 +def update_mod_push_msg(): + + if os.path.exists(PUSH_DATA_PATH + "/update_sender.pl"): + return + with open(PUSH_DATA_PATH + "/update_sender.pl", "w") as f: + f.write("") + + WeChatAccountMsg.refresh_config(force=True) + sms_status = False + sc = SenderConfig() + for conf in sc.config: + if conf["sender_type"] == "sms": + sms_status = True + break + + if not sms_status: + sc.config.append({ + "id": sc.nwe_id(), + "used": True, + "sender_type": "sms", + "data": {}, + "original": True # 标记这个通道是该类型 旧有的通道, 同时也是默认通道 + }) + + panel_data_path = "/www/server/panel/data" + + # weixin + if os.path.exists(panel_data_path + "/weixin.json"): + try: + weixin_data = json.loads(read_file(panel_data_path + "/weixin.json")) + except: + weixin_data = None + + if isinstance(weixin_data, dict) and "weixin_url" in weixin_data: + sc.config.append({ + "id": sc.nwe_id(), + "used": True, + "sender_type": "weixin", + "data": { + "url": weixin_data["weixin_url"], + "title": "企业微信" if "title" not in weixin_data else weixin_data["title"] + }, + "original": True + }) + + # mail + stmp_file = panel_data_path + "/stmp_mail.json" + mail_list_file = panel_data_path + "/mail_list.json" + if os.path.exists(stmp_file) and os.path.exists(mail_list_file): + stmp_data = None + try: + stmp_data = json.loads(read_file(stmp_file)) + mail_list_data = json.loads(read_file(mail_list_file)) + except: + mail_list_data = None + + if isinstance(stmp_data, dict): + if 'qq_mail' in stmp_data or 'qq_stmp_pwd' in stmp_data or 'hosts' in stmp_data: + sc.config.append({ + "id": sc.nwe_id(), + "used": True, + "sender_type": "mail", + "data": { + "send": stmp_data, + "title": "邮箱", + "receive": [] if not mail_list_data else mail_list_data, + }, + "original": True + }) + + # webhook + webhook_file = panel_data_path + "/hooks_msg.json" + if os.path.exists(stmp_file) and os.path.exists(mail_list_file): + try: + webhook_data = json.loads(read_file(webhook_file)) + except: + webhook_data = None + + if isinstance(webhook_data, list): + for i in webhook_data: + i["title"] = i["name"] + sc.config.append({ + "id": sc.nwe_id(), + "used": True, + "sender_type": "webhook", + "data": i, + }) + + # feishu + if os.path.exists(panel_data_path + "/feishu.json"): + try: + feishu_data = json.loads(read_file(panel_data_path + "/feishu.json")) + except: + feishu_data = None + + if isinstance(feishu_data, dict) and "feishu_url" in feishu_data: + sc.config.append({ + "id": sc.nwe_id(), + "used": True, + "sender_type": "feishu", + "data": { + "url": feishu_data["feishu_url"], + "title": "飞书" if "title" not in feishu_data else feishu_data["title"] + }, + "original": True + }) + + # dingding + if os.path.exists(panel_data_path + "/dingding.json"): + try: + dingding_data = json.loads(read_file(panel_data_path + "/dingding.json")) + except: + dingding_data = None + + if isinstance(dingding_data, dict) and "dingding_url" in dingding_data: + sc.config.append({ + "id": sc.nwe_id(), + "used": True, + "sender_type": "dingding", + "data": { + "url": dingding_data["dingding_url"], + "title": "钉钉" if "title" not in dingding_data else dingding_data["title"] + }, + "original": True + }) + + sc.save_config() + read_file(PUSH_DATA_PATH + "/update_sender.pl", "") diff --git a/mod/base/msg/dingding_msg.py b/mod/base/msg/dingding_msg.py new file mode 100644 index 00000000..c1a681c2 --- /dev/null +++ b/mod/base/msg/dingding_msg.py @@ -0,0 +1,155 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: baozi < +# | 消息通道邮箱模块(新) +# +------------------------------------------------------------------- + +import re +import json +import requests +import traceback +import socket + +import requests.packages.urllib3.util.connection as urllib3_cn +from requests.packages import urllib3 +from typing import Optional, Union + +from .util import write_push_log, get_test_msg + +# 关闭警告 +urllib3.disable_warnings() + + +class DingDingMsg: + def __init__(self, dingding_data): + self.id = dingding_data["id"] + self.config = dingding_data["data"] + + def send_msg(self, msg: str, title) -> Optional[str]: + """ + 钉钉发送信息 + @msg 消息正文 + """ + if not self.config: + return '未正确配置钉钉信息' + + # user没有时默认为空 + if "user" not in self.config: + self.config['user'] = [] + + if "isAtAll" not in self.config: + self.config['isAtAll'] = [] + + if not isinstance(self.config['url'], str): + return '钉钉配置错误,请重新配置钉钉机器人' + + at_info = '' + for user in self.config['user']: + if re.match(r"^[0-9]{11}$", str(user)): + at_info += '@' + user + ' ' + + if at_info: + msg = msg + '\n\n>' + at_info + + + + headers = {'Content-Type': 'application/json'} + data = { + "msgtype": "markdown", + "markdown": { + "title": "服务器通知", + "text": msg + }, + "at": { + "atMobiles": self.config['user'], + "isAtAll": self.config['isAtAll'] + } + } + status = False + error = None + try: + def allowed_gai_family(): + family = socket.AF_INET + return family + + allowed_gai_family_lib = urllib3_cn.allowed_gai_family + urllib3_cn.allowed_gai_family = allowed_gai_family + + response = requests.post( + url=self.config["url"], + data=json.dumps(data), + verify=False, + headers=headers, + timeout=10 + ) + + urllib3_cn.allowed_gai_family = allowed_gai_family_lib + + if response.json()["errcode"] == 0: + status = True + except: + error = traceback.format_exc() + status = False + + write_push_log("钉钉", status, title) + return error + + @classmethod + def check_args(cls, args: dict) -> Union[dict, str]: + if "url" not in args or "title" not in args: + return "信息不完整" + + title = args["title"] + if len(title) > 15: + return '备注名称不能超过15个字符' + + if "user" in args and isinstance(args["user"], list): + user = args["user"] + else: + user = [] + + if "atall" in args and isinstance(args["atall"], bool): + atall = args["atall"] + else: + atall = True + + data = { + "url": args["url"], + "user": user, + "title": title, + "isAtAll": atall, + } + + test_obj = cls({"data": data, "id": None}) + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + + test_task = get_test_msg("面板消息通道配置提醒") + + res = test_obj.send_msg( + test_task.to_dingding_msg(test_msg, test_task.the_push_public_data()), + "面板消息通道配置提醒" + ) + if res is None: + return data + + return res + + def test_send_msg(self) -> Optional[str]: + + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + test_task = get_test_msg("面板消息通道配置提醒") + res = self.send_msg( + test_task.to_dingding_msg(test_msg, test_task.the_push_public_data()), + "面板消息通道配置提醒" + ) + if res is None: + return None + return res diff --git a/mod/base/msg/feishu_msg.py b/mod/base/msg/feishu_msg.py new file mode 100644 index 00000000..0af9b3e4 --- /dev/null +++ b/mod/base/msg/feishu_msg.py @@ -0,0 +1,139 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: lx +# | 消息通道飞书通知模块 +# +------------------------------------------------------------------- + +import re +import json +import requests +import traceback +import socket + +import requests.packages.urllib3.util.connection as urllib3_cn +from requests.packages import urllib3 +from typing import Optional, Union + +from .util import write_push_log, get_test_msg + +# 关闭警告 +urllib3.disable_warnings() + + +class FeiShuMsg: + + def __init__(self, feishu_data): + self.id = feishu_data["id"] + self.config = feishu_data["data"] + + @classmethod + def check_args(cls, args: dict) -> Union[dict, str]: + if "url" not in args or "title" not in args: + return "信息不完整" + + title = args["title"] + if len(title) > 15: + return '备注名称不能超过15个字符' + + if "user" in args and isinstance(args["user"], list): + user = args["user"] + else: + user = [] + + if "atall" in args and isinstance(args["atall"], bool): + atall = args["atall"] + else: + atall = True + + data = { + "url": args["url"], + "user": user, + "title": title, + "isAtAll": atall, + } + + test_obj = cls({"data": data, "id": None}) + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + + test_task = get_test_msg("消息通道配置提醒") + + res = test_obj.send_msg( + test_task.to_feishu_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒" + ) + if res is None: + return data + + return res + + def send_msg(self, msg: str, title: str) -> Optional[str]: + """ + 飞书发送信息 + @msg 消息正文 + """ + if not self.config: + return '未正确配置飞书信息。' + + reg = '(.+)' + tmp = re.search(reg, msg) + if tmp: + tmp = tmp.groups()[0] + msg = re.sub(reg, tmp, msg) + + if "isAtAll" not in self.config: + self.config["isAtAll"] = True + + if self.config["isAtAll"]: + msg += "所有人" + + headers = {'Content-Type': 'application/json'} + data = { + "msg_type": "text", + "content": { + "text": msg + } + } + status = False + error = None + try: + def allowed_gai_family(): + family = socket.AF_INET + return family + allowed_gai_family_lib = urllib3_cn.allowed_gai_family + urllib3_cn.allowed_gai_family = allowed_gai_family + rdata = requests.post( + url=self.config['url'], + data=json.dumps(data), + verify=False, + headers=headers, + timeout=10 + ).json() + urllib3_cn.allowed_gai_family = allowed_gai_family_lib + + if "StatusCode" in rdata and rdata["StatusCode"] == 0: + status = True + except: + error = traceback.format_exc() + + write_push_log("飞书", status, title) + + return error + + def test_send_msg(self) -> Optional[str]: + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + test_task = get_test_msg("消息通道配置提醒") + res = self.send_msg( + test_task.to_feishu_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒" + ) + if res is None: + return None + return res diff --git a/mod/base/msg/mail_msg.py b/mod/base/msg/mail_msg.py new file mode 100644 index 00000000..57b2b252 --- /dev/null +++ b/mod/base/msg/mail_msg.py @@ -0,0 +1,135 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: 沐落 +# | Author: lx +# | 消息通道邮箱模块 +# +------------------------------------------------------------------- + +import smtplib +import traceback +from email.mime.text import MIMEText +from email.utils import formataddr +from typing import Tuple, Union, Optional + +from mod.base.msg.util import write_push_log, write_mail_push_log, get_test_msg + + +class MailMsg: + + def __init__(self, mail_data): + self.id = mail_data["id"] + self.config = mail_data["data"] + + @classmethod + def check_args(cls, args: dict) -> Tuple[bool, Union[dict, str]]: + if "send" not in args or "receive" not in args or len(args["receive"]) < 1: + return False, "信息不完整,必须有发送方和至少一个接收方" + + if "title" not in args: + return False, "没有必要的备注信息" + + title = args["title"] + if len(title) > 15: + return False, '备注名称不能超过15个字符' + + send_data = args["send"] + send = {} + for i in ("qq_mail", "qq_stmp_pwd", "hosts", "port"): + if i not in send_data: + return False, "发送方配置信息不完整" + send[i] = send_data[i].strip() + + receive_data = args["receive"] + if isinstance(receive_data, str): + receive_list = [i.strip() for i in receive_data.split("\n") if i.strip()] + else: + receive_list = [i.strip() for i in receive_data if i.strip()] + + data = { + "send": send, + "title": title, + "receive": receive_list, + } + + test_obj = cls({"data": data, "id": None}) + test_msg = { + "msg_list": ['>配置状态:成功
                                    '] + } + + test_task = get_test_msg("消息通道配置提醒") + + res = test_obj.send_msg( + test_task.to_mail_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒" + ) + if res is None or res.find("部分接收者时失败") != -1: + return True, data + + return False, res + + def send_msg(self, msg: str, title: str): + """ + 邮箱发送 + @msg 消息正文 + @title 消息标题 + """ + if not self.config: + return '未正确配置邮箱信息。' + + if 'port' not in self.config['send']: + self.config['send']['port'] = 465 + + receive_list = self.config['receive'] + + error_list, success_list = [], [] + error_msg_dict = {} + for email in receive_list: + if not email.strip(): + continue + try: + data = MIMEText(msg, 'html', 'utf-8') + data['From'] = formataddr((self.config['send']['qq_mail'], self.config['send']['qq_mail'])) + data['To'] = formataddr((self.config['send']['qq_mail'], email.strip())) + data['Subject'] = title + if int(self.config['send']['port']) == 465: + server = smtplib.SMTP_SSL(str(self.config['send']['hosts']), int(self.config['send']['port'])) + else: + server = smtplib.SMTP(str(self.config['send']['hosts']), int(self.config['send']['port'])) + + server.login(self.config['send']['qq_mail'], self.config['send']['qq_stmp_pwd']) + server.sendmail(self.config['send']['qq_mail'], [email.strip(), ], data.as_string()) + server.quit() + success_list.append(email) + except: + error_list.append(email) + error_msg_dict[email] = traceback.format_exc() + + if not error_list and not success_list: # 没有接收者 + return "未配置接收邮箱" + if not error_list: + write_push_log("邮箱", True, title, success_list) # 没有失败 + return None + if not success_list: + write_push_log("邮箱", False, title, error_list) # 全都失败 + return "发送信息失败, 发送失败的接收人:{}".format(error_list) + write_mail_push_log(title, error_list, success_list) + + return "发送邮件到部分接收者时失败,包含:{}".format(error_list) + + def test_send_msg(self) -> Optional[str]: + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + test_task = get_test_msg("消息通道配置提醒") + res = self.send_msg( + test_task.to_mail_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒" + ) + if res is None: + return None + return res + diff --git a/mod/base/msg/manager.py b/mod/base/msg/manager.py new file mode 100644 index 00000000..a246d608 --- /dev/null +++ b/mod/base/msg/manager.py @@ -0,0 +1,282 @@ +import time +import traceback + +from mod.base.push_mod import SenderConfig +from .weixin_msg import WeiXinMsg +from .mail_msg import MailMsg +from .web_hook_msg import WebHookMsg +from .feishu_msg import FeiShuMsg +from .dingding_msg import DingDingMsg +from .sms_msg import SMSMsg +from .wx_account_msg import WeChatAccountMsg +import json +from mod.base import json_response +from .util import write_file, read_file +import sys,os +sys.path.insert(0, "/www/server/panel/class/") +import public + +# 短信会自动添加到 sender 库中的第一个 且通过官方接口更新 +# 微信公众号信息通过官网接口更新, 不写入数据库,需要时由文件中读取并序列化 +# 其他告警通道本质都类似于web hook 在确认完数据信息无误后,都可以自行添加或启用 +class SenderManager: + def __init__(self): + self.custom_parameter_filename = "/www/server/panel/data/mod_push_data/custom_parameter.pl" + + def set_sender_conf(self, get): + sender_id = None + try: + if hasattr(get, "sender_id"): + sender_id = get.sender_id.strip() + if not sender_id: + sender_id = None + sender_type = get.sender_type.strip() + args = json.loads(get.sender_data.strip()) + except (json.JSONDecoder, AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + sender_config = SenderConfig() + if sender_id is not None: + tmp = sender_config.get_by_id(sender_id) + if tmp is None: + sender_id = None + + if sender_type == "weixin": + data = WeiXinMsg.check_args(args) + if isinstance(data, str): + return json_response(status=False, data=data, msg="测试发送失败") + + elif sender_type == "mail": + _, data = MailMsg.check_args(args) + if isinstance(data, str): + return json_response(status=False, data=data, msg="测试发送失败") + + elif sender_type == "webhook": + custom_parameter = args.get("custom_parameter", {}) + if custom_parameter: + try: + public.writeFile(self.custom_parameter_filename, json.dumps(custom_parameter)) + except: + pass + + # 检查参数 + data = WebHookMsg.check_args(args) + if isinstance(data, str): + return json_response(status=False, data=data, msg="测试发送失败") + + # 从文件读取并删除文件 + try: + if os.path.exists(self.custom_parameter_filename): + custom_parameter = json.loads(public.readFile(self.custom_parameter_filename)) + data['custom_parameter'] = custom_parameter + os.remove(self.custom_parameter_filename) + except: + pass + + elif sender_type == "feishu": + data = FeiShuMsg.check_args(args) + if isinstance(data, str): + return json_response(status=False, data=data, msg="测试发送失败") + + elif sender_type == "dingding": + data = DingDingMsg.check_args(args) + if isinstance(data, str): + return json_response(status=False, data=data, msg="测试发送失败") + else: + return json_response(status=False, msg="当前接口不适应的类型") + # Check if the sender configuration already exists + existing_sender = any( + conf for conf in sender_config.config + if conf['sender_type'] == sender_type and 'title' in conf['data'] and conf['data']['title'] == data['title'] and conf['id'] != sender_id + ) + if existing_sender: + return json_response(status=False, msg="同样的发送配置已存在,无法重复添加") + now_sender_id = None + if not sender_id: + now_sender_id = sender_config.nwe_id() + sender_config.config.append( + { + "id": now_sender_id, + "sender_type": sender_type, + "data": data, + "used": True, + }) + + else: + now_sender_id = sender_id + tmp = sender_config.get_by_id(sender_id) + tmp["data"].update(data) + + type_senders = [conf for conf in sender_config.config if conf['sender_type'] == sender_type] + if len(type_senders) == 1: + for conf in sender_config.config: + conf["original"] = (conf['id'] == now_sender_id) + + sender_config.save_config() + if sender_type == "webhook": + self.set_default_for_compatible(sender_config.get_by_id(now_sender_id)) + + return json_response(status=True, msg="保存成功") + + @staticmethod + def change_sendr_used(get): + try: + sender_id = get.sender_id.strip() + except (AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + + sender_config = SenderConfig() + tmp = sender_config.get_by_id(sender_id) + if tmp is None: + return json_response(status=False, msg="未找到对应发送者") + tmp["used"] = not tmp["used"] + + sender_config.save_config() + + return json_response(status=True, msg="保存成功") + + @staticmethod + def remove_sender(get): + try: + sender_id = get.sender_id.strip() + except (AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + + sender_config = SenderConfig() + tmp = sender_config.get_by_id(sender_id) + if tmp is None: + return json_response(status=False, msg="未找到对应发送者") + sender_config.config.remove(tmp) + sender_config.save_config() + + return json_response(status=True, msg="删除成功") + + @staticmethod + def get_sender_list(get): + # 微信, 飞书, 钉钉, web-hook, 邮箱 + refresh = False + try: + if hasattr(get, 'refresh'): + refresh = get.refresh.strip() + if refresh in ("1", "true"): + refresh = True + except (AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + + res = [] + WeChatAccountMsg.refresh_config(force=refresh) + simple = ("weixin", "mail", "webhook", "feishu", "dingding") + + for conf in SenderConfig().config: + if conf["sender_type"] in simple or conf["sender_type"] == "wx_account": + res.append(conf) + elif conf["sender_type"] == "sms": + conf["data"] = SMSMsg(conf).refresh_config(force=refresh) + res.append(conf) + res.sort(key=lambda x: x["sender_type"]) + return json_response(status=True, data=res) + + @staticmethod + def test_send_msg(get): + try: + sender_id = get.sender_id.strip() + except (json.JSONDecoder, AttributeError, TypeError): + return json_response(status=False, msg="参数错误") + + sender_config = SenderConfig() + tmp = sender_config.get_by_id(sender_id) + if tmp is None: + return json_response(status=False, msg="未找到对应发送者") + + sender_type = tmp["sender_type"] + + if sender_type == "weixin": + sender_obj = WeiXinMsg(tmp) + + elif sender_type == "mail": + sender_obj = MailMsg(tmp) + + elif sender_type == "webhook": + sender_obj = WebHookMsg(tmp) + + elif sender_type == "feishu": + sender_obj = FeiShuMsg(tmp) + + elif sender_type == "dingding": + sender_obj = DingDingMsg(tmp) + + elif sender_type == "wx_account": + sender_obj = WeChatAccountMsg(tmp) + else: + return json_response(status=False, msg="当前接口不适应的类型") + + res = sender_obj.test_send_msg() + if isinstance(res, str): + return json_response(status=False, data=res, msg="测试发送失败") + return json_response(status=True, msg="发送成功") + + @staticmethod + def set_default_for_compatible(sender_data: dict): + if sender_data["sender_type"] in ("sms", "wx_account"): + return + + panel_data = "/www/server/panel/data" + if sender_data["sender_type"] == "weixin": + weixin_file = "{}/weixin.json".format(panel_data) + write_file(weixin_file, json.dumps({ + "state": 1, + "weixin_url": sender_data["data"]["url"], + "title": sender_data["data"]["title"], + "list": { + "default": { + "data": sender_data["data"]["url"], + "title": sender_data["data"]["title"], + "status": 1, + "addtime": int(time.time()) + } + } + })) + + elif sender_data["sender_type"] == "mail": + stmp_mail_file = "{}/stmp_mail.json".format(panel_data) + mail_list_file = "{}/mail_list.json".format(panel_data) + write_file(stmp_mail_file, json.dumps(sender_data["data"]["send"])) + write_file(mail_list_file, json.dumps(sender_data["data"]["receive"])) + + elif sender_data["sender_type"] == "feishu": + feishu_file = "{}/feishu.json".format(panel_data) + write_file(feishu_file, json.dumps({ + "feishu_url": sender_data["data"]["url"], + "title": sender_data["data"]["title"], + "isAtAll": True, + "user": [] + })) + + elif sender_data["sender_type"] == "dingding": + dingding_file = "{}/dingding.json".format(panel_data) + write_file(dingding_file, json.dumps({ + "dingding_url": sender_data["data"]["url"], + "title": sender_data["data"]["title"], + "isAtAll": True, + "user": [] + })) + + elif sender_data["sender_type"] == "webhook": + webhook_file = "{}/hooks_msg.json".format(panel_data) + try: + webhook_data = json.loads(read_file(webhook_file)) + except: + webhook_data =[] + target_idx = -1 + for idx, i in enumerate(webhook_data): + if i["name"] == sender_data["data"]["title"]: + target_idx = idx + break + else: + sender_data["data"]["name"] = sender_data["data"]["title"] + webhook_data.append(sender_data["data"]) + if target_idx != -1: + sender_data["data"]["name"] = sender_data["data"]["title"] + webhook_data[target_idx] = sender_data["data"] + write_file(webhook_file, json.dumps(webhook_data)) + + diff --git a/mod/base/msg/sms_msg.py b/mod/base/msg/sms_msg.py new file mode 100644 index 00000000..e6ec2652 --- /dev/null +++ b/mod/base/msg/sms_msg.py @@ -0,0 +1,121 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: baozi +# | 消息通道 短信模块(新) +# +------------------------------------------------------------------- +import json +import os +import time +import traceback +from typing import Union, Optional +from mod.base.push_mod import SenderConfig +from .util import write_push_log, PANEL_PATH, write_file, read_file, public_http_post + + +class SMSMsg: + API_URL = 'http://www.bt.cn/api/wmsg' + USER_PATH = '{}/data/userInfo.json'.format(PANEL_PATH) + + # 构造方法 + def __init__(self, msm_data: dict): + self.id = msm_data["id"] + self.data = msm_data["data"] + self.user_info = None + try: + self.user_info = json.loads(read_file(self.USER_PATH)) + except: + self.user_info = None + + self._PDATA = { + "access_key": "" if self.user_info is None else self.user_info["access_key"], + "data": {} + } + + def refresh_config(self, force=False): + if "last_refresh_time" not in self.data: + self.data["last_refresh_time"] = 0 + if self.data.get("last_refresh_time") + 60 * 60 * 24 < time.time() or force: # 一天最多更新一次 + result = self._request('get_user_sms') + if not isinstance(result, dict) or ("status" in result and not result["status"]): + return { + "count": 0, + "total": 0 + } + sc = SenderConfig() + tmp = sc.get_by_id(self.id) + if tmp is not None: + result["last_refresh_time"] = time.time() + tmp["data"] = result + sc.save_config() + else: + result = self.data + return result + + def send_msg(self, sm_type: str, sm_args: dict): + """ + @发送短信 + @sm_type 预警类型, ssl_end|宝塔SSL到期提醒 + @sm_args 预警参数 + """ + if not self.user_info: + return "未成功绑定官网账号,无法发送信息,请尝试重新绑定" + tmp = sm_type.split('|') + if "|" in sm_type and len(tmp) >= 2: + s_type = tmp[0] + title = tmp[1] + else: + s_type = sm_type + title = '宝塔告警提醒' + + sm_args = self.canonical_data(sm_args) + self._PDATA['data']['sm_type'] = s_type + self._PDATA['data']['sm_args'] = sm_args + print(s_type) + print(sm_args) + result = self._request('send_msg') + u_key = '{}****{}'.format(self.user_info['username'][:3], self.user_info['username'][-3:]) + print(result) + if isinstance(result, str): + write_push_log("短信", False, title, [u_key]) + return result + + if result['status']: + write_push_log("短信", True, title, [u_key]) + return None + else: + write_push_log("短信", False, title, [u_key]) + return result.get("msg", "发送错误") + + @staticmethod + def canonical_data(args): + """规范数据内容""" + if not isinstance(args, dict): + return args + new_args = {} + for param, value in args.items(): + if type(value) != str: + new_str = str(value) + else: + new_str = value.replace(".", "_").replace("+", "+") + new_args[param] = new_str + return new_args + + def push_data(self, data): + return self.send_msg(data['sm_type'], data['sm_args']) + + # 发送请求 + def _request(self, d_name: str) -> Union[dict, str]: + pdata = { + 'access_key': self._PDATA['access_key'], + 'data': json.dumps(self._PDATA['data']) + } + try: + result = public_http_post(self.API_URL + '/' + d_name, pdata) + result = json.loads(result) + return result + except Exception: + return traceback.format_exc() diff --git a/mod/base/msg/test.json b/mod/base/msg/test.json new file mode 100644 index 00000000..016f6ac2 --- /dev/null +++ b/mod/base/msg/test.json @@ -0,0 +1,105 @@ +[ + { + "id": "f4e98e478b85e876", + "used": true, + "sender_type": "sms", + "data": {} + }, + { + "id": "fb3e9e409b9d7c27", + "sender_type": "mail", + "data": { + "send": { + "qq_mail": "1191604998@qq.com", + "qq_stmp_pwd": "alvonbfcwhlahbcg", + "hosts": "smtp.qq.com", + "port": "465" + }, + "title": "test_mail", + "receive": [ + "1191604998@qq.com", + "225326944@qq.com" + ] + }, + "used": true + }, + { + "id": "79900d4fb37fa83d", + "sender_type": "feishu", + "data": { + "url": "https://open.feishu.cn/open-apis/bot/v2/hook/ba6a3f77-0349-4492-a8ad-0b4c99435bf1", + "user": [], + "title": "test_feishu", + "isAtAll": true + }, + "used": true + }, + { + "id": "8f70de4baa89133e", + "sender_type": "webhook", + "data": { + "title": "webhook", + "url": "http://192.168.69.172:11211", + "query": {}, + "headers": {}, + "body_type": "json", + "custom_parameter": {}, + "method": "POST", + "ssl_verify": null, + "status": true + }, + "used": true + }, + { + "id": "10bcf5439299d9dd", + "used": true, + "sender_type": "wx_account", + "data": { + "id": "jsbRCBBinMmFjYjczNTQyYmUzxNDiWQw", + "uid": 1228262, + "is_subscribe": 1, + "head_img": "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83epBUaqBcCkkxtKwuaOHLy1qjGeDvmf1hZsrkFGNrldyRgSuA3sYB1xlgKv1Z98PUciaxju71PUKchA/132", + "nickname": "沈涛", + "status": 1, + "create_time": "2023-12-27 11:30:15", + "update_time": "2023-12-27 11:30:15", + "remaining": 98, + "title": "沈涛" + } + }, + { + "id": "2c7c094eb23ddaae", + "used": true, + "sender_type": "webhook", + "data": { + "url": "http://192.168.69.159:8888/hook?access_key=IUSEViIMMhQio1WyP0ztCyoa8sIBjaWulihhcJX4rRJ4sW79", + "query": {}, + "headers": {}, + "body_type": "json", + "custom_parameter": {}, + "method": "GET", + "ssl_verify": 1, + "status": true, + "name": "aaa", + "title": "aaa" + } + }, + { + "id": "63c30845916fa722", + "used": true, + "sender_type": "feishu", + "data": { + "url": "https://open.feishu.cn/open-apis/bot/v2/hook/c6906d9f-01c5-4a74-80bd-3ccda33bf4ec", + "title": "amber" + } + }, + { + "id": "6ccf834a95010bed", + "used": true, + "sender_type": "dingding", + "data": { + "url": "https://oapi.dingtalk.com/robot/send?access_token=00732dec605edc1c07f441eb9d470c8bdfa301c4ce89959916fe535d08c09043", + "title": "dd" + } + } +] \ No newline at end of file diff --git a/mod/base/msg/util.py b/mod/base/msg/util.py new file mode 100644 index 00000000..a5e38c9e --- /dev/null +++ b/mod/base/msg/util.py @@ -0,0 +1,139 @@ +import sys +from typing import Optional, List, Tuple +from mod.base.push_mod import BaseTask, WxAccountMsgBase, WxAccountMsg, get_push_public_data + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public + + +PANEL_PATH = "/www/server/panel" +public_http_post = public.httpPost + + +def write_push_log( + module_name: str, + status: bool, + title: str, + user: Optional[List[str]] = None): + """ + 记录 告警推送情况 + @param module_name: 通道方式 + @param status: 是否成功 + @param title: 标题 + @param user: 推送到的用户,可以为空,如:钉钉 不需要 + @return: + """ + if status: + status_str = '成功' + else: + status_str = '失败' + + if not user: + user_str = '[ 默认 ]' + else: + user_str = '[ {} ]'.format(",".join(user)) + + log = '标题:【{}】,通知方式:【{}】,结果:【{}】,收件人:{}'.format(title, module_name, status_str, user_str) + public.WriteLog('告警通知', log) + return True + + +def write_mail_push_log( + title: str, + error_user: List[str], + success_user: List[str], +): + """ + 记录 告警推送情况 + @param title: 标题 + @param error_user: 失败的用户 + @param success_user: 成功的用户 + @return: + """ + e_fmt = '{}' + s_fmt = '{}' + error_user_msg = ",".join([e_fmt.format(i) for i in error_user]) + success_user = ",".join([s_fmt.format(i) for i in success_user]) + log = '标题:【{}】,通知方式:【邮箱】,发送失败的收件人:{},发送成功的收件人:{}'.format( + title, error_user_msg, success_user + ) + public.WriteLog('告警通知', log) + return True + + +def write_file(filename: str, s_body: str, mode='w+') -> bool: + """ + 写入文件内容 + @filename 文件名 + @s_body 欲写入的内容 + return bool 若文件不存在则尝试自动创建 + """ + try: + fp = open(filename, mode=mode) + fp.write(s_body) + fp.close() + return True + except: + try: + fp = open(filename, mode=mode, encoding="utf-8") + fp.write(s_body) + fp.close() + return True + except: + return False + + +def read_file(filename, mode='r') -> Optional[str]: + """ + 读取文件内容 + @filename 文件名 + return string(bin) 若文件不存在,则返回None + """ + import os + if not os.path.exists(filename): + return None + fp = None + try: + fp = open(filename, mode=mode) + f_body = fp.read() + except: + return None + finally: + if fp and not fp.closed: + fp.close() + return f_body + + +class _TestMsgTask(BaseTask): + """ + 用来测试的短息 + """ + + @staticmethod + def the_push_public_data(): + return get_push_public_data() + + def get_keywords(self, task_data: dict) -> str: + pass + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + raise NotImplementedError() + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = self.title + msg.msg = "消息通道配置成功" + return msg + + +def get_test_msg(title: str, task_name="消息通道配置提醒") -> _TestMsgTask: + """ + 用来测试的短息 + """ + t = _TestMsgTask() + + t.title = title + t.template_name = task_name + return t diff --git a/mod/base/msg/web_hook_msg.py b/mod/base/msg/web_hook_msg.py new file mode 100644 index 00000000..9fa174f6 --- /dev/null +++ b/mod/base/msg/web_hook_msg.py @@ -0,0 +1,221 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: baozi +# | 消息通道HOOK模块 +# +------------------------------------------------------------------- + + +import requests +from typing import Optional, Union +from urllib3.util import parse_url + +from .util import write_push_log, get_test_msg +import json + +# config = { +# "name": "default", +# "url": "https://www.bt.cn", +# "query": { +# "aaa": "111" +# }, +# "header": { +# "AAA": "BBBB", +# }, +# "body_type": ["json", "form_data", "null"], +# "custom_parameter": { +# "rrr": "qqqq" +# }, +# "method": ["GET", "POST", "PUT", "PATCH"], +# "ssl_verify": [True, False] +# } +# # +# # 1.自动解析Query参数,拼接并展示给用户 # 可不做 +# # 2.自定义Header头 # 必做 +# # 3.Body中的内容是: type:str="首页磁盘告警", time:int=168955427, data:str="xxxxxx" # ? +# # 4.自定义参数: key=value 添加在Body中 # 可不做 +# # 5.请求类型自定义 # 必做 +# # 以上内容需要让用户可测试--! + + +class WebHookMsg(object): + DEFAULT_HEADERS = { + "User-Agent": "BT-Panel", + } + + def __init__(self, hook_data: dict): + self.id = hook_data["id"] + self.config = hook_data["data"] + + def _replace_and_parse(self, value, real_data): + """替换占位符并递归解析JSON字符串""" + if isinstance(value, str): + value = value.replace("$1", json.dumps(real_data, ensure_ascii=False)) + elif isinstance(value, dict): + for k, v in value.items(): + value[k] = self._replace_and_parse(v, real_data) + return value + + def send_msg(self, msg: str, title:str, push_type:str) -> Optional[str]: + the_url = parse_url(self.config['url']) + + ssl_verify = self.config.get("ssl_verify", None) + if ssl_verify is None: + ssl_verify = the_url.scheme == "https" + + real_data = { + "title": title, + "msg": msg, + "type": push_type, + } + # 处理custom_parameter,将$1替换为real_data内容并递归解析 + custom_data = {} + for k, v in self.config.get("custom_parameter", {}).items(): + custom_data[k] = self._replace_and_parse(v, real_data) + + if custom_data: + real_data = custom_data + + + data = None + json_data = None + headers = self.DEFAULT_HEADERS.copy() + if self.config["body_type"] == "json": + json_data = real_data + elif self.config["body_type"] == "form_data": + data = real_data + + for k, v in self.config.get("headers", {}).items(): + if not isinstance(v, str): + v = str(v) + headers[k] = v + + status = False + error = None + timeout = 10 + if data: + for k, v in data.items(): + if isinstance(v, str): + continue + else: + data[k]=json.dumps(v) + + for i in range(3): + try: + if json_data is not None: + res = requests.request( + method=self.config["method"], + url=str(the_url), + json=json_data, + headers=headers, + timeout=timeout, + verify=ssl_verify, + ) + else: + res = requests.request( + method=self.config["method"], + url=str(the_url), + data=data, + headers=headers, + timeout=timeout, + verify=ssl_verify, + ) + + if res.status_code == 200: + status = True + break + else: + status = False + return res.text + except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): + timeout += 5 + continue + except requests.exceptions.RequestException as e: + error = str(e) + break + + write_push_log("Web Hook", status, title) + return error + + @classmethod + def check_args(cls, args) -> Union[str, dict]: + """配置hook""" + try: + title = args['title'] + url = args["url"] + query = args.get("query", {}) + headers = args.get("headers", {}) + body_type = args.get("body_type", "json") + custom_parameter = args.get("custom_parameter", {}) + method = args.get("method", "POST") + ssl_verify = args.get("ssl_verify", None) # null Ture + except (ValueError, KeyError): + return "参数错误" + + the_url = parse_url(url) + if the_url.scheme is None or the_url.host is None: + return"url解析错误,这可能不是一个合法的url" + + for i in (query, headers, custom_parameter): + if not isinstance(i, dict): + return "参数格式错误" + + if body_type not in ('json', 'form_data', 'null'): + return "body_type必须为json,form_data或者null" + + if method not in ('GET', 'POST', 'PUT', 'PATCH'): + return "发送方式选择错误" + + if ssl_verify not in (True, False, None): + return "是否验证ssl选项错误" + + title = title.strip() + if title == "": + return"名称不能为空" + + data = { + "title": title, + "url": url, + "query": query, + "headers": headers, + "body_type": body_type, + "custom_parameter": custom_parameter, + "method": method, + "ssl_verify": ssl_verify, + "status": True + } + + test_obj = cls({"data": data, "id": None}) + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + + test_task = get_test_msg("消息通道配置提醒") + + res = test_obj.send_msg( + test_task.to_web_hook_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒", + "消息通道配置提醒" + ) + if res is None: + return data + + return res + + def test_send_msg(self) -> Optional[str]: + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + test_task = get_test_msg("消息通道配置提醒") + res = self.send_msg( + test_task.to_web_hook_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒", + "消息通道配置提醒" + ) + if res is None: + return None + return res + diff --git a/mod/base/msg/weixin_msg.py b/mod/base/msg/weixin_msg.py new file mode 100644 index 00000000..6ef27d65 --- /dev/null +++ b/mod/base/msg/weixin_msg.py @@ -0,0 +1,130 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: baozi +# | 消息通道邮箱模块 +# +------------------------------------------------------------------- + +import re +import json +import requests +import traceback +import socket + +import requests.packages.urllib3.util.connection as urllib3_cn +from requests.packages import urllib3 +from typing import Optional, Union + +from .util import write_push_log, get_test_msg + +# 关闭警告 +urllib3.disable_warnings() + + +class WeiXinMsg: + + def __init__(self, weixin_data): + self.id = weixin_data["id"] + self.config = weixin_data["data"] + + @classmethod + def check_args(cls, args: dict) -> Union[dict, str]: + if "url" not in args or "title" not in args: + return "信息不完整" + + title = args["title"] + if len(title) > 15: + return '备注名称不能超过15个字符' + + data = { + "url": args["url"], + "title": title, + } + + test_obj = cls({"data": data, "id": None}) + test_msg = { + "msg_list": ['>配置状态:成功\n'] + } + + test_task = get_test_msg("消息通道配置提醒") + + res = test_obj.send_msg( + test_task.to_weixin_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒" + ) + if res is None: + return data + + return res + + def send_msg(self, msg: str, title: str) -> Optional[str]: + """ + @name 微信发送信息 + @msg string 消息正文(正文内容,必须包含 + 1、服务器名称 + 2、IP地址 + 3、发送时间 + ) + @to_user string 指定发送人 + """ + if not self.config: + return '未正确配置微信信息。' + + reg = '(.+)' + tmp = re.search(reg, msg) + if tmp: + tmp = tmp.groups()[0] + msg = re.sub(reg, tmp, msg) + + data = { + "msgtype": "markdown", + "markdown": { + "content": msg + } + } + headers = {'Content-Type': 'application/json'} + + status = False + error = None + try: + def allowed_gai_family(): + family = socket.AF_INET + return family + allowed_gai_family_lib = urllib3_cn.allowed_gai_family + urllib3_cn.allowed_gai_family = allowed_gai_family + response = requests.post( + url=self.config["url"], + data=json.dumps(data), + verify=False, + headers=headers, + timeout=10 + ) + urllib3_cn.allowed_gai_family = allowed_gai_family_lib + + if response.json()["errcode"] == 0: + status = True + except: + error = traceback.format_exc() + + write_push_log("企业微信", status, title) + return error + + def test_send_msg(self) -> Optional[str]: + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + test_task = get_test_msg("消息通道配置提醒") + res = self.send_msg( + test_task.to_weixin_msg(test_msg, test_task.the_push_public_data()), + "消息通道配置提醒", + ) + if res is None: + return None + return res + + + + diff --git a/mod/base/msg/wx_account_msg.py b/mod/base/msg/wx_account_msg.py new file mode 100644 index 00000000..5a624e15 --- /dev/null +++ b/mod/base/msg/wx_account_msg.py @@ -0,0 +1,556 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2020 宝塔软件(http://www.bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: baozi +# | 消息通道微信公众号模块 +# +------------------------------------------------------------------- + +import os, sys +import time, base64 + +import re +import json +import requests +import traceback +import socket + +import requests.packages.urllib3.util.connection as urllib3_cn +from requests.packages import urllib3 +from typing import Optional, Union, List, Dict, Any + +from .util import write_push_log, get_test_msg, read_file, public_http_post +from mod.base.push_mod import WxAccountMsg, SenderConfig +from mod.base import json_response + +# 关闭警告 +urllib3.disable_warnings() + + +class WeChatAccountMsg: + USER_PATH = '/www/server/panel/data/userInfo.json' + need_refresh_file = '/www/server/panel/data/mod_push_data/refresh_wechat_account.tip' + refresh_time = '/www/server/panel/data/mod_push_data/refresh_wechat_account_time.pl' + + def __init__(self, *config_data): + if len(config_data) == 0: + self.config = None + elif len(config_data) == 1: + self.config = config_data[0]["data"] + else: + self.config = config_data[0]["data"] + self.config["users"] = [i["data"]['id'] for i in config_data] + self.config["users_nickname"] = [i["data"]['nickname'] for i in config_data] + try: + self.user_info = json.loads(read_file(self.USER_PATH)) + except: + self.user_info = None + + @classmethod + def get_user_info(cls) -> Optional[dict]: + try: + return json.loads(read_file(cls.USER_PATH)) + except: + return None + + @classmethod + def last_refresh(cls): + tmp = read_file(cls.refresh_time) + if not tmp: + last_refresh_time = 0 + else: + try: + last_refresh_time = int(tmp) + except: + last_refresh_time = 0 + return last_refresh_time + + @staticmethod + def get_local_ip() -> str: + """获取内网IP""" + import socket + s = None + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + return ip + except: + pass + finally: + if s is not None: + s.close() + return '127.0.0.1' + + def send_msg(self, msg: WxAccountMsg) -> Optional[str]: + if self.user_info is None: + return '未获取到用户信息' + + msg.set_ip_address(self.user_info["address"], self.get_local_ip()) + template_id, msg_data = msg.to_send_data() + url = "https://www.bt.cn/api/v2/user/wx_web/send_template_msg_v3" + wx_account_ids = self.config["users"] if "users" in self.config else [self.config["id"], ] + data = { + "uid": self.user_info["uid"], + "access_key": self.user_info["access_key"], + "data": base64.b64encode(json.dumps(msg_data).encode('utf-8')).decode('utf-8'), + "wx_account_ids": base64.b64encode(json.dumps(wx_account_ids).encode('utf-8')).decode('utf-8'), + } + if template_id != "": + data["template_id"] = template_id + + status = False + error = None + user_name = self.config["users_nickname"] if "users_nickname" in self.config else [self.config["nickname"], ] + try: + + resp = public_http_post(url, data) + x = json.loads(resp) + if x["success"]: + status = True + else: + status = False + error = x["res"] + except: + error = traceback.format_exc() + + write_push_log("微信公众号", status, msg.thing_type, user_name) + + return error + + @classmethod + def refresh_config(cls, force: bool = False): + if os.path.exists(cls.need_refresh_file): + force = True + os.remove(cls.need_refresh_file) + if force or cls.last_refresh() + 60 * 10 < time.time(): + cls._get_by_web() + + @classmethod + def _get_by_web(cls) -> Optional[List]: + user_info = cls.get_user_info() + url = "https://www.bt.cn/api/v2/user/wx_web/bound_wx_accounts" + data = { + "uid": user_info["uid"], + "access_key": user_info["access_key"], + "serverid": user_info["serverid"] + } + try: + data = json.loads(public_http_post(url, data)) + if not data["success"]: + return None + except: + return None + + cls._save_user_info(data["res"]) + return data["res"] + + @staticmethod + def _save_user_info(user_config_list: List[Dict[str, Any]]): + print(user_config_list) + user_config_dict = {i["hex"]: i for i in user_config_list} + + remove_list = [] + sc = SenderConfig() + for i in sc.config: + if i['sender_type'] != "wx_account": + continue + if i['data'].get("hex", None) in user_config_dict: + i['data'].update(user_config_dict[i['data']["hex"]]) + user_config_dict.pop(i['data']["hex"]) + else: + remove_list.append(i) + + for r in remove_list: + sc.config.remove(r) + + if user_config_dict: # 还有多的 + for v in user_config_dict.values(): + v["title"] = v["nickname"] + sc.config.append({ + "id": sc.nwe_id(), + "used": True, + "sender_type": "wx_account", + "data": v + }) + sc.save_config() + + @classmethod + def unbind(cls, wx_account_uid: str): + user_info = cls.get_user_info() + if user_info is None: + return json_response(status=True, msg='未获取到用户绑定的信息') + url = "https://www.bt.cn/api/v2/user/wx_web/unbind_wx_accounts" + data = { + "uid": user_info["uid"], + "access_key": user_info["access_key"], + "serverid": user_info["serverid"], + "ids": str(wx_account_uid) + } + try: + datas = json.loads(public_http_post(url, data)) + if datas["success"]: + return json_response(status=True, data=datas, msg="解绑成功") + else: + return json_response(status=False, data=datas, msg=datas["res"]) + except: + return json_response(status=True, msg="链接云端失败") + + @classmethod + def get_auth_url(cls): + user_info = cls.get_user_info() + if user_info is None: + return json_response(status=True, msg='未获取到用户绑定的信息') + url = "https://www.bt.cn/api/v2/user/wx_web/get_auth_url" + data = { + "uid": user_info["uid"], + "access_key": user_info["access_key"], + "serverid": user_info["serverid"], + } + try: + datas = json.loads(public_http_post(url, data)) + if datas["success"]: + return json_response(status=True, data=datas) + else: + return json_response(status=False, data=datas, msg=datas["res"]) + except: + return json_response(status=True, msg="链接云端失败") + + def test_send_msg(self) -> Optional[str]: + test_msg = { + "msg_list": ['>配置状态:成功\n\n'] + } + test_task = get_test_msg("消息通道配置提醒") + res = self.send_msg( + test_task.to_wx_account_msg(test_msg, test_task.the_push_public_data()), + ) + if res is None: + return None + return res + + +# class wx_account_msg: +# __module_name = None +# __default_pl = "{}/data/default_msg_channel.pl".format(panelPath) +# conf_path = '{}/data/wx_account_msg.json'.format(panelPath) +# user_info = None +# +# def __init__(self): +# try: +# self.user_info = json.loads(public.ReadFile("{}/data/userInfo.json".format(public.get_panel_path()))) +# except: +# self.user_info = None +# self.__module_name = self.__class__.__name__.replace('_msg', '') +# +# def get_version_info(self, get): +# """ +# 获取版本信息 +# """ +# data = {} +# data['ps'] = '宝塔微信公众号,用于接收面板消息推送' +# data['version'] = '1.0' +# data['date'] = '2022-08-15' +# data['author'] = '宝塔' +# data['title'] = '微信公众号' +# data['help'] = 'http://www.bt.cn' +# return data +# +# def get_local_ip(self): +# '''获取内网IP''' +# import socket +# try: +# s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +# s.connect(('8.8.8.8', 80)) +# ip = s.getsockname()[0] +# return ip +# finally: +# s.close() +# return '127.0.0.1' +# +# def get_config(self, get): +# """ +# 微信公众号配置 +# """ +# if os.path.exists(self.conf_path): +# # 60S内不重复加载 +# start_time = int(time.time()) +# if os.path.exists("data/wx_account_msg.lock"): +# lock_time = 0 +# try: +# lock_time = int(public.ReadFile("data/wx_account_msg.lock")) +# except: +# pass +# # 大于60S重新加载 +# if start_time - lock_time > 60: +# public.run_thread(self.get_web_info2) +# public.WriteFile("data/wx_account_msg.lock", str(start_time)) +# else: +# public.WriteFile("data/wx_account_msg.lock", str(start_time)) +# public.run_thread(self.get_web_info2) +# data = json.loads(public.ReadFile(self.conf_path)) +# +# if not 'list' in data: data['list'] = {} +# +# title = '默认' +# if 'res' in data and 'nickname' in data['res']: title = data['res']['nickname'] +# +# data['list']['default'] = {'title': title, 'data': ''} +# +# data['default'] = self.__get_default_channel() +# return data +# else: +# public.run_thread(self.get_web_info2) +# return {"success": False, "res": "未获取到配置信息"} +# +# def set_config(self, get): +# """ +# @设置默认值 +# """ +# if 'default' in get and get['default']: +# public.writeFile(self.__default_pl, self.__module_name) +# +# return public.returnMsg(True, '设置成功') +# +# def get_web_info(self, get): +# if self.user_info is None: return public.returnMsg(False, '未获取到用户绑定的信息') +# url = "https://www.bt.cn/api/v2/user/wx_web/info" +# data = { +# "uid": self.user_info["uid"], +# "access_key": self.user_info["access_key"], +# "serverid": self.user_info["serverid"] +# } +# try: +# +# datas = json.loads(public.httpPost(url, data)) +# +# if datas["success"]: +# public.WriteFile(self.conf_path, json.dumps(datas)) +# return public.returnMsg(True, datas) +# else: +# public.WriteFile(self.conf_path, json.dumps(datas)) +# return public.returnMsg(False, datas) +# except: +# public.WriteFile(self.conf_path, json.dumps({"success": False, "res": "链接云端失败,请检查网络"})) +# return public.returnMsg(False, "链接云端失败,请检查网络") +# +# def unbind(self): +# if self.user_info is None: +# return public.returnMsg(False, '未获取到用户绑定的信息') +# url = "https://www.bt.cn/api/v2/user/wx_web/unbind" +# data = { +# "uid": self.user_info["uid"], +# "access_key": self.user_info["access_key"], +# "serverid": self.user_info["serverid"] +# } +# try: +# +# datas = json.loads(public.httpPost(url, data)) +# +# if os.path.exists(self.conf_path): +# os.remove(self.conf_path) +# +# if datas["success"]: +# return public.returnMsg(True, datas) +# else: +# return public.returnMsg(False, datas) +# except: +# public.WriteFile(self.conf_path, json.dumps({"success": False, "res": "链接云端失败,请检查网络"})) +# return public.returnMsg(False, "链接云端失败,请检查网络") +# +# def get_web_info2(self): +# if self.user_info is None: +# return public.returnMsg(False, '未获取到用户绑定的信息') +# url = "https://www.bt.cn/api/v2/user/wx_web/info" +# data = { +# "uid": self.user_info["uid"], +# "access_key": self.user_info["access_key"], +# "serverid": self.user_info["serverid"] +# } +# try: +# datas = json.loads(public.httpPost(url, data)) +# if datas["success"]: +# public.WriteFile(self.conf_path, json.dumps(datas)) +# return public.returnMsg(True, datas) +# else: +# public.WriteFile(self.conf_path, json.dumps(datas)) +# return public.returnMsg(False, datas) +# except: +# public.WriteFile(self.conf_path, json.dumps({"success": False, "res": "链接云端失败"})) +# return public.returnMsg(False, "链接云端失败") +# +# def get_send_msg(self, msg): +# """ +# @name 处理md格式 +# """ +# try: +# import re +# title = '宝塔告警通知' +# if msg.find("####") >= 0: +# try: +# title = re.search(r"####(.+)", msg).groups()[0] +# except: +# pass +# +# msg = msg.replace("####", ">").replace("\n\n", "\n").strip() +# s_list = msg.split('\n') +# +# if len(s_list) > 3: +# s_title = s_list[0].replace(" ", "") +# s_list = s_list[3:] +# s_list.insert(0, s_title) +# msg = '\n'.join(s_list) +# +# s_list = [] +# for msg_info in msg.split('\n'): +# reg = '(.+)' +# tmp = re.search(reg, msg_info) +# if tmp: +# tmp = tmp.groups()[0] +# msg_info = re.sub(reg, tmp, msg_info) +# s_list.append(msg_info) +# msg = '\n'.join(s_list) +# except: +# pass +# return msg, title +# +# def send_msg(self, msg): +# """ +# 微信发送信息 +# @msg 消息正文 +# """ +# +# if self.user_info is None: +# return public.returnMsg(False, '未获取到用户信息') +# +# if not isinstance(msg, str): +# return self.send_msg_v2(msg) +# +# msg, title = self.get_send_msg(msg) +# url = "https://www.bt.cn/api/v2/user/wx_web/send_template_msg_v2" +# datassss = { +# "first": { +# "value": "堡塔主机告警", +# }, +# "keyword1": { +# "value": "内网IP " + self.get_local_ip() + "\n外网IP " + self.user_info[ +# "address"] + " \n服务器别名 " + public.GetConfigValue("title"), +# }, +# "keyword2": { +# "value": "堡塔主机告警", +# }, +# "keyword3": { +# "value": msg, +# }, +# "remark": { +# "value": "如有疑问,请联系宝塔客服", +# }, +# } +# data = { +# "uid": self.user_info["uid"], +# "access_key": self.user_info["access_key"], +# "data": base64.b64encode(json.dumps(datassss).encode('utf-8')).decode('utf-8') +# } +# +# try: +# res = {} +# error, success = 0, 0 +# +# x = json.loads(public.httpPost(url, data)) +# conf = self.get_config(None)['list'] +# +# # 立即刷新剩余次数 +# public.run_thread(self.get_web_info2) +# +# res[conf['default']['title']] = 0 +# if x['success']: +# res[conf['default']['title']] = 1 +# success += 1 +# else: +# error += 1 +# +# try: +# public.write_push_log(self.__module_name, title, res) +# except: +# pass +# +# result = public.returnMsg(True, '发送完成,发送成功{},发送失败{}.'.format(success, error)) +# result['success'] = success +# result['error'] = error +# return result +# +# except: +# print(public.get_error_info()) +# return public.returnMsg(False, '微信消息发送失败。 --> {}'.format(public.get_error_info())) +# +# def push_data(self, data): +# if isinstance(data, dict): +# return self.send_msg(data['msg']) +# else: +# return self.send_msg_v2(data) +# +# def uninstall(self): +# if os.path.exists(self.conf_path): +# os.remove(self.conf_path) +# +# def send_msg_v2(self, msg): +# from push.base_push import WxAccountMsgBase, WxAccountMsg +# if self.user_info is None: +# return public.returnMsg(False, '未获取到用户信息') +# +# if isinstance(msg, public.dict_obj): +# msg = getattr(msg, "msg", "测试信息") +# if len(msg) >= 20: +# return self.send_msg(msg) +# +# if isinstance(msg, str): +# the_msg = WxAccountMsg.new_msg() +# the_msg.thing_type = msg +# the_msg.msg = msg +# msg = the_msg +# +# if not isinstance(msg, WxAccountMsgBase): +# return public.returnMsg(False, '消息类型错误') +# +# msg.set_ip_address(self.user_info["address"], self.get_local_ip()) +# +# template_id, msg_data = msg.to_send_data() +# url = "https://www.bt.cn/api/v2/user/wx_web/send_template_msg_v2" +# data = { +# "uid": self.user_info["uid"], +# "access_key": self.user_info["access_key"], +# "data": base64.b64encode(json.dumps(msg_data).encode('utf-8')).decode('utf-8'), +# } +# if template_id != "": +# data["template_id"] = template_id +# +# try: +# error, success = 0, 0 +# resp = public.httpPost(url, data) +# x = json.loads(resp) +# conf = self.get_config(None)['list'] +# +# # 立即刷新剩余次数 +# public.run_thread(self.get_web_info2) +# +# res = { +# conf['default']['title']: 0 +# } +# if x['success']: +# res[conf['default']['title']] = 1 +# success += 1 +# else: +# error += 1 +# +# try: +# public.write_push_log(self.__module_name, msg.thing_type, res) +# except: +# pass +# result = public.returnMsg(True, '发送完成,发送成功{},发送失败{}.'.format(success, error)) +# result['success'] = success +# result['error'] = error +# return result +# +# except: +# return public.returnMsg(False, '微信消息发送失败。 --> {}'.format(public.get_error_info())) diff --git a/mod/base/process/__init__.py b/mod/base/process/__init__.py new file mode 100644 index 00000000..7bc1c36f --- /dev/null +++ b/mod/base/process/__init__.py @@ -0,0 +1,7 @@ +from .process import RealProcess +from .user import RealUser +from .server import RealServer +from .process import Process +from .user import User +from .server import Server +__all__ = ['RealProcess', 'Process', 'RealUser', 'User', 'RealServer', 'Server'] diff --git a/mod/base/process/process.py b/mod/base/process/process.py new file mode 100644 index 00000000..65204437 --- /dev/null +++ b/mod/base/process/process.py @@ -0,0 +1,889 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: sww +# ------------------------------------------------------------------- +import json +import os +# ------------------------------ +# 进程模型 +# ------------------------------ +import sys +import time +import traceback + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +os.chdir("/www/server/panel") +import public +import psutil +from typing import Any + +try: + from BTPanel import cache +except: + import cachelib + + cache = cachelib.SimpleCache() + + +class RealProcess: + process_path = '/proc' + ps = json.loads(public.readFile('/www/server/panel/mod/base/process/process_ps.json')) + __isUfw = False + __isFirewalld = False + old_info = {} + new_info = {} + old_path = '/tmp/bt_task_old1.json' + __cpu_time = None + __process_net_list = {} + last_net_process = None + last_net_process_time = 0 + old_net_path = '/tmp/bt_network_old1.json' + old_net_info = {} + new_net_info = {} + + def __init__(self): + if os.path.exists('/usr/sbin/firewalld'): self.__isFirewalld = True + if os.path.exists('/usr/sbin/ufw'): self.__isUfw = True + + def object_to_dict(self, obj): + result = {} + for name in dir(obj): + value = getattr(obj, name) + if not name.startswith('__') and not callable(value) and not name.startswith('_'): result[name] = value + return result + + def get_computers_use(self): + result = {} + cpu_usage = psutil.cpu_percent(interval=1, percpu=True) + result['cpu'] = round(sum(cpu_usage) / len(cpu_usage), 2) + memory = psutil.virtual_memory() + print(memory.total) + result['memory_usage'] = memory.percent + disk = psutil.disk_usage('/') + result['disk_usage'] = round(((disk.used / disk.total) * 100), 0) + network_io = psutil.net_io_counters() + result['network_io_bytes_sent'] = network_io.bytes_sent + result['network_io_bytes_recv'] = network_io.bytes_recv + + return result + + # ------------------------------ 获取进程列表 start ------------------------------ + def get_process_list(self): + """ + 获取进程列表 + :return: + """ + try: + process_list = [] + if type(self.new_info) != dict: self.new_info = {} + self.new_info['cpu_time'] = self.get_cpu_time() + self.new_info['time'] = time.time() + self.get_process_net_list() + for proc in psutil.process_iter( + ['pid', 'ppid', 'name', 'username', 'create_time', 'memory_info', 'io_counters', 'num_threads', 'create_time', 'connections', 'open_files', 'status', 'cmdline']): + try: + proc_info = proc.as_dict( + attrs=['pid', 'ppid', 'name', 'username', 'create_time', 'memory_info', 'io_counters', 'num_threads', 'create_time', 'connections', 'open_files', 'status', + 'cmdline']) + p_cpus = proc.cpu_times() + process_list.append({ + 'pid': proc_info['pid'], + 'ppid': proc_info['ppid'], + 'name': proc_info['name'], + 'username': proc_info['username'], + 'cpu_percent': self.get_cpu_percent(str(proc_info['pid']), p_cpus, self.new_info['cpu_time']), + 'running_time': time.time() - proc_info['create_time'], + 'memory_info': proc_info['memory_info'], + 'io_info': proc_info['io_counters'], + 'num_threads': proc_info['num_threads'], + 'create_time': proc_info['create_time'], + 'connections_info': proc_info['connections'], + 'open_files': proc_info['open_files'], + 'ps': self.get_process_ps(proc.name())['data'], + 'status': proc_info['status'], + 'cmdline': proc_info['cmdline'], + 'net_info': self.get_process_network(proc_info['pid']) + }) + cache.set(self.old_path, self.new_info, 600) + except: + pass + return public.returnResult(code=1, msg='success', status=True, data=process_list) + except Exception as e: + return public.returnResult(code=0, msg='获取进程列表失败' + str(e), status=False) + + # ------------------------------ 获取进程列表 end ------------------------------ + + # ------------------------------ 获取进程信息 start ------------------------------ + + @staticmethod + def _format_connections(connects): + result = [] + for i in connects: + r_addr = i.raddr + if not i.raddr: + r_addr = ('', 0) + l_addr = i.laddr + if not i.laddr: + l_addr = ('', 0) + result.append({ + "fd": i.fd, + "family": i.family, + "local_addr": l_addr[0], + "local_port": l_addr[1], + "client_addr": r_addr[0], + "client_rport": r_addr[1], + "status": i.status + }) + return result + + @staticmethod + def get_connects(pid: str): + ''' + @name 获取进程连接信息 + @author hwliang<2021-08-09> + @param pid + @return dict + ''' + connects = 0 + try: + if pid == 1: + return connects + tp = '/proc/' + str(pid) + '/fd/' + if not os.path.exists(tp): + return connects + for d in os.listdir(tp): + f_name = tp + d + if os.path.islink(f_name): + l = os.readlink(f_name) + if l.find('socket:') != -1: + connects += 1 + except: + pass + return connects + + def get_process_info_by_pid(self, pid: int) -> dict: + """ + 获取进程信息 + :param pid: + :return: + """ + try: + status_ps = {'sleeping': '睡眠', 'running': '活动'} + process = psutil.Process(int(pid)) + if type(self.new_info) != dict: self.new_info = {} + self.new_info['cpu_time'] = self.get_cpu_time() + self.new_info['time'] = time.time() + self.get_process_net_list() + p_cpus = process.cpu_times() + # 获取连接信息 + connections = process.connections() + p_mem = process.memory_full_info() + io_info = process.io_counters() + info = { + 'pid': process.pid, + 'ppid': process.ppid(), + 'name': process.name(), + 'threads': process.num_threads(), + 'user': process.username(), + 'username': process.username(), + 'cpu_percent': self.get_cpu_percent(process.pid, p_cpus, self.new_info['cpu_time']), + 'memory_info': self.object_to_dict(p_mem), + 'memory_used': p_mem.uss, + 'io_info': self.object_to_dict(io_info), + "io_write_bytes": io_info.write_bytes, + "io_read_bytes": io_info.read_bytes, + 'connections': self._format_connections(connections), + "connects": self.get_connects(str(process.pid)), + 'status': status_ps[process.status()] if process.status() in status_ps else process.status(), + 'create_time': process.create_time(), + 'running_time': process.cpu_times().user + process.cpu_times().system, + 'cmdline': process.cmdline(), + 'open_files': [self.object_to_dict(i) for i in process.open_files()], + 'ps': self.get_process_ps(process.name())['data'], + 'net_info': self.get_process_network(process.pid), + "exe": ' '.join(process.cmdline()), + } + cache.set(self.old_path, self.new_info, 600) + return public.returnResult(code=1, msg='success', status=True, data=info) + except Exception as e: + return public.returnResult(code=0, msg='获取进程信息失败' + str(e), status=False) + + # 通过name获取进程信息 + def get_process_info_by_name(self, name: str) -> dict: + """ + 通过name获取进程信息 + :param name: + :return: + """ + try: + pids = [i.pid for i in psutil.process_iter(['pid', 'name', 'cmdline']) if i.name() == name] + infos = [] + for pid in pids: + try: + info = self.get_process_info_by_pid(pid) + if info['status']: + infos.append(info['data']) + except: + pass + return public.returnResult(code=1, msg='success', status=True, data=infos) + except Exception as e: + return public.returnResult(code=0, msg='获取进程信息失败' + str(e), status=False) + + # 通过启动命令获取进程信息 + def get_process_info_by_exec(self, cli: str) -> dict: + """ + 通过启动命令获取进程信息 + :param cli:启动命令 + :return: + """ + + try: + pids = [i.pid for i in psutil.process_iter(['pid', 'cmdline']) if cli in ' '.join(i.cmdline())] + infos = [] + for pid in pids: + try: + info = self.get_process_info_by_pid(pid) + if info['status']: + infos.append(info['data']) + except: + pass + return public.returnResult(code=1, msg='success', status=True, data=infos) + except Exception as e: + return public.returnResult(code=0, msg='获取进程信息失败' + str(e), status=False) + + def get_process_info_by_port(self, port: int) -> dict: + """ + 通过端口获取进程信息 + :param port: + :return: + """ + try: + infos = [] + for i in psutil.process_iter(['pid', 'connections']): + for conn in i.connections(): + try: + if conn.laddr.port == int(port): + info = self.get_process_info_by_pid(i.pid) + if info['status']: + infos.append(info['data']) + except: + pass + return public.returnResult(code=1, msg='success', status=True, data=infos) + except Exception as e: + return public.returnResult(code=0, msg='获取进程信息失败' + str(e), status=False) + + def get_process_info_by_ip(self, ip: str) -> dict: + """ + 通过远程ip获取进程信息 + :param ip: + :return: + """ + infos = [] + try: + for i in psutil.process_iter(['pid', 'connections']): + for conn in i.connections(): + try: + if conn.raddr: + if conn.raddr.ip == ip: + info = self.get_process_info_by_pid(i.pid)['data'] + if info: + infos.append(info) + except: + pass + return public.returnResult(code=1, msg='success', status=True, data=infos) + except: + return public.returnResult(code=0, msg='获取进程信息失败', status=False, data=infos) + + def get_process_info_by_openfile(self, file_path: str) -> dict: + """ + 通过打开文件获取进程信息 + :param file_path: + :return: + """ + infos = [] + try: + for i in psutil.process_iter(['pid', 'open_files']): + try: + for file in i.open_files(): + if file.path == file_path: + info = self.get_process_info_by_pid(i.pid)['data'] + if info: + infos.append(info) + except: + pass + return public.returnResult(code=1, msg='success', status=True, data=infos) + except: + return public.returnResult(code=0, msg='获取进程信息失败', status=False, data=infos) + + # ------------------------------ 获取进程信息 end ------------------------------ + + # ------------------------------ 获取进程ps start ------------------------------ + + def get_process_ps(self, name: str) -> dict: + """ + 获取进程ps + :param name: + :return: + """ + + return public.returnResult(code=1, msg='success', status=True, data=self.ps.get(name, '未知进程')) + + # ------------------------------ 获取进程ps end ------------------------------ + + # ------------------------------ 获取进程树 start ------------------------------ + + def get_process_tree(self, pid: int) -> dict: + """ + 获取进程树 + :param pid: + :return: + """ + try: + pid = int(pid) + process = psutil.Process(pid) + process_tree = process.children(recursive=True) + infos = [] + info = self.get_process_info_by_pid(pid) + if info['status']: + infos.append(info['data']) + + for prc in process_tree: + info = self.get_process_info_by_pid(prc.pid) + if info['status']: + infos.append(info['data']) + return public.returnResult(code=1, msg='success', status=True, data=infos) + except Exception as e: + return public.returnResult(code=0, msg='获取进程树失败' + str(e), status=False) + + # ------------------------------ 获取进程树 end ------------------------------ + + # ------------------------------ 结束进程 start ------------------------------ + # 结束进程pid + def kill_pid(self, pid: int) -> dict: + """ + 通过关闭进程 + :param pid: + :return: + """ + try: + os.kill(pid, 9) + return public.returnResult(code=1, msg='success', status=True, data='') + except Exception as e: + public.ExecShell('kill -9 ' + str(pid)) + return public.returnResult(code=1, msg='结束进程失败' + str(e), status=True) + + # 结束进程名 + def kill_name(self, name: str) -> dict: + """ + 通过name关闭进程 + :param name: + :return: + """ + try: + os.system('killall ' + name) + return public.returnResult(code=1, msg='success', status=True, data='') + except Exception as e: + return public.returnResult(code=0, msg='结束进程失败' + str(e), status=False) + + # 结束进程树 + def kill_tree(self, pid: int) -> dict: + """ + 通过关闭进程树 + :param pid: + :return: + """ + try: + p = psutil.Process(pid) + p.kill() + for i in p.children(recursive=True): + i.kill() + return public.returnResult(code=1, msg='success', status=True, data='') + except Exception as e: + public.ExecShell('kill -9 ' + str(pid)) + return public.returnResult(code=1, msg='success', status=True) + + # 结束所有进程 pid,进程名,进程树 + def kill_proc_all(self, pid: int) -> dict: + """ + 结束所有进程 + :return: + """ + try: + proc = psutil.Process(pid) + name = proc.name() + self.kill_pid(pid) + self.kill_name(name) + self.kill_tree(pid) + return public.returnResult(code=1, msg='success', status=True, data='') + except Exception as e: + return public.returnResult(code=0, msg='结束进程失败' + str(e), status=False) + + def kill_port(self, port: str) -> dict: + """ + 结束端口进程 + :param port: + :return: + """ + for process in psutil.process_iter(['pid', 'name', 'connections']): + try: + for conn in process.connections(): + if conn.laddr.port == int(port): + self.kill_pid(process.pid) + except: + pass + return public.returnResult(code=1, msg='success', status=True, data='') + + # ------------------------------ 结束进程 end ------------------------------ + + # ------------------------------ 拉黑ip start ------------------------------ + def add_black_ip(self, ips: list, ) -> dict: + """ + 拉黑ip + :param ip: + :return: + """ + try: + if not public.get_firewall_status() == 1: return public.returnMsg(False, '当前系统防火墙未开启') + if [ip for ip in ips if ip in ['0.0.0.0', '127.0.0.0', "::1"]]: return {'status': False, 'msg': '禁止拉黑本机ip', 'data': ''} + for ip in ips: + if not public.check_ip(ip): continue + if public.M('firewall_ip').where("port=?", (ip,)).count() > 0: continue + if self.__isUfw: + if public.is_ipv6(ip): + public.ExecShell('ufw deny from ' + ip + ' to any') + else: + public.ExecShell('ufw insert 1 deny from ' + ip + ' to any') + else: + if self.__isFirewalld: + if public.is_ipv6(ip): + public.ExecShell('firewall-cmd --permanent --add-rich-rule=\'rule family=ipv6 source address="' + ip + '" drop\'') + else: + public.ExecShell('firewall-cmd --permanent --add-rich-rule=\'rule family=ipv4 source address="' + ip + '" drop\'') + else: + if public.is_ipv6(ip): return public.returnMsg(False, 'FIREWALL_IP_FORMAT') + public.ExecShell('iptables -I INPUT -s ' + ip + ' -j DROP') + addtime = time.strftime('%Y-%m-%d %X', time.localtime()) + public.M('firewall_ip').add('address,addtime,types', (ip, addtime, 'drop')) + self.firewall_reload() + return public.returnResult(code=1, msg='success', status=True, data='') + except Exception as e: + return public.returnResult(code=0, msg='拉黑失败' + str(e), status=False) + + # ------------------------------ 拉黑ip end ------------------------------ + + # ------------------------------ 取消拉黑ip start ------------------------------ + # 删除IP屏蔽 + def del_black_ip(self, ips: list) -> dict: + try: + if not public.get_firewall_status() == 1: return public.returnMsg(False, '当前系统防火墙未开启') + for ip in ips: + if not public.check_ip(ip): continue + if self.__isUfw: + public.ExecShell('ufw delete deny from ' + ip + ' to any') + else: + if self.__isFirewalld: + if public.is_ipv6(ip): + public.ExecShell('firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv6 source address="' + ip + '" drop\'') + else: + public.ExecShell('firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv4 source address="' + ip + '" drop\'') + else: + public.ExecShell('iptables -D INPUT -s ' + ip + ' -j DROP') + + public.WriteLog("TYPE_FIREWALL", 'FIREWALL_ACCEPT_IP', (ip,)) + public.M('firewall_ip').where("address=?", (ip,)).delete() + + self.firewall_reload() + return public.returnResult(code=1, msg='success', status=True) + except Exception as e: + return public.returnResult(code=0, msg='删除失败' + str(e), status=False) + + # 重载防火墙配置 + def firewall_reload(self): + try: + if self.__isUfw: + public.ExecShell('/usr/sbin/ufw reload &') + return public.returnResult(code=1, msg='success', status=True) + if self.__isFirewalld: + public.ExecShell('firewall-cmd --reload &') + else: + public.ExecShell('/etc/init.d/iptables save &') + public.ExecShell('/etc/init.d/iptables restart &') + return public.returnResult(code=1, msg='success', status=True) + except: + return public.returnResult(code=0, msg='重载防火墙失败', status=False) + + # ------------------------------ 取消拉黑ip end ------------------------------ + + # ------------------------------ 获取进程cpu start ------------------------------ + + # 获取cpu使用率 + def get_cpu_percent(self, pid, cpu_times, cpu_time): + self.get_old() + percent = 0.00 + process_cpu_time = self.get_process_cpu_time(cpu_times) + if not self.old_info: self.old_info = {} + if not pid in self.old_info: + self.new_info[pid] = {} + self.new_info[pid]['cpu_time'] = process_cpu_time + return percent + try: + percent = round( + 100.00 * (process_cpu_time - self.old_info[pid]['cpu_time']) / (cpu_time - self.old_info['cpu_time']), 2) + except: + return 0 + self.new_info[pid] = {} + self.new_info[pid]['cpu_time'] = process_cpu_time + if percent > 0: return percent + return 0.00 + + def get_process_cpu_time(self, cpu_times): + cpu_time = 0.00 + for s in cpu_times: cpu_time += s + return cpu_time + + def get_old(self): + if self.old_info: return True + data = cache.get(self.old_path) + if not data: return False + self.old_info = data + del (data) + return True + + def get_cpu_time(self): + if self.__cpu_time: return self.__cpu_time + self.__cpu_time = 0.00 + s = psutil.cpu_times() + self.__cpu_time = s.user + s.system + s.nice + s.idle + return self.__cpu_time + + # ------------------------------ 获取进程cpu end ------------------------------ + + # ------------------------------ 获取进程net start ------------------------------ + + def get_process_network(self, pid): + ''' + @name 获取进程网络流量 + @author hwliang<2021-09-13> + @param pid 进程ID + @return tuple + ''' + if not self.__process_net_list: + self.get_process_net_list() + if not self.last_net_process_time: return 0, 0, 0, 0 + if not pid in self.__process_net_list: return 0, 0, 0, 0 + + if not pid in self.last_net_process: + return self.__process_net_list[pid]['up'], self.__process_net_list[pid]['up_package'], \ + self.__process_net_list[pid]['down'], self.__process_net_list[pid]['down_package'] + + up = int((self.__process_net_list[pid]['up'] - self.last_net_process[pid]['up']) / ( + time.time() - self.last_net_process_time)) + down = int((self.__process_net_list[pid]['down'] - self.last_net_process[pid]['down']) / ( + time.time() - self.last_net_process_time)) + up_package = int((self.__process_net_list[pid]['up_package'] - self.last_net_process[pid]['up_package']) / ( + time.time() - self.last_net_process_time)) + down_package = int( + (self.__process_net_list[pid]['down_package'] - self.last_net_process[pid]['down_package']) / ( + time.time() - self.last_net_process_time)) + return up, up_package, down, down_package + + def get_process_net_list(self): + w_file = '/dev/shm/bt_net_process' + if not os.path.exists(w_file): return + self.last_net_process = cache.get('net_process') + self.last_net_process_time = cache.get('last_net_process') + net_process_body = public.readFile(w_file) + if not net_process_body: return + net_process = net_process_body.split('\n') + for np in net_process: + if not np: continue + tmp = {} + np_list = np.split() + if len(np_list) < 5: continue + tmp['pid'] = int(np_list[0]) + tmp['down'] = int(np_list[1]) + tmp['up'] = int(np_list[2]) + tmp['down_package'] = int(np_list[3]) + tmp['up_package'] = int(np_list[4]) + self.__process_net_list[tmp['pid']] = tmp + cache.set('net_process', self.__process_net_list, 600) + cache.set('last_net_process', time.time(), 600) + + def get_network(self): + try: + self.get_net_old() + networkIo = psutil.net_io_counters()[:4] + self.new_net_info['upTotal'] = networkIo[0] + self.new_net_info['downTotal'] = networkIo[1] + self.new_net_info['upPackets'] = networkIo[2] + self.new_net_info['downPackets'] = networkIo[3] + self.new_net_info['time'] = time.time() + + if not self.old_net_info: self.old_net_info = {} + if not 'upTotal' in self.old_net_info: + time.sleep(0.1) + networkIo = psutil.net_io_counters()[:4] + self.old_net_info['upTotal'] = networkIo[0] + self.old_net_info['downTotal'] = networkIo[1] + self.old_net_info['upPackets'] = networkIo[2] + self.old_net_info['downPackets'] = networkIo[3] + self.old_net_info['time'] = time.time() + + s = self.new_net_info['time'] - self.old_net_info['time'] + networkInfo = {} + networkInfo['upTotal'] = networkIo[0] + networkInfo['downTotal'] = networkIo[1] + networkInfo['up'] = round((float(networkIo[0]) - self.old_net_info['upTotal']) / s, 2) + networkInfo['down'] = round((float(networkIo[1]) - self.old_net_info['downTotal']) / s, 2) + networkInfo['downPackets'] = networkIo[3] + networkInfo['upPackets'] = networkIo[2] + networkInfo['downPackets_s'] = int((networkIo[3] - self.old_net_info['downPackets']) / s) + networkInfo['upPackets_s'] = int((networkIo[2] - self.old_net_info['upPackets']) / s) + cache.set(self.old_net_path, self.new_net_info, 600) + return networkInfo + except: + return None + + def get_net_old(self): + if self.old_net_info: return True + data = cache.get(self.old_net_path) + if not data: return False + if not data: return False + self.old_net_info = data + del (data) + return True + + # ------------------------------ 获取进程net end ------------------------------ + + # ------------------------------ 获取启动项列表 start ------------------------------ + def get_run_list(self, search: str = ''): + runFile = ['/etc/rc.local', '/etc/profile', '/etc/inittab', '/etc/rc.sysinit'] + runList = [] + for rfile in runFile: + if not os.path.exists(rfile): continue + bodyR = self.clear_comments(public.readFile(rfile)) + if not bodyR: continue + stat = os.stat(rfile) + accept = str(oct(stat.st_mode)[-3:]) + if accept == '644': continue + tmp = {} + tmp['name'] = rfile + tmp['srcfile'] = rfile + tmp['size'] = os.path.getsize(rfile) + tmp['access'] = accept + tmp['ps'] = self.get_run_ps(rfile) + runList.append(tmp) + runlevel = self.get_my_runlevel() + runPath = ['/etc/init.d', '/etc/rc' + runlevel + '.d'] + tmpAll = [] + islevel = False + for rpath in runPath: + if not os.path.exists(rpath): continue + if runPath[1] == rpath: islevel = True + for f in os.listdir(rpath): + if f[:1] != 'S': continue + filename = rpath + '/' + f + if not os.path.exists(filename): continue + if os.path.isdir(filename): continue + if os.path.islink(filename): + flink = os.readlink(filename).replace('../', '/etc/') + if not os.path.exists(flink): continue + filename = flink + tmp = {} + tmp['name'] = f + if islevel: tmp['name'] = f[3:] + if tmp['name'] in tmpAll: continue + stat = os.stat(filename) + accept = str(oct(stat.st_mode)[-3:]) + if accept == '644': continue + tmp['srcfile'] = filename + tmp['access'] = accept + tmp['size'] = os.path.getsize(filename) + tmp['ps'] = self.get_run_ps(tmp['name']) + runList.append(tmp) + tmpAll.append(tmp['name']) + data = {} + data['run_list'] = runList + data['run_level'] = runlevel + if search: + data['run_list'] = self.search_run(data['run_list'], search) + return public.returnResult(code=1, msg='success', status=True, data=data) + + # 启动项查询 + def search_run(self, data, search): + try: + ldata = [] + for i in data: + if search in i['name'] or search in i['srcfile'] or search in i['ps']: + ldata.append(i) + return ldata + except: + return data + + # 清除注释 + def clear_comments(self, body): + bodyTmp = body.split("\n") + bodyR = "" + for tmp in bodyTmp: + if tmp.startswith('#'): continue + if tmp.strip() == '': continue + bodyR += tmp + return bodyR + + # 服务注释 + def get_run_ps(self, name): + runPs = {'netconsole': '网络控制台日志', 'network': '网络服务', 'jexec': 'JAVA', 'tomcat8': 'Apache Tomcat', + 'tomcat7': 'Apache Tomcat', 'mariadb': 'Mariadb', + 'tomcat9': 'Apache Tomcat', 'tomcat': 'Apache Tomcat', 'memcached': 'Memcached缓存器', + 'php-fpm-53': 'PHP-5.3', 'php-fpm-52': 'PHP-5.2', + 'php-fpm-54': 'PHP-5.4', 'php-fpm-55': 'PHP-5.5', 'php-fpm-56': 'PHP-5.6', 'php-fpm-70': 'PHP-7.0', + 'php-fpm-71': 'PHP-7.1', + 'php-fpm-72': 'PHP-7.2', 'rsync_inotify': 'rsync实时同步', 'pure-ftpd': 'FTP服务', + 'mongodb': 'MongoDB', 'nginx': 'Web服务器(Nginx)', + 'httpd': 'Web服务器(Apache)', 'bt': '宝塔面板', 'mysqld': 'MySQL数据库', 'rsynd': 'rsync主服务', + 'php-fpm': 'PHP服务', 'systemd': '系统核心服务', + '/etc/rc.local': '用户自定义启动脚本', '/etc/profile': '全局用户环境变量', + '/etc/inittab': '用于自定义系统运行级别', '/etc/rc.sysinit': '系统初始化时调用的脚本', + 'sshd': 'SSH服务', 'crond': '计划任务服务', 'udev-post': '设备管理系统', 'auditd': '审核守护进程', + 'rsyslog': 'rsyslog服务', 'sendmail': '邮件发送服务', 'blk-availability': 'lvm2相关', + 'local': '用户自定义启动脚本', 'netfs': '网络文件系统', 'lvm2-monitor': 'lvm2相关', + 'xensystem': 'xen云平台相关', 'iptables': 'iptables防火墙', 'ip6tables': 'iptables防火墙 for IPv6', + 'firewalld': 'firewall防火墙'} + if name in runPs: return runPs[name] + return name + + # 获取当前运行级别 + def get_my_runlevel(self): + try: + runlevel = public.ExecShell('runlevel')[0].split()[1] + except: + runlevel_dict = {"multi-user.target": '3', 'rescue.target': '1', 'poweroff.target': '0', + 'graphical.target': '5', "reboot.target": '6'} + r_tmp = public.ExecShell('systemctl get-default')[0].strip() + if r_tmp in runlevel_dict: + runlevel = runlevel_dict[r_tmp] + else: + runlevel = '3' + return runlevel + + # ------------------------------ 获取启动项列表 end ------------------------------ + + +class Process(object): + process = RealProcess() + + # 获取进程列表 + def get_process_list(self): + return self.process.get_process_list() + + # 获取进程信息->pid + def get_process_info_by_pid(self, get: Any) -> dict: + if not hasattr(get, 'pid'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_info_by_pid(get.pid) + + # 通过name获取进程信息 + def get_process_info_by_name(self, get: Any) -> dict: + if not hasattr(get, 'name'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_info_by_name(get.name) + + def get_process_info_by_exec(self, get: Any) -> dict: + if not hasattr(get, 'cli'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_info_by_exec(get.cli) + + def get_process_info_by_port(self, get: Any) -> dict: + if not hasattr(get, 'port'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_info_by_port(get.port) + + def get_process_info_by_ip(self, get: Any) -> dict: + if not hasattr(get, 'ip'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_info_by_ip(get.ip) + + def get_process_info_by_openfile(self, get: Any) -> dict: + if not hasattr(get, 'file_path'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_info_by_openfile(get.file_path) + + # 获取进程树 + def get_process_tree(self, get: Any) -> dict: + if not hasattr(get, 'pid'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_tree(get.pid) + + # 结束进程pid + def kill_pid(self, get: Any) -> dict: + if not hasattr(get, 'pid'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.kill_pid(get.pid) + + # 结束进程名 + def kill_name(self, get: Any) -> dict: + if not hasattr(get, 'name'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.kill_name(get.name) + + # 结束进程树 + def kill_tree(self, get: Any) -> dict: + if not hasattr(get, 'pid'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.kill_tree(get.pid) + + # 结束所有进程 pid,进程名,进程树 + def kill_proc_all(self, get: Any) -> dict: + if not hasattr(get, 'pid'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.kill_proc_all(get.pid) + + def kill_port(self, get: Any) -> dict: + if not hasattr(get, 'port'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.kill_port(get.port) + + def add_black_ip(self, get: Any) -> dict: + if not hasattr(get, 'ips'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.add_black_ip(get.ips) + + def del_black_ip(self, get: Any) -> dict: + if not hasattr(get, 'ips'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.del_black_ip(get.ips) + + def get_process_ps(self, get: Any) -> dict: + if not hasattr(get, 'name'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_process_ps(get.name) + + def get_run_list(self, get: Any) -> dict: + if not hasattr(get, 'search'): return {'status': False, 'msg': '参数错误', 'data': {}} + return self.process.get_run_list(get.search) + + +if __name__ == "__main__": + p = RealProcess() + print(p.get_computers_use()) + # print('========================') + # print(p.get_process_list()['data']) + # print('========================') + # print(p.get_process_info_by_pid(1)['data']) + # print('========================') + # print(p.get_process_info_by_name('systemd')) + # print('========================') + # res = p.get_process_tree(1) + # print(res['data'][1]) + # print('========================') + # print(p.kill_pid(1)) + # print('========================') + # print(p.kill_name('systemd')) + # print('========================') + # print(p.kill_tree(1)) + # print('========================') + # print(p.kill_proc_all(1)) + # print('========================') + # print(p.get_process_info_by_exec('nginx')) + # print('========================') + # print(p.get_process_info_by_port(8888)) + # print('========================') + # print(p.get_process_ps('nginx')) + # print('========================') + # print(p.get_process_info_by_ip('192.168.168.66')) + # print('========================') + # print(p.add_black_ip(['1.1.1.1'])) + # print('========================') + # print(p.del_black_ip(['1.1.1.1'])) + # print('========================') diff --git a/mod/base/process/process_ps.json b/mod/base/process/process_ps.json new file mode 100644 index 00000000..8863f7f3 --- /dev/null +++ b/mod/base/process/process_ps.json @@ -0,0 +1,133 @@ +{ + "bioset": "用于处理块设备上的I/O请求的进程", + "BT-MonitorAgent": "面板程序的进程", + "rngd": "一个熵守护的进程", + "master": "用于管理和协调子进程的活动的进程", + "irqbalance": "一个IRQ平衡守护的进程", + "rhsmcertd": "主要用于管理Red Hat订阅证书,并维护系统的订阅状态的进程", + "auditd": "是Linux审计系统中用户空间的一个组的进程", + "chronyd": "调整内核中运行的系统时钟和时钟服务器同步的进程", + "qmgr": "PBS管理器的进程", + "oneavd": "面板微步木马检测的进程", + "postgres": "PostgreSQL数据库的进程", + "grep": "一个命令行工具的进程", + "lsof": "一个命令行工具的进程", + "containerd-shim-runc-v2": "Docker容器的一个组件的进程", + "pickup": "用于监听Unix域套接字的进程", + "cleanup": "邮件传输代理(MTA)中的一个组件的进程", + "trivial-rewrite": "邮件传输代理(MTA)中的一个组件的进程", + "containerd": "docker依赖服务的进程", + "redis-server": "redis服务的进程", + "rcu_sched": "linux系统rcu机制服务的进程", + "jsvc": "面板tomcat服务的进程", + "oneav": "面板微步木马检测的进程", + "mysqld": "MySQL服务的进程", + "php-fpm": "PHP的子进程", + "php-cgi": "PHP-CGI的进程", + "nginx": "Nginx服务的进程", + "httpd": "Apache服务的进程", + "sshd": "SSH服务的进程", + "pure-ftpd": "FTP服务的进程", + "sftp-server": "SFTP服务的进程", + "mysqld_safe": "MySQL服务的进程", + "firewalld": "防火墙服务的进程", + "BT-Panel": "宝塔面板-主的进程", + "BT-Task": "宝塔面板-后台任务的进程", + "NetworkManager": "网络管理服务的进程", + "svlogd": "日志守护的进程", + "memcached": "Memcached缓存器的进程", + "gunicorn": "宝塔面板的进程", + "BTPanel": "宝塔面板的进程", + "baota_coll": "堡塔云控-主控端的进程", + "baota_client": "堡塔云控-被控端的进程", + "node": "Node.js程序的进程", + "supervisord": "Supervisor的进程", + "rsyslogd": "rsyslog日志服务的进程", + "crond": "计划任务服务的进程", + "cron": "计划任务服务的进程", + "rsync": "rsync文件同步的进程", + "ntpd": "网络时间同步服务的进程", + "rpc.mountd": "NFS网络文件系统挂载服务的进程", + "sendmail": "sendmail邮件服务的进程", + "postfix": "postfix邮件服务的进程", + "npm": "Node.js NPM管理器的进程", + "PM2": "Node.js PM2进程管理器的进程", + "htop": "htop进程监控软件的进程", + "btpython": "宝塔面板-独立Python环境的进程", + "btappmanagerd": "宝塔应用管理器插件的进程", + "dockerd": "Docker容器管理器的进程", + "docker-proxy": "Docker容器管理器的进程", + "docker-registry": "Docker容器管理器的进程", + "docker-distribution": "Docker容器管理器的进程", + "docker-network": "Docker容器管理器的进程", + "docker-volume": "Docker容器管理器的进程", + "docker-swarm": "Docker容器管理器的进程", + "docker-systemd": "Docker容器管理器的进程", + "docker-containerd": "Docker容器管理器的进程", + "docker-containerd-shim": "Docker容器管理器的进程", + "docker-runc": "Docker容器管理器的进程", + "docker-init": "Docker容器管理器的进程", + "docker-init-systemd": "Docker容器管理器的进程", + "docker-init-upstart": "Docker容器管理器的进程", + "docker-init-sysvinit": "Docker容器管理器的进程", + "docker-init-openrc": "Docker容器管理器的进程", + "docker-init-runit": "Docker容器管理器的进程", + "docker-init-systemd-resolved": "Docker容器管理器的进程", + "rpcbind": "NFS网络文件系统服务的进程", + "dbus-daemon": "D-Bus消息总线守护的进程", + "systemd-logind": "登录管理器的进程", + "systemd-journald": "Systemd日志管理服务的进程", + "systemd-udevd": "系统设备管理服务的进程", + "systemd-timedated": "系统时间日期服务的进程", + "systemd-timesyncd": "系统时间同步服务的进程", + "systemd-resolved": "系统DNS解析服务的进程", + "systemd-hostnamed": "系统主机名服务的进程", + "systemd-networkd": "系统网络管理服务的进程", + "systemd-resolvconf": "系统DNS解析服务的进程", + "systemd-local-resolv": "系统DNS解析服务的进程", + "systemd-sysctl": "系统系统参数服务的进程", + "systemd-modules-load": "系统模块加载服务的进程", + "systemd-modules-restore": "系统模块恢复服务的进程", + "agetty": "TTY登陆验证程序的进程", + "sendmail-mta": "MTA邮件传送代理的进程", + "(sd-pam)": "可插入认证模块的进程", + "polkitd": "授权管理服务的进程", + "mongod": "MongoDB数据库服务的进程", + "mongodb": "MongoDB数据库服务的进程", + "mongodb-mms-monitor": "MongoDB数据库服务的进程", + "mongodb-mms-backup": "MongoDB数据库服务的进程", + "mongodb-mms-restore": "MongoDB数据库服务的进程", + "mongodb-mms-agent": "MongoDB数据库服务的进程", + "mongodb-mms-analytics": "MongoDB数据库服务的进程", + "mongodb-mms-tools": "MongoDB数据库服务的进程", + "mongodb-mms-backup-agent": "MongoDB数据库服务的进程", + "mongodb-mms-backup-tools": "MongoDB数据库服务的进程", + "mongodb-mms-restore-agent": "MongoDB数据库服务的进程", + "mongodb-mms-restore-tools": "MongoDB数据库服务的进程", + "mongodb-mms-analytics-agent": "MongoDB数据库服务的进程", + "mongodb-mms-analytics-tools": "MongoDB数据库服务的进程", + "dhclient": "DHCP协议客户端的进程", + "dhcpcd": "DHCP协议客户端的进程", + "dhcpd": "DHCP服务器的进程", + "isc-dhcp-server": "DHCP服务器的进程", + "isc-dhcp-server6": "DHCP服务器的进程", + "dhcp6c": "DHCP服务器的进程", + "dhcpcd": "DHCP服务器的进程", + "dhcpd": "DHCP服务器的进程", + "avahi-daemon": "Zeroconf守护的进程", + "login": "登录的进程", + "systemd": "系统管理服务的进程", + "systemd-sysv": "系统管理服务的进程", + "systemd-journal-gateway": "系统管理服务的进程", + "systemd-journal-remote": "系统管理服务的进程", + "systemd-journal-upload": "系统管理服务的进程", + "systemd-networkd": "系统网络管理服务的进程", + "rpc.idmapd": "NFS网络文件系统相关服务的进程", + "cupsd": "打印服务的进程", + "cups-browsed": "打印服务的进程", + "sh": "shell的进程", + "php": "PHP CLI模式的进程", + "blkmapd": "NFS映射服务的进程", + "lsyncd": "文件同步服务的进程", + "sleep": "延迟的进程" +} diff --git a/mod/base/process/server.py b/mod/base/process/server.py new file mode 100644 index 00000000..2a300445 --- /dev/null +++ b/mod/base/process/server.py @@ -0,0 +1,715 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: sww +# ------------------------------------------------------------------- +import json +import os +# ------------------------------ +# 服务模型 +# ------------------------------ +import sys, re +import time +import traceback + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +os.chdir("/www/server/panel") +import public +import glob + +# 关闭系统加固执行函数后打开 +def syssafe_admin(func): + def wrapper(*args, **kwargs): + syssafe_flag = 0 + # 检查系统加固并且关闭 + if os.path.exists('/www/server/panel/plugin/syssafe/init.sh'): + res = public.ExecShell('/www/server/panel/plugin/syssafe/init.sh status') + if 'already running' in res[0]: + try: + syssafe_flag = 1 + public.ExecShell('/www/server/panel/plugin/syssafe/init.sh stop') + res = public.ExecShell('/www/server/panel/plugin/syssafe/init.sh status') + if 'already running' in res[0]: + import PluginLoader + PluginLoader.plugin_run('syssafe', 'set_open', public.to_dict_obj({'status': 0})) + print('已关闭系统加固!') + except: + pass + e = None + result = None + try: + result = func(*args, **kwargs) + except Exception as ex: + e= ex + try: + if syssafe_flag: + public.ExecShell('/www/server/panel/plugin/syssafe/init.sh stop') + res = public.ExecShell('/www/server/panel/plugin/syssafe/init.sh status') + if 'already running' not in res[0]: + import PluginLoader + PluginLoader.plugin_run('syssafe', 'set_open', public.to_dict_obj({'status': 1})) + print('已开启系统加固!') + except: + pass + if e is not None: + raise e + return result + return wrapper + + + +class RealServer: + + server_list = ['mysqld_safe', 'redis-server', 'mongod', 'postgres', 'nginx', 'memcached', 'httpd', 'pure-ftpd', 'jsvc', 'dockerd'] + system_info = None + # --------------------- 常用服务管理 start---------------------- + def server_admin(self, server_name: str, option: str) -> dict: + + """ + 服务管理 + :param server_name:'mysqld_safe', 'redis-server', 'mongod', 'postgres', 'nginx', 'memcached', 'httpd', 'pure-ftpd', 'jsvc', 'dockerd' + :param option: start,stop,restart + :return: + """ + servers = { + "mongod": self.__mongod_admin, + "redis-server": self.__redis_admin, + "memcached": self.__memcached_admin, + "dockerd": self.__docker_admin, + "jsvc": self.__tomcat_admin, + "pure-ftpd": self.__ftp_admin, + "httpd": self.__apache_admin, + "mysqld_safe": self.__mysqld_admin, + "nginx": self.__nginx_admin, + "postgres": self.__pgsql_admin, + } + from system import system + self.syst = system() + if server_name in self.server_list: + res = servers[server_name](option) + return public.returnResult(code=1, msg=res['msg'], status=res['status']) + else: + return public.returnResult(code=0, msg='操作失败!参数不存在', status=False) + + def __mongod_admin(self, option: str) -> dict: + try: + Command = {"start": "/etc/init.d/mongodb start", + "stop": "/etc/init.d/mongodb stop", } + if option != 'restart': + public.ExecShell(Command.get(option)) + return public.returnMsg(True, '操作成功!') + public.ExecShell(Command.get('stop')) + public.ExecShell(Command.get('start')) + return public.returnMsg(True, '操作成功!') + except: + return public.returnMsg(False, '操作失败!') + + def __redis_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'redis' + get.type = option + + return self.syst.serverAdmin(get) + except: + return public.returnMsg(False, '操作失败!') + + def __memcached_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'memcached' + get.type = option + return self.syst.serverAdmin(get) + except: + return public.returnMsg(False, '操作失败!') + + def __docker_admin(self, option: str) -> dict: + try: + exec_str = 'systemctl {} docker.socket'.format(option) + public.ExecShell(exec_str) + return public.returnMsg(True, "操作成功") + except: + return public.returnMsg(False, '操作失败!') + + def __tomcat_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'tomcat' + get.type = option + self.syst.serverAdmin(get) + return public.returnMsg(True, '操作成功!') + except: + return public.returnMsg(False, '操作失败!') + + def __ftp_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'pure-ftpd' + get.type = option + return self.syst.serverAdmin(get) + except: + return public.returnMsg(False, '操作失败!') + + def __apache_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'apache' + get.type = option + res = self.syst.serverAdmin(get) + import time + time.sleep(1) + return res + except: + return public.returnMsg(False, '操作失败!') + + def __mysqld_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'mysqld' + get.type = option + return self.syst.serverAdmin(get) + except: + return public.returnMsg(False, '操作失败!') + + def __nginx_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'nginx' + get.type = option + return self.syst.serverAdmin(get) + except: + return public.returnMsg(False, '操作失败!') + + def __pgsql_admin(self, option: str) -> dict: + try: + get = public.dict_obj() + get.name = 'pgsql' + get.type = option + return self.syst.serverAdmin(get) + except: + return public.returnMsg(False, '操作失败!') + + # ----------------------常用服务管理 end---------------------- + + # ----------------------常用服务状态 start---------------------- + def server_status(self, server_name: str) -> dict: + """ + 服务状态 + :param server_name: 'mysqld_safe', 'redis-server', 'mongod', 'postgres', 'nginx', 'memcached', 'httpd', 'pure-ftpd', 'jsvc', 'dockerd' + :return: + """ + try: + if server_name in self.server_list: + res = self.__get_status(server_name) + return public.returnResult(code=1, msg=res['msg'], data=res['data'], status=res['status']) + else: + return public.returnResult(code=0, msg='操作失败!参数不存在', status=False) + except Exception as e: + return public.returnResult(code=0, msg='操作失败!', status=False) + + def __is_installation(self, name: str) -> bool: + map = { + "mysqld_safe": "mysqld", + "redis-server": "redis", + "mongod": "mongodb", + "postgres": "pgsql", + "nginx": "nginx", + "memcached": "memcached", + "httpd": "httpd", + "pure-ftpd": "pure-ftpd", + "jsvc": "tomcat", + "dockerd": "docker", + "php": "php", + "tamper_proof": "tamper_proof", + "bt_security": "bt_security", + "syssafe": "syssafe", + + } + import glob + dir_path = '/etc/init.d/' + files = [os.path.basename(f) for f in glob.glob(dir_path + "*")] + if name == "dockerd": + res = public.ExecShell('docker -v')[0] + if 'version' in res: + return True + return False + if name == "postgres": + res = public.ExecShell('/www/server/pgsql/bin/psql --version')[0] + pgsql = False + if 'PostgreSQL' in res: + pgsql = True + Manager = False + if os.path.exists('/www/server/panel/plugin/pgsql_manager'): + Manager = True + return {'pgsql': pgsql, 'Manager': Manager} + if name == "php": + php_l = [i for i in files if name in i.lower()] + if len(php_l) != 0: + return True + if name == "tamper_proof": + return os.path.exists('/www/server/panel/plugin/tamper_proof') + + if name == "bt_security": + return os.path.exists('/www/server/panel/plugin/bt_security') + + if name == "syssafe": + return os.path.exists('/www/server/panel/plugin/syssafe') + + if map[name] in files: + return True + return False + + def __get_status(self, server_name: str) -> dict: + try: + if not self.__is_installation(server_name): + return {'status': True, 'msg': '', 'data': {'install': False, 'status': False}} + res = public.ExecShell('ps -ef|grep {}|grep -v grep'.format(server_name))[0] + if 'mongod' in res: + return {'status': True, 'msg': '', 'data': {'install': True, 'status': True}} + return {'status': True, 'msg': '', 'data': {'install': True, 'status': False}} + except: + return {'status': False, 'msg': '获取失败!', 'data': {'install': False, 'status': False}} + + # ----------------------常用服务状态 end---------------------- + + # ---------------------- 通用服务管理 start---------------------- + def universal_server_admin(self, server_name: str, option: str) -> dict: + """ + 通用服务管理 服务器在/etc/init.d/目录下有同名的启动文件,且启动文件中有start,stop,restart,status命令 + :param server_name: 服务名称 + :param option: start,stop,restart + :return: + """ + try: + get = public.dict_obj() + get.name = server_name + get.type = option + dir_path = '/etc/init.d/' + files = [os.path.basename(f) for f in glob.glob(dir_path + "*")] + if server_name in files: + res = public.ExecShell('/etc/init.d/{} {}'.format(server_name, option)) + if 'is running' in res[0].lower() or 'is active' in res[0].lower() or 'already running' in res[0].lower(): + return public.returnResult(code=1, msg='操作成功!', status=True) + if 'is stopped' in res[0].lower() or 'is not running' in res[0].lower(): + return public.returnResult(code=1, msg='操作成功!', status=True) + else: + return public.returnResult(code=0, msg='操作失败!未在/etc/init.d/目录下找到该服务', status=False) + except: + return public.returnResult(code=0, msg='操作失败!', status=False) + + # ---------------------- 通用服务管理 end---------------------- + + # ---------------------- 通用服务状态 start---------------------- + def universal_server_status(self, server_name: str) -> dict: + """ + 通用服务状态 服务器在/etc/init.d/目录下有同名的启动文件,且启动文件中有status命令,status中有输出is running或is active + :param server_name: 服务名称 + :return: + """ + try: + get = public.dict_obj() + get.name = server_name + get.type = 'status' + dir_path = '/etc/init.d/' + files = [os.path.basename(f) for f in glob.glob(dir_path + "*")] + if server_name in files: + res = public.ExecShell('/etc/init.d/{} status'.format(server_name)) + if 'is running' in res[0].lower() or 'is active' in res[0].lower() or 'already running' in res[0].lower(): + return public.returnResult(code=1, msg='运行中', data=True) + return public.returnResult(code=1, msg='未运行', data=False) + return public.returnResult(code=0, msg='服务不存在!', status=False) + except: + return public.returnResult(code=0, msg='获取失败!', data=False) + + # ---------------------- 通用服务状态 end---------------------- + + # ---------------------- 添加开机自启 启动脚本 start---------------------- + + # 添加开机自启 + @syssafe_admin + def add_boot(self, server_name: str, pid_file: str, start_exec: str, stop_exec: str, default_start: str = '2 3 4 5') -> dict: + """ + 添加开机自启 + :param server_name: 服务名称 + :param pid_file: 启动pid记录文件 + :param start_exec: 启动命令 + :param stop_exec: 停止命令 + :param default_start: 默认启动级别 + :return: + """ + + content = """ +#! /bin/sh +# chkconfig: 2345 55 25 + +### BEGIN INIT INFO +# Provides: {name} +# Required-Start: $all +# Required-Stop: $all +# Default-Start: {default_start} +# Default-Stop: 0 1 6 +# Short-Description: {name} +# Description: {name} +### END INIT INFO + +# Author: licess +# website: http://www.bt.cn + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +case "$1" in + start) + echo -n "Starting {name}... " + if [ -f {pid_file} ];then + mPID=$(cat {pid_file}) + isStart=`ps ax | awk '{{ print $1 }}' | grep -e "^${{mPID}}$"` + if [ "$isStart" != "" ];then + echo "{name} (pid $mPID) already running." + exit 1 + fi + fi + nohup {start_exec} & + if [ $? != 0 ]; then + echo " failed" + exit 1 + else + pid=`ps -ef|grep "{start_exec}" |grep -v grep|awk '{{print $2}}'` + echo $! > {pid_file} + echo " done" + fi + ;; + stop) + echo -n "Stopping {name}... " + if [ -f {pid_file} ];then + mPID=$(cat {pid_file}) + isStart = `ps ax | awk '{{ print $1 }}' | grep -e "^${{mPID}}$"` + if [ "$isStart" = "" ];then + echo "{name} is stopped" + exit 1 + fi + else + echo "{name} is stopped" + exit 1 + fi + nohup {stop_exec} & + if [ $? != 0 ]; then + echo " failed. Use force-quit" + exit 1 + else + echo " done" + fi + ;; + status) + if [ -f {pid_file} ];then + mPID=`cat {pid_file}` + isStart=`ps ax | awk '{{ print $1 }}' | grep -e "^${{mPID}}$"` + if [ "$isStart" != '' ];then + echo "{name} (pid `pidof {name}`) is running." + exit 1 + else + echo "{name} is stopped" + exit 0 + fi + else + echo "{name} is stopped" + exit 0 + fi + ;; + restart) + $0 stop + sleep 1 + $0 start + ;; +esac +""".format(name=server_name, pid_file=pid_file, start_exec=start_exec, stop_exec=stop_exec, default_start=default_start) + + + if os.path.exists(os.path.join('/etc/init.d/', server_name)): + return public.returnResult(code=1, msg='操作失败!服务已存在', status=False) + try: + public.writeFile(os.path.join('/etc/init.d/', server_name), content) + os.chmod(os.path.join('/etc/init.d/', server_name), 0o777) + if os.path.exists('/usr/sbin/update-rc.d'): + public.ExecShell('update-rc.d -f {} defaults'.format(server_name)) + else: + public.ExecShell('systemctl enable {}'.format(server_name)) + return public.returnResult(code=1, msg='操作成功!', status=True) + except: + return public.returnResult(code=0, msg='操作失败!', status=False) + + # ---------------------- 添加开机自启 启动脚本 end---------------------- + + # ---------------------- 删除开机自启 启动脚本 start---------------------- + def del_boot(self, server_name: str) -> dict: + """ + 删除启动脚本 + :param server_name: 服务名称 + :return: + """ + try: + if os.path.exists(os.path.join('/etc/init.d/', server_name)): + if os.path.exists('/usr/sbin/update-rc.d'): + public.ExecShell('update-rc.d -f {} remove'.format(server_name)) + else: + public.ExecShell('systemctl disable {}'.format(server_name)) + os.remove(os.path.join('/etc/init.d/', server_name)) + return public.returnResult(code=1, msg='操作成功!', status=True) + return public.returnResult(code=0, msg='操作失败!服务不存在', status=False) + except: + return public.returnResult(code=0, msg='操作失败!', status=False) + + # ---------------------- 删除开机自启 启动脚本 end---------------------- + + # ---------------------- 创建服务守护进程 start---------------------- + + @syssafe_admin + def create_daemon(self, server_name: str, pid_file: str, start_exec: str, workingdirectory: str, user: str = 'root', is_power_on: int = 1, logs_file: str = '', environments: str = '', + is_fork=0, restart_type='always') -> dict: + """ + 创建服务守护进程 + :param server_name: 服务名称 + :param pid_file: 启动pid记录文件 + :param start_exec: 启动命令 + :param stop_exec: 停止命令 + :return: + """ + + # 检查系统加固插件是否存在 + try: + content = ''' +[Unit] +Description={server_name} +After=network.target + +[Service] +{environments} +ExecStart={start_exec} +ExecStop=/usr/bin/pkill -9 "{start_exec}" +WorkingDirectory={workingdirectory} +Restart={restart_type} +SyslogIdentifier={server_name} +User={user} +Type=simple +PrivateTmp=false +PIDFile={pid_file} + +[Install] +WantedBy=multi-user.target +'''.format( + start_exec=start_exec, + workingdirectory=workingdirectory, + user=user, pid_file=pid_file, + server_name=server_name, + environments=environments, + restart_type=restart_type, + ) + + if is_fork: + content = content.replace('Type=simple', 'Type=forking') + if not os.path.exists('/usr/lib/systemd/system/'): + public.ExecShell('mkdir -p /usr/lib/systemd/system/') + public.writeFile('/usr/lib/systemd/system/{}.service'.format(server_name), content) + if is_power_on: + public.ExecShell('systemctl enable {}'.format(server_name)) + public.ExecShell('systemctl daemon-reload') + + if not logs_file: + logs_file = '/www/wwwlogs/project_{}.log'.format(server_name) + + rsyslog_conf = public.readFile('/etc/rsyslog.conf') + add_conf = "if $programname == '{}' then {}\n".format(server_name, logs_file) + if rsyslog_conf: + idx = rsyslog_conf.find("if $programname == '{}' then".format(server_name)) + if idx == -1: + rsyslog_conf += "\n" + add_conf + else: + line_idx = rsyslog_conf.find('\n', idx) + rsyslog_conf = rsyslog_conf[:idx] + add_conf + rsyslog_conf[line_idx:] + public.writeFile('/etc/rsyslog.conf', rsyslog_conf) + + public.ExecShell('systemctl restart rsyslog') + if not os.path.exists(logs_file): + public.ExecShell('touch {}'.format(logs_file)) + public.ExecShell('chown -R {user}:{user} {logs_file}'.format(user=user, logs_file=logs_file)) + self.daemon_admin(server_name, 'restart') + return public.returnResult(code=1, msg='操作成功!', status=True) + except: + return public.returnResult(code=0, msg='操作失败!', status=False) + + # ---------------------- 创建服务守护进程 end---------------------- + + # ---------------------- 删除服务守护进程 start---------------------- + @syssafe_admin + def del_daemon(self, server_name: str) -> dict: + """ + 删除服务守护进程 + :param server_name: 服务名称 + :return: + """ + try: + public.ExecShell('systemctl stop {}'.format(server_name)) + if os.path.exists('/usr/lib/systemd/system/{}.service'.format(server_name)): + public.ExecShell('systemctl disable {}'.format(server_name)) + os.remove('/usr/lib/systemd/system/{}.service'.format(server_name)) + public.ExecShell('systemctl daemon-reload') + public.ExecShell(r'sed -i "/if \$programname == {}/d" /etc/rsyslog.conf'.format(server_name)) + public.ExecShell('systemctl restart rsyslog') + return public.returnResult(code=1, msg='操作成功!', status=True) + return public.returnResult(code=0, msg='操作失败!', status=False) + except: + return public.returnResult(code=0, msg='操作失败!', status=False) + + # ---------------------- 删除服务守护进程 end---------------------- + + # ---------------------- 服务守护进程状态 start---------------------- + def daemon_status(self, server_name: str) -> dict: + """ + 服务守护进程状态 + :param server_name: 服务名称 + :return: + """ + try: + if not os.path.exists('/usr/lib/systemd/system/{}.service'.format(server_name)): + return public.returnResult(code=0, msg='服务不存在!', status=False) + if not self.system_info: + self.system_info = public.ExecShell("systemctl |grep service|grep -E 'active|deactivating'|awk '{print $1}'")[0] + if server_name+'.service' in self.system_info: + return public.returnResult(code=1, msg='运行中', status=True) + return public.returnResult(code=1, msg='未运行', status=False) + except: + return public.returnResult(code=0, msg='操作失败!', status=False) + + # ---------------------- 服务守护进程状态 end---------------------- + + def daemon_admin(self, server_name: str,action:str) -> dict: + """ + + :param server_name: 项目名称 + :param action: 操作 + """ + public.ExecShell('systemctl {} {}'.format(action,server_name)) + return public.returnResult(code=1, msg='操作指令已执行', status=True) + # if action == 'start' or action == 'restart': + # num = 0 + # for i in range(5): + # time.sleep(0.01) + # if self.daemon_status(server_name)['status']: + # num += 1 + # if num > 3: + # return public.returnResult(code=1, msg='启动成功!', status=True) + # return public.returnResult(code=0, msg='启动失败!', status=False) + # return public.returnResult(code=1, msg='关闭成功!' + res[0] + res[1], status=True) + + def get_daemon_pid(self, server_name: str) -> dict: + """ + 获取守护进程pid + :param server_name: 项目名称 + """ + res = public.ExecShell("systemctl show --property=MainPID {}".format(server_name))[0] # type: str + if not res.startswith('MainPID='): + return public.returnResult(code=0, msg='获取失败!', status=False) + + try: + pid = int(res.split("=", 1)[1]) + return public.returnResult(code=1, msg='获取成功!', data=pid, status=True) + except: + return public.returnResult(code=0, msg='获取失败', status=False) + + + # ---------------------- 延时定时启动 start---------------------- + def add_task(self, shell: str, time: int) -> dict: + """ + 服务定时启动 + :param server_name: 服务名称 + :param start_exec: 启动命令 + :param minute: 定时启动时间 + :return: + """ + data = { + 'type': 3, + 'time': time, + 'name': shell, + 'title': '', + 'fun': '', + 'args': '' + } + + res = public.set_tasks_run(data) + if res['status']: + return public.returnResult(code=1, msg='操作成功!', status=True) + return public.returnResult(code=0, msg='操作失败!', status=False) + + # ---------------------- 服务定时启动 end---------------------- + + +class Server: + server = RealServer() + + def server_admin(self, get): + try: + if hasattr(self.server, get.name): + return getattr(self.server, get.name)(get.type) + return public.returnMsg(False, '操作失败!参数不存在') + except: + return public.returnMsg(False, '操作失败!') + + def server_status(self, get): + try: + if hasattr(self.server, get.name): + return getattr(self.server, get.name)() + return public.returnMsg(False, '操作失败!参数不存在') + except: + return public.returnMsg(False, '操作失败!') + + def universal_server_admin(self, get): + try: + return self.server.universal_server_admin(get.name, get.type) + except: + return public.returnMsg(False, '操作失败!') + + def universal_server_status(self, get): + try: + return self.server.universal_server_status(get.name) + except: + return public.returnMsg(False, '操作失败!') + + def add_boot(self, get): + try: + return self.server.add_boot(get.name, get.pid_file, get.start_exec, get.stop_exec) + except: + return public.returnMsg(False, '操作失败!') + + def del_boot(self, get): + try: + return self.server.del_boot(get.name) + except: + return public.returnMsg(False, '操作失败!') + + def create_daemon(self, get): + try: + return self.server.create_daemon(get.name, get.pid_file, get.start_exec, get.user) + except: + return public.returnMsg(False, '操作失败!') + + def del_daemon(self, get): + try: + return self.server.del_daemon(get.name) + except: + return public.returnMsg(False, '操作失败!') + + def daemon_status(self, get): + try: + return self.server.daemon_status(get.name) + except: + return public.returnMsg(False, '操作失败!') + + def add_task(self, get): + try: + return self.server.add_task(get.shell, get.time) + except: + return public.returnMsg(False, '操作失败!') diff --git a/mod/base/process/user.py b/mod/base/process/user.py new file mode 100644 index 00000000..e205a4a1 --- /dev/null +++ b/mod/base/process/user.py @@ -0,0 +1,630 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: sww +# ------------------------------------------------------------------- +import json +import os +# ------------------------------ +# 用户模型 +# ------------------------------ +import sys +import traceback +import psutil + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +os.chdir("/www/server/panel") +import public +from typing import List, Dict, Any, Union + + +class RealUser: + def __init__(self): + self.groupList = self.get_group_list()['data'] + print(self.groupList) + + # --------------------获取用户列表 Start-------------------- + def get_user_list(self, search: str = '') -> Dict[str, Union[bool, str, List[Dict[str, Any]]]]: + """ + 获取用户列表 + :param search: 搜索关键词 可搜索 用户名、备注、shell、home、用户组 + :return: list + """ + try: + tmpList = public.readFile('/etc/passwd').split("\n") + userList = [] + for ul in tmpList: + tmp = ul.split(':') + if len(tmp) < 6: continue + userInfo = {} + userInfo['username'] = tmp[0] + userInfo['uid'] = tmp[2] + userInfo['gid'] = tmp[3] + userInfo['group'] = self._get_group_name(tmp[3]) + userInfo['ps'] = self._get_user_ps(tmp[0], tmp[4]) + userInfo['home'] = tmp[5] + userInfo['login_shell'] = tmp[6] + userList.append(userInfo) + if search != '': + userList = self._search_user(userList, search) + return public.returnResult(code=1, data=userList, msg='获取用户列表成功!', status=True) + except Exception as e: + print(traceback.format_exc()) + return public.returnResult(code=0, data=[], msg='获取用户列表失败!错误:' + str(e), status=False) + + def _get_user_ps(self, name: str, ps: str) -> str: + """ + 获取用户备注 + :param name: 用户名 + :param ps: 备注 + :return: str + """ + userPs = {'www': '宝塔面板', 'root': '超级管理员', 'mysql': '用于运行MySQL的用户', + 'mongo': '用于运行MongoDB的用户', + 'git': 'git用户', 'mail': 'mail', 'nginx': '第三方nginx用户', 'postfix': 'postfix邮局用户', + 'lp': '打印服务帐号', + 'daemon': '控制后台进程的系统帐号', 'nobody': '匿名帐户', 'bin': '管理大部分命令的帐号', + 'adm': '管理某些管理文件的帐号', 'smtp': 'smtp邮件'} + if name in userPs: return userPs[name] + if not ps: return name + return ps + + def _get_group_name(self, gid: str) -> str: + """ + 获取用户组名称 + :param gid: 用户组ID + :return: str + """ + for g in self.groupList: + if g['gid'] == gid: return g['group'] + return '' + + def _search_user(self, data: List[Dict[str, Any]], search: str) -> List[Dict[str, Union[str, Any]]]: + """ + 搜索用户 + :param data: 用户列表 + :param search: 搜索关键词 + :return: list + """ + try: + ldata = [] + for i in data: + if search in i['username'] or search in i['ps'] or search in i['login_shell'] or search in i['home'] or search in i['group']: + ldata.append(i) + return ldata + except: + return data + + def get_group_list(self): + """ + 获取用户组列表 + :return:list + + """ + tmpList = public.readFile('/etc/group').split("\n") + groupList = [] + for gl in tmpList: + tmp = gl.split(':') + if len(tmp) < 3: continue + groupInfo = {} + groupInfo['group'] = tmp[0] + groupInfo['gid'] = tmp[2] + groupList.append(groupInfo) + return public.returnResult(code=1, data=groupList, msg='获取用户组列表成功!', status=True) + + # --------------------获取用户列表 End---------------------- + + # --------------------删除用户 Start------------------------ + def remove_user(self, user: str) -> Dict[str, Any]: + users = ['www', 'root', 'mysql', 'shutdown', 'postfix', 'smmsp', 'sshd', 'systemd-network', 'systemd-bus-proxy', + 'avahi-autoipd', 'mail', 'sync', 'lp', 'adm', 'bin', 'mailnull', 'ntp', 'daemon', 'sys'] + if user in users: return public.returnResult(code=0, msg='系统用户或关键用户不能删除!', status=False) + r = public.ExecShell("userdel " + user) + if r[1].find('process') != -1: + try: + pid = r[1].split()[-1] + p = psutil.Process(int(pid)) + pname = p.name() + p.kill() + public.ExecShell("pkill -9 " + pname) + r = public.ExecShell("userdel " + user) + public.ExecShell("rm -rf /home/" + user) + except: + pass + if r[1].find('userdel:') != -1: return public.returnMsg(False, r[1]) + return public.returnResult(code=1, msg='删除成功!', status=True) + + # --------------------删除用户 End------------------------ + + # --------------------添加用户 Start------------------------ + def add_user(self, user: str, pwd: str, group: str) -> Dict[str, Any]: + try: + if not user: return public.returnResult(code=0, msg='用户名不能为空!', status=False) + if not pwd: return public.returnResult(code=0, msg='密码不能为空!', status=False) + if not self._check_user(user): return public.returnResult(code=1, msg='用户已存在!', status=True) + if not self._check_group(group): self.add_group(group) + r = public.ExecShell("useradd -g " + group + " -m " + user + ' -p' + pwd) + if r[1].find('useradd:') != -1 and r[1].find('already exists') == 1: return public.returnResult(code=0, msg=r[1], status=False) + # public.ExecShell("echo \"" + user + ":" + pwd + "\" | chpasswd") + return public.returnResult(code=1, msg='添加成功!', status=True) + except: + print(traceback.format_exc()) + return public.returnResult(code=0, msg='添加失败!', status=False) + + def _check_user(self, user: str) -> bool: + """ + 检查用户是否存在 + :param user: 用户名 + :return: bool + """ + tmpList = public.readFile('/etc/passwd').split("\n") + for ul in tmpList: + tmp = ul.split(':') + if len(tmp) < 6: continue + if tmp[0] == user: return False + return True + + def _check_group(self, group: str) -> bool: + """ + 检查用户组是否存在 + :param group: 用户组 + :return: bool + """ + tmpList = public.readFile('/etc/group').split("\n") + for gl in tmpList: + tmp = gl.split(':') + if len(tmp) < 3: continue + if tmp[0] == group: return True + return False + + # --------------------添加用户 End------------------------ + + # --------------------修改用户密码 Start------------------------ + def edit_user_pwd(self, user: str, pwd: str) -> Dict[str, Any]: + try: + if not user: return public.returnResult(code=0, msg='用户名不能为空!', status=False) + if not pwd: return public.returnResult(code=0, msg='密码不能为空!', status=False) + if self._check_user(user): return public.returnResult(code=0, msg='用户不存在!', status=False) + public.ExecShell("echo \"" + user + ":" + pwd + "\" | chpasswd") + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------修改用户密码 End------------------------ + + # --------------------修改用户组 Start------------------------ + def edit_user_group(self, user: str, group: str) -> Dict[str, Any]: + try: + if not user: return public.returnMsg(False, '用户名不能为空!') + if not group: return public.returnMsg(False, '用户组不能为空!') + if self._check_user(user): return public.returnMsg(False, '用户不存在!') + if not self._check_group(group): self.add_group(group) + r = public.ExecShell("usermod -g " + group + " " + user) + if r[1].find('usermod:') != -1: return public.returnMsg(False, r[1]) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------修改用户组 End------------------------ + + # --------------------修改用户备注 Start------------------------ + def edit_user_ps(self, user: str, ps: str) -> Dict[str, Any]: + try: + if not user: return public.returnResult(code=0, msg='用户名不能为空!', status=False) + if self._check_user(user): return public.returnResult(code=0, msg='用户不存在!', status=False) + r = public.ExecShell("usermod -c " + ps + " " + user) + if r[1].find('usermod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------修改用户备注 End------------------------ + + # --------------------修改用户备注 Start------------------------ + def edit_user_status(self, user: str, status: int): + try: + if not user: return public.returnResult(code=0, msg='用户名不能为空!', status=False) + if self._check_user(user): return public.returnResult(code=0, msg='用户不存在!', status=False) + if int(status) == 1: + r = public.ExecShell("usermod -L " + user) + else: + r = public.ExecShell("usermod -U " + user) + if r[1].find('usermod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------修改用户备注 End------------------------ + + # --------------------修改用户登录Shell Start------------------------ + def edit_user_login_shell(self, user: str, login_shell: str) -> Dict[str, Any]: + try: + if not user: return public.returnResult(code=0, msg='用户名不能为空!', status=False) + if self._check_user(user): return public.returnResult(code=0, msg='用户不存在!', status=False) + r = public.ExecShell("usermod -s " + login_shell + " " + user) + if r[1].find('usermod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------修改用户登录Shell End------------------------ + + # --------------------修改用户家目录 Start------------------------ + def edit_user_home(self, user: str, home: str) -> Dict[str, Any]: + try: + if not user: return public.returnResult(code=0, msg='用户名不能为空!', status=False) + if self._check_user(user): return public.returnResult(code=0, msg='用户不存在!', status=False) + r = public.ExecShell("usermod -d " + home + " " + user) + if r[1].find('usermod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------修改用户家目录 End------------------------ + + # --------------------获取用户信息 Start------------------------ + def get_user_info(self, user: str) -> Dict[str, Any]: + try: + user = user.strip() + tmpList = public.readFile('/etc/passwd').split("\n") + userInfo = {} + for ul in tmpList: + tmp = ul.split(':') + if len(tmp) < 6: continue + if tmp[0] == user: + userInfo['username'] = tmp[0] + userInfo['uid'] = tmp[2] + userInfo['gid'] = tmp[3] + userInfo['group'] = self._get_group_name(tmp[3]) + userInfo['ps'] = self._get_user_ps(tmp[0], tmp[4]) + userInfo['home'] = tmp[5] + userInfo['login_shell'] = tmp[6] + break + return public.returnResult(code=1, data=userInfo, msg='获取用户信息成功!', status=True) + except: + print(traceback.format_exc()) + return public.returnResult(code=0, msg='获取用户信息失败!', status=False) + + # --------------------添加用户组 Start------------------------ + def add_group(self, group: str) -> Dict[str, Any]: + """ + 添加用户组 + :param group: 用户组 + :return: dict + """ + try: + if not group: return public.returnResult(code=0, msg='用户组不能为空!', status=False) + if self._check_group(group): return public.returnResult(code=0, msg='用户组已存在!', status=False) + r = public.ExecShell("groupadd " + group) + if r[1].find('groupadd:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='添加成功!', status=True) + except: + return public.returnResult(code=0, msg='添加失败!', status=False) + + # --------------------添加用户组 End------------------------ + + # --------------------删除用户组 Start------------------------ + def remove_group(self, group: str) -> Dict[str, Any]: + """ + 删除用户组 + :param group: 用户组 + :return: dict + """ + try: + if not group: return public.returnResult(code=0, msg='用户组不能为空!', status=False) + if not self._check_group(group): return public.returnResult(code=0, msg='用户组不存在!', status=False) + r = public.ExecShell("groupdel " + group) + if r[1].find('groupdel:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='删除成功!', status=True) + except: + return public.returnResult(code=0, msg='删除失败!', status=False) + + # --------------------删除用户组 End------------------------ + + # --------------------修改用户组名称 Start------------------------ + def edit_group_name(self, group: str, new_group: str) -> Dict[str, Any]: + """ + 修改用户组名称 + :param group: 用户组 + :param new_group: 新用户组 + :return: dict + """ + try: + if not group: return public.returnResult(code=0, msg='用户组不能为空!', status=False) + if not new_group: return public.returnResult(code=0, msg='新用户组不能为空!', status=False) + if not self._check_group(group): return public.returnResult(code=0, msg='用户组不存在!', status=False) + if self._check_group(new_group): return public.returnResult(code=0, msg='新用户组已存在!', status=False) + r = public.ExecShell("groupmod -n " + new_group + " " + group) + if r[1].find('groupmod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------获取用户组列表 End------------------------ + + # --------------------获取用户组信息 Start------------------------ + def get_group_info(self, group) -> Dict[str, Any]: + """ + 获取用户组信息 + :param group: 用户组 + :return: dict + """ + try: + group = group.strip() + tmpList = public.readFile('/etc/group').split("\n") + groupInfo = {} + for gl in tmpList: + tmp = gl.split(':') + if len(tmp) < 3: continue + if tmp[0] == group: + groupInfo['group'] = tmp[0] + groupInfo['gid'] = tmp[2] + break + return public.returnResult(code=1, data=groupInfo, msg='获取用户组信息成功!', status=True) + except: + return public.returnResult(code=0, msg='获取用户组信息失败!', status=False) + + # --------------------获取用户组信息 End------------------------ + + # --------------------获取用户组信息 Start------------------------ + def get_group_user(self, group: str) -> Dict[str, Any]: + """ + 获取用户组用户 + :param group: 用户组 + :return: dict + """ + try: + group = group.strip() + tmpList = self.get_user_list()['data'] + userList = [] + for ul in tmpList: + if ul['group'] == group: + userList.append(ul['username']) + return public.returnResult(code=1, data=userList, msg='获取用户组用户成功!', status=True) + except: + return public.returnResult(code=0, msg='获取用户组用户失败!', status=False) + + # --------------------获取用户组信息 End------------------------ + + # --------------------获取用户组信息 Start------------------------ + def get_user_group(self, user: str) -> Dict[str, Any]: + """ + 获取用户组用户 + :param user: 用户 + :return: dict + """ + try: + user = user.strip() + tmpList = self.get_user_list()['data'] + groupList = [] + for gl in tmpList: + if gl['username'] == user: + groupList.append(gl['group']) + return public.returnResult(code=1, data=groupList, msg='获取用户组用户成功!', status=True) + except: + return public.returnResult(code=0, msg='获取用户组用户失败!', status=False) + + # --------------------获取用户组信息 End------------------------ + + # --------------------修改用户权限 Start------------------------ + def edit_user_permission(self, user: str, permission: str) -> Dict[str, Any]: + """ + 修改用户权限 + :param user: + :param permission: + :return: + """ + try: + if not user: return public.returnResult(code=0, msg='用户名不能为空!', status=False) + if self._check_user(user): return public.returnResult(code=0, msg='用户不存在!', status=False) + r = public.ExecShell("chmod -R " + permission + " /home/" + user) + if r[1].find('chmod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + # --------------------修改用户权限 End------------------------ + + # --------------------修改用户组权限 Start------------------------ + def edit_group_permission(self, group: str, permission: str) -> Dict[str, Any]: + """ + 修改用户组权限 + :param group: + :param permission: + :return: + """ + try: + if not group: return public.returnResult(code=0, msg='用户组不能为空!', status=False) + if not self._check_group(group): return public.returnResult(code=0, msg='用户组不存在!', status=False) + r = public.ExecShell("chmod -R " + permission + " /home/" + group) + if r[1].find('chmod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + def edit_user_name(self, user: str, new_user: str) -> Dict[str, Any]: + try: + user = user.strip() + new_user = new_user.strip() + r = public.ExecShell("usermod -l " + new_user + " " + user) + if r[1].find('usermod:') != -1: return public.returnResult(code=0, msg=r[1], status=False) + return public.returnResult(code=1, msg='修改成功!', status=True) + except: + return public.returnResult(code=0, msg='修改失败!', status=False) + + +class User(object): + def __init__(self): + self.real_user = RealUser() + + # 获取用户列表 + def get_user_list(self, get): + search = '' + if hasattr(get, 'search'): + search = get.search + return self.real_user.get_user_list(search) + + # 删除用户 + def remove_user(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户不存在!') + user = get.user.strip() + return self.real_user.remove_user(user) + + # 添加用户 + def add_user(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'pwd'): + return public.returnMsg(False, '密码不能为空!') + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + user = get.user.strip() + pwd = get.pwd.strip() + group = get.group.strip() + return self.real_user.add_user(user, pwd, group) + + # 修改用户密码 + def edit_user_pwd(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'pwd'): + return public.returnMsg(False, '密码不能为空!') + user = get.user.strip() + pwd = get.pwd.strip() + return self.real_user.edit_user(user, pwd) + + # 修改用户的用户组 + def edit_user_group(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + user = get.user.strip() + group = get.group.strip() + return self.real_user.edit_group(user, group) + + # 修改用户备注 + def edit_user_ps(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + user = get.user.strip() + return self.real_user.edit_user_ps(user) + + # 添加用户组 + def add_group(self, get): + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + group = get.group.strip() + return self.real_user.add_group(group) + + # 删除用户组 + def remove_group(self, get): + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + group = get.group.strip() + return self.real_user.remove_group(group) + + # 修改用户组名称 + def edit_group_name(self, get): + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + if not hasattr(get, 'new_group'): + return public.returnMsg(False, '新用户组不能为空!') + group = get.group.strip() + new_group = get.new_group.strip() + return self.real_user.edit_group_name(group, new_group) + + # 获取用户组列表 + def get_group_list(self, get): + return self.real_user.get_group_list() + + # 获取用户组信息 + def get_group_info(self, get): + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + group = get.group.strip() + return self.real_user.get_group_info(group) + + # 获取用户组用户 + def get_group_user(self, get): + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + group = get.group.strip() + return self.real_user.get_group_user(group) + + # 获取用户组用户 + def get_user_group(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户不能为空!') + user = get.user.strip() + return self.real_user.get_user_group(user) + + # 修改用户备注 + def edit_ps(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'ps'): + return public.returnMsg(False, '备注不能为空!') + user = get.user.strip() + ps = get.ps.strip() + return self.real_user.edit_ps(user, ps) + + # 修改用户登录Shell + def edit_user_login_shell(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'login_shell'): + return public.returnMsg(False, '登录Shell不能为空!') + user = get.user.strip() + login_shell = get.login_shell.strip() + return self.real_user.edit_login_shell(user, login_shell) + + # 修改用户家目录 + def edit_user_home(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'home'): + return public.returnMsg(False, '家目录不能为空!') + user = get.user.strip() + home = get.home.strip() + return self.real_user.edit_home(user, home) + + # 修改用户权限 + def edit_user_permission(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'permission'): + return public.returnMsg(False, '权限不能为空!') + user = get.user.strip() + permission = get.permission.strip() + return self.real_user.edit_user_permission(user, permission) + + # 修改用户组权限 + def edit_group_permission(self, get): + if not hasattr(get, 'group'): + return public.returnMsg(False, '用户组不能为空!') + if not hasattr(get, 'permission'): + return public.returnMsg(False, '权限不能为空!') + group = get.group.strip() + permission = get.permission.strip() + return self.real_user.edit_group_permission(group, permission) + + def edit_user_name(self, get): + if not hasattr(get, 'user'): + return public.returnMsg(False, '用户名不能为空!') + if not hasattr(get, 'new_user'): + return public.returnMsg(False, '新用户名不能为空!') + user = get.user.strip() + new_user = get.new_user.strip() + return self.real_user.edit_user_name(user, new_user) + + +if __name__ == "__main__": + user = User() + print(user.get_user_list(public.to_dict_obj({}))) diff --git a/mod/base/process/user_readme.text b/mod/base/process/user_readme.text new file mode 100644 index 00000000..bf028237 --- /dev/null +++ b/mod/base/process/user_readme.text @@ -0,0 +1,86 @@ +User类: + 返回值默认类型: + Dict[str, Any] + { + 'status': bool, + 'msg': str, + 'data': Any + } + def get_user_list(self, search: str = '') -> Dict[str, Union[bool, str, List[Dict[str, Any]]]]: + # 获取用户列表 + # 传参:search(可选参数,搜索关键词) + + def _get_user_ps(self, name: str, ps: str) -> str: + # 获取用户备注 + # 传参:name(用户名),ps(备注) + + def _get_group_name(self, gid: str) -> str: + # 获取用户组名称 + # 传参:gid(用户组ID) + + def _search_user(self, data: List[Dict[str, Any]], search: str) -> List[Dict[str, Union[str, Any]]]: + # 搜索用户 + # 传参:data(用户列表),search(搜索关键词) + + def _get_group_list(self) -> List[Dict[str, Union[str, str]]]: + # 获取用户组列表 + + def remove_user(self, user: str) -> Dict[str, Any]: + # 删除用户 + # 传参:user(用户名) + + def add_user(self, user: str, pwd: str, group: str) -> Dict[str, Any]: + # 添加用户 + # 传参:user(用户名),pwd(密码),group(用户组) + + def edit_user(self, user: str, pwd: str) -> Dict[str, Any]: + # 修改用户密码 + # 传参:user(用户名),pwd(新密码) + + def edit_group(self, user: str, group: str) -> Dict[str, Any]: + # 修改用户组 + # 传参:user(用户名),group(新用户组) + + def edit_ps(self, user: str, ps: str) -> Dict[str, Any]: + # 修改用户备注 + # 传参:user(用户名),ps(新备注) + + def edit_login_shell(self, user: str, login_shell: str) -> Dict[str, Any]: + # 修改用户登录Shell + # 传参:user(用户名),login_shell(新Shell) + + def edit_home(self, user: str, home: str) -> Dict[str, Any]: + # 修改用户家目录 + # 传参:user(用户名),home(新家目录) + + def get_user_info(self, user: str) -> Dict[str, Any]: + # 获取用户信息 + # 传参:user(用户名) + + def add_group(self, group: str) -> Dict[str, Any]: + # 添加用户组 + # 传参:group(用户组) + + def remove_group(self, group: str) -> Dict[str, Any]: + # 删除用户组 + # 传参:group(用户组) + + def edit_group_name(self, group: str, new_group: str) -> Dict[str, Any]: + # 修改用户组名称 + # 传参:group(用户组),new_group(新用户组) + + def get_group_list(self) -> Dict[str, Union[bool, str, List[Dict[str, Any]]]]: + # 获取用户组列表 + + def get_group_info(self, group) -> Dict[str, Any]: + # 获取用户组信息 + # 传参:group(用户组) + + def get_group_user(self, group: str) -> Dict[str, Any]: + # 获取用户组用户 + # 传参:group(用户组) + + def get_user_group(self, user: str) -> Dict[str, Any]: + # 获取用户组用户 + # 传参:user(用户) + diff --git a/mod/base/push_mod/__init__.py b/mod/base/push_mod/__init__.py new file mode 100644 index 00000000..1aa6df3f --- /dev/null +++ b/mod/base/push_mod/__init__.py @@ -0,0 +1,495 @@ +import json +import os +from typing import Dict, Union + +from .mods import TaskConfig, TaskTemplateConfig, TaskRecordConfig, SenderConfig, load_task_template_by_config, \ + load_task_template_by_file, UPDATE_MOD_PUSH_FILE, UPDATE_VERSION_FILE, PUSH_DATA_PATH +from .base_task import BaseTask +from .send_tool import WxAccountMsg, WxAccountLoginMsg, WxAccountMsgBase +from .system import PushSystem, get_push_public_data, push_by_task_keyword, push_by_task_id +from .manager import PushManager +from .util import read_file, write_file + + +__all__ = [ + "TaskConfig", + "TaskTemplateConfig", + "TaskRecordConfig", + "SenderConfig", + "load_task_template_by_config", + "load_task_template_by_file", + "BaseTask", + "WxAccountMsg", + "WxAccountLoginMsg", + "WxAccountMsgBase", + "PushSystem", + "get_push_public_data", + "PushManager", + "push_by_task_keyword", + "push_by_task_id", + "UPDATE_MOD_PUSH_FILE", + "update_mod_push_system", + "UPDATE_VERSION_FILE", + "PUSH_DATA_PATH", + "get_default_module_dict", +] + + +def update_mod_push_system(): + if os.path.exists(UPDATE_MOD_PUSH_FILE): + return + + # 只将已有的告警任务("site_push", "system_push", "database_push") 移动 + + try: + push_data = json.loads(read_file("/www/server/panel/class/push/push.json")) + except: + return + + if not isinstance(push_data, dict): + return + pmgr = PushManager() + default_module_dict = get_default_module_dict() + for key, value in push_data.items(): + if key == "site_push": + _update_site_push(value, pmgr, default_module_dict) + elif key == "system_push": + _update_system_push(value, pmgr, default_module_dict) + elif key == "database_push": + _update_database_push(value, pmgr, default_module_dict) + elif key == "rsync_push": + _update_rsync_push(value, pmgr, default_module_dict) + elif key == "load_balance_push": + _update_load_push(value, pmgr, default_module_dict) + elif key == "task_manager_push": + _update_task_manager_push(value, pmgr, default_module_dict) + + write_file(UPDATE_MOD_PUSH_FILE, "") + + +def get_default_module_dict(): + res = {} + wx_account_list = [] + for data in SenderConfig().config: + if not data["used"]: + continue + if data.get("original", False): + res[data["sender_type"]] = data["id"] + + if data["sender_type"] == "webhook": + res[data["data"].get("title")] = data["id"] + + if data["sender_type"] == "wx_account": + wx_account_list.append(data) + + wx_account_list.sort(key=lambda x: x.get("data", {}).get("create_time", "")) + if wx_account_list: + res["wx_account"] = wx_account_list[0]["id"] + + return res + + +def _update_site_push(old_data: Dict[str, Dict[str, Union[str, int, float, list]]], + pmgr: PushManager, + df_mdl: Dict[str, str]): + + for k, v in old_data.items(): + sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl] + if v["type"] == "ssl": + push_data = { + "template_id": "1", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "project": v.get("project", "all"), + "cycle": v.get("cycle", 15) + }, + "number_rule": { + "total": v.get("push_count", 1) + } + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "site_endtime": + push_data = { + "template_id": "2", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "cycle": v.get("cycle", 7) + }, + "number_rule": { + "total": v.get("push_count", 1) + } + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "panel_pwd_endtime": + push_data = { + "template_id": "3", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "cycle": v.get("cycle", 15), + "interval": 600 + }, + "number_rule": { + "total": v.get("push_count", 1) + } + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "ssh_login_error": + push_data = { + "template_id": "4", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "cycle": v.get("cycle", 30), + "count": v.get("count", 3), + "interval": v.get("interval", 600) + }, + "number_rule": { + "day_num": v.get("day_limit", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "services": + push_data = { + "template_id": "5", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "project": v.get("project", "nginx"), + "count": v.get("count", 3), + "interval": v.get("interval", 600) + }, + "number_rule": { + "day_num": v.get("day_limit", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "panel_safe_push": + push_data = { + "template_id": "6", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": {}, + "number_rule": { + "day_num": v.get("day_limit", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "ssh_login": + push_data = { + "template_id": "7", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": {}, + "number_rule": {} + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "panel_login": + push_data = { + "template_id": "8", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": {}, + "number_rule": {} + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "project_status": + push_data = { + "template_id": "9", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "cycle": v.get("cycle", 1), + "project": v.get("project", 0), + "count": v.get("count", 2) if v.get("count", 2) not in (1, 2) else 2, + "interval": v.get("interval", 600) + }, + "number_rule": { + "day_num": v.get("push_count", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "panel_update": + push_data = { + "template_id": "10", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": {}, + "number_rule": { + "day_num": 1 + } + } + } + pmgr.set_task_conf_data(push_data) + + send_type = None + login_send_type_conf = "/www/server/panel/data/panel_login_send.pl" + if os.path.exists(login_send_type_conf): + send_type = read_file(login_send_type_conf).strip() + else: + # 兼容之前的 + if os.path.exists("/www/server/panel/data/login_send_type.pl"): + send_type = read_file("/www/server/panel/data/login_send_type.pl") + else: + if os.path.exists('/www/server/panel/data/login_send_mail.pl'): + send_type = "mail" + if os.path.exists('/www/server/panel/data/login_send_dingding.pl'): + send_type = "dingding" + + if isinstance(send_type, str): + sender_list = [df_mdl[i.strip()] for i in send_type.split(",") if i.strip() in df_mdl] + push_data = { + "template_id": "8", + "task_data": { + "status": True, + "sender": sender_list, + "task_data": {}, + "number_rule": {} + } + } + pmgr.set_task_conf_data(push_data) + + login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl" + if os.path.exists(login_send_type_conf): + ssh_send_type = read_file(login_send_type_conf).strip() + if isinstance(ssh_send_type, str): + sender_list = [df_mdl[i.strip()] for i in ssh_send_type.split(",") if i.strip() in df_mdl] + push_data = { + "template_id": "7", + "task_data": { + "status": True, + "sender": sender_list, + "task_data": {}, + "number_rule": {} + } + } + pmgr.set_task_conf_data(push_data) + return + + +def _update_system_push(old_data: Dict[str, Dict[str, Union[str, int, float, list]]], + pmgr: PushManager, + df_mdl: Dict[str, str]): + + for k, v in old_data.items(): + sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl] + if v["type"] == "disk": + push_data = { + "template_id": "20", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "project": v.get("project", "/"), + "cycle": v.get("cycle", 2) if v.get("cycle", 2) not in (1, 2) else 2, + "count": v.get("count", 80), + }, + "number_rule": { + "total": v.get("push_count", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + if v["type"] == "disk": + push_data = { + "template_id": "21", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "cycle": v.get("cycle", 5) if v.get("cycle", 5) not in (3, 5, 15) else 5, + "count": v.get("count", 80), + }, + "number_rule": { + "total": v.get("push_count", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + if v["type"] == "load": + push_data = { + "template_id": "22", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "cycle": v.get("cycle", 5) if v.get("cycle", 5) not in (1, 5, 15) else 5, + "count": v.get("count", 80), + }, + "number_rule": { + "total": v.get("push_count", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + if v["type"] == "mem": + push_data = { + "template_id": "23", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "cycle": v.get("cycle", 5) if v.get("cycle", 5) not in (3, 5, 15) else 5, + "count": v.get("count", 80), + }, + "number_rule": { + "total": v.get("push_count", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + return + + +def _update_database_push(old_data: Dict[str, Dict[str, Union[str, int, float, list]]], + pmgr: PushManager, + df_mdl: Dict[str, str]): + + for k, v in old_data.items(): + sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl] + if v["type"] == "mysql_pwd_endtime": + push_data = { + "template_id": "30", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "project": v.get("project", []), + "cycle": v.get("cycle", 15), + }, + "number_rule": {} + } + } + pmgr.set_task_conf_data(push_data) + + elif v["type"] == "mysql_replicate_status": + push_data = { + "template_id": "31", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "project": v.get("project", []), + "count": v.get("cycle", 15), + "interval": v.get("interval", 600) + }, + "number_rule": {} + } + } + pmgr.set_task_conf_data(push_data) + + return None + + +def _update_rsync_push( + old_data: Dict[str, Dict[str, Union[str, int, float, list]]], + pmgr: PushManager, + df_mdl: Dict[str, str]): + + for k, v in old_data.items(): + sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl] + push_data = { + "template_id": "40", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "interval": v.get("interval", 600) + }, + "number_rule": { + "day_num": v.get("push_count", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + + +def _update_load_push( + old_data: Dict[str, Dict[str, Union[str, int, float, list]]], + pmgr: PushManager, + df_mdl: Dict[str, str]): + + for k, v in old_data.items(): + sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl] + push_data = { + "template_id": "50", + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "project": v.get("project", ""), + "cycle": v.get("cycle", "200|301|302|403|404") + }, + "number_rule": { + "day_num": v.get("push_count", 2) + } + } + } + + pmgr.set_task_conf_data(push_data) + + +def _update_task_manager_push( + old_data: Dict[str, Dict[str, Union[str, int, float, list]]], + pmgr: PushManager, + df_mdl: Dict[str, str]): + + for k, v in old_data.items(): + sender_list = [df_mdl[i.strip()] for i in v.get("module", "").split(",") if i.strip() in df_mdl] + template_id_dict = { + "task_manager_cpu": "60", + "task_manager_mem": "61", + "task_manager_process": "62" + } + if v["type"] in template_id_dict: + push_data = { + "template_id": template_id_dict[v["type"]], + "task_data": { + "status": bool(v.get("status", True)), + "sender": sender_list, + "task_data": { + "project": v.get("project", ""), + "count": v.get("count", 80), + "interval": v.get("count", 600), + }, + "number_rule": { + "day_num": v.get("push_count", 3) + } + } + } + pmgr.set_task_conf_data(push_data) + diff --git a/mod/base/push_mod/base_task.py b/mod/base/push_mod/base_task.py new file mode 100644 index 00000000..ca63e62e --- /dev/null +++ b/mod/base/push_mod/base_task.py @@ -0,0 +1,207 @@ +from typing import Union, Optional, List, Tuple +from .send_tool import WxAccountMsg + + +# 告警系统在处理每个任务时,都会重新建立有一个Task的对象,(请勿在__init__的初始化函数中添加任何参数) +# 故每个对象中都可以大胆存放本任务所有数据,不会影响同类型的其他任务 +class BaseTask: + + def __init__(self): + self.source_name: str = '' + self.title: str = '' # 这个是告警任务的标题(根据实际情况改变) + self.template_name: str = '' # 这个告警模板的标题(不会改变) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + """ + 检查设置的告警参数(是否合理) + @param task_data: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值) + @return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据, + 当检查有误时, 直接返回错误信息的字符串 + """ + raise NotImplementedError() + + def get_keyword(self, task_data: dict) -> str: + """ + 返回一个关键字,用于后续查询或执行任务时使用, 例如:防篡改告警,可以根据其规则id生成一个关键字, + 后续通过规则id和来源tamper 查询并使用 + @param task_data: 通过check_args后生成的告警参数字典 + @return: 返回一个关键词字符串 + """ + raise NotImplementedError() + + def get_title(self, task_data: dict) -> str: + """ + 返回一个标题 + @param task_data: 通过check_args后生成的告警参数字典 + @return: 返回一个关键词字符串 + """ + if self.title: + return self.title + return self.template_name + + def task_run_end_hook(self, res: dict) -> None: + """ + 在告警系统中。执行完了任务后,会去掉用这个函数 + @type res: dict, 执行任务的结果 + @return: + """ + return + + def task_config_update_hook(self, task: dict) -> None: + """ + 在告警管理中。更新任务数据后,会去掉用这个函数 + @return: + """ + return + + def task_config_remove_hook(self, task: dict) -> None: + """ + 在告警管理中。移除这个任务后,会去掉用这个函数 + @return: + """ + return + + def task_config_create_hook(self, task: dict) -> None: + """ + 在告警管理中。新建这个任务后,会去掉用这个函数 + @return: + """ + return + + def check_time_rule(self, time_rule: dict) -> Union[dict, str]: + """ + 检查和修改设置的告警的时间控制参数是是否合理 + 可以添加参数 get_by_func 字段用于指定使用本类中的那个函数执行时间判断标准, 替换标准的时间规则判断功能 + ↑示例如本类中的: can_send_by_time_rule + @param time_rule: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值) + @return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据, + 当检查有误时, 直接返回错误信息的字符串 + """ + return time_rule + + def check_num_rule(self, num_rule: dict) -> Union[dict, str]: + """ + 检查和修改设置的告警的次数控制参数是是否合理 + 可以添加参数 get_by_func 字段用于指定使用本类中的那个函数执行次数判断标准, 替换标准的次数规则判断功能 + ↑示例如本类中的: can_send_by_num_rule + @param num_rule: 传入的告警参数,提前会经过默认值处理(即没有的字段添加默认值) + @return: 当检查无误时,返回一个 dict 当做后续的添加和修改的数据, + 当检查有误时, 直接返回错误信息的字符串 + """ + return num_rule + + def can_send_by_num_rule(self, task_id: str, task_data: dict, number_rule: dict, push_data: dict) -> Optional[str]: + """ + 这是一个通过函数判断是否能够发送告警的示例,并非每一个告警任务都需要有 + @param task_id: 任务id + @param task_data: 告警参数信息 + @param number_rule: 次数控制信息 + @param push_data: 本次要发送的告警信息的原文,应当为字典, 来自 get_push_data 函数的返回值 + @return: 返回None + """ + return None + + def can_send_by_time_rule(self, task_id: str, task_data: dict, time_rule: dict, push_data: dict) -> Optional[str]: + """ + 这是一个通过函数判断是否能够发送告警的示例,并非每一个告警任务都需要有 + @param task_id: 任务id + @param task_data: 告警参数信息 + @param time_rule: 时间控制信息 + @param push_data: 本次要发送的告警信息的原文,应当为字典, 来自 get_push_data 函数的返回值 + @return: + """ + return None + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + """ + 判断这个任务是否需要返送 + @param task_id: 任务id + @param task_data: 任务的告警参数 + @return: 如果触发了告警,返回一个dict的原文,作为告警信息,否则应当返回None表示未触发 + 返回之中应当包含一个 msg_list 的键(值为List[str]类型),将主要的信息返回 + 用于以下信息的自动序列化包含[dingding, feishu, mail, weixin, web_hook] + 短信和微信公众号由于长度问题,必须每个任务手动实现 + """ + raise NotImplementedError() + + def filter_template(self, template: dict) -> Optional[dict]: + """ + 过滤 和 更改模板中的信息, 返回空表是当前无法设置该任务 + @param template: 任务的模板信息 + @return: + """ + raise NotImplementedError() + + # push_public_data 公共的告警参数提取位置 + # 内容包含: + # ip 网络ip + # local_ip 本机ip + # time 时间日志的字符串 + # timestamp 当前的时间戳 + # server_name 服务器别名 + def to_dingding_msg(self, push_data: dict, push_public_data: dict) -> str: + print("dddddddddddddddddddddddddddddddddddddddddd") + msg_list = push_data.get('msg_list', None) + if msg_list is None: + raise ValueError("任务:{}的告警推送数据参数错误, 没有msg_list字段".format(self.title)) + print("dddddddddddddddddddddddddddddddddddddddddd") + return self.public_headers_msg(push_public_data,dingding=True) + "\n\n" + "\n\n".join(msg_list) + + def to_feishu_msg(self, push_data: dict, push_public_data: dict) -> str: + msg_list = push_data.get('msg_list', None) + if msg_list is None: + raise ValueError("任务:{}的告警推送数据参数错误, 没有msg_list字段".format(self.title)) + return self.public_headers_msg(push_public_data) + "\n\n" + "\n\n".join(msg_list) + + def to_mail_msg(self, push_data: dict, push_public_data: dict) -> str: + msg_list = push_data.get('msg_list', None) + if msg_list is None: + raise ValueError("任务:{}的告警推送数据参数错误, 没有msg_list字段".format(self.title)) + public_headers = self.public_headers_msg(push_public_data, "
                                    ") + return public_headers + "
                                    " + "
                                    ".join(msg_list) + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + """ + 返回 短信告警的类型和数据 + @param push_data: + @param push_public_data: + @return: 第一项是类型, 第二项是数据 + """ + raise NotImplementedError() + + def to_weixin_msg(self, push_data: dict, push_public_data: dict) -> str: + msg_list = push_data.get('msg_list', None) + if msg_list is None: + raise ValueError("任务:{}的告警推送数据参数错误, 没有msg_list字段".format(self.title)) + spc = "\n " + public_headers = self.public_headers_msg(push_public_data, "\n ") + return public_headers + spc + spc.join(msg_list) + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + raise NotImplementedError() + + def to_web_hook_msg(self, push_data: dict, push_public_data: dict) -> str: + msg_list = push_data.get('msg_list', None) + if msg_list is None: + raise ValueError("任务:{}的告警推送数据参数错误, 没有msg_list字段".format(self.title)) + public_headers = self.public_headers_msg(push_public_data, "\n") + return public_headers + "\n" + "\n".join(msg_list) + + def public_headers_msg(self, push_public_data: dict, spc: str = None,dingding=False) -> str: + if spc is None: + spc = "\n\n" + title = self.title + print(title) + if dingding: + print("dingdingtitle",title) + if "面板" not in title: + title += "面板" + print("dingdingtitle",title) + + print(title) + return spc.join([ + "#### {}".format(title), + ">服务器:" + push_public_data['server_name'], + ">IP地址:{}(外) {}(内)".format(push_public_data['ip'], push_public_data['local_ip']), + ">发送时间:" + push_public_data['time'] + ]) diff --git a/mod/base/push_mod/compatible.py b/mod/base/push_mod/compatible.py new file mode 100644 index 00000000..b459f918 --- /dev/null +++ b/mod/base/push_mod/compatible.py @@ -0,0 +1,27 @@ +import os +from .util import read_file, write_file + + +def rsync_compatible(): + files = [ + "/www/server/panel/class/push/rsync_push.py", + "/www/server/panel/plugin/rsync/rsync_push.py", + ] + for f in files: + print(f) + if not os.path.exists(f): + continue + src_data = read_file(f) + if src_data.find("push_rsync_by_task_name") != -1: + continue + src_data = src_data.replace("""if __name__ == "__main__": + rsync_push().main()""", """ +if __name__ == "__main__": + try: + sys.path.insert(0, "/www/server/panel") + from mod.base.push_mod.rsync_push import push_rsync_by_task_name + push_rsync_by_task_name(sys.argv[1]) + except: + rsync_push().main() +""") + write_file(f, src_data) diff --git a/mod/base/push_mod/database_push.py b/mod/base/push_mod/database_push.py new file mode 100644 index 00000000..5b3b728e --- /dev/null +++ b/mod/base/push_mod/database_push.py @@ -0,0 +1,239 @@ +import json +import os +import sys +import ipaddress +from datetime import datetime, timedelta +from typing import Tuple, Union, Optional + +from .send_tool import WxAccountMsg +from .base_task import BaseTask +from .util import read_file, DB, GET_CLASS + +try: + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + from panel_msg.collector import DatabasePushMsgCollect +except ImportError: + DatabasePushMsgCollect = None + + +def is_ipaddress(ip_data: str) -> bool: + try: + ipaddress.ip_address(ip_data) + except ValueError: + return False + return True + + +class MysqlPwdEndTimeTask(BaseTask): + + def __init__(self): + super().__init__() + self.template_name = "MySQL数据库密码到期" + self.source_name = "mysql_pwd_end" + + self.push_db_user = "" + + def get_title(self, task_data: dict) -> str: + return "Msql:" + task_data["project"][1] + "用户密码到期提醒" + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + task_data["interval"] = 600 + if not (isinstance(task_data["project"], list) and len(task_data["project"]) == 3): + return "设置的用户格式错误" + project = task_data["project"] + if not (isinstance(project[0], int) and isinstance(project[1], str) and is_ipaddress(project[2])): + return "设置的检测用户格式错误" + + if not (isinstance(task_data["cycle"], int) and task_data["cycle"] >= 1): + return "到期时间参数错误,至少为 1 天" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "_".join([str(i) for i in task_data["project"]]) + + def check_num_rule(self, num_rule: dict) -> Union[dict, str]: + num_rule["day_num"] = 1 + return num_rule + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + sid = task_data["project"][0] + username = task_data["project"][1] + host = task_data["project"][2] + + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + try: + import panelMysql + import db_mysql + except ImportError: + return None + + if sid == 0: + try: + db_port = int(panelMysql.panelMysql().query("show global variables like 'port'")[0][1]) + if db_port == 0: + db_port = 3306 + except: + db_port = 3306 + conn_config = { + "db_host": "localhost", + "db_port": db_port, + "db_user": "root", + "db_password": DB("config").where("id=?", (1,)).getField("mysql_root"), + "ps": "本地服务器", + } + else: + conn_config = DB("database_servers").where("id=? AND LOWER(db_type)=LOWER('mysql')", (sid,)).find() + if not conn_config: + return None + + mysql_obj = db_mysql.panelMysql().set_host(conn_config["db_host"], conn_config["db_port"], None, + conn_config["db_user"], conn_config["db_password"]) + if isinstance(mysql_obj, bool): + return None + + data_list = mysql_obj.query( + "SELECT password_last_changed FROM mysql.user WHERE user='{}' AND host='{}';".format(username, host)) + + if not isinstance(data_list, list) or not data_list: + return None + + try: + # todo:检查这里的时间转化逻辑问题 + last_time = data_list[0][0] + expire_time = last_time + timedelta(days=task_data["cycle"]) + except: + return None + + if datetime.now() > expire_time: + self.title = self.get_title(task_data) + self.push_db_user = username + return {"msg_list": [ + ">告警类型:MySQL密码即将到期", + ">告警内容:{} {}@{} 密码过期时间{} 天".format( + conn_config["ps"], username, host, expire_time.strftime("%Y-%m-%d %H:%M:%S")) + ]} + + def filter_template(self, template: dict) -> Optional[dict]: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return "", {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "MySQL数据库密码到期" + msg.msg = "Mysql用户:{}的密码即将过期,请注意".format(self.push_db_user) + msg.next_msg = "请登录面板,查看主机情况" + return msg + + +class MysqlReplicateStatusTask(BaseTask): + + def __init__(self): + super().__init__() + self.template_name = "MySQL主从复制异常告警" + self.source_name = "mysql_replicate_status" + self.title = "MySQL主从复制异常告警" + + self.slave_ip = '' + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not (isinstance(task_data["project"], str) and task_data["project"]): + return "请选择告警的从库!" + + if not (isinstance(task_data["count"], int) and task_data["count"] in (1, 2)): + return "是否自动修复选择错误!" + + if not (isinstance(task_data["interval"], int) and task_data["interval"] >= 60): + return "检查间隔时间错误,至少需要60s的间隔" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return task_data["project"] + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + import PluginLoader + + args = GET_CLASS() + args.slave_ip = task_data["project"] + res = PluginLoader.plugin_run("mysql_replicate", "get_replicate_status", args) + if res.get("status", False) is False: + return None + + self.slave_ip = task_data["project"] + if len(res.get("data", [])) == 0: + s_list = [">告警类型:MySQL主从复制异常告警", + ">告警内容:从库 {} 主从复制已停止,请尽快登录面板查看详情".format( + task_data["project"])] + return {"msg_list": s_list} + + sql_status = io_status = False + for item in res.get("data", []): + if item["name"] == "Slave_IO_Running" and item["value"] == "Yes": + io_status = True + if item["name"] == "Slave_SQL_Running" and item["value"] == "Yes": + sql_status = True + if io_status is True and sql_status is True: + break + + if io_status is False or sql_status is False: + repair_txt = "请尽快登录面板查看详情" + if task_data["count"] == 1: # 自动修复 + PluginLoader.plugin_run("mysql_replicate", "repair_replicate", args) + repair_txt = ",正在尝试修复" + + s_list = [">告警类型:MySQL主从复制异常告警", + ">告警内容:从库 {} 主从复制发生异常{}".format( + task_data["project"], repair_txt)] + return {"msg_list": s_list} + return None + + @staticmethod + def _get_mysql_replicate(): + slave_list = [] + mysql_replicate_path = os.path.join("/www/server/panel/plugin", "mysql_replicate", "config.json") + if os.path.isfile(mysql_replicate_path): + conf = read_file(mysql_replicate_path) + try: + conf = json.loads(conf) + slave_list = [{"title": slave_ip, "value": slave_ip} for slave_ip in conf["slave"].keys()] + except: + pass + return slave_list + + def filter_template(self, template: dict) -> Optional[dict]: + template["field"][0]["items"] = self._get_mysql_replicate() + if not template["field"][0]["items"]: + return None + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "MySQL主从复制异常告警" + msg.msg = "从库 {} 主从复制发生异常".format(self.slave_ip) + msg.next_msg = "请登录面板,在[软件商店-MySQL主从复制(重构版)]中查看" + return msg + + +class ViewMsgFormat(object): + _FORMAT = { + "30": ( + lambda x: "剩余时间小于{}天{}".format( + x["task_data"].get("cycle"), + ("(如未处理,次日会重新发送1次,持续%d天)" % x.get("number_rule", {}).get("day_num", 0)) if x.get( + "number_rule", {}).get("day_num", 0) else "" + + ) + ), + "31": (lambda x: "MySQL主从复制异常告警".format()), + } + + def get_msg(self, task: dict) -> Optional[str]: + if task["template_id"] in self._FORMAT: + return self._FORMAT[task["template_id"]](task) + return None diff --git a/mod/base/push_mod/database_push_template.json b/mod/base/push_mod/database_push_template.json new file mode 100644 index 00000000..0304a96f --- /dev/null +++ b/mod/base/push_mod/database_push_template.json @@ -0,0 +1,154 @@ +[ + { + "id": "30", + "ver": "1", + "used": true, + "source": "mysql_pwd_end", + "title": "MySQL数据库密码到期", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.database_push", + "name": "MysqlPwdEndTimeTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "选择用户", + "type": "cascader", + "default": 0, + "items": [ + { + "url": "database?action=GetPushUser" + }, + { + "url": "database?action=GetPushUser", + "data": [ + "sid" + ] + }, + { + "url": "database?action=GetPushUser", + "data": [ + "sid", + "username" + ] + } + ] + }, + { + "attr": "cycle", + "name": "剩余天数", + "type": "number", + "unit": "天", + "suffix": "", + "default": 15 + } + ], + "sorted": [ + [ + "project" + ], + [ + "cycle" + ] + ] + }, + "default": { + "project": [], + "cycle": 15 + }, + "advanced_default": { + "number_rule": { + "total": 2 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": false + }, + { + "id": "31", + "ver": "1", + "used": true, + "source": "mysql_replicate_status", + "title": "Msql主从同步告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.database_push", + "name": "MysqlReplicateStatusTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "选择监控的从库", + "type": "select", + "default": null, + "items": [] + }, + { + "attr": "count", + "name": "自动修复", + "type": "radio", + "suffix": "", + "default": 1, + "items": [ + { + "title": "自动尝试修复", + "value": 1 + }, + { + "title": "不做修复尝试", + "value": 2 + } + ] + }, + { + "attr": "interval", + "name": "间隔时间", + "type": "number", + "unit": "秒", + "suffix": "后再次监控检测条件", + "default": 600 + } + ], + "sorted": [ + [ + "project" + ], + [ + "count" + ], + [ + "interval" + ] + ] + }, + "default": { + "project": "", + "count": 2, + "interval": 600 + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": false + } +] \ No newline at end of file diff --git a/mod/base/push_mod/load_push.py b/mod/base/push_mod/load_push.py new file mode 100644 index 00000000..e3430983 --- /dev/null +++ b/mod/base/push_mod/load_push.py @@ -0,0 +1,271 @@ +import json +import os +import time +from typing import Tuple, Union, Optional + +from .mods import PUSH_DATA_PATH, TaskTemplateConfig +from .send_tool import WxAccountMsg +from .base_task import BaseTask +from .util import read_file, DB, GET_CLASS, write_file + + +class NginxLoadTask(BaseTask): + def __init__(self): + super().__init__() + self.source_name = "nginx_load_push" + self.template_name = "负载均衡告警" + self._tip_counter = None + + @property + def tip_counter(self) -> dict: + if self._tip_counter is not None: + return self._tip_counter + tip_counter = '{}/load_balance_push.json'.format(PUSH_DATA_PATH) + if os.path.exists(tip_counter): + try: + self._tip_counter = json.loads(read_file(tip_counter)) + except json.JSONDecodeError: + self._tip_counter = {} + else: + self._tip_counter = {} + return self._tip_counter + + def save_tip_counter(self): + tip_counter = '{}/load_balance_push.json'.format(PUSH_DATA_PATH) + write_file(tip_counter, json.dumps(self.tip_counter)) + + def get_title(self, task_data: dict) -> str: + if task_data["project"] == "all": + return "负载节点异常告警" + return "负载节点[{}]异常告警".format(task_data["project"]) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + all_upstream_name = DB("upstream").field("name").select() + if isinstance(all_upstream_name, str) and all_upstream_name.startswith("error"): + return '没有负载均衡配置,无法设置告警' + all_upstream_name = [i["name"] for i in all_upstream_name] + if not bool(all_upstream_name): + return '没有负载均衡配置,无法设置告警' + if task_data["project"] not in all_upstream_name and task_data["project"] != "all": + return '没有该负载均衡配置,无法设置告警' + + cycle = [] + for i in task_data["cycle"].split("|"): + if bool(i) and i.isdecimal(): + code = int(i) + if 100 <= code < 600: + cycle.append(str(code)) + if not bool(cycle): + return '没有指定任何错误码,无法设置告警' + + task_data["cycle"] = "|".join(cycle) + return task_data + + def get_keyword(self, task_data: dict) -> str: + return task_data["project"] + + def _check_func(self, upstream_name: str, codes: str) -> list: + import PluginLoader + get_obj = GET_CLASS() + get_obj.upstream_name = upstream_name + # 调用外部插件检查负载均衡的健康状况 + upstreams = PluginLoader.plugin_run("load_balance", "get_check_upstream", get_obj) + access_codes = [int(i) for i in codes.split("|") if bool(i.strip())] + res_list = [] + for upstream in upstreams: + # 检查每个节点,返回有问题的节点信息 + res = upstream.check_nodes(access_codes, return_nodes=True) + for ping_url in res: + if ping_url in self.tip_counter: + self.tip_counter[ping_url].append(int(time.time())) + idx = 0 + for i in self.tip_counter[ping_url]: + # 清理超过4分钟的记录 + if time.time() - i > 60 * 4: + idx += 1 + self.tip_counter[ping_url] = self.tip_counter[ping_url][idx:] + print("self.tip_counter[ping_url]",self.tip_counter[ping_url]) + # 如果一个节点连续三次出现在告警列表中,则视为需要告警 + if len(self.tip_counter[ping_url]) >= 3: + res_list.append(ping_url) + self.tip_counter[ping_url] = [] + else: + self.tip_counter[ping_url] = [int(time.time()), ] + self.save_tip_counter() + return res_list + + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + err_nodes = self._check_func(task_data["project"], task_data["cycle"]) + if not err_nodes: + return None + pj = "负载均衡:【{}】".format(task_data["project"]) if task_data["project"] != "all" else "负载均衡" + nodes = '、'.join(err_nodes) + return { + "msg_list": [ + ">通知类型:企业版负载均衡告警", + ">告警内容:{}配置下的节点【{}】出现访问错误,请及时关注节点情况并处理。 ".format( + pj, nodes), + ], + "pj": pj, + "nodes": nodes + } + + def filter_template(self, template: dict) -> Optional[dict]: + if not os.path.exists("/www/server/panel/plugin/load_balance/load_balance_main.py"): + return None + all_upstream = DB("upstream").field("name").select() + if isinstance(all_upstream, str) and all_upstream.startswith("error"): + return None + all_upstream_name = [i["name"] for i in all_upstream] + if not all_upstream_name: + return None + for name in all_upstream_name: + template["field"][0]["items"].append({ + "title": name, + "value": name + }) + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "负载均衡告警" + msg.msg = "负载均衡出现节点异常,请登录面板查看" + return msg + + def task_config_create_hook(self, task: dict) -> None: + old_config_file = "/www/server/panel/class/push/push.json" + try: + old_config = json.loads(read_file(old_config_file)) + except: + return + if "load_balance_push" not in old_config: + old_config["load_balance_push"] = {} + old_data = { + "push_count": task["number_rule"].get("day_num", 2), + "cycle": task["task_data"].get("cycle", "200|301|302|403|404"), + "interval": task["task_data"].get("interval", 60), + "title": task["title"], + "status": task['status'], + "module": ",".join(task["sender"]) + } + for k, v in old_config["load_balance_push"].items(): + if v["project"] == task["task_data"]["project"]: + v.update(old_data) + else: + old_data["project"] = task["task_data"]["project"] + old_config["load_balance_push"][int(time.time())] = old_data + + write_file(old_config_file, json.dumps(old_config)) + + def task_config_update_hook(self, task: dict) -> None: + return self.task_config_create_hook(task) + + def task_config_remove_hook(self, task: dict) -> None: + old_config_file = "/www/server/panel/class/push/push.json" + try: + old_config = json.loads(read_file(old_config_file)) + except: + return + if "load_balance_push" not in old_config: + old_config["load_balance_push"] = {} + old_config["load_balance_push"] = { + k: v for k, v in old_config["load_balance_push"].items() + if v["project"] != task["task_data"]["project"] + } + + +def load_load_template(): + if TaskTemplateConfig().get_by_id("50"): + return None + + from .mods import load_task_template_by_config + load_task_template_by_config( + [{ + "id": "50", + "ver": "1", + "used": True, + "source": "nginx_load_push", + "title": "负载均衡", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.load_push", + "name": "NginxLoadTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "负载名称", + "type": "select", + "default": "all", + "unit": "", + "suffix": ( + "*" + "选中的负载配置中,出现节点访问失败时,触发告警" + ), + "items": [ + { + "title": "所有已配置的负载", + "value": "all" + } + ] + }, + { + "attr": "cycle", + "name": "成功的状态码", + "type": "textarea", + "unit": "", + "suffix": ( + "
                                    *" + "状态码以竖线分隔,如:200|301|302|403|404" + ), + "width": "400px", + "style": { + 'height': '70px', + }, + "default": "200|301|302|403|404" + } + ], + "sorted": [ + [ + "project" + ], + [ + "cycle" + ] + ], + }, + "default": { + "project": "all", + "cycle": "200|301|302|403|404" + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": False + }] + ) + + +class ViewMsgFormat(object): + + @staticmethod + def get_msg(task: dict) -> Optional[str]: + if task["template_id"] == "50": + return "节点访问异常时,推送告警信息(每日推送{}次后不在推送)".format( + task.get("number_rule", {}).get("day_num")) + return None diff --git a/mod/base/push_mod/manager.py b/mod/base/push_mod/manager.py new file mode 100644 index 00000000..009edb87 --- /dev/null +++ b/mod/base/push_mod/manager.py @@ -0,0 +1,312 @@ +import json +import os +import time +from typing import Union, Optional + +from .mods import TaskTemplateConfig, TaskConfig, SenderConfig, TaskRecordConfig +from .system import PushSystem +from mod.base import json_response + + +class PushManager: + + def __init__(self): + self.template_conf = TaskTemplateConfig() + self.task_conf = TaskConfig() + self.send_config = SenderConfig() + self._send_conf_cache = {} + + def _get_sender_conf(self, sender_id): + if sender_id in self._send_conf_cache: + return self._send_conf_cache[sender_id] + tmp = self.send_config.get_by_id(sender_id) + self._send_conf_cache[sender_id] = tmp + return tmp + + def normalize_task_config(self, task, template) -> Union[dict, str]: + result = {} + sender = task.get("sender", None) + if sender is None: + return "未设置告警通道" + if not isinstance(sender, list): + return "告警通道设置错误" + + new_sender = [] + for i in sender: + sender_conf = self._get_sender_conf(i) + if not sender_conf: + continue + else: + new_sender.append(i) + if sender_conf["sender_type"] not in template["send_type_list"]: + if sender_conf["sender_type"] == "sms": + return "不支持短信告警" + return "不支持的告警方式:{}".format(sender_conf['data']["title"]) + if not sender_conf["used"]: + if sender_conf["sender_type"] == "sms": + return "短信告警通道已关闭" + return "已关闭的告警方式:{}".format(sender_conf['data']["title"]) + + result["sender"] = new_sender + + if "default" in template and template["default"]: + task_data = task.get("task_data", {}) + for k, v in template["default"].items(): + if k not in task_data: + task_data[k] = v + + result["task_data"] = task_data + + if "task_data" not in result: + result["task_data"] = {} + + time_rule = task.get("time_rule", {}) + + if "send_interval" in time_rule: + if not isinstance(time_rule["send_interval"], int): + return "最小间隔时间设置错误" + if time_rule["send_interval"] < 0: + return "最小间隔时间设置错误" + + if "time_range" in time_rule: + if not isinstance(time_rule["time_range"], list): + return "时间范围设置错误" + if not len(time_rule["time_range"]) == 2: + del time_rule["time_range"] + else: + time_range = time_rule["time_range"] + if not (isinstance(time_range[0], int) and isinstance(time_range[1], int) and + 0 <= time_range[0] < time_range[1] <= 60 * 60 * 24): + return "时间范围设置错误" + + result["time_rule"] = time_rule + + number_rule = task.get("number_rule", {}) + if "day_num" in number_rule: + if not (isinstance(number_rule["day_num"], int) and number_rule["day_num"] >= 0): + return "每日最小次数设置错误" + + if "total" in number_rule: + if not (isinstance(number_rule["total"], int) and number_rule["total"] >= 0): + return "最大告警次数设置错误" + + result["number_rule"] = number_rule + + if "status" not in task: + result["status"] = True + if "status" in task: + if isinstance(task["status"], bool): + result["status"] = task["status"] + + return result + + def set_task_conf_data(self, push_data: dict) -> Optional[str]: + task_id = push_data.get("task_id", None) + template_id = push_data.get("template_id") + task = push_data.get("task_data") + + target_task_conf = None + if task_id is not None: + tmp = self.task_conf.get_by_id(task_id) + if tmp is None: + target_task_conf = tmp + + template = self.template_conf.get_by_id(template_id) + if not template: + return "未查询到告警模板" + + if template["unique"] and not target_task_conf: + for i in self.task_conf.config: + if i["template_id"] == template["id"]: + target_task_conf = i + break + + task_obj = PushSystem().get_task_object(template_id, template["load_cls"]) + if not task_obj: + return "加载任务类型错误,您可以尝试修复面板" + + res = self.normalize_task_config(task, template) + if isinstance(res, str): + return res + + task_data = task_obj.check_task_data(res["task_data"]) + if isinstance(task_data, str): + return task_data + + number_rule = task_obj.check_num_rule(res["number_rule"]) + if isinstance(number_rule, str): + return number_rule + + time_rule = task_obj.check_time_rule(res["time_rule"]) + if isinstance(time_rule, str): + return time_rule + + res["task_data"] = task_data + res["number_rule"] = number_rule + res["time_rule"] = time_rule + + res["keyword"] = task_obj.get_keyword(task_data) + res["source"] = task_obj.source_name + res["title"] = task_obj.get_title(task_data) + + if not target_task_conf: + tmp = self.task_conf.get_by_keyword(res["source"], res["keyword"]) + if tmp: + target_task_conf = tmp + + if not target_task_conf: + res["id"] = self.task_conf.nwe_id() + res["template_id"] = template_id + res["status"] = True + res["pre_hook"] = {} + res["after_hook"] = {} + res["last_check"] = 0 + res["last_send"] = 0 + res["number_data"] = {} + res["create_time"] = time.time() + res["record_time"] = 0 + self.task_conf.config.append(res) + task_obj.task_config_create_hook(res) + else: + target_task_conf.update(res) + target_task_conf["last_check"] = 0 + target_task_conf["number_data"] = {} # 次数控制数据置空 + task_obj.task_config_update_hook(target_task_conf) + + self.task_conf.save_config() + + return None + + def set_task_conf(self, get): + task_id = None + try: + if hasattr(get, "task_id"): + task_id = get.task_id.strip() + if not task_id: + task_id = None + else: + self.remove_task_conf(get) + template_id = get.template_id.strip() + task = json.loads(get.task_data.strip()) + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数错误") + push_data = { + "task_id": task_id, + "template_id": template_id, + "task_data": task, + } + res = self.set_task_conf_data(push_data) + if res: + return json_response(status=False, msg=res) + # target_task_conf = None + # if task_id is not None: + # tmp = self.task_conf.get_by_id(task_id) + # if tmp is None: + # target_task_conf = tmp + # + # template = self.template_conf.get_by_id(template_id) + # if not template: + # return json_response(status=False, msg="为查询到告警模板") + # + # if template["unique"] and not target_task_conf: + # for i in self.task_conf.config: + # if i["template_id"] == template["id"]: + # target_task_conf = i + # break + # + # task_obj = PushSystem().get_task_object(template_id, template["load_cls"]) + # if not task_obj: + # return json_response(status=False, msg="加载任务类型错误,您可以尝试修复面板") + # + # res = self.normalize_task_config(task, template) + # if isinstance(res, str): + # return json_response(status=True, msg=res) + # + # task_data = task_obj.check_task_data(res["task_data"]) + # if isinstance(task_data, str): + # return json_response(status=True, msg=task_data) + # + # number_rule = task_obj.check_num_rule(res["number_rule"]) + # if isinstance(number_rule, str): + # return json_response(status=True, msg=number_rule) + # + # time_rule = task_obj.check_time_rule(res["time_rule"]) + # if isinstance(time_rule, str): + # return json_response(status=True, msg=time_rule) + # + # res["task_data"] = task_data + # res["number_rule"] = number_rule + # res["time_rule"] = time_rule + # + # res["keyword"] = task_obj.get_keyword(task_data) + # res["source"] = task_obj.source_name + # res["title"] = task_obj.get_title(task_data) + # + # if not target_task_conf: + # tmp = self.task_conf.get_by_keyword(res["source"], res["keyword"]) + # if tmp: + # target_task_conf = tmp + # + # if not target_task_conf: + # res["id"] = self.task_conf.nwe_id() + # res["template_id"] = template_id + # res["status"] = True + # res["pre_hook"] = {} + # res["after_hook"] = {} + # res["last_check"] = 0 + # res["last_send"] = 0 + # res["number_data"] = {} + # res["create_time"] = time.time() + # res["record_time"] = 0 + # self.task_conf.config.append(res) + # task_obj.task_config_create_hook(res) + # else: + # target_task_conf.update(res) + # target_task_conf["last_check"] = 0 + # target_task_conf["number_data"] = {} # 次数控制数据置空 + # task_obj.task_config_update_hook(target_task_conf) + # + # self.task_conf.save_config() + return json_response(status=True, msg="告警任务保存成功") + + def change_task_conf(self, get): + try: + task_id = get.task_id.strip() + except AttributeError: + return json_response(status=False, msg="参数错误") + + tmp = self.task_conf.get_by_id(task_id) + if tmp is None: + return json_response(status=True, msg="为查询到告警任务") + + tmp["status"] = not tmp["status"] + + self.task_conf.save_config() + return json_response(status=True, msg="操作成功") + + def remove_task_conf(self, get): + try: + task_id = get.task_id.strip() + except AttributeError: + return json_response(status=False, msg="参数错误") + + tmp = self.task_conf.get_by_id(task_id) + if tmp is None: + return json_response(status=True, msg="为查询到告警任务") + + self.task_conf.config.remove(tmp) + + self.task_conf.save_config() + template = self.template_conf.get_by_id(tmp["template_id"]) + if template: + task_obj = PushSystem().get_task_object(template["id"], template["load_cls"]) + if task_obj: + task_obj.task_config_remove_hook(tmp) + + return json_response(status=True, msg="操作成功") + + @staticmethod + def clear_task_record_by_task_id(task_id): + tr_conf = TaskRecordConfig(task_id) + if os.path.exists(tr_conf.config_file_path): + os.remove(tr_conf.config_file_path) diff --git a/mod/base/push_mod/mods.py b/mod/base/push_mod/mods.py new file mode 100644 index 00000000..ab7827c0 --- /dev/null +++ b/mod/base/push_mod/mods.py @@ -0,0 +1,368 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: baozi +# ------------------------------------------------------------------- +# 新告警的所有数据库操作 +# ------------------------------ +import json +import os +import types +from typing import Any, Dict, Optional, List +from threading import Lock +from uuid import uuid4 + +import fcntl + +from .util import Sqlite, write_log, read_file, write_file + +_push_db_lock = Lock() + + +# 代替 class/db.py 中, 离谱的两个query函数 +def msg_db_query_func(self, sql, param=()): + # 执行SQL语句返回数据集 + self._Sql__GetConn() + try: + return self._Sql__DB_CONN.execute(sql, self._Sql__to_tuple(param)) + except Exception as ex: + return "error: " + str(ex) + + +def get_push_db(): + db_file = "/www/server/panel/data/db/mod_push.db" + if not os.path.isdir(os.path.dirname(db_file)): + os.makedirs(os.path.dirname(db_file)) + + db = Sqlite() + setattr(db, "_Sql__DB_FILE", db_file) + setattr(db, "query", types.MethodType(msg_db_query_func, db)) + return db + + +def get_table(table_name: str): + db = get_push_db() + db.table = table_name + return db + + +def lock_push_db(): + with open("/www/server/panel/data/db/mod_push.db", mode="rb") as msg_fd: + fcntl.flock(msg_fd.fileno(), fcntl.LOCK_EX) + _push_db_lock.locked() + + +def unlock_push_db(): + with open("/www/server/panel/data/db/mod_push.db", mode="rb") as msg_fd: + fcntl.flock(msg_fd.fileno(), fcntl.LOCK_UN) + _push_db_lock.acquire() + + +def push_db_locker(func): + def inner_func(*args, **kwargs): + lock_push_db() + try: + res = func(*args, **kwargs) + except Exception as e: + unlock_push_db() # 即使 报错了 也先解锁再操作 + raise e + else: + unlock_push_db() + return res + + return inner_func + + +DB_INIT_ERROR = False + +PANEL_PATH = "/www/server/panel" +PUSH_DATA_PATH = "{}/data/mod_push_data".format(PANEL_PATH) +UPDATE_VERSION_FILE = "{}/update_panel.pl".format(PUSH_DATA_PATH) +UPDATE_MOD_PUSH_FILE = "{}/update_mod.pl".format(PUSH_DATA_PATH) + + +class BaseConfig: + config_file_path = "" + + def __init__(self): + if not os.path.exists(PUSH_DATA_PATH): + os.makedirs(PUSH_DATA_PATH) + self._config: Optional[List[Dict[str, Any]]] = None + + @property + def config(self) -> List[Dict[str, Any]]: + if self._config is None: + try: + self._config = json.loads(read_file(self.config_file_path)) + except: + self._config = [] + return self._config + + def save_config(self) -> None: + write_file(self.config_file_path, json.dumps(self.config)) + + @staticmethod + def nwe_id() -> str: + return uuid4().hex[::2] + + def get_by_id(self, target_id: str) -> Optional[Dict[str, Any]]: + for i in self.config: + if i.get("id", None) == target_id: + return i + + +class TaskTemplateConfig(BaseConfig): + config_file_path = "{}/task_template.json".format(PUSH_DATA_PATH) + + +class TaskConfig(BaseConfig): + config_file_path = "{}/task.json".format(PUSH_DATA_PATH) + + def get_by_keyword(self, source: str, keyword: str) -> Optional[Dict[str, Any]]: + for i in self.config: + if i.get("source", None) == source and i.get("keyword", None) == keyword: + return i + + +class TaskRecordConfig(BaseConfig): + config_file_path_fmt = "%s/task_record_{}.json" % PUSH_DATA_PATH + + def __init__(self, task_id: str): + super().__init__() + self.config_file_path = self.config_file_path_fmt.format(task_id) + + +class SenderConfig(BaseConfig): + config_file_path = "{}/sender.json".format(PUSH_DATA_PATH) + + def __init__(self): + super(SenderConfig, self).__init__() + if not os.path.exists(self.config_file_path): + write_file(self.config_file_path, json.dumps([{ + "id": self.nwe_id(), + "used": True, + "sender_type": "sms", + "data": {}, + "original": True + }])) + + +def init_db(): + global DB_INIT_ERROR + # id 模板id 必须唯一, 后端开发需要协商 + # ver 模板版本号, 用于更新 + # used 是否在使用用 + # source 来源, 如Waf, rsync + # title 标题 + # load_cls 要加载的类,或者从那种调用方法中获取到任务处理对象 + # template 给前端,用于展示的数据 + # default 默认数据,用于数据过滤, 和默认值 + # unique 是否仅可唯一设置 + # create_time 创建时间 + create_task_template_sql = ( + "CREATE TABLE IF NOT EXISTS 'task_template' (" + "'id' INTEGER PRIMARY KEY AUTOINCREMENT, " + "'ver' TEXT NOT NULL DEFAULT '1.0.0', " + "'used' INTEGER NOT NULL DEFAULT 1, " + "'source' TEXT NOT NULL DEFAULT 'site_push', " + "'title' TEXT NOT NULL DEFAULT '', " + "'load_cls' TEXT NOT NULL DEFAULT '{}', " + "'template' TEXT NOT NULL DEFAULT '{}', " + "'default' TEXT NOT NULL DEFAULT '{}', " + "'send_type_list' TEXT NOT NULL DEFAULT '[]', " + "'unique' INTEGER NOT NULL DEFAULT 0, " + "'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))" + ");" + ) + # source 来源, 例如waf(防火墙), rsync(文件同步) + # keyword 关键词, 不同的来源在使用中可以以此查出具体的任务,需要每个来源自己约束 + # task_data 任务数据字典,字段可以自由设计 + # sender 告警通道信息,为字典,可通过get_by_func字段指定从某个函数获取,用于发送 + # time_rule 告警的时间规则,包含 间隔时间(send_interval), (time-range) + # number_rule 告警的次数规则,包含 每日次数(day_num), 总次数(total), 通过函数判断(get_by_func) + # status 状态是否开启 + # pre_hook, after_hook 前置处理和后置处理 + # record_time, 告警记录存储时间, 默认为0, 认为长时间储存 + # last_check, 上次执行检查的时间 + # last_send, 上次次发送时间 + # number_data, 发送次数信息 + create_task_sql = ( + "CREATE TABLE IF NOT EXISTS 'task' (" + "'id' INTEGER PRIMARY KEY AUTOINCREMENT, " + "'template_id' INTEGER NOT NULL DEFAULT 0, " + "'source' TEXT NOT NULL DEFAULT '', " + "'keyword' TEXT NOT NULL DEFAULT '', " + "'title' TEXT NOT NULL DEFAULT '', " + "'task_data' TEXT NOT NULL DEFAULT '{}', " + "'sender' TEXT NOT NULL DEFAULT '[]', " + "'time_rule' TEXT NOT NULL DEFAULT '{}', " + "'number_rule' TEXT NOT NULL DEFAULT '{}', " + "'status' INTEGER NOT NULL DEFAULT 1, " + "'pre_hook' TEXT NOT NULL DEFAULT '{}', " + "'after_hook' TEXT NOT NULL DEFAULT '{}', " + "'last_check' INTEGER NOT NULL DEFAULT 0, " + "'last_send' INTEGER NOT NULL DEFAULT 0, " + "'number_data' TEXT NOT NULL DEFAULT '{}', " + "'create_time' INTEGER NOT NULL DEFAULT (strftime('%s')), " + "'record_time' INTEGER NOT NULL DEFAULT 0" + ");" + ) + + create_task_record_sql = ( + "CREATE TABLE IF NOT EXISTS 'task_record' (" + "'id' INTEGER PRIMARY KEY AUTOINCREMENT, " + "'template_id' INTEGER NOT NULL DEFAULT 0, " + "'task_id' INTEGER NOT NULL DEFAULT 0, " + "'do_send' TEXT NOT NULL DEFAULT '{}', " + "'send_data' TEXT NOT NULL DEFAULT '{}', " + "'result' TEXT NOT NULL DEFAULT '{}', " + "'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))" + ");" + ) + + create_send_record_sql = ( + "CREATE TABLE IF NOT EXISTS 'send_record' (" + "'id' INTEGER PRIMARY KEY AUTOINCREMENT, " + "'record_id' INTEGER NOT NULL DEFAULT 0, " + "'sender_name' TEXT NOT NULL DEFAULT '', " + "'sender_id' INTEGER NOT NULL DEFAULT 0, " + "'sender_type' TEXT NOT NULL DEFAULT '', " + "'send_data' TEXT NOT NULL DEFAULT '{}', " + "'result' TEXT NOT NULL DEFAULT '{}', " + "'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))" + ");" + ) + + create_sender_sql = ( + "CREATE TABLE IF NOT EXISTS 'sender' (" + "'id' INTEGER PRIMARY KEY AUTOINCREMENT, " + "'used' INTEGER NOT NULL DEFAULT 1, " + "'sender_type' TEXT NOT NULL DEFAULT '', " + "'name' TEXT NOT NULL DEFAULT '', " + "'data' TEXT NOT NULL DEFAULT '{}', " + "'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))" + ");" + ) + + lock_push_db() + with get_push_db() as db: + db.execute("pragma journal_mode=wal") + + res = db.execute(create_task_template_sql) + if isinstance(res, str) and res.startswith("error"): + write_log("告警系统", "task_template数据表创建错误:" + res) + DB_INIT_ERROR = True + return + + res = db.execute(create_task_sql) + if isinstance(res, str) and res.startswith("error"): + write_log("告警系统", "task数据表创建错误:" + res) + DB_INIT_ERROR = True + return + + res = db.execute(create_task_record_sql) + if isinstance(res, str) and res.startswith("error"): + write_log("告警系统", "task_recorde数据表创建错误:" + res) + DB_INIT_ERROR = True + return + + res = db.execute(create_send_record_sql) + if isinstance(res, str) and res.startswith("error"): + write_log("告警系统", "send_record数据表创建错误:" + res) + DB_INIT_ERROR = True + return + + res = db.execute(create_sender_sql) + if isinstance(res, str) and res.startswith("error"): + write_log("告警系统", "sender数据表创建错误:" + res) + DB_INIT_ERROR = True + return + + db.execute( + "INSERT INTO 'sender' (id, sender_type, data) VALUES (?,?,?)", + (1, 'sms', json.dumps({"count": 0, "total": 0})) + ) # 插入短信 + + unlock_push_db() + + init_template_file = "/www/server/panel/config/mod_push_init.json" + err = load_task_template_by_file(init_template_file) + if err: + write_log("告警系统", "task_template数据表初始数据加载失败:" + res) + + +def _check_fields(template: dict) -> bool: + if not isinstance(template, dict): + return False + + fields = ("id", "ver", "used", "source", "title", "load_cls", "template", "default", "unique", "create_time") + for field in fields: + if field not in template: + return False + return True + + +def load_task_template_by_config(templates: List[Dict]) -> None: + """ + 通过 传入的配置信息 执行一次模板更新操作 + @param templates: 模板内容,为一个数据列表 + @return: 报错信息,如果返回None则表示执行成功 + """ + + task_template_config = TaskTemplateConfig() + add_list = [] + for template in templates: + tmp = task_template_config.get_by_id(template['id']) + if tmp is not None: + tmp.update(template) + else: + add_list.append(template) + + task_template_config.config.extend(add_list) + task_template_config.save_config() + + # with get_table('task_template') as table: + # for template in templates: + # if not _check_fields(template): + # continue + # res = table.where("id = ?", (template['id'])).field('ver').select() + # if isinstance(res, str): + # return "数据库损坏:" + res + # if not res: # 没有就插入 + # table.insert(template) + # else: + # # 版本不一致就更新版本 + # if res['ver'] != template['ver']: + # template.pop("id") + # table.where("id = ?", (template['id'])).update(template) + # + + +def load_task_template_by_file(template_file: str) -> Optional[str]: + """ + 执行一次模板更新操作 + @param template_file: 模板文件路径 + @return: 报错信息,如果返回None则表示执行成功 + """ + if not os.path.isfile(template_file): + return "模板文件不存在,更新失败" + + if DB_INIT_ERROR: + return "数据库初始化时报错,无法更新" + + res = read_file(template_file) + if not isinstance(res, str): + return "数据读取失败" + + try: + templates = json.loads(res) + except (json.JSONDecoder, TypeError, ValueError): + return "仅支持JSON格式数据" + + if not isinstance(templates, list): + return "数据格式错误,应当为一个列表" + + return load_task_template_by_config(templates) diff --git a/mod/base/push_mod/rsync_push.py b/mod/base/push_mod/rsync_push.py new file mode 100644 index 00000000..7b07c24e --- /dev/null +++ b/mod/base/push_mod/rsync_push.py @@ -0,0 +1,301 @@ +import json +import os +import re +from datetime import datetime, timedelta +from typing import Tuple, Union, Optional, Iterator + +from .send_tool import WxAccountMsg +from .base_task import BaseTask +from .mods import TaskTemplateConfig +from .util import read_file + + +def rsync_ver_is_38() -> Optional[bool]: + """ + 检查rsync的版本是否为3.8。 + 该函数不接受任何参数。 + 返回值: + - None: 如果无法确定rsync的版本或文件不存在。 + - bool: 如果版本确定为3.8,则返回True;否则返回False。 + """ + push_file = "/www/server/panel/plugin/rsync/rsync_push.py" + if not os.path.exists(push_file): + return None + ver_info_file = "/www/server/panel/plugin/rsync/info.json" + if not os.path.exists(ver_info_file): + return None + try: + info = json.loads(read_file(ver_info_file)) + except (json.JSONDecodeError, TypeError): + return None + ver = info["versions"] + ver_tuples = [int(i) for i in ver.split(".")] + if len(ver_tuples) < 3: + ver_tuples = ver_tuples.extend([0] * (3 - len(ver_tuples))) + if ver_tuples[0] < 3: + return None + if ver_tuples[1] <= 8 and ver_tuples[0] == 3: + return True + + return False + + +class Rsync38Task(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "rsync_push" + self.template_name = "文件同步告警" + self.title = "文件同步告警" + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if "interval" not in task_data or not isinstance(task_data["interval"], int): + task_data["interval"] = 600 + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "rsync_push" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + has_err = self._check(task_data.get("interval", 600)) + if not has_err: + return None + + return { + "msg_list": [ + ">通知类型:文件同步告警", + ">告警内容:文件同步执行中出错了,请及时关注文件同步情况并处理。 ", + ] + } + + @staticmethod + def _check(interval: int) -> bool: + if not isinstance(interval, int): + return False + start_time = datetime.now() - timedelta(seconds=interval * 1.2) + log_file = "{}/plugin/rsync/lsyncd.log".format("/www/server/panel") + if not os.path.exists(log_file): + return False + return LogChecker(log_file=log_file, start_time=start_time)() + + def check_time_rule(self, time_rule: dict) -> Union[dict, str]: + if "send_interval" not in time_rule or not isinstance(time_rule["interval"], int): + time_rule["send_interval"] = 3 * 60 + if time_rule["send_interval"] < 60: + time_rule["send_interval"] = 60 + return time_rule + + def filter_template(self, template: dict) -> Optional[dict]: + res = rsync_ver_is_38() + if res is None: + return None + if res: + return template + else: + return None + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "文件同步告警" + msg.msg = "同步执行出错了,请及时关注同步情况" + return msg + + +class Rsync39Task(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "rsync_push" + self.template_name = "文件同步告警" + self.title = "文件同步告警" + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if "interval" not in task_data or not isinstance(task_data["interval"], int): + task_data["interval"] = 600 + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "rsync_push" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + """ + 不返回数据,以实时触发为主 + """ + return None + + def check_time_rule(self, time_rule: dict) -> Union[dict, str]: + if "send_interval" not in time_rule or not isinstance(time_rule["send_interval"], int): + time_rule["send_interval"] = 3 * 60 + if time_rule["send_interval"] < 60: + time_rule["send_interval"] = 60 + return time_rule + + def filter_template(self, template: dict) -> Optional[dict]: + res = rsync_ver_is_38() + if res is None: + return None + if res is False: + return template + else: + return None + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + task_name = push_data.get("task_name", None) + msg = WxAccountMsg.new_msg() + msg.thing_type = "文件同步告警" + if task_name: + msg.msg = "文件同步任务{}出错了".format(task_name) + else: + msg.msg = "同步执行出错了,请及时关注同步情况" + return msg + + +class LogChecker: + """ + 排序查询并获取日志内容 + """ + rep_time = re.compile(r'(?P(\w{3}\s+){2}(\d{1,2})\s+(\d{2}:?){3}\s+\d{4})') + format_str = '%a %b %d %H:%M:%S %Y' + err_datetime = datetime.fromtimestamp(0) + err_list = ("error", "Error", "ERROR", "exitcode = 10", "failed") + + def __init__(self, log_file: str, start_time: datetime): + self.log_file = log_file + self.start_time = start_time + self.is_over_time = None # None:还没查到时间,未知, False: 可以继续网上查询, True:比较早的数据了,不再向上查询 + self.has_err = False # 目前已查询的内容中是否有报错信息 + + def _format_time(self, log_line) -> Optional[datetime]: + try: + date_str_res = self.rep_time.search(log_line) + if date_str_res: + time_str = date_str_res.group("target") + return datetime.strptime(time_str, self.format_str) + except Exception: + return self.err_datetime + return None + + # 返回日志内容 + def __call__(self): + _buf = b"" + file_size, fp = os.stat(self.log_file).st_size - 1, open(self.log_file, mode="rb") + fp.seek(-1, 2) + while file_size: + read_size = min(1024, file_size) + fp.seek(-read_size, 1) + buf: bytes = fp.read(read_size) + _buf + fp.seek(-read_size, 1) + if file_size > 1024: + idx = buf.find(ord("\n")) + _buf, buf = buf[:idx], buf[idx + 1:] + for i in self._get_log_line_from_buf(buf): + self._check(i) + if self.is_over_time: + return self.has_err + file_size -= read_size + return False + + # 从缓冲中读取日志 + @staticmethod + def _get_log_line_from_buf(buf: bytes) -> Iterator[str]: + n, m = 0, 0 + buf_len = len(buf) - 1 + for i in range(buf_len, -1, -1): + if buf[i] == ord("\n"): + log_line = buf[buf_len + 1 - m: buf_len - n + 1].decode("utf-8") + yield log_line + n = m = m + 1 + else: + m += 1 + yield buf[0: buf_len - n + 1].decode("utf-8") + + # 格式化并筛选查询条件 + def _check(self, log_line: str) -> None: + # 筛选日期 + for err in self.err_list: + if err in log_line: + self.has_err = True + + ck_time = self._format_time(log_line) + if ck_time: + self.is_over_time = self.start_time > ck_time + + +def load_rsync_template(): + """ + 加载rsync模板 + """ + if TaskTemplateConfig().get_by_id("40"): + return None + from .mods import load_task_template_by_config + load_task_template_by_config( + [{ + "id": "40", + "ver": "1", + "used": True, + "source": "rsync_push", + "title": "文件同步告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.rsync_push", + "name": "RsyncTask" + }, + "template": { + "field": [ + ], + "sorted": [ + ] + }, + "default": { + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": True + }] + ) + + +RsyncTask = Rsync39Task +if rsync_ver_is_38() is True: + RsyncTask = Rsync38Task + + +def push_rsync_by_task_name(task_name: str): + from .system import push_by_task_keyword + + push_data = { + "task_name": task_name, + "msg_list": [ + ">通知类型:文件同步告警", + ">告警内容:文件同步任务{}在执行中出错了,请及时关注文件同步情况并处理。 ".format( + task_name), + ] + } + push_by_task_keyword("rsync_push", "rsync_push", push_data=push_data) + + +class ViewMsgFormat(object): + + @staticmethod + def get_msg(task: dict) -> Optional[str]: + if task["template_id"] == "40": + return "文件同步出现异常时,推送告警信息(每日推送{}次后不在推送)".format( + task.get("number_rule", {}).get("day_num")) + return None diff --git a/mod/base/push_mod/send_tool.py b/mod/base/push_mod/send_tool.py new file mode 100644 index 00000000..85fbeb44 --- /dev/null +++ b/mod/base/push_mod/send_tool.py @@ -0,0 +1,133 @@ +import ipaddress +import re + +from .util import get_config_value + + +class WxAccountMsgBase: + + @classmethod + def new_msg(cls): + return cls() + + def set_ip_address(self, server_ip, local_ip): + pass + + def to_send_data(self): + return "", {} + + +class WxAccountMsg(WxAccountMsgBase): + def __init__(self): + self.ip_address: str = "" + self.thing_type: str = "" + self.msg: str = "" + self.next_msg: str = "" + + def set_ip_address(self, server_ip, local_ip): + self.ip_address = "{}({})".format(server_ip, local_ip) + if len(self.ip_address) > 32: + self.ip_address = self.ip_address[:29] + "..." + + def to_send_data(self): + res = { + "first": {}, + "keyword1": { + "value": self.ip_address, + }, + "keyword2": { + "value": self.thing_type, + }, + "keyword3": { + "value": self.msg, + } + } + + if self.next_msg != "": + res["keyword4"] = {"value": self.next_msg} + + return "", res + + +class WxAccountLoginMsg(WxAccountMsgBase): + tid = "RJNG8dBZ5Tb9EK6j6gOlcAgGs2Fjn5Fb07vZIsYg1P4" + + def __init__(self): + self.login_name: str = "" + self.login_ip: str = "" + self.thing_type: str = "" + self.login_type: str = "" + self.address: str = "" + self._server_name: str = "" + + def set_ip_address(self, server_ip, local_ip): + if self._server_name == "": + self._server_name = "服务器IP{}".format(server_ip) + + def _get_server_name(self): + data = get_config_value("title") # 若获得别名,则使用别名 + if data != "": + self._server_name = data + + def to_send_data(self): + self._get_server_name() + if self.address.startswith(">归属地:"): + self.address = self.address[5:] + if self.address == "": + self.address = "未知的归属地" + + if not _is_ipv4(self.login_ip): + self.login_ip = "ipv6-can not show" + + res = { + "thing10": { + "value": self._server_name, + }, + "character_string9": { + "value": self.login_ip, + }, + "thing7": { + "value": self.login_type, + }, + "thing11": { + "value": self.address, + }, + "thing2": { + "value": self.login_name, + } + } + return self.tid, res + + +# 处理短信告警信息的不规范问题 +def sms_msg_normalize(sm_args: dict) -> dict: + for key, val in sm_args.items(): + sm_args[key] = _norm_sms_push_argv(str(val)) + return sm_args + + +def _norm_sms_push_argv(data): + """ + @处理短信参数,否则会被拦截 + """ + if _is_ipv4(data): + tmp1 = data.split('.') + return '{}_***_***_{}'.format(tmp1[0], tmp1[3]) + + data = data.replace(".", "_").replace("+", "+") + return data + + +def _is_ipv4(data: str) -> bool: + try: + ipaddress.IPv4Address(data) + except: + return False + return True + + +def _is_domain(domain): + rep_domain = re.compile(r"^([\w\-*]{1,100}\.){1,10}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$") + if rep_domain.match(domain): + return True + return False diff --git a/mod/base/push_mod/site_push.py b/mod/base/push_mod/site_push.py new file mode 100644 index 00000000..f3f8bf12 --- /dev/null +++ b/mod/base/push_mod/site_push.py @@ -0,0 +1,1290 @@ +import glob +import hashlib +import json +import os +import re +import sys +import time + +import psutil +from datetime import datetime +from importlib import import_module +from typing import Tuple, Union, Optional, List + +from .send_tool import WxAccountMsg, WxAccountLoginMsg +from .base_task import BaseTask +from .mods import PUSH_DATA_PATH, TaskConfig, SenderConfig +from .util import read_file, DB, write_file, GET_CLASS, ExecShell, get_config_value, public_get_cache_func, \ + public_set_cache_func, get_network_ip, public_get_user_info, public_http_post, panel_version +from mod.base.web_conf import RealSSLManger + + +class _WebInfo: + + def __init__(self): + self.last_time = 0 + self._items = None + self._items_by_type = None + + def __call__(self): + if self._items is not None and self.last_time > time.time() - 300: + return self._items, self._items_by_type + + items = [] + items_by_type = [[], [], [], [], []] + + res_list = DB('sites').field('id,name,project_type').select() + for i in res_list: + items.append({ + "title": i["name"] + "[" + i["project_type"] + "]", + "value": i["name"] + }) + + if i["project_type"] == "PHP" or i["project_type"] == "proxy": + continue + idx: int = ProjectStatusTask._to_project_id(i["project_type"]) + if idx is None: + continue + items_by_type[idx].append({ + "title": i["name"], + "value": i["id"] + }) + + self._items = items + self._items_by_type = items_by_type + return items, items_by_type + + +web_info = _WebInfo() + + +class SSLTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "site_ssl" + self.template_name = "网站证书(SSL)到期" + self._tip_file = "{}/site_ssl.tip".format(PUSH_DATA_PATH) + self._tip_data: Optional[dict] = None + self._task_config = TaskConfig() + + # 每次任务使用 + self.ssl_list = [] + self.push_keys = [] + self.task_id = None + + @property + def tips(self) -> dict: + if self._tip_data is not None: + return self._tip_data + try: + self._tip_data = json.loads(read_file(self._tip_file)) + except: + self._tip_data = {} + return self._tip_data + + def save_tip(self): + write_file(self._tip_file, json.dumps(self.tips)) + + def get_keyword(self, task_data: dict) -> str: + return task_data["project"] + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + # 过滤单独设置提醒的网站 + not_push_web = [i["task_data"]["project"] for i in self._task_config.config if i["source"] == self.source_name] + sql = DB("sites") + total = self._task_config.get_by_id(task_id).get("number_rule", {}).get("total", 1) + if "all" in not_push_web: + not_push_web.remove("all") + + need_check_list = [] + if task_data["project"] == "all": + # 所有正常网站 + web_list = sql.where('status=1', ()).select() + for web in web_list: + if web['name'] in not_push_web: + continue + if self.tips.get(task_id, {}).get(web['name'], 0) > total: + continue + + if not web['project_type'].lower() in ['php', 'proxy']: + project_type = web['project_type'].lower() + '_' + else: + project_type = '' + + need_check_list.append((web['name'], project_type)) + + else: + find = sql.where('name=? and status=1', (task_data['project'],)).find() + if not find: + return None + + if not find['project_type'].lower() in ['php', 'proxy']: + project_type = find['project_type'].lower() + '_' + else: + project_type = '' + + need_check_list.append((find['name'], project_type)) + + for name, project_type in need_check_list: + info = self._check_ssl_end_time(name, task_data['cycle'], project_type) + if isinstance(info, dict): # 返回的是详情,说明需要推送了 + info['site_name'] = name + self.push_keys.append(name) + self.ssl_list.append(info) + + if len(self.ssl_list) == 0: + return None + + s_list = ['>即将到期:{} 张'.format(len(self.ssl_list))] + for x in self.ssl_list: + s_list.append(">网站:{} 到期:{}".format(x['site_name'], x['notAfter'])) + + self.task_id = task_id + return {"msg_list": s_list} + + @staticmethod + def _check_ssl_end_time(site_name, limit, prefix) -> Optional[dict]: + info = RealSSLManger(conf_prefix=prefix).get_site_ssl_info(site_name) + if info is not None: + end_time = datetime.strptime(info['notAfter'], '%Y-%m-%d') + if int((end_time.timestamp() - time.time()) / 86400) <= limit: + return info + return None + + def get_title(self, task_data: dict) -> str: + if task_data["project"] == "all": + return "所有网站证书(SSL)到期提醒" + return "网站[{}]证书(SSL)到期提醒".format(task_data["project"]) + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return 'ssl_end|宝塔面板SSL到期提醒', { + "name": push_public_data["ip"], + "website": self.ssl_list[0]['site_name'], + 'time': self.ssl_list[0]["notAfter"], + 'total': len(self.ssl_list) + } + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "网站SSL到期提醒" + msg.msg = "有{}个网站的证书将到期,会影响访问".format(len(self.ssl_list)) + msg.next_msg = "请登录面板,在[网站]中进行续签操作" + return msg + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + task_data["interval"] = 60 * 60 * 24 # 默认检测间隔时间 1 天 + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] > 1): + return "剩余时间参数错误,至少为1天" + return task_data + + def filter_template(self, template) -> dict: + items, _ = web_info() + template["field"][0]["items"].extend(items) + return template + + def check_num_rule(self, num_rule: dict) -> Union[dict, str]: + num_rule["get_by_func"] = "can_send_by_num_rule" + return num_rule + + # 实际的次数检查已在 get_push_data 其他位置完成 + def can_send_by_num_rule(self, task_id: str, task_data: dict, number_rule: dict, push_data: dict) -> Optional[str]: + return None + + def task_run_end_hook(self, res) -> None: + if not res["do_send"]: + return + if self.task_id: + if self.task_id not in self.tips: + self.tips[self.task_id] = {} + + for w in self.push_keys: + if w in self.tips[self.task_id]: + self.tips[self.task_id][w] += 1 + else: + self.tips[self.task_id][w] = 1 + + self.save_tip() + + def task_config_update_hook(self, task: dict) -> None: + if task["id"] in self.tips: + self.tips.pop(task["id"]) + self.save_tip() + + def task_config_remove_hook(self, task: dict) -> None: + if task["id"] in self.tips: + self.tips.pop(task["id"]) + self.save_tip() + + +class SiteEndTimeTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "site_end_time" + self.template_name = "站点到期提醒" + self.title = "站点到期提醒" + self._tip_file = "{}/site_end_time.tip".format(PUSH_DATA_PATH) + self._tip_data: Optional[dict] = None + self._task_config = TaskConfig() + + self.push_keys = [] + self.task_id = None + + @property + def tips(self) -> dict: + if self._tip_data is not None: + return self._tip_data + try: + self._tip_data = json.loads(read_file(self._tip_file)) + except: + self._tip_data = {} + return self._tip_data + + def save_tip(self): + write_file(self._tip_file, json.dumps(self.tips)) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + task_data["interval"] = 60 * 60 * 24 # 默认检测间隔时间 1 天 + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] > 1): + return "剩余时间参数错误,至少为1天" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "site_end_time" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + m_end_date = time.strftime('%Y-%m-%d', time.localtime(time.time() + 86400 * int(task_data['cycle']))) + web_list = DB('sites').where( + 'edate>? AND edate= 1): + return None + + total = self._task_config.get_by_id(task_id).get("number_rule", {}).get("total", 1) + s_list = ['>即将到期:{} 个站点'.format(len(web_list))] + for x in web_list: + if self.tips.get(x['name'], 0) >= total: + continue + self.push_keys.append(x['name']) + s_list.append(">网站:{} 到期:{}".format(x['name'], x[' edate'])) + + if not self.push_keys: + return None + + self.task_id = task_id + + return { + "msg_list": s_list + } + + def check_num_rule(self, num_rule: dict) -> Union[dict, str]: + num_rule["get_by_func"] = "can_send_by_num_rule" + return num_rule + + # 实际的次数检查已在 get_push_data 其他位置完成 + def can_send_by_num_rule(self, task_id: str, task_data: dict, number_rule: dict, push_data: dict) -> Optional[str]: + return None + + def filter_template(self, template) -> dict: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "网站到期提醒" + msg.msg = "有{}个站点即将到期,可能影响网站访问".format(len(self.push_keys)) + msg.next_msg = "请登录面板,在[网站]中查看详情" + return msg + + def task_run_end_hook(self, res) -> None: + if not res["do_send"]: + return + if self.push_keys: + for w in self.push_keys: + if w in self.tips: + self.tips[w] += 1 + else: + self.tips[w] = 1 + self.save_tip() + + def task_config_update_hook(self, task: dict) -> None: + if os.path.exists(self._tip_file): + os.remove(self._tip_file) + + def task_config_remove_hook(self, task: dict) -> None: + if os.path.exists(self._tip_file): + os.remove(self._tip_file) + + +class PanelPwdEndTimeTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "panel_pwd_end_time" + self.template_name = "面板密码有效期" + self.title = "面板密码有效期" + + self.limit_days = 0 + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + task_data["interval"] = 60 * 60 * 24 # 默认检测间隔时间 1 天 + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] > 1): + return "剩余时间参数错误,至少为1天" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "pwd_end_time" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + import config + c_obj = config.config() + res = c_obj.get_password_config(None) + if res['expire'] > 0 and res['expire_day'] < task_data['cycle']: + self.limit_days = res['expire_day'] + + s_list = [">告警类型:登录密码即将过期", + ">剩余天数:{} 天".format(res['expire_day'])] + + return { + 'msg_list': s_list + } + + return None + + def filter_template(self, template) -> dict: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', dict() + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "面板密码到期提醒" + msg.msg = "登录密码将于{}天后过期".format(self.limit_days) + msg.next_msg = "请登录面板,在[设置]中修改密码" + return msg + + +class PanelLoginTask(BaseTask): + push_tip_file = "/www/server/panel/data/panel_login_send.pl" + + def __init__(self): + import public + public.print_log("panel_login") + super().__init__() + self.source_name = "panel_login" + self.template_name = "面板登录告警" + self.title = "面板登录告警" + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + return {} + + def get_keyword(self, task_data: dict) -> str: + return "panel_login" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + return None + + def filter_template(self, template) -> dict: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return "login_panel|面板登录提醒", { + 'name': '[' + push_data.get("ip") + ']', + 'time': time.strftime('%Y-%m-%d %X', time.localtime()), + 'type': '[' + push_data.get("is_type") + ']', + 'user': push_data.get("username") + } + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountLoginMsg.new_msg() + msg.thing_type = "面板登录提醒" + msg.login_name = push_data.get("username") + msg.login_ip = push_data.get("login_ip") + msg.login_type = push_data.get("is_type") + msg.address = push_data.get("login_ip_area") + return msg + + def task_config_update_hook(self, task: dict) -> None: + import public + public.print_log(4444444444444) + sender = task["sender"] + if len(sender) > 0: + send_id = sender[0] + else: + return + + sender_data = SenderConfig().get_by_id(send_id) + if sender_data: + write_file(self.push_tip_file, sender_data["sender_type"]) + + def task_config_create_hook(self, task: dict) -> None: + import public + public.print_log(444444433333) + sender = task["sender"] + if len(sender) > 0: + send_id = sender[0] + else: + return + + sender_data = SenderConfig().get_by_id(send_id) + if sender_data: + write_file(self.push_tip_file, sender_data["sender_type"]) + + def task_config_remove_hook(self, task: dict) -> None: + import public + public.print_log(33333333333333333333333) + if os.path.exists(self.push_tip_file): + os.remove(self.push_tip_file) + + +class SSHLoginErrorTask(BaseTask): + _months = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', + 'Sep': '09', 'Sept': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'} + + def __init__(self): + super().__init__() + self.source_name = "ssh_login_error" + self.template_name = "SSH登录失败告警" + self.title = "SSH登录失败告警" + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] >= 1): + return "时间长度参数错误,至少为1分钟" + if not (isinstance(task_data['count'], int) and task_data['count'] >= 1): + return "数量参数错误,至少为1次" + if not (isinstance(task_data['interval'], int) and task_data['interval'] >= 60): + return "间隔时间参数错误,至少为60秒" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "ssh_login_error" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + import PluginLoader + args = GET_CLASS() + args.model_index = 'safe' + args.count = task_data['count'] + args.p = 1 + res = PluginLoader.module_run("syslog", "get_ssh_error", args) + if 'status' in res: + return None + + last_info = res[task_data['count'] - 1] + if self.to_date(times=last_info['time']) >= time.time() - task_data['cycle'] * 60: + s_list = [">通知类型:SSH登录失败告警", + ">告警内容:{} 分钟内登录失败超过 {} 次 ".format( + task_data['cycle'], task_data['count'])] + + return { + 'msg_list': s_list, + 'count': task_data['count'] + } + + return None + + @staticmethod + def to_date(times, fmt_str="%Y-%m-%d %H:%M:%S"): + if times: + if isinstance(times, int): + return times + if isinstance(times, float): + return int(times) + if re.match(r"^\d+$", times): + return int(times) + else: + return 0 + ts = time.strptime(times, fmt_str) + return time.mktime(ts) + + def filter_template(self, template) -> dict: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', dict() + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "SSH登录失败告警" + msg.msg = "登录失败超过{}次".format(push_data['count']) + msg.next_msg = "请登录面板,查看SSH登录日志" + return msg + + +class ServicesTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "services" + self.template_name = "服务停止告警" + + self.pids = None + + self.service_name = '' + self.restart = None + + @staticmethod + def services_list() -> Tuple[str, List]: + res_list = [] + default = None + php_path = "/www/server/php" + if os.path.exists(php_path) and glob.glob(php_path + "/*"): + res_list.append({ + "title": "php-fpm服务停止", + "value": "php-fpm" + }) + if os.path.exists('/etc/init.d/httpd'): + default = "apache" + res_list.append({ + "title": "apache服务停止", + "value": "apache" + }) + if os.path.exists('/etc/init.d/nginx'): + default = "nginx" + res_list.append({ + "title": "nginx服务停止", + "value": "nginx" + }) + if os.path.exists('/etc/init.d/mysqld'): + res_list.append({ + "title": "mysql服务停止", + "value": "mysql" + }) + if os.path.exists('/www/server/tomcat/bin'): + res_list.append({ + "title": "tomcat服务停止", + "value": "tomcat" + }) + if os.path.exists('/etc/init.d/pure-ftpd'): + res_list.append({ + "title": "pure-ftpd服务停止", + "value": "pure-ftpd" + }) + if os.path.exists('/www/server/redis'): + res_list.append({ + "title": "redis服务停止", + "value": "redis" + }) + if os.path.exists('/etc/init.d/memcached'): + res_list.append({ + "title": "memcached服务停止", + "value": "memcached" + }) + if not default: + default = res_list[0]["value"] + return default, res_list + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + default, s_list = self.services_list() + if task_data["project"] not in {i["value"] for i in s_list}: + return "所选择的服务不存在" + if task_data["count"] not in (1, 2): + return "自动重启选择错误" + if not (isinstance(task_data['interval'], int) and task_data['interval'] >= 60): + return "间隔时间参数错误,至少为60秒" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return task_data["project"] + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + self.title = self.get_title(task_data) + ser_name = task_data['project'] + default, server_list = self.services_list() + if ser_name not in [v["value"] for v in server_list]: + return None + if self.get_server_status(ser_name): + return None + + s_list = [ + ">服务类型:" + task_data["project"], + ">服务状态:【" + task_data["project"] + "】服务已停止"] + + self.service_name = task_data["project"] + + if task_data["count"] == 1: + self._services_start(task_data["project"]) + if not self.get_server_status(task_data["project"]): + self.restart = False + s_list[1] = ">服务状态:【" + task_data["project"] + "】服务重启失败" + else: + self.restart = True + s_list[1] = ">服务状态:【" + task_data["project"] + "】服务重启成功" + + return { + "msg_list": s_list + } + + def get_title(self, task_data: dict) -> str: + return task_data["project"] + "服务停止告警" + + @staticmethod + def _services_start(service_name: str): + if service_name == "php-fpm": + base_path = "/www/server/php" + if not os.path.exists(base_path): + return None + for p in os.listdir(base_path): + init_file = os.path.join("/etc/init.d", "php-fpm-{}".format(p)) + if not os.path.isfile(init_file): + return None + ExecShell("{} start".format(init_file)) + elif service_name == 'mysql': + init_file = os.path.join("/etc/init.d", "mysqld") + ExecShell("{} start".format(init_file)) + + elif service_name == 'apache': + init_file = os.path.join("/etc/init.d", "httpd") + ExecShell("{} start".format(init_file)) + + else: + init_file = os.path.join("/etc/init.d", service_name) + ExecShell("{} start".format(init_file)) + + def get_pid_name(self, pname): + try: + if not self.pids: + self.pids = psutil.pids() + for pid in self.pids: + if psutil.Process(pid).name() == pname: return True + return False + except: + return True + + def get_server_status(self, name: str) -> bool: + time.sleep(5) + if name == "php-fpm": + base_path = "/www/server/php" + if not os.path.exists(base_path): + return False + for p in os.listdir(base_path): + pid_file = os.path.join(base_path, p, "var/run/php-fpm.pid") + if os.path.exists(pid_file): + php_pid = int(read_file(pid_file)) + status = self.check_process(php_pid) + if status: + return True + return False + + elif name == 'nginx': + if os.path.exists('/etc/init.d/nginx'): + pid_f = '/www/server/nginx/logs/nginx.pid' + if os.path.exists(pid_f): + try: + pid = read_file(pid_f) + return self.check_process(pid) + except: + pass + return False + + elif name == 'apache': + if os.path.exists('/etc/init.d/httpd'): + pid_f = '/www/server/apache/logs/httpd.pid' + if os.path.exists(pid_f): + pid = read_file(pid_f) + return self.check_process(pid) + return False + + elif name == 'mysql': + if os.path.exists('/tmp/mysql.sock'): + return True + return False + + elif name == 'tomcat': + status = False + if os.path.exists('/www/server/tomcat/logs/catalina-daemon.pid'): + if self.get_pid_name('jsvc'): + status = True + if not status: + if self.get_pid_name('java'): + status = True + return status + + elif name == 'pure-ftpd': + pid_f = '/var/run/pure-ftpd.pid' + if os.path.exists(pid_f): + pid = read_file(pid_f) + return self.check_process(pid) + return False + + elif name == 'redis': + pid_f = '/www/server/redis/redis.pid' + if os.path.exists(pid_f): + pid = read_file(pid_f) + return self.check_process(pid) + return False + + elif name == 'memcached': + pid_f = '/var/run/memcached.pid' + if os.path.exists(pid_f): + pid = read_file(pid_f) + return self.check_process(pid) + return False + + return True + + def check_process(self, pid): + try: + if not self.pids: + self.pids = psutil.pids() + if int(pid) in self.pids: + return True + return False + except Exception as e: + return False + + def filter_template(self, template: dict) -> Optional[dict]: + default, server_list = self.services_list() + if not server_list: + return None + template["field"][0]["items"] = server_list + template["field"][0]["default"] = default + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return "servcies|{}".format(self.title), { + 'name': '{}'.format(get_config_value('title')), + 'product': self.service_name, + 'product1': self.service_name + } + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + if len(self.service_name) > 14: + service_name = self.service_name[:11] + "..." + else: + service_name = self.service_name + msg.thing_type = "{}服务停止提醒".format(service_name) + if self.restart is None: + msg.msg = "{}服务已停止".format(service_name) + elif self.restart is True: + msg.msg = "{}服务重启成功".format(service_name) + else: + msg.msg = "{}服务重启失败".format(service_name) + return msg + + +class PanelSafePushTask(BaseTask): + def __init__(self): + super().__init__() + self.source_name = "panel_safe_push" + self.template_name = "面板安全告警" + self.title = "面板安全告警" + + self.msg_list = [] + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + task_data["interval"] = 60 + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "panel_safe_push" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + s_list = [] + # 面板登录用户安全 + t_add, t_del, total = self.get_records_calc('login_user_safe', DB('users')) + if t_add > 0 or t_del > 0: + s_list.append( + ">登录用户变更:总 {} 个,新增 {} 个 ,删除 {} 个.".format(total, t_add, t_del)) + + # 面板日志发生删除 + t_add, t_del, total = self.get_records_calc('panel_logs_safe', DB('logs'), 1) + if t_del > 0: + s_list.append(">面板日志发生删除,删除条数:{} 条".format(t_del)) + + debug_str = '关闭' + debug_status = 'False' + # 面板开启开发者模式告警 + if os.path.exists('/www/server/panel/data/debug.pl'): + debug_status = 'True' + debug_str = '开启' + + skey = 'panel_debug_safe' + tmp = public_get_cache_func(skey)['data'] + if not tmp: + public_set_cache_func(skey, debug_status) + else: + if str(debug_status) != tmp: + s_list.append(">面板开发者模式发生变更,当前状态:{}".format(debug_str)) + public_set_cache_func(skey, debug_status) + + # 面板用户名和密码发生变更 + find = DB('users').where('id=?', (1,)).find() + if find: + skey = 'panel_user_change_safe' + user_str = self.hash_md5(find['username']) + '|' + self.hash_md5(find['password']) + tmp = public_get_cache_func(skey)['data'] + if not tmp: + public_set_cache_func(skey, user_str) + else: + if user_str != tmp: + s_list.append(">面板登录帐号或密码发生变更") + public_set_cache_func(skey, user_str) + + if len(s_list) == 0: + return None + self.msg_list = s_list + return {"msg_list": s_list} + + @staticmethod + def hash_md5(data: str) -> str: + h = hashlib.md5() + h.update(data.encode('utf-8')) + return h.hexdigest() + + @staticmethod + def get_records_calc(skey, table, stype=0): + """ + @name 获取指定表数据是否发生改变 + @param skey string 缓存key + @param table db 表对象 + @param stype : 0 计算总条数 1 只计算删除 + @return array + total int 总数 + """ + total_add = 0 + total_del = 0 + + # 获取当前总数和最大索引值 + u_count = table.count() + u_max = table.order('id desc').getField('id') + + n_data = {'count': u_count, 'max': u_max} + tmp = public_get_cache_func(skey)['data'] + if not tmp: + public_set_cache_func(skey, n_data) + else: + n_data = tmp + # 检测上一次记录条数是否被删除 + pre_count = table.where('id<=?', (n_data['max'])).count() + if stype == 1: + if pre_count < n_data['count']: # 有数据被删除,记录被删条数 + total_del += n_data['count'] - pre_count + + n_count = u_max - pre_count # 上次记录后新增的条数 + n_idx = u_max - n_data['max'] # 上次记录后新增的索引差 + if n_count < n_idx: + total_del += n_idx - n_count + else: + + if pre_count < n_data['count']: # 有数据被删除,记录被删条数 + total_del += n_data['count'] - pre_count + elif pre_count > n_data['count']: + total_add += pre_count - n_data['count'] + + t1_del = 0 + n_count = u_count - pre_count # 上次记录后新增的条数 + + if u_max > n_data['max']: + n_idx = u_max - n_data['max'] # 上次记录后新增的索引差 + if n_count < n_idx: t1_del = n_idx - n_count + + # 新纪录除开删除,全部计算为新增 + t1_add = n_count - t1_del + if t1_add > 0: + total_add += t1_add + + total_del += t1_del + + public_set_cache_func(skey, {'count': u_count, 'max': u_max}) + return total_add, total_del, u_count + + def filter_template(self, template: dict) -> Optional[dict]: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "面板安全告警" + the_msg = [] + for d in self.msg_list: + if d.find("用户变更"): + the_msg.append("用户变更") + if d.find("日志发生删除"): + the_msg.append("面板日志删除") + if d.find("开发者模式"): + the_msg.append("开发者模式变更") + if d.find("登录帐号或密码"): + the_msg.append("帐号密码变更") + + msg.msg = "、".join(the_msg) + if len(the_msg) > 20: + msg.msg = msg.msg[:17] + "..." + msg.next_msg = "请登录面板,查看对应事项" + return msg + + +class SSHLoginTask(BaseTask): + push_tip_file = "/www/server/panel/data/ssh_send_type.pl" + + def __init__(self): + super().__init__() + self.source_name = "ssh_login" + self.template_name = "SSH登录告警" + self.title = "SSH登录告警" + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + return {} + + def get_keyword(self, task_data: dict) -> str: + return "ssh_login" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + return None + + def filter_template(self, template) -> dict: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return "", {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + login_ip = push_data.get("login_ip") + msg = WxAccountMsg.new_msg() + msg.thing_type = "SSH登录安全告警" + if len(login_ip) == 0: # 检查后门用户时使同 + msg.msg = "服务器存在后门用户" + msg.next_msg = "请检查/ect/passwd文件" + return msg + + elif len(login_ip) > 15: + login_ip = login_ip[:12] + "..." + + msg.msg = "登录ip:{}".format(login_ip) + msg.next_msg = "请登录面板,检查是否为安全登录" + return msg + + def task_config_update_hook(self, task: dict) -> None: + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + from ssh_security import ssh_security + ssh_security().start_jian(None) + + sender = task["sender"] + if len(sender) > 0: + send_id = sender[0] + else: + return + + sender_data = SenderConfig().get_by_id(send_id) + if sender_data: + write_file(self.push_tip_file, sender_data["sender_type"]) + + def task_config_create_hook(self, task: dict) -> None: + return self.task_config_update_hook(task) + + def task_config_remove_hook(self, task: dict) -> None: + if os.path.exists(self.push_tip_file): + os.remove(self.push_tip_file) + + +class PanelUpdateTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "panel_update" + self.template_name = "面板更新提醒" + self.title = "面板更新提醒" + self.new_ver = '' + + def _get_no_user_tip(self) -> str: + """没有用户信息的需要,写一个临时文件做标记,并尽可能保持不变""" + tip_file = "/www/server/panel/data/no_user_tip.pl" + if not os.path.exists(tip_file): + data: str = get_network_ip() + data = "没有用户信息时的标记文件\n" + hashlib.sha256(data.encode("utf-8")).hexdigest() + write_file(tip_file, data) + else: + data = read_file(tip_file) + if isinstance(data, bool): + os.remove(tip_file) + return self._get_no_user_tip() + return data + + def user_can_request_hour(self): + """根据哈希值,输出一个用户可查询""" + user_info = public_get_user_info() + if not bool(user_info): + user_info_str = self._get_no_user_tip() + else: + user_info_str = json.dumps(user_info) + + hash_value = hashlib.md5(user_info_str.encode("utf-8")).digest() + sum_value = 0 + for i in range(4): + sum_value = sum_value + int.from_bytes(hash_value[i * 32: (i + 1) * 32], "big") + + res = sum_value % 24 + return res + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + task_data["interval"] = 60 * 60 # 默认检测间隔时间 1 小时 + return task_data + + def check_num_rule(self, num_rule: dict) -> Union[dict, str]: + num_rule['day_num'] = 1 # 默认一天发一次 + return num_rule + + def get_keyword(self, task_data: dict) -> str: + return "panel_update" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + # 不在固定时间段内,跳过 + if self.user_can_request_hour() != datetime.now().hour: + return + + s_url = 'https://www.bt.cn/api/panel/updateLinux' + try: + res = json.loads(public_http_post(s_url, {})) + if not res: + return None + except: + return None + + n_ver = res['version'] + if res['is_beta']: + n_ver = res['beta']['version'] + + self.new_ver = n_ver + + cache_key = "panel_update_cache" + old_ver = public_get_cache_func(cache_key)['data'] + if old_ver and old_ver != n_ver: + s_list = [">通知类型:面板版本更新", + ">当前版本:{} ".format(panel_version()), + ">最新版本:{}".format(n_ver)] + return { + "msg_list": s_list + } + else: + public_set_cache_func(cache_key, n_ver) + return None + + def filter_template(self, template: dict) -> Optional[dict]: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return "", {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "面板更新提醒" + msg.msg = "最新版:{}已发布".format(self.new_ver) + msg.next_msg = "您可以登录面板,执行更新" + return msg + + def task_run_end_hook(self, res: dict) -> None: + if res["do_send"]: + public_set_cache_func("panel_update_cache", self.new_ver) + + +class ProjectStatusTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "project_status" + self.template_name = "项目停止告警" + + self.project_name = '' + self.restart = None + + @staticmethod + def _to_project_type(type_id: int): + if type_id == 1: + return "Java" + if type_id == 2: + return "Node" + if type_id == 3: + return "Go" + if type_id == 4: + return "Python" + if type_id == 5: + return "Other" + + @staticmethod + def _to_project_id(type_name): + if type_name == "Java": + return 0 + if type_name == "Node": + return 1 + if type_name == "Go": + return 2 + if type_name == "Python": + return 3 + if type_name == "Other": + return 4 + + @staticmethod + def _to_project_model(type_id: int): + if type_id == 1: + return "javaModel" + if type_id == 2: + return "nodejsModel" + if type_id == 3: + return "goModel" + if type_id == 4: + return "pythonModel" + if type_id == 5: + return "otherModel" + + def get_title(self, task_data: dict) -> str: + return "项目{}停止告警".format(self._get_project_name(task_data["project"])) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not (isinstance(task_data["cycle"], int) and 1 <= task_data["cycle"] <= 5): + return '不支持的项目类型.' + sql = DB("sites") + web_info = sql.where( + "project_type = ? and id = ?", + (self._to_project_type(task_data["cycle"]), task_data["project"]) + ).field("id,name").find() + + if not web_info: + return '没有该项目,不可设置告警' + + if task_data["count"] not in (1, 2): + return "自动重启选择错误" + if not (isinstance(task_data['interval'], int) and task_data['interval'] >= 60): + return "间隔时间参数错误,至少为60秒" + return task_data + + def get_web_list(self) -> List: + items_by_type = [[], [], [], [], []] + res_list = DB('sites').field('id,name,project_type').select() + for i in res_list: + if i["project_type"] == "PHP" or i["project_type"] == "proxy": + continue + idx: int = self._to_project_id(i["project_type"]) + if idx is None: + continue + items_by_type[idx].append({ + "title": i["name"], + "value": i["id"] + }) + return items_by_type + + def get_keyword(self, task_data: dict) -> str: + return "{}_{}".format(task_data["cycle"], self._get_project_name(task_data["project"])) + + @staticmethod + def _get_project_name(project_id: int) -> str: + data = DB('sites').where('id = ?', (project_id,)).field('id,name').find() + if isinstance(data, dict): + return data["name"] + return "" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + model_obj = import_module(".{}".format(self._to_project_model(task_data["cycle"])), package="projectModel") + model_main_obj = model_obj.main() + running, project_name = getattr(model_main_obj, "get_project_status")(task_data["project"]) + if running is not False: + return None + + s_list = [ + ">项目类型:" + self._to_project_type(task_data["cycle"]) + "项目", + ">项目名称:" + project_name, + ">项目状态:检查到项目状态为停止"] + self.project_name = project_name + + if int(task_data["count"]) == 1: + get_obj = GET_CLASS() + get_obj.project_name = project_name + result = getattr(model_main_obj, "start_project")(get_obj) + if result["status"] is True: + self.restart = True + s_list[2] = ">项目状态:检查到项目状态为停止,现已重启成功" + else: + self.restart = False + s_list[2] = ">项目状态:检查到项目状态为停止,尝试重启但失败" + + self.title = self.get_title(task_data) + + return { + "msg_list": s_list, + } + + def filter_template(self, template: dict) -> Optional[dict]: + _, web_by_type = web_info() + template["field"][1]["all_items"] = web_by_type + template["field"][1]["items"] = web_by_type[0] + if not web_by_type: + return None + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + if len(self.project_name) >= 14: + project_name = self.project_name[:11] + "..." + else: + project_name = self.project_name + msg.thing_type = "项目停止告警" + if self.restart is None: + msg.msg = "项目{}已停止".format(project_name) + elif self.restart is True: + msg.msg = "项目{}重启成功".format(project_name) + else: + msg.msg = "项目{}重启失败".format(project_name) + return msg + + +class ViewMsgFormat(object): + _FORMAT = { + "1": ( + lambda x: "剩余时间小于{}天{}".format( + x["task_data"].get("cycle"), + ("(如未处理,次日会重新发送1次,持续%d天)" % x.get("number_rule", {}).get("total", 0)) if x.get("number_rule", {}).get("total", 0) else "" + ) + ), + "2": (), + "3": (), + "8": ( + lambda x: "面板登录时,发出告警" + ), + "7": ( + lambda x: "检测到SSH登录本机时,发出告警" + ), + "4": ( + lambda x: "{}分钟内连续{}次失败登录触发,每{}秒后再次检测".format( + x["task_data"].get("cycle"), x["task_data"].get("count"), x["task_data"].get("interval"), + ) + ), + "5": ( + lambda x: "服务停止时发送一次通知,{}秒后再次检测".format(x["task_data"].get("interval")) + ), + "9": ( + lambda x: "项目停止时发送通知,{}秒后再次检测,每日发送{}次".format( + x["task_data"].get("interval"), + x.get("number_rule", {}).get("day_num", 0)) + ), + "6": ( + lambda x: "面板出现如:用户变更、面板日志删除、开启开发者等危险操作时发送告警" + ), + "10": ( + lambda x: "检测到新的版本时发送一次通知" + ) + } + + def get_msg(self, task: dict) -> Optional[str]: + if task["template_id"] in ["1", "2", "3"]: + return self._FORMAT["1"](task) + if task["template_id"] in self._FORMAT: + return self._FORMAT[task["template_id"]](task) + return None diff --git a/mod/base/push_mod/site_push_template.json b/mod/base/push_mod/site_push_template.json new file mode 100644 index 00000000..539390d7 --- /dev/null +++ b/mod/base/push_mod/site_push_template.json @@ -0,0 +1,571 @@ +[ + { + "id": "1", + "ver": "1", + "used": true, + "source": "site_ssl", + "title": "网站证书(SSL)到期", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "SSLTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "网站", + "type": "select", + "default": "all", + "items": [ + { + "title": "所有网站", + "value": "all" + } + ] + }, + { + "attr": "cycle", + "name": "剩余天数", + "type": "number", + "suffix": "", + "unit": "天", + "default": 15 + } + ], + "sorted": [ + [ + "project" + ], + [ + "cycle" + ] + ] + }, + "default": { + "project": "all", + "cycle": 15 + }, + "advanced_default": { + "number_rule": { + "total": 2 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook", + "sms" + ], + "unique": false + }, + { + "id": "2", + "ver": "1", + "used": true, + "source": "site_end_time", + "title": "网站到期", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "SiteEndTimeTask" + }, + "template": { + "field": [ + { + "attr": "cycle", + "name": "剩余天数", + "type": "number", + "unit": "天", + "suffix": "", + "default": 7 + } + ], + "sorted": [ + [ + "cycle" + ] + ] + }, + "default": { + "cycle": 7 + }, + "advanced_default": { + "number_rule": { + "total": 2 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": true + }, + { + "id": "3", + "ver": "1", + "used": true, + "source": "panel_pwd_end_time", + "title": "面板密码有效期", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "PanelPwdEndTimeTask" + }, + "template": { + "field": [ + { + "attr": "cycle", + "name": "剩余天数", + "type": "number", + "unit": "天", + "suffix": "", + "default": 15 + } + ], + "sorted": [ + [ + "cycle" + ] + ] + }, + "default": { + "cycle": 15 + }, + "advanced_default": { + "number_rule": { + "total": 2 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": true + }, + { + "id": "4", + "ver": "1", + "used": true, + "source": "ssh_login_error", + "title": "SSH登录失败告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "SSHLoginErrorTask" + }, + "template": { + "field": [ + { + "attr": "cycle", + "name": "触发条件", + "type": "number", + "unit": "分钟", + "suffix": "内,", + "default": 30 + }, + { + "attr": "count", + "name": "登录失败", + "type": "number", + "unit": "次", + "suffix": "", + "default": 3 + }, + { + "attr": "interval", + "name": "间隔时间", + "type": "number", + "unit": "秒", + "suffix": "后再次监控检测条件", + "default": 600 + } + ], + "sorted": [ + [ + "cycle", + "count" + ], + [ + "interval" + ] + ] + }, + "default": { + "cycle": 30, + "count": 3, + "interval": 600 + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + }, + "time_rule": { + "send_interval": 600 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": true + }, + { + "id": "5", + "ver": "1", + "used": true, + "source": "services", + "title": "服务停止告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "ServicesTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "通知类型", + "type": "select", + "default": null, + "items": [ + ] + }, + { + "attr": "count", + "name": "自动重启", + "type": "radio", + "suffix": "", + "default": 1, + "items": [ + { + "title": "自动尝试重启项目", + "value": 1 + }, + { + "title": "不做重启尝试", + "value": 2 + } + ] + }, + { + "attr": "interval", + "name": "间隔时间", + "type": "number", + "unit": "秒", + "suffix": "后再次监控检测条件", + "default": 600 + } + ], + "sorted": [ + [ + "project" + ], + [ + "count" + ], + [ + "interval" + ] + ] + }, + "default": { + "project": "", + "count": 2, + "interval": 600 + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": false + }, + { + "id": "6", + "ver": "1", + "used": true, + "source": "panel_safe_push", + "title": "面板安全告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "PanelSafePushTask" + }, + "template": { + "field": [ + { + "attr": "help", + "name": "告警内容", + "type": "help", + "unit": "", + "style": { + "margin-top": "6px" + }, + "list": [ + "面板用户变更、面板日志删除、面板开启开发者" + ], + "suffix": "", + "default": 600 + } + ], + "sorted": [ + [ + "help" + ] + ] + }, + "default": { + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": true + }, + { + "id": "7", + "ver": "1", + "used": true, + "source": "ssh_login", + "title": "SSH登录告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "SSHLoginTask" + }, + "template": { + "field": [ + ], + "sorted": [ + ] + }, + "default": { + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": true + }, + { + "id": "8", + "ver": "1", + "used": true, + "source": "panel_login", + "title": "面板登录告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "PanelLoginTask" + }, + "template": { + "field": [ + ], + "sorted": [ + ] + }, + "default": { + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook", + "sms" + ], + "unique": true + }, + { + "id": "9", + "ver": "1", + "used": true, + "source": "project_status", + "title": "项目停止告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "ProjectStatusTask" + }, + "template": { + "field": [ + { + "attr": "cycle", + "name": "项目类型", + "type": "select", + "default": 1, + "items": [ + { + "title": "Java项目", + "value": 1 + }, + { + "title": "Node项目", + "value": 2 + }, + { + "title": "Go项目", + "value": 3 + }, + { + "title": "Python项目", + "value": 4 + }, + { + "title": "其他项目", + "value": 5 + } + ] + }, + { + "attr": "project", + "name": "项目名称", + "type": "select", + "default": null, + "all_items": null, + "items": [ + ] + }, + { + "attr": "interval", + "name": "间隔时间", + "type": "number", + "unit": "秒", + "suffix": "后再次监控检测条件", + "default": 600 + }, + { + "attr": "count", + "name": "自动重启", + "type": "radio", + "suffix": "", + "default": 1, + "items": [ + { + "title": "自动尝试重启项目", + "value": 1 + }, + { + "title": "不做重启尝试", + "value": 2 + } + ] + } + ], + "sorted": [ + [ + "cycle" + ], + [ + "project" + ], + [ + "interval" + ], + [ + "count" + ] + ] + }, + "default": { + "cycle": 1, + "project": "", + "interval": 600, + "count": 2 + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": false + }, + { + "id": "10", + "ver": "1", + "used": true, + "source": "panel_update", + "title": "面板更新提醒", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.site_push", + "name": "PanelUpdateTask" + }, + "template": { + "field": [ + ], + "sorted": [ + ] + }, + "default": { + }, + "advanced_default": { + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": true + } +] + diff --git a/mod/base/push_mod/system.py b/mod/base/push_mod/system.py new file mode 100644 index 00000000..5d29f682 --- /dev/null +++ b/mod/base/push_mod/system.py @@ -0,0 +1,422 @@ +import os +import time +from typing import Optional, List, Tuple, Dict, Type, Any, Union +import datetime +from threading import Thread + +from .base_task import BaseTask +from .mods import TaskTemplateConfig, TaskConfig, TaskRecordConfig, SenderConfig +from .send_tool import sms_msg_normalize +from .tool import load_task_cls_by_path, load_task_cls_by_function, T_CLS +from .util import get_server_ip, get_network_ip, format_date, get_config_value +from .compatible import rsync_compatible + + +WAIT_TASK_LIST: List[Thread] = [] + + +class PushSystem: + + def __init__(self): + self.task_cls_cache: Dict[str, Type[T_CLS]] = {} + self._today_zero: Optional[datetime.datetime] = None + self._sender_type_class: Optional[dict] = {} + self.sd_cfg = SenderConfig() + + def sender_cls(self, sender_type: str): + if not self._sender_type_class: + from mod.base.msg import WeiXinMsg, MailMsg, WebHookMsg, FeiShuMsg, DingDingMsg, SMSMsg, WeChatAccountMsg + self._sender_type_class = { + "weixin": WeiXinMsg, + "mail": MailMsg, + "webhook": WebHookMsg, + "feishu": FeiShuMsg, + "dingding": DingDingMsg, + "sms": SMSMsg, + "wx_account": WeChatAccountMsg, + } + return self._sender_type_class[sender_type] + + @staticmethod + def can_run_task_list() -> Tuple[List[dict], Dict[int, dict]]: + result = [] + result_template = {} + task_template_ids = set() + for task in TaskConfig().config: + if not task["status"]: + continue + task_template_ids.add(task['template_id']) + # 间隔检测时间未到跳过 + if "interval" in task["task_data"] and isinstance(task["task_data"]["interval"], int): + if time.time() < task["last_check"] + task["task_data"]["interval"]: + continue + result.append(task) + + for template in TaskTemplateConfig().config: + if template["id"] not in task_template_ids: + continue + result_template[template['id']] = template + + return result, result_template + + def get_task_object(self, template_id, load_cls_data: dict) -> Optional[BaseTask]: + if template_id in self.task_cls_cache: + return self.task_cls_cache[template_id]() + if "load_type" not in load_cls_data: + return None + if load_cls_data["load_type"] == "func": + cls = load_task_cls_by_function( + name=load_cls_data["name"], + func_name=load_cls_data["func_name"], + is_model=load_cls_data.get("is_model", False), + model_index=load_cls_data.get("is_model", ''), + args=load_cls_data.get("args", None), + sub_name=load_cls_data.get("sub_name", None), + ) + else: + cls_path = load_cls_data["cls_path"] + cls = load_task_cls_by_path(cls_path, load_cls_data["name"]) + + if not cls: + return None + self.task_cls_cache[template_id] = cls + return cls() + + def run(self): + rsync_compatible() + task_list, task_template = self.can_run_task_list() + for t in task_list: + if t["template_id"] not in task_template: + continue + template = task_template[t["template_id"]] + if not template["used"]: + continue + print(t) + print(_PushRunner(t, template, self)()) + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") + + global WAIT_TASK_LIST + if WAIT_TASK_LIST: # 有任务启用子线程的,要等到这个线程结束,再结束主线程 + for i in WAIT_TASK_LIST: + i.join() + + def get_today_zero(self) -> datetime.datetime: + if self._today_zero is None: + t = datetime.datetime.today() + t_zero = datetime.datetime.combine(t, datetime.time.min) + self._today_zero = t_zero + return self._today_zero + + +class _PushRunner: + + def __init__(self, task: dict, template: dict, push_system: PushSystem, custom_push_data: Optional[dict] = None): + self._public_push_data: Optional[dict] = None + self.result: dict = { + "do_send": False, + "stop_msg": "", + "push_data": {}, + "check_res": False, + "check_stop_on": "", + "send_data": {}, + } # 记录结果 + self.change_fields = set() # 记录task变化值 + self.task_obj: Optional[BaseTask] = None + self.task = task + self.template = template + self.push_system = push_system + self._add_hook_msg: Optional[str] = None # 记录前置钩子处理后的追加信息 + self.custom_push_data = custom_push_data + + self.tr_cfg = TaskRecordConfig(task["id"]) + self.is_number_rule_by_func = False # 记录这个任务是否使用自定义的次数检测, 如果是,就不需要做次数更新 + + def save_result(self): + + t = TaskConfig() + tmp = t.get_by_id(self.task["id"]) + if tmp: + for f in self.change_fields: + tmp[f] = self.task[f] + + if self.result["do_send"]: + tmp["last_send"] = int(time.time()) + tmp["last_check"] = int(time.time()) + + t.save_config() + + if self.result["push_data"]: + result_data = self.result.copy() + self.tr_cfg.config.append( + { + "id": self.tr_cfg.nwe_id(), + "template_id": self.template["id"], + "task_id": self.task["id"], + "do_send": result_data.pop("do_send"), + "send_data": result_data.pop("push_data"), + "result": result_data, + "create_time": int(time.time()), + } + ) + self.tr_cfg.save_config() + + @property + def public_push_data(self) -> dict: + if self._public_push_data is None: + self._public_push_data = { + 'ip': get_server_ip(), + 'local_ip': get_network_ip(), + 'server_name': get_config_value('title') + } + data = self._public_push_data.copy() + data['time'] = format_date() + data['timestamp'] = int(time.time()) + return data + + def __call__(self): + self.run() + self.save_result() + if self.task_obj: + self.task_obj.task_run_end_hook(self.result) + return self.result_to_return() + + def result_to_return(self) -> dict: + return self.result + + def run(self): + self.task_obj = self.push_system.get_task_object(self.template["id"], self.template["load_cls"]) + + if not self.task_obj: + self.result["stop_msg"] = "任务类加载失败" + return + + if self.custom_push_data is None: + push_data = self.task_obj.get_push_data(self.task["id"], self.task["task_data"]) + if not push_data: + return + else: + push_data = self.custom_push_data + + self.result["push_data"] = push_data + # 执行前置钩子 + if self.task["pre_hook"] and "hook_type" in self.task["pre_hook"]: + if not self.run_hook(self.task["pre_hook"], "pre_hook"): + return + + # 执行时间规则判断 + if not self.run_time_rule(self.task["time_rule"]): + return + + # 执行时间规则判断 + if not self.number_rule(self.task["number_rule"]): + return + + # 执行发送信息 + self.send_message(push_data) + self.change_fields.add("number_data") + if "day_num" not in self.task["number_data"]: + self.task["number_data"]["day_num"] = 0 + + if "total" not in self.task["number_data"]: + self.task["number_data"]["total"] = 0 + + self.task["number_data"]["day_num"] += 1 + self.task["number_data"]["total"] += 1 + self.task["number_data"]["time"] = int(time.time()) + + # 执行后置钩子 + if self.task["after_hook"] and "hook_type" in self.task["after_hook"]: + self.run_hook(self.task["after_hook"], "after_hook") + + # todo: 下个版本实现一些自定义的hook函数,同时实现用户脚本的hook记录在 self.result 最后统一储存 + def run_hook(self, hook_data: dict, hook_name: str) -> bool: + """ + 执行hook操作,并返回是否继续执行, 并将hook的执行结果记录 + @param hook_name: 钩子的名称,如:after_hook, pre_hook + @param hook_data: 执行的内容 + @return: + """ + return True + + def run_time_rule(self, time_rule: dict) -> bool: + if "send_interval" in time_rule and time_rule["send_interval"] > 0: + if self.task["last_send"] + time_rule["send_interval"] > time.time(): + self.result['stop_msg'] = '小于最小发送时间,不进行发送' + self.result['check_stop_on'] = "time_rule_send_interval" + return False + + time_range = time_rule.get("time_range", None) + if time_range and isinstance(time_range, list) and len(time_range) == 2: + t_zero = self.push_system.get_today_zero() + start_time = t_zero + datetime.timedelta(seconds=time_range[0]) + end_time = t_zero + datetime.timedelta(seconds=time_range[1]) + if not start_time < datetime.datetime.now() < end_time: + self.result['stop_msg'] = '不在可发送告警的时间范围之内' + self.result['check_stop_on'] = "time_rule_time_range" + return False + return True + + def number_rule(self, number_rule: dict) -> bool: + number_data = self.task.get("number_data", {}) + # 判断通过 自定义函数的方式确认是否达到发送次数 + if "get_by_func" in number_rule and isinstance(number_rule["get_by_func"], str): + f = getattr(self.task_obj, number_rule["get_by_func"], None) + if f is not None and callable(f): + res = f(self.task["id"], self.task["task_data"], number_data, self.result["push_data"]) + if isinstance(res, str): + self.result['stop_msg'] = res + self.result['check_stop_on'] = "number_rule_get_by_func" + return False + + # 只要是走了使用函数检查的,不再处理默认情况 change_fields 中不添加 number_data + return True + + if "day_num" in number_rule and isinstance(number_rule["day_num"], int) and number_rule["day_num"] > 0: + record_time = number_data.get("time", 0) + if record_time < self.push_system.get_today_zero().timestamp(): # 昨日触发 + self.task["number_data"]["day_num"] = record_num = 0 + self.task["number_data"]["time"] = time.time() + self.change_fields.add("number_data") + else: + record_num = self.task["number_data"].get("day_num") + if record_num >= number_rule["day_num"]: + self.result['stop_msg'] = "超过每日限制次数:{}".format(number_rule["day_num"]) + self.result['check_stop_on'] = "number_rule_day_num" + return False + + if "total" in number_rule and isinstance(number_rule["total"], int) and number_rule["total"] > 0: + record_total = number_data.get("total", 0) + if record_total >= number_rule["total"]: + self.result['stop_msg'] = "超过最大限制次数:{}".format(number_rule["total"]) + self.result['check_stop_on'] = "number_rule_total" + return False + + return True + + def send_message(self, push_data: dict): + self.result["do_send"] = True + self.result["push_data"] = push_data + wx_account = [] + for sender_id in self.task["sender"]: + conf = self.push_system.sd_cfg.get_by_id(sender_id) + if conf is None: + continue + if not conf["used"]: + self.result["send_data"][sender_id] = "告警通道{}已关闭,跳过发送".format(conf["data"].get("title")) + continue + sd_cls = self.push_system.sender_cls(conf["sender_type"]) + if conf["sender_type"] == "weixin": + res = sd_cls(conf).send_msg( + self.task_obj.to_weixin_msg(push_data, self.public_push_data), + self.task_obj.title + ) + + elif conf["sender_type"] == "mail": + res = sd_cls(conf).send_msg( + self.task_obj.to_mail_msg(push_data, self.public_push_data), + self.task_obj.title + ) + + elif conf["sender_type"] == "webhook": + res = sd_cls(conf).send_msg( + self.task_obj.to_web_hook_msg(push_data, self.public_push_data), + self.task_obj.title, + self.task_obj.title + ) + + elif conf["sender_type"] == "feishu": + res = sd_cls(conf).send_msg( + self.task_obj.to_feishu_msg(push_data, self.public_push_data), + self.task_obj.title + ) + elif conf["sender_type"] == "dingding": + res = sd_cls(conf).send_msg( + self.task_obj.to_dingding_msg(push_data, self.public_push_data), + self.task_obj.title + ) + elif conf["sender_type"] == "sms": + sm_type, sm_args = self.task_obj.to_sms_msg(push_data, self.public_push_data) + if not sm_type or not sm_args: + continue + sm_args = sms_msg_normalize(sm_args) + res = sd_cls(conf).send_msg(sm_type, sm_args) + + elif conf["sender_type"] == "wx_account": + wx_account.append(conf) + continue + else: + continue + if isinstance(res, str) and res.find("Traceback") != -1: + self.result["send_data"][sender_id] = "执行信息发送过程中报错了, 未发送成功" + if isinstance(res, str): + self.result["send_data"][sender_id] = res + else: + self.result["send_data"][sender_id] = 1 + + if len(wx_account) > 0: + sd_cls = self.push_system.sender_cls("wx_account") + res = sd_cls(*wx_account).send_msg(self.task_obj.to_wx_account_msg(push_data, self.public_push_data)) + for i in wx_account: + if isinstance(res, str): + self.result["send_data"][i["id"]] = res + else: + self.result["send_data"][i["id"]] = 1 + + +def push_by_task_keyword(source: str, keyword: str, push_data: Optional[dict] = None) -> Union[str, dict]: + """ + 通过关键字查询告警任务,并发送信息 + @param push_data: + @param source: + @type keyword: + @return: + """ + push_system = PushSystem() + target_task = None + for i in TaskConfig().config: + if i["source"] == source and i["keyword"] == keyword: + target_task = i + break + if not target_task: + return "未查找到该任务" + + target_template = TaskTemplateConfig().get_by_id(target_task["template_id"]) + if not target_template["used"]: + return "该任务类型已被禁止使用" + if not target_task['status']: + return "该任务已停止" + + return _PushRunner(target_task, target_template, push_system, push_data)() + + +def push_by_task_id(task_id: str, push_data: Optional[dict] = None): + """ + 通过任务id触发告警 并 发送信息 + @param push_data: + @param task_id: + @return: + """ + push_system = PushSystem() + target_task = TaskConfig().get_by_id(task_id) + if not target_task: + return "未查找到该任务" + + target_template = TaskTemplateConfig().get_by_id(target_task["template_id"]) + if not target_template["used"]: + return "该任务类型已被禁止使用" + if not target_task['status']: + return "该任务已停止" + + return _PushRunner(target_task, target_template, push_system, push_data)() + + +def get_push_public_data(): + data = { + 'ip': get_server_ip(), + 'local_ip': get_network_ip(), + 'server_name': get_config_value('title'), + 'time': format_date(), + 'timestamp': int(time.time())} + + return data + diff --git a/mod/base/push_mod/system_push.py b/mod/base/push_mod/system_push.py new file mode 100644 index 00000000..d54eefb3 --- /dev/null +++ b/mod/base/push_mod/system_push.py @@ -0,0 +1,421 @@ + +import json +import os +import sys +import threading +import time +from datetime import datetime, timedelta +from importlib import import_module +from typing import Tuple, Union, Optional, List + +import psutil + +from .send_tool import WxAccountMsg +from .base_task import BaseTask +from .mods import PUSH_DATA_PATH +from .util import read_file, write_file, get_config_value + + +from .system import WAIT_TASK_LIST + +try: + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + from panel_msg.collector import SitePushMsgCollect, SystemPushMsgCollect +except ImportError: + SitePushMsgCollect = None + SystemPushMsgCollect = None + + +def _get_panel_name() -> str: + data = get_config_value("title") # 若获得别名,则使用别名 + if data == "": + data = "宝塔面板" + return data + + +class PanelSysDiskTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "system_disk" + self.template_name = "首页磁盘告警" + + self.wx_msg = "" + + def get_title(self, task_data: dict) -> str: + return "挂载目录【{}】的磁盘余量告警".format(task_data["project"]) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if task_data["project"] not in [i[0] for i in self._get_disk_name()]: + return "指定的磁盘不存在" + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] in (1, 2)): + return "类型参数错误" + if not (isinstance(task_data['count'], int) and task_data['count'] >= 1): + return "阈值参数错误" + if task_data['cycle'] == 2 and task_data['count'] >= 100: + return "阈值参数错误, 设置的检查范围不正确" + task_data['interval'] = 600 + return task_data + + @staticmethod + def _get_disk_name() -> list: + """获取硬盘挂载点""" + if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + + system_modul = import_module('.system', package="class") + system = getattr(system_modul, "system") + + disk_info = system.GetDiskInfo2(None, human=False) + + return [(d.get("path"), d.get("size")[0]) for d in disk_info] + + @staticmethod + def _get_disk_info() -> list: + """获取硬盘挂载点""" + if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + + system_modul = import_module('.system', package="class") + system = getattr(system_modul, "system") + + disk_info = system.GetDiskInfo2(None, human=False) + + return disk_info + + def get_keyword(self, task_data: dict) -> str: + print(task_data) + return task_data["project"] + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + disk_info = self._get_disk_info() + unsafe_disk_list = [] + + for d in disk_info: + if task_data["project"] != d["path"]: + continue + free = int(d["size"][2]) / 1048576 + proportion = int(d["size"][3] if d["size"][3][-1] != "%" else d["size"][3][:-1]) + + if task_data["cycle"] == 1 and free < task_data["count"]: + unsafe_disk_list.append( + "挂载在【{}】上的磁盘剩余容量为{}G,小于告警值{}G.".format( + d["path"], round(free, 2), task_data["count"]) + ) + self.wx_msg = "剩余容量小于{}G".format(task_data["count"]) + + elif task_data["cycle"] == 2 and proportion > task_data["count"]: + unsafe_disk_list.append( + "挂载在【{}】上的磁盘已使用容量为{}%,大于告警值{}%.".format( + d["path"], round(proportion, 2), task_data["count"]) + ) + self.wx_msg = "占用量大于{}%".format(task_data["count"]) + + if len(unsafe_disk_list) == 0: + return None + + return { + "msg_list": [ + ">通知类型:磁盘余量告警", + ">告警内容:\n" + "\n".join(unsafe_disk_list) + ] + } + + def filter_template(self, template: dict) -> Optional[dict]: + for (path, total_size) in self._get_disk_name(): + template["field"][0]["items"].append({ + "title": "【{}】的磁盘".format(path), + "value": path, + "count_default": round((int(total_size) * 0.2) / 1024 / 1024, 1) + }) + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return 'machine_exception|磁盘余量告警', { + 'name': _get_panel_name(), + 'type': "磁盘空间不足", + } + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "宝塔首页磁盘告警" + if len(self.wx_msg) > 20: + self.wx_msg = self.wx_msg[:17] + "..." + msg.msg = self.wx_msg + return msg + + +class PanelSysCPUTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "system_cpu" + self.template_name = "首页CPU告警" + self.title = "首页CPU告警" + + self.cpu_count = 0 + + self._tip_file = "{}/system_cpu.tip".format(PUSH_DATA_PATH) + self._tip_data: Optional[List[Tuple[float, float]]] = None + + @property + def cache_list(self) -> List[Tuple[float, float]]: + if self._tip_data is not None: + return self._tip_data + try: + self._tip_data = json.loads(read_file(self._tip_file)) + except: + self._tip_data = [] + return self._tip_data + + def save_cache_list(self): + write_file(self._tip_file, json.dumps(self.cache_list)) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] >= 1): + return "时间参数错误" + if not (isinstance(task_data['count'], int) and task_data['count'] >= 1): + return "阈值参数错误,至少为1%" + task_data['interval'] = 60 + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "system_cpu" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + expiration = datetime.now() - timedelta(seconds=task_data["cycle"] * 60 + 10) + for i in range(len(self.cache_list) - 1, -1, -1): + data_time, _ = self.cache_list[i] + if datetime.fromtimestamp(data_time) < expiration: + del self.cache_list[i] + + # 记录下次的 + def thread_get_cpu_data(): + self.cache_list.append((time.time(), psutil.cpu_percent(10))) + self.save_cache_list() + + thread_active = threading.Thread(target=thread_get_cpu_data, args=()) + thread_active.start() + WAIT_TASK_LIST.append(thread_active) + + if len(self.cache_list) < task_data["cycle"]: # 小于指定次数不推送 + return None + + if len(self.cache_list) > 0: + avg_data = sum(i[1] for i in self.cache_list) / len(self.cache_list) + else: + avg_data = 0 + + if avg_data < task_data["count"]: + return None + else: + self.cache_list.clear() + self.cpu_count = round(avg_data, 2) + s_list = [ + ">通知类型:CPU高占用告警", + ">告警内容:最近{}分钟内机器CPU平均占用率为{}%,高于告警值{}%".format( + task_data["cycle"], round(avg_data, 2), task_data["count"]), + ] + + return { + "msg_list": s_list, + } + + def filter_template(self, template: dict) -> Optional[dict]: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return 'machine_exception|CPU高占用告警', { + 'name': _get_panel_name(), + 'type': "CPU高占用", + } + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "宝塔首页cpu告警" + msg.msg = "主机CPU占用超过:{}%".format(self.cpu_count) + msg.next_msg = "请登录面板,查看主机情况" + return msg + + +class PanelSysLoadTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "system_load" + self.template_name = "首页负载告警" + self.title = "首页负载告警" + + self.avg_data = 0 + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] >= 1): + return "时间参数错误" + if not (isinstance(task_data['count'], int) and task_data['count'] >= 1): + return "阈值参数错误,至少为1%" + task_data['interval'] = 60 * task_data['cycle'] + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "system_load" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + now_load = os.getloadavg() + cpu_count = psutil.cpu_count() + now_load = [i / (cpu_count * 2) * 100 for i in now_load] + need_push = False + avg_data = 0 + if task_data["cycle"] == 15 and task_data["count"] < now_load[2]: + avg_data = now_load[2] + need_push = True + elif task_data["cycle"] == 5 and task_data["count"] < now_load[1]: + avg_data = now_load[1] + need_push = True + elif task_data["cycle"] == 1 and task_data["count"] < now_load[0]: + avg_data = now_load[0] + need_push = True + + if need_push is False: + return None + + self.avg_data = avg_data + + return { + "msg_list": [ + ">通知类型:负载超标告警", + ">告警内容:最近{}分钟内机器平均负载率为{}%,高于{}%告警值".format( + task_data["cycle"], round(avg_data, 2), task_data["count"]), + ] + } + + def filter_template(self, template: dict) -> Optional[dict]: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return 'machine_exception|负载超标告警', { + 'name': _get_panel_name(), + 'type': "平均负载过高", + } + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "宝塔首页负载告警" + msg.msg = "主机负载超过:{}%".format(round(self.avg_data, 2)) + msg.next_msg = "请登录面板,查看主机情况" + return msg + + +class PanelSysMEMTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "system_mem" + self.template_name = "首页内存告警" + self.title = "首页内存告警" + + self.wx_data = 0 + + self._tip_file = "{}/system_mem.tip".format(PUSH_DATA_PATH) + self._tip_data: Optional[List[Tuple[float, float]]] = None + + @property + def cache_list(self) -> List[Tuple[float, float]]: + if self._tip_data is not None: + return self._tip_data + try: + self._tip_data = json.loads(read_file(self._tip_file)) + except: + self._tip_data = [] + return self._tip_data + + def save_cache_list(self): + write_file(self._tip_file, json.dumps(self.cache_list)) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not (isinstance(task_data['cycle'], int) and task_data['cycle'] >= 1): + return "次数参数错误" + if not (isinstance(task_data['count'], int) and task_data['count'] >= 1): + return "阈值参数错误,至少为1%" + task_data['interval'] = task_data['cycle'] * 60 + return task_data + + def get_keyword(self, task_data: dict) -> str: + return "system_mem" + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + mem = psutil.virtual_memory() + real_used: float = (mem.total - mem.free - mem.buffers - mem.cached) / mem.total + stime = datetime.now() + expiration = stime - timedelta(seconds=task_data["cycle"] * 60 + 10) + + self.cache_list.append((stime.timestamp(), real_used)) + + for i in range(len(self.cache_list) - 1, -1, -1): + data_time, _ = self.cache_list[i] + if datetime.fromtimestamp(data_time) < expiration: + del self.cache_list[i] + + avg_data = sum(i[1] for i in self.cache_list) / len(self.cache_list) + + if avg_data * 100 < task_data["count"]: + self.save_cache_list() + return None + else: + self.cache_list.clear() + self.save_cache_list() + self.wx_data = round(avg_data * 100, 2) + return { + 'msg_list': [ + ">通知类型:内存高占用告警", + ">告警内容:最近{}分钟内机器内存平均占用率为{}%,高于告警值{}%".format( + task_data["cycle"], round(avg_data * 100, 2), task_data["count"]), + ] + } + + def filter_template(self, template: dict) -> Optional[dict]: + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return 'machine_exception|内存高占用告警', { + 'name': _get_panel_name(), + 'type': "内存高占用", + } + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "宝塔首页内存告警" + msg.msg = "主机内存占用超过:{}%".format(self.wx_data) + msg.next_msg = "请登录面板,查看主机情况" + return msg + + +class ViewMsgFormat(object): + _FORMAT = { + "20": ( + lambda x: "挂载在{}上的磁盘{}触发".format( + x.get("project"), + "余量不足%.1fG" % round(x.get("count"), 1) if x.get("cycle") == 1 else "占用超过%d%%" % x.get("count"), + ) + ), + "21": ( + lambda x: "{}分钟内平均CUP占用超过{}%触发".format( + x.get("cycle"), x.get("count") + ) + ), + "22": ( + lambda x: "{}分钟内平均负载超过{}%触发".format( + x.get("cycle"), x.get("count") + ) + ), + "23": ( + lambda x: "{}分钟内内存使用率超过{}%触发".format( + x.get("cycle"), x.get("count") + ) + ) + } + + def get_msg(self, task: dict) -> Optional[str]: + if task["template_id"] in self._FORMAT: + return self._FORMAT[task["template_id"]](task["task_data"]) + return None \ No newline at end of file diff --git a/mod/base/push_mod/system_push_template.json b/mod/base/push_mod/system_push_template.json new file mode 100644 index 00000000..d672613f --- /dev/null +++ b/mod/base/push_mod/system_push_template.json @@ -0,0 +1,284 @@ +[ + { + "id": "20", + "ver": "1", + "used": true, + "source": "system_disk", + "title": "首页磁盘告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.system_push", + "name": "PanelSysDiskTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "磁盘信息", + "type": "select", + "items": [ + ] + }, + { + "attr": "cycle", + "name": "检测类型", + "type": "radio", + "suffix": "", + "default": 2, + "items": [ + { + "title": "剩余容量", + "value": 1 + }, + { + "title": "占用百分比", + "value": 2 + } + ] + }, + { + "attr": "count", + "name": "占用率超过", + "type": "number", + "unit": "%", + "suffix": "后触发告警", + "default": 80, + "err_msg_prefix": "磁盘阈值" + } + ], + "sorted": [ + [ + "project" + ], + [ + "cycle" + ], + [ + "count" + ] + ] + }, + "default": { + "project": "/", + "cycle": 2, + "count": 80 + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook", + "sms" + ], + "unique": false + }, + { + "id": "21", + "ver": "1", + "used": true, + "source": "system_cpu", + "title": "首页CPU告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.system_push", + "name": "PanelSysCPUTask" + }, + "template": { + "field": [ + { + "attr": "cycle", + "name": "每", + "type": "select", + "unit": "分钟", + "suffix": "内平均", + "width": "70px", + "disabled": true, + "default": 5, + "items": [ + { + "title": "1", + "value": 3 + }, + { + "title": "5", + "value": 5 + }, + { + "title": "15", + "value": 15 + } + ] + }, + { + "attr": "count", + "name": "CPU占用超过", + "type": "number", + "unit": "%", + "suffix": "后触发告警", + "default": 80, + "err_msg_prefix": "CPU" + } + ], + "sorted": [ + [ + "cycle", + "count" + ] + ] + }, + "default": { + "cycle": 5, + "count": 80 + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook", + "sms" + ], + "unique": true + }, + { + "id": "22", + "ver": "1", + "used": true, + "source": "system_load", + "title": "首页负载告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.system_push", + "name": "PanelSysLoadTask" + }, + "template": { + "field": [ + { + "attr": "cycle", + "name": "每", + "type": "select", + "unit": "分钟", + "suffix": "内平均", + "default": 5, + "width": "70px", + "disabled": true, + "items": [ + { + "title": "1", + "value": 1 + }, + { + "title": "5", + "value": 5 + }, + { + "title": "15", + "value": 15 + } + ] + }, + { + "attr": "count", + "name": "负载超过", + "type": "number", + "unit": "%", + "suffix": "后触发告警", + "default": 80, + "err_msg_prefix": "负载" + } + ], + "sorted": [ + [ + "cycle", + "count" + ] + ] + }, + "default": { + "cycle": 5, + "count": 80 + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook", + "sms" + ], + "unique": true + }, + { + "id": "23", + "ver": "1", + "used": true, + "source": "system_mem", + "title": "首页内存告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.system_push", + "name": "PanelSysMEMTask" + }, + "template": { + "field": [ + { + "attr": "cycle", + "name": "每", + "type": "select", + "unit": "分钟", + "suffix": "内平均", + "width": "70px", + "disabled": true, + "default": 5, + "items": [ + { + "title": "1", + "value": 3 + }, + { + "title": "5", + "value": 5 + }, + { + "title": "15", + "value": 15 + } + ] + }, + { + "attr": "count", + "name": "内存使用率超过", + "type": "number", + "unit": "%", + "suffix": "后触发告警", + "default": 80, + "err_msg_prefix": "内存" + } + ], + "sorted": [ + [ + "cycle", + "count" + ] + ] + }, + "default": { + "cycle": 5, + "count": 80 + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook", + "sms" + ], + "unique": true + } +] \ No newline at end of file diff --git a/mod/base/push_mod/task_manager_push.py b/mod/base/push_mod/task_manager_push.py new file mode 100644 index 00000000..bb4d9cbb --- /dev/null +++ b/mod/base/push_mod/task_manager_push.py @@ -0,0 +1,503 @@ +import json +import os +import sys +import threading +import time +from datetime import datetime, timedelta +from importlib import import_module +from typing import Tuple, Union, Optional, List + +import psutil + +from .send_tool import WxAccountMsg +from .base_task import BaseTask +from .mods import PUSH_DATA_PATH, TaskTemplateConfig +from .util import read_file, write_file, get_config_value, GET_CLASS + + +class _ProcessInfo: + + def __init__(self): + self.data = None + self.last_time = 0 + + def __call__(self) -> list: + if self.data is not None and time.time() - self.last_time < 60: + return self.data + + try: + import PluginLoader + get_obj = GET_CLASS() + get_obj.sort = "status" + p_info = PluginLoader.plugin_run("task_manager", "get_process_list", get_obj) + except: + return [] + + if isinstance(p_info, dict) and "process_list" in p_info and isinstance( + p_info["process_list"], list): + self._process_info = p_info["process_list"] + self.last_time = time.time() + return self._process_info + else: + return [] + + +get_process_info = _ProcessInfo() + + +def have_task_manager_plugin(): + """ + 通过文件判断是否有进程管理器 + """ + return os.path.exists("/www/server/panel/plugin/task_manager/task_manager_push.py") + + +def load_task_manager_template(): + if TaskTemplateConfig().get_by_id("60"): + return None + + from .mods import load_task_template_by_config + load_task_template_by_config([ + { + "id": "60", + "ver": "1", + "used": True, + "source": "task_manager_cpu", + "title": "任务管理器CPU占用量告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.task_manager_push", + "name": "TaskManagerCPUTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "进程名称", + "type": "select", + "items": { + "url": "plugin?action=a&name=task_manager&s=get_process_list_to_push" + } + }, + { + "attr": "count", + "name": "占用率超过", + "type": "number", + "unit": "%", + "suffix": "后触发告警", + "default": 80, + "err_msg_prefix": "CUP占用率" + }, + { + "attr": "interval", + "name": "间隔时间", + "type": "number", + "unit": "秒", + "suffix": "后再次监控检测条件", + "default": 600 + } + ], + "sorted": [ + [ + "project" + ], + [ + "count" + ], + [ + "interval" + ] + ], + }, + "default": { + "project": '', + "count": 80, + "interval": 600 + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": False + }, + { + "id": "61", + "ver": "1", + "used": True, + "source": "task_manager_mem", + "title": "任务管理器内存占用量告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.task_manager_push", + "name": "TaskManagerMEMTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "进程名称", + "type": "select", + "items": { + "url": "plugin?action=a&name=task_manager&s=get_process_list_to_push" + } + }, + { + "attr": "count", + "name": "占用量超过", + "type": "number", + "unit": "MB", + "suffix": "后触发告警", + "default": None, + "err_msg_prefix": "占用量" + }, + { + "attr": "interval", + "name": "间隔时间", + "type": "number", + "unit": "秒", + "suffix": "后再次监控检测条件", + "default": 600 + } + ], + "sorted": [ + [ + "project" + ], + [ + "count" + ], + [ + "interval" + ] + ], + }, + "default": { + "project": '', + "count": 80, + "interval": 600 + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": False + }, + { + "id": "62", + "ver": "1", + "used": True, + "source": "task_manager_process", + "title": "任务管理器进程开销告警", + "load_cls": { + "load_type": "path", + "cls_path": "mod.base.push_mod.task_manager_push", + "name": "TaskManagerProcessTask" + }, + "template": { + "field": [ + { + "attr": "project", + "name": "进程名称", + "type": "select", + "items": { + "url": "plugin?action=a&name=task_manager&s=get_process_list_to_push" + } + }, + { + "attr": "count", + "name": "进程数超过", + "type": "number", + "unit": "个", + "suffix": "后触发告警", + "default": 20, + "err_msg_prefix": "进程数" + }, + { + "attr": "interval", + "name": "间隔时间", + "type": "number", + "unit": "秒", + "suffix": "后再次监控检测条件", + "default": 600 + } + ], + "sorted": [ + [ + "project" + ], + [ + "count" + ], + [ + "interval" + ] + ], + }, + "default": { + "project": '', + "count": 80, + "interval": 600 + }, + "advanced_default": { + "number_rule": { + "day_num": 3 + } + }, + "send_type_list": [ + "wx_account", + "dingding", + "feishu", + "mail", + "weixin", + "webhook" + ], + "unique": False + } + ]) + + +class TaskManagerCPUTask(BaseTask): + + def __init__(self): + super().__init__() + self.source_name = "task_manager_cpu" + self.template_name = "任务管理器CUP占用量告警" + + def get_title(self, task_data: dict) -> str: + return "进程【{}】的CPU占用量告警".format(task_data["project"]) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if "interval" not in task_data or not isinstance(task_data["interval"], int): + task_data["interval"] = 600 + if task_data["interval"] < 60: + task_data["interval"] = 60 + if "count" not in task_data or not isinstance(task_data["count"], int): + return "设置的检查范围不正确" + if not 1 <= task_data["count"] < 100: + return "设置的检查范围不正确" + if not task_data["project"]: + return "请选择进程" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return task_data["project"] + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + process_info = get_process_info() + self.title = self.get_title(task_data) + count = used = 0 + for p in process_info: + if p["name"] == task_data['project']: + used += p["cpu_percent"] + count += 1 if "children" not in p else len(p["children"]) + 1 + + if used <= task_data['count']: + return None + + return { + 'msg_list': + [ + ">通知类型:任务管理器CPU占用量告警", + ">告警内容: 进程名称为【{}】的进程共有{}个,消耗的CPU资源占比为{}%,大于告警阈值{}%。".format( + task_data['project'], count, used, task_data['count'] + ) + ], + "project": task_data['project'], + "count": int(task_data['count']) + } + + def filter_template(self, template: dict) -> Optional[dict]: + if not have_task_manager_plugin(): + return None + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "任务管理器CPU占用量告警" + if len(push_data["project"]) > 11: + project = push_data["project"][:9] + ".." + else: + project = push_data["project"] + + msg.msg = "{}的CUP超过{}%".format(project, push_data["count"]) + return msg + + +class TaskManagerMEMTask(BaseTask): + def __init__(self): + super().__init__() + self.source_name = "task_manager_mem" + self.template_name = "任务管理器内存占用量告警" + + def get_title(self, task_data: dict) -> str: + return "进程【{}】的内存占用量告警".format(task_data["project"]) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not task_data["project"]: + return "请选择进程" + if "interval" not in task_data or not isinstance(task_data["interval"], int): + task_data["interval"] = 600 + task_data["interval"] = max(60, task_data["interval"]) + if "count" not in task_data or not isinstance(task_data["count"], int): + return "设置的检查范围不正确" + if task_data["count"] < 1: + return "设置的检查范围不正确" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return task_data["project"] + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + process_info = get_process_info() + self.title = self.get_title(task_data) + + used = count = 0 + for p in process_info: + if p["name"] == task_data['project']: + used += p["memory_used"] + count += 1 if "children" not in p else len(p["children"]) + 1 + + if used <= task_data['count'] * 1024 * 1024: + return None + return { + 'msg_list': [ + ">通知类型:任务管理器内存占用量告警", + ">告警内容: 进程名称为【{}】的进程共有{}个,消耗的内存资源为{}MB,大于告警阈值{}MB。".format( + task_data['project'], count, int(used / 1024 / 1024), task_data['count'] + ) + ], + "project": task_data['project'] + } + + def filter_template(self, template: dict) -> Optional[dict]: + if not have_task_manager_plugin(): + return None + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + if len(push_data["project"]) > 11: + project = push_data["project"][:9] + ".." + else: + project = push_data["project"] + msg.thing_type = "任务管理器内存占用量告警" + msg.msg = "{}的内存超过告警数值".format(project) + return msg + + +class TaskManagerProcessTask(BaseTask): + def __init__(self): + super().__init__() + self.source_name = "task_manager_process" + self.title = "任务管理器进程开销告警" + + def get_title(self, task_data: dict) -> str: + return "进程【{}】的子进程开销告警".format(task_data["project"]) + + def check_task_data(self, task_data: dict) -> Union[dict, str]: + if not task_data["project"]: + return "请选择进程" + if "interval" not in task_data or not isinstance(task_data["interval"], int): + task_data["interval"] = 600 + task_data["interval"] = max(60, task_data["interval"]) + if "count" not in task_data or not isinstance(task_data["count"], int): + return "设置的检查范围不正确" + if task_data["count"] < 1: + return "设置的检查范围不正确" + return task_data + + def get_keyword(self, task_data: dict) -> str: + return task_data["project"] + + def get_push_data(self, task_id: str, task_data: dict) -> Optional[dict]: + process_info = get_process_info() + count = 0 + for p in process_info: + if p["name"] == task_data['project']: + count += 1 if "children" not in p else len(p["children"]) + 1 + + if count <= task_data['count']: + return None + + return { + 'msg_list': + [ + ">通知类型:任务管理器进程开销告警", + ">告警内容: 进程名称为【{}】的进程共有{}个,大于告警阈值{}个。".format( + task_data['project'], count, task_data['count'] + ) + ], + "project": task_data['project'], + "count": task_data['count'], + } + + def filter_template(self, template: dict) -> Optional[dict]: + if not have_task_manager_plugin(): + return None + return template + + def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]: + return '', {} + + def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg: + msg = WxAccountMsg.new_msg() + msg.thing_type = "任务管理器进程开销告警" + if len(push_data["project"]) > 11: + project = push_data["project"][:9] + ".." + else: + project = push_data["project"] + + if push_data["count"] > 100: # 节省字数 + push_data["count"] = "限制" + + msg.msg = "{}的子进程数超过{}".format(project, push_data["count"]) + return msg + + +class ViewMsgFormat(object): + _FORMAT = { + "60": ( + lambda x: "进程:{}的CUP占用超过{}%触发".format( + x.get("project"), x.get("count") + ) + ), + "61": ( + lambda x: "进程:{}的内存使用率超过{}MB后触发".format( + x.get("project"), x.get("count") + ) + ), + "62": ( + lambda x: "进程:{}的子进程数量超过{}后触发".format( + x.get("project"), x.get("count") + ) + ), + } + + def get_msg(self, task: dict) -> Optional[str]: + if task["template_id"] in self._FORMAT: + return self._FORMAT[task["template_id"]](task["task_data"]) + return None diff --git a/mod/base/push_mod/tool.py b/mod/base/push_mod/tool.py new file mode 100644 index 00000000..37970546 --- /dev/null +++ b/mod/base/push_mod/tool.py @@ -0,0 +1,75 @@ +import sys +from typing import Optional, Type, TypeVar +import traceback +from importlib import import_module + +from .base_task import BaseTask +from .util import GET_CLASS, get_client_ip, debug_log + + +T_CLS = TypeVar('T_CLS', bound=BaseTask) + + +def load_task_cls_by_function( + name: str, + func_name: str, + is_model: bool = False, + model_index: str = '', + args: Optional[dict] = None, + sub_name: Optional[str] = None, +) -> Optional[Type[T_CLS]]: + """ + 从执行函数的结果中获取任务类 + @param model_index: 模块来源,例如:新场景就是mod + @param name: 名称 + @param func_name: 函数名称 + @param is_model: 是否在Model中,不在Model中,就应该在插件中 + @param args: 请求这个接口的参数, 默认为空 + @param sub_name: 自分类名称, 如果有,则会和主名称name做拼接 + @return: 返回None 或者有效的任务类 + """ + import PluginLoader + real_name = name + if isinstance(sub_name, str): + real_name = "{}/{}".format(name, sub_name) + + get_obj = GET_CLASS() + if args is not None and isinstance(args, dict): + for key, value in args.items(): + setattr(get_obj, key, value) + try: + if is_model: + get_obj.model_index = model_index + res = PluginLoader.module_run(real_name, func_name, get_obj) + else: + get_obj.fun = func_name + get_obj.s = func_name + get_obj.client_ip = get_client_ip + res = PluginLoader.plugin_run(name, func_name, get_obj) + except: + debug_log(traceback.format_exc()) + return None + if isinstance(res, dict): + return None + elif isinstance(res, BaseTask): + return res.__class__ + elif issubclass(res, BaseTask): + return res + return None + + +def load_task_cls_by_path(path: str, cls_name: str) -> Optional[Type[T_CLS]]: + try: + module = import_module(path) + cls = getattr(module, cls_name, None) + if issubclass(cls, BaseTask): + return cls + elif isinstance(cls, BaseTask): + return cls.__class__ + else: + return None + except: + print(traceback.format_exc()) + print(sys.path) + debug_log(traceback.format_exc()) + return None diff --git a/mod/base/push_mod/util.py b/mod/base/push_mod/util.py new file mode 100644 index 00000000..dff88441 --- /dev/null +++ b/mod/base/push_mod/util.py @@ -0,0 +1,98 @@ +import sys +import time +from typing import Optional, Callable + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + +import public +from db import Sql + + +def write_file(filename: str, s_body: str, mode='w+') -> bool: + """ + 写入文件内容 + @filename 文件名 + @s_body 欲写入的内容 + return bool 若文件不存在则尝试自动创建 + """ + try: + fp = open(filename, mode=mode) + fp.write(s_body) + fp.close() + return True + except: + try: + fp = open(filename, mode=mode, encoding="utf-8") + fp.write(s_body) + fp.close() + return True + except: + return False + + +def read_file(filename, mode='r') -> Optional[str]: + """ + 读取文件内容 + @filename 文件名 + return string(bin) 若文件不存在,则返回None + """ + import os + if not os.path.exists(filename): + return None + fp = None + try: + fp = open(filename, mode=mode) + f_body = fp.read() + except: + return None + finally: + if fp and not fp.closed: + fp.close() + return f_body + + +ExecShell: Callable = public.ExecShell + +write_log: Callable = public.WriteLog + +Sqlite: Callable = Sql + +GET_CLASS: Callable = public.dict_obj + +debug_log: Callable = public.print_log + +get_config_value: Callable = public.GetConfigValue + +get_server_ip: Callable = public.get_server_ip + +get_network_ip: Callable = public.get_network_ip + +format_date: Callable = public.format_date + +public_get_cache_func: Callable = public.get_cache_func + +public_set_cache_func: Callable = public.set_cache_func + +public_get_user_info: Callable = public.get_user_info + +public_http_post = public.httpPost + +panel_version = public.version + + +def get_client_ip() -> str: + return public.GetClientIp() + + +class _DB: + + def __call__(self, table: str): + import db + with db.Sql() as t: + t.table(table) + return t + + +DB = _DB() diff --git a/mod/base/web_conf/__init__.py b/mod/base/web_conf/__init__.py new file mode 100644 index 00000000..5c858f75 --- /dev/null +++ b/mod/base/web_conf/__init__.py @@ -0,0 +1,66 @@ +import json +import os.path +import shutil +import sys + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +from .ip_restrict import IpRestrict, RealIpRestrict +from .redirect import RealRedirect, Redirect +from .access_restriction import AccessRestriction, RealAccessRestriction +from .domain_tool import domain_to_puny_code, check_domain, normalize_domain, NginxDomainTool, ApacheDomainTool, \ + is_domain +from .dns_api import DNSApiManager, RealDnsMager +from .dir_tool import DirTool +from .referer import Referer, RealReferer +from .logmanager import LogMgr, RealLogMgr +from .proxy import Proxy, RealProxy +from .ssl import SSLManager, RealSSLManger +from .config_mgr import ConfigMgr +from .default_site import set_default_site, get_default_site, check_default + + +def remove_sites_service_config(site_name: str, config_prefix: str = ""): + """ + 用于删除一个网站的nginx,apache的所有相关配置文件和配置项 + 包含: + 配置文件,访问限制, 反向代理, 重定向, 防盗链,证书目录, IP黑白名单, 历史配置文件, 默认站点, 日志格式配置记录, 伪静态等 + """ + # 配置文件 + ng_file = "/www/server/panel/vhost/nginx/{}{}.conf".format(config_prefix, site_name) + if os.path.exists(ng_file): + os.remove(ng_file) + ap_file = "/www/server/panel/vhost/apache/{}{}.conf".format(config_prefix, site_name) + if os.path.exists(ap_file): + os.remove(ap_file) + # 访问限制 + RealAccessRestriction(config_prefix=config_prefix).remove_site_access_restriction_info(site_name) + # 反向代理 + RealProxy(config_prefix=config_prefix).remove_site_proxy_info(site_name) + # 重定向 + RealRedirect(config_prefix=config_prefix).remove_site_redirect_info(site_name) + # 防盗链 + RealReferer(config_prefix=config_prefix).remove_site_referer_info(site_name) + # 证书目录 + cert_path = "/www/server/panel/vhost/cert/" + site_name + if os.path.isdir(cert_path): + shutil.rmtree(cert_path) + # IP黑白名单 + RealIpRestrict(config_prefix=config_prefix).remove_site_ip_restrict_info(site_name) + # 历史配置文件 + ConfigMgr(site_name=site_name, config_prefix=config_prefix).clear_history_file() + # 默认站点 + d_site_name, d_prefix = get_default_site() + if d_site_name == site_name and d_prefix == config_prefix: + d_file = "/www/server/panel/data/mod_default_site.pl" + f = open(d_file, mode="w+") + json.dump({"name": None, "prefix": None}, f) + + # 日志格式配置记录 + RealLogMgr(conf_prefix=config_prefix).remove_site_log_format_info(site_name) + + # 伪静态 + rewrite_path = "/www/server/panel/vhost/rewrite/{}{}.conf".format(config_prefix, site_name) + if os.path.isdir(rewrite_path): + os.remove(rewrite_path) diff --git a/mod/base/web_conf/access_restriction.py b/mod/base/web_conf/access_restriction.py new file mode 100644 index 00000000..807b8b04 --- /dev/null +++ b/mod/base/web_conf/access_restriction.py @@ -0,0 +1,607 @@ +# 访问限制, 目前不兼容之前版本的访问限制 +# nginx 使用 if 和 正则实现,保障与反向代理、重定向的兼容性 +# apache 实现方案未变 +import os +import re +import json +import shutil +from typing import Optional, Union, List, Dict +from itertools import chain +from .util import webserver, check_server_config, write_file, read_file, DB, service_reload, get_log_path, pre_re_key +from mod.base import json_response + + +class _ConfigObject: + _config_file_path = "" + panel_path = "/www/server/panel" + + def __init__(self): + self._config: Optional[dict] = None + + @property + def config(self) -> Dict[str, dict]: + if self._config is None: + try: + self._config = json.loads(read_file(self._config_file_path)) + except (json.JSONDecodeError, TypeError, ValueError): + self._config = {} + return self._config + + def save_config(self): + if self._config: + write_file(self._config_file_path, json.dumps(self._config)) + + +class ServerConfig: + _vhost_path = "/www/server/panel/vhost" + + def __init__(self, config_prefix: str): + self.config_prefix: str = config_prefix + + @staticmethod + def crypt_password(password) -> str: + import crypt + return crypt.crypt(password,password) + + +# nginx配置文件相关操作 +class _NginxAccessConf(ServerConfig): + + # 添加 include 导入配置项 + def set_nginx_access_include(self, site_name) -> Optional[str]: + ng_file = "{}/nginx/{}{}.conf".format(self._vhost_path, self.config_prefix, site_name) + ng_conf = read_file(ng_file) + if not ng_conf: + return "配置文件丢失" + access_dir = "{}/nginx/access/{}".format(self._vhost_path, site_name) + if not os.path.isdir(os.path.dirname(access_dir)): + os.makedirs(os.path.dirname(access_dir)) + + if not os.path.isdir(access_dir): + os.makedirs(access_dir) + + include_conf = ( + " #引用访问限制规则,注释后配置的访问限制将无效\n" + " include /www/server/panel/vhost/nginx/access/%s/*.conf;\n" + ) % site_name + + rep_include = re.compile(r"\s*include.*/access/.*/\*\.conf\s*;", re.M) + if rep_include.search(ng_conf): + return + # 添加 引入 + rep_list = [ + (re.compile(r"#SSL-END"), False), # 匹配Referer配置, 加其下 + (re.compile(r"(\s*#.*)?\s*include\s+.*/redirect/.*\.conf;"), True), # 重定向 + (re.compile(r"(\s*#.*)?\s*include\s+.*/ip-restrict/.*\.conf;"), True), # Ip黑白名单 + ] + + # 使用正则匹配确定插入位置 use_start 在前面插入还是后面插入 + def set_by_rep_idx(tmp_rep: re.Pattern, use_start: bool) -> bool: + tmp_res = tmp_rep.search(ng_conf) + if not tmp_res: + return False + if use_start: + new_conf = ng_conf[:tmp_res.start()] + include_conf + tmp_res.group() + ng_conf[tmp_res.end():] + else: + new_conf = ng_conf[:tmp_res.start()] + tmp_res.group() + include_conf + ng_conf[tmp_res.end():] + + write_file(ng_file, new_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return False + return True + for r, s in rep_list: + if set_by_rep_idx(r, s): + break + else: + return "无法在配置文件中定位到需要添加的项目" + + # 写入配置文件 + def set_nginx_access_by_conf(self, site_name: str, configs: Dict[str, List[Dict[str, str]]]) -> Optional[str]: + """ configs 示例结构 + configs = { + "auth_dir": [ + { + "name": "aaa", + "dir_path": "/", + "auth_file": "/www/server/pass/www.cache.com/aaa.pass", + "username":"aaaa", + "password":"aaaa", + } + ], + "file_deny": [ + { + "name": "bbb", + "dir_path": "/", + "suffix": ["png", "jpg"] + } + ] + } + """ + + path_map = {} + for c in chain(configs.get("auth_dir", []), configs.get("file_deny", [])): + if c["dir_path"] not in path_map: + path_map[c["dir_path"]] = {"path": c["dir_path"]} + path_map[c["dir_path"]].update(c) + + path_list = list(path_map.values()) + path_list.sort(key=lambda x: len(x["path"].split("/")), reverse=True) + conf_template = r"""location ~ "^%s.*$" { + auth_basic "Authorization"; + auth_basic_user_file %s; +%s +} +""" + suffix_template = '{tmp_pre}if ( $uri ~ "\\.({suffix})$" ) {{\n{tmp_pre} return 404;\n{tmp_pre}}}' + suffix_template2 = 'if ( $uri ~ "^{path}.*\\.({suffix})$" ) {{\n return 404;\n}}\n' + tmp_conf_list = [] + for i in path_list: + if "auth_file" in i and "suffix" in i: + tmp_pre = " " + tmp_conf = conf_template % ( + i["path"], i["auth_file"], suffix_template.format(tmp_pre=tmp_pre, suffix="|".join(i["suffix"])) + ) + write_file(i["auth_file"], "{}:{}".format(i["username"], self.crypt_password(i["password"]))) + + elif "auth_file" in i: + tmp_conf = conf_template % (i["path"], i["auth_file"], "") + write_file(i["auth_file"], "{}:{}".format(i["username"], self.crypt_password(i["password"]))) + else: + tmp_conf = suffix_template2.format(path=i["path"], suffix="|".join(i["suffix"])) + + tmp_conf_list.append(tmp_conf) + + config_data = "\n".join(tmp_conf_list) + config_file = "{}/nginx/access/{}/{}{}.conf".format(self._vhost_path, site_name, self.config_prefix, site_name) + old_config = read_file(config_file) + write_file(config_file, config_data) + if webserver() == "nginx" and check_server_config() is not None: + if isinstance(old_config, str): + write_file(config_file, old_config) + else: + write_file(config_file, "") + return "配置失败" + + +class _ApacheAccessConf(ServerConfig): + + def set_apache_access_include(self, site_name) -> Optional[str]: + ap_file = "{}/apache/{}{}.conf".format(self._vhost_path, self.config_prefix, site_name) + ap_conf = read_file(ap_file) + if not ap_conf: + return "配置文件丢失" + access_dir = "{}/apache/access/{}".format(self._vhost_path, site_name) + if not os.path.isdir(os.path.dirname(access_dir)): + os.makedirs(os.path.dirname(access_dir)) + + if not os.path.isdir(access_dir): + os.makedirs(access_dir) + + pass_dir = "/www/server/pass/" + site_name + if not os.path.isdir(os.path.dirname(pass_dir)): + os.makedirs(os.path.dirname(pass_dir)) + + if not os.path.isdir(pass_dir): + os.makedirs(pass_dir) + + include_conf = ( + "\n #引用访问限制规则,注释后配置的访问限制将无效\n" + " IncludeOptional /www/server/panel/vhost/apache/access/%s/*.conf\n" + ) % site_name + + rep_include = re.compile(r"\s*IncludeOptional.*/access/.*/\*\.conf", re.M) + if rep_include.search(ap_conf): + return + # 添加 引入 + rep_vhost_r = re.compile(r"") + new_conf = rep_vhost_r.sub(include_conf + "", ap_conf) + if not rep_include.search(new_conf): + return "配置添加失败" + + write_file(ap_file, new_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return "配置添加失败" + + def set_apache_access_by_conf(self, site_name: str, configs: Dict[str, List[Dict[str, str]]]) -> Optional[str]: + """ configs 示例结构 + configs = { + "auth_dir": [ + { + "name": "aaa", + "dir_path": "/", + "auth_file": "/www/server/pass/www.cache.com/aaa.pass", + "username":"aaaa", + "password":"aaaa", + } + ], + "file_deny": [ + { + "name": "bbb", + "dir_path": "/", + "suffix": ["png", "jpg"] + } + ] + } + """ + site_path = DB("sites").where("name=?", (site_name, )).find()["path"] + names = [] + old_configs = [] + access_dir = "{}/apache/access/{}".format(self._vhost_path, site_name) + for i in os.listdir(access_dir): + if not os.path.isfile(os.path.join(access_dir, i)): + continue + old_configs.append((i, read_file(os.path.join(access_dir, i)))) + + for c in chain(configs.get("auth_dir", []), configs.get("file_deny", [])): + if "suffix" in c: + self._set_apache_file_deny(c, site_name) + names.append("deny_{}.conf".format(c["name"])) + else: + self._set_apache_auth_dir(c, site_name, site_path) + names.append("auth_{}.conf".format(c["name"])) + + for i in os.listdir(access_dir): + if i not in names: + os.remove(os.path.join(access_dir, i)) + + if webserver() == "apache" and check_server_config() is not None: + for i in os.listdir(access_dir): + os.remove(os.path.join(access_dir, i)) + for n, data in old_configs: # 还原之前的配置文件 + write_file(os.path.join(access_dir, n), data) + return "配置保存失败" + + def _set_apache_file_deny(self, data: dict, site_name: str): + conf = r''' +#BEGIN_DENY_{n} + + Order allow,deny + Deny from all + +#END_DENY_{n} +'''.format(n=data["name"], d=data["dir_path"], s="|".join(data["suffix"])) + access_file = "{}/apache/access/{}/deny_{}.conf".format(self._vhost_path, site_name, data["name"]) + write_file(access_file, conf) + + def _set_apache_auth_dir(self, data: dict, site_path: str, site_name: str): + conf = ''' + + #AUTH_START + AuthType basic + AuthName "Authorization " + AuthUserFile {auth_file} + Require user {username} + #AUTH_END + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + #Require all granted + DirectoryIndex index.php index.html index.htm default.php default.html default.htm +'''.format(site_path=site_path, site_dir=data["dir_path"], auth_file=data["auth_file"], + username=data["username"], site_name=site_name) + write_file(data["auth_file"], "{}:{}".format(data["username"], self.crypt_password(data["password"]))) + access_file = "{}/apache/access/{}/auth_{}.conf".format(self._vhost_path, site_path, data["name"]) + write_file(access_file, conf) + + +class RealAccessRestriction(_ConfigObject, _ApacheAccessConf, _NginxAccessConf): + _config_file_path = "/www/server/panel/data/site_access.json" + + def __init__(self, config_prefix: str): + super(RealAccessRestriction, self).__init__() + super(_ApacheAccessConf, self).__init__(config_prefix) + + # 把配置信息更新到服务配置文件中 + def _refresh_web_server_conf(self, site_name: str, site_access_conf: dict, web_server=None) -> Optional[str]: + if web_server is None: + web_server = webserver() + error_msg = self.set_apache_access_by_conf(site_name, site_access_conf) + if web_server == "apache" and error_msg is not None: + return error_msg + error_msg = self.set_nginx_access_by_conf(site_name, site_access_conf) + if web_server == "nginx" and error_msg is not None: + return error_msg + + # 添加include配置到对应站点的配置文件中 + def _set_web_server_conf_include(self, site_name, web_server=None) -> Optional[str]: + if web_server is None: + web_server = webserver() + error_msg = self.set_apache_access_include(site_name) + if web_server == "apache" and error_msg is not None: + return error_msg + error_msg = self.set_nginx_access_include(site_name) + if web_server == "nginx" and error_msg is not None: + return error_msg + + def check_auth_dir_args(self, get, is_modify=False) -> Union[str, dict]: + values = {} + try: + values["site_name"] = get.site_name.strip() + values["dir_path"] = get.dir_path.strip() + except AttributeError: + return "参数错误" + + if hasattr(get, "password"): + password = get.password.strip() + if len(password) < 3: + return '密码不能少于3位' + if re.search(r'\s', password): + return '密码不能存在空格' + values['password'] = password + else: + return '请输入密码!' + + if hasattr(get, "username"): + username = get.username.strip() + if len(username) < 3: + return '账号不能少于3位' + if re.search(r'\s', username): + return '账号不能存在空格' + values['username'] = username + else: + return '请输入用户!' + + if hasattr(get, "name"): + name = get.name.strip() + if len(name) < 3: + return '名称不能少于3位' + if re.search(r'\s', name): + return '名称不能存在空格' + if not re.search(r'^\w+$', name): + return '名称格式错误,仅支持数字字母下划线,请参考格式:aaa_bbb' + values['name'] = name + else: + return '请输入名称!' + if not is_modify: + data = self.config.get(values["site_name"], {}).get("auth_dir", []) + for i in data: + if i["dir_path"] == values["dir_path"]: + return "此路径已存在" + if i["name"] == values["name"]: + return "此名称已存在" + + values["auth_file"] = "/www/server/pass/{}/{}.pass".format(values["site_name"], values["name"]) + return values + + def create_auth_dir(self, get) -> Optional[str]: + conf = self.check_auth_dir_args(get, is_modify=False) + if isinstance(conf, str): + return conf + + web_server = webserver() + error_msg = self._set_web_server_conf_include(conf["site_name"], web_server) + if error_msg: + return error_msg + + if conf["site_name"] not in self.config: + self.config[conf["site_name"]] = {"auth_dir": [], "file_deny": []} + self.config[conf["site_name"]]["auth_dir"].append(conf) + + error_msg = self._refresh_web_server_conf(conf["site_name"], self.config[conf["site_name"]], web_server) + if error_msg: + return error_msg + self.save_config() + service_reload() + + def modify_auth_dir(self, get) -> Optional[str]: + conf = self.check_auth_dir_args(get, is_modify=True) + if isinstance(conf, str): + return conf + + data = self.config.get(conf["site_name"], {}).get("auth_dir", []) + target_idx = None + for idx, i in enumerate(data): + if i["name"] == conf["name"]: + target_idx = idx + break + if target_idx is None: + return "没有指定的配置信息" + web_server = webserver() + error_msg = self._set_web_server_conf_include(conf["site_name"], web_server) + if error_msg: + return error_msg + if conf["site_name"] not in self.config: + self.config[conf["site_name"]] = {"auth_dir": [], "file_deny": []} + + self.config[conf["site_name"]]["auth_dir"][target_idx] = conf + + error_msg = self._refresh_web_server_conf(conf["site_name"], self.config[conf["site_name"]], web_server) + if error_msg: + return error_msg + self.save_config() + service_reload() + + def remove_auth_dir(self, site_name: str, name: str) -> Optional[str]: + if site_name not in self.config: + return "没有该网站的配置" + + target = None + for idx, i in enumerate(self.config[site_name].get("auth_dir", [])): + if i.get("name", None) == name: + target = idx + + if target is None: + return "没有该路径的配置" + + del self.config[site_name]["auth_dir"][target] + web_server = webserver() + error_msg = self._refresh_web_server_conf(site_name, self.config[site_name], web_server) + if error_msg: + return error_msg + self.save_config() + service_reload() + return + + def check_file_deny_args(self, get, is_modify=False) -> Union[str, dict]: + values = {} + try: + values["site_name"] = get.site_name.strip() + values["name"] = get.name.strip() + values["dir_path"] = get.dir_path.strip() + values["suffix"] = list(filter(lambda x: bool(x.strip()), json.loads(get.suffix.strip()))) + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return "参数错误" + + if len(values["name"]) < 3: + return '规则名最少需要输入3个字符串!' + if not values["suffix"]: + return '文件扩展名不可为空!' + if not values["dir_path"]: + return '目录不可为空!' + + if not is_modify: + data = self.config.get(values["site_name"], {}).get("file_deny", []) + for i in data: + if i["dir_path"] == values["dir_path"]: + return "此路径已存在" + if i["name"] == values["name"]: + return "此名称已存在" + return values + + def create_file_deny(self, get) -> Optional[str]: + conf = self.check_file_deny_args(get, is_modify=False) + if isinstance(conf, str): + return conf + web_server = webserver() + error_msg = self._set_web_server_conf_include(conf["site_name"], web_server) + if error_msg: + return error_msg + if conf["site_name"] not in self.config: + self.config[conf["site_name"]] = {"auth_dir": [], "file_deny": []} + + self.config[conf["site_name"]]["file_deny"].append(conf) + error_msg = self._refresh_web_server_conf(conf["site_name"], self.config[conf["site_name"]], web_server) + if error_msg: + return error_msg + self.save_config() + service_reload() + + def modify_file_deny(self, get) -> Optional[str]: + conf = self.check_file_deny_args(get, is_modify=True) + if isinstance(conf, str): + return conf + + data = self.config.get(conf["site_name"], {}).get("file_deny", []) + target_idx = None + for idx, i in enumerate(data): + if i["name"] == conf["name"]: + target_idx = idx + break + if target_idx is None: + return "没有指定的配置信息" + web_server = webserver() + error_msg = self._set_web_server_conf_include(conf["site_name"], web_server) + if error_msg: + return error_msg + if conf["site_name"] not in self.config: + self.config[conf["site_name"]] = {"auth_dir": [], "file_deny": []} + + self.config[conf["site_name"]]["file_deny"][target_idx] = conf + + error_msg = self._refresh_web_server_conf(conf["site_name"], self.config[conf["site_name"]], web_server) + if error_msg: + return error_msg + self.save_config() + service_reload() + + def remove_file_deny(self, site_name: str, name: str) -> Optional[str]: + if site_name not in self.config: + return "没有该网站的配置" + + target = None + for idx, i in enumerate(self.config[site_name].get("file_deny", [])): + if i.get("name", None) == name: + target = idx + + if target is None: + return "没有该路径的配置" + + del self.config[site_name]["file_deny"][target] + web_server = webserver() + error_msg = self._refresh_web_server_conf(site_name, self.config[site_name], web_server) + if error_msg: + return error_msg + self.save_config() + service_reload() + return + + def site_access_restriction_info(self, site_name: str) -> dict: + if site_name not in self.config: + return {"auth_dir": [], "file_deny": []} + else: + return self.config[site_name] + + def remove_site_access_restriction_info(self, site_name): + if site_name in self.config: + del self.config["site_name"] + self.save_config() + ng_access_dir = "{}/nginx/access/{}".format(self._vhost_path, site_name) + ap_access_dir = "{}/apache/access/{}".format(self._vhost_path, site_name) + if os.path.isdir(ng_access_dir): + shutil.rmtree(ng_access_dir) + + if os.path.isdir(ap_access_dir): + shutil.rmtree(ap_access_dir) + + +class AccessRestriction: + + def __init__(self, config_prefix: str = ""): + self.config_prefix: str = config_prefix + self._ar = RealAccessRestriction(config_prefix) + + def create_auth_dir(self, get): + res = self._ar.create_auth_dir(get) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, msg="添加成功") + + def modify_auth_dir(self, get): + res = self._ar.modify_auth_dir(get) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, msg="修改成功") + + def remove_auth_dir(self, get): + try: + site_name = get.site_name.strip() + name = get.name.strip() + except AttributeError: + return json_response(status=False, msg="请求参数错误") + res = self._ar.remove_auth_dir(site_name, name) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, msg="删除成功") + + def create_file_deny(self, get): + res = self._ar.create_file_deny(get) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, msg="添加成功") + + def modify_file_deny(self, get): + res = self._ar.modify_file_deny(get) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, msg="修改成功") + + def remove_file_deny(self, get): + try: + site_name = get.site_name.strip() + name = get.name.strip() + except AttributeError: + return json_response(status=False, msg="请求参数错误") + res = self._ar.remove_file_deny(site_name, name) + if isinstance(res, str): + return json_response(status=False, msg=res) + return json_response(status=True, msg="删除成功") + + def site_access_restriction_info(self, get): + try: + site_name = get.site_name.strip() + except AttributeError: + return json_response(status=False, msg="请求参数错误") + data = self._ar.site_access_restriction_info(site_name) + return json_response(status=True, data=data) diff --git a/mod/base/web_conf/config_mgr.py b/mod/base/web_conf/config_mgr.py new file mode 100644 index 00000000..a0741a4c --- /dev/null +++ b/mod/base/web_conf/config_mgr.py @@ -0,0 +1,154 @@ +import os +import time +from hashlib import md5 +from typing import Optional +from .util import service_reload, check_server_config, write_file, read_file + + +# 支持读取配置文件 +# 保存并重启配置文件 +# 历史文件记录 +class ConfigMgr: + _vhost_path = "/www/server/panel/vhost" + + def __init__(self, site_name: str, config_prefix: str = ""): + self.site_name = site_name + self.config_prefix = config_prefix + + def _read_config(self, web_server: str) -> Optional[str]: + config_file = "{}/{}/{}{}.conf".format(self._vhost_path, web_server, self.config_prefix, self.site_name) + res = read_file(config_file) + if isinstance(res, str): + return res + return None + + def nginx_config(self) -> Optional[str]: + return self._read_config("nginx") + + def apache_config(self) -> Optional[str]: + return self._read_config("apache") + + def save_config(self, conf_data: str, web_server: str): + config_file = "{}/{}/{}{}.conf".format(self._vhost_path, web_server, self.config_prefix, self.site_name) + old_config = self._read_config(web_server) + write_file(config_file, conf_data) + errmsg = check_server_config() + if errmsg: + write_file(config_file, old_config) + return errmsg + self._save_history(web_server) + service_reload() + + def save_nginx_config(self, conf_data: str) -> Optional[str]: + return self.save_config(conf_data, "nginx") + + def save_apache_config(self, conf_data: str) -> Optional[str]: + return self.save_config(conf_data, "apache") + + def history_list(self): + his_path = '/www/backup/file_history' + nginx_config_file = "{}/nginx/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ng_save_path = "{}{}".format(his_path, nginx_config_file) + apache_config_file = "{}/apache/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ap_save_path = "{}{}".format(his_path, apache_config_file) + return { + "nginx": [] if not os.path.isdir(ng_save_path) else sorted(os.listdir(ng_save_path), reverse=True), + "apache": [] if not os.path.isdir(ap_save_path) else sorted(os.listdir(ap_save_path), reverse=True) + } + + def history_conf(self, history_id: str) -> Optional[str]: + his_path = '/www/backup/file_history' + nginx_config_file = "{}/nginx/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ng_save_path = "{}{}".format(his_path, nginx_config_file) + if os.path.isdir(ng_save_path): + for i in os.listdir(ng_save_path): + if i == history_id: + return read_file(os.path.join(ng_save_path, i)) + + apache_config_file = "{}/apache/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ap_save_path = "{}{}".format(his_path, apache_config_file) + if os.path.isdir(ap_save_path): + for i in os.listdir(ap_save_path): + if i == history_id: + return read_file(os.path.join(ap_save_path, i)) + return None + + def remove_history_file(self, history_id: str) -> None: + his_path = '/www/backup/file_history' + nginx_config_file = "{}/nginx/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ng_save_path = "{}{}".format(his_path, nginx_config_file) + if os.path.isdir(ng_save_path): + for i in os.listdir(ng_save_path): + if i == history_id: + os.remove(os.path.join(ng_save_path, i)) + + apache_config_file = "{}/apache/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ap_save_path = "{}{}".format(his_path, apache_config_file) + if os.path.isdir(ap_save_path): + for i in os.listdir(ap_save_path): + if i == history_id: + os.remove(os.path.join(ng_save_path, i)) + + def clear_history_file(self) -> None: + """ + 清空所有的历史文件 + """ + his_path = '/www/backup/file_history' + nginx_config_file = "{}/nginx/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ng_save_path = "{}{}".format(his_path, nginx_config_file) + if os.path.isdir(ng_save_path): + for i in os.listdir(ng_save_path): + os.remove(os.path.join(ng_save_path, i)) + + apache_config_file = "{}/apache/{}{}.conf".format(self._vhost_path, self.config_prefix, self.site_name) + ap_save_path = "{}{}".format(his_path, apache_config_file) + if os.path.isdir(ap_save_path): + for i in os.listdir(ap_save_path): + os.remove(os.path.join(ng_save_path, i)) + + @staticmethod + def _file_md5(filename): + if not os.path.isfile(filename): + return False + md5_obj = md5() + with open(filename, mode="rb") as f: + while True: + b = f.read(8096) + if not b: + break + md5_obj.update(b) + + return md5_obj.hexdigest() + + def _save_history(self, web_server: str): + if os.path.exists('/www/server/panel/data/not_file_history.pl'): + return True + + his_path = '/www/backup/file_history' + filename = "{}/{}/{}{}.conf".format(self._vhost_path, web_server, self.config_prefix, self.site_name) + save_path = "{}{}".format(his_path, filename) + if not os.path.isdir(save_path): + os.makedirs(save_path, 384) + + his_list = sorted(os.listdir(save_path), reverse=True) # 倒序排列已有的历史文件 + try: + num = int(read_file('data/history_num.pl')) + except (ValueError, TypeError): + num = 100 + + is_write = True + if len(his_list) > 0: + new_file_md5 = self._file_md5(filename) + last_file_md5 = self._file_md5(os.path.join(save_path, his_list[0])) + is_write = new_file_md5 != last_file_md5 + + if is_write: + new_name = str(int(time.time())) + write_file(os.path.join(save_path, new_name), read_file(filename, 'rb'), "wb") + his_list.insert(0, new_name) + + # 删除多余的副本 + for i in his_list[num:]: + rm_file = save_path + '/' + i + if os.path.exists(rm_file): + os.remove(rm_file) \ No newline at end of file diff --git a/mod/base/web_conf/default_site.py b/mod/base/web_conf/default_site.py new file mode 100644 index 00000000..b7ec0457 --- /dev/null +++ b/mod/base/web_conf/default_site.py @@ -0,0 +1,136 @@ +import json +import os +import re +from typing import Optional, Tuple +from .util import listen_ipv6, write_file, read_file, service_reload + + +def check_default(): + vhost_path = "/www/server/panel/vhost" + nginx = vhost_path + '/nginx' + httpd = vhost_path + '/apache' + httpd_default = ''' + ServerAdmin webmaster@example.com + DocumentRoot "/www/server/apache/htdocs" + ServerName bt.default.com + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + Order allow,deny + Allow from all + DirectoryIndex index.html + + +''' + + listen_ipv6_str = '' + if listen_ipv6(): + listen_ipv6_str = "\n listen [::]:80;" + + nginx_default = '''server +{ + listen 80;%s + server_name _; + index index.html; + root /www/server/nginx/html; +}''' % listen_ipv6_str + + if not os.path.exists(httpd + '/0.default.conf') and not os.path.exists(httpd + '/default.conf'): + write_file(httpd + '/0.default.conf', httpd_default) + if not os.path.exists(nginx + '/0.default.conf') and not os.path.exists(nginx + '/default.conf'): + write_file(nginx + '/0.default.conf', nginx_default) + + +def get_default_site() -> Tuple[Optional[str], Optional[str]]: + panel_path = "/www/server/panel" + + old_ds_file = panel_path + "/data/defaultSite.pl" + new_ds_file = panel_path + "/data/mod_default_site.pl" + if os.path.exists(old_ds_file) and not os.path.exists(new_ds_file): + write_file(new_ds_file, json.dumps({ + "name": read_file(old_ds_file).strip(), + "prefix": '' + })) + + res = read_file(new_ds_file) + if not isinstance(res, str): + return None, None + data = json.loads(res) + return data["name"], data["prefix"] + + +# site_name 传递None的时候,表示将默认站点设置给关闭 +# prefix 表示配置文件前缀, 如 "net_", 默认为空字符串 +# domain 站点的域名 如: "www.sss.com:8456" +def set_default_site(site_name: Optional[str], prefix="", domain: str = None) -> Optional[str]: + # 清理旧的 + old_default_name, old_prefix = get_default_site() + panel_path = "/www/server/panel" + default_site_save = panel_path + '/data/mod_default_site.pl' + if old_default_name: + ng_conf_file = os.path.join(panel_path, "vhost/nginx/{}{}.conf".format(old_prefix, old_default_name)) + old_conf = read_file(ng_conf_file) + if isinstance(old_conf, str): + rep_listen_ds = re.compile(r"listen\s+.*default_server.*;") + new_conf_list = [] + start_idx = 0 + for tmp_res in rep_listen_ds.finditer(old_conf): + new_conf_list.append(old_conf[start_idx: tmp_res.start()]) + new_conf_list.append(tmp_res.group().replace("default_server", "")) + start_idx = tmp_res.end() + + new_conf_list.append(old_conf[start_idx:]) + + write_file(ng_conf_file, "".join(new_conf_list)) + + path = '/www/server/apache/htdocs/.htaccess' + if os.path.exists(path): + os.remove(path) + + if site_name is None: + write_file(default_site_save, json.dumps({ + "name": None, + "prefix": None + })) + service_reload() + return + + # 处理新的 + ap_path = '/www/server/apache/htdocs' + if os.path.exists(ap_path): + conf = ''' + RewriteEngine on + RewriteCond %{{HTTP_HOST}} !^127.0.0.1 [NC] + RewriteRule (.*) http://{}/$1 [L] +'''.format(domain) + + write_file(ap_path + '/.htaccess', conf) + + ng_conf_file = os.path.join(panel_path, "vhost/nginx/{}{}.conf".format(prefix, site_name)) + ng_conf = read_file(ng_conf_file) + if isinstance(ng_conf, str): + rep_listen = re.compile(r"listen[^;]*;") + new_conf_list = [] + + start_idx = 0 + for tmp_res in rep_listen.finditer(ng_conf): + new_conf_list.append(ng_conf[start_idx: tmp_res.start()]) + print(tmp_res.group()) + if tmp_res.group().find("default_server") == -1: + new_conf_list.append(tmp_res.group()[:-1] + " default_server;") + else: + new_conf_list.append(tmp_res.group()) + start_idx = tmp_res.end() + + new_conf_list.append(ng_conf[start_idx:]) + + write_file(ng_conf_file, "".join(new_conf_list)) + + write_file(default_site_save, json.dumps({ + "name": site_name, + "prefix": prefix + })) + + service_reload() + return diff --git a/mod/base/web_conf/dir_tool.py b/mod/base/web_conf/dir_tool.py new file mode 100644 index 00000000..5e7cd6ae --- /dev/null +++ b/mod/base/web_conf/dir_tool.py @@ -0,0 +1,252 @@ +# 网站文件相关操作 + +import os +import re +from typing import Optional, Union, List + +from .util import webserver, check_server_config, write_file, read_file, DB, service_reload, pre_re_key, ExecShell + + +class DirTool: + + def __init__(self, conf_prefix: str = ""): + self.conf_prefix = conf_prefix + self._vhost_path = "/www/server/panel/vhost" + + # 修改站点路径 + def modify_site_path(self, site_name: str, old_site_path: str, new_site_path: str) -> Optional[str]: + """ + 修改 站点root 路径 + site_name 站点名称 + old_site_path 旧的root 路径 + new_site_path 新的root 路径 + """ + site_info = DB("sites").where("name=?", (site_name,)).find() + if not isinstance(site_info, dict): + return "站点信息查询错误" + + error_msg = check_server_config() + if error_msg: + return "服务配置无法重载,请检查配置错误再操作。\n" + error_msg + + if not self._check_site_path(new_site_path): + return '请不要将网站根目录设置到以下关键目录中' + + if not os.path.exists(new_site_path): + return '指定的网站根目录不存在,无法设置,请检查输入信息.' + if old_site_path[-1] == '/': + old_site_path = old_site_path[:-1] + + if new_site_path[-1] == '/': + new_site_path = new_site_path[:-1] + + old_run_path = self.get_site_run_path(site_name) + if old_run_path is None: + return '读取网站当前运行目录失败,请检查配置文件' + old_run_path_sub = old_run_path.replace(old_site_path, "") + new_run_path = new_site_path + old_run_path_sub + if not os.path.exists(new_site_path): + new_run_path = new_site_path + nginx_file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + nginx_conf = read_file(nginx_file) + if nginx_conf: + rep_root = re.compile(r'\s*root\s+(.+);', re.M) + new_conf = rep_root.sub(" root {};".format(new_run_path), nginx_conf) + write_file(nginx_file, new_conf) + + apache_file = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + apache_conf = read_file(apache_file) + if apache_conf: + rep_doc = re.compile(r"DocumentRoot\s+.*\n") + new_conf = rep_doc.sub('DocumentRoot "' + new_run_path + '"\n', apache_conf) + + rep_dir = re.compile(r'''\n', new_conf) + write_file(apache_file, new_conf) + + # 创建basedir + userIni = new_run_path + '/.user.ini' + if os.path.exists(userIni): + ExecShell("chattr -i " + userIni) + write_file(userIni, 'open_basedir=' + new_run_path + '/:/tmp/') + ExecShell('chmod 644 ' + userIni) + ExecShell('chown root:root ' + userIni) + ExecShell('chattr +i ' + userIni) + service_reload() + DB("sites").where("id=?", (site_info["id"],)).setField('path', new_site_path) + return + + # 修改站点的运行路径 + def modify_site_run_path(self, site_name, site_path, new_run_path_sub: str) -> Optional[str]: + """ + 修改 站点运行路径 + site_name 站点名称 + site_path 站点路径 + new_run_path_sub root路径的子运行目录 + 如 site_path -> /www/wwwroots/aaaa + new_run_path_sub -> bbb/ccc + new_run_path -> /www/wwwroots/aaaa/bbb/ccc + """ + # 处理Nginx + old_run_path = self.get_site_run_path(site_name) + if old_run_path is None: + return '读取网站当前运行目录失败,请检查配置文件' + if new_run_path_sub.startswith("/"): + new_run_path_sub = new_run_path_sub[1:] + new_run_path = os.path.join(site_path, new_run_path_sub) + filename = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + nginx_conf = read_file(filename) + if nginx_conf: + tmp = re.search(r'\s*root\s+(.+);', nginx_conf) + if tmp: + o_path = tmp.groups()[0] + new_conf = nginx_conf.replace(o_path, new_run_path) + write_file(filename, new_conf) + + # 处理Apache + filename = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + ap_conf = read_file(filename) + if ap_conf: + tmp = re.search(r'\s*DocumentRoot\s*"(.+)"\s*\n', ap_conf) + if tmp: + o_path = tmp.groups()[0] + new_conf = ap_conf.replace(o_path, new_run_path) + write_file(filename, new_conf) + + s_path = old_run_path + "/.user.ini" + d_path = new_run_path + "/.user.ini" + if s_path != d_path: + ExecShell("chattr -i {}".format(s_path)) + ExecShell("mv {} {}".format(s_path, d_path)) + ExecShell("chattr +i {}".format(d_path)) + + service_reload() + + # 获取站点的运行路径, 返回的路径是完整路径 + def get_site_run_path(self, site_name) -> Optional[str]: + web_server = webserver() + filename = "{}/{}/{}{}.conf".format(self._vhost_path, web_server, self.conf_prefix, site_name) + if not os.path.exists(filename): + return None + run_path = None + conf = read_file(filename) + if web_server == 'nginx': + tmp1 = re.search(r'\s*root\s+(?P.+);', conf) + if tmp1: + run_path = tmp1.group("path").strip() + elif web_server == 'apache': + tmp1 = re.search(r'\s*DocumentRoot\s*"(?P.+)"\s*\n', conf) + if tmp1: + run_path = tmp1.group("path") + else: + tmp1 = re.search(r"vhRoot\s*(?P.*)", conf) + if tmp1: + run_path = tmp1.group("path").strip() + + return run_path + + # 获取index 文件 + def get_index_conf(self, site_name) -> Union[str, List[str]]: + web_server = webserver() + filename = "{}/{}/{}{}.conf".format(self._vhost_path, web_server, self.conf_prefix, site_name) + if not os.path.exists(filename): + return "配置文件丢失" + conf = read_file(filename) + if not conf: + return "配置文件丢失" + split_char = " " + if web_server == 'nginx': + rep = re.compile(r"\s+index\s+(?P.+);", re.M) + elif web_server == 'apache': + rep = re.compile(r"DirectoryIndex\s+(?P.+)", re.M) + else: + rep = re.compile(r"indexFiles\s+(?P.+)", re.M) + split_char = "," + res = rep.search(conf) + if not res: + return "获取失败,配置文件中不存在默认文档" + + res_list = list(filter(None, map(lambda x: x.strip(), res.group("target").split(split_char)))) + + return res_list + + # 获取设置index 文件 可以用 filenames 参数依次传入多个, 或 通过 file_list 参数传入index 列表 + def set_index_conf(self, site_name, *filenames: str, file_list: Optional[List[str]] = None): + index_list = set() + for i in filenames: + f = i.strip() + if not f: + continue + index_list.add(f) + + if file_list is not None: + for i in file_list: + f = i.strip() + if not f: + continue + index_list.add(f) + + # nginx + file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = read_file(file) + if conf: + rep_index = re.compile(r"\s*index\s+.+;") + new_conf = rep_index.sub(" index {};".format(" ".join(index_list)), conf) + write_file(file, new_conf) + + # apache + file = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = read_file(file) + if conf: + rep_index = re.compile(r"\s*DirectoryIndex\s+.+\n") + new_conf = rep_index.sub(" DirectoryIndex {}\n".format(" ".join(index_list)), conf) + write_file(file, new_conf) + + # openlitespeed + file = '{}/openlitespeed/detail/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = read_file(file) + if conf: + rep_index = re.compile(r"indexFiles\s+.+\n") + new_conf = rep_index.sub('indexFiles {}\n'.format(",".join(index_list)), conf) + write_file(file, new_conf) + + service_reload() + return + + def _check_site_path(self, site_path): + try: + if site_path.find('/usr/local/lighthouse/') >= 0: + return True + + if site_path in ['/', '/usr', '/dev', '/home', '/media', '/mnt', '/opt', '/tmp', '/var']: + return False + whites = ['/www/server/tomcat', '/www/server/stop', '/www/server/phpmyadmin'] + for w in whites: + if site_path.find(w) == 0: + return True + a, error_paths = self._get_sys_path() + site_path = site_path.strip() + if site_path[-1] == '/': site_path = site_path[:-1] + if site_path in a: + return False + site_path += '/' + for ep in error_paths: + if site_path.find(ep) == 0: + return False + return True + except: + return False + + @staticmethod + def _get_sys_path(): + """ + @name 关键目录 + @author hwliang<2021-06-11> + @return tuple + """ + a = ['/www', '/usr', '/', '/dev', '/home', '/media', '/mnt', '/opt', '/tmp', '/var'] + c = ['/www/.Recycle_bin/', '/www/backup/', '/www/php_session/', '/www/wwwlogs/', '/www/server/', '/etc/', + '/usr/', '/var/', '/boot/', '/proc/', '/sys/', '/tmp/', '/root/', '/lib/', '/bin/', '/sbin/', '/run/', + '/lib64/', '/lib32/', '/srv/'] + return a, c + diff --git a/mod/base/web_conf/dns_api.py b/mod/base/web_conf/dns_api.py new file mode 100644 index 00000000..85e7664d --- /dev/null +++ b/mod/base/web_conf/dns_api.py @@ -0,0 +1,1682 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 x3 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: bazoi +# +-------------------------------------------------------------------- +# | 宝塔DNS管理平台 +# +-------------------------------------------------------------------- +import shutil +import sys +import os +import json +import random +import datetime +import hmac +import re +import base64 +from urllib.parse import urljoin +from hashlib import sha1 +from uuid import uuid4 +from itertools import chain +from typing import Set, List, Optional, Union, Dict, Tuple +from mod.base import json_response + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public + +try: + import requests +except: + public.ExecShell('pip install requests') + import requests + +caa_value = '0 issue "letsencrypt.org"' + + +class ExtractZoneTool(object): + def __init__(self): + self.top_domain_list = [ + '.ac.cn', '.ah.cn', '.bj.cn', '.com.cn', '.cq.cn', '.fj.cn', '.gd.cn', + '.gov.cn', '.gs.cn', '.gx.cn', '.gz.cn', '.ha.cn', '.hb.cn', '.he.cn', + '.hi.cn', '.hk.cn', '.hl.cn', '.hn.cn', '.jl.cn', '.js.cn', '.jx.cn', + '.ln.cn', '.mo.cn', '.net.cn', '.nm.cn', '.nx.cn', '.org.cn'] + top_domain_list_data = public.readFile('{}/config/domain_root.txt'.format(public.get_panel_path())) + if top_domain_list_data: + self.top_domain_list = set(top_domain_list_data.strip().split('\n')) + + def __call__(self, domain_name): + if domain_name.startswith('*.'): + domain_name = domain_name[2:] + domain_name_copy = domain_name + top_domain = "." + ".".join(domain_name.rsplit('.')[-2:]) + new_top_domain = "." + top_domain.replace(".", "") + is_tow_top = False + if top_domain in self.top_domain_list: + is_tow_top = True + domain_name = domain_name[:-len(top_domain)] + new_top_domain + + if domain_name.count(".") <= 1: + zone = "" + root = domain_name_copy + else: + zone, middle, last = domain_name.rsplit(".", 2) + if is_tow_top: + last = top_domain[1:] + root = ".".join([middle, last]) + + return root, zone + + +extract_zone = ExtractZoneTool() + + +class BaseDns(object): + def __init__(self): + self.dns_provider_name = self.__class__.__name__ + + @staticmethod + def load_response(response): + try: + log_body = response.json() + except ValueError: + log_body = response.content + return log_body + + def add_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + raise NotImplementedError("add_record method must be implemented.") + + def remove_record(self, domain: str, host: Optional[str] = None, r_type: Optional[str] = None): + raise NotImplementedError("remove_record method must be implemented.") + + def modify_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + raise NotImplementedError("modify_record method must be implemented.") + + def get_record_list(self, domain, host: Optional[str] = None, r_type: Optional[str] = None): + raise NotImplementedError("get_record_list must be implemented.") + + def get_record_list_by_view(self, domain): + raise NotImplementedError("get_record_list must be implemented.") + + def get_domain_list(self): + return [] + + @classmethod + def new(cls, conf_data): + raise NotImplementedError("new method must be implemented.") + + def transform_record_type(self, r_type: str): + if r_type not in ("显性URL", "隐性URL"): + return r_type + self_cls = self.__class__ + if self_cls is AliyunDns: + return "REDIRECT_URL" if r_type == "显性URL" else "FORWARD_URL" + elif self_cls is CloudFlareDns: + return "URI" + + +class DNSPodDns(BaseDns): + dns_provider_name = "dnspod" + + def __init__(self, DNSPOD_ID, DNSPOD_API_KEY, DNSPOD_API_BASE_URL="https://dnsapi.cn/"): + self.DNSPOD_ID = DNSPOD_ID + self.DNSPOD_API_KEY = DNSPOD_API_KEY + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + self.HTTP_TIMEOUT = 65 # seconds + self.DNSPOD_LOGIN = "{0},{1}".format(self.DNSPOD_ID, self.DNSPOD_API_KEY) + + if DNSPOD_API_BASE_URL[-1] != "/": + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + "/" + else: + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + super(DNSPodDns, self).__init__() + + def add_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + url = urljoin(self.DNSPOD_API_BASE_URL, "Record.Create") + body = { + "record_type": r_type, + "domain": domain, + "sub_domain": host, + "value": value, + "record_line_id": "0", + "format": "json", + "weight": weight, + "ttl": ttl, + "login_token": self.DNSPOD_LOGIN, + } + add_response = requests.post( + url, data=body, timeout=self.HTTP_TIMEOUT + ).json() + if add_response["status"]["code"] != "1": + raise ValueError( + "Error creating dnspod dns record: status_code={status_code} response={response}".format( + status_code=add_response["status"]["code"], + response=add_response["status"]["message"], + ) + ) + + def get_record_list_by_view(self, domain): + records = self.get_record_list(domain) + if records is None: + return [] + res = [] + for record in records: + res.append({ + "host": record['name'], + "type": record['type'], + "value": record['value'], + "ttl": record['ttl'], + "updated_on": datetime.datetime.strptime(record['updated_on'], "%Y-%m-%d %H:%M:%S").timestamp(), + }) + return res + + def get_record_list(self, domain, host: Optional[str] = None, r_type: Optional[str] = None): + url = urljoin(self.DNSPOD_API_BASE_URL, "Record.List") + body = { + "login_token": self.DNSPOD_LOGIN, + "format": "json", + "domain": domain + } + if host is not None and isinstance(host, str): + body["subdomain"] = host + if r_type is not None and isinstance(r_type, str): + body["record_type"] = r_type + + list_response = requests.post(url, data=body, timeout=self.HTTP_TIMEOUT).json() + if "records" in list_response: + return list_response["records"] + else: + return None + + def remove_record(self, domain, host: Optional[str] = None, r_type: Optional[str] = None): + records = self.get_record_list(domain, host, r_type) + if records is None: + return False, "未查询到记录信息,没有执行删除" + for record in records: + if record['name'] != host or record['type'] != r_type: + continue + record_id = record["id"] + url = urljoin(self.DNSPOD_API_BASE_URL, "Record.Remove") + body = { + "login_token": self.DNSPOD_LOGIN, + "format": "json", + "domain": domain, + "record_id": record_id, + } + requests.post( + url, data=body, timeout=self.HTTP_TIMEOUT + ).json() + + return True, "删除成功" + + def modify_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + records = self.get_record_list(domain, host, r_type) + if records is None: + return False, "未查询到记录信息,无法执行修改" + + for record in records: + if record['name'] != host and record['type'] != r_type: + continue + + record_id = record["id"] + url = urljoin(self.DNSPOD_API_BASE_URL, "Record.Modify") + body = { + "record_type": r_type, + "record_id": record_id, + "domain": domain, + "sub_domain": host, + "value": value, + "record_line_id": "0", + "format": "json", + "weight": weight, + "ttl": ttl, + "login_token": self.DNSPOD_LOGIN, + } + add_response = requests.post( + url, data=body, timeout=self.HTTP_TIMEOUT + ).json() + + if add_response["status"]["code"] != "1": + raise ValueError( + "Error creating dnspod dns record: status_code={status_code} response={response}".format( + status_code=add_response["status"]["code"], + response=add_response["status"]["message"], + ) + ) + + def get_domain_list(self): + url = urljoin(self.DNSPOD_API_BASE_URL, "Domain.List") + body = { + "format": "json", + "login_token": self.DNSPOD_LOGIN, + } + domain_list_resp = requests.post( + url, data=body, timeout=self.HTTP_TIMEOUT + ).json() + if domain_list_resp["status"]["code"] != "1": + return {} + domains = domain_list_resp["domains"] + return {d["name"]: d for d in domains} + + @classmethod + def new(cls, conf_data: dict) -> BaseDns: + key = conf_data.get("key", None) or conf_data.get("ID", "") + secret = conf_data.get("secret", None) or conf_data.get("Token", "") + base_url = "https://dnsapi.cn/" + + return cls(key, secret, base_url) + + +class CloudFlareDns(BaseDns): + dns_provider_name = "cloudflare" + + def __init__( + self, + CLOUDFLARE_EMAIL, + CLOUDFLARE_API_KEY, + CLOUDFLARE_API_BASE_URL="https://api.cloudflare.com/client/v4/", + ): + self.CLOUDFLARE_EMAIL = CLOUDFLARE_EMAIL + self.CLOUDFLARE_API_KEY = CLOUDFLARE_API_KEY + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + self.HTTP_TIMEOUT = 65 # seconds + + self._domain_zone_id_cache = {} + + if CLOUDFLARE_API_BASE_URL[-1] != "/": + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + "/" + else: + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + super(CloudFlareDns, self).__init__() + + def _get_auth_headers(self) -> dict: + if self.CLOUDFLARE_EMAIL is None and isinstance(self.CLOUDFLARE_API_KEY, str): + return {"Authorization": "Bearer " + self.CLOUDFLARE_API_KEY} + else: + return {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + + def find_dns_zone(self, domain): + for domain_key, zone_id in self._domain_zone_id_cache.items(): + if domain_key in domain: + return self._domain_zone_id_cache[domain_key] + + url = urljoin(self.CLOUDFLARE_API_BASE_URL, "zones?status=active&per_page=1000") + headers = self._get_auth_headers() + find_dns_zone_response = requests.get(url, headers=headers, timeout=self.HTTP_TIMEOUT) + if find_dns_zone_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=find_dns_zone_response.status_code, + response=self.load_response(find_dns_zone_response), + ) + ) + + result = find_dns_zone_response.json()["result"] + self._domain_zone_id_cache = {} + for i in result: + self._domain_zone_id_cache[i["name"]] = i["id"] + + for domain_key, zone_id in self._domain_zone_id_cache.items(): + if domain_key in domain: + return self._domain_zone_id_cache[domain_key] + + raise ValueError( + ("Error unable to get DNS zone for domain_name={domain_name}: " + "status_code={status_code} response={response}").format( + domain_name=domain, + status_code=find_dns_zone_response.status_code, + response=self.load_response(find_dns_zone_response), + ) + ) + + def get_domain_list(self): + url = urljoin(self.CLOUDFLARE_API_BASE_URL, "zones?status=active&per_page=1000") + headers = self._get_auth_headers() + find_dns_zone_response = requests.get(url, headers=headers, timeout=self.HTTP_TIMEOUT) + if find_dns_zone_response.status_code != 200: + return {} + domains = find_dns_zone_response.json()["result"] + return {d["name"]: d for d in domains} + + def add_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + zone_id = self.find_dns_zone(domain) + url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records".format(zone_id), + ) + headers = self._get_auth_headers() + body = { + "type": r_type, + "name": host, + "content": "{0}".format(value), + "ttl": ttl + } + + create_cloudflare_dns_record_response = requests.post( + url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT + ) + if create_cloudflare_dns_record_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=create_cloudflare_dns_record_response.status_code, + response=self.load_response(create_cloudflare_dns_record_response), + ) + ) + + def get_record_list_by_view(self, domain): + records = self.get_record_list(domain) + if records is None: + return [] + res = [] + for record in records: + res.append({ + "host": record['name'], + "type": record['type'], + "value": record['content'], + "ttl": record['ttl'], + "modified_on": datetime.datetime.strptime(record['updated_on'], "%Y-%m-%dT%H:%M:%S.%fZ").timestamp(), + }) + return res + + def get_record_list(self, domain, host: Optional[str] = None, r_type: Optional[str] = None): + zone_id = self.find_dns_zone(domain) + list_dns_url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records?per_page=50000".format(zone_id), + ) + list_dns_payload = {} + if host is not None and isinstance(host, str): + list_dns_payload["name"] = host + if r_type is not None and isinstance(r_type, str): + list_dns_payload["type"] = r_type + + headers = self._get_auth_headers() + list_response = requests.get( + list_dns_url, params=list_dns_payload, headers=headers, timeout=self.HTTP_TIMEOUT + ).json() + if "success" and list_response["success"] is False: + return None + if "result" in list_response: + return list_response["result"] + else: + return None + + def remove_record(self, domain: str, host: Optional[str] = None, r_type: Optional[str] = None): + headers = self._get_auth_headers() + zone_id = self.find_dns_zone(domain) + records = self.get_record_list(domain, host, r_type) + if records is None: + return False, "未查询到记录信息,无法执行修改" + + for record in records: + if record['name'] != host or record['type'] != r_type: + continue + dns_record_id = record["id"] + url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records/{1}".format(zone_id, dns_record_id), + ) + requests.delete( + url, headers=headers, timeout=self.HTTP_TIMEOUT + ) + + def modify_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + records = self.get_record_list(domain, host, r_type) + if records is None: + return False, "未查询到记录信息,无法执行修改" + + headers = self._get_auth_headers() + zone_id = self.find_dns_zone(domain) + + for record in records: + if record['name'] != host and record['type'] != r_type: + continue + + record_id = record["id"] + url = urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records/{1}".format(zone_id, record_id), + ) + + body = { + "type": r_type, + "name": host, + "content": "{0}".format(value), + "ttl": ttl + } + modify_response = requests.put( + url, data=body, timeout=self.HTTP_TIMEOUT, headers=headers + ).json() + + if modify_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=modify_response.status_code, + response=self.load_response(modify_response), + ) + ) + + @classmethod + def new(cls, conf_data: dict) -> BaseDns: + key = conf_data.get("key", None) or conf_data.get("E-Mail", None) + secret = conf_data.get("secret", None) or conf_data.get("API Key", None) + base_url = "https://api.cloudflare.com/client/v4/" + + if key is None and secret is None: + secret = conf_data.get("API Token", None) # 处理api - token的情况 + if key is None and secret is None: + raise Exception("没有找到有效的DNS API密钥信息") + return cls(key, secret, base_url) + + +class AliyunDns(BaseDns): + dns_provider_name = "alidns" + + def __init__(self, key, secret): + self.key = str(key).strip() + self.secret = str(secret).strip() + self.url = "https://alidns.aliyuncs.com" + super(AliyunDns, self).__init__() + + def sign(self, accessKeySecret, parameters): # '''签名方法 + def percent_encode(encodeStr): + import urllib.request + encodeStr = str(encodeStr) + res = urllib.request.quote(encodeStr, '') + res = res.replace('+', '%20') + res = res.replace('*', '%2A') + res = res.replace('%7E', '~') + return res + + sortedParameters = sorted(parameters.items(), key=lambda parameters: parameters[0]) + canonicalizedQueryString = '' + for (k, v) in sortedParameters: + canonicalizedQueryString += '&' + percent_encode(k) + '=' + percent_encode(v) + stringToSign = 'GET&%2F&' + percent_encode(canonicalizedQueryString[1:]) + if sys.version_info[0] == 2: + h = hmac.new(accessKeySecret + "&", stringToSign, sha1) + else: + h = hmac.new(bytes(accessKeySecret + "&", encoding="utf8"), stringToSign.encode('utf8'), sha1) + signature = base64.encodebytes(h.digest()).strip() + return signature + + def add_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + random_int = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + paramsdata = { + "Action": "AddDomainRecord", + "Format": "json", + "Version": "2015-01-09", + "SignatureMethod": "HMAC-SHA1", + "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", + "SignatureNonce": str(random_int), + "AccessKeyId": self.key, + "DomainName": domain, + "RR": host, + "Type": r_type, + "Value": value, + "TTL": ttl, + } + + Signature = self.sign(self.secret, paramsdata) + paramsdata['Signature'] = Signature + req = requests.get(url=self.url, params=paramsdata) + if req.status_code != 200: + if req.json()['Code'] == 'IncorrectDomainUser' or req.json()['Code'] == 'InvalidDomainName.NoExist': + raise ValueError("这个阿里云账户下面不存在这个域名,添加解析失败") + elif req.json()['Code'] == 'InvalidAccessKeyId.NotFound' or req.json()['Code'] == 'SignatureDoesNotMatch': + raise ValueError("API密钥错误,添加解析失败") + else: + raise ValueError(req.json()['Message']) + + def get_record_list(self, domain, host: Optional[str] = None, r_type: Optional[str] = None): + random_int = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + params_data = { + "Action": "DescribeDomainRecords", + "Format": "json", + "Version": "2015-01-09", + "SignatureMethod": "HMAC-SHA1", + "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", + "SignatureNonce": str(random_int), + "AccessKeyId": self.key, + "DomainName": domain, + "PageNumber": 1, + "PageSize": 1000, + } + if host is not None and isinstance(host, str): + params_data["RRKeyWord"] = host + if r_type is not None and isinstance(r_type, str): + params_data["TypeKeyWord"] = r_type + + Signature = self.sign(self.secret, params_data) + params_data['Signature'] = Signature + req_data = requests.get(url=self.url, params=params_data).json() + record_list = req_data.get("DomainRecords", {}).get("Record", []) + return record_list + + def get_record_list_by_view(self, domain): + records = self.get_record_list(domain) + if records is None: + return [] + res = [] + for record in records: + res.append({ + "host": record['RR'], + "type": record['Type'], + "value": record['Value'], + "ttl": record['ttl'], + "modified_on": record['UpdateTimestamp'], + }) + return res + + def remove_record(self, domain: str, host: Optional[str] = None, r_type: Optional[str] = None): + records = self.get_record_list(domain, host, r_type) + if records is None: + return False, "未查询到记录信息,无法执行修改" + + random_int = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + params_data = { + "Action": "DeleteDomainRecord", + "Format": "json", + "Version": "2015-01-09", + "SignatureMethod": "HMAC-SHA1", + "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", + "SignatureNonce": str(random_int), + "AccessKeyId": self.key, + "RecordId": None, + } + for record in records: + if record['RR'] != host or record['Type'] != r_type: + continue + record_id = record["RecordId"] + p_data = params_data.copy() + p_data["RecordId"] = record_id + + Signature = self.sign(self.secret, p_data) + p_data['Signature'] = Signature + req = requests.get(url=self.url, params=p_data) + if req.status_code != 200: + raise ValueError("删除解析记录失败") + + def modify_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + records = self.get_record_list(domain, host, r_type) + if records is None: + return False, "未查询到记录信息,无法执行修改" + + random_int = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + params_data = { + "Action": "UpdateDomainRecord", + "Format": "json", + "Version": "2015-01-09", + "SignatureMethod": "HMAC-SHA1", + "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", + "SignatureNonce": str(random_int), + "AccessKeyId": self.key, + "RR": host, + "Type": r_type, + "Value": value, + "TTL": ttl, + "RecordId": None + } + + for record in records: + if record['RR'] != host and record['Type'] != r_type: + continue + + record_id = record["RecordId"] + p_data = params_data.copy() + p_data["RecordId"] = record_id + + Signature = self.sign(self.secret, p_data) + p_data['Signature'] = Signature + req = requests.get(url=self.url, params=p_data) + if req.status_code != 200: + if req.json()['Code'] == 'IncorrectDomainUser' or req.json()['Code'] == 'InvalidDomainName.NoExist': + raise ValueError("这个阿里云账户下面不存在这个域名,添加解析失败") + elif req.json()['Code'] == 'InvalidAccessKeyId.NotFound' or req.json()['Code'] == 'SignatureDoesNotMatch': + raise ValueError("API密钥错误,添加解析失败") + else: + raise ValueError(req.json()['Message']) + + def get_domain_list(self): + random_int = random.randint(11111111111111, 99999999999999) + now = datetime.datetime.utcnow() + otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") + paramsdata = { + "Action": "DescribeDomains", + "Format": "json", + "Version": "2015-01-09", + "SignatureMethod": "HMAC-SHA1", + "Timestamp": otherStyleTime, + "SignatureVersion": "1.0", + "SignatureNonce": str(random_int), + "AccessKeyId": self.key, + "PageNumber": 1, + "PageSize": 1000, + } + + Signature = self.sign(self.secret, paramsdata) + paramsdata['Signature'] = Signature + req = requests.get(url=self.url, params=paramsdata) + if req.status_code != 200: + req_json = req.json() + if req_json['Code'] == 'IncorrectDomainUser' or req_json['Code'] == 'InvalidDomainName.NoExist': + raise ValueError("这个阿里云账户下面不存在这个域名,添加解析失败") + elif req_json['Code'] == 'InvalidAccessKeyId.NotFound' or req_json['Code'] == 'SignatureDoesNotMatch': + raise ValueError("API密钥错误,添加解析失败") + else: + raise ValueError(req_json['Message']) + + domains = req.json()["Domains"]["Domain"] + return {d["DomainName"]: d for d in domains} + + @classmethod + def new(cls, conf_data: dict) -> "AliyunDns": + key = conf_data.get("key", None) or conf_data.get("AccessKey", "") + secret = conf_data.get("secret", None) or conf_data.get("SecretKey", "") + + return cls(key, secret) + + +# 未完善请勿使用 +class GoDaddyDns(BaseDns): + + _type = 0 # 0:lest 1:锐成 + http_timeout = 65 + + def __init__(self, sso_key: str, sso_secret: str, base_url='https://api.godaddy.com'): + self.sso_key = sso_key + self.sso_secret = sso_secret + self.base_url = base_url + super(GoDaddyDns, self).__init__() + self._headers = None + + def _get_auth_headers(self) -> dict: + if self._headers is not None: + return self._headers + self._headers = { + "Authorization": "sso-key {}:{}".format(self.sso_key, self.sso_secret) + } + return self._headers + + + def add_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + url = urljoin( + self.base_url, + "/v1/domains/{}/records".format(root), + ) + headers = self._get_auth_headers() + + body = [{ + "type": s_type, + "name": host, + "data": "{0}".format(value), + }] + + create_cloudflare_dns_record_response = requests.patch( + url, headers=headers, json=body, timeout=self.http_timeout + ) + if create_cloudflare_dns_record_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=create_cloudflare_dns_record_response.status_code, + response=self.load_response(create_cloudflare_dns_record_response), + ) + ) + + def modify_record(self, domain: str, host: str, r_type: str, value: str, weight: int = 1, ttl=600): + pass + + + def remove_record(self, domain: str, host: Optional[str] = None, r_type: Optional[str] = None): + headers = self._get_auth_headers() + + list_dns_url = urljoin( + self.base_url, + "/v1/domains/{}/records/{}/{}".format(domain_name, s_type, dns_name), + ) + + dns_response = requests.delete( + list_dns_url, headers=headers, timeout=self.http_timeout + ) + if dns_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=dns_response.status_code, + response=self.load_response(dns_response), + ) + ) + + def get_record_list(self, domain, host: Optional[str] = None, r_type: Optional[str] = None): + pass + + def get_record_list_by_view(self, domain): + pass + + + @classmethod + def new(cls, conf_data: dict) -> BaseDns: + key = conf_data.get("key", None) or conf_data.get("Key", "") + secret = conf_data.get("secret", None) or conf_data.get("Secret", "") + base_url = "https://api.godaddy.com" + + return cls(key, secret, base_url) + + +# 未上线 +class DNSLADns(BaseDns): + dns_provider_name = "dnsla" + _type = 0 # 0:lest 1:锐成 + + def __init__(self, api_id, api_secret): + self.api_id = api_id + self.api_secret = api_secret + self.base_url = "https://api.dns.la" + self.http_timeout = 65 # seconds + self._token = None + self.domain_list = None + super(DNSLADns, self).__init__() + + @classmethod + def new(cls, conf_data) -> BaseDns: + key = conf_data.get("key", None) or conf_data.get("APIID", "") + secret = conf_data.get("secret", None) or conf_data.get("API密钥", "") + + return cls(key, secret) + + def _get_auth_headers(self) -> dict: + if self._token is None: + self._token = base64.b64encode("{}:{}".format(self.api_id, self.api_secret).encode("utf-8")).decode("utf-8") + return {"Authorization": "Basic " + self._token} + + def find_dns_zone(self, domain_name): + url = urljoin(self.base_url, "/api/domainList?pageIndex=1&pageSize=1000") + headers = self._get_auth_headers() + find_dns_zone_response = requests.get(url, headers=headers, timeout=self.http_timeout) + if find_dns_zone_response.status_code != 200: + raise ValueError( + "Error creating DNS.LA domains: status_code={status_code} response={response}".format( + status_code=find_dns_zone_response.status_code, + response=self.load_response(find_dns_zone_response), + ) + ) + + result = find_dns_zone_response.json()["data"]["results"] + self.domain_list = result + have = False + for domain_data in result: + if domain_data["domain"].rstrip(".") == domain_name: + have = True + break + + if not have: + raise ValueError( + ( + "Error unable to get DNS zone for domain_name={domain_name}: " + "status_code={status_code} response={response}" + ).format( + domain_name=domain_name, + status_code=find_dns_zone_response.status_code, + response=self.load_response(find_dns_zone_response), + ) + ) + + @staticmethod + def _format_s_type_to_request(s_type): + trans = { + "A": 1, + "NS": 2, + "CNAME": 5, + "MX": 15, + "TXT": 16, + "AAAA": 28, + "SRV": 33, + "CAA": 257, + "URL": 256 + } + + if isinstance(s_type, (int, float)): + if int(s_type) in trans.values(): + return int(s_type) + + if isinstance(s_type, str): + if s_type in trans: + return trans[s_type] + return 16 + + def add_record(self, domain, s_type, host, value): + url = urljoin(self.base_url, "/api/record", ) + headers = self._get_auth_headers() + + domain_id = self._get_domain_id(domain) + + body = { + "domainId": domain_id, + "type": self._format_s_type_to_request(s_type), + "host": host, + "data": value, + "ttl": 600, + } + + create_dns_la_record_response = requests.post( + url, headers=headers, json=body, timeout=self.http_timeout + ) + if create_dns_la_record_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=create_dns_la_record_response.status_code, + response=self.load_response(create_dns_la_record_response), + ) + ) + + def create_dns_record(self, domain_name, domain_dns_value): + domain_name = domain_name.lstrip("*.") + root, zone, acme_txt = extract_zone(domain_name) + self.find_dns_zone(root) + + if self._type == 1: + return self.add_record(root, 'CNAME', acme_txt.replace('_acme-challenge.', ''), domain_dns_value) + else: + return self.add_record(root, 'TXT', acme_txt, domain_dns_value) + + def add_record_for_creat_site(self, domain, server_ip): + root, zone, _ = extract_zone(domain) + self.add_record(root, "A", zone, server_ip) + + def get_record_list(self, domain_id: str) -> list: + url = urljoin(self.base_url, "/api/recordList?pageIndex=1&pageSize=10&domainId={}".format(domain_id)) + headers = self._get_auth_headers() + get_record_list_response = requests.get(url, headers=headers, timeout=self.http_timeout) + if get_record_list_response.status_code != 200: + raise ValueError( + "Error unable to get record list : status_code={status_code} response={response}".format( + status_code=get_record_list_response.status_code, + response=self.load_response(get_record_list_response), + ) + ) + + result = get_record_list_response.json()["data"]["results"] + if isinstance(result, list): + return result + else: + return [] + + def _get_domain_id(self, domain_name: str) -> str: + if domain_name.count('.') > 2: + domain_name, _, _ = extract_zone(domain_name) + if self.domain_list is None: + self.find_dns_zone(domain_name) + domain_id = None + for domain_data in self.domain_list: + if domain_data["domain"].rstrip(".") == domain_name: + domain_id = domain_data["id"] + if domain_id is None: + raise ValueError( + "Error unable to get DNS zone for domain_name={domain_name}".format(domain_name=domain_name)) + return domain_id + + def remove_record(self, domain_name, dns_name, s_type): + domain_id = self._get_domain_id(domain_name) + record_list = self.get_record_list(domain_id) + trans_type = self._format_s_type_to_request(s_type) + remove_record_id_list = [] + for record in record_list: + if record["type"] == trans_type and (record["host"] == dns_name or record["displayHost"] == dns_name): + remove_record_id_list.append(record["id"]) + + headers = self._get_auth_headers() + + del_record_url_list = [urljoin(self.base_url, "/api/record?id={}".format(i)) for i in remove_record_id_list] + + for del_record_url in del_record_url_list: + requests.delete( + del_record_url, headers=headers, timeout=self.http_timeout + ) + + def delete_dns_record(self, domain_name, domain_dns_value): + domain_name = domain_name.lstrip("*.") + root, zone, acme_txt = extract_zone(domain_name) + self.remove_record(root, acme_txt, 'TXT') + + +class DomainListCache(object): + _CACHE_TIP = '{}/plugin/dns_api_manager/cache.tip'.format(public.get_panel_path()) + _CACHE_DATA = '{}/plugin/dns_api_manager/cache.json'.format(public.get_panel_path()) + + def __init__(self): + self._cache = None + self.read() + + def clear(self): + self._cache = {} + if os.path.exists(self._CACHE_DATA): + os.remove(self._CACHE_DATA) + + def set(self, cache_id, data: dict): + self._cache[cache_id] = data + + def get(self, cache_id): + if cache_id in self._cache: + return self._cache[cache_id] + return None + + def save(self): + now = int(datetime.datetime.now().timestamp()) + if self._cache: + res = public.writeFile(self._CACHE_DATA, json.dumps(self._cache)) + public.writeFile(self._CACHE_TIP, str(now + 60 * 60)) + + def _remove_cache(self): + now = int(datetime.datetime.now().timestamp()) + last = int(public.ReadFile(self._CACHE_TIP)) + if last < now: + if os.path.exists(self._CACHE_DATA): + os.remove(self._CACHE_DATA) + + def read(self): + self._remove_cache() + if not os.path.exists(self._CACHE_DATA): + self._cache = {} + + try: + data = json.loads(public.readFile(self._CACHE_DATA)) + if not isinstance(data, dict): + data = {} + except: + data = {} + + self._cache = data + + +class RealDnsMager(object): + """ + config = { + "CloudFlareDns": [ + { + "E-Mail": "122456944@qq.com", + "API Key": "dsgvfcdkjausvgfkjasdfgakj", + "ps": "xxx", + "id": 1, + "domains": [ # domains 可以不存在,内容是根域名 + ] + } + ] + ........ + }""" + + CONF_FILE = "{}/config/dns_mager.conf".format(public.get_panel_path()) + CLS_MAP: Dict = { + "AliyunDns": AliyunDns, + "DNSPodDns": DNSPodDns, + "CloudFlareDns": CloudFlareDns, + "GoDaddyDns": GoDaddyDns, + "DNSLADns": DNSLADns, + } + + NOT_USED_LIST: list = ["GoDaddyDns", "DNSLADns"] + + RULE_MAP: Dict[str, List[str]] = { + "AliyunDns": ["AccessKey", "SecretKey"], + "DNSPodDns": ["ID", "Token"], + "CloudFlareDns": ["E-Mail", "API Key"], + "GoDaddyDns": ["Key", "Secret"], + "DNSLADns": ["APIID", "API密钥"] + } + + def __init__(self): + self._config: Optional[Dict[str, Dict[str, Union[int, str]]]] = None + + @staticmethod + def _get_new_id() -> str: + return uuid4().hex + + @classmethod + def _read_config_old(cls) -> Optional[Dict[str, List[Dict[str, Union[str, list]]]]]: + old_config_file = "{}/config/dns_api.json".format(public.get_panel_path()) + if os.path.isfile(old_config_file): + try: + data = json.loads(public.readFile(old_config_file)) + except json.JSONDecodeError: + return None + res = {} + rule_list = ("AliyunDns", "DNSPodDns", "CloudFlareDns", "GoDaddyDns") + if isinstance(data, list): + for d in data: + if d["name"] not in rule_list: + continue + + conf_data = d.get("data", None) + if isinstance(data, list): + tmp = {i["name"]: i["value"] for i in conf_data if i["value"].strip()} + tmp["ps"] = "默认账户" + tmp["id"] = cls._get_new_id() + if len(tmp) > 2: + res[d["name"]] = [tmp] + + if res: + return res + return None + + @staticmethod + def _get_acme_dns_api() -> Dict[str, Dict[str, str]]: + path = '/root/.acme.sh' + if not os.path.exists(path + '/account.conf'): + path = "/.acme.sh" + account = public.readFile(path + '/account.conf') + if not account: + return {} + rule_map: Dict[str, Dict[str, str]] = { + "AliyunDns": { + "AccessKey": "SAVED_Ali_Key", + "SecretKey": "SAVED_Ali_Secret", + }, + "DNSPodDns": { + "ID": "SAVED_DP_Id", + "Token": "SAVED_DP_Key" + }, + "CloudFlareDns": { + "E-Mail": "SAVED_CF_MAIL", + "API Key": "SAVED_CF_KEY", + }, + "GoDaddyDns": { + "Key": "SAVED_GD_Key", + "Secret": "SAVED_GD_Secret", + }, + "DNSLADns": { + "APIID": "SAVED_LA_Id", + "API密钥": "SAVED_LA_Key" + } + } + res = {} + for rule_name, rule in rule_map.items(): + tmp = {} + for r_key, r_value in rule.items(): + account_res = re.search(r_value + r"\s*=\s*'(.+)'", account) + if account_res: + tmp[r_key] = account_res.groups()[0] + + if len(tmp) == len(rule): + res[rule_name] = tmp + return res + + @property + def config(self) -> dict: + if self._config is not None: + return self._config + + change = False + if not os.path.exists(self.CONF_FILE): + change = True + old_config = self._read_config_old() + if old_config is not None: + self._config = old_config + else: + self._config = {} + + l_data = self._get_config_data_from_letsencrypt_data() + if l_data is not None: + for tmp_conf in l_data: + key = tmp_conf["dns_name"] + if key not in self._config: + self._config[key] = [] + for v in self._config[key]: + if all([v.get(n, None) == m for n, m in tmp_conf["conf_data"].items()]): + break + else: + tmp_data = { + "ps": "配置中的默认账户", + "id": self._get_new_id(), + } + tmp_data.update(tmp_conf["conf_data"]) + self._config[key].append(tmp_data) + else: + try: + config_data = json.loads(public.readFile(self.CONF_FILE)) + except json.JSONDecodeError: + self._config = {} + else: + if isinstance(config_data, dict): + self._config = config_data + else: + self._config = {} + + acme_conf = self._get_acme_dns_api() + if acme_conf: + for key, value in acme_conf.items(): + if key not in self._config: + self._config[key] = [] + for v in self._config[key]: + if all([v.get(n, None) == m for n, m in value.items()]): + break + else: + change = True + value["ps"] = "检测到的acme_dns" + value["id"] = self._get_new_id() + self._config[key].append(value) + + if change: + self.save_config() + + return self._config + + def save_config(self): + if self._config is None: + _ = self.config + public.writeFile(self.CONF_FILE, json.dumps(self._config)) + + def get_dns_obj_by_domain(self, domain) -> BaseDns: + root, _ = extract_zone(domain) + for key, value in self.config.items(): + for dns_config in value: + if root in dns_config.get("domains", []): + return self.CLS_MAP[key].new(dns_config) + raise Exception("没有找到域名为{}的有效的DNS API密钥信息".format(domain)) + + def add_conf(self, + dns_type: str, + conf_data: List[dict], + ps: str, + domains: List[str], + force_domain: Optional[str]): + + if dns_type in self.NOT_USED_LIST: + return False, "当前DNS平台还未完全支持,请等待后续更新" + + if dns_type not in self.CLS_MAP: + return False, "不支持的DNS平台" + + f, data = self._parse_data(conf_data, dns_type) + if not f: + return False, data + + if not isinstance(domains, list): + return False, "域名参数格式错误" + + if dns_type not in self.config: + self.config[dns_type] = [] + + for v in self.config[dns_type]: + if all([v.get(n, None) == m for n, m in data.items()]): + return False, "该通行凭证已添加过" + + data["ps"] = ps + data["id"] = self._get_new_id() + root_list = self.paser_domains_list_to_root_list(domains) + all_domains = self._get_all_root(with_out=None) + for root in root_list: + if root in all_domains: + return False, "域名{}已绑定其他api账户,不能添加".format(root) + + if force_domain is not None and not isinstance(force_domain, str): + return False, "域名参数格式错误" + if force_domain is not None: + force_root = self.paser_domains_list_to_root_list([force_domain])[0] + if force_root not in root_list: + if force_root in all_domains: + self.remove_domains_by_root(force_root) + + data["domains"] = root_list + self.config[dns_type].append(data) + self.save_config() + return True, "保存成功" + + def _parse_data(self, conf_data: List[dict], dns_type: str) -> Tuple[bool, Union[dict, str]]: + data = {} + if not isinstance(conf_data, list): + return False, "参数格式错误" + for conf in conf_data: + if isinstance(conf, dict) and "name" in conf and "value" in conf: + data[conf.get("name")] = str(conf.get("value")).strip() + if not data: + return False, "参数格式错误,没有指定参数" + if dns_type == "CloudFlareDns" and len(data) == 1 and "API Token" in data: + return True, data + + for n in self.RULE_MAP[dns_type]: + if n not in data: + return False, "参数格式错误,参数名称与平台不对应" + + return True, data + + def modify_conf(self, + api_id: str, + dns_type: str, + conf_data: Optional[List[Dict]], + ps: Optional[str], + domains: Optional[List[str]], + force_domain: Optional[str]): # 强制添加的域名 + + if dns_type in self.NOT_USED_LIST: + return False, "当前DNS平台还未完全支持,请等待后续更新" + + if dns_type not in self.CLS_MAP: + return False, "不支持的DNS平台" + + target_idx = -1 + if dns_type not in self.config: + self.config[dns_type] = [] + + for idx, v in enumerate(self.config[dns_type]): + if api_id == v.get("id", None): + target_idx = idx + + if target_idx == -1: + return False, "不存在这个通行凭证" + + if conf_data is not None: + f, data = self._parse_data(conf_data, dns_type) + if not f: + return False, data + + self.config[dns_type][target_idx].update(**data) + if ps is not None: + self.config[dns_type][target_idx].update(ps=ps) + + if domains is not None and not isinstance(domains, list): + return False, "域名参数格式错误" + + if domains is not None: + root_list = self.paser_domains_list_to_root_list(domains) + all_domains = self._get_all_root(with_out=self.config[dns_type][target_idx].get("domains")) + for root in root_list: + if root in all_domains: + return False, "域名{}已绑定其他api账户,不能添加".format(root) + self.config[dns_type][target_idx]["domains"] = root_list + + if force_domain is not None and not isinstance(force_domain, str): + return False, "域名参数格式错误" + if force_domain is not None: + root = self.paser_domains_list_to_root_list([force_domain])[0] + self.remove_domains_by_root(root) + if "domains" not in self.config[dns_type][target_idx]: + self.config[dns_type][target_idx]["domains"] = [] + self.config[dns_type][target_idx]["domains"].append(root) + + self.save_config() + return True, "修改成功" + + def remove_domains_by_root(self, root: str): + for key, value in self.config.items(): + for dns_config in value: + domains = dns_config.get("domains", None) + if domains is not None and root in domains: + domains.remove(root) + + def _get_all_root(self, with_out: Optional[List[str]]) -> Set[str]: + all_domains = set( + chain(*[c.get("domains", []) for c in + chain(*[c_list for c_list in self.config.values()])] + ) + ) + if with_out is not None: + return all_domains - set(with_out) + return all_domains + + def test_domains_api(self, domains: List[str]) -> List[dict]: + res = [{}] * len(domains) + for idx, domain in enumerate(domains): + root = self.paser_domains_list_to_root_list([domain])[0] + for key, conf in self.config.items(): + for c in conf: + if root in c.get("domains", []): + res[idx] = { + "dns_name": key, + "conf": c, + "rooot": root, + "domain": domain + } + return res + + def remove_conf(self, api_id: str, dns_type: str): + if dns_type in self.NOT_USED_LIST: + return False, "当前DNS平台还未完全支持,请等待后续更新" + + if dns_type not in self.CLS_MAP: + return False, "不支持的DNS平台" + + if dns_type not in self.config: + self.config[dns_type] = [] + + target_idx = -1 + for idx, v in enumerate(self.config[dns_type]): + if api_id == v.get("id", None): + target_idx = idx + + if target_idx == -1: + return False, "不存在这个通行凭证" + + del self.config[dns_type][target_idx] + self.save_config() + return True, "删除成功" + + @classmethod + def paser_auth_to(cls, auth_to_string: str) -> Tuple[Optional[str], Optional[Dict[str, str]]]: + tmp = auth_to_string.split('|') + dns_name = tmp[0] + if dns_name not in cls.CLS_MAP: + return None, None + if len(tmp) != 3: + return None, None + + if tmp[2] == "": + key = None + secret = tmp[1] + else: + key = tmp[1] + secret = tmp[2] + + if dns_name == "CloudFlareDns" and key is None: + return "CloudFlareDns", {"API Token": secret} + elif key and secret: + return dns_name, dict(zip(cls.RULE_MAP[dns_name], [key, secret])) + return None, None + + @classmethod + def paser_domains_list_to_root_list(cls, domains_list: List[str]) -> List[str]: + res = [] + for domain in domains_list: + root, _ = extract_zone(domain) + if root in res: + continue + res.append(root) + return res + + @classmethod + def _get_config_data_from_letsencrypt_data(cls) -> Optional[List[Dict[str, Union[str, list, dict]]]]: + conf_file = "{}/config/letsencrypt.json".format(public.get_panel_path()) + if not os.path.exists(conf_file): + return None + tmp_config = public.readFile(conf_file) + try: + orders = json.loads(tmp_config)["orders"] + except (json.JSONDecodeError, KeyError): + return None + + res = {} + for order in orders: + if 'auth_type' in order and order['auth_type'] == "dns": + if order["auth_to"].find("|") == -1 or order["auth_to"].find("/") != -1: # 文件验证跳过 + continue + if order["auth_to"] in res: + tmp_conf = res[order["auth_to"]] + else: + dns_name, conf_dict = cls.paser_auth_to(order["auth_to"]) + if dns_name is None: + continue + tmp_conf = { + "dns_name": dns_name, + "conf_data": conf_dict, + "domains": [] + } + res[order["auth_to"]] = tmp_conf + root_list = order.get("domains", []) + for root in root_list: + if root not in tmp_conf["domains"]: + tmp_conf["domains"].append(root) + + if len(res) == 0: + return None + return list(res.values()) + + +class DNSApiManager: + _DEFAULT_ERROR = ( + "执行中发生了错误,您可以尝试:
                                    " + "1. 检查API通行证是否有效,或是否填写错误
                                    " + "2. 检查域名是否托管在该平台下
                                    " + "3. 检查解析值是否正确
                                    " + ) + + def __init__(self): + pass + + @staticmethod + def get_dns_api_conf(get=None): + api_init = '{}/config/dns_api_init_v2.json'.format(public.get_panel_path()) + + apis = json.loads(public.ReadFile(api_init)) + m = RealDnsMager() + result = [] + + for data in apis: + if data["name"] == "dns": + continue + if data["name"] in m.NOT_USED_LIST: + continue + if data["name"] in m.CLS_MAP: + conf_list = m.config.get(data["name"], None) + tmp = [] + if conf_list: + for conf in conf_list: + tmp_dict = { + "ps": conf.pop("ps", ""), + "domains": conf.pop("domains", []), + "id": conf.pop("id") + } + for table in data["add_table"]: + if table["fields"][0] in conf: + tmp_dict["conf"] = [{"name": f, "value": conf.get(f, "")} for f in table["fields"]] + if "conf" in tmp_dict: + tmp.append(tmp_dict) + data["data"] = tmp + result.append(data) + + return json_response(status=True, data=result) + + @staticmethod + def add_dns_api(get): + try: + dns_type = get.dns_type.strip() + ps = get.ps.strip() + conf_data = json.loads(get.pdata.strip()) + domains = json.loads(get.domains.strip()) + force_domain = None + if "force_domain" in get: + force_domain = get.force_domain.strip() + except (json.JSONDecodeError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + + f, msg = RealDnsMager().add_conf(dns_type, conf_data, ps, domains, force_domain) + return json_response(status=f, msg=msg) + + @staticmethod + def set_dns_api(get): + try: + dns_type = get.dns_type.strip() + api_id = get.api_id.strip() + ps = None + conf_data = None + domains = None + force_domain = None + if "ps" in get: + ps = get.ps.strip() + if "force_domain" in get: + force_domain = get.force_domain.strip() + if "pdata" in get: + conf_data = json.loads(get.pdata.strip()) + if "domains" in get: + domains = json.loads(get.domains.strip()) + except (json.JSONDecodeError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + try: + f, msg = RealDnsMager().modify_conf(api_id, dns_type, conf_data, ps, domains, force_domain) + return json_response(status=f, msg=msg) + except: + public.print_log(public.get_error_info()) + + @staticmethod + def remove_dns_api(get): + try: + dns_type = get.dns_type.strip() + api_id = get.api_id.strip() + except (json.JSONDecodeError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + f, msg = RealDnsMager().remove_conf(api_id, dns_type) + return json_response(status=f, msg=msg) + + @staticmethod + def remove_domain(get): + try: + domain = get.domain.strip() + except (json.JSONDecodeError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + root, _ = extract_zone(domain) + m = RealDnsMager() + m.remove_domains_by_root(root) + m.save_config() + return json_response(status=True, msg="删除成功") + + def query_dns(self, get): + domain = get.domain + dns_type = get.dns_type + res = public.query_dns(domain, dns_type) + if not res: + return json_response(status=False, msg="参数错误") + + return json_response(status=True, data=res) + + @staticmethod + def get_record_list(get): + """ + 获取解析记录列表 + """ + try: + domain = get.domain.strip() + except (json.JSONDecodeError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + + m = RealDnsMager() + try: + dns_onj = m.get_dns_obj_by_domain(domain) + return json_response(status=True, data=dns_onj.get_record_list_by_view(domain)) + except Exception as e: + return json_response(status=False, msg=str(e)) + + @classmethod + def create_record(cls, get): + """ + @创建解析记录 + """ + try: + domain = get.domain.strip() + host = get.host.strip() + r_type = get.r_type.strip() + value = get.value.strip() + ttl = int(get.ttl.strip()) + except (json.JSONDecodeError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + domain, _ = extract_zone(domain) + + try: + m = RealDnsMager() + dns_obj = m.get_dns_obj_by_domain(domain) + r_type = dns_obj.transform_record_type(r_type) + dns_obj.add_record(domain, host, r_type, value, ttl=ttl) + return json_response(status=True, msg="添加成功") + except: + # return public.returnMsg(False, public.get_error_info()) + return json_response(status=False, msg=cls._DEFAULT_ERROR) + + @classmethod + def delete_record(cls, get): + """ + @删除解析记录 + @domain String 解析记录所在的域名 + @recordId Int 解析记录 ID + """ + try: + domain = get.domain.strip() + host = get.host.strip() + r_type = get.r_type.strip() + except (json.JSONDecodeError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + domain, _ = extract_zone(domain) + + try: + m = RealDnsMager() + dns_obj = m.get_dns_obj_by_domain(domain) + dns_obj.remove_record(domain, host, r_type) + return json_response(status=True, msg="删除成功") + except: + # return public.returnMsg(False, public.get_error_info()) + return json_response(status=False, msg=cls._DEFAULT_ERROR) + + @staticmethod + def get_domain_list(get=None): + """ + @name 获取域名列表 + @param search 搜索关键字 + """ + res = [] + cache = DomainListCache() + cache_change = False + domain_change = False + m = RealDnsMager() + for key, value in m.config.items(): + if key in m.NOT_USED_LIST: + continue + for dns_config in value: + config_id = dns_config["id"] + dns_obj: BaseDns = m.CLS_MAP[key].new(dns_config) + cloud_domain_data = cache.get(config_id) + if cloud_domain_data is None: + try: + cloud_domain_data = dns_obj.get_domain_list() + except Exception: + public.print_log(public.get_error_info()) + cloud_domain_data = {} + cache_change = True + cache.set(config_id, cloud_domain_data) + + if "domains" not in dns_config: + dns_config['domains'] = [] + + domains = dns_config.get("domains") + for d in domains: + tmp = { + "name": d, + "api_id": config_id, + "api_type": key, + "cloud_have": False, + "ps": dns_config["ps"] + } + if d in cloud_domain_data: + tmp["cloud_have"] = True + + res.append(tmp) + + for cloud_d in cloud_domain_data.keys(): + if cloud_d not in domains: + m.remove_domains_by_root(cloud_d) # 删除其他地方的数据 + dns_config["domains"].append(cloud_d) + domain_change = True + res.append({ + "name": cloud_d, + "api_id": config_id, + "api_type": key, + "cloud_have": True, + "ps": dns_config["ps"] + }) + + if cache_change: + cache.save() + + if domain_change: + m.save_config() + + return json_response(status=True, data=res) diff --git a/mod/base/web_conf/domain_tool.py b/mod/base/web_conf/domain_tool.py new file mode 100644 index 00000000..3fe753bb --- /dev/null +++ b/mod/base/web_conf/domain_tool.py @@ -0,0 +1,335 @@ +import os +import re +from typing import Tuple, Optional, Union, List, Dict + +from .util import webserver, check_server_config, write_file, read_file, service_reload, listen_ipv6, use_http2 + + +def domain_to_puny_code(domain: str) -> str: + new_domain = '' + for dkey in domain.split('.'): + if dkey == '*' or dkey == "": + continue + # 匹配非ascii字符 + match = re.search(u"[\x80-\xff]+", dkey) + if not match: + match = re.search(u"[\u4e00-\u9fa5]+", dkey) + if not match: + new_domain += dkey + '.' + else: + new_domain += 'xn--' + dkey.encode('punycode').decode('utf-8') + '.' + if domain.startswith('*.'): + new_domain = "*." + new_domain + return new_domain[:-1] + + +def check_domain(domain: str) -> Optional[str]: + domain = domain_to_puny_code(domain) + # 判断通配符域名格式 + if domain.find('*') != -1 and domain.find('*.') == -1: + return None + + # 判断域名格式 + rep_domain = re.compile(r"^([\w\-*]{1,100}\.){1,24}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$") + if not rep_domain.match(domain): + return None + return domain + + +def is_domain(domain: str) -> bool: + domain_regex = re.compile( + r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(? Tuple[List[Tuple[str, str]], List[Dict]]: + res, error = [], [] + for i in domains: + if not i.strip(): + continue + d_list = [i.strip() for i in i.split(":")] + if len(d_list) > 1: + try: + p = int(d_list[1]) + if not (1 < p < 65535): + error.append({ + "domain": i, + "msg": "端口范围错误" + }) + continue + else: + d_list[1] = str(p) + except: + error.append({ + "domain": i, + "msg": "端口范围错误" + }) + continue + else: + d_list.append("80") + d, p = d_list + d = check_domain(d) + if isinstance(d, str): + res.append((d, p)), + continue + error.append({ + "domain": i, + "msg": "域名格式错误" + }) + + res = list(set(res)) + return res, error + + +class NginxDomainTool: + ng_vhost = "/www/server/panel/vhost/nginx" + + def __init__(self, conf_prefix: str = ""): + self.conf_prefix = conf_prefix + + # 在给定的配置文件中添加端口 + @staticmethod + def nginx_add_port_by_config(conf, *port: str, is_http3=False) -> str: + ports = set() + for p in port: + ports.add(p) + + # 设置端口 + rep_port = re.compile(r"\s*listen\s+[\[\]:]*(?P[0-9]+)(?P\s*default_server)?.*;[^\n]*\n", re.M) + use_ipv6 = listen_ipv6() + last_port_idx = None + need_remove_port_idx = [] + had_ports = set() + is_default_server = False + for tmp_res in rep_port.finditer(conf): + last_port_idx = tmp_res.end() + if tmp_res.group("ds") and tmp_res.group("ds").strip(): + is_default_server = True + if tmp_res.group("port") in ports: + had_ports.add(tmp_res.group("port")) + elif tmp_res.group("port") != "443": + need_remove_port_idx.append((tmp_res.start(), tmp_res.end())) + + if not last_port_idx: + last_port_idx = re.search(r"server\s*\{\s*?\n", conf).end() + + need_add_ports = ports - had_ports + d_s = " default_server" if is_default_server else "" + h2 = " http2" if use_http2() else "" + if need_add_ports or is_http3: + listen_add_list = [] + for p in need_add_ports: + if p == "443": + tmp = " listen 443 ssl{}{};\n".format(h2, d_s) + if use_ipv6: + tmp += " listen [::]:443 ssl{}{};\n".format(h2, d_s) + listen_add_list.append(tmp) + continue + + tmp = " listen {}{};\n".format(p, d_s) + if use_ipv6: + tmp += " listen [::]:{}{};\n".format(p, d_s) + listen_add_list.append(tmp) + + if is_http3 and "443" in (had_ports | had_ports): + listen_add_list.append(" listen 443 quic{};\n".format(d_s)) + if use_ipv6: + listen_add_list.append(" listen [::]:443 quic{};\n".format(d_s)) + + new_conf = conf[:last_port_idx] + "".join(listen_add_list) + conf[last_port_idx:] + return new_conf + return conf + + # 将站点配置的域名和端口,写到配置文件中 + def nginx_set_domain(self, site_name, *domain: Tuple[str, str]) -> Optional[str]: + ng_file = '{}/{}{}.conf'.format(self.ng_vhost, self.conf_prefix, site_name) + ng_conf = read_file(ng_file) + if not ng_conf: + return "nginx配置文件丢失" + + domains_set, ports = set(), set() + for d, p in domain: + domains_set.add(d) + ports.add(p) + + # 设置域名 + rep_server_name = re.compile(r"\s*server_name\s*(.*);", re.M) + new_conf = rep_server_name.sub("\n server_name {};".format(" ".join(domains_set)), ng_conf, 1) + + # 设置端口 + rep_port = re.compile(r"\s*listen\s+[\[\]:]*(?P[0-9]+)(?P\s*default_server)?.*;[^\n]*\n", re.M) + use_ipv6 = listen_ipv6() + last_port_idx = None + need_remove_port_idx = [] + had_ports = set() + is_default_server = False + for tmp_res in rep_port.finditer(new_conf): + last_port_idx = tmp_res.end() + if tmp_res.group("ds") is not None and tmp_res.group("ds").strip(): + is_default_server = True + if tmp_res.group("port") in ports: + had_ports.add(tmp_res.group("port")) + elif tmp_res.group("port") != "443": + need_remove_port_idx.append((tmp_res.start(), tmp_res.end())) + + if not last_port_idx: + last_port_idx = re.search(r"server\s*\{\s*?\n", new_conf).end() + + ports = ports - had_ports + if ports: + d_s = " default_server" if is_default_server else "" + listen_add_list = [] + for p in ports: + tmp = " listen {}{};\n".format(p, d_s) + if use_ipv6: + tmp += " listen [::]:{}{};\n".format(p, d_s) + listen_add_list.append(tmp) + + new_conf = new_conf[:last_port_idx] + "".join(listen_add_list) + new_conf[last_port_idx:] + + # 移除多余的port监听: + # 所有遍历的索引都在 last_port_idx 之前,所有不会影响之前的修改 ↑ + if need_remove_port_idx: + conf_list = [] + idx = 0 + for start, end in need_remove_port_idx: + conf_list.append(new_conf[idx:start]) + idx = end + conf_list.append(new_conf[idx:]) + new_conf = "".join(conf_list) + + # 保存配置文件 + write_file(ng_file, new_conf) + web_server = webserver() + if web_server == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return "配置失败" + if web_server == "nginx": + service_reload() + + +class ApacheDomainTool: + ap_vhost = "/www/server/panel/vhost/apache" + ap_path = "/www/server/apache" + + def __init__(self, conf_prefix: str = ""): + self.conf_prefix = conf_prefix + + # 将站点配置的域名和端口,写到配置文件中 + def apache_set_domain(self, + site_name, # 站点名称 + *domain: Tuple[str, str], # 域名列表,可以为多个 + template_path: Optional[str] = None, # 在新加端口时使用一个模板作为添加内容 + template_kwargs: Optional[dict] = None, # 在使用一个模板时的填充参数, + ) -> Optional[str]: + """ + template_path: 在新加端口时使用一个模板作为添加内容 + template_kwargs: 在使用一个模板时的填充参数 + port domains server_admin server_name 四个参数会自动生成并填充 + 没有传入 template_path 将会复制第一个虚拟机(VirtualHost)配置 + """ + ap_file = '{}/{}{}.conf'.format(self.ap_vhost, self.conf_prefix, site_name) + ap_conf: str = read_file(ap_file) + if not ap_conf: + return "nginx配置文件丢失" + + domains, ports = set(), set() + for i in domain: + domains.add(str(i[0])) + ports.add(str(i[1])) + + domains_str = " ".join(domains) + + # 设置域名 + rep_server_name = re.compile(r"\s*ServerAlias\s*(.*)\n", re.M) + new_conf = rep_server_name.sub("\n ServerAlias {}\n".format(domains_str), ap_conf) + + tmp_template_res = re.search(r"", new_conf) + if not tmp_template_res: + tmp_template = None + else: + tmp_template = tmp_template_res.group() + + rep_ports = re.compile(r"\d+)+\s*>") + need_remove_port = [] + for tmp in rep_ports.finditer(new_conf): + if tmp.group("port") in ports: + ports.remove(tmp.group("port")) + elif tmp.group("port") != "443": + need_remove_port.append(tmp.group("port")) + + if need_remove_port: + for i in need_remove_port: + tmp_rep = re.compile(r"".format(p), tmp_template, 1)) + + new_conf += "\n" + "\n".join(other_config_body_list) + write_file(ap_file, new_conf) + # 添加端口 + self.apache_add_ports(*ports) + web_server = webserver() + if web_server == "apache" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return "配置失败" + + if web_server == "apache": + service_reload() + + # 添加apache主配置文件中的端口监听 + @classmethod + def apache_add_ports(cls, *ports: Union[str, int]) -> None: + real_ports = set() + for p in ports: + real_ports.add(str(p)) + + ssl_conf_file = '{}/conf/extra/httpd-ssl.conf'.format(cls.ap_path) + if os.path.isfile(ssl_conf_file): + ssl_conf = read_file(ssl_conf_file) + if isinstance(ssl_conf, str) and ssl_conf.find('Listen 443') != -1: + ssl_conf = ssl_conf.replace('Listen 443', '') + write_file(ssl_conf_file, ssl_conf) + + ap_conf_file = '{}/conf/httpd.conf'.format(cls.ap_path) + if not os.path.isfile(ap_conf_file): + return + ap_conf = read_file(ap_conf_file) + if ap_conf is None: + return + + rep_ports = re.compile(r"Listen\s+(?P[0-9]+)\n", re.M) + last_idx = None + for key in rep_ports.finditer(ap_conf): + last_idx = key.end() + if key.group("port") in real_ports: + real_ports.remove(key.group("port")) + + if not last_idx: + return + new_conf = ap_conf[:last_idx] + "\n".join(["Listen %s" % i for i in real_ports]) + "\n" + ap_conf[last_idx:] + write_file(ap_conf_file, new_conf) diff --git a/mod/base/web_conf/ip_restrict.py b/mod/base/web_conf/ip_restrict.py new file mode 100644 index 00000000..72c5681d --- /dev/null +++ b/mod/base/web_conf/ip_restrict.py @@ -0,0 +1,326 @@ +import os +import re +import json +from typing import Tuple, Optional, Union +from ipaddress import ip_address + +from .util import webserver, check_server_config, write_file, read_file, DB, service_reload +from mod.base import json_response + + +class _BaseRestrict: + def __init__(self, config_file: str, site_name: str): + self._conf_file = config_file + self._conf = self._read_conf() + self.site_name = site_name + + def _read_conf(self): + default_conf = { + "restrict_type": "closed", + "black_list": [], + "white_list": [] + } + + if not os.path.exists(self._conf_file): + return default_conf + try: + conf = json.loads(read_file(self._conf_file)) + except: + conf = default_conf + return conf + + def to_view(self): + return self._conf + + +class _IpRestrict(_BaseRestrict): + def __init__(self, site_name: str, config_prefix: str): + setup_path = "/www/server/panel" + ip_restrict_conf_dir = "{}/data/ip_restrict_data".format(setup_path) + if not os.path.exists(ip_restrict_conf_dir): + os.makedirs(ip_restrict_conf_dir) + super().__init__("{}/{}{}".format(ip_restrict_conf_dir, config_prefix, site_name), site_name) + self.config_prefix = config_prefix + self.nginx_sub_file = "{}/vhost/ip-restrict/{}{}.conf".format(setup_path, self.config_prefix, self.site_name) + + @property + def restrict_type(self): + return self._conf.get("restrict_type", "black") + + @restrict_type.setter + def restrict_type(self, data: str): + if data in ("black", "white", "closed"): + self._conf["restrict_type"] = data + + @property + def black_list(self): + return self._conf.get("black_list", []) + + @black_list.setter + def black_list(self, list_data: list): + self._conf["black_list"] = list_data + + @property + def white_list(self): + return self._conf.get("white_list", []) + + @white_list.setter + def white_list(self, list_data: list): + self._conf["white_list"] = list_data + + def save(self) -> Tuple[bool, str]: + if not self._conf: # 没有的时候不操作 + return True, "操作成功" + write_file(self._conf_file, json.dumps(self._conf)) + + if self.restrict_type == "closed": + write_file(self.nginx_sub_file, "") + service_reload() + return True, "操作成功" + + tmp_conf = [] + if self.restrict_type == "white": + for i in self.white_list: + tmp_conf.append("allow {};".format(i)) + + tmp_conf.append("deny all; # 除开上述IP外,其他IP全部禁止访问") + elif self.restrict_type == "black": + for i in self.black_list: + tmp_conf.append("deny {};".format(i)) + else: + raise ValueError("错误的类型,无法操作") + + write_file(self.nginx_sub_file, "\n".join(tmp_conf)) + error_msg = check_server_config() + if error_msg is not None: + write_file(self.nginx_sub_file, "") + return False, "操作失败" + service_reload() + return True, "操作成功" + + # 删除网站时调用,删除配置文件 + def remove_config_for_remove_site(self): + if os.path.isfile(self.nginx_sub_file): + os.remove(self.nginx_sub_file) + + if os.path.isfile(self._conf_file): + os.remove(self._conf_file) + + +class RealIpRestrict: + + def __init__(self, config_prefix: str = ""): + self.config_prefix = config_prefix + self.web_server = webserver() + + # 获取某个站点的IP黑白名单详情 + def restrict_conf(self, site_name: str) -> Tuple[bool, Union[str, dict]]: + if self.web_server != "nginx": + return False, "不支持除nginx之外的服务器" + ip_conf = _IpRestrict(site_name, self.config_prefix) + if not self._get_status_in_nginx_conf(ip_conf): + ip_conf.restrict_type = "closed" + return True, ip_conf.to_view() + + # 从配置文件中获取状态 + def _get_status_in_nginx_conf(self, ip_conf: _IpRestrict) -> bool: + setup_path = "/www/server/panel" + ng_file = "{}/vhost/nginx/{}{}.conf".format(setup_path, self.config_prefix, ip_conf.site_name) + rep_include = re.compile(r"\sinclude +.*/ip-restrict/.*\.conf;", re.M) + ng_conf = read_file(ng_file) + if not isinstance(ng_conf, str): + return False + if rep_include.search(ng_conf): + return True + return False + + def _set_nginx_include(self, ip_conf: _IpRestrict) -> Tuple[bool, str]: + setup_path = "/www/server/panel" + ng_file = "{}/vhost/nginx/{}{}.conf".format(setup_path, self.config_prefix, ip_conf.site_name) + if not os.path.exists(os.path.dirname(ip_conf.nginx_sub_file)): + os.makedirs(os.path.dirname(ip_conf.nginx_sub_file), 0o600) + if not os.path.isfile(ip_conf.nginx_sub_file): + write_file(ip_conf.nginx_sub_file, "") + + ng_conf = read_file(ng_file) + if not isinstance(ng_conf, str): + return False, "nginx配置文件读取失败" + + rep_include = re.compile(r"\s*include\s+.*/ip-restrict/.*\.conf;", re.M) + if rep_include.search(ng_conf): + return True, "" + + _include_str = ( + "\n #引用IP黑白名单规则,注释后配置的IP黑白名单将无效\n" + " include {};" + ).format(ip_conf.nginx_sub_file) + + rep_redirect_include = re.compile(r"\s*include\s+.*/redirect/.*\.conf;", re.M) # 如果有重定向,添加到重定向之后 + redirect_include_res = rep_redirect_include.search(ng_conf) + if redirect_include_res: + new_conf = ng_conf[:redirect_include_res.end()] + _include_str + ng_conf[redirect_include_res.end():] + else: + if "#SSL-END" not in ng_conf: + return False, "添加配置失败,无法定位SSL相关配置的位置" + + new_conf = ng_conf.replace("#SSL-END", "#SSL-END" + _include_str) + write_file(ng_file, new_conf) + if self.web_server == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return False, "添加配置失败" + + return True, "" + + def set_ip_restrict(self, site_name: str, set_type: str) -> Tuple[bool, str]: + ip_restrict = _IpRestrict(site_name, self.config_prefix) + if set_type not in ("black", "white", "closed"): + return False, "不支持的类型【{}】".format(set_type) + ip_restrict.restrict_type = set_type + f, msg = self._set_nginx_include(ip_restrict) + if not f: + return False, msg + + return ip_restrict.save() + + def add_black_ip_restrict(self, site_name: str, *ips: str) -> Tuple[bool, str]: + try: + for ip in ips: + _ = ip_address(ip) # 引发valueError + except ValueError: + return False, "ip参数解析错误" + ip_restrict = _IpRestrict(site_name, self.config_prefix) + black_list = ip_restrict.black_list + for i in ips: + if i not in black_list: + black_list.append(i) + + ip_restrict.black_list = black_list + f, msg = self._set_nginx_include(ip_restrict) + if not f: + return False, msg + + return ip_restrict.save() + + def remove_black_ip_restrict(self, site_name: str, *ips: str): + ip_restrict = _IpRestrict(site_name, self.config_prefix) + black_list = ip_restrict.black_list + for i in ips: + if i in black_list: + black_list.remove(i) + + ip_restrict.black_list = black_list + f, msg = self._set_nginx_include(ip_restrict) + if not f: + return False, msg + + return ip_restrict.save() + + def add_white_ip_restrict(self, site_name: str, *ips: str) -> Tuple[bool, str]: + try: + for ip in ips: + _ = ip_address(ip) # 引发valueError + except ValueError: + return False, "ip参数解析错误" + ip_restrict = _IpRestrict(site_name, self.config_prefix) + white_list = ip_restrict.white_list + for i in ips: + if i not in white_list: + white_list.append(i) + + ip_restrict.white_list = white_list + f, msg = self._set_nginx_include(ip_restrict) + if not f: + return False, msg + + return ip_restrict.save() + + def remove_white_ip_restrict(self, site_name: str, *ips: str) -> Tuple[bool, str]: + ip_restrict = _IpRestrict(site_name, self.config_prefix) + white_list = ip_restrict.white_list + for i in ips: + if i in white_list: + white_list.remove(i) + + ip_restrict.white_list = white_list + + return ip_restrict.save() + + def remove_site_ip_restrict_info(self, site_name: str): + ip_restrict = _IpRestrict(site_name, self.config_prefix) + ip_restrict.remove_config_for_remove_site() + + +class IpRestrict: + + def __init__(self, config_prefix: str = ""): + self.config_prefix = config_prefix + self._ri = RealIpRestrict(self.config_prefix) + + # 获取ip控制信息 + def restrict_conf(self, get): + try: + site_name = get.site_name.strip() + except (AttributeError, json.JSONDecodeError): + return json_response(status=False, msg="参数错误") + + f, d = self._ri.restrict_conf(site_name) + if not f: + return json_response(status=f, msg=d) + return json_response(status=f, data=d) + + # 设置ip黑白名单状态 + def set_ip_restrict(self, get): + try: + site_name = get.site_name.strip() + set_ip_restrict = get.set_type.strip() + except (AttributeError, json.JSONDecodeError): + return json_response(status=False, msg="参数错误") + + f, m = self._ri.set_ip_restrict(site_name, set_ip_restrict) + return json_response(status=f, msg=m) + + # 添加黑名单 + def add_black_ip_restrict(self, get): + try: + site_name = get.site_name.strip() + value = get.value.strip() + except AttributeError: + return json_response(status=False, msg="参数错误") + + f, m = self._ri.add_black_ip_restrict(site_name, value) + return json_response(status=f, msg=m) + + # 移除黑名单 + def remove_black_ip_restrict(self, get): + try: + site_name = get.site_name.strip() + value = get.value.strip() + except (AttributeError, json.JSONDecodeError): + return json_response(status=False, msg="参数错误") + + f, m = self._ri.remove_black_ip_restrict(site_name, value) + return json_response(status=f, msg=m) + + # 添加白名单 + def add_white_ip_restrict(self, get): + try: + site_name = get.site_name.strip() + value = get.value.strip() + except (AttributeError, json.JSONDecodeError): + return json_response(status=False, msg="参数错误") + + f, m = self._ri.add_white_ip_restrict(site_name, value) + return json_response(status=f, msg=m) + + # 移除白名单 + def remove_white_ip_restrict(self, get): + try: + site_name = get.site_name.strip() + value = get.value.strip() + except (AttributeError, json.JSONDecodeError): + return json_response(status=False, msg="参数错误") + + f, m = self._ri.remove_white_ip_restrict(site_name, value) + return json_response(status=f, msg=m) + + diff --git a/mod/base/web_conf/limit_net.py b/mod/base/web_conf/limit_net.py new file mode 100644 index 00000000..13605201 --- /dev/null +++ b/mod/base/web_conf/limit_net.py @@ -0,0 +1,238 @@ +import os +import re +from typing import Tuple, Union + +from .util import webserver + + +class LimitNet(object): + + def get_limit_net(self, get) -> Union[bool, str]: + if webserver() != 'nginx': + return False, "" + try: + site_id = int(get.site_id) + except (AttributeError, TypeError, ValueError): + return public.returnMsg(False, "参数错误") + + if self.config_prefix is None: + return public.returnMsg(False, "不支持的网站类型") + + # 取配置文件 + 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, "配置文件读取错误") + + # 站点总并发 + data = { + 'perserver': 0, + 'perip': 0, + 'limit_rate': 0, + } + + rep_per_server = re.compile(r"(?P.*)limit_conn +perserver +(?P\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.*)limit_conn +perip +(?P\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.*)limit_rate +(?P\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, "全局配置缓存配置失败" + 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, "全局配置缓存配置失败" + 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, "参数错误") + + if per_server < 1 or perip < 1 or limit_rate < 1: + return public.returnMsg(False, '并发限制,IP限制,流量限制必需大于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:
                                    ' + is_error.replace("\n", '
                                    ') + '
                                    ') + + public.serviceReload() + public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_OPEN_SUCCESS', (site_name,)) + return public.returnMsg(True, 'SET_SUCCESS') + + # 关闭流量限制 + 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:
                                    ' + is_error.replace("\n", '
                                    ') + '
                                    ') + public.serviceReload() + public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_CLOSE_SUCCESS', (site_name,)) + return public.returnMsg(True, 'SITE_NETLIMIT_CLOSE_SUCCESS') diff --git a/mod/base/web_conf/logmanager.py b/mod/base/web_conf/logmanager.py new file mode 100644 index 00000000..4739189d --- /dev/null +++ b/mod/base/web_conf/logmanager.py @@ -0,0 +1,855 @@ +import os +import re +import json +import sys +from typing import Tuple, Optional, Union, List +from .util import webserver, check_server_config, write_file, read_file, DB, service_reload, get_log_path, pre_re_key +from mod.base import json_response + + +class _BaseLogFormat: + panel_path = "/www/server/panel" + + def __init__(self): + self._config_file = "" + self._config: Optional[dict] = None + self._format_dict = None + self._log_format_dir = '' + + @property + def config(self) -> dict: + if self._config is None: + try: + self._config = json.loads(read_file(self._config_file)) + except (json.JSONDecodeError, TypeError, ValueError): + self._config = {} + return self._config + + def save_config(self): + if self._config is not None: + write_file(self._config_file, json.dumps(self._config)) + + @property + def log_format(self) -> dict: + raise NotImplementedError() + + def check_config(self, name: str, keys: List[str], space_character=None) -> Optional[str]: + if space_character and len(space_character) > 4: + return "间隔符过长,请输入小于4位的间隔符" + rep_name = re.compile(r"^\w+$") + if rep_name.match(name) is None: + return "名称只能包含数字、字母和下划线" + if name in ("combined", "main"): + return "请勿使用默认名称" + error_key = [] + for k in keys: + if k not in self.log_format: + error_key.append(k) + if error_key: + return "无法识别以下日志关键字:【{}】".format(",".join(error_key)) + + # 添加日志格式 + def add_log_format(self, name: str, keys: List[str], space_character=" ") -> Optional[str]: + error_msg = self.check_config(name, keys, space_character) + if error_msg: + return error_msg + if name in self.config: + return "该名称的日志格式已存在" + error_msg = self._set_to_config(name, keys, space_character, is_modify=False) + if error_msg: + return error_msg + + self.config[name] = {"keys": keys, "space_character": space_character, "sites": []} + self.save_config() + service_reload() + return None + + # 修改日志格式 + def modify_log_format(self, name: str, keys: List[str], space_character=None) -> Optional[str]: + error_msg = self.check_config(name, keys, space_character) + if error_msg: + return error_msg + if name not in self.config: + return "该名称的日志格式不存在" + + self.config[name]["keys"] = keys + if space_character: + self.config[name]["space_character"] = space_character + else: + space_character = self.config[name]["space_character"] + + error_msg = self._set_to_config(name, keys, space_character, is_modify=True) + if error_msg: + return error_msg + self.save_config() + service_reload() + return None + + # 删除日志格式 + def remove_log_format(self, name: str) -> Optional[str]: + if name not in self.config: + return "该名称的日志格式不存在" + if len(self.config[name].get("sites", [])) > 1: + return "该日志格式在【{}】网站中正在使用,请先移除".format(",".join(self.config[name]["sites"])) + self._remove_form_config(name) + + del self.config[name] + self.save_config() + service_reload() + return None + + def _set_to_config(self, name: str, keys: List[str], space_character, is_modify=False) -> Optional[str]: + raise NotImplementedError + + def _remove_form_config(self, name) -> None: + conf_file = self._log_format_dir + "/{}_format.conf".format(name) + if os.path.isfile(conf_file): + os.remove(conf_file) + + # 在配置文件中设置日志格式, log_format_name传入空字符串时,设置会默认 + def set_site_log_format_in_config(self, site_name, log_format_name, conf_prefix, mutil=False) -> Optional[str]: + """ + mutil 为True时,不会自动重载配置 + """ + raise NotImplementedError() + + # 设置日志格式 + def set_site_log_format(self, site_name, log_format_name, conf_prefix, mutil=False) -> Optional[str]: + if log_format_name not in self.config and log_format_name != "": + return "该名称的日志格式不存在" + error_msg = self.set_site_log_format_in_config(site_name, log_format_name, conf_prefix, mutil=mutil) + if error_msg is not None: + return error_msg + if "sites" not in self.config[log_format_name]: + self.config[log_format_name]["sites"] = [] + for name, sub_conf in self.config.items(): + if name == log_format_name: + sub_conf["sites"].append(site_name) # 记录到配置文件中 + + if site_name in sub_conf.get("sites", []): + sub_conf["sites"].remove(site_name) # 如果之前使用了其他的配置,则移除其他配置中的这个站点的关联 + + self.save_config() + + +class _NgLog(_BaseLogFormat): + + @property + def log_format(self) -> dict: + if self._format_dict is None: + self._format_dict = { + "server_addr": { + "name": "服务器地址", + "key": "$server_addr", + }, + "server_port": { + "name": "服务器端口", + "key": "$server_port", + }, + "host": { + "name": "域名", + "key": "$http_host", + }, + "remote_addr": { + "name": "客户端地址", + "key": "$server_addr", + }, + "remote_port": { + "name": "客户端端口", + "key": "$server_addr", + }, + "protocol": { + "name": "服务器协议", + "key": "$server_protocol", + }, + "req_length": { + "name": "请求长度", + "key": "$request_length", + }, + "method": { + "name": "请求方法", + "key": "$request_method", + }, + "uri": { + "name": "请求uri", + "key": "$request_uri", + }, + "status": { + "name": "状态码", + "key": "$status", + }, + "sent_bytes": { + "name": "发送字节数", + "key": "$body_bytes_sent", + }, + "referer": { + "name": "来源地址", + "key": "$http_referer", + }, + "user_agent": { + "name": "用户代理(User-Agent)", + "key": "$http_user_agent", + }, + "take_time": { + "name": "请求用时", + "key": "$request_time", + }, + } + return self._format_dict + + def __init__(self): + super().__init__() + self._config_file = "{}/data/ng_log_format.json".format(self.panel_path) + self._log_format_dir = "{}/vhost/nginx/log_format".format(self.panel_path) + + def _set_log_format_include(self) -> Optional[str]: + config_file = "/www/server/nginx/conf/nginx.conf" + config_data = read_file(config_file) + if not config_data: + return "配置文件丢失无法操作" + if not os.path.isdir(self._log_format_dir): + os.makedirs(self._log_format_dir) + rep_include = re.compile(r"include\s+/www/server/panel/vhost/nginx/log_format/\*\.conf\s*;") + if rep_include.search(config_data): + return + + rep_http = re.compile(r"\s*http\s*\{[^\n]*\n") + res = rep_http.search(config_data) + if not res: + return "主配置文件中缺少http配置项,无法添加" + include_str = "include {}/*.conf;\n".format(self._log_format_dir) + new_conf = config_data[:res.end()] + include_str + config_data[res.end():] + write_file(config_file, new_conf) + + def _set_to_config(self, name: str, keys: List[str], space_character, is_modify=False) -> Optional[str]: + error_msg = self._set_log_format_include() + if error_msg: + return error_msg + conf_file = self._log_format_dir + "/{}_format.conf".format(name) + write_file(conf_file, ( + "log_format {} '{}';".format(name, space_character.join(map(lambda x: self.log_format[x]["key"], keys))) + )) + + def set_site_log_format_in_config(self, site_name, log_format_name, conf_prefix, mutil=False) -> Optional[str]: + """ + mutil 为True时,不会自动重载配置 + """ + config_file = "{}/vhost/nginx/{}{}.conf".format(self.panel_path, conf_prefix, site_name) + config_data = read_file(config_file) + if not config_data: + return "配置文件丢失无法操作" + + start_idx, end_idx = self.get_first_server_log_idx(config_data) + if start_idx: + rep_access_log = re.compile(r"\s*access_log\s+(?P[^;\s]*)(\s+(?P\w+))?;") + res = rep_access_log.search(config_data[start_idx: end_idx]) + if res.group("name") == log_format_name: + return + new_access_log = "\n access_log {} {};".format(res.group("path"), log_format_name) + new_conf = config_data[:start_idx] + new_access_log + config_data[end_idx:] + else: + last_server_idx = config_data.rfind("}") # server 范围内最后一个}的位置 + if last_server_idx == -1: + return "配置文件格式错误无法操作" + log_path = "{}/{}.log".format(get_log_path(), site_name) + new_access_log = "\n access_log {} {};\n".format(log_path, log_format_name) + new_conf = config_data[:last_server_idx] + new_access_log + config_data[last_server_idx:] + write_file(config_file, new_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(config_file, config_data) + return "配置修改失败" + if webserver() == "nginx" and not mutil: + service_reload() + + # 获取配置文件中server等级的第一个access_log的位置 + @staticmethod + def get_first_server_log_idx(config_data) -> Tuple[Optional[int], Optional[int]]: + rep_server = re.compile(r"\s*server\s*\{") + res = rep_server.search(config_data) + if res is None: + return None, None + rep_log = re.compile(r"\s*access_log\s+(?P[^;\s]*)(\s+(?P\w+))?;", re.M) + s_idx = res.end() + l_n = 1 + length = len(config_data) + while l_n > 0: + next_l = config_data[s_idx:].find("{") + next_r = config_data[s_idx:].find("}") + if next_l == -1 and next_r == -1: # 都没有了跳过 + return None, None + if next_r == -1 and next_l != -1: # 还剩 { 但是没有 } ,跳过 + return None, None + if next_l == -1: + next_l = length + if next_l < next_r: + if l_n == 1: + res = rep_log.search(config_data[s_idx: s_idx + next_l]) + if res: + return s_idx + res.start(), s_idx + res.end() + l_n += 1 + else: + l_n -= 1 + if l_n == 0: + res = rep_log.search(config_data[s_idx: s_idx + next_l]) + if res: + return s_idx + res.start(), s_idx + res.end() + s_idx += min(next_l, next_r) + 1 + return None, None + + # 设置站点的日志路径 + def set_site_log_path(self, site_name, site_log_path, conf_prefix, mutil=False) -> Optional[str]: + if not os.path.isdir(site_log_path): + return "不是一个存在的文件夹路径" + + if site_log_path[-1] == "/": + site_log_path = site_log_path[:-1] + + # nginx + nginx_config_path = '/www/server/panel/vhost/nginx/{}{}.conf'.format(conf_prefix, site_name) + nginx_config = read_file(nginx_config_path) + if not nginx_config: + return "网站配置文件丢失,无法配置" + + # nginx + old_log_file = self.nginx_get_log_file_path(nginx_config, site_name, is_error_log=False) + old_error_log_file = self.nginx_get_log_file_path(nginx_config, site_name, is_error_log=True) + + if old_log_file and old_error_log_file: + new_nginx_conf = nginx_config + log_file_rep = re.compile(r"access_log +" + pre_re_key(old_log_file)) + error_log_file_rep = re.compile(r"error_log +" + pre_re_key(old_error_log_file)) + if log_file_rep.search(nginx_config): + new_nginx_conf = log_file_rep.sub("access_log {}/{}.log".format(site_log_path, site_name), + new_nginx_conf, 1) + + if error_log_file_rep.search(nginx_config): + new_nginx_conf = error_log_file_rep.sub("error_log {}/{}.error.log".format(site_log_path, site_name), + new_nginx_conf, 1) + + write_file(nginx_config_path, new_nginx_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(nginx_config_path, nginx_config) + return "配置修改失败" + if webserver() == "nginx" and not mutil: + service_reload() + + else: + return "未找到日志配置,无法操作" + + @staticmethod + def nginx_get_log_file_path(nginx_config: str, site_name: str, is_error_log: bool = False): + log_file = None + if is_error_log: + re_data = re.findall(r"error_log +(/(\S+/?)+) ?(.*?);", nginx_config) + else: + re_data = re.findall(r"access_log +(/(\S+/?)+) ?(.*?);", nginx_config) + if re_data is None: + log_file = None + else: + for i in re_data: + file_path = i[0].strip(";") + if file_path != "/dev/null" and not file_path.endswith("purge_cache.log"): + if os.path.isdir(os.path.dirname(file_path)): + log_file = file_path + break + + logsPath = '/www/wwwlogs/' + if log_file is None: + if is_error_log: + log_file = logsPath + site_name + '.log' + else: + log_file = logsPath + site_name + '.error.log' + if not os.path.isfile(log_file): + log_file = None + + return log_file + + def get_site_log_path(self, site_name, conf_prefix) -> Union[str, dict]: + config_path = '/www/server/panel/vhost/nginx/{}{}.conf'.format(conf_prefix, site_name) + config = read_file(config_path) + if not config: + return "站点配置文件丢失" + log_file = self.nginx_get_log_file_path(config, site_name, is_error_log=False) + error_log_file = self.nginx_get_log_file_path(config, site_name, is_error_log=False) + if not (error_log_file and log_file): + return "获取失败" + return { + "log_file": log_file, + "error_log_file": error_log_file, + } + + def close_access_log(self, site_name, conf_prefix) -> Optional[str]: + nginx_config_path = '/www/server/panel/vhost/nginx/{}{}.conf'.format(conf_prefix, site_name) + nginx_config = read_file(nginx_config_path) + if not nginx_config: + return "网站配置文件丢失,无法配置" + + start_idx, end_idx = self.get_first_server_log_idx(nginx_config) + if not start_idx: + return None + new_conf = nginx_config + + while start_idx is not None: + new_conf = new_conf[:start_idx] + '# ' + new_conf[start_idx:] + start_idx, end_idx = self.get_first_server_log_idx(new_conf) + + write_file(nginx_config_path, new_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(nginx_config_path, nginx_config) + return "配置修改失败" + + return None + + # 未完成 + def open_access_log(self, site_name, conf_prefix) -> Optional[str]: + nginx_config_path = '/www/server/panel/vhost/nginx/{}{}.conf'.format(conf_prefix, site_name) + nginx_config = read_file(nginx_config_path) + if not nginx_config: + return "网站配置文件丢失,无法配置" + + new_conf = nginx_config.replace("#") + + write_file(nginx_config_path, new_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(nginx_config_path, nginx_config) + return "配置修改失败" + + return None + + def access_log_is_open(self, site_name, conf_prefix) -> bool: + nginx_config_path = '/www/server/panel/vhost/nginx/{}{}.conf'.format(conf_prefix, site_name) + nginx_config = read_file(nginx_config_path) + if not nginx_config: + return False + + start_idx, end_idx = self.get_first_server_log_idx(nginx_config) + return start_idx is not None + + +class _ApLog(_BaseLogFormat): + + def set_site_log_format_in_config(self, site_name, log_format_name, conf_prefix, mutil=False) -> Optional[str]: + if log_format_name == "": + log_format_name = "combined" + config_file = "{}/vhost/apache/{}{}.conf".format(self.panel_path, conf_prefix, site_name) + config_data = read_file(config_file) + if not config_data: + return "配置文件丢失无法操作" + + custom_log_rep = re.compile(r'''\s*CustomLog\s+['"](?P.*)['"](\s+(?P.*))?''', re.M) + new_custom_log = '\n CustomLog "{}" %s\n' % log_format_name + new_conf_list = [] + idx = 0 + for tmp_res in custom_log_rep.finditer(config_data): + new_conf_list.append(config_data[idx:tmp_res.start()]) + new_conf_list.append(new_custom_log.format(tmp_res.group("path"))) + idx = tmp_res.end() + new_conf_list.append(config_data[idx:]) + new_conf = "".join(new_conf_list) + + write_file(config_file, new_conf) + if webserver() == "apache" and check_server_config() is not None: + write_file(config_file, config_data) + return "配置修改失败" + if webserver() == "apache" and not mutil: + service_reload() + + # 设置站点的日志路径 + def set_site_log_path(self, site_name, site_log_path, conf_prefix, mutil=False) -> Optional[str]: + if not os.path.isdir(site_log_path): + return "不是一个存在的文件夹路径" + + if site_log_path[-1] == "/": + site_log_path = site_log_path[:-1] + + # apache + apache_config_path = '/www/server/panel/vhost/apache/{}{}.conf'.format(conf_prefix, site_name) + apache_config = read_file(apache_config_path) + if not apache_config: + return "网站配置文件丢失,无法配置" + + # apache + old_log_file = self.apache_get_log_file_path(apache_config, site_name, is_error_log=False) + old_error_log_file = self.apache_get_log_file_path(apache_config, site_name, is_error_log=True) + + if old_log_file and old_error_log_file: + new_apache_conf = apache_config + log_file_rep = re.compile(r'''CustomLog +['"]?''' + pre_re_key(old_log_file) + '''['"]?''') + error_log_file_rep = re.compile(r'''ErrorLog +['"]?''' + pre_re_key(old_error_log_file) + '''['"]?''') + if log_file_rep.search(apache_config): + new_apache_conf = log_file_rep.sub('CustomLog "{}/{}-access_log"'.format(site_log_path, site_name), + new_apache_conf) + + if error_log_file_rep.search(apache_config): + new_apache_conf = error_log_file_rep.sub('ErrorLog "{}/{}.-error_log"'.format(site_log_path, site_name), + new_apache_conf) + write_file(apache_config_path, new_apache_conf) + print(new_apache_conf) + if webserver() == "apache" and check_server_config() is not None: + write_file(apache_config_path, apache_config) + return "配置修改失败" + if webserver() == "apache" and not mutil: + service_reload() + else: + return "未找到日志配置,无法操作" + + @staticmethod + def apache_get_log_file_path(apache_config: str, site_name: str, is_error_log: bool = False): + log_file = None + if is_error_log: + re_data = re.findall(r'''ErrorLog +['"]?(/(\S+/?)+)['"]? ?(.*?)\n''', apache_config) + else: + re_data = re.findall(r'''CustomLog +['"]?(/(\S+/?)+)['"]? ?(.*?)\n''', apache_config) + if re_data is None: + log_file = None + else: + for i in re_data: + file_path = i[0].strip('"').strip("'") + if file_path != "/dev/null": + if os.path.isdir(os.path.dirname(file_path)): + log_file = file_path + break + + logsPath = '/www/wwwlogs/' + if log_file is None: + if is_error_log: + log_file = logsPath + site_name + '-access_log' + else: + log_file = logsPath + site_name + '-error_log' + if not os.path.isfile(log_file): + log_file = None + + return log_file + + @staticmethod + def close_access_log(site_name, conf_prefix) -> Optional[str]: + apache_config_path = '/www/server/panel/vhost/apache/{}{}.conf'.format(conf_prefix, site_name) + apache_config = read_file(apache_config_path) + if not apache_config: + return "网站配置文件丢失,无法配置" + custom_log_rep = re.compile(r'''CustomLog +['"]?(/(\S+/?)+)['"]?(\s*.*)?''', re.M) + new_conf_list = [] + idx = 0 + for tmp_res in custom_log_rep.finditer(apache_config): + new_conf_list.append(apache_config[idx:tmp_res.start()]) + new_conf_list.append("# " + tmp_res.group()) + idx = tmp_res.end() + new_conf_list.append(apache_config[idx:]) + new_conf = "".join(new_conf_list) + write_file(apache_config_path, new_conf) + if webserver() == "apache" and check_server_config() is not None: + write_file(apache_config_path, apache_config) + return "配置修改失败" + return None + + @staticmethod + def open_access_log(site_name, conf_prefix) -> Optional[str]: + apache_config_path = '/www/server/panel/vhost/apache/{}{}.conf'.format(conf_prefix, site_name) + apache_config = read_file(apache_config_path) + if not apache_config: + return "网站配置文件丢失,无法配置" + new_conf = apache_config.replace("#CustomLog", "CustomLog") + write_file(apache_config_path, new_conf) + if webserver() == "apache" and check_server_config() is not None: + write_file(apache_config_path, apache_config) + return "配置修改失败" + return None + + @staticmethod + def access_log_is_open(site_name, conf_prefix) -> bool: + apache_config_path = '/www/server/panel/vhost/apache/{}{}.conf'.format(conf_prefix, site_name) + apache_config = read_file(apache_config_path) + if not apache_config: + return False + if apache_config.find("#CustomLog") != -1: + return False + return True + + + def get_site_log_path(self, site_name, conf_prefix) -> Union[str, dict]: + config_path = '/www/server/panel/vhost/apache/{}{}.conf'.format(conf_prefix, site_name) + config = read_file(config_path) + if not config: + return "站点配置文件丢失" + log_file = self.apache_get_log_file_path(config, site_name, is_error_log=False) + error_log_file = self.apache_get_log_file_path(config, site_name, is_error_log=False) + if not (error_log_file and log_file): + return "获取失败" + return { + "log_file": log_file, + "error_log_file": error_log_file, + } + + @property + def log_format(self) -> dict: + if self._format_dict is None: + self._format_dict = { + "server_addr": { + "name": "服务器地址", + "key": "%A", + }, + "server_port": { + "name": "服务器端口", + "key": "%p", + }, + "host": { + "name": "域名", + "key": "%V", + }, + "remote_addr": { + "name": "客户端地址", + "key": "%{c}a", + }, + "remote_port": { + "name": "客户端端口", + "key": "%{remote}p", + }, + "protocol": { + "name": "服务器协议", + "key": "%H", + }, + "method": { + "name": "请求方法", + "key": "%m", + }, + "uri": { + "name": "请求uri", + "key": r"\"%U\"", + }, + "status": { + "name": "状态码", + "key": "%>s", + }, + "sent_bytes": { + "name": "发送字节数", + "key": "%B", + }, + "referer": { + "name": "来源地址", + "key": r"\"%{Referer}i\"", + }, + "user_agent": { + "name": "用户代理(User-Agent)", + "key": r"\"%{User-Agent}i\"", + }, + "take_time": { + "name": "请求用时", + "key": "%{ms}T", + }, + } + return self._format_dict + + def __init__(self): + super().__init__() + self._config_file = "{}/data/ap_log_format.json".format(self.panel_path) + self._log_format_dir = "{}/vhost/apache/log_format".format(self.panel_path) + + def _set_log_format_include(self) -> Optional[str]: + config_file = "/www/server/apache/conf/httpd.conf" + config_data = read_file(config_file) + if not config_data: + return "配置文件丢失无法操作" + if not os.path.isdir(self._log_format_dir): + os.makedirs(self._log_format_dir) + rep_include = re.compile(r"IncludeOptional\s+/www/server/panel/vhost/apache/log_format/\*\.conf") + if rep_include.search(config_data): + return + new_conf = config_data + """ + + IncludeOptional /www/server/panel/vhost/apache/log_format/*.conf + +""" + write_file(config_file, new_conf) + + def _set_to_config(self, name: str, keys: List[str], space_character, is_modify=False) -> Optional[str]: + error_msg = self._set_log_format_include() + if error_msg: + return error_msg + conf_file = self._log_format_dir + "/{}_format.conf".format(name) + write_file(conf_file, ( + 'LogFormat "{}" {}'.format(space_character.join(map(lambda x: self.log_format[x]["key"], keys)), name) + )) + + +class RealLogMgr: + + def __init__(self, conf_prefix: str = ""): + self.conf_prefix = conf_prefix + if webserver() == "nginx": + self._log_format_tool = _NgLog() + else: + self._log_format_tool = _ApLog() + + @staticmethod + def remove_site_log_format_info(site_name: str): + for logtool in (_NgLog(), _ApLog()): + for _, conf in logtool.config.items(): + if site_name in conf.get("sites", []): + conf["sites"].remove(site_name) + logtool.save_config() + + def log_format_data(self, site_name: str): + log_format_data = None + for name, data in self._log_format_tool.config.items(): + if site_name in data.get("sites", []): + log_format_data = data + log_format_data.update(name=name) + return { + "log_format": log_format_data, + "rule": self._log_format_tool.log_format, + "all_log_format": self._log_format_tool.config + } + + def add_log_format(self, name: str, keys: List[str], space_character=" ") -> Optional[str]: + return self._log_format_tool.add_log_format(name, keys, space_character) + + def modify_log_format(self, name: str, keys: List[str], space_character=None) -> Optional[str]: + return self._log_format_tool.modify_log_format(name, keys, space_character) + + def remove_log_format(self, name: str) -> Optional[str]: + return self._log_format_tool.remove_log_format(name) + + # log_format_name 为空字符串时表示恢复成默认的日志格式 + def set_site_log_format(self, site_name, log_format_name, mutil=False) -> Optional[str]: + return self._log_format_tool.set_site_log_format(site_name, log_format_name, self.conf_prefix, mutil) + + def set_site_log_path(self, site_name, site_log_path, mutil=False) -> Optional[str]: + return self._log_format_tool.set_site_log_path(site_name, site_log_path, self.conf_prefix, mutil) + + def get_site_log_path(self, site_name) -> Union[str, dict]: + return self._log_format_tool.get_site_log_path(site_name, self.conf_prefix) + + @staticmethod + def site_crontab_log(site_name: str, hour: int, minute: int, save: int) -> bool: + if DB("crontab").where("sName =? and sType = ?", ("ALL", "logs")).find(): + return True + + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + import crontab + crontabs = crontab.crontab() + args = { + "name": "切割日志[{}]".format(site_name), + "type": 'day', + "where1": '', + "hour": hour, + "minute": minute, + "sName": site_name, + "sType": 'logs', + "notice": '', + "notice_channel": '', + "save": save, + "save_local": '1', + "backupTo": '', + "sBody": '', + "urladdress": '' + } + res = crontabs.AddCrontab(args) + if res and "id" in res.keys(): + return True + return False + + +class LogMgr: + + def __init__(self, conf_prefix: str = ""): + self.conf_prefix = conf_prefix + self._real_log_mgr = RealLogMgr(self.conf_prefix) + + def log_format_data(self, get): + try: + site_name = get.site_name.strip() + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + data = self._real_log_mgr.log_format_data(site_name) + return json_response(status=True, data=data) + + def add_log_format(self, get): + try: + space_character = " " + format_name = get.format_name.strip() + keys = json.loads(get.keys.strip()) + if "space_character" in get: + space_character = get.space_character + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + + msg = self._real_log_mgr.add_log_format(format_name, keys, space_character) + if isinstance(msg, str): + return json_response(status=False, msg=msg) + return json_response(status=True, msg="添加成功") + + def modify_log_format(self, get): + try: + space_character = None + format_name = get.format_name.strip() + keys = json.loads(get.keys.strip()) + if "space_character" in get: + space_character = get.space_character + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + + msg = self._real_log_mgr.modify_log_format(format_name, keys, space_character) + if isinstance(msg, str): + return json_response(status=False, msg=msg) + return json_response(status=True, msg="修改成功") + + def remove_log_format(self, get): + try: + format_name = get.format_name.strip() + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + + msg = self._real_log_mgr.remove_log_format(format_name) + if isinstance(msg, str): + return json_response(status=False, msg=msg) + return json_response(status=True, msg="删除成功") + + def set_site_log_format(self, get): + try: + format_name = get.format_name.strip() + site_name = get.site_name.strip() + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + + msg = self._real_log_mgr.set_site_log_format(site_name, log_format_name=format_name) + if isinstance(msg, str): + return json_response(status=False, msg=msg) + return json_response(status=True, msg="添加成功") + + def set_site_log_path(self, get): + try: + log_path = get.log_path.strip() + site_name = get.site_name.strip() + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + + msg = self._real_log_mgr.set_site_log_path(site_name, site_log_path=log_path) + if isinstance(msg, str): + return json_response(status=False, msg=msg) + return json_response(status=True, msg="修改路径成功") + + def get_site_log_path(self, get): + try: + site_name = get.site_name.strip() + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + + msg = self._real_log_mgr.get_site_log_path(site_name) + if isinstance(msg, str): + return json_response(status=False, msg=msg) + return json_response(status=True, data=msg) + + def site_crontab_log(self, get): + try: + site_name = get.site_name.strip() + hour = int(get.hour.strip()) + minute = int(get.minute.strip()) + save = int(get.save.strip()) + except (AttributeError, json.JSONDecodeError, TypeError, ValueError): + return json_response(status=False, msg="参数类型错误") + + msg = self._real_log_mgr.site_crontab_log(site_name, hour=hour, minute=minute, save=save) + if isinstance(msg, str): + return json_response(status=False, msg=msg) + return json_response(status=True, data=msg) diff --git a/mod/base/web_conf/proxy.py b/mod/base/web_conf/proxy.py new file mode 100644 index 00000000..b9a9d7eb --- /dev/null +++ b/mod/base/web_conf/proxy.py @@ -0,0 +1,636 @@ +import os +import re +import json +import shutil +import sys +import traceback +from hashlib import md5 +from typing import Tuple, Optional, Union, List, Dict, Any +from .util import webserver, check_server_config, write_file, read_file, DB, service_reload, get_log_path, pre_re_key +from mod.base import json_response + + +class RealProxy: + panel_path = "/www/server/panel" + _proxy_conf_file = "{}/data/mod_proxy_file.conf".format(panel_path) + + def __init__(self, config_prefix: str): + self.config_prefix: str = config_prefix + self._config: Optional[List[dict]] = None + + # { + # "proxyname": "yyy", + # "sitename": "www.12345test.com", + # "proxydir": "/", + # "proxysite": "http://www.baidu.com", + # "todomain": "www.baidu.com", + # "type": 0, + # "cache": 0, + # "subfilter": [ + # {"sub1": "", "sub2": ""}, + # {"sub1": "", "sub2": ""}, + # {"sub1": "", "sub2": ""}], + # "advanced": 1, + # "cachetime": 1 + # } + + @property + def config(self) -> List[dict]: + if self._config is None: + try: + self._config = json.loads(read_file(self._proxy_conf_file)) + except (json.JSONDecodeError, TypeError, ValueError): + self._config = [] + return self._config + + def save_config(self): + if self._config is not None: + write_file(self._proxy_conf_file, json.dumps(self._config)) + + # 检查代理是否存在 + def _check_even(self, proxy_conf: dict, is_modify) -> bool: + for i in self.config: + if i["sitename"] == proxy_conf["sitename"]: + if is_modify is False: + if i["proxydir"] == proxy_conf["proxydir"] or i["proxyname"] == proxy_conf["proxyname"]: + return True + else: + if i["proxyname"] != proxy_conf["proxyname"] and i["proxydir"] == proxy_conf["proxydir"]: + return True + + # 检测全局代理和目录代理是否同时存在 + def _check_proxy_even(self, proxy_conf: dict, is_modify) -> bool: + n = 0 + if is_modify: + for i in self.config: + if i["sitename"] == proxy_conf["sitename"]: + n += 1 + if n == 1: + return False + for i in self.config: + if i["sitename"] == proxy_conf["sitename"]: + if i["advanced"] != proxy_conf["advanced"]: + return True + return False + + def check_args(self, get, is_modify=False) -> Union[str, dict]: + if check_server_config(): + return '配置文件出错请先排查配置' + data = { + "advanced": 0, + "proxydir": "", + "cache": 0, + "cachetime": 1, + "type": 0, + "todomain": "$host", + } + try: + data["proxyname"] = get.proxyname.strip() + data["sitename"] = get.sitename.strip() + if "proxydir" in get: + data["proxydir"] = get.proxydir.strip() + data["proxysite"] = get.proxysite.strip() + if "todomain" in get: + data["todomain"] = get.todomain.strip() + data["type"] = int(get.type.strip()) + data["cache"] = int(get.cache.strip()) + data["subfilter"] = json.loads(get.subfilter.strip()) + data["advanced"] = int(get.advanced.strip()) + data["cachetime"] = int(get.cachetime.strip()) + except: + return "参数错误" + + if is_modify is False: + if len(data["proxyname"]) < 3 or len(data["proxyname"]) > 40: + return '名称必须大于3小于40个字符串' + + if self._check_even(data, is_modify): + return '指定反向代理名称或代理文件夹已存在' + # 判断代理,只能有全局代理或目录代理 + if self._check_proxy_even(data, is_modify): + return '不能同时设置目录代理和全局代理' + # 判断cachetime类型 + if data["cachetime"] < 1: + return "缓存时间不能为空" + + rep = r"http(s)?\:\/\/" + rep_re_key = re.compile(r'''[?=\[\])(*&^%$#@!~`{}><,'"\\]+''') + # 检测代理目录格式 + if rep_re_key.search(data["proxydir"]): + return "代理目录不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]" + # 检测发送域名格式 + if get.todomain: + if re.search("[}{#;\"\']+", data["todomain"]): + return '发送域名格式错误:' + data["todomain"] + '
                                    不能存在以下特殊字符【 } { # ; \" \' 】 ' + if webserver() != 'openlitespeed' and not get.todomain: + data["todomain"] = "$host" + + # 检测目标URL格式 + if not re.match(rep, data["proxysite"]): + return '域名格式错误 ' + data["proxysite"] + if rep_re_key.search(data["proxysite"]): + return "目标URL不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\\,',\"]" + + if not data["proxysite"].split('//')[-1]: + return '目标URL不能为[http://或https://],请填写完整URL,如:https://www.bt.cn' + + for s in data["subfilter"]: + if not s["sub1"]: + continue + if not s["sub1"] and s["sub2"]: + return '请输入被替换的内容' + elif s["sub1"] == s["sub2"]: + return '替换内容与被替换内容不能一致' + return data + + def check_location(self, site_name, proxy_dir: str) -> Optional[str]: + # 伪静态文件路径 + rewrite_conf_path = "%s/vhost/rewrite/%s%s.conf" % (self.panel_path, self.config_prefix, site_name) + # vhost文件 + vhost_path = "%s/vhost/nginx/%s%s.conf" % (self.panel_path, self.config_prefix, site_name) + + rep_location = re.compile(r"location\s+(\^~\s*)?%s\s*{" % proxy_dir) + + for i in [rewrite_conf_path, vhost_path]: + conf = read_file(i) + if isinstance(conf, str) and rep_location.search(conf): + return '伪静态/站点主配置文件已经存在全局反向代理' + + @staticmethod + def _set_nginx_proxy_base(): + file = "/www/server/nginx/conf/proxy.conf" + setup_path = "/www/server" + if not os.path.exists(file): + conf = '''proxy_temp_path %s/nginx/proxy_temp_dir; +proxy_cache_path %s/nginx/proxy_cache_dir levels=1:2 keys_zone=cache_one:10m inactive=1d max_size=5g; +client_body_buffer_size 512k; +proxy_connect_timeout 60; +proxy_read_timeout 60; +proxy_send_timeout 60; +proxy_buffer_size 32k; +proxy_buffers 4 64k; +proxy_busy_buffers_size 128k; +proxy_temp_file_write_size 128k; +proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; +proxy_cache cache_one;''' % (setup_path, setup_path) + write_file(file, conf) + + conf = read_file(file) + if conf and conf.find('include proxy.conf;') == -1: + rep = r"include\s+mime.types;" + conf = re.sub(rep, "include mime.types;\n\tinclude proxy.conf;", conf) + write_file(file, conf) + + def set_nginx_proxy_include(self, site_name) -> Optional[str]: + self._set_nginx_proxy_base() + ng_file = "{}/vhost/nginx/{}{}.conf".format(self.panel_path, self.config_prefix, site_name) + ng_conf = read_file(ng_file) + if not ng_conf: + return "配置文件丢失" + cure_cache = '''location ~ /purge(/.*) { + proxy_cache_purge cache_one $host$1$is_args$args; + #access_log /www/wwwlogs/%s_purge_cache.log; + }''' % site_name + + proxy_dir = "{}/vhost/nginx/proxy/{}".format(self.panel_path, site_name) + if not os.path.isdir(os.path.dirname(proxy_dir)): + os.makedirs(os.path.dirname(proxy_dir)) + + if not os.path.isdir(proxy_dir): + os.makedirs(proxy_dir) + + include_conf = ( + "\n #清理缓存规则\n" + " %s\n" + " #引用反向代理规则,注释后配置的反向代理将无效\n" + " include /www/server/panel/vhost/nginx/proxy/%s/*.conf;\n" + ) % (cure_cache, site_name) + + rep_include = re.compile(r"\s*include.*/proxy/.*/\*\.conf\s*;", re.M) + if rep_include.search(ng_conf): + return + # 添加 引入 + rep_list = [ + (re.compile(r"\s*include\s+.*/rewrite/.*\.conf;(\s*#REWRITE-END)?"), False), # 先匹配伪静态,有伪静态就加到伪静态下 + (re.compile(r"#PHP-INFO-END"), False), # 匹配PHP配置, 加到php配置下 + (re.compile(r"\sinclude +.*/ip-restrict/.*\*\.conf;", re.M), False), # 匹配IP配置, 加其下 + (re.compile(r"#SECURITY-END"), False), # 匹配Referer配置, 加其下 + ] + + # 使用正则匹配确定插入位置 + def set_by_rep_idx(tmp_rep: re.Pattern, use_start: bool) -> bool: + tmp_res = tmp_rep.search(ng_conf) + if not tmp_res: + return False + if use_start: + new_conf = ng_conf[:tmp_res.start()] + include_conf + tmp_res.group() + ng_conf[tmp_res.end():] + else: + new_conf = ng_conf[:tmp_res.start()] + tmp_res.group() + include_conf + ng_conf[tmp_res.end():] + + write_file(ng_file, new_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return False + return True + for r, s in rep_list: + if set_by_rep_idx(r, s): + break + else: + return "无法在配置文件中定位到需要添加的项目" + + now_ng_conf = read_file(ng_file) + # 清理文件缓存 + rep_location = re.compile(r"location\s+~\s+\.\*\\\.[^{]*{(\s*(expires|error_log|access_log).*;){3}\s*}\s*") + + new__ng_conf = rep_location.sub("", now_ng_conf) + write_file(ng_file, new__ng_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(ng_file, now_ng_conf) + + def un_set_nginx_proxy_include(self, site_name) -> Optional[str]: + ng_file = "{}/vhost/nginx/{}{}.conf".format(self.panel_path, self.config_prefix, site_name) + ng_conf = read_file(ng_file) + if not ng_conf: + return "配置文件丢失" + rep_list = [ + re.compile(r"\s*#清理缓存规则\n"), + re.compile(r"\s*location\s+~\s+/purge[^{]*{[^}]*}\s*"), + re.compile(r"(#[^#\n]*\n)?\s*include.*/proxy/.*/\*\.conf\s*;[^\n]*\n"), + ] + new_conf = ng_conf + for rep in rep_list: + new_conf = rep.sub("", new_conf, 1) + + write_file(ng_file, new_conf) + if webserver() == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return "配置移除失败" + + def set_apache_proxy_include(self, site_name): + ap_file = "{}/vhost/apache/{}{}.conf".format(self.panel_path, self.config_prefix, site_name) + ap_conf = read_file(ap_file) + if not ap_conf: + return "配置文件丢失" + proxy_dir = "{}/vhost/apache/proxy/{}".format(self.panel_path, site_name) + + if not os.path.isdir(os.path.dirname(proxy_dir)): + os.makedirs(os.path.dirname(proxy_dir)) + if not os.path.isdir(proxy_dir): + os.makedirs(proxy_dir) + + include_conf = ( + " #引用反向代理规则,注释后配置的反向代理将无效\n" + " IncludeOptional /www/server/panel/vhost/apache/proxy/%s/*.conf\n" + ) % site_name + + rep_include = re.compile(r"\s*IncludeOptional.*/proxy/.*/\*\.conf\s*;", re.M) + if rep_include.search(ap_conf): + return + + # 添加 引入 + rep_list = [ + (re.compile(r"(.|\n)*?[^\n]*\n"), False), # 匹配PHP配置, 加到php配置下 + (re.compile(r"CustomLog[^\n]*\n"), False), # 匹配Referer配置, 加其下 + ] + + # 使用正则匹配确定插入位置 + 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() + if last_idx == 0: + return False + + new_conf_list.append(ap_conf[last_idx:]) + + new_conf = "".join(new_conf_list) + write_file(ap_file, new_conf) + if webserver() == "apache" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return False + return True + + for r, s in rep_list: + if set_by_rep_idx(r, s): + break + else: + return "无法在配置文件中定位到需要添加的项目" + + def un_set_apache_proxy_include(self, site_name) -> Optional[str]: + ng_file = "{}/vhost/apache/{}{}.conf".format(self.panel_path, self.config_prefix, site_name) + ap_conf = read_file(ng_file) + if not ap_conf: + return "配置文件丢失" + rep_include = re.compile(r"(#.*\n)?\s*IncludeOptiona.*/proxy/.*/\*\.conf\s*[^\n]\n") + + new_conf = rep_include.sub("", ap_conf) + + write_file(ng_file, new_conf) + if webserver() == "apache" and check_server_config() is not None: + write_file(ng_file, ap_conf) + return "配置移除失败" + + def set_nginx_proxy(self, proxy_data: dict) -> Optional[str]: + proxy_name_md5 = self._calc_proxy_name_md5(proxy_data["proxyname"]) + ng_proxy_file = "%s/vhost/nginx/proxy/%s/%s_%s.conf" % ( + self.panel_path, proxy_data["sitename"], proxy_name_md5, proxy_data["sitename"]) + if proxy_data["type"] == 0: + if os.path.isfile(ng_proxy_file): + os.remove(ng_proxy_file) + return + + random_string = self._random_string() + + # websocket前置map + map_file = "{}/vhost/nginx/0.websocket.conf".format(self.panel_path) + if not os.path.exists(map_file): + write_file(map_file, ''' +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +}''') + # 构造缓存配置 + ng_cache = r""" + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + expires 1m; + } + proxy_ignore_headers Set-Cookie Cache-Control expires; + proxy_cache cache_one; + proxy_cache_key $host$uri$is_args$args; + proxy_cache_valid 200 304 301 302 %sm;""" % proxy_data["cachetime"] + no_cache = r""" + set $static_file%s 0; + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + set $static_file%s 1; + expires 1m; + } + if ( $static_file%s = 0 ) + { + add_header Cache-Control no-cache; + }""" % (random_string, random_string, random_string) + + ng_proxy = ''' +#PROXY-START%s + +location ^~ %s +{ + proxy_pass %s; + proxy_set_header Host %s; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_http_version 1.1; + # proxy_hide_header Upgrade; + + add_header X-Cache $upstream_cache_status; + + #Set Nginx Cache + %s + %s +} + +#PROXY-END%s''' + + # 构造替换字符串 + ng_sub_data_list = [] + for s in proxy_data["subfilter"]: + if not s["sub1"]: + continue + if '"' in s["sub1"]: + s["sub1"] = s["sub1"].replace('"', '\\"') + if '"' in s["sub2"]: + s["sub2"] = s["sub2"].replace('"', '\\"') + ng_sub_data_list.append(' sub_filter "%s" "%s";' % (s["sub1"], s["sub2"])) + if ng_sub_data_list: + ng_sub_filter = ''' + proxy_set_header Accept-Encoding ""; + %s + sub_filter_once off;''' % "\n".join(ng_sub_data_list) + else: + ng_sub_filter = '' + + if proxy_data["proxydir"][-1] != '/': + proxy_dir = proxy_data["proxydir"] + "/" + else: + proxy_dir = proxy_data["proxydir"] + + if proxy_data["proxysite"][-1] != '/': + proxy_site = proxy_data["proxysite"] + "/" + else: + proxy_site = proxy_data["proxysite"] + + # 构造反向代理 + if proxy_data["cache"] == 1: + ng_proxy_cache = ng_proxy % ( + proxy_dir, proxy_dir, proxy_site, proxy_data["todomain"], ng_sub_filter, ng_cache, proxy_dir) + else: + ng_proxy_cache = ng_proxy % ( + proxy_dir, proxy_dir, proxy_site, proxy_data["todomain"], ng_sub_filter, no_cache, proxy_dir) + + write_file(ng_proxy_file, ng_proxy_cache) + if webserver() == "nginx" and check_server_config() is not None: + import public + public.print_log(check_server_config()) + os.remove(ng_proxy_file) + return "配置添加失败" + + def set_apache_proxy(self, proxy_data: dict): + proxy_name_md5 = self._calc_proxy_name_md5(proxy_data["proxyname"]) + ap_proxy_file = "%s/vhost/apache/proxy/%s/%s_%s.conf" % ( + self.panel_path, proxy_data["sitename"], proxy_name_md5, proxy_data["sitename"]) + if proxy_data["type"] == 0: + if os.path.isfile(ap_proxy_file): + os.remove(ap_proxy_file) + return + + ap_proxy = '''#PROXY-START%s + + ProxyRequests Off + SSLProxyEngine on + ProxyPass %s %s/ + ProxyPassReverse %s %s/ + +#PROXY-END%s''' % (proxy_data["proxydir"], proxy_data["proxydir"], proxy_data["proxysite"], + proxy_data["proxydir"],proxy_data["proxysite"], proxy_data["proxydir"]) + write_file(ap_proxy_file, ap_proxy) + + @staticmethod + def _random_string() -> str: + from uuid import uuid4 + return "bt" + uuid4().hex[:6] + + @staticmethod + def _calc_proxy_name_md5(data: str) -> str: + m = md5() + m.update(data.encode("utf-8")) + return m.hexdigest() + + def create_proxy(self, get) -> Optional[str]: + proxy_data = self.check_args(get, is_modify=False) + if isinstance(proxy_data, str): + return proxy_data + if webserver() == "nginx": + error_msg = self.check_location(proxy_data["sitename"], proxy_data["proxydir"]) + if error_msg: + return error_msg + + error_msg = self.set_nginx_proxy_include(proxy_data["sitename"]) + if webserver() == "nginx" and error_msg: + return error_msg + error_msg = self.set_apache_proxy_include(proxy_data["sitename"]) + if webserver() == "apache" and error_msg: + return error_msg + error_msg = self.set_nginx_proxy(proxy_data) + if webserver() == "nginx" and error_msg: + return error_msg + self.set_apache_proxy(proxy_data) + self.config.append(proxy_data) + self.save_config() + service_reload() + + def modify_proxy(self, get) -> Optional[str]: + proxy_data = self.check_args(get, is_modify=True) + if isinstance(proxy_data, str): + return proxy_data + idx = None + + for index, i in enumerate(self.config): + if i["proxyname"] == proxy_data["proxyname"] and i["sitename"] == proxy_data["sitename"]: + idx = index + break + if idx is None: + return "未找到该名称的反向代理配置" + + if webserver() == "nginx" and proxy_data["proxydir"] != self.config[idx]["proxydir"]: + error_msg = self.check_location(proxy_data["sitename"], proxy_data["proxydir"]) + if error_msg: + return error_msg + + error_msg = self.set_nginx_proxy_include(proxy_data["sitename"]) + if webserver() == "nginx" and error_msg: + return error_msg + error_msg = self.set_apache_proxy_include(proxy_data["sitename"]) + if webserver() == "apache" and error_msg: + return error_msg + error_msg = self.set_nginx_proxy(proxy_data) + if webserver() == "nginx" and error_msg: + return error_msg + self.set_apache_proxy(proxy_data) + self.config[idx] = proxy_data + self.save_config() + service_reload() + + def remove_proxy(self, site_name, proxy_name, multiple=False) -> Optional[str]: + idx = None + site_other = False + for index, i in enumerate(self.config): + if i["proxyname"] == proxy_name and i["sitename"] == site_name: + idx = index + if i["sitename"] == site_name and i["proxyname"] != proxy_name: + site_other = True + + if idx is None: + return "未找到该名称的反向代理配置" + + proxy_name_md5 = self._calc_proxy_name_md5(proxy_name) + ng_proxy_file = "%s/vhost/nginx/proxy/%s/%s_%s.conf" % ( + self.panel_path, site_name, proxy_name_md5, site_name) + ap_proxy_file = "%s/vhost/apache/proxy/%s/%s_%s.conf" % ( + self.panel_path, site_name, proxy_name_md5, site_name) + if os.path.isfile(ap_proxy_file): + os.remove(ap_proxy_file) + + if os.path.isfile(ng_proxy_file): + os.remove(ng_proxy_file) + del self.config[idx] + self.save_config() + if not site_other: + self.un_set_apache_proxy_include(site_name) + self.un_set_nginx_proxy_include(site_name) + if not multiple: + service_reload() + + def get_proxy_list(self, get) -> Union[str, List[Dict[str, Any]]]: + try: + site_name = get.sitename.strip() + except (AttributeError, ValueError, TypeError): + return "参数错误" + proxy_list = [] + web_server = webserver() + for conf in self.config: + if conf["sitename"] != site_name: + continue + md5_name = self._calc_proxy_name_md5(conf['proxyname']) + conf["proxy_conf_file"] = "%s/vhost/%s/proxy/%s/%s_%s.conf" % ( + self.panel_path, web_server, site_name, md5_name, site_name) + proxy_list.append(conf) + return proxy_list + + def remove_site_proxy_info(self, site_name): + idx_list = [] + for index, i in enumerate(self.config): + if i["sitename"] == site_name: + idx_list.append(index) + + for idx in idx_list[::-1]: + del self.config[idx] + + self.save_config() + + ng_proxy_dir = "%s/vhost/nginx/proxy/%s" % (self.panel_path, site_name) + ap_proxy_dir = "%s/vhost/apache/proxy/%s" % (self.panel_path, site_name) + + if os.path.isdir(ng_proxy_dir): + shutil.rmtree(ng_proxy_dir) + + if os.path.isdir(ap_proxy_dir): + shutil.rmtree(ap_proxy_dir) + + +class Proxy(object): + + def __init__(self, config_prefix=""): + self.config_prefix = config_prefix + self._p = RealProxy(self.config_prefix) + + def create_proxy(self, get): + msg = self._p.create_proxy(get) + if msg: + return json_response(status=False, msg=msg) + return json_response(status=True, msg="添加成功") + + def modify_proxy(self, get): + msg = self._p.modify_proxy(get) + if msg: + return json_response(status=False, msg=msg) + return json_response(status=True, msg="修改成功") + + def remove_proxy(self, get): + try: + site_name = get.sitename.strip() + proxy_name = get.proxyname.strip() + except: + return json_response(status=False, msg="参数错误") + msg = self._p.remove_proxy(site_name, proxy_name) + if msg: + return json_response(status=False, msg=msg) + return json_response(status=True, msg="删除成功") + + def get_proxy_list(self, get): + data = self._p.get_proxy_list(get) + if isinstance(data, str): + return json_response(status=False, msg=data) + else: + return json_response(status=True, data=data) \ No newline at end of file diff --git a/mod/base/web_conf/redirect.py b/mod/base/web_conf/redirect.py new file mode 100644 index 00000000..fcdf818c --- /dev/null +++ b/mod/base/web_conf/redirect.py @@ -0,0 +1,737 @@ +import os +import re +import json +import hashlib +import shutil +import time +from typing import Tuple, Optional, Union, Dict, List, Any +from urllib import parse +from itertools import product +from .util import webserver, check_server_config, write_file, read_file, DB, service_reload +from mod.base import json_response + + +class RealRedirect: + panel_path = "/www/server/panel" + _redirect_conf_file = "{}/data/redirect.conf".format(panel_path) + + _ng_redirect_domain_format = """ +if ($host ~ '^%s'){ + return %s %s%s; +} +""" + _ng_redirect_path_format = """ +rewrite ^%s(.*) %s%s %s; +""" + _ap_redirect_domain_format = """ + + RewriteEngine on + RewriteCond %%{HTTP_HOST} ^%s [NC] + RewriteRule ^(.*) %s%s [L,R=%s] + +""" + _ap_redirect_path_format = """ + + RewriteEngine on + RewriteRule ^%s(.*) %s%s [L,R=%s] + +""" + + 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 = 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(read_file(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 write_file(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 check_server_config() is not None: + return '配置文件出错请先排查配置' + + 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 '参数错误' + + if not is_modify: + if not redirect_name: + return "参数错误,配置名称不能为空" + # 检测名称是否重复 + if not (3 <= len(redirect_name) < 15): + return '名称必须大于2小于15个字符串' + + if self._check_redirect(site_name, redirect_name, error_page == 1): + return '指定重定向名称已存在' + + site_info = DB('sites').where("name=?", (site_name,)).find() + if not isinstance(site_info, dict): + return "站点信息查询错误" + 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 '目标URL格式不对【%s】' % to_url + + # 非404页面de重定向检测项 + if error_page != 1: + # 检测是否选择域名 + if domain_or_path == "domain": + if not redirect_domain: + return '请选择重定向域名' + # 检测域名是否已经存在配置文件 + repeat_domain = self._check_redirect_domain_exist(site_name, redirect_domain, redirect_name, is_modify) + if repeat_domain: + return '重定向域名重复 %s' % repeat_domain + + # 检查目标URL的域名和被重定向的域名是否一样 + tu = self._parse_url_domain(to_url) + for d in redirect_domain: + if d == tu: + return '域名 "%s" 和目标域名一致请取消选择' % d + else: + if not redirect_path: + return '请输入重定向路径' + if redirect_path[0] != "/": + return "路径格式不正确,格式为/xxx" + # 检测路径是否有存在配置文件 + if self._check_redirect_path_exist(site_name, redirect_path, redirect_name): + return '重定向路径重复 %s' % redirect_path + + to_url_path = self._parse_url_path(to_url) + if to_url_path.startswith(redirect_path): + return '目标URL[%s]以被重定向的路径[%s]开头,会导致循环匹配' % (to_url_path, redirect_path) + # 404页面重定向检测项 + else: + if not to_url and not to_path: + return '首页或自定义页面必须二选一' + 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) -> Tuple[bool, str]: + res_conf = self._check_redirect_args(get, is_modify=False) + if isinstance(res_conf, str): + return False, res_conf + + res = self._set_include(res_conf) + if res is not None: + return False, res + res = self._write_config(res_conf) + if res is not None: + return False, res + self.config.append(res_conf) + self.save_config() + service_reload() + return True, '创建成功' + + def _set_include(self, res_conf) -> Optional[str]: + flag, msg = self._set_nginx_redirect_include(res_conf) + if not flag and webserver() == "nginx": + return msg + flag, msg = self._set_apache_redirect_include(res_conf) + if not flag and webserver() == "apache": + 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) -> Tuple[bool, str]: + """ + @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 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 False, res + res = self._write_config(res_conf) + if res is not None: + return False, res + + if old_idx is not None: + self.config[old_idx].update(res_conf) + else: + self.config.append(res_conf) + self.save_config() + service_reload() + return True, '修改成功' + + def _set_nginx_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]: + ng_redirect_dir = "%s/vhost/nginx/redirect/%s" % (self.panel_path, redirect_conf["sitename"]) + ng_file = "{}/vhost/nginx/{}{}.conf".format(self.panel_path, self.config_prefix, redirect_conf["sitename"]) + if not os.path.exists(ng_redirect_dir): + os.makedirs(ng_redirect_dir, 0o600) + ng_conf = read_file(ng_file) + if not isinstance(ng_conf, str): + return False, "nginx配置文件读取失败" + + rep_include = re.compile(r"\sinclude +.*/redirect/.*\*\.conf;", re.M) + if rep_include.search(ng_conf): + return True, "" + redirect_include = ( + "#SSL-END\n" + " #引用重定向规则,注释后配置的重定向代理将无效\n" + " include {}/*.conf;" + ).format(ng_redirect_dir) + + if "#SSL-END" not in ng_conf: + return False, "添加配置失败,无法定位SSL相关配置的位置" + + new_conf = ng_conf.replace("#SSL-END", redirect_include) + write_file(ng_file, new_conf) + if self.webserver == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return False, "添加配置失败" + + return True, "" + + def _un_set_nginx_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]: + ng_file = "{}/vhost/nginx/{}{}.conf".format(self.panel_path, self.config_prefix, redirect_conf["sitename"]) + ng_conf = read_file(ng_file) + if not isinstance(ng_conf, str): + return False, "nginx配置文件读取失败" + + 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) + write_file(ng_file, new_conf) + if self.webserver == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return False, "移除配置失败" + + return True, "" + + def _set_apache_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]: + ap_redirect_dir = "%s/vhost/apache/redirect/%s" % (self.panel_path, redirect_conf["sitename"]) + ap_file = "{}/vhost/apache/{}{}.conf".format(self.panel_path, self.config_prefix, redirect_conf["sitename"]) + if not os.path.exists(ap_redirect_dir): + os.makedirs(ap_redirect_dir, 0o600) + + ap_conf = read_file(ap_file) + if not isinstance(ap_conf, str): + return False, "apache配置文件读取失败" + + rep_include = re.compile(r"\sIncludeOptional +.*/redirect/.*\*\.conf", re.M) + include_count = len(list(rep_include.finditer(ap_conf))) + if ap_conf.count("
                                    ") == 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 # 引用重定向规则,注释后配置的重定向代理将无效\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) + write_file(ap_file, new_conf) + if self.webserver == "apache" and check_server_config() is not None: + write_file(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, "设置失败" + + def _un_set_apache_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]: + ap_file = "{}/vhost/apache/{}{}.conf".format(self.panel_path, self.config_prefix, redirect_conf["sitename"]) + ap_conf = read_file(ap_file) + if not isinstance(ap_conf, str): + return False, "apache配置文件读取失败" + + 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) + write_file(ap_file, new_conf) + if self.webserver == "apache" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return False, "移除配置失败" + + return True, "" + + def write_nginx_redirect_file(self, redirect_conf: dict) -> Optional[str]: + conf_file = "{}/vhost/nginx/redirect/{}/{}_{}.conf".format( + self.panel_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_redirect_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_redirect_path_format % (redirect_path, to_url, hold_path, redirect_type)) + + conf_list.append("#REWRITE-END") + + conf_data = "\n".join(conf_list) + write_file(conf_file, conf_data) + + if self.webserver == "nginx": + error_msg = check_server_config() + if error_msg is not None: + if os.path.exists(conf_file): + os.remove(conf_file) + return 'ERROR: 配置出错
                                    ' + error_msg.replace("\n", '
                                    ') + '
                                    ' + 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.panel_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_redirect_domain_format % ( + sd, to_url, hold_path, redirect_conf["redirecttype"] + )) + else: + redirect_path = redirect_conf["redirectpath"] + conf_list.append(self._ap_redirect_path_format % (redirect_path, to_url, hold_path, redirect_conf["redirecttype"])) + + conf_list.append("#REWRITE-END") + + write_file(conf_file, "\n".join(conf_list)) + if self.webserver == "apache": + error_msg = check_server_config() + if error_msg is not None: + if os.path.exists(conf_file): + os.remove(conf_file) + return 'ERROR: 配置出错
                                    ' + error_msg.replace("\n", '
                                    ') + '
                                    ' + + def unset_nginx_404_conf(self, site_name): + """ + 清理已有的 404 页面 配置 + """ + need_clear_files = [ + "{}/vhost/nginx/{}{}.conf".format(self.panel_path, self.config_prefix, site_name), + "{}/vhost/nginx/rewrite/{}{}.conf".format(self.panel_path, self.config_prefix, site_name), + ] + rep_error_page = re.compile(r'(?P.*)error_page +404 +/404\.html[^\n]*\n', re.M) + rep_location_404 = re.compile(r'(?P.*)location += +/404\.html[^}]*}') + clear_files = [ + { + "data": read_file(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 + write_file(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.panel_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) + + write_file(conf_file, conf_data) + if self.webserver == "nginx": + error_msg = check_server_config() + if error_msg is not None: + if os.path.exists(conf_file): + os.remove(conf_file) + return 'ERROR: 配置出错
                                    ' + error_msg.replace("\n", '
                                    ') + '
                                    ' + + 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.panel_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 + + RewriteEngine on + RewriteCond %{{REQUEST_FILENAME}} !-f + RewriteCond %{{REQUEST_FILENAME}} !-d + RewriteRule . {} [L,R={}] + +#REWRITE-END +""".format(_path, redirect_conf["redirecttype"]) + + write_file(conf_file, conf_data) + if self.webserver == "apache": + error_msg = check_server_config() + if error_msg is not None: + if os.path.exists(conf_file): + os.remove(conf_file) + return 'ERROR: 配置出错
                                    ' + error_msg.replace("\n", '
                                    ') + '
                                    ' + + def remove_redirect(self, get, multiple=None) -> Tuple[bool, str]: + try: + site_name = get.sitename.strip() + redirect_name = get.redirectname.strip() + except AttributeError: + return False, "参数错误" + target_idx = None + have_other_redirect = False + target_conf = None + for i, conf in enumerate(self.config): + 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: + return False, '没有指定的配置' + + r_md5_name = self._calc_redirect_name_md5(target_conf["redirectname"]) + ng_conf_file = "%s/vhost/nginx/redirect/%s/%s_%s.conf" % ( + self.panel_path, site_name, r_md5_name, site_name) + if os.path.exists(ng_conf_file): + os.remove(ng_conf_file) + + ap_conf_file = "%s/vhost/nginx/apache/%s/%s_%s.conf" % ( + self.panel_path, site_name, r_md5_name, site_name) + if os.path.exists(ap_conf_file): + os.remove(ap_conf_file) + + 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: + service_reload() + + return True, '删除成功' + + def mutil_remove_redirect(self, get): + try: + redirect_names = json.loads(get.redirectnames.strip()) + site_name = get.sitename.strip() + except (AttributeError, json.JSONDecodeError, TypeError): + return False, "参数错误" + del_successfully = [] + del_failed = [] + get_obj = type(get)() + for redirect_name in redirect_names: + get_obj.redirectname = redirect_name + get_obj.sitename = site_name + try: + flag, msg = self.remove_redirect(get, multiple=1) + if flag: + del_failed[redirect_name] = msg + continue + del_successfully.append(redirect_name) + except: + del_failed.append(redirect_name) + + service_reload() + if not del_failed: + return True, '删除重定向【{}】成功'.format(','.join(del_successfully)) + else: + return True, '重定向【{}】删除成功,【{}】删除失败'.format( + ','.join(del_successfully), ','.join(del_failed) + ) + + def get_redirect_list(self, get) -> Tuple[bool, Union[str, List[Dict[str, Any]]]]: + try: + error_page = None + site_name = get.sitename.strip() + if "errorpage" in get: + error_page = int(get.errorpage) + except (AttributeError, ValueError, TypeError): + return False, "参数错误" + redirect_list = [] + web_server = self.webserver + if self.webserver == 'openlitespeed': + web_server = '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页面'] + + md5_name = self._calc_redirect_name_md5(conf['redirectname']) + conf["redirect_conf_file"] = "%s/vhost/%s/redirect/%s/%s_%s.conf" % ( + self.panel_path, web_server, site_name, md5_name, site_name) + conf["type"] = 1 if os.path.isfile(conf["redirect_conf_file"]) else 0 + redirect_list.append(conf) + return True, redirect_list + + def remove_site_redirect_info(self, site_name): + for i in range(len(self.config) - 1, -1, -1): + if self.config[i]["sitename"] == site_name: + del self.config[i] + self.save_config() + + m_path = self.panel_path + '/vhost/nginx/redirect/' + site_name + if os.path.exists(m_path): + shutil.rmtree(m_path) + + m_path = self.panel_path + '/vhost/apache/redirect/' + site_name + if os.path.exists(m_path): + shutil.rmtree(m_path) + + +class Redirect(RealRedirect): + + def __init__(self, config_prefix: str = ""): + super().__init__(config_prefix) + self.config_prefix = config_prefix + + def remove_redirect_by_project_name(self, project_name): + return self.remove_site_redirect_info(project_name) + + def create_project_redirect(self, get): + flag, msg = self.create_redirect(get) + return json_response(status=flag, msg=msg) + + def modify_project_redirect(self, get): + flag, msg = self.modify_redirect(get) + return json_response(status=flag, msg=msg) + + def remove_project_redirect(self, get): + flag, msg = self.remove_redirect(get) + return json_response(status=flag, msg=msg) + + def mutil_remove_project_redirect(self, get): + flag, msg = self.mutil_remove_redirect(get) + return json_response(status=flag, msg=msg) + + def get_project_redirect_list(self, get): + flag, data = self.get_redirect_list(get) + if not flag: + return json_response(status=flag, msg=data) + else: + return json_response(status=flag, data=data) diff --git a/mod/base/web_conf/referer.py b/mod/base/web_conf/referer.py new file mode 100644 index 00000000..28a5931f --- /dev/null +++ b/mod/base/web_conf/referer.py @@ -0,0 +1,363 @@ +import os +import re +import json +from dataclasses import dataclass +from typing import Tuple, Optional, Union, Dict +from .util import webserver, check_server_config, DB, \ + write_file, read_file, GET_CLASS, service_reload, pre_re_key +from mod.base import json_response + + +@dataclass +class _RefererConf: + name: str + fix: str + domains: str + status: str + return_rule: str + http_status: str + + def __str__(self): + return '{"name"="%s","fix"="%s","domains"="%s","status"="%s","http_status"="%s","return_rule"="%s"}' % ( + self.name, self.fix, self.domains, self.status, self.http_status, self.return_rule + ) + + +class RealReferer: + _referer_conf_dir = '/www/server/panel/vhost/config' # 防盗链配置 + _ng_referer_conf_format = r''' #SECURITY-START 防盗链配置 + location ~ .*\.(%s)$ { + expires 30d; + access_log /dev/null; + valid_referers %s; + if ($invalid_referer){ + %s; + } + } + #SECURITY-END''' + + def __init__(self, config_prefix: str): + if not os.path.isdir(self._referer_conf_dir): + os.makedirs(self._referer_conf_dir) + self.config_prefix: str = config_prefix + self._webserver = None + + @property + def webserver(self) -> str: + if self._webserver is not None: + return self._webserver + self._webserver = webserver() + return self._webserver + + def get_config(self, site_name: str) -> Optional[_RefererConf]: + try: + config = json.loads(read_file("{}/{}{}_door_chain.json".format(self._referer_conf_dir, self.config_prefix, site_name))) + except (json.JSONDecodeError, TypeError, ValueError): + config = None + if isinstance(config, dict): + return _RefererConf(**config) + return None + + def save_config(self, site_name: str, data: Union[dict, str, _RefererConf]) -> bool: + if isinstance(data, dict): + c = json.dumps(data) + elif isinstance(data, _RefererConf): + c = json.dumps(str(data)) + else: + c = data + + file_path = "{}/{}{}_door_chain.json".format(self._referer_conf_dir, self.config_prefix, site_name) + return write_file(file_path, c) + + # 检测参数,如果正确则返回 配置数据类型的值,否则返回错误信息 + @staticmethod + def check_args(get: Union[Dict, GET_CLASS]) -> Union[_RefererConf, str]: + res = {} + if isinstance(get, GET_CLASS): + try: + res["status"] = "true" if not hasattr(get, "status") else get.status.strip() + res["http_status"] = "false" if not hasattr(get, "http_status") else get.http_status.strip() + res["name"] = get.name.strip() + res["fix"] = get.fix.strip() + res["domains"] = get.domains.strip() + res["return_rule"] = get.return_rule.strip() + except AttributeError: + return "参数错误" + else: + try: + res["status"] = "true" if "status" not in get else get["status"].strip() + res["http_status"] = "false" if "http_status" not in get else get["http_status"].strip() + res["name"] = get["name"].strip() + res["fix"] = get["fix"].strip() + res["domains"] = get["domains"].strip() + res["return_rule"] = get["return_rule"].strip() + except KeyError: + return "参数错误" + + rconf = _RefererConf(**res) + if rconf.status not in ("true", "false") and rconf.return_rule not in ("true", "false"): + return "状态参数只能使用【true,false】" + if rconf.return_rule not in ('404', '403', '200', '301', '302', '401') and rconf.return_rule[0] != "/": + return "响应资源应使用URI路径或HTTP状态码,如:/test.png 或 404" + if len(rconf.domains) < 3: + return "防盗链域名不能为空" + if len(rconf.fix) < 2: + return 'URL后缀不能为空!' + return rconf + + def set_referer_security(self, rc: _RefererConf) -> Tuple[bool, str]: + error_msg = self._set_nginx_referer_security(rc) + if error_msg and self.webserver == "nginx": + return False, error_msg + error_msg = self._set_apache_referer_security(rc) + if error_msg and self.webserver == "apache": + return False, error_msg + service_reload() + self.save_config(rc.name, rc) + return True, "设置成功" + + def _set_nginx_referer_security(self, rc: _RefererConf) -> Optional[str]: + ng_file = '/www/server/panel/vhost/nginx/{}{}.conf'.format(self.config_prefix, rc.name) + ng_conf = read_file(ng_file) + if not isinstance(ng_conf, str): + return "nginx配置文件丢失,无法设置" + start_idx, end_idx = self._get_nginx_referer_security_idx(ng_conf) + if rc.status == "true": + if rc.return_rule[0] == "/": + return_rule = "rewrite /.* {} break".format(rc.return_rule) + else: + return_rule = 'return {}'.format(rc.return_rule) + + valid_args_list = [] + if rc.http_status == "true": + valid_args_list.extend(("none", "blocked")) + valid_args_list.extend(map(lambda x: x.strip(), rc.domains.split(","))) + valid_args = " ".join(valid_args_list) + + location_args = "|".join(map(lambda x: pre_re_key(x.strip()), rc.fix.split(","))) + if start_idx is not None: + new_conf = ng_conf[:start_idx] + "\n" + ( + self._ng_referer_conf_format % (location_args, valid_args, return_rule) + ) + "\n" + ng_conf[end_idx:] + else: + rep_redirect_include = re.compile(r"\sinclude +.*/redirect/.*\*\.conf;", re.M) + redirect_include_res = rep_redirect_include.search(ng_conf) + if redirect_include_res: + new_conf = ng_conf[:redirect_include_res.end()] + "\n" + ( + self._ng_referer_conf_format % (location_args, valid_args, return_rule) + ) + ng_conf[redirect_include_res.end():] + else: + if "#SSL-END" not in ng_conf: + return "添加配置失败,无法定位SSL相关配置的位置" + + new_conf = ng_conf.replace("#SSL-END", "#SSL-END\n" + self._ng_referer_conf_format % ( + location_args, valid_args, return_rule)) + + else: + if start_idx is None: + return + new_conf = ng_conf[:start_idx] + "\n" + ng_conf[end_idx:] + + write_file(ng_file, new_conf) + if self.webserver == "nginx" and check_server_config() is not None: + write_file(ng_file, ng_conf) + return "配置失败" + + @staticmethod + def _get_nginx_referer_security_idx(ng_conf: str) -> Tuple[Optional[int], Optional[int]]: + rep_security = re.compile( + r"(\s*#\s*SECURITY-START.*\n)?\s*location\s+~\s+\.\*\\\.\(.*(\|.*)?\)\$\s*\{[^}]*valid_referers" + ) + res = rep_security.search(ng_conf) + if res is None: + return None, None + + start_idx = res.start() + s_idx = start_idx + ng_conf[start_idx:].find("{") + 1 # 起始位置 + l_n = 1 + max_idx = len(ng_conf) + while l_n > 0: + next_l = ng_conf[s_idx:].find("{") + next_r = ng_conf[s_idx:].find("}") # 可能存在报错 + if next_r == -1: + return None, None + if next_l == -1: + next_l = max_idx + + if next_l < next_r: + l_n += 1 + else: + l_n -= 1 + s_idx += min(next_l, next_r) + 1 + + rep_comment = re.search(r"^\s*#\s*SECURITY-END[^\n]*\n", ng_conf[s_idx:]) + if rep_comment is not None: + end_idx = s_idx + rep_comment.end() + else: + end_idx = s_idx + + return start_idx, end_idx + + @staticmethod + def _build_apache_referer_security_conf(rc: _RefererConf) -> str: + r_conf_list = ["#SECURITY-START 防盗链配置"] + cond_format = " RewriteCond %{{HTTP_REFERER}} !{} [NC]" + if rc.http_status == "false": + r_conf_list.append(cond_format.format("^$")) + + r_conf_list.extend(map(lambda x: cond_format.format(x.strip()), rc.domains.split(","))) + + rule_format = " RewriteRule .({}) {} " + if rc.return_rule[0] == "/": + r_conf_list.append(rule_format.format( + "|".join(map(lambda x: x.strip(), rc.fix.split(","))), + rc.return_rule + )) + else: + r_conf_list.append(rule_format.format( + "|".join(map(lambda x: x.strip(), rc.fix.split(","))), + "/{s}.html [R={s},NC,L]".format(s=rc.return_rule) + )) + + r_conf_list.append(" #SECURITY-END") + + return "\n".join(r_conf_list) + + # 根据配置正则确定位置 并将配置文件添加进去 use_start 参数指定添加的前后 + def _add_apache_referer_security_by_rep_idx(self, + rep: re.Pattern, + use_start: bool, + ap_conf, ap_file, r_conf) -> bool: + tmp_conf_list = [] + last_idx = 0 + for tmp in rep.finditer(ap_conf): + tmp_conf_list.append(ap_conf[last_idx:tmp.start()]) + if use_start: + tmp_conf_list.append("\n" + r_conf + "\n") + tmp_conf_list.append(tmp.group()) + else: + tmp_conf_list.append(tmp.group()) + tmp_conf_list.append("\n" + r_conf + "\n") + last_idx = tmp.end() + if last_idx == 0: + return False + + tmp_conf_list.append(ap_conf[last_idx:]) + _conf = "".join(tmp_conf_list) + write_file(ap_file, _conf) + if self.webserver == "apache" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return False + return True + + def _set_apache_referer_security(self, rc: _RefererConf) -> Optional[str]: + ap_file = '/www/server/panel/vhost/apache/{}{}.conf'.format(self.config_prefix, rc.name) + ap_conf = read_file(ap_file) + if not isinstance(ap_conf, str): + return "nginx配置文件丢失,无法设置" + rep_security = re.compile(r"#\s*SECURITY-START(.|\n)#SECURITY-END.*\n") + res = rep_security.search(ap_conf) + if rc.status == "true": + r_conf = self._build_apache_referer_security_conf(rc) + if res is not None: + new_conf_list = [] + _idx = 0 + for tmp_res in rep_security.finditer(ap_conf): + new_conf_list.append(ap_conf[_idx:tmp_res.start()]) + new_conf_list.append("\n" + r_conf + "\n") + _idx = tmp_res.end() + new_conf_list.append(ap_conf[_idx:]) + new_conf = "".join(new_conf_list) + write_file(ap_file, new_conf) + if self.webserver == "apache" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return "配置修改失败" + + rep_redirect_include = re.compile(r"IncludeOptional +.*/redirect/.*\*\.conf.*\n", re.M) + rep_custom_log = re.compile(r"CustomLog .*\n") + rep_deny_files = re.compile(r"\n\s*#DENY FILES") + if self._add_apache_referer_security_by_rep_idx(rep_redirect_include, False, ap_conf, ap_file, r_conf): + return + if self._add_apache_referer_security_by_rep_idx(rep_custom_log, False, ap_conf, ap_file, r_conf): + return + if self._add_apache_referer_security_by_rep_idx(rep_deny_files, True, ap_conf, ap_file, r_conf): + return + return "设置添加失败" + + else: + if res is None: + return + + new_conf_list = [] + _idx = 0 + for tmp_res in rep_security.finditer(ap_conf): + new_conf_list.append(ap_conf[_idx:tmp_res.start()]) + _idx = tmp_res.end() + new_conf_list.append(ap_conf[_idx:]) + new_conf = "".join(new_conf_list) + write_file(ap_file, new_conf) + if self.webserver == "apache" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return "配置修改失败" + + def get_referer_security(self, site_name) -> Optional[dict]: + r = self.get_config(site_name) + if r is None: + return None + return json.loads(str(r)) + + def remove_site_referer_info(self, site_name): + file_path = "{}/{}{}_door_chain.json".format(self._referer_conf_dir, self.config_prefix, site_name) + if os.path.exists(file_path): + os.remove(file_path) + + # 从配置文件中获取referer配置信息 + # 暂时不实现,意义不大 + def _get_referer_security_by_conf(self, site_name): + if self.webserver == "nginx": + self._get_nginx_referer_security() + else: + self._get_apache_referer_security() + + +class Referer: + + def __init__(self, config_prefix: str): + self.config_prefix: str = config_prefix + self._r = RealReferer(self.config_prefix) + + def get_referer_security(self, get): + try: + site_name = get.site_name.strip() + except AttributeError: + return json_response(status=False, msg="参数错误") + + data = self._r.get_referer_security(site_name) + if data is None: + default_conf = { + "name": site_name, + "fix": "jpg,jpeg,gif,png,js,css", + "domains": "", + "status": "false", + "return_rule": "404", + "http_status": "false", + } + site_info = DB("sites").where("name=?", (site_name,)).field('id').find() + if not isinstance(site_info, dict): + return json_response(status=False, msg="站点查询错误") + domains_info = DB("domain").where("pid=?", (site_info["id"],)).field('name').select() + if not isinstance(domains_info, list): + return json_response(status=False, msg="站点查询错误") + + default_conf["domains"] = ",".join(map(lambda x: x["name"], domains_info)) + return json_response(status=True, data=default_conf) + + return json_response(status=True, data=data) + + def set_referer_security(self, get): + r = self._r.check_args(get) + if isinstance(r, str): + return json_response(status=False, msg=r) + + flag, msg = self._r.set_referer_security(r) + return json_response(status=flag, msg=msg) diff --git a/mod/base/web_conf/ssl.py b/mod/base/web_conf/ssl.py new file mode 100644 index 00000000..d33faf0b --- /dev/null +++ b/mod/base/web_conf/ssl.py @@ -0,0 +1,1327 @@ +import json +import os +import sys +import shutil +import time +# import OpenSSL +import re +from hashlib import md5 +from datetime import datetime, timedelta +from typing import Optional, Tuple, List, Dict, Union, Callable + +from mod.base import json_response +from .util import webserver, check_server_config, write_file, read_file, GET_CLASS, service_reload + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public +import db +from panelAes import AesCryptPy3 + + +SSL_SAVE_PATH = "{}/vhost/ssl_saved".format(public.get_panel_path()) + + +class _SSLDatabase: + + def __init__(self): + db_path = "{}/data/db".format(public.get_panel_path()) + if not os.path.exists(db_path): + os.makedirs(db_path, 0o600) + self.db_file = '{}/data/db/ssl_data.db'.format(public.get_panel_path()) + if not os.path.exists(self.db_file): + self.init_db() + if not os.path.exists(SSL_SAVE_PATH): + os.makedirs(SSL_SAVE_PATH, 0o600) + + def init_db(self): + tmp_db = db.Sql() + setattr(tmp_db, "_Sql__DB_FILE", self.db_file) + + create_sql_str = ( + "CREATE TABLE IF NOT EXISTS 'ssl_info' (" + "'id' INTEGER PRIMARY KEY AUTOINCREMENT, " + "'hash' TEXT NOT NULL UNIQUE, " + "'path' TEXT NOT NULL, " + "'dns' TEXT NOT NULL, " + "'subject' TEXT NOT NULL, " + "'info' TEXT NOT NULL DEFAULT '', " + "'cloud_id' INTEGER NOT NULL DEFAULT -1, " + "'not_after' TEXT NOT NULL, " + "'use_for_panel' INTEGER NOT NULL DEFAULT 0, " + "'use_for_site' TEXT NOT NULL DEFAULT '[]', " + "'auth_info' TEXT NOT NULL DEFAULT '{}', " + "'create_time' INTEGER NOT NULL DEFAULT (strftime('%s'))" + ");" + ) + res = tmp_db.execute(create_sql_str) + if isinstance(res, str) and res.startswith("error"): + public.WriteLog("SSL管理", "建表ssl_info失败") + return + + index_sql_str = "CREATE INDEX IF NOT EXISTS 'hash_index' ON 'ssl_info' ('hash');" + + res = tmp_db.execute(index_sql_str) + if isinstance(res, str) and res.startswith("error"): + public.WriteLog("SSL管理", "为ssl_info建立索引hash_index失败") + return + tmp_db.close() + + def connection(self): + tmp_db = db.Sql() + setattr(tmp_db, "_Sql__DB_FILE", self.db_file) + tmp_db.table("ssl_info") + return tmp_db + + +ssl_db = _SSLDatabase() + + +class _LocalSSLInfoTool: + + def __init__(self): + self._letsencrypt = self.get_letsencrypt_conf() + + @staticmethod + def get_letsencrypt_conf(): + conf_file = "{}/config/letsencrypt_v2.json".format(public.get_panel_path()) + if not os.path.exists(conf_file): + conf_file = "{}/config/letsencrypt.json".format(public.get_panel_path()) + if not os.path.exists(conf_file): + return None + tmp_config = public.readFile(conf_file) + try: + orders = json.loads(tmp_config)["orders"] + except (json.JSONDecodeError, KeyError): + return None + return orders + + def get_auth(self, domains): + if self._letsencrypt is None: + return None + + for _, data in self._letsencrypt.items(): + if 'save_path' not in data: + continue + for d in data['domains']: + if d in domains: + return { + "auth_type": data.get('auth_type'), + "auth_to": data.get('auth_to') + } + + +class RealSSLManger: + _REFRESH_TIP = "{}/data/ssl_cloud_refresh.tip".format(public.get_panel_path()) + _OTHER_DATA_NAME = ("use_for_panel", "use_for_site",) + + def __init__(self, conf_prefix=""): + self._local_ssl_info_tool = None + self._vhost_path = "/www/server/panel/vhost" + self.conf_prefix = conf_prefix + self._tls_v3 = None + self._is_nginx_http3 = None + + # 与letsencrypt对接 + @property + def local_tool(self): + if self._local_ssl_info_tool is None: + self._local_ssl_info_tool = _LocalSSLInfoTool() + return self._local_ssl_info_tool + return self._local_ssl_info_tool + + # 用于部署 + @classmethod + def get_cert_for_deploy(cls, ssl_data: dict) -> Union[Dict, str]: + data = { + 'privkey': public.readFile(ssl_data["path"] + '/privkey.pem'), + 'fullchain': public.readFile(ssl_data["path"] + '/fullchain.pem') + } + if not isinstance(data["privkey"], str) or not isinstance(data["fullchain"], str): + return '证书读取错误!' + return data + + # 是否刷新 + @classmethod + def need_refresh(cls): + now = int(time.time()) + if not os.path.isfile(cls._REFRESH_TIP): + public.writeFile(cls._REFRESH_TIP, str(now)) + return True + last_time = int(public.readFile(cls._REFRESH_TIP)) + if last_time + 60 * 60 * 4 < now: + public.writeFile(cls._REFRESH_TIP, str(now)) + return True + return False + + # 获取hash指纹 + @staticmethod + def ssl_hash(cert_filename: str = None, certificate: str = None, ignore_errors: bool = False) -> Optional[str]: + if cert_filename is not None and os.path.isfile(cert_filename): + certificate = public.readFile(cert_filename) + + if not isinstance(certificate, str) or not certificate.startswith("-----BEGIN"): + if ignore_errors: + return None + raise ValueError("证书格式错误") + + md5_obj = md5() + md5_obj.update(certificate.encode("utf-8")) + return md5_obj.hexdigest() + + @staticmethod + def strf_date(sdate): + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + + # 获取证书信息 + @classmethod + def get_cert_info(cls, cert_filename: str = None, certificate: str = None): + if cert_filename is not None and os.path.isfile(cert_filename): + certificate = public.readFile(cert_filename) + + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + import ssl_info + return ssl_info.ssl_info().load_ssl_info_by_data(certificate) + + # try: + # result = { + # "issuer": '', + # "dns": [], + # } + # x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, certificate.encode("utf-8")) + # # 取产品名称 + # issuer = x509.get_issuer() + # result['issuer'] = '' + # if hasattr(issuer, 'CN'): + # result['issuer'] = issuer.CN + # if not result['issuer']: + # is_key = [b'0', '0'] + # issue_comp = issuer.get_components() + # if len(issue_comp) == 1: + # is_key = [b'CN', 'CN'] + # for iss in issue_comp: + # if iss[0] in is_key: + # result['issuer'] = iss[1].decode() + # break + # if not result['issuer']: + # if hasattr(issuer, 'O'): + # result['issuer'] = issuer.O + # # 取到期时间 + # result['notAfter'] = cls.strf_date(x509.get_notAfter().decode("utf-8")[:-1]) + # # 取申请时间 + # result['notBefore'] = cls.strf_date(x509.get_notBefore().decode("utf-8")[:-1]) + # # 取可选名称 + # for i in range(x509.get_extension_count()): + # s_name = x509.get_extension(i) + # if s_name.get_short_name() in [b'subjectAltName', 'subjectAltName']: + # s_dns = str(s_name).split(',') + # for d in s_dns: + # result['dns'].append(d.split(':')[1]) + # subject = x509.get_subject().get_components() + # # 取主要认证名称 + # if len(subject) == 1: + # result['subject'] = subject[0][1].decode() + # else: + # if not result['dns']: + # for sub in subject: + # if sub[0] == b'CN': + # result['subject'] = sub[1].decode() + # break + # if 'subject' in result: + # result['dns'].append(result['subject']) + # else: + # result['subject'] = result['dns'][0] + # return result + # except: + # return None + + # 通过文件名称检查并保存 + def save_by_file(self, cert_filename, private_key_filename, cloud_id=None, other_data: Optional[Dict] = None): + if not os.path.isfile(cert_filename) or not os.path.isfile(private_key_filename): + raise ValueError("不存在的证书") + + certificate = public.readFile(cert_filename) + private_key = public.readFile(private_key_filename) + if not isinstance(certificate, str) or not isinstance(private_key, str): + raise ValueError("证书格式错误") + return self.save_by_data(certificate, private_key, cloud_id=cloud_id) + + # 通过证书内容检查并保存 + def save_by_data(self, certificate: str, + private_key: str, + cloud_id: Optional[int] = None, + other_data: Optional[Dict] = None) -> Dict: + + if not certificate.startswith("-----BEGIN") or not private_key.startswith("-----BEGIN"): + raise ValueError("证书格式检查错误") + + if cloud_id is None: + cloud_id = -1 + + hash_data = self.ssl_hash(certificate=certificate) + db_data = self.get_ssl_info_by_hash(hash_data) + if db_data is not None: # 已经保存过的 + # 检查 cloud_id 与 保存的 cloud_id 不同时,更新cloud_id + if db_data['cloud_id'] != cloud_id and cloud_id != -1: + ssl_db.connection().where("id = ?", (db_data["id"],)).update({"cloud_id": cloud_id}) + db_data['cloud_id'] = cloud_id + db_data["dns"] = json.loads(db_data["dns"]) + db_data["info"] = json.loads(db_data["info"]) + db_data["auth_info"] = json.loads(db_data["auth_info"]) + db_data["use_for_site"] = json.loads(db_data["use_for_site"]) + return db_data + info = self.get_cert_info(certificate=certificate) + if info is None: + raise ValueError("证书信息解析错误") + + auth_info = self.local_tool.get_auth(info['dns']) + if auth_info is None: + auth_info = {} + + pdata = { + "hash": hash_data, + "path": "{}/{}".format(SSL_SAVE_PATH, hash_data), + "dns": json.dumps(info['dns']), + "subject": info['subject'], + "info": json.dumps(info), + "cloud_id": cloud_id, + "not_after": info["notAfter"], + "auth_info": json.dumps(auth_info) + } + + if other_data: + for key, other_data in other_data.items(): + if key in self._OTHER_DATA_NAME: + pdata[key] = other_data + + res_id = ssl_db.connection().insert(pdata) + if isinstance(res_id, str) and res_id.startswith("error"): + raise ValueError("数据库写入错误:" + res_id) + + pdata["id"] = res_id + if not os.path.exists(pdata["path"]): + os.makedirs(pdata["path"], 0o600) + + public.writeFile("{}/privkey.pem".format(pdata["path"]), private_key) + public.writeFile("{}/fullchain.pem".format(pdata["path"]), certificate) + public.writeFile("{}/info.json".format(pdata["path"]), pdata["info"]) + + pdata["info"] = info + return pdata + + # 通过hash指纹获取ssl信息 + @staticmethod + def get_ssl_info_by_hash(hash_data: str) -> Optional[dict]: + data = ssl_db.connection().where("hash = ?", (hash_data,)).find() + if isinstance(data, str): + raise ValueError("数据库查询错误:" + data) + if len(data) == 0: + return None + return data + + @staticmethod + def _get_cbc_key_and_iv(with_uer_info=True): + uer_info_file = "{}/data/userInfo.json".format(public.get_panel_path()) + try: + user_info = json.loads(public.readFile(uer_info_file)) + uid = user_info["uid"] + except (json.JSONDecodeError, KeyError): + return None, None, None + + md5_obj = md5() + md5_obj.update(str(uid).encode('utf8')) + bytes_data = md5_obj.hexdigest() + + key = '' + iv = '' + for i in range(len(bytes_data)): + if i % 2 == 0: + iv += bytes_data[i] + else: + key += bytes_data[i] + + if with_uer_info: + return key, iv, user_info + + return key, iv, None + + def get_cert_list(self, + param: Optional[Tuple[str, List]] = None, + force_refresh: bool = False, + local_refresh: bool = False) -> List: + if self.need_refresh() or force_refresh: + self._refresh_ssl_info_by_cloud() + self._get_ssl_by_local_data() + elif local_refresh: + self._get_ssl_by_local_data() + + return self._get_cert_list(param) + + # 获取证书列表 + @classmethod + def _get_cert_list(cls, param: Optional[Tuple[str, List]]) -> List: + db_conn = ssl_db.connection() + if param is not None and len(param) == 2 and isinstance(param[0], str) and isinstance(param[1], (tuple, list)): + db_conn.where(param[0], param[1]) + res = db_conn.select() + if isinstance(res, str): + raise ValueError("数据库查询错误:" + res) + + for value in res: + value["dns"] = json.loads(value["dns"]) + value["info"] = json.loads(value["info"]) + value["auth_info"] = json.loads(value["auth_info"]) + value["use_for_site"] = json.loads(value["use_for_site"]) + value['endtime'] = int((datetime.strptime(value['not_after'], "%Y-%m-%d").timestamp() + - datetime.today().timestamp()) / (60 * 60 * 24)) + + res.sort(key=lambda x: x["not_after"], reverse=True) + + return res + + # 从云端收集证书 + def _refresh_ssl_info_by_cloud(self): + key, iv, user_info = self._get_cbc_key_and_iv(with_uer_info=True) + if key is None or iv is None: + raise ValueError('面板未登录,无法链接云端!') + + AES = AesCryptPy3(key, "CBC", iv, char_set="utf8") + + # 对接云端 + url = "https://www.bt.cn/api/Cert_cloud_deploy/get_cert_list" + try: + res_text = public.httpPost(url, { + "uid": user_info["uid"], + "access_key": user_info["access_key"], + "serverid": user_info["serverid"], + }) + res_data = json.loads(res_text) + if res_data["status"] is False: + raise ValueError("获取云端数据失败") + + res_list = res_data['data'] + except: + raise ValueError("链接云端失败") + + change_set = set() + for data in res_list: + try: + privateKey = AES.aes_decrypt(data["privateKey"]) + certificate = AES.aes_decrypt(data["certificate"]) + cloud_id = data["id"] + change_data = self.save_by_data(certificate, privateKey, cloud_id) + change_set.add(change_data.get("id")) + except: + pass + + all_ids = ssl_db.connection().field("id").select() + for ssl_id in all_ids: + if ssl_id["id"] not in change_set: + ssl_db.connection().where("id = ?", (ssl_id["id"],)).update({"cloud_id": -1}) + + # 从本地收集证书 + def _get_ssl_by_local_data(self): # 从本地获取可用证书 + local_paths = ['/www/server/panel/vhost/cert', '/www/server/panel/vhost/ssl'] + for path in local_paths: + if not os.path.exists(path): + continue + for p_name in os.listdir(path): + pem_file = "{}/{}/fullchain.pem".format(path, p_name) + key_file = "{}/{}/privkey.pem".format(path, p_name) + if os.path.isfile(pem_file) and os.path.isfile(key_file): + try: + self.save_by_file(pem_file, key_file) + except: + pass + + panel_pem_file = "/www/server/panel/ssl/fullchain.pem" + panel_key_file = "/www/server/panel/ssl/privkey.pem" + if os.path.isfile(panel_pem_file) and os.path.isfile(panel_key_file): + try: + self.save_by_file(panel_pem_file, panel_key_file, other_data={"use_for_panel": 1}) + except: + pass + + # 从源储存位置删除 + @classmethod + def _remove_ssl_from_local(cls, ssh_hash: str): + local_path = '/www/server/panel/vhost/ssl' + if not os.path.exists(local_path): + return + + for p_name in os.listdir(local_path): + pem_file = "{}/{}/fullchain.pem".format(local_path, p_name) + + if os.path.isfile(pem_file): + hash_data = cls.ssl_hash(cert_filename=pem_file) + if hash_data == ssh_hash: + shutil.rmtree("{}/{}".format(local_path, p_name)) + + # 查询证书 + @staticmethod + def find_ssl_info(ssl_id=None, ssl_hash=None) -> Optional[dict]: + tmp_conn = ssl_db.connection() + if ssl_id is None and ssl_hash is None: + raise ValueError("没有参数信息") + if ssl_id is not None: + tmp_conn.where("id = ?", (ssl_id,)) + else: + tmp_conn.where("hash = ?", (ssl_hash,)) + + target = tmp_conn.find() + if isinstance(target, str) and target.startswith("error"): + raise ValueError("数据库查询错误:" + target) + + if not bool(target): + return None + + target["auth_info"] = json.loads(target["auth_info"]) + target["use_for_site"] = json.loads(target["use_for_site"]) + target["dns"] = json.loads(target["dns"]) + target["info"] = json.loads(target["info"]) + target['endtime'] = int((datetime.strptime(target['not_after'], "%Y-%m-%d").timestamp() + - datetime.today().timestamp()) / (60 * 60 * 24)) + return target + + @classmethod + def add_use_for_site(cls, site_id, ssl_id=None, ssl_hash=None) -> bool: + return cls.change_use_for_site(site_id, ssl_id, ssl_hash, is_add=True) + + @classmethod + def remove_use_for_site(cls, site_id, ssl_id=None, ssl_hash=None): + return cls.change_use_for_site(site_id, ssl_id, ssl_hash, is_add=False) + + @classmethod + def change_use_for_site(cls, site_id, ssl_id=None, ssl_hash=None, is_add=True): + target = cls.find_ssl_info(ssl_id=ssl_id, ssl_hash=ssl_hash) + if not target: + return False + try: + site_ids = json.loads(target["use_for_site"]) + except: + site_ids = [] + + if site_id in site_ids and is_add is False: + site_ids.remove(site_id) + up_res = ssl_db.connection().where("id = ?", (target["id"],)).update({"use_for_site": json.dumps(site_ids)}) + if isinstance(up_res, str) and up_res.startswith("error"): + raise ValueError("数据库查询错误:" + up_res) + + if site_id not in site_ids and is_add is True: + site_ids.append(site_id) + up_res = ssl_db.connection().where("id = ?", (target["id"],)).update({"use_for_site": json.dumps(site_ids)}) + if isinstance(up_res, str) and up_res.startswith("error"): + raise ValueError("数据库查询错误:" + up_res) + + return True + + def get_all_site_ssl(self): + all_sites = public.M("sites").select() + self.clear_use_for_site() + if isinstance(all_sites, str) and all_sites.startswith("error"): + raise ValueError(all_sites) + for site in all_sites: + prefix = "" if site["project_type"] == "PHP" else site["project_type"].lower() + "_" + tmp = self._get_site_ssl_info(site["name"], prefix=prefix) + if tmp is None: + continue + + hash_data = self.ssl_hash(cert_filename=tmp[0]) + self.add_use_for_site(site["id"], ssl_hash=hash_data) + + @staticmethod + def clear_use_for_site(): + ssl_db.connection().update({"use_for_site": "[]"}) + + @staticmethod + def _get_site_ssl_info(site_name, prefix='') -> Optional[Tuple[str, str]]: + path = os.path.join('/www/server/panel/vhost/cert/', site_name) + + pem_file = os.path.join(path, "fullchain.pem") + key_file = os.path.join(path, "privkey.pem") + if not os.path.isfile(pem_file) or not os.path.isfile(key_file): + path = os.path.join('/etc/letsencrypt/live/', site_name) + pem_file = os.path.join(path, "fullchain.pem") + key_file = os.path.join(path, "privkey.pem") + if not os.path.isfile(pem_file) or not os.path.isfile(key_file): + return None + + webserver = public.get_webserver() + if webserver == "nginx": + conf_file = "{}/vhost/nginx/{}{}.conf".format(public.get_panel_path(), prefix, site_name) + elif webserver == "apache": + conf_file = "{}/vhost/apache/{}{}.conf".format(public.get_panel_path(), prefix, site_name) + else: + conf_file = "{}/vhost/openlitespeed/detail/{}.conf".format(public.get_panel_path(), site_name) + + conf = public.readFile(conf_file) + if not conf: + return None + + if public.get_webserver() == 'nginx': + keyText = 'ssl_certificate' + elif public.get_webserver() == 'apache': + keyText = 'SSLCertificateFile' + else: + keyText = 'openlitespeed/detail/ssl' + + if conf.find(keyText) == -1: + return None + + return pem_file, key_file + + # 删除证书 + def remove_cert(self, ssl_id=None, ssl_hash=None, local: bool = False) -> Dict: + _, _, user_info = self._get_cbc_key_and_iv(with_uer_info=True) + if user_info is None: + raise ValueError('面板未登录,无法上传云端!') + + target = self.find_ssl_info(ssl_id=ssl_id, ssl_hash=ssl_hash) + if not target: + raise ValueError('没有指定的证书') + + if local: + shutil.rmtree(target["path"]) + self._remove_ssl_from_local(target["hash"]) # 把ssl下的也删除 + ssl_db.connection().delete(id=target["id"]) + + if target["cloud_id"] != -1: + url = "https://www.bt.cn/api/Cert_cloud_deploy/del_cert" + try: + res_text = public.httpPost(url, { + "cert_id": target["cloud_id"], + "hashVal": target["hash"], + "uid": user_info["uid"], + "access_key": user_info["access_key"], + "serverid": user_info["serverid"], + }) + res_data = json.loads(res_text) + if res_data["status"] is False: + return res_data + except: + if local: + raise ValueError("本地以删除成功, 链接云端失败, 无法删除云端数据") + raise ValueError("链接云端失败, 无法删除云端数据") + + if not local: + ssl_db.connection().where("id = ?", (target["id"],)).update({"cloud_id": -1}) + + return public.returnMsg(True, "删除成功") + + def mutil_remove_cert(self, ssl_id_list: List[int], local: bool = False): + result = [] + for i in ssl_id_list: + try: + ssl_id = int(i) + except: + result.append({"status": False, "msg": "id信息解析错误"}) + continue + res = self.remove_cert(ssl_id=ssl_id, local=local) + result.append(res) + return result + + # 下载证书 + def upload_cert(self, ssl_id=None, ssl_hash=None) -> Dict: + key, iv, user_info = self._get_cbc_key_and_iv() + if key is None or iv is None: + raise ValueError(False, '面板未登录,无法上传云端!') + + target = self.find_ssl_info(ssl_id=ssl_id, ssl_hash=ssl_hash) + if not target: + raise ValueError("没有指定的证书信息") + + data = { + 'privateKey': public.readFile(target["path"] + '/privkey.pem'), + 'certificate': public.readFile(target["path"] + '/fullchain.pem'), + "encryptWay": "AES-128-CBC", + "hashVal": target['hash'], + "uid": user_info["uid"], + "access_key": user_info["access_key"], + "serverid": user_info["serverid"], + } + if data["privateKey"] is False or data["certificate"] is False: + raise ValueError('证书文件读取错误') + + AES = AesCryptPy3(key, "CBC", iv, char_set="utf8") + data["privateKey"] = AES.aes_encrypt(data["privateKey"]) + data["certificate"] = AES.aes_encrypt(data["certificate"]) + # 对接云端 + url = "https://www.bt.cn/api/Cert_cloud_deploy/cloud_deploy" + + try: + res_text = public.httpPost(url, data) + res_data = json.loads(res_text) + if res_data["status"] is True: + cloud_id = int(res_data["data"].get("id")) + ssl_db.connection().where("id = ?", (target["id"],)).update({"cloud_id": cloud_id}) + + return res_data + else: + return res_data + except: + raise ValueError('链接云端失败') + + # ssl_hash 证书储存记录的唯一值 + def set_site_ssl_conf(self, site_name: str, ssl_data: dict, mutil=False) -> Optional[str]: + privkey = ssl_data["privkey"] + fullchain = ssl_data["fullchain"] + path = '/www/server/panel/vhost/cert/' + site_name + if not os.path.exists(path): + os.makedirs(path) + + csrpath = path + "/fullchain.pem" + keypath = path + "/privkey.pem" + + # 清理旧的证书链 + remove_list = [keypath, csrpath, path + "/certOrderId", path + "/README"] + for i in remove_list: + if os.path.exists(i): + os.remove(i) + public.ExecShell('rm -rf ' + path + '-00*') + public.ExecShell('rm -rf /etc/letsencrypt/archive/' + site_name) + public.ExecShell('rm -rf /etc/letsencrypt/archive/' + site_name + '-00*') + public.ExecShell('rm -f /etc/letsencrypt/renewal/' + site_name + '.conf') + public.ExecShell('rm -f /etc/letsencrypt/renewal/' + site_name + '-00*.conf') + + public.writeFile(keypath, privkey) + public.writeFile(csrpath, fullchain) + error_msg = self._set_ssl_conf_to_nginx(site_name, mutil) + if error_msg is not None and webserver() == "nginx": + return error_msg + error_msg = self._set_ssl_conf_to_apache(site_name, mutil) + if error_msg is not None and webserver() == "apache": + return error_msg + + # http3是否可用 + 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 + + # 在防火墙放行443 + @staticmethod + def open_firewall_443() -> None: + import firewalls + get = GET_CLASS() + get.port = '443' + get.ps = 'HTTPS' + firewalls.firewalls().AddAcceptPort(get) + + # 在nginx配置文件中设置ssl信息 + def _set_ssl_conf_to_nginx(self, site_name, mutil=False) -> Optional[str]: + # Nginx配置 + file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + ng_conf = read_file(file) + if not ng_conf: + return "配置文件丢失,配置失败" + + http3_header = "" + if self.is_nginx_http3(): + http3_header = '''\n add_header Alt-Svc 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"';''' + + if ng_conf.find('ssl_certificate') == -1: + sslStr = """#error_page 404/404.html; + ssl_certificate /www/server/panel/vhost/cert/%s/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/%s/privkey.pem; + ssl_protocols %s; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000";%s + error_page 497 https://$host$request_uri;""" % ( + site_name, site_name, self._get_tls_protocol(is_apache=False), http3_header + ) + + new_ng_conf = ng_conf.replace('#error_page 404/404.html;', sslStr) + # 添加端口 + from .domain_tool import NginxDomainTool + new_ng_conf = NginxDomainTool.nginx_add_port_by_config(new_ng_conf, "443", is_http3=self.is_nginx_http3()) + write_file(file, new_ng_conf) + if webserver() == "nginx" and check_server_config() is not None: + return "配置失败" + if webserver() == "nginx" and not mutil: + service_reload() + self.open_firewall_443() + + # 在apache配置文件中设置ssl信息 + def _set_ssl_conf_to_apache(self, site_name, mutil=False) -> Optional[str]: + ap_file = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + ap_conf = read_file(ap_file) + if not ap_conf: + return "配置文件丢失,配置失败" + + tmp_template_res = re.search(r"", ap_conf) + if not tmp_template_res: + return "配置文件丢失,配置失败" + else: + tmp_template = tmp_template_res.group() + + rep_template_with_ports = re.compile(r"\d+)+\s*>(.|\n)*?") + target_vhost = None + for tmp in rep_template_with_ports.finditer(ap_conf): + if tmp.group("port") == "443": + target_vhost = tmp.group() + + if target_vhost and (target_vhost.find("SSLEngine On") or target_vhost.find("SSLCertificateFile")): + return + if not target_vhost: + rep_ports = re.compile(r"\d+)+\s*>") + target_vhost = rep_ports.sub("", tmp_template, 1) + + # 添加SSL配置 + ssl_conf = """ + #SSL + SSLEngine On + SSLCertificateFile /www/server/panel/vhost/cert/%s/fullchain.pem + SSLCertificateKeyFile /www/server/panel/vhost/cert/%s/privkey.pem + SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5:ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL + SSLProtocol All -SSLv2 -SSLv3 %s + SSLHonorCipherOrder On + """ % (site_name, site_name, self._get_tls_protocol(is_apache=True)) + + rep_list = [ + (re.compile(r"#DENY FILES"), True), + (re.compile(r"CustomLog[^\n]*\n"), False), + ] + + # 使用正则匹配确定插入位置 + def set_by_rep_idx(tmp_rep: re.Pattern, use_start: bool) -> Optional[str]: + tmp_res = tmp_rep.search(target_vhost) + if not tmp_res: + return None + if use_start: + new_conf = target_vhost[:tmp_res.start()] + ssl_conf + tmp_res.group() + target_vhost[tmp_res.end():] + else: + new_conf = target_vhost[:tmp_res.start()] + tmp_res.group() + ssl_conf + target_vhost[tmp_res.end():] + return new_conf + + ssl_vhost = None + for r, s in rep_list: + ssl_vhost = set_by_rep_idx(r, s) + if ssl_vhost is not None: + break + + if ssl_vhost is None: + return "无法定位SSL配置文件位置,配置失败" + + write_file(ap_file, ap_conf + "\n" + ssl_vhost) + # 添加端口 + from .domain_tool import ApacheDomainTool + ApacheDomainTool.apache_add_ports("443") + web_server = webserver() + if web_server == "apache" and check_server_config() is not None: + write_file(ap_file, ap_conf) + return "配置失败" + + if web_server == "apache" and not mutil: + service_reload() + self.open_firewall_443() + + def close_site_ssl_conf(self, site_name) -> Optional[str]: + error_msg = self._close_ssl_conf_to_nginx(site_name) + if error_msg is not None and webserver() == "nginx": + return error_msg + error_msg = self._close_ssl_conf_to_apache(site_name) + if error_msg is not None and webserver() == "apache": + return error_msg + service_reload() + return None + + def _close_ssl_conf_to_nginx(self, site_name) -> Optional[str]: + file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + ng_conf = read_file(file) + if not ng_conf: + return "配置文件丢失,配置失败" + rep_list = ( + re.compile("\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END"), # 关闭 强制https + re.compile(r"\s*ssl_(certificate|certificate_key|protocols|" + r"ciphers|prefer_server_ciphers|session_cache|session_timeout)[^;]*;"), # 关闭 强制https + re.compile(r"\s*add_header\s+(Strict-Transport-Security|Alt-Svc)[^;]*;"), # 关闭 https 请求头配置 + re.compile(r"\s*error_page\s+497\s+[^;]*;"), + re.compile(r"\s+listen\s+(\[::]:)?443.*;"), # 关闭端口监听 + ) + new_conf = ng_conf + for rep in rep_list: + new_conf = rep.sub("", new_conf) + + write_file(file, new_conf) + + def _close_ssl_conf_to_apache(self, site_name) -> Optional[str]: + file = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + ap_conf = read_file(file) + if not ap_conf: + return "配置文件丢失,配置失败" + rep_list = ( + re.compile("\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END"), + re.compile(r"\n(.|\n)*"), + ) + new_conf = ap_conf + for rep in rep_list: + new_conf = rep.sub("", new_conf) + + write_file(file, new_conf) + + def _get_tls_protocol(self, is_apache=False): + """获取使用的协议 + @author baozi <202-04-18> + @param: + @return + """ + protocols = { + "TLSv1": False, + "TLSv1.1": True, + "TLSv1.2": True, + "TLSv1.3": False, + } + tls1_3 = self.get_tls13() + file_path = public.get_panel_path() + "/data/ssl_protocol.json" + if os.path.exists(file_path): + data = public.readFile(file_path) + if data is not False: + protocols = json.loads(data) + if protocols["TLSv1.3"] and tls1_3 == "": + protocols["TLSv1.3"] = False + if is_apache is False: + return " ".join([p for p, v in protocols.items() if v is True]) + else: + return " ".join(["-" + p for p, v in protocols.items() if v is False]) + else: + if tls1_3 != "": + protocols["TLSv1.3"] = True + if is_apache is False: + return " ".join([p for p, v in protocols.items() if v is True]) + else: + return " ".join(["-" + p for p, v in protocols.items() if v is False]) + + # 获取TLS1.3标记 + def get_tls13(self): + if self._tls_v3 is not None: + return self._tls_v3 + nginx_bin = '/www/server/nginx/sbin/nginx' + nginx_v = public.ExecShell(nginx_bin + ' -V 2>&1')[0] + nginx_v_re = re.search(r"nginx/(?P\d\.\d+).+OpenSSL\s+(?P\d\.\d+)", nginx_v) + if nginx_v_re: + ng_ver = nginx_v_re.group("ng_ver") + ssl_ver = nginx_v_re.group("ssl_ver") + if float(ng_ver) >= 1.15 and float(ssl_ver) >= 1.1: + self._tls_v3 = 'TLSv1.3' + else: + can_ng_ver = re.search(r'nginx/1\.(1[5-9]|2\d)', nginx_v) + openssl_v = public.ExecShell(nginx_bin + ' -V 2>&1|grep OpenSSL')[0].find('OpenSSL 1.1.') != -1 + if can_ng_ver and openssl_v: + self._tls_v3 = 'TLSv1.3' + + if self._tls_v3 is None: + self._tls_v3 = '' + return self._tls_v3 + + # HttpToHttps + def set_http_to_https(self, site_name: str): + # Nginx配置 + file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = read_file(file) + if conf: + if conf.find('ssl_certificate') == -1: + return public.returnMsg(False, '当前未开启SSL') + to_str = """#error_page 404/404.html; + #HTTP_TO_HTTPS_START + if ($server_port !~ 443){ + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END +""" + conf = conf.replace('#error_page 404/404.html;', to_str) + write_file(file, conf) + + file = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = public.readFile(file) + if conf: + to_str = ''' + #HTTP_TO_HTTPS_START + + RewriteEngine on + RewriteCond %{SERVER_PORT} !^443$ + RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] + + #HTTP_TO_HTTPS_END + SSLEngine On''' + conf = re.sub('SSLEngine On', to_str, conf, 1) + public.writeFile(file, conf) + + service_reload() + + # CloseToHttps + def close_to_https(self, site_name): + file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = public.readFile(file) + if conf: + rep_https = re.compile(r"(#HTTP_TO_HTTPS_START\s*)?if\s+\(\s*\$server_port\s+!~\s+443\s*\)" + r"[^{]*\{[^}]*}\s*(#HTTP_TO_HTTPS_END\s*)?") + new_conf = rep_https.sub('', conf) + write_file(file, new_conf) + + file = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = public.readFile(file) + if conf: + rep_https = re.compile("\n\\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END") + new_conf = rep_https.sub('', conf) + write_file(file, new_conf) + + service_reload() + + # 是否有跳转到https + def is_to_https(self, site_name) -> bool: + file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + conf = public.readFile(file) + if conf: + if conf.find('HTTP_TO_HTTPS_START') != -1: + return True + if conf.find('$server_port !~ 443') != -1: + return True + return False + + def get_site_ssl_info(self, site_name: str) -> Optional[dict]: + try: + w_s = webserver() + if w_s == 'nginx': + conf_file = '{}/nginx/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + elif w_s == "apach": + conf_file = '{}/apache/{}{}.conf'.format(self._vhost_path, self.conf_prefix, site_name) + else: + return None + + if not os.path.exists(conf_file): + return None + + s_conf = public.readFile(conf_file) + if not s_conf: + return None + if w_s == "apach": + s_tmp = re.findall(r"SSLCertificateFile\s+(.+\.pem)", s_conf) + if not s_tmp: + return None + ssl_file = s_tmp[0] + else: + s_tmp = re.findall(r"ssl_certificate\s+(.+\.pem);", s_conf) + if not s_tmp: + return None + ssl_file = s_tmp[0] + + ssl_info = self.get_cert_info(cert_filename=ssl_file) + if not ssl_info: + return None + ssl_info['endtime'] = int( + int(time.mktime(time.strptime(ssl_info['notAfter'], "%Y-%m-%d")) - time.time()) / 86400) + return ssl_info + except: + return None + + +class SSLManager: + + def __init__(self, conf_prefix: str = ""): + self.conf_prefix = conf_prefix + + def set_site_ssl_conf(self, get): + ssl_id = None + ssl_hash = None + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + site_name = get.site_name.strip() + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "参数错误") + ssl_mgr = RealSSLManger(self.conf_prefix) + try: + info = ssl_mgr.find_ssl_info(ssl_id=ssl_id, ssl_hash=ssl_hash) + if not info: + return json_response(status=False, msg="未查询到证书信息") + ssl_data = ssl_mgr.get_cert_for_deploy(info) + if isinstance(ssl_data, str): + return json_response(status=False, msg=ssl_data) + except ValueError as e: + return json_response(status=False, msg=str(e)) + + err_msg = ssl_mgr.set_site_ssl_conf(site_name=site_name, ssl_data=ssl_data) + if err_msg: + return json_response(status=False, msg=err_msg) + return json_response(status=True, msg="部署成功") + + def mutil_set_site_ssl_conf(self, get): + ssl_id = None + ssl_hash = None + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + site_names = json.loads(get.site_names.strip()) + except (ValueError, AttributeError, KeyError, json.JSONDecodeError): + return public.ReturnMsg(False, "参数错误") + ssl_mgr = RealSSLManger(self.conf_prefix) + try: + info = ssl_mgr.find_ssl_info(ssl_id=ssl_id, ssl_hash=ssl_hash) + if not info: + return json_response(status=False, msg="未查询到证书信息") + ssl_data = ssl_mgr.get_cert_for_deploy(info) + if isinstance(ssl_data, str): + return json_response(status=False, msg=ssl_data) + except ValueError as e: + return json_response(status=False, msg=str(e)) + + result = { + "total": len(site_names), + "success": 0, + "failed": 0, + "success_list": [], + "failed_list": [], + "failed_msg": [] + } + for i in site_names: + err_msg = ssl_mgr.set_site_ssl_conf(site_name=i, ssl_data=ssl_data) + if err_msg: + result["failed"] += 1 + result["failed_list"].append(i) + result["failed_msg"].append(err_msg) + else: + result["success"] += 1 + result["success_list"].append(i) + + return json_response(status=True, data=result) + + def close_site_ssl_conf(self, get): + try: + site_name = get.site_name.strip() + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "参数错误") + + ssl_mgr = RealSSLManger(self.conf_prefix) + try: + err_msg = ssl_mgr.close_site_ssl_conf(site_name) + if err_msg: + return json_response(status=False, msg=err_msg) + return json_response(status=True, msg="关闭成功") + except Exception as e: + return json_response(status=False, msg=str(e)) + + def upload_cert_to_cloud(self, get): + ssl_id = None + ssl_hash = None + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "参数错误") + try: + data = RealSSLManger(self.conf_prefix).upload_cert(ssl_id, ssl_hash) + return json_response(status=True, data=data) + except ValueError as e: + return json_response(status=False, msg=str(e)) + except Exception as e: + return json_response(status=False, msg="操作错误:" + str(e)) + + def remove_cloud_cert(self, get): + ssl_id = None + ssl_hash = None + local = False + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + + if "local" in get and get.local.strip() in ("1", 1, True, "true"): + local = True + + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "参数错误") + try: + data = RealSSLManger(self.conf_prefix).remove_cert(ssl_id, ssl_hash, local=local) + return json_response(status=data.get("status", True), msg=data.get("msg", ""), data=data) + except ValueError as e: + return json_response(status=False, msg=str(e)) + except Exception as e: + return json_response(status=False, msg="操作错误:" + str(e)) + + def mutil_remove_cloud_cert(self, get): + local = False + try: + ssl_id_list = json.loads(get.ssl_id_list.strip()) + if "local" in get and get.local.strip() in ("1", 1, True, "true"): + local = True + + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "参数错误") + try: + data = RealSSLManger(self.conf_prefix).mutil_remove_cert(ssl_id_list, local=local) + return json_response(status=True, data=data) + except ValueError as e: + return json_response(status=False, msg=str(e)) + except Exception as e: + return json_response(status=False, msg="操作错误:" + str(e)) + + # 未使用 + def refresh_cert_list(self, get=None): + try: + data = RealSSLManger(self.conf_prefix).get_cert_list(force_refresh=True) + return json_response(status=True, data=data) + except ValueError as e: + return json_response(status=False, msg=str(e)) + except Exception as e: + return json_response(status=False, msg="操作错误:" + str(e)) + + def get_cert_info(self, get): + ssl_id = None + ssl_hash = None + try: + if "ssl_id" in get: + ssl_id = int(get.ssl_id) + if "ssl_hash" in get: + ssl_hash = get.ssl_hash.strip() + except (ValueError, AttributeError, KeyError): + return public.ReturnMsg(False, "参数错误") + try: + ssl_mager = RealSSLManger(self.conf_prefix) + target = ssl_mager.find_ssl_info(ssl_id, ssl_hash) + if target is None: + return json_response(status=False, msg="未获取到证书信息") + data = ssl_mager.get_cert_for_deploy(target) + if isinstance(data, dict): + target.update(data) + return json_response(status=True, data=target) + else: + return json_response(status=False, msg=data) + except ValueError as e: + return json_response(status=False, msg=str(e)) + except Exception as e: + return json_response(status=False, msg="操作错误:" + str(e)) + + def get_site_ssl_info(self, get): + try: + site_name = get.site_name.strip() + except (ValueError, AttributeError, KeyError): + return json_response(False, "参数错误") + ssl_info = RealSSLManger(self.conf_prefix).get_site_ssl_info(site_name) + if ssl_info is None: + return json_response(status=False, msg="未获取到证书信息") + else: + return json_response(status=True, data=ssl_info) + + def get_cert_list(self, get): + """ + search_limit 0 -> 所有证书 + search_limit 1 -> 没有过期的证书 + search_limit 2 -> 有效期小于等于15天的证书 但未过期 + search_limit 3 -> 过期的证书 + search_limit 4 -> 过期时间1年以上的证书 + """ + search_name = None + search_limit = 0 + force_refresh = False + + try: + if "search_name" in get: + search_name = get.search_name.strip() + if "search_limit" in get: + search_limit = int(get.search_limit.strip()) + if "force_refresh" in get and get.force_refresh.strip() in ("1", 1, "True", True): + force_refresh = True + + except (ValueError, AttributeError, KeyError): + return json_response(status=False, msg="参数错误") + + param = None + if search_name is not None: + param = ['subject LIKE ?', ["%{}%".format(search_name)]] + + now = datetime.now() + filter_func: Callable[[dict, ], bool] = lambda x: True + if search_limit == 1: + date = now.strftime("%Y-%m-%d") + filter_func: Callable[[dict, ], bool] = lambda x: x["not_after"] >= date + elif search_limit == 2: + date1 = now.strftime("%Y-%m-%d") + date2 = (now + timedelta(days=15)).strftime("%Y-%m-%d") + filter_func: Callable[[dict, ], bool] = lambda x: date1 <= x["not_after"] <= date2 + elif search_limit == 3: + date = now.strftime("%Y-%m-%d") + filter_func: Callable[[dict, ], bool] = lambda x: x["not_after"] < date + elif search_limit == 4: + date = (now + timedelta(days=366)).strftime("%Y-%m-%d") + filter_func: Callable[[dict, ], bool] = lambda x: x["not_after"] > date + try: + res_list = RealSSLManger(self.conf_prefix).get_cert_list(param=param, force_refresh=force_refresh) + res_list = list(filter(filter_func, res_list)) + res_list.sort(key=lambda x: x["not_after"]) + return json_response(status=True, data=res_list) + except ValueError as e: + return json_response(False, str(e)) + except Exception as e: + return json_response(False, "操作错误:" + str(e)) + + @staticmethod + def set_ssl_protocol(get): + """ 设置全局TLS版本 + @author baozi <202-04-18> + @param: + @return + """ + protocols = { + "TLSv1": False, + "TLSv1.1": False, + "TLSv1.2": False, + "TLSv1.3": False, + } + if "use_protocols" in get: + use_protocols = getattr(get, "use_protocols", []) + if isinstance(use_protocols, list): + for protocol in use_protocols: + if protocol in protocols: + protocols[protocol] = True + elif isinstance(use_protocols, str): + for protocol in use_protocols.split(","): + if protocol in protocols: + protocols[protocol] = True + else: + protocols["TLSv1.1"] = True + protocols["TLSv1.2"] = True + protocols["TLSv1.3"] = True + + else: + protocols["TLSv1.1"] = True + protocols["TLSv1.2"] = True + protocols["TLSv1.3"] = True + + public.print_log(protocols) + public.WriteFile(public.get_panel_path() + "/data/ssl_protocol.json", json.dumps(protocols)) + return public.returnMsg(True, 'SET_SUCCESS') + + @staticmethod + def get_ssl_protocol(get=None): + """ 获取全局TLS版本 + @author baozi <202-04-18> + @param: + @return + """ + protocols = { + "TLSv1": False, + "TLSv1.1": True, + "TLSv1.2": True, + "TLSv1.3": False, + } + file_path = public.get_panel_path() + "/data/ssl_protocol.json" + if os.path.exists(file_path): + data = public.readFile(file_path) + if data is not False: + protocols = json.loads(data) + return protocols + + return protocols diff --git a/mod/base/web_conf/util.py b/mod/base/web_conf/util.py new file mode 100644 index 00000000..0ebae060 --- /dev/null +++ b/mod/base/web_conf/util.py @@ -0,0 +1,200 @@ +import os +import sys +from typing import Optional, Tuple, Callable + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public + + +def webserver() -> Optional[str]: + if os.path.exists('/www/server/apache/bin/apachectl'): + web_server = 'apache' + elif os.path.exists('/www/server/nginx/sbin/nginx'): + web_server = 'nginx' + elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): + web_server = 'openlitespeed' + else: + web_server = None + return web_server + + +def check_server_config() -> Optional[str]: + w_s = webserver() + setup_path = "/www/server" + if w_s == 'nginx': + shell_str = ( + "ulimit -n 8192; " + "{setup_path}/nginx/sbin/nginx -t -c {setup_path}/nginx/conf/nginx.conf" + ).format(setup_path=setup_path) + result: Tuple[str, str] = public.ExecShell(shell_str) + searchStr = 'successful' + elif w_s == 'apache': + shell_str = ( + "ulimit -n 8192; " + "{setup_path}/apache/bin/apachectl -t" + ).format(setup_path=setup_path) + result: Tuple[str, str] = public.ExecShell(shell_str) + searchStr = 'Syntax OK' + else: + return None + if result[1].find(searchStr) == -1: + public.WriteLog("TYPE_SOFT", 'CONF_CHECK_ERR', (result[1],)) + return result[1] + + +def read_file(filename, mode='r') -> Optional[str]: + """ + 读取文件内容 + @filename 文件名 + return string(bin) 若文件不存在,则返回None + """ + import os + if not os.path.exists(filename): + return None + fp = None + try: + fp = open(filename, mode=mode) + f_body = fp.read() + except: + return None + finally: + if fp and not fp.closed: + fp.close() + return f_body + + +def write_file(filename: str, s_body: str, mode='w+') -> bool: + """ + 写入文件内容 + @filename 文件名 + @s_body 欲写入的内容 + return bool 若文件不存在则尝试自动创建 + """ + try: + fp = open(filename, mode=mode) + fp.write(s_body) + fp.close() + return True + except: + try: + fp = open(filename, mode=mode, encoding="utf-8") + fp.write(s_body) + fp.close() + return True + except: + return False + + +def debug_api_warp(fn): + def inner(*args, **kwargs): + try: + return fn(*args, **kwargs) + except: + public.print_log(public.get_error_info()) + return { + + } + + return inner + + +# 重载Web服务配置 +def service_reload(): + setup_path = "/www/server" + if os.path.exists('{}/nginx/sbin/nginx'.format(setup_path)): + result = public.ExecShell('/etc/init.d/nginx reload') + if result[1].find('nginx.pid') != -1: + public.ExecShell('pkill -9 nginx && sleep 1') + public.ExecShell('/etc/init.d/nginx start') + elif os.path.exists('{}/apache/bin/apachectl'.format(setup_path)): + result = public.ExecShell('/etc/init.d/httpd reload') + else: + result = public.ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart') + return result + + +# 防正则转译 +def pre_re_key(input_str: str) -> str: + re_char = ['$', '(', ')', '*', '+', '.', '[', ']', '{', '}', '?', '^', '|', '\\'] + res = [] + for i in input_str: + if i in re_char: + res.append("\\" + i) + else: + res.append(i) + return "".join(res) + + +def get_log_path() -> str: + log_path = public.readFile("{}/data/sites_log_path.pl".format(public.get_panel_path())) + if isinstance(log_path, str) and os.path.isdir(log_path): + return log_path + return public.GetConfigValue('logs_path') + + + +# 2024/4/18 上午9:44 域名编码转换 +def to_puny_code(domain): + try: + try: + import idna + except: + os.system("btpip install idna -I") + import idna + + import re + 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 + except: + return domain + + +# 2024/4/18 下午5:48 中文路径处理 +def to_puny_code_path(path): + if sys.version_info[0] == 2: path = path.encode('utf-8') + if os.path.exists(path): return path + import re + match = re.search(u"[\x80-\xff]+", path) + if not match: match = re.search(u"[\u4e00-\u9fa5]+", path) + if not match: return path + npath = '' + for ph in path.split('/'): + npath += '/' + to_puny_code(ph) + return npath.replace('//', '/') + + + +class _DB: + + def __call__(self, table: str): + import db + with db.Sql() as t: + t.table(table) + return t + + +DB = _DB() + +GET_CLASS = public.dict_obj + +listen_ipv6: Callable[[], bool] = public.listen_ipv6 + +ExecShell: Callable = public.ExecShell + + +def use_http2() -> bool: + versionStr = public.readFile('/www/server/nginx/version.pl') + if isinstance(versionStr, str): + if versionStr.find('1.8.1') == -1: + return True + return False diff --git a/mod/common/__init__.py b/mod/common/__init__.py new file mode 100644 index 00000000..fecc39d2 --- /dev/null +++ b/mod/common/__init__.py @@ -0,0 +1,8 @@ +from .limit_net import LimitNet +from .redirect import Redirect + + +__all__ = [ + "LimitNet", + "Redirect" +] diff --git a/mod/common/base.py b/mod/common/base.py new file mode 100644 index 00000000..bad1f6ee --- /dev/null +++ b/mod/common/base.py @@ -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) diff --git a/mod/common/limit_net.py b/mod/common/limit_net.py new file mode 100644 index 00000000..f044654d --- /dev/null +++ b/mod/common/limit_net.py @@ -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, "参数错误") + + if self.config_prefix is None: + return public.returnMsg(False, "不支持的网站类型") + + # 取配置文件 + 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, "配置文件读取错误") + + # 站点总并发 + data = { + 'perserver': 0, + 'perip': 0, + 'limit_rate': 0, + } + + rep_per_server = re.compile(r"(?P.*)limit_conn +perserver +(?P\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.*)limit_conn +perip +(?P\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.*)limit_rate +(?P\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, "全局配置缓存配置失败" + 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, "全局配置缓存配置失败" + 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, "参数错误") + + if per_server < 1 or perip < 1 or limit_rate < 1: + return public.returnMsg(False, '并发限制,IP限制,流量限制必需大于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:
                                    ' + is_error.replace("\n", '
                                    ') + '
                                    ') + + public.serviceReload() + public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_OPEN_SUCCESS', (site_name,)) + return public.returnMsg(True, 'SET_SUCCESS') + + # 关闭流量限制 + 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:
                                    ' + is_error.replace("\n", '
                                    ') + '
                                    ') + public.serviceReload() + public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_CLOSE_SUCCESS', (site_name,)) + return public.returnMsg(True, 'SITE_NETLIMIT_CLOSE_SUCCESS') diff --git a/mod/common/redirect.py b/mod/common/redirect.py new file mode 100644 index 00000000..cff6683a --- /dev/null +++ b/mod/common/redirect.py @@ -0,0 +1,746 @@ +import os +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 .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 = """ + + RewriteEngine on + RewriteCond %%{HTTP_HOST} ^%s [NC] + RewriteRule ^(.*) %s%s [L,R=%s] + +""" + _ap_path_format = """ + + RewriteEngine on + RewriteRule ^%s(.*) %s%s [L,R=%s] + +""" + + 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 '配置文件出错请先排查配置' + + 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 '参数错误' + + if not is_modify: + if not redirect_name: + return "参数错误,配置名称不能为空" + # 检测名称是否重复 + if not (3 < len(redirect_name) < 15): + return '名称必须大于3小于15个字符串' + + if self._check_redirect(site_name, redirect_name, error_page == 1): + return '指定重定向名称已存在' + + site_info = public.M('sites').where("name=?", (site_name,)).find() + if not isinstance(site_info, dict): + return "站点信息查询错误" + 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 '目标URL格式不对【%s】' % to_url + + # 非404页面de重定向检测项 + if error_page != 1: + # 检测是否选择域名 + if domain_or_path == "domain": + if not redirect_domain: + return '请选择重定向域名' + # 检测域名是否已经存在配置文件 + repeat_domain = self._check_redirect_domain_exist(site_name, redirect_domain, redirect_name, is_modify) + if repeat_domain: + return '重定向域名重复 %s' % repeat_domain + + # 检查目标URL的域名和被重定向的域名是否一样 + tu = self._parse_url_domain(to_url) + for d in redirect_domain: + if d == tu: + return '域名 "%s" 和目标域名一致请取消选择' % d + else: + if not redirect_path: + return '请输入重定向路径' + if redirect_path[0] != "/": + return "路径格式不正确,格式为/xxx" + # 检测路径是否有存在配置文件 + if self._check_redirect_path_exist(site_name, redirect_path, redirect_name): + return '重定向路径重复 %s' % redirect_path + + to_url_path = self._parse_url_path(to_url) + if to_url_path.startswith(redirect_path): + return '目标URL[%s]以被重定向的路径[%s]开头,会导致循环匹配' % (to_url_path, redirect_path) + # 404页面重定向检测项 + else: + if not to_url and not to_path: + return '首页或自定义页面必须二选一' + 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, '创建成功') + + 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, '修改成功') + + 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, "nginx配置文件读取失败" + + rep_include = re.compile(r"\sinclude +.*/redirect/.*\*\.conf;", re.M) + if rep_include.search(ng_conf): + return True, "" + redirect_include = ( + "#SSL-END\n" + " #引用重定向规则,注释后配置的重定向代理将无效\n" + " include {}/*.conf;" + ).format(ng_redirect_dir) + + if "#SSL-END" not in ng_conf: + return False, "添加配置失败,无法定位SSL相关配置的位置" + + 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, "添加配置失败" + + 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, "nginx配置文件读取失败" + + 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, "移除配置失败" + + 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, "apache配置文件读取失败" + + 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("
                                    ") == 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 # 引用重定向规则,注释后配置的重定向代理将无效\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, "设置失败" + + 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, "apache配置文件读取失败" + + 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, "移除配置失败" + + 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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + 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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + + 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.*)error_page +404 +/404\.html[^\n]*\n', re.M) + rep_location_404 = re.compile(r'(?P.*)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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + + 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 + + RewriteEngine on + RewriteCond %{{REQUEST_FILENAME}} !-f + RewriteCond %{{REQUEST_FILENAME}} !-d + RewriteRule . {} [L,R={}] + +#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: 配置出错
                                    ' + isError.replace("\n", '
                                    ') + '
                                    ' + + def remove_redirect(self, get, multiple=None): + try: + site_name = get.sitename.strip() + redirect_name = get.redirectname.strip() + except AttributeError: + return public.returnMsg(False, "参数错误") + target_idx = None + have_other_redirect = False + target_conf = None + for i, conf in enumerate(self.config): + 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 not target_idx: + return public.returnMsg(False, '没有指定的配置') + + 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, '删除成功') + + 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, "参数错误") + 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.serviceReload() + return { + 'status': True, + 'msg': '删除重定向 [ {} ] 成功'.format(','.join(del_successfully)), + 'error': del_failed, + 'success': del_successfully + } + + 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 (AttributeError, ValueError, TypeError): + return public.returnMsg(False, "参数错误") + 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页面'] + + 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 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 Redirect(BaseProjectCommon): + # 匹配目标URL的域名并返回 + + 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) + + def create_project_redirect(self, get): + if not isinstance(self.config_prefix, str): + return public.returnMsg(False, "不支持的网站类型") + return _RealRedirect(self.config_prefix).create_redirect(get) + + def modify_project_redirect(self, get): + if not isinstance(self.config_prefix, str): + return public.returnMsg(False, "不支持的网站类型") + + return _RealRedirect(self.config_prefix).modify_redirect(get) + + def remove_project_redirect(self, get): + if not isinstance(self.config_prefix, str): + return public.returnMsg(False, "不支持的网站类型") + return _RealRedirect(self.config_prefix).remove_redirect(get) + + def mutil_remove_project_redirect(self, get): + if not isinstance(self.config_prefix, str): + return public.returnMsg(False, "不支持的网站类型") + + return _RealRedirect(self.config_prefix).mutil_remove_redirect(get) + + def get_project_redirect_list(self, get): + public.print_log(get) + if not isinstance(self.config_prefix, str): + return public.returnMsg(False, "不支持的网站类型") + return _RealRedirect(self.config_prefix).get_redirect_list(get) diff --git a/mod/modController.py b/mod/modController.py new file mode 100644 index 00000000..c2fe11b8 --- /dev/null +++ b/mod/modController.py @@ -0,0 +1,100 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- + +# ------------------------------ +# 网站模型管理控制器 +# ------------------------------ +import json +import public +import re + + +class Controller: + + def __init__(self): + pass + + def model(self, args): + ''' + @name 调用指定项目模型 + @author wzz <2024/1/24 上午 11:07> + @param {"mod_name":"string<模型名称>","def_name":"string<方法名称>","data":JSON,} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: return public.return_status_code(1000, '错误的调用!') + public.exists_args('def_name,mod_name', args) + if args['def_name'].find('__') != -1: return public.return_status_code(1000, + '调用的方法名称中不能包含“__”字符') + if not re.match(r"^\w+$", args['mod_name']): return public.return_status_code(1000, + r'调用的模块名称中不能包含\w以外的字符') + if not re.match(r"^\w+$", args['def_name']): return public.return_status_code(1000, + r'调用的方法名称中不能包含\w以外的字符') + except: + return public.get_error_object() + # 参数处理 + module_name = args['mod_name'].strip() + sub_mod_name = args['sub_mod_name'].strip() + mod_name = "{}Mod".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() + model_index = None + if 'model_index' in args: model_index = args['model_index'] + + if not hasattr(args, 'data'): args.data = {} + if args.data: + if isinstance(args.data, str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.get_error_object() + else: + pdata = args.data + else: + pdata = args + + if isinstance(pdata, dict): + pdata = public.to_dict_obj(pdata) + + if not isinstance(pdata, public.dict_obj): + return public.return_error("传递的参数不是通用的内部对象") + + # 告诉加载器,要加载什么模块 + if model_index: pdata.model_index = model_index + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(), def_name.upper()) + hook_result = public.exec_hook(hook_index, pdata) + if isinstance(hook_result, public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result, dict): + return hook_result # 响应具体错误信息 + elif isinstance(hook_result, bool): + if not hook_result: # 直接中断操作 + return public.return_data(False, {}, error_msg='前置HOOK中断操作') + + # 调用处理方法 + # result = run_object(pdata) + import PluginLoader + result = PluginLoader.module_run("{}/{}".format(module_name, sub_mod_name), def_name, pdata) + if isinstance(result, dict): + if 'status' in result and result['status'] == False and 'msg' in result: + if isinstance(result['msg'], str): + if result['msg'].find('Traceback ') != -1: + raise public.PanelError(result['msg']) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(), def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index, hook_data) + if isinstance(hook_result, dict): + result = hook_result['result'] + return result diff --git a/mod/project/docker/__init__.py b/mod/project/docker/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/project/docker/routetestMod.py b/mod/project/docker/routetestMod.py new file mode 100644 index 00000000..abca8bf9 --- /dev/null +++ b/mod/project/docker/routetestMod.py @@ -0,0 +1,65 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import os +# ------------------------------ +# Docker模型 +# ------------------------------ +import sys + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +os.chdir("/www/server/panel") +import public + + +class main(): + + def returnResult(self, get): + ''' + @name 模型测试方法,请求方式 + /mod/docker/routetestMod/returnResult + 支持form-data和json + + 使用通用的响应对象,返回json格式数据 + @author wzz <2024/2/19 上午 10:37> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + print(public.returnResult(msg="hello")) + return public.returnResult(msg="hello") + + def wsRequest(self, get): + """ + 处理websocket,ws测试方法,请求方式 + ws://192.168.x.x:8888/ws_mod + 连接成功后先发送第一条信息{"x-http-token":"token"} + 然后再发第二条信息,信息内容如下格式 + + 备注:如果需要使用apipost测试,请将__init__.py中ws模型路由的comReturn和csrf检查注释掉再测试 + @param get: + {"mod_name":"docker","sub_mod_name":"routetest","def_name":"wsRequest","ws_callback":"111"} + {"mod_name":"模型名称","sub_mod_name":"子模块名称","def_name":"函数名称","ws_callback":"ws必传参数,传111",其他参数接后面} + @return: + """ + if not hasattr(get, "_ws"): + return True + + import time + sum = 0 + while sum < 10: + time.sleep(0.2) + get._ws.send("hello\r\n") + sum += 1 + + return True + + +if __name__ == '__main__': + main().returnResult({}) diff --git a/mod/project/java/__init__.py b/mod/project/java/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/project/java/groupMod.py b/mod/project/java/groupMod.py new file mode 100644 index 00000000..108a1991 --- /dev/null +++ b/mod/project/java/groupMod.py @@ -0,0 +1,1169 @@ +import copy +import json +import os.path +import sys +import time +import psutil +from typing import Optional, Dict, Union, List, Tuple, Any, Iterable + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public + +from mod.base import RealServer, json_response +from mod.project.java import utils +from mod.project.java.projectMod import debug, main as JavaProject + + +class Group: + GROUP_DATA_DIR = "/www/server/panel/data/java_group" + GROUP_TMP_DIR = "/var/tmp/springboot/group" + + def __init__(self, group_id: Optional[str] = None, group_data: Optional[dict] = None): + self.group_id = group_id + if isinstance(group_data, dict): + self.config: Optional[Dict[str, Union[str, List]]] = group_data + else: + self.config = self.load_group_data_by_id() + + if not os.path.exists(self.GROUP_DATA_DIR): + os.makedirs(self.GROUP_DATA_DIR, 0o600) + + if not os.path.exists(self.GROUP_TMP_DIR): + os.makedirs(self.GROUP_TMP_DIR) + + self.running_data = { + "length": 0, # 已完成的数量 + "remaining": 0, # 未完成的数量 + "executing": 0, # 正在执行的数量 + "projects": [], # 项目操作详情 + "msg": "", # 信息 + "status": True, + } + + # 如果有配置文件,则表示可以执行 + def can_running(self) -> bool: + return isinstance(self.config, dict) + + @staticmethod + def new_group_id() -> str: + from uuid import uuid4 + return uuid4().hex[::2] + + # 从配置文件中加载数据 + def load_group_data_by_id(self) -> Optional[dict]: + config_file = "{}/{}.json".format(self.GROUP_DATA_DIR, self.group_id) + try: + data = json.loads(public.readFile(config_file)) + except: + return None + if isinstance(data, dict): + return data + else: + return None + + def save_group_data(self): + if self.config: + config_file = "{}/{}.json".format(self.GROUP_DATA_DIR, self.group_id) + public.writeFile(config_file, json.dumps(self.config)) + + # 更新旧版数据 + @classmethod + def update_group_data(cls) -> None: + json_file = "/www/server/panel/class/projectModel/java_project_groups.json" + if not os.path.isfile(json_file): + return + data_str = public.readFile(json_file) + try: + data = json.loads(data_str) + except: + os.remove(json_file) + return + + try: + java_projects = public.M("sites").where('project_type=?', ('Java',)).field("id,name").select() + except: + return + + projects_dict = {i["name"]: i["id"] for i in java_projects} + + for idx, i in enumerate(data): + name = i.get("group_name", "默认分组-{}".format(idx + 1)) + projects = i.get("projects", []) + order = i.get("order", []) # type: list + tmp_p = [] + for p in projects: + if p["project_name"] in projects_dict and p["project_name"] in order: + tmp_p.append({ + "id": projects_dict[p["project_name"]], + "name": p["project_name"], + "level": order.index(p["project_name"]) + 1, + "check_info": { + "type": "port", + "port": [], + "wait_time": 180, + } + }) + + if tmp_p: + group_id = cls.new_group_id() + public.writeFile( + "{}/{}.json".format(cls.GROUP_DATA_DIR, group_id), + json.dumps({ + "group_name": name, + "projects": tmp_p, + "sort_type": "sequence", + }) + ) + + if os.path.isfile(json_file): + os.remove(json_file) + + # 检查数据 + def check_group_data(self) -> Optional[str]: + """ + 一个组的格式 + group = { + "group_name": "aaa", + "projects": [ + { + "id": 84, + "name": "tduck-api", + "level": 1, + "check_info": { + "type": ("port" or "active"), + "port": [8456, 8511], + "wait_time": 180, + } + } + ], + "sort_type": ("simultaneous" or "sequence") + } + """ + # 检查self.config是否为字典 + if not isinstance(self.config, dict): + return "参数格式错误" + + # 检查'sort_type'键是否存在且值为("simultaneous" or "sequence") + if "sort_type" not in self.config or self.config["sort_type"] not in ("simultaneous", "sequence"): + return "编排方式设置错误" + + if 'group_name' not in self.config or not isinstance(self.config["group_name"], str) \ + or not self.config["group_name"]: + return "分组名称设置错误" + + # 检查projects键是否存在且为列表 + if "projects" not in self.config or not isinstance(self.config["projects"], list): + return "项目列表设置错误" + + # 遍历projects列表中的每个项目 + for project in self.config["projects"]: + # 检查每个项目是否为字典 + if not isinstance(project, dict): + return "项目列表设置错误" + + # 检查'id', 'name', 'level', 'check_info'键是否存在 + if not all(key in project for key in ("id", "name", "level", "check_info")): + return "项目项目配置信息缺失" + + # 检查id、level是否为整数 + if not isinstance(project["id"], int) or not isinstance(project["level"], int): + return "项目id或优先级参数格式错误" + + # 检查'check_info'是否为字典 + if not isinstance(project["check_info"], dict): + return "项目{}检查策略设置错误".format(project["name"]) + + # 检查'check_info'中的type, 'port', 'wait_time' + if not all(key in project["check_info"] for key in ("type", "port", "wait_time")): + return "项目{}检查策略信息缺失".format(project["name"]) + + # 检查type的值 + if project["check_info"]["type"] not in ("port", "active"): + return "项目{}检查策略类型设置错误".format(project["name"]) + + # 如果所有检查都通过,则数据格式正确 + return None + + # 执行 check_info 中的判断,返回现在的进程是否属于在运行中, 如果不是运行中, 就是异常 + @staticmethod + def do_check_info(check_info: dict, process: psutil.Process): + if check_info["type"] == "port": + timeout = time.time() > process.create_time() + 60 * 3 + listen = [] + connections = process.connections() + for connection in connections: + if connection.status == "LISTEN": + listen.append(connection.laddr.port) + + if not check_info["port"]: + if not timeout and not bool(listen): + return "waiting" + if not bool(listen): + return 'failed' + else: + return 'succeeded' + else: + res = not bool(set(check_info["port"]) - set(listen)) # 如果所有的端口都在监听中,则返回True + if not timeout and not res: + return 'waiting' + if res: + return 'succeeded' + else: + return 'failed' + else: + create_time = process.create_time() + if time.time() > create_time + int(check_info["wait_time"]): + return "succeeded" + else: + return "waiting" + + @staticmethod + def is_running(pid: int): + try: + return psutil.Process(pid).is_running() + except: + return False + + # 获取运行状态信息 + def run_status(self, last_write_time: float) -> dict: + default_error = { + "running": False, + "msg": "操作出错,已退出", + "running_data": None, + "last_write_time": 0, + } + pid_file = "{}/{}.pid".format(Group.GROUP_TMP_DIR, self.group_id) + log_file = "{}/{}.log".format(Group.GROUP_TMP_DIR, self.group_id) + if not os.path.isfile(pid_file): + return default_error + try: + pid = int(public.readFile(pid_file)) + except: + return default_error + + try: + data = json.loads(public.readFile(log_file)) + except: # 如果读取出错,则说明进程出现问题,则杀死进程,并清除数据,返回错误 + if os.path.isfile(log_file): + os.remove(log_file) + if os.path.isfile(pid_file): + os.remove(pid_file) + if self.is_running(pid): + psutil.Process(pid).kill() + + return default_error + + # 如果进程不在运行,则返回上一次运行的数据(防止卡顿导致导致最后的数据没有读取到) + if not self.is_running(pid): + return { + "running": False, + "msg": "操作进程已退出", + "running_data": data, + "last_write_time": os.path.getmtime(log_file), + } + + # 如果还在运行, 则进行长链接, 等待最多5秒, 如果没有数据被写入, 且进程依旧在运行,则返回上一次的数据; + # 若不在运行则说明,且没有写入则说明等待期间出错了 (启动进程退出前必定会进行一次写入) + # 如果有数据被写入则返回最新数据 + + for i in range(50): + m_time = os.path.getmtime(log_file) + if abs(m_time - last_write_time) > 0.001: # 如果时间差大于0.001秒,则说明数据再次被写入了,则退出循环,返回数据 + now_write_time = m_time + break + elif i == 49: # 最后一次检测后,直接退出循环 + continue + else: + time.sleep(0.1) + else: # 如果 5 秒内没有数据被写入,则返回上一次的数据 + if not self.is_running(pid): # 不在运行, 说明等待期间出错了 + if os.path.isfile(log_file): + os.remove(log_file) + if os.path.isfile(pid_file): + os.remove(pid_file) + return default_error + + # 在运行,则返回上一次的数据 + return { + "running": self.is_running(pid), + "msg": "运行中", + "running_data": data, + "last_write_time": last_write_time, + } + + # + try: + data = json.loads(public.readFile(log_file)) + except: + if self.is_running(pid): + psutil.Process(pid).kill() + if os.path.isfile(log_file): + os.remove(log_file) + if os.path.isfile(pid_file): + os.remove(pid_file) + return { + "running": False, + "msg": "操作进程出错,已退出", + "running_data": None, + "last_write_time": 0, + } + running = self.is_running(pid) + return { + "running": running, + "msg": "运行中" if running else "操作进程已结束", + "running_data": data, + "last_write_time": now_write_time, + } + + # 获取可操作状态的信息 + def get_operation_info(self) -> Tuple[Iterable[str], str]: + pid_file = "{}/{}.pid".format(Group.GROUP_TMP_DIR, self.group_id) + try: + pid = int(public.readFile(pid_file)) + p = psutil.Process(pid) + if not p.is_running(): + return ("start", "stop"), "" + else: + last_cmd = p.cmdline()[-2] if len(p.cmdline()) == 4 else "" + return ("termination",), last_cmd + + except: + pass + return ("start", "stop"), "" + + def group_info(self, project_cache: Optional[dict] = None) -> Optional[dict]: + if not self.config: + return + if not project_cache: + project_ids = [i["id"] for i in self.config["projects"]] + if not project_ids: + data = copy.deepcopy(self.config) + data["group_id"] = self.group_id + return data + + projects = public.M('sites').where( + 'project_type=? and id IN ({})'.format(",".join(["?"] * len(project_ids))), + ('Java', *project_ids)).select() + project_cache = {} + for i in projects: + i["project_config"] = json.loads(i["project_config"]) + project_cache[i["id"]] = i + + j_pro = JavaProject() + + res = copy.deepcopy(self.config) + res["group_id"] = self.group_id + res["operation_info"], res["now_operation"] = self.get_operation_info() + running_num = 0 + for i in res["projects"]: + if i["id"] in project_cache: + try: + listen = [] + pid = j_pro.get_project_pid(project_cache[i["id"]]) + p = psutil.Process(int(pid)) + connections = p.connections() + for connection in connections: + if connection.status == "LISTEN": + listen.append(connection.laddr.port) + except: + i["running"] = False + i["project_status"] = 'not_run' + i["pid"] = None + i["listen"] = [] + continue + + running_num += 1 + i["running"] = True + i["project_status"] = self.do_check_info(i["check_info"], p) + i["pid"] = p.pid + i["listen"] = listen + + if not res["now_operation"]: + all_num = len(res["projects"]) + if all_num == 0: + res["operation_info"] = [] + else: + if running_num == all_num: + res["operation_info"] = ["stop"] + if running_num == 0: + res["operation_info"] = ["start"] + + return res + + # 运行一个Group相关的函数 + # ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ + def run_operation(self, operation: str) -> Optional[str]: + pid_file = "{}/{}.pid".format(Group.GROUP_TMP_DIR, self.group_id) + + if operation not in ("start", "stop"): + return "指定的操作不存在" + + try: + p = psutil.Process(public.readFile(pid_file)) + if p.is_running(): + return "操作运行中, 请等待上一个操作执行完成" + except: + pass + + if os.path.exists(pid_file): + os.remove(pid_file) + + self.build_run_sort(reverse=(operation == "stop")) + self.save_running_data() + panel_path = "/www/server/panel" + public.ExecShell( + "nohup {}/pyenv/bin/python3 {}/mod/project/java/group_script.py {} {} &> /tmp/group_script.log & \n" + "echo $! > {} ".format( + panel_path, panel_path, operation, self.group_id, pid_file) + ) + return None + + def build_run_sort(self, reverse=False) -> List[List[dict]]: + projects = self.config["projects"] # type: List[Dict[str, Union[int, str, dict]]] + if self.config["sort_type"] == "simultaneous": + projects_group = [projects] + else: + projects_group = [] + projects.sort(key=lambda x: x['level']) + last_level = -1 + for i in projects: + if i['level'] != last_level: + last_level = i['level'] + projects_group.append([i]) + else: + projects_group[-1].append(i) + + self.running_data['length'] = len(projects) + self.running_data['remaining'] = len(projects) + for g in projects_group: + names = [i["name"] for i in g] + self.running_data["projects"].append({ + "names": names, + "msg": "", + "running": False, + "data": { + i["name"]: {"status": False, "msg": "", "pid": 0, "name": i["name"]} + for i in g + } + }) + + if reverse: + self.running_data["projects"] = self.running_data["projects"][::-1] + return projects_group[::-1] + + return projects_group + + # 真正执行启动的逻辑 + def real_run_start(self): + pid_file = "{}/{}.pid".format(Group.GROUP_TMP_DIR, self.group_id) + pid = os.getpid() + public.writeFile(pid_file, str(pid)) + if not self.can_running(): + self.running_data["msg"] = "项目组配置文件错误,无法启动该项目组" + self.running_data["status"] = False + self.save_running_data() + return + + # 构建运行启动的信息,并返回运行顺序 + self.running_data["msg"] = "启动操作运行中" + run_sort = self.build_run_sort(reverse=False) + self.save_running_data() + for idx, g in enumerate(run_sort): + res_msg = self._run_start_one_step(g, sort_idx=idx) + if isinstance(res_msg, str): + self.running_data["msg"] = res_msg + self.running_data["status"] = False + self.save_running_data() + return + + self.running_data["msg"] = "启动操作运行成功" + self.running_data["status"] = True + self.save_running_data() + + def save_running_data(self): + log_file = "{}/{}.log".format(Group.GROUP_TMP_DIR, self.group_id) + public.writeFile(log_file, json.dumps(self.running_data)) + + # 执行启动任务的每一个优先级中所有的项目 + def _run_start_one_step(self, projects: List[dict], sort_idx: int) -> Optional[str]: + j_pro = JavaProject() + + step_running_data = self.running_data["projects"][sort_idx] # type: Dict[str, Any] + step_running_data["running"] = True + step_running_data["msg"] = "正在启动【{}】项目".format(", ".join(step_running_data["names"])) + self.running_data["remaining"] -= len(projects) + self.running_data["executing"] = len(projects) + self.save_running_data() + + need_wait_project = [] + error_msg = None + # 尝试启动每一个项目 + for p in projects: + project_data = j_pro.get_project_find(p["name"]) + if not project_data: + step_running_data["data"][p["name"]]["msg"] = "项目【{}】已丢失,无法启动".format(p["name"]) + continue + project_pid = j_pro.get_project_pid(project_data) + if project_pid: + step_running_data["data"][p["name"]]["status"] = True + step_running_data["data"][p["name"]]["msg"] = "项目【{}】已处于运行状态,未重新执行启动操作".format( + p["name"]) + step_running_data["data"][p["name"]]["pid"] = project_pid + continue + + res = j_pro.start_spring_boot_project(project_data, wait=False) # 不等待启动成功 + if not res["status"]: + step_running_data["data"][p["name"]]["status"] = False + step_running_data["data"][p["name"]]["msg"] = "在执行启动项目【{}】指令的时出现错误:{}".format( + p["name"], res["msg"]) + + error_msg = "出现无法处理的项目启动问题:" + res["msg"] + break # 出现无法启动的项目,直接退出 + else: + need_wait_project.append((project_data, p["check_info"])) + + if error_msg: + return error_msg + + if not need_wait_project: + return None + + self.save_running_data() + time.sleep(0.5) + while need_wait_project: + remove_idx = [] + error_projects = [] + change = False + for idx, (pd, check_info) in enumerate(need_wait_project): + if "process" not in pd or not isinstance(pd["process"], psutil.Process): + try: + pid = j_pro.get_project_pid(pd) + process = psutil.Process(int(pid)) + except: + process = None + else: + process = pd["process"] + + if not isinstance(process, psutil.Process) or not process.is_running(): + step_running_data["data"][pd["name"]]["status"] = False + step_running_data["data"][pd["name"]]["msg"] = "项目【{}】启动失败,详细情况请查看日志".format( + pd["name"]) + change = True + remove_idx.append(idx) + error_projects.append(pd["name"]) + continue + else: + if step_running_data["data"][pd["name"]]["pid"] == 0: + step_running_data["data"][pd["name"]]["pid"] = process.pid + change = True + + tmp_res = self.do_check_info(check_info, process) + if tmp_res == "succeeded": + step_running_data["data"][pd["name"]]["status"] = True + step_running_data["data"][pd["name"]]["msg"] = "项目【{}】启动成功".format(pd["name"]) + remove_idx.append(idx) + self.running_data["executing"] -= 1 + change = True + elif tmp_res == "failed": + step_running_data["data"][pd["name"]]["status"] = False + step_running_data["data"][pd["name"]]["msg"] = "项目【{}】启动失败,详细情况请查看日志".format( + pd["name"]) + remove_idx.append(idx) + error_projects.append(pd["name"]) + change = True + continue + + if remove_idx: + for idx in remove_idx: + need_wait_project.pop(idx) + + if error_projects: + return "项目【{}】启动失败无法继续执行启动操作,请查看日志".format(",".join(error_projects)) + if change is True: + self.save_running_data() + if need_wait_project: + time.sleep(0.2) + + step_running_data["running"] = False + self.save_running_data() + return + + def real_run_stop(self): + pid_file = "{}/{}.pid".format(Group.GROUP_TMP_DIR, self.group_id) + pid = os.getpid() + public.writeFile(pid_file, str(pid)) + if not self.can_running(): + self.running_data["msg"] = "项目组配置文件错误,无法停止该项目组" + self.running_data["status"] = False + self.save_running_data() + return + + # 构建运行启动的信息,并返回运行顺序 + self.running_data["msg"] = "项目组停止操作运行中" + run_sort = self.build_run_sort(reverse=True) + self.save_running_data() + for idx, g in enumerate(run_sort): + self._run_stop_one_step(g, sort_idx=idx) + + self.running_data["msg"] = "停止操作运行成功" + self.running_data["status"] = True + self.save_running_data() + + def _run_stop_one_step(self, projects: List[dict], sort_idx: int) -> None: + j_pro = JavaProject() + + step_running_data = self.running_data["projects"][sort_idx] # type: Dict[str, Any] + step_running_data["running"] = True + step_running_data["msg"] = "正在执行【{}】项目的停止任务".format(", ".join(step_running_data["names"])) + self.running_data["remaining"] -= len(projects) + self.running_data["executing"] = len(projects) + + need_wait_project = [] + # 尝试停止每一个项目 + for p in projects: + project_data = j_pro.get_project_find(p["name"]) + if not project_data: + step_running_data["data"][p["name"]]["msg"] = "项目【】已丢失,无法执行停止操作" + continue + + project_pid = j_pro.get_project_pid(project_data) + if not project_pid: + step_running_data["data"][p["name"]]["status"] = True + step_running_data["data"][p["name"]]["msg"] = "项目【{}】已停止".format(p["name"]) + continue + else: + project_data["pid"] = project_pid + + project_config = project_data["project_config"] + server_name = "spring_" + project_config["project_name"] + project_config.get("server_name_suffix", "") + s_admin = RealServer() + if s_admin.daemon_status(server_name)["msg"] == "服务不存在!": + j_pro.stop_by_kill_pid(project_data) + if os.path.isfile(project_config["pids"]): + os.remove(project_config["pids"]) + else: + s_admin.daemon_admin(server_name, "stop") + utils.stop_by_user(project_data["id"]) + need_wait_project.append((project_data, p["check_info"])) + + if not need_wait_project: + return None + + self.save_running_data() + time.sleep(0.05) + wait_num = 0 + while need_wait_project and wait_num < 10: + remove_idx = [] + change = False + for idx, (pd, check_info) in enumerate(need_wait_project): + pid = pd["pid"] + pid_info = j_pro.real_process.get_process_info_by_pid(pid)["data"] + if not pid_info: + step_running_data["data"][pd["name"]]["status"] = True + step_running_data["data"][pd["name"]]["msg"] = "项目【{}】停止成功".format(pd["name"]) + remove_idx.append(idx) + self.running_data["executing"] -= 1 + change = True + + if remove_idx: + for idx in remove_idx: + need_wait_project.pop(idx) + + if change is True: + self.save_running_data() + + wait_num += 1 + if need_wait_project: + time.sleep(0.1) + + step_running_data["running"] = False + self.save_running_data() + + return + + def termination_operation(self) -> None: + pid_file = "{}/{}.pid".format(Group.GROUP_TMP_DIR, self.group_id) + try: + pid = int(public.readFile(pid_file)) + p = psutil.Process(pid) + p.kill() + os.remove(pid_file) + except: + pass + + +class GroupMager: + cache_type = ["springboot"] + + def __init__(self): + self.project_cache: Optional[dict] = None + + if not os.path.exists(Group.GROUP_DATA_DIR): + os.makedirs(Group.GROUP_DATA_DIR, 0o600) + + if not os.path.exists(Group.GROUP_TMP_DIR): + os.makedirs(Group.GROUP_TMP_DIR) + + def _get_project_cache(self): + if self.project_cache is not None: + return + java_projects = public.M("sites").where('project_type=?', ('Java',)).select() + _cache = {} + for i in java_projects: + config = json.loads(i["project_config"]) + if config["java_type"] in self.cache_type: + i["project_config"] = config + _cache[i["id"]] = i + + self.project_cache = _cache + + def group_list(self) -> List[dict]: + group_list = [] + for i in os.listdir(Group.GROUP_DATA_DIR): + file = "{}/{}".format(Group.GROUP_DATA_DIR, i) + if os.path.isfile(file) and i.endswith(".json"): + group_id = i[:-5] + try: + data = json.loads(public.readFile(file)) + except: + continue + g = Group(group_id, data) + group_list.append(g) + + if not group_list: + return [] + + self._get_project_cache() + res = [] + for i in group_list: + res.append(i.group_info(self.project_cache)) + + return res + + @staticmethod + def add_group(data: dict) -> Optional[str]: + g = Group(Group.new_group_id(), data) + res = g.check_group_data() + if isinstance(res, str): + return res + g.save_group_data() + + @staticmethod + def remove_group(group_id: str) -> None: + config_file = "{}/{}.json".format(Group.GROUP_DATA_DIR, group_id) + if os.path.isfile(config_file): + os.remove(config_file) + + @staticmethod + def add_project_to_group(group_id: str, project_name: str, check_info: dict, level: Optional[int]) -> Optional[str]: + g = Group(group_id) + if not g.config: + return "项目组不存在" + p = public.M("sites").where('project_type=? AND name=?', ('Java', project_name)).find() + if not p: + return "指定项目【{}】不存在".format(project_name) + project_config = json.loads(p["project_config"]) + if not project_config["java_type"] in GroupMager.cache_type: + return "指定项目【{}】不是spring boot项目, 不支持添加到项目组".format(project_name) + + used_project = [p["id"] for p in g.config["projects"]] + if p["id"] in used_project: + return "项目【{}】已存在于项目组【{}】".format(project_name, group_id) + + if level is None: + if not g.config["projects"]: + level = 1 + else: + level = max([p["level"] for p in g.config["projects"]]) + 1 + + g.config["projects"].append({ + "name": project_name, + "id": p["id"], + "check_info": check_info, + "level": level, + }) + res = g.check_group_data() + if isinstance(res, str): + return res + g.save_group_data() + + @staticmethod + def add_projects_to_group(group_id: str, project_ids: List[int]) -> Optional[str]: + g = Group(group_id) + if not g.config: + return "项目组不存在" + project_list = public.M("sites").where( + 'project_type=? AND id IN ({})'.format(",".join(["?"] * len(project_ids))), + ('Java', *project_ids) + ).select() + + used_project = [p["id"] for p in g.config["projects"]] + start_level = max([p["level"] for p in g.config["projects"]] + [1]) # 最小值为1 + for p in project_list: + if p["id"] in used_project: + return "项目【{}】已存在于项目组【{}】".format(p["name"], group_id) + project_config = json.loads(p["project_config"]) + if not project_config["java_type"] in GroupMager.cache_type: + return "指定项目【{}】不是spring boot项目, 不支持添加到项目组".format(p["name"]) + + g.config["projects"].append({ + "name": p["name"], + "id": p["id"], + "check_info": { + "type": "port", + "port": [], + "wait_time": 180, + }, + "level": start_level + 1, + }) + start_level += 1 + + res = g.check_group_data() + if isinstance(res, str): + return res + g.save_group_data() + + @staticmethod + def remove_project_from_group(group_id: str, project_id: int) -> Optional[str]: + g = Group(group_id) + if not g.config: + return "项目组不存在" + target_idx = None + for idx, p in enumerate(g.config["projects"]): + if p["id"] == project_id: + target_idx = idx + break + if target_idx is not None: + g.config["projects"].pop(target_idx) + g.save_group_data() + + @staticmethod + def modify_group(group_id: str, group_data: dict) -> Optional[str]: + g = Group(group_id, group_data) + res = g.check_group_data() + if isinstance(res, str): + return res + g.save_group_data() + + @staticmethod + def modify_group_projects(group_id: str, project_datas: list) -> Optional[str]: + g = Group(group_id) + if not g.config: + return "项目组不存在" + + project_ids = [p["id"] for p in project_datas] + project_list = public.M("sites").where( + 'project_type=? AND id IN ({})'.format(",".join(["?"] * len(project_ids))), + ('Java', *project_ids) + ).select() + + for p in project_list: + project_config = json.loads(p["project_config"]) + if not project_config["java_type"] in GroupMager.cache_type: + return "指定项目【{}】不是spring boot项目, 不支持设置到项目组".format(p["name"]) + + g.config["projects"] = project_datas + res = g.check_group_data() + if isinstance(res, str): + return res + + g.save_group_data() + + @staticmethod + def modify_group_project(group_id: str, project_id: int, + check_info: Optional[dict], level: Optional[int]) -> Optional[str]: + g = Group(group_id) + if not g.config: + return "项目组不存在" + p = public.M("sites").where('project_type=? AND id=?', ('Java', project_id)).find() + if not p: + return "指定项目不存在" + + target_idx = None + for idx, p in enumerate(g.config["projects"]): + if p["id"] == project_id: + target_idx = idx + break + + if target_idx is None: + return "指定项目不在该项目组内" + update_data = {} + if check_info is not None: + update_data["check_info"] = check_info + if level is not None: + update_data["level"] = level + + g.config["projects"][target_idx].update(update_data) + + res = g.check_group_data() + if isinstance(res, str): + return res + + g.save_group_data() + + @staticmethod + def change_sort_type(group_id: str, sort_type: str) -> Optional[str]: + g = Group(group_id) + if not g.config: + return "项目组不存在" + + if sort_type not in ("simultaneous", "sequence"): + return "排序方式错误" + g.config["sort_type"] = sort_type + g.save_group_data() + + +Group.update_group_data() + + +class main: + + def __init__(self): + pass + + @staticmethod + def group_list(get): + return GroupMager().group_list() + + @staticmethod + def group_info(get): + try: + group_id = get.group_id.strip() + except: + return json_response(status=False, msg="参数错误") + g = Group(group_id) + if not g.config: + return json_response(status=False, msg="项目组不存在") + else: + return json_response(status=True, data=g.group_info()) + + @staticmethod + def add_group(get): + try: + group_name = get.group_name.strip() + except: + return json_response(status=False, msg="参数错误") + res = GroupMager().add_group({ + "group_name": group_name, + "sort_type": "simultaneous", + "projects": [], + }) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="添加成功") + + @staticmethod + def remove_group(get): + try: + group_id = get.group_id.strip() + except: + return json_response(status=False, msg="参数错误") + GroupMager.remove_group(group_id) + return json_response(status=True, msg="删除成功") + + @staticmethod + def add_project_to_group(get): + check_info = { + "type": "port", + "port": [], + "wait_time": 180, + } + level = None + try: + group_id = get.group_id.strip() + project_name = get.project_name.strip() + if hasattr(get, "check_info") and get.check_info: + check_info = json.loads(get.check_info) + if hasattr(get, "level") and get.level: + level = int(get.level) + except: + return json_response(status=False, msg="参数错误") + res = GroupMager.add_project_to_group(group_id, project_name, check_info, level) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="添加成功") + + @staticmethod + def add_projects_to_group(get): + + try: + group_id = get.group_id.strip() + if hasattr(get, "project_ids") and isinstance(get.project_ids, str): + project_ids = json.loads(get.project_ids) + else: + if isinstance(get.project_ids, list): + project_ids = get.project_ids + else: + return json_response(status=False, msg="参数错误") + except: + return json_response(status=False, msg="参数错误") + res = GroupMager.add_projects_to_group(group_id, project_ids) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="添加成功") + + @staticmethod + def remove_project_from_group(get): + try: + group_id = get.group_id.strip() + project_id = int(get.project_id) + except: + return json_response(status=False, msg="参数错误") + res = GroupMager.remove_project_from_group(group_id, project_id) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="删除成功") + + @staticmethod + def modify_group(get): + try: + group_id = get.group_id.strip() + if isinstance(get.group_data, str): + group_data = json.loads(get.group_data) + else: + group_data = get.group_data + except: + return json_response(status=False, msg="参数错误") + res = GroupMager.modify_group(group_id, group_data) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="修改成功") + + @staticmethod + def modify_group_project(get): + check_info = None + level = None + try: + group_id = get.group_id.strip() + project_id = int(get.project_id) + if hasattr(get, "check_info") and get.check_info: + check_info = json.loads(get.check_info) + if hasattr(get, "level") and get.level: + level = int(get.level) + except: + return json_response(status=False, msg="参数错误") + res = GroupMager.modify_group_project(group_id, project_id, check_info, level) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="修改成功") + + @staticmethod + def modify_group_projects(get): + try: + group_id = get.group_id.strip() + if isinstance(get.project_datas, str): + project_datas = json.loads(get.project_datas) + else: + project_datas = get.project_datas + except: + return json_response(status=False, msg="参数错误") + res = GroupMager.modify_group_projects(group_id, project_datas) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="修改成功") + + @staticmethod + def change_sort_type(get): + try: + group_id = get.group_id.strip() + sort_type = get.sort_type.strip() + except: + return json_response(status=False, msg="参数错误") + res = GroupMager.change_sort_type(group_id, sort_type) + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="修改成功") + + @staticmethod + def start_group(get): + try: + group_id = get.group_id.strip() + except: + return json_response(status=False, msg="参数错误") + + g = Group(group_id) + if not g.config: + return json_response(status=False, msg="项目组不存在") + + res = g.run_operation("start") + + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="启动进行中") + + @staticmethod + def stop_group(get): + try: + group_id = get.group_id.strip() + except: + return json_response(status=False, msg="参数错误") + + g = Group(group_id) + if not g.config: + return json_response(status=False, msg="项目组不存在") + + res = g.run_operation("stop") + + if isinstance(res, str): + return json_response(status=False, msg=res) + else: + return json_response(status=True, msg="停止进行中") + + @staticmethod + def get_run_status(get): + last_write_time = 0 + try: + group_id = get.group_id.strip() + if hasattr(get, "last_write_time") and get.last_write_time: + last_write_time = float(get.last_write_time) + except: + return json_response(status=False, msg="参数错误") + + g = Group(group_id) + if not g.config: + return json_response(status=False, msg="项目组不存在") + + run_status = g.run_status(last_write_time) + run_status["running_step"] = None + if isinstance(run_status, dict): + if isinstance(run_status["running_data"], dict) and "projects" in run_status["running_data"]: + projects = run_status["running_data"]["projects"] + for i in projects: + if i["running"] is True: + run_status["running_step"] = i + + return json_response(status=True, data=run_status) + + @staticmethod + def termination_operation(get): + try: + group_id = get.group_id.strip() + except: + return json_response(status=False, msg="参数错误") + Group(group_id).termination_operation() + return json_response(status=True, msg="操作成功") + + @staticmethod + def spring_projects(get): + java_projects = public.M("sites").where('project_type=?', ('Java',)).field("name,id,project_config").select() + res = [] + for i in java_projects: + config = json.loads(i["project_config"]) + if config["java_type"] in GroupMager.cache_type: + res.append({ + "name": i["name"], + "id": i["id"] + }) + + return res diff --git a/mod/project/java/group_script.py b/mod/project/java/group_script.py new file mode 100644 index 00000000..c3deeb01 --- /dev/null +++ b/mod/project/java/group_script.py @@ -0,0 +1,34 @@ +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +from mod.project.java.groupMod import Group + + +def start_group(g_id: str): + g = Group(g_id) + g.real_run_start() + + +def stop_group(g_id: str): + g = Group(g_id) + g.real_run_stop() + + +if __name__ == '__main__': + if len(sys.argv) >= 3: + action = sys.argv[1] + group_id = sys.argv[2] + else: + print("参数错误") + exit(1) + + if action == "start": + start_group(group_id) + else: + stop_group(group_id) + + diff --git a/mod/project/java/java_web_conf.py b/mod/project/java/java_web_conf.py new file mode 100644 index 00000000..4ebf8897 --- /dev/null +++ b/mod/project/java/java_web_conf.py @@ -0,0 +1,575 @@ +import os +import re +import shutil +import sys +from typing import List, Optional, Union, Tuple + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public +from mod.base.web_conf.util import listen_ipv6, get_log_path, GET_CLASS, service_reload +from mod.base.web_conf import NginxDomainTool, ApacheDomainTool + + +class JavaNginxTool: + def __init__(self): + self._panel_path = "/www/server/panel" + self._vhost_path = "{}/vhost".format(self._panel_path) + self._nginx_bak_path = "/var/tmp/springboot/nginx_conf_backup" + if not os.path.exists(self._nginx_bak_path): + os.makedirs(self._nginx_bak_path, 0o600) + + def set_nginx_config(self, project_data: dict, domains: List[Tuple[str, Union[str, int]]], + use_ssl: bool = False, force_ssl=False): + if use_ssl: + use_http2_on = public.is_change_nginx_http2() + use_http3 = public.is_nginx_http3() + else: + use_http2_on = False + use_http3 = False + + project_config = project_data["project_config"] + if project_config['java_type'] == "springboot": + project_path = project_data["project_config"]["jar_path"] + else: + project_path = project_data["path"] + if os.path.isfile(project_path): + project_path = os.path.dirname(project_path) + + port_set = set() + domain_set = set() + use_ipv6 = listen_ipv6() + listen_ports_list = [] + for d, p in domains: + if str(p) == "443": # 443 端口特殊处理 + continue + if str(p) not in port_set: + listen_ports_list.append(" listen {};".format(str(p))) + if use_ipv6: + listen_ports_list.append(" listen [::]:{};".format(str(p))) + + port_set.add(str(p)) + domain_set.add(d) + + if use_ssl: + if not use_http2_on: + http2 = " http2" + else: + http2 = "" + listen_ports_list.append(" http2 on;") + + listen_ports_list.append(" listen 443 ssl{};".format(http2)) + if use_ipv6: + listen_ports_list.append(" listen [::]:443 ssl{};".format(http2)) + + if use_http3: + listen_ports_list.append(" listen 443 quic;") + if use_ipv6: + listen_ports_list.append(" listen [::]:443 quic;") + + listen_ports = "\n".join(listen_ports_list).strip() + + static_conf = self._build_static_conf(project_config, project_path) + proxy_conf = self._build_proxy_conf(project_config) + ssl_conf = "#error_page 404/404.html;" + if use_ssl: + ssl_conf += "\n" + self._build_ssl_conf(project_config, use_http3=use_http3, force_ssl=force_ssl) + + nginx_template_file = "{}/template/nginx/java_mod_http.conf".format(self._vhost_path) + nginx_conf_file = "{}/nginx/java_{}.conf".format(self._vhost_path, project_data["name"]) + + nginx_template = public.ReadFile(nginx_template_file) + if not isinstance(nginx_template, str): + return "读取模版文件失败" + + nginx_conf = nginx_template.format( + listen_ports=listen_ports, + domains=" ".join(domain_set), + site_path=project_path, + site_name=project_data["name"], + panel_path=self._panel_path, + log_path=get_log_path(), + ssl_conf=ssl_conf, + static_conf=static_conf, + proxy_conf=proxy_conf, + ) + rewrite_file = "{}/rewrite/java_{}.conf".format(self._vhost_path, project_data["name"]) + if not os.path.exists(rewrite_file): + public.writeFile(rewrite_file, '# 请将伪静态规则或自定义NGINX配置填写到此处\n') + apply_check = "{}/nginx/well-known/{}.conf".format(self._vhost_path, project_data["name"]) + if not os.path.exists(os.path.dirname(apply_check)): + os.makedirs(os.path.dirname(apply_check), 0o600) + if not os.path.exists(apply_check): + public.writeFile(apply_check, '') + + public.writeFile(nginx_conf_file, nginx_conf) + return None + + @staticmethod + def _build_proxy_conf(project_config: dict) -> str: + if "proxy_info" not in project_config: + return "" + + proxy_info = project_config["proxy_info"] + proxy_conf_list = [] + if not proxy_info: + return "" + ng_proxy = ''' #PROXY-START{proxy_dir} + location {proxy_dir} {{{rewrite} + proxy_pass {proxy_url}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;{add_headers} + proxy_set_header REMOTE-HOST $remote_addr; + add_header X-Cache $upstream_cache_status; + proxy_set_header X-Host $host:$server_port; + proxy_set_header X-Scheme $scheme; + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + proxy_send_timeout 30s; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + }} + #PROXY-END{proxy_dir}''' + for i in proxy_info: + if i.get("status", False): + continue + rewrite = "" + if "rewrite" in i and i["rewrite"].get("status", False): + rewrite = i["rewrite"] + src_path = i["src_path"] + if not src_path.endswith("/"): + src_path += "/" + target_path = rewrite["target_path"] + if target_path.endswith("/"): + target_path += target_path[:-1] + + rewrite = "\n rewrite ^{}(.*)$ {}/$1 break;".format(src_path, target_path) + + add_headers = "" + if "add_headers" in i: + header_tmp = " add_header {} {};" + add_headers_list = [header_tmp.format(h["k"], h["v"]) for h in i["add_headers"] if + "k" in h and "v" in h] + add_headers = "\n".join(add_headers_list) + if add_headers: + add_headers = "\n" + add_headers + + proxy_conf_list.append(ng_proxy.format( + proxy_dir=i["proxy_dir"], + rewrite=rewrite, + add_headers=add_headers, + proxy_url="http://127.0.0.1:{}".format(i["proxy_port"]), + )) + + return ("\n".join(proxy_conf_list) + "\n").lstrip() + + @staticmethod + def _build_static_conf(project_config: dict, default_path: str) -> str: + if project_config['java_type'] == "springboot" and "static_info" in project_config: + static_info = project_config["static_info"] + if not static_info.get("status", False): + return "" + index_str = "index.html" + index = static_info.get("index", "") + if index: + if isinstance(index, list): + index_str = " ".join(index) + elif isinstance(index, str): + index_str = " ".join([i.strip() for i in index.split(",") if i.strip()]) + + path = static_info.get("path") + if not path: + path = default_path + try_file = '' + if static_info.get("use_try_file", True): + try_file = " try_files $uri $uri/ /index.html;\n" + static_conf = ( + "location / {\n" + " root %s;\n" + " index %s;\n%s" + " }" + ) % (path, index_str, try_file) + + return static_conf + return "" + + def _build_ssl_conf(self, project_config: dict, use_http3=False, force_ssl=False) -> str: + force_ssl_str = "" + if force_ssl: + force_ssl_str = ''' + #HTTP_TO_HTTPS_START + if ($server_port !~ 443){ + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END''' + http3_header = "" + if use_http3: + http3_header = '''\n add_header Alt-Svc 'quic=":443"; h3=":443"; h3-27=":443";h3-29=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"';''' + + return ''' ssl_certificate {vhost_path}/cert/{project_name}/fullchain.pem; + ssl_certificate_key {vhost_path}/cert/{project_name}/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000";{http3_header} + error_page 497 https://$host$request_uri;{force_ssl}'''.format( + vhost_path=self._vhost_path, + project_name=project_config["project_name"], + http3_header=http3_header, + force_ssl=force_ssl_str, + ) + + def open_nginx_config_file(self, project_data: dict, domains: List[Tuple[str, str]], ) -> Optional[str]: + project_name = project_data["name"] + back_path = "{}/{}".format(self._nginx_bak_path, project_name) + target_file = "{}/nginx/java_{}.conf".format(self._vhost_path, project_name) + if os.path.isfile(target_file): + return + + if os.path.isfile(back_path): + shutil.copyfile(back_path, target_file) + if os.path.isfile(target_file): + NginxDomainTool("java_").nginx_set_domain(project_name, *domains) + error_msg = public.checkWebConfig() + if not isinstance(error_msg, str): # 没有报错时直接退出 + service_reload() + return + + res = self.set_nginx_config(project_data, domains, use_ssl=False) + if not res: + service_reload() + return res + + def close_nginx_config_file(self, project_data: dict) -> None: + project_name = project_data["name"] + back_path = "{}/{}".format(self._nginx_bak_path, project_name) + target_file = "{}/nginx/java_{}.conf".format(self._vhost_path, project_name) + if not os.path.isfile(target_file): + return + + if os.path.isfile(back_path): + os.remove(back_path) + + shutil.move(target_file, back_path) + service_reload() + + def exists_nginx_ssl(self, project_name): + """ + 判断项目是否配置Nginx SSL配置 + """ + config_file = "{}/nginx/java_{}.conf".format(self._vhost_path, project_name) + if not os.path.exists(config_file): + return False, False + + config_body = public.readFile(config_file) + if isinstance(config_body, str): + return False, False + + is_ssl, is_force_ssl = False, False + if config_body.find('ssl_certificate') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl, is_force_ssl + + def set_static_path(self, project_data: dict) -> Optional[Union[bool, str]]: + project_path = project_data["project_config"]["jar_path"] + static_str = self._build_static_conf(project_data["project_config"], project_path) + ng_file = "{}/nginx/java_{}.conf".format(self._vhost_path, project_data["name"]) + ng_conf = public.readFile(ng_file) + if not isinstance(ng_conf, str): + return "配置文件读取错误" + + static_conf = "#STATIC-START 静态资源相关配置\n {}\n #STATIC-END".format(static_str) + rep_static = re.compile(r"#STATIC-START(.*\n){2,9}\s*#STATIC-END.*") + res = rep_static.search(ng_conf) + if res: + new_ng_conf = ng_conf.replace(res.group(), static_conf) + public.writeFile(ng_file, new_ng_conf) + error_msg = public.checkWebConfig() + if not isinstance(error_msg, str): # 没有报错时直接退出 + service_reload() + return None + else: + public.writeFile(ng_file, ng_conf) + return 'WEB服务器配置配置文件错误ERROR:
                                    ' + \ + error_msg.replace("\n", '
                                    ') + '
                                    ' + + # 添加配置信息到配置文件中 + rep_list = [ + (re.compile(r"\s*#PROXY-LOCAl-START.*", re.M), True), # 添加到反向代理结尾的上面 + (re.compile(r"\s*#REWRITE-END.*", re.M), False), # 添加到伪静态的下面 + (re.compile(r"\s*#SSL-END.*", re.M), False), # 添加到SSL END的下面 + ] + + # 使用正则匹配确定插入位置 + def set_by_rep_idx(tmp_rep: re.Pattern, use_start: bool) -> bool: + tmp_res = tmp_rep.search(ng_conf) + if not tmp_res: + return False + if use_start: + new_conf = ng_conf[:tmp_res.start()] + static_conf + tmp_res.group() + ng_conf[tmp_res.end():] + else: + new_conf = ng_conf[:tmp_res.start()] + tmp_res.group() + static_conf + ng_conf[tmp_res.end():] + + public.writeFile(ng_file, new_conf) + if public.get_webserver() == "nginx" and isinstance(public.checkWebConfig(), str): + public.writeFile(ng_file, ng_conf) + return False + return True + + for r, s in rep_list: + if set_by_rep_idx(r, s): + service_reload() + return None + else: + return False + + +class JavaApacheTool: + def __init__(self): + self._panel_path = "/www/server/panel" + self._vhost_path = "{}/vhost".format(self._panel_path) + self._apache_bak_path = "/var/tmp/springboot/httpd_conf_backup" + if not os.path.exists(self._apache_bak_path): + os.makedirs(self._apache_bak_path, 0o600) + + def set_apache_config_for_ssl(self, project_data): + domains = public.M('domain').where('pid=?', (project_data["id"],)).select() + domain_list = [(i["name"], i["port"]) for i in domains] + return self.set_apache_config(project_data, domain_list, use_ssl=True) + + def set_apache_config(self, project_data: dict, domains: List[Tuple[str, Union[str, int]]], + use_ssl: bool = False, force_ssl: bool = False): + name = project_data['name'] + port_set = set() + domain_set = set() + for d, p in domains: + port_set.add(str(p)) + domain_set.add(d) + + domains_str = ' '.join(domain_set) + project_config = project_data["project_config"] + if project_config['java_type'] == "springboot": + project_path = project_data["project_config"]["jar_path"] + else: + project_path = project_data["path"] + if os.path.isfile(project_path): + project_path = os.path.dirname(project_path) + + apache_template_file = "{}/template/apache/java_mod_http.conf".format(self._vhost_path) + apache_conf_file = "{}/apache/java_{}.conf".format(self._vhost_path, name) + + apache_template = public.ReadFile(apache_template_file) + if not isinstance(apache_template, str): + return "读取模版文件失败" + + apache_conf_list = [] + proxy_conf = self._build_proxy_conf(project_config) + for p in port_set: + apache_conf_list.append(apache_template.format( + site_path=project_path, + server_name='{}.{}'.format(p, project_path), + domains=domains_str, + log_path=get_log_path(), + server_admin='admin@{}'.format(name), + port=p, + ssl_config='', + project_name=name, + proxy_conf=proxy_conf, + )) + + if use_ssl: + ssl_config = '''SSLEngine On + SSLCertificateFile {vhost_path}/cert/{project_name}/fullchain.pem + SSLCertificateKeyFile {vhost_path}/cert/{project_name}/privkey.pem + SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 + SSLProtocol All -SSLv2 -SSLv3 -TLSv1 + SSLHonorCipherOrder On'''.format(project_name=name, vhost_path=public.get_vhost_path()) + if force_ssl: + ssl_config += ''' + #HTTP_TO_HTTPS_START + + RewriteEngine on + RewriteCond %{SERVER_PORT} !^443$ + RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] + + #HTTP_TO_HTTPS_END''' + + apache_conf_list.append(apache_template.format( + site_path=project_path, + server_name='{}.{}'.format("443", project_path), + domains=domains_str, + log_path=get_log_path(), + server_admin='admin@{}'.format(name), + port="443", + ssl_config=ssl_config, + project_name=name, + proxy_conf=proxy_conf, + )) + + apache_conf = '\n'.join(apache_conf_list) + public.writeFile(apache_conf_file, apache_conf) + ApacheDomainTool.apache_add_ports(*port_set) + return None + + @staticmethod + def _build_proxy_conf(project_config: dict) -> str: + if "proxy_info" not in project_config: + return "" + + proxy_info = project_config["proxy_info"] + proxy_conf_list = [] + if not proxy_info: + return "" + ap_proxy = ''' #PROXY-START{proxy_dir} + + ProxyRequests Off + SSLProxyEngine on + ProxyPass {proxy_dir} {proxy_url}/ + ProxyPassReverse {proxy_dir} {proxy_url}/ + RequestHeader set Host "%{Host}e" + RequestHeader set X-Real-IP "%{REMOTE_ADDR}e" + RequestHeader set X-Forwarded-For "%{X-Forwarded-For}e" + RequestHeader setifempty X-Forwarded-For "%{REMOTE_ADDR}e" + + #PROXY-END{proxy_dir}''' + + for i in proxy_info: + if i.get("status", False): + continue + + proxy_conf_list.append(ap_proxy.format( + proxy_dir=i["proxy_dir"], + proxy_url="http://127.0.0.1:{}".format(i["proxy_port"]), + )) + + return ("\n".join(proxy_conf_list) + "\n").lstrip() + + def open_apache_config_file(self, project_data: dict, domains: List[Tuple[str, str]]) -> Optional[str]: + project_name = project_data["name"] + back_path = "{}/{}".format(self._apache_bak_path, project_name) + target_file = "{}/apache/java_{}.conf".format(self._vhost_path, project_name) + if os.path.isfile(target_file): + return + + if os.path.isfile(back_path): + shutil.copyfile(back_path, target_file) + if os.path.isfile(target_file): + ApacheDomainTool("java_").apache_set_domain(project_name, *domains) + error_msg = public.checkWebConfig() + if not isinstance(error_msg, str): # 没有报错时直接退出 + service_reload() + return + + res = self.set_apache_config( + project_data, + domains=domains, + use_ssl=False, + ) + + if not res: + service_reload() + return res + + def close_apache_config_file(self, project_data: dict) -> None: + project_name = project_data["name"] + back_path = "{}/{}".format(self._apache_bak_path, project_name) + target_file = "{}/apache/java_{}.conf".format(self._vhost_path, project_name) + if not os.path.isfile(target_file): + return + + if os.path.isfile(back_path): + os.remove(back_path) + + shutil.move(target_file, back_path) + service_reload() + + def exists_apache_ssl(self, project_name) -> Tuple[bool, bool]: + """ + 判断项目是否配置Apache SSL配置 + """ + config_file = "{}/apache/java_{}.conf".format(self._vhost_path, project_name) + if not os.path.exists(config_file): + return False, False + + config_body = public.readFile(config_file) + if not isinstance(config_body, str): + return False, False + + is_ssl, is_force_ssl = False, False + if config_body.find('SSLCertificateFile') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl, is_force_ssl + + +class JvavWebConfig: + + def __init__(self): + self._ng_conf_onj = JavaNginxTool() + self._ap_conf_onj = JavaApacheTool() + self.ws_type = public.get_webserver() + + def create_config(self, project_data: dict, domains: List[Tuple[str, Union[str, int]]], + use_ssl: bool = False, force_ssl=False): + ng_res = self._ng_conf_onj.set_nginx_config(project_data, domains, use_ssl, force_ssl=force_ssl) + ap_res = self._ap_conf_onj.set_apache_config(project_data, domains, use_ssl, force_ssl=force_ssl) + if self.ws_type == "nginx" and ng_res: + return ng_res + elif self.ws_type == "apache" and ap_res: + return ap_res + service_reload() + + def _open_config_file(self, project_data: dict): + domain_list = public.M('domain').where('pid=?', (project_data["id"],)).field("name,port").select() + domains = [(i["name"], str(i["port"])) for i in domain_list] + if not domains: + return "域名不能为空" + ng_res = self._ng_conf_onj.open_nginx_config_file(project_data, domains) + ap_res = self._ap_conf_onj.open_apache_config_file(project_data, domains) + if self.ws_type == "nginx" and ng_res: + return ng_res + elif self.ws_type == "apache" and ap_res: + return ap_res + + def _close_apache_config_file(self, project_data: dict) -> None: + self._ap_conf_onj.close_apache_config_file(project_data) + self._ng_conf_onj.close_nginx_config_file(project_data) + + def _set_domain(self, project_data: dict, domains: List[Tuple[str, str]]) -> Optional[str]: + ng_res = NginxDomainTool("java_").nginx_set_domain(project_data["name"], *domains) + ap_res = ApacheDomainTool("java_").apache_set_domain(project_data["name"], *domains) + if self.ws_type == "nginx" and ng_res: + return ng_res + elif self.ws_type == "apache" and ap_res: + return ap_res + + def _get_ssl_status(self, project_name) -> Tuple[bool, bool]: + if self.ws_type == "nginx": + return self._ng_conf_onj.exists_nginx_ssl(project_name) + elif self.ws_type == "apache": + return self._ap_conf_onj.exists_apache_ssl(project_name) + return False, False + + def _set_static_path(self, project_data: dict): + if self.ws_type == "nginx": + res = self._ng_conf_onj.set_static_path(project_data) + if res is None: + return None + elif res is False: + err_msg = public.checkWebConfig() + if isinstance(err_msg, str): + return 'WEB服务器配置配置文件错误ERROR:
                                    ' + \ + err_msg.replace("\n", '
                                    ') + '
                                    ' + + return self._open_config_file(project_data) + else: + return res + return "只支持nginx设置静态路由" diff --git a/mod/project/java/jmxquery/JMXQuery-0.1.8.jar b/mod/project/java/jmxquery/JMXQuery-0.1.8.jar new file mode 100644 index 00000000..af2bf0c8 Binary files /dev/null and b/mod/project/java/jmxquery/JMXQuery-0.1.8.jar differ diff --git a/mod/project/java/jmxquery/__init__.py b/mod/project/java/jmxquery/__init__.py new file mode 100644 index 00000000..31f05ee6 --- /dev/null +++ b/mod/project/java/jmxquery/__init__.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 + +""" + Python interface to JMX. Uses local jar to pass commands to JMX and read JSON + results returned. +""" + +import subprocess +import os +import json +from typing import List +from enum import Enum +import logging + +# Full Path to Jar +JAR_PATH = os.path.dirname(os.path.realpath(__file__)) + '/JMXQuery-0.1.8.jar' +# Default Java path +DEFAULT_JAVA_PATH = 'java' +# Default timeout for running jar in seconds +DEFAULT_JAR_TIMEOUT = 10 + +logger = logging.getLogger(__name__) + + +class MetricType(Enum): + COUNTER = 'counter' + GAUGE = 'gauge' + + +class JMXQuery: + """ + A JMX Query which is used to fetch specific MBean attributes/values from the JVM. The object_name can support wildcards + to pull multiple metrics at once, for example '*:*' will bring back all MBeans and attributes in the JVM with their values. + + You can set a metric name if you want to override the generated metric name created from the MBean path + """ + + def __init__(self, + mBeanName: str, + attribute: str = None, + attributeKey: str = None, + value: object = None, + value_type: str = None, + metric_name: str = None, + metric_labels: dict = None): + + self.mBeanName = mBeanName + self.attribute = attribute + self.attributeKey = attributeKey + self.value = value + self.value_type = value_type + self.metric_name = metric_name + self.metric_labels = metric_labels + + def to_query_string(self) -> str: + """ + Build a query string to pass via command line to JMXQuery Jar + + :return: The query string to find the MBean in format: + + {mBeanName}/{attribute}/{attributeKey} + + Example: java.lang:type=Memory/HeapMemoryUsage/init + """ + query = "" + if self.metric_name: + query += self.metric_name + + if ((self.metric_labels != None) and (len(self.metric_labels) > 0)): + query += "<" + keyCount = 0 + for key, value in self.metric_labels.items(): + query += key + "=" + value + keyCount += 1 + if keyCount < len(self.metric_labels): + query += "," + query += ">" + query += "==" + + query += self.mBeanName + if self.attribute: + query += "/" + self.attribute + if self.attributeKey: + query += "/" + self.attributeKey + + return query + + def to_string(self): + + string = "" + if self.metric_name: + string += self.metric_name + + if ((self.metric_labels != None) and (len(self.metric_labels) > 0)): + string += " {" + keyCount = 0 + for key, value in self.metric_labels.items(): + string += key + "=" + value + keyCount += 1 + if keyCount < len(self.metric_labels): + string += "," + string += "}" + else: + string += self.mBeanName + if self.attribute: + string += "/" + self.attribute + if self.attributeKey: + string += "/" + self.attributeKey + + string += " = " + string += str(self.value) + " (" + self.value_type + ")" + + return string + + +class JMXConnection(object): + """ + The main class that connects to the JMX endpoint via a local JAR to run queries + """ + + def __init__(self, connection_uri: str, jmx_username: str = None, jmx_password: str = None, java_path: str = DEFAULT_JAVA_PATH): + """ + Creates instance of JMXQuery set to a specific connection uri for the JMX endpoint + + :param connection_uri: The JMX connection URL. E.g. service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi + :param jmx_username: (Optional) Username if JMX endpoint is secured + :param jmx_password: (Optional) Password if JMX endpoint is secured + :param java_path: (Optional) Provide an alternative Java path on the machine to run the JAR. + Default is 'java' which will use the machines default JVM + """ + self.connection_uri = connection_uri + self.jmx_username = jmx_username + self.jmx_password = jmx_password + self.java_path = java_path + + def __run_jar(self, queries: List[JMXQuery], timeout) -> List[JMXQuery]: + """ + Run the JAR and return the results + + :param query: The query + :return: The full command array to run via subprocess + """ + + command = [self.java_path, '-jar', JAR_PATH, '-url', self.connection_uri, "-json"] + if (self.jmx_username): + command.extend(["-u", self.jmx_username, "-p", self.jmx_password]) + + queryString = "" + for query in queries: + queryString += query.to_query_string() + ";" + + command.extend(["-q", queryString]) + logger.debug("Running command: " + str(command)) + + jsonOutput = "[]" + try: + output = subprocess.run(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=True) + + jsonOutput = output.stdout.decode('utf-8') + except subprocess.TimeoutExpired as err: + logger.error("Error calling JMX, Timeout of " + str(err.timeout) + " Expired: " + err.output.decode('utf-8')) + except subprocess.CalledProcessError as err: + logger.error("Error calling JMX: " + err.output.decode('utf-8')) + raise err + + logger.debug("JSON Output Received: " + jsonOutput) + metrics = self.__load_from_json(jsonOutput) + return metrics + + def __load_from_json(self, jsonOutput: str) -> List[JMXQuery]: + """ + Loads the list of returned metrics from JSON response + + :param jsonOutput: The JSON Array returned from the command line + :return: An array of JMXQuerys + """ + if "\n" in jsonOutput: + jsonOutput = jsonOutput.replace("\n", "") + if "\t" in jsonOutput: + jsonOutput = jsonOutput.replace("\t", "") + jsonMetrics = json.loads(jsonOutput) + metrics = [] + for jsonMetric in jsonMetrics: + mBeanName = jsonMetric['mBeanName'] + attribute = jsonMetric['attribute'] + attributeType = jsonMetric['attributeType'] + metric_name = None + if 'metricName' in jsonMetric: + metric_name = jsonMetric['metricName'] + metric_labels = None + if 'metricLabels' in jsonMetric: + metric_labels = jsonMetric['metricLabels'] + attributeKey = None + if 'attributeKey' in jsonMetric: + attributeKey = jsonMetric['attributeKey'] + value = None + if 'value' in jsonMetric: + value = jsonMetric['value'] + + metrics.append( + JMXQuery(mBeanName, attribute, attributeKey, value, attributeType, metric_name, metric_labels)) + return metrics + + def query(self, queries: List[JMXQuery], timeout=DEFAULT_JAR_TIMEOUT) -> List[JMXQuery]: + """ + Run a list of JMX Queries against the JVM and get the results + + :param queries: A list of JMXQuerys to query the JVM for + :return: A list of JMXQuerys found in the JVM with their current values + """ + return self.__run_jar(queries, timeout) diff --git a/mod/project/java/projectMod.py b/mod/project/java/projectMod.py new file mode 100644 index 00000000..3dfdf394 --- /dev/null +++ b/mod/project/java/projectMod.py @@ -0,0 +1,3252 @@ +import itertools +import json +import re +import shutil +import sys +import os +import time +import traceback +import threading +from datetime import datetime + +import psutil +from typing import Dict, List, Optional, Union, Any + +from mod.base import json_response +from mod.base.backup_tool import VersionTool +from mod.project.java import utils +from mod.base.process import RealUser, RealProcess + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public + +from projectModel.watchModel import use_project_watch, add_project_watch, del_project_watch +from mod.base.web_conf import normalize_domain, is_domain, Redirect, remove_sites_service_config, RealSSLManger +from mod.base.web_conf import RealRedirect, RealLogMgr, Proxy, RealProxy +from mod.base.process.server import RealServer +from mod.project.java.java_web_conf import JvavWebConfig +from mod.project.java.server_proxy import RealServerProxy +from mod.base.git_tool import GitMager +from mod.project.java.springboot_parser import SpringConfigParser, SpringLogConfigParser + +_DEBUG = True + + +def debug(func): + if not _DEBUG: + return func + + def inner(*args, **kwargs): + try: + return func(*args, **kwargs) + except: + err = traceback.format_exc() + print(err) + public.print_log(err) + return {"msg": "报错了"} + + return inner + + +def set_java_service_link(): + java_service_bin = '/usr/bin/java-service' + java_service_src = '/www/server/panel/script/java-service.py' + if os.path.exists(java_service_src): + public.ExecShell("chmod 700 " + java_service_src) + if not os.path.exists(java_service_bin): + if os.path.exists(java_service_src): + public.ExecShell("ln -sf {} {}".format(java_service_src, java_service_bin)) + + +set_java_service_link() + + +class main(JvavWebConfig, Proxy, Redirect, GitMager): + + def __init__(self): + super().__init__() + self._bt_tomcat_path = "/usr/local/bttomcat" + self._mod_tomcat_path = "/usr/local/bt_mod_tomcat" + self._bt_jdk_path = "/usr/local/btjdk/" + self._java_path = "/www/server/java/" + self._vhost_path = "/www/server/panel/vhost" + self._java_project_path = "/var/tmp/springboot/" + self._java_project_vhost = "/var/tmp/springboot/vhost" + self._java_spring_boot_log_path = "/www/wwwlogs/java/springboot" + self._site_tomcat_path = '/www/server/bt_tomcat_web/' + if not os.path.exists(self._java_project_vhost): + os.makedirs(self._java_project_vhost + "/pids", 0o777) + os.makedirs(self._java_project_vhost + "/scripts", 0o755) + if not os.path.exists(self._java_project_vhost + "/env"): + os.makedirs(self._java_project_vhost + "/env", 0o755) + if not os.path.exists(self._java_spring_boot_log_path): + os.makedirs(self._java_spring_boot_log_path, 0o755) + if not os.path.exists(self._site_tomcat_path): + os.makedirs(self._site_tomcat_path, 0o755) + + if not os.path.exists(self._mod_tomcat_path): + os.makedirs(self._mod_tomcat_path, 0o755) + + self._default_java_log = "/www/wwwlogs/java" + if not os.path.exists(self._default_java_log): + os.makedirs(self._default_java_log, 0o701) + + # 实现Proxy初始化 + Proxy.__init__(self, config_prefix="java_") + # 实现Redirect初始化 + Redirect.__init__(self, config_prefix="java_") + + self._real_process: Optional[RealProcess] = None + + @property + def real_process(self) -> RealProcess: + if self._real_process is None: + self._real_process = RealProcess() + return self._real_process + + @staticmethod + def is_stop_by_user(project_id): + return utils.is_stop_by_user(project_id) + + @staticmethod + def write_project_log(log_str: str): + public.WriteLog("项目管理", log_str) + + def get_system_info(self, get=None): + return json_response(status=True, data={ + "jdk_info": self._local_jdk_info(), + "tomcat_status": self._bt_tomcat_info(), + }) + + @staticmethod + def _local_jdk_info() -> List[Dict]: + ret = [] + jdk_tool = utils.JDKManager() + current_java_home = jdk_tool.get_env_jdk() + # 获取已安装的JDK版本 + for version in jdk_tool.versions_list: + jdk_path = '/www/server/java/' + version + '/bin/java' + is_current = os.path.dirname(os.path.dirname(jdk_path)) == current_java_home + if os.path.exists('/www/server/java/' + version): + ret.append({'name': version, 'path': jdk_path, 'operation': 1, 'is_current': is_current}) + else: + ret.append({'name': version, 'path': '', 'operation': 0, 'is_current': False}) + name = '安装[{}]'.format(version) + install_data = public.M('tasks').where("status in (0, -1) and name=?", (name,)).find() + if install_data: + ret[-1]['operation'] = 3 + + ret.sort(key=lambda x: (x['operation'] == 1, x['operation'] == 3), reverse=True) + + # 检查其他JDK路径 + jdk_paths = [ + ('JDK', '/usr/bin/java'), + ('jdk8', '/usr/java/jdk1.8.0_121/bin/java'), + ('openjdk8', '/usr/local/btjdk/jdk8/bin/java'), + ('jdk7', '/usr/java/jdk1.7.0_80/bin/java') + ] + + for i in jdk_tool.custom_jdk_list: + is_current = utils.normalize_jdk_path(i) == current_java_home + ret.append({'name': '自定义JDK', 'path': i, 'operation': 1, 'is_current': is_current}) + + for name, path in jdk_paths: + if os.path.exists(path): + is_current = os.path.dirname(os.path.dirname(path)) == current_java_home + ret.append({'name': name, 'path': path, 'operation': 2, 'is_current': is_current}) + + return ret + + @staticmethod + def _bt_tomcat_info() -> List[Dict]: + versions = [7, 8, 9, 10] + data = [] + for i in versions: + soft_name = "安装[Java项目Tomcat-{}]".format(i) + install_data = public.M('tasks').where("status in (0, -1) and name=?", (soft_name,)).find() + tmp = utils.bt_tomcat(i).status() + tmp["version"] = i + if install_data: + tmp["is_install"] = True + else: + tmp["is_install"] = False + data.append(tmp) + return data + + @staticmethod + def process_for_create(get): + is_java_process = True + search = '' + + if hasattr(get, "is_java_process"): + is_java_process = get.is_java_process.strip() + if is_java_process in ("0", 0, False, "false"): + is_java_process = False + else: + is_java_process = True + if hasattr(get, "search"): + search = get.search.strip() + + res = [] + rep_bt_tomcat = re.compile(r"(/usr/local/bttomcat/.*)|(/www/server/bt_tomcat_web/.*)/jsvc") + rep_not_use = re.compile( + r"php|system|crond|NetworkManager|uwsgi|dotnet|bash|/www/server/tamper|mysql|nginx|httpd|tail|python|sshd" + ) + if is_java_process: + target_list = utils.jps() + for pid in target_list: + try: + p = psutil.Process(pid) + if search and not (search in p.name() or search in ' '.join(p.cmdline())): # 有搜索条件,但不符合搜索条件 + continue + if rep_bt_tomcat.search(p.exe()): + continue + res.append({ + "pid": pid, + "name": p.name(), + "exe": p.exe(), + "cmdline": p.cmdline(), + "username": p.username(), + "create_time:": p.create_time(), + "status": p.status(), + }) + except: + continue + + return res + + # 若未选择java进程, 则在所有进程中搜索 + for i in psutil.process_iter(["pid", "name", "exe", "cmdline", "username", "create_time", "status"]): + if not i.exe(): + continue + if rep_not_use.search(i.exe()): + continue + if search and not (search in i.name() or search in ' '.join(i.cmdline())): # 有搜索条件, 但不符合搜索条件 + continue + if rep_bt_tomcat.search(i.exe()): + continue + try: + res.append({ + "pid": i.pid, + "name": i.name(), + "exe": i.exe(), + "cmdline": i.cmdline(), + "username": i.username(), + "create_time:": i.create_time(), + "status": i.status(), + }) + except: + continue + + return res + + @staticmethod + def process_info_for_create(get): + try: + pid = int(get.pid.strip()) + except: + return json_response(status=False, msg="参数错误") + + try: + p = psutil.Process(pid) + + cmdline = p.cmdline() + ports = [] + for i in p.connections(): + if i.status == "LISTEN" and i.laddr and i.laddr.port not in ports: + ports.append(i.laddr.port) + user = p.username() + env = p.environ() + exe = p.exe() + except: + public.print_log(public.get_error_info()) + return json_response(status=False, msg="进程解析失败") + + pwd_path = env.get("PWD") + env_list = [] + for key, value in env.items(): + if key.lower().find("spring") != -1 or key.lower().find("java") != -1: + env_list.append({"k": key, "v": value}) + + java_bin = "" + if exe.endswith("java"): # 目前支持用户使用java命令启动的java进程 + java_bin = exe + if cmdline[0].endswith("java"): + cmdline[0] = java_bin + + jar_path = "" + target_jar_idx = None + for idx, i in enumerate(cmdline): + if i.endswith(".jar") or i.endswith(".war") and not (i.startswith("-") or "=" in i): + if not jar_path: + jar_path = i + target_jar_idx = idx + + # 处理获取的java路径 + project_name = "" + if jar_path: + if not jar_path.startswith("/"): # 处理相对路径 + jar_path = os.path.abspath(os.path.join(pwd_path, jar_path)) + cmdline[target_jar_idx] = jar_path + project_name = os.path.basename(jar_path).rsplit(".", 1)[0] + + if not os.path.exists(jar_path): + jar_path = None + + return json_response(status=True, data={ + "project_name": project_name, + "jar_path": jar_path, + "java_bin": java_bin, + "port": ports[0] if len(ports) else 0, + "user": user, + "env": env_list, + "cmdline": " ".join(cmdline), + "pid": pid, + }) + + @staticmethod + def check_spring_boot_args(get) -> Union[Dict, str]: + by_process = 0 + domains = [] + proxy_path = "/" + jmx_status = False + watch_file = False + env_file = "" + env_list = [] + try: + project_name = get.project_name.strip() + project_jar = get.project_jar.strip() + project_jdk = get.project_jdk.strip() + # port = int(get.port) + run_user = get.run_user.strip() + project_cmd = get.project_cmd.strip() # type:str + ps = get.project_ps.strip() + if hasattr(get, "env_file"): + env_file = get.env_file.strip() + if not isinstance(env_file, str) and os.path.exists(env_file): + return "环境变量文件不存在" + if hasattr(get, "env_list"): + env_list = get.env_list + if not isinstance(env_list, list): + return "环境变量格式错误" + if hasattr(get, "domains"): + domains = get.domains + if not isinstance(domains, list): + return "域名参数错误" + if hasattr(get, "proxy_path"): + proxy_path = get.proxy_path + if not isinstance(domains, str) and not proxy_path.startswith("/"): + return "代理路径必须是以/开头的字符串" + if hasattr(get, "jmx_status"): + jmx_status = utils.js_value_to_bool(get.jmx_status) + if hasattr(get, "watch_file"): + watch_file = utils.js_value_to_bool(get.watch_file) + if hasattr(get, "by_process"): + by_process = int(get.by_process) + if by_process < 0: + by_process = 0 + except (AttributeError, ValueError): + return "参数格式错误" + + if not project_name: + return "项目名称不能为空" + if not 1 <= len(project_name) <= 20: + return "项目名称不超过20字符" + if public.M('sites').where('name=?', (project_name,)).count(): + return '指定项目名称已存在: {}'.format(project_name) + + if not os.path.exists(project_jar): + return '请输入正确的jar包路径' + + project_jdk = utils.normalize_jdk_path(project_jdk) + if not isinstance(project_jdk, str): + return json_response(False, '项目JDK路径不存在') + if not os.path.exists(project_jdk): + return '请输入正确的JDK路径' + if not utils.test_jdk(project_jdk): + return '请输入正确的JDK路径' + + if project_cmd.find(project_jdk) == -1: + return '启动命令中不包含JDK路径,请确认无误再添加' + + if project_cmd.find(project_jar) == -1: + return '启动命令中不包含jar路径,请确认无误后再添加' + + # if not by_process and not utils.check_port(port): + # return '端口格式错误或已被其他进程使用' + + if domains: + if not public.is_apache_nginx(): + return "未安装Nginx" + err_msg = public.checkWebConfig() + if isinstance(err_msg, str): + return ( + 'WEB服务器配置配置文件错误ERROR:
                                    ' + + err_msg.replace("\n", '
                                    ') + '
                                    ' + ) + + if run_user not in [i["username"] for i in RealUser().get_user_list()["data"]]: + return '请输入正确的启动用户' + + if domains: + domains, err = normalize_domain(*domains) + if err: + return "
                                    ".join(["域名:{},错误信息:{}".format(i['domain'], i['msg']) for i in err]) + else: + for d, p in domains: + if public.M('domain').where('name=?', d).count(): + return '指定域名已存在: {}'.format(d) + + return { + "project_name": project_name, + "project_jar": project_jar, + "project_jdk": project_jdk, + # "port": port, + "run_user": run_user, + "project_cmd": project_cmd, + "ps": ps, + "env_list": env_list, + "env_file": env_file, + "domains": ["{}:{}".format(i[0], i[1]) for i in domains], + "proxy_path": proxy_path, + "jmx_status": jmx_status, + "watch_file": watch_file, + "by_process": by_process, + } + + def create_spring_boot_project(self, get): + config = self.check_spring_boot_args(get) + if isinstance(config, str): + return json_response(status=False, msg=config) + + project_config = { + 'ssl_path': '/www/wwwroot/java_node_ssl', + 'project_jdk': config["project_jdk"], + 'project_name': config["project_name"], + 'project_jar': config["project_jar"], + 'bind_extranet': 0 if not config["domains"] else 1, + 'domains': config["domains"], + 'run_user': get.run_user.strip(), + 'jmx_status': config["jmx_status"], + 'project_cmd': config["project_cmd"], + 'java_type': 'springboot', + 'jar_path': os.path.dirname(config["project_jar"]), + 'pids': "{}/pids/{}.pid".format(self._java_project_vhost, config["project_name"]), + 'logs': "{}/{}.log".format(self._java_spring_boot_log_path, config["project_name"]), + 'scripts': "{}/scripts/{}.sh".format(self._java_project_vhost, config["project_name"]), + 'watch_file': config["watch_file"], + 'env_list': config["env_list"], + 'env_file': config["env_file"], + 'proxy_path': config["proxy_path"], + "nohup_log": True, + "static_info": {}, + "proxy_info": [], + "daemon_status": False, + "server_name_suffix": "" + } + + pdata = { + 'name': config["project_name"], + 'path': config["project_jar"], + 'ps': config["ps"], + 'status': 1, + 'type_id': 0, + 'project_type': 'Java', + 'project_config': json.dumps(project_config), + 'addtime': public.getDate() + } + project_id = public.M('sites').insert(pdata) + if not isinstance(project_id, int): + return json_response(status=False, msg='创建项目失败') + pdata["project_config"] = project_config + pdata["id"] = project_id + + if project_config["domains"]: + for domain in project_config["domains"]: + domain_name, port = domain.split(":") + public.M('domain').insert( + { + 'name': domain_name, + 'pid': project_id, + 'port': port, + 'addtime': public.getDate() + } + ) + + error_msg = self._setup_spring_boot_project(pdata, by_process=config["by_process"]) + if error_msg: + return json_response(status=True, msg=error_msg) + return json_response(status=True, msg='项目创建成功, 若未能启动成功, 请查看日志信息,获取启动失败原因') + + def _setup_spring_boot_project(self, pdata, by_process=False) -> Optional[str]: + project_config = pdata["project_config"] + if project_config["jmx_status"]: + pass + # project_config["project_cmd"] = self._build_jmx_cmd(project_config["project_cmd"], project_config['jmx_status']) + if not by_process: + start_status = self._start_spring_boot_project(pdata, write_systemd_file=True, need_wait=False) + if isinstance(start_status, dict): + start_status = start_status["msg"] + + else: + public.writeFile(project_config['pids'], str(by_process)) + start_status = "添加成功" + + if project_config["watch_file"]: + add_project_watch( + p_name=project_config["project_name"], + p_type="java", + site_id=pdata["id"], + watch_path=project_config["project_jar"] + ) + if project_config["domains"]: + res = self.create_config( + pdata, + domains=[i.split(":", 1) for i in project_config["domains"]], + use_ssl=False, + ) + + return start_status + + # 实际启动项目的函数 + def _start_spring_boot_project(self, + project_data: dict, + write_systemd_file=True, + need_wait=True, + ) -> dict: + + old_pid = self.get_project_pid(project_data) + project_config = project_data["project_config"] + server_name = "spring_" + project_config["project_name"] + project_config.get("server_name_suffix", "") + + # 先检查环境变量文件是否存在,没有的时候先写空 + env_file = project_config['env_file'] + env_list = project_config['env_list'] + env_path = "{}/env/{}.env".format(self._java_project_vhost, project_config["project_name"]) + + if isinstance(env_list, list): + if not os.path.exists(os.path.dirname(env_path)): + os.makedirs(os.path.dirname(env_path), 0o755) + public.writeFile(env_path, "\n".join( + ["{}={}".format(i["k"], i["v"]) for i in env_list if "k" in i and "v" in i] + )) + else: + if not os.path.isfile(env_path): + public.writeFile(env_path, "") + + if env_file and not os.path.isfile(env_file): + if not os.path.exists(os.path.dirname(env_file)): + os.makedirs(os.path.dirname(env_file), 0o755) + public.writeFile(env_file, "") + + log_file = project_config['logs'] + pid_file = project_config['pids'] + + if not os.path.exists(log_file): + public.writeFile(log_file, "") + + if not os.path.exists(pid_file): + public.writeFile(pid_file, "0") + + # 修改文件权限 + public.ExecShell( + "chown {usr}:{usr} {file}".format(usr=project_config["run_user"], file=project_config["project_jar"]) + ) + public.ExecShell( + "chown {usr}:{usr} {file}".format(usr=project_config["run_user"], file=log_file) + ) + public.ExecShell( + "chown {usr}:{usr} {file}".format(usr=project_config["run_user"], file=pid_file) + ) + utils.pass_dir_for_user(os.path.dirname(log_file), project_config['run_user']) + + # 如果存在服务文件,没有要求写入时,就直接执行启动停止操作 + s_admin = RealServer() + if not write_systemd_file: + res = s_admin.daemon_status(server_name) + msg = res['msg'] + if msg in ("运行中", "未运行"): + s_admin.daemon_admin(server_name, "restart") + if need_wait: + return self._wait_start_status(project_data, old_pid=old_pid) + else: + return json_response(status=True, msg="操作已执行") + + if msg == "操作失败!": + return res + # 还有一种服务文件丢失的情况走下面的写文件并启动 + + # 要求写入时, 重新写入启动文件 + project_cmd = project_config['project_cmd'] + # 前置准备 + + if project_config["nohup_log"]: + collect_log = log_file + else: + collect_log = '/dev/null' + + jar_path = project_config['jar_path'] + + env = "EnvironmentFile={}".format(env_path) + if env_file: + env += "\nEnvironmentFile={}".format(env_file) + + res = s_admin.create_daemon( + server_name=server_name, + pid_file=pid_file, + start_exec=project_cmd, + workingdirectory=jar_path, + user=project_config["run_user"], + logs_file=collect_log, + environments=env, + restart_type="always" if project_config.get("daemon_status", False) else "no" + ) + if not res["status"]: + return res + + s_admin.daemon_admin(server_name, "start") + if need_wait: + return self._wait_start_status(project_data, old_pid=old_pid) + return json_response(status=True, msg="操作已执行") + + def _wait_start_status(self, project_data: dict, old_pid: int) -> dict: + # 当为重启时,等待关闭 + if old_pid: + try: + p = psutil.Process(old_pid) + for i in range(20): + time.sleep(0.05) + if not p.is_running(): + break + except: + pass + + project_config = project_data['project_config'] + pid = self.get_project_pid(project_data) + if not pid: + for i in range(2): + time.sleep(0.05) + pid = self.get_project_pid(project_data) + if pid: + break + + if not pid: + return json_response(False, + msg="启动失败,详细信息请查看日志", + data=public.GetNumLines(project_config["logs"], 30)) + + for i in range(3): + port = self._get_port_by_pid(pid) + if port: + return json_response(True, "启动成功") + time.sleep(0.05) + if i > 1 and i % 5 == 0: + if pid not in psutil.pids(): + return json_response(False, + msg="启动失败,详细信息请查看日志", + data=public.GetNumLines(project_config["logs"], 30)) + + pid = self.get_project_pid(project_data) + if not pid: + return json_response(False, + msg="启动失败,详细信息请查看日志", + data=public.GetNumLines(project_config["logs"], 30)) + else: + return json_response(True, "未检查到端口监听,启动过程超时,请注意启动情况") + + @staticmethod + def _get_port_by_pid(pid: int) -> Optional[int]: + try: + p = psutil.Process(pid) + for i in p.connections(): + if i.status == "LISTEN": + return i.laddr.port + except: + return None + + # # 启动脚本 + # start_cmd = '''#!/bin/bash + # PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin + # export PATH + # {env} + # cd {jar_path} + # nohup {project_cmd} {collect_log} & + # echo $! > {pid_file} + # '''.format( + # jar_path=jar_path, + # project_cmd=project_cmd, + # pid_file=pid_file, + # log_file=log_file, + # env=env, + # collect_log=collect_log + # ) + # script_file = project_config['scripts'] + # # 写入启动脚本 + # public.writeFile(script_file, start_cmd) + # if os.path.exists(pid_file): + # os.remove(pid_file) + # public.set_mode(script_file, 755) + # # 修改文件权限 + # public.ExecShell( + # "chown {usr}:{usr} {file}".format(usr=project_config["run_user"], file=project_config["project_jar"]) + # ) + # if not os.path.exists(log_file): + # public.writeFile(log_file, "") + # + # public.ExecShell( + # "chown {usr}:{usr} {file}".format(usr=project_config["run_user"], file=log_file) + # ) + # utils.pass_dir_for_user(os.path.dirname(log_file), project_config['run_user']) + # # 执行脚本文件 + # res = public.ExecShell("bash {}".format(script_file), user=project_config['run_user'], env=os.environ.copy()) + # + # time.sleep(1) + # error_msg = '启动失败,您可以尝试查看控制台日志,以获取具体失败原因' + # if not os.path.exists(pid_file): + # return error_msg + # + # # 获取PID + # try: + # pid = int(public.readFile(pid_file)) + # except: + # return error_msg + # if pid not in psutil.pids(): + # return error_msg + # + # return None + + @staticmethod + def _build_jmx_cmd(cmd_str: str, jmx_status: bool): + has_jmx_args = cmd_str.find("-Dcom.sun.management.jmxremote") != -1 + if not jmx_status: + if not has_jmx_args: + return cmd_str + + cmd_str_list = cmd_str.split(" ") + for i in range(len(cmd_str_list) - 1, -1, -1): + if cmd_str_list[i].startswith("-Dcom.sun.management.jmxremote"): + cmd_str_list.pop(i) + if cmd_str_list[i].startswith("-Djava.rmi.server.hostname=127.0.0.1"): + cmd_str_list.pop(i) + + return " ".join(cmd_str_list) + + if jmx_status and has_jmx_args: + port = re.search(r"-Dcom\.sun\.management\.jmxremote\.port=(?P\d+)", cmd_str) + if not utils.check_port(port.group("port")): # 如果原来的port被占用了,则生成一个随机的port并替换 + cmd_str = re.sub( + r"-Dcom\.sun\.management\.jmxremote\.port=(\d+)", + "-Dcom.sun.management.jmxremote.port={}".format(utils.create_a_not_used_port()), + cmd_str + ) + return cmd_str + + add_cmd = ( + " -Dcom.sun.management.jmxremote" + " -Dcom.sun.management.jmxremote.port={}" + " -Djava.rmi.server.hostname=127.0.0.1" + " -Dcom.sun.management.jmxremote.local.only=true" + " -Dcom.sun.management.jmxremote.authenticate=false" + " -Dcom.sun.management.jmxremote.ssl=false" + ).format(utils.create_a_not_used_port()) + + cmd_str = cmd_str.lstrip(" ") + first_space_idx = cmd_str.find(" ") # 把jmx的配置放到第一个空格之后 + if first_space_idx == -1: + return cmd_str + add_cmd + else: + return cmd_str[:first_space_idx] + add_cmd + cmd_str[first_space_idx:] + + @staticmethod + def check_tomcat_args(get) -> Union[Dict, str]: + port = None + jdk_path = None + try: + domain = get.domain.strip() + tomcat_version = int(get.tomcat_version) + project_path = get.project_path.strip() + if hasattr(get, "port"): + port = int(get.port) + if hasattr(get, "jdk_path"): + jdk_path = get.jdk_path.strip() + ps = get.project_ps.strip() + except: + return "参数错误" + + if not is_domain(domain): + return "请输入正确的域名" + + if not os.path.exists(project_path): + os.makedirs(project_path) + public.set_own(project_path, 'www') + if tomcat_version not in (7, 8, 9, 10): + return "请选择正确的Tomcat版本" + + if jdk_path is not None: + if not os.path.exists(jdk_path): + return "JDK路径不存在" + jdk_path = utils.normalize_jdk_path(jdk_path) + if not isinstance(jdk_path, str): + return '项目JDK路径不存在' + if not utils.test_jdk(jdk_path): + return "JDK路径错误" + + if port is not None: + if not utils.check_port(port): + return "端口被占用" + + domain_list, err = normalize_domain(domain) + if not err: + domain = domain_list[0] + if public.M('domain').where('name=?', domain[0]).count(): + return '指定域名已存在: {}'.format(domain[0]) + project_name = domain[0] + domain = "{}:{}".format(domain[0], domain[1]) + else: + return "
                                    ".join(["域名:{},错误信息:{}".format(i['domain'], i['msg']) for i in err]) + + return { + "project_name": project_name, + "project_jdk": jdk_path, + "ps": ps, + "port": port, + "domains": [domain, ], + "tomcat_version": tomcat_version, + "project_path": project_path, + } + + def create_tomcat_project(self, get): + config = self.check_tomcat_args(get) + if isinstance(config, str): + return json_response(status=False, msg=config) + + tomcat = utils.bt_tomcat(config["tomcat_version"]) + if not tomcat.installed: + return json_response(status=False, msg="指定的Tomcat版本未安装") + + if config["project_jdk"] and tomcat.jdk_path != config["project_jdk"]: + if not tomcat.replace_jdk(config["project_jdk"]): + return json_response(status=False, msg="替换JDK失败") + + if config["port"] and not tomcat.set_port(config["port"]): + return json_response(status=False, msg="设置端口失败") + + if not tomcat.add_host(config["project_name"], config["project_path"]): + return json_response(status=False, msg="添加失败") + + # 有web服务且可以重启 + if public.is_apache_nginx() and not isinstance(public.checkWebConfig(), str): + bind_extranet = 1 + else: + bind_extranet = 0 + + if os.path.isfile(config["project_path"]): + path = os.path.basename(config["project_path"]) + else: + path = config["project_path"] + + project_config = { + 'project_name': config["project_name"], + 'bind_extranet': bind_extranet, + 'domains': config['domains'], + 'tomcat_version': config["tomcat_version"], + 'java_type': 'neizhi', + 'server_xml': '/usr/local/bt_mod_tomcat/tomcat%s/conf/server.xml' % config["tomcat_version"], + 'port': int(tomcat.port()), + 'auth': '1', # 默认开机自动启动 + 'logs': os.path.dirname(tomcat.log_file), + 'ssl_path': '/www/wwwroot/java_node_ssl', + "proxy_info": [], + } + + # 添加数据库信息 + pdata = { + 'name': config["project_name"], + 'path': config["project_path"], + 'ps': config["ps"], + 'status': 1, + 'type_id': 0, + 'project_type': 'Java', + 'project_config': json.dumps(project_config), + 'addtime': public.getDate() + } + tomcat.save_config_xml() + project_id = public.M('sites').insert(pdata) + pdata["id"] = project_id + for domain in config["domains"]: + domain_name, port = domain.split(":") + public.M('domain').insert({ + 'name': domain_name, + 'pid': project_id, + 'port': port, + 'addtime': public.getDate() + }) + tomcat.restart() + + pdata["project_config"] = project_config + + if bind_extranet: + self.create_config( + pdata, + domains=[i.split(":") for i in config['domains']], + use_ssl=False) + + return json_response(status=True, msg='项目创建成功') + + @staticmethod + def check_site_tomcat_args(get) -> Union[Dict, str]: + jdk_path = None + try: + domain = get.domain.strip() + tomcat_version = int(get.tomcat_version) + project_path = get.project_path.strip() + if hasattr(get, "project_jdk"): + jdk_path = get.project_jdk.strip() + port = int(get.port) + run_user = get.run_user.strip() + ps = get.project_ps.strip() + except: + return "参数错误" + + if not utils.check_port(port): + return "端口已被使用" + + if not is_domain(domain): + return "请输入正确的域名" + if not os.path.exists(project_path): + os.makedirs(project_path) + public.set_own(project_path, 'www') + if tomcat_version not in (7, 8, 9, 10): + return "请选择正确的Tomcat版本" + + if jdk_path: + if not os.path.exists(jdk_path): + return "JDK路径不存在" + jdk_path = utils.normalize_jdk_path(jdk_path) + if not isinstance(jdk_path, str): + return "JDK路径错误" + if not utils.test_jdk(jdk_path): + return "JDK检查错误" + + if run_user not in [i["username"] for i in RealUser().get_user_list()["data"]]: + return '请输入正确的启动用户' + + domain_list, err = normalize_domain(domain) + if err: + return "
                                    ".join(["域名:{},错误信息:{}".format(i['domain'], i['msg']) for i in err]) + else: + for d, p in domain_list: + if public.M('domain').where('name=?', d).count(): + return '指定域名已存在: {}'.format(d) + domain = domain_list[0] + project_name = domain[0] + domain = "{}:{}".format(domain[0], domain[1]) + + return { + "project_name": project_name, + "project_jdk": jdk_path, + "run_user": run_user, + "port": port, + "ps": ps, + "domains": [domain, ], + "tomcat_version": tomcat_version, + "project_path": project_path, + 'ssl_path': '/www/wwwroot/java_node_ssl', + } + + def create_site_tomcat_project(self, get): + config = self.check_site_tomcat_args(get) + if isinstance(config, str): + return json_response(status=False, msg=config) + + site_tomcat_path = os.path.join(self._site_tomcat_path, config["project_name"]) + if os.path.exists(site_tomcat_path): + return json_response(status=False, msg="该网站已经存在。如想建立请删除%s" % site_tomcat_path) + + # 首先需要先复制好文件过去 + if not os.path.exists(site_tomcat_path): + os.makedirs(site_tomcat_path) + + bt_tomcat_back = "/usr/local/bttomcat/tomcat_bak%d" + if not os.path.exists(bt_tomcat_back % config["tomcat_version"] + '/conf/server.xml'): + return json_response(False, "tomcat7的配置文件不存在,请重新安装tomcat7") + public.ExecShell('cp -r %s/* %s && chown -R %s:%s %s' % ( + bt_tomcat_back % int(config["tomcat_version"]), site_tomcat_path, + config["run_user"], config["run_user"], site_tomcat_path + )) + tomcat = utils.site_tomcat(config["project_name"]) + if not tomcat: + return json_response(status=False, msg="Tomcat未能初始化成功") + + # server.xml + if os.path.exists(site_tomcat_path + '/conf/server.xml'): + tomcat.reset_tomcat_server_config(config["port"]) + else: + os.system('rm -rf %s' % site_tomcat_path) + return json_response(False, "配置文件不存在请重新安装tomcat后尝试新建网站") + + if tomcat.jdk_path != config["project_jdk"]: + res = tomcat.replace_jdk(config["project_jdk"]) + if res: + os.system('rm -rf %s' % site_tomcat_path) + return json_response(status=False, msg="JDK替换失败") + + log_path = "/www/wwwlogs/java/{}".format(config["project_name"]) + if not os.path.exists(log_path) or os.path.isfile(log_path): + os.makedirs(log_path, mode=0o755) + tomcat.change_log_path(log_path, prefix=config["project_name"].replace(".", '_')) + + if not tomcat.add_host(config["project_name"], config["project_path"]): + os.system('rm -rf %s' % site_tomcat_path) + return json_response(status=False, msg="添加失败") + + # 有web服务且可以重启 + if public.is_apache_nginx() and not isinstance(public.checkWebConfig(), str): + bind_extranet = 1 + else: + bind_extranet = 0 + + if os.path.isfile(config["project_path"]): + path = os.path.basename(config["project_path"]) + else: + path = config["project_path"] + + project_config = { + 'project_name': config["project_name"], + "project_jdk": config["project_jdk"], + 'bind_extranet': bind_extranet, + 'domains': config["domains"], + 'tomcat_version': config["tomcat_version"], + 'java_type': 'duli', + 'server_xml': site_tomcat_path + "/conf/server.xml", + 'port': config["port"], + "run_user": config["run_user"], + 'logs': log_path, + "proxy_info": [], + } + + pdata = { + 'name': config["project_name"], + 'path': config["project_path"], + 'ps': config["ps"], + 'status': 1, + 'type_id': 0, + 'project_type': 'Java', + 'project_config': json.dumps(project_config), + 'addtime': public.getDate() + } + tomcat.save_config_xml() + project_id = public.M('sites').insert(pdata) + pdata["id"] = project_id + for domain in config["domains"]: + domain_name, port = domain.split(":") + public.M('domain').insert({ + 'name': domain_name, + 'pid': project_id, + 'port': port, + 'addtime': public.getDate() + }) + + tomcat.restart(by_user=config["run_user"]) + + pdata["project_config"] = project_config + if bind_extranet: + self.create_config( + pdata, + domains=[i.split(":") for i in config['domains']], + use_ssl=False) + + return json_response(status=True, msg='项目创建成功') + + def create_project(self, get): + try: + project_type = int(get.project_type) + except: + return json_response(status=False, msg="项目类型错误") + + if project_type == 0: + return self.create_spring_boot_project(get) + elif project_type == 1: + return self.create_tomcat_project(get) + else: + return self.create_site_tomcat_project(get) + + @staticmethod + def get_project_find(project_name: str) -> Optional[dict]: + project_info = public.M('sites').where('project_type=? AND name=?', ('Java', project_name)).find() + if not project_info: + return None + + # 做旧项目的兼容性处理 + project_info['project_config'] = json.loads(project_info['project_config']) + if "jmx_status" not in project_info['project_config']: + project_info['project_config']['jmx_status'] = False + if "nohup_log" not in project_info['project_config']: + project_info['project_config']['nohup_log'] = True + + if "env_file" not in project_info['project_config']: + project_info['project_config']['env_file'] = '' + if "env_list" not in project_info['project_config']: + project_info['project_config']['env_list'] = [] + + if "static_path" in project_info['project_config'] and "static_info" not in project_info['project_config']: + project_info['project_config']['static_info'] = { + "path": project_info['project_config']['static_path'], + "status": True, + "index": "", + "use_try_file": True + } + public.M('sites').where("id=?", (project_info['id'],)).update( + {'project_config': json.dumps(project_info['project_config'])} + ) + if "proxy_info" not in project_info['project_config']: + rsp = RealServerProxy(project_info) + proxy_list = rsp.get_proxy_list() + project_info['project_config']['proxy_info'] = proxy_list + if 'auth' in project_info['project_config'] and project_info['project_config']['auth'] in (1, "1") and \ + "daemon_status" not in project_info['project_config']: + project_info['project_config']['daemon_status'] = True + + if 'daemon_status' not in project_info['project_config']: + project_info['project_config']['daemon_status'] = False + + return project_info + + def modify_spring_boot_project(self, get, project_data: Optional[dict] = None): + if not project_data: + try: + project_name = self.get_project_find(get.project_name.strip()) + except: + return json_response(status=False, msg="项目名称错误") + if not project_data: + return json_response(status=False, msg="项目不存在") + else: + project_name = project_data['name'] + + change_flag = False + project_config = project_data['project_config'] + if hasattr(get, 'run_user') and get.run_user.strip() != project_config['run_user']: + project_config['run_user'] = get.run_user.strip() + change_flag = True + if hasattr(get, 'project_jdk'): + input_jdk = utils.normalize_jdk_path(get.project_jdk.strip()) + if not isinstance(input_jdk, str): + return json_response(False, 'JDK检测错误') + + if input_jdk != project_config['project_jdk']: + project_config['project_jdk'] = input_jdk + change_flag = True + + if hasattr(get, 'project_jar') and get.project_jar.strip() != project_config['project_jar']: + project_config['project_jar'] = get.project_jar.strip() + if not os.path.isfile(project_config['project_jar']): + return json_response(False, '项目jar包不存在') + project_config['jar_path'] = os.path.dirname(project_config['project_jar']) + change_flag = True + + if hasattr(get, 'project_cmd') and get.project_cmd.strip(): + if get.project_cmd.strip() != project_config['project_cmd']: + project_config['project_cmd'] = get.project_cmd.strip() + change_flag = True + else: + return json_response(False, '缺少project_cmd参数') + + if hasattr(get, 'daemon_status'): + daemon_status = utils.js_value_to_bool(get.daemon_status) + if daemon_status != project_config['daemon_status']: + project_config['daemon_status'] = daemon_status + change_flag = True + + # 检查jar包是否在cmd中 + if project_config['project_cmd'].find(get.project_jar.strip()) == -1: + return json_response(False, '项目jar包名称不在项目启动命令中,请检查') + + if hasattr(get, 'jmx_status') and utils.js_value_to_bool(get.jmx_status) != project_config['jmx_status']: + project_config['jmx_status'] = utils.js_value_to_bool(get.jmx_status) + change_flag = True + + if hasattr(get, "env_file") and get.env_file.strip() != project_config['env_file']: + project_config['env_file'] = get.env_file.strip() + if project_config['env_file'] and not os.path.exists(project_config['env_file']): + return json_response(False, '项目环境文件不存在') + change_flag = True + + if hasattr(get, "env_list"): + env_list = get.env_list + if isinstance(env_list, str): + try: + env_list = json.loads(env_list) + except: + return json_response(False, '项目环境变量格式错误') + if not isinstance(env_list, list): + return json_response(False, '项目环境变量格式错误') + if env_list != project_config['env_list']: + project_config['env_list'] = env_list + change_flag = True + + if hasattr(get, 'watch_file'): + project_config['watch_file'] = utils.js_value_to_bool(get.watch_file) + if project_config['watch_file']: + add_project_watch(p_name=get.project_name.strip(), + p_type="java", + site_id=project_data["id"], + watch_path=get.project_jar.strip()) + else: + del_project_watch(get.project_name.strip()) + + if change_flag: + project_config["change_flag"] = True + + pdata = { + 'name': project_name, + 'path': get.project_jar.strip(), + 'ps': get.project_ps.strip(), + 'project_config': json.dumps(project_config) + } + public.M('sites').where('name=?', (get.project_name,)).update(pdata) + + return json_response(True, '项目修改成功,重启后生效') + + def modify_tomcat_project(self, get, project_data: Optional[dict] = None): + if not project_data: + try: + project_name = self.get_project_find(get.project_name.strip()) + except: + return json_response(status=False, msg="项目名称错误") + if not project_data: + return json_response(status=False, msg="项目不存在") + else: + project_name = project_data['name'] + + project_config = project_data['project_config'] + tomcat = utils.bt_tomcat(project_config["tomcat_version"]) + + flag = False + # 更换JDK + if hasattr(get, 'project_jdk') and get.project_jdk: + jdk_path = utils.normalize_jdk_path(get.project_jdk.strip()) + if not isinstance(jdk_path, str): + return json_response(False, 'JDK检测错误') + if jdk_path != tomcat.jdk_path: + if not utils.test_jdk(jdk_path): + return json_response(False, '当前JDK不可用') + + res = tomcat.replace_jdk(jdk_path) + if not res: + return json_response(False, res) + flag = True + + if hasattr(get, 'project_path'): + if get.project_path.strip() == project_data['path']: + pass + else: + if not tomcat.set_host_path_by_name(project_name, get.project_path.strip()): + return json_response(False, '项目路径设置失败') + project_data['path'] = get.project_path.strip() + + # 更换描述 + if hasattr(get, 'project_ps'): + if get.project_ps.strip() != project_data['ps']: + project_data['ps'] = get.project_ps.strip() + + if flag: + tomcat.restart() + + pdata = { + 'path': project_data['path'], + 'ps': project_data['ps'], + } + public.M('sites').where('name=?', (get.project_name,)).update(pdata) + return json_response(True, '项目修改成功') + + def modify_site_tomcat_project(self, get, project_data: Optional[dict] = None): + if not project_data: + try: + project_name = self.get_project_find(get.project_name.strip()) + except: + return json_response(status=False, msg="项目名称错误") + if not project_data: + return json_response(status=False, msg="项目不存在") + else: + project_name = project_data['name'] + + project_config = project_data['project_config'] + tomcat = utils.site_tomcat(project_name) + if not tomcat: + return json_response(False, '项目的Tomcat已被删除,请尝试修复项目') + + flag = False + # 更换JDK + if hasattr(get, 'project_jdk') and get.project_jdk: + jdk_path = utils.normalize_jdk_path(get.project_jdk.strip()) + if not isinstance(jdk_path, str): + return json_response(False, 'JDK检测错误') + if jdk_path != tomcat.jdk_path: + if not utils.test_jdk(jdk_path): + return json_response(False, '当前JDK不可用') + + res = tomcat.replace_jdk(jdk_path) + if isinstance(res, str): + return json_response(False, res) + flag = True + project_config["project_jdk"] = jdk_path + + if hasattr(get, 'port'): + try: + port = int(get.port) + except: + return json_response(False, '端口信息错误') + + if port != tomcat.port(): + if not utils.check_port(port): + return json_response(False, '端口已被占用') + + res = tomcat.set_port(port) + if not res: + return json_response(False, '端口设置失败') + tomcat.save_config_xml() + flag = True + project_config["port"] = port + + # 更换描述 + if hasattr(get, 'project_ps'): + if get.project_ps.strip() != project_data['ps']: + project_data['ps'] = get.project_ps.strip() + + if hasattr(get, 'project_path'): + if get.project_path.strip() != project_data['path']: + if not tomcat.set_host_path_by_name(project_name, get.project_path.strip()): + return json_response(False, '项目路径设置失败') + project_data['path'] = get.project_path.strip() + flag = True + + if hasattr(get, 'run_user'): + if "run_user" not in project_config: + project_config["run_user"] = "root" + if get.run_user.strip() != project_config['run_user']: + run_user = get.run_user.strip() + if run_user not in [i["username"] for i in RealUser().get_user_list()["data"]]: + return json_response(False, '请输入正确的启动用户') + else: + project_config["run_user"] = run_user + flag = True + + if flag: + tomcat.restart(by_user=project_config["run_user"]) + + pdata = { + 'path': project_data['path'], + 'ps': project_data['ps'], + 'project_config': json.dumps(project_config), + } + public.M('sites').where('name=?', (get.project_name,)).update(pdata) + return json_response(True, '项目修改成功') + + def modify_project(self, get): + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(status=False, msg="参数错误") + if not project_data: + return json_response(status=False, msg="项目不存在") + project_config = project_data["project_config"] + if project_config["java_type"] == "springboot": + return self.modify_spring_boot_project(get, project_data) + elif project_config["java_type"] == "duli": + return self.modify_site_tomcat_project(get, project_data) + else: + return self.modify_tomcat_project(get, project_data) + + def get_project_pid(self, project_data: dict) -> Optional[int]: + # 从pid文件中获取项目pid + project_config = project_data["project_config"] + if project_config["java_type"] == "springboot": + pid_file = project_config["pids"] + pid = None + if os.path.isfile(pid_file): + try: + pid = int(public.readFile(pid_file)) + except: + pass + elif project_config["java_type"] == "neizhi": + pid = utils.bt_tomcat(project_config["tomcat_version"]).pid() + else: + pid = utils.site_tomcat(project_data["name"]).pid() + if not pid and project_config["java_type"] == "springboot": + try: + return self._get_pid_by_command(project_data) + except: + return None + + try: + psutil.Process(pid) + except: + return None + + return pid + + @staticmethod + def _get_pid_by_command(project_data: dict) -> Optional[int]: + project_config = project_data["project_config"] + server_name = "spring_" + project_config["project_name"] + project_config.get("server_name_suffix", "") + print(server_name) + server_admin = RealServer() + pid = server_admin.get_daemon_pid(server_name)["data"] + if pid == 0: + time.sleep(0.5) + pid = server_admin.get_daemon_pid(server_name)["data"] + if isinstance(pid, int): + try: + p = psutil.Process(pid) + if p.is_running(): + public.writeFile(project_data["project_config"]['pids'], str(pid)) + return pid + except: + pass + + if project_config["java_type"] == "springboot": + jdk_path = utils.normalize_jdk_path(project_config['project_jdk']) + project_jar = project_config['project_jar'] + jar_name = os.path.basename(project_jar) + jar_cmd = project_config['project_cmd'] + port_rep = re.search(r"--server\.port=\d+", jar_cmd) + pids = [] + for pro in psutil.process_iter(['pid', 'exe', 'cmdline']): + if pro.status() == "zombie": + continue + try: + if port_rep and port_rep.group() not in pro.cmdline(): + continue + if jdk_path == utils.normalize_jdk_path(pro.exe()) and any((jar_name in i for i in pro.cmdline())): + pids.append(pro.pid) + except: + pass + + if not pids: + return None + + running_pid = [] + for pid in pids: + if pid in psutil.pids(): + running_pid.append(pid) + + if len(running_pid) == 1: + public.writeFile(project_data["project_config"]['pids'], str(running_pid[0])) + return running_pid[0] + for pid in running_pid: + p = psutil.Process(pid) + if p.ppid() not in running_pid: + public.writeFile(project_data["project_config"]['pids'], str(pid)) + return pid + + return None + + def project_process(self, project_data) -> Optional[psutil.Process]: + pid = self.get_project_pid(project_data) + if not pid: + return None + try: + return psutil.Process(pid) + except: + return None + + def start_spring_boot_project(self, project_data: dict, wait: bool = True): + change_flag = False + project_config = project_data['project_config'] + if "change_flag" in project_config: + change_flag = project_config.get("change_flag", False) + del project_config["change_flag"] + public.M("sites").where("id=?", (project_data["id"],)).update( + {"project_config": json.dumps(project_config)} + ) + res = self._start_spring_boot_project(project_data, change_flag, need_wait=wait) + if res["status"] is True: + utils.start_by_user(project_data["id"]) + return res + + def start_project(self, get): + try: + project_data = self.get_project_find(get.project_name) + except: + return json_response(False, '项目名称参数错误') + + if not project_data: + return json_response(False, '项目不存在') + + p = self.project_process(project_data) + if p and p.is_running(): + return json_response(False, '项目已启动') + + project_config = project_data['project_config'] + if project_config["java_type"] == "springboot": + return self.start_spring_boot_project(project_data) + + elif project_config["java_type"] == "neizhi": + res = utils.bt_tomcat(project_config["tomcat_version"]).start() + if not res: + return json_response(False, '项目启动失败') + else: + tomcat = utils.site_tomcat(project_config["project_name"]) + if not tomcat: + return json_response(False, '独立项目的Tomcat文件丢失,请尝试修复项目') + if not tomcat.running(): + res = tomcat.start() + if not res: + return json_response(False, '项目启动失败') + + utils.start_by_user(project_data["id"]) + return json_response(True, "项目启动成功") + + def stop_project(self, get): + try: + project_data = self.get_project_find(get.project_name) + except: + return json_response(False, '项目名称参数错误') + + if not project_data: + return json_response(False, '项目不存在') + + project_config = project_data['project_config'] + if project_config["java_type"] == "springboot": + server_name = "spring_" + project_config["project_name"] + project_config.get("server_name_suffix", "") + s_admin = RealServer() + if s_admin.daemon_status(server_name)["msg"] == "服务不存在!": + self.stop_by_kill_pid(project_data) + if os.path.isfile(project_config["pids"]): + os.remove(project_config["pids"]) + else: + s_admin.daemon_admin(server_name, "stop") + utils.stop_by_user(project_data["id"]) + return json_response(True, msg="项目停止指令已执行") + elif project_config["java_type"] == "neizhi": + res = utils.bt_tomcat(project_config["tomcat_version"]).stop() + if not res: + return json_response(False, '项目停止失败') + else: + tomcat = utils.site_tomcat(project_config["project_name"]) + if not tomcat: + return json_response(False, '独立项目的Tomcat文件丢失,请尝试修复项目') + if tomcat.running(): + res = tomcat.stop() + if not res: + return json_response(False, '项目停止失败') + utils.stop_by_user(project_data["id"]) + return json_response(True, "项目停止成功") + + def stop_by_kill_pid(self, project_data): + pid = self.get_project_pid(project_data) + if not pid: + return + try: + p = psutil.Process(pid) + p.kill() + except: + pass + + def restart_project(self, get): + try: + project_data = self.get_project_find(get.project_name) + except: + return json_response(False, '项目名称参数错误') + + if not project_data: + return json_response(False, '项目不存在') + + project_config = project_data['project_config'] + if project_config["java_type"] == "springboot": + s_admin = RealServer() + server_name = "spring_" + project_config["project_name"] + project_config.get("server_name_suffix", "") + if s_admin.daemon_status(server_name)["msg"] == "服务不存在!": + self.stop_by_kill_pid(project_data) + if os.path.isfile(project_config["pids"]): + os.remove(project_config["pids"]) + return self._start_spring_boot_project(project_data, write_systemd_file=True) + + if "change_flag" in project_config and project_config.get("change_flag", False): + del project_config["change_flag"] + s_admin.daemon_admin(server_name, "stop") + s_admin.del_daemon(server_name) + self.stop_by_kill_pid(project_data) + if os.path.isfile(project_config["pids"]): + os.remove(project_config["pids"]) + + public.M("sites").where("id=?", (project_data["id"],)).update( + {"project_config": json.dumps(project_config)} + ) + return self._start_spring_boot_project(project_data, write_systemd_file=True) + else: + return self._start_spring_boot_project(project_data, write_systemd_file=False) + + self.stop_project(get) + time.sleep(0.5) + self.start_project(get) + + return json_response(True, "项目重启已执行") + + def project_list(self, get): + """取项目列表""" + p = 1 + limit = 12 + callback = "" + order = "id desc" + search = "" + type_id = None + try: + if hasattr(get, "p"): + p = int(get.p) + if hasattr(get, "limit"): + limit = int(get.limit) + if hasattr(get, "callback"): + callback = get.callback.strip() + if hasattr(get, "order"): + order = get.order.strip() + if hasattr(get, "search"): + search = get.search.strip() + if hasattr(get, "type_id") and get.type_id: + type_id = int(get.type_id) + except: + return json_response(False, '参数错误') + + type_filter = '' + if type_id is not None: + type_filter = ' AND type_id=?' + + if search: + search = "%{}%".format(search) + if type_filter: + where_str = 'project_type=? AND (name LIKE ? OR ps LIKE ?)' + type_filter + where_args = ('Java', search, search, type_id) + else: + where_str = 'project_type=? AND (name LIKE ? OR ps LIKE ?)' + where_args = ('Java', search, search) + count = public.M('sites').where(where_str, where_args).count() + + data = public.get_page(count, p, limit, callback) + data['data'] = public.M('sites').where(where_str, where_args).limit( + data['shift'] + ',' + data['row']).order(order).select() + else: + if type_filter: + where_str = 'project_type=?' + type_filter + where_args = ('Java', type_id) + else: + where_str = 'project_type=?' + where_args = ('Java',) + count = public.M('sites').where(where_str, where_args).count() + data = public.get_page(count, p, limit, callback) + data['data'] = public.M('sites').where(where_str, where_args).limit( + data['shift'] + ',' + data['row']).order(order).select() + + for project_data in data['data']: + project_config = json.loads(project_data['project_config']) + project_data['project_config'] = project_config + + # 如果内置项目 或 独立项目 的tomcat配置文件丢失 + if project_config['java_type'] == 'neizhi' or project_config['java_type'] == 'duli': + if not os.path.exists(project_config['server_xml']): + + project_data['server_file_status'] = False + else: + project_data['server_file_status'] = True + + for i in range(len(data['data'])): + self.get_project_stat(data['data'][i]) + + return data + + def get_project_stat(self, project_data: dict) -> dict: + if isinstance(project_data['project_config'], str): + project_config = json.loads(project_data['project_config']) + project_data['project_config'] = project_config + + project_data["pid"] = self.get_project_pid(project_data) + if project_data["project_config"]["java_type"] == "springboot": + project_data["project_config"]["watch_file"] = use_project_watch(project_data["name"]) + project_data["listen"] = [] + project_data["ssl"] = RealSSLManger("java_").get_site_ssl_info(project_data["name"]) + if not project_data["ssl"]: + project_data["ssl"] = -1 + if project_data["pid"]: + project_data["pid_info"] = self.real_process.get_process_info_by_pid(project_data["pid"])["data"] + listen = [] + if project_data["pid_info"] and "connections" in project_data["pid_info"]: + for connection in project_data["pid_info"]["connections"]: + if connection["status"] == "LISTEN": + listen.append(connection["local_port"]) + project_data["listen"] = listen + if project_data['project_config']["java_type"] == "springboot": + project_data['project_config']["port"] = ",".join([str(i) for i in listen]) + else: + project_data["pid_info"] = None + + project_data["starting"] = False + if project_data["pid"] and not project_data["listen"]: + project_data["starting"] = True + + if os.path.exists("{}/nginx/{}.conf".format(self._vhost_path, project_data["name"])) or \ + os.path.exists("{}/apache/{}.conf".format(self._vhost_path, project_data["name"])): + + project_data["bind_extranet"] = True + else: + project_data["bind_extranet"] = False + + if project_data['project_config']["java_type"] == "duli" and "project_jdk" not in project_data['project_config']: + tomcat = utils.site_tomcat(project_data["name"]) + if tomcat: + project_data['project_config']["project_jdk"] = tomcat.jdk_path + else: + project_data['project_config']["project_jdk"] = "/usr/local/btjdk/jdk8" + + return project_data + + @staticmethod + def _project_domain_list(project_id: int): + return public.M('domain').where('pid=?', (project_id,)).select() + + def project_domain_list(self, get): + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not project_data: + return json_response(False, '指定项目不存在') + + data = self._project_domain_list(project_data['id']) + return json_response(True, data=data) + + def add_domains(self, get): + """ 为指定项目添加域名 """ + try: + if isinstance(get.domains, str): + domains = json.loads(get.domains) + else: + domains = get.domains + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not isinstance(domains, list): + return json_response(False, '域名参数错误') + + if not project_data: + return json_response(False, '指定项目不存在') + + project_id = project_data['id'] + project_name = project_data["name"] + + domain_list, err = normalize_domain(*domains) + if err: + return "
                                    ".join(["域名:{},错误信息:{}".format(i['domain'], i['msg']) for i in err]) + + res_domains = [] + for d, p in domain_list: + if not public.M('domain').where('name=?', (d,)).count(): + public.M('domain').add('name,pid,port,addtime', (d, project_id, p, public.getDate())) + self.write_project_log('成功添加域名{}到项目{}'.format(d, get.project_name)) + res_domains.append({"name": d, "status": True, "msg": '添加成功'}) + else: + self.write_project_log('添加域名错误,域名{}已存在'.format(d)) + res_domains.append({"name": d, "status": False, "msg": '添加失败,域名{}已存在'.format(d)}) + all_domain = self._project_domain_list(project_id) + print(all_domain) + # 写配置文件 + if utils.js_value_to_bool(project_data["project_config"]["bind_extranet"]): + res = self._set_domain(project_data, [(i["name"], str(i["port"])) for i in all_domain]) + if res: + return json_response(True, msg="域名记录成功,但配置文件写入时失败:" + res, data=res_domains) + return json_response(True, msg="添加成功", data=res_domains) + + def remove_domains(self, get): + """ 移除指定项目中的域名 """ + try: + if isinstance(get.domains, str): + remove_list = json.loads(get.domains) + else: + remove_list = get.domains + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not project_data: + return json_response(False, '指定项目不存在') + + if not isinstance(remove_list, list): + return json_response(False, '域名参数错误') + + all_domain = self._project_domain_list(project_data["id"]) + if not all_domain: + return json_response(False, '指定项目中没有域名可被删除') + + bind_extranet = False + if os.path.exists("{}/nginx/{}.conf".format(self._vhost_path, project_data["name"])) or \ + os.path.exists("{}/apache/{}.conf".format(self._vhost_path, project_data["name"])): + bind_extranet = True + + if len(all_domain) == 1 and bind_extranet: + return json_response(False, '请至少保留一个域名,如无需外网映射,关闭即可', data=[{ + "domain": all_domain[0]["name"], + "status": False, + "msg": "无法删除最后一个域名" + }]) + + all_domain.sort(key=lambda x: x["id"], reverse=True) + all_domain_id_dict = {i["id"]: i for i in all_domain} + + del_id_list = [i for i in all_domain_id_dict if i in remove_list] + + res_data = [] + # 说明选中了所有域名,这时需要保持一个默认域名 + default_domain = None + if len(all_domain_id_dict) == len(del_id_list) and bind_extranet: + default_domain = all_domain[0] + del_id_list.remove(default_domain["id"]) + + for i in del_id_list: + public.M('domain').delete(id=i) + res_data.append( + { + "domain": all_domain_id_dict[i]["name"], + "status": True, + "msg": "删除成功" + } + ) + self.write_project_log('成功删除域名{}'.format([all_domain_id_dict[i]["name"] for i in del_id_list])) + if default_domain: + res_data.append( + { + "domain": default_domain["name"], + "status": False, + "msg": "无法删除最后一个域名" + } + ) + + if bind_extranet: + now_domain = self._project_domain_list(project_data["id"]) + # 写配置文件 + res = self._set_domain(project_data, [(i["name"], str(i["port"])) for i in now_domain]) + if res: + return json_response(False, msg="域名记录删除成功,但配置文件写入时失败:" + res, data=res_data) + return json_response(True, msg="删除成功", data=res_data) + + def bind_extranet(self, get): + """开放外网映射""" + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + + if not public.is_apache_nginx(): + return json_response(False, "未安装Nginx") + + err_msg = public.checkWebConfig() + if isinstance(err_msg, str): + msg = 'WEB服务器配置配置文件错误ERROR:
                                    ' + \ + err_msg.replace("\n", '
                                    ') + '
                                    ' + return json_response(False, msg=msg) + + res = self._open_config_file(project_data) + if isinstance(res, str): + return json_response(False, msg=res) + project_config = project_data["project_config"] + project_config["bind_extranet"] = 1 + public.M('sites').where('id=?', (project_data["id"],)).update( + {"project_config": json.dumps(project_config)} + ) + return json_response(True, msg="设置成功") + + def unbind_extranet(self, get): + """关闭外网映射""" + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + + project_config = project_data["project_config"] + self._close_apache_config_file(project_data) + project_config["bind_extranet"] = 0 + public.M('sites').where('id=?', (project_data["id"],)).update( + {"project_config": json.dumps(project_config)} + ) + return json_response(True, msg="设置成功") + + def remove_project(self, get): + """删除指定项目""" + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + + project_config = project_data["project_config"] + project_name = project_data["name"] + if project_config['java_type'] == 'duli': + tomcat = utils.site_tomcat(project_name) + # 关闭独立项目 + if tomcat: + tomcat.stop() + if os.path.exists(tomcat.path): + shutil.rmtree(tomcat.path) + + elif project_config['java_type'] == 'neizhi': + # 删除tomcat站点 + tomcat = utils.bt_tomcat(project_config["tomcat_version"]) + tomcat.remove_host(project_name) + tomcat.save_config_xml() + if tomcat.running(): + tomcat.restart() + + elif project_config['java_type'] == 'springboot': + # 停止项目 + server_name = "spring_" + project_config["project_name"] + project_config.get("server_name_suffix", "") + s_admin = RealServer() + s_admin.daemon_admin(server_name, "stop") + s_admin.del_daemon(server_name) + + pid_file = project_config['pids'] + if os.path.exists(pid_file): + os.remove(pid_file) + script_file = project_config['scripts'] + if os.path.exists(script_file): + os.remove(script_file) + env_path = "{}/env/{}.env".format(self._java_project_vhost, project_config["project_name"]) + if os.path.exists(env_path): + os.remove(env_path) + log_file = project_config['logs'] + if os.path.exists(log_file): + os.remove(log_file) + else: + return json_response(False, '项目类型错误') + + threading.Thread(target=remove_sites_service_config, args=(project_name, "java_")).start() + public.M('domain').where('pid=?', (project_data['id'],)).delete() + public.M('sites').where('name=?', (project_name,)).delete() + self.write_project_log('删除Java项目{}'.format(get.project_name)) + return json_response(True, '删除项目成功') + + def config_file_list(self, get): + """获取配置文件列表""" + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + project_name = project_data["name"] + res_list = [] + bind_extranet = int(project_data["project_config"]["bind_extranet"]) != 0 + if bind_extranet: + if public.get_webserver() == "nginx": + config_file = "{}/nginx/java_{}.conf".format(self._vhost_path, project_name) + res_list.append( + { + "name": "nginx配置文件", + "path": config_file, + "status": os.path.exists(config_file), + "type": "server" + } + ) + else: + config_file = "{}/apache/java_{}.conf".format(self._vhost_path, project_name) + res_list.append( + { + "name": "apache配置文件", + "path": config_file, + "status": os.path.exists(config_file), + "type": "server" + } + ) + rewrite_file = "{}/rewrite/java_{}.conf".format(self._vhost_path, project_name) + res_list.append( + { + "name": "伪静态配置文件", + "path": rewrite_file, + "status": os.path.exists(rewrite_file), + "type": "rewrite" + } + ) + + p_list = RealProxy("java_").get_proxy_list(public.to_dict_obj({"sitename": project_name})) + f, r_list = RealRedirect("java_").get_redirect_list(public.to_dict_obj({"sitename": project_name})) + for p in p_list: + res_list.append({ + "name": "反向代理【{}】".format(p["proxyname"]), + "path": p["proxy_conf_file"], + "status": os.path.exists(p["proxy_conf_file"]), + "type": "proxy" + }) + + if f: + for r in r_list: + res_list.append({ + "name": "重定向", + "path": r["redirect_conf_file"], + "status": os.path.exists(r["redirect_conf_file"]), + "type": "redirect" + }) + + if project_data["project_config"]["java_type"] == "neizhi": + tomcat = utils.bt_tomcat(project_data["project_config"]["tomcat_version"]) + res_list.append({ + "name": "Tomcat【{}】配置文件".format(project_data["project_config"]["tomcat_version"]), + "path": os.path.join(tomcat.path, "conf/server.xml"), + "status": tomcat.installed, + "type": "tomcat" + }) + elif project_data["project_config"]["java_type"] == "duli": + tomcat = utils.site_tomcat(project_data["name"]) + res_list.append({ + "name": "Tomcat配置文件", + "path": os.path.join(tomcat.path, "conf/server.xml"), + "status": tomcat.installed, + "type": "tomcat" + }) + else: + t = time.time() + pid = self.get_project_pid(project_data) + if not pid: + pid = 0 + spc = SpringConfigParser( + jar_path=project_data["project_config"]["project_jar"], + process=pid, + ) + used, _ = spc.app_config() + for i, _ in used: + if i == "命令行或环境变量": + continue + profile = os.path.basename(i)[len(spc.config_name):] # 取出 - + profile + 后缀部分 + if profile in (".yml", ".yaml", ".properties"): + profile = "" + else: + profile = profile.rsplit(".", 1)[0] + res_list.append({ + "name": "Spring配置" + profile, + "path": i, + "status": True, + "type": "local_spring" if i.startswith("/") else "spring", + "data": spc.raw_data.get(i, "") + }) + + sort_tuple = { + "local_spring": 10, + "spring": 9, + "tomcat": 8, + "server": 7, + "proxy": 6, + "redirect": 5, + "rewrite": 4, + + } + res_list.sort(key=lambda x: sort_tuple[x["type"]], reverse=True) + return json_response(True, data=res_list) + + # 未使用 + def project_log_list(self, get): + """获取日志文件列表""" + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + project_name = project_data["name"] + res = RealLogMgr(conf_prefix="java_").get_site_log_path(public.to_dict_obj({"sitename": project_name})) + res_list = [] + if isinstance(res, str): + res_list.extend([ + {"type": "access", "path": None, "log_size": 0, "msg": "无法从配置文件中获取日志文件路径"}, + {"type": "error", "path": None, "log_size": 0, "msg": "无法从配置文件中获取错误日志文件路径"} + ]) + else: + access_file = res["log_file"] + error_file = res["error_log_file"] + access_file_size = error_file_size = 0 + if os.path.isfile(access_file): + access_file_size = os.path.getsize(access_file) + + if os.path.isfile(error_file): + error_file_size = os.path.getsize(error_file) + + res_list.extend([ + {"type": "access", "path": access_file, "log_size": access_file_size, "msg": ""}, + {"type": "error", "path": error_file, "log_size": error_file_size, "msg": ""} + ]) + + project_config = project_data["project_config"] + if project_config["java_type"] == "springboot": + log_file = project_config['logs'] + if not os.path.isfile(log_file): + pass + + def get_command(self, get): + """获取命令, 包含设置和取消jmx的设置""" + project_cmd = None + jmx_status = None + try: + if hasattr(get, "project_cmd"): + project_cmd = get.project_cmd.strip() + if hasattr(get, "jmx_status"): + jmx_status = utils.js_value_to_bool(get.jmx_status) + project_jdk = utils.normalize_jdk_path(get.project_jdk.strip()) + project_jar = get.project_jar.strip() + except json.JSONDecodeError: + return json_response(False, '参数错误') + + if not isinstance(project_jdk, str): + return json_response(False, '项目JDK路径不存在') + + project_jdk = os.path.join(project_jdk, "bin/java") + if not os.path.isfile(project_jdk): + return json_response(False, '项目JDK不存在') + if not os.path.isfile(project_jar): + return json_response(False, '项目jar不存在') + + if not isinstance(project_cmd, str) and project_cmd: + cmd = '{} -jar {} -Xmx1024M -Xms256M'.format(project_jdk, project_jar) + cmd = self._build_jmx_cmd(cmd, jmx_status) + + else: + cmd = self._build_jmx_cmd(project_cmd, jmx_status) + + return json_response(True, data=cmd) + + def set_project_log_status(self, get): + """设置项目日志是否开启""" + try: + project_data = self.get_project_find(get.project_name.strip()) + status = utils.js_value_to_bool(get.status) + except: + return json_response(False, '参数错误') + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + + if project_data["project_config"]["java_type"] == "springboot": + project_config = project_data["project_config"] + project_config["nohup_log"] = status + project_config["change_flag"] = True + pdata = { + 'project_config': json.dumps(project_config) + } + public.M('sites').where('id=?', (project_data["id"],)).update(pdata) + + return json_response(True, "修改成功, 重启项目后生效") + else: + return json_response(False, "非springboot项目无法关闭") + + def get_jmx_status(self, get): + """设置项目日志是否开启""" + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + + project_config = project_data["project_config"] + if not project_config["java_type"] == "springboot": + return json_response(False, "目前支持springboot 项目的jmx 监控") + + pid = self.get_project_pid(project_data) + if not pid: + return json_response(False, "未启动的项目,不能获取jmx信息") + + jmx_info = self.get_jmx_data_by_pid(pid) + if not jmx_info: + return json_response(False, "项目未启用jmx,无法获取jmx信息") + jmx_url = 'service:jmx:rmi:///jndi/rmi://{}:{}/jmxrmi'.format(jmx_info["host"], jmx_info["port"]) + + from mod.project.java.jmxquery import JMXConnection, JMXQuery + try: + # 创建 JMXConnection + # JMX 连接信息 + jmxConnection = JMXConnection( + connection_uri=jmx_url, + java_path=os.path.join(project_config["project_jdk"], "bin/java") + ) + jmxQuery = [JMXQuery("*:*")] + # 执行查询 + metrics = jmxConnection.query(jmxQuery) + except Exception: + public.print_log(public.get_error_info()) + return json_response(False, "连接失败! {}".format(jmx_url)) + + # 创建 JMX 查询 + jmx_status_info = {} + type_list = ["MemoryPool", "GarbageCollector"] + percent_list = ["SystemCpuLoad", "ProcessCpuLoad"] + microsecond_list = [ + "StartTime", "Uptime", "endTime", "startTime", "CollectionTime", "CurrentThreadCpuTime", + "CurrentThreadUserTime", "endTime", "startTime", "CollectionTime", "TotalCompilationTime", + ] + nanoseconds_list = ["ProcessCpuTime", ] + size_list = ["FreePhysicalMemorySize", "TotalPhysicalMemorySize", "committed", "init", "max", "used"] + value_dict = { + "True": "是", + "False": "否", + "None": "无", + } + + # 解析结果 + for metric in metrics: + java_type_obj = re.search(r"type=([\w\s]+)", metric.mBeanName) + if not java_type_obj: + continue + java_type = java_type_obj.group(1) + name_obj = re.search(r"name=([\w\s]+)", metric.mBeanName) + name = None + if name_obj: + name = name_obj.group(1) + + if jmx_status_info.get(java_type) is None: + if java_type in type_list: + jmx_status_info[java_type] = [] + else: + jmx_status_info[java_type] = {} + type_info: Union[Dict[str, Any], List[Dict[str, Any]]] = jmx_status_info[java_type] + + if name is not None: + name = name.replace(" ", "_") + if isinstance(type_info, list): + for info in type_info: + if info["name"] == name: + type_info = info + break + else: + info = {"name": name} + type_info.append(info) + type_info = info + else: + if type_info.get(name) is None: + type_info[name] = {} + type_info = type_info[name] + + value = value_dict.get(str(metric.value)) + if value is None: + value = metric.value + + if metric.attributeKey: + if metric.attribute is not None and type_info.get(metric.attribute) is None: + type_info[metric.attribute] = {} + + if value == -1: + value = "无限制" + elif metric.attributeKey in size_list: + value = public.to_size(value) + elif metric.attributeKey in microsecond_list: + value = datetime.fromtimestamp(int(value) / 1000).strftime('%Y-%m-%d %H:%M:%S') + elif metric.attributeKey in nanoseconds_list: + value = "{} 秒".format(int(value) / 1e9) + + type_info[metric.attribute][metric.attributeKey] = value + else: + if metric.attribute in size_list: + value = public.to_size(value) + elif metric.attribute in percent_list: + value = "{}%".format(round(int(value) * 100, 2)) + elif metric.attribute in microsecond_list: + value = datetime.fromtimestamp(int(value) / 1000).strftime('%Y-%m-%d %H:%M:%S') + elif metric.attribute in nanoseconds_list: + value = "{} 秒".format(int(value) / 1e9) + type_info[metric.attribute] = value + + return json_response(True, data=jmx_status_info) + + @staticmethod + def get_jmx_data_by_pid(pid) -> Optional[dict]: + try: + p = psutil.Process(pid) + cmd_line = p.cmdline() + except: + return None + + data = { + "port": "", + "host": "127.0.0.1" + } + for i in cmd_line: + if i.startswith("-Dcom.sun.management.jmxremote.port"): + data["port"] = i.split("=")[1] + + if i.startswith("-Djava.rmi.server.hostname"): + data["host"] = i.split("=")[1] + if not data["port"]: + return None + return data + + def get_project_info(self, get): + """设置项目日志是否开启""" + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.project_name)) + + project_data = self.get_project_stat(project_data) + return json_response(True, data=project_data) + + # 上传版本 + def upload_version(self, get): + """ + 上传压缩包并存储为版本 + """ + if not hasattr(get, 'sitename'): + return json_response(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return json_response(False, '版本号不能为空') + if not hasattr(get, 'ps'): + get.ps = '' + try: + upload_files = os.path.join("/tmp", get.f_name) + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + from files import files + + file_obj = files() + ff = file_obj.upload(get) + if type(ff) == int: + return ff + if not ff['status']: + return json_response(False, ff['msg']) + + output_dir = str(os.path.join('/tmp', public.GetRandomString(16))) + os.makedirs(output_dir, 777) + is_jar_war = os.path.splitext(upload_files)[-1] in (".jar", ".war") + if not self.extract_archive(upload_files, output_dir)[0] and not is_jar_war: + return json_response(False, '解压失败,仅支持zip,tar.gz,tar,bz2,gz格式的压缩包') + + version_tool = VersionTool() + if not is_jar_war: + if len(os.listdir(output_dir)) == 1: + real_output_dir = str(os.path.join(output_dir, os.listdir(output_dir)[0])) + if os.path.isdir(real_output_dir): + output_dir = real_output_dir + + res = version_tool.publish_by_src_path(get.sitename, output_dir, get.version, get.ps, sync=True) + public.ExecShell('rm -rf {}'.format(output_dir)) + public.ExecShell('rm -rf {}'.format(upload_files)) + else: + res = version_tool.publish_by_file(get.sitename, upload_files, get.version, get.ps) + public.ExecShell('rm -rf {}'.format(upload_files)) + if res is None: + return public.returnResult(True, '添加成功') + return json_response(False, '添加失败' + res) + except: + return json_response(False, traceback.format_exc()) + + @staticmethod + def extract_archive(file_path, output_dir): + try: + import tarfile + import zipfile + import gzip + import bz2 + + name = os.path.basename(file_path) + if name.endswith('.tar.gz'): + with tarfile.open(file_path, 'r:gz') as tar: + tar.extractall(output_dir) + elif name.endswith('.zip'): + with zipfile.ZipFile(file_path, 'r') as zip_ref: + zip_ref.extractall(output_dir) + elif name.endswith('.gz'): + with gzip.open(file_path, 'rb') as f_in, open(os.path.join(output_dir, name[:-3]), 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + elif name.endswith('.tar'): + with tarfile.open(file_path, 'r') as tar: + tar.extractall(output_dir) + elif name.endswith('.bz2'): + with open(file_path, 'rb') as f_in, open(os.path.join(output_dir, name[:-4]), 'wb') as f_out: + with bz2.BZ2File(f_in) as bz: + shutil.copyfileobj(bz, f_out) + else: + return False, '文件格式错误.' + except: + return False, '解压失败' + + return True, '解压成功' + + # 获取列表 + @staticmethod + def get_version_list(get): + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + version_tool = VersionTool() + return json_response(True, data=version_tool.version_list(get.sitename)) + + # 删除版本 + @staticmethod + def remove_version(get): + if not hasattr(get, 'sitename'): + return json_response(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return json_response(False, '版本号不能为空') + version_tool = VersionTool() + if version_tool.remove(get.sitename, get.version) is None: + return public.returnResult(True, '删除成功') + return json_response(False, '删除失败') + + # 恢复版本 + def recover_version(self, get): + try: + if not hasattr(get, 'sitename'): + return json_response(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return json_response(False, '版本号不能为空') + + project_data = self.get_project_find(get.sitename) + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.sitename)) + + version_tool = VersionTool() + project_config = project_data['project_config'] + # spring jar 包情况 + if project_config["java_type"] == "springboot": + v_info = version_tool.get_version_info(get.sitename, get.version) + if not v_info: + return json_response(False, '指定版本不存在: {}'.format(get.version)) + + path = os.path.join(version_tool.pack_path, v_info['zip_name']) + if not os.path.isfile: + return json_response(False, '指定版本文件丢失') + if os.path.exists(project_config["project_jar"] + "_back"): + os.remove(project_config["project_jar"] + "_back") + if not path.endswith(".jar"): # 如果不是jar包,则解压并寻找jar包 + output_dir = str(os.path.join('/tmp', public.GetRandomString(16))) + os.makedirs(output_dir, 777) + if not self.extract_archive(path, output_dir)[0]: + return json_response(False, '解压失败') + else: + for f in os.listdir(output_dir): + if f.endswith(".jar"): + path = os.path.join(output_dir, f) + break + else: + return json_response(False, '未找到jar包文件') + + shutil.move(project_config["project_jar"], project_config["project_jar"] + "_back") + shutil.copyfile(path, project_config["project_jar"]) + + return json_response(True, "版本文件替换成功,重启后生效") + # war 包 + v_info = version_tool.get_version_info(get.sitename, get.version) + if not v_info: + return json_response(False, '指定版本不存在: {}'.format(get.version)) + path = os.path.join(version_tool.pack_path, v_info['zip_name']) + if path.endswith(".war") and os.path.isfile(project_data["path"]): # 如果是war包 + if os.path.exists(project_data["path"] + "_back"): + os.remove(project_data["path"] + "_back") + shutil.move(project_data["path"], project_data["path"] + "_back") + shutil.copyfile(path, project_data["path"]) + return json_response(True, "版本文件替换成功,重启后生效") + + res = version_tool.recover(get.sitename, get.version, project_data['path']) + if res is not True: + return json_response(False, res) + return json_response(True, '恢复成功') + except: + return json_response(False, traceback.format_exc()) + + def now_file_backup(self, get): + try: + if not hasattr(get, 'sitename'): + return json_response(False, '项目名称不能为空') + if not hasattr(get, 'version') or not get.version.strip(): + return json_response(False, '版本号不能为空') + else: + get.version = get.version.strip() + if not hasattr(get, 'ps'): + get.ps = '' + project_data = self.get_project_find(get.sitename) + if not project_data: + return json_response(False, '指定项目不存在: {}'.format(get.sitename)) + path = project_data['path'] + version_tool = VersionTool() + project_config = project_data['project_config'] + if project_config["java_type"] == "springboot": + res = version_tool.publish_by_file(get.sitename, project_config["project_jar"], get.version, get.ps) + else: + if os.path.isdir(path): + res = version_tool.publish_by_src_path(get.sitename, path, get.version, get.ps, sync=True) + elif os.path.isfile(path): + res = version_tool.publish_by_file(get.sitename, path, get.version, get.ps) + else: + return json_response(False, '添加失败') + if res is None: + return json_response(True, '添加成功') + return json_response(False, '添加失败' + res) + except: + return json_response(False, traceback.format_exc()) + + @staticmethod + def set_version_ps(get): + if not hasattr(get, 'sitename'): + return json_response(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return json_response(False, '版本号不能为空') + if not hasattr(get, 'ps'): + get.ps = '' + version_tool = VersionTool() + res = version_tool.set_ps(get.sitename, get.version, get.ps) + if not res: + return json_response(False, '设置失败') + return json_response(True, '设置成功') + + def change_log_path(self, get): + """"修改日志文件地址 + @author baozi <202-03-13> + @param: + get ( dict ): 请求: 包含项目名称和新的路径 + @return + """ + try: + project_data = self.get_project_find(get.project_name.strip()) + new_log_path = get.path.strip().rstrip("/") + except: + return json_response(False, '参数错误') + if not project_data: + return json_response(False, '项目不存在') + if not new_log_path.startswith('/'): + return json_response(False, '路径格式错误') + if not os.path.exists(new_log_path): + os.makedirs(new_log_path, mode=0o777) + + project_config = project_data["project_config"] + if project_config['java_type'] == 'springboot': + project_config['logs'] = new_log_path + '/' + project_data["name"] + '.log' + pdata = { + 'name': project_data["name"], + 'project_config': json.dumps(project_config) + } + public.M('sites').where('id=?', (project_data["id"],)).update(pdata) + # 重启项目 + res = self.restart_project(get) + self.write_project_log('修改Java项目{}日志路径为:{}'.format(get.project_name, new_log_path)) + return json_response(True, "项目日志路径修改成功") + elif project_config['java_type'] == 'duli': + project_config['logs'] = new_log_path + '/' + tomcat = utils.site_tomcat(project_data["name"]) + if not tomcat.change_log_path(new_log_path, project_data["name"].replace(".", "_")): + return public.returnMsg(False, "项目日志路径修改失败") + pdata = { + 'name': project_data["name"], + 'project_config': json.dumps(project_config) + } + public.M('sites').where('name=?', (get.project_name.strip(),)).update(pdata) + # 重启项目 + tomcat.restart() + self.write_project_log('修改Java项目{}日志路径为:{}'.format(get.project_name, new_log_path)) + return json_response(True, "项目日志路径修改成功") + + elif project_config['java_type'] == 'neizhi': + tomcat = utils.bt_tomcat(project_config['tomcat_version']) + if not tomcat.change_log_path(new_log_path, str(project_config['tomcat_version'])): + return public.returnMsg(False, "项目日志路径修改失败") + + tomcat.restart() + self.write_project_log('修改Java项目{}日志路径为:{}'.format(get.project_name, new_log_path)) + return json_response(True, "项目日志路径修改成功") + else: + return json_response(False, "项目类型错误") + + def multi_remove_project(self, get): + """ + @name 批量删除项目 + @author baozi<2023-3-2> + @param get{ + project_names: list[string] <项目名称>所组成的列表 + } + @return dict + """ + try: + project_names = get.project_names + if isinstance(project_names, list): + project_names = [i.strip() for i in project_names] + else: + project_names = [] + except: + return json_response(False, "参数错误") + if not project_names: + return json_response(False, "未选中要删除的站点") + + projects = public.M('sites').where( + 'project_type=? AND name in ({})'.format(",".join(["?"] * len(project_names))), + ('Java', *project_names)).select() + + if not projects: + return public.returnMsg(False, "未选中要删除的站点") + + _duli, _neizh, _springboot = [], [], [] + for project in projects: + project['project_config'] = json.loads(project['project_config']) + if project['project_config']['java_type'] == 'duli': + _duli.append(project) + elif project['project_config']['java_type'] == 'neizhi': + _neizh.append(project) + elif project['project_config']['java_type'] == 'springboot': + _springboot.append(project) + + # 执行每种删除的独特操作 + if _duli: + self._multi_remove_duli(_duli) + if _neizh: + self._multi_remove_neizhi(_neizh) + if _springboot: + self._multi_remove_springboot(_springboot) + + # 清除Nginx, Apache 配置文件,并重起服务 + for i in projects: + remove_sites_service_config(i["name"], config_prefix="java_") + + # 从面板数据库删除信息 + project_ids = tuple([i["id"] for i in projects]) + public.M('domain').where('pid IN ({})'.format(",".join(["?"] * len(projects))), project_ids).delete() + public.M('sites').where('id IN ({})'.format(",".join(["?"] * len(project_ids))), project_ids).delete() + self.write_project_log('批量删除Java项目:[{}]'.format("; ".join([i["name"] for i in projects]))) + + for project in projects: + self.del_crontab(project["name"]) + + return json_response(True, "删除项目成功", data=[i["name"] for i in projects]) + + @staticmethod + def _multi_remove_duli(projects): + for project in projects: + # 关闭独立项目 + tomcat = utils.site_tomcat(project["name"]) + if tomcat: + tomcat.stop() + if os.path.exists(tomcat.path): + shutil.rmtree(tomcat.path) + + @staticmethod + def _multi_remove_neizhi(projects): + used_tomcat: Dict[int, utils.TomCat] = {} + for project in projects: + ver = int(project['project_config']['tomcat_version']) + if ver in used_tomcat: + tomcat = used_tomcat[ver] + else: + tomcat = utils.bt_tomcat(ver) + if not tomcat: + continue + used_tomcat[ver] = tomcat + + tomcat.remove_host(project["name"]) + + for t in used_tomcat.values(): + t.save_config_xml() + t.restart() + + def _multi_remove_springboot(self, projects): + for project in projects: + # 停止项目 + server_name = "spring_" + project["name"] + project.get("server_name_suffix", "") + s_admin = RealServer() + s_admin.daemon_admin(server_name, "stop") + s_admin.del_daemon(server_name) + + project_config = project["project_config"] + pid_file = project_config['pids'] + if os.path.exists(pid_file): + os.remove(pid_file) + script_file = project_config['scripts'] + if os.path.exists(script_file): + os.remove(script_file) + env_path = "{}/env/{}.env".format(self._java_project_vhost, project_config["project_name"]) + if os.path.exists(env_path): + os.remove(env_path) + log_file = project_config['logs'] + if os.path.exists(log_file): + os.remove(log_file) + + def multi_set_project(self, get): + """ + @name 批量设置项目 + @author baozi<2023-3-2> + @param get{ + project_names: list[string] <项目名称>所组成的列表 + } + @return dict + """ + try: + project_names = get.project_names + set_type = get.operation.strip() + except: + return json_response(False, "参数错误") + if set_type not in ["start", "stop"]: + return public.returnMsg(False, "操作信息错误") + if isinstance(project_names, list): + project_names = [i.strip() for i in project_names] + else: + project_names = [] + + projects = public.M('sites').where( + 'project_type=? AND name in ({})'.format(",".join(["?"] * len(project_names))), + ('Java', *project_names) + ).select() + + if not projects: + return json_response(False, "未选中要启动的站点") + + project_names = [i["name"] for i in projects] + spring_boot_projects = [] + duli_tomcat = [] + bt_tomcat = {} + error_list = [] + for project in projects: + project['project_config'] = json.loads(project['project_config']) + if project['project_config']['java_type'] == 'neizhi': + ver = int(project['project_config']['tomcat_version']) + if ver not in bt_tomcat: + tomcat = utils.bt_tomcat(project['project_config']['tomcat_version']) + if not tomcat: + error_list.append({"project_name": project["name"], "msg": "启动失败,没有安装Tomcat{}".format( + project['project_config']['tomcat_version'])}) + project_names.remove(project["name"]) + continue + bt_tomcat[ver] = tomcat + + if project['project_config']['java_type'] == 'duli': + tomcat = utils.site_tomcat(project["name"]) + if tomcat: + duli_tomcat.append(tomcat) + + if project['project_config']['java_type'] == 'springboot': + spring_boot_projects.append(project) + + for t in itertools.chain(bt_tomcat.values(), duli_tomcat): + if set_type == "start": + t.start() + else: + t.stop() + + for i in spring_boot_projects: + if set_type == "start": + self.start_project(public.to_dict_obj({"project_name": i["name"]})) + else: + self.stop_project(public.to_dict_obj({"project_name": i["name"]})) + + if error_list: + return json_response(True, msg="部分项目操作失败", data={ + "error_list": error_list, + "project_names": project_names + }) + return json_response(True, msg="启动成功" if set_type == "start" else "停止成功", data={ + "project_names": project_names + }) + + @staticmethod + def del_crontab(project_name: str): + """ + @name 删除项目日志切割任务 + @auther hezhihong<2022-10-31> + @return + """ + cron_name = '[勿删]Java项目[{}]运行日志切割'.format(project_name) + cron_path = public.GetConfigValue('setup_path') + '/cron/' + cron_list = public.M('crontab').where("name=?", (cron_name,)).select() + if cron_list: + for i in cron_list: + if not i: continue + cron_echo = public.M('crontab').where("id=?", (i['id'],)).getField('echo') + args = {"id": i['id']} + import crontab + crontab.crontab().DelCrontab(args) + del_cron_file = cron_path + cron_echo + public.ExecShell("crontab -u root -l| grep -v '{}'|crontab -u root -".format(del_cron_file)) + + def get_load_info(self, get): + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not project_data: + return json_response(False, "未找到项目") + + pid = self.get_project_pid(project_data) + if not pid: + return json_response(False, "项目未启动") + + res = self.real_process.get_process_tree(pid) + if isinstance(res["data"], list): + res["data"] = {i.get("pid", "0"): i for i in res["data"]} + return res + + def get_port_status(self, get): + try: + project_data = self.get_project_find(get.project_name.strip()) + except: + return json_response(False, '参数错误') + + if not project_data: + return json_response(False, "未找到项目") + + pid = self.get_project_pid(project_data) + if not pid: + return json_response(False, "项目未启动") + + ports = [] + try: + p = psutil.Process(pid) + for i in p.connections(): + if i.status == "LISTEN" and i.laddr.port not in ports: + ports.append(str(i.laddr.port)) + except: + pass + + if not ports: + return json_response(False, "未找到端口") + + res = {str(i): { + "port": i, + "fire_wall": None, + "nginx_proxy": None, + } for i in ports} + + from firewallModel.comModel import main + port_list = main().port_rules_list(get) + for i in port_list: + if str(i["Port"]) in res: + res[str(i["Port"])]['fire_wall'] = i + + rsp = RealServerProxy(project_data) + proxy_list = rsp.get_proxy_list() + for i in proxy_list: + if str(i["proxy_port"]) in res: + res[str(i["proxy_port"])]['nginx_proxy'] = i + + return json_response(True, "获取成功", data=list(res.values())) + + def add_server_proxy(self, get): + if not hasattr(get, "site_name") or not get.site_name.strip(): + return json_response(status=False, msg="参数错误") + + project_data = self.get_project_find(get.site_name) + if not project_data: + return json_response(False, "未找到项目") + + rp = RealServerProxy(project_data) + proxy_data = rp.check_args(get, is_modify=False) + if isinstance(proxy_data, str): + return json_response(status=False, msg=proxy_data) + + # 使用正则匹配尝试添加 + res = rp.create_proxy(proxy_data) + if res is None: + return json_response(status=True, msg="添加成功") + + # 尝试模板生成 + proxy_info = project_data['project_config'].get("proxy_info", []) + proxy_info.append(proxy_data) + project_data['project_config']['proxy_info'] = proxy_info + domain_list = self._project_domain_list(project_data['id']) + domains = [(i["name"], str(i["port"])) for i in domain_list] + + ssl, f_ssl = self._get_ssl_status(project_data['name']) + error_msg = self.create_config(project_data, domains, ssl, f_ssl) + if not error_msg: + public.serviceReload() + pdata = { + 'project_config': json.dumps(project_data['project_config']) + } + public.M("sites").where("id=?", (project_data['id'],)).update(pdata) + return json_response(status=True, msg="添加成功") + + return json_response(status=False, msg=error_msg) + + def modify_server_proxy(self, get): + if not hasattr(get, "site_name") or not get.site_name.strip(): + return json_response(status=False, msg="参数错误") + + project_data = self.get_project_find(get.site_name) + if not project_data: + return json_response(False, "未找到项目") + + rp = RealServerProxy(project_data) + proxy_data = rp.check_args(get, is_modify=True) + if isinstance(proxy_data, str): + return json_response(status=False, msg=proxy_data) + + proxy_info = project_data['project_config'].get("proxy_info", []) + idx = None + for index, i in enumerate(proxy_info): + if i["proxy_id"] == proxy_data["proxy_id"] and i["site_name"] == proxy_data["site_name"]: + idx = index + break + + if idx is None: + return json_response(status=False, msg="未找到该id的反向代理配置") + + # 使用正则匹配尝试添加 + res = rp.modify_proxy(proxy_data) + if res is None: + return json_response(status=True, msg="修改成功") + + # 尝试模板生成 + proxy_info[idx] = proxy_data + project_data['project_config']['proxy_info'] = proxy_info + domain_list = self._project_domain_list(project_data['id']) + domains = [(i["name"], str(i["port"])) for i in domain_list] + ssl, f_ssl = self._get_ssl_status(project_data['name']) + error_msg = self.create_config(proxy_data, domains, ssl, f_ssl) + if not error_msg: + public.serviceReload() + pdata = { + 'project_config': json.dumps(project_data['project_config']) + } + public.M("sites").where("id=?", (project_data['id'],)).update(pdata) + return json_response(status=True, msg="修改成功") + + return json_response(status=False, msg=error_msg) + + def remove_server_proxy(self, get): + try: + site_name = get.site_name.strip() + proxy_id = get.proxy_id.strip() + except: + return json_response(status=False, msg="参数错误") + project_data = self.get_project_find(site_name) + if not project_data: + return json_response(False, "未找到项目") + + rp = RealServerProxy(project_data) + msg = rp.remove_proxy(site_name, proxy_id) + if msg: + return json_response(status=False, msg=msg) + return json_response(status=True, msg="删除成功") + + def server_proxy_list(self, get): + try: + site_name = get.site_name.strip() + except: + return json_response(status=False, msg="参数错误") + + project_data = self.get_project_find(site_name) + if not project_data: + return json_response(False, "未找到项目") + + _p = RealServerProxy(project_data) + data = _p.get_proxy_list() + return json_response(status=True, data=data) + + @staticmethod + def check_env_for_project(get): + project_cmd = "" + by_process = 0 + env_list = [] + env_file = '' + try: + if hasattr(get, "project_cmd") and get.project_cmd: + project_cmd = get.project_cmd.strip() + project_jar = get.project_jar.strip() + if hasattr(get, "by_process") and get.by_process: + by_process = int(get.by_process) + if hasattr(get, "env_list") and get.env_list: + env_list = get.env_list + if hasattr(get, "env_file") and get.env_file: + env_file = get.env_file + except: + return json_response(status=False, msg="参数错误") + if not os.path.exists(project_jar): + return json_response(status=False, msg="jar文件不存在") + if env_file and not os.path.exists(env_file): + return json_response(status=False, msg="环境变量文件不存在") + + spring_parser = SpringConfigParser( + jar_path=project_jar, + process=by_process, + cmd=project_cmd, + env_list=env_list, + env_file=env_file, + ) + data = spring_parser.get_tip() + if not data: + return json_response(status=True, msg="未检测到配置问题", data=data) + + return json_response(status=True, data=data) + + def set_static_path(self, get): + try: + project_name = get.project_name.strip() + project_data = self.get_project_find(project_name) + status = utils.js_value_to_bool(get.status) + index = get.index.strip() + path = get.path.strip() + except: + return json_response(status=False, msg="参数错误") + + if not project_data: + return json_response(False, "未找到项目") + + if not project_data["project_config"]["java_type"] == "springboot": + return json_response(status=False, msg="非springboot项目无法设置静态文件") + + proxy_info = project_data['project_config'].get("proxy_info", []) + for i in proxy_info: + if i["proxy_dir"] == "/": + return json_response(status=False, msg="项目已存在根路由【/】配置,无法设置态文件配置") + + project_data["project_config"]["static_info"] = { + "status": status, + "index": index, + "path": path, + "use_try_file": True, + } + + res = self._set_static_path(project_data) + if isinstance(res, str): + return json_response(status=False, msg=res) + + public.M("sites").where("id=?", (project_data["id"],)).update({ + "project_config": json.dumps(project_data["project_config"]) + }) + + return json_response(status=True, msg="设置成功") + + def get_keep_status(self, get): + try: + project_name = get.project_name.strip() + project_data = self.get_project_find(project_name) + except: + return json_response(status=False, msg="参数错误") + from mod.project.java.project_update import ProjectUpdate + + if not project_data: + return json_response(False, "未找到项目") + + if not project_data["project_config"]["java_type"] == "springboot": + return json_response(status=False, msg="非springboot项目不支持该功能") + + p = ProjectUpdate(project_name, project_data["project_config"]["project_jar"]) + res = p.get_keep_status() + return res + + def update_project_by_restart(self, get): + try: + project_name = get.project_name.strip() + project_jar = get.project_jar.strip() + project_data = self.get_project_find(project_name) + except: + return json_response(status=False, msg="参数错误") + + if not project_data: + return json_response(False, "未找到项目") + + if not project_data["project_config"]["java_type"] == "springboot": + return json_response(status=False, msg="非springboot项目不支持该功能") + + from mod.project.java.project_update import ProjectUpdate + + p = ProjectUpdate(project_name, new_jar=project_jar) + res = p.restart_update() + return res + + def update_project_by_keep(self, get): + now_port = 0 + run_time = 0 + try: + project_name = get.project_name.strip() + project_jar = get.project_jar.strip() + if hasattr(get, "now_port") and get.now_port: + now_port = int(get.now_port) + if hasattr(get, "run_time") and get.run_time: + run_time = int(get.run_time) + project_data = self.get_project_find(project_name) + except: + return json_response(status=False, msg="参数错误") + + if not project_data: + return json_response(False, "未找到项目") + + if not project_data["project_config"]["java_type"] == "springboot": + return json_response(status=False, msg="非springboot项目不支持该功能") + + if not os.path.isfile(project_jar): + return json_response(status=False, msg="jar文件不存在") + + if public.get_webserver() != "nginx": + return json_response(status=False, msg="当前只支持nginx使用") + + ng_file = "/www/server/panel/vhost/nginx/java_{}.conf".format(project_name) + if not os.path.exists(ng_file): + return json_response(status=False, msg="未启用外网访问的不能进行不停机更新") + + panel_path = "/www/server/panel" + pid_file = "{}/keep/{}.pid".format(self._java_project_path, project_name) + public.ExecShell( + "nohup {}/pyenv/bin/python3 {}/mod/project/java/project_update.py {} {} {} {} &> /dev/null & \n" + "echo $! > {} ".format( + panel_path, panel_path, project_name, project_jar, now_port, run_time, pid_file) + ) + return json_response(status=True, msg="更新任务已开始") + + def force_stop(self, get): + try: + project_name = get.project_name.strip() + project_data = self.get_project_find(project_name) + except: + return json_response(status=False, msg="参数错误") + + if not project_data: + return json_response(False, "未找到项目") + + project_config = project_data["project_config"] + pid_file = "{}/keep/{}.pid".format(self._java_project_path, project_name) + pid_data = public.readFile(pid_file) + if isinstance(pid_data, str) and pid_data != "0": + try: + p = psutil.Process(int(pid_data)) + p.kill() + except: + pass + + service_list = [] + rep_service = re.compile(r"^spring_%s_\S{8}$" % public.prevent_re_key(project_name), re.M) + for i in os.scandir("/usr/lib/systemd/system"): + if i.is_file() and rep_service.match(i.name): + service_list.append(i.name) + + now_name = "spring_" + project_config["project_name"] + "_" + project_config.get("server_name_suffix", "") + if now_name in service_list: + service_list.remove(now_name) + + for i in service_list: + public.ExecShell("systemctl stop {}".format(i)) + os.remove("/usr/lib/systemd/system/{}".format(i)) + + public.ExecShell("systemctl daemon-reload") + upstream_file = "/www/server/panel/vhost/nginx/java_{}_upstream.conf".format(project_name) + if os.path.isfile(upstream_file): # 说明新旧同时存在 则删除旧的 + upstream_data = public.readFile(upstream_file) + if isinstance(upstream_data, str): + old_upstream_data = re.search(r"server 127\.0\.0\.1:(?P\d+);", upstream_data) + if old_upstream_data: + old_port = old_upstream_data.group("port") + ng_file = "/www/server/panel/vhost/nginx/java_{}.conf".format(project_name) + ng_data = public.readFile(ng_file) + if not isinstance(ng_data, str): + return "Nginx配置文件读取错误,无法取消轮询,使用新实例" + new_config = ng_data.replace("{}_backend".format(project_name), "127.0.0.1:{}".format(old_port)) + + public.writeFile(ng_file, new_config) + res = public.checkWebConfig() + if res is not True: + public.writeFile(ng_file, ng_data) + else: + os.remove(upstream_file) + public.serviceReload() + + return json_response(status=True, msg="强制停止成功") + + def keep_option(self, get): + try: + project_name = get.project_name.strip() + option = get.option.strip() + project_data = self.get_project_find(project_name) + except: + return json_response(status=False, msg="参数错误") + + if not project_data: + return json_response(False, "未找到项目") + + if option not in ("use_new", "use_old", "stop_new"): + return json_response(status=False, msg="参数错误") + + if not project_data["project_config"]["java_type"] == "springboot": + return json_response(status=False, msg="非springboot项目不支持该功能") + + from mod.project.java.project_update import ProjectUpdate + + p = ProjectUpdate(project_name, project_data["project_config"]["project_jar"]) + res = p.keep_option(option) + return res + + def get_spring_log_list(self, get): + try: + project_name = get.project_name.strip() + project_data = self.get_project_find(project_name) + except: + return json_response(status=False, msg="参数错误") + + if not project_data: + return json_response(False, "未找到项目") + + if not project_data["project_config"]["java_type"] == "springboot": + return json_response(status=False, msg="非springboot项目不支持该功能") + + pid = self.get_project_pid(project_data) + project_config = project_data["project_config"] + project_jar = project_config["project_jar"] + project_cmd = project_config["project_cmd"] + env_list = project_config["env_list"] + env_file = project_config["env_file"] + + spring_log_parser = SpringLogConfigParser( + jar_path=project_jar, + process=pid, + cmd=project_cmd, + env_list=env_list, + env_file=env_file, + ) + res = [] + for i in spring_log_parser.get_all_log_ptah(): + for j in os.scandir(i): + if j.is_file() and j.name.endswith(".log"): + log_path = os.path.join(i, j.name) + res.append(log_path) + + return json_response(status=True, data=res) + + @staticmethod + def get_spring_log_data(get): + try: + log_file = get.log_file.strip() + except: + return json_response(status=False, msg="参数错误") + + if not os.path.isfile(log_file): + return json_response(status=False, msg="日志文件不存在") + + return json_response(status=True, data=public.GetNumLines(log_file, 1000)) + + @staticmethod + def install_jdk_new(get): + if not hasattr(get, 'version') or not get.version.strip(): + return json_response(False, '版本号不能为空') + version = get.version.strip() + if os.path.exists('/www/server/java/' + version): + return json_response(False, '版本已经存在') + jdk_manager = utils.JDKManager() + if version not in jdk_manager.versions_list: + return public.returnMsg(False, '版本号不存在') + + jdk_manager.async_install_jdk(version) + + return json_response(True, '已添加到安装任务,请在消息盒子中查看安装情况') + + @staticmethod + def install_tomcat_new(get): + java_path = None + if not hasattr(get, 'version') or not get.version.strip(): + return json_response(False, '版本号不能为空') + version = get.version.strip() + if hasattr(get, 'java_path') and get.java_path.strip(): + java_path = get.java_path.strip() + + res = utils.TomCat.async_install_tomcat_new(version, java_path) + if res is not None: + return json_response(False, res) + + return json_response(True, '已添加到安装任务,请在消息盒子中查看安装情况') \ No newline at end of file diff --git a/mod/project/java/project_update.py b/mod/project/java/project_update.py new file mode 100644 index 00000000..957b549a --- /dev/null +++ b/mod/project/java/project_update.py @@ -0,0 +1,600 @@ +import copy +import os +import re +import sys +import json +import socket +import time +import traceback + +import psutil +import errno + +from typing import Optional, List +from threading import Thread +from urllib3.util import parse_url, Url + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +import public + + +from mod.base import RealServer +from mod.base import json_response +from mod.project.java.projectMod import main as java_mod +from mod.project.java import utils + + +class ProjectUpdate: + + def __init__(self, project_name: str, new_jar: str, new_port: int = None, run_time: int = None): + self.project_name = project_name + self.new_jar = new_jar + self.j_project = java_mod() + self.keep_path = self.j_project._java_project_path + "/keep" + + if not os.path.exists(self.keep_path): + os.makedirs(self.keep_path, 0o755) + + self.keep_log = "{}/{}.log".format(self.keep_path, self.project_name) + + # 不停机更新时使用 + self.new_port = new_port + self.run_time = run_time + self.keep_status = [] + self.new_project_config = None + self.old_project_config = None + + self.old_pro: Optional[psutil.Process] = None + self.new_pro: Optional[psutil.Process] = None + + self.end = False + self.old_project_data = None + self.proxy_data = { + "scheme": "http" + } + + @staticmethod + def new_suffix() -> str: + import uuid + return "_" + uuid.uuid4().hex[::4] + + def start_spring_project(self, project_data: dict, write_systemd_file=True, need_wait=True, ) -> dict: + return self.j_project._start_spring_boot_project(project_data, write_systemd_file, need_wait) + + def restart_update(self) -> dict: + project_data = self.j_project.get_project_find(self.project_name) + if not project_data: + return json_response(False, msg="项目不存在") + + project_config = project_data['project_config'] + old_jar = project_config['project_jar'] + if self.new_jar != old_jar: + if not os.path.isfile(self.new_jar): + return json_response(False, msg="项目jar包不存在") + + project_config['jar_path'] = os.path.dirname(self.new_jar) + project_config['project_jar'] = self.new_jar + old_jar_name = os.path.basename(old_jar) + project_cmd_list = project_config['project_cmd'].split(" ") + for i in range(len(project_cmd_list)): + if old_jar_name in project_cmd_list[i]: + project_cmd_list[i] = self.new_jar + break + + new_project_cmd = " ".join(project_cmd_list) + project_config['project_cmd'] = new_project_cmd + project_config["change_flag"] = True + + s_admin = RealServer() + server_name = "spring_" + project_config["project_name"] + project_config.get("server_name_suffix", "") + if s_admin.daemon_status(server_name)["msg"] == "服务不存在!": + self.j_project.stop_by_kill_pid(project_data) + if os.path.isfile(project_config["pids"]): + os.remove(project_config["pids"]) + return self.start_spring_project(project_data, write_systemd_file=True, need_wait=False) + + if "change_flag" in project_config and project_config.get("change_flag", False): + del project_config["change_flag"] + s_admin.daemon_admin(server_name, "stop") + s_admin.del_daemon(server_name) + self.j_project.stop_by_kill_pid(project_data) + if os.path.isfile(project_config["pids"]): + os.remove(project_config["pids"]) + + public.M("sites").where("id=?", (project_data["id"],)).update( + {"project_config": json.dumps(project_config)} + ) + return self.start_spring_project(project_data, write_systemd_file=True) + else: + return self.start_spring_project(project_data, write_systemd_file=False) + + # 实际执行启动的线程 + def run_task(self): + print("___________开始________________") + try: + res = self.start_new() + self.keep_status[0]["status"] = 1 + if res: + self.keep_status[0]["msg"] = res + return + else: + self.keep_status[0]["msg"] = "新实例已启动,新实例pid:{}".format(self.new_pro.pid) + res = self.set_nginx_upstream() + self.keep_status[1]["status"] = 1 + if res: + self.stop_new() + self.keep_status[1]["msg"] = res + return + else: + self.keep_status[1]["msg"] = "Nginx已配置完成轮询设置,您可以访问新实例了" + res = self.wait_time() + self.keep_status[2]["status"] = 1 + if res: + self.keep_status[2]["msg"] = res + return + else: + self.keep_status[2]["msg"] = "等待时间结束,新实例已启动成功" + res = self.stop_old() + self.keep_status[3]["status"] = 1 + self.keep_status[3]["msg"] = res if res else "停止旧实例成功,项目更新已结束" + public.M("sites").where("id=?", (self.old_project_data["id"],)).update( + {"project_config": json.dumps(self.new_project_config)} + ) + except: + print(traceback.format_exc()) + pass + + def stop_new(self): + new_server_name = "spring_" + self.project_name + self.new_project_config.get("server_name_suffix", "") + RealServer().server_admin(new_server_name, "stop") + RealServer().del_daemon(new_server_name) + if self.new_pro and self.new_pro.is_running(): + self.new_pro.kill() + + def start_new(self) -> Optional[str]: + self.keep_status[0]["status"] = -1 + self.new_project_config['server_name_suffix'] = self.new_suffix() + self.new_project_config['pids'] = "{}/pids/{}.pid".format( + self.j_project._java_project_vhost, self.project_name + self.new_project_config['server_name_suffix'] + ) + + if not self.new_port or self.new_port in self.old_listen_port() or \ + utils.check_port_with_net_connections(self.new_port): + self.new_port = utils.create_a_not_used_port() + + old_jar = self.old_project_config['project_jar'] + if self.new_jar != old_jar: + if not os.path.isfile(self.new_jar): + return "项目jar包不存在" + + self.new_project_config['jar_path'] = os.path.dirname(self.new_jar) + self.new_project_config['project_jar'] = self.new_jar + old_jar_name = os.path.basename(old_jar) + project_cmd_list = self.new_project_config['project_cmd'].split(" ") + for i in range(len(project_cmd_list)): + if old_jar_name in project_cmd_list[i]: + project_cmd_list[i] = self.new_jar + break + + new_project_cmd = " ".join(project_cmd_list) + self.new_project_config['project_cmd'] = new_project_cmd + + if "--server.port=" in self.new_project_config['project_cmd']: + self.new_project_config['project_cmd'] = re.sub( + r"--server\.port=\d+", + "--server.port={}".format(self.new_port), + self.new_project_config['project_cmd'] + ) + else: + self.new_project_config['project_cmd'] += " --server.port={}".format(self.new_port) + + self.old_project_data["project_config"] = self.new_project_config + + self.start_spring_project(self.old_project_data, write_systemd_file=True) + time.sleep(1) + new_pid = self.j_project.get_project_pid(self.old_project_data) + if not new_pid: + return "项目启动失败" + self.new_pro = psutil.Process(new_pid) + self.keep_status[0]["msg"] = "新实例pid为:{}".format(new_pid) + # 开始等待进程启动 + server_name = "spring_" + self.project_name + self.new_project_config.get("server_name_suffix", "") + wait_num = 1 + for i in range(5 * 60 * 2 - 2): + if self.end: + RealServer().server_admin(server_name, "stop") + RealServer().del_daemon(server_name) + return "退出操作" + if not self.new_pro.is_running(): + RealServer().del_daemon(server_name) + return "项目启动失败" + + conns = self.new_pro.connections() + for c in conns: + if c.status == "LISTEN" and c.laddr.port == self.new_port: + return + self.keep_status[0]["msg"] = "新实例pid为:{}, 正在等待该进程监听端口:{}, 已等待{}s".format(new_pid, self.new_port, wait_num) + wait_num += 0.5 + time.sleep(0.5) + + RealServer().server_admin(server_name, "stop") + RealServer().del_daemon(server_name) + return "启动超时" + + def old_listen_port(self) -> List[int]: + connects = self.old_pro.connections() + res = [] + for i in connects: + if i.status == "LISTEN": + res.append(i.laddr.port) + return res + + def set_nginx_upstream(self) -> Optional[str]: + self.keep_status[1]["status"] = -1 + ng_file = "/www/server/panel/vhost/nginx/java_{}.conf".format(self.project_name) + res = public.checkWebConfig() + if res is not True: + return "Nginx配置文件错误,无法开始轮询配置" + ng_data = public.readFile(ng_file) + if not isinstance(ng_data, str): + return "Nginx配置文件读取错误,无法开始轮询配置" + + old_proxy_res = None + for tmp_res in re.finditer(r"\s*proxy_pass\s+(?P\S+)\s*;", ng_data, re.M): + url: Url = parse_url(tmp_res.group("url")) + if url.hostname in ("127.0.0.1", "localhost", "0.0.0.0") and url.port in self.old_listen_port(): + old_proxy_res = tmp_res + self.proxy_data["scheme"] = url.scheme + self.proxy_data["old_port"] = url.port + if not old_proxy_res: + return "未找到原实例的代理配置" + + upstream_file = "/www/server/panel/vhost/nginx/java_{}_upstream.conf".format(self.project_name) + public.writeFile(upstream_file, """ +upstream {}_backend {{ + server 127.0.0.1:{}; + server 127.0.0.1:{}; +}} +""".format(self.project_name, self.proxy_data["old_port"], self.new_port)) + + new_config = ng_data.replace(old_proxy_res.group(), "\n proxy_pass {}://{}_backend;".format( + self.proxy_data["scheme"], self.project_name)) + + public.writeFile(ng_file, new_config) + + res = public.checkWebConfig() + if res is not True: + public.writeFile(ng_file, ng_data) + return "Nginx配置文件错误,无法开始轮询配置" + else: + public.serviceReload() + + def wait_time(self): + self.keep_status[2]["status"] = -1 + if not self.run_time: + self.run_time = 10 * 60 + for i in range(self.run_time): + if self.end: + return "退出操作" + self.keep_status[2]["msg"] = "已进入轮询测试等待" + if i > 0: + self.keep_status[2]["msg"] = "已进入轮询测试等待,已等待{}s, 共需等待{}s".format(i, self.run_time) + time.sleep(1) + if not self.new_pro.is_running(): + return "新示例已退出,无法继续执行操作" + return None + + def select_new_or_old(self, option: str): + if option == "use_new": + self.keep_status[2]["status"] = 1 + self.keep_status[2]["msg"] = "已跳过等待时间,使用新实例运行" + res = self.stop_old() + public.M("sites").where("id=?", (self.old_project_data["id"],)).update( + {"project_config": json.dumps(self.new_project_config)} + ) + self.keep_status[3]["status"] = 1 + self.keep_status[3]["msg"] = res if res else "停止旧实例成功,项目更新已结束" + return {"status": False if res else True, "msg": res if res else "停止旧实例成功,项目更新已结束"} + + self.keep_status[2]["status"] = 1 + self.keep_status[2]["msg"] = "已跳过等待时间,使用原实例运行" + self.keep_status[3]["name"] = "停止新实例" + self.keep_status[3]["status"] = 1 + ng_file = "/www/server/panel/vhost/nginx/java_{}.conf".format(self.project_name) + ng_data = public.readFile(ng_file) + if not isinstance(ng_data, str): + return {"status": False, "msg": "Nginx配置文件读取错误,无法取消轮询并使用原实例"} + res = public.checkWebConfig() + if res is not True: + return {"status": False, "msg": "Nginx配置文件错误,无法取消轮询并使用原实例"} + + upstream_file = "/www/server/panel/vhost/nginx/java_{}_upstream.conf".format(self.project_name) + new_config = ng_data.replace( + "{}_backend".format(self.project_name), + "127.0.0.1:{}".format(self.proxy_data["old_port"]) + ) + public.writeFile(ng_file, new_config) + res = public.checkWebConfig() + if res is not True: + public.writeFile(ng_file, ng_data) + return {"status": False, "msg": "Nginx配置文件设置错误,无法取消轮询并使用原实例"} + else: + os.remove(upstream_file) + public.serviceReload() + self.stop_new() + + return {"status": True, "msg": "停止新实例成功,项目更新已结束"} + + def stop_old(self): + self.keep_status[3]["status"] = -1 + ng_file = "/www/server/panel/vhost/nginx/java_{}.conf".format(self.project_name) + ng_data = public.readFile(ng_file) + if not isinstance(ng_data, str): + return "Nginx配置文件读取错误,无法取消轮询,使用新实例" + + res = public.checkWebConfig() + if res is not True: + return "Nginx配置文件错误,无法取消轮询,使用新实例" + + old_proxy_res = None + for tmp_res in re.finditer(r"\s*proxy_pass\s+(?P\S+)\s*;", ng_data, re.M): + if tmp_res.group("url").find("{}_backend".format(self.project_name)): + old_proxy_res = tmp_res + + if not old_proxy_res: + return "未找到轮询的代理配置" + + upstream_file = "/www/server/panel/vhost/nginx/java_{}_upstream.conf".format(self.project_name) + if os.path.isfile(upstream_file): + os.remove(upstream_file) + + new_config = ng_data.replace(old_proxy_res.group(), "\n proxy_pass {}://127.0.0.1:{};".format( + self.proxy_data["scheme"], self.new_port)) + + public.writeFile(ng_file, new_config) + + res = public.checkWebConfig() + if res is not True: + public.writeFile(ng_file, ng_data) + return "Nginx配置文件错误,无法结束轮询配置" + else: + public.serviceReload() + + old_server_name = "spring_" + self.project_name + self.old_project_config.get("server_name_suffix", "") + RealServer().server_admin(old_server_name, "stop") + RealServer().del_daemon(old_server_name) + if self.old_pro and self.old_pro.is_running(): + self.old_pro.kill() + + return None + + def keep_update(self): + pid_file = "{}/{}.pid".format(self.keep_path, self.project_name) + log_file = "{}/{}.log".format(self.keep_path, self.project_name) + pid = os.getpid() + public.writeFile(pid_file, str(pid)) + if os.path.exists(log_file): + os.remove(log_file) + + project_data = self.j_project.get_project_find(self.project_name) + if not project_data: + return json_response(False, msg="项目不存在") + + project_config = project_data['project_config'] + self.old_project_data = project_data + self.old_project_config = project_config + self.new_project_config = copy.deepcopy(project_config) + + try: + self.old_pro = psutil.Process(self.j_project.get_project_pid(project_data)) + except: + pass + if not self.old_pro: + return json_response(False, msg="项目未启动") + + self.end = False + self.keep_status = [ + { + "name": "启动新实例", + "status": 0, + "msg": "", + }, + { + "name": "设置Nginx轮询", + "status": 0, + "msg": "", + }, + { + "name": "等待并检查新实例", + "status": 0, + "msg": "", + }, + { + "name": "停止旧实例", + "status": 0, + "msg": "", + } + ] + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + socket_file = "{}/{}.socket".format(self.keep_path, self.project_name) + # 清理旧的socket文件,如果存在 + if os.path.exists(socket_file): + os.remove(socket_file) + + # 设置为非阻塞 + sock.bind(socket_file) + sock.setblocking(False) # 0表示非阻塞,1表示阻塞 + sock.listen(2) + + update_run_task = Thread(target=self.run_task) + update_run_task.start() + + while True: + if not update_run_task.is_alive(): + public.writeFile(log_file, json.dumps(self.keep_status)) + break + + try: + # 读取客户端发送的数据 + conn, _ = sock.accept() + data = conn.recv(1024) + except socket.error as e: + if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK): + raise e + time.sleep(0.1) + continue + + if not data: + time.sleep(0.1) + continue + + # 打印接收到的数据 + print("Received:", data.decode()) + data_str = data.decode() + if data_str == "stop_new": + if self.keep_status[0]["status"] == -1: + self.end = True + update_run_task.join() + public.writeFile(log_file, json.dumps(self.keep_status)) + conn.sendall(json.dumps({ + "status": True, + "msg": "已关闭更新任务,并停止新实例" + }).encode()) + break + else: + conn.sendall(json.dumps({ + "status": False, + "msg": "新实例启动完成,已加入轮询,无法继续执行该操作" + }).encode()) + elif data_str == "status": + conn.sendall(json.dumps(self.keep_status).encode()) + elif data_str in ("use_new", "use_old"): + if self.keep_status[2]["status"] != -1: + conn.sendall(json.dumps({ + "status": False, + "msg": "已超过轮询等待时间,无法执行该操作" + }).encode()) + else: + self.end = True + update_run_task.join() + public.writeFile(log_file, json.dumps(self.keep_status)) + res = self.select_new_or_old(data_str) + conn.sendall(json.dumps(res).encode()) + + time.sleep(0.1) + + # 关闭服务器端socket + sock.close() + # 清理旧的socket文件,如果存在 + if os.path.exists(socket_file): + os.remove(socket_file) + + def get_keep_status(self): + try: + log_file = "{}/{}.log".format(self.keep_path, self.project_name) + pid_file = "{}/{}.pid".format(self.keep_path, self.project_name) + log_data = public.readFile(log_file) + data = None + if isinstance(log_data, str): + try: + data = json.loads(log_data) + except: + pass + + if data: + return json_response(True, data={ + "running": False, + "keep_msg": data + }) + + pid_data = public.readFile(pid_file) + er_msg = "没有正在进行的更新任务" + if not isinstance(pid_data, str): + return json_response(False, msg=er_msg) + try: + pid = int(pid_data) + if not psutil.pid_exists(pid): + return json_response(False, msg=er_msg) + except: + return json_response(False, msg=er_msg) + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect("{}/{}.socket".format(self.keep_path, self.project_name)) + except Exception: + public.print_log(public.get_error_info()) + return json_response(False, msg="链接错误请尝试强制停止更新") + data = b"status" + sock.sendall(data) + + # 接收响应 + sock.settimeout(1) + response = sock.recv(1024 * 2) + sock.close() + try: + data = json.loads(response.decode()) + except: + public.print_log(public.get_error_info()) + return json_response(False, msg="链接错误请尝试强制停止更新") + return json_response(True, data={ + "running": True, + "keep_msg": data + }) + except: + public.print_log(public.get_error_info()) + return json_response(False, msg="链接错误请尝试强制停止更新") + + def keep_option(self, option: str) -> dict: + try: + pid_file = "{}/{}.pid".format(self.keep_path, self.project_name) + pid_data = public.readFile(pid_file) + er_msg = "没有正在进行的更新任务, 无法执行操作" + if not isinstance(pid_data, str) or pid_data == "0": + return json_response(False, msg=er_msg) + try: + pid = int(pid_data) + if not psutil.pid_exists(pid): + return json_response(False, msg=er_msg) + except: + return json_response(False, msg=er_msg) + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect("{}/{}.socket".format(self.keep_path, self.project_name)) + except Exception: + return json_response(False, msg="链接错误,无法执行操作,请尝试强制停止更新") + + sock.sendall(option.encode()) + + # 接收响应 + sock.settimeout(10) + response = sock.recv(1024) + sock.close() + try: + data = json.loads(response.decode()) + except: + public.print_log(public.get_error_info()) + return json_response(False, msg="链接错误请尝试强制停止更新") + if isinstance(data, dict): + return json_response(data['status'], msg=data['msg']) + else: + return json_response(False, msg="链接错误请尝试强制停止更新") + except: + public.print_log(public.get_error_info()) + return json_response(False, msg="链接错误请尝试强制停止更新") + + +if __name__ == '__main__': + def run_main(project_name: str, new_jar: str, new_port: int, run_time: int,): + pu = ProjectUpdate(project_name, new_jar=new_jar, run_time=run_time, new_port=new_port) + pu.keep_update() + + if len(sys.argv) == 5: + run_main(sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])) + diff --git a/mod/project/java/server_proxy.py b/mod/project/java/server_proxy.py new file mode 100644 index 00000000..3aa96a07 --- /dev/null +++ b/mod/project/java/server_proxy.py @@ -0,0 +1,644 @@ +import os +import re +import json +from typing import Optional, Union, List, Dict, Any +from mod.base.web_conf.util import check_server_config, write_file, read_file, service_reload +from mod.base import json_response +from urllib3.util import Url, parse_url + +import public + + +class RealServerProxy: + panel_path = "/www/server/panel" + default_headers = ( + "Host", "X-Real-IP", "X-Forwarded-For", "REMOTE-HOST", "X-Host", "X-Scheme", "Upgrade", "Connection" + ) + + def __init__(self, project_data: dict): + self.config_prefix: str = "java_" + + site_name = project_data["name"] + self.project_id = project_data["id"] + self.project_config = project_data["project_config"] + + self._config: Optional[List[dict]] = None + self._ng_file: str = "{}/vhost/nginx/{}{}.conf".format(self.panel_path, self.config_prefix, site_name) + self._ap_file: str = "{}/vhost/apache/{}{}.conf".format(self.panel_path, self.config_prefix, site_name) + self.site_name = site_name + self.ws_type = public.get_webserver() + + @staticmethod + def new_id() -> str: + from uuid import uuid4 + return uuid4().hex[::3] + + @property + def config(self) -> List[dict]: + if self._config is None: + if "proxy_info" in self.project_config: + self._config = self.project_config["proxy_info"] + else: + self._config = [] + + return self._config + + def save_config(self): + if self._config is not None: + self.project_config["proxy_info"] = self._config + public.M("sites").where("id=?", (self.project_id,)).update( + {"project_config": json.dumps(self.project_config)} + ) + + # 检查代理是否存在 + def _check_even(self, proxy_conf: dict, is_modify) -> Optional[str]: + if is_modify is True: + for i in self.config: + if i["proxy_dir"] == proxy_conf["proxy_dir"] and i["proxy_id"] != proxy_conf["proxy_id"]: + return '指定反向代理名称或代理文件夹已存在' + if i["proxy_port"] == proxy_conf["proxy_port"] and i["proxy_id"] != proxy_conf["proxy_id"]: + return '指定反向代理端口已存在对应的代理' + else: + for i in self.config: + if i["proxy_port"] == proxy_conf["proxy_port"]: + return '指定反向代理端口已存在对应的代理' + + def check_args(self, get, is_modify=False) -> Union[str, dict]: + err_msg = check_server_config() + if isinstance(err_msg, str): + return 'WEB服务器配置配置文件错误ERROR:
                                    ' + \ + err_msg.replace("\n", '
                                    ') + '
                                    ' + data = { + "proxy_dir": "/", + "status": 1, + "proxy_id": self.new_id(), + "rewrite": { + "status": False, + "src_path": "", + "target_path": "", + }, + "add_headers": [], + } + try: + data["site_name"] = get.site_name.strip() + if "proxy_dir" in get: + data["proxy_dir"] = get.proxy_dir.strip() + if "proxy_id" in get: + data["proxy_id"] = get.proxy_id.strip() + data["proxy_port"] = int(get.proxy_port) + data["status"] = int(get.status.strip()) + if hasattr(get, "rewrite"): + data["rewrite"] = get.rewrite + if isinstance(get.rewrite, str): + data["rewrite"] = json.loads(get.rewrite) + + if hasattr(get, "add_headers"): + data["add_headers"] = get.add_headers + if isinstance(get.add_headers, str): + data["add_headers"] = json.loads(get.add_headers) + except: + public.print_log(public.get_error_info()) + return "参数错误" + + if not 1 < data["proxy_port"] < 65536: + return '代理端口范围错误' + + if not data["proxy_dir"].endswith("/"): + data["proxy_dir"] += "/" + + evn_msg = self._check_even(data, is_modify) + if isinstance(evn_msg, str): + return evn_msg + + rep_re_key = re.compile(r'''[?=\[\])(*&^%$#@!~`{}><,'"\\]+''') + special = r'''?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\,',"''' + # 检测代理目录格式 + if rep_re_key.search(data["proxy_dir"]): + return "代理路由不能有以下特殊符号" + special + + if not isinstance(data["rewrite"], dict): + return "路由重写配置错误" + if "status" not in data["rewrite"] or not data["rewrite"]["status"]: + data["rewrite"] = { + "status": False, + "src_path": "", + "target_path": "", + } + else: + if not ("src_path" in data["rewrite"] and "target_path" in data["rewrite"]): + return "路由重写参数配置错误" + if not isinstance(data["rewrite"]["src_path"], str) or not isinstance(data["rewrite"]["target_path"], str): + return "路由重写参数配置错误" + if rep_re_key.search(data["rewrite"]["src_path"]): + return "路由重写匹配路由不能有以下特殊符号" + special + if rep_re_key.search(data["rewrite"]["target_path"]): + return "路由重写目标路由不能有以下特殊符号" + special + + if not isinstance(data["add_headers"], list): + return "自定义代理头配置错误" + else: + rep_blank_space = re.compile(r"\s+") + for h in data["add_headers"]: + if "k" not in h or "v" not in h: + return "自定义代理头配置错误" + if not isinstance(h["k"], str) or not isinstance(h["v"], str): + return "自定义代理头配置错误" + if rep_blank_space.search(h["k"]) or rep_blank_space.search(h["v"]): + return "代理头配置中不能包含有空格" + if h["k"] in self.default_headers: + return '代理头配置中不能包含有默认头【{}】'.format(h["k"]) + + return data + + def check_location(self, proxy_dir: str) -> Optional[str]: + # 伪静态文件路径 + rewrite_conf_path = "%s/vhost/rewrite/%s%s.conf" % (self.panel_path, self.config_prefix, self.site_name) + + rep_location = re.compile(r"s*location\s+(\^~\s*)?%s\s*{" % proxy_dir) + + for i in [rewrite_conf_path, self._ng_file]: + conf = read_file(i) + if isinstance(conf, str) and rep_location.search(conf): + return '伪静态/站点主配置文件已经存路径【{}】的配置'.format(proxy_dir) + + @staticmethod + def _set_nginx_proxy_base(): + file = "/www/server/nginx/conf/proxy.conf" + setup_path = "/www/server" + if not os.path.exists(file): + conf = '''proxy_temp_path %s/nginx/proxy_temp_dir; +proxy_cache_path %s/nginx/proxy_cache_dir levels=1:2 keys_zone=cache_one:10m inactive=1d max_size=5g; +client_body_buffer_size 512k; +proxy_connect_timeout 60; +proxy_read_timeout 60; +proxy_send_timeout 60; +proxy_buffer_size 32k; +proxy_buffers 4 64k; +proxy_busy_buffers_size 128k; +proxy_temp_file_write_size 128k; +proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; +proxy_cache cache_one;''' % (setup_path, setup_path) + write_file(file, conf) + + conf = read_file(file) + if conf and conf.find('include proxy.conf;') == -1: + conf = re.sub(r"include\s+mime\.types;", "include mime.types;\n\tinclude proxy.conf;", conf) + write_file(file, conf) + + # websocket前置map + map_file = "/www/server/panel/vhost/nginx/0.websocket.conf" + if not os.path.exists(map_file): + write_file(map_file, ''' +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +}''') + + @staticmethod + def build_proxy_conf(proxy_data: dict) -> str: + ng_proxy = ''' + #PROXY-START{proxy_dir} + location {proxy_dir} {{{rewrite} + proxy_pass {proxy_url}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;{add_headers} + proxy_set_header REMOTE-HOST $remote_addr; + add_header X-Cache $upstream_cache_status; + proxy_set_header X-Host $host:$server_port; + proxy_set_header X-Scheme $scheme; + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + proxy_send_timeout 30s; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + }} + #PROXY-END{proxy_dir} +''' + + rewrite = "" + if "rewrite" in proxy_data and proxy_data["rewrite"].get("status", False): + rewrite = proxy_data["rewrite"] + src_path = rewrite["src_path"] + if not src_path.endswith("/"): + src_path += "/" + target_path = rewrite["target_path"] + if target_path.endswith("/"): + target_path = target_path[:-1] + + rewrite = "\n rewrite ^{}(.*)$ {}/$1 break;".format(src_path, target_path) + + add_headers = "" + if "add_headers" in proxy_data: + header_tmp = " proxy_set_header {} {};" + add_headers_list = [header_tmp.format(h["k"], h["v"]) for h in proxy_data["add_headers"] if + "k" in h and "v" in h] + add_headers = "\n".join(add_headers_list) + if add_headers: + add_headers = "\n" + add_headers + + # 构造替换字符串 + proxy_dir = proxy_data["proxy_dir"] + proxy_site = "http://127.0.0.1:{}".format(proxy_data["proxy_port"]) + + proxy = ng_proxy.format( + proxy_dir=proxy_dir, + proxy_url=proxy_site, + rewrite=rewrite, + add_headers=add_headers, + ) + + return proxy + + def add_nginx_proxy(self, proxy_data: dict) -> Optional[str]: + ng_conf = read_file(self._ng_file) + if not ng_conf: + return "Nginx配置文件不存在" + + proxy_str = self.build_proxy_conf(proxy_data) + + # 添加配置信息到配置文件中 + rep_list = [ + (re.compile(r"\s*#PROXY-LOCAl-END.*", re.M), True), # 添加到反向代理结尾的上面 + (re.compile(r"\s*#ROXY-END.*", re.M), False), # 添加到其他的反向代理的下面 + (re.compile(r"\s*#\s*HTTP反向代理相关配置结束\s*<<<.*", re.M), False), # 添加到其他的反向代理的下面 + (re.compile(r"\s*include\s*/www/server/panel/vhost/rewrite/.*(\s*#.*)?"), False), + # 添加到伪静态的下面 + # (re.compile(r"(#.*)?\s*location\s+/\.well-known/\s*{"), True), # 添加到location /.well-known/上面 + ] + + # 使用正则匹配确定插入位置 + def set_by_rep_idx(tmp_rep: re.Pattern, use_start: bool) -> bool: + tmp_res = tmp_rep.search(ng_conf) + if not tmp_res: + return False + if use_start: + new_conf = ng_conf[:tmp_res.start()] + proxy_str + tmp_res.group() + ng_conf[tmp_res.end():] + else: + new_conf = ng_conf[:tmp_res.start()] + tmp_res.group() + proxy_str + ng_conf[tmp_res.end():] + + write_file(self._ng_file, new_conf) + if self.ws_type == "nginx" and check_server_config() is not None: + write_file(self._ng_file, ng_conf) + return False + return True + + for r, s in rep_list: + if set_by_rep_idx(r, s): + break + else: + return "无法在配置文件中定位到需要添加的项目" + + def _unset_nginx_proxy(self, proxy_data) -> Optional[str]: + ng_conf = read_file(self._ng_file) + if not isinstance(ng_conf, str): + return "配置文件不存在" + + proxy_dir = proxy_data["proxy_dir"] + rep_start_end = re.compile(r"\s*#PROXY-START%s(.|\n)*?#PROXY-END%s[^\n]*" % (proxy_dir, proxy_dir)) + if rep_start_end.search(ng_conf): + new_ng_conf = rep_start_end.sub("", ng_conf) + write_file(self._ng_file, new_ng_conf) + if self.ws_type == "nginx": + err_msg = check_server_config() + if isinstance(err_msg, str): + write_file(self._ng_file, ng_conf) + return err_msg + else: + return + + rep_location = re.compile(r"(\s*#.*?)\s*location\s+(\^~\s*)?%s\s*{" % proxy_dir) + res = rep_location.search(ng_conf) + if res: + end_idx = self.find_nginx_block_end(ng_conf, res.end() + 1) + if not end_idx: + return + + block = ng_conf[res.start(): end_idx] + if block.find("proxy_pass") == -1: # 如果这块内不包含proxy_pass 则跳过 + return + + # 异除下一个注释行 + res_end = re.search(r"\s*#PROXY-END.*", ng_conf[end_idx + 1:]) + if res_end: + end_idx += res_end.end() + + new_ng_conf = ng_conf[:res.start()] + ng_conf[end_idx + 1:] + write_file(self._ng_file, new_ng_conf) + if self.ws_type == "nginx": + err_msg = check_server_config() + if isinstance(err_msg, str): + write_file(self._ng_file, ng_conf) + return err_msg + + @staticmethod + def find_nginx_block_end(data: str, start_idx: int) -> Optional[int]: + if len(data) < start_idx + 1: + return None + + level = 1 + line_start = 0 + for i in range(start_idx + 1, len(data)): + if data[i] == '\n': + line_start = i + 1 + if data[i] == '{' and line_start and data[line_start: i].find("#") == -1: # 没有注释的下一个{ + level += 1 + elif data[i] == '}' and line_start and data[line_start: i].find("#") == -1: # 没有注释的下一个} + level -= 1 + if level == 0: + return i + + return None + + @staticmethod + def build_apache_conf(proxy_data: dict) -> str: + return ''' + #PROXY-START{proxy_dir} + + ProxyRequests Off + SSLProxyEngine on + ProxyPass {proxy_dir} {url}/ + ProxyPassReverse {proxy_dir} {url}/ + RequestHeader set Host "%{{Host}}e" + RequestHeader set X-Real-IP "%{{REMOTE_ADDR}}e" + RequestHeader set X-Forwarded-For "%{{X-Forwarded-For}}e" + RequestHeader setifempty X-Forwarded-For "%{{REMOTE_ADDR}}e" + + #PROXY-END{proxy_dir} +'''.format(proxy_dir=proxy_data["proxy_dir"], url="http://127.0.0.1:{}".format(proxy_data["proxy_port"])) + + def add_apache_proxy(self, proxy_data: dict) -> Optional[str]: + ap_conf = read_file(self._ap_file) + if not ap_conf: + return "Apache配置文件不存在" + + proxy_str = self.build_apache_conf(proxy_data) + + # 添加配置信息到配置文件中 + rep_list = [ + (re.compile(r"#ROXY-END[^\n]*\n"), False), # 添加到其他的反向代理的下面 + (re.compile(r"#\s*HTTP反向代理相关配置结束\s*<<<[^\n]*\n"), False), # 添加到其他的反向代理的下面 + ( + re.compile(r"\s*(#SSL[^\n]*)?\s*[^\n]*\s*.*/.well-known/[^\n]*\s*"), + True # 添加到location /.well-known/上面 + ), + (re.compile(r"\s*[^\n]*\n?"), True), + ] + + # 使用正则匹配确定插入位置 + def set_by_rep_idx(tmp_rep: re.Pattern, use_start: bool) -> bool: + new_conf_list = [] + change_flag = False + start_idx = 0 + for tmp in tmp_rep.finditer(ap_conf): + change_flag = True + new_conf_list.append(ap_conf[start_idx:tmp.start()]) + start_idx = tmp.end() + if use_start: + new_conf_list.append(proxy_str) + new_conf_list.append(tmp.group()) + else: + new_conf_list.append(tmp.group()) + new_conf_list.append(proxy_str) + + if not change_flag: + return False + + new_conf_list.append(ap_conf[start_idx:]) + write_file(self._ap_file, "".join(new_conf_list)) + if self.ws_type == "apache" and check_server_config() is not None: + write_file(self._ap_file, ap_conf) + return False + return True + + for r, s in rep_list: + if set_by_rep_idx(r, s): + break + else: + return "无法在配置文件中定位到需要添加的项目" + + def remove_apache_proxy(self, proxy_data) -> Optional[str]: + ap_conf = read_file(self._ap_file) + if not isinstance(ap_conf, str): + return "配置文件不存在" + + proxy_dir = proxy_data["proxy_dir"] + rep_start_end = re.compile(r"\s*#PROXY-START%s(.|\n)*?#PROXY-END%s[^\n]*" % (proxy_dir, proxy_dir)) + if rep_start_end.search(ap_conf): + new_ap_conf = rep_start_end.sub("", ap_conf) + write_file(self._ap_file, new_ap_conf) + if self.ws_type == "apache": + err_msg = check_server_config() + if isinstance(err_msg, str): + write_file(self._ap_file, ap_conf) + return err_msg + else: + return + + rep_if_mod = re.compile( + r"(\s*#.*)?\s*\s*(.*\n){3,5}\s*" + r"ProxyPass\s+%s\s+\S+/\s*(.*\n){1,2}\s*(\s*#.*)?" % proxy_dir) + + res = rep_if_mod.search(ap_conf) + if res: + new_ap_conf = rep_if_mod.sub("", ap_conf) + write_file(self._ap_file, new_ap_conf) + if self.ws_type == "apache": + err_msg = check_server_config() + if isinstance(err_msg, str): + write_file(self._ap_file, ap_conf) + return err_msg + + def create_proxy(self, proxy_data: dict) -> Optional[str]: + for i in self.config: + if i["proxy_dir"] == proxy_data["proxy_dir"]: + proxy_data["proxy_id"] = i["proxy_id"] + return self.modify_proxy(proxy_data) + + if self.ws_type == "nginx": + err_msg = self.check_location(proxy_data["proxy_dir"]) + if err_msg: + return json_response(False, err_msg) + + self._set_nginx_proxy_base() + error_msg = self.add_nginx_proxy(proxy_data) + if self.ws_type == "nginx" and error_msg: + return error_msg + error_msg = self.add_apache_proxy(proxy_data) + if self.ws_type == "apache" and error_msg: + return error_msg + self.config.append(proxy_data) + self.save_config() + service_reload() + + def modify_proxy(self, proxy_data: dict) -> Optional[str]: + idx = None + for index, i in enumerate(self.config): + if i["proxy_id"] == proxy_data["proxy_id"] and i["site_name"] == proxy_data["site_name"]: + idx = index + break + + if idx is None: + return "未找到该id的反向代理配置" + + if proxy_data["proxy_dir"] != self.config[idx]["proxy_dir"] and self.ws_type == "nginx": + err_msg = self.check_location(proxy_data["proxy_dir"]) + if err_msg: + return json_response(False, err_msg) + + self._set_nginx_proxy_base() + error_msg = self._unset_nginx_proxy(self.config[idx]) + if self.ws_type == "nginx" and error_msg: + return error_msg + + error_msg = self.remove_apache_proxy(self.config[idx]) + if self.ws_type == "apache" and error_msg: + return error_msg + + error_msg = self.add_nginx_proxy(proxy_data) + if self.ws_type == "nginx" and error_msg: + return error_msg + + error_msg = self.add_apache_proxy(proxy_data) + if self.ws_type == "apache" and error_msg: + return error_msg + + self.config[idx] = proxy_data + self.save_config() + service_reload() + + def remove_proxy(self, site_name, proxy_id, multiple=False) -> Optional[str]: + idx = None + for index, i in enumerate(self.config): + if i["proxy_id"] == proxy_id and i["site_name"] == site_name: + idx = index + + if idx is None: + return "未找到该名称的反向代理配置" + + err_msg = self._unset_nginx_proxy(self.config[idx]) + if err_msg and self.ws_type == "nginx": + return err_msg + + error_msg = self.remove_apache_proxy(self.config[idx]) + if self.ws_type == "apache" and error_msg: + return error_msg + + del self.config[idx] + self.save_config() + if not multiple: + service_reload() + + def get_proxy_list_by_nginx(self) -> List[Dict[str, Any]]: + ng_conf = read_file(self._ng_file) + if not isinstance(ng_conf, str): + return [] + + rep_location = re.compile(r"\s*location\s+([=*~^]*\s+)?(?P\S+)\s*{") + proxy_location_path_info = {} + for tmp in rep_location.finditer(ng_conf): + end_idx = self.find_nginx_block_end(ng_conf, tmp.end() + 1) + if end_idx and ng_conf[tmp.start(): end_idx].find("proxy_pass") != -1: + p = tmp.group("path") + if not p.endswith("/"): + p += "/" + proxy_location_path_info[p] = (tmp.start(), end_idx) + + res_pass = re.compile(r"proxy_pass\s+(?P\S+)\s*;", re.M) + remove_list = [] + local_host = ("127.0.0.1", "localhost", "0.0.0.0") + for i in self.config: + if i["proxy_dir"] in proxy_location_path_info: + start_idx, end_idx = proxy_location_path_info[i["proxy_dir"]] + block = ng_conf[start_idx: end_idx] + res_pass_res = res_pass.search(block) + if res_pass_res: + url = parse_url(res_pass_res.group("pass")) + if isinstance(url, Url) and url.hostname in local_host and url.port == i["proxy_port"]: + i["status"] = True + proxy_location_path_info.pop(i["proxy_dir"]) + continue + + remove_list.append(i) + + need_save = False + for i in remove_list: + self.config.remove(i) + need_save = True + + for path, (start_idx, end_idx) in proxy_location_path_info.items(): + block = ng_conf[start_idx: end_idx] + res_pass_res = res_pass.search(block) + if res_pass_res: + url = parse_url(res_pass_res.group("pass")) + if isinstance(url, Url) and url.hostname in ("127.0.0.1", "localhost", "0.0.0.0"): + self.config.insert(0, { + "proxy_id": self.new_id(), + "site_name": self.site_name, + "proxy_dir": "/", + "proxy_port": url.port, + "status": 1, + "rewrite": { + "status": False, + "src_path": "", + "target_path": "", + }, + "add_headers": [], + }) + need_save = True + if need_save: + self.save_config() + + return self.config + + def get_proxy_list_by_apache(self) -> List[Dict[str, Any]]: + ap_conf = read_file(self._ap_file) + if not isinstance(ap_conf, str): + return [] + + rep_proxy_pass = r"ProxyPass\s+%s\s+\S+/" + mian_location_use = False + for i in self.config: + if i["proxy_dir"] == "/": + mian_location_use = True + rep_l = re.search(rep_proxy_pass % i["proxy_dir"], ap_conf, re.M) + if rep_l: + i["status"] = 1 + else: + i["status"] = 0 + + if not mian_location_use: + res_l = re.search( + r"\s*\s*(.*\n){3,5}\s*" + r"ProxyPass\s+/\s+(?P\S+)/\s*(.*\n){1,2}\s*", ap_conf) + + if not res_l: + return self.config + + url = parse_url(res_l.group("pass")) + if isinstance(url, Url) and url.hostname in ("127.0.0.1", "localhost", "0.0.0.0"): + self.config.insert(0, { + "proxy_id": self.new_id(), + "site_name": self.site_name, + "proxy_dir": "/", + "proxy_port": url.port, + "status": 1, + "rewrite": { + "status": False, + "src_path": "", + "target_path": "", + }, + "add_headers": [], + }) + self.save_config() + + return self.config + + def get_proxy_list(self) -> List[Dict[str, Any]]: + if self.ws_type == "nginx": + return self.get_proxy_list_by_nginx() + else: + return self.get_proxy_list_by_apache() + + diff --git a/mod/project/java/springboot_parser.py b/mod/project/java/springboot_parser.py new file mode 100644 index 00000000..bc8615bc --- /dev/null +++ b/mod/project/java/springboot_parser.py @@ -0,0 +1,1047 @@ +import itertools +import json +import os +import re +import zipfile + +import psutil +import yaml +import public +from mod.project.java import utils + +from typing import Optional, List, Tuple, AnyStr, Dict, Callable + +""" +针对jar包的【spring boot】配置文件加载逻辑 + +原理:spring boot 会在固定路径下寻找配置文件 + PWD = 运行目录 + CLASSES = jar包中的类目录 + 默认路径顺序: + PWD/config > PWD > CLASSES/config > CLASSES + +# 补充 + --spring.config.name == application 可以用于指定配置文件前缀 + +这个些路径可以被配置项spring.config.location 更改,可以指定为多个目录或文件,但该配置项目,一般不会在配置文件中使用,而是在命令行 +或者环境变量中使用。 +在加载完成application.properties 或 application.yml文件后,一般会根据配置项spring.profiles.active 来加载 +子配制项, 例如 spring.profiles.active=dev, 则会加载application-dev.properties 或 application-dev.yml文件。 +同时,spring boot 会根据配置项spring.profiles.include 来加载子配制项, 例如 spring.profiles.include=dev1,dev2, +则会加载application-dev1(2).properties 或 application-dev1(2).yml文件。 +按照激活顺序,后面的配置项将会覆盖前面的配置项。 + +故: 该模块主要支持从jar、命令行中环境变量中获取spring boot配置信息 + +判断依据:命令行 > 环境变量 > jar包中配置文件 +命令行 和 环境变量 + 先检查spring.config.location (SPRING_CONFIG_LOCATION) + 有: 从location 位置加载 + 无: 从jar加载 + 在检查 spring.profiles.active (SPRING_CONFIG_ACTIVE) + 有: 记录一下激活的 flag + 无: 不记录 + + 解析文件 + 先从 jar 和 location 加载所有配置 + 然后 如果没有 flag : 从配置所有 主配置 (application.properties 或 application.yml)中加载 flag项(spring.profiles.active) + 然后 从包含的 flag 和 主配置 加载 spring.profiles.include + + 最后 组合这些项目并分析其中的外部依赖 + +""" + + +class SpringConfigParser: + other_server_keywords = ( + "redis", + "rabbitmq", + "rocketmq", + "kafka", + "elasticsearch", + ) + rep_jdbc_url = re.compile(r"jdbc:(?P\S+)://(?P.*?):(?P\d+)/(?P[^?\s]*)") + localhost_key = ("localhost", "127.0.0.1", "0.0.0.0", public.get_server_ip(), public.get_network_ip()) + + # 其他服务解析host时, 不支持 ${XXX} 的格式 + rep_host_port = re.compile( + r'^(?P(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|' # ipv4 + r'(\[?(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::[0-9a-fA-F]{0,4})]?)|localhost))' # ipv6 + localhost + r':(?P[0-9]{1,5})$') # 端口 + + def __init__(self, jar_path: str, process: int = -1, cmd: str = None, + env_list: List[dict] = None, env_file: str = None): + self.jar_path = jar_path + self.find_flag_list = [] # 获取到的配置文件flag列表 + self.location_conf_list = [] # 从外部加载的配置文件列表 + self.not_used_flag_list = [] # 从外部加载的配置文件列表 + self.cmd = cmd + self.pwd: str = "" + self.config_name = "application" + if not isinstance(process, int) or process <= 0: + process = -1 + try: + p = psutil.Process(process) + self.pwd = p.cwd() + except: + self.process_env = {} + else: + self.process_env = p.environ() + if not self.cmd: + self.cmd = " ".join(p.cmdline()) + + self.find_by_default = True + + if not self.pwd or not os.path.isdir(self.pwd): + self.pwd = os.path.dirname(self.jar_path) + + self.my_sql_root_auth = None + + if env_list: + for i in env_list: + if "k" in i and "v" in i: + self.process_env[i["k"]] = i["v"] + break + if env_file: + self.process_env.update(self.get_env_file_data(env_file)) + + self.location_tip_list = [] + self.raw_data = {} + + @staticmethod + def get_env_file_data(env_file) -> dict: + if not os.path.exists(env_file): + return {} + data = {} + out, _ = public.ExecShell(". {} && env".format(env_file), env={"PATH": os.environ["PATH"]}) + for i in out: + if "=" not in i: + continue + k, v = i.split("=", 1) + if k in ("PATH", "SHLVL", "PWD", "_"): + continue + if k and v: + data[k] = v + + def format_location_conf(self, location_str: str) -> List[str]: + res = [] + for i in location_str.split(","): + tmp = i.strip() + if not tmp: + continue + if tmp.startswith("file:"): + tmp = tmp[5:] + if tmp.startswith("/"): + if os.path.exists(tmp): + res.append(tmp) + else: + self.location_tip_list.append({ + "type": "config_location", + "level": "error", + "msg": "配置文件路径:{}不存在于文件系统中".format(tmp) + }) + else: + path = os.path.abspath(os.path.join(self.pwd, tmp)) + if os.path.exists(path): + res.append(os.path.join(self.pwd, tmp)) + else: + self.location_tip_list.append({ + "type": "config_location", + "level": "error", + "msg": "配置文件路径:{}不存在于文件系统中".format(tmp) + }) + + return res + + def _parser_by_cmd_or_env(self): + """从命令行或者环境变量中获取配置信息""" + # 检查 location 配置项 + # 先从cmd中获取, 再从环境变量中获取 + location_find = False + name_find = False + flag_find = False + if self.cmd: + rep_location = re.compile(r"-[-D]spring\.config\.location=(?P\S+)") + rep_name = re.compile(r"-[-D]spring\.config\.name=(?P\S+)") + rep_flag = re.compile(r"-[-D]spring\.profiles\.active=(?P\S+)") + + res = rep_location.search(self.cmd) + if res: + location_find = True + location_str = res.group("location").strip("'\"") + location_file = self.format_location_conf(location_str) + self.location_conf_list.extend(location_file) + self.find_by_default = False + + res = rep_name.search(self.cmd) + if res: + name_find = True + self.config_name = res.group("name").strip().strip("'\"") + + res = rep_flag.search(self.cmd) + if res: + flag_find = True + flags = res.group("flag").strip("'\"").split(",") + self.find_flag_list.extend(flags) + + if self.process_env: + if "SPRING_PROFILES_ACTIVE" in self.process_env and not flag_find: + flags = self.process_env["SPRING_PROFILES_ACTIVE"].split(",") + self.find_flag_list.extend(flags) + + if "SPRING_CONFIG_NAME" in self.process_env and not name_find: + self.config_name = self.process_env["SPRING_CONFIG_NAME"] + + if "SPRING_CONFIG_LOCATION" in self.process_env and not location_find: + location_str = self.process_env["SPRING_CONFIG_LOCATION"] + location_file = self.format_location_conf(location_str) + self.location_conf_list.extend(location_file) + self.find_by_default = False + + # 从默认路径结构中获取配置文件信息 + def _get_all_config_by_default(self) -> List[Tuple[str, Dict]]: + y_jar, p_jar = self.get_jar_config(self.jar_path) + y_custom, p_custom = self.search_config_file_by_path(self.pwd) + if os.path.exists(os.path.join(self.pwd, "config")): + y_custom_c, p_custom_c = self.search_config_file_by_path(os.path.join(self.pwd, "config")) + y_custom += y_custom_c + p_custom += p_custom_c + + y_jar = self.to_utf8(y_jar) + p_jar = self.to_utf8(p_jar) + + for i, data in itertools.chain(y_jar, y_custom, p_jar, p_custom): + self.raw_data[i] = data + + return self.parse_application_yaml(y_jar + y_custom) + self.parse_application_properties(p_jar + p_custom) + + def _get_all_config_by_location(self) -> List[Tuple[str, Dict]]: + y_list, p_list = [], [] + for i in self.location_conf_list: + if os.path.isdir(i): + y_list_tmp, p_list_tmp = self.search_config_file_by_path(i) + y_list.extend(y_list_tmp) + p_list.extend(p_list_tmp) + elif os.path.isfile(i): + if i.endswith(".yml") or i.endswith(".yaml"): + data = public.readFile(i) + if data: + y_list.append((i, data)) + elif i.endswith(".properties"): + data = public.readFile(i) + if data: + p_list.append((i, data)) + + for i, data in itertools.chain(y_list, p_list): + self.raw_data[i] = data + + return self.parse_application_yaml(y_list) + self.parse_application_properties(p_list) + + def app_config(self) -> Tuple[List[Tuple[str, dict]], List[Tuple[str, dict]]]: + """获取所有的配置信息, 并分为主配置和其他配置""" + self._parser_by_cmd_or_env() + if self.find_by_default: + all_config = self._get_all_config_by_default() + else: + all_config = self._get_all_config_by_location() + + mian_conf, other_conf = [], [] + main_name = [self.config_name + i for i in (".yml", ".yaml", ".properties")] + for i, conf in all_config: + file_name = os.path.basename(i) + if file_name in main_name: + mian_conf.append((i, conf)) + else: + other_conf.append((i, conf)) + + used_conf = mian_conf + self._get_flags_by_mian_conf(mian_conf) + + use_flag = [] + ned_del_other_conf = [] + if self.find_flag_list: + for idx, conf_data in enumerate(other_conf): + file_name = os.path.basename(conf_data[0]) + tmp_flag = file_name[len(self.config_name) + 1:].rsplit(".", 1)[0] + if tmp_flag in self.find_flag_list: + use_flag.append(tmp_flag) + used_conf.append(conf_data) + ned_del_other_conf.append(idx) + + self.not_used_flag_list = [i for i in self.find_flag_list if i not in use_flag] + + include_list = self._get_include_by_config_list(used_conf) + if include_list: + for idx, conf_data in enumerate(other_conf): + file_name = os.path.basename(conf_data[0]) + tmp_flag = file_name[len(self.config_name) + 1:].rsplit(".", 1)[0] + if tmp_flag in include_list: + used_conf.append(conf_data) + ned_del_other_conf.append(idx) + + for i in ned_del_other_conf: + del other_conf[i] + + cmd_or_env_data = self.get_cmd_or_env_config() + used_conf.append(("命令行或环境变量", cmd_or_env_data)) # 加到最后,优先级最高 + # public.print_log(used_conf) + return used_conf, other_conf + + def get_cmd_or_env_config(self) -> dict: + data = {} + if self.process_env: + for k, v in self.process_env.items(): + if not k.startswith("SPRING"): + continue + + key_list = [j.strip().lower() for j in k.split("_") if j.strip()] + node = data + for n in key_list[:-1]: + if n not in node: + node[n] = {} + node = node[n] + node[key_list[-1]] = v + + if self.cmd: + rep_spring = re.compile(r"-[-D]spring\.(?P\S*?)=(?P\S+)") + for i in rep_spring.finditer(self.cmd): + key = "spring." + i.group("key").lower() + value = i.group("value") + key_list = [j.strip() for j in key.split(".") if j.strip()] + node = data + for n in key_list[:-1]: + if n not in node: + node[n] = {} + node = node[n] + node[key_list[-1]] = value + + return data + + def _get_flags_by_mian_conf(self, mian_conf: List[Tuple[str, dict]]): + if self.find_flag_list: # 在命令和环境变量中找到了就不需要了 + return + + flags = "" + for i, conf in mian_conf: + tmp_f = conf.get("spring", {}).get("profiles", {}).get("active", None) # active 只有最后一个生效 + if isinstance(tmp_f, str): + flags = tmp_f + + if isinstance(flags, str) and flags: + self.find_flag_list.extend([i.strip() for i in flags.split(",") if i.strip()]) + + @staticmethod + def _get_include_by_config_list(config_list: List[Tuple[str, dict]]) -> List[str]: + include_list = [] + for i, conf in config_list: + flags = conf.get("spring", {}).get("profiles", {}).get("include", None) + if isinstance(flags, str): + include_list.extend([i.strip() for i in flags.split(",") if i.strip()]) + return include_list + + def get_jar_config(self, jar_file: str) -> Tuple[List[Tuple[str, AnyStr]], List[Tuple[str, AnyStr]]]: + """获取jar文件中的配置文件""" + if not os.path.exists(jar_file): + return [], [] + if not zipfile.is_zipfile(jar_file): # 判断是否为zip文件 + return [], [] + # 打开jar文件 + yaml_list = [] + prop_list = [] + with zipfile.ZipFile(jar_file, 'r') as jar: + for i in jar.namelist(): + # 查询所有文件中可能是配置文件的项目 + i_base_name = os.path.basename(i) + if i_base_name.find(self.config_name) == -1: + continue + + try: + if i_base_name.endswith(".yml") or i_base_name.endswith(".yaml"): + with jar.open(i) as f: + yaml_list.append((i, f.read())) + + if i.endswith(".properties"): + with jar.open(i) as f: + prop_list.append((i, f.read())) + except: + # public.print_log("压缩文件读取错误" + public.get_error_info()) + continue + + return yaml_list, prop_list + + # 检测是不是项目的目录 + # 暂不使用 + @staticmethod + def test_spring_boot_name(spring_path: str, target_name: str) -> bool: + if os.path.isfile(spring_path + "/pom.xml"): + data = public.readFile(spring_path + "/pom.xml") + if data: + start_idx = data.find("") + if start_idx != -1: + name = data[start_idx + 12: data.find("", start_idx)] + if target_name.startswith(name): + return True + + if os.path.isfile(spring_path + "/build.gradle"): + data = public.readFile(spring_path + "/build.gradle") + if data: + base_name_rep = re.compile(r'''archivesBaseName\s*=\s*['"]?(?P\S+)["']?''', re.M) + res = base_name_rep.search(data) + if res and target_name.startswith(res.group("name")): + return True + return False + + # 暂不使用 + def get_spring_project_src_by_path(self, path: str, target_name: str) -> Optional[str]: + """" + 尝试寻找项目源文件位置 + 寻找本层级、父层级、和两个子层级的spring boot项目目录 "src/main/resources"目录 和 "src/pom.xml" + """ + if not os.path.isdir(path): + return None + + parent_path = os.path.dirname(path) + if os.path.isdir(parent_path + "/src/main/resources"): + if self.test_spring_boot_name(parent_path, target_name): + return parent_path + + def _get_by_sub(inpt_path: str, limit_num: int) -> Optional[str]: + if limit_num < 0: + return None + + for i in os.listdir(inpt_path): + if i.startswith("."): + continue + p = os.path.join(inpt_path, i) + if os.path.isdir(p): + if os.path.isdir(p + "/src/main/resources"): + if self.test_spring_boot_name(p, target_name): + return p + if limit_num >= 1: + res = _get_by_sub(p, limit_num - 1) + if res is not None: + return res + return None + + return _get_by_sub(path, 2) + + # 暂不使用 + def get_custom_spring_config(self, path: str) -> Tuple[List[Tuple[str, AnyStr]], List[Tuple[str, AnyStr]]]: + """ + 尝试寻找外部配置文件 + 寻找本层级和子层级的spring boot配置文件 + """ + if not os.path.isdir(path): + return [], [] + + yaml_list, prop_list = [], [] + + def _get_by_sub(inpt_path: str, limit_num: int): + if limit_num < 0: + return None + y, p = self.search_config_file_by_path(inpt_path) + yaml_list.extend(y) + prop_list.extend(p) + + for i in os.listdir(inpt_path): + tmp_p = os.path.join(inpt_path, i) + if os.path.isdir(tmp_p): + _get_by_sub(tmp_p, limit_num - 1) + + _get_by_sub(path, 2) + return yaml_list, prop_list + + # 用于搜索外部配置文件 + def search_config_file_by_path(self, path: str) -> Tuple[List[Tuple[str, AnyStr]], List[Tuple[str, AnyStr]]]: + yaml_list, prop_list = [], [] + for i in os.listdir(path): + # 查询所有文件中可能是配置文件的项目 + p = os.path.join(path, i) + if i.find(self.config_name) != -1 and os.path.isfile(p): + if i.endswith(".yml") or i.endswith(".yaml"): + tmp_data = public.readFile(p) + if isinstance(tmp_data, str): + yaml_list.append((p, tmp_data)) + + if i.endswith(".properties"): + tmp_data = public.readFile(p) + if isinstance(tmp_data, str): + prop_list.append((p, tmp_data)) + + return yaml_list, prop_list + + @staticmethod + def to_utf8(file_data_list: List[Tuple[str, AnyStr]]) -> List[Tuple[str, str]]: + res_list = [] + for i, data in file_data_list: + if isinstance(data, bytes): + try: + new_data = data.decode("utf-8") + except: + continue + else: + res_list.append((i, new_data)) + return res_list + + # 合并两个配置文件 + @classmethod + def merge_dict_tries(cls, root: dict, node: dict): + # 合并字典树, 用于分档的yaml配置文件 + for k, v in node.items(): + if isinstance(v, dict): + if k not in root: + root[k] = {} + cls.merge_dict_tries(root[k], v) + else: + root[k] = v + + @classmethod + def parse_application_yaml(cls, conf_data_list: List[Tuple[str, AnyStr]]) -> List[Tuple[str, Dict]]: + res_list = [] + for i, data in conf_data_list: + try: + d = yaml.safe_load_all(data) + if isinstance(d, dict): + res_list.append((i, d)) + else: + tmp = {} + for j in d: + cls.merge_dict_tries(tmp, j) + res_list.append((i, tmp)) + except: + print("yaml解析错误", i) + continue + + return res_list + + @staticmethod + def _parse_application_properties(data: str) -> dict: + res_dict = {} + + # 添加时处理key + def add_to_res_dict(res_dict_data: dict, tmp_k: str, tmp_value: str): + key_list = tmp_k.split(".") + if len(key_list) == 1: + res_dict_data[key_list[0]] = tmp_value + return + + node = res_dict_data + for n in key_list[:-1]: + if n not in node: + node[n] = {} + node = node[n] + node[key_list[-1]] = tmp_value + + last_line = "" + for line in data.split("\n"): + line = line.lstrip() # type: str + if line.startswith("#"): + continue + if line.endswith("\\"): + last_line += line.rstrip("\\") + continue + else: + if last_line: + line = last_line + line + last_line = "" + if "=" in line: + k, v = line.split("=", 1) + tmp_v = v.strip() + + if "#" not in tmp_v and not tmp_v.startswith("'") and not tmp_v.startswith('"'): + add_to_res_dict(res_dict, k.strip(), tmp_v) + continue + + if (tmp_v.startswith("'") and tmp_v.endswith("'")) or (tmp_v.startswith('"') and tmp_v.endswith('"')): + value = tmp_v.strip("\"'") + add_to_res_dict(res_dict, k.strip(), value) + continue + + if tmp_v.startswith("#"): + continue + + last_idx = 0 + for _ in range(tmp_v.count("#")): + idx = tmp_v.find("#", last_idx + 1) + last_idx = idx + if tmp_v[idx - 1] != "\\": + break + + add_to_res_dict(res_dict, k.strip(), tmp_v[:last_idx].strip()) + + return res_dict + + @classmethod + def parse_application_properties(cls, conf_data_list: List[Tuple[str, AnyStr]]) -> List[Tuple[str, Dict]]: + res_list = [] + for i, data in conf_data_list: + try: + tmp = cls._parse_application_properties(data) + if tmp: + res_list.append((i, tmp)) + except: + print("properties解析错误", i) + continue + + return res_list + + def check_config_env(self, use_conf: List[Tuple[str, dict]]): + """ + 检测项目依赖的环境是否存在 + MySQL、Redis、RabbitMQ、Kafka、Elasticsearch、MongoDB + + Mysql检测用户是否存在 + """ + db_conf = {} + other_server_conf = {} + for i, spring_conf in use_conf: # 检查的同时根据配置路径即 .spring.data.redis + self.check_one_config_env(spring_conf, i, db_conf, other_server_conf) + + l_db, s_db = self.check_db_env(list(db_conf.values())) + l_service, s_service = self.check_service_env(list(other_server_conf.values())) + return l_db, s_db, l_service, s_service + + def check_one_config_env(self, + conf: dict, + file: str, + db_conf: Dict[str, dict] = None, + other_server_conf: Dict[str, dict] = None, + ) -> None: + + def _parse_keyword_nodes(node: dict) -> Tuple[str, str]: + """ + 形式1: 使用:分割 + # Kafka + spring.kafka.bootstrap-servers = localhost:9092 + # 该字段见 Kafka 安装包中的 consumer.proerties,可自行修改, 修改完毕后需要重启 Kafka + spring.kafka.consumer.group-id = test-consumer-group + spring.kafka.consumer.enable-auto-commit = true + spring.kafka.consumer.auto-commit-interval = 3000 + + 形式2: + spring.data.redis.init.database = 11 + spring.data.redis.init.host = localhost + spring.data.redis.init.port = 6379 + """ + host, port = "", "" + if "host" in node: + host = node["host"] + if "port" in node: + port = node["port"] + + if host and port: + return host, port + + for v in node.values(): + if isinstance(v, dict): + host, port = _parse_keyword_nodes(v) + if host and port: + return host, port + + elif isinstance(v, str): + if ":" not in v: + continue + res = self.rep_host_port.search(v) + if res: + return res.group("host"), res.group("port") + return "", "" + + def traversal_all_node(node: dict, last_key: str): + for k, v in node.items(): + if not isinstance(v, dict): # 如果子层级不是字典,不在检查 + continue + # 检查是不是一个数据库配置, 如果是就不必要检查子层级 + if "url" in v and isinstance(v["url"], str): + res = self.rep_jdbc_url.search(v["url"]) + if res: + tmp = { + "url": v["url"], + "db": res.group("db"), + "host": res.group("host"), + "port": res.group("port"), + "name": res.group("name"), + "file": file, + } + if last_key + "." + k in db_conf: + db_conf[last_key + "." + k].update(tmp) + if "password" in v: + db_conf[last_key + "." + k]["password"] = v["password"] + if "username" in v: + db_conf[last_key + "." + k]["username"] = v["username"] + else: + tmp["password"] = v.get("password", "") + tmp["username"] = v.get("username", "") + db_conf[last_key + "." + k] = tmp + continue + + # 检测关键字 + if k in self.other_server_keywords: + host, port = _parse_keyword_nodes(v) + if host and port: + tmp = { + "key": k, + "host": host, + "port": port, + "file": file, + } + if last_key + "." + k in other_server_conf: + other_server_conf[last_key + "." + k].update(tmp) + else: + other_server_conf[last_key + "." + k] = tmp + continue + + # 不是一个数据库配置, 检查子层级 + traversal_all_node(v, last_key + "." + k) + + traversal_all_node(conf, "") + + # 处理 ${XXX} 格式的名称 + for i in db_conf.values(): + for tmp_k, tmp_v in i.items(): + if isinstance(tmp_v, str) and tmp_v.startswith("${") and tmp_v.endswith("}"): + tmp_v_list = tmp_v[2:-1].split(".") + tmp_node = conf + for j in tmp_v_list[:-1]: + tmp_node = tmp_node.get(j, {}) + i[tmp_k] = tmp_node.get(tmp_v_list[-1], "") + + return None + + def check_db_env(self, db_conf: List[dict]): + local_conf = [] + server_conf = [] + for i in db_conf: + i["database"] = True # 远程数据库默认不检查 + i["listening"] = True + i["auth"] = True + if i["host"] in self.localhost_key: + i["is_local"] = True + local_conf.append(i) + if i["db"] in ("mysql", "postgresql", "mongodb"): + i["database"], i["auth"] = self.has_database(i["db"], i["name"], i["username"], i["password"]) + + i["listening"] = False + try: + i["listening"] = utils.check_port_with_net_connections(int(i["port"])) is False + except: + pass + + else: + i["is_local"] = False + i["listening"] = self.port_is_open(i["host"], i["port"]) + server_conf.append(i) + + return local_conf, server_conf + + def check_service_env(self, service_conf: List[dict]): + local_conf = [] + server_conf = [] + for i in service_conf: + i["listening"] = True # 远程数据库默认不检查 + if i["host"] in self.localhost_key: + i["is_local"] = True + local_conf.append(i) + i["listening"] = False + try: + i["listening"] = utils.check_port_with_net_connections(int(i["port"])) is False + except: + pass + + else: + i["is_local"] = False + i["listening"] = self.port_is_open(i["host"], i["port"]) + server_conf.append(i) + + return local_conf, server_conf + + @staticmethod + def port_is_open(host: str, port): + import socket + try: + + # 创建一个socket对象 + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # 设置超时时间,防止连接挂起 + sock.settimeout(5) # 5秒超时 + # 尝试连接到指定的端口 + result = sock.connect_ex((host, int(port))) + # 关闭socket + sock.close() + # 如果连接成功,connect_ex返回0,否则返回错误代码 + return result == 0 + except socket.error as e: + print(f"Socket error: {e}") + return False + + def has_database(self, db_type: str, db_name: str, user_name: str, password: str) -> Tuple[bool, bool]: + """ + 返回数据库是否存在, 和数据库密码是否正确 + """ + if self.my_sql_root_auth is None and db_type == "mysql": + try: + self.my_sql_root_auth = public.M("config").where("id=1", ()).find()["mysql_root"] + except: + self.my_sql_root_auth = False + try: + find = public.M("databases").where("name=? AND LOWER(type)=?", (db_name, db_type.lower())).find() + if find: + if find["username"] == user_name and find["password"] == password: + return True, True # 数据库和密码都存在 + if user_name == "root" and db_type == "mysql": + if self.my_sql_root_auth == password or self.my_sql_root_auth is False: + return True, True # 数据库存在, 用的root密码 + return True, False # 数据库存在, 密码未知 + else: + if user_name == "root" and db_type == "mysql" and \ + (self.my_sql_root_auth == password or self.my_sql_root_auth is False): + + return False, True # 数据库不存在, 用的root密码 + return False, False + except: + return False, False + + # 被外部调用的函数 + def get_tip(self) -> List[dict]: + use, other = self.app_config() + l_db, s_db, l_service, s_service = self.check_config_env(use) + res = [] + # error 大概率存在错误的 + # 配置文件路径不存在 + if self.location_tip_list: + res.extend(self.location_tip_list) + + # 本地数据库不存在 + for i in l_db: + if i["listening"] is False: + res.append({ + "type": "local_database", + "level": "error", + "msg": "链接本地{}数据库的端口{}未被监听,可能是配置错误或数据库服务未开启".format(i["db"], i["port"]) + }) + + # 本地数据连接不上 + for i in l_db: + if i["auth"] is False: + res.append({ + "type": "local_database_auth", + "level": "warn", + "file": i["file"], + "msg": "链接到本地的{}数据库【{}】的用户名【{}】或密码【{}】可能错误".format( + i["db"], i["name"], i["username"], i["password"]) + }) + + # warn 可能存在错误的 + # 本地数据库中没有数据数据库 + if l_db: + for i in l_db: + if i["database"] is False: + res.append({ + "type": "local_database", + "level": "warn", + "file": i["file"], + "msg": "本地的{}数据库服务中未检测到数据库【{}】".format(i["db"], i["name"]) + }) + # 本地服务未监听端口 + if l_service: + for i in l_service: + if i["listening"] is False: + res.append({ + "type": "local_service", + "level": "warn", + "file": i["file"], + "msg": "链接本地【{}服务】的端口{}未被监听,可能是配置错误或服务未开启".format(i["key"], i["port"]) + }) + + # tip 有较低可能得存在的问题 + # 远程服务 或 远程数据库 + + if s_db or s_service: + for i in itertools.chain(s_db, s_service): + if i["listening"] is False: + if "db" in i: + msg = "链接远程{}数据库服务【{}:{}/{}】,可能存在问题".format( + i["db"], i["host"], i["port"], i["name"] + ) + else: + msg = "链接远程{}服务【{}:{}】,可能存在问题".format(i["key"], i["host"], i["port"]) + res.append({ + "type": "service", + "level": "tip", + "file": i["file"], + "msg": msg + }) + + if self.not_used_flag_list: + res.append({ + "type": "spring_profiles", + "level": "tip", + "file": None, + "msg": "配置文件中提及了这些profiles ->【{}】,但实际未找到相关配置文件".format(",".join(self.not_used_flag_list)) + }) + + return res + + +class SpringLogConfigParser(SpringConfigParser): + """ + 日志解析部分,主要依靠在配置中指定的logging.config + """ + + @staticmethod + def get_config_data_by_use(use: List[Tuple[str, dict]], key: str) -> Optional[str]: + key_list = [i for i in key.split(".") if i] + if len(key_list) == 0: + return None + + res_data = None + for _, config in use: + tmp_config = config + for k in key_list: + if k not in tmp_config: + break + tmp_config = tmp_config[k] + else: + # 如果没有退出表示上面的key依序存在,且最后一次取出来的就是所需要的值 + res_data = tmp_config + + if not res_data or not isinstance(res_data, str): + return None + + return res_data + + # 获取jar包内文件 + @staticmethod + def get_jar_file_path(jar_file: str, jar_file_path: str) -> str: + if not zipfile.is_zipfile(jar_file): # 判断是否为zip文件 + return "" + # 打开jar文件 + data = b'' + with zipfile.ZipFile(jar_file, 'r') as jar: + if jar_file_path in jar.namelist(): + with jar.open(jar_file_path) as f: + data = f.read() + + if isinstance(data, bytes): + try: + return data.decode("utf-8") + except: + pass + elif isinstance(data, str): + return data + + return "" + + def get_all_log_ptah(self) -> List[str]: + use, other = self.app_config() + logging_config_data = self.get_config_data_by_use(use, "logging.config") + if not logging_config_data: + return [] + if not logging_config_data.endswith(".xml"): + return [] + if logging_config_data.startswith("classpath:"): + jar_file_path = "BOOT-INF/classes/{}".format(logging_config_data[10:].lstrip("/")) + file_data = self.get_jar_file_path(self.jar_path, jar_file_path) + filename = os.path.basename(jar_file_path) + else: + if logging_config_data.startswith("file:"): + logging_config_data = logging_config_data[5:] + file_data = public.readFile(logging_config_data) + filename = os.path.basename(logging_config_data) + + if not file_data or not isinstance(file_data, str): + return [] + + # 闭包处理获取信息的过程 + def get_config(key: str) -> Optional[str]: + return self.get_config_data_by_use(use, key) + if filename.find("logback") != -1: + return self.logback_parser(file_data, get_config) + elif filename.find("log4j") != -1: + return self.log4j_parser(file_data) + elif file_data.find("rollingPolicy") != -1: + return self.logback_parser(file_data, get_config) + elif file_data.find("RollingFile") != -1: + return self.log4j_parser(file_data) + + return [] + + # 从property标签加属性获取是通用的 + @staticmethod + def __update_property_by_attr(file_data: str, property_dict: dict): + rep_property = re.compile(r'''<[pP]roperty.*?/([pP]roperty)?>''') + rep_attr = re.compile(r'''(?P\S+)=['"](?P.*?)['"]''') + + for tmp in rep_property.finditer(file_data): + tmp_dict = {} + for attr in rep_attr.finditer(tmp.group()): + tmp_dict[attr.group("k")] = attr.group("v") + if "name" in tmp_dict and "value" in tmp_dict: + property_dict[tmp_dict["name"]] = tmp_dict["value"] + + def logback_parser(self, file_data: str, get_config: Callable[[str], Optional[str]]) -> List[str]: + property_dict = dict() + rep_spring_property = re.compile(r'''''') + rep_attr = re.compile(r'''(?P\S+)=['"](?P.*?)['"]''') + rep_file_name_pattern = re.compile(r"(?P.*?)[^}]*)}") + + for tmp in rep_spring_property.finditer(file_data): + tmp_dict = {} + for attr in rep_attr.finditer(tmp.group()): + tmp_dict[attr.group("k")] = attr.group("v") + if "name" in tmp_dict and "source" in tmp_dict: + source_data = get_config(tmp_dict["source"]) + if source_data: + property_dict[tmp_dict["name"]] = source_data + + self.__update_property_by_attr(file_data, property_dict) + + res_list = [] + for tmp in rep_file_name_pattern.finditer(file_data): + tmp_data = tmp.group("file_name_pattern") + for var in rep_var.finditer(tmp_data): + if var.group("var") in property_dict: + tmp_data = tmp_data.replace(var.group(), property_dict[var.group("var")]) + res_list.append(tmp_data) + return self.__normalize_res_list(res_list) + + def log4j_parser(self, file_data: str) -> List[str]: + property_dict = dict() + rep_property = re.compile(r'<[pP]roperty.*?>(?P.*?).+?)['"]''') + rep_file_name = re.compile(r'''<[Rr]ollingFile.*fileName=['"](?P[^'"]*?)['"](.|\n)*?>''') + rep_var = re.compile(r"\$\{(?P[^}]*)}") + + for tmp in rep_property.finditer(file_data): + tmp_search = rep_name_attr.search(tmp.group()) + if tmp_search: + property_dict[tmp_search.group("value")] = tmp.group("prop") + + self.__update_property_by_attr(file_data, property_dict) + + res_list = [] + for tmp in rep_file_name.finditer(file_data): + tmp_data = tmp.group("file_name") + for var in rep_var.finditer(tmp_data): + var_str = var.group("var") + if var_str in property_dict: + tmp_data = tmp_data.replace(var.group(), property_dict[var_str]) + + res_list.append(tmp_data) + + return self.__normalize_res_list(res_list) + + def __normalize_res_list(self, res_list: List[str]) -> List[str]: + res = [] + for i in res_list: + file_path = os.path.dirname(i) + if not file_path.startswith("/"): + file_path = os.path.abspath(os.path.join(self.pwd, file_path)) + # 可能还有占位符 + if os.path.basename(file_path).find("%") != -1 and not os.path.isdir(file_path): + file_path = os.path.dirname(file_path) + + if os.path.isdir(file_path) and file_path not in res: + res.append(file_path) + return res diff --git a/mod/project/java/utils.py b/mod/project/java/utils.py new file mode 100644 index 00000000..6c09fc0d --- /dev/null +++ b/mod/project/java/utils.py @@ -0,0 +1,946 @@ +import json +import re +import sys +import time +import zipfile +import os +import yaml +import psutil +import platform +from xml.etree.ElementTree import Element, ElementTree, parse, XMLParser +from typing import Optional, Dict, Tuple, AnyStr, List, Any +import threading +import itertools + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +import public + + +def get_jar_war_config(jar_war_file: str) -> Optional[List[Tuple[str, AnyStr]]]: + """获取jar文件中的配置文件""" + if not os.path.exists(jar_war_file): + return None + if not zipfile.is_zipfile(jar_war_file): # 判断是否为zip文件 + return None + # 打开jar文件 + res_list = [] + with zipfile.ZipFile(jar_war_file, 'r') as jar: + for i in jar.namelist(): + # 查询所有文件中可能是配置文件的项目 + if i.endswith("application.yaml") or i.endswith("application.yml"): + with jar.open(i) as f: + res_list.append((i, f.read())) + + if not res_list: + return None + + return res_list + + +def to_utf8(file_data_list: List[Tuple[str, AnyStr]]) -> List[Tuple[str, str]]: + res_list = [] + for i, data in file_data_list: + if isinstance(data, bytes): + try: + new_data = data.decode("utf-8") + except: + continue + else: + res_list.append((i, new_data)) + return res_list + + +def parse_application_yaml(conf_data_list: List[Tuple[str, AnyStr]]) -> List[Tuple[str, Dict]]: + res_list = [] + for i, data in conf_data_list: + d = yaml.safe_load(data) + if isinstance(d, dict): + res_list.append((i, d)) + + return res_list + + +# 接收一个jdk路径并将其规范化 +def normalize_jdk_path(jdk_path: str) -> Optional[str]: + if jdk_path.endswith("/java"): + jdk_path = os.path.dirname(jdk_path) + if jdk_path.endswith("/bin"): + jdk_path = os.path.dirname(jdk_path) + if jdk_path.endswith("/jre"): + jdk_path = os.path.dirname(jdk_path) + if not os.path.isdir(jdk_path): + return None + if not os.path.exists(os.path.join(jdk_path, "bin/java")): + return None + return jdk_path + + +def test_jdk(jdk_path: str) -> bool: + java_bin = os.path.join(jdk_path, "bin/java") + if os.path.exists(java_bin): + out, err = public.ExecShell("{} -version 2>&1".format(java_bin)) # type: str, str + if out.lower().find("version") != -1: + return True + return False + + +class TomCat: + + def __init__(self, tomcat_path: str): + self.path = tomcat_path.rstrip("/") # 移除多余的右"/" 统一管理 + self._jdk_path: Optional[str] = None + self._config_xml: Optional[ElementTree] = None + self._bt_tomcat_conf: Optional[dict] = None + self._log_file = None + self._version = None + + @property + def jdk_path(self) -> Optional[str]: + p = os.path.join(self.path, "bin/daemon.sh") + if not os.path.exists(p): + return None + + tmp_data = public.readFile(p) + if isinstance(tmp_data, str): + rep_deemon_sh = re.compile(r"^JAVA_HOME=(?P.*)\n", re.M) + re_res_jdk_path = rep_deemon_sh.search(tmp_data) + if re_res_jdk_path: + self._jdk_path = re_res_jdk_path.group("path").strip() + self._jdk_path = normalize_jdk_path(self._jdk_path) + return self._jdk_path + + return None + + def version(self) -> Optional[int]: + if isinstance(self._version, int): + return self._version + v_file = os.path.join(self.path, "version.pl") + if os.path.isfile(v_file): + ver = public.readFile(v_file) + if isinstance(ver, str): + try: + ver_int = int(ver.split(".")[0]) + self._version = ver_int + return self._version + except: + pass + return None + + @property + def log_file(self) -> str: + if self._log_file is not None: + return self._log_file + default_file = os.path.join(self.path, "logs/catalina-daemon.out") + target_sh = os.path.join(self.path, "bin/daemon.sh") + file_data = public.readFile(target_sh) + conf_path = os.path.join(self.path, "conf/logpath.conf") + if not isinstance(file_data, str): + return default_file + rep = re.compile(r'''\n\s?test ?"\.\$CATALINA_OUT" ?= ?\. +&& +CATALINA_OUT=['"](?P\S+)['"]''') + if rep.search(file_data): + self._log_file = rep.search(file_data).group("path") + public.writeFile(conf_path, os.path.dirname(self._log_file)) + return self._log_file + + if os.path.isfile(conf_path): + path = public.readFile(conf_path) + else: + return default_file + log_file = os.path.join(path, "catalina-daemon.out") + if os.path.exists(log_file): + self._log_file = log_file + return self._log_file + + ver = self.version() + if ver: + log_file = os.path.join(path, "catalina-daemon-{}.out".format(ver)) + return log_file + else: + return os.path.join(path, "catalina-daemon.out") + + @property + def bt_tomcat_conf(self) -> Optional[dict]: + if self._bt_tomcat_conf is None: + p = os.path.join(self.path, "bt_tomcat.json") + if not os.path.exists(p): + self._bt_tomcat_conf = {} + return self._bt_tomcat_conf + try: + self._bt_tomcat_conf = json.loads(public.readFile(p)) + except: + self._bt_tomcat_conf = {} + return self._bt_tomcat_conf + + def save_bt_tomcat_conf(self): + if self._bt_tomcat_conf is not None: + p = os.path.join(self.path, "bt_tomcat.json") + public.writeFile(p, json.dumps(self._bt_tomcat_conf)) + + def change_log_path(self, log_path: str, prefix: str = "") -> bool: + log_path = log_path.rstrip("/") + target_sh = os.path.join(self.path, "bin/daemon.sh") + if not os.path.exists(target_sh): + return False + file_data = public.readFile(target_sh) + if not isinstance(file_data, str): + return False + rep = re.compile(r'''\n ?test ?"\.\$CATALINA_OUT" ?= ?\. && {0,3}CATALINA_OUT="[^\n]*"[^\n]*\n''') + if prefix and not prefix.startswith("-"): + prefix = "-{}".format(prefix) + repl = '\ntest ".$CATALINA_OUT" = . && CATALINA_OUT="{}/catalina-daemon{}.out"\n'.format(log_path, prefix) + file_data = rep.sub(repl, file_data) + public.writeFile(target_sh, file_data) + conf_path = os.path.join(self.path, "conf/logpath.conf") + public.WriteFile(conf_path, log_path) + return True + + @property + def config_xml(self) -> Optional[ElementTree]: + if self._config_xml is None: + p = os.path.join(self.path, "conf/server.xml") + if not os.path.exists(p): + return None + + self._config_xml = parse(p, parser=XMLParser(encoding="utf-8")) + return self._config_xml + + def set_port(self, port: int) -> bool: + if self.config_xml is None: + return False + conf_elem = self.config_xml.findall("Service/Connector") + if conf_elem is None: + return False + for i in conf_elem: + if 'protocol' in i.attrib and 'port' in i.attrib: + if i.attrib['protocol'] == 'HTTP/1.1': + i.attrib['port'] = str(port) + return True + return False + + def pid(self) -> Optional[int]: + pid_file = os.path.join(self.path, 'logs/catalina-daemon.pid') + if os.path.exists(pid_file): + # 使用psutil判断进程是否在运行 + try: + pid = public.readFile(pid_file) + return int(pid) + except: + return None + return None + + def port(self) -> int: + if self.config_xml is None: + return 0 + for i in self.config_xml.findall("Service/Connector"): + if i.attrib.get("protocol") == "HTTP/1.1" and 'port' in i.attrib: + return int(i.attrib.get("port")) + return 8080 + + @property + def installed(self) -> bool: + start_path = os.path.join(self.path, 'bin/daemon.sh') + conf_path = os.path.join(self.path, 'conf/server.xml') + if not os.path.exists(self.path): + return False + if not os.path.isfile(start_path): + return False + if not os.path.isfile(conf_path): + return False + return True + + def running(self) -> bool: + pid = self.pid() + if pid: + try: + p = psutil.Process(pid) + return p.is_running() + except: + return False + return False + + def status(self) -> dict: + return { + "status": os.path.exists(self.path) and os.path.exists(os.path.join(self.path, "bin/daemon.sh")), + "jdk_path": self.jdk_path, + "path": self.path, + "running": self.running(), + "port": self.port(), + "stype": "built" if os.path.exists(os.path.join(self.path, "conf/server.xml")) else "uninstall" + } + + def save_config_xml(self) -> bool: + if self.config_xml is None: + return False + p = os.path.join(self.path, "conf/server.xml") + + def _indent(elem: Element, level=0): + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + _indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + _indent(self.config_xml.getroot()) + self.config_xml.write(p, encoding="utf-8", xml_declaration=True) + return True + + def host_by_name(self, name: str) -> Optional[Element]: + if self.config_xml is None: + return None + engines = self.config_xml.findall("Service/Engine") + if not engines: + return None + engine = engines[0] + for h in engine: + if h.tag == "Host" and h.attrib.get("name", None) == name: + return h + return None + + def add_host(self, name: str, path: str) -> bool: + if self.config_xml is None: + return False + if not os.path.exists(path): + os.makedirs(path) + if self.host_by_name(name): + return False + engine = self.config_xml.findall("Service/Engine") + if not engine: + return False + path_name = "" + if os.path.isfile(path): + app_base = os.path.dirname(path) + if path.endswith(".war"): + path_name = os.path.basename(path).rsplit(".", 1)[0] + else: + app_base = path + + host = Element("Host", attrib={ + "appBase": app_base, + "autoDeploy": "true", + "name": name, + "unpackWARs": "true", + "xmlNamespaceAware": "false", + "xmlValidation": "false", + }) + + context = Element("Context", attrib={ + "docBase": path, + "path": path_name, + "reloadable": "true", + "crossContext": "true", + }) + host.append(context) + + engine[0].append(host) + return True + + def set_host_path_by_name(self, name: str, path: str) -> bool: + if self.config_xml is None: + return False + for i in self.config_xml.findall("Service/Engine/Host"): + if i.attrib.get("name", None) != name: + continue + for j in i: + if j.tag == "Context": + j.attrib["docBase"] = path + return True + return False + + def remove_host(self, name: str) -> bool: + if self.config_xml is None: + return False + target_host = self.host_by_name(name) + if not target_host: + return False + engine = self.config_xml.findall("Service/Engine") + if not engine: + return False + engine[0].remove(target_host) + return True + + def mutil_remove_host(self, name_list: List[str]) -> bool: + if self.config_xml is None: + return False + for name in name_list: + self.remove_host(name) + return False + + def start(self, by_user: str = "root") -> bool: + if not self.running(): + daemon_file = os.path.join(self.path, "bin/daemon.sh") + if not os.path.isfile(self.log_file): + public.ExecShell("touch {}".format(self.log_file)) + public.ExecShell("chown {}:{} {}".format(by_user, by_user, self.log_file)) + public.ExecShell("bash {} start".format(daemon_file), user=by_user) + + return self.running() + + def stop(self) -> bool: + if self.running(): + daemon_file = os.path.join(self.path, "bin/daemon.sh") + public.ExecShell("bash {} stop".format(daemon_file)) + return not self.running() + + def restart(self, by_user: str = "root") -> bool: + daemon_file = os.path.join(self.path, "bin/daemon.sh") + if self.running(): + public.ExecShell("bash {} stop".format(daemon_file)) + if not os.path.isfile(self.log_file): + public.ExecShell("touch {}".format(self.log_file)) + public.ExecShell("chown {}:{} {}".format(by_user, by_user, self.log_file)) + public.ExecShell("bash {} start".format(daemon_file), user=by_user) + return self.running() + + def replace_jdk(self, jdk_path: str) -> Optional[str]: + jdk_path = normalize_jdk_path(jdk_path) + if not jdk_path: + return "jdk路径错误或无法识别" + + deemon_sh_path = "{}/bin/daemon.sh".format(self.path) + if not os.path.isfile(deemon_sh_path): + return 'Tomcat启动文件丢失!' + + deemon_sh_data = public.readFile(deemon_sh_path) + if not isinstance(deemon_sh_data, str): + return 'Tomcat启动文件读取失败!' + + # deemon_sh + rep_deemon_sh = re.compile(r"^JAVA_HOME=(?P.*)\n", re.M) + re_res_deemon_sh = rep_deemon_sh.search(deemon_sh_data) + if not re_res_deemon_sh: + return 'Tomcat启动文件解析失败!' + + jsvc_make_path = None + for i in os.listdir(self.path + "/bin"): + tmp_dir = "{}/bin/{}".format(self.path, i) + if i.startswith("commons-daemon") and os.path.isdir(tmp_dir): + make_path = tmp_dir + "/unix" + if os.path.isdir(make_path): + jsvc_make_path = make_path + break + + if jsvc_make_path is None: + return 'Jsvc文件丢失!' + + # 重装jsvc + if os.path.isfile(self.path + "/bin/jsvc"): + os.rename(self.path + "/bin/jsvc", self.path + "/bin/jsvc_back") + + if os.path.isfile(jsvc_make_path + "/jsvc"): + os.remove(jsvc_make_path + "/jsvc") + + shell_str = r''' +cd {} +make clean +./configure --with-java={} +make + '''.format(jsvc_make_path, jdk_path) + public.ExecShell(shell_str) + if os.path.isfile(jsvc_make_path + "/jsvc"): + os.rename(jsvc_make_path + "/jsvc", self.path + "/bin/jsvc") + public.ExecShell("chmod +x {}/bin/jsvc".format(self.path)) + os.remove(self.path + "/bin/jsvc_back") + else: + os.rename(self.path + "/bin/jsvc_back", self.path + "/bin/jsvc") + return 'Jsvc编译失败!' + + new_deemon_sh_data = deemon_sh_data[:re_res_deemon_sh.start()] + ( + 'JAVA_HOME={}\n'.format(jdk_path) + ) + deemon_sh_data[re_res_deemon_sh.end():] + public.writeFile(deemon_sh_path, new_deemon_sh_data) + return None + + def reset_tomcat_server_config(self, port: int): + ret = ''' + + + + + + + + + + + + + + + + + + + +'''.format(create_a_not_used_port(), port) + public.WriteFile(self.path + '/conf/server.xml', ret) + + @staticmethod + def _get_os_version() -> str: + # 获取Centos + if os.path.exists('/usr/bin/yum') and os.path.exists('/etc/yum.conf'): + return 'Centos' + # 获取Ubuntu + if os.path.exists('/usr/bin/apt-get') and os.path.exists('/usr/bin/dpkg'): + return 'Ubuntu' + return 'Unknown' + + @classmethod + def async_install_tomcat_new(cls, version: str, jdk_path: Optional[str]) -> Optional[str]: + os_ver = cls._get_os_version() + if version == "7" and os_ver == 'Ubuntu': + return '操作系统不支持!' + + if jdk_path: + jdk_path = normalize_jdk_path(jdk_path) + if not jdk_path: + return 'jdk路径错误或无法识别' + if not test_jdk(jdk_path): + return '指定的jdk不可用' + + if not jdk_path: + jdk_path = '' + + shell_str = ( + 'rm -rf /tmp/1.sh && ' + '/usr/local/curl/bin/curl -o /tmp/1.sh %s/install/src/webserver/shell/new_jdk.sh && ' + 'bash /tmp/1.sh install %s %s' + ) % (public.get_url(), version, jdk_path) + + if not os.path.exists("/tmp/panelTask.pl"): # 如果当前任务队列并未执行,就把日志清空 + public.writeFile('/tmp/panelExec.log', '') + soft_name = "Java项目Tomcat-" + version + task_id = public.M('tasks').add( + 'id,name,type,status,addtime,execstr', + (None, '安装[{}]'.format(soft_name), 'execshell', '0', time.strftime('%Y-%m-%d %H:%M:%S'), shell_str)) + + cls._create_install_wait_msg(task_id, version) + + @staticmethod + def _create_install_wait_msg(task_id: int, version: str): + from panel_msg.msg_file import message_mgr + + file_path = "/tmp/panelExec.log" + if not os.path.exists(file_path): + public.writeFile(file_path, "") + + soft_name = "Java项目Tomcat-" + version + data = { + "soft_name": soft_name, + "install_status": "等待安装" + soft_name, + "file_name": file_path, + "self_type": "soft_install", + "status": 0, + "task_id": task_id + } + title = "等待安装" + soft_name + res = message_mgr.collect_message(title, ["Java环境管理", soft_name], data) + if isinstance(res, str): + public.WriteLog("消息盒子", "安装信息收集失败") + return None + return res + + +def bt_tomcat(ver: int) -> Optional[TomCat]: + if ver not in (7, 8, 9, 10) and ver not in ("7", "8", "9", "10"): + return None + return TomCat(tomcat_path="/usr/local/bttomcat/tomcat%d" % int(ver)) + + +def site_tomcat(site_name: str) -> Optional[TomCat]: + tomcat_path = os.path.join("/www/server/bt_tomcat_web", site_name) + if not os.path.exists(tomcat_path): + return None + return TomCat(tomcat_path=tomcat_path) + + +class JDKManager: + + def __init__(self): + self._versions_list: Optional[List[str]] = None + self._custom_jdk_list: Optional[List[str]] = None + self._jdk_path = "/www/server/java" + self._custom_file = "/www/server/panel/data/get_local_jdk.json" + if not os.path.exists(self._jdk_path): + os.makedirs(self._jdk_path, 0o755) + + @property + def versions_list(self) -> List[str]: + if self._versions_list: + return self._versions_list + jdk_json_file = '/www/server/panel/data/jdk.json' + tip_file = '/www/server/panel/data/jdk.json.pl' + try: + last_refresh = int(public.readFile(tip_file)) + except ValueError: + last_refresh = 0 + versions_data = public.readFile(jdk_json_file) + if time.time() - last_refresh > 3600: + public.run_thread(public.downloadFile, ('{}/src/jdk/jdk.json'.format(public.get_url()), jdk_json_file)) + public.writeFile(tip_file, str(int(time.time()))) + + try: + versions = json.loads(versions_data) + except Exception: + versions = { + "x64": [ + "jdk1.7.0_80", "jdk1.8.0_371", "jdk-9.0.4", "jdk-10.0.2", + "jdk-11.0.19", "jdk-12.0.2", "jdk-13.0.2", "jdk-14.0.2", + "jdk-15.0.2", "jdk-16.0.2", "jdk-17.0.8", "jdk-18.0.2.1", + "jdk-19.0.2", "jdk-20.0.2" + ], + "arm": [ + "jdk1.8.0_371", "jdk-11.0.19", "jdk-15.0.2", "jdk-16.0.2", + "jdk-17.0.8", "jdk-18.0.2.1", "jdk-19.0.2", "jdk-20.0.2" + ], + "loongarch64": [ + "jdk-8.1.18", "jdk-11.0.22", "jdk-17.0.10", "jdk-21.0.2" + ] + } + arch = platform.machine() + if arch == "aarch64" or 'arm' in arch: + arch = "arm" + elif arch == "loongarch64": + arch = "loongarch64" + elif arch == "x86_64": + arch = "x64" + + self._versions_list = versions.get(arch, []) + return self._versions_list + + def jdk_list_path(self) -> List[str]: + return ["{}/{}".format(self._jdk_path, i) for i in self.versions_list] + + @property + def custom_jdk_list(self) -> List[str]: + if self._custom_jdk_list: + return self._custom_jdk_list + + try: + self._custom_jdk_list = json.loads(public.readFile(self._custom_file)) + except: + self._custom_jdk_list = [] + + if not isinstance(self._custom_jdk_list, list): + self._custom_jdk_list = [] + + return self._custom_jdk_list + + def add_custom_jdk(self, jdk_path: str) -> Optional[str]: + jdk_path = normalize_jdk_path(jdk_path) + if not jdk_path: + return "jdk路径错误或无法识别" + + if jdk_path in self.custom_jdk_list or jdk_path in self.jdk_list_path: + return + + self.custom_jdk_list.append(jdk_path) + public.writeFile(self._custom_file, json.dumps(self.custom_jdk_list)) + + def remove_custom_jdk(self, jdk_path: str) -> None: + if jdk_path not in self.custom_jdk_list: + return + + self.custom_jdk_list.remove(jdk_path) + public.writeFile(self._custom_file, json.dumps(self.custom_jdk_list)) + + def async_install_jdk(self, version: str) -> None: + sh_str = "cd /www/server/panel/install && /bin/bash install_soft.sh {} install {} {}".format(0, 'jdk', version) + + if not os.path.exists("/tmp/panelTask.pl"): # 如果当前任务队列并未执行,就把日志清空 + public.writeFile('/tmp/panelExec.log', '') + task_id = public.M('tasks').add( + 'id,name,type,status,addtime,execstr', + (None, '安装[{}]'.format(version), 'execshell', '0', time.strftime('%Y-%m-%d %H:%M:%S'), sh_str)) + + self._create_install_wait_msg(task_id, version) + + @staticmethod + def _create_install_wait_msg(task_id: int, version: str): + from panel_msg.msg_file import message_mgr + + file_path = "/tmp/panelExec.log" + if not os.path.exists(file_path): + public.writeFile(file_path, "") + + data = { + "soft_name": version, + "install_status": "等待安装" + version, + "file_name": file_path, + "self_type": "soft_install", + "status": 0, + "task_id": task_id + } + title = "等待安装" + version + res = message_mgr.collect_message(title, ["Java环境管理", version], data) + if isinstance(res, str): + public.WriteLog("消息盒子", "安装信息收集失败") + return None + return res + + def install_jdk(self, version: str) -> Optional[str]: + if version not in self.versions_list: + return "版本不存在, 无法安装" + + if os.path.exists(self._jdk_path + "/" + version): + return "已存在的版本, 无法再次安装,如需再次安装请先卸载" + + if os.path.exists("{}/{}.pl".format(self._jdk_path, version)): + return "安装任务进行中,请勿再次添加" + + public.writeFile("{}/{}.pl".format(self._jdk_path, version), "installing") + t = threading.Thread(target=self._install_jdk, args=(version,)) + t.start() + return None + + def _install_jdk(self, version: str) -> None: + try: + log_file = "{}/{}_install.log".format(self._jdk_path, version) + if not os.path.exists('/www/server/panel/install/jdk.sh'): + public.ExecShell('wget -O /www/server/panel/install/jdk.sh ' + public.get_url() + '/install/0/jdk.sh') + public.ExecShell('bash /www/server/panel/install/jdk.sh install {} 2>&1 > {}'.format(version, log_file)) + except: + pass + public.ExecShell('rm -rf /www/server/java/{}.*'.format(version)) + + def uninstall_jdk(self, version: str) -> Optional[str]: + if not os.path.exists(self._jdk_path + "/" + version): + return "没有安装指定的版本,无法卸载" + public.ExecShell('rm -rf /www/server/java/{}*'.format(version)) + return + + @staticmethod + def set_jdk_env(jdk_path) -> Optional[str]: + if jdk_path != "": + jdk_path = normalize_jdk_path(jdk_path) + if not jdk_path: + return "jdk路径错误或无法识别" + + # 写入全局的shell配置文件 + profile_path = '/etc/profile' + java_home_line = "export JAVA_HOME={}".format(jdk_path) if jdk_path else "" + path_line = "export PATH=$JAVA_HOME/bin:$PATH" + profile_data = public.readFile(profile_path) + if not isinstance(profile_data, str): + return "无法读取环境变量文件" + + rep_java_home = re.compile(r"export\s+JAVA_HOME=.*\n") + rep_path = re.compile(r"export\s+PATH=\$JAVA_HOME/bin:\$PATH\s*?\n") + if rep_java_home.search(profile_data): + profile_data = rep_java_home.sub(java_home_line, profile_data) + elif jdk_path: + profile_data = profile_data + "\n" + java_home_line + + if rep_path.search(profile_data): + if not jdk_path: + profile_data = rep_path.sub("", profile_data) + elif jdk_path: + profile_data = profile_data + "\n" + path_line + + try: + with open(profile_path, "w") as f: + f.write(profile_data) + except PermissionError: + return "无法修改环境变量,可能是系统加固插件拒绝了操作" + except: + return "修改失败" + + return + + @staticmethod + def get_env_jdk() -> Optional[str]: + profile_data = public.readFile('/etc/profile') + if not isinstance(profile_data, str): + return None + current_java_home = None + for line in profile_data.split("\n"): + if 'export JAVA_HOME=' in line: + current_java_home = line.split('=')[1].strip().replace('"', '').replace("'", "") + + return current_java_home + + +def jps() -> List[int]: + dir_list = [i for i in os.listdir("/tmp") if i.startswith("hsperfdata_")] + return [int(j) for j in itertools.chain(*[os.listdir("/tmp/" + i) for i in dir_list]) if j.isdecimal()] + + +def js_value_to_bool(value: Any) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower() in ("true", "yes", "1") + return bool(value) + + +def check_port_with_net_connections(port: int) -> bool: + try: + for conn in psutil.net_connections(): + if conn.status == 'LISTEN' and conn.laddr.port == port: + return False + except: + pass + return True + + +def check_port(port) -> bool: + """ + 返回false表示端口不可用 + """ + if not isinstance(port, int): + port = int(port) + if port == 0: + return False + if not 0 < port < 65535: + return False + project_list = public.M('sites').field('name,path,project_config').select() + for project_find in project_list: + try: + project_config = json.loads(project_find['project_config']) + except json.JSONDecodeError: + continue + if 'port' not in project_config: + continue + if int(project_config['port']) == port: + return False + + try: + for conn in psutil.net_connections(): + if conn.status == 'LISTEN' and conn.laddr.port == port: + return False + except: + pass + return True + + +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) + + +def create_a_not_used_port() -> int: + """ + 生成一个可用的端口 + """ + import random + while True: + port = random.randint(2000, 65535) + if check_port_with_net_connections(port): + return port + + +# 记录项目是通过用户停止的 +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)) + + +# 记录项目是通过用户操作启动的 +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)) + + +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)] + +# # 内置项目复制Tomcat +# def check_and_copy_tomcat(version: int): +# old_path = "/usr/local/bttomcat/tomcat_bak%d" +# new_path = "/usr/local/bt_mod_tomcat/tomcat%d" +# if not os.path.exists("/usr/local/bt_mod_tomcat"): +# os.makedirs("/usr/local/bt_mod_tomcat", 0o755) +# +# src_path = old_path % version +# if not os.path.exists(old_path % version) or not os.path.isfile(src_path + '/conf/server.xml'): +# return +# if os.path.exists(new_path % version): +# return +# else: +# os.makedirs(new_path % version) +# +# public.ExecShell('cp -r %s/* %s ' % (src_path, new_path % version,)) +# t = bt_tomcat(version) +# if t: +# t.reset_tomcat_server_config(8330 + version - 6) + + +# def tomcat_install_status() -> List[dict]: +# res_list = [] +# install_path = "/usr/local/bttomcat/tomcat_bak%d" +# for i in range(7, 11): +# src_path = install_path % i +# start_path = src_path + '/bin/daemon.sh' +# conf_path = src_path + '/conf/server.xml' +# if os.path.exists(src_path) and os.path.isfile(start_path) and os.path.isfile(conf_path): +# res_list.append({"version": i, "installed": True}) +# else: +# res_list.append({"version": i, "installed": False}) +# return res_list + diff --git a/mod/project/php/__init__.py b/mod/project/php/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/project/php/aepgMod.py b/mod/project/php/aepgMod.py new file mode 100644 index 00000000..87f04bb2 --- /dev/null +++ b/mod/project/php/aepgMod.py @@ -0,0 +1,1691 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import json +import os +# ------------------------------ +# 一键应用环境包模型 +# ------------------------------ +import sys +import time + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +os.chdir("/www/server/panel") +import public +import panelMysql +import db_mysql + +try: + from BTPanel import cache +except: + from cachelib import SimpleCache + cache = SimpleCache() + + +class main(): + def __init__(self): + # 2024/5/6 上午10:09 前期临时使用版本列表,后期再动态获取 + self._php_versions = ['52', '53', '54', '55', '56', '70', '71', '72', '73', '74', '80', '81', '82', '83'] + self._mysql_versions = ['5.1', '5.5', '5.6', '5.7', '8.0'] + # self._nginx_versions = ['1.8', '1.10', '1.12', '1.14', '1.16', '1.18', '1.20', '1.22', '1.24'] + self._nginx_install = True + self._php_install = True + self._mysql_install = True + self._ftpd_install = True + self.APP_PACKAGE_DB_NAME = "BT_APP_PACKAGE_DB_NAME" + self.APP_PACKAGE_DB_USER = "BT_APP_PACKAGE_DB_USER" + self.APP_PACKAGE_DB_PASS = "BT_APP_PACKAGE_DB_PASS" + self._MYSQLDUMP_BIN = public.get_mysqldump_bin() + self._rewrite_file = "{panel_path}/vhost/rewrite/{site_name}.conf" + self._package_dir = "/www/backup/package" + self._upload_package_dir = "/www/backup/upload_package" + self._temp_backup_dir = "{}/temp".format(self._package_dir) + self._temp_upload_package_dir = "{}/temp".format(self._upload_package_dir) + self._upload_package_path = os.path.join(self._upload_package_dir, "{app_name}") + self._backup_dir = None + self.skey = "app_package_create" + self._package_conf = { + "app_name": "", + "app_version": "", + "exclude_dir": [], + "php_versions": [], + "php_libs": [], + "php_functions": "", + "mysql_versions": [], + "init_sql": 0, + "db_character": "", + "db_config_file": [], + "nginx_install": True, + "php_install": True, + "mysql_install": False, + "ftpd_install": True, + "run_path": "/", + "dir_permission": [], + "update_log": "", + "size": 0, + "success_url": "", + } + + # 2024/5/6 上午10:26 返回支持选择的PHP列表 + def get_php_versions(self, get): + ''' + @name 返回支持选择的PHP列表 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return { + "used": public.get_site_php_version(get.site_name), + "versions": self._php_versions, + } + + # 2024/5/6 上午10:26 返回支持的mysql列表 + def get_mysql_versions(self, get): + ''' + @name 返回支持的mysql列表 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + used = "00" + if os.path.exists("{}/mysql/bin/mysqld".format(public.get_setup_path())): + version = public.ReadFile("{}/mysql/version.pl".format(public.get_setup_path())) + if version != "" and "." in version: + used = version.rsplit(".", 1)[0] + + return { + "used": used if "." in used else "00", + "versions": self._mysql_versions, + } + + # 2024/5/6 上午10:38 获取指定版本PHP的当前配置情况 + def get_php_config(self, get): + ''' + @name 获取指定版本PHP的当前配置情况 + @author wzz <2024/5/6 上午10:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.version = get.get("version/s", "00") + if get.version == "00": + return public.returnResult(status=False, msg="php_version不能为空,请传入需要获取扩展的PHP版本") + + import ajax + a = ajax.ajax() + res = a.GetPHPConfig(get) + + is_install = [] + for r in res["libs"]: + if r["status"]: + is_install.append(r) + + res["libs"] = is_install + + return public.returnResult(status=True, data=res) + + # 2024/5/8 下午4:31 获取环境信息 + def get_env_info(self, get): + ''' + @name 获取环境信息 + @author wzz <2024/5/8 下午4:31> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name/s", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空") + + result = cache.get(self.skey) + last_php_functions = "" + last_php_versions = "" + last_mysql_versions = "" + last_init_sql = 0 + last_db_name = "" + last_db_id = "" + last_db_config_files_l = [] + if isinstance(result, dict): + last_php_functions = result["php_functions"] if "php_functions" in result else "" + last_php_versions = result["php_versions"].split(",") if "php_versions" in result else "" + last_mysql_versions = result["mysql_versions"].split(",") if "mysql_versions" in result else "" + last_init_sql = result["init_sql"] if "init_sql" in result else 0 + last_db_config_files = result["db_config_files"] if "db_config_files" in result else [] + for db_c in last_db_config_files: + last_db_config_files_l.append({"file": db_c, "body": ""}) + last_db_name = result["db_name"] if "db_name" in result else "" + last_db_id = result["db_id"] if "db_id" in result else None + + php_version = self.get_php_versions(get) + php_version["last_php_versions"] = last_php_versions + mysql_version = self.get_mysql_versions(get) + mysql_version["last_mysql_versions"] = last_mysql_versions + mysql_version["last_init_sql"] = last_init_sql + mysql_version["last_db_config_files"] = last_db_config_files_l + mysql_version["last_db_name"] = last_db_name + mysql_version["last_db_id"] = last_db_id + + data = { + "php": php_version, + "mysql": mysql_version, + "db": { + "used": {}, + "all": [], + }, + "db_config_file": [], + "last_php_functions": last_php_functions, + } + + get.site_info = public.M('sites').where('name=?', (get.site_name,)).find() + if not get.site_info: + return public.returnResult(status=False, msg="获取网站信息失败") + + get.db_info = public.M('databases').where('pid=?', (get.site_info['id'],)).find() + get.db_all = public.M('databases').select() + data["db"]["all"] = get.db_all + get.config_list = [] + if get.db_info: + data["db"]["used"] = get.db_info + + stdout, stderr = public.ExecShell("find {site_path}/* -name *.php|xargs grep \"{db_name}\"".format( + site_path=get.site_info["path"], + db_name=get.db_info["name"], + )) + find_result = stdout.split("\n") + for fr in find_result: + if not fr: + continue + + find_file = fr.split(":")[0] + if not os.path.exists(find_file): + continue + if not find_file.endswith(".php"): + continue + find_body = fr.split(":")[1] + if not find_body: + continue + + if len(get.config_list) > 0: + for conf in get.config_list: + if find_file in conf["file"]: + break + else: + get.config_list.append({"file": find_file, "body": find_body}) + else: + get.config_list.append({"file": find_file, "body": find_body}) + + data["db_config_file"] = get.config_list + self._package_conf["db_config_file"] = get.config_list + + return public.returnResult(status=True, data=data) + + # 2024/5/6 下午3:50 获取指定目录的权限信息 + def get_path_permission(self, get): + ''' + @name 获取指定目录的权限信息 + @author wzz <2024/5/6 下午3:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + import pwd + site_path_stat = os.stat(get.check_path) + info = pwd.getpwuid(site_path_stat.st_uid) + + return { + "path": get.check_path, + "pw_name": info.pw_name, + "st_mode": site_path_stat.st_mode, + } + except Exception as e: + return {} + + # 2024/5/11 下午6:02 列出至少X级的目录 + def scan_directory(self, directory): + for root, dirs, files in os.walk(directory): + yield root, dirs, files + # 限制深度为2 + if root.count(os.sep) >= directory.count(os.sep) + 2: + del dirs[:] + + # 2024/5/11 上午11:34 根据目录获取最多两层目录 + def get_dir_list(self, get): + ''' + @name 根据目录获取最多两层目录 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.path = get.site_info["path"] + get.dir_permission_list = [] + has_root = False + root_path = "" + for root, dirs, files in self.scan_directory(get.path): + if not has_root: + get.check_path = root + path_perm = self.get_path_permission(get) + path_perm["path"] = "/" + if path_perm: + get.dir_permission_list.append(path_perm) + has_root = True + root_path = root + + if len(dirs) > 0: + for d in dirs: + get.check_path = os.path.join(root, d) + path_perm = self.get_path_permission(get) + path_perm["path"] = get.check_path.replace(root_path, "") + if path_perm: + get.dir_permission_list.append(path_perm) + + if len(files) > 0: + for f in files: + get.check_path = os.path.join(root, f) + path_perm = self.get_path_permission(get) + path_perm["path"] = get.check_path.replace(root_path, "") + if path_perm: + get.dir_permission_list.append(path_perm) + + # 2024/5/6 下午3:52 为指定目录设置权限 + def set_path_permission(self, get): + ''' + @name 为指定目录设置权限 + @author wzz <2024/5/6 下午3:52> + @param get.path/s: 指定目录 + get.permission/dict = { + "pw_name": "root", + "st_mode": 16877 + } + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + import pwd + import stat + pw_info = pwd.getpwnam(get.permission["pw_name"]) + public.ExecShell("chown -R {}:{} {}".format(pw_info.pw_uid, pw_info.pw_gid, get.path)) + public.ExecShell("chmod -R {} {}".format(stat.S_IMODE(get.permission["st_mode"]), get.path)) + + # 2024/5/6 下午3:54 检查是否设置成功 + site_path_stat = os.stat(get.path) + info = pwd.getpwuid(site_path_stat.st_uid) + if info.pw_name == get.permission["pw_name"] and site_path_stat.st_mode == get.permission["st_mode"]: + return public.returnResult(status=True, msg="设置成功") + else: + return public.returnResult(status=False, msg="设置失败") + except Exception as e: + return public.returnResult(status=False, msg="设置失败: {}".format(str(e))) + + # 2024/5/6 下午3:30 获取指定网站的根目录权限和运行目录权限 + def get_site_permission(self, get): + ''' + @name 获取指定网站的根目录权限和运行目录权限 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + get.check_path = get.site_info["path"] + get.root_path_permission = self.get_path_permission(get) + get.root_path_permission.pop("path") + from panelSite import panelSite + site_obj = panelSite() + get.id = get.site_info["id"] + runPath = site_obj.GetSiteRunPath(get) + get.check_path = get.site_info["path"] + runPath["runPath"] + get.run_path = runPath["runPath"] + get.run_path_permission = self.get_path_permission(get) + get.run_path_permission.pop("path") + + # 2024/5/6 下午3:37 获取指定目录权限 + get.dir_permission = { + "root_permission": get.root_path_permission, + "run_permission": get.run_path_permission, + } + except Exception as e: + get.dir_permission = {} + + # 2024/5/6 下午5:01 获取系统类型 + def get_os_type(self): + ''' + @name 获取系统类型 + @author wzz <2024/5/6 下午5:02> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if os.path.exists('/usr/bin/yum'): + return "1" + elif os.path.exists('/usr/bin/apt-get'): + return "3" + else: + return "0" + + # 2024/5/6 下午4:54 安装指定运行环境 + def install_env_soft(self, get): + ''' + @name 安装指定运行环境 + @author wzz <2024/5/6 下午4:55> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.lib_name = get.get("name/s", "") + get.version = get.get("version/s", "") + if get.lib_name == "" or get.version == "": + return public.returnResult(status=False, msg="name或version不能为空") + + # 2024/3/28 上午 10:36 检测是否已存在安装任务 + mmsg = "安装[{}]".format(get.lib_name + "-" + get.version) + if public.M('tasks').where('name=? and status=?', (mmsg, "-1")).count(): + return public.returnMsg(False, "已存在安装任务,请勿重复添加!") + + public.ExecShell("rm -rf {}/install/{}.sh".format(public.get_panel_path(), get.lib_name)) + execstr = ("wget -O /tmp/{name}.sh {url}/install/{os_type}/{name}.sh && " + "bash /tmp/{name}.sh install {version}").format( + name=get.lib_name, + url=public.get_url(), + os_type=self.get_os_type(), + version=get.version, + ) + public.M('tasks').add('id,name,type,status,addtime,execstr', + (None, mmsg, 'execshell', '0', time.strftime('%Y-%m-%d %H:%M:%S'), execstr)) + + return public.returnResult(status=True, msg="安装任务添加成功") + + # 2024/5/6 下午5:07 将指定目录压缩成tar.gz包 + def tar_path(self, get): + ''' + @name 将指定目录压缩成tar.gz包 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.path = get.get("path/s", "") + if get.path == "": + return public.returnResult(status=False, msg="path不能为空") + + get.target_path = get.get("target_path/s", "") + if get.target_path == "": + return public.returnResult(status=False, msg="target_path不能为空") + + if not os.path.exists(get.target_path): + public.ExecShell("mkdir -p {}".format(get.target_path)) + if not os.path.isdir(get.target_path): + return public.returnResult(status=False, msg="保存位置必须是一个目录") + + get.package_name = get.get("package_name/s", "") + if get.package_name == "": + return public.returnResult(status=False, msg="package_name不能为空") + get.package_name = get.package_name.replace(".tar.gz", "") + + # 2024/5/6 下午5:08 排除的目录 + exclude = [ + get.path + "/.git", + get.path + "/.svn", + get.path + "/.idea", + get.path + "/.vscode", + get.path + "/.vs", + get.path + "/.github", + get.path + "/.gitignore", + get.path + "/.gitattributes", + get.path + "/.gitmodules", + get.path + "/.gitkeep", + get.path + "/.gitlab-ci.yml", + get.path + "/.gitlab", + get.path + "/.gitlab-ci", + get.path + "/.user.ini", + ] + try: + exclude.extend(get.exclude_dir) + except: + pass + + exclude_str = "" + for e in exclude: + if not os.path.exists(e): + continue + + exclude_str += " --exclude='{}'".format(e) + + temp_sites_path = os.path.join(self._temp_backup_dir, "temp_sites") + if not os.path.exists(temp_sites_path): + public.ExecShell("mkdir -p {}".format(temp_sites_path)) + if not os.path.exists(os.path.join(temp_sites_path, get.app_name)): + public.ExecShell("mkdir -p {}".format(os.path.join(temp_sites_path, get.app_name))) + public.ExecShell(r"\cp -r {path}/* {temp_sites_path}/{app_name}/".format( + path=get.path, + temp_sites_path=temp_sites_path, + app_name=get.app_name, + )) + + public.ExecShell( + "cd {temp_sites_path} && tar -zcvf {target_path}/{package_name}.tar.gz {exclude_str} {target_dir} ".format( + temp_sites_path=temp_sites_path, + target_path=get.target_path, + package_name=get.package_name, + exclude_str=exclude_str, + target_dir=get.app_name, + )) + + if not os.path.exists("{}/{}.tar.gz".format(get.target_path, get.package_name)): + return public.returnResult(status=False, msg="打包失败") + + return public.returnResult(status=True, msg="打包成功") + + def __get_db_name_config(self, db_name: str): + from database import database + database = database() + db_find = public.M("databases").where("name=? AND LOWER(type)=LOWER('mysql')", (db_name,)).find() + + if db_find["db_type"] == 0: # 本地数据库 + result = panelMysql.panelMysql().execute("show databases") + isError = database.IsSqlError(result) + if isError: + return public.returnResult(status=False, msg=isError) + db_password = public.M("config").where("id=?", (1,)).getField("mysql_root") + if not db_password: + return public.returnResult(status=False, msg="数据库密码为空!请先设置数据库密码!") + try: + db_port = int(panelMysql.panelMysql().query("show global variables like 'port'")[0][1]) + except: + db_port = 3306 + if not db_password: + return public.returnResult(status=False, msg="{} 数据库密码不能为空".format(db_find["name"])) + conn_config = { + "db_host": "localhost", + "db_port": db_port, + "db_user": db_find["username"], + "db_password": db_find["password"], + } + elif db_find["db_type"] == 1: + # 远程数据库 + conn_config = json.loads(db_find["conn_config"]) + res = database.CheckCloudDatabase(conn_config) + if isinstance(res, dict): return public.returnResult(status=False, msg=res) + conn_config["db_port"] = int(conn_config["db_port"]) + elif db_find["db_type"] == 2: + conn_config = public.M("database_servers").where("id=? AND LOWER(db_type)=LOWER('mysql')", + db_find["sid"]).find() + res = database.CheckCloudDatabase(conn_config) + if isinstance(res, dict): return public.returnResult(status=False, msg=res) + conn_config["db_name"] = None + conn_config["db_port"] = int(conn_config["db_port"]) + else: + return public.returnResult(status=False, msg="{} 未知的数据库类型".format(db_find["name"])) + return public.returnResult(status=True, data=conn_config) + + # 2024/5/7 上午10:24 备份指定数据库为sql文件 + def backup_database(self, get): + ''' + @name 备份指定数据库为sql文件 + @author wzz <2024/5/7 上午10:28> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.db_name = get.get("db_name/s", "") + if get.db_name == "": + return public.returnResult(status=False, msg="db_name不能为空") + get.db_id = get.get("db_id/s", "") + if get.db_id == "": + return public.returnResult(status=False, msg="db_id不能为空") + + if not os.path.exists(self._MYSQLDUMP_BIN): + return public.returnResult(status=False, msg="缺少备份工具,请先通过软件管理安装MySQL!") + + db_find = public.M("databases").where("id=? AND name=? AND LOWER(type)=LOWER('mysql')", + (get.db_id, get.db_name)).find() + if not db_find: + return public.returnResult(status=False, msg="数据库[{}]不存在".format(get.db_name)) + + from database import database + database = database() + + db_name = db_find["name"] + + conn_config = self.__get_db_name_config(db_name) + if not conn_config["status"]: + return conn_config + + conn_config = conn_config["data"] + + mysql_obj = db_mysql.panelMysql() + flag = mysql_obj.set_host( + conn_config["db_host"], + conn_config["db_port"], + None, + conn_config["db_user"], + conn_config["db_password"] + ) + + if flag is False: + return public.returnMsg(False, database.GetMySQLError(mysql_obj._ex)) + + # 2024/5/7 上午10:56 备份目录待定 + self._MYSQL_BACKUP_DIR = "/www/backup/database" + db_backup_dir = os.path.join(self._MYSQL_BACKUP_DIR, db_name) + if not os.path.exists(db_backup_dir): + os.makedirs(db_backup_dir) + + file_name = "{db_name}_{backup_time}_mysql_data_{number}".format( + db_name=db_name, + backup_time=time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()), + number=public.GetRandomString(5), + ) + + get.db_charset = public.get_database_character(db_name) + self._package_conf["db_character"] = get.db_charset + + set_gtid_purged = "" + resp = public.ExecShell("{} --help | grep set-gtid-purged >> /tmp/backup_sql.log".format( + self._MYSQLDUMP_BIN))[0] + if resp.find("--set-gtid-purged") != -1: + set_gtid_purged = "--set-gtid-purged=OFF" + + if db_find["db_type"] == 2: + db_user = conn_config["db_user"] + db_password = conn_config["db_password"] + db_port = int(conn_config["db_port"]) + else: + db_user = "root" + db_password = public.M("config").where("id=?", (1,)).getField("mysql_root") + db_port = conn_config["db_port"] + + shell = "'{mysqldump_bin}' {set_gtid_purged} --opt --skip-lock-tables --single-transaction --routines --events --skip-triggers --default-character-set='{db_charset}' --force " \ + "--host='{db_host}' --port={db_port} --user='{db_user}' --password='{db_password}' '{db_name}'".format( + mysqldump_bin=self._MYSQLDUMP_BIN, + set_gtid_purged=set_gtid_purged, + db_charset=get.db_charset, + db_host=conn_config["db_host"], + db_port=db_port, + db_user=db_user, + db_password=db_password, + db_name=db_name, + ) + + get.export_file = os.path.join(db_backup_dir, file_name + ".sql") + shell += "| tee /tmp/backup_sql.log > '{backup_path}' ".format(backup_path=get.export_file) + public.ExecShell(shell, env={"MYSQL_PWD": conn_config["db_password"]}) + + if not os.path.exists(get.export_file): + return public.returnResult(status=False, msg="备份失败") + + return public.returnResult(status=True, msg="备份成功", data={"file": get.export_file}) + + # 2024/5/7 下午5:24 备份指定网站的伪静态 + def backup_rewrite(self, get): + ''' + @name 备份指定网站的伪静态 + @author wzz <2024/5/7 下午5:41> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name/s", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空") + + self._rewrite_file = self._rewrite_file.format(panel_path=public.get_panel_path(), site_name=get.site_name) + if not os.path.exists(self._rewrite_file): + return public.returnResult(status=True, msg="伪静态文件不存在,不需要备份") + + public.ExecShell(r"\cp -r {rewrite_file} {backup_dir}/rewrite.conf".format( + rewrite_file=self._rewrite_file, + backup_dir=self._backup_dir) + ) + if not os.path.exists("{}/rewrite.conf".format(self._backup_dir)): + return public.returnResult(status=False, msg="备份失败") + + return public.returnResult(status=True, msg="备份成功") + + # 2024/5/9 下午2:19 还原指定包的伪静态到指定网站中 + def restore_rewrite(self, get): + ''' + @name 还原指定包的伪静态到指定网站中 + @author wzz <2024/5/9 下午2:20> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + self._rewrite_file = self._rewrite_file.format(panel_path=public.get_panel_path(), site_name=get.site_name) + + public.ExecShell(r"\cp -r {_temp_import_dir}/{app_name}/rewrite.conf {_rewrite_file}".format( + _temp_import_dir=self._temp_import_dir, + app_name=get.app_name, + _rewrite_file=self._rewrite_file, + )) + + # 2024/5/9 下午6:05 还原指定上传包的伪静态到指定网站中 + def restore_upload_rewrite(self, get): + ''' + @name 还原指定上传包的伪静态到指定网站中 + @author wzz <2024/5/9 下午6:05> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + self._rewrite_file = self._rewrite_file.format(panel_path=public.get_panel_path(), site_name=get.site_name) + + public.ExecShell(r"\cp -r {_upload_package_path}/{app_name}/rewrite.conf {_rewrite_file}".format( + _upload_package_path=self._upload_package_path, + app_name=get.app_name, + _rewrite_file=self._rewrite_file, + )) + + # 2024/5/8 上午9:45 获取指定数据库连接配置文件的相对路径,并将匹配到的数据库账号密码和数据库名 + def get_db_config_file(self, get): + ''' + @name 获取指定数据库连接配置文件的相对路径,并将匹配到的数据库账号密码和数据库名 + @author wzz <2024/5/8 上午9:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if len(get.db_config_files) == 0: + return public.returnResult(status=False, msg="数据库配置文件不能为空,请选择.php文件") + + get.config_list = [] + for db_conf in get.db_config_files: + if not db_conf: + continue + if not os.path.exists(db_conf): + return public.returnResult(status=False, msg="{}不存在".format(db_conf)) + if not db_conf.endswith(".php"): + return public.returnResult(status=False, msg="{}不是PHP文件".format(db_conf)) + + # 2024/5/9 上午10:06 打开指定文件,将匹配到的内容替换成self.APP_PACKAGE_DB_NAME,self.APP_PACKAGE_DB_USER,self.APP_PACKAGE_DB_PASS + conf_body = public.readFile(db_conf) + if not conf_body: + return public.returnResult(status=False, msg="{}内容为空".format(db_conf)) + + db_find = public.M("databases").where("id=? AND name=? AND LOWER(type)=LOWER('mysql')", + (get.db_id, get.db_name)).find() + if db_find: + conf_body = conf_body.replace(db_find["username"], self.APP_PACKAGE_DB_USER) + conf_body = conf_body.replace(db_find["name"], self.APP_PACKAGE_DB_NAME) + conf_body = conf_body.replace(db_find["password"], self.APP_PACKAGE_DB_PASS) + public.writeFile(db_conf, conf_body) + + get.config_list.append(db_conf.replace(get.path + "/", "")) + self._package_conf["db_config_file"] = get.config_list + + return public.returnResult(status=True, msg="据库连接配置文件处理成功") + + # 2024/5/7 下午6:32 更新json配置文件 + def update_package_conf(self, get): + ''' + @name 更新json配置文件 + @author wzz <2024/5/7 下午6:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + self._package_conf["app_name"] = get.app_name + self._package_conf["app_version"] = get.app_version + self._package_conf["exclude_dir"] = get.exclude_dir + self._package_conf["php_versions"] = get.php_versions + self._package_conf["php_functions"] = get.php_functions + self._package_conf["mysql_versions"] = get.mysql_versions + self._package_conf["init_sql"] = get.init_sql + self._package_conf["run_path"] = get.run_path + self._package_conf["dir_permission"] = get.dir_permission + self._package_conf["update_log"] = get.update_log + + public.writeFile("{}/dir_permission.json".format(self._backup_dir), json.dumps(get.dir_permission_list)) + public.writeFile("{}/package.json".format(self._backup_dir), json.dumps(self._package_conf)) + + # 2024/5/8 上午10:05 将所有数据打包成一个压缩包 + def tar_package(self, get): + ''' + @name 将所有数据打包成一个压缩包 + @author wzz <2024/5/8 上午10:06> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.package_name = get.app_name + "_v" + get.app_version + get.app_package_dir = os.path.join(self._package_dir, get.app_name) + if not os.path.exists(get.app_package_dir): + public.ExecShell("mkdir -p {}".format(get.app_package_dir)) + + if get.export_file != "": + public.ExecShell(r"\cp -r {export_file} {temp_backup_dir}/packages/{app_name}/init.sql".format( + export_file=get.export_file, + temp_backup_dir=self._temp_backup_dir, + app_name=get.app_name, + )) + + public.ExecShell( + "cd {temp_backup_dir}/packages/ && tar -zcvf {app_package_dir}/{package_name}.tar.gz {app_name}".format( + temp_backup_dir=self._temp_backup_dir, + app_package_dir=get.app_package_dir, + package_name=get.package_name, + app_name=get.app_name, + )) + + public.ExecShell("rm -rf {}/*".format(self._temp_backup_dir)) + + if not os.path.exists("{}/{}.tar.gz".format(get.app_package_dir, get.package_name)): + return public.returnResult(status=False, msg="打包失败") + + get.size = os.path.getsize("{}/{}.tar.gz".format(get.app_package_dir, get.package_name)) + + return public.returnResult(status=True, msg="打包成功", + data={"file": "{}/{}.tar.gz".format(get.app_package_dir, get.package_name)}) + + # 2024/5/8 下午3:33 更新数据库 + def update_database(self, get): + ''' + @name 更新数据库 + @author wzz <2024/5/8 下午3:34> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # CREATE TABLE `app_package` ( + # `id` INTEGER PRIMARY KEY AUTOINCREMENT, + # `pid` INTEGER, + # `site_name` TEXT, + # `app_name` TEXT, + # `app_version` TEXT, + # `package_name` TEXT, + # `package_path` TEXT, + # `size` TEXT, + # `addtime` TEXT, + # `update_log` TEXT, + # `php_versions` TEXT, + # `php_libs` TEXT, + # `php_functions` TEXT, + # `mysql_versions` TEXT, + # `db_character` TEXT, + # `init_sql` TEXT, + # `db_config_file` TEXT + # ); + find_version = public.M('app_package').where('site_name=? AND app_name=? AND app_version=?', + (get.site_name, get.app_name, get.app_version)).find() + + try: + php_libs = json.dumps(self._package_conf["php_libs"]) + except: + php_libs = "[]" + + if find_version and "site_name" in find_version: + public.M('app_package').where('id=?', (find_version["id"],)).save( + 'package_name,package_path,size,update_log,addtime,php_versions,php_libs,php_functions,mysql_versions,db_character,init_sql,db_config_file', + (get.package_name, "{}/{}.tar.gz".format(get.app_package_dir, get.package_name), get.size, + get.update_log, time.strftime('%Y-%m-%d %H:%M:%S'), get.php_versions, php_libs, get.php_functions, + get.mysql_versions, get.db_charset, get.init_sql, json.dumps(get.config_list)) + ) + else: + public.M('app_package').add( + 'pid,site_name,app_name,app_version,package_name,package_path,size,addtime,update_log,php_libs,php_versions,php_functions,mysql_versions,db_character,init_sql,db_config_file', + (get.site_info["id"], get.site_name, get.app_name, get.app_version, get.package_name, + "{}/{}.tar.gz".format(get.app_package_dir, get.package_name), get.size, + time.strftime('%Y-%m-%d %H:%M:%S'), get.update_log, php_libs, get.php_versions, get.php_functions, + get.mysql_versions, get.db_charset, get.init_sql, json.dumps(get.config_list)) + ) + + # 2024/5/9 下午4:42 更新上传应用包的数据库 + def update_upload_database(self, get): + ''' + @name 更新上传应用包的数据库 + @author wzz <2024/5/9 下午4:43> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # CREATE TABLE `app_package_upload` ( + # `id` INTEGER PRIMARY KEY AUTOINCREMENT, + # `app_name` TEXT, + # `app_version` TEXT, + # `package_name` TEXT, + # `package_path` TEXT, + # `size` TEXT, + # `addtime` TEXT, + # `update_log` TEXT, + # `php_versions` TEXT, + # `php_libs` TEXT, + # `php_functions` TEXT, + # `mysql_versions` TEXT, + # `db_character` TEXT, + # `init_sql` TEXT, + # `db_config_file` TEXT + # ); + find_version = public.M('app_package_upload').where('app_name=? AND app_version=?', + (get.app_name, get.package_info["app_version"])).find() + + try: + php_libs = json.dumps(get.package_info["php_libs"]) + except: + php_libs = "[]" + + self._upload_package_path = self._upload_package_path.format(app_name=get.app_name) + + if find_version and "app_name" in find_version: + public.M('app_package_upload').where('id=?', (find_version["id"],)).save( + 'package_path,size,update_log,addtime,php_versions,php_libs,php_functions,mysql_versions,db_character,init_sql,db_config_file', + (self._upload_package_path, get.size, get.package_info["update_log"], + time.strftime('%Y-%m-%d %H:%M:%S'), get.package_info["php_versions"], php_libs, get.package_info["php_functions"], + get.package_info["mysql_versions"], get.package_info["db_character"], get.package_info["init_sql"], + json.dumps(get.package_info["db_config_file"])) + ) + else: + public.M('app_package_upload').add( + 'app_name,app_version,package_path,size,addtime,update_log,php_libs,php_versions,php_functions,mysql_versions,db_character,init_sql,db_config_file', + (get.app_name, get.package_info["app_version"], + self._upload_package_path, get.size, + time.strftime('%Y-%m-%d %H:%M:%S'), get.package_info["update_log"], php_libs, get.package_info["php_versions"], + get.package_info["php_functions"], get.package_info["mysql_versions"], get.package_info["db_character"], + get.package_info["init_sql"], json.dumps(get.package_info["db_config_file"])) + ) + + # 2024/5/6 上午10:49 创建一件应用环境包 + def create(self, get): + ''' + @name 创建一件应用环境包 + @author wzz <2024/5/6 上午10:50> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name/s", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空") + + get.app_name = get.get("app_name/s", "") + if get.app_name == "": + return public.returnResult(status=False, msg="app_name不能为空") + get.app_version = get.get("app_version/s", "") + if get.app_version == "": + return public.returnResult(status=False, msg="app_version不能为空") + + get.php_versions = get.get("php_versions/s", "00") + # get.php_libs = get.get("php_libs/s", "") + get.php_functions = get.get("php_functions/s", "") + get.mysql_versions = get.get("mysql_versions/s", "00") + get.init_sql = int(get.get("init_sql/d", 0)) + get.db_name = get.get("db_name/s", "") + if get.db_name == "" and get.init_sql == 1: + return public.returnResult(status=False, msg="db_name不能为空") + get.db_id = get.get("db_id/s", "") + if get.db_id == "" and get.init_sql == 1: + return public.returnResult(status=False, msg="db_id不能为空") + + get.exclude_dir = get.get("exclude_dir", "[]") + if type(get.exclude_dir) == str: + try: + get.exclude_dir = json.loads(get.exclude_dir) + except: + get.exclude_dir = [] + else: + get.exclude_dir = [] + + get.db_config_files = get.get("db_config_files", "[]") + if type(get.db_config_files) == str: + get.db_config_files = json.loads(get.db_config_files) + + get.success_url = get.get("success_url/s", "") + if not get.success_url.startswith("/"): + get.success_url = get.success_url + "/" + self._package_conf["success_url"] = get.success_url + + get.update_log = get.get("update_log/s", "") + get.update_log = public.xssencode2(get.update_log) + + # 2024/5/9 上午11:56 处理php扩展和函数 + get.version = public.get_site_php_version(get.site_name) + if get.version != "00": + php_config = self.get_php_config(get) + self._package_conf["php_libs"] = php_config["data"]["libs"] + self._package_conf["php_functions"] = get.php_functions + + # 2024/5/7 下午6:15 处理打包目录 + self._backup_dir = os.path.join(self._temp_backup_dir, "packages", get.app_name) + if os.path.exists(self._backup_dir): + public.ExecShell("rm -rf {}".format(self._backup_dir)) + public.ExecShell("mkdir -p {}".format(self._backup_dir)) + + get.site_info = public.M('sites').where('name=?', (get.site_name,)).find() + if not get.site_info: + return public.returnResult(status=False, msg="获取网站信息失败") + + get.path = get.site_info["path"] + get.target_path = self._backup_dir + get.package_name = get.app_name + + # 2024/5/6 上午11:01 备份数据库 + get.export_file = "" + get.db_charset = "" + get.config_list = [] + if get.init_sql == 1: + bk_result = self.backup_database(get) + if not bk_result["status"]: + return public.returnResult(status=False, msg=bk_result["msg"]) + + # 2024/5/8 上午9:51 获取指定数据库连接配置文件的相对路径 + d_result = self.get_db_config_file(get) + if not d_result["status"]: + return public.returnResult(status=False, msg=d_result["msg"]) + + # 2024/5/6 上午11:00 备份网站目录 + self.tar_path(get) + + # 2024/5/6 下午5:41 备份伪静态 + self.backup_rewrite(get) + + # 2024/5/8 上午9:55 获取原来网站目录的根目录和运行目录权限 + self.get_site_permission(get) + + # 2024/5/11 下午6:14 获取网站目录下最多2层的文件和目录权限 + self.get_dir_list(get) + + # 2024/5/8 上午9:59 更新压缩包里面的json配置文件 + self.update_package_conf(get) + + # 2024/5/8 上午10:12 将所有数据打包成一个压缩包 + self.tar_package(get) + + # 2024/5/8 下午3:42 更新数据库 + self.update_database(get) + + # 2024/5/10 下午3:38 保存这次填的php、mysql版本和php函数到一个json文件,方便下一次创建的时候自动填充 + last_config = { + "php_versions": get.php_versions, + "php_functions": get.php_functions, + "mysql_versions": get.mysql_versions, + "init_sql": get.init_sql, + "db_config_files": get.db_config_files, + "db_name": get.db_name, + "db_id": int(get.db_id) if get.db_id != "" else "", + } + cache.set(self.skey, last_config, 86400) + + public.WriteLog("SITE_APP_PACKAGE", "创建一键应用环境包[{}]".format(get.app_name)) + public.set_module_logs('site_app_package', 'create', 1) + + return public.returnResult(status=True, msg="打包成功") + + # 2024/5/10 下午12:10 删除指定的应用包 + def delete(self, get): + ''' + @name 删除指定的应用包 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name/s", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空") + + get.app_name = get.get("app_name/s", "") + if get.app_name == "": + return public.returnResult(status=False, msg="app_name不能为空") + + get.app_version = get.get("app_version/s", "") + if get.app_version == "": + return public.returnResult(status=False, msg="app_version不能为空") + + find_version = public.M('app_package').where('site_name=? AND app_name=? AND app_version=?', + (get.site_name, get.app_name, get.app_version)).find() + if not find_version: + return public.returnResult(status=False, msg="应用包不存在") + + public.ExecShell("rm -rf {}".format(find_version["package_path"])) + + public.M('app_package').where('id=?', (find_version["id"],)).delete() + return public.returnResult(status=True, msg="删除成功") + + # 2024/5/9 上午10:41 返回指定网站的app_package表所有数据 + def get_db_all_result(self, get): + ''' + @name 返回指定网站的app_package表所有数据 + @author wzz <2024/5/9 上午10:42> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # return public.M('app_package').where("site_name=?", (get.site_name,)).order('addtime desc').select() + return public.M('app_package').where("site_name=?", (get.site_name,)).order('app_version asc').select() + + # 2024/5/9 上午11:02 返回指定网站的app_package表的某条数据 + def get_db_result(self, get): + ''' + @name 返回指定网站的app_package表的某条数据 + @author wzz <2024/5/9 上午11:03> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return public.M('app_package').where("site_name=? and app_name=? and app_version=?", (get.site_name, get.app_name, get.app_version)).find() + + # 2024/5/9 下午5:15 返回上传数据库中所有的数据 + def get_upload_db_all_result(self, get): + ''' + @name 返回上传数据库中所有的数据 + @author wzz <2024/5/9 下午5:15> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return public.M('app_package_upload').order('app_version asc').select() + + # 2024/5/9 下午5:19 返回上传数据库中指定包的数据 + def get_upload_db_result(self, get): + ''' + @name 返回上传数据库中指定包的数据 + @author wzz <2024/5/9 下午5:19> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return public.M('app_package_upload').where("app_name=? and app_version=?", (get.app_name, get.app_version)).find() + + # 2024/5/9 上午10:39 获取所有的应用包列表 + def get_list(self, get): + ''' + @name 获取所有的应用包列表 + @author wzz <2024/5/9 上午10:40> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name/s", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空") + + all_result = self.get_db_all_result(get) + for result in all_result: + result["status"] = True + result["info"] = "正常" + if not os.path.exists(result["package_path"]) or os.path.getsize(result["package_path"]) < 10: + result["status"] = False + result["info"] = "文件小于10B或压缩包不存在,请确认包是否正常,如果异常请删除重新创建!" + + try: + result["db_config_file"] = json.loads(result["db_config_file"]) + except: + pass + + try: + result["php_libs"] = json.loads(result["php_libs"]) + except: + pass + + all_result.reverse() + public.set_module_logs('site_app_package', 'get_list', 1) + return public.returnResult(status=True, data=all_result) + + # 2024/5/9 上午11:17 获取指定包中的package_conf + def get_package_conf(self, get): + ''' + @name 获取指定包中的package_conf + @author wzz <2024/5/9 上午11:18> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + get.package_info = json.loads(public.readFile(get.package_json_path)) + except: + get.package_info = {} + + # 2024/5/9 下午3:09 检查当前站点的php设置是否符合包要求的php + def check_php_mysql(self, get): + ''' + @name 检查当前站点的php设置是否符合包要求的php + @author wzz <2024/5/9 下午3:10> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.version = public.get_site_php_version(get.site_name) + if not get.version in get.package_info["php_versions"]: + return public.returnResult(status=False, msg="当前站点PHP版本[{}]不符合包要求的PHP版本[{}]".format( + get.version, + get.package_info["php_versions"] + )) + + used_mysql = self.get_mysql_versions(get)["used"] + if get.package_info["mysql_versions"] != "00": + if not used_mysql in get.package_info["mysql_versions"]: + return public.returnResult(status=False, msg="当前站点MySQL版本[{}]不符合包要求的MySQL版本[{}]".format( + used_mysql, + get.package_info["mysql_versions"] + )) + + return public.returnResult(status=True, msg="当前站点PHP和MySQL版本符合包要求") + + # 2024/5/9 下午3:06 设置php函数到指定php版本 + def set_php_disable(self, get): + ''' + @name 设置php函数到指定php版本 + @author wzz <2024/5/9 下午3:07> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if len(get.package_info["php_functions"]) != 0: + php_config = self.get_php_config(get) + get.disable_functions = php_config["data"]["disable_functions"] + + p_disable_functions = get.package_info["php_functions"].split(",") + if len(p_disable_functions) != 0: + for p_disable_function in p_disable_functions: + if p_disable_function in get.disable_functions: + get.disable_functions = get.disable_functions.replace(p_disable_function + ",", "") + + from config import config + c = config() + c.setPHPDisable(get) + + # 2024/5/9 下午3:07 安装php扩展到指定php版本 + def install_phplib(self, get): + ''' + @name 安装php扩展到指定php版本 + @author wzz <2024/5/9 下午3:08> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if len(get.package_info["php_libs"]) != 0: + for lib_name in get.package_info["php_libs"]: + get.lib_name = lib_name["name"] + self.install_env_soft(get) + + # 2024/5/9 下午3:08 从已经解压的应用包中找到网站文件,然后解压拷贝到指定网站目录 + def copy_site(self, get): + ''' + @name 从已经解压的应用包中找到网站文件,然后解压拷贝到指定网站目录 + @author wzz <2024/5/9 下午3:08> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + public.ExecShell("cd {_temp_import_dir}/{app_name} && tar -zxvf {app_name}.tar.gz".format( + _temp_import_dir=self._temp_import_dir, + app_name=get.app_name, + )) + public.ExecShell(r"\cp -r {_temp_import_dir}/{app_name}/{app_name}/* {site_path}".format( + _temp_import_dir=self._temp_import_dir, + app_name=get.app_name, + site_path=get.site_info["path"], + )) + + for dir_permission in get.package_info["dir_permission"].keys(): + if dir_permission == "root_permission": + get.path = get.site_info["path"] + if dir_permission == "run_permission": + if get.package_info["run_path"] == "/": + continue + + get.path = os.path.join(get.site_info["path"], get.package_info["run_path"]) + + get.permission = get.package_info["dir_permission"][dir_permission] + self.set_path_permission(get) + + # 2024/5/11 下午6:22 恢复网站目录下最多2层的文件和目录权限 + self.set_dir_list_permission(get) + + # 2024/5/11 下午6:22 恢复网站目录下最多2层的文件和目录权限 + def set_dir_list_permission(self, get): + ''' + @name 恢复网站目录下最多2层的文件和目录权限 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + dir_permission_list = json.loads(public.readFile("{}/dir_permission.json".format(self._backup_dir))) + except: + return + + for dir_permission in dir_permission_list: + get.path = os.path.join(get.site_info["path"], dir_permission["path"]) + get.permission = dir_permission + self.set_path_permission(get) + + # 2024/5/9 下午5:52 从上传的应用包目录找到网站文件,然后解压拷贝到指定网站目录 + def copy_upload_site(self, get): + ''' + @name 从上传的应用包目录找到网站文件,然后解压拷贝到指定网站目录 + @author wzz <2024/5/9 下午5:52> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + public.ExecShell("cd {upload_package_path} && tar -zxvf {app_name}.tar.gz".format( + upload_package_path=self._upload_package_path, + app_name=get.app_name, + )) + public.ExecShell(r"\cp -r {upload_package_path}/{app_name}/* {site_path}".format( + upload_package_path=self._upload_package_path, + app_name=get.app_name, + site_path=get.site_info["path"], + )) + + for dir_permission in get.package_info["dir_permission"].keys(): + if dir_permission == "root_permission": + get.path = get.site_info["path"] + if dir_permission == "run_permission": + if get.package_info["run_path"] == "/": + continue + + get.path = os.path.join(get.site_info["path"], get.package_info["run_path"]) + + get.permission = get.package_info["dir_permission"][dir_permission] + self.set_path_permission(get) + + # 2024/5/11 下午6:22 恢复网站目录下最多2层的文件和目录权限 + self.set_dir_list_permission(get) + + # 2024/5/9 下午3:09 检查包json配置文件中的init_sql是否为1,如果是则执行查询网站关联的数据库并导入init.sql + def import_init_sql(self, get): + ''' + @name 检查包json配置文件中的init_sql是否为1,如果是则执行查询网站关联的数据库并导入init.sql + @author wzz <2024/5/9 下午3:09> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if get.package_info["init_sql"] == 1: + get.db_info = public.M('databases').where('pid=?', (get.site_info['id'],)).find() + if get.db_info: + for db_conf in get.package_info["db_config_file"]: + db_conf = os.path.join(get.site_info["path"], db_conf) + if not os.path.exists(db_conf): + continue + + conf_body = public.readFile(db_conf) + if not conf_body: + return public.returnResult(status=False, msg="{}内容为空".format(db_conf)) + + conf_body = conf_body.replace(self.APP_PACKAGE_DB_USER, get.db_info["username"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_NAME, get.db_info["name"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_PASS, get.db_info["password"]) + + public.writeFile(db_conf, conf_body) + + from database import database + database = database() + get.file = "{}/{}/init.sql".format(self._temp_import_dir, get.app_name) + get.name = get.db_info["name"] + import_result = database.InputSql(get) + if not import_result["status"]: + return public.returnResult(status=False, msg=import_result["msg"]) + + return public.returnResult(status=True, msg="导入init.sql成功") + + return public.returnResult(status=True, msg="不需要导入") + + # 2024/5/9 下午6:08 检查指定上传包json配置文件中的init_sql是否为1,如果是则执行查询网站关联的数据库并导入init.sql + def import_upload_init_sql(self, get): + ''' + @name 检查指定上传包json配置文件中的init_sql是否为1,如果是则执行查询网站关联的数据库并导入init.sql + @author wzz <2024/5/9 下午6:08> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.db_info = public.M('databases').where('pid=?', (get.site_info['id'],)).find() + if get.db_info: + if get.package_info["init_sql"] == 1: + from database import database + database = database() + get.file = "{}/init.sql".format(self._upload_package_path) + get.name = get.db_info["name"] + import_result = database.InputSql(get) + if not import_result["status"]: + return public.returnResult(status=False, msg=import_result["msg"]) + + for db_conf in get.package_info["db_config_file"]: + sample_db_conf = "" + if "sample" in db_conf: + sample_db_conf = db_conf.replace("sample_", "").replace("_sample", "").replace("-sample", "").replace("sample-", "").replace(".sample", "").replace("sample.", "").replace("sample", "") + db_conf = db_conf.replace("sample_", "").replace("_sample", "").replace("-sample", "").replace("sample-", "").replace(".sample", "").replace("sample.", "").replace("sample", "") + + if sample_db_conf != "": + sample_db_conf = os.path.join(get.site_info["path"], sample_db_conf) + if os.path.exists(sample_db_conf): + conf_body = public.readFile(sample_db_conf) + if conf_body: + conf_body = conf_body.replace(self.APP_PACKAGE_DB_USER, get.db_info["username"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_NAME, get.db_info["name"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_PASS, get.db_info["password"]) + + public.writeFile(sample_db_conf, conf_body) + + db_conf = os.path.join(get.site_info["path"], db_conf) + if not os.path.exists(db_conf): + public.ExecShell("cp -f {} {}".format(sample_db_conf, db_conf)) + + conf_body = public.readFile(db_conf) + if not conf_body: + return public.returnResult(status=False, msg="{}内容为空".format(db_conf)) + + conf_body = conf_body.replace(self.APP_PACKAGE_DB_USER, get.db_info["username"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_NAME, get.db_info["name"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_PASS, get.db_info["password"]) + + public.writeFile(db_conf, conf_body) + else: + db_conf = os.path.join(get.site_info["path"], db_conf) + if not os.path.exists(db_conf): + continue + + conf_body = public.readFile(db_conf) + if not conf_body: + return public.returnResult(status=False, msg="{}内容为空".format(db_conf)) + + conf_body = conf_body.replace(self.APP_PACKAGE_DB_USER, get.db_info["username"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_NAME, get.db_info["name"]) + conf_body = conf_body.replace(self.APP_PACKAGE_DB_PASS, get.db_info["password"]) + + public.writeFile(db_conf, conf_body) + + return public.returnResult(status=True, msg="导入init.sql成功") + + # 2024/5/9 上午10:54 应用指定的应用环境包到指定网站 + def apply_site(self, get): + ''' + @name 应用指定的应用环境包到指定网站 + @author wzz <2024/5/9 上午10:55> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name/s", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空") + + get.app_name = get.get("app_name/s", "") + if get.app_name == "": + return public.returnResult(status=False, msg="app_name不能为空") + + get.app_version = get.get("app_version/s", "") + if get.app_version == "": + return public.returnResult(status=False, msg="app_version不能为空") + + get.site_info = public.M('sites').where('name=?', (get.site_name,)).find() + if not get.site_info: + return public.returnResult(status=False, msg="获取网站信息失败") + + get.package_info = self.get_db_result(get) + if not get.package_info: + return public.returnResult(status=False, msg="获取应用包信息失败") + + if not os.path.exists(get.package_info["package_path"]): + return public.returnResult(status=False, msg="应用包不存在") + + # 2024/5/9 上午11:15 创建临时导入目录 + self._temp_import_dir = os.path.join(self._temp_backup_dir, "import") + if os.path.exists(self._temp_import_dir): + public.ExecShell("rm -rf {}".format(self._temp_import_dir)) + public.ExecShell("mkdir -p {}".format(self._temp_import_dir)) + + # 2024/5/9 上午11:15 解压应用包 + public.ExecShell("tar -zxvf {} -C {}".format(get.package_info["package_path"], self._temp_import_dir)) + + # 2024/5/9 上午11:23 处理包里面的json配置文件 + get.package_json_path = "{_temp_import_dir}/{app_name}/package.json".format( + _temp_import_dir=self._temp_import_dir, + app_name=get.app_name, + ) + + self.get_package_conf(get) + if not get.package_info: + return public.returnResult(status=False, msg="包异常,获取package.json配置文件失败") + + # 2024/5/9 上午11:41 检查当前站点的php设置是否符合包要求的php + check_result = self.check_php_mysql(get) + if not check_result["status"]: + return public.returnResult(status=False, msg=check_result["msg"]) + + # 2024/5/9 上午11:53 设置php函数到指定php版本 + self.set_php_disable(get) + # 2024/5/9 下午12:08 安装php扩展到指定php版本 + self.install_phplib(get) + # 2024/5/9 下午2:37 还原指定包的伪静态到指定网站中 + self.restore_rewrite(get) + # 2024/5/9 上午11:01 从已经解压的应用包中找到网站文件,然后解压拷贝到指定网站目录 + self.copy_site(get) + # 2024/5/9 上午11:16 检查包json配置文件中的init_sql是否为1,如果是则执行查询网站关联的数据库并导入init.sql + import_result = self.import_init_sql(get) + if not import_result["status"]: + return public.returnResult(status=False, msg=import_result["msg"]) + + public.set_module_logs('site_app_package', 'apply_site', 1) + return public.returnResult(status=True, msg="应用成功") + + # 2024/5/9 下午3:50 上传应用包到指定位置 + def upload_package(self, get): + ''' + @name 上传应用包到指定位置 + @author wzz <2024/5/9 下午3:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + if not os.path.exists(self._temp_upload_package_dir): + public.ExecShell("mkdir -p {}".format(self._temp_upload_package_dir)) + + get.f_path = get.get("f_path/s", "") + get.f_name = get.get("f_name/s", "") + get.f_size = get.get("f_size/s", "") + get.f_start = get.get("f_start/s", "") + get.blob = get.get("blob/s", "") + + from files import files + fileObj = files() + upload_result = fileObj.upload(get) + if type(upload_result) == dict and not upload_result["status"]: + return public.returnResult(status=False, msg=upload_result["msg"]) + + get.size = os.path.getsize(os.path.join(self._temp_upload_package_dir, get.f_name)) + + if get.size != int(get.f_size): + return public.returnResult(status=False, msg="上传文件大小不一致,上传失败!") + + # 2024/5/9 下午4:26 解压压缩包 + public.ExecShell("tar -zxvf {}/{} -C {}".format(self._temp_upload_package_dir, get.f_name, self._upload_package_dir)) + + # 2024/5/9 下午4:27 获取配置信息写入数据库 + get.app_name = get.f_name.split("_v")[0] + # 2024/5/9 上午11:23 处理包里面的json配置文件 + get.package_json_path = "{_upload_package_dir}/{app_name}/package.json".format( + _upload_package_dir=self._upload_package_dir, + app_name=get.app_name, + ) + + self.get_package_conf(get) + if not get.package_info: + public.ExecShell("rm -rf {}".format(os.path.join(self._temp_upload_package_dir, get.f_name))) + public.ExecShell("rm -rf {}".format(os.path.join(self._upload_package_dir, get.app_name))) + return public.returnResult(status=False, msg="应用包异常,获取package.json配置文件失败") + + # 2024/5/9 下午4:29 更新数据库 + self.update_upload_database(get) + + public.set_module_logs('site_app_package', 'upload_package', 1) + return public.returnResult(status=True, msg="上传成功") + + # 2024/5/9 下午5:13 获取上传列表的所有包数据 + def get_upload_list(self, get): + ''' + @name 获取上传列表的所有包数据 + @author wzz <2024/5/9 下午5:13> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + all_result = self.get_upload_db_all_result(get) + for result in all_result: + result["status"] = True + result["info"] = "正常" + if not os.path.exists(result["package_path"]) or os.path.getsize(result["package_path"]) < 10: + result["status"] = False + result["info"] = "文件小于10B或压缩包不存在,请确认包是否正常,如果异常请删除重新创建!" + + try: + result["db_config_file"] = json.loads(result["db_config_file"]) + except: + pass + + try: + result["php_libs"] = json.loads(result["php_libs"]) + except: + pass + + all_result.reverse() + + public.set_module_logs('site_app_package', 'get_upload_list', 1) + return public.returnResult(status=True, data=all_result) + + # 2024/5/10 下午12:16 删除指定的上传包 + def delete_upload(self, get): + ''' + @name 删除指定的上传包 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.app_name = get.get("app_name/s", "") + if get.app_name == "": + return public.returnResult(status=False, msg="app_name不能为空") + + get.app_version = get.get("app_version/s", "") + if get.app_version == "": + return public.returnResult(status=False, msg="app_version不能为空") + + find_version = public.M('app_package_upload').where('app_name=? AND app_version=?', + (get.app_name, get.app_version)).find() + if not find_version: + return public.returnResult(status=False, msg="应用包不存在") + + public.ExecShell("rm -rf {}".format(find_version["package_path"])) + + public.M('app_package_upload').where('id=?', (find_version["id"],)).delete() + return public.returnResult(status=True, msg="删除成功") + + # 2024/5/9 下午6:33 获取上传列表中某一个包的数据 + def get_upload_result(self, get): + ''' + @name 获取上传列表中某一个包的数据 + @author wzz <2024/5/9 下午6:33> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"sbt 2 + tatus":True/False,"msg":"提示信息"} + ''' + get.app_name = get.get("app_name/s", "") + if get.app_name == "": + return public.returnResult(status=False, msg="app_name不能为空") + + get.app_version = get.get("app_version/s", "") + if get.app_version == "": + return public.returnResult(status=False, msg="app_version不能为空") + + installed_phps = [] + for v in self._php_versions: + if os.path.exists(public.get_setup_path() + "/php/" + v + "/bin/php"): + installed_phps.append(v) + + result = self.get_upload_db_result(get) + result["installed_phps"] = installed_phps + result["installed_mysql"] = self.get_mysql_versions(get)["used"] + + result["status"] = True + result["info"] = "正常" + if not os.path.exists(result["package_path"]) or os.path.getsize(result["package_path"]) < 10: + result["status"] = False + result["info"] = "文件小于10B或压缩包不存在,请确认包是否正常,如果异常请删除重新创建!" + + try: + result["db_config_file"] = json.loads(result["db_config_file"]) + except: + pass + + try: + result["php_libs"] = json.loads(result["php_libs"]) + except: + pass + + return public.returnResult(status=True, data=result) + + # 2024/5/9 下午3:35 创建网站并应用应用包 + def create_site(self, get): + ''' + @name 创建网站并应用应用包到指定网站中,如果网站已经存在则直接应用应用包到指定网站中,如果网站不存在则创建网站并应用应用包到指定网站中 + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name/s", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空") + + get.port = "80" + if ":" in get.site_name: + get.port = get.site_name.split(":")[1] + get.site_name = get.site_name.split(":")[0] + + get.webname = get.get("webname", "") + if get.webname == "": + return public.returnResult(status=False, msg="webname不能为空") + + get.app_name = get.get("app_name/s", "") + if get.app_name == "": + return public.returnResult(status=False, msg="app_name不能为空") + + get.app_version = get.get("app_version/s", "") + if get.app_version == "": + return public.returnResult(status=False, msg="app_version不能为空") + + get.ps = get.get("ps/s", "") + get.ps = public.xssencode2(get.ps) + + # 2024/5/9 上午11:23 处理包里面的json配置文件 + get.package_json_path = "{_upload_package_dir}/{app_name}/package.json".format( + _upload_package_dir=self._upload_package_dir, + app_name=get.app_name, + ) + + self.get_package_conf(get) + if not get.package_info: + return public.returnResult(status=False, msg="包异常,获取package.json配置文件失败") + + self._upload_package_path = self._upload_package_path.format(app_name=get.app_name) + + # 2024/5/9 下午5:28 选择get.package_info["php_versions"]里面版本最高的php版本 + get.php_list = get.package_info["php_versions"].split(",") + get.php_list.sort() + get.php_list.reverse() + for php_version in get.php_list: + if os.path.exists(public.get_setup_path() + "/php/" + php_version + "/bin/php"): + get.php_version = php_version + break + + from panelSite import panelSite + tmp_args = { + 'webname': get.webname, + 'type': 'PHP', + 'port': get.port, + 'ps': get.ps, + 'path': os.path.join("/www/wwwroot", get.site_name), + 'type_id': 0, + 'version': get.php_version, + 'ftp': False, + 'sql': False, + } + if get.package_info["db_config_file"]: + tmp_args["sql"] = "MySQL" + tmp_args["codeing"] = get.package_info["db_character"] if get.package_info["db_character"] != "" else "utf8mb4" + tmp_args["datauser"] = get.site_name.replace(".", "_") + tmp_args["datapassword"] = public.GetRandomString(16) + + args = public.to_dict_obj(tmp_args) + add_site_result = panelSite().AddSite(args) + if "status" in add_site_result and not add_site_result["status"]: + return public.returnResult(status=False, msg=add_site_result["msg"]) + if not add_site_result["siteStatus"]: + return public.returnResult(status=False, msg="创建网站失败") + + get.site_info = public.M('sites').where('name=?', (get.site_name,)).find() + if not get.site_info: + return public.returnResult(status=False, msg="获取网站信息失败") + + get.version = public.get_site_php_version(get.site_name) + if not get.version in get.package_info["php_versions"]: + return public.returnResult(status=False, msg="当前站点PHP版本[{}]不符合包要求的PHP版本[{}]".format( + get.version, + get.package_info["php_versions"] + )) + + # 2024/5/9 上午11:53 设置php函数到指定php版本 + self.set_php_disable(get) + # 2024/5/9 下午12:08 安装php扩展到指定php版本 + self.install_phplib(get) + # 2024/5/9 下午2:37 还原指定上传包的伪静态到指定网站中 + self.restore_upload_rewrite(get) + # 2024/5/9 下午5:54 从上传的应用包目录找到网站文件,然后解压拷贝到指定网站目录 + self.copy_upload_site(get) + # 2024/5/9 上午11:16 检查包json配置文件中的init_sql是否为1,如果是则执行查询网站关联的数据库并导入init.sql + import_result = self.import_upload_init_sql(get) + + if not import_result["status"]: + return public.returnResult(status=False, msg=import_result["msg"]) + + success_url = "http://{}".format(get.site_name) + if "success_url" in get.package_info: + success_url = success_url + get.package_info["success_url"] + + add_site_result["success_url"] = success_url + + public.set_module_logs('site_app_package', 'create_site', 1) + return public.returnResult(status=True, msg="网站创建成功", data=add_site_result) diff --git a/mod/project/php/php_asyncMod.py b/mod/project/php/php_asyncMod.py new file mode 100644 index 00000000..0be873a2 --- /dev/null +++ b/mod/project/php/php_asyncMod.py @@ -0,0 +1,1898 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: sww +# ------------------------------------------------------------------- +# php异步项目部署模块 +# ------------------------------ +import traceback +import re, os, sys, json, time, hashlib +import shutil + +if '/www/server/panel/' not in sys.path: + sys.path.insert(0, '/www/server/panel/') +from mod.base.web_conf import IpRestrict, remove_sites_service_config +from mod.base.process.process import RealProcess +from mod.base.process.server import RealServer +from mod.base.database_tool import add_database +from mod.base.web_conf import NginxDomainTool, check_domain +from mod.base.web_conf.dir_tool import DirTool +from mod.base.web_conf.proxy import Proxy +from mod.base.backup_tool import VersionTool +from mod.base.web_conf.referer import Referer +from mod.base.web_conf.redirect import Redirect +from mod.base.process.server import syssafe_admin + +if '/www/server/panel/class/' not in sys.path: + sys.path.insert(0, '/www/server/panel/class/') +import public +import tarfile +import psutil + +try: + import zipfile +except: + public.ExecShell("btpip install zipfile") + import zipfile +try: + import gzip +except: + public.ExecShell("btpip install indexed-gzip") + import gzip +try: + import bz2 +except: + public.ExecShell("btpip install bz2file") + import bz2 + + +class main(IpRestrict, RealProcess, Proxy, Redirect): # 继承并使用同ip黑白名单限制 SSLManager ssl继承证书管理 + _phpa_path = '/www/server/phpa_project' # + _phpa_logs = '{}/logs'.format(_phpa_path) + setupPath = public.get_setup_path() + is_ipv6 = os.path.exists(setupPath + '/panel/data/ipv6.pl') + siteName = None + sitePath = None + sitePort = None + phpVersion = None + pids = None + _log_name = '项目管理' + _php_logs_path = "/www/wwwlogs/php_async" + __proxyfile = '{}/data/proxyfile.json'.format(public.get_panel_path()) + group_file = "/www/server/panel/data/phpa_project_groups.json" + + def __init__(self): + IpRestrict.__init__(self) + Proxy.__init__(self) + RealProcess.__init__(self) + Redirect.__init__(self) + if not os.path.exists(self.group_file): public.writeFile(self.group_file, '{}') + + def create_project(self, get): + try: + public.set_module_logs('phpmod', 'create_phpmod', 1) + if not hasattr(get, 'webname'): + return public.returnResult(False, '请输入域名!') + if not hasattr(get, 'site_path'): + return public.returnResult(False, '请输入项目路径!') + if not os.path.exists(get.site_path): public.ExecShell('mkdir -p {}'.format(get.site_path)) + if not hasattr(get, 'project_proxy_path') and hasattr(get, 'open_proxy') and int(get.open_proxy) == 1: + return public.returnResult(False, '设置反向代理!') + if not hasattr(get, 'project_port') or get.project_port == '': + get.project_port = 0 + if not hasattr(get, 'php_version'): + return public.returnResult(False, '请选择PHP版本!') + if not hasattr(get, 'project_cmd'): + return public.returnResult(False, '请输入启动命令!') + if not hasattr(get, 'is_power_on'): + get.is_power_on = 1 + if not hasattr(get, 'project_ps'): + get.project_ps = '' + if not hasattr(get, 'sql'): + get.sql = "" + if not hasattr(get, 'run_user'): + get.run_user = 'www' + if not hasattr(get, 'composer_version'): + get.composer_version = '2.7.3' + if not os.path.exists('/www/server/phpa_project/logs'): + public.ExecShell('mkdir -p /www/server/phpa_project/logs') + public.ExecShell('chmod -R 777 /www/server/phpa_project/logs') + # 设置项目目录权限 + public.ExecShell("chown -R {}:{} {}".format(get.run_user.strip(), get.run_user.strip(), get.site_path)) + public.ExecShell("chmod -R 755 {}".format(get.site_path)) + webname = json.loads(get.webname) + # 域名重复性检查 + domains = [webname['domain']] + webname['domainlist'] + for domain in domains: + if public.M('sites').where('name=?', (domain.split(':')[0].strip(),)).count(): + return public.returnResult(False, '指定域名已存在: {}'.format(domain)) + + # 域名存在检查 + get.project_cmd = get.project_cmd.strip() + + # 创建项目 + if public.M('sites').where('name=?', (webname['domain'].split(':')[0].strip(),)).count(): + return public.returnResult(False, '指定项目已存在: {}'.format(webname)) + + self.siteName = self.ToPunycode(webname['domain'].split(':')[0].strip()) + self.sitePath = self.ToPunycodePath(get.site_path) + self.sitePort = 80 if ':' not in webname['domain'] else webname['domain'].split(':')[1].strip() + self.phpVersion = get.php_version + # 添加nginx网站配置文件 + self.nginxAdd() + # 添加多余的域名 + domainlist = [] + for domain in domains: + domainname = domain.split(':')[0].strip() + domainport = '80' if ':' not in domain else domain.split(':')[1].strip() + domainlist.append((domainname, domainport)) + if len(domainlist) > 1: + NginxDomainTool().nginx_set_domain(self.siteName, *domainlist) + # 创建默认文档 + if not os.path.exists('{}/index.ttml'.format(self.sitePath)): + connect = """ + + + + + 恭喜,异步项目创建成功! + + + +

                                    +

                                    恭喜,异步项目创建成功!

                                    +

                                    这是默认index.html,本页面由系统自动生成

                                    +
                                      +
                                    • 本页面在项目目录下的index.html
                                    • +
                                    • 您可以修改、删除或覆盖本页面
                                    • +
                                    • 您可以将web目录修改为您的前端文件所在目录
                                    • +
                                    • 您也可以将后端直接反代到此域名
                                    • +
                                    +
                                    + + """ + public.writeFile('{}/index.html'.format(self.sitePath), connect) + # 添加反向代理 + if hasattr(get, 'open_proxy') and int(get.open_proxy) == 1: + args = { + "proxyname": self.siteName, + "sitename": self.siteName, + "proxydir": get.project_proxy_path, + "proxysite": "http://127.0.0.1:{}".format(get.project_port), + "todomain": "$host", + "type": "1", + "cache": "1", + "subfilter": '[{"sub1":"","sub2":""},{"sub1":"","sub2":""},{"sub1":"","sub2":""}]', + "advanced": "1", + "cachetime": "1", + } + + self.create_proxy(public.to_dict_obj(args)) + # 修改启动命令 + start_cmd = get.project_cmd + if 'php' == get.project_cmd[:3]: + get.project_cmd = '/www/server/php/{}/bin/php -c {}/php-cli.ini {}'.format(get.php_version, get.site_path, get.project_cmd[3:]) + else: + get.project_cmd = '/www/server/php/{}/bin/php -c {}/php-cli.ini {}'.format(get.php_version, get.site_path, get.project_cmd) + is_fork = 0 + if ' -d' in get.project_cmd: + is_fork = 1 + # 创建服务 并设置开机启动 + realserver = RealServer() + realserver.create_daemon(self.siteName, '', get.project_cmd, get.site_path, get.run_user.strip(), get.is_power_on, logs_file=os.path.join(self._phpa_logs, self.siteName + '.log'), + is_fork=is_fork) + # 安装依赖 + dependence = 0 + if hasattr(get, 'install_dependence') and get.install_dependence == "1": + public.run_thread(self.install_dependence, (self.siteName, get.php_version, get.site_path, get.composer_version)) + dependence = 1 + else: + public.ExecShell('cp /www/server/php/{}/etc/php-cli.ini {}/'.format(get.php_version, get.site_path)) + public.ExecShell("sed -i '/disable_functions/d' {}".format(os.path.join(get.site_path, 'php-cli.ini'))) + public.ExecShell("chown root:root {}".format(os.path.join(get.site_path, 'php-cli.ini'))) + public.ExecShell("chomd 644 {}".format(os.path.join(get.site_path, 'php-cli.ini'))) + # 写入数据库 + pdata = { + 'name': self.siteName, + 'path': get.site_path, + 'ps': get.project_ps, + 'status': 1, + 'type_id': 0, + 'project_type': 'PHP', + 'project_config': json.dumps( + { + 'ssl_path': '/www/wwwroot/phpa_ssl', + 'sitename': self.siteName, + 'domains': [webname['domain']] + webname['domainlist'], + 'project_path': get.site_path, + 'project_cmd': get.project_cmd, + 'is_power_on': get.is_power_on, + 'port': int(get.project_port), + 'log_path': self._log_name, + 'php_version': get.php_version, + 'dependence': dependence, + 'start_cmd': start_cmd, + 'project_port': get.project_port, + 'type': 'PHPMOD', + 'run_user': get.run_user.strip(), + } + ), + 'addtime': public.getDate() + } + pid = public.M('sites').insert(pdata) + for domain in domains: + domain_arr = domain.split(':') + if len(domain_arr) == 1: + domain_arr.append(80) + domain_arr[0] = self.ToPunycode(domain_arr[0].strip()) + public.M('domain').add('pid,name,port,addtime', (pid, domain_arr[0], domain_arr[1], public.getDate())) + + # 检查是否创建数据库 + if get.sql == "MYSQL": + if not (hasattr(get, 'sql_user') or hasattr(get, 'sql_pwd') or hasattr(get, 'sql_codeing')): + return public.returnResult(False, '请填写数据库信息') + mysql_data = { + "server_id": 0, + "database_name": get.sql_user, + "db_user": get.sql_user, + "password": get.sql_pwd, + "dataAccess": "ip", + "address": "127.0.0.1", + "codeing": get.sql_codeing, + "ps": "", + "listen_ip": "0.0.0.0/0", + "host": "", + "pid": pid, + } + add_database(db_type="mysql", data=mysql_data) + + self.modify_project_run_state(public.to_dict_obj({'sitename': self.siteName, 'project_action': 'start'})) + # 写日志 + public.WriteLog(self._log_name, '添加PHP动态项目{}'.format(webname)) + return public.returnResult(True, '添加成功' + ("" if dependence == 0 else ",正在安装依赖!")) + except: + return public.returnResult(False, str(traceback.format_exc())) + + def re_install_dependence(self, get): + if not hasattr(get, 'id'): + return public.returnResult(False, '参数缺失:ids!') + project_config = public.M('sites').where('id=?', (get.id,)).select()[0] + project_config = json.loads(project_config['project_config']) + if os.path.exists('/tmp/php_mod_{}.pl'.format(project_config['name'])) and int(project_config['dependence']) == 1: + pid = public.readFile('/tmp/php_mod_{}.pl'.format(project_config['name'])) + try: + import psutil + p = psutil.Process(int(pid)) + if p.name() == 'BT-Panel' or p.name() == 'btpython': + return public.returnResult(False, '正在安装中请稍后重试!') + except: + pass + public.run_thread(self.install_dependence, (project_config['name'], project_config['php_version'], project_config['path'])) + return public.returnResult(True, '正在安装依赖!') + + def check_install(self, get): + if not hasattr(get, 'name'): + return public.returnResult(False, '参数缺失:name!') + if not hasattr(get, 'php_version'): + return public.returnResult(False, '参数缺失:php_version!') + if get.name == 'swoole': + res = public.ExecShell("/www/server/php/{}/bin/php --ri {}|grep Version|awk '{{print $3}}'".format(get.php_version, get.name)) + if res[0] != '': + return public.returnResult(True, res[0].strip()) + return public.returnResult(False, '未安装!请前往PHP-安装扩展中安装{}!'.format(get.name)) + if get.name == 'fileinfo': + res = public.ExecShell("/www/server/php/{}/bin/php --ri {}".format(get.php_version, get.name)) + if res[0] != '' and 'fileinfo' in res[0] and 'enabled' in res[0]: + return public.returnResult(True, '已安装!') + return public.returnResult(False, '未安装!请前往PHP-安装扩展中安装{}!'.format(get.name)) + + def print_log(self, path, log): + public.writeFile(path, log, 'a+') + + def check_auto_install(self, get): + if not hasattr(get, 'path'): + return public.returnResult(False, '参数缺失:path!') + if not os.path.exists("{}/composer.json".format(get.path)): + return False + if not os.path.exists('{}/composer.lock'.format(get.path)) or not os.path.exists('{}/vendor'.format(get.path)): + return True + return False + + def get_swoole_correspondence_php(self, get): + if not hasattr(get, 'swoole_version') and get.swoole_version.strip() in ['2', '4', '5']: + return public.returnResult(False, '参数缺失:swoole_version!') + swoole_version = get.swoole_version.strip() + php_correspondence = { + '2': ['52', '53', '54', '55', '70', '71', '72'], + '4': ['72', '73', '74', '80', '81', '82'], + '5': ['80', '81', '82', '83'], + } + php_list = php_correspondence[swoole_version] + result = [] + for php in php_list: + setup_status = os.path.exists('/www/server/php/{}/bin/php'.format(php)) + res = public.ExecShell("/www/server/php/{}/bin/php --ri swoole|grep Version|awk '{{print $3}}'".format(php))[0] + swoole_status = True if res != '' else False + swoole_version = res.strip() + result.append({ + 'php_version': php, + 'setup_status': setup_status, + 'swoole_status': swoole_status, + 'swoole_version': swoole_version, + 'setup_swoole_isactive': True if swoole_version != '' and swoole_version[0] == get.swoole_version else False + }) + return public.returnResult(True, data=result, msg='获取成功!') + + @syssafe_admin + def install_composer(self, composer_version, logs_path): + composer_name = '/usr/bin/composer{}'.format(('_' + composer_version.replace('.', '_')) if composer_version != '' else '') + if not os.path.exists(composer_name) and composer_version != '': + url = public.get_url() + '/src/compose/composer_{}'.format(composer_version.replace('.', '_')) + public.ExecShell('wget -O {} {} &> {}'.format(composer_name, url, logs_path)) + if os.path.exists(composer_name): + public.ExecShell('chmod +x {}'.format(composer_name)) + public.ExecShell('echo "{}" >> {}'.format('composer{}下载成功!'.format(composer_version), logs_path)) + + def install_dependence(self, webname, php_version, site_path, composer_version='2.7.3'): + logs_path = '/tmp/{}_install_dependence.log'.format(webname) + + # 检查锁和所文件 + if os.path.exists('{}/composer.lock'.format(site_path)): + if os.path.exists('{}/vendor'.format(site_path)): + public.ExecShell('echo "{}" >> {}'.format('composer.lock和vendor目录存在,不需要安装依赖!', logs_path)) + try: + project_list = public.M('sites').where('project_type=? and name=?', ('PHP', webname)).field('project_config').select()[0] + project_config = json.loads(project_list['project_config']) + project_config['dependence'] = 0 + public.M('sites').where('project_type=? and name=?', ('PHP', webname)).setField('project_config', json.dumps(project_config)) + except: + pass + if not os.path.exists('/www/server/phpa_project/logs'): + public.ExecShell('mkdir -p /www/server/phpa_project/logs') + public.ExecShell('chmod -R 755 /www/server/phpa_project/logs') + RealServer().daemon_admin(str(webname), 'start') + return + else: + public.ExecShell('echo "{}" >> {}'.format('composer.lock存在,但vendor目录不存在,开始安装依赖!', logs_path)) + else: + public.ExecShell('echo "{}" > {}'.format('composer.lock不存在,开始安装依赖!', logs_path)) + self.install_composer(composer_version, logs_path) + public.writeFile('/tmp/php_mod_{}.pl'.format(webname), str(os.getpid())) + public.ExecShell('cp /www/server/php/{}/etc/php-cli.ini {}/'.format(php_version, site_path)) + public.ExecShell("sed -i '/disable_functions/d' {}".format(os.path.join(site_path, 'php-cli.ini'))) + # 尝试安装composer中的依赖 + self.print_log(logs_path, '开始安装composer依赖!\n') + # self.print_log(logs_path, 'cd {} && export COMPOSER_ALLOW_SUPERUSER=1 && /www/server/php/{}/bin/php -c {} /usr/bin/composer{} install --no-interaction &>> {}'.format(site_path, php_version, + # os.path.join(site_path, + # 'php-cli.ini'), + # '_' + composer_version.replace( + # '.', + # '_') if composer_version != '' else '', + # logs_path)) + public.ExecShell( + 'cd {} && export COMPOSER_ALLOW_SUPERUSER=1 && /www/server/php/{}/bin/php -c {} /usr/bin/composer{} install --no-interaction &>> {}'.format(site_path, php_version, + os.path.join(site_path, 'php-cli.ini'), + '_' + composer_version.replace('.', + '_') if composer_version != '' else '', + logs_path)) + # 查看安装依赖报错 + logs = public.readFile(logs_path) + if "No composer.lock file present. Updating dependencies to latest instead of installing from lock file" in logs and not os.path.exists('{}/composer.lock'.format(site_path)): + self.print_log(logs_path, '\ncomposer.lock文件不存在,安装依赖时需要使用此文件,请前往项目官网下载composer.lock文件放入{}目录后前往重新安装依赖!'.format(site_path)) + public.ExecShell('cp /www/server/php/{}/etc/php-cli.ini {} >> {}'.format(php_version, site_path, logs_path)) + public.ExecShell("sed -i '/disable_functions/d' {}".format(os.path.join(site_path, 'php-cli.ini'))) + public.ExecShell("chown root:root {}".format(os.path.join(site_path, 'php-cli.ini'))) + public.ExecShell("chomd 644 {}".format(os.path.join(site_path, 'php-cli.ini'))) + self.print_log(logs_path, '安装依赖结束!') + try: + project_list = public.M('sites').where('project_type=? and name=?', ('PHP', webname)).field('project_config').select()[0] + project_config = json.loads(project_list['project_config']) + project_config['dependence'] = 0 + public.M('sites').where('project_type=? and name=?', ('PHP', webname)).setField('project_config', json.dumps(project_config)) + except: + pass + if not os.path.exists('/www/server/phpa_project/logs'): + public.ExecShell('mkdir -p /www/server/phpa_project/logs') + public.ExecShell('chmod -R 755 /www/server/phpa_project/logs') + self.modify_project_run_state(public.to_dict_obj({'sitename': webname, 'project_action': 'start'})) + + def check_development_setup(self, php_version, name): + try: + # /ajax?action=GetPHPConfig + import ajax + a = ajax.ajax() + get = public.to_dict_obj({}) + get.version = php_version + res = a.GetPHPConfig(get) + for i in res['libs']: + if i['name'] == name and i['status'] == True: + return True + return False + except: + return False + + # 域名编码转换 + def ToPunycode(self, domain): + import re + if sys.version_info[0] == 2: domain = domain.encode('utf8') + tmp = domain.split('.') + newdomain = '' + for dkey in tmp: + if dkey == '*': continue + # 匹配非ascii字符 + match = re.search(u"[\x80-\xff]+", dkey) + if not match: match = re.search(u"[\u4e00-\u9fa5]+", dkey) + if not match: + newdomain += dkey + '.' + else: + if sys.version_info[0] == 2: + newdomain += 'xn--' + dkey.decode('utf-8').encode('punycode') + '.' + else: + newdomain += 'xn--' + dkey.encode('punycode').decode('utf-8') + '.' + if tmp[0] == '*': newdomain = "*." + newdomain + return newdomain[0:-1] + + def ToPunycodePath(self, path): + if sys.version_info[0] == 2: path = path.encode('utf-8') + if os.path.exists(path): return path + import re + match = re.search(u"[\x80-\xff]+", path) + if not match: match = re.search(u"[\u4e00-\u9fa5]+", path) + if not match: return path + npath = '' + for ph in path.split('/'): + npath += '/' + self.ToPunycode(ph) + return npath.replace('//', '/') + + # 添加到nginx + def nginxAdd(self): + if not os.path.exists('/www/server/panel/class/404_settings.json'): + public.writeFile('/www/server/panel/class/404_settings.json', json.dumps({})) + + template = None + use_template = public.readFile("{}/data/use_nginx_template.pl".format(public.get_panel_path())) + if isinstance(use_template, str): + template_file = "{}/data/nginx_template/{}".format(public.get_panel_path(), use_template) + if os.path.isfile(template_file): + template = public.readFile(template_file) + + with open('/www/server/panel/class/404_settings.json', 'r') as f: + settings = json.load(f) + status = settings.get('status', "0") + filename = settings.get('filename', "404.html") + error_page_line = 'error_page 404 /' + filename + ';' + + if status == "0": + error_page_line = '#' + error_page_line + else: + error_page_line = 'error_page 404 /' + filename + ';' + + listen_ipv6 = '' + if self.is_ipv6: listen_ipv6 = "\n listen [::]:%s;" % self.sitePort + + conf = r'''server +{{ + listen {listen_port};{listen_ipv6} + server_name {site_name}; + index index.php index.html index.htm default.php default.htm default.html; + root {site_path}; + #CERT-APPLY-CHECK--START + # 用于SSL证书申请时的文件验证相关配置 -- 请勿删除 + include /www/server/panel/vhost/nginx/well-known/{site_name}.conf; + #CERT-APPLY-CHECK--END + + #SSL-START {ssl_start_msg} + #error_page 404/404.html; + #SSL-END + + #ERROR-PAGE-START {err_page_msg} + {error_page_line} + #error_page 502 /502.html; + #ERROR-PAGE-END + + #PHP-INFO-START {php_info_start} + include enable-php-{php_version}.conf; + #PHP-INFO-END + + #REWRITE-START {rewrite_start_msg} + include {setup_path}/panel/vhost/rewrite/{site_name}.conf; + #REWRITE-END + + #禁止访问的文件或目录 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + {{ + return 404; + }} + + #一键申请SSL证书验证目录相关设置 + location ~ \.well-known{{ + allow all; + }} + + #禁止在证书验证目录放入敏感文件 + if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {{ + return 403; + }} + + location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ + {{ + expires 30d; + error_log /dev/null; + access_log /dev/null; + }} + + location ~ .*\\.(js|css)?$ + {{ + expires 12h; + error_log /dev/null; + access_log /dev/null; + }} + access_log {log_path}/{site_name}.log; + error_log {log_path}/{site_name}.error.log; +}}'''.format( + listen_port=self.sitePort, + listen_ipv6=listen_ipv6, + site_path=self.sitePath, + ssl_start_msg=public.getMsg('NGINX_CONF_MSG1'), + err_page_msg=public.getMsg('NGINX_CONF_MSG2'), + php_info_start=public.getMsg('NGINX_CONF_MSG3'), + php_version=self.phpVersion, + setup_path=self.setupPath, + rewrite_start_msg=public.getMsg('NGINX_CONF_MSG4'), + log_path=self.get_sites_log_path(), + site_name=self.siteName, + error_page_line=error_page_line + ) + + template_conf = None + if isinstance(template, str): + try: + template_conf = template.format( + listen_port=self.sitePort, + listen_ipv6=listen_ipv6, + site_path=self.sitePath, + ssl_start_msg=public.getMsg('NGINX_CONF_MSG1'), + err_page_msg=public.getMsg('NGINX_CONF_MSG2'), + php_info_start=public.getMsg('NGINX_CONF_MSG3'), + php_version=self.phpVersion, + setup_path=self.setupPath, + rewrite_start_msg=public.getMsg('NGINX_CONF_MSG4'), + log_path=self.get_sites_log_path(), + site_name=self.siteName, + error_page_line=error_page_line + ) + except: + template_conf = None + # 写配置文件 + if not os.path.exists("/www/server/panel/vhost/nginx/well-known"): + os.makedirs("/www/server/panel/vhost/nginx/well-known", 0o600) + public.writeFile("/www/server/panel/vhost/nginx/well-known/{}.conf".format(self.siteName), "") + filename = self.setupPath + '/panel/vhost/nginx/' + self.siteName + '.conf' + if template_conf is not None: + public.writeFile(filename, template_conf) + else: + public.writeFile(filename, conf) + + # 生成伪静态文件 + urlrewritePath = self.setupPath + '/panel/vhost/rewrite' + urlrewriteFile = urlrewritePath + '/' + self.siteName + '.conf' + if not os.path.exists(urlrewritePath): os.makedirs(urlrewritePath) + open(urlrewriteFile, 'w+').close() + if not os.path.exists(urlrewritePath): + public.writeFile(urlrewritePath, '') + + return True + + # 删除站点 + def DeleteSite(self, get, multiple=None): + try: + proxyconf = [] if not os.path.exists(self.__proxyfile) else json.loads(public.readFile(self.__proxyfile)) + id = get.id + if public.M('sites').where('id=?', (id,)).count() < 1: return public.returnResult(False, '指定站点不存在!') + siteName = get.webname + get.siteName = siteName + # 删除反向代理 + for i in range(len(proxyconf) - 1, -1, -1): + if proxyconf[i]["sitename"] == siteName: + del proxyconf[i] + public.writeFile(self.__proxyfile, json.dumps(proxyconf)) + m_path = self.setupPath + '/panel/vhost/nginx/proxy/' + siteName + if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) + + # 删除目录保护 + _dir_aith_file = "%s/panel/data/site_dir_auth.json" % self.setupPath + _dir_aith_conf = public.readFile(_dir_aith_file) + # 删除保护目录 + if _dir_aith_conf: + try: + _dir_aith_conf = json.loads(_dir_aith_conf) + if siteName in _dir_aith_conf: + del (_dir_aith_conf[siteName]) + except: + pass + public.writeFile(_dir_aith_file, _dir_aith_conf) + dir_aith_path = self.setupPath + '/panel/vhost/nginx/dir_auth/' + siteName + if os.path.exists(dir_aith_path): public.ExecShell("rm -rf %s" % dir_aith_path) + + # 删除重定向 + __redirectfile = "%s/panel/data/redirect.conf" % self.setupPath + redirectconf = [] if not os.path.exists(__redirectfile) else json.loads(public.readFile(__redirectfile)) + for i in range(len(redirectconf) - 1, -1, -1): + if redirectconf[i]["sitename"] == siteName: + del redirectconf[i] + public.writeFile(__redirectfile, json.dumps(redirectconf)) + m_path = self.setupPath + '/panel/vhost/nginx/redirect/' + siteName + if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) + + # 删除配置文件 + confPath = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if os.path.exists(confPath): os.remove(confPath) + + # 删除伪静态文件 + filename = '/www/server/panel/vhost/rewrite/' + siteName + '.conf' + if os.path.exists(filename): + os.remove(filename) + public.ExecShell("rm -f " + confPath + '/rewrite/' + siteName + "_*") + + # 删除日志文件 + filename = public.GetConfigValue('logs_path') + '/' + siteName + '*' + public.ExecShell("rm -f " + filename) + + # 重载服务 + public.serviceReload() + + # 从数据库删除 + public.M('sites').where("id=?", (id,)).delete() + public.M('binding').where("pid=?", (id,)).delete() + public.M('domain').where("pid=?", (id,)).delete() + public.WriteLog('TYPE_SITE', "SITE_DEL_SUCCESS", (siteName,)) + + # 是否删除关联数据库 + if hasattr(get, 'database'): + if get.database == '1': + find = public.M('databases').where("pid=?", (id,)).field('id,name').find() + if find: + import database + get.name = find['name'] + get.id = find['id'] + database.database().DeleteDatabase(get) + + # 是否删除关联FTP + if hasattr(get, 'ftp'): + if get.ftp == '1': + find = public.M('ftps').where("pid=?", (id,)).field('id,name').find() + if find: + import ftp + get.username = find['name'] + get.id = find['id'] + ftp.ftp().DeleteUser(get) + try: + # 删除项目 + Redirect().remove_redirect_by_project_name(siteName) + except: + pass + RealServer().del_daemon(siteName) + remove_sites_service_config(siteName) + return public.returnResult(True, '删除成功') + except: + return public.returnResult(False, traceback.format_exc()) + + def get_sites_log_path(get=None): + log_path = public.readFile("{}/data/sites_log_path.pl".format(public.get_panel_path())) + if isinstance(log_path, str) and os.path.isdir(log_path): + return log_path + return public.GetConfigValue('logs_path') + + # 检查端口是否被占用 + def check_port_is_used(self, port, sock=False): + ''' + @name 检查端口是否被占用 + @author sww + @param port: int<端口> + @return bool + ''' + if not isinstance(port, int): port = int(port) + if port == 0: return False + project_list = public.M('sites').where('status=? AND project_type=?', (1, 'PHP')).field( + 'name,path,project_config').select() + for project_find in project_list: + project_config = json.loads(project_find['project_config']) + if not 'port' in project_config: continue + try: + if int(project_config['port']) == port: + return True + except: + continue + if sock: return False + return public.check_tcp('127.0.0.1', port) + + # 获取项目列表 + def get_project_list(self, get): + ''' + @name 获取项目列表 + @author sww + @param get{ + sitename: string<项目名称> + } + @return dict + ''' + try: + if not 'p' in get: get.p = 1 + if not 'limit' in get: get.limit = 20 + if not 'callback' in get: get.callback = '' + if not 'order' in get: get.order = 'id desc' + type_id = None + if "type_id" in get: + try: + type_id = int(get.type_id) + except: + type_id = None + + if 'search' in get: + get.sitename = get.search.strip() + search = "%{}%".format(get.sitename) + if type_id is None: + count = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)', ('PHP', search, search)).count() + data = public.get_page(count, int(get.p), int(get.limit), get.callback) + data['data'] = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)', ('PHP', search, search)).limit(data['shift'] + ',' + data['row']).order(get.order).select() + else: + count = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?) AND type_id = ?', + ('PHP', search, search, type_id)).count() + data = public.get_page(count, int(get.p), int(get.limit), get.callback) + data['data'] = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?) AND type_id = ?', ('PHP', search, search, type_id)).limit( + data['shift'] + ',' + data['row']).order(get.order).select() + else: + if type_id is None: + count = public.M('sites').where('project_type=?', 'PHP').count() + data = public.get_page(count, int(get.p), int(get.limit), get.callback) + data['data'] = public.M('sites').where('project_type=?', 'PHP').limit(data['shift'] + ',' + data['row']).order(get.order).select() + else: + count = public.M('sites').where('project_type=? AND type_id = ?', ('PHP', type_id)).count() + data = public.get_page(count, int(get.p), int(get.limit), get.callback) + data['data'] = public.M('sites').where('project_type=? AND type_id = ?', ('PHP', type_id)).limit(data['shift'] + ',' + data['row']).order(get.order).select() + + for i in range(len(data['data'])): + data['data'][i] = self.get_project_stat(data['data'][i]) + logs_path = '/tmp/{}_install_dependence.log'.format(data['data'][i]['name']) + if not os.path.exists(logs_path) or int(os.path.getmtime(logs_path)) - int(time.time()) > 20: + data['data'][i]['dependence'] = 0 + return public.returnResult(True, data=data) + except: + return public.returnResult(False, '获取项目列表失败') + + # 获取项目状态信息 + def get_project_stat(self, project_info): + ''' + @name 获取项目状态信息 + @author sww + @param project_info 项目信息 + @return list + ''' + project_info['project_config'] = json.loads(project_info['project_config']) + # project_info['project_config']['bind_extranet'] = int(project_info['project_config']['bind_extranet']) + # project_info['run'] = self.get_project_run_state(sitename=project_info['name']) + # project_info['load_info'] = self.get_project_load_info(sitename=project_info['name']) + project_info['run'] = RealServer().daemon_status(project_info['name'])['status'] + project_info['ssl'] = self.get_ssl_end_date(sitename=project_info['name']) + project_info['listen'] = [] + project_info['listen_ok'] = True + # if project_info['load_info']: + # for pid in project_info['load_info'].keys(): + # if not 'connections' in project_info['load_info'][pid]: + # project_info['load_info'][pid]['connections'] = [] + # if 'connections' in project_info['load_info'][pid]: + # for conn in project_info['load_info'][pid]['connections']: + # if not conn['status'] == 'LISTEN': continue + # if not conn['local_port'] in project_info['listen']: + # project_info['listen'].append(conn['local_port']) + # if project_info['listen']: + # project_info['listen_ok'] = project_info['project_config']['port'] in project_info['listen'] + return project_info + + # 获取指定项目的域名列表 + def project_get_domain(self, get): + ''' + @name 获取指定项目的域名列表 + @author sww + @param get{ + sitename: string<项目名称> + } + @return dict + ''' + try: + project_id = public.M('sites').where('name=?', (get.sitename,)).getField('id') + domains = public.M('domain').where('pid=?', (project_id,)).order('id desc').select() + # project_find = self.get_project_find(get.sitename) + # if len(domains) != len(project_find['project_config']['domains']): + # public.M('domain').where('pid=?', (project_id,)).delete() + # if not project_find: return [] + # for d in project_find['project_config']['domains']: + # domain = {} + # arr = d.split(':') + # if len(arr) < 2: arr.append(80) + # domain['name'] = arr[0] + # domain['port'] = int(arr[1]) + # domain['pid'] = project_id + # domain['addtime'] = public.getDate() + # public.M('domain').insert(domain) + # if project_find['project_config']['domains']: + # domains = public.M('domain').where('pid=?', (project_id,)).select() + return public.returnResult(True, data=domains) + except: + return public.returnResult(False, traceback.format_exc()) + + # 获取指定项目配置 + def get_project_find(self, sitename): + ''' + @name 获取指定项目配置 + @author sww + @param sitename 项目名称 + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?', ('PHP', sitename)).find() + if not project_info: return False + project_info['project_config'] = json.loads(project_info['project_config']) + if 'run_user' not in project_info['project_config'].keys(): + project_info['run_user'] = 'www' + return project_info + + # 获取项目SSL信息 + def get_ssl_end_date(self, sitename): + ''' + @name 获取SSL信息 + @author sww + @param sitename 项目名称 + @return dict + ''' + import data + return data.data().get_site_ssl_info(sitename) + + # 删除指定项目中的域名 + def project_remove_domain(self, get): + ''' + @name 为指定项目删除域名 + @author sww + @param get{ + sitename: string<项目名称> + domain: string<域名> + } + @return dict + ''' + try: + project_find = self.get_project_find(get.sitename) + if not project_find: + return public.returnResult(False, '指定项目不存在') + result = [] + domain_list = json.loads(get.domain) + project_id = public.M('sites').where('name=?', (get.sitename,)).getField('id') + for domain in domain_list: + if public.M('domain').where('pid=?', (project_id,)).count() == 1: + result.append({'name': domain, 'msg': "项目中至少需要一个域名", "status": False}) + continue + domain_id = public.M('domain').where('name=? AND pid=?', (domain, project_id)).getField('id') + if not domain_id: + return public.returnResult(False, '指定域名不存在') + public.M('domain').where('id=?', (domain_id,)).delete() + public.WriteLog(self._log_name, '从项目:{},删除域名{}'.format(get.sitename, get.domain)) + result.append({'name': domain, 'msg': "删除成功", "status": True}) + domain_list = public.M('domain').where('pid=?', (project_id)).select() + domain_list = [(domain['name'], str(domain['port'])) for domain in domain_list] + NginxDomainTool().nginx_set_domain(get.sitename, *domain_list) + return public.returnResult(True, data=result) + except: + return public.returnResult(False, traceback.format_exc()) + + # 为指定项目添加域名 + def project_add_domain(self, get): + ''' + @name 为指定项目添加域名 + @author sww + @param get{ + sitename: string<项目名称> + domains: list<域名列表> + } + @return dict + ''' + try: + project_find = self.get_project_find(get.sitename) + if not project_find: + return public.returnResult(False, '指定项目不存在') + project_id = project_find['id'] + domains = get.domains + if not isinstance(domains, list): + domains = json.loads(domains) + flag = False + res_domains = [] + for domain in domains: + domain = domain.strip() + if not domain: continue + if not self.check_domain(domain): + res_domains.append({"name": domain, "status": False, "msg": '域名格式错误'}) + continue + domain_arr = domain.split(':') + domain_arr[0] = check_domain(domain_arr[0]) + domain_arr[0] = self.ToPunycode(domain_arr[0]) + if domain_arr[0] is False: + res_domains.append({"name": domain, "status": False, "msg": '域名格式错误'}) + continue + if len(domain_arr) == 1: + domain_arr.append("") + if domain_arr[1] == "": + domain_arr[1] = 80 + domain += ':80' + try: + if not (0 < int(domain_arr[1]) < 65535): + res_domains.append({"name": domain, "status": False, "msg": '域名格式错误'}) + continue + except ValueError: + res_domains.append({"name": domain, "status": False, "msg": '域名格式错误'}) + continue + if not public.M('domain').where('name=?', (domain_arr[0],)).count(): + public.M('domain').add('name,pid,port,addtime', + (domain_arr[0], project_id, domain_arr[1], public.getDate())) + if not domain in project_find['project_config']['domains']: + project_find['project_config']['domains'].append(domain) + public.WriteLog(self._log_name, '成功添加域名{}到项目{}'.format(domain, get.sitename)) + res_domains.append({"name": domain_arr[0], "status": True, "msg": '添加成功'}) + flag = True + else: + public.WriteLog(self._log_name, '添加域名错误,域名{}已存在'.format(domain)) + res_domains.append({"name": domain_arr[0], "status": False, "msg": '添加失败,域名{}已存在'.format(domain)}) + if flag: + public.M('sites').where('id=?', (project_id,)).save('project_config', json.dumps(project_find['project_config'])) + domain_list = public.M('domain').where('pid=?', (project_id)).select() + domain_list = [(domain['name'], str(domain['port'])) for domain in domain_list] + NginxDomainTool().nginx_set_domain(get.sitename, *domain_list) + return self._ckeck_add_domain(get.sitename, res_domains) + except: + return public.returnResult(False, traceback.format_exc()) + + def _ckeck_add_domain(self, site_name, domains): + from panelSite import panelSite + ssl_data = panelSite().GetSSL(type("get", tuple(), {"siteName": site_name})()) + if not ssl_data["status"]: return public.returnResult(True, data={"domains": domains}, msg="添加成功") + domain_rep = [] + for i in ssl_data["cert_data"]["dns"]: + if i.startswith("*"): + _rep = r"^[^\.]+\." + i[2:].replace(".", r"\.") + else: + _rep = "^" + i.replace(".", r"\.") + domain_rep.append(_rep) + no_ssl = [] + for domain in domains: + if not domain["status"]: continue + for _rep in domain_rep: + if re.search(_rep, domain["name"]): + break + else: + no_ssl.append(domain["name"]) + if no_ssl: + return public.returnResult(True, data={ + "domains": domains, + "not_ssl": no_ssl, + "tip": "本站点已启用SSL证书,但本次添加的域名:{},无法匹配当前证书,如有需求,请重新申请证书。".format(str(no_ssl)) + }, msg="添加成功") + return public.returnResult(True, data={"domains": domains}, msg="添加成功") + + # 获取项目状态 + def get_project_run_state(self, get): + ''' + @name 获取项目运行状态信息 + @param sitename 项目名称 + @return dict + ''' + try: + sitename = get.sitename.strip() + result = {} + project_info = self.get_project_find(sitename) + res = RealServer().daemon_status(sitename) + result['status'] = res['status'] + result['sitename'] = sitename + result['is_power_on'] = int(project_info["project_config"]['is_power_on']) + result['project_path'] = project_info['path'] # type : str + result['project_cmd'] = project_info["project_config"].get('start_cmd', '') + result['project_port'] = project_info['project_config']['port'] + if result['project_path'][-1] != "/": + result['project_path'] += "/" + result['site_run_path'] = DirTool().get_site_run_path(sitename).replace(result['project_path'].rstrip("/"), "") + if not result['site_run_path']: + result['site_run_path'] = "/" + result['php_version'] = project_info['project_config']['php_version'] + result['run_user'] = project_info['project_config'].get('run_user', 'www') + result['ps'] = project_info['ps'] + result['pid'] = RealServer().get_daemon_pid(sitename)["data"] + if result['pid'] in [0, '0', ''] and result['status']: + res = public.ExecShell("""ps -aux |grep "{}"|grep -v "grep"|awk '{{print $2}}'""".format(result['project_cmd']))[0] + res = res.strip().split("\n") + if res and len(res) >= 1: + res.sort() + result['pid'] = res[0] + result['Listen'] = [] + if result['pid']: + children = RealProcess().get_process_tree(result['pid'])["data"] + for chi in children: + for i in chi.get("connections", []): + port = i.get("local_port", '') + addr = i.get("local_addr", '') + status = i.get("status", 'dasd') + if addr == '0.0.0.0': + addr = public.GetLocalIp() + if port and addr and status.lower() == 'listen': + result['Listen'].append((addr, port)) + result['Listen'] = list(set(result['Listen'])) + + return public.returnResult(True, data=result) + except: + public.print_log(traceback.format_exc()) + return public.returnResult(False, '获取项目状态失败') + + def domain_to_puny_code(self, domain: str) -> str: + new_domain = '' + for dkey in domain.split('.'): + if dkey == '*' or dkey == "": + continue + # 匹配非ascii字符 + match = re.search(u"[\x80-\xff]+", dkey) + if not match: + match = re.search(u"[\u4e00-\u9fa5]+", dkey) + if not match: + new_domain += dkey + '.' + else: + new_domain += 'xn--' + dkey.encode('punycode').decode('utf-8') + '.' + if domain.startswith('*.'): + new_domain = "*." + new_domain + return new_domain[:-1] + + def check_domain(self, domain: str): + domain = self.domain_to_puny_code(domain) + # 判断通配符域名格式 + if domain.find('*') != -1 and domain.find('*.') == -1: + return None + + # 判断域名格式 + rep_domain = re.compile(r"^([\w\-*]{1,100}\.){1,24}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$") + if not rep_domain.match(domain): + return None + return domain + + # 修改项目运行状态 + def modify_project_run_state(self, get): + sitename = get.sitename + project_action = get.project_action + logs_path = "/www/server/phpa_project/logs" + if not os.path.exists(logs_path): + public.ExecShell('mkdir -p {}'.format(logs_path)) + public.ExecShell('chmod 777 -R {}'.format(logs_path)) + if project_action in ['start', 'restart']: + self.async_dependence_config(sitename) + res = RealServer().daemon_admin(sitename, project_action) + if res['status'] and project_action in ['start', 'restart']: + public.M('sites').where('name=? and project_type=?', (sitename, 'PHP')).setField('status', 1) + elif res['status'] or project_action == 'stop': + public.M('sites').where('name=? and project_type=?', (sitename, 'PHP')).setField('status', 0) + # if project_action in ['start', 'restart'] and not res['status']: + # log = os.path.join(self._phpa_logs, sitename + '.log') + # logs = public.GetNumLines(log, 5) + # res['msg'] += '\n' + logs + # res['msg'] += '\n 详情前往日志-项目日志查看' + + if project_action in ['start', 'restart']: + site_info = self.get_project_find(sitename) + for i in range(20): + time.sleep(0.1) + conn = public.ExecShell('systemctl status {}'.format(sitename)) + public.print_log(conn) + if 'deactivating' in conn[0]: + # 开启forking模式 + sys_conf = '/usr/lib/systemd/system/{}.service'.format(sitename) + sys_conf = public.readFile(sys_conf) + public.print_log(sys_conf) + if 'Type=forking' not in sys_conf: + sys_conf = sys_conf.replace('Type=simple', 'Type=forking') + public.writeFile('/usr/lib/systemd/system/{}.service'.format(sitename), sys_conf) + public.ExecShell('systemctl daemon-reload') + public.ExecShell('systemctl restart {}'.format(sitename)) + + pid = RealServer().get_daemon_pid(sitename)["data"] + if pid in [0, '0', '']: + res = public.ExecShell("""ps -aux |grep "{}"|grep -v "grep"|awk '{{print $2}}'""".format(site_info['project_config']['project_cmd']))[0] + res = res.strip().split("\n") + if res and len(res) >= 1: + res.sort() + pid = res[0] + if pid not in [0, '0', '']: + pids = psutil.Process(int(pid)).children(recursive=True) + pids = [str(i.pid) for i in pids] + pids.append(str(pid)) + if pid not in [0, '0']: + res = public.ExecShell('lsof -i |grep -E "{}" |grep LISTEN'.format('|'.join(pids))) + if res[0]: + return public.returnResult(True, '启动成功') + return public.returnResult(False, '启动失败') + return public.returnResult(True, '关闭成功') + + def async_dependence_config(self, sitename): + """ + 增加安装依赖的配置文件 + """ + try: + config = self.get_project_find(sitename) + # public.ExecShell("chown -R {}:{} {}".format(config['project_config'].run_user.strip(), config['project_config'].run_user.strip(), get.site_path)) + # public.ExecShell("chmod -R 755 {}".format(config['path'])) + php_version = config['project_config']['php_version'] + php_ini_path = os.path.join(config['path'], 'php-cli.ini') + public.ExecShell('cp /www/server/php/{}/etc/php-cli.ini {}'.format(php_version, config['path'])) + public.ExecShell('chown root:root {}'.format(php_ini_path)) + public.ExecShell('chmod 644 {}'.format(php_ini_path)) + public.ExecShell("sed -i '/disable_functions/d' {}".format(php_ini_path)) + public.writeFile(php_ini_path, php_cli_ini) + + except: + pass + + # 修改项目网站运行目录 + def modify_project_path(self, get): + if not hasattr(get, 'new_run_path_sub'): + return public.returnResult(False, '新的运行目录不能为空') + new_run_path_sub = get.new_run_path_sub + sitename = get.sitename.strip() + project_info = self.get_project_find(sitename) + DirTool().modify_site_run_path(sitename, project_info['path'], new_run_path_sub) + return public.returnResult(True, '修改成功') + + def modify_project(self, get): + try: + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + if not hasattr(get, 'project_cmd'): + return public.returnResult(False, 'project_cmd不能为空') + if not hasattr(get, 'project_path'): + return public.returnResult(False, 'project_path不能为空') + if not hasattr(get, 'site_run_path'): + return public.returnResult(False, 'site_run_path不能为空') + if not hasattr(get, 'run_user'): + get.run_user = 'www' + + public.ExecShell('chown -R {}:{} {}'.format(get.run_user, get.run_user, get.project_path)) + public.ExecShell('chmod 755 {}'.format(get.project_path)) + config = self.get_project_find(get.sitename) + config['project_config']['run_user'] = get.run_user + start_cmd = get.project_cmd + if 'php' == get.project_cmd[:3]: + get.project_cmd = '/www/server/php/{}/bin/php -c {}/php-cli.ini {}'.format(get.php_version, get.project_path, get.project_cmd[3:]) + else: + get.project_cmd = '/www/server/php/{}/bin/php -c {}/php-cli.ini {}'.format(get.php_version, get.project_path, get.project_cmd) + if config['project_config'].get('start_cmd') != start_cmd: + config['project_config']['start_cmd'] = start_cmd + if config['path'] != get.project_path: + if not os.path.exists(get.project_path): + return public.returnResult(False, '项目目录不存在') + config['path'] = get.project_path + config['project_config']['project_path'] = get.project_path + + # get.site_run_path = os.path.join(config['path'], get.site_run_path.lstrip("/")) + config['project_config']['site_run_path'] = get.site_run_path.rstrip('/') + get.new_run_path_sub = config['project_config']['site_run_path'] + self.modify_project_path(get) + if config['project_config']['php_version'] != get.php_version: + config['project_config']['php_version'] = get.php_version + public.ExecShell('cp /www/server/php/{}/etc/php-cli.ini {}'.format(get.php_version, config['path'])) + public.ExecShell('chown root:root {}'.format(os.path.join(config['path'], 'php-cli.ini'))) + public.ExecShell('chmod 644 {}'.format(os.path.join(config['path'], 'php-cli.ini'))) + public.ExecShell("sed -i '/disable_functions/d' {}".format(os.path.join(config['path'], 'php-cli.ini'))) + if config['project_config']['project_cmd'] != get.project_cmd or config['project_config']['is_power_on'] != get.get('is_power_on', 1) or config['project_config'][ + 'run_user'] != get.run_user: + config['project_config']['is_power_on'] = get.get('is_power_on', 1) + config['project_config']['project_cmd'] = get.project_cmd + systemd_conf = '/usr/lib/systemd/system/{}.service'.format(get.sitename) + conf = public.readFile(systemd_conf) + is_fork = 0 + if conf: + if 'Type=forking' in conf: + is_fork = 1 + realserver = RealServer() + realserver.create_daemon(get.sitename, '', get.project_cmd, get.project_path, config['project_config'].get('run_user', 'www'), get.is_power_on, + logs_file=os.path.join(self._phpa_logs, get.sitename + '.log'), is_fork=is_fork) + if config['ps'] != get.get('ps', '') and get.get('ps', ''): + public.M('sites').where('name=? and project_type=?', (get.sitename, 'PHP')).setField('ps', get.get('ps', '')) + config['project_config']['port'] = get.get('project_port', 0) + public.M('sites').where('name=? and project_type=?', (get.sitename, 'PHP')).setField('project_config', json.dumps(config['project_config'])) + return public.returnResult(True, '修改成功') + except: + return public.returnResult(False, traceback.format_exc()) + + def get_project_log(self, get): + log_file = self._phpa_logs + '/' + get.sitename + '.log' + if not os.path.exists(log_file): + return public.returnResult(status=True, msg='暂无项目日志') + return public.returnResult(True, msg=public.GetNumLines(log_file, 1000)) + + def get_access_log(self, get): + from logsModel.siteModel import main as siteModel + get.sitename = get.siteName.strip() + sitelog = siteModel() + return public.returnResult(True, data=sitelog.get_site_access_logs(get)['msg']) + + def get_error_log(self, get): + from logsModel.siteModel import main as siteModel + get.sitename = get.siteName.strip() + sitelog = siteModel() + return public.returnResult(True, data=sitelog.get_site_error_logs(get)['msg']) + + def get_config_file(self, get): + result = {} + site_name = get.sitename + conf = self.get_project_find(site_name) + result['nginx配置文件'] = self.setupPath + '/panel/vhost/nginx/' + site_name + '.conf' + result['php-cli配置文件'] = os.path.join(conf['path'], 'php-cli.ini') + env_path = os.path.join(conf['path'], '.env') + if os.path.exists(env_path): + result['.env配置文件'] = env_path + result['伪静态配置文件'] = "/www/server/panel/vhost/rewrite/{}.conf".format(site_name) + composer_path = os.path.join(conf['path'], 'composer.json') + if os.path.exists(composer_path): + result['composer配置文件'] = composer_path + return public.returnResult(True, data=result) + + # 上传版本 + def upload_version(self, get): + """ + 上传压缩包并存储为版本 + """ + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return public.returnResult(False, '版本号不能为空') + if not hasattr(get, 'ps'): + get.ps = '' + try: + upload_files = os.path.join("/tmp", get.f_name) + from files import files + fileObj = files() + ff = fileObj.upload(get) + if type(ff) == int: + return ff + if not ff['status']: + return public.returnResult(False, ff['msg']) + output_dir = str(os.path.join('/tmp', public.GetRandomString(16))) + os.makedirs(output_dir, 777) + if not self.extract_archive(upload_files, output_dir)[0]: + return public.returnResult(False, '解压失败,仅支持zip,tar.gz,tar,bz2,gz,xz格式的压缩包') + if len(os.listdir(output_dir)) == 1: + output_dir = str(os.path.join(output_dir, os.listdir(output_dir)[0])) + versiontool = VersionTool() + res = versiontool.publish_by_src_path(get.sitename, output_dir, get.version, get.ps, sync=True) + public.ExecShell('rm -rf {}'.format(output_dir)) + public.ExecShell('rm -rf {}'.format(upload_files)) + if res is None: + return public.returnResult(True, '添加成功') + return public.returnResult(False, '添加失败' + res) + except: + return public.returnResult(False, traceback.format_exc()) + + def extract_archive(self, file_path, output_dir): + name = os.path.basename(file_path) + if name.endswith('.tar.gz'): + with tarfile.open(file_path, 'r:gz') as tar: + tar.extractall(output_dir) + elif name.endswith('.zip'): + with zipfile.ZipFile(file_path, 'r') as zip_ref: + zip_ref.extractall(output_dir) + elif name.endswith('.gz'): + with gzip.open(file_path, 'rb') as f_in, open(os.path.join(output_dir, name[:-3]), 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + elif name.endswith('.tar'): + with tarfile.open(file_path, 'r') as tar: + tar.extractall(output_dir) + elif name.endswith('.bz2'): + with open(file_path, 'rb') as f_in, open(os.path.join(output_dir, name[:-4]), 'wb') as f_out: + with bz2.BZ2File(f_in) as bz: + shutil.copyfileobj(bz, f_out) + # elif name.endswith('.xz'): + # with lzma.open(file_path, 'rb') as f_in, open(os.path.join(output_dir, name[:-3]), 'wb') as f_out: + # shutil.copyfileobj(f_in, f_out) + else: + return False, '文件格式错误.' + + return True, '解压成功' + + # 获取列表 + def get_version_list(self, get): + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + versiontool = VersionTool() + return public.returnResult(True, data=versiontool.version_list(get.sitename)) + + # 删除版本 + def remove_version(self, get): + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return public.returnResult(False, '版本号不能为空') + versiontool = VersionTool() + if versiontool.remove(get.sitename, get.version) is None: + return public.returnResult(True, '删除成功') + return public.returnResult(False, '删除失败') + + # 恢复版本 + def recover_version(self, get): + try: + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return publihuoc.returnResult(False, '版本号不能为空') + conf = self.get_project_find(get.sitename) + versiontool = VersionTool() + res = versiontool.recover(get.sitename, get.version, conf['path'], DirTool().get_site_run_path(get.sitename)) + if res is not True: + return public.returnResult(False, res) + return public.returnResult(True, '恢复成功') + except: + return public.returnResult(False, traceback.format_exc()) + + def now_file_backup(self, get): + try: + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return public.returnResult(False, '版本号不能为空') + if not hasattr(get, 'ps'): + get.ps = '' + config = self.get_project_find(get.sitename) + path = config['path'] + versiontool = VersionTool() + res = versiontool.publish_by_src_path(get.sitename, path, get.version, get.ps, sync=True) + if res is None: + return public.returnResult(True, '添加成功') + return public.returnResult(False, '添加失败' + res) + except: + return public.returnResult(False, traceback.format_exc()) + + def set_version_ps(self, get): + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + if not hasattr(get, 'version'): + return public.returnResult(False, '版本号不能为空') + if not hasattr(get, 'ps'): + get.ps = '' + versiontool = VersionTool() + versiontool.set_ps(get.sitename, get.version, get.ps) + return public.returnResult(True, '设置成功') + + def get_setup_log(self, get): + log_file = "/tmp/{}_install_dependence.log".format(get.sitename) + if not os.path.exists(log_file): + return public.returnResult(True, data='暂无项目日志') + return public.returnResult(True, data=public.GetNumLines(log_file, 1000)) + + def add_crontab(self, get): + try: + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + if not hasattr(get, 'cron_name'): + return public.returnResult(False, '定时任务名称不能为空') + if not hasattr(get, 'sBody'): + return public.returnResult(False, '定时任务内容不能为空') + args = { + "name": get.cron_name, + "type": get.type, + "where1": get.where1, + "hour": get.hour, + "minute": get.minute, + "week": get.week, + "sType": "toShell", + "sName": "", + "backupTo": "localhost", + "save": get.sitename, + "sBody": get.sBody, + "urladdress": "", + "flock": get.flock + } + import crontab + res = crontab.crontab().AddCrontab(public.to_dict_obj(args)) + if res['status']: + return public.returnResult(True, msg='添加成功!') + return public.returnResult(False, res['msg']) + except: + return public.returnResult(False, traceback.format_exc()) + + def get_crontab_list(self, get): + try: + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名称不能为空') + cront = public.M('crontab').where('save=?', (get.sitename,)).select() + + data = [] + for i in range(len(cront)): + tmp = cront[i] + + if cront[i]['type'] == "day": + tmp['type_zh'] = public.getMsg('CRONTAB_TODAY') + tmp['cycle'] = public.getMsg('CRONTAB_TODAY_CYCLE', + (str(cront[i]['where_hour']), str(cront[i]['where_minute']))) + elif cront[i]['type'] == "day-n": + tmp['type_zh'] = public.getMsg('CRONTAB_N_TODAY', (str(cront[i]['where1']),)) + tmp['cycle'] = public.getMsg('CRONTAB_N_TODAY_CYCLE', ( + str(cront[i]['where1']), str(cront[i]['where_hour']), str(cront[i]['where_minute']))) + elif cront[i]['type'] == "hour": + tmp['type_zh'] = public.getMsg('CRONTAB_HOUR') + tmp['cycle'] = public.getMsg('CRONTAB_HOUR_CYCLE', (str(cront[i]['where_minute']),)) + elif cront[i]['type'] == "hour-n": + tmp['type_zh'] = public.getMsg('CRONTAB_N_HOUR', (str(cront[i]['where1']),)) + tmp['cycle'] = public.getMsg('CRONTAB_N_HOUR_CYCLE', + (str(cront[i]['where1']), str(cront[i]['where_minute']))) + elif cront[i]['type'] == "minute-n": + tmp['type_zh'] = public.getMsg('CRONTAB_N_MINUTE', (str(cront[i]['where1']),)) + tmp['cycle'] = public.getMsg('CRONTAB_N_MINUTE_CYCLE', (str(cront[i]['where1']),)) + elif cront[i]['type'] == "week": + tmp['type_zh'] = public.getMsg('CRONTAB_WEEK') + if not cront[i]['where1']: cront[i]['where1'] = '0' + tmp['cycle'] = public.getMsg('CRONTAB_WEEK_CYCLE', ( + self.toWeek(int(cront[i]['where1'])), str(cront[i]['where_hour']), + str(cront[i]['where_minute']))) + elif cront[i]['type'] == "month": + tmp['type_zh'] = public.getMsg('CRONTAB_MONTH') + tmp['cycle'] = public.getMsg('CRONTAB_MONTH_CYCLE', ( + str(cront[i]['where1']), str(cront[i]['where_hour']), str(cront[i]['where_minute']))) + + log_file = '/www/server/cron/{}.log'.format(tmp['echo']) + if os.path.exists(log_file): + tmp['addtime'] = self.get_last_exec_time(log_file) + data.append(tmp) + + for i in data: + if i['backup_mode'] == "1": + i['backup_mode'] = 1 + else: + i['backup_mode'] = 0 + if i['db_backup_path'] == "": + i['db_backup_path'] = "/www/backup" + + if not i.get('rname', ''): + i['rname'] = i['name'] + if i['time_type'] == 'sweek': + i['type'] = 'sweek' + week_str = self.toweek(i['time_set']) + if week_str: # 检查week_str是否为空 + i['type_zh'] = week_str + i['cycle'] = "每" + week_str + i['special_time'] + "执行" + + elif i['time_type'] == 'sday': + i['type'] = 'sweek' + i['type_zh'] = i['special_time'] + i['cycle'] = "每天" + i['special_time'] + "执行" + + elif i['time_type'] == 'smonth': + i['type'] = 'sweek' + i['type_zh'] = i['special_time'] + i['cycle'] = "每月" + i['time_set'] + "号" + i['special_time'] + "执行" + if i['sType'] == 'site_restart': + i['cycle'] = "每天" + i['special_time'] + "执行" + return public.returnResult(True, data=data) + except: + return public.returnResult(False, traceback.format_exc()) + + # 转换大写星期 + def toWeek(self, num): + wheres = { + 0: public.getMsg('CRONTAB_SUNDAY'), + 1: public.getMsg('CRONTAB_MONDAY'), + 2: public.getMsg('CRONTAB_TUESDAY'), + 3: public.getMsg('CRONTAB_WEDNESDAY'), + 4: public.getMsg('CRONTAB_THURSDAY'), + 5: public.getMsg('CRONTAB_FRIDAY'), + 6: public.getMsg('CRONTAB_SATURDAY') + } + try: + return wheres[num] + except: + return '' + + def get_last_exec_time(self, log_file): + ''' + @name 获取上次执行时间 + @author hwliang + @param log_file 日志文件路径 + @return format_date + ''' + exec_date = '' + try: + log_body = public.GetNumLines(log_file, 20) + if log_body: + log_arr = log_body.split('\n') + date_list = [] + for i in log_arr: + if i.find('★') != -1 and i.find('[') != -1 and i.find(']') != -1: + date_list.append(i) + if date_list: + exec_date = date_list[-1].split(']')[0].split('[')[1] + except: + pass + + finally: + if not exec_date: + exec_date = public.format_date(times=int(os.path.getmtime(log_file))) + return exec_date + + def start_task(self, get): + try: + if not hasattr(get, 'id'): + return public.returnResult(False, 'ID不能为空') + import crontab + res = crontab.crontab().StartTask(get) + if res['status']: + return public.returnResult(True, '启动成功') + return public.returnResult(False, res['msg']) + except: + return public.returnResult(False, traceback.format_exc()) + + def modify_crontab_status(self, get): + try: + if not hasattr(get, 'id'): + return public.returnResult(False, 'ID不能为空') + if hasattr(get, 'status'): + cronInfo = public.M('crontab').where('id=?', (get.id,)).select()[0] + if int(cronInfo['status']) == int(get.status): + return public.returnResult(True, '修改成功') + get.if_stop = False + import crontab + res = crontab.crontab().set_cron_status(get) + if res['status']: + return public.returnResult(True, '修改成功') + return public.returnResult(False, res['msg']) + except: + return public.returnResult(False, traceback.format_exc()) + + def remove_crontab(self, get): + try: + if not hasattr(get, 'id'): + return public.returnResult(False, 'ID不能为空') + import crontab + res = crontab.crontab().DelCrontab(get) + if res['status']: + return public.returnResult(True, '删除成功') + return public.returnResult(False, res['msg']) + except: + return public.returnResult(False, traceback.format_exc()) + + def modify_crontab(self, get): + try: + if not hasattr(get, 'id'): + return public.returnResult(False, 'ID不能为空') + self.remove_crontab(get) + res = self.add_crontab(get) + res['msg'] = res['msg'].replace('添加', '修改') + return res + except: + return public.returnResult(False, traceback.format_exc()) + + def get_crontab_log(self, get): + try: + if not hasattr(get, 'id'): + return public.returnResult(False, 'ID不能为空') + import crontab + res = crontab.crontab().GetLogs(get) + return public.returnResult(True, res['msg']) + except: + return public.returnResult(False, traceback.format_exc()) + + def clearn_logs(self, get): + try: + if not hasattr(get, 'id'): + return public.returnResult(False, '项目名称不能为空') + import crontab + crontab.crontab().DelLogs(get) + return public.returnResult(True, '清空成功') + except: + return public.returnResult(False, traceback.format_exc()) + + def get_group_file(self): + config = json.loads(public.readFile(self.group_file)) + return config + + def get_group_list(self, get): + try: + config = self.get_group_file() + result = [] + for i, j in config.items(): + tmp = [] + for k in j['project_list']: + con = self.get_project_run_state(public.to_dict_obj({'sitename': k}))['data'] + if con: + tmp.append(con) + else: + self.group_remove_project(public.to_dict_obj({'group_name': i, 'sitename': k})) + j['project_list'] = tmp + j['name'] = i + result.append(j) + + for i in result: + for j in i['project_list']: + if not j: + print(j) + continue + if not j['status']: + i['status'] = 0 + break + if len(i['project_list']) == 0: + i['status'] = 0 + return public.returnResult(True, data=result) + except: + return public.returnResult(False, traceback.format_exc()) + + def set_order(self, get): + project_list = get.project_list.strip(',').split(',') + group_name = get.group_name + config = self.get_group_file() + if group_name not in config: + return public.returnResult(False, '组名不存在') + config[group_name]['project_list'] = project_list + public.writeFile(self.group_file, json.dumps(config)) + return public.returnResult(True, '设置成功') + + def create_group(self, get): + try: + if not hasattr(get, 'group_name'): + return public.returnResult(False, '组名不能为空') + config = self.get_group_file() + if get.group_name in config: + return public.returnResult(False, '组名已存在') + config[get.group_name] = {'interval': get.get('interval', 30), 'project_list': [], 'status': 0} + public.writeFile(self.group_file, json.dumps(config)) + return public.returnResult(True, '添加成功') + except: + return public.returnResult(False, traceback.format_exc()) + + def remove_group(self, get): + if not hasattr(get, 'group_name'): + return public.returnResult(False, '组名不能为空') + config = self.get_group_file() + if get.group_name not in config: + return public.returnResult(False, '组名不存在') + del config[get.group_name] + public.writeFile(self.group_file, json.dumps(config)) + return public.returnResult(True, '删除成功') + + def group_add_project(self, get): + try: + if not hasattr(get, 'group_name'): + return public.returnResult(False, '组名不能为空') + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名不能为空') + config = self.get_group_file() + if get.group_name not in config: + return public.returnResult(False, '组名不存在') + if get.sitename not in config[get.group_name]['project_list']: + config[get.group_name]['project_list'].append(get.sitename) + flag = 1 + for i in config[get.group_name]['project_list']: + if not self.get_project_run_state(public.to_dict_obj({'sitename': i}))['status']: + flag = 0 + break + if flag: + config[get.group_name]['status'] = 1 + public.writeFile(self.group_file, json.dumps(config)) + return public.returnResult(True, '添加成功') + except: + return public.returnResult(False, traceback.format_exc()) + + def group_remove_project(self, get): + try: + if not hasattr(get, 'group_name'): + return public.returnResult(False, '组名不能为空') + if not hasattr(get, 'sitename'): + return public.returnResult(False, '项目名不能为空') + config = self.get_group_file() + if get.group_name not in config: + return public.returnResult(False, '组名不存在') + if get.sitename not in config[get.group_name]['project_list']: + return public.returnResult(False, '项目不存在') + config[get.group_name]['project_list'].remove(get.sitename) + flag = 1 + for i in config[get.group_name]['project_list']: + if not self.get_project_run_state(public.to_dict_obj({'sitename': i}))['status']: + flag = 0 + break + if flag: + config[get.group_name]['status'] = 1 + else: + config[get.group_name]['status'] = 0 + public.writeFile(self.group_file, json.dumps(config)) + return public.returnResult(True, '删除成功') + except: + return public.returnResult(False, traceback.format_exc()) + + def set_group_interval(self, get): + if not hasattr(get, 'group_name'): + return public.returnResult(False, '组名不能为空') + if not hasattr(get, 'interval'): + return public.returnResult(False, '间隔时间不能为空') + config = self.get_group_file() + if get.group_name not in config: + return public.returnResult(False, '组名不存在') + config[get.group_name]['interval'] = get.interval + public.writeFile(self.group_file, json.dumps(config)) + return public.returnResult(True, '设置成功') + + def set_group_status(self, get): + if not hasattr(get, 'group_name'): + return public.returnResult(False, '组名不能为空') + if not hasattr(get, 'status'): + return public.returnResult(False, '状态不能为空') + config = self.get_group_file() + if get.group_name not in config: + return public.returnResult(False, '组名不存在') + if get.status == 'stop': + public.run_thread(self.stop_groups, (get.group_name,)) + return public.returnResult(True, '开始停止项目') + if get.status == 'start': + public.run_thread(self.start_groups, (get.group_name,)) + return public.returnResult(True, '开始启动项目') + return public.returnResult(True, '设置成功') + + def stop_groups(self, group): + config = self.get_group_file() + if group not in config: + return public.returnResult(False, '组名不存在') + config[group]['status'] = 0 + for i in config[group]['project_list']: + self.modify_project_run_state(public.to_dict_obj({'sitename': i, 'project_action': 'stop'})) + public.writeFile(self.group_file, json.dumps(config)) + + def start_groups(self, group): + config = self.get_group_file() + self.stop_groups(group) + if group not in config: + return public.returnResult(False, '组名不存在') + for i in config[group]['project_list']: + self.modify_project_run_state(public.to_dict_obj({'sitename': i, 'project_action': 'start'})) + if config[group]['project_list'].index(i) == len(config[group]['project_list']) - 1: + continue + time.sleep(int(config[group]['interval'])) + flag = 1 + for i in config[group]['project_list']: + if not self.get_project_run_state(public.to_dict_obj({'sitename': i}))['status']: + flag = 0 + break + if flag: + config[group]['status'] = 1 + else: + config[group]['status'] = 0 + public.writeFile(self.group_file, json.dumps(config)) + + # 取代理配置文件 + def get_proxy_file(self, get): + try: + import files + conf = [] if not os.path.exists(self.__proxyfile) else json.loads(public.readFile(self.__proxyfile)) + get.webserver = public.GetWebServer() + sitename = get.sitename + proxyname = get.proxyname + proxyname_md5 = self.__calc_md5(proxyname) + get.path = "%s/panel/vhost/%s/proxy/%s/%s_%s.conf" % (self.setupPath, get.webserver, sitename, proxyname_md5, sitename) + for i in conf: + if proxyname == i["proxyname"] and sitename == i["sitename"] and i["type"] != 1: + return public.returnResult(False, '代理已暂停') + f = files.files() + return public.returnResult(True, data=(f.GetFileBody(get), get.path)) + except: + return public.returnResult(False, traceback.format_exc()) + + # 计算proxyname md5 + def __calc_md5(self, proxyname): + md5 = hashlib.md5() + md5.update(proxyname.encode('utf-8')) + return md5.hexdigest() + + # 保存重定向配置文件 + def save_proxy_file(self, get): + import files + f = files.files() + get.data = get.config + return public.returnResult(True, f.SaveFileBody(get)) + + def set_cron_status_all(self, get): + import crontab + return crontab.crontab().set_cron_status_all(get) + + def get_index_conf(self, get): + if not hasattr(get, 'id'): + return public.returnResult(False, msg='请输入id') + sitename = public.M('sites').where('id=?', (get.id,)).getField('name') + if not sitename: + return public.returnResult(False, msg='项目不存在') + res = DirTool().get_index_conf(sitename) + if str(res) == str: + return public.returnResult(False, msg='获取失败' + res) + else: + return public.returnResult(True, data=res) + + def set_index_conf(self, get): + if not hasattr(get, 'id'): + return public.returnResult(False, msg='请输入id') + if not hasattr(get, 'Index'): + return public.returnResult(False, msg='配置不能为空') + try: + sitename = public.M('sites').where('id=?', (get.id,)).getField('name') + index = get.Index + if type(get.Index) == str: + index = get.Index.split(',') + DirTool().set_index_conf(sitename, file_list=index) + return public.returnResult(True, msg='设置成功') + except: + return public.returnResult(False, msg='设置失败') + + def get_referer_security(self, get): + if not hasattr(get, 'id'): + return public.returnResult(False, msg='请输入id') + sitename = public.M('sites').where('id=?', (get.id,)).getField('name') + if not sitename: + return public.returnResult(False, msg='项目不存在') + get.site_name = sitename + return Referer('').get_referer_security(get) + + def set_referer_security(self, get): + return Referer('').set_referer_security(get) + + def get_system_user_list(self, 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() + with open('/etc/passwd') as fp: + for line in fp.readlines(): + tmp = line.split(':') + user_name = tmp[0] + uid = int(tmp[2]) + if uid == 0: + user_set.add(user_name) + continue + if uid >= 1000: + user_set.add(user_name) + continue + if uid >= 100 and sys_user: + user_set.add(user_name) + continue + if all_user: + user_set.add(user_name) + continue + + return list(user_set) + + +if __name__ == "__main__": + m = main() + m.install_dependence("555.com", "82", "/xiaopacai/laravel11.5.0/", composer_version="2.7.3") diff --git a/mod/project/php/serviceconfMod.py b/mod/project/php/serviceconfMod.py new file mode 100644 index 00000000..88514712 --- /dev/null +++ b/mod/project/php/serviceconfMod.py @@ -0,0 +1,17 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: baozi +# ------------------------------------------------------------------- +# 服务配置模块 +# ------------------------------ + +from mod.base.web_conf import IpRestrict + + +class main(IpRestrict): # 继承并使用同ip黑白名单限制 + def __init__(self): + super().__init__(config_prefix="") diff --git a/mod/project/proxy/__init__.py b/mod/project/proxy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/project/proxy/comMod.py b/mod/project/proxy/comMod.py new file mode 100644 index 00000000..2ad47842 --- /dev/null +++ b/mod/project/proxy/comMod.py @@ -0,0 +1,3884 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +import json +import os +# ------------------------------ +# 反向代理模型 +# ------------------------------ +import sys + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +os.chdir("/www/server/panel") +import public +from public.validate import Param + + +class main(): + + def __init__(self): + self._proxy_path = '/www/server/proxy_project' + self._proxy_config_path = self._proxy_path + '/sites' + self._site_proxy_conf_path = "" + if not os.path.exists(self._proxy_config_path): + public.ExecShell('mkdir -p {}'.format(self._proxy_config_path)) + public.ExecShell('chown -R www:www {}'.format(self._proxy_config_path)) + public.ExecShell('chmod -R 755 {}'.format(self._proxy_config_path)) + + self._init_proxy_conf = { + "site_name": "", + "domain_list": [], + "site_port": [], + "https_port": "443", + "ipv4_port_conf": "listen {listen_port};", + "ipv6_port_conf": "listen [::]:{listen_port};", + "port_conf": "listen {listen_port};{listen_ipv6}", + "ipv4_ssl_port_conf": "{ipv4_port_conf}\n listen {https_port} ssl http2 ;", + "ipv6_ssl_port_conf": "{ipv6_port_conf}\n listen [::]:{https_port} ssl http2 ;", + "ipv4_http3_ssl_port_conf": "{ipv4_port_conf}\n listen {https_port} quic;\n listen {https_port} ssl;", + "ipv6_http3_ssl_port_conf": "{ipv6_port_conf}\n listen [::]:{https_port} quic;\n listen [::]:{https_port} ssl ;", + "site_path": "", + "ssl_info": { + "ssl_status": False, + "ssl_default_conf": "#error_page 404/404.html;", + "ssl_conf": '#error_page 404/404.html;\n ssl_certificate /www/server/panel/vhost/cert/{site_name}/fullchain.pem;\n ssl_certificate_key /www/server/panel/vhost/cert/{site_name}/privkey.pem;\n ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;\n ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;\n ssl_prefer_server_ciphers on;\n ssl_session_cache shared:SSL:10m;\n ssl_session_timeout 10m;\n add_header Strict-Transport-Security "max-age=31536000";\n error_page 497 https://$host$request_uri;', + "force_ssl_conf": '#error_page 404/404.html;{force_conf}\n ssl_certificate /www/server/panel/vhost/cert/{site_name}/fullchain.pem;\n ssl_certificate_key /www/server/panel/vhost/cert/{site_name}/privkey.pem;\n ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;\n ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;\n ssl_prefer_server_ciphers on;\n ssl_session_cache shared:SSL:10m;\n ssl_session_timeout 10m;\n add_header Strict-Transport-Security "max-age=31536000";\n error_page 497 https://$host$request_uri;', + "force_https": False, + "force_conf": " #HTTP_TO_HTTPS_START\n if ($server_port !~ 443){\n rewrite ^(/.*)$ https://$host$1 permanent;\n }\n #HTTP_TO_HTTPS_END", + }, + "err_age_404": "", + "err_age_502": "", + "ip_limit": { + "ip_black": [], + "ip_white": [], + }, + "basic_auth": [], + "proxy_cache": { + "cache_status": False, + "cache_zone": "", + "static_cache": "", + "expires": "1d", + "cache_conf": "", + }, + "gzip": { + "gzip_status": False, + "gzip_min_length": "1k", + "gzip_comp_level": "6", + "gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/json image/jpeg image/gif image/png font/ttf font/otf image/svg+xml application/xml+rss text/x-js", + "gzip_conf": "gzip on;\n gzip_min_length 10k;\n gzip_buffers 4 16k;\n gzip_http_version 1.1;\n gzip_comp_level 2;\n gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;\n gzip_vary on;\n gzip_proxied expired no-cache no-store private auth;\n gzip_disable \"MSIE [1-6]\\.\";", + }, + "subs_filter": False, + "sub_filter": { + "sub_filter_str": [], + }, + "websocket": { + "websocket_status": True, + "websocket_conf": "proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";", + }, + "security": { + "security_status": False, + "static_resource": "jpg|jpeg|gif|png|js|css", + "return_resource": "404", + "http_status": False, + "domains": "", + "security_conf": " #SECURITY-START Anti theft chain configuration" + "\n location ~ .*\\.({static_resource})$" + "\n {{\n expires {expires};" + "\n access_log /dev/null;" + "\n valid_referers {domains};" + "\n if ($invalid_referer){{" + "\n return {return_resource};" + "\n }}" + "\n }}\n #SECURITY-END", + }, + "redirect": { + "redirect_status": False, + "redirect_conf": " #Referencing redirection rules, the redirection proxy configured after annotation will be invalid\n include /www/server/panel/vhost/nginx/redirect/{site_name}/*.conf;", + }, + "proxy_log": { + "log_type": "default", + "server_port": "", + "log_path": "", + "log_conf": "\naccess_log {log_path}/{site_name}.log;\n error_log {log_path}/{site_name}.error.log;", + }, + "default_cache": "proxy_cache_path /www/wwwroot/{site_name}/proxy_cache_dir levels=1:2 keys_zone={cache_name}_cache:20m inactive=1d max_size=5g;", + "default_describe": "# If there is abnormal access to the reverse proxy website and the content has already been configured here, please prioritize checking if the configuration here is correct\n", + "http_block": "", + "server_block": "", + "remark": "", + "proxy_info": [], + } + self._template_conf = r'''{http_block} +server {{ + {port_conf} + server_name {domains}; + index index.php index.html index.htm default.php default.htm default.html; + root {site_path}; + + #CERT-APPLY-CHECK--START + # Configuration related to file verification for SSL certificate application - Do not delete + include /www/server/panel/vhost/nginx/well-known/{site_name}.conf; + #CERT-APPLY-CHECK--END + + #SSL-START {ssl_start_msg} + {ssl_info} + #SSL-END + #REDIRECT START + {redirect_conf} + #REDIRECT END + + #ERROR-PAGE-START {err_page_msg} + {err_age_404} + {err_age_502} + #ERROR-PAGE-END + + #PHP-INFO-START PHP reference configuration, can be annotated or modified + {security_conf} + include enable-php-00.conf; + #PHP-INFO-END + + #IP-RESTRICT-START Restrict access to IP configuration, IP blacklist and whitelist + {ip_limit_conf} + #IP-RESTRICT-END + + #BASICAUTH START + {auth_conf} + #BASICAUTH END + + #SUB_FILTER START + {sub_filter} + #SUB_FILTER END + + #GZIP START + {gzip_conf} + #GZIP END + + #GLOBAL-CACHE START + {proxy_cache} + #GLOBAL-CACHE END + + #WEBSOCKET-SUPPORT START + {websocket_support} + #WEBSOCKET-SUPPORT END + + #PROXY-CONF-START + {proxy_conf} + #PROXY-CONF-END + + #SERVER-BLOCK START + {server_block} + #SERVER-BLOCK END + + #Prohibited access to files or directories + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + {{ + return 404; + }} + + #One click application for SSL certificate verification directory related settings + location /.well-known{{ + allow all; + root /www/wwwroot/{site_name}; + }} + + #Prohibit placing sensitive files in the certificate verification directory + if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {{ + return 403; + }} + + #LOG START + {server_log} + {monitor_conf} + #LOG END +}}''' + self._template_proxy_conf = '''location ^~ {proxy_path} {{ + {ip_limit} + {basic_auth} + proxy_pass {proxy_pass}; + proxy_set_header Host {proxy_host}; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-Port $remote_port; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + {timeout_conf} + {websocket_support} + {custom_conf} + {proxy_cache} + {gzip} + {sub_filter} + {server_log} + }}''' + + def structure_proxy_conf(self, get): + ''' + @name + @author wzz <2024/4/19 下午4:29> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.proxy_conf = self._template_proxy_conf.format( + ip_limit="", + gzip="", + proxy_cache="", + sub_filter="", + server_log="", + basic_auth="", + proxy_pass=get.proxy_pass, + proxy_host=get.proxy_host, + proxy_path=get.proxy_path, + custom_conf="", + timeout_conf=get.proxy_timeout, + websocket_support=self._init_proxy_conf["websocket"]["websocket_conf"], + ) + + get.proxy_info = { + "proxy_type": get.proxy_type, + "proxy_path": get.proxy_path, + "proxy_pass": get.proxy_pass, + "proxy_host": get.proxy_host, + "ip_limit": { + "ip_black": [], + "ip_white": [], + }, + "basic_auth": {}, + "proxy_cache": { + "cache_status": False, + "cache_zone": get.site_name.replace(".", "_") + "_cache", + "static_cache": "", + "expires": "1d", + "cache_conf": "", + }, + "gzip": { + "gzip_status": False, + "gzip_min_length": "1k", + "gzip_comp_level": "6", + "gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/json image/jpeg image/gif image/png font/ttf font/otf image/svg+xml application/xml+rss text/x-js", + "gzip_conf": "gzip on;\n gzip_min_length 10k;\n gzip_buffers 4 16k;\n gzip_http_version 1.1;\n gzip_comp_level 2;\n gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;\n gzip_vary on;\n gzip_proxied expired no-cache no-store private auth;\n gzip_disable \"MSIE [1-6]\\.\";", + }, + "sub_filter": { + "sub_filter_str": [], + }, + "websocket": { + "websocket_status": True, + "websocket_conf": "proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";", + }, + "proxy_log": { + "log_type": "off", + "log_conf": get.server_log, + }, + "timeout": { + "proxy_connect_timeout": "60", + "proxy_send_timeout": "600", + "proxy_read_timeout": "600", + "timeout_conf": "proxy_connect_timeout 60s;\n proxy_send_timeout 600s;\n proxy_read_timeout 600s;", + }, + "custom_conf": "", + "proxy_conf": get.proxy_conf, + "remark": "", + "template_proxy_conf": self._template_proxy_conf, + } + + # 2024/4/18 上午10:53 构造反向代理的配置文件 + def structure_nginx(self, get): + ''' + @name 构造反向代理的配置文件 + @author wzz <2024/4/18 上午10:54> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.err_age_404 = get.get("err_age_404", "#error_page 404 /404.html;") + get.err_age_502 = get.get("err_age_502", "#error_page 502 /502.html;") + get.proxy_info = get.get("proxy_info", "") + + get.server_log = get.get( + "server_log", + self._init_proxy_conf["proxy_log"]["log_conf"].format( + log_path=public.get_logs_path(), + site_name=get.site_name + ) + ) + get.remark = get.get("remark", "") + get.server_block = get.get("server_block", "") + get.websocket_status = get.get("websocket_status", True) + get.proxy_timeout = "proxy_connect_timeout 60s;\n proxy_send_timeout 600s;\n proxy_read_timeout 600s;" + self.structure_proxy_conf(get) + is_subs = public.ExecShell("nginx -V 2>&1|grep 'ngx_http_substitutions_filter' -o")[0] + + self._init_proxy_conf["subs_filter"] = True if is_subs != "" else False + self._init_proxy_conf["site_name"] = get.site_name + self._init_proxy_conf["domain_list"] = get.domain_list + self._init_proxy_conf["site_port"] = get.port_list + self._init_proxy_conf["site_path"] = get.site_path + self._init_proxy_conf["err_age_404"] = get.err_age_404 + self._init_proxy_conf["err_age_502"] = get.err_age_502 + self._init_proxy_conf["proxy_log"]["log_conf"] = get.server_log + self._init_proxy_conf["remark"] = get.remark + self._init_proxy_conf["http_block"] = "" + self._init_proxy_conf["proxy_info"].append(get.proxy_info) + self._init_proxy_conf["proxy_cache"]["cache_zone"] = get.site_name.replace(".", "_") + "_cache" + + # 2024/4/18 上午10:35 写入Nginx配置文件 + def write_nginx_conf(self, get): + ''' + @name 写入Nginx配置文件 + @author wzz <2024/4/18 上午10:36> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 这里可能会报错 + self.structure_nginx(get) + listen_port = " ".join(get.port_list) if len(get.port_list) > 1 else get.site_port + # listen_ipv6 = "\n listen [::]:{};".format( + # " ".join(get.port_list) if len(get.port_list) > 1 else get.site_port) + # port_conf = self._init_proxy_conf["port_conf"].format(listen_port=listen_port, listen_ipv6=listen_ipv6) + + if type(listen_port) == list: + ipv4_port_conf = "" + ipv6_port_conf = "" + for p in listen_port: + ipv4_port_conf += self._init_proxy_conf["ipv4_port_conf"].format(listen_port=p) + "\n " + ipv6_port_conf += self._init_proxy_conf["ipv6_port_conf"].format(listen_port=p) + "\n " + else: + ipv4_port_conf = self._init_proxy_conf["ipv4_port_conf"].format(listen_port=listen_port) + ipv6_port_conf = self._init_proxy_conf["ipv6_port_conf"].format(listen_port=listen_port) + port_conf = ipv4_port_conf + "\n" + ipv6_port_conf + + # 2024/6/4 下午4:20 兼容新版监控报表的配置 + monitor_conf = "" + if os.path.exists("/www/server/panel/plugin/monitor/monitor_main.py"): + monitor_conf = '''#Monitor-Config-Start monitor log sending configuration + access_log syslog:server=unix:/tmp/bt-monitor.sock,nohostname,tag={pid}__access monitor; + error_log syslog:server=unix:/tmp/bt-monitor.sock,nohostname,tag={pid}__error; + #Monitor-Config-End'''.format(pid=get.pid) + + conf = self._template_conf.format( + http_block=get.http_block, + server_block="", + port_conf=port_conf, + ssl_start_msg=public.getMsg('NGINX_CONF_MSG1'), + err_page_msg=public.getMsg('NGINX_CONF_MSG2'), + php_info_start=public.getMsg('NGINX_CONF_MSG3'), + rewrite_start_msg=public.getMsg('NGINX_CONF_MSG4'), + log_path=public.get_logs_path(), + domains=' '.join(get.domain_list) if len(get.domain_list) > 1 else get.site_name, + site_name=get.site_name, + ssl_info="#error_page 404/404.html;", + err_age_404=get.err_age_404, + err_age_502=get.err_age_502, + ip_limit_conf="", + auth_conf="", + sub_filter="", + gzip_conf="", + redirect_conf="", + security_conf="", + proxy_conf=get.proxy_conf, + server_log=get.server_log, + site_path=get.site_path, + proxy_cache="", + websocket_support=self._init_proxy_conf["websocket"]["websocket_conf"], + monitor_conf=monitor_conf, + ) + + # 写配置文件 + well_known_path = "{}/vhost/nginx/well-known".format(public.get_panel_path()) + if not os.path.exists(well_known_path): + os.makedirs(well_known_path, 0o600) + public.writeFile("{}/{}.conf".format(well_known_path, get.site_name), "") + + get.filename = public.get_setup_path() + '/panel/vhost/nginx/' + get.site_name + '.conf' + + return public.writeFile(get.filename, conf) + + # 2024/4/25 上午11:11 检查nginx是否支持http3 + def check_http3_support(self): + ''' + @name 检查nginx是否支持http3 + @author wzz <2024/4/25 上午11:13> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + return public.ExecShell("nginx -V 2>&1| grep 'http_v3_module' -o")[0] + + # 2024/4/18 上午9:26 创建反向代理 + def create(self, get): + ''' + @name 创建反向代理 + @author wzz <2024/4/18 上午9:27> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('remark').String(), + Param('proxy_pass').String(), + Param('domains').String(), + Param('proxy_host').String(), + Param('proxy_type').Bool(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + # 2024/4/18 上午9:41 前置处理开始 + from mod.base.web_conf import util + webserver = util.webserver() + if webserver != "nginx" or webserver is None: + return public.return_message(0,0,public.returnResult(status=False, msg="Only Nginx is supported, please install Nginx before use!")) + from panelSite import panelSite + site_obj = panelSite() + site_obj.check_default() + + wc_err = public.checkWebConfig() + if not wc_err: + return public.returnResult( + status=False, + msg='ERROR: Detected an error in the configuration file, please troubleshoot before proceeding

                                    ' + + wc_err.replace("\n", '
                                    ') + '
                                    ' + ) + + # 2024/4/18 上午9:45 参数处理 + get.domains = get.get("domains", "") + get.proxy_path = get.get("proxy_path", "/") + get.proxy_pass = get.get("proxy_pass", "") + get.proxy_host = get.get("proxy_host", "$http_host") + get.remark = get.get("remark", "") + get.proxy_type = get.get("proxy_type", "http") + + # 2024/4/18 上午9:45 参数校验 + if not get.domains: + return public.return_message(-1, 0,"The domain name cannot be empty, please enter at least one domain name!") + if not get.proxy_pass: + return public.return_message(-1, 0,"The proxy target cannot be empty!") + if get.remark != "": + get.remark = public.xssencode2(get.remark) + if get.proxy_type == "unix": + if not get.proxy_pass.startswith("/"): + return public.return_message(-1, 0,"The Unix file path must start with/, such as/tmp/flash.app.sock!") + if not get.proxy_pass.endswith(".sock"): + return public.return_message(-1, 0,"Unix files must end with. lock, such as/tmp/flash.app. lock!") + if not os.path.exists(get.proxy_pass): + return public.return_message(-1, 0,"The proxy target does not exist!") + get.proxy_pass = "http://unix:{}".format(get.proxy_pass) + elif get.proxy_type == "http": + if not get.proxy_pass.startswith("http://") and not get.proxy_pass.startswith("https://"): + return public.return_message(-1, 0,"The proxy target must start with http://or https://!") + + # 2024/4/18 上午9:45 创建反向代理 + get.domain_list = get.domains.split("\n") + get.site_name = util.to_puny_code(get.domain_list[0].strip().split(":")[0]).strip().lower() + get.site_path = "/www/wwwroot/" + get.site_name + get.site_port = get.domain_list[0].strip().split(":")[1] if ":" in get.domain_list[0] else "80" + get.port_list = [get.site_port] + if not public.checkPort(get.site_port): + return public.return_message(-1, 0,'Port [{}] is illegal!'.format(get.site_port)) + + if len(get.domain_list) > 1: + for domain in get.domain_list[1:]: + if not ":" in domain.strip(): + continue + + d_port = domain.strip().split(":")[1] + if not public.checkPort(d_port): + return public.return_message(-1, 0,'Port [{}] is illegal!'.format(d_port)) + + if not d_port in get.port_list: + get.port_list.append(d_port) + + # 2024/4/18 上午10:06 检查域名是否存在 + main_domain = get.site_name + opid = public.M('domain').where("name=? and port=?", (main_domain, int(get.site_port))).getField('pid') + if opid: + if public.M('sites').where('id=?', (opid,)).count(): + return public.return_message(-1, 0,'The website [{}] already exists, please do not add it again!'.format(main_domain)) + public.M('domain').where('pid=?', (opid,)).delete() + + if public.M('binding').where('domain=?', (main_domain,)).count(): + return public.return_message(-1, 0,'The website [{}] already exists, please do not add it again!'.format(main_domain)) + + # 2024/4/18 上午10:06 检查网站是否存在 + sql = public.M('sites') + if sql.where("name=?", (get.site_name,)).count(): + if public.is_ipv4(get.site_name): + get.site_name = get.site_name + "_" + str(get.site_port) + else: + return public.return_message(-1, 0,'The website [{}] already exists, please do not add it again!'.format(main_domain)) + + # 2024/4/18 上午10:21 添加端口到系统防火墙 + from firewallModel.comModel import main as comModel + firewall_com = comModel() + get.port = get.site_port + firewall_com.set_port_rule(get) + + # 2024/4/18 上午9:41 前置处理结束 + + # 2024/4/18 上午10:46 写入网站配置文件 + get.http_block = "proxy_cache_path /www/wwwroot/{site_name}/proxy_cache_dir levels=1:2 keys_zone={cache_name}_cache:20m inactive=1d max_size=5g;".format( + site_name=get.site_name, + cache_name=get.site_name.replace(".", "_") + ) + self._site_path = self._proxy_config_path + '/' + get.site_name + if not os.path.exists(self._site_path): + public.ExecShell('mkdir -p {}'.format(self._site_path)) + public.ExecShell('chown -R www:www {}'.format(self._site_path)) + public.ExecShell('chmod -R 755 {}'.format(self._site_path)) + + if not os.path.exists(get.site_path): + public.ExecShell('mkdir -p {}'.format(get.site_path)) + public.ExecShell('chown -R www:www {}'.format(get.site_path)) + public.ExecShell('chmod -R 755 {}'.format(get.site_path)) + + if not os.path.exists(get.site_path + "/proxy_cache_dir"): + public.ExecShell('mkdir -p {}'.format(get.site_path + "/proxy_cache_dir")) + public.ExecShell('chown -R www:www {}'.format(get.site_path + "/proxy_cache_dir")) + public.ExecShell('chmod -R 755 {}'.format(get.site_path + "/proxy_cache_dir")) + + self._site_proxy_conf_path = '{}/{}.json'.format(self._site_path, get.site_name) + + # 2024/4/18 上午10:22 写入数据库 + pdata = { + 'name': get.site_name, + 'path': "/www/wwwroot/" + get.site_name, + 'ps': get.remark, + 'status': 1, + 'type_id': 0, + 'project_type': 'proxy', + 'project_config': json.dumps(self._init_proxy_conf), + 'addtime': public.getDate() + } + + get.pid = public.M('sites').insert(pdata) + public.M('domain').add('pid,name,port,addtime', (get.pid, main_domain, get.site_port, public.getDate())) + for domain in get.domain_list: + get.domain = domain + get.webname = get.site_name + get.id = str(get.pid) + from panelSite import panelSite + panelSite().AddDomain(get) + + # 2024/6/4 下午4:30 写nginx配置文件 + self.write_nginx_conf(get) + public.writeFile(self._site_proxy_conf_path, json.dumps(self._init_proxy_conf)) + wc_err = public.checkWebConfig() + if not wc_err: + public.ExecShell("rm -f {}".format(self._site_proxy_conf_path)) + public.ExecShell("rm -rf {}".format(get.filename)) + public.M('sites').where('id=?', (get.pid,)).delete() + public.M('domain').where('pid=?', (get.pid,)).delete() + return public.return_message(-1, 0,'ERROR: Detected an error in the configuration file, please troubleshoot before proceeding

                                    ' + + wc_err.replace("\n", '
                                    ') + '
                                    ' + ) + if type(wc_err) != bool and "test failed" in wc_err: + public.ExecShell("rm -f {}".format(self._site_proxy_conf_path)) + public.ExecShell("rm -rf {}".format(get.filename)) + public.M('sites').where('id=?', (get.pid,)).delete() + public.M('domain').where('pid=?', (get.pid,)).delete() + return public.return_message(-1, 0,'ERROR: Detected an error in the configuration file, please troubleshoot before proceeding

                                    ' + + wc_err.replace("\n", '
                                    ') + '
                                    ' + ) + + public.WriteLog('TYPE_SITE', 'SITE_ADD_SUCCESS', (get.site_name,)) + public.set_module_logs('site_proxy', 'create', 1) + public.serviceReload() + return public.return_message(0, 0,"Reverse proxy project added successfully!") + + def read_json_conf(self, get): + ''' + @name + @author wzz <2024/4/18 下午9:53> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + try: + proxy_json_conf = json.loads(public.readFile(conf_path)) + conf_file = public.get_setup_path() + '/panel/vhost/nginx/' + get.site_name + '.conf' + conf_string =public.readFile(conf_file) + if 'ssl_certificate_key' in conf_string: + proxy_json_conf['ssl_info']['ssl_status']=True + else: + proxy_json_conf['ssl_info']['ssl_status']=False + except Exception as e: + proxy_json_conf = {} + + return public.return_message(0,0,proxy_json_conf) + + # 2024/4/18 下午9:58 设置全局日志 + def set_global_log(self, get): + ''' + @name + @author wzz <2024/4/18 下午9:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('log_type').String(), + Param('log_path').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.log_type = get.get("log_type", "default") + if not get.log_type in ["default", "file", "rsyslog", "off"]: + return public.return_message(-1, 0,"The log type is incorrect. Please pass in default/file/rsyslog/off!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.proxy_json_conf["proxy_log"]["log_type"] = get.log_type + + if get.log_type == "file": + get.log_path = get.get("log_path", "") + if get.log_path == "": + return public.return_message(-1, 0,"The log path cannot be empty!") + if not get.log_path.startswith("/"): + return public.return_message(-1, 0,"The log path must start with/") + + get.proxy_json_conf["proxy_log"]["log_path"] = get.log_path + get.proxy_json_conf["proxy_log"]["log_conf"] = self._init_proxy_conf["proxy_log"]["log_conf"].format( + log_path=get.log_path, + site_name=get.site_name + ) + elif get.log_type == "rsyslog": + get.log_path = get.get("log_path", "") + if get.log_path == "": + return public.return_message(-1, 0,"The log path cannot be empty!") + site_name = get.site_name.replace(".", "_") + get.proxy_json_conf["proxy_log"]["log_conf"] = ( + "\n access_log syslog:server={server_host},nohostname,tag=nginx_{site_name}_access;" + "\n error_log syslog:server={server_host},nohostname,tag=nginx_{site_name}_error;" + .format( + server_host=get.log_path, + site_name=site_name + )) + get.proxy_json_conf["proxy_log"]["rsyslog_host"] = get.log_path + elif get.log_type == "off": + get.proxy_json_conf["proxy_log"]["log_conf"] = "\n access_log off;\n error_log off;" + else: + get.proxy_json_conf["proxy_log"]["log_conf"] = " " + self._init_proxy_conf["proxy_log"][ + "log_conf"].format( + log_path=public.get_logs_path(), + site_name=get.site_name + ) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + public.serviceReload() + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/18 下午10:21 设置basic_auth + def set_dir_auth(self, get): + ''' + @name 设置basic_auth + @param auth_type: add/edit + auth_path: /api + username: admin + password: admin + @return: + '''# 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('auth_path').String(), + Param('username').String(), + Param('password').String(), + Param('name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.name = get.get("name", "") + if get.name == "": + return public.return_message(-1, 0,"Name cannot be empty!") + + get.auth_path = get.get("auth_path", "") + if get.auth_path == "": + return public.return_message(-1, 0,"Auth_path cannot be empty!") + if not get.auth_path.startswith("/"): + return public.return_message(-1, 0,"Auth_path must start with/!") + + get.username = get.get("username", "") + get.password = get.get("password", "") + if get.username == "" or get.password == "": + return public.return_message(-1, 0,"The username and password cannot be empty!") + + get.password = public.hasPwd(get.password) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + if len(get.proxy_json_conf["basic_auth"]) == 0: + return public.return_message(-1, 0,"[{}] does not exist in HTTP authentication, please add it first!".format(get.auth_path)) + + for i in range(len(get.proxy_json_conf["basic_auth"])): + if get.proxy_json_conf["basic_auth"][i]["auth_path"] == get.auth_path: + if get.proxy_json_conf["basic_auth"][i]["auth_name"] == get.name: + get.proxy_json_conf["basic_auth"][i]["username"] = get.username + get.proxy_json_conf["basic_auth"][i]["password"] = get.password + break + + auth_file = "/www/server/pass/{site_name}/{name}.htpasswd".format(site_name=get.site_name, name=get.name) + public.writeFile(auth_file, "{}:{}".format(get.username, get.password)) + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + public.serviceReload() + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/22 下午4:17 添加指定网站的basic_auth + def add_dir_auth(self, get): + ''' + @name 添加指定网站的basic_auth + @author wzz <2024/4/22 下午4:17> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('auth_path').String(), + Param('username').String(), + Param('password').String(), + Param('name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.auth_path = get.get("auth_path", "") + if get.auth_path == "": + return public.return_message(-1, 0,"Auth_path cannot be empty!") + if not get.auth_path.startswith("/"): + return public.return_message(-1, 0,"Auth_path must start with/!") + + get.name = get.get("name", "") + if get.name == "": + return public.return_message(-1, 0,"Name cannot be empty!") + + get.username = get.get("username", "") + if get.username == "": + return public.return_message(-1, 0,"Username cannot be empty!") + + get.password = get.get("password", "") + if get.password == "": + return public.return_message(-1, 0,"Password cannot be empty!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + auth_file = "/www/server/pass/{site_name}/{name}.htpasswd".format(site_name=get.site_name, name=get.name) + + auth_conf = { + "auth_status": True, + "auth_path": get.auth_path, + "auth_name": get.name, + "username": get.username, + "password": public.hasPwd(get.password), + "auth_file": auth_file, + } + + if len(get.proxy_json_conf["basic_auth"]) != 0: + for i in range(len(get.proxy_json_conf["basic_auth"])): + if get.proxy_json_conf["basic_auth"][i]["auth_path"] == get.auth_path: + return public.return_message(-1, 0,"[{}] already exists in HTTP authentication and cannot be added again!".format(get.auth_path)) + + if not os.path.exists("/www/server/pass"): + public.ExecShell("mkdir -p /www/server/pass") + if not os.path.exists("/www/server/pass/{}".format(get.site_name)): + public.ExecShell("mkdir -p /www/server/pass/{}".format(get.site_name)) + public.writeFile(auth_file, "{}:{}".format(get.username, public.hasPwd(get.password))) + + get.proxy_json_conf["basic_auth"].append(auth_conf) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Added successfully!") + + # 2024/4/23 上午9:34 删除指定网站的basic_auth + def del_dir_auth(self, get): + ''' + @name 删除指定网站的basic_auth + @author wzz <2024/4/23 上午9:35> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('auth_path').String(), + Param('name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.auth_path = get.get("auth_path", "") + if get.auth_path == "": + return public.return_message(-1, 0,"Auth_path cannot be empty!") + + get.name = get.get("name", "") + if get.name == "": + return public.return_message(-1, 0,"Name cannot be empty!") + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + auth_file = "/www/server/pass/{site_name}/{name}.htpasswd".format(site_name=get.site_name, name=get.name) + + panel_port = public.readFile('/www/server/panel/data/port.pl') + proxy_result = self.get_proxy_list(get) + if proxy_result['status']==-1:return proxy_result + proxy_list=proxy_result['message'] + if proxy_list[0]["proxy_pass"] == "https://127.0.0.1:{}".format(panel_port.strip()) and get.auth_path.strip() == "/": + return public.return_message(-1, 0,"[{}] is a reverse representation of the panel, and the HTTP authentication of [/] cannot be deleted!".format(get.site_name)) + if len(get.proxy_json_conf["basic_auth"]) != 0: + for i in range(len(get.proxy_json_conf["basic_auth"])): + if get.proxy_json_conf["basic_auth"][i]["auth_path"] == get.auth_path: + if get.proxy_json_conf["basic_auth"][i]["auth_name"] == get.name: + get.proxy_json_conf["basic_auth"].pop(i) + break + + public.ExecShell("rm -f {}".format(auth_file)) + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + return public.return_message(0, 0,"Delete successful!") + + # 2024/4/18 下午10:26 设置全局gzip + def set_global_gzip(self, get): + ''' + @name 设置全局gzip + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('gzip_min_length').String(), + Param('gzip_types').String(), + Param('gzip_status').Integer(), + Param('gzip_comp_level').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.gzip_status = get.get("gzip_status/d", 999) + if get.gzip_status == 999: + return public.return_message(-1, 0,"Gzip_status cannot be empty, please pass number 1 or 0!") + get.gzip_min_length = get.get("gzip_min_length", "10k") + get.gzip_comp_level = get.get("gzip_comp_level", "6") + if get.gzip_min_length[0] == "0" or get.gzip_min_length.startswith("-"): + return public.return_message(-1, 0,"The gzip_min_length parameter is invalid. Please enter a number greater than 0!") + if get.gzip_comp_level == "0" or get.gzip_comp_level.startswith("-"): + return public.return_message(-1, 0,"The gzip_comp_level parameter is invalid. Please enter a number greater than 0!") + get.gzip_types = get.get( + "gzip_types", + "text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/json image/jpeg image/gif image/png font/ttf font/otf image/svg+xml application/xml+rss text/x-js" + ) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.proxy_json_conf["gzip"]["gzip_status"] = True if get.gzip_status == 1 else False + if get.proxy_json_conf["gzip"]["gzip_status"]: + get.proxy_json_conf["gzip"]["gzip_status"] = True + get.proxy_json_conf["gzip"]["gzip_min_length"] = get.gzip_min_length + get.proxy_json_conf["gzip"]["gzip_comp_level"] = get.gzip_comp_level + get.proxy_json_conf["gzip"]["gzip_types"] = get.gzip_types + get.gzip_conf = ("gzip on;" + "\n gzip_min_length {gzip_min_length};" + "\n gzip_buffers 4 16k;" + "\n gzip_http_version 1.1;" + "\n gzip_comp_level {gzip_comp_level};" + "\n gzip_types {gzip_types};" + "\n gzip_vary on;" + "\n gzip_proxied expired no-cache no-store private auth;" + "\n gzip_disable \"MSIE [1-6]\\.\";").format( + gzip_min_length=get.gzip_min_length, + gzip_comp_level=get.gzip_comp_level, + gzip_types=get.gzip_types + ) + get.proxy_json_conf["gzip"]["gzip_conf"] = get.gzip_conf + else: + get.proxy_json_conf["gzip"]["gzip_conf"] = "" + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/18 下午10:27 设置全局缓存 + def set_global_cache(self, get): + ''' + @name 设置全局缓存 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('expires').String(), + Param('cache_status').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.cache_status = get.get("cache_status/d", 999) + if get.cache_status == 999: + return public.return_message(-1, 0,"Cache_status cannot be empty, please pass number 1 or 0!") + + get.expires = get.get("expires", "1d") + if get.expires[0] == "0" or get.expires.startswith("-"): + return public.return_message(-1, 0,"The expires parameter is illegal. Please enter a number greater than 0!") + expires = "expires {}".format(get.expires) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + static_cache = ("\n location ~ .*\\.(css|js|jpe?g|gif|png|webp|woff|eot|ttf|svg|ico|css\\.map|js\\.map)$" + "\n {{" + "\n {expires};" + "\n error_log /dev/null;" + "\n access_log /dev/null;" + "\n }}").format( + expires=expires, + ) + + cache_conf = ("\n proxy_cache {cache_zone};" + "\n proxy_cache_key $host$uri$is_args$args;" + "\n proxy_ignore_headers Set-Cookie Cache-Control expires X-Accel-Expires;" + "\n proxy_cache_valid 200 304 301 302 {expires};" + "\n proxy_cache_valid 404 1m;" + "{static_cache}").format( + cache_zone=get.proxy_json_conf["proxy_cache"]["cache_zone"], + expires=get.expires, + static_cache=get.proxy_json_conf["proxy_cache"]["static_cache"] if get.proxy_json_conf["proxy_cache"][ + "static_cache"] != "" else static_cache + ) + + get.proxy_json_conf["proxy_cache"]["cache_status"] = True if get.cache_status == 1 else False + if get.proxy_json_conf["proxy_cache"]["cache_status"]: + get.proxy_json_conf["proxy_cache"]["cache_status"] = True + get.proxy_json_conf["proxy_cache"]["expires"] = get.expires + get.proxy_json_conf["proxy_cache"]["cache_conf"] = cache_conf + else: + get.proxy_json_conf["proxy_cache"]["cache_status"] = False + get.proxy_json_conf["proxy_cache"]["cache_conf"] = static_cache + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/18 下午10:43 设置全局websocket支持 + def set_global_websocket(self, get): + ''' + @name + @author wzz <2024/4/19 下午2:37> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('websocket_status').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.websocket_status = get.get("websocket_status/d", 999) + if get.websocket_status == 999: + return public.return_message(-1, 0,"Websocket_status cannot be empty, please pass number 1 or 0!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + if get.websocket_status == 1: + get.proxy_json_conf["websocket"]["websocket_status"] = True + else: + get.proxy_json_conf["websocket"]["websocket_status"] = False + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/19 下午2:54 设置备注 + def set_remak(self, get): + ''' + @name 设置备注 + @param get: + @return: + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.id = get.get("id", "") + if get.id == "": + return public.returnResult(status=False, msg="id不能为空!") + + get.remark = get.get("remark", "") + get.table = "sites" + get.ps = get.remark + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from data import data + data_obj = data() + result = data_obj.setPs(get) + if not result["status"]: + public.returnResult(status=False, msg=result["msg"]) + + get.proxy_json_conf["remark"] = get.remark + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return public.returnResult(msg=result["msg"]) + + # 2024/4/19 下午2:59 添加反向代理 + def add_proxy(self, get): + ''' + @name + @author wzz <2024/4/19 下午3:00> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + Param('proxy_pass').String(), + Param('proxy_host').String(), + Param('proxy_type').String(), + Param('remark').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.proxy_pass = get.get("proxy_pass", "") + if get.proxy_pass == "": + return public.return_message(-1, 0,"Proxy_pass cannot be empty!") + + get.proxy_host = get.get("proxy_host", "$http_host") + get.proxy_type = get.get("proxy_type", "http") + get.remark = get.get("remark", "") + get.proxy_timeout = "proxy_connect_timeout 60s;\n proxy_send_timeout 600s;\n proxy_read_timeout 600s;" + + if get.remark != "": + get.remark = public.xssencode2(get.remark) + if get.proxy_type == "unix": + if not get.proxy_pass.startswith("/"): + return public.return_message(-1, 0,"The Unix file path must start with/, such as/tmp/flash.app.sock!") + if not get.proxy_pass.endswith(".sock"): + return public.return_message(-1, 0,"Unix files must end with. lock, such as/tmp/flash.app. lock!") + if not os.path.exists(get.proxy_pass): + return public.return_message(-1, 0,"The proxy target does not exist!") + + get.proxy_pass = "http://unix:{}".format(get.proxy_pass) + elif get.proxy_type == "http": + if not get.proxy_pass.startswith("http://") and not get.proxy_pass.startswith("https://"): + return public.return_message(-1, 0,"The proxy target must start with http://or https://") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + # 2024/4/19 下午3:45 检测是否已经存在proxy_path,有的话就返回错误 + for proxy_info in get.proxy_json_conf["proxy_info"]: + if proxy_info["proxy_path"] == get.proxy_path: + return public.return_message(-1, 0,"【 {} 】 already exists in the reverse proxy and cannot be added again!".format(get.proxy_path)) + + if len(get.proxy_json_conf["basic_auth"]) != 0: + for i in range(len(get.proxy_json_conf["basic_auth"])): + if get.proxy_json_conf["basic_auth"][i]["auth_path"] == get.proxy_path: + return public.return_message(-1, 0,"[{}] already exists in Basicauth, please delete it before adding a reverse proxy!".format( + get.proxy_path)) + + get.proxy_conf = self._template_proxy_conf.format( + ip_limit="", + gzip="", + proxy_cache="", + sub_filter="", + server_log="", + basic_auth="", + proxy_pass=get.proxy_pass, + proxy_host=get.proxy_host, + proxy_path=get.proxy_path, + custom_conf="", + timeout_conf=get.proxy_timeout, + websocket_support=get.proxy_json_conf["websocket"]["websocket_conf"], + ) + + get.proxy_json_conf["proxy_info"].append({ + "proxy_type": get.proxy_type, + "proxy_path": get.proxy_path, + "proxy_pass": get.proxy_pass, + "proxy_host": get.proxy_host, + "ip_limit": { + "ip_black": [], + "ip_white": [], + }, + "basic_auth": {}, + "proxy_cache": { + "cache_status": False, + "cache_zone": "", + "static_cache": "", + "expires": "1d", + "cache_conf": "", + }, + "gzip": { + "gzip_status": False, + "gzip_min_length": "10k", + "gzip_comp_level": "6", + "gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/json image/jpeg image/gif image/png font/ttf font/otf image/svg+xml application/xml+rss text/x-js", + "gzip_conf": "gzip on;\n gzip_min_length 10k;\n gzip_buffers 4 16k;\n gzip_http_version 1.1;\n gzip_comp_level 2;\n gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;\n gzip_vary on;\n gzip_proxied expired no-cache no-store private auth;\n gzip_disable \"MSIE [1-6]\\.\";", + }, + "sub_filter": { + "sub_filter_str": [], + }, + "websocket": { + "websocket_status": True, + "websocket_conf": "proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";", + }, + "proxy_log": { + "log_type": "off", + "log_conf": "", + }, + "timeout": { + "proxy_connect_timeout": "60", + "proxy_send_timeout": "600", + "proxy_read_timeout": "600", + "timeout_conf": "proxy_connect_timeout 60s;\n proxy_send_timeout 600s;\n proxy_read_timeout 600s;", + }, + "custom_conf": "", + "proxy_conf": get.proxy_conf, + "remark": get.remark, + "template_proxy_conf": self._template_proxy_conf, + }) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + public.set_module_logs('site_proxy', 'add_proxy', 1) + return public.return_message(0, 0,"Added successfully!") + + # 2024/4/19 下午9:45 删除指定站点 + def delete(self, get): + ''' + @name + @author wzz <2024/4/19 下午9:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('remove_path').Integer(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + get.id = get.get("id", "") + get.remove_path = get.get("remove_path/d", 0) + if get.id == "": + return public.return_message(-1, 0,"ID cannot be empty!") + + get.reload = get.get("reload/d", 1) + + if public.M('sites').where('id=?', (get.id,)).count() < 1: + return public.return_message(-1, 0,'The specified site does not exist!') + + site_file = public.get_setup_path() + '/panel/vhost/nginx/' + get.site_name + '.conf' + if os.path.exists(site_file): + public.ExecShell('rm -f {}'.format(site_file)) + redirect_dir = public.get_setup_path() + '/panel/vhost/nginx/redirect/' + get.site_name + if os.path.exists(redirect_dir): + public.ExecShell('rm -rf {}'.format(redirect_dir)) + + logs_file = public.get_logs_path() + '/{}*'.format(get.site_name) + public.ExecShell('rm -f {}'.format(logs_file)) + + self._site_proxy_conf_path = "{path}/{site_name}".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.ExecShell('rm -f {}'.format(self._site_proxy_conf_path)) + + if get.remove_path == 1: + public.ExecShell('rm -rf /www/wwwroot/{}'.format(get.site_name)) + + if get.reload == 1: + public.serviceReload() + + # 从数据库删除 + public.M('sites').where("id=?", (get.id,)).delete() + public.M('domain').where("pid=?", (get.id,)).delete() + public.WriteLog('TYPE_SITE', "SITE_DEL_SUCCESS", (get.site_name,)) + + return public.return_message(0, 0,"Reverse proxy project deleted successfully!") + + # 2024/5/28 上午9:50 批量删除站点 + def batch_delete(self, get): + ''' + @name 批量删除站点 + @author wzz <2024/5/28 上午9:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_list = get.get("site_list", []) + get.remove_path = get.get("remove_path/d", 0) + get.reload = get.get("reload/d", 0) + + try: + site_list = json.loads(get.site_list) + except: + return public.returnResult(False, "请传入需要删除的网站列表!") + + acc_list = [] + for site in site_list: + args = public.dict_obj() + args.site_name = site["site_name"] + args.remove_path = get.remove_path + args.reload = get.reload + args.id = site["id"] + de_result = self.delete(args) + if not de_result["status"]: + acc_list.append({"site_name": site["site_name"], "status": False}) + continue + + acc_list.append({"site_name": site["site_name"], "status": True}) + + public.serviceReload() + + return public.returnResult(True, msg="批量删除站点成功!", data=acc_list) + + # 2024/4/26 下午4:57 获取证书的部署状态 + def get_site_ssl_info(self, siteName): + ''' + @name 获取证书的部署状态 + @author wzz <2024/4/26 下午4:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + import time + import re + s_file = 'vhost/nginx/{}.conf'.format(siteName) + is_apache = False + if not os.path.exists(s_file): + s_file = 'vhost/apache/{}.conf'.format(siteName) + is_apache = True + + if not os.path.exists(s_file): + return -1 + + s_conf = public.readFile(s_file) + if not s_conf: return -1 + ssl_file = None + if is_apache: + if s_conf.find('SSLCertificateFile') == -1: + return -1 + s_tmp = re.findall(r"SSLCertificateFile\s+(.+\.pem)", s_conf) + if not s_tmp: return -1 + ssl_file = s_tmp[0] + else: + if s_conf.find('ssl_certificate') == -1: + return -1 + s_tmp = re.findall(r"ssl_certificate\s+(.+\.pem);", s_conf) + if not s_tmp: return -1 + ssl_file = s_tmp[0] + ssl_info = public.get_cert_data(ssl_file) + if not ssl_info: return -1 + ssl_info['endtime'] = int( + int(time.mktime(time.strptime(ssl_info['notAfter'], "%Y-%m-%d")) - time.time()) / 86400) + return ssl_info + except: + return -1 + + # 2024/4/19 下午10:05 获取所有project_type为proxy的站点,需要做分页配置,按照添加时间排序 + def get_list(self, get): + ''' + @name 获取所有project_type为proxy的站点,需要做分页配置,按照添加时间排序 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('search').String(), + Param('p').Integer(), + Param('limit').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.p = get.get("p/d", 1) + get.limit = get.get("limit/d", 10) + get.search = get.get("search", "") + + where = "project_type=?" + if get.search != "": + where += " and name like ?" + param = ("proxy", "%{}%".format(get.search)) + else: + param = ("proxy",) + + import db + sql = db.Sql() + count = sql.table('sites').where(where, param).count() + + import page + page = page.Page() + data = {} + info = {} + info['count'] = count + info['row'] = get.limit + + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + if info['p'] < 1: info['p'] = 1 + + try: + from flask import request + info['uri'] = public.url_encode(request.full_path) + except: + info['uri'] = '' + info['return_js'] = '' + data['where'] = where + data['page'] = page.GetPage(info) + + sql.table('sites').where(where, param) + sql.field('id,name,path,status,ps,addtime,edate').order('id desc') + sql.limit(str(page.SHIFT) + ',' + str(page.ROW)) + data['data'] = sql.select() + + try: + path = '/www/server/btwaf/site.json' + waf_res = json.loads(public.readFile(path)) + except: + waf_res = {} + + for site in data['data']: + get.site_name = site["name"] + project_config = self.read_json_conf(get)['message'] + site["healthy"] = 1 + site["waf"] = {} + if not project_config: + site["healthy"] = 0 + site["conf_path"] = "" + site["ssl"] = -1 + site["proxy_pass"] = "" + continue + + site["conf_path"] = public.get_setup_path() + '/panel/vhost/nginx/' + get.site_name + '.conf' + site["ssl"] = self.get_site_ssl_info(get.site_name) + site["proxy_pass"] = project_config["proxy_info"][0]["proxy_pass"] + + if waf_res: + for waf in waf_res: + if "open" in waf_res[waf]: + site["waf"] = {"status": True} + + return public.return_message(0,0,public.returnResult(data=data)) + + # 2024/4/19 下午11:46 给指定网站添加域名 + def add_domain(self, get): + ''' + @name + @author wzz <2024/4/19 下午11:46> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('domains').String(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1,0,"Sitename cannot be empty!") + get.id = get.get("id", "") + if get.id == "": + return public.return_message(-1,0,"ID cannot be empty!") + get.domains = get.get("domains", "") + if get.domains == "": + return public.return_message(-1,0,"Domains cannot be empty!") + if "," in get.domains: + return public.return_message(-1,0,"The domain name cannot contain commas!") + + get.domain_list = get.domains.strip().replace(' ', '').split("\n") + get.domain = ",".join(get.domain_list) + get.webname = get.site_name + port_list = [] + for domain in get.domain_list: + if not ":" in domain.strip(): + continue + + d_port = domain.strip().split(":")[1] + if not public.checkPort(d_port): + return public.return_message(-1,0,'The port number of domain name [{}] is illegal!'.format(domain)) + + port_list.append(d_port) + + # 2024/4/20 上午12:02 更新json文件 + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.proxy_json_conf["domain_list"].extend(get.domain_list) + get.proxy_json_conf["site_port"].extend(port_list) + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + from panelSite import panelSite + result = panelSite().AddDomain(get) + result_status=0 + if not result['status']: + result_status=-1 + return public.return_message(result_status,0,result["msg"]) + + # 2024/4/20 上午12:07 删除指定网站的某个域名 + def del_domain(self, get): + ''' + @name + @author wzz <2024/4/20 上午12:07> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('domains').String(), + Param('id').Integer(), + Param('port').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.id = get.get("id", "") + if get.id == "": + return public.return_message(-1, 0,"ID cannot be empty!") + get.port = get.get("port", "") + if get.port == "": + return public.return_message(-1, 0,"Port cannot be empty!") + get.domain = get.get("domain", "") + if get.domain == "": + return public.return_message(-1, 0,"Domain cannot be empty!") + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.webname = get.site_name + + # 2024/4/20 上午12:02 更新json文件 + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1, 0,"Reading configuration file failed, please delete the website and add it again!") + + if len(get.proxy_json_conf["domain_list"]) == 1: + return public.return_message(-1, 0,"Keep at least one domain name!") + + while get.domain in get.proxy_json_conf["domain_list"]: + get.proxy_json_conf["domain_list"].remove(get.domain) + if get.port in get.proxy_json_conf["site_port"] and len(get.proxy_json_conf["site_port"]) != 1: + while get.port in get.proxy_json_conf["site_port"]: + get.proxy_json_conf["site_port"].remove(get.port) + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + from panelSite import panelSite + result = panelSite().DelDomain(get) + return_status =0 + if not result['status']: + return_status=-1 + return public.return_message(return_status,0, result["msg"]) + + # 2024/4/20 上午12:20 批量删除指定网站域名 + def batch_del_domain(self, get): + ''' + @name 批量删除指定网站域名 + @param get: + @return: + ''' + get.id = get.get("id", "") + if get.id == "": + return public.returnResult(status=False, msg="id不能为空!") + get.domains = get.get("domains", "") + if get.domains == "": + return public.returnResult(status=False, msg="domains不能为空!") + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.webname = get.site_name + + # 2024/4/20 上午12:02 更新json文件 + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + # if len(get.proxy_json_conf["domain_list"]) == 1: + # return public.returnResult(status=False, msg="至少保留一个域名!") + + get.domain_list = get.domains.strip().replace(' ', '').split("\n") + + # if len(get.domain_list) == len(get.proxy_json_conf["domain_list"]): + # return public.returnResult(status=False, msg="至少保留一个域名!") + + for domain in get.domain_list: + while domain in get.proxy_json_conf["domain_list"]: + get.proxy_json_conf["domain_list"].remove(domain) + + if ":" in domain: + port = domain.split(":")[1] + if len(get.proxy_json_conf["site_port"]) == 1: + continue + + while port in get.proxy_json_conf["site_port"]: + get.proxy_json_conf["site_port"].remove(port) + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + from panelSite import panelSite + res_domains = [] + for domain in get.domain_list: + get.domain = domain + get.port = "80" + if ":" in domain: + get.port = domain.split(":")[1] + result = panelSite().DelDomain(get) + res_domains.append({"name": domain, "status": result["status"], "msg": result["msg"]}) + + public.serviceReload() + return public.returnResult(status=True, data=res_domains) + + # 2024/4/20 上午9:17 获取域名列表和https端口 + def get_domain_list(self, get): + ''' + @name 获取域名列表和https端口 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + get.id = get.get("id", "") + if get.id == "": + return public.return_message(-1,0,"ID cannot be empty!") + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1,0,"Sitename cannot be empty!") + + get.siteName = get.webname = get.site_name + get.table = "domain" + get.list = True + get.search = get.id + + # 2024/4/20 上午12:02 更新json文件 + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + result_data = {} + import data + dataObject = data.data() + result_data["domain_list"] = dataObject.getData(get) + if get.proxy_json_conf["ssl_info"]["ssl_status"]: + if not "https_port" in get.proxy_json_conf or get.proxy_json_conf["https_port"] == "": + get.proxy_json_conf["https_port"] = "443" + result_data["https_port"] = get.proxy_json_conf["https_port"] + else: + result_data["https_port"] = "HTTPS not enabled" + + # 2024/4/20 上午9:21 domain_list里面没有的域名健康状态显示为0 + for domain in result_data["domain_list"]: + domain["healthy"] = 1 + if domain["name"] not in get.proxy_json_conf["domain_list"]: + domain["healthy"] = 0 + + public.set_module_logs('site_proxy', 'get_domain_list', 1) + return public.return_message(0,0,result_data) + + # 2024/4/20 下午2:22 获取配置文件 + def get_config(self, get): + ''' + @name + @author wzz <2024/4/20 下午2:22> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + '''# 校验参数 + try: + get.validate([ + Param('site_name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + site_conf = public.readFile(public.get_setup_path() + '/panel/vhost/nginx/' + get.site_name + '.conf') + ssl_conf = get.proxy_json_conf["ssl_info"]["ssl_conf"].format( + site_name=get.site_name, + ) + if get.proxy_json_conf["ssl_info"]["force_https"]: + ssl_conf = get.proxy_json_conf["ssl_info"]["force_ssl_conf"].format( + site_name=get.site_name, + force_conf=get.proxy_json_conf["ssl_info"]["force_ssl_conf"], + ) + + if "If there is abnormal access to the reverse proxy website and the content has already been configured here, please prioritize checking if the configuration here is correct" in get.proxy_json_conf["http_block"]: + http_block = get.proxy_json_conf["http_block"] + else: + http_block = '''# All HTTP fields such as server | upstream | map can be set, such as: +# server {{ +# listen 10086; +# server_name ... +# }} +# upstream stream_ser {{ +# server back_test.com; +# server ... +# }} +{default_describe} +{http_block}'''.format( + default_describe=self._init_proxy_conf["default_describe"], + http_block=get.proxy_json_conf["http_block"], + ) + + if "If there is abnormal access to the reverse proxy website and the content has already been configured here, please prioritize checking if the configuration here is correct" in get.proxy_json_conf["server_block"]: + server_block = get.proxy_json_conf["server_block"] + else: + server_block = '''# All server fields such as server | location can be set, such as: +# location /web {{ +# try_files $uri $uri/ /index.php$is_args$args; +# }} +# error_page 404 /diy_404.html; +{default_describe} +{server_block}'''.format( + default_describe=self._init_proxy_conf["default_describe"], + server_block=get.proxy_json_conf["server_block"], + ) + + data = { + "site_conf": site_conf if not site_conf is False else "", + "http_block": http_block, + "server_block": server_block, + "ssl_conf": ssl_conf, + } + + return public.return_message(0, 0,data) + + # 2024/4/20 下午2:38 保存配置文件 + def save_config(self, get): + ''' + @name 保存配置文件 + @param get: + @return: + '''# 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('conf_type').String(), + Param('body').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.conf_type = get.get("conf_type", "") + if get.conf_type == "": + return public.return_message(-1, 0,"Conf_type cannot be empty!") + + get.body = get.get("body", "") + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + if get.conf_type == "http_block": + get.proxy_json_conf["http_block"] = get.body + else: + get.proxy_json_conf["server_block"] = get.body + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + public.serviceReload() + + return public.return_message(0, 0,"Save successful!") + + # 2024/4/20 下午3:11 根据proxy_json_conf,填入self._template_conf,然后生成nginx配置,保存到指定网站的conf文件中 + def generate_config(self, get): + """ + @name 根据proxy_json_conf,填入self._template_conf,然后生成nginx配置,保存到指定网站的conf文件中 + @param get: + @return: + """ + # 2024/4/20 下午3:36 构造ip黑白名单 + ip_black = "" + ip_white = "" + for ip in get.proxy_json_conf["ip_limit"]["ip_black"]: + ip_black += ("deny {};\n ").format(ip) + for ip in get.proxy_json_conf["ip_limit"]["ip_white"]: + ip_white += ("allow {};\n ").format(ip) + + if ip_white != "": + ip_white += "deny all;" + ip_limit_conf = ip_black + "\n " + ip_white + + proxy_conf = "" + ignore_path = [] + if len(get.proxy_json_conf["proxy_info"]) != 0: + for info in get.proxy_json_conf["proxy_info"]: + proxy_auth_conf = "" + if info["basic_auth"]: + proxy_auth_conf = ("auth_basic \"Authorization\";" + "\n auth_basic_user_file {auth_file};").format( + auth_path=info["basic_auth"]["auth_path"], + auth_file=info["basic_auth"]["auth_file"], + ) + + if len(get.proxy_json_conf["basic_auth"]) != 0: + for auth in get.proxy_json_conf["basic_auth"]: + if info["proxy_path"] == auth["auth_path"]: + ignore_path.append(auth["auth_path"]) + proxy_auth_conf = ("auth_basic \"Authorization\";" + "\n auth_basic_user_file {auth_file};").format( + auth_path=auth["auth_path"], + auth_file=auth["auth_file"], + ) + break + + p_ip_black = "" + p_ip_white = "" + for ip in info["ip_limit"]["ip_black"]: + p_ip_black += ("deny {};\n ").format(ip) + for ip in info["ip_limit"]["ip_white"]: + p_ip_white += ("allow {};\n ").format(ip) + + if p_ip_white != "": + p_ip_white += "deny all;" + p_ip_limit_conf = p_ip_black + "\n " + p_ip_white + + if p_ip_black == "" and p_ip_white == "": + p_ip_limit_conf = "" + + p_gzip_conf = "" + if info["gzip"]["gzip_status"]: + p_gzip_conf = info["gzip"]["gzip_conf"] + + p_sub_filter = "" + if len(info["sub_filter"]["sub_filter_str"]) != 0: + p_sub_filter = 'proxy_set_header Accept-Encoding \"\";' + if not "subs_filter" in get.proxy_json_conf: + get.proxy_json_conf["subs_filter"] = public.ExecShell("nginx -V 2>&1|grep 'ngx_http_substitutions_filter' -o")[0] != "" + + if not get.proxy_json_conf["subs_filter"]: + for filter in info["sub_filter"]["sub_filter_str"]: + p_sub_filter += "\n sub_filter {oldstr} {newstr};".format( + oldstr=filter["oldstr"] if filter["oldstr"] != "" else "\"\"", + newstr=filter["newstr"] if filter["newstr"] != "" else "\"\"", + ) + + p_sub_filter += "\n sub_filter_once off;" + else: + for filter in info["sub_filter"]["sub_filter_str"]: + p_sub_filter += "\n subs_filter {oldstr} {newstr} {sub_type};".format( + oldstr=filter["oldstr"] if filter["oldstr"] != "" else "\"\"", + newstr=filter["newstr"] if filter["newstr"] != "" else "\"\"", + sub_type=filter["sub_type"] if "sub_type" in filter and filter["sub_type"] != "" else "\"\"", + ) + + p_websocket_support = "" + if info["websocket"]["websocket_status"]: + p_websocket_support = info["websocket"]["websocket_conf"] + + timeout_conf = ("proxy_connect_timeout {proxy_connect_timeout};" + "\n proxy_send_timeout {proxy_send_timeout};" + "\n proxy_read_timeout {proxy_read_timeout};").format( + proxy_connect_timeout=info["timeout"]["proxy_connect_timeout"].replace("s", "") + "s", + proxy_send_timeout=info["timeout"]["proxy_send_timeout"].replace("s", "") + "s", + proxy_read_timeout=info["timeout"]["proxy_read_timeout"].replace("s", "") + "s", + ) + + tmp_conf = info["template_proxy_conf"].format( + basic_auth=proxy_auth_conf, + ip_limit=p_ip_limit_conf, + gzip=p_gzip_conf, + sub_filter=p_sub_filter, + proxy_cache=info["proxy_cache"]["cache_conf"], + server_log="", + proxy_pass=info["proxy_pass"], + proxy_host=info["proxy_host"], + proxy_path=info["proxy_path"], + custom_conf=info["custom_conf"], + timeout_conf=timeout_conf, + websocket_support=p_websocket_support, + ) + info["proxy_conf"] = tmp_conf + proxy_conf += tmp_conf + "\n " + + # 2024/4/20 下午3:37 构造basicauth + auth_conf = "" + if len(get.proxy_json_conf["basic_auth"]) != 0: + for auth in get.proxy_json_conf["basic_auth"]: + if auth["auth_path"] not in ignore_path: + tmp_conf = ("location ^~ {auth_path} {{" + "\n auth_basic \"Authorization\";" + "\n auth_basic_user_file {auth_file};" + "\n }}").format(auth_path=auth["auth_path"], auth_file=auth["auth_file"]) + auth_conf += tmp_conf + "\n " + + websocket_support = "" + if get.proxy_json_conf["websocket"]["websocket_status"]: + websocket_support = get.proxy_json_conf["websocket"]["websocket_conf"] + + gzip_conf = "" + if get.proxy_json_conf["gzip"]["gzip_status"]: + gzip_conf = get.proxy_json_conf["gzip"]["gzip_conf"] + + ssl_conf = "#error_page 404/404.html;" + # listen_port = " ".join(get.proxy_json_conf["site_port"]) + # listen_ipv6 = "\n listen [::]:{};".format(" ".join(get.proxy_json_conf["site_port"])) + # port_conf = get.proxy_json_conf["port_conf"].format( + # listen_port=listen_port, + # listen_ipv6=listen_ipv6, + # ) + if not "https_port" in get.proxy_json_conf or get.proxy_json_conf["https_port"] == "": + get.proxy_json_conf["https_port"] = "443" + if not "ipv4_port_conf" in get.proxy_json_conf: + get.proxy_json_conf["ipv4_port_conf"] = "listen {listen_port};" + if not "ipv6_port_conf" in get.proxy_json_conf: + get.proxy_json_conf["ipv6_port_conf"] = "listen [::]:{listen_port};" + if not "ipv4_http3_ssl_port_conf" in get.proxy_json_conf: + get.proxy_json_conf["ipv4_http3_ssl_port_conf"] = "{ipv4_port_conf}\n listen {https_port} quic;\n listen {https_port} ssl;" + if not "ipv6_http3_ssl_port_conf" in get.proxy_json_conf: + get.proxy_json_conf["ipv6_http3_ssl_port_conf"] = "{ipv6_port_conf}\n listen [::]:{https_port} quic;\n listen [::]:{https_port} ssl ;" + if not "ipv4_ssl_port_conf" in get.proxy_json_conf: + get.proxy_json_conf["ipv4_ssl_port_conf"] = "{ipv4_port_conf}\n listen {https_port} ssl http2 ;" + if not "ipv6_ssl_port_conf" in get.proxy_json_conf: + get.proxy_json_conf["ipv6_ssl_port_conf"] = "{ipv6_port_conf}\n listen [::]:{https_port} ssl http2 ;" + + ipv4_port_conf = "" + ipv6_port_conf = "" + for p in get.proxy_json_conf["site_port"]: + ipv4_port_conf += get.proxy_json_conf["ipv4_port_conf"].format( + listen_port=p, + ) + "\n " + ipv6_port_conf += get.proxy_json_conf["ipv6_port_conf"].format( + listen_port=p, + ) + "\n " + if get.proxy_json_conf["ssl_info"]["ssl_status"]: + if public.ExecShell("nginx -V 2>&1| grep 'http_v3_module' -o")[0] != "": + ipv4_http3_ssl_port_conf = get.proxy_json_conf["ipv4_http3_ssl_port_conf"].format( + ipv4_port_conf=ipv4_port_conf, + https_port=get.proxy_json_conf["https_port"], + ) + "\n " + ipv6_http3_ssl_port_conf = get.proxy_json_conf["ipv6_http3_ssl_port_conf"].format( + ipv6_port_conf=ipv6_port_conf, + https_port=get.proxy_json_conf["https_port"], + ) + "\n " + port_conf = ipv4_http3_ssl_port_conf + ipv6_http3_ssl_port_conf + "\n http2 on;" + else: + ipv4_ssl_port_conf = get.proxy_json_conf["ipv4_ssl_port_conf"].format( + ipv4_port_conf=ipv4_port_conf, + https_port=get.proxy_json_conf["https_port"], + ) + "\n " + ipv6_ssl_port_conf = get.proxy_json_conf["ipv6_ssl_port_conf"].format( + ipv6_port_conf=ipv6_port_conf, + https_port=get.proxy_json_conf["https_port"], + ) + "\n " + port_conf = ipv4_ssl_port_conf + ipv6_ssl_port_conf + ssl_conf = get.proxy_json_conf["ssl_info"]["ssl_conf"].format(site_name=get.site_name) + if get.proxy_json_conf["ssl_info"]["force_https"]: + ssl_conf = get.proxy_json_conf["ssl_info"]["force_ssl_conf"].format( + site_name=get.site_name, + force_conf=get.proxy_json_conf["ssl_info"]["force_conf"] + ) + else: + port_conf = ipv4_port_conf + "\n" + ipv6_port_conf + + redirect_conf = "" + if get.proxy_json_conf["redirect"]["redirect_status"]: + redirect_conf = get.proxy_json_conf["redirect"]["redirect_conf"].format(site_name=get.site_name) + + security_conf = "" + if get.proxy_json_conf["security"]["security_status"]: + domains = get.proxy_json_conf["security"]["domains"] if not get.proxy_json_conf["security"][ + "http_status"] else "none blocked " + get.proxy_json_conf["security"]["domains"] + security_conf = get.proxy_json_conf["security"]["security_conf"].format( + static_resource=get.proxy_json_conf["security"]["static_resource"], + expires="30d", + domains=domains.replace(",", " "), + return_resource=get.proxy_json_conf["security"]["return_resource"], + ) + + default_cache = get.proxy_json_conf["default_cache"].format( + site_name=get.site_name, + cache_name=get.site_name.replace(".", "_") + ) + get.http_block = default_cache + "\n" + get.proxy_json_conf["http_block"] + + # 2024/6/4 下午4:20 兼容新版监控报表的配置 + monitor_conf = "" + if (os.path.exists("/www/server/panel/plugin/monitor/monitor_main.py") and + os.path.exists("/www/server/monitor/config/sites.json")): + try: + sites_data = json.loads(public.readFile("/www/server/monitor/config/sites.json")) + + if sites_data[get.site_name]["open"]: + id = public.M('domain').where("name=?", (get.site_name,)).getField('id') + monitor_conf = '''#Monitor-Config-Start monitor log sending configuration + access_log syslog:server=unix:/tmp/bt-monitor.sock,nohostname,tag={sid}__access monitor; + error_log syslog:server=unix:/tmp/bt-monitor.sock,nohostname,tag={sid}__error; + #Monitor-Config-End'''.format(sid=id) + except: + pass + + get.site_conf = self._template_conf.format( + http_block=get.http_block, + server_block=get.proxy_json_conf["server_block"], + port_conf=port_conf, + ssl_start_msg=public.getMsg('NGINX_CONF_MSG1'), + err_page_msg=public.getMsg('NGINX_CONF_MSG2'), + php_info_start=public.getMsg('NGINX_CONF_MSG3'), + rewrite_start_msg=public.getMsg('NGINX_CONF_MSG4'), + domains=' '.join(get.proxy_json_conf["domain_list"]) if len( + get.proxy_json_conf["domain_list"]) > 1 else get.site_name, + site_name=get.site_name, + ssl_info=ssl_conf, + err_age_404=get.proxy_json_conf["err_age_404"], + err_age_502=get.proxy_json_conf["err_age_502"], + ip_limit_conf=ip_limit_conf, + auth_conf=auth_conf, + sub_filter="", + gzip_conf=gzip_conf, + security_conf=security_conf, + redirect_conf=redirect_conf, + proxy_conf=proxy_conf, + proxy_cache=get.proxy_json_conf["proxy_cache"]["cache_conf"], + server_log=get.proxy_json_conf["proxy_log"]["log_conf"], + site_path=get.proxy_json_conf["site_path"], + websocket_support=websocket_support, + monitor_conf=monitor_conf, + ) + + # 2024/4/21 下午10:46 设置商业ssl证书 + def set_cert(self, get): + ''' + @name + @author wzz <2024/4/21 下午10:46> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.oid = get.get("oid", "") + if get.oid == "": + return public.returnResult(status=False, msg="oid不能为空!") + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.siteName = get.site_name + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from panelSSL import panelSSL + ssl_obj = panelSSL() + set_result = ssl_obj.set_cert(get) + if set_result["status"] == False: + return set_result + + get.proxy_json_conf["ssl_info"]["ssl_status"] = True + get.proxy_json_conf["https_port"] = "443" + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return set_result + + # 2024/4/21 下午11:03 关闭SSl证书 + def close_ssl(self, get): + ''' + @name + @author wzz <2024/4/21 下午11:04> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.siteName = get.site_name + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from panelSite import panelSite + result = panelSite().CloseSSLConf(get) + if not result["status"]: + return result + + get.proxy_json_conf["ssl_info"]["ssl_status"] = False + get.proxy_json_conf["https_port"] = "443" + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return result + + # 2024/4/21 下午11:10 保存指定网站的SSL证书 + def set_ssl(self, get): + ''' + @name 保存指定网站的SSL证书 + @author wzz <2024/4/21 下午11:12> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.key = get.get("key", "") + if get.key == "": + return public.returnResult(status=False, msg="key不能为空!") + + get.csr = get.get("csr", "") + if get.csr == "": + return public.returnResult(status=False, msg="csr不能为空!") + + get.siteName = get.site_name + get.type = -1 + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from panelSite import panelSite + result = panelSite().SetSSL(get) + if not result["status"]: + # return public.returnResult(status=False, msg=result["msg"]) + return result + + get.proxy_json_conf["ssl_info"]["ssl_status"] = True + get.proxy_json_conf["https_port"] = "443" + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return result + + # 2024/4/21 下午11:29 部署测试证书 + def set_test_cert(self, get): + ''' + @name + @author wzz <2024/4/21 下午11:30> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.partnerOrderId = get.get("partnerOrderId", "") + if get.partnerOrderId == "": + return public.returnResult(status=False, msg="partnerOrderId不能为空!") + + get.siteName = get.site_name + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from panelSSL import panelSSL + ssl_obj = panelSSL() + set_result = ssl_obj.GetSSLInfo(get) + if set_result["status"] == False: + return set_result + + get.proxy_json_conf["ssl_info"]["ssl_status"] = True + get.proxy_json_conf["https_port"] = "443" + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return set_result + + # 2024/4/21 下午11:33 申请let' encrypt证书 + def apply_cert_api(self, get): + ''' + @name + @author wzz <2024/4/21 下午11:34> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.domains = get.get("domains", "") + if get.domains == "": + return public.returnResult(status=False, msg="domains不能为空!") + + get.auth_type = get.get("auth_type", "") + if get.auth_type == "": + return public.returnResult(status=False, msg="auth_type不能为空!") + + get.auth_to = get.get("auth_to", "") + if get.auth_to == "": + return public.returnResult(status=False, msg="auth_to不能为空!") + + get.auto_wildcard = get.get("auto_wildcard", "") + if get.auto_wildcard == "": + return public.returnResult(status=False, msg="auto_wildcard不能为空!") + + get.id = get.get("id", "") + if get.id == "": + return public.returnResult(status=False, msg="id不能为空!") + + get.siteName = get.site_name + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from acme_v2 import acme_v2 + acme = acme_v2() + result = acme.apply_cert_api(get) + if not result["status"]: + return result + + get.proxy_json_conf["ssl_info"]["ssl_status"] = True + get.proxy_json_conf["https_port"] = "443" + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return result + + # 2024/4/21 下午11:36 验证let' encrypt dns + def apply_dns_auth(self, get): + ''' + @name + @author wzz <2024/4/21 下午11:36> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.index = get.get("index", "") + if get.index == "": + return public.returnResult(status=False, msg="index不能为空!") + + from acme_v2 import acme_v2 + acme = acme_v2() + return acme.apply_dns_auth(get) + + # 2024/4/21 下午11:44 设置证书夹里面的证书 + def SetBatchCertToSite(self, get): + ''' + @name + @author wzz <2024/4/21 下午11:44> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.BatchInfo = get.get("BatchInfo", "") + if get.BatchInfo == "": + return public.returnResult(status=False, msg="BatchInfo不能为空!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from panelSSL import panelSSL + ssl_obj = panelSSL() + set_result = ssl_obj.SetBatchCertToSite(get) + if not "successList" in set_result: + return set_result + + for re in set_result["successList"]: + if re["status"] and re["siteName"] == get.site_name: + get.proxy_json_conf["ssl_info"]["ssl_status"] = True + break + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return set_result + + # 2024/4/22 上午9:43 设置强制https + def set_force_https(self, get): + ''' + @name 设置强制https + @param get: + site_name: 网站名 + force_https: 1/0 + @return: + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.force_https = get.get("force_https/d", 999) + if get.force_https == 999: + return public.returnResult(status=False, msg="force_https不能为空!") + + get.siteName = get.site_name + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.proxy_json_conf["ssl_info"]["force_https"] = True if get.force_https == 1 else False + + from panelSite import panelSite + if get.force_https == 1: + result = panelSite().HttpToHttps(get) + else: + result = panelSite().CloseToHttps(get) + + if not result["status"]: + return result + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return result + + # 2024/4/22 上午10:27 创建重定向 + def CreateRedirect(self, get): + ''' + @name + @author wzz <2024/4/22 上午10:27> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('domainorpath').String(), + Param('redirecttype').String(), + Param('redirectpath').String(), + Param('tourl').String(), + Param('redirectdomain').String(), + Param('redirectname').String(), + Param('type').Integer(), + Param('holdpath').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.domainorpath = get.get("domainorpath", "") + if get.domainorpath == "": + return public.return_message(-1, 0,"Domainorpath cannot be empty!") + + get.redirecttype = get.get("redirecttype", "") + if get.redirecttype == "": + return public.return_message(-1, 0,"Redirecttype cannot be empty!") + + get.redirectpath = get.get("redirectpath", "") + if get.domainorpath == "path" and get.redirectpath == "": + return public.return_message(-1, 0,"Redirectpath cannot be empty!") + + get.tourl = get.get("tourl", "") + if get.tourl == "": + return public.return_message(-1, 0,"Tour cannot be empty!") + + get.redirectdomain = get.get("redirectdomain", "") + if get.domainorpath == "domain" and get.redirectdomain == "": + return public.return_message(-1, 0,"Redirectdomain cannot be empty!") + + get.redirectname = get.get("redirectname", "") + if get.redirectname == "": + return public.return_message(-1, 0,"Redirectname cannot be empty!") + + get.sitename = get.site_name + get.type = 1 + get.holdpath = 1 + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.proxy_json_conf["redirect"]["redirect_status"] = True + + from panelRedirect import panelRedirect + result = panelRedirect().CreateRedirect(get) + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + if not result['status']: + return public.return_message(-1,0,result['msg']) + return public.return_message(0,0,result['msg']) + + # 2024/4/22 上午10:45 删除指定网站的某个重定向规则 + def DeleteRedirect(self, get): + ''' + @name 删除指定网站的某个重定向规则 + @author wzz <2024/4/22 上午10:45> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('redirectname').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.redirectname = get.get("redirectname", "") + if get.redirectname == "": + return public.return_message(-1, 0,"Redirectname cannot be empty!") + + get.sitename = get.site_name + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + from panelRedirect import panelRedirect + redirect_list = panelRedirect().GetRedirectList(get) + if len(redirect_list) == 0: + get.proxy_json_conf["redirect"]["redirect_status"] = False + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + result =panelRedirect().DeleteRedirect(get) + if not result['status']: + return public.return_message(-1,0,result['msg']) + return public.return_message(0,0,result['msg']) + + # 2024/4/23 上午10:38 编辑指定网站的某个重定向规则 + def ModifyRedirect(self, get): + ''' + @name 编辑指定网站的某个重定向规则 + @author wzz <2024/4/23 上午10:38> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + '''# 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('domainorpath').String(), + Param('redirecttype').String(), + Param('redirectpath').String(), + Param('tourl').String(), + Param('redirectdomain').String(), + Param('redirectname').String(), + Param('type').Integer(), + Param('holdpath').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.domainorpath = get.get("domainorpath", "") + if get.domainorpath == "": + return public.return_message(-1, 0,"Domainorpath cannot be empty!") + + get.redirecttype = get.get("redirecttype", "") + if get.redirecttype == "": + return public.return_message(-1, 0,"Redirecttype cannot be empty!") + + get.redirectpath = get.get("redirectpath", "") + if get.domainorpath == "path" and get.redirectpath == "": + return public.return_message(-1, 0,"Redirectpath cannot be empty!") + + get.tourl = get.get("tourl", "") + if get.tourl == "": + return public.return_message(-1, 0,"Tour cannot be empty!") + + get.redirectdomain = get.get("redirectdomain", "") + if get.domainorpath == "domain" and get.redirectdomain == "": + return public.return_message(-1, 0,"Redirectdomain cannot be empty!") + + get.redirectname = get.get("redirectname", "") + if get.redirectname == "": + return public.return_message(-1, 0,"Redirectname cannot be empty!") + + get.sitename = get.site_name + get.type = get.get("type/d", 1) + get.holdpath = get.get("holdpath/d", 1) + + from panelRedirect import panelRedirect + result =panelRedirect().ModifyRedirect(get) + if not result['status']: + return public.return_message(-1, 0,result['msg']) + return public.return_message(0, 0,result['msg']) + + # 2024/4/26 下午3:32 获取指定网站指定重定向规则的信息 + def GetRedirectFile(self, get): + ''' + @name 获取指定网站指定重定向规则的信息 + @author wzz <2024/4/26 下午3:32> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + try: + get.validate([ + Param('path').String(), + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.path = get.get("path", "") + if get.path == "": + return public.return_message(-1, 0,"Path cannot be empty!") + + if not os.path.exists(get.path): + return public.return_message(-1, 0,"Redirection has stopped or the configuration file directory does not exist!") + import files + f = files.files() + result = f.GetFileBody(get) + if not result['status']: + del result['status'] + return public.return_message(-1, 0,result) + del result['status'] + return public.return_message(0, 0,result) + + # 2024/4/22 上午11:12 设置防盗链 + def SetSecurity(self, get): + ''' + @name 设置防盗链 + @author wzz <2024/4/22 上午11:12> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('fix').String(), + Param('domains').String(), + Param('return_rule').String(), + Param('name').String(), + Param('http_status').Bool(), + Param('status').Bool(), + Param('id').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.fix = get.get("fix", "") + if get.fix == "": + return public.return_message(-1, 0,"Fix cannot be empty!") + + get.domains = get.get("domains", "") + if get.domains == "": + return public.return_message(-1, 0,"Domains cannot be empty!") + + get.return_rule = get.get("return_rule", "") + if get.return_rule == "": + return public.return_message(-1, 0,"Return_rule cannot be empty!") + + get.http_status = get.get("http_status", "") + if get.http_status == "": + return public.return_message(-1, 0,"Https status cannot be empty!") + + get.status = get.get("status", "") + if get.status == "": + return public.return_message(-1, 0,"Status cannot be empty!") + + get.id = get.get("id", "") + if get.id == "": + return public.return_message(-1, 0,"ID cannot be empty!") + + get.name = get.site_name + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.proxy_json_conf["security"]["security_status"] = True if get.status == "true" else False + get.proxy_json_conf["security"]["static_resource"] = get.fix + get.proxy_json_conf["security"]["domains"] = get.domains + get.proxy_json_conf["security"]["return_resource"] = get.return_rule + get.proxy_json_conf["security"]["http_status"] = True if get.http_status else False + + from panel_site_v2 import panelSite + result = panelSite().SetSecurity(get) + if result["status"]==-1: + return result + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return result + + # 2024/4/23 下午3:07 添加全局IP黑白名单 + def add_ip_limit(self, get): + ''' + @name 添加全局IP黑白名单 + @author wzz <2024/4/23 下午3:08> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('ip_type').String(), + Param('ips').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.ip_type = get.get("ip_type", "black") + if get.ip_type not in ["black", "white"]: + return public.return_message(-1, 0,"The ip_type parameter is incorrect, black or white must be passed!") + + get.ips = get.get("ips", "") + if get.ips == "": + return public.return_message(-1, 0,"IPS cannot be empty, please enter IP, one per line!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.ips = get.ips.split("\n") + for ip in get.ips: + if ip not in get.proxy_json_conf["ip_limit"]["ip_{}".format(get.ip_type)]: + get.proxy_json_conf["ip_limit"]["ip_{}".format(get.ip_type)].append(ip) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/23 下午3:12 删除全局IP黑白名单 + def del_ip_limit(self, get): + ''' + @name 删除全局IP黑白名单 + @author wzz <2024/4/23 下午3:12> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('ip_type').String(), + Param('ip').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.ip_type = get.get("ip_type", "black") + if get.ip_type not in ["black", "white"]: + return public.return_message(-1, 0,"The ip_type parameter is incorrect, black or white must be passed!") + + get.ip = get.get("ip", "") + if get.ip == "": + return public.return_message(-1, 0,"IP cannot be empty, please enter IP!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + if get.ip in get.proxy_json_conf["ip_limit"]["ip_{}".format(get.ip_type)]: + get.proxy_json_conf["ip_limit"]["ip_{}".format(get.ip_type)].remove(get.ip) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Delete successful!") + + # 2024/4/23 下午3:13 批量删除全局IP黑白名单 + def batch_del_ip_limit(self, get): + ''' + @name 批量删除全局IP黑白名单 + @author wzz <2024/4/23 下午3:14> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('ip_type').String(), + Param('ips').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.ip_type = get.get("ip_type", "black") + if get.ip_type not in ["black", "white", "all"]: + return public.return_message(-1, 0,"The ip_type parameter is incorrect, black or white must be passed!") + + get.ips = get.get("ips", "") + if get.ips == "": + return public.return_message(-1, 0,"IPS cannot be empty, please enter IP, one per line!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.ips = get.ips.split("\n") + for ip in get.ips: + if get.ip_type == "all": + if ip in get.proxy_json_conf["ip_limit"]["ip_black"]: + get.proxy_json_conf["ip_limit"]["ip_black"].remove(ip) + if ip in get.proxy_json_conf["ip_limit"]["ip_white"]: + get.proxy_json_conf["ip_limit"]["ip_white"].remove(ip) + else: + if ip in get.proxy_json_conf["ip_limit"]["ip_{}".format(get.ip_type)]: + get.proxy_json_conf["ip_limit"]["ip_{}".format(get.ip_type)].remove(ip) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Delete successful!") + + # 2024/4/22 下午9:01 获取指定网站的方向代理列表 + def get_proxy_list(self, get): + ''' + @name + @author wzz <2024/4/22 下午9:02> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + '''# 校验参数 + try: + get.validate([ + Param('site_name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + if len(get.proxy_json_conf["proxy_info"]) == 0: + return public.return_message(-1, 0,"No proxy information!") + + subs_filter = get.proxy_json_conf["subs_filter"] if "subs_filter" in get.proxy_json_conf else public.ExecShell("nginx -V 2>&1|grep 'ngx_http_substitutions_filter' -o")[0] != "" + + if get.proxy_path != "": + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + info["global_websocket"] = get.proxy_json_conf["websocket"]["websocket_status"] + info["subs_filter"] = subs_filter + if "http://unix:" in info["proxy_pass"]: + info["proxy_pass"] = info["proxy_pass"].replace("http://unix:", "") + return public.return_message(0, 0,info) + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + public.set_module_logs('site_proxy', 'get_proxy_list', 1) + return public.return_message(0, 0,get.proxy_json_conf["proxy_info"]) + + # 2024/4/23 上午11:16 获取指定网站的所有配置信息 + def get_global_conf(self, get): + ''' + @name 获取指定网站的所有配置信息 + @author wzz <2024/4/23 上午11:16> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0, "Sitename cannot be empty!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + return public.return_message(0, 0, get.proxy_json_conf) + + # 2024/4/22 下午9:04 设置指定网站指定URL的反向代理 + def set_url_proxy(self, get): + ''' + @name + @author wzz <2024/4/22 下午9:04> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + Param('remark').String(), + Param('proxy_pass').String(), + Param('proxy_host').String(), + Param('proxy_type').String(), + Param('websocket').Integer(), + Param('proxy_connect_timeout').Integer(), + Param('proxy_send_timeout').Integer(), + Param('proxy_read_timeout').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.proxy_host = get.get("proxy_host", "") + if get.proxy_host == "": + return public.return_message(-1, 0,"Proxy_host cannot be empty!") + + get.proxy_pass = get.get("proxy_pass", "") + if get.proxy_pass == "": + return public.return_message(-1, 0,"Proxy_pass cannot be empty!") + + get.proxy_type = get.get("proxy_type", "") + if get.proxy_type == "": + return public.return_message(-1, 0,"Proxy_type cannot be empty!") + + get.proxy_connect_timeout = get.get("proxy_connect_timeout", "60s") + get.proxy_send_timeout = get.get("proxy_send_timeout", "600s") + get.proxy_read_timeout = get.get("proxy_read_timeout", "600s") + + get.remark = get.get("remark", "") + if get.remark != "": + get.remark = public.xssencode2(get.remark) + + if get.proxy_type == "unix": + if not get.proxy_pass.startswith("http://unix:"): + if not get.proxy_pass.startswith("/"): + return public.return_message(-1, 0,"Unix file path must be in/or http://unix: At the beginning, such as/tmp/flash.app. lock!") + if not get.proxy_pass.endswith(".sock"): + return public.return_message(-1, 0,"Unix files must end with. lock, such as/tmp/flash.app. lock!") + if not os.path.exists(get.proxy_pass): + return public.return_message(-1, 0,"The proxy target does not exist!") + get.proxy_pass = "http://unix:" + get.proxy_pass + elif get.proxy_type == "http": + if not get.proxy_pass.startswith("http://") and not get.proxy_pass.startswith("https://"): + return public.return_message(-1, 0,"The proxy target must start with http://or https://!") + + get.websocket = get.get("websocket/d", 1) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + if get.proxy_json_conf["websocket"]["websocket_status"] and get.websocket != 1: + return public.return_message(-1, 0,"The global websocket is in an open state, and it is not allowed to individually disable websocket support for this URL!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + info["proxy_host"] = get.proxy_host + info["proxy_pass"] = get.proxy_pass + info["proxy_type"] = get.proxy_type + info["timeout"]["proxy_connect_timeout"] = get.proxy_connect_timeout.replace("s", "") + info["timeout"]["proxy_send_timeout"] = get.proxy_send_timeout.replace("s", "") + info["timeout"]["proxy_read_timeout"] = get.proxy_read_timeout.replace("s", "") + info["websocket"]["websocket_status"] = True if get.websocket == 1 else False + info["remark"] = get.remark + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/22 下午9:34 删除指定网站指定URL的反向代理 + def del_url_proxy(self, get): + ''' + @name 删除指定网站指定URL的反向代理 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + get.proxy_json_conf["proxy_info"].remove(info) + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Delete successful!") + + # 2024/4/22 下午9:36 设置指定网站指定URL反向代理的备注 + def set_url_remark(self, get): + ''' + @name 设置指定网站指定URL反向代理的备注 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + Param('remark').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1,0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1,0,"Proxy_path cannot be empty!") + + get.remark = get.get("remark", "") + if get.remark != "": + get.remark = public.xssencode2(get.remark) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + info["remark"] = get.remark + break + else: + return public.return_message(-1,0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0,0,"Set successfully!") + + # 2024/4/22 下午9:40 添加指定网站指定URL的内容替换 + def add_sub_filter(self, get): + ''' + @name 添加指定网站指定URL的内容替换 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('oldstr').String(), + Param('newstr').String(), + Param('proxy_path').String(), + Param('sub_type').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.oldstr = get.get("oldstr", "") + get.newstr = get.get("newstr", "") + + if get.oldstr == "" and get.newstr == "": + return public.return_message(-1, 0,"Oldstr and Newstr cannot be empty at the same time!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.sub_type = get.get("sub_type", "g") + if get.sub_type == "": + get.sub_type = "g" + import re + if not re.match(r'^[ior]+$|^g(?!.*o)|^o(?!.*g)$', get.sub_type): + return public.return_message(-1, 0,"Get.sub_type can only contain letter combinations from 'g', 'i', 'o', or 'r', and 'g' and 'o' cannot coexist!") + + is_subs = public.ExecShell("nginx -V 2>&1|grep 'ngx_http_substitutions_filter' -o")[0] + if not is_subs and re.search(u'[\u4e00-\u9fa5]', get.oldstr + get.newstr): + return public.return_message(-1, 0,"The content you entered contains Chinese. We have detected that the current version of nginx does not support it. Please try reinstalling a version of nginx 1.20 or higher and try again!") + + if get.sub_type != "g" and not is_subs: + return public.return_message(-1, 0,"Detected that the current nginx version only supports default replacement types. Please try reinstalling nginx version 1.20 or higher and try again!") + + if not "g" in get.sub_type and not "o" in get.sub_type: + get.sub_type = "g" + get.sub_type + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + for sub in info["sub_filter"]["sub_filter_str"]: + if get.oldstr == sub["oldstr"]: + return public.return_message(-1, 0,"Content before replacement: The configuration information for [{}] already exists, please do not add it again!".format( + get.oldstr)) + info["sub_filter"]["sub_filter_str"].append( + { + "sub_type": get.sub_type, + "oldstr": get.oldstr, + "newstr": get.newstr + } + ) + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/22 下午10:00 删除指定网站指定URL的内容替换 + def del_sub_filter(self, get): + ''' + @name 删除指定网站指定URL的内容替换 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('oldstr').String(), + Param('newstr').String(), + Param('proxy_path').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.oldstr = get.get("oldstr", "") + get.newstr = get.get("newstr", "") + + if get.oldstr == "" and get.newstr == "": + return public.return_message(-1, 0,"Oldstr and Newstr cannot be empty at the same time!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + for sub in info["sub_filter"]["sub_filter_str"]: + if get.oldstr == sub["oldstr"]: + info["sub_filter"]["sub_filter_str"].remove(sub) + break + else: + return public.return_message(-1, 0,"No configuration information found for content before replacement: [{}]!".format(get.oldstr)) + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Delete successful!") + + # 2024/4/22 下午10:03 设置指定网站指定URL的内容压缩 + def set_url_gzip(self, get): + ''' + @name 设置指定网站指定URL的内容压缩 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('gzip_min_length').String(), + Param('proxy_path').String(), + Param('gzip_types').String(), + Param('gzip_status').Integer(), + Param('gzip_comp_level').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.gzip_status = get.get("gzip_status/d", 999) + if get.gzip_status == 999: + return public.return_message(-1, 0,"Gzip_status cannot be empty, please pass number 1 or 0!") + get.gzip_min_length = get.get("gzip_min_length", "10k") + get.gzip_comp_level = get.get("gzip_comp_level", "6") + if get.gzip_min_length[0] == "0" or get.gzip_min_length.startswith("-"): + return public.return_message(-1, 0,"The gzip_min_length parameter is invalid. Please enter a number greater than 0!") + if get.gzip_comp_level == "0" or get.gzip_comp_level.startswith("-"): + return public.return_message(-1, 0,"The gzip_comp_level parameter is invalid. Please enter a number greater than 0!") + get.gzip_types = get.get( + "gzip_types", + "text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/json image/jpeg image/gif image/png font/ttf font/otf image/svg+xml application/xml+rss text/x-js" + ) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + info["gzip"]["gzip_status"] = True if get.gzip_status == 1 else False + if get.gzip_status == 1: + info["gzip"]["gzip_types"] = get.gzip_types + info["gzip"]["gzip_min_length"] = get.gzip_min_length + info["gzip"]["gzip_comp_level"] = get.gzip_comp_level + info["gzip"]["gzip_conf"] = ("gzip on;" + "\n gzip_min_length {gzip_min_length};" + "\n gzip_buffers 4 16k;" + "\n gzip_http_version 1.1;" + "\n gzip_comp_level {gzip_comp_level};" + "\n gzip_types {gzip_types};" + "\n gzip_vary on;" + "\n gzip_proxied expired no-cache no-store private auth;" + "\n gzip_disable \"MSIE [1-6]\\.\";").format( + gzip_min_length=get.gzip_min_length, + gzip_comp_level=get.gzip_comp_level, + gzip_types=get.gzip_types, + ) + else: + info["gzip"]["gzip_conf"] = "" + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/22 下午10:15 添加指定网站指定URL的IP黑白名单 + def add_url_ip_limit(self, get): + ''' + @name 添加指定网站指定URL的IP黑白名单 + @author wzz <2024/4/22 下午10:16> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + Param('ip_type').String(), + Param('ips').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.ip_type = get.get("ip_type", "black") + if get.ip_type not in ["black", "white"]: + return public.return_message(-1, 0,"The ip_type parameter is incorrect, black or white must be passed!") + + get.ips = get.get("ips", "") + if get.ips == "": + return public.return_message(-1, 0,"IPS cannot be empty, please enter IP, one per line!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.ips = get.ips.split("\n") + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + for ip in get.ips: + if get.ip_type == "black": + if not ip in info["ip_limit"]["ip_black"]: + info["ip_limit"]["ip_black"].append(ip) + else: + if not ip in info["ip_limit"]["ip_white"]: + info["ip_limit"]["ip_white"].append(ip) + + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/22 下午10:21 删除指定网站指定URL的IP黑白名单 + def del_url_ip_limit(self, get): + ''' + @name 删除指定网站指定URL的IP黑白名单 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + Param('ip_type').String(), + Param('ip').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0,"Proxy_path cannot be empty!") + + get.ip_type = get.get("ip_type", "black") + if get.ip_type not in ["black", "white"]: + return public.return_message(-1, 0,"The ip_type parameter is incorrect, black or white must be passed!") + + get.ip = get.get("ip", "") + if get.ip == "": + return public.return_message(-1, 0,"IP cannot be empty, please enter IP!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + if get.ip in info["ip_limit"]["ip_{ip_type}".format(ip_type=get.ip_type)]: + info["ip_limit"]["ip_{ip_type}".format(ip_type=get.ip_type)].remove(get.ip) + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/24 上午11:21 批量删除指定网站指定URL的IP黑白名单 + def batch_del_url_ip_limit(self, get): + ''' + @name 批量删除指定网站指定URL的IP黑白名单 + @author wzz <2024/4/24 上午11:22> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + Param('ip_type').String(), + Param('ips').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0, "Sitename cannot be empty!") + + get.ip_type = get.get("ip_type", "black") + if get.ip_type not in ["black", "white", "all"]: + return public.return_message(-1, 0, "The ip_type parameter is incorrect, black or white must be passed!") + + get.ips = get.get("ips", "") + if get.ips == "": + return public.return_message(-1, 0, "IPS cannot be empty, please enter IP, one per line!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + get.ips = get.ips.split("\n") + if get.ip_type == "all": + for ip in get.ips: + if ip in info["ip_limit"]["ip_black"]: + info["ip_limit"]["ip_black"].remove(ip) + if ip in info["ip_limit"]["ip_white"]: + info["ip_limit"]["ip_white"].remove(ip) + else: + for ip in get.ips: + if ip in info["ip_limit"]["ip_{ip_type}".format(ip_type=get.ip_type)]: + info["ip_limit"]["ip_{ip_type}".format(ip_type=get.ip_type)].remove(ip) + break + else: + return public.return_message(-1, 0, "No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0, "Delete successful!") + + # 2024/4/22 下午8:14 设置指定网站指定URL的缓存 + def set_url_cache(self, get): + ''' + @name 设置指定网站指定URL的缓存 + @param get: + @return: + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('expires').String(), + Param('proxy_path').String(), + Param('cache_status').Integer(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.cache_status = get.get("cache_status/d", 999) + if get.cache_status == 999: + return public.return_message(-1, 0,"Cache_status cannot be empty, please pass number 1 or 0!") + + get.expires = get.get("expires", "1d") + if get.expires[0] == "0" or get.expires.startswith("-"): + return public.return_message(-1, 0,"The expires parameter is illegal. Please enter a number greater than 0!") + + expires = "expires {}".format(get.expires) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + static_cache = ("\n location ~ .*\\.(css|js|jpe?g|gif|png|webp|woff|eot|ttf|svg|ico|css\\.map|js\\.map)$" + "\n {{" + "\n {expires};" + "\n error_log /dev/null;" + "\n access_log /dev/null;" + "\n }}").format( + expires=expires, + ) + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + info["proxy_cache"]["cache_status"] = True if get.cache_status == 1 else False + info["proxy_cache"]["expires"] = get.expires + if get.cache_status == 1: + info["proxy_cache"]["cache_conf"] = ("\n proxy_cache {cache_zone};" + "\n proxy_cache_key $host$uri$is_args$args;" + "\n proxy_ignore_headers Set-Cookie Cache-Control expires X-Accel-Expires;" + "\n proxy_cache_valid 200 304 301 302 {expires};" + "\n proxy_cache_valid 404 1m;" + "{static_cache}").format( + cache_zone=get.proxy_json_conf["proxy_cache"]["cache_zone"], + expires=get.expires, + static_cache=static_cache, + ) + else: + info["proxy_cache"]["cache_conf"] = "" + break + else: + return public.return_message(-1, 0,"No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0,"Set successfully!") + + # 2024/4/24 上午9:57 设置指定网站指定URL的自定义配置 + def set_url_custom_conf(self, get): + ''' + @name 设置指定网站指定URL的自定义配置 + @author wzz <2024/4/24 上午9:58> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + Param('proxy_path').String(), + Param('custom_conf').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0, "Sitename cannot be empty!") + + get.proxy_path = get.get("proxy_path", "") + if get.proxy_path == "": + return public.return_message(-1, 0, "Proxy_path cannot be empty!") + + get.custom_conf = get.get("custom_conf", "") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + for info in get.proxy_json_conf["proxy_info"]: + if info["proxy_path"] == get.proxy_path: + info["custom_conf"] = get.custom_conf + break + else: + return public.return_message(-1, 0, "No proxy information found for this URL [{}]!".format(get.proxy_path)) + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.return_message(0, 0, "Set successfully!") + + @staticmethod + def nginx_get_log_file(nginx_config: str, is_error_log: bool = False): + import re + if is_error_log: + re_data = re.findall(r"error_log +(/(\S+/?)+) ?(.*?);", nginx_config) + else: + re_data = re.findall(r"access_log +(/(\S+/?)+) ?(.*?);", nginx_config) + if re_data is None: + return None + for i in re_data: + file_path = i[0].strip(";") + if file_path != "/dev/null": + return file_path + return None + + def xsssec(self, text): + replace_list = { + "<": "<", + ">": ">", + "'": "'", + '"': """, + } + for k, v in replace_list.items(): + text = text.replace(k, v) + return public.xssencode2(text) + + # 2024/4/24 下午5:39 获取指定网站的网站日志 + def GetSiteLogs(self, get): + ''' + @name 获取指定网站的网站日志 + @author wzz <2024/4/24 下午5:39> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('type').String(), + Param('site_name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"site_name不能为空!") + + get.type = get.get("type", "access") + log_name = get.site_name + if get.type != "access": + log_name = get.site_name + ".error" + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + if get.proxy_json_conf["proxy_log"]["log_type"] == "default": + log_file = public.get_logs_path() + "/" + log_name + '.log' + elif get.proxy_json_conf["proxy_log"]["log_type"] == "file": + log_file = get.proxy_json_conf["proxy_log"]["log_path"] + "/" + log_name + '.log' + else: + return public.return_message(0, 0,{"msg": "", "size": 0}) + + if os.path.exists(log_file): + return public.return_message(0, 0,{ + "msg": self.xsssec(public.GetNumLines(log_file, 1000)), + "size": public.to_size(os.path.getsize(log_file)) + } + ) + + return public.return_message(0, 0,{"msg": "", "size": 0}) + + # 2024/4/25 上午10:51 清理指定网站的反向代理缓存 + def clear_cache(self, get): + ''' + @name 清理指定网站的反向代理缓存 + @author wzz <2024/4/25 上午10:51> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + # 校验参数 + try: + get.validate([ + Param('site_name').String(), + + ], [ + public.validate.trim_filter(), + ]) + except Exception as ex: + public.print_log("error info: {}".format(ex)) + return public.return_message(-1, 0, str(ex)) + + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.return_message(-1, 0,"Sitename cannot be empty!") + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + cache_dir = "/www/wwwroot/{site_name}/proxy_cache_dir".format(site_name=get.site_name) + if os.path.exists(cache_dir): + public.ExecShell("rm -rf {cache_dir}/*".format(cache_dir=cache_dir)) + + public.serviceReload() + return public.return_message(0, 0,"Cleanup successful!") + + return public.return_message(-1, 0,"Cleanup failed, cache directory does not exist!") + + # 2024/4/25 下午9:24 设置指定网站的https端口 + def set_https_port(self, get): + ''' + @name 设置指定网站的https端口 + @author wzz <2024/4/25 下午9:24> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.site_name = get.get("site_name", "") + if get.site_name == "": + return public.returnResult(status=False, msg="site_name不能为空!") + + get.https_port = get.get("https_port", "443") + if not public.checkPort(get.https_port) and get.https_port != "443": + return public.returnResult(status=False, msg="https端口【{}】不合法!".format(get.https_port)) + + get.proxy_json_conf = self.read_json_conf(get)['message'] + if not get.proxy_json_conf: + return public.return_message(-1,0,"Reading configuration file failed, please delete the website and add it again!") + + get.proxy_json_conf["https_port"] = get.https_port + + update_result = self.update_conf(get) + if update_result["status"]==-1: + return update_result + + return public.returnResult(msg="设置成功!") + + # 2024/4/23 下午2:12 保存并重新生成新的nginx配置文件 + def update_conf(self, get): + ''' + @name + @author wzz <2024/4/23 下午2:13> + @param "data":{"参数名":""} <数据类型> 参数描述 + @return dict{"status":True/False,"msg":"提示信息"} + ''' + get.conf_file = public.get_setup_path() + '/panel/vhost/nginx/' + get.site_name + '.conf' + self.generate_config(get) + get.data = get.site_conf + get.encoding = "utf-8" + get.path = get.conf_file + + import files + f = files.files() + save_result = f.SaveFileBody(get) + if save_result["status"] == False: + return public.return_message(-1,0,save_result["msg"]) + + self._site_proxy_conf_path = "{path}/{site_name}/{site_name}.json".format( + path=self._proxy_config_path, + site_name=get.site_name + ) + public.writeFile(self._site_proxy_conf_path, json.dumps(get.proxy_json_conf)) + + return public.return_message(0,0,"Save successful!") diff --git a/mod/project/push/__init__.py b/mod/project/push/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/project/push/msgconfMod.py b/mod/project/push/msgconfMod.py new file mode 100644 index 00000000..d5a94e6a --- /dev/null +++ b/mod/project/push/msgconfMod.py @@ -0,0 +1,70 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: baozi +# ------------------------------------------------------------------- +# 新告警通道管理模块 +# ------------------------------ +from mod.base.msg import SenderManager, WeChatAccountMsg, update_mod_push_msg +from mod.base.push_mod import SenderConfig +from mod.base import json_response + +import public + + +update_mod_push_msg() + + +class main(SenderManager): + + @staticmethod + def wx_account_auth(get=None): + return WeChatAccountMsg.get_auth_url() + + @staticmethod + def unbind_wx_account(get): + try: + sender_id = get.sender_id.strip() + except AttributeError: + return json_response(status=False, msg="参数错误") + + conf = SenderConfig().get_by_id(sender_id) + if not conf: + return json_response(status=False, msg="未查询到对应绑定信息") + + res = WeChatAccountMsg.unbind(conf["data"]["id"]) + public.WriteFile(WeChatAccountMsg.need_refresh_file, "") + return res + + def set_default_sender(self, get): + try: + + try: + sender_id = get.sender_id.strip() + sender_type = get.sender_type.strip() + except AttributeError: + return json_response(status=False, msg="参数错误") + + sc = SenderConfig() + print(sc) + change = False + print("SenderConfig",sc.config) + for conf in sc.config: + if conf["sender_type"] == sender_type: + is_original = conf.get("original", False) + if conf["id"] == sender_id: + change = True + conf["original"] = True + else: + conf["original"] = False + + sc.save_config() + if change: + self.set_default_for_compatible(sc.get_by_id(sender_id)) + return json_response(status=True, msg="设置成功") + except Exception as e: + return json_response(status=False, msg=e) + diff --git a/mod/project/push/taskMod.py b/mod/project/push/taskMod.py new file mode 100644 index 00000000..95b0edab --- /dev/null +++ b/mod/project/push/taskMod.py @@ -0,0 +1,140 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: baozi +# ------------------------------------------------------------------- +# 新告警通道管理模块 +# ------------------------------ +import json +import traceback +import os +from mod.base import json_response + +from mod.base.push_mod import PushManager, TaskConfig, TaskRecordConfig, TaskTemplateConfig, PushSystem +from mod.base.push_mod import update_mod_push_system, UPDATE_MOD_PUSH_FILE, load_task_template_by_file, \ + UPDATE_VERSION_FILE +from mod.base.msg import update_mod_push_msg +from mod.base.push_mod.rsync_push import load_rsync_template +from mod.base.push_mod.task_manager_push import load_task_manager_template +from mod.base.push_mod.load_push import load_load_template +from mod.base.push_mod import PUSH_DATA_PATH + +def update_mod(): + + if not os.path.exists(UPDATE_VERSION_FILE): + load_task_template_by_file("/www/server/panel/mod/base/push_mod/site_push_template.json") + load_task_template_by_file("/www/server/panel/mod/base/push_mod/system_push_template.json") + load_task_template_by_file("/www/server/panel/mod/base/push_mod/database_push_template.json") + with open(UPDATE_VERSION_FILE, "w") as f: + f.write("") + + if not os.path.exists(UPDATE_MOD_PUSH_FILE): + update_mod_push_msg() + + load_rsync_template() + load_task_manager_template() + load_load_template() + + update_mod_push_system() + + +update_mod() +del update_mod + + +class main(PushManager): + + def get_task_list(self, get=None): + try: + res = TaskConfig().config + res.sort(key=lambda x: x["create_time"]) + for i in res: + i['view_msg'] = self.get_view_msg_format(i) + return json_response(status=True, data=res) + except: + print(traceback.format_exc()) + + @staticmethod + def get_task_record(get): + page = 1 + size = 10 + try: + if hasattr(get, "page"): + page = int(get.page.strip()) + if hasattr(get, "size"): + size = int(get.size.strip()) + task_id = get.task_id.strip() + except (AttributeError, ValueError, TypeError): + return json_response(status=False, msg="参数错误") + + t = TaskRecordConfig(task_id) + t.config.sort(key=lambda x: x["create_time"]) + page = max(page, 1) + size = max(size, 1) + count = len(t.config) + data = t.config[(page - 1) * size: page * size] + return json_response(status=True, data={ + "count": count, + "list": data, + }) + + def clear_task_record(self, get): + try: + task_id = get.task_id.strip() + except (AttributeError, ValueError, TypeError): + return json_response(status=False, msg="参数错误") + self.clear_task_record_by_task_id(task_id) + + return json_response(status=True, msg="清除成功") + + @staticmethod + def remove_task_records(get): + try: + task_id = get.task_id.strip() + record_ids = set(json.loads(get.record_ids.strip())) + except (AttributeError, ValueError, TypeError): + return json_response(status=False, msg="参数错误") + task_records = TaskRecordConfig(task_id) + for i in range(len(task_records.config) - 1, -1, -1): + if task_records.config[i]["id"] in record_ids: + del task_records.config[i] + + task_records.save_config() + return json_response(status=True, msg="清除成功") + + @staticmethod + def get_task_template_list(get=None): + res = [] + p_sys = PushSystem() + for i in TaskTemplateConfig().config: + if not i['used']: + continue + to = p_sys.get_task_object(i["id"], i["load_cls"]) + if not to: + continue + t = to.filter_template(i["template"]) + if not t: + continue + i["template"] = t + res.append(i) + + return json_response(status=True, data=res) + + @staticmethod + def get_view_msg_format(task: dict) -> str: + from mod.base.push_mod.rsync_push import ViewMsgFormat as Rv + from mod.base.push_mod.site_push import ViewMsgFormat as Sv + from mod.base.push_mod.task_manager_push import ViewMsgFormat as Tv + from mod.base.push_mod.database_push import ViewMsgFormat as Dv + from mod.base.push_mod.system_push import ViewMsgFormat as SSv + from mod.base.push_mod.load_push import ViewMsgFormat as Lv + + list_obj = [Rv(), Sv(), Tv(), Dv(), SSv(), Lv()] + for i in list_obj: + res = i.get_msg(task) + if res is not None: + return res + return '--' diff --git a/mod/test/__init__.py b/mod/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/test/docker/routetestModTest.py b/mod/test/docker/routetestModTest.py new file mode 100644 index 00000000..ada85da8 --- /dev/null +++ b/mod/test/docker/routetestModTest.py @@ -0,0 +1,92 @@ +# coding: utf-8 +# ------------------------------------------------------------------- +# 宝塔Linux面板 +# ------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# ------------------------------------------------------------------- +# Author: wzz +# ------------------------------------------------------------------- +# ------------------------------ +# Docker模型测试模块 - 容器模型 +# ------------------------------ +import sys +import unittest + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, '/www/server/panel') + +from mod.project.docker.routetestMod import main as routetest_main + +routetest = routetest_main() + + +# +class TestContainerModel(unittest.TestCase): + """ + 创建测试用例 + """ + + def test_returnResult(self): + """ + 测试模型测试方法,检测返回结果是否为json格式数据 + @return: + """ + result = routetest.returnResult({'data': {}}) + self.assertIsInstance(result, dict) + self.assertIn('status', result) + self.assertIn('msg', result) + self.assertIn('data', result) + self.assertIn('code', result) + self.assertIn('timestamp', result) + + def test_wsRequest(self): + """ + 使用ws长链请求ws://127.0.0.1:8888/ws_mod + 并发送{"mod_name":"docker","sub_mod_name":"routetest","def_name":"wsRequest","ws_callback":"111"},检测返回结果是否为True + + 备注:请将__init__.py中ws模型路由的comReturn和csrf检查注释掉再测试 + @param get: + {"mod_name":"docker","sub_mod_name":"routetest","def_name":"wsRequest","ws_callback":"111"} + {"mod_name":"模型名称","sub_mod_name":"子模块名称","def_name":"函数名称","ws_callback":"ws必传参数,传111",其他参数接后面} + @return: + """ + import json + import time + from websocket import create_connection + ws = create_connection("ws://127.0.0.1:8888/ws_mod") + print("连接状态:", ws.connected) + + params = {"mod_name": "docker", "sub_mod_name": "routetest", "def_name": "wsRequest", "ws_callback": "111"} + ws.send(json.dumps(params)) + + while True: + result = ws.recv() + print("接收到结果:", result.strip()) + + try: + result_data = json.loads(result) + if "result" in result_data and "callback" in result_data: + if result_data["result"] == True and result_data["callback"] == "111": + print("websocket测试成功!") + break + except Exception as e: + pass + + # 等待一段时间再继续接收消息 + time.sleep(0.1) + + ws.close() + self.assertIn('result', result) + self.assertIn('callback', result) + + +if __name__ == '__main__': + # unittest.main() + # 创建测试套件 + suite = unittest.TestSuite() + suite.addTest(TestContainerModel('test_returnResult')) + suite.addTest(TestContainerModel('test_wsRequest')) + + # 创建测试运行器 + runner = unittest.TextTestRunner() + runner.run(suite) diff --git a/mod/test/java/__init__.py b/mod/test/java/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/test/java/test_spring_config.py b/mod/test/java/test_spring_config.py new file mode 100644 index 00000000..f77a88d8 --- /dev/null +++ b/mod/test/java/test_spring_config.py @@ -0,0 +1,46 @@ +import json +import os +import time +import unittest + +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + +from mod.project.java.springboot_parser import SpringConfigParser, SpringLogConfigParser + + +class TestSpringConfigParser(unittest.TestCase): + + # def test_parser_config(self): + # file = "/www/java_mall/yami-shop-admin-0.0.1-SNAPSHOT.jar" + # # file = "/www/lilishop/lili-shop-error_jar.jar" + # # file = "/www/wwwroot/111_java/demo.jar" + # # file = "/www/wwwroot/33_java/tduck-api.jar" + # # file = "/www/java_mall/yami-shop-api-0.0.1-SNAPSHOT.jar" + # + # scp = SpringConfigParser(file) + # print(scp.get_tip()) + # + # def test_shell_env(self): + # SpringConfigParser.get_env_file_data("/www/wwwroot/33_java/test.sh") + + def test_parser_log_config(self): + # file = "/www/java_mall/yami-shop-admin-0.0.1-SNAPSHOT.jar" + # file = "/www/lilishop/run_api/buyer-api-4.3.jar" + # file = "/www/wwwroot/111_java/demo.jar" + # file = "/www/wwwroot/33_java/tduck-api.jar" + file = "/www/wwwroot/55_MCMS_java/ms-mcms.jar" + # file = "/www/java_mall/yami-shop-api-0.0.1-SNAPSHOT.jar" + + if not os.path.exists(file): + print("文件不存在") + scp = SpringLogConfigParser(file) + print(scp.get_all_log_ptah()) + + +if __name__ == '__main__': + unittest.main() diff --git a/mod/test/java/test_sprintboot_mod.py b/mod/test/java/test_sprintboot_mod.py new file mode 100644 index 00000000..f7653c43 --- /dev/null +++ b/mod/test/java/test_sprintboot_mod.py @@ -0,0 +1,46 @@ +import time +import unittest + +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.project.java.projectMod import main as springboot_main +from mod.base.web_conf.util import GET_CLASS +from mod.project.java import utils + + +class TestSpringBootConfig(unittest.TestCase): + + def setUp(self) -> None: + self.springboot = springboot_main() + + # def test_process_for_create(self): + # get = GET_CLASS() + # get.is_java_process = '' + # get.search = 'ja' + # data = self.springboot.process_for_create(get) + # print(data) + # + # def test_process_info_for_create(self): + # get = GET_CLASS() + # get.pid = "15701" + # data = self.springboot.process_info_for_create(get) + # print(data) + + # def test_create_port(self): + # t = time.time() + # print(t) + # print(utils.create_a_not_used_port()) + # print(time.time() - t) + + def test_get_jar_war_config(self): + file = "/www/wwwroot/33_java/tduck-api.jar" + data = utils.get_jar_war_config(file) + if data: + data = utils.to_utf8(data) + print(data) + + +if __name__ == '__main__': + unittest.main() diff --git a/mod/test/java/test_util.py b/mod/test/java/test_util.py new file mode 100644 index 00000000..85ad5163 --- /dev/null +++ b/mod/test/java/test_util.py @@ -0,0 +1,126 @@ +import unittest +import tempfile +import os +import zipfile +import sys +from unittest.mock import patch, MagicMock +if "/www/server/panel" not in sys.path: + sys.path.append("/www/server/panel") + +from mod.project.java.utils import get_jar_war_config, to_utf8, parse_application_yaml, TomCat + + +class TestJarWarConfig(unittest.TestCase): + def setUp(self): + # 创建测试用的zip文件 + self.test_jar = tempfile.NamedTemporaryFile(delete=False) + self.test_jar_name = self.test_jar.name + self.test_jar.close() + + # 创建一个包含application.yaml的zip文件 + with zipfile.ZipFile(self.test_jar_name, 'w') as jar: + jar.writestr('application.yaml', 'spring:\n datasource:\n url: jdbc:mysql://localhost:3306/testdb') + + # 创建一个不包含application.yaml的zip文件 + self.test_jar_no_config = tempfile.NamedTemporaryFile(delete=False) + self.test_jar_no_config_name = self.test_jar_no_config.name + self.test_jar_no_config.close() + + with zipfile.ZipFile(self.test_jar_no_config_name, 'w') as jar: + jar.writestr('README.txt', 'This is a test JAR without configuration') + + # 创建一个非zip文件 + self.test_non_zip = tempfile.NamedTemporaryFile(delete=False) + self.test_non_zip_name = self.test_non_zip.name + self.test_non_zip.close() + + # 创建一个不存在的文件路径 + self.test_non_existent = 'non_existent.jar' + + def tearDown(self): + # 删除测试文件 + os.unlink(self.test_jar_name) + os.unlink(self.test_jar_no_config_name) + os.unlink(self.test_non_zip_name) + + def test_get_jar_war_config_with_valid_jar(self): + # 测试有效的JAR文件 + result = get_jar_war_config(self.test_jar_name) + self.assertIsNotNone(result) + self.assertEqual(len(result), 1) + self.assertIn('application.yaml', result[0][0]) + + def test_get_jar_war_config_with_no_config(self): + # 测试没有配置文件的JAR + result = get_jar_war_config(self.test_jar_no_config_name) + self.assertIsNone(result) + + def test_get_jar_war_config_with_non_zip(self): + # 测试非ZIP文件 + result = get_jar_war_config(self.test_non_zip_name) + self.assertIsNone(result) + + def test_get_jar_war_config_with_non_existent(self): + # 测试不存在的文件 + result = get_jar_war_config(self.test_non_existent) + self.assertIsNone(result) + + def test_to_utf8(self): + # 测试转换为UTF-8 + byte_data = 'test'.encode('utf-8') + file_data_list = [('test.txt', byte_data)] + result = to_utf8(file_data_list) + self.assertIsNotNone(result) + self.assertEqual(result, [('test.txt', 'test')]) + + def test_parse_application_yaml_valid(self): + # 测试有效的YAML配置 + byte_data = 'spring:\n datasource:\n url: jdbc:mysql://localhost:3306/testdb' + file_data_list = [('application.yaml', byte_data.encode('utf-8'))] + result = parse_application_yaml(file_data_list) + self.assertIsNotNone(result) + self.assertEqual(len(result), 1) + self.assertIn('application.yaml', result[0][0]) + self.assertIsInstance(result[0][1], dict) + self.assertEqual(result[0][1]['spring']['datasource']['url'], 'jdbc:mysql://localhost:3306/testdb') + + def test_parse_application_yaml_invalid(self): + # 测试无效的YAML配置 + invalid_byte_data = 'not --- a valid yaml' + file_data_list = [('application.yaml', invalid_byte_data.encode('utf-8'))] + result = parse_application_yaml(file_data_list) + self.assertIsNotNone(result) + self.assertEqual(len(result), 0) + + +class TestTomCat(unittest.TestCase): + def setUp(self): + self.tomcat_path = "/usr/local/bttomcat/tomcat10" + self.tomcat = TomCat(self.tomcat_path) + + def test_jdk_path(self): + self.assertEqual(self.tomcat.jdk_path, "/usr/local/btjdk/jdk8") + + def test_config_xml(self): + self.assertIsNotNone(self.tomcat.config_xml) + print(self.tomcat.config_xml) + + def test_save_config_xml(self): + self.tomcat.add_host("taaaaa", "/tmp/aaaa") + self.tomcat.add_host("taaaaa", "/tmp/aaaa") + self.tomcat.add_host("tbbb", "/tmp/tbbb") + self.assertEqual(self.tomcat.save_config_xml(), True) + with open(self.tomcat_path + "/conf/server.xml", "r") as f: + print(f.read()) + + self.tomcat.remove_host("tbbb") + self.assertEqual(self.tomcat.save_config_xml(), True) + with open(self.tomcat_path + "/conf/server.xml", "r") as f: + print(f.read()) + + def test_status(self): + print(self.tomcat.status()) + + +if __name__ == '__main__': + unittest.main() diff --git a/mod/test/php/__init__.py b/mod/test/php/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/test/php/test_php_asyncMod.py b/mod/test/php/test_php_asyncMod.py new file mode 100644 index 00000000..827674b5 --- /dev/null +++ b/mod/test/php/test_php_asyncMod.py @@ -0,0 +1,273 @@ +import unittest +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") +from mod.base import RealProcess + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public + +from mod.project.php.php_asyncMod import main as php_async +phpasync = php_async() + +class Testmain(unittest.TestCase): + def test_create_project(self): + args = { + 'webname':{"domain":"test.c","domainlist":[]}, + 'php_version':'74', + 'site_path':'/xiaopacai/swoole-webim-demo-master', + 'project_cmd':'php server/hsw_server.php start', + 'install_dependence':'1', + 'sql':'', + 'sql_name':'', + 'sql_user': '', + 'sql_pwd': '', + 'sql_codeing': '', + 'project_ps': '', + 'open_proxy': '', + 'project_proxy_path': '', + 'project_port': '', + } + # phpasync.create_project(public.to_dict_obj(args)) + + + def test_delete_site(self): + args = { + 'webname':"test.c", + 'id':'1', + } + # phpasync.delete_site(public.to_dict_obj(args)) + + def test_get_project_list(self): + res = phpasync.get_project_list(public.to_dict_obj({})) + self.assertEqual(type(res), dict) + + def test_project_get_domain(self): + args = { + 'sitename':'sdadaw.c' + } + res = phpasync.project_get_domain(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + + def test_project_remove_domain(self): + self.fail() + + def test_project_add_domain(self): + args = { + 'sitename':'sdadaw.c', + 'domain':["daw.daw","ssff.cxs"] + } + res = phpasync.project_add_domain(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + def test_get_project_run_state(self): + args = { + 'sitename':'sdadaw.c', + } + res = phpasync.get_project_run_state(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + def test_modify_project_run_state(self): + args = { + 'sitename':'sdadaw.c', + 'status':'stop' + } + res = phpasync.modify_project_run_state(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + def test_async_dependence_config(self): + self.fail() + + def test_modify_project(self): + args = { + 'sitename':'sdadaw.c', + 'php_version':'74', + 'project_path':'/xiaopacai/swoole-webim-demo-master', + 'project_cmd':'php server/hsw_server.php start', + 'site_run_path':'/xiaopacai/swoole-webim-demo-master', + 'project_port': '', + } + res = phpasync.modify_project(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + def test_get_project_log(self): + args = { + 'sitename':'sdadaw.c', + } + res = phpasync.get_project_log(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + def test_get_config_file(self): + args = { + 'sitename':'sdadaw.c', + } + res = phpasync.get_config_file(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + def test_upload_version(self): + self.fail() + + def test_get_version_list(self): + args = { + 'sitename':'sdadaw.c', + } + res = phpasync.get_version_list(public.to_dict_obj(args)) + + def test_remove_version(self): + args = { + 'sitename':'sdadaw.c', + 'version':'1' + } + res = phpasync.remove_version(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + def test_recover_version(self): + args = { + 'sitename':'sdadaw.c', + 'version':'1' + } + res = phpasync.recover_version(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_now_file_backup(self): + args = { + 'sitename':'sdadaw.c', + 'version':'2' + } + res = phpasync.now_file_backup(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_set_version_ps(self): + args = { + 'sitename':'sdadaw.c', + 'version':'2', + 'ps':'test' + } + res = phpasync.set_version_ps(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_get_setup_log(self): + args = { + 'sitename':'sdadaw.c', + } + res = phpasync.get_setup_log(public.to_dict_obj(args)) + self.assertEqual(type(res), dict) + + def test_add_crontab(self): + pass + + def test_get_crontab_list(self): + args = { + 'sitename':'sdadaw.c', + } + res = phpasync.get_crontab_list(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_start_task(self): + args = { + 'id':'19', + } + res = phpasync.start_task(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_modify_crontab_status(self): + args = { + 'id':'19', + } + res = phpasync.modify_crontab_status(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_remove_crontab(self): + args = { + 'id':'19', + } + res = phpasync.remove_crontab(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_modify_crontab(self): + pass + + def test_get_crontab_log(self): + args = { + 'id':'19', + } + res = phpasync.get_crontab_log(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_clearn_logs(self): + args = { + 'id':'19', + } + res = phpasync.clearn_logs(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_get_group_list(self): + res = phpasync.get_group_list(public.to_dict_obj('{}')) + self.assertEqual(res['code'], 1) + + def test_create_group(self): + args = { + 'group_name':'test', + } + res = phpasync.create_group(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_remove_group(self): + args = { + 'group_name':'test', + } + res = phpasync.remove_group(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_group_add_project(self): + args = { + 'group_name':'test', + 'project_name':'sdadaw.c' + } + res = phpasync.group_add_project(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_group_remove_project(self): + args = { + 'group_name':'test', + 'project_name':'sdadaw.c' + } + res = phpasync.group_remove_project(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_set_group_interval(self): + args = { + 'group_name':'test', + 'interval':'15' + } + res = phpasync.set_group_interval(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_set_group_status(self): + args = { + 'group_name':'test', + 'status':'1' + } + res = phpasync.set_group_status(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_get_proxy_file(self): + args = { + 'sitename':'sdadaw.c', + 'proxyname':'test' + } + res = phpasync.get_proxy_file(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + + def test_save_proxy_file(self): + args = { + 'sitename':'sdadaw.c', + 'proxyname':'test', + 'file':'test' + } + res = phpasync.save_proxy_file(public.to_dict_obj(args)) + self.assertEqual(res['code'], 1) + +if __name__ == '__main__': + unittest.main() diff --git a/mod/test/process/__init__.py b/mod/test/process/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mod/test/process/test.py b/mod/test/process/test.py new file mode 100644 index 00000000..48f40fca --- /dev/null +++ b/mod/test/process/test.py @@ -0,0 +1,4 @@ +import time +while True: + print("Hello, World!") + time.sleep(1) \ No newline at end of file diff --git a/mod/test/process/test_process.py b/mod/test/process/test_process.py new file mode 100644 index 00000000..e9856c94 --- /dev/null +++ b/mod/test/process/test_process.py @@ -0,0 +1,105 @@ +import unittest +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") +from mod.base import RealProcess + +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +import public + +real_process = RealProcess() + + +class TestRealProcess(unittest.TestCase): + + def test_get_process_list(self): + res = real_process.get_process_list() + self.assertEqual(res['code'], 1) + + def test_get_process_info_by_pid(self): + res = real_process.get_process_info_by_pid(1) + self.assertEqual(res['code'], 1) + + def test_get_process_info_by_name(self): + res = real_process.get_process_info_by_name('system') + self.assertEqual(res['code'], 1) + + def test_get_process_info_by_exec(self): + res = real_process.get_process_info_by_exec('/usr/sbin/sshd') + self.assertEqual(res['code'], 1) + + def test_get_process_info_by_port(self): + res = real_process.get_process_info_by_port(22) + self.assertEqual(res['code'], 1) + + def test_get_process_info_by_ip(self): + res = real_process.get_process_info_by_ip('192.168.168.66') + self.assertEqual(res['code'], 1) + + def test_get_process_info_by_openfile(self): + res = real_process.get_process_info_by_openfile('/etc/passwd') + self.assertEqual(res['code'], 1) + + def test_get_process_ps(self): + res = real_process.get_process_ps('grep') + self.assertEqual(res['code'], 1) + + def test_get_process_tree(self): + res = real_process.get_process_tree(1) + self.assertEqual(res['code'], 1) + + # def test_kill_pid(self): + # try: + # pid = real_process.get_process_info_by_exec('/www/server/panel/mod/test/process/test.py')['data'][0]['pid'] + # res = real_process.kill_pid(pid) + # self.assertEqual(res['code'], 1) + # except: + # pass + + # def test_kill_name(self): + # try: + # name = real_process.get_process_info_by_exec('/www/server/panel/mod/test/process/test.py')['data'][0]['name'] + # res = real_process.kill_name(name) + # self.assertEqual(res['code'], 1) + # except: + # pass + # + # def test_kill_tree(self): + # try: + # pid = real_process.get_process_info_by_exec('/www/server/panel/mod/test/process/test.py')['data'][0]['pid'] + # res = real_process.kill_tree(pid) + # self.assertEqual(res['code'], 1) + # except: + # pass + # + # def test_kill_proc_all(self): + # pid = real_process.get_process_info_by_exec('/www/server/panel/mod/test/process/test.py')['data'][0]['pid'] + # res = real_process.kill_proc_all(pid) + # self.assertEqual(res['code'], 1) + # + # def test_kill_port(self): + # res = real_process.kill_port(22) + # self.assertEqual(res['code'], 1) + + def test_add_black_ip(self): + res = real_process.add_black_ip(['1.2.3.4']) + self.assertEqual(res['code'], 1) + + def test_del_black_ip(self): + res = real_process.del_black_ip(['1.2.3.4']) + self.assertEqual(res['code'], 1) + + def test_firewall_reload(self): + res = real_process.firewall_reload() + self.assertEqual(res['code'], 1) + + def test_get_run_list(self): + res = real_process.get_run_list() + self.assertEqual(res['code'], 1) + + + +if __name__ == '__main__': + unittest.main() diff --git a/mod/test/process/test_server.py b/mod/test/process/test_server.py new file mode 100644 index 00000000..76eb47ca --- /dev/null +++ b/mod/test/process/test_server.py @@ -0,0 +1,69 @@ +import unittest +import sys +import time + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +from mod.base import RealServer + +realserver = RealServer() + + +class TestRealServer(unittest.TestCase): + def test_server_admin(self): + res = realserver.server_admin('httpd', 'stop') + self.assertEqual(res['code'], 1) + res = realserver.server_admin('httpd', 'start') + self.assertEqual(res['code'], 1) + + def test_server_status(self): + res = realserver.server_status('httpd') + self.assertEqual(res['code'], 1) + + def test_add_boot(self): + server_name = 'swwnb' + pidfile = '/tmp/test.pl' + start_exec = 'btpython /root/1.py' + stop_exec = 'kill /time/111.log', int(time.time()) + 3000) + self.assertEqual(res['code'], 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/mod/test/process/test_user.py b/mod/test/process/test_user.py new file mode 100644 index 00000000..f3818551 --- /dev/null +++ b/mod/test/process/test_user.py @@ -0,0 +1,149 @@ +import unittest +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") +if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") +from mod.base import RealUser + +realuser = RealUser() + + +class TestRealUser(unittest.TestCase): + def setUp(self): + realuser.add_user('test1', 'test1', 'test1') + realuser.add_group('test') + + def tearDown(self): + realuser.remove_user('test1') + realuser.remove_user('test') + realuser.remove_group('test1') + realuser.remove_group('test') + + def test_get_user_list(self): + print('get_user_list:') + res = realuser.get_user_list() + print(res) + self.assertEqual(res['code'], 1) + + def test_get_group_list(self): + print('get_group_list:') + res = realuser.get_group_list() + print(res) + self.assertEqual(res['code'], 1) + + def test_add_user(self): + print('add_user:') + res = realuser.add_user('test', 'test', 'test') + print(res) + self.assertEqual(res['code'], 1) + + def test_remove_user(self): + print('remove_user:') + res = realuser.remove_user('test1') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_pwd(self): + print('edit_user_pwd:') + res = realuser.edit_user_pwd('test1', 'test111') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_group(self): + print('edit_user_group:') + res = realuser.edit_user_group('test1', 'test') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_ps(self): + print('edit_user_ps:') + res = realuser.edit_user_ps('test1', 'test') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_status(self): + print('edit_user_status:') + res = realuser.edit_user_status('test1', '0') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_login_shell(self): + print('edit_user_login_shell:') + res = realuser.edit_user_login_shell('test1', '/bin/bash') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_home(self): + print('edit_user_home:') + res = realuser.edit_user_home('test1', '/home/test1') + print(res) + self.assertEqual(res['code'], 1) + + def test_get_user_info(self): + print('get_user_info:') + res = realuser.get_user_info('test1') + print(res) + self.assertEqual(res['code'], 1) + + def test_add_group(self): + print('add_group:') + res = realuser.add_group('test2') + print(res) + realuser.remove_group('test2') + self.assertEqual(res['code'], 1) + + def test_remove_group(self): + realuser.add_group('test') + print('remove_group:') + res = realuser.remove_group('test') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_group_name(self): + print('edit_group_name:') + res = realuser.edit_group_name('test', 'test5') + print(res) + print(realuser.remove_group('test5')) + self.assertEqual(res['code'], 1) + + def test_get_group_info(self): + print('get_group_info:') + res = realuser.get_group_info('test1') + print(res) + self.assertEqual(res['code'], 1) + + def test_get_group_user(self): + print('get_group_user:') + res = realuser.get_group_user('test1') + print(res) + self.assertEqual(res['code'], 1) + + def test_get_user_group(self): + print('get_user_group:') + res = realuser.get_user_group('test1') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_permission(self): + print('edit_user_permission:') + res = realuser.edit_user_permission('test1', '777') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_group_permission(self): + print('edit_group_permission:') + res = realuser.edit_group_permission('test1', '777') + print(res) + self.assertEqual(res['code'], 1) + + def test_edit_user_name(self): + print('edit_user_name:') + res = realuser.edit_user_name('test1', 'test') + print(res) + self.assertEqual(res['code'], 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/mod/test/test_backup_tool.py b/mod/test/test_backup_tool.py new file mode 100644 index 00000000..7760039e --- /dev/null +++ b/mod/test/test_backup_tool.py @@ -0,0 +1,35 @@ +import os +import sys +import time +from unittest import TestCase + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + + +from mod.base.backup_tool import BackupTool, DB + + +class TestBackupTool(TestCase): + + def test_backup(self): + src = "/www/wwwroot/aaa.test.com" + sub_dir = "site/aaa.test.com" + + site_info = DB("sites").where("name= ?", ("aaa.test.com", )).find() + print(site_info) + print(BackupTool().backup(src, sub_dir=sub_dir, sync=False, site_info=site_info)) + time.sleep(2) # 等待执行完成 + # print(BackupTool().backup(src, sub_dir=sub_dir, sync=True, site_info=site_info)) + + print(os.listdir(BackupTool().backup_path + "/" + sub_dir)) + + def runTest(self): + self.test_backup() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestBackupTool()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_database_tool.py b/mod/test/test_database_tool.py new file mode 100644 index 00000000..435070f5 --- /dev/null +++ b/mod/test/test_database_tool.py @@ -0,0 +1,54 @@ +import os +import sys +from unittest import TestCase + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + + +from mod.base.database_tool import add_database + + +class TestDataBaseTool(TestCase): + + def test_create_data_base(self): + mysql_data = { + "server_id": 0, + "database_name": "aaa", + "db_user": "eee", + "password": "ffff", + "dataAccess": "ip", + "address": "127.0.0.1", + "codeing": "utf8mb4", + "ps": "", + "listen_ip": "0.0.0.0/0", + "host": "", + } + print(add_database(db_type="mysql", data=mysql_data)) + + pgsql_data = { + "server_id": 0, + "database_name": "aaa", + "db_user": "eee", + "password": "ffff", + "ps": "", + "listen_ip": "0.0.0.0/0", + } + print(add_database(db_type="pgsql", data=pgsql_data)) + + mgo_data = { + "server_id": 0, + "database_name": "aaa", + "ps": "", + } + print(add_database(db_type="mongodb", data=mgo_data)) + + def runTest(self): + self.test_create_data_base() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestDataBaseTool()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_git/test_git_tool.py b/mod/test/test_git/test_git_tool.py new file mode 100644 index 00000000..de9e8ce3 --- /dev/null +++ b/mod/test/test_git/test_git_tool.py @@ -0,0 +1,59 @@ +import json +import os +import sys +import time +from unittest import TestCase + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.base.git_tool import GitTool, GitMager +from mod.base.web_conf.util import GET_CLASS + + +class TestGitTool(TestCase): + def runTest(self): + # g = GitTool( + # project_path="/www/test/git_test", + # git_url="http://git.bt.cn/baozi/bt_sync.git", + # user_config={ + # "name": "baozi", + # "password": "swt258452.", + # "email": "1191604998@qq.com", + # } + # ) + # g.pull("master") + os.remove("/www/server/panel/data/site_git_config.json") + + get = GET_CLASS() + get.git_path = "/www/test/git_test" + get.site_name = "git_test" + get.url = "http://git.bt.cn/baozi/bt_sync.git" + get.config = json.dumps({ + "name": "baozi", + "password": "swt258452.", + "email": "1191604998@qq.com", + }) + + gm = GitMager() + print(gm.add_git(get)) + + get = GET_CLASS() + get.site_name = "git_test" + get.refresh = "1" + print(gm.site_git_configure(get)) + + # get = GET_CLASS() + # get.git_path = "/www/test/git_test1" + # get.site_name = "git_test" + # get.git_id = "git_test" + # print(gm.modify_git(get)) + # + # gm.git_pull("master") + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestGitTool()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_version_tool.py b/mod/test/test_version_tool.py new file mode 100644 index 00000000..d1d2f1b7 --- /dev/null +++ b/mod/test/test_version_tool.py @@ -0,0 +1,33 @@ +import os +import shutil +import sys +import time +from unittest import TestCase + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + + +from mod.base.backup_tool import VersionTool + + +class TestVersionTool(TestCase): + + def test_backup(self): + src = "/www/wwwroot/aaa.test.com" + v = VersionTool() + print(v.publish("aaa", src, "1.0.0", sync=False)) + print(v.version_list("aaa")) + time.sleep(2) # 等待执行完成 + # print(BackupTool().backup(src, sub_dir=sub_dir, sync=True, site_info=site_info)) + v.recover('aaa', '1.0.0', src) + + def runTest(self): + self.test_backup() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestVersionTool()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/__init__.py b/mod/test/test_web_conf/__init__.py new file mode 100644 index 00000000..e544f9a6 --- /dev/null +++ b/mod/test/test_web_conf/__init__.py @@ -0,0 +1,190 @@ +import os +import sys +from unittest import TestCase + +SITE_NAME_CASE = "aaa.test.com" +SITE_PATH = "/www/wwwroot/aaa.test.com" +SUB_SITE_PATH = SITE_PATH + "/test_run" +if not os.path.exists(SUB_SITE_PATH): + os.makedirs(SUB_SITE_PATH) +VHOST_PATH = "/www/server/panel/vhost" +PREFIX = "" +NGINX_CONFIG_FILE = "{}/nginx/{}{}.conf".format(VHOST_PATH, PREFIX, SITE_NAME_CASE) +APACHE_CONFIG_FILE = "{}/apache/{}{}.conf".format(VHOST_PATH, PREFIX, SITE_NAME_CASE) +NGINX_CONFIG_CASE = r"""server +{ + listen 80; + server_name aaa.test.com; + index index.php index.html index.htm default.php default.html default.htm; + root /www/wwwroot/aaa.test.com; + #CERT-APPLY-CHECK--START + # 用于SSL证书申请时的文件验证相关配置 -- 请勿删除 + include /www/server/panel/vhost/nginx/well-known/aaa.test.com.conf; + #CERT-APPLY-CHECK--END + + #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则 + #error_page 404/404.html; + #SSL-END + + #ERROR-PAGE-START 错误页配置,可以注释、删除或修改 + #error_page 404 /404.html; + #error_page 502 /502.html; + #ERROR-PAGE-END + + #PHP-INFO-START PHP引用配置,可以注释或修改 + include enable-php-00.conf; + #PHP-INFO-END + + #REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效 + include /www/server/panel/vhost/rewrite/aaa.test.com.conf; + #REWRITE-END + + #禁止访问的文件或目录 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + { + return 404; + } + + #一键申请SSL证书验证目录相关设置 + location ~ \.well-known{ + allow all; + } + + #禁止在证书验证目录放入敏感文件 + if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { + return 403; + } + + location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + error_log /dev/null; + access_log /dev/null; + } + + location ~ .*\.(js|css)?$ + { + expires 12h; + error_log /dev/null; + access_log /dev/null; + } + access_log /www/wwwlogs/aaa.test.com.log; + error_log /www/wwwlogs/aaa.test.com.error.log; +}""" + +# access_log /www/wwwlogs/aaa.test.com.log; + +APACHE_CONFIG_CASE = r""" + ServerAdmin webmaster@example.com + DocumentRoot "/www/wwwroot/aaa.test.com" + ServerName 630e5c70.aaa.test.com + ServerAlias aaa.test.com + #errorDocument 404 /404.html + ErrorLog "/www/wwwlogs/aaa.test.com-error_log" + CustomLog "/www/wwwlogs/aaa.test.com-access_log" combined + + #DENY FILES + + Order allow,deny + Deny from all + + + #PHP + + SetHandler "proxy:unix:/tmp/php-cgi-00.sock|fcgi://localhost" + + + #PATH + + SetOutputFilter DEFLATE + Options FollowSymLinks + AllowOverride All + Require all granted + DirectoryIndex index.php index.html index.htm default.php default.html default.htm + +""" + +APACHE_PATH = "/www/server/apache" +NGINX_PATH = "/www/server/nginx" + + +class WebBaseTestcase(TestCase): + + def __init__(self): + super(WebBaseTestcase, self).__init__() + if not os.path.isfile(NGINX_CONFIG_FILE): + with open(NGINX_CONFIG_FILE, "w+") as f: + f.write(NGINX_CONFIG_CASE) + + if not os.path.isfile(APACHE_CONFIG_FILE): + with open(APACHE_CONFIG_FILE, "w+") as f: + f.write(APACHE_CONFIG_CASE) + + self.site_name = SITE_NAME_CASE + + @staticmethod + def reset_site_config(): + with open(NGINX_CONFIG_FILE, "w+") as f: + f.write(NGINX_CONFIG_CASE) + + with open(APACHE_CONFIG_FILE, "w+") as f: + f.write(APACHE_CONFIG_CASE) + + @staticmethod + def change_env_to_apache(): + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + import public + + if os.path.exists(NGINX_PATH): + public.ExecShell("/etc/init.d/nginx stop") + os.rename(NGINX_PATH, NGINX_PATH+"_back") + + if os.path.exists(APACHE_PATH + "_back"): + os.rename(APACHE_PATH + "_back", APACHE_PATH) + + @staticmethod + def change_env_to_nginx(): + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + import public + + if os.path.exists(APACHE_PATH): + public.ExecShell("/etc/init.d/httpd stop") + os.rename(APACHE_PATH, APACHE_PATH + "_back") + + if os.path.exists(NGINX_PATH + "_back"): + os.rename(NGINX_PATH + "_back", NGINX_PATH) + + def check_web_server_config(self): + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + + import public + + # nginx + ng_error = "" + if os.path.exists(NGINX_PATH): + shell_str = "ulimit -n 8192; {np}/sbin/nginx -t -c {np}/conf/nginx.conf".format(np=NGINX_PATH) + ng_result = public.ExecShell(shell_str) + if ng_result[1].find("successful") == -1: + ng_error = ng_result[1] + + # apache + ap_error = "" + if os.path.exists(APACHE_PATH): + shell_str = "ulimit -n 8192; {ap}/bin/apachectl -t".format(ap=APACHE_PATH) + ap_result = public.ExecShell(shell_str) + if ap_result[1].find("Syntax OK") == -1: + ap_error = ap_result[1] + + if ng_error: + print(ng_error) + + if ap_error: + print(ap_error) + + if ng_error or ap_error: + self.fail("Failed to execute") diff --git a/mod/test/test_web_conf/test_access_restriction.py b/mod/test/test_web_conf/test_access_restriction.py new file mode 100644 index 00000000..23db8292 --- /dev/null +++ b/mod/test/test_web_conf/test_access_restriction.py @@ -0,0 +1,119 @@ +import os.path +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX +from mod.base.web_conf import AccessRestriction +from mod.base.web_conf.util import GET_CLASS + + +class TestAccessRestriction(WebBaseTestcase): + as_obj = AccessRestriction(PREFIX) + + def test_create_auth_dir(self): + get = GET_CLASS() + get.site_name = self.site_name + get.name = "fshfd" + get.dir_path = "/" + get.password = "ssss" + get.username = "aaaa" + res = self.as_obj.create_auth_dir(get) + self.assertTrue(res["status"], res["msg"]) + + def test_modify_auth_dir(self): + get = GET_CLASS() + get.site_name = self.site_name + get.name = "fshfd" + get.dir_path = "/" + get.password = "ssss" + get.username = "aarrr" + res = self.as_obj.modify_auth_dir(get) + self.assertTrue(res["status"], res["msg"]) + + def test_remove_auth_dir(self): + get = GET_CLASS() + get.site_name = self.site_name + get.name = "fshfd" + res = self.as_obj.remove_auth_dir(get) + self.assertTrue(res["status"], res["msg"]) + + def test_create_file_deny(self): + get = GET_CLASS() + get.site_name = self.site_name + get.name = "fshfd" + get.dir_path = "/" + get.suffix = "[\"txt\"]" + res = self.as_obj.create_file_deny(get) + self.assertTrue(res["status"], res["msg"]) + + def test_modify_file_deny(self): + get = GET_CLASS() + get.site_name = self.site_name + get.name = "fshfd" + get.dir_path = "/" + get.suffix = "[\"ffff\"]" + res = self.as_obj.modify_file_deny(get) + self.assertTrue(res["status"], res["msg"]) + + def test_remove_file_deny(self): + get = GET_CLASS() + get.site_name = self.site_name + get.name = "fshfd" + res = self.as_obj.remove_file_deny(get) + self.assertTrue(res["status"], res["msg"]) + + def test_site_access_restriction_info(self): + get = GET_CLASS() + get.site_name = self.site_name + res = self.as_obj.site_access_restriction_info(get) + self.assertTrue(res["status"], res["msg"]) + print(res["data"]) + + def setUp(self) -> None: + if os.path.exists("/www/server/panel/data/site_access.json"): + os.remove("/www/server/panel/data/site_access.json") + self.reset_site_config() + + def runTest(self): + # self.change_env_to_apache() + + self.change_env_to_nginx() + self.test_create_auth_dir() + self.check_web_server_config() + self.test_create_file_deny() + self.check_web_server_config() + self.test_modify_file_deny() + self.check_web_server_config() + self.test_modify_auth_dir() + self.check_web_server_config() + self.test_site_access_restriction_info() + self.test_remove_auth_dir() + self.test_remove_file_deny() + self.check_web_server_config() + + self.change_env_to_apache() + self.test_create_auth_dir() + self.check_web_server_config() + self.test_create_file_deny() + self.check_web_server_config() + self.test_modify_file_deny() + self.check_web_server_config() + self.test_modify_auth_dir() + self.check_web_server_config() + self.test_site_access_restriction_info() + self.test_remove_auth_dir() + self.test_remove_file_deny() + self.check_web_server_config() + + def tearDown(self): + if os.path.exists("/www/server/panel/data/site_access.json"): + os.remove("/www/server/panel/data/site_access.json") + self.reset_site_config() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestAccessRestriction()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_config_mgr.py b/mod/test/test_web_conf/test_config_mgr.py new file mode 100644 index 00000000..0f5cd05a --- /dev/null +++ b/mod/test/test_web_conf/test_config_mgr.py @@ -0,0 +1,68 @@ + +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, NGINX_CONFIG_CASE, APACHE_CONFIG_CASE +from mod.base.web_conf import ConfigMgr + + +class TestConfigMgr(WebBaseTestcase): + + def test_nginx_config(self): + self.assertEqual(self.config_mgr.nginx_config(), NGINX_CONFIG_CASE, "nginx 配置文件读取错误") + + def test_apache_config(self): + self.assertEqual(self.config_mgr.apache_config(), APACHE_CONFIG_CASE, "apache 配置文件读取错误") + + def test_save_nginx_config(self): + self.config_mgr.save_nginx_config(NGINX_CONFIG_CASE + "\n\n") + self.assertEqual(self.config_mgr.nginx_config(), NGINX_CONFIG_CASE + "\n\n", "nginx 配置文件保存错误") + + self.assertIsInstance(self.config_mgr.save_nginx_config("hshdgajdgg"), str, "nginx 配置文件保存错误") + + def test_save_apache_config(self): + self.config_mgr.save_apache_config(APACHE_CONFIG_CASE + "\n\n") + self.assertEqual(self.config_mgr.apache_config(), APACHE_CONFIG_CASE + "\n\n", "apache 配置文件保存错误") + + self.assertIsInstance(self.config_mgr.save_apache_config("hshdgajdgg"), str, "apache 配置文件保存错误") + + def test_history_list(self): + print(self.config_mgr.history_list()) + + def test_history_conf(self): + res = self.config_mgr.history_list() + if len(res["nginx"]) > 0: + print(self.config_mgr.history_conf(res["nginx"][0])) + + if len(res["apache"]) > 0: + print(self.config_mgr.history_conf(res["apache"][0])) + + def setUp(self) -> None: + self.reset_site_config() + self.config_mgr = ConfigMgr(self.site_name, PREFIX) + + def runTest(self): + self.change_env_to_nginx() + self.test_nginx_config() + self.test_save_nginx_config() + self.test_history_list() + self.test_history_conf() + self.check_web_server_config() + + self.change_env_to_apache() + self.test_apache_config() + self.test_save_apache_config() + self.test_history_list() + self.test_history_conf() + self.check_web_server_config() + + def tearDown(self): + self.reset_site_config() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestConfigMgr()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_default_site.py b/mod/test/test_web_conf/test_default_site.py new file mode 100644 index 00000000..bec090c1 --- /dev/null +++ b/mod/test/test_web_conf/test_default_site.py @@ -0,0 +1,66 @@ +import os +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX +from mod.base.web_conf import get_default_site, set_default_site, check_default + + +class TestDefaultSite(WebBaseTestcase): + def test_check_default(self): + vhost_path = "/www/server/panel/vhost" + nginx = vhost_path + '/nginx' + httpd = vhost_path + '/apache' + check_default() + self.assertTrue(os.path.exists(httpd + '/0.default.conf')) + self.assertTrue(os.path.exists(nginx + '/0.default.conf')) + + def test_get_default_site(self): + self.assertEqual(get_default_site(), (self.site_name, PREFIX), "设置默认站点错误") + + def test_set_default_site(self): + set_default_site(site_name=self.site_name, prefix=PREFIX, domain=self.site_name) + + def setUp(self) -> None: + self.reset_site_config() + vhost_path = "/www/server/panel/vhost" + nginx = vhost_path + '/nginx' + httpd = vhost_path + '/apache' + + if os.path.exists(httpd + '/0.default.conf'): + os.remove(httpd + '/0.default.conf') + if os.path.exists(httpd + '/default.conf'): + os.remove(httpd + '/default.conf') + + if os.path.exists(nginx + '/0.default.conf'): + os.remove(nginx + '/0.default.conf') + if os.path.exists(nginx + '/default.conf'): + os.remove(nginx + '/default.conf') + + panel_path = "/www/server/panel" + new_ds_file = panel_path + "/data/mod_default_site.pl" + if os.path.exists(new_ds_file): + os.remove(new_ds_file) + + def runTest(self): + self.test_check_default() + self.test_set_default_site() + self.test_get_default_site() + + self.change_env_to_nginx() + self.check_web_server_config() + + self.change_env_to_apache() + self.check_web_server_config() + + def tearDown(self): + self.reset_site_config() + set_default_site(None) + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestDefaultSite()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_dir_tool.py b/mod/test/test_web_conf/test_dir_tool.py new file mode 100644 index 00000000..8c5274ef --- /dev/null +++ b/mod/test/test_web_conf/test_dir_tool.py @@ -0,0 +1,95 @@ +import os +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, SITE_PATH, SUB_SITE_PATH +from mod.base.web_conf import DirTool +from mod.base.web_conf.util import DB, write_file + + +class TestDirTool(WebBaseTestcase): + dir_tool = DirTool(PREFIX) + + def reset_site_config(self) -> None: + super(TestDirTool, self).reset_site_config() + + if "/www/server/panel/class" not in sys.path: + sys.path.insert(0, "/www/server/panel/class") + import public + + if os.path.exists(SUB_SITE_PATH + "/.user.ini"): + public.ExecShell("chattr -i " + SUB_SITE_PATH + "/.user.ini") + os.remove(SUB_SITE_PATH + "/.user.ini") + + if not os.path.exists(SITE_PATH + "/.user.ini"): + write_file(SITE_PATH + "/.user.ini", "open_basedir=/www/wwwroot/aaa.test.com/:/tmp/") + + site_info = DB("sites").where("name=?", (self.site_name,)).find() + DB("sites").where("id=?", (site_info["id"],)).setField('path', SITE_PATH) + + def test_modify_site_path(self): + res = self.dir_tool.modify_site_path(self.site_name, SITE_PATH, SUB_SITE_PATH) + self.assertIsNone(res, "修改路径失败") + self.assertTrue(os.path.exists(SUB_SITE_PATH + "/.user.ini"), ".user.ini错误") + self.assertEqual(DB("sites").where("name=?", (self.site_name,)).find()["path"], SUB_SITE_PATH, "数据库错误") + self.reset_site_config() + + def test_modify_site_run_path(self): + self.dir_tool.modify_site_run_path(self.site_name, SITE_PATH, "/test_run") + self.assertEqual(self.dir_tool.get_site_run_path(self.site_name), SUB_SITE_PATH, "修改运行目录失败") + self.assertTrue(os.path.exists(SUB_SITE_PATH + "/.user.ini"), ".user.ini错误") + self.reset_site_config() + + def test_index_conf(self): + self.assertCountEqual(self.dir_tool.get_index_conf(self.site_name), + ["index.php", "index.html", "index.htm", "default.php", "default.html", "default.htm"], + "获取index信息错误") + self.dir_tool.set_index_conf(self.site_name, "index.php", "default.php", "default.html", "default.htm") + self.assertCountEqual(self.dir_tool.get_index_conf(self.site_name), + ["index.php", "default.php", "default.html", "default.htm"], + "设置index信息错误") + + self.reset_site_config() + + def setUp(self) -> None: + self.reset_site_config() + + def runTest(self): + # 测试改变path + # self.change_env_to_nginx() + # self.test_modify_site_path() + # self.check_web_server_config() + # + # self.change_env_to_apache() + # self.test_modify_site_path() + # self.check_web_server_config() + + # 测试改变run_path + # self.change_env_to_nginx() + # self.test_modify_site_run_path() + # self.check_web_server_config() + # + # self.change_env_to_apache() + # self.test_modify_site_run_path() + # self.check_web_server_config() + + self.change_env_to_nginx() + self.test_index_conf() + self.check_web_server_config() + + self.change_env_to_apache() + self.test_index_conf() + self.check_web_server_config() + + def tearDown(self): + self.reset_site_config() + + +if __name__ == '__main__': + import unittest + + s = unittest.TestSuite() + s.addTest(TestDirTool()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_dns_api.py b/mod/test/test_web_conf/test_dns_api.py new file mode 100644 index 00000000..808c8d86 --- /dev/null +++ b/mod/test/test_web_conf/test_dns_api.py @@ -0,0 +1 @@ +# dns_api 部分来自dns集中管理插件, 未做单元测试 diff --git a/mod/test/test_web_conf/test_domain_tool.py b/mod/test/test_web_conf/test_domain_tool.py new file mode 100644 index 00000000..3aae17de --- /dev/null +++ b/mod/test/test_web_conf/test_domain_tool.py @@ -0,0 +1,68 @@ +import os +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, SITE_PATH, SUB_SITE_PATH +from mod.base.web_conf import normalize_domain, NginxDomainTool, ApacheDomainTool, ConfigMgr, set_default_site +from mod.base.web_conf.util import DB, write_file, read_file + + +class DomainToolTest(WebBaseTestcase): + + def test_normalize_domain(self): + res, err = normalize_domain("www.example.com", "www.example.com:2546545", "www.example.com:4545") + self.assertCountEqual(res, [("www.example.com", "80"), ("www.example.com", "4545")]) + print(err) + + def test_nginx_domain_tool(self): + ngd_tool = NginxDomainTool(PREFIX) + # 检查default_site 与 域名设置 + set_default_site(self.site_name) + res, _ = normalize_domain("www.example.com", "www.example.com:2546545", "www.example.com:4545") + res = ngd_tool.nginx_set_domain(self.site_name, (self.site_name, "80"), *res) + self.assertEqual(res, None, "设置域名出错") + + conf_mager = ConfigMgr(self.site_name, PREFIX) + conf = conf_mager.nginx_config() + self.assertIn("www.example.com", conf) + self.assertIn("4545", conf) + print(conf) + + def test_apache_domain_tool(self): + ngd_tool = ApacheDomainTool(PREFIX) + + res, _ = normalize_domain("www.example.com", "www.example.com:2546545", "www.example.com:4545") + res = ngd_tool.apache_set_domain(self.site_name, (self.site_name, "80"), *res) + self.assertEqual(res, None, "设置域名出错") + + conf_mager = ConfigMgr(self.site_name, PREFIX) + conf = conf_mager.apache_config() + self.assertIn("www.example.com", conf) + self.assertIn("4545", conf) + print(conf) + + def setUp(self) -> None: + self.reset_site_config() + + def runTest(self): + self.test_normalize_domain() + + # self.change_env_to_nginx() + # self.test_nginx_domain_tool() + # self.check_web_server_config() + + self.change_env_to_apache() + self.test_apache_domain_tool() + self.check_web_server_config() + + def tearDown(self): + self.reset_site_config() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(DomainToolTest()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_ip_restrict.py b/mod/test/test_web_conf/test_ip_restrict.py new file mode 100644 index 00000000..53d36ad8 --- /dev/null +++ b/mod/test/test_web_conf/test_ip_restrict.py @@ -0,0 +1,123 @@ +import os.path +import sys +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, NGINX_CONFIG_CASE, APACHE_CONFIG_CASE +from mod.base.web_conf.util import GET_CLASS, service_reload +from mod.base.web_conf import IpRestrict, ConfigMgr + + +class TestIpRestrict(WebBaseTestcase): + ip_restrict = IpRestrict(PREFIX) + + def setUp(self) -> None: + self.reset_site_config() + setup_path = "/www/server/panel" + ip_restrict_conf = "{}/data/ip_restrict_data/{}{}".format(setup_path, PREFIX, self.site_name) + if os.path.exists(ip_restrict_conf): + os.remove(ip_restrict_conf) + nginx_ip_restrict_conf = "{}/vhost/ip-restrict/{}{}.conf".format(setup_path, PREFIX, self.site_name) + if os.path.exists(nginx_ip_restrict_conf): + os.remove(nginx_ip_restrict_conf) + + def test_black_ip_restrict(self): + # 设置为黑名单格式 + get = GET_CLASS() + get.site_name = self.site_name + # black white closed + get.set_type = "black" + self.ip_restrict.set_ip_restrict(get) + + # 添加黑名单信息 + get = GET_CLASS() + get.site_name = self.site_name + get.value = "192.168.168.65" + self.ip_restrict.add_black_ip_restrict(get) + + config_mgr = ConfigMgr(self.site_name, PREFIX) + self.assertIn("ip-restrict", config_mgr.nginx_config()) + setup_path = "/www/server/panel" + ip_restrict_conf = "{}/data/ip_restrict_data/{}{}".format(setup_path, PREFIX, self.site_name) + nginx_ip_restrict_conf = "{}/vhost/ip-restrict/{}{}.conf".format(setup_path, PREFIX, self.site_name) + + self.assertTrue(os.path.exists(ip_restrict_conf)) + self.assertTrue(os.path.exists(nginx_ip_restrict_conf)) + + # 移除黑名单信息 + # get = GET_CLASS() + # get.site_name = self.site_name + # get.value = "192.168.168.65" + # self.ip_restrict.remove_black_ip_restrict(get) + + def test_white_ip_restrict(self): + # 设置为白名单格式 + get = GET_CLASS() + get.site_name = self.site_name + # black white closed + get.set_type = "white" + self.ip_restrict.set_ip_restrict(get) + + # 添加白名单信息 + get = GET_CLASS() + get.site_name = self.site_name + get.value = "192.168.168.65" # "192.168.168.66" + self.ip_restrict.add_white_ip_restrict(get) + + config_mgr = ConfigMgr(self.site_name, PREFIX) + self.assertIn("ip-restrict", config_mgr.nginx_config()) + setup_path = "/www/server/panel" + ip_restrict_conf = "{}/data/ip_restrict_data/{}{}".format(setup_path, PREFIX, self.site_name) + nginx_ip_restrict_conf = "{}/vhost/ip-restrict/{}{}.conf".format(setup_path, PREFIX, self.site_name) + + self.assertTrue(os.path.exists(ip_restrict_conf)) + self.assertTrue(os.path.exists(nginx_ip_restrict_conf)) + + # 移除白名单信息 + get = GET_CLASS() + get.site_name = self.site_name + get.value = "192.168.168.65" + self.ip_restrict.remove_white_ip_restrict(get) + + def test_ip_restrict_conf(self): + # 添加黑名单信息 + get = GET_CLASS() + get.site_name = self.site_name + get.value = "192.168.168.65" + self.ip_restrict.add_black_ip_restrict(get) + + # 设置为白单格式 + get = GET_CLASS() + get.site_name = self.site_name + # black white closed + get.set_type = "white" + self.ip_restrict.set_ip_restrict(get) + # 添加白名单信息 + get = GET_CLASS() + get.site_name = self.site_name + get.value = "192.168.168.65" # "192.168.168.66" + self.ip_restrict.add_white_ip_restrict(get) + + get = GET_CLASS() + get.site_name = self.site_name + print(self.ip_restrict.restrict_conf(get)) + + def runTest(self): + # self.change_env_to_nginx() + # self.test_black_ip_restrict() + # self.check_web_server_config() + + # self.change_env_to_nginx() + # self.test_white_ip_restrict() + # self.check_web_server_config() + + self.change_env_to_nginx() + self.test_ip_restrict_conf() + self.check_web_server_config() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestIpRestrict()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_logmanager.py b/mod/test/test_web_conf/test_logmanager.py new file mode 100644 index 00000000..aa437a65 --- /dev/null +++ b/mod/test/test_web_conf/test_logmanager.py @@ -0,0 +1,159 @@ +import json +import os.path +import shutil +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, NGINX_CONFIG_CASE, APACHE_CONFIG_CASE +from mod.base.web_conf import ConfigMgr, LogMgr +from mod.base.web_conf.util import GET_CLASS + +LOG_FORMAT_1 = json.dumps([ + "server_addr", "server_port", "host", "remote_addr", "remote_port", "protocol", "method", "uri", + "status", "sent_bytes", "referer", "user_agent", "take_time" +]) +LOG_FORMAT_2 = json.dumps([ + "server_addr", "host", "remote_addr", "remote_port", "protocol", "method", "uri", + "status", "user_agent", "take_time" +]) + + +class TestRealLogMgr(WebBaseTestcase): + log_mgr = LogMgr(PREFIX) + + def test_log_format_mager(self): + # 查看初始 + get = GET_CLASS() + get.site_name = self.site_name + print(self.log_mgr.log_format_data(get)) + + # 添加 一个格式 + get = GET_CLASS() + get.format_name = "btlog1" + get.keys = LOG_FORMAT_1 + get.space_character = "|" + print(self.log_mgr.add_log_format(get)) + + # 添加 第二个格式 + get = GET_CLASS() + get.format_name = "btlog2" + get.keys = LOG_FORMAT_2 + get.space_character = " " + print(self.log_mgr.add_log_format(get)) + + # 修改 第二个格式 + get = GET_CLASS() + get.format_name = "btlog2" + get.keys = LOG_FORMAT_2 + get.space_character = "|" + print(self.log_mgr.modify_log_format(get)) + + # 删除 第一个格式 + # get = GET_CLASS() + # get.format_name = "btlog1" + # print(self.log_mgr.remove_log_format(get)) + + # 查看格式 + get = GET_CLASS() + get.site_name = self.site_name + print(self.log_mgr.log_format_data(get)) + + def test_set_site_log_format(self): + # 设置使用 第二个 + get = GET_CLASS() + get.site_name = self.site_name + get.format_name = "btlog2" + print(self.log_mgr.set_site_log_format(get)) + + # 修改 第二个格式 + get = GET_CLASS() + get.format_name = "btlog2" + get.keys = LOG_FORMAT_2 + get.space_character = "|" + print(self.log_mgr.modify_log_format(get)) + + # 设置使用 第一个 + get = GET_CLASS() + get.site_name = self.site_name + get.format_name = "btlog1" + print(self.log_mgr.set_site_log_format(get)) + + # 查看格式 + get = GET_CLASS() + get.site_name = self.site_name + print(self.log_mgr.log_format_data(get)) + + def test_site_log_path(self): + # 查看初始 + get = GET_CLASS() + get.site_name = self.site_name + print(self.log_mgr.get_site_log_path(get)) + + # 修改日志路径 + get = GET_CLASS() + get.site_name = self.site_name + get.log_path = "/www/test/logs" + print(self.log_mgr.set_site_log_path(get)) + + def test_site_crontab_log(self): + get = GET_CLASS() + get.site_name = self.site_name + get.hour = "1" + get.minute = "1" + get.save = "180" + print(self.log_mgr.site_crontab_log(get)) + + def setUp(self) -> None: + self.reset_site_config() + self.config_mgr = ConfigMgr(self.site_name, PREFIX) + panel_path = "/www/server/panel" + if os.path.exists("{}/data/ng_log_format.json".format(panel_path)): + os.remove("{}/data/ng_log_format.json".format(panel_path)) + + if os.path.exists("{}/vhost/nginx/log_format".format(panel_path)): + shutil.rmtree("{}/vhost/nginx/log_format".format(panel_path)) + + if os.path.exists("{}/data/ap_log_format.json".format(panel_path)): + os.remove("{}/data/ap_log_format.json".format(panel_path)) + + if os.path.exists("{}/vhost/apache/log_format".format(panel_path)): + shutil.rmtree("{}/vhost/apache/log_format".format(panel_path)) + + def runTest(self): + # self.change_env_to_nginx() + # self.test_log_format_mager() + # print("==================================") + # self.test_set_site_log_format() + # self.check_web_server_config() + + # self.change_env_to_apache() + # self.test_log_format_mager() + # print("==================================") + # self.test_set_site_log_format() + # self.check_web_server_config() + + # self.change_env_to_nginx() + # self.log_mgr = LogMgr(PREFIX) + # self.test_site_log_path() + # self.check_web_server_config() + + # self.change_env_to_apache() + # self.log_mgr = LogMgr(PREFIX) + # self.test_site_log_path() + # self.check_web_server_config() + + self.test_site_crontab_log() + + def tearDown(self): + pass + self.reset_site_config() + + +if __name__ == '__main__': + import unittest + + s = unittest.TestSuite() + s.addTest(TestRealLogMgr()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_proxy.py b/mod/test/test_web_conf/test_proxy.py new file mode 100644 index 00000000..d97cf623 --- /dev/null +++ b/mod/test/test_web_conf/test_proxy.py @@ -0,0 +1,107 @@ + +import os +import shutil +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, NGINX_CONFIG_CASE, APACHE_CONFIG_CASE +from mod.base.web_conf import ConfigMgr, Proxy +from mod.base.web_conf.util import GET_CLASS + + +class TestProxy(WebBaseTestcase): + proxy_obj = Proxy(PREFIX) + + def test_create_project_proxy(self): + get = GET_CLASS() + get.proxyname = "aaa" + get.sitename = self.site_name + get.proxydir = "/" + get.proxysite = "https://www.baidu.com" + get.todomain = "www.baidu.com" + get.type = "1" + get.cache = "1" + get.subfilter = '[{"sub1":"","sub2":""},{"sub1":"","sub2":""},{"sub1":"","sub2":""}]' + get.advanced = "1" + get.cachetime = "1" + print(self.proxy_obj.create_proxy(get)) + + # get = GET_CLASS() + # get.proxyname = "ggfff" + # get.sitename = self.site_name + # get.proxydir = "/dad" + # get.proxysite = "https://www.baidu.com" + # get.todomain = "www.baidu.com" + # get.type = "1" + # get.cache = "1" + # get.subfilter = '[{"sub1":"","sub2":""},{"sub1":"","sub2":""},{"sub1":"","sub2":""}]' + # get.advanced = "1" + # get.cachetime = "1" + # print(self.proxy_obj.create_proxy(get)) + + def test_modify_project_proxy(self): + get = GET_CLASS() + get.proxyname = "ggfff" + get.sitename = self.site_name + get.proxydir = "/dygvccc" + get.proxysite = "https://www.baidu.com" + get.todomain = "www.baidu.com" + get.type = "1" + get.cache = "1" + get.subfilter = '[{"sub1":"","sub2":""},{"sub1":"","sub2":""},{"sub1":"","sub2":""}]' + get.advanced = "0" + get.cachetime = "1" + print(self.proxy_obj.modify_proxy(get)) + + def test_remove_project_proxy(self): + get = GET_CLASS() + get.proxyname = "aaa" + get.sitename = self.site_name + print(self.proxy_obj.remove_proxy(get)) + + def test_get_project_proxy_list(self): + get = GET_CLASS() + get.sitename = self.site_name + print(self.proxy_obj.get_proxy_list(get)) + + def setUp(self) -> None: + self.reset_site_config() + self.config_mgr = ConfigMgr(self.site_name, PREFIX) + panel_path = "/www/server/panel" + _proxy_conf_file = "{}/data/mod_proxy_file.conf".format(panel_path) + if os.path.exists(_proxy_conf_file): + os.remove(_proxy_conf_file) + + ng_proxy_dir = "/www/server/panel/vhost/nginx/proxy/" + self.site_name + ap_proxy_dir = "/www/server/panel/vhost/apache/proxy/" + self.site_name + if os.path.exists(ng_proxy_dir): + shutil.rmtree(ng_proxy_dir) + + if os.path.exists(ap_proxy_dir): + shutil.rmtree(ap_proxy_dir) + + def runTest(self): + # self.change_env_to_nginx() + # self.test_create_project_proxy() + # self.test_remove_project_proxy() + # self.check_web_server_config() + + self.change_env_to_apache() + self.test_create_project_proxy() + self.test_remove_project_proxy() + self.test_modify_project_proxy() + self.test_get_project_proxy_list() + self.check_web_server_config() + + # def tearDown(self): + # self.reset_site_config() + + +if __name__ == '__main__': + import unittest + + s = unittest.TestSuite() + s.addTest(TestProxy()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_redirect.py b/mod/test/test_web_conf/test_redirect.py new file mode 100644 index 00000000..35579d3b --- /dev/null +++ b/mod/test/test_web_conf/test_redirect.py @@ -0,0 +1,87 @@ +import json +import os +import shutil +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, NGINX_CONFIG_CASE, APACHE_CONFIG_CASE +from mod.base.web_conf import ConfigMgr, Redirect +from mod.base.web_conf.util import GET_CLASS + + +class TestRedirect(WebBaseTestcase): + redirect = Redirect() + + def test_create_project_redirect(self): + get = GET_CLASS() + get.sitename = self.site_name + get.redirectpath = "/" + get.redirecttype = "301" + get.domainorpath = "path" + get.redirectname = "aaa" + get.tourl = "" + get.topath = "/ashdjadg" + get.redirectdomain = "[]" + get.type = "1" + get.errorpage = "0" + get.holdpath = "1" + print(self.redirect.create_project_redirect(get)) + + def test_modify_project_redirect(self): + get = GET_CLASS() + get.sitename = self.site_name + get.redirectpath = "/" + get.redirecttype = "301" + get.domainorpath = "domain" + get.redirectname = "aaa" + get.tourl = "https://www.baidu.com" + get.topath = "" + get.redirectdomain = json.dumps([self.site_name]) + get.type = "1" + get.errorpage = "0" + get.holdpath = "1" + print(self.redirect.modify_project_redirect(get)) + + def test_remove_project_redirect(self): + get = GET_CLASS() + get.sitename = self.site_name + get.redirectname = "aaa" + print(self.redirect.remove_project_redirect(get)) + + def test_get_project_redirect_list(self): + get = GET_CLASS() + get.sitename = self.site_name + print(self.redirect.get_project_redirect_list(get)) + + def setUp(self) -> None: + self.reset_site_config() + self.config_mgr = ConfigMgr(self.site_name, PREFIX) + + def runTest(self): + # self.change_env_to_nginx() + # self.test_create_project_redirect() + # self.test_modify_project_redirect() + # self.test_get_project_redirect_list() + # self.test_remove_project_redirect() + # self.check_web_server_config() + # + self.change_env_to_apache() + self.test_create_project_redirect() + self.test_modify_project_redirect() + self.test_get_project_redirect_list() + self.test_remove_project_redirect() + self.check_web_server_config() + + # def tearDown(self): + # pass + # self.reset_site_config() + + +if __name__ == '__main__': + import unittest + + s = unittest.TestSuite() + s.addTest(TestRedirect()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_referer.py b/mod/test/test_web_conf/test_referer.py new file mode 100644 index 00000000..8478a070 --- /dev/null +++ b/mod/test/test_web_conf/test_referer.py @@ -0,0 +1,70 @@ +import json +import os +import shutil +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, NGINX_CONFIG_CASE, APACHE_CONFIG_CASE +from mod.base.web_conf import ConfigMgr, Referer +from mod.base.web_conf.util import GET_CLASS + + +class TestReferer(WebBaseTestcase): + referer = Referer(PREFIX) + + def test_referer_security(self): + # 开启 + get = GET_CLASS() + get.status = "true" + get.http_status = "false" + get.name = self.site_name + get.fix = "fsf,dhjdh,uooo" + get.domains = self.site_name + ",www.asdad.com" + get.return_rule = "403" + print(self.referer.set_referer_security(get)) + + # 修改 + get = GET_CLASS() + get.status = "true" + get.http_status = "false" + get.name = self.site_name + get.fix = "fsf,dhjdh,hjhlh" + get.domains = self.site_name + ",www.asdad.com" + get.return_rule = "404" + print(self.referer.set_referer_security(get)) + + # 删除 + get = GET_CLASS() + get.status = "false" + get.http_status = "false" + get.name = self.site_name + get.fix = "fsf,dhjdh,hjhlh" + get.domains = self.site_name + ",www.asdad.com" + get.return_rule = "404" + print(self.referer.set_referer_security(get)) + + get = GET_CLASS() + get.site_name = self.site_name + print(self.referer.get_referer_security(get)) + + def setUp(self) -> None: + self.reset_site_config() + self.config_mgr = ConfigMgr(self.site_name, PREFIX) + + def runTest(self): + self.change_env_to_nginx() + self.test_referer_security() + self.check_web_server_config() + + # def tearDown(self): + # pass + # self.reset_site_config() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestReferer()) + unittest.TextTestRunner().run(s) diff --git a/mod/test/test_web_conf/test_ssl.py b/mod/test/test_web_conf/test_ssl.py new file mode 100644 index 00000000..0e841a7a --- /dev/null +++ b/mod/test/test_web_conf/test_ssl.py @@ -0,0 +1,63 @@ +import json +import os +import shutil +import sys + +if "/www/server/panel" not in sys.path: + sys.path.insert(0, "/www/server/panel") + +from mod.test.test_web_conf import WebBaseTestcase, PREFIX, NGINX_CONFIG_CASE, APACHE_CONFIG_CASE +from mod.base.web_conf import SSLManager, set_default_site +from mod.base.web_conf.util import GET_CLASS + + +class TestSSLManager(WebBaseTestcase): + ssl = SSLManager(PREFIX) + + def test_set_site_ssl_conf(self): + # 开启 + get = GET_CLASS() + get.ssl_id = "2" # 保证这个ID存在 + get.site_name = self.site_name + print(self.ssl.set_site_ssl_conf(get)) + + def test_mutil_set_site_ssl_conf(self): + # 开启 + get = GET_CLASS() + get.ssl_id = "9" # 保证这个ID存在 + get.site_names = json.dumps([self.site_name, "www.123test.com"]) + print(self.ssl.mutil_set_site_ssl_conf(get)) + + def test_close_site_ssl_conf(self): + # 关闭 + get = GET_CLASS() + get.site_name = self.site_name + print(self.ssl.close_site_ssl_conf(get)) + + def setUp(self) -> None: + self.reset_site_config() + + def runTest(self): + set_default_site(site_name=self.site_name) + self.change_env_to_nginx() + self.test_set_site_ssl_conf() + self.test_mutil_set_site_ssl_conf() + self.test_close_site_ssl_conf() + self.check_web_server_config() + + # self.change_env_to_apache() + # self.test_set_site_ssl_conf() + # self.test_mutil_set_site_ssl_conf() + # self.test_close_site_ssl_conf() + # self.check_web_server_config() + + # def tearDown(self): + # pass + # self.reset_site_config() + + +if __name__ == '__main__': + import unittest + s = unittest.TestSuite() + s.addTest(TestSSLManager()) + unittest.TextTestRunner().run(s) diff --git a/requirements.txt b/requirements.txt index 0302613c..e5277fd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,54 +1,128 @@ -bcrypt==3.2.0 -certifi==2021.10.8 -cffi==1.15.0 -chardet==4.0.0 -charset-normalizer==2.0.12 -click==8.0.4 -configparser==5.2.0 -cos-python-sdk-v5==1.9.15 +aliyun-python-sdk-core==2.13.36 +aliyun-python-sdk-core-v3==2.13.11 +aliyun-python-sdk-kms==2.16.0 +async-timeout==4.0.3 +bcrypt==4.0.1 +beautifulsoup4==4.12.2 +cachelib==0.12.0 +cachetools==5.3.0 +certifi==2024.2.2 +cffi==1.15.1 +chardet==5.1.0 +charset-normalizer==3.3.2 +click==8.1.7 +configobj==5.0.8 +configparser==5.3.0 +cos-python-sdk-v5==1.9.23 crcmod==1.7 -cryptography==36.0.1 -Cython==0.29.28 -Deprecated==1.2.13 -dicttoxml==1.7.4 -dnspython==2.2.1 -docker==5.0.3 -Flask==2.0.3 -flask-sock==0.5.1 -h11==0.13.0 -idna==3.3 +cryptography==40.0.2 +Cython==0.29.34 +decorator==5.1.1 +dicttoxml==1.7.16 +dnspython==2.3.0 +docker==6.0.1 +enum34==1.1.10 +Flask==2.2.5 +Flask-Session==0.4.0 +flask-sock==0.7.0 +Flask-SQLAlchemy==3.0.3 +future==0.18.3 +geoip2==4.8.0 +gevent==24.2.1 +gevent-websocket==0.10.1 +google-api-core==2.11.0 +google-api-python-client==2.86.0 +google-auth==2.17.3 +google-auth-httplib2==0.1.0 +google-auth-oauthlib==1.0.0 +google-cloud-core==2.3.2 +google-cloud-storage==2.8.0 +google-crc32c==1.5.0 +google-resumable-media==2.5.0 +googleapis-common-protos==1.59.0 +greenlet==3.0.3 +h11==0.14.0 +httplib2==0.22.0 +idna==3.7 +importlib-metadata==6.7.0 +iniparse==0.5 +ipaddress==1.0.23 IPy==1.1 -itsdangerous==2.1.1 -Jinja2==3.0.3 -MarkupSafe==2.1.0 -oauthlib==3.2.0 -packaging==21.3 -paramiko==2.10.2 -Pillow==9.0.1 -psutil==5.9.0 -pyasn1==0.4.8 +itsdangerous==2.1.2 +Jinja2==3.1.3 +jmespath==0.10.0 +kitchen==1.2.6 +MarkupSafe==2.1.5 +mongo==0.2.0 +natsort==8.4.0 +oauthlib==3.2.2 +oss2==2.17.0 +packaging==23.1 +paramiko==3.4.0 +peewee==3.16.2 +pillow==10.3.0 +protobuf==4.22.3 +psutil==5.9.5 +psycopg2-binary==2.9.9 +pyasn1==0.5.0 +pyasn1-modules==0.3.0 +pyasyncore==1.0.4 pycparser==2.21 -pycryptodome==3.14.1 +pycryptodome==3.17 +pycurl==7.45.2 +Pygments==2.15.1 pyinotify==0.9.6 -PyMySQL==1.0.2 +pymongo==4.6.3 +PyMySQL==1.0.3 PyNaCl==1.5.0 -pyOpenSSL==22.0.0 -pyparsing==3.0.7 -qiniu==7.5.0 -qrcode==7.3.1 -redis==4.1.4 -requests==2.27.1 +pyOpenSSL==23.1.1 +pyparsing==3.0.9 +pypdf==3.17.0 +PySocks==1.7.1 +pytz==2023.3 +pyudev==0.24.1 +pyxattr==0.8.1 +PyYAML==6.0.1 +qiniu==7.10.0 +qrcode==7.4.2 +redis==4.5.4 +requests==2.31.0 requests-file==1.5.1 requests-oauthlib==1.3.1 -rsa==4.8 -simple-websocket==0.5.1 +rsa==4.9 +simple-websocket==0.10.0 six==1.16.0 -soupsieve==2.3.1 +soupsieve==2.4.1 +SQLAlchemy==2.0.10 +supervisor==4.2.5 +typing-extensions==4.7.1 upyun==2.5.5 -urllib3==1.26.8 -websocket-client==1.3.1 -Werkzeug==2.0.3 -wrapt==1.14.0 -wsproto==1.1.0 -gevent -pyyaml \ No newline at end of file +uritemplate==4.1.1 +urlgrabber==4.1.0 +urllib3==1.26.18 +websocket-client==1.5.1 +Werkzeug==2.2.3 +wheel==0.38.1 +wsproto==1.2.0 +xmltodict==0.13.0 +zipp==3.15.0 +zope.event==5.0 +zope.interface==6.2 +pandas==2.2.2 +Brotli==1.1.0 +rarfile==4.2 +pyotp==2.9.0 +unrar==0.4 +sphinxapi-py3==2.1.11 +jmxquery==0.6.0 +billiard==4.2.0 +lxml==5.0.0 +docxtpl==0.16.8 +ntplib==0.4.0 +pyasynchat==1.0.4 +toml==0.10.2 +boto3==1.34.102 +distro==1.9.0 +pymssql==2.3.0 +distro==1.9.0 +pymssql==2.3.0 \ No newline at end of file diff --git a/runserver.py b/runserver.py index 3c991ee7..c059ae4b 100644 --- a/runserver.py +++ b/runserver.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- from os import environ from BTPanel import app,sys diff --git a/script/Werkzeug-2.2.3-py3-none-any.whl b/script/Werkzeug-2.2.3-py3-none-any.whl new file mode 100644 index 00000000..4c9b46f2 Binary files /dev/null and b/script/Werkzeug-2.2.3-py3-none-any.whl differ diff --git a/script/backup.py b/script/backup.py index 7068ae60..47b205a4 100644 --- a/script/backup.py +++ b/script/backup.py @@ -1,12 +1,15 @@ #!/usr/bin/python #coding: utf-8 #----------------------------- -# 宝塔Linux面板网站备份工具 +# aaPanel +# 网站备份工具 #----------------------------- import sys,os os.chdir('/www/server/panel') +sys.path.append("./") sys.path.append("class/") +sys.path.append("class_v2/") if sys.version_info[0] == 2: reload(sys) sys.setdefaultencoding('utf-8') @@ -24,11 +27,9 @@ class backupTools(panelBackup.backup): def backupPath(self,path,count,echo_id=None): self.backup_path(path,save=count,echo_id=echo_id) - def backupSiteAll(self,save,echo_id=None): self.backup_site_all(save,echo_id=echo_id) - def backupDatabaseAll(self,save,echo_id=None): self.backup_database_all(save,echo_id=echo_id) diff --git a/script/check_msg.py b/script/check_msg.py index c689915e..26f97449 100644 --- a/script/check_msg.py +++ b/script/check_msg.py @@ -14,7 +14,7 @@ data = msgObj.get_messages() for x in data: if x['level'] in ['danger', 'error'] and not x['send'] and x['retry_num'] < 5: msg = '服务器IP【{}】: {}'.format( - public.GetLocalIp(), re.sub(',?', '', x['msg'])) + public.GetLocalIp(), re.sub(r',?', '', x['msg'])) is_send = False ret = public.return_is_send_info() diff --git a/script/dk_log_split.py b/script/dk_log_split.py new file mode 100644 index 00000000..0fdf3d16 --- /dev/null +++ b/script/dk_log_split.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# coding: utf-8 +# ----------------------------- +# docker container log cutting script +# ----------------------------- +import sys +import os +import time +import datetime + +os.chdir("/www/server/panel") +sys.path.append('class/') +import public + + +class DkLogSpilt: + task_list = [] + + def __init__(self): + if not public.M('sqlite_master').db('docker_log_split').where('type=? AND name=?', ('table', 'docker_log_split')).count(): + self.task_list = [] + else: + self.task_list = public.M('docker_log_split').select() + + def run(self): + if not self.task_list: + print('No docker log cutting task') + for task in self.task_list: + try: + if task['split_type'] == 'day': + self.day_split(task) + elif task['split_type'] == 'size': + self.size_split(task) + except: + print('{} Failed to cut log!'.format(task['name'])) + + def day_split(self, task): + now_time = int(time.time()) + exec_time = int(self.get_timestamp_of_hour_minute(task['split_hour'], task['split_minute'])) + if now_time <= exec_time <= now_time + 300: + print("{} container starts log cutting".format(task['name'])) + split_path = '/var/lib/docker/containers/history_logs/{}/'.format(task['pid']) + if not os.path.exists(split_path): + os.makedirs(split_path) + os.rename(task['log_path'], split_path + task['pid'] + "-json.log" + '_' + str(int(time.time()))) + public.writeFile(task['log_path'], '') + print("{} log has been cut to:{}".format(task['name'],split_path + task['pid'] + "-json.log" + '_' + str(int(time.time())))) + self.check_save(task) + else: + print('{}container log has not reached the cutting time'.format(task['name'])) + + + def size_split(self, task): + if not os.path.exists(task['log_path']): + print('Log file does not exist') + return + if os.path.getsize(task['log_path']) >= task['split_size']: + print("{} container starts log cutting".format(task['name'])) + split_path = '/var/lib/docker/containers/history_logs/{}/'.format(task['pid']) + if not os.path.exists(split_path): + os.makedirs(split_path) + os.rename(task['log_path'], split_path + task['pid'] + "-json.log" + '_' + str(int(time.time()))) + public.writeFile(task['log_path'], '') + print("{} log has been cut to:{}".format(task['name'],split_path + task['pid'] + "-json.log" + '_' + str(int(time.time())))) + self.check_save(task) + else: + print('{} container log has not reached cutting size'.format(task['name'])) + + def check_save(self, task): + split_path = '/var/lib/docker/containers/history_logs/{}/'.format(task['pid']) + file_count = len(os.listdir(split_path)) + if file_count > task['save']: + file_list = os.listdir(split_path) + file_list.sort() + for i in range(file_count - task['save']): + os.remove(split_path + file_list[i]) + print('Delete log files:{}'.format(split_path + file_list[i])) + print('The latest {} logs have been retained'.format(task['save'])) + + def get_timestamp_of_hour_minute(self, hour, minute): + """获取当天指定时刻的时间戳。 + Args: + hour: 小时。 + minute: 分钟。 + Returns: + 时间戳。 + """ + current_time = datetime.datetime.now() + timestamp = current_time.replace(hour=hour, minute=minute, second=0, microsecond=0) + return int(timestamp.timestamp()) + + +if __name__ == '__main__': + dk = DkLogSpilt() + dk.run() diff --git a/script/logsBackup b/script/logsBackup index 943299e0..9333bf6f 100644 --- a/script/logsBackup +++ b/script/logsBackup @@ -1,7 +1,8 @@ #!/usr/bin/python #coding: utf-8 #----------------------------- -#宝塔Linux面板网站日志切割脚本 +#aaPanel +# 网站日志切割脚本 #----------------------------- import sys import os diff --git a/script/logsBackup.py b/script/logsBackup.py index 6daa506f..a6e99ec4 100644 --- a/script/logsBackup.py +++ b/script/logsBackup.py @@ -1,7 +1,8 @@ #!/usr/bin/python #coding: utf-8 #----------------------------- -#宝塔Linux面板网站日志切割脚本 +#aaPanel +# 网站日志切割脚本 #----------------------------- import sys import os diff --git a/script/polkit_upgrade.py b/script/polkit_upgrade.py index b7c33e7b..67a7ce0c 100644 --- a/script/polkit_upgrade.py +++ b/script/polkit_upgrade.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- #-------------------------------- diff --git a/script/process_network_total.py b/script/process_network_total.py index aae02adf..59e1eb5a 100644 --- a/script/process_network_total.py +++ b/script/process_network_total.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- import sys diff --git a/script/run_script.py b/script/run_script.py index 331f0ed8..920d9895 100644 --- a/script/run_script.py +++ b/script/run_script.py @@ -1,10 +1,10 @@ #coding: utf-8 #------------------------------------------------------------------- -# 宝塔Linux面板 +# aaPanel #------------------------------------------------------------------- -# Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- -# Author: hwliang +# Author: hwliang #------------------------------------------------------------------- #------------------------------ diff --git a/script/webserver-ctl.sh b/script/webserver-ctl.sh new file mode 100644 index 00000000..8d942409 --- /dev/null +++ b/script/webserver-ctl.sh @@ -0,0 +1,228 @@ +#!/bin/bash +action=$1 +panel_path="/www/server/panel" # 面板路径 +webserver_bin="$panel_path/webserver/sbin/webserver" # nginx二进制文件 +webserver_conf="$panel_path/webserver/conf/webserver.conf" # nginx配置文件 +webserver_pid="$panel_path/webserver/logs/webserver.pid" # nginx pid文件 + + +PID=0 + + + +get_pid() { + if [ ! -f "$webserver_pid" ]; then + PID=0 + else + PID=$(cat $webserver_pid) + if [ "$PID" == "" ]; then + PID=0 + else + PID=$(ps aux | grep "$PID" | grep -v grep | awk '{print $2}') + PID_11=$(ps aux | grep "$webserver_bin" | grep -v grep | awk '{print $2}') + if [ "$PID" != "$PID_11" ]; then + PID=0 + fi + fi + fi + + PID=$(ps aux | grep "$webserver_bin" | grep -v grep | awk '{print $2}') + if [ -z "$PID" ]; then + PID=0 + else + PID="$PID" + fi + + if [ -z "$PID" ]; then + PID=0 + fi +} + +validate_server_files() { + if [ ! -f "$webserver_conf" ]; then + echo "BaoTa Web server configuration not found: $webserver_conf" + exit 1 + fi + + if [ ! -f "$webserver_bin" ]; then + echo "BaoTa Web server binary not found: $webserver_bin" + exit 1 + fi +} + +start() { + validate_server_files + get_pid + + if [ $PID -gt 0 ]; then + echo "BaoTa Web server is already running with PID ($PID)" + exit 1 + fi + + echo -n "Starting BaoTa web server..." + if [ -f "$webserver_pid" ]; then + rm -f $webserver_pid + fi + chmod 700 $webserver_bin + $webserver_bin -c $webserver_conf + if [ $? -ne 0 ]; then + echo "Failed to start BaoTa web server" + exit 1 + fi + + echo " Started" +} + +stop() { + validate_server_files + get_pid + if [ $PID -eq 0 ]; then + echo "BaoTa Web server is not running" + exit 1 + fi + + echo -n "Stopping BaoTa web server..." + $webserver_bin -c $webserver_conf -s stop + + pids=$(lsof -c webserver|grep LISTEN|awk '{print $2}'|sort -u) + for pid in $pids; do + kill -9 $pid + done + + echo " Stopped" +} + +restart() { + validate_server_files + get_pid + echo -n "Restarting BaoTa web server..." + if [ $PID -eq 0 ]; then + $webserver_bin -c $webserver_conf + else + $webserver_bin -c $webserver_conf -s reopen + fi + + if [ $? -ne 0 ]; then + echo "Failed to restart BaoTa web server" + exit 1 + fi + + echo " Restarted" +} + +status() { + validate_server_files + get_pid + if [ $PID -eq 0 ]; then + echo "BaoTa Web server is not running" + else + cmdline=/proc/$PID/cmdline + if [ ! -f $cmdline ]; then + echo "BaoTa Web server is not running" + rm -f $webserver_pid + exit 1 + fi + echo "BaoTa Web server is running with PID ($PID)" + fi +} + +reload() { + validate_server_files + get_pid + if [ $PID -eq 0 ]; then + echo "BaoTa Web server is not running" + exit 1 + fi + + echo -n "Reloading BaoTa web server..." + $webserver_bin -c $webserver_conf -s reload + if [ $? -ne 0 ]; then + echo "Failed to reload BaoTa web server" + exit 1 + fi + echo " Reloaded" +} + +configtest() { + validate_server_files + # 检查配置文件正确性,检查程序自动输出检查结果 + $webserver_bin -c $webserver_conf -t + +} + +download() { + tip_file=$panel_path/data/download.pl + if [ -f $tip_file ]; then + echo "BaoTa web server binary has been downloaded" + exit 1 + fi + + # 标记已下载 + echo "1" > $tip_file + + # 获取machine + machine=$(uname -m) + zip_file=$panel_path/data/webserver-$machine.zip + wget -O $zip_file https://node.aapanel.com/webserver/webserver-$machine.zip + if [ $? -ne 0 ]; then + echo "Failed to download BaoTa web server binary" + rm -f $zip_file + exit 1 + fi + + # 验证文件hash + hash256=$(sha256sum $zip_file | awk '{print $1}') + cloud_hash256=$(wget -q -O - https://node.aapanel.com/webserver/webserver-$machine.txt) + if [ "$hash256" != "$cloud_hash256" ]; then + echo "Failed to verify BaoTa web server binary" + rm -f $zip_file + exit 1 + fi + + # 解压文件 + unzip -o $zip_file -d $panel_path/ + if [ ! -f $webserver_bin ]; then + echo "Failed to extract BaoTa web server binary" + rm -f $zip_file + exit 1 + fi + + # 删除临时文件 + rm -f $zip_file + # 设置权限 + chmod 700 $webserver_bin + echo "BaoTa web server binary has been downloaded" + bash /www/server/panel/init.sh reload +} + + +case "$action" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + status) + status + ;; + reload) + reload + ;; + configtest) + configtest + ;; + test) + configtest + ;; + download) + download + ;; + *) + echo "Usage: $0 {start|stop|restart|status|reload|configtest|test|download}" + exit 1 + ;; +esac \ No newline at end of file diff --git a/task.py b/task.py index 17a2c082..83d99dfd 100644 --- a/task.py +++ b/task.py @@ -1,11 +1,11 @@ #!/bin/python #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2016 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2016 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- # ------------------------------ @@ -625,7 +625,7 @@ def daemon_panel(): def update_panel(): - os.system("curl -k https://node.aapanel.com/install/update6_en.sh|bash &") + os.system("curl -k https://node.aapanel.com/install/update_7.x_en.sh|bash &") def service_panel(action='reload'): @@ -730,7 +730,7 @@ def push_msg(): def JavaProDaemons(): ''' @name Java 项目守护进程 - @author lkq@bt.cn + @author lkq@aapanel.com @time 2022-07-19 @param None ''' @@ -816,6 +816,7 @@ def run_thread(): "update_software_list": update_software_list, "send_mail_time": send_mail_time, "check_panel_msg": check_panel_msg, + # "check_panel_auth": check_panel_auth, "push_msg": push_msg, "ProDadmons":ProDadmons, "process_task_thread":process_task_thread @@ -854,6 +855,21 @@ def scan_log_site(): # os.system('{} {}/script/check_msg.py &'.format(python_bin,base_path)) # time.sleep(600) +# # 检测面板授权 +# def check_panel_auth(): +# python_bin = get_python_bin() +# from BTPanel import cache +# if cache: +# key='pro_check_sdfjslk' +# res = cache.get(key) +# while True: +# update_file='/www/server/panel/data/now_update_pro.pl' +# # pro_file='/www/server/panel/data/panel_pro.pl' +# if os.path.exists(update_file) or res is None: +# os.system('nohup {} /www/server/panel/script/check_auth.py > /dev/null 2>&1 &'.format(python_bin)) +# if cache: +# cache.set(key, 'sddsf', 3600) + def main(): main_pid = 'logs/task.pid' diff --git a/tools.py b/tools.py index 965d2457..e8d703a0 100644 --- a/tools.py +++ b/tools.py @@ -1,10 +1,10 @@ #coding: utf-8 # +------------------------------------------------------------------- -# | 宝塔Linux面板 +# | aaPanel # +------------------------------------------------------------------- -# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# | Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # +------------------------------------------------------------------- -# | Author: hwliang +# | Author: hwliang # +------------------------------------------------------------------- #------------------------------ @@ -24,8 +24,8 @@ if sys.version_info[0] == 3: raw_input = input def set_mysql_root(password): import db,os sql = db.Sql() - - root_mysql = '''#!/bin/bash + + root_mysql = r'''#!/bin/bash PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH pwd=$1 @@ -34,7 +34,7 @@ mysqld_safe --skip-grant-tables& echo 'Changing password...'; sleep 6 m_version=$(cat /www/server/mysql/version.pl|grep -E "(5.1.|5.5.|5.6.|10.0|10.1)") -m2_version=$(cat /www/server/mysql/version.pl|grep -E "(10.5.|10.4.)") +m2_version=$(cat /www/server/mysql/version.pl|grep -E "10.([4-9]|[1-9][0-9]).") if [ "$m_version" != "" ];then mysql -uroot -e "UPDATE mysql.user SET password=PASSWORD('${pwd}') WHERE user='root'"; elif [ "$m2_version" != "" ];then @@ -56,18 +56,18 @@ sleep 2 echo '===========================================' echo "The root password set ${pwd} successuful"'''; - + public.writeFile('mysql_root.sh',root_mysql) os.system("/bin/bash mysql_root.sh " + password) os.system("rm -f mysql_root.sh") - + result = sql.table('config').where('id=?',(1,)).setField('mysql_root',password) print(result) #设置面板密码 def set_panel_pwd(password,ncli = False): password = password.strip() - re_list = re.findall("[^\w\d,.]+", password) + re_list = re.findall(r"[^\w\d,.]+", password) if re_list: print("|-Error: password cannot contain special characters: {}".format(" ".join(re_list))) return @@ -83,7 +83,7 @@ def set_panel_pwd(password,ncli = False): #设置数据库目录 def set_mysql_dir(path): - mysql_dir = '''#!/bin/bash + mysql_dir = r'''#!/bin/bash PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH oldDir=`cat /etc/my.cnf |grep 'datadir'|awk '{print $3}'` @@ -197,7 +197,7 @@ def CloseTask(): os.system("kill `ps -ef |grep 'install_soft.sh'|grep -v grep|grep -v panelExec|awk '{print $2}'`") os.system('/etc/init.d/bt restart') print(public.GetMsg("CLEAR_TASK",(int(ncount),))) - + def get_ipaddress(): ''' @name 获取本机IP地址 @@ -351,7 +351,7 @@ def ClearRecycle_Bin(): import files f = files.files(); f.Close_Recycle_bin(None); - + #清理其它 def ClearOther(): clearPath = [ @@ -362,7 +362,7 @@ def ClearOther(): {'path':'/www/server/panel/install','find':'.zip'}, {'path':'/www/server/panel/install','find':'.gz'} ] - + total = count = 0 print(public.GetMsg("CLEAR_RUBBISH3")) for c in clearPath: @@ -409,7 +409,7 @@ def set_panel_username(username = None): import db sql = db.Sql() if username: - re_list = re.findall("[^\w\d,.]+", username) + re_list = re.findall(r"[^\w\d,.]+", username) if re_list: print("|-Error: username cannot contain special characters: {}".format(" ".join(re_list))) return @@ -423,13 +423,13 @@ def set_panel_username(username = None): sql.table('users').where('id=?',(1,)).setField('username',username) print(public.GetMsg("NEW_NAME",(username,))) return; - + username = sql.table('users').where('id=?',(1,)).getField('username') - if username == 'admin': + if username == 'admin': username = public.GetRandomString(8).lower() sql.table('users').where('id=?',(1,)).setField('username',username) print('username: ' + username) - + #设定idc def setup_idc(): try: @@ -449,7 +449,7 @@ def setup_idc(): titleNew = (pInfo['brand'] + public.GetMsg("PANEL")).encode('utf-8') if os.path.exists(tFile): title = public.GetConfigValue('title') - if title == '宝塔Linux面板' or title == '': + if title == 'aaPanel' or title == '': public.writeFile(tFile,titleNew) public.SetConfigValue('title',titleNew) else: @@ -494,6 +494,8 @@ def bt_cli(u_input = 0): print("(23) %s (16) %s"% ("Turn off BasicAuth Authenticator","Repair panel (check for errors and update panel files to the latest version)")) print("(24) Turn off Google Authenticator (17) Set log cutting on/off compression") print("(25) Save copy when modify file in panel (18) Set whether to back up the panel automatically") + # if not os.path.exists('/www/server/panel/data/panel_pro.pl'): + # print(" (19) Update to aapanel pro version") print("(26) Keep/Remove local backup when backing up to cloud storage") print("(27) Turn on/off panel SSL (28) Modify panel security entrance") print("(0) Cancel") @@ -550,7 +552,7 @@ def bt_cli(u_input = 0): exit() except: pass - nums = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,22,23,24,25,26,27,28] + nums = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,22,23,24,25,26,27,28] if not u_input in nums: print(raw_tip) print(public.GetMsg("CANCELLED")) @@ -606,6 +608,7 @@ def bt_cli(u_input = 0): print('Security entrance set successfully:{}'.format(admin_path)) if u_input == 1: + if os.path.exists('data/plugin_bin.pl'):os.remove('data/plugin_bin.pl') os.system("/etc/init.d/bt restart") elif u_input == 2: os.system("/etc/init.d/bt stop") @@ -643,7 +646,7 @@ def bt_cli(u_input = 0): if not re.match(rep, input_mysql): print(public.GetMsg("PASS_SPECIAL_CHARACTRES_ERR")) return; - + print(input_mysql) set_mysql_root(input_mysql.strip()) elif u_input == 8: @@ -715,8 +718,15 @@ def bt_cli(u_input = 0): elif u_input == 15: ClearSystem() elif u_input == 16: - os.system("/www/server/panel/pyenv/bin/pip install cachelib") - os.system("curl -k https://node.aapanel.com/install/update6_en.sh|bash") + pro_path = '/www/server/panel/data/panel_pro.pl' + if os.path.exists(pro_path): + print("|-Updating aapanel version to pro version...") + os.system("curl -k https://node.aapanel.com/install/update_pro_en.sh|bash") + else: + # os.system("/www/server/panel/pyenv/bin/pip install cachelib") + only_update_pyenv312 = '/tmp/only_update_pyenv312.pl' + if os.path.exists(only_update_pyenv312): os.remove(only_update_pyenv312) + os.system("curl -k https://node.aapanel.com/install/update_7.x_en.sh|bash") elif u_input == 17: l_path = '/www/server/panel/data/log_not_gzip.pl' if os.path.exists(l_path): @@ -737,6 +747,10 @@ def bt_cli(u_input = 0): print("|-Detected that the panel automatic backup function is turned on and is closing...") public.writeFile(l_path,'True') print("|-Panel auto-backup function turned off") + elif u_input == 19: + if os.path.exists('/tmp/update_to7.pl'):os.remove('/tmp/update_to7.pl') + print("|-Updating aapanel version to pro version...") + os.system("curl -k https://node.aapanel.com/install/update_pro_en.sh|bash") elif u_input == 22: os.system('tail -100 /www/server/panel/logs/error.log') elif u_input == 23: @@ -768,6 +782,45 @@ def bt_cli(u_input = 0): os.mknod(keep_local) +# 旧的插件系统升级到新的插件系统 +def upgrade_plugins(): + print("====================================================") + print(public.GetMsg("PLUG_UPDATEING")) + print("====================================================") + exlodes = ['gitlab', 'pm2', 'mongodb', 'deployment_jd', 'logs', 'docker', 'beta', 'btyw'] + for pname in os.listdir('plugin/'): + if not os.path.isdir('plugin/' + pname): continue + if pname in exlodes: continue + + print("|-upgrading [ %s ]..." % pname) + + try: + # 查找是否存在主程序SO文件 + specified_so_file = 'plugin/{plugin_name}/{plugin_name}_main.cpython-{major}{minor}m-x86_64-linux-gnu.so'.format(plugin_name=pname, major=sys.version_info.major, minor=sys.version_info.minor) + if os.path.isfile(specified_so_file): + # 存在SO文件则将其删除 + os.remove(specified_so_file) + + so_file = 'plugin/{plugin_name}/{plugin_name}_main.so'.format(plugin_name=pname) + if os.path.isfile(so_file): + # 存在SO文件则将其删除 + os.remove(so_file) + + # 检查主程序py文件是否为空 + main_file = 'plugin/{plugin_name}/{plugin_name}_main.py'.format(plugin_name=pname) + if os.path.isfile(main_file) and os.path.getsize(main_file) < 10: + # 主程序py文件为空时,重新下载py文件 + public.re_download_main(pname) + + print(" \033[32m[success]\033[0m") + except Exception as e: + print(" \033[31m[fail] {}\033[0m".format(str(e))) + upgrade_plugins_exists = '/www/server/panel/data/upgrade_plugins_3.12.pl' + public.writeFile(upgrade_plugins_exists, 'True') + print("====================================================") + print("\033[32m" + public.GetMsg("PLUG_UPDATE_TO_6") + "\033[0m") + print("====================================================") + if __name__ == "__main__": type = sys.argv[1] @@ -799,5 +852,7 @@ if __name__ == "__main__": except: clinum = sys.argv[2] bt_cli(clinum) + elif type == "upgrade_plugins": + upgrade_plugins() else: print('ERROR: Parameter error') diff --git a/webserver/tpls/webserver.conf b/webserver/tpls/webserver.conf new file mode 100644 index 00000000..26ef6006 --- /dev/null +++ b/webserver/tpls/webserver.conf @@ -0,0 +1,100 @@ +user www www; +worker_processes 1; +error_log /www/server/panel/webserver/logs/error.log crit; +pid /www/server/panel/webserver/logs/webserver.pid; +worker_rlimit_nofile 512; + +events + {{ + use epoll; + worker_connections 8192; + multi_accept on; + }} + +http + {{ + include mime.types; + default_type application/octet-stream; + server_names_hash_bucket_size 512; + client_header_buffer_size 32k; + large_client_header_buffers 4 32k; + client_max_body_size 50m; + client_body_buffer_size 512k; + + proxy_connect_timeout 3600; + proxy_read_timeout 3600; + proxy_send_timeout 3600; + proxy_buffer_size 32k; + proxy_buffers 4 64k; + proxy_busy_buffers_size 128k; + proxy_temp_file_write_size 128k; + proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; + + sendfile on; + tcp_nopush on; + keepalive_timeout 3600; + tcp_nodelay on; + + gzip on; + gzip_min_length 256; + gzip_buffers 16 16k; + gzip_http_version 1.1; + gzip_comp_level 3; + gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/json image/jpeg image/gif image/png font/ttf font/otf image/svg+xml application/xml+rss text/x-js; + gzip_vary on; + gzip_proxied expired no-cache no-store private auth; + gzip_disable "MSIE [1-6]\."; + + server_tokens off; + access_log /dev/null; + + server + {{ +{LISTEN} + server_name _; + + # 错误页 + error_page 502 /error_page/502.json; + + # rewrite + rewrite ^/site/static/(.*) /static/$1 last; + + {HTTP3_HEADER} + add_header X-Quic "h3"; + + # 静态文件 + location ~ ^/static/ {{ + root /www/server/panel/BTPanel; + expires 24h; + }} + + # 图标处理 + location ~ ^/static\/img/ {{ + root /www/server/panel/BTPanel; + try_files $uri /static/img/soft_ico/icon_plug.svg; + expires 24h; + }} + + #PROXY-START/ + location / + {{ + proxy_pass http://unix:/tmp/panel.sock; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-Port $remote_port; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + # proxy_http_version 1.1; + + {HTTP3_HEADER} + add_header X-Quic "h3"; + }} + #PROXY-END/ + +{SSL_CONFIG} + + access_log /dev/null; + error_log /www/server/panel/webserver/logs/webserver.log; + }} +}} + diff --git a/webserver/tpls/webserver_listen.conf b/webserver/tpls/webserver_listen.conf new file mode 100644 index 00000000..edde8f75 --- /dev/null +++ b/webserver/tpls/webserver_listen.conf @@ -0,0 +1,2 @@ + listen {PORT} default_server; + listen [::]:{PORT}; \ No newline at end of file diff --git a/webserver/tpls/webserver_listen_ssl.conf b/webserver/tpls/webserver_listen_ssl.conf new file mode 100644 index 00000000..f5af7688 --- /dev/null +++ b/webserver/tpls/webserver_listen_ssl.conf @@ -0,0 +1,6 @@ + listen {PORT} quic reuseport; + listen {PORT} ssl default_server; + listen [::]:{PORT} ssl; + listen [::]:{PORT} quic reuseport; + http2 on; + http3 on; \ No newline at end of file diff --git a/webserver/tpls/webserver_ssl.conf b/webserver/tpls/webserver_ssl.conf new file mode 100644 index 00000000..2ef626a1 --- /dev/null +++ b/webserver/tpls/webserver_ssl.conf @@ -0,0 +1,16 @@ + # HTTPS安全配置 + proxy_cookie_path / "/; httponly; secure; SameSite=Lax"; + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"; + + #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则 + ssl_certificate /www/server/panel/ssl/certificate.pem; + ssl_certificate_key /www/server/panel/ssl/privateKey.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 1m; + add_header Strict-Transport-Security "max-age=31536000"; + add_header X-Frame-Options SAMEORIGIN; + error_page 497 https://$http_host$request_uri; + #SSL-END \ No newline at end of file