ProxyCat/ProxyCat.py
本间白猫 086d37aa73 2.0.4
2025-04-01 15:03:42 +08:00

444 lines
17 KiB
Python

from wsgiref import headers
from modules.modules import ColoredFormatter, load_config, DEFAULT_CONFIG, check_proxies, check_for_updates, get_message, load_ip_list, print_banner, logos
import threading, argparse, logging, asyncio, time, socket, signal, sys, os
from concurrent.futures import ThreadPoolExecutor
from modules.proxyserver import AsyncProxyServer
from colorama import init, Fore, Style
from itertools import cycle
from tqdm import tqdm
import base64
from configparser import ConfigParser
init(autoreset=True)
def setup_logging():
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):
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._update_config_values(new_config)
if old_use_getip != server.use_getip or old_mode != server.mode:
server._handle_mode_change()
if old_port != server.port:
logging.info(get_message('port_changed', server.language, old_port, server.port))
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
display_level = int(server.config.get('display_level', '1'))
is_docker = os.path.exists('/.dockerenv')
while True:
try:
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._reload_proxies()
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
if server.mode == 'loadbalance':
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
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()
except Exception as e:
if display_level >= 2:
logging.error(f"Status update error: {e}")
elif display_level >= 1:
logging.error(get_message('status_update_error', server.language))
time.sleep(1)
async def handle_client_wrapper(server, reader, writer, clients):
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:
await server.start()
except asyncio.CancelledError:
logging.info(get_message('server_closing', server.language))
except Exception as e:
if not server.stop_server:
logging.error(f"Server error: {e}")
finally:
await server.stop()
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, server.test_url)
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))
class ProxyCat:
def __init__(self):
cpu_count = os.cpu_count() or 1
self.executor = ThreadPoolExecutor(
max_workers=min(32, cpu_count + 4),
thread_name_prefix="proxy_worker",
thread_name_format="proxy_worker_%d"
)
loop = asyncio.get_event_loop()
loop.set_default_executor(self.executor)
if hasattr(loop, 'set_task_factory'):
loop.set_task_factory(None)
socket.setdefaulttimeout(30)
if hasattr(socket, 'TCP_NODELAY'):
socket.TCP_NODELAY = True
if hasattr(socket, 'SO_KEEPALIVE'):
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':
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
try:
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
except ValueError:
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, soft))
soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
try:
resource.setrlimit(resource.RLIMIT_NPROC, (hard, hard))
except ValueError:
pass
self.running = True
self.tasks = set()
self.max_tasks = 20000
self.task_semaphore = asyncio.Semaphore(self.max_tasks)
signal.signal(signal.SIGINT, self.handle_shutdown)
signal.signal(signal.SIGTERM, self.handle_shutdown)
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)
self.proxy_pool = {}
self.max_connections = 1000
self.connection_timeout = 30
self.read_timeout = 60
async def start_server(self):
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(
self.handle_client,
sock=sock
)
logging.info(get_message('server_running', self.language,
self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port')))
async with server:
await server.serve_forever()
except Exception as e:
logging.error(get_message('server_start_error', self.language, e))
sys.exit(1)
def handle_shutdown(self, signum, frame):
logging.info(get_message('server_shutting_down', self.language))
self.running = False
self.executor.shutdown(wait=True)
sys.exit(0)
async def handle_client(self, reader, writer):
task = asyncio.current_task()
self.tasks.add(task)
try:
if self.auth_required:
auth_header = headers.get('proxy-authorization')
if not auth_header or not self._authenticate(auth_header):
writer.write(b'HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="Proxy"\r\n\r\n')
await writer.drain()
return
await asyncio.get_event_loop().run_in_executor(
self.executor,
self._handle_proxy_request,
reader,
writer
)
except Exception as e:
logging.error(get_message('client_process_error', self.language, e))
finally:
try:
writer.close()
await writer.wait_closed()
except:
pass
self.tasks.remove(task)
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
async def _handle_proxy_request(self, reader, writer):
try:
request_line = await reader.readline()
if not request_line:
return
method, target, version = request_line.decode().strip().split(' ')
if method == 'CONNECT':
await self._handle_connect(target, reader, writer)
else:
await self._handle_http(method, target, version, reader, writer)
except Exception as e:
logging.error(get_message('request_handling_error', self.language, e))
try:
writer.close()
except:
pass
async def _handle_connect(self, target, reader, writer):
host, port = target.split(':')
port = int(port)
try:
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(host, port),
timeout=self.connection_timeout
)
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
await writer.drain()
await self._create_pipe(reader, writer, remote_reader, remote_writer)
except Exception as e:
logging.error(get_message('proxy_forward_error', self.language, e))
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
async def _handle_http(self, method, target, version, reader, writer):
try:
from urllib.parse import urlparse
url = urlparse(target)
host = url.hostname
port = url.port or 80
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(host, port),
timeout=self.connection_timeout
)
path = url.path + ('?' + url.query if url.query else '')
request = f'{method} {path} {version}\r\n'
remote_writer.write(request.encode())
while True:
line = await reader.readline()
if line == b'\r\n':
break
remote_writer.write(line)
remote_writer.write(b'\r\n')
await self._create_pipe(reader, writer, remote_reader, remote_writer)
except Exception as e:
logging.error(get_message('proxy_forward_error', self.language, e))
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
async def _create_pipe(self, client_reader, client_writer, remote_reader, remote_writer):
try:
pipe1 = asyncio.create_task(self._pipe(client_reader, remote_writer))
pipe2 = asyncio.create_task(self._pipe(remote_reader, client_writer))
done, pending = await asyncio.wait(
[pipe1, pipe2],
return_when=asyncio.FIRST_COMPLETED
)
for task in pending:
task.cancel()
except Exception as e:
logging.error(get_message('data_transfer_error', self.language, e))
finally:
try:
remote_writer.close()
await remote_writer.wait_closed()
except:
pass
async def _pipe(self, reader, writer):
try:
while True:
data = await reader.read(8192)
if not data:
break
writer.write(data)
await writer.drain()
except asyncio.CancelledError:
pass
except Exception as 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__':
setup_logging()
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()))
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))
status_thread = threading.Thread(target=update_status, args=(server,), daemon=True)
status_thread.start()
cleanup_thread = threading.Thread(target=lambda: asyncio.run(server.cleanup_clients()), daemon=True)
cleanup_thread.start()
try:
asyncio.run(run_server(server))
except KeyboardInterrupt:
logging.info(get_message('user_interrupt', server.language))