GNServer 0.0.0.0.16__py3-none-any.whl → 0.0.0.0.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
GNServer/___client.py ADDED
@@ -0,0 +1,628 @@
1
+
2
+ import os
3
+ import httpx
4
+ import asyncio
5
+ import typing as _typing
6
+ import logging as logging2
7
+ from typing import Union, List, Dict, Tuple, Optional
8
+ import datetime
9
+ logging2.basicConfig(level=logging2.INFO)
10
+
11
+ from KeyisBLogging import logging
12
+ from typing import Dict, List, Tuple, Optional, cast, AsyncGenerator, Callable, Literal
13
+ from itertools import count
14
+ from aioquic.asyncio.client import connect
15
+ from aioquic.asyncio.protocol import QuicConnectionProtocol
16
+ from aioquic.h3.connection import H3_ALPN, H3Connection
17
+ from aioquic.h3.events import DataReceived, DatagramReceived, H3Event, HeadersReceived
18
+ from aioquic.quic.configuration import QuicConfiguration
19
+ from aioquic.quic.events import QuicEvent
20
+
21
+
22
+ import time
23
+ import json, ssl, asyncio, struct, base64, hashlib
24
+ from typing import Any, Dict, Optional
25
+ import websockets
26
+
27
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
28
+ import os
29
+ import msgpack
30
+ import logging
31
+ from httpx import Request, Headers, URL
32
+ logging.basicConfig(level=logging.DEBUG)
33
+ logging.getLogger("websockets").setLevel(logging.DEBUG)
34
+
35
+ import KeyisBClient
36
+ from KeyisBClient import Url
37
+ httpxAsyncClient = httpx.AsyncClient(verify=KeyisBClient.ssl_gw_crt_path, timeout=200)
38
+
39
+ class GNExceptions:
40
+ class ConnectionError:
41
+ class openconnector():
42
+ """Ошибка подключения к серверу openconnector.gn"""
43
+
44
+ class connection(Exception):
45
+ def __init__(self, message="Ошибка подключения к серверу openconnector.gn. Сервер не найден."):
46
+ super().__init__(message)
47
+
48
+ class timeout(Exception):
49
+ def __init__(self, message="Ошибка подключения к серверу openconnector.gn. Проблема с сетью или сервер перегружен."):
50
+ super().__init__(message)
51
+
52
+ class data(Exception):
53
+ def __init__(self, message="Ошибка подключения к серверу openconnector.gn. Сервер не подтвердил подключение."):
54
+ super().__init__(message)
55
+
56
+ class dns_core():
57
+ """Ошибка подключения к серверу dns.core"""
58
+ class connection(Exception):
59
+ def __init__(self, message="Ошибка подключения к серверу dns.core Сервер не найден."):
60
+ super().__init__(message)
61
+
62
+ class timeout(Exception):
63
+ def __init__(self, message="Ошибка подключения к серверу dns.core Проблема с сетью или сервер перегружен"):
64
+ super().__init__(message)
65
+
66
+ class data(Exception):
67
+ def __init__(self, message="Ошибка подключения к серверу dns.core Сервер не подтвердил подключение."):
68
+ super().__init__(message)
69
+
70
+
71
+
72
+ class connector():
73
+ """Ошибка подключения к серверу <?>~connector.gn"""
74
+
75
+ class connection(Exception):
76
+ def __init__(self, message="Ошибка подключения к серверу <?>~connector.gn. Сервер не найден."):
77
+ super().__init__(message)
78
+
79
+ class timeout(Exception):
80
+ def __init__(self, message="Ошибка подключения к серверу <?>~connector.gn. Проблема с сетью или сервер перегружен"):
81
+ super().__init__(message)
82
+
83
+ class data(Exception):
84
+ def __init__(self, message="Ошибка подключения к серверу <?>~connector.gn. Сервер не подтвердил подключение."):
85
+ super().__init__(message)
86
+
87
+
88
+
89
+ from KeyisBClient.gn import GNRequest, GNResponse, GNProtocol
90
+
91
+
92
+
93
+
94
+ class AsyncClient:
95
+ def __init__(self):
96
+ self.__dns_core__ipv4 = '51.250.85.38:52943'
97
+ self.__dns_gn__ipv4 = None
98
+
99
+ self.__user = {}
100
+ self.__current_session = {}
101
+ self.__request_callbacks = {}
102
+ self.__response_callbacks = {}
103
+
104
+ self._active_connections: Dict[str, QuicClient] = {}
105
+
106
+ async def _getCoreDNS(self, domain: str):
107
+
108
+ if domain.split('.')[-1].split(':')[0].isdigit() and domain.split(':')[-1].isdigit():
109
+ r2_data = {
110
+ "ip": domain.split(':')[0],
111
+ "port": int(domain.split(':')[-1])
112
+ }
113
+ return r2_data
114
+
115
+ try:
116
+ if self.__dns_gn__ipv4 is None:
117
+ r1 = await httpxAsyncClient.request('GET', f'https://{self.__dns_core__ipv4}/gn/getIp?d=dns.gn')
118
+ if r1.status_code != 200:
119
+ raise GNExceptions.ConnectionError.dns_core.data
120
+ r1_data = r1.json()
121
+ self.__dns_gn__ipv4 = r1_data['ip'] + ':' + str(r1_data['port'])
122
+
123
+
124
+ r2 = await httpxAsyncClient.request('GET', f'https://{self.__dns_gn__ipv4}/gn/getIp?d={domain}')
125
+ except httpx.TimeoutException:
126
+ raise GNExceptions.ConnectionError.dns_core.timeout
127
+ except:
128
+ raise GNExceptions.ConnectionError.dns_core.connection
129
+
130
+ if r2.status_code != 200:
131
+ raise GNExceptions.ConnectionError.dns_core.data
132
+
133
+ r2_data = r2.json()
134
+
135
+ return r2_data
136
+
137
+ def addRequestCallback(self, callback: Callable, name: str):
138
+ self.__request_callbacks[name] = callback
139
+
140
+ def addResponseCallback(self, callback: Callable, name: str):
141
+ self.__response_callbacks[name] = callback
142
+
143
+
144
+ async def connect(self, domain: str, restart_connection: bool = False, reconnect_wait: float = 10) -> 'QuicClient':
145
+ print('Запрос подключения')
146
+ if not restart_connection and domain in self._active_connections:
147
+ print('Подключение уже было')
148
+ c = self._active_connections[domain]
149
+ if c.status == 'connecting':
150
+ if (c.connection_time + datetime.timedelta(seconds=11)) < datetime.datetime.now():
151
+ print('ждем поделючения')
152
+ try:
153
+ await asyncio.wait_for(c.connect_future, reconnect_wait)
154
+ print('дождались')
155
+ return c
156
+ except:
157
+ print('Заново соеденяемся...')
158
+
159
+ else:
160
+ return c
161
+
162
+ c = QuicClient()
163
+ c.status = 'connecting'
164
+ self._active_connections[domain] = c
165
+
166
+ data = await self._getCoreDNS(domain)
167
+
168
+
169
+
170
+ def f(domain):
171
+ self._active_connections.pop(domain)
172
+
173
+ c._disconnect_signal = f
174
+ c._domain = domain
175
+
176
+ await c.connect(data['ip'], data['port'])
177
+ await c.connect_future
178
+
179
+ return c
180
+
181
+ async def disconnect(self, domain):
182
+ if domain not in self._active_connections:
183
+ return
184
+
185
+ await self._active_connections[domain].disconnect()
186
+
187
+
188
+ def _return_token(self, bigToken: str, s: bool = True) -> str:
189
+ return bigToken[:128] if s else bigToken[128:]
190
+
191
+
192
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]], restart_connection: bool = False, reconnect_wait: float = 10) -> GNResponse:
193
+ if isinstance(request, GNRequest):
194
+
195
+
196
+ c = await self.connect(request.url.hostname, restart_connection, reconnect_wait)
197
+
198
+
199
+
200
+ for f in self.__request_callbacks.values():
201
+ asyncio.create_task(f(request))
202
+
203
+ r = await c.asyncRequest(request)
204
+
205
+ for f in self.__response_callbacks.values():
206
+ asyncio.create_task(f(r))
207
+
208
+ return r
209
+
210
+ # else:
211
+ # async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
212
+ # async for req in request:
213
+ # if req.gn_protocol is None:
214
+ # req.setGNProtocol(self.__current_session['protocols'][0])
215
+ # req._stream = True
216
+
217
+ # for f in self.__request_callbacks.values():
218
+ # asyncio.create_task(f(req))
219
+
220
+ # yield req
221
+ # r = await self.client.asyncRequest(wrapped(request))
222
+
223
+ # for f in self.__response_callbacks.values():
224
+ # asyncio.create_task(f(r))
225
+
226
+ # return r
227
+
228
+ async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
229
+ """
230
+ Build and send a async request.
231
+ """
232
+ if isinstance(request, GNRequest):
233
+ if request.gn_protocol is None:
234
+ request.setGNProtocol(self.__current_session['protocols'][0])
235
+
236
+ for f in self.__request_callbacks.values():
237
+ asyncio.create_task(f(request))
238
+
239
+ async for response in self.client.asyncRequestStream(request):
240
+
241
+ for f in self.__response_callbacks.values():
242
+ asyncio.create_task(f(response))
243
+
244
+ yield response
245
+ else:
246
+ async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
247
+ async for req in request:
248
+ if req.gn_protocol is None:
249
+ req.setGNProtocol(self.__current_session['protocols'][0])
250
+
251
+ for f in self.__request_callbacks.values():
252
+ asyncio.create_task(f(req))
253
+
254
+ req._stream = True
255
+ yield req
256
+ async for response in self.client.asyncRequestStream(wrapped(request)):
257
+
258
+ for f in self.__response_callbacks.values():
259
+ asyncio.create_task(f(response))
260
+
261
+ yield response
262
+
263
+ from aioquic.asyncio.protocol import QuicConnectionProtocol
264
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
265
+ from aioquic.quic.connection import END_STATES
266
+ import asyncio
267
+ from collections import deque
268
+ from typing import Dict, Deque, Tuple, Optional, List
269
+
270
+ import asyncio
271
+ import time
272
+ from collections import deque
273
+ from dataclasses import dataclass
274
+ from itertools import count
275
+ from typing import Deque, Dict, Optional, Tuple, Union
276
+
277
+ from aioquic.quic.configuration import QuicConfiguration
278
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset, ConnectionTerminated
279
+
280
+
281
+ class RawQuicClient(QuicConnectionProtocol):
282
+
283
+ SYS_RATIO_NUM = 9 # SYS 9/10
284
+ SYS_RATIO_DEN = 10
285
+ KEEPALIVE_INTERVAL = 10 # сек
286
+ KEEPALIVE_IDLE_TRIGGER = 30 # сек
287
+
288
+ # ────────────────────────────────────────────────────────────────── init ─┐
289
+ def __init__(self, *args, **kwargs):
290
+ super().__init__(*args, **kwargs)
291
+
292
+ self.quicClient: QuicClient = None
293
+
294
+ self._sys_stream_id: Optional[int] = None
295
+ self._queue_sys: Deque[Tuple[int, bytes, bool]] = deque()
296
+ self._queue_user: Deque[Tuple[int, bytes, bool]] = deque()
297
+
298
+ # <‑‑ Future | Queue[bytes | None]
299
+ self._inflight: Dict[int, Union[asyncio.Future, asyncio.Queue[Optional[GNResponse]]]] = {}
300
+ self._inflight_streams: Dict[int, bytearray] = {}
301
+ self._sys_inflight: Dict[int, asyncio.Future] = {}
302
+ self._buffer: Dict[Union[int, str], bytearray] = {}
303
+
304
+ self._sys_budget = self.SYS_RATIO_NUM
305
+ self._sys_id_gen = count(1) # int64 message‑id generator
306
+
307
+ self._last_activity = time.time()
308
+ self._running = True
309
+ self._ping_id_gen = count(1) # int64 ping‑id generator
310
+ asyncio.create_task(self._keepalive_loop())
311
+
312
+ # ───────────────────────────────────────── private helpers ─┤
313
+ def _activity(self):
314
+ self._last_activity = time.time()
315
+
316
+ async def _keepalive_loop(self):
317
+ while self._running:
318
+ await asyncio.sleep(self.KEEPALIVE_INTERVAL)
319
+ idle_time = time.time() - self._last_activity
320
+ if idle_time > self.KEEPALIVE_IDLE_TRIGGER:
321
+ self._quic.send_ping(next(self._ping_id_gen))
322
+ self.transmit()
323
+ self._last_activity = time.time()
324
+
325
+ def stop(self):
326
+ self._running = False
327
+
328
+ # ───────────────────────────────────────────── events ─┤
329
+ def quic_event_received(self, event: QuicEvent) -> None: # noqa: C901
330
+ # ─── DATA ───────────────────────────────────────────
331
+ if isinstance(event, StreamDataReceived):
332
+ #print(event)
333
+ # SYS поток
334
+ if event.stream_id == self._sys_stream_id:
335
+ buf = self._buffer.setdefault("sys", bytearray())
336
+ buf.extend(event.data)
337
+ while True:
338
+ if len(buf) < 12:
339
+ break
340
+ msg_id = int.from_bytes(buf[:8], "little")
341
+ size = int.from_bytes(buf[8:12], "little")
342
+ if len(buf) < 12 + size:
343
+ break
344
+ payload = bytes(buf[12 : 12 + size])
345
+ del buf[: 12 + size]
346
+ fut = self._sys_inflight.pop(msg_id, None) if msg_id else None
347
+ if fut and not fut.done():
348
+ fut.set_result(payload)
349
+ # USER поток
350
+ else:
351
+ handler = self._inflight.get(event.stream_id)
352
+ if handler is None:
353
+ return
354
+
355
+ # Чтение в зависимости от режима
356
+ if isinstance(handler, asyncio.Queue): # стрим от сервера
357
+ # получаем байты
358
+
359
+ buf = self._buffer.setdefault(event.stream_id, bytearray())
360
+ buf.extend(event.data)
361
+
362
+ if len(buf) < 8: # не дошел даже frame пакета
363
+ return
364
+
365
+ # получаем длинну пакета
366
+ mode, stream, lenght = GNResponse.type(buf)
367
+
368
+ if mode != 4: # не наш пакет
369
+ self._buffer.pop(event.stream_id)
370
+ return
371
+
372
+ if not stream: # клиент просил стрим, а сервер прислал один пакет
373
+ self._buffer.pop(event.stream_id)
374
+ return
375
+
376
+ # читаем пакет
377
+ if len(buf) < lenght: # если пакет не весь пришел, пропускаем
378
+ return
379
+
380
+ # пакет пришел весь
381
+
382
+ # берем пакет
383
+ data = buf[:lenght]
384
+
385
+ # удаляем его из буфера
386
+ del buf[:lenght]
387
+
388
+
389
+ r = GNResponse.deserialize(data, 2)
390
+ handler.put_nowait(r)
391
+ if event.end_stream:
392
+ handler.put_nowait(None)
393
+ self._buffer.pop(event.stream_id)
394
+ self._inflight.pop(event.stream_id, None)
395
+
396
+
397
+
398
+ else: # Future
399
+ buf = self._buffer.setdefault(event.stream_id, bytearray())
400
+ buf.extend(event.data)
401
+ if event.end_stream:
402
+ self._inflight.pop(event.stream_id, None)
403
+ data = bytes(self._buffer.pop(event.stream_id, b""))
404
+ if not handler.done():
405
+ handler.set_result(data)
406
+
407
+ # ─── RESET ──────────────────────────────────────────
408
+ elif isinstance(event, StreamReset):
409
+ handler = self._inflight.pop(event.stream_id, None) or self._sys_inflight.pop(
410
+ event.stream_id, None
411
+ )
412
+ if handler is None:
413
+ return
414
+ if isinstance(handler, asyncio.Queue):
415
+ handler.put_nowait(None)
416
+ else:
417
+ if not handler.done():
418
+ handler.set_exception(RuntimeError("stream reset"))
419
+
420
+
421
+ elif isinstance(event, ConnectionTerminated):
422
+ print("QUIC connection closed")
423
+ print("Error code:", event.error_code)
424
+ print("Reason:", event.reason_phrase)
425
+ if self.quicClient is None:
426
+ return
427
+
428
+ asyncio.create_task(self.quicClient.disconnect())
429
+
430
+
431
+ # ─────────────────────────────────────────── scheduler ─┤
432
+ def _enqueue(self, sid: int, blob: bytes, end_stream: bool, is_sys: bool):
433
+ (self._queue_sys if is_sys else self._queue_user).append((sid, blob, end_stream))
434
+
435
+ def _schedule_flush(self):
436
+ while (self._queue_sys or self._queue_user) and self._quic._close_event is None:
437
+ q = None
438
+ if self._queue_sys and (self._sys_budget > 0 or not self._queue_user):
439
+ q = self._queue_sys
440
+ self._sys_budget -= 1
441
+ elif self._queue_user:
442
+ q = self._queue_user
443
+ self._sys_budget = self.SYS_RATIO_NUM
444
+ if q is None:
445
+ break
446
+ sid, blob, end_stream = q.popleft()
447
+ print(f'Отправка стрима {sid}')
448
+ self._quic.send_stream_data(sid, blob, end_stream=end_stream)
449
+ self.transmit()
450
+ self._activity()
451
+
452
+ # ─────────────────────────────────────────── public API ─┤
453
+ async def ensure_sys_stream(self):
454
+ if self._sys_stream_id is None:
455
+ self._sys_stream_id = self._quic.get_next_available_stream_id()
456
+ self._enqueue(self._sys_stream_id, b"", False, True) # dummy
457
+ self._schedule_flush()
458
+
459
+ async def send_sys(self, request: GNRequest, response: bool = False) -> Optional[bytes]:
460
+ await self.ensure_sys_stream()
461
+ if response:
462
+ msg_id = next(self._sys_id_gen)
463
+ blob = request.serialize(2)
464
+ payload = (
465
+ msg_id.to_bytes(8, "little") + len(blob).to_bytes(4, "little") + blob
466
+ )
467
+ fut = asyncio.get_running_loop().create_future()
468
+ self._sys_inflight[msg_id] = fut
469
+ self._enqueue(self._sys_stream_id, payload, False, True)
470
+ self._schedule_flush()
471
+ return await fut
472
+ payload = (0).to_bytes(8, "little") + request.serialize(2)
473
+ self._enqueue(self._sys_stream_id, payload, False, True)
474
+ self._schedule_flush()
475
+ return None
476
+
477
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]):
478
+ if isinstance(request, GNRequest):
479
+ blob = request.serialize(2)
480
+ sid = self._quic.get_next_available_stream_id()
481
+ self._enqueue(sid, blob, True, False)
482
+ self._schedule_flush()
483
+
484
+
485
+ fut = asyncio.get_running_loop().create_future()
486
+ self._inflight[sid] = fut
487
+ return await fut
488
+
489
+ else:
490
+ sid = self._quic.get_next_available_stream_id()
491
+ #if sid in self._quic._streams and not self._quic._streams[sid].is_finished:
492
+
493
+ async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
494
+ _last = None
495
+ async for req in request:
496
+ _last = req
497
+ blob = req.serialize(2)
498
+ self._enqueue(sid, blob, False, False)
499
+
500
+
501
+ self._schedule_flush()
502
+
503
+ print(f'Отправлен stream запрос {req}')
504
+
505
+
506
+ _last.setPayload(None)
507
+ _last.setMethod('gn:end-stream')
508
+ blob = _last.serialize(2)
509
+ self._enqueue(sid, blob, True, False)
510
+ self._schedule_flush()
511
+
512
+ asyncio.create_task(_stream_sender(sid, request))
513
+
514
+
515
+ fut = asyncio.get_running_loop().create_future()
516
+ self._inflight[sid] = fut
517
+ return await fut
518
+
519
+ async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> asyncio.Queue[GNResponse]:
520
+ if isinstance(request, GNRequest):
521
+ blob = request.serialize(2)
522
+ sid = self._quic.get_next_available_stream_id()
523
+ self._enqueue(sid, blob, False, False)
524
+ self._schedule_flush()
525
+
526
+
527
+ q = asyncio.Queue()
528
+ self._inflight[sid] = q
529
+ return q
530
+
531
+ else:
532
+ sid = self._quic.get_next_available_stream_id()
533
+
534
+ async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
535
+ _last = None
536
+ async for req in request:
537
+ _last = req
538
+ blob = req.serialize(2)
539
+ self._enqueue(sid, blob, False, False)
540
+
541
+
542
+ self._schedule_flush()
543
+
544
+ print(f'Отправлен stream запрос {req}')
545
+
546
+
547
+ _last.setPayload(None)
548
+ _last.setMethod('gn:end-stream')
549
+ blob = _last.serialize(2)
550
+ self._enqueue(sid, blob, True, False)
551
+ self._schedule_flush()
552
+
553
+ asyncio.create_task(_stream_sender(sid, request))
554
+
555
+
556
+ q = asyncio.Queue()
557
+ self._inflight[sid] = q
558
+ return q
559
+
560
+
561
+
562
+ class QuicClient:
563
+ """Обёртка‑фасад над RawQuicClient."""
564
+
565
+ def __init__(self):
566
+ self._quik_core: Optional[RawQuicClient] = None
567
+ self._client_cm = None
568
+ self._disconnect_signal = None
569
+ self._domain = None
570
+
571
+ self.status: Literal['active', 'connecting', 'disconnect']
572
+
573
+ self.connect_future = asyncio.get_event_loop().create_future()
574
+ self.connection_time: datetime.datetime = None
575
+
576
+ async def connect(self, ip: str, port: int):
577
+ self.status = 'connecting'
578
+ self.connection_time = datetime.datetime.now()
579
+ cfg = QuicConfiguration(is_client=True, alpn_protocols=["gn:backend"])
580
+ cfg.load_verify_locations(KeyisBClient.ssl_gw_crt_path)
581
+ cfg.idle_timeout = 10
582
+
583
+ self._client_cm = connect(
584
+ ip,
585
+ port,
586
+ configuration=cfg,
587
+ create_protocol=RawQuicClient,
588
+ wait_connected=True,
589
+ )
590
+ self._quik_core = await self._client_cm.__aenter__()
591
+ self._quik_core.quicClient = self
592
+
593
+ self.status = 'active'
594
+ self.connect_future.set_result(True)
595
+
596
+ async def disconnect(self):
597
+ self.status = 'disconnect'
598
+ if self._disconnect_signal is not None:
599
+ self._disconnect_signal(self._domain)
600
+
601
+ self._quik_core.close()
602
+ await self._quik_core.wait_closed()
603
+ self._quik_core = None
604
+
605
+ def syncRequest(self, request: GNRequest):
606
+ return asyncio.get_event_loop().run_until_complete(self.asyncRequest(request))
607
+
608
+ async def asyncRequest(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
609
+ if self._quik_core is None:
610
+ raise RuntimeError("Not connected")
611
+
612
+ resp = await self._quik_core.request(request)
613
+ return GNResponse.deserialize(resp, 2)
614
+
615
+ async def asyncRequestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
616
+
617
+ if self._quik_core is None:
618
+ raise RuntimeError("Not connected")
619
+
620
+ queue = await self._quik_core.requestStream(request)
621
+
622
+ while True:
623
+ chunk = await queue.get()
624
+ if chunk is None or chunk.command == 'gn:end-stream':
625
+ break
626
+ yield chunk
627
+
628
+
GNServer/_app.py CHANGED
@@ -290,6 +290,7 @@ def _host_matches_pattern(host: str, pattern: str) -> bool:
290
290
 
291
291
 
292
292
 
293
+
293
294
  @dataclass
294
295
  class Route:
295
296
  method: str
@@ -367,6 +368,15 @@ def _convert_value(raw: str | list[str], ann: Any, fallback: Callable[[str], Any
367
368
  raw = [raw]
368
369
  return [_convert_value(r, subtype, fallback) for r in raw]
369
370
 
371
+ # --- fix Union ---
372
+ if origin is Union:
373
+ for subtype in args:
374
+ try:
375
+ return _convert_value(raw, subtype, fallback)
376
+ except Exception:
377
+ continue
378
+ return raw # если ни один тип не подошёл
379
+
370
380
  conv = _CONVERTER_FUNC.get(ann, ann) if ann is not inspect._empty else fallback
371
381
  return conv(raw) if callable(conv) else raw
372
382
 
@@ -436,7 +446,7 @@ class App:
436
446
  if r.method != method:
437
447
  continue
438
448
 
439
- if r.cors is not None:
449
+ if r.cors is not None and r.cors._allow_origins is not None:
440
450
  if request._origin is None:
441
451
  return gn.GNResponse("gn:backend:801", {'error': 'Cors error. Route has cors but request has no origin url.'})
442
452
  if not resolve_cors(request._origin, r.cors._allow_origins):
@@ -468,11 +478,16 @@ class App:
468
478
 
469
479
  result = await r.handler(**kw)
470
480
  if isinstance(result, gn.GNResponse):
471
- if result._cors is None:
472
- result._cors = self._cors
473
- if result._cors is not None and result._cors != r.cors:
481
+ if r.cors is None:
482
+ if result._cors is None:
483
+ result._cors = self._cors
484
+ else:
485
+ result._cors = r.cors
486
+
487
+ if result._cors is not None and result._cors != r.cors and result._cors._allow_origins is not None:
474
488
  if request._origin is None:
475
- return gn.GNResponse("gn:backend:801", {'error': 'Cors error. Route has cors but request has no origin url.'})
489
+ print(result._cors._allow_origins)
490
+ return gn.GNResponse("gn:backend:801", {'error': 'Cors error. Route has cors but request has no origin url. [2]'})
476
491
  if not resolve_cors(request._origin, result._cors._allow_origins):
477
492
  return gn.GNResponse("gn:backend:802", {'error': 'Cors error: origin'})
478
493
  if request.method not in result._cors._allow_methods and '*' not in result._cors._allow_methods:
@@ -520,10 +535,12 @@ class App:
520
535
 
521
536
 
522
537
  def _init_sys_routes(self):
523
- @self.get('/!gn-vm-host/ping')
538
+ @self.post('/!gn-vm-host/ping', cors=gn.CORSObject(None))
524
539
  async def r_ping(request: gn.GNRequest):
525
- if request
526
- return gn.GNResponse('ok', {'status': 'pong'})
540
+
541
+ if request._client_ip != '127.0.0.1':
542
+ return gn.GNResponse('gn:backend:403', {'error': 'Forbidden'})
543
+ return gn.GNResponse('ok', {'time': datetime.datetime.now(datetime.UTC).isoformat()})
527
544
 
528
545
 
529
546
 
@@ -621,7 +638,7 @@ class App:
621
638
  asyncio.create_task(self._handle_request(request, mode))
622
639
 
623
640
  async def _handle_request(self, request: gn.GNRequest, mode: int):
624
-
641
+
625
642
  request._client_ip = self._quic._network_paths[0].addr[0]
626
643
 
627
644
  try:
@@ -672,7 +689,8 @@ class App:
672
689
  key_path: str,
673
690
  *,
674
691
  idle_timeout: float = 20.0,
675
- wait: bool = True
692
+ wait: bool = True,
693
+ run: Optional[Callable] = None
676
694
  ):
677
695
 
678
696
  self._init_sys_routes()
@@ -690,6 +708,11 @@ class App:
690
708
  create_protocol=lambda *a, **kw: App._ServerProto(*a, api=self, **kw),
691
709
  retry=False,
692
710
  )
711
+
712
+ if run is not None:
713
+ await run()
714
+
715
+
693
716
  if wait:
694
717
  await asyncio.Event().wait()
695
718
 
GNServer/_client.py CHANGED
@@ -9,7 +9,7 @@ import datetime
9
9
  logging2.basicConfig(level=logging2.INFO)
10
10
 
11
11
  from KeyisBLogging import logging
12
- from typing import Dict, List, Tuple, Optional, cast, AsyncGenerator, Callable
12
+ from typing import Dict, List, Tuple, Optional, cast, AsyncGenerator, Callable, Literal
13
13
  from itertools import count
14
14
  from aioquic.asyncio.client import connect
15
15
  from aioquic.asyncio.protocol import QuicConnectionProtocol
@@ -66,8 +66,6 @@ class GNExceptions:
66
66
  class data(Exception):
67
67
  def __init__(self, message="Ошибка подключения к серверу dns.core Сервер не подтвердил подключение."):
68
68
  super().__init__(message)
69
-
70
-
71
69
 
72
70
  class connector():
73
71
  """Ошибка подключения к серверу <?>~connector.gn"""
@@ -84,6 +82,22 @@ class GNExceptions:
84
82
  def __init__(self, message="Ошибка подключения к серверу <?>~connector.gn. Сервер не подтвердил подключение."):
85
83
  super().__init__(message)
86
84
 
85
+ class client():
86
+ """Ошибка клиента"""
87
+
88
+ class connection(Exception):
89
+ def __init__(self, message="Ошибка подключения к серверу. Сервер не найден."):
90
+ super().__init__(message)
91
+
92
+ class timeout(Exception):
93
+ def __init__(self, message="Ошибка подключения к серверу. Проблема с сетью или сервер перегружен"):
94
+ super().__init__(message)
95
+
96
+ class data(Exception):
97
+ def __init__(self, message="Ошибка подключения к серверу. Сервер не подтвердил подключение."):
98
+ super().__init__(message)
99
+
100
+
87
101
 
88
102
 
89
103
  from KeyisBClient.gn import GNRequest, GNResponse, GNProtocol
@@ -101,13 +115,11 @@ class AsyncClient:
101
115
  self.__request_callbacks = {}
102
116
  self.__response_callbacks = {}
103
117
 
104
- self._client: QuicClient = QuicClient()
105
-
106
- self._active_connections: Dict[str, Any] = {}
118
+ self._active_connections: Dict[str, QuicClient] = {}
107
119
 
108
120
  async def _getCoreDNS(self, domain: str):
109
121
 
110
- if domain.split('.')[-1].isdigit() and domain.split(':')[-1].isdigit():
122
+ if domain.split('.')[-1].split(':')[0].isdigit() and domain.split(':')[-1].isdigit():
111
123
  r2_data = {
112
124
  "ip": domain.split(':')[0],
113
125
  "port": int(domain.split(':')[-1])
@@ -143,53 +155,72 @@ class AsyncClient:
143
155
  self.__response_callbacks[name] = callback
144
156
 
145
157
 
146
- async def connect(self, domain: str):
147
- if domain in self._active_connections:
148
- return
158
+ async def connect(self, domain: str, restart_connection: bool = False, reconnect_wait: float = 10) -> 'QuicClient':
159
+ print('Запрос подключения')
160
+ if not restart_connection and domain in self._active_connections:
161
+ c = self._active_connections[domain]
162
+ print(f'Подключение уже было [{c.status}]')
163
+ if c.status == 'connecting':
164
+ print('ждем поделючения')
165
+ try:
166
+ await asyncio.wait_for(c.connect_future, reconnect_wait)
167
+ print('дождались')
168
+ if c.status == 'active':
169
+ return c
170
+ elif c.status == 'connecting': # если очень дого подключаемся, то кидаем ошибку
171
+ await self.disconnect(domain)
172
+ raise GNExceptions.ConnectionError.client.timeout
173
+ elif c.status == 'disconnect':
174
+ raise GNExceptions.ConnectionError.client.connection
175
+ except:
176
+ print('Заново соеденяемся...')
177
+ await self.disconnect(domain)
178
+
179
+ else:
180
+ return c
181
+
182
+ c = QuicClient()
183
+ c.status = 'connecting'
184
+ self._active_connections[domain] = c
149
185
 
150
186
  data = await self._getCoreDNS(domain)
151
- # подключаемся к серверу gn-proxy
152
- await self._client.connect(data['ip'], data['port'])
153
- self._active_connections[domain] = 'active'
154
187
 
155
- async def disconnect(self):
156
- await self._client.disconnect()
157
188
 
158
189
 
159
- def _return_token(self, bigToken: str, s: bool = True) -> str:
160
- return bigToken[:128] if s else bigToken[128:]
190
+ def f(domain):
191
+ self._active_connections.pop(domain)
161
192
 
193
+ c._disconnect_signal = f
194
+ c._domain = domain
162
195
 
163
- async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
164
- """
165
- Build and send a async request.
166
-
167
- ```python
168
- gnAsyncClient = KeyisBClient.AsyncClient()
169
- async def func():
170
- response = await gnAsyncClient.request(GNRequest('GET', Url('gn://example.com/example')))
171
- command = response.command()
172
- data = response.payload()
173
- ```
174
- """
196
+ await c.connect(data['ip'], data['port'])
197
+ await c.connect_future
175
198
 
199
+ return c
176
200
 
201
+ async def disconnect(self, domain):
202
+ if domain not in self._active_connections:
203
+ return
204
+
205
+ await self._active_connections[domain].disconnect()
177
206
 
178
207
 
208
+ def _return_token(self, bigToken: str, s: bool = True) -> str:
209
+ return bigToken[:128] if s else bigToken[128:]
179
210
 
180
211
 
212
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]], restart_connection: bool = False, reconnect_wait: float = 10) -> GNResponse:
181
213
  if isinstance(request, GNRequest):
182
214
 
183
215
 
184
- if request.url.hostname not in self._active_connections:
185
- await self.connect(request.url.hostname)
216
+ c = await self.connect(request.url.hostname, restart_connection, reconnect_wait)
186
217
 
187
218
 
188
219
 
189
220
  for f in self.__request_callbacks.values():
190
221
  asyncio.create_task(f(request))
191
222
 
192
- r = await self._client.asyncRequest(request)
223
+ r = await c.asyncRequest(request)
193
224
 
194
225
  for f in self.__response_callbacks.values():
195
226
  asyncio.create_task(f(r))
@@ -249,16 +280,6 @@ class AsyncClient:
249
280
 
250
281
  yield response
251
282
 
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
- # gn:quik
261
-
262
283
  from aioquic.asyncio.protocol import QuicConnectionProtocol
263
284
  from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
264
285
  from aioquic.quic.connection import END_STATES
@@ -266,18 +287,6 @@ import asyncio
266
287
  from collections import deque
267
288
  from typing import Dict, Deque, Tuple, Optional, List
268
289
 
269
- # ---------------------------------------------------------------------------
270
- # Raw QUIC client with a dedicated SYS‑stream that consumes ~90 % CWND
271
- # ---------------------------------------------------------------------------
272
- # Основная идея:
273
- # • Один постоянный bidirectional stream (sys_stream_id) используется для
274
- # служебных сообщений.
275
- # • Остальные запросы открываются в обычных потоках (user streams).
276
- # • Отправка данных идёт через собственный scheduler: берём 9 «квантов» из
277
- # SYS‑очереди и 1 квант из USER‑очереди, пока есть SYS‑данные.
278
- # • Таким образом SYS‑канал получает ~90 % пропускной способности.
279
- # ---------------------------------------------------------------------------
280
-
281
290
  import asyncio
282
291
  import time
283
292
  from collections import deque
@@ -286,11 +295,10 @@ from itertools import count
286
295
  from typing import Deque, Dict, Optional, Tuple, Union
287
296
 
288
297
  from aioquic.quic.configuration import QuicConfiguration
289
- from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
298
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset, ConnectionTerminated
290
299
 
291
300
 
292
301
  class RawQuicClient(QuicConnectionProtocol):
293
- """Чистый‑QUIC клиент с приоритизированным SYS‑каналом + стриминг."""
294
302
 
295
303
  SYS_RATIO_NUM = 9 # SYS 9/10
296
304
  SYS_RATIO_DEN = 10
@@ -301,6 +309,8 @@ class RawQuicClient(QuicConnectionProtocol):
301
309
  def __init__(self, *args, **kwargs):
302
310
  super().__init__(*args, **kwargs)
303
311
 
312
+ self.quicClient: QuicClient = None
313
+
304
314
  self._sys_stream_id: Optional[int] = None
305
315
  self._queue_sys: Deque[Tuple[int, bytes, bool]] = deque()
306
316
  self._queue_user: Deque[Tuple[int, bytes, bool]] = deque()
@@ -427,6 +437,17 @@ class RawQuicClient(QuicConnectionProtocol):
427
437
  if not handler.done():
428
438
  handler.set_exception(RuntimeError("stream reset"))
429
439
 
440
+
441
+ elif isinstance(event, ConnectionTerminated):
442
+ print("QUIC connection closed")
443
+ print("Error code:", event.error_code)
444
+ print("Reason:", event.reason_phrase)
445
+ if self.quicClient is None:
446
+ return
447
+
448
+ asyncio.create_task(self.quicClient.disconnect())
449
+
450
+
430
451
  # ─────────────────────────────────────────── scheduler ─┤
431
452
  def _enqueue(self, sid: int, blob: bytes, end_stream: bool, is_sys: bool):
432
453
  (self._queue_sys if is_sys else self._queue_user).append((sid, blob, end_stream))
@@ -443,6 +464,7 @@ class RawQuicClient(QuicConnectionProtocol):
443
464
  if q is None:
444
465
  break
445
466
  sid, blob, end_stream = q.popleft()
467
+ print(f'Отправка стрима {sid}')
446
468
  self._quic.send_stream_data(sid, blob, end_stream=end_stream)
447
469
  self.transmit()
448
470
  self._activity()
@@ -563,10 +585,20 @@ class QuicClient:
563
585
  def __init__(self):
564
586
  self._quik_core: Optional[RawQuicClient] = None
565
587
  self._client_cm = None
588
+ self._disconnect_signal = None
589
+ self._domain = None
590
+
591
+ self.status: Literal['active', 'connecting', 'disconnect']
592
+
593
+ self.connect_future = asyncio.get_event_loop().create_future()
594
+ self.connection_time: datetime.datetime = None
566
595
 
567
596
  async def connect(self, ip: str, port: int):
597
+ self.status = 'connecting'
598
+ self.connection_time = datetime.datetime.now()
568
599
  cfg = QuicConfiguration(is_client=True, alpn_protocols=["gn:backend"])
569
600
  cfg.load_verify_locations(KeyisBClient.ssl_gw_crt_path)
601
+ cfg.idle_timeout = 40
570
602
 
571
603
  self._client_cm = connect(
572
604
  ip,
@@ -575,15 +607,48 @@ class QuicClient:
575
607
  create_protocol=RawQuicClient,
576
608
  wait_connected=True,
577
609
  )
578
- self._quik_core = await self._client_cm.__aenter__()
610
+
611
+
612
+ try:
613
+ self._quik_core = await self._client_cm.__aenter__()
614
+ self._quik_core.quicClient = self
615
+
616
+ self.status = 'active'
617
+ if not self.connect_future.done():
618
+ self.connect_future.set_result(True)
619
+ except Exception as e:
620
+ print(f'Error connecting: {e}')
621
+ if not self.connect_future.done():
622
+ self.connect_future.set_exception(GNExceptions.ConnectionError.client.connection)
623
+ await self._client_cm.__aexit__()
579
624
 
580
625
  async def disconnect(self):
581
- self._quik_core.close()
582
- await self._quik_core.wait_closed()
583
- self._quik_core = None
626
+ self.status = 'disconnect'
627
+
628
+ if self._quik_core is not None:
629
+ self._quik_core.stop()
630
+
631
+
632
+ if self._disconnect_signal is not None:
633
+ self._disconnect_signal(self._domain)
634
+
635
+
636
+ if self._quik_core is not None:
637
+
638
+
639
+ for fut in self._quik_core._inflight.values():
640
+ if isinstance(fut, asyncio.Queue):
641
+ del fut
642
+ else:
643
+ fut.set_exception(Exception)
644
+
645
+ for fut in self._quik_core._sys_inflight.values():
646
+ fut.set_exception(Exception)
647
+
584
648
 
585
- def syncRequest(self, request: GNRequest):
586
- return asyncio.get_event_loop().run_until_complete(self.asyncRequest(request))
649
+ self._quik_core.close()
650
+ await self._quik_core.wait_closed()
651
+ self._quik_core = None
587
652
 
588
653
  async def asyncRequest(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
589
654
  if self._quik_core is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GNServer
3
- Version: 0.0.0.0.16
3
+ Version: 0.0.0.0.18
4
4
  Summary: GNServer
5
5
  Home-page: https://github.com/KeyisB/libs/tree/main/GNServer
6
6
  Author: KeyisB
@@ -0,0 +1,9 @@
1
+ GNServer/___client.py,sha256=hmeUL2Vqp-BnwJeRcLZAaIfRNVxBrRRB_AFk9ofkei4,25459
2
+ GNServer/__init__.py,sha256=J4bjDqXVSEgOhD-FmkhpCeU4NJkp2BKMUKgltiuSXVo,1437
3
+ GNServer/_app.py,sha256=D3rZl2_hHTqFdeyUnaRd7uoAGzZBaG5JLxBzpzjApU0,25418
4
+ GNServer/_client.py,sha256=uM88y3yHBBH6qjG5L6bqFMCOVveTeyjyV-vlw1JOmG8,27397
5
+ gnserver-0.0.0.0.18.dist-info/licenses/LICENSE,sha256=WH_t7dKZyWJ5Ld07eYIkUG4Tv6zZWXtAdsUqYAUesn0,1084
6
+ gnserver-0.0.0.0.18.dist-info/METADATA,sha256=ZSCemdmfrT9yP4Xxd4V96oQ72l8OajQztbxTt7RaqsQ,805
7
+ gnserver-0.0.0.0.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ gnserver-0.0.0.0.18.dist-info/top_level.txt,sha256=-UOUBuD4u7Qkb1o5PdcwyA3kx8xCH2lwy0tJHi26Wb4,9
9
+ gnserver-0.0.0.0.18.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- GNServer/__init__.py,sha256=J4bjDqXVSEgOhD-FmkhpCeU4NJkp2BKMUKgltiuSXVo,1437
2
- GNServer/_app.py,sha256=ZXJTTFCMhKWWVrauhvKjrwztvUhg6k_93ujU_m8CQRE,24592
3
- GNServer/_client.py,sha256=tSpFkUuwHRPqmIjdTdcYWzPwDUTDaDRNhQOBTlL4jpg,24725
4
- gnserver-0.0.0.0.16.dist-info/licenses/LICENSE,sha256=WH_t7dKZyWJ5Ld07eYIkUG4Tv6zZWXtAdsUqYAUesn0,1084
5
- gnserver-0.0.0.0.16.dist-info/METADATA,sha256=2_RVRAUtZAacts8XPJWZ8pniCSHMBJznmcZijtNaDwI,805
6
- gnserver-0.0.0.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- gnserver-0.0.0.0.16.dist-info/top_level.txt,sha256=-UOUBuD4u7Qkb1o5PdcwyA3kx8xCH2lwy0tJHi26Wb4,9
8
- gnserver-0.0.0.0.16.dist-info/RECORD,,