mirror of
https://github.com/honmashironeko/ProxyCat.git
synced 2025-06-20 09:51:00 +00:00
Update 2.0.0
This commit is contained in:
parent
1ee8daff0d
commit
91ffaa9fcf
13
Dockerfile
13
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"]
|
||||
|
||||
|
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 |
@ -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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 问题Q&A
|
||||
|
||||
Q:为什么运行后我的XXX工具代理还是没换?
|
||||
|
@ -1,3 +1,17 @@
|
||||
### 2025/02/21
|
||||
|
||||
- 增加 Web 管理界面
|
||||
|
||||
- 增加多用户模式
|
||||
|
||||
- 代码结构大改
|
||||
|
||||
- config.in及相关文件动态更新不需要重启
|
||||
|
||||
- 增加日志显示级别控制
|
||||
- 增加记录连接人信息日志,包括连接的IP和使用的账号密码
|
||||
- 以及其他乱七八糟的修改,这次大版本更新改的太多,我有点忘记了~
|
||||
|
||||
### 2025/02/06
|
||||
|
||||
- Docker 安装依赖库采用国内源
|
||||
|
253
ProxyCat.py
253
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='配置文件路径')
|
||||
|
12
README.md
12
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 ,方便直接拉取使用。
|
||||
|
652
app.py
Normal file
652
app.py
Normal 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)
|
@ -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
|
||||
|
||||
|
@ -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
0
logs/proxycat.log
Normal 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}")
|
@ -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
|
||||
|
@ -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
1679
web/templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user