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())