""" 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() # 启动时初始化内部 CA(gunicorn 模式下也必须执行) 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/', 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/', 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/', 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//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//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//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//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//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//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/', 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//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//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/', 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/', 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)