robathome

MC receiver turtle

Nov 6th, 2025 (edited)
630
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.16 KB | None | 0 0
  1. -- pushfs_server.lua
  2. -- CC:Tweaked turtle/computer daemon to receive files and run remote commands.
  3. -- Ops: ping | put | begin/chunk/end | exec
  4. -- Protocol: "pushfs-v1"
  5.  
  6. --------------------------
  7. -- Config
  8. --------------------------
  9. local PROTOCOL = "pushfs-v1"
  10. local SHARED_SECRET = nil -- set to a string to require a token
  11. local MAX_IDLE_SEC = 120
  12. local MAX_RETURN_BYTES = 64*1024 -- cap returned output
  13.  
  14. -- Lua 5.1/5.2 compat
  15. local unpack = table.unpack or unpack
  16.  
  17. --------------------------
  18. -- Utilities
  19. --------------------------
  20. local function openModems()
  21. local ok = false
  22. for _, s in ipairs(rs.getSides()) do
  23. if peripheral.getType(s) == "modem" then
  24. if not rednet.isOpen(s) then rednet.open(s) end
  25. ok = true
  26. end
  27. end
  28. if not ok then error("No modem found. Attach a wireless modem.") end
  29. end
  30.  
  31. local function adler32(str)
  32. local MOD = 65521
  33. local a,b = 1,0
  34. for i=1,#str do
  35. a = (a + string.byte(str,i)) % MOD
  36. b = (b + a) % MOD
  37. end
  38. return string.format("%08x", bit32.bor(bit32.lshift(b,16), a))
  39. end
  40.  
  41. local function readAll(path)
  42. if not fs.exists(path) or fs.isDir(path) then return "" end
  43. local h = fs.open(path, "rb"); local d = h.readAll() or ""; h.close(); return d
  44. end
  45.  
  46. local function parentDir(path)
  47. local p = path:gsub("/+$","")
  48. local i = p:match("^(.*)/[^/]+$")
  49. return i or ""
  50. end
  51.  
  52. local function sanitizePath(p)
  53. if p:sub(1,1) ~= "/" then p = "/"..p end
  54. p = p:gsub("//+","/")
  55. if p:find("%.%.") then return nil, "Path traversal not allowed" end
  56. if p:sub(1,5) == "/rom/" or p == "/rom" then return nil, "ROM is read-only" end
  57. return p
  58. end
  59.  
  60. local function matchesTarget(msg, myId, myLabel)
  61. local t = msg.target
  62. if t == "all" then return true end
  63. if type(t) == "table" then
  64. if t.id and t.id == myId then return true end
  65. if t.label and myLabel and t.label == myLabel then return true end
  66. end
  67. return false
  68. end
  69.  
  70. local function ack(toId, txid, ok, op, note, extra)
  71. local m = { proto=PROTOCOL, txid=txid, ok=ok, op=op, note=note }
  72. if extra then for k,v in pairs(extra) do m[k]=v end end
  73. rednet.send(toId, m, PROTOCOL)
  74. end
  75.  
  76. --------------------------
  77. -- File transfers
  78. --------------------------
  79. local xfers = {} -- txid -> { tmp, dst, size, checksum, received, h, t_start }
  80.  
  81. local function cleanupTx(tx)
  82. if tx.h then pcall(function() tx.h.close() end) end
  83. if tx.tmp and fs.exists(tx.tmp) then pcall(function() fs.delete(tx.tmp) end) end
  84. end
  85.  
  86. local function handle_begin(sender, msg)
  87. if SHARED_SECRET and msg.token ~= SHARED_SECRET then
  88. return ack(sender, msg.txid, false, "begin", "auth failed")
  89. end
  90. if type(msg.fname) ~= "string" or type(msg.size) ~= "number" then
  91. return ack(sender, msg.txid, false, "begin", "bad params")
  92. end
  93. local dst, err = sanitizePath(msg.fname)
  94. if not dst then return ack(sender, msg.txid, false, "begin", err) end
  95.  
  96. local tmp = dst..".push.tmp"
  97. local pdir = parentDir(dst)
  98. if pdir ~= "" and not fs.exists(pdir) then fs.makeDir(pdir) end
  99. if fs.exists(tmp) then fs.delete(tmp) end
  100.  
  101. local h, e = fs.open(tmp, "wb")
  102. if not h then return ack(sender, msg.txid, false, "begin", "open tmp failed: "..tostring(e)) end
  103.  
  104. xfers[msg.txid] = {
  105. tmp = tmp, dst = dst, size = msg.size,
  106. checksum = msg.checksum, received = 0, h = h, t_start = os.clock()
  107. }
  108. return ack(sender, msg.txid, true, "begin", "ready")
  109. end
  110.  
  111. local function handle_chunk(sender, msg)
  112. local tx = xfers[msg.txid]
  113. if not tx then return ack(sender, msg.txid, false, "chunk", "unknown txid") end
  114. if not msg.data or type(msg.data) ~= "string" then
  115. return ack(sender, msg.txid, false, "chunk", "no data")
  116. end
  117. tx.h.write(msg.data)
  118. tx.received = tx.received + #msg.data
  119. tx.t_start = os.clock()
  120. return ack(sender, msg.txid, true, "chunk", "ok", {received=tx.received})
  121. end
  122.  
  123. local function handle_end(sender, msg)
  124. local tx = xfers[msg.txid]
  125. if not tx then return ack(sender, msg.txid, false, "end", "unknown txid") end
  126.  
  127. tx.h.close(); tx.h = nil
  128.  
  129. if tx.size and tx.received ~= tx.size then
  130. cleanupTx(tx); xfers[msg.txid]=nil
  131. return ack(sender, msg.txid, false, "end", ("size mismatch %d!=%d"):format(tx.received, tx.size))
  132. end
  133.  
  134. if tx.checksum then
  135. local data = readAll(tx.tmp)
  136. local sum = adler32(data)
  137. if sum ~= tx.checksum then
  138. cleanupTx(tx); xfers[msg.txid]=nil
  139. return ack(sender, msg.txid, false, "end", "checksum mismatch", {have=sum, want=tx.checksum})
  140. end
  141. end
  142.  
  143. if fs.exists(tx.dst) and fs.isDir(tx.dst) then
  144. cleanupTx(tx); xfers[msg.txid]=nil
  145. return ack(sender, msg.txid, false, "end", "destination is a directory")
  146. end
  147. if fs.exists(tx.dst) then fs.delete(tx.dst) end
  148. fs.move(tx.tmp, tx.dst)
  149.  
  150. xfers[msg.txid]=nil
  151. return ack(sender, msg.txid, true, "end", "written", {path=tx.dst, bytes=tx.received})
  152. end
  153.  
  154. local function handle_put(sender, msg)
  155. if SHARED_SECRET and msg.token ~= SHARED_SECRET then
  156. return ack(sender, msg.txid, false, "put", "auth failed")
  157. end
  158. if type(msg.fname) ~= "string" or type(msg.data) ~= "string" then
  159. return ack(sender, msg.txid, false, "put", "bad params")
  160. end
  161. local dst, err = sanitizePath(msg.fname)
  162. if not dst then return ack(sender, msg.txid, false, "put", err) end
  163. local pdir = parentDir(dst)
  164. if pdir ~= "" and not fs.exists(pdir) then fs.makeDir(pdir) end
  165.  
  166. local tmp = dst..".push.tmp"
  167. if fs.exists(tmp) then fs.delete(tmp) end
  168. local h, e = fs.open(tmp, "wb")
  169. if not h then return ack(sender, msg.txid, false, "put", "open tmp failed: "..tostring(e)) end
  170. h.write(msg.data); h.close()
  171.  
  172. if msg.checksum then
  173. local have = adler32(readAll(tmp))
  174. if have ~= msg.checksum then
  175. fs.delete(tmp)
  176. return ack(sender, msg.txid, false, "put", "checksum mismatch", {have=have, want=msg.checksum})
  177. end
  178. end
  179.  
  180. if fs.exists(dst) then fs.delete(dst) end
  181. fs.move(tmp, dst)
  182. return ack(sender, msg.txid, true, "put", "written", {path=dst, bytes=#msg.data})
  183. end
  184.  
  185. --------------------------
  186. -- Exec
  187. --------------------------
  188. -- tiny in-memory terminal to capture output
  189. local function makeBufferTerm(width, height)
  190. local buf = {}
  191. local x,y,tx,bc = 1,1,colors.white,colors.black
  192. local w,h = width or 80, height or 25
  193. for i=1,h do buf[i] = string.rep(" ", w) end
  194.  
  195. local function clamp()
  196. if x < 1 then x = 1 elseif x > w then x = w end
  197. if y < 1 then y = 1 elseif y > h then y = h end
  198. end
  199.  
  200. local t = {}
  201. function t.write(s)
  202. s = tostring(s)
  203. local line = buf[y]
  204. local left = line:sub(1, x-1)
  205. local right = line:sub(x + #s)
  206. buf[y] = left .. s .. right
  207. x = x + #s
  208. clamp()
  209. end
  210. function t.blit(s, tc, bc_) t.write(s) end
  211. function t.clear() for i=1,h do buf[i] = string.rep(" ", w) end x,y=1,1 end
  212. function t.clearLine() buf[y] = string.rep(" ", w); x=1 end
  213. function t.getCursorPos() return x,y end
  214. function t.setCursorPos(nx,ny) x,y=nx,ny; clamp() end
  215. function t.setCursorBlink(_) end
  216. function t.getSize() return w,h end
  217. function t.scroll(n)
  218. n = n or 1
  219. if n > 0 then for _=1,n do table.remove(buf,1); table.insert(buf, string.rep(" ", w)) end
  220. elseif n < 0 then for _=1,-n do table.remove(buf,#buf); table.insert(buf,1, string.rep(" ", w)) end
  221. end
  222. end
  223. function t.isColor() return term.isColor and term.isColor() or true end
  224. function t.setTextColor(c) tx=c end
  225. function t.setBackgroundColor(c) bc=c end
  226. function t.concat() return table.concat(buf, "\n") end
  227. return t
  228. end
  229.  
  230. local function split_cmdline(line)
  231. local out, i, n = {}, 1, #line
  232. while i <= n do
  233. while i<=n and line:sub(i,i):match("%s") do i=i+1 end
  234. if i>n then break end
  235. local tok = {}
  236. local quote = nil
  237. while i<=n do
  238. local ch = line:sub(i,i)
  239. if quote then
  240. if ch == quote then quote=nil; i=i+1; break
  241. else tok[#tok+1]=ch; i=i+1 end
  242. else
  243. if ch == '"' or ch == "'" then quote = ch; i=i+1
  244. elseif ch:match("%s") then i=i+1; break
  245. else tok[#tok+1]=ch; i=i+1 end
  246. end
  247. end
  248. out[#out+1] = table.concat(tok)
  249. end
  250. return out
  251. end
  252.  
  253. local function run_command_capture(line, cwd, timeout_s)
  254. local w,h = term.getSize()
  255. local cap = makeBufferTerm(w,h)
  256. local oldTerm = term.redirect(cap)
  257. local oldDir = shell.dir()
  258. if cwd and type(cwd)=="string" and fs.isDir(cwd) then shell.setDir(cwd) end
  259.  
  260. local ok, err
  261. local function runner()
  262. local args = split_cmdline(line)
  263. if #args == 0 then return true end
  264. ok = shell.run(unpack(args))
  265. if not ok then err = "command failed" end
  266. end
  267.  
  268. local done = false
  269. local function wrap()
  270. local s, e = pcall(runner)
  271. if not s then ok=false; err=tostring(e) end
  272. done = true
  273. end
  274.  
  275. if timeout_s and timeout_s > 0 then
  276. local tid = os.startTimer(timeout_s)
  277. parallel.waitForAny(function() wrap() end, function()
  278. while true do local ev,id=os.pullEvent("timer"); if id==tid then return end end
  279. end)
  280. if not done then ok=false; err="timeout" end
  281. else
  282. wrap()
  283. end
  284.  
  285. term.redirect(oldTerm)
  286. shell.setDir(oldDir)
  287. local out = cap.concat()
  288. if #out > MAX_RETURN_BYTES then
  289. out = out:sub(1, MAX_RETURN_BYTES) .. "\n[output truncated]"
  290. end
  291. return ok == true, out, err
  292. end
  293.  
  294. local function handle_exec(sender, msg)
  295. -- msg: {op="exec", txid, line="<cmd>", capture=true/false, timeout=sec, cwd="/", token=...}
  296. if SHARED_SECRET and msg.token ~= SHARED_SECRET then
  297. return ack(sender, msg.txid, false, "exec", "auth failed")
  298. end
  299. if type(msg.line) ~= "string" or #msg.line == 0 then
  300. return ack(sender, msg.txid, false, "exec", "missing line")
  301. end
  302.  
  303. if msg.capture then
  304. local ok, out, err = run_command_capture(msg.line, msg.cwd, msg.timeout)
  305. return ack(sender, msg.txid, ok, "exec", ok and "ok" or (err or "error"), {output=out})
  306. else
  307. -- background only when tabs exist; otherwise reject so client can use capture
  308. local cmdline = msg.line
  309. if multishell and multishell.launch then
  310. multishell.launch({}, "/rom/programs/shell", "-c", cmdline)
  311. return ack(sender, msg.txid, true, "exec", "started")
  312. elseif shell.openTab then
  313. shell.openTab("/rom/programs/shell", "-c", cmdline)
  314. return ack(sender, msg.txid, true, "exec", "started")
  315. else
  316. return ack(sender, msg.txid, false, "exec", "no background support on this device; use capture=true")
  317. end
  318. end
  319. end
  320.  
  321. local function handle_ping(sender, msg, myId, myLabel)
  322. return ack(sender, msg.txid, true, "ping", "pong", {id=myId, label=myLabel or "", time=os.epoch("utc")})
  323. end
  324.  
  325. --------------------------
  326. -- Reaper
  327. --------------------------
  328. local function reapStale()
  329. local now = os.clock()
  330. for txid, tx in pairs(xfers) do
  331. if now - (tx.t_start or now) > MAX_IDLE_SEC then
  332. cleanupTx(tx)
  333. xfers[txid] = nil
  334. end
  335. end
  336. end
  337.  
  338. --------------------------
  339. -- Main
  340. --------------------------
  341. local function main()
  342. openModems()
  343. local myId = os.getComputerID()
  344. local myLabel = os.getComputerLabel()
  345. rednet.host(PROTOCOL, (myLabel or ("node-"..tostring(myId))))
  346.  
  347. print(("pushfs_server: id=%d label=%s"):format(myId, tostring(myLabel)))
  348. print("Waiting...")
  349.  
  350. while true do
  351. local ev = { os.pullEvent() }
  352. if ev[1] == "rednet_message" then
  353. local sender, message, proto = ev[2], ev[3], ev[4]
  354. if proto == PROTOCOL and type(message) == "table" and matchesTarget(message, myId, myLabel) then
  355. local op = message.op
  356. if op == "begin" then handle_begin(sender, message)
  357. elseif op == "chunk" then handle_chunk(sender, message)
  358. elseif op == "end" then handle_end(sender, message)
  359. elseif op == "put" then handle_put(sender, message)
  360. elseif op == "exec" then handle_exec(sender, message)
  361. elseif op == "ping" then handle_ping(sender, message, myId, myLabel)
  362. else ack(sender, message.txid, false, op or "?", "unknown op") end
  363. end
  364. elseif ev[1] == "timer" then
  365. reapStale()
  366. end
  367. os.startTimer(5)
  368. end
  369. end
  370.  
  371. local ok, err = pcall(main)
  372. if not ok then
  373. print("Error: "..tostring(err))
  374. for _, tx in pairs(xfers) do cleanupTx(tx) end
  375. end
  376.  
Advertisement
Add Comment
Please, Sign In to add comment