sub / app.py
sfun's picture
Update app.py
62fd134 verified
raw
history blame
10.6 kB
import re
import json
import yaml
import aiohttp
import asyncio
import datetime
import sys
import traceback
from aiohttp import web, ClientTimeout, TCPConnector, ClientError, ServerTimeoutError, TooManyRedirects
from collections import namedtuple
CacheEntry = namedtuple('CacheEntry', ['data', 'timestamp'])
class CustomCache:
def __init__(self, ttl=1800):
self.cache = {}
self.ttl = ttl
def get(self, key):
if key in self.cache:
entry = self.cache[key]
if (datetime.datetime.now() - entry.timestamp).total_seconds() < self.ttl:
return entry
return None
def set(self, key, value):
self.cache[key] = CacheEntry(value, datetime.datetime.now())
cache = CustomCache(ttl=1800) # 30 minutes cache
CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
async def fetch_url(url, session, max_retries=5):
headers = {"User-Agent": CHROME_USER_AGENT}
for attempt in range(max_retries):
try:
timeout = ClientTimeout(total=40 * (attempt + 1)) # 递增的超时时间
async with session.get(url, headers=headers, timeout=timeout) as response:
response.raise_for_status()
content = await response.read()
return content.decode('utf-8', errors='ignore')
except (ClientError, asyncio.TimeoutError, ServerTimeoutError, TooManyRedirects) as e:
wait_time = 2 ** attempt # 指数退避
print(f"Attempt {attempt + 1} failed: {str(e)}. Retrying in {wait_time} seconds...", flush=True)
if attempt == max_retries - 1:
raise
await asyncio.sleep(wait_time)
async def extract_and_transform_proxies(input_text):
try:
# 首先尝试处理特殊的JSON格式
if input_text.strip().startswith('{"type"'):
# 处理多个相邻的JSON对象
# 将它们用逗号分隔并包装在数组中
input_text = f"[{input_text}]"
# 替换相邻的JSON对象之间的}{为},{
input_text = input_text.replace('},{', '},{')
try:
proxies_list = json.loads(input_text)
except json.JSONDecodeError:
# 如果JSON解析失败,尝试其他格式
proxies_list = None
else:
# 尝试常规的YAML格式
try:
data = yaml.safe_load(input_text)
if isinstance(data, dict) and 'proxies' in data:
proxies_list = data['proxies']
elif isinstance(data, list):
proxies_list = data
else:
proxies_match = re.search(r'proxies:\s*\n((?:[-\s]*{.*\n?)*)', input_text, re.MULTILINE)
if proxies_match:
proxies_text = proxies_match.group(1)
proxies_list = yaml.safe_load(proxies_text)
else:
proxies_list = None
except yaml.YAMLError:
proxies_list = None
if not proxies_list:
return "未找到有效的代理配置"
transformed_proxies = []
for proxy in proxies_list:
proxy_type = proxy.get('type')
if proxy_type in ['ss', 'trojan', 'hysteria2']:
# 获取名称,优先使用tag字段
name = proxy.get('tag', '') or proxy.get('name', '')
name = name.strip()
server = proxy.get('server', '').strip()
# 处理端口,可能是server_port或port
port = str(proxy.get('server_port', '') or proxy.get('port', '')).strip()
parts = [f"{name} = {proxy_type}, {server}, {port}"]
if proxy_type == 'ss':
if 'cipher' in proxy:
parts.append(f"encrypt-method={proxy['cipher'].strip()}")
if 'password' in proxy:
parts.append(f"password={proxy['password'].strip()}")
elif proxy_type in ['trojan', 'hysteria2']:
if 'password' in proxy:
parts.append(f"password={proxy['password'].strip()}")
if 'sni' in proxy:
parts.append(f"sni={proxy['sni'].strip()}")
# 处理TLS配置
if 'tls' in proxy and isinstance(proxy['tls'], dict):
if proxy['tls'].get('insecure', False):
parts.append("skip-cert-verify=true")
elif 'skip-cert-verify' in proxy:
parts.append(f"skip-cert-verify={str(proxy['skip-cert-verify']).lower()}")
elif proxy_type == 'hysteria2':
parts.append(f"skip-cert-verify=true")
# 处理TCP Fast Open配置
if 'tcp_fast_open' in proxy:
parts.append(f"tfo={str(proxy['tcp_fast_open']).lower()}")
if 'udp' in proxy:
parts.append(f"udp-relay={'true' if proxy['udp'] in [True, 'true', 'True'] else 'false'}")
transformed_proxies.append(", ".join(parts))
return "\n".join(transformed_proxies) if transformed_proxies else "未找到有效的代理配置"
except Exception as e:
print(f"Error in extract_and_transform_proxies: {str(e)}", flush=True)
return f"配置解析错误: {str(e)}"
def get_client_ip(request):
headers_to_check = [
'X-Forwarded-For',
'X-Real-IP',
'CF-Connecting-IP',
'True-Client-IP',
'X-Client-IP',
]
for header in headers_to_check:
ip = request.headers.get(header)
if ip:
return ip.split(',')[0].strip()
return request.remote
async def handle_request(request):
if request.path == '/':
if 'url' in request.query:
url = request.query['url']
no_cache = 'nocache' in request.query
cache_entry = None if no_cache else cache.get(url)
cache_hit = False
if cache_entry and not no_cache:
result = cache_entry.data
cache_hit = True
cache_time = cache_entry.timestamp
if not cache_hit or no_cache:
try:
async with aiohttp.ClientSession(connector=TCPConnector(ssl=False)) as session:
input_text = await fetch_url(url, session)
new_result = await extract_and_transform_proxies(input_text)
if new_result != "未找到有效的代理配置" and new_result != "YAML解析错误":
result = new_result
cache.set(url, result)
cache_time = datetime.datetime.now()
elif not cache_hit:
result = new_result
cache_time = datetime.datetime.now()
except Exception as e:
error_message = f"Error processing request: {str(e)}"
print(error_message, flush=True)
traceback.print_exc()
if cache_entry:
print("Using cached data due to error", flush=True)
result = cache_entry.data
cache_time = cache_entry.timestamp
cache_hit = True
else:
return web.Response(text=error_message, status=500)
proxy_count = result.count('\n') + 1 if result and result != "未找到有效的代理配置" else 0
return web.Response(text=result, content_type='text/plain', headers={
'X-Proxy-Count': str(proxy_count),
'X-Cache-Hit': str(cache_hit),
'X-Cache-Time': cache_time.strftime('%Y-%m-%d %H:%M:%S'),
'X-No-Cache': str(no_cache)
})
else:
usage_guide = """
<html>
<body>
<h1>代理配置转换工具</h1>
<p>使用方法:在URL参数中提供包含代理配置的网址。</p>
<p>示例:<code>http://localhost:8080/?url=https://example.com/path-to-proxy-config</code></p>
<p>强制获取新数据:<code>http://localhost:8080/?url=https://example.com/path-to-proxy-config&nocache</code></p>
</body>
</html>
"""
return web.Response(text=usage_guide, content_type='text/html')
else:
return web.Response(text="Not Found", status=404)
@web.middleware
async def logging_middleware(request, handler):
start_time = datetime.datetime.now()
try:
response = await handler(request)
end_time = datetime.datetime.now()
timestamp = end_time.strftime('%Y-%m-%d %H:%M:%S')
client_ip = get_client_ip(request)
target_url = request.query.get('url', '-')
no_cache = 'nocache' in request.query
status_code = response.status
proxy_count = response.headers.get('X-Proxy-Count', '0')
cache_hit = "Hit" if response.headers.get('X-Cache-Hit') == 'True' else "Miss"
cache_time = response.headers.get('X-Cache-Time', '-')
log_message = f"{timestamp} - {client_ip} - \"GET /?url={target_url}{'&nocache' if no_cache else ''}\" - Status: {status_code} - Proxies: {proxy_count} - Cache: {cache_hit} - CacheTime: {cache_time} - NoCache: {'Yes' if no_cache else 'No'}"
print(log_message, flush=True)
return response
except Exception as e:
end_time = datetime.datetime.now()
print(f"Error occurred: {str(e)}", flush=True)
print(f"Request processing time: {end_time - start_time}", flush=True)
print("Traceback:", flush=True)
traceback.print_exc()
return web.Response(text=f"Internal Server Error: {str(e)}", status=500)
async def init_app():
app = web.Application(middlewares=[logging_middleware])
app.router.add_get('/', handle_request)
return app
if __name__ == "__main__":
print(f"===== Application Startup at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} =====")
print("Server running on port 8080")
web.run_app(init_app(), port=8080, print=lambda _: None)