fleetmesh-node 0.2.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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""The fips-native listener rung: the socket loop that feeds the router's direct-rung rule.
|
|
2
|
+
|
|
3
|
+
Separate from the router because it is the one piece that feeds the FIPS incoming flows
|
|
4
|
+
into the direct-rung rule, and nothing in the send ladder needs to see it.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import socket
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Optional,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from . import core as fips_transport
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FipsRungMixin:
|
|
24
|
+
"""Mixed into TransportRouter, which owns every attribute and method read here."""
|
|
25
|
+
|
|
26
|
+
nostr: Any
|
|
27
|
+
fips: Optional[Any]
|
|
28
|
+
_fips_listener_sock: Optional[Any]
|
|
29
|
+
deliver_direct: Callable[..., Optional[str]]
|
|
30
|
+
|
|
31
|
+
def _dispatch_fips_payload(self, seq: int, flags: int, payload: bytes) -> None:
|
|
32
|
+
"""One reassembled FIPS datagram payload, through the direct-rung rule."""
|
|
33
|
+
self.deliver_direct('fips-native', payload, f"fips-{seq}")
|
|
34
|
+
|
|
35
|
+
def _read_fips_flow(self, flow_sock: socket.socket) -> None:
|
|
36
|
+
reasm = fips_transport.Reassembler()
|
|
37
|
+
while not self.nostr._stop.is_set():
|
|
38
|
+
try:
|
|
39
|
+
if self.fips is None:
|
|
40
|
+
break
|
|
41
|
+
flow_sock.settimeout(0.5)
|
|
42
|
+
data = flow_sock.recv(65536)
|
|
43
|
+
seq, flags, msg_id, payload = fips_transport.decode_frame_ex(data)
|
|
44
|
+
done = reasm.feed(seq, flags, msg_id, payload)
|
|
45
|
+
if done is not None:
|
|
46
|
+
self._dispatch_fips_payload(seq, done[0], done[1])
|
|
47
|
+
except (socket.timeout, TimeoutError):
|
|
48
|
+
continue
|
|
49
|
+
except OSError as e:
|
|
50
|
+
# The flow socket is gone (daemon closed it): this flow ends;
|
|
51
|
+
# the listener loop keeps accepting new ones.
|
|
52
|
+
log.debug('fips-native: flow reader ending: %s', e)
|
|
53
|
+
break
|
|
54
|
+
except fips_transport.FipsTransportError as e:
|
|
55
|
+
# A frame the decoder refused is a bad datagram, not a dead
|
|
56
|
+
# flow: drop it and keep reading, as the reassembler drops gaps.
|
|
57
|
+
log.debug('fips-native: bad frame on flow dropped: %s', e)
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
flow_sock.close()
|
|
61
|
+
except OSError:
|
|
62
|
+
pass # best-effort close; the flow may already be gone
|
|
63
|
+
|
|
64
|
+
def _listen_fips(self) -> None:
|
|
65
|
+
sock = self._fips_listener_sock
|
|
66
|
+
if sock is None or self.fips is None:
|
|
67
|
+
return
|
|
68
|
+
sock.settimeout(0.5)
|
|
69
|
+
listener_reasm = fips_transport.Reassembler()
|
|
70
|
+
while not self.nostr._stop.is_set():
|
|
71
|
+
try:
|
|
72
|
+
flow_sock, arrival_info, msg = self.fips.accept_listener_event(sock)
|
|
73
|
+
if flow_sock is not None:
|
|
74
|
+
t = threading.Thread(target=self._read_fips_flow, args=(flow_sock,), daemon=True)
|
|
75
|
+
t.start()
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Case 2: Direct frame received on listener socket (Mock/Cluster daemon)
|
|
79
|
+
if msg.startswith(fips_transport.FRAME_MAGIC):
|
|
80
|
+
seq, fl, mid, payload = fips_transport.decode_frame_ex(msg)
|
|
81
|
+
done = listener_reasm.feed(seq, fl, mid, payload)
|
|
82
|
+
if done is not None:
|
|
83
|
+
self._dispatch_fips_payload(seq, done[0], done[1])
|
|
84
|
+
except (socket.timeout, TimeoutError):
|
|
85
|
+
continue
|
|
86
|
+
except fips_transport.FipsTransportError as e:
|
|
87
|
+
# An undecodable direct frame: drop it, keep listening.
|
|
88
|
+
log.debug('fips-native: bad direct frame dropped: %s', e)
|
|
89
|
+
except OSError as e:
|
|
90
|
+
# The listener socket itself failed. During shutdown that is
|
|
91
|
+
# expected; otherwise log it and keep listening after a short
|
|
92
|
+
# pause, since the alternative is a silent dead rung.
|
|
93
|
+
if self.nostr._stop.is_set():
|
|
94
|
+
break
|
|
95
|
+
log.warning('fips-native: listener error, continuing: %s', e)
|
|
96
|
+
time.sleep(0.01)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Types and records for the FIPS native transport rung."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FipsArrivalInfo(TypedDict, total=False):
|
|
8
|
+
"""Arrival notice payload received alongside accepted flow socket."""
|
|
9
|
+
peer: Optional[str]
|
|
10
|
+
remote_port: Optional[int]
|
|
11
|
+
max_payload: Optional[int]
|
|
12
|
+
ticket: Optional[str]
|
node/transport/host.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""ProtocolHostRouter: Unified multiplexer for HTTP and WebSocket protocol suites.
|
|
2
|
+
|
|
3
|
+
Standard library and ws.py only. Multiplexes foreign and native protocol suites
|
|
4
|
+
(Nostr, ATProto, ActivityPub, Blossom CAS, Prometheus metrics) over a single
|
|
5
|
+
public edge port (e.g. 8000 or 443 with TLS).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import collections
|
|
10
|
+
import dataclasses
|
|
11
|
+
import logging
|
|
12
|
+
import socket
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
Callable,
|
|
18
|
+
Mapping,
|
|
19
|
+
Optional,
|
|
20
|
+
Sequence,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from fleetmesh_core.wire.interfaces import ProtocolHealth
|
|
24
|
+
from . import ws
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass(frozen=True)
|
|
30
|
+
class HttpRoute:
|
|
31
|
+
prefix: str
|
|
32
|
+
handler: Callable[[socket.socket, ws.PlainHTTP], Any]
|
|
33
|
+
methods: tuple[str, ...]
|
|
34
|
+
owner: str = ''
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclasses.dataclass(frozen=True)
|
|
38
|
+
class WebSocketRoute:
|
|
39
|
+
path: str
|
|
40
|
+
handler: Callable[[ws.WebSocket, str], Any]
|
|
41
|
+
owner: str = ''
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProtocolHost:
|
|
45
|
+
"""Unified HTTP & WebSocket multiplexer mounted on the node's public port."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
host: str = '127.0.0.1',
|
|
50
|
+
port: int = 0,
|
|
51
|
+
name: str = 'meshnode',
|
|
52
|
+
clock: Optional[Callable[[], int]] = None,
|
|
53
|
+
bind_wait: float = 10.0,
|
|
54
|
+
max_body_bytes: int = 16 * 1024 * 1024,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.host: str = host
|
|
57
|
+
self.port: int = port
|
|
58
|
+
self.name: str = name
|
|
59
|
+
self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
60
|
+
self.bind_wait: float = bind_wait
|
|
61
|
+
self.max_body_bytes: int = max_body_bytes
|
|
62
|
+
|
|
63
|
+
self._http_routes: list[HttpRoute] = []
|
|
64
|
+
self._ws_routes: list[WebSocketRoute] = []
|
|
65
|
+
self._lock: threading.RLock = threading.RLock()
|
|
66
|
+
|
|
67
|
+
self._server: Optional[ws.Server] = None
|
|
68
|
+
self._started: bool = False
|
|
69
|
+
self._active_connections: int = 0
|
|
70
|
+
self._requests_in: int = 0
|
|
71
|
+
self._requests_out: int = 0
|
|
72
|
+
self._errors: collections.deque[str] = collections.deque(maxlen=100)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def url(self) -> str:
|
|
76
|
+
if self._server is not None:
|
|
77
|
+
return self._server.url
|
|
78
|
+
return 'ws://%s:%d' % (self.host, self.port)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def http_url(self) -> str:
|
|
82
|
+
if self._server is not None:
|
|
83
|
+
return 'http://%s:%d' % (self._server.host, self._server.port)
|
|
84
|
+
return 'http://%s:%d' % (self.host, self.port)
|
|
85
|
+
|
|
86
|
+
# ---- Route Registration --------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def mount_route(
|
|
89
|
+
self,
|
|
90
|
+
prefix: str,
|
|
91
|
+
handler: Callable[..., Any],
|
|
92
|
+
methods: Sequence[str] = ('GET',),
|
|
93
|
+
owner: str = '',
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Mount an HTTP path prefix handler.
|
|
96
|
+
|
|
97
|
+
Longest prefix matches take precedence. Handler receives `(conn, req: ws.PlainHTTP)`
|
|
98
|
+
and returns True if handled, or False to fall through.
|
|
99
|
+
"""
|
|
100
|
+
clean_prefix = '/' + prefix.strip('/') if prefix != '/' else '/'
|
|
101
|
+
norm_methods = tuple(m.upper() for m in methods)
|
|
102
|
+
with self._lock:
|
|
103
|
+
# Replace existing identical prefix and owner if present
|
|
104
|
+
self._http_routes = [
|
|
105
|
+
r for r in self._http_routes
|
|
106
|
+
if not (r.prefix == clean_prefix and r.owner == owner and r.methods == norm_methods)
|
|
107
|
+
]
|
|
108
|
+
self._http_routes.append(HttpRoute(
|
|
109
|
+
prefix=clean_prefix,
|
|
110
|
+
handler=handler,
|
|
111
|
+
methods=norm_methods,
|
|
112
|
+
owner=owner,
|
|
113
|
+
))
|
|
114
|
+
# Sort by longest prefix first
|
|
115
|
+
self._http_routes.sort(key=lambda r: len(r.prefix), reverse=True)
|
|
116
|
+
|
|
117
|
+
def mount_websocket(
|
|
118
|
+
self,
|
|
119
|
+
path: str,
|
|
120
|
+
handler: Callable[[ws.WebSocket, str], Any],
|
|
121
|
+
owner: str = '',
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Mount a WebSocket handler for an exact path or prefix."""
|
|
124
|
+
clean_path = '/' + path.strip('/') if path != '/' else '/'
|
|
125
|
+
with self._lock:
|
|
126
|
+
self._ws_routes = [
|
|
127
|
+
r for r in self._ws_routes
|
|
128
|
+
if not (r.path == clean_path and r.owner == owner)
|
|
129
|
+
]
|
|
130
|
+
self._ws_routes.append(WebSocketRoute(
|
|
131
|
+
path=clean_path,
|
|
132
|
+
handler=handler,
|
|
133
|
+
owner=owner,
|
|
134
|
+
))
|
|
135
|
+
self._ws_routes.sort(key=lambda r: len(r.path), reverse=True)
|
|
136
|
+
|
|
137
|
+
def unmount_all(self, owner: str) -> None:
|
|
138
|
+
"""Remove all HTTP and WebSocket routes registered by owner."""
|
|
139
|
+
if not owner:
|
|
140
|
+
return
|
|
141
|
+
with self._lock:
|
|
142
|
+
self._http_routes = [r for r in self._http_routes if r.owner != owner]
|
|
143
|
+
self._ws_routes = [r for r in self._ws_routes if r.owner != owner]
|
|
144
|
+
|
|
145
|
+
# Compatibility aliases conforming to ProtocolHostRouter interface
|
|
146
|
+
def register_route(self, method: str, prefix: str, handler: Callable[..., Any], owner: str = '') -> None:
|
|
147
|
+
"""Alias for mount_route for compatibility."""
|
|
148
|
+
self.mount_route(prefix=prefix, handler=handler, methods=(method,), owner=owner)
|
|
149
|
+
|
|
150
|
+
def register_websocket(self, path: str, handler: Callable[..., Any], owner: str = '') -> None:
|
|
151
|
+
"""Alias for mount_websocket for compatibility."""
|
|
152
|
+
self.mount_websocket(path=path, handler=handler, owner=owner)
|
|
153
|
+
|
|
154
|
+
def unregister_owner(self, owner: str) -> None:
|
|
155
|
+
"""Alias for unmount_all for compatibility."""
|
|
156
|
+
self.unmount_all(owner=owner)
|
|
157
|
+
|
|
158
|
+
# ---- Lifecycle -----------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def start(self) -> ProtocolHost:
|
|
161
|
+
with self._lock:
|
|
162
|
+
if self._started:
|
|
163
|
+
return self
|
|
164
|
+
self._server = ws.Server(
|
|
165
|
+
handler=self._dispatch_websocket,
|
|
166
|
+
host=self.host,
|
|
167
|
+
port=self.port,
|
|
168
|
+
http_handler=self._dispatch_http,
|
|
169
|
+
bind_wait=self.bind_wait,
|
|
170
|
+
)
|
|
171
|
+
self.host = self._server.host
|
|
172
|
+
self.port = self._server.port
|
|
173
|
+
self._server.start()
|
|
174
|
+
self._started = True
|
|
175
|
+
log.info("ProtocolHost serving on %s (%s)", self.url, self.http_url)
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def server(self) -> Optional[Any]:
|
|
180
|
+
"""Underlying ws.Server instance, preserving socket address metadata even after stop."""
|
|
181
|
+
return self._server
|
|
182
|
+
|
|
183
|
+
def stop(self) -> None:
|
|
184
|
+
with self._lock:
|
|
185
|
+
if not self._started or self._server is None:
|
|
186
|
+
return
|
|
187
|
+
self._server.stop()
|
|
188
|
+
self._started = False
|
|
189
|
+
|
|
190
|
+
# ---- Multiplexing Dispatch -----------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _dispatch_http(self, conn: socket.socket, req_or_path: Any, headers: Optional[dict[bytes, bytes]] = None) -> None:
|
|
193
|
+
self._requests_in += 1
|
|
194
|
+
if isinstance(req_or_path, ws.PlainHTTP) or hasattr(req_or_path, 'headers'):
|
|
195
|
+
req = req_or_path
|
|
196
|
+
else:
|
|
197
|
+
path = str(req_or_path)
|
|
198
|
+
hdrs = headers or {}
|
|
199
|
+
req = ws.PlainHTTP(path=path, headers=hdrs, method='GET', sock=conn)
|
|
200
|
+
|
|
201
|
+
clean_path = req.path.split('?', 1)[0]
|
|
202
|
+
method = req.method.upper()
|
|
203
|
+
|
|
204
|
+
# Enforce max body size from Content-Length header if present
|
|
205
|
+
cl_val = None
|
|
206
|
+
if hasattr(req, 'headers') and isinstance(req.headers, Mapping):
|
|
207
|
+
cl_val = req.headers.get(b'content-length') or req.headers.get('content-length') or req.headers.get('Content-Length')
|
|
208
|
+
if cl_val:
|
|
209
|
+
try:
|
|
210
|
+
cl_int = int(cl_val)
|
|
211
|
+
if cl_int > self.max_body_bytes:
|
|
212
|
+
ws.http_response(conn, '413 Payload Too Large',
|
|
213
|
+
f'{{"error":"payload_too_large","max_bytes":{self.max_body_bytes}}}'.encode('utf-8'),
|
|
214
|
+
'application/json')
|
|
215
|
+
self._requests_out += 1
|
|
216
|
+
return
|
|
217
|
+
if cl_int < 0:
|
|
218
|
+
ws.http_response(conn, '400 Bad Request',
|
|
219
|
+
b'{"error":"invalid_content_length"}',
|
|
220
|
+
'application/json')
|
|
221
|
+
self._requests_out += 1
|
|
222
|
+
return
|
|
223
|
+
except (ValueError, TypeError):
|
|
224
|
+
ws.http_response(conn, '400 Bad Request',
|
|
225
|
+
b'{"error":"invalid_content_length"}',
|
|
226
|
+
'application/json')
|
|
227
|
+
self._requests_out += 1
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
with self._lock:
|
|
231
|
+
routes = list(self._http_routes)
|
|
232
|
+
|
|
233
|
+
method_mismatch = False
|
|
234
|
+
allowed_methods: set[str] = set()
|
|
235
|
+
|
|
236
|
+
for route in routes:
|
|
237
|
+
prefix = route.prefix
|
|
238
|
+
# Match if path equals prefix or starts with prefix/
|
|
239
|
+
matched = False
|
|
240
|
+
if prefix == '/':
|
|
241
|
+
matched = True
|
|
242
|
+
elif clean_path == prefix or clean_path.startswith(prefix + '/') or clean_path.startswith(prefix + '?'):
|
|
243
|
+
matched = True
|
|
244
|
+
|
|
245
|
+
if matched:
|
|
246
|
+
if '*' in route.methods or method in route.methods:
|
|
247
|
+
try:
|
|
248
|
+
handled = route.handler(conn, req)
|
|
249
|
+
if handled is not False:
|
|
250
|
+
self._requests_out += 1
|
|
251
|
+
return
|
|
252
|
+
except Exception as e:
|
|
253
|
+
err = f"route {route.prefix} error: {type(e).__name__}: {e}"
|
|
254
|
+
self._errors.append(err)
|
|
255
|
+
log.exception("ProtocolHost HTTP error on %s %s: %s", method, clean_path, e)
|
|
256
|
+
try:
|
|
257
|
+
ws.http_response(conn, '500 Internal Server Error',
|
|
258
|
+
b'{"error":"internal_server_error"}',
|
|
259
|
+
'application/json')
|
|
260
|
+
except OSError:
|
|
261
|
+
pass
|
|
262
|
+
return
|
|
263
|
+
else:
|
|
264
|
+
method_mismatch = True
|
|
265
|
+
allowed_methods.update(route.methods)
|
|
266
|
+
|
|
267
|
+
# Handle CORS OPTIONS pre-flight if unhandled
|
|
268
|
+
if method == 'OPTIONS':
|
|
269
|
+
headers_map = {
|
|
270
|
+
'Allow': ', '.join(sorted(allowed_methods or {'GET', 'POST', 'OPTIONS'})),
|
|
271
|
+
}
|
|
272
|
+
ws.http_response(conn, '204 No Content', b'', 'text/plain', headers=headers_map)
|
|
273
|
+
self._requests_out += 1
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
if method_mismatch and allowed_methods:
|
|
277
|
+
headers_map = {'Allow': ', '.join(sorted(allowed_methods))}
|
|
278
|
+
ws.http_response(conn, '405 Method Not Allowed',
|
|
279
|
+
b'{"error":"method_not_allowed"}',
|
|
280
|
+
'application/json', headers=headers_map)
|
|
281
|
+
self._requests_out += 1
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# 404 fallback
|
|
285
|
+
ws.http_response(conn, '404 Not Found',
|
|
286
|
+
f'{{"error":"not_found","path":"{clean_path}"}}'.encode('utf-8'),
|
|
287
|
+
'application/json')
|
|
288
|
+
self._requests_out += 1
|
|
289
|
+
|
|
290
|
+
def _dispatch_websocket(self, sock: ws.WebSocket, path: str) -> None:
|
|
291
|
+
self._active_connections += 1
|
|
292
|
+
clean_path = path.split('?', 1)[0]
|
|
293
|
+
with self._lock:
|
|
294
|
+
routes = list(self._ws_routes)
|
|
295
|
+
|
|
296
|
+
handler: Optional[Callable[[ws.WebSocket, str], Any]] = None
|
|
297
|
+
for r in routes:
|
|
298
|
+
if r.path == '/' or clean_path == r.path or clean_path.startswith(r.path + '/'):
|
|
299
|
+
handler = r.handler
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
if handler is None:
|
|
303
|
+
# No matching websocket route
|
|
304
|
+
try:
|
|
305
|
+
sock.close()
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
self._active_connections = max(0, self._active_connections - 1)
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
handler(sock, path)
|
|
313
|
+
except (ws.ConnectionClosed, ConnectionResetError, BrokenPipeError, OSError):
|
|
314
|
+
pass
|
|
315
|
+
except Exception as e:
|
|
316
|
+
err = f"ws {clean_path} error: {type(e).__name__}: {e}"
|
|
317
|
+
self._errors.append(err)
|
|
318
|
+
log.exception("ProtocolHost WebSocket error on %s: %s", clean_path, e)
|
|
319
|
+
finally:
|
|
320
|
+
self._active_connections = max(0, self._active_connections - 1)
|
|
321
|
+
|
|
322
|
+
# ---- Health & Telemetry --------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def health(self) -> ProtocolHealth:
|
|
325
|
+
with self._lock:
|
|
326
|
+
errs = list(self._errors)
|
|
327
|
+
return ProtocolHealth(
|
|
328
|
+
healthy=self._started,
|
|
329
|
+
status="serving" if self._started else "stopped",
|
|
330
|
+
connections_active=self._active_connections,
|
|
331
|
+
messages_in=self._requests_in,
|
|
332
|
+
messages_out=self._requests_out,
|
|
333
|
+
errors=tuple(errs[-10:]),
|
|
334
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Iroh QUIC / stream transport rung package.
|
|
2
|
+
|
|
3
|
+
Exports the IrohTransport adapter, IrohTicket parser, and QUIC live engine.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .types import IrohHealthReport
|
|
10
|
+
from fleetmesh_core.identity.iroh_ticket import ALPN, IrohTicket
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'ALPN',
|
|
14
|
+
'DEFAULT_PREFERENCE',
|
|
15
|
+
'IrohHealthReport',
|
|
16
|
+
'IrohTicket',
|
|
17
|
+
'IrohTransport',
|
|
18
|
+
'MAX_STREAM_PAYLOAD',
|
|
19
|
+
'SOCK',
|
|
20
|
+
'sidecar_socket_path',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def __getattr__(name: str) -> Any:
|
|
24
|
+
if name in ('IrohTransport', 'DEFAULT_PREFERENCE', 'MAX_STREAM_PAYLOAD', 'SOCK', 'sidecar_socket_path'):
|
|
25
|
+
from . import core
|
|
26
|
+
val = getattr(core, name)
|
|
27
|
+
globals()[name] = val
|
|
28
|
+
return val
|
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|