├── decl.d.ts ├── .gitignore ├── README.md ├── tsconfig.json ├── package.json ├── music ├── index.html ├── site.css └── index.js ├── LICENSE ├── index.ejs ├── music.ts ├── server.lua ├── index.ts ├── string_pack.lua └── rawterm.lua /decl.d.ts: -------------------------------------------------------------------------------- 1 | declare module "luamin" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.json 3 | *.code-workspace 4 | build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remote.craftos-pc.cc 2 | WebSocket server to connect VS Code to a ComputerCraft computer. 3 | 4 | ## Using 5 | Install all dependencies with `npm install`. 6 | 7 | Create a file named `config.json` with contents like this: 8 | 9 | ```json 10 | { 11 | "ip": "127.0.0.1", 12 | "port": 4000, 13 | "isSecure": false 14 | } 15 | ``` 16 | 17 | Then run `npm run start`. -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "rootDir": "./", 6 | "outDir": "./build", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "resolveJsonModule": true 10 | }, 11 | "files": [ 12 | "decl.d.ts", 13 | "index.ts", 14 | "music.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rawshell-vscode", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon -e ts,json,ejs,lua index.ts" 9 | }, 10 | "author": "JackMacWindows", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@pm2/io": "^5.0.0", 14 | "ejs": "^3.1.6", 15 | "express": "^4.17.1", 16 | "express-rate-limit": "^6.6.0", 17 | "fluent-ffmpeg": "^2.1.2", 18 | "luamin": "^1.0.4", 19 | "prom-client": "^13.1.0", 20 | "ws": "^7.4.6" 21 | }, 22 | "devDependencies": { 23 | "@types/ejs": "^3.0.6", 24 | "@types/express": "^4.17.12", 25 | "@types/fluent-ffmpeg": "^2.1.20", 26 | "@types/ws": "^7.4.4", 27 | "nodemon": "^2.0.7", 28 | "ts-node": "^10.0.0", 29 | "typescript": "^4.3.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /music/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DFPWM Conversion & File Transfer 7 | 8 | 9 | 10 |
11 |

DFPWM Conversion & File Transfer

12 |

Drop any MP3, OGG, M4A, WAV, AIFF, FLAC, WMA, instantly get a DFPWM link
Files are kept for 10 minutes, then deleted

13 |
14 |

Drop files here (25 MB limit)

15 |
16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 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 | -------------------------------------------------------------------------------- /index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CraftOS-PC/ComputerCraft Remote Connection 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

ComputerCraft VS Code Connection

19 |

Use this page to access a ComputerCraft server from inside Visual Studio Code.

20 |

To use this, you must have the CraftOS-PC extension installed in VS Code. Click here to install it if you haven't.

21 |
    22 |
  1. Paste this into your ComputerCraft computer: wget run <%= url.replace(/^ws/, "http") %>server.lua <%= id %>
  2. 23 |
  3. Click this button to open the computer in VS Code: Connect Now
  4. 24 |
25 |

More information is available on the CraftOS-PC website.

26 | 27 |

NEW! DFPWM converter & temporary file transfer

28 | 29 | 30 | -------------------------------------------------------------------------------- /music/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | font-weight: bold; 10 | color: #666666; 11 | } 12 | 13 | .header { 14 | width: calc(100% - 10px); 15 | height: 80px; 16 | display: flex; 17 | flex-direction: row-reverse; 18 | padding-right: 10px; 19 | align-items: center; 20 | } 21 | 22 | .footer { 23 | width: calc(100% - 10px); 24 | height: 50px; 25 | display: flex; 26 | flex-direction: row; 27 | align-items: center; 28 | padding-left: 10px; 29 | background-color: #dddddd; 30 | } 31 | 32 | .header > *, .footer > * { 33 | padding: 10px; 34 | } 35 | 36 | .main { 37 | width: calc(100% - 40px); 38 | height: calc(100vh - 170px); 39 | display: flex; 40 | flex-direction: column; 41 | padding: 20px; 42 | justify-content: center; 43 | align-items: center; 44 | margin-top: -50px; 45 | margin-bottom: 50px; 46 | } 47 | 48 | #dropped-files-outer { 49 | background-color: #dddddd; 50 | border-style: solid; 51 | border-radius: 10px; 52 | border-width: 2px; 53 | border-color: #999999; 54 | width: 75%; 55 | display: flex; 56 | justify-content: center; 57 | align-items: center; 58 | } 59 | 60 | #dropped-files { 61 | background-color: #dddddd; 62 | border-style: none; 63 | border-radius: 10px; 64 | border-width: 2px; 65 | border-color: #999999; 66 | width: 100%; 67 | border-collapse: collapse; 68 | table-layout: fixed; 69 | } 70 | 71 | #dropped-files td { 72 | border-style: solid; 73 | border-width: 2px; 74 | border-color: #999999; 75 | border-collapse: collapse; 76 | padding: 5px; 77 | height: 32px; 78 | overflow: hidden; 79 | text-overflow: ellipsis; 80 | word-wrap: break-word; 81 | white-space: nowrap; 82 | } 83 | 84 | #dropped-files td:first-child { 85 | border-left: none; 86 | } 87 | 88 | #dropped-files td:last-child { 89 | border-right: none; 90 | } 91 | 92 | #dropped-files tr:first-child td { 93 | border-top: none; 94 | } 95 | 96 | #dropped-files tr:last-child td { 97 | border-bottom: none; 98 | } 99 | 100 | .dropped-file-percent { 101 | text-align: center; 102 | width: 50px !important; 103 | max-width: 50px; 104 | min-width: 50px; 105 | white-space: initial !important; 106 | word-wrap: initial !important; 107 | overflow: initial !important; 108 | } 109 | -------------------------------------------------------------------------------- /music.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import express from "express"; 3 | import rateLimit from "express-rate-limit"; 4 | import ffmpeg from "fluent-ffmpeg"; 5 | import {Readable} from "stream"; 6 | 7 | var cache: {[key: string]: string} = {} 8 | function getCachedFile(name: string) { 9 | if (cache[name] !== undefined) return cache[name]; 10 | let data = fs.readFileSync("music/" + name, {encoding: "utf8"}); 11 | cache[name] = data; 12 | return data; 13 | } 14 | 15 | export = function(url: string, isSecure: boolean): express.Router { 16 | let route = express.Router(); 17 | 18 | route.use(rateLimit({windowMs: 15 * 60 * 1000, max: 40, standardHeaders: true, legacyHeaders: true})); 19 | 20 | route.get('/', (req, res) => {res.set("Content-Type", "text/html"); res.send(getCachedFile("index.html"));}); 21 | route.get('/index.js', (req, res) => {res.set("Content-Type", "application/javascript"); res.send(getCachedFile("index.js").replace(/\$URL_PLACEHOLDER/g, (isSecure ? "https://" : "http://") + req.hostname + url));}); 22 | route.get('/site.css', (req, res) => {res.set("Content-Type", "text/css"); res.send(getCachedFile("site.css"));}); 23 | 24 | route.use('/upload', express.raw({limit: "25MB", inflate: false, type: "*/*"})); 25 | route.post("/upload", (req, res) => { 26 | let binstr = ""; 27 | for (let i = 0; i < 6; i++) binstr += String.fromCharCode(Math.random() * 255); 28 | let id = Buffer.from(binstr, "binary").toString("base64").replace(/\//g, "_"); 29 | ffmpeg(Readable.from(req.body)) 30 | .audioCodec('dfpwm') 31 | .audioChannels(1) 32 | .audioFrequency(48000) 33 | .noVideo() 34 | .outputOptions("-fs 25000000") 35 | .output("content/" + id + ".dfpwm") 36 | .format("dfpwm") 37 | .on('error', (err) => { 38 | res.status(500).json({status: 500, error: err.message}); 39 | res.end(); 40 | }) 41 | .on('end', () => { 42 | setTimeout(() => fs.rmSync("content/" + id + ".dfpwm"), 10 * 60000); 43 | res.status(200).send(id); 44 | }) 45 | .run(); 46 | }); 47 | 48 | route.get("/content/[a-zA-Z0-9+_]{8}.dfpwm", (req, res) => { 49 | res.sendFile(req.url.substr(1), {maxAge: 900000, root: "/var/www/html"}); 50 | }); 51 | 52 | if (fs.existsSync("content")) fs.rmSync("content", {recursive: true}); 53 | fs.mkdirSync("content"); 54 | 55 | setInterval(() => { 56 | for (let p of fs.readdirSync("content")) { 57 | if (p === "." || p === "..") continue; 58 | fs.stat("content/" + p, (e, stat) => { 59 | if (stat === null) return; 60 | if (stat.ctimeMs + 1200000 < Date.now()) fs.rm("content/" + p, () => {}); 61 | }); 62 | } 63 | }, 1200000); 64 | 65 | return route; 66 | } 67 | -------------------------------------------------------------------------------- /music/index.js: -------------------------------------------------------------------------------- 1 | var dropPlaceholder = true; 2 | var url = "$URL_PLACEHOLDER" 3 | 4 | function uploadFile(file) { 5 | let el = document.createElement("tr"); 6 | let nameel = document.createElement("td"); 7 | nameel.className = "dropped-file-name"; 8 | nameel.innerText = file.name; 9 | el.appendChild(nameel); 10 | let linkel = document.createElement("td"); 11 | linkel.className = "dropped-file-link"; 12 | let linka = document.createElement("a"); 13 | linka.href = ""; 14 | linka.innerText = "Uploading..."; 15 | linkel.appendChild(linka); 16 | el.appendChild(linkel); 17 | let percentel = document.createElement("td"); 18 | percentel.className = "dropped-file-percent"; 19 | percentel.innerText = "0%"; 20 | el.appendChild(percentel); 21 | document.getElementById("dropped-files").appendChild(el); 22 | fetch(url + "/upload", { 23 | method: "POST", 24 | headers: {"Content-Type": "application/octet-stream"}, 25 | body: file 26 | }).then(response => { 27 | if (!response.ok) { 28 | percentel.innerText = "Failed"; 29 | linka.remove(); 30 | response.json().then(error => linkel.innerText = error.error); 31 | } else { 32 | response.text().then(id => { 33 | percentel.innerText = "100%"; 34 | linka.href = url + "/content/" + id + ".dfpwm"; 35 | linka.innerText = url + "/content/" + id + ".dfpwm"; 36 | }); 37 | } 38 | }).catch(error => { 39 | percentel.innerText = "Failed"; 40 | linka.remove(); 41 | linkel.innerText = error; 42 | }); 43 | } 44 | 45 | function dropFile(event) { 46 | event.preventDefault(); 47 | dragEnd(event); 48 | if (dropPlaceholder) { 49 | document.getElementById("dropped-files-outer").innerHTML = ""; 50 | let el = document.createElement("table"); 51 | el.id = "dropped-files"; 52 | document.getElementById("dropped-files-outer").appendChild(el); 53 | dropPlaceholder = false; 54 | } 55 | if (event.dataTransfer.items) { 56 | // Use DataTransferItemList interface to access the file(s) 57 | for (var i = 0; i < event.dataTransfer.items.length; i++) { 58 | // If dropped items aren't files, reject them 59 | if (event.dataTransfer.items[i].kind === 'file') { 60 | var file = event.dataTransfer.items[i].getAsFile(); 61 | console.log('... file[' + i + '].name = ' + file.name); 62 | uploadFile(file); 63 | } 64 | } 65 | } else { 66 | // Use DataTransfer interface to access the file(s) 67 | for (var i = 0; i < event.dataTransfer.files.length; i++) { 68 | console.log('... file[' + i + '].name = ' + event.dataTransfer.files[i].name); 69 | uploadFile(event.dataTransfer.files[i]); 70 | } 71 | } 72 | } 73 | 74 | function dropFileButton(event) { 75 | event.preventDefault(); 76 | dragEnd(event); 77 | if (dropPlaceholder) { 78 | document.getElementById("dropped-files-outer").innerHTML = ""; 79 | let el = document.createElement("table"); 80 | el.id = "dropped-files"; 81 | document.getElementById("dropped-files-outer").appendChild(el); 82 | dropPlaceholder = false; 83 | } 84 | let dataTransfer = document.getElementById("drop-files-button"); 85 | for (var i = 0; i < dataTransfer.files.length; i++) { 86 | console.log('... file[' + i + '].name = ' + dataTransfer.files[i].name); 87 | uploadFile(dataTransfer.files[i]); 88 | } 89 | dataTransfer.value = ""; 90 | } 91 | 92 | function dragOver(event) { 93 | event.preventDefault(); 94 | document.getElementById("dropped-files-outer").style.borderColor = "#00aaff" 95 | } 96 | 97 | function dragEnd(event) { 98 | document.getElementById("dropped-files-outer").style.borderColor = "#999999" 99 | } -------------------------------------------------------------------------------- /server.lua: -------------------------------------------------------------------------------- 1 | local function minver(version) 2 | local res 3 | if _CC_VERSION then res = version <= _CC_VERSION 4 | elseif not _HOST then res = version <= os.version():gsub("CraftOS ", "") 5 | elseif _HOST:match("ComputerCraft 1%.1%d+") ~= version:match("1%.1%d+") then 6 | version = version:gsub("(1%.)([02-9])", "%10%2") 7 | local host = _HOST:gsub("(ComputerCraft 1%.)([02-9])", "%10%2") 8 | res = version <= host:match("ComputerCraft ([0-9%.]+)") 9 | else res = version <= _HOST:match("ComputerCraft ([0-9%.]+)") end 10 | assert(res, "This program requires ComputerCraft " .. version .. " or later.") 11 | end 12 | minver "1.85.0" 13 | local url = "${URL}" 14 | if not string.pack then 15 | if not fs.exists("string_pack.lua") then 16 | print("Downloading string.pack polyfill...") 17 | local handle, err = http.get(url:gsub("^ws", "http") .. "string_pack.lua") 18 | if not handle then error("Could not download string.pack polyfill: " .. err) end 19 | local file, err = fs.open("string_pack.lua", "w") 20 | if not file then handle.close() error("Could not open string_pack.lua for writing: " .. err) end 21 | file.write(handle.readAll()) 22 | handle.close() 23 | file.close() 24 | end 25 | local sp = dofile "string_pack.lua" 26 | for k,v in pairs(sp) do string[k] = v end 27 | end 28 | local rawterm 29 | if not fs.exists("rawterm.lua") or fs.getSize("rawterm.lua") ~= [[${SIZE}]] then 30 | print("Downloading rawterm API...") 31 | local handle, err = http.get(url:gsub("^ws", "http") .. "rawterm.lua") 32 | if not handle then error("Could not download rawterm API: " .. err) end 33 | local data = handle.readAll() 34 | handle.close() 35 | if fs.getFreeSpace("/") >= #data + 4096 then 36 | local file, err = fs.open("rawterm.lua", "w") 37 | if not file then error("Could not open rawterm.lua for writing: " .. err) end 38 | file.write(data) 39 | file.close() 40 | else rawterm = assert(load(data, "@rawterm.lua", "t"))() end 41 | end 42 | rawterm = rawterm or dofile "rawterm.lua" 43 | 44 | local arg, cmd = ... 45 | print("Connecting to " .. url .. "...") 46 | local conn, err = rawterm.wsDelegate(url .. arg, {["X-Rawterm-Is-Server"] = "Yes"}) 47 | if not conn then error("Could not connect to server: " .. err) end 48 | local oldClose, oldReceive, oldSend = conn.close, conn.receive, conn.send 49 | local isOpen = true 50 | function conn:close() isOpen = false return oldClose(self) end 51 | function conn:receive(...) 52 | if not isOpen then return nil end 53 | local buf, res, size = "" 54 | repeat 55 | repeat res = table.pack(pcall(oldReceive, self, ...)) 56 | until not (not res[1] and res[2]:match("Terminated$")) 57 | if not res[1] then error(res[2]) 58 | elseif not res[2] then return nil end 59 | if not size then size = tonumber(res[2]:match "!CPC(%x%x%x%x)" or res[2]:match("!CPD(" .. ("%x"):rep(12) .. ")") or "", 16) end 60 | if size then buf = buf .. res[2]:gsub("\n", "") end 61 | until size and #buf >= size + 16 + (buf:match "^!CPD" and 8 or 0) 62 | return buf .. "\n" 63 | end 64 | function conn:send(data) if isOpen then for i = 1, #data, 65530 do oldSend(self, data:sub(i, math.min(i + 65529, #data))) end end end 65 | local w, h = term.getSize() 66 | local win = rawterm.server(conn, w, h, 0, "ComputerCraft Remote Terminal: " .. (os.computerLabel() or ("Computer " .. os.computerID())), term.current()) 67 | win.setVisible(false) 68 | local monitors, ids = {}, {[0] = true} 69 | local oldcall = peripheral.call 70 | for i, v in ipairs{peripheral.find "monitor"} do 71 | local mw, mh = v.getSize() 72 | local name = peripheral.getName(v) 73 | local methods = peripheral.getMethods(name) 74 | local p = {} 75 | for _, v in ipairs(methods) do p[v] = function(...) return oldcall(name, v, ...) end end 76 | monitors[name] = {id = i, win = rawterm.server(conn, mw, mh, i, "ComputerCraft Remote Terminal: Monitor " .. name, p, nil, nil, nil, true)} 77 | monitors[name].win.setVisible(false) 78 | ids[i] = true 79 | end 80 | function peripheral.call(side, method, ...) 81 | if monitors[side] then return monitors[side].win[method](...) 82 | else return oldcall(side, method, ...) end 83 | end 84 | local oldterm = term.redirect(win) 85 | local ok, tm 86 | ok, err = pcall(parallel.waitForAny, function() 87 | local coro = coroutine.create(shell.run) 88 | local ok, filter = coroutine.resume(coro, cmd or (settings.get("bios.use_multishell") and "multishell" or "shell")) 89 | while ok and coroutine.status(coro) == "suspended" do 90 | local ev = {} 91 | local pullers = {function() ev = table.pack(win.pullEvent(filter, true, true)) end} 92 | for k, v in pairs(monitors) do pullers[#pullers+1] = function() 93 | ev = table.pack(v.win.pullEvent(filter, true, true)) 94 | if ev[1] == "mouse_click" then ev = {"monitor_touch", k, ev[3], ev[4]} 95 | elseif ev[1] == "mouse_up" or ev[1] == "mouse_drag" or ev[1] == "mouse_scroll" or ev[1] == "mouse_move" then ev = {} end 96 | end end 97 | pullers[#pullers+1] = function() 98 | repeat ev = table.pack(os.pullEventRaw(filter)) until not (ev[1] == "websocket_message" and ev[2] == url .. arg) and not (ev[1] == "timer" and ev[2] == tm) 99 | end 100 | parallel.waitForAny(table.unpack(pullers)) 101 | if ev[1] then ok, filter = coroutine.resume(coro, table.unpack(ev, 1, ev.n)) end 102 | end 103 | if not ok then err = filter end 104 | end, function() 105 | while isOpen do 106 | win.setVisible(true) 107 | win.setVisible(false) 108 | for _, v in pairs(monitors) do 109 | v.win.setVisible(true) 110 | v.win.setVisible(false) 111 | end 112 | tm = os.startTimer(0.05) 113 | repeat local ev, p = os.pullEventRaw("timer") until p == tm 114 | end 115 | end, function() 116 | while true do 117 | local ev, side = os.pullEventRaw() 118 | if ev == "peripheral" and peripheral.getType(side) == "monitor" and not monitors[side] then 119 | local id = #ids + 1 120 | local mw, mh = oldcall(side, "getSize") 121 | local methods = peripheral.getMethods(side) 122 | for _, v in ipairs(methods) do methods[v] = true end 123 | local p = setmetatable({}, {__index = function(_, idx) if methods[idx] then return function(...) return oldcall(side, idx, ...) end end end}) 124 | monitors[side] = {id = id, win = rawterm.server(conn, mw, mh, id, "ComputerCraft Remote Terminal: Monitor " .. side, p, nil, nil, nil, true)} 125 | monitors[side].win.setVisible(false) 126 | ids[id] = true 127 | elseif ev == "peripheral_detach" and monitors[side] then 128 | monitors[side].win.close(true) 129 | ids[monitors[side].id] = nil 130 | monitors[side] = nil 131 | elseif ev == "term_resize" then 132 | win.reposition(nil, nil, term.getSize()) 133 | elseif ev == "monitor_resize" and monitors[side] then 134 | monitors[side].win.reposition(nil, nil, oldcall(side, "getSize")) 135 | elseif ev == "websocket_closed" and side == url .. arg then 136 | isOpen = false 137 | end 138 | end 139 | end) 140 | term.redirect(oldterm) 141 | for _, v in pairs(monitors) do v.win.close(true) end 142 | win.close() 143 | peripheral.call = oldcall 144 | shell.run("clear") 145 | if type(err) == "string" and not err:match("attempt to use closed file") then printError(err) end 146 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import WebSocket from "ws"; 3 | import fs from "fs/promises"; 4 | import {readFileSync} from "fs"; 5 | import ejs from "ejs"; 6 | import http from "http"; 7 | import https from "https"; 8 | import tls from "tls"; 9 | import net from "net"; 10 | import pmx from "@pm2/io"; 11 | import luamin from "luamin"; 12 | import { collectDefaultMetrics, register, Gauge } from "prom-client"; 13 | import { ip, port, isSecure } from "./config.json"; 14 | import music from "./music"; 15 | 16 | var crcTable: [number]; 17 | 18 | function makeCRCTable() { 19 | let c; 20 | let crcTable = []; 21 | for (let n = 0; n < 256; n++) { 22 | c = n; 23 | for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; 24 | crcTable[n] = c; 25 | } 26 | return crcTable; 27 | } 28 | 29 | function crc32(str: string) { 30 | crcTable = crcTable || makeCRCTable(); 31 | let crc = 0 ^ (-1); 32 | for (let i = 0; i < str.length; i++) { 33 | crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF]; 34 | } 35 | return (crc ^ (-1)) >>> 0; 36 | }; 37 | 38 | let connectionPools: {[key: string]: WebSocket[]} = {}; 39 | let connectionPoolOpen: {[key: string]: number} = {}; 40 | 41 | const app = express(); 42 | const serverURL = `ws${isSecure ? "s" : ""}://${ip}:${port}/`; 43 | 44 | let serverFile: string; 45 | let rawtermFile: string; 46 | let stringPackFile: string; 47 | let indexFile: string; 48 | fs.readFile("server.lua", {encoding: "utf8"}).then(data => serverFile = luamin.minify(data)); 49 | fs.readFile("rawterm.lua", {encoding: "utf8"}).then(data => rawtermFile = luamin.minify(data)); 50 | fs.readFile("string_pack.lua", {encoding: "utf8"}).then(data => stringPackFile = luamin.minify(data)); 51 | fs.readFile("index.ejs", {encoding: "utf8"}).then(data => indexFile = data); 52 | let ctx: tls.SecureContext | undefined; 53 | if (isSecure) { 54 | ctx = tls.createSecureContext({ 55 | key: readFileSync(`/etc/letsencrypt/live/remote.craftos-pc.cc/privkey.pem`), 56 | cert: readFileSync(`/etc/letsencrypt/live/remote.craftos-pc.cc/fullchain.pem`) 57 | }); 58 | } 59 | 60 | collectDefaultMetrics(); 61 | let connectionCounter = new Gauge({name: "http_active_connections", help: "Active Connections"}); 62 | let pipeCounter = new Gauge({name: "http_active_pipes", help: "Active Pipes"}); 63 | 64 | let awaitingRestart = false; 65 | let pipeCount = 0; 66 | 67 | pmx.action("schedule-restart", (reply: (res: any) => void) => { 68 | if (pipeCount === 0) { 69 | reply("Exiting NOW!"); 70 | process.exit(0); 71 | } else { 72 | awaitingRestart = true; 73 | reply("Restart scheduled - waiting for all pipes to close."); 74 | } 75 | }); 76 | 77 | pmx.action("send-message", (param: string, reply: (res: any) => void) => { 78 | if (typeof param === "function") { 79 | (param as (res: any) => void)("Missing parameter"); 80 | return; 81 | } 82 | const buf = Buffer.alloc(27 + param.length); 83 | buf.fill(0); 84 | buf[0] = 5; 85 | buf[2] = 0x40; 86 | buf.write("Message from server", 6); 87 | buf.write(param, 26); 88 | const b64 = buf.toString("base64"); 89 | // Send one packet with binary encoding and one packet without to make sure both get the packet 90 | // (We can't tell what the connections are using from here) 91 | const packet = "!CPC" + ("000" + b64.length.toString(16)).slice(-4) + b64 + ("0000000" + crc32(b64).toString(16)).slice(-8) + "\n"; 92 | const bpacket = "!CPC" + ("000" + b64.length.toString(16)).slice(-4) + b64 + ("0000000" + crc32(buf.toString("binary")).toString(16)).slice(-8) + "\n"; 93 | for (let x in connectionPools) { 94 | for (let socket of connectionPools[x]) { 95 | socket.send(packet); 96 | socket.send(bpacket); 97 | } 98 | } 99 | reply("Message sent."); 100 | }); 101 | 102 | pmx.action("reload", (reply: (res: any) => void) => { 103 | fs.readFile("server.lua", {encoding: "utf8"}).then(data => serverFile = luamin.minify(data)); 104 | fs.readFile("rawterm.lua", {encoding: "utf8"}).then(data => rawtermFile = luamin.minify(data)); 105 | fs.readFile("string_pack.lua", {encoding: "utf8"}).then(data => stringPackFile = luamin.minify(data)); 106 | fs.readFile("index.ejs", {encoding: "utf8"}).then(data => indexFile = data); 107 | if (isSecure) { 108 | ctx = tls.createSecureContext({ 109 | key: readFileSync(`/etc/letsencrypt/live/remote.craftos-pc.cc/privkey.pem`), 110 | cert: readFileSync(`/etc/letsencrypt/live/remote.craftos-pc.cc/fullchain.pem`) 111 | }); 112 | } 113 | reply("Reloaded all files."); 114 | }); 115 | 116 | function makeID(): string { 117 | const buf = Buffer.alloc(30); 118 | for (let i = 0; i < 30; i++) buf[i] = Math.floor(Math.random() * 255); 119 | return buf.toString("base64").replace(/\//g, "_").replace(/\+/g, "-"); 120 | } 121 | 122 | function getURL(req: http.IncomingMessage) { 123 | return req.headers.host ? `ws${isSecure ? "s" : ""}://${req.headers.host}/` : serverURL; 124 | } 125 | 126 | app.get("/", (req, res) => { 127 | const id = makeID(); 128 | const url = getURL(req); 129 | res.send(ejs.render(indexFile, {url: url, id: id})); 130 | }); 131 | 132 | app.get("/new", (req, res) => { 133 | res.send(makeID()); 134 | }); 135 | 136 | app.get("/server.lua", (req, res) => { 137 | const url = getURL(req); 138 | res.contentType(".lua"); 139 | res.send(serverFile.replace("${URL}", url).replace("[[${SIZE}]]", `(${rawtermFile.length})`)); 140 | }); 141 | 142 | app.get("/rawterm.lua", (req, res) => { 143 | res.contentType(".lua"); 144 | res.send(rawtermFile); 145 | }); 146 | 147 | app.get("/string_pack.lua", (req, res) => { 148 | res.contentType(".lua"); 149 | res.send(stringPackFile); 150 | }); 151 | 152 | app.get("/.well-known/acme-challenge/:file", (req, res) => { 153 | fs.readFile(".well-known/acme-challenge/" + req.params.file) 154 | .then(data => res.send(data)) 155 | .catch(() => res.status(404).send("404 Not Found")); 156 | }); 157 | 158 | app.use("/music", music("/music", isSecure)); 159 | app.get("/music", (req, res) => res.redirect(301, "/music/")); 160 | 161 | const wsServer = new WebSocket.Server({ noServer: true }); 162 | wsServer.on('connection', (socket, request) => { 163 | const url: string = request.url!; 164 | const isServer = request.headers["X-Rawterm-Is-Server"] === "Yes"; 165 | if (connectionPools[url] === undefined) { 166 | connectionPools[url] = []; 167 | connectionPoolOpen[url] = Date.now(); 168 | pipeCounter.inc(); 169 | pipeCount++; 170 | } 171 | connectionPools[url].push(socket); 172 | connectionCounter.inc(); 173 | socket.on('message', message => { 174 | //console.log(message); 175 | for (let s of connectionPools[url]) if (s !== socket) s.send(message); 176 | }); 177 | socket.on('close', () => { 178 | connectionPools[url].splice(connectionPools[url].indexOf(socket), 1); 179 | if (isServer) for (let s of connectionPools[url]) s.close(); 180 | if (connectionPools[url].length === 0) { 181 | delete connectionPools[url]; 182 | delete connectionPoolOpen[url]; 183 | pipeCounter.dec(); 184 | if (--pipeCount === 0 && awaitingRestart) process.exit(0); 185 | } 186 | connectionCounter.dec(); 187 | }); 188 | }); 189 | 190 | // Clean up any pipes that only have one connection every few minutes 191 | setInterval(() => { 192 | const now = Date.now(); 193 | for (let x in connectionPools) 194 | if (connectionPools[x].length === 1 && now - connectionPoolOpen[x] > 60000) 195 | connectionPools[x][0].close(); 196 | }, 10 * 60 * 1000); 197 | 198 | const server = isSecure ? https.createServer({SNICallback: (servername, cb) => cb(null, ctx!)}, app).listen(443, ip) : app.listen(port, ip); 199 | server.on('upgrade', (request: http.IncomingMessage, socket: net.Socket, head: Buffer) => { 200 | wsServer.handleUpgrade(request, socket, head, socket => { 201 | wsServer.emit('connection', socket, request); 202 | }); 203 | }); 204 | 205 | const app2 = express(); 206 | app2.get("/metrics", async (_req, res) => { 207 | try { 208 | res.set('Content-Type', register.contentType); 209 | res.end(await register.metrics()); 210 | } catch (err) { 211 | res.status(500).end(err); 212 | } 213 | }); 214 | app2.listen(9991, "0.0.0.0"); 215 | 216 | if (isSecure) { 217 | // https://stackoverflow.com/a/7458587/2032154 218 | const httpApp = express(); 219 | httpApp.get('*', function(req, res) { 220 | const url = getURL(req); 221 | res.redirect(url.replace(/^ws/, "http").replace(/\/$/, "") + req.url); 222 | }) 223 | const httpServer = httpApp.listen(port, ip); 224 | httpServer.on('upgrade', (request: http.IncomingMessage, socket: net.Socket, head: Buffer) => { 225 | const url = getURL(request); 226 | socket.write(request.httpVersion + " 301 Moved Permanently\r\nLocation: " + url.replace(/\/$/, "") + request.url + "\r\nContent-Length: 0\r\n\r\n"); 227 | socket.end(); 228 | }); 229 | server.on("listening", () => console.log("Running! (insecure)")); 230 | } 231 | 232 | server.on("listening", () => console.log("Running!")); 233 | -------------------------------------------------------------------------------- /string_pack.lua: -------------------------------------------------------------------------------- 1 | --local expect = require "cc.expect".expect 2 | local expect = dofile "/rom/modules/main/cc/expect.lua".expect 3 | 4 | local ByteOrder = {BIG_ENDIAN = 1, LITTLE_ENDIAN = 2} 5 | local isint = {b = 1, B = 1, h = 1, H = 1, l = 1, L = 1, j = 1, J = 1, T = 1} 6 | local packoptsize_tbl = {b = 1, B = 1, x = 1, h = 2, H = 2, f = 4, j = 4, J = 4, l = 8, L = 8, T = 8, d = 8, n = 8} 7 | 8 | local function round(n) if n % 1 >= 0.5 then return math.ceil(n) else return math.floor(n) end end 9 | 10 | local function floatToRawIntBits(f) 11 | if f == 0 then return 0 12 | elseif f == -0 then return 0x80000000 13 | elseif f == math.huge then return 0x7F800000 14 | elseif f == -math.huge then return 0xFF800000 end 15 | local m, e = math.frexp(f) 16 | if e > 127 or e < -126 then error("number out of range", 3) end 17 | e, m = e + 126, round((math.abs(m) - 0.5) * 0x1000000) 18 | if m > 0x7FFFFF then e = e + 1 end 19 | return bit32.bor(f < 0 and 0x80000000 or 0, bit32.lshift(bit32.band(e, 0xFF), 23), bit32.band(m, 0x7FFFFF)) 20 | end 21 | 22 | local function doubleToRawLongBits(f) 23 | if f == 0 then return 0, 0 24 | elseif f == -0 then return 0x80000000, 0 25 | elseif f == math.huge then return 0x7FF00000, 0 26 | elseif f == -math.huge then return 0xFFF00000, 0 end 27 | local m, e = math.frexp(f) 28 | if e > 1023 or e < -1022 then error("number out of range", 3) end 29 | e, m = e + 1022, round((math.abs(m) - 0.5) * 0x20000000000000) 30 | if m > 0xFFFFFFFFFFFFF then e = e + 1 end 31 | return bit32.bor(f < 0 and 0x80000000 or 0, bit32.lshift(bit32.band(e, 0x7FF), 20), bit32.band(m / 0x100000000, 0xFFFFF)), bit32.band(m, 0xFFFFFFFF) 32 | end 33 | 34 | local function intBitsToFloat(l) 35 | if l == 0 then return 0 36 | elseif l == 0x80000000 then return -0 37 | elseif l == 0x7F800000 then return math.huge 38 | elseif l == 0xFF800000 then return -math.huge end 39 | local m, e = bit32.band(l, 0x7FFFFF), bit32.band(bit32.rshift(l, 23), 0xFF) 40 | e, m = e - 126, m / 0x1000000 + 0.5 41 | local n = math.ldexp(m, e) 42 | return bit32.btest(l, 0x80000000) and -n or n 43 | end 44 | 45 | local function longBitsToDouble(lh, ll) 46 | if lh == 0 and ll == 0 then return 0 47 | elseif lh == 0x80000000 and ll == 0 then return -0 48 | elseif lh == 0x7FF00000 and ll == 0 then return math.huge 49 | elseif lh == 0xFFF00000 and ll == 0 then return -math.huge end 50 | local m, e = bit32.band(lh, 0xFFFFF) * 0x100000000 + bit32.band(ll, 0xFFFFFFFF), bit32.band(bit32.rshift(lh, 20), 0x7FF) 51 | e, m = e - 1022, m / 0x20000000000000 + 0.5 52 | local n = math.ldexp(m, e) 53 | return bit32.btest(lh, 0x80000000) and -n or n 54 | end 55 | 56 | local function packint(num, size, output, offset, alignment, endianness, signed) 57 | local total_size = 0 58 | if offset % math.min(size, alignment) ~= 0 and alignment > 1 then 59 | local i = 0 60 | while offset % math.min(size, alignment) ~= 0 and i < alignment do 61 | output[offset] = 0 62 | offset = offset + 1 63 | total_size = total_size + 1 64 | i = i + 1 65 | end 66 | end 67 | if endianness == ByteOrder.BIG_ENDIAN then 68 | local added_padding = 0 69 | if size > 8 then for i = 0, size - 9 do 70 | output[offset + i] = (signed and num >= 2^(size * 8 - 1) ~= 0) and 0xFF or 0 71 | added_padding = added_padding + 1 72 | total_size = total_size + 1 73 | end end 74 | for i = added_padding, size - 1 do 75 | output[offset + i] = bit32.band(bit32.rshift(num, ((size - i - 1) * 8)), 0xFF) 76 | total_size = total_size + 1 77 | end 78 | else 79 | for i = 0, math.min(size, 8) - 1 do 80 | output[offset + i] = num / 2^(i * 8) % 256 81 | total_size = total_size + 1 82 | end 83 | for i = 8, size - 1 do 84 | output[offset + i] = (signed and num >= 2^(size * 8 - 1) ~= 0) and 0xFF or 0 85 | total_size = total_size + 1 86 | end 87 | end 88 | return total_size 89 | end 90 | 91 | local function unpackint(str, offset, size, endianness, alignment, signed) 92 | local result, rsize = 0, 0 93 | if offset % math.min(size, alignment) ~= 0 and alignment > 1 then 94 | for i = 0, alignment - 1 do 95 | if offset % math.min(size, alignment) == 0 then break end 96 | offset = offset + 1 97 | rsize = rsize + 1 98 | end 99 | end 100 | for i = 0, size - 1 do 101 | result = result + str:byte(offset + i) * 2^((endianness == ByteOrder.BIG_ENDIAN and size - i - 1 or i) * 8) 102 | rsize = rsize + 1 103 | end 104 | if (signed and result >= 2^(size * 8 - 1)) then result = result - 2^(size * 8) end 105 | return result, rsize 106 | end 107 | 108 | local function packoptsize(opt, alignment) 109 | local retval = packoptsize_tbl[opt] or 0 110 | if (alignment > 1 and retval % alignment ~= 0) then retval = retval + (alignment - (retval % alignment)) end 111 | return retval 112 | end 113 | 114 | --[[ 115 | * string.pack (fmt, v1, v2, ...) 116 | * 117 | * Returns a binary string containing the values v1, v2, etc. 118 | * serialized in binary form (packed) according to the format string fmt. 119 | ]] 120 | local function pack(...) 121 | local fmt = expect(1, ..., "string") 122 | local endianness = ByteOrder.LITTLE_ENDIAN 123 | local alignment = 1 124 | local pos = 1 125 | local argnum = 2 126 | local output = {} 127 | local i = 1 128 | while i <= #fmt do 129 | local c = fmt:sub(i, i) 130 | i = i + 1 131 | if c == '=' or c == '<' then 132 | endianness = ByteOrder.LITTLE_ENDIAN 133 | elseif c == '>' then 134 | endianness = ByteOrder.BIG_ENDIAN 135 | elseif c == '!' then 136 | local size = -1 137 | while (i <= #fmt and fmt:sub(i, i):match("%d")) do 138 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 139 | size = (math.max(size, 0) * 10) + tonumber(fmt:sub(i, i)) 140 | i = i + 1 141 | end 142 | if (size > 16 or size == 0) then error(string.format("integral size (%d) out of limits [1,16]", size), 2) 143 | elseif (size == -1) then alignment = 4 144 | else alignment = size end 145 | elseif isint[c] then 146 | local num = expect(argnum, select(argnum, ...), "number") 147 | argnum = argnum + 1 148 | if (num >= math.pow(2, (packoptsize(c, 0) * 8 - (c:match("%l") and 1 or 0))) or 149 | num < (c:match("%l") and -math.pow(2, (packoptsize(c, 0) * 8 - 1)) or 0)) then 150 | error(string.format("bad argument #%d to 'pack' (integer overflow)", argnum - 1), 2) 151 | end 152 | pos = pos + packint(num, packoptsize(c, 0), output, pos, alignment, endianness, false) 153 | elseif c:lower() == 'i' then 154 | local signed = c == 'i' 155 | local size = -1 156 | while i <= #fmt and fmt:sub(i, i):match("%d") do 157 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 158 | size = (math.max(size, 0) * 10) + tonumber(fmt:sub(i, i)) 159 | i = i + 1 160 | end 161 | if (size > 16 or size == 0) then 162 | error(string.format("integral size (%d) out of limits [1,16]", size), 2) 163 | elseif (alignment > 1 and (size ~= 1 and size ~= 2 and size ~= 4 and size ~= 8 and size ~= 16)) then 164 | error("bad argument #1 to 'pack' (format asks for alignment not power of 2)", 2) 165 | elseif (size == -1) then size = 4 end 166 | local num = expect(argnum, select(argnum, ...), "number") 167 | argnum = argnum + 1 168 | if (num >= math.pow(2, (size * 8 - (c:match("%l") and 1 or 0))) or 169 | num < (c:match("%l") and -math.pow(2, (size * 8 - 1)) or 0)) then 170 | error(string.format("bad argument #%d to 'pack' (integer overflow)", argnum - 1), 2) 171 | end 172 | pos = pos + packint(num, size, output, pos, alignment, endianness, signed) 173 | elseif c == 'f' then 174 | local f = expect(argnum, select(argnum, ...), "number") 175 | argnum = argnum + 1 176 | local l = floatToRawIntBits(f) 177 | if (pos % math.min(4, alignment) ~= 0 and alignment > 1) then 178 | for j = 0, alignment - 1 do 179 | if pos % math.min(4, alignment) == 0 then break end 180 | output[pos] = 0 181 | pos = pos + 1 182 | end 183 | end 184 | for j = 0, 3 do output[pos + (endianness == ByteOrder.BIG_ENDIAN and 3 - j or j)] = bit32.band(bit32.rshift(l, (j * 8)), 0xFF) end 185 | pos = pos + 4 186 | elseif c == 'd' or c == 'n' then 187 | local f = expect(argnum, select(argnum, ...), "number") 188 | argnum = argnum + 1 189 | local lh, ll = doubleToRawLongBits(f) 190 | if (pos % math.min(8, alignment) ~= 0 and alignment > 1) then 191 | for j = 0, alignment - 1 do 192 | if pos % math.min(8, alignment) == 0 then break end 193 | output[pos] = 0 194 | pos = pos + 1 195 | end 196 | end 197 | for j = 0, 3 do output[pos + (endianness == ByteOrder.BIG_ENDIAN and 7 - j or j)] = bit32.band(bit32.rshift(ll, (j * 8)), 0xFF) end 198 | for j = 4, 7 do output[pos + (endianness == ByteOrder.BIG_ENDIAN and 7 - j or j)] = bit32.band(bit32.rshift(lh, ((j - 4) * 8)), 0xFF) end 199 | pos = pos + 8 200 | elseif c == 'c' then 201 | local size = 0 202 | if (i > #fmt or not fmt:sub(i, i):match("%d")) then 203 | error("missing size for format option 'c'", 2) 204 | end 205 | while (i <= #fmt and fmt:sub(i, i):match("%d")) do 206 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 207 | size = (size * 10) + tonumber(fmt:sub(i, i)) 208 | i = i + 1 209 | end 210 | if (pos + size < pos or pos + size > 0xFFFFFFFF) then error("bad argument #1 to 'pack' (format result too large)", 2) end 211 | local str = expect(argnum, select(argnum, ...), "string") 212 | argnum = argnum + 1 213 | if (#str > size) then error(string.format("bad argument #%d to 'pack' (string longer than given size)", argnum - 1), 2) end 214 | if size > 0 then 215 | for j = 0, size - 1 do output[pos+j] = str:byte(j + 1) or 0 end 216 | pos = pos + size 217 | end 218 | elseif c == 'z' then 219 | local str = expect(argnum, select(argnum, ...), "string") 220 | argnum = argnum + 1 221 | for b in str:gmatch "." do if (b == '\0') then error(string.format("bad argument #%d to 'pack' (string contains zeros)", argnum - 1), 2) end end 222 | for j = 0, #str - 1 do output[pos+j] = str:byte(j + 1) end 223 | output[pos + #str] = 0 224 | pos = pos + #str + 1 225 | elseif c == 's' then 226 | local size = 0 227 | while (i <= #fmt and fmt:sub(i, i):match("%d")) do 228 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 229 | size = (size * 10) + tonumber(fmt:sub(i, i)) 230 | i = i + 1 231 | end 232 | if (size > 16) then 233 | error(string.format("integral size (%d) out of limits [1,16]", size), 2) 234 | elseif (size == 0) then size = 4 end 235 | local str = expect(argnum, select(argnum, ...), "string") 236 | argnum = argnum + 1 237 | if (#str >= math.pow(2, (size * 8))) then 238 | error(string.format("bad argument #%d to 'pack' (string length does not fit in given size)", argnum - 1), 2) 239 | end 240 | packint(#str, size, output, pos, 1, endianness, false) 241 | for j = size, #str + size - 1 do output[pos+j] = str:byte(j - size + 1) or 0 end 242 | pos = pos + #str + size 243 | elseif c == 'x' then 244 | output[pos] = 0 245 | pos = pos + 1 246 | elseif c == 'X' then 247 | if (i >= #fmt) then error("invalid next option for option 'X'", 2) end 248 | local size = 0 249 | local c = fmt:sub(i, i) 250 | i = i + 1 251 | if c:lower() == 'i' then 252 | while i <= #fmt and fmt:sub(i, i):match("%d") do 253 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 254 | size = (size * 10) + tonumber(fmt:sub(i, i)) 255 | i = i + 1 256 | end 257 | if (size > 16 or size == 0) then 258 | error(string.format("integral size (%d) out of limits [1,16]", size), 2) 259 | end 260 | else size = packoptsize(c, 0) end 261 | if (size < 1) then error("invalid next option for option 'X'", 2) end 262 | if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then 263 | for j = 1, alignment do 264 | if pos % math.min(size, alignment) == 0 then break end 265 | output[pos] = 0 266 | pos = pos + 1 267 | end 268 | end 269 | elseif c ~= ' ' then error(string.format("invalid format option '%s'", c), 2) end 270 | end 271 | return string.char(table.unpack(output)) 272 | end 273 | 274 | --[[ 275 | * string.packsize (fmt) 276 | * 277 | * Returns the size of a string resulting from string.pack with the given format. 278 | * The format string cannot have the variable-length options 's' or 'z'. 279 | ]] 280 | local function packsize(fmt) 281 | local pos = 0 282 | local alignment = 1 283 | local i = 1 284 | while i <= #fmt do 285 | local c = fmt:sub(i, i) 286 | i = i + 1 287 | if c == '!' then 288 | local size = 0 289 | while i <= #fmt and fmt:sub(i, i):match("%d") do 290 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 291 | size = (size * 10) + tonumber(fmt:sub(i, i)) 292 | i = i + 1 293 | end 294 | if (size > 16) then error(string.format("integral size (%d) out of limits [1,16]", size), 2) 295 | elseif (size == 0) then alignment = 4 296 | else alignment = size end 297 | elseif isint[c] then 298 | local size = packoptsize(c, 0) 299 | if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then 300 | for j = 1, alignment do 301 | if pos % math.min(size, alignment) == 0 then break end 302 | pos = pos + 1 303 | end 304 | end 305 | pos = pos + size 306 | elseif c:lower() == 'i' then 307 | local size = 0 308 | while i <= #fmt and fmt:sub(i, i):match("%d") do 309 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 310 | size = (size * 10) + tonumber(fmt:sub(i, i)) 311 | i = i + 1 312 | end 313 | if (size > 16) then 314 | error(string.format("integral size (%d) out of limits [1,16]", size)) 315 | elseif (alignment > 1 and (size ~= 1 and size ~= 2 and size ~= 4 and size ~= 8 and size ~= 16)) then 316 | error("bad argument #1 to 'pack' (format asks for alignment not power of 2)", 2) 317 | elseif (size == 0) then size = 4 end 318 | if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then 319 | for j = 1, alignment do 320 | if pos % math.min(size, alignment) == 0 then break end 321 | pos = pos + 1 322 | end 323 | end 324 | pos = pos + size 325 | elseif c == 'f' then 326 | if (pos % math.min(4, alignment) ~= 0 and alignment > 1) then 327 | for j = 1, alignment do 328 | if pos % math.min(4, alignment) == 0 then break end 329 | pos = pos + 1 330 | end 331 | end 332 | pos = pos + 4 333 | elseif c == 'd' or c == 'n' then 334 | if (pos % math.min(8, alignment) ~= 0 and alignment > 1) then 335 | for j = 1, alignment do 336 | if pos % math.min(8, alignment) == 0 then break end 337 | pos = pos + 1 338 | end 339 | end 340 | pos = pos + 8 341 | elseif c == 'c' then 342 | local size = 0 343 | if (i > #fmt or not fmt:sub(i, i):match("%d")) then 344 | error("missing size for format option 'c'", 2) 345 | end 346 | while i <= #fmt and fmt:sub(i, i):match("%d") do 347 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 348 | size = (size * 10) + tonumber(fmt:sub(i, i)) 349 | i = i + 1 350 | end 351 | if (pos + size < pos or pos + size > 0x7FFFFFFF) then error("bad argument #1 to 'packsize' (format result too large)", 2) end 352 | pos = pos + size 353 | elseif c == 'x' then 354 | pos = pos + 1 355 | elseif c == 'X' then 356 | if (i >= #fmt) then error("invalid next option for option 'X'", 2) end 357 | local size = 0 358 | local c = fmt:sub(i, i) 359 | i = i + 1 360 | if c:lower() == 'i' then 361 | while i <= #fmt and fmt:sub(i, i):match("%d") do 362 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 363 | size = (size * 10) + tonumber(fmt:sub(i, i)) 364 | i = i + 1 365 | end 366 | if (size > 16 or size == 0) then 367 | error(string.format("integral size (%d) out of limits [1,16]", size), 2) 368 | end 369 | else size = packoptsize(c, 0) end 370 | if (size < 1) then error("invalid next option for option 'X'", 2) end 371 | if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then 372 | for j = 1, alignment do 373 | if pos % math.min(size, alignment) == 0 then break end 374 | pos = pos + 1 375 | end 376 | end 377 | elseif c == 's' or c == 'z' then error("bad argument #1 to 'packsize' (variable-length format)", 2) 378 | elseif c ~= ' ' and c ~= '<' and c ~= '>' and c ~= '=' then error(string.format("invalid format option '%s'", c), 2) end 379 | end 380 | return pos 381 | end 382 | 383 | --[[ 384 | * string.unpack (fmt, s [, pos]) 385 | * 386 | * Returns the values packed in string s (see string.pack) according to the format string fmt. 387 | * An optional pos marks where to start reading in s (default is 1). 388 | * After the read values, this function also returns the index of the first unread byte in s. 389 | ]] 390 | local function unpack(fmt, str, pos) 391 | expect(1, fmt, "string") 392 | expect(2, str, "string") 393 | expect(3, pos, "number", "nil") 394 | if pos then 395 | if (pos < 0) then pos = #str + pos 396 | elseif (pos == 0) then error("bad argument #3 to 'unpack' (initial position out of string)", 2) end 397 | if (pos > #str or pos < 0) then error("bad argument #3 to 'unpack' (initial position out of string)", 2) end 398 | else pos = 1 end 399 | local endianness = ByteOrder.LITTLE_ENDIAN 400 | local alignment = 1 401 | local retval = {} 402 | local i = 1 403 | while i <= #fmt do 404 | local c = fmt:sub(i, i) 405 | i = i + 1 406 | if c == '<' or c == '=' then 407 | endianness = ByteOrder.LITTLE_ENDIAN 408 | elseif c == '>' then 409 | endianness = ByteOrder.BIG_ENDIAN 410 | elseif c == '!' then 411 | local size = 0 412 | while i <= #fmt and fmt:sub(i, i):match("%d") do 413 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 414 | size = (size * 10) + tonumber(fmt:sub(i, i)) 415 | i = i + 1 416 | end 417 | if (size > 16) then 418 | error(string.format("integral size (%d) out of limits [1,16]", size)) 419 | elseif (size == 0) then alignment = 4 420 | else alignment = size end 421 | elseif isint[c] then 422 | if (pos + packoptsize(c, 0) > #str + 1) then error("data string too short", 2) end 423 | local res, ressz = unpackint(str, pos, packoptsize(c, 0), endianness, alignment, c:match("%l") ~= nil) 424 | retval[#retval+1] = res 425 | pos = pos + ressz 426 | elseif c:lower() == 'i' then 427 | local signed = c == 'i' 428 | local size = 0 429 | while (i <= #fmt and fmt:sub(i, i):match("%d")) do 430 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 431 | size = (size * 10) + tonumber(fmt:sub(i, i)) 432 | i = i + 1 433 | end 434 | if (size > 16) then 435 | error(string.format("integral size (%d) out of limits [1,16]", size), 2) 436 | elseif (size > 8) then 437 | error(string.format("%d-byte integer does not fit into Lua Integer", size), 2) 438 | elseif (size == 0) then size = 4 end 439 | if (pos + size > #str + 1) then error("data string too short", 2) end 440 | local res, ressz = unpackint(str, pos, size, endianness, alignment, signed) 441 | retval[#retval+1] = res 442 | pos = pos + ressz 443 | elseif c == 'f' then 444 | if (pos % math.min(4, alignment) ~= 0 and alignment > 1) then 445 | for j = 1, alignment do 446 | if pos % math.min(4, alignment) == 0 then break end 447 | pos = pos + 1 448 | end 449 | end 450 | if (pos + 4 > #str + 1) then error("data string too short", 2) end 451 | local res = unpackint(str, pos, 4, endianness, alignment, false) 452 | retval[#retval+1] = intBitsToFloat(res) 453 | pos = pos + 4 454 | elseif c == 'd' or c == 'n' then 455 | if (pos % math.min(8, alignment) ~= 0 and alignment > 1) then 456 | for j = 1, alignment do 457 | if pos % math.min(8, alignment) == 0 then break end 458 | pos = pos + 1 459 | end 460 | end 461 | if (pos + 8 > #str + 1) then error("data string too short", 2) end 462 | local lh, ll = 0, 0 463 | for j = 0, 3 do lh = bit32.bor(lh, bit32.lshift((str:byte(pos + j)), ((endianness == ByteOrder.BIG_ENDIAN and 3 - j or j) * 8))) end 464 | for j = 0, 3 do ll = bit32.bor(ll, bit32.lshift((str:byte(pos + j + 4)), ((endianness == ByteOrder.BIG_ENDIAN and 3 - j or j) * 8))) end 465 | if endianness == ByteOrder.LITTLE_ENDIAN then lh, ll = ll, lh end 466 | retval[#retval+1] = longBitsToDouble(lh, ll) 467 | pos = pos + 8 468 | elseif c == 'c' then 469 | local size = 0 470 | if (i > #fmt or not fmt:sub(i, i):match("%d")) then 471 | error("missing size for format option 'c'", 2) 472 | end 473 | while i <= #fmt and fmt:sub(i, i):match("%d") do 474 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)") end 475 | size = (size * 10) + tonumber(fmt:sub(i, i)) 476 | i = i + 1 477 | end 478 | if (pos + size > #str + 1) then error("data string too short", 2) end 479 | retval[#retval+1] = str:sub(pos, pos + size - 1) 480 | pos = pos + size 481 | elseif c == 'z' then 482 | local size = 0 483 | while (str:byte(pos + size) ~= 0) do 484 | size = size + 1 485 | if (pos + size > #str) then error("unfinished string for format 'z'", 2) end 486 | end 487 | retval[#retval+1] = str:sub(pos, pos + size - 1) 488 | pos = pos + size + 1 489 | elseif c == 's' then 490 | local size = 0 491 | while i <= #fmt and fmt:sub(i, i):match("%d") do 492 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 493 | size = (size * 10) + tonumber(fmt:sub(i, i)) 494 | i = i + 1 495 | end 496 | if (size > 16) then 497 | error(string.format("integral size (%d) out of limits [1,16]", size), 2) 498 | elseif (size == 0) then size = 4 end 499 | if (pos + size > #str + 1) then error("data string too short", 2) end 500 | local num, numsz = unpackint(str, pos, size, endianness, alignment, false) 501 | pos = pos + numsz 502 | if (pos + num > #str + 1) then error("data string too short", 2) end 503 | retval[#retval+1] = str:sub(pos, pos + num - 1) 504 | pos = pos + num 505 | elseif c == 'x' then 506 | pos = pos + 1 507 | elseif c == 'X' then 508 | if (i >= #fmt) then error("invalid next option for option 'X'", 2) end 509 | local size = 0 510 | local c = fmt:sub(i, i) 511 | i = i + 1 512 | if c:lower() == 'i' then 513 | while i <= #fmt and fmt:sub(i, i):match("%d") do 514 | if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end 515 | size = (size * 10) + tonumber(fmt:sub(i, i)) 516 | i = i + 1 517 | end 518 | if (size > 16 or size == 0) then 519 | error(string.format("integral size (%d) out of limits [1,16]", size), 2) 520 | elseif (size == -1) then size = 4 end 521 | else size = packoptsize(c, 0) end 522 | if (size < 1) then error("invalid next option for option 'X'", 2) end 523 | if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then 524 | for j = 1, alignment do 525 | if pos % math.min(size, alignment) == 0 then break end 526 | pos = pos + 1 527 | end 528 | end 529 | elseif c ~= ' ' then error(string.format("invalid format option '%s'", c), 2) end 530 | end 531 | retval[#retval+1] = pos 532 | return table.unpack(retval) 533 | end 534 | 535 | return { 536 | pack = pack, 537 | packsize = packsize, 538 | unpack = unpack 539 | } -------------------------------------------------------------------------------- /rawterm.lua: -------------------------------------------------------------------------------- 1 | --- rawterm.lua - CraftOS-PC raw mode protocol client/server API 2 | -- By JackMacWindows 3 | -- 4 | -- @module rawterm 5 | -- 6 | -- This API provides the ability to host terminals accessible from remote 7 | -- systems, as well as to render those terminals on the screen. It uses the raw 8 | -- mode protocol defined by CraftOS-PC to communicate between client and server. 9 | -- This means that this API can be used to host and connect to a CraftOS-PC 10 | -- instance running over a WebSocket connection (using an external server 11 | -- application). 12 | -- 13 | -- In addition, this API supports raw mode version 1.1, which includes support 14 | -- for filesystem access. This lets the server send and receive files and query 15 | -- file information over the raw connection. 16 | -- 17 | -- To allow the ability to use any type of connection medium to send/receive 18 | -- data, a delegate object is used for communication. This must have a send and 19 | -- receive method, and may also have additional methods as mentioned below. 20 | -- Built-in delegate constructors are provided for WebSockets and Rednet. 21 | -- 22 | -- See the adjacent rawtermtest.lua file for an example of how to use this API. 23 | 24 | -- MIT License 25 | -- 26 | -- Copyright (c) 2021 JackMacWindows 27 | -- 28 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 29 | -- of this software and associated documentation files (the "Software"), to deal 30 | -- in the Software without restriction, including without limitation the rights 31 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | -- copies of the Software, and to permit persons to whom the Software is 33 | -- furnished to do so, subject to the following conditions: 34 | -- 35 | -- The above copyright notice and this permission notice shall be included in all 36 | -- copies or substantial portions of the Software. 37 | -- 38 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | -- SOFTWARE. 45 | 46 | local expect = dofile "/rom/modules/main/cc/expect.lua" 47 | setmetatable(expect, {__call = function(_, ...) return expect.expect(...) end}) 48 | 49 | local rawterm = {} 50 | 51 | local b64str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 52 | local keymap = { 53 | [1] = 0, 54 | [2] = keys.one, 55 | [3] = keys.two, 56 | [4] = keys.three, 57 | [5] = keys.four, 58 | [6] = keys.five, 59 | [7] = keys.six, 60 | [8] = keys.seven, 61 | [9] = keys.eight, 62 | [10] = keys.nine, 63 | [11] = keys.zero, 64 | [12] = keys.minus, 65 | [13] = keys.equals, 66 | [14] = keys.backspace, 67 | [15] = keys.tab, 68 | [16] = keys.q, 69 | [17] = keys.w, 70 | [18] = keys.e, 71 | [19] = keys.r, 72 | [20] = keys.t, 73 | [21] = keys.y, 74 | [22] = keys.u, 75 | [23] = keys.i, 76 | [24] = keys.o, 77 | [25] = keys.p, 78 | [26] = keys.leftBracket, 79 | [27] = keys.rightBracket, 80 | [28] = keys.enter, 81 | [29] = keys.leftCtrl, 82 | [30] = keys.a, 83 | [31] = keys.s, 84 | [32] = keys.d, 85 | [33] = keys.f, 86 | [34] = keys.g, 87 | [35] = keys.h, 88 | [36] = keys.j, 89 | [37] = keys.k, 90 | [38] = keys.l, 91 | [39] = keys.semiColon, 92 | [40] = keys.apostrophe, 93 | [41] = keys.grave, 94 | [42] = keys.leftShift, 95 | [43] = keys.backslash, 96 | [44] = keys.z, 97 | [45] = keys.x, 98 | [46] = keys.c, 99 | [47] = keys.v, 100 | [48] = keys.b, 101 | [49] = keys.n, 102 | [50] = keys.m, 103 | [51] = keys.comma, 104 | [52] = keys.period, 105 | [53] = keys.slash, 106 | [54] = keys.rightShift, 107 | [55] = keys.multiply, 108 | [56] = keys.leftAlt, 109 | [57] = keys.space, 110 | [58] = keys.capsLock, 111 | [59] = keys.f1, 112 | [60] = keys.f2, 113 | [61] = keys.f3, 114 | [62] = keys.f4, 115 | [63] = keys.f5, 116 | [64] = keys.f6, 117 | [65] = keys.f7, 118 | [66] = keys.f8, 119 | [67] = keys.f9, 120 | [68] = keys.f10, 121 | [69] = keys.numLock, 122 | [70] = keys.scrollLock, 123 | [71] = keys.numPad7, 124 | [72] = keys.numPad8, 125 | [73] = keys.numPad9, 126 | [74] = keys.numPadSubtract, 127 | [75] = keys.numPad4, 128 | [76] = keys.numPad5, 129 | [77] = keys.numPad6, 130 | [78] = keys.numPadAdd, 131 | [79] = keys.numPad1, 132 | [80] = keys.numPad2, 133 | [81] = keys.numPad3, 134 | [82] = keys.numPad0, 135 | [83] = keys.numPadDecimal, 136 | [87] = keys.f11, 137 | [88] = keys.f12, 138 | [100] = keys.f13, 139 | [101] = keys.f14, 140 | [102] = keys.f15, 141 | [111] = keys.kana, 142 | [121] = keys.convert, 143 | [123] = keys.noconvert, 144 | [125] = keys.yen, 145 | [141] = keys.numPadEquals, 146 | [144] = keys.cimcumflex, 147 | [145] = keys.at, 148 | [146] = keys.colon, 149 | [147] = keys.underscore, 150 | [148] = keys.kanji, 151 | [149] = keys.stop, 152 | [150] = keys.ax, 153 | [156] = keys.numPadEnter, 154 | [157] = keys.rightCtrl, 155 | [179] = keys.numPadComma, 156 | [181] = keys.numPadDivide, 157 | [184] = keys.rightAlt, 158 | [197] = keys.pause, 159 | [199] = keys.home, 160 | [200] = keys.up, 161 | [201] = keys.pageUp, 162 | [203] = keys.left, 163 | [205] = keys.right, 164 | [207] = keys["end"], 165 | [208] = keys.down, 166 | [209] = keys.pageDown, 167 | [210] = keys.insert, 168 | [211] = keys.delete 169 | } 170 | local keymap_rev = { 171 | [0] = 1, 172 | [keys.one] = 2, 173 | [keys.two] = 3, 174 | [keys.three] = 4, 175 | [keys.four] = 5, 176 | [keys.five] = 6, 177 | [keys.six] = 7, 178 | [keys.seven] = 8, 179 | [keys.eight] = 9, 180 | [keys.nine] = 10, 181 | [keys.zero] = 11, 182 | [keys.minus] = 12, 183 | [keys.equals] = 13, 184 | [keys.backspace] = 14, 185 | [keys.tab] = 15, 186 | [keys.q] = 16, 187 | [keys.w] = 17, 188 | [keys.e] = 18, 189 | [keys.r] = 19, 190 | [keys.t] = 20, 191 | [keys.y] = 21, 192 | [keys.u] = 22, 193 | [keys.i] = 23, 194 | [keys.o] = 24, 195 | [keys.p] = 25, 196 | [keys.leftBracket] = 26, 197 | [keys.rightBracket] = 27, 198 | [keys.enter] = 28, 199 | [keys.leftCtrl] = 29, 200 | [keys.a] = 30, 201 | [keys.s] = 31, 202 | [keys.d] = 32, 203 | [keys.f] = 33, 204 | [keys.g] = 34, 205 | [keys.h] = 35, 206 | [keys.j] = 36, 207 | [keys.k] = 37, 208 | [keys.l] = 38, 209 | [keys.semicolon or keys.semiColon] = 39, 210 | [keys.apostrophe] = 40, 211 | [keys.grave] = 41, 212 | [keys.leftShift] = 42, 213 | [keys.backslash] = 43, 214 | [keys.z] = 44, 215 | [keys.x] = 45, 216 | [keys.c] = 46, 217 | [keys.v] = 47, 218 | [keys.b] = 48, 219 | [keys.n] = 49, 220 | [keys.m] = 50, 221 | [keys.comma] = 51, 222 | [keys.period] = 52, 223 | [keys.slash] = 53, 224 | [keys.rightShift] = 54, 225 | [keys.leftAlt] = 56, 226 | [keys.space] = 57, 227 | [keys.capsLock] = 58, 228 | [keys.f1] = 59, 229 | [keys.f2] = 60, 230 | [keys.f3] = 61, 231 | [keys.f4] = 62, 232 | [keys.f5] = 63, 233 | [keys.f6] = 64, 234 | [keys.f7] = 65, 235 | [keys.f8] = 66, 236 | [keys.f9] = 67, 237 | [keys.f10] = 68, 238 | [keys.numLock] = 69, 239 | [keys.scollLock or keys.scrollLock] = 70, 240 | [keys.numPad7] = 71, 241 | [keys.numPad8] = 72, 242 | [keys.numPad9] = 73, 243 | [keys.numPadSubtract] = 74, 244 | [keys.numPad4] = 75, 245 | [keys.numPad5] = 76, 246 | [keys.numPad6] = 77, 247 | [keys.numPadAdd] = 78, 248 | [keys.numPad1] = 79, 249 | [keys.numPad2] = 80, 250 | [keys.numPad3] = 81, 251 | [keys.numPad0] = 82, 252 | [keys.numPadDecimal] = 83, 253 | [keys.f11] = 87, 254 | [keys.f12] = 88, 255 | [keys.f13] = 100, 256 | [keys.f14] = 101, 257 | [keys.f15] = 102, 258 | [keys.numPadEquals or keys.numPadEqual] = 141, 259 | [keys.numPadEnter] = 156, 260 | [keys.rightCtrl] = 157, 261 | [keys.rightAlt] = 184, 262 | [keys.pause] = 197, 263 | [keys.home] = 199, 264 | [keys.up] = 200, 265 | [keys.pageUp] = 201, 266 | [keys.left] = 203, 267 | [keys.right] = 205, 268 | [keys["end"]] = 207, 269 | [keys.down] = 208, 270 | [keys.pageDown] = 209, 271 | [keys.insert] = 210, 272 | [keys.delete] = 211 273 | } 274 | 275 | local function minver(version) 276 | local res 277 | if _CC_VERSION then res = version <= _CC_VERSION 278 | elseif not _HOST then res = version <= os.version():gsub("CraftOS ", "") 279 | elseif _HOST:match("ComputerCraft 1%.1%d+") ~= version:match("1%.1%d+") then 280 | version = version:gsub("(1%.)([02-9])", "%10%2") 281 | local host = _HOST:gsub("(ComputerCraft 1%.)([02-9])", "%10%2") 282 | res = version <= host:match("ComputerCraft ([0-9%.]+)") 283 | else res = version <= _HOST:match("ComputerCraft ([0-9%.]+)") end 284 | assert(res, "This program requires ComputerCraft " .. version .. " or later.") 285 | end 286 | 287 | local function base64encode(str) 288 | local retval = "" 289 | for s in str:gmatch "..." do 290 | local n = s:byte(1) * 65536 + s:byte(2) * 256 + s:byte(3) 291 | local a, b, c, d = bit32.extract(n, 18, 6), bit32.extract(n, 12, 6), bit32.extract(n, 6, 6), bit32.extract(n, 0, 6) 292 | retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. b64str:sub(d+1, d+1) 293 | end 294 | if #str % 3 == 1 then 295 | local n = str:byte(-1) 296 | local a, b = bit32.rshift(n, 2), bit32.lshift(bit32.band(n, 3), 4) 297 | retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. "==" 298 | elseif #str % 3 == 2 then 299 | local n = str:byte(-2) * 256 + str:byte(-1) 300 | local a, b, c, d = bit32.extract(n, 10, 6), bit32.extract(n, 4, 6), bit32.lshift(bit32.extract(n, 0, 4), 2) 301 | retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. "=" 302 | end 303 | return retval 304 | end 305 | 306 | local function base64decode(str) 307 | local retval = "" 308 | for s in str:gmatch "...." do 309 | if s:sub(3, 4) == '==' then 310 | retval = retval .. string.char(bit32.bor(bit32.lshift(b64str:find(s:sub(1, 1)) - 1, 2), bit32.rshift(b64str:find(s:sub(2, 2)) - 1, 4))) 311 | elseif s:sub(4, 4) == '=' then 312 | local n = (b64str:find(s:sub(1, 1))-1) * 4096 + (b64str:find(s:sub(2, 2))-1) * 64 + (b64str:find(s:sub(3, 3))-1) 313 | retval = retval .. string.char(bit32.extract(n, 10, 8)) .. string.char(bit32.extract(n, 2, 8)) 314 | else 315 | local n = (b64str:find(s:sub(1, 1))-1) * 262144 + (b64str:find(s:sub(2, 2))-1) * 4096 + (b64str:find(s:sub(3, 3))-1) * 64 + (b64str:find(s:sub(4, 4))-1) 316 | retval = retval .. string.char(bit32.extract(n, 16, 8)) .. string.char(bit32.extract(n, 8, 8)) .. string.char(bit32.extract(n, 0, 8)) 317 | end 318 | end 319 | return retval 320 | end 321 | 322 | local crctable 323 | local function crc32(str) 324 | -- calculate CRC-table 325 | if not crctable then 326 | crctable = {} 327 | for i = 0, 0xFF do 328 | local rem = i 329 | for j = 1, 8 do 330 | if bit32.band(rem, 1) == 1 then 331 | rem = bit32.rshift(rem, 1) 332 | rem = bit32.bxor(rem, 0xEDB88320) 333 | else rem = bit32.rshift(rem, 1) end 334 | end 335 | crctable[i] = rem 336 | end 337 | end 338 | local crc = 0xFFFFFFFF 339 | for x = 1, #str do crc = bit32.bxor(bit32.rshift(crc, 8), crctable[bit32.bxor(bit32.band(crc, 0xFF), str:byte(x))]) end 340 | return bit32.bxor(crc, 0xFFFFFFFF) 341 | end 342 | 343 | local function decodeIBT(data, pos) 344 | local ptyp = data:byte(pos) 345 | pos = pos + 1 346 | local pat 347 | if ptyp == 0 then pat = "= -0x80000000 and val < 0x80000000 then return string.pack(" 65535 and flags.isVersion11 then d = "!CPD" .. string.format("%012X", #payload) 455 | else d = "!CPC" .. string.format("%04X", #payload) end 456 | d = d .. payload 457 | if flags.binaryChecksum and id ~= 6 then d = d .. ("%08X"):format(crc32(string.char(type) .. string.char(id or 0) .. data)) 458 | else d = d .. ("%08X"):format(crc32(payload)) end 459 | return d .. "\n" 460 | end 461 | 462 | -- Term functions 463 | 464 | function win.write(text) 465 | text = tostring(text) 466 | expect(1, text, "string") 467 | if cursorY < 1 or cursorY > height then return 468 | elseif cursorX > width or cursorX + #text < 1 then 469 | cursorX = cursorX + #text 470 | return 471 | elseif cursorX < 1 then 472 | text = text:sub(-cursorX + 2) 473 | cursorX = 1 474 | end 475 | local ntext = #text 476 | if cursorX + #text > width then text = text:sub(1, width - cursorX + 1) end 477 | screen[cursorY] = screen[cursorY]:sub(1, cursorX - 1) .. text .. screen[cursorY]:sub(cursorX + #text) 478 | colors[cursorY] = colors[cursorY]:sub(1, cursorX - 1) .. string.char(current_colors):rep(#text) .. colors[cursorY]:sub(cursorX + #text) 479 | cursorX = cursorX + ntext 480 | changed = true 481 | win.redraw() 482 | end 483 | 484 | function win.blit(text, fg, bg) 485 | text = tostring(text) 486 | expect(1, text, "string") 487 | expect(2, fg, "string") 488 | expect(3, bg, "string") 489 | if #text ~= #fg or #fg ~= #bg then error("Arguments must be the same length", 2) end 490 | if cursorY < 1 or cursorY > height then return 491 | elseif cursorX > width or cursorX < 1 - #text then 492 | cursorX = cursorX + #text 493 | win.redraw() 494 | return 495 | elseif cursorX < 1 then 496 | text, fg, bg = text:sub(-cursorX + 2), fg:sub(-cursorX + 2), bg:sub(-cursorX + 2) 497 | cursorX = 1 498 | win.redraw() 499 | end 500 | local ntext = #text 501 | if cursorX + #text > width then text, fg, bg = text:sub(1, width - cursorX + 1), fg:sub(1, width - cursorX + 1), bg:sub(1, width - cursorX + 1) end 502 | local col = "" 503 | for i = 1, #text do col = col .. string.char((tonumber(bg:sub(i, i), 16) or 0) * 16 + (tonumber(fg:sub(i, i), 16) or 0)) end 504 | screen[cursorY] = screen[cursorY]:sub(1, cursorX - 1) .. text .. screen[cursorY]:sub(cursorX + #text) 505 | colors[cursorY] = colors[cursorY]:sub(1, cursorX - 1) .. col .. colors[cursorY]:sub(cursorX + #text) 506 | cursorX = cursorX + ntext 507 | changed = true 508 | win.redraw() 509 | end 510 | 511 | function win.clear() 512 | if mode == 0 then 513 | for i = 1, height do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end 514 | else 515 | for i = 1, height*9 do pixels[i] = ("\x0F"):rep(width*6) end 516 | end 517 | changed = true 518 | win.redraw() 519 | end 520 | 521 | function win.clearLine() 522 | if cursorY >= 1 and cursorY <= height then 523 | screen[cursorY], colors[cursorY] = (" "):rep(width), string.char(current_colors):rep(width) 524 | changed = true 525 | win.redraw() 526 | end 527 | end 528 | 529 | function win.getCursorPos() 530 | return cursorX, cursorY 531 | end 532 | 533 | function win.setCursorPos(cx, cy) 534 | expect(1, cx, "number") 535 | expect(2, cy, "number") 536 | cx, cy = math.floor(cx), math.floor(cy) 537 | if cx == cursorX and cy == cursorY then return end 538 | cursorX, cursorY = cx, cy 539 | changed = true 540 | win.redraw() 541 | end 542 | 543 | function win.getCursorBlink() 544 | return canBlink 545 | end 546 | 547 | function win.setCursorBlink(b) 548 | expect(1, b, "boolean") 549 | canBlink = b 550 | if parent then parent.setCursorBlink(b) end 551 | win.redraw() 552 | end 553 | 554 | function win.isColor() 555 | if parent then return parent.isColor() end 556 | return true 557 | end 558 | 559 | function win.getSize(m) 560 | if (type(m) == "number" and m > 1) or (type(m) == "boolean" and m == true) then return width * 6, height * 9 561 | else return width, height end 562 | end 563 | 564 | function win.scroll(lines) 565 | expect(1, lines, "number") 566 | if math.abs(lines) >= width then 567 | for i = 1, height do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end 568 | elseif lines > 0 then 569 | for i = lines + 1, height do screen[i - lines], colors[i - lines] = screen[i], colors[i] end 570 | for i = height - lines + 1, height do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end 571 | elseif lines < 0 then 572 | for i = 1, height + lines do screen[i - lines], colors[i - lines] = screen[i], colors[i] end 573 | for i = 1, -lines do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end 574 | else return end 575 | changed = true 576 | win.redraw() 577 | end 578 | 579 | function win.getTextColor() 580 | return 2^bit32.band(current_colors, 0x0F) 581 | end 582 | 583 | function win.setTextColor(color) 584 | expect(1, color, "number") 585 | current_colors = bit32.band(current_colors, 0xF0) + bit32.band(math.floor(math.log(color, 2)), 0x0F) 586 | end 587 | 588 | function win.getBackgroundColor() 589 | return 2^bit32.rshift(current_colors, 4) 590 | end 591 | 592 | function win.setBackgroundColor(color) 593 | expect(1, color, "number") 594 | current_colors = bit32.band(current_colors, 0x0F) + bit32.band(math.floor(math.log(color, 2)), 0x0F) * 16 595 | end 596 | 597 | function win.getPaletteColor(color) 598 | expect(1, color, "number") 599 | if mode == 2 then if color < 0 or color > 255 then error("bad argument #1 (value out of range)", 2) end 600 | else color = bit32.band(math.floor(math.log(color, 2)), 0x0F) end 601 | return table.unpack(palette[color]) 602 | end 603 | 604 | function win.setPaletteColor(color, r, g, b) 605 | expect(1, color, "number") 606 | expect(2, r, "number") 607 | expect(3, g, "number") 608 | expect(4, b, "number") 609 | if r < 0 or r > 1 then error("bad argument #2 (value out of range)", 2) end 610 | if g < 0 or g > 1 then error("bad argument #3 (value out of range)", 2) end 611 | if b < 0 or b > 1 then error("bad argument #4 (value out of range)", 2) end 612 | if mode == 2 then if color < 0 or color > 255 then error("bad argument #1 (value out of range)", 2) end 613 | else color = bit32.band(math.floor(math.log(color, 2)), 0x0F) end 614 | palette[color] = {r, g, b} 615 | changed = true 616 | win.redraw() 617 | end 618 | 619 | -- Graphics functions 620 | 621 | function win.getGraphicsMode() 622 | if mode == 0 then return false 623 | else return mode end 624 | end 625 | 626 | function win.setGraphicsMode(m) 627 | expect(1, m, "boolean", "number") 628 | local om = mode 629 | if m == false then mode = 0 630 | elseif m == true then mode = 1 631 | elseif m >= 0 and m <= 2 then mode = math.floor(m) 632 | else error("bad argument #1 (invalid mode)", 2) end 633 | if mode ~= om then changed = true win.redraw() end 634 | end 635 | 636 | function win.getPixel(px, py) 637 | expect(1, px, "number") 638 | expect(2, py, "number") 639 | if px < 0 or px >= width * 6 or py < 0 or py >= height * 9 then return nil end 640 | local c = pixels[py + 1]:byte(px + 1, px + 1) 641 | return mode == 2 and c or 2^c 642 | end 643 | 644 | function win.setPixel(px, py, color) 645 | expect(1, px, "number") 646 | expect(2, py, "number") 647 | expect(3, color, "number") 648 | if px < 0 or px >= width * 6 or py < 0 or py >= height * 9 then return nil end 649 | if mode == 2 then if color < 0 or color > 255 then error("bad argument #3 (value out of range)", 2) end 650 | else color = bit32.band(math.floor(math.log(color, 2)), 0x0F) end 651 | pixels[py + 1] = pixels[py + 1]:sub(1, px) .. string.char(color) .. pixels[py + 1]:sub(px + 2) 652 | changed = true 653 | win.redraw() 654 | end 655 | 656 | function win.drawPixels(px, py, pix, pw, ph) 657 | expect(1, px, "number") 658 | expect(2, py, "number") 659 | expect(3, pix, "table", "number") 660 | expect(4, pw, "number", type(pix) ~= "number" and "nil" or nil) 661 | expect(5, ph, "number", type(pix) ~= "number" and "nil" or nil) 662 | if type(pix) == "number" then 663 | if mode == 2 then if pix < 0 or pix > 255 then error("bad argument #3 (value out of range)", 2) end 664 | else pix = bit32.band(math.floor(math.log(pix, 2)), 0x0F) end 665 | for cy = py + 1, py + ph do pixels[cy] = pixels[cy]:sub(1, px) .. string.char(pix):rep(pw) .. pixels[cy]:sub(px + pw + 1) end 666 | else 667 | for cy = py + 1, py + (ph or #pix) do 668 | local row = pix[cy - py] 669 | if row and pixels[cy] then 670 | if type(row) == "string" then 671 | pixels[cy] = pixels[cy]:sub(1, px) .. row:sub(1, pw or -1) .. pixels[cy]:sub(px + (pw or #row) + 1) 672 | elseif type(row) == "table" then 673 | local str = "" 674 | for cx = 1, pw or #row do str = str .. string.char(row[cx] or pixels[cy]:byte(px + cx)) end 675 | pixels[cy] = pixels[cy]:sub(1, px) .. str .. pixels[cy]:sub(px + #str + 1) 676 | end 677 | end 678 | end 679 | end 680 | changed = true 681 | win.redraw() 682 | end 683 | 684 | function win.getPixels(px, py, pw, ph, str) 685 | expect(1, px, "number") 686 | expect(2, py, "number") 687 | expect(3, pw, "number") 688 | expect(4, ph, "number") 689 | expect(5, str, "boolean", "nil") 690 | local retval = {} 691 | for cy = py + 1, py + ph do 692 | if pixels[cy] then if str then retval[cy - py] = pixels[cy]:sub(px + 1, px + pw) else 693 | retval[cy - py] = {pixels[cy]:byte(px + 1, px + pw)} 694 | if mode < 2 then for i = 1, pw do retval[cy - py][i] = 2^retval[cy - py][i] end end 695 | end end 696 | end 697 | return retval 698 | end 699 | 700 | win.isColour = win.isColor 701 | win.getTextColour = win.getTextColor 702 | win.setTextColour = win.setTextColor 703 | win.getBackgroundColour = win.getBackgroundColor 704 | win.setBackgroundColour = win.setBackgroundColor 705 | win.getPaletteColour = win.getPaletteColor 706 | win.setPaletteColour = win.setPaletteColor 707 | 708 | -- Window functions 709 | 710 | function win.getLine(cy) 711 | if cy < 1 or cy > height then return nil end 712 | local fg, bg = "", "" 713 | for c in colors[cy]:gmatch "." do 714 | fg, bg = fg .. ("%x"):format(bit32.band(c:byte(), 0x0F)), bg .. ("%x"):format(bit32.rshift(c:byte(), 4)) 715 | end 716 | return screen[cy], fg, bg 717 | end 718 | 719 | function win.isVisible() 720 | return visible 721 | end 722 | 723 | function win.setVisible(v) 724 | expect(1, v, "boolean") 725 | visible = v 726 | win.redraw() 727 | end 728 | 729 | function win.redraw() 730 | if visible and changed then 731 | -- Draw to parent screen 732 | if parent then 733 | -- This is NOT efficient, but it's not really supposed to be anyway. 734 | if parent.getGraphicsMode and (parent.getGraphicsMode() or 0) ~= mode then parent.setGraphicsMode(mode) end 735 | if mode == 0 then 736 | local b = parent.getCursorBlink() 737 | parent.setCursorBlink(false) 738 | for cy = 1, height do 739 | parent.setCursorPos(x, y + cy - 1) 740 | parent.blit(win.getLine(cy)) 741 | end 742 | parent.setCursorBlink(b) 743 | win.restoreCursor() 744 | elseif parent.drawPixels then 745 | parent.drawPixels((x - 1) * 6, (y - 1) * 9, pixels, width, height) 746 | end 747 | for i = 0, (parent.getGraphicsMode and mode == 2 and 255 or 15) do parent.setPaletteColor(2^i, table.unpack(palette[i])) end 748 | end 749 | -- Draw to raw target 750 | if not isClosed then 751 | local rleText = "" 752 | if mode == 0 then 753 | local c, n = screen[1]:sub(1, 1), 0 754 | for cy = 1, height do 755 | for ch in screen[cy]:gmatch "." do 756 | if ch ~= c or n == 255 then 757 | rleText = rleText .. c .. string.char(n) 758 | c, n = ch, 0 759 | end 760 | n=n+1 761 | end 762 | end 763 | if n > 0 then rleText = rleText .. c .. string.char(n) end 764 | c, n = colors[1]:sub(1, 1), 0 765 | for cy = 1, height do 766 | for ch in colors[cy]:gmatch "." do 767 | if ch ~= c or n == 255 then 768 | rleText = rleText .. c .. string.char(n) 769 | c, n = ch, 0 770 | end 771 | n=n+1 772 | end 773 | end 774 | if n > 0 then rleText = rleText .. c .. string.char(n) end 775 | else 776 | local c, n = pixels[1]:sub(1, 1), 0 777 | for cy = 1, height * 9 do 778 | for ch in pixels[cy]:gmatch "." do 779 | if ch ~= c or n == 255 then 780 | rleText = rleText .. c .. string.char(n) 781 | c, n = ch, 0 782 | end 783 | n=n+1 784 | end 785 | end 786 | end 787 | for i = 0, (mode == 2 and 255 or 15) do rleText = rleText .. string.char(palette[i][1] * 255) .. string.char(palette[i][2] * 255) .. string.char(palette[i][3] * 255) end 788 | delegate:send(makePacket(0, id, string.pack(" width then 817 | for cy = 1, height do 818 | screen[cy], colors[cy] = screen[cy] .. (" "):rep(nwidth - width), colors[cy] .. string.char(current_colors):rep(nwidth - width) 819 | for i = 1, 9 do pixels[(cy - 1)*9 + i] = pixels[(cy - 1)*9 + i] .. ("\x0F"):rep((nwidth - width) * 6) end 820 | end 821 | end 822 | width = nwidth 823 | end 824 | if nheight then 825 | if nheight < height then 826 | for cy = nheight + 1, height do 827 | screen[cy], colors[cy] = nil 828 | for i = 1, 9 do pixels[(cy - 1)*9 + i] = nil end 829 | end 830 | elseif nheight > height then 831 | for cy = height + 1, nheight do 832 | screen[cy], colors[cy] = (" "):rep(width), string.char(current_colors):rep(width) 833 | for i = 1, 9 do pixels[(cy - 1)*9 + i] = ("\x0F"):rep(width * 6) end 834 | end 835 | end 836 | height = nheight 837 | end 838 | if resized and not isClosed then delegate:send(makePacket(4, id, string.pack(" 65535 and flags.isVersion11 then d = "!CPD" .. string.format("%012X", #payload) 1063 | else d = "!CPC" .. string.format("%04X", #payload) end 1064 | d = d .. payload 1065 | if flags.binaryChecksum then d = d .. ("%08X"):format(crc32(string.char(type) .. string.char(id or 0) .. data)) 1066 | else d = d .. ("%08X"):format(crc32(payload)) end 1067 | return d .. "\n" 1068 | end 1069 | 1070 | local function makeFSFunction(fid, type, p2) 1071 | local f = function(path, path2) 1072 | expect(1, path, "string") 1073 | if p2 then expect(2, path, "string") end 1074 | local n = nextFSID 1075 | delegate:send(makePacket(7, id, string.pack(p2 and "= #data then return nil end 1186 | if n == nil then 1187 | if bit32.btest(m, 4) then 1188 | pos = pos + 1 1189 | return data:byte(pos - 1) 1190 | else n = 1 end 1191 | end 1192 | pos = pos + n 1193 | return data:sub(pos - n, pos - 1) 1194 | end, 1195 | readLine = function(strip) 1196 | if closed then error("attempt to use closed file", 2) end 1197 | if pos >= #data then return nil end 1198 | local oldpos, line = pos 1199 | line, pos = data:match("([^\n]" .. (strip and "+)\n" or "*\n)").."()", pos) 1200 | if not pos then 1201 | line = data:sub(pos) 1202 | pos = #data 1203 | end 1204 | return line 1205 | end, 1206 | readAll = function() 1207 | if closed then error("attempt to use closed file", 2) end 1208 | if pos >= #data then return nil end 1209 | local d = data:sub(pos) 1210 | pos = #data 1211 | return d 1212 | end, 1213 | close = function() 1214 | if closed then error("attempt to use closed file", 2) end 1215 | closed = true 1216 | end, 1217 | seek = bit32.btest(m, 4) and function(whence, offset) 1218 | expect(1, whence, "string", "nil") 1219 | expect(2, offset, "number", "nil") 1220 | whence = whence or "cur" 1221 | offset = offset or 0 1222 | if closed then error("attempt to use closed file", 2) end 1223 | if whence == "set" then pos = offset 1224 | elseif whence == "cur" then pos = pos + offset 1225 | elseif whence == "end" then pos = #data - offset 1226 | else error("Invalid whence", 2) end 1227 | return pos 1228 | end or nil 1229 | } 1230 | end 1231 | end 1232 | } 1233 | 1234 | --- Updates the window with the raw message provided. 1235 | -- @param message A raw message to parse. 1236 | function handle.update(message) 1237 | expect(1, message, "string") 1238 | if message:sub(1, 3) == "!CP" then 1239 | local off = 8 1240 | if message:sub(4, 4) == 'D' then off = 16 end 1241 | local size = tonumber(message:sub(5, off), 16) 1242 | local payload = message:sub(off + 1, off + size) 1243 | local expected = tonumber(message:sub(off + size + 1, off + size + 8), 16) 1244 | local data = base64decode(payload) 1245 | if crc32(flags.binaryChecksum and data or payload) == expected then 1246 | local typ, wid = data:byte(1, 2) 1247 | if wid == id then 1248 | if typ == 0 and window then 1249 | local mode, blink, width, height, cursorX, cursorY, grayscale = string.unpack("