punchd/examples/rendezvous_server/rendezvous_server.nim

158 lines
5.1 KiB
Nim

import asyncdispatch, asyncnet, os, sequtils, strformat, strutils, tables
from nativesockets import htonl
from net import IpAddress, Port, getPrimaryIPAddr, parseIpAddress, `$`
import ../../asyncutils
import ../../message
import ../../network_interface
type
Client = object
sock: AsyncSocket
ip: IpAddress
port: Port
probedPorts: seq[Port]
# Requests
Register = object
peerId: string
ip: IpAddress
port: Port
probedPorts: seq[Port]
GetPeerinfo = object
peerId: string
NotifyPeer = object
sender: string
recipient: string
data: string
proc isPrivateIp(ip: IpAddress): bool =
const ranges: array[5, tuple[first: IpAddress, last: IpAddress]] =
[(parseIpAddress("10.0.0.0"), parseIpAddress("10.255.255.255")),
(parseIpAddress("172.16.0.0"), parseIpAddress("172.31.255.255")),
(parseIpAddress("192.168.0.0"), parseIpAddress("192.168.255.255")),
(parseIpAddress("169.254.0.0"), parseIpAddress("169.254.255.255")),
(parseIpAddress("127.0.0.0"), parseIpAddress("127.255.255.255"))]
for r in ranges:
let ipScalar = htonl(cast[uint32](ip.address_v4))
if ipScalar > htonl(cast[uint32](r.first.address_v4)) and
ipScalar < htonl(cast[uint32](r.last.address_v4)):
return true
return false
proc isInNetwork(ip: IpAddress, iface: NetworkInterface): bool =
let ipScalar = htonl(cast[uint32](ip.address_v4))
let ifaceIpScalar = htonl(cast[uint32](iface.ipAddress.address_v4))
let netmaskScalar = htonl(cast[uint32](iface.netMask.address_v4))
(ipScalar and netmaskScalar) == (ifaceIpScalar and netmaskScalar)
proc probePublicIp(): Future[IpAddress] {.async.} =
# FIXME: need more reliable solution
let output = await asyncExecCmd("ping -R -c 1 -s 1 -n 193.0.14.129")
let ipLines = output.splitLines()
.filter(proc(l: string): bool = l.startsWith("\t") or l.startsWith("RR:"))
.map(proc(l: string): string = l.strip(true, false, {'R', ':', '\t', ' '}))
for line in ipLines:
let ipAddr = parseIpAddress(line)
if not isPrivateIp(ipAddr):
return ipAddr
block:
raise newException(OSError, "cannot probe public IP address")
proc removeClient(clients: TableRef[string, Client], peerId: string) =
if peerId.len > 0: clients.del(peerId)
proc sendEndpoint(client: AsyncSocket, requestId: string) {.async.} =
let (address, port) = client.getPeerAddr()
var ipAddr = parseIpAddress(address)
if ipAddr.isPrivateIp() and
ipAddr.isInNetwork(getNetworkInterface(getPrimaryIPAddr(ipAddr))):
ipAddr = await probePublicIp()
await client.send(&"ok|{requestId}|{ipAddr}|{port.int}\n")
client.close
proc processClient(client: AsyncSocket,
clients: TableRef[string, Client]) {.async.} =
var id = ""
var peerId = ""
while true:
var line = await client.recvLine(maxLength = 400)
line = line.strip(leading = false, trailing = true, chars = {'\r', '\n'})
if line.len == 0:
removeClient(clients, peerId)
break
try:
let args = line.parseArgs(3, 1)
id = args[1]
case args[0]:
of "register":
let req = parseMessage[Register](args[2])
echo "register: ", req
peerId = req.peerId
clients[peerId] = Client(sock: client, ip: req.ip, port: req.port,
probedPorts: req.probedPorts)
asyncCheck client.send(&"ok|{id}\n")
of "get-endpoint":
echo "get-endpoint"
asyncCheck client.sendEndpoint(id)
removeClient(clients, peerId)
break
of "get-peerinfo":
let req = parseMessage[GetPeerinfo](args[2])
echo "get-info: ", req
let peer = clients[req.peerId]
let probedPorts = peer.probedPorts.join(",")
asyncCheck client.send(&"ok|{id}|{peer.ip}|{peer.port}|{probedPorts}\n")
of "notify-peer":
let req = parseMessage[NotifyPeer](args[2])
echo "notify-peer: ", req
let recipient = clients[req.recipient]
asyncCheck recipient.sock.send(&"notify-peer|{req.sender}|{req.recipient}|{req.data}\n")
asyncCheck client.send(&"ok|{id}\n")
else:
echo "invalid request"
client.close()
removeClient(clients, peerId)
break
except KeyError:
asyncCheck client.send(&"error|{id}|peer not registered\n")
except ValueError:
echo "invalid message"
client.close
removeClient(clients, peerId)
break
proc serve(port: Port) {.async.} =
# FIXME: causes Error: unhandled exception: Too many open files [OSError] after a while
var clients = newTable[string, Client]()
var server = newAsyncSocket()
server.setSockOpt(OptReuseAddr, true)
server.bindAddr(port)
server.listen()
while true:
let client = await server.accept()
asyncCheck processClient(client, clients)
proc main() =
if paramCount() != 1:
echo(&"usage: {paramStr(0)} PORT")
quit(1)
try:
let portNumber = paramStr(1).parseUInt
if portNumber > uint16.high:
raise newException(ValueError, "port out of range")
let port = Port(portNumber)
asyncCheck serve(port)
runForever()
except ValueError as e:
echo e.msg
when isMainModule:
main()