mrok 0.3.0__py3-none-any.whl → 0.4.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/__init__.py +0 -0
- mrok/agent/devtools/__main__.py +34 -0
- mrok/agent/devtools/inspector/__init__.py +0 -0
- mrok/agent/devtools/inspector/__main__.py +25 -0
- mrok/agent/devtools/inspector/app.py +556 -0
- mrok/agent/devtools/inspector/server.py +18 -0
- mrok/agent/sidecar/app.py +9 -10
- mrok/agent/sidecar/main.py +35 -16
- mrok/agent/ziticorn.py +27 -18
- mrok/cli/commands/__init__.py +2 -1
- mrok/cli/commands/agent/__init__.py +2 -0
- mrok/cli/commands/agent/dev/__init__.py +7 -0
- mrok/cli/commands/agent/dev/console.py +25 -0
- mrok/cli/commands/agent/dev/web.py +37 -0
- mrok/cli/commands/agent/run/asgi.py +35 -16
- mrok/cli/commands/agent/run/sidecar.py +29 -13
- mrok/cli/commands/agent/utils.py +5 -0
- mrok/cli/commands/controller/run.py +1 -5
- mrok/cli/commands/proxy/__init__.py +6 -0
- mrok/cli/commands/proxy/run.py +49 -0
- mrok/cli/utils.py +5 -0
- mrok/conf.py +6 -0
- mrok/controller/auth.py +2 -2
- mrok/datastructures.py +159 -0
- mrok/http/config.py +3 -6
- mrok/http/constants.py +22 -0
- mrok/http/forwarder.py +62 -23
- mrok/http/lifespan.py +29 -0
- mrok/http/middlewares.py +143 -0
- mrok/http/types.py +43 -0
- mrok/http/utils.py +90 -0
- mrok/logging.py +22 -0
- mrok/master.py +269 -0
- mrok/metrics.py +139 -0
- mrok/proxy/__init__.py +3 -0
- mrok/proxy/app.py +73 -0
- mrok/proxy/dataclasses.py +12 -0
- mrok/proxy/main.py +58 -0
- mrok/proxy/streams.py +124 -0
- mrok/proxy/types.py +12 -0
- mrok/proxy/ziti.py +173 -0
- {mrok-0.3.0.dist-info → mrok-0.4.0.dist-info}/METADATA +7 -1
- {mrok-0.3.0.dist-info → mrok-0.4.0.dist-info}/RECORD +46 -20
- mrok/http/master.py +0 -132
- {mrok-0.3.0.dist-info → mrok-0.4.0.dist-info}/WHEEL +0 -0
- {mrok-0.3.0.dist-info → mrok-0.4.0.dist-info}/entry_points.txt +0 -0
- {mrok-0.3.0.dist-info → mrok-0.4.0.dist-info}/licenses/LICENSE.txt +0 -0
mrok/agent/sidecar/main.py
CHANGED
|
@@ -1,27 +1,46 @@
|
|
|
1
|
-
import
|
|
2
|
-
import contextlib
|
|
3
|
-
from functools import partial
|
|
1
|
+
import logging
|
|
4
2
|
from pathlib import Path
|
|
5
3
|
|
|
6
4
|
from mrok.agent.sidecar.app import ForwardApp
|
|
7
|
-
from mrok.
|
|
8
|
-
from mrok.http.master import Master
|
|
9
|
-
from mrok.http.server import MrokServer
|
|
5
|
+
from mrok.master import MasterBase
|
|
10
6
|
|
|
7
|
+
logger = logging.getLogger("mrok.proxy")
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
|
|
10
|
+
class SidecarAgent(MasterBase):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
identity_file: str,
|
|
14
|
+
target_addr: str | Path | tuple[str, int],
|
|
15
|
+
workers: int = 4,
|
|
16
|
+
publishers_port: int = 50000,
|
|
17
|
+
subscribers_port: int = 50001,
|
|
18
|
+
):
|
|
19
|
+
super().__init__(
|
|
20
|
+
identity_file,
|
|
21
|
+
workers,
|
|
22
|
+
False,
|
|
23
|
+
publishers_port,
|
|
24
|
+
subscribers_port,
|
|
25
|
+
)
|
|
26
|
+
self.target_address = target_addr
|
|
27
|
+
|
|
28
|
+
def get_asgi_app(self):
|
|
29
|
+
return ForwardApp(self.target_address)
|
|
17
30
|
|
|
18
31
|
|
|
19
32
|
def run(
|
|
20
33
|
identity_file: str,
|
|
21
34
|
target_addr: str | Path | tuple[str, int],
|
|
22
|
-
workers=4,
|
|
23
|
-
|
|
35
|
+
workers: int = 4,
|
|
36
|
+
publishers_port: int = 50000,
|
|
37
|
+
subscribers_port: int = 50001,
|
|
24
38
|
):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
agent = SidecarAgent(
|
|
40
|
+
identity_file,
|
|
41
|
+
target_addr,
|
|
42
|
+
workers=workers,
|
|
43
|
+
publishers_port=publishers_port,
|
|
44
|
+
subscribers_port=subscribers_port,
|
|
45
|
+
)
|
|
46
|
+
agent.run()
|
mrok/agent/ziticorn.py
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import os
|
|
4
|
-
from functools import partial
|
|
1
|
+
from mrok.http.types import ASGIApp
|
|
2
|
+
from mrok.master import MasterBase
|
|
5
3
|
|
|
6
|
-
from mrok.http.config import ASGIApplication, MrokBackendConfig
|
|
7
|
-
from mrok.http.master import Master
|
|
8
|
-
from mrok.http.server import MrokServer
|
|
9
4
|
|
|
5
|
+
class ZiticornAgent(MasterBase):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
app: ASGIApp | str,
|
|
9
|
+
identity_file: str,
|
|
10
|
+
workers: int = 4,
|
|
11
|
+
reload: bool = False,
|
|
12
|
+
publishers_port: int = 50000,
|
|
13
|
+
subscribers_port: int = 50001,
|
|
14
|
+
):
|
|
15
|
+
super().__init__(identity_file, workers, reload, publishers_port, subscribers_port)
|
|
16
|
+
self.app = app
|
|
10
17
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
sys.path.insert(0, os.getcwd())
|
|
15
|
-
config = MrokBackendConfig(app, identity_file)
|
|
16
|
-
server = MrokServer(config)
|
|
17
|
-
with contextlib.suppress(KeyboardInterrupt, asyncio.CancelledError):
|
|
18
|
-
server.run()
|
|
18
|
+
def get_asgi_app(self):
|
|
19
|
+
return self.app
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def run(
|
|
22
|
-
app:
|
|
23
|
+
app: ASGIApp | str,
|
|
23
24
|
identity_file: str,
|
|
24
25
|
workers: int = 4,
|
|
25
26
|
reload: bool = False,
|
|
27
|
+
publishers_port: int = 50000,
|
|
28
|
+
subscribers_port: int = 50001,
|
|
26
29
|
):
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
master = ZiticornAgent(
|
|
31
|
+
app,
|
|
32
|
+
identity_file,
|
|
33
|
+
workers=workers,
|
|
34
|
+
reload=reload,
|
|
35
|
+
publishers_port=publishers_port,
|
|
36
|
+
subscribers_port=subscribers_port,
|
|
37
|
+
)
|
|
29
38
|
master.run()
|
mrok/cli/commands/__init__.py
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from mrok.agent.devtools.inspector.app import InspectorApp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(app: typer.Typer) -> None:
|
|
9
|
+
@app.command("console")
|
|
10
|
+
def run_dev_console(
|
|
11
|
+
subscriber_port: Annotated[
|
|
12
|
+
int,
|
|
13
|
+
typer.Option(
|
|
14
|
+
"--subscriber-port",
|
|
15
|
+
"-s",
|
|
16
|
+
help=(
|
|
17
|
+
"TCP port where the mrok inspector console application "
|
|
18
|
+
"should connect to subscribe to request/response messages."
|
|
19
|
+
),
|
|
20
|
+
show_default=True,
|
|
21
|
+
),
|
|
22
|
+
] = 50001,
|
|
23
|
+
):
|
|
24
|
+
app = InspectorApp(subscriber_port)
|
|
25
|
+
app.run()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from mrok.agent.devtools.inspector.server import InspectorServer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(app: typer.Typer) -> None:
|
|
9
|
+
@app.command("web")
|
|
10
|
+
def run_web_console(
|
|
11
|
+
port: Annotated[
|
|
12
|
+
int,
|
|
13
|
+
typer.Option(
|
|
14
|
+
"--port",
|
|
15
|
+
"-p",
|
|
16
|
+
help="TCP port where the mrok inspector web application will listen for requests.",
|
|
17
|
+
show_default=True,
|
|
18
|
+
),
|
|
19
|
+
] = 7777,
|
|
20
|
+
subscriber_port: Annotated[
|
|
21
|
+
int,
|
|
22
|
+
typer.Option(
|
|
23
|
+
"--subscriber-port",
|
|
24
|
+
"-s",
|
|
25
|
+
help=(
|
|
26
|
+
"TCP port where the mrok inspector web application "
|
|
27
|
+
"should connect to subscribe to request/response messages."
|
|
28
|
+
),
|
|
29
|
+
show_default=True,
|
|
30
|
+
),
|
|
31
|
+
] = 50001,
|
|
32
|
+
):
|
|
33
|
+
server = InspectorServer(
|
|
34
|
+
port=port,
|
|
35
|
+
subscriber_port=subscriber_port,
|
|
36
|
+
)
|
|
37
|
+
server.serve()
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import multiprocessing
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
from typing import Annotated
|
|
4
3
|
|
|
5
4
|
import typer
|
|
6
5
|
|
|
7
|
-
# from app.logging import get_logging_config
|
|
8
6
|
from mrok.agent import ziticorn
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def number_of_workers():
|
|
12
|
-
return (multiprocessing.cpu_count() * 2) + 1
|
|
13
|
-
|
|
7
|
+
from mrok.cli.utils import number_of_workers
|
|
14
8
|
|
|
15
9
|
default_workers = number_of_workers()
|
|
16
10
|
|
|
@@ -18,14 +12,8 @@ default_workers = number_of_workers()
|
|
|
18
12
|
def register(app: typer.Typer) -> None:
|
|
19
13
|
@app.command("asgi")
|
|
20
14
|
def run_asgi(
|
|
21
|
-
app: str
|
|
22
|
-
|
|
23
|
-
help="ASGI application",
|
|
24
|
-
),
|
|
25
|
-
identity_file: Path = typer.Argument(
|
|
26
|
-
...,
|
|
27
|
-
help="Identity json file",
|
|
28
|
-
),
|
|
15
|
+
app: Annotated[str, typer.Argument(..., help="ASGI application")],
|
|
16
|
+
identity_file: Annotated[Path, typer.Argument(..., help="Identity json file")],
|
|
29
17
|
workers: Annotated[
|
|
30
18
|
int,
|
|
31
19
|
typer.Option(
|
|
@@ -44,6 +32,37 @@ def register(app: typer.Typer) -> None:
|
|
|
44
32
|
show_default=True,
|
|
45
33
|
),
|
|
46
34
|
] = False,
|
|
35
|
+
publishers_port: Annotated[
|
|
36
|
+
int,
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--publishers-port",
|
|
39
|
+
"-p",
|
|
40
|
+
help=(
|
|
41
|
+
"TCP port where the mrok agent "
|
|
42
|
+
"should connect to publish to request/response messages."
|
|
43
|
+
),
|
|
44
|
+
show_default=True,
|
|
45
|
+
),
|
|
46
|
+
] = 50000,
|
|
47
|
+
subscribers_port: Annotated[
|
|
48
|
+
int,
|
|
49
|
+
typer.Option(
|
|
50
|
+
"--subscribers-port",
|
|
51
|
+
"-s",
|
|
52
|
+
help=(
|
|
53
|
+
"TCP port where the mrok agent should listen for incoming subscribers "
|
|
54
|
+
"connections for request/response messages."
|
|
55
|
+
),
|
|
56
|
+
show_default=True,
|
|
57
|
+
),
|
|
58
|
+
] = 50001,
|
|
47
59
|
):
|
|
48
60
|
"""Run an ASGI application exposing it through OpenZiti network."""
|
|
49
|
-
ziticorn.run(
|
|
61
|
+
ziticorn.run(
|
|
62
|
+
app,
|
|
63
|
+
str(identity_file),
|
|
64
|
+
workers=workers,
|
|
65
|
+
reload=reload,
|
|
66
|
+
publishers_port=publishers_port,
|
|
67
|
+
subscribers_port=subscribers_port,
|
|
68
|
+
)
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import multiprocessing
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
from typing import Annotated
|
|
4
3
|
|
|
5
4
|
import typer
|
|
6
5
|
|
|
7
6
|
from mrok.agent import sidecar
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def number_of_workers():
|
|
11
|
-
return (multiprocessing.cpu_count() * 2) + 1
|
|
12
|
-
|
|
7
|
+
from mrok.cli.utils import number_of_workers
|
|
13
8
|
|
|
14
9
|
default_workers = number_of_workers()
|
|
15
10
|
|
|
@@ -34,15 +29,30 @@ def register(app: typer.Typer) -> None:
|
|
|
34
29
|
show_default=True,
|
|
35
30
|
),
|
|
36
31
|
] = default_workers,
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
publishers_port: Annotated[
|
|
33
|
+
int,
|
|
34
|
+
typer.Option(
|
|
35
|
+
"--publishers-port",
|
|
36
|
+
"-p",
|
|
37
|
+
help=(
|
|
38
|
+
"TCP port where the mrok agent "
|
|
39
|
+
"should connect to publish to request/response messages."
|
|
40
|
+
),
|
|
41
|
+
show_default=True,
|
|
42
|
+
),
|
|
43
|
+
] = 50000,
|
|
44
|
+
subscribers_port: Annotated[
|
|
45
|
+
int,
|
|
39
46
|
typer.Option(
|
|
40
|
-
"--
|
|
41
|
-
"-
|
|
42
|
-
help=
|
|
47
|
+
"--subscribers-port",
|
|
48
|
+
"-s",
|
|
49
|
+
help=(
|
|
50
|
+
"TCP port where the mrok agent should listen for incoming subscribers "
|
|
51
|
+
"connections for request/response messages."
|
|
52
|
+
),
|
|
43
53
|
show_default=True,
|
|
44
54
|
),
|
|
45
|
-
] =
|
|
55
|
+
] = 50001,
|
|
46
56
|
):
|
|
47
57
|
"""Run a Sidecar Proxy to expose a web application through OpenZiti."""
|
|
48
58
|
if ":" in str(target):
|
|
@@ -51,4 +61,10 @@ def register(app: typer.Typer) -> None:
|
|
|
51
61
|
else:
|
|
52
62
|
target_addr = str(target) # type: ignore
|
|
53
63
|
|
|
54
|
-
sidecar.run(
|
|
64
|
+
sidecar.run(
|
|
65
|
+
str(identity_file),
|
|
66
|
+
target_addr,
|
|
67
|
+
workers=workers,
|
|
68
|
+
publishers_port=publishers_port,
|
|
69
|
+
subscribers_port=subscribers_port,
|
|
70
|
+
)
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import multiprocessing
|
|
2
1
|
from collections.abc import Callable
|
|
3
2
|
from typing import Annotated, Any
|
|
4
3
|
|
|
5
4
|
import typer
|
|
6
5
|
from gunicorn.app.base import BaseApplication
|
|
7
6
|
|
|
7
|
+
from mrok.cli.utils import number_of_workers
|
|
8
8
|
from mrok.controller.app import app as asgi_app
|
|
9
9
|
from mrok.logging import get_logging_config
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def number_of_workers():
|
|
13
|
-
return (multiprocessing.cpu_count() * 2) + 1
|
|
14
|
-
|
|
15
|
-
|
|
16
12
|
class StandaloneApplication(BaseApplication): # pragma: no cover
|
|
17
13
|
def __init__(self, application: Callable, options: dict[str, Any] | None = None):
|
|
18
14
|
self.options = options or {}
|
|
@@ -0,0 +1,49 @@
|
|
|
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/cli/utils.py
ADDED
mrok/conf.py
CHANGED
|
@@ -15,6 +15,12 @@ DEFAULT_SETTINGS = {
|
|
|
15
15
|
"ssl_verify": False,
|
|
16
16
|
},
|
|
17
17
|
"PAGINATION": {"limit": 50},
|
|
18
|
+
"SIDECAR": {
|
|
19
|
+
"textual_port": 4040,
|
|
20
|
+
"store_port": 5051,
|
|
21
|
+
"store_size": 1000,
|
|
22
|
+
"textual_command": "python mrok/agent/sidecar/inspector.py",
|
|
23
|
+
},
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
_settings = None
|
mrok/controller/auth.py
CHANGED
|
@@ -80,8 +80,8 @@ async def authenticate(
|
|
|
80
80
|
)
|
|
81
81
|
return payload
|
|
82
82
|
except jwt.InvalidKeyError as e:
|
|
83
|
-
logger.error(f"Invalid jwt token: {e}")
|
|
83
|
+
logger.error(f"Invalid jwt token: {e} ({credentials.credentials})")
|
|
84
84
|
raise UNAUTHORIZED_EXCEPTION
|
|
85
85
|
except jwt.InvalidTokenError as e:
|
|
86
|
-
logger.error(f"Invalid jwt token: {e}")
|
|
86
|
+
logger.error(f"Invalid jwt token: {e} ({credentials.credentials})")
|
|
87
87
|
raise UNAUTHORIZED_EXCEPTION
|
mrok/datastructures.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from pydantic_core import core_schema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FixedSizeByteBuffer:
|
|
10
|
+
def __init__(self, max_size: int):
|
|
11
|
+
self._max_size = max_size
|
|
12
|
+
self._buf = bytearray()
|
|
13
|
+
self.overflow = False
|
|
14
|
+
|
|
15
|
+
def write(self, data: bytes) -> None:
|
|
16
|
+
if not data:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
remaining = self._max_size - len(self._buf)
|
|
20
|
+
if remaining <= 0:
|
|
21
|
+
self.overflow = True
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
if len(data) > remaining:
|
|
25
|
+
self._buf.extend(data[:remaining])
|
|
26
|
+
self.overflow = True
|
|
27
|
+
else:
|
|
28
|
+
self._buf.extend(data)
|
|
29
|
+
|
|
30
|
+
def getvalue(self) -> bytes:
|
|
31
|
+
return bytes(self._buf)
|
|
32
|
+
|
|
33
|
+
def clear(self) -> None:
|
|
34
|
+
self._buf.clear()
|
|
35
|
+
self.overflow = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HTTPHeaders(dict):
|
|
39
|
+
def __init__(self, initial=None):
|
|
40
|
+
super().__init__()
|
|
41
|
+
if initial:
|
|
42
|
+
for k, v in initial.items():
|
|
43
|
+
super().__setitem__(str(k).lower(), str(v))
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, key: str) -> str:
|
|
46
|
+
return super().__getitem__(key.lower())
|
|
47
|
+
|
|
48
|
+
def __setitem__(self, key: str, value: str) -> None:
|
|
49
|
+
super().__setitem__(str(key).lower(), str(value))
|
|
50
|
+
|
|
51
|
+
def __delitem__(self, key: str) -> None:
|
|
52
|
+
super().__delitem__(key.lower())
|
|
53
|
+
|
|
54
|
+
def get(self, k, default=None):
|
|
55
|
+
return super().get(k.lower(), default)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_asgi(cls, items: list[tuple[bytes, bytes]]) -> HTTPHeaders:
|
|
59
|
+
d = {k.decode("latin-1"): v.decode("latin-1") for k, v in items}
|
|
60
|
+
return cls(d)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def __get_pydantic_core_schema__(cls, source, handler):
|
|
64
|
+
"""Provide a pydantic-core schema so Pydantic treats this as a mapping of str->str.
|
|
65
|
+
|
|
66
|
+
We generate the schema for `dict[str, str]` using the provided handler and wrap
|
|
67
|
+
it with a validator that converts the validated dict into `HTTPHeaders`.
|
|
68
|
+
"""
|
|
69
|
+
# handler may be a callable or an object with `generate_schema`; handle both
|
|
70
|
+
try:
|
|
71
|
+
dict_schema = handler.generate_schema(dict[str, str])
|
|
72
|
+
except AttributeError:
|
|
73
|
+
dict_schema = handler(dict[str, str])
|
|
74
|
+
|
|
75
|
+
def _wrap(v, validator):
|
|
76
|
+
# `validator` will validate input according to `dict_schema` and return a dict
|
|
77
|
+
validated = validator(input_value=v)
|
|
78
|
+
if isinstance(validated, HTTPHeaders):
|
|
79
|
+
return validated
|
|
80
|
+
return cls(validated)
|
|
81
|
+
|
|
82
|
+
return core_schema.no_info_wrap_validator_function(
|
|
83
|
+
_wrap,
|
|
84
|
+
dict_schema,
|
|
85
|
+
serialization=core_schema.plain_serializer_function_ser_schema(lambda v: dict(v)),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class HTTPRequest(BaseModel):
|
|
90
|
+
method: str
|
|
91
|
+
url: str
|
|
92
|
+
headers: HTTPHeaders
|
|
93
|
+
query_string: bytes
|
|
94
|
+
start_time: float
|
|
95
|
+
body: bytes | None = None
|
|
96
|
+
body_truncated: bool | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class HTTPResponse(BaseModel):
|
|
100
|
+
type: Literal["response"] = "response"
|
|
101
|
+
request: HTTPRequest
|
|
102
|
+
status: int
|
|
103
|
+
headers: HTTPHeaders
|
|
104
|
+
duration: float
|
|
105
|
+
body: bytes | None = None
|
|
106
|
+
body_truncated: bool | None = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProcessMetrics(BaseModel):
|
|
110
|
+
cpu: float
|
|
111
|
+
mem: float
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class DataTransferMetrics(BaseModel):
|
|
115
|
+
bytes_in: int
|
|
116
|
+
bytes_out: int
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RequestsMetrics(BaseModel):
|
|
120
|
+
rps: int
|
|
121
|
+
total: int
|
|
122
|
+
successful: int
|
|
123
|
+
failed: int
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ResponseTimeMetrics(BaseModel):
|
|
127
|
+
avg: float
|
|
128
|
+
min: int
|
|
129
|
+
max: int
|
|
130
|
+
p50: int
|
|
131
|
+
p90: int
|
|
132
|
+
p99: int
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class WorkerMetrics(BaseModel):
|
|
136
|
+
worker_id: str
|
|
137
|
+
data_transfer: DataTransferMetrics
|
|
138
|
+
requests: RequestsMetrics
|
|
139
|
+
response_time: ResponseTimeMetrics
|
|
140
|
+
process: ProcessMetrics
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Meta(BaseModel):
|
|
144
|
+
identity: str
|
|
145
|
+
extension: str
|
|
146
|
+
instance: str
|
|
147
|
+
domain: str
|
|
148
|
+
tags: dict[str, str] | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Status(BaseModel):
|
|
152
|
+
type: Literal["status"] = "status"
|
|
153
|
+
meta: Meta
|
|
154
|
+
metrics: WorkerMetrics
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Event(BaseModel):
|
|
158
|
+
type: Literal["status", "response"]
|
|
159
|
+
data: Status | HTTPResponse = Field(discriminator="type")
|
mrok/http/config.py
CHANGED
|
@@ -8,21 +8,18 @@ from typing import Any
|
|
|
8
8
|
import openziti
|
|
9
9
|
from uvicorn import config
|
|
10
10
|
|
|
11
|
-
from mrok.conf import get_settings
|
|
12
11
|
from mrok.http.protocol import MrokHttpToolsProtocol
|
|
13
|
-
from mrok.
|
|
12
|
+
from mrok.http.types import ASGIApp
|
|
14
13
|
|
|
15
14
|
logger = logging.getLogger("mrok.proxy")
|
|
16
15
|
|
|
17
16
|
config.LIFESPAN["auto"] = "mrok.http.lifespan:MrokLifespan"
|
|
18
17
|
|
|
19
|
-
ASGIApplication = config.ASGIApplication
|
|
20
|
-
|
|
21
18
|
|
|
22
19
|
class MrokBackendConfig(config.Config):
|
|
23
20
|
def __init__(
|
|
24
21
|
self,
|
|
25
|
-
app:
|
|
22
|
+
app: ASGIApp | Callable[..., Any] | str,
|
|
26
23
|
identity_file: str | Path,
|
|
27
24
|
ziti_load_timeout_ms: int = 5000,
|
|
28
25
|
backlog: int = 2048,
|
|
@@ -62,4 +59,4 @@ class MrokBackendConfig(config.Config):
|
|
|
62
59
|
return sock
|
|
63
60
|
|
|
64
61
|
def configure_logging(self) -> None:
|
|
65
|
-
|
|
62
|
+
return
|
mrok/http/constants.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024
|
|
2
|
+
MAX_RESPONSE_BODY_BYTES = 5 * 1024 * 1024
|
|
3
|
+
|
|
4
|
+
BINARY_CONTENT_TYPES = {
|
|
5
|
+
"application/octet-stream",
|
|
6
|
+
"application/pdf",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
BINARY_PREFIXES = (
|
|
10
|
+
"image/",
|
|
11
|
+
"video/",
|
|
12
|
+
"audio/",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
TEXTUAL_CONTENT_TYPES = {
|
|
16
|
+
"application/json",
|
|
17
|
+
"application/xml",
|
|
18
|
+
"application/javascript",
|
|
19
|
+
"application/x-www-form-urlencoded",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
TEXTUAL_PREFIXES = ("text/",)
|