1
1
import std/ [algorithm, segfaults, strformat, strutils, asyncdispatch, terminal,
2
- times, os, sysrand, httpclient, osproc, streams, unicode]
2
+ times, os, sysrand, httpclient, osproc, streams, unicode, typedthreads ]
3
3
import checksums/ md5
4
4
import json, jsony
5
5
import ws
52
52
network* : NetworkConfig
53
53
agent* : AgentConfig
54
54
55
+ WhichStream * = enum stdoutStr, stderrStr
56
+
57
+ LineMsg * = object
58
+ stream* : WhichStream # # stdout or stderr
59
+ txt* : string # # one complete line
60
+ done* : bool # # true when process finished
61
+ exit* : int # # exit code (only valid if done)
62
+
55
63
# ----------------------------------------------------------------------------
56
64
# Config IO (fails hard if unreadable)
57
65
# ----------------------------------------------------------------------------
@@ -199,7 +207,63 @@ proc recvBinary(ws: WebSocket): Future[string] {.async.} =
199
207
discard # ignore text, pong …
200
208
201
209
# ----------------------------------------------------------------------------
202
- # Challenge-response handshake (server-initiated)
210
+ # Shell command helpers
211
+ # ----------------------------------------------------------------------------
212
+ type
213
+ StreamParams = tuple [
214
+ pipe: Stream ;
215
+ which: WhichStream ;
216
+ ch: ptr Channel [LineMsg ]
217
+ ]
218
+
219
+ OutParams = tuple [
220
+ process: Process ;
221
+ ch: ptr Channel [LineMsg ]
222
+ ]
223
+
224
+ proc readErr (p: StreamParams ) {.thread .} =
225
+ let (pipe, which, ch) = p
226
+ var line: string
227
+ while pipe.readLine (line):
228
+ ch[].send LineMsg (stream: which, txt: line, done: false )
229
+
230
+ proc readOut (p: OutParams ) {.thread .} =
231
+ let (process, ch) = p
232
+ var line: string
233
+ while process.outputStream.readLine (line):
234
+ ch[].send LineMsg (stream: stdoutStr, txt: line, done: false )
235
+ let rc = process.waitForExit () # EOF ⇒ child quit
236
+ ch[].send LineMsg (done: true , exit: rc) # final signal
237
+
238
+ proc execShellThreaded (rawCmd: string ; ws: WebSocket ;
239
+ cfg: FrameConfig ; id: string ): Future [void ] {.async .} =
240
+ var ch: Channel [LineMsg ]
241
+ ch.open ()
242
+ var p = startProcess (" /bin/bash" ,
243
+ args = [" -c" , rawCmd],
244
+ options = {poUsePath})
245
+
246
+ var thrOut: Thread [OutParams ]
247
+ var thrErr: Thread [StreamParams ]
248
+
249
+ createThread (thrOut, readOut, (p, addr ch)) # stdout + exit code
250
+ createThread (thrErr, readErr, (p.errorStream, stderrStr, addr ch))
251
+
252
+ while true :
253
+ let (ready, msg) = tryRecv (ch) # new one-arg form
254
+ if ready:
255
+ if msg.done:
256
+ await sendResp (ws, cfg, id, msg.exit == 0 , %* {" exit" : msg.exit})
257
+ break
258
+ else :
259
+ let which = if msg.stream == stdoutStr: " stdout" else : " stderr"
260
+ await streamChunk (ws, cfg, id, which, msg.txt & " \n " )
261
+ else :
262
+ await sleepAsync (100 )
263
+ ch.close ()
264
+
265
+ # ----------------------------------------------------------------------------
266
+ # All command handlers
203
267
# ----------------------------------------------------------------------------
204
268
proc handleCmd (cmd: JsonNode ; ws: WebSocket ; cfg: FrameConfig ): Future [void ] {.async .} =
205
269
let id = cmd{" id" }.getStr ()
@@ -210,7 +274,7 @@ proc handleCmd(cmd: JsonNode; ws: WebSocket; cfg: FrameConfig): Future[void] {.a
210
274
211
275
# No remote execution available
212
276
if not cfg.agent.agentRunCommands:
213
- if name != " version" :
277
+ if name != " version" : # only allow "version" command
214
278
await sendResp (ws, cfg, id, false , %* {" error" : " agentRunCommands disabled in config" })
215
279
return
216
280
@@ -265,31 +329,9 @@ proc handleCmd(cmd: JsonNode; ws: WebSocket; cfg: FrameConfig): Future[void] {.a
265
329
" binary" : false })
266
330
of " shell" :
267
331
if not args.hasKey (" cmd" ):
268
- await sendResp (ws, cfg, id, false ,
269
- %* {" error" : " `cmd` missing" })
270
- return
271
-
272
- let cmdStr = args[" cmd" ].getStr
273
-
274
- var p = startProcess (
275
- " /bin/sh" , # command
276
- args = [" -c" , cmdStr], # argv
277
- options = {poUsePath, poStdErrToStdOut}
278
- )
279
-
280
- let bufSize = 4096
281
- var buf = newString (bufSize)
282
-
283
- while true :
284
- let n = p.outputStream.readData (addr buf[0 ], buf.len)
285
- if n == 0 :
286
- if p.running: await sleepAsync (100 ) # no data yet – yield
287
- else : break # process finished – exit loop
288
- else :
289
- await streamChunk (ws, cfg, id, " stdout" , buf[0 ..< n])
290
-
291
- let rc = p.waitForExit ()
292
- await sendResp (ws, cfg, id, rc == 0 , %* {" exit" : rc})
332
+ await sendResp (ws, cfg, id, false , %* {" error" : " `cmd` missing" })
333
+ else :
334
+ asyncCheck execShellThreaded (args[" cmd" ].getStr (), ws, cfg, id)
293
335
294
336
of " file_md5" :
295
337
let path = args{" path" }.getStr (" " )
@@ -452,14 +494,11 @@ proc doHandshake(ws: WebSocket; cfg: FrameConfig): Future[void] {.async.} =
452
494
echo & " ⚠️ handshake failed, unexpected action: { act} in { ackMsg} "
453
495
raise newException (Exception , " Handshake failed: " & ackMsg)
454
496
455
- # ----------------------------------------------------------------------------
456
- # Heartbeat helper
457
- # ----------------------------------------------------------------------------
458
497
proc startHeartbeat (ws: WebSocket ; cfg: FrameConfig ): Future [void ] {.async .} =
459
498
# # Keeps server-side idle-timeout at bay.
460
499
try :
461
500
while true :
462
- await sleepAsync (20_000 )
501
+ await sleepAsync (40_000 )
463
502
let env = makeSecureEnvelope (%* {" type" : " heartbeat" }, cfg)
464
503
await ws.send ($ env)
465
504
except Exception : discard # will quit when ws closes / errors out
@@ -483,7 +522,7 @@ proc runAgent(cfg: FrameConfig) {.async.} =
483
522
await doHandshake (ws, cfg) # throws on failure
484
523
backoff = InitialBackoffSeconds # reset back-off
485
524
486
- asyncCheck startHeartbeat (ws, cfg) # fire-and-forget
525
+ asyncCheck startHeartbeat (ws, cfg)
487
526
488
527
# ── Main receive loop ───────────────────────────────────────────────
489
528
while true :
0 commit comments