3257 lines
73 KiB
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("")
|