mirror of
https://github.com/honmashironeko/ProxyCat.git
synced 2025-06-21 02:11:11 +00:00
444 lines
17 KiB
Python
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))
|