KeyisBVMHost 0.0.0.0.2__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.
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost/__init__.py +40 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost/_app.py +390 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost/_client.py +600 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost.egg-info/PKG-INFO +31 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost.egg-info/SOURCES.txt +13 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost.egg-info/dependency_links.txt +1 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost.egg-info/requires.txt +5 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/KeyisBVMHost.egg-info/top_level.txt +1 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/LICENSE +28 -0
- keyisbvmhost-0.0.0.0.2/KeyisBVMHost/mmbConfig.json +9 -0
- keyisbvmhost-0.0.0.0.2/LICENSE +21 -0
- keyisbvmhost-0.0.0.0.2/MANIFEST.in +4 -0
- keyisbvmhost-0.0.0.0.2/PKG-INFO +31 -0
- keyisbvmhost-0.0.0.0.2/setup.cfg +4 -0
- keyisbvmhost-0.0.0.0.2/setup.py +25 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024 KeyisB. All rights reserved.
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person
|
|
5
|
+
obtaining a copy of this software and associated documentation
|
|
6
|
+
files (the "Software"), to use the Software exclusively for
|
|
7
|
+
projects related to the GN or GW systems, including personal,
|
|
8
|
+
educational, and commercial purposes, subject to the following
|
|
9
|
+
conditions:
|
|
10
|
+
|
|
11
|
+
1. Copying, modification, merging, publishing, distribution,
|
|
12
|
+
sublicensing, and/or selling copies of the Software are
|
|
13
|
+
strictly prohibited.
|
|
14
|
+
2. The licensee may use the Software only in its original,
|
|
15
|
+
unmodified form.
|
|
16
|
+
3. All copies or substantial portions of the Software must
|
|
17
|
+
remain unaltered and include this copyright notice and these terms of use.
|
|
18
|
+
4. Use of the Software for projects not related to GN or
|
|
19
|
+
GW systems is strictly prohibited.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
22
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
23
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
|
|
24
|
+
A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT
|
|
25
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
26
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
27
|
+
OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
28
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
29
|
+
DEALINGS IN THE SOFTWARE.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from ._app import App
|
|
33
|
+
from KeyisBClient.gn import GNRequest, GNResponse, CORSObject, FileObject, TemplateObject
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
from ._client import AsyncClient
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
from GNServer import App as _App, GNRequest, GNResponse, AsyncClient
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from typing import Iterable, Set, Tuple
|
|
11
|
+
import os
|
|
12
|
+
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from KeyisBClient import Url
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _kill_process_by_port(port: int):
|
|
20
|
+
|
|
21
|
+
def _run(cmd: list[str]) -> Tuple[int, str, str]:
|
|
22
|
+
try:
|
|
23
|
+
p = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
24
|
+
return p.returncode, p.stdout.strip(), p.stderr.strip()
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
return 127, "", f"{cmd[0]} not found"
|
|
27
|
+
|
|
28
|
+
def pids_from_fuser(port: int, proto: str) -> Set[int]:
|
|
29
|
+
# fuser понимает 59367/udp и 59367/tcp (оба стека)
|
|
30
|
+
rc, out, _ = _run(["fuser", f"{port}/{proto}"])
|
|
31
|
+
if rc != 0:
|
|
32
|
+
return set()
|
|
33
|
+
return {int(x) for x in re.findall(r"\b(\d+)\b", out)}
|
|
34
|
+
|
|
35
|
+
def pids_from_lsof(port: int, proto: str) -> Set[int]:
|
|
36
|
+
# lsof -ti UDP:59367 / lsof -ti TCP:59367
|
|
37
|
+
rc, out, _ = _run(["lsof", "-ti", f"{proto.upper()}:{port}"])
|
|
38
|
+
if rc != 0 or not out:
|
|
39
|
+
return set()
|
|
40
|
+
return {int(x) for x in out.splitlines() if x.isdigit()}
|
|
41
|
+
|
|
42
|
+
def pids_from_ss(port: int, proto: str) -> Set[int]:
|
|
43
|
+
# ss -H -uapn 'sport = :59367' (UDP) / ss -H -tapn ... (TCP)
|
|
44
|
+
flag = "-uapn" if proto == "udp" else "-tapn"
|
|
45
|
+
rc, out, _ = _run(["ss", "-H", flag, f"sport = :{port}"])
|
|
46
|
+
if rc != 0 or not out:
|
|
47
|
+
return set()
|
|
48
|
+
pids = set()
|
|
49
|
+
for line in out.splitlines():
|
|
50
|
+
# ... users:(("python3",pid=1234,fd=55))
|
|
51
|
+
for m in re.finditer(r"pid=(\d+)", line):
|
|
52
|
+
pids.add(int(m.group(1)))
|
|
53
|
+
return pids
|
|
54
|
+
|
|
55
|
+
def find_pids(port: int, proto: str | None) -> Set[int]:
|
|
56
|
+
protos: Iterable[str] = [proto] if proto in ("udp","tcp") else ("udp","tcp")
|
|
57
|
+
found: Set[int] = set()
|
|
58
|
+
for pr in protos:
|
|
59
|
+
# Порядок: fuser -> ss -> lsof (достаточно любого)
|
|
60
|
+
found |= pids_from_fuser(port, pr)
|
|
61
|
+
found |= pids_from_ss(port, pr)
|
|
62
|
+
found |= pids_from_lsof(port, pr)
|
|
63
|
+
# не убивать себя
|
|
64
|
+
found.discard(os.getpid())
|
|
65
|
+
return found
|
|
66
|
+
|
|
67
|
+
def kill_pids(pids: Set[int]) -> None:
|
|
68
|
+
if not pids:
|
|
69
|
+
return
|
|
70
|
+
me = os.getpid()
|
|
71
|
+
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
72
|
+
still = set()
|
|
73
|
+
for pid in pids:
|
|
74
|
+
if pid == me:
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
os.kill(pid, sig)
|
|
78
|
+
except ProcessLookupError:
|
|
79
|
+
continue
|
|
80
|
+
except PermissionError:
|
|
81
|
+
print(f"[WARN] No permission to signal {pid}")
|
|
82
|
+
still.add(pid)
|
|
83
|
+
continue
|
|
84
|
+
still.add(pid)
|
|
85
|
+
if not still:
|
|
86
|
+
return
|
|
87
|
+
# подождём чуть-чуть
|
|
88
|
+
for _ in range(10):
|
|
89
|
+
live = set()
|
|
90
|
+
for pid in still:
|
|
91
|
+
try:
|
|
92
|
+
os.kill(pid, 0)
|
|
93
|
+
live.add(pid)
|
|
94
|
+
except ProcessLookupError:
|
|
95
|
+
pass
|
|
96
|
+
still = live
|
|
97
|
+
if not still:
|
|
98
|
+
return
|
|
99
|
+
time.sleep(0.1)
|
|
100
|
+
|
|
101
|
+
def wait_port_free(port: int, proto: str | None, timeout: float = 3.0) -> bool:
|
|
102
|
+
t0 = time.time()
|
|
103
|
+
while time.time() - t0 < timeout:
|
|
104
|
+
if not find_pids(port, proto):
|
|
105
|
+
return True
|
|
106
|
+
time.sleep(0.1)
|
|
107
|
+
return not find_pids(port, proto)
|
|
108
|
+
|
|
109
|
+
for proto in ("udp", "tcp"):
|
|
110
|
+
pids = find_pids(port, proto)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
print(f"Гашу процессы на порту {port}: {sorted(pids)}")
|
|
114
|
+
kill_pids(pids)
|
|
115
|
+
|
|
116
|
+
if wait_port_free(port, proto):
|
|
117
|
+
print(f"Порт {port} освобождён.")
|
|
118
|
+
else:
|
|
119
|
+
print(f"[ERROR] Не удалось освободить порт {port}. Возможно, другой netns/служба перезапускает процесс.")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _sign(k:bytes)->bytes:nonce=os.urandom(16);m=b"keyisb-vm-host-"+os.urandom(32);return nonce+Cipher(algorithms.ChaCha20(k[:32],nonce),None).encryptor().update(m)
|
|
125
|
+
def _verify(k:bytes,s:bytes)->bool:nonce,ct=s[:16],s[16:];return Cipher(algorithms.ChaCha20(k[:32],nonce),None).decryptor().update(ct).startswith(b"keyisb-vm-host-")
|
|
126
|
+
|
|
127
|
+
class App():
|
|
128
|
+
def __init__(self):
|
|
129
|
+
self._app = _App()
|
|
130
|
+
|
|
131
|
+
self._servers_start_files = {}
|
|
132
|
+
|
|
133
|
+
self._access_key: Optional[str] = None
|
|
134
|
+
|
|
135
|
+
self._default_venv_path = None
|
|
136
|
+
|
|
137
|
+
self._client = AsyncClient()
|
|
138
|
+
|
|
139
|
+
self.__add_routes()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def setAccessKey(self, key: str):
|
|
144
|
+
self._access_key = key
|
|
145
|
+
|
|
146
|
+
def setVenvPath(self, venv_path: str):
|
|
147
|
+
self._default_venv_path = venv_path
|
|
148
|
+
|
|
149
|
+
def addServerStartFile(self, name: str, file_path: str, port: Optional[int] = None, start_when_run: bool = False, venv_path: Optional[str] = None):
|
|
150
|
+
self._servers_start_files[name] = {"name": name, "path": file_path, "port": port, "start_when_run": start_when_run, "venv_path": venv_path if venv_path is not None else self._default_venv_path}
|
|
151
|
+
|
|
152
|
+
async def startLikeRun(self):
|
|
153
|
+
for server in self._servers_start_files:
|
|
154
|
+
if self._servers_start_files[server]["start_when_run"]:
|
|
155
|
+
asyncio.create_task(self.startServer(server))
|
|
156
|
+
|
|
157
|
+
async def startServer(self, name: str, timeout: float = 10.0) -> bool:
|
|
158
|
+
if name in self._servers_start_files:
|
|
159
|
+
server = self._servers_start_files[name]
|
|
160
|
+
path = server["path"]
|
|
161
|
+
venv_path = server["venv_path"]
|
|
162
|
+
if not os.path.isfile(path):
|
|
163
|
+
raise ValueError(f"Server start file not found: {path}")
|
|
164
|
+
|
|
165
|
+
if path.endswith('.py'):
|
|
166
|
+
if venv_path is not None:
|
|
167
|
+
if not os.path.isdir(venv_path):
|
|
168
|
+
raise ValueError(f"Virtual environment path not found: {venv_path}")
|
|
169
|
+
python_executable = os.path.join(venv_path, 'bin', 'python')
|
|
170
|
+
if not os.path.isfile(python_executable):
|
|
171
|
+
raise ValueError(f"Python executable not found in virtual environment: {python_executable}")
|
|
172
|
+
else:
|
|
173
|
+
python_executable = sys.executable
|
|
174
|
+
|
|
175
|
+
subprocess.Popen([python_executable, path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
subprocess.Popen([path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
179
|
+
|
|
180
|
+
return await self.checkServerHealth(name, timeout=timeout)
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(f"No server start file found with name: {name}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def _send_message_to_server(self, name: str, path: str, payload: Optional[dict] = None, timeout: float = 1.0) -> Optional[GNResponse]:
|
|
187
|
+
port = self._servers_start_files[name].get("port")
|
|
188
|
+
if port is None:
|
|
189
|
+
raise ValueError(f"No port specified for server: {name}")
|
|
190
|
+
|
|
191
|
+
if path.startswith('/'):
|
|
192
|
+
path = path[1:]
|
|
193
|
+
|
|
194
|
+
c = self._client.request(GNRequest('POST', Url(f'gn://127.0.0.1:{port}/!gn-vm-host/{path}'), payload=payload))
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
result = await asyncio.wait_for(c, timeout=timeout)
|
|
198
|
+
except asyncio.TimeoutError:
|
|
199
|
+
result = None
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
async def checkServerHealth(self, name: str, timeout: float = 3.0, interval=0.5):
|
|
204
|
+
end = asyncio.get_event_loop().time() + timeout
|
|
205
|
+
while asyncio.get_event_loop().time() < end:
|
|
206
|
+
result = await self._send_message_to_server(name, '/ping', timeout=timeout)
|
|
207
|
+
if result is not None:
|
|
208
|
+
return True
|
|
209
|
+
else:
|
|
210
|
+
await asyncio.sleep(interval)
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def stopServer(self, name: str):
|
|
216
|
+
if name in self._servers_start_files:
|
|
217
|
+
server = self._servers_start_files[name]
|
|
218
|
+
port = server["port"]
|
|
219
|
+
if port is not None:
|
|
220
|
+
_kill_process_by_port(port)
|
|
221
|
+
else:
|
|
222
|
+
raise ValueError(f"No port specified for server: {name}")
|
|
223
|
+
else:
|
|
224
|
+
raise ValueError(f"No server start file found with name: {name}")
|
|
225
|
+
|
|
226
|
+
async def reloadServer(self, name: str, timeout: float = 1):
|
|
227
|
+
if name in self._servers_start_files:
|
|
228
|
+
self.stopServer(name)
|
|
229
|
+
await asyncio.sleep(timeout)
|
|
230
|
+
return await self.startServer(name)
|
|
231
|
+
else:
|
|
232
|
+
raise ValueError(f"No server start file found with name: {name}")
|
|
233
|
+
|
|
234
|
+
def run(self,
|
|
235
|
+
host,
|
|
236
|
+
port,
|
|
237
|
+
cert_path: str,
|
|
238
|
+
key_path: str,
|
|
239
|
+
*,
|
|
240
|
+
idle_timeout: float = 20.0,
|
|
241
|
+
wait: bool = True
|
|
242
|
+
):
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
asyncio.create_task(self.startLikeRun())
|
|
246
|
+
|
|
247
|
+
self._app.run(
|
|
248
|
+
host=host,
|
|
249
|
+
port=port,
|
|
250
|
+
cert_path=cert_path,
|
|
251
|
+
key_path=key_path,
|
|
252
|
+
idle_timeout=idle_timeout,
|
|
253
|
+
wait=wait
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def __resolve_access_key(self, request: GNRequest) -> bool:
|
|
258
|
+
if self._access_key is None:
|
|
259
|
+
raise ValueError("Access key is not set.")
|
|
260
|
+
|
|
261
|
+
sign = request.cookies.get('vm-host-sign')
|
|
262
|
+
|
|
263
|
+
if sign is None:
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
return _verify(self._access_key.encode(), sign)
|
|
267
|
+
|
|
268
|
+
def __add_routes(self):
|
|
269
|
+
@self._app.route('POST', '/ping')
|
|
270
|
+
async def ping_handler(request: GNRequest, name: Optional[str] = None, timeout: float = 3.0):
|
|
271
|
+
if not self.__resolve_access_key(request):
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
if not name:
|
|
275
|
+
return GNResponse('ok', {'time': datetime.datetime.now(datetime.timezone.utc).isoformat()})
|
|
276
|
+
else:
|
|
277
|
+
try:
|
|
278
|
+
result = await self.checkServerHealth(name, timeout=timeout)
|
|
279
|
+
if result:
|
|
280
|
+
return GNResponse('ok', {'message': f'Server {name} is alive.'})
|
|
281
|
+
else:
|
|
282
|
+
return GNResponse('error', {'error': f'Server {name} is not responding.'})
|
|
283
|
+
except ValueError as e:
|
|
284
|
+
return GNResponse('error', {'error': str(e)})
|
|
285
|
+
|
|
286
|
+
@self._app.route('GET', '/servers')
|
|
287
|
+
async def list_servers_handler(request: GNRequest):
|
|
288
|
+
if not self.__resolve_access_key(request):
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
servers_info = []
|
|
292
|
+
for server in self._servers_start_files.values():
|
|
293
|
+
servers_info.append({
|
|
294
|
+
'name': server['name'],
|
|
295
|
+
'port': server['port'],
|
|
296
|
+
'start_when_run': server['start_when_run']
|
|
297
|
+
})
|
|
298
|
+
return GNResponse('ok', {'servers': servers_info})
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@self._app.route('POST', '/start-server')
|
|
305
|
+
async def start_server_handler(request: GNRequest, name: str = ''):
|
|
306
|
+
if not self.__resolve_access_key(request):
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
if not name:
|
|
310
|
+
return GNResponse('error', {'error': 'Server name is required.'})
|
|
311
|
+
try:
|
|
312
|
+
result = await self.startServer(name)
|
|
313
|
+
if result:
|
|
314
|
+
return GNResponse('ok', {'message': f'Server {name} started.'})
|
|
315
|
+
else:
|
|
316
|
+
return GNResponse('error', {'error': f'Server {name} failed to start within the timeout period.'})
|
|
317
|
+
except ValueError as e:
|
|
318
|
+
return GNResponse('error', {'error': str(e)})
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@self._app.route('POST', '/reload-server')
|
|
323
|
+
async def reload_server_handler(request: GNRequest, name: str = '', timeout: float = 0.5):
|
|
324
|
+
if not self.__resolve_access_key(request):
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
if not name:
|
|
328
|
+
return GNResponse('error', {'error': 'Server name is required.'})
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
result = await self.reloadServer(name, timeout)
|
|
332
|
+
if result:
|
|
333
|
+
return GNResponse('ok', {'message': f'Server {name} reloaded.'})
|
|
334
|
+
else:
|
|
335
|
+
return GNResponse('error', {'error': f'Server {name} failed to reload within the timeout period.'})
|
|
336
|
+
except ValueError as e:
|
|
337
|
+
return GNResponse('error', {'error': str(e)})
|
|
338
|
+
|
|
339
|
+
@self._app.route('POST', '/stop-server')
|
|
340
|
+
async def stop_server_handler(request: GNRequest, name: str = ''):
|
|
341
|
+
if not self.__resolve_access_key(request):
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
if not name:
|
|
345
|
+
return GNResponse('error', {'error': 'Server name is required.'})
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
self.stopServer(name)
|
|
349
|
+
return GNResponse('ok', {'message': f'Server {name} stopped.'})
|
|
350
|
+
except ValueError as e:
|
|
351
|
+
return GNResponse('error', {'error': str(e)})
|
|
352
|
+
|
|
353
|
+
@self._app.route('POST', '/start-all-servers')
|
|
354
|
+
async def start_all_servers_handler(request: GNRequest):
|
|
355
|
+
if not self.__resolve_access_key(request):
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
for server in self._servers_start_files:
|
|
359
|
+
try:
|
|
360
|
+
result = await self.startServer(server)
|
|
361
|
+
if not result:
|
|
362
|
+
return GNResponse('error', {'error': f'Server {server} failed to start within the timeout period.'})
|
|
363
|
+
except ValueError as e:
|
|
364
|
+
return GNResponse('error', {'error': str(e)})
|
|
365
|
+
|
|
366
|
+
@self._app.route('POST', '/stop-all-servers')
|
|
367
|
+
async def stop_all_servers_handler(request: GNRequest):
|
|
368
|
+
if not self.__resolve_access_key(request):
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
for server in self._servers_start_files:
|
|
372
|
+
try:
|
|
373
|
+
self.stopServer(server)
|
|
374
|
+
except ValueError as e:
|
|
375
|
+
return GNResponse('error', {'error': str(e)})
|
|
376
|
+
|
|
377
|
+
return GNResponse('ok', {'message': 'All servers stopped.'})
|
|
378
|
+
|
|
379
|
+
@self._app.route('POST', '/reload-all-servers')
|
|
380
|
+
async def reload_all_servers_handler(request: GNRequest, timeout: float = 0.5):
|
|
381
|
+
if not self.__resolve_access_key(request):
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
for server in self._servers_start_files:
|
|
385
|
+
try:
|
|
386
|
+
result = await self.reloadServer(server, timeout)
|
|
387
|
+
if not result:
|
|
388
|
+
return GNResponse('error', {'error': f'Server {server} failed to reload within the timeout period.'})
|
|
389
|
+
except ValueError as e:
|
|
390
|
+
return GNResponse('error', {'error': str(e)})
|
|
@@ -0,0 +1,600 @@
|
|
|
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
|
+
# подключаемся к серверу gn-proxy
|
|
144
|
+
await self._client.connect(data['ip'], data['port'])
|
|
145
|
+
self._active_connections[domain] = 'active'
|
|
146
|
+
|
|
147
|
+
async def disconnect(self):
|
|
148
|
+
await self._client.disconnect()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _return_token(self, bigToken: str, s: bool = True) -> str:
|
|
152
|
+
return bigToken[:128] if s else bigToken[128:]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
|
|
156
|
+
"""
|
|
157
|
+
Build and send a async request.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
gnAsyncClient = KeyisBClient.AsyncClient()
|
|
161
|
+
async def func():
|
|
162
|
+
response = await gnAsyncClient.request(GNRequest('GET', Url('gn://example.com/example')))
|
|
163
|
+
command = response.command()
|
|
164
|
+
data = response.payload()
|
|
165
|
+
```
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if isinstance(request, GNRequest):
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if request.url.hostname not in self._active_connections:
|
|
177
|
+
await self.connect(request.url.hostname)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
for f in self.__request_callbacks.values():
|
|
182
|
+
asyncio.create_task(f(request))
|
|
183
|
+
|
|
184
|
+
r = await self._client.asyncRequest(request)
|
|
185
|
+
|
|
186
|
+
for f in self.__response_callbacks.values():
|
|
187
|
+
asyncio.create_task(f(r))
|
|
188
|
+
|
|
189
|
+
return r
|
|
190
|
+
|
|
191
|
+
# else:
|
|
192
|
+
# async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
|
|
193
|
+
# async for req in request:
|
|
194
|
+
# if req.gn_protocol is None:
|
|
195
|
+
# req.setGNProtocol(self.__current_session['protocols'][0])
|
|
196
|
+
# req._stream = True
|
|
197
|
+
|
|
198
|
+
# for f in self.__request_callbacks.values():
|
|
199
|
+
# asyncio.create_task(f(req))
|
|
200
|
+
|
|
201
|
+
# yield req
|
|
202
|
+
# r = await self.client.asyncRequest(wrapped(request))
|
|
203
|
+
|
|
204
|
+
# for f in self.__response_callbacks.values():
|
|
205
|
+
# asyncio.create_task(f(r))
|
|
206
|
+
|
|
207
|
+
# return r
|
|
208
|
+
|
|
209
|
+
async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
|
|
210
|
+
"""
|
|
211
|
+
Build and send a async request.
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(request, GNRequest):
|
|
214
|
+
if request.gn_protocol is None:
|
|
215
|
+
request.setGNProtocol(self.__current_session['protocols'][0])
|
|
216
|
+
|
|
217
|
+
for f in self.__request_callbacks.values():
|
|
218
|
+
asyncio.create_task(f(request))
|
|
219
|
+
|
|
220
|
+
async for response in self.client.asyncRequestStream(request):
|
|
221
|
+
|
|
222
|
+
for f in self.__response_callbacks.values():
|
|
223
|
+
asyncio.create_task(f(response))
|
|
224
|
+
|
|
225
|
+
yield response
|
|
226
|
+
else:
|
|
227
|
+
async def wrapped(request) -> AsyncGenerator[GNRequest, None]:
|
|
228
|
+
async for req in request:
|
|
229
|
+
if req.gn_protocol is None:
|
|
230
|
+
req.setGNProtocol(self.__current_session['protocols'][0])
|
|
231
|
+
|
|
232
|
+
for f in self.__request_callbacks.values():
|
|
233
|
+
asyncio.create_task(f(req))
|
|
234
|
+
|
|
235
|
+
req._stream = True
|
|
236
|
+
yield req
|
|
237
|
+
async for response in self.client.asyncRequestStream(wrapped(request)):
|
|
238
|
+
|
|
239
|
+
for f in self.__response_callbacks.values():
|
|
240
|
+
asyncio.create_task(f(response))
|
|
241
|
+
|
|
242
|
+
yield response
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# gn:quik
|
|
253
|
+
|
|
254
|
+
from aioquic.asyncio.protocol import QuicConnectionProtocol
|
|
255
|
+
from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
|
|
256
|
+
from aioquic.quic.connection import END_STATES
|
|
257
|
+
import asyncio
|
|
258
|
+
from collections import deque
|
|
259
|
+
from typing import Dict, Deque, Tuple, Optional, List
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Raw QUIC client with a dedicated SYS‑stream that consumes ~90 % CWND
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
# Основная идея:
|
|
265
|
+
# • Один постоянный bidirectional stream (sys_stream_id) используется для
|
|
266
|
+
# служебных сообщений.
|
|
267
|
+
# • Остальные запросы открываются в обычных потоках (user streams).
|
|
268
|
+
# • Отправка данных идёт через собственный scheduler: берём 9 «квантов» из
|
|
269
|
+
# SYS‑очереди и 1 квант из USER‑очереди, пока есть SYS‑данные.
|
|
270
|
+
# • Таким образом SYS‑канал получает ~90 % пропускной способности.
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
import asyncio
|
|
274
|
+
import time
|
|
275
|
+
from collections import deque
|
|
276
|
+
from dataclasses import dataclass
|
|
277
|
+
from itertools import count
|
|
278
|
+
from typing import Deque, Dict, Optional, Tuple, Union
|
|
279
|
+
|
|
280
|
+
from aioquic.quic.configuration import QuicConfiguration
|
|
281
|
+
from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class RawQuicClient(QuicConnectionProtocol):
|
|
285
|
+
"""Чистый‑QUIC клиент с приоритизированным SYS‑каналом + стриминг."""
|
|
286
|
+
|
|
287
|
+
SYS_RATIO_NUM = 9 # SYS 9/10
|
|
288
|
+
SYS_RATIO_DEN = 10
|
|
289
|
+
KEEPALIVE_INTERVAL = 10 # сек
|
|
290
|
+
KEEPALIVE_IDLE_TRIGGER = 30 # сек
|
|
291
|
+
|
|
292
|
+
# ────────────────────────────────────────────────────────────────── init ─┐
|
|
293
|
+
def __init__(self, *args, **kwargs):
|
|
294
|
+
super().__init__(*args, **kwargs)
|
|
295
|
+
|
|
296
|
+
self._sys_stream_id: Optional[int] = None
|
|
297
|
+
self._queue_sys: Deque[Tuple[int, bytes, bool]] = deque()
|
|
298
|
+
self._queue_user: Deque[Tuple[int, bytes, bool]] = deque()
|
|
299
|
+
|
|
300
|
+
# <‑‑ Future | Queue[bytes | None]
|
|
301
|
+
self._inflight: Dict[int, Union[asyncio.Future, asyncio.Queue[Optional[GNResponse]]]] = {}
|
|
302
|
+
self._inflight_streams: Dict[int, bytearray] = {}
|
|
303
|
+
self._sys_inflight: Dict[int, asyncio.Future] = {}
|
|
304
|
+
self._buffer: Dict[Union[int, str], bytearray] = {}
|
|
305
|
+
|
|
306
|
+
self._sys_budget = self.SYS_RATIO_NUM
|
|
307
|
+
self._sys_id_gen = count(1) # int64 message‑id generator
|
|
308
|
+
|
|
309
|
+
self._last_activity = time.time()
|
|
310
|
+
self._running = True
|
|
311
|
+
self._ping_id_gen = count(1) # int64 ping‑id generator
|
|
312
|
+
asyncio.create_task(self._keepalive_loop())
|
|
313
|
+
|
|
314
|
+
# ───────────────────────────────────────── private helpers ─┤
|
|
315
|
+
def _activity(self):
|
|
316
|
+
self._last_activity = time.time()
|
|
317
|
+
|
|
318
|
+
async def _keepalive_loop(self):
|
|
319
|
+
while self._running:
|
|
320
|
+
await asyncio.sleep(self.KEEPALIVE_INTERVAL)
|
|
321
|
+
idle_time = time.time() - self._last_activity
|
|
322
|
+
if idle_time > self.KEEPALIVE_IDLE_TRIGGER:
|
|
323
|
+
self._quic.send_ping(next(self._ping_id_gen))
|
|
324
|
+
self.transmit()
|
|
325
|
+
self._last_activity = time.time()
|
|
326
|
+
|
|
327
|
+
def stop(self):
|
|
328
|
+
self._running = False
|
|
329
|
+
|
|
330
|
+
# ───────────────────────────────────────────── events ─┤
|
|
331
|
+
def quic_event_received(self, event: QuicEvent) -> None: # noqa: C901
|
|
332
|
+
# ─── DATA ───────────────────────────────────────────
|
|
333
|
+
if isinstance(event, StreamDataReceived):
|
|
334
|
+
#print(event)
|
|
335
|
+
# SYS поток
|
|
336
|
+
if event.stream_id == self._sys_stream_id:
|
|
337
|
+
buf = self._buffer.setdefault("sys", bytearray())
|
|
338
|
+
buf.extend(event.data)
|
|
339
|
+
while True:
|
|
340
|
+
if len(buf) < 12:
|
|
341
|
+
break
|
|
342
|
+
msg_id = int.from_bytes(buf[:8], "little")
|
|
343
|
+
size = int.from_bytes(buf[8:12], "little")
|
|
344
|
+
if len(buf) < 12 + size:
|
|
345
|
+
break
|
|
346
|
+
payload = bytes(buf[12 : 12 + size])
|
|
347
|
+
del buf[: 12 + size]
|
|
348
|
+
fut = self._sys_inflight.pop(msg_id, None) if msg_id else None
|
|
349
|
+
if fut and not fut.done():
|
|
350
|
+
fut.set_result(payload)
|
|
351
|
+
# USER поток
|
|
352
|
+
else:
|
|
353
|
+
handler = self._inflight.get(event.stream_id)
|
|
354
|
+
if handler is None:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
# Чтение в зависимости от режима
|
|
358
|
+
if isinstance(handler, asyncio.Queue): # стрим от сервера
|
|
359
|
+
# получаем байты
|
|
360
|
+
|
|
361
|
+
buf = self._buffer.setdefault(event.stream_id, bytearray())
|
|
362
|
+
buf.extend(event.data)
|
|
363
|
+
|
|
364
|
+
if len(buf) < 8: # не дошел даже frame пакета
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
# получаем длинну пакета
|
|
368
|
+
mode, stream, lenght = GNResponse.type(buf)
|
|
369
|
+
|
|
370
|
+
if mode != 4: # не наш пакет
|
|
371
|
+
self._buffer.pop(event.stream_id)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
if not stream: # клиент просил стрим, а сервер прислал один пакет
|
|
375
|
+
self._buffer.pop(event.stream_id)
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# читаем пакет
|
|
379
|
+
if len(buf) < lenght: # если пакет не весь пришел, пропускаем
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
# пакет пришел весь
|
|
383
|
+
|
|
384
|
+
# берем пакет
|
|
385
|
+
data = buf[:lenght]
|
|
386
|
+
|
|
387
|
+
# удаляем его из буфера
|
|
388
|
+
del buf[:lenght]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
r = GNResponse.deserialize(data, 2)
|
|
392
|
+
handler.put_nowait(r)
|
|
393
|
+
if event.end_stream:
|
|
394
|
+
handler.put_nowait(None)
|
|
395
|
+
self._buffer.pop(event.stream_id)
|
|
396
|
+
self._inflight.pop(event.stream_id, None)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
else: # Future
|
|
401
|
+
buf = self._buffer.setdefault(event.stream_id, bytearray())
|
|
402
|
+
buf.extend(event.data)
|
|
403
|
+
if event.end_stream:
|
|
404
|
+
self._inflight.pop(event.stream_id, None)
|
|
405
|
+
data = bytes(self._buffer.pop(event.stream_id, b""))
|
|
406
|
+
if not handler.done():
|
|
407
|
+
handler.set_result(data)
|
|
408
|
+
|
|
409
|
+
# ─── RESET ──────────────────────────────────────────
|
|
410
|
+
elif isinstance(event, StreamReset):
|
|
411
|
+
handler = self._inflight.pop(event.stream_id, None) or self._sys_inflight.pop(
|
|
412
|
+
event.stream_id, None
|
|
413
|
+
)
|
|
414
|
+
if handler is None:
|
|
415
|
+
return
|
|
416
|
+
if isinstance(handler, asyncio.Queue):
|
|
417
|
+
handler.put_nowait(None)
|
|
418
|
+
else:
|
|
419
|
+
if not handler.done():
|
|
420
|
+
handler.set_exception(RuntimeError("stream reset"))
|
|
421
|
+
|
|
422
|
+
# ─────────────────────────────────────────── scheduler ─┤
|
|
423
|
+
def _enqueue(self, sid: int, blob: bytes, end_stream: bool, is_sys: bool):
|
|
424
|
+
(self._queue_sys if is_sys else self._queue_user).append((sid, blob, end_stream))
|
|
425
|
+
|
|
426
|
+
def _schedule_flush(self):
|
|
427
|
+
while (self._queue_sys or self._queue_user) and self._quic._close_event is None:
|
|
428
|
+
q = None
|
|
429
|
+
if self._queue_sys and (self._sys_budget > 0 or not self._queue_user):
|
|
430
|
+
q = self._queue_sys
|
|
431
|
+
self._sys_budget -= 1
|
|
432
|
+
elif self._queue_user:
|
|
433
|
+
q = self._queue_user
|
|
434
|
+
self._sys_budget = self.SYS_RATIO_NUM
|
|
435
|
+
if q is None:
|
|
436
|
+
break
|
|
437
|
+
sid, blob, end_stream = q.popleft()
|
|
438
|
+
self._quic.send_stream_data(sid, blob, end_stream=end_stream)
|
|
439
|
+
self.transmit()
|
|
440
|
+
self._activity()
|
|
441
|
+
|
|
442
|
+
# ─────────────────────────────────────────── public API ─┤
|
|
443
|
+
async def ensure_sys_stream(self):
|
|
444
|
+
if self._sys_stream_id is None:
|
|
445
|
+
self._sys_stream_id = self._quic.get_next_available_stream_id()
|
|
446
|
+
self._enqueue(self._sys_stream_id, b"", False, True) # dummy
|
|
447
|
+
self._schedule_flush()
|
|
448
|
+
|
|
449
|
+
async def send_sys(self, request: GNRequest, response: bool = False) -> Optional[bytes]:
|
|
450
|
+
await self.ensure_sys_stream()
|
|
451
|
+
if response:
|
|
452
|
+
msg_id = next(self._sys_id_gen)
|
|
453
|
+
blob = request.serialize(2)
|
|
454
|
+
payload = (
|
|
455
|
+
msg_id.to_bytes(8, "little") + len(blob).to_bytes(4, "little") + blob
|
|
456
|
+
)
|
|
457
|
+
fut = asyncio.get_running_loop().create_future()
|
|
458
|
+
self._sys_inflight[msg_id] = fut
|
|
459
|
+
self._enqueue(self._sys_stream_id, payload, False, True)
|
|
460
|
+
self._schedule_flush()
|
|
461
|
+
return await fut
|
|
462
|
+
payload = (0).to_bytes(8, "little") + request.serialize(2)
|
|
463
|
+
self._enqueue(self._sys_stream_id, payload, False, True)
|
|
464
|
+
self._schedule_flush()
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
async def request(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]):
|
|
468
|
+
if isinstance(request, GNRequest):
|
|
469
|
+
blob = request.serialize(2)
|
|
470
|
+
sid = self._quic.get_next_available_stream_id()
|
|
471
|
+
self._enqueue(sid, blob, True, False)
|
|
472
|
+
self._schedule_flush()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
fut = asyncio.get_running_loop().create_future()
|
|
476
|
+
self._inflight[sid] = fut
|
|
477
|
+
return await fut
|
|
478
|
+
|
|
479
|
+
else:
|
|
480
|
+
sid = self._quic.get_next_available_stream_id()
|
|
481
|
+
#if sid in self._quic._streams and not self._quic._streams[sid].is_finished:
|
|
482
|
+
|
|
483
|
+
async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
|
|
484
|
+
_last = None
|
|
485
|
+
async for req in request:
|
|
486
|
+
_last = req
|
|
487
|
+
blob = req.serialize(2)
|
|
488
|
+
self._enqueue(sid, blob, False, False)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
self._schedule_flush()
|
|
492
|
+
|
|
493
|
+
print(f'Отправлен stream запрос {req}')
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
_last.setPayload(None)
|
|
497
|
+
_last.setMethod('gn:end-stream')
|
|
498
|
+
blob = _last.serialize(2)
|
|
499
|
+
self._enqueue(sid, blob, True, False)
|
|
500
|
+
self._schedule_flush()
|
|
501
|
+
|
|
502
|
+
asyncio.create_task(_stream_sender(sid, request))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
fut = asyncio.get_running_loop().create_future()
|
|
506
|
+
self._inflight[sid] = fut
|
|
507
|
+
return await fut
|
|
508
|
+
|
|
509
|
+
async def requestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> asyncio.Queue[GNResponse]:
|
|
510
|
+
if isinstance(request, GNRequest):
|
|
511
|
+
blob = request.serialize(2)
|
|
512
|
+
sid = self._quic.get_next_available_stream_id()
|
|
513
|
+
self._enqueue(sid, blob, False, False)
|
|
514
|
+
self._schedule_flush()
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
q = asyncio.Queue()
|
|
518
|
+
self._inflight[sid] = q
|
|
519
|
+
return q
|
|
520
|
+
|
|
521
|
+
else:
|
|
522
|
+
sid = self._quic.get_next_available_stream_id()
|
|
523
|
+
|
|
524
|
+
async def _stream_sender(sid, request: AsyncGenerator[GNRequest, Any]):
|
|
525
|
+
_last = None
|
|
526
|
+
async for req in request:
|
|
527
|
+
_last = req
|
|
528
|
+
blob = req.serialize(2)
|
|
529
|
+
self._enqueue(sid, blob, False, False)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
self._schedule_flush()
|
|
533
|
+
|
|
534
|
+
print(f'Отправлен stream запрос {req}')
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
_last.setPayload(None)
|
|
538
|
+
_last.setMethod('gn:end-stream')
|
|
539
|
+
blob = _last.serialize(2)
|
|
540
|
+
self._enqueue(sid, blob, True, False)
|
|
541
|
+
self._schedule_flush()
|
|
542
|
+
|
|
543
|
+
asyncio.create_task(_stream_sender(sid, request))
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
q = asyncio.Queue()
|
|
547
|
+
self._inflight[sid] = q
|
|
548
|
+
return q
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class QuicClient:
|
|
553
|
+
"""Обёртка‑фасад над RawQuicClient."""
|
|
554
|
+
|
|
555
|
+
def __init__(self):
|
|
556
|
+
self._quik_core: Optional[RawQuicClient] = None
|
|
557
|
+
self._client_cm = None
|
|
558
|
+
|
|
559
|
+
async def connect(self, ip: str, port: int):
|
|
560
|
+
cfg = QuicConfiguration(is_client=True, alpn_protocols=["gn:backend"])
|
|
561
|
+
cfg.load_verify_locations(KeyisBClient.ssl_gw_crt_path)
|
|
562
|
+
|
|
563
|
+
self._client_cm = connect(
|
|
564
|
+
ip,
|
|
565
|
+
port,
|
|
566
|
+
configuration=cfg,
|
|
567
|
+
create_protocol=RawQuicClient,
|
|
568
|
+
wait_connected=True,
|
|
569
|
+
)
|
|
570
|
+
self._quik_core = await self._client_cm.__aenter__()
|
|
571
|
+
|
|
572
|
+
async def disconnect(self):
|
|
573
|
+
self._quik_core.close()
|
|
574
|
+
await self._quik_core.wait_closed()
|
|
575
|
+
self._quik_core = None
|
|
576
|
+
|
|
577
|
+
def syncRequest(self, request: GNRequest):
|
|
578
|
+
return asyncio.get_event_loop().run_until_complete(self.asyncRequest(request))
|
|
579
|
+
|
|
580
|
+
async def asyncRequest(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> GNResponse:
|
|
581
|
+
if self._quik_core is None:
|
|
582
|
+
raise RuntimeError("Not connected")
|
|
583
|
+
|
|
584
|
+
resp = await self._quik_core.request(request)
|
|
585
|
+
return GNResponse.deserialize(resp, 2)
|
|
586
|
+
|
|
587
|
+
async def asyncRequestStream(self, request: Union[GNRequest, AsyncGenerator[GNRequest, Any]]) -> AsyncGenerator[GNResponse, None]:
|
|
588
|
+
|
|
589
|
+
if self._quik_core is None:
|
|
590
|
+
raise RuntimeError("Not connected")
|
|
591
|
+
|
|
592
|
+
queue = await self._quik_core.requestStream(request)
|
|
593
|
+
|
|
594
|
+
while True:
|
|
595
|
+
chunk = await queue.get()
|
|
596
|
+
if chunk is None or chunk.command == 'gn:end-stream':
|
|
597
|
+
break
|
|
598
|
+
yield chunk
|
|
599
|
+
|
|
600
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: KeyisBVMHost
|
|
3
|
+
Version: 0.0.0.0.2
|
|
4
|
+
Summary: KeyisBVMHost
|
|
5
|
+
Home-page: https://github.com/KeyisB/libs/tree/main/KeyisBVMHost
|
|
6
|
+
Author: KeyisB
|
|
7
|
+
Author-email: keyisb.pip@gmail.com
|
|
8
|
+
License: MMB License v1.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/plain
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: aioquic
|
|
15
|
+
Requires-Dist: anyio
|
|
16
|
+
Requires-Dist: KeyisBClient
|
|
17
|
+
Requires-Dist: aiofiles
|
|
18
|
+
Requires-Dist: uvloop
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
GW and MMB Project libraries
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
setup.py
|
|
4
|
+
KeyisBVMHost/LICENSE
|
|
5
|
+
KeyisBVMHost/mmbConfig.json
|
|
6
|
+
KeyisBVMHost/KeyisBVMHost/__init__.py
|
|
7
|
+
KeyisBVMHost/KeyisBVMHost/_app.py
|
|
8
|
+
KeyisBVMHost/KeyisBVMHost/_client.py
|
|
9
|
+
KeyisBVMHost/KeyisBVMHost.egg-info/PKG-INFO
|
|
10
|
+
KeyisBVMHost/KeyisBVMHost.egg-info/SOURCES.txt
|
|
11
|
+
KeyisBVMHost/KeyisBVMHost.egg-info/dependency_links.txt
|
|
12
|
+
KeyisBVMHost/KeyisBVMHost.egg-info/requires.txt
|
|
13
|
+
KeyisBVMHost/KeyisBVMHost.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
KeyisBVMHost
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright (C) 2024 KeyisB. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
|
4
|
+
obtaining a copy of this software and associated
|
|
5
|
+
1. Copying, modification, merging, publishing, distribution,
|
|
6
|
+
sublicensing, and/or selling copies of the Software are
|
|
7
|
+
strictly prohibited.documentation
|
|
8
|
+
files (the "Software"), to use the Software exclusively for
|
|
9
|
+
projects related to the GN or GW systems, including personal,
|
|
10
|
+
educational, and commercial purposes, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
|
|
13
|
+
2. The licensee may use the Software only in its original,
|
|
14
|
+
unmodified form.
|
|
15
|
+
3. All copies or substantial portions of the Software must
|
|
16
|
+
remain unaltered and include this copyright notice and these terms of use.
|
|
17
|
+
4. Use of the Software for projects not related to GN or
|
|
18
|
+
GW systems is strictly prohibited.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
21
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
22
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
|
|
23
|
+
A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT
|
|
24
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
25
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
26
|
+
OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
27
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
28
|
+
DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 KeyisB
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: KeyisBVMHost
|
|
3
|
+
Version: 0.0.0.0.2
|
|
4
|
+
Summary: KeyisBVMHost
|
|
5
|
+
Home-page: https://github.com/KeyisB/libs/tree/main/KeyisBVMHost
|
|
6
|
+
Author: KeyisB
|
|
7
|
+
Author-email: keyisb.pip@gmail.com
|
|
8
|
+
License: MMB License v1.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/plain
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: aioquic
|
|
15
|
+
Requires-Dist: anyio
|
|
16
|
+
Requires-Dist: KeyisBClient
|
|
17
|
+
Requires-Dist: aiofiles
|
|
18
|
+
Requires-Dist: uvloop
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
GW and MMB Project libraries
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
name = 'KeyisBVMHost'
|
|
4
|
+
filesName = 'KeyisBVMHost'
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name=name,
|
|
8
|
+
version='0.0.0.0.2',
|
|
9
|
+
author="KeyisB",
|
|
10
|
+
author_email="keyisb.pip@gmail.com",
|
|
11
|
+
description=name,
|
|
12
|
+
long_description = 'GW and MMB Project libraries',
|
|
13
|
+
long_description_content_type= 'text/plain',
|
|
14
|
+
url=f"https://github.com/KeyisB/libs/tree/main/{name}",
|
|
15
|
+
include_package_data=True,
|
|
16
|
+
package_data = {}, # type: ignore
|
|
17
|
+
package_dir={'': f'{filesName}'.replace('-','_')},
|
|
18
|
+
classifiers=[
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
],
|
|
22
|
+
python_requires='>=3.12',
|
|
23
|
+
license="MMB License v1.0",
|
|
24
|
+
install_requires = ['aioquic', 'anyio', 'KeyisBClient', 'aiofiles', 'uvloop'],
|
|
25
|
+
)
|