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,666 @@
|
|
|
1
|
+
"""FIPS Native Datagram API Transport Client for Fleetmesh.
|
|
2
|
+
|
|
3
|
+
Spec reference: docs/fips-native-transport.md.
|
|
4
|
+
Connects to the FIPS v0.5.1 native API socket (/var/run/fips/api.sock, ~/.fips/api.sock, ...),
|
|
5
|
+
requests FSP service flows and listener descriptors via SCM_RIGHTS, and exchanges binary-framed
|
|
6
|
+
datagrams addressed to peer npub identities without requiring TUN/IPv6 adapters or root privileges.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import array
|
|
11
|
+
import errno
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import socket
|
|
15
|
+
import struct
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Mapping, MutableMapping, Optional, Union
|
|
19
|
+
|
|
20
|
+
from .types import FipsArrivalInfo
|
|
21
|
+
|
|
22
|
+
# Protocol Framing Constants (docs/fips-native-transport.md section 6)
|
|
23
|
+
FRAMING_MAGIC: int = 0x4D # 'M'
|
|
24
|
+
FRAMING_VERSION: int = 0x01
|
|
25
|
+
FRAME_MAGIC: bytes = bytes([FRAMING_MAGIC, FRAMING_VERSION])
|
|
26
|
+
HEADER_FORMAT: str = '!BBHBBH'
|
|
27
|
+
HEADER_SIZE: int = struct.calcsize(HEADER_FORMAT) # 8 bytes
|
|
28
|
+
|
|
29
|
+
FLAG_FINAL: int = 0x01
|
|
30
|
+
FLAG_COMPRESSED: int = 0x02
|
|
31
|
+
FLAG_DIDCOMM: int = 0x04
|
|
32
|
+
FLAG_ACK_REQUESTED: int = 0x08
|
|
33
|
+
|
|
34
|
+
# Where a FIPS daemon may be found when the operator did not say. Every path
|
|
35
|
+
# here is one only root or this user can create: /var/run and /run are
|
|
36
|
+
# root-owned, ~/.fips is ours. /tmp/fips/api.sock used to be on this list and
|
|
37
|
+
# is not any more, because any local user can create /tmp/fips first, and a
|
|
38
|
+
# node that then connected to it would hand its flows - and, until the
|
|
39
|
+
# envelope became mandatory on this rung, its peers' plaintext - to whoever
|
|
40
|
+
# squatted the name. An operator with a daemon in /tmp names it explicitly.
|
|
41
|
+
DEFAULT_SOCKET_PATHS: tuple[str, ...] = (
|
|
42
|
+
"/var/run/fips/api.sock",
|
|
43
|
+
"/run/fips/api.sock",
|
|
44
|
+
os.path.expanduser("~/.fips/api.sock"),
|
|
45
|
+
"/var/run/fips/control.sock",
|
|
46
|
+
os.path.expanduser("~/.fips/control.sock"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Standard FSP Service Ports
|
|
50
|
+
PORT_PEERING: int = 11801
|
|
51
|
+
PORT_TELEMETRY: int = 21801
|
|
52
|
+
PORT_ACCOUNTING: int = 30801
|
|
53
|
+
PORT_COMPUTE_MARKET: int = 30810
|
|
54
|
+
PORT_COMPUTE_STREAM: int = 30811
|
|
55
|
+
PORT_COMPUTE_CANCEL: int = 30812
|
|
56
|
+
|
|
57
|
+
# FIPS v0.5.1 advertises max_payload per flow (1170 over a UDP transport,
|
|
58
|
+
# 1362 in-process); fall back to the smaller value when a flow did not say.
|
|
59
|
+
DEFAULT_MAX_PAYLOAD: int = 1170
|
|
60
|
+
# Upper bound on frames per message; larger messages raise so the transport
|
|
61
|
+
# ladder can cascade to a stream-capable rung (iroh / nostr) instead.
|
|
62
|
+
MAX_FRAGMENTS: int = 64
|
|
63
|
+
REASSEMBLY_TIMEOUT_S: float = 30.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def find_api_socket() -> Optional[str]:
|
|
67
|
+
"""Find first existing and accessible FIPS Native API socket path, or None."""
|
|
68
|
+
for p in DEFAULT_SOCKET_PATHS:
|
|
69
|
+
if os.path.exists(p) and os.access(p, os.R_OK | os.W_OK):
|
|
70
|
+
return p
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FipsTransportError(Exception):
|
|
75
|
+
"""Raised when FIPS control or flow operations fail."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def encode_frame(
|
|
79
|
+
seq: int,
|
|
80
|
+
payload: bytes,
|
|
81
|
+
is_final: bool = False,
|
|
82
|
+
is_didcomm: bool = True,
|
|
83
|
+
compressed: bool = False,
|
|
84
|
+
ack_requested: bool = False,
|
|
85
|
+
flags: Optional[int] = None,
|
|
86
|
+
reserved: int = 0,
|
|
87
|
+
) -> bytes:
|
|
88
|
+
"""Pack payload with 8-byte binary framing header.
|
|
89
|
+
|
|
90
|
+
``reserved`` carries the message id used to reassemble fragmented messages.
|
|
91
|
+
"""
|
|
92
|
+
if len(payload) > 65535:
|
|
93
|
+
raise FipsTransportError(f"payload exceeds maximum 65535 bytes ({len(payload)})")
|
|
94
|
+
|
|
95
|
+
if flags is not None:
|
|
96
|
+
computed_flags = flags
|
|
97
|
+
if is_final:
|
|
98
|
+
computed_flags |= FLAG_FINAL
|
|
99
|
+
else:
|
|
100
|
+
computed_flags = 0
|
|
101
|
+
if is_final:
|
|
102
|
+
computed_flags |= FLAG_FINAL
|
|
103
|
+
if compressed:
|
|
104
|
+
computed_flags |= FLAG_COMPRESSED
|
|
105
|
+
if is_didcomm:
|
|
106
|
+
computed_flags |= FLAG_DIDCOMM
|
|
107
|
+
if ack_requested:
|
|
108
|
+
computed_flags |= FLAG_ACK_REQUESTED
|
|
109
|
+
|
|
110
|
+
header = struct.pack(
|
|
111
|
+
HEADER_FORMAT,
|
|
112
|
+
FRAMING_MAGIC,
|
|
113
|
+
FRAMING_VERSION,
|
|
114
|
+
seq & 0xFFFF,
|
|
115
|
+
computed_flags,
|
|
116
|
+
reserved & 0xFF, # message id for fragment reassembly
|
|
117
|
+
len(payload),
|
|
118
|
+
)
|
|
119
|
+
return header + payload
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def decode_frame_ex(data: bytes) -> tuple[int, int, int, bytes]:
|
|
123
|
+
"""Like decode_frame but also returns the message id: (seq, flags, msg_id, payload)."""
|
|
124
|
+
seq, flags, payload = decode_frame(data)
|
|
125
|
+
return seq, flags, data[5], payload
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Reassembler:
|
|
129
|
+
"""Rebuild messages split by FipsNativeTransport.send_message on one flow.
|
|
130
|
+
|
|
131
|
+
Frames with message id 0 are legacy single-frame messages and pass through.
|
|
132
|
+
Frames arrive in order on a FIPS flow but may be lost. A message is
|
|
133
|
+
delivered only when fragments 0..N arrive contiguously for one message id
|
|
134
|
+
and fragment N has FLAG_FINAL; any gap discards the partial message.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, timeout_s: float = REASSEMBLY_TIMEOUT_S, max_fragments: int = MAX_FRAGMENTS,
|
|
138
|
+
clock: Any = time.monotonic) -> None:
|
|
139
|
+
self.timeout_s = timeout_s
|
|
140
|
+
self.max_fragments = max_fragments
|
|
141
|
+
self.clock = clock
|
|
142
|
+
self._msg_id: Optional[int] = None
|
|
143
|
+
self._parts: list[bytes] = []
|
|
144
|
+
self._flags: int = 0
|
|
145
|
+
self._started: float = 0.0
|
|
146
|
+
self.dropped: int = 0
|
|
147
|
+
|
|
148
|
+
def _reset(self, dropped: bool) -> None:
|
|
149
|
+
if dropped and self._parts:
|
|
150
|
+
self.dropped += 1
|
|
151
|
+
self._msg_id, self._parts, self._flags = None, [], 0
|
|
152
|
+
|
|
153
|
+
def feed(self, seq: int, flags: int, msg_id: int, payload: bytes) -> Optional[tuple[int, bytes]]:
|
|
154
|
+
"""Feed one frame; returns (flags, full_payload) when a message completes."""
|
|
155
|
+
if msg_id == 0:
|
|
156
|
+
# Pre-fragmentation sender (reserved byte was always 0): one frame = one message.
|
|
157
|
+
return flags, payload
|
|
158
|
+
now = self.clock()
|
|
159
|
+
if self._parts and now - self._started > self.timeout_s:
|
|
160
|
+
self._reset(dropped=True)
|
|
161
|
+
if seq == 0:
|
|
162
|
+
self._reset(dropped=True)
|
|
163
|
+
self._msg_id, self._flags, self._started = msg_id, flags, now
|
|
164
|
+
elif msg_id != self._msg_id or seq != len(self._parts):
|
|
165
|
+
self._reset(dropped=True) # gap, loss, or interleaving: discard
|
|
166
|
+
return None
|
|
167
|
+
self._parts.append(payload)
|
|
168
|
+
if flags & FLAG_FINAL:
|
|
169
|
+
out = (self._flags | FLAG_FINAL, b"".join(self._parts))
|
|
170
|
+
self._reset(dropped=False)
|
|
171
|
+
return out
|
|
172
|
+
if len(self._parts) >= self.max_fragments:
|
|
173
|
+
self._reset(dropped=True)
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def decode_frame(data: bytes) -> tuple[int, int, bytes]:
|
|
178
|
+
"""Unpack 8-byte binary framing header.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
(seq, flags, payload)
|
|
182
|
+
"""
|
|
183
|
+
if len(data) < HEADER_SIZE:
|
|
184
|
+
raise FipsTransportError(f"frame truncated ({len(data)} < {HEADER_SIZE} bytes)")
|
|
185
|
+
|
|
186
|
+
magic, ver, seq, flags, _, plen = struct.unpack(HEADER_FORMAT, data[:HEADER_SIZE])
|
|
187
|
+
if magic != FRAMING_MAGIC:
|
|
188
|
+
raise FipsTransportError(f"invalid framing magic 0x{magic:02x} (expected 0x{FRAMING_MAGIC:02x})")
|
|
189
|
+
if ver != FRAMING_VERSION:
|
|
190
|
+
raise FipsTransportError(f"unsupported framing version {ver} (expected {FRAMING_VERSION})")
|
|
191
|
+
|
|
192
|
+
payload = data[HEADER_SIZE:HEADER_SIZE + plen]
|
|
193
|
+
if len(payload) != plen:
|
|
194
|
+
raise FipsTransportError(f"payload length mismatch ({len(payload)} != {plen})")
|
|
195
|
+
|
|
196
|
+
return seq, flags, payload
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def pubkey_to_npub(pubkey: object) -> str:
|
|
200
|
+
"""Normalize a pubkey (hex, did:nostr:, or npub) to standard npub format.
|
|
201
|
+
|
|
202
|
+
Takes `object` because config and CLI hand it whatever they parsed; a
|
|
203
|
+
non-string is stringified rather than refused, as before."""
|
|
204
|
+
if not isinstance(pubkey, str):
|
|
205
|
+
return str(pubkey)
|
|
206
|
+
if pubkey.startswith("npub1"):
|
|
207
|
+
return pubkey
|
|
208
|
+
if pubkey.startswith("did:nostr:"):
|
|
209
|
+
pubkey = pubkey[len("did:nostr:"):]
|
|
210
|
+
if len(pubkey) == 64:
|
|
211
|
+
try:
|
|
212
|
+
from fleetmesh_core.crypto.bech32 import bech32_encode
|
|
213
|
+
return bech32_encode("npub", bytes.fromhex(pubkey))
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
return pubkey
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class FipsNativeTransport:
|
|
220
|
+
"""Manages bilateral datagram flows over FIPS Native Control API."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, sock_path: Optional[str] = None, node_pk: Optional[str] = None) -> None:
|
|
223
|
+
self.sock_path = sock_path or self._discover_socket()
|
|
224
|
+
self.node_pk: Optional[str] = node_pk
|
|
225
|
+
self.ctrl_sock: Optional[socket.socket] = None
|
|
226
|
+
self._lock: threading.Lock = threading.Lock()
|
|
227
|
+
self.open_flows: dict[str, socket.socket] = {}
|
|
228
|
+
self.listeners: dict[int, socket.socket] = {}
|
|
229
|
+
# fileno -> max datagram size the daemon reported for that flow
|
|
230
|
+
self.flow_max_payload: dict[int, int] = {}
|
|
231
|
+
self._msg_counter: int = 0
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _discover_socket() -> str:
|
|
235
|
+
for p in DEFAULT_SOCKET_PATHS:
|
|
236
|
+
if os.path.exists(p):
|
|
237
|
+
return p
|
|
238
|
+
return DEFAULT_SOCKET_PATHS[0]
|
|
239
|
+
|
|
240
|
+
def is_available(self) -> bool:
|
|
241
|
+
"""Check if local FIPS control socket exists and is accessible."""
|
|
242
|
+
return os.path.exists(self.sock_path) and os.access(self.sock_path, os.R_OK | os.W_OK)
|
|
243
|
+
|
|
244
|
+
def connect(self) -> None:
|
|
245
|
+
"""Connect to local FIPS Unix control socket."""
|
|
246
|
+
if not self.is_available():
|
|
247
|
+
raise FipsTransportError(f"FIPS control socket not accessible at {self.sock_path}")
|
|
248
|
+
with self._lock:
|
|
249
|
+
if self.ctrl_sock is not None:
|
|
250
|
+
return
|
|
251
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
252
|
+
s.connect(self.sock_path)
|
|
253
|
+
self.ctrl_sock = s
|
|
254
|
+
|
|
255
|
+
def open_flow(self, peer_npub: str, port: int, timeout_s: float = 5.0) -> socket.socket:
|
|
256
|
+
"""Request connected FSP datagram flow descriptor from FIPS via SCM_RIGHTS."""
|
|
257
|
+
if self.ctrl_sock is None:
|
|
258
|
+
self.connect()
|
|
259
|
+
assert self.ctrl_sock is not None
|
|
260
|
+
|
|
261
|
+
norm_peer = pubkey_to_npub(peer_npub)
|
|
262
|
+
params: MutableMapping[str, Any] = {
|
|
263
|
+
"peer": norm_peer,
|
|
264
|
+
"remote_port": int(port),
|
|
265
|
+
}
|
|
266
|
+
cmd_dict: MutableMapping[str, Any] = {
|
|
267
|
+
"command": "connect",
|
|
268
|
+
"params": params,
|
|
269
|
+
}
|
|
270
|
+
if self.node_pk:
|
|
271
|
+
norm_caller = pubkey_to_npub(self.node_pk)
|
|
272
|
+
params["caller"] = norm_caller
|
|
273
|
+
req = json.dumps(cmd_dict) + "\n"
|
|
274
|
+
with self._lock:
|
|
275
|
+
try:
|
|
276
|
+
self.ctrl_sock.settimeout(timeout_s)
|
|
277
|
+
self.ctrl_sock.sendall(req.encode("utf-8"))
|
|
278
|
+
|
|
279
|
+
fds = array.array("i")
|
|
280
|
+
cmsg_space = socket.CMSG_SPACE(fds.itemsize * 4)
|
|
281
|
+
msg, ancdata, flags, addr = self.ctrl_sock.recvmsg(4096, cmsg_space)
|
|
282
|
+
except (OSError, BrokenPipeError, ConnectionResetError) as e:
|
|
283
|
+
try:
|
|
284
|
+
self.ctrl_sock.close()
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
self.ctrl_sock = None
|
|
288
|
+
raise FipsTransportError(f"FIPS control socket disconnected: {e}")
|
|
289
|
+
|
|
290
|
+
if not msg and not ancdata:
|
|
291
|
+
try:
|
|
292
|
+
self.ctrl_sock.close()
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
self.ctrl_sock = None
|
|
296
|
+
raise FipsTransportError("FIPS control socket closed unexpectedly (EOF)")
|
|
297
|
+
|
|
298
|
+
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
|
299
|
+
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
|
|
300
|
+
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
|
301
|
+
if fds:
|
|
302
|
+
raw_fd = fds[0]
|
|
303
|
+
for extra_fd in fds[1:]:
|
|
304
|
+
try:
|
|
305
|
+
os.close(extra_fd)
|
|
306
|
+
except OSError:
|
|
307
|
+
pass
|
|
308
|
+
try:
|
|
309
|
+
flow_sock = socket.fromfd(raw_fd, socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
310
|
+
finally:
|
|
311
|
+
try:
|
|
312
|
+
os.close(raw_fd)
|
|
313
|
+
except OSError:
|
|
314
|
+
pass
|
|
315
|
+
key = f"{peer_npub}:{port}"
|
|
316
|
+
self.open_flows[key] = flow_sock
|
|
317
|
+
self._record_max_payload(flow_sock, msg)
|
|
318
|
+
return flow_sock
|
|
319
|
+
|
|
320
|
+
resp = {}
|
|
321
|
+
if msg:
|
|
322
|
+
try:
|
|
323
|
+
resp = json.loads(msg.decode("utf-8"))
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
err_msg = resp.get("message") or resp.get("error") or "no descriptor received"
|
|
327
|
+
raise FipsTransportError(f"FIPS flow connect failed: {err_msg}")
|
|
328
|
+
|
|
329
|
+
def listen_port(self, port: int, timeout_s: float = 5.0) -> socket.socket:
|
|
330
|
+
"""Register inbound listener on FSP port, receiving descriptor via SCM_RIGHTS."""
|
|
331
|
+
if self.ctrl_sock is None:
|
|
332
|
+
self.connect()
|
|
333
|
+
assert self.ctrl_sock is not None
|
|
334
|
+
|
|
335
|
+
params: MutableMapping[str, Any] = {
|
|
336
|
+
"local_port": int(port),
|
|
337
|
+
}
|
|
338
|
+
cmd_dict: MutableMapping[str, Any] = {
|
|
339
|
+
"command": "listen",
|
|
340
|
+
"params": params,
|
|
341
|
+
}
|
|
342
|
+
if self.node_pk:
|
|
343
|
+
norm_pk = pubkey_to_npub(self.node_pk)
|
|
344
|
+
params["node_pk"] = norm_pk
|
|
345
|
+
req = json.dumps(cmd_dict) + "\n"
|
|
346
|
+
with self._lock:
|
|
347
|
+
try:
|
|
348
|
+
self.ctrl_sock.settimeout(timeout_s)
|
|
349
|
+
self.ctrl_sock.sendall(req.encode("utf-8"))
|
|
350
|
+
|
|
351
|
+
fds = array.array("i")
|
|
352
|
+
cmsg_space = socket.CMSG_SPACE(fds.itemsize * 4)
|
|
353
|
+
msg, ancdata, flags, addr = self.ctrl_sock.recvmsg(4096, cmsg_space)
|
|
354
|
+
except (OSError, BrokenPipeError, ConnectionResetError) as e:
|
|
355
|
+
try:
|
|
356
|
+
self.ctrl_sock.close()
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
self.ctrl_sock = None
|
|
360
|
+
raise FipsTransportError(f"FIPS control socket disconnected: {e}")
|
|
361
|
+
|
|
362
|
+
if not msg and not ancdata:
|
|
363
|
+
try:
|
|
364
|
+
self.ctrl_sock.close()
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
self.ctrl_sock = None
|
|
368
|
+
raise FipsTransportError("FIPS control socket closed unexpectedly (EOF)")
|
|
369
|
+
|
|
370
|
+
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
|
371
|
+
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
|
|
372
|
+
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
|
373
|
+
if fds:
|
|
374
|
+
raw_fd = fds[0]
|
|
375
|
+
for extra_fd in fds[1:]:
|
|
376
|
+
try:
|
|
377
|
+
os.close(extra_fd)
|
|
378
|
+
except OSError:
|
|
379
|
+
pass
|
|
380
|
+
try:
|
|
381
|
+
listener_sock = socket.fromfd(raw_fd, socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
382
|
+
finally:
|
|
383
|
+
try:
|
|
384
|
+
os.close(raw_fd)
|
|
385
|
+
except OSError:
|
|
386
|
+
pass
|
|
387
|
+
self.listeners[port] = listener_sock
|
|
388
|
+
return listener_sock
|
|
389
|
+
|
|
390
|
+
resp = {}
|
|
391
|
+
if msg:
|
|
392
|
+
try:
|
|
393
|
+
resp = json.loads(msg.decode("utf-8"))
|
|
394
|
+
except Exception:
|
|
395
|
+
pass
|
|
396
|
+
err_msg = resp.get("message") or resp.get("error") or "no descriptor received"
|
|
397
|
+
raise FipsTransportError(f"FIPS listen failed: {err_msg}")
|
|
398
|
+
|
|
399
|
+
def accept_flow(
|
|
400
|
+
self,
|
|
401
|
+
listener_sock: socket.socket,
|
|
402
|
+
timeout_s: Optional[float] = 5.0,
|
|
403
|
+
) -> tuple[socket.socket, FipsArrivalInfo]:
|
|
404
|
+
"""Accept an incoming flow on a registered listener socket.
|
|
405
|
+
|
|
406
|
+
In FIPS v0.5.1, when a remote peer initiates a flow to a listening port,
|
|
407
|
+
the listener descriptor receives an arrival notice datagram with SCM_RIGHTS
|
|
408
|
+
carrying the new connected flow datagram socket.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
(flow_socket, arrival_info_dict)
|
|
412
|
+
"""
|
|
413
|
+
if timeout_s is not None:
|
|
414
|
+
listener_sock.settimeout(timeout_s)
|
|
415
|
+
fds = array.array("i")
|
|
416
|
+
cmsg_space = socket.CMSG_SPACE(fds.itemsize * 4)
|
|
417
|
+
msg, ancdata, flags, addr = listener_sock.recvmsg(4096, cmsg_space)
|
|
418
|
+
flow_fd = None
|
|
419
|
+
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
|
420
|
+
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
|
|
421
|
+
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
|
422
|
+
if fds:
|
|
423
|
+
flow_fd = fds[0]
|
|
424
|
+
for extra_fd in fds[1:]:
|
|
425
|
+
try:
|
|
426
|
+
os.close(extra_fd)
|
|
427
|
+
except OSError:
|
|
428
|
+
pass
|
|
429
|
+
break
|
|
430
|
+
if flow_fd is None:
|
|
431
|
+
raise FipsTransportError("no flow descriptor received in listener arrival")
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
flow_sock = socket.fromfd(flow_fd, socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
435
|
+
finally:
|
|
436
|
+
try:
|
|
437
|
+
os.close(flow_fd)
|
|
438
|
+
except OSError:
|
|
439
|
+
pass
|
|
440
|
+
arrival_info: FipsArrivalInfo = {}
|
|
441
|
+
if msg:
|
|
442
|
+
try:
|
|
443
|
+
arrival_info = json.loads(msg.decode("utf-8"))
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
peer = arrival_info.get("peer")
|
|
447
|
+
remote_port = arrival_info.get("remote_port")
|
|
448
|
+
if peer and remote_port:
|
|
449
|
+
self.open_flows[f"{peer}:{remote_port}"] = flow_sock
|
|
450
|
+
self._record_max_payload(flow_sock, arrival_info)
|
|
451
|
+
return flow_sock, arrival_info
|
|
452
|
+
|
|
453
|
+
def accept_listener_event(
|
|
454
|
+
self,
|
|
455
|
+
listener_sock: socket.socket,
|
|
456
|
+
) -> tuple[Optional[socket.socket], Mapping[str, Any], bytes]:
|
|
457
|
+
"""Receive the next incoming event on a registered listener socket.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
(flow_sock, arrival_info, direct_msg)
|
|
461
|
+
If an incoming flow arrived via SCM_RIGHTS: returns (flow_sock, arrival_info, b"")
|
|
462
|
+
If a direct binary frame arrived: returns (None, {}, direct_msg)
|
|
463
|
+
If empty/timeout: returns (None, {}, b"")
|
|
464
|
+
"""
|
|
465
|
+
fds = array.array("i")
|
|
466
|
+
cmsg_space = socket.CMSG_SPACE(fds.itemsize * 4)
|
|
467
|
+
msg, ancdata, flags, addr = listener_sock.recvmsg(65536, cmsg_space)
|
|
468
|
+
if not msg and not ancdata:
|
|
469
|
+
return None, {}, b""
|
|
470
|
+
|
|
471
|
+
flow_fd = None
|
|
472
|
+
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
|
473
|
+
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
|
|
474
|
+
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
|
475
|
+
if fds:
|
|
476
|
+
flow_fd = fds[0]
|
|
477
|
+
for extra_fd in fds[1:]:
|
|
478
|
+
try:
|
|
479
|
+
os.close(extra_fd)
|
|
480
|
+
except OSError:
|
|
481
|
+
pass
|
|
482
|
+
break
|
|
483
|
+
|
|
484
|
+
if flow_fd is not None:
|
|
485
|
+
try:
|
|
486
|
+
flow_sock = socket.fromfd(flow_fd, socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
487
|
+
finally:
|
|
488
|
+
try:
|
|
489
|
+
os.close(flow_fd)
|
|
490
|
+
except OSError:
|
|
491
|
+
pass
|
|
492
|
+
arrival_info = {}
|
|
493
|
+
if msg:
|
|
494
|
+
try:
|
|
495
|
+
arrival_info = json.loads(msg.decode("utf-8"))
|
|
496
|
+
peer = arrival_info.get("peer")
|
|
497
|
+
rport = arrival_info.get("remote_port")
|
|
498
|
+
if peer and rport:
|
|
499
|
+
self.open_flows[f"{peer}:{rport}"] = flow_sock
|
|
500
|
+
except (ValueError, UnicodeDecodeError, AttributeError):
|
|
501
|
+
pass
|
|
502
|
+
self._record_max_payload(flow_sock, arrival_info)
|
|
503
|
+
return flow_sock, arrival_info, b""
|
|
504
|
+
|
|
505
|
+
return None, {}, msg
|
|
506
|
+
|
|
507
|
+
def _record_max_payload(self, flow_sock: socket.socket, reply: Any) -> None:
|
|
508
|
+
"""Remember the per-flow datagram ceiling from a connect reply or arrival."""
|
|
509
|
+
info: Any = reply
|
|
510
|
+
if isinstance(reply, (bytes, bytearray)):
|
|
511
|
+
try:
|
|
512
|
+
info = json.loads(bytes(reply).decode("utf-8"))
|
|
513
|
+
except Exception:
|
|
514
|
+
info = {}
|
|
515
|
+
if isinstance(info, dict) and isinstance(info.get("data"), dict):
|
|
516
|
+
info = info["data"]
|
|
517
|
+
mp = info.get("max_payload") if isinstance(info, dict) else None
|
|
518
|
+
if isinstance(mp, int) and mp > HEADER_SIZE:
|
|
519
|
+
try:
|
|
520
|
+
self.flow_max_payload[flow_sock.fileno()] = mp
|
|
521
|
+
except Exception:
|
|
522
|
+
pass
|
|
523
|
+
|
|
524
|
+
def max_payload_for(self, flow_sock: socket.socket) -> int:
|
|
525
|
+
"""Largest datagram (header included) this flow will carry."""
|
|
526
|
+
try:
|
|
527
|
+
return self.flow_max_payload.get(flow_sock.fileno(), DEFAULT_MAX_PAYLOAD)
|
|
528
|
+
except Exception:
|
|
529
|
+
return DEFAULT_MAX_PAYLOAD
|
|
530
|
+
|
|
531
|
+
def send_message(
|
|
532
|
+
self,
|
|
533
|
+
flow_sock: socket.socket,
|
|
534
|
+
payload: bytes,
|
|
535
|
+
flags: int = 0,
|
|
536
|
+
max_fragments: int = MAX_FRAGMENTS,
|
|
537
|
+
) -> int:
|
|
538
|
+
"""Send one logical message, fragmenting it to fit the flow's max_payload.
|
|
539
|
+
|
|
540
|
+
Every frame carries the same message id in the header's reserved byte
|
|
541
|
+
and a per-message fragment index in ``seq``; the last frame sets
|
|
542
|
+
FLAG_FINAL. Raises FipsTransportError before sending anything if the
|
|
543
|
+
message would need more than ``max_fragments`` frames, so callers can
|
|
544
|
+
fall back to a transport without a datagram ceiling. Returns the number
|
|
545
|
+
of frames sent.
|
|
546
|
+
"""
|
|
547
|
+
raw = bytes(payload)
|
|
548
|
+
chunk = self.max_payload_for(flow_sock) - HEADER_SIZE
|
|
549
|
+
if chunk <= 0:
|
|
550
|
+
raise FipsTransportError("flow max_payload too small for framing header")
|
|
551
|
+
parts = [raw[i:i + chunk] for i in range(0, len(raw), chunk)] or [b""]
|
|
552
|
+
if len(parts) > max_fragments:
|
|
553
|
+
raise FipsTransportError(
|
|
554
|
+
f"message of {len(raw)} bytes needs {len(parts)} frames "
|
|
555
|
+
f"(limit {max_fragments} at {chunk} bytes/frame)"
|
|
556
|
+
)
|
|
557
|
+
with self._lock:
|
|
558
|
+
# ids 1..255; 0 marks a legacy/unfragmented frame
|
|
559
|
+
self._msg_counter = self._msg_counter % 255 + 1
|
|
560
|
+
msg_id = self._msg_counter
|
|
561
|
+
base = flags & ~FLAG_FINAL
|
|
562
|
+
for idx, part in enumerate(parts):
|
|
563
|
+
fl = base | (FLAG_FINAL if idx == len(parts) - 1 else 0)
|
|
564
|
+
packet = encode_frame(seq=idx, payload=part, flags=fl, is_final=False, reserved=msg_id)
|
|
565
|
+
sent = 0
|
|
566
|
+
for attempt in range(10):
|
|
567
|
+
try:
|
|
568
|
+
sent = flow_sock.send(packet)
|
|
569
|
+
break
|
|
570
|
+
except OSError as e:
|
|
571
|
+
if e.errno == getattr(errno, 'ENOBUFS', 55) and attempt < 9:
|
|
572
|
+
time.sleep(0.005 * (attempt + 1))
|
|
573
|
+
continue
|
|
574
|
+
raise
|
|
575
|
+
if sent != len(packet):
|
|
576
|
+
raise FipsTransportError(f"short datagram write ({sent} of {len(packet)} bytes)")
|
|
577
|
+
return len(parts)
|
|
578
|
+
|
|
579
|
+
def send_framed(
|
|
580
|
+
self,
|
|
581
|
+
flow_sock: socket.socket,
|
|
582
|
+
payload: Union[bytes, str, Mapping[str, Any]],
|
|
583
|
+
seq: int = 0,
|
|
584
|
+
is_final: bool = False,
|
|
585
|
+
flags: Optional[int] = None,
|
|
586
|
+
) -> int:
|
|
587
|
+
"""Transmit binary-framed datagram over open flow socket."""
|
|
588
|
+
if isinstance(payload, Mapping):
|
|
589
|
+
raw_bytes = json.dumps(payload, sort_keys=True).encode("utf-8")
|
|
590
|
+
is_didcomm = True
|
|
591
|
+
elif isinstance(payload, str):
|
|
592
|
+
raw_bytes = payload.encode("utf-8")
|
|
593
|
+
is_didcomm = False
|
|
594
|
+
else:
|
|
595
|
+
raw_bytes = bytes(payload)
|
|
596
|
+
is_didcomm = False
|
|
597
|
+
|
|
598
|
+
packet = encode_frame(
|
|
599
|
+
seq=seq,
|
|
600
|
+
payload=raw_bytes,
|
|
601
|
+
is_final=is_final,
|
|
602
|
+
is_didcomm=is_didcomm,
|
|
603
|
+
flags=flags,
|
|
604
|
+
)
|
|
605
|
+
limit = self.max_payload_for(flow_sock)
|
|
606
|
+
if len(packet) > limit:
|
|
607
|
+
raise FipsTransportError(
|
|
608
|
+
f"frame of {len(packet)} bytes exceeds flow max_payload {limit}; use send_message()"
|
|
609
|
+
)
|
|
610
|
+
for attempt in range(10):
|
|
611
|
+
try:
|
|
612
|
+
return flow_sock.send(packet)
|
|
613
|
+
except OSError as e:
|
|
614
|
+
if e.errno == getattr(errno, 'ENOBUFS', 55) and attempt < 9:
|
|
615
|
+
time.sleep(0.005 * (attempt + 1))
|
|
616
|
+
continue
|
|
617
|
+
raise
|
|
618
|
+
return flow_sock.send(packet)
|
|
619
|
+
|
|
620
|
+
def recv_framed(
|
|
621
|
+
self,
|
|
622
|
+
flow_sock: socket.socket,
|
|
623
|
+
timeout_s: Optional[float] = 5.0,
|
|
624
|
+
) -> tuple[int, bool, bytes]:
|
|
625
|
+
"""Receive and decode framed datagram. Returns (seq, is_final, payload_bytes)."""
|
|
626
|
+
if timeout_s is not None:
|
|
627
|
+
flow_sock.settimeout(timeout_s)
|
|
628
|
+
data = flow_sock.recv(65536)
|
|
629
|
+
seq, flags, payload = decode_frame(data)
|
|
630
|
+
is_final = bool(flags & FLAG_FINAL)
|
|
631
|
+
return seq, is_final, payload
|
|
632
|
+
|
|
633
|
+
def recv_raw_frame(
|
|
634
|
+
self,
|
|
635
|
+
flow_sock: socket.socket,
|
|
636
|
+
timeout_s: Optional[float] = 5.0,
|
|
637
|
+
) -> tuple[int, int, bytes]:
|
|
638
|
+
"""Receive and decode raw frame with exact integer bitflags. Returns (seq, flags, payload_bytes)."""
|
|
639
|
+
if timeout_s is not None:
|
|
640
|
+
flow_sock.settimeout(timeout_s)
|
|
641
|
+
data = flow_sock.recv(65536)
|
|
642
|
+
return decode_frame(data)
|
|
643
|
+
|
|
644
|
+
def close(self) -> None:
|
|
645
|
+
"""Close all open flows, listeners, and control connection."""
|
|
646
|
+
with self._lock:
|
|
647
|
+
for s in self.open_flows.values():
|
|
648
|
+
try:
|
|
649
|
+
s.close()
|
|
650
|
+
except Exception:
|
|
651
|
+
pass
|
|
652
|
+
self.open_flows.clear()
|
|
653
|
+
|
|
654
|
+
for s in self.listeners.values():
|
|
655
|
+
try:
|
|
656
|
+
s.close()
|
|
657
|
+
except Exception:
|
|
658
|
+
pass
|
|
659
|
+
self.listeners.clear()
|
|
660
|
+
|
|
661
|
+
if self.ctrl_sock is not None:
|
|
662
|
+
try:
|
|
663
|
+
self.ctrl_sock.close()
|
|
664
|
+
except Exception:
|
|
665
|
+
pass
|
|
666
|
+
self.ctrl_sock = None
|