diff options
| author | Andrew Lee <alee14498@protonmail.com> | 2020-11-15 13:37:32 -0500 |
|---|---|---|
| committer | Andrew Lee <alee14498@protonmail.com> | 2020-11-15 13:37:32 -0500 |
| commit | 621e13d8bed7215a076e505eba998c7a0ec38519 (patch) | |
| tree | 46a8be63ce5fe3c5a38bfbdce6ec269382f39c01 /Programs/Firewolf.bup/Contents/bits-UI | |
| parent | bed8f884aa3acbca1bc5a0772c73d17c373be77e (diff) | |
| download | bits-UI-621e13d8bed7215a076e505eba998c7a0ec38519.tar.gz bits-UI-621e13d8bed7215a076e505eba998c7a0ec38519.tar.bz2 bits-UI-621e13d8bed7215a076e505eba998c7a0ec38519.zip | |
New program system; Capitalized folders
Diffstat (limited to 'Programs/Firewolf.bup/Contents/bits-UI')
| -rw-r--r-- | Programs/Firewolf.bup/Contents/bits-UI/firewolf.lua | 3243 |
1 files changed, 3243 insertions, 0 deletions
diff --git a/Programs/Firewolf.bup/Contents/bits-UI/firewolf.lua b/Programs/Firewolf.bup/Contents/bits-UI/firewolf.lua new file mode 100644 index 0000000..d1ba857 --- /dev/null +++ b/Programs/Firewolf.bup/Contents/bits-UI/firewolf.lua @@ -0,0 +1,3243 @@ + +-- +-- Firewolf +-- Made by GravityScore and 1lann +-- + + + +-- Variables + + +local version = "3.5" +local build = 18 + +local w, h = term.getSize() + +local isMenubarOpen = true +local menubarWindow = nil + +local allowUnencryptedConnections = true +local enableTabBar = true + +local currentWebsiteURL = "" +local builtInSites = {} + +local currentProtocol = "" +local protocols = {} + +local currentTab = 1 +local maxTabs = 5 +local maxTabNameWidth = 8 +local tabs = {} + +local languages = {} + +local history = {} + +local publicDNSChannel = 9999 +local publicResponseChannel = 9998 +local responseID = 41738 + +local httpTimeout = 10 +local searchResultTimeout = 1 +local initiationTimeout = 2 +local animationInterval = 0.125 +local fetchTimeout = 3 +local serverLimitPerComputer = 1 + +local websiteErrorEvent = "firewolf_websiteErrorEvent" +local redirectEvent = "firewolf_redirectEvent" + +local baseURL = "https://raw.githubusercontent.com/1lann/Firewolf/master/src" +local buildURL = baseURL .. "/build.txt" +local firewolfURL = baseURL .. "/client.lua" +local serverURL = baseURL .. "/server.lua" + +local originalTerminal = term.current() + +local firewolfLocation = "/" .. shell.getRunningProgram() +local downloadsLocation = "/downloads" + + +local theme = {} + +local colorTheme = { + background = colors.gray, + accent = colors.red, + subtle = colors.orange, + + lightText = colors.gray, + text = colors.white, + errorText = colors.red, +} + +local grayscaleTheme = { + background = colors.black, + accent = colors.black, + subtle = colors.black, + + lightText = colors.white, + text = colors.white, + errorText = colors.white, +} + + + +-- Utilities + + +local modifiedRead = function(properties) + local text = "" + local startX, startY = term.getCursorPos() + local pos = 0 + + local previousText = "" + local readHistory = nil + local historyPos = 0 + + if not properties then + properties = {} + end + + if properties.displayLength then + properties.displayLength = math.min(properties.displayLength, w - 2) + else + properties.displayLength = w - startX - 1 + end + + if properties.startingText then + text = properties.startingText + pos = text:len() + end + + if properties.history then + readHistory = {} + for k, v in pairs(properties.history) do + readHistory[k] = v + end + end + + if readHistory[1] == text then + table.remove(readHistory, 1) + end + + local draw = function(replaceCharacter) + local scroll = 0 + if properties.displayLength and pos > properties.displayLength then + scroll = pos - properties.displayLength + end + + local repl = replaceCharacter or properties.replaceCharacter + term.setTextColor(theme.text) + term.setCursorPos(startX, startY) + if repl then + term.write(string.rep(repl:sub(1, 1), text:len() - scroll)) + else + term.write(text:sub(scroll + 1)) + end + + term.setCursorPos(startX + pos - scroll, startY) + end + + term.setCursorBlink(true) + draw() + while true do + local event, key, x, y, param4, param5 = os.pullEvent() + + if properties.onEvent then + -- Actions: + -- - exit (bool) + -- - text + -- - nullifyText + + term.setCursorBlink(false) + local action = properties.onEvent(text, event, key, x, y, param4, param5) + if action then + if action.text then + draw(" ") + text = action.text + pos = text:len() + end if action.nullifyText then + text = nil + action.exit = true + end if action.exit then + break + end + end + draw() + end + + term.setCursorBlink(true) + if event == "char" then + local canType = true + if properties.maxLength and text:len() >= properties.maxLength then + canType = false + end + + if canType then + text = text:sub(1, pos) .. key .. text:sub(pos + 1, -1) + pos = pos + 1 + draw() + end + elseif event == "key" then + if key == keys.enter then + break + elseif key == keys.left and pos > 0 then + pos = pos - 1 + draw() + elseif key == keys.right and pos < text:len() then + pos = pos + 1 + draw() + elseif key == keys.backspace and pos > 0 then + draw(" ") + text = text:sub(1, pos - 1) .. text:sub(pos + 1, -1) + pos = pos - 1 + draw() + elseif key == keys.delete and pos < text:len() then + draw(" ") + text = text:sub(1, pos) .. text:sub(pos + 2, -1) + draw() + elseif key == keys.home then + pos = 0 + draw() + elseif key == keys["end"] then + pos = text:len() + draw() + elseif (key == keys.up or key == keys.down) and readHistory then + local shouldDraw = false + if historyPos == 0 then + previousText = text + elseif historyPos > 0 then + readHistory[historyPos] = text + end + + if key == keys.up then + if historyPos < #readHistory then + historyPos = historyPos + 1 + shouldDraw = true + end + else + if historyPos > 0 then + historyPos = historyPos - 1 + shouldDraw = true + end + end + + if shouldDraw then + draw(" ") + if historyPos > 0 then + text = readHistory[historyPos] + else + text = previousText + end + pos = text:len() + draw() + end + end + elseif event == "mouse_click" then + local scroll = 0 + if properties.displayLength and pos > properties.displayLength then + scroll = pos - properties.displayLength + end + + if y == startY and x >= startX and x <= math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then + pos = x - startX + scroll + draw() + elseif y == startY then + if x < startX then + pos = scroll + draw() + elseif x > math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then + pos = text:len() + draw() + end + end + end + end + + term.setCursorBlink(false) + print("") + return text +end + + +local prompt = function(items, x, y, w, h) + local selected = 1 + local scroll = 0 + + local draw = function() + for i = scroll + 1, scroll + h do + local item = items[i] + if item then + term.setCursorPos(x, y + i - 1) + term.setBackgroundColor(theme.background) + term.setTextColor(theme.lightText) + + if scroll + selected == i then + term.setTextColor(theme.text) + term.write(" > ") + else + term.write(" - ") + end + + term.write(item) + end + end + end + + draw() + while true do + local event, key, x, y = os.pullEvent() + + if event == "key" then + if key == keys.up and selected > 1 then + selected = selected - 1 + + if selected - scroll == 0 then + scroll = scroll - 1 + end + elseif key == keys.down and selected < #items then + selected = selected + 1 + end + + draw() + elseif event == "mouse_click" then + + elseif event == "mouse_scroll" then + if key > 0 then + os.queueEvent("key", keys.down) + else + os.queueEvent("key", keys.up) + end + end + end +end + + + +-- GUI + + +local clear = function(bg, fg) + term.setTextColor(fg) + term.setBackgroundColor(bg) + term.clear() + term.setCursorPos(1, 1) +end + + +local fill = function(x, y, width, height, bg) + term.setBackgroundColor(bg) + for i = y, y + height - 1 do + term.setCursorPos(x, i) + term.write(string.rep(" ", width)) + end +end + + +local center = function(text) + local x, y = term.getCursorPos() + term.setCursorPos(math.floor(w / 2 - text:len() / 2) + (text:len() % 2 == 0 and 1 or 0), y) + term.write(text) + term.setCursorPos(1, y + 1) +end + + +local centerSplit = function(text, width) + local words = {} + for word in text:gmatch("[^ \t]+") do + table.insert(words, word) + end + + local lines = {""} + while lines[#lines]:len() < width do + lines[#lines] = lines[#lines] .. words[1] .. " " + table.remove(words, 1) + + if #words == 0 then + break + end + + if lines[#lines]:len() + words[1]:len() >= width then + table.insert(lines, "") + end + end + + for _, line in pairs(lines) do + center(line) + end +end + + + +-- Updating + + +local download = function(url) + http.request(url) + local timeoutID = os.startTimer(httpTimeout) + while true do + local event, fetchedURL, response = os.pullEvent() + if (event == "timer" and fetchedURL == timeoutID) or event == "http_failure" then + return false + elseif event == "http_success" and fetchedURL == url then + local contents = response.readAll() + response.close() + return contents + end + end +end + + +local downloadAndSave = function(url, path) + local contents = download(url) + if contents and not fs.isReadOnly(path) and not fs.isDir(path) then + local f = io.open(path, "w") + f:write(contents) + f:close() + return false + end + return true +end + + +local updateAvailable = function() + local number = download(buildURL) + if not number then + return false, true + end + + if number and tonumber(number) and tonumber(number) > build then + return true, false + end + + return false, false +end + + +local redownloadBrowser = function() + return downloadAndSave(firewolfURL, firewolfLocation) +end + + + +-- Display Websites + + +builtInSites["display"] = {} + + +builtInSites["display"]["firewolf"] = function() + local logo = { + "______ _ __ ", + "| ___| | |/ _|", + "| |_ _ ____ _____ _____ | | |_ ", + "| _|| | __/ _ \\ \\ /\\ / / _ \\| | _|", + "| | | | | | __/\\ V V / <_> | | | ", + "\\_| |_|_| \\___| \\_/\\_/ \\___/|_|_| ", + } + + clear(theme.background, theme.text) + fill(1, 3, w, 9, theme.subtle) + + term.setCursorPos(1, 3) + for _, line in pairs(logo) do + center(line) + end + + term.setCursorPos(1, 10) + center(version) + + term.setBackgroundColor(theme.background) + term.setTextColor(theme.text) + term.setCursorPos(1, 14) + center("Search using the Query Box above") + center("Visit rdnt://help for help using Firewolf.") + + term.setCursorPos(1, h - 2) + center("Made by GravityScore and 1lann") +end + + +builtInSites["display"]["credits"] = function() + clear(theme.background, theme.text) + + fill(1, 6, w, 3, theme.subtle) + term.setCursorPos(1, 7) + center("Credits") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 11) + center("Written by GravityScore and 1lann") + print("") + center("RC4 Implementation by AgentE382") +end + + +builtInSites["display"]["help"] = function() + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Help") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 7) + center("Click on the URL bar or press control to") + center("open the query box") + print("") + center("Type in a search query or website URL") + center("into the query box.") + print("") + center("Search for nothing to see all available") + center("websites.") + print("") + center("Visit rdnt://server to setup a server.") + center("Visit rdnt://update to update Firewolf.") +end + + +builtInSites["display"]["server"] = function() + clear(theme.background, theme.text) + + fill(1, 6, w, 3, theme.subtle) + term.setCursorPos(1, 7) + center("Server Software") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 11) + if not http then + center("HTTP is not enabled!") + print("") + center("Please enable it in your config file") + center("to download Firewolf Server.") + else + center("Press space to download") + center("Firewolf Server to:") + print("") + center("/fwserver") + + while true do + local event, key = os.pullEvent() + if event == "key" and key == 57 then + fill(1, 11, w, 4, theme.background) + term.setCursorPos(1, 11) + center("Downloading...") + + local err = downloadAndSave(serverURL, "/fwserver") + + fill(1, 11, w, 4, theme.background) + term.setCursorPos(1, 11) + center(err and "Download failed!" or "Download successful!") + end + end + end +end + + +builtInSites["display"]["update"] = function() + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Update") + + term.setBackgroundColor(theme.background) + if not http then + term.setCursorPos(1, 9) + center("HTTP is not enabled!") + print("") + center("Please enable it in your config") + center("file to download Firewolf updates.") + else + term.setCursorPos(1, 10) + center("Checking for updates...") + + local available, err = updateAvailable() + + term.setCursorPos(1, 10) + if available then + term.clearLine() + center("Update found!") + center("Press enter to download.") + + while true do + local event, key = os.pullEvent() + if event == "key" and key == keys.enter then + break + end + end + + fill(1, 10, w, 2, theme.background) + term.setCursorPos(1, 10) + center("Downloading...") + + local err = redownloadBrowser() + + term.setCursorPos(1, 10) + term.clearLine() + if err then + center("Download failed!") + else + center("Download succeeded!") + center("Please restart Firewolf...") + end + elseif err then + term.clearLine() + center("Checking failed!") + else + term.clearLine() + center("No updates found.") + end + end +end + + + +-- Built In Websites + + +builtInSites["error"] = function(err) + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Failed to load page!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 9) + center(err) + print("") + center("Please try again.") +end + + +builtInSites["noresults"] = function() + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("No results!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 9) + center("Your search didn't return") + center("any results!") + + os.pullEvent("key") + os.queueEvent("") + os.pullEvent() +end + + +builtInSites["search advanced"] = function(results) + local startY = 6 + local height = h - startY - 1 + local scroll = 0 + + local draw = function() + fill(1, startY, w, height + 1, theme.background) + + for i = scroll + 1, scroll + height do + if results[i] then + term.setCursorPos(5, (i - scroll) + startY) + term.write(currentProtocol .. "://" .. results[i]) + end + end + end + + draw() + while true do + local event, but, x, y = os.pullEvent() + + if event == "mouse_click" and y >= startY and y <= startY + height then + local item = results[y - startY + scroll] + if item then + os.queueEvent(redirectEvent, item) + coroutine.yield() + end + elseif event == "key" then + if but == keys.up then + scroll = math.max(0, scroll - 1) + elseif but == keys.down and #results > height then + scroll = math.min(scroll + 1, #results - height) + end + + draw() + elseif event == "mouse_scroll" then + if but > 0 then + os.queueEvent("key", keys.down) + else + os.queueEvent("key", keys.up) + end + end + end +end + + +builtInSites["search basic"] = function(results) + local startY = 6 + local height = h - startY - 1 + local scroll = 0 + local selected = 1 + + local draw = function() + fill(1, startY, w, height + 1, theme.background) + + for i = scroll + 1, scroll + height do + if results[i] then + if i == selected + scroll then + term.setCursorPos(3, (i - scroll) + startY) + term.write("> " .. currentProtocol .. "://" .. results[i]) + else + term.setCursorPos(5, (i - scroll) + startY) + term.write(currentProtocol .. "://" .. results[i]) + end + end + end + end + + draw() + while true do + local event, but, x, y = os.pullEvent() + + if event == "key" then + if but == keys.up and selected + scroll > 1 then + if selected > 1 then + selected = selected - 1 + else + scroll = math.max(0, scroll - 1) + end + elseif but == keys.down and selected + scroll < #results then + if selected < height then + selected = selected + 1 + else + scroll = math.min(scroll + 1, #results - height) + end + elseif but == keys.enter then + local item = results[scroll + selected] + if item then + os.queueEvent(redirectEvent, item) + coroutine.yield() + end + end + + draw() + elseif event == "mouse_scroll" then + if but > 0 then + os.queueEvent("key", keys.down) + else + os.queueEvent("key", keys.up) + end + end + end +end + + +builtInSites["search"] = function(results) + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center(#results .. " Search " .. (#results == 1 and "Result" or "Results")) + + term.setBackgroundColor(theme.background) + + if term.isColor() then + builtInSites["search advanced"](results) + else + builtInSites["search basic"](results) + end +end + + +builtInSites["crash"] = function(err) + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("The website crashed!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 8) + centerSplit(err, w - 4) + print("\n") + center("Please report this error to") + center("the website creator.") +end + + + +-- Menubar + + +local getTabName = function(url) + local name = url:match("^[^/]+") + + if not name then + name = "Search" + end + + if name:sub(1, 3) == "www" then + name = name:sub(5):gsub("^%s*(.-)%s*$", "%1") + end + + if name:len() > maxTabNameWidth then + name = name:sub(1, maxTabNameWidth):gsub("^%s*(.-)%s*$", "%1") + end + + if name:sub(-1, -1) == "." then + name = name:sub(1, -2):gsub("^%s*(.-)%s*$", "%1") + end + + return name:gsub("^%s*(.-)%s*$", "%1") +end + + +local determineClickedTab = function(x, y) + if y == 2 then + local minx = 2 + for i, tab in pairs(tabs) do + local name = getTabName(tab.url) + + if x >= minx and x <= minx + name:len() - 1 then + return i + elseif x == minx + name:len() and i == currentTab and #tabs > 1 then + return "close" + else + minx = minx + name:len() + 2 + end + end + + if x == minx and #tabs < maxTabs then + return "new" + end + end + + return nil +end + + +local setupMenubar = function() + if enableTabBar then + menubarWindow = window.create(originalTerminal, 1, 1, w, 2, false) + else + menubarWindow = window.create(originalTerminal, 1, 1, w, 1, false) + end +end + + +local drawMenubar = function() + if isMenubarOpen then + term.redirect(menubarWindow) + menubarWindow.setVisible(true) + + fill(1, 1, w, 1, theme.accent) + term.setTextColor(theme.text) + + term.setBackgroundColor(theme.accent) + term.setCursorPos(2, 1) + if currentWebsiteURL:match("^[^%?]+") then + term.write(currentProtocol .. "://" .. currentWebsiteURL:match("^[^%?]+")) + else + term.write(currentProtocol .. "://" ..currentWebsiteURL) + end + + term.setCursorPos(w - 5, 1) + term.write("[===]") + + if enableTabBar then + fill(1, 2, w, 1, theme.subtle) + + term.setCursorPos(1, 2) + for i, tab in pairs(tabs) do + term.setBackgroundColor(theme.subtle) + term.setTextColor(theme.lightText) + if i == currentTab then + term.setTextColor(theme.text) + end + + local tabName = getTabName(tab.url) + term.write(" " .. tabName) + + if i == currentTab and #tabs > 1 then + term.setTextColor(theme.errorText) + term.write("x") + else + term.write(" ") + end + end + + if #tabs < maxTabs then + term.setTextColor(theme.lightText) + term.setBackgroundColor(theme.subtle) + term.write(" + ") + end + end + else + menubarWindow.setVisible(false) + end +end + + + +-- RC4 +-- Implementation by AgentE382 + + +local cryptWrapper = function(plaintext, salt) + local key = type(salt) == "table" and {unpack(salt)} or {string.byte(salt, 1, #salt)} + local S = {} + for i = 0, 255 do + S[i] = i + end + + local j, keylength = 0, #key + for i = 0, 255 do + j = (j + S[i] + key[i % keylength + 1]) % 256 + S[i], S[j] = S[j], S[i] + end + + local i = 0 + j = 0 + local chars, astable = type(plaintext) == "table" and {unpack(plaintext)} or {string.byte(plaintext, 1, #plaintext)}, false + + for n = 1, #chars do + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + chars[n] = bit.bxor(S[(S[i] + S[j]) % 256], chars[n]) + if chars[n] > 127 or chars[n] == 13 then + astable = true + end + end + + return astable and chars or string.char(unpack(chars)) +end + + +local crypt = function(text, key) + local resp, msg = pcall(cryptWrapper, text, key) + if resp then + return msg + else + return nil + end +end + + + +-- Base64 +-- +-- Base64 Encryption/Decryption +-- By KillaVanilla +-- http://www.computercraft.info/forums2/index.php?/topic/12450-killavanillas-various-apis/ +-- http://pastebin.com/rCYDnCxn +-- + + +local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + + +local function sixBitToBase64(input) + return string.sub(alphabet, input+1, input+1) +end + + +local function base64ToSixBit(input) + for i=1, 64 do + if input == string.sub(alphabet, i, i) then + return i-1 + end + end +end + + +local function octetToBase64(o1, o2, o3) + local shifted = bit.brshift(bit.band(o1, 0xFC), 2) + local i1 = sixBitToBase64(shifted) + local i2 = "A" + local i3 = "=" + local i4 = "=" + if o2 then + i2 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o1, 3), 4), bit.brshift(bit.band(o2, 0xF0), 4) )) + if not o3 then + i3 = sixBitToBase64(bit.blshift(bit.band(o2, 0x0F), 2)) + else + i3 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o2, 0x0F), 2), bit.brshift(bit.band(o3, 0xC0), 6) )) + end + else + i2 = sixBitToBase64(bit.blshift(bit.band(o1, 3), 4)) + end + if o3 then + i4 = sixBitToBase64(bit.band(o3, 0x3F)) + end + + return i1..i2..i3..i4 +end + + +local function base64ToThreeOctet(s1) + local c1 = base64ToSixBit(string.sub(s1, 1, 1)) + local c2 = base64ToSixBit(string.sub(s1, 2, 2)) + local c3 = 0 + local c4 = 0 + local o1 = 0 + local o2 = 0 + local o3 = 0 + if string.sub(s1, 3, 3) == "=" then + c3 = nil + c4 = nil + elseif string.sub(s1, 4, 4) == "=" then + c3 = base64ToSixBit(string.sub(s1, 3, 3)) + c4 = nil + else + c3 = base64ToSixBit(string.sub(s1, 3, 3)) + c4 = base64ToSixBit(string.sub(s1, 4, 4)) + end + o1 = bit.bor( bit.blshift(c1, 2), bit.brshift(bit.band( c2, 0x30 ), 4) ) + if c3 then + o2 = bit.bor( bit.blshift(bit.band(c2, 0x0F), 4), bit.brshift(bit.band( c3, 0x3C ), 2) ) + else + o2 = nil + end + if c4 then + o3 = bit.bor( bit.blshift(bit.band(c3, 3), 6), c4 ) + else + o3 = nil + end + return o1, o2, o3 +end + + +local function splitIntoBlocks(bytes) + local blockNum = 1 + local blocks = {} + for i=1, #bytes, 3 do + blocks[blockNum] = {bytes[i], bytes[i+1], bytes[i+2]} + blockNum = blockNum+1 + end + return blocks +end + + +function base64Encode(bytes) + local blocks = splitIntoBlocks(bytes) + local output = "" + for i=1, #blocks do + output = output..octetToBase64( unpack(blocks[i]) ) + end + return output +end + + +function base64Decode(str) + local bytes = {} + local blocks = {} + local blockNum = 1 + + for i=1, #str, 4 do + blocks[blockNum] = string.sub(str, i, i+3) + blockNum = blockNum+1 + end + + for i=1, #blocks do + local o1, o2, o3 = base64ToThreeOctet(blocks[i]) + table.insert(bytes, o1) + table.insert(bytes, o2) + table.insert(bytes, o3) + end + + return bytes +end + + + +-- SHA-256 +-- +-- Adaptation of the Secure Hashing Algorithm (SHA-244/256) +-- Found Here: http://lua-users.org/wiki/SecureHashAlgorithm +-- +-- Using an adapted version of the bit library +-- Found Here: https://bitbucket.org/Boolsheet/bslf/src/1ee664885805/bit.lua + + +local MOD = 2^32 +local MODM = MOD-1 + + +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k) + t[k] = v + return v + end + return t +end + + +local function make_bitop_uncached(t, m) + local function bitop(a, b) + local res,p = 0,1 + while a ~= 0 and b ~= 0 do + local am, bm = a % m, b % m + res = res + t[am][bm] * p + a = (a - am) / m + b = (b - bm) / m + p = p * m + end + res = res + (a + b) * p + return res + end + + return bitop +end + + +local function make_bitop(t) + local op1 = make_bitop_uncached(t,2^1) + local op2 = memoize(function(a) + return memoize(function(b) + return op1(a, b) + end) + end) + return make_bitop_uncached(op2, 2 ^ (t.n or 1)) +end + + +local customBxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4}) + +local function customBxor(a, b, c, ...) + local z = nil + if b then + a = a % MOD + b = b % MOD + z = customBxor1(a, b) + if c then + z = customBxor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end + + +local function customBand(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = ((a + b) - customBxor1(a,b)) / 2 + if c then + z = customBand(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return MODM + end +end + + +local function bnot(x) + return (-1 - x) % MOD +end + + +local function rshift1(a, disp) + if disp < 0 then + return lshift(a, -disp) + end + return math.floor(a % 2 ^ 32 / 2 ^ disp) +end + + +local function rshift(x, disp) + if disp > 31 or disp < -31 then + return 0 + end + return rshift1(x % MOD, disp) +end + + +local function lshift(a, disp) + if disp < 0 then + return rshift(a, -disp) + end + return (a * 2 ^ disp) % 2 ^ 32 +end + + +local function rrotate(x, disp) + x = x % MOD + disp = disp % 32 + local low = customBand(x, 2 ^ disp - 1) + return rshift(x, disp) + lshift(low, 32 - disp) +end + + +local k = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +} + + +local function str2hexa(s) + return (string.gsub(s, ".", function(c) + return string.format("%02x", string.byte(c)) + end)) +end + + +local function num2s(l, n) + local s = "" + for i = 1, n do + local rem = l % 256 + s = string.char(rem) .. s + l = (l - rem) / 256 + end + return s +end + + +local function s232num(s, i) + local n = 0 + for i = i, i + 3 do + n = n*256 + string.byte(s, i) + end + return n +end + + +local function preproc(msg, len) + local extra = 64 - ((len + 9) % 64) + len = num2s(8 * len, 8) + msg = msg .. "\128" .. string.rep("\0", extra) .. len + assert(#msg % 64 == 0) + return msg +end + + +local function initH256(H) + H[1] = 0x6a09e667 + H[2] = 0xbb67ae85 + H[3] = 0x3c6ef372 + H[4] = 0xa54ff53a + H[5] = 0x510e527f + H[6] = 0x9b05688c + H[7] = 0x1f83d9ab + H[8] = 0x5be0cd19 + return H +end + + +local function digestblock(msg, i, H) + local w = {} + for j = 1, 16 do + w[j] = s232num(msg, i + (j - 1)*4) + end + for j = 17, 64 do + local v = w[j - 15] + local s0 = customBxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3)) + v = w[j - 2] + w[j] = w[j - 16] + s0 + w[j - 7] + customBxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10)) + end + + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for i = 1, 64 do + local s0 = customBxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22)) + local maj = customBxor(customBand(a, b), customBand(a, c), customBand(b, c)) + local t2 = s0 + maj + local s1 = customBxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25)) + local ch = customBxor (customBand(e, f), customBand(bnot(e), g)) + local t1 = h + s1 + ch + k[i] + w[i] + h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2 + end + + H[1] = customBand(H[1] + a) + H[2] = customBand(H[2] + b) + H[3] = customBand(H[3] + c) + H[4] = customBand(H[4] + d) + H[5] = customBand(H[5] + e) + H[6] = customBand(H[6] + f) + H[7] = customBand(H[7] + g) + H[8] = customBand(H[8] + h) +end + + +local function sha256(msg) + msg = preproc(msg, #msg) + local H = initH256({}) + for i = 1, #msg, 64 do + digestblock(msg, i, H) + end + return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) .. + num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4)) +end + + +local protocolName = "Firewolf" + + + +-- Cryptography + + +local Cryptography = {} +Cryptography.sha = {} +Cryptography.base64 = {} +Cryptography.aes = {} + + +function Cryptography.bytesFromMessage(msg) + local bytes = {} + + for i = 1, msg:len() do + local letter = string.byte(msg:sub(i, i)) + table.insert(bytes, letter) + end + + return bytes +end + + +function Cryptography.messageFromBytes(bytes) + local msg = "" + + for i = 1, #bytes do + local letter = string.char(bytes[i]) + msg = msg .. letter + end + + return msg +end + + +function Cryptography.bytesFromKey(key) + local bytes = {} + + for i = 1, key:len() / 2 do + local group = key:sub((i - 1) * 2 + 1, (i - 1) * 2 + 1) + local num = tonumber(group, 16) + table.insert(bytes, num) + end + + return bytes +end + + +function Cryptography.sha.sha256(msg) + return sha256(msg) +end + + +function Cryptography.aes.encrypt(msg, key) + return base64Encode(crypt(msg, key)) +end + + +function Cryptography.aes.decrypt(msg, key) + return crypt(base64Decode(msg), key) +end + + +function Cryptography.base64.encode(msg) + return base64Encode(Cryptography.bytesFromMessage(msg)) +end + + +function Cryptography.base64.decode(msg) + return Cryptography.messageFromBytes(base64Decode(msg)) +end + + +function Cryptography.channel(text) + local hashed = Cryptography.sha.sha256(text) + + local total = 0 + + for i = 1, hashed:len() do + total = total + string.byte(hashed:sub(i, i)) + end + + return (total % 55530) + 10000 +end + + +function Cryptography.sanatize(text) + local sanatizeChars = {"%", "(", ")", "[", "]", ".", "+", "-", "*", "?", "^", "$"} + + for _, char in pairs(sanatizeChars) do + text = text:gsub("%"..char, "%%%"..char) + end + return text +end + + + +-- Modem + + +local Modem = {} +Modem.modems = {} + + +function Modem.exists() + Modem.exists = false + for _, side in pairs(rs.getSides()) do + if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then + Modem.exists = true + + if not Modem.modems[side] then + Modem.modems[side] = peripheral.wrap(side) + end + end + end + + return Modem.exists +end + + +function Modem.open(channel) + if not Modem.exists then + return false + end + + for side, modem in pairs(Modem.modems) do + modem.open(channel) + rednet.open(side) + end + + return true +end + + +function Modem.close(channel) + if not Modem.exists then + return false + end + + for side, modem in pairs(Modem.modems) do + modem.close(channel) + end + + return true +end + + +function Modem.closeAll() + if not Modem.exists then + return false + end + + for side, modem in pairs(Modem.modems) do + modem.closeAll() + end + + return true +end + + +function Modem.isOpen(channel) + if not Modem.exists then + return false + end + + local isOpen = false + for side, modem in pairs(Modem.modems) do + if modem.isOpen(channel) then + isOpen = true + break + end + end + + return isOpen +end + + +function Modem.transmit(channel, msg) + if not Modem.exists then + return false + end + + if not Modem.isOpen(channel) then + Modem.open(channel) + end + + for side, modem in pairs(Modem.modems) do + modem.transmit(channel, channel, msg) + end + + return true +end + + + +-- Handshake + + +local Handshake = {} + +Handshake.prime = 625210769 +Handshake.channel = 54569 +Handshake.base = -1 +Handshake.secret = -1 +Handshake.sharedSecret = -1 +Handshake.packetHeader = "["..protocolName.."-Handshake-Packet-Header]" +Handshake.packetMatch = "%["..protocolName.."%-Handshake%-Packet%-Header%](.+)" + + +function Handshake.exponentWithModulo(base, exponent, modulo) + local remainder = base + + for i = 1, exponent-1 do + remainder = remainder * remainder + if remainder >= modulo then + remainder = remainder % modulo + end + end + + return remainder +end + + +function Handshake.clear() + Handshake.base = -1 + Handshake.secret = -1 + Handshake.sharedSecret = -1 +end + + +function Handshake.generateInitiatorData() + Handshake.base = math.random(10,99999) + Handshake.secret = math.random(10,99999) + return { + type = "initiate", + prime = Handshake.prime, + base = Handshake.base, + moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime) + } +end + + +function Handshake.generateResponseData(initiatorData) + local isPrimeANumber = type(initiatorData.prime) == "number" + local isPrimeMatching = initiatorData.prime == Handshake.prime + local isBaseANumber = type(initiatorData.base) == "number" + local isInitiator = initiatorData.type == "initiate" + local isModdedSecretANumber = type(initiatorData.moddedSecret) == "number" + local areAllNumbersNumbers = isPrimeANumber and isBaseANumber and isModdedSecretANumber + + if areAllNumbersNumbers and isPrimeMatching then + if isInitiator then + Handshake.base = initiatorData.base + Handshake.secret = math.random(10,99999) + Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime) + return { + type = "response", + prime = Handshake.prime, + base = Handshake.base, + moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime) + }, Handshake.sharedSecret + elseif initiatorData.type == "response" and Handshake.base > 0 and Handshake.secret > 0 then + Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime) + return Handshake.sharedSecret + else + return false + end + else + return false + end +end + + + +-- Secure Connection + + +local SecureConnection = {} +SecureConnection.__index = SecureConnection + + +SecureConnection.packetHeaderA = "["..protocolName.."-" +SecureConnection.packetHeaderB = "-SecureConnection-Packet-Header]" +SecureConnection.packetMatchA = "%["..protocolName.."%-" +SecureConnection.packetMatchB = "%-SecureConnection%-Packet%-Header%](.+)" +SecureConnection.connectionTimeout = 0.1 +SecureConnection.successPacketTimeout = 0.1 + + +function SecureConnection.new(secret, key, identifier, distance, isRednet) + local self = setmetatable({}, SecureConnection) + self:setup(secret, key, identifier, distance, isRednet) + return self +end + + +function SecureConnection:setup(secret, key, identifier, distance, isRednet) + local rawSecret + + if isRednet then + self.isRednet = true + self.distance = -1 + self.rednet_id = distance + rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) .. + "|" .. tostring(key) .. "|rednet" + else + self.isRednet = false + self.distance = distance + rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) .. + "|" .. tostring(key) .. "|" .. tostring(distance) + end + + self.identifier = identifier + self.packetMatch = SecureConnection.packetMatchA .. Cryptography.sanatize(identifier) .. SecureConnection.packetMatchB + self.packetHeader = SecureConnection.packetHeaderA .. identifier .. SecureConnection.packetHeaderB + self.secret = Cryptography.sha.sha256(rawSecret) + self.channel = Cryptography.channel(self.secret) + + if not self.isRednet then + Modem.open(self.channel) + end +end + + +function SecureConnection:verifyHeader(msg) + if msg:match(self.packetMatch) then + return true + else + return false + end +end + + +function SecureConnection:sendMessage(msg, rednetProtocol) + local rawEncryptedMsg = Cryptography.aes.encrypt(self.packetHeader .. msg, self.secret) + local encryptedMsg = self.packetHeader .. rawEncryptedMsg + + if self.isRednet then + rednet.send(self.rednet_id, encryptedMsg, rednetProtocol) + return true + else + return Modem.transmit(self.channel, encryptedMsg) + end +end + + +function SecureConnection:decryptMessage(msg) + if self:verifyHeader(msg) then + local encrypted = msg:match(self.packetMatch) + + local unencryptedMsg = nil + pcall(function() unencryptedMsg = Cryptography.aes.decrypt(encrypted, self.secret) end) + if not unencryptedMsg then + return false, "Could not decrypt" + end + + if self:verifyHeader(unencryptedMsg) then + return true, unencryptedMsg:match(self.packetMatch) + else + return false, "Could not verify" + end + else + return false, "Could not stage 1 verify" + end +end + + + +-- RDNT Protocol + + +protocols["rdnt"] = {} + +local header = {} +header.dnsPacket = "[Firewolf-DNS-Packet]" +header.dnsHeaderMatch = "^%[Firewolf%-DNS%-Response%](.+)$" +header.rednetHeader = "[Firewolf-Rednet-Channel-Simulation]" +header.rednetMatch = "^%[Firewolf%-Rednet%-Channel%-Simulation%](%d+)$" +header.responseMatchA = "^%[Firewolf%-" +header.responseMatchB = "%-" +header.responseMatchC = "%-Handshake%-Response%](.+)$" +header.requestHeaderA = "[Firewolf-" +header.requestHeaderB = "-Handshake-Request]" +header.pageRequestHeaderA = "[Firewolf-" +header.pageRequestHeaderB = "-Page-Request]" +header.pageResponseMatchA = "^%[Firewolf%-" +header.pageResponseMatchB = "%-Page%-Response%]%[HEADER%](.-)%[BODY%](.+)$" +header.closeHeaderA = "[Firewolf-" +header.closeHeaderB = "-Connection-Close]" + + +protocols["rdnt"]["setup"] = function() + if not Modem.exists() then + error("No modem found!") + end +end + + +protocols["rdnt"]["fetchAllSearchResults"] = function() + Modem.open(publicDNSChannel) + Modem.open(publicResponseChannel) + Modem.transmit(publicDNSChannel, header.dnsPacket) + Modem.close(publicDNSChannel) + + rednet.broadcast(header.dnsPacket, header.rednetHeader .. publicDNSChannel) + + local uniqueServers = {} + local uniqueDomains = {} + + local timer = os.startTimer(searchResultTimeout) + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "modem_message" then + if channel == publicResponseChannel and message:match(header.dnsHeaderMatch) then + if not uniqueServers[tostring(dist)] then + uniqueServers[tostring(dist)] = true + local domain = message:match(header.dnsHeaderMatch) + if not uniqueDomains[domain] then + if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then + timer = os.startTimer(searchResultTimeout) + uniqueDomains[message:match(header.dnsHeaderMatch)] = tostring(dist) + end + end + end + end + elseif event == "rednet_message" and allowUnencryptedConnections then + if protocol and tonumber(protocol:match(header.rednetMatch)) == publicResponseChannel and channel:match(header.dnsHeaderMatch) then + if not uniqueServers[tostring(id)] then + uniqueServers[tostring(id)] = true + local domain = channel:match(header.dnsHeaderMatch) + if not uniqueDomains[domain] then + if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then + timer = os.startTimer(searchResultTimeout) + uniqueDomains[domain] = tostring(id) + end + end + end + end + elseif event == "timer" and id == timer then + local results = {} + for k, _ in pairs(uniqueDomains) do + table.insert(results, k) + end + + return results + end + end +end + + +protocols["rdnt"]["fetchConnectionObject"] = function(url) + local serverChannel = Cryptography.channel(url) + local requestHeader = header.requestHeaderA .. url .. header.requestHeaderB + local responseMatch = header.responseMatchA .. Cryptography.sanatize(url) .. header.responseMatchB + + local serializedHandshake = textutils.serialize(Handshake.generateInitiatorData()) + + local rednetResults = {} + local directResults = {} + + local disconnectOthers = function(ignoreDirect) + for k,v in pairs(rednetResults) do + v.close() + end + for k,v in pairs(directResults) do + if k ~= ignoreDirect then + v.close() + end + end + end + + local timer = os.startTimer(initiationTimeout) + + Modem.open(serverChannel) + Modem.transmit(serverChannel, requestHeader .. serializedHandshake) + + rednet.broadcast(requestHeader .. serializedHandshake, header.rednetHeader .. serverChannel) + + -- Extendable to have server selection + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "modem_message" then + local fullMatch = responseMatch .. tostring(dist) .. header.responseMatchC + if channel == serverChannel and message:match(fullMatch) and type(textutils.unserialize(message:match(fullMatch))) == "table" then + local key = Handshake.generateResponseData(textutils.unserialize(message:match(fullMatch))) + if key then + local connection = SecureConnection.new(key, url, url, dist) + table.insert(directResults, { + connection = connection, + fetchPage = function(page) + if not connection then + return nil + end + + local fetchTimer = os.startTimer(fetchTimeout) + + local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page + local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB + + connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel) + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "modem_message" and channel == connection.channel and connection:verifyHeader(message) then + local resp, data = connection:decryptMessage(message) + if not resp then + -- Decryption error + elseif data and data ~= page then + if data:match(pageResponseMatch) then + local head, body = data:match(pageResponseMatch) + return body, textutils.unserialize(head) + end + end + elseif event == "timer" and id == fetchTimer then + return nil + end + end + end, + close = function() + if connection ~= nil then + connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel) + Modem.close(connection.channel) + connection = nil + end + end + }) + + disconnectOthers(1) + return directResults[1] + end + end + elseif event == "rednet_message" then + local fullMatch = responseMatch .. os.getComputerID() .. header.responseMatchC + if protocol and tonumber(protocol:match(header.rednetMatch)) == serverChannel and channel:match(fullMatch) and type(textutils.unserialize(channel:match(fullMatch))) == "table" then + local key = Handshake.generateResponseData(textutils.unserialize(channel:match(fullMatch))) + if key then + local connection = SecureConnection.new(key, url, url, id, true) + table.insert(rednetResults, { + connection = connection, + fetchPage = function(page) + if not connection then + return nil + end + + local fetchTimer = os.startTimer(fetchTimeout) + + local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page + local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB + + connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel) + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "rednet_message" and protocol and tonumber(protocol:match(header.rednetMatch)) == connection.channel and connection:verifyHeader(channel) then + local resp, data = connection:decryptMessage(channel) + if not resp then + -- Decryption error + elseif data and data ~= page then + if data:match(pageResponseMatch) then + local head, body = data:match(pageResponseMatch) + return body, textutils.unserialize(head) + end + end + elseif event == "timer" and id == fetchTimer then + return nil + end + end + end, + close = function() + connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel) + Modem.close(connection.channel) + connection = nil + end + }) + + if #rednetResults == 1 then + timer = os.startTimer(0.2) + end + end + end + elseif event == "timer" and id == timer then + -- Return + if #directResults > 0 then + disconnectOthers(1) + return directResults[1] + elseif #rednetResults > 0 then + local lowestID = math.huge + local lowestResult = nil + for k,v in pairs(rednetResults) do + if v.connection.rednet_id < lowestID then + lowestID = v.connection.rednet_id + lowestResult = v + end + end + + for k,v in pairs(rednetResults) do + if v.connection.rednet_id ~= lowestID then + v.close() + end + end + + return lowestResult + else + return nil + end + end + end +end + + + +-- Fetching Raw Data + + +local fetchSearchResultsForQuery = function(query) + local all = protocols[currentProtocol]["fetchAllSearchResults"]() + local results = {} + if query and query:len() > 0 then + for _, v in pairs(all) do + if v:find(query:lower()) then + table.insert(results, v) + end + end + else + results = all + end + + table.sort(results) + return results +end + + +local getConnectionObjectFromURL = function(url) + local domain = url:match("^([^/]+)") + return protocols[currentProtocol]["fetchConnectionObject"](domain) +end + + +local determineLanguage = function(header) + if type(header) == "table" then + if header.language and header.language == "Firewolf Markup" then + return "fwml" + else + return "lua" + end + else + return "lua" + end +end + + + +-- History + + +local appendToHistory = function(url) + if history[1] ~= url then + table.insert(history, 1, url) + end +end + + + +-- Fetch Websites + + +local loadingAnimation = function() + local state = -2 + + term.setTextColor(theme.text) + term.setBackgroundColor(theme.accent) + + term.setCursorPos(w - 5, 1) + term.write("[= ]") + + local timer = os.startTimer(animationInterval) + + while true do + local event, timerID = os.pullEvent() + if event == "timer" and timerID == timer then + term.setTextColor(theme.text) + term.setBackgroundColor(theme.accent) + + state = state + 1 + + term.setCursorPos(w - 5, 1) + term.write("[ ]") + term.setCursorPos(w - 2 - math.abs(state), 1) + term.write("=") + + if state == 2 then + state = -2 + end + + timer = os.startTimer(animationInterval) + end + end +end + + +local normalizeURL = function(url) + url = url:lower():gsub(" ", "") + if url == "home" or url == "homepage" then + url = "firewolf" + end + + return url +end + + +local normalizePage = function(page) + if not page then page = "" end + page = page:lower() + if page == "" then + page = "/" + end + return page +end + + +local determineActionForURL = function(url) + if url:len() > 0 and url:gsub("/", ""):len() == 0 then + return "none" + end + + if url == "exit" then + return "exit" + elseif builtInSites["display"][url] then + return "internal website" + elseif url == "" then + local results = fetchSearchResultsForQuery() + if #results > 0 then + return "search", results + else + return "none" + end + else + local connection = getConnectionObjectFromURL(url) + if connection then + return "external website", connection + else + local results = fetchSearchResultsForQuery(url) + if #results > 0 then + return "search", results + else + return "none" + end + end + end +end + + +local fetchSearch = function(url, results) + return languages["lua"]["runWithoutAntivirus"](builtInSites["search"], results) +end + + +local fetchInternal = function(url) + return languages["lua"]["runWithoutAntivirus"](builtInSites["display"][url]) +end + + +local fetchError = function(err) + return languages["lua"]["runWithoutAntivirus"](builtInSites["error"], err) +end + + +local fetchExternal = function(url, connection) + if connection.multipleServers then + -- Please forgive me + -- GravityScore forced me to do it like this + -- I don't mean it, I really don't. + connection = connection.servers[1] + end + + local page = normalizePage(url:match("^[^/]+/(.+)")) + local contents, head = connection.fetchPage(page) + if contents then + if type(contents) ~= "string" then + return fetchNone() + else + local language = determineLanguage(head) + return languages[language]["run"](contents, page, connection) + end + else + if connection then + connection.close() + return "retry" + end + return fetchError("A connection error/timeout has occurred!") + end +end + + +local fetchNone = function() + return languages["lua"]["runWithoutAntivirus"](builtInSites["noresults"]) +end + + +local fetchURL = function(url, inheritConnection) + url = normalizeURL(url) + currentWebsiteURL = url + + if inheritConnection then + local resp = fetchExternal(url, inheritConnection) + if resp ~= "retry" then + return resp, false, inheritConnection + end + end + + local action, connection = determineActionForURL(url) + + if action == "search" then + return fetchSearch(url, connection), true + elseif action == "internal website" then + return fetchInternal(url), true + elseif action == "external website" then + local resp = fetchExternal(url, connection) + if resp == "retry" then + return fetchError("A connection error/timeout has occurred!"), false, connection + else + return resp, false, connection + end + elseif action == "none" then + return fetchNone(), true + elseif action == "exit" then + os.queueEvent("terminate") + end + + return nil +end + + + +-- Tabs + + +local switchTab = function(index, shouldntResume) + if not tabs[index] then + return + end + + if tabs[currentTab].win then + tabs[currentTab].win.setVisible(false) + end + + currentTab = index + isMenubarOpen = tabs[currentTab].isMenubarOpen + currentWebsiteURL = tabs[currentTab].url + + term.redirect(originalTerminal) + clear(theme.background, theme.text) + drawMenubar() + + term.redirect(tabs[currentTab].win) + term.setCursorPos(1, 1) + tabs[currentTab].win.setVisible(true) + tabs[currentTab].win.redraw() + + if not shouldntResume then + coroutine.resume(tabs[currentTab].thread) + end +end + + +local closeCurrentTab = function() + if #tabs <= 0 then + return + end + + table.remove(tabs, currentTab) + + currentTab = math.max(currentTab - 1, 1) + switchTab(currentTab, true) +end + + +local loadTab = function(index, url, givenFunc) + url = normalizeURL(url) + + local func = nil + local isOpen = true + local currentConnection = false + + isMenubarOpen = true + currentWebsiteURL = url + drawMenubar() + + if tabs[index] and tabs[index].connection and tabs[index].url then + if url:match("^([^/]+)") == tabs[index].url:match("^([^/]+)") then + currentConnection = tabs[index].connection + else + tabs[index].connection.close() + tabs[index].connection = nil + end + end + + if givenFunc then + func = givenFunc + else + parallel.waitForAny(function() + func, isOpen, connection = fetchURL(url, currentConnection) + end, function() + while true do + local event, key = os.pullEvent() + if event == "key" and (key == 29 or key == 157) then + break + end + end + end, loadingAnimation) + end + + if func then + appendToHistory(url) + + tabs[index] = {} + tabs[index].url = url + tabs[index].connection = connection + tabs[index].win = window.create(originalTerminal, 1, 1, w, h, false) + + tabs[index].thread = coroutine.create(func) + tabs[index].isMenubarOpen = isOpen + tabs[index].isMenubarPermanent = isOpen + + tabs[index].ox = 1 + tabs[index].oy = 1 + + term.redirect(tabs[index].win) + clear(theme.background, theme.text) + + switchTab(index) + end +end + + + +-- Website Environments + + +local getWhitelistedEnvironment = function() + local env = {} + + local function copy(source, destination, key) + destination[key] = {} + for k, v in pairs(source) do + destination[key][k] = v + end + end + + copy(bit, env, "bit") + copy(colors, env, "colors") + copy(colours, env, "colours") + copy(coroutine, env, "coroutine") + + copy(disk, env, "disk") + env["disk"]["setLabel"] = nil + env["disk"]["eject"] = nil + + copy(gps, env, "gps") + copy(help, env, "help") + copy(keys, env, "keys") + copy(math, env, "math") + + copy(os, env, "os") + env["os"]["run"] = nil + env["os"]["shutdown"] = nil + env["os"]["reboot"] = nil + env["os"]["setComputerLabel"] = nil + env["os"]["queueEvent"] = nil + env["os"]["pullEvent"] = function(filter) + while true do + local event = {os.pullEvent(filter)} + if not filter then + return unpack(event) + elseif filter and event[1] == filter then + return unpack(event) + end + end + end + env["os"]["pullEventRaw"] = env["os"]["pullEvent"] + + copy(paintutils, env, "paintutils") + copy(parallel, env, "parallel") + copy(peripheral, env, "peripheral") + copy(rednet, env, "rednet") + copy(redstone, env, "redstone") + copy(redstone, env, "rs") + + copy(shell, env, "shell") + env["shell"]["run"] = nil + env["shell"]["exit"] = nil + env["shell"]["setDir"] = nil + env["shell"]["setAlias"] = nil + env["shell"]["clearAlias"] = nil + env["shell"]["setPath"] = nil + + copy(string, env, "string") + copy(table, env, "table") + + copy(term, env, "term") + env["term"]["redirect"] = nil + env["term"]["restore"] = nil + + copy(textutils, env, "textutils") + copy(vector, env, "vector") + + if turtle then + copy(turtle, env, "turtle") + end + + if http then + copy(http, env, "http") + end + + env["assert"] = assert + env["printError"] = printError + env["tonumber"] = tonumber + env["tostring"] = tostring + env["type"] = type + env["next"] = next + env["unpack"] = unpack + env["pcall"] = pcall + env["xpcall"] = xpcall + env["sleep"] = sleep + env["pairs"] = pairs + env["ipairs"] = ipairs + env["read"] = read + env["write"] = write + env["select"] = select + env["print"] = print + env["setmetatable"] = setmetatable + env["getmetatable"] = getmetatable + + env["_G"] = env + + return env +end + + +local overrideEnvironment = function(env) + local localTerm = {} + for k, v in pairs(term) do + localTerm[k] = v + end + + env["term"]["clear"] = function() + localTerm.clear() + drawMenubar() + end + + env["term"]["scroll"] = function(n) + localTerm.scroll(n) + drawMenubar() + end + + env["shell"]["getRunningProgram"] = function() + return currentWebsiteURL + end +end + +local urlEncode = function(url) + local result = url + + result = result:gsub("%%", "%%a") + result = result:gsub(":", "%%c") + result = result:gsub("/", "%%s") + result = result:gsub("\n", "%%n") + result = result:gsub(" ", "%%w") + result = result:gsub("&", "%%m") + result = result:gsub("%?", "%%q") + result = result:gsub("=", "%%e") + result = result:gsub("%.", "%%d") + + return result +end + +local urlDecode = function(url) + local result = url + + result = result:gsub("%%c", ":") + result = result:gsub("%%s", "/") + result = result:gsub("%%n", "\n") + result = result:gsub("%%w", " ") + result = result:gsub("%%&", "&") + result = result:gsub("%%q", "%?") + result = result:gsub("%%e", "=") + result = result:gsub("%%d", "%.") + result = result:gsub("%%m", "%%") + + return result +end + +local applyAPIFunctions = function(env, connection) + env["firewolf"] = {} + env["firewolf"]["version"] = version + env["firewolf"]["domain"] = currentWebsiteURL:match("^[^/]+") + + env["firewolf"]["redirect"] = function(url) + if type(url) ~= "string" then + return error("string (url) expected, got " .. type(url)) + end + + os.queueEvent(redirectEvent, url) + coroutine.yield() + end + + env["firewolf"]["download"] = function(page) + if type(page) ~= "string" then + return error("string (page) expected") + end + local bannedNames = {"ls", "dir", "delete", "copy", "move", "list", "rm", "cp", "mv", "clear", "cd", "lua"} + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local filename = page:match("([^/]+)$") + if not filename then + return false, "Cannot download index" + end + + for k, v in pairs(bannedNames) do + if filename == v then + return false, "Filename prohibited!" + end + end + + if not fs.exists(downloadsLocation) then + fs.makeDir(downloadsLocation) + elseif not fs.isDir(downloadsLocation) then + return false, "Downloads disabled!" + end + + contents = connection.fetchPage(normalizePage(page)) + if type(contents) ~= "string" then + return false, "Download error!" + else + local f = io.open(downloadsLocation .. "/" .. filename, "w") + f:write(contents) + f:close() + return true, downloadsLocation .. "/" .. filename + end + end + + env["firewolf"]["encode"] = function(vars) + if type(vars) ~= "table" then + return error("table (vars) expected, got " .. type(vars)) + end + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local construct = "?" + for k,v in pairs(vars) do + construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&" + end + -- Get rid of that last ampersand + construct = construct:sub(1, -2) + + return construct + end + + env["firewolf"]["query"] = function(page, vars) + if type(page) ~= "string" then + return error("string (page) expected, got " .. type(page)) + end + if vars and type(vars) ~= "table" then + return error("table (vars) expected, got " .. type(vars)) + end + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local construct = page .. "?" + if vars then + for k,v in pairs(vars) do + construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&" + end + end + -- Get rid of that last ampersand + construct = construct:sub(1, -2) + + contents = connection.fetchPage(normalizePage(construct)) + if type(contents) == "string" then + return contents + else + return false + end + end + + env["firewolf"]["loadImage"] = function(page) + if type(page) ~= "string" then + return error("string (page) expected, got " .. type(page)) + end + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local filename = page:match("([^/]+)$") + if not filename then + return false, "Cannot load index as an image!" + end + + contents = connection.fetchPage(normalizePage(page)) + if type(contents) ~= "string" then + return false, "Download error!" + else + local colorLookup = {} + for n = 1, 16 do + colorLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) + end + + local image = {} + for line in contents:gmatch("[^\n]+") do + local lines = {} + for x = 1, line:len() do + lines[x] = colorLookup[string.byte(line, x, x)] or 0 + end + table.insert(image, lines) + end + + return image + end + end + + env["center"] = center + env["fill"] = fill +end + + +local getWebsiteEnvironment = function(antivirus, connection) + local env = {} + + if antivirus then + env = getWhitelistedEnvironment() + overrideEnvironment(env) + else + setmetatable(env, {__index = _G}) + end + + applyAPIFunctions(env, connection) + + return env +end + + + +-- FWML Execution + + +local render = {} + +render["functions"] = {} +render["functions"]["public"] = {} +render["alignations"] = {} + +render["variables"] = { + scroll, + maxScroll, + align, + linkData = {}, + blockLength, + link, + linkStart, + markers, + currentOffset, +} + + +local function getLine(loc, data) + local _, changes = data:sub(1, loc):gsub("\n", "") + if not changes then + return 1 + else + return changes + 1 + end +end + + +local function parseData(data) + local commands = {} + local searchPos = 1 + + while #data > 0 do + local sCmd, eCmd = data:find("%[[^%]]+%]", searchPos) + if sCmd then + sCmd = sCmd + 1 + eCmd = eCmd - 1 + + if (sCmd > 2) then + if data:sub(sCmd - 2, sCmd - 2) == "\\" then + local t = data:sub(searchPos, sCmd - 1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]") + if #t > 0 then + if #commands > 0 and type(commands[#commands][1]) == "string" then + commands[#commands][1] = commands[#commands][1] .. t + else + table.insert(commands, {t}) + end + end + searchPos = sCmd + else + local t = data:sub(searchPos, sCmd - 2):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]") + if #t > 0 then + if #commands > 0 and type(commands[#commands][1]) == "string" then + commands[#commands][1] = commands[#commands][1] .. t + else + table.insert(commands, {t}) + end + end + + t = data:sub(sCmd, eCmd):gsub("\n", "") + table.insert(commands, {getLine(sCmd, data), t}) + searchPos = eCmd + 2 + end + else + local t = data:sub(sCmd, eCmd):gsub("\n", "") + table.insert(commands, {getLine(sCmd, data), t}) + searchPos = eCmd + 2 + end + else + local t = data:sub(searchPos, -1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]") + if #t > 0 then + if #commands > 0 and type(commands[#commands][1]) == "string" then + commands[#commands][1] = commands[#commands][1] .. t + else + table.insert(commands, {t}) + end + end + + break + end + end + + return commands +end + + +local function proccessData(commands) + searchIndex = 0 + + while searchIndex < #commands do + searchIndex = searchIndex + 1 + + local length = 0 + local origin = searchIndex + + if type(commands[searchIndex][1]) == "string" then + length = length + #commands[searchIndex][1] + local endIndex = origin + for i = origin + 1, #commands do + if commands[i][2] then + local command = commands[i][2]:match("^(%w+)%s-") + if not (command == "c" or command == "color" or command == "bg" + or command == "background" or command == "newlink" or command == "endlink") then + endIndex = i + break + end + elseif commands[i][2] then + + else + length = length + #commands[i][1] + end + if i == #commands then + endIndex = i + end + end + + commands[origin][2] = length + searchIndex = endIndex + length = 0 + end + end + + return commands +end + + +local function parse(original) + return proccessData(parseData(original)) +end + + +render["functions"]["display"] = function(text, length, offset, center) + if not offset then + offset = 0 + end + + return render.variables.align(text, length, w, offset, center); +end + + +render["functions"]["displayText"] = function(source) + if source[2] then + render.variables.blockLength = source[2] + if render.variables.link and not render.variables.linkStart then + render.variables.linkStart = render.functions.display( + source[1], render.variables.blockLength, render.variables.currentOffset, w / 2) + else + render.functions.display(source[1], render.variables.blockLength, render.variables.currentOffset, w / 2) + end + else + if render.variables.link and not render.variables.linkStart then + render.variables.linkStart = render.functions.display(source[1], nil, render.variables.currentOffset, w / 2) + else + render.functions.display(source[1], nil, render.variables.currentOffset, w / 2) + end + end +end + + +render["functions"]["public"]["br"] = function(source) + if render.variables.link then + return "Cannot insert new line within a link on line " .. source[1] + end + + render.variables.scroll = render.variables.scroll + 1 + render.variables.maxScroll = math.max(render.variables.scroll, render.variables.maxScroll) +end + + +render["functions"]["public"]["c "] = function(source) + local sColor = source[2]:match("^%w+%s+(.+)$") or "" + if colors[sColor] then + term.setTextColor(colors[sColor]) + else + return "Invalid color: \"" .. sColor .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["color "] = render["functions"]["public"]["c "] + + +render["functions"]["public"]["bg "] = function(source) + local sColor = source[2]:match("^%w+%s+(.+)$") or "" + if colors[sColor] then + term.setBackgroundColor(colors[sColor]) + else + return "Invalid color: \"" .. sColor .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["background "] = render["functions"]["public"]["bg "] + + +render["functions"]["public"]["newlink "] = function(source) + if render.variables.link then + return "Cannot nest links on line " .. source[1] + end + + render.variables.link = source[2]:match("^%w+%s+(.+)$") or "" + render.variables.linkStart = false +end + + +render["functions"]["public"]["endlink"] = function(source) + if not render.variables.link then + return "Cannot end a link without a link on line " .. source[1] + end + + local linkEnd = term.getCursorPos()-1 + table.insert(render.variables.linkData, {render.variables.linkStart, + linkEnd, render.variables.scroll, render.variables.link}) + render.variables.link = false + render.variables.linkStart = false +end + + +render["functions"]["public"]["offset "] = function(source) + local offset = tonumber((source[2]:match("^%w+%s+(.+)$") or "")) + if offset then + render.variables.currentOffset = offset + else + return "Invalid offset value: \"" .. (source[2]:match("^%w+%s+(.+)$") or "") .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["marker "] = function(source) + render.variables.markers[(source[2]:match("^%w+%s+(.+)$") or "")] = render.variables.scroll +end + + +render["functions"]["public"]["goto "] = function(source) + local location = source[2]:match("%w+%s+(.+)$") + if render.variables.markers[location] then + render.variables.scroll = render.variables.markers[location] + else + return "No such location: \"" .. (source[2]:match("%w+%s+(.+)$") or "") .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["box "] = function(source) + local sColor, align, height, width, offset, url = source[2]:match("^box (%a+) (%a+) (%-?%d+) (%-?%d+) (%-?%d+) ?([^ ]*)") + if not sColor then + return "Invalid box syntax on line " .. source[1] + end + + local x, y = term.getCursorPos() + local startX + + if align == "center" or align == "centre" then + startX = math.ceil((w / 2) - width / 2) + offset + elseif align == "left" then + startX = 1 + offset + elseif align == "right" then + startX = (w - width + 1) + offset + else + return "Invalid align option for box on line " .. source[1] + end + + if not colors[sColor] then + return "Invalid color: \"" .. sColor .. "\" for box on line " .. source[1] + end + + term.setBackgroundColor(colors[sColor]) + for i = 0, height - 1 do + term.setCursorPos(startX, render.variables.scroll + i) + term.write(string.rep(" ", width)) + if url:len() > 3 then + table.insert(render.variables.linkData, {startX, startX + width - 1, render.variables.scroll + i, url}) + end + end + + render.variables.maxScroll = math.max(render.variables.scroll + height - 1, render.variables.maxScroll) + term.setCursorPos(x, y) +end + + +render["alignations"]["left"] = function(text, length, _, offset) + local x, y = term.getCursorPos() + if length then + term.setCursorPos(1 + offset, render.variables.scroll) + term.write(text) + return 1 + offset + else + term.setCursorPos(x, render.variables.scroll) + term.write(text) + return x + end +end + + +render["alignations"]["right"] = function(text, length, width, offset) + local x, y = term.getCursorPos() + if length then + term.setCursorPos((width - length + 1) + offset, render.variables.scroll) + term.write(text) + return (width - length + 1) + offset + else + term.setCursorPos(x, render.variables.scroll) + term.write(text) + return x + end +end + + +render["alignations"]["center"] = function(text, length, _, offset, center) + local x, y = term.getCursorPos() + if length then + term.setCursorPos(math.ceil(center - length / 2) + offset, render.variables.scroll) + term.write(text) + return math.ceil(center - length / 2) + offset + else + term.setCursorPos(x, render.variables.scroll) + term.write(text) + return x + end +end + + +render["render"] = function(data, startScroll) + if startScroll == nil then + render.variables.startScroll = 0 + else + render.variables.startScroll = startScroll + end + + render.variables.scroll = startScroll + 1 + render.variables.maxScroll = render.variables.scroll + + render.variables.linkData = {} + + render.variables.align = render.alignations.left + + render.variables.blockLength = 0 + render.variables.link = false + render.variables.linkStart = false + render.variables.markers = {} + render.variables.currentOffset = 0 + + for k, v in pairs(data) do + if type(v[2]) ~= "string" then + render.functions.displayText(v) + elseif v[2] == "<" or v[2] == "left" then + render.variables.align = render.alignations.left + elseif v[2] == ">" or v[2] == "right" then + render.variables.align = render.alignations.right + elseif v[2] == "=" or v[2] == "center" then + render.variables.align = render.alignations.center + else + local existentFunction = false + + for name, func in pairs(render.functions.public) do + if v[2]:find(name) == 1 then + existentFunction = true + local ret = func(v) + if ret then + return ret + end + end + end + + if not existentFunction then + return "Non-existent tag: \"" .. v[2] .. "\" on line " .. v[1] + end + end + end + + return render.variables.linkData, render.variables.maxScroll - render.variables.startScroll +end + + + +-- Lua Execution + + +languages["lua"] = {} +languages["fwml"] = {} + + +languages["lua"]["runWithErrorCatching"] = function(func, ...) + local _, err = pcall(func, ...) + if err then + os.queueEvent(websiteErrorEvent, err) + end +end + + +languages["lua"]["runWithoutAntivirus"] = function(func, ...) + local args = {...} + local env = getWebsiteEnvironment(false) + setfenv(func, env) + return function() + languages["lua"]["runWithErrorCatching"](func, unpack(args)) + end +end + + +languages["lua"]["run"] = function(contents, page, connection, ...) + local func, err = loadstring("sleep(0) " .. contents, page) + if err then + return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], err) + else + local args = {...} + local env = getWebsiteEnvironment(true, connection) + setfenv(func, env) + return function() + languages["lua"]["runWithErrorCatching"](func, unpack(args)) + end + end +end + + +languages["fwml"]["run"] = function(contents, page, connection, ...) + local err, data = pcall(parse, contents) + if not err then + return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], data) + end + + return function() + local currentScroll = 0 + local err, links, pageHeight = pcall(render.render, data, currentScroll) + if type(links) == "string" or not err then + term.clear() + os.queueEvent(websiteErrorEvent, links) + else + while true do + local e, scroll, x, y = os.pullEvent() + if e == "mouse_click" then + for k, v in pairs(links) do + if x >= math.min(v[1], v[2]) and x <= math.max(v[1], v[2]) and y == v[3] then + os.queueEvent(redirectEvent, v[4]) + coroutine.yield() + end + end + elseif e == "mouse_scroll" then + if currentScroll - scroll - h >= -pageHeight and currentScroll - scroll <= 0 then + currentScroll = currentScroll - scroll + clear(theme.background, theme.text) + links = render.render(data, currentScroll) + end + elseif e == "key" and scroll == keys.up or scroll == keys.down then + local scrollAmount + + if scroll == keys.up then + scrollAmount = 1 + elseif scroll == keys.down then + scrollAmount = -1 + end + + local scrollLessHeight = currentScroll + scrollAmount - h >= -pageHeight + local scrollZero = currentScroll + scrollAmount <= 0 + if scrollLessHeight and scrollZero then + currentScroll = currentScroll + scrollAmount + clear(theme.background, theme.text) + links = render.render(data, currentScroll) + end + end + end + end + end +end + + + +-- Query Bar + + +local readNewWebsiteURL = function() + local onEvent = function(text, event, key, x, y) + if event == "mouse_click" then + if y == 2 then + local index = determineClickedTab(x, y) + if index == "new" and #tabs < maxTabs then + loadTab(#tabs + 1, "firewolf") + elseif index == "close" then + closeCurrentTab() + elseif index then + switchTab(index) + end + + return {["nullifyText"] = true, ["exit"] = true} + elseif y > 2 then + return {["nullifyText"] = true, ["exit"] = true} + end + elseif event == "key" then + if key == 29 or key == 157 then + return {["nullifyText"] = true, ["exit"] = true} + end + end + end + + isMenubarOpen = true + drawMenubar() + term.setCursorPos(2, 1) + term.setTextColor(theme.text) + term.setBackgroundColor(theme.accent) + term.clearLine() + term.write(currentProtocol .. "://") + + local website = modifiedRead({ + ["onEvent"] = onEvent, + ["displayLength"] = w - 9, + ["history"] = history, + }) + + if not website then + if not tabs[currentTab].isMenubarPermanent then + isMenubarOpen = false + menubarWindow.setVisible(false) + else + isMenubarOpen = true + menubarWindow.setVisible(true) + end + + term.redirect(tabs[currentTab].win) + tabs[currentTab].win.setVisible(true) + tabs[currentTab].win.redraw() + + return + elseif website == "exit" then + error() + end + + loadTab(currentTab, website) +end + + + +-- Event Management + + +local handleKeyDown = function(event) + if event[2] == 29 or event[2] == 157 then + readNewWebsiteURL() + return true + end + + return false +end + + +local handleMouseDown = function(event) + if isMenubarOpen then + if event[4] == 1 then + readNewWebsiteURL() + return true + elseif event[4] == 2 then + local index = determineClickedTab(event[3], event[4]) + if index == "new" and #tabs < maxTabs then + loadTab(#tabs + 1, "firewolf") + elseif index == "close" then + closeCurrentTab() + elseif index then + switchTab(index) + end + + return true + end + end + + return false +end + + +local handleEvents = function() + loadTab(1, "firewolf") + currentTab = 1 + + while true do + drawMenubar() + local event = {os.pullEventRaw()} + drawMenubar() + + local cancelEvent = false + if event[1] == "terminate" then + break + elseif event[1] == "key" then + cancelEvent = handleKeyDown(event) + elseif event[1] == "mouse_click" then + cancelEvent = handleMouseDown(event) + elseif event[1] == websiteErrorEvent then + cancelEvent = true + + loadTab(currentTab, tabs[currentTab].url, function() + builtInSites["crash"](event[2]) + end) + elseif event[1] == redirectEvent then + cancelEvent = true + + if (event[2]:match("^rdnt://(.+)$")) then + event[2] = event[2]:match("^rdnt://(.+)$") + end + + loadTab(currentTab, event[2]) + end + + if not cancelEvent then + term.redirect(tabs[currentTab].win) + term.setCursorPos(tabs[currentTab].ox, tabs[currentTab].oy) + + coroutine.resume(tabs[currentTab].thread, unpack(event)) + + local ox, oy = term.getCursorPos() + tabs[currentTab].ox = ox + tabs[currentTab].oy = oy + end + end +end + + + +-- Main + + +local main = function() + currentProtocol = "rdnt" + currentTab = 1 + + if term.isColor() then + theme = colorTheme + enableTabBar = true + else + theme = grayscaleTheme + enableTabBar = false + end + + setupMenubar() + protocols[currentProtocol]["setup"]() + + clear(theme.background, theme.text) + handleEvents() +end + + +local handleError = function(err) + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Firewolf has crashed!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 8) + centerSplit(err, w - 4) + print("\n") + center("Please report this error to") + center("GravityScore or 1lann.") + print("") + center("Press any key to exit.") + + os.pullEvent("key") + os.queueEvent("") + os.pullEvent() +end + +local _, err = pcall(main) +term.redirect(originalTerminal) + +Modem.closeAll() + +if err and not err:lower():find("terminate") then + handleError(err) +end + + +clear(colors.black, colors.white) +center("Thanks for using Firewolf " .. version) +center("Made by GravityScore and 1lann") +print("")
\ No newline at end of file |
