mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-09-27 20:04:50 +02:00
Update to v7.63.0
This commit is contained in:
@@ -0,0 +1,943 @@
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import fcntl
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
APACHE_CONF_DIRS = [
|
||||
"/www/server/panel/vhost/apache"
|
||||
]
|
||||
|
||||
def is_ipv4(ip):
|
||||
'''
|
||||
@name 是否是IPV4地址
|
||||
@author hwliang
|
||||
@param ip<string> IP地址
|
||||
@return True/False
|
||||
'''
|
||||
# 验证基本格式
|
||||
if not re.match(r"^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$", ip):
|
||||
return False
|
||||
|
||||
# 验证每个段是否在合理范围
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, ip)
|
||||
except AttributeError:
|
||||
try:
|
||||
socket.inet_aton(ip)
|
||||
except socket.error:
|
||||
return False
|
||||
except socket.error:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_ipv6(ip):
|
||||
'''
|
||||
@name 是否为IPv6地址
|
||||
@author hwliang
|
||||
@param ip<string> 地址
|
||||
@return True/False
|
||||
'''
|
||||
# 验证基本格式
|
||||
if not re.match(r"^[\w:]+$", ip):
|
||||
return False
|
||||
|
||||
# 验证IPv6地址
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, ip)
|
||||
except socket.error:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_ip(ip):
|
||||
return is_ipv4(ip) or is_ipv6(ip)
|
||||
|
||||
def find_apache_conf_files(keyword):
|
||||
"""查找 Apache 主配置和 vhost 文件"""
|
||||
files = set()
|
||||
for base in APACHE_CONF_DIRS:
|
||||
base_path = Path(base)
|
||||
if not base_path.exists():
|
||||
continue
|
||||
for f in base_path.rglob("*.conf"):
|
||||
with open(f, "r") as file:
|
||||
content = file.read()
|
||||
# 检查是否包含 ServerName 或 ServerAlias 指令
|
||||
if keyword in content:
|
||||
files.add(str(f))
|
||||
return list(files)
|
||||
|
||||
|
||||
def insert_location_into_vhost(file_path, keyword, verify_file):
|
||||
LOCATION_BLOCK = [
|
||||
" <Location /.well-known/acme-challenge/{}>\n".format(verify_file),
|
||||
" Require all granted\n",
|
||||
" Header set Content-Type \"text/plain\"\n",
|
||||
" </Location>\n",
|
||||
" Alias /.well-known/acme-challenge/{} /tmp/{}\n".format(verify_file, verify_file),
|
||||
]
|
||||
|
||||
path = Path(file_path)
|
||||
backup = path.with_suffix(path.suffix + ".bak")
|
||||
shutil.copy(path, backup)
|
||||
|
||||
with open(path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
in_vhost = False
|
||||
hit_vhost = False
|
||||
location_exists = False
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
if stripped.lower().startswith("<virtualhost"):
|
||||
in_vhost = True
|
||||
hit_vhost = False
|
||||
location_exists = False
|
||||
|
||||
if in_vhost:
|
||||
low = stripped.lower()
|
||||
if (low.startswith("servername") or low.startswith("serveralias")) and keyword in stripped:
|
||||
hit_vhost = True
|
||||
|
||||
if "<location /.well-known/acme-challenge/>" in low:
|
||||
location_exists = True
|
||||
|
||||
if stripped.lower() == "</virtualhost>":
|
||||
if hit_vhost and not location_exists:
|
||||
new_lines.extend(LOCATION_BLOCK)
|
||||
in_vhost = False
|
||||
|
||||
new_lines.append(line)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
return True
|
||||
|
||||
def find_nginx_files_by_servername(keyword):
|
||||
"""通过 nginx -T 找到包含 server_name 的配置文件"""
|
||||
result = subprocess.run(
|
||||
["nginx", "-T"],
|
||||
stderr=subprocess.STDOUT,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
files = set()
|
||||
current_file = None
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith("# configuration file"):
|
||||
current_file = line.split()[-1].rstrip(":")
|
||||
if "server_name" in line and keyword in line:
|
||||
if current_file:
|
||||
files.add(current_file)
|
||||
|
||||
return list(files)
|
||||
|
||||
def insert_location_into_server(file_path, keyword, verify_file, verify_content):
|
||||
path = Path(file_path)
|
||||
backup = path.with_suffix(path.suffix + ".bak")
|
||||
|
||||
shutil.copy(path, backup)
|
||||
|
||||
with open(path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
brace_level = 0
|
||||
in_server = False
|
||||
hit_server = False
|
||||
location_exists = False
|
||||
|
||||
LOCATION_BLOCK = [
|
||||
" location = /.well-known/acme-challenge/{} {{\n".format(verify_file),
|
||||
" default_type text/plain;\n",
|
||||
" return 200 \"{}\";\n".format(verify_content),
|
||||
" }\n"
|
||||
]
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
|
||||
# server 开始
|
||||
if stripped.startswith("server"):
|
||||
in_server = True
|
||||
hit_server = False
|
||||
location_exists = False
|
||||
|
||||
if in_server:
|
||||
brace_level += line.count("{")
|
||||
brace_level -= line.count("}")
|
||||
|
||||
if "server_name" in line and keyword in line:
|
||||
hit_server = True
|
||||
|
||||
if "location /.well-known/acme-challenge/" in line:
|
||||
location_exists = True
|
||||
|
||||
# server 结束
|
||||
if brace_level == 0:
|
||||
if hit_server and not location_exists:
|
||||
new_lines.extend(LOCATION_BLOCK)
|
||||
in_server = False
|
||||
|
||||
new_lines.append(line)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
return True
|
||||
|
||||
class AutoApplyIPSSL:
|
||||
# 请求到ACME接口
|
||||
def __init__(self):
|
||||
self._wait_time = 5
|
||||
self._max_check_num = 15
|
||||
self._url = 'https://acme-v02.api.letsencrypt.org/directory'
|
||||
self._bits = 2048
|
||||
self._conf_file_v2 = '/www/server/panel/config/letsencrypt_v2.json'
|
||||
self._apis = None
|
||||
self._replay_nonce = None
|
||||
self._config = self.read_config()
|
||||
|
||||
|
||||
# 取接口目录
|
||||
def get_apis(self):
|
||||
if not self._apis:
|
||||
# 尝试从配置文件中获取
|
||||
api_index = "Production"
|
||||
if not 'apis' in self._config:
|
||||
self._config['apis'] = {}
|
||||
if api_index in self._config['apis']:
|
||||
if 'expires' in self._config['apis'][api_index] and 'directory' in self._config['apis'][api_index]:
|
||||
if time.time() < self._config['apis'][api_index]['expires']:
|
||||
self._apis = self._config['apis'][api_index]['directory']
|
||||
return self._apis
|
||||
|
||||
# 尝试从云端获取
|
||||
res = requests.get(self._url)
|
||||
if not res.status_code in [200, 201]:
|
||||
result = res.json()
|
||||
if "type" in result:
|
||||
if result['type'] == 'urn:acme:error:serverInternal':
|
||||
raise Exception('Service is closed for maintenance or internal error occurred, check <a href="https://letsencrypt.status.io/" target="_blank" class="btlink">https://letsencrypt.status.io/</a> .')
|
||||
raise Exception(res.content)
|
||||
s_body = res.json()
|
||||
self._apis = {}
|
||||
self._apis['newAccount'] = s_body['newAccount']
|
||||
self._apis['newNonce'] = s_body['newNonce']
|
||||
self._apis['newOrder'] = s_body['newOrder']
|
||||
self._apis['revokeCert'] = s_body['revokeCert']
|
||||
self._apis['keyChange'] = s_body['keyChange']
|
||||
|
||||
# 保存到配置文件
|
||||
self._config['apis'][api_index] = {}
|
||||
self._config['apis'][api_index]['directory'] = self._apis
|
||||
self._config['apis'][api_index]['expires'] = time.time() + \
|
||||
86400 # 24小时后过期
|
||||
self.save_config()
|
||||
return self._apis
|
||||
|
||||
def acme_request(self, url, payload):
|
||||
headers = {}
|
||||
payload = self.stringfy_items(payload)
|
||||
|
||||
if payload == "":
|
||||
payload64 = payload
|
||||
else:
|
||||
payload64 = self.calculate_safe_base64(json.dumps(payload))
|
||||
protected = self.get_acme_header(url)
|
||||
protected64 = self.calculate_safe_base64(json.dumps(protected))
|
||||
signature = self.sign_message(
|
||||
message="{0}.{1}".format(protected64, payload64)) # bytes
|
||||
signature64 = self.calculate_safe_base64(signature) # str
|
||||
data = json.dumps(
|
||||
{"protected": protected64, "payload": payload64,
|
||||
"signature": signature64}
|
||||
)
|
||||
headers.update({"Content-Type": "application/jose+json"})
|
||||
response = requests.post(url, data=data.encode("utf8"), headers=headers)
|
||||
# 更新随机数
|
||||
self.update_replay_nonce(response)
|
||||
return response
|
||||
|
||||
# 更新随机数
|
||||
def update_replay_nonce(self, res):
|
||||
replay_nonce = res.headers.get('Replay-Nonce')
|
||||
if replay_nonce:
|
||||
self._replay_nonce = replay_nonce
|
||||
|
||||
def stringfy_items(self, payload):
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
|
||||
for k, v in payload.items():
|
||||
if isinstance(k, bytes):
|
||||
k = k.decode("utf-8")
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode("utf-8")
|
||||
payload[k] = v
|
||||
return payload
|
||||
|
||||
# 转为无填充的Base64
|
||||
def calculate_safe_base64(self, un_encoded_data):
|
||||
if sys.version_info[0] == 3:
|
||||
if isinstance(un_encoded_data, str):
|
||||
un_encoded_data = un_encoded_data.encode("utf8")
|
||||
r = base64.urlsafe_b64encode(un_encoded_data).rstrip(b"=")
|
||||
return r.decode("utf8")
|
||||
|
||||
# 获请ACME请求头
|
||||
def get_acme_header(self, url):
|
||||
header = {"alg": "RS256", "nonce": self.get_nonce(), "url": url}
|
||||
if url in [self._apis['newAccount'], 'GET_THUMBPRINT']:
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
private_key = serialization.load_pem_private_key(
|
||||
self.get_account_key().encode(),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
public_key_public_numbers = private_key.public_key().public_numbers()
|
||||
|
||||
exponent = "{0:x}".format(public_key_public_numbers.e)
|
||||
exponent = "0{0}".format(exponent) if len(
|
||||
exponent) % 2 else exponent
|
||||
modulus = "{0:x}".format(public_key_public_numbers.n)
|
||||
jwk = {
|
||||
"kty": "RSA",
|
||||
"e": self.calculate_safe_base64(binascii.unhexlify(exponent)),
|
||||
"n": self.calculate_safe_base64(binascii.unhexlify(modulus)),
|
||||
}
|
||||
header["jwk"] = jwk
|
||||
else:
|
||||
header["kid"] = self.get_kid()
|
||||
return header
|
||||
|
||||
def get_nonce(self, force=False):
|
||||
# 如果没有保存上一次的随机数或force=True时则重新获取新的随机数
|
||||
if not self._replay_nonce or force:
|
||||
response = requests.get(
|
||||
self._apis['newNonce'],
|
||||
)
|
||||
self._replay_nonce = response.headers["Replay-Nonce"]
|
||||
return self._replay_nonce
|
||||
|
||||
def analysis_private_key(self, key_pem, password=None):
|
||||
"""
|
||||
解析私钥
|
||||
:param key_pem: 私钥内容
|
||||
:param password: 私钥密码
|
||||
:return: 私钥对象
|
||||
"""
|
||||
try:
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key_pem.encode(),
|
||||
password=password,
|
||||
backend=default_backend()
|
||||
)
|
||||
return private_key
|
||||
except:
|
||||
return None
|
||||
|
||||
def sign_message(self, message):
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
pk = self.analysis_private_key(self.get_account_key())
|
||||
return pk.sign(message.encode("utf8"), padding.PKCS1v15(), hashes.SHA256())
|
||||
|
||||
# 获用户取密钥对
|
||||
def get_account_key(self):
|
||||
if not 'account' in self._config:
|
||||
self._config['account'] = {}
|
||||
k = "Production"
|
||||
if not k in self._config['account']:
|
||||
self._config['account'][k] = {}
|
||||
|
||||
if not 'key' in self._config['account'][k]:
|
||||
self._config['account'][k]['key'] = self.create_key()
|
||||
if type(self._config['account'][k]['key']) == bytes:
|
||||
self._config['account'][k]['key'] = self._config['account'][k]['key'].decode()
|
||||
self.save_config()
|
||||
return self._config['account'][k]['key']
|
||||
|
||||
def create_key(self, key_type='RSA'):
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519
|
||||
|
||||
if key_type == 'RSA':
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=self._bits
|
||||
)
|
||||
elif key_type == 'EC':
|
||||
private_key = ec.generate_private_key(ec.SECP256R1())
|
||||
elif key_type == 'ED25519':
|
||||
private_key = ed25519.Ed25519PrivateKey.generate()
|
||||
else:
|
||||
raise ValueError(f"Unsupported key type: {key_type}")
|
||||
|
||||
private_key_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
return private_key_pem
|
||||
|
||||
def get_kid(self, force=False):
|
||||
#如果配置文件中不存在kid或force = True时则重新注册新的acme帐户
|
||||
if not 'account' in self._config:
|
||||
self._config['account'] = {}
|
||||
k = "Production"
|
||||
if not k in self._config['account']:
|
||||
self._config['account'][k] = {}
|
||||
|
||||
if not 'kid' in self._config['account'][k]:
|
||||
self._config['account'][k]['kid'] = self.register()
|
||||
self.save_config()
|
||||
time.sleep(3)
|
||||
self._config = self.read_config()
|
||||
return self._config['account'][k]['kid']
|
||||
|
||||
# 读配置文件
|
||||
def read_config(self):
|
||||
if not os.path.exists(self._conf_file_v2):
|
||||
self._config = {'orders': {}, 'account': {}, 'apis': {}, 'email': None}
|
||||
self.save_config()
|
||||
return self._config
|
||||
with open(self._conf_file_v2, 'r') as f:
|
||||
fcntl.flock(f, fcntl.LOCK_SH) # 加锁
|
||||
tmp_config = f.read()
|
||||
fcntl.flock(f, fcntl.LOCK_UN) # 解锁
|
||||
f.close()
|
||||
if not tmp_config:
|
||||
return self._config
|
||||
try:
|
||||
self._config = json.loads(tmp_config)
|
||||
except:
|
||||
self.save_config()
|
||||
return self._config
|
||||
return self._config
|
||||
|
||||
# 写配置文件
|
||||
def save_config(self):
|
||||
fp = open(self._conf_file_v2, 'w+')
|
||||
fcntl.flock(fp, fcntl.LOCK_EX) # 加锁
|
||||
fp.write(json.dumps(self._config))
|
||||
fcntl.flock(fp, fcntl.LOCK_UN) # 解锁
|
||||
fp.close()
|
||||
return True
|
||||
|
||||
# 注册acme帐户
|
||||
def register(self, existing=False):
|
||||
if not 'email' in self._config:
|
||||
self._config['email'] = 'demo@aapanel.com'
|
||||
if existing:
|
||||
payload = {"onlyReturnExisting": True}
|
||||
elif self._config['email']:
|
||||
payload = {
|
||||
"termsOfServiceAgreed": True,
|
||||
"contact": ["mailto:{0}".format(self._config['email'])],
|
||||
}
|
||||
else:
|
||||
payload = {"termsOfServiceAgreed": True}
|
||||
|
||||
res = self.acme_request(url=self._apis['newAccount'], payload=payload)
|
||||
|
||||
if res.status_code not in [201, 200, 409]:
|
||||
raise Exception("Failed to register ACME account: {}".format(res.json()))
|
||||
kid = res.headers["Location"]
|
||||
return kid
|
||||
|
||||
def create_csr(self, ips):
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import ipaddress
|
||||
|
||||
|
||||
# 生成私钥
|
||||
pk = self.create_key()
|
||||
private_key = serialization.load_pem_private_key(pk, password=None)
|
||||
|
||||
# IP证书不需要CN
|
||||
csr_builder = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([]))
|
||||
# 添加 subjectAltName 扩展
|
||||
alt_names = [x509.IPAddress(ipaddress.ip_address(ip)) for ip in ips]
|
||||
|
||||
|
||||
csr_builder = csr_builder.add_extension(
|
||||
x509.SubjectAlternativeName(alt_names),
|
||||
critical=False
|
||||
)
|
||||
|
||||
# 签署 CSR
|
||||
csr = csr_builder.sign(private_key, hashes.SHA256())
|
||||
|
||||
# 返回 CSR (ASN1 格式)
|
||||
return csr.public_bytes(serialization.Encoding.DER), pk
|
||||
|
||||
def apply_ip_ssl(self, ips, email, webroot=None, mode=None, path=None):
|
||||
print("Starting to apply for Let's Encrypt IP SSL certificate...")
|
||||
print("Retrieving ACME API directory...")
|
||||
self.get_apis()
|
||||
self._config['email'] = email
|
||||
print("Creating order...")
|
||||
order_data = self.create_order(ips)
|
||||
if not order_data:
|
||||
raise Exception("Failed to create order!")
|
||||
print("Order created successfully")
|
||||
print("Performing domain verification...")
|
||||
try:
|
||||
self.get_and_set_authorizations(order_data, webroot, mode, ips)
|
||||
except Exception as e:
|
||||
raise Exception("Domain verification failed! {}".format(e))
|
||||
# 完成订单
|
||||
print("Creating CSR...")
|
||||
csr, private_key = self.create_csr(ips)
|
||||
print("Sending CSR and completing order...")
|
||||
res = self.acme_request(order_data['finalize'], payload={
|
||||
"csr": self.calculate_safe_base64(csr)
|
||||
})
|
||||
if res.status_code not in [200, 201]:
|
||||
raise Exception("Failed to complete order! {}".format(res.json()))
|
||||
# 获取证书
|
||||
print("Retrieving certificate...")
|
||||
cert_url = res.json().get('certificate')
|
||||
if not cert_url:
|
||||
raise Exception("Failed to retrieve certificate URL!")
|
||||
cert_res = self.acme_request(cert_url, payload="")
|
||||
if cert_res.status_code not in [200, 201]:
|
||||
raise Exception("Failed to retrieve certificate! {}".format(cert_res.json()))
|
||||
print("Certificate retrieved successfully!")
|
||||
cert_pem = cert_res.content.decode()
|
||||
# 保存证书和私钥
|
||||
if not path:
|
||||
path = "/www/server/panel/ssl"
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
cert_path = os.path.join(path, 'certificate.pem')
|
||||
key_path = os.path.join(path, 'privateKey.pem')
|
||||
with open(cert_path, 'w') as f:
|
||||
f.write(cert_pem)
|
||||
with open(key_path, 'w') as f:
|
||||
f.write(private_key.decode())
|
||||
return cert_path, key_path
|
||||
|
||||
def create_order(self, ips):
|
||||
identifiers = []
|
||||
for ip in ips:
|
||||
identifiers.append({"type": "ip", "value": ip})
|
||||
payload = {"identifiers": identifiers, "profile": "shortlived"}
|
||||
print("Create order, domain name list:{}".format(','.join(ips)))
|
||||
res = self.acme_request(self._apis['newOrder'], payload)
|
||||
if not res.status_code in [201,200]: # 如果创建失败
|
||||
print("Failed to create order, attempting to fix error...")
|
||||
e_body = res.json()
|
||||
if 'type' in e_body:
|
||||
# 如果随机数失效
|
||||
if e_body['type'].find('error:badNonce') != -1:
|
||||
print("Nonce invalid, retrieving new nonce and retrying...")
|
||||
self.get_nonce(force=True)
|
||||
res = self.acme_request(self._apis['newOrder'], payload)
|
||||
# 如果帐户失效
|
||||
if e_body['detail'].find('KeyID header contained an invalid account URL') != -1:
|
||||
print("Account invalid, re-registering account and retrying...")
|
||||
k = "Production"
|
||||
del(self._config['account'][k])
|
||||
self.get_kid()
|
||||
self.get_nonce(force=True)
|
||||
res = self.acme_request(self._apis['newOrder'], payload)
|
||||
if not res.status_code in [201,200]:
|
||||
print(res.json())
|
||||
# 2025/12/25 aapanel
|
||||
raise Exception(str(res.json()))
|
||||
# return {}
|
||||
return res.json()
|
||||
|
||||
# UTC时间转时间戳
|
||||
def utc_to_time(self, utc_string):
|
||||
try:
|
||||
utc_string = utc_string.split('.')[0]
|
||||
utc_date = datetime.datetime.strptime(
|
||||
utc_string, "%Y-%m-%dT%H:%M:%S")
|
||||
# 按北京时间返回
|
||||
return int(time.mktime(utc_date.timetuple())) + (3600 * 8)
|
||||
except:
|
||||
return int(time.time() + 86400 * 7)
|
||||
|
||||
def get_keyauthorization(self, token):
|
||||
acme_header_jwk_json = json.dumps(
|
||||
self.get_acme_header("GET_THUMBPRINT")["jwk"], sort_keys=True, separators=(",", ":")
|
||||
)
|
||||
acme_thumbprint = self.calculate_safe_base64(
|
||||
hashlib.sha256(acme_header_jwk_json.encode("utf8")).digest()
|
||||
)
|
||||
acme_keyauthorization = "{0}.{1}".format(token, acme_thumbprint)
|
||||
base64_of_acme_keyauthorization = self.calculate_safe_base64(
|
||||
hashlib.sha256(acme_keyauthorization.encode("utf8")).digest()
|
||||
)
|
||||
|
||||
return acme_keyauthorization, base64_of_acme_keyauthorization
|
||||
|
||||
# 获取并设置验证信息
|
||||
def get_and_set_authorizations(self, order_data, webroot=None, mode=None, ips=None):
|
||||
import os
|
||||
|
||||
if 'authorizations' not in order_data:
|
||||
raise Exception("Abnormal order data, missing authorization information!")
|
||||
for auth_url in order_data['authorizations']:
|
||||
res = self.acme_request(auth_url, payload="")
|
||||
if not res.status_code in [200, 201]:
|
||||
raise Exception("Failed to get authorization information! {}".format(res.json()))
|
||||
s_body = res.json()
|
||||
if 'status' in s_body:
|
||||
if s_body['status'] in ['invalid']:
|
||||
raise Exception("Invalid order, current order status is verification failed!")
|
||||
if s_body['status'] in ['valid']: # 跳过无需验证的域名
|
||||
continue
|
||||
for challenge in s_body['challenges']:
|
||||
if challenge['type'] == "http-01":
|
||||
break
|
||||
if challenge['type'] != "http-01":
|
||||
raise Exception("http-01 verification method not found, cannot continue applying for certificate!")
|
||||
# 检查是否需要验证
|
||||
check_auth_data = self.check_auth_status(challenge['url'])
|
||||
if check_auth_data.json()['status'] == 'invalid':
|
||||
raise Exception('Domain verification failed, please try applying again!')
|
||||
if check_auth_data.json()['status'] == 'valid':
|
||||
continue
|
||||
|
||||
acme_keyauthorization, auth_value = self.get_keyauthorization(
|
||||
challenge['token'])
|
||||
print(challenge)
|
||||
|
||||
if mode:
|
||||
if mode == 'standalone':
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
import threading
|
||||
import os
|
||||
|
||||
class ACMERequestHandler(SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
# 屏蔽默认的请求日志输出
|
||||
return
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/.well-known/acme-challenge/{}'.format(challenge['token']):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(acme_keyauthorization.encode())
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
server_address = ('', 80)
|
||||
httpd = HTTPServer(server_address, ACMERequestHandler)
|
||||
|
||||
def start_server():
|
||||
httpd.serve_forever()
|
||||
|
||||
server_thread = threading.Thread(target=start_server)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
|
||||
# 2025/12/26 aapanel
|
||||
time.sleep(2) # 等待服务器启动
|
||||
try:
|
||||
# ========================= 校验服务 ==================================
|
||||
server_started = False
|
||||
challenge_local = f"http://127.0.0.1/.well-known/acme-challenge/{challenge['token']}"
|
||||
for i in range(10): # 最大尝试时间5秒
|
||||
try:
|
||||
# 临时检查80 打印
|
||||
with socket.create_connection(("127.0.0.1", 80), timeout=0.2):
|
||||
print("Temporary server started on port 80.")
|
||||
|
||||
response = requests.get(challenge_local, timeout=0.5)
|
||||
if response.status_code == 200 and response.text == acme_keyauthorization:
|
||||
print("Temporary server started and responding correctly challenge token on port 80.")
|
||||
server_started = True
|
||||
break
|
||||
else:
|
||||
print("Temporary server response incorrect, retrying...")
|
||||
time.sleep(0.5)
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
|
||||
if not server_started:
|
||||
# raise Exception("Failed to start temporary HTTP server on port 80 in 5 seconds.")
|
||||
print("Failed to start temporary HTTP server on port 80 in 5 seconds.")
|
||||
|
||||
# ========================= 通知ACME服务器进行验证 ===============================
|
||||
self.acme_request(challenge['url'], payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
|
||||
self.check_auth_status(challenge['url'], [
|
||||
'valid', 'invalid'])
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
server_thread.join()
|
||||
elif mode == 'nginx':
|
||||
tmp_path = '/www/server/panel/vhost/nginx/tmp_apply_ip_ssl.conf'
|
||||
files = find_nginx_files_by_servername(ips[0])
|
||||
if not files:
|
||||
print("No related Nginx configuration files found, attempting to create temporary configuration file...")
|
||||
if not os.path.exists('/www/server/panel/vhost/nginx'):
|
||||
raise Exception("No Nginx configuration files found, and Nginx configuration directory does not exist!")
|
||||
# 如果没有找到相关配置文件,则创建一个临时配置文件
|
||||
with open(tmp_path, 'w') as f:
|
||||
f.write("""server
|
||||
{{
|
||||
listen 80;
|
||||
server_name {0};
|
||||
location /.well-known/acme-challenge/{1} {{
|
||||
default_type text/plain;
|
||||
return 200 "{2}";
|
||||
}}
|
||||
}}
|
||||
""".format(ips[0], challenge['token'], acme_keyauthorization))
|
||||
try:
|
||||
for file in files:
|
||||
print("Modifying Nginx configuration file: {}".format(file))
|
||||
insert_location_into_server(file, ips[0], verify_file=challenge['token'], verify_content=acme_keyauthorization)
|
||||
# 重新加载Nginx配置
|
||||
subprocess.run(["nginx", "-t"], check=True)
|
||||
subprocess.run(["nginx", "-s", "reload"], check=True)
|
||||
|
||||
# 通知ACME服务器进行验证
|
||||
self.acme_request(challenge['url'],
|
||||
payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
|
||||
self.check_auth_status(challenge['url'], [
|
||||
'valid', 'invalid'])
|
||||
finally:
|
||||
for file in files:
|
||||
print("Restoring Nginx configuration file: {}".format(file))
|
||||
# 恢复备份文件
|
||||
backup_file = file + ".bak"
|
||||
if os.path.exists(backup_file):
|
||||
shutil.move(backup_file, file)
|
||||
if not files:
|
||||
print("Deleting temporary Nginx configuration file...")
|
||||
# 删除临时配置文件
|
||||
os.remove(tmp_path)
|
||||
# 重新加载Nginx配置
|
||||
subprocess.run(["nginx", "-t"], check=True)
|
||||
subprocess.run(["nginx", "-s", "reload"], check=True)
|
||||
|
||||
elif mode == 'apache':
|
||||
tmp_path = '/www/server/panel/vhost/apache/tmp_apply_ip_ssl.conf'
|
||||
files = find_apache_conf_files(ips[0])
|
||||
if not files:
|
||||
print("No related Apache configuration files found, attempting to create temporary configuration file...")
|
||||
if not os.path.exists('/www/server/panel/vhost/apache'):
|
||||
raise Exception("No Apache configuration files found, and Apache configuration directory does not exist!")
|
||||
# 如果没有找到相关配置文件,则创建一个临时配置文件
|
||||
with open(tmp_path, 'w') as f:
|
||||
f.write("""<VirtualHost *:80>
|
||||
ServerName {0}
|
||||
<Location /.well-known/acme-challenge/{1}>
|
||||
Require all granted
|
||||
Header set Content-Type "text/plain"
|
||||
</Location>
|
||||
Alias /.well-known/acme-challenge/{1} /tmp/{1}
|
||||
</VirtualHost>
|
||||
""".format(ips[0], challenge['token']))
|
||||
try:
|
||||
for file in files:
|
||||
print("Modifying Apache configuration file: {}".format(file))
|
||||
insert_location_into_vhost(file, ips[0], verify_file=challenge['token'])
|
||||
# 写入验证文件
|
||||
with open('/tmp/{}'.format(challenge['token']), 'w') as f:
|
||||
f.write(acme_keyauthorization)
|
||||
# 重新加载Apache配置
|
||||
subprocess.run(["/etc/init.d/httpd", "reload"], check=True)
|
||||
|
||||
# 通知ACME服务器进行验证
|
||||
self.acme_request(challenge['url'],
|
||||
payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
|
||||
self.check_auth_status(challenge['url'], [
|
||||
'valid', 'invalid'])
|
||||
finally:
|
||||
for file in files:
|
||||
print("Restoring Apache configuration file: {}".format(file))
|
||||
# 恢复备份文件
|
||||
backup_file = file + ".bak"
|
||||
if os.path.exists(backup_file):
|
||||
shutil.move(backup_file, file)
|
||||
if not files:
|
||||
print("Deleting temporary Apache configuration file...")
|
||||
# 删除临时配置文件
|
||||
os.remove(tmp_path)
|
||||
# 重新加载Apache配置
|
||||
subprocess.run(["systemctl", "restart", "httpd"], check=True)
|
||||
else:
|
||||
# 使用webroot方式验证
|
||||
challenge_path = os.path.join(
|
||||
webroot, '.well-known', 'acme-challenge')
|
||||
if not os.path.exists(challenge_path):
|
||||
os.makedirs(challenge_path)
|
||||
file_path = os.path.join(challenge_path, challenge['token'])
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(acme_keyauthorization)
|
||||
|
||||
try:
|
||||
# 通知ACME服务器进行验证
|
||||
self.acme_request(challenge['url'], payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
|
||||
self.check_auth_status(challenge['url'], [
|
||||
'valid', 'invalid'])
|
||||
finally:
|
||||
os.remove(file_path)
|
||||
|
||||
# 检查验证状态
|
||||
def check_auth_status(self, url, desired_status=None):
|
||||
desired_status = desired_status or ["pending", "valid", "invalid"]
|
||||
number_of_checks = 0
|
||||
authorization_status = "pending"
|
||||
while True:
|
||||
print("|- {} checking verification result...".format(number_of_checks + 1))
|
||||
if desired_status == ['valid', 'invalid']:
|
||||
time.sleep(self._wait_time)
|
||||
check_authorization_status_response = self.acme_request(url, "")
|
||||
a_auth = check_authorization_status_response.json()
|
||||
if not isinstance(a_auth, dict):
|
||||
continue
|
||||
authorization_status = a_auth["status"]
|
||||
number_of_checks += 1
|
||||
if authorization_status in desired_status:
|
||||
if authorization_status == "invalid":
|
||||
try:
|
||||
if 'error' in a_auth['challenges'][0]:
|
||||
ret_title = a_auth['challenges'][0]['error']['detail']
|
||||
elif 'error' in a_auth['challenges'][1]:
|
||||
ret_title = a_auth['challenges'][1]['error']['detail']
|
||||
elif 'error' in a_auth['challenges'][2]:
|
||||
ret_title = a_auth['challenges'][2]['error']['detail']
|
||||
else:
|
||||
ret_title = str(a_auth)
|
||||
except:
|
||||
ret_title = str(a_auth)
|
||||
raise StopIteration(
|
||||
"{0} >>>> {1}".format(
|
||||
ret_title,
|
||||
json.dumps(a_auth)
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
if number_of_checks == self._max_check_num:
|
||||
raise StopIteration(
|
||||
"Error: Verification attempted {0} times. Maximum verification attempts: {1}. Verification interval: {2} seconds.".format(
|
||||
number_of_checks,
|
||||
self._max_check_num,
|
||||
self._wait_time
|
||||
)
|
||||
)
|
||||
print("|-Verification result: {}".format(authorization_status))
|
||||
return check_authorization_status_response
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
# 解析命令行参数
|
||||
parser = argparse.ArgumentParser(description='Auto-apply IP SSL certificate script')
|
||||
parser.add_argument('-ips', type=str, required=True, help='IP addresses to apply SSL certificate for', dest='ips')
|
||||
parser.add_argument('-email', type=str, required=False, help='Email for SSL certificate application', dest='email')
|
||||
parser.add_argument('-w', type=str, help='Website root directory', dest='webroot')
|
||||
parser.add_argument('--standalone', help='Apply certificate using standalone mode', dest='standalone', action='store_true')
|
||||
parser.add_argument('--nginx', help='Apply certificate using nginx mode', dest='nginx', action='store_true')
|
||||
parser.add_argument('--apache', help='Apply certificate using apache mode', dest='apache', action='store_true')
|
||||
parser.add_argument('-path', type=str, help='Certificate save path', dest='path')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.standalone and not args.webroot and not args.nginx and not args.apache:
|
||||
print("No verification mode detected, will attempt to auto-select verification mode!")
|
||||
# 自动选择验证模式
|
||||
# 判断80端口是否被占用
|
||||
use_80 = False
|
||||
result = subprocess.run(
|
||||
["lsof", "-i:80"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if result.stdout:
|
||||
result = subprocess.run(
|
||||
["netstat", "-lntup", "|", "grep", "80"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if result.stdout:
|
||||
use_80 = True
|
||||
if use_80:
|
||||
print("It is detected that port 80 is occupied, try to use Nginx or Apache mode to verify...")
|
||||
# 检查是否安装Nginx
|
||||
if os.path.exists('/www/server/nginx/sbin/nginx'):
|
||||
args.nginx = True
|
||||
print("Selected Nginx mode for verification...")
|
||||
elif os.path.exists('/www/server/apache/bin/httpd'):
|
||||
args.apache = True
|
||||
print("Selected Apache mode for verification...")
|
||||
else:
|
||||
print("[ERROR] Nginx or Apache installation not detected, cannot use Nginx or Apache mode for verification! Please release port 80 and try again.")
|
||||
exit(1)
|
||||
else:
|
||||
args.standalone = True
|
||||
print("Port 80 not occupied, selected standalone mode for verification...")
|
||||
|
||||
if not args.email:
|
||||
# 使用默认邮箱
|
||||
email = "demo@aapanel.com"
|
||||
else:
|
||||
email = args.email
|
||||
|
||||
ips = args.ips.split(',')
|
||||
# 先只支持单个IP申请
|
||||
if len(ips) > 1 and not args.standalone:
|
||||
print("[ERROR] Multiple IP SSL certificate application not supported in non-standalone mode!")
|
||||
exit(1)
|
||||
# 先只支持IPv4
|
||||
if not is_ipv4(ips[0]):
|
||||
print("[ERROR] Only IPv4 addresses are supported for SSL certificate application at this time!")
|
||||
exit(1)
|
||||
auto_ssl = AutoApplyIPSSL()
|
||||
mode = None
|
||||
if args.standalone:
|
||||
mode = 'standalone'
|
||||
elif args.nginx:
|
||||
mode = 'nginx'
|
||||
elif args.apache:
|
||||
mode = 'apache'
|
||||
try:
|
||||
cert_path, key_path = auto_ssl.apply_ip_ssl(ips, email, webroot=args.webroot, mode=mode, path=args.path)
|
||||
except Exception as e:
|
||||
# 2025/12/25 aapanel
|
||||
print(f"[ERROR] Certificate application failed! Error details: {e}", file=sys.stderr)
|
||||
exit(1)
|
||||
exit(0)
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# encoding utf-8
|
||||
import subprocess
|
||||
import requests
|
||||
import os
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
|
||||
class UpdateSiteTotal:
|
||||
# 类级别的常量定义,集中管理固定配置
|
||||
DEFAULT_USER_AGENT = "BT-Panel/10.0"
|
||||
INSTALL_SCRIPT_TIMEOUT = 600 # 安装脚本超时时间(秒)
|
||||
VERSION_CHECK_TIMEOUT = 10 # 版本检查超时时间(秒)
|
||||
SCRIPT_DOWNLOAD_TIMEOUT = 15 # 脚本下载超时时间(秒)
|
||||
|
||||
site_total_path = '/www/server/site_total'
|
||||
download_url = 'https://node.aapanel.com/site_total/'
|
||||
version_url = download_url + 'version.txt'
|
||||
install_sh = download_url + 'install.sh'
|
||||
site_total_bin = os.path.join(site_total_path, 'site_total')
|
||||
|
||||
def __init__(self):
|
||||
"""初始化更新工具
|
||||
"""
|
||||
|
||||
# 初始化日志配置
|
||||
self._init_logger()
|
||||
|
||||
def _init_logger(self):
|
||||
"""初始化日志配置(封装为独立方法,便于维护)"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# ------------------------------
|
||||
# 版本相关方法
|
||||
# ------------------------------
|
||||
def get_current_version(self):
|
||||
"""获取当前安装的版本号"""
|
||||
if not self.is_installed():
|
||||
self._log_warning(f"{self.site_total_bin} is not detected, so the current version cannot be obtained")
|
||||
return None
|
||||
|
||||
try:
|
||||
# 执行版本命令
|
||||
result = subprocess.run(
|
||||
[self.site_total_bin, 'version'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=self.VERSION_CHECK_TIMEOUT
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, Exception) as e:
|
||||
return self._handle_version_cmd_error(e)
|
||||
|
||||
# 解析版本号(调用辅助方法)
|
||||
return self._parse_version_output(result)
|
||||
|
||||
def _parse_version_output(self, result):
|
||||
"""解析版本命令的输出结果,提取版本号"""
|
||||
# 合并stdout和stderr,避免版本信息输出到错误流
|
||||
output_lines = result.stdout.splitlines() + result.stderr.splitlines()
|
||||
|
||||
for line in output_lines:
|
||||
if "Version:" in line:
|
||||
# 提取版本号并格式化(确保x.y格式)
|
||||
raw_version = line.split(":")[-1].strip()
|
||||
return self._format_version(raw_version)
|
||||
|
||||
self._log_warning(f"The version number cannot be parsed from the output: {output_lines}")
|
||||
return None
|
||||
|
||||
def _format_version(self, raw_version):
|
||||
"""将原始版本号格式化为x.y的字符串格式"""
|
||||
try:
|
||||
float_version = float(raw_version)
|
||||
return f"{float_version:.1f}"
|
||||
except ValueError:
|
||||
self._log_error(f"The version number format is abnormal. It's the original value: {raw_version}")
|
||||
return None
|
||||
|
||||
def _handle_version_cmd_error(self, error):
|
||||
"""处理版本命令执行中的异常"""
|
||||
if isinstance(error, subprocess.CalledProcessError):
|
||||
self._log_error(f"The version command failed to execute: {error.stderr.strip()}")
|
||||
elif isinstance(error, subprocess.TimeoutExpired):
|
||||
self._log_error("The version command has timed out")
|
||||
else:
|
||||
self._log_error(f"An error occurred when obtaining the version number: {str(error)}")
|
||||
return None
|
||||
|
||||
def get_latest_version(self):
|
||||
"""获取最新版本号(返回字符串格式)"""
|
||||
try:
|
||||
response = requests.get(
|
||||
self.version_url,
|
||||
timeout=self.VERSION_CHECK_TIMEOUT,
|
||||
headers={"User-Agent": self.DEFAULT_USER_AGENT}
|
||||
)
|
||||
response.raise_for_status()
|
||||
latest_version = response.text.strip()
|
||||
|
||||
if not latest_version:
|
||||
self._log_warning("The latest version number obtained is empty")
|
||||
return None
|
||||
return latest_version
|
||||
except requests.RequestException as e:
|
||||
self._log_error(f"Failed to get the latest version: {str(e)}")
|
||||
return None
|
||||
|
||||
def check_update_available(self):
|
||||
"""检查是否有可用更新(基于float格式版本号比较)"""
|
||||
current_version_str = self.get_current_version()
|
||||
latest_version_str = self.get_latest_version()
|
||||
|
||||
# 检查版本号获取结果
|
||||
if not current_version_str:
|
||||
return False, "The current version cannot be obtained"
|
||||
if not latest_version_str:
|
||||
return False, "The latest version cannot be obtained"
|
||||
|
||||
# 版本号比较
|
||||
try:
|
||||
current_version = float(current_version_str)
|
||||
latest_version = float(latest_version_str)
|
||||
except ValueError as e:
|
||||
error_msg = f"Version number format error (it should be x.y): {str(e)}"
|
||||
self._log_error(f"{error_msg}(Current: {current_version_str}, Latest: {latest_version_str})")
|
||||
return False, error_msg
|
||||
|
||||
if latest_version > current_version:
|
||||
return True, f"Current version: {current_version_str}, The latest version: {latest_version_str}"
|
||||
else:
|
||||
return False, f"Current version: {current_version_str}, The latest version: {latest_version_str}"
|
||||
|
||||
# ------------------------------
|
||||
# 安装/更新相关方法
|
||||
# ------------------------------
|
||||
def is_installed(self):
|
||||
"""检查site_total是否已安装且可执行"""
|
||||
return os.path.exists(self.site_total_bin) and os.access(self.site_total_bin, os.X_OK)
|
||||
|
||||
def install_or_update(self):
|
||||
"""执行安装或更新操作"""
|
||||
# 下载安装脚本
|
||||
install_script = self._download_install_script()
|
||||
if not install_script:
|
||||
return False, "The download and installation script failed"
|
||||
|
||||
# 执行安装脚本
|
||||
return self._execute_install_script(install_script)
|
||||
|
||||
def _download_install_script(self):
|
||||
"""下载安装脚本"""
|
||||
try:
|
||||
response = requests.get(
|
||||
self.install_sh,
|
||||
timeout=self.SCRIPT_DOWNLOAD_TIMEOUT,
|
||||
headers={"User-Agent": self.DEFAULT_USER_AGENT}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except requests.RequestException as e:
|
||||
self._log_error(f"The download and installation script failed: {str(e)}")
|
||||
return None
|
||||
|
||||
def _execute_install_script(self, script_content):
|
||||
"""执行安装脚本"""
|
||||
temp_script = None
|
||||
try:
|
||||
# 创建临时脚本文件
|
||||
temp_script = self._create_temp_script(script_content)
|
||||
|
||||
# 执行脚本
|
||||
result = subprocess.run(
|
||||
["bash", temp_script],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=self.INSTALL_SCRIPT_TIMEOUT
|
||||
)
|
||||
|
||||
# 检查执行结果
|
||||
if result.returncode != 0:
|
||||
error_msg = (f"The installation script failed to execute (return code: {result.returncode})\n"
|
||||
f"Error output: {result.stderr.strip()[:500]}")
|
||||
self._log_error(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
if not self.is_installed():
|
||||
return False, "The installation is complete but the executable file was not found"
|
||||
|
||||
return True, "Installation/update successful"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "The installation script has timed out"
|
||||
except Exception as e:
|
||||
return False, f"An error occurred during the installation process: {str(e)}"
|
||||
finally:
|
||||
# 清理临时文件
|
||||
self._cleanup_temp_script(temp_script)
|
||||
|
||||
def _create_temp_script(self, content):
|
||||
"""创建临时脚本文件(辅助方法,封装文件操作)"""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.sh',
|
||||
delete=False,
|
||||
dir='/tmp'
|
||||
) as f:
|
||||
f.write(content)
|
||||
return f.name
|
||||
|
||||
def _cleanup_temp_script(self, script_path):
|
||||
"""清理临时脚本文件(辅助方法,确保资源释放)"""
|
||||
if script_path and os.path.exists(script_path):
|
||||
try:
|
||||
os.remove(script_path)
|
||||
except OSError as e:
|
||||
self._log_warning(f"Temporary files cannot be deleted {script_path}: {str(e)}")
|
||||
|
||||
# ------------------------------
|
||||
# 主逻辑与工具方法
|
||||
# ------------------------------
|
||||
def update_if_needed(self):
|
||||
"""有更新时执行更新,未安装时执行安装(主逻辑入口)"""
|
||||
if not self.is_installed():
|
||||
self._log_info("Installation not detected. Start the installation")
|
||||
return self.install_or_update()
|
||||
|
||||
has_update, msg = self.check_update_available()
|
||||
if not has_update:
|
||||
return False, f"No update required: {msg}"
|
||||
|
||||
self._log_info(f"Update detected: {msg},Start implementing the update")
|
||||
return self.install_or_update()
|
||||
|
||||
# 日志工具方法(封装日志调用,便于统一管理)
|
||||
def _log_info(self, msg):
|
||||
logging.info(msg)
|
||||
|
||||
def _log_warning(self, msg):
|
||||
logging.warning(msg)
|
||||
|
||||
def _log_error(self, msg):
|
||||
logging.error(msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
updater = UpdateSiteTotal()
|
||||
success, message = updater.update_if_needed()
|
||||
logging.info(f"Operation result: {'Success' if success else 'Failure'} - {message}")
|
||||
+25
-1
@@ -50,6 +50,30 @@ if [ -f "/etc/init.d/php-fpm-74" ];then
|
||||
/etc/init.d/php-fpm-74 reload
|
||||
fi
|
||||
|
||||
if [ -f "/etc/init.d/php-fpm-80" ];then
|
||||
/etc/init.d/php-fpm-80 reload
|
||||
fi
|
||||
|
||||
if [ -f "/etc/init.d/php-fpm-81" ];then
|
||||
/etc/init.d/php-fpm-81 reload
|
||||
fi
|
||||
|
||||
if [ -f "/etc/init.d/php-fpm-82" ];then
|
||||
/etc/init.d/php-fpm-82 reload
|
||||
fi
|
||||
|
||||
if [ -f "/etc/init.d/php-fpm-83" ];then
|
||||
/etc/init.d/php-fpm-83 reload
|
||||
fi
|
||||
|
||||
if [ -f "/etc/init.d/php-fpm-84" ];then
|
||||
/etc/init.d/php-fpm-84 reload
|
||||
fi
|
||||
|
||||
if [ -f "/etc/init.d/php-fpm-85" ];then
|
||||
/etc/init.d/php-fpm-85 reload
|
||||
fi
|
||||
|
||||
if [ -f "/etc/init.d/mysqld" ];then
|
||||
/etc/init.d/mysqld reload
|
||||
fi
|
||||
@@ -73,4 +97,4 @@ sleep 2
|
||||
sync
|
||||
echo 3 > /proc/sys/vm/drop_caches
|
||||
|
||||
echo '----------------------------------------------------------------------------'
|
||||
echo '----------------------------------------------------------------------------'
|
||||
|
||||
+509
-172
@@ -1,65 +1,319 @@
|
||||
# coding: utf-8
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import Optional, Dict
|
||||
from functools import wraps, partial
|
||||
from typing import Optional, Dict, Callable
|
||||
|
||||
import fcntl
|
||||
|
||||
os.chdir("/www/server/panel")
|
||||
sys.path.insert(0, "class/")
|
||||
sys.path.insert(0, "class_v2/")
|
||||
from public import (
|
||||
WriteLog,
|
||||
get_setup_path,
|
||||
get_panel_path,
|
||||
readFile,
|
||||
writeFile,
|
||||
get_url,
|
||||
)
|
||||
from public import readFile
|
||||
|
||||
SETUP_PATH = get_setup_path()
|
||||
SETUP_PATH = "/www/server"
|
||||
DATA_PATH = os.path.join(SETUP_PATH, "panel/data")
|
||||
PLUGINS_PATH = os.path.join(SETUP_PATH, "panel/plugin")
|
||||
|
||||
DAEMON_SERVICE = os.path.join(DATA_PATH, "daemon_service.pl")
|
||||
DAEMON_SERVICE_LOCK = os.path.join(DATA_PATH, "daemon_service_lock.pl")
|
||||
DAEMON_RESTART_RECORD = os.path.join(DATA_PATH, "daemon_restart_record.pl")
|
||||
MANUAL_FLAG = os.path.join(get_panel_path(), "data/mod_push_data", "manual_flag.pl")
|
||||
MANUAL_FLAG = os.path.join(SETUP_PATH, "panel/data/mod_push_data", "manual_flag.pl")
|
||||
|
||||
|
||||
def run_command(cmd, timeout=5) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False
|
||||
)
|
||||
output = result.stdout.strip()
|
||||
return output if output else ""
|
||||
except subprocess.TimeoutExpired as t:
|
||||
write_logs(f"Command execution timed out: {cmd}, error: {t}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
write_logs(f"Command execution failed: {cmd}, error: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def service_shop_name(services_name: str) -> str:
|
||||
# 对应商店名字
|
||||
shop_name = {
|
||||
"postfix": "mail_sys",
|
||||
"pgsql": "pgsql_manager",
|
||||
"pure-ftpd": "pureftpd",
|
||||
|
||||
"php-fpm-52": "php-5.2",
|
||||
"php-fpm-53": "php-5.3",
|
||||
"php-fpm-54": "php-5.4",
|
||||
"php-fpm-55": "php-5.5",
|
||||
"php-fpm-56": "php-5.6",
|
||||
"php-fpm-70": "php-7.0",
|
||||
"php-fpm-71": "php-7.1",
|
||||
"php-fpm-72": "php-7.2",
|
||||
"php-fpm-73": "php-7.3",
|
||||
"php-fpm-74": "php-7.4",
|
||||
"php-fpm-80": "php-8.0",
|
||||
"php-fpm-81": "php-8.1",
|
||||
"php-fpm-82": "php-8.2",
|
||||
"php-fpm-83": "php-8.3",
|
||||
"php-fpm-84": "php-8.4",
|
||||
"php-fpm-85": "php-8.5",
|
||||
}
|
||||
return shop_name.get(services_name, services_name)
|
||||
|
||||
|
||||
def pretty_title(services_name: str) -> str:
|
||||
title = {
|
||||
"apache": "Apache",
|
||||
"nginx": "Nginx",
|
||||
"openlitespeed": "OpenLiteSpeed",
|
||||
"redis": "Redis",
|
||||
"mysql": "MySQL/MariaDB",
|
||||
"mongodb": "MongoDB",
|
||||
"pgsql": "PostgreSQL",
|
||||
"pure-ftpd": "Pure-FTPd",
|
||||
"memcached": "Memcached",
|
||||
"ssh": "SSH",
|
||||
"postfix": "Postfix",
|
||||
"pdns": "PowerDNS",
|
||||
|
||||
# PHP-FPM
|
||||
"php-fpm-52": "PHP 5.2 FPM",
|
||||
"php-fpm-53": "PHP 5.3 FPM",
|
||||
"php-fpm-54": "PHP 5.4 FPM",
|
||||
"php-fpm-55": "PHP 5.5 FPM",
|
||||
"php-fpm-56": "PHP 5.6 FPM",
|
||||
"php-fpm-70": "PHP 7.0 FPM",
|
||||
"php-fpm-71": "PHP 7.1 FPM",
|
||||
"php-fpm-72": "PHP 7.2 FPM",
|
||||
"php-fpm-73": "PHP 7.3 FPM",
|
||||
"php-fpm-74": "PHP 7.4 FPM",
|
||||
"php-fpm-80": "PHP 8.0 FPM",
|
||||
"php-fpm-81": "PHP 8.1 FPM",
|
||||
"php-fpm-82": "PHP 8.2 FPM",
|
||||
"php-fpm-83": "PHP 8.3 FPM",
|
||||
"php-fpm-84": "PHP 8.4 FPM",
|
||||
"php-fpm-85": "PHP 8.5 FPM",
|
||||
|
||||
# plugins
|
||||
"btwaf": "aaPanel WAF",
|
||||
"fail2ban": "Fail2Ban",
|
||||
}
|
||||
return title.get(services_name, services_name)
|
||||
|
||||
|
||||
# =======================================================
|
||||
def ssh_ver() -> str:
|
||||
version_info = run_command(["ssh", "-V"])
|
||||
if version_info and "OpenSSH" in version_info:
|
||||
return version_info.split(",")[0]
|
||||
return ""
|
||||
|
||||
|
||||
def postfix_ver() -> str:
|
||||
# mail_version = x.x.x
|
||||
output = run_command(["/usr/sbin/postconf", "mail_version"])
|
||||
if output and "=" in output:
|
||||
return output.split("=")[-1].strip()
|
||||
return ""
|
||||
|
||||
|
||||
def pgsql_pid() -> str:
|
||||
pid_str = run_command(["pgrep", "-o", "postgres"])
|
||||
if pid_str and pid_str.isdigit():
|
||||
return pid_str
|
||||
return ""
|
||||
|
||||
|
||||
def pgsql_ver() -> str:
|
||||
# psql (PostgreSQL) 18.0
|
||||
output = run_command([f"{SETUP_PATH}/pgsql/bin/psql", "--version"])
|
||||
if output:
|
||||
from re import search
|
||||
match = search(r"(\d+\.\d+(\.\d+)?)", output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
org_file = f"{SETUP_PATH}/pgsql/data/PG_VERSION",
|
||||
if os.path.exists(str(org_file)):
|
||||
try:
|
||||
with open(str(org_file), "r") as f:
|
||||
version = f.read().strip()
|
||||
return version
|
||||
except:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
def pdns_pid() -> str:
|
||||
pid_str = run_command(["pgrep", "-x", "pdns_server"])
|
||||
if pid_str and pid_str.isdigit():
|
||||
return pid_str
|
||||
return ""
|
||||
|
||||
|
||||
def pdns_ver() -> str:
|
||||
# PowerDNS Authoritative Server x.x.x
|
||||
output = run_command(["pdns_server", "--version"])
|
||||
if not output:
|
||||
return ""
|
||||
from re import search
|
||||
match = search(r"(\d+\.\d+\.\d+)", output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
if "PowerDNS" in output:
|
||||
return output.split()[-1]
|
||||
return ""
|
||||
|
||||
|
||||
def waf_pid() -> str:
|
||||
pid_str = run_command(["pgrep", "-x", "BT-WAF"])
|
||||
if pid_str and pid_str.isdigit():
|
||||
return pid_str
|
||||
return ""
|
||||
|
||||
|
||||
def pluign_ver(plugin_name: str) -> str:
|
||||
info = f"{PLUGINS_PATH}/{plugin_name}/info.json"
|
||||
if not os.path.exists(info):
|
||||
return ""
|
||||
try:
|
||||
with open(info, "r") as f:
|
||||
json_data = json.load(f)
|
||||
ret = json_data.get("versions", "")
|
||||
return ret
|
||||
except:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
SERVICES_MAP = {
|
||||
# ========================= core base =============================
|
||||
"panel": (
|
||||
"BT-Panel", f"{SETUP_PATH}/panel/logs/panel.pid", f"{SETUP_PATH}/panel/init.sh"
|
||||
"BT-Panel", f"{SETUP_PATH}/panel/logs/panel.pid", f"{SETUP_PATH}/panel/init.sh",
|
||||
f"{SETUP_PATH}/nginx/version.pl",
|
||||
),
|
||||
"apache": (
|
||||
"httpd", f"{SETUP_PATH}/apache/logs/httpd.pid", "/etc/init.d/httpd"
|
||||
"httpd", f"{SETUP_PATH}/apache/logs/httpd.pid", "/etc/init.d/httpd",
|
||||
f"{SETUP_PATH}/apache/version.pl",
|
||||
),
|
||||
"nginx": (
|
||||
"nginx", f"{SETUP_PATH}/nginx/logs/nginx.pid", "/etc/init.d/nginx"
|
||||
),
|
||||
"redis": (
|
||||
"redis-server", f"{SETUP_PATH}/redis/redis.pid", "/etc/init.d/redis"
|
||||
),
|
||||
"mysql": (
|
||||
"mysqld", "/tmp/mysql.sock", "/etc/init.d/mysqld"
|
||||
),
|
||||
"mongodb": (
|
||||
"mongod", f"{SETUP_PATH}/mongodb/log/configsvr.pid", "/etc/init.d/mongodb"
|
||||
),
|
||||
"pure-ftpd": (
|
||||
"pure-ftpd", "/var/run/pure-ftpd.pid", "/etc/init.d/pure-ftpd"
|
||||
),
|
||||
"memcached": (
|
||||
"memcached", "/var/run/memcached.pid", "/etc/init.d/memcached"
|
||||
"nginx", f"{SETUP_PATH}/nginx/logs/nginx.pid", "/etc/init.d/nginx",
|
||||
f"{SETUP_PATH}/nginx/version.pl",
|
||||
),
|
||||
"openlitespeed": (
|
||||
"litespeed", "/tmp/lshttpd/lsphp.sock", "/usr/local/lsws/bin/lswsctrl"
|
||||
)
|
||||
"litespeed", "/tmp/lshttpd/lsphp.sock", "/usr/local/lsws/bin/lswsctrl",
|
||||
"/usr/local/lsws/VERSION",
|
||||
),
|
||||
"redis": (
|
||||
"redis-server", f"{SETUP_PATH}/redis/redis.pid", "/etc/init.d/redis",
|
||||
f"{SETUP_PATH}/redis/version.pl",
|
||||
),
|
||||
"mysql": (
|
||||
"mysqld", "/tmp/mysql.sock", "/etc/init.d/mysqld",
|
||||
f"{SETUP_PATH}/mysql/version.pl",
|
||||
),
|
||||
"mongodb": (
|
||||
"mongod", f"{SETUP_PATH}/mongodb/log/configsvr.pid", "/etc/init.d/mongodb",
|
||||
f"{SETUP_PATH}/mongodb/version.pl",
|
||||
),
|
||||
"pgsql": (
|
||||
"postgres", pgsql_pid, "/etc/init.d/pgsql",
|
||||
pgsql_ver,
|
||||
),
|
||||
"pure-ftpd": (
|
||||
"pure-ftpd", "/var/run/pure-ftpd.pid", "/etc/init.d/pure-ftpd",
|
||||
f"{SETUP_PATH}/pure-ftpd/version.pl",
|
||||
),
|
||||
"memcached": (
|
||||
"memcached", "/var/run/memcached.pid", "/etc/init.d/memcached",
|
||||
"/usr/local/memcached/version_check.pl",
|
||||
),
|
||||
"ssh": (
|
||||
"sshd", "/var/run/sshd.pid", "/etc/init.d/ssh",
|
||||
ssh_ver,
|
||||
),
|
||||
"postfix": (
|
||||
"master", "/var/spool/postfix/pid/master.pid", "/etc/init.d/postfix",
|
||||
postfix_ver,
|
||||
),
|
||||
"pdns": (
|
||||
# "pdns_server", pdns_pid, "/usr/sbin/pdns_server",
|
||||
"pdns_server", pdns_pid, "/www/server/panel/class_v2/ssl_dnsV2/aadns.pl",
|
||||
pdns_ver,
|
||||
),
|
||||
|
||||
# ======================== PHP-FPM ============================
|
||||
"php-fpm-52": ("php-fpm", f"{SETUP_PATH}/php/52/var/run/php-fpm.pid", "/etc/init.d/php-fpm-52",
|
||||
f"{SETUP_PATH}/php/52/version.pl"),
|
||||
"php-fpm-53": ("php-fpm", f"{SETUP_PATH}/php/53/var/run/php-fpm.pid", "/etc/init.d/php-fpm-53",
|
||||
f"{SETUP_PATH}/php/53/version.pl"),
|
||||
"php-fpm-54": ("php-fpm", f"{SETUP_PATH}/php/54/var/run/php-fpm.pid", "/etc/init.d/php-fpm-54",
|
||||
f"{SETUP_PATH}/php/54/version.pl"),
|
||||
"php-fpm-55": ("php-fpm", f"{SETUP_PATH}/php/55/var/run/php-fpm.pid", "/etc/init.d/php-fpm-55",
|
||||
f"{SETUP_PATH}/php/55/version.pl"),
|
||||
"php-fpm-56": ("php-fpm", f"{SETUP_PATH}/php/56/var/run/php-fpm.pid", "/etc/init.d/php-fpm-56",
|
||||
f"{SETUP_PATH}/php/56/version.pl"),
|
||||
"php-fpm-70": ("php-fpm", f"{SETUP_PATH}/php/70/var/run/php-fpm.pid", "/etc/init.d/php-fpm-70",
|
||||
f"{SETUP_PATH}/php/70/version.pl"),
|
||||
"php-fpm-71": ("php-fpm", f"{SETUP_PATH}/php/71/var/run/php-fpm.pid", "/etc/init.d/php-fpm-71",
|
||||
f"{SETUP_PATH}/php/71/version.pl"),
|
||||
"php-fpm-72": ("php-fpm", f"{SETUP_PATH}/php/72/var/run/php-fpm.pid", "/etc/init.d/php-fpm-72",
|
||||
f"{SETUP_PATH}/php/72/version.pl"),
|
||||
"php-fpm-73": ("php-fpm", f"{SETUP_PATH}/php/73/var/run/php-fpm.pid", "/etc/init.d/php-fpm-73",
|
||||
f"{SETUP_PATH}/php/73/version.pl"),
|
||||
"php-fpm-74": ("php-fpm", f"{SETUP_PATH}/php/74/var/run/php-fpm.pid", "/etc/init.d/php-fpm-74",
|
||||
f"{SETUP_PATH}/php/74/version.pl"),
|
||||
"php-fpm-80": ("php-fpm", f"{SETUP_PATH}/php/80/var/run/php-fpm.pid", "/etc/init.d/php-fpm-80",
|
||||
f"{SETUP_PATH}/php/80/version.pl"),
|
||||
"php-fpm-81": ("php-fpm", f"{SETUP_PATH}/php/81/var/run/php-fpm.pid", "/etc/init.d/php-fpm-81",
|
||||
f"{SETUP_PATH}/php/81/version.pl"),
|
||||
"php-fpm-82": ("php-fpm", f"{SETUP_PATH}/php/82/var/run/php-fpm.pid", "/etc/init.d/php-fpm-82",
|
||||
f"{SETUP_PATH}/php/82/version.pl"),
|
||||
"php-fpm-83": ("php-fpm", f"{SETUP_PATH}/php/83/var/run/php-fpm.pid", "/etc/init.d/php-fpm-83",
|
||||
f"{SETUP_PATH}/php/83/version.pl"),
|
||||
"php-fpm-84": ("php-fpm", f"{SETUP_PATH}/php/84/var/run/php-fpm.pid", "/etc/init.d/php-fpm-84",
|
||||
f"{SETUP_PATH}/php/84/version_check.pl"),
|
||||
"php-fpm-85": ("php-fpm", f"{SETUP_PATH}/php/85/var/run/php-fpm.pid", "/etc/init.d/php-fpm-85",
|
||||
f"{SETUP_PATH}/php/85/version_check.pl"),
|
||||
|
||||
# ======================== plugins ============================
|
||||
# nginx : btwaf
|
||||
# apache: btwaf_httpd
|
||||
"btwaf": (
|
||||
# "BT-WAF", f"{PLUGINS_PATH}/btwaf/BT-WAF.pid", f"/etc/init.d/btwaf",
|
||||
"BT-WAF", waf_pid, f"/etc/init.d/btwaf",
|
||||
partial(pluign_ver, "btwaf"),
|
||||
),
|
||||
"fail2ban": (
|
||||
"fail2ban-server", f"{PLUGINS_PATH}/fail2ban/fail2ban.pid", "/etc/init.d/fail2ban",
|
||||
partial(pluign_ver, "fail2ban"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# 日志
|
||||
def write_logs(msg: str):
|
||||
try:
|
||||
from public import M
|
||||
t = time.strftime('%Y-%m-%d %X', time.localtime())
|
||||
M("logs").add("uid,username,type,log,addtime", (1, "system", "Service Daemon", msg, t))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# 手动干预
|
||||
def manual_flag(server_name: str = None, open_: str = None) -> Optional[dict]:
|
||||
if not server_name: # only read
|
||||
return DaemonManager.manual_safe_read()
|
||||
@@ -71,6 +325,198 @@ def manual_flag(server_name: str = None, open_: str = None) -> Optional[dict]:
|
||||
return DaemonManager.manual_safe_read()
|
||||
|
||||
|
||||
# 管理服务助手
|
||||
class ServicesHelper:
|
||||
def __init__(self, nick_name: str = None):
|
||||
self.nick_name = nick_name
|
||||
self._serviced = None
|
||||
self._pid_source = None
|
||||
self._bash = None
|
||||
self._ver_source = None
|
||||
self._pid_cache = None
|
||||
self._ver_cache = None
|
||||
self._install_cache = None
|
||||
self._info_inited = False
|
||||
|
||||
def __check_pid_process(self, pid: int) -> bool:
|
||||
try:
|
||||
# 是否活跃
|
||||
with open(f"/proc/{pid}/stat", "r") as f:
|
||||
if f.read().split()[2] == "Z":
|
||||
return False # 僵尸进程
|
||||
# 进程是否名字匹配
|
||||
with open(f"/proc/{pid}/comm", "r") as f:
|
||||
proc_name = f.read().strip()
|
||||
return proc_name == self.nick_name or proc_name == self._serviced
|
||||
except (FileNotFoundError, IndexError):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _init_info(self) -> None:
|
||||
if self._info_inited:
|
||||
return
|
||||
if not self.nick_name or not isinstance(self.nick_name, str):
|
||||
self._info_inited = True
|
||||
return
|
||||
|
||||
map_info = SERVICES_MAP.get(self.nick_name)
|
||||
if map_info:
|
||||
self._serviced, self._pid_source, self._bash, self._ver_source = map_info
|
||||
|
||||
self._info_inited = True
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
"""返回pid文件路径或pid值"""
|
||||
if self._pid_cache is None:
|
||||
self._init_info()
|
||||
if isinstance(self._pid_source, Callable):
|
||||
try:
|
||||
pid_val = self._pid_source()
|
||||
self._pid_cache = int(pid_val) if pid_val and pid_val.isdigit() else pid_val
|
||||
except Exception:
|
||||
self._pid_cache = self._pid_source
|
||||
else:
|
||||
self._pid_cache = self._pid_source
|
||||
return self._pid_cache
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""返回服务版本号"""
|
||||
if self._ver_cache is None:
|
||||
self._init_info()
|
||||
if isinstance(self._ver_source, Callable):
|
||||
try:
|
||||
self._ver_cache = self._ver_source()
|
||||
except:
|
||||
self._ver_cache = ""
|
||||
elif isinstance(self._ver_source, str) and os.path.exists(self._ver_source):
|
||||
try:
|
||||
with open(self._ver_source, "r") as f:
|
||||
self._ver_cache = f.read().strip()
|
||||
except:
|
||||
self._ver_cache = ""
|
||||
else:
|
||||
self._ver_cache = ""
|
||||
|
||||
return str(self._ver_cache).strip()
|
||||
|
||||
@property
|
||||
def is_install(self) -> bool:
|
||||
"""判断是否安装"""
|
||||
if self._install_cache is not None:
|
||||
return self._install_cache
|
||||
|
||||
self._init_info()
|
||||
self._install_cache = False
|
||||
# waf特殊处理
|
||||
if self.nick_name == "btwaf":
|
||||
if os.path.exists(f"{PLUGINS_PATH}/btwaf"):
|
||||
self._install_cache = True
|
||||
return self._install_cache
|
||||
# postfix特殊处理
|
||||
if self.nick_name == "postfix":
|
||||
if os.path.exists(f"{PLUGINS_PATH}/mail_sys"):
|
||||
self._install_cache = True
|
||||
return self._install_cache
|
||||
|
||||
if any([
|
||||
self._serviced and os.path.exists(f"/etc/init.d/{self._serviced}"),
|
||||
self.nick_name and os.path.exists(f"/etc/init.d/{self.nick_name}"),
|
||||
self._bash and os.path.exists(self._bash),
|
||||
]):
|
||||
self._install_cache = True
|
||||
|
||||
return self._install_cache
|
||||
|
||||
@property
|
||||
def shop_name(self) -> str:
|
||||
return service_shop_name(self.nick_name)
|
||||
|
||||
@property
|
||||
def pretty_title(self) -> str:
|
||||
return pretty_title(self.nick_name)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""
|
||||
判断进程是否存活
|
||||
pid: 进程pid文件 str 或 int
|
||||
serviced: 进程服务名称
|
||||
nick_name: 进程别名
|
||||
"""
|
||||
if not self.is_install:
|
||||
return False
|
||||
|
||||
if not self.pid:
|
||||
return False
|
||||
|
||||
if isinstance(self.pid, int):
|
||||
return self.__check_pid_process(self.pid)
|
||||
|
||||
if isinstance(self.pid, str):
|
||||
if not os.path.exists(self.pid):
|
||||
return False
|
||||
if self.pid.endswith(".pid"):
|
||||
try:
|
||||
with open(self.pid, "r") as f:
|
||||
temp_pid = int(f.read().strip())
|
||||
return self.__check_pid_process(temp_pid)
|
||||
except (ValueError, FileNotFoundError):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
elif self.pid.endswith(".sock"):
|
||||
try:
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
|
||||
s.settimeout(0.1)
|
||||
s.connect(self.pid)
|
||||
return True
|
||||
except (socket.timeout, ConnectionRefusedError, FileNotFoundError):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
# not str and not int
|
||||
return False
|
||||
|
||||
def script(self, act: str) -> None:
|
||||
if not self.is_install or act not in ["start", "stop", "restart", "status"]:
|
||||
return
|
||||
try:
|
||||
# "try to {act} [{self.nick_name}]..."
|
||||
self._init_info()
|
||||
bash_path = self._bash if self._bash else f"/etc/init.d/{self._serviced}"
|
||||
|
||||
if isinstance(self.pid, str) and (
|
||||
self.pid.endswith(".sock") or self.pid.endswith(".pid")
|
||||
):
|
||||
try:
|
||||
os.remove(self.pid)
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.nick_name == "panel":
|
||||
if not os.path.exists(f"{SETUP_PATH}/panel/init.sh"):
|
||||
from public import get_url
|
||||
os.system(f"curl -k {get_url()}/install/update_7.x_en.sh|bash &")
|
||||
cmd = ["bash", bash_path, act]
|
||||
|
||||
elif self.nick_name == "pdns":
|
||||
cmd = ["systemctl", act, "pdns"]
|
||||
else:
|
||||
cmd = [bash_path, act]
|
||||
|
||||
result = run_command(cmd)
|
||||
if not result and act in ["start", "restart"]:
|
||||
write_logs(f"Failed to {act} {self.nick_name}, error: command returned no output.")
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
write_logs(f"Failed to {act} {self.nick_name}, error: {e}")
|
||||
|
||||
|
||||
# 守护服务管理
|
||||
class DaemonManager:
|
||||
@classmethod
|
||||
def __ensure(cls):
|
||||
@@ -231,16 +677,12 @@ class DaemonManager:
|
||||
return record
|
||||
|
||||
|
||||
# 服务守护
|
||||
class RestartServices:
|
||||
COUNT = 30
|
||||
|
||||
def __init__(self):
|
||||
self.nick_name = None
|
||||
self.serviced = None
|
||||
self.pid_file = None
|
||||
self.bash = None
|
||||
|
||||
def __keep_flag_right(self, manual_info: dict) -> None:
|
||||
@staticmethod
|
||||
def __keep_flag_right(manual_info: dict) -> None:
|
||||
try:
|
||||
with open(MANUAL_FLAG, "r+") as f:
|
||||
try:
|
||||
@@ -256,115 +698,11 @@ class RestartServices:
|
||||
except Exception as e:
|
||||
print("Error keep_flag_right:", e)
|
||||
|
||||
def _overhead(self) -> bool:
|
||||
def _overhead(self, nick_name) -> bool:
|
||||
return DaemonManager.update_restart_record(
|
||||
self.nick_name, self.COUNT
|
||||
nick_name, self.COUNT
|
||||
)
|
||||
|
||||
def _script(self, act: str) -> None:
|
||||
try:
|
||||
if act not in ["start", "stop", "restart", "status"]:
|
||||
return
|
||||
# "try to {act} [{self.nick_name}]..."
|
||||
bash_path = self.bash if self.bash else f"/etc/init.d/{self.serviced}"
|
||||
if self.pid_file and self.pid_file.endswith(".sock"):
|
||||
try:
|
||||
os.remove(self.pid_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.nick_name == "panel":
|
||||
if not os.path.exists(f"{SETUP_PATH}/panel/init.sh"):
|
||||
os.system(f"curl -k {get_url()}/install/update_7.x_en.sh|bash &")
|
||||
cmd = ["bash", bash_path, act]
|
||||
else:
|
||||
cmd = [bash_path, act]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
WriteLog(
|
||||
"Service Daemon", f"Failed to {act} {self.nick_name}, error: {result.stderr.strip()}"
|
||||
)
|
||||
except subprocess.TimeoutExpired as t:
|
||||
WriteLog(
|
||||
"Service Daemon", f"Failed to {act} {self.nick_name}, error: time out, {t}"
|
||||
)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
WriteLog(
|
||||
"Service Daemon", f"Failed to {act} {self.nick_name}, error: {e}"
|
||||
)
|
||||
|
||||
def is_support(self) -> bool:
|
||||
try:
|
||||
map_info = SERVICES_MAP.get(self.nick_name)
|
||||
if not map_info:
|
||||
return False
|
||||
|
||||
self.serviced, self.pid_file, self.bash = map_info
|
||||
|
||||
if not all([self.serviced, self.pid_file, self.bash]):
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_install_service(self) -> bool:
|
||||
if self.serviced == "mysqld": # mysql系列卸载根本不干净, 判断bin
|
||||
if any([
|
||||
os.path.exists(f"{SETUP_PATH}/mysql/bin/mariadbd"),
|
||||
os.path.exists(f"{SETUP_PATH}/mysql/bin/mysqld"),
|
||||
]):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
# other
|
||||
if any([
|
||||
os.path.exists(f"/etc/init.d/{self.serviced}"),
|
||||
os.path.exists(f"/etc/init.d/{self.nick_name}"),
|
||||
os.path.exists(self.bash),
|
||||
]):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_process_running(self) -> bool:
|
||||
if not os.path.exists(self.pid_file):
|
||||
return False
|
||||
# sock file
|
||||
if self.pid_file.endswith(".sock"):
|
||||
try:
|
||||
check_list = ["mysqld", "mariadbd"] if self.serviced == "mysqld" else [self.serviced]
|
||||
check_list = '|'.join(check_list)
|
||||
# read pid -> show process name -> grep
|
||||
command = f"lsof -t {self.pid_file} 2>/dev/null | xargs -r ps -o comm= -p | grep -Ewq '{check_list}'"
|
||||
return subprocess.run(command, shell=True).returncode == 0
|
||||
except:
|
||||
return False
|
||||
else: # pid file
|
||||
try:
|
||||
with open(self.pid_file, "r") as f:
|
||||
pid = int(f.read().strip())
|
||||
# 是否活跃
|
||||
with open(f"/proc/{pid}/stat", "r") as f:
|
||||
if f.read().split()[2] == "Z":
|
||||
return False # 僵尸进程
|
||||
# 进程是否名字匹配
|
||||
with open(f"/proc/{pid}/comm", "r") as f:
|
||||
proc_name = f.read().strip()
|
||||
|
||||
if proc_name == self.nick_name or proc_name == self.serviced:
|
||||
return True
|
||||
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
@DaemonManager.read_lock
|
||||
def main(self):
|
||||
manaul = readFile(MANUAL_FLAG)
|
||||
@@ -381,26 +719,27 @@ class RestartServices:
|
||||
for service in [
|
||||
x for x in list(set(check_list)) if record.get(x, 0) < self.COUNT
|
||||
]:
|
||||
self.nick_name = service
|
||||
if not self.is_support() or not self.is_install_service():
|
||||
obj = ServicesHelper(service)
|
||||
if not obj.is_install:
|
||||
continue
|
||||
if not self.is_process_running():
|
||||
if int(manual_info.get(self.nick_name, 0)) == 1:
|
||||
|
||||
if not obj.is_running:
|
||||
if int(manual_info.get(obj.nick_name, 0)) == 1:
|
||||
# service closed maually, skip
|
||||
continue
|
||||
WriteLog(
|
||||
"Service Daemon", f"Service [ {self.nick_name} ] is Not Running, Try to start it..."
|
||||
)
|
||||
if not self._overhead():
|
||||
self._script("start")
|
||||
time.sleep(3)
|
||||
if not self.is_process_running():
|
||||
if not self._overhead():
|
||||
self._script("restart")
|
||||
if obj.nick_name != "panel":
|
||||
write_logs(f"Service [ {obj.nick_name} ] is Not Running, Try to start it...")
|
||||
|
||||
if manual_info.get(self.nick_name) == 1:
|
||||
if not self._overhead(obj.nick_name):
|
||||
obj.script("start")
|
||||
time.sleep(3)
|
||||
if not obj.is_running:
|
||||
if not self._overhead(obj.nick_name):
|
||||
obj.script("restart")
|
||||
|
||||
if manual_info.get(obj.nick_name) == 1:
|
||||
# service is running, fix the wrong flag
|
||||
manual_info[self.nick_name] = 0
|
||||
manual_info[obj.nick_name] = 0
|
||||
# under lock file read lock
|
||||
self.__keep_flag_right(manual_info)
|
||||
|
||||
@@ -409,10 +748,18 @@ def first_time_installed(data: dict) -> None:
|
||||
"""
|
||||
首次安装服务启动守护进程服务
|
||||
"""
|
||||
# todo 虽然支持, 但是守护目前不干预, 前端没开放
|
||||
exculde = [
|
||||
"pgsql", "fail2ban", "btwaf", "ssh", "pdns", "php-fpm", "memcached"
|
||||
]
|
||||
if not data:
|
||||
return
|
||||
try:
|
||||
for service in SERVICES_MAP.keys(): # support service
|
||||
if service in exculde:
|
||||
continue
|
||||
if "php-fpm" in service:
|
||||
continue
|
||||
pl_name = f"{DATA_PATH}/first_installed_flag_{service}.pl"
|
||||
if data.get(service): # panel installed
|
||||
setup = data[service].get("setup", False)
|
||||
@@ -420,7 +767,8 @@ def first_time_installed(data: dict) -> None:
|
||||
os.remove(pl_name)
|
||||
elif setup is True and not os.path.exists(pl_name):
|
||||
DaemonManager.add_daemon(service)
|
||||
writeFile(pl_name, "1", mode="w")
|
||||
with open(pl_name, "w") as f:
|
||||
f.write("1")
|
||||
else:
|
||||
pass
|
||||
except:
|
||||
@@ -429,14 +777,3 @@ def first_time_installed(data: dict) -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
# import tracemalloc
|
||||
|
||||
# tracemalloc.start()
|
||||
# snapshot1 = tracemalloc.take_snapshot()
|
||||
# RestartServices().main()
|
||||
# snapshot2 = tracemalloc.take_snapshot()
|
||||
|
||||
# top_stats = snapshot2.compare_to(snapshot1, 'lineno')
|
||||
# print("[Top memory differences]")
|
||||
# for stat in top_stats[:3]:
|
||||
# print("stat", stat)
|
||||
|
||||
Reference in New Issue
Block a user