mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-17 21:25:47 +02:00
Update to v8.21.0
This commit is contained in:
+1
-1
@@ -47,7 +47,7 @@ class panelSetup:
|
||||
if ua.find('spider') != -1 or g.ua.find('bot') != -1:
|
||||
return abort(403)
|
||||
|
||||
g.version = '8.19.0'
|
||||
g.version = '8.21.0'
|
||||
g.title = public.GetConfigValue('title')
|
||||
g.uri = request.path
|
||||
g.debug = os.path.exists('data/debug.pl')
|
||||
|
||||
@@ -1554,6 +1554,7 @@ listener Default%s{
|
||||
if os.path.exists(backup_cert):
|
||||
shutil.move(backup_cert, path)
|
||||
return public.return_msg_gettext(False, public.lang('ERROR: <br><a style="color:red;">' + isError.replace("\n", '<br>') + '</a>'))
|
||||
self._clear_nginx_proxy_cache()
|
||||
public.serviceReload()
|
||||
|
||||
if os.path.exists(path + '/partnerOrderId'): os.remove(path + '/partnerOrderId')
|
||||
@@ -2110,6 +2111,14 @@ listener SSL443 {
|
||||
# except:
|
||||
# return False;
|
||||
|
||||
# 多服务模式下清除Nginx反向代理缓存并重启(避免部署SSL后重定向过多)
|
||||
def _clear_nginx_proxy_cache(self):
|
||||
if not public.get_multi_webservice_status():
|
||||
return
|
||||
proxy_cache_dir = '/www/server/nginx/proxy_cache_dir'
|
||||
if os.path.isdir(proxy_cache_dir):
|
||||
public.ExecShell('rm -rf /www/server/nginx/proxy_cache_dir/*')
|
||||
|
||||
# HttpToHttps
|
||||
def HttpToHttps(self, get):
|
||||
siteName = get.siteName
|
||||
@@ -2158,6 +2167,7 @@ listener SSL443 {
|
||||
</IfModule>
|
||||
#HTTP_TO_HTTPS_END'''
|
||||
public.writeFile(file, ols_force_https)
|
||||
self._clear_nginx_proxy_cache()
|
||||
public.serviceReload()
|
||||
return public.return_msg_gettext(True, public.lang("Setup successfully!"))
|
||||
|
||||
@@ -2279,6 +2289,7 @@ listener SSL443 {
|
||||
if os.path.exists(p_file): public.ExecShell('rm -f ' + p_file)
|
||||
|
||||
public.write_log_gettext('Site manager', 'Site [{}] turned off SSL successfully!', (siteName,))
|
||||
self._clear_nginx_proxy_cache()
|
||||
public.serviceReload()
|
||||
return public.return_msg_gettext(True, public.lang("SSL turned off!"))
|
||||
|
||||
|
||||
+33
-4
@@ -5702,6 +5702,35 @@ def get_free_ip_info(address):
|
||||
except:
|
||||
pass
|
||||
|
||||
geo2_mmdb = '{}/config/GeoLite2-City.mmdb'.format(get_panel_path())
|
||||
if os.path.exists(geo2_mmdb):
|
||||
try:
|
||||
from geoip2 import database
|
||||
reader = database.Reader(geo2_mmdb)
|
||||
res = reader.city(ip)
|
||||
reader.close()
|
||||
country = res.raw.get('country', {})
|
||||
geo_info = {
|
||||
'continent': '',
|
||||
'country': country.get('country', ''),
|
||||
'province': country.get('province', ''),
|
||||
'city': country.get('city', ''),
|
||||
'region': '',
|
||||
'carrier': '',
|
||||
'division': '',
|
||||
'en_country': country.get('country', ''),
|
||||
'en_short_code': country.get('en_short_code', ''),
|
||||
'longitude': country.get('longitude', ''),
|
||||
'latitude': country.get('latitude', ''),
|
||||
'tag': '',
|
||||
}
|
||||
geo_info['info'] = '{} {} {}'.format(
|
||||
geo_info['country'], geo_info['province'], geo_info['city']
|
||||
).strip()
|
||||
return geo_info
|
||||
except:
|
||||
pass
|
||||
|
||||
return {'info': 'Unknown'}
|
||||
|
||||
|
||||
@@ -5715,11 +5744,11 @@ def free_login_area(login_ip, login_type='panel'):
|
||||
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:
|
||||
# 改英文IP库:用本地 GeoLite2 替代云端免费IP库,避免登录告警/推送出现中文归属地
|
||||
ip_info = get_free_ip_info(login_ip)
|
||||
if not ip_info:
|
||||
return False, {}
|
||||
|
||||
ip_info = ip_info[login_ip]
|
||||
if not 'city' in ip_info:
|
||||
login_ip_area = ip_info['info']
|
||||
status = True
|
||||
@@ -5738,7 +5767,7 @@ def free_login_area(login_ip, login_type='panel'):
|
||||
if city == 'Local':
|
||||
login_ip_area += '(<font color=red>Intranet</font>)'
|
||||
else:
|
||||
login_ip_area += '(<font color=red>Abnormal login</font>)'
|
||||
login_ip_area += '(<font color=red>Public</font>)'
|
||||
data[city] += 1
|
||||
writeFile(s_conf, json.dumps(data))
|
||||
data['login_ip_area'] = login_ip_area
|
||||
|
||||
+94
-39
@@ -150,10 +150,11 @@ class ssh_security:
|
||||
|
||||
def return_profile(self):
|
||||
if os.path.exists('/root/.bash_profile'): return '/root/.bash_profile'
|
||||
if os.path.exists('/root/.profile'): return '/root/.profile'
|
||||
if os.path.exists('/etc/profile'): return '/etc/profile'
|
||||
fd = open('/root/.bash_profil', mode="w", encoding="utf-8")
|
||||
fd = open('/root/.bash_profile', mode="w", encoding="utf-8")
|
||||
fd.close()
|
||||
return '/root/.bash_profil'
|
||||
return '/root/.bash_profile'
|
||||
|
||||
def return_bashrc(self):
|
||||
if os.path.exists('/root/.bashrc'):return '/root/.bashrc'
|
||||
@@ -230,8 +231,34 @@ class ssh_security:
|
||||
return data
|
||||
|
||||
################## SSH 登陆报警设置 ####################################
|
||||
def send_mail_data(self,title,body,type=None):
|
||||
def send_mail_data(self, title, body, type=None, login_ip=""):
|
||||
try:
|
||||
# 优先走告警任务系统: 发送到 ssh_login 任务配置的所有通道(含 Telegram),
|
||||
# 而非仅 ssh_send_type.pl 中的单通道 (旧逻辑只发一个通道, 导致 tg 收不到)
|
||||
try:
|
||||
import sys
|
||||
if "/www/server/panel" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel")
|
||||
from mod.base.push_mod import push_by_task_keyword
|
||||
push_data = {
|
||||
"msg_list": ['>Send content:' + body],
|
||||
"login_ip": login_ip
|
||||
}
|
||||
res = push_by_task_keyword("ssh_login", "ssh_login", push_data=push_data)
|
||||
# 任务存在时 res 为 dict (已按任务配置发送到所有通道), 直接结束
|
||||
if isinstance(res, dict):
|
||||
if public.is_debug():
|
||||
public.print_log("SSH login alert -> task system OK, result: {}".format(
|
||||
json.dumps(res, ensure_ascii=False, default=str)))
|
||||
return
|
||||
else:
|
||||
if public.is_debug():
|
||||
public.print_log("SSH login alert -> task not used, push_by_task_keyword returned: {}".format(res))
|
||||
except Exception as _e:
|
||||
if public.is_debug():
|
||||
public.print_log("SSH login alert -> task system exception: {}".format(_e))
|
||||
|
||||
# 回退: 旧单通道逻辑
|
||||
login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl"
|
||||
if not os.path.exists(login_send_type_conf):
|
||||
return
|
||||
@@ -316,6 +343,13 @@ class ssh_security:
|
||||
|
||||
#获取ROOT当前登陆的IP
|
||||
def get_ip(self):
|
||||
ssh_conn = os.environ.get('SSH_CONNECTION', '')
|
||||
if ssh_conn:
|
||||
client_ip = ssh_conn.split()[0]
|
||||
if re.match(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]?)$", client_ip):
|
||||
return [client_ip]
|
||||
|
||||
# Compatible with old logic: extract IP from who am i
|
||||
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
|
||||
@@ -354,21 +388,26 @@ class ssh_security:
|
||||
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
|
||||
ip = self.get_ip()
|
||||
if not ip or len(ip) == 0 or len(ip[0]) == 0:
|
||||
if public.is_debug():
|
||||
public.print_log("SSH login alert -> unable to get login IP, skip")
|
||||
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
|
||||
if public.M('logs').where('type=? AND 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]))
|
||||
if public.M('logs').where('type=? AND addtime=?', ('SSH security', mDate,)).count(): return False
|
||||
self.send_mail_data('Server {} login alarm'.format(public.GetLocalIp()),' Login alarm triggered on server {}. Login IP: {}, login user: root'.format(public.GetLocalIp(),ip[0]), login_ip=ip[0])
|
||||
public.WriteLog('SSH security','Login alarm triggered on server {}. Login IP: {}, login user: root'.format(public.GetLocalIp(),ip [0]))
|
||||
return True
|
||||
except:
|
||||
except Exception as e:
|
||||
if public.is_debug():
|
||||
public.print_log("SSH login alert -> exception in login(): {}".format(e))
|
||||
pass
|
||||
|
||||
|
||||
@@ -384,49 +423,64 @@ class ssh_security:
|
||||
public.WriteFile(self.return_bashrc(),datassss.replace(self.return_python(),''))
|
||||
|
||||
|
||||
#开启监控
|
||||
#清理所有候选profile里的钩子,避免多文件重复告警
|
||||
def _clean_profile(self):
|
||||
# 清除所有候选 profile 文件中的钩子(bash_profile / profile / etc/profile)
|
||||
for profile in ('/root/.bash_profile', '/root/.profile', '/etc/profile'):
|
||||
if not os.path.exists(profile):
|
||||
continue
|
||||
data = public.ReadFile(profile)
|
||||
if not data:
|
||||
continue
|
||||
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(profile,data)
|
||||
#检查是否还存在遗留
|
||||
if re.search(self.return_python()+' /www/server/panel/class/ssh_security.py', data):
|
||||
public.WriteFile(profile,data.replace(self.return_python()+' /www/server/panel/class/ssh_security.py login',''))
|
||||
#遗留的错误信息
|
||||
datassss = public.ReadFile(profile)
|
||||
if re.search(self.return_python(),datassss):
|
||||
public.WriteFile(profile,datassss.replace(self.return_python(),''))
|
||||
|
||||
# 开启监控(当前生效的是 v1 实现;class_v2/ssh_security_v2.py 未启用)
|
||||
def start_jian(self,get):
|
||||
self.repair_bashrc()
|
||||
# 若存在用户级 profile(.bash_profile/.profile) 且 /etc/profile 里有旧钩子,则调用 _clean_profile 清除
|
||||
if os.path.exists('/root/.bash_profile') or os.path.exists('/root/.profile'):
|
||||
ep_data = public.ReadFile('/etc/profile')
|
||||
if ep_data and re.search('/www/server/panel/class/ssh_security.py login', ep_data):
|
||||
self._clean_profile()
|
||||
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())
|
||||
disown $!
|
||||
''' % (self.return_python())
|
||||
public.WriteFile(self.return_profile(), data.strip() + '\n' + cmd)
|
||||
return public.returnMsg(True, public.lang("Open successfully"))
|
||||
return public.returnMsg(False, public.lang("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, public.lang("Closed successfully"))
|
||||
else:
|
||||
return public.returnMsg(True, public.lang("Closed successfully"))
|
||||
self._clean_profile()
|
||||
return public.returnMsg(True, public.lang("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, public.lang("1"))
|
||||
else:
|
||||
return public.returnMsg(False, public.lang("1"))
|
||||
# 检查所有候选 profile 文件(完整路径匹配),避免钩子在其它文件时误报未开启
|
||||
for profile in ('/root/.bash_profile', '/root/.profile', '/etc/profile'):
|
||||
if not os.path.exists(profile):
|
||||
continue
|
||||
data = public.ReadFile(profile) or ''
|
||||
if re.search('/www/server/panel/class/ssh_security.py login', data):
|
||||
return public.returnMsg(True, public.lang("1"))
|
||||
return public.returnMsg(False, public.lang("1"))
|
||||
|
||||
def set_password(self, get):
|
||||
'''
|
||||
@@ -525,7 +579,8 @@ class ssh_security:
|
||||
#取消告警
|
||||
def clear_login_send(self,get):
|
||||
login_send_type_conf = "/www/server/panel/data/ssh_send_type.pl"
|
||||
os.remove(login_send_type_conf)
|
||||
if os.path.exists(login_send_type_conf):
|
||||
os.remove(login_send_type_conf)
|
||||
self.stop_jian(get)
|
||||
return public.returnMsg(True, public.lang("Successfully cancel the login alarm!"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user