初始化1

This commit is contained in:
zg
2026-07-16 13:03:11 +08:00
parent 0bdfcbc1c8
commit e6b0f287cc
100 changed files with 12184 additions and 0 deletions

View File

@@ -0,0 +1,567 @@
"""
Nginx 配置管理服务
负责读写 Nginx 配置文件、重载 Nginx
全局优化(在 /etc/nginx/nginx.conf http 块中):
- gzip + gzip_types14种MIME— 压缩传输
- SSL session cache (10m) + timeout (60m) — 减少重复TLS握手
- ssl_session_tickets off + ssl_buffer_size 4k
每个域名配置的优化:
- listen 443 ssl http2 — HTTP/2 多路复用
- proxy_buffering off — 边收边发,不阻塞
- 静态资源 30天强缓存public, immutable
"""
import os
import re
import subprocess
import logging
logger = logging.getLogger(__name__)
# Nginx HTTP 配置模板
HTTP_TEMPLATE = """# Nginx转发规则 - {domain}
# 协议: HTTP
# 自动生成,请勿手动修改
server {{
listen 80;
server_name {domain};
client_max_body_size 2048m;
# 静态资源缓存JS/CSS 带 hash30天
location ~* \\.(js|css|svg|woff2|png|jpg|ico)$ {{
expires 30d;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;
add_header Access-Control-Allow-Headers \"Content-Type, Authorization\" always;
add_header Cache-Control \"public, immutable\";
proxy_pass {target};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection \"\";
proxy_set_header Accept-Encoding \"gzip\";
proxy_connect_timeout 43200s;
proxy_send_timeout 43200s;
proxy_read_timeout 43200s;
proxy_buffering off;
}}
location / {{
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;
add_header Access-Control-Allow-Headers \"Content-Type, Authorization\" always;
proxy_pass {target};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection \"\";
proxy_set_header Accept-Encoding \"gzip\";
proxy_connect_timeout 43200s;
proxy_send_timeout 43200s;
proxy_read_timeout 43200s;
proxy_buffering off;
}}
}}
"""
# Nginx HTTPS 配置模板
HTTPS_TEMPLATE = """# Nginx转发规则 - {domain}
# 协议: HTTPS
# 自动生成,请勿手动修改
# HTTP → HTTPS 重定向
server {{
listen 80;
server_name {domain};
return 301 https://$host$request_uri;
}}
# HTTPS 服务
server {{
listen 443 ssl http2;
server_name {domain};
# CORS 跨域响应头server 级别,确保 OPTIONS 204 也带 CORS 头)
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;
add_header Access-Control-Allow-Headers \"Content-Type, Authorization\" always;
# CORS 预检请求快速返回(避免穿透到后端)
if ($request_method = OPTIONS) {{ return 204; }}
ssl_certificate {ssl_cert};
ssl_certificate_key {ssl_key};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 静态资源缓存JS/CSS 带 hash30天
location ~* \\.(js|css|svg|woff2|png|jpg|ico)$ {{
expires 30d;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;
add_header Access-Control-Allow-Headers \"Content-Type, Authorization\" always;
add_header Cache-Control \"public, immutable\";
proxy_pass {target};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection \"\";
proxy_set_header Accept-Encoding \"gzip\";
proxy_connect_timeout 43200s;
proxy_send_timeout 43200s;
proxy_read_timeout 43200s;
proxy_buffering off;
}}
location / {{
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;
add_header Access-Control-Allow-Headers \"Content-Type, Authorization\" always;
proxy_pass {target};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection \"\";
proxy_set_header Accept-Encoding \"gzip\";
proxy_connect_timeout 43200s;
proxy_send_timeout 43200s;
proxy_read_timeout 43200s;
proxy_buffering off;
}}
}}
"""
class NginxService:
"""Nginx 配置管理服务"""
def __init__(self, conf_dir='/etc/nginx/conf.d', ssl_dir='/etc/nginx/ssl'):
self.conf_dir = conf_dir
self.ssl_dir = ssl_dir
# 确保目录存在
os.makedirs(self.conf_dir, exist_ok=True)
def create_config(self, domain, protocol, target, ssl_cert=None, ssl_key=None):
"""
创建域名转发配置文件
Args:
domain: 域名
protocol: 协议 (http/https)
target: 转发目标地址
ssl_cert: SSL证书路径仅在HTTPS时需要
ssl_key: SSL私钥路径仅在HTTPS时需要
Returns:
config_file: 生成的配置文件路径
"""
config_path = os.path.join(self.conf_dir, f'{domain}.conf')
if protocol == 'https':
content = HTTPS_TEMPLATE.format(
domain=domain,
target=target,
ssl_cert=ssl_cert or f'{self.ssl_dir}/{domain}.pem',
ssl_key=ssl_key or f'{self.ssl_dir}/{domain}.key'
)
else:
content = HTTP_TEMPLATE.format(
domain=domain,
target=target
)
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"已创建配置文件: {config_path}")
return config_path
def delete_config(self, domain):
"""
删除域名配置文件
Args:
domain: 域名
Returns:
bool: 是否成功删除
"""
config_path = os.path.join(self.conf_dir, f'{domain}.conf')
if os.path.exists(config_path):
os.remove(config_path)
logger.info(f"已删除配置文件: {config_path}")
return True
logger.warning(f"配置文件不存在: {config_path}")
return False
def list_rules(self):
"""
解析所有 Nginx 配置文件,提取转发规则列表
Returns:
list[dict]: 规则列表
"""
rules = []
if not os.path.exists(self.conf_dir):
return rules
for filename in sorted(os.listdir(self.conf_dir)):
if not filename.endswith('.conf'):
continue
filepath = os.path.join(self.conf_dir, filename)
rule = self._parse_config(filepath)
if rule:
rules.append(rule)
return rules
def find_rule(self, domain):
"""
查找指定域名的规则
Args:
domain: 域名
Returns:
dict | None: 规则信息
"""
config_path = os.path.join(self.conf_dir, f'{domain}.conf')
if not os.path.exists(config_path):
return None
return self._parse_config(config_path)
def _parse_config(self, filepath):
"""
解析单个 Nginx 配置文件
Args:
filepath: 配置文件路径
Returns:
dict | None: 解析出的规则信息
"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
rule = {
'config_file': filepath
}
# 提取域名 (server_name)
server_name_match = re.search(r'server_name\s+([^;]+);', content)
if server_name_match:
rule['domain'] = server_name_match.group(1).strip()
# 提取转发目标 (proxy_pass)
proxy_pass_match = re.search(r'proxy_pass\s+([^;]+);', content)
if proxy_pass_match:
rule['target'] = proxy_pass_match.group(1).strip()
# 判断协议类型
if 'listen 443 ssl' in content:
rule['protocol'] = 'https'
# 提取 SSL 证书路径
cert_match = re.search(r'ssl_certificate\s+([^;]+);', content)
if cert_match:
rule['ssl_cert'] = cert_match.group(1).strip()
key_match = re.search(r'ssl_certificate_key\s+([^;]+);', content)
if key_match:
rule['ssl_key'] = key_match.group(1).strip()
else:
rule['protocol'] = 'http'
return rule
except Exception as e:
logger.error(f"解析配置文件失败 {filepath}: {e}")
return None
def check_config(self):
"""
检查 Nginx 配置语法
Returns:
tuple: (是否通过, 错误信息)
"""
try:
result = subprocess.run(
['nginx', '-t'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True,
timeout=10
)
if result.returncode == 0:
return True, 'ok'
else:
error_msg = result.stderr.strip() or '配置语法错误'
return False, error_msg
except FileNotFoundError:
return False, 'nginx 命令未找到,请确认 Nginx 已安装'
except subprocess.TimeoutExpired:
return False, 'nginx -t 执行超时'
except Exception as e:
return False, str(e)
def reload(self):
"""
重载 Nginx 配置
Returns:
tuple: (是否成功, 消息)
"""
# 先检查配置语法
valid, msg = self.check_config()
if not valid:
return False, f'配置语法检查失败: {msg}'
# 执行 reload
try:
# 尝试 systemctl reload
result = subprocess.run(
['systemctl', 'reload', 'nginx'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True,
timeout=30
)
if result.returncode == 0:
logger.info("Nginx 重载成功 (systemctl)")
return True, 'Nginx 重载成功'
# 回退到 nginx -s reload
result = subprocess.run(
['nginx', '-s', 'reload'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True,
timeout=30
)
if result.returncode == 0:
logger.info("Nginx 重载成功 (nginx -s reload)")
return True, 'Nginx 重载成功'
error_msg = result.stderr.strip() or '未知错误'
return False, f'重载失败: {error_msg}'
except FileNotFoundError:
return False, 'nginx 命令未找到'
except subprocess.TimeoutExpired:
return False, 'reload 执行超时'
except Exception as e:
return False, str(e)
def parse_gzip_config(self, domain):
"""
从配置文件中解析 gzip 压缩配置
Args:
domain: 域名
Returns:
dict: gzip 配置信息
"""
config_path = os.path.join(self.conf_dir, f'{domain}.conf')
if not os.path.exists(config_path):
return {
'enabled': False,
'types': [],
'comp_level': 6,
'min_length': '1000'
}
with open(config_path, 'r', encoding='utf-8') as f:
content = f.read()
result = {
'enabled': False,
'types': [],
'comp_level': 6,
'min_length': '1000'
}
# 解析 gzip on/off
gzip_match = re.search(r'^\s*gzip\s+(on|off)\s*;', content, re.MULTILINE)
if gzip_match:
result['enabled'] = gzip_match.group(1) == 'on'
# 解析 gzip_types
types_match = re.search(r'^\s*gzip_types\s+([^;]+);', content, re.MULTILINE)
if types_match:
types_str = types_match.group(1).strip()
result['types'] = [t.strip() for t in types_str.split() if t.strip()]
# 解析 gzip_comp_level
level_match = re.search(r'^\s*gzip_comp_level\s+(\d+)\s*;', content, re.MULTILINE)
if level_match:
result['comp_level'] = int(level_match.group(1))
# 解析 gzip_min_length
length_match = re.search(r'^\s*gzip_min_length\s+(\S+)\s*;', content, re.MULTILINE)
if length_match:
result['min_length'] = length_match.group(1).strip()
return result
def set_gzip_config(self, domain, gzip_config):
"""
修改配置文件中的 gzip 压缩配置(在 server 块内添加/更新 gzip 指令)
Args:
domain: 域名
gzip_config: dict with keys:
- enabled (bool): 是否开启
- types (list[str]): MIME 类型列表
- comp_level (int): 压缩级别 1-9
- min_length (str): 最小压缩长度
Returns:
tuple: (成功, 消息)
"""
config_path = os.path.join(self.conf_dir, f'{domain}.conf')
if not os.path.exists(config_path):
return False, f'域名 {domain} 的配置文件不存在'
# 读取原内容
with open(config_path, 'r', encoding='utf-8') as f:
original_content = f.read()
content = original_content
# 1. 删除 server 块内所有已有的 gzip 指令(包括注释行)
content = re.sub(r'^\s*#\s*内容压缩[^\n]*\n?', '', content, flags=re.MULTILINE)
content = re.sub(r'^\s*gzip\s+(on|off)\s*;\s*\n?', '', content, flags=re.MULTILINE)
content = re.sub(r'^\s*gzip_types\s+[^;]+;\s*\n?', '', content, flags=re.MULTILINE)
content = re.sub(r'^\s*gzip_comp_level\s+\d+\s*;\s*\n?', '', content, flags=re.MULTILINE)
content = re.sub(r'^\s*gzip_min_length\s+\S+\s*;\s*\n?', '', content, flags=re.MULTILINE)
# 2. 构建新的 gzip 指令块
gzip_lines = []
if gzip_config.get('enabled', False):
gzip_lines.append(' # 内容压缩gzip')
gzip_lines.append(' gzip on;')
types = gzip_config.get('types', [])
if types:
types_str = ' '.join(types)
gzip_lines.append(f' gzip_types {types_str};')
comp_level = gzip_config.get('comp_level', 6)
gzip_lines.append(f' gzip_comp_level {comp_level};')
min_length = gzip_config.get('min_length', '1000')
gzip_lines.append(f' gzip_min_length {min_length};')
else:
gzip_lines.append(' # 内容压缩gzip')
gzip_lines.append(' gzip off;')
gzip_block = '\n'.join(gzip_lines) + '\n'
# 3. 在 server_name 行之后插入 gzip 指令块
# 匹配 server_name xxx; 并在其后插入
def insert_gzip(match):
return match.group(0) + '\n' + gzip_block
content = re.sub(
r'^\s*server_name\s+[^;]+;\s*$',
insert_gzip,
content,
flags=re.MULTILINE
)
# 4. 写入文件
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"已更新 {domain} 的 gzip 压缩配置: enabled={gzip_config.get('enabled')}")
# 5. 语法检查
valid, msg = self.check_config()
if not valid:
# 回滚
with open(config_path, 'w', encoding='utf-8') as f:
f.write(original_content)
logger.error(f"gzip 配置语法错误,已回滚: {msg}")
return False, f'配置语法检查失败,已自动回滚: {msg}'
# 6. 重载 Nginx
reload_success, reload_msg = self.reload()
if not reload_success:
logger.warning(f"Nginx 重载失败: {reload_msg}")
return False, f'gzip 配置已写入但重载失败: {reload_msg}'
return True, 'gzip 压缩配置已保存并生效'
def get_domain_logs(self, domain, lines=200):
"""
获取指定域名的 Nginx 访问日志和错误日志
Args:
domain: 域名
lines: 返回最近多少行日志默认200
Returns:
dict: access_log 和 error_log 内容
"""
result = {
'domain': domain,
'access_log': '',
'error_log': '',
'access_log_path': '/var/log/nginx/access.log',
'error_log_path': '/var/log/nginx/error.log'
}
# 访问日志 — 过滤包含域名的行
access_log_path = '/var/log/nginx/access.log'
if os.path.exists(access_log_path):
try:
# 使用 tail + grep 获取最近包含域名的日志行
import subprocess as _sp
proc = _sp.run(
['tail', '-n', str(lines * 5), access_log_path],
stdout=_sp.PIPE, stderr=_sp.PIPE,
universal_newlines=True, timeout=10
)
if proc.returncode == 0:
all_lines = proc.stdout.splitlines()
matched = [l for l in all_lines if domain in l]
result['access_log'] = '\n'.join(matched[-lines:])
except Exception as e:
logger.warning(f"读取访问日志失败: {e}")
result['access_log'] = f'[读取失败] {e}'
# 错误日志 — 过滤包含域名的行
error_log_path = '/var/log/nginx/error.log'
if os.path.exists(error_log_path):
try:
import subprocess as _sp
proc = _sp.run(
['tail', '-n', str(lines * 5), error_log_path],
stdout=_sp.PIPE, stderr=_sp.PIPE,
universal_newlines=True, timeout=10
)
if proc.returncode == 0:
all_lines = proc.stdout.splitlines()
matched = [l for l in all_lines if domain in l]
result['error_log'] = '\n'.join(matched[-lines:])
except Exception as e:
logger.warning(f"读取错误日志失败: {e}")
result['error_log'] = f'[读取失败] {e}'
return result
def _escape_nginx(content):
"""转义 Nginx 配置中的特殊字符(模板用)"""
return content