GNServer 0.0.0.0.17__tar.gz → 0.0.0.0.18__tar.gz

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.
@@ -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
@@ -101,13 +101,11 @@ class AsyncClient:
101
101
  self.__request_callbacks = {}
102
102
  self.__response_callbacks = {}
103
103
 
104
- self._client: QuicClient = QuicClient()
105
-
106
- self._active_connections: Dict[str, Any] = {}
104
+ self._active_connections: Dict[str, QuicClient] = {}
107
105
 
108
106
  async def _getCoreDNS(self, domain: str):
109
107
 
110
- if domain.split('.')[-1].isdigit() and domain.split(':')[-1].isdigit():
108
+ if domain.split('.')[-1].split(':')[0].isdigit() and domain.split(':')[-1].isdigit():
111
109
  r2_data = {
112
110
  "ip": domain.split(':')[0],
113
111
  "port": int(domain.split(':')[-1])
@@ -143,53 +141,66 @@ class AsyncClient:
143
141
  self.__response_callbacks[name] = callback
144
142
 
145
143
 
146
- async def connect(self, domain: str):
147
- if domain in self._active_connections:
148
- return
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
149
165
 
150
166
  data = await self._getCoreDNS(domain)
151
- # подключаемся к серверу gn-proxy
152
- await self._client.connect(data['ip'], data['port'])
153
- self._active_connections[domain] = 'active'
154
167
 
155
- async def disconnect(self):
156
- await self._client.disconnect()
157
168
 
158
169
 
159
- def _return_token(self, bigToken: str, s: bool = True) -> str:
160
- return bigToken[:128] if s else bigToken[128:]
170
+ def f(domain):
171
+ self._active_connections.pop(domain)
161
172
 
173
+ c._disconnect_signal = f
174
+ c._domain = domain
162
175
 
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
- """
176
+ await c.connect(data['ip'], data['port'])
177
+ await c.connect_future
175
178
 
179
+ return c
176
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()
177
186
 
178
187
 
188
+ def _return_token(self, bigToken: str, s: bool = True) -> str:
189
+ return bigToken[:128] if s else bigToken[128:]
179
190
 
180
191
 
192
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]], restart_connection: bool = False, reconnect_wait: float = 10) -> GNResponse:
181
193
  if isinstance(request, GNRequest):
182
194
 
183
195
 
184
- if request.url.hostname not in self._active_connections:
185
- await self.connect(request.url.hostname)
196
+ c = await self.connect(request.url.hostname, restart_connection, reconnect_wait)
186
197
 
187
198
 
188
199
 
189
200
  for f in self.__request_callbacks.values():
190
201
  asyncio.create_task(f(request))
191
202
 
192
- r = await self._client.asyncRequest(request)
203
+ r = await c.asyncRequest(request)
193
204
 
194
205
  for f in self.__response_callbacks.values():
195
206
  asyncio.create_task(f(r))
@@ -249,16 +260,6 @@ class AsyncClient:
249
260
 
250
261
  yield response
251
262
 
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
- # gn:quik
261
-
262
263
  from aioquic.asyncio.protocol import QuicConnectionProtocol
263
264
  from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
264
265
  from aioquic.quic.connection import END_STATES
@@ -266,18 +267,6 @@ import asyncio
266
267
  from collections import deque
267
268
  from typing import Dict, Deque, Tuple, Optional, List
268
269
 
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
270
  import asyncio
282
271
  import time
283
272
  from collections import deque
@@ -286,11 +275,10 @@ from itertools import count
286
275
  from typing import Deque, Dict, Optional, Tuple, Union
287
276
 
288
277
  from aioquic.quic.configuration import QuicConfiguration
289
- from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
278
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset, ConnectionTerminated
290
279
 
291
280
 
292
281
  class RawQuicClient(QuicConnectionProtocol):
293
- """Чистый‑QUIC клиент с приоритизированным SYS‑каналом + стриминг."""
294
282
 
295
283
  SYS_RATIO_NUM = 9 # SYS 9/10
296
284
  SYS_RATIO_DEN = 10
@@ -301,6 +289,8 @@ class RawQuicClient(QuicConnectionProtocol):
301
289
  def __init__(self, *args, **kwargs):
302
290
  super().__init__(*args, **kwargs)
303
291
 
292
+ self.quicClient: QuicClient = None
293
+
304
294
  self._sys_stream_id: Optional[int] = None
305
295
  self._queue_sys: Deque[Tuple[int, bytes, bool]] = deque()
306
296
  self._queue_user: Deque[Tuple[int, bytes, bool]] = deque()
@@ -427,6 +417,17 @@ class RawQuicClient(QuicConnectionProtocol):
427
417
  if not handler.done():
428
418
  handler.set_exception(RuntimeError("stream reset"))
429
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
+
430
431
  # ─────────────────────────────────────────── scheduler ─┤
431
432
  def _enqueue(self, sid: int, blob: bytes, end_stream: bool, is_sys: bool):
432
433
  (self._queue_sys if is_sys else self._queue_user).append((sid, blob, end_stream))
@@ -443,6 +444,7 @@ class RawQuicClient(QuicConnectionProtocol):
443
444
  if q is None:
444
445
  break
445
446
  sid, blob, end_stream = q.popleft()
447
+ print(f'Отправка стрима {sid}')
446
448
  self._quic.send_stream_data(sid, blob, end_stream=end_stream)
447
449
  self.transmit()
448
450
  self._activity()
@@ -563,10 +565,20 @@ class QuicClient:
563
565
  def __init__(self):
564
566
  self._quik_core: Optional[RawQuicClient] = None
565
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
566
575
 
567
576
  async def connect(self, ip: str, port: int):
577
+ self.status = 'connecting'
578
+ self.connection_time = datetime.datetime.now()
568
579
  cfg = QuicConfiguration(is_client=True, alpn_protocols=["gn:backend"])
569
580
  cfg.load_verify_locations(KeyisBClient.ssl_gw_crt_path)
581
+ cfg.idle_timeout = 10
570
582
 
571
583
  self._client_cm = connect(
572
584
  ip,
@@ -576,8 +588,16 @@ class QuicClient:
576
588
  wait_connected=True,
577
589
  )
578
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)
579
595
 
580
596
  async def disconnect(self):
597
+ self.status = 'disconnect'
598
+ if self._disconnect_signal is not None:
599
+ self._disconnect_signal(self._domain)
600
+
581
601
  self._quik_core.close()
582
602
  await self._quik_core.wait_closed()
583
603
  self._quik_core = None
@@ -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,9 +535,10 @@ 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.client_ip != '127.0.0.1':
540
+
541
+ if request._client_ip != '127.0.0.1':
526
542
  return gn.GNResponse('gn:backend:403', {'error': 'Forbidden'})
527
543
  return gn.GNResponse('ok', {'time': datetime.datetime.now(datetime.UTC).isoformat()})
528
544
 
@@ -622,7 +638,7 @@ class App:
622
638
  asyncio.create_task(self._handle_request(request, mode))
623
639
 
624
640
  async def _handle_request(self, request: gn.GNRequest, mode: int):
625
-
641
+
626
642
  request._client_ip = self._quic._network_paths[0].addr[0]
627
643
 
628
644
  try:
@@ -673,7 +689,8 @@ class App:
673
689
  key_path: str,
674
690
  *,
675
691
  idle_timeout: float = 20.0,
676
- wait: bool = True
692
+ wait: bool = True,
693
+ run: Optional[Callable] = None
677
694
  ):
678
695
 
679
696
  self._init_sys_routes()
@@ -691,6 +708,11 @@ class App:
691
708
  create_protocol=lambda *a, **kw: App._ServerProto(*a, api=self, **kw),
692
709
  retry=False,
693
710
  )
711
+
712
+ if run is not None:
713
+ await run()
714
+
715
+
694
716
  if wait:
695
717
  await asyncio.Event().wait()
696
718
 
@@ -0,0 +1,673 @@
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
+ class connector():
71
+ """Ошибка подключения к серверу <?>~connector.gn"""
72
+
73
+ class connection(Exception):
74
+ def __init__(self, message="Ошибка подключения к серверу <?>~connector.gn. Сервер не найден."):
75
+ super().__init__(message)
76
+
77
+ class timeout(Exception):
78
+ def __init__(self, message="Ошибка подключения к серверу <?>~connector.gn. Проблема с сетью или сервер перегружен"):
79
+ super().__init__(message)
80
+
81
+ class data(Exception):
82
+ def __init__(self, message="Ошибка подключения к серверу <?>~connector.gn. Сервер не подтвердил подключение."):
83
+ super().__init__(message)
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
+
101
+
102
+
103
+ from KeyisBClient.gn import GNRequest, GNResponse, GNProtocol
104
+
105
+
106
+
107
+
108
+ class AsyncClient:
109
+ def __init__(self):
110
+ self.__dns_core__ipv4 = '51.250.85.38:52943'
111
+ self.__dns_gn__ipv4 = None
112
+
113
+ self.__user = {}
114
+ self.__current_session = {}
115
+ self.__request_callbacks = {}
116
+ self.__response_callbacks = {}
117
+
118
+ self._active_connections: Dict[str, QuicClient] = {}
119
+
120
+ async def _getCoreDNS(self, domain: str):
121
+
122
+ if domain.split('.')[-1].split(':')[0].isdigit() and domain.split(':')[-1].isdigit():
123
+ r2_data = {
124
+ "ip": domain.split(':')[0],
125
+ "port": int(domain.split(':')[-1])
126
+ }
127
+ return r2_data
128
+
129
+ try:
130
+ if self.__dns_gn__ipv4 is None:
131
+ r1 = await httpxAsyncClient.request('GET', f'https://{self.__dns_core__ipv4}/gn/getIp?d=dns.gn')
132
+ if r1.status_code != 200:
133
+ raise GNExceptions.ConnectionError.dns_core.data
134
+ r1_data = r1.json()
135
+ self.__dns_gn__ipv4 = r1_data['ip'] + ':' + str(r1_data['port'])
136
+
137
+
138
+ r2 = await httpxAsyncClient.request('GET', f'https://{self.__dns_gn__ipv4}/gn/getIp?d={domain}')
139
+ except httpx.TimeoutException:
140
+ raise GNExceptions.ConnectionError.dns_core.timeout
141
+ except:
142
+ raise GNExceptions.ConnectionError.dns_core.connection
143
+
144
+ if r2.status_code != 200:
145
+ raise GNExceptions.ConnectionError.dns_core.data
146
+
147
+ r2_data = r2.json()
148
+
149
+ return r2_data
150
+
151
+ def addRequestCallback(self, callback: Callable, name: str):
152
+ self.__request_callbacks[name] = callback
153
+
154
+ def addResponseCallback(self, callback: Callable, name: str):
155
+ self.__response_callbacks[name] = callback
156
+
157
+
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
185
+
186
+ data = await self._getCoreDNS(domain)
187
+
188
+
189
+
190
+ def f(domain):
191
+ self._active_connections.pop(domain)
192
+
193
+ c._disconnect_signal = f
194
+ c._domain = domain
195
+
196
+ await c.connect(data['ip'], data['port'])
197
+ await c.connect_future
198
+
199
+ return c
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()
206
+
207
+
208
+ def _return_token(self, bigToken: str, s: bool = True) -> str:
209
+ return bigToken[:128] if s else bigToken[128:]
210
+
211
+
212
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]], restart_connection: bool = False, reconnect_wait: float = 10) -> GNResponse:
213
+ if isinstance(request, GNRequest):
214
+
215
+
216
+ c = await self.connect(request.url.hostname, restart_connection, reconnect_wait)
217
+
218
+
219
+
220
+ for f in self.__request_callbacks.values():
221
+ asyncio.create_task(f(request))
222
+
223
+ r = await c.asyncRequest(request)
224
+
225
+ for f in self.__response_callbacks.values():
226
+ asyncio.create_task(f(r))
227
+
228
+ return r
229
+
230
+ # else:
231
+ # async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
232
+ # async for req in request:
233
+ # if req.gn_protocol is None:
234
+ # req.setGNProtocol(self.__current_session['protocols'][0])
235
+ # req._stream = True
236
+
237
+ # for f in self.__request_callbacks.values():
238
+ # asyncio.create_task(f(req))
239
+
240
+ # yield req
241
+ # r = await self.client.asyncRequest(wrapped(request))
242
+
243
+ # for f in self.__response_callbacks.values():
244
+ # asyncio.create_task(f(r))
245
+
246
+ # return r
247
+
248
+ async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
249
+ """
250
+ Build and send a async request.
251
+ """
252
+ if isinstance(request, GNRequest):
253
+ if request.gn_protocol is None:
254
+ request.setGNProtocol(self.__current_session['protocols'][0])
255
+
256
+ for f in self.__request_callbacks.values():
257
+ asyncio.create_task(f(request))
258
+
259
+ async for response in self.client.asyncRequestStream(request):
260
+
261
+ for f in self.__response_callbacks.values():
262
+ asyncio.create_task(f(response))
263
+
264
+ yield response
265
+ else:
266
+ async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
267
+ async for req in request:
268
+ if req.gn_protocol is None:
269
+ req.setGNProtocol(self.__current_session['protocols'][0])
270
+
271
+ for f in self.__request_callbacks.values():
272
+ asyncio.create_task(f(req))
273
+
274
+ req._stream = True
275
+ yield req
276
+ async for response in self.client.asyncRequestStream(wrapped(request)):
277
+
278
+ for f in self.__response_callbacks.values():
279
+ asyncio.create_task(f(response))
280
+
281
+ yield response
282
+
283
+ from aioquic.asyncio.protocol import QuicConnectionProtocol
284
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
285
+ from aioquic.quic.connection import END_STATES
286
+ import asyncio
287
+ from collections import deque
288
+ from typing import Dict, Deque, Tuple, Optional, List
289
+
290
+ import asyncio
291
+ import time
292
+ from collections import deque
293
+ from dataclasses import dataclass
294
+ from itertools import count
295
+ from typing import Deque, Dict, Optional, Tuple, Union
296
+
297
+ from aioquic.quic.configuration import QuicConfiguration
298
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset, ConnectionTerminated
299
+
300
+
301
+ class RawQuicClient(QuicConnectionProtocol):
302
+
303
+ SYS_RATIO_NUM = 9 # SYS 9/10
304
+ SYS_RATIO_DEN = 10
305
+ KEEPALIVE_INTERVAL = 10 # сек
306
+ KEEPALIVE_IDLE_TRIGGER = 30 # сек
307
+
308
+ # ────────────────────────────────────────────────────────────────── init ─┐
309
+ def __init__(self, *args, **kwargs):
310
+ super().__init__(*args, **kwargs)
311
+
312
+ self.quicClient: QuicClient = None
313
+
314
+ self._sys_stream_id: Optional[int] = None
315
+ self._queue_sys: Deque[Tuple[int, bytes, bool]] = deque()
316
+ self._queue_user: Deque[Tuple[int, bytes, bool]] = deque()
317
+
318
+ # <‑‑ Future | Queue[bytes | None]
319
+ self._inflight: Dict[int, Union[asyncio.Future, asyncio.Queue[Optional[GNResponse]]]] = {}
320
+ self._inflight_streams: Dict[int, bytearray] = {}
321
+ self._sys_inflight: Dict[int, asyncio.Future] = {}
322
+ self._buffer: Dict[Union[int, str], bytearray] = {}
323
+
324
+ self._sys_budget = self.SYS_RATIO_NUM
325
+ self._sys_id_gen = count(1) # int64 message‑id generator
326
+
327
+ self._last_activity = time.time()
328
+ self._running = True
329
+ self._ping_id_gen = count(1) # int64 ping‑id generator
330
+ asyncio.create_task(self._keepalive_loop())
331
+
332
+ # ───────────────────────────────────────── private helpers ─┤
333
+ def _activity(self):
334
+ self._last_activity = time.time()
335
+
336
+ async def _keepalive_loop(self):
337
+ while self._running:
338
+ await asyncio.sleep(self.KEEPALIVE_INTERVAL)
339
+ idle_time = time.time() - self._last_activity
340
+ if idle_time > self.KEEPALIVE_IDLE_TRIGGER:
341
+ self._quic.send_ping(next(self._ping_id_gen))
342
+ self.transmit()
343
+ self._last_activity = time.time()
344
+
345
+ def stop(self):
346
+ self._running = False
347
+
348
+ # ───────────────────────────────────────────── events ─┤
349
+ def quic_event_received(self, event: QuicEvent) -> None: # noqa: C901
350
+ # ─── DATA ───────────────────────────────────────────
351
+ if isinstance(event, StreamDataReceived):
352
+ #print(event)
353
+ # SYS поток
354
+ if event.stream_id == self._sys_stream_id:
355
+ buf = self._buffer.setdefault("sys", bytearray())
356
+ buf.extend(event.data)
357
+ while True:
358
+ if len(buf) < 12:
359
+ break
360
+ msg_id = int.from_bytes(buf[:8], "little")
361
+ size = int.from_bytes(buf[8:12], "little")
362
+ if len(buf) < 12 + size:
363
+ break
364
+ payload = bytes(buf[12 : 12 + size])
365
+ del buf[: 12 + size]
366
+ fut = self._sys_inflight.pop(msg_id, None) if msg_id else None
367
+ if fut and not fut.done():
368
+ fut.set_result(payload)
369
+ # USER поток
370
+ else:
371
+ handler = self._inflight.get(event.stream_id)
372
+ if handler is None:
373
+ return
374
+
375
+ # Чтение в зависимости от режима
376
+ if isinstance(handler, asyncio.Queue): # стрим от сервера
377
+ # получаем байты
378
+
379
+ buf = self._buffer.setdefault(event.stream_id, bytearray())
380
+ buf.extend(event.data)
381
+
382
+ if len(buf) < 8: # не дошел даже frame пакета
383
+ return
384
+
385
+ # получаем длинну пакета
386
+ mode, stream, lenght = GNResponse.type(buf)
387
+
388
+ if mode != 4: # не наш пакет
389
+ self._buffer.pop(event.stream_id)
390
+ return
391
+
392
+ if not stream: # клиент просил стрим, а сервер прислал один пакет
393
+ self._buffer.pop(event.stream_id)
394
+ return
395
+
396
+ # читаем пакет
397
+ if len(buf) < lenght: # если пакет не весь пришел, пропускаем
398
+ return
399
+
400
+ # пакет пришел весь
401
+
402
+ # берем пакет
403
+ data = buf[:lenght]
404
+
405
+ # удаляем его из буфера
406
+ del buf[:lenght]
407
+
408
+
409
+ r = GNResponse.deserialize(data, 2)
410
+ handler.put_nowait(r)
411
+ if event.end_stream:
412
+ handler.put_nowait(None)
413
+ self._buffer.pop(event.stream_id)
414
+ self._inflight.pop(event.stream_id, None)
415
+
416
+
417
+
418
+ else: # Future
419
+ buf = self._buffer.setdefault(event.stream_id, bytearray())
420
+ buf.extend(event.data)
421
+ if event.end_stream:
422
+ self._inflight.pop(event.stream_id, None)
423
+ data = bytes(self._buffer.pop(event.stream_id, b""))
424
+ if not handler.done():
425
+ handler.set_result(data)
426
+
427
+ # ─── RESET ──────────────────────────────────────────
428
+ elif isinstance(event, StreamReset):
429
+ handler = self._inflight.pop(event.stream_id, None) or self._sys_inflight.pop(
430
+ event.stream_id, None
431
+ )
432
+ if handler is None:
433
+ return
434
+ if isinstance(handler, asyncio.Queue):
435
+ handler.put_nowait(None)
436
+ else:
437
+ if not handler.done():
438
+ handler.set_exception(RuntimeError("stream reset"))
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
+
451
+ # ─────────────────────────────────────────── scheduler ─┤
452
+ def _enqueue(self, sid: int, blob: bytes, end_stream: bool, is_sys: bool):
453
+ (self._queue_sys if is_sys else self._queue_user).append((sid, blob, end_stream))
454
+
455
+ def _schedule_flush(self):
456
+ while (self._queue_sys or self._queue_user) and self._quic._close_event is None:
457
+ q = None
458
+ if self._queue_sys and (self._sys_budget > 0 or not self._queue_user):
459
+ q = self._queue_sys
460
+ self._sys_budget -= 1
461
+ elif self._queue_user:
462
+ q = self._queue_user
463
+ self._sys_budget = self.SYS_RATIO_NUM
464
+ if q is None:
465
+ break
466
+ sid, blob, end_stream = q.popleft()
467
+ print(f'Отправка стрима {sid}')
468
+ self._quic.send_stream_data(sid, blob, end_stream=end_stream)
469
+ self.transmit()
470
+ self._activity()
471
+
472
+ # ─────────────────────────────────────────── public API ─┤
473
+ async def ensure_sys_stream(self):
474
+ if self._sys_stream_id is None:
475
+ self._sys_stream_id = self._quic.get_next_available_stream_id()
476
+ self._enqueue(self._sys_stream_id, b"", False, True) # dummy
477
+ self._schedule_flush()
478
+
479
+ async def send_sys(self, request: GNRequest, response: bool = False) -> Optional[bytes]:
480
+ await self.ensure_sys_stream()
481
+ if response:
482
+ msg_id = next(self._sys_id_gen)
483
+ blob = request.serialize(2)
484
+ payload = (
485
+ msg_id.to_bytes(8, "little") + len(blob).to_bytes(4, "little") + blob
486
+ )
487
+ fut = asyncio.get_running_loop().create_future()
488
+ self._sys_inflight[msg_id] = fut
489
+ self._enqueue(self._sys_stream_id, payload, False, True)
490
+ self._schedule_flush()
491
+ return await fut
492
+ payload = (0).to_bytes(8, "little") + request.serialize(2)
493
+ self._enqueue(self._sys_stream_id, payload, False, True)
494
+ self._schedule_flush()
495
+ return None
496
+
497
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]):
498
+ if isinstance(request, GNRequest):
499
+ blob = request.serialize(2)
500
+ sid = self._quic.get_next_available_stream_id()
501
+ self._enqueue(sid, blob, True, False)
502
+ self._schedule_flush()
503
+
504
+
505
+ fut = asyncio.get_running_loop().create_future()
506
+ self._inflight[sid] = fut
507
+ return await fut
508
+
509
+ else:
510
+ sid = self._quic.get_next_available_stream_id()
511
+ #if sid in self._quic._streams and not self._quic._streams[sid].is_finished:
512
+
513
+ async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
514
+ _last = None
515
+ async for req in request:
516
+ _last = req
517
+ blob = req.serialize(2)
518
+ self._enqueue(sid, blob, False, False)
519
+
520
+
521
+ self._schedule_flush()
522
+
523
+ print(f'Отправлен stream запрос {req}')
524
+
525
+
526
+ _last.setPayload(None)
527
+ _last.setMethod('gn:end-stream')
528
+ blob = _last.serialize(2)
529
+ self._enqueue(sid, blob, True, False)
530
+ self._schedule_flush()
531
+
532
+ asyncio.create_task(_stream_sender(sid, request))
533
+
534
+
535
+ fut = asyncio.get_running_loop().create_future()
536
+ self._inflight[sid] = fut
537
+ return await fut
538
+
539
+ async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> asyncio.Queue[GNResponse]:
540
+ if isinstance(request, GNRequest):
541
+ blob = request.serialize(2)
542
+ sid = self._quic.get_next_available_stream_id()
543
+ self._enqueue(sid, blob, False, False)
544
+ self._schedule_flush()
545
+
546
+
547
+ q = asyncio.Queue()
548
+ self._inflight[sid] = q
549
+ return q
550
+
551
+ else:
552
+ sid = self._quic.get_next_available_stream_id()
553
+
554
+ async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
555
+ _last = None
556
+ async for req in request:
557
+ _last = req
558
+ blob = req.serialize(2)
559
+ self._enqueue(sid, blob, False, False)
560
+
561
+
562
+ self._schedule_flush()
563
+
564
+ print(f'Отправлен stream запрос {req}')
565
+
566
+
567
+ _last.setPayload(None)
568
+ _last.setMethod('gn:end-stream')
569
+ blob = _last.serialize(2)
570
+ self._enqueue(sid, blob, True, False)
571
+ self._schedule_flush()
572
+
573
+ asyncio.create_task(_stream_sender(sid, request))
574
+
575
+
576
+ q = asyncio.Queue()
577
+ self._inflight[sid] = q
578
+ return q
579
+
580
+
581
+
582
+ class QuicClient:
583
+ """Обёртка‑фасад над RawQuicClient."""
584
+
585
+ def __init__(self):
586
+ self._quik_core: Optional[RawQuicClient] = None
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
595
+
596
+ async def connect(self, ip: str, port: int):
597
+ self.status = 'connecting'
598
+ self.connection_time = datetime.datetime.now()
599
+ cfg = QuicConfiguration(is_client=True, alpn_protocols=["gn:backend"])
600
+ cfg.load_verify_locations(KeyisBClient.ssl_gw_crt_path)
601
+ cfg.idle_timeout = 40
602
+
603
+ self._client_cm = connect(
604
+ ip,
605
+ port,
606
+ configuration=cfg,
607
+ create_protocol=RawQuicClient,
608
+ wait_connected=True,
609
+ )
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__()
624
+
625
+ async def disconnect(self):
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
+
648
+
649
+ self._quik_core.close()
650
+ await self._quik_core.wait_closed()
651
+ self._quik_core = None
652
+
653
+ async def asyncRequest(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
654
+ if self._quik_core is None:
655
+ raise RuntimeError("Not connected")
656
+
657
+ resp = await self._quik_core.request(request)
658
+ return GNResponse.deserialize(resp, 2)
659
+
660
+ async def asyncRequestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
661
+
662
+ if self._quik_core is None:
663
+ raise RuntimeError("Not connected")
664
+
665
+ queue = await self._quik_core.requestStream(request)
666
+
667
+ while True:
668
+ chunk = await queue.get()
669
+ if chunk is None or chunk.command == 'gn:end-stream':
670
+ break
671
+ yield chunk
672
+
673
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GNServer
3
- Version: 0.0.0.0.17
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
@@ -3,6 +3,7 @@ MANIFEST.in
3
3
  setup.py
4
4
  GNServer/LICENSE
5
5
  GNServer/mmbConfig.json
6
+ GNServer/GNServer/___client.py
6
7
  GNServer/GNServer/__init__.py
7
8
  GNServer/GNServer/_app.py
8
9
  GNServer/GNServer/_client.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GNServer
3
- Version: 0.0.0.0.17
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
@@ -5,7 +5,7 @@ filesName = 'GNServer'
5
5
 
6
6
  setup(
7
7
  name=name,
8
- version='0.0.0.0.17',
8
+ version='0.0.0.0.18',
9
9
  author="KeyisB",
10
10
  author_email="keyisb.pip@gmail.com",
11
11
  description=name,
File without changes
File without changes
File without changes