This commit is contained in:
本间白猫 2025-04-01 15:03:42 +08:00
parent d207a3bb8f
commit 086d37aa73
12 changed files with 830 additions and 354 deletions

View File

@ -1,5 +1,11 @@
# ProxyCat 使用手册 # ProxyCat 使用手册
## 重要事项
- Python版本最好为Python 3.11
- Releases中为较为稳定的打包版本不一定是最新
- API 接口所获取的代理地址必须为 IP:PORT 格式且只提供一条地址
## 源码使用及 Docker 部署 ## 源码使用及 Docker 部署
### 源码手册 ### 源码手册
@ -52,6 +58,7 @@ getip_url = 获取代理地址的 API 接口
``` ```
python ProxyCat.py python ProxyCat.py
python app.py (Web控制管理-推荐方式)
``` ```
![Run](./Operation%20Manual.assets/Run.png) ![Run](./Operation%20Manual.assets/Run.png)
@ -90,6 +97,7 @@ docker logs proxycat
# 0: 仅显示代理切换和错误信息 # 0: 仅显示代理切换和错误信息
# 1: 显示代理切换、倒计时和错误信息 # 1: 显示代理切换、倒计时和错误信息
# 2: 显示所有详细信息 # 2: 显示所有详细信息
# 仅终端管理时生效
display_level = 1 display_level = 1
# 本地服务器监听端口(默认为:1080) # 本地服务器监听端口(默认为:1080)
@ -99,8 +107,8 @@ port = 1080
# Web 管理页面端口(默认为:5000) # Web 管理页面端口(默认为:5000)
web_port = 5000 web_port = 5000
# 代理地址轮换模式cycle 表示循环使用,custom 表示使用自定义模式,load_balance 表示负载均衡(默认为:cycle) # 代理地址轮换模式cycle 表示循环使用loadbalance 表示负载均衡(默认为:cycle)
# Proxy rotation mode: cycle means cyclic use, custom means custom mode, load_balance means load balancing (default:cycle) # Proxy rotation mode: cycle means cyclic use, loadbalance means load balancing (default:cycle)
mode = cycle mode = cycle
# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300) # 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300)

View File

@ -1,3 +1,20 @@
### 2025/03/23
- 修复负载均衡模式无法调用的BUG
- 修复socks5连接错误
- 修复http、socks5监听下的目标网站错误和代理地址失效情况一致导致无法正常触发代理切换
- 修改代理有效性校验,配置为可控检测,关闭后将不会进行有效性检查避免特殊情况一直切换
- 修复并发下导致大规模触发更换和提示的问题,锁定操作的原子性
- 修复大量细节逻辑、描述错误
- 当前代理切换触发条件为时间间隔到期切换、代理失效自动切换、Web手动切换、API下首次请求自动获取
### 2025/03/17
- 修复目标站点本身错误时会触发代理切换的错误逻辑
- 修改连接关闭方式
- 优化监听服务器性能
- 修复多处错误BUG
### 2025/03/14 ### 2025/03/14
- 修复'_last_used'报错问题,连接关闭方式修正 - 修复'_last_used'报错问题,连接关闭方式修正

View File

@ -70,7 +70,7 @@ def update_status(server):
time.sleep(1) time.sleep(1)
continue continue
if server.mode == 'load_balance': if server.mode == 'loadbalance':
if display_level >= 1: if display_level >= 1:
print_proxy_info() print_proxy_info()
time.sleep(5) time.sleep(5)
@ -155,7 +155,7 @@ async def run_server(server):
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':
logging.info(get_message('proxy_check_start', server.language)) logging.info(get_message('proxy_check_start', server.language))
valid_proxies = await check_proxies(server.proxies) valid_proxies = await check_proxies(server.proxies, server.test_url)
if valid_proxies: if valid_proxies:
server.proxies = valid_proxies server.proxies = valid_proxies
server.proxy_cycle = cycle(valid_proxies) server.proxy_cycle = cycle(valid_proxies)
@ -187,6 +187,11 @@ class ProxyCat:
if hasattr(socket, 'SO_KEEPALIVE'): if hasattr(socket, 'SO_KEEPALIVE'):
socket.SO_KEEPALIVE = True socket.SO_KEEPALIVE = True
if hasattr(socket, 'SO_REUSEADDR'):
socket.SO_REUSEADDR = True
if hasattr(socket, 'SO_REUSEPORT') and os.name != 'nt':
socket.SO_REUSEPORT = True
if os.name != 'nt': if os.name != 'nt':
import resource import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
@ -225,11 +230,17 @@ class ProxyCat:
async def start_server(self): async def start_server(self):
try: try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, 'SO_REUSEPORT') and os.name != 'nt':
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind((self.config.get('SERVER', 'host'), int(self.config.get('SERVER', 'port'))))
server = await asyncio.start_server( server = await asyncio.start_server(
self.handle_client, self.handle_client,
self.config.get('SERVER', 'host'), sock=sock
self.config.get('SERVER', 'port')
) )
logging.info(get_message('server_running', self.language, logging.info(get_message('server_running', self.language,
self.config.get('SERVER', 'host'), self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port'))) self.config.get('SERVER', 'port')))
@ -396,6 +407,16 @@ class ProxyCat:
except Exception as e: except Exception as e:
logging.error(get_message('data_transfer_error', self.language, e)) logging.error(get_message('data_transfer_error', self.language, e))
def monitor_resources(self):
import psutil
process = psutil.Process(os.getpid())
while self.running:
mem_info = process.memory_info()
logging.debug(f"Memory usage: {mem_info.rss / 1024 / 1024:.2f} MB, "
f"Connections: {len(self.tasks)}")
time.sleep(60)
if __name__ == '__main__': if __name__ == '__main__':
setup_logging() setup_logging()
parser = argparse.ArgumentParser(description=logos()) parser = argparse.ArgumentParser(description=logos())
@ -413,6 +434,9 @@ if __name__ == '__main__':
status_thread = threading.Thread(target=update_status, args=(server,), daemon=True) status_thread = threading.Thread(target=update_status, args=(server,), daemon=True)
status_thread.start() status_thread.start()
cleanup_thread = threading.Thread(target=lambda: asyncio.run(server.cleanup_clients()), daemon=True)
cleanup_thread.start()
try: try:
asyncio.run(run_server(server)) asyncio.run(run_server(server))
except KeyboardInterrupt: except KeyboardInterrupt:

87
app.py
View File

@ -119,37 +119,26 @@ def get_status():
else: else:
current_proxy = get_message('no_proxy', server.language) current_proxy = get_message('no_proxy', server.language)
time_left = server.time_until_next_switch()
if server.mode == 'loadbalance':
if time_left == float('inf'):
time_left = -1
return jsonify({ return jsonify({
'current_proxy': current_proxy, 'current_proxy': current_proxy,
'mode': server.mode, 'mode': server.mode,
'port': int(server_config.get('port', '1080')), 'port': int(server_config.get('port', '1080')),
'interval': server.interval, 'interval': server.interval,
'time_left': server.time_until_next_switch(), 'time_left': time_left,
'total_proxies': len(server.proxies) if hasattr(server, 'proxies') else 0, 'total_proxies': len(server.proxies) if hasattr(server, 'proxies') else 0,
'use_getip': server.use_getip, 'use_getip': server.use_getip,
'getip_url': getattr(server, 'getip_url', '') if getattr(server, 'use_getip', False) else '', 'getip_url': getattr(server, 'getip_url', '') if getattr(server, 'use_getip', False) else '',
'auth_required': server.auth_required, 'auth_required': server.auth_required,
'display_level': int(config.get('DEFAULT', 'display_level', fallback='1')), 'display_level': int(server_config.get('display_level', '1')),
'service_status': 'running' if server.running else 'stopped', 'service_status': 'running' if server.running else 'stopped',
'config': { 'config': server_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=['POST']) @app.route('/api/config', methods=['POST'])
@ -158,6 +147,8 @@ def save_config():
new_config = request.get_json() new_config = request.get_json()
current_config = load_config('config/config.ini') current_config = load_config('config/config.ini')
port_changed = str(new_config.get('port', '')) != str(current_config.get('port', '')) port_changed = str(new_config.get('port', '')) != str(current_config.get('port', ''))
mode_changed = new_config.get('mode', '') != current_config.get('mode', '')
use_getip_changed = (new_config.get('use_getip', 'False').lower() == 'true') != (current_config.get('use_getip', 'False').lower() == 'true')
config_parser = ConfigParser() config_parser = ConfigParser()
config_parser.read('config/config.ini', encoding='utf-8') config_parser.read('config/config.ini', encoding='utf-8')
@ -172,12 +163,27 @@ def save_config():
with open('config/config.ini', 'w', encoding='utf-8') as f: with open('config/config.ini', 'w', encoding='utf-8') as f:
config_parser.write(f) config_parser.write(f)
old_mode = server.mode
old_use_getip = server.use_getip
server.config = load_config('config/config.ini') server.config = load_config('config/config.ini')
server._init_config_values(server.config) server._init_config_values(server.config)
if mode_changed or use_getip_changed:
server._handle_mode_change()
if new_config.get('mode') == 'loadbalance':
server.last_switch_time = time.time()
server.last_switch_attempt = 0
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'port_changed': port_changed 'port_changed': port_changed,
'service_status': 'running' if server.running else 'stopped'
}) })
except Exception as e: except Exception as e:
@ -324,35 +330,8 @@ def clear_logs():
@require_token @require_token
def switch_proxy(): def switch_proxy():
try: try:
if server.use_getip: result = asyncio.run(server.switch_proxy())
from config.getip import newip if result:
try:
old_proxy = server.current_proxy
new_proxy = newip()
server.current_proxy = new_proxy
server.last_switch_time = time.time()
server._log_proxy_switch(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()
server._log_proxy_switch(old_proxy, server.current_proxy)
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'current_proxy': server.current_proxy, 'current_proxy': server.current_proxy,
@ -361,7 +340,7 @@ def switch_proxy():
else: else:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': get_message('no_proxies_available', server.language) 'message': get_message('switch_failed', server.language, 'Proxy switch not needed or in cooldown')
}) })
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
@ -523,7 +502,7 @@ def check_version():
original_level = httpx_logger.level original_level = httpx_logger.level
httpx_logger.setLevel(logging.WARNING) httpx_logger.setLevel(logging.WARNING)
CURRENT_VERSION = "ProxyCat-V2.0.2" CURRENT_VERSION = "ProxyCat-V2.0.4"
try: try:
client = httpx.Client(transport=httpx.HTTPTransport(retries=3)) client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
@ -630,7 +609,7 @@ if __name__ == '__main__':
web_port = int(config.get('web_port', '5000')) web_port = int(config.get('web_port', '5000'))
web_url = f"http://127.0.0.1:{web_port}" web_url = f"http://127.0.0.1:{web_port}"
if config.get('token'): if config.get('token'):
web_url += f"?token={config.get('token')}" web_url += f"/web?token={config.get('token')}"
logging.info(get_message('web_panel_url', server.language, web_url)) logging.info(get_message('web_panel_url', server.language, web_url))
logging.info(get_message('web_panel_notice', server.language)) logging.info(get_message('web_panel_notice', server.language))

View File

@ -4,12 +4,12 @@ port = 1080
web_port = 5000 web_port = 5000
mode = cycle mode = cycle
interval = 300 interval = 300
use_getip = False use_getip = false
getip_url = http://example.com/getip getip_url = http://example.com/getip
proxy_username = proxy_username =
proxy_password = proxy_password =
proxy_file = ip.txt proxy_file = ip.txt
check_proxies = True check_proxies = true
test_url = test_url =
language = cn language = cn
whitelist_file = whitelist.txt whitelist_file = whitelist.txt
@ -19,4 +19,5 @@ token = honmashironeko
[Users] [Users]
neko = 123456 neko = 123456
k = 123

View File

@ -1,2 +1,2 @@
socks5://127.0.0.1:7890
http://127.0.0.1:7890 http://127.0.0.1:7890
socks5://127.0.0.1:7890

View File

@ -28,6 +28,9 @@ MESSAGES = {
'proxy_switch': '切换代理: {} -> {}', 'proxy_switch': '切换代理: {} -> {}',
'proxy_consecutive_fails': '代理 {} 连续失败 {} 次,正在切换新代理', 'proxy_consecutive_fails': '代理 {} 连续失败 {} 次,正在切换新代理',
'proxy_invalid': '代理 {} 无效,立即切换代理', 'proxy_invalid': '代理 {} 无效,立即切换代理',
'proxy_failure': '代理检测失败: {}',
'proxy_failure_threshold': '代理无效,开始切换',
'proxy_check_error': '代理检查时发生错误: {}',
'connection_timeout': '连接超时', 'connection_timeout': '连接超时',
'data_transfer_timeout': '数据传输超时,正在重试...', 'data_transfer_timeout': '数据传输超时,正在重试...',
'connection_reset': '连接被重置', 'connection_reset': '连接被重置',
@ -49,7 +52,7 @@ MESSAGES = {
'blog': '博客', 'blog': '博客',
'proxy_mode': '代理轮换模式', 'proxy_mode': '代理轮换模式',
'cycle': '循环', 'cycle': '循环',
'load_balance': '负载均衡', 'loadbalance': '负载均衡',
'single_round': '单轮', 'single_round': '单轮',
'proxy_interval': '代理更换时间', 'proxy_interval': '代理更换时间',
'default_auth': '默认账号密码', 'default_auth': '默认账号密码',
@ -105,7 +108,7 @@ MESSAGES = {
'proxy_check_result': '代理检查完成,有效代理:{}', 'proxy_check_result': '代理检查完成,有效代理:{}',
'no_proxy': '无代理', 'no_proxy': '无代理',
'cycle_mode': '循环模式', 'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡模式', 'loadbalance_mode': '负载均衡模式',
'proxy_check_start': '开始检查代理...', 'proxy_check_start': '开始检查代理...',
'proxy_check_complete': '代理检查完成', 'proxy_check_complete': '代理检查完成',
'proxy_save_success': '代理保存成功', 'proxy_save_success': '代理保存成功',
@ -181,6 +184,11 @@ MESSAGES = {
'web_panel_url': '网页控制面板地址: {}', 'web_panel_url': '网页控制面板地址: {}',
'web_panel_notice': '请使用浏览器访问上述地址来管理代理服务器', 'web_panel_notice': '请使用浏览器访问上述地址来管理代理服务器',
'api_proxy_settings_title': 'API代理设置', 'api_proxy_settings_title': 'API代理设置',
'all_retries_failed': '所有重试均已失败,最后错误: {}',
'proxy_get_failed': '获取代理失败',
'proxy_get_error': '获取代理错误: {}',
'request_error': '请求错误: {}',
'proxy_switch_error': '代理切换错误: {}',
}, },
'en': { 'en': {
'getting_new_proxy': 'Getting new proxy IP', 'getting_new_proxy': 'Getting new proxy IP',
@ -193,6 +201,9 @@ MESSAGES = {
'proxy_switch': 'Switch proxy: {} -> {}', 'proxy_switch': 'Switch proxy: {} -> {}',
'proxy_consecutive_fails': 'Proxy {} failed {} times consecutively, switching to new proxy', 'proxy_consecutive_fails': 'Proxy {} failed {} times consecutively, switching to new proxy',
'proxy_invalid': 'Proxy {} is invalid, switching proxy immediately', 'proxy_invalid': 'Proxy {} is invalid, switching proxy immediately',
'proxy_failure': 'Proxy check failed: {}',
'proxy_failure_threshold': 'Proxy invalid, switching now',
'proxy_check_error': 'Error occurred during proxy check: {}',
'connection_timeout': 'Connection timeout', 'connection_timeout': 'Connection timeout',
'data_transfer_timeout': 'Data transfer timeout, retrying...', 'data_transfer_timeout': 'Data transfer timeout, retrying...',
'connection_reset': 'Connection reset', 'connection_reset': 'Connection reset',
@ -214,7 +225,7 @@ MESSAGES = {
'blog': 'Blog', 'blog': 'Blog',
'proxy_mode': 'Proxy Rotation Mode', 'proxy_mode': 'Proxy Rotation Mode',
'cycle': 'Cycle', 'cycle': 'Cycle',
'load_balance': 'Load Balance', 'loadbalance': 'Load Balance',
'single_round': 'Single Round', 'single_round': 'Single Round',
'proxy_interval': 'Proxy Change Interval', 'proxy_interval': 'Proxy Change Interval',
'default_auth': 'Default Username and Password', 'default_auth': 'Default Username and Password',
@ -248,37 +259,40 @@ MESSAGES = {
'invalid_proxy': 'Current proxy is invalid: {}', 'invalid_proxy': 'Current proxy is invalid: {}',
'whitelist_error': 'Failed to add whitelist: {}', 'whitelist_error': 'Failed to add whitelist: {}',
'api_mode_notice': 'Currently in API mode, proxy address will be automatically obtained upon request', 'api_mode_notice': 'Currently in API mode, proxy address will be automatically obtained upon request',
'all_retries_failed': 'All retries failed, last error: {}',
'proxy_get_failed': 'Failed to get proxy',
'proxy_get_error': 'Error getting proxy: {}',
'proxy_switch_error': 'Error switching proxy: {}',
'server_running': 'Proxy server running at {}:{}', 'server_running': 'Proxy server running at {}:{}',
'server_start_error': 'Server startup error: {}', 'server_start_error': 'Server startup error: {}',
'server_shutting_down': 'Shutting down server...', 'server_shutting_down': 'Server shutting down...',
'client_process_error': 'Error processing client request: {}', 'client_process_error': 'Client processing error: {}',
'request_handling_error': 'Request handling error: {}', 'request_handling_error': 'Request handling error: {}',
'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: {}',
'status_update_error': 'Status update error', 'status_update_error': 'Status update error',
'display_level_notice': 'Current display level: {}', 'display_level_notice': 'Current display level: {}',
'display_level_desc': '''Display level description: 'display_level_desc': '''Display level description:
0: Show only proxy switches and errors 0: Only show proxy switch and error messages
1: Show proxy switches, countdown and errors 1: Show proxy switch, countdown and error messages
2: Show all detailed information''', 2: Show all detailed information''',
'new_client_connect': 'New client connection - IP: {}, User: {}', 'new_client_connect': 'New client connection - IP: {}, User: {}',
'no_auth': 'No authentication', 'no_auth': 'No authentication',
'connection_error': 'Connection handling error: {}', 'connection_error': 'Connection handling error: {}',
'cleanup_error': 'IP cleanup error: {}', 'cleanup_error': 'Cleanup IP error: {}',
'port_changed': 'Port changed: {} -> {}, server restart required', 'port_changed': 'Port changed: {} -> {}, server restart required to take effect',
'config_updated': 'Server configuration updated', 'config_updated': 'Server configuration updated',
'load_proxy_file_error': 'Failed to load proxy file: {}', 'load_proxy_file_error': 'Failed to load proxy file: {}',
'proxy_check_result': 'Proxy check completed, valid proxies: {}', 'proxy_check_result': 'Proxy check completed, valid proxies: {}',
'no_proxy': 'No proxy', 'no_proxy': 'No proxy',
'cycle_mode': 'Cycle Mode', 'cycle_mode': 'Cycle mode',
'load_balance_mode': 'Load Balance Mode', 'loadbalance_mode': 'Load balance mode',
'proxy_check_start': 'Starting proxy check...', 'proxy_check_start': 'Starting proxy check...',
'proxy_check_complete': 'Proxy check completed', 'proxy_check_complete': 'Proxy check completed',
'proxy_save_success': 'Proxies saved successfully', 'proxy_save_success': 'Proxy saved successfully',
'proxy_save_failed': 'Failed to save proxies: {}', 'proxy_save_failed': 'Failed to save proxy: {}',
'ip_list_save_success': 'IP lists saved successfully', 'ip_list_save_success': 'IP list saved successfully',
'ip_list_save_failed': 'Failed to save IP lists: {}', 'ip_list_save_failed': 'Failed to save IP list: {}',
'switch_success': 'Proxy switched successfully', 'switch_success': 'Proxy switched successfully',
'switch_failed': 'Failed to switch proxy: {}', 'switch_failed': 'Failed to switch proxy: {}',
'service_start_success': 'Service started successfully', 'service_start_success': 'Service started successfully',
@ -305,20 +319,20 @@ MESSAGES = {
'language_label': 'Language', 'language_label': 'Language',
'chinese': 'Chinese', 'chinese': 'Chinese',
'english': 'English', 'english': 'English',
'manual_switch_btn': 'Manual Switch', 'manual_switch_btn': 'Switch Manually',
'service_control_title': 'Service Control', 'service_control_title': 'Service Control',
'language_switch_success': '', 'language_switch_success': 'Language switched successfully',
'language_switch_failed': '', 'language_switch_failed': 'Failed to switch language',
'refresh_failed': 'Failed to refresh data: {}', 'refresh_failed': 'Failed to refresh data: {}',
'auth_username_label': 'Auth Username', 'auth_username_label': 'Authentication Username',
'auth_password_label': 'Auth Password', 'auth_password_label': 'Authentication Password',
'proxy_auth_username_label': 'Proxy Auth Username', 'proxy_auth_username_label': 'Proxy Authentication Username',
'proxy_auth_password_label': 'Proxy Auth Password', 'proxy_auth_password_label': 'Proxy Authentication Password',
'progress_bar_label': 'Switch Progress', 'progress_bar_label': 'Switch Progress',
'proxy_settings_title': 'Proxy Settings', 'proxy_settings_title': 'Proxy Settings',
'config_save_success': 'Configuration saved successfully', 'config_save_success': 'Configuration saved successfully',
'config_save_failed': 'Failed to save configuration: {}', 'config_save_failed': 'Failed to save configuration: {}',
'config_restart_required': 'Configuration changed, server restart required', 'config_restart_required': 'Configuration changed, server restart required to take effect',
'confirm_restart_service': 'Restart server now?', 'confirm_restart_service': 'Restart server now?',
'service_status': 'Service Status', 'service_status': 'Service Status',
'running': 'Running', 'running': 'Running',
@ -329,10 +343,10 @@ MESSAGES = {
'service_stop_failed': 'Failed to stop service: {}', 'service_stop_failed': 'Failed to stop service: {}',
'service_restart_failed': 'Failed to restart service: {}', 'service_restart_failed': 'Failed to restart service: {}',
'invalid_token': 'Invalid access token', 'invalid_token': 'Invalid access token',
'config_file_changed': 'Configuration file change detected, reloading...', 'config_file_changed': 'Config file change detected, reloading...',
'proxy_file_changed': 'Proxy file changed, reloading...', 'proxy_file_changed': 'Proxy file changed, reloading...',
'test_target_label': 'Test Target URL', 'test_target_label': 'Test Target Address',
'invalid_test_target': 'Invalid test target URL', 'invalid_test_target': 'Invalid test target address',
'users_save_success': 'Users saved successfully', 'users_save_success': 'Users saved successfully',
'users_save_failed': 'Failed to save users: {}', 'users_save_failed': 'Failed to save users: {}',
'user_management_title': 'User Management', 'user_management_title': 'User Management',
@ -340,13 +354,13 @@ MESSAGES = {
'password_column': 'Password', 'password_column': 'Password',
'actions_column': 'Actions', 'actions_column': 'Actions',
'add_user_btn': 'Add User', 'add_user_btn': 'Add User',
'enter_username': 'Enter username', 'enter_username': 'Please enter username',
'enter_password': 'Enter password', 'enter_password': 'Please enter password',
'confirm_delete_user': 'Are you sure you want to delete this user?', 'confirm_delete_user': 'Are you sure you want to delete this user?',
'no_logs_found': 'No matching logs found', 'no_logs_found': 'No matching logs found',
'clear_search': 'Clear Search', 'clear_search': 'Clear Search',
'web_panel_url': 'Web control panel URL: {}', 'web_panel_url': 'Web panel URL: {}',
'web_panel_notice': 'Please use a browser to visit the above URL to manage the proxy server', 'web_panel_notice': 'Please use a browser to access the above URL to manage the proxy server',
'api_proxy_settings_title': 'API Proxy Settings', 'api_proxy_settings_title': 'API Proxy Settings',
} }
} }
@ -376,7 +390,7 @@ def print_banner(config):
banner_info = [ banner_info = [
(get_message('public_account', language), '樱花庄的本间白猫'), (get_message('public_account', language), '樱花庄的本间白猫'),
(get_message('blog', language), 'https://y.shironekosan.cn'), (get_message('blog', language), 'https://y.shironekosan.cn'),
(get_message('proxy_mode', language), get_message('cycle', language) if config.get('mode') == 'cycle' else get_message('load_balance', language) if config.get('mode') == 'load_balance' else get_message('single_round', language)), (get_message('proxy_mode', language), get_message('cycle', language) if config.get('mode') == 'cycle' else get_message('loadbalance', language) if config.get('mode') == 'loadbalance' else get_message('single_round', language)),
(get_message('proxy_interval', language), f"{config.get('interval')}{get_message('seconds', language)}"), (get_message('proxy_interval', language), f"{config.get('interval')}{get_message('seconds', language)}"),
(get_message('default_auth', language), auth_info), (get_message('default_auth', language), auth_info),
(get_message('local_http', language), http_addr), (get_message('local_http', language), http_addr),
@ -542,10 +556,9 @@ async def check_http_proxy(proxy, test_url=None):
except: except:
return False return False
async def check_https_proxy(proxy, test_url=None):
return await check_http_proxy(proxy, test_url)
async def check_socks_proxy(proxy, test_url=None): async def check_socks_proxy(proxy, test_url=None):
if test_url is None:
test_url = 'https://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
@ -605,7 +618,7 @@ async def check_proxy(proxy, test_url=None):
proxy_type = proxy.split('://')[0] proxy_type = proxy.split('://')[0]
check_funcs = { check_funcs = {
'http': check_http_proxy, 'http': check_http_proxy,
'https': check_https_proxy, 'https': check_http_proxy,
'socks5': check_socks_proxy 'socks5': check_socks_proxy
} }
@ -637,7 +650,7 @@ 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-V2.0.2" CURRENT_VERSION = "ProxyCat-V2.0.4"
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}{Style.RESET_ALL}") print(f"{Fore.YELLOW}{get_message('new_version_found', language)} 当前版本: {CURRENT_VERSION}, 最新版本: {latest_version}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}{get_message('visit_quark', language)}{Style.RESET_ALL}") print(f"{Fore.YELLOW}{get_message('visit_quark', language)}{Style.RESET_ALL}")

View File

@ -25,6 +25,7 @@ class AsyncProxyServer:
self._init_config_values(config) self._init_config_values(config)
self._init_server_state() self._init_server_state()
self._init_connection_settings() self._init_connection_settings()
self.proxy_failure_lock = asyncio.Lock()
def _init_config_values(self, config): def _init_config_values(self, config):
self.port = int(config.get('port', '1080')) self.port = int(config.get('port', '1080'))
@ -61,11 +62,11 @@ class AsyncProxyServer:
self.switch_cooldown = 5 self.switch_cooldown = 5
self.proxy_check_cache = {} self.proxy_check_cache = {}
self.last_check_time = {} self.last_check_time = {}
self.proxy_check_ttl = 300 self.proxy_check_ttl = 60
self.check_cooldown = 10 self.check_cooldown = 10
self.max_fail_count = 3
self.proxy_fail_count = 0
self.connected_clients = set() self.connected_clients = set()
self.last_proxy_failure_time = 0
self.proxy_failure_cooldown = 3
def _init_server_state(self): def _init_server_state(self):
self.running = False self.running = False
@ -73,7 +74,6 @@ class AsyncProxyServer:
self.server_instance = None self.server_instance = None
self.tasks = set() self.tasks = set()
self.last_switch_time = time.time() self.last_switch_time = time.time()
self.proxy_failed = False
self.proxy_cycle = None self.proxy_cycle = None
self.current_proxy = None self.current_proxy = None
self.proxies = [] self.proxies = []
@ -89,36 +89,82 @@ class AsyncProxyServer:
self.buffer_size = 8192 self.buffer_size = 8192
self.connection_timeout = 30 self.connection_timeout = 30
self.read_timeout = 60 self.read_timeout = 60
self.max_concurrent_requests = 50 self.max_concurrent_requests = 1000
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.max_pool_size = 100 self.max_pool_size = 500
self.client_pool = {}
self.client_pool_lock = asyncio.Lock()
self.proxy_pool = {}
self.active_connections = set()
def _update_config_values(self, new_config): def _update_config_values(self, new_config):
self._init_config_values(new_config) self._init_config_values(new_config)
self.last_switch_time = time.time() self.last_switch_time = time.time()
self.last_switch_attempt = 0
def _handle_mode_change(self): def _handle_mode_change(self):
self.last_switch_attempt = 0
if self.use_getip: if self.use_getip:
self.proxies = [] self.proxies = []
self.proxy_cycle = None self.proxy_cycle = None
self.current_proxy = None self.current_proxy = None
logging.info(get_message('api_mode_notice', self.language)) logging.info(get_message('api_mode_notice', self.language))
else: else:
logging.info(f"切换到{'负载均衡' if self.mode == 'loadbalance' else '循环模式'}模式,从 {self.proxy_file} 加载代理列表")
self.proxies = self._load_file_proxies() self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies: if self.proxies:
self.proxy_cycle = cycle(self.proxies) self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle) self.current_proxy = next(self.proxy_cycle)
if self.check_proxies: logging.info(f"当前使用代理: {self.current_proxy}")
asyncio.run(self._check_proxies())
if self.check_proxies and self.mode != 'loadbalance':
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self._check_proxies_wrapper())
else:
loop.run_until_complete(self._check_proxies())
except Exception as e:
logging.error(f"检查代理时出错: {str(e)}")
else:
logging.error(f"从文件 {self.proxy_file} 加载代理失败,请检查文件是否存在且包含有效代理")
async def _check_proxies_wrapper(self):
"""包装 _check_proxies 方法,用于在已运行的事件循环中调用"""
await self._check_proxies()
def _reload_proxies(self): def _reload_proxies(self):
self.last_switch_attempt = 0
logging.info(f"重新加载代理列表文件 {self.proxy_file}")
self.proxies = self._load_file_proxies() self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies: if self.proxies:
self.proxy_cycle = cycle(self.proxies) self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle) self.current_proxy = next(self.proxy_cycle)
logging.info(f"当前使用代理: {self.current_proxy}")
if self.check_proxies: if self.check_proxies:
asyncio.run(self._check_proxies()) try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self._check_proxies_wrapper())
else:
loop.run_until_complete(self._check_proxies())
except Exception as e:
logging.error(f"检查代理时出错: {str(e)}")
else:
logging.error(f"从文件 {self.proxy_file} 加载代理失败,请检查文件是否存在且包含有效代理")
async def _check_proxies(self): async def _check_proxies(self):
from modules.modules import check_proxies from modules.modules import check_proxies
@ -148,15 +194,44 @@ class AsyncProxyServer:
self.running = True self.running = True
try: try:
self.server_instance = await asyncio.start_server( sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 1024)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.bind(('0.0.0.0', self.port))
loop = asyncio.get_event_loop()
if hasattr(loop, 'set_default_executor'):
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor(max_workers=max(32, os.cpu_count() * 4))
loop.set_default_executor(executor)
server = await asyncio.start_server(
self.handle_client, self.handle_client,
'0.0.0.0', sock=sock,
self.port backlog=2048,
limit=32768,
) )
self.server_instance = server
logging.info(get_message('server_running', self.language, '0.0.0.0', self.port)) logging.info(get_message('server_running', self.language, '0.0.0.0', self.port))
async with self.server_instance: self.tasks.add(asyncio.create_task(self.cleanup_clients()))
await self.server_instance.serve_forever() self.tasks.add(asyncio.create_task(self._cleanup_pool()))
self.tasks.add(asyncio.create_task(self.cleanup_disconnected_ips()))
if hasattr(os, 'sched_setaffinity'):
try:
os.sched_setaffinity(0, range(os.cpu_count()))
except:
pass
async with server:
await server.serve_forever()
except Exception as e: except Exception as e:
if not self.stop_server: if not self.stop_server:
logging.error(get_message('server_start_error', self.language, str(e))) logging.error(get_message('server_start_error', self.language, str(e)))
@ -185,12 +260,32 @@ class AsyncProxyServer:
try: try:
current_time = time.time() current_time = time.time()
if self.interval != 0 and (self.switching_proxy or (current_time - self.last_switch_attempt < self.switch_cooldown)):
if self.mode == 'loadbalance' and self.proxies:
if not self.switching_proxy:
try:
self.switching_proxy = True
self.last_switch_attempt = current_time
if not self.use_getip:
if not self.proxy_cycle:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"负载均衡模式选择代理: {self.current_proxy}")
else:
await self.get_proxy()
finally:
self.switching_proxy = False
return self.current_proxy return self.current_proxy
if (self.use_getip and (not self.current_proxy or
current_time - self.last_switch_time >= self.interval)) or \ if self.switching_proxy or (current_time - self.last_switch_attempt < self.switch_cooldown):
(not self.use_getip and self.interval == 0): return self.current_proxy
if self.interval > 0 and current_time - self.last_switch_time >= self.interval or \
(self.use_getip and not self.current_proxy):
try: try:
self.switching_proxy = True self.switching_proxy = True
self.last_switch_attempt = current_time self.last_switch_attempt = current_time
@ -221,7 +316,7 @@ class AsyncProxyServer:
return valid_proxies[0] return valid_proxies[0]
def time_until_next_switch(self): def time_until_next_switch(self):
return float('inf') if self.mode == 'load_balance' else max(0, self.interval - (time.time() - self.last_switch_time)) return float('inf') if self.mode == 'loadbalance' else max(0, self.interval - (time.time() - self.last_switch_time))
def check_ip_auth(self, ip): def check_ip_auth(self, ip):
try: try:
@ -270,9 +365,25 @@ class AsyncProxyServer:
return False return False
async def _close_connection(self, writer):
try:
if writer and not writer.is_closing():
writer.write_eof()
await writer.drain()
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
except Exception:
pass
async def handle_client(self, reader, writer): async def handle_client(self, reader, writer):
task = asyncio.current_task() task = asyncio.current_task()
self.tasks.add(task) self.tasks.add(task)
peername = writer.get_extra_info('peername')
if peername:
self.active_connections.add(peername)
try: try:
peername = writer.get_extra_info('peername') peername = writer.get_extra_info('peername')
if peername: if peername:
@ -295,25 +406,37 @@ class AsyncProxyServer:
except Exception as e: except Exception as e:
logging.error(get_message('client_handle_error', self.language, e)) logging.error(get_message('client_handle_error', self.language, e))
finally: finally:
try: if peername:
writer.close() self.active_connections.discard(peername)
await writer.wait_closed() await self._close_connection(writer)
except:
pass
self.tasks.remove(task) self.tasks.remove(task)
async def _pipe(self, reader, writer): async def _pipe(self, reader, writer):
try: try:
while True: while True:
try:
data = await reader.read(self.buffer_size) data = await reader.read(self.buffer_size)
if not data: if not data:
break break
try:
writer.write(data) writer.write(data)
await writer.drain() await writer.drain()
except (ConnectionError, ConnectionResetError):
await self.handle_proxy_failure()
break
except (ConnectionError, ConnectionResetError):
await self.handle_proxy_failure()
break
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except Exception as e: except Exception as e:
logging.error(get_message('data_transfer_error', self.language, e))
await self.handle_proxy_failure()
pass
finally:
await self._close_connection(writer)
def _split_proxy_auth(self, proxy_addr): def _split_proxy_auth(self, proxy_addr):
match = re.match(r'((?P<username>.+?):(?P<password>.+?)@)?(?P<host>.+)', proxy_addr) match = re.match(r'((?P<username>.+?):(?P<password>.+?)@)?(?P<host>.+)', proxy_addr)
@ -338,7 +461,12 @@ class AsyncProxyServer:
else: else:
proxy_url = f"{proxy_type}://{proxy_addr}" proxy_url = f"{proxy_type}://{proxy_addr}"
client = httpx.AsyncClient( import logging as httpx_logging
httpx_logging.getLogger("httpx").setLevel(logging.WARNING)
httpx_logging.getLogger("hpack").setLevel(logging.WARNING)
httpx_logging.getLogger("h2").setLevel(logging.WARNING)
return httpx.AsyncClient(
proxies={"all://": proxy_url}, proxies={"all://": proxy_url},
limits=httpx.Limits( limits=httpx.Limits(
max_keepalive_connections=100, max_keepalive_connections=100,
@ -347,10 +475,9 @@ class AsyncProxyServer:
), ),
timeout=30.0, timeout=30.0,
http2=True, http2=True,
verify=False verify=False,
follow_redirects=True
) )
client._last_used = time.time()
return client
async def _cleanup_connections(self): async def _cleanup_connections(self):
current_time = time.time() current_time = time.time()
@ -423,7 +550,7 @@ class AsyncProxyServer:
dst_port = struct.unpack('!H', await reader.readexactly(2))[0] dst_port = struct.unpack('!H', await reader.readexactly(2))[0]
max_retries = 3 max_retries = 1
retry_count = 0 retry_count = 0
last_error = None last_error = None
@ -456,7 +583,6 @@ class AsyncProxyServer:
self._pipe(remote_reader, writer) self._pipe(remote_reader, writer)
) )
self.proxy_failed = False
return return
except (asyncio.TimeoutError, ConnectionRefusedError, ConnectionResetError) as e: except (asyncio.TimeoutError, ConnectionRefusedError, ConnectionResetError) as e:
@ -477,8 +603,8 @@ class AsyncProxyServer:
await asyncio.sleep(1) await asyncio.sleep(1)
continue continue
if last_error: #if last_error:
logging.error(get_message('all_retries_failed', self.language, str(last_error))) #logging.error(get_message('all_retries_failed', self.language, str(last_error)))
writer.write(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00') writer.write(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00')
await writer.drain() await writer.drain()
@ -600,6 +726,8 @@ class AsyncProxyServer:
async def _handle_client_impl(self, reader, writer, first_byte): async def _handle_client_impl(self, reader, writer, first_byte):
try: try:
peername = writer.get_extra_info('peername') peername = writer.get_extra_info('peername')
client_info = f"{peername[0]}:{peername[1]}" if peername else "未知客户端"
if peername: if peername:
client_ip = peername[0] client_ip = peername[0]
if not self.check_ip_auth(client_ip): if not self.check_ip_auth(client_ip):
@ -614,7 +742,7 @@ class AsyncProxyServer:
try: try:
method, path, _ = request_line.decode('utf-8', errors='ignore').split() method, path, _ = request_line.decode('utf-8', errors='ignore').split()
except ValueError: except (ValueError, UnicodeDecodeError) as e:
return return
headers = {} headers = {}
@ -651,10 +779,14 @@ class AsyncProxyServer:
else: else:
await self._handle_request(method, path, headers, reader, writer) await self._handle_request(method, path, headers, reader, writer)
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except asyncio.CancelledError: except asyncio.CancelledError:
raise return
except Exception as e: except Exception as e:
logging.error(get_message('client_request_error', self.language, e)) if not isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError,
asyncio.CancelledError, asyncio.TimeoutError)):
logging.error(get_message('client_request_error', self.language, str(e)))
async def _handle_connect(self, path, reader, writer): async def _handle_connect(self, path, reader, writer):
try: try:
@ -665,6 +797,12 @@ class AsyncProxyServer:
await writer.drain() await writer.drain()
return return
max_retries = 1
retry_count = 0
last_error = None
while retry_count < max_retries:
try:
proxy = await self.get_next_proxy() proxy = await self.get_next_proxy()
if not proxy: if not proxy:
writer.write(b'HTTP/1.1 503 Service Unavailable\r\n\r\n') writer.write(b'HTTP/1.1 503 Service Unavailable\r\n\r\n')
@ -692,6 +830,14 @@ class AsyncProxyServer:
await remote_writer.drain() await remote_writer.drain()
response = await remote_reader.readline() response = await remote_reader.readline()
if not response.startswith(b'HTTP/1.1 200'): if not response.startswith(b'HTTP/1.1 200'):
await self.handle_proxy_failure()
last_error = f"Bad Gateway: {response.decode('utf-8', errors='ignore')}"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
raise Exception("Bad Gateway") raise Exception("Bad Gateway")
while (await remote_reader.readline()) != b'\r\n': while (await remote_reader.readline()) != b'\r\n':
pass pass
@ -702,6 +848,14 @@ class AsyncProxyServer:
remote_writer.write(b'\x05\x01\x00\x03' + len(host).to_bytes(1, 'big') + host.encode() + port.to_bytes(2, 'big')) remote_writer.write(b'\x05\x01\x00\x03' + len(host).to_bytes(1, 'big') + host.encode() + port.to_bytes(2, 'big'))
await remote_writer.drain() await remote_writer.drain()
if (await remote_reader.read(10))[1] != 0: if (await remote_reader.read(10))[1] != 0:
await self.handle_proxy_failure()
last_error = "SOCKS5 connection failed"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
raise Exception("Bad Gateway") raise Exception("Bad Gateway")
else: else:
raise Exception("Unsupported proxy type") raise Exception("Unsupported proxy type")
@ -713,22 +867,58 @@ class AsyncProxyServer:
self._pipe(reader, remote_writer), self._pipe(reader, remote_writer),
self._pipe(remote_reader, writer) self._pipe(remote_reader, writer)
) )
return
except asyncio.TimeoutError: except asyncio.TimeoutError:
await self.handle_proxy_failure()
last_error = "Connection Timeout"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
logging.error(get_message('connect_timeout', self.language)) logging.error(get_message('connect_timeout', self.language))
writer.write(b'HTTP/1.1 504 Gateway Timeout\r\n\r\n') writer.write(b'HTTP/1.1 504 Gateway Timeout\r\n\r\n')
await writer.drain() await writer.drain()
return
except Exception as e: except Exception as e:
logging.error(get_message('proxy_invalid_switch', self.language))
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n') writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain() await writer.drain()
if not self.proxy_failed: return
self.proxy_failed = True
await self.get_proxy() except Exception as e:
else: last_error = str(e)
self.proxy_failed = False retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
#if last_error:
#logging.error(get_message('all_retries_failed', self.language, last_error))
async def _handle_request(self, method, path, headers, reader, writer): async def _handle_request(self, method, path, headers, reader, writer):
async with self.request_semaphore: async with self.request_semaphore:
max_retries = 1
retry_count = 0
last_error = None
while retry_count < max_retries:
try: try:
proxy = await self.get_next_proxy() proxy = await self.get_next_proxy()
if not proxy: if not proxy:
@ -736,7 +926,8 @@ class AsyncProxyServer:
await writer.drain() await writer.drain()
return return
key = f"{proxy}:{path}" try:
client = await self._get_client(proxy)
proxy_headers = headers.copy() proxy_headers = headers.copy()
proxy_type, proxy_addr = proxy.split('://') proxy_type, proxy_addr = proxy.split('://')
@ -745,79 +936,228 @@ class AsyncProxyServer:
auth_header = f'Basic {base64.b64encode(auth.encode()).decode()}' auth_header = f'Basic {base64.b64encode(auth.encode()).decode()}'
proxy_headers['Proxy-Authorization'] = auth_header proxy_headers['Proxy-Authorization'] = auth_header
if key in self.connection_pool: try:
client = self.connection_pool[key]
else:
client = await self._create_client(proxy)
self.connection_pool[key] = client
async with client.stream( async with client.stream(
method, method,
path, path,
headers=proxy_headers, headers=proxy_headers,
content=reader, content=reader,
timeout=30.0
) as response: ) as response:
writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode()) writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode())
for header_name, header_value in response.headers.items(): for header_name, header_value in response.headers.items():
if header_name.lower() != 'transfer-encoding': if header_name.lower() not in ('transfer-encoding', 'connection'):
writer.write(f'{header_name}: {header_value}\r\n'.encode()) writer.write(f'{header_name}: {header_value}\r\n'.encode())
writer.write(b'\r\n') writer.write(b'\r\n')
try:
async for chunk in response.aiter_bytes(chunk_size=self.buffer_size): async for chunk in response.aiter_bytes(chunk_size=self.buffer_size):
if not chunk:
break
try:
writer.write(chunk) writer.write(chunk)
if len(chunk) >= self.buffer_size: if len(chunk) >= self.buffer_size:
await writer.drain() await writer.drain()
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except Exception:
break
await writer.drain() await writer.drain()
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except Exception:
pass
return
except httpx.RequestError:
await self.handle_proxy_failure()
last_error = "Request Error"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
except Exception as e: except Exception as e:
logging.error(get_message('request_handling_error', self.language, str(e))) if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n') writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain() await writer.drain()
finally: return
await self._cleanup_connections()
except httpx.HTTPError:
await self.handle_proxy_failure()
last_error = "HTTP Error"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
except Exception as e:
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
except Exception as e:
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
if not isinstance(e, (asyncio.CancelledError,)):
logging.error(f"请求处理错误: {str(e)}")
try:
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
except:
pass
return
#if last_error:
logging.error(get_message('all_retries_failed', self.language, last_error))
async def _get_client(self, proxy):
async with self.client_pool_lock:
current_time = time.time()
if proxy in self.client_pool:
client, last_used = self.client_pool[proxy]
if current_time - last_used < 30 and not client.is_closed:
self.client_pool[proxy] = (client, current_time)
return client
else:
await client.aclose()
del self.client_pool[proxy]
try:
client = await self._create_client(proxy)
if len(self.client_pool) >= self.max_pool_size:
oldest_proxy = min(self.client_pool, key=lambda x: self.client_pool[x][1])
old_client, _ = self.client_pool[oldest_proxy]
await old_client.aclose()
del self.client_pool[oldest_proxy]
self.client_pool[proxy] = (client, current_time)
return client
except Exception as e:
logging.error(f"创建客户端失败: {str(e)}")
raise
async def handle_proxy_failure(self): async def handle_proxy_failure(self):
if not self.check_proxies:
return
current_time = time.time()
if current_time - self.last_proxy_failure_time < self.proxy_failure_cooldown:
return
if self.switching_proxy:
return
try:
if not self.proxy_failure_lock.locked():
async with self.proxy_failure_lock:
if (current_time - self.last_proxy_failure_time < self.proxy_failure_cooldown or
self.switching_proxy):
return
self.last_proxy_failure_time = current_time
try:
is_valid = await self.check_current_proxy()
if not is_valid:
#logging.warning(get_message('proxy_failure', self.language, self.current_proxy))
await self.switch_proxy()
except Exception as e:
logging.error(get_message('proxy_check_error', self.language, str(e)))
except Exception as e:
logging.error(f"代理失败处理出错: {str(e)}")
async def switch_proxy(self):
try: try:
current_time = time.time() current_time = time.time()
if self.switching_proxy or (current_time - self.last_switch_attempt < self.switch_cooldown):
return
self.proxy_fail_count += 1 if current_time - self.last_switch_attempt < self.switch_cooldown:
return False
if self.switching_proxy:
return False
if self.proxy_fail_count >= self.max_fail_count:
current_proxy = self.current_proxy if self.current_proxy else get_message('no_proxy', self.language)
logging.warning(get_message('proxy_consecutive_fails', self.language,
current_proxy, self.proxy_fail_count))
try:
self.switching_proxy = True self.switching_proxy = True
self.last_switch_attempt = current_time self.last_switch_attempt = current_time
old_proxy = current_proxy old_proxy = self.current_proxy
temp_current_proxy = self.current_proxy
await self.get_proxy() await self.get_proxy()
self.proxy_fail_count = 0
self.proxy_failed = False
finally: if temp_current_proxy != self.current_proxy:
self.switching_proxy = False self._log_proxy_switch(old_proxy, self.current_proxy)
else:
current_proxy = self.current_proxy if self.current_proxy else get_message('no_proxy', self.language) self.last_proxy_failure_time = current_time
logging.warning(get_message('request_retry', self.language, return True
self.max_fail_count - self.proxy_fail_count))
return False
except Exception as e: except Exception as e:
current_proxy = self.current_proxy if self.current_proxy else get_message('no_proxy', self.language) logging.error(get_message('proxy_switch_error', self.language, str(e)))
logging.error(get_message('proxy_invalid', self.language, current_proxy)) return False
finally:
self.switching_proxy = False self.switching_proxy = False
async def check_current_proxy(self): async def check_current_proxy(self):
try: try:
proxy = self.current_proxy proxy = self.current_proxy
if not proxy:
return False
current_time = time.time() current_time = time.time()
if not self.check_proxies:
return True
if proxy in self.last_check_time: if proxy in self.last_check_time:
if current_time - self.last_check_time[proxy] < self.check_cooldown: if current_time - self.last_check_time[proxy] < self.check_cooldown:
return self.proxy_check_cache.get(proxy, (current_time, True))[1] return self.proxy_check_cache.get(proxy, (current_time, True))[1]
@ -828,21 +1168,25 @@ class AsyncProxyServer:
return is_valid return is_valid
self.last_check_time[proxy] = current_time self.last_check_time[proxy] = current_time
test_url = self.config.get('test_url', 'https://www.baidu.com') test_url = self.config.get('test_url', 'https://www.baidu.com')
proxy_type = proxy.split('://')[0]
async with httpx.AsyncClient( try:
proxies={f"{proxy_type}://": proxy}, from modules.modules import check_proxy
timeout=10,
verify=False is_valid = await check_proxy(proxy, test_url)
) as client: logging.warning(f"代理检查结果: {proxy} - {'有效' if is_valid else '无效'}")
response = await client.get(test_url) except Exception as e:
is_valid = response.status_code == 200 logging.error(f"代理检测错误: {proxy} - {str(e)}")
is_valid = False
self.proxy_check_cache[proxy] = (current_time, is_valid) self.proxy_check_cache[proxy] = (current_time, is_valid)
return is_valid return is_valid
except Exception: except Exception as e:
logging.error(f"代理检测异常: {str(e)}")
if 'proxy' in locals():
self.proxy_check_cache[proxy] = (current_time, False) self.proxy_check_cache[proxy] = (current_time, False)
return False return False
@ -860,39 +1204,40 @@ class AsyncProxyServer:
} }
def initialize_proxies(self): def initialize_proxies(self):
if self.mode == 'cycle':
if hasattr(self, 'proxies') and self.proxies: if hasattr(self, 'proxies') and self.proxies:
self.proxy_cycle = cycle(self.proxies) self.proxy_cycle = cycle(self.proxies)
elif self.use_getip: return
pass
else: if self.use_getip:
logging.info("API模式将在请求时动态获取代理")
return
try: try:
with open(self.proxy_file, 'r') as f: logging.info(f"从文件 {self.proxy_file} 加载代理列表")
self.proxies = [line.strip() for line in f if line.strip()] self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies: if self.proxies:
self.proxy_cycle = cycle(self.proxies) self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"初始代理: {self.current_proxy}")
except Exception as e: except Exception as e:
logging.error(get_message('load_proxy_file_error', self.language, str(e))) logging.error(f"初始化代理列表失败: {str(e)}")
async def cleanup_disconnected_ips(self): async def cleanup_disconnected_ips(self):
while True: while True:
try: try:
active_ips = set() active_ips = {addr[0] for addr in self.active_connections}
for client_info in self.get_active_connections():
active_ips.add(client_info[0])
self.connected_clients = active_ips self.connected_clients = active_ips
except Exception as e: except Exception as e:
logging.error(get_message('cleanup_error', self.language, str(e))) logging.error(get_message('cleanup_error', self.language, str(e)))
await asyncio.sleep(30) await asyncio.sleep(30)
def is_docker(): def is_docker():
return os.path.exists('/.dockerenv') return os.path.exists('/.dockerenv')
async def get_proxy_status(self): async def get_proxy_status(self):
if self.mode == 'load_balance': if self.mode == 'loadbalance':
return f"{get_message('current_proxy', self.language)}: {self.current_proxy}" return f"{get_message('current_proxy', self.language)}: {self.current_proxy}"
else: else:
time_left = self.time_until_next_switch() time_left = self.time_until_next_switch()
@ -979,8 +1324,17 @@ class AsyncProxyServer:
async def _cleanup_pool(self): async def _cleanup_pool(self):
while True: while True:
try: try:
def is_expired(conn):
return hasattr(conn, 'is_closed') and conn.is_closed
to_remove = []
for proxy, conn in list(self.proxy_pool.items()): for proxy, conn in list(self.proxy_pool.items()):
if conn.is_closed: if is_expired(conn):
to_remove.append(proxy)
for proxy in to_remove:
if proxy in self.proxy_pool:
del self.proxy_pool[proxy] del self.proxy_pool[proxy]
except Exception as e: except Exception as e:
logging.error(f'连接池清理错误: {e}') logging.error(f'连接池清理错误: {e}')
@ -990,12 +1344,24 @@ class AsyncProxyServer:
if old_proxy != new_proxy: if old_proxy != new_proxy:
old_proxy = old_proxy if old_proxy else get_message('no_proxy', self.language) old_proxy = old_proxy if old_proxy else get_message('no_proxy', self.language)
new_proxy = new_proxy if new_proxy else get_message('no_proxy', self.language) new_proxy = new_proxy if new_proxy else get_message('no_proxy', self.language)
current_time = time.time()
if not hasattr(self, '_last_log_time') or \
not hasattr(self, '_last_log_content') or \
current_time - self._last_log_time > 1 or \
self._last_log_content != f"{old_proxy} -> {new_proxy}":
logging.info(get_message('proxy_switch', self.language, old_proxy, new_proxy)) logging.info(get_message('proxy_switch', self.language, old_proxy, new_proxy))
self._last_log_time = current_time
self._last_log_content = f"{old_proxy} -> {new_proxy}"
async def _validate_proxy(self, proxy): async def _validate_proxy(self, proxy):
if not proxy: if not proxy:
return False return False
if not self.check_proxies:
return True
try: try:
if not validate_proxy(proxy): if not validate_proxy(proxy):
logging.warning(get_message('proxy_invalid', self.language, proxy)) logging.warning(get_message('proxy_invalid', self.language, proxy))
@ -1027,6 +1393,7 @@ class AsyncProxyServer:
async def get_proxy(self): async def get_proxy(self):
try: try:
old_proxy = self.current_proxy old_proxy = self.current_proxy
temp_current_proxy = self.current_proxy
if not self.use_getip and self.proxies: if not self.use_getip and self.proxies:
if not self.proxy_cycle: if not self.proxy_cycle:
@ -1037,6 +1404,7 @@ class AsyncProxyServer:
if await self._validate_proxy(new_proxy): if await self._validate_proxy(new_proxy):
self.current_proxy = new_proxy self.current_proxy = new_proxy
self.last_switch_time = time.time() self.last_switch_time = time.time()
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy) self._log_proxy_switch(old_proxy, self.current_proxy)
return self.current_proxy return self.current_proxy
@ -1047,8 +1415,10 @@ class AsyncProxyServer:
try: try:
new_proxy = await self._load_getip_proxy() new_proxy = await self._load_getip_proxy()
if new_proxy and await self._validate_proxy(new_proxy): if new_proxy and await self._validate_proxy(new_proxy):
self.current_proxy = new_proxy self.current_proxy = new_proxy
self.last_switch_time = time.time() self.last_switch_time = time.time()
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy) self._log_proxy_switch(old_proxy, self.current_proxy)
return self.current_proxy return self.current_proxy
else: else:
@ -1061,3 +1431,36 @@ class AsyncProxyServer:
except Exception as e: except Exception as e:
logging.error(get_message('proxy_get_error', self.language, str(e))) logging.error(get_message('proxy_get_error', self.language, str(e)))
return self.current_proxy return self.current_proxy
async def cleanup_clients(self):
while True:
try:
async with self.client_pool_lock:
current_time = time.time()
expired_proxies = [
proxy for proxy, (_, last_used) in self.client_pool.items()
if current_time - last_used > 30
]
for proxy in expired_proxies:
client, _ = self.client_pool[proxy]
await client.aclose()
del self.client_pool[proxy]
except Exception as e:
logging.error(f"清理客户端池错误: {str(e)}")
await asyncio.sleep(30)
def get_active_connections(self):
active = []
for task in self.tasks:
if not task.done():
try:
coro = task.get_coro()
if coro.__qualname__.startswith('AsyncProxyServer.handle_client'):
writer = coro.cr_frame.f_locals.get('writer')
if writer:
peername = writer.get_extra_info('peername')
if peername:
active.append(peername)
except Exception:
continue
return active

View File

@ -6,3 +6,5 @@ Requests==2.32.3
tqdm>=4.65.0 tqdm>=4.65.0
flask>=2.0.1 flask>=2.0.1
werkzeug>=2.0.0 werkzeug>=2.0.0
asyncio>=3.4.3
configparser>=5.0.0

7
web/static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
web/static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1329,7 +1329,7 @@
<label class="form-label" data-i18n="run_mode_label">运行模式</label> <label class="form-label" data-i18n="run_mode_label">运行模式</label>
<select class="form-select" name="mode"> <select class="form-select" name="mode">
<option value="cycle" data-i18n="cycle_mode">循环模式</option> <option value="cycle" data-i18n="cycle_mode">循环模式</option>
<option value="load_balance" data-i18n="load_balance_mode">负载均衡</option> <option value="loadbalance" data-i18n="loadbalance_mode">负载均衡</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -1375,7 +1375,7 @@
<div class="mb-3"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="check_proxies" id="check-proxies"> <input class="form-check-input" type="checkbox" name="check_proxies" id="check-proxies">
<label class="form-check-label" for="check-proxies" data-i18n="enable_proxy_check_label">启用代理检测</label> <label class="form-check-label" for="check-proxies" data-i18n="enable_proxy_check_label">代理有效性检测</label>
</div> </div>
</div> </div>
</form> </form>
@ -1590,8 +1590,8 @@
<div class="notification-container" id="notification-container"></div> <div class="notification-container" id="notification-container"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script> <script src="/static/js/jquery.min.js"></script>
<script> <script>
let lastLogId = 0; let lastLogId = 0;
let searchTimeout = null; let searchTimeout = null;
@ -1630,7 +1630,13 @@
$.get(appendToken('/api/status'), function(data) { $.get(appendToken('/api/status'), function(data) {
// 只在值发生变化时更新显示 // 只在值发生变化时更新显示
const currentProxy = $('#current-proxy').text(); const currentProxy = $('#current-proxy').text();
const newProxy = data.current_proxy || translations[currentLanguage].no_proxy; let newProxy = data.current_proxy || translations[currentLanguage].no_proxy;
// 负载均衡模式特殊处理
if (data.mode === 'loadbalance') {
newProxy = translations[currentLanguage].loadbalance_mode || '负载均衡模式';
}
if (currentProxy !== newProxy) { if (currentProxy !== newProxy) {
$('#current-proxy').text(newProxy); $('#current-proxy').text(newProxy);
} }
@ -1651,7 +1657,11 @@
} }
// 更新进度条和其他状态... // 更新进度条和其他状态...
if (data.interval && data.time_left !== undefined) { if (data.mode === 'loadbalance') {
// 负载均衡模式不显示倒计时
$('#next-switch').text(translations[currentLanguage].loadbalance_mode || '负载均衡模式');
$('.progress-bar').css('width', '100%');
} else if (data.interval && data.time_left !== undefined) {
const timeLeft = Math.max(0, Math.ceil(data.time_left)); const timeLeft = Math.max(0, Math.ceil(data.time_left));
$('#next-switch').text(`${timeLeft} ${translations[currentLanguage].seconds}`); $('#next-switch').text(`${timeLeft} ${translations[currentLanguage].seconds}`);
@ -1673,13 +1683,20 @@
function updateCountdown() { function updateCountdown() {
$.get(appendToken('/api/status'), function(data) { $.get(appendToken('/api/status'), function(data) {
if (data.time_left !== undefined && data.interval) { if (data.time_left !== undefined) {
// 负载均衡模式特殊处理
if (data.time_left === -1 || data.mode === 'loadbalance') {
// 负载均衡模式不显示倒计时
$('.progress-bar').css('width', '100%');
$('#next-switch').text(translations[currentLanguage]['loadbalance_mode'] || '负载均衡模式');
} else if (data.interval) {
const timeLeft = Math.max(0, Math.ceil(data.time_left)); const timeLeft = Math.max(0, Math.ceil(data.time_left));
const progress = Math.min(100, ((data.interval - data.time_left) / data.interval) * 100); const progress = Math.min(100, ((data.interval - data.time_left) / data.interval) * 100);
$('.progress-bar').css('width', progress + '%'); $('.progress-bar').css('width', progress + '%');
$('#next-switch').text(timeLeft + ' ' + translations[currentLanguage]['seconds']); $('#next-switch').text(timeLeft + ' ' + translations[currentLanguage]['seconds']);
} }
}
}); });
} }
@ -1688,31 +1705,34 @@
if (data.config) { if (data.config) {
const config = data.config; const config = data.config;
// 更新表单值 // 更新表单值 - 始终使用配置文件中的值避免空值
$('input[name="port"]').val(config.port); $('input[name="port"]').val(config.port || '1080');
$('select[name="mode"]').val(config.mode); $('select[name="mode"]').val(config.mode || 'cycle');
$('input[name="interval"]').val(config.interval); $('input[name="interval"]').val(config.interval || '300');
$('input[name="username"]').val(config.username); $('input[name="username"]').val(config.username || '');
$('input[name="password"]').val(config.password); $('input[name="password"]').val(config.password || '');
// 修正checkbox的状态设置 // 修正checkbox的状态设置
$('input[name="use_getip"]').prop('checked', config.use_getip.toLowerCase() === 'true'); $('input[name="use_getip"]').prop('checked', (config.use_getip || '').toLowerCase() === 'true');
$('input[name="getip_url"]').val(config.getip_url); $('input[name="getip_url"]').val(config.getip_url || '');
$('input[name="proxy_username"]').val(config.proxy_username); $('input[name="proxy_username"]').val(config.proxy_username || '');
$('input[name="proxy_password"]').val(config.proxy_password); $('input[name="proxy_password"]').val(config.proxy_password || '');
$('input[name="proxy_file"]').val(config.proxy_file); $('input[name="proxy_file"]').val(config.proxy_file || 'ip.txt');
$('input[name="check_proxies"]').prop('checked', config.check_proxies.toLowerCase() === 'true'); $('input[name="check_proxies"]').prop('checked', (config.check_proxies || '').toLowerCase() === 'true');
$('select[name="language"]').val(config.language); $('select[name="language"]').val(config.language || 'cn');
$('input[name="whitelist_file"]').val(config.whitelist_file); $('input[name="whitelist_file"]').val(config.whitelist_file || '');
$('input[name="blacklist_file"]').val(config.blacklist_file); $('input[name="blacklist_file"]').val(config.blacklist_file || '');
$('select[name="ip_auth_priority"]').val(config.ip_auth_priority); $('select[name="ip_auth_priority"]').val(config.ip_auth_priority || 'whitelist');
$('select[name="display_level"]').val(config.display_level); $('select[name="display_level"]').val(config.display_level || '1');
// 根据 use_getip 的状态更新 getip_url 输入框的禁用状态 // 根据 use_getip 的状态更新 getip_url 输入框的禁用状态
const useGetip = config.use_getip.toLowerCase() === 'true'; const useGetip = (config.use_getip || '').toLowerCase() === 'true';
$('input[name="getip_url"]').prop('disabled', !useGetip); $('input[name="getip_url"]').prop('disabled', !useGetip);
// 更新其他相关UI元素 // 更新其他相关UI元素
updateUIState(); updateUIState();
// 更新地址显示
updateAddresses(config);
} }
}); });
} }
@ -2146,7 +2166,7 @@
'switch_interval': '切换间隔(秒)', 'switch_interval': '切换间隔(秒)',
'run_mode_text': '运行模式', 'run_mode_text': '运行模式',
'cycle_mode': '循环模式', 'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡', 'loadbalance_mode': '负载均衡',
'language_text': '语言', 'language_text': '语言',
'auth_config': '认证配置', 'auth_config': '认证配置',
'username': '用户名', 'username': '用户名',
@ -2196,11 +2216,11 @@
'switch_interval_label': '切换间隔(秒)', 'switch_interval_label': '切换间隔(秒)',
'run_mode_label': '运行模式', 'run_mode_label': '运行模式',
'cycle_mode': '循环模式', 'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡', 'loadbalance_mode': '负载均衡',
'auth_config_title': '认证配置', 'auth_config_title': '认证配置',
'use_api_label': '使用API获取代理', 'use_api_label': '使用API获取代理',
'api_url_label': 'API地址', 'api_url_label': 'API地址',
'enable_proxy_check_label': '启用代理检测', 'enable_proxy_check_label': '代理有效性检测',
'proxy_list_label': '代理列表', 'proxy_list_label': '代理列表',
'save_proxy_btn': '保存代理', 'save_proxy_btn': '保存代理',
'check_proxy_btn': '检测代理', 'check_proxy_btn': '检测代理',
@ -2301,11 +2321,11 @@
'loading': '加载中...', 'loading': '加载中...',
'manual_switch_btn': '手动切换', 'manual_switch_btn': '手动切换',
'cycle_mode': '循环模式', 'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡', 'loadbalance_mode': '负载均衡',
'next_switch': '下次切换', 'next_switch': '下次切换',
'proxy_mode': { 'proxy_mode': {
'cycle': '循环模式', 'cycle': '循环模式',
'load_balance': '负载均衡' 'loadbalance': '负载均衡'
}, },
'service_action': { 'service_action': {
'start_success': '服务启动成功', 'start_success': '服务启动成功',
@ -2373,7 +2393,7 @@
'switch_interval': 'Switch Interval(s)', 'switch_interval': 'Switch Interval(s)',
'run_mode_text': 'Run Mode', 'run_mode_text': 'Run Mode',
'cycle_mode': 'Cycle Mode', 'cycle_mode': 'Cycle Mode',
'load_balance_mode': 'Load Balance', 'loadbalance_mode': 'Load Balance',
'language_text': 'Language', 'language_text': 'Language',
'auth_config': 'Auth Config', 'auth_config': 'Auth Config',
'username': 'Username', 'username': 'Username',
@ -2424,11 +2444,11 @@
'switch_interval_label': 'Switch Interval(s)', 'switch_interval_label': 'Switch Interval(s)',
'run_mode_label': 'Run Mode', 'run_mode_label': 'Run Mode',
'cycle_mode': 'Cycle Mode', 'cycle_mode': 'Cycle Mode',
'load_balance_mode': 'Load Balance', 'loadbalance_mode': 'Load Balance',
'auth_config_title': 'Authentication', 'auth_config_title': 'Authentication',
'use_api_label': 'Use API for Proxy', 'use_api_label': 'Use API for Proxy',
'api_url_label': 'API URL', 'api_url_label': 'API URL',
'enable_proxy_check_label': 'Enable Proxy Check', 'enable_proxy_check_label': 'Proxy Validity Check',
'proxy_list_label': 'Proxy List', 'proxy_list_label': 'Proxy List',
'save_proxy_btn': 'Save Proxies', 'save_proxy_btn': 'Save Proxies',
'check_proxy_btn': 'Check Proxies', 'check_proxy_btn': 'Check Proxies',