mrok 0.4.6__py3-none-any.whl → 0.6.0__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.
- mrok/agent/devtools/inspector/app.py +2 -2
- mrok/agent/sidecar/app.py +61 -35
- mrok/agent/sidecar/main.py +35 -9
- mrok/agent/ziticorn.py +9 -3
- mrok/cli/commands/__init__.py +2 -2
- mrok/cli/commands/admin/bootstrap.py +3 -2
- mrok/cli/commands/admin/utils.py +2 -2
- mrok/cli/commands/agent/run/sidecar.py +59 -1
- mrok/cli/commands/{proxy → frontend}/__init__.py +1 -1
- mrok/cli/commands/frontend/run.py +91 -0
- mrok/constants.py +0 -2
- mrok/controller/openapi/examples.py +13 -0
- mrok/controller/schemas.py +2 -2
- mrok/frontend/__init__.py +3 -0
- mrok/frontend/app.py +75 -0
- mrok/{proxy → frontend}/main.py +12 -10
- mrok/proxy/__init__.py +0 -3
- mrok/proxy/app.py +158 -83
- mrok/proxy/asgi.py +96 -0
- mrok/proxy/backend.py +45 -0
- mrok/proxy/event_publisher.py +66 -0
- mrok/proxy/exceptions.py +22 -0
- mrok/{master.py → proxy/master.py} +36 -81
- mrok/{metrics.py → proxy/metrics.py} +38 -50
- mrok/{http/middlewares.py → proxy/middleware.py} +17 -26
- mrok/{datastructures.py → proxy/models.py} +43 -10
- mrok/proxy/stream.py +68 -0
- mrok/{http → proxy}/utils.py +1 -1
- mrok/proxy/worker.py +64 -0
- mrok/{http/config.py → proxy/ziticorn.py} +29 -6
- mrok/types/proxy.py +20 -0
- mrok/types/ziti.py +1 -0
- mrok/ziti/api.py +15 -18
- mrok/ziti/bootstrap.py +3 -2
- mrok/ziti/identities.py +5 -4
- mrok/ziti/services.py +3 -2
- {mrok-0.4.6.dist-info → mrok-0.6.0.dist-info}/METADATA +2 -5
- {mrok-0.4.6.dist-info → mrok-0.6.0.dist-info}/RECORD +43 -39
- mrok/cli/commands/proxy/run.py +0 -49
- mrok/http/forwarder.py +0 -354
- mrok/http/lifespan.py +0 -39
- mrok/http/pool.py +0 -239
- mrok/http/protocol.py +0 -11
- mrok/http/server.py +0 -14
- mrok/http/types.py +0 -18
- /mrok/{http → proxy}/constants.py +0 -0
- /mrok/{http → types}/__init__.py +0 -0
- {mrok-0.4.6.dist-info → mrok-0.6.0.dist-info}/WHEEL +0 -0
- {mrok-0.4.6.dist-info → mrok-0.6.0.dist-info}/entry_points.txt +0 -0
- {mrok-0.4.6.dist-info → mrok-0.6.0.dist-info}/licenses/LICENSE.txt +0 -0
mrok/ziti/identities.py
CHANGED
|
@@ -5,8 +5,9 @@ from typing import Any
|
|
|
5
5
|
import jwt
|
|
6
6
|
|
|
7
7
|
from mrok.conf import Settings
|
|
8
|
+
from mrok.types.ziti import Tags
|
|
8
9
|
from mrok.ziti import pki
|
|
9
|
-
from mrok.ziti.api import
|
|
10
|
+
from mrok.ziti.api import ZitiClientAPI, ZitiManagementAPI
|
|
10
11
|
from mrok.ziti.constants import (
|
|
11
12
|
MROK_IDENTITY_TYPE_TAG_NAME,
|
|
12
13
|
MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE,
|
|
@@ -29,7 +30,7 @@ async def register_identity(
|
|
|
29
30
|
client_api: ZitiClientAPI,
|
|
30
31
|
service_external_id: str,
|
|
31
32
|
identity_external_id: str,
|
|
32
|
-
tags:
|
|
33
|
+
tags: Tags | None = None,
|
|
33
34
|
):
|
|
34
35
|
service_name = service_external_id.lower()
|
|
35
36
|
identity_tags = copy.copy(tags or {})
|
|
@@ -39,7 +40,7 @@ async def register_identity(
|
|
|
39
40
|
if not service:
|
|
40
41
|
raise ServiceNotFoundError(f"A service with name `{service_external_id}` does not exists.")
|
|
41
42
|
|
|
42
|
-
identity_name =
|
|
43
|
+
identity_name = identity_external_id.lower()
|
|
43
44
|
service_policy_name = f"{identity_name}:bind"
|
|
44
45
|
self_service_policy_name = f"self.{service_policy_name}"
|
|
45
46
|
|
|
@@ -129,7 +130,7 @@ async def enroll_proxy_identity(
|
|
|
129
130
|
mgmt_api: ZitiManagementAPI,
|
|
130
131
|
client_api: ZitiClientAPI,
|
|
131
132
|
identity_name: str,
|
|
132
|
-
tags:
|
|
133
|
+
tags: Tags | None = None,
|
|
133
134
|
):
|
|
134
135
|
identity = await mgmt_api.search_identity(identity_name)
|
|
135
136
|
if identity:
|
mrok/ziti/services.py
CHANGED
|
@@ -2,7 +2,8 @@ import logging
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
from mrok.conf import Settings
|
|
5
|
-
from mrok.ziti
|
|
5
|
+
from mrok.types.ziti import Tags
|
|
6
|
+
from mrok.ziti.api import ZitiManagementAPI
|
|
6
7
|
from mrok.ziti.errors import (
|
|
7
8
|
ConfigTypeNotFoundError,
|
|
8
9
|
ProxyIdentityNotFoundError,
|
|
@@ -14,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
async def register_service(
|
|
17
|
-
settings: Settings, mgmt_api: ZitiManagementAPI, external_id: str, tags:
|
|
18
|
+
settings: Settings, mgmt_api: ZitiManagementAPI, external_id: str, tags: Tags | None
|
|
18
19
|
) -> dict[str, Any]:
|
|
19
20
|
service_name = external_id.lower()
|
|
20
21
|
registered = False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mrok
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: MPT Extensions OpenZiti Orchestrator
|
|
5
5
|
Author: SoftwareOne AG
|
|
6
6
|
License: Apache License
|
|
@@ -207,21 +207,18 @@ License: Apache License
|
|
|
207
207
|
License-File: LICENSE.txt
|
|
208
208
|
Requires-Python: <4,>=3.12
|
|
209
209
|
Requires-Dist: asn1crypto<2.0.0,>=1.5.1
|
|
210
|
-
Requires-Dist: cachetools<7.0.0,>=6.2.2
|
|
211
210
|
Requires-Dist: cryptography<46.0.0,>=45.0.7
|
|
212
211
|
Requires-Dist: dynaconf<4.0.0,>=3.2.11
|
|
213
212
|
Requires-Dist: fastapi-pagination<0.15.0,>=0.14.1
|
|
214
213
|
Requires-Dist: fastapi[standard]<0.120.0,>=0.119.0
|
|
215
214
|
Requires-Dist: gunicorn<24.0.0,>=23.0.0
|
|
216
215
|
Requires-Dist: hdrhistogram<0.11.0,>=0.10.3
|
|
217
|
-
Requires-Dist:
|
|
218
|
-
Requires-Dist: httpx<0.29.0,>=0.28.1
|
|
216
|
+
Requires-Dist: httpcore<2.0.0,>=1.0.9
|
|
219
217
|
Requires-Dist: openziti<2.0.0,>=1.3.1
|
|
220
218
|
Requires-Dist: psutil<8.0.0,>=7.1.3
|
|
221
219
|
Requires-Dist: pydantic<3.0.0,>=2.11.7
|
|
222
220
|
Requires-Dist: pyfiglet<2.0.0,>=1.0.4
|
|
223
221
|
Requires-Dist: pyjwt<3.0.0,>=2.10.1
|
|
224
|
-
Requires-Dist: pytest-textual-snapshot<2.0.0,>=1.1.0
|
|
225
222
|
Requires-Dist: pyyaml<7.0.0,>=6.0.2
|
|
226
223
|
Requires-Dist: pyzmq<28.0.0,>=27.1.0
|
|
227
224
|
Requires-Dist: rich<15.0.0,>=14.1.0
|
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
mrok/__init__.py,sha256=D1PUs3KtMCqG4bFLceVNG62L3RN53NS95uSCNXpgvzs,181
|
|
2
2
|
mrok/conf.py,sha256=_5Z-A5LyojQeY8J7W8C0QidsmrPl99r9qKYEoMf4kcI,840
|
|
3
|
-
mrok/constants.py,sha256=
|
|
4
|
-
mrok/datastructures.py,sha256=gp8KF2JoNOxIRzYStVZLKL_XVDbcIVSIDnmpQo4FNt0,4067
|
|
3
|
+
mrok/constants.py,sha256=UTGYqs3DgEd_SN-k0JK10ekmHVWQaaARtdh1Y-0JG_s,122
|
|
5
4
|
mrok/errors.py,sha256=ruNMDFr2_0ezCGXuCG1OswCEv-bHOIzMMd02J_0ABcs,37
|
|
6
5
|
mrok/logging.py,sha256=ZMWn0w4fJ-F_g-L37H_GM14BSXAIF2mFF_ougX5S7mg,2856
|
|
7
|
-
mrok/master.py,sha256=XuketJZuB1YWdbTs819pjLum7Qfv232F9ZCxdwRztCQ,8340
|
|
8
|
-
mrok/metrics.py,sha256=asweK_7xiV5MtkDkvbEm9Tktqrl2KHM8VflF0AkNGI0,4036
|
|
9
6
|
mrok/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
mrok/agent/ziticorn.py,sha256=
|
|
7
|
+
mrok/agent/ziticorn.py,sha256=eHUYs9QaSp35rBzYHRV-SrYxF5ySyECaQg7U-XbdINE,1025
|
|
11
8
|
mrok/agent/devtools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
9
|
mrok/agent/devtools/__main__.py,sha256=R8ezbW7hCik5r45U3w2TgiTubg9SlbVsWA-bapILJXU,781
|
|
13
10
|
mrok/agent/devtools/inspector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
11
|
mrok/agent/devtools/inspector/__main__.py,sha256=HeYcRf1bjXPji2LKMPCcTU61afrRH2P1RqnFmHClRTc,524
|
|
15
|
-
mrok/agent/devtools/inspector/app.py,sha256=
|
|
12
|
+
mrok/agent/devtools/inspector/app.py,sha256=rmfm0GVz_iZ_tqne6E966D2He5QVprO2g7PHFrnjG0U,16484
|
|
16
13
|
mrok/agent/devtools/inspector/server.py,sha256=C4uD6_1psSHMjJLUDCMPGvKdQYKaEwYTw27NAbwuuA0,636
|
|
17
14
|
mrok/agent/sidecar/__init__.py,sha256=DrjJGhqFyxsVODW06KI20Wpr6HsD2lD6qFCKUXc7GIE,59
|
|
18
|
-
mrok/agent/sidecar/app.py,sha256=
|
|
19
|
-
mrok/agent/sidecar/main.py,sha256=
|
|
15
|
+
mrok/agent/sidecar/app.py,sha256=sPQqjnwETRQk0cj8hAxSVUDCkYqsVZCS6ixEqkcGY5A,2534
|
|
16
|
+
mrok/agent/sidecar/main.py,sha256=jeJzrCbltfXOYsKSCjcw8h5lxh4_bGT87kCC5dV4kYU,2190
|
|
20
17
|
mrok/cli/__init__.py,sha256=mtFEa8IeS1x6Gm4dUYoSnAxyEzNqbUVSmWxtuZUMR84,61
|
|
21
18
|
mrok/cli/main.py,sha256=DFcYPwDskXi8SKAgEsuP4GMFzaniIf_6bZaSDWvYKDk,2724
|
|
22
19
|
mrok/cli/rich.py,sha256=P3Dyu8EArUR9_0j7DPK7LRx85TWdYdZ1SaJzD_S1ZCE,511
|
|
23
20
|
mrok/cli/utils.py,sha256=m_olScdIUGks5IoC6p2F9D6CQIucWZ7LHyrvwm2bkJw,106
|
|
24
|
-
mrok/cli/commands/__init__.py,sha256
|
|
21
|
+
mrok/cli/commands/__init__.py,sha256=-UOGzh38oWX7fPeI2nc5I9z8LylRdQAt868q4G6rNGk,140
|
|
25
22
|
mrok/cli/commands/admin/__init__.py,sha256=WU49jpMF9p18UONjYywWEFzjF57zLpLKJ0qAZvrzcR4,414
|
|
26
|
-
mrok/cli/commands/admin/bootstrap.py,sha256=
|
|
27
|
-
mrok/cli/commands/admin/utils.py,sha256=
|
|
23
|
+
mrok/cli/commands/admin/bootstrap.py,sha256=9ADSeiVbFAZXh6GxHEf9h2g_XHGOIlMmg1rgsxMfdow,1699
|
|
24
|
+
mrok/cli/commands/admin/utils.py,sha256=Z7YTAFZKOi6nkw2oX4rJoGoUD41RYL3AOqEDhlV3jR0,1357
|
|
28
25
|
mrok/cli/commands/admin/list/__init__.py,sha256=kjCMcpn1gopcrQaaHxfFh8Kyngldepnle8R2br5dJ_0,195
|
|
29
26
|
mrok/cli/commands/admin/list/extensions.py,sha256=16fhDB5ucL8su2WQnSaQ1E6MhgC4vkP9-nuHAcPpzyE,4405
|
|
30
27
|
mrok/cli/commands/admin/list/instances.py,sha256=kaqeyidwUxgYqfaHXqp2m76rm5h2ErBsYyZcNeaBRwY,5912
|
|
@@ -41,50 +38,57 @@ mrok/cli/commands/agent/dev/console.py,sha256=rrKAGoKXVQQBOC75H0JSuX1sYyvc2QSrV-
|
|
|
41
38
|
mrok/cli/commands/agent/dev/web.py,sha256=O9dYk-o1FV2E_sKLOezdEmLsnexwbJNDdsYL5pATZRQ,1028
|
|
42
39
|
mrok/cli/commands/agent/run/__init__.py,sha256=E_IJCl3BfMffqFASe8gzJwhhQgt5bQfjhuyekVwdEBA,164
|
|
43
40
|
mrok/cli/commands/agent/run/asgi.py,sha256=dCgzwJtTLv2eyEIP7v1tDfe_PrFBS02SfN5dSDw1Jzg,2054
|
|
44
|
-
mrok/cli/commands/agent/run/sidecar.py,sha256=
|
|
41
|
+
mrok/cli/commands/agent/run/sidecar.py,sha256=UOewegTLFwAZ70VFJb6_9kV0LmsvnXuq-yqgrMlTeZo,4182
|
|
45
42
|
mrok/cli/commands/controller/__init__.py,sha256=2xw-YVN0akiLiuGUU3XbYyZZ0ugOjQ6XhtTkzEKSmMA,161
|
|
46
43
|
mrok/cli/commands/controller/openapi.py,sha256=QLjVao9UkB2vBaGkFi_q_jrlg4Np4ldMRwDIJsrJ7A8,1175
|
|
47
44
|
mrok/cli/commands/controller/run.py,sha256=yl1p7oRHhQINWWjUKlRHtMIWUCV0KsxYdyVyazhX834,2406
|
|
48
|
-
mrok/cli/commands/
|
|
49
|
-
mrok/cli/commands/
|
|
45
|
+
mrok/cli/commands/frontend/__init__.py,sha256=0kK37yG6qs7yAa8TYlKZUA-nHrWsO4y5CjbVkXafnuk,123
|
|
46
|
+
mrok/cli/commands/frontend/run.py,sha256=_X1ylMe4-YCTghsu0XY-PB4nk3PL-PQq9YIgbkgJok8,2796
|
|
50
47
|
mrok/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
48
|
mrok/controller/app.py,sha256=XxCIB7N1YE52vSYfvGW2UPgEEOZ9jxDMe2l9D2SfXi8,1866
|
|
52
49
|
mrok/controller/auth.py,sha256=hYa0OPJ5X0beGxRP6qbxwJOVXj5TmzHjmam2OjTBKn4,2704
|
|
53
50
|
mrok/controller/pagination.py,sha256=raYpYa34q8Ckl4BXBOEdpWlKkFj6z7e6QLWr2HT7dzI,2187
|
|
54
|
-
mrok/controller/schemas.py,sha256=
|
|
51
|
+
mrok/controller/schemas.py,sha256=PZPEsSJNrGSuplfjCPF_E-VJ721AzgR1Jj8P-Shw1cg,1699
|
|
55
52
|
mrok/controller/dependencies/__init__.py,sha256=voewk6gjkA0OarL6HFmfT_RLqBns0Fpl-VIqK5xVAEI,202
|
|
56
53
|
mrok/controller/dependencies/conf.py,sha256=2Pa8fxJHkZ29q6UL-w6hUP_wr7WnNELfw5LlzWg1Tec,162
|
|
57
54
|
mrok/controller/dependencies/ziti.py,sha256=fYoxeJb4s6p2_3gxbExbFSRabjpvp_gZMBb3ocXZV3Y,702
|
|
58
55
|
mrok/controller/openapi/__init__.py,sha256=U1dw45w76CcoQagyqg_FXdMuJF3qJZZM6wG8TeTe3Zo,101
|
|
59
|
-
mrok/controller/openapi/examples.py,sha256
|
|
56
|
+
mrok/controller/openapi/examples.py,sha256=-Mwj41veIeydYaRSxcihN02g_jMuUrtvMfTjs08Dzf8,1852
|
|
60
57
|
mrok/controller/openapi/utils.py,sha256=Kn55ISAWlMJNwrJTum7iFrBvJvr81To76pCK8W-s79Q,1114
|
|
61
58
|
mrok/controller/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
59
|
mrok/controller/routes/extensions.py,sha256=zoY4sNz_BIZcbly6WXM7Rbpn2jmB89njS_0xdJkoKfs,9192
|
|
63
60
|
mrok/controller/routes/instances.py,sha256=v-fn_F6JHbDZ4YUNCIZzClgHp6aC1Eu5HB7k7qBG5pk,2202
|
|
64
|
-
mrok/
|
|
65
|
-
mrok/
|
|
66
|
-
mrok/
|
|
67
|
-
mrok/
|
|
68
|
-
mrok/
|
|
69
|
-
mrok/
|
|
70
|
-
mrok/
|
|
71
|
-
mrok/
|
|
72
|
-
mrok/
|
|
73
|
-
mrok/
|
|
74
|
-
mrok/
|
|
75
|
-
mrok/proxy/
|
|
76
|
-
mrok/proxy/
|
|
77
|
-
mrok/proxy/
|
|
61
|
+
mrok/frontend/__init__.py,sha256=SN3LoFwAye18lfJ8OKNNS-7kLc2A9OxPGIEIEYYtAOA,54
|
|
62
|
+
mrok/frontend/app.py,sha256=I2cvEI2ZGhbeazFhF6LavxBYywsv-4QkuNxCDo6-dkA,2627
|
|
63
|
+
mrok/frontend/main.py,sha256=0KtchIGLn70A_Oxekmhr_qSYUg5QqIMfrdYcyFdpj9s,1717
|
|
64
|
+
mrok/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
|
+
mrok/proxy/app.py,sha256=flnVPoUO3pSF3b0nYFBhKjZ9jp5ljijo2a-5e37gACs,6016
|
|
66
|
+
mrok/proxy/asgi.py,sha256=2uw5bLquyUsiYlNwq8RhJd8OqVvSJDvYjzOVGLLB3Cs,3528
|
|
67
|
+
mrok/proxy/backend.py,sha256=dRmIUJin2DM3PUxrVX0j4t1oB6DOX7N9JV2lIcopE38,1649
|
|
68
|
+
mrok/proxy/constants.py,sha256=ao5gI2HFBWmrdd2Yc6XFK_RGaHk-omxI4AqvfIiGes8,409
|
|
69
|
+
mrok/proxy/event_publisher.py,sha256=TAuwEqIhRYxgazJFgC3DekwUAXlJ2UFjbdx_A9vwA1g,2511
|
|
70
|
+
mrok/proxy/exceptions.py,sha256=61OhdihQNdnBUqAI9mbBkXD1yWg-6Eftk7EhWCU1VF0,642
|
|
71
|
+
mrok/proxy/master.py,sha256=HB2q_nPLim23z0mGDGKs_RshhGVAO8VgOYP4hA__zC4,6891
|
|
72
|
+
mrok/proxy/metrics.py,sha256=Sg2aIiaj9fzkyu190YCsJvNn5P-XLun3BcvuVBsdWbA,3640
|
|
73
|
+
mrok/proxy/middleware.py,sha256=St8r2hY5vfn4q5FtuqeI85tVmSZmC6Vkbecpx_iX6SM,4455
|
|
74
|
+
mrok/proxy/models.py,sha256=CA520bqexPwYUHDrkg58OXAyzBIKQSU9i-SlymU_vt0,5188
|
|
75
|
+
mrok/proxy/stream.py,sha256=7V-bSAF9uNV1yVHKaEhHo95WxafSGWkyHPVABZX0djY,2134
|
|
76
|
+
mrok/proxy/utils.py,sha256=OxX6pJv_Wh_KgWx95YeJ3YeuSgwm2tsd00897P3fxys,2126
|
|
77
|
+
mrok/proxy/worker.py,sha256=uEUC2Hbx0YQiDMFNZfwkHMDnijN96b6iRoIErfI21Tg,1921
|
|
78
|
+
mrok/proxy/ziticorn.py,sha256=YwbyNUK-TL3dANntM6gtlP9Su39QvDCC0dSbuZisXIo,2873
|
|
79
|
+
mrok/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
|
+
mrok/types/proxy.py,sha256=40Yds4tUykMpzsoQbMtHG85r8xtm5Q3fQZ17bp7cDiM,818
|
|
81
|
+
mrok/types/ziti.py,sha256=EeQnTbDEJ-Y-KMS6zu1Xjxb58Up2VxUwqzUwy3H28JY,36
|
|
78
82
|
mrok/ziti/__init__.py,sha256=20OWMiexRhOovZOX19zlX87-V78QyWnEnSZfyAftUdE,263
|
|
79
|
-
mrok/ziti/api.py,sha256=
|
|
80
|
-
mrok/ziti/bootstrap.py,sha256=
|
|
83
|
+
mrok/ziti/api.py,sha256=ikl3l446Gu-YZuJJFRHFSBpKhh5KJxmk-Jr4YbSrqbk,16072
|
|
84
|
+
mrok/ziti/bootstrap.py,sha256=PRZlYDtcGbix-1bQDuJ4wX4dy845VAx9SFPotc4BCeg,2715
|
|
81
85
|
mrok/ziti/constants.py,sha256=Urq1X3bCBQZfw8NbnEa1pqmY4oq1wmzkwPfzam3kbTw,339
|
|
82
86
|
mrok/ziti/errors.py,sha256=yYCbVDwktnR0AYduqtynIjo73K3HOhIrwA_vQimvEd4,368
|
|
83
|
-
mrok/ziti/identities.py,sha256=
|
|
87
|
+
mrok/ziti/identities.py,sha256=9BIBQOirvcdAkRFNqZPTOkC8kvDQbq6EgaQMq22NQsQ,6730
|
|
84
88
|
mrok/ziti/pki.py,sha256=o2tySqHC8-7bvFuI2Tqxg9vX6H6ZSxWxfP_9x29e19M,1954
|
|
85
|
-
mrok/ziti/services.py,sha256=
|
|
86
|
-
mrok-0.
|
|
87
|
-
mrok-0.
|
|
88
|
-
mrok-0.
|
|
89
|
-
mrok-0.
|
|
90
|
-
mrok-0.
|
|
89
|
+
mrok/ziti/services.py,sha256=TukG0vAZxgjbS8OLiyg7u1GwuVeGTco-rb9ne6a4PUA,3213
|
|
90
|
+
mrok-0.6.0.dist-info/METADATA,sha256=0z3llP3xsGv5I-CZANA3tA9rhqNQ9sgH2j2sdLX5z7A,15705
|
|
91
|
+
mrok-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
92
|
+
mrok-0.6.0.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
|
|
93
|
+
mrok-0.6.0.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
|
|
94
|
+
mrok-0.6.0.dist-info/RECORD,,
|
mrok/cli/commands/proxy/run.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import Annotated
|
|
3
|
-
|
|
4
|
-
import typer
|
|
5
|
-
|
|
6
|
-
from mrok import proxy
|
|
7
|
-
from mrok.cli.utils import number_of_workers
|
|
8
|
-
|
|
9
|
-
default_workers = number_of_workers()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def register(app: typer.Typer) -> None:
|
|
13
|
-
@app.command("run")
|
|
14
|
-
def run_proxy(
|
|
15
|
-
ctx: typer.Context,
|
|
16
|
-
identity_file: Path = typer.Argument(
|
|
17
|
-
...,
|
|
18
|
-
help="Identity json file",
|
|
19
|
-
),
|
|
20
|
-
host: Annotated[
|
|
21
|
-
str,
|
|
22
|
-
typer.Option(
|
|
23
|
-
"--host",
|
|
24
|
-
"-h",
|
|
25
|
-
help="Host to bind to. Default: 127.0.0.1",
|
|
26
|
-
show_default=True,
|
|
27
|
-
),
|
|
28
|
-
] = "127.0.0.1",
|
|
29
|
-
port: Annotated[
|
|
30
|
-
int,
|
|
31
|
-
typer.Option(
|
|
32
|
-
"--port",
|
|
33
|
-
"-P",
|
|
34
|
-
help="Port to bind to. Default: 8000",
|
|
35
|
-
show_default=True,
|
|
36
|
-
),
|
|
37
|
-
] = 8000,
|
|
38
|
-
workers: Annotated[
|
|
39
|
-
int,
|
|
40
|
-
typer.Option(
|
|
41
|
-
"--workers",
|
|
42
|
-
"-w",
|
|
43
|
-
help=f"Number of workers. Default: {default_workers}",
|
|
44
|
-
show_default=True,
|
|
45
|
-
),
|
|
46
|
-
] = default_workers,
|
|
47
|
-
):
|
|
48
|
-
"""Run the mrok proxy with Gunicorn and Uvicorn workers."""
|
|
49
|
-
proxy.run(identity_file, host, port, workers)
|
mrok/http/forwarder.py
DELETED
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import asyncio
|
|
3
|
-
import logging
|
|
4
|
-
from contextlib import AbstractAsyncContextManager
|
|
5
|
-
|
|
6
|
-
from mrok.http.types import ASGIReceive, ASGISend, Scope, StreamPair
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger("mrok.proxy")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class BackendSelectionError(Exception):
|
|
12
|
-
def __init__(self, status: int = 500, message: str = "Internal Server Error"):
|
|
13
|
-
self.status = status
|
|
14
|
-
self.message = message
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class InvalidBackendError(BackendSelectionError):
|
|
18
|
-
def __init__(self):
|
|
19
|
-
super().__init__(status=502, message="Bad Gateway")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class BackendUnavailableError(BackendSelectionError):
|
|
23
|
-
def __init__(self):
|
|
24
|
-
super().__init__(status=503, message="Service Unavailable")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ForwardAppBase(abc.ABC):
|
|
28
|
-
"""Generic HTTP forwarder base class.
|
|
29
|
-
|
|
30
|
-
Subclasses must implement `select_backend(scope)` to return an
|
|
31
|
-
(asyncio.StreamReader, asyncio.StreamWriter) pair connected to the
|
|
32
|
-
desired backend. The base class implements the HTTP/1.1 framing
|
|
33
|
-
and streaming logic (requests and responses).
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
read_chunk_size: int = 65536,
|
|
39
|
-
lifespan_timeout: float = 10.0,
|
|
40
|
-
) -> None:
|
|
41
|
-
self._read_chunk_size: int = int(read_chunk_size)
|
|
42
|
-
self._lifespan_timeout = lifespan_timeout
|
|
43
|
-
|
|
44
|
-
async def handle_lifespan(self, receive: ASGIReceive, send: ASGISend) -> None:
|
|
45
|
-
while True:
|
|
46
|
-
event = await receive()
|
|
47
|
-
etype = event.get("type")
|
|
48
|
-
|
|
49
|
-
if etype == "lifespan.startup":
|
|
50
|
-
try:
|
|
51
|
-
await asyncio.wait_for(self.startup(), self._lifespan_timeout)
|
|
52
|
-
except TimeoutError:
|
|
53
|
-
logger.exception("Lifespan startup timed out")
|
|
54
|
-
await send({"type": "lifespan.startup.failed", "message": "startup timeout"})
|
|
55
|
-
continue
|
|
56
|
-
except Exception as e:
|
|
57
|
-
logger.exception("Exception during lifespan startup")
|
|
58
|
-
await send({"type": "lifespan.startup.failed", "message": str(e)})
|
|
59
|
-
continue
|
|
60
|
-
await send({"type": "lifespan.startup.complete"})
|
|
61
|
-
|
|
62
|
-
elif etype == "lifespan.shutdown":
|
|
63
|
-
try:
|
|
64
|
-
await asyncio.wait_for(self.shutdown(), self._lifespan_timeout)
|
|
65
|
-
except TimeoutError:
|
|
66
|
-
logger.exception("Lifespan shutdown timed out")
|
|
67
|
-
await send({"type": "lifespan.shutdown.failed", "message": "shutdown timeout"})
|
|
68
|
-
return
|
|
69
|
-
except Exception as exc:
|
|
70
|
-
logger.exception("Exception during lifespan shutdown")
|
|
71
|
-
await send({"type": "lifespan.shutdown.failed", "message": str(exc)})
|
|
72
|
-
return
|
|
73
|
-
await send({"type": "lifespan.shutdown.complete"})
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
async def startup(self):
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
async def shutdown(self):
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
@abc.abstractmethod
|
|
83
|
-
def select_backend(
|
|
84
|
-
self, scope: Scope, headers: dict[str, str]
|
|
85
|
-
) -> AbstractAsyncContextManager[StreamPair]:
|
|
86
|
-
raise NotImplementedError()
|
|
87
|
-
|
|
88
|
-
async def __call__(self, scope: Scope, receive: ASGIReceive, send: ASGISend) -> None:
|
|
89
|
-
"""ASGI callable entry point.
|
|
90
|
-
|
|
91
|
-
Delegates to smaller helper methods for readability. Subclasses only
|
|
92
|
-
need to implement backend selection.
|
|
93
|
-
"""
|
|
94
|
-
scope_type = scope.get("type")
|
|
95
|
-
if scope_type == "lifespan":
|
|
96
|
-
await self.handle_lifespan(receive, send)
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
if scope.get("type") != "http":
|
|
100
|
-
await send({"type": "http.response.start", "status": 500, "headers": []})
|
|
101
|
-
await send({"type": "http.response.body", "body": b"Unsupported"})
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
method = scope.get("method", "GET")
|
|
105
|
-
path_qs = self.format_path(scope)
|
|
106
|
-
|
|
107
|
-
headers = list(scope.get("headers", []))
|
|
108
|
-
headers = self.ensure_host_header(headers, scope)
|
|
109
|
-
try:
|
|
110
|
-
async with self.select_backend(
|
|
111
|
-
scope, {k[0].decode().lower(): k[1].decode() for k in headers}
|
|
112
|
-
) as (reader, writer):
|
|
113
|
-
if not (reader and writer):
|
|
114
|
-
await send({"type": "http.response.start", "status": 502, "headers": []})
|
|
115
|
-
await send({"type": "http.response.body", "body": b"Bad Gateway"})
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
use_chunked = self.ensure_chunked_if_needed(headers)
|
|
119
|
-
|
|
120
|
-
await self.write_request_line_and_headers(writer, method, path_qs, headers)
|
|
121
|
-
|
|
122
|
-
await self.stream_request_body(receive, writer, use_chunked)
|
|
123
|
-
|
|
124
|
-
status_line = await reader.readline()
|
|
125
|
-
if not status_line:
|
|
126
|
-
await send({"type": "http.response.start", "status": 502, "headers": []})
|
|
127
|
-
await send({"type": "http.response.body", "body": b"Bad Gateway"})
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
status, headers_out, raw_headers = await self.read_status_and_headers(
|
|
131
|
-
reader, status_line
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
await send(
|
|
135
|
-
{"type": "http.response.start", "status": status, "headers": headers_out}
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
await self.stream_response_body(reader, send, raw_headers)
|
|
139
|
-
except BackendSelectionError as bse:
|
|
140
|
-
await send({"type": "http.response.start", "status": bse.status, "headers": []})
|
|
141
|
-
await send({"type": "http.response.body", "body": bse.message.encode()})
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
def format_path(self, scope: Scope) -> str:
|
|
145
|
-
raw_path = scope.get("raw_path")
|
|
146
|
-
if raw_path:
|
|
147
|
-
return raw_path.decode()
|
|
148
|
-
q = scope.get("query_string", b"")
|
|
149
|
-
path = scope.get("path", "/")
|
|
150
|
-
path_qs = path
|
|
151
|
-
if q:
|
|
152
|
-
path_qs += "?" + q.decode()
|
|
153
|
-
return path_qs
|
|
154
|
-
|
|
155
|
-
def ensure_host_header(
|
|
156
|
-
self, headers: list[tuple[bytes, bytes]], scope: Scope
|
|
157
|
-
) -> list[tuple[bytes, bytes]]:
|
|
158
|
-
if any(n.lower() == b"host" for n, _ in headers):
|
|
159
|
-
return headers
|
|
160
|
-
server = scope.get("server")
|
|
161
|
-
if server:
|
|
162
|
-
host = f"{server[0]}:{server[1]}" if server[1] else server[0]
|
|
163
|
-
headers.append((b"host", host.encode()))
|
|
164
|
-
return headers
|
|
165
|
-
|
|
166
|
-
def ensure_chunked_if_needed(self, headers: list[tuple[bytes, bytes]]) -> bool:
|
|
167
|
-
has_content_length = any(n.lower() == b"content-length" for n, _ in headers)
|
|
168
|
-
has_transfer_encoding = any(n.lower() == b"transfer-encoding" for n, _ in headers)
|
|
169
|
-
if not has_content_length and not has_transfer_encoding:
|
|
170
|
-
headers.append((b"transfer-encoding", b"chunked"))
|
|
171
|
-
return True
|
|
172
|
-
return False
|
|
173
|
-
|
|
174
|
-
async def write_request_line_and_headers(
|
|
175
|
-
self,
|
|
176
|
-
writer: asyncio.StreamWriter,
|
|
177
|
-
method: str,
|
|
178
|
-
path_qs: str,
|
|
179
|
-
headers: list[tuple[bytes, bytes]],
|
|
180
|
-
) -> None:
|
|
181
|
-
writer.write(f"{method} {path_qs} HTTP/1.1\r\n".encode())
|
|
182
|
-
for name, value in headers:
|
|
183
|
-
if name.lower() == b"expect":
|
|
184
|
-
continue
|
|
185
|
-
writer.write(name + b": " + value + b"\r\n")
|
|
186
|
-
writer.write(b"\r\n")
|
|
187
|
-
await writer.drain()
|
|
188
|
-
|
|
189
|
-
async def stream_request_body(
|
|
190
|
-
self, receive: ASGIReceive, writer: asyncio.StreamWriter, use_chunked: bool
|
|
191
|
-
) -> None:
|
|
192
|
-
if use_chunked:
|
|
193
|
-
await self.stream_request_chunked(receive, writer)
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
await self.stream_request_until_end(receive, writer)
|
|
197
|
-
|
|
198
|
-
async def stream_request_chunked(
|
|
199
|
-
self, receive: ASGIReceive, writer: asyncio.StreamWriter
|
|
200
|
-
) -> None:
|
|
201
|
-
"""Send request body to backend using HTTP/1.1 chunked encoding."""
|
|
202
|
-
while True:
|
|
203
|
-
event = await receive()
|
|
204
|
-
if event["type"] == "http.request":
|
|
205
|
-
body = event.get("body", b"") or b""
|
|
206
|
-
if body:
|
|
207
|
-
writer.write(f"{len(body):X}\r\n".encode())
|
|
208
|
-
writer.write(body)
|
|
209
|
-
writer.write(b"\r\n")
|
|
210
|
-
await writer.drain()
|
|
211
|
-
if not event.get("more_body", False):
|
|
212
|
-
break
|
|
213
|
-
elif event["type"] == "http.disconnect":
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
writer.write(b"0\r\n\r\n")
|
|
217
|
-
await writer.drain()
|
|
218
|
-
|
|
219
|
-
async def stream_request_until_end(
|
|
220
|
-
self, receive: ASGIReceive, writer: asyncio.StreamWriter
|
|
221
|
-
) -> None:
|
|
222
|
-
"""Send request body to backend when content length/transfer-encoding
|
|
223
|
-
already provided (no chunking).
|
|
224
|
-
"""
|
|
225
|
-
while True:
|
|
226
|
-
event = await receive()
|
|
227
|
-
if event["type"] == "http.request":
|
|
228
|
-
body = event.get("body", b"") or b""
|
|
229
|
-
if body:
|
|
230
|
-
writer.write(body)
|
|
231
|
-
await writer.drain()
|
|
232
|
-
if not event.get("more_body", False):
|
|
233
|
-
break
|
|
234
|
-
elif event["type"] == "http.disconnect":
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
async def read_status_and_headers(
|
|
238
|
-
self, reader: asyncio.StreamReader, first_line: bytes
|
|
239
|
-
) -> tuple[int, list[tuple[bytes, bytes]], dict[bytes, bytes]]:
|
|
240
|
-
parts = first_line.decode(errors="ignore").split(" ", 2)
|
|
241
|
-
status = int(parts[1]) if len(parts) >= 2 and parts[1].isdigit() else 502
|
|
242
|
-
headers: list[tuple[bytes, bytes]] = []
|
|
243
|
-
raw_headers: dict[bytes, bytes] = {}
|
|
244
|
-
while True:
|
|
245
|
-
line = await reader.readline()
|
|
246
|
-
if line in (b"\r\n", b"\n", b""):
|
|
247
|
-
break
|
|
248
|
-
i = line.find(b":")
|
|
249
|
-
if i == -1:
|
|
250
|
-
continue
|
|
251
|
-
name = line[:i].strip().lower()
|
|
252
|
-
value = line[i + 1 :].strip()
|
|
253
|
-
headers.append((name, value))
|
|
254
|
-
raw_headers[name] = value
|
|
255
|
-
|
|
256
|
-
return status, headers, raw_headers
|
|
257
|
-
|
|
258
|
-
def is_chunked(self, te_value: bytes) -> bool:
|
|
259
|
-
"""Return True if transfer-encoding header tokens include 'chunked'."""
|
|
260
|
-
if not te_value:
|
|
261
|
-
return False
|
|
262
|
-
# split on commas, strip spaces and check tokens
|
|
263
|
-
tokens = [t.strip() for t in te_value.split(b",")]
|
|
264
|
-
return any(t.lower() == b"chunked" for t in tokens)
|
|
265
|
-
|
|
266
|
-
def parse_content_length(self, cl_value: bytes | None) -> int | None:
|
|
267
|
-
"""Parse Content-Length header value to int, or return None if invalid."""
|
|
268
|
-
if cl_value is None:
|
|
269
|
-
return None
|
|
270
|
-
try:
|
|
271
|
-
return int(cl_value)
|
|
272
|
-
except Exception:
|
|
273
|
-
return None
|
|
274
|
-
|
|
275
|
-
async def drain_trailers(self, reader: asyncio.StreamReader) -> None:
|
|
276
|
-
"""Consume trailer header lines until an empty line is encountered."""
|
|
277
|
-
while True:
|
|
278
|
-
trailer = await reader.readline()
|
|
279
|
-
if trailer in (b"\r\n", b"\n", b""):
|
|
280
|
-
break
|
|
281
|
-
|
|
282
|
-
async def stream_response_chunked(self, reader: asyncio.StreamReader, send: ASGISend) -> None:
|
|
283
|
-
"""Read chunked-encoded response from reader, decode and forward to ASGI send."""
|
|
284
|
-
while True:
|
|
285
|
-
size_line = await reader.readline()
|
|
286
|
-
if not size_line:
|
|
287
|
-
break
|
|
288
|
-
size_str = size_line.split(b";", 1)[0].strip()
|
|
289
|
-
try:
|
|
290
|
-
size = int(size_str, 16)
|
|
291
|
-
except Exception:
|
|
292
|
-
break
|
|
293
|
-
if size == 0:
|
|
294
|
-
# consume trailers
|
|
295
|
-
await self.drain_trailers(reader)
|
|
296
|
-
break
|
|
297
|
-
try:
|
|
298
|
-
chunk = await reader.readexactly(size)
|
|
299
|
-
except Exception:
|
|
300
|
-
break
|
|
301
|
-
# consume the CRLF after the chunk
|
|
302
|
-
try:
|
|
303
|
-
await reader.readexactly(2)
|
|
304
|
-
except Exception:
|
|
305
|
-
logger.warning("failed to read CRLF after chunk from backend")
|
|
306
|
-
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
307
|
-
|
|
308
|
-
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
|
309
|
-
|
|
310
|
-
async def stream_response_with_content_length(
|
|
311
|
-
self, reader: asyncio.StreamReader, send: ASGISend, content_length: int
|
|
312
|
-
) -> None:
|
|
313
|
-
"""Read exactly content_length bytes and forward to ASGI send events."""
|
|
314
|
-
remaining = content_length
|
|
315
|
-
sent_final = False
|
|
316
|
-
while remaining > 0:
|
|
317
|
-
to_read = min(self._read_chunk_size, remaining)
|
|
318
|
-
chunk = await reader.read(to_read)
|
|
319
|
-
if not chunk:
|
|
320
|
-
break
|
|
321
|
-
remaining -= len(chunk)
|
|
322
|
-
more = remaining > 0
|
|
323
|
-
await send({"type": "http.response.body", "body": chunk, "more_body": more})
|
|
324
|
-
if not more:
|
|
325
|
-
sent_final = True
|
|
326
|
-
|
|
327
|
-
if not sent_final:
|
|
328
|
-
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
|
329
|
-
|
|
330
|
-
async def stream_response_until_eof(self, reader: asyncio.StreamReader, send: ASGISend) -> None:
|
|
331
|
-
"""Read from reader until EOF and forward chunks to ASGI send events."""
|
|
332
|
-
while True:
|
|
333
|
-
chunk = await reader.read(self._read_chunk_size)
|
|
334
|
-
if not chunk:
|
|
335
|
-
break
|
|
336
|
-
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
337
|
-
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
|
338
|
-
|
|
339
|
-
async def stream_response_body(
|
|
340
|
-
self, reader: asyncio.StreamReader, send: ASGISend, raw_headers: dict[bytes, bytes]
|
|
341
|
-
) -> None:
|
|
342
|
-
te = raw_headers.get(b"transfer-encoding", b"").lower()
|
|
343
|
-
cl = raw_headers.get(b"content-length")
|
|
344
|
-
|
|
345
|
-
if self.is_chunked(te):
|
|
346
|
-
await self.stream_response_chunked(reader, send)
|
|
347
|
-
return
|
|
348
|
-
|
|
349
|
-
content_length = self.parse_content_length(cl)
|
|
350
|
-
if content_length is not None:
|
|
351
|
-
await self.stream_response_with_content_length(reader, send, content_length)
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
await self.stream_response_until_eof(reader, send)
|
mrok/http/lifespan.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from collections.abc import Awaitable, Callable
|
|
3
|
-
|
|
4
|
-
from uvicorn.config import Config
|
|
5
|
-
from uvicorn.lifespan.on import LifespanOn
|
|
6
|
-
|
|
7
|
-
AsyncCallback = Callable[[], Awaitable[None]]
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class MrokLifespan(LifespanOn):
|
|
11
|
-
def __init__(self, config: Config) -> None:
|
|
12
|
-
super().__init__(config)
|
|
13
|
-
self.logger = logging.getLogger("mrok.proxy")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class LifespanWrapper:
|
|
17
|
-
def __init__(
|
|
18
|
-
self, app, on_startup: AsyncCallback | None = None, on_shutdown: AsyncCallback | None = None
|
|
19
|
-
):
|
|
20
|
-
self.app = app
|
|
21
|
-
self.on_startup = on_startup
|
|
22
|
-
self.on_shutdown = on_shutdown
|
|
23
|
-
|
|
24
|
-
async def __call__(self, scope, receive, send):
|
|
25
|
-
if scope["type"] == "lifespan":
|
|
26
|
-
while True:
|
|
27
|
-
event = await receive()
|
|
28
|
-
if event["type"] == "lifespan.startup":
|
|
29
|
-
if self.on_startup:
|
|
30
|
-
await self.on_startup()
|
|
31
|
-
await send({"type": "lifespan.startup.complete"})
|
|
32
|
-
|
|
33
|
-
elif event["type"] == "lifespan.shutdown":
|
|
34
|
-
if self.on_shutdown:
|
|
35
|
-
await self.on_shutdown()
|
|
36
|
-
await send({"type": "lifespan.shutdown.complete"})
|
|
37
|
-
break
|
|
38
|
-
else:
|
|
39
|
-
await self.app(scope, receive, send)
|