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.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -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())