nim-libp2p-experimental/libp2p/debugutils.nim

281 lines
8.2 KiB
Nim

## Nim-LibP2P
## Copyright (c) 2018 Status Research & Development GmbH
## Licensed under either of
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
## at your option.
## This file may not be copied, modified, or distributed except according to
## those terms.
## To enable dump of all incoming and outgoing unencrypted messages you need
## to compile project with ``-d:libp2p_dump`` compile-time option. When this
## option enabled ``nim-libp2p`` will create dumps of unencrypted messages for
## every peer libp2p communicates.
##
## Every file is created with name "<PeerID>.pbcap". One file represents
## all the communication with peer which identified by ``PeerID``.
##
## File can have multiple protobuf encoded messages of this format:
##
## Protobuf's message fields:
## 1. SeqID: optional uint64
## 2. Timestamp: required uint64
## 3. Type: optional uint64
## 4. Direction: required uint64
## 5. LocalAddress: optional bytes
## 6. RemoteAddress: optional bytes
## 7. Message: required bytes
import os, options
import nimcrypto/utils, stew/endians2
import protobuf/minprotobuf, stream/connection, protocols/secure/secure,
multiaddress, peerid, varint, muxers/mplex/coder
from times import getTime, toUnix, fromUnix, nanosecond, format, Time,
NanosecondRange, initTime
from strutils import toHex, repeat
export peerid, options, multiaddress
type
FlowDirection* = enum
Outgoing, Incoming
ProtoMessage* = object
timestamp*: uint64
direction*: FlowDirection
message*: seq[byte]
seqID*: Option[uint64]
mtype*: Option[uint64]
local*: Option[MultiAddress]
remote*: Option[MultiAddress]
const
libp2p_dump_dir* {.strdefine.} = "nim-libp2p"
## default directory where all the dumps will be stored, if the path
## relative it will be created in home directory. You can overload this path
## using ``-d:libp2p_dump_dir=<otherpath>``.
proc getTimestamp(): uint64 =
## This procedure is present because `stdlib.times` missing it.
let time = getTime()
uint64(time.toUnix() * 1_000_000_000 + time.nanosecond())
proc getTimedate(value: uint64): string =
## This procedure is workaround for `stdlib.times` to just convert
## nanoseconds' timestamp to DateTime object.
let time = initTime(int64(value div 1_000_000_000), value mod 1_000_000_000)
time.format("yyyy-MM-dd HH:mm:ss'.'fffzzz")
proc dumpMessage*(conn: SecureConn, direction: FlowDirection,
data: openArray[byte]) =
## Store unencrypted message ``data`` to dump file, all the metadata will be
## extracted from ``conn`` instance.
var pb = initProtoBuffer(options = {WithVarintLength})
pb.write(2, getTimestamp())
pb.write(4, uint64(direction))
if len(conn.peerInfo.addrs) > 0:
pb.write(6, conn.peerInfo.addrs[0])
pb.write(7, data)
pb.finish()
let dirName =
if isAbsolute(libp2p_dump_dir):
libp2p_dump_dir
else:
getHomeDir() / libp2p_dump_dir
let fileName = $(conn.peerInfo.peerId) & ".pbcap"
# This is debugging procedure so it should not generate any exceptions,
# and we going to return at every possible OS error.
if not(dirExists(dirName)):
try:
createDir(dirName)
except CatchableError:
return
let pathName = dirName / fileName
var handle: File
try:
if open(handle, pathName, fmAppend):
discard writeBuffer(handle, addr pb.buffer[pb.offset], pb.getLen())
finally:
close(handle)
proc decodeDumpMessage*(data: openArray[byte]): Option[ProtoMessage] =
## Decode protobuf's message ProtoMessage from array of bytes ``data``.
var
pb = initProtoBuffer(data)
value: uint64
ma1, ma2: MultiAddress
pmsg: ProtoMessage
let res2 = pb.getField(2, pmsg.timestamp)
if res2.isErr() or not(res2.get()):
return none[ProtoMessage]()
let res4 = pb.getField(4, value)
if res4.isErr() or not(res4.get()):
return none[ProtoMessage]()
# `case` statement could not work here with an error "selector must be of an
# ordinal type, float or string"
pmsg.direction =
if value == uint64(Outgoing):
Outgoing
elif value == uint64(Incoming):
Incoming
else:
return none[ProtoMessage]()
let res7 = pb.getField(7, pmsg.message)
if res7.isErr() or not(res7.get()):
return none[ProtoMessage]()
value = 0'u64
let res1 = pb.getField(1, value)
if res1.isOk() and res1.get():
pmsg.seqID = some(value)
value = 0'u64
let res3 = pb.getField(3, value)
if res3.isOk() and res3.get():
pmsg.mtype = some(value)
let res5 = pb.getField(5, ma1)
if res5.isOk() and res5.get():
pmsg.local = some(ma1)
let res6 = pb.getField(6, ma2)
if res6.isOk() and res6.get():
pmsg.remote = some(ma2)
some(pmsg)
iterator messages*(data: seq[byte]): Option[ProtoMessage] =
## Iterate over sequence of bytes and decode all the ``ProtoMessage``
## messages we found.
var value: uint64
var size: int
var offset = 0
while offset < len(data):
value = 0
size = 0
let res = PB.getUVarint(data.toOpenArray(offset, len(data) - 1),
size, value)
if res.isOk():
if (value > 0'u64) and (value < uint64(len(data) - offset)):
offset += size
yield decodeDumpMessage(data.toOpenArray(offset,
offset + int(value) - 1))
# value is previously checked to be less then len(data) which is `int`.
offset += int(value)
else:
break
else:
break
proc dumpHex*(pbytes: openarray[byte], groupBy = 1, ascii = true): string =
## Get hexadecimal dump of memory for array ``pbytes``.
var res = ""
var offset = 0
var ascii = ""
while offset < len(pbytes):
if (offset mod 16) == 0:
res = res & toHex(uint64(offset)) & ": "
for k in 0 ..< groupBy:
let ch = pbytes[offset + k]
ascii.add(if ord(ch) > 31 and ord(ch) < 127: char(ch) else: '.')
let item =
case groupBy:
of 1:
toHex(pbytes[offset])
of 2:
toHex(uint16.fromBytes(pbytes.toOpenArray(offset, len(pbytes) - 1)))
of 4:
toHex(uint32.fromBytes(pbytes.toOpenArray(offset, len(pbytes) - 1)))
of 8:
toHex(uint64.fromBytes(pbytes.toOpenArray(offset, len(pbytes) - 1)))
else:
""
res.add(item)
res.add(" ")
offset = offset + groupBy
if (offset mod 16) == 0:
res.add(" ")
res.add(ascii)
ascii.setLen(0)
res.add("\p")
if (offset mod 16) != 0:
let spacesCount = ((16 - (offset mod 16)) div groupBy) *
(groupBy * 2 + 1) + 1
res = res & repeat(' ', spacesCount)
res = res & ascii
res.add("\p")
res
proc mplexMessage*(data: seq[byte]): string =
var value = 0'u64
var size = 0
let res = PB.getUVarint(data, size, value)
if res.isOk():
if size < len(data) and data[size] == 0x00'u8:
let header = cast[MessageType](value and 0x07'u64)
let ident = (value shr 3)
"mplex: [" & $header & ", ident = " & $ident & "] "
else:
""
else:
""
proc toString*(msg: ProtoMessage, dump = true): string =
## Convert message ``msg`` to its string representation.
## If ``dump`` is ``true`` (default) full hexadecimal dump with ascii will be
## used, otherwise just a simple hexadecimal string will be printed.
var res = getTimedate(msg.timestamp)
let direction =
case msg.direction
of Incoming:
" << "
of Outgoing:
" >> "
let address =
block:
let local =
if msg.local.isSome():
"[" & $(msg.local.get()) & "]"
else:
"[LOCAL]"
let remote =
if msg.remote.isSome():
"[" & $(msg.remote.get()) & "]"
else:
"[REMOTE]"
local & direction & remote
let seqid =
if msg.seqID.isSome():
"seqID = " & $(msg.seqID.get()) & " "
else:
""
let mtype =
if msg.mtype.isSome():
"type = " & $(msg.mtype.get()) & " "
else:
""
res.add(" ")
res.add(address)
res.add(" ")
res.add(mtype)
res.add(seqid)
res.add(mplexMessage(msg.message))
res.add(" ")
res.add("\p")
if dump:
res.add(dumpHex(msg.message))
else:
res.add(utils.toHex(msg.message))
res.add("\p")
res