Last active 1791962501

replace .so file in app dir

Revision 3af53bccfa49f28f850ae5b3be31fc5e9a8169f7

patch-ups5-for-uc-flora Raw
1import json
2import logging
3import traceback
4import time
5import os
6import jwt
7import asyncio
8from urllib.parse import unquote, urlencode
9from datetime import datetime
10
11import aiohttp
12from aiohttp import web
13
14from app.tools import tools
15from app.auth import key, alg, checkRight, userlog, hasRights
16from app.db import DB
17from app.logger import getProxyLogger
18
19proxy_logger = getProxyLogger()
20
21try:
22 config = json.load(open('app/conf/proxy.json', 'r'))
23except Exception as error:
24 proxy_logger.error(f"Ошибка загрузки конфигурации прокси: {error}")
25 proxy_logger.error(traceback.format_exc())
26 raise
27
28kibana = tools.params['servers']['res']['cluster'][0]
29kibana_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
36class 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
746proxy_handler = ProxyHandler()
747
748
749async 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
906async 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
961async 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
989async 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
1009def run_proxy():
1010 asyncio.run(start_proxy_server())