Replaced prologue implementation with mummy for listener management, since it seems more suitable for future use (websockets, etc.).

This commit is contained in:
Jakob Friedl
2025-09-19 18:31:45 +02:00
parent 6b41efe1ed
commit 42cc58b30b
8 changed files with 122 additions and 96 deletions

View File

@@ -25,8 +25,8 @@ requires "argparse >= 4.0.2"
requires "parsetoml >= 0.7.2" requires "parsetoml >= 0.7.2"
requires "nimcrypto >= 0.6.4" requires "nimcrypto >= 0.6.4"
requires "tiny_sqlite >= 0.2.0" requires "tiny_sqlite >= 0.2.0"
requires "prologue >= 0.6.6"
requires "winim >= 3.9.4" requires "winim >= 3.9.4"
requires "ptr_math >= 0.3.0" requires "ptr_math >= 0.3.0"
requires "imguin >= 1.92.2.1" requires "imguin >= 1.92.2.1"
requires "zippy >= 0.10.16" requires "zippy >= 0.10.16"
requires "mummy >= 0.4.6"

View File

@@ -3,6 +3,6 @@
-d:release -d:release
--opt:size --opt:size
--passL:"-s" # Strip symbols, such as sensitive function names --passL:"-s" # Strip symbols, such as sensitive function names
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" -d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
-d:MODULES=1 -d:MODULES=1
-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe" -o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe"

View File

@@ -1,24 +1,24 @@
[Window][Sessions [Table View]] [Window][Sessions [Table View]]
Pos=10,43 Pos=10,43
Size=1533,389 Size=1533,946
Collapsed=0 Collapsed=0
DockId=0x00000003,0 DockId=0x00000003,0
[Window][Listeners] [Window][Listeners]
Pos=10,43 Pos=10,43
Size=1533,389 Size=1533,946
Collapsed=0 Collapsed=0
DockId=0x00000003,1 DockId=0x00000003,1
[Window][Eventlog] [Window][Eventlog]
Pos=1545,43 Pos=1545,43
Size=353,389 Size=353,946
Collapsed=0 Collapsed=0
DockId=0x00000004,0 DockId=0x00000004,0
[Window][Dear ImGui Demo] [Window][Dear ImGui Demo]
Pos=1545,43 Pos=1545,43
Size=353,389 Size=353,946
Collapsed=0 Collapsed=0
DockId=0x00000004,1 DockId=0x00000004,1
@@ -139,6 +139,6 @@ DockNode ID=0x00000009 Pos=100,200 Size=754,103 Selected=0x64D005CF
DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y 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=0x00000005 Parent=0x85940918 SizeRef=1888,389 Split=X
DockNode ID=0x00000003 Parent=0x00000005 SizeRef=1533,159 CentralNode=1 Selected=0x61E02D75 DockNode ID=0x00000003 Parent=0x00000005 SizeRef=1533,159 CentralNode=1 Selected=0x61E02D75
DockNode ID=0x00000004 Parent=0x00000005 SizeRef=353,159 Selected=0x0FA43D88 DockNode ID=0x00000004 Parent=0x00000005 SizeRef=353,159 Selected=0x5E5F7166
DockNode ID=0x00000006 Parent=0x85940918 SizeRef=1888,555 Selected=0x65D642C0 DockNode ID=0x00000006 Parent=0x85940918 SizeRef=1888,555 Selected=0x65D642C0

View File

@@ -2,6 +2,7 @@ import prompt
import tables import tables
import times import times
import parsetoml import parsetoml
import mummy
# Custom Binary Task structure # Custom Binary Task structure
const const
@@ -196,6 +197,7 @@ type
HTTP = "http" HTTP = "http"
Listener* = ref object of RootObj Listener* = ref object of RootObj
server*: Server
listenerId*: string listenerId*: string
address*: string address*: string
port*: int port*: int
@@ -212,7 +214,7 @@ type
Conquest* = ref object Conquest* = ref object
prompt*: Prompt prompt*: Prompt
dbPath*: string dbPath*: string
listeners*: Table[string, Listener] listeners*: Table[string, tuple[listener: Listener, thread: Thread[Listener]]]
agents*: Table[string, Agent] agents*: Table[string, Agent]
interactAgent*: Agent interactAgent*: Agent
keyPair*: KeyPair keyPair*: KeyPair

View File

@@ -1,4 +1,4 @@
import prologue, terminal, strformat, parsetoml, tables import mummy, terminal, strformat, parsetoml, tables
import strutils, base64 import strutils, base64
import ./handlers import ./handlers
@@ -6,15 +6,32 @@ import ../globals
import ../core/logger import ../core/logger
import ../../common/[types, utils, serialize, profile] import ../../common/[types, utils, serialize, profile]
proc error404*(ctx: Context) {.async.} = # Not Found
resp "", Http404 proc error404*(request: Request) =
request.respond(404, body = "")
# Method not allowed
proc error405*(request: Request) =
request.respond(404, body = "")
# Utils
proc hasKey(headers: seq[(string, string)], headerName: string): bool =
for (name, value) in headers:
if name.toLower() == headerName.toLower():
return true
return false
proc get(headers: seq[(string, string)], headerName: string): string =
for (name, value) in headers:
if name.toLower() == headerName.toLower():
return value
return ""
#[ #[
GET GET
Called from agent to check for new tasks Called from agent to check for new tasks
]# ]#
proc httpGet*(ctx: Context) {.async.} = proc httpGet*(request: Request) =
{.cast(gcsafe).}: {.cast(gcsafe).}:
# Check heartbeat metadata placement # Check heartbeat metadata placement
@@ -24,17 +41,16 @@ proc httpGet*(ctx: Context) {.async.} =
case cq.profile.getString("http-get.agent.heartbeat.placement.type"): case cq.profile.getString("http-get.agent.heartbeat.placement.type"):
of "header": of "header":
let heartbeatHeader = cq.profile.getString("http-get.agent.heartbeat.placement.name") let heartbeatHeader = cq.profile.getString("http-get.agent.heartbeat.placement.name")
if not ctx.request.hasHeader(heartbeatHeader): if not request.headers.hasKey(heartbeatHeader):
resp "", Http404 request.respond(404, body = "")
return return
heartbeatString = request.headers.get(heartbeatHeader)
heartbeatString = ctx.request.getHeader(heartbeatHeader)[0]
of "parameter": of "parameter":
let param = cq.profile.getString("http-get.agent.heartbeat.placement.name") let param = cq.profile.getString("http-get.agent.heartbeat.placement.name")
heartbeatString = ctx.getQueryParams(param) heartbeatString = request.queryParams.get(param)
if heartbeatString.len <= 0: if heartbeatString.len <= 0:
resp "", Http404 request.respond(404, body = "")
return return
of "uri": of "uri":
@@ -60,7 +76,7 @@ proc httpGet*(ctx: Context) {.async.} =
let tasks: seq[seq[byte]] = getTasks(heartbeat) let tasks: seq[seq[byte]] = getTasks(heartbeat)
if tasks.len <= 0: if tasks.len <= 0:
resp "", Http200 request.respond(200, body = "")
return return
# Create response, containing number of tasks, as well as length and content of each task # Create response, containing number of tasks, as well as length and content of each task
@@ -73,7 +89,6 @@ proc httpGet*(ctx: Context) {.async.} =
# Apply data transformation to the response # Apply data transformation to the response
var response: string var response: string
case cq.profile.getString("http-get.server.output.encoding.type", default = "none"): case cq.profile.getString("http-get.server.output.encoding.type", default = "none"):
of "none": of "none":
response = Bytes.toString(responseBytes) response = Bytes.toString(responseBytes)
@@ -85,52 +100,52 @@ proc httpGet*(ctx: Context) {.async.} =
let suffix = cq.profile.getString("http-get.server.output.suffix") let suffix = cq.profile.getString("http-get.server.output.suffix")
# Add headers, as defined in the team server profile # Add headers, as defined in the team server profile
var headers: HttpHeaders
for header, value in cq.profile.getTable("http-get.server.headers"): for header, value in cq.profile.getTable("http-get.server.headers"):
ctx.response.setHeader(header, value.getStringValue()) headers.add((header, value.getStringValue()))
await ctx.respond(Http200, prefix & response & suffix, ctx.response.headers) request.respond(200, headers = headers, body = prefix & response & suffix)
ctx.handled = true # Ensure that HTTP response is sent only once
# Notify operator that agent collected tasks # Notify operator that agent collected tasks
cq.info(fmt"{$response.len} bytes sent.") cq.info(fmt"{$response.len} bytes sent.")
except CatchableError: except CatchableError:
resp "", Http404 request.respond(404, body = "")
#[ #[
POST POST
Called from agent to register itself or post results of a task Called from agent to register itself or post results of a task
]# ]#
proc httpPost*(ctx: Context) {.async.} = proc httpPost*(request: Request) =
{.cast(gcsafe).}: {.cast(gcsafe).}:
# Check headers # Check headers
# If POST data is not binary data, return 404 error code # If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/octet-stream": if request.headers.get("Content-Type") != "application/octet-stream":
resp "", Http404 request.respond(404, body = "")
return return
try: try:
# Differentiate between registration and task result packet # Differentiate between registration and task result packet
var unpacker = Unpacker.init(ctx.request.body) var unpacker = Unpacker.init(request.body)
let header = unpacker.deserializeHeader() let header = unpacker.deserializeHeader()
# Add response headers, as defined in team server profile # Add response headers, as defined in team server profile
var headers: HttpHeaders
for header, value in cq.profile.getTable("http-post.server.headers"): for header, value in cq.profile.getTable("http-post.server.headers"):
ctx.response.setHeader(header, value.getStringValue()) headers.add((header, value.getStringValue()))
if cast[PacketType](header.packetType) == MSG_REGISTER: if cast[PacketType](header.packetType) == MSG_REGISTER:
if not register(string.toBytes(ctx.request.body)): if not register(string.toBytes(request.body)):
resp "", Http400 request.respond(400, body = "")
return return
elif cast[PacketType](header.packetType) == MSG_RESULT: elif cast[PacketType](header.packetType) == MSG_RESULT:
handleResult(string.toBytes(ctx.request.body)) handleResult(string.toBytes(request.body))
resp "", Http200 request.respond(200, body = "")
except CatchableError: except CatchableError:
resp "", Http404 request.respond(404, body = "")
return return

View File

@@ -141,7 +141,7 @@ proc agentBuild*(cq: Conquest, listener, sleepDelay: string, sleepTechnique: str
cq.error(fmt"Listener {listener.toUpperAscii} does not exist.") cq.error(fmt"Listener {listener.toUpperAscii} does not exist.")
return false return false
let listener = cq.listeners[listener.toUpperAscii] let listener = cq.listeners[listener.toUpperAscii].listener
var config: seq[byte] var config: seq[byte]
if sleepDelay.isEmptyOrWhitespace(): if sleepDelay.isEmptyOrWhitespace():

View File

@@ -1,19 +1,14 @@
import strformat, strutils, terminal import strformat, strutils, terminal
import prologue, parsetoml import mummy, mummy/routers
import parsetoml
import ../globals
import ../utils import ../utils
import ../api/routes import ../api/routes
import ../db/database import ../db/database
import ../core/logger import ../core/logger
import ../../common/[types, utils, profile] import ../../common/[types, utils, profile]
# Utility functions
proc delListener(cq: Conquest, listenerName: string) =
cq.listeners.del(listenerName)
proc add(cq: Conquest, listener: Listener) =
cq.listeners[listener.listenerId] = listener
#[ #[
Listener management Listener management
]# ]#
@@ -36,104 +31,110 @@ proc listenerList*(cq: Conquest) =
let listeners = cq.dbGetAllListeners() let listeners = cq.dbGetAllListeners()
cq.drawTable(listeners) cq.drawTable(listeners)
proc serve(listener: Listener) {.thread.} =
try:
listener.server.serve(Port(listener.port), listener.address)
except Exception:
discard
proc listenerStart*(cq: Conquest, host: string, portStr: string) = proc listenerStart*(cq: Conquest, host: string, portStr: string) =
# Validate arguments # Validate arguments
try: try:
if not validatePort(portStr): if not validatePort(portStr):
raise newException(CatchableError,fmt"[ - ] Invalid port number: {portStr}") raise newException(CatchableError, fmt"[ - ] Invalid port number: {portStr}")
let port = portStr.parseInt let port = portStr.parseInt
# Create new listener # Create new listener
let let name: string = generateUUID()
name: string = generateUUID() var router: Router
listenerSettings = newSettings( router.notFoundHandler = routes.error404
appName = name, router.methodNotAllowedHandler = routes.error405
debug = false,
address = "", # For some reason, the program crashes when the ip parameter is passed to the newSettings function
port = Port(port) # As a result, I will hardcode the listener to be served on all interfaces (0.0.0.0) by default
) # TODO: fix this issue and start the listener on the address passed as the HOST parameter
var listener = newApp(settings = listenerSettings)
# Define API endpoints based on C2 profile # Define API endpoints based on C2 profile
# GET requests # GET requests
for endpoint in cq.profile.getArray("http-get.endpoints"): for endpoint in cq.profile.getArray("http-get.endpoints"):
listener.addRoute(endpoint.getStringValue(), routes.httpGet) router.addRoute("GET", endpoint.getStringValue(), routes.httpGet)
# POST requests # POST requests
var postMethods: seq[HttpMethod] var postMethods: seq[string]
for reqMethod in cq.profile.getArray("http-post.request-methods"): for reqMethod in cq.profile.getArray("http-post.request-methods"):
postMethods.add(parseEnum[HttpMethod](reqMethod.getStringValue())) postMethods.add(reqMethod.getStringValue())
# Default method is POST # Default method is POST
if postMethods.len == 0: if postMethods.len == 0:
postMethods = @[HttpPost] postMethods = @["POST"]
for endpoint in cq.profile.getArray("http-post.endpoints"): for endpoint in cq.profile.getArray("http-post.endpoints"):
listener.addRoute(endpoint.getStringValue(), routes.httpPost, postMethods) for httpMethod in postMethods:
router.addRoute(httpMethod, endpoint.getStringValue(), routes.httpPost)
listener.registerErrorHandler(Http404, routes.error404) let server = newServer(router.toHandler())
# Store listener in database # Store listener in database
var listenerInstance = Listener( var listener = Listener(
server: server,
listenerId: name, listenerId: name,
address: host, address: host,
port: port, port: port,
protocol: HTTP protocol: HTTP
) )
if not cq.dbStoreListener(listenerInstance):
raise newException(CatchableError, "Failed to store listener in database.")
# Start serving # Start serving
discard listener.runAsync() var thread: Thread[Listener]
cq.add(listenerInstance) createThread(thread, serve, listener)
server.waitUntilReady()
cq.listeners[name] = (listener, thread)
if not cq.dbStoreListener(listener):
raise newException(CatchableError, "Failed to store listener in database.")
cq.success("Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on {host}:{portStr}.") cq.success("Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on {host}:{portStr}.")
except CatchableError as err: except CatchableError as err:
cq.error("Failed to start listener: ", err.msg) cq.error("Failed to start listener: ", err.msg)
proc restartListeners*(cq: Conquest) = proc restartListeners*(cq: Conquest) =
let listeners: seq[Listener] = cq.dbGetAllListeners() var listeners: seq[Listener] = cq.dbGetAllListeners()
# Restart all active listeners that are stored in the database # Restart all active listeners that are stored in the database
for l in listeners: for listener in listeners:
try: try:
let # Create new listener
settings = newSettings( let name: string = generateUUID()
appName = l.listenerId, var router: Router
debug = false, router.notFoundHandler = routes.error404
address = "", router.methodNotAllowedHandler = routes.error405
port = Port(l.port)
)
listener = newApp(settings = settings)
# Define API endpoints based on C2 profile # Define API endpoints based on C2 profile
# GET requests # GET requests
for endpoint in cq.profile.getArray("http-get.endpoints"): for endpoint in cq.profile.getArray("http-get.endpoints"):
listener.get(endpoint.getStringValue(), routes.httpGet) router.addRoute("GET", endpoint.getStringValue(), routes.httpGet)
# POST requests # POST requests
var postMethods: seq[HttpMethod] var postMethods: seq[string]
for reqMethod in cq.profile.getArray("http-post.request-methods"): for reqMethod in cq.profile.getArray("http-post.request-methods"):
postMethods.add(parseEnum[HttpMethod](reqMethod.getStringValue())) postMethods.add(reqMethod.getStringValue())
# Default method is POST # Default method is POST
if postMethods.len == 0: if postMethods.len == 0:
postMethods = @[HttpPost] postMethods = @["POST"]
for endpoint in cq.profile.getArray("http-post.endpoints"): for endpoint in cq.profile.getArray("http-post.endpoints"):
listener.addRoute(endpoint.getStringValue(), routes.httpPost, postMethods) for httpMethod in postMethods:
router.addRoute(httpMethod, endpoint.getStringValue(), routes.httpPost)
listener.registerErrorHandler(Http404, routes.error404) let server = newServer(router.toHandler())
listener.server = server
discard listener.runAsync() # Start serving
cq.add(l) var thread: Thread[Listener]
cq.success("Restarted listener", fgGreen, fmt" {l.listenerId} ", resetStyle, fmt"on {l.address}:{$l.port}.") createThread(thread, serve, listener)
server.waitUntilReady()
# Delay before serving another listener to avoid crashing the application cq.listeners[listener.listenerId] = (listener, thread)
waitFor sleepAsync(100) cq.success("Restarted listener", fgGreen, fmt" {listener.listenerId} ", resetStyle, fmt"on {listener.address}:{$listener.port}.")
except CatchableError as err: except CatchableError as err:
cq.error("Failed to restart listener: ", err.msg) cq.error("Failed to restart listener: ", err.msg)
@@ -154,6 +155,14 @@ proc listenerStop*(cq: Conquest, name: string) =
cq.error("Failed to stop listener: ", getCurrentExceptionMsg()) cq.error("Failed to stop listener: ", getCurrentExceptionMsg())
return return
cq.delListener(name) cq.listeners.del(name)
cq.success("Stopped listener ", fgGreen, name.toUpperAscii, resetStyle, ".") cq.success("Stopped listener ", fgGreen, name.toUpperAscii, resetStyle, ".")
# TODO: Make listener stoppable
# try:
# cq.listeners[name].listener .server.close()
# joinThread(cq.listeners[name].thread)
# except:
# cq.error("Failed to stop listener.")

View File

@@ -129,7 +129,7 @@ proc header() =
proc init*(T: type Conquest, profile: Profile): Conquest = proc init*(T: type Conquest, profile: Profile): Conquest =
var cq = new Conquest var cq = new Conquest
cq.prompt = Prompt.init() cq.prompt = Prompt.init()
cq.listeners = initTable[string, Listener]() cq.listeners = initTable[string, tuple[listener: Listener, thread: Thread[Listener]]]()
cq.agents = initTable[string, Agent]() cq.agents = initTable[string, Agent]()
cq.interactAgent = nil cq.interactAgent = nil
cq.profile = profile cq.profile = profile