ProxyCat/app.py

652 lines
24 KiB
Python
Raw Normal View History

2025-02-21 16:48:20 +08:00
from flask import Flask, render_template, jsonify, request, redirect, url_for
import sys
import os
import logging
from datetime import datetime
from enum import Enum
import json
from configparser import ConfigParser
from itertools import cycle
import werkzeug.serving
from functools import wraps
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ProxyCat import run_server
from modules.modules import load_config, check_proxies, get_message, load_ip_list
from modules.proxyserver import AsyncProxyServer
import asyncio
import threading
import time
app = Flask(__name__,
template_folder='web/templates')
werkzeug.serving.WSGIRequestHandler.log = lambda self, type, message, *args: None
logging.getLogger('werkzeug').setLevel(logging.ERROR)
config = load_config('config/config.ini')
server = AsyncProxyServer(config)
log_file = 'logs/proxycat.log'
os.makedirs('logs', exist_ok=True)
log_messages = []
max_log_messages = 10000
class CustomFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
return datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S')
file_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setFormatter(file_formatter)
class MemoryHandler(logging.Handler):
def emit(self, record):
global log_messages
log_messages.append({
'time': datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S'),
'level': record.levelname,
'message': self.format(record)
})
if len(log_messages) > max_log_messages:
log_messages = log_messages[-max_log_messages:]
console_handler = logging.StreamHandler()
console_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
memory_handler = MemoryHandler()
memory_handler.setFormatter(CustomFormatter('%(message)s'))
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
root_logger.addHandler(memory_handler)
def require_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.args.get('token')
config_token = server.config.get('token', '')
if not config_token:
return f(*args, **kwargs)
if not token or token != config_token:
return jsonify({
'status': 'error',
'message': get_message('invalid_token', server.language)
}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/')
def root():
token = request.args.get('token')
if token:
return redirect(f'/web?token={token}')
return redirect('/web')
@app.route('/web')
@require_token
def web():
return render_template('index.html')
@app.route('/api/status')
@require_token
def get_status():
with open('config/config.ini', 'r', encoding='utf-8') as f:
config_content = f.read()
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
server_config = dict(config.items('Server')) if config.has_section('Server') else {}
return jsonify({
'current_proxy': server.current_proxy,
'mode': server.mode,
'port': int(server_config.get('port', '1080')),
'interval': server.interval,
'time_left': server.time_until_next_switch(),
'total_proxies': len(server.proxies) if hasattr(server, 'proxies') else 0,
'use_getip': server.use_getip,
'getip_url': getattr(server, 'getip_url', '') if getattr(server, 'use_getip', False) else '',
'auth_required': server.auth_required,
'display_level': int(config.get('DEFAULT', 'display_level', fallback='1')),
'config': {
'port': server_config.get('port', ''),
'mode': server_config.get('mode', 'cycle'),
'interval': server_config.get('interval', ''),
'username': server_config.get('username', ''),
'password': server_config.get('password', ''),
'use_getip': server_config.get('use_getip', 'False'),
'getip_url': server_config.get('getip_url', ''),
'proxy_username': server_config.get('proxy_username', ''),
'proxy_password': server_config.get('proxy_password', ''),
'proxy_file': server_config.get('proxy_file', ''),
'check_proxies': server_config.get('check_proxies', 'False'),
'language': server_config.get('language', 'cn'),
'whitelist_file': server_config.get('whitelist_file', ''),
'blacklist_file': server_config.get('blacklist_file', ''),
'ip_auth_priority': server_config.get('ip_auth_priority', 'whitelist'),
'display_level': config.get('DEFAULT', 'display_level', fallback='1'),
'raw_content': config_content
}
})
@app.route('/api/config', methods=['GET', 'POST'])
def handle_config():
if request.method == 'POST':
new_config = request.json
try:
with open('config/config.ini', 'r', encoding='utf-8') as f:
lines = f.readlines()
current_section = None
updated_lines = []
i = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith('['):
current_section = line[1:-1]
updated_lines.append(lines[i])
i += 1
continue
if line.startswith('#') or not line:
updated_lines.append(lines[i])
i += 1
continue
if '=' in line:
key = line.split('=')[0].strip()
if key in new_config:
updated_lines.append(f"{key} = {new_config[key]}\n")
else:
updated_lines.append(lines[i])
i += 1
continue
updated_lines.append(lines[i])
i += 1
with open('config/config.ini', 'w', encoding='utf-8') as f:
f.writelines(updated_lines)
config = load_config('config/config.ini')
server.config = config
server.mode = config.get('mode', 'cycle')
server.interval = int(config.get('interval', '300'))
server.language = config.get('language', 'cn')
server.use_getip = config.get('use_getip', 'False').lower() == 'true'
server.check_proxies = config.get('check_proxies', 'True').lower() == 'true'
server.username = config.get('username', '')
server.password = config.get('password', '')
server.proxy_username = config.get('proxy_username', '')
server.proxy_password = config.get('proxy_password', '')
server.auth_required = bool(server.username and server.password)
server.proxy_file = config.get('proxy_file')
server.whitelist_file = config.get('whitelist_file', '')
server.blacklist_file = config.get('blacklist_file', '')
if server.use_getip:
server.getip_url = config.get('getip_url', '')
old_port = int(server.config.get('port', '1080'))
new_port = int(new_config.get('port', '1080'))
needs_restart = old_port != new_port
return jsonify({
'status': 'success',
'needs_restart': needs_restart,
'message': '配置已更新,需要重启服务器' if needs_restart else '配置已更新'
})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)})
else:
with open('config/config.ini', 'r', encoding='utf-8') as f:
config_content = f.read()
return jsonify({'config': config_content})
@app.route('/api/proxies', methods=['GET', 'POST'])
def handle_proxies():
if request.method == 'POST':
try:
proxies = request.json.get('proxies', [])
with open(server.proxy_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(proxies))
server.proxies = server._load_file_proxies()
if server.proxies:
server.proxy_cycle = cycle(server.proxies)
server.current_proxy = next(server.proxy_cycle)
return jsonify({
'status': 'success',
'message': get_message('proxy_save_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('proxy_save_failed', server.language, str(e))
})
else:
try:
with open(server.proxy_file, 'r', encoding='utf-8') as f:
proxies = f.read().splitlines()
return jsonify({'proxies': proxies})
except Exception as e:
return jsonify({'proxies': []})
@app.route('/api/check_proxies')
def check_proxies_api():
try:
test_url = request.args.get('test_url', 'https://www.baidu.com')
valid_proxies = asyncio.run(check_proxies(server.proxies, test_url))
total_valid = len(valid_proxies)
return jsonify({
'status': 'success',
'valid_proxies': valid_proxies,
'total': total_valid,
'message': get_message('proxy_check_result', server.language, total_valid)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('proxy_check_failed', server.language, str(e))
})
@app.route('/api/ip_lists', methods=['GET', 'POST'])
def handle_ip_lists():
if request.method == 'POST':
try:
list_type = request.json.get('type')
ip_list = request.json.get('list', [])
filename = server.whitelist_file if list_type == 'whitelist' else server.blacklist_file
with open(filename, 'w', encoding='utf-8') as f:
f.write('\n'.join(ip_list))
if list_type == 'whitelist':
server.whitelist = load_ip_list(filename)
else:
server.blacklist = load_ip_list(filename)
return jsonify({
'status': 'success',
'message': get_message('ip_list_save_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('ip_list_save_failed', server.language, str(e))
})
else:
return jsonify({
'whitelist': list(load_ip_list(server.whitelist_file)),
'blacklist': list(load_ip_list(server.blacklist_file))
})
@app.route('/api/logs')
def get_logs():
try:
start = int(request.args.get('start', 0))
limit = int(request.args.get('limit', 100))
level = request.args.get('level', 'ALL')
search = request.args.get('search', '').lower()
filtered_logs = log_messages
if level != 'ALL':
filtered_logs = [log for log in log_messages if log['level'] == level]
if search:
filtered_logs = [
log for log in filtered_logs
if search in log['message'].lower() or
search in log['level'].lower() or
search in log['time'].lower()
]
return jsonify({
'logs': filtered_logs[start:start+limit],
'total': len(filtered_logs),
'status': 'success'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
})
@app.route('/api/logs/clear', methods=['POST'])
def clear_logs():
try:
global log_messages
log_messages = []
with open(log_file, 'w', encoding='utf-8') as f:
f.write('')
return jsonify({
'status': 'success',
'message': get_message('logs_cleared', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('clear_logs_failed', server.language, str(e))
})
@app.route('/api/switch_proxy')
@require_token
def switch_proxy():
try:
if server.use_getip:
from config.getip import newip
try:
old_proxy = server.current_proxy
new_proxy = newip()
server.current_proxy = new_proxy
server.last_switch_time = time.time()
logging.info(get_message('manual_switch', server.language, old_proxy, new_proxy))
return jsonify({
'status': 'success',
'current_proxy': server.current_proxy,
'message': get_message('switch_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('get_proxy_failed', server.language, str(e))
})
else:
if not server.proxies:
server.proxies = server._load_file_proxies()
if server.proxies:
server.proxy_cycle = cycle(server.proxies)
if server.proxy_cycle:
old_proxy = server.current_proxy
server.current_proxy = next(server.proxy_cycle)
server.last_switch_time = time.time()
logging.info(get_message('manual_switch', server.language, old_proxy, server.current_proxy))
return jsonify({
'status': 'success',
'current_proxy': server.current_proxy,
'message': get_message('switch_success', server.language)
})
else:
return jsonify({
'status': 'error',
'message': get_message('no_proxies_available', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('switch_failed', server.language, str(e))
})
@app.route('/api/service', methods=['POST'])
@require_token
def control_service():
try:
action = request.json.get('action')
if action == 'start':
if not server.running:
server.stop_server = False
if hasattr(server, 'proxy_thread') and server.proxy_thread and server.proxy_thread.is_alive():
server.proxy_thread.join(timeout=5)
server.proxy_thread = threading.Thread(target=lambda: asyncio.run(run_server(server)), daemon=True)
server.proxy_thread.start()
for _ in range(10):
if server.running:
break
time.sleep(0.5)
if server.running:
return jsonify({
'status': 'success',
'message': get_message('service_start_success', server.language)
})
else:
return jsonify({
'status': 'error',
'message': get_message('service_start_failed', server.language)
})
return jsonify({
'status': 'success',
'message': get_message('service_already_running', server.language)
})
elif action == 'stop':
if server.running:
server.stop_server = True
if server.server_instance:
server.server_instance.close()
for _ in range(10):
if not server.running:
break
time.sleep(0.5)
if server.running:
if hasattr(server, 'proxy_thread') and server.proxy_thread:
server.proxy_thread = None
server.running = False
return jsonify({
'status': 'success',
'message': get_message('service_stop_success', server.language)
})
return jsonify({
'status': 'success',
'message': get_message('service_not_running', server.language)
})
elif action == 'restart':
if server.running:
server.stop_server = True
if server.server_instance:
server.server_instance.close()
for _ in range(10):
if not server.running:
break
time.sleep(0.5)
if server.running:
if hasattr(server, 'proxy_thread') and server.proxy_thread:
server.proxy_thread = None
server.running = False
server.stop_server = False
server.proxy_thread = threading.Thread(target=lambda: asyncio.run(run_server(server)), daemon=True)
server.proxy_thread.start()
for _ in range(10):
if server.running:
break
time.sleep(0.5)
if server.running:
return jsonify({
'status': 'success',
'message': get_message('service_restart_success', server.language)
})
else:
return jsonify({
'status': 'error',
'message': get_message('service_restart_failed', server.language)
})
return jsonify({
'status': 'error',
'message': get_message('invalid_action', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('operation_failed', server.language, str(e))
})
@app.route('/api/language', methods=['POST'])
def change_language():
try:
new_language = request.json.get('language', 'cn')
if new_language not in ['cn', 'en']:
return jsonify({
'status': 'error',
'message': get_message('unsupported_language', server.language)
})
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
if 'Server' not in config:
config.add_section('Server')
config.set('Server', 'language', new_language)
with open('config/config.ini', 'w', encoding='utf-8') as f:
config.write(f)
server.language = new_language
return jsonify({
'status': 'success',
'language': new_language
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('operation_failed', server.language, str(e))
})
@app.route('/api/version')
def check_version():
try:
import re
import httpx
from packaging import version
import logging
httpx_logger = logging.getLogger('httpx')
original_level = httpx_logger.level
httpx_logger.setLevel(logging.WARNING)
CURRENT_VERSION = "ProxyCat-V2.0.0"
try:
client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
response = client.get("https://y.shironekosan.cn/1.html", timeout=10)
response.raise_for_status()
content = response.text
finally:
httpx_logger.setLevel(original_level)
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
if match:
latest_version = match.group(1)
is_latest = version.parse(latest_version.split('-V')[1]) <= version.parse(CURRENT_VERSION.split('-V')[1])
return jsonify({
'status': 'success',
'is_latest': is_latest,
'current_version': CURRENT_VERSION,
'latest_version': latest_version
})
else:
return jsonify({
'status': 'error',
'message': get_message('version_info_not_found', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('update_check_error', server.language, str(e))
})
@app.route('/api/users', methods=['GET', 'POST'])
@require_token
def handle_users():
if request.method == 'POST':
try:
users = request.json.get('users', {})
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
sections_to_preserve = {}
for section in config.sections():
if section != 'Users':
sections_to_preserve[section] = dict(config.items(section))
config = ConfigParser()
for section, options in sections_to_preserve.items():
config.add_section(section)
for key, value in options.items():
config.set(section, key, value)
if users:
config.add_section('Users')
for username, password in users.items():
config.set('Users', username, password)
with open('config/config.ini', 'w', encoding='utf-8') as f:
config.write(f)
server.users = users
server.auth_required = bool(users)
if hasattr(server, 'proxy_server') and server.proxy_server:
server.proxy_server.users = users
server.proxy_server.auth_required = bool(users)
return jsonify({
'status': 'success',
'message': get_message('users_save_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('users_save_failed', server.language, str(e))
})
else:
try:
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
users = {}
if config.has_section('Users'):
users = dict(config.items('Users'))
return jsonify({'users': users})
except Exception as e:
logging.error(f"Error getting users: {e}")
return jsonify({'users': {}})
def run_proxy_server():
asyncio.run(run_server(server))
if __name__ == '__main__':
web_port = int(config.get('web_port', '5000'))
web_url = f"http://127.0.0.1:{web_port}"
if config.get('token'):
web_url += f"?token={config.get('token')}"
logging.info(get_message('web_panel_url', server.language, web_url))
logging.info(get_message('web_panel_notice', server.language))
proxy_thread = threading.Thread(target=run_proxy_server, daemon=True)
proxy_thread.start()
app.run(host='0.0.0.0', port=web_port)