Update 2.0.0

This commit is contained in:
本间白猫 2025-02-21 16:48:20 +08:00
parent 1ee8daff0d
commit 91ffaa9fcf
19 changed files with 3124 additions and 317 deletions

View File

@ -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"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

View File

@ -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工具代理还是没换

View File

@ -1,3 +1,17 @@
### 2025/02/21
- 增加 Web 管理界面
- 增加多用户模式
- 代码结构大改
- config.in及相关文件动态更新不需要重启
- 增加日志显示级别控制
- 增加记录连接人信息日志包括连接的IP和使用的账号密码
- 以及其他乱七八糟的修改,这次大版本更新改的太多,我有点忘记了~
### 2025/02/06
- Docker 安装依赖库采用国内源

View File

@ -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='配置文件路径')

View File

@ -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 ,方便直接拉取使用。

652
app.py Normal file
View File

@ -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'<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)

View File

@ -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

View File

@ -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

0
logs/proxycat.log Normal file
View File

View File

@ -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'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', 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)}")
print(f"{Fore.RED}{get_message('update_check_error', language, e)}{Style.RESET_ALL}")

View File

@ -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

View File

@ -1,4 +1,6 @@
colorama==0.4.6
httpx==0.27.2
packaging==24.1
Requests==2.32.3
Requests==2.32.3
tqdm>=4.65.0
flask>=2.0.1

1679
web/templates/index.html Normal file

File diff suppressed because it is too large Load Diff