diff --git a/Dockerfile b/Dockerfile index fe6dccf..4aecfcc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,16 @@ FROM python:3.11 WORKDIR /app +COPY requirements.txt . + +RUN pip install --upgrade pip -i https://pypi.mirrors.ustc.edu.cn/simple/ && \ + pip install --no-cache-dir -r requirements.txt -i https://pypi.mirrors.ustc.edu.cn/simple/ + COPY . . -RUN pip install --upgrade pip -i https://pypi.mirrors.ustc.edu.cn/simple/ +RUN rm -f config/config.ini -RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.mirrors.ustc.edu.cn/simple/ +VOLUME ["/app/config"] -EXPOSE 1080 - -CMD ["python", "ProxyCat.py"] +CMD ["python", "app.py"] diff --git a/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-23-35.png b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-23-35.png new file mode 100644 index 0000000..c83112a Binary files /dev/null and b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-23-35.png differ diff --git a/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-28-30-1740126513899-10.png b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-28-30-1740126513899-10.png new file mode 100644 index 0000000..5793997 Binary files /dev/null and b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-28-30-1740126513899-10.png differ diff --git a/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-29-21.png b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-29-21.png new file mode 100644 index 0000000..d5b8977 Binary files /dev/null and b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-29-21.png differ diff --git a/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-29-45.png b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-29-45.png new file mode 100644 index 0000000..b5b6194 Binary files /dev/null and b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-29-45.png differ diff --git a/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-30-51.png b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-30-51.png new file mode 100644 index 0000000..cd5dd97 Binary files /dev/null and b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-30-51.png differ diff --git a/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-32-14.png b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-32-14.png new file mode 100644 index 0000000..fe15589 Binary files /dev/null and b/ProxyCat-Manual/Operation Manual.assets/Clip_2025-02-21_16-32-14.png differ diff --git a/ProxyCat-Manual/Operation Manual.md b/ProxyCat-Manual/Operation Manual.md index 5ae566e..f4076e9 100644 --- a/ProxyCat-Manual/Operation Manual.md +++ b/ProxyCat-Manual/Operation Manual.md @@ -79,8 +79,102 @@ docker-compose down | docker-compose up -d # 查看日志信息 docker logs proxycat + +# docker端口默认为1080和5000,1080为监听端口,5000为web页面管理如需其他端口请对应修改并放行 ``` +### 配置文件介绍 + +``` +# 日志显示级别(默认为:1) +# 0: 仅显示代理切换和错误信息 +# 1: 显示代理切换、倒计时和错误信息 +# 2: 显示所有详细信息 +display_level = 1 + +# 本地服务器监听端口(默认为:1080) +# Local server listening port (default:1080) +port = 1080 + +# Web 管理页面端口(默认为:5000) +web_port = 5000 + +# 代理地址轮换模式:cycle 表示循环使用,custom 表示使用自定义模式,load_balance 表示负载均衡(默认为:cycle) +# Proxy rotation mode: cycle means cyclic use, custom means custom mode, load_balance means load balancing (default:cycle) +mode = cycle + +# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300) +# Proxy address rotation interval (seconds), when set to 0, IP changes with each request (default:300) +interval = 300 + +# 是否使用 getip 模块获取代理地址 True or False(默认为:False) +# Whether to use getip module to obtain proxy addresses True or False (default:False) +use_getip = False + +# 获取新代理地址的URL +# URL to get new proxy address +getip_url = http://example.com/getip + +# 代理服务器认证用户名(如果代理服务器需要认证) +# Proxy server authentication username (if proxy server requires authentication) +proxy_username = + +# 代理服务器认证密码(如果代理服务器需要认证) +# Proxy server authentication password (if proxy server requires authentication) +proxy_password = + +# 代理地址列表文件(默认为:ip.txt) +# Proxy address list file (default:ip.txt) +proxy_file = ip.txt + +# 是否启用代理检测功能 True or False(默认为True) +# Whether to enable proxy detection feature True or False (default:True) +check_proxies = True + +# 语言设置 (cn/en) +# Language setting (cn/en) +language = cn + +# IP白名单文件路径(留空则不启用白名单) +# IP whitelist file path (leave empty to disable whitelist) +whitelist_file = whitelist.txt + +# IP黑名单文件路径(留空则不启用黑名单) +# IP blacklist file path (leave empty to disable blacklist) +blacklist_file = blacklist.txt + +# IP认证优先级(whitelist/blacklist) +# IP authentication priority (whitelist/blacklist) +# whitelist: 优先判断白名单,在白名单中的IP直接放行 +# whitelist: prioritize whitelist check, IPs in whitelist are allowed directly +# blacklist: 优先判断黑名单,在黑名单中的IP直接拒绝 +# blacklist: prioritize blacklist check, IPs in blacklist are rejected directly +ip_auth_priority = whitelist + +# Web 管理页面访问token,留空则无需token(默认为:honmashironeko) +token = honmashironeko + +# 在[Users]下面是用户管理组,"账号=密码"一行一个,留空时代理无需身份鉴别(默认为:neko=123456) +[Users] +neko=123456 +``` + +### Web 控制面板 + +采用源码部署的话通过 **python app.py** 启动 Web 控制面板,并根据提示访问 Web + +![Clip_2025-02-21_16-23-35](./Operation%20Manual.assets/Clip_2025-02-21_16-23-35.png) + +![Clip_2025-02-21_16-32-14](./Operation%20Manual.assets/Clip_2025-02-21_16-32-14.png) + +![Clip_2025-02-21_16-28-30](./Operation%20Manual.assets/Clip_2025-02-21_16-28-30-1740126513899-10.png) + +![Clip_2025-02-21_16-29-21](./Operation%20Manual.assets/Clip_2025-02-21_16-29-21.png) + +![Clip_2025-02-21_16-29-45](./Operation%20Manual.assets/Clip_2025-02-21_16-29-45.png) + +![Clip_2025-02-21_16-30-51](./Operation%20Manual.assets/Clip_2025-02-21_16-30-51.png) + ## 问题Q&A Q:为什么运行后我的XXX工具代理还是没换? diff --git a/ProxyCat-Manual/logs.md b/ProxyCat-Manual/logs.md index 5302adb..e05c67e 100644 --- a/ProxyCat-Manual/logs.md +++ b/ProxyCat-Manual/logs.md @@ -1,3 +1,17 @@ +### 2025/02/21 + +- 增加 Web 管理界面 + +- 增加多用户模式 + +- 代码结构大改 + +- config.in及相关文件动态更新不需要重启 + +- 增加日志显示级别控制 +- 增加记录连接人信息日志,包括连接的IP和使用的账号密码 +- 以及其他乱七八糟的修改,这次大版本更新改的太多,我有点忘记了~ + ### 2025/02/06 - Docker 安装依赖库采用国内源 diff --git a/ProxyCat.py b/ProxyCat.py index 3dbf66f..ddaab67 100644 --- a/ProxyCat.py +++ b/ProxyCat.py @@ -1,25 +1,16 @@ -from modules.modules import load_config, DEFAULT_CONFIG, check_proxies, check_for_updates, get_message, print_banner, logos +from wsgiref import headers +from modules.modules import ColoredFormatter, load_config, DEFAULT_CONFIG, check_proxies, check_for_updates, get_message, load_ip_list, print_banner, logos import threading, argparse, logging, asyncio, time, socket, signal, sys, os from concurrent.futures import ThreadPoolExecutor from modules.proxyserver import AsyncProxyServer from colorama import init, Fore, Style from itertools import cycle +from tqdm import tqdm +import base64 +from configparser import ConfigParser init(autoreset=True) -class ColoredFormatter(logging.Formatter): - COLORS = { - logging.INFO: Fore.GREEN, - logging.WARNING: Fore.YELLOW, - logging.ERROR: Fore.RED, - logging.CRITICAL: Fore.RED + Style.BRIGHT, - } - - def format(self, record): - log_color = self.COLORS.get(record.levelno, Fore.WHITE) - record.msg = f"{log_color}{record.msg}{Style.RESET_ALL}" - return super().format(record) - log_format = '%(asctime)s - %(levelname)s - %(message)s' formatter = ColoredFormatter(log_format) @@ -29,24 +20,174 @@ console_handler.setFormatter(formatter) logging.basicConfig(level=logging.INFO, handlers=[console_handler]) def update_status(server): + def print_proxy_info(): + status = f"{get_message('current_proxy', server.language)}: {server.current_proxy}" + logging.info(status) + + def reload_server_config(new_config): + old_use_getip = server.use_getip + old_mode = server.mode + old_port = int(server.config.get('port', '1080')) + + server.config.update(new_config) + + server.port = int(new_config.get('port', '1080')) + server.mode = new_config.get('mode', 'cycle') + server.interval = int(new_config.get('interval', '300')) + server.language = new_config.get('language', 'cn') + server.use_getip = new_config.get('use_getip', 'False').lower() == 'true' + server.check_proxies = new_config.get('check_proxies', 'True').lower() == 'true' + + server.username = new_config.get('username', '') + server.password = new_config.get('password', '') + server.proxy_username = new_config.get('proxy_username', '') + server.proxy_password = new_config.get('proxy_password', '') + server.auth_required = bool(server.username and server.password) + + server.proxy_file = new_config.get('proxy_file', 'ip.txt') + server.whitelist_file = new_config.get('whitelist_file', '') + server.blacklist_file = new_config.get('blacklist_file', '') + server.ip_auth_priority = new_config.get('ip_auth_priority', 'whitelist') + + server.whitelist = load_ip_list(new_config.get('whitelist_file', '')) + server.blacklist = load_ip_list(new_config.get('blacklist_file', '')) + + if old_use_getip != server.use_getip or old_mode != server.mode: + if server.use_getip: + server.proxies = [] + server.proxy_cycle = None + server.current_proxy = None + logging.info(get_message('api_mode_notice', server.language)) + else: + server.proxies = server._load_file_proxies() + if server.proxies: + server.proxy_cycle = cycle(server.proxies) + server.current_proxy = next(server.proxy_cycle) + if server.check_proxies: + asyncio.run(run_proxy_check(server)) + + if server.use_getip: + server.getip_url = new_config.get('getip_url', '') + + server.last_switch_time = time.time() + + nonlocal display_level + display_level = int(new_config.get('display_level', '1')) + + if hasattr(server, 'progress_bar'): + if not is_docker: + server.progress_bar.close() + delattr(server, 'progress_bar') + if hasattr(server, 'last_update_time'): + delattr(server, 'last_update_time') + + if old_port != server.port: + logging.info(get_message('port_changed', server.language, old_port, server.port)) + + logging.info(get_message('config_updated', server.language)) + + display_level = int(server.config.get('display_level', '1')) + is_docker = os.path.exists('/.dockerenv') + + config_file = 'config/config.ini' + ip_file = server.proxy_file + last_config_modified_time = os.path.getmtime(config_file) if os.path.exists(config_file) else 0 + last_ip_modified_time = os.path.getmtime(ip_file) if os.path.exists(ip_file) else 0 + while True: try: - if server.mode == 'load_balance': - status = f"\r{Fore.YELLOW}{get_message('current_proxy', server.language)}: {Fore.GREEN}{server.current_proxy}" - else: - time_left = server.time_until_next_switch() - if time_left == float('inf'): - status = f"\r{Fore.YELLOW}{get_message('current_proxy', server.language)}: {Fore.GREEN}{server.current_proxy}" - else: - status = f"\r{Fore.YELLOW}{get_message('current_proxy', server.language)}: {Fore.GREEN}{server.current_proxy} | {Fore.YELLOW}{get_message('next_switch', server.language)}: {Fore.GREEN}{time_left:.1f}{get_message('seconds', server.language)}" + if os.path.exists(config_file): + current_config_modified_time = os.path.getmtime(config_file) + if current_config_modified_time > last_config_modified_time: + logging.info(get_message('config_file_changed', server.language)) + new_config = load_config(config_file) + reload_server_config(new_config) + last_config_modified_time = current_config_modified_time + continue + + if os.path.exists(ip_file) and not server.use_getip: + current_ip_modified_time = os.path.getmtime(ip_file) + if current_ip_modified_time > last_ip_modified_time: + logging.info(get_message('proxy_file_changed', server.language)) + server.proxies = server._load_file_proxies() + if server.proxies: + server.proxy_cycle = cycle(server.proxies) + server.current_proxy = next(server.proxy_cycle) + if server.check_proxies: + asyncio.run(run_proxy_check(server)) + last_ip_modified_time = current_ip_modified_time + continue - if os.path.exists('/.dockerenv'): - logging.info(status) - else: - print(status, end='', flush=True) + if display_level == 0: + if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy: + print_proxy_info() + server.last_proxy = server.current_proxy + time.sleep(1) + continue + + if server.mode == 'load_balance': + if display_level >= 1: + print_proxy_info() + time.sleep(5) + continue + + time_left = server.time_until_next_switch() + if time_left == float('inf'): + if display_level >= 1: + print_proxy_info() + time.sleep(5) + continue + + if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy: + print_proxy_info() + server.last_proxy = server.current_proxy + if display_level >= 2: + logging.info(get_message('proxy_switch_detail', server.language, + getattr(server, 'previous_proxy', 'None'), + server.current_proxy)) + server.previous_proxy = server.current_proxy + + total_time = int(server.interval) + elapsed_time = total_time - int(time_left) + + if display_level >= 1: + if elapsed_time > total_time: + if hasattr(server, 'progress_bar'): + if not is_docker: + server.progress_bar.n = total_time + server.progress_bar.refresh() + server.progress_bar.close() + delattr(server, 'progress_bar') + if hasattr(server, 'last_update_time'): + delattr(server, 'last_update_time') + time.sleep(0.5) + continue - except Exception: - pass + if is_docker: + if not hasattr(server, 'last_update_time') or \ + (time.time() - server.last_update_time >= (5 if display_level == 1 else 1) and elapsed_time <= total_time): + if display_level >= 2: + logging.info(f"{get_message('next_switch', server.language)}: {time_left:.0f} {get_message('seconds', server.language)} ({elapsed_time}/{total_time})") + else: + logging.info(f"{get_message('next_switch', server.language)}: {time_left:.0f} {get_message('seconds', server.language)}") + server.last_update_time = time.time() + else: + if not hasattr(server, 'progress_bar'): + server.progress_bar = tqdm( + total=total_time, + desc=f"{Fore.YELLOW}{get_message('next_switch', server.language)}{Style.RESET_ALL}", + bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} ' + get_message('seconds', server.language), + colour='green' + ) + + server.progress_bar.n = min(elapsed_time, total_time) + server.progress_bar.refresh() + + except Exception as e: + if display_level >= 2: + logging.error(f"Status update error: {e}") + elif display_level >= 1: + logging.error(get_message('status_update_error', server.language)) time.sleep(1) async def handle_client_wrapper(server, reader, writer, clients): @@ -60,26 +201,15 @@ async def handle_client_wrapper(server, reader, writer, clients): clients.remove(task) async def run_server(server): - clients = set() - server_instance = None try: - server_instance = await asyncio.start_server( - lambda r, w: handle_client_wrapper(server, r, w, clients), - '0.0.0.0', - int(server.config['port']), - limit=256 * 1024 - ) - async with server_instance: - await server_instance.serve_forever() + await server.start() except asyncio.CancelledError: logging.info(get_message('server_closing', server.language)) + except Exception as e: + if not server.stop_server: + logging.error(f"Server error: {e}") finally: - if server_instance: - server_instance.close() - await server_instance.wait_closed() - for client in clients: - client.cancel() - await asyncio.gather(*clients, return_exceptions=True) + await server.stop() async def run_proxy_check(server): if server.config.get('check_proxies', 'False').lower() == 'true': @@ -117,7 +247,15 @@ class ProxyCat: signal.signal(signal.SIGINT, self.handle_shutdown) signal.signal(signal.SIGTERM, self.handle_shutdown) - self.language = config.get('language', 'cn').lower() + self.config = load_config('config/config.ini') + self.language = self.config.get('language', 'cn').lower() + + self.users = {} + config = ConfigParser() + config.read('config/config.ini', encoding='utf-8') + if config.has_section('Users'): + self.users = dict(config.items('Users')) + self.auth_required = bool(self.users) async def start_server(self): try: @@ -143,7 +281,16 @@ class ProxyCat: sys.exit(0) async def handle_client(self, reader, writer): + task = asyncio.current_task() + self.tasks.add(task) try: + if self.auth_required: + auth_header = headers.get('proxy-authorization') + if not auth_header or not self._authenticate(auth_header): + writer.write(b'HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="Proxy"\r\n\r\n') + await writer.drain() + return + await asyncio.get_event_loop().run_in_executor( self.executor, self.process_client_request, @@ -159,6 +306,22 @@ class ProxyCat: except: pass + def _authenticate(self, auth_header): + if not self.users: + return True + + try: + scheme, credentials = auth_header.split() + if scheme.lower() != 'basic': + return False + + decoded_auth = base64.b64decode(credentials).decode() + username, password = decoded_auth.split(':') + + return username in self.users and self.users[username] == password + except: + return False + if __name__ == '__main__': parser = argparse.ArgumentParser(description=logos()) parser.add_argument('-c', '--config', default='config/config.ini', help='配置文件路径') diff --git a/README.md b/README.md index 1c9bd83..9207bc9 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ ## 功能特点 -- **双协议监听**:支持 HTTP/SOCKS5 协议监听,兼容更多工具。 -- **三种协议代理地址**:支持 HTTP/HTTPS/SOCKS5 代理服务器及身份鉴别,满足不同需求。 +- **两种协议监听**:支持 HTTP/SOCKS5 协议监听,兼容更多工具。 +- **三种代理地址**:支持 HTTP/HTTPS/SOCKS5 代理服务器及身份鉴别。 - **灵活切换模式**:支持顺序、随机及自定义代理选择,优化流量分配。 - **动态获取代理**:通过 GetIP 函数即时获取可用代理,支持 API 接口调用。 - **代理保护机制**:在使用 GetIP 方式获取代理时,首次运行不会直接请求获取,将会在收到请求的时候才获取。 @@ -38,6 +38,10 @@ - **失效代理切换**:代理失效后自动验证切换新代理,确保不中断服务。 - **身份认证支持**:支持用户名/密码认证和黑白名单管理,提高安全性。 - **实时状态显示**:展示代理状态和切换时间,实时掌握代理动态。 +- **动态更新配置**:无需重启服务,动态检测配置并更新。 +- **Web UI界面**:提供 Web 管理界面,操作管理更加便捷。 +- **Docker部署**:Docker 一键部署,Web 统一管理。 +- **中英文双语**:支持中文英文一键切换。 - **配置灵活**:通过 config.ini 文件自定义端口、模式和认证信息等。 - **版本检测**:自动检查软件更新,保证版本最新。 @@ -64,8 +68,8 @@ ## 开发计划 -- [ ] 增加详细日志记录,记录所有连接 ProxyCat 的 IP 身份,支持多用户。 -- [ ] 增加Web UI,提供更加强大易用的界面。 +- [x] 增加详细日志记录,记录所有连接 ProxyCat 的 IP 身份,支持多用户。 +- [x] 增加Web UI,提供更加强大易用的界面。 - [ ] 开发 babycat 模块,可将 babycat 在任意服务器或主机上运行,即可变成一台代理服务器。 - [ ] 增加请求的黑白名单,可以指定某些URL、IP或域名强制丢弃的黑名单和不经过代理的白名单。 - [ ] 打包到 PyPi ,方便直接拉取使用。 diff --git a/app.py b/app.py new file mode 100644 index 0000000..57d30aa --- /dev/null +++ b/app.py @@ -0,0 +1,652 @@ +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'

(ProxyCat-V\d+\.\d+\.\d+)

', 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) \ No newline at end of file diff --git a/config/config.ini b/config/config.ini index 88185ab..82e39d7 100644 --- a/config/config.ini +++ b/config/config.ini @@ -1,64 +1,21 @@ -[SETTINGS] -# 本地服务器监听端口(默认为:1080) -# Local server listening port (default:1080) +[Server] +display_level = 1 port = 1080 - -# 代理地址轮换模式:cycle 表示循环使用,custom 表示使用自定义模式,load_balance 表示负载均衡(默认为:cycle) -# Proxy rotation mode: cycle means cyclic use, custom means custom mode, load_balance means load balancing (default:cycle) +web_port = 5000 mode = cycle - -# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300) -# Proxy address rotation interval (seconds), when set to 0, IP changes with each request (default:300) interval = 300 - -# 本地服务器端口认证用户名((默认为:neko)当为空时不需要认证 -# Local server authentication username (default:neko), no authentication required when empty -username = neko - -# 本地服务器端口认证密码(默认为:123456)当为空时不需要认证 -# Local server authentication password (default:123456), no authentication required when empty -password = 123456 - -# 是否使用 getip 模块获取代理地址 True or False(默认为:False) -# Whether to use getip module to obtain proxy addresses True or False (default:False) use_getip = False - -# 获取新代理地址的URL -# URL to get new proxy address getip_url = http://example.com/getip - -# 代理服务器认证用户名(如果代理服务器需要认证) -# Proxy server authentication username (if proxy server requires authentication) proxy_username = - -# 代理服务器认证密码(如果代理服务器需要认证) -# Proxy server authentication password (if proxy server requires authentication) proxy_password = - -# 代理地址列表文件(默认为:ip.txt) -# Proxy address list file (default:ip.txt) proxy_file = ip.txt - -# 是否启用代理检测功能 True or False(默认为True) -# Whether to enable proxy detection feature True or False (default:True) check_proxies = True - -# 语言设置 (cn/en) -# Language setting (cn/en) language = cn - -# IP白名单文件路径(留空则不启用白名单) -# IP whitelist file path (leave empty to disable whitelist) whitelist_file = whitelist.txt - -# IP黑名单文件路径(留空则不启用黑名单) -# IP blacklist file path (leave empty to disable blacklist) blacklist_file = blacklist.txt - -# IP认证优先级(whitelist/blacklist) -# IP authentication priority (whitelist/blacklist) -# whitelist: 优先判断白名单,在白名单中的IP直接放行 -# whitelist: prioritize whitelist check, IPs in whitelist are allowed directly -# blacklist: 优先判断黑名单,在黑名单中的IP直接拒绝 -# blacklist: prioritize blacklist check, IPs in blacklist are rejected directly ip_auth_priority = whitelist +token = honmashironeko + +[Users] +neko = 123456 + diff --git a/docker-compose.yml b/docker-compose.yml index 3e125af..69c805a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,10 @@ version: '3' services: - app: + proxycat: build: . - container_name: proxycat ports: - "1080:1080" + - "5000:5000" volumes: - ./config:/app/config restart: unless-stopped diff --git a/logs/proxycat.log b/logs/proxycat.log new file mode 100644 index 0000000..e69de29 diff --git a/modules/modules.py b/modules/modules.py index 219f06c..287d7c5 100644 --- a/modules/modules.py +++ b/modules/modules.py @@ -1,7 +1,20 @@ import asyncio, logging, random, httpx, re, os, time from configparser import ConfigParser from packaging import version -from colorama import Fore +from colorama import Fore, Style + +class ColoredFormatter(logging.Formatter): + COLORS = { + logging.INFO: Fore.GREEN, + logging.WARNING: Fore.YELLOW, + logging.ERROR: Fore.RED, + logging.CRITICAL: Fore.RED + Style.BRIGHT, + } + + def format(self, record): + log_color = self.COLORS.get(record.levelno, Fore.WHITE) + record.msg = f"{log_color}{record.msg}{Style.RESET_ALL}" + return super().format(record) MESSAGES = { 'cn': { @@ -45,30 +58,26 @@ MESSAGES = { 'local_http': '本地监听地址 (HTTP)', 'local_socks5': '本地监听地址 (SOCKS5)', 'star_project': '开源项目求 Star', - 'client_request_error': '客户端请求错误: {}', 'client_handle_error': '客户端处理错误: {}', 'proxy_invalid_switch': '代理无效,切换代理', 'request_fail_retry': '请求失败,重试剩余次数: {}', - 'request_error': '请求错误: {}', 'user_interrupt': '用户中断程序', 'new_version_found': '发现新版本!', - 'visit_quark': '请访问 https://pan.quark.cn/s/39b4b5674570 获取最新版本。', - 'visit_github': '请访问 https://github.com/honmashironeko/ProxyCat 获取最新版本。', - 'visit_baidu': '请访问 https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5 获取最新版本。', - 'latest_version': '当前版本已是最新', - 'version_info_not_found': '无法在响应中找到版本信息', - 'update_check_error': '检查更新时发生错误: {}', + 'visit_quark': '夸克网盘: https://pan.quark.cn/s/39b4b5674570', + 'visit_github': 'GitHub: https://github.com/honmashironeko/ProxyCat', + 'visit_baidu': '百度网盘: https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5', + 'latest_version': '当前已是最新版本', + 'version_info_not_found': '未找到版本信息', + 'update_check_error': '检查更新失败: {}', 'unauthorized_ip': '未授权的IP尝试访问: {}', 'client_cancelled': '客户端连接已取消', 'socks5_connection_error': 'SOCKS5连接错误: {}', 'connect_timeout': '连接超时', 'connection_reset': '连接被重置', 'transfer_cancelled': '传输已取消', - 'client_request_error': '客户端请求处理错误: {}', + 'client_request_error': '客户端请求错误: {}', 'unsupported_protocol': '不支持的协议: {}', - 'proxy_invalid_switch': '代理无效,正在切换', 'request_retry': '请求失败,重试中 (剩余{}次)', - 'request_error': '请求过程中出错: {}', 'response_write_error': '写入响应时出错: {}', 'consecutive_failures': '检测到连续代理失败: {}', 'invalid_proxy': '当前代理无效: {}', @@ -83,6 +92,101 @@ MESSAGES = { 'proxy_forward_error': '代理转发错误: {}', 'data_transfer_timeout': '{}数据传输超时', 'data_transfer_error': '{}数据传输错误: {}', + 'status_update_error': '状态更新出错', + 'display_level_notice': '当前显示级别: {}', + 'display_level_desc': '''显示级别说明: +0: 仅显示代理切换和错误信息 +1: 显示代理切换、倒计时和错误信息 +2: 显示所有详细信息''', + 'new_client_connect': '新客户端连接 - IP: {}, 用户: {}', + 'no_auth': '无认证', + 'proxy_changed': '代理变更: {} -> {}', + 'connection_error': '连接处理错误: {}', + 'cleanup_error': '清理IP错误: {}', + 'port_changed': '端口已更改: {} -> {},需要重启服务器生效', + 'config_updated': '服务器配置已更新', + 'load_proxy_file_error': '加载代理文件失败: {}', + 'manual_switch': '手动切换代理: {} -> {}', + 'auto_switch': '自动切换代理: {} -> {}', + 'proxy_check_result': '代理检查完成,有效代理:{}个', + 'no_proxy': '无代理', + 'cycle_mode': '循环模式', + 'load_balance_mode': '负载均衡模式', + 'proxy_check_start': '开始检查代理...', + 'proxy_check_complete': '代理检查完成', + 'proxy_save_success': '代理保存成功', + 'proxy_save_failed': '代理保存失败: {}', + 'ip_list_save_success': 'IP名单保存成功', + 'ip_list_save_failed': 'IP名单保存失败: {}', + 'switch_success': '代理切换成功', + 'switch_failed': '代理切换失败: {}', + 'service_start_success': '服务启动成功', + 'service_start_failed': '服务启动失败', + 'service_already_running': '服务已在运行', + 'service_stop_success': '服务停止成功', + 'service_not_running': '服务未在运行', + 'service_restart_success': '服务重启成功', + 'service_restart_failed': '服务重启失败', + 'invalid_action': '无效的操作', + 'operation_failed': '操作失败: {}', + 'logs_cleared': '日志已清除', + 'clear_logs_failed': '清除日志失败: {}', + 'unsupported_language': '不支持的语言', + 'language_changed': '语言已切换为{}', + 'loading': '加载中...', + 'get_proxy_failed': '获取新代理失败: {}', + 'log_level_all': '全部', + 'log_level_info': '信息', + 'log_level_warning': '警告', + 'log_level_error': '错误', + 'log_level_critical': '严重错误', + 'confirm_clear_logs': '确定要清除所有日志吗?此操作不可恢复。', + 'language_label': '语言', + 'chinese': '中文', + 'english': 'English', + 'manual_switch_btn': '手动切换', + 'service_control_title': '服务控制', + 'language_switch_success': '', + 'language_switch_failed': '', + 'refresh_failed': '刷新数据失败: {}', + 'auth_username_label': '认证用户名', + 'auth_password_label': '认证密码', + 'proxy_auth_username_label': '代理认证用户名', + 'proxy_auth_password_label': '代理认证密码', + 'progress_bar_label': '切换进度', + 'proxy_settings_title': '代理设置', + 'config_save_success': '配置保存成功', + 'config_save_failed': '配置保存失败:{}', + 'config_restart_required': '配置已更改,需要重启服务器生效', + 'confirm_restart_service': '是否立即重启服务器?', + 'service_status': '服务状态', + 'running': '运行中', + 'stopped': '已停止', + 'restarting': '重启中', + 'unknown': '未知', + 'service_start_failed': '服务启动失败:{}', + 'service_stop_failed': '服务停止失败:{}', + 'service_restart_failed': '服务重启失败:{}', + 'invalid_token': '无效的访问令牌', + 'config_file_changed': '检测到配置文件更改,正在重新加载...', + 'proxy_file_changed': '代理文件已更改,正在重新加载...', + 'test_target_label': '测试目标地址', + 'invalid_test_target': '无效的测试目标地址', + 'users_save_success': '用户保存成功', + 'users_save_failed': '用户保存失败:{}', + 'user_management_title': '用户管理', + 'username_column': '用户名', + 'password_column': '密码', + 'actions_column': '操作', + 'add_user_btn': '添加用户', + 'enter_username': '请输入用户名', + 'enter_password': '请输入密码', + 'confirm_delete_user': '确定要删除该用户吗?', + 'no_logs_found': '未找到匹配的日志', + 'clear_search': '清除搜索', + 'web_panel_url': '网页控制面板地址: {}', + 'web_panel_notice': '请使用浏览器访问上述地址来管理代理服务器', + 'api_proxy_settings_title': 'API代理设置', }, 'en': { 'getting_new_proxy': 'Getting new proxy IP', @@ -125,19 +229,17 @@ MESSAGES = { 'local_http': 'Local Listening Address (HTTP)', 'local_socks5': 'Local Listening Address (SOCKS5)', 'star_project': 'Star the Project', - 'client_request_error': 'Client request error: {}', 'client_handle_error': 'Client handling error: {}', 'proxy_invalid_switch': 'Proxy invalid, switching proxy', 'request_fail_retry': 'Request failed, retrying remaining times: {}', - 'request_error': 'Request error: {}', 'user_interrupt': 'User interrupted the program', - 'new_version_found': 'New version found!', - 'visit_quark': 'Please visit https://pan.quark.cn/s/39b4b5674570 to get the latest version.', - 'visit_github': 'Please visit https://github.com/honmashironeko/ProxyCat to get the latest version.', - 'visit_baidu': 'Please visit https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5 to get the latest version.', + 'new_version_found': 'New version available!', + 'visit_quark': 'Quark Drive: https://pan.quark.cn/s/39b4b5674570', + 'visit_github': 'GitHub: https://github.com/honmashironeko/ProxyCat', + 'visit_baidu': 'Baidu Drive: https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5', 'latest_version': 'You are using the latest version', - 'version_info_not_found': 'Version information not found in the response', - 'update_check_error': 'Error occurred while checking for updates: {}', + 'version_info_not_found': 'Version information not found', + 'update_check_error': 'Failed to check for updates: {}', 'unauthorized_ip': 'Unauthorized IP attempt: {}', 'client_cancelled': 'Client connection cancelled', 'socks5_connection_error': 'SOCKS5 connection error: {}', @@ -147,7 +249,6 @@ MESSAGES = { 'data_transfer_error': 'Data transfer error: {}', 'client_request_error': 'Client request handling error: {}', 'unsupported_protocol': 'Unsupported protocol: {}', - 'proxy_invalid_switch': 'Proxy invalid, switching', 'request_retry': 'Request failed, retrying ({} left)', 'request_error': 'Error during request: {}', 'response_write_error': 'Error writing response: {}', @@ -164,6 +265,101 @@ MESSAGES = { 'proxy_forward_error': 'Proxy forwarding error: {}', 'data_transfer_timeout': '{} data transfer timeout', 'data_transfer_error': '{} data transfer error: {}', + 'status_update_error': 'Status update error', + 'display_level_notice': 'Current display level: {}', + 'display_level_desc': '''Display level description: +0: Show only proxy switches and errors +1: Show proxy switches, countdown and errors +2: Show all detailed information''', + 'new_client_connect': 'New client connection - IP: {}, User: {}', + 'no_auth': 'No authentication', + 'proxy_changed': 'Proxy changed: {} -> {}', + 'connection_error': 'Connection handling error: {}', + 'cleanup_error': 'IP cleanup error: {}', + 'port_changed': 'Port changed: {} -> {}, server restart required', + 'config_updated': 'Server configuration updated', + 'load_proxy_file_error': 'Failed to load proxy file: {}', + 'manual_switch': 'Manual proxy switch: {} -> {}', + 'auto_switch': 'Auto switch proxy: {} -> {}', + 'proxy_check_result': 'Proxy check completed, valid proxies: {}', + 'no_proxy': 'No proxy', + 'cycle_mode': 'Cycle Mode', + 'load_balance_mode': 'Load Balance Mode', + 'proxy_check_start': 'Starting proxy check...', + 'proxy_check_complete': 'Proxy check completed', + 'proxy_save_success': 'Proxies saved successfully', + 'proxy_save_failed': 'Failed to save proxies: {}', + 'ip_list_save_success': 'IP lists saved successfully', + 'ip_list_save_failed': 'Failed to save IP lists: {}', + 'switch_success': 'Proxy switched successfully', + 'switch_failed': 'Failed to switch proxy: {}', + 'service_start_success': 'Service started successfully', + 'service_start_failed': 'Failed to start service', + 'service_already_running': 'Service is already running', + 'service_stop_success': 'Service stopped successfully', + 'service_not_running': 'Service is not running', + 'service_restart_success': 'Service restarted successfully', + 'service_restart_failed': 'Failed to restart service', + 'invalid_action': 'Invalid action', + 'operation_failed': 'Operation failed: {}', + 'logs_cleared': 'Logs cleared', + 'clear_logs_failed': 'Failed to clear logs: {}', + 'unsupported_language': 'Unsupported language', + 'language_changed': 'Language changed to {}', + 'loading': 'Loading...', + 'get_proxy_failed': 'Failed to get new proxy: {}', + 'log_level_all': 'All', + 'log_level_info': 'Info', + 'log_level_warning': 'Warning', + 'log_level_error': 'Error', + 'log_level_critical': 'Critical', + 'confirm_clear_logs': 'Are you sure you want to clear all logs? This action cannot be undone.', + 'language_label': 'Language', + 'chinese': 'Chinese', + 'english': 'English', + 'manual_switch_btn': 'Manual Switch', + 'service_control_title': 'Service Control', + 'language_switch_success': '', + 'language_switch_failed': '', + 'refresh_failed': 'Failed to refresh data: {}', + 'auth_username_label': 'Auth Username', + 'auth_password_label': 'Auth Password', + 'proxy_auth_username_label': 'Proxy Auth Username', + 'proxy_auth_password_label': 'Proxy Auth Password', + 'progress_bar_label': 'Switch Progress', + 'proxy_settings_title': 'Proxy Settings', + 'config_save_success': 'Configuration saved successfully', + 'config_save_failed': 'Failed to save configuration: {}', + 'config_restart_required': 'Configuration changed, server restart required', + 'confirm_restart_service': 'Restart server now?', + 'service_status': 'Service Status', + 'running': 'Running', + 'stopped': 'Stopped', + 'restarting': 'Restarting', + 'unknown': 'Unknown', + 'service_start_failed': 'Failed to start service: {}', + 'service_stop_failed': 'Failed to stop service: {}', + 'service_restart_failed': 'Failed to restart service: {}', + 'invalid_token': 'Invalid access token', + 'config_file_changed': 'Configuration file change detected, reloading...', + 'proxy_file_changed': 'Proxy file changed, reloading...', + 'test_target_label': 'Test Target URL', + 'invalid_test_target': 'Invalid test target URL', + 'users_save_success': 'Users saved successfully', + 'users_save_failed': 'Failed to save users: {}', + 'user_management_title': 'User Management', + 'username_column': 'Username', + 'password_column': 'Password', + 'actions_column': 'Actions', + 'add_user_btn': 'Add User', + 'enter_username': 'Enter username', + 'enter_password': 'Enter password', + 'confirm_delete_user': 'Are you sure you want to delete this user?', + 'no_logs_found': 'No matching logs found', + 'clear_search': 'Clear Search', + 'web_panel_url': 'Web control panel URL: {}', + 'web_panel_notice': 'Please use a browser to visit the above URL to manage the proxy server', + 'api_proxy_settings_title': 'API Proxy Settings', } } @@ -201,9 +397,15 @@ def print_banner(config): ] print(f"{Fore.MAGENTA}{'=' * 55}") for key, value in banner_info: - print(f"{Fore.YELLOW}{key}: {Fore.GREEN}{value}") + print(f"{Fore.YELLOW}{key}: {Fore.GREEN}{value}{Style.RESET_ALL}") print(f"{Fore.MAGENTA}{'=' * 55}\n") + display_level = config.get('display_level', '1') + if int(display_level) >= 2: + print(f"\n{Fore.CYAN}{get_message('display_level_desc', language)}{Style.RESET_ALL}") + else: + print(f"\n{Fore.CYAN}{get_message('display_level_notice', language).format(display_level)}{Style.RESET_ALL}") + logo1 = r""" |\ _,,,---,,_ by 本间白猫 ZZZzz /,`.-'`' -. ;-;;,_ @@ -281,14 +483,17 @@ def load_config(config_file='config/config.ini'): config.read(config_file, encoding='utf-8') settings = {} - if config.has_section('SETTINGS'): - settings.update(dict(config.items('SETTINGS'))) - + if config.has_section('Server'): + settings.update(dict(config.items('Server'))) + + config_dir = os.path.dirname(config_file) for key in ['proxy_file', 'whitelist_file', 'blacklist_file']: if key in settings and settings[key]: - config_dir = os.path.dirname(config_file) settings[key] = os.path.join(config_dir, settings[key]) - + + if config.has_section('DEFAULT'): + settings.update(dict(config.items('DEFAULT'))) + return {**DEFAULT_CONFIG, **settings} def load_ip_list(file_path): @@ -316,32 +521,7 @@ def parse_proxy(proxy): except Exception: return None, None, None, None -async def check_proxy(proxy): - current_time = time.time() - if proxy in _proxy_check_cache: - cache_time, is_valid = _proxy_check_cache[proxy] - if current_time - cache_time < _proxy_check_ttl: - return is_valid - - proxy_type = proxy.split('://')[0] - check_funcs = { - 'http': check_http_proxy, - 'https': check_https_proxy, - 'socks5': check_socks_proxy - } - - if proxy_type not in check_funcs: - return False - - try: - is_valid = await check_funcs[proxy_type](proxy) - _proxy_check_cache[proxy] = (current_time, is_valid) - return is_valid - except Exception: - _proxy_check_cache[proxy] = (current_time, False) - return False - -async def check_http_proxy(proxy): +async def check_http_proxy(proxy, test_url='https://www.baidu.com'): protocol, auth, host, port = parse_proxy(proxy) proxies = {} if auth: @@ -354,18 +534,21 @@ async def check_http_proxy(proxy): try: async with httpx.AsyncClient(proxies=proxies, timeout=10, verify=False) as client: try: - response = await client.get('https://www.baidu.com') + response = await client.get(test_url) return response.status_code == 200 except: - response = await client.get('http://www.baidu.com') - return response.status_code == 200 + if test_url.startswith('https://'): + http_url = 'http://' + test_url[8:] + response = await client.get(http_url) + return response.status_code == 200 + return False except: return False -async def check_https_proxy(proxy): - return await check_http_proxy(proxy) +async def check_https_proxy(proxy, test_url='https://www.baidu.com'): + return await check_http_proxy(proxy, test_url) -async def check_socks_proxy(proxy): +async def check_socks_proxy(proxy, test_url='www.baidu.com'): protocol, auth, host, port = parse_proxy(proxy) if not all([host, port]): return False @@ -394,7 +577,10 @@ async def check_socks_proxy(proxy): if auth_response[1] != 0x00: return False - domain = b"www.baidu.com" + from urllib.parse import urlparse + domain = urlparse(test_url).netloc if '://' in test_url else test_url + domain = domain.encode() + writer.write(b'\x05\x01\x00\x03' + bytes([len(domain)]) + domain + b'\x00\x50') await writer.drain() @@ -410,10 +596,38 @@ async def check_socks_proxy(proxy): except Exception: return False -async def check_proxies(proxies): +async def check_proxy(proxy, test_url=None): + current_time = time.time() + cache_key = f"{proxy}:{test_url}" + + if cache_key in _proxy_check_cache: + cache_time, is_valid = _proxy_check_cache[cache_key] + if current_time - cache_time < _proxy_check_ttl: + return is_valid + + proxy_type = proxy.split('://')[0] + check_funcs = { + 'http': check_http_proxy, + 'https': check_https_proxy, + 'socks5': check_socks_proxy + } + + if proxy_type not in check_funcs: + return False + + try: + test_url = test_url or 'https://www.baidu.com' + is_valid = await check_funcs[proxy_type](proxy, test_url) + _proxy_check_cache[cache_key] = (current_time, is_valid) + return is_valid + except Exception: + _proxy_check_cache[cache_key] = (current_time, False) + return False + +async def check_proxies(proxies, test_url=None): valid_proxies = [] for proxy in proxies: - if await check_proxy(proxy): + if await check_proxy(proxy, test_url): valid_proxies.append(proxy) return valid_proxies @@ -426,15 +640,15 @@ async def check_for_updates(language='cn'): match = re.search(r'

(ProxyCat-V\d+\.\d+\.\d+)

', content) if match: latest_version = match.group(1) - CURRENT_VERSION = "ProxyCat-V1.9.5" + CURRENT_VERSION = "ProxyCat-V2.0.0" if version.parse(latest_version.split('-V')[1]) > version.parse(CURRENT_VERSION.split('-V')[1]): - print(f"{Fore.YELLOW}{get_message('new_version_found', language)} 当前版本: {CURRENT_VERSION}, 最新版本: {latest_version}") - print(f"{Fore.YELLOW}{get_message('visit_quark', language)}") - print(f"{Fore.YELLOW}{get_message('visit_github', language)}") - print(f"{Fore.YELLOW}{get_message('visit_baidu', language)}") + print(f"{Fore.YELLOW}{get_message('new_version_found', language)} 当前版本: {CURRENT_VERSION}, 最新版本: {latest_version}{Style.RESET_ALL}") + print(f"{Fore.YELLOW}{get_message('visit_quark', language)}{Style.RESET_ALL}") + print(f"{Fore.YELLOW}{get_message('visit_github', language)}{Style.RESET_ALL}") + print(f"{Fore.YELLOW}{get_message('visit_baidu', language)}{Style.RESET_ALL}") else: - print(f"{Fore.GREEN}{get_message('latest_version', language)} ({CURRENT_VERSION})") + print(f"{Fore.GREEN}{get_message('latest_version', language)} ({CURRENT_VERSION}){Style.RESET_ALL}") else: - print(f"{Fore.RED}{get_message('version_info_not_found', language)}") + print(f"{Fore.RED}{get_message('version_info_not_found', language)}{Style.RESET_ALL}") except Exception as e: - print(f"{Fore.RED}{get_message('update_check_error', language, e)}") \ No newline at end of file + print(f"{Fore.RED}{get_message('update_check_error', language, e)}{Style.RESET_ALL}") \ No newline at end of file diff --git a/modules/proxyserver.py b/modules/proxyserver.py index fcba95d..55cc059 100644 --- a/modules/proxyserver.py +++ b/modules/proxyserver.py @@ -3,6 +3,7 @@ from modules.modules import get_message, load_ip_list from asyncio import TimeoutError from itertools import cycle from config import getip +from configparser import ConfigParser def load_proxies(file_path='ip.txt'): @@ -21,17 +22,27 @@ def validate_proxy(proxy): class AsyncProxyServer: def __init__(self, config): self.config = config - self.username = self.config['username'].strip() - self.password = self.config['password'].strip() - self.auth_required = bool(self.username and self.password) - self.mode = self.config['mode'] - self.interval = int(self.config['interval']) + self.username = self.config.get('username', '').strip() + self.password = self.config.get('password', '').strip() + self.mode = self.config.get('mode', 'cycle') + self.interval = int(self.config.get('interval', '300')) self.use_getip = self.config.get('use_getip', 'False').lower() == 'true' - self.proxy_file = self.config['proxy_file'] + self.proxy_file = self.config.get('proxy_file', 'ip.txt') self.language = self.config.get('language', 'cn').lower() - self.whitelist = load_ip_list(config.get('whitelist_file', '')) - self.blacklist = load_ip_list(config.get('blacklist_file', '')) - self.ip_auth_priority = config.get('ip_auth_priority', 'whitelist') + + config_parser = ConfigParser() + config_parser.read('config/config.ini', encoding='utf-8') + + self.users = {} + if config_parser.has_section('Users'): + self.users = dict(config_parser.items('Users')) + self.auth_required = bool(self.users) + + self.whitelist_file = self.config.get('whitelist_file', '') + self.blacklist_file = self.config.get('blacklist_file', '') + self.whitelist = load_ip_list(self.whitelist_file) + self.blacklist = load_ip_list(self.blacklist_file) + self.ip_auth_priority = self.config.get('ip_auth_priority', 'whitelist') if not self.use_getip: self.proxies = self._load_file_proxies() @@ -64,6 +75,57 @@ class AsyncProxyServer: self.request_semaphore = asyncio.Semaphore(self.max_concurrent_requests) self.connection_pool = {} self.pipeline_enabled = True + self.connected_clients = set() + self.known_clients = set() + self.last_proxy = None + + self.running = False + self.stop_server = False + self.server_instance = None + self.tasks = set() + + async def start(self): + if not self.running: + self.stop_server = False + self.running = True + try: + self.server_instance = await asyncio.start_server( + self.handle_client, + '0.0.0.0', + int(self.config.get('port', '1080')) + ) + logging.info(get_message('server_running', self.language, '0.0.0.0', self.config.get('port', '1080'))) + + async with self.server_instance: + await self.server_instance.serve_forever() + except Exception as e: + if not self.stop_server: + logging.error(get_message('server_start_error', self.language, str(e))) + finally: + self.running = False + self.server_instance = None + + async def stop(self): + if self.running: + self.stop_server = True + if self.server_instance: + self.server_instance.close() + await self.server_instance.wait_closed() + self.server_instance = None + + for task in self.tasks: + task.cancel() + if self.tasks: + await asyncio.gather(*self.tasks, return_exceptions=True) + self.tasks.clear() + + self.running = False + logging.info(get_message('server_shutting_down', self.language)) + + async def restart(self): + await self.stop() + await asyncio.sleep(1) + await self.start() async def get_next_proxy(self): if self.mode == 'load_balance': @@ -110,7 +172,6 @@ class AsyncProxyServer: else: self.current_proxy = next(self.proxy_cycle) self.last_switch_time = time.time() - logging.info(get_message('proxy_switch', self.language, self.current_proxy)) async def custom_proxy_switch(self): return self.proxies[0] if self.proxies else "No proxies available" @@ -138,30 +199,62 @@ class AsyncProxyServer: return not self.blacklist async def handle_client(self, reader, writer): - async with self.semaphore: - try: - client_ip = writer.get_extra_info('peername')[0] - if not self.check_ip_auth(client_ip): - logging.warning(get_message('unauthorized_ip', self.language, client_ip)) + task = asyncio.current_task() + self.tasks.add(task) + try: + peername = writer.get_extra_info('peername') + if not peername: + return + + client_ip = peername[0] + + if client_ip not in self.known_clients: + self.known_clients.add(client_ip) + auth_info = f"{self.username}:{self.password}" if self.auth_required else get_message('no_auth', self.language) + logging.info(get_message('new_client_connect', self.language, client_ip, auth_info)) + + self.connected_clients.add(client_ip) + + if self.current_proxy != self.last_proxy: + self.last_proxy = self.current_proxy + logging.info(get_message('proxy_switch', self.language, self.current_proxy)) + + async with self.semaphore: + try: + if not self.check_ip_auth(client_ip): + logging.warning(get_message('unauthorized_ip', self.language, client_ip)) + writer.close() + await writer.wait_closed() + return + + first_byte = await reader.read(1) + if not first_byte: + return + + if (first_byte == b'\x05'): + await self.handle_socks5_connection(reader, writer) + else: + await self._handle_client_impl(reader, writer, first_byte) + except asyncio.CancelledError: + logging.info(get_message('client_cancelled', self.language)) + except Exception as e: + logging.error(get_message('client_error', self.language, e)) + finally: writer.close() await writer.wait_closed() - return - - first_byte = await reader.read(1) - if not first_byte: - return - - if (first_byte == b'\x05'): - await self.handle_socks5_connection(reader, writer) - else: - await self._handle_client_impl(reader, writer, first_byte) - except asyncio.CancelledError: - logging.info(get_message('client_cancelled', self.language)) - except Exception as e: - logging.error(get_message('client_error', self.language, e)) - finally: + if client_ip in self.connected_clients: + self.connected_clients.discard(client_ip) + except Exception as e: + logging.error(get_message('connection_error', self.language, str(e))) + finally: + self.tasks.discard(task) + try: writer.close() await writer.wait_closed() + if client_ip in self.connected_clients: + self.connected_clients.discard(client_ip) + except: + pass async def handle_socks5_connection(self, reader, writer): try: @@ -383,18 +476,20 @@ class AsyncProxyServer: logging.error(get_message('client_request_error', self.language, e)) def _authenticate(self, headers): - if not self.auth_required: + if not self.users: return True auth = headers.get('proxy-authorization') if not auth: return False + try: scheme, credentials = auth.split() if scheme.lower() != 'basic': return False + username, password = base64.b64decode(credentials).decode().split(':') - return username == self.username and password == self.password + return username in self.users and self.users[username] == password except: return False @@ -540,52 +635,6 @@ class AsyncProxyServer: verify=False ) - async def _handle_request(self, method, path, headers, reader, writer): - async with self.request_semaphore: - try: - proxy = await self.get_next_proxy() - key = f"{proxy}:{path}" - - if key in self.connection_pool: - client = self.connection_pool[key] - else: - client = await self._create_client(proxy) - self.connection_pool[key] = client - - async with client.stream( - method, - path, - headers=headers, - content=reader, - ) as response: - writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode()) - - async for chunk in response.aiter_bytes(chunk_size=self.buffer_size): - writer.write(chunk) - if len(chunk) >= self.buffer_size: - await writer.drain() - - await writer.drain() - - except Exception as e: - logging.error(get_message('request_handling_error', self.language, e)) - writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n') - await writer.drain() - finally: - await self._cleanup_connections() - - async def _create_client(self, proxy): - return httpx.AsyncClient( - proxies={"all://": proxy}, - limits=httpx.Limits( - max_keepalive_connections=100, - max_connections=1000, - keepalive_expiry=30 - ), - timeout=30.0, - http2=True, - ) - async def _cleanup_connections(self): current_time = time.time() expired_keys = [ @@ -627,9 +676,6 @@ class AsyncProxyServer: self.proxy_check_cache[proxy] = (current_time, False) return False - finally: - self._clean_proxy_cache() - def _clean_proxy_cache(self): current_time = time.time() self.proxy_check_cache = { @@ -671,65 +717,53 @@ class AsyncProxyServer: old_proxy = self.current_proxy await self.get_proxy() self.last_switch_time = current_time - logging.info(get_message('proxy_switched', self.language, old_proxy, self.current_proxy)) + + if self.connected_clients: + clients = ", ".join(sorted(self.connected_clients)) + logging.info(get_message('proxy_switch_clients', self.language, clients)) + logging.info(get_message('proxy_changed', self.language, old_proxy, self.current_proxy)) + self.proxy_failed = False finally: self.switching_proxy = False async def check_proxy(self, proxy): - current_time = time.time() - if proxy in self.proxy_cache: - cache_time, is_valid = self.proxy_cache[proxy] - if current_time - cache_time < self.proxy_cache_ttl: - return is_valid - + cache_key = f"{proxy}_{time.time() // self.proxy_cache_ttl}" + if cache_key in self.proxy_cache: + return self.proxy_cache[cache_key] + is_valid = await self._check_proxy_impl(proxy) - self.proxy_cache[proxy] = (current_time, is_valid) + self.proxy_cache[cache_key] = is_valid return is_valid - async def handle_request(self, client_reader, client_writer, target_host, target_port): - for retry in range(self.retry_count): - try: - target_reader, target_writer = await asyncio.wait_for( - asyncio.open_connection(target_host, target_port), - timeout=self.timeout - ) - forward_task = asyncio.create_task( - self.forward_data(client_reader, target_writer, "客户端 -> 目标") - ) - backward_task = asyncio.create_task( - self.forward_data(target_reader, client_writer, "目标 -> 客户端") - ) - await asyncio.gather(forward_task, backward_task) - break - - except TimeoutError: - print(f"连接超时,重试 {retry + 1}/{self.retry_count}") - continue - except Exception as e: - logging.error(get_message('proxy_forward_error', self.language, e)) - if retry == self.retry_count - 1: - raise - continue - finally: + def initialize_proxies(self): + if self.mode == 'cycle': + if hasattr(self, 'proxies') and self.proxies: + self.proxy_cycle = cycle(self.proxies) + elif self.use_getip: + pass + else: try: - target_writer.close() - await target_writer.wait_closed() - except: - pass + with open(self.proxy_file, 'r') as f: + self.proxies = [line.strip() for line in f if line.strip()] + if self.proxies: + self.proxy_cycle = cycle(self.proxies) + except Exception as e: + logging.error(get_message('load_proxy_file_error', self.language, str(e))) - async def forward_data(self, reader, writer, direction): - try: - while True: - data = await asyncio.wait_for(reader.read(8192), timeout=self.timeout) - if not data: - break - writer.write(data) - await writer.drain() - except TimeoutError: - logging.error(get_message('data_transfer_timeout', self.language, direction)) - except Exception as e: - logging.error(get_message('data_transfer_error', self.language, direction, e)) + async def cleanup_disconnected_ips(self): + while True: + try: + active_ips = set() + for client_info in self.get_active_connections(): + active_ips.add(client_info[0]) + + self.connected_clients = active_ips + + except Exception as e: + logging.error(get_message('cleanup_error', self.language, str(e))) + + await asyncio.sleep(30) def is_docker(): return os.path.exists('/.dockerenv') @@ -753,12 +787,3 @@ class AsyncProxyServer: logging.error(error_messages.get(error_type, str(details))) if error_type in ['timeout', 'invalid']: await self.handle_proxy_failure() - - async def check_proxy(self, proxy): - cache_key = f"{proxy}_{time.time() // self.proxy_cache_ttl}" - if cache_key in self.proxy_cache: - return self.proxy_cache[cache_key] - - is_valid = await self._check_proxy_impl(proxy) - self.proxy_cache[cache_key] = is_valid - return is_valid diff --git a/requirements.txt b/requirements.txt index 5dd2edc..90031ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ colorama==0.4.6 httpx==0.27.2 packaging==24.1 -Requests==2.32.3 \ No newline at end of file +Requests==2.32.3 +tqdm>=4.65.0 +flask>=2.0.1 \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..3346b6a --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,1679 @@ + + + + + + ProxyCat 控制面板 + + + + + +
+
+
+

ProxyCat 控制面板

+
+
+ + +
+
+
+
+
当前代理
+

加载中...

+ +
+
+
+
+
+
+
运行模式
+

加载中...

+
+
+
+
+
+
+
代理总数
+

加载中...

+
+
+
+
+
+
+
下次切换
+

加载中...

+
+
+
+
+
+
+
+ + +
+
+
服务控制
+
+ + + +
+
+
+ + +
+
+
+
+
+
本地监听地址
+

+ 本地监听地址 (HTTP):
+ 本地监听地址 (SOCKS5): +

+
+
+
关于信息
+

+ 开源项目求 Star: + https://github.com/honmashironeko/ProxyCat
+ 公众号: + 樱花庄的本间白猫 +

+
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+
+
+
+
+ +
基本配置
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
API代理设置
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+
用户管理
+ +
+
+ + + + + + + + + + +
用户名密码操作
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+ + +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+
+
+ + +
+
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+
+
+
+ + +
+ +
+ + + + + + + + + \ No newline at end of file