Commander-64/programs/Firewolf.lua

3257 lines
73 KiB
Lua

-- Firewolf
-- Made by GravityScore and 1lann
-- Port to Commander 64 by Mr_Iron2
os.loadAPI("/apis/gpu.lua")
os.loadAPI("/apis/cpu.lua")
os.loadAPI("/apis/iop.lua")
-- QLink test
local modem = peripheral.find("modem")
modem.open(6464)
modem.transmit(6464, 6464, {sType = "requestStatus"})
local t = os.startTimer(3)
while true do
local e = {os.pullEvent()}
if e[1] == "modem_message" then
if e[3] == 6464 then
if type(e[5]) == "table" then
if e[5].sType then
if e[5].sContents == "offline" then
error("Q-Link has been terminated")
end
end
end
end
elseif e[1] == "timer" then
error("Q-Link is unavailable")
break
end
end
-- Variables
local version = "3.5.3"
local build = 21
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 = gpu.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)
gpu.cursPos(startX, startY)
if repl then
term.write(string.rep(repl:sub(1, 1), text:len() - scroll))
else
term.write(text:sub(scroll + 1))
end
gpu.cursPos(startX + pos - scroll, startY)
end
term.setCursorBlink(true)
draw()
while true do
local event, key, x, y, param4, param5 = iop.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
gpu.cursPos(x, y + i - 1)
gpu.bg(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 = iop.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
iop.queueEvent("key", keys.down)
else
iop.queueEvent("key", keys.up)
end
end
end
end
-- GUI
local clear = function(bg, fg)
term.setTextColor(fg)
gpu.bg(bg)
term.clear()
gpu.cursPos(1, 1)
end
local fill = function(x, y, width, height, bg)
gpu.bg(bg)
for i = y, y + height - 1 do
gpu.cursPos(x, i)
term.write(string.rep(" ", width))
end
end
local center = function(text)
local x, y = term.getCursorPos()
gpu.cursPos(math.floor(w / 2 - text:len() / 2) + (text:len() % 2 == 0 and 1 or 0), y)
term.write(text)
gpu.cursPos(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 = cpu.startTimer(httpTimeout)
while true do
local event, fetchedURL, response = iop.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)
gpu.cursPos(1, 3)
for _, line in pairs(logo) do
center(line)
end
gpu.cursPos(1, 10)
center(version)
gpu.bg(theme.background)
term.setTextColor(theme.text)
gpu.cursPos(1, 14)
center("Search using the Query Box above")
center("Visit rdnt://help for help using Firewolf.")
gpu.cursPos(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)
gpu.cursPos(1, 7)
center("Credits")
gpu.bg(theme.background)
gpu.cursPos(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)
gpu.cursPos(1, 4)
center("Help")
gpu.bg(theme.background)
gpu.cursPos(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)
gpu.cursPos(1, 7)
center("Server Software")
gpu.bg(theme.background)
gpu.cursPos(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 = iop.pullEvent()
if event == "key" and key == 57 then
fill(1, 11, w, 4, theme.background)
gpu.cursPos(1, 11)
center("Downloading...")
local err = downloadAndSave(serverURL, "/fwserver")
fill(1, 11, w, 4, theme.background)
gpu.cursPos(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)
gpu.cursPos(1, 4)
center("Update")
gpu.bg(theme.background)
if not http then
gpu.cursPos(1, 9)
center("HTTP is not enabled!")
print("")
center("Please enable it in your config")
center("file to download Firewolf updates.")
else
gpu.cursPos(1, 10)
center("Checking for updates...")
local available, err = updateAvailable()
gpu.cursPos(1, 10)
if available then
term.clearLine()
center("Update found!")
center("Press enter to download.")
while true do
local event, key = iop.pullEvent()
if event == "key" and key == keys.enter then
break
end
end
fill(1, 10, w, 2, theme.background)
gpu.cursPos(1, 10)
center("Downloading...")
local err = redownloadBrowser()
gpu.cursPos(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)
gpu.cursPos(1, 4)
center("Failed to load page!")
gpu.bg(theme.background)
gpu.cursPos(1, 9)
center(err)
print("")
center("Please try again.")
end
builtInSites["noresults"] = function()
fill(1, 3, w, 3, theme.subtle)
gpu.cursPos(1, 4)
center("No results!")
gpu.bg(theme.background)
gpu.cursPos(1, 9)
center("Your search didn't return")
center("any results!")
iop.pullEvent("key")
iop.queueEvent("")
iop.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
gpu.cursPos(5, (i - scroll) + startY)
term.write(currentProtocol .. "://" .. results[i])
end
end
end
draw()
while true do
local event, but, x, y = iop.pullEvent()
if event == "mouse_click" and y >= startY and y <= startY + height then
local item = results[y - startY + scroll]
if item then
iop.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
iop.queueEvent("key", keys.down)
else
iop.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
gpu.cursPos(3, (i - scroll) + startY)
term.write("> " .. currentProtocol .. "://" .. results[i])
else
gpu.cursPos(5, (i - scroll) + startY)
term.write(currentProtocol .. "://" .. results[i])
end
end
end
end
draw()
while true do
local event, but, x, y = iop.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
iop.queueEvent(redirectEvent, item)
coroutine.yield()
end
end
draw()
elseif event == "mouse_scroll" then
if but > 0 then
iop.queueEvent("key", keys.down)
else
iop.queueEvent("key", keys.up)
end
end
end
end
builtInSites["search"] = function(results)
clear(theme.background, theme.text)
fill(1, 3, w, 3, theme.subtle)
gpu.cursPos(1, 4)
center(#results .. " Search " .. (#results == 1 and "Result" or "Results"))
gpu.bg(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)
gpu.cursPos(1, 4)
center("The website crashed!")
gpu.bg(theme.background)
gpu.cursPos(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
gpu.redirect(menubarWindow)
menubarWindow.setVisible(true)
fill(1, 1, w, 1, theme.accent)
term.setTextColor(theme.text)
gpu.bg(theme.accent)
gpu.cursPos(2, 1)
if currentWebsiteURL:match("^[^%?]+") then
term.write(currentProtocol .. "://" .. currentWebsiteURL:match("^[^%?]+"))
else
term.write(currentProtocol .. "://" ..currentWebsiteURL)
end
gpu.cursPos(w - 5, 1)
term.write("[===]")
if enableTabBar then
fill(1, 2, w, 1, theme.subtle)
gpu.cursPos(1, 2)
for i, tab in pairs(tabs) do
gpu.bg(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)
gpu.bg(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 iop.mCloseAll
if not Modem.exists then
return false
end
for side, modem in pairs(Modem.modems) do
iop.mCloseAll
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 type(msg) ~= "string" then return false end
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 = cpu.startTimer(searchResultTimeout)
while true do
local event, id, channel, protocol, message, dist = iop.pullEventRaw()
if event == "modem_message" then
if channel == publicResponseChannel and type(message) == "string" 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 = cpu.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 = cpu.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 = cpu.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 = iop.pullEventRaw()
if event == "modem_message" then
local fullMatch = responseMatch .. tostring(dist) .. header.responseMatchC
if channel == serverChannel and type(message) == "string" 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 = cpu.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 = iop.pullEventRaw()
if event == "modem_message" and channel == connection.channel and type(message) == "string" 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 = cpu.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 = iop.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 = cpu.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)
gpu.bg(theme.accent)
gpu.cursPos(w - 5, 1)
term.write("[= ]")
local timer = cpu.startTimer(animationInterval)
while true do
local event, timerID = iop.pullEvent()
if event == "timer" and timerID == timer then
term.setTextColor(theme.text)
gpu.bg(theme.accent)
state = state + 1
gpu.cursPos(w - 5, 1)
term.write("[ ]")
gpu.cursPos(w - 2 - math.abs(state), 1)
term.write("=")
if state == 2 then
state = -2
end
timer = cpu.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
iop.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
gpu.redirect(originalTerminal)
clear(theme.background, theme.text)
drawMenubar()
gpu.redirect(tabs[currentTab].win)
gpu.cursPos(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 = iop.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
gpu.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 = {iop.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
env["shell"]["openTab"] = 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
iop.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
gpu.bg(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
gpu.bg(colors[sColor])
for i = 0, height - 1 do
gpu.cursPos(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)
gpu.cursPos(x, y)
end
render["alignations"]["left"] = function(text, length, _, offset)
local x, y = term.getCursorPos()
if length then
gpu.cursPos(1 + offset, render.variables.scroll)
term.write(text)
return 1 + offset
else
gpu.cursPos(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
gpu.cursPos((width - length + 1) + offset, render.variables.scroll)
term.write(text)
return (width - length + 1) + offset
else
gpu.cursPos(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
gpu.cursPos(math.ceil(center - length / 2) + offset, render.variables.scroll)
term.write(text)
return math.ceil(center - length / 2) + offset
else
gpu.cursPos(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
iop.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()
iop.queueEvent(websiteErrorEvent, links)
else
while true do
local e, scroll, x, y = iop.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
iop.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()
gpu.cursPos(2, 1)
term.setTextColor(theme.text)
gpu.bg(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
gpu.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 = {iop.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
gpu.redirect(tabs[currentTab].win)
gpu.cursPos(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)
gpu.cursPos(1, 4)
center("Firewolf has crashed!")
gpu.bg(theme.background)
gpu.cursPos(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.")
iop.pullEvent("key")
iop.queueEvent("")
iop.pullEvent()
end
local _, err = pcall(main)
gpu.redirect(originalTerminal)
iop.mCloseAll
if err and not err:lower():find("terminate") then
handleError(err)
end
gpu.clrBg("black")
gpu.setTxt("white"
center("Thanks for using Firewolf " .. version)
center("Made by GravityScore and 1lann")
print("")