Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ yarn.lock
.DS_Store
go-peer/go-peer
**/.idea
nim-peer/nim_peer
nim-peer/local.*
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Some of the cool and cutting-edge [transport protocols](https://connectivity.lib
| [`node-js-peer`](./node-js-peer/) | Node.js Chat Peer in TypeScript | ✅ | ✅ | ✅ | ✅ | ✅ |
| [`go-peer`](./go-peer/) | Chat peer implemented in Go | ✅ | ❌ | ✅ | ✅ | ✅ |
| [`rust-peer`](./rust-peer/) | Chat peer implemented in Rust | ❌ | ❌ | ✅ | ✅ | ✅ |
| [`nim-peer`](./nim-peer/) | Chat peer implemented in Nim | ❌ | ❌ | ❌ | ❌ | ✅ |

✅ - Protocol supported
❌ - Protocol not supported
Expand Down Expand Up @@ -97,3 +98,15 @@ cargo run -- --help
cd go-peer
go run .
```

## Getting started: Nim
```
cd nim-peer
nimble build

# Wait for connections in tcp/9093
./nim_peer

# Connect to another node (e.g. in localhost tcp/9092)
./nim_peer --connect /ip4/127.0.0.1/tcp/9092/p2p/12D3KooSomePeerId
```
8 changes: 8 additions & 0 deletions nim-peer/config.nims
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# begin Nimble config (version 2)
when withDir(thisDir(), system.fileExists("nimble.paths")):
include "nimble.paths"
--define:
"chronicles_sinks=textblocks[dynamic]"
--define:
"chronicles_log_level=DEBUG"
# end Nimble config
13 changes: 13 additions & 0 deletions nim-peer/nim_peer.nimble
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Package

version = "0.1.0"
author = "Status Research & Development GmbH"
description = "universal-connectivity nim peer"
license = "MIT"
srcDir = "src"
bin = @["nim_peer"]


# Dependencies

requires "nim >= 2.2.0", "nimwave", "chronos", "chronicles", "libp2p", "illwill", "cligen", "stew"
33 changes: 33 additions & 0 deletions nim-peer/src/file_exchange.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import libp2p, chronos, chronicles, stew/byteutils

const
MaxFileSize: int = 1024 # 1KiB
MaxFileIdSize: int = 1024 # 1KiB
FileExchangeCodec*: string = "/universal-connectivity-file/1"

type FileExchange* = ref object of LPProtocol

proc new*(T: typedesc[FileExchange]): T =
proc handle(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} =
try:
let fileId = string.fromBytes(await conn.readLp(MaxFileIdSize))
# filename is /tmp/{fileid}
let filename = getTempDir().joinPath(fileId)
if filename.fileExists:
let fileContent = cast[seq[byte]](readFile(filename))
await conn.writeLp(fileContent)
except CancelledError as e:
raise e
except CatchableError as e:
error "Exception in handler", error = e.msg
finally:
await conn.close()

return T.new(codecs = @[FileExchangeCodec], handler = handle)

proc requestFile*(
p: FileExchange, conn: Connection, fileId: string
): Future[seq[byte]] {.async.} =
await conn.writeLp(cast[seq[byte]](fileId))
await conn.readLp(MaxFileSize)
241 changes: 241 additions & 0 deletions nim-peer/src/nim_peer.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
{.push raises: [Exception].}

import tables, deques, strutils, os, streams

import libp2p, chronos, cligen, chronicles
from libp2p/protocols/pubsub/rpc/message import Message

from illwave as iw import nil, `[]`, `[]=`, `==`, width, height
from terminal import nil

import ./ui/root
import ./utils
import ./file_exchange

const
KeyFile: string = "local.key"
PeerIdFile: string = "local.peerid"
MaxKeyLen: int = 4096
ListenPort: int = 9093

proc cleanup() {.noconv: (raises: []).} =
try:
iw.deinit()
except:
discard
try:
terminal.resetAttributes()
terminal.showCursor()
# Clear screen and move cursor to top-left
stdout.write("\e[2J\e[H") # ANSI escape: clear screen & home
stdout.flushFile()
quit(130) # SIGINT conventional exit code
except IOError as exc:
echo "Unexpected error: " & exc.msg
quit(1)

proc readKeyFile(
filename: string
): PrivateKey {.raises: [OSError, IOError, ResultError[crypto.CryptoError]].} =
let size = getFileSize(filename)

if size == 0:
raise newException(OSError, "Empty key file")

var buf: seq[byte]
buf.setLen(size)

var fs = openFileStream(filename, fmRead)
defer:
fs.close()

discard fs.readData(buf[0].addr, size.int)
PrivateKey.init(buf).tryGet()

proc writeKeyFile(
filename: string, key: PrivateKey
) {.raises: [OSError, IOError, ResultError[crypto.CryptoError]].} =
var fs = openFileStream(filename, fmWrite)
defer:
fs.close()

let buf = key.getBytes().tryGet()
fs.writeData(buf[0].addr, buf.len)

proc loadOrCreateKey(rng: var HmacDrbgContext): PrivateKey =
if fileExists(KeyFile):
try:
return readKeyFile(KeyFile)
except:
discard # overwrite file
try:
let k = PrivateKey.random(rng).tryGet()
writeKeyFile(KeyFile, k)
k
except:
echo "Could not create new key"
quit(1)

proc start(
addrs: Opt[MultiAddress], headless: bool, room: string, port: int
) {.async: (raises: [CancelledError]).} =
# Handle Ctrl+C
setControlCHook(cleanup)

var rng = newRng()

let switch =
try:
SwitchBuilder
.new()
.withRng(rng)
.withTcpTransport()
.withAddresses(@[MultiAddress.init("/ip4/0.0.0.0/tcp/" & $port).tryGet()])
.withYamux()
.withNoise()
.withPrivateKey(loadOrCreateKey(rng[]))
.build()
except LPError as exc:
echo "Could not start switch: " & $exc.msg
quit(1)
except Exception as exc:
echo "Could not start switch: " & $exc.msg
quit(1)

try:
writeFile(PeerIdFile, $switch.peerInfo.peerId)
except IOError as exc:
error "Could not write PeerId to file", description = exc.msg

let (gossip, fileExchange) =
try:
(GossipSub.init(switch = switch, triggerSelf = true), FileExchange.new())
except InitializationError as exc:
echo "Could not initialize gossipsub: " & $exc.msg
quit(1)

try:
switch.mount(gossip)
switch.mount(fileExchange)
await switch.start()
except LPError as exc:
echo "Could start switch: " & $exc.msg

info "Started switch", peerId = $switch.peerInfo.peerId

let
recvQ = newAsyncQueue[string]()
peerQ = newAsyncQueue[(PeerId, PeerEventKind)]()
systemQ = newAsyncQueue[string]()

# if --connect was specified, connect to peer
if addrs.isSome():
try:
discard await switch.connect(addrs.get())
except Exception as exc:
error "Connection error", description = exc.msg

# wait so that gossipsub can form mesh
await sleepAsync(3.seconds)

# topic handlers
# chat and file handlers actually need to be validators instead of regular handlers
# validators allow us to get information about which peer sent a message
let onChatMsg = proc(
topic: string, msg: Message
): Future[ValidationResult] {.async, gcsafe.} =
let strMsg = cast[string](msg.data)
await recvQ.put(shortPeerId(msg.fromPeer) & ": " & strMsg)
await systemQ.put("Received message")
await systemQ.put(" Source: " & $msg.fromPeer)
await systemQ.put(" Topic: " & $topic)
await systemQ.put(" Seqno: " & $seqnoToUint64(msg.seqno))
await systemQ.put(" ") # empty line
return ValidationResult.Accept

# when a new file is announced, download it
let onNewFile = proc(
topic: string, msg: Message
): Future[ValidationResult] {.async, gcsafe.} =
let fileId = sanitizeFileId(cast[string](msg.data))
# this will only work if we're connected to `fromPeer` (since we don't have kad-dht)
let conn = await switch.dial(msg.fromPeer, FileExchangeCodec)
let filePath = getTempDir() / fileId
let fileContents = await fileExchange.requestFile(conn, fileId)
writeFile(filePath, fileContents)
await conn.close()
# Save file in /tmp/fileId
await systemQ.put("Downloaded file to " & filePath)
await systemQ.put(" ") # empty line
return ValidationResult.Accept

# when a new peer is announced
let onNewPeer = proc(topic: string, data: seq[byte]) {.async, gcsafe.} =
let peerId = PeerId.init(data).valueOr:
error "Could not parse PeerId from data", data = $data
return
await peerQ.put((peerId, PeerEventKind.Joined))

# register validators and handlers

# receive chat messages
gossip.subscribe(room, nil)
gossip.addValidator(room, onChatMsg)

# receive files offerings
gossip.subscribe(ChatFileTopic, nil)
gossip.addValidator(ChatFileTopic, onNewFile)

# receive newly connected peers through gossipsub
gossip.subscribe(PeerDiscoveryTopic, onNewPeer)

let onPeerJoined = proc(
peer: PeerId, peerEvent: PeerEvent
) {.gcsafe, async: (raises: [CancelledError]).} =
await peerQ.put((peer, PeerEventKind.Joined))

let onPeerLeft = proc(
peer: PeerId, peerEvent: PeerEvent
) {.gcsafe, async: (raises: [CancelledError]).} =
await peerQ.put((peer, PeerEventKind.Left))

# receive newly connected peers through direct connections
switch.addPeerEventHandler(onPeerJoined, PeerEventKind.Joined)
switch.addPeerEventHandler(onPeerLeft, PeerEventKind.Left)

# add already connected peers
for peerId in switch.peerStore[AddressBook].book.keys:
await peerQ.put((peerId, PeerEventKind.Joined))

if headless:
runForever()
else:
try:
await runUI(gossip, room, recvQ, peerQ, systemQ, switch.peerInfo.peerId)
except Exception as exc:
error "Unexpected error", description = exc.msg
finally:
if switch != nil:
await switch.stop()
try:
cleanup()
except:
discard

proc cli(connect = "", room = ChatTopic, port = ListenPort, headless = false) =
var addrs = Opt.none(MultiAddress)
if connect.len > 0:
addrs = Opt.some(MultiAddress.init(connect).get())
try:
waitFor start(addrs, headless, room, port)
except CancelledError:
echo "Operation cancelled"

when isMainModule:
dispatch cli,
help = {
"connect": "full multiaddress (with /p2p/ peerId) of the node to connect to",
"room": "Room name",
"port": "TCP listen port",
"headless": "No UI, can only receive messages",
}
4 changes: 4 additions & 0 deletions nim-peer/src/ui/context.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type State* = object
inputBuffer*: string

include nimwave/prelude
Loading