patch-ups5-for-uc-flora
· 43 KiB · Text
Raw
import json
import logging
import traceback
import time
import os
import jwt
import asyncio
from urllib.parse import unquote, urlencode
from datetime import datetime
import aiohttp
from aiohttp import web
from app.tools import tools
from app.auth import key, alg, checkRight, userlog, hasRights
from app.db import DB
from app.logger import getProxyLogger
proxy_logger = getProxyLogger()
try:
config = json.load(open('app/conf/proxy.json', 'r'))
except Exception as error:
proxy_logger.error(f"Ошибка загрузки конфигурации прокси: {error}")
proxy_logger.error(traceback.format_exc())
raise
kibana = tools.params['servers']['res']['cluster'][0]
kibana_config = {
'url': kibana,
'username': tools.params['servers']['res'].get('auth', {}).get('username', ''),
'password': tools.params['servers']['res'].get('auth', {}).get('password', '')
}
class ProxyHandler:
def __init__(self):
self.sessions = {}
async def close(self):
"""Закрытие всех сессий"""
for session_dict in self.sessions.values():
for session in session_dict.values():
await session.close()
def get_session(self, service, method, path):
t = config.get(service,{}).get('cache',{}).get(method,{}).get(path)
if t:
connect_timeout = t
read_timeout = t
else:
connect_timeout=tools.params['servers'][service]['timers']['connect']
read_timeout=tools.params['servers'][service]['timers']['read']
session_key = f"{connect_timeout}_{read_timeout}"
if service not in self.sessions:
self.sessions[service] = {}
if session_key not in self.sessions[service]:
timeout = aiohttp.ClientTimeout(
total=None,
connect=connect_timeout,
sock_read=read_timeout
)
self.sessions[service][session_key] = aiohttp.ClientSession(timeout=timeout, auto_decompress=False)
return self.sessions[service][session_key]
async def response_postprocessing(self, response_data, dataset, request_path, request_params=None, request_method='GET', raw_response=None):
"""Постобработка ответа - фильтрация по dataset"""
if dataset is None:
dataset = "0"
def new_value(value, dataset):
if dataset == "0":
if "~" in value:
return None
else:
if f"~{dataset}" not in value:
return None
value = "~".join(value.split('~')[:-1])
return value
# Логирование классификации
if '/smc/classify' in request_path:
corpus_name = datetime.today().strftime('%Y-%m-%d') + '.csv'
t = datetime.now().strftime("%H:%M:%S")
try:
model = unquote(request_path.split('/')[-1].split('?')[0])
os.makedirs(f'data/smc/logs/{model}', exist_ok=True)
with open(f'data/smc/logs/{model}/{corpus_name}', 'a') as f:
query_text = request_params.get('text', '') if request_params else ''
response_text = raw_response if raw_response else json.dumps(response_data)
f.write(f"{t}\t{query_text}\t{response_text}\n")
except Exception as e:
proxy_logger.error(f"[SMC ERROR] Ошибка при логировании классификации: {e}")
if not isinstance(response_data, dict):
return response_data
if response_data.get('error'):
return response_data
# Фильтрация результатов по dataset
for service, params in config.items():
if not request_path.startswith(f"/{service}"):
continue
for method in params.get("filters", {}):
if method != request_method:
continue
paths = params['filters'][method]
for path, filters in paths.items():
if not request_path.startswith(f"/{service}{path}"):
continue
for cnf in filters:
# Фильтрация списков
if cnf['type'] == 'list':
if cnf['name'] in response_data and isinstance(response_data[cnf['name']], list):
filtered_list = []
for item in response_data[cnf['name']]:
value = new_value(item, dataset)
if value is not None:
filtered_list.append(value)
response_data[cnf['name']] = filtered_list
# Фильтрация ключей
elif cnf['type'] == 'key':
keys_to_process = list(response_data.keys())
for resp_key in keys_to_process:
if resp_key in cnf.get('exclude', []):
continue
value = new_value(resp_key, dataset)
if value is None:
del response_data[resp_key]
elif resp_key != value:
response_data[value] = response_data[resp_key]
del response_data[resp_key]
return response_data
async def modify_request_data(self, request, service, path, method, dataset, user_id):
"""Модификация данных запроса - добавление dataset"""
# Без авторизации или с дефолтным dataset не модифицируем
if user_id is None or dataset == "0" or dataset is None or dataset == "None":
return request
dataset_config = config.get(service, {}).get('datasets', {}).get(method, {}).get(path)
if not dataset_config:
return request
for param in dataset_config:
if param['type'] == 'path':
# Модификация пути
path_parts = request['full_path'].split('?')
url_parts = path_parts[0].split('/')
if not param.get('splitter'):
if url_parts[param['position']] in param.get('exclude', []):
return request
if f"~{dataset}" not in url_parts[param['position']]:
url_parts[param['position']] = f"{url_parts[param['position']]}~{dataset}"
else:
split_parts = url_parts[param['position']].split(param['splitter'])
for i in range(len(split_parts)):
if split_parts[i] not in param.get('exclude', []):
if f"~{dataset}" not in split_parts[i]:
split_parts[i] = f"{split_parts[i].strip()}~{dataset}"
url_parts[param['position']] = param['splitter'].join(split_parts)
path_parts[0] = '/'.join(url_parts)
request['full_path'] = '?'.join(path_parts)
elif param['type'] == 'param':
# Модификация параметра
param_name = param['name']
if 'params' in request and param_name in request['params']:
value = request['params'][param_name]
if value not in param.get('exclude', []) and f"~{dataset}" not in value:
request['params'][param_name] = f"{value}~{dataset}"
elif 'data' in request and isinstance(request['data'], dict) and param_name in request['data']:
value = request['data'][param_name]
if value not in param.get('exclude', []) and f"~{dataset}" not in value:
request['data'][param_name] = f"{value}~{dataset}"
elif param['type'] == 'insert':
# Вставка нового параметра
param_name = param['name']
if method == 'GET' and 'params' in request:
request['params'][param_name] = dataset
elif 'data' in request and isinstance(request['data'], dict):
# Если data это dict - добавляем в data
request['data'][param_name] = dataset
elif 'data' in request and isinstance(request['data'], bytes):
# Если data это urlencoded/multipart - добавляем в квери
if 'params' not in request:
request['params'] = {}
request['params'][param_name] = dataset
return request
async def change_request(self, request, service, servertype):
"""Специальная обработка запросов для разных сервисов"""
path = request['full_path']
method = request['method']
# Обрабатываем data - может быть dict или bytes
data = request.get('data', {})
if isinstance(data, bytes):
# Если это сырые байты, пытаемся распарсить
return 0, request, servertype
# SMC/SEE: Проверка на модели-черновики
if '/see/entities' in path or '/smc/classify' in path:
model = path.split('/')[-1].split('?')[0]
if len(model) > 4 and model[-4:] == '-new':
servertype = 'trainer'
# SMC: Замена корпуса на группы
elif '/smc/groups' in path and method == 'POST':
if isinstance(data, dict) and 'corpus' in data:
corpus = data['corpus']
corpus_file = f"data/db/marks/{corpus}.json"
if not os.path.isfile(corpus_file):
return 1, f"CorpusNotFound: {corpus}", servertype
try:
grouplist = json.load(open(corpus_file, 'r'))['groups']
groups = dict()
for item in grouplist:
if not item[1] in groups:
groups[item[1]] = list()
groups[item[1]].append(list(item[0]))
data['groups'] = json.dumps(groups)
del data['corpus']
request['data'] = data
except Exception:
return 1, "GroupsLoadFailed", servertype
# SMC: Обучение/тестирование
elif '/smc/train' in path or '/smc/test' in path:
if isinstance(data, dict) and 'corpus' in data:
corpus = data['corpus']
is_marks = data.get('isMarks', '0')
corpus_path = f"data/db/marks/{corpus}.json"
if not os.path.isfile(corpus_path):
return 1, f"CorpusNotFound: {corpus}", servertype
try:
corpus_name = json.load(open(corpus_path, 'r'))['name']
except Exception:
return 1, f"CorpusCorrupted: {corpus}", servertype
if str(is_marks) != '1':
csv, _ = tools.loadCorpusToCSV(corpusFile=corpus_path)
else:
csv = open(corpus_path, 'r').read()
data['corpusName'] = corpus_name
data['corpusData'] = csv
del data['corpus']
request['data'] = data
if not path.endswith('-new'):
request['full_path'] = path + '-new'
# SEE: Обучение
elif '/see/train' in path:
if isinstance(data, dict) and 'corpus' in data:
corpus = data['corpus']
corpus_path = f"data/db/marks/{corpus}.json"
if not os.path.isfile(corpus_path):
return 1, f"CorpusNotFound: {corpus}", servertype
try:
corpus_name = json.load(open(corpus_path, 'r'))['name']
except Exception:
return 1, f"CorpusCorrupted: {corpus}", servertype
csv, _ = tools.loadCorpusToCSV(corpusFile=corpus_path)
data['corpusName'] = corpus_name
data['corpusData'] = csv
del data['corpus']
request['data'] = data
if not path.endswith('-new'):
request['full_path'] = path + '-new'
# SMC: Автоматическая разметка
elif '/smc/automark/start' in path:
if isinstance(data, dict):
corpus = data.get('corpus')
marks = data.get('marks')
if corpus and marks:
try:
corpus_data = json.load(open(f"data/db/marks/{corpus}.json", "r"))['data']
except Exception:
return 1, f"CorpusCorrupted: {corpus}", servertype
try:
marks_data = json.load(open(f"data/db/marks/{marks}.json", "r"))['data']
except Exception:
return 1, f"CorpusCorrupted: {marks}", servertype
data['corpus'] = json.dumps(corpus_data)
data['marks'] = json.dumps(marks_data)
request['data'] = data
return 0, request, servertype
def parse_multipart_form(self, content, content_type):
"""Парсинг multipart/form-data"""
if not content_type or 'multipart/form-data' not in content_type:
return None
try:
boundary = content_type.split('boundary=')[-1].strip()
if boundary.startswith('"') and boundary.endswith('"'):
boundary = boundary[1:-1]
parts = content.split(f'--{boundary}'.encode())
form_data = {}
for part in parts:
if not part or part == b'--\r\n' or part == b'--':
continue
# Разделяем заголовки и данные
if b'\r\n\r\n' in part:
headers_part, data_part = part.split(b'\r\n\r\n', 1)
# Ищем имя поля
name_match = None
for line in headers_part.split(b'\r\n'):
if b'name="' in line:
name_start = line.find(b'name="') + 6
name_end = line.find(b'"', name_start)
if name_end > name_start:
name_match = line[name_start:name_end].decode('utf-8', errors='ignore')
break
if name_match:
# Убираем trailing \r\n
data_part = data_part.rstrip(b'\r\n')
form_data[name_match] = data_part.decode('utf-8', errors='ignore')
return form_data
except Exception as e:
proxy_logger.error(f"[MULTIPART] Parse error: {e}")
return None
def parse_urlencoded_form(self, content):
"""Парсинг application/x-www-form-urlencoded"""
try:
from urllib.parse import parse_qs
decoded = content.decode('utf-8', errors='ignore')
parsed = parse_qs(decoded, keep_blank_values=True)
# Преобразуем списки в единичные значения
return {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
except Exception as e:
proxy_logger.error(f"[URLENCODED] Parse error: {e}")
return None
async def proxy_request(self, service, path, method, headers, params, data, dataset, user_id):
"""Основная функция проксирования"""
proxy_logger.info(f"[PROXY] {method} /{service}{path}")
# Определяем тип сервера и путь из конфигурации
servertype = None
matched_path = None
for conf_method, paths in config[service].get('requests', {}).items():
if conf_method != method:
continue
for conf_path, conf_servertype in paths.items():
if path.startswith(conf_path):
servertype = conf_servertype
matched_path = conf_path
break
if servertype:
break
if not servertype:
return {'error': 1, 'message': 'Route not found'}, 404
# Статические файлы
if "/" in servertype:
if not os.path.isfile(servertype):
return {'error': 1, 'message': 'File not found'}, 404
with open(servertype, 'rb') as f:
return f.read(), 200, {'Content-Type': 'text/javascript; charset=utf-8'}
# Фейковый запрос
if servertype == 'fake':
return b'', 200, {'Content-Type': 'application/octet-stream', 'Content-Length': '50'}
request_data = {
'method': method,
'path': path,
'full_path': f"/{service}{path}",
'headers': dict(headers),
'params': params or {},
'data': data or {}
}
# Специальная обработка
error, result, servertype = await self.change_request(request_data, service, servertype)
if error:
return {'error': 1, 'message': result}, 405
request_data = result
# Добавление dataset
request_data = await self.modify_request_data(
request_data, service, matched_path, method, dataset, user_id
)
# Определение серверов для запроса
if servertype in ['find', 'all']:
servers = list(tools.params['servers'][service]['cluster'])
if not isinstance(servers, list):
servers = list([servers])
else:
servers = list(servers)
# Специальная обработка для архива
if '/archive/dialog' in path:
dialog_id = path.split('/')[-1]
servers.insert(0, (tools.params['servers']['res']['trainer'], f'/res/dialog/{dialog_id}'))
elif '/archive/list' in path:
res_path = '/res/archive/list'
if '?' in path:
res_path += '?' + path.split('?')[-1]
servers.insert(0, (tools.params['servers']['res']['trainer'], res_path))
else:
servers = None
# Выполнение запроса
if servertype == 'find':
return await self.proxy_find(service, request_data, servers, dataset)
elif servertype == 'all':
return await self.proxy_all(service, request_data, servers, dataset)
else:
return await self.proxy_single(service, request_data, servertype, dataset)
async def proxy_single(self, service, request_data, servertype, dataset):
"""Проксирование на один сервер (cluster/trainer) с повторными попытками при ошибках"""
if servertype == 'trainer':
max_attempts = 1
else: # cluster
servers = tools.params['servers'][service].get('cluster', [])
max_attempts = len(servers) if isinstance(servers, list) else 1
method = request_data['method']
session = self.get_session(service=service, method=method, path=request_data['path'])
for attempt in range(max_attempts):
server_url = tools.getProductionServer(servicetype=service, servertype=servertype)
url = server_url + request_data['full_path']
headers = {k: v for k, v in request_data['headers'].items()
if k.lower() not in ['content-length']}
proxy_logger.info(f"[SINGLE][{service.upper()}] Attempt {attempt + 1}/{max_attempts} -> {server_url}")
try:
if method == 'GET':
resp = await session.get(url, params=request_data['params'], headers=headers)
# это streaming ответ
content_type = resp.headers.get('Content-Type', '')
if 'text/event-stream' in content_type:
proxy_logger.info(f"[STREAMING][{service.upper()}] {server_url}")
# Возвращаем маркер streaming и response объект
return ('stream', resp), resp.status
# Обычный ответ - читаем полностью
async with resp:
content = await resp.read()
status = resp.status
elif method == 'POST':
if isinstance(request_data['data'], dict):
headers['Content-Type'] = 'application/json'
resp = await session.post(url, json=request_data['data'], headers=headers, params=request_data['params'])
else:
resp = await session.post(url, data=request_data['data'], headers=headers, params=request_data['params'])
# Проверка на streaming ответ
content_type = resp.headers.get('Content-Type', '')
if 'text/event-stream' in content_type:
proxy_logger.info(f"[STREAMING][{service.upper()}] {server_url}")
return ('stream', resp), resp.status
# Обычный ответ - читаем полностью
async with resp:
content = await resp.read()
status = resp.status
elif method == 'DELETE':
if isinstance(request_data['data'], dict):
headers['Content-Type'] = 'application/json'
async with session.delete(url, json=request_data['data'], headers=headers, params=request_data['params']) as resp:
content = await resp.read()
status = resp.status
else:
async with session.delete(url, data=request_data['data'], headers=headers, params=request_data['params']) as resp:
content = await resp.read()
status = resp.status
elif method == 'PUT':
if isinstance(request_data['data'], dict):
headers['Content-Type'] = 'application/json'
async with session.put(url, json=request_data['data'], headers=headers, params=request_data['params']) as resp:
content = await resp.read()
status = resp.status
else:
async with session.put(url, data=request_data['data'], headers=headers, params=request_data['params']) as resp:
content = await resp.read()
status = resp.status
else:
return {'error': 1, 'message': 'Method not supported'}, 405
if status >= 400:
try:
error_text = content.decode('utf-8')
proxy_logger.error(f"[RESPONSE ERROR][{service.upper()}] ERROR {status} {server_url}: {error_text}")
except:
proxy_logger.error(f"[RESPONSE ERROR][{service.upper()}] ERROR {status} {server_url}: (binary data, {len(content)} bytes)")
else:
proxy_logger.info(f"[RESPONSE][{service.upper()}] {status} {server_url}")
try:
response_data = json.loads(content.decode('utf-8'))
response_data = await self.response_postprocessing(
response_data, dataset, request_data['full_path'], request_data.get('params'), request_data['method']
)
# Сохраним результат в кэше, если это требуется
id = self.get_cache_id(service=service,method=method,path=request_data['path'],dataset=dataset)
if id:
cont = json.dumps(response_data)
error, result = DB.get(table='cache',id=id,md5clean=False)
if error:
logging.info(DB.create(table='cache',data={'id': id, 'content': cont}))
else:
if DB.md5(cont) != result['md5']:
DB.modify(table='cache',data={'id': id, 'content': cont})
return response_data, status
except Exception:
return content, status
except (asyncio.TimeoutError, Exception) as e:
error_type = "TIMEOUT" if isinstance(e, asyncio.TimeoutError) else "ERROR"
proxy_logger.error(f"[{error_type}][{service.upper()}] {server_url}: {str(e)}")
tools.setFailedServer(server_url, service)
# Если таймаут, то пробуем вернуть из кэша
if error_type == "TIMEOUT":
id = self.get_cache_id(service=service,method=method,path=request_data['path'],dataset=dataset)
if id:
error, result = DB.get(table='cache',id=id,md5clean=False)
if not error:
return json.loads(result['content']), 200
# Если это последняя попытка - возвращаем ошибку
if attempt == max_attempts - 1:
if isinstance(e, asyncio.TimeoutError):
return {'error': 1, 'message': 'Request timeout'}, 504
return {'error': 1, 'message': str(e)}, 500
proxy_logger.info("[RETRY] Trying next server...")
continue
return {'error': 1, 'message': 'No servers available'}, 503
def get_cache_id(self,service,method,path,dataset=None):
if path in config.get(service,{}).get('cache',{}).get(method,{}):
cache_key = f"{method}:/{service}{path}:{dataset if dataset else '0'}"
return DB.md5(cache_key)
else:
return False
async def proxy_find(self, service, request_data, servers, dataset):
"""Поиск на первом доступном сервере"""
method = request_data['method']
session = self.get_session(service=service, method=method, path=request_data['path'])
for server_info in servers:
if isinstance(server_info, tuple):
server_url, custom_path = server_info
url = server_url + custom_path
else:
server_url = server_info
url = server_url + request_data['full_path']
proxy_logger.info(f"[FIND][{service.upper()}] Trying {server_url}")
try:
method = request_data['method']
headers = {k: v for k, v in request_data['headers'].items()
if k.lower() not in ['content-length']}
if method == 'GET':
async with session.get(url, params=request_data['params'], headers=headers) as resp:
if resp.status == 200:
content = await resp.read()
try:
response_data = json.loads(content.decode('utf-8'))
response_data = await self.response_postprocessing(
response_data, dataset, request_data['full_path'], request_data.get('params'), request_data['method']
)
proxy_logger.info(f"[FIND] Success on {server_url}")
return response_data, 200
except Exception:
return content, 200
else:
proxy_logger.info(f"[FIND] {resp.status} from {server_url}, trying next")
continue
else:
# Для POST/DELETE/PUT аналогично
async with session.request(method, url, data=request_data.get('data'), headers=headers) as resp:
if resp.status == 200:
content = await resp.read()
try:
response_data = json.loads(content.decode('utf-8'))
response_data = await self.response_postprocessing(
response_data, dataset, request_data['full_path'], request_data.get('params'), request_data['method']
)
return response_data, 200
except Exception:
return content, 200
else:
continue
except Exception as e:
proxy_logger.error(f"[FIND] Error on {server_url}: {str(e)}")
tools.setFailedServer(server_url, service)
continue
return {'error': 1, 'message': 'No servers available'}, 503
async def proxy_all(self, service, request_data, servers, dataset):
"""Сбор данных со всех серверов и объединение результатов"""
method = request_data['method']
session = self.get_session(service=service, method=method, path=request_data['path'])
tasks = []
for server_info in servers:
if isinstance(server_info, tuple):
server_url, custom_path = server_info
url = server_url + custom_path
else:
server_url = server_info
url = server_url + request_data['full_path']
tasks.append(self._fetch_from_server(session, request_data, url, server_url, service))
responses = await asyncio.gather(*tasks, return_exceptions=True)
# Объединение результатов
combined_result = None
for resp_data in responses:
if isinstance(resp_data, Exception):
continue
if resp_data is None:
continue
if combined_result is None:
combined_result = [] if isinstance(resp_data, list) else {}
if isinstance(combined_result, list) and isinstance(resp_data, list):
combined_result.extend(resp_data)
elif isinstance(combined_result, dict) and isinstance(resp_data, dict):
combined_result.update(resp_data)
if combined_result is None:
return {'error': 1, 'message': 'No data collected'}, 503
# Уникализация и сортировка
if isinstance(combined_result, list) and combined_result:
if 'uuid' in str(combined_result):
seen_uuids = set()
unique_result = []
for item in combined_result:
if isinstance(item, dict) and 'uuid' in item:
if item['uuid'] not in seen_uuids:
seen_uuids.add(item['uuid'])
unique_result.append(item)
else:
unique_result.append(item)
combined_result = unique_result
if combined_result and isinstance(combined_result[0], dict) and 'datetime' in combined_result[0]:
combined_result = sorted(combined_result, key=lambda x: x.get('datetime', ''))
combined_result = tools.add_utc_suffix(combined_result)
combined_result = await self.response_postprocessing(
combined_result, dataset, request_data['full_path'], request_data.get('params'), request_data['method']
)
return combined_result, 200
async def _fetch_from_server(self, session, request_data, url, server_url, service):
"""Вспомогательная функция для получения данных с одного сервера"""
try:
method = request_data['method']
headers = {k: v for k, v in request_data['headers'].items()
if k.lower() not in ['content-length']}
async with session.request(
method,
url,
params=request_data.get('params') if method == 'GET' else None,
data=request_data.get('data') if method != 'GET' else None,
headers=headers
) as resp:
if resp.status == 200:
content = await resp.read()
return json.loads(content.decode('utf-8'))
return None
except Exception as e:
proxy_logger.error(f"[ALL] Error on {server_url}: {str(e)}")
tools.setFailedServer(server_url, service)
return None
proxy_handler = ProxyHandler()
async def handle_proxy(request):
"""Основной обработчик для всех запросов"""
path = request.path
method = request.method
proxy_logger.info(f"[INCOMING] {method} {path}")
if path == "/":
return await handle_internal_api(request)
# Проверка на внутренние запросы
for internal_path in config.get('internals', {}).get(method, []):
if path.startswith(internal_path):
proxy_logger.info(f"[INTERNAL] {method} {path} -> Flask")
return await handle_internal_api(request)
# Определение сервиса
service = None
service_path = None
for serv in config.keys():
if serv == 'internals':
continue
if path.startswith(f"/{serv}"):
service = serv
service_path = path[len(f"/{serv}"):]
break
# Проверка на Kibana
if not service:
return await handle_kibana(request)
# Аутентификация
token = request.headers.get('x-access-token', '')
error, message, user_id = checkRight(token, path, method)
if error:
client_ip = request.remote
ses_trainer_ip = tools.params['servers']['ses']['trainer'].split('://')[1].split(':')[0]
# Пропускаем без токена если запрос от локального IP или SES trainer (отсекаем только внешние)
if client_ip == '127.0.0.1' or client_ip == ses_trainer_ip:
user_id = None
else:
proxy_logger.warning(f"[AUTH FAILED] {client_ip}, path: {path}, error: {message}")
return web.json_response({'error': 1, 'message': message}, status=error)
# Получение dataset пользователя
dataset = None
if user_id:
error, user_data = DB.get('users', user_id)
if error:
return web.json_response({'error': 1, 'message': user_data}, status=405)
dataset = user_data.get('dataset', '0')
# Проверим, существует ли этот набор
error, d = DB.get(table='datasets', id=dataset)
if error:
return web.json_response({'error': 1, 'message': 'DatasetNotFound'}, status=405)
# Чтение тела запроса один раз
body = None
if request.can_read_body and method in ['POST', 'PUT', 'DELETE']:
body = await request.read()
# Парсинг данных
params = dict(request.query) if request.query else None
data = None
if body:
content_type = request.headers.get('Content-Type', '')
if 'application/json' in content_type:
try:
data = json.loads(body.decode('utf-8'))
except Exception:
data = body
elif 'application/x-www-form-urlencoded' in content_type:
data = body
elif 'multipart/form-data' in content_type:
data = body
else:
data = body
# Логирование пользователя
try:
request_data = {}
if params:
request_data.update(params)
if data and isinstance(data, dict):
request_data.update(data)
userlog(user_id, method, path, request_data)
except Exception as e:
proxy_logger.error(f"[USERLOG] Failed: {e}")
# Проксирование
try:
result = await proxy_handler.proxy_request(
service, service_path, method,
request.headers, params, data,
dataset, user_id
)
if len(result) == 2:
response_data, status = result
headers_out = {}
else:
response_data, status, headers_out = result
# Формирование ответа
if isinstance(response_data, tuple) and len(response_data) > 0 and response_data[0] == 'stream':
# Streaming ответ
_, upstream_resp = response_data
response = web.StreamResponse(
status=status,
headers={
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Transfer-Encoding': 'chunked'
}
)
await response.prepare(request)
try:
async for chunk in upstream_resp.content.iter_any():
if chunk:
await response.write(chunk)
except Exception as e:
proxy_logger.error(f"[STREAM ERROR] {e}")
finally:
await upstream_resp.release()
await response.write_eof()
return response
elif isinstance(response_data, (dict, list)):
return web.json_response(response_data, status=status, headers=headers_out)
else:
return web.Response(
body=response_data,
status=status,
headers=headers_out or {'Content-Type': 'application/octet-stream'}
)
except Exception as e:
proxy_logger.error(f"[ERROR] {traceback.format_exc()}")
return web.json_response({'error': 1, 'message': str(e)}, status=500)
async def handle_kibana(request):
"""Обработка запросов к Kibana"""
token_cookie = request.cookies.get('accessToken', '')
if not token_cookie:
proxy_logger.warning("[KIBANA] No accessToken cookie found")
return web.json_response({'error': 1, 'message': 'access denied'}, status=403)
try:
data = jwt.decode(token_cookie, key, algorithms=[alg])
if (data['type'] == 'access' and
time.time() - data['created'] < tools.params['auth']['accessLifeTime'] and
hasRights(data['user_id'], '/res/dashboards', 'GET')):
# Проксирование на Kibana
import base64
auth_string = f"{kibana_config['username']}:{kibana_config['password']}"
basic_auth = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')
headers = dict(request.headers)
headers['Authorization'] = f'Basic {basic_auth}'
headers = {k: v for k, v in headers.items() if k.lower() not in ['host', 'cookie']}
async with aiohttp.ClientSession(auto_decompress=False) as session:
url = kibana_config['url'] + request.path_qs
async with session.request(
request.method,
url,
data=await request.read() if request.can_read_body else None,
headers=headers
) as resp:
content = await resp.read()
response_headers = {}
for header_key, header_value in resp.headers.items():
if header_key.lower() not in ['connection', 'transfer-encoding', 'x-frame-options']:
response_headers[header_key] = header_value
response_headers.update({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'x-access-token',
'x-frame-options': 'SAMEORIGIN'
})
return web.Response(body=content, status=resp.status, headers=response_headers)
else:
proxy_logger.warning("[KIBANA] Access denied: token validation failed")
return web.json_response({'error': 1, 'message': 'access denied'}, status=403)
except Exception as e:
proxy_logger.error(f"[KIBANA] Error: {e}")
return web.json_response({'error': 1, 'message': 'access denied'}, status=403)
async def handle_internal_api(request):
"""Проксирование на внутренний Flask"""
try:
headers = {k: v for k, v in request.headers.items() if k.lower() not in ['host']}
async with aiohttp.ClientSession() as session:
url = f"http://127.0.0.1:6282{request.path_qs}"
async with session.request(
request.method,
url,
data=await request.read() if request.can_read_body else None,
headers=headers,
params=request.query if request.query else None
) as resp:
content = await resp.read()
response_headers = dict(resp.headers)
return web.Response(
body=content,
status=resp.status,
headers=response_headers
)
except Exception as e:
proxy_logger.error(f"[INTERNAL API] Error: {str(e)}")
return web.json_response({'error': 1, 'message': str(e)}, status=500)
async def start_proxy_server():
max_body_size = tools.params.get('proxy', {}).get('maxBodySize', 209715200) # Default 200MB
app = web.Application(client_max_size=max_body_size)
app.router.add_route('*', '/{tail:.*}', handle_proxy)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '0.0.0.0', 6182)
await site.start()
proxy_logger.info("[START] Proxy server started on 0.0.0.0:6182")
try:
await asyncio.Future()
finally:
await proxy_handler.close()
await runner.cleanup()
def run_proxy():
asyncio.run(start_proxy_server())
| 1 | import json |
| 2 | import logging |
| 3 | import traceback |
| 4 | import time |
| 5 | import os |
| 6 | import jwt |
| 7 | import asyncio |
| 8 | from urllib.parse import unquote, urlencode |
| 9 | from datetime import datetime |
| 10 | |
| 11 | import aiohttp |
| 12 | from aiohttp import web |
| 13 | |
| 14 | from app.tools import tools |
| 15 | from app.auth import key, alg, checkRight, userlog, hasRights |
| 16 | from app.db import DB |
| 17 | from app.logger import getProxyLogger |
| 18 | |
| 19 | proxy_logger = getProxyLogger() |
| 20 | |
| 21 | try: |
| 22 | config = json.load(open('app/conf/proxy.json', 'r')) |
| 23 | except Exception as error: |
| 24 | proxy_logger.error(f"Ошибка загрузки конфигурации прокси: {error}") |
| 25 | proxy_logger.error(traceback.format_exc()) |
| 26 | raise |
| 27 | |
| 28 | kibana = tools.params['servers']['res']['cluster'][0] |
| 29 | kibana_config = { |
| 30 | 'url': kibana, |
| 31 | 'username': tools.params['servers']['res'].get('auth', {}).get('username', ''), |
| 32 | 'password': tools.params['servers']['res'].get('auth', {}).get('password', '') |
| 33 | } |
| 34 | |
| 35 | |
| 36 | class ProxyHandler: |
| 37 | def __init__(self): |
| 38 | self.sessions = {} |
| 39 | |
| 40 | async def close(self): |
| 41 | """Закрытие всех сессий""" |
| 42 | for session_dict in self.sessions.values(): |
| 43 | for session in session_dict.values(): |
| 44 | await session.close() |
| 45 | |
| 46 | def get_session(self, service, method, path): |
| 47 | |
| 48 | t = config.get(service,{}).get('cache',{}).get(method,{}).get(path) |
| 49 | |
| 50 | if t: |
| 51 | connect_timeout = t |
| 52 | read_timeout = t |
| 53 | else: |
| 54 | connect_timeout=tools.params['servers'][service]['timers']['connect'] |
| 55 | read_timeout=tools.params['servers'][service]['timers']['read'] |
| 56 | |
| 57 | session_key = f"{connect_timeout}_{read_timeout}" |
| 58 | |
| 59 | if service not in self.sessions: |
| 60 | self.sessions[service] = {} |
| 61 | |
| 62 | if session_key not in self.sessions[service]: |
| 63 | timeout = aiohttp.ClientTimeout( |
| 64 | total=None, |
| 65 | connect=connect_timeout, |
| 66 | sock_read=read_timeout |
| 67 | ) |
| 68 | self.sessions[service][session_key] = aiohttp.ClientSession(timeout=timeout, auto_decompress=False) |
| 69 | |
| 70 | return self.sessions[service][session_key] |
| 71 | |
| 72 | async def response_postprocessing(self, response_data, dataset, request_path, request_params=None, request_method='GET', raw_response=None): |
| 73 | """Постобработка ответа - фильтрация по dataset""" |
| 74 | |
| 75 | if dataset is None: |
| 76 | dataset = "0" |
| 77 | |
| 78 | def new_value(value, dataset): |
| 79 | if dataset == "0": |
| 80 | if "~" in value: |
| 81 | return None |
| 82 | else: |
| 83 | if f"~{dataset}" not in value: |
| 84 | return None |
| 85 | value = "~".join(value.split('~')[:-1]) |
| 86 | return value |
| 87 | |
| 88 | # Логирование классификации |
| 89 | if '/smc/classify' in request_path: |
| 90 | corpus_name = datetime.today().strftime('%Y-%m-%d') + '.csv' |
| 91 | t = datetime.now().strftime("%H:%M:%S") |
| 92 | try: |
| 93 | model = unquote(request_path.split('/')[-1].split('?')[0]) |
| 94 | os.makedirs(f'data/smc/logs/{model}', exist_ok=True) |
| 95 | with open(f'data/smc/logs/{model}/{corpus_name}', 'a') as f: |
| 96 | query_text = request_params.get('text', '') if request_params else '' |
| 97 | response_text = raw_response if raw_response else json.dumps(response_data) |
| 98 | f.write(f"{t}\t{query_text}\t{response_text}\n") |
| 99 | except Exception as e: |
| 100 | proxy_logger.error(f"[SMC ERROR] Ошибка при логировании классификации: {e}") |
| 101 | |
| 102 | if not isinstance(response_data, dict): |
| 103 | return response_data |
| 104 | |
| 105 | if response_data.get('error'): |
| 106 | return response_data |
| 107 | |
| 108 | # Фильтрация результатов по dataset |
| 109 | for service, params in config.items(): |
| 110 | if not request_path.startswith(f"/{service}"): |
| 111 | continue |
| 112 | |
| 113 | for method in params.get("filters", {}): |
| 114 | |
| 115 | if method != request_method: |
| 116 | continue |
| 117 | |
| 118 | paths = params['filters'][method] |
| 119 | for path, filters in paths.items(): |
| 120 | if not request_path.startswith(f"/{service}{path}"): |
| 121 | continue |
| 122 | |
| 123 | for cnf in filters: |
| 124 | # Фильтрация списков |
| 125 | if cnf['type'] == 'list': |
| 126 | if cnf['name'] in response_data and isinstance(response_data[cnf['name']], list): |
| 127 | filtered_list = [] |
| 128 | for item in response_data[cnf['name']]: |
| 129 | value = new_value(item, dataset) |
| 130 | if value is not None: |
| 131 | filtered_list.append(value) |
| 132 | response_data[cnf['name']] = filtered_list |
| 133 | |
| 134 | # Фильтрация ключей |
| 135 | elif cnf['type'] == 'key': |
| 136 | keys_to_process = list(response_data.keys()) |
| 137 | for resp_key in keys_to_process: |
| 138 | if resp_key in cnf.get('exclude', []): |
| 139 | continue |
| 140 | value = new_value(resp_key, dataset) |
| 141 | if value is None: |
| 142 | del response_data[resp_key] |
| 143 | elif resp_key != value: |
| 144 | response_data[value] = response_data[resp_key] |
| 145 | del response_data[resp_key] |
| 146 | |
| 147 | return response_data |
| 148 | |
| 149 | async def modify_request_data(self, request, service, path, method, dataset, user_id): |
| 150 | """Модификация данных запроса - добавление dataset""" |
| 151 | |
| 152 | # Без авторизации или с дефолтным dataset не модифицируем |
| 153 | if user_id is None or dataset == "0" or dataset is None or dataset == "None": |
| 154 | return request |
| 155 | |
| 156 | dataset_config = config.get(service, {}).get('datasets', {}).get(method, {}).get(path) |
| 157 | if not dataset_config: |
| 158 | return request |
| 159 | |
| 160 | for param in dataset_config: |
| 161 | if param['type'] == 'path': |
| 162 | # Модификация пути |
| 163 | path_parts = request['full_path'].split('?') |
| 164 | url_parts = path_parts[0].split('/') |
| 165 | |
| 166 | if not param.get('splitter'): |
| 167 | |
| 168 | if url_parts[param['position']] in param.get('exclude', []): |
| 169 | return request |
| 170 | |
| 171 | if f"~{dataset}" not in url_parts[param['position']]: |
| 172 | url_parts[param['position']] = f"{url_parts[param['position']]}~{dataset}" |
| 173 | else: |
| 174 | split_parts = url_parts[param['position']].split(param['splitter']) |
| 175 | for i in range(len(split_parts)): |
| 176 | if split_parts[i] not in param.get('exclude', []): |
| 177 | if f"~{dataset}" not in split_parts[i]: |
| 178 | split_parts[i] = f"{split_parts[i].strip()}~{dataset}" |
| 179 | url_parts[param['position']] = param['splitter'].join(split_parts) |
| 180 | |
| 181 | path_parts[0] = '/'.join(url_parts) |
| 182 | request['full_path'] = '?'.join(path_parts) |
| 183 | |
| 184 | elif param['type'] == 'param': |
| 185 | # Модификация параметра |
| 186 | param_name = param['name'] |
| 187 | if 'params' in request and param_name in request['params']: |
| 188 | value = request['params'][param_name] |
| 189 | if value not in param.get('exclude', []) and f"~{dataset}" not in value: |
| 190 | request['params'][param_name] = f"{value}~{dataset}" |
| 191 | elif 'data' in request and isinstance(request['data'], dict) and param_name in request['data']: |
| 192 | value = request['data'][param_name] |
| 193 | if value not in param.get('exclude', []) and f"~{dataset}" not in value: |
| 194 | request['data'][param_name] = f"{value}~{dataset}" |
| 195 | |
| 196 | elif param['type'] == 'insert': |
| 197 | # Вставка нового параметра |
| 198 | param_name = param['name'] |
| 199 | if method == 'GET' and 'params' in request: |
| 200 | request['params'][param_name] = dataset |
| 201 | elif 'data' in request and isinstance(request['data'], dict): |
| 202 | # Если data это dict - добавляем в data |
| 203 | request['data'][param_name] = dataset |
| 204 | elif 'data' in request and isinstance(request['data'], bytes): |
| 205 | # Если data это urlencoded/multipart - добавляем в квери |
| 206 | if 'params' not in request: |
| 207 | request['params'] = {} |
| 208 | request['params'][param_name] = dataset |
| 209 | |
| 210 | return request |
| 211 | |
| 212 | async def change_request(self, request, service, servertype): |
| 213 | """Специальная обработка запросов для разных сервисов""" |
| 214 | |
| 215 | path = request['full_path'] |
| 216 | method = request['method'] |
| 217 | |
| 218 | # Обрабатываем data - может быть dict или bytes |
| 219 | data = request.get('data', {}) |
| 220 | if isinstance(data, bytes): |
| 221 | # Если это сырые байты, пытаемся распарсить |
| 222 | return 0, request, servertype |
| 223 | |
| 224 | # SMC/SEE: Проверка на модели-черновики |
| 225 | if '/see/entities' in path or '/smc/classify' in path: |
| 226 | model = path.split('/')[-1].split('?')[0] |
| 227 | if len(model) > 4 and model[-4:] == '-new': |
| 228 | servertype = 'trainer' |
| 229 | |
| 230 | # SMC: Замена корпуса на группы |
| 231 | elif '/smc/groups' in path and method == 'POST': |
| 232 | if isinstance(data, dict) and 'corpus' in data: |
| 233 | corpus = data['corpus'] |
| 234 | corpus_file = f"data/db/marks/{corpus}.json" |
| 235 | if not os.path.isfile(corpus_file): |
| 236 | return 1, f"CorpusNotFound: {corpus}", servertype |
| 237 | try: |
| 238 | grouplist = json.load(open(corpus_file, 'r'))['groups'] |
| 239 | groups = dict() |
| 240 | for item in grouplist: |
| 241 | if not item[1] in groups: |
| 242 | groups[item[1]] = list() |
| 243 | groups[item[1]].append(list(item[0])) |
| 244 | data['groups'] = json.dumps(groups) |
| 245 | del data['corpus'] |
| 246 | request['data'] = data |
| 247 | except Exception: |
| 248 | return 1, "GroupsLoadFailed", servertype |
| 249 | |
| 250 | # SMC: Обучение/тестирование |
| 251 | elif '/smc/train' in path or '/smc/test' in path: |
| 252 | if isinstance(data, dict) and 'corpus' in data: |
| 253 | corpus = data['corpus'] |
| 254 | is_marks = data.get('isMarks', '0') |
| 255 | corpus_path = f"data/db/marks/{corpus}.json" |
| 256 | |
| 257 | if not os.path.isfile(corpus_path): |
| 258 | return 1, f"CorpusNotFound: {corpus}", servertype |
| 259 | |
| 260 | try: |
| 261 | corpus_name = json.load(open(corpus_path, 'r'))['name'] |
| 262 | except Exception: |
| 263 | return 1, f"CorpusCorrupted: {corpus}", servertype |
| 264 | |
| 265 | if str(is_marks) != '1': |
| 266 | csv, _ = tools.loadCorpusToCSV(corpusFile=corpus_path) |
| 267 | else: |
| 268 | csv = open(corpus_path, 'r').read() |
| 269 | |
| 270 | data['corpusName'] = corpus_name |
| 271 | data['corpusData'] = csv |
| 272 | del data['corpus'] |
| 273 | request['data'] = data |
| 274 | |
| 275 | if not path.endswith('-new'): |
| 276 | request['full_path'] = path + '-new' |
| 277 | |
| 278 | # SEE: Обучение |
| 279 | elif '/see/train' in path: |
| 280 | if isinstance(data, dict) and 'corpus' in data: |
| 281 | corpus = data['corpus'] |
| 282 | corpus_path = f"data/db/marks/{corpus}.json" |
| 283 | |
| 284 | if not os.path.isfile(corpus_path): |
| 285 | return 1, f"CorpusNotFound: {corpus}", servertype |
| 286 | |
| 287 | try: |
| 288 | corpus_name = json.load(open(corpus_path, 'r'))['name'] |
| 289 | except Exception: |
| 290 | return 1, f"CorpusCorrupted: {corpus}", servertype |
| 291 | |
| 292 | csv, _ = tools.loadCorpusToCSV(corpusFile=corpus_path) |
| 293 | data['corpusName'] = corpus_name |
| 294 | data['corpusData'] = csv |
| 295 | del data['corpus'] |
| 296 | request['data'] = data |
| 297 | |
| 298 | if not path.endswith('-new'): |
| 299 | request['full_path'] = path + '-new' |
| 300 | |
| 301 | # SMC: Автоматическая разметка |
| 302 | elif '/smc/automark/start' in path: |
| 303 | if isinstance(data, dict): |
| 304 | corpus = data.get('corpus') |
| 305 | marks = data.get('marks') |
| 306 | |
| 307 | if corpus and marks: |
| 308 | try: |
| 309 | corpus_data = json.load(open(f"data/db/marks/{corpus}.json", "r"))['data'] |
| 310 | except Exception: |
| 311 | return 1, f"CorpusCorrupted: {corpus}", servertype |
| 312 | |
| 313 | try: |
| 314 | marks_data = json.load(open(f"data/db/marks/{marks}.json", "r"))['data'] |
| 315 | except Exception: |
| 316 | return 1, f"CorpusCorrupted: {marks}", servertype |
| 317 | |
| 318 | data['corpus'] = json.dumps(corpus_data) |
| 319 | data['marks'] = json.dumps(marks_data) |
| 320 | request['data'] = data |
| 321 | |
| 322 | return 0, request, servertype |
| 323 | |
| 324 | def parse_multipart_form(self, content, content_type): |
| 325 | """Парсинг multipart/form-data""" |
| 326 | if not content_type or 'multipart/form-data' not in content_type: |
| 327 | return None |
| 328 | |
| 329 | try: |
| 330 | boundary = content_type.split('boundary=')[-1].strip() |
| 331 | if boundary.startswith('"') and boundary.endswith('"'): |
| 332 | boundary = boundary[1:-1] |
| 333 | |
| 334 | parts = content.split(f'--{boundary}'.encode()) |
| 335 | form_data = {} |
| 336 | |
| 337 | for part in parts: |
| 338 | if not part or part == b'--\r\n' or part == b'--': |
| 339 | continue |
| 340 | |
| 341 | # Разделяем заголовки и данные |
| 342 | if b'\r\n\r\n' in part: |
| 343 | headers_part, data_part = part.split(b'\r\n\r\n', 1) |
| 344 | |
| 345 | # Ищем имя поля |
| 346 | name_match = None |
| 347 | for line in headers_part.split(b'\r\n'): |
| 348 | if b'name="' in line: |
| 349 | name_start = line.find(b'name="') + 6 |
| 350 | name_end = line.find(b'"', name_start) |
| 351 | if name_end > name_start: |
| 352 | name_match = line[name_start:name_end].decode('utf-8', errors='ignore') |
| 353 | break |
| 354 | |
| 355 | if name_match: |
| 356 | # Убираем trailing \r\n |
| 357 | data_part = data_part.rstrip(b'\r\n') |
| 358 | form_data[name_match] = data_part.decode('utf-8', errors='ignore') |
| 359 | |
| 360 | return form_data |
| 361 | except Exception as e: |
| 362 | proxy_logger.error(f"[MULTIPART] Parse error: {e}") |
| 363 | return None |
| 364 | |
| 365 | def parse_urlencoded_form(self, content): |
| 366 | """Парсинг application/x-www-form-urlencoded""" |
| 367 | try: |
| 368 | from urllib.parse import parse_qs |
| 369 | decoded = content.decode('utf-8', errors='ignore') |
| 370 | parsed = parse_qs(decoded, keep_blank_values=True) |
| 371 | # Преобразуем списки в единичные значения |
| 372 | return {k: v[0] if len(v) == 1 else v for k, v in parsed.items()} |
| 373 | except Exception as e: |
| 374 | proxy_logger.error(f"[URLENCODED] Parse error: {e}") |
| 375 | return None |
| 376 | |
| 377 | async def proxy_request(self, service, path, method, headers, params, data, dataset, user_id): |
| 378 | """Основная функция проксирования""" |
| 379 | |
| 380 | proxy_logger.info(f"[PROXY] {method} /{service}{path}") |
| 381 | |
| 382 | # Определяем тип сервера и путь из конфигурации |
| 383 | servertype = None |
| 384 | matched_path = None |
| 385 | |
| 386 | for conf_method, paths in config[service].get('requests', {}).items(): |
| 387 | if conf_method != method: |
| 388 | continue |
| 389 | for conf_path, conf_servertype in paths.items(): |
| 390 | if path.startswith(conf_path): |
| 391 | servertype = conf_servertype |
| 392 | matched_path = conf_path |
| 393 | break |
| 394 | if servertype: |
| 395 | break |
| 396 | |
| 397 | if not servertype: |
| 398 | return {'error': 1, 'message': 'Route not found'}, 404 |
| 399 | |
| 400 | # Статические файлы |
| 401 | if "/" in servertype: |
| 402 | if not os.path.isfile(servertype): |
| 403 | return {'error': 1, 'message': 'File not found'}, 404 |
| 404 | with open(servertype, 'rb') as f: |
| 405 | return f.read(), 200, {'Content-Type': 'text/javascript; charset=utf-8'} |
| 406 | |
| 407 | # Фейковый запрос |
| 408 | if servertype == 'fake': |
| 409 | return b'', 200, {'Content-Type': 'application/octet-stream', 'Content-Length': '50'} |
| 410 | |
| 411 | request_data = { |
| 412 | 'method': method, |
| 413 | 'path': path, |
| 414 | 'full_path': f"/{service}{path}", |
| 415 | 'headers': dict(headers), |
| 416 | 'params': params or {}, |
| 417 | 'data': data or {} |
| 418 | } |
| 419 | |
| 420 | # Специальная обработка |
| 421 | error, result, servertype = await self.change_request(request_data, service, servertype) |
| 422 | if error: |
| 423 | return {'error': 1, 'message': result}, 405 |
| 424 | |
| 425 | request_data = result |
| 426 | |
| 427 | # Добавление dataset |
| 428 | request_data = await self.modify_request_data( |
| 429 | request_data, service, matched_path, method, dataset, user_id |
| 430 | ) |
| 431 | |
| 432 | # Определение серверов для запроса |
| 433 | if servertype in ['find', 'all']: |
| 434 | servers = list(tools.params['servers'][service]['cluster']) |
| 435 | if not isinstance(servers, list): |
| 436 | servers = list([servers]) |
| 437 | else: |
| 438 | servers = list(servers) |
| 439 | |
| 440 | # Специальная обработка для архива |
| 441 | if '/archive/dialog' in path: |
| 442 | dialog_id = path.split('/')[-1] |
| 443 | servers.insert(0, (tools.params['servers']['res']['trainer'], f'/res/dialog/{dialog_id}')) |
| 444 | elif '/archive/list' in path: |
| 445 | res_path = '/res/archive/list' |
| 446 | if '?' in path: |
| 447 | res_path += '?' + path.split('?')[-1] |
| 448 | servers.insert(0, (tools.params['servers']['res']['trainer'], res_path)) |
| 449 | else: |
| 450 | servers = None |
| 451 | |
| 452 | # Выполнение запроса |
| 453 | if servertype == 'find': |
| 454 | return await self.proxy_find(service, request_data, servers, dataset) |
| 455 | elif servertype == 'all': |
| 456 | return await self.proxy_all(service, request_data, servers, dataset) |
| 457 | else: |
| 458 | return await self.proxy_single(service, request_data, servertype, dataset) |
| 459 | |
| 460 | async def proxy_single(self, service, request_data, servertype, dataset): |
| 461 | """Проксирование на один сервер (cluster/trainer) с повторными попытками при ошибках""" |
| 462 | |
| 463 | if servertype == 'trainer': |
| 464 | max_attempts = 1 |
| 465 | else: # cluster |
| 466 | servers = tools.params['servers'][service].get('cluster', []) |
| 467 | max_attempts = len(servers) if isinstance(servers, list) else 1 |
| 468 | |
| 469 | method = request_data['method'] |
| 470 | session = self.get_session(service=service, method=method, path=request_data['path']) |
| 471 | |
| 472 | |
| 473 | for attempt in range(max_attempts): |
| 474 | server_url = tools.getProductionServer(servicetype=service, servertype=servertype) |
| 475 | url = server_url + request_data['full_path'] |
| 476 | headers = {k: v for k, v in request_data['headers'].items() |
| 477 | if k.lower() not in ['content-length']} |
| 478 | |
| 479 | proxy_logger.info(f"[SINGLE][{service.upper()}] Attempt {attempt + 1}/{max_attempts} -> {server_url}") |
| 480 | |
| 481 | try: |
| 482 | if method == 'GET': |
| 483 | resp = await session.get(url, params=request_data['params'], headers=headers) |
| 484 | |
| 485 | # это streaming ответ |
| 486 | content_type = resp.headers.get('Content-Type', '') |
| 487 | if 'text/event-stream' in content_type: |
| 488 | proxy_logger.info(f"[STREAMING][{service.upper()}] {server_url}") |
| 489 | # Возвращаем маркер streaming и response объект |
| 490 | return ('stream', resp), resp.status |
| 491 | |
| 492 | # Обычный ответ - читаем полностью |
| 493 | async with resp: |
| 494 | content = await resp.read() |
| 495 | status = resp.status |
| 496 | |
| 497 | elif method == 'POST': |
| 498 | if isinstance(request_data['data'], dict): |
| 499 | headers['Content-Type'] = 'application/json' |
| 500 | resp = await session.post(url, json=request_data['data'], headers=headers, params=request_data['params']) |
| 501 | else: |
| 502 | resp = await session.post(url, data=request_data['data'], headers=headers, params=request_data['params']) |
| 503 | |
| 504 | # Проверка на streaming ответ |
| 505 | content_type = resp.headers.get('Content-Type', '') |
| 506 | if 'text/event-stream' in content_type: |
| 507 | proxy_logger.info(f"[STREAMING][{service.upper()}] {server_url}") |
| 508 | return ('stream', resp), resp.status |
| 509 | |
| 510 | # Обычный ответ - читаем полностью |
| 511 | async with resp: |
| 512 | content = await resp.read() |
| 513 | status = resp.status |
| 514 | elif method == 'DELETE': |
| 515 | if isinstance(request_data['data'], dict): |
| 516 | headers['Content-Type'] = 'application/json' |
| 517 | async with session.delete(url, json=request_data['data'], headers=headers, params=request_data['params']) as resp: |
| 518 | content = await resp.read() |
| 519 | status = resp.status |
| 520 | else: |
| 521 | async with session.delete(url, data=request_data['data'], headers=headers, params=request_data['params']) as resp: |
| 522 | content = await resp.read() |
| 523 | status = resp.status |
| 524 | elif method == 'PUT': |
| 525 | if isinstance(request_data['data'], dict): |
| 526 | headers['Content-Type'] = 'application/json' |
| 527 | async with session.put(url, json=request_data['data'], headers=headers, params=request_data['params']) as resp: |
| 528 | content = await resp.read() |
| 529 | status = resp.status |
| 530 | else: |
| 531 | async with session.put(url, data=request_data['data'], headers=headers, params=request_data['params']) as resp: |
| 532 | content = await resp.read() |
| 533 | status = resp.status |
| 534 | else: |
| 535 | return {'error': 1, 'message': 'Method not supported'}, 405 |
| 536 | |
| 537 | if status >= 400: |
| 538 | try: |
| 539 | error_text = content.decode('utf-8') |
| 540 | proxy_logger.error(f"[RESPONSE ERROR][{service.upper()}] ERROR {status} {server_url}: {error_text}") |
| 541 | except: |
| 542 | proxy_logger.error(f"[RESPONSE ERROR][{service.upper()}] ERROR {status} {server_url}: (binary data, {len(content)} bytes)") |
| 543 | else: |
| 544 | proxy_logger.info(f"[RESPONSE][{service.upper()}] {status} {server_url}") |
| 545 | |
| 546 | try: |
| 547 | response_data = json.loads(content.decode('utf-8')) |
| 548 | response_data = await self.response_postprocessing( |
| 549 | response_data, dataset, request_data['full_path'], request_data.get('params'), request_data['method'] |
| 550 | ) |
| 551 | |
| 552 | # Сохраним результат в кэше, если это требуется |
| 553 | id = self.get_cache_id(service=service,method=method,path=request_data['path'],dataset=dataset) |
| 554 | if id: |
| 555 | cont = json.dumps(response_data) |
| 556 | error, result = DB.get(table='cache',id=id,md5clean=False) |
| 557 | if error: |
| 558 | logging.info(DB.create(table='cache',data={'id': id, 'content': cont})) |
| 559 | else: |
| 560 | if DB.md5(cont) != result['md5']: |
| 561 | DB.modify(table='cache',data={'id': id, 'content': cont}) |
| 562 | |
| 563 | return response_data, status |
| 564 | except Exception: |
| 565 | return content, status |
| 566 | |
| 567 | except (asyncio.TimeoutError, Exception) as e: |
| 568 | error_type = "TIMEOUT" if isinstance(e, asyncio.TimeoutError) else "ERROR" |
| 569 | proxy_logger.error(f"[{error_type}][{service.upper()}] {server_url}: {str(e)}") |
| 570 | tools.setFailedServer(server_url, service) |
| 571 | |
| 572 | # Если таймаут, то пробуем вернуть из кэша |
| 573 | if error_type == "TIMEOUT": |
| 574 | id = self.get_cache_id(service=service,method=method,path=request_data['path'],dataset=dataset) |
| 575 | if id: |
| 576 | error, result = DB.get(table='cache',id=id,md5clean=False) |
| 577 | if not error: |
| 578 | return json.loads(result['content']), 200 |
| 579 | |
| 580 | # Если это последняя попытка - возвращаем ошибку |
| 581 | if attempt == max_attempts - 1: |
| 582 | if isinstance(e, asyncio.TimeoutError): |
| 583 | return {'error': 1, 'message': 'Request timeout'}, 504 |
| 584 | return {'error': 1, 'message': str(e)}, 500 |
| 585 | |
| 586 | proxy_logger.info("[RETRY] Trying next server...") |
| 587 | continue |
| 588 | |
| 589 | return {'error': 1, 'message': 'No servers available'}, 503 |
| 590 | |
| 591 | def get_cache_id(self,service,method,path,dataset=None): |
| 592 | |
| 593 | if path in config.get(service,{}).get('cache',{}).get(method,{}): |
| 594 | |
| 595 | cache_key = f"{method}:/{service}{path}:{dataset if dataset else '0'}" |
| 596 | return DB.md5(cache_key) |
| 597 | else: |
| 598 | return False |
| 599 | |
| 600 | async def proxy_find(self, service, request_data, servers, dataset): |
| 601 | """Поиск на первом доступном сервере""" |
| 602 | |
| 603 | method = request_data['method'] |
| 604 | session = self.get_session(service=service, method=method, path=request_data['path']) |
| 605 | |
| 606 | for server_info in servers: |
| 607 | if isinstance(server_info, tuple): |
| 608 | server_url, custom_path = server_info |
| 609 | url = server_url + custom_path |
| 610 | else: |
| 611 | server_url = server_info |
| 612 | url = server_url + request_data['full_path'] |
| 613 | |
| 614 | proxy_logger.info(f"[FIND][{service.upper()}] Trying {server_url}") |
| 615 | |
| 616 | try: |
| 617 | method = request_data['method'] |
| 618 | headers = {k: v for k, v in request_data['headers'].items() |
| 619 | if k.lower() not in ['content-length']} |
| 620 | |
| 621 | if method == 'GET': |
| 622 | async with session.get(url, params=request_data['params'], headers=headers) as resp: |
| 623 | if resp.status == 200: |
| 624 | content = await resp.read() |
| 625 | try: |
| 626 | response_data = json.loads(content.decode('utf-8')) |
| 627 | response_data = await self.response_postprocessing( |
| 628 | response_data, dataset, request_data['full_path'], request_data.get('params'), request_data['method'] |
| 629 | ) |
| 630 | proxy_logger.info(f"[FIND] Success on {server_url}") |
| 631 | return response_data, 200 |
| 632 | except Exception: |
| 633 | return content, 200 |
| 634 | else: |
| 635 | proxy_logger.info(f"[FIND] {resp.status} from {server_url}, trying next") |
| 636 | continue |
| 637 | else: |
| 638 | # Для POST/DELETE/PUT аналогично |
| 639 | async with session.request(method, url, data=request_data.get('data'), headers=headers) as resp: |
| 640 | if resp.status == 200: |
| 641 | content = await resp.read() |
| 642 | try: |
| 643 | response_data = json.loads(content.decode('utf-8')) |
| 644 | response_data = await self.response_postprocessing( |
| 645 | response_data, dataset, request_data['full_path'], request_data.get('params'), request_data['method'] |
| 646 | ) |
| 647 | return response_data, 200 |
| 648 | except Exception: |
| 649 | return content, 200 |
| 650 | else: |
| 651 | continue |
| 652 | |
| 653 | except Exception as e: |
| 654 | proxy_logger.error(f"[FIND] Error on {server_url}: {str(e)}") |
| 655 | tools.setFailedServer(server_url, service) |
| 656 | continue |
| 657 | |
| 658 | return {'error': 1, 'message': 'No servers available'}, 503 |
| 659 | |
| 660 | async def proxy_all(self, service, request_data, servers, dataset): |
| 661 | """Сбор данных со всех серверов и объединение результатов""" |
| 662 | |
| 663 | method = request_data['method'] |
| 664 | session = self.get_session(service=service, method=method, path=request_data['path']) |
| 665 | |
| 666 | tasks = [] |
| 667 | for server_info in servers: |
| 668 | if isinstance(server_info, tuple): |
| 669 | server_url, custom_path = server_info |
| 670 | url = server_url + custom_path |
| 671 | else: |
| 672 | server_url = server_info |
| 673 | url = server_url + request_data['full_path'] |
| 674 | |
| 675 | tasks.append(self._fetch_from_server(session, request_data, url, server_url, service)) |
| 676 | |
| 677 | responses = await asyncio.gather(*tasks, return_exceptions=True) |
| 678 | |
| 679 | # Объединение результатов |
| 680 | combined_result = None |
| 681 | for resp_data in responses: |
| 682 | if isinstance(resp_data, Exception): |
| 683 | continue |
| 684 | if resp_data is None: |
| 685 | continue |
| 686 | |
| 687 | if combined_result is None: |
| 688 | combined_result = [] if isinstance(resp_data, list) else {} |
| 689 | |
| 690 | if isinstance(combined_result, list) and isinstance(resp_data, list): |
| 691 | combined_result.extend(resp_data) |
| 692 | elif isinstance(combined_result, dict) and isinstance(resp_data, dict): |
| 693 | combined_result.update(resp_data) |
| 694 | |
| 695 | if combined_result is None: |
| 696 | return {'error': 1, 'message': 'No data collected'}, 503 |
| 697 | |
| 698 | # Уникализация и сортировка |
| 699 | if isinstance(combined_result, list) and combined_result: |
| 700 | if 'uuid' in str(combined_result): |
| 701 | seen_uuids = set() |
| 702 | unique_result = [] |
| 703 | for item in combined_result: |
| 704 | if isinstance(item, dict) and 'uuid' in item: |
| 705 | if item['uuid'] not in seen_uuids: |
| 706 | seen_uuids.add(item['uuid']) |
| 707 | unique_result.append(item) |
| 708 | else: |
| 709 | unique_result.append(item) |
| 710 | combined_result = unique_result |
| 711 | |
| 712 | if combined_result and isinstance(combined_result[0], dict) and 'datetime' in combined_result[0]: |
| 713 | combined_result = sorted(combined_result, key=lambda x: x.get('datetime', '')) |
| 714 | |
| 715 | combined_result = tools.add_utc_suffix(combined_result) |
| 716 | combined_result = await self.response_postprocessing( |
| 717 | combined_result, dataset, request_data['full_path'], request_data.get('params'), request_data['method'] |
| 718 | ) |
| 719 | |
| 720 | return combined_result, 200 |
| 721 | |
| 722 | async def _fetch_from_server(self, session, request_data, url, server_url, service): |
| 723 | """Вспомогательная функция для получения данных с одного сервера""" |
| 724 | try: |
| 725 | method = request_data['method'] |
| 726 | headers = {k: v for k, v in request_data['headers'].items() |
| 727 | if k.lower() not in ['content-length']} |
| 728 | |
| 729 | async with session.request( |
| 730 | method, |
| 731 | url, |
| 732 | params=request_data.get('params') if method == 'GET' else None, |
| 733 | data=request_data.get('data') if method != 'GET' else None, |
| 734 | headers=headers |
| 735 | ) as resp: |
| 736 | if resp.status == 200: |
| 737 | content = await resp.read() |
| 738 | return json.loads(content.decode('utf-8')) |
| 739 | return None |
| 740 | except Exception as e: |
| 741 | proxy_logger.error(f"[ALL] Error on {server_url}: {str(e)}") |
| 742 | tools.setFailedServer(server_url, service) |
| 743 | return None |
| 744 | |
| 745 | |
| 746 | proxy_handler = ProxyHandler() |
| 747 | |
| 748 | |
| 749 | async def handle_proxy(request): |
| 750 | """Основной обработчик для всех запросов""" |
| 751 | |
| 752 | path = request.path |
| 753 | method = request.method |
| 754 | |
| 755 | proxy_logger.info(f"[INCOMING] {method} {path}") |
| 756 | |
| 757 | if path == "/": |
| 758 | return await handle_internal_api(request) |
| 759 | |
| 760 | # Проверка на внутренние запросы |
| 761 | for internal_path in config.get('internals', {}).get(method, []): |
| 762 | if path.startswith(internal_path): |
| 763 | proxy_logger.info(f"[INTERNAL] {method} {path} -> Flask") |
| 764 | return await handle_internal_api(request) |
| 765 | |
| 766 | # Определение сервиса |
| 767 | service = None |
| 768 | service_path = None |
| 769 | for serv in config.keys(): |
| 770 | if serv == 'internals': |
| 771 | continue |
| 772 | if path.startswith(f"/{serv}"): |
| 773 | service = serv |
| 774 | service_path = path[len(f"/{serv}"):] |
| 775 | break |
| 776 | |
| 777 | # Проверка на Kibana |
| 778 | if not service: |
| 779 | return await handle_kibana(request) |
| 780 | |
| 781 | # Аутентификация |
| 782 | token = request.headers.get('x-access-token', '') |
| 783 | error, message, user_id = checkRight(token, path, method) |
| 784 | |
| 785 | if error: |
| 786 | client_ip = request.remote |
| 787 | ses_trainer_ip = tools.params['servers']['ses']['trainer'].split('://')[1].split(':')[0] |
| 788 | |
| 789 | # Пропускаем без токена если запрос от локального IP или SES trainer (отсекаем только внешние) |
| 790 | if client_ip == '127.0.0.1' or client_ip == ses_trainer_ip: |
| 791 | user_id = None |
| 792 | else: |
| 793 | proxy_logger.warning(f"[AUTH FAILED] {client_ip}, path: {path}, error: {message}") |
| 794 | return web.json_response({'error': 1, 'message': message}, status=error) |
| 795 | |
| 796 | # Получение dataset пользователя |
| 797 | dataset = None |
| 798 | if user_id: |
| 799 | error, user_data = DB.get('users', user_id) |
| 800 | if error: |
| 801 | return web.json_response({'error': 1, 'message': user_data}, status=405) |
| 802 | dataset = user_data.get('dataset', '0') |
| 803 | |
| 804 | # Проверим, существует ли этот набор |
| 805 | error, d = DB.get(table='datasets', id=dataset) |
| 806 | if error: |
| 807 | return web.json_response({'error': 1, 'message': 'DatasetNotFound'}, status=405) |
| 808 | |
| 809 | # Чтение тела запроса один раз |
| 810 | body = None |
| 811 | if request.can_read_body and method in ['POST', 'PUT', 'DELETE']: |
| 812 | body = await request.read() |
| 813 | |
| 814 | # Парсинг данных |
| 815 | params = dict(request.query) if request.query else None |
| 816 | data = None |
| 817 | |
| 818 | if body: |
| 819 | content_type = request.headers.get('Content-Type', '') |
| 820 | |
| 821 | if 'application/json' in content_type: |
| 822 | try: |
| 823 | data = json.loads(body.decode('utf-8')) |
| 824 | except Exception: |
| 825 | data = body |
| 826 | |
| 827 | elif 'application/x-www-form-urlencoded' in content_type: |
| 828 | data = body |
| 829 | |
| 830 | elif 'multipart/form-data' in content_type: |
| 831 | data = body |
| 832 | |
| 833 | else: |
| 834 | data = body |
| 835 | |
| 836 | # Логирование пользователя |
| 837 | try: |
| 838 | request_data = {} |
| 839 | if params: |
| 840 | request_data.update(params) |
| 841 | if data and isinstance(data, dict): |
| 842 | request_data.update(data) |
| 843 | |
| 844 | userlog(user_id, method, path, request_data) |
| 845 | |
| 846 | except Exception as e: |
| 847 | proxy_logger.error(f"[USERLOG] Failed: {e}") |
| 848 | |
| 849 | # Проксирование |
| 850 | try: |
| 851 | |
| 852 | result = await proxy_handler.proxy_request( |
| 853 | service, service_path, method, |
| 854 | request.headers, params, data, |
| 855 | dataset, user_id |
| 856 | ) |
| 857 | |
| 858 | if len(result) == 2: |
| 859 | response_data, status = result |
| 860 | headers_out = {} |
| 861 | else: |
| 862 | response_data, status, headers_out = result |
| 863 | |
| 864 | # Формирование ответа |
| 865 | if isinstance(response_data, tuple) and len(response_data) > 0 and response_data[0] == 'stream': |
| 866 | # Streaming ответ |
| 867 | _, upstream_resp = response_data |
| 868 | response = web.StreamResponse( |
| 869 | status=status, |
| 870 | headers={ |
| 871 | 'Content-Type': 'text/event-stream', |
| 872 | 'Cache-Control': 'no-cache', |
| 873 | 'Transfer-Encoding': 'chunked' |
| 874 | } |
| 875 | ) |
| 876 | await response.prepare(request) |
| 877 | |
| 878 | try: |
| 879 | async for chunk in upstream_resp.content.iter_any(): |
| 880 | if chunk: |
| 881 | await response.write(chunk) |
| 882 | |
| 883 | except Exception as e: |
| 884 | proxy_logger.error(f"[STREAM ERROR] {e}") |
| 885 | |
| 886 | finally: |
| 887 | await upstream_resp.release() |
| 888 | await response.write_eof() |
| 889 | |
| 890 | return response |
| 891 | |
| 892 | elif isinstance(response_data, (dict, list)): |
| 893 | return web.json_response(response_data, status=status, headers=headers_out) |
| 894 | else: |
| 895 | return web.Response( |
| 896 | body=response_data, |
| 897 | status=status, |
| 898 | headers=headers_out or {'Content-Type': 'application/octet-stream'} |
| 899 | ) |
| 900 | |
| 901 | except Exception as e: |
| 902 | proxy_logger.error(f"[ERROR] {traceback.format_exc()}") |
| 903 | return web.json_response({'error': 1, 'message': str(e)}, status=500) |
| 904 | |
| 905 | |
| 906 | async def handle_kibana(request): |
| 907 | """Обработка запросов к Kibana""" |
| 908 | |
| 909 | token_cookie = request.cookies.get('accessToken', '') |
| 910 | if not token_cookie: |
| 911 | proxy_logger.warning("[KIBANA] No accessToken cookie found") |
| 912 | return web.json_response({'error': 1, 'message': 'access denied'}, status=403) |
| 913 | |
| 914 | try: |
| 915 | data = jwt.decode(token_cookie, key, algorithms=[alg]) |
| 916 | if (data['type'] == 'access' and |
| 917 | time.time() - data['created'] < tools.params['auth']['accessLifeTime'] and |
| 918 | hasRights(data['user_id'], '/res/dashboards', 'GET')): |
| 919 | |
| 920 | # Проксирование на Kibana |
| 921 | import base64 |
| 922 | auth_string = f"{kibana_config['username']}:{kibana_config['password']}" |
| 923 | basic_auth = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8') |
| 924 | |
| 925 | headers = dict(request.headers) |
| 926 | headers['Authorization'] = f'Basic {basic_auth}' |
| 927 | headers = {k: v for k, v in headers.items() if k.lower() not in ['host', 'cookie']} |
| 928 | |
| 929 | async with aiohttp.ClientSession(auto_decompress=False) as session: |
| 930 | url = kibana_config['url'] + request.path_qs |
| 931 | async with session.request( |
| 932 | request.method, |
| 933 | url, |
| 934 | data=await request.read() if request.can_read_body else None, |
| 935 | headers=headers |
| 936 | ) as resp: |
| 937 | |
| 938 | content = await resp.read() |
| 939 | |
| 940 | response_headers = {} |
| 941 | for header_key, header_value in resp.headers.items(): |
| 942 | if header_key.lower() not in ['connection', 'transfer-encoding', 'x-frame-options']: |
| 943 | response_headers[header_key] = header_value |
| 944 | |
| 945 | response_headers.update({ |
| 946 | 'Access-Control-Allow-Origin': '*', |
| 947 | 'Access-Control-Allow-Credentials': 'true', |
| 948 | 'Access-Control-Allow-Headers': 'x-access-token', |
| 949 | 'x-frame-options': 'SAMEORIGIN' |
| 950 | }) |
| 951 | |
| 952 | return web.Response(body=content, status=resp.status, headers=response_headers) |
| 953 | else: |
| 954 | proxy_logger.warning("[KIBANA] Access denied: token validation failed") |
| 955 | return web.json_response({'error': 1, 'message': 'access denied'}, status=403) |
| 956 | except Exception as e: |
| 957 | proxy_logger.error(f"[KIBANA] Error: {e}") |
| 958 | return web.json_response({'error': 1, 'message': 'access denied'}, status=403) |
| 959 | |
| 960 | |
| 961 | async def handle_internal_api(request): |
| 962 | """Проксирование на внутренний Flask""" |
| 963 | |
| 964 | try: |
| 965 | headers = {k: v for k, v in request.headers.items() if k.lower() not in ['host']} |
| 966 | |
| 967 | async with aiohttp.ClientSession() as session: |
| 968 | url = f"http://127.0.0.1:6282{request.path_qs}" |
| 969 | |
| 970 | async with session.request( |
| 971 | request.method, |
| 972 | url, |
| 973 | data=await request.read() if request.can_read_body else None, |
| 974 | headers=headers, |
| 975 | params=request.query if request.query else None |
| 976 | ) as resp: |
| 977 | content = await resp.read() |
| 978 | response_headers = dict(resp.headers) |
| 979 | return web.Response( |
| 980 | body=content, |
| 981 | status=resp.status, |
| 982 | headers=response_headers |
| 983 | ) |
| 984 | except Exception as e: |
| 985 | proxy_logger.error(f"[INTERNAL API] Error: {str(e)}") |
| 986 | return web.json_response({'error': 1, 'message': str(e)}, status=500) |
| 987 | |
| 988 | |
| 989 | async def start_proxy_server(): |
| 990 | max_body_size = tools.params.get('proxy', {}).get('maxBodySize', 209715200) # Default 200MB |
| 991 | app = web.Application(client_max_size=max_body_size) |
| 992 | app.router.add_route('*', '/{tail:.*}', handle_proxy) |
| 993 | |
| 994 | runner = web.AppRunner(app) |
| 995 | await runner.setup() |
| 996 | |
| 997 | site = web.TCPSite(runner, '0.0.0.0', 6182) |
| 998 | await site.start() |
| 999 | |
| 1000 | proxy_logger.info("[START] Proxy server started on 0.0.0.0:6182") |
| 1001 | |
| 1002 | try: |
| 1003 | await asyncio.Future() |
| 1004 | finally: |
| 1005 | await proxy_handler.close() |
| 1006 | await runner.cleanup() |
| 1007 | |
| 1008 | |
| 1009 | def run_proxy(): |
| 1010 | asyncio.run(start_proxy_server()) |