Advertisement
goldfiction

rawshell

Jun 22nd, 2025
377
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 19.58 KB | Gaming | 0 0
  1. -- MIT License
  2. --
  3. -- Copyright (c) 2021 JackMacWindows
  4. --
  5. -- Permission is hereby granted, free of charge, to any person obtaining a copy
  6. -- of this software and associated documentation files (the "Software"), to deal
  7. -- in the Software without restriction, including without limitation the rights
  8. -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. -- copies of the Software, and to permit persons to whom the Software is
  10. -- furnished to do so, subject to the following conditions:
  11. --
  12. -- The above copyright notice and this permission notice shall be included in all
  13. -- copies or substantial portions of the Software.
  14. --
  15. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. -- SOFTWARE.
  22.  
  23. local rawterm = require "rawterm" -- https://gist.github.com/MCJack123/50b211c55ceca4376e51d33435026006
  24. local hasECC, ecc                 -- https://pastebin.com/ZGJGBJdg (comment out `os.pullEvent`s)
  25. local hasRedrun, redrun           -- https://gist.github.com/MCJack123/473475f07b980d57dd2bd818026c97e8
  26.  
  27. local localEvents = {key = true, key_up = true, char = true, mouse_click = true, mouse_up = true, mouse_drag = true, mouse_scroll = true, mouse_move = true, term_resize = true, paste = true}
  28. local serverRunning = false
  29. local width, height = term.getSize()
  30.  
  31. local function randomString()
  32.     local str = ""
  33.     for i = 1, 16 do str = str .. string.char(math.random(32, 127)) end
  34.     return str
  35. end
  36.  
  37. local function singleserver(delegate, func, ...)
  38.     local server = rawterm.server(delegate, width, height, 0, "Remote Shell")
  39.     delegate.server = server
  40.     local coro = coroutine.create(func)
  41.     local oldterm = term.redirect(server)
  42.     local ok, filter = coroutine.resume(coro, ...)
  43.     term.redirect(oldterm)
  44.     server.setVisible(false)
  45.     local lastRender = os.epoch "utc"
  46.     while ok and coroutine.status(coro) == "suspended" and not delegate.closed do
  47.         local ev = table.pack(server.pullEvent(filter, true))
  48.         oldterm = term.redirect(server)
  49.         ok, filter = coroutine.resume(coro, table.unpack(ev, 1, ev.n))
  50.         term.redirect(oldterm)
  51.         if os.epoch "utc" - lastRender >= 50 then
  52.             server.setVisible(true)
  53.             server.setVisible(false)
  54.             lastRender = os.epoch "utc"
  55.         end
  56.     end
  57.     if not ok then printError(filter) end
  58.     server.close()
  59.     if coroutine.status(coro) == "suspended" then
  60.         oldterm = term.redirect(server)
  61.         filter = coroutine.resume(coro, "terminate")
  62.         term.redirect(oldterm)
  63.     end
  64. end
  65.  
  66. local delegate_mt = {}
  67. delegate_mt.__index = delegate_mt
  68. function delegate_mt:send(data)
  69.     if self.closed then return end
  70.     if self.key then data = string.char(table.unpack(ecc.encrypt(randomString() .. data, self.key))) end
  71.     self.modem.transmit(self.port, self.port, {id = os.computerID(), data = data})
  72. end
  73. function delegate_mt:receive()
  74.     if self.closed then return nil end
  75.     while true do
  76.         local ev, side, channel, reply, message = os.pullEventRaw("modem_message")
  77.         if ev == "modem_message" and channel == self.port and type(message) == "table" and message.id == self.id then
  78.             message = message.data
  79.             if self.key then
  80.                 message = string.char(table.unpack(ecc.decrypt(message, self.key)))
  81.                 --[[ argh, decrypt yields and that will break this, so we have to run it in a coroutine!
  82.                 local coro = coroutine.create(ecc.decrypt)
  83.                 local ok, a
  84.                 while coroutine.status(coro) == "suspended" do ok, a = coroutine.resume(coro, message, self.key) end
  85.                 if not ok then printError(message) return end
  86.                 message = string.char(table.unpack(a))
  87.                 ]]
  88.                 if #message > 16 and not self.nonces[message:sub(1, 16)] then
  89.                     self.nonces[message:sub(1, 16)] = true
  90.                     self.port = reply
  91.                     return message:sub(17)
  92.                 end
  93.             else
  94.                 self.port = reply
  95.                 return message
  96.             end
  97.         end
  98.     end
  99. end
  100. function delegate_mt:close()
  101.     if self.closed then return end
  102.     if not self.silent then print("> Closed connection on port " .. self.port) end
  103.     self.modem.close(self.port)
  104.     self.key = nil
  105.     self.nonces = nil
  106.     self.closed = true
  107. end
  108.  
  109. local function makeDelegate(modem, port, key, id, silent)
  110.     modem.open(port)
  111.     return setmetatable({
  112.         modem = modem,
  113.         port = port,
  114.         key = key,
  115.         id = id,
  116.         silent = silent,
  117.         closed = false,
  118.         nonces = key and {}
  119.     }, delegate_mt)
  120. end
  121.  
  122. local function serve(password, secure, modem, program, url, background)
  123.     if secure and not hasECC then error("Secure mode requires the ECC library to function.", 2)
  124.     elseif password and not secure then
  125.         term.setTextColor(colors.yellow)
  126.         print("Warning: A password was set, but secure mode is disabled. Password will be sent in plaintext.")
  127.         term.setTextColor(colors.white)
  128.     end
  129.     modem = modem or peripheral.find("modem")
  130.     if not modem then error("Please attach a modem.", 2) end
  131.     modem.open(5731)
  132.     local priv, pub
  133.     if secure then
  134.         priv, pub = ecc.keypair(ecc.random.random())
  135.         if password then password = ecc.sha256.digest(password):toHex() end
  136.     end
  137.     print("Server is now listening for connections.")
  138.     local threads = {}
  139.     local usedChallenges = {}
  140.     serverRunning = true
  141.     while serverRunning do
  142.         local ev = table.pack(coroutine.yield())
  143.         if ev[1] == "modem_message" and ev[3] == 5731 and type(ev[5]) == "table" and ev[5].server == os.computerID() then
  144.             if not ev[5].id then
  145.                 modem.transmit(5731, 5731, {server = os.computerID(), status = "Missing ID"})
  146.             elseif secure and (not ev[5].key or not ev[5].challenge) then
  147.                 modem.transmit(5731, 5731, {server = os.computerID(), id = ev[5].id, status = "Secure connection required", key = pub, challenge = randomString()})
  148.             elseif secure and (not ev[5].response or string.char(table.unpack(ecc.decrypt(ev[5].response, ecc.exchange(priv, ev[5].key)) or {})) ~= ev[5].challenge) then
  149.                 modem.transmit(5731, 5731, {server = os.computerID(), id = ev[5].id, status = "Challenge failed", key = pub, challenge = randomString()})
  150.             elseif password and not ev[5].password then
  151.                 modem.transmit(5731, 5731, {server = os.computerID(), id = ev[5].id, status = "Password required"})
  152.             else
  153.                 local ok = true
  154.                 local key
  155.                 if secure then key = ecc.exchange(priv, ev[5].key) end
  156.                 if password then
  157.                     if secure then ok = not usedChallenges[ev[5].challenge] and string.char(table.unpack(ecc.decrypt(ev[5].password, key))) == password .. ev[5].challenge
  158.                     else ok = ev[5].password == password end
  159.                 end
  160.                 if ok then
  161.                     if secure then usedChallenges[ev[5].challenge] = true end
  162.                     local port = math.random(1000, 65500)
  163.                     while modem.isOpen(port) do port = math.random(1000, 65500) end
  164.                     if not background then print("> New connection from ID " .. ev[5].id .. " on port " .. port) end
  165.                     modem.transmit(5731, port, {server = os.computerID(), id = ev[5].id, status = "Opening connection"})
  166.                     local coro = coroutine.create(singleserver)
  167.                     local delegate = makeDelegate(modem, port, key, ev[5].id, background)
  168.                     local ok, filter
  169.                     if background then
  170.                         if program then program = program:gsub("^%S+", shell.resolveProgram) end
  171.                         ok, filter = coroutine.resume(coro, delegate, os.run, setmetatable({}, {__index = _G}), program or "rom/programs/shell.lua")
  172.                     else ok, filter = coroutine.resume(coro, delegate, shell.run, program or "shell") end
  173.                     if ok then threads[#threads+1] = {delegate = delegate, coro = coro, filter = filter}
  174.                     else printError(filter) end
  175.                 else
  176.                     modem.transmit(5731, 5731, {server = os.computerID(), id = ev[5].id, status = "Password incorrect"})
  177.                 end
  178.             end
  179.         elseif ev[1] == "terminate" then serverRunning = false
  180.         else
  181.             local ok
  182.             local delete = {}
  183.             for i,v in pairs(threads) do
  184.                 if (v.filter == nil or v.filter == ev[1]) and not localEvents[ev[1]] then
  185.                     ok, v.filter = coroutine.resume(v.coro, table.unpack(ev, 1, ev.n))
  186.                     if not ok or coroutine.status(v.coro) ~= "suspended" then
  187.                         if not ok then printError(v.filter) end
  188.                         delete[#delete+1] = i
  189.                     end
  190.                 end
  191.             end
  192.             for _,v in ipairs(delete) do threads[v] = nil end
  193.         end
  194.     end
  195.     for _,v in pairs(threads) do
  196.         if coroutine.status(v.coro) == "suspended" then coroutine.resume(v.coro, "terminate") end
  197.         v.delegate.server.close()
  198.     end
  199.     print("Server closed.")
  200. end
  201.  
  202. local function recv(id)
  203.     local tm = os.startTimer(5)
  204.     while true do
  205.         local ev = table.pack(os.pullEvent())
  206.         if ev[1] == "modem_message" and ev[3] == 5731 and type(ev[5]) == "table" and ev[5].server == id then return ev[5], ev[4]
  207.         elseif ev[1] == "timer" and ev[2] == tm then return nil end
  208.     end
  209. end
  210.  
  211. local function connect(id, modem, win)
  212.     if not tonumber(id) then
  213.         if not http.checkURL(id:gsub("wss?://", "http://")) then error("ID argument must be a number or URL", 2) end
  214.         local delegate = rawterm.wsDelegate(id)
  215.         return rawterm.client(delegate, 0, win), delegate
  216.     end
  217.     id = tonumber(id)
  218.     modem = modem or peripheral.find("modem")
  219.     if not modem then error("Please attach a modem.", 2) end
  220.     modem.open(5731)
  221.     local req = {server = id, id = os.computerID()}
  222.     local key, res, port
  223.     while true do
  224.         modem.transmit(5731, 5731, req)
  225.         res, port = recv(id)
  226.         if not res then error("Connection failed: Timeout") end
  227.         if res.status == "Secure connection required" then
  228.             if not hasECC then hasECC, ecc = pcall(require, "ecc") end
  229.             if not hasECC then error("Connection failed: Server requires secure connection, but ECC library is not installed.", 2) end
  230.             local priv, pub = ecc.keypair(ecc.random.random())
  231.             key = ecc.exchange(priv, res.key)
  232.             req.key = pub
  233.             req.challenge = res.challenge
  234.             req.response = string.char(table.unpack(ecc.encrypt(res.challenge, key)))
  235.         elseif res.status == "Password required" then
  236.             if not key then print("Warning: This connection is not secure. Your password will be sent unencrypted.") end
  237.             write("Password: ")
  238.             req.password = read("\7")
  239.             if key then req.password = string.char(table.unpack(ecc.encrypt(ecc.sha256.digest(req.password):toHex() .. req.challenge, key))) end
  240.         elseif res.status == "Opening connection" then break
  241.         else error("Connection failed: " .. res.status, 2) end
  242.     end
  243.     local delegate = makeDelegate(modem, port, key, id, true)
  244.     return rawterm.client(delegate, 0, win), delegate
  245. end
  246.  
  247. local args = {...}
  248.  
  249. if args[1] == "serve" or args[1] == "host" then
  250.     local background = false
  251.     local program = nil
  252.     local modem = nil
  253.     local password = nil
  254.     local secure = false
  255.     local url = nil
  256.     local nextarg = nil
  257.     for _, arg in ipairs(args) do
  258.         if nextarg then
  259.             if nextarg == 1 then program = arg
  260.             elseif nextarg == 2 then modem = arg
  261.             elseif nextarg == 3 then password = arg
  262.             elseif nextarg == 4 then url = arg
  263.             elseif nextarg == 5 then
  264.                 local w, h = arg:match("^(%d+)x(%d+)$")
  265.                 if not w then error("Invalid argument for -r") end
  266.                 width, height = tonumber(w), tonumber(h)
  267.             end
  268.             nextarg = nil
  269.         elseif arg == "-b" then
  270.             hasRedrun, redrun = pcall(require, "redrun")
  271.             background = true
  272.         elseif arg == "-s" then
  273.             hasECC, ecc = pcall(require, "ecc")
  274.             secure = true
  275.         elseif arg == "-c" then nextarg = 1
  276.         elseif arg == "-m" then nextarg = 2
  277.         elseif arg == "-p" then nextarg = 3
  278.         elseif arg == "-r" then nextarg = 5
  279.         elseif arg == "-w" then nextarg = 4 end
  280.     end
  281.  
  282.     if modem then
  283.         if peripheral.getType(modem) ~= "modem" then error("Peripheral on selected side is not a modem.") end
  284.         modem = peripheral.wrap(modem)
  285.     end
  286.     if background then
  287.         if not hasRedrun then error("Background task running requires the RedRun library.") end
  288.         if url then
  289.             redrun.start(function() return singleserver(rawterm.wsDelegate(url, {["X-Rawterm-Is-Server"] = "Yes"}), os.run, setmetatable({}, {__index = _G}), program or "rom/programs/shell.lua") end, "rawshell_server")
  290.         else
  291.             redrun.start(function() return serve(password, secure, modem, program, url, true) end, "rawshell_server")
  292.             while not serverRunning do coroutine.yield() end
  293.         end
  294.     elseif url then singleserver(rawterm.wsDelegate(url, {["X-Rawterm-Is-Server"] = "Yes"}), shell.run, program or "shell")
  295.     else serve(password, secure, modem, program, url, false) end
  296. elseif args[1] == "connect" and args[2] then
  297.     local modem
  298.     if args[3] then
  299.         if peripheral.getType(args[3]) ~= "modem" then error("Peripheral on selected side is not a modem.") end
  300.         modem = peripheral.wrap(args[3])
  301.     end
  302.     local handle = connect(args[2], modem, term.current())
  303.     local ok, err = pcall(handle.run)
  304.     if term.current().setVisible then term.current().setVisible(true) end
  305.     handle.close()
  306.     term.setBackgroundColor(colors.black)
  307.     term.setTextColor(colors.white)
  308.     term.clear()
  309.     term.setCursorPos(1, 1)
  310.     term.setCursorBlink(true)
  311.     if not ok then error(err, 2) end
  312. elseif args[1] == "get" and args[2] and args[3] then
  313.     local modem
  314.     if args[5] then
  315.         if peripheral.getType(args[5]) ~= "modem" then error("Peripheral on selected side is not a modem.") end
  316.         modem = peripheral.wrap(args[5])
  317.     end
  318.     local handle, delegate = connect(args[2], modem, nil)
  319.     parallel.waitForAny(
  320.         function() while not handle.fs do handle.update(delegate:receive()) end end,
  321.         function() sleep(2) end)
  322.     if not handle.fs then error("Connection failed: Server does not support filesystem transfers") end
  323.     local infile, err = handle.fs.open(args[3], "rb")
  324.     if not infile then error("Could not open remote file: " .. (err or "Unknown error")) end
  325.     local outfile, err = fs.open(args[4] or shell.resolve(fs.getName(args[3])), "wb")
  326.     if not outfile then
  327.         infile.close()
  328.         error("Could not open local file: " .. (err or "Unknown error"))
  329.     end
  330.     outfile.write(infile.readAll())
  331.     infile.close()
  332.     outfile.close()
  333.     handle.close()
  334.     print("Downloaded file as " .. (args[4] or shell.resolve(fs.getName(args[3]))))
  335. elseif args[1] == "put" and args[2] and args[3] and args[4] then
  336.     local modem
  337.     if args[5] then
  338.         if peripheral.getType(args[5]) ~= "modem" then error("Peripheral on selected side is not a modem.") end
  339.         modem = peripheral.wrap(args[5])
  340.     end
  341.     local handle, delegate = connect(args[2], modem, nil)
  342.     parallel.waitForAny(
  343.         function() while not handle.fs do handle.update(delegate:receive()) end end,
  344.         function() sleep(2) end)
  345.     if not handle.fs then error("Connection failed: Server does not support filesystem transfers") end
  346.     local infile, err = fs.open(args[3], "rb")
  347.     if not infile then error("Could not open remote file: " .. (err or "Unknown error")) end
  348.     local outfile, err = handle.fs.open(args[4] or shell.resolve(fs.getName(args[3])), "wb")
  349.     if not outfile then
  350.         infile.close()
  351.         error("Could not open local file: " .. (err or "Unknown error"))
  352.     end
  353.     outfile.write(infile.readAll())
  354.     infile.close()
  355.     outfile.close()
  356.     handle.close()
  357.     print("Uploaded file as " .. (args[4] or shell.resolve(fs.getName(args[3]))))
  358. elseif (args[1] == "ls" or args[1] == "list") and args[2] then
  359.     local modem
  360.     if args[4] then
  361.         if peripheral.getType(args[5]) ~= "modem" then error("Peripheral on selected side is not a modem.") end
  362.         modem = peripheral.wrap(args[5])
  363.     end
  364.     local handle, delegate = connect(args[2], modem, nil)
  365.     parallel.waitForAny(
  366.         function() while not handle.fs do handle.update(delegate:receive()) end end,
  367.         function() sleep(2) end)
  368.     if not handle.fs then error("Connection failed: Server does not support filesystem transfers") end
  369.     local files = handle.fs.list(args[3] or "/")
  370.     local fileList, dirList = {}, {}
  371.     local showHidden = settings.get("list.show_hidden")
  372.     for _, v in pairs(files) do
  373.         if showHidden or v:sub(1, 1) ~= "." then
  374.             local path = fs.combine(args[3] or "/", v)
  375.             if handle.fs.isDir(path) then dirList[#dirList+1] = v
  376.             else fileList[#fileList+1] = v end
  377.         end
  378.     end
  379.     handle.close()
  380.     table.sort(dirList)
  381.     table.sort(fileList)
  382.     if term.isColor() then textutils.pagedTabulate(colors.green, dirList, colors.white, fileList)
  383.     else textutils.pagedTabulate(colors.lightGray, dirList, colors.white, fileList) end
  384. elseif args[1] == "status" then
  385.     hasRedrun, redrun = pcall(require, "redrun")
  386.     if hasRedrun then
  387.         local id = redrun.getid("rawshell_server")
  388.         if not id then print("Status: Server is not running.")
  389.         else print("Status: Server is running as ID " .. id .. ".") end
  390.     else error("Background task running requires the RedRun library.") end
  391. elseif args[1] == "stop" then
  392.     hasRedrun, redrun = pcall(require, "redrun")
  393.     if hasRedrun then
  394.         local id = redrun.getid("rawshell_server")
  395.         if not id then error("Server is not running.") end
  396.         redrun.terminate(id)
  397.     else error("Background task running requires the RedRun library.") end
  398. else
  399.     term.setTextColor(colors.red)
  400.     textutils.pagedPrint[[
  401. Usage:
  402.     rawshell connect <id> [side]
  403.     rawshell get <id> <remote path> [local path] [side]
  404.     rawshell put <id> <local path> <remote path> [side]
  405.     raswhell ls <id> [remote path]
  406.     rawshell serve [-c <program>] [-m <side>] [-p <password>] [-w <url>] [-b] [-s]
  407.     rawshell status
  408.     rawshell stop
  409. Arguments:
  410.     <id>                The ID of the server to connect to, or a WebSocket URL
  411.     -b                  Run in background (requires RedRun)
  412.     -c <program>        Program to run on connection (defaults to "shell")
  413.     -m <side> / [side]  Use modem attached to the selected side
  414.     -p <password>       Require password to log in
  415.     -r <width>x<height> Set the resolution of the virtual screen
  416.     -s                  Use secure connection (requires ECC)
  417.     -w <url>            Serve to a WebSocket URL instead of over a modem]]
  418.     term.setTextColor(colors.white)
  419. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement