mrok 0.4.6__py3-none-any.whl → 0.5.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.
Files changed (38) hide show
  1. mrok/agent/devtools/inspector/app.py +2 -2
  2. mrok/agent/sidecar/app.py +61 -35
  3. mrok/agent/sidecar/main.py +5 -5
  4. mrok/agent/ziticorn.py +2 -2
  5. mrok/cli/commands/__init__.py +2 -2
  6. mrok/cli/commands/{proxy → frontend}/__init__.py +1 -1
  7. mrok/cli/commands/{proxy → frontend}/run.py +4 -4
  8. mrok/constants.py +0 -2
  9. mrok/controller/openapi/examples.py +13 -0
  10. mrok/frontend/__init__.py +3 -0
  11. mrok/frontend/app.py +75 -0
  12. mrok/{proxy → frontend}/main.py +4 -10
  13. mrok/proxy/__init__.py +0 -3
  14. mrok/proxy/app.py +157 -83
  15. mrok/proxy/backend.py +43 -0
  16. mrok/{http → proxy}/config.py +3 -3
  17. mrok/{datastructures.py → proxy/datastructures.py} +43 -10
  18. mrok/proxy/exceptions.py +22 -0
  19. mrok/proxy/lifespan.py +10 -0
  20. mrok/{master.py → proxy/master.py} +35 -38
  21. mrok/{metrics.py → proxy/metrics.py} +37 -49
  22. mrok/{http → proxy}/middlewares.py +47 -26
  23. mrok/proxy/streams.py +45 -0
  24. mrok/proxy/types.py +15 -0
  25. mrok/{http → proxy}/utils.py +1 -1
  26. {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/METADATA +2 -5
  27. {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/RECORD +33 -31
  28. mrok/http/__init__.py +0 -0
  29. mrok/http/forwarder.py +0 -354
  30. mrok/http/lifespan.py +0 -39
  31. mrok/http/pool.py +0 -239
  32. mrok/http/types.py +0 -18
  33. /mrok/{http → proxy}/constants.py +0 -0
  34. /mrok/{http → proxy}/protocol.py +0 -0
  35. /mrok/{http → proxy}/server.py +0 -0
  36. {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/WHEEL +0 -0
  37. {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/entry_points.txt +0 -0
  38. {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mrok
3
- Version: 0.4.6
3
+ Version: 0.5.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: httptools<0.8.0,>=0.7.1
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,27 +1,24 @@
1
1
  mrok/__init__.py,sha256=D1PUs3KtMCqG4bFLceVNG62L3RN53NS95uSCNXpgvzs,181
2
2
  mrok/conf.py,sha256=_5Z-A5LyojQeY8J7W8C0QidsmrPl99r9qKYEoMf4kcI,840
3
- mrok/constants.py,sha256=65OlmploxfND686E4mt9LR9MqYn8I5k-L0H-R5KsLG8,201
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=marXGcr6CbDdiNi8V3CZJhg8YCUbKw6ySuO3-0-zf8g,900
7
+ mrok/agent/ziticorn.py,sha256=iENhronSKhPIhXhTMj7pElaLZgeEsgEVTKp_1yMa3Dk,907
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=_pzxemMqIunE5EdMq5amjqpOGsMWIOw17GgiCtRAi6Q,16464
12
+ mrok/agent/devtools/inspector/app.py,sha256=E1WKcc5k7afq6xq_efEpOk9g5CaXmYjU1kNACZrYYyg,16486
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=YOQLwPPqcElbF2kU15bcw-ePzZM09eVJQZ6Z5NYg9u8,1509
19
- mrok/agent/sidecar/main.py,sha256=h31wynUCcFmRckvqLHtH97w1QgMv4fzcmYjhRPUobxY,1076
15
+ mrok/agent/sidecar/app.py,sha256=8gLB9tgxzC4l6CBYB9MW9A6-v2cjJHx-xVNwAnCD7hI,2506
16
+ mrok/agent/sidecar/main.py,sha256=m3aKhOHwIEsXDeTMynHmv41JnBIjv-QUgQ4uw1rrsDA,1068
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=jihISOj3ZZQ_dn0rogXrJOx6b283KLRUfTw9USQgAhI,134
21
+ mrok/cli/commands/__init__.py,sha256=-UOGzh38oWX7fPeI2nc5I9z8LylRdQAt868q4G6rNGk,140
25
22
  mrok/cli/commands/admin/__init__.py,sha256=WU49jpMF9p18UONjYywWEFzjF57zLpLKJ0qAZvrzcR4,414
26
23
  mrok/cli/commands/admin/bootstrap.py,sha256=iOnHctYajgcHrG_Idjn5Y7VVSaWYRIhdgqKSw9TWq9I,1680
27
24
  mrok/cli/commands/admin/utils.py,sha256=wQ-qQJGFyhikMJY_CWT-G6sTEIZb-LUdj1AUZisLPBw,1363
@@ -45,8 +42,8 @@ mrok/cli/commands/agent/run/sidecar.py,sha256=Tj5inAeSX1E3yCVs2q4P3sP3trvvwk2lYM
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/proxy/__init__.py,sha256=Y9oluemsuWSaykDniLVsI2cyTurcEO3_GJDHgf-7BdU,120
49
- mrok/cli/commands/proxy/run.py,sha256=QzKAjNCib-SS8IrGGHOxDjTtgTQxfeeeqmI3LaIkiLo,1293
45
+ mrok/cli/commands/frontend/__init__.py,sha256=0kK37yG6qs7yAa8TYlKZUA-nHrWsO4y5CjbVkXafnuk,123
46
+ mrok/cli/commands/frontend/run.py,sha256=4w9B662_7ASWuN_skWWWXXQyTcp0G4I-FzvfjL7Yiu0,1305
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
@@ -56,25 +53,30 @@ mrok/controller/dependencies/__init__.py,sha256=voewk6gjkA0OarL6HFmfT_RLqBns0Fpl
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=ZI0BP7L6sI0z7Mq1I3uc2UrweGpzpPeGSIuf1bUKkgg,1419
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/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
- mrok/http/config.py,sha256=k73-4hBo6jag1RpyZagJLtTCL6EQoebZaX8Vv-CMN_k,2050
66
- mrok/http/constants.py,sha256=ao5gI2HFBWmrdd2Yc6XFK_RGaHk-omxI4AqvfIiGes8,409
67
- mrok/http/forwarder.py,sha256=vAf2nh6Fmr07JdRJkK4dPHKJilP9PnsYZcroqsnilB8,13751
68
- mrok/http/lifespan.py,sha256=UdbOqjWZsHzJJjX0CTd2hY96Jpk5QWtdHJEzPG6Z4hQ,1288
69
- mrok/http/middlewares.py,sha256=SGo4EwhTId2uJx1aMuqGbNy7MXgZlDEdZI0buzBYVv0,5011
70
- mrok/http/pool.py,sha256=Q-pRwgYPusqEKQCwZsRQ2mnGaDfyWknWpvydUu5KtEU,7696
71
- mrok/http/protocol.py,sha256=ap8jbLUvgbAH81ZJZCBkQiYR7mkV_eL3rpfwEkoE8sU,392
72
- mrok/http/server.py,sha256=Mj7C85fc-DXp-WTBWaOd7ag808oliLmFBH5bf-G2FHg,370
73
- mrok/http/types.py,sha256=A82zloEqW8KdKahdNrbW5fhlJNUo2enLNRVMWIJTatA,632
74
- mrok/http/utils.py,sha256=sOixYu3R9-nNoMFYdifrreYvcFRIHYVtb6AAmtVzaLE,2125
75
- mrok/proxy/__init__.py,sha256=vWXyImroqM1Eq8e_oFPBup8VJ3reyp8SVjFTbLzRkI8,51
76
- mrok/proxy/app.py,sha256=VvMRmYLwsItjCcecy6ccrkk564LnArIermHTRVDxh9U,3469
77
- mrok/proxy/main.py,sha256=ZXpticE6J4FABaslDB_8J5qklPsf3e7xIFSZmcPAAjQ,1588
61
+ mrok/frontend/__init__.py,sha256=SN3LoFwAye18lfJ8OKNNS-7kLc2A9OxPGIEIEYYtAOA,54
62
+ mrok/frontend/app.py,sha256=We7z5xrbxvOuLsmWgsqfoECZ8FTWOtlw80a14g_8p9M,2632
63
+ mrok/frontend/main.py,sha256=kaje2kfS902TNQwwKn12WEr4N7hKHPg_NwczXAdplwY,1445
64
+ mrok/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
+ mrok/proxy/app.py,sha256=mqb07evKlGUzgqEy-LMzjyeIg1VjUFu83pZeBA4zVyM,5976
66
+ mrok/proxy/backend.py,sha256=T8DDA733CqbFWp5lrf_tKQK_VDgcERKTfesvxofl3AY,1493
67
+ mrok/proxy/config.py,sha256=aUTx0-dNAT7U0UEp5bX7-IO7p3HS082QUocAVq2NUWk,2053
68
+ mrok/proxy/constants.py,sha256=ao5gI2HFBWmrdd2Yc6XFK_RGaHk-omxI4AqvfIiGes8,409
69
+ mrok/proxy/datastructures.py,sha256=SDwhWpJTJrwK9hamHb_Og5PxKXG6VGOwVeoPxtaQLnU,5173
70
+ mrok/proxy/exceptions.py,sha256=61OhdihQNdnBUqAI9mbBkXD1yWg-6Eftk7EhWCU1VF0,642
71
+ mrok/proxy/lifespan.py,sha256=9qevhD_5Y0f8fGTh2axdfWx7v1K4vnWtiUNyJLesOHE,262
72
+ mrok/proxy/master.py,sha256=Houudf2eYZkP5Ri8JtmFeIlWtMuTvybpPaMeIPNyLRg,8484
73
+ mrok/proxy/metrics.py,sha256=NPhEbHoRdZ7YLKJPxmLc-sCFmos_L0iZwO0ufUs4SNg,3654
74
+ mrok/proxy/middlewares.py,sha256=XHOSQPCXAqUWU2lg1iNkIfTWcfCOJrhEJ_bhVpX-ToQ,5504
75
+ mrok/proxy/protocol.py,sha256=ap8jbLUvgbAH81ZJZCBkQiYR7mkV_eL3rpfwEkoE8sU,392
76
+ mrok/proxy/server.py,sha256=Mj7C85fc-DXp-WTBWaOd7ag808oliLmFBH5bf-G2FHg,370
77
+ mrok/proxy/streams.py,sha256=KxiEFiyAWlFQ7t-t8PZHfDveysBk4Wjo2La-aL7XghQ,1371
78
+ mrok/proxy/types.py,sha256=oET5CXqcBvk4kf_giNojZ9kfF5wygy3Z-MvcDd38LMU,555
79
+ mrok/proxy/utils.py,sha256=OxX6pJv_Wh_KgWx95YeJ3YeuSgwm2tsd00897P3fxys,2126
78
80
  mrok/ziti/__init__.py,sha256=20OWMiexRhOovZOX19zlX87-V78QyWnEnSZfyAftUdE,263
79
81
  mrok/ziti/api.py,sha256=KvGiT9d4oSgC3JbFWLDQyuHcLX2HuZJoJ8nHmWtCDkY,16154
80
82
  mrok/ziti/bootstrap.py,sha256=QIDhlkIxPW2QRuumFq2D1WDbD003P5f3z24pAUsyeBI,2696
@@ -83,8 +85,8 @@ mrok/ziti/errors.py,sha256=yYCbVDwktnR0AYduqtynIjo73K3HOhIrwA_vQimvEd4,368
83
85
  mrok/ziti/identities.py,sha256=1BcwfqAJHMBhc3vRaf0aLaIkoHskj5Xe2Lsq2lO9Vs8,6735
84
86
  mrok/ziti/pki.py,sha256=o2tySqHC8-7bvFuI2Tqxg9vX6H6ZSxWxfP_9x29e19M,1954
85
87
  mrok/ziti/services.py,sha256=zR1PEBYwXVou20iJK4euh0ZZFAo9UB8PZk8f6SDmiUE,3194
86
- mrok-0.4.6.dist-info/METADATA,sha256=Io64noW9WGLw9asC4xjeuLS7Wh8bFefufJmTjUK8Syo,15836
87
- mrok-0.4.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
88
- mrok-0.4.6.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
89
- mrok-0.4.6.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
90
- mrok-0.4.6.dist-info/RECORD,,
88
+ mrok-0.5.0.dist-info/METADATA,sha256=-rLLRvaOAveuVDJR_ZLNWohgU9EdIM1jgrnH6LoqgHc,15705
89
+ mrok-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
90
+ mrok-0.5.0.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
91
+ mrok-0.5.0.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
92
+ mrok-0.5.0.dist-info/RECORD,,
mrok/http/__init__.py DELETED
File without changes
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)