koi-net 1.1.0b8__py3-none-any.whl → 1.2.0b2__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.
Potentially problematic release.
This version of koi-net might be problematic. Click here for more details.
- koi_net/__init__.py +2 -1
- koi_net/assembler.py +82 -0
- koi_net/cli/__init__.py +1 -0
- koi_net/cli/commands.py +99 -0
- koi_net/cli/models.py +41 -0
- koi_net/config.py +34 -0
- koi_net/context.py +11 -28
- koi_net/core.py +63 -179
- koi_net/default_actions.py +10 -1
- koi_net/effector.py +61 -34
- koi_net/handshaker.py +39 -0
- koi_net/identity.py +2 -3
- koi_net/interfaces/entrypoint.py +5 -0
- koi_net/interfaces/worker.py +17 -0
- koi_net/lifecycle.py +85 -48
- koi_net/logger.py +176 -0
- koi_net/network/error_handler.py +18 -16
- koi_net/network/event_queue.py +17 -185
- koi_net/network/graph.py +15 -10
- koi_net/network/poll_event_buffer.py +26 -0
- koi_net/network/request_handler.py +54 -47
- koi_net/network/resolver.py +18 -21
- koi_net/network/response_handler.py +79 -15
- koi_net/poller.py +18 -9
- koi_net/processor/event_worker.py +117 -0
- koi_net/processor/handler.py +4 -2
- koi_net/processor/{default_handlers.py → handlers.py} +109 -59
- koi_net/processor/knowledge_object.py +19 -7
- koi_net/processor/kobj_queue.py +51 -0
- koi_net/processor/kobj_worker.py +44 -0
- koi_net/processor/{knowledge_pipeline.py → pipeline.py} +31 -53
- koi_net/protocol/api_models.py +7 -3
- koi_net/protocol/envelope.py +5 -6
- koi_net/protocol/model_map.py +61 -0
- koi_net/protocol/node.py +3 -3
- koi_net/protocol/secure.py +8 -8
- koi_net/secure.py +33 -13
- koi_net/sentry.py +13 -0
- koi_net/server.py +44 -78
- koi_net/utils.py +18 -0
- {koi_net-1.1.0b8.dist-info → koi_net-1.2.0b2.dist-info}/METADATA +8 -3
- koi_net-1.2.0b2.dist-info/RECORD +52 -0
- koi_net-1.2.0b2.dist-info/entry_points.txt +2 -0
- koi_net/actor.py +0 -60
- koi_net/processor/interface.py +0 -101
- koi_net-1.1.0b8.dist-info/RECORD +0 -38
- {koi_net-1.1.0b8.dist-info → koi_net-1.2.0b2.dist-info}/WHEEL +0 -0
- {koi_net-1.1.0b8.dist-info → koi_net-1.2.0b2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import Callable
|
|
3
|
-
from rid_lib.core import RIDType
|
|
1
|
+
import structlog
|
|
4
2
|
from rid_lib.types import KoiNetEdge, KoiNetNode
|
|
5
3
|
from rid_lib.ext import Cache
|
|
6
4
|
from ..protocol.event import EventType
|
|
7
5
|
from ..network.request_handler import RequestHandler
|
|
8
|
-
from ..network.event_queue import
|
|
6
|
+
from ..network.event_queue import EventQueue
|
|
9
7
|
from ..network.graph import NetworkGraph
|
|
10
8
|
from ..identity import NodeIdentity
|
|
11
9
|
from .handler import (
|
|
@@ -15,55 +13,36 @@ from .handler import (
|
|
|
15
13
|
StopChain
|
|
16
14
|
)
|
|
17
15
|
from .knowledge_object import KnowledgeObject
|
|
16
|
+
from ..context import HandlerContext
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from ..context import HandlerContext
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
18
|
+
log = structlog.stdlib.get_logger()
|
|
24
19
|
|
|
25
20
|
|
|
26
21
|
class KnowledgePipeline:
|
|
27
|
-
handler_context:
|
|
22
|
+
handler_context: HandlerContext
|
|
28
23
|
cache: Cache
|
|
29
24
|
identity: NodeIdentity
|
|
30
25
|
request_handler: RequestHandler
|
|
31
|
-
event_queue:
|
|
26
|
+
event_queue: EventQueue
|
|
32
27
|
graph: NetworkGraph
|
|
33
|
-
|
|
28
|
+
knowledge_handlers: list[KnowledgeHandler]
|
|
34
29
|
|
|
35
30
|
def __init__(
|
|
36
31
|
self,
|
|
37
|
-
handler_context:
|
|
32
|
+
handler_context: HandlerContext,
|
|
38
33
|
cache: Cache,
|
|
39
34
|
request_handler: RequestHandler,
|
|
40
|
-
event_queue:
|
|
35
|
+
event_queue: EventQueue,
|
|
41
36
|
graph: NetworkGraph,
|
|
42
|
-
|
|
37
|
+
knowledge_handlers: list[KnowledgeHandler] = []
|
|
43
38
|
):
|
|
44
39
|
self.handler_context = handler_context
|
|
45
40
|
self.cache = cache
|
|
46
41
|
self.request_handler = request_handler
|
|
47
42
|
self.event_queue = event_queue
|
|
48
43
|
self.graph = graph
|
|
49
|
-
self.
|
|
44
|
+
self.knowledge_handlers = knowledge_handlers
|
|
50
45
|
|
|
51
|
-
def add_handler(self, handler: KnowledgeHandler):
|
|
52
|
-
self.handlers.append(handler)
|
|
53
|
-
|
|
54
|
-
def register_handler(
|
|
55
|
-
self,
|
|
56
|
-
handler_type: HandlerType,
|
|
57
|
-
rid_types: list[RIDType] | None = None,
|
|
58
|
-
event_types: list[EventType | None] | None = None
|
|
59
|
-
):
|
|
60
|
-
"""Assigns decorated function as handler for this processor."""
|
|
61
|
-
def decorator(func: Callable) -> Callable:
|
|
62
|
-
handler = KnowledgeHandler(func, handler_type, rid_types, event_types)
|
|
63
|
-
self.add_handler(handler)
|
|
64
|
-
return func
|
|
65
|
-
return decorator
|
|
66
|
-
|
|
67
46
|
def call_handler_chain(
|
|
68
47
|
self,
|
|
69
48
|
handler_type: HandlerType,
|
|
@@ -79,7 +58,7 @@ class KnowledgePipeline:
|
|
|
79
58
|
Handlers will only be called in the chain if their handler and RID type match that of the inputted knowledge object.
|
|
80
59
|
"""
|
|
81
60
|
|
|
82
|
-
for handler in self.
|
|
61
|
+
for handler in self.knowledge_handlers:
|
|
83
62
|
if handler_type != handler.handler_type:
|
|
84
63
|
continue
|
|
85
64
|
|
|
@@ -89,7 +68,7 @@ class KnowledgePipeline:
|
|
|
89
68
|
if handler.event_types and kobj.event_type not in handler.event_types:
|
|
90
69
|
continue
|
|
91
70
|
|
|
92
|
-
|
|
71
|
+
log.debug(f"Calling {handler_type} handler '{handler.func.__name__}'")
|
|
93
72
|
|
|
94
73
|
resp = handler.func(
|
|
95
74
|
ctx=self.handler_context,
|
|
@@ -98,7 +77,7 @@ class KnowledgePipeline:
|
|
|
98
77
|
|
|
99
78
|
# stops handler chain execution
|
|
100
79
|
if resp is STOP_CHAIN:
|
|
101
|
-
|
|
80
|
+
log.debug(f"Handler chain stopped by {handler.func.__name__}")
|
|
102
81
|
return STOP_CHAIN
|
|
103
82
|
# kobj unmodified
|
|
104
83
|
elif resp is None:
|
|
@@ -106,7 +85,7 @@ class KnowledgePipeline:
|
|
|
106
85
|
# kobj modified by handler
|
|
107
86
|
elif isinstance(resp, KnowledgeObject):
|
|
108
87
|
kobj = resp
|
|
109
|
-
|
|
88
|
+
log.debug(f"Knowledge object modified by {handler.func.__name__}")
|
|
110
89
|
else:
|
|
111
90
|
raise ValueError(f"Handler {handler.func.__name__} returned invalid response '{resp}'")
|
|
112
91
|
|
|
@@ -125,36 +104,36 @@ class KnowledgePipeline:
|
|
|
125
104
|
The pipeline may be stopped by any point by a single handler returning the `STOP_CHAIN` sentinel. In that case, the process will exit immediately. Further handlers of that type and later handler chains will not be called.
|
|
126
105
|
"""
|
|
127
106
|
|
|
128
|
-
|
|
107
|
+
log.debug(f"Handling {kobj!r}")
|
|
129
108
|
kobj = self.call_handler_chain(HandlerType.RID, kobj)
|
|
130
109
|
if kobj is STOP_CHAIN: return
|
|
131
110
|
|
|
132
111
|
if kobj.event_type == EventType.FORGET:
|
|
133
112
|
bundle = self.cache.read(kobj.rid)
|
|
134
113
|
if not bundle:
|
|
135
|
-
|
|
114
|
+
log.debug("Local bundle not found")
|
|
136
115
|
return
|
|
137
116
|
|
|
138
117
|
# the bundle (to be deleted) attached to kobj for downstream analysis
|
|
139
|
-
|
|
118
|
+
log.debug("Adding local bundle (to be deleted) to knowledge object")
|
|
140
119
|
kobj.manifest = bundle.manifest
|
|
141
120
|
kobj.contents = bundle.contents
|
|
142
121
|
|
|
143
122
|
else:
|
|
144
123
|
# attempt to retrieve manifest
|
|
145
124
|
if not kobj.manifest:
|
|
146
|
-
|
|
125
|
+
log.debug("Manifest not found")
|
|
147
126
|
if not kobj.source:
|
|
148
127
|
return
|
|
149
128
|
|
|
150
|
-
|
|
129
|
+
log.debug("Attempting to fetch remote manifest from source")
|
|
151
130
|
payload = self.request_handler.fetch_manifests(
|
|
152
131
|
node=kobj.source,
|
|
153
132
|
rids=[kobj.rid]
|
|
154
133
|
)
|
|
155
134
|
|
|
156
135
|
if not payload.manifests:
|
|
157
|
-
|
|
136
|
+
log.debug("Failed to find manifest")
|
|
158
137
|
return
|
|
159
138
|
|
|
160
139
|
kobj.manifest = payload.manifests[0]
|
|
@@ -164,24 +143,24 @@ class KnowledgePipeline:
|
|
|
164
143
|
|
|
165
144
|
# attempt to retrieve bundle
|
|
166
145
|
if not kobj.bundle:
|
|
167
|
-
|
|
146
|
+
log.debug("Bundle not found")
|
|
168
147
|
if kobj.source is None:
|
|
169
148
|
return
|
|
170
149
|
|
|
171
|
-
|
|
150
|
+
log.debug("Attempting to fetch remote bundle from source")
|
|
172
151
|
payload = self.request_handler.fetch_bundles(
|
|
173
152
|
node=kobj.source,
|
|
174
153
|
rids=[kobj.rid]
|
|
175
154
|
)
|
|
176
155
|
|
|
177
156
|
if not payload.bundles:
|
|
178
|
-
|
|
157
|
+
log.debug("Failed to find bundle")
|
|
179
158
|
return
|
|
180
159
|
|
|
181
160
|
bundle = payload.bundles[0]
|
|
182
161
|
|
|
183
162
|
if kobj.manifest != bundle.manifest:
|
|
184
|
-
|
|
163
|
+
log.warning("Retrieved bundle contains a different manifest")
|
|
185
164
|
|
|
186
165
|
kobj.manifest = bundle.manifest
|
|
187
166
|
kobj.contents = bundle.contents
|
|
@@ -190,31 +169,30 @@ class KnowledgePipeline:
|
|
|
190
169
|
if kobj is STOP_CHAIN: return
|
|
191
170
|
|
|
192
171
|
if kobj.normalized_event_type in (EventType.UPDATE, EventType.NEW):
|
|
193
|
-
|
|
172
|
+
log.info(f"Writing to cache: {kobj!r}")
|
|
194
173
|
self.cache.write(kobj.bundle)
|
|
195
174
|
|
|
196
175
|
elif kobj.normalized_event_type == EventType.FORGET:
|
|
197
|
-
|
|
176
|
+
log.info(f"Deleting from cache: {kobj!r}")
|
|
198
177
|
self.cache.delete(kobj.rid)
|
|
199
178
|
|
|
200
179
|
else:
|
|
201
|
-
|
|
180
|
+
log.debug("Normalized event type was never set, no cache or network operations will occur")
|
|
202
181
|
return
|
|
203
182
|
|
|
204
183
|
if type(kobj.rid) in (KoiNetNode, KoiNetEdge):
|
|
205
|
-
|
|
184
|
+
log.debug("Change to node or edge, regenerating network graph")
|
|
206
185
|
self.graph.generate()
|
|
207
186
|
|
|
208
187
|
kobj = self.call_handler_chain(HandlerType.Network, kobj)
|
|
209
188
|
if kobj is STOP_CHAIN: return
|
|
210
189
|
|
|
211
190
|
if kobj.network_targets:
|
|
212
|
-
|
|
191
|
+
log.debug(f"Broadcasting event to {len(kobj.network_targets)} network target(s)")
|
|
213
192
|
else:
|
|
214
|
-
|
|
193
|
+
log.debug("No network targets set")
|
|
215
194
|
|
|
216
195
|
for node in kobj.network_targets:
|
|
217
196
|
self.event_queue.push_event_to(kobj.normalized_event, node)
|
|
218
|
-
self.event_queue.flush_webhook_queue(node)
|
|
219
197
|
|
|
220
198
|
kobj = self.call_handler_chain(HandlerType.Final, kobj)
|
koi_net/protocol/api_models.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Pydantic models for request and response
|
|
1
|
+
"""Pydantic models for request and response objects in the KOI-net API."""
|
|
2
2
|
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
from rid_lib import RID, RIDType
|
|
6
6
|
from rid_lib.ext import Bundle, Manifest
|
|
@@ -59,4 +59,8 @@ class ErrorResponse(BaseModel):
|
|
|
59
59
|
# TYPES
|
|
60
60
|
|
|
61
61
|
type RequestModels = EventsPayload | PollEvents | FetchRids | FetchManifests | FetchBundles
|
|
62
|
-
type ResponseModels = RidsPayload | ManifestsPayload | BundlesPayload | EventsPayload | ErrorResponse
|
|
62
|
+
type ResponseModels = RidsPayload | ManifestsPayload | BundlesPayload | EventsPayload | ErrorResponse
|
|
63
|
+
type ApiModels = Annotated[
|
|
64
|
+
RequestModels | ResponseModels,
|
|
65
|
+
Field(discriminator="type")
|
|
66
|
+
]
|
koi_net/protocol/envelope.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import structlog
|
|
2
2
|
from typing import Generic, TypeVar
|
|
3
3
|
from pydantic import BaseModel, ConfigDict
|
|
4
4
|
from rid_lib.types import KoiNetNode
|
|
@@ -6,8 +6,7 @@ from rid_lib.types import KoiNetNode
|
|
|
6
6
|
from .secure import PrivateKey, PublicKey
|
|
7
7
|
from .api_models import RequestModels, ResponseModels
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
9
|
+
log = structlog.stdlib.get_logger()
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
T = TypeVar("T", bound=RequestModels | ResponseModels)
|
|
@@ -28,7 +27,7 @@ class SignedEnvelope(BaseModel, Generic[T]):
|
|
|
28
27
|
target_node=self.target_node
|
|
29
28
|
)
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
log.debug(f"Verifying envelope: {unsigned_envelope.model_dump_json(exclude_none=True)}")
|
|
32
31
|
|
|
33
32
|
pub_key.verify(
|
|
34
33
|
self.signature,
|
|
@@ -43,8 +42,8 @@ class UnsignedEnvelope(BaseModel, Generic[T]):
|
|
|
43
42
|
target_node: KoiNetNode
|
|
44
43
|
|
|
45
44
|
def sign_with(self, priv_key: PrivateKey) -> SignedEnvelope[T]:
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
log.debug(f"Signing envelope: {self.model_dump_json(exclude_none=True)}")
|
|
46
|
+
log.debug(f"Type: [{type(self.payload)}]")
|
|
48
47
|
|
|
49
48
|
signature = priv_key.sign(
|
|
50
49
|
self.model_dump_json(exclude_none=True).encode()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import NamedTuple
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from .envelope import SignedEnvelope
|
|
4
|
+
from .consts import (
|
|
5
|
+
BROADCAST_EVENTS_PATH,
|
|
6
|
+
POLL_EVENTS_PATH,
|
|
7
|
+
FETCH_BUNDLES_PATH,
|
|
8
|
+
FETCH_MANIFESTS_PATH,
|
|
9
|
+
FETCH_RIDS_PATH
|
|
10
|
+
)
|
|
11
|
+
from .api_models import (
|
|
12
|
+
EventsPayload,
|
|
13
|
+
PollEvents,
|
|
14
|
+
FetchBundles,
|
|
15
|
+
BundlesPayload,
|
|
16
|
+
FetchManifests,
|
|
17
|
+
ManifestsPayload,
|
|
18
|
+
FetchRids,
|
|
19
|
+
RidsPayload
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Models(NamedTuple):
|
|
24
|
+
request: type[BaseModel]
|
|
25
|
+
response: type[BaseModel] | None
|
|
26
|
+
request_envelope: type[SignedEnvelope]
|
|
27
|
+
response_envelope: type[SignedEnvelope] | None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
API_MODEL_MAP: dict[str, Models] = {
|
|
31
|
+
BROADCAST_EVENTS_PATH: Models(
|
|
32
|
+
request=EventsPayload,
|
|
33
|
+
response=None,
|
|
34
|
+
request_envelope=SignedEnvelope[EventsPayload],
|
|
35
|
+
response_envelope=None
|
|
36
|
+
),
|
|
37
|
+
POLL_EVENTS_PATH: Models(
|
|
38
|
+
request=PollEvents,
|
|
39
|
+
response=EventsPayload,
|
|
40
|
+
request_envelope=SignedEnvelope[PollEvents],
|
|
41
|
+
response_envelope=SignedEnvelope[EventsPayload]
|
|
42
|
+
),
|
|
43
|
+
FETCH_BUNDLES_PATH: Models(
|
|
44
|
+
request=FetchBundles,
|
|
45
|
+
response=BundlesPayload,
|
|
46
|
+
request_envelope=SignedEnvelope[FetchBundles],
|
|
47
|
+
response_envelope=SignedEnvelope[BundlesPayload]
|
|
48
|
+
),
|
|
49
|
+
FETCH_MANIFESTS_PATH: Models(
|
|
50
|
+
request=FetchManifests,
|
|
51
|
+
response=ManifestsPayload,
|
|
52
|
+
request_envelope=SignedEnvelope[FetchManifests],
|
|
53
|
+
response_envelope=SignedEnvelope[ManifestsPayload]
|
|
54
|
+
),
|
|
55
|
+
FETCH_RIDS_PATH: Models(
|
|
56
|
+
request=FetchRids,
|
|
57
|
+
response=RidsPayload,
|
|
58
|
+
request_envelope=SignedEnvelope[FetchRids],
|
|
59
|
+
response_envelope=SignedEnvelope[RidsPayload]
|
|
60
|
+
)
|
|
61
|
+
}
|
koi_net/protocol/node.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import StrEnum
|
|
2
|
-
from pydantic import BaseModel
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
3
|
from rid_lib import RIDType
|
|
4
4
|
|
|
5
5
|
|
|
@@ -8,8 +8,8 @@ class NodeType(StrEnum):
|
|
|
8
8
|
PARTIAL = "PARTIAL"
|
|
9
9
|
|
|
10
10
|
class NodeProvides(BaseModel):
|
|
11
|
-
event: list[RIDType] =
|
|
12
|
-
state: list[RIDType] =
|
|
11
|
+
event: list[RIDType] = Field(default_factory=list)
|
|
12
|
+
state: list[RIDType] = Field(default_factory=list)
|
|
13
13
|
|
|
14
14
|
class NodeProfile(BaseModel):
|
|
15
15
|
base_url: str | None = None
|
koi_net/protocol/secure.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import structlog
|
|
2
2
|
from base64 import b64decode, b64encode
|
|
3
3
|
from cryptography.hazmat.primitives import hashes
|
|
4
4
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
@@ -9,7 +9,7 @@ from cryptography.hazmat.primitives.asymmetric.utils import (
|
|
|
9
9
|
encode_dss_signature
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
log = structlog.stdlib.get_logger()
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def der_to_raw_signature(der_signature: bytes, curve=ec.SECP256R1()) -> bytes:
|
|
@@ -91,9 +91,9 @@ class PrivateKey:
|
|
|
91
91
|
|
|
92
92
|
signature = b64encode(raw_signature_bytes).decode()
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
log.debug(f"Signing message with [{self.public_key().to_der()}]")
|
|
95
|
+
log.debug(f"hash: {hashed_message}")
|
|
96
|
+
log.debug(f"signature: {signature}")
|
|
97
97
|
|
|
98
98
|
return signature
|
|
99
99
|
|
|
@@ -144,9 +144,9 @@ class PublicKey:
|
|
|
144
144
|
# print()
|
|
145
145
|
# print(message.decode())
|
|
146
146
|
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
147
|
+
# log.debug(f"Verifying message with [{self.to_der()}]")
|
|
148
|
+
# log.debug(f"hash: {hashed_message}")
|
|
149
|
+
# log.debug(f"signature: {signature}")
|
|
150
150
|
|
|
151
151
|
raw_signature_bytes = b64decode(signature)
|
|
152
152
|
der_signature_bytes = raw_to_der_signature(raw_signature_bytes)
|
koi_net/secure.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import structlog
|
|
2
2
|
from functools import wraps
|
|
3
3
|
|
|
4
4
|
import cryptography.exceptions
|
|
5
|
-
from rid_lib.ext import Bundle
|
|
5
|
+
from rid_lib.ext import Bundle, Cache
|
|
6
6
|
from rid_lib.ext.utils import sha256_hash
|
|
7
|
+
from rid_lib.types import KoiNetNode
|
|
7
8
|
from .identity import NodeIdentity
|
|
8
9
|
from .protocol.envelope import UnsignedEnvelope, SignedEnvelope
|
|
9
10
|
from .protocol.secure import PublicKey
|
|
10
|
-
from .protocol.api_models import EventsPayload
|
|
11
|
+
from .protocol.api_models import ApiModels, EventsPayload
|
|
11
12
|
from .protocol.event import EventType
|
|
12
13
|
from .protocol.node import NodeProfile
|
|
13
14
|
from .protocol.secure import PrivateKey
|
|
@@ -17,31 +18,32 @@ from .protocol.errors import (
|
|
|
17
18
|
InvalidSignatureError,
|
|
18
19
|
InvalidTargetError
|
|
19
20
|
)
|
|
20
|
-
from .effector import Effector
|
|
21
21
|
from .config import NodeConfig
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
log = structlog.stdlib.get_logger()
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class Secure:
|
|
27
|
+
"""Subsystem handling secure protocol logic."""
|
|
27
28
|
identity: NodeIdentity
|
|
28
|
-
|
|
29
|
+
cache: Cache
|
|
29
30
|
config: NodeConfig
|
|
30
31
|
priv_key: PrivateKey
|
|
31
32
|
|
|
32
33
|
def __init__(
|
|
33
34
|
self,
|
|
34
35
|
identity: NodeIdentity,
|
|
35
|
-
|
|
36
|
+
cache: Cache,
|
|
36
37
|
config: NodeConfig
|
|
37
38
|
):
|
|
38
39
|
self.identity = identity
|
|
39
|
-
self.
|
|
40
|
+
self.cache = cache
|
|
40
41
|
self.config = config
|
|
41
42
|
|
|
42
43
|
self.priv_key = self._load_priv_key()
|
|
43
44
|
|
|
44
45
|
def _load_priv_key(self) -> PrivateKey:
|
|
46
|
+
"""Loads private key from PEM file path in config."""
|
|
45
47
|
with open(self.config.koi_net.private_key_pem_path, "r") as f:
|
|
46
48
|
priv_key_pem = f.read()
|
|
47
49
|
|
|
@@ -51,6 +53,14 @@ class Secure:
|
|
|
51
53
|
)
|
|
52
54
|
|
|
53
55
|
def _handle_unknown_node(self, envelope: SignedEnvelope) -> Bundle | None:
|
|
56
|
+
"""Attempts to find node profile in proided envelope.
|
|
57
|
+
|
|
58
|
+
If an unknown node sends an envelope, it may still be able to be
|
|
59
|
+
validated if that envelope contains their node profile. This is
|
|
60
|
+
essential for allowing unknown nodes to handshake and introduce
|
|
61
|
+
themselves. Only an `EventsPayload` contain a `NEW` event for a
|
|
62
|
+
node profile for the source node is permissible.
|
|
63
|
+
"""
|
|
54
64
|
if type(envelope.payload) != EventsPayload:
|
|
55
65
|
return None
|
|
56
66
|
|
|
@@ -64,7 +74,10 @@ class Secure:
|
|
|
64
74
|
return event.bundle
|
|
65
75
|
return None
|
|
66
76
|
|
|
67
|
-
def create_envelope(
|
|
77
|
+
def create_envelope(
|
|
78
|
+
self, payload: ApiModels, target: KoiNetNode
|
|
79
|
+
) -> SignedEnvelope:
|
|
80
|
+
"""Returns signed envelope to target from provided payload."""
|
|
68
81
|
return UnsignedEnvelope(
|
|
69
82
|
payload=payload,
|
|
70
83
|
source_node=self.identity.rid,
|
|
@@ -72,8 +85,10 @@ class Secure:
|
|
|
72
85
|
).sign_with(self.priv_key)
|
|
73
86
|
|
|
74
87
|
def validate_envelope(self, envelope: SignedEnvelope):
|
|
88
|
+
"""Validates signed envelope from another node."""
|
|
89
|
+
|
|
75
90
|
node_bundle = (
|
|
76
|
-
self.
|
|
91
|
+
self.cache.read(envelope.source_node) or
|
|
77
92
|
self._handle_unknown_node(envelope)
|
|
78
93
|
)
|
|
79
94
|
|
|
@@ -98,17 +113,22 @@ class Secure:
|
|
|
98
113
|
raise InvalidTargetError(f"Envelope target {envelope.target_node!r} is not me")
|
|
99
114
|
|
|
100
115
|
def envelope_handler(self, func):
|
|
116
|
+
"""Wrapper function validates envelopes for server endpoints.
|
|
117
|
+
|
|
118
|
+
Validates incoming envelope and passes payload to endpoint
|
|
119
|
+
handler. Resulting payload is returned as a signed envelope.
|
|
120
|
+
"""
|
|
101
121
|
@wraps(func)
|
|
102
122
|
async def wrapper(req: SignedEnvelope, *args, **kwargs) -> SignedEnvelope | None:
|
|
103
|
-
|
|
123
|
+
log.info("Validating envelope")
|
|
104
124
|
|
|
105
125
|
self.validate_envelope(req)
|
|
106
|
-
|
|
126
|
+
log.info("Calling endpoint handler")
|
|
107
127
|
|
|
108
128
|
result = await func(req, *args, **kwargs)
|
|
109
129
|
|
|
110
130
|
if result is not None:
|
|
111
|
-
|
|
131
|
+
log.info("Creating response envelope")
|
|
112
132
|
return self.create_envelope(
|
|
113
133
|
payload=result,
|
|
114
134
|
target=req.source_node
|
koi_net/sentry.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# import sentry_sdk
|
|
2
|
+
# from sentry_sdk.integrations.logging import LoggingIntegration
|
|
3
|
+
|
|
4
|
+
# sentry_sdk.init(
|
|
5
|
+
# dsn="https://7bbafef3c7dbd652506db3cb2aca9f98@o4510149352357888.ingest.us.sentry.io/4510149355765760",
|
|
6
|
+
# # Add data like request headers and IP for users,
|
|
7
|
+
# # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
|
|
8
|
+
# send_default_pii=True,
|
|
9
|
+
# enable_logs=True,
|
|
10
|
+
# integrations=[
|
|
11
|
+
# LoggingIntegration(sentry_logs_level=None)
|
|
12
|
+
# ]
|
|
13
|
+
# )
|