en2zmax revised this gist . Go to revision
No changes
en2zmax revised this gist . Go to revision
No changes
en2zmax revised this gist . Go to revision
1 file changed, 1010 insertions
patch-ups5-for-uc-flora(file created)
| @@ -0,0 +1,1010 @@ | |||
| 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()) | |
Newer
Older