This commit is contained in:
本间白猫 2025-01-14 15:30:16 +08:00
parent 5b1040c40a
commit 3822ca13b3
3 changed files with 252 additions and 33 deletions

View File

@ -7,6 +7,12 @@ def newip():
global first_run_flag
config = load_config()
language = config.get('language', 'cn')
def handle_error(error_type, details=None):
error_msg = 'whitelist_error' if error_type == 'whitelist' else 'proxy_file_not_found'
print(get_message(error_msg, language, str(details)))
raise ValueError(f"{error_type}: {details}")
try:
if first_run_flag:
appKey = ""
@ -16,16 +22,25 @@ def newip():
first_run_flag = False
url = config.get('getip_url', '')
username = config.get('proxy_username', '')
password = config.get('proxy_password', '')
if not url:
raise ValueError('getip_url')
response = requests.get(url)
response.raise_for_status()
return "socks5://" + response.text.split("\r\n")[0]
proxy = response.text.split("\r\n")[0]
if username and password:
return f"socks5://{username}:{password}@{proxy}"
return f"socks5://{proxy}"
except requests.RequestException as e:
handle_error('request', e)
except ValueError as e:
handle_error('config', e)
except Exception as e:
error_msg = 'whitelist_error' if first_run_flag else 'proxy_file_not_found'
print(get_message(error_msg, language, str(e)))
raise
handle_error('unknown', e)

View File

@ -149,11 +149,19 @@ MESSAGES = {
}
}
def get_message(key, lang='cn', *args):
try:
return MESSAGES[lang][key].format(*args) if args else MESSAGES[lang][key]
except KeyError:
return MESSAGES['cn'][key] if key in MESSAGES['cn'] else key
class MessageManager:
def __init__(self, messages=MESSAGES):
self.messages = messages
self.default_lang = 'cn'
def get(self, key, lang='cn', *args):
try:
return self.messages[lang][key].format(*args) if args else self.messages[lang][key]
except KeyError:
return self.messages[self.default_lang][key] if key in self.messages[self.default_lang] else key
message_manager = MessageManager(MESSAGES)
get_message = message_manager.get
def print_banner(config):
language = config.get('language', 'cn').lower()
@ -275,6 +283,21 @@ def load_ip_list(file_path):
_proxy_check_cache = {}
_proxy_check_ttl = 10
def parse_proxy(proxy):
try:
protocol = proxy.split('://')[0]
remaining = proxy.split('://')[1]
if '@' in remaining:
auth, address = remaining.split('@')
host, port = address.split(':')
return protocol, auth, host, int(port)
else:
host, port = remaining.split(':')
return protocol, None, host, int(port)
except Exception:
return None, None, None, None
async def check_proxy(proxy):
current_time = time.time()
if proxy in _proxy_check_cache:
@ -302,27 +325,71 @@ async def check_proxy(proxy):
return False
async def check_http_proxy(proxy):
async with httpx.AsyncClient(proxies={'http://': proxy}, timeout=10) as client:
response = await client.get('http://www.baidu.com')
return response.status_code == 200
protocol, auth, host, port = parse_proxy(proxy)
proxies = {}
if auth:
proxies['http://'] = f'{protocol}://{auth}@{host}:{port}'
proxies['https://'] = f'{protocol}://{auth}@{host}:{port}'
else:
proxies['http://'] = f'{protocol}://{host}:{port}'
proxies['https://'] = f'{protocol}://{host}:{port}'
try:
async with httpx.AsyncClient(proxies=proxies, timeout=10, verify=False) as client:
try:
response = await client.get('https://www.baidu.com')
return response.status_code == 200
except:
response = await client.get('http://www.baidu.com')
return response.status_code == 200
except:
return False
async def check_https_proxy(proxy):
async with httpx.AsyncClient(proxies={'https://': proxy}, timeout=10) as client:
response = await client.get('https://www.baidu.com')
return response.status_code == 200
return await check_http_proxy(proxy)
async def check_socks_proxy(proxy):
proxy_type, proxy_addr = proxy.split('://')
proxy_host, proxy_port = proxy_addr.split(':')
proxy_port = int(proxy_port)
protocol, auth, host, port = parse_proxy(proxy)
if not all([host, port]):
return False
try:
reader, writer = await asyncio.wait_for(asyncio.open_connection(proxy_host, proxy_port), timeout=5)
writer.write(b'\x05\x01\x00')
reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=5)
if auth:
writer.write(b'\x05\x02\x00\x02')
else:
writer.write(b'\x05\x01\x00')
await writer.drain()
response = await asyncio.wait_for(reader.readexactly(2), timeout=5)
auth_method = await asyncio.wait_for(reader.readexactly(2), timeout=5)
if auth_method[0] != 0x05:
return False
if auth_method[1] == 0x02 and auth:
username, password = auth.split(':')
auth_packet = bytes([0x01, len(username)]) + username.encode() + bytes([len(password)]) + password.encode()
writer.write(auth_packet)
await writer.drain()
auth_response = await asyncio.wait_for(reader.readexactly(2), timeout=5)
if auth_response[1] != 0x00:
return False
domain = b"www.baidu.com"
writer.write(b'\x05\x01\x00\x03' + bytes([len(domain)]) + domain + b'\x00\x50')
await writer.drain()
response = await asyncio.wait_for(reader.readexactly(10), timeout=5)
writer.close()
await writer.wait_closed()
return response == b'\x05\x00'
try:
await writer.wait_closed()
except:
pass
return response[1] == 0x00
except Exception:
return False

View File

@ -10,8 +10,13 @@ def load_proxies(file_path='ip.txt'):
return [line.strip() for line in file if '://' in line]
def validate_proxy(proxy):
pattern = re.compile(r'^(?P<scheme>socks5|http|https)://(?P<host>[^:]+):(?P<port>\d+)$')
return pattern.match(proxy) is not None
pattern = re.compile(r'^(?P<scheme>socks5|http|https)://(?:(?P<auth>[^@]+)@)?(?P<host>[^:]+):(?P<port>\d+)$')
match = pattern.match(proxy)
if not match:
return False
port = int(match.group('port'))
return 0 < port < 65536
class AsyncProxyServer:
def __init__(self, config):
@ -258,14 +263,56 @@ class AsyncProxyServer:
await writer.drain()
async def _initiate_socks5(self, remote_reader, remote_writer, dst_addr, dst_port):
remote_writer.write(b'\x05\x01\x00')
await remote_writer.drain()
await remote_reader.readexactly(2)
remote_writer.write(b'\x05\x01\x00' + (b'\x03' + len(dst_addr).to_bytes(1, 'big') + dst_addr.encode() if isinstance(dst_addr, str) else b'\x01' + socket.inet_aton(dst_addr)) + struct.pack('!H', dst_port))
await remote_writer.drain()
await remote_reader.readexactly(10)
try:
auth = None
proxy_type, proxy_addr = self.current_proxy.split('://')
if '@' in proxy_addr:
auth, _ = proxy_addr.split('@')
if auth:
remote_writer.write(b'\x05\x02\x00\x02')
else:
remote_writer.write(b'\x05\x01\x00')
await remote_writer.drain()
auth_method = await remote_reader.readexactly(2)
if auth_method[0] != 0x05:
raise Exception("Invalid SOCKS5 proxy response")
if auth_method[1] == 0x02 and auth:
username, password = auth.split(':')
auth_packet = bytes([0x01, len(username)]) + username.encode() + bytes([len(password)]) + password.encode()
remote_writer.write(auth_packet)
await remote_writer.drain()
auth_response = await remote_reader.readexactly(2)
if auth_response[1] != 0x00:
raise Exception("Authentication failed")
if isinstance(dst_addr, str):
remote_writer.write(b'\x05\x01\x00\x03' + len(dst_addr).to_bytes(1, 'big') +
dst_addr.encode() + dst_port.to_bytes(2, 'big'))
else:
remote_writer.write(b'\x05\x01\x00\x01' + socket.inet_aton(dst_addr) +
dst_port.to_bytes(2, 'big'))
await remote_writer.drain()
response = await remote_reader.readexactly(4)
if response[1] != 0x00:
raise Exception(f"Connection failed with code {response[1]}")
if response[3] == 0x01:
await remote_reader.readexactly(6)
elif response[3] == 0x03:
domain_len = (await remote_reader.readexactly(1))[0]
await remote_reader.readexactly(domain_len + 2)
elif response[3] == 0x04:
await remote_reader.readexactly(18)
except Exception as e:
raise Exception(f"SOCKS5 initialization failed: {str(e)}")
async def _initiate_http(self, remote_reader, remote_writer, dst_addr, dst_port, proxy_auth):
connect_request = f'CONNECT {dst_addr}:{dst_port} HTTP/1.1\r\nHost: {dst_addr}:{dst_port}\r\n'
@ -422,6 +469,77 @@ class AsyncProxyServer:
return f"{username}:{password}", host
return None, proxy_addr
async def _handle_request(self, method, path, headers, reader, writer):
async with self.request_semaphore:
try:
proxy = await self.get_next_proxy()
key = f"{proxy}:{path}"
proxy_headers = headers.copy()
proxy_type, proxy_addr = proxy.split('://')
if '@' in proxy_addr:
auth, _ = proxy_addr.split('@')
auth_header = f'Basic {base64.b64encode(auth.encode()).decode()}'
proxy_headers['Proxy-Authorization'] = auth_header
if key in self.connection_pool:
client = self.connection_pool[key]
else:
client = await self._create_client(proxy)
self.connection_pool[key] = client
async with client.stream(
method,
path,
headers=proxy_headers,
content=reader,
) as response:
writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode())
for header_name, header_value in response.headers.items():
if header_name.lower() != 'transfer-encoding':
writer.write(f'{header_name}: {header_value}\r\n'.encode())
writer.write(b'\r\n')
async for chunk in response.aiter_bytes(chunk_size=self.buffer_size):
writer.write(chunk)
if len(chunk) >= self.buffer_size:
await writer.drain()
await writer.drain()
except Exception as e:
logging.error(f"请求处理错误: {e}")
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
finally:
await self._cleanup_connections()
async def _create_client(self, proxy):
proxy_type, proxy_addr = proxy.split('://')
proxy_auth = None
if '@' in proxy_addr:
auth, proxy_addr = proxy_addr.split('@')
proxy_auth = auth
if proxy_auth:
proxy_url = f"{proxy_type}://{proxy_auth}@{proxy_addr}"
else:
proxy_url = f"{proxy_type}://{proxy_addr}"
return httpx.AsyncClient(
proxies={"all://": proxy_url},
limits=httpx.Limits(
max_keepalive_connections=100,
max_connections=1000,
keepalive_expiry=30
),
timeout=30.0,
http2=True,
verify=False
)
async def _handle_request(self, method, path, headers, reader, writer):
async with self.request_semaphore:
try:
@ -625,3 +743,22 @@ class AsyncProxyServer:
return f"{get_message('current_proxy', self.language)}: {self.current_proxy}"
else:
return f"{get_message('current_proxy', self.language)}: {self.current_proxy} | {get_message('next_switch', self.language)}: {time_left:.1f}{get_message('seconds', self.language)}"
async def _handle_proxy_error(self, error_type, details=None):
error_messages = {
'timeout': get_message('connect_timeout', self.language),
'invalid': get_message('proxy_invalid', self.language, details),
'switch': get_message('proxy_invalid_switching', self.language)
}
logging.error(error_messages.get(error_type, str(details)))
if error_type in ['timeout', 'invalid']:
await self.handle_proxy_failure()
async def check_proxy(self, proxy):
cache_key = f"{proxy}_{time.time() // self.proxy_cache_ttl}"
if cache_key in self.proxy_cache:
return self.proxy_cache[cache_key]
is_valid = await self._check_proxy_impl(proxy)
self.proxy_cache[cache_key] = is_valid
return is_valid