GNServer 0.0.0.0.34__py3-none-any.whl → 0.0.0.0.36__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/_app.py +13 -290
- GNServer/_client.py +7 -4
- GNServer/_cors_resolver.py +129 -0
- GNServer/_func_params_validation.py +254 -0
- GNServer/_routes.py +107 -0
- GNServer/_template_resolver.py +48 -0
- {gnserver-0.0.0.0.34.dist-info → gnserver-0.0.0.0.36.dist-info}/METADATA +1 -1
- gnserver-0.0.0.0.36.dist-info/RECORD +13 -0
- gnserver-0.0.0.0.34.dist-info/RECORD +0 -9
- {gnserver-0.0.0.0.34.dist-info → gnserver-0.0.0.0.36.dist-info}/WHEEL +0 -0
- {gnserver-0.0.0.0.34.dist-info → gnserver-0.0.0.0.36.dist-info}/licenses/LICENSE +0 -0
- {gnserver-0.0.0.0.34.dist-info → gnserver-0.0.0.0.36.dist-info}/top_level.txt +0 -0
GNServer/_app.py
CHANGED
@@ -1,11 +1,8 @@
|
|
1
1
|
|
2
2
|
|
3
3
|
|
4
|
-
import re
|
5
4
|
import os
|
6
5
|
import sys
|
7
|
-
import uuid
|
8
|
-
import decimal
|
9
6
|
import asyncio
|
10
7
|
import inspect
|
11
8
|
import traceback
|
@@ -16,16 +13,17 @@ from aioquic.asyncio.server import serve
|
|
16
13
|
from aioquic.asyncio.protocol import QuicConnectionProtocol
|
17
14
|
from aioquic.quic.configuration import QuicConfiguration
|
18
15
|
from aioquic.quic.events import QuicEvent, StreamDataReceived
|
19
|
-
from typing import Any, AsyncGenerator, Union
|
16
|
+
from typing import Any, AsyncGenerator, Union
|
20
17
|
|
21
18
|
|
22
19
|
from gnobjects.net.objects import GNRequest, GNResponse, FileObject, CORSObject, TemplateObject
|
23
20
|
from gnobjects.net.fastcommands import AllGNFastCommands, GNFastCommand
|
24
21
|
|
25
22
|
|
23
|
+
from ._func_params_validation import register_schema_by_key, validate_params_by_key
|
24
|
+
from ._cors_resolver import resolve_cors
|
26
25
|
|
27
|
-
|
28
|
-
|
26
|
+
from ._routes import Route, _compile_path, _ensure_async, _convert_value
|
29
27
|
|
30
28
|
|
31
29
|
|
@@ -112,281 +110,11 @@ def guess_type(filename: str) -> str:
|
|
112
110
|
return mime_map.get(ext, "application/octet-stream")
|
113
111
|
|
114
112
|
|
115
|
-
import re
|
116
|
-
from typing import List
|
117
|
-
|
118
|
-
# regex для !{var}, поддерживает вложенность через точку
|
119
|
-
TPL_VAR_RE = re.compile(r'(?<!\\)!\{([A-Za-z_][A-Za-z0-9_\.]*)\}')
|
120
|
-
|
121
|
-
# список mime, которые считаем текстовыми
|
122
|
-
TEXTUAL_MIME_PREFIXES = [
|
123
|
-
"text/", # text/html, text/css, text/plain
|
124
|
-
]
|
125
|
-
TEXTUAL_MIME_EXACT = {
|
126
|
-
"application/javascript",
|
127
|
-
"application/json",
|
128
|
-
"application/xml",
|
129
|
-
"application/xhtml+xml"
|
130
|
-
}
|
131
|
-
TEXTUAL_MIME_SUFFIXES = (
|
132
|
-
"+xml", # например application/rss+xml
|
133
|
-
"+json", # application/ld+json
|
134
|
-
)
|
135
|
-
|
136
|
-
def extract_template_vars(filedata: bytes, mime: str) -> List[str]:
|
137
|
-
"""
|
138
|
-
Ищет все !{var} в тексте, если MIME относится к текстовым.
|
139
|
-
"""
|
140
|
-
mime = (mime or "").lower().strip()
|
141
|
-
|
142
|
-
# определяем, текстовый ли mime
|
143
|
-
is_textual = (
|
144
|
-
mime.startswith(tuple(TEXTUAL_MIME_PREFIXES))
|
145
|
-
or mime in TEXTUAL_MIME_EXACT
|
146
|
-
or mime.endswith(TEXTUAL_MIME_SUFFIXES)
|
147
|
-
or "javascript" in mime
|
148
|
-
or "json" in mime
|
149
|
-
or "xml" in mime
|
150
|
-
)
|
151
|
-
|
152
|
-
if not is_textual:
|
153
|
-
return []
|
154
|
-
|
155
|
-
try:
|
156
|
-
text = filedata.decode("utf-8", errors="ignore")
|
157
|
-
except Exception:
|
158
|
-
return []
|
159
|
-
|
160
|
-
return list(set(m.group(1) for m in TPL_VAR_RE.finditer(text)))
|
161
|
-
|
162
|
-
|
163
|
-
import re
|
164
|
-
from urllib.parse import urlparse
|
165
|
-
|
166
|
-
def resolve_cors(origin_url: str, rules: list[str]) -> bool:
|
167
|
-
"""
|
168
|
-
Возвращает origin_url если он матчится хотя бы с одним правилом.
|
169
|
-
Правила:
|
170
|
-
- "*.example.com" -> wildcard (одна метка)
|
171
|
-
- "**.example.com" -> globstar (0+ меток)
|
172
|
-
- "pages.*.core.gn" -> смешанное
|
173
|
-
- "gn://*.site.tld" -> с проверкой схемы
|
174
|
-
- "!<regex>" -> полное соответствие по regex к origin_url
|
175
|
-
"""
|
176
|
-
|
177
|
-
if origin_url == 'gn:proxy:sys':
|
178
|
-
return True
|
179
|
-
|
180
|
-
|
181
|
-
|
182
113
|
|
183
114
|
|
184
|
-
origin = origin_url.rstrip("/")
|
185
|
-
pu = urlparse(origin)
|
186
|
-
scheme = (pu.scheme or "").lower()
|
187
|
-
host = (pu.hostname or "").lower()
|
188
|
-
port = pu.port # может быть None
|
189
115
|
|
190
|
-
if not host:
|
191
|
-
return False
|
192
|
-
|
193
|
-
for rule in rules:
|
194
|
-
rule = rule.rstrip("/")
|
195
|
-
|
196
|
-
# 1) Регекс-правило
|
197
|
-
if rule.startswith("!"):
|
198
|
-
pattern = rule[1:]
|
199
|
-
if re.fullmatch(pattern, origin):
|
200
|
-
return True
|
201
|
-
continue
|
202
|
-
|
203
|
-
# 2) Разбор схемы/хоста в правиле
|
204
|
-
r_scheme = ""
|
205
|
-
r_host = ""
|
206
|
-
r_port = None
|
207
|
-
|
208
|
-
if "://" in rule:
|
209
|
-
pr = urlparse(rule)
|
210
|
-
r_scheme = (pr.scheme or "").lower()
|
211
|
-
# pr.netloc может содержать порт
|
212
|
-
netloc = pr.netloc.lower()
|
213
|
-
# разберём порт, если есть
|
214
|
-
if ":" in netloc and not netloc.endswith("]"): # простая обработка IPv6 не требуется здесь
|
215
|
-
name, _, p = netloc.rpartition(":")
|
216
|
-
r_host = name
|
217
|
-
try:
|
218
|
-
r_port = int(p)
|
219
|
-
except ValueError:
|
220
|
-
r_port = None
|
221
|
-
else:
|
222
|
-
r_host = netloc
|
223
|
-
else:
|
224
|
-
r_host = rule.lower()
|
225
116
|
|
226
|
-
# схема в правиле задана -> должна совпасть
|
227
|
-
if r_scheme and r_scheme != scheme:
|
228
|
-
continue
|
229
|
-
# порт в правиле задан -> должен совпасть
|
230
|
-
if r_port is not None and r_port != port:
|
231
|
-
continue
|
232
117
|
|
233
|
-
# 3) Сопоставление хоста по шаблону с * и ** (по меткам)
|
234
|
-
if _host_matches_pattern(host, r_host):
|
235
|
-
return True
|
236
|
-
|
237
|
-
return False
|
238
|
-
|
239
|
-
|
240
|
-
def _host_matches_pattern(host: str, pattern: str) -> bool:
|
241
|
-
"""
|
242
|
-
Матчит host против pattern по доменным меткам:
|
243
|
-
- '*' -> ровно одна метка
|
244
|
-
- '**' -> ноль или больше меток
|
245
|
-
Остальные метки — точное совпадение (без внутр. вайлдкардов).
|
246
|
-
Примеры:
|
247
|
-
host=pages.static.core.gn, pattern=**.core.gn -> True
|
248
|
-
host=pages.static.core.gn, pattern=pages.*.core.gn -> True
|
249
|
-
host=pages.static.core.gn, pattern=*.gn.gn -> False
|
250
|
-
host=abc.def.example.com, pattern=*.example.com -> False (нужно **.example.com)
|
251
|
-
host=abc.example.com, pattern=*.example.com -> True
|
252
|
-
"""
|
253
|
-
host_labels = host.split(".")
|
254
|
-
pat_labels = pattern.split(".")
|
255
|
-
|
256
|
-
# быстрый путь: точное совпадение без вайлдкардов
|
257
|
-
if "*" not in pattern:
|
258
|
-
return host == pattern
|
259
|
-
|
260
|
-
# рекурсивный матч с поддержкой ** (globstar)
|
261
|
-
def match(hi: int, pi: int) -> bool:
|
262
|
-
# оба дошли до конца
|
263
|
-
if pi == len(pat_labels) and hi == len(host_labels):
|
264
|
-
return True
|
265
|
-
# закончился паттерн — нет
|
266
|
-
if pi == len(pat_labels):
|
267
|
-
return False
|
268
|
-
|
269
|
-
token = pat_labels[pi]
|
270
|
-
if token == "**":
|
271
|
-
# два варианта:
|
272
|
-
# - пропустить '**' (ноль меток)
|
273
|
-
if match(hi, pi + 1):
|
274
|
-
return True
|
275
|
-
# - съесть одну метку (если есть) и остаться на '**'
|
276
|
-
if hi < len(host_labels) and match(hi + 1, pi):
|
277
|
-
return True
|
278
|
-
return False
|
279
|
-
elif token == "*":
|
280
|
-
# нужно съесть ровно одну метку
|
281
|
-
if hi < len(host_labels):
|
282
|
-
return match(hi + 1, pi + 1)
|
283
|
-
return False
|
284
|
-
else:
|
285
|
-
# точное совпадение метки
|
286
|
-
if hi < len(host_labels) and host_labels[hi] == token:
|
287
|
-
return match(hi + 1, pi + 1)
|
288
|
-
return False
|
289
|
-
|
290
|
-
return match(0, 0)
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
@dataclass
|
296
|
-
class Route:
|
297
|
-
method: str
|
298
|
-
path_expr: str
|
299
|
-
regex: Pattern[str]
|
300
|
-
param_types: dict[str, Callable[[str], Any]]
|
301
|
-
handler: Callable[..., Any]
|
302
|
-
name: str
|
303
|
-
cors: Optional[CORSObject]
|
304
|
-
|
305
|
-
_PARAM_REGEX: dict[str, str] = {
|
306
|
-
"str": r"[^/]+",
|
307
|
-
"path": r".+",
|
308
|
-
"int": r"\d+",
|
309
|
-
"float": r"[+-]?\d+(?:\.\d+)?",
|
310
|
-
"bool": r"(?:true|false|1|0)",
|
311
|
-
"uuid": r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-"
|
312
|
-
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
313
|
-
r"[0-9a-fA-F]{12}",
|
314
|
-
"datetime": r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?",
|
315
|
-
"date": r"\d{4}-\d{2}-\d{2}",
|
316
|
-
"time": r"\d{2}:\d{2}:\d{2}(?:\.\d+)?",
|
317
|
-
"decimal": r"[+-]?\d+(?:\.\d+)?",
|
318
|
-
}
|
319
|
-
|
320
|
-
_CONVERTER_FUNC: dict[str, Callable[[str], Any]] = {
|
321
|
-
"int": int,
|
322
|
-
"float": float,
|
323
|
-
"bool": lambda s: s.lower() in {"1","true","yes","on"},
|
324
|
-
"uuid": uuid.UUID,
|
325
|
-
"decimal": decimal.Decimal,
|
326
|
-
"datetime": datetime.datetime.fromisoformat,
|
327
|
-
"date": datetime.date.fromisoformat,
|
328
|
-
"time": datetime.time.fromisoformat,
|
329
|
-
}
|
330
|
-
|
331
|
-
def _compile_path(path: str) -> tuple[Pattern[str], dict[str, Callable[[str], Any]]]:
|
332
|
-
param_types: dict[str, Callable[[str], Any]] = {}
|
333
|
-
rx_parts: list[str] = ["^"]
|
334
|
-
i = 0
|
335
|
-
while i < len(path):
|
336
|
-
if path[i] != "{":
|
337
|
-
rx_parts.append(re.escape(path[i]))
|
338
|
-
i += 1
|
339
|
-
continue
|
340
|
-
j = path.index("}", i)
|
341
|
-
spec = path[i+1:j]
|
342
|
-
i = j + 1
|
343
|
-
|
344
|
-
if ":" in spec:
|
345
|
-
name, conv = spec.split(":", 1)
|
346
|
-
else:
|
347
|
-
name, conv = spec, "str"
|
348
|
-
|
349
|
-
if conv.startswith("^"):
|
350
|
-
rx = f"(?P<{name}>{conv})"
|
351
|
-
typ = str
|
352
|
-
else:
|
353
|
-
rx = f"(?P<{name}>{_PARAM_REGEX.get(conv, _PARAM_REGEX['str'])})"
|
354
|
-
typ = _CONVERTER_FUNC.get(conv, str)
|
355
|
-
|
356
|
-
rx_parts.append(rx)
|
357
|
-
param_types[name] = typ
|
358
|
-
|
359
|
-
rx_parts.append("$")
|
360
|
-
return re.compile("".join(rx_parts)), param_types
|
361
|
-
|
362
|
-
def _convert_value(raw: str | list[str], ann: Any, fallback: Callable[[str], Any]) -> Any:
|
363
|
-
origin = get_origin(ann)
|
364
|
-
args = get_args(ann)
|
365
|
-
|
366
|
-
if isinstance(raw, list) or origin is list:
|
367
|
-
subtype = args[0] if (origin is list and args) else str
|
368
|
-
if not isinstance(raw, list):
|
369
|
-
raw = [raw]
|
370
|
-
return [_convert_value(r, subtype, fallback) for r in raw]
|
371
|
-
|
372
|
-
# --- fix Union ---
|
373
|
-
if origin is Union:
|
374
|
-
for subtype in args:
|
375
|
-
try:
|
376
|
-
return _convert_value(raw, subtype, fallback)
|
377
|
-
except Exception:
|
378
|
-
continue
|
379
|
-
return raw # если ни один тип не подошёл
|
380
|
-
|
381
|
-
conv = _CONVERTER_FUNC.get(ann, ann) if ann is not inspect._empty else fallback
|
382
|
-
return conv(raw) if callable(conv) else raw
|
383
|
-
|
384
|
-
def _ensure_async(fn: Callable[..., Any]) -> Callable[..., Any]:
|
385
|
-
if inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn):
|
386
|
-
return fn
|
387
|
-
async def wrapper(*args, **kw):
|
388
|
-
return fn(*args, **kw)
|
389
|
-
return wrapper
|
390
118
|
|
391
119
|
class App:
|
392
120
|
def __init__(self):
|
@@ -412,6 +140,7 @@ class App:
|
|
412
140
|
cors
|
413
141
|
)
|
414
142
|
)
|
143
|
+
register_schema_by_key(fn)
|
415
144
|
return fn
|
416
145
|
return decorator
|
417
146
|
|
@@ -490,7 +219,7 @@ class App:
|
|
490
219
|
return GNResponse("gn:backend:801", {'error': 'Cors error. Route has cors but request has no origin url.'})
|
491
220
|
if not resolve_cors(request._origin, r.cors.allow_origins):
|
492
221
|
return GNResponse("gn:backend:802", {'error': 'Cors error: origin'})
|
493
|
-
if request.method not in r.cors.allow_methods and '*' not in r.cors.allow_methods:
|
222
|
+
if r.cors.allow_methods is not None and request.method not in r.cors.allow_methods and '*' not in r.cors.allow_methods:
|
494
223
|
return GNResponse("gn:backend:803", {'error': 'Cors error: method'})
|
495
224
|
|
496
225
|
sig = inspect.signature(r.handler)
|
@@ -515,6 +244,12 @@ class App:
|
|
515
244
|
params = set(sig.parameters.keys())
|
516
245
|
kw = {k: v for k, v in kw.items() if k in params}
|
517
246
|
|
247
|
+
|
248
|
+
rv = validate_params_by_key(kw, r.handler)
|
249
|
+
if rv is not None:
|
250
|
+
AllGNFastCommands.UnprocessableEntity({'dev_error': rv, 'user_error': f'Server request error {self.domain}'})
|
251
|
+
|
252
|
+
|
518
253
|
if inspect.isasyncgenfunction(r.handler):
|
519
254
|
return r.handler(**kw)
|
520
255
|
|
@@ -532,7 +267,7 @@ class App:
|
|
532
267
|
return GNResponse("gn:backend:801", {'error': 'Cors error. Route has cors but request has no origin url. [2]'})
|
533
268
|
if not resolve_cors(request._origin, result._cors.allow_origins):
|
534
269
|
return GNResponse("gn:backend:802", {'error': 'Cors error: origin'})
|
535
|
-
if request.method not in result._cors.allow_methods and '*' not in result._cors.allow_methods:
|
270
|
+
if result._cors.allow_methods is not None and request.method not in result._cors.allow_methods and '*' not in result._cors.allow_methods:
|
536
271
|
return GNResponse("gn:backend:803", {'error': 'Cors error: method'})
|
537
272
|
return result
|
538
273
|
else:
|
@@ -579,7 +314,6 @@ class App:
|
|
579
314
|
def _init_sys_routes(self):
|
580
315
|
@self.post('/!gn-vm-host/ping', cors=CORSObject())
|
581
316
|
async def r_ping(request: GNRequest):
|
582
|
-
|
583
317
|
if request.client.ip != '127.0.0.1':
|
584
318
|
raise AllGNFastCommands.Forbidden()
|
585
319
|
return GNResponse('ok', {'time': datetime.datetime.now(datetime.UTC).isoformat()})
|
@@ -615,11 +349,6 @@ class App:
|
|
615
349
|
if not stream: # если не стрим, то ждем конец quic стрима и запускаем обработку ответа
|
616
350
|
if event.end_stream:
|
617
351
|
request = GNRequest.deserialize(buf, mode)
|
618
|
-
# request.stream_id = event.stream_id
|
619
|
-
# loop = asyncio.get_event_loop()
|
620
|
-
# request.fut = loop.create_future()
|
621
|
-
|
622
|
-
|
623
352
|
request.stream_id = event.stream_id # type: ignore
|
624
353
|
asyncio.create_task(self._handle_request(request, mode))
|
625
354
|
logger.debug(f'Отправлена задача разрешения пакета {request} route -> {request.route}')
|
@@ -660,9 +389,6 @@ class App:
|
|
660
389
|
logger.debug(f'Закрываем стрим [{event.stream_id}]')
|
661
390
|
return
|
662
391
|
|
663
|
-
|
664
|
-
|
665
|
-
|
666
392
|
queue.put_nowait(request)
|
667
393
|
|
668
394
|
# отдаем очередь в интерфейс
|
@@ -684,7 +410,6 @@ class App:
|
|
684
410
|
request.client._data['remote_addr'] = self._quic._network_paths[0].addr
|
685
411
|
|
686
412
|
try:
|
687
|
-
|
688
413
|
response = await self._api.dispatchRequest(request)
|
689
414
|
|
690
415
|
if inspect.isasyncgen(response):
|
@@ -764,8 +489,6 @@ class App:
|
|
764
489
|
if run is not None:
|
765
490
|
await run()
|
766
491
|
|
767
|
-
|
768
|
-
|
769
492
|
|
770
493
|
if wait:
|
771
494
|
await asyncio.Event().wait()
|
GNServer/_client.py
CHANGED
@@ -76,15 +76,15 @@ class GNExceptions:
|
|
76
76
|
|
77
77
|
|
78
78
|
from KeyisBTools import TTLDict
|
79
|
-
from KeyisBTools.cryptography.sign import
|
80
|
-
from KeyisBTools.cryptography import m1
|
79
|
+
from KeyisBTools.cryptography.sign import s2
|
80
|
+
from KeyisBTools.cryptography import m1
|
81
|
+
from KeyisBTools.cryptography.bytes import hash
|
81
82
|
from KeyisBTools.models.serialization import serialize, deserialize
|
82
83
|
from gnobjects.net.objects import GNRequest, GNResponse, Url
|
83
84
|
|
84
85
|
from ._crt import crt_client
|
85
86
|
|
86
87
|
|
87
|
-
s1 = S1()
|
88
88
|
|
89
89
|
|
90
90
|
async def chain_async(first_item, rest: AsyncIterable) -> AsyncGenerator:
|
@@ -124,6 +124,9 @@ class AsyncClient:
|
|
124
124
|
resuilt = self._dns_cache.get(domain)
|
125
125
|
if resuilt is not None:
|
126
126
|
return resuilt
|
127
|
+
|
128
|
+
if domain == 'api.dns.core':
|
129
|
+
return self.__dns_core__ipv4
|
127
130
|
|
128
131
|
if ':' in domain and domain.split('.')[-1].split(':')[0].isdigit() and domain.split(':')[-1].isdigit():
|
129
132
|
return domain
|
@@ -133,7 +136,7 @@ class AsyncClient:
|
|
133
136
|
self.__dns_client = AsyncClient()
|
134
137
|
|
135
138
|
if self.__server_key is not None:
|
136
|
-
s =
|
139
|
+
s = s2.sign(self.__server_key)
|
137
140
|
data = m1.encrypt(s, domain.encode(), serialize({'domain': domain}), hash(self.__server_key))
|
138
141
|
payload = {'sign': {'alg': 'KeyisB-c-s-m1', 'data': s}, 'data': data}
|
139
142
|
else:
|
@@ -0,0 +1,129 @@
|
|
1
|
+
|
2
|
+
import re
|
3
|
+
from urllib.parse import urlparse
|
4
|
+
|
5
|
+
def resolve_cors(origin_url: str, rules: list[str]) -> bool:
|
6
|
+
"""
|
7
|
+
Возвращает origin_url если он матчится хотя бы с одним правилом.
|
8
|
+
Правила:
|
9
|
+
- "*.example.com" -> wildcard (одна метка)
|
10
|
+
- "**.example.com" -> globstar (0+ меток)
|
11
|
+
- "pages.*.core.gn" -> смешанное
|
12
|
+
- "gn://*.site.tld" -> с проверкой схемы
|
13
|
+
- "!<regex>" -> полное соответствие по regex к origin_url
|
14
|
+
"""
|
15
|
+
|
16
|
+
if origin_url == 'gn:proxy:sys':
|
17
|
+
return True
|
18
|
+
|
19
|
+
if rules in ('*', ['*']):
|
20
|
+
return True
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
origin = origin_url.rstrip("/")
|
25
|
+
pu = urlparse(origin)
|
26
|
+
scheme = (pu.scheme or "").lower()
|
27
|
+
host = (pu.hostname or "").lower()
|
28
|
+
port = pu.port # может быть None
|
29
|
+
|
30
|
+
if not host:
|
31
|
+
return False
|
32
|
+
|
33
|
+
for rule in rules:
|
34
|
+
rule = rule.rstrip("/")
|
35
|
+
|
36
|
+
# 1) Регекс-правило
|
37
|
+
if rule.startswith("!"):
|
38
|
+
pattern = rule[1:]
|
39
|
+
if re.fullmatch(pattern, origin):
|
40
|
+
return True
|
41
|
+
continue
|
42
|
+
|
43
|
+
# 2) Разбор схемы/хоста в правиле
|
44
|
+
r_scheme = ""
|
45
|
+
r_host = ""
|
46
|
+
r_port = None
|
47
|
+
|
48
|
+
if "://" in rule:
|
49
|
+
pr = urlparse(rule)
|
50
|
+
r_scheme = (pr.scheme or "").lower()
|
51
|
+
# pr.netloc может содержать порт
|
52
|
+
netloc = pr.netloc.lower()
|
53
|
+
# разберём порт, если есть
|
54
|
+
if ":" in netloc and not netloc.endswith("]"): # простая обработка IPv6 не требуется здесь
|
55
|
+
name, _, p = netloc.rpartition(":")
|
56
|
+
r_host = name
|
57
|
+
try:
|
58
|
+
r_port = int(p)
|
59
|
+
except ValueError:
|
60
|
+
r_port = None
|
61
|
+
else:
|
62
|
+
r_host = netloc
|
63
|
+
else:
|
64
|
+
r_host = rule.lower()
|
65
|
+
|
66
|
+
# схема в правиле задана -> должна совпасть
|
67
|
+
if r_scheme and r_scheme != scheme:
|
68
|
+
continue
|
69
|
+
# порт в правиле задан -> должен совпасть
|
70
|
+
if r_port is not None and r_port != port:
|
71
|
+
continue
|
72
|
+
|
73
|
+
# 3) Сопоставление хоста по шаблону с * и ** (по меткам)
|
74
|
+
if _host_matches_pattern(host, r_host):
|
75
|
+
return True
|
76
|
+
|
77
|
+
return False
|
78
|
+
|
79
|
+
def _host_matches_pattern(host: str, pattern: str) -> bool:
|
80
|
+
"""
|
81
|
+
Матчит host против pattern по доменным меткам:
|
82
|
+
- '*' -> ровно одна метка
|
83
|
+
- '**' -> ноль или больше меток
|
84
|
+
Остальные метки — точное совпадение (без внутр. вайлдкардов).
|
85
|
+
Примеры:
|
86
|
+
host=pages.static.core.gn, pattern=**.core.gn -> True
|
87
|
+
host=pages.static.core.gn, pattern=pages.*.core.gn -> True
|
88
|
+
host=pages.static.core.gn, pattern=*.gn.gn -> False
|
89
|
+
host=abc.def.example.com, pattern=*.example.com -> False (нужно **.example.com)
|
90
|
+
host=abc.example.com, pattern=*.example.com -> True
|
91
|
+
"""
|
92
|
+
host_labels = host.split(".")
|
93
|
+
pat_labels = pattern.split(".")
|
94
|
+
|
95
|
+
# быстрый путь: точное совпадение без вайлдкардов
|
96
|
+
if "*" not in pattern:
|
97
|
+
return host == pattern
|
98
|
+
|
99
|
+
# рекурсивный матч с поддержкой ** (globstar)
|
100
|
+
def match(hi: int, pi: int) -> bool:
|
101
|
+
# оба дошли до конца
|
102
|
+
if pi == len(pat_labels) and hi == len(host_labels):
|
103
|
+
return True
|
104
|
+
# закончился паттерн — нет
|
105
|
+
if pi == len(pat_labels):
|
106
|
+
return False
|
107
|
+
|
108
|
+
token = pat_labels[pi]
|
109
|
+
if token == "**":
|
110
|
+
# два варианта:
|
111
|
+
# - пропустить '**' (ноль меток)
|
112
|
+
if match(hi, pi + 1):
|
113
|
+
return True
|
114
|
+
# - съесть одну метку (если есть) и остаться на '**'
|
115
|
+
if hi < len(host_labels) and match(hi + 1, pi):
|
116
|
+
return True
|
117
|
+
return False
|
118
|
+
elif token == "*":
|
119
|
+
# нужно съесть ровно одну метку
|
120
|
+
if hi < len(host_labels):
|
121
|
+
return match(hi + 1, pi + 1)
|
122
|
+
return False
|
123
|
+
else:
|
124
|
+
# точное совпадение метки
|
125
|
+
if hi < len(host_labels) and host_labels[hi] == token:
|
126
|
+
return match(hi + 1, pi + 1)
|
127
|
+
return False
|
128
|
+
|
129
|
+
return match(0, 0)
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# fast_validate.py
|
2
|
+
import inspect, datetime, functools, threading
|
3
|
+
from inspect import Parameter, _empty
|
4
|
+
from typing import Any, Union, get_origin, get_args
|
5
|
+
|
6
|
+
_NoneType = type(None)
|
7
|
+
_KEYTYPES = (str, bytes, int)
|
8
|
+
|
9
|
+
def _is_int_not_bool(v): return isinstance(v, int) and not isinstance(v, bool)
|
10
|
+
|
11
|
+
def _serializable_ok(v) -> bool:
|
12
|
+
if v is None or isinstance(v, (bool, float, str, bytes, datetime.datetime, datetime.time)):
|
13
|
+
return True
|
14
|
+
if _is_int_not_bool(v): return True
|
15
|
+
t = type(v)
|
16
|
+
if t is list:
|
17
|
+
for x in v:
|
18
|
+
if not _serializable_ok(x): return False
|
19
|
+
return True
|
20
|
+
if t is set:
|
21
|
+
for x in v:
|
22
|
+
if not _serializable_ok(x): return False
|
23
|
+
return True
|
24
|
+
if t is tuple:
|
25
|
+
for x in v:
|
26
|
+
if not _serializable_ok(x): return False
|
27
|
+
return True
|
28
|
+
if t is dict:
|
29
|
+
for k, vv in v.items():
|
30
|
+
if not isinstance(k, _KEYTYPES): return False
|
31
|
+
if not _serializable_ok(vv): return False
|
32
|
+
return True
|
33
|
+
return False
|
34
|
+
|
35
|
+
@functools.lru_cache(maxsize=8192)
|
36
|
+
def _compile_checker(ann: Any):
|
37
|
+
"""
|
38
|
+
Компилирует аннотацию в быстрый чекер: (value) -> None | str.
|
39
|
+
Возвращает None, если значение валидно; иначе — строку/многострочную строку с ошибками.
|
40
|
+
Поддерживаются только типы из SerializableType.
|
41
|
+
"""
|
42
|
+
if ann is _empty:
|
43
|
+
def chk(v):
|
44
|
+
if _serializable_ok(v):
|
45
|
+
return None
|
46
|
+
return f"значение не SerializableType: {type(v).__name__}({v!r})"
|
47
|
+
return chk
|
48
|
+
|
49
|
+
origin = get_origin(ann)
|
50
|
+
if origin is Union:
|
51
|
+
alts = tuple(_compile_checker(a) for a in get_args(ann))
|
52
|
+
types_str = " | ".join(_ann_to_str(a) for a in get_args(ann))
|
53
|
+
def chk(v):
|
54
|
+
for c in alts:
|
55
|
+
if c(v) is None:
|
56
|
+
return None
|
57
|
+
return f"ожидалось {types_str}, получено {type(v).__name__}({v!r})"
|
58
|
+
return chk
|
59
|
+
|
60
|
+
# Примитивы
|
61
|
+
if ann is type(None) or ann is None:
|
62
|
+
return lambda v: None if v is None else f"ожидалось None, получено {type(v).__name__}({v!r})"
|
63
|
+
if ann is bool:
|
64
|
+
return lambda v: None if isinstance(v, bool) else f"ожидалось bool, получено {type(v).__name__}({v!r})"
|
65
|
+
if ann is int:
|
66
|
+
return lambda v: None if _is_int_not_bool(v) else f"ожидалось int, получено {type(v).__name__}({v!r})"
|
67
|
+
if ann is float:
|
68
|
+
return lambda v: None if isinstance(v, float) else f"ожидалось float, получено {type(v).__name__}({v!r})"
|
69
|
+
if ann is str:
|
70
|
+
return lambda v: None if isinstance(v, str) else f"ожидалось str, получено {type(v).__name__}({v!r})"
|
71
|
+
if ann is bytes:
|
72
|
+
return lambda v: None if isinstance(v, bytes) else f"ожидалось bytes, получено {type(v).__name__}({v!r})"
|
73
|
+
if ann is datetime.datetime:
|
74
|
+
return lambda v: None if isinstance(v, datetime.datetime) else f"ожидалось datetime.datetime, получено {type(v).__name__}({v!r})"
|
75
|
+
if ann is datetime.time:
|
76
|
+
return lambda v: None if isinstance(v, datetime.time) else f"ожидалось datetime.time, получено {type(v).__name__}({v!r})"
|
77
|
+
|
78
|
+
# Контейнеры
|
79
|
+
if origin is list:
|
80
|
+
(elem_t,) = get_args(ann) or (_empty,)
|
81
|
+
elem_chk = _compile_checker(elem_t)
|
82
|
+
def chk(v):
|
83
|
+
if type(v) is not list:
|
84
|
+
return f"ожидался list, получено {type(v).__name__}({v!r})"
|
85
|
+
i = 0
|
86
|
+
for x in v:
|
87
|
+
# сначала гейт SerializableType для элемента
|
88
|
+
if not _serializable_ok(x):
|
89
|
+
return f"[{i}]: значение не SerializableType: {type(x).__name__}({x!r})"
|
90
|
+
e = elem_chk(x)
|
91
|
+
if e:
|
92
|
+
return f"[{i}]: {e}"
|
93
|
+
i += 1
|
94
|
+
return None
|
95
|
+
return chk
|
96
|
+
|
97
|
+
if origin is set:
|
98
|
+
(elem_t,) = get_args(ann) or (_empty,)
|
99
|
+
elem_chk = _compile_checker(elem_t)
|
100
|
+
def chk(v):
|
101
|
+
if type(v) is not set:
|
102
|
+
return f"ожидался set, получено {type(v).__name__}({v!r})"
|
103
|
+
for x in v:
|
104
|
+
if not _serializable_ok(x):
|
105
|
+
return f"set-elem: значение не SerializableType: {type(x).__name__}({x!r})"
|
106
|
+
e = elem_chk(x)
|
107
|
+
if e:
|
108
|
+
return f"set-elem: {e}"
|
109
|
+
return None
|
110
|
+
return chk
|
111
|
+
|
112
|
+
if origin is tuple:
|
113
|
+
args = get_args(ann)
|
114
|
+
# Только Tuple[T, ...]
|
115
|
+
if len(args) == 2 and args[1] is ...:
|
116
|
+
elem_chk = _compile_checker(args[0])
|
117
|
+
def chk(v):
|
118
|
+
if type(v) is not tuple:
|
119
|
+
return f"ожидался tuple, получено {type(v).__name__}({v!r})"
|
120
|
+
i = 0
|
121
|
+
for x in v:
|
122
|
+
if not _serializable_ok(x):
|
123
|
+
return f"[{i}]: значение не SerializableType: {type(x).__name__}({x!r})"
|
124
|
+
e = elem_chk(x)
|
125
|
+
if e:
|
126
|
+
return f"[{i}]: {e}"
|
127
|
+
i += 1
|
128
|
+
return None
|
129
|
+
return chk
|
130
|
+
return lambda _: "разрешён только Tuple[T, ...]"
|
131
|
+
|
132
|
+
if origin is dict:
|
133
|
+
kt, vt = (get_args(ann) + (_empty, _empty))[:2]
|
134
|
+
kchk = _compile_checker(kt) if kt is not _empty else None
|
135
|
+
vchk = _compile_checker(vt)
|
136
|
+
def chk(v):
|
137
|
+
if type(v) is not dict:
|
138
|
+
return f"ожидался dict, получено {type(v).__name__}({v!r})"
|
139
|
+
loc_errs = []
|
140
|
+
append = loc_errs.append
|
141
|
+
for k, vv in v.items():
|
142
|
+
# 1) базовое правило SerializableKey
|
143
|
+
if not isinstance(k, _KEYTYPES):
|
144
|
+
append(f"некорректный ключ {k!r} ({type(k).__name__}); разрешены str|bytes|int")
|
145
|
+
# 2) если схема уточняет тип ключа — проверяем отдельно
|
146
|
+
if kchk:
|
147
|
+
ek = kchk(k)
|
148
|
+
if ek:
|
149
|
+
# точная причина
|
150
|
+
append(f"key: {ek}")
|
151
|
+
# и общая подсказка, чтобы совпало с ожидаемым форматом из тестов
|
152
|
+
append(f"некорректный ключ {k!r} ({type(k).__name__}); разрешены str|bytes|int")
|
153
|
+
# 3) проверка значения по схеме (всегда выполняем, даже если ключ сломан)
|
154
|
+
ev = vchk(vv)
|
155
|
+
if ev:
|
156
|
+
for line in str(ev).splitlines():
|
157
|
+
append(f"{k!r}: {line}")
|
158
|
+
if loc_errs:
|
159
|
+
return "\n".join(loc_errs)
|
160
|
+
return None
|
161
|
+
return chk
|
162
|
+
|
163
|
+
return lambda _: f"неподдерживаемая аннотация: {ann!r} (разрешены только типы из SerializableType)"
|
164
|
+
|
165
|
+
|
166
|
+
def _ann_to_str(t):
|
167
|
+
o = get_origin(t)
|
168
|
+
if o is Union: return " | ".join(_ann_to_str(a) for a in get_args(t))
|
169
|
+
try: return t.__name__
|
170
|
+
except Exception: return str(t)
|
171
|
+
|
172
|
+
class _Field:
|
173
|
+
__slots__ = ("name","required","checker")
|
174
|
+
def __init__(self, name, required, checker): self.name=name; self.required=required; self.checker=checker
|
175
|
+
|
176
|
+
class Schema:
|
177
|
+
__slots__ = ("fields","names","has_kwargs")
|
178
|
+
def __init__(self, fields, names, has_kwargs): self.fields=fields; self.names=names; self.has_kwargs=has_kwargs
|
179
|
+
|
180
|
+
_SCHEMA_CACHE: dict[Any, Schema] = {}
|
181
|
+
_SCHEMA_LOCK = threading.Lock()
|
182
|
+
|
183
|
+
def _build_schema_from_params(func_params) -> Schema:
|
184
|
+
pmap = dict(func_params)
|
185
|
+
fields = []
|
186
|
+
names = set()
|
187
|
+
has_kwargs = False
|
188
|
+
for name, p in pmap.items():
|
189
|
+
if p.kind == Parameter.VAR_KEYWORD:
|
190
|
+
has_kwargs = True; continue
|
191
|
+
if p.kind == Parameter.VAR_POSITIONAL:
|
192
|
+
continue
|
193
|
+
required = (p.default is _empty) # Optional[...] не делает параметр необязательным
|
194
|
+
checker = _compile_checker(p.annotation)
|
195
|
+
fields.append(_Field(name, required, checker))
|
196
|
+
names.add(name)
|
197
|
+
return Schema(tuple(fields), frozenset(names), has_kwargs)
|
198
|
+
|
199
|
+
def register_schema_by_key(func) -> Schema:
|
200
|
+
"""Регистрируем схему по самой функции (делать на старте)."""
|
201
|
+
sc = _SCHEMA_CACHE.get(func)
|
202
|
+
if sc is not None: return sc
|
203
|
+
with _SCHEMA_LOCK:
|
204
|
+
sc = _SCHEMA_CACHE.get(func)
|
205
|
+
if sc is not None: return sc
|
206
|
+
sc = _build_schema_from_params(inspect.signature(func).parameters)
|
207
|
+
_SCHEMA_CACHE[func] = sc
|
208
|
+
return sc
|
209
|
+
|
210
|
+
def get_schema_for_func(func) -> Schema:
|
211
|
+
sc = _SCHEMA_CACHE.get(func)
|
212
|
+
if sc is not None: return sc
|
213
|
+
# На всякий случай (если забыли зарегистрировать) — соберём и закешируем
|
214
|
+
return register_schema_by_key(func)
|
215
|
+
|
216
|
+
def validate_params(params: dict, schema: Schema) -> str | None:
|
217
|
+
"""
|
218
|
+
Валидирует dict по заранее скомпилированной Schema.
|
219
|
+
Возвращает None, если всё ок; иначе — многострочную строку со ВСЕМИ ошибками.
|
220
|
+
Гарантирует, что каждая строка ошибки начинается с "'<param>': ..." (полный путь виден).
|
221
|
+
"""
|
222
|
+
errors = []
|
223
|
+
names = schema.names
|
224
|
+
has_kwargs = schema.has_kwargs
|
225
|
+
|
226
|
+
# Лишние ключи (если нет **kwargs) — собираем все
|
227
|
+
if not has_kwargs:
|
228
|
+
for k in params.keys():
|
229
|
+
if k not in names:
|
230
|
+
errors.append(f"Лишний параметр: '{k}' не принят сигнатурой.")
|
231
|
+
|
232
|
+
get = params.get
|
233
|
+
for f in schema.fields:
|
234
|
+
v = get(f.name, _empty)
|
235
|
+
if v is _empty:
|
236
|
+
if f.required:
|
237
|
+
errors.append(f"Отсутствует обязательный параметр '{f.name}'.")
|
238
|
+
continue
|
239
|
+
|
240
|
+
# Всегда зовём чекер, чтобы получить детальные сообщения по вложенным частям.
|
241
|
+
e = f.checker(v)
|
242
|
+
if e:
|
243
|
+
# Префиксуем КАЖДУЮ строку именем параметра, чтобы тесты видели "'d': b'k': ..."
|
244
|
+
lines = str(e).splitlines()
|
245
|
+
for line in lines:
|
246
|
+
errors.append(f"'{f.name}': {line}")
|
247
|
+
|
248
|
+
return None if not errors else "\n".join(errors)
|
249
|
+
|
250
|
+
|
251
|
+
|
252
|
+
def validate_params_by_key(params: dict, func) -> str | None:
|
253
|
+
"""В рантайме: передаём dict и саму функцию (схема уже в кеше)."""
|
254
|
+
return validate_params(params, get_schema_for_func(func))
|
GNServer/_routes.py
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
|
2
|
+
import re
|
3
|
+
import uuid
|
4
|
+
import decimal
|
5
|
+
import inspect
|
6
|
+
import datetime
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Pattern, Tuple, Union, AsyncGenerator
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from typing import Any, Union, get_origin, get_args
|
10
|
+
|
11
|
+
|
12
|
+
from gnobjects.net.objects import CORSObject
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class Route:
|
16
|
+
method: str
|
17
|
+
path_expr: str
|
18
|
+
regex: Pattern[str]
|
19
|
+
param_types: dict[str, Callable[[str], Any]]
|
20
|
+
handler: Callable[..., Any]
|
21
|
+
name: str
|
22
|
+
cors: Optional[CORSObject]
|
23
|
+
|
24
|
+
_PARAM_REGEX: dict[str, str] = {
|
25
|
+
"str": r"[^/]+",
|
26
|
+
"path": r".+",
|
27
|
+
"int": r"\d+",
|
28
|
+
"float": r"[+-]?\d+(?:\.\d+)?",
|
29
|
+
"bool": r"(?:true|false|1|0)",
|
30
|
+
"uuid": r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-"
|
31
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
32
|
+
r"[0-9a-fA-F]{12}",
|
33
|
+
"datetime": r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?",
|
34
|
+
"date": r"\d{4}-\d{2}-\d{2}",
|
35
|
+
"time": r"\d{2}:\d{2}:\d{2}(?:\.\d+)?",
|
36
|
+
"decimal": r"[+-]?\d+(?:\.\d+)?",
|
37
|
+
}
|
38
|
+
|
39
|
+
_CONVERTER_FUNC: dict[str, Callable[[str], Any]] = {
|
40
|
+
"int": int,
|
41
|
+
"float": float,
|
42
|
+
"bool": lambda s: s.lower() in {"1","true","yes","on"},
|
43
|
+
"uuid": uuid.UUID,
|
44
|
+
"decimal": decimal.Decimal,
|
45
|
+
"datetime": datetime.datetime.fromisoformat,
|
46
|
+
"date": datetime.date.fromisoformat,
|
47
|
+
"time": datetime.time.fromisoformat,
|
48
|
+
}
|
49
|
+
|
50
|
+
def _compile_path(path: str) -> tuple[Pattern[str], dict[str, Callable[[str], Any]]]:
|
51
|
+
param_types: dict[str, Callable[[str], Any]] = {}
|
52
|
+
rx_parts: list[str] = ["^"]
|
53
|
+
i = 0
|
54
|
+
while i < len(path):
|
55
|
+
if path[i] != "{":
|
56
|
+
rx_parts.append(re.escape(path[i]))
|
57
|
+
i += 1
|
58
|
+
continue
|
59
|
+
j = path.index("}", i)
|
60
|
+
spec = path[i+1:j]
|
61
|
+
i = j + 1
|
62
|
+
|
63
|
+
if ":" in spec:
|
64
|
+
name, conv = spec.split(":", 1)
|
65
|
+
else:
|
66
|
+
name, conv = spec, "str"
|
67
|
+
|
68
|
+
if conv.startswith("^"):
|
69
|
+
rx = f"(?P<{name}>{conv})"
|
70
|
+
typ = str
|
71
|
+
else:
|
72
|
+
rx = f"(?P<{name}>{_PARAM_REGEX.get(conv, _PARAM_REGEX['str'])})"
|
73
|
+
typ = _CONVERTER_FUNC.get(conv, str)
|
74
|
+
|
75
|
+
rx_parts.append(rx)
|
76
|
+
param_types[name] = typ
|
77
|
+
|
78
|
+
rx_parts.append("$")
|
79
|
+
return re.compile("".join(rx_parts)), param_types
|
80
|
+
|
81
|
+
def _convert_value(raw: str | list[str], ann: Any, fallback: Callable[[str], Any]) -> Any:
|
82
|
+
origin = get_origin(ann)
|
83
|
+
args = get_args(ann)
|
84
|
+
|
85
|
+
if isinstance(raw, list) or origin is list:
|
86
|
+
subtype = args[0] if (origin is list and args) else str
|
87
|
+
if not isinstance(raw, list):
|
88
|
+
raw = [raw]
|
89
|
+
return [_convert_value(r, subtype, fallback) for r in raw]
|
90
|
+
|
91
|
+
if origin is Union:
|
92
|
+
for subtype in args:
|
93
|
+
try:
|
94
|
+
return _convert_value(raw, subtype, fallback)
|
95
|
+
except Exception:
|
96
|
+
continue
|
97
|
+
return raw # если ни один тип не подошёл
|
98
|
+
|
99
|
+
conv = _CONVERTER_FUNC.get(ann, ann) if ann is not inspect._empty else fallback
|
100
|
+
return conv(raw) if callable(conv) else raw
|
101
|
+
|
102
|
+
def _ensure_async(fn: Callable[..., Any]) -> Callable[..., Any]:
|
103
|
+
if inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn):
|
104
|
+
return fn
|
105
|
+
async def wrapper(*args, **kw):
|
106
|
+
return fn(*args, **kw)
|
107
|
+
return wrapper
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
import re
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
# regex для !{var}, поддерживает вложенность через точку
|
6
|
+
TPL_VAR_RE = re.compile(r'(?<!\\)!\{([A-Za-z_][A-Za-z0-9_\.]*)\}')
|
7
|
+
|
8
|
+
# список mime, которые считаем текстовыми
|
9
|
+
TEXTUAL_MIME_PREFIXES = [
|
10
|
+
"text/", # text/html, text/css, text/plain
|
11
|
+
]
|
12
|
+
TEXTUAL_MIME_EXACT = {
|
13
|
+
"application/javascript",
|
14
|
+
"application/json",
|
15
|
+
"application/xml",
|
16
|
+
"application/xhtml+xml"
|
17
|
+
}
|
18
|
+
TEXTUAL_MIME_SUFFIXES = (
|
19
|
+
"+xml", # например application/rss+xml
|
20
|
+
"+json", # application/ld+json
|
21
|
+
)
|
22
|
+
|
23
|
+
def extract_template_vars(filedata: bytes, mime: str) -> List[str]:
|
24
|
+
"""
|
25
|
+
Ищет все !{var} в тексте, если MIME относится к текстовым.
|
26
|
+
"""
|
27
|
+
mime = (mime or "").lower().strip()
|
28
|
+
|
29
|
+
# определяем, текстовый ли mime
|
30
|
+
is_textual = (
|
31
|
+
mime.startswith(tuple(TEXTUAL_MIME_PREFIXES))
|
32
|
+
or mime in TEXTUAL_MIME_EXACT
|
33
|
+
or mime.endswith(TEXTUAL_MIME_SUFFIXES)
|
34
|
+
or "javascript" in mime
|
35
|
+
or "json" in mime
|
36
|
+
or "xml" in mime
|
37
|
+
)
|
38
|
+
|
39
|
+
if not is_textual:
|
40
|
+
return []
|
41
|
+
|
42
|
+
try:
|
43
|
+
text = filedata.decode("utf-8", errors="ignore")
|
44
|
+
except Exception:
|
45
|
+
return []
|
46
|
+
|
47
|
+
return list(set(m.group(1) for m in TPL_VAR_RE.finditer(text)))
|
48
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
GNServer/__init__.py,sha256=RCYh6f0lwh-Xh-5fv0jZvbUE6x7l8xYJhbn-o8_ITlE,1551
|
2
|
+
GNServer/_app.py,sha256=yHnpR30gCSDG2WL1a8fiLYmDeEc8YEaRh9UMo0PiqUA,18841
|
3
|
+
GNServer/_client.py,sha256=wK5F_ilMX70tYEcWYRHvWyKGMaKA0ivvsUeg6PrEk9s,30249
|
4
|
+
GNServer/_cors_resolver.py,sha256=U9IFGN7vpVsEM2smhuf5QGj8vYgs7HeFQwDdzWVVy9c,4832
|
5
|
+
GNServer/_crt.py,sha256=SOmyX7zBiCY9EhVSekksQtBHgTIZVvdqNZ8Ni-E5Zow,1390
|
6
|
+
GNServer/_func_params_validation.py,sha256=QnSX74khneQpJer9uVFGG7JcqpquzSce6CrAxFA2hUY,11361
|
7
|
+
GNServer/_routes.py,sha256=bJnmQ8uEhPVQgy2tTqE5TEIM8aFXV-lVI7c2nG0rQwk,3384
|
8
|
+
GNServer/_template_resolver.py,sha256=vdJYb_7PjIeTWq-Clr7jyj7QIvPBxplU7EqeOuMJ64c,1409
|
9
|
+
gnserver-0.0.0.0.36.dist-info/licenses/LICENSE,sha256=WH_t7dKZyWJ5Ld07eYIkUG4Tv6zZWXtAdsUqYAUesn0,1084
|
10
|
+
gnserver-0.0.0.0.36.dist-info/METADATA,sha256=Dkc11JmXE5UQH-8sB29LYnMnioJcdQ_8G1xCS8ZRD_E,830
|
11
|
+
gnserver-0.0.0.0.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
12
|
+
gnserver-0.0.0.0.36.dist-info/top_level.txt,sha256=-UOUBuD4u7Qkb1o5PdcwyA3kx8xCH2lwy0tJHi26Wb4,9
|
13
|
+
gnserver-0.0.0.0.36.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
GNServer/__init__.py,sha256=RCYh6f0lwh-Xh-5fv0jZvbUE6x7l8xYJhbn-o8_ITlE,1551
|
2
|
-
GNServer/_app.py,sha256=77g8nGa1-5faslltJeffpU585T9xZKf7D7wm8EjBMyU,27610
|
3
|
-
GNServer/_client.py,sha256=fq-n322jWQZSYj1IR4UjDlmf9sMDYExVGMMIgtlMiAw,30123
|
4
|
-
GNServer/_crt.py,sha256=SOmyX7zBiCY9EhVSekksQtBHgTIZVvdqNZ8Ni-E5Zow,1390
|
5
|
-
gnserver-0.0.0.0.34.dist-info/licenses/LICENSE,sha256=WH_t7dKZyWJ5Ld07eYIkUG4Tv6zZWXtAdsUqYAUesn0,1084
|
6
|
-
gnserver-0.0.0.0.34.dist-info/METADATA,sha256=83WIyIrJaW3uyTn2DWrUIxTqQpatFS6VEoATt2DOXvQ,830
|
7
|
-
gnserver-0.0.0.0.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
gnserver-0.0.0.0.34.dist-info/top_level.txt,sha256=-UOUBuD4u7Qkb1o5PdcwyA3kx8xCH2lwy0tJHi26Wb4,9
|
9
|
-
gnserver-0.0.0.0.34.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|