Files
aaPanel/mod/base/push_mod/site_push.py
T
Jack ed55fa708d Update to 7.7.0
Since version 7.7.0, we recommend yours update python to 3.12.

[+] Using nginx technology to load static files improves access speed
[+] Refactor homepage, website, FTP, and database using vue3
[+] Table loading changed to skeleton screen
[+] Add Website statistics-v2 professional plug-in
[+] Add Home page - top 5 resource occupancy
[+] Add protection for Files management (requires Tamper-proof for Enterprise 3.7)
[+] Website, FTP, Databases page add program status
[+] Add FTP log analysis (only supports Centos)
[+] Add password-free login to phpMyAdmin
[+] Add Proxy Project in Website (Supported when web service uses Nginx)
[+] Add WP Toolkit (Pro version only)
[+] Redesigned Docker module
[+] Add WP Toolkit Protection
[+] Add WP Toolkit Backup and Restore
[+] Add WP Toolkit Migrated
[+] Add WP Toolkit Clone site (supports new domain and subdomain)
[+] Add WP Toolkit Create site from backup of other panel
[+] Add WP Toolkit support for Cron automatic backup (only save Local disk)
[+] Add WP Toolkit operation log
[+] Add Integrity check for WP Toolkit
[+] Add WP Toolkit plug-in management and themes management

[*] Optimize phpMyAdmin formula access method
[*] Optimize Home page PHP display problem
[*] Optimize jump to the login interface after the login expires
[*] Optimize automatic renewal of SSL at some times
[*] Optimize Let's Encrypt to increase application success rate

[-] Fix Logs Audit cannot be opened
[-] Fix apache URL rewrite issue
[-] Fix phpmyadmin installation problem
[-] Fix the problem that some servers cannot install software
[-] Fix upload file error
[-] Fix left menu hiding problem
[-] Fix aaPanel Mobile QR code display problem
[-] Fix problem that third-party plug-ins are not displayed in the App Store
[-] Fix issue where the menu bar is blank when opening new tabs
[-] Fixed panel not being accessible in some cases
[-] Fix the issue where Curl warning caused the inability to apply for SSL
[-] Fix Quota issues for Website, FTP, Databases
[-] Fix file interface display problem on mobile terminal
2024-07-19 11:25:10 +08:00

1291 lines
45 KiB
Python

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 = ['>即将到期:<font color=#ff0000>{} 张</font>'.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<? AND (status=? OR status=?)',
('0000-00-00', m_end_date, 1, u'正在运行')
).field('id,name, edate').select()
print(web_list)
print(m_end_date)
if not (isinstance(web_list, list) and len(web_list) >= 1):
return None
total = self._task_config.get_by_id(task_id).get("number_rule", {}).get("total", 1)
s_list = ['>即将到期:<font color=#ff0000>{} 个站点</font>'.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 = [">告警类型:登录密码即将过期",
">剩余天数:<font color=#ff0000>{} 天</font>".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登录失败告警",
">告警内容:<font color=#ff0000>{} 分钟内登录失败超过 {} 次</font> ".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(
">登录用户变更:<font color=#ff0000>总 {} 个,新增 {} 个 ,删除 {} 个</font>.".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(">面板日志发生删除,删除条数:<font color=#ff0000>{} 条</font>".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 "<unknown>"
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: "<span>剩余时间小于{}{}</span>".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: "<span>面板登录时,发出告警</span>"
),
"7": (
lambda x: "<span>检测到SSH登录本机时,发出告警</span>"
),
"4": (
lambda x: "<span>{}分钟内连续{}次失败登录触发,每{}秒后再次检测</span>".format(
x["task_data"].get("cycle"), x["task_data"].get("count"), x["task_data"].get("interval"),
)
),
"5": (
lambda x: "<span>服务停止时发送一次通知,{}秒后再次检测</span>".format(x["task_data"].get("interval"))
),
"9": (
lambda x: "<span>项目停止时发送通知,{}秒后再次检测,每日发送{}次</span>".format(
x["task_data"].get("interval"),
x.get("number_rule", {}).get("day_num", 0))
),
"6": (
lambda x: "<span>面板出现如:用户变更、面板日志删除、开启开发者等危险操作时发送告警</span>"
),
"10": (
lambda x: "<span>检测到新的版本时发送一次通知</span>"
)
}
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