mirror of
https://github.com/honmashironeko/ProxyCat.git
synced 2025-06-20 18:01:01 +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
|
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 . .
|
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", "app.py"]
|
||||||
|
|
||||||
CMD ["python", "ProxyCat.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 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&A
|
||||||
|
|
||||||
Q:为什么运行后我的XXX工具代理还是没换?
|
Q:为什么运行后我的XXX工具代理还是没换?
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
|
### 2025/02/21
|
||||||
|
|
||||||
|
- 增加 Web 管理界面
|
||||||
|
|
||||||
|
- 增加多用户模式
|
||||||
|
|
||||||
|
- 代码结构大改
|
||||||
|
|
||||||
|
- config.in及相关文件动态更新不需要重启
|
||||||
|
|
||||||
|
- 增加日志显示级别控制
|
||||||
|
- 增加记录连接人信息日志,包括连接的IP和使用的账号密码
|
||||||
|
- 以及其他乱七八糟的修改,这次大版本更新改的太多,我有点忘记了~
|
||||||
|
|
||||||
### 2025/02/06
|
### 2025/02/06
|
||||||
|
|
||||||
- Docker 安装依赖库采用国内源
|
- 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
|
import threading, argparse, logging, asyncio, time, socket, signal, sys, os
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from modules.proxyserver import AsyncProxyServer
|
from modules.proxyserver import AsyncProxyServer
|
||||||
from colorama import init, Fore, Style
|
from colorama import init, Fore, Style
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
from tqdm import tqdm
|
||||||
|
import base64
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
init(autoreset=True)
|
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'
|
log_format = '%(asctime)s - %(levelname)s - %(message)s'
|
||||||
formatter = ColoredFormatter(log_format)
|
formatter = ColoredFormatter(log_format)
|
||||||
|
|
||||||
@ -29,24 +20,174 @@ console_handler.setFormatter(formatter)
|
|||||||
logging.basicConfig(level=logging.INFO, handlers=[console_handler])
|
logging.basicConfig(level=logging.INFO, handlers=[console_handler])
|
||||||
|
|
||||||
def update_status(server):
|
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:
|
while True:
|
||||||
try:
|
try:
|
||||||
if server.mode == 'load_balance':
|
if os.path.exists(config_file):
|
||||||
status = f"\r{Fore.YELLOW}{get_message('current_proxy', server.language)}: {Fore.GREEN}{server.current_proxy}"
|
current_config_modified_time = os.path.getmtime(config_file)
|
||||||
else:
|
if current_config_modified_time > last_config_modified_time:
|
||||||
time_left = server.time_until_next_switch()
|
logging.info(get_message('config_file_changed', server.language))
|
||||||
if time_left == float('inf'):
|
new_config = load_config(config_file)
|
||||||
status = f"\r{Fore.YELLOW}{get_message('current_proxy', server.language)}: {Fore.GREEN}{server.current_proxy}"
|
reload_server_config(new_config)
|
||||||
else:
|
last_config_modified_time = current_config_modified_time
|
||||||
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)}"
|
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'):
|
if display_level == 0:
|
||||||
logging.info(status)
|
if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy:
|
||||||
else:
|
print_proxy_info()
|
||||||
print(status, end='', flush=True)
|
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:
|
if is_docker:
|
||||||
pass
|
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)
|
time.sleep(1)
|
||||||
|
|
||||||
async def handle_client_wrapper(server, reader, writer, clients):
|
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)
|
clients.remove(task)
|
||||||
|
|
||||||
async def run_server(server):
|
async def run_server(server):
|
||||||
clients = set()
|
|
||||||
server_instance = None
|
|
||||||
try:
|
try:
|
||||||
server_instance = await asyncio.start_server(
|
await server.start()
|
||||||
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()
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logging.info(get_message('server_closing', server.language))
|
logging.info(get_message('server_closing', server.language))
|
||||||
|
except Exception as e:
|
||||||
|
if not server.stop_server:
|
||||||
|
logging.error(f"Server error: {e}")
|
||||||
finally:
|
finally:
|
||||||
if server_instance:
|
await server.stop()
|
||||||
server_instance.close()
|
|
||||||
await server_instance.wait_closed()
|
|
||||||
for client in clients:
|
|
||||||
client.cancel()
|
|
||||||
await asyncio.gather(*clients, return_exceptions=True)
|
|
||||||
|
|
||||||
async def run_proxy_check(server):
|
async def run_proxy_check(server):
|
||||||
if server.config.get('check_proxies', 'False').lower() == 'true':
|
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.SIGINT, self.handle_shutdown)
|
||||||
signal.signal(signal.SIGTERM, 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):
|
async def start_server(self):
|
||||||
try:
|
try:
|
||||||
@ -143,7 +281,16 @@ class ProxyCat:
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
async def handle_client(self, reader, writer):
|
async def handle_client(self, reader, writer):
|
||||||
|
task = asyncio.current_task()
|
||||||
|
self.tasks.add(task)
|
||||||
try:
|
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(
|
await asyncio.get_event_loop().run_in_executor(
|
||||||
self.executor,
|
self.executor,
|
||||||
self.process_client_request,
|
self.process_client_request,
|
||||||
@ -159,6 +306,22 @@ class ProxyCat:
|
|||||||
except:
|
except:
|
||||||
pass
|
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__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description=logos())
|
parser = argparse.ArgumentParser(description=logos())
|
||||||
parser.add_argument('-c', '--config', default='config/config.ini', help='配置文件路径')
|
parser.add_argument('-c', '--config', default='config/config.ini', help='配置文件路径')
|
||||||
|
12
README.md
12
README.md
@ -28,8 +28,8 @@
|
|||||||
|
|
||||||
## 功能特点
|
## 功能特点
|
||||||
|
|
||||||
- **双协议监听**:支持 HTTP/SOCKS5 协议监听,兼容更多工具。
|
- **两种协议监听**:支持 HTTP/SOCKS5 协议监听,兼容更多工具。
|
||||||
- **三种协议代理地址**:支持 HTTP/HTTPS/SOCKS5 代理服务器及身份鉴别,满足不同需求。
|
- **三种代理地址**:支持 HTTP/HTTPS/SOCKS5 代理服务器及身份鉴别。
|
||||||
- **灵活切换模式**:支持顺序、随机及自定义代理选择,优化流量分配。
|
- **灵活切换模式**:支持顺序、随机及自定义代理选择,优化流量分配。
|
||||||
- **动态获取代理**:通过 GetIP 函数即时获取可用代理,支持 API 接口调用。
|
- **动态获取代理**:通过 GetIP 函数即时获取可用代理,支持 API 接口调用。
|
||||||
- **代理保护机制**:在使用 GetIP 方式获取代理时,首次运行不会直接请求获取,将会在收到请求的时候才获取。
|
- **代理保护机制**:在使用 GetIP 方式获取代理时,首次运行不会直接请求获取,将会在收到请求的时候才获取。
|
||||||
@ -38,6 +38,10 @@
|
|||||||
- **失效代理切换**:代理失效后自动验证切换新代理,确保不中断服务。
|
- **失效代理切换**:代理失效后自动验证切换新代理,确保不中断服务。
|
||||||
- **身份认证支持**:支持用户名/密码认证和黑白名单管理,提高安全性。
|
- **身份认证支持**:支持用户名/密码认证和黑白名单管理,提高安全性。
|
||||||
- **实时状态显示**:展示代理状态和切换时间,实时掌握代理动态。
|
- **实时状态显示**:展示代理状态和切换时间,实时掌握代理动态。
|
||||||
|
- **动态更新配置**:无需重启服务,动态检测配置并更新。
|
||||||
|
- **Web UI界面**:提供 Web 管理界面,操作管理更加便捷。
|
||||||
|
- **Docker部署**:Docker 一键部署,Web 统一管理。
|
||||||
|
- **中英文双语**:支持中文英文一键切换。
|
||||||
- **配置灵活**:通过 config.ini 文件自定义端口、模式和认证信息等。
|
- **配置灵活**:通过 config.ini 文件自定义端口、模式和认证信息等。
|
||||||
- **版本检测**:自动检查软件更新,保证版本最新。
|
- **版本检测**:自动检查软件更新,保证版本最新。
|
||||||
|
|
||||||
@ -64,8 +68,8 @@
|
|||||||
|
|
||||||
## 开发计划
|
## 开发计划
|
||||||
|
|
||||||
- [ ] 增加详细日志记录,记录所有连接 ProxyCat 的 IP 身份,支持多用户。
|
- [x] 增加详细日志记录,记录所有连接 ProxyCat 的 IP 身份,支持多用户。
|
||||||
- [ ] 增加Web UI,提供更加强大易用的界面。
|
- [x] 增加Web UI,提供更加强大易用的界面。
|
||||||
- [ ] 开发 babycat 模块,可将 babycat 在任意服务器或主机上运行,即可变成一台代理服务器。
|
- [ ] 开发 babycat 模块,可将 babycat 在任意服务器或主机上运行,即可变成一台代理服务器。
|
||||||
- [ ] 增加请求的黑白名单,可以指定某些URL、IP或域名强制丢弃的黑名单和不经过代理的白名单。
|
- [ ] 增加请求的黑白名单,可以指定某些URL、IP或域名强制丢弃的黑名单和不经过代理的白名单。
|
||||||
- [ ] 打包到 PyPi ,方便直接拉取使用。
|
- [ ] 打包到 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]
|
[Server]
|
||||||
# 本地服务器监听端口(默认为:1080)
|
display_level = 1
|
||||||
# Local server listening port (default:1080)
|
|
||||||
port = 1080
|
port = 1080
|
||||||
|
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
|
mode = cycle
|
||||||
|
|
||||||
# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300)
|
|
||||||
# Proxy address rotation interval (seconds), when set to 0, IP changes with each request (default:300)
|
|
||||||
interval = 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
|
use_getip = False
|
||||||
|
|
||||||
# 获取新代理地址的URL
|
|
||||||
# URL to get new proxy address
|
|
||||||
getip_url = http://example.com/getip
|
getip_url = http://example.com/getip
|
||||||
|
|
||||||
# 代理服务器认证用户名(如果代理服务器需要认证)
|
|
||||||
# Proxy server authentication username (if proxy server requires authentication)
|
|
||||||
proxy_username =
|
proxy_username =
|
||||||
|
|
||||||
# 代理服务器认证密码(如果代理服务器需要认证)
|
|
||||||
# Proxy server authentication password (if proxy server requires authentication)
|
|
||||||
proxy_password =
|
proxy_password =
|
||||||
|
|
||||||
# 代理地址列表文件(默认为:ip.txt)
|
|
||||||
# Proxy address list file (default:ip.txt)
|
|
||||||
proxy_file = ip.txt
|
proxy_file = ip.txt
|
||||||
|
|
||||||
# 是否启用代理检测功能 True or False(默认为True)
|
|
||||||
# Whether to enable proxy detection feature True or False (default:True)
|
|
||||||
check_proxies = True
|
check_proxies = True
|
||||||
|
|
||||||
# 语言设置 (cn/en)
|
|
||||||
# Language setting (cn/en)
|
|
||||||
language = cn
|
language = cn
|
||||||
|
|
||||||
# IP白名单文件路径(留空则不启用白名单)
|
|
||||||
# IP whitelist file path (leave empty to disable whitelist)
|
|
||||||
whitelist_file = whitelist.txt
|
whitelist_file = whitelist.txt
|
||||||
|
|
||||||
# IP黑名单文件路径(留空则不启用黑名单)
|
|
||||||
# IP blacklist file path (leave empty to disable blacklist)
|
|
||||||
blacklist_file = blacklist.txt
|
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
|
ip_auth_priority = whitelist
|
||||||
|
token = honmashironeko
|
||||||
|
|
||||||
|
[Users]
|
||||||
|
neko = 123456
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
app:
|
proxycat:
|
||||||
build: .
|
build: .
|
||||||
container_name: proxycat
|
|
||||||
ports:
|
ports:
|
||||||
- "1080:1080"
|
- "1080:1080"
|
||||||
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
restart: unless-stopped
|
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
|
import asyncio, logging, random, httpx, re, os, time
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from packaging import version
|
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 = {
|
MESSAGES = {
|
||||||
'cn': {
|
'cn': {
|
||||||
@ -45,30 +58,26 @@ MESSAGES = {
|
|||||||
'local_http': '本地监听地址 (HTTP)',
|
'local_http': '本地监听地址 (HTTP)',
|
||||||
'local_socks5': '本地监听地址 (SOCKS5)',
|
'local_socks5': '本地监听地址 (SOCKS5)',
|
||||||
'star_project': '开源项目求 Star',
|
'star_project': '开源项目求 Star',
|
||||||
'client_request_error': '客户端请求错误: {}',
|
|
||||||
'client_handle_error': '客户端处理错误: {}',
|
'client_handle_error': '客户端处理错误: {}',
|
||||||
'proxy_invalid_switch': '代理无效,切换代理',
|
'proxy_invalid_switch': '代理无效,切换代理',
|
||||||
'request_fail_retry': '请求失败,重试剩余次数: {}',
|
'request_fail_retry': '请求失败,重试剩余次数: {}',
|
||||||
'request_error': '请求错误: {}',
|
|
||||||
'user_interrupt': '用户中断程序',
|
'user_interrupt': '用户中断程序',
|
||||||
'new_version_found': '发现新版本!',
|
'new_version_found': '发现新版本!',
|
||||||
'visit_quark': '请访问 https://pan.quark.cn/s/39b4b5674570 获取最新版本。',
|
'visit_quark': '夸克网盘: https://pan.quark.cn/s/39b4b5674570',
|
||||||
'visit_github': '请访问 https://github.com/honmashironeko/ProxyCat 获取最新版本。',
|
'visit_github': 'GitHub: https://github.com/honmashironeko/ProxyCat',
|
||||||
'visit_baidu': '请访问 https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5 获取最新版本。',
|
'visit_baidu': '百度网盘: https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5',
|
||||||
'latest_version': '当前版本已是最新',
|
'latest_version': '当前已是最新版本',
|
||||||
'version_info_not_found': '无法在响应中找到版本信息',
|
'version_info_not_found': '未找到版本信息',
|
||||||
'update_check_error': '检查更新时发生错误: {}',
|
'update_check_error': '检查更新失败: {}',
|
||||||
'unauthorized_ip': '未授权的IP尝试访问: {}',
|
'unauthorized_ip': '未授权的IP尝试访问: {}',
|
||||||
'client_cancelled': '客户端连接已取消',
|
'client_cancelled': '客户端连接已取消',
|
||||||
'socks5_connection_error': 'SOCKS5连接错误: {}',
|
'socks5_connection_error': 'SOCKS5连接错误: {}',
|
||||||
'connect_timeout': '连接超时',
|
'connect_timeout': '连接超时',
|
||||||
'connection_reset': '连接被重置',
|
'connection_reset': '连接被重置',
|
||||||
'transfer_cancelled': '传输已取消',
|
'transfer_cancelled': '传输已取消',
|
||||||
'client_request_error': '客户端请求处理错误: {}',
|
'client_request_error': '客户端请求错误: {}',
|
||||||
'unsupported_protocol': '不支持的协议: {}',
|
'unsupported_protocol': '不支持的协议: {}',
|
||||||
'proxy_invalid_switch': '代理无效,正在切换',
|
|
||||||
'request_retry': '请求失败,重试中 (剩余{}次)',
|
'request_retry': '请求失败,重试中 (剩余{}次)',
|
||||||
'request_error': '请求过程中出错: {}',
|
|
||||||
'response_write_error': '写入响应时出错: {}',
|
'response_write_error': '写入响应时出错: {}',
|
||||||
'consecutive_failures': '检测到连续代理失败: {}',
|
'consecutive_failures': '检测到连续代理失败: {}',
|
||||||
'invalid_proxy': '当前代理无效: {}',
|
'invalid_proxy': '当前代理无效: {}',
|
||||||
@ -83,6 +92,101 @@ MESSAGES = {
|
|||||||
'proxy_forward_error': '代理转发错误: {}',
|
'proxy_forward_error': '代理转发错误: {}',
|
||||||
'data_transfer_timeout': '{}数据传输超时',
|
'data_transfer_timeout': '{}数据传输超时',
|
||||||
'data_transfer_error': '{}数据传输错误: {}',
|
'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': {
|
'en': {
|
||||||
'getting_new_proxy': 'Getting new proxy IP',
|
'getting_new_proxy': 'Getting new proxy IP',
|
||||||
@ -125,19 +229,17 @@ MESSAGES = {
|
|||||||
'local_http': 'Local Listening Address (HTTP)',
|
'local_http': 'Local Listening Address (HTTP)',
|
||||||
'local_socks5': 'Local Listening Address (SOCKS5)',
|
'local_socks5': 'Local Listening Address (SOCKS5)',
|
||||||
'star_project': 'Star the Project',
|
'star_project': 'Star the Project',
|
||||||
'client_request_error': 'Client request error: {}',
|
|
||||||
'client_handle_error': 'Client handling error: {}',
|
'client_handle_error': 'Client handling error: {}',
|
||||||
'proxy_invalid_switch': 'Proxy invalid, switching proxy',
|
'proxy_invalid_switch': 'Proxy invalid, switching proxy',
|
||||||
'request_fail_retry': 'Request failed, retrying remaining times: {}',
|
'request_fail_retry': 'Request failed, retrying remaining times: {}',
|
||||||
'request_error': 'Request error: {}',
|
|
||||||
'user_interrupt': 'User interrupted the program',
|
'user_interrupt': 'User interrupted the program',
|
||||||
'new_version_found': 'New version found!',
|
'new_version_found': 'New version available!',
|
||||||
'visit_quark': 'Please visit https://pan.quark.cn/s/39b4b5674570 to get the latest version.',
|
'visit_quark': 'Quark Drive: https://pan.quark.cn/s/39b4b5674570',
|
||||||
'visit_github': 'Please visit https://github.com/honmashironeko/ProxyCat to get the latest version.',
|
'visit_github': 'GitHub: https://github.com/honmashironeko/ProxyCat',
|
||||||
'visit_baidu': 'Please visit https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5 to get the latest version.',
|
'visit_baidu': 'Baidu Drive: https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5',
|
||||||
'latest_version': 'You are using the latest version',
|
'latest_version': 'You are using the latest version',
|
||||||
'version_info_not_found': 'Version information not found in the response',
|
'version_info_not_found': 'Version information not found',
|
||||||
'update_check_error': 'Error occurred while checking for updates: {}',
|
'update_check_error': 'Failed to check for updates: {}',
|
||||||
'unauthorized_ip': 'Unauthorized IP attempt: {}',
|
'unauthorized_ip': 'Unauthorized IP attempt: {}',
|
||||||
'client_cancelled': 'Client connection cancelled',
|
'client_cancelled': 'Client connection cancelled',
|
||||||
'socks5_connection_error': 'SOCKS5 connection error: {}',
|
'socks5_connection_error': 'SOCKS5 connection error: {}',
|
||||||
@ -147,7 +249,6 @@ MESSAGES = {
|
|||||||
'data_transfer_error': 'Data transfer error: {}',
|
'data_transfer_error': 'Data transfer error: {}',
|
||||||
'client_request_error': 'Client request handling error: {}',
|
'client_request_error': 'Client request handling error: {}',
|
||||||
'unsupported_protocol': 'Unsupported protocol: {}',
|
'unsupported_protocol': 'Unsupported protocol: {}',
|
||||||
'proxy_invalid_switch': 'Proxy invalid, switching',
|
|
||||||
'request_retry': 'Request failed, retrying ({} left)',
|
'request_retry': 'Request failed, retrying ({} left)',
|
||||||
'request_error': 'Error during request: {}',
|
'request_error': 'Error during request: {}',
|
||||||
'response_write_error': 'Error writing response: {}',
|
'response_write_error': 'Error writing response: {}',
|
||||||
@ -164,6 +265,101 @@ MESSAGES = {
|
|||||||
'proxy_forward_error': 'Proxy forwarding error: {}',
|
'proxy_forward_error': 'Proxy forwarding error: {}',
|
||||||
'data_transfer_timeout': '{} data transfer timeout',
|
'data_transfer_timeout': '{} data transfer timeout',
|
||||||
'data_transfer_error': '{} data transfer error: {}',
|
'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}")
|
print(f"{Fore.MAGENTA}{'=' * 55}")
|
||||||
for key, value in banner_info:
|
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")
|
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"""
|
logo1 = r"""
|
||||||
|\ _,,,---,,_ by 本间白猫
|
|\ _,,,---,,_ by 本间白猫
|
||||||
ZZZzz /,`.-'`' -. ;-;;,_
|
ZZZzz /,`.-'`' -. ;-;;,_
|
||||||
@ -281,14 +483,17 @@ def load_config(config_file='config/config.ini'):
|
|||||||
config.read(config_file, encoding='utf-8')
|
config.read(config_file, encoding='utf-8')
|
||||||
|
|
||||||
settings = {}
|
settings = {}
|
||||||
if config.has_section('SETTINGS'):
|
if config.has_section('Server'):
|
||||||
settings.update(dict(config.items('SETTINGS')))
|
settings.update(dict(config.items('Server')))
|
||||||
|
|
||||||
|
config_dir = os.path.dirname(config_file)
|
||||||
for key in ['proxy_file', 'whitelist_file', 'blacklist_file']:
|
for key in ['proxy_file', 'whitelist_file', 'blacklist_file']:
|
||||||
if key in settings and settings[key]:
|
if key in settings and settings[key]:
|
||||||
config_dir = os.path.dirname(config_file)
|
|
||||||
settings[key] = os.path.join(config_dir, settings[key])
|
settings[key] = os.path.join(config_dir, settings[key])
|
||||||
|
|
||||||
|
if config.has_section('DEFAULT'):
|
||||||
|
settings.update(dict(config.items('DEFAULT')))
|
||||||
|
|
||||||
return {**DEFAULT_CONFIG, **settings}
|
return {**DEFAULT_CONFIG, **settings}
|
||||||
|
|
||||||
def load_ip_list(file_path):
|
def load_ip_list(file_path):
|
||||||
@ -316,32 +521,7 @@ def parse_proxy(proxy):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None, None, None, None
|
return None, None, None, None
|
||||||
|
|
||||||
async def check_proxy(proxy):
|
async def check_http_proxy(proxy, test_url='https://www.baidu.com'):
|
||||||
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):
|
|
||||||
protocol, auth, host, port = parse_proxy(proxy)
|
protocol, auth, host, port = parse_proxy(proxy)
|
||||||
proxies = {}
|
proxies = {}
|
||||||
if auth:
|
if auth:
|
||||||
@ -354,18 +534,21 @@ async def check_http_proxy(proxy):
|
|||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(proxies=proxies, timeout=10, verify=False) as client:
|
async with httpx.AsyncClient(proxies=proxies, timeout=10, verify=False) as client:
|
||||||
try:
|
try:
|
||||||
response = await client.get('https://www.baidu.com')
|
response = await client.get(test_url)
|
||||||
return response.status_code == 200
|
return response.status_code == 200
|
||||||
except:
|
except:
|
||||||
response = await client.get('http://www.baidu.com')
|
if test_url.startswith('https://'):
|
||||||
return response.status_code == 200
|
http_url = 'http://' + test_url[8:]
|
||||||
|
response = await client.get(http_url)
|
||||||
|
return response.status_code == 200
|
||||||
|
return False
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def check_https_proxy(proxy):
|
async def check_https_proxy(proxy, test_url='https://www.baidu.com'):
|
||||||
return await check_http_proxy(proxy)
|
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)
|
protocol, auth, host, port = parse_proxy(proxy)
|
||||||
if not all([host, port]):
|
if not all([host, port]):
|
||||||
return False
|
return False
|
||||||
@ -394,7 +577,10 @@ async def check_socks_proxy(proxy):
|
|||||||
if auth_response[1] != 0x00:
|
if auth_response[1] != 0x00:
|
||||||
return False
|
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')
|
writer.write(b'\x05\x01\x00\x03' + bytes([len(domain)]) + domain + b'\x00\x50')
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
@ -410,10 +596,38 @@ async def check_socks_proxy(proxy):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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 = []
|
valid_proxies = []
|
||||||
for proxy in proxies:
|
for proxy in proxies:
|
||||||
if await check_proxy(proxy):
|
if await check_proxy(proxy, test_url):
|
||||||
valid_proxies.append(proxy)
|
valid_proxies.append(proxy)
|
||||||
return valid_proxies
|
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)
|
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
|
||||||
if match:
|
if match:
|
||||||
latest_version = match.group(1)
|
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]):
|
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('new_version_found', language)} 当前版本: {CURRENT_VERSION}, 最新版本: {latest_version}{Style.RESET_ALL}")
|
||||||
print(f"{Fore.YELLOW}{get_message('visit_quark', language)}")
|
print(f"{Fore.YELLOW}{get_message('visit_quark', language)}{Style.RESET_ALL}")
|
||||||
print(f"{Fore.YELLOW}{get_message('visit_github', language)}")
|
print(f"{Fore.YELLOW}{get_message('visit_github', language)}{Style.RESET_ALL}")
|
||||||
print(f"{Fore.YELLOW}{get_message('visit_baidu', language)}")
|
print(f"{Fore.YELLOW}{get_message('visit_baidu', language)}{Style.RESET_ALL}")
|
||||||
else:
|
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:
|
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:
|
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 asyncio import TimeoutError
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
from config import getip
|
from config import getip
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
def load_proxies(file_path='ip.txt'):
|
def load_proxies(file_path='ip.txt'):
|
||||||
@ -21,17 +22,27 @@ def validate_proxy(proxy):
|
|||||||
class AsyncProxyServer:
|
class AsyncProxyServer:
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.username = self.config['username'].strip()
|
self.username = self.config.get('username', '').strip()
|
||||||
self.password = self.config['password'].strip()
|
self.password = self.config.get('password', '').strip()
|
||||||
self.auth_required = bool(self.username and self.password)
|
self.mode = self.config.get('mode', 'cycle')
|
||||||
self.mode = self.config['mode']
|
self.interval = int(self.config.get('interval', '300'))
|
||||||
self.interval = int(self.config['interval'])
|
|
||||||
self.use_getip = self.config.get('use_getip', 'False').lower() == 'true'
|
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.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', ''))
|
config_parser = ConfigParser()
|
||||||
self.ip_auth_priority = config.get('ip_auth_priority', 'whitelist')
|
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:
|
if not self.use_getip:
|
||||||
self.proxies = self._load_file_proxies()
|
self.proxies = self._load_file_proxies()
|
||||||
@ -64,6 +75,57 @@ class AsyncProxyServer:
|
|||||||
self.request_semaphore = asyncio.Semaphore(self.max_concurrent_requests)
|
self.request_semaphore = asyncio.Semaphore(self.max_concurrent_requests)
|
||||||
self.connection_pool = {}
|
self.connection_pool = {}
|
||||||
self.pipeline_enabled = True
|
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):
|
async def get_next_proxy(self):
|
||||||
if self.mode == 'load_balance':
|
if self.mode == 'load_balance':
|
||||||
@ -110,7 +172,6 @@ class AsyncProxyServer:
|
|||||||
else:
|
else:
|
||||||
self.current_proxy = next(self.proxy_cycle)
|
self.current_proxy = next(self.proxy_cycle)
|
||||||
self.last_switch_time = time.time()
|
self.last_switch_time = time.time()
|
||||||
logging.info(get_message('proxy_switch', self.language, self.current_proxy))
|
|
||||||
|
|
||||||
async def custom_proxy_switch(self):
|
async def custom_proxy_switch(self):
|
||||||
return self.proxies[0] if self.proxies else "No proxies available"
|
return self.proxies[0] if self.proxies else "No proxies available"
|
||||||
@ -138,30 +199,62 @@ class AsyncProxyServer:
|
|||||||
return not self.blacklist
|
return not self.blacklist
|
||||||
|
|
||||||
async def handle_client(self, reader, writer):
|
async def handle_client(self, reader, writer):
|
||||||
async with self.semaphore:
|
task = asyncio.current_task()
|
||||||
try:
|
self.tasks.add(task)
|
||||||
client_ip = writer.get_extra_info('peername')[0]
|
try:
|
||||||
if not self.check_ip_auth(client_ip):
|
peername = writer.get_extra_info('peername')
|
||||||
logging.warning(get_message('unauthorized_ip', self.language, client_ip))
|
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()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
return
|
if client_ip in self.connected_clients:
|
||||||
|
self.connected_clients.discard(client_ip)
|
||||||
first_byte = await reader.read(1)
|
except Exception as e:
|
||||||
if not first_byte:
|
logging.error(get_message('connection_error', self.language, str(e)))
|
||||||
return
|
finally:
|
||||||
|
self.tasks.discard(task)
|
||||||
if (first_byte == b'\x05'):
|
try:
|
||||||
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()
|
writer.close()
|
||||||
await writer.wait_closed()
|
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):
|
async def handle_socks5_connection(self, reader, writer):
|
||||||
try:
|
try:
|
||||||
@ -383,18 +476,20 @@ class AsyncProxyServer:
|
|||||||
logging.error(get_message('client_request_error', self.language, e))
|
logging.error(get_message('client_request_error', self.language, e))
|
||||||
|
|
||||||
def _authenticate(self, headers):
|
def _authenticate(self, headers):
|
||||||
if not self.auth_required:
|
if not self.users:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
auth = headers.get('proxy-authorization')
|
auth = headers.get('proxy-authorization')
|
||||||
if not auth:
|
if not auth:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scheme, credentials = auth.split()
|
scheme, credentials = auth.split()
|
||||||
if scheme.lower() != 'basic':
|
if scheme.lower() != 'basic':
|
||||||
return False
|
return False
|
||||||
|
|
||||||
username, password = base64.b64decode(credentials).decode().split(':')
|
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:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -540,52 +635,6 @@ class AsyncProxyServer:
|
|||||||
verify=False
|
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):
|
async def _cleanup_connections(self):
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
expired_keys = [
|
expired_keys = [
|
||||||
@ -627,9 +676,6 @@ class AsyncProxyServer:
|
|||||||
self.proxy_check_cache[proxy] = (current_time, False)
|
self.proxy_check_cache[proxy] = (current_time, False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
|
||||||
self._clean_proxy_cache()
|
|
||||||
|
|
||||||
def _clean_proxy_cache(self):
|
def _clean_proxy_cache(self):
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
self.proxy_check_cache = {
|
self.proxy_check_cache = {
|
||||||
@ -671,65 +717,53 @@ class AsyncProxyServer:
|
|||||||
old_proxy = self.current_proxy
|
old_proxy = self.current_proxy
|
||||||
await self.get_proxy()
|
await self.get_proxy()
|
||||||
self.last_switch_time = current_time
|
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
|
self.proxy_failed = False
|
||||||
finally:
|
finally:
|
||||||
self.switching_proxy = False
|
self.switching_proxy = False
|
||||||
|
|
||||||
async def check_proxy(self, proxy):
|
async def check_proxy(self, proxy):
|
||||||
current_time = time.time()
|
cache_key = f"{proxy}_{time.time() // self.proxy_cache_ttl}"
|
||||||
if proxy in self.proxy_cache:
|
if cache_key in self.proxy_cache:
|
||||||
cache_time, is_valid = self.proxy_cache[proxy]
|
return self.proxy_cache[cache_key]
|
||||||
if current_time - cache_time < self.proxy_cache_ttl:
|
|
||||||
return is_valid
|
|
||||||
|
|
||||||
is_valid = await self._check_proxy_impl(proxy)
|
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
|
return is_valid
|
||||||
|
|
||||||
async def handle_request(self, client_reader, client_writer, target_host, target_port):
|
def initialize_proxies(self):
|
||||||
for retry in range(self.retry_count):
|
if self.mode == 'cycle':
|
||||||
try:
|
if hasattr(self, 'proxies') and self.proxies:
|
||||||
target_reader, target_writer = await asyncio.wait_for(
|
self.proxy_cycle = cycle(self.proxies)
|
||||||
asyncio.open_connection(target_host, target_port),
|
elif self.use_getip:
|
||||||
timeout=self.timeout
|
pass
|
||||||
)
|
else:
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
target_writer.close()
|
with open(self.proxy_file, 'r') as f:
|
||||||
await target_writer.wait_closed()
|
self.proxies = [line.strip() for line in f if line.strip()]
|
||||||
except:
|
if self.proxies:
|
||||||
pass
|
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):
|
async def cleanup_disconnected_ips(self):
|
||||||
try:
|
while True:
|
||||||
while True:
|
try:
|
||||||
data = await asyncio.wait_for(reader.read(8192), timeout=self.timeout)
|
active_ips = set()
|
||||||
if not data:
|
for client_info in self.get_active_connections():
|
||||||
break
|
active_ips.add(client_info[0])
|
||||||
writer.write(data)
|
|
||||||
await writer.drain()
|
self.connected_clients = active_ips
|
||||||
except TimeoutError:
|
|
||||||
logging.error(get_message('data_transfer_timeout', self.language, direction))
|
except Exception as e:
|
||||||
except Exception as e:
|
logging.error(get_message('cleanup_error', self.language, str(e)))
|
||||||
logging.error(get_message('data_transfer_error', self.language, direction, e))
|
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
def is_docker():
|
def is_docker():
|
||||||
return os.path.exists('/.dockerenv')
|
return os.path.exists('/.dockerenv')
|
||||||
@ -753,12 +787,3 @@ class AsyncProxyServer:
|
|||||||
logging.error(error_messages.get(error_type, str(details)))
|
logging.error(error_messages.get(error_type, str(details)))
|
||||||
if error_type in ['timeout', 'invalid']:
|
if error_type in ['timeout', 'invalid']:
|
||||||
await self.handle_proxy_failure()
|
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
|
colorama==0.4.6
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
packaging==24.1
|
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