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 +1 -0
- GNServer/_app.py +27 -17
- GNServer/_client.py +601 -0
- {gnserver-0.0.0.0.12.dist-info → gnserver-0.0.0.0.14.dist-info}/METADATA +1 -1
- gnserver-0.0.0.0.14.dist-info/RECORD +8 -0
- gnserver-0.0.0.0.12.dist-info/RECORD +0 -7
- {gnserver-0.0.0.0.12.dist-info → gnserver-0.0.0.0.14.dist-info}/WHEEL +0 -0
- {gnserver-0.0.0.0.12.dist-info → gnserver-0.0.0.0.14.dist-info}/licenses/LICENSE +0 -0
- {gnserver-0.0.0.0.12.dist-info → gnserver-0.0.0.0.14.dist-info}/top_level.txt +0 -0
GNServer/__init__.py
CHANGED
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,
|
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
|
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
|
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,
|
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,
|
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(
|
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(
|
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(
|
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(
|
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
|
+
|
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|