Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
zedeus committed Jul 5, 2019
0 parents commit f90d46d
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 0 deletions.
12 changes: 12 additions & 0 deletions nimchat.nimble
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Package

version = "0.1.0"
author = "zedeus"
description = "A chat application"
license = "GPL-3.0"
srcDir = "src"


# Dependencies

requires "nim >= 0.19.0"
65 changes: 65 additions & 0 deletions src/client.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import asyncdispatch, asyncnet, os, threadpool, strutils, strformat
import ./protocol

type
Client = ref object
socket: AsyncSocket
server: string
port: int
username: string

proc displayMessage(message: Message) =
echo &"{message.username}: {message.text}"

proc connect(client: Client) {.async.} =
echo "Connecting to ", client.server
await connect(client.socket, client.server, Port(client.port))
echo "Connected!"

proc login(client: Client) {.async.} =
echo "Logging in to ", client.server
let message = createMessage(client.username, "login", ConnectionMessage)
await client.socket.send(message)

let response = await client.socket.recvLine()
let parsed = parseMessage(response)

if parsed.mtype == ServiceMessage:
displayMessage(parsed)
elif parsed.mtype == ErrorMessage:
quit("ERROR: " & parsed.text)

proc processMessages(client: Client) {.async.} =
while true:
let line = await client.socket.recvLine()
let parsed = parseMessage(line)
displayMessage(parsed)

proc processInput(client: Client) {.async.} =
var messageFlowVar = spawn stdin.readLine()
while true:
if messageFlowVar.isReady():
let message = createMessage(client.username, ^messageFlowVar, TextMessage)
asyncCheck client.socket.send(message)
messageFlowVar = spawn stdin.readLine()
poll()

proc newClient(): Client =
if paramCount() < 3:
quit("Please specify server address, port and username\nExample: ./client localhost 7687 user")

new(result)
result.server = paramStr(1)
result.port = parseInt(paramStr(2))
result.username = paramStr(3)
result.socket = newAsyncSocket()

proc main() =
let client = newClient()
waitFor client.connect()
waitFor client.login()

asyncCheck client.processMessages()
waitFor client.processInput()

main()
1 change: 1 addition & 0 deletions src/client.nims
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--threads:on
47 changes: 47 additions & 0 deletions src/protocol.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import json, hashes, strutils

type
MessageType* = enum
TextMessage, ServiceMessage, ConnectionMessage,
WarningMessage, ErrorMessage

Message* = object
username*: string
mtype*: MessageType
text*: string
hash*: int

proc getHash*(username, text: string): int =
return hash(username & text)

proc getHash*(message: Message): int =
return hash(message.username & message.text)

proc verifyMessage*(message: Message): bool =
return getHash(message) == message.hash

proc parseMessage*(data: string): Message =
let dataJson = parseJson(data)
result.username = dataJson["username"].getStr()
result.text = dataJson["text"].getStr()
result.mtype = parseEnum[MessageType](dataJson["mtype"].getStr())
result.hash = dataJson["hash"].getInt()

proc createMessage*(username, text: string; mtype: MessageType): string =
result = $(%{
"username": %username,
"text": %text,
"mtype": %mtype,
"hash": %getHash(username, text)
}) & "\c\l"


when isMainModule:
let hash = getHash("John" & "Hi!")
let data = """{"username": "John", "text": "Hi!", "hash": 6635146331310275049, "mtype": 0}"""

let parsed = parseMessage(data)
doAssert parsed.username == "John"
doAssert parsed.text == "Hi!"
doAssert parsed.hash == getHash("John" & "Hi!")
echo "All tests passed"
118 changes: 118 additions & 0 deletions src/server.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import asyncdispatch, asyncnet, os, strformat, strutils
import ./protocol

type
Client = ref object
socket: AsyncSocket
netAddr: string
connected: bool
username: string
id: int

Server = ref object
socket: AsyncSocket
clients: seq[Client]

proc newServer(): Server =
Server(socket: newAsyncSocket(), clients: @[])

proc `$`(client: Client): string =
&"{client.id} ({client.netAddr})"

proc sendMessage(client: Client; text: string; mtype: MessageType) {.async.} =
let message = createMessage("server", text, mtype)
await client.socket.send(message)

proc sendError(client: Client; text: string) {.async.} =
await sendMessage(client, text, ErrorMessage)

proc sendWarning(client: Client; text: string) {.async.} =
await sendMessage(client, text, WarningMessage)

proc sendService(client: Client; text: string) {.async.} =
if not client.connected: return
await sendMessage(client, text, ServiceMessage)

proc relayMessage(server: Server; message: string; skip=(-1)) {.async.} =
for c in server.clients:
if c.connected and (skip == -1 or c.id != skip):
await c.socket.send(message)

proc broadcast(server: Server; text: string; skip=(-1); mtype=ServiceMessage) {.async.} =
let message = createMessage("server", text, mtype)
await relayMessage(server, message, skip)

proc disconnect(client: Client) =
echo client, " disconnected!"
client.connected = false
client.username = ""
client.socket.close()

proc handleLogin(server: Server; client: Client; message: Message) {.async.} =
if client.connected:
return

for c in server.clients:
if c.username == message.username:
await sendError(client, "Username already taken.")
return

client.username = message.username
client.connected = true

await sendService(client, "Login succesful.")
await server.broadcast(&"user \"{client.username}\" has joined")

proc processMessage(server: Server; client: Client; message: string) {.async.} =
let parsed = parseMessage(message)
echo client, " sent: ", message

case parsed.mtype
of TextMessage:
if verifyMessage(parsed):
await relayMessage(server, message & "\c\l", skip=client.id)
else:
await sendWarning(client, "Message hash verification failed.")
of ConnectionMessage:
if parsed.text == "login":
await handleLogin(server, client, parsed)
else:
discard

proc processClient(server: Server, client: Client) {.async.} =
while true:
let line = await client.socket.recvLine()
if line.len == 0:
if client.username != "":
await server.broadcast(&"user \"{client.username}\" disconnected", skip=client.id)

disconnect(client)
return

await processMessage(server, client, line)

proc loop(server: Server) {.async.} =
while true:
let (netAddr, clientSocket) = await server.socket.acceptAddr()
let client = Client(
socket: clientSocket,
netAddr: netAddr,
connected: false,
id: server.clients.len
)
server.clients.add(client)
asyncCheck server.processClient(client)
echo "Accepted connection from ", netAddr, ", client id: ", client.id

proc main() =
let server = newServer()
var port = 7687

if paramCount() == 1:
port = paramStr(1).parseInt

server.socket.bindAddr(Port(port))
server.socket.listen()
waitFor server.loop()

main()

0 comments on commit f90d46d

Please sign in to comment.