Skip to content

Commit 9fdc60e

Browse files
authored
feat: add nim peer (#1) (libp2p#308)
1 parent a5bf7c2 commit 9fdc60e

File tree

10 files changed

+669
-0
lines changed

10 files changed

+669
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ yarn.lock
99
.DS_Store
1010
go-peer/go-peer
1111
**/.idea
12+
nim-peer/nim_peer
13+
nim-peer/local.*

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Some of the cool and cutting-edge [transport protocols](https://connectivity.lib
2626
| [`node-js-peer`](./node-js-peer/) | Node.js Chat Peer in TypeScript ||||||
2727
| [`go-peer`](./go-peer/) | Chat peer implemented in Go ||||||
2828
| [`rust-peer`](./rust-peer/) | Chat peer implemented in Rust ||||||
29+
| [`nim-peer`](./nim-peer/) | Chat peer implemented in Nim ||||||
2930

3031
✅ - Protocol supported
3132
❌ - Protocol not supported
@@ -97,3 +98,15 @@ cargo run -- --help
9798
cd go-peer
9899
go run .
99100
```
101+
102+
## Getting started: Nim
103+
```
104+
cd nim-peer
105+
nimble build
106+
107+
# Wait for connections in tcp/9093
108+
./nim_peer
109+
110+
# Connect to another node (e.g. in localhost tcp/9092)
111+
./nim_peer --connect /ip4/127.0.0.1/tcp/9092/p2p/12D3KooSomePeerId
112+
```

nim-peer/config.nims

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# begin Nimble config (version 2)
2+
when withDir(thisDir(), system.fileExists("nimble.paths")):
3+
include "nimble.paths"
4+
--define:
5+
"chronicles_sinks=textblocks[dynamic]"
6+
--define:
7+
"chronicles_log_level=DEBUG"
8+
# end Nimble config

nim-peer/nim_peer.nimble

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Package
2+
3+
version = "0.1.0"
4+
author = "Status Research & Development GmbH"
5+
description = "universal-connectivity nim peer"
6+
license = "MIT"
7+
srcDir = "src"
8+
bin = @["nim_peer"]
9+
10+
11+
# Dependencies
12+
13+
requires "nim >= 2.2.0", "nimwave", "chronos", "chronicles", "libp2p", "illwill", "cligen", "stew"

nim-peer/src/file_exchange.nim

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
import libp2p, chronos, chronicles, stew/byteutils
3+
4+
const
5+
MaxFileSize: int = 1024 # 1KiB
6+
MaxFileIdSize: int = 1024 # 1KiB
7+
FileExchangeCodec*: string = "/universal-connectivity-file/1"
8+
9+
type FileExchange* = ref object of LPProtocol
10+
11+
proc new*(T: typedesc[FileExchange]): T =
12+
proc handle(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} =
13+
try:
14+
let fileId = string.fromBytes(await conn.readLp(MaxFileIdSize))
15+
# filename is /tmp/{fileid}
16+
let filename = getTempDir().joinPath(fileId)
17+
if filename.fileExists:
18+
let fileContent = cast[seq[byte]](readFile(filename))
19+
await conn.writeLp(fileContent)
20+
except CancelledError as e:
21+
raise e
22+
except CatchableError as e:
23+
error "Exception in handler", error = e.msg
24+
finally:
25+
await conn.close()
26+
27+
return T.new(codecs = @[FileExchangeCodec], handler = handle)
28+
29+
proc requestFile*(
30+
p: FileExchange, conn: Connection, fileId: string
31+
): Future[seq[byte]] {.async.} =
32+
await conn.writeLp(cast[seq[byte]](fileId))
33+
await conn.readLp(MaxFileSize)

nim-peer/src/nim_peer.nim

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
{.push raises: [Exception].}
2+
3+
import tables, deques, strutils, os, streams
4+
5+
import libp2p, chronos, cligen, chronicles
6+
from libp2p/protocols/pubsub/rpc/message import Message
7+
8+
from illwave as iw import nil, `[]`, `[]=`, `==`, width, height
9+
from terminal import nil
10+
11+
import ./ui/root
12+
import ./utils
13+
import ./file_exchange
14+
15+
const
16+
KeyFile: string = "local.key"
17+
PeerIdFile: string = "local.peerid"
18+
MaxKeyLen: int = 4096
19+
ListenPort: int = 9093
20+
21+
proc cleanup() {.noconv: (raises: []).} =
22+
try:
23+
iw.deinit()
24+
except:
25+
discard
26+
try:
27+
terminal.resetAttributes()
28+
terminal.showCursor()
29+
# Clear screen and move cursor to top-left
30+
stdout.write("\e[2J\e[H") # ANSI escape: clear screen & home
31+
stdout.flushFile()
32+
quit(130) # SIGINT conventional exit code
33+
except IOError as exc:
34+
echo "Unexpected error: " & exc.msg
35+
quit(1)
36+
37+
proc readKeyFile(
38+
filename: string
39+
): PrivateKey {.raises: [OSError, IOError, ResultError[crypto.CryptoError]].} =
40+
let size = getFileSize(filename)
41+
42+
if size == 0:
43+
raise newException(OSError, "Empty key file")
44+
45+
var buf: seq[byte]
46+
buf.setLen(size)
47+
48+
var fs = openFileStream(filename, fmRead)
49+
defer:
50+
fs.close()
51+
52+
discard fs.readData(buf[0].addr, size.int)
53+
PrivateKey.init(buf).tryGet()
54+
55+
proc writeKeyFile(
56+
filename: string, key: PrivateKey
57+
) {.raises: [OSError, IOError, ResultError[crypto.CryptoError]].} =
58+
var fs = openFileStream(filename, fmWrite)
59+
defer:
60+
fs.close()
61+
62+
let buf = key.getBytes().tryGet()
63+
fs.writeData(buf[0].addr, buf.len)
64+
65+
proc loadOrCreateKey(rng: var HmacDrbgContext): PrivateKey =
66+
if fileExists(KeyFile):
67+
try:
68+
return readKeyFile(KeyFile)
69+
except:
70+
discard # overwrite file
71+
try:
72+
let k = PrivateKey.random(rng).tryGet()
73+
writeKeyFile(KeyFile, k)
74+
k
75+
except:
76+
echo "Could not create new key"
77+
quit(1)
78+
79+
proc start(
80+
addrs: Opt[MultiAddress], headless: bool, room: string, port: int
81+
) {.async: (raises: [CancelledError]).} =
82+
# Handle Ctrl+C
83+
setControlCHook(cleanup)
84+
85+
var rng = newRng()
86+
87+
let switch =
88+
try:
89+
SwitchBuilder
90+
.new()
91+
.withRng(rng)
92+
.withTcpTransport()
93+
.withAddresses(@[MultiAddress.init("/ip4/0.0.0.0/tcp/" & $port).tryGet()])
94+
.withYamux()
95+
.withNoise()
96+
.withPrivateKey(loadOrCreateKey(rng[]))
97+
.build()
98+
except LPError as exc:
99+
echo "Could not start switch: " & $exc.msg
100+
quit(1)
101+
except Exception as exc:
102+
echo "Could not start switch: " & $exc.msg
103+
quit(1)
104+
105+
try:
106+
writeFile(PeerIdFile, $switch.peerInfo.peerId)
107+
except IOError as exc:
108+
error "Could not write PeerId to file", description = exc.msg
109+
110+
let (gossip, fileExchange) =
111+
try:
112+
(GossipSub.init(switch = switch, triggerSelf = true), FileExchange.new())
113+
except InitializationError as exc:
114+
echo "Could not initialize gossipsub: " & $exc.msg
115+
quit(1)
116+
117+
try:
118+
switch.mount(gossip)
119+
switch.mount(fileExchange)
120+
await switch.start()
121+
except LPError as exc:
122+
echo "Could start switch: " & $exc.msg
123+
124+
info "Started switch", peerId = $switch.peerInfo.peerId
125+
126+
let
127+
recvQ = newAsyncQueue[string]()
128+
peerQ = newAsyncQueue[(PeerId, PeerEventKind)]()
129+
systemQ = newAsyncQueue[string]()
130+
131+
# if --connect was specified, connect to peer
132+
if addrs.isSome():
133+
try:
134+
discard await switch.connect(addrs.get())
135+
except Exception as exc:
136+
error "Connection error", description = exc.msg
137+
138+
# wait so that gossipsub can form mesh
139+
await sleepAsync(3.seconds)
140+
141+
# topic handlers
142+
# chat and file handlers actually need to be validators instead of regular handlers
143+
# validators allow us to get information about which peer sent a message
144+
let onChatMsg = proc(
145+
topic: string, msg: Message
146+
): Future[ValidationResult] {.async, gcsafe.} =
147+
let strMsg = cast[string](msg.data)
148+
await recvQ.put(shortPeerId(msg.fromPeer) & ": " & strMsg)
149+
await systemQ.put("Received message")
150+
await systemQ.put(" Source: " & $msg.fromPeer)
151+
await systemQ.put(" Topic: " & $topic)
152+
await systemQ.put(" Seqno: " & $seqnoToUint64(msg.seqno))
153+
await systemQ.put(" ") # empty line
154+
return ValidationResult.Accept
155+
156+
# when a new file is announced, download it
157+
let onNewFile = proc(
158+
topic: string, msg: Message
159+
): Future[ValidationResult] {.async, gcsafe.} =
160+
let fileId = sanitizeFileId(cast[string](msg.data))
161+
# this will only work if we're connected to `fromPeer` (since we don't have kad-dht)
162+
let conn = await switch.dial(msg.fromPeer, FileExchangeCodec)
163+
let filePath = getTempDir() / fileId
164+
let fileContents = await fileExchange.requestFile(conn, fileId)
165+
writeFile(filePath, fileContents)
166+
await conn.close()
167+
# Save file in /tmp/fileId
168+
await systemQ.put("Downloaded file to " & filePath)
169+
await systemQ.put(" ") # empty line
170+
return ValidationResult.Accept
171+
172+
# when a new peer is announced
173+
let onNewPeer = proc(topic: string, data: seq[byte]) {.async, gcsafe.} =
174+
let peerId = PeerId.init(data).valueOr:
175+
error "Could not parse PeerId from data", data = $data
176+
return
177+
await peerQ.put((peerId, PeerEventKind.Joined))
178+
179+
# register validators and handlers
180+
181+
# receive chat messages
182+
gossip.subscribe(room, nil)
183+
gossip.addValidator(room, onChatMsg)
184+
185+
# receive files offerings
186+
gossip.subscribe(ChatFileTopic, nil)
187+
gossip.addValidator(ChatFileTopic, onNewFile)
188+
189+
# receive newly connected peers through gossipsub
190+
gossip.subscribe(PeerDiscoveryTopic, onNewPeer)
191+
192+
let onPeerJoined = proc(
193+
peer: PeerId, peerEvent: PeerEvent
194+
) {.gcsafe, async: (raises: [CancelledError]).} =
195+
await peerQ.put((peer, PeerEventKind.Joined))
196+
197+
let onPeerLeft = proc(
198+
peer: PeerId, peerEvent: PeerEvent
199+
) {.gcsafe, async: (raises: [CancelledError]).} =
200+
await peerQ.put((peer, PeerEventKind.Left))
201+
202+
# receive newly connected peers through direct connections
203+
switch.addPeerEventHandler(onPeerJoined, PeerEventKind.Joined)
204+
switch.addPeerEventHandler(onPeerLeft, PeerEventKind.Left)
205+
206+
# add already connected peers
207+
for peerId in switch.peerStore[AddressBook].book.keys:
208+
await peerQ.put((peerId, PeerEventKind.Joined))
209+
210+
if headless:
211+
runForever()
212+
else:
213+
try:
214+
await runUI(gossip, room, recvQ, peerQ, systemQ, switch.peerInfo.peerId)
215+
except Exception as exc:
216+
error "Unexpected error", description = exc.msg
217+
finally:
218+
if switch != nil:
219+
await switch.stop()
220+
try:
221+
cleanup()
222+
except:
223+
discard
224+
225+
proc cli(connect = "", room = ChatTopic, port = ListenPort, headless = false) =
226+
var addrs = Opt.none(MultiAddress)
227+
if connect.len > 0:
228+
addrs = Opt.some(MultiAddress.init(connect).get())
229+
try:
230+
waitFor start(addrs, headless, room, port)
231+
except CancelledError:
232+
echo "Operation cancelled"
233+
234+
when isMainModule:
235+
dispatch cli,
236+
help = {
237+
"connect": "full multiaddress (with /p2p/ peerId) of the node to connect to",
238+
"room": "Room name",
239+
"port": "TCP listen port",
240+
"headless": "No UI, can only receive messages",
241+
}

nim-peer/src/ui/context.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type State* = object
2+
inputBuffer*: string
3+
4+
include nimwave/prelude

0 commit comments

Comments
 (0)