GNServer 0.0.0.0.12__py3-none-any.whl → 0.0.0.0.14__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/__init__.py CHANGED
@@ -37,3 +37,4 @@ from KeyisBClient.gn import GNRequest, GNResponse, CORSObject, FileObject, Templ
37
37
 
38
38
 
39
39
 
40
+ from ._client import AsyncClient
GNServer/_app.py CHANGED
@@ -75,6 +75,7 @@ def guess_type(filename: str) -> str:
75
75
  "webp": "image/webp",
76
76
  "svg": "image/svg+xml",
77
77
  "avif": "image/avif",
78
+ "ico": "image/x-icon",
78
79
 
79
80
  # 🔹 Видео (современные форматы)
80
81
  "mp4": "video/mp4",
@@ -171,6 +172,14 @@ def resolve_cors(origin_url: str, rules: list[str]) -> bool:
171
172
  - "https://*.site.tld" -> с проверкой схемы
172
173
  - "!<regex>" -> полное соответствие по regex к origin_url
173
174
  """
175
+
176
+ if origin_url == 'gn:proxy:sys':
177
+ return True
178
+
179
+
180
+
181
+
182
+
174
183
  origin = origin_url.rstrip("/")
175
184
  pu = urlparse(origin)
176
185
  scheme = (pu.scheme or "").lower()
@@ -374,6 +383,8 @@ class App:
374
383
  self._cors: Optional[gn.CORSObject] = None
375
384
 
376
385
  def route(self, method: str, path: str, cors: Optional[gn.CORSObject] = None):
386
+ if path == '/':
387
+ path = ''
377
388
  def decorator(fn: Callable[..., Any]):
378
389
  regex, param_types = _compile_path(path)
379
390
  self._routes.append(
@@ -430,7 +441,7 @@ class App:
430
441
  return gn.GNResponse("gn:backend:801", {'error': 'Cors error. Route has cors but request has no origin url.'})
431
442
  if not resolve_cors(request._origin, r.cors._allow_origins):
432
443
  return gn.GNResponse("gn:backend:802", {'error': 'Cors error: origin'})
433
- if request.method not in r.cors._allow_methods:
444
+ if request.method not in r.cors._allow_methods and '*' not in r.cors._allow_methods:
434
445
  return gn.GNResponse("gn:backend:803", {'error': 'Cors error: method'})
435
446
 
436
447
  sig = inspect.signature(r.handler)
@@ -457,12 +468,14 @@ class App:
457
468
 
458
469
  result = await r.handler(**kw)
459
470
  if isinstance(result, gn.GNResponse):
471
+ if result._cors is None:
472
+ result._cors = self._cors
460
473
  if result._cors is not None and result._cors != r.cors:
461
474
  if request._origin is None:
462
475
  return gn.GNResponse("gn:backend:801", {'error': 'Cors error. Route has cors but request has no origin url.'})
463
- if not resolve_cors(request._origin, r.cors._allow_origins):
476
+ if not resolve_cors(request._origin, result._cors._allow_origins):
464
477
  return gn.GNResponse("gn:backend:802", {'error': 'Cors error: origin'})
465
- if request.method not in r.cors._allow_methods:
478
+ if request.method not in result._cors._allow_methods and '*' not in result._cors._allow_methods:
466
479
  return gn.GNResponse("gn:backend:803", {'error': 'Cors error: method'})
467
480
  return result
468
481
  else:
@@ -536,19 +549,19 @@ class App:
536
549
  # получаем длинну пакета
537
550
  mode, stream, lenght = gn.GNRequest.type(buf)
538
551
 
539
- if mode != 2: # не наш пакет
540
- logger.debug(f'Пакет отклонен: mode пакета {mode}. Разрешен 2')
552
+ if mode not in (1, 2): # не наш пакет
553
+ logger.debug(f'Пакет отклонен: mode пакета {mode}. Разрешен 1, 2')
541
554
  return
542
555
 
543
556
  if not stream: # если не стрим, то ждем конец quic стрима и запускаем обработку ответа
544
557
  if event.end_stream:
545
- request = gn.GNRequest.deserialize(buf, 2)
558
+ request = gn.GNRequest.deserialize(buf, mode)
546
559
  # request.stream_id = event.stream_id
547
560
  # loop = asyncio.get_event_loop()
548
561
  # request.fut = loop.create_future()
549
562
 
550
563
  request.stream_id = event.stream_id
551
- asyncio.create_task(self._handle_request(request))
564
+ asyncio.create_task(self._handle_request(request, mode))
552
565
  logger.debug(f'Отправлена задача разрешения пакета {request} route -> {request.route}')
553
566
 
554
567
  self._buffer.pop(event.stream_id, None)
@@ -567,7 +580,7 @@ class App:
567
580
  del buf[:lenght]
568
581
 
569
582
  # формируем запрос
570
- request = gn.GNRequest.deserialize(data, 2)
583
+ request = gn.GNRequest.deserialize(data, mode)
571
584
 
572
585
  logger.debug(request, f'event.stream_id -> {event.stream_id}')
573
586
 
@@ -604,9 +617,9 @@ class App:
604
617
  yield chunk
605
618
 
606
619
  request._stream = w
607
- asyncio.create_task(self._handle_request(request))
620
+ asyncio.create_task(self._handle_request(request, mode))
608
621
 
609
- async def _handle_request(self, request: gn.GNRequest):
622
+ async def _handle_request(self, request: gn.GNRequest, mode: int):
610
623
  try:
611
624
 
612
625
  response = await self._api.dispatch(request)
@@ -615,32 +628,29 @@ class App:
615
628
  async for chunk in response: # type: ignore[misc]
616
629
  chunk._stream = True
617
630
  chunk = await self.resolve_response(chunk)
618
- self._quic.send_stream_data(request.stream_id, chunk.serialize(3), end_stream=False)
631
+ self._quic.send_stream_data(request.stream_id, chunk.serialize(mode), end_stream=False)
619
632
  self.transmit()
620
633
 
621
634
  l = gn.GNResponse('gn:end-stream')
622
635
  l._stream = True
623
636
  l = self.resolve_response(l)
624
- self._quic.send_stream_data(request.stream_id, l.serialize(3), end_stream=True)
637
+ self._quic.send_stream_data(request.stream_id, l.serialize(mode), end_stream=True)
625
638
  self.transmit()
626
639
  return
627
640
 
628
641
 
629
642
  response = await self.resolve_response(response)
630
- self._quic.send_stream_data(request.stream_id, response.serialize(3), end_stream=True)
643
+ self._quic.send_stream_data(request.stream_id, response.serialize(mode), end_stream=True)
631
644
  logger.debug(f'Отправлен на сервер ответ -> {response.command} {response.payload if response.payload and len((response.payload)) < 200 else ''}')
632
645
  self.transmit()
633
646
  except Exception as e:
634
647
  logger.error('GNServer: error\n' + traceback.format_exc())
635
648
 
636
649
  response = gn.GNResponse('gn:backend:500')
637
- self._quic.send_stream_data(request.stream_id, response.serialize(3), end_stream=True)
650
+ self._quic.send_stream_data(request.stream_id, response.serialize(mode), end_stream=True)
638
651
  self.transmit()
639
652
 
640
653
  async def resolve_response(self, response: gn.GNResponse) -> gn.GNResponse:
641
- if response._cors is None:
642
- response._cors = self._api._cors
643
-
644
654
  await response.assembly()
645
655
 
646
656
  return response
GNServer/_client.py ADDED
@@ -0,0 +1,601 @@
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
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._client: QuicClient = QuicClient()
105
+
106
+ self._active_connections: Dict[str, Any] = {}
107
+
108
+ async def _getCoreDNS(self, domain: str):
109
+ try:
110
+ if self.__dns_gn__ipv4 is None:
111
+ r1 = await httpxAsyncClient.request('GET', f'https://{self.__dns_core__ipv4}/gn/getIp?d=dns.gn')
112
+ if r1.status_code != 200:
113
+ raise GNExceptions.ConnectionError.dns_core.data
114
+ r1_data = r1.json()
115
+ self.__dns_gn__ipv4 = r1_data['ip'] + ':' + str(r1_data['port'])
116
+
117
+
118
+ r2 = await httpxAsyncClient.request('GET', f'https://{self.__dns_gn__ipv4}/gn/getIp?d={domain}')
119
+ except httpx.TimeoutException:
120
+ raise GNExceptions.ConnectionError.dns_core.timeout
121
+ except:
122
+ raise GNExceptions.ConnectionError.dns_core.connection
123
+
124
+ if r2.status_code != 200:
125
+ raise GNExceptions.ConnectionError.dns_core.data
126
+
127
+ r2_data = r2.json()
128
+
129
+ return r2_data
130
+
131
+ def addRequestCallback(self, callback: Callable, name: str):
132
+ self.__request_callbacks[name] = callback
133
+
134
+ def addResponseCallback(self, callback: Callable, name: str):
135
+ self.__response_callbacks[name] = callback
136
+
137
+
138
+ async def connect(self, domain: str):
139
+ if domain in self._active_connections:
140
+ return
141
+
142
+ data = await self._getCoreDNS(domain)
143
+
144
+ # подключаемся к серверу gn-proxy
145
+ await self._client.connect(data['ip'], data['port'])
146
+ self._active_connections[domain] = 'active'
147
+
148
+ async def disconnect(self):
149
+ await self._client.disconnect()
150
+
151
+
152
+ def _return_token(self, bigToken: str, s: bool = True) -> str:
153
+ return bigToken[:128] if s else bigToken[128:]
154
+
155
+
156
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
157
+ """
158
+ Build and send a async request.
159
+
160
+ ```python
161
+ gnAsyncClient = KeyisBClient.AsyncClient()
162
+ async def func():
163
+ response = await gnAsyncClient.request(GNRequest('GET', Url('gn://example.com/example')))
164
+ command = response.command()
165
+ data = response.payload()
166
+ ```
167
+ """
168
+
169
+
170
+
171
+
172
+
173
+
174
+ if isinstance(request, GNRequest):
175
+
176
+
177
+ if request.url.hostname not in self._active_connections:
178
+ await self.connect(request.url.hostname)
179
+
180
+
181
+
182
+ for f in self.__request_callbacks.values():
183
+ asyncio.create_task(f(request))
184
+
185
+ r = await self._client.asyncRequest(request)
186
+
187
+ for f in self.__response_callbacks.values():
188
+ asyncio.create_task(f(r))
189
+
190
+ return r
191
+
192
+ # else:
193
+ # async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
194
+ # async for req in request:
195
+ # if req.gn_protocol is None:
196
+ # req.setGNProtocol(self.__current_session['protocols'][0])
197
+ # req._stream = True
198
+
199
+ # for f in self.__request_callbacks.values():
200
+ # asyncio.create_task(f(req))
201
+
202
+ # yield req
203
+ # r = await self.client.asyncRequest(wrapped(request))
204
+
205
+ # for f in self.__response_callbacks.values():
206
+ # asyncio.create_task(f(r))
207
+
208
+ # return r
209
+
210
+ async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
211
+ """
212
+ Build and send a async request.
213
+ """
214
+ if isinstance(request, GNRequest):
215
+ if request.gn_protocol is None:
216
+ request.setGNProtocol(self.__current_session['protocols'][0])
217
+
218
+ for f in self.__request_callbacks.values():
219
+ asyncio.create_task(f(request))
220
+
221
+ async for response in self.client.asyncRequestStream(request):
222
+
223
+ for f in self.__response_callbacks.values():
224
+ asyncio.create_task(f(response))
225
+
226
+ yield response
227
+ else:
228
+ async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
229
+ async for req in request:
230
+ if req.gn_protocol is None:
231
+ req.setGNProtocol(self.__current_session['protocols'][0])
232
+
233
+ for f in self.__request_callbacks.values():
234
+ asyncio.create_task(f(req))
235
+
236
+ req._stream = True
237
+ yield req
238
+ async for response in self.client.asyncRequestStream(wrapped(request)):
239
+
240
+ for f in self.__response_callbacks.values():
241
+ asyncio.create_task(f(response))
242
+
243
+ yield response
244
+
245
+
246
+
247
+
248
+
249
+
250
+
251
+
252
+
253
+ # gn:quik
254
+
255
+ from aioquic.asyncio.protocol import QuicConnectionProtocol
256
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
257
+ from aioquic.quic.connection import END_STATES
258
+ import asyncio
259
+ from collections import deque
260
+ from typing import Dict, Deque, Tuple, Optional, List
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Raw QUIC client with a dedicated SYS‑stream that consumes ~90 % CWND
264
+ # ---------------------------------------------------------------------------
265
+ # Основная идея:
266
+ # • Один постоянный bidirectional stream (sys_stream_id) используется для
267
+ # служебных сообщений.
268
+ # • Остальные запросы открываются в обычных потоках (user streams).
269
+ # • Отправка данных идёт через собственный scheduler: берём 9 «квантов» из
270
+ # SYS‑очереди и 1 квант из USER‑очереди, пока есть SYS‑данные.
271
+ # • Таким образом SYS‑канал получает ~90 % пропускной способности.
272
+ # ---------------------------------------------------------------------------
273
+
274
+ import asyncio
275
+ import time
276
+ from collections import deque
277
+ from dataclasses import dataclass
278
+ from itertools import count
279
+ from typing import Deque, Dict, Optional, Tuple, Union
280
+
281
+ from aioquic.quic.configuration import QuicConfiguration
282
+ from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
283
+
284
+
285
+ class RawQuicClient(QuicConnectionProtocol):
286
+ """Чистый‑QUIC клиент с приоритизированным SYS‑каналом + стриминг."""
287
+
288
+ SYS_RATIO_NUM = 9 # SYS 9/10
289
+ SYS_RATIO_DEN = 10
290
+ KEEPALIVE_INTERVAL = 10 # сек
291
+ KEEPALIVE_IDLE_TRIGGER = 30 # сек
292
+
293
+ # ────────────────────────────────────────────────────────────────── init ─┐
294
+ def __init__(self, *args, **kwargs):
295
+ super().__init__(*args, **kwargs)
296
+
297
+ self._sys_stream_id: Optional[int] = None
298
+ self._queue_sys: Deque[Tuple[int, bytes, bool]] = deque()
299
+ self._queue_user: Deque[Tuple[int, bytes, bool]] = deque()
300
+
301
+ # <‑‑ Future | Queue[bytes | None]
302
+ self._inflight: Dict[int, Union[asyncio.Future, asyncio.Queue[Optional[GNResponse]]]] = {}
303
+ self._inflight_streams: Dict[int, bytearray] = {}
304
+ self._sys_inflight: Dict[int, asyncio.Future] = {}
305
+ self._buffer: Dict[Union[int, str], bytearray] = {}
306
+
307
+ self._sys_budget = self.SYS_RATIO_NUM
308
+ self._sys_id_gen = count(1) # int64 message‑id generator
309
+
310
+ self._last_activity = time.time()
311
+ self._running = True
312
+ self._ping_id_gen = count(1) # int64 ping‑id generator
313
+ asyncio.create_task(self._keepalive_loop())
314
+
315
+ # ───────────────────────────────────────── private helpers ─┤
316
+ def _activity(self):
317
+ self._last_activity = time.time()
318
+
319
+ async def _keepalive_loop(self):
320
+ while self._running:
321
+ await asyncio.sleep(self.KEEPALIVE_INTERVAL)
322
+ idle_time = time.time() - self._last_activity
323
+ if idle_time > self.KEEPALIVE_IDLE_TRIGGER:
324
+ self._quic.send_ping(next(self._ping_id_gen))
325
+ self.transmit()
326
+ self._last_activity = time.time()
327
+
328
+ def stop(self):
329
+ self._running = False
330
+
331
+ # ───────────────────────────────────────────── events ─┤
332
+ def quic_event_received(self, event: QuicEvent) -> None: # noqa: C901
333
+ # ─── DATA ───────────────────────────────────────────
334
+ if isinstance(event, StreamDataReceived):
335
+ #print(event)
336
+ # SYS поток
337
+ if event.stream_id == self._sys_stream_id:
338
+ buf = self._buffer.setdefault("sys", bytearray())
339
+ buf.extend(event.data)
340
+ while True:
341
+ if len(buf) < 12:
342
+ break
343
+ msg_id = int.from_bytes(buf[:8], "little")
344
+ size = int.from_bytes(buf[8:12], "little")
345
+ if len(buf) < 12 + size:
346
+ break
347
+ payload = bytes(buf[12 : 12 + size])
348
+ del buf[: 12 + size]
349
+ fut = self._sys_inflight.pop(msg_id, None) if msg_id else None
350
+ if fut and not fut.done():
351
+ fut.set_result(payload)
352
+ # USER поток
353
+ else:
354
+ handler = self._inflight.get(event.stream_id)
355
+ if handler is None:
356
+ return
357
+
358
+ # Чтение в зависимости от режима
359
+ if isinstance(handler, asyncio.Queue): # стрим от сервера
360
+ # получаем байты
361
+
362
+ buf = self._buffer.setdefault(event.stream_id, bytearray())
363
+ buf.extend(event.data)
364
+
365
+ if len(buf) < 8: # не дошел даже frame пакета
366
+ return
367
+
368
+ # получаем длинну пакета
369
+ mode, stream, lenght = GNResponse.type(buf)
370
+
371
+ if mode != 4: # не наш пакет
372
+ self._buffer.pop(event.stream_id)
373
+ return
374
+
375
+ if not stream: # клиент просил стрим, а сервер прислал один пакет
376
+ self._buffer.pop(event.stream_id)
377
+ return
378
+
379
+ # читаем пакет
380
+ if len(buf) < lenght: # если пакет не весь пришел, пропускаем
381
+ return
382
+
383
+ # пакет пришел весь
384
+
385
+ # берем пакет
386
+ data = buf[:lenght]
387
+
388
+ # удаляем его из буфера
389
+ del buf[:lenght]
390
+
391
+
392
+ r = GNResponse.deserialize(data, 2)
393
+ handler.put_nowait(r)
394
+ if event.end_stream:
395
+ handler.put_nowait(None)
396
+ self._buffer.pop(event.stream_id)
397
+ self._inflight.pop(event.stream_id, None)
398
+
399
+
400
+
401
+ else: # Future
402
+ buf = self._buffer.setdefault(event.stream_id, bytearray())
403
+ buf.extend(event.data)
404
+ if event.end_stream:
405
+ self._inflight.pop(event.stream_id, None)
406
+ data = bytes(self._buffer.pop(event.stream_id, b""))
407
+ if not handler.done():
408
+ handler.set_result(data)
409
+
410
+ # ─── RESET ──────────────────────────────────────────
411
+ elif isinstance(event, StreamReset):
412
+ handler = self._inflight.pop(event.stream_id, None) or self._sys_inflight.pop(
413
+ event.stream_id, None
414
+ )
415
+ if handler is None:
416
+ return
417
+ if isinstance(handler, asyncio.Queue):
418
+ handler.put_nowait(None)
419
+ else:
420
+ if not handler.done():
421
+ handler.set_exception(RuntimeError("stream reset"))
422
+
423
+ # ─────────────────────────────────────────── scheduler ─┤
424
+ def _enqueue(self, sid: int, blob: bytes, end_stream: bool, is_sys: bool):
425
+ (self._queue_sys if is_sys else self._queue_user).append((sid, blob, end_stream))
426
+
427
+ def _schedule_flush(self):
428
+ while (self._queue_sys or self._queue_user) and self._quic._close_event is None:
429
+ q = None
430
+ if self._queue_sys and (self._sys_budget > 0 or not self._queue_user):
431
+ q = self._queue_sys
432
+ self._sys_budget -= 1
433
+ elif self._queue_user:
434
+ q = self._queue_user
435
+ self._sys_budget = self.SYS_RATIO_NUM
436
+ if q is None:
437
+ break
438
+ sid, blob, end_stream = q.popleft()
439
+ self._quic.send_stream_data(sid, blob, end_stream=end_stream)
440
+ self.transmit()
441
+ self._activity()
442
+
443
+ # ─────────────────────────────────────────── public API ─┤
444
+ async def ensure_sys_stream(self):
445
+ if self._sys_stream_id is None:
446
+ self._sys_stream_id = self._quic.get_next_available_stream_id()
447
+ self._enqueue(self._sys_stream_id, b"", False, True) # dummy
448
+ self._schedule_flush()
449
+
450
+ async def send_sys(self, request: GNRequest, response: bool = False) -> Optional[bytes]:
451
+ await self.ensure_sys_stream()
452
+ if response:
453
+ msg_id = next(self._sys_id_gen)
454
+ blob = request.serialize(2)
455
+ payload = (
456
+ msg_id.to_bytes(8, "little") + len(blob).to_bytes(4, "little") + blob
457
+ )
458
+ fut = asyncio.get_running_loop().create_future()
459
+ self._sys_inflight[msg_id] = fut
460
+ self._enqueue(self._sys_stream_id, payload, False, True)
461
+ self._schedule_flush()
462
+ return await fut
463
+ payload = (0).to_bytes(8, "little") + request.serialize(2)
464
+ self._enqueue(self._sys_stream_id, payload, False, True)
465
+ self._schedule_flush()
466
+ return None
467
+
468
+ async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]):
469
+ if isinstance(request, GNRequest):
470
+ blob = request.serialize(2)
471
+ sid = self._quic.get_next_available_stream_id()
472
+ self._enqueue(sid, blob, True, False)
473
+ self._schedule_flush()
474
+
475
+
476
+ fut = asyncio.get_running_loop().create_future()
477
+ self._inflight[sid] = fut
478
+ return await fut
479
+
480
+ else:
481
+ sid = self._quic.get_next_available_stream_id()
482
+ #if sid in self._quic._streams and not self._quic._streams[sid].is_finished:
483
+
484
+ async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
485
+ _last = None
486
+ async for req in request:
487
+ _last = req
488
+ blob = req.serialize(2)
489
+ self._enqueue(sid, blob, False, False)
490
+
491
+
492
+ self._schedule_flush()
493
+
494
+ print(f'Отправлен stream запрос {req}')
495
+
496
+
497
+ _last.setPayload(None)
498
+ _last.setMethod('gn:end-stream')
499
+ blob = _last.serialize(2)
500
+ self._enqueue(sid, blob, True, False)
501
+ self._schedule_flush()
502
+
503
+ asyncio.create_task(_stream_sender(sid, request))
504
+
505
+
506
+ fut = asyncio.get_running_loop().create_future()
507
+ self._inflight[sid] = fut
508
+ return await fut
509
+
510
+ async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> asyncio.Queue[GNResponse]:
511
+ if isinstance(request, GNRequest):
512
+ blob = request.serialize(2)
513
+ sid = self._quic.get_next_available_stream_id()
514
+ self._enqueue(sid, blob, False, False)
515
+ self._schedule_flush()
516
+
517
+
518
+ q = asyncio.Queue()
519
+ self._inflight[sid] = q
520
+ return q
521
+
522
+ else:
523
+ sid = self._quic.get_next_available_stream_id()
524
+
525
+ async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
526
+ _last = None
527
+ async for req in request:
528
+ _last = req
529
+ blob = req.serialize(2)
530
+ self._enqueue(sid, blob, False, False)
531
+
532
+
533
+ self._schedule_flush()
534
+
535
+ print(f'Отправлен stream запрос {req}')
536
+
537
+
538
+ _last.setPayload(None)
539
+ _last.setMethod('gn:end-stream')
540
+ blob = _last.serialize(2)
541
+ self._enqueue(sid, blob, True, False)
542
+ self._schedule_flush()
543
+
544
+ asyncio.create_task(_stream_sender(sid, request))
545
+
546
+
547
+ q = asyncio.Queue()
548
+ self._inflight[sid] = q
549
+ return q
550
+
551
+
552
+
553
+ class QuicClient:
554
+ """Обёртка‑фасад над RawQuicClient."""
555
+
556
+ def __init__(self):
557
+ self._quik_core: Optional[RawQuicClient] = None
558
+ self._client_cm = None
559
+
560
+ async def connect(self, ip: str, port: int):
561
+ cfg = QuicConfiguration(is_client=True, alpn_protocols=["gn:backend"])
562
+ cfg.load_verify_locations(KeyisBClient.ssl_gw_crt_path)
563
+
564
+ self._client_cm = connect(
565
+ ip,
566
+ port,
567
+ configuration=cfg,
568
+ create_protocol=RawQuicClient,
569
+ wait_connected=True,
570
+ )
571
+ self._quik_core = await self._client_cm.__aenter__()
572
+
573
+ async def disconnect(self):
574
+ self._quik_core.close()
575
+ await self._quik_core.wait_closed()
576
+ self._quik_core = None
577
+
578
+ def syncRequest(self, request: GNRequest):
579
+ return asyncio.get_event_loop().run_until_complete(self.asyncRequest(request))
580
+
581
+ async def asyncRequest(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
582
+ if self._quik_core is None:
583
+ raise RuntimeError("Not connected")
584
+
585
+ resp = await self._quik_core.request(request)
586
+ return GNResponse.deserialize(resp, 2)
587
+
588
+ async def asyncRequestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
589
+
590
+ if self._quik_core is None:
591
+ raise RuntimeError("Not connected")
592
+
593
+ queue = await self._quik_core.requestStream(request)
594
+
595
+ while True:
596
+ chunk = await queue.get()
597
+ if chunk is None or chunk.command == 'gn:end-stream':
598
+ break
599
+ yield chunk
600
+
601
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GNServer
3
- Version: 0.0.0.0.12
3
+ Version: 0.0.0.0.14
4
4
  Summary: GNServer
5
5
  Home-page: https://github.com/KeyisB/libs/tree/main/GNServer
6
6
  Author: KeyisB
@@ -0,0 +1,8 @@
1
+ GNServer/__init__.py,sha256=J4bjDqXVSEgOhD-FmkhpCeU4NJkp2BKMUKgltiuSXVo,1437
2
+ GNServer/_app.py,sha256=KlsB4dzCwGEgGUX_I-vHE9w1xzjCxj1p_YN5gBeI9ms,24300
3
+ GNServer/_client.py,sha256=P6RtHr4Ktap5FvpDeftrBG8V2fys3Ac1zIBbphsbOgA,24477
4
+ gnserver-0.0.0.0.14.dist-info/licenses/LICENSE,sha256=WH_t7dKZyWJ5Ld07eYIkUG4Tv6zZWXtAdsUqYAUesn0,1084
5
+ gnserver-0.0.0.0.14.dist-info/METADATA,sha256=oUAb9-MotUwASLmV51rlnZYpZwACyhlNXGlQoMWWcww,805
6
+ gnserver-0.0.0.0.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ gnserver-0.0.0.0.14.dist-info/top_level.txt,sha256=-UOUBuD4u7Qkb1o5PdcwyA3kx8xCH2lwy0tJHi26Wb4,9
8
+ gnserver-0.0.0.0.14.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- GNServer/__init__.py,sha256=GLKkKL03phIhxRLpl2X1ibwTw7BvnMMOwKgRD578RaU,1404
2
- GNServer/_app.py,sha256=t4h0RFttUAGufXm2moYsWLju52NnghisrK1d4RkSmHs,24029
3
- gnserver-0.0.0.0.12.dist-info/licenses/LICENSE,sha256=WH_t7dKZyWJ5Ld07eYIkUG4Tv6zZWXtAdsUqYAUesn0,1084
4
- gnserver-0.0.0.0.12.dist-info/METADATA,sha256=U17Yx7wt5IXf_sTaDuVLFXwcFHrHQdW2X_P7KpCfsU8,805
5
- gnserver-0.0.0.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- gnserver-0.0.0.0.12.dist-info/top_level.txt,sha256=-UOUBuD4u7Qkb1o5PdcwyA3kx8xCH2lwy0tJHi26Wb4,9
7
- gnserver-0.0.0.0.12.dist-info/RECORD,,