robathome icon

MC receiver turtle

robathome | PRO | 11/07/25 01:59:18 AM UTC (Edited) | 0 ⭐ | 543 👁️ | Never ⏰ | []
text |

12.16 KB

|

None

|

0 👍

/

0 👎

-- pushfs_server.lua
-- CC:Tweaked turtle/computer daemon to receive files and run remote commands.
-- Ops: ping | put | begin/chunk/end | exec
-- Protocol: "pushfs-v1"
 --------------------------
-- Config
--------------------------
local PROTOCOL = "pushfs-v1"
local SHARED_SECRET = nil         -- set to a string to require a token
local MAX_IDLE_SEC = 120
local MAX_RETURN_BYTES = 64*1024  -- cap returned output
 -- Lua 5.1/5.2 compat
local unpack = table.unpack or unpack
 --------------------------
-- Utilities
--------------------------
local function openModems()
  local ok = false
  for _, s in ipairs(rs.getSides()) do
    if peripheral.getType(s) == "modem" then
      if not rednet.isOpen(s) then rednet.open(s) end
      ok = true
    end
  end
  if not ok then error("No modem found. Attach a wireless modem.") end
end
 local function adler32(str)
  local MOD = 65521
  local a,b = 1,0
  for i=1,#str do
    a = (a + string.byte(str,i)) % MOD
    b = (b + a) % MOD
  end
  return string.format("%08x", bit32.bor(bit32.lshift(b,16), a))
end
 local function readAll(path)
  if not fs.exists(path) or fs.isDir(path) then return "" end
  local h = fs.open(path, "rb"); local d = h.readAll() or ""; h.close(); return d
end
 local function parentDir(path)
  local p = path:gsub("/+$","")
  local i = p:match("^(.*)/[^/]+$")
  return i or ""
end
 local function sanitizePath(p)
  if p:sub(1,1) ~= "/" then p = "/"..p end
  p = p:gsub("//+","/")
  if p:find("%.%.") then return nil, "Path traversal not allowed" end
  if p:sub(1,5) == "/rom/" or p == "/rom" then return nil, "ROM is read-only" end
  return p
end
 local function matchesTarget(msg, myId, myLabel)
  local t = msg.target
  if t == "all" then return true end
  if type(t) == "table" then
    if t.id and t.id == myId then return true end
    if t.label and myLabel and t.label == myLabel then return true end
  end
  return false
end
 local function ack(toId, txid, ok, op, note, extra)
  local m = { proto=PROTOCOL, txid=txid, ok=ok, op=op, note=note }
  if extra then for k,v in pairs(extra) do m[k]=v end end
  rednet.send(toId, m, PROTOCOL)
end
 --------------------------
-- File transfers
--------------------------
local xfers = {}  -- txid -> { tmp, dst, size, checksum, received, h, t_start }
 local function cleanupTx(tx)
  if tx.h then pcall(function() tx.h.close() end) end
  if tx.tmp and fs.exists(tx.tmp) then pcall(function() fs.delete(tx.tmp) end) end
end
 local function handle_begin(sender, msg)
  if SHARED_SECRET and msg.token ~= SHARED_SECRET then
    return ack(sender, msg.txid, false, "begin", "auth failed")
  end
  if type(msg.fname) ~= "string" or type(msg.size) ~= "number" then
    return ack(sender, msg.txid, false, "begin", "bad params")
  end
  local dst, err = sanitizePath(msg.fname)
  if not dst then return ack(sender, msg.txid, false, "begin", err) end
   local tmp = dst..".push.tmp"
  local pdir = parentDir(dst)
  if pdir ~= "" and not fs.exists(pdir) then fs.makeDir(pdir) end
  if fs.exists(tmp) then fs.delete(tmp) end
   local h, e = fs.open(tmp, "wb")
  if not h then return ack(sender, msg.txid, false, "begin", "open tmp failed: "..tostring(e)) end
   xfers[msg.txid] = {
    tmp = tmp, dst = dst, size = msg.size,
    checksum = msg.checksum, received = 0, h = h, t_start = os.clock()
  }
  return ack(sender, msg.txid, true, "begin", "ready")
end
 local function handle_chunk(sender, msg)
  local tx = xfers[msg.txid]
  if not tx then return ack(sender, msg.txid, false, "chunk", "unknown txid") end
  if not msg.data or type(msg.data) ~= "string" then
    return ack(sender, msg.txid, false, "chunk", "no data")
  end
  tx.h.write(msg.data)
  tx.received = tx.received + #msg.data
  tx.t_start = os.clock()
  return ack(sender, msg.txid, true, "chunk", "ok", {received=tx.received})
end
 local function handle_end(sender, msg)
  local tx = xfers[msg.txid]
  if not tx then return ack(sender, msg.txid, false, "end", "unknown txid") end
   tx.h.close(); tx.h = nil
   if tx.size and tx.received ~= tx.size then
    cleanupTx(tx); xfers[msg.txid]=nil
    return ack(sender, msg.txid, false, "end", ("size mismatch %d!=%d"):format(tx.received, tx.size))
  end
   if tx.checksum then
    local data = readAll(tx.tmp)
    local sum = adler32(data)
    if sum ~= tx.checksum then
      cleanupTx(tx); xfers[msg.txid]=nil
      return ack(sender, msg.txid, false, "end", "checksum mismatch", {have=sum, want=tx.checksum})
    end
  end
   if fs.exists(tx.dst) and fs.isDir(tx.dst) then
    cleanupTx(tx); xfers[msg.txid]=nil
    return ack(sender, msg.txid, false, "end", "destination is a directory")
  end
  if fs.exists(tx.dst) then fs.delete(tx.dst) end
  fs.move(tx.tmp, tx.dst)
   xfers[msg.txid]=nil
  return ack(sender, msg.txid, true, "end", "written", {path=tx.dst, bytes=tx.received})
end
 local function handle_put(sender, msg)
  if SHARED_SECRET and msg.token ~= SHARED_SECRET then
    return ack(sender, msg.txid, false, "put", "auth failed")
  end
  if type(msg.fname) ~= "string" or type(msg.data) ~= "string" then
    return ack(sender, msg.txid, false, "put", "bad params")
  end
  local dst, err = sanitizePath(msg.fname)
  if not dst then return ack(sender, msg.txid, false, "put", err) end
  local pdir = parentDir(dst)
  if pdir ~= "" and not fs.exists(pdir) then fs.makeDir(pdir) end
   local tmp = dst..".push.tmp"
  if fs.exists(tmp) then fs.delete(tmp) end
  local h, e = fs.open(tmp, "wb")
  if not h then return ack(sender, msg.txid, false, "put", "open tmp failed: "..tostring(e)) end
  h.write(msg.data); h.close()
   if msg.checksum then
    local have = adler32(readAll(tmp))
    if have ~= msg.checksum then
      fs.delete(tmp)
      return ack(sender, msg.txid, false, "put", "checksum mismatch", {have=have, want=msg.checksum})
    end
  end
   if fs.exists(dst) then fs.delete(dst) end
  fs.move(tmp, dst)
  return ack(sender, msg.txid, true, "put", "written", {path=dst, bytes=#msg.data})
end
 --------------------------
-- Exec
--------------------------
-- tiny in-memory terminal to capture output
local function makeBufferTerm(width, height)
  local buf = {}
  local x,y,tx,bc = 1,1,colors.white,colors.black
  local w,h = width or 80, height or 25
  for i=1,h do buf[i] = string.rep(" ", w) end
   local function clamp()
    if x < 1 then x = 1 elseif x > w then x = w end
    if y < 1 then y = 1 elseif y > h then y = h end
  end
   local t = {}
  function t.write(s)
    s = tostring(s)
    local line = buf[y]
    local left = line:sub(1, x-1)
    local right = line:sub(x + #s)
    buf[y] = left .. s .. right
    x = x + #s
    clamp()
  end
  function t.blit(s, tc, bc_) t.write(s) end
  function t.clear() for i=1,h do buf[i] = string.rep(" ", w) end x,y=1,1 end
  function t.clearLine() buf[y] = string.rep(" ", w); x=1 end
  function t.getCursorPos() return x,y end
  function t.setCursorPos(nx,ny) x,y=nx,ny; clamp() end
  function t.setCursorBlink(_) end
  function t.getSize() return w,h end
  function t.scroll(n)
    n = n or 1
    if n > 0 then for _=1,n do table.remove(buf,1); table.insert(buf, string.rep(" ", w)) end
    elseif n < 0 then for _=1,-n do table.remove(buf,#buf); table.insert(buf,1, string.rep(" ", w)) end
    end
  end
  function t.isColor() return term.isColor and term.isColor() or true end
  function t.setTextColor(c) tx=c end
  function t.setBackgroundColor(c) bc=c end
  function t.concat() return table.concat(buf, "\n") end
  return t
end
 local function split_cmdline(line)
  local out, i, n = {}, 1, #line
  while i <= n do
    while i<=n and line:sub(i,i):match("%s") do i=i+1 end
    if i>n then break end
    local tok = {}
    local quote = nil
    while i<=n do
      local ch = line:sub(i,i)
      if quote then
        if ch == quote then quote=nil; i=i+1; break
        else tok[#tok+1]=ch; i=i+1 end
      else
        if ch == '"' or ch == "'" then quote = ch; i=i+1
        elseif ch:match("%s") then i=i+1; break
        else tok[#tok+1]=ch; i=i+1 end
      end
    end
    out[#out+1] = table.concat(tok)
  end
  return out
end
 local function run_command_capture(line, cwd, timeout_s)
  local w,h = term.getSize()
  local cap = makeBufferTerm(w,h)
  local oldTerm = term.redirect(cap)
  local oldDir = shell.dir()
  if cwd and type(cwd)=="string" and fs.isDir(cwd) then shell.setDir(cwd) end
   local ok, err
  local function runner()
    local args = split_cmdline(line)
    if #args == 0 then return true end
    ok = shell.run(unpack(args))
    if not ok then err = "command failed" end
  end
   local done = false
  local function wrap()
    local s, e = pcall(runner)
    if not s then ok=false; err=tostring(e) end
    done = true
  end
   if timeout_s and timeout_s > 0 then
    local tid = os.startTimer(timeout_s)
    parallel.waitForAny(function() wrap() end, function()
      while true do local ev,id=os.pullEvent("timer"); if id==tid then return end end
    end)
    if not done then ok=false; err="timeout" end
  else
    wrap()
  end
   term.redirect(oldTerm)
  shell.setDir(oldDir)
  local out = cap.concat()
  if #out > MAX_RETURN_BYTES then
    out = out:sub(1, MAX_RETURN_BYTES) .. "\n[output truncated]"
  end
  return ok == true, out, err
end
 local function handle_exec(sender, msg)
  -- msg: {op="exec", txid, line="<cmd>", capture=true/false, timeout=sec, cwd="/", token=...}
  if SHARED_SECRET and msg.token ~= SHARED_SECRET then
    return ack(sender, msg.txid, false, "exec", "auth failed")
  end
  if type(msg.line) ~= "string" or #msg.line == 0 then
    return ack(sender, msg.txid, false, "exec", "missing line")
  end
   if msg.capture then
    local ok, out, err = run_command_capture(msg.line, msg.cwd, msg.timeout)
    return ack(sender, msg.txid, ok, "exec", ok and "ok" or (err or "error"), {output=out})
  else
    -- background only when tabs exist; otherwise reject so client can use capture
    local cmdline = msg.line
    if multishell and multishell.launch then
      multishell.launch({}, "/rom/programs/shell", "-c", cmdline)
      return ack(sender, msg.txid, true, "exec", "started")
    elseif shell.openTab then
      shell.openTab("/rom/programs/shell", "-c", cmdline)
      return ack(sender, msg.txid, true, "exec", "started")
    else
      return ack(sender, msg.txid, false, "exec", "no background support on this device; use capture=true")
    end
  end
end
 local function handle_ping(sender, msg, myId, myLabel)
  return ack(sender, msg.txid, true, "ping", "pong", {id=myId, label=myLabel or "", time=os.epoch("utc")})
end
 --------------------------
-- Reaper
--------------------------
local function reapStale()
  local now = os.clock()
  for txid, tx in pairs(xfers) do
    if now - (tx.t_start or now) > MAX_IDLE_SEC then
      cleanupTx(tx)
      xfers[txid] = nil
    end
  end
end
 --------------------------
-- Main
--------------------------
local function main()
  openModems()
  local myId = os.getComputerID()
  local myLabel = os.getComputerLabel()
  rednet.host(PROTOCOL, (myLabel or ("node-"..tostring(myId))))
   print(("pushfs_server: id=%d label=%s"):format(myId, tostring(myLabel)))
  print("Waiting...")
   while true do
    local ev = { os.pullEvent() }
    if ev[1] == "rednet_message" then
      local sender, message, proto = ev[2], ev[3], ev[4]
      if proto == PROTOCOL and type(message) == "table" and matchesTarget(message, myId, myLabel) then
        local op = message.op
        if     op == "begin" then handle_begin(sender, message)
        elseif op == "chunk" then handle_chunk(sender, message)
        elseif op == "end"   then handle_end(sender, message)
        elseif op == "put"   then handle_put(sender, message)
        elseif op == "exec"  then handle_exec(sender, message)
        elseif op == "ping"  then handle_ping(sender, message, myId, myLabel)
        else ack(sender, message.txid, false, op or "?", "unknown op") end
      end
    elseif ev[1] == "timer" then
      reapStale()
    end
    os.startTimer(5)
  end
end
 local ok, err = pcall(main)
if not ok then
  print("Error: "..tostring(err))
  for _, tx in pairs(xfers) do cleanupTx(tx) end
end
 

Comments