Update to v7.52.0

This commit is contained in:
aapanel.com
2025-09-26 14:22:35 +08:00
parent 51580549e6
commit 51ce0647bf
1378 changed files with 11443 additions and 23123 deletions
+9 -3
View File
@@ -398,7 +398,7 @@ class acme_v2:
# 构造域名列表
def format_domains(self, domains):
if type(domains) != list:
if not isinstance(domains, list):
return []
# 是否自动构造通配符
if self._auto_wildcard:
@@ -2932,7 +2932,7 @@ fullchain.pem Paste into certificate input box
from hashlib import md5
try:
md5_obj = md5()
body = f"{auth_to}{domains}"
body = f"{auth_to.rstrip('/')}{domains}"
md5_obj.update(body.encode("utf-8"))
self._log_file = f"{self._log_path}/{md5_obj.hexdigest()}.log"
if not os.path.exists(self._log_path):
@@ -3077,8 +3077,14 @@ fullchain.pem Paste into certificate input box
continue
# other site ssl
site_path = ssl.auth_info.get("auth_to")
site_path = ssl.auth_info.get("auth_to").rstrip("/")
site_info = public.S("sites").where("path=?", site_path).find()
if not site_info:
site_name, _ = self.get_site_name_by_domains(ssl.dns)
if site_name:
site_info = public.S("sites").where("name=?", site_name).find()
site_path = site_info.get("path", "") if site_info else site_path
site_name = site_info.get("name") if site_info else ""
if self._check_site(site_path, site_info, ssl):
# try http verfication
+1 -1
View File
@@ -48,7 +48,7 @@ class panelSetup:
if ua.find('spider') != -1 or g.ua.find('bot') != -1:
return abort(403)
g.version = '7.49.0'
g.version = '7.52.0'
g.title = public.GetConfigValue('title')
g.uri = request.path
g.debug = os.path.exists('data/debug.pl')
+6 -2
View File
@@ -7,7 +7,7 @@
# | Author: hwliang <hwl@aapanel.com>
# +-------------------------------------------------------------------
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"
@@ -148,6 +148,7 @@ class crontab:
#检查环境
def checkBackup(self):
from BTPanel import cache
if cache.get('check_backup'): return None
# 检查备份表是否正确
@@ -189,7 +190,10 @@ class crontab:
return public.return_msg_gettext(False, public.lang("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])))
public.WriteLog(
'TYPE_CRON',
"Modified cron job [{}] status to [{}]".format(cronInfo['name'], str(status_msg[status]))
)
return public.return_msg_gettext(True, public.lang("Setup successfully!"))
#修改计划任务
+1 -1
View File
@@ -198,7 +198,7 @@ class firewalls:
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,))
public.WriteLog("TYPE_FIREWALL", 'Successfully accepted port [{}]!'.format(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()
+12 -8
View File
@@ -239,13 +239,13 @@ def clear_other_files():
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/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))
# 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)
@@ -285,8 +285,8 @@ def clear_other_files():
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/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'):
@@ -321,6 +321,9 @@ def sql_pacth():
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', '%service_type%')).count():
public.M('sites').execute("alter TABLE sites add service_type TEXT DEFAULT ''", ())
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'",())
@@ -765,6 +768,7 @@ def files_set_mode():
["/dev/shm/session_py3","","root",600,True],
["/dev/shm/session_py2","","root",600,True],
["/www/server/phpmyadmin","","root",755,True],
["/www/server/adminer","","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],
+215 -89
View File
@@ -1,61 +1,82 @@
#coding: utf-8
#-------------------------------------------------------------------
# coding: utf-8
# -------------------------------------------------------------------
# aaPanel
#-------------------------------------------------------------------
# -------------------------------------------------------------------
# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved.
#-------------------------------------------------------------------
# -------------------------------------------------------------------
# Author: hwliang <hwl@aapanel.com>
#-------------------------------------------------------------------
# -------------------------------------------------------------------
#------------------------------
# ------------------------------
# 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 os
import re
import socket
import time
from http.cookies import SimpleCookie
import requests
import urllib3.util.connection as urllib3_conn
from BTPanel import request, Response, public, app, get_phpmyadmin_dir, session
class HttpProxy:
_pma_path = None
def get_res_headers(self,p_res):
'''
@staticmethod
def _err_resp(msg: str = None):
return Response(
msg or "something wrong with socket, please cheak and try again...", 500
)
def get_res_headers(self, p_res):
"""
@name 获取响应头
@author hwliang<2022-01-19>
@param p_res<Response> requests响应对像
@return dict
'''
"""
headers = {}
for h in p_res.headers.keys():
if h in ['content-encoding', 'Content-Encoding', 'transfer-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', 'Location']:
# ============ redirect ===================
# phpmyadmin
if headers[h].find('phpmyadmin_') != -1:
if not self._pma_path:
if not self._pma_path:
self._pma_path = get_phpmyadmin_dir()
if self._pma_path:
if self._pma_path:
self._pma_path = self._pma_path[0]
else:
self._pma_path = ''
headers[h] = headers[h].replace(self._pma_path,'phpmyadmin')
headers[h] = headers.get(h, "").replace(self._pma_path, 'phpmyadmin')
# adminer
elif headers[h].find("adminer_") != -1:
from adminer.manager import AdminerManager
adminer_dir, _ = AdminerManager().adminer_dir_port
headers[h] = headers.get(h, "").replace(adminer_dir, 'adminer')
# ============ redirect end ==================
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])
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://')
headers[h] = headers.get(h, '').replace('http://', 'https://')
return headers
def set_res_headers(self,res,p_res):
'''
def set_res_headers(self, res, p_res):
"""
@name 设置响应头
@author hwliang<2022-01-19>
@param res<Response> flask响应对像
@param p_res<Response> requests响应对像
@return res<Response>
'''
"""
# from datetime import datetime
# cookie_dict = p_res.cookies.get_dict()
# expires = datetime.utcnow() + app.permanent_session_lifetime
@@ -65,15 +86,15 @@ class HttpProxy:
# 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:
@@ -81,11 +102,11 @@ class HttpProxy:
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()
@@ -93,11 +114,11 @@ class HttpProxy:
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
@@ -105,19 +126,19 @@ class HttpProxy:
old_phpversion = self.get_pma_phpversion()
if not old_phpversion: return False
if pma_version == '4.0':
php_versions = ['52','53','54']
php_versions = ['52', '53', '54']
elif pma_version == '4.4':
php_versions = ['54','55','56']
php_versions = ['54', '55', '56']
elif pma_version == '4.9':
php_versions = ['55','56','70','71','72','73','74']
php_versions = ['55', '56', '70', '71', '72', '73', '74']
elif pma_version == '5.0':
php_versions = ['70','71','72','73','74']
php_versions = ['70', '71', '72', '73', '74']
elif pma_version == '5.1':
php_versions = ['71','72','73','74','80']
php_versions = ['71', '72', '73', '74', '80']
elif pma_version == '5.2':
php_versions = ['72','73','74','80','81']
php_versions = ['72', '73', '74', '80', '81']
elif pma_version == '5.3':
php_versions = ['72','73','74','80','81']
php_versions = ['72', '73', '74', '80', '81']
else:
return False
@@ -138,92 +159,94 @@ class HttpProxy:
args = public.dict_obj()
args.phpversion = php_version
ajax.ajax().setPHPMyAdmin(args)
public.WriteLog('Database','The PHP version used by phpMyAdmin has been detected to be incompatible and has been automatically changed to the best compatible version: PHP-' + php_version)
public.WriteLog(
'Database',
'The PHP version used by phpMyAdmin has been detected to be incompatible and has been automatically changed to the best compatible version: 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']
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':
# noinspection PyUnresolvedReferences
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()
del (cookie_dict[rm_cookie])
headers[k] = cookie_dict.output(header='', sep=';').strip()
return headers
def form_to_dict(self,form):
'''
def form_to_dict(self, form):
"""
@name 将表单转为字典
@author hwliang<2022-02-18>
@param form<request.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):
'''
def proxy(self, proxy_url: str, allow_redirects: bool = False):
"""
@name 代理指定URL地址
@author hwliang<2022-01-19>
@param proxy_url<string> 被代理的URL地址
@return Response
'''
"""
try:
urllib3_conn.allowed_gai_family = lambda: socket.AF_INET
s_key = 'proxy_{}_{}'.format(app.secret_key,self.get_pma_version())
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'
'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'})
session[s_key].cookies.update({'pma_lang_https': 'zh_CN'})
else:
session[s_key].cookies.update({'pma_lang':'zh_CN'})
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()
session[s_key].headers['Host'] = public.en_punycode(
request.url_root
).replace('http://', '').replace('https://', '').split('/')[0]
except:
pass
headers = None
if request.method == 'GET':
# 转发GET请求
p_res = session[s_key].get(proxy_url,headers=headers,verify=False,allow_redirects=False)
p_res = session[s_key].get(
proxy_url, headers=headers, verify=False, allow_redirects=allow_redirects
)
elif request.method == 'POST':
# 转发POST请求
if request.files: # 如果上传文件
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'])
if not os.path.exists(tmp_path): os.makedirs(tmp_path, 384)
# 遍历form表单中的所有文件
files = {}
f_list = {}
@@ -231,47 +254,150 @@ class HttpProxy:
upload_files = request.files.getlist(key)
filename = upload_files[0].filename
if not filename: filename = public.GetRandomString(12)
tmp_file = '{}/{}'.format(tmp_path,filename)
tmp_file = '{}/{}'.format(tmp_path, filename)
# 保存上传文件到临时目录
with open(tmp_file,'wb') as f:
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')
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)
p_res = session[s_key].post(
proxy_url,
self.form_to_dict(request.form),
headers=headers,
files=files,
verify=False,
allow_redirects=allow_redirects
)
# 释放文件对象
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)
p_res = session[s_key].post(
proxy_url,
self.form_to_dict(request.form),
headers=headers,
verify=False,
allow_redirects=allow_redirects
)
else:
return Response('不支持的请求类型',500)
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:
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'<strong>Deprecation Notice</strong>') != -1 and not session.get('set_pma_phpversion'):
elif p_res.content.find(b'<strong>Deprecation Notice</strong>') != -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)
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)
err_msg = re.sub(r"adminer_\S+", "adminer_...", str(ex))
err_msg = re.sub(r"phpmyadmin_\S+", "phpmyadmin_...", err_msg)
return Response(err_msg, 500)
# todo未完善
def proxy_socket(self, proxy_url: str, allow_redirects: bool = False):
"""
@name socket代理
@param proxy_url http+unix://<socket_path>/<request_uri>
@return Response
"""
try:
if not proxy_url.startswith("http+unix://"):
return self._err_resp(
"Socket proxy error: proxy_url format error. It should start with 'http+unix://'"
)
from urllib.parse import urlparse, urlunparse, quote
try:
from requests_unixsocket import Session as ux_Session
except ImportError:
public.ExecShell("btpip install requests_unixsocket")
try:
# noinspection PyUnresolvedReferences
from requests_unixsocket import Session as ux_Session
except:
return self._err_resp("The 'requests_unixsocket' module is not installed")
parsed_url = urlparse(proxy_url)
if parsed_url.scheme == "http+unix":
full_path = parsed_url.netloc + parsed_url.path
if not full_path.startswith("/"):
full_path = "/" + full_path
socket_ext = ".sock"
socket_pos = full_path.find(socket_ext)
if socket_pos != -1:
socket_path_end = socket_pos + len(socket_ext)
socket_path = full_path[:socket_path_end]
request_uri = full_path[socket_path_end:]
if not request_uri:
request_uri = "/"
encoded_socket_path = quote(socket_path, safe="")
# format: http+unix://<socket_path>/<request_uri>
proxy_url = urlunparse((
parsed_url.scheme,
encoded_socket_path,
request_uri,
parsed_url.params,
parsed_url.query,
parsed_url.fragment
))
else:
return self._err_resp("Socket proxy error: Invalid socket proxy URL format.")
sess = ux_Session()
headers = self.get_request_headers()
if request.method == "GET":
p_res = sess.get(
proxy_url, headers=headers, timeout=10, allow_redirects=allow_redirects
)
elif request.method == "POST":
data = self.form_to_dict(request.form)
files = None
if request.files:
files = {}
for key in request.files:
fs = request.files.getlist(key)[0]
files[key] = (fs.filename, fs.stream)
p_res = sess.post(
proxy_url, data=data, files=files, headers=headers, timeout=10, allow_redirects=allow_redirects
)
else:
return self._err_resp(f"Unsupported method: {request.method}")
return Response(
p_res.content,
status=p_res.status_code,
headers=headers,
content_type=p_res.headers.get("content-type", None)
)
except Exception as ex:
return self._err_resp(f"Socket proxy error: {str(ex)}")
+22 -8
View File
@@ -5015,11 +5015,17 @@ location %s
#取当站点前运行目录
def GetSiteRunPath(self,get):
siteName = public.M('sites').where('id=?',(get.id,)).getField('name')
sitePath = public.M('sites').where('id=?',(get.id,)).getField('path')
site = public.M('sites').where('id=?',(get.id,)).field('name,path,service_type').find()
if not site: return {"runPath": "/", 'dirs': []}
siteName = site['name']
sitePath = site['path']
if not siteName or os.path.isfile(sitePath): return {"runPath":"/",'dirs':[]}
path = sitePath
if public.get_webserver() == 'nginx':
# 兼容多服务
webserver = public.get_webserver()
if public.get_multi_webservice_status():
webserver = site['service_type'] if site['service_type'] else 'nginx'
if webserver == 'nginx':
filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf'
if os.path.exists(filename):
conf = public.readFile(filename)
@@ -5028,7 +5034,7 @@ location %s
if not path:
return public.return_msg_gettext(False, public.lang("Get Site run path false"))
path = path.groups()[0]
elif public.get_webserver() == 'apache':
elif webserver == 'apache':
filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf'
if os.path.exists(filename):
conf = public.readFile(filename)
@@ -5720,7 +5726,12 @@ RewriteRule \.(BTPFILE)$ /404.html [R,NC]
@author hwliang<2022-01-14>
@return bool
'''
default_conf_body = '''<VirtualHost *:80>
port_80 = '80'
port_443 = '443'
if public.get_multi_webservice_status():
port_443 = '8290'
port_80 = '8288'
default_conf_body = f'''<VirtualHost *:{port_80}>
ServerAdmin webmaster@example.com
DocumentRoot "/www/server/apache/htdocs"
ServerName bt.default.com
@@ -5733,7 +5744,7 @@ RewriteRule \.(BTPFILE)$ /404.html [R,NC]
DirectoryIndex index.html
</Directory>
</VirtualHost>
<VirtualHost *:443>
<VirtualHost *:{port_443}>
ServerAdmin webmaster@example.com
DocumentRoot "/www/server/apache/htdocs"
ServerName ssl.default.com
@@ -5765,7 +5776,10 @@ RewriteRule \.(BTPFILE)$ /404.html [R,NC]
@author hwliang<2022-01-14>
@return bool
'''
default_conf_body = '''<VirtualHost *:80>
port = '80'
if public.get_multi_webservice_status():
port = '8290'
default_conf_body = f'''<VirtualHost *:{port}>
ServerAdmin webmaster@example.com
DocumentRoot "/www/server/apache/htdocs"
ServerName bt.default.com
+9 -3
View File
@@ -1,11 +1,17 @@
# coding: utf-8
from .fields import *
from .manager import Q, QueryProperty
from .model import aaModel
__version__ = "1.1.2"
# from .file_model import *
__version__ = "1.1.3"
__all__ = [
"aaModel", "Q", "QueryProperty",
"__version__",
# "DictFileModel",
# "ListFileModel",
"QueryProperty",
"aaModel",
"Q",
] + fields.__all__
+37 -28
View File
@@ -1,12 +1,12 @@
# coding: utf-8
import json
import sys
import os
import sqlite3 as Engine
import uuid
from functools import reduce
from itertools import chain
from typing import Optional, TypeVar, Generic, Any, List, Dict, Generator, Iterable
from public import ExecShell
from public.aaModel.fields import COMPARE
from public.exceptions import HintException, PanelError
from public.sqlite_easy import Db
@@ -16,37 +16,48 @@ __all__ = ["aaManager", "Q", "QueryProperty"]
M = TypeVar("M", bound="aaModel")
def get_flag() -> bool:
import sqlite3
# ==================== Patch ==================
def _builtin(check_engine: Engine = None) -> bool:
if not check_engine:
check_engine = Engine
try:
conn = sqlite3.connect(":memory:")
conn = check_engine.connect(":memory:")
cursor = conn.cursor()
cursor.execute("SELECT json_extract('{\"a\": 1}', '$.a')")
cursor.execute("SELECT COUNT(*) FROM json_each('{\"a\":1, \"b\":2}')")
conn.close()
return True
except sqlite3.Error:
return False
except:
return False
def _setup():
if get_flag():
return
def _get_engine() -> tuple[bool, Engine]:
try:
import pysqlite3
sys.modules["sqlite3"] = pysqlite3
except ImportError:
import pysqlite3 as engine
flag = True
except:
try:
ExecShell("btpip install pysqlite3-binary")
import pysqlite3
sys.modules["sqlite3"] = pysqlite3
os.system("btpip install pysqlite3-binary")
import pysqlite3 as engine
flag = True
except:
pass
engine = Engine
flag = False
return flag, engine
# _setup()
_ENGINE = None
_INSTEAD = False
_ORG = _builtin()
if not _ORG:
_INSTEAD, _ENGINE = _get_engine()
else:
_ENGINE = Engine
# ==================== Patch End ==================
class QueryProperty:
@@ -58,20 +69,13 @@ class QueryProperty:
class Operator:
_shared_flag = None
def __new__(cls, *args, **kwargs):
if cls._shared_flag is None:
cls._shared_flag = get_flag()
return super().__new__(cls)
def __init__(self, model_class: M, query: Db.query):
self._model_class: M = model_class
self._query = query
self._tb = self._model_class.__table_name__
self._fields = self._model_class._get_fields()
self._serializes = self._model_class._get_serialized()
self._flag = self._shared_flag # fk flag
self._flag = _ORG or _INSTEAD # fk flag
def _q_error(self, key: str, act: str, val: Any, sp_act: tuple):
raise HintException(
@@ -760,7 +764,10 @@ class aaObjects(Generic[M]):
@property
def _query(self) -> Db.query:
if not self.__q:
q = Db(self._model.__db_name__).query()
q = Db(
db_name=self._model.__db_name__,
engine=_ENGINE,
).query()
self.__q = q.table(self._model.__table_name__)
return self.__q
@@ -856,7 +863,9 @@ class aaMigrate:
raise PanelError(f"{self.__model.__class__.__name__} need 'fields'")
try:
self.__client = Db(self.__model.__db_name__)
self.__client = Db(
db_name=self.__model.__db_name__, engine=_ENGINE
)
self.__table_exists()
self.__index_exists()
except Exception as e:
+231 -34
View File
@@ -1,26 +1,40 @@
# 公共模块
# @author Zhj<2024/06/15>
import base64
import binascii
import contextlib
import json, os, sys, time, re, socket, importlib, binascii, base64, io, string, psutil, requests
import fnmatch
import gettext
import gzip
import importlib
import json
import os
import psutil
import re
import shutil
import socket
import string
import sys
import tempfile
import threading
import time
import typing
from datetime import datetime
from typing import Any, List, Set
import fcntl
import werkzeug.datastructures
from typing import Any, List
import public
from .exceptions import PanelError
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, search_sql_special_chars
from .tools import is_number
from .structures import aap_t_simple_result, aap_t_mysql_dump_info, aap_t_http_multipart
import gzip
import fcntl
import shutil
import tempfile
from datetime import datetime
from .regexplib import *
from .sqlite_easy import Db, SqliteEasy
import threading
from .structures import *
from .tools import is_number
from .validate import Param, trim_filter
aap_t_simple_result = aap_t_simple_result
aap_t_mysql_dump_info = aap_t_mysql_dump_info
path = "/www/server/panel/BTPanel/languages/language.pl"
if os.path.exists(path):
@@ -608,11 +622,12 @@ def WriteLog(type, logMsg, args=(), not_web=False):
if not _LAN_LOG:
_LAN_LOG = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/log.json'))
keys = _LAN_LOG.keys()
if type in keys:
type = _LAN_LOG[type]
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)
@@ -722,6 +737,27 @@ def getMsg(key, args=()):
# 获取Web服务器
def GetWebServer():
# 优先从请求头获取(仅在 Flask 请求上下文中)
try:
from flask import has_request_context
except Exception:
has_request_context = lambda: False
if has_request_context():
try:
from flask import request
header_val = request.headers.get('Aap-Web-Server')
if header_val:
v = str(header_val).strip().lower()
# 支持常见别名
if v in ('nginx', 'apache', 'openlitespeed', 'ols'):
# 规范化返回 openlitespeed 名称
if v == 'ols':
return 'openlitespeed'
return v
except Exception:
pass
nginxSbin = '{}/nginx/sbin/nginx'.format(get_setup_path())
apacheBin = '{}/apache/bin/apachectl'.format(get_setup_path())
olsBin = '/usr/local/lsws/bin/lswsctrl'
@@ -744,16 +780,49 @@ def get_webserver():
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')
# 获取多服务状态和安装路径
is_multi = get_multi_webservice_status()
setup_path = get_setup_path()
# 定义服务操作映射
services = [
(
f"{setup_path}/nginx/sbin/nginx",
"/etc/init.d/nginx reload",
"pkill -9 nginx && sleep 1 && /etc/init.d/nginx start"
),
(
f"{setup_path}/apache/bin/apachectl",
"/etc/init.d/httpd reload",
None
),
(
"/usr/local/lsws/bin/lswsctrl",
"rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart",
None
)
]
result = None
# 多服务模式:遍历所有服务并执行
if is_multi:
for path, cmd, err_cmd in services:
if os.path.exists(path):
result = ExecShell(cmd)
# 处理nginx pid异常
if "nginx" in path and result[1].find("nginx.pid") != -1:
result = ExecShell(err_cmd)
# 单服务模式:找到第一个存在的服务执行
else:
result = ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart')
for path, cmd, err_cmd in services:
if os.path.exists(path):
result = ExecShell(cmd)
# 处理nginx pid异常
if "nginx" in path and result[1].find("nginx.pid") != -1:
result = ExecShell(err_cmd)
break # 只执行第一个匹配的服务
return result
@@ -1788,10 +1857,18 @@ def checkIp(ip):
# 检查端口是否合法
def checkPort(port):
if not is_number(port): return False
ports = ['21', '25', '443', '8080', '888', '8888', '8443', '7800']
ports = [
'21', '25', '443', '8080', '888', '999', '8888', '8443', '7800', '8188', '8189', '8288', '8289', '8290'
]
if port in ports: return False
intport = int(port)
if intport < 1 or intport > 65535: return False
# 判断端口占用,避免多服务崩溃
res = ExecShell(f'lsof -i :{port} -P -n -l -F pnc')
if res[0] and port != '80':
return False
return True
@@ -2213,7 +2290,7 @@ def load_module(pluginCode):
# 解密数据
def auth_decode(data):
token = GetToken()
token: dict = GetToken()
# 是否有生成Token
if not token: return returnMsg(False, 'REQUEST_ERR')
@@ -3184,11 +3261,14 @@ def auto_backup_panel():
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, 'wp_package_checksums', 'wp_packages','maillog', 'mail'))
shutil.copytree(panel_paeh + '/config', backup_path + '/config')
shutil.copytree(panel_paeh + '/vhost', backup_path + '/vhost')
ignore_list = [
ignore_system, ignore_default,
'wp_package_checksums', 'wp_packages', 'maillog', 'mail', '*.sock'
]
cp_dir(f"{panel_paeh}/data", f"{backup_path}/data", ignores=ignore_list)
cp_dir(f"{panel_paeh}/config", f"{backup_path}/config")
cp_dir(f"{panel_paeh}/vhost", f"{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)
@@ -3446,6 +3526,9 @@ def get_site_php_version(siteName):
@return string
'''
web_server = get_webserver()
if public.get_multi_webservice_status():
site = public.M('sites').where('name = ?',siteName).field('service_type').find()
web_server = site['service_type'] if site['service_type'] else 'nginx'
vhost_path = get_vhost_path()
conf = readFile(vhost_path + '/' + web_server + '/' + siteName + '.conf')
if web_server == 'openlitespeed':
@@ -5371,7 +5454,7 @@ def check_site_path(site_path):
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']
whites = ['/www/server/tomcat', '/www/server/stop', '/www/server/phpmyadmin', '/www/server/adminer']
for w in whites:
if site_path.find(w) == 0: return True
a, error_paths = get_sys_path()
@@ -8174,7 +8257,6 @@ def check_area_panel():
@name: 检查地区限制
@return:
'''
import contextlib
areas_dict = get_limit_area()
# 关闭状态直接返回false
@@ -9445,4 +9527,119 @@ def split_domain_sld(domain: str):
if len(parts) <= num_of_tld_parts + 1:
return domain, ""
else:
return ".".join(parts[-num_of_tld_parts-1:]), ".".join(parts[:-num_of_tld_parts-1])
return ".".join(parts[-num_of_tld_parts-1:]), ".".join(parts[:-num_of_tld_parts-1])
# 获取多服务状态
def get_multi_webservice_status():
nginxSbin = '{}/nginx/sbin/nginx'.format(get_setup_path())
apacheBin = '{}/apache/bin/apachectl'.format(get_setup_path())
olsBin = '/usr/local/lsws/bin/lswsctrl'
if os.path.exists(nginxSbin) and (os.path.exists(apacheBin) or os.path.exists(olsBin)):
return True
return False
# 获取已存在的服务
def get_multi_webservice_list() -> list:
nginxSbin = '{}/nginx/sbin/nginx'.format(get_setup_path())
apacheBin = '{}/apache/bin/apachectl'.format(get_setup_path())
olsBin = '/usr/local/lsws/bin/lswsctrl'
server_list = []
if os.path.exists(nginxSbin):
server_list.append('nginx')
if os.path.exists(apacheBin):
server_list.append('apache')
if os.path.exists(olsBin):
server_list.append('openlitespeed')
return server_list
# 操作指定web服务
def webservice_operation(service: str, type = 'restart') -> bool:
"""
service 服务名称
type 类型关闭重启开启
"""
try:
import system_v2
server_restart = system_v2.system()
get = public.to_dict_obj({
'name': service,
'type': type
})
ok = server_restart.ServiceAdmin(get)
if ok.get('status') == 0:
return True
return False
except Exception as e:
print(e)
return False
# Base64URL 编码
def base64url_encode(data: bytes) -> str:
import base64
return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')
# Base64URL 解码
def base64url_decode(data: str) -> bytes:
import base64
padding = '=' * (4 - (len(data) % 4)) if len(data) % 4 != 0 else ''
return base64.urlsafe_b64decode(data + padding)
def cp_dir(src: str, dst: str, ignores: List[str] | Set[str] = None, overwrite: bool = True, ) -> None:
"""
site 递归复制文件夹
src: 源路径
dst: 目标路径
ignore: 忽略的 list
overwrite: 存在时是否覆盖
ps: ignore内的每个对象支持
*匹配任意数量的任意字符包括零个字符
?匹配任意单个字符
[abc]匹配序列 abc中的任意字符
[!abc]匹配不在序列 abc中的任意字符
"""
if not os.path.exists(src):
return
overwrite = False if isinstance(overwrite, int) and overwrite == 0 else True
ignores = set(ignores) if ignores else set()
# 确保目标目录存在
if not os.path.exists(dst):
os.makedirs(dst, 0o755, exist_ok=True)
def _copy2(src_file: str, dst_file: str):
if overwrite or not os.path.exists(dst_file):
try:
shutil.copy2(src_file, dst_file)
except Exception:
lock = False
try:
a, e = ExecShell(f"lsattr -d {dst_file}")
if not e and "i" in a:
lock = True
ExecShell(f"chattr -i {dst_file}")
shutil.copy2(src_file, dst_file)
except:
pass
finally:
if lock is True and os.path.exists(dst_file):
ExecShell(f"chattr +i {dst_file}")
# 复制源目录下的所有内容到目标目录
for item in os.listdir(src):
src_item = os.path.join(src, item)
dst_item = os.path.join(dst, item)
if ignores and any(fnmatch.fnmatch(item, ignore) for ignore in ignores):
continue
if os.path.isdir(src_item):
cp_dir(src_item, dst_item, ignores, overwrite)
else:
_copy2(src_item, dst_item)
+16
View File
@@ -1,5 +1,21 @@
import re
__all__ = [
'match_ipv4',
'match_ipv6',
'match_safe_path',
'match_class_private_property',
'match_based_host',
'find_url_root',
'search_php_first_fatal_error',
'search_http_response_status_line',
'match_general_version_format',
'match_md5_format',
'search_sql_special_chars',
'match_email',
]
# 匹配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}|:))')
+15 -12
View File
@@ -887,15 +887,17 @@ class DbConnection:
@name Sqlite数据库连接类(相比Db类更加底层)
@author Zhj<2022-12-13>
'''
__slots__ = ['__DB_NAME', '__DB_PATH', '__DB_LOCK_FILE', '__CONN', '__DEBUG_LOG']
__slots__ = ['__DB_NAME', '__DB_PATH', '__DB_LOCK_FILE', '__CONN', '__DEBUG_LOG', 'ENGINE']
def __init__(self, db_name):
def __init__(self, db_name, engine=None):
'''
@name 初始化函数
@author Zhj<2022-12-14>
@param db_name<string> 数据库名称(全路径 不包含.db)
@return void
'''
self.ENGINE = sqlite3 if engine is None else engine
self.__DB_NAME = db_name
self.__DB_PATH = '{}.db'.format(db_name)
@@ -961,11 +963,11 @@ class DbConnection:
# 连接sqlite
def connect(self):
if isinstance(self.__CONN, sqlite3.Connection):
if isinstance(self.__CONN, self.ENGINE.Connection):
return self.__CONN
# 连接数据库(写)
self.__CONN = sqlite3.connect(self.__DB_PATH, timeout=15, check_same_thread=False)
self.__CONN = self.ENGINE.connect(self.__DB_PATH, timeout=15, check_same_thread=False)
self.__CONN.text_factory = str
self.__CONN.isolation_level = 'IMMEDIATE'
@@ -975,7 +977,7 @@ class DbConnection:
@name 关闭sqlite连接
'''
# 关闭sqlite数据库连接
if isinstance(self.__CONN, sqlite3.Connection):
if isinstance(self.__CONN, self.ENGINE.Connection):
try:
self.__CONN.close()
except BaseException as e:
@@ -995,7 +997,7 @@ class DbConnection:
@name 提交事务
@return bool
'''
if isinstance(self.__CONN, sqlite3.Connection) and self.__CONN.in_transaction:
if isinstance(self.__CONN, self.ENGINE.Connection) and self.__CONN.in_transaction:
self.__CONN.commit()
return True
@@ -1007,7 +1009,7 @@ class DbConnection:
@name 回滚事务
@return bool
'''
if isinstance(self.__CONN, sqlite3.Connection) and self.__CONN.in_transaction:
if isinstance(self.__CONN, self.ENGINE.Connection) and self.__CONN.in_transaction:
self.__CONN.rollback()
return True
@@ -1022,9 +1024,9 @@ class DbConnection:
try:
return fn(*args, **kwargs)
except (
SystemError, KeyError, sqlite3.InterfaceError, sqlite3.InternalError, sqlite3.OperationalError) as e:
SystemError, KeyError, self.ENGINE.InterfaceError, self.ENGINE.InternalError, self.ENGINE.OperationalError) as e:
# 数据库操作错误,不是锁协议错误,直接抛出异常
if isinstance(e, sqlite3.OperationalError) and str(e) not in ['locking protocol',
if isinstance(e, self.ENGINE.OperationalError) and str(e) not in ['locking protocol',
'database is locked']:
raise e
@@ -1299,11 +1301,11 @@ class Db:
@name Sqlite数据库连接类
@author Zhj<2022-07-18>
'''
__slots__ = ['__DB_NAME', '__DB_CONN', '__AUTO_COMMIT', '__AUTO_VACUUM', '__NEED_VACUUM', '__DEBUG_LOG', '__QUERIES']
__slots__ = ['__DB_NAME', '__DB_CONN', '__AUTO_COMMIT', '__AUTO_VACUUM', '__NEED_VACUUM', '__DEBUG_LOG', '__QUERIES', 'ENGINE']
__DB_ROOT_DIR = '{}/data/'.format(_BASE_DIR)
def __init__(self, db_name):
def __init__(self, db_name, engine=None):
'''
@name 初始化函数
@author Zhj<2022-12-14>
@@ -1311,6 +1313,7 @@ class Db:
@param brandnew<bool> 是否开启一个全新连接
@return void
'''
self.ENGINE = sqlite3 if engine is None else engine
if str(db_name).startswith(':memory:'):
self.__DB_NAME = db_name # 内存数据库
else:
@@ -1340,7 +1343,7 @@ class Db:
@author Zhj<2022-07-16>
@return self
'''
self.__DB_CONN = DbConnection(self.__DB_NAME)
self.__DB_CONN = DbConnection(db_name=self.__DB_NAME, engine=self.ENGINE)
return self
def close(self):
+6
View File
@@ -1,5 +1,11 @@
import collections
__all__ = [
'aap_t_simple_result',
'aap_t_mysql_dump_info',
'aap_t_http_multipart',
]
# 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'])
+3 -2
View File
@@ -61,8 +61,9 @@ class userRegister:
else:
sUrl = '{}/api/user/register_on_panel'.format(public.OfficialApiBase())
aa = public.httpPost(sUrl, params, timeout=60)
aa = public.httpPost(sUrl, params, headers={
'X-Forwarded-For': public.GetClientIp(),
}, timeout=60)
data = json.loads(aa)