mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-17 21:25:47 +02:00
Update to v8.16.0
This commit is contained in:
+40
-8
@@ -3354,6 +3354,8 @@ fullchain.pem Paste into certificate input box
|
||||
continue
|
||||
continue
|
||||
write_log(f"|- Domain Subject:【{ssl.subject}】Trying to use DNS Verification for Renewal!")
|
||||
# dns 续签成功会销毁旧 ssl, 后续判断用快照
|
||||
ssl_snapshot = {"user_for": dict(ssl.user_for or {}), "subject": ssl.subject, "hash": ssl.hash}
|
||||
try:
|
||||
dns_apply = self.apply_cert_domain(
|
||||
domains=ssl.dns,
|
||||
@@ -3364,14 +3366,16 @@ fullchain.pem Paste into certificate input box
|
||||
)
|
||||
if dns_apply.get("status"):
|
||||
write_log(
|
||||
f"|- Domain Subject:【{ssl.subject}】DNS Verification "
|
||||
f"|- Domain Subject:【{ssl_snapshot['subject']}】DNS Verification "
|
||||
f"Renewal SSL certificate Successfully!"
|
||||
)
|
||||
# 类sub_all_cert: 面板证书续签后部署到面板SSL目录
|
||||
self._deploy_panel_after_renew(self._renewed_ssl_of(dns_apply), ssl_snapshot)
|
||||
else:
|
||||
raise Exception(dns_apply.get("msg"))
|
||||
except Exception as e:
|
||||
write_log(
|
||||
f"|- Domain Subject:【{ssl.subject}】DNS Verification "
|
||||
f"|- Domain Subject:【{ssl_snapshot['subject']}】DNS Verification "
|
||||
f"Renewal SSL certificate Failed:{str(e)}"
|
||||
)
|
||||
if self.http_fallback_renew(ssl):
|
||||
@@ -3386,8 +3390,13 @@ fullchain.pem Paste into certificate input box
|
||||
"""HTTP-01 最终兜底续签"""
|
||||
if any("*." in d for d in ssl.dns):
|
||||
return False
|
||||
ssl_snapshot = {
|
||||
"user_for": dict(ssl.user_for or {}),
|
||||
"subject": ssl.subject,
|
||||
"hash": ssl.hash,
|
||||
}
|
||||
write_log(
|
||||
f"|- Domain Subject:【{ssl.subject}】Trying HTTP-01 fallback verification..."
|
||||
f"|- Domain Subject:【{ssl_snapshot['subject']}】Trying HTTP-01 fallback verification..."
|
||||
)
|
||||
try:
|
||||
from ssl_domainModelV2.service import HttpFallback
|
||||
@@ -3400,22 +3409,45 @@ fullchain.pem Paste into certificate input box
|
||||
)
|
||||
if http_res.get("status"):
|
||||
write_log(
|
||||
f"|- Domain Subject:【{ssl.subject}】HTTP-01 fallback ({mode}) "
|
||||
f"|- Domain Subject:【{ssl_snapshot['subject']}】HTTP-01 fallback ({mode}) "
|
||||
f"renewal SSL certificate successfully!"
|
||||
)
|
||||
renewed_ssl = self._renewed_ssl_of(http_res)
|
||||
# 仅 webroot 模式保存 auth_info(站点路径持久可用)
|
||||
if mode == "webroot":
|
||||
ssl.auth_info = {"auth_type": "http", "auth_to": auth_to}
|
||||
ssl.save()
|
||||
if mode == "webroot" and renewed_ssl:
|
||||
renewed_ssl.auth_info = {"auth_type": "http", "auth_to": auth_to}
|
||||
renewed_ssl.save()
|
||||
# 类sub_all_cert: 面板证书续签后部署到面板SSL目录
|
||||
self._deploy_panel_after_renew(renewed_ssl, ssl_snapshot)
|
||||
return True
|
||||
else:
|
||||
raise Exception(http_res.get("msg"))
|
||||
finally:
|
||||
fallback.cleanup()
|
||||
except Exception as e:
|
||||
write_log(f"|- Domain Subject:【{ssl.subject}】HTTP-01 fallback failed: {str(e)}")
|
||||
write_log(f"|- Domain Subject:【{ssl_snapshot['subject']}】HTTP-01 fallback failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def _renewed_ssl_of(self, apply_res: dict):
|
||||
"""按 apply_cert_domain 返回的新证书算 hash, 定位续签后的新记录; 旧 ssl 可能已被 keep_same_dns_ssl_unique 销毁"""
|
||||
from ssl_domainModelV2.model import DnsDomainSSL
|
||||
from ssl_domainModelV2.service import CertHandler
|
||||
new_hash = CertHandler.get_hash(
|
||||
(apply_res.get("cert") or "") + (apply_res.get("root") or "")
|
||||
)
|
||||
return DnsDomainSSL.objects.filter(hash=new_hash).first() if new_hash else None
|
||||
|
||||
def _deploy_panel_after_renew(self, renewed_ssl, ssl_snapshot: dict):
|
||||
"""续签成功后部署面板证书; 站点/邮件由 sub_all_cert 自动替换, 仅 panel 需显式部署"""
|
||||
if ssl_snapshot["user_for"].get("panel") != ["panel"]:
|
||||
return
|
||||
if renewed_ssl and renewed_ssl.hash != ssl_snapshot["hash"]:
|
||||
renewed_ssl.deploy_panel()
|
||||
write_log(
|
||||
f"|- Domain Subject:【{ssl_snapshot['subject']}】Panel SSL "
|
||||
f"certificate deployed successfully!"
|
||||
)
|
||||
|
||||
def apply_cert_domain(
|
||||
self,
|
||||
domains: list,
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ from flask import session,request
|
||||
|
||||
import public,os,json,time,apache,psutil
|
||||
class ajax:
|
||||
__official_url = 'https://www.aapanel.com'
|
||||
__official_url = public.OfficialApiBase()
|
||||
|
||||
def GetApacheStatus(self,get):
|
||||
a = apache.apache()
|
||||
@@ -886,7 +886,7 @@ class ajax:
|
||||
|
||||
# 下载云端php扩展配置
|
||||
def _get_cloud_phplib(self):
|
||||
if not session.get('download_url'): session['download_url'] = 'https://node.aapanel.com'
|
||||
if not session.get('download_url'): session['download_url'] = public.OfficialDownloadBase()
|
||||
download_url = session['download_url'] + '/install/lib/phplib_en.json'
|
||||
tstr = public.httpGet(download_url)
|
||||
data = json.loads(tstr)
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ class panelSetup:
|
||||
if ua.find('spider') != -1 or g.ua.find('bot') != -1:
|
||||
return abort(403)
|
||||
|
||||
g.version = '8.10.0'
|
||||
g.version = '8.16.0'
|
||||
g.title = public.GetConfigValue('title')
|
||||
g.uri = request.path
|
||||
g.debug = os.path.exists('data/debug.pl')
|
||||
@@ -136,7 +136,7 @@ class panelAdmin(panelSetup):
|
||||
session['brand'] = public.GetConfigValue('brand')
|
||||
session['product'] = public.GetConfigValue('product')
|
||||
session['rootPath'] = '/www'
|
||||
session['download_url'] = 'https://node.aapanel.com'
|
||||
session['download_url'] = public.OfficialDownloadBase()
|
||||
session['setupPath'] = session['rootPath'] + '/server'
|
||||
session['logsPath'] = '/www/wwwlogs'
|
||||
session['yaer'] = datetime.now().year
|
||||
|
||||
+79
-2
@@ -20,6 +20,53 @@ try:
|
||||
from BTPanel import session,admin_path_checks,g,request,cache
|
||||
import send_mail
|
||||
except:pass
|
||||
|
||||
_JSON_UNSAFE = object()
|
||||
|
||||
|
||||
def _json_safe_value(value, depth=0):
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if depth >= 4:
|
||||
return _JSON_UNSAFE
|
||||
if isinstance(value, (list, tuple)):
|
||||
safe_list = []
|
||||
for item in value:
|
||||
safe_item = _json_safe_value(item, depth + 1)
|
||||
if safe_item is not _JSON_UNSAFE:
|
||||
safe_list.append(safe_item)
|
||||
return safe_list
|
||||
if isinstance(value, dict):
|
||||
safe_dict = {}
|
||||
for key, item in value.items():
|
||||
safe_item = _json_safe_value(item, depth + 1)
|
||||
if safe_item is not _JSON_UNSAFE:
|
||||
safe_dict[str(key)] = safe_item
|
||||
return safe_dict
|
||||
return _JSON_UNSAFE
|
||||
|
||||
|
||||
def _get_panel_ssl_switch_session():
|
||||
session_data = {}
|
||||
for key, value in dict(session).items():
|
||||
safe_value = _json_safe_value(value)
|
||||
if safe_value is not _JSON_UNSAFE:
|
||||
session_data[str(key)] = safe_value
|
||||
return session_data
|
||||
|
||||
|
||||
def _reload_panel_webserver():
|
||||
try:
|
||||
import webserver
|
||||
return webserver.webserver().run_webserver()
|
||||
except Exception as e:
|
||||
try:
|
||||
public.print_log('reload panel webserver failed: {}'.format(e))
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
class config:
|
||||
_setup_path = "/www/server/panel"
|
||||
_key_file = _setup_path+"/data/two_step_auth.txt"
|
||||
@@ -981,6 +1028,27 @@ class config:
|
||||
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
|
||||
try:
|
||||
switch_token = public.GetRandomString(48)
|
||||
switch_data = {
|
||||
"token": switch_token,
|
||||
"expires": int(time.time()) + 180,
|
||||
"client_ip": public.GetClientIp(),
|
||||
"user_agent": request.headers.get('User-Agent', ''),
|
||||
"session": _get_panel_ssl_switch_session()
|
||||
}
|
||||
public.writeFile('{}/data/panel_ssl_switch.json'.format(public.get_panel_path()), json.dumps(switch_data))
|
||||
g.panel_ssl_switch_token = switch_token
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
from flask import current_app
|
||||
current_app.config['SSL'] = False
|
||||
current_app.config['SESSION_COOKIE_SECURE'] = False
|
||||
except:
|
||||
pass
|
||||
_reload_panel_webserver()
|
||||
public.reload_panel()
|
||||
return public.return_msg_gettext(True, 'SSL turned off,Please use http protocol to access the panel!')
|
||||
else:
|
||||
public.ExecShell('btpip install cffi')
|
||||
@@ -1001,6 +1069,15 @@ class config:
|
||||
except:
|
||||
return public.return_msg_gettext(False,
|
||||
'Error, unable to auto install pyOpenSSL!<p>Plesea try to manually install: pip install pyOpenSSL</p>')
|
||||
try:
|
||||
from flask import current_app
|
||||
current_app.config['SSL'] = True
|
||||
current_app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
current_app.config['SESSION_COOKIE_SECURE'] = True
|
||||
except:
|
||||
pass
|
||||
_reload_panel_webserver()
|
||||
public.reload_panel()
|
||||
return public.return_msg_gettext(True,
|
||||
'SSL is turned on, plesea use https protocol to access the panel!')
|
||||
#自签证书
|
||||
@@ -1041,7 +1118,7 @@ class config:
|
||||
"access_key": 'B' * 32,
|
||||
"panel": 1
|
||||
}
|
||||
cert_api = 'https://api.aapanel.com/aapanel_cert'
|
||||
cert_api = f'{public.OfficialApiUrlBase()}/aapanel_cert'
|
||||
result = json.loads(public.httpPost(cert_api, {'data': json.dumps(pdata)}))
|
||||
if 'status' in result:
|
||||
if result['status']:
|
||||
@@ -3714,7 +3791,7 @@ class config:
|
||||
# 提交
|
||||
if not public.cache_get(pkey):
|
||||
try:
|
||||
public.run_thread(public.httpPost("https://geterror.aapanel.com/bt_error/index.php", error_infos))
|
||||
public.run_thread(public.httpPost(f"{public.OfficialGetErrorBase()}/bt_error/index.php", error_infos))
|
||||
public.cache_set(pkey, 1, 1800)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
+24
-2
@@ -29,6 +29,23 @@ class DictCache(object):
|
||||
self.data[key] = value
|
||||
|
||||
|
||||
def _get_current_session_cookie(app):
|
||||
try:
|
||||
sid = getattr(session, 'sid', None)
|
||||
if not sid:
|
||||
return request.cookies.get(app.config['SESSION_COOKIE_NAME'], '')
|
||||
if app.config.get('SESSION_USE_SIGNER'):
|
||||
from itsdangerous import want_bytes
|
||||
signer = app.session_interface._get_signer(app)
|
||||
signed_sid = signer.sign(want_bytes(sid))
|
||||
if not isinstance(signed_sid, str):
|
||||
signed_sid = signed_sid.decode()
|
||||
return signed_sid
|
||||
return sid
|
||||
except:
|
||||
return request.cookies.get(app.config['SESSION_COOKIE_NAME'], '')
|
||||
|
||||
|
||||
class Compress(object):
|
||||
"""
|
||||
The Compress object allows your application to use Flask-Compress.
|
||||
@@ -91,19 +108,24 @@ class Compress(object):
|
||||
|
||||
if 'rm_ssl' in g:
|
||||
import public
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=0'
|
||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||
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()
|
||||
session_id = _get_current_session_cookie(app)
|
||||
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)
|
||||
response.set_cookie(session_name, session_id, path='/', max_age=86400 * 30,httponly=True,samesite='Lax')
|
||||
|
||||
request_token = request.cookies.get('request_token','')
|
||||
if request_token:
|
||||
response.set_cookie('request_token',request_token,path='/',max_age=86400 * 30)
|
||||
switch_token = getattr(g, 'panel_ssl_switch_token', '')
|
||||
if switch_token:
|
||||
response.set_cookie('panel_ssl_switch', switch_token, path='/', max_age=180, httponly=True, samesite='Lax')
|
||||
|
||||
if (response.mimetype not in app.config['COMPRESS_MIMETYPES'] or
|
||||
'gzip' not in accept_encoding.lower() or
|
||||
|
||||
+16
-1
@@ -80,6 +80,17 @@ def control_init_new():
|
||||
control_init_delay()
|
||||
|
||||
def install_packages():
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
public.ExecShell("btpip install pydantic==2.5.3 openai==1.39.0")
|
||||
import openai
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
public.ExecShell("btpip install numpy==1.21.6")
|
||||
import numpy
|
||||
|
||||
try:
|
||||
import dns.resolver
|
||||
except ImportError:
|
||||
@@ -352,6 +363,10 @@ def sql_pacth():
|
||||
if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%parent_id%')).count():
|
||||
public.M('sites').execute("alter TABLE sites add parent_id STRING DEFAULT 0",())
|
||||
|
||||
# 添加备份类型字段,0为标准类型,1为全量备份
|
||||
if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'backup','%backup_type%')).count():
|
||||
public.M('backup').execute("alter TABLE backup add backup_type STRING DEFAULT 0",())
|
||||
|
||||
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'",())
|
||||
|
||||
@@ -1032,7 +1047,7 @@ def update_py312():
|
||||
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))
|
||||
public.ExecShell("nohup curl -k {}/install/update_panel_en.sh|bash &>/tmp/panelUpdate.pl &".format(download_url))
|
||||
return True
|
||||
|
||||
def test_ping():
|
||||
|
||||
@@ -118,7 +118,7 @@ class wx_account_msg:
|
||||
|
||||
def get_web_info(self,get):
|
||||
if self.user_info is None: return public.returnMsg(False, public.lang("The user binding information was not obtained"))
|
||||
url = "https://wafapi2.aapanel.com/api/v2/user/wx_web/info"
|
||||
url = f"{public.OfficialWaf2Base()}/api/v2/user/wx_web/info"
|
||||
data = {
|
||||
"uid": self.user_info["uid"],
|
||||
"access_key": 'B' * 32,
|
||||
@@ -140,7 +140,7 @@ class wx_account_msg:
|
||||
|
||||
def get_web_info2(self):
|
||||
if self.user_info is None: return public.returnMsg(False, public.lang("The user binding information was not obtained"))
|
||||
url = "https://wafapi2.aapanel.com/api/v2/user/wx_web/info"
|
||||
url = f"{public.OfficialWaf2Base()}/api/v2/user/wx_web/info"
|
||||
data = {
|
||||
"uid": self.user_info["uid"],
|
||||
"access_key": 'B' * 32,
|
||||
@@ -160,7 +160,7 @@ class wx_account_msg:
|
||||
|
||||
def get_auth_url(self,get):
|
||||
if self.user_info is None: return public.returnMsg(False, public.lang("The user binding information was not obtained"))
|
||||
url = "https://wafapi2.aapanel.com/api/v2/user/wx_web/get_auth_url"
|
||||
url = f"{public.OfficialWaf2Base()}/api/v2/user/wx_web/get_auth_url"
|
||||
data = {
|
||||
"uid": self.user_info["uid"],
|
||||
"access_key": 'B' * 32,
|
||||
@@ -220,7 +220,7 @@ class wx_account_msg:
|
||||
return public.returnMsg(False, public.lang("No user information was obtained"))
|
||||
|
||||
msg,title = self.get_send_msg(msg)
|
||||
url="https://wafapi2.aapanel.com/api/v2/user/wx_web/send_template_msg_v2"
|
||||
url=f"{public.OfficialWaf2Base()}/api/v2/user/wx_web/send_template_msg_v2"
|
||||
datassss = {
|
||||
"first": {
|
||||
"value": "堡塔主机告警",
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# aaPanel
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
import re
|
||||
import json
|
||||
import socket
|
||||
import urllib.parse
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
import urllib3.util.connection as urllib3_conn
|
||||
|
||||
from BTPanel import request, Response, public, app, session
|
||||
from mod.project.mail.billionmailMod import main as BillionMailMod
|
||||
|
||||
|
||||
class BillionMailProxy:
|
||||
HOP_BY_HOP_HEADERS = frozenset((
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"content-length",
|
||||
"content-encoding",
|
||||
))
|
||||
PANEL_COOKIES = (
|
||||
"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",
|
||||
)
|
||||
|
||||
def __init__(self, proxy_prefix="/billionmail"):
|
||||
self.proxy_prefix = "/" + proxy_prefix.strip("/")
|
||||
|
||||
@staticmethod
|
||||
def _error(message, status=500):
|
||||
return Response(message, status=status)
|
||||
|
||||
def _clean_cookie(self, cookie_header):
|
||||
if not cookie_header:
|
||||
return ""
|
||||
cookie_dict = SimpleCookie(cookie_header)
|
||||
remove_names = list(self.PANEL_COOKIES)
|
||||
session_cookie_name = app.config.get("SESSION_COOKIE_NAME")
|
||||
if session_cookie_name:
|
||||
remove_names.append(session_cookie_name)
|
||||
for cookie_name in remove_names:
|
||||
if cookie_name in cookie_dict:
|
||||
del cookie_dict[cookie_name]
|
||||
return cookie_dict.output(header="", sep=";").strip()
|
||||
|
||||
def _request_headers(self, target_base):
|
||||
headers = {}
|
||||
target = urllib.parse.urlparse(target_base)
|
||||
for key in request.headers.keys():
|
||||
lower_key = key.lower()
|
||||
if lower_key in self.HOP_BY_HOP_HEADERS or lower_key == "host":
|
||||
continue
|
||||
value = request.headers.get(key)
|
||||
if lower_key == "cookie":
|
||||
value = self._clean_cookie(value)
|
||||
if not value:
|
||||
continue
|
||||
elif lower_key == "origin":
|
||||
value = "{}://{}".format(target.scheme, target.netloc)
|
||||
elif lower_key == "referer":
|
||||
value = self._rewrite_request_referer(value, target_base)
|
||||
headers[key] = value
|
||||
|
||||
headers["Accept-Encoding"] = "identity"
|
||||
headers["X-Forwarded-Host"] = request.host
|
||||
headers["X-Forwarded-Proto"] = request.scheme
|
||||
headers["X-Forwarded-Prefix"] = self.proxy_prefix
|
||||
headers["X-Real-IP"] = request.headers.get("X-Real-IP", request.remote_addr or "")
|
||||
return headers
|
||||
|
||||
def _rewrite_request_referer(self, value, target_base):
|
||||
if not value:
|
||||
return value
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(value)
|
||||
path = parsed.path or "/"
|
||||
if path == self.proxy_prefix or path.startswith(self.proxy_prefix + "/"):
|
||||
new_path = path[len(self.proxy_prefix):] or "/"
|
||||
target = urllib.parse.urlparse(target_base)
|
||||
return urllib.parse.urlunparse((
|
||||
target.scheme,
|
||||
target.netloc,
|
||||
new_path,
|
||||
"",
|
||||
parsed.query,
|
||||
parsed.fragment,
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
|
||||
def _target_url(self, target_base, path_full):
|
||||
path = "/" + (path_full or "").lstrip("/")
|
||||
path = urllib.parse.quote(path, safe="/:@!$&'()*+,;=-._~%")
|
||||
query = request.query_string.decode("latin-1")
|
||||
url = target_base.rstrip("/") + path
|
||||
if query:
|
||||
url += "?" + query
|
||||
return url
|
||||
|
||||
def _request_kwargs(self, target_base):
|
||||
kwargs = {
|
||||
"headers": self._request_headers(target_base),
|
||||
"verify": False,
|
||||
"timeout": 300,
|
||||
"allow_redirects": False,
|
||||
}
|
||||
content_type = request.headers.get("Content-Type", "").lower()
|
||||
if content_type.startswith("multipart/form-data"):
|
||||
files = []
|
||||
for key in request.files:
|
||||
for storage in request.files.getlist(key):
|
||||
files.append((
|
||||
key,
|
||||
(
|
||||
storage.filename,
|
||||
storage.stream,
|
||||
storage.content_type,
|
||||
),
|
||||
))
|
||||
form = {}
|
||||
for key in request.form.keys():
|
||||
values = request.form.getlist(key)
|
||||
form[key] = values if len(values) > 1 else values[0]
|
||||
if files:
|
||||
kwargs["files"] = files
|
||||
kwargs["data"] = form
|
||||
else:
|
||||
body = request.get_data()
|
||||
if body:
|
||||
kwargs["data"] = body
|
||||
return kwargs
|
||||
|
||||
def _rewrite_location(self, value, target_base):
|
||||
if not value:
|
||||
return value
|
||||
target = urllib.parse.urlparse(target_base)
|
||||
parsed = urllib.parse.urlparse(value)
|
||||
if parsed.scheme and parsed.netloc:
|
||||
if parsed.hostname in ("127.0.0.1", "localhost", "::1") or parsed.netloc == target.netloc:
|
||||
path = parsed.path or "/"
|
||||
if path == self.proxy_prefix or path.startswith(self.proxy_prefix + "/"):
|
||||
return path + (("?" + parsed.query) if parsed.query else "")
|
||||
return self.proxy_prefix + path + (("?" + parsed.query) if parsed.query else "")
|
||||
return value
|
||||
if value.startswith("/"):
|
||||
if value == self.proxy_prefix or value.startswith(self.proxy_prefix + "/"):
|
||||
return value
|
||||
return self.proxy_prefix + value
|
||||
return value
|
||||
|
||||
def _rewrite_set_cookie(self, value):
|
||||
if not value:
|
||||
return value
|
||||
value = re.sub(r";\s*domain=(127\.0\.0\.1|localhost|\[?::1\]?)[^;]*", "", value, flags=re.I)
|
||||
if re.search(r";\s*path=", value, re.I):
|
||||
return re.sub(r";\s*path=[^;]*", "; Path={}".format(self.proxy_prefix), value, flags=re.I)
|
||||
return value + "; Path={}".format(self.proxy_prefix)
|
||||
|
||||
def _response_headers(self, upstream_response, target_base):
|
||||
headers = []
|
||||
for key, value in upstream_response.headers.items():
|
||||
lower_key = key.lower()
|
||||
if lower_key in self.HOP_BY_HOP_HEADERS or lower_key == "set-cookie":
|
||||
continue
|
||||
if lower_key in ("x-frame-options", "content-security-policy"):
|
||||
continue
|
||||
if lower_key == "location":
|
||||
value = self._rewrite_location(value, target_base)
|
||||
headers.append((key, value))
|
||||
headers.append(("Referrer-Policy", "same-origin"))
|
||||
return headers
|
||||
|
||||
def _set_cookie_headers(self, response, upstream_response):
|
||||
raw_headers = getattr(upstream_response.raw, "headers", None)
|
||||
cookies = []
|
||||
if raw_headers is not None:
|
||||
if hasattr(raw_headers, "get_all"):
|
||||
cookies = raw_headers.get_all("Set-Cookie") or []
|
||||
elif hasattr(raw_headers, "getlist"):
|
||||
cookies = raw_headers.getlist("Set-Cookie") or []
|
||||
if not cookies:
|
||||
cookie_header = upstream_response.headers.get("Set-Cookie")
|
||||
if cookie_header:
|
||||
cookies = [cookie_header]
|
||||
for cookie in cookies:
|
||||
response.headers.add("Set-Cookie", self._rewrite_set_cookie(cookie))
|
||||
return response
|
||||
|
||||
def _safe_redirect(self, value):
|
||||
value = (value or "").strip()
|
||||
if not value or not value.startswith("/") or value.startswith("//"):
|
||||
return self.proxy_prefix + "/"
|
||||
if value == self.proxy_prefix or value.startswith(self.proxy_prefix + "/"):
|
||||
return value
|
||||
return self.proxy_prefix + (value if value != "/" else "/")
|
||||
|
||||
def _sso_bridge(self):
|
||||
service_name = request.args.get("service_name", "")
|
||||
redirect = self._safe_redirect(request.args.get("redirect", ""))
|
||||
apsess_token = self._panel_apsess_token()
|
||||
panel_api_prefix = "/{}".format(apsess_token) if apsess_token else ""
|
||||
csrf_token = public.get_csrf_sess_html_token_value() or ""
|
||||
html = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="referrer" content="same-origin">
|
||||
<title>BillionMail</title>
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||
body { display: flex; align-items: center; justify-content: center; color: #1f2937; background: #f8fafc; }
|
||||
.box { text-align: center; font-size: 14px; line-height: 22px; }
|
||||
.err { color: #b91c1c; white-space: pre-wrap; max-width: 720px; padding: 24px; text-align: left; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box" id="message">Signing in to BillionMail...</div>
|
||||
<script>
|
||||
(function () {
|
||||
var serviceName = __SERVICE_NAME__;
|
||||
var redirectUrl = __REDIRECT_URL__;
|
||||
var panelApiPrefix = __PANEL_API_PREFIX__;
|
||||
var csrfToken = __CSRF_TOKEN__;
|
||||
|
||||
function getPanelApiPrefix() {
|
||||
if (panelApiPrefix) return panelApiPrefix;
|
||||
var match = window.location.pathname.match(/^\\/(apsess_[A-Za-z0-9]{16,32})(?:\\/|$)/);
|
||||
return match ? '/' + match[1] : '';
|
||||
}
|
||||
|
||||
function fail(err) {
|
||||
var message = document.getElementById('message');
|
||||
message.className = 'err';
|
||||
message.textContent = 'BillionMail SSO failed:\\n' + (err && (err.message || err.responseText || String(err)));
|
||||
}
|
||||
|
||||
function saveLogin(data) {
|
||||
var ttl = Number(data.ttl || 0);
|
||||
var login = {
|
||||
token: data.token || '',
|
||||
refresh_token: data.refreshToken || data.refresh_token || '',
|
||||
ttl: ttl,
|
||||
expire: Date.now() + ttl * 1000
|
||||
};
|
||||
if (!login.token) {
|
||||
throw new Error('SSO response does not contain token.');
|
||||
}
|
||||
|
||||
var state = {};
|
||||
try {
|
||||
state = JSON.parse(localStorage.getItem('UserStore') || '{}') || {};
|
||||
} catch (e) {
|
||||
state = {};
|
||||
}
|
||||
state.login = login;
|
||||
localStorage.setItem('UserStore', JSON.stringify(state));
|
||||
}
|
||||
|
||||
var body = new URLSearchParams();
|
||||
if (serviceName) body.set('service_name', serviceName);
|
||||
|
||||
fetch(getPanelApiPrefix() + '/v2/mod/mail/billionmail/sso', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'x-http-token': csrfToken},
|
||||
body: body
|
||||
}).then(function (res) {
|
||||
return res.json();
|
||||
}).then(function (res) {
|
||||
if (!res || res.status !== 0) {
|
||||
throw new Error(JSON.stringify(res && res.message ? res.message : res));
|
||||
}
|
||||
saveLogin(res.message || {});
|
||||
window.location.replace(redirectUrl);
|
||||
}).catch(fail);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
html = html.replace("__SERVICE_NAME__", json.dumps(service_name))
|
||||
html = html.replace("__REDIRECT_URL__", json.dumps(redirect))
|
||||
html = html.replace("__PANEL_API_PREFIX__", json.dumps(panel_api_prefix))
|
||||
html = html.replace("__CSRF_TOKEN__", json.dumps(csrf_token))
|
||||
response = Response(html, content_type="text/html; charset=utf-8", status=200)
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
response.headers["Referrer-Policy"] = "same-origin"
|
||||
return response
|
||||
|
||||
def _panel_apsess_token(self):
|
||||
token = request.environ.get("bt.apsess_token", "") or session.get("apsess_token", "")
|
||||
token = str(token or "").strip()
|
||||
while token.startswith("apsess_apsess_"):
|
||||
token = token[len("apsess_"):]
|
||||
if token and not token.startswith("apsess_"):
|
||||
token = "apsess_" + token
|
||||
if not re.match(r"^apsess_[A-Za-z0-9]{16,32}$", token):
|
||||
return ""
|
||||
return token
|
||||
|
||||
def _is_text_response(self, upstream_response, path_full):
|
||||
content_type = (upstream_response.headers.get("content-type") or "").lower()
|
||||
path = (path_full or "").split("?", 1)[0].lower()
|
||||
if any(item in content_type for item in (
|
||||
"text/html",
|
||||
"text/css",
|
||||
"javascript",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
)):
|
||||
return True
|
||||
return path.endswith((".html", ".htm", ".css", ".js", ".json", ".xml"))
|
||||
|
||||
@staticmethod
|
||||
def _webmail_url(config):
|
||||
if not isinstance(config, dict):
|
||||
return ""
|
||||
webmail_url = str(config.get("webmail_url") or "").strip()
|
||||
if webmail_url:
|
||||
return webmail_url.rstrip("/")
|
||||
direct_console_url = str(config.get("direct_console_url") or "").strip()
|
||||
if not direct_console_url:
|
||||
return ""
|
||||
return direct_console_url.rstrip("/") + "/roundcube"
|
||||
|
||||
def _rewrite_webmail_url(self, text, webmail_url):
|
||||
if not webmail_url:
|
||||
return text
|
||||
|
||||
panel_origin = "{}://{}".format(request.scheme, request.host).rstrip("/")
|
||||
text = text.replace(panel_origin + "/roundcube", webmail_url)
|
||||
text = text.replace("${location.origin}/roundcube", webmail_url)
|
||||
text = text.replace("${window.location.origin}/roundcube", webmail_url)
|
||||
|
||||
webmail_literal = json.dumps(webmail_url)
|
||||
text = re.sub(
|
||||
r'(?:(?:window|globalThis)\.)?location\.origin\s*\+\s*(["\'])/roundcube\1',
|
||||
webmail_literal,
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r'(\.concat\(\s*)(?:(?:window|globalThis)\.)?location\.origin\s*,\s*(["\'])/roundcube\2',
|
||||
lambda match: match.group(1) + webmail_literal,
|
||||
text,
|
||||
)
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def _webmail_rewrite_script(webmail_url):
|
||||
return """<script>
|
||||
(function () {
|
||||
var webmailUrl = __WEBMAIL_URL__;
|
||||
if (!webmailUrl) return;
|
||||
var panelRoundcube = window.location.origin.replace(/\\/$/, '') + '/roundcube';
|
||||
function rewrite(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
return value.split(panelRoundcube).join(webmailUrl);
|
||||
}
|
||||
window.__AAPANEL_BILLIONMAIL_WEBMAIL_URL__ = webmailUrl;
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText && !navigator.clipboard.__aapanelBillionMailPatched) {
|
||||
var rawWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
|
||||
navigator.clipboard.writeText = function (value) {
|
||||
return rawWriteText(rewrite(value));
|
||||
};
|
||||
navigator.clipboard.__aapanelBillionMailPatched = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
document.addEventListener('copy', function (event) {
|
||||
if (!event.clipboardData) return;
|
||||
var selection = String(window.getSelection ? window.getSelection() : '');
|
||||
var rewritten = rewrite(selection);
|
||||
if (rewritten !== selection) {
|
||||
event.clipboardData.setData('text/plain', rewritten);
|
||||
event.preventDefault();
|
||||
}
|
||||
}, true);
|
||||
})();
|
||||
</script>""".replace("__WEBMAIL_URL__", json.dumps(webmail_url))
|
||||
|
||||
def _inject_webmail_rewrite_script(self, text, webmail_url):
|
||||
if not webmail_url or "__AAPANEL_BILLIONMAIL_WEBMAIL_URL__" in text:
|
||||
return text
|
||||
script = self._webmail_rewrite_script(webmail_url)
|
||||
if re.search(r"</head\s*>", text, re.I):
|
||||
return re.sub(r"</head\s*>", lambda match: script + "\n" + match.group(0), text, count=1, flags=re.I)
|
||||
if re.search(r"</body\s*>", text, re.I):
|
||||
return re.sub(r"</body\s*>", lambda match: script + "\n" + match.group(0), text, count=1, flags=re.I)
|
||||
return script + "\n" + text
|
||||
|
||||
def _rewrite_text_content(self, text, path_full, config=None):
|
||||
proxy = self.proxy_prefix
|
||||
path = (path_full or "").split("?", 1)[0].lower()
|
||||
webmail_url = self._webmail_url(config)
|
||||
|
||||
replacements = (
|
||||
('${location.origin}/static/', '${location.origin}' + proxy + '/static/'),
|
||||
('"/static/', '"{}/static/'.format(proxy)),
|
||||
("'/static/", "'{}/static/".format(proxy)),
|
||||
("`/static/", "`{}/static/".format(proxy)),
|
||||
("(/static/", "({}/static/".format(proxy)),
|
||||
("url(/static/", "url({}/static/".format(proxy)),
|
||||
("=/static/", "={}/static/".format(proxy)),
|
||||
)
|
||||
for old, new in replacements:
|
||||
text = text.replace(old, new)
|
||||
|
||||
if path.endswith(".js"):
|
||||
js_base = proxy.rstrip("/") + "/"
|
||||
text = re.sub(
|
||||
r'(\b[A-Za-z_$][\w$]*\.p=)(["\'])/(["\'])',
|
||||
r'\1\2{}/\3'.format(proxy),
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r'(history\s*:\s*\(0,\s*[A-Za-z_$][\w$]*\.PO\)\(["\'])/(["\']\))',
|
||||
lambda match: match.group(1) + js_base + match.group(2),
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r'(createWebHistory\s*\(\s*["\'])/(["\']\s*\))',
|
||||
lambda match: match.group(1) + js_base + match.group(2),
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r'(prefix\s*:\s*["\'])/api(["\'])',
|
||||
r'\1{}/api\2'.format(proxy),
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r'(baseURL\s*:\s*["\'])/api(["\'])',
|
||||
r'\1{}/api\2'.format(proxy),
|
||||
text,
|
||||
)
|
||||
text = self._rewrite_webmail_url(text, webmail_url)
|
||||
elif path.endswith((".html", ".htm")) or "<html" in text[:1024].lower():
|
||||
text = self._rewrite_webmail_url(text, webmail_url)
|
||||
text = self._inject_webmail_rewrite_script(text, webmail_url)
|
||||
return text
|
||||
|
||||
def _response_content(self, upstream_response, path_full, config=None):
|
||||
content = upstream_response.content
|
||||
if not content or not self._is_text_response(upstream_response, path_full):
|
||||
return content
|
||||
encoding = upstream_response.encoding or "utf-8"
|
||||
try:
|
||||
text = content.decode(encoding)
|
||||
except Exception:
|
||||
try:
|
||||
text = content.decode("utf-8")
|
||||
except Exception:
|
||||
return content
|
||||
return self._rewrite_text_content(text, path_full, config).encode(encoding, errors="ignore")
|
||||
|
||||
def proxy(self, path_full=""):
|
||||
try:
|
||||
if (path_full or "").strip("/") == "__aapanel_sso__":
|
||||
return self._sso_bridge()
|
||||
|
||||
urllib3.disable_warnings()
|
||||
urllib3_conn.allowed_gai_family = lambda: socket.AF_INET
|
||||
config = BillionMailMod().get_proxy_config()
|
||||
if not config.get("installed"):
|
||||
return self._error("BillionMail is not installed", 404)
|
||||
target_base = config.get("target_base")
|
||||
if not target_base:
|
||||
return self._error("BillionMail web port is not configured", 502)
|
||||
|
||||
proxy_url = self._target_url(target_base, path_full)
|
||||
upstream_response = requests.request(
|
||||
request.method,
|
||||
proxy_url,
|
||||
**self._request_kwargs(target_base)
|
||||
)
|
||||
response = Response(
|
||||
self._response_content(upstream_response, path_full, config),
|
||||
headers=self._response_headers(upstream_response, target_base),
|
||||
content_type=upstream_response.headers.get("content-type", None),
|
||||
status=upstream_response.status_code,
|
||||
)
|
||||
return self._set_cookie_headers(response, upstream_response)
|
||||
except Exception as ex:
|
||||
return self._error(str(ex), 500)
|
||||
+106
-22
@@ -15,6 +15,7 @@ import os
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import urllib.parse
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
import requests
|
||||
@@ -25,6 +26,8 @@ from BTPanel import request, Response, public, app, get_phpmyadmin_dir, session
|
||||
|
||||
class HttpProxy:
|
||||
_pma_path = None
|
||||
_apsess_token_rep = re.compile(r"^apsess_[A-Za-z0-9]{16,32}$")
|
||||
_internal_hosts = frozenset(("127.0.0.1", "localhost", "::1"))
|
||||
|
||||
@staticmethod
|
||||
def _err_resp(msg: str = None):
|
||||
@@ -32,6 +35,97 @@ class HttpProxy:
|
||||
msg or "something wrong with socket, please cheak and try again...", 500
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _has_invalid_header_chars(value):
|
||||
return '\r' in value or '\n' in value
|
||||
|
||||
def _get_apsess_path_token(self):
|
||||
token = request.environ.get('bt.apsess_token', '')
|
||||
if self._apsess_token_rep.match(token):
|
||||
return token
|
||||
return ''
|
||||
|
||||
def _get_pma_proxy_path(self):
|
||||
if self._pma_path is None:
|
||||
self._pma_path = get_phpmyadmin_dir()
|
||||
if self._pma_path:
|
||||
self._pma_path = self._pma_path[0]
|
||||
else:
|
||||
self._pma_path = ''
|
||||
return self._pma_path
|
||||
|
||||
@staticmethod
|
||||
def _is_phpmyadmin_proxy_request():
|
||||
return request.path == '/phpmyadmin' or request.path.startswith('/phpmyadmin/') \
|
||||
or request.path == '/v2/phpmyadmin' or request.path.startswith('/v2/phpmyadmin/')
|
||||
|
||||
@staticmethod
|
||||
def _replace_path_prefix(path, old_prefix, new_prefix):
|
||||
if not old_prefix:
|
||||
return path
|
||||
old_path = '/' + old_prefix.strip('/')
|
||||
new_path = '/' + new_prefix.strip('/')
|
||||
if path == old_path:
|
||||
return new_path
|
||||
if path.startswith(old_path + '/'):
|
||||
return new_path + path[len(old_path):]
|
||||
return path
|
||||
|
||||
def _add_apsess_to_proxy_path(self, path):
|
||||
token = self._get_apsess_path_token()
|
||||
if not token:
|
||||
return path
|
||||
token_path = '/' + token
|
||||
if path == token_path or path.startswith(token_path + '/'):
|
||||
return path
|
||||
if path == '/phpmyadmin' or path.startswith('/phpmyadmin/'):
|
||||
return token_path + path
|
||||
return path
|
||||
|
||||
def _rewrite_location_header(self, location):
|
||||
if not location:
|
||||
return location
|
||||
if self._has_invalid_header_chars(location):
|
||||
return None
|
||||
|
||||
try:
|
||||
parts = urllib.parse.urlsplit(location)
|
||||
hostname = parts.hostname
|
||||
except ValueError:
|
||||
return location
|
||||
is_absolute = parts.scheme in ('http', 'https') and hostname in self._internal_hosts
|
||||
is_root_relative = not parts.scheme and not parts.netloc and location.startswith('/') and not location.startswith('//')
|
||||
|
||||
if not is_absolute and not is_root_relative:
|
||||
return location
|
||||
|
||||
path = self._replace_path_prefix(parts.path or '/', self._get_pma_proxy_path(), 'phpmyadmin')
|
||||
path = self._add_apsess_to_proxy_path(path)
|
||||
|
||||
if is_absolute:
|
||||
return urllib.parse.urlunsplit(('', '', path, parts.query, parts.fragment))
|
||||
return urllib.parse.urlunsplit(('', '', path, parts.query, parts.fragment))
|
||||
|
||||
def _rewrite_legacy_location_header(self, location):
|
||||
if location.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 = ''
|
||||
location = location.replace(self._pma_path, 'phpmyadmin')
|
||||
elif location.find("adminer_") != -1:
|
||||
from adminer.manager import AdminerManager
|
||||
adminer_dir, _ = AdminerManager().adminer_dir_port
|
||||
location = location.replace(adminer_dir, 'adminer')
|
||||
|
||||
if location.find('127.0.0.1') != -1:
|
||||
location = re.sub(r"https?://127.0.0.1(:\d+)?/", request.url_root, location)
|
||||
if request.url_root.find('https://') == 0:
|
||||
location = location.replace('http://', 'https://')
|
||||
return location
|
||||
|
||||
def get_res_headers(self, p_res):
|
||||
"""
|
||||
@name 获取响应头
|
||||
@@ -45,28 +139,14 @@ class HttpProxy:
|
||||
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:
|
||||
self._pma_path = get_phpmyadmin_dir()
|
||||
if self._pma_path:
|
||||
self._pma_path = self._pma_path[0]
|
||||
else:
|
||||
self._pma_path = ''
|
||||
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])
|
||||
if request.url_root.find('https://') == 0:
|
||||
headers[h] = headers.get(h, '').replace('http://', 'https://')
|
||||
if self._is_phpmyadmin_proxy_request():
|
||||
rewrite_location = self._rewrite_location_header(headers[h])
|
||||
else:
|
||||
rewrite_location = self._rewrite_legacy_location_header(headers[h])
|
||||
if rewrite_location is None:
|
||||
headers.pop(h, None)
|
||||
continue
|
||||
headers[h] = rewrite_location
|
||||
return headers
|
||||
|
||||
def set_res_headers(self, res, p_res):
|
||||
@@ -87,6 +167,10 @@ class HttpProxy:
|
||||
# expires=expires, httponly=httponly,
|
||||
# path='/')
|
||||
|
||||
if request.path == '/phpmyadmin' or request.path.startswith('/phpmyadmin/') \
|
||||
or request.path == '/v2/phpmyadmin' or request.path.startswith('/v2/phpmyadmin/'):
|
||||
res.headers['Referrer-Policy'] = 'same-origin'
|
||||
|
||||
return res
|
||||
|
||||
def get_pma_phpversion(self):
|
||||
|
||||
@@ -36,7 +36,7 @@ class main(panelBase):
|
||||
res = cache.get(skey)
|
||||
if res: return res
|
||||
|
||||
res = public.httpPost('https://wafapi2.aapanel.com/Api/getUpdateLogs?type=Linux',{})
|
||||
res = public.httpPost(f'{public.OfficialWaf2Base()}/Api/getUpdateLogs?type=Linux', {})
|
||||
|
||||
start_index = res.find('(') + 1
|
||||
end_index = res.rfind(')')
|
||||
|
||||
@@ -2159,7 +2159,7 @@ class panelPlugin:
|
||||
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'
|
||||
if not session.get('download_url'): session['download_url'] = public.OfficialDownloadBase()
|
||||
|
||||
#获取列表
|
||||
try:
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class panelSSL:
|
||||
__BINDURL = '{}/api/user'.format(public.OfficialApiBase()) # 获取token 获取官网token
|
||||
# __BINDURL = 'http://dev.aapanel.com/api/user' # 获取token 获取官网token
|
||||
|
||||
__CODEURL = 'https://wafapi.aapanel.com/Auth/GetBindCode' # 获取绑定验证码
|
||||
__CODEURL = f'{public.OfficialWafBase()}/Auth/GetBindCode' # 获取绑定验证码
|
||||
__UPATH = 'data/userInfo.json'
|
||||
|
||||
# __APIURL = 'http://dev.aapanel.com/api'
|
||||
|
||||
+1
-1
@@ -2582,7 +2582,7 @@ listener SSL443 {
|
||||
return public.returnMsg(True, public.lang("Site stopped"))
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
public.downloadFile('https://node.aapanel.com/stop_en.html', path + '/index.html')
|
||||
public.downloadFile(f'{public.OfficialDownloadBase()}/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')
|
||||
|
||||
@@ -55,8 +55,8 @@ class process_network_total:
|
||||
f.close()
|
||||
if red_body.find('CentOS Linux release 8.') != -1:
|
||||
rpm_file = '/root/libpcap-1.9.1.rpm'
|
||||
down_url = "wget -O {} https://node.aapanel.com/src/libpcap-devel-1.9.1-5.el8.x86_64.rpm --no-check-certificate -T 10".format(
|
||||
rpm_file)
|
||||
down_url = "wget -O {} {}/src/libpcap-devel-1.9.1-5.el8.x86_64.rpm --no-check-certificate -T 10".format(
|
||||
rpm_file, public.OfficialDownloadBase())
|
||||
if os.path.exists(rpm_file):
|
||||
os.system(down_url)
|
||||
os.system("rpm -ivh {}".format(rpm_file))
|
||||
|
||||
+324
-71
@@ -105,6 +105,18 @@ def default_languages_config():
|
||||
"google": "fa",
|
||||
"title": "فارسی",
|
||||
"cn": "波斯语"
|
||||
},
|
||||
{
|
||||
"name": "ar",
|
||||
"google": "ar",
|
||||
"title": "العربية",
|
||||
"cn": "阿拉伯语"
|
||||
},
|
||||
{
|
||||
"name": "lo",
|
||||
"google": "lo",
|
||||
"title": "ພາສາລາວ",
|
||||
"cn": "老挝语"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -766,7 +778,7 @@ def GetConfigValue(key):
|
||||
"template": "default", "logs_path": "/www/wwwlogs", "home": "https://www.aapanel.com", "recycle_bin": True}
|
||||
writeFile('/www/server/panel/config/config.json',json.dumps(config))
|
||||
if not key in config.keys():
|
||||
if key == 'download': return 'http://node.aapanel.com'
|
||||
if key == 'download': return OfficialDownloadBase()
|
||||
return None
|
||||
return config[key]
|
||||
|
||||
@@ -1152,58 +1164,184 @@ def get_timeout(url, timeout=3):
|
||||
|
||||
|
||||
def get_url(timeout=0.5):
|
||||
return 'https://node.aapanel.com'
|
||||
return smart_get_node("download_base")
|
||||
|
||||
import json
|
||||
|
||||
# ================== 节点容灾切换 ==================
|
||||
NODES_CONFIG = {
|
||||
# 要拿的url 主 备
|
||||
'offical_base': ['https://www.aapanel.com', 'https://sg1-www.aapanel.com'],
|
||||
'api_base': ['https://api.aapanel.com', 'https://sg1-api.aapanel.com'],
|
||||
'plugin_base': ['https://download.aapanel.com', 'https://sg1-www.aapanel.com'],
|
||||
'download_base': ['https://node.aapanel.com', 'https://jp1-node.aapanel.com'],
|
||||
'waf_base': ['https://wafapi.aapanel.com', 'https://sg1-wafapi.aapanel.com'],
|
||||
'waf2_base': ['https://wafapi2.aapanel.com', 'https://sg1-wafapi2.aapanel.com'],
|
||||
'w-check_base': ['https://w-check.aapanel.com', 'https://sg1-w-check.aapanel.com'],
|
||||
'webshellcheck_base':['https://webshellcheck.aapanel.com','https://sg1-webshellcheck.aapanel.com'],
|
||||
'geterror_base': ['https://geterror.aapanel.com', 'https://sg1-geterror.aapanel.com'],
|
||||
}
|
||||
|
||||
_NODE_HOST_INDEX = {} # host -> [node_type] install 时构建一次,只读
|
||||
_STATE_FILE = '/www/server/panel/data/node_state.json'
|
||||
_original_session_request = None
|
||||
|
||||
|
||||
def _host_of(url):
|
||||
"""提取 url 的 host"""
|
||||
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)
|
||||
from urllib.parse import urlparse
|
||||
return urlparse(url).hostname
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
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
|
||||
def _build_node_host_index():
|
||||
"""host -> [node_type] 反索引 存在一对多的情况注意"""
|
||||
idx = {}
|
||||
for node_type, nodes_list in NODES_CONFIG.items():
|
||||
for node_url in nodes_list:
|
||||
host = _host_of(node_url)
|
||||
if host:
|
||||
idx.setdefault(host, []).append(node_type)
|
||||
return idx
|
||||
|
||||
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 _is_node_ok(status_code):
|
||||
"""节点健康判定:status_code==0失败/非2xx-3xx/403/404 均失败"""
|
||||
return status_code and 200 <= status_code < 400 and status_code not in [403, 404]
|
||||
|
||||
|
||||
def _load_state():
|
||||
"""current 状态: {node_type: url}"""
|
||||
try:
|
||||
return json.loads(readFile(_STATE_FILE) or '{}')
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _save_state(data):
|
||||
"""原子写状态"""
|
||||
tmp = _STATE_FILE + '.tmp'
|
||||
with open(tmp, 'w') as f:
|
||||
json.dump(data, f)
|
||||
os.replace(tmp, _STATE_FILE)
|
||||
|
||||
|
||||
def _set_state(node_type, url):
|
||||
"""更新单 node_type 的 current; 返回是否发生变化"""
|
||||
import fcntl
|
||||
with open(_STATE_FILE + '.lock', 'w') as lf:
|
||||
fcntl.flock(lf, fcntl.LOCK_EX)
|
||||
try:
|
||||
data = _load_state()
|
||||
if data.get(node_type) == url:
|
||||
return False
|
||||
data[node_type] = url
|
||||
_save_state(data)
|
||||
return True
|
||||
finally:
|
||||
fcntl.flock(lf, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def _record_node_result(node_type, success):
|
||||
"""记录节点请求结果; 失败且当前仍是 Primary 则返回 True(应切 Secondary)。阈值=1: 任一次失败即切"""
|
||||
if success:
|
||||
return False
|
||||
primary = NODES_CONFIG[node_type][0]
|
||||
return _load_state().get(node_type, primary) == primary
|
||||
|
||||
|
||||
def _switch_to_secondary(node_type):
|
||||
nodes = NODES_CONFIG[node_type]
|
||||
secondary = nodes[1] if len(nodes) > 1 else nodes[0]
|
||||
if not _set_state(node_type, secondary):
|
||||
return
|
||||
print_log('[AAPANEL] [failover] {} primary {} down, switched to secondary {}'.format(
|
||||
node_type, nodes[0], secondary))
|
||||
# 恢复探测由 BTTask node_failover_recovery 定时兜底(常驻跨进程), 本进程不起探测线程
|
||||
|
||||
|
||||
def _record_node_failure(url):
|
||||
"""url 探测/下载失败且为某 node_type 的 Primary 时喂判定"""
|
||||
host = _host_of(url)
|
||||
if not host:
|
||||
return
|
||||
for node_type, nodes in NODES_CONFIG.items():
|
||||
if host == _host_of(nodes[0]):
|
||||
if _record_node_result(node_type, False):
|
||||
_switch_to_secondary(node_type)
|
||||
break
|
||||
|
||||
|
||||
def _switch_back_to_primary(node_type):
|
||||
primary = NODES_CONFIG[node_type][0]
|
||||
if not _set_state(node_type, primary):
|
||||
return
|
||||
print_log('[AAPANEL] [failover] {} primary {} recovered, switched back from secondary'.format(node_type, primary))
|
||||
|
||||
|
||||
def _try_recover_to_primary(node_type):
|
||||
"""单次探测 Primary 恢复: current 已 Primary→'primary'; 探测 OK 切回→'recovered'; 否则→'still_down'. 仅 BTTask node_failover_recovery 调用"""
|
||||
primary = NODES_CONFIG[node_type][0]
|
||||
if _load_state().get(node_type, primary) == primary:
|
||||
return 'primary'
|
||||
try:
|
||||
import http_requests
|
||||
res = http_requests.get(primary, timeout=(2, 3))
|
||||
if _is_node_ok(res.status_code):
|
||||
_switch_back_to_primary(node_type)
|
||||
return 'recovered'
|
||||
except Exception:
|
||||
pass
|
||||
return 'still_down'
|
||||
|
||||
|
||||
def _patched_session_request(self, method, url, *args, **kwargs):
|
||||
"""requests.Session.request:命中节点 Primary host 的请求被动计数,连续失败切 Secondary
|
||||
非节点 url / stream 下载 直透, 依赖其他失败来切换"""
|
||||
host = _host_of(url)
|
||||
node_types = _NODE_HOST_INDEX.get(host) if host else None
|
||||
if not node_types or kwargs.get('stream'):
|
||||
return _original_session_request(self, method, url, *args, **kwargs)
|
||||
if 'timeout' not in kwargs: # 防御:节点请求未指定 timeout 时兜底
|
||||
kwargs['timeout'] = 10
|
||||
success = False
|
||||
try:
|
||||
resp = _original_session_request(self, method, url, *args, **kwargs)
|
||||
success = _is_node_ok(resp.status_code)
|
||||
return resp
|
||||
except Exception:
|
||||
success = False
|
||||
raise
|
||||
finally:
|
||||
for nt in node_types:
|
||||
# 仅当请求的 host 是该 node_type 的 Primary host 才计 streak(P1)
|
||||
if _host_of(NODES_CONFIG[nt][0]) == host:
|
||||
if _record_node_result(nt, success):
|
||||
_switch_to_secondary(nt)
|
||||
|
||||
|
||||
def install_node_failover_patch():
|
||||
"""开局全局补丁(被动判定+切换 current); 恢复探测交 BTTask node_failover_recovery 定时兜底, 本进程不做"""
|
||||
global _original_session_request, _NODE_HOST_INDEX
|
||||
import requests
|
||||
if _original_session_request is None:
|
||||
_original_session_request = requests.Session.request
|
||||
requests.Session.request = _patched_session_request
|
||||
_NODE_HOST_INDEX = _build_node_host_index()
|
||||
|
||||
|
||||
def smart_get_node(node_type: str = "offical_base"):
|
||||
"""
|
||||
查询指定类型的当前可用节点 url(跨进程共享)
|
||||
node_type名字 详见 NODES_CONFIG
|
||||
"""
|
||||
nodes_list = NODES_CONFIG.get(node_type)
|
||||
if not nodes_list:
|
||||
return ''
|
||||
return _load_state().get(node_type, nodes_list[0])
|
||||
|
||||
# ================== 节点容灾切换 End ==================
|
||||
|
||||
# 过滤输入
|
||||
def checkInput(data):
|
||||
@@ -1470,13 +1608,59 @@ def get_requests_headers():
|
||||
return {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "BT-Panel"}
|
||||
|
||||
|
||||
def downloadFile(url, filename):
|
||||
def _alt_node_url(url):
|
||||
"""url 命中节点时返回替换节点(Primary↔Secondary)的 url(保留 path/query);否则 None"""
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
p = urlparse(url)
|
||||
host = p.hostname
|
||||
if not host:
|
||||
return None
|
||||
suffix = p.path + (('?' + p.query) if p.query else '')
|
||||
for nodes in NODES_CONFIG.values():
|
||||
if len(nodes) < 2:
|
||||
continue
|
||||
if host == _host_of(nodes[0]):
|
||||
return nodes[1] + suffix
|
||||
if host == _host_of(nodes[1]):
|
||||
return nodes[0] + suffix
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _download_candidates(url):
|
||||
"""下载候选 url:当前节点优先 + 替换节点(去重)"""
|
||||
cands = [url]
|
||||
alt = _alt_node_url(url)
|
||||
if alt and alt not in cands:
|
||||
cands.append(alt)
|
||||
return cands
|
||||
|
||||
|
||||
def _probe_node_reachable(url, timeout=3):
|
||||
"""TCP 探测节点可达性(短 timeout connect),可达返回 True"""
|
||||
try:
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
p = urlparse(url)
|
||||
host = p.hostname
|
||||
if not host:
|
||||
return False
|
||||
port = p.port or (443 if p.scheme == 'https' else 80)
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _download_once(url, filename):
|
||||
"""单次下载:Py3 urlretrieve / Py2 requests,wget 兜底。成功(文件非空)返回 True"""
|
||||
ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36'
|
||||
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)
|
||||
r = requests.get(url, headers={'User-agent': ua}, verify=False)
|
||||
with open(filename, "wb") as f:
|
||||
f.write(r.content)
|
||||
else:
|
||||
@@ -1484,12 +1668,41 @@ def downloadFile(url, filename):
|
||||
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')]
|
||||
opener.addheaders = [('User-agent', ua)]
|
||||
urllib.request.install_opener(opener)
|
||||
urllib.request.urlretrieve(url, filename=filename)
|
||||
except:
|
||||
ExecShell("wget -O {} {} --no-check-certificate".format(filename, url))
|
||||
return os.path.exists(filename) and os.path.getsize(filename) > 0
|
||||
|
||||
|
||||
def downloadFile(url, filename):
|
||||
"""下载文件;下载前 TCP 探测候选节点选可达的再下载 """
|
||||
for candidate in _download_candidates(url):
|
||||
if not _probe_node_reachable(candidate):
|
||||
print_log('[AAPANEL] [failover] downloadFile {} unreachable, skip'.format(candidate))
|
||||
_record_node_failure(candidate)
|
||||
continue
|
||||
if _download_once(candidate, filename):
|
||||
if candidate != url:
|
||||
print_log('[AAPANEL] [failover] downloadFile {} unreachable, switched to {}'.format(url, candidate))
|
||||
return True
|
||||
print_log('[AAPANEL] [failover] downloadFile {} reachable but download failed, try next'.format(candidate))
|
||||
_record_node_failure(candidate)
|
||||
return False
|
||||
|
||||
|
||||
def get_reachable_url(url, timeout=3):
|
||||
"""探测 url 可达性返回可用下载节点, 同时协同容灾状态(不可达喂判定切 Secondary + 试备用)
|
||||
downloadFile / OfficialDownloadBase 友好下载类型 共用, 孤儿api跨进程使用"""
|
||||
if _probe_node_reachable(url, timeout):
|
||||
return url
|
||||
_record_node_failure(url)
|
||||
alt = _alt_node_url(url)
|
||||
if alt and _probe_node_reachable(alt, timeout):
|
||||
print_log('[AAPANEL] [failover] {} unreachable, use {}'.format(url, alt))
|
||||
return alt
|
||||
return url
|
||||
|
||||
|
||||
def exists_args(args, get):
|
||||
@@ -1907,14 +2120,12 @@ def checkWebConfig(repair_num=2, path=None):
|
||||
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],))
|
||||
@@ -1976,7 +2187,7 @@ def err_collect(error_info, type, error_id):
|
||||
# 提交异常报告
|
||||
if not cache_get(pkey):
|
||||
try:
|
||||
run_thread(httpPost, ("https://geterror.aapanel.com/bt_error/index.php", error_infos))
|
||||
run_thread(httpPost, (f"{OfficialGetErrorBase()}/bt_error/index.php", error_infos))
|
||||
cache_set(pkey, 1, 1800)
|
||||
except Exception as e:
|
||||
pass # 错误信息
|
||||
@@ -2080,6 +2291,7 @@ def CheckMyCnf():
|
||||
version = key
|
||||
break
|
||||
|
||||
download_url = smart_get_node('download_base')
|
||||
shellStr = '''
|
||||
#!/bin/bash
|
||||
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
|
||||
@@ -2104,7 +2316,7 @@ export PATH
|
||||
# nodeAddr=$CF
|
||||
# fi
|
||||
|
||||
Download_Url=https://node.aapanel.com
|
||||
Download_Url=%s
|
||||
|
||||
|
||||
MySQL_Opt()
|
||||
@@ -2182,7 +2394,7 @@ MySQL_Opt()
|
||||
wget -O /etc/my.cnf $Download_Url/install/conf/mysql-%s.conf -T 5
|
||||
chmod 644 /etc/my.cnf
|
||||
MySQL_Opt
|
||||
''' % (version,)
|
||||
''' % (download_url, version)
|
||||
ExecShell(shellStr)
|
||||
# 判断是否迁移目录
|
||||
if os.path.exists('data/datadir.pl'):
|
||||
@@ -7680,11 +7892,11 @@ def check_auth_ip():
|
||||
"""
|
||||
import http_requests
|
||||
result = {'www': '', 'api': ''}
|
||||
res = http_requests.post('https://wafapi2.aapanel.com/api/getIpAddress', data={}, timeout=5, headers={})
|
||||
res = http_requests.post(f'{OfficialWaf2Base()}/api/getIpAddress', data={}, timeout=5, headers={})
|
||||
if res.status_code == 200:
|
||||
result['www'] = res.text
|
||||
|
||||
res1 = http_requests.post('https://wafapi.aapanel.com/api/getIpAddress', data={}, timeout=5, headers={})
|
||||
res1 = http_requests.post(f'{OfficialWafBase()}/api/getIpAddress', data={}, timeout=5, headers={})
|
||||
if res1.status_code == 200:
|
||||
result['api'] = res1.text
|
||||
|
||||
@@ -9182,9 +9394,9 @@ def load_soft_list(force: bool = True, retry_count: int = 0):
|
||||
time.sleep(2 * retry_count + 1)
|
||||
|
||||
cloudUrl = '{}/api/panel/getSoftListEn'.format(OfficialApiBase())
|
||||
import panelAuth
|
||||
from panel_auth_v2 import panelAuth
|
||||
import requests
|
||||
pdata = panelAuth.panelAuth().create_serverid(None)
|
||||
pdata = panelAuth().create_serverid(None)
|
||||
url_headers = {
|
||||
'user-agent': 'aaPanel/1.0',
|
||||
}
|
||||
@@ -9256,20 +9468,44 @@ def load_soft_list(force: bool = True, retry_count: int = 0):
|
||||
|
||||
return plugin_list_data
|
||||
|
||||
|
||||
# ======================== 获取节点, 统一风格入口 ============================
|
||||
# 官网API根地址
|
||||
def OfficialApiBase():
|
||||
return 'https://www.aapanel.com'
|
||||
# return 'http://dev.aapanel.com'
|
||||
return smart_get_node()
|
||||
|
||||
# 官网API服务接口地址
|
||||
def OfficialApiUrlBase():
|
||||
return get_reachable_url(smart_get_node("api_base"))
|
||||
|
||||
# 部分插件下载地址
|
||||
def sync_plugin_OfficialApiBase():
|
||||
return 'https://download.aapanel.com'
|
||||
def SyncPluginOfficialApiBase():
|
||||
return smart_get_node("plugin_base")
|
||||
|
||||
# 官网下载根地址
|
||||
def OfficialDownloadBase():
|
||||
return 'https://node.aapanel.com'
|
||||
return get_reachable_url(smart_get_node("download_base"))
|
||||
|
||||
def OfficialGetErrorBase():
|
||||
return smart_get_node('geterror_base')
|
||||
|
||||
# WAFapi
|
||||
def OfficialWafBase():
|
||||
return smart_get_node('waf_base')
|
||||
|
||||
# WAFapi2
|
||||
def OfficialWaf2Base():
|
||||
return smart_get_node('waf2_base')
|
||||
|
||||
# w check
|
||||
def OfficialWCheckBase():
|
||||
return smart_get_node('w-check_base')
|
||||
|
||||
# WEBshell
|
||||
def OfficialWebShellCheckBase():
|
||||
return smart_get_node('webshellcheck_base')
|
||||
|
||||
|
||||
# ======================== 获取节点 End ============================
|
||||
|
||||
# 获取安装路径
|
||||
def get_setup_path():
|
||||
@@ -9830,9 +10066,9 @@ def progress_acquire_lock(lock_file):
|
||||
if os.path.exists(lock_file):
|
||||
os.remove(lock_file)
|
||||
|
||||
# 创建新锁文件并写入当前线程ID
|
||||
# 创建新锁文件并写入当前线程ID(原子操作,避免空窗口期)
|
||||
with open(lock_file, 'w') as f:
|
||||
f.write('')
|
||||
f.write(str(threading.get_ident()))
|
||||
f.flush()
|
||||
os.fsync(f.fileno()) # 确保写入磁盘
|
||||
return True
|
||||
@@ -10443,4 +10679,21 @@ def replace_conf_without_sub(text, old, new):
|
||||
new=new,
|
||||
block_start="#Subdirectory-configuration-start",
|
||||
block_end="#Subdirectory-configuration-end"
|
||||
)
|
||||
)
|
||||
|
||||
def get_secret_key():
|
||||
secret_key_file = get_panel_path() + "/data/panel_secret_key.json"
|
||||
if os.path.exists(secret_key_file):
|
||||
try:
|
||||
data = readFile(secret_key_file)
|
||||
if data:
|
||||
data_dict = json.loads(data)
|
||||
if "secret_key" in data_dict and data_dict["secret_key"]:
|
||||
return data_dict["secret_key"]
|
||||
except:
|
||||
secret_key = GetRandomString(64)+get_mac_address()
|
||||
writeFile(secret_key_file, json.dumps({"secret_key": secret_key}))
|
||||
return secret_key
|
||||
secret_key = GetRandomString(64)+get_mac_address()
|
||||
writeFile(secret_key_file, json.dumps({"secret_key": secret_key}))
|
||||
return secret_key
|
||||
|
||||
@@ -117,10 +117,8 @@ def del_bak(bak_file: str) -> aap_t_simple_result:
|
||||
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)
|
||||
if os.path.exists(bak_file):
|
||||
os.remove(bak_file)
|
||||
|
||||
return aap_t_simple_result(True, get_msg_gettext('Remove backup successfully'))
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class main(safeBase):
|
||||
# data["serverid"]=self.user_info["serverid"]
|
||||
data["serverid"] = self.user_info["server_id"]
|
||||
#如果不是我们的用户,那么不返回数据
|
||||
res = public.httpPost('https://wafapi2.aapanel.com/api/ip/info',data)
|
||||
res = public.httpPost(f'{public.OfficialWaf2Base()}/api/ip/info', data)
|
||||
res = json.loads(res)
|
||||
data = self.get_ip_area_cache()
|
||||
for key in res:
|
||||
|
||||
@@ -80,8 +80,6 @@ NhnIR3Ilo4H2su9/cTNo/JEWIdEQUX/MlMcOaFZ8tGgjCfBcJNHhmedart7PDRhF
|
||||
NhnIR3Ilo4H2su9/cTNo/E4e/PRk3DEtgyjsUDeOpSOwGpZehbHYaLN6kP4SrhN+PFbXCI6E8Z+8865ULB7IOw==
|
||||
LVgN8QZ1tkuTtU9+mDfpHbABBExfuPmmfo3E06+A/BM9PmE7jeC8kd00z+BCB2M1v4xu/9vQ4uMzqn0yz/r/zg==
|
||||
NhnIR3Ilo4H2su9/cTNo/C/mkLay+Rx5WJjwGcYV7pSktyrOzy78W2NcadJ49lO1TawFf1/X1sBVYqIElsTA2w==
|
||||
UGSctzorKMmnxEYKNtDxBsMiRa+LzvFu7RsagwDRjHDjiVYn4zDsZyr78hpjoOnsS9Yzsz+GIvg2uKsO373vP6MBvlJMwYI2zUo8zjMn5LXpp5dEs5Ye5bPkCCEC4fWv
|
||||
UGSctzorKMmnxEYKNtDxBvmbpWmtdiTclQjVTr357iIpqijI/y4QfER4haYbO69t
|
||||
i2/r4l7qMbzsMax2VRyMhtAmCDVM605WJJJXLvaIPWs=
|
||||
lOh2GtzHjjMM8E9J40AuOcdQKKiNsG4KLWGuABfb2Fk=
|
||||
p9oVsBgcyiBMjDb8CcPOqAYk8y+JZgXclTIm+iHATKY=
|
||||
|
||||
@@ -500,7 +500,7 @@ class ssh_security:
|
||||
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)
|
||||
public.downloadFile('{}/linux/panel/msg/msg.json'.format(public.OfficialDownloadBase(),cpath))
|
||||
except : pass
|
||||
|
||||
data = {}
|
||||
|
||||
@@ -628,7 +628,7 @@ class main(sslBase):
|
||||
public.M('ssl_info').delete(id=target["id"])
|
||||
|
||||
if target["cloud_id"] != -1 and cloud:
|
||||
url = "https://wafapi2.aapanel.com/api/Cert_cloud_deploy/del_cert"
|
||||
url = f"{public.OfficialWaf2Base()}/api/Cert_cloud_deploy/del_cert"
|
||||
try:
|
||||
res_text = public.httpPost(url, {
|
||||
"cert_id": target["cloud_id"],
|
||||
@@ -699,7 +699,7 @@ class main(sslBase):
|
||||
data["privateKey"] = AES.aes_encrypt(data["privateKey"])
|
||||
data["certificate"] = AES.aes_encrypt(data["certificate"])
|
||||
# 对接云端
|
||||
url = "https://wafapi2.aapanel.com/api/Cert_cloud_deploy/cloud_deploy"
|
||||
url = f"{public.OfficialWaf2Base()}/api/Cert_cloud_deploy/cloud_deploy"
|
||||
|
||||
try:
|
||||
res_text = public.httpPost(url, data)
|
||||
@@ -1284,7 +1284,7 @@ class main(sslBase):
|
||||
AES = AesCryptPy3(key, "CBC", iv, char_set="utf8")
|
||||
|
||||
# 对接云端
|
||||
url = "https://wafapi2.aapanel.com/api/Cert_cloud_deploy/get_cert_list"
|
||||
url = f"{public.OfficialWaf2Base()}/api/Cert_cloud_deploy/get_cert_list"
|
||||
try:
|
||||
res_text = public.httpPost(url, {
|
||||
"uid": user_info["uid"],
|
||||
|
||||
+36
-7
@@ -144,14 +144,43 @@ class ssl_info:
|
||||
# 取申请时间
|
||||
result['notBefore'] = self.strf_date(
|
||||
bytes.decode(x509.get_notBefore())[:-1])
|
||||
# 取可选名称
|
||||
# 取可选名称 - 使用cryptography稳定解析
|
||||
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])
|
||||
try:
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509.oid import ExtensionOID
|
||||
cert_obj = load_pem_x509_certificate(pem_data.encode(), default_backend())
|
||||
try:
|
||||
san_ext = cert_obj.extensions.get_extension_for_oid(
|
||||
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
||||
)
|
||||
dns_list = []
|
||||
for dns in san_ext.value:
|
||||
val = dns.value
|
||||
if isinstance(val, bytes):
|
||||
try:
|
||||
import ipaddress
|
||||
val = str(ipaddress.ip_address(val))
|
||||
except Exception:
|
||||
val = val.decode('utf-8', errors='ignore')
|
||||
elif not isinstance(val, str):
|
||||
val = str(val)
|
||||
dns_list.append(val)
|
||||
result['dns'] = dns_list
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
# fallback
|
||||
try:
|
||||
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])
|
||||
except:
|
||||
pass
|
||||
subject = x509.get_subject().get_components()
|
||||
# 取主要认证名称
|
||||
if len(subject) == 1:
|
||||
|
||||
+3
-3
@@ -432,7 +432,7 @@ class SSLManger:
|
||||
AES = AesCryptPy3(key, "CBC", iv, char_set="utf8")
|
||||
|
||||
# 对接云端
|
||||
url = "https://wafapi2.aapanel.com/api/Cert_cloud_deploy/get_cert_list"
|
||||
url = f"{public.OfficialWaf2Base()}/api/Cert_cloud_deploy/get_cert_list"
|
||||
try:
|
||||
res_text = public.httpPost(url, {
|
||||
"uid": user_info["uid"],
|
||||
@@ -630,7 +630,7 @@ class SSLManger:
|
||||
ssl_db.connection().delete(id=target["id"])
|
||||
|
||||
if target["cloud_id"] != -1:
|
||||
url = "https://wafapi2.aapanel.com/api/Cert_cloud_deploy/del_cert"
|
||||
url = f"{public.OfficialWaf2Base()}/api/Cert_cloud_deploy/del_cert"
|
||||
try:
|
||||
res_text = public.httpPost(url, {
|
||||
"cert_id": target["cloud_id"],
|
||||
@@ -677,7 +677,7 @@ class SSLManger:
|
||||
data["privateKey"] = AES.aes_encrypt(data["privateKey"])
|
||||
data["certificate"] = AES.aes_encrypt(data["certificate"])
|
||||
# 对接云端
|
||||
url = "https://wafapi2.aapanel.com/api/Cert_cloud_deploy/cloud_deploy"
|
||||
url = f"{public.OfficialWaf2Base()}/api/Cert_cloud_deploy/cloud_deploy"
|
||||
|
||||
try:
|
||||
res_text = public.httpPost(url, data)
|
||||
|
||||
+2
-2
@@ -993,12 +993,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/update_7.x_en.sh && bash update.sh")
|
||||
public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update_panel_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/update_7.x_en.sh && bash update.sh")
|
||||
public.ExecShell("wget --no-check-certificate -O update.sh " + public.get_url() + "/install/update_panel_en.sh && bash update.sh")
|
||||
self.ReWeb(None)
|
||||
return True
|
||||
|
||||
@@ -75,6 +75,8 @@ class userLang:
|
||||
name = post.name
|
||||
if name == "":
|
||||
return
|
||||
if len(name) > 5:
|
||||
return
|
||||
|
||||
# 登录阶段不设置 只记录
|
||||
path = "/www/server/panel/BTPanel/languages/language.pl"
|
||||
|
||||
+1
-1
@@ -220,7 +220,7 @@ class userlogin:
|
||||
# 提交
|
||||
if not public.cache_get(pkey):
|
||||
try:
|
||||
public.run_thread(public.httpPost, ("https://geterror.aapanel.com/bt_error/index.php", error_infos))
|
||||
public.run_thread(public.httpPost, (f"{public.OfficialGetErrorBase()}/bt_error/index.php", error_infos))
|
||||
public.cache_set(pkey, 1, 1800)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user