blob: 42f1d62af334ba21e704327e1f4a4b4a358b17b7 [file] [log] [blame] [raw]
from flask import Flask, request, send_from_directory, jsonify
from flask_cors import CORS
import json
import re
import os
import shutil
from datetime import datetime
import glob
app = Flask(__name__, static_folder='public')
CORS(app, resources={
r"/*": {
"origins": "*",
"methods": ["GET", "POST", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization", "Access-Control-Allow-Origin"],
"expose_headers": ["Content-Type", "Authorization"],
"supports_credentials": True
}
})
def read_config(filename):
try:
# 使用 os.path.join 来正确处理路径
filepath = filename # 现在 filename 已经包含完整路径
print(f"Reading config from: {filepath}") # 添加日志
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 移除注释
json_content = re.sub(r'/\*[\s\S]*?\*/', '', content) # 移除多行注释
json_content = '\n'.join(
line for line in json_content.split('\n')
if not line.strip().startswith('//') # 移除单行注释
)
json_content = re.sub(r',(\s*[}\]])', r'\1', json_content) # 移除末尾逗号
try:
config = json.loads(json_content)
print(f"Successfully parsed config: {config}") # 添加日志
except json.JSONDecodeError as e:
print(f"JSON parse error: {str(e)}") # 添加日志
print(f"Content being parsed: {json_content}")
raise
# 确保必要字段存在
config.setdefault('rncn-config-version', 1)
config.setdefault('geodata-mode', False)
config.setdefault('geo-auto-update', True)
config.setdefault('geo-update-interval', 24)
config.setdefault('geox-url', {'mmdb': ''})
config.setdefault('hosts', {})
config.setdefault('dns', {})
return config
except Exception as e:
print(f"解析 {filename} 失败:", str(e))
print("文件内容:", content)
raise Exception(f"解析 {filename} 失败: {str(e)}")
def write_config(filename, data):
try:
filepath = filename
print(f"Writing config to: {filepath}")
# 格式化 hosts 对象
hosts_entries = []
for domain, ip in sorted(data['hosts'].items()):
hosts_entries.append(f'\t\t"{domain}"\t\t:\t"{ip}"')
hosts_content = ',\n'.join(hosts_entries)
# 根据文件名判断是国内还是海外配置
is_cn_config = 'cn-config' in filepath
# 构建新的文件内容
config = {
"rncn-config-version": data["rncn-config-version"],
"geodata-mode": data.get("geodata-mode", False),
"geo-auto-update": data.get("geo-auto-update", True),
"geo-update-interval": data.get("geo-update-interval", 24),
"geox-url": {
"mmdb": data.get("geox-url", {}).get("mmdb", "")
},
"hosts": data["hosts"],
"dns": {
"use-hosts": True,
"use-system-hosts": False,
"respect-rules": False,
"enhanced-mode": "normal",
"ipv6": True
}
}
# 根据配置类型设置不同的 DNS 配置
if is_cn_config:
config["dns"].update({
"default-nameserver": ["223.5.5.5"],
"nameserver": [
"https://223.5.5.5/dns-query",
"https://223.6.6.6/dns-query"
],
"fallback": [
"https://1.0.0.1/dns-query"
],
"fallback-filter": {
"geoip": True,
"geoip-code": "CN"
}
})
else:
config["dns"].update({
"default-nameserver": ["1.1.1.1", "1.0.0.1"],
"nameserver": [
"https://1.1.1.1/dns-query",
"https://1.0.0.1/dns-query"
]
})
# 使用 json.dumps 生成基本 JSON 字符串
json_str = json.dumps(config, indent='\t')
# 替换 hosts 部分的格式
json_str = re.sub(
r'"hosts":\s*{[^}]*}',
f'"hosts": {{\n{hosts_content}\n\t}}',
json_str
)
# 写入文件
with open(filepath, 'w', encoding='utf-8') as f:
f.write(json_str)
except Exception as e:
print(f"写入 {filename} 失败:", str(e))
print("尝试写入的内容:", json_str)
raise Exception(f"写入 {filename} 失败: {str(e)}")
def load_app_config():
try:
with open('config.json', 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载配置文件失败: {str(e)}")
return {
"title": "DNS 配置管理",
"files": {
"oversea": "gen/rncn-dns-oversea-config.js",
"cn": "gen/rncn-dns-cn-config.js",
"history": {
"dir": "gen/history",
"maxVersions": 10
}
},
"server": {
"host": "0.0.0.0",
"port": 3001
}
}
# 加载配置
app_config = load_app_config()
@app.route('/')
def index():
return send_from_directory('public', 'index.html')
@app.route('/api/config', methods=['GET'])
def get_config():
try:
# 读取配置文件
oversea_config = read_config(app_config['files']['oversea'])
cn_config = read_config(app_config['files']['cn'])
# 提供默认的 hints
default_hints = {
"domain": "[默认提示] 域名格式如 \"+.example.com\" 或 \"specific.example.com\"",
"ip": {
"cn": "[默认提示] 国内解锁集群: 10.100.253.2",
"overseas": "[默认提示] 海外解锁集群: 10.100.253.1"
}
}
# 检查是否使用了默认值
using_default_hints = 'hints' not in app_config
if using_default_hints:
print("警告: 未找到配置文件中的 hints 配置,使用默认值")
# 构建域名映射
hosts = {
domain: {
'cnIp': cn_config['hosts'].get(domain, ''),
'overseasIp': oversea_config['hosts'].get(domain, '')
}
for domain in set(list(cn_config['hosts'].keys()) + list(oversea_config['hosts'].keys()))
}
return jsonify({
'title': app_config.get('title', 'DNS 配置管理'),
'hosts': hosts, # 使用合并后的域名映射
'hints': app_config.get('hints', default_hints),
'cnVersion': cn_config['rncn-config-version'],
'overseaVersion': oversea_config['rncn-config-version'],
'usingDefaultHints': using_default_hints
})
except Exception as e:
print(f"Error in get_config: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/config', methods=['POST'])
def update_config():
try:
data = request.json
cn_hosts = data['hosts']['cn']
overseas_hosts = data['hosts']['overseas']
# 读取现有配置
oversea_config = read_config(app_config['files']['oversea'])
cn_config = read_config(app_config['files']['cn'])
# 检查是否有实际更改
cn_changed = cn_hosts != cn_config['hosts']
oversea_changed = overseas_hosts != oversea_config['hosts']
if not cn_changed and not oversea_changed:
return jsonify({
'success': True,
'message': '配置未发生变化'
})
# 只更新发生变化的配置
if cn_changed:
cn_config['rncn-config-version'] += 1
cn_config['hosts'] = cn_hosts
save_history('cn', cn_config)
write_config(app_config['files']['cn'], cn_config)
if oversea_changed:
oversea_config['rncn-config-version'] += 1
oversea_config['hosts'] = overseas_hosts
save_history('oversea', oversea_config)
write_config(app_config['files']['oversea'], oversea_config)
return jsonify({
'success': True,
'message': '配置已更新',
'updated': {
'cn': cn_changed,
'oversea': oversea_changed
}
})
except Exception as e:
print(f"Error in update_config: {str(e)}")
return jsonify({'error': str(e)}), 500
def save_history(config_type, config_data):
history_dir = os.path.join(app_config['files']['history']['dir'], config_type)
os.makedirs(history_dir, exist_ok=True)
# 只使用版本号命名文件
version = config_data['rncn-config-version']
history_file = f"{config_type}_v{version}.js"
# 保存历史文件
filepath = os.path.join(history_dir, history_file)
with open(filepath, 'w', encoding='utf-8') as f:
json_str = json.dumps(config_data, indent='\t')
f.write(json_str)
# 清理旧版本
max_versions = app_config['files']['history'].get('maxVersions', 10)
files = glob.glob(os.path.join(history_dir, f"{config_type}_v*.js"))
files.sort(key=lambda x: os.path.getmtime(x), reverse=True) # 按修改时间排序
for old_file in files[max_versions:]:
os.remove(old_file)
@app.route('/api/history', methods=['GET'])
def get_history():
try:
history = {'cn': [], 'oversea': []}
history_dir = app_config['files']['history']['dir']
for config_type in ['cn', 'oversea']:
type_dir = os.path.join(history_dir, config_type)
if os.path.exists(type_dir):
files = glob.glob(os.path.join(type_dir, f"{config_type}_v*.js"))
# 按修改时间排序
files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
for file in files:
with open(file, 'r', encoding='utf-8') as f:
config = json.load(f)
filename = os.path.basename(file)
mtime = os.path.getmtime(file)
history[config_type].append({
'version': config['rncn-config-version'],
'timestamp': int(mtime), # 使用文件修改时间
'filename': filename,
'config': config
})
return jsonify(history)
except Exception as e:
print(f"Error in get_history: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/history/restore', methods=['POST'])
def restore_history():
try:
data = request.json
config_type = data['type'] # 'cn' 或 'oversea'
version = data['version']
# 读取历史文件
history_dir = os.path.join(app_config['files']['history']['dir'], config_type)
files = glob.glob(os.path.join(history_dir, f"{config_type}_v{version}.js"))
if not files:
raise ValueError(f"找不到版本 {version} 的历史记录")
# 读取历史配置
with open(files[0], 'r', encoding='utf-8') as f:
history_config = json.load(f)
# 读取当前配置
current_file = app_config['files'][config_type]
current_config = read_config(current_file)
# 保存当前配置到历史记录
save_history(config_type, current_config)
# 更新历史配置的版本号为当前版本号+1
history_config['rncn-config-version'] = current_config['rncn-config-version'] + 1
# 恢复历史配置
write_config(current_file, history_config)
# 保存恢复后的配置到历史记录
save_history(config_type, history_config)
return jsonify({
'success': True,
'newVersion': history_config['rncn-config-version']
})
except Exception as e:
print(f"Error in restore_history: {str(e)}")
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(
host=app_config.get('server', {}).get('host', '0.0.0.0'),
port=app_config.get('server', {}).get('port', 3001),
debug=True
)