ProxyCat/ProxyCat.py

345 lines
14 KiB
Python
Raw Normal View History

2025-02-21 16:48:20 +08:00
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
2025-01-07 14:41:40 +08:00
import threading, argparse, logging, asyncio, time, socket, signal, sys, os
from concurrent.futures import ThreadPoolExecutor
2025-01-02 17:14:36 +08:00
from modules.proxyserver import AsyncProxyServer
from colorama import init, Fore, Style
from itertools import cycle
2025-02-21 16:48:20 +08:00
from tqdm import tqdm
import base64
from configparser import ConfigParser
2025-01-02 17:14:36 +08:00
init(autoreset=True)
log_format = '%(asctime)s - %(levelname)s - %(message)s'
formatter = ColoredFormatter(log_format)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logging.basicConfig(level=logging.INFO, handlers=[console_handler])
def update_status(server):
2025-02-21 16:48:20 +08:00
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
2025-01-02 17:14:36 +08:00
while True:
2025-01-07 15:38:23 +08:00
try:
2025-02-21 16:48:20 +08:00
if os.path.exists(config_file):
current_config_modified_time = os.path.getmtime(config_file)
if current_config_modified_time > last_config_modified_time:
logging.info(get_message('config_file_changed', server.language))
new_config = load_config(config_file)
reload_server_config(new_config)
last_config_modified_time = current_config_modified_time
continue
if os.path.exists(ip_file) and not server.use_getip:
current_ip_modified_time = os.path.getmtime(ip_file)
if current_ip_modified_time > last_ip_modified_time:
logging.info(get_message('proxy_file_changed', server.language))
server.proxies = server._load_file_proxies()
if server.proxies:
server.proxy_cycle = cycle(server.proxies)
server.current_proxy = next(server.proxy_cycle)
if server.check_proxies:
asyncio.run(run_proxy_check(server))
last_ip_modified_time = current_ip_modified_time
continue
if display_level == 0:
if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy:
print_proxy_info()
server.last_proxy = server.current_proxy
time.sleep(1)
continue
2025-01-07 15:38:23 +08:00
if server.mode == 'load_balance':
2025-02-21 16:48:20 +08:00
if display_level >= 1:
print_proxy_info()
time.sleep(5)
continue
2025-01-07 15:38:23 +08:00
2025-02-21 16:48:20 +08:00
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
if is_docker:
if not hasattr(server, 'last_update_time') or \
(time.time() - server.last_update_time >= (5 if display_level == 1 else 1) and elapsed_time <= total_time):
if display_level >= 2:
logging.info(f"{get_message('next_switch', server.language)}: {time_left:.0f} {get_message('seconds', server.language)} ({elapsed_time}/{total_time})")
else:
logging.info(f"{get_message('next_switch', server.language)}: {time_left:.0f} {get_message('seconds', server.language)}")
server.last_update_time = time.time()
else:
if not hasattr(server, 'progress_bar'):
server.progress_bar = tqdm(
total=total_time,
desc=f"{Fore.YELLOW}{get_message('next_switch', server.language)}{Style.RESET_ALL}",
bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} ' + get_message('seconds', server.language),
colour='green'
)
server.progress_bar.n = min(elapsed_time, total_time)
server.progress_bar.refresh()
2025-01-07 15:38:23 +08:00
2025-02-21 16:48:20 +08:00
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))
2025-01-02 17:14:36 +08:00
time.sleep(1)
async def handle_client_wrapper(server, reader, writer, clients):
task = asyncio.create_task(server.handle_client(reader, writer))
clients.add(task)
try:
await task
except Exception as e:
logging.error(get_message('client_handle_error', server.language, e))
finally:
clients.remove(task)
async def run_server(server):
try:
2025-02-21 16:48:20 +08:00
await server.start()
2025-01-02 17:14:36 +08:00
except asyncio.CancelledError:
logging.info(get_message('server_closing', server.language))
2025-02-21 16:48:20 +08:00
except Exception as e:
if not server.stop_server:
logging.error(f"Server error: {e}")
2025-01-02 17:14:36 +08:00
finally:
2025-02-21 16:48:20 +08:00
await server.stop()
2025-01-02 17:14:36 +08:00
async def run_proxy_check(server):
if server.config.get('check_proxies', 'False').lower() == 'true':
logging.info(get_message('proxy_check_start', server.language))
valid_proxies = await check_proxies(server.proxies)
if valid_proxies:
server.proxies = valid_proxies
server.proxy_cycle = cycle(valid_proxies)
server.current_proxy = next(server.proxy_cycle)
logging.info(get_message('valid_proxies', server.language, valid_proxies))
else:
logging.error(get_message('no_valid_proxies', server.language))
else:
logging.info(get_message('proxy_check_disabled', server.language))
2025-01-07 14:41:40 +08:00
class ProxyCat:
def __init__(self):
self.executor = ThreadPoolExecutor(
max_workers=min(32, (os.cpu_count() or 1) * 4),
thread_name_prefix="proxy_worker"
)
loop = asyncio.get_event_loop()
loop.set_default_executor(self.executor)
if hasattr(asyncio, 'WindowsSelectorEventLoopPolicy'):
if os.name == 'nt':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
socket.setdefaulttimeout(30)
if hasattr(socket, 'TCP_NODELAY'):
socket.TCP_NODELAY = True
self.running = True
signal.signal(signal.SIGINT, self.handle_shutdown)
signal.signal(signal.SIGTERM, self.handle_shutdown)
2025-02-21 16:48:20 +08:00
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)
2025-01-07 14:41:40 +08:00
async def start_server(self):
try:
server = await asyncio.start_server(
self.handle_client,
self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port')
)
2025-02-10 16:53:09 +08:00
logging.info(get_message('server_running', self.language,
self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port')))
2025-01-07 14:41:40 +08:00
async with server:
await server.serve_forever()
except Exception as e:
2025-02-10 16:53:09 +08:00
logging.error(get_message('server_start_error', self.language, e))
2025-01-07 14:41:40 +08:00
sys.exit(1)
def handle_shutdown(self, signum, frame):
2025-02-10 16:53:09 +08:00
logging.info(get_message('server_shutting_down', self.language))
2025-01-07 14:41:40 +08:00
self.running = False
self.executor.shutdown(wait=True)
sys.exit(0)
async def handle_client(self, reader, writer):
2025-02-21 16:48:20 +08:00
task = asyncio.current_task()
self.tasks.add(task)
2025-01-07 14:41:40 +08:00
try:
2025-02-21 16:48:20 +08:00
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
2025-01-07 14:41:40 +08:00
await asyncio.get_event_loop().run_in_executor(
self.executor,
self.process_client_request,
reader,
writer
)
except Exception as e:
2025-02-10 16:53:09 +08:00
logging.error(get_message('client_process_error', self.language, e))
2025-01-07 14:41:40 +08:00
finally:
try:
writer.close()
await writer.wait_closed()
except:
pass
2025-02-21 16:48:20 +08:00
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
2025-01-02 17:14:36 +08:00
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=logos())
parser.add_argument('-c', '--config', default='config/config.ini', help='配置文件路径')
args = parser.parse_args()
config = load_config(args.config)
server = AsyncProxyServer(config)
print_banner(config)
asyncio.run(check_for_updates(config.get('language', 'cn').lower()))
2025-02-10 16:50:10 +08:00
if not config.get('use_getip', 'False').lower() == 'true':
asyncio.run(run_proxy_check(server))
else:
logging.info(get_message('api_mode_notice', server.language))
2025-01-02 17:14:36 +08:00
status_thread = threading.Thread(target=update_status, args=(server,), daemon=True)
status_thread.start()
try:
asyncio.run(run_server(server))
except KeyboardInterrupt:
logging.info(get_message('user_interrupt', server.language))