Last active 1791962501

replace .so file in app dir

en2zmax's Avatar en2zmax revised this gist 1791962501. Go to revision

No changes

en2zmax's Avatar en2zmax revised this gist 1791962465. Go to revision

No changes

en2zmax's Avatar en2zmax revised this gist 1791962456. 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