ProxyCat/ProxyCat.py

444 lines
17 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)
2025-03-03 11:13:52 +08:00
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])
2025-01-02 17:14:36 +08:00
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)
2025-03-03 11:13:52 +08:00
server._update_config_values(new_config)
2025-02-21 16:48:20 +08:00
if old_use_getip != server.use_getip or old_mode != server.mode:
2025-03-03 11:13:52 +08:00
server._handle_mode_change()
2025-02-21 16:48:20 +08:00
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
2025-03-03 11:13:52 +08:00
display_level = int(server.config.get('display_level', '1'))
is_docker = os.path.exists('/.dockerenv')
2025-02-21 16:48:20 +08:00
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))
2025-03-03 11:13:52 +08:00
server._reload_proxies()
2025-02-21 16:48:20 +08:00
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-04-01 15:03:42 +08:00
if server.mode == 'loadbalance':
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
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))
2025-04-01 15:03:42 +08:00
valid_proxies = await check_proxies(server.proxies, server.test_url)
2025-01-02 17:14:36 +08:00
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):
2025-03-03 11:13:52 +08:00
cpu_count = os.cpu_count() or 1
2025-01-07 14:41:40 +08:00
self.executor = ThreadPoolExecutor(
2025-03-03 11:13:52 +08:00
max_workers=min(32, cpu_count + 4),
thread_name_prefix="proxy_worker",
thread_name_format="proxy_worker_%d"
2025-01-07 14:41:40 +08:00
)
loop = asyncio.get_event_loop()
loop.set_default_executor(self.executor)
2025-03-03 11:13:52 +08:00
if hasattr(loop, 'set_task_factory'):
loop.set_task_factory(None)
2025-01-07 14:41:40 +08:00
socket.setdefaulttimeout(30)
if hasattr(socket, 'TCP_NODELAY'):
socket.TCP_NODELAY = True
2025-03-03 11:13:52 +08:00
if hasattr(socket, 'SO_KEEPALIVE'):
socket.SO_KEEPALIVE = True
2025-01-07 14:41:40 +08:00
2025-04-01 15:03:42 +08:00
if hasattr(socket, 'SO_REUSEADDR'):
socket.SO_REUSEADDR = True
if hasattr(socket, 'SO_REUSEPORT') and os.name != 'nt':
socket.SO_REUSEPORT = True
2025-03-03 11:13:52 +08:00
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
2025-01-07 14:41:40 +08:00
2025-03-03 11:13:52 +08:00
self.running = True
self.tasks = set()
self.max_tasks = 20000
self.task_semaphore = asyncio.Semaphore(self.max_tasks)
2025-01-07 14:41:40 +08:00
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-03-03 11:13:52 +08:00
self.proxy_pool = {}
self.max_connections = 1000
self.connection_timeout = 30
self.read_timeout = 60
2025-01-07 14:41:40 +08:00
async def start_server(self):
try:
2025-04-01 15:03:42 +08:00
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'))))
2025-01-07 14:41:40 +08:00
server = await asyncio.start_server(
self.handle_client,
2025-04-01 15:03:42 +08:00
sock=sock
2025-01-07 14:41:40 +08:00
)
2025-04-01 15:03:42 +08:00
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(
2025-03-03 11:13:52 +08:00
self.executor,
self._handle_proxy_request,
reader,
2025-01-07 14:41:40 +08:00
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-03-03 11:13:52 +08:00
self.tasks.remove(task)
2025-01-07 14:41:40 +08:00
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-03-03 11:13:52 +08:00
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))
2025-04-01 15:03:42 +08:00
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)
2025-01-02 17:14:36 +08:00
if __name__ == '__main__':
2025-03-03 11:13:52 +08:00
setup_logging()
2025-01-02 17:14:36 +08:00
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()
2025-04-01 15:03:42 +08:00
cleanup_thread = threading.Thread(target=lambda: asyncio.run(server.cleanup_clients()), daemon=True)
cleanup_thread.start()
2025-01-02 17:14:36 +08:00
try:
asyncio.run(run_server(server))
except KeyboardInterrupt:
logging.info(get_message('user_interrupt', server.language))