Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- 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
Advertisement
Add Comment
Please, Sign In to add comment