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
sidecar/fleetmesh_p2p.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The libp2p half of the direct P2P transport, in its own process.
|
|
3
|
+
|
|
4
|
+
fleetmesh-p2p DIR [--port N] [--socket PATH] [--listen MULTIADDR]...
|
|
5
|
+
|
|
6
|
+
Serves the stream protocol `/fleetmesh/didcomm/1.0.0` (schema/messages.json,
|
|
7
|
+
`transports.libp2p`) and dials it. One stream carries one message: the dialer
|
|
8
|
+
writes a JWE as a single line, half-closes, and the listener answers one
|
|
9
|
+
JSON line `{"ack": true}` after it has queued the message for the node. The
|
|
10
|
+
node talks to this process over `p2p.sock` in its directory, one JSON object
|
|
11
|
+
per line, the protocol `fleetmesh-p2p/1`, the shape `node/p2p_wire/compartment.py` speaks.
|
|
12
|
+
|
|
13
|
+
**What this process holds.** The Ed25519 transport key, read from
|
|
14
|
+
`transport_seed` in the node's `keys.json`, and nothing else from it. That
|
|
15
|
+
key is the libp2p PeerId (an Ed25519 key is small enough to be inlined under
|
|
16
|
+
an identity multihash), and it is the `key transport` Multikey the node's
|
|
17
|
+
descriptor publishes, so a peer that dials this process proves the same key
|
|
18
|
+
its descriptor claims. `node_sk` is never read here: a compromise of this
|
|
19
|
+
process costs one descriptor republish, which the specification allows to be
|
|
20
|
+
free, and not the node's identity.
|
|
21
|
+
|
|
22
|
+
**What this process does not authenticate.** The libp2p security handshake
|
|
23
|
+
proves which transport key wrote to a stream. The node never sees that
|
|
24
|
+
proof - it reads a line off a local socket, and on a shared host anyone with
|
|
25
|
+
the socket could write one - so the payload is handed over with the remote
|
|
26
|
+
PeerId's raw key and treated by the node exactly as a stream off the internet
|
|
27
|
+
is: a JWE it can open, whose sender is whoever the ECDH-1PU agreement proves
|
|
28
|
+
and never the `from` field. A plaintext body on this path is a drop with a
|
|
29
|
+
reason on the node side.
|
|
30
|
+
|
|
31
|
+
Why a separate process: the same reason as `fleetmesh_announce.py`. `node/`
|
|
32
|
+
depends on `cryptography` alone and `fleetmesh_core/` on nothing; the libp2p stack is
|
|
33
|
+
sixty-odd packages, several compiled, and it runs on trio where the node is
|
|
34
|
+
threads. A node without this process has the relay rung and works.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import argparse
|
|
39
|
+
import collections
|
|
40
|
+
import json
|
|
41
|
+
import os
|
|
42
|
+
import stat
|
|
43
|
+
import sys
|
|
44
|
+
import time
|
|
45
|
+
from typing import Any, Deque, Mapping, Optional
|
|
46
|
+
|
|
47
|
+
import multiaddr
|
|
48
|
+
import trio
|
|
49
|
+
from libp2p import new_host
|
|
50
|
+
from libp2p.crypto.ed25519 import create_new_key_pair
|
|
51
|
+
from libp2p.custom_types import TProtocol
|
|
52
|
+
from libp2p.network.stream.exceptions import StreamEOF, StreamError
|
|
53
|
+
from libp2p.peer.id import ID
|
|
54
|
+
from libp2p.peer.peerinfo import info_from_p2p_addr
|
|
55
|
+
|
|
56
|
+
_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
|
57
|
+
sys.path.insert(0, _ROOT)
|
|
58
|
+
from node.planes.transport.p2p.compartment import (DRAIN_MAX, MAX_LINE, PROTOCOL, # noqa: E402
|
|
59
|
+
STREAM_PROTOCOL, socket_path)
|
|
60
|
+
|
|
61
|
+
PROTO: TProtocol = TProtocol(STREAM_PROTOCOL)
|
|
62
|
+
# Inbound messages held for the node to drain. The node ticks every thirty
|
|
63
|
+
# seconds; a burst must not outgrow the interval, so the oldest goes and the
|
|
64
|
+
# loss is counted rather than hidden.
|
|
65
|
+
INBOUND_MAX: int = 256
|
|
66
|
+
# A JWE for one DIDComm message. Anything larger is refused before it is
|
|
67
|
+
# buffered, on both the stream and the control socket.
|
|
68
|
+
MESSAGE_MAX: int = MAX_LINE
|
|
69
|
+
# How long one dial, one write and one ack may take together. The ladder's
|
|
70
|
+
# attempt budget is five seconds (schema/constants.json transport), so a
|
|
71
|
+
# rung that takes longer has already lost to the next one.
|
|
72
|
+
DIAL_TIMEOUT: float = 5.0
|
|
73
|
+
# A dialer that opens a stream and then says nothing holds a task; this is
|
|
74
|
+
# how long it may do so.
|
|
75
|
+
INBOUND_TIMEOUT: float = 10.0
|
|
76
|
+
# The control socket: a client that connects and does not finish a line.
|
|
77
|
+
CONTROL_TIMEOUT: float = 10.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def transport_seed(directory: str) -> bytes:
|
|
81
|
+
"""The Ed25519 seed out of the node's key file, and nothing else from it."""
|
|
82
|
+
path = os.path.join(directory, 'keys.json')
|
|
83
|
+
mode = stat.S_IMODE(os.stat(path).st_mode)
|
|
84
|
+
if mode & 0o077:
|
|
85
|
+
raise PermissionError('%s is readable by others (mode %o); refusing to load' % (path, mode))
|
|
86
|
+
with open(path, encoding='utf-8') as f:
|
|
87
|
+
d = json.load(f)
|
|
88
|
+
if d.get('format') != 'meshnode-keys/1':
|
|
89
|
+
raise ValueError('not a meshnode key file')
|
|
90
|
+
seed = bytes.fromhex(d['transport_seed'])
|
|
91
|
+
if len(seed) != 32:
|
|
92
|
+
raise ValueError('the transport seed is 32 bytes')
|
|
93
|
+
return seed
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def ed25519_of(peer: ID) -> Optional[str]:
|
|
97
|
+
"""The raw Ed25519 public key a PeerId carries, as hex, or None.
|
|
98
|
+
|
|
99
|
+
An RSA or secp256k1 peer has a hashed id and returns None; fleetmesh nodes
|
|
100
|
+
are Ed25519 by the key table, so such a peer is refused.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
pub = peer.extract_public_key()
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
if pub is None:
|
|
107
|
+
return None
|
|
108
|
+
raw = pub.to_bytes()
|
|
109
|
+
return raw.hex() if len(raw) == 32 else None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def read_line(stream: Any, limit: int, timeout: float) -> Optional[bytes]:
|
|
113
|
+
"""One newline-terminated line off a stream, or None when it is over
|
|
114
|
+
`limit`, unterminated at EOF, or slower than `timeout`.
|
|
115
|
+
|
|
116
|
+
libp2p signals EOF by raising rather than by returning b'', so both
|
|
117
|
+
spellings end the read.
|
|
118
|
+
"""
|
|
119
|
+
buf = b''
|
|
120
|
+
with trio.move_on_after(timeout) as scope:
|
|
121
|
+
while len(buf) <= limit:
|
|
122
|
+
try:
|
|
123
|
+
chunk = await stream.read(65536)
|
|
124
|
+
except (StreamEOF, StreamError):
|
|
125
|
+
break
|
|
126
|
+
if not chunk:
|
|
127
|
+
break
|
|
128
|
+
buf += chunk
|
|
129
|
+
if buf.endswith(b'\n'):
|
|
130
|
+
break
|
|
131
|
+
if scope.cancelled_caught or not buf.endswith(b'\n') or len(buf) > limit + 1:
|
|
132
|
+
return None
|
|
133
|
+
return buf
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class P2P:
|
|
137
|
+
"""The host, what it carried, and what it refused."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, now: Any = None) -> None:
|
|
140
|
+
self.now = now or (lambda: int(time.time()))
|
|
141
|
+
self.host: Any = None
|
|
142
|
+
self.inbound: Deque[Mapping[str, Any]] = collections.deque(maxlen=INBOUND_MAX)
|
|
143
|
+
self.refused: collections.Counter[str] = collections.Counter()
|
|
144
|
+
self.received: int = 0
|
|
145
|
+
self.sent: int = 0
|
|
146
|
+
self.overflowed: int = 0
|
|
147
|
+
self.sent_bytes: int = 0
|
|
148
|
+
self.recv_bytes: int = 0
|
|
149
|
+
|
|
150
|
+
# ---- the stream protocol ------------------------------------------------
|
|
151
|
+
async def handle(self, stream: Any) -> None:
|
|
152
|
+
"""One inbound stream: one line, one ack."""
|
|
153
|
+
try:
|
|
154
|
+
remote = stream.muxed_conn.peer_id
|
|
155
|
+
origin = ed25519_of(remote)
|
|
156
|
+
if origin is None:
|
|
157
|
+
self.refused['originator is not an Ed25519 peer'] += 1
|
|
158
|
+
await stream.reset()
|
|
159
|
+
return
|
|
160
|
+
line = await read_line(stream, MESSAGE_MAX, INBOUND_TIMEOUT)
|
|
161
|
+
if line is None:
|
|
162
|
+
self.refused['no complete line within %d bytes and %ds' % (MESSAGE_MAX, INBOUND_TIMEOUT)] += 1
|
|
163
|
+
await stream.reset()
|
|
164
|
+
return
|
|
165
|
+
try:
|
|
166
|
+
msg = json.loads(line)
|
|
167
|
+
except ValueError:
|
|
168
|
+
self.refused['not JSON'] += 1
|
|
169
|
+
await stream.reset()
|
|
170
|
+
return
|
|
171
|
+
if not isinstance(msg, dict):
|
|
172
|
+
self.refused['not an object'] += 1
|
|
173
|
+
await stream.reset()
|
|
174
|
+
return
|
|
175
|
+
if len(self.inbound) == self.inbound.maxlen:
|
|
176
|
+
self.overflowed += 1
|
|
177
|
+
self.inbound.append({'from_peer': str(remote), 'from_key': origin,
|
|
178
|
+
'message': msg, 'at': self.now()})
|
|
179
|
+
self.received += 1
|
|
180
|
+
self.recv_bytes += len(line)
|
|
181
|
+
await stream.write(b'{"ack":true}\n')
|
|
182
|
+
await stream.close()
|
|
183
|
+
except (StreamEOF, StreamError, trio.BrokenResourceError, trio.ClosedResourceError, OSError):
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
async def send(self, target: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
187
|
+
"""Dial `target` and deliver one message; accepted only after the ack.
|
|
188
|
+
|
|
189
|
+
`target` is a full multiaddr ending in /p2p/<PeerId>. The PeerId is
|
|
190
|
+
what the connection authenticates, so a caller that learned it from
|
|
191
|
+
the peer's descriptor has bound the pipe to the descriptor's key.
|
|
192
|
+
"""
|
|
193
|
+
if self.host is None:
|
|
194
|
+
raise RuntimeError('the host is not up yet')
|
|
195
|
+
body = json.dumps(payload, separators=(',', ':'), sort_keys=True).encode('utf-8') + b'\n'
|
|
196
|
+
if len(body) > MESSAGE_MAX:
|
|
197
|
+
raise ValueError('a message of %d bytes is over the %d the stream carries' % (len(body), MESSAGE_MAX))
|
|
198
|
+
info = info_from_p2p_addr(multiaddr.Multiaddr(target))
|
|
199
|
+
ack: Optional[bytes] = None
|
|
200
|
+
with trio.move_on_after(DIAL_TIMEOUT) as scope:
|
|
201
|
+
await self.host.connect(info)
|
|
202
|
+
stream = await self.host.new_stream(info.peer_id, [PROTO])
|
|
203
|
+
try:
|
|
204
|
+
await stream.write(body)
|
|
205
|
+
await stream.close_write()
|
|
206
|
+
ack = await read_line(stream, 1024, DIAL_TIMEOUT)
|
|
207
|
+
finally:
|
|
208
|
+
await stream.close()
|
|
209
|
+
if scope.cancelled_caught:
|
|
210
|
+
raise TimeoutError('%s did not answer within %.0fs' % (target, DIAL_TIMEOUT))
|
|
211
|
+
if ack is None or not json.loads(ack).get('ack'):
|
|
212
|
+
raise RuntimeError('%s took the stream but did not acknowledge' % target)
|
|
213
|
+
self.sent += 1
|
|
214
|
+
self.sent_bytes += len(body)
|
|
215
|
+
return {'accepted': True, 'peer_id': str(info.peer_id), 'bytes': len(body)}
|
|
216
|
+
|
|
217
|
+
# ---- the control socket -------------------------------------------------
|
|
218
|
+
async def serve(self, path: str) -> None:
|
|
219
|
+
# Refuse to take a socket another instance is answering on; a stale
|
|
220
|
+
# file left by a crash is unlinked.
|
|
221
|
+
if os.path.exists(path):
|
|
222
|
+
probe = trio.socket.socket(trio.socket.AF_UNIX, trio.socket.SOCK_STREAM)
|
|
223
|
+
try:
|
|
224
|
+
await probe.connect(path)
|
|
225
|
+
except OSError:
|
|
226
|
+
os.unlink(path)
|
|
227
|
+
else:
|
|
228
|
+
raise RuntimeError('another fleetmesh-p2p is already serving %s' % path)
|
|
229
|
+
finally:
|
|
230
|
+
probe.close()
|
|
231
|
+
s = trio.socket.socket(trio.socket.AF_UNIX, trio.socket.SOCK_STREAM)
|
|
232
|
+
old = os.umask(0o177)
|
|
233
|
+
try:
|
|
234
|
+
await s.bind(path)
|
|
235
|
+
os.chmod(path, 0o600)
|
|
236
|
+
finally:
|
|
237
|
+
os.umask(old)
|
|
238
|
+
s.listen(8)
|
|
239
|
+
await trio.serve_listeners(self._one, [trio.SocketListener(s)])
|
|
240
|
+
|
|
241
|
+
async def _one(self, stream: trio.SocketStream) -> None:
|
|
242
|
+
try:
|
|
243
|
+
buf = b''
|
|
244
|
+
with trio.move_on_after(CONTROL_TIMEOUT):
|
|
245
|
+
while not buf.endswith(b'\n') and len(buf) <= MAX_LINE:
|
|
246
|
+
chunk = await stream.receive_some(65536)
|
|
247
|
+
if not chunk:
|
|
248
|
+
break
|
|
249
|
+
buf += chunk
|
|
250
|
+
try:
|
|
251
|
+
if not buf.endswith(b'\n') or len(buf) > MAX_LINE:
|
|
252
|
+
raise ValueError('a request is one JSON line under %d bytes' % MAX_LINE)
|
|
253
|
+
req = json.loads(buf.decode('utf-8'))
|
|
254
|
+
if not isinstance(req, dict) or not isinstance(req.get('cmd'), str):
|
|
255
|
+
raise ValueError('a request is {"cmd": ..., "args": {...}}')
|
|
256
|
+
args = req.get('args') or {}
|
|
257
|
+
if not isinstance(args, dict):
|
|
258
|
+
raise ValueError('args is an object')
|
|
259
|
+
result = await self.dispatch(req['cmd'], args)
|
|
260
|
+
reply = {'ok': True, 'protocol': PROTOCOL, 'result': result}
|
|
261
|
+
except Exception as e: # one bad request answers one request
|
|
262
|
+
reply = {'ok': False, 'protocol': PROTOCOL, 'error': '%s: %s' % (type(e).__name__, e)}
|
|
263
|
+
await stream.send_all((json.dumps(reply) + '\n').encode('utf-8'))
|
|
264
|
+
except (trio.BrokenResourceError, trio.ClosedResourceError, OSError):
|
|
265
|
+
pass
|
|
266
|
+
finally:
|
|
267
|
+
await trio.aclose_forcefully(stream)
|
|
268
|
+
|
|
269
|
+
async def dispatch(self, cmd: str, args: Mapping[str, Any]) -> Any:
|
|
270
|
+
match cmd:
|
|
271
|
+
case 'status':
|
|
272
|
+
return self.status()
|
|
273
|
+
case 'drain':
|
|
274
|
+
try:
|
|
275
|
+
want = min(int(args.get('max_items') or DRAIN_MAX), INBOUND_MAX)
|
|
276
|
+
except (TypeError, ValueError):
|
|
277
|
+
want = DRAIN_MAX
|
|
278
|
+
out = [self.inbound.popleft() for _ in range(min(want, len(self.inbound)))]
|
|
279
|
+
refused = dict(self.refused)
|
|
280
|
+
self.refused.clear()
|
|
281
|
+
return {'items': out, 'refused': refused, 'overflowed': self.overflowed}
|
|
282
|
+
case 'send':
|
|
283
|
+
target = args.get('target')
|
|
284
|
+
payload = args.get('payload')
|
|
285
|
+
if not isinstance(target, str) or '/p2p/' not in target:
|
|
286
|
+
raise ValueError('target is a multiaddr ending in /p2p/<PeerId>')
|
|
287
|
+
if not isinstance(payload, dict):
|
|
288
|
+
raise ValueError('payload is one message object')
|
|
289
|
+
return await self.send(target, payload)
|
|
290
|
+
case 'inject':
|
|
291
|
+
# The test hook: what an inbound stream would have queued,
|
|
292
|
+
# without a second host. It is labelled as such so a node
|
|
293
|
+
# cannot mistake it for a stream that was authenticated.
|
|
294
|
+
msg = args.get('message')
|
|
295
|
+
if not isinstance(msg, dict):
|
|
296
|
+
raise ValueError('message is an object')
|
|
297
|
+
self.inbound.append({'from_peer': str(args.get('from_peer', 'injected')),
|
|
298
|
+
'from_key': None, 'message': msg, 'at': self.now(),
|
|
299
|
+
'injected': True})
|
|
300
|
+
return {'injected': True}
|
|
301
|
+
case _:
|
|
302
|
+
raise KeyError(cmd)
|
|
303
|
+
|
|
304
|
+
def status(self) -> Mapping[str, Any]:
|
|
305
|
+
addrs = [str(a) for a in self.host.get_addrs()] if self.host is not None else []
|
|
306
|
+
return {
|
|
307
|
+
'protocol': PROTOCOL,
|
|
308
|
+
'stream_protocol': STREAM_PROTOCOL,
|
|
309
|
+
'peer_id': str(self.host.get_id()) if self.host is not None else None,
|
|
310
|
+
'transport_key': ed25519_of(self.host.get_id()) if self.host is not None else None,
|
|
311
|
+
'addrs': addrs,
|
|
312
|
+
'connected_peers': len(self.host.get_connected_peers()) if self.host is not None else 0,
|
|
313
|
+
'inbound_buffered': len(self.inbound),
|
|
314
|
+
'received': self.received,
|
|
315
|
+
'sent': self.sent,
|
|
316
|
+
'sent_bytes': self.sent_bytes,
|
|
317
|
+
'recv_bytes': self.recv_bytes,
|
|
318
|
+
'overflowed': self.overflowed,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def run(directory: str, port: int, listen: list[str], path: Optional[str] = None) -> None:
|
|
323
|
+
key_pair = create_new_key_pair(transport_seed(directory))
|
|
324
|
+
host = new_host(key_pair=key_pair)
|
|
325
|
+
state = P2P()
|
|
326
|
+
state.host = host
|
|
327
|
+
sock = path or socket_path(directory)
|
|
328
|
+
addrs = [multiaddr.Multiaddr(a) for a in listen] or [multiaddr.Multiaddr('/ip4/0.0.0.0/tcp/%d' % port)]
|
|
329
|
+
async with host.run(listen_addrs=addrs), trio.open_nursery() as nursery:
|
|
330
|
+
host.set_stream_handler(PROTO, state.handle)
|
|
331
|
+
print('%s peer %s' % (STREAM_PROTOCOL, host.get_id()), flush=True)
|
|
332
|
+
for a in host.get_addrs():
|
|
333
|
+
print(' listening %s' % a, flush=True)
|
|
334
|
+
nursery.start_soon(state.serve, sock)
|
|
335
|
+
print(' socket %s' % sock, flush=True)
|
|
336
|
+
await trio.sleep_forever()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _leaves(e: BaseException) -> list[BaseException]:
|
|
340
|
+
"""The real exceptions under however many ExceptionGroups trio nested."""
|
|
341
|
+
subs = getattr(e, 'exceptions', None)
|
|
342
|
+
if not subs:
|
|
343
|
+
return [e]
|
|
344
|
+
out: list[BaseException] = []
|
|
345
|
+
for s in subs:
|
|
346
|
+
out.extend(_leaves(s))
|
|
347
|
+
return out
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
351
|
+
ap = argparse.ArgumentParser(prog='fleetmesh-p2p', description=__doc__.splitlines()[0])
|
|
352
|
+
ap.add_argument('directory', help='the node directory, for keys.json and the socket')
|
|
353
|
+
ap.add_argument('--port', type=int, default=0, help='tcp port to listen on (0 asks the kernel)')
|
|
354
|
+
ap.add_argument('--listen', action='append', default=[], metavar='MULTIADDR',
|
|
355
|
+
help='listen on this multiaddr instead of /ip4/0.0.0.0/tcp/PORT; repeatable')
|
|
356
|
+
ap.add_argument('--socket', default=None, help='where to listen (default: p2p.sock in the directory)')
|
|
357
|
+
a = ap.parse_args(argv)
|
|
358
|
+
try:
|
|
359
|
+
trio.run(run, a.directory, a.port, a.listen, a.socket)
|
|
360
|
+
except KeyboardInterrupt:
|
|
361
|
+
return 0
|
|
362
|
+
except Exception as e:
|
|
363
|
+
# A refusal at startup - a live socket, a loose keys.json, a missing
|
|
364
|
+
# key file - is one line, not a nursery traceback. trio wraps what a
|
|
365
|
+
# nursery task raised in an ExceptionGroup; unwrap for the message.
|
|
366
|
+
for inner in _leaves(e):
|
|
367
|
+
print('fleetmesh-p2p: %s: %s' % (type(inner).__name__, inner), file=sys.stderr, flush=True)
|
|
368
|
+
return 1
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == '__main__':
|
|
373
|
+
raise SystemExit(main())
|