From d3b37aa4a1ddf30412c4aa1bd48f6fb0ef2dc55d Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:53:13 +0200 Subject: [PATCH] Started work on websocket communication: Parsing/Serialization of WebSocket packets. --- conquest.nimble | 5 +- src/client/layout.ini | 54 ++++++------ src/client/main.nim | 27 +++++- src/client/views/console.nim | 36 ++++---- src/client/views/eventlog.nim | 6 +- src/client/views/listeners.nim | 9 +- src/client/views/sessions.nim | 4 +- src/client/websocket.nim | 135 ++++++++++++++++++++++++++++++ src/common/types.nim | 87 ++++++++++++++++++- src/server/core/server.nim | 38 ++++++++- src/server/nim.cfg | 1 + src/server/protocol/websocket.nim | 47 +++++++++++ 12 files changed, 388 insertions(+), 61 deletions(-) create mode 100644 src/client/websocket.nim create mode 100644 src/server/protocol/websocket.nim diff --git a/conquest.nimble b/conquest.nimble index f13fe73..27a37c6 100644 --- a/conquest.nimble +++ b/conquest.nimble @@ -2,7 +2,7 @@ version = "0.1.0" author = "Jakob Friedl" -description = "Command & control framework written in Nim" +description = "Conquest command & control/post-exploitation framework" license = "BSD-3-Clause" srcDir = "src" @@ -29,4 +29,5 @@ requires "winim >= 3.9.4" requires "ptr_math >= 0.3.0" requires "imguin >= 1.92.2.1" requires "zippy >= 0.10.16" -requires "mummy >= 0.4.6" \ No newline at end of file +requires "mummy >= 0.4.6" +requires "whisky >= 0.1.3" \ No newline at end of file diff --git a/src/client/layout.ini b/src/client/layout.ini index a2e9a8b..aa54bc3 100644 --- a/src/client/layout.ini +++ b/src/client/layout.ini @@ -1,41 +1,41 @@ [Window][Sessions [Table View]] Pos=10,43 -Size=1533,946 +Size=2016,548 Collapsed=0 DockId=0x00000003,0 [Window][Listeners] -Pos=10,43 -Size=1533,946 +Pos=10,593 +Size=2528,804 Collapsed=0 -DockId=0x00000003,1 +DockId=0x00000006,0 [Window][Eventlog] -Pos=1545,43 -Size=353,946 +Pos=2028,43 +Size=510,548 Collapsed=0 DockId=0x00000004,0 [Window][Dear ImGui Demo] -Pos=1545,43 -Size=353,946 +Pos=2028,43 +Size=510,548 Collapsed=0 DockId=0x00000004,1 [Window][Dockspace] Pos=0,0 -Size=1908,999 +Size=2548,1407 Collapsed=0 [Window][[FACEDEAD] bob@LAPTOP-02] -Pos=956,326 -Size=942,663 +Pos=10,593 +Size=2528,804 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000006,1 [Window][[C9D8E7F6] charlie@SERVER-03] -Pos=10,434 -Size=1888,555 +Pos=10,593 +Size=2528,804 Collapsed=0 DockId=0x00000006,1 @@ -45,16 +45,16 @@ Size=400,400 Collapsed=0 [Window][[G1H2I3J5] diana@WORKSTATION-04] -Pos=10,434 -Size=1888,555 +Pos=10,593 +Size=2528,804 Collapsed=0 -DockId=0x00000006,0 +DockId=0x00000006,1 [Window][[DEADBEEF] alice@DESKTOP-01] -Pos=10,402 -Size=1888,587 +Pos=10,716 +Size=2848,969 Collapsed=0 -DockId=0x00000005,1 +DockId=0x00000006,2 [Window][Example: Console] Pos=10,572 @@ -110,8 +110,8 @@ Size=76,76 Collapsed=0 [Window][Start Listener] -Pos=704,387 -Size=500,225 +Pos=955,591 +Size=637,225 Collapsed=0 [Table][0x32886A44,8] @@ -136,9 +136,9 @@ Column 3 Weight=0.9746 [Docking][Data] DockNode ID=0x00000009 Pos=100,200 Size=754,103 Selected=0x64D005CF -DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y - DockNode ID=0x00000005 Parent=0x85940918 SizeRef=1888,389 Split=X - DockNode ID=0x00000003 Parent=0x00000005 SizeRef=1533,159 CentralNode=1 Selected=0x61E02D75 - DockNode ID=0x00000004 Parent=0x00000005 SizeRef=353,159 Selected=0x5E5F7166 - DockNode ID=0x00000006 Parent=0x85940918 SizeRef=1888,555 Selected=0x65D642C0 +DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=2528,1354 Split=Y + DockNode ID=0x00000005 Parent=0x85940918 SizeRef=1888,548 Split=X + DockNode ID=0x00000003 Parent=0x00000005 SizeRef=1376,159 CentralNode=1 Selected=0x61E02D75 + DockNode ID=0x00000004 Parent=0x00000005 SizeRef=510,159 Selected=0x5E5F7166 + DockNode ID=0x00000006 Parent=0x85940918 SizeRef=1888,804 Selected=0x6BE22050 diff --git a/src/client/main.nim b/src/client/main.nim index 546a421..0390eb8 100644 --- a/src/client/main.nim +++ b/src/client/main.nim @@ -1,6 +1,9 @@ -import tables +import whisky +import tables, strutils import ./utils/appImGui import ./views/[dockspace, sessions, listeners, eventlog, console] +import ../common/[types, utils] +import ./websocket proc main() = var app = createApp(1024, 800, imnodes = true, title = "Conquest", docking = true) @@ -35,6 +38,10 @@ proc main() = let io = igGetIO() + # Initiate WebSocket connection + let ws = newWebSocket("ws://localhost:12345") + defer: ws.close() + # main loop while not app.handle.windowShouldClose: pollEvents() @@ -44,10 +51,24 @@ proc main() = continue newFrame() + #[ + WebSocket communication with the team server + ]# + # Continuously send heartbeat messages + ws.sendHeartbeat() + + # Receive and parse websocket response message + let message = ws.receiveMessage().get() + case message.getMessageType() + of CLIENT_EVENT_LOG: + message.receiveEventlogItem(addr eventlog) + else: discard + + # Draw/update UI components/views dockspace.draw(addr showConquest, views, addr dockTop, addr dockBottom, addr dockTopLeft, addr dockTopRight) if showSessionsTable: sessionsTable.draw(addr showSessionsTable) - if showListeners: listenersTable.draw(addr showListeners) + if showListeners: listenersTable.draw(addr showListeners, ws) if showEventlog: eventlog.draw(addr showEventlog) # Show console windows @@ -56,7 +77,7 @@ proc main() = if console.showConsole: # Ensure that new console windows are docked to the bottom panel by default igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32) - console.draw() + console.draw(ws) newConsoleTable[agentId] = console # Update the consoles table with only those sessions that have not been closed yet diff --git a/src/client/views/console.nim b/src/client/views/console.nim index 4ad0864..d30691c 100644 --- a/src/client/views/console.nim +++ b/src/client/views/console.nim @@ -1,3 +1,4 @@ +import whisky import strformat, strutils, times import imguin/[cimgui, glfw_opengl, simple] import ../utils/[appImGui, colors] @@ -113,11 +114,11 @@ proc callback(data: ptr ImGuiInputTextCallbackData): cint {.cdecl.} = #[ API to add new console item ]# -proc addItem*(component: ConsoleComponent, itemType: LogType, data: string) = +proc addItem*(component: ConsoleComponent, itemType: LogType, data: string, timestamp: int64 = now().toTime().toUnix()) = for line in data.split("\n"): component.console.items.add(ConsoleItem( - timestamp: if itemType == LOG_OUTPUT: 0 else: now().toTime().toUnix(), + timestamp: if itemType == LOG_OUTPUT: 0 else: timestamp, itemType: itemType, text: line )) @@ -148,7 +149,7 @@ proc print(item: ConsoleItem) = igSameLine(0.0f, 0.0f) igTextUnformatted(item.text.cstring, nil) -proc draw*(component: ConsoleComponent) = +proc draw*(component: ConsoleComponent, ws: WebSocket) = igBegin(fmt"[{component.agent.agentId}] {component.agent.username}@{component.agent.hostname}", addr component.showConsole, 0) defer: igEnd() @@ -251,22 +252,25 @@ proc draw*(component: ConsoleComponent) = let inputFlags = ImGuiInputTextFlags_EnterReturnsTrue.int32 or ImGuiInputTextFlags_EscapeClearsAll.int32 or ImGuiInputTextFlags_CallbackHistory.int32 or ImGuiInputTextFlags_CallbackCompletion.int32 if igInputText("##Input", addr component.inputBuffer[0], MAX_INPUT_LENGTH, inputFlags, callback, cast[pointer](component)): - let command = $(addr component.inputBuffer[0]).cstring - component.addItem(LOG_COMMAND, command) + let command = ($(addr component.inputBuffer[0])).strip() + if not command.isEmptyOrWhitespace(): + + component.addItem(LOG_COMMAND, command) - # For testing - component.addItem(LOG_ERROR, "error message") - component.addItem(LOG_SUCCESS, "success message") - component.addItem(LOG_INFO, "info message") - component.addItem(LOG_WARNING, "warning message") - component.addItem(LOG_OUTPUT, "error message\nLong output\n\tindented output\nasdasd") + # For testing + # component.addItem(LOG_ERROR, "error message") + # component.addItem(LOG_SUCCESS, "success message") + # component.addItem(LOG_INFO, "info message") + # component.addItem(LOG_WARNING, "warning message") + # component.addItem(LOG_OUTPUT, "error message\nLong output\n\tindented output\nasdasd") - # TODO: Handle command execution - # console.handleCommand(command) + # TODO: Handle command execution + # console.handleCommand(command) + ws.send("CMD:" & component.agent.agentId & ":" & command) - # Add command to console history - component.history.add(command) - component.historyPosition = -1 + # Add command to console history + component.history.add(command) + component.historyPosition = -1 zeroMem(addr component.inputBuffer[0], MAX_INPUT_LENGTH) focusInput = true diff --git a/src/client/views/eventlog.nim b/src/client/views/eventlog.nim index 3d5cffd..03226a9 100644 --- a/src/client/views/eventlog.nim +++ b/src/client/views/eventlog.nim @@ -4,7 +4,7 @@ import ../utils/[appImGui, colors] import ../../common/types type - EventlogComponent = ref object of RootObj + EventlogComponent* = ref object of RootObj title: string log*: ConsoleItems textSelect: ptr TextSelect @@ -41,11 +41,11 @@ proc Eventlog*(title: string): EventlogComponent = #[ API to add new log entry ]# -proc addItem*(component: EventlogComponent, itemType: LogType, data: string) = +proc addItem*(component: EventlogComponent, itemType: LogType, data: string, timestamp: int64 = now().toTime().toUnix()) = for line in data.split("\n"): component.log.items.add(ConsoleItem( - timestamp: if itemType == LOG_OUTPUT: 0 else: now().toTime().toUnix(), + timestamp: if itemType == LOG_OUTPUT: 0 else: timestamp, itemType: itemType, text: line )) diff --git a/src/client/views/listeners.nim b/src/client/views/listeners.nim index 82a636c..f10cce9 100644 --- a/src/client/views/listeners.nim +++ b/src/client/views/listeners.nim @@ -3,11 +3,12 @@ import imguin/[cimgui, glfw_opengl, simple] import ../utils/appImGui import ../../common/[types, utils] import ./modals/startListener +import whisky type - ListenersTableComponent = ref object of RootObj + ListenersTableComponent* = ref object of RootObj title: string - listeners: seq[Listener] + listeners*: seq[Listener] selection: ptr ImGuiSelectionBasicStorage startListenerModal: ListenerModalComponent @@ -33,7 +34,7 @@ proc ListenersTable*(title: string): ListenersTableComponent = result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage() result.startListenerModal = ListenerModal() -proc draw*(component: ListenersTableComponent, showComponent: ptr bool) = +proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebSocket) = igBegin(component.title, showComponent, 0) defer: igEnd() @@ -46,7 +47,7 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool) = let listener = component.startListenerModal.draw() if listener != nil: # TODO: Start listener - + ws.send("Starting listener: " & listener.listenerId) component.listeners.add(listener) #[ diff --git a/src/client/views/sessions.nim b/src/client/views/sessions.nim index c57e686..7afb070 100644 --- a/src/client/views/sessions.nim +++ b/src/client/views/sessions.nim @@ -6,9 +6,9 @@ import ../utils/appImGui import ../../common/[types, utils] type - SessionsTableComponent = ref object of RootObj + SessionsTableComponent* = ref object of RootObj title: string - agents: seq[Agent] + agents*: seq[Agent] selection: ptr ImGuiSelectionBasicStorage consoles: ptr Table[string, ConsoleComponent] diff --git a/src/client/websocket.nim b/src/client/websocket.nim new file mode 100644 index 0000000..b364dd7 --- /dev/null +++ b/src/client/websocket.nim @@ -0,0 +1,135 @@ +import times, tables +import ../common/[types, utils, serialize] +import views/[sessions, listeners, console, eventlog] +import whisky + +#[ + [ Sending Functions ] + Client -> Server + - Heartbeat + - ListenerStart + - ListenerStop + - AgentBuild + - AgentCommand +]# +proc sendHeartbeat*(ws: WebSocket) = + var packer = Packer.init() + + packer.add(cast[uint8](CLIENT_HEARTBEAT)) + let data = packer.pack() + + ws.send(Bytes.toString(data), BinaryMessage) + +proc sendStartListener*(ws: WebSocket, listener: Listener) = + var packer = Packer.init() + + packer.add(cast[uint8](CLIENT_LISTENER_START)) + packer.add(string.toUUid(listener.listenerId)) + packer.addDataWithLengthPrefix(string.toBytes(listener.address)) + packer.add(cast[uint16](listener.port)) + packer.add(cast[uint8](listener.protocol)) + + let data = packer.pack() + + ws.send(Bytes.toString(data), BinaryMessage) + +proc sendStopListener*(ws: WebSocket, listenerId: string) = + var packer = Packer.init() + + packer.add(cast[uint8](CLIENT_LISTENER_STOP)) + packer.add(string.toUuid(listenerId)) + let data = packer.pack() + + ws.send(Bytes.toString(data), BinaryMessage) + +proc sendAgentCommand*(ws: WebSocket, agentId: string, command: string) = + var packer = Packer.init() + + packer.add(cast[uint8](CLIENT_AGENT_COMMAND)) + packer.add(string.toUuid(agentId)) + packer.addDataWithLengthPrefix(string.toBytes(command)) + let data = packer.pack() + + ws.send(Bytes.toString(data), BinaryMessage) + +proc sendAgentBuild*(ws: WebSocket, listenerId: string, sleepDelay: int, sleepMask: SleepObfuscationTechnique, spoofStack: bool, modules: uint32) = + var packer = Packer.init() + + packer.add(cast[uint8](CLIENT_AGENT_BUILD)) + packer.add(string.toUuid(listenerId)) + packer.add(cast[uint32](sleepDelay)) + packer.add(cast[uint8](sleepMask)) + packer.add(cast[uint8](spoofStack)) + packer.add(modules) + let data = packer.pack() + + ws.send(Bytes.toString(data), BinaryMessage) + +#[ + [ Retrieval Functions ] + Server -> Client +]# +proc getMessageType*(message: Message): WsMessageAction = + var unpacker = Unpacker.init(message.data) + return cast[WsMessageAction](unpacker.getUint8()) + +proc receiveAgentPayload*(message: Message): seq[byte] = + var unpacker = Unpacker.init(message.data) + + discard unpacker.getUint8() + return string.toBytes(unpacker.getDataWithLengthPrefix()) + +proc receiveAgentConnection*(message: Message, sessions: ptr SessionsTableComponent) = + var unpacker = Unpacker.init(message.data) + + discard unpacker.getUint8() + let agent = Agent( + agentId: Uuid.toString(unpacker.getUint32()), + listenerId: Uuid.toString(unpacker.getUint32()), + username: unpacker.getDataWithLengthPrefix(), + hostname: unpacker.getDataWithLengthPrefix(), + domain: unpacker.getDataWithLengthPrefix(), + ip: unpacker.getDataWithLengthPrefix(), + os: unpacker.getDataWithLengthPrefix(), + process: unpacker.getDataWithLengthPrefix(), + pid: int(unpacker.getUint32()), + elevated: unpacker.getUint8() != 0, + sleep: int(unpacker.getUint32()), + tasks: @[], + firstCheckin: cast[int64](unpacker.getUint32()).fromUnix().utc(), + latestCheckin: now(), + ) + + sessions.agents.add(agent) + +proc receiveAgentCheckin*(message: Message, sessions: ptr SessionsTableComponent)= + var unpacker = Unpacker.init(message.data) + + discard unpacker.getUint8() + let agentId = Uuid.toString(unpacker.getUint32()) + let timestamp = cast[int64](unpacker.getUint32()) + + # TODO: Update checkin + +proc receiveConsoleItem*(message: Message, consoles: ptr Table[string, ConsoleComponent]) = + var unpacker = Unpacker.init(message.data) + + discard unpacker.getUint8() + let + agentId = Uuid.toString(unpacker.getUint32()) + logType = cast[LogType](unpacker.getUint8()) + timestamp = cast[int64](unpacker.getUint32()) + message = unpacker.getDataWithLengthPrefix() + + consoles[][agentId].addItem(logType, message, timestamp) + +proc receiveEventlogItem*(message: Message, eventlog: ptr EventlogComponent) = + var unpacker = Unpacker.init(message.data) + + discard unpacker.getUint8() + let + logType = cast[LogType](unpacker.getUint8()) + timestamp = cast[int64](unpacker.getUint32()) + message = unpacker.getDataWithLengthPrefix() + + eventlog[].addItem(logType, message, timestamp) \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index 7753b01..38d2ef9 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -81,7 +81,7 @@ type CONFIG_PUBLIC_KEY = 4'u8 CONFIG_PROFILE = 5'u8 - LogType* = enum + LogType* {.size: sizeof(uint8).} = enum LOG_INFO = " [INFO] " LOG_ERROR = " [FAIL] " LOG_SUCCESS = " [DONE] " @@ -193,7 +193,7 @@ type # Listener structure type - Protocol* = enum + Protocol* {.size: sizeof(uint8).} = enum HTTP = "http" Listener* = ref object of RootObj @@ -262,4 +262,85 @@ type text*: string ConsoleItems* = ref object - items*: seq[ConsoleItem] \ No newline at end of file + items*: seq[ConsoleItem] + +#[ + Client <-> Server WebSocket communication +]# +type + WsMessageAction* = enum + # Sent by client + CLIENT_HEARTBEAT = 0'u8 # Basic checkin + CLIENT_AGENT_COMMAND = 1'u8 # Instruct TS to send queue a command for a specific agent + CLIENT_LISTENER_START = 2'u8 # Start a listener on the TS + CLIENT_LISTENER_STOP = 3'u8 # Stop a listener + CLIENT_AGENT_BUILD = 4'u8 # Generate an agent binary for a specific listener + + # Sent by team server + CLIENT_AGENT_BINARY = 100'u8 # Return the agent binary to write to the operator's client machine + CLIENT_AGENT_CONNECTION = 101'u8 # Notify new agent connection + CLIENT_AGENT_CHECKIN = 102'u8 # Update agent checkin + CLIENT_CONSOLE_LOG = 103'u8 # Add entry to a agent's console + CLIENT_EVENT_LOG = 104'u8 # Add entry to the eventlog + + CLIENT_CONNECTION = 200'u8 # Return team server profile + + # Client -> Server + WsHeartbeat* = object + msgType* = CLIENT_HEARTBEAT + + WsCommand* = object + msgType* = CLIENT_AGENT_COMMAND + agentId*: uint32 + command*: seq[byte] # Command input field in the console window, prefixed with length + + WsListenerStart* = object + msgType* = CLIENT_LISTENER_START + listener*: Listener + + WsListenerStop* = object + msgType* = CLIENT_LISTENER_STOP + listenerId*: uint32 + + WsAgentBuild* = object + msgType* = CLIENT_AGENT_BUILD + listenerId*: uint32 + sleepDelay*: uint32 + sleepMask*: SleepObfuscationTechnique + spoofStack*: uint8 + modules*: uint64 + + # Server -> Client + WsAgentBinary* = object + msgType* = CLIENT_AGENT_BINARY + agentPayload*: seq[byte] # Agent binary in byte-form, opens file browser to select location on the client + + WsAgentConnection* = object + msgType* = CLIENT_AGENT_CONNECTION + agent*: Agent + + WsAgentCheckin* = object + msgType* = CLIENT_AGENT_CHECKIN + agentId*: uint32 + timestamp*: uint32 + + WsConsoleLog* = object + msgType* = CLIENT_CONSOLE_LOG + agentId*: uint32 + logType*: LogType + timestamp*: uint32 + data*: seq[byte] + + WsEventLog* = object + msgType* = CLIENT_EVENT_LOG + logType*: LogType + timestamp*: uint32 + data*: seq[byte] + + WsClientConnection* = object + msgType* = CLIENT_CONNECTION + version: uint8 + profile*: seq[byte] + agents*: seq[Agent] + listeners*: seq[Listener] + \ No newline at end of file diff --git a/src/server/core/server.nim b/src/server/core/server.nim index e8770f2..659cb5e 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -1,4 +1,4 @@ -import prompt, terminal, argparse, parsetoml +import prompt, terminal, argparse, parsetoml, times import strutils, strformat, system, tables import ./[agent, listener, builder] @@ -6,6 +6,8 @@ import ../globals import ../db/database import ../core/logger import ../../common/[types, crypto, profile] +import ../protocol/websocket +import mummy, mummy/routers #[ Argument parsing @@ -137,6 +139,33 @@ proc init*(T: type Conquest, profile: Profile): Conquest = cq.dbPath = CONQUEST_ROOT & "/" & profile.getString("database-file") return cq +#[ + WebSocket +]# +proc upgradeHandler(request: Request) = + let ws = request.upgradeToWebSocket() + + # Send client connection message + ws.sendEventlogItem(LOG_SUCCESS_SHORT, now().toTime().toUnix(), "CQ-V1") + + +proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) = + case event: + of OpenEvent: + discard + of MessageEvent: + ws.sendHeartbeat() + of ErrorEvent: + discard + of CloseEvent: + discard + +proc serve(server: Server) {.thread.} = + try: + server.serve(Port(12345)) + except Exception: + discard + proc startServer*(profilePath: string) = # Ensure that the conquest root directory was passed as a compile-time define @@ -167,6 +196,13 @@ proc startServer*(profilePath: string) = cq.restartListeners() cq.addMultiple(cq.dbGetAllAgents()) + var router: Router + router.get("/*", upgradeHandler) + let server = newServer(router, websocketHandler) + + var thread: Thread[Server] + createThread(thread, serve, server) + # Main loop while true: cq.prompt.setIndicator("[conquest]> ") diff --git a/src/server/nim.cfg b/src/server/nim.cfg index 516aad0..4c5a4fa 100644 --- a/src/server/nim.cfg +++ b/src/server/nim.cfg @@ -1,5 +1,6 @@ # Compiler flags -d:server --threads:on +--mm:orc -d:httpxServerName="" -o:"bin/server" \ No newline at end of file diff --git a/src/server/protocol/websocket.nim b/src/server/protocol/websocket.nim new file mode 100644 index 0000000..573ff3c --- /dev/null +++ b/src/server/protocol/websocket.nim @@ -0,0 +1,47 @@ +import times, tables +import ../../common/[types, utils, serialize] +import mummy + +#[ + [ Sending functions ] + Server -> Client +]# +proc sendHeartbeat*(ws: WebSocket) = + var packer = Packer.init() + + packer.add(cast[uint8](CLIENT_HEARTBEAT)) + let data = packer.pack() + + ws.send(Bytes.toString(data), BinaryMessage) + +proc sendEventlogItem*(ws: WebSocket, logType: LogType, timestamp: int64, message: string) = + var packer = Packer.init() + + packer.add(cast[uint8](CLIENT_EVENT_LOG)) + packer.add(cast[uint8](logType)) + packer.add(cast[uint32](timestamp)) + packer.addDataWithLengthPrefix(string.toBytes(message)) + let data = packer.pack() + + ws.send(Bytes.toString(data), BinaryMessage) + +#[ + [ Retrieval functions ] + Client -> Server +]# +proc getMessageType*(message: Message): WsMessageAction = + var unpacker = Unpacker.init(message.data) + return cast[WsMessageAction](unpacker.getUint8()) + +proc receiveStartListener*(message: Message): Listener = + var unpacker = Unpacker.init(message.data) + + discard unpacker.getUint8() + + return Listener( + server: nil, + listenerId: Uuid.toString(unpacker.getUint32()), + address: unpacker.getDataWithLengthPrefix(), + port: int(unpacker.getUint16()), + protocol: cast[Protocol](unpacker.getUint8()) + ) \ No newline at end of file