Files
nginxserver/nginx-python-api/app.py
2026-07-16 13:03:11 +08:00

620 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Nginx域名转发管理 - Python API 主入口
部署在 Nginx 服务器 (10.1.1.160:5000) 上
直接操作本机 Nginx 配置和证书
"""
import os
import logging
from flask import Flask, request, g
from services.nginx_service import NginxService
from services.ssl_service import SSLService
from services.dns_service import DnsService
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False # 中文不转 Unicode
# ════════════════════════════════════════════
# 网关用户信息中间件
# ════════════════════════════════════════════
USER_HEADERS = [
'X-User-Id', 'X-User-Name', 'X-User-Account',
'X-User-Email', 'X-User-Post', 'X-User-Roles',
'X-System-Id', 'X-System-Code', 'X-System-Name',
]
@app.before_request
def user_context_middleware():
"""从网关注入的请求头提取用户信息,存到 Flask g 对象"""
for h in USER_HEADERS:
val = request.headers.get(h)
if val:
setattr(g, h.replace('-', '_').lower(), val)
# 初始化服务
nginx_conf_dir = os.environ.get('NGINX_CONF_DIR', '/etc/nginx/conf.d')
nginx_ssl_dir = os.environ.get('NGINX_SSL_DIR', '/etc/nginx/ssl')
ca_dir = os.environ.get('CA_DIR', '/etc/nginx/ssl/ca')
nginx_service = NginxService(nginx_conf_dir, nginx_ssl_dir)
ssl_service = SSLService(ca_dir, nginx_ssl_dir)
dns_service = DnsService()
# 启动时初始化内部 CAgunicorn 模式下也必须执行)
ssl_service.init_ca()
# ──────────────────────────────────────────────
# API 路由
# ──────────────────────────────────────────────
@app.route('/api/nginx/health', methods=['GET'])
def health_check():
"""健康检查"""
return {
'code': 0,
'message': 'Nginx Manager API is running',
'data': {
'nginx_installed': os.path.exists('/usr/sbin/nginx') or os.path.exists('/usr/bin/nginx'),
'conf_dir': nginx_conf_dir,
'ssl_dir': nginx_ssl_dir
}
}
@app.route('/api/nginx/rules', methods=['GET'])
def list_rules():
"""获取所有转发规则列表"""
try:
rules = nginx_service.list_rules()
return {'code': 0, 'message': 'success', 'data': rules}
except Exception as e:
logger.error(f"获取规则列表失败: {e}")
return {'code': -1, 'message': str(e), 'data': []}
@app.route('/api/nginx/rules', methods=['POST'])
def add_rule():
"""新增转发规则"""
try:
body = request.get_json(force=True)
domain = body.get('domain', '').strip()
protocol = body.get('protocol', '').strip().lower()
target = body.get('target', '').strip()
# 参数校验
if not domain:
return {'code': -1, 'message': '域名不能为空'}, 400
if protocol not in ('http', 'https'):
return {'code': -1, 'message': '协议类型必须为 http 或 https'}, 400
if not target.startswith('http://') and not target.startswith('https://'):
return {'code': -1, 'message': '转发目标必须以 http:// 或 https:// 开头'}, 400
# 系统保护域名不允许手动创建zgitm.com / www.zgitm.com
if ssl_service.is_system_domain(domain):
return {'code': -1, 'message': f'{domain} 是系统保护域名,不能手动创建'}, 400
logger.info(f"新增规则: domain={domain}, protocol={protocol}, target={target}")
# 检查是否已存在
existing = nginx_service.find_rule(domain)
if existing:
return {'code': -1, 'message': f'域名 {domain} 的规则已存在'}, 409
# HTTPS证书签发
ssl_cert = None
ssl_key = None
if protocol == 'https':
if ssl_service.is_zgitm_domain(domain):
# zgitm.com 子域名 → Let's Encrypt DNS-01
ssl_cert, ssl_key = ssl_service.ensure_le_certificate(domain)
if not ssl_cert or not ssl_key:
return {'code': -1, 'message': f'{domain} Let\'s Encrypt 证书签发失败,请查看服务器日志'}, 500
else:
# 其他域名 → 内部 CA
ssl_cert, ssl_key = ssl_service.ensure_certificate(domain)
# 创建 Nginx 配置文件
config_file = nginx_service.create_config(domain, protocol, target, ssl_cert, ssl_key)
# 检查 Nginx 配置语法
valid, error_msg = nginx_service.check_config()
if not valid:
# 回滚:删除刚创建的配置文件
nginx_service.delete_config(domain)
logger.error(f"Nginx 配置语法错误: {error_msg}")
return {'code': -1, 'message': f'Nginx 配置语法检查失败: {error_msg}'}, 400
return {
'code': 0,
'message': '规则创建成功',
'data': {
'domain': domain,
'protocol': protocol,
'target': target,
'config_file': config_file,
'ssl_cert': ssl_cert,
'ssl_key': ssl_key
}
}
except Exception as e:
logger.error(f"新增规则失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/rules/<domain>', methods=['GET'])
def get_rule(domain):
"""获取单个规则详情"""
try:
rule = nginx_service.find_rule(domain)
if rule:
return {'code': 0, 'message': 'success', 'data': rule}
return {'code': -1, 'message': f'域名 {domain} 的规则不存在'}, 404
except Exception as e:
logger.error(f"获取规则详情失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/rules/<domain>', methods=['PUT'])
def update_rule(domain):
"""修改转发规则"""
try:
body = request.get_json(force=True)
new_domain = body.get('domain', '').strip()
new_protocol = body.get('protocol', '').strip().lower()
new_target = body.get('target', '').strip()
if not new_domain:
return {'code': -1, 'message': '域名不能为空'}, 400
if new_protocol not in ('http', 'https'):
return {'code': -1, 'message': '协议类型必须为 http 或 https'}, 400
if not new_target.startswith('http://') and not new_target.startswith('https://'):
return {'code': -1, 'message': '转发目标必须以 http:// 或 https:// 开头'}, 400
logger.info(f"修改规则: domain={domain} -> new_domain={new_domain}")
# 检查原规则是否存在
old_rule = nginx_service.find_rule(domain)
if not old_rule:
return {'code': -1, 'message': f'域名 {domain} 的规则不存在'}, 404
# 删除旧配置
nginx_service.delete_config(domain)
# HTTPS证书签发
ssl_cert = None
ssl_key = None
if new_protocol == 'https':
if ssl_service.is_zgitm_domain(new_domain):
ssl_cert, ssl_key = ssl_service.ensure_le_certificate(new_domain)
if not ssl_cert or not ssl_key:
return {'code': -1, 'message': f'{new_domain} Let\'s Encrypt 证书签发失败'}, 500
else:
ssl_cert, ssl_key = ssl_service.ensure_certificate(new_domain)
# 创建新配置
config_file = nginx_service.create_config(new_domain, new_protocol, new_target, ssl_cert, ssl_key)
# 语法检查
valid, error_msg = nginx_service.check_config()
if not valid:
nginx_service.delete_config(new_domain)
# 恢复旧配置
old_proto = old_rule.get('protocol', 'http')
old_cert = old_rule.get('ssl_cert')
old_key = old_rule.get('ssl_key')
nginx_service.create_config(domain, old_proto, old_rule.get('target', ''), old_cert, old_key)
logger.error(f"Nginx 配置语法错误: {error_msg}")
return {'code': -1, 'message': f'Nginx 配置语法检查失败: {error_msg}'}, 400
return {
'code': 0,
'message': '规则修改成功',
'data': {
'domain': new_domain,
'protocol': new_protocol,
'target': new_target,
'config_file': config_file,
'ssl_cert': ssl_cert,
'ssl_key': ssl_key
}
}
except Exception as e:
logger.error(f"修改规则失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/rules/<domain>', methods=['DELETE'])
def delete_rule(domain):
"""删除转发规则"""
try:
logger.info(f"删除规则: domain={domain}")
rule = nginx_service.find_rule(domain)
# 删除配置文件(如果存在)
deleted = nginx_service.delete_config(domain)
# 文件不存在但数据库有记录 → 允许继续(清理脏数据)
if not rule and not deleted:
logger.info(f"域名 {domain} 的规则文件不存在,跳过删除")
# 检查语法(删除后)
valid, error_msg = nginx_service.check_config()
if not valid:
logger.warning(f"删除后 Nginx 配置语法错误: {error_msg}")
return {'code': 0, 'message': '规则已删除', 'data': {'domain': domain}}
except Exception as e:
logger.error(f"删除规则失败: {e}")
return {'code': -1, 'message': str(e)}, 500
# ──────────────────────────────────────────────
# 配置文件查看/编辑接口
# ──────────────────────────────────────────────
@app.route('/api/nginx/rules/<domain>/config', methods=['GET'])
def get_config(domain):
"""获取指定域名的 Nginx 配置文件原始内容"""
try:
import os as _os
config_path = _os.path.join(nginx_conf_dir, f'{domain}.conf')
if not _os.path.exists(config_path):
return {'code': -1, 'message': f'{domain} 的配置文件不存在', 'data': None}, 404
with open(config_path, 'r', encoding='utf-8') as f:
content = f.read()
return {
'code': 0,
'message': 'success',
'data': {
'domain': domain,
'config_file': config_path,
'content': content
}
}
except Exception as e:
logger.error(f"获取配置文件失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/rules/<domain>/config', methods=['PUT'])
def update_config(domain):
"""直接写入 Nginx 配置文件(会进行语法检查,失败自动回滚)"""
try:
body = request.get_json(force=True)
new_content = body.get('content', '')
if not new_content.strip():
return {'code': -1, 'message': '配置内容不能为空'}, 400
import os as _os
config_path = _os.path.join(nginx_conf_dir, f'{domain}.conf')
old_content = None
old_exists = _os.path.exists(config_path)
# 备份旧内容(如果存在)
if old_exists:
with open(config_path, 'r', encoding='utf-8') as f:
old_content = f.read()
# 写入新内容
with open(config_path, 'w', encoding='utf-8') as f:
f.write(new_content)
logger.info(f"已写入配置文件: {config_path}")
# 语法检查
valid, error_msg = nginx_service.check_config()
if not valid:
# 回滚
if old_exists:
with open(config_path, 'w', encoding='utf-8') as f:
f.write(old_content)
else:
_os.remove(config_path)
logger.error(f"Nginx 配置语法错误,已回滚: {error_msg}")
return {'code': -1, 'message': f'配置语法检查失败,已自动回滚: {error_msg}'}, 400
# 重载 Nginx
nginx_service.reload()
logger.info(f"Nginx 配置已重载: {domain}")
return {
'code': 0,
'message': '配置已保存并重载生效',
'data': {'domain': domain, 'config_file': config_path}
}
except Exception as e:
logger.error(f"更新配置文件失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/rules/<domain>/ssl', methods=['GET'])
def get_ssl_files(domain):
"""获取指定域名的 SSL 证书和密钥文件内容(仅查看,不可编辑)"""
try:
import os as _os
cert_path = _os.path.join(nginx_ssl_dir, f'{domain}.pem')
key_path = _os.path.join(nginx_ssl_dir, f'{domain}.key')
cert_content = None
key_content = None
if _os.path.exists(cert_path):
with open(cert_path, 'r', encoding='utf-8') as f:
cert_content = f.read()
if _os.path.exists(key_path):
with open(key_path, 'r', encoding='utf-8') as f:
key_content = f.read()
if not cert_content and not key_content:
return {'code': -1, 'message': f'{domain} 没有 SSL 证书文件', 'data': None}, 404
return {
'code': 0,
'message': 'success',
'data': {
'domain': domain,
'cert_path': cert_path,
'cert_content': cert_content,
'key_path': key_path,
'key_content': key_content,
'has_cert': bool(cert_content),
'has_key': bool(key_content)
}
}
except Exception as e:
logger.error(f"获取SSL证书文件失败: {e}")
return {'code': -1, 'message': str(e)}, 500
# ──────────────────────────────────────────────
# 内容压缩gzip接口
# ──────────────────────────────────────────────
@app.route('/api/nginx/rules/<domain>/gzip', methods=['GET'])
def get_gzip_config(domain):
"""获取指定域名的 gzip 压缩配置"""
try:
gzip_config = nginx_service.parse_gzip_config(domain)
return {'code': 0, 'message': 'success', 'data': gzip_config}
except Exception as e:
logger.error(f"获取gzip配置失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/rules/<domain>/gzip', methods=['PUT'])
def update_gzip_config(domain):
"""更新指定域名的 gzip 压缩配置"""
try:
body = request.get_json(force=True)
enabled = body.get('enabled', False)
types = body.get('types', [])
comp_level = body.get('comp_level', 6)
min_length = body.get('min_length', '1000')
# 参数校验
if enabled and not isinstance(types, list):
return {'code': -1, 'message': 'types 必须是数组'}, 400
if enabled and comp_level not in range(1, 10):
return {'code': -1, 'message': '压缩级别必须在 1-9 之间'}, 400
if enabled and not min_length:
return {'code': -1, 'message': '最小压缩长度不能为空'}, 400
gzip_config = {
'enabled': enabled,
'types': types,
'comp_level': comp_level,
'min_length': min_length
}
success, message = nginx_service.set_gzip_config(domain, gzip_config)
if success:
return {'code': 0, 'message': message, 'data': gzip_config}
return {'code': -1, 'message': message}, 400
except Exception as e:
logger.error(f"更新gzip配置失败: {e}")
return {'code': -1, 'message': str(e)}, 500
# ──────────────────────────────────────────────
# 域名日志接口
# ──────────────────────────────────────────────
@app.route('/api/nginx/rules/<domain>/logs', methods=['GET'])
def get_domain_logs(domain):
"""获取指定域名的 Nginx 访问日志和错误日志"""
try:
lines = request.args.get('lines', 200, type=int)
# 限制最大行数
lines = min(lines, 1000)
logs = nginx_service.get_domain_logs(domain, lines=lines)
return {'code': 0, 'message': 'success', 'data': logs}
except Exception as e:
logger.error(f"获取域名日志失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/reload', methods=['POST'])
def reload_nginx():
"""重载 Nginx 配置"""
try:
logger.info("重载 Nginx 配置")
success, message = nginx_service.reload()
if success:
return {'code': 0, 'message': message}
return {'code': -1, 'message': message}, 500
except Exception as e:
logger.error(f"重载 Nginx 失败: {e}")
return {'code': -1, 'message': str(e)}, 500
# ──────────────────────────────────────────────
# 证书管理接口Let's Encrypt
# ──────────────────────────────────────────────
@app.route('/api/nginx/certs', methods=['GET'])
def list_certs():
"""获取所有 SSL 证书信息列表Let's Encrypt + 内部 CA"""
try:
certs = []
# 扫描 SSL 目录中所有的 .pem 证书文件
import glob as _glob
# 要排除的文件CA 根证书等)
exclude_files = {'ca.pem', 'ca.crt'}
for cert_file in _glob.glob(f'{nginx_ssl_dir}/*.pem'):
filename = os.path.basename(cert_file)
if filename in exclude_files:
continue
domain = filename.replace('.pem', '')
info = ssl_service.get_le_cert_info(domain)
certs.append(info)
return {'code': 0, 'message': 'success', 'data': certs}
except Exception as e:
logger.error(f"获取证书列表失败: {e}")
return {'code': -1, 'message': str(e), 'data': []}
@app.route('/api/nginx/certs/<domain>', methods=['GET'])
def get_cert_info(domain):
"""获取单个 SSL 证书详情Let's Encrypt + 内部 CA"""
try:
info = ssl_service.get_le_cert_info(domain)
return {'code': 0, 'message': 'success', 'data': info}
except Exception as e:
logger.error(f"获取证书信息失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/certs/<domain>/issue', methods=['POST'])
def issue_cert(domain):
"""首次签发或重新签发 Let's Encrypt 证书"""
try:
if not ssl_service.is_zgitm_domain(domain):
return {'code': -1, 'message': f'{domain} 不是 zgitm.com 域名'}, 400
logger.info(f"签发 LE 证书: {domain}")
cert_path, key_path = ssl_service.ensure_le_certificate(domain)
if cert_path and key_path:
# 获取最新证书信息
info = ssl_service.get_le_cert_info(domain)
return {'code': 0, 'message': f'{domain} 证书签发成功', 'data': info}
else:
return {'code': -1, 'message': f'{domain} 证书签发失败,请检查阿里云 AccessKey 和网络'}, 500
except Exception as e:
logger.error(f"签发证书失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/certs/<domain>/renew', methods=['POST'])
def renew_cert(domain):
"""手动续期 Let's Encrypt 证书"""
try:
if not ssl_service.is_zgitm_domain(domain):
return {'code': -1, 'message': f'{domain} 不是 zgitm.com 域名'}, 400
logger.info(f"手动续期 LE 证书: {domain}")
result = ssl_service.renew_le_certificate(domain)
if result['success']:
return {'code': 0, 'message': result['message'], 'data': {'log': result['log']}}
else:
return {'code': -1, 'message': result['message'], 'data': {'log': result['log']}}, 500
except Exception as e:
logger.error(f"续期证书失败: {e}")
return {'code': -1, 'message': str(e)}, 500
# ──────────────────────────────────────────────
# DNS 解析管理接口
# ──────────────────────────────────────────────
@app.route('/api/nginx/dns/records', methods=['GET'])
def list_dns_records():
"""获取 DNS 解析记录列表"""
try:
zone = request.args.get('zone', ssl_service.get_le_root_domain())
rr = request.args.get('rr', None)
rtype = request.args.get('type', None)
records = dns_service.list_records(zone, rr_keyword=rr, record_type=rtype)
return {'code': 0, 'message': 'success', 'data': records}
except Exception as e:
logger.error(f"获取DNS记录失败: {e}")
return {'code': -1, 'message': str(e), 'data': []}
@app.route('/api/nginx/dns/records', methods=['POST'])
def add_dns_record():
"""添加 DNS 解析记录"""
try:
body = request.get_json(force=True)
zone = body.get('zone', ssl_service.get_le_root_domain())
rr = body.get('rr', '').strip()
record_type = body.get('type', 'A').upper()
value = body.get('value', '').strip()
ttl = body.get('ttl', 600)
if not rr:
return {'code': -1, 'message': '主机记录(rr)不能为空'}, 400
if not value:
return {'code': -1, 'message': '记录值(value)不能为空'}, 400
result = dns_service.add_record(zone, rr, record_type, value, ttl)
return {'code': 0, 'message': f'{result["full_domain"]} 记录已添加', 'data': result}
except Exception as e:
logger.error(f"添加DNS记录失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/dns/records/<record_id>', methods=['DELETE'])
def delete_dns_record(record_id):
"""删除 DNS 解析记录"""
try:
dns_service.delete_record(record_id)
return {'code': 0, 'message': 'DNS记录已删除'}
except Exception as e:
logger.error(f"删除DNS记录失败: {e}")
return {'code': -1, 'message': str(e)}, 500
@app.route('/api/nginx/dns/records/<record_id>', methods=['PUT'])
def update_dns_record(record_id):
"""修改 DNS 解析记录"""
try:
body = request.get_json(force=True)
rr = body.get('rr', '@')
record_type = body.get('type', 'A').upper()
value = body.get('value', '').strip()
ttl = body.get('ttl', 600)
if not value:
return {'code': -1, 'message': '记录值(value)不能为空'}, 400
dns_service.update_record(record_id, rr, record_type, value, ttl)
return {'code': 0, 'message': 'DNS记录已更新'}
except Exception as e:
logger.error(f"修改DNS记录失败: {e}")
return {'code': -1, 'message': str(e)}, 500
# ──────────────────────────────────────────────
# 启动入口
# ──────────────────────────────────────────────
if __name__ == '__main__':
logger.info("Nginx Manager API 启动在端口 5000")
app.run(host='0.0.0.0', port=5000, debug=False)